Render launch prompts in native chat and refine initial view mode

- Seed and render the agent launch prompt as a synthetic, pending user
  message in the native chat view until the transcript catches up.
- Refine initial view mode logic so native chat does not auto-open for
  draft prompt delivery, unsupported agents, or when disabled.
- Add delivery failure tracking for the launch prompt, rendering an
  error status in the message list if pasting into the terminal fails.
- Implement corresponding store actions, selectors, cleanup routines,
  and extensive test coverage.
This commit is contained in:
Jinjing
2026-07-05 11:31:05 -07:00
parent 021cfc4f5d
commit 33f5eb184c
29 changed files with 772 additions and 78 deletions
@@ -127,7 +127,8 @@ function MessageRow({
expandSignal,
onScrollMessageToTop,
onLinkClick,
allowFileUriLinks = false
allowFileUriLinks = false,
deliveryFailed = false
}: {
message: NativeChatMessage
expandSignal: boolean
@@ -135,6 +136,7 @@ function MessageRow({
onScrollMessageToTop: (el: HTMLElement) => void
onLinkClick?: CommentMarkdownLinkClickHandler
allowFileUriLinks?: boolean
deliveryFailed?: boolean
}): React.JSX.Element | null {
const rowRef = useRef<HTMLDivElement | null>(null)
const { prose, tools } = useMemo(() => splitNativeChatBlocks(message.blocks), [message.blocks])
@@ -181,6 +183,14 @@ function MessageRow({
<ImageAttachmentRefs blocks={prose} />
)}
</div>
{deliveryFailed ? (
<div className="max-w-[85%] text-[11px] text-destructive/80">
{translate(
'components.native-chat.launchPromptNotDelivered',
'Not delivered — check the terminal'
)}
</div>
) : null}
</div>
)
}
@@ -227,7 +237,8 @@ export function NativeChatMessageList({
expandSignal,
fontScale,
onLinkClick,
allowFileUriLinks = false
allowFileUriLinks = false,
failedDeliveryMessageIds
}: {
session: NativeChatLiveSession
isWorking: boolean
@@ -237,6 +248,7 @@ export function NativeChatMessageList({
fontScale: number
onLinkClick?: CommentMarkdownLinkClickHandler
allowFileUriLinks?: boolean
failedDeliveryMessageIds?: ReadonlySet<string>
}): React.JSX.Element {
const scrollRef = useRef<HTMLDivElement | null>(null)
const [stuckToBottom, setStuckToBottom] = useState(true)
@@ -370,6 +382,7 @@ export function NativeChatMessageList({
onScrollMessageToTop={scrollMessageToTop}
onLinkClick={onLinkClick}
allowFileUriLinks={allowFileUriLinks}
deliveryFailed={failedDeliveryMessageIds?.has(message.id) === true}
/>
))}
{showTypingIndicator ? <TypingIndicatorRow /> : null}
@@ -24,10 +24,12 @@ import {
appendPendingSendCache,
commandMarkersAsMessages,
appendCommandMarkerCache,
launchPromptAsMessage,
pendingSendsAsMessages,
prunePendingSends,
readCommandMarkerCache,
readPendingSendCache,
shouldPruneLaunchPrompt,
writePendingSendCache,
type NativeChatCommandMarker,
type NativeChatPendingSend
@@ -156,6 +158,9 @@ function NativeChatResolvedView({
contextMenuActions?: Omit<NativeChatContextMenuActions, 'onPaste'>
}): React.JSX.Element {
const session = useNativeChatLiveSession({ paneKey, agent, sessionId, transcriptPath })
const launchPrompt = useAppStore((s) => s.nativeChatLaunchPromptByTabId[terminalTabId] ?? null)
const clearNativeChatLaunchPrompt = useAppStore((s) => s.clearNativeChatLaunchPrompt)
const paneLaunchPrompt = launchPrompt?.agent === agent ? launchPrompt : null
// Live hook state for this pane, selected directly so the working indicator
// flips the instant the agent reports 'working' — even when switching to chat
// mid-turn before the transcript merge has caught up.
@@ -270,6 +275,12 @@ function NativeChatResolvedView({
writePendingSendCache(pendingScope, prunePendingSends(prev, session.messages))
)
}, [session.messages, pendingScope])
useEffect(() => {
if (!paneLaunchPrompt || !shouldPruneLaunchPrompt(paneLaunchPrompt, session.messages)) {
return
}
clearNativeChatLaunchPrompt(terminalTabId)
}, [clearNativeChatLaunchPrompt, paneLaunchPrompt, session.messages, terminalTabId])
const onOptimisticSend = useCallback(
(text: string, imagePaths?: string[]) => {
setWorkingInterrupted(false)
@@ -291,10 +302,32 @@ function NativeChatResolvedView({
[commandMarkerScope]
)
const launchPromptMessage = useMemo(
() => launchPromptAsMessage(paneLaunchPrompt, session.messages),
[paneLaunchPrompt, session.messages]
)
const sessionWithLaunchPrompt = useMemo<typeof session>(() => {
if (!launchPromptMessage) {
return session
}
return { ...session, messages: [...session.messages, launchPromptMessage] }
}, [launchPromptMessage, session])
const sessionAfterCommandBoundaries = useMemo<typeof session>(() => {
const messages = applyCommandMarkerBoundaries(session.messages, commandMarkers)
return messages === session.messages ? session : { ...session, messages }
}, [session, commandMarkers])
const messages = applyCommandMarkerBoundaries(sessionWithLaunchPrompt.messages, commandMarkers)
return messages === sessionWithLaunchPrompt.messages
? sessionWithLaunchPrompt
: { ...sessionWithLaunchPrompt, messages }
}, [sessionWithLaunchPrompt, commandMarkers])
const launchPromptVisible =
launchPromptMessage !== null &&
sessionAfterCommandBoundaries.messages.some((message) => message.id === launchPromptMessage.id)
const failedLaunchPromptMessageIds = useMemo(() => {
if (!paneLaunchPrompt?.failed || !launchPromptVisible || !launchPromptMessage) {
return undefined
}
return new Set([launchPromptMessage.id])
}, [paneLaunchPrompt?.failed, launchPromptMessage, launchPromptVisible])
// The streaming preview bubble (if any) sits after the transcript but before
// the optimistic user echoes — same order mobile uses.
@@ -422,6 +455,7 @@ function NativeChatResolvedView({
fontScale={fontScale.scale}
onLinkClick={nativeChatFileLinkClick}
allowFileUriLinks={fileLinkContext !== null}
failedDeliveryMessageIds={failedLaunchPromptMessageIds}
/>
)}
</div>
@@ -1,25 +1,8 @@
import type { Tab, TuiAgent } from '../../../../shared/types'
import type { AgentType } from '../../../../shared/agent-status-types'
import { isNativeChatSupportedAgent } from '@/lib/native-chat-supported-agent'
/** Agents whose transcripts the native chat view can actually parse and render.
* Native chat depends on provider-specific transcript/streaming parsing, so the
* toggle must stay limited to the providers we support — currently Claude
* (including the OpenClaude variant) and Codex. Other agents (Grok, Gemini, …)
* run fine in the terminal but have no native-chat rendering, so they must not
* show the toggle. */
const NATIVE_CHAT_SUPPORTED_AGENTS: ReadonlySet<string> = new Set<string>([
'claude',
'openclaude',
'codex'
])
/** Whether the given agent identity (from any signal: launch hint, live
* detection, or title resolution) is one native chat can render. */
export function isNativeChatSupportedAgent(
agent: TuiAgent | AgentType | null | undefined
): boolean {
return agent != null && NATIVE_CHAT_SUPPORTED_AGENTS.has(agent)
}
export { isNativeChatSupportedAgent } from '@/lib/native-chat-supported-agent'
/** Inputs that decide whether a tab may toggle into the native chat view.
* Kept as a plain shape (not the live store) so the decision stays pure and
@@ -1,6 +1,7 @@
import { describe, it, expect } from 'vitest'
import type { NativeChatMessage } from '../../../../shared/native-chat-types'
import { buildNativeChatRenderItems, orderNativeChatMessages } from './native-chat-message-grouping'
import { NATIVE_CHAT_STREAMING_ID } from '../../../../shared/native-chat-streaming'
function msg(
overrides: Partial<NativeChatMessage> & Pick<NativeChatMessage, 'id'>
@@ -31,6 +32,15 @@ describe('orderNativeChatMessages', () => {
])
expect(ordered.map((m) => m.id)).toEqual(['a', 'z'])
})
it('sorts the synthetic streaming message after timestamped content', () => {
const ordered = orderNativeChatMessages([
msg({ id: NATIVE_CHAT_STREAMING_ID, timestamp: null }),
msg({ id: 'real-user', role: 'user', timestamp: 10 }),
msg({ id: 'pending-user', role: 'user', timestamp: 20, source: 'scrape' })
])
expect(ordered.map((m) => m.id)).toEqual(['real-user', 'pending-user', 'streaming'])
})
})
describe('buildNativeChatRenderItems', () => {
@@ -8,11 +8,14 @@ import {
clearPendingSendCacheForTests,
commandMarkersAsMessages,
isCommandMarkerId,
isLaunchPromptMessageId,
isPendingMessageId,
launchPromptAsMessage,
pendingSendsAsMessages,
prunePendingSends,
readCommandMarkerCache,
readPendingSendCache,
shouldPruneLaunchPrompt,
writePendingSendCache,
type NativeChatPendingSend
} from './native-chat-pending'
@@ -135,6 +138,86 @@ describe('pendingSendsAsMessages', () => {
})
})
describe('launchPromptAsMessage', () => {
it('maps a launch prompt to a tab-keyed scrape-source user message', () => {
expect(
launchPromptAsMessage({
tabId: 'tab-1',
agent: 'codex',
text: 'Fix failing checks',
createdAt: 42
})
).toEqual({
id: 'launch-pending:tab-1',
role: 'user',
blocks: [{ type: 'text', text: 'Fix failing checks' }],
timestamp: 42,
source: 'scrape'
})
})
it('hides the launch prompt while its transcript user turn is visible', () => {
expect(
launchPromptAsMessage(
{
tabId: 'tab-1',
agent: 'codex',
text: 'Fix failing checks',
createdAt: 42
},
[userMessage('u1', 'Fix failing checks')]
)
).toBeNull()
})
it('uses pending-send normalization for large multiline generated prompts', () => {
const prompt = [
'[Image #1] Resolve the failing checks:',
'',
'Resolve the failing checks:',
'',
'- lint failed',
' fix spacing'
].join('\n')
const transcript = [
userMessage(
'u1',
'Resolve the failing checks: Resolve the failing checks: - lint failed fix spacing'
),
assistantMessage('a1', 'I will fix it')
]
expect(
shouldPruneLaunchPrompt(
{
tabId: 'tab-1',
agent: 'codex',
text: prompt,
createdAt: 42
},
transcript
)
).toBe(true)
})
it('keeps the launch prompt until the transcript advances past the user turn', () => {
const prompt = {
tabId: 'tab-1',
agent: 'claude' as const,
text: 'Fix failing checks',
createdAt: 42
}
expect(shouldPruneLaunchPrompt(prompt, [userMessage('u1', 'Fix failing checks')])).toBe(false)
expect(
shouldPruneLaunchPrompt(prompt, [
userMessage('u1', 'Fix failing checks'),
assistantMessage('a1', 'working')
])
).toBe(true)
})
})
describe('pending send cache', () => {
it('persists optimistic sends for the same pane and agent', () => {
clearPendingSendCacheForTests()
@@ -165,6 +248,13 @@ describe('isPendingMessageId', () => {
})
})
describe('isLaunchPromptMessageId', () => {
it('recognizes the launch prompt id prefix', () => {
expect(isLaunchPromptMessageId('launch-pending:tab-1')).toBe(true)
expect(isLaunchPromptMessageId('pending:p1')).toBe(false)
})
})
describe('commandMarkersAsMessages', () => {
it('renders a slash command as a system "Ran <cmd>" message', () => {
expect(commandMarkersAsMessages([{ id: 'c1', command: '/clear', sentAt: 7 }])).toEqual([
@@ -5,6 +5,7 @@
import { isTextBlock, type NativeChatMessage } from '../../../../shared/native-chat-types'
import { stripImagePromptMarker } from './native-chat-image-transcript-markers'
import type { NativeChatLaunchPrompt } from '@/lib/native-chat-launch-prompt'
/** An optimistic, not-yet-confirmed composer send. */
export type NativeChatPendingSend = {
@@ -152,6 +153,37 @@ export function isPendingMessageId(id: string): boolean {
return id.startsWith('pending:')
}
export function launchPromptAsMessage(
entry: NativeChatLaunchPrompt | null,
existingMessages: NativeChatMessage[] = []
): NativeChatMessage | null {
if (!entry) {
return null
}
const represented = matchingUserMessageTexts(existingMessages)
if (represented.has(normalize(entry.text))) {
return null
}
return {
id: `launch-pending:${entry.tabId}`,
role: 'user' as const,
blocks: entry.text.trim().length > 0 ? [{ type: 'text' as const, text: entry.text }] : [],
timestamp: entry.createdAt,
source: 'scrape' as const
}
}
export function shouldPruneLaunchPrompt(
entry: NativeChatLaunchPrompt,
messages: NativeChatMessage[]
): boolean {
return advancedPastUserMessageTexts(messages).has(normalize(entry.text))
}
export function isLaunchPromptMessageId(id: string): boolean {
return id.startsWith('launch-pending:')
}
/** A locally-recorded slash command (e.g. `/clear`). Slash commands dispatch to
* the agent's TUI and are not chat turns, so we surface a small system line as
* feedback that the command ran rather than echoing a user bubble. */
@@ -6,6 +6,7 @@ import {
type NativeChatSession,
type NativeChatSessionStatus
} from '../../../../shared/native-chat-types'
import { NATIVE_CHAT_STREAMING_ID } from '../../../../shared/native-chat-streaming'
import { normalizeImageTranscriptMessages } from './native-chat-image-transcript-markers'
/** Messages grouped by source. Higher-priority sources (transcript > hook >
@@ -84,12 +85,20 @@ function supersedes(candidate: NativeChatMessage, existing: NativeChatMessage):
return candidateRank > existingRank
}
function messageSortTimestamp(message: NativeChatMessage): number {
if (message.id === NATIVE_CHAT_STREAMING_ID) {
return Number.POSITIVE_INFINITY
}
return message.timestamp ?? Number.NEGATIVE_INFINITY
}
// Why: null timestamps (sources that can't supply one, e.g. scrape segments)
// sort before any real timestamp so they don't jump to the end. Ties break on
// id for a stable, deterministic order.
// sort before any real timestamp so they don't jump to the end. The synthetic
// streaming preview is the one exception: it is a tail bubble and sorts last.
// Ties break on id for a stable, deterministic order.
export function compareMessages(a: NativeChatMessage, b: NativeChatMessage): number {
const at = a.timestamp ?? Number.NEGATIVE_INFINITY
const bt = b.timestamp ?? Number.NEGATIVE_INFINITY
const at = messageSortTimestamp(a)
const bt = messageSortTimestamp(b)
if (at !== bt) {
return at - bt
}
+6 -2
View File
@@ -1423,7 +1423,9 @@ export function useIpcEvents(): void {
...(launchAgent
? {
launchAgent,
...initialAgentTabViewModeProps(store.settings)
...initialAgentTabViewModeProps(store.settings, {
agent: launchAgent
})
}
: {}),
// Why: tabId hint comes from CLI-spawned PTYs whose env
@@ -1602,7 +1604,9 @@ export function useIpcEvents(): void {
? {
...(shouldActivate ? {} : { activate: false, recordInteraction: false }),
launchAgent: data.launchAgent,
...initialAgentTabViewModeProps(store.settings)
...initialAgentTabViewModeProps(store.settings, {
agent: data.launchAgent
})
}
: shouldActivate
? undefined
+2 -1
View File
@@ -12364,7 +12364,8 @@
"title": "Allow {{value0}}?",
"allow": "Allow",
"deny": "Deny"
}
},
"launchPromptNotDelivered": "Not delivered — check the terminal"
},
"tab": {
"bar": {
+2 -1
View File
@@ -12364,7 +12364,8 @@
"title": "¿Permitir {{value0}}?",
"allow": "Permitir",
"deny": "Denegar"
}
},
"launchPromptNotDelivered": "Not delivered — check the terminal"
},
"tab": {
"bar": {
+2 -1
View File
@@ -12364,7 +12364,8 @@
"title": "{{value0}} を許可しますか?",
"allow": "許可する",
"deny": "拒否"
}
},
"launchPromptNotDelivered": "Not delivered — check the terminal"
},
"tab": {
"bar": {
+2 -1
View File
@@ -12364,7 +12364,8 @@
"title": "{{value0}}을(를) 허용하시겠습니까?",
"allow": "허용하다",
"deny": "부인하다"
}
},
"launchPromptNotDelivered": "Not delivered — check the terminal"
},
"tab": {
"bar": {
+2 -1
View File
@@ -12364,7 +12364,8 @@
"title": "允许 {{value0}}?",
"allow": "允许",
"deny": "拒绝"
}
},
"launchPromptNotDelivered": "Not delivered — check the terminal"
},
"tab": {
"bar": {
@@ -0,0 +1,131 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
pasteDraftWhenAgentReady: vi.fn(),
seedNativeChatLaunchPrompt: vi.fn(),
markNativeChatLaunchPromptFailed: vi.fn()
}))
vi.mock('@/lib/agent-paste-draft', () => ({
pasteDraftWhenAgentReady: mocks.pasteDraftWhenAgentReady
}))
vi.mock('@/store', () => ({
useAppStore: {
getState: () => ({
seedNativeChatLaunchPrompt: mocks.seedNativeChatLaunchPrompt,
markNativeChatLaunchPromptFailed: mocks.markNativeChatLaunchPromptFailed
})
}
}))
import { deliverLaunchPromptToAgentTab } from './agent-launch-prompt-delivery'
describe('deliverLaunchPromptToAgentTab', () => {
beforeEach(() => {
vi.clearAllMocks()
mocks.pasteDraftWhenAgentReady.mockResolvedValue(true)
})
it('seeds a native-chat launch prompt for supported submitted content', async () => {
await expect(
deliverLaunchPromptToAgentTab({
tabId: 'tab-1',
agent: 'codex',
content: 'Fix failing checks',
submit: true,
forcePaste: true
})
).resolves.toBe(true)
expect(mocks.seedNativeChatLaunchPrompt).toHaveBeenCalledWith({
tabId: 'tab-1',
agent: 'codex',
text: 'Fix failing checks',
createdAt: expect.any(Number)
})
expect(mocks.pasteDraftWhenAgentReady).toHaveBeenCalledWith({
tabId: 'tab-1',
agent: 'codex',
content: 'Fix failing checks',
submit: true,
forcePaste: true,
timeoutMs: undefined,
onTimeout: undefined
})
})
it('does not seed for drafts, unsupported agents, or empty content', async () => {
await deliverLaunchPromptToAgentTab({
tabId: 'draft-tab',
agent: 'codex',
content: 'Review first',
submit: false,
forcePaste: false
})
await deliverLaunchPromptToAgentTab({
tabId: 'unsupported-tab',
agent: 'grok',
content: 'Fix failing checks',
submit: true,
forcePaste: true
})
await deliverLaunchPromptToAgentTab({
tabId: 'empty-tab',
agent: 'claude',
content: ' ',
submit: true,
forcePaste: true
})
expect(mocks.seedNativeChatLaunchPrompt).not.toHaveBeenCalled()
})
it('marks a seeded launch prompt failed when paste delivery returns false', async () => {
mocks.pasteDraftWhenAgentReady.mockResolvedValue(false)
await expect(
deliverLaunchPromptToAgentTab({
tabId: 'tab-1',
agent: 'claude',
content: 'Large generated prompt',
submit: true,
forcePaste: true
})
).resolves.toBe(false)
expect(mocks.markNativeChatLaunchPromptFailed).toHaveBeenCalledWith('tab-1')
})
it('does not mark unseeded launches failed', async () => {
mocks.pasteDraftWhenAgentReady.mockResolvedValue(false)
await deliverLaunchPromptToAgentTab({
tabId: 'tab-1',
agent: 'grok',
content: 'Large generated prompt',
submit: true,
forcePaste: true
})
expect(mocks.markNativeChatLaunchPromptFailed).not.toHaveBeenCalled()
})
it('passes timeout options through to the paste transport', async () => {
const onTimeout = vi.fn()
await deliverLaunchPromptToAgentTab({
tabId: 'tab-1',
agent: 'codex',
content: 'Fix failing checks',
submit: true,
forcePaste: true,
timeoutMs: 123,
onTimeout
})
expect(mocks.pasteDraftWhenAgentReady).toHaveBeenCalledWith(
expect.objectContaining({ timeoutMs: 123, onTimeout })
)
})
})
@@ -0,0 +1,42 @@
import { pasteDraftWhenAgentReady } from '@/lib/agent-paste-draft'
import { isNativeChatSupportedAgent } from '@/lib/native-chat-supported-agent'
import { useAppStore } from '@/store'
import type { TuiAgent } from '../../../shared/types'
export function deliverLaunchPromptToAgentTab(args: {
tabId: string
agent: TuiAgent
content: string
submit: boolean
forcePaste: boolean
timeoutMs?: number
onTimeout?: () => void
}): Promise<boolean> {
const { tabId, agent, content, submit, forcePaste, timeoutMs, onTimeout } = args
const shouldSeed =
submit === true && content.trim().length > 0 && isNativeChatSupportedAgent(agent)
if (shouldSeed) {
useAppStore.getState().seedNativeChatLaunchPrompt({
tabId,
agent,
text: content,
createdAt: Date.now()
})
}
return pasteDraftWhenAgentReady({
tabId,
content,
agent,
submit,
forcePaste,
timeoutMs,
onTimeout
}).then((delivered) => {
if (shouldSeed && !delivered) {
useAppStore.getState().markNativeChatLaunchPromptFailed(tabId)
}
return delivered
})
}
@@ -6,6 +6,8 @@ const mockSetActiveTabType = vi.fn()
const mockSetTabBarOrder = vi.fn()
const mockSetAgentStatus = vi.fn()
const mockPasteDraftWhenAgentReady = vi.fn()
const mockSeedNativeChatLaunchPrompt = vi.fn()
const mockMarkNativeChatLaunchPromptFailed = vi.fn()
const mockTrack = vi.fn()
const LEAF_ID = '11111111-1111-4111-8111-111111111111'
@@ -18,6 +20,13 @@ const store = {
agentDefaultArgs: {} as Record<string, string>,
agentDefaultEnv: {} as Record<string, Record<string, string>>,
activeRuntimeEnvironmentId: null as string | null
} as {
agentCmdOverrides: Record<string, string>
agentDefaultArgs: Record<string, string>
agentDefaultEnv: Record<string, Record<string, string>>
activeRuntimeEnvironmentId: string | null
experimentalNativeChat?: boolean
openAgentTabsInChatByDefault?: boolean
},
projects: [
{
@@ -56,7 +65,9 @@ const store = {
queueTabStartupCommand: mockQueueTabStartupCommand,
setActiveTabType: mockSetActiveTabType,
setTabBarOrder: mockSetTabBarOrder,
setAgentStatus: mockSetAgentStatus
setAgentStatus: mockSetAgentStatus,
seedNativeChatLaunchPrompt: mockSeedNativeChatLaunchPrompt,
markNativeChatLaunchPromptFailed: mockMarkNativeChatLaunchPromptFailed
}
vi.mock('@/store', () => ({
@@ -152,6 +163,66 @@ describe('launchAgentInNewTab', () => {
})
})
it('opens supported submit-after-ready launches in chat and seeds a launch prompt echo', async () => {
store.settings = {
agentCmdOverrides: {},
agentDefaultArgs: {},
agentDefaultEnv: {},
activeRuntimeEnvironmentId: null,
experimentalNativeChat: true,
openAgentTabsInChatByDefault: true
}
const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab')
launchAgentInNewTab({
agent: 'codex',
worktreeId: 'wt-1',
prompt: 'large generated prompt',
promptDelivery: 'submit-after-ready'
})
expect(mockCreateTab).toHaveBeenCalledWith('wt-1', undefined, undefined, {
launchAgent: 'codex',
viewMode: 'chat'
})
expect(mockQueueTabStartupCommand).toHaveBeenCalledWith(
'tab-1',
expect.objectContaining({
command: expect.not.stringContaining('large generated prompt')
})
)
expect(mockSeedNativeChatLaunchPrompt).toHaveBeenCalledWith({
tabId: 'tab-1',
agent: 'codex',
text: 'large generated prompt',
createdAt: expect.any(Number)
})
})
it('keeps unsupported submit-after-ready launches in terminal mode and does not seed chat', async () => {
store.settings = {
agentCmdOverrides: {},
agentDefaultArgs: {},
agentDefaultEnv: {},
activeRuntimeEnvironmentId: null,
experimentalNativeChat: true,
openAgentTabsInChatByDefault: true
}
const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab')
launchAgentInNewTab({
agent: 'grok',
worktreeId: 'wt-1',
prompt: 'large generated prompt',
promptDelivery: 'submit-after-ready'
})
expect(mockCreateTab).toHaveBeenCalledWith('wt-1', undefined, undefined, {
launchAgent: 'grok'
})
expect(mockSeedNativeChatLaunchPrompt).not.toHaveBeenCalled()
})
it('passes quick command labels only to locally-created agent tabs', async () => {
const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab')
@@ -414,6 +485,7 @@ describe('launchAgentInNewTab', () => {
})
store.terminalLayoutsByTabId = { 'tab-1': { activeLeafId: LEAF_ID } }
await Promise.resolve()
await Promise.resolve()
expect(mockQueueTabStartupCommand).toHaveBeenCalledWith(
'tab-1',
@@ -9,7 +9,7 @@ import { CLIENT_PLATFORM } from '@/lib/new-workspace'
import { getAgentLaunchPlatformForRepo } from '@/lib/agent-launch-platform'
import { reconcileTabOrder } from '@/components/tab-bar/reconcile-order'
import { track, tuiAgentToAgentKind } from '@/lib/telemetry'
import { pasteDraftWhenAgentReady } from '@/lib/agent-paste-draft'
import { deliverLaunchPromptToAgentTab } from '@/lib/agent-launch-prompt-delivery'
import { initialAgentTabViewModeProps } from '@/lib/native-chat-initial-view-mode'
import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner'
import { getLocalProjectExecutionRuntimeContext } from '@/lib/local-preflight-context'
@@ -271,7 +271,7 @@ export function launchAgentInNewTab(args: LaunchAgentInNewTabArgs): LaunchAgentI
const tab = store.createTab(worktreeId, groupId, undefined, {
launchAgent: agent,
quickCommandLabel,
...initialAgentTabViewModeProps(store.settings)
...initialAgentTabViewModeProps(store.settings, { agent, promptDelivery })
})
store.queueTabStartupCommand(tab.id, {
command: startupPlan.launchCommand,
@@ -302,7 +302,7 @@ export function launchAgentInNewTab(args: LaunchAgentInNewTabArgs): LaunchAgentI
// don't fire for user-initiated cancellation (mirrors the 5s launch
// watchdog in QuickLaunchButton).
const tabId = tab.id
void pasteDraftWhenAgentReady({
void deliverLaunchPromptToAgentTab({
tabId,
content: pasteDraftAfterLaunch,
agent,
@@ -1,5 +1,5 @@
import { toast } from 'sonner'
import { pasteDraftWhenAgentReady } from '@/lib/agent-paste-draft'
import { deliverLaunchPromptToAgentTab } from '@/lib/agent-launch-prompt-delivery'
import { track, tuiAgentToAgentKind } from '@/lib/telemetry'
import {
buildAgentDraftLaunchPlan,
@@ -144,7 +144,7 @@ export async function pasteDirectWorkItemDraftWhenAgentReady(args: {
forcePaste?: boolean
}): Promise<void> {
const { primaryTabId, startupPlan, content, submit = false, forcePaste = false } = args
await pasteDraftWhenAgentReady({
await deliverLaunchPromptToAgentTab({
tabId: primaryTabId,
content,
agent: startupPlan.agent,
@@ -10,6 +10,8 @@ const mocks = vi.hoisted(() => ({
ensureRemoteDetectedAgents: vi.fn(),
updateWorktreeMeta: vi.fn(),
setSidebarOpen: vi.fn(),
seedNativeChatLaunchPrompt: vi.fn(),
markNativeChatLaunchPromptFailed: vi.fn(),
activateAndRevealWorktree: vi.fn(),
pasteDraftWhenAgentReady: vi.fn(),
openModalFallback: vi.fn(),
@@ -21,6 +23,8 @@ const mocks = vi.hoisted(() => ({
createWorktree: ReturnType<typeof vi.fn>
updateWorktreeMeta: ReturnType<typeof vi.fn>
setSidebarOpen: ReturnType<typeof vi.fn>
seedNativeChatLaunchPrompt: ReturnType<typeof vi.fn>
markNativeChatLaunchPromptFailed: ReturnType<typeof vi.fn>
}
}))
@@ -185,7 +189,9 @@ describe('launchWorkItemDirect', () => {
ensureRemoteDetectedAgents: mocks.ensureRemoteDetectedAgents,
createWorktree: mocks.createWorktree,
updateWorktreeMeta: mocks.updateWorktreeMeta,
setSidebarOpen: mocks.setSidebarOpen
setSidebarOpen: mocks.setSidebarOpen,
seedNativeChatLaunchPrompt: mocks.seedNativeChatLaunchPrompt,
markNativeChatLaunchPromptFailed: mocks.markNativeChatLaunchPromptFailed
} as typeof mocks.store
// @ts-expect-error -- test shim
globalThis.window = { api: mockApi }
@@ -414,13 +420,21 @@ describe('launchWorkItemDirect', () => {
).resolves.toBe(true)
expect(buildAgentDraftLaunchPlan).not.toHaveBeenCalled()
expect(pasteDraftWhenAgentReady).toHaveBeenCalledWith({
expect(pasteDraftWhenAgentReady).toHaveBeenCalledWith(
expect.objectContaining({
tabId: 'tab-1',
content: 'Use this explicit user prompt.',
agent: 'claude',
submit: true,
forcePaste: true,
onTimeout: expect.any(Function)
})
)
expect(mocks.seedNativeChatLaunchPrompt).toHaveBeenCalledWith({
tabId: 'tab-1',
content: 'Use this explicit user prompt.',
agent: 'claude',
submit: true,
forcePaste: true,
onTimeout: expect.any(Function)
text: 'Use this explicit user prompt.',
createdAt: expect.any(Number)
})
})
@@ -6,33 +6,84 @@ import {
describe('decideInitialAgentTabViewMode', () => {
it("returns 'chat' when native chat and the opt-in default setting are on", () => {
expect(decideInitialAgentTabViewMode(true, true)).toBe('chat')
expect(
decideInitialAgentTabViewMode({
experimentalNativeChat: true,
openAgentTabsInChatByDefault: true,
agent: 'codex'
})
).toBe('chat')
})
it('returns undefined when native chat is disabled', () => {
expect(decideInitialAgentTabViewMode(false, true)).toBeUndefined()
expect(
decideInitialAgentTabViewMode({
experimentalNativeChat: false,
openAgentTabsInChatByDefault: true,
agent: 'codex'
})
).toBeUndefined()
})
it('returns undefined when the default-chat setting is off', () => {
expect(decideInitialAgentTabViewMode(true, false)).toBeUndefined()
expect(
decideInitialAgentTabViewMode({
experimentalNativeChat: true,
openAgentTabsInChatByDefault: false,
agent: 'codex'
})
).toBeUndefined()
})
it('returns undefined when the setting is missing (legacy settings)', () => {
expect(decideInitialAgentTabViewMode(true, undefined)).toBeUndefined()
expect(
decideInitialAgentTabViewMode({
experimentalNativeChat: true,
openAgentTabsInChatByDefault: undefined,
agent: 'codex'
})
).toBeUndefined()
})
it('returns undefined for unsupported agents', () => {
expect(
decideInitialAgentTabViewMode({
experimentalNativeChat: true,
openAgentTabsInChatByDefault: true,
agent: 'grok'
})
).toBeUndefined()
})
it('returns undefined for draft delivery', () => {
expect(
decideInitialAgentTabViewMode({
experimentalNativeChat: true,
openAgentTabsInChatByDefault: true,
agent: 'claude',
promptDelivery: 'draft'
})
).toBeUndefined()
})
it('returns tab creation props only when chat should be the initial mode', () => {
expect(
initialAgentTabViewModeProps({
experimentalNativeChat: true,
openAgentTabsInChatByDefault: true
})
initialAgentTabViewModeProps(
{
experimentalNativeChat: true,
openAgentTabsInChatByDefault: true
},
{ agent: 'claude' }
)
).toEqual({ viewMode: 'chat' })
expect(
initialAgentTabViewModeProps({
experimentalNativeChat: false,
openAgentTabsInChatByDefault: true
})
initialAgentTabViewModeProps(
{
experimentalNativeChat: false,
openAgentTabsInChatByDefault: true
},
{ agent: 'claude' }
)
).toEqual({})
})
})
@@ -1,29 +1,49 @@
import type { GlobalSettings, Tab } from '../../../shared/types'
import type { GlobalSettings, Tab, TuiAgent } from '../../../shared/types'
import { isNativeChatSupportedAgent } from '@/lib/native-chat-supported-agent'
export type NativeChatLaunchPromptDelivery = 'auto-submit' | 'draft' | 'submit-after-ready'
/**
* Decide the initial `viewMode` for a newly launched agent tab from the
* opt-in `openAgentTabsInChatByDefault` setting.
*
* Returns `'chat'` only when the setting is explicitly on; otherwise returns
* `undefined` so the tab keeps the implicit default (`'terminal'`) and stays
* backward-compatible with tabs persisted before the setting existed. A pure
* function so the decision can be unit-tested without the store or launch path.
* Returns `'chat'` only when the setting is explicitly on and the launched
* agent has a native-chat renderer. Draft launches stay in the terminal because
* their prompt exists only in the TUI input buffer.
*/
export function decideInitialAgentTabViewMode(
experimentalNativeChat: boolean | undefined,
openAgentTabsInChatByDefault: boolean | undefined
): Tab['viewMode'] {
return experimentalNativeChat === true && openAgentTabsInChatByDefault === true
? 'chat'
: undefined
export function decideInitialAgentTabViewMode(args: {
experimentalNativeChat?: boolean
openAgentTabsInChatByDefault?: boolean
agent?: TuiAgent | null
promptDelivery?: NativeChatLaunchPromptDelivery
}): Tab['viewMode'] {
if (args.experimentalNativeChat !== true || args.openAgentTabsInChatByDefault !== true) {
return undefined
}
if (!isNativeChatSupportedAgent(args.agent)) {
return undefined
}
if (args.promptDelivery === 'draft') {
return undefined
}
return 'chat'
}
export function initialAgentTabViewModeProps(
settings: Pick<GlobalSettings, 'experimentalNativeChat' | 'openAgentTabsInChatByDefault'> | null
settings:
| Pick<GlobalSettings, 'experimentalNativeChat' | 'openAgentTabsInChatByDefault'>
| null
| undefined,
options: {
agent?: TuiAgent | null
promptDelivery?: NativeChatLaunchPromptDelivery
} = {}
): { viewMode?: Tab['viewMode'] } {
const viewMode = decideInitialAgentTabViewMode(
settings?.experimentalNativeChat,
settings?.openAgentTabsInChatByDefault
)
const viewMode = decideInitialAgentTabViewMode({
experimentalNativeChat: settings?.experimentalNativeChat,
openAgentTabsInChatByDefault: settings?.openAgentTabsInChatByDefault,
agent: options.agent,
promptDelivery: options.promptDelivery
})
return viewMode ? { viewMode } : {}
}
@@ -0,0 +1,9 @@
import type { TuiAgent } from '../../../shared/types'
export type NativeChatLaunchPrompt = {
tabId: string
agent: TuiAgent
text: string
createdAt: number
failed?: boolean
}
@@ -0,0 +1,15 @@
import type { AgentType } from '../../../shared/agent-status-types'
import type { TuiAgent } from '../../../shared/types'
/** Agents whose transcripts the native chat view can parse and render. */
export const NATIVE_CHAT_SUPPORTED_AGENTS: ReadonlySet<string> = new Set<string>([
'claude',
'openclaude',
'codex'
])
export function isNativeChatSupportedAgent(
agent: TuiAgent | AgentType | null | undefined
): boolean {
return agent != null && NATIVE_CHAT_SUPPORTED_AGENTS.has(agent)
}
@@ -421,6 +421,32 @@ describe('ensureWorktreeHasInitialTerminal', () => {
})
})
it('keeps draft startup payloads in terminal mode even when native chat is configured', () => {
const store = createMockStore({
settings: {
experimentalNativeChat: true,
openAgentTabsInChatByDefault: true
}
})
ensureWorktreeHasInitialTerminal(
store,
'wt-1',
{
command: 'claude',
launchAgent: 'claude',
draftPrompt: 'Review before sending'
},
undefined,
undefined
)
expect(store.createTab).toHaveBeenCalledWith('wt-1', undefined, undefined, {
pendingActivationSpawn: true,
launchAgent: 'claude'
})
})
it('gates startup behind setup completion when both are provided in new-tab mode', () => {
setSetupScriptLaunchMode('new-tab')
let createdIndex = 0
+14 -2
View File
@@ -559,7 +559,13 @@ export function ensureWorktreeHasInitialTerminal(
const terminalTab = store.createTab(worktreeId, undefined, undefined, {
pendingActivationSpawn: true,
...(launchAgent
? { launchAgent, ...initialAgentTabViewModeProps(store.settings ?? null) }
? {
launchAgent,
...initialAgentTabViewModeProps(store.settings ?? null, {
agent: launchAgent,
promptDelivery: sequencedStartup?.draftPrompt != null ? 'draft' : undefined
})
}
: {}),
...(opts?.activateCreatedTabs === false ? { activate: false } : {})
})
@@ -618,7 +624,13 @@ function applyDefaultTerminalTabs(
pendingActivationSpawn: true,
recordInteraction: false,
...(launchAgent
? { launchAgent, ...initialAgentTabViewModeProps(store.settings ?? null) }
? {
launchAgent,
...initialAgentTabViewModeProps(store.settings ?? null, {
agent: launchAgent,
promptDelivery: isStartupTab && startup?.draftPrompt != null ? 'draft' : undefined
})
}
: {}),
...(opts?.activateCreatedTabs === false ? { activate: false } : {})
})
@@ -18,6 +18,7 @@ type OrphanTerminalCleanupState = Pick<
| 'pendingSetupSplitByTabId'
| 'pendingIssueCommandSplitByTabId'
| 'automaticAgentResumeClaimsByTabId'
| 'nativeChatLaunchPromptByTabId'
| 'tabBarOrderByWorktree'
| 'cacheTimerByKey'
| 'activeTabIdByWorktree'
@@ -65,6 +66,7 @@ export function buildOrphanTerminalCleanupPatch(
| 'pendingSetupSplitByTabId'
| 'pendingIssueCommandSplitByTabId'
| 'automaticAgentResumeClaimsByTabId'
| 'nativeChatLaunchPromptByTabId'
| 'tabBarOrderByWorktree'
| 'cacheTimerByKey'
| 'activeTabIdByWorktree'
@@ -83,6 +85,7 @@ export function buildOrphanTerminalCleanupPatch(
pendingSetupSplitByTabId: state.pendingSetupSplitByTabId,
pendingIssueCommandSplitByTabId: state.pendingIssueCommandSplitByTabId,
automaticAgentResumeClaimsByTabId: state.automaticAgentResumeClaimsByTabId,
nativeChatLaunchPromptByTabId: state.nativeChatLaunchPromptByTabId,
tabBarOrderByWorktree: state.tabBarOrderByWorktree,
cacheTimerByKey: state.cacheTimerByKey,
activeTabIdByWorktree: state.activeTabIdByWorktree,
@@ -105,6 +108,7 @@ export function buildOrphanTerminalCleanupPatch(
const nextAutomaticAgentResumeClaimsByTabId = {
...state.automaticAgentResumeClaimsByTabId
}
const nextNativeChatLaunchPromptByTabId = { ...state.nativeChatLaunchPromptByTabId }
const nextTabBarOrderByWorktree = {
...state.tabBarOrderByWorktree,
[worktreeId]: (state.tabBarOrderByWorktree[worktreeId] ?? []).filter(
@@ -128,6 +132,7 @@ export function buildOrphanTerminalCleanupPatch(
delete nextPendingSetupSplitByTabId[orphanTabId]
delete nextPendingIssueCommandSplitByTabId[orphanTabId]
delete nextAutomaticAgentResumeClaimsByTabId[orphanTabId]
delete nextNativeChatLaunchPromptByTabId[orphanTabId]
for (const key of Object.keys(nextCacheTimerByKey)) {
if (key.startsWith(`${orphanTabId}:`)) {
delete nextCacheTimerByKey[key]
@@ -157,6 +162,7 @@ export function buildOrphanTerminalCleanupPatch(
pendingSetupSplitByTabId: nextPendingSetupSplitByTabId,
pendingIssueCommandSplitByTabId: nextPendingIssueCommandSplitByTabId,
automaticAgentResumeClaimsByTabId: nextAutomaticAgentResumeClaimsByTabId,
nativeChatLaunchPromptByTabId: nextNativeChatLaunchPromptByTabId,
tabBarOrderByWorktree: nextTabBarOrderByWorktree,
cacheTimerByKey: nextCacheTimerByKey,
activeTabIdByWorktree: nextActiveTabIdByWorktree,
@@ -64,6 +64,7 @@ import { sanitizeTerminalLayoutPaneTitles } from '@/lib/terminal-pane-title-sani
import { focusTerminalTabSurface } from '@/lib/focus-terminal-tab-surface'
import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner'
import { getLocalProjectExecutionRuntimeContext } from '@/lib/local-preflight-context'
import type { NativeChatLaunchPrompt } from '@/lib/native-chat-launch-prompt'
import {
collectHibernatedCompletionEvidenceForWorktree,
collectSleepingAgentSessionRecordsForWorktree,
@@ -340,6 +341,11 @@ export type TerminalSlice = {
* bridges the gap after startup payload consumption and before hooks go live. */
automaticAgentResumeClaimsByTabId: Record<string, AutomaticAgentResumeClaim>
claimAutomaticAgentResume: (tabId: string, claim: AutomaticAgentResumeClaim) => void
/** Launch-time native-chat prompt echo, keyed by terminal tab. In-memory only. */
nativeChatLaunchPromptByTabId: Record<string, NativeChatLaunchPrompt>
seedNativeChatLaunchPrompt: (prompt: NativeChatLaunchPrompt) => void
markNativeChatLaunchPromptFailed: (tabId: string) => void
clearNativeChatLaunchPrompt: (tabId: string) => void
pendingStartupByTabId: Record<
string,
{
@@ -599,6 +605,7 @@ export const createTerminalSlice: StateCreator<AppState, [], [], TerminalSlice>
pendingSetupSplitByTabId: {},
pendingIssueCommandSplitByTabId: {},
automaticAgentResumeClaimsByTabId: {},
nativeChatLaunchPromptByTabId: {},
tabBarOrderByWorktree: {},
workspaceSessionReady: false,
defaultTerminalTabsAppliedByWorktreeId: {},
@@ -648,6 +655,41 @@ export const createTerminalSlice: StateCreator<AppState, [], [], TerminalSlice>
}))
},
seedNativeChatLaunchPrompt: (prompt) => {
set((s) => ({
nativeChatLaunchPromptByTabId: {
...s.nativeChatLaunchPromptByTabId,
[prompt.tabId]: prompt
}
}))
},
markNativeChatLaunchPromptFailed: (tabId) => {
set((s) => {
const current = s.nativeChatLaunchPromptByTabId[tabId]
if (!current || current.failed) {
return {}
}
return {
nativeChatLaunchPromptByTabId: {
...s.nativeChatLaunchPromptByTabId,
[tabId]: { ...current, failed: true }
}
}
})
},
clearNativeChatLaunchPrompt: (tabId) => {
set((s) => {
if (!s.nativeChatLaunchPromptByTabId[tabId]) {
return {}
}
const next = { ...s.nativeChatLaunchPromptByTabId }
delete next[tabId]
return { nativeChatLaunchPromptByTabId: next }
})
},
recordTerminalInput: (paneKey, timestamp = Date.now()) => {
if (!paneKey || !Number.isFinite(timestamp)) {
return
@@ -1052,6 +1094,8 @@ export const createTerminalSlice: StateCreator<AppState, [], [], TerminalSlice>
delete nextPendingStartupByTabId[tabId]
const nextAutomaticAgentResumeClaimsByTabId = { ...s.automaticAgentResumeClaimsByTabId }
delete nextAutomaticAgentResumeClaimsByTabId[tabId]
const nextNativeChatLaunchPromptByTabId = { ...s.nativeChatLaunchPromptByTabId }
delete nextNativeChatLaunchPromptByTabId[tabId]
const nextPendingInitialCwdByTabId = { ...s.pendingInitialCwdByTabId }
delete nextPendingInitialCwdByTabId[tabId]
const nextPendingSetupSplitByTabId = { ...s.pendingSetupSplitByTabId }
@@ -1129,6 +1173,7 @@ export const createTerminalSlice: StateCreator<AppState, [], [], TerminalSlice>
terminalLayoutsByTabId: nextLayouts,
pendingStartupByTabId: nextPendingStartupByTabId,
automaticAgentResumeClaimsByTabId: nextAutomaticAgentResumeClaimsByTabId,
nativeChatLaunchPromptByTabId: nextNativeChatLaunchPromptByTabId,
pendingInitialCwdByTabId: nextPendingInitialCwdByTabId,
pendingSetupSplitByTabId: nextPendingSetupSplitByTabId,
pendingIssueCommandSplitByTabId: nextPendingIssueCommandSplitByTabId,
@@ -188,6 +188,73 @@ describe('worktree removal evicts the per-worktree + per-page maps it previously
expect(getAgentHibernationPaneOutputEpoch(survivingPaneKey)).toBe(1)
})
it('bulk purgeWorktreeTerminalState drops native-chat launch prompts for removed tabs only', () => {
const store = createTestStore()
const TAB1 = 'tab-wt1'
const TAB2 = 'tab-wt2'
seedStore(store, {
worktreesByRepo: {
repo1: [
makeWorktree({ id: WT1, repoId: 'repo1', path: '/path/wt1' }),
makeWorktree({ id: WT2, repoId: 'repo1', path: '/path/wt2' })
]
},
tabsByWorktree: {
[WT1]: [makeTab({ id: TAB1, worktreeId: WT1 })],
[WT2]: [makeTab({ id: TAB2, worktreeId: WT2 })]
},
nativeChatLaunchPromptByTabId: {
[TAB1]: { tabId: TAB1, agent: 'codex', text: 'fix wt1', createdAt: 1 },
[TAB2]: { tabId: TAB2, agent: 'codex', text: 'fix wt2', createdAt: 2 }
}
})
store.getState().purgeWorktreeTerminalState([WT1])
const s = store.getState()
expect(s.nativeChatLaunchPromptByTabId[TAB1]).toBeUndefined()
expect(s.nativeChatLaunchPromptByTabId[TAB2]).toEqual({
tabId: TAB2,
agent: 'codex',
text: 'fix wt2',
createdAt: 2
})
})
it('single removeWorktree drops native-chat launch prompts for removed tabs only', async () => {
const store = createTestStore()
const TAB1 = 'tab-wt1'
const TAB2 = 'tab-wt2'
seedStore(store, {
worktreesByRepo: {
repo1: [
makeWorktree({ id: WT1, repoId: 'repo1', path: '/path/wt1' }),
makeWorktree({ id: WT2, repoId: 'repo1', path: '/path/wt2' })
]
},
tabsByWorktree: {
[WT1]: [makeTab({ id: TAB1, worktreeId: WT1 })],
[WT2]: [makeTab({ id: TAB2, worktreeId: WT2 })]
},
nativeChatLaunchPromptByTabId: {
[TAB1]: { tabId: TAB1, agent: 'claude', text: 'fix wt1', createdAt: 1 },
[TAB2]: { tabId: TAB2, agent: 'claude', text: 'fix wt2', createdAt: 2 }
}
})
const result = await store.getState().removeWorktree(WT1)
expect(result).toEqual({ ok: true })
const s = store.getState()
expect(s.nativeChatLaunchPromptByTabId[TAB1]).toBeUndefined()
expect(s.nativeChatLaunchPromptByTabId[TAB2]).toEqual({
tabId: TAB2,
agent: 'claude',
text: 'fix wt2',
createdAt: 2
})
})
it('bulk purgeWorktreeTerminalState drops page/workspace-keyed browser maps for the removed worktree only', () => {
const store = createTestStore()
const WS1 = 'ws-1'
@@ -1969,6 +1969,7 @@ function buildWorktreePurgeState(s: AppState, worktreeIds: string[]): Partial<Ap
ptyIdsByTabId: omitByTabId(s.ptyIdsByTabId),
runtimePaneTitlesByTabId: omitByTabId(s.runtimePaneTitlesByTabId),
automaticAgentResumeClaimsByTabId: omitByTabId(s.automaticAgentResumeClaimsByTabId),
nativeChatLaunchPromptByTabId: omitByTabId(s.nativeChatLaunchPromptByTabId),
// Delete state
deleteStateByWorktreeId: omitByWorktree(s.deleteStateByWorktreeId),
baseStatusByWorktreeId: omitByWorktree(s.baseStatusByWorktreeId),
@@ -3055,11 +3056,13 @@ export const createWorktreeSlice: StateCreator<AppState, [], [], WorktreeSlice>
const nextAutomaticAgentResumeClaimsByTabId = {
...s.automaticAgentResumeClaimsByTabId
}
const nextNativeChatLaunchPromptByTabId = { ...s.nativeChatLaunchPromptByTabId }
for (const tabId of tabIds) {
delete nextLayouts[tabId]
delete nextPtyIdsByTabId[tabId]
delete nextRuntimePaneTitlesByTabId[tabId]
delete nextAutomaticAgentResumeClaimsByTabId[tabId]
delete nextNativeChatLaunchPromptByTabId[tabId]
}
const nextDeleteState = { ...s.deleteStateByWorktreeId }
delete nextDeleteState[worktreeId]
@@ -3205,6 +3208,7 @@ export const createWorktreeSlice: StateCreator<AppState, [], [], WorktreeSlice>
ptyIdsByTabId: nextPtyIdsByTabId,
runtimePaneTitlesByTabId: nextRuntimePaneTitlesByTabId,
automaticAgentResumeClaimsByTabId: nextAutomaticAgentResumeClaimsByTabId,
nativeChatLaunchPromptByTabId: nextNativeChatLaunchPromptByTabId,
terminalLayoutsByTabId: nextLayouts,
deleteStateByWorktreeId: nextDeleteState,
baseStatusByWorktreeId: (() => {