From 8a075795f3058b2cd334000aa856751c3946ef03 Mon Sep 17 00:00:00 2001 From: Julius Olsson Date: Fri, 24 Apr 2026 15:30:36 +0200 Subject: [PATCH 1/2] fix(workspace): stabilize new tab picker --- src/renderer/src/app/App.tsx | 31 +++++++++---- .../path-picker/ui/PathPickerModal.tsx | 46 +++++++++++-------- 2 files changed, 50 insertions(+), 27 deletions(-) diff --git a/src/renderer/src/app/App.tsx b/src/renderer/src/app/App.tsx index 1158fcc38..d0d65e19f 100644 --- a/src/renderer/src/app/App.tsx +++ b/src/renderer/src/app/App.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect } from 'react' +import { useCallback, useEffect, useRef } from 'react' import { CommandPalette } from '@renderer/features/command-palette/ui/CommandPalette' import { DebugPanel } from '@renderer/features/debug/ui/DebugPanel' @@ -94,20 +94,35 @@ export default function App() { }, [settings]) const workspace = useWorkspace(dangerousAgentsEnabled, useProxyStreaming) + const pathPickerDefaultedRef = useRef(false) - // Pre-fill the path input with a sensible default when the modal - // opens: the cwd of the most recently-used session, or main's - // default cwd if no sessions exist yet. Cached so opening the modal - // doesn't hit IPC every time. + // Pre-fill the path input once per modal open. Do not keep syncing + // while the modal is visible: newTab/resume mutates workspace + // sessions before the modal closes, and re-syncing here resets the + // picker mid-submit. Also preserve explicit defaults from the + // resume shortcut. useEffect(() => { - if (!pathPickerOpen) return + if (!pathPickerOpen) { + pathPickerDefaultedRef.current = false + return + } + if (pathPickerDefaultedRef.current) return + pathPickerDefaultedRef.current = true + if (pathPickerDefault) return + + let cancelled = false const mostRecent = Object.values(workspace.state.sessions).pop() if (mostRecent?.cwd) { setPathPickerDefault(mostRecent.cwd) return } - void window.api.defaultCwd().then(setPathPickerDefault) - }, [pathPickerOpen, workspace.state.sessions]) + void window.api.defaultCwd().then(cwd => { + if (!cancelled) setPathPickerDefault(cwd) + }) + return () => { + cancelled = true + } + }, [pathPickerDefault, pathPickerOpen, setPathPickerDefault, workspace.state.sessions]) // New tab flow: show the path modal. On accept it calls workspace.newTab // with the expanded absolute path and closes the modal. diff --git a/src/renderer/src/features/path-picker/ui/PathPickerModal.tsx b/src/renderer/src/features/path-picker/ui/PathPickerModal.tsx index 18b6525a6..bc1e03bf5 100644 --- a/src/renderer/src/features/path-picker/ui/PathPickerModal.tsx +++ b/src/renderer/src/features/path-picker/ui/PathPickerModal.tsx @@ -123,35 +123,43 @@ export function PathPickerModal({ if (busy) return setBusy(true) setError(null) - const result = await window.api.expandCwd(value) - if (!result.ok) { - setError(result.error) + try { + const result = await window.api.expandCwd(value) + if (!result.ok) { + setError(result.error) + return + } + await onAccept(result.path, provider) + } catch (err) { + setError(err instanceof Error ? err.message : String(err)) + } finally { setBusy(false) - return } - await onAccept(result.path, provider) - setBusy(false) } const resume = async (sessionId: string) => { if (busy) return setBusy(true) setError(null) - // Use the already-validated path if we have one; fall back to - // re-validating just in case the user typed something new since - // the last debounce. - let path = resolvedPath - if (!path) { - const result = await window.api.expandCwd(value) - if (!result.ok) { - setError(result.error) - setBusy(false) - return + try { + // Use the already-validated path if we have one; fall back to + // re-validating just in case the user typed something new since + // the last debounce. + let path = resolvedPath + if (!path) { + const result = await window.api.expandCwd(value) + if (!result.ok) { + setError(result.error) + return + } + path = result.path } - path = result.path + await onResume(path, sessionId, provider) + } catch (err) { + setError(err instanceof Error ? err.message : String(err)) + } finally { + setBusy(false) } - await onResume(path, sessionId, provider) - setBusy(false) } return ( From 5d7fee94ee2b62591cbf72f904fb613d75f79230 Mon Sep 17 00:00:00 2001 From: Julius Olsson Date: Fri, 24 Apr 2026 18:51:25 +0200 Subject: [PATCH 2/2] Add workflow command pickers --- .../src/features/command-palette/registry.ts | 2 + .../src/features/command-palette/types.ts | 2 + .../command-palette/ui/CommandPalette.tsx | 189 +++++++++++++++++- .../commands/promptTemplateCommands.ts | 49 +++++ .../features/prompt-templates/templates.ts | 159 +++++++++++++++ .../workspace/commands/paneCommands.ts | 7 + .../src/workspace/hook/actions/pane.ts | 62 +++++- src/renderer/src/workspace/hook/index.ts | 1 + 8 files changed, 463 insertions(+), 8 deletions(-) create mode 100644 src/renderer/src/features/prompt-templates/commands/promptTemplateCommands.ts create mode 100644 src/renderer/src/features/prompt-templates/templates.ts diff --git a/src/renderer/src/features/command-palette/registry.ts b/src/renderer/src/features/command-palette/registry.ts index a27371cdb..5de07f01c 100644 --- a/src/renderer/src/features/command-palette/registry.ts +++ b/src/renderer/src/features/command-palette/registry.ts @@ -7,6 +7,7 @@ import { spotlightCommands } from '@renderer/features/spotlight/commands/spotlig import { tileTabsCommands } from '@renderer/features/tile-tabs/commands/tileTabsCommands' import { readerCommands } from '@renderer/features/reader/commands/readerCommands' import { copyAssistantCommands } from '@renderer/features/copy-assistant/commands/copyAssistantCommands' +import { promptTemplateCommands } from '@renderer/features/prompt-templates/commands/promptTemplateCommands' import type { CommandContext, CommandDef, ResolvedCommand } from '@renderer/features/command-palette/types' const commandDefs: CommandDef[] = [ @@ -19,6 +20,7 @@ const commandDefs: CommandDef[] = [ ...tileTabsCommands, ...settingsCommands, ...copyAssistantCommands, + ...promptTemplateCommands, ] export function buildCommandRegistry(ctx: CommandContext): ResolvedCommand[] { diff --git a/src/renderer/src/features/command-palette/types.ts b/src/renderer/src/features/command-palette/types.ts index 4ee8cc31d..d6f7bda1c 100644 --- a/src/renderer/src/features/command-palette/types.ts +++ b/src/renderer/src/features/command-palette/types.ts @@ -25,6 +25,8 @@ export type CommandContext = { setDangerousAgentsEnabled: (enabled: boolean) => void enterResumeMode: () => void enterBuriedMode: () => void + enterKillBuriedMode: () => void + enterPromptTemplateMode: () => void closePalette: () => void } flags: { diff --git a/src/renderer/src/features/command-palette/ui/CommandPalette.tsx b/src/renderer/src/features/command-palette/ui/CommandPalette.tsx index f93c1ae62..c93235e0e 100644 --- a/src/renderer/src/features/command-palette/ui/CommandPalette.tsx +++ b/src/renderer/src/features/command-palette/ui/CommandPalette.tsx @@ -2,6 +2,11 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { buildCommandRegistry } from '@renderer/features/command-palette/registry' import type { CommandContext, ResolvedCommand } from '@renderer/features/command-palette/types' +import { + allPromptTemplates, + loadCustomPromptTemplates, + type PromptTemplate, +} from '@renderer/features/prompt-templates/templates' import type { Workspace } from '@renderer/workspace/workspaceStore' // CommandPalette — VS Code-style ⌘⇧P command menu. @@ -29,7 +34,7 @@ type BuriedPaneInfo = { buriedAt: number } -type PaletteMode = 'commands' | 'resume' | 'buried' +type PaletteMode = 'commands' | 'resume' | 'buried' | 'kill-buried' | 'prompt-template' type Props = { open: boolean @@ -101,6 +106,7 @@ export function CommandPalette({ const [mode, setMode] = useState('commands') const [sessions, setSessions] = useState([]) const [sessionsLoading, setSessionsLoading] = useState(false) + const [customPromptTemplates, setCustomPromptTemplates] = useState([]) const inputRef = useRef(null) const listRef = useRef(null) @@ -154,6 +160,19 @@ export function CommandPalette({ setSelectedIndex(0) }, []) + const enterKillBuriedMode = useCallback(() => { + setMode('kill-buried') + setQuery('') + setSelectedIndex(0) + }, []) + + const enterPromptTemplateMode = useCallback(() => { + setCustomPromptTemplates(loadCustomPromptTemplates()) + setMode('prompt-template') + setQuery('') + setSelectedIndex(0) + }, []) + const commandContext = useMemo( () => ({ workspace, @@ -175,6 +194,8 @@ export function CommandPalette({ setDangerousAgentsEnabled, enterResumeMode, enterBuriedMode, + enterKillBuriedMode, + enterPromptTemplateMode, closePalette: onClose, }, flags: { @@ -206,6 +227,8 @@ export function CommandPalette({ setDangerousAgentsEnabled, enterResumeMode, enterBuriedMode, + enterKillBuriedMode, + enterPromptTemplateMode, onClose, customRenderingEnabled, dangerousAgentsEnabled, @@ -222,6 +245,11 @@ export function CommandPalette({ [commandContext], ) + const promptTemplates = useMemo( + () => allPromptTemplates(customPromptTemplates), + [customPromptTemplates], + ) + const filtered = useMemo(() => { if (mode === 'resume') { if (!query.trim()) return sessions @@ -232,7 +260,7 @@ export function CommandPalette({ fuzzyMatch(s.gitBranch ?? '', query), ) } - if (mode === 'buried') { + if (mode === 'buried' || mode === 'kill-buried') { if (!query.trim()) return buried return buried.filter( item => @@ -241,13 +269,22 @@ export function CommandPalette({ fuzzyMatch(item.note ?? '', query), ) } + if (mode === 'prompt-template') { + if (!query.trim()) return promptTemplates + return promptTemplates.filter( + template => + fuzzyMatch(template.title, query) || + fuzzyMatch(template.description, query) || + fuzzyMatch(template.body, query), + ) + } if (!query.trim()) return commands return commands.filter( command => fuzzyMatch(command.title, query) || command.keywords.some(keyword => fuzzyMatch(keyword, query)), ) - }, [mode, buried, commands, sessions, query]) + }, [mode, buried, commands, sessions, promptTemplates, query]) useEffect(() => { if (open) { @@ -256,6 +293,7 @@ export function CommandPalette({ setMode('commands') setSessions([]) setSessionsLoading(false) + setCustomPromptTemplates(loadCustomPromptTemplates()) requestAnimationFrame(() => inputRef.current?.focus()) } }, [open]) @@ -272,7 +310,12 @@ export function CommandPalette({ const executeCommand = useCallback( (command: ResolvedCommand) => { - if (command.id === 'resume-session' || command.id === 'revive-pane') { + if ( + command.id === 'resume-session' || + command.id === 'revive-pane' || + command.id === 'kill-buried-pane' || + command.id === 'prompt-template' + ) { void command.run(commandContext) return } @@ -302,12 +345,46 @@ export function CommandPalette({ [onClose, workspace], ) + const executeKillBuried = useCallback( + (item: BuriedPaneInfo) => { + const wasLastBuriedPane = workspace.state.buried.length <= 1 + void workspace.killBuried(item.id).then(() => { + if (wasLastBuriedPane) onClose() + else setSelectedIndex(i => Math.max(0, Math.min(i, workspace.state.buried.length - 2))) + }) + }, + [onClose, workspace], + ) + + const executePromptTemplate = useCallback( + (template: PromptTemplate) => { + const tab = workspace.activeTab + if (!tab) return + const sessionId = tab.focusedSessionId + + // Template insertion deliberately stops at the draft boundary. + // The user's next action is still visible and editable in the + // composer; nothing is sent to Claude/Codex until they press + // Enter themselves. This mirrors rewind-to-prompt's "prefill, + // don't replay" contract. + workspace.setDraftInput(sessionId, template.body) + workspace.showPaneToast(sessionId, `Inserted template: ${template.title}`) + onClose() + }, + [onClose, workspace], + ) + const onKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === 'Escape') { e.preventDefault() e.stopPropagation() - if (mode === 'resume' || mode === 'buried') { + if ( + mode === 'resume' || + mode === 'buried' || + mode === 'kill-buried' || + mode === 'prompt-template' + ) { setMode('commands') setQuery('') setSelectedIndex(0) @@ -334,13 +411,29 @@ export function CommandPalette({ } else if (mode === 'buried') { const item = filtered[selectedIndex] as BuriedPaneInfo | undefined if (item) executeBuried(item) + } else if (mode === 'kill-buried') { + const item = filtered[selectedIndex] as BuriedPaneInfo | undefined + if (item) executeKillBuried(item) + } else if (mode === 'prompt-template') { + const template = filtered[selectedIndex] as PromptTemplate | undefined + if (template) executePromptTemplate(template) } else { const command = filtered[selectedIndex] as ResolvedCommand | undefined if (command) executeCommand(command) } } }, - [mode, filtered, selectedIndex, executeBuried, executeCommand, executeResume, onClose], + [ + mode, + filtered, + selectedIndex, + executeBuried, + executeCommand, + executeKillBuried, + executePromptTemplate, + executeResume, + onClose, + ], ) if (!open) return null @@ -373,6 +466,16 @@ export function CommandPalette({ revive › )} + {mode === 'kill-buried' && ( + + kill buried › + + )} + {mode === 'prompt-template' && ( + + template › + + )} )) ))} + + {mode === 'kill-buried' && + (filtered.length === 0 ? ( +
+ No buried panes +
+ ) : ( + (filtered as BuriedPaneInfo[]).map((item, i) => ( +
setSelectedIndex(i)} + onClick={() => executeKillBuried(item)} + > +
{item.label}
+ {item.note && ( +
+ {item.note} +
+ )} +
+ {item.description} +
+
+ )) + ))} + + {mode === 'prompt-template' && + (filtered.length === 0 ? ( +
+ No matching templates +
+ ) : ( + (filtered as PromptTemplate[]).map((template, i) => ( +
setSelectedIndex(i)} + onClick={() => executePromptTemplate(template)} + > +
+
{template.title}
+ + {template.scope} + +
+
+ {template.description} +
+
+ )) + ))} diff --git a/src/renderer/src/features/prompt-templates/commands/promptTemplateCommands.ts b/src/renderer/src/features/prompt-templates/commands/promptTemplateCommands.ts new file mode 100644 index 000000000..dff324746 --- /dev/null +++ b/src/renderer/src/features/prompt-templates/commands/promptTemplateCommands.ts @@ -0,0 +1,49 @@ +import type { CommandDef } from '@renderer/features/command-palette/types' +import { saveCustomPromptTemplate } from '@renderer/features/prompt-templates/templates' +import type { Workspace } from '@renderer/workspace/workspaceStore' + +function focusedAgentSessionId(workspace: Workspace): string | null { + const tab = workspace.activeTab + if (!tab) return null + const kind = workspace.state.sessions[tab.focusedSessionId]?.kind ?? 'claude' + return kind === 'terminal' ? null : tab.focusedSessionId +} + +export const promptTemplateCommands: CommandDef[] = [ + { + id: 'prompt-template', + title: 'Prompt Template…', + keywords: ['prompt', 'template', 'snippet', 'insert', 'draft'], + when: ({ workspace }) => focusedAgentSessionId(workspace) !== null, + run: ({ ui }) => ui.enterPromptTemplateMode(), + }, + { + id: 'save-composer-as-prompt-template', + title: 'Save Composer as Prompt Template…', + keywords: ['prompt', 'template', 'save', 'composer', 'custom', 'snippet'], + when: ({ workspace }) => { + const sessionId = focusedAgentSessionId(workspace) + if (!sessionId) return false + return workspace.getRuntime(sessionId).draftInput.trim().length > 0 + }, + run: ({ workspace, ui }) => { + const sessionId = focusedAgentSessionId(workspace) + if (!sessionId) return + + const draft = workspace.getRuntime(sessionId).draftInput.trim() + if (!draft) return + + // This is intentionally the smallest possible custom-template UI + // for the first pass. The composer is already the full-featured + // multiline editor; this command treats its current contents as + // the body and only asks for a title. A later Manage Templates + // modal can edit/delete records without changing the storage + // shape introduced here. + const title = window.prompt('Template name') + if (!title?.trim()) return + const template = saveCustomPromptTemplate(title, draft) + ui.closePalette() + workspace.showPaneToast(sessionId, `Saved prompt template: ${template.title}`) + }, + }, +] diff --git a/src/renderer/src/features/prompt-templates/templates.ts b/src/renderer/src/features/prompt-templates/templates.ts new file mode 100644 index 000000000..5ec6438cb --- /dev/null +++ b/src/renderer/src/features/prompt-templates/templates.ts @@ -0,0 +1,159 @@ +export type PromptTemplate = { + id: string + title: string + description: string + body: string + scope: 'builtin' | 'custom' + createdAt?: number + updatedAt?: number +} + +const CUSTOM_TEMPLATES_KEY = 'cc-shell.promptTemplates.v1' + +export const builtinPromptTemplates: PromptTemplate[] = [ + { + id: 'builtin:investigate-rendering', + title: 'Deep Bug Investigation', + description: 'Read logs and code first, identify root causes before patching.', + scope: 'builtin', + body: [ + 'Please investigate this deeply before patching.', + '', + 'Symptoms:', + '- ', + '', + 'What I want:', + '- Read the relevant logs and code paths closely.', + '- Check timestamps, duplicate events, ordering, and ownership/state transitions.', + '- Identify likely root causes and explain the evidence.', + '- Only patch once the failure mode is concrete.', + '', + 'Constraints:', + '- Keep changes scoped.', + '- Preserve existing behavior unless it is part of the bug.', + '- Add comments explaining why the fix is shaped this way.', + ].join('\n'), + }, + { + id: 'builtin:code-review', + title: 'Code Review', + description: 'Review a diff for bugs, regressions, and missing tests.', + scope: 'builtin', + body: [ + 'Please review this like a senior engineer.', + '', + 'Focus on:', + '- Bugs or behavioral regressions.', + '- State, lifecycle, concurrency, or ordering risks.', + '- Missing tests or weak verification.', + '- Places where the implementation conflicts with existing architecture.', + '', + 'Return findings first, ordered by severity, with file/line references.', + ].join('\n'), + }, + { + id: 'builtin:refactor-safely', + title: 'Refactor Safely', + description: 'Make a scoped refactor while preserving behavior.', + scope: 'builtin', + body: [ + 'Please refactor this safely.', + '', + 'Goals:', + '- Preserve behavior.', + '- Match the existing codebase patterns.', + '- Keep the diff focused and avoid unrelated cleanup.', + '- Explain any tradeoffs in comments where future readers need the why.', + '', + 'Verification:', + '- Run the narrowest useful tests/build checks.', + '- Call out anything that could not be verified.', + ].join('\n'), + }, + { + id: 'builtin:write-tests', + title: 'Write Tests', + description: 'Add focused tests for a behavior or regression.', + scope: 'builtin', + body: [ + 'Please add focused tests for this behavior.', + '', + 'Behavior to cover:', + '- ', + '', + 'Expectations:', + '- Prefer existing test utilities and local style.', + '- Cover the regression path, not only the happy path.', + '- Keep the tests readable and deterministic.', + '- Run the relevant test command and report the result.', + ].join('\n'), + }, + { + id: 'builtin:explain-code', + title: 'Explain Code Path', + description: 'Trace how a feature works through the codebase.', + scope: 'builtin', + body: [ + 'Please explain this code path from entry point to render/effect.', + '', + 'I care about:', + '- Source of truth.', + '- State transitions.', + '- Important side effects.', + '- Where errors or races are likely.', + '', + 'Use concrete file references and keep the explanation grounded in the code.', + ].join('\n'), + }, +] + +function normalizeCustomTemplates(value: unknown): PromptTemplate[] { + if (!Array.isArray(value)) return [] + return value.flatMap(item => { + if (!item || typeof item !== 'object') return [] + const record = item as Record + if (typeof record.id !== 'string') return [] + if (typeof record.title !== 'string') return [] + if (typeof record.body !== 'string') return [] + return [{ + id: record.id, + title: record.title, + description: typeof record.description === 'string' ? record.description : 'Saved locally', + body: record.body, + scope: 'custom' as const, + createdAt: typeof record.createdAt === 'number' ? record.createdAt : undefined, + updatedAt: typeof record.updatedAt === 'number' ? record.updatedAt : undefined, + }] + }) +} + +export function loadCustomPromptTemplates(): PromptTemplate[] { + try { + const raw = window.localStorage.getItem(CUSTOM_TEMPLATES_KEY) + if (!raw) return [] + return normalizeCustomTemplates(JSON.parse(raw)) + .sort((a, b) => (b.updatedAt ?? b.createdAt ?? 0) - (a.updatedAt ?? a.createdAt ?? 0)) + } catch { + return [] + } +} + +export function saveCustomPromptTemplate(title: string, body: string): PromptTemplate { + const now = Date.now() + const template: PromptTemplate = { + id: `custom:${crypto.randomUUID()}`, + title: title.trim(), + description: 'Saved locally', + body, + scope: 'custom', + createdAt: now, + updatedAt: now, + } + const next = [template, ...loadCustomPromptTemplates()] + window.localStorage.setItem(CUSTOM_TEMPLATES_KEY, JSON.stringify(next)) + return template +} + +export function allPromptTemplates(customTemplates = loadCustomPromptTemplates()): PromptTemplate[] { + return [...customTemplates, ...builtinPromptTemplates] +} diff --git a/src/renderer/src/features/workspace/commands/paneCommands.ts b/src/renderer/src/features/workspace/commands/paneCommands.ts index b74c0d77e..f49e1366b 100644 --- a/src/renderer/src/features/workspace/commands/paneCommands.ts +++ b/src/renderer/src/features/workspace/commands/paneCommands.ts @@ -92,6 +92,13 @@ export const paneCommands: CommandDef[] = [ when: ({ workspace }) => workspace.state.buried.length > 0, run: ({ ui }) => ui.enterBuriedMode(), }, + { + id: 'kill-buried-pane', + title: 'Kill Buried Pane…', + keywords: ['kill', 'buried', 'hidden', 'pane', 'session'], + when: ({ workspace }) => workspace.state.buried.length > 0, + run: ({ ui }) => ui.enterKillBuriedMode(), + }, { id: 'toggle-tail', title: 'Tail', diff --git a/src/renderer/src/workspace/hook/actions/pane.ts b/src/renderer/src/workspace/hook/actions/pane.ts index f0495a8a8..34f02b686 100644 --- a/src/renderer/src/workspace/hook/actions/pane.ts +++ b/src/renderer/src/workspace/hook/actions/pane.ts @@ -36,7 +36,7 @@ import type { SessionActions } from '@renderer/workspace/hook/actions/session' // // Covers: splitFocused, startNewAgentPlacement, commitNewAgentPlacement, // closeFocused, closeSession, requestBuryFocused, buryFocused, -// reviveBuried, focusSession, focusSessionInTab, navigate. +// reviveBuried, killBuried, focusSession, focusSessionInTab, navigate. // ----------------------------------------------------------------------------- export function usePaneActions( @@ -61,6 +61,7 @@ export function usePaneActions( requestBuryFocused: () => void buryFocused: (note?: string, targetSessionId?: SessionId) => void reviveBuried: (buriedId: string) => void + killBuried: (buriedId: string) => Promise focusSession: (sessionId: SessionId) => void focusSessionInTab: (tabId: string, sessionId: SessionId) => void navigate: (direction: 'left' | 'right' | 'up' | 'down') => void @@ -623,6 +624,64 @@ export function usePaneActions( [refs.stateRef, setState], ) + const killBuried = useCallback( + async (buriedId: string) => { + const snapshot = refs.stateRef.current + const entry = snapshot.buried.find(item => item.id === buriedId) + if (!entry) return + + // Buried panes are live sessions removed from every visible tab + // tree. `closeSession` intentionally only handles visible panes + // because it needs tree geometry and undo-close placement data; + // using it here would no-op. Killing a buried pane is a different + // operation: terminate the hidden backend and delete the buried + // record directly, without briefly reviving or mutating layout. + await window.api.killSession(entry.sessionId) + + setRuntimes(prev => { + const next = { ...prev } + delete next[entry.sessionId] + return next + }) + delete refs.seenUuidsRef.current[entry.sessionId] + delete refs.latestScreenRef.current[entry.sessionId] + const bootstrapTimer = refs.bootstrapTimersRef.current.get(entry.sessionId) + if (bootstrapTimer) { + clearTimeout(bootstrapTimer) + refs.bootstrapTimersRef.current.delete(entry.sessionId) + } + const paneToastTimer = refs.paneToastTimers.current[entry.sessionId] + if (paneToastTimer) { + clearTimeout(paneToastTimer) + delete refs.paneToastTimers.current[entry.sessionId] + } + + setState(prev => { + const sessions = { ...prev.sessions } + delete sessions[entry.sessionId] + return { + ...prev, + sessions, + buried: prev.buried.filter(item => item.id !== buriedId), + } + }) + + const kindLabel = entry.sessionMeta.kind ?? 'claude' + const cwdBase = entry.sessionMeta.cwd.split('/').filter(Boolean).pop() ?? entry.sessionMeta.cwd + showToast(`Killed buried ${kindLabel} pane (${cwdBase})`) + }, + [ + refs.bootstrapTimersRef, + refs.latestScreenRef, + refs.paneToastTimers, + refs.seenUuidsRef, + refs.stateRef, + setRuntimes, + setState, + showToast, + ], + ) + const focusSession = useCallback( (sessionId: SessionId) => { setState(prev => ({ @@ -682,6 +741,7 @@ export function usePaneActions( requestBuryFocused, buryFocused, reviveBuried, + killBuried, focusSession, focusSessionInTab, navigate, diff --git a/src/renderer/src/workspace/hook/index.ts b/src/renderer/src/workspace/hook/index.ts index c4b4fe331..361bd2ef2 100644 --- a/src/renderer/src/workspace/hook/index.ts +++ b/src/renderer/src/workspace/hook/index.ts @@ -288,6 +288,7 @@ export function useWorkspace( requestBuryFocused: paneActions.requestBuryFocused, buryFocused: paneActions.buryFocused, reviveBuried: paneActions.reviveBuried, + killBuried: paneActions.killBuried, focusSession: paneActions.focusSession, focusSessionInTab: paneActions.focusSessionInTab, navigate: paneActions.navigate,