Skip to content

Commit a5f487c

Browse files
committed
feat: introduce planner view and enhance navigation across the application
- Added a new PlannerView component for structured planning within threads, allowing users to create, refine, and execute plans. - Updated the command palette and workspace sidebar to include navigation to the new planner view. - Enhanced the agent panel to persist agent mode in local storage, improving user experience. - Refined global CSS styles for better visual consistency and updated various UI components to support the new planner functionality.
1 parent 44a778c commit a5f487c

File tree

11 files changed

+639
-30
lines changed

11 files changed

+639
-30
lines changed

app/globals.css

Lines changed: 91 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3218,14 +3218,14 @@ p {
32183218

32193219
.shell-sidebar--expanded {
32203220
background: var(--shell-panel-bg);
3221-
border-radius: 20px 0 0 20px;
3221+
border-radius: 12px 0 0 12px;
32223222
box-shadow: var(--shell-panel-shadow);
32233223
}
32243224

32253225
.shell-sidebar--collapsed {
32263226
border: 1px solid var(--shell-panel-border);
32273227
border-right: none;
3228-
border-radius: 999px 0 0 999px;
3228+
border-radius: 12px 0 0 12px;
32293229
background: var(--shell-panel-bg);
32303230
box-shadow: none;
32313231
}
@@ -3362,26 +3362,104 @@ p {
33623362
}
33633363

33643364
.codex-sidebar-hero {
3365+
position: relative;
33653366
display: flex;
33663367
align-items: center;
33673368
justify-content: space-between;
3368-
gap: 0.5rem;
3369-
margin-bottom: 0.25rem;
3370-
padding: 0.65rem 0.75rem;
3371-
border: 1px solid var(--border);
3372-
border-radius: 8px;
3373-
background: var(--bg-elevated);
3369+
gap: 0.75rem;
3370+
margin-bottom: 0.45rem;
3371+
padding: 0.85rem 0.9rem;
3372+
border: 1px solid color-mix(in srgb, var(--border) 88%, transparent);
3373+
border-radius: 14px;
3374+
background: linear-gradient(
3375+
180deg,
3376+
color-mix(in srgb, var(--bg-elevated) 96%, white 4%),
3377+
color-mix(in srgb, var(--bg-elevated) 92%, var(--bg) 8%)
3378+
);
3379+
box-shadow:
3380+
inset 0 1px 0 color-mix(in srgb, white 8%, transparent),
3381+
0 10px 30px color-mix(in srgb, black 18%, transparent);
3382+
overflow: hidden;
3383+
}
3384+
3385+
.codex-sidebar-hero::before {
3386+
content: '';
3387+
position: absolute;
3388+
inset: 0;
3389+
background:
3390+
radial-gradient(
3391+
circle at top left,
3392+
color-mix(in srgb, var(--brand) 20%, transparent) 0,
3393+
transparent 42%
3394+
),
3395+
linear-gradient(135deg, color-mix(in srgb, white 3%, transparent), transparent 45%);
3396+
pointer-events: none;
33743397
}
33753398

33763399
.codex-sidebar-hero__icon {
3400+
position: relative;
3401+
z-index: 1;
33773402
display: flex;
3378-
width: 28px;
3379-
height: 28px;
3403+
width: 42px;
3404+
height: 42px;
3405+
flex-shrink: 0;
33803406
align-items: center;
33813407
justify-content: center;
3382-
border-radius: 8px;
3383-
background: var(--bg-subtle);
3384-
color: var(--text-secondary);
3408+
border-radius: 14px;
3409+
background: color-mix(in srgb, var(--bg-subtle) 80%, transparent);
3410+
color: var(--text-primary);
3411+
box-shadow:
3412+
inset 0 1px 0 color-mix(in srgb, white 10%, transparent),
3413+
0 0 0 1px color-mix(in srgb, var(--border) 70%, transparent);
3414+
}
3415+
3416+
.codex-sidebar-hero__icon-ring {
3417+
display: inline-flex;
3418+
align-items: center;
3419+
justify-content: center;
3420+
width: 30px;
3421+
height: 30px;
3422+
border-radius: 999px;
3423+
background: radial-gradient(
3424+
circle,
3425+
color-mix(in srgb, var(--brand) 22%, transparent),
3426+
transparent 72%
3427+
);
3428+
}
3429+
3430+
.codex-sidebar-hero__logo {
3431+
color: color-mix(in srgb, var(--text-primary) 88%, var(--brand) 12%);
3432+
filter: drop-shadow(0 1px 6px color-mix(in srgb, var(--brand) 24%, transparent));
3433+
}
3434+
3435+
.codex-sidebar-hero__text {
3436+
position: relative;
3437+
z-index: 1;
3438+
display: flex;
3439+
min-width: 0;
3440+
flex-direction: column;
3441+
gap: 0.18rem;
3442+
}
3443+
3444+
.codex-sidebar-hero__eyebrow {
3445+
font-size: 10px;
3446+
line-height: 1;
3447+
font-weight: 600;
3448+
letter-spacing: 0.18em;
3449+
text-transform: uppercase;
3450+
color: color-mix(in srgb, var(--text-disabled) 88%, var(--brand) 12%);
3451+
}
3452+
3453+
.codex-sidebar-hero__title {
3454+
display: block;
3455+
font-family: 'Iowan Old Style', 'Palatino Linotype', 'Book Antiqua', Georgia, serif;
3456+
font-size: 19px;
3457+
line-height: 1.05;
3458+
font-weight: 600;
3459+
letter-spacing: 0.03em;
3460+
color: var(--text-primary);
3461+
text-wrap: balance;
3462+
text-shadow: 0 1px 0 color-mix(in srgb, white 8%, transparent);
33853463
}
33863464

33873465
.codex-sidebar-hero__action {

app/page.tsx

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -197,21 +197,51 @@ export default function EditorLayout() {
197197

198198
// ─── Dev server detection ─────────────────────────────
199199
useEffect(() => {
200-
const checkDevServer = async () => {
200+
let cancelled = false
201+
let timeoutId: ReturnType<typeof setTimeout> | null = null
202+
let failureCount = 0
203+
204+
const schedule = (ms: number) => {
205+
if (cancelled) return
206+
if (timeoutId) clearTimeout(timeoutId)
207+
timeoutId = setTimeout(runCheck, ms)
208+
}
209+
210+
const runCheck = async () => {
211+
if (cancelled) return
212+
if (typeof document !== 'undefined' && document.visibilityState === 'hidden') {
213+
schedule(30000)
214+
return
215+
}
216+
201217
try {
202218
const controller = new AbortController()
203-
const timeoutId = setTimeout(() => controller.abort(), 2000)
204-
await fetch('http://localhost:3000', { mode: 'no-cors', signal: controller.signal })
205-
clearTimeout(timeoutId)
219+
const abortId = setTimeout(() => controller.abort(), 1500)
220+
await fetch('http://localhost:3000', {
221+
method: 'HEAD',
222+
mode: 'no-cors',
223+
cache: 'no-store',
224+
signal: controller.signal,
225+
})
226+
clearTimeout(abortId)
227+
failureCount = 0
206228
setDevServerReady(true)
229+
schedule(30000)
207230
} catch {
231+
failureCount += 1
208232
setDevServerReady(false)
233+
// Back off aggressively when the dev server is unavailable so we don't
234+
// spam the console/network with pointless localhost retries.
235+
const delay = failureCount >= 3 ? 120000 : 30000
236+
schedule(delay)
209237
}
210238
}
211239

212-
checkDevServer()
213-
const interval = setInterval(checkDevServer, 5000)
214-
return () => clearInterval(interval)
240+
runCheck()
241+
return () => {
242+
cancelled = true
243+
if (timeoutId) clearTimeout(timeoutId)
244+
}
215245
}, [])
216246

217247
// ─── Save file handler ─────────────────────────────────
@@ -887,6 +917,9 @@ export default function EditorLayout() {
887917
case 'view-preview':
888918
setView('preview')
889919
break
920+
case 'view-planner':
921+
setView('planner')
922+
break
890923
case 'view-git':
891924
setView('git')
892925
break

components/agent-panel.tsx

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ import {
6565

6666
// ChatMessage type imported from @/lib/chat-stream
6767

68+
const AGENT_MODE_STORAGE_PREFIX = 'code-editor:agent-mode:'
69+
6870
function AgentConnectPrompt() {
6971
const { status, error, connect } = useGateway()
7072
const isMobileDevice = typeof window !== 'undefined' && window.innerWidth <= 768
@@ -425,7 +427,17 @@ export function AgentPanel({ onClose }: { onClose?: () => void } = {}) {
425427
current: '',
426428
available: [],
427429
})
428-
const [agentMode, setAgentMode] = useState<AgentMode>('ask')
430+
const [agentMode, setAgentMode] = useState<AgentMode>(() => {
431+
if (typeof window === 'undefined') return 'ask'
432+
try {
433+
return (
434+
(localStorage.getItem(`${AGENT_MODE_STORAGE_PREFIX}${activeThreadId}`) as AgentMode) ||
435+
'ask'
436+
)
437+
} catch {
438+
return 'ask'
439+
}
440+
})
429441
const [contextTokens, setContextTokens] = useState(0)
430442
const [activeSuggestionIdx, setActiveSuggestionIdx] = useState(-1)
431443
const [confirmClear, setConfirmClear] = useState(false)
@@ -473,7 +485,21 @@ export function AgentPanel({ onClose }: { onClose?: () => void } = {}) {
473485
useEffect(() => {
474486
setMessages(loadMessagesForThread(storageKey))
475487
sessionInitRef.current = false
488+
try {
489+
const storedMode = localStorage.getItem(
490+
`${AGENT_MODE_STORAGE_PREFIX}${activeThreadId}`,
491+
) as AgentMode | null
492+
setAgentMode(storedMode ?? 'ask')
493+
} catch {
494+
setAgentMode('ask')
495+
}
476496
}, [activeThreadId, storageKey])
497+
498+
useEffect(() => {
499+
try {
500+
localStorage.setItem(`${AGENT_MODE_STORAGE_PREFIX}${activeThreadId}`, agentMode)
501+
} catch {}
502+
}, [activeThreadId, agentMode])
477503
useEffect(() => {
478504
sendingRef.current = sending
479505
}, [sending])
@@ -895,6 +921,12 @@ export function AgentPanel({ onClose }: { onClose?: () => void } = {}) {
895921
return on('focus-agent-input', () => inputRef.current?.focus())
896922
}, [])
897923

924+
useEffect(() => {
925+
return on('agent-mode-change', (detail) => {
926+
setAgentMode(detail.mode)
927+
})
928+
}, [])
929+
898930
// ─── Build per-message context ────────────────────────────────
899931
const buildContext = useCallback(() => {
900932
const file = activeFile ? getFile(activeFile) : undefined
@@ -981,15 +1013,18 @@ export function AgentPanel({ onClose }: { onClose?: () => void } = {}) {
9811013
// ─── Message helpers ──────────────────────────────────────────
9821014
// parsePlanSteps moved to components/chat/message-list.tsx
9831015

984-
// Persist messages to localStorage for current thread and notify sidebar to refresh list
1016+
// Persist messages to localStorage for current thread and notify sidebar to refresh list.
1017+
// If the thread is empty, remove it entirely so delete/clear actually deletes the chat.
9851018
useEffect(() => {
986-
if (messages.length > 0) {
987-
try {
1019+
try {
1020+
if (messages.length > 0) {
9881021
localStorage.setItem(storageKey, JSON.stringify(messages.slice(-50)))
989-
emit('threads-updated')
990-
} catch {
991-
// Ignore storage errors
1022+
} else {
1023+
localStorage.removeItem(storageKey)
9921024
}
1025+
emit('threads-updated')
1026+
} catch {
1027+
// Ignore storage errors
9931028
}
9941029
}, [messages, storageKey])
9951030

@@ -1301,6 +1336,7 @@ export function AgentPanel({ onClose }: { onClose?: () => void } = {}) {
13011336
if (text === '/plan') {
13021337
setAgentMode('plan')
13031338
setInput('')
1339+
window.dispatchEvent(new CustomEvent('view-change', { detail: { view: 'planner' } }))
13041340
return
13051341
}
13061342
if (text === '/clear') {

components/command-palette.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type CommandId =
2828
// Navigation
2929
| 'view-editor'
3030
| 'view-preview'
31+
| 'view-planner'
3132
| 'view-git'
3233
| 'view-skills'
3334
| 'view-prompts'
@@ -212,6 +213,14 @@ const COMMANDS: CommandItem[] = [
212213
icon: 'lucide:eye',
213214
group: 'navigate',
214215
},
216+
{
217+
id: 'view-planner',
218+
label: 'Go to Planner',
219+
hint: 'Open the dedicated planning surface',
220+
keywords: ['plan', 'planner', 'cursor', 'steps', 'approval'],
221+
icon: 'lucide:list-checks',
222+
group: 'navigate',
223+
},
215224

216225
{
217226
id: 'view-git',

components/view-router.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ const SplitPreviewChat = dynamic(
4040
() => import('@/components/split-preview-chat').then((m) => m.SplitPreviewChat),
4141
{ ssr: false },
4242
)
43+
const PlannerView = dynamic(
44+
() => import('@/components/views/planner-view').then((m) => m.PlannerView),
45+
{ ssr: false },
46+
)
4347
const WorkshopView = dynamic(
4448
() => import('@/components/views/workshop-view').then((m) => m.WorkshopView),
4549
{ ssr: false },
@@ -48,6 +52,7 @@ const VIEW_ICONS: Record<string, { label: string }> = {
4852
chat: { label: 'Chat' },
4953
editor: { label: 'Editor' },
5054
preview: { label: 'Preview' },
55+
planner: { label: 'Planner' },
5156
git: { label: 'Git' },
5257
kanban: { label: 'Kanban' },
5358
skills: { label: 'Skills' },
@@ -123,6 +128,7 @@ export function ViewRouter() {
123128
<>
124129
{activeView === 'chat' && <AgentPanel />}
125130
{(activeView === 'editor' || activeView === 'preview') && <EditorView />}
131+
{activeView === 'planner' && <PlannerView />}
126132
{activeView === 'git' && <GitView />}
127133
{activeView === 'kanban' && <KanbanView />}
128134
{activeView === 'skills' && <SkillsView />}

0 commit comments

Comments
 (0)