diff --git a/apps/desktop/src/renderer/lib/preset-icon-key.test.ts b/apps/desktop/src/renderer/lib/preset-icon-key.test.ts index 7ba5a50bbd3..1700e264556 100644 --- a/apps/desktop/src/renderer/lib/preset-icon-key.test.ts +++ b/apps/desktop/src/renderer/lib/preset-icon-key.test.ts @@ -20,6 +20,7 @@ function createAgent( env: {}, order: 0, ...rest, + forkArgs: rest.forkArgs ?? [], }; } diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalPaneHeaderExtras/TerminalPaneHeaderExtras.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalPaneHeaderExtras/TerminalPaneHeaderExtras.tsx index c5016339209..3697e9e037f 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalPaneHeaderExtras/TerminalPaneHeaderExtras.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalPaneHeaderExtras/TerminalPaneHeaderExtras.tsx @@ -10,11 +10,18 @@ import { import { TerminalConnectionIndicator } from "./components/TerminalConnectionIndicator"; import { TerminalIdCopyMenu } from "./components/TerminalIdCopyMenu"; import { TerminalPageWatchChip } from "./components/TerminalPageWatchChip"; +import { TerminalSessionHandoffMenu } from "./components/TerminalSessionHandoffMenu"; interface TerminalPaneHeaderExtrasProps { workspaceId: string; terminalId: string; terminalInstanceId: string; + onCreateNewAgentSession: (input: { + configId: string; + placement: "split-pane" | "new-tab"; + prompt: string; + forkSessionId?: string; + }) => Promise<{ terminalId: string } | null>; } /** @@ -27,6 +34,7 @@ export function TerminalPaneHeaderExtras({ workspaceId, terminalId, terminalInstanceId, + onCreateNewAgentSession, }: TerminalPaneHeaderExtrasProps) { const { t } = useLingui(); const isOpen = useTerminalRichInputOpen(); @@ -53,6 +61,11 @@ export function TerminalPaneHeaderExtras({ terminalId={terminalId} /> + + + + + + Continue or fork session + + + + + openAction("handoff")}> + + + Continue with another agent… + + + openAction("fork")} + > + + Fork session… + + + + + { + if (!open && !isStarting) setAction(null); + }} + > + + + {title} + + {action === "fork" ? ( + + Create a native provider fork with the same conversation + context. The original session stays unchanged. + + ) : ( + + Start a fresh agent session seeded with this terminal's recent + context. Workspace files remain the source of truth. + + )} + + + +
+ {action === "handoff" ? ( +
+ + ({ + id: config.id, + label: config.label, + iconId: config.presetId, + presetId: config.presetId, + }))} + value={targetConfigId} + placeholder={t({ + id: "workspace.terminalPane.selectAgent", + message: "Select an agent", + })} + onValueChange={setTargetConfigId} + disabled={isStarting || configs.length === 0} + triggerClassName="w-full" + onBeforeConfigureAgents={() => setAction(null)} + /> + {selectedConfig && ( +

+ {transcriptFailed ? ( + + Couldn't read this terminal's context. + + ) : transcript === null ? ( + + Measuring the context to send to {selectedConfig.label}… + + ) : transcript.length === 0 ? ( + + This terminal has no output to hand over yet. + + ) : ( + + Sends {formatNumber(transcript.length)} characters of + terminal context (about{" "} + {formatNumber(estimateTokens(transcript.length))}{" "} + tokens) to {selectedConfig.label}. + + )} +

+ )} +
+ ) : ( +
+ + Provider: {sourceConfig?.label ?? binding.agentId} + +
+ )} + +
+ +
+ + +
+
+
+ + + + + +
+
+ + ); +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalPaneHeaderExtras/components/TerminalSessionHandoffMenu/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalPaneHeaderExtras/components/TerminalSessionHandoffMenu/index.ts new file mode 100644 index 00000000000..12befe3000d --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/components/TerminalPane/components/TerminalPaneHeaderExtras/components/TerminalSessionHandoffMenu/index.ts @@ -0,0 +1 @@ +export { TerminalSessionHandoffMenu } from "./TerminalSessionHandoffMenu"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx index 161cf42bd14..ead197c0eee 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/usePaneRegistry/usePaneRegistry.tsx @@ -198,6 +198,7 @@ export function usePaneRegistry({ configId: string; placement: "split-pane" | "new-tab"; prompt: string; + forkSessionId?: string; }): Promise<{ terminalId: string } | null> => { try { // Host pipeline bakes the prompt into the initialCommand using the @@ -207,6 +208,9 @@ export function usePaneRegistry({ workspaceId, agent: input.configId, prompt: input.prompt, + ...(input.forkSessionId + ? { forkSessionId: input.forkSessionId } + : {}), }); if (result.kind !== "terminal") { toast.error( @@ -464,6 +468,7 @@ export function usePaneRegistry({ workspaceId={workspaceId} terminalId={terminalId} terminalInstanceId={ctx.pane.id} + onCreateNewAgentSession={createNewAgentSession} /> ); }, diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useDefaultV2TerminalPresets/default-v2-terminal-presets.test.ts b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useDefaultV2TerminalPresets/default-v2-terminal-presets.test.ts index cdbbe88ce6b..d75182d50f4 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useDefaultV2TerminalPresets/default-v2-terminal-presets.test.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/components/AgentHooks/hooks/useDefaultV2TerminalPresets/default-v2-terminal-presets.test.ts @@ -22,6 +22,7 @@ function createAgent( env: {}, order: 0, ...rest, + forkArgs: rest.forkArgs ?? [], }; } diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/agents/components/V2AgentsSettings/V2AgentsSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/agents/components/V2AgentsSettings/V2AgentsSettings.tsx index d2d1a853f14..701fa959b7b 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/agents/components/V2AgentsSettings/V2AgentsSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/agents/components/V2AgentsSettings/V2AgentsSettings.tsx @@ -37,6 +37,7 @@ const KNOWN_PRESETS: HostAgentPreset[] = HOST_AGENT_PRESETS.map((preset) => ({ args: [...preset.args], promptArgs: [...preset.promptArgs], resumeArgs: [...preset.resumeArgs], + forkArgs: [...preset.forkArgs], env: { ...preset.env }, })); diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/agents/components/V2AgentsSettings/components/AgentDetail/AgentDetail.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/agents/components/V2AgentsSettings/components/AgentDetail/AgentDetail.tsx index c2c75cf7a0a..abd3f3da593 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/agents/components/V2AgentsSettings/components/AgentDetail/AgentDetail.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/agents/components/V2AgentsSettings/components/AgentDetail/AgentDetail.tsx @@ -93,6 +93,9 @@ export function AgentDetail({ const [resumeArgsText, setResumeArgsText] = useState( joinArgs(config.resumeArgs), ); + const [forkArgsText, setForkArgsText] = useState( + joinArgs(config.forkArgs ?? []), + ); const [promptTransport, setPromptTransport] = useState( config.promptTransport, ); @@ -108,6 +111,7 @@ export function AgentDetail({ ); setPromptArgsText(joinArgs(config.promptArgs)); setResumeArgsText(joinArgs(config.resumeArgs)); + setForkArgsText(joinArgs(config.forkArgs ?? [])); setPromptTransport(config.promptTransport); }, [ config.label, @@ -116,6 +120,7 @@ export function AgentDetail({ config.env, config.promptArgs, config.resumeArgs, + config.forkArgs, config.promptTransport, ]); @@ -252,6 +257,14 @@ export function AgentDetail({ if (changed) updateMutation.mutate({ resumeArgs: args }); }; + const handleForkArgsBlur = () => { + const args = parseArgs(forkArgsText); + const changed = + args.length !== (config.forkArgs ?? []).length || + args.some((arg, i) => arg !== (config.forkArgs ?? [])[i]); + if (changed) updateMutation.mutate({ forkArgs: args }); + }; + const handleTransportChange = (next: PromptTransport) => { if (next === promptTransport) return; const prev = promptTransport; @@ -312,6 +325,9 @@ export function AgentDetail({ resumeArgsText={resumeArgsText} onResumeArgsTextChange={setResumeArgsText} onResumeArgsBlur={handleResumeArgsBlur} + forkArgsText={forkArgsText} + onForkArgsTextChange={setForkArgsText} + onForkArgsBlur={handleForkArgsBlur} promptTransport={promptTransport} onPromptTransportChange={handleTransportChange} /> diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/agents/components/V2AgentsSettings/components/AgentFormControls/AgentFormControls.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/agents/components/V2AgentsSettings/components/AgentFormControls/AgentFormControls.tsx index 53003587fb4..0f31e687a2c 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/agents/components/V2AgentsSettings/components/AgentFormControls/AgentFormControls.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/agents/components/V2AgentsSettings/components/AgentFormControls/AgentFormControls.tsx @@ -1,4 +1,5 @@ import { Trans, useLingui } from "@lingui/react/macro"; +import { FORK_SESSION_ID_TOKEN } from "@superset/shared/agent-definition"; import type { PromptTransport } from "@superset/shared/agent-prompt-launch"; import { Input } from "@superset/ui/input"; import { Label } from "@superset/ui/label"; @@ -51,12 +52,15 @@ interface AgentLaunchFieldsProps { resumeArgsText: string; onResumeArgsTextChange: (value: string) => void; onResumeArgsBlur?: () => void; + forkArgsText: string; + onForkArgsTextChange: (value: string) => void; + onForkArgsBlur?: () => void; promptTransport: PromptTransport; onPromptTransportChange: (next: PromptTransport) => void; } /** - * The "Launch" section (command, prompt-only args, resume args, transport). + * The "Launch" section (command, prompt-only args, resume/fork args, transport). * The edit pane wires the blur callbacks to autosave; the create pane omits * them and reads the controlled values on submit. */ @@ -71,10 +75,17 @@ export function AgentLaunchFields({ resumeArgsText, onResumeArgsTextChange, onResumeArgsBlur, + forkArgsText, + onForkArgsTextChange, + onForkArgsBlur, promptTransport, onPromptTransportChange, }: AgentLaunchFieldsProps) { const { t } = useLingui(); + // Named `sessionId` on purpose: the macro takes the placeholder name from + // this identifier, and an inline "{sessionId}" literal would be inlined + // into the message and then read back as an ICU argument with no value. + const sessionId = FORK_SESSION_ID_TOKEN; return (
+ + Used to clone a previous session without changing it. Put{" "} + {sessionId} where the source id belongs, or leave empty + if the agent cannot fork sessions. + + } + htmlFor={`${idPrefix}-fork-args`} + > + onForkArgsTextChange(e.target.value)} + onBlur={onForkArgsBlur} + placeholder="--resume {sessionId} --fork-session" + /> + +