feat(source-control): send all notes to agent in a new terminal tab (#1568)
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
parent
2fafb9b351
commit
2050fa87a0
|
|
@ -153,16 +153,13 @@ export default function TerminalSettingsScreen() {
|
|||
<Text style={styles.heading}>Terminal</Text>
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<ScrollView contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
|
||||
<Text style={styles.groupHeading}>WHEN YOU LEAVE THE APP</Text>
|
||||
<Text style={styles.groupDescription}>
|
||||
While you're using a terminal on your phone, Orca shrinks it to fit your
|
||||
screen. When you close the app or switch away, this controls whether it stays at
|
||||
phone size (so interactive CLI tools don't reflow) or resizes back to your
|
||||
desktop. You can always tap Restore on the terminal banner to resize it manually.
|
||||
While you're using a terminal on your phone, Orca shrinks it to fit your screen. When
|
||||
you close the app or switch away, this controls whether it stays at phone size (so
|
||||
interactive CLI tools don't reflow) or resizes back to your desktop. You can always
|
||||
tap Restore on the terminal banner to resize it manually.
|
||||
</Text>
|
||||
|
||||
{hosts.length === 0 ? (
|
||||
|
|
|
|||
|
|
@ -742,10 +742,7 @@ describe('mobile subscribe integration', () => {
|
|||
rows: 20
|
||||
})
|
||||
expect(runtime.isMobileSubscriberActive('pty-1'), `iter ${i}: no subscribers`).toBe(false)
|
||||
expect(
|
||||
runtime.getTerminalFitOverride('pty-1'),
|
||||
`iter ${i}: override held`
|
||||
).not.toBeNull()
|
||||
expect(runtime.getTerminalFitOverride('pty-1'), `iter ${i}: override held`).not.toBeNull()
|
||||
|
||||
// Desktop clicks Restore — held-override branch.
|
||||
const ok = await runtime.reclaimTerminalForDesktop('pty-1')
|
||||
|
|
@ -754,10 +751,7 @@ describe('mobile subscribe integration', () => {
|
|||
cols: 150,
|
||||
rows: 40
|
||||
})
|
||||
expect(
|
||||
runtime.getTerminalFitOverride('pty-1'),
|
||||
`iter ${i}: override cleared`
|
||||
).toBeNull()
|
||||
expect(runtime.getTerminalFitOverride('pty-1'), `iter ${i}: override cleared`).toBeNull()
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -778,10 +772,7 @@ describe('mobile subscribe integration', () => {
|
|||
cols: 150,
|
||||
rows: 40
|
||||
})
|
||||
expect(
|
||||
runtime.getTerminalFitOverride('pty-1'),
|
||||
`iter ${i}: override cleared`
|
||||
).toBeNull()
|
||||
expect(runtime.getTerminalFitOverride('pty-1'), `iter ${i}: override cleared`).toBeNull()
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -2631,11 +2631,7 @@ export class OrcaRuntimeService {
|
|||
// hidden tab on desktop, container went 0×0 → 1782×1195) reports
|
||||
// different dims and is the right baseline to remember.
|
||||
const activeOverride = this.terminalFitOverrides.get(ptyId)
|
||||
if (
|
||||
activeOverride &&
|
||||
activeOverride.cols === cols &&
|
||||
activeOverride.rows === rows
|
||||
) {
|
||||
if (activeOverride && activeOverride.cols === cols && activeOverride.rows === rows) {
|
||||
return
|
||||
}
|
||||
this.refreshRendererGeometry(ptyId, cols, rows)
|
||||
|
|
|
|||
|
|
@ -445,7 +445,9 @@
|
|||
border: none;
|
||||
color: var(--muted-foreground);
|
||||
cursor: pointer;
|
||||
transition: background 100ms, color 100ms;
|
||||
transition:
|
||||
background 100ms,
|
||||
color 100ms;
|
||||
}
|
||||
|
||||
.window-controls-btn:hover {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import {
|
|||
GitPullRequestArrow,
|
||||
MessageSquare,
|
||||
Pencil,
|
||||
Send,
|
||||
Trash,
|
||||
TriangleAlert,
|
||||
CircleCheck,
|
||||
|
|
@ -79,6 +80,8 @@ import {
|
|||
} from '@/components/ui/dialog'
|
||||
import { BaseRefPicker } from '@/components/settings/BaseRefPicker'
|
||||
import { formatDiffComment, formatDiffComments } from '@/lib/diff-comments-format'
|
||||
import { QuickLaunchAgentMenuItems } from '@/components/tab-bar/QuickLaunchButton'
|
||||
import { focusTerminalTabSurface } from '@/lib/focus-terminal-tab-surface'
|
||||
import {
|
||||
notifyEditorExternalFileChange,
|
||||
requestEditorSaveQuiesce
|
||||
|
|
@ -180,6 +183,9 @@ function SourceControlInner(): React.JSX.Element {
|
|||
const commitInFlightRef = useRef<Record<string, boolean>>({})
|
||||
const activeWorktree = useActiveWorktree()
|
||||
const activeWorktreeId = useAppStore((s) => s.activeWorktreeId)
|
||||
const activeGroupId = useAppStore((s) =>
|
||||
activeWorktreeId ? s.activeGroupIdByWorktree[activeWorktreeId] : undefined
|
||||
)
|
||||
const worktreeMap = useWorktreeMap()
|
||||
const rightSidebarTab = useAppStore((s) => s.rightSidebarTab)
|
||||
const activeRepo = useRepoById(activeWorktree?.repoId ?? null)
|
||||
|
|
@ -229,6 +235,10 @@ function SourceControlInner(): React.JSX.Element {
|
|||
}
|
||||
return map
|
||||
}, [diffCommentsForActive])
|
||||
const diffCommentsPrompt = useMemo(
|
||||
() => formatDiffComments(diffCommentsForActive),
|
||||
[diffCommentsForActive]
|
||||
)
|
||||
const [diffCommentsExpanded, setDiffCommentsExpanded] = useState(false)
|
||||
const [diffCommentsCopied, setDiffCommentsCopied] = useState(false)
|
||||
|
||||
|
|
@ -236,15 +246,14 @@ function SourceControlInner(): React.JSX.Element {
|
|||
if (diffCommentsForActive.length === 0) {
|
||||
return
|
||||
}
|
||||
const text = formatDiffComments(diffCommentsForActive)
|
||||
try {
|
||||
await window.api.ui.writeClipboardText(text)
|
||||
await window.api.ui.writeClipboardText(diffCommentsPrompt)
|
||||
setDiffCommentsCopied(true)
|
||||
} catch {
|
||||
// Why: swallow — clipboard write can fail when the window isn't focused.
|
||||
// No dedicated error surface is warranted for a best-effort copy action.
|
||||
}
|
||||
}, [diffCommentsForActive])
|
||||
}, [diffCommentsForActive, diffCommentsPrompt])
|
||||
|
||||
// Why: auto-dismiss the "copied" indicator so the button returns to its
|
||||
// default icon after a brief confirmation window.
|
||||
|
|
@ -1268,6 +1277,35 @@ function SourceControlInner(): React.JSX.Element {
|
|||
</span>
|
||||
)}
|
||||
</button>
|
||||
<DropdownMenu>
|
||||
<TooltipProvider delayDuration={400}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex size-6 items-center justify-center rounded text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||
aria-label="Send notes to a new agent"
|
||||
>
|
||||
<Send className="size-3.5" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" sideOffset={6}>
|
||||
Send notes to a new agent
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<DropdownMenuContent align="end" className="min-w-[180px]">
|
||||
<QuickLaunchAgentMenuItems
|
||||
worktreeId={activeWorktreeId}
|
||||
groupId={activeGroupId ?? activeWorktreeId}
|
||||
onFocusTerminal={focusTerminalTabSurface}
|
||||
prompt={diffCommentsPrompt}
|
||||
launchSource="diff_notes_send"
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{diffCommentCount > 0 && (
|
||||
<TooltipProvider delayDuration={400}>
|
||||
<Tooltip>
|
||||
|
|
|
|||
|
|
@ -309,9 +309,9 @@ export function MobilePane(): React.JSX.Element {
|
|||
</div>
|
||||
<p className="text-muted-foreground mb-3 text-xs">
|
||||
While you're using a terminal on your phone, Orca shrinks it to fit your phone
|
||||
screen. When you close the app or switch away, this controls whether it stays at
|
||||
phone size (so interactive CLI tools don't reflow) or resizes back to your
|
||||
desktop. You can always click Restore on the terminal banner to resize it manually.
|
||||
screen. When you close the app or switch away, this controls whether it stays at phone
|
||||
size (so interactive CLI tools don't reflow) or resizes back to your desktop. You can
|
||||
always click Restore on the terminal banner to resize it manually.
|
||||
</p>
|
||||
<Select
|
||||
value={autoRestoreValueFromMs(autoRestoreFitMs)}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import React, { useCallback } from 'react'
|
||||
import { Settings as SettingsIcon } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { DropdownMenuItem, DropdownMenuSeparator } from '@/components/ui/dropdown-menu'
|
||||
import { DropdownMenuItem } from '@/components/ui/dropdown-menu'
|
||||
import { AGENT_CATALOG, AgentIcon } from '@/lib/agent-catalog'
|
||||
import { useAppStore } from '@/store'
|
||||
import { useDetectedAgents } from '@/hooks/useDetectedAgents'
|
||||
import { launchAgentInNewTab } from '@/lib/launch-agent-in-new-tab'
|
||||
import { waitForAgentReady } from '@/lib/agent-ready-wait'
|
||||
import type { TuiAgent } from '../../../../shared/types'
|
||||
import type { LaunchSource } from '../../../../shared/telemetry-events'
|
||||
|
||||
export type QuickLaunchAgentMenuItemsProps = {
|
||||
worktreeId: string
|
||||
|
|
@ -16,6 +17,14 @@ export type QuickLaunchAgentMenuItemsProps = {
|
|||
* Reuses the TabBar's existing double-rAF handoff — this component does
|
||||
* not duplicate the focus logic. */
|
||||
onFocusTerminal: (tabId: string) => void
|
||||
/** Optional initial prompt forwarded to `launchAgentInNewTab`. When set,
|
||||
* the picked agent boots with this prompt — argv/flag agents auto-submit,
|
||||
* followup-path agents land it as a draft for the user to confirm. */
|
||||
prompt?: string
|
||||
/** Telemetry surface for `agent_started.launch_source`. Defaults to
|
||||
* `'tab_bar_quick_launch'` so the existing tab-bar `+` callsite is
|
||||
* unchanged. */
|
||||
launchSource?: LaunchSource
|
||||
}
|
||||
|
||||
function getCatalogEntry(agent: TuiAgent): { id: TuiAgent; label: string } | null {
|
||||
|
|
@ -40,7 +49,9 @@ function orderAgents(
|
|||
function QuickLaunchAgentMenuItemsInner({
|
||||
worktreeId,
|
||||
groupId,
|
||||
onFocusTerminal
|
||||
onFocusTerminal,
|
||||
prompt,
|
||||
launchSource
|
||||
}: QuickLaunchAgentMenuItemsProps): React.JSX.Element | null {
|
||||
// Why: must be a reactive selector (not getConnectionId() which reads a
|
||||
// snapshot via getState()). This ensures the component re-renders when the
|
||||
|
|
@ -69,7 +80,13 @@ function QuickLaunchAgentMenuItemsInner({
|
|||
(agent: TuiAgent) => {
|
||||
const entry = getCatalogEntry(agent)
|
||||
const label = entry?.label ?? agent
|
||||
const result = launchAgentInNewTab({ agent, worktreeId, groupId })
|
||||
const result = launchAgentInNewTab({
|
||||
agent,
|
||||
worktreeId,
|
||||
groupId,
|
||||
...(prompt !== undefined ? { prompt } : {}),
|
||||
...(launchSource !== undefined ? { launchSource } : {})
|
||||
})
|
||||
if (!result) {
|
||||
toast.error(`Could not build launch command for ${label}.`)
|
||||
return
|
||||
|
|
@ -79,7 +96,8 @@ function QuickLaunchAgentMenuItemsInner({
|
|||
// Why: the watchdog guards against "queued startup command never ran" —
|
||||
// e.g. shell failed to spawn. Suppress the toast if the tab has been
|
||||
// closed or the worktree has been navigated away from before the
|
||||
// deadline (see §States: Launch failure handling).
|
||||
// deadline (see §States: Launch failure handling). Bracketed-paste
|
||||
// failures have their own toast in launch-agent-in-new-tab.ts.
|
||||
void waitForAgentReady(result.tabId, result.startupPlan.expectedProcess, {
|
||||
timeoutMs: 5000
|
||||
}).then((ready) => {
|
||||
|
|
@ -99,14 +117,13 @@ function QuickLaunchAgentMenuItemsInner({
|
|||
toast.message(`Couldn't launch ${label} — the terminal is still open.`)
|
||||
})
|
||||
},
|
||||
[worktreeId, groupId, onFocusTerminal]
|
||||
[worktreeId, groupId, onFocusTerminal, prompt, launchSource]
|
||||
)
|
||||
|
||||
const agents = detectedIds ? orderAgents(defaultAgent, detectedIds) : []
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
{agents.length === 0 ? (
|
||||
<DropdownMenuItem
|
||||
disabled
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import {
|
|||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
|
|
@ -557,6 +558,7 @@ function TabBarInner({
|
|||
<DropdownMenuShortcut>{NEW_FILE_SHORTCUT}</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<QuickLaunchAgentMenuItems
|
||||
worktreeId={worktreeId}
|
||||
groupId={resolvedGroupId}
|
||||
|
|
|
|||
|
|
@ -117,6 +117,9 @@ vi.mock('@/components/ui/dropdown-menu', () => ({
|
|||
}) {
|
||||
return { type: 'DropdownMenuItem', props }
|
||||
},
|
||||
DropdownMenuSeparator: function DropdownMenuSeparator() {
|
||||
return { type: 'DropdownMenuSeparator', props: {} }
|
||||
},
|
||||
DropdownMenuShortcut: function DropdownMenuShortcut(props: { children?: unknown }) {
|
||||
return { type: 'DropdownMenuShortcut', props }
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
import { toast } from 'sonner'
|
||||
import { useAppStore } from '@/store'
|
||||
import { buildAgentStartupPlan, type AgentStartupPlan } from '@/lib/tui-agent-startup'
|
||||
import { CLIENT_PLATFORM } from '@/lib/new-workspace'
|
||||
import { reconcileTabOrder } from '@/components/tab-bar/reconcile-order'
|
||||
import { tuiAgentToAgentKind } from '@/lib/telemetry'
|
||||
import { track, tuiAgentToAgentKind } from '@/lib/telemetry'
|
||||
import { pasteDraftWhenAgentReady } from '@/lib/agent-paste-draft'
|
||||
import { TUI_AGENT_CONFIG } from '../../../shared/tui-agent-config'
|
||||
import type { TuiAgent } from '../../../shared/types'
|
||||
import type { LaunchSource } from '../../../shared/telemetry-events'
|
||||
|
||||
export type LaunchAgentInNewTabArgs = {
|
||||
agent: TuiAgent
|
||||
|
|
@ -11,6 +15,13 @@ export type LaunchAgentInNewTabArgs = {
|
|||
/** The tab group the user clicked from. Keeps split-group launches in the
|
||||
* pane the user initiated from instead of falling through to the active group. */
|
||||
groupId?: string
|
||||
/** Optional initial prompt. When non-empty, dispatched per the agent's
|
||||
* `promptInjectionMode`: argv/flag agents auto-submit via the launch
|
||||
* command; followup-path agents land the prompt as an unsent draft. */
|
||||
prompt?: string
|
||||
/** Telemetry surface that initiated this launch. Defaults to the tab-bar
|
||||
* quick-launch entry point so existing callers stay unchanged. */
|
||||
launchSource?: LaunchSource
|
||||
}
|
||||
|
||||
export type LaunchAgentInNewTabResult = {
|
||||
|
|
@ -19,34 +30,65 @@ export type LaunchAgentInNewTabResult = {
|
|||
} | null
|
||||
|
||||
/**
|
||||
* Create a new terminal tab and queue the agent's empty-prompt launch command.
|
||||
* Create a new terminal tab and queue the agent's launch command, optionally
|
||||
* with an initial prompt.
|
||||
*
|
||||
* Why: this is the single entry point for "launch agent X in a new tab" from
|
||||
* the tab-bar quick-launch menu. It mirrors the `+` button's path
|
||||
* (`createNewTerminalTab`) — createTab, flip `activeTabType` to terminal, and
|
||||
* persist the appended tab-bar order — then queues the empty-prompt agent
|
||||
* startup through the same `pendingStartupByTabId` channel the
|
||||
* new-workspace ("cmd+N") flow uses. TerminalPane consumes the queued command
|
||||
* on first mount and the local PTY provider writes it once the shell is ready
|
||||
* (see `pty-connection.ts`: startup-command path), so the CLI boots in exactly
|
||||
* the same way as a composer-initiated launch.
|
||||
* the tab-bar quick-launch menu and the Source Control "send notes to agent"
|
||||
* action. It mirrors the `+` button's path (`createNewTerminalTab`) — createTab,
|
||||
* flip `activeTabType` to terminal, and persist the appended tab-bar order —
|
||||
* then queues the agent startup through the same `pendingStartupByTabId`
|
||||
* channel the new-workspace ("cmd+N") flow uses. TerminalPane consumes the
|
||||
* queued command on first mount and the local PTY provider writes it once the
|
||||
* shell is ready (see `pty-connection.ts`: startup-command path).
|
||||
*
|
||||
* Returns `null` when `buildAgentStartupPlan` cannot produce a plan (should
|
||||
* not happen with `allowEmptyPromptLaunch: true` but guarded for safety).
|
||||
* Submission mode by `promptInjectionMode`: argv/flag agents include the
|
||||
* prompt directly in the launch command (auto-submit, atomic via the shell);
|
||||
* followup-path agents have no argv prompt slot, so we launch empty-prompt
|
||||
* and bracketed-paste the prompt as an unsent draft once the agent's input
|
||||
* box is ready.
|
||||
*
|
||||
* Returns `null` when no startup plan can be built — for example, a whitespace-
|
||||
* only prompt on the trim-empty branch of `buildAgentStartupPlan`. Callers
|
||||
* surface that as a launch failure (see `QuickLaunchButton.runLaunch`).
|
||||
*/
|
||||
export function launchAgentInNewTab(args: LaunchAgentInNewTabArgs): LaunchAgentInNewTabResult {
|
||||
const { agent, worktreeId, groupId } = args
|
||||
const { agent, worktreeId, groupId, prompt, launchSource } = args
|
||||
const store = useAppStore.getState()
|
||||
const cmdOverrides = store.settings?.agentCmdOverrides ?? {}
|
||||
const trimmedPrompt = prompt?.trim() ?? ''
|
||||
const hasPrompt = trimmedPrompt.length > 0
|
||||
const isFollowupPath = TUI_AGENT_CONFIG[agent].promptInjectionMode === 'stdin-after-start'
|
||||
|
||||
// Why: argv/flag agents fold the prompt into the launch command and
|
||||
// auto-submit — keeping behavior consistent with the composer/tab-bar `+`
|
||||
// mental model, where the prompt is "the first turn the user sent".
|
||||
// Followup-path agents have no argv prompt slot, so the only way to
|
||||
// deliver a prompt is post-launch bracketed paste; we leave it as an
|
||||
// unsent draft so the user confirms before sending (avoids the typed-`\r`
|
||||
// race if readiness detection misses).
|
||||
let startupPlan: AgentStartupPlan | null = null
|
||||
let pasteDraftAfterLaunch: string | null = null
|
||||
|
||||
if (hasPrompt && isFollowupPath) {
|
||||
startupPlan = buildAgentStartupPlan({
|
||||
agent,
|
||||
prompt: '',
|
||||
cmdOverrides,
|
||||
platform: CLIENT_PLATFORM,
|
||||
allowEmptyPromptLaunch: true
|
||||
})
|
||||
pasteDraftAfterLaunch = trimmedPrompt
|
||||
} else {
|
||||
startupPlan = buildAgentStartupPlan({
|
||||
agent,
|
||||
prompt: hasPrompt ? trimmedPrompt : '',
|
||||
cmdOverrides,
|
||||
platform: CLIENT_PLATFORM,
|
||||
allowEmptyPromptLaunch: !hasPrompt
|
||||
})
|
||||
}
|
||||
|
||||
// Why: empty-prompt launch is the whole point of quick-launch — the user
|
||||
// just wants to get into the agent's input box with no prefilled prompt.
|
||||
const startupPlan = buildAgentStartupPlan({
|
||||
agent,
|
||||
prompt: '',
|
||||
cmdOverrides: store.settings?.agentCmdOverrides ?? {},
|
||||
platform: CLIENT_PLATFORM,
|
||||
allowEmptyPromptLaunch: true
|
||||
})
|
||||
if (!startupPlan) {
|
||||
return null
|
||||
}
|
||||
|
|
@ -60,17 +102,60 @@ export function launchAgentInNewTab(args: LaunchAgentInNewTabArgs): LaunchAgentI
|
|||
// The telemetry payload is threaded through the queue → pty-connection →
|
||||
// pty-transport → pty:spawn IPC → main, where main fires `agent_started`
|
||||
// only after the spawn succeeds. `request_kind: 'new'` because
|
||||
// quick-launch always opens a fresh empty-prompt session.
|
||||
// quick-launch always opens a fresh session.
|
||||
const tab = store.createTab(worktreeId, groupId)
|
||||
store.queueTabStartupCommand(tab.id, {
|
||||
command: startupPlan.launchCommand,
|
||||
...(startupPlan.env ? { env: startupPlan.env } : {}),
|
||||
telemetry: {
|
||||
agent_kind: tuiAgentToAgentKind(agent),
|
||||
launch_source: 'tab_bar_quick_launch',
|
||||
launch_source: launchSource ?? 'tab_bar_quick_launch',
|
||||
request_kind: 'new'
|
||||
}
|
||||
})
|
||||
|
||||
// Why: schedule the bracketed-paste-after-ready follow-up immediately after
|
||||
// the startup command is queued. Fire-and-forget so callers keep their
|
||||
// synchronous `{ tabId, startupPlan }` signature. The helper short-circuits
|
||||
// for agents with a `draftPromptFlag`, so calling it on the followup path
|
||||
// is safe even when the draft was already injected via the native flag.
|
||||
if (pasteDraftAfterLaunch !== null) {
|
||||
// Why: surface silent paste failures — without onTimeout, a stalled agent
|
||||
// readiness wait drops the user's notes with no feedback. Suppress when
|
||||
// the user closed the tab or switched worktrees so the toast/telemetry
|
||||
// don't fire for user-initiated cancellation (mirrors the 5s launch
|
||||
// watchdog in QuickLaunchButton).
|
||||
const tabId = tab.id
|
||||
void pasteDraftWhenAgentReady({
|
||||
tabId,
|
||||
content: pasteDraftAfterLaunch,
|
||||
agent,
|
||||
onTimeout: () => {
|
||||
const state = useAppStore.getState()
|
||||
const tabsForWorktree = state.tabsByWorktree[worktreeId] ?? []
|
||||
const tab = tabsForWorktree.find((t) => t.id === tabId)
|
||||
// Why: if the PTY never spawned, QuickLaunch's 5s watchdog already
|
||||
// surfaced the launch failure. Don't double-toast for the same root
|
||||
// cause. Looking up directly in `worktreeId` (not scanning every
|
||||
// worktree) also preserves "still in this worktree" intent.
|
||||
if (!tab) {
|
||||
return // tab closed by user
|
||||
}
|
||||
if (tab.ptyId === null) {
|
||||
return // launch failed; QuickLaunch handled the user-facing toast
|
||||
}
|
||||
if (state.activeWorktreeId !== worktreeId) {
|
||||
return
|
||||
}
|
||||
toast.message("Your notes weren't sent — paste them once the agent is ready.")
|
||||
track('agent_error', {
|
||||
error_class: 'paste_readiness_timeout',
|
||||
agent_kind: tuiAgentToAgentKind(agent)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Why: match the `+` button's `createNewTerminalTab` sequence — without
|
||||
// `setActiveTabType('terminal')`, a worktree currently showing an editor
|
||||
// file keeps rendering the editor and the new terminal tab stays invisible.
|
||||
|
|
|
|||
|
|
@ -54,20 +54,23 @@ export const AGENT_KIND_VALUES = [
|
|||
export const agentKindSchema = z.enum(AGENT_KIND_VALUES)
|
||||
export type AgentKind = z.infer<typeof agentKindSchema>
|
||||
|
||||
// Trimmed to the two values Orca's PTY-typed-command launch architecture can
|
||||
// actually emit:
|
||||
// Trimmed to a small set of values Orca's PTY-typed-command launch architecture
|
||||
// can emit:
|
||||
// - `binary_not_found` — `provider.spawn` ENOENT (the *shell* binary is
|
||||
// missing). The agent CLI being missing is invisible: Orca spawns a
|
||||
// healthy shell and types the command, and bash/zsh's "command not found"
|
||||
// surfaces only as terminal output.
|
||||
// - `unknown` — every other thrown error (paste-readiness timeout, env-build
|
||||
// failures, unclassifiable shell-spawn errors).
|
||||
// - `paste_readiness_timeout` — bracketed-paste readiness wait timed out.
|
||||
// The agent process spawned but its TUI input box didn't reach a ready
|
||||
// state before the watchdog deadline, so the queued draft was dropped.
|
||||
// - `unknown` — every other thrown error (env-build failures,
|
||||
// unclassifiable shell-spawn errors).
|
||||
// Provider-side errors (`auth_expired`, `rate_limited`, `network_timeout`,
|
||||
// `provider_*`) happen inside the agent CLI subprocess and are not observable
|
||||
// to Orca — see telemetry-plan.md §Decision: Defer per-incident error fields.
|
||||
// Adding a new value is additive-safe; do it when the call site lands, not in
|
||||
// anticipation.
|
||||
export const errorClassSchema = z.enum(['binary_not_found', 'unknown'])
|
||||
export const errorClassSchema = z.enum(['binary_not_found', 'paste_readiness_timeout', 'unknown'])
|
||||
export type ErrorClass = z.infer<typeof errorClassSchema>
|
||||
|
||||
export const repoMethodSchema = z.enum(['folder_picker', 'clone_url', 'drag_drop'])
|
||||
|
|
@ -117,6 +120,7 @@ export const launchSourceSchema = z.enum([
|
|||
'new_workspace_composer',
|
||||
'workspace_jump_palette',
|
||||
'shortcut',
|
||||
'diff_notes_send',
|
||||
'unknown'
|
||||
])
|
||||
export type LaunchSource = z.infer<typeof launchSourceSchema>
|
||||
|
|
|
|||
Loading…
Reference in New Issue