@deepseek-harness-tui/dsh-tui 0.10.2 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +171 -124
- package/bin/dsh-tui.js +748 -22
- package/cordis.patch.yml +38 -10
- package/cordis.yml +2 -0
- package/lib/types/adapter/channel/session-projection.d.ts.map +1 -1
- package/lib/types/adapter/channel/session-projection.js +2 -0
- package/lib/types/adapter/channel/ui-policy.d.ts +4 -1
- package/lib/types/adapter/channel/ui-policy.d.ts.map +1 -1
- package/lib/types/adapter/channel/ui-policy.js +5 -1
- package/lib/types/adapter/kernel/host-facade.d.ts.map +1 -1
- package/lib/types/adapter/kernel/host-facade.js +1 -0
- package/lib/types/adapter/kernel/replay.d.ts.map +1 -1
- package/lib/types/adapter/kernel/replay.js +3 -0
- package/lib/types/adapter/kernel/runtime.d.ts.map +1 -1
- package/lib/types/adapter/kernel/runtime.js +2 -0
- package/lib/types/adapter/kernel/slices/workspace.d.ts.map +1 -1
- package/lib/types/adapter/kernel/slices/workspace.js +1 -0
- package/lib/types/adapter/ports/channel-ui.d.ts +22 -2
- package/lib/types/adapter/ports/channel-ui.d.ts.map +1 -1
- package/lib/types/adapter/ports/channel-view.d.ts +66 -0
- package/lib/types/adapter/ports/channel-view.d.ts.map +1 -1
- package/lib/types/adapter/ports/channel-workspace.d.ts +23 -0
- package/lib/types/adapter/ports/channel-workspace.d.ts.map +1 -1
- package/lib/types/adapter/ports/workspace.d.ts +2 -0
- package/lib/types/adapter/ports/workspace.d.ts.map +1 -1
- package/lib/types/adapter/upstream/workspace-driver.d.ts.map +1 -1
- package/lib/types/adapter/upstream/workspace-driver.js +10 -2
- package/lib/types/commands.d.ts.map +1 -1
- package/lib/types/commands.js +1 -0
- package/lib/types/components/ContextBarView.d.ts +15 -7
- package/lib/types/components/ContextBarView.d.ts.map +1 -1
- package/lib/types/components/ContextBarView.js +26 -22
- package/lib/types/components/ImagePreviewOverlay.d.ts.map +1 -1
- package/lib/types/components/ImagePreviewOverlay.js +11 -5
- package/lib/types/components/Markdown.d.ts +11 -1
- package/lib/types/components/Markdown.d.ts.map +1 -1
- package/lib/types/components/Markdown.js +20 -2
- package/lib/types/components/MermaidDiagram.d.ts +13 -0
- package/lib/types/components/MermaidDiagram.d.ts.map +1 -0
- package/lib/types/components/MermaidDiagram.js +61 -0
- package/lib/types/components/MessageList.d.ts +26 -1
- package/lib/types/components/MessageList.d.ts.map +1 -1
- package/lib/types/components/MessageList.js +72 -7
- package/lib/types/components/PageMargin.d.ts +1 -1
- package/lib/types/components/PromptInput.d.ts +37 -1
- package/lib/types/components/PromptInput.d.ts.map +1 -1
- package/lib/types/components/PromptInput.js +243 -28
- package/lib/types/components/ScrollbarGutter.d.ts +20 -5
- package/lib/types/components/ScrollbarGutter.d.ts.map +1 -1
- package/lib/types/components/ScrollbarGutter.js +88 -10
- package/lib/types/components/StreamingMarkdown.d.ts.map +1 -1
- package/lib/types/components/StreamingMarkdown.js +30 -28
- package/lib/types/components/Tooltip.d.ts.map +1 -1
- package/lib/types/components/Tooltip.js +45 -7
- package/lib/types/components/messages/UserPromptMessage.d.ts.map +1 -1
- package/lib/types/components/messages/UserPromptMessage.js +5 -7
- package/lib/types/components/messages/useImageInspection.d.ts.map +1 -1
- package/lib/types/components/messages/useImageInspection.js +19 -9
- package/lib/types/components/promptDraftCache.d.ts +102 -0
- package/lib/types/components/promptDraftCache.d.ts.map +1 -0
- package/lib/types/components/promptDraftCache.js +76 -0
- package/lib/types/components/questions/AskUserQuestionPanel.d.ts +17 -1
- package/lib/types/components/questions/AskUserQuestionPanel.d.ts.map +1 -1
- package/lib/types/components/questions/AskUserQuestionPanel.js +40 -7
- package/lib/types/components/questions/QuestionMinimizedBar.d.ts +12 -0
- package/lib/types/components/questions/QuestionMinimizedBar.d.ts.map +1 -0
- package/lib/types/components/questions/QuestionMinimizedBar.js +43 -0
- package/lib/types/components/sessions/SessionListRow.d.ts +23 -1
- package/lib/types/components/sessions/SessionListRow.d.ts.map +1 -1
- package/lib/types/components/sessions/SessionListRow.js +52 -5
- package/lib/types/components/workspaces/HomeWorkspaceRow.d.ts +49 -0
- package/lib/types/components/workspaces/HomeWorkspaceRow.d.ts.map +1 -0
- package/lib/types/components/workspaces/HomeWorkspaceRow.js +50 -0
- package/lib/types/dsh-adapter/bundled-presets.d.ts +10 -0
- package/lib/types/dsh-adapter/bundled-presets.d.ts.map +1 -0
- package/lib/types/dsh-adapter/bundled-presets.js +80 -0
- package/lib/types/dsh-adapter/channel/action-readiness.d.ts +1 -1
- package/lib/types/dsh-adapter/channel/action-readiness.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/action-readiness.js +3 -0
- package/lib/types/dsh-adapter/channel/agent-view-projection.d.ts +1 -1
- package/lib/types/dsh-adapter/channel/agent-view-projection.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/agent-view-projection.js +19 -2
- package/lib/types/dsh-adapter/channel/background-action.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/background-action.js +52 -29
- package/lib/types/dsh-adapter/channel/binding.d.ts +19 -2
- package/lib/types/dsh-adapter/channel/binding.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/binding.js +78 -18
- package/lib/types/dsh-adapter/channel/composer-images.d.ts +5 -0
- package/lib/types/dsh-adapter/channel/composer-images.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/composer-images.js +105 -6
- package/lib/types/dsh-adapter/channel/context-bookkeeping.d.ts +8 -0
- package/lib/types/dsh-adapter/channel/context-bookkeeping.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/context-bookkeeping.js +3 -2
- package/lib/types/dsh-adapter/channel/ide-selection.d.ts +58 -0
- package/lib/types/dsh-adapter/channel/ide-selection.d.ts.map +1 -0
- package/lib/types/dsh-adapter/channel/ide-selection.js +226 -0
- package/lib/types/dsh-adapter/channel/input-delivery.d.ts +6 -1
- package/lib/types/dsh-adapter/channel/input-delivery.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/input-delivery.js +9 -1
- package/lib/types/dsh-adapter/channel/local-actions.d.ts +3 -19
- package/lib/types/dsh-adapter/channel/local-actions.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/local-actions.js +2 -1
- package/lib/types/dsh-adapter/channel/model-actions.d.ts +4 -1
- package/lib/types/dsh-adapter/channel/model-actions.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/model-actions.js +82 -6
- package/lib/types/dsh-adapter/channel/model-switch.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/model-switch.js +49 -31
- package/lib/types/dsh-adapter/channel/projection.d.ts +4 -0
- package/lib/types/dsh-adapter/channel/projection.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/projection.js +29 -19
- package/lib/types/dsh-adapter/channel/reports.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/reports.js +4 -12
- package/lib/types/dsh-adapter/channel/session-fork.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/session-fork.js +26 -2
- package/lib/types/dsh-adapter/channel/session-live-adoption.d.ts +3 -0
- package/lib/types/dsh-adapter/channel/session-live-adoption.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/session-live-adoption.js +1 -0
- package/lib/types/dsh-adapter/channel/session-metadata.d.ts +10 -0
- package/lib/types/dsh-adapter/channel/session-metadata.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/session-metadata.js +2 -2
- package/lib/types/dsh-adapter/channel/session-resume.d.ts +11 -2
- package/lib/types/dsh-adapter/channel/session-resume.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/session-resume.js +291 -94
- package/lib/types/dsh-adapter/channel/session-rewind.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/session-rewind.js +34 -16
- package/lib/types/dsh-adapter/channel/session-tree-actions.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/session-tree-actions.js +19 -3
- package/lib/types/dsh-adapter/channel/settings-host.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/settings-host.js +4 -3
- package/lib/types/dsh-adapter/channel/state.d.ts +1 -1
- package/lib/types/dsh-adapter/channel/state.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/state.js +1 -1
- package/lib/types/dsh-adapter/channel/transcript.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/transcript.js +6 -12
- package/lib/types/dsh-adapter/channel/types.d.ts +6 -1
- package/lib/types/dsh-adapter/channel/types.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/workspace-actions.d.ts +4 -1
- package/lib/types/dsh-adapter/channel/workspace-actions.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/workspace-actions.js +41 -1
- package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel.js +67 -6
- package/lib/types/dsh-adapter/compat/messages.d.ts +15 -0
- package/lib/types/dsh-adapter/compat/messages.d.ts.map +1 -0
- package/lib/types/dsh-adapter/compat/messages.js +15 -0
- package/lib/types/dsh-adapter/compat/settings.d.ts +33 -0
- package/lib/types/dsh-adapter/compat/settings.d.ts.map +1 -0
- package/lib/types/dsh-adapter/compat/settings.js +43 -0
- package/lib/types/dsh-adapter/compat/shell.d.ts +25 -0
- package/lib/types/dsh-adapter/compat/shell.d.ts.map +1 -0
- package/lib/types/dsh-adapter/compat/shell.js +11 -0
- package/lib/types/dsh-adapter/contract.d.ts +4 -4
- package/lib/types/dsh-adapter/contract.d.ts.map +1 -1
- package/lib/types/dsh-adapter/contract.js +15 -6
- package/lib/types/dsh-adapter/ide-channel.d.ts +224 -0
- package/lib/types/dsh-adapter/ide-channel.d.ts.map +1 -0
- package/lib/types/dsh-adapter/ide-channel.js +556 -0
- package/lib/types/dsh-adapter/index.d.ts +18 -3
- package/lib/types/dsh-adapter/index.d.ts.map +1 -1
- package/lib/types/dsh-adapter/index.js +32 -5
- package/lib/types/dsh-adapter/plugin.d.ts +5 -2
- package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
- package/lib/types/dsh-adapter/plugin.js +173 -42
- package/lib/types/dsh-adapter/preset-resolution.d.ts +8 -0
- package/lib/types/dsh-adapter/preset-resolution.d.ts.map +1 -1
- package/lib/types/dsh-adapter/preset-resolution.js +10 -0
- package/lib/types/dsh-adapter/session-mount-heartbeat.d.ts +53 -0
- package/lib/types/dsh-adapter/session-mount-heartbeat.d.ts.map +1 -0
- package/lib/types/dsh-adapter/session-mount-heartbeat.js +100 -0
- package/lib/types/dsh-adapter/sessionTree.d.ts.map +1 -1
- package/lib/types/dsh-adapter/sessionTree.js +3 -2
- package/lib/types/dsh-adapter/subagents.d.ts.map +1 -1
- package/lib/types/dsh-adapter/subagents.js +6 -5
- package/lib/types/dsh-adapter/workspaces.d.ts +46 -3
- package/lib/types/dsh-adapter/workspaces.d.ts.map +1 -1
- package/lib/types/dsh-adapter/workspaces.js +116 -0
- package/lib/types/effortPrefs.d.ts +15 -3
- package/lib/types/effortPrefs.d.ts.map +1 -1
- package/lib/types/effortPrefs.js +37 -3
- package/lib/types/homePrefs.d.ts +15 -0
- package/lib/types/homePrefs.d.ts.map +1 -0
- package/lib/types/homePrefs.js +53 -0
- package/lib/types/i18n.d.ts +257 -296
- package/lib/types/i18n.d.ts.map +1 -1
- package/lib/types/i18n.js +78 -84
- package/lib/types/ink/colorize.d.ts +10 -0
- package/lib/types/ink/colorize.d.ts.map +1 -1
- package/lib/types/ink/colorize.js +21 -0
- package/lib/types/ink/components/ScrollBox.d.ts.map +1 -1
- package/lib/types/ink/components/ScrollBox.js +12 -1
- package/lib/types/ink/dom.d.ts +1 -0
- package/lib/types/ink/dom.d.ts.map +1 -1
- package/lib/types/ink/hooks/use-copy-on-select.d.ts +5 -1
- package/lib/types/ink/hooks/use-copy-on-select.d.ts.map +1 -1
- package/lib/types/ink/hooks/use-copy-on-select.js +15 -1
- package/lib/types/ink/ink.d.ts.map +1 -1
- package/lib/types/ink/ink.js +36 -1
- package/lib/types/ink/output.d.ts +10 -1
- package/lib/types/ink/output.d.ts.map +1 -1
- package/lib/types/ink/output.js +109 -17
- package/lib/types/ink/render-border.d.ts +6 -1
- package/lib/types/ink/render-border.d.ts.map +1 -1
- package/lib/types/ink/render-border.js +25 -9
- package/lib/types/ink/render-node-to-output.d.ts.map +1 -1
- package/lib/types/ink/render-node-to-output.js +48 -6
- package/lib/types/ink/renderer.d.ts +6 -0
- package/lib/types/ink/renderer.d.ts.map +1 -1
- package/lib/types/ink/renderer.js +2 -1
- package/lib/types/ink/selection.d.ts +60 -0
- package/lib/types/ink/selection.d.ts.map +1 -1
- package/lib/types/ink/selection.js +129 -0
- package/lib/types/ink/sixel-graphics.d.ts +19 -1
- package/lib/types/ink/sixel-graphics.d.ts.map +1 -1
- package/lib/types/ink/sixel-graphics.js +124 -12
- package/lib/types/ink/terminal-image.d.ts +18 -1
- package/lib/types/ink/terminal-image.d.ts.map +1 -1
- package/lib/types/ink/terminal.d.ts +17 -0
- package/lib/types/ink/terminal.d.ts.map +1 -1
- package/lib/types/ink/terminal.js +20 -1
- package/lib/types/screens/Chat.d.ts +17 -1
- package/lib/types/screens/Chat.d.ts.map +1 -1
- package/lib/types/screens/Chat.js +284 -72
- package/lib/types/screens/SessionSupervisor.d.ts +84 -0
- package/lib/types/screens/SessionSupervisor.d.ts.map +1 -0
- package/lib/types/screens/SessionSupervisor.js +350 -0
- package/lib/types/screens/SessionTree.js +2 -2
- package/lib/types/screens/Settings.d.ts.map +1 -1
- package/lib/types/screens/Settings.js +32 -9
- package/lib/types/screens/StatusLine.d.ts +13 -0
- package/lib/types/screens/StatusLine.d.ts.map +1 -1
- package/lib/types/screens/StatusLine.js +36 -17
- package/lib/types/screens/StatusMetrics.d.ts +79 -22
- package/lib/types/screens/StatusMetrics.d.ts.map +1 -1
- package/lib/types/screens/StatusMetrics.js +153 -88
- package/lib/types/screens/sessionSupervisor/model.d.ts +83 -0
- package/lib/types/screens/sessionSupervisor/model.d.ts.map +1 -0
- package/lib/types/screens/sessionSupervisor/model.js +69 -0
- package/lib/types/screens/sessionSupervisor/useSessionSupervisor.d.ts +120 -0
- package/lib/types/screens/sessionSupervisor/useSessionSupervisor.d.ts.map +1 -0
- package/lib/types/screens/sessionSupervisor/useSessionSupervisor.js +593 -0
- package/lib/types/sessionMounts.d.ts +272 -0
- package/lib/types/sessionMounts.d.ts.map +1 -0
- package/lib/types/sessionMounts.js +706 -0
- package/lib/types/sessions/format.d.ts.map +1 -1
- package/lib/types/sessions/format.js +4 -1
- package/lib/types/sessions/resumeFailure.d.ts +38 -0
- package/lib/types/sessions/resumeFailure.d.ts.map +1 -0
- package/lib/types/sessions/resumeFailure.js +58 -0
- package/lib/types/terminal-utils/mermaid.d.ts +44 -0
- package/lib/types/terminal-utils/mermaid.d.ts.map +1 -0
- package/lib/types/terminal-utils/mermaid.js +83 -0
- package/lib/types/tips.d.ts.map +1 -1
- package/lib/types/tips.js +175 -19
- package/lib/types/tuiDisplayPrefs.d.ts +7 -9
- package/lib/types/tuiDisplayPrefs.d.ts.map +1 -1
- package/lib/types/tuiDisplayPrefs.js +31 -28
- package/lib/types/update.d.ts +61 -0
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +194 -0
- package/lib/types/utils/imageResize.d.ts +90 -0
- package/lib/types/utils/imageResize.d.ts.map +1 -0
- package/lib/types/utils/imageResize.js +272 -0
- package/lib/types/utils/keymap.d.ts +5 -1
- package/lib/types/utils/keymap.d.ts.map +1 -1
- package/lib/types/utils/keymap.js +23 -0
- package/lib/types/workspaces.d.ts +2 -1
- package/lib/types/workspaces.d.ts.map +1 -1
- package/lib/types/workspaces.js +1 -1
- package/node_modules/@deepseek-harness-tui/dsh-auth/package.json +10 -10
- package/package.json +120 -103
- package/presets/liangshen/.dsh-tui-managed.json +1 -1
- package/presets/liangshen/agent.cordis.yml +15 -0
- package/lib/types/screens/AgentView.d.ts +0 -23
- package/lib/types/screens/AgentView.d.ts.map +0 -1
- package/lib/types/screens/AgentView.js +0 -546
- package/lib/types/screens/SessionBrowser.d.ts +0 -42
- package/lib/types/screens/SessionBrowser.d.ts.map +0 -1
- package/lib/types/screens/SessionBrowser.js +0 -911
|
@@ -63,6 +63,16 @@ export function isProgressReportingAvailable() {
|
|
|
63
63
|
export function isJetBrainsIdeTerminal() {
|
|
64
64
|
return process.env.TERMINAL_EMULATOR === 'JetBrains-JediTerm';
|
|
65
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Detects zellij, which multiplexes the outer terminal instead of emulating
|
|
68
|
+
* it. Capability probes therefore describe the *outer* terminal — zellij even
|
|
69
|
+
* forwards KITTY_WINDOW_ID into the pane, so kitty detection matches — while
|
|
70
|
+
* zellij's own grid decides how control sequences actually apply.
|
|
71
|
+
* @returns true when the process runs inside a zellij pane.
|
|
72
|
+
*/
|
|
73
|
+
export function isZellij() {
|
|
74
|
+
return !!process.env.ZELLIJ;
|
|
75
|
+
}
|
|
66
76
|
/**
|
|
67
77
|
* Checks if the terminal supports DEC mode 2026 (synchronized output).
|
|
68
78
|
* When supported, BSU/ESU sequences prevent visible flicker during redraws.
|
|
@@ -272,10 +282,19 @@ export const SYNC_OUTPUT_SUPPORTED = isSynchronizedOutputSupported();
|
|
|
272
282
|
* as content scrolls (the "JetBrains terminal slowly garbles" bug class).
|
|
273
283
|
* The diff engine falls back to repainting the shifted rows cell-by-cell,
|
|
274
284
|
* which every terminal renders identically. Disable DECSTBM on JetBrains terminals.
|
|
285
|
+
*
|
|
286
|
+
* zellij is excluded for the same class of reason. Its `CSI T`
|
|
287
|
+
* (grid.rs rotate_scroll_region_up) shifts rows only while the cursor sits
|
|
288
|
+
* inside the scroll region, and its `CSI r` does not home the cursor outside
|
|
289
|
+
* origin mode — but the renderer parks the cursor on the last screen row,
|
|
290
|
+
* below every ScrollBox. The shift is silently dropped while the diff assumes
|
|
291
|
+
* it happened, so stale rows survive every scroll-up frame (tearing, leftover
|
|
292
|
+
* transcript lines). Only DECSTBM is withdrawn: zellij does implement DEC
|
|
293
|
+
* 2026, so BSU/ESU stay on and frames remain atomic.
|
|
275
294
|
* @returns true when DECSTBM scroll optimization is safe on this terminal.
|
|
276
295
|
*/
|
|
277
296
|
export function isDecstbmSafe() {
|
|
278
|
-
return SYNC_OUTPUT_SUPPORTED && !isJetBrainsIdeTerminal();
|
|
297
|
+
return SYNC_OUTPUT_SUPPORTED && !isJetBrainsIdeTerminal() && !isZellij();
|
|
279
298
|
}
|
|
280
299
|
/**
|
|
281
300
|
* Render forensics: when DSH_TUI_RENDER_LOG names a file path, every painted
|
|
@@ -6,8 +6,9 @@ import { TuiStatusStore } from '../dsh-adapter/status.js';
|
|
|
6
6
|
import type { TuiShortcutHost } from '../dsh-adapter/shortcuts.js';
|
|
7
7
|
import type { TuiThemeHost } from '../dsh-adapter/themes.js';
|
|
8
8
|
import { ApprovalStore } from '../dsh-adapter/approvals.js';
|
|
9
|
+
import { type PromptController } from '../components/PromptInput.js';
|
|
9
10
|
import type { InjectController } from '../dsh-adapter/inject-channel.js';
|
|
10
|
-
export declare function Chat({ channel, questionStore, approvalStore, extensionDialogs, extensionStatus, extensionShortcuts, themeHost, onExit, onUpdate, onRestart, fullscreen, trajectorySeen: trajectorySeenProp, injectControllerRef, renderScene, }: {
|
|
11
|
+
export declare function Chat({ channel, questionStore, approvalStore, extensionDialogs, extensionStatus, extensionShortcuts, themeHost, onExit, onUpdate, onRestart, fullscreen, trajectorySeen: trajectorySeenProp, injectControllerRef, promptControllerRef: promptControllerRefProp, renderScene, openHomeOnBoot, }: {
|
|
11
12
|
channel: Channel;
|
|
12
13
|
renderScene?: (id: string, channel: Channel) => React.ReactNode;
|
|
13
14
|
questionStore: QuestionStore;
|
|
@@ -59,5 +60,20 @@ export declare function Chat({ channel, questionStore, approvalStore, extensionD
|
|
|
59
60
|
* open the channel (headless scripts, bare embeds).
|
|
60
61
|
*/
|
|
61
62
|
injectControllerRef?: React.RefObject<InjectController | null>;
|
|
63
|
+
/**
|
|
64
|
+
* Show the workspace home screen as this session's first frame.
|
|
65
|
+
*
|
|
66
|
+
* A prop rather than a filesystem read inside the component: the host knows
|
|
67
|
+
* whether this launch was an ordinary one (no `--resume`, no workspace
|
|
68
|
+
* target) and whether the home screen has already been shown on this
|
|
69
|
+
* installation, and tests need it deterministic.
|
|
70
|
+
*/
|
|
71
|
+
openHomeOnBoot?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* The composer's live controller, published every render. Exposed as a prop
|
|
74
|
+
* so a regression can read the draft the composer HOLDS — the ownership
|
|
75
|
+
* question (does a screen swap lose it?) is about state, not about pixels.
|
|
76
|
+
*/
|
|
77
|
+
promptControllerRef?: React.RefObject<PromptController | null>;
|
|
62
78
|
}): React.JSX.Element | null;
|
|
63
79
|
//# sourceMappingURL=Chat.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Chat.d.ts","sourceRoot":"","sources":["../../../src/screens/Chat.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AA0BzB,OAAO,KAAK,EAAE,SAAS,IAAI,OAAO,EAAE,MAAM,iCAAiC,CAAA;AAE3E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAA;AAC1D,OAAO,EAAE,cAAc,EAAwB,MAAM,0BAA0B,CAAA;AAE/E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAA;AAClE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAG5D,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;
|
|
1
|
+
{"version":3,"file":"Chat.d.ts","sourceRoot":"","sources":["../../../src/screens/Chat.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AA0BzB,OAAO,KAAK,EAAE,SAAS,IAAI,OAAO,EAAE,MAAM,iCAAiC,CAAA;AAE3E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;AAChE,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAA;AAC1D,OAAO,EAAE,cAAc,EAAwB,MAAM,0BAA0B,CAAA;AAE/E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAA;AAClE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAG5D,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;AAkB3D,OAAO,EAAe,KAAK,gBAAgB,EAAE,MAAM,8BAA8B,CAAA;AAEjF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAA;AAgNxE,wBAAgB,IAAI,CAAC,EACnB,OAAO,EACP,aAAa,EACb,aAAa,EACb,gBAAgB,EAChB,eAAe,EACf,kBAAkB,EAClB,SAAS,EACT,MAAM,EACN,QAAQ,EACR,SAAS,EACT,UAAkB,EAClB,cAAc,EAAE,kBAAkB,EAClC,mBAAmB,EACnB,mBAAmB,EAAE,uBAAuB,EAC5C,WAAW,EACX,cAAc,GACf,EAAE;IACD,OAAO,EAAE,OAAO,CAAA;IAChB,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,KAAK,CAAC,SAAS,CAAA;IAC/D,aAAa,EAAE,aAAa,CAAA;IAC5B;;;;OAIG;IACH,aAAa,CAAC,EAAE,aAAa,CAAA;IAC7B;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,cAAc,CAAA;IACjC,yDAAyD;IACzD,eAAe,CAAC,EAAE,cAAc,CAAA;IAChC,iDAAiD;IACjD,kBAAkB,CAAC,EAAE,eAAe,CAAA;IACpC,uEAAuE;IACvE,SAAS,CAAC,EAAE,YAAY,CAAA;IACxB,MAAM,EAAE,MAAM,IAAI,CAAA;IAClB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAA;IACrB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;IACtB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAA;IAC9D;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAA;CAC/D,4BA2rIA"}
|
|
@@ -18,7 +18,6 @@ import { homeDir } from '../utils/paths.js';
|
|
|
18
18
|
import { cleanRenderText, cleanScalarText } from '../dsh-adapter/sanitize.js';
|
|
19
19
|
import { deriveModelGroups, modelPickerLanding, recentCatalogModels, RECENTS_GROUP_PROVIDER, } from '../modelGroups.js';
|
|
20
20
|
import { readModelRecents, recordModelUse } from '../modelRecents.js';
|
|
21
|
-
import { sessionCwdMatches } from '../dsh-adapter/channel.js';
|
|
22
21
|
import { TuiDialogStore } from '../dsh-adapter/dialogs.js';
|
|
23
22
|
import { TuiStatusStore } from '../dsh-adapter/status.js';
|
|
24
23
|
import { runProviderWizard } from '../dsh-adapter/providerWizard.js';
|
|
@@ -52,7 +51,7 @@ import { PluginSceneBoundary } from '../components/PluginSceneBoundary.js';
|
|
|
52
51
|
import { PluginStatusViewBoundary } from '../components/PluginStatusViewBoundary.js';
|
|
53
52
|
import { ImagePreviewOverlay } from '../components/ImagePreviewOverlay.js';
|
|
54
53
|
import { SkillsPicker, SkillsPickerLoading } from '../components/SkillsPicker.js';
|
|
55
|
-
import {
|
|
54
|
+
import { SessionSupervisor } from './SessionSupervisor.js';
|
|
56
55
|
import { SessionTree } from './SessionTree.js';
|
|
57
56
|
import { Settings } from './Settings.js';
|
|
58
57
|
import { WorkspacePicker } from '../components/WorkspacePicker.js';
|
|
@@ -89,7 +88,8 @@ import instances from '../ink/instances.js';
|
|
|
89
88
|
import { useAnimationFrame } from '../ink/hooks/use-animation-frame.js';
|
|
90
89
|
import { useExternalVersion } from '../hooks/useExternalVersion.js';
|
|
91
90
|
import { TrajectoryScene } from './TrajectoryScene.js';
|
|
92
|
-
import {
|
|
91
|
+
import { resumeFailureText } from '../sessions/resumeFailure.js';
|
|
92
|
+
import { markHomeSeen } from '../homePrefs.js';
|
|
93
93
|
import { extendTrajectory, projectWave } from '../dsh-adapter/trajectory/index.js';
|
|
94
94
|
import { miniWakeWidth } from '../components/trajectory/MiniWake.js';
|
|
95
95
|
import { readTrajectorySeen, writeTrajectorySeen } from '../trajectoryPrefs.js';
|
|
@@ -204,7 +204,7 @@ let fallbackStatusStore;
|
|
|
204
204
|
function peekKey(image, title) {
|
|
205
205
|
return `${title ?? ''} ${image.id}`;
|
|
206
206
|
}
|
|
207
|
-
export function Chat({ channel, questionStore, approvalStore, extensionDialogs, extensionStatus, extensionShortcuts, themeHost, onExit, onUpdate, onRestart, fullscreen = false, trajectorySeen: trajectorySeenProp, injectControllerRef, renderScene, }) {
|
|
207
|
+
export function Chat({ channel, questionStore, approvalStore, extensionDialogs, extensionStatus, extensionShortcuts, themeHost, onExit, onUpdate, onRestart, fullscreen = false, trajectorySeen: trajectorySeenProp, injectControllerRef, promptControllerRef: promptControllerRefProp, renderScene, openHomeOnBoot, }) {
|
|
208
208
|
const writeRaw = React.useContext(TerminalWriteContext);
|
|
209
209
|
// Re-render whenever the channel mutates; rows/status are read fresh below.
|
|
210
210
|
// DEFAULT lane on purpose (useExternalVersion): the channel version bumps
|
|
@@ -358,18 +358,33 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
358
358
|
};
|
|
359
359
|
/** `/skills` 技能目录(issue #204):null = 注册表快照在途。 */
|
|
360
360
|
const [skillsList, setSkillsList] = React.useState(null);
|
|
361
|
-
/**
|
|
362
|
-
*
|
|
363
|
-
|
|
361
|
+
/**
|
|
362
|
+
* The session supervisor — the ONE screen behind `/resume`, `/agentview`,
|
|
363
|
+
* `/home`, `/bg` and the composer's 🏠 button.
|
|
364
|
+
*
|
|
365
|
+
* Those were three screens over one domain (a workspace rail here, a
|
|
366
|
+
* search surface there, a live-status overview somewhere else), which is why
|
|
367
|
+
* each new session feature needed patching into all three and why the
|
|
368
|
+
* three disagreed about what switching a session even does. There is now a
|
|
369
|
+
* single surface and a single runtime behind every entry point: this
|
|
370
|
+
* terminal hosts several sessions, leaving one parks it rather than ending
|
|
371
|
+
* it, and a session another terminal holds is visible but not enterable.
|
|
372
|
+
*
|
|
373
|
+
* Seeded from the host's one-shot landing decision (`openHomeOnBoot`): the
|
|
374
|
+
* first ordinary launch of an installation lands here instead of on a blank
|
|
375
|
+
* conversation, because that is the launch where "which project am I working
|
|
376
|
+
* on" has not been answered yet. Every later launch starts on the chat
|
|
377
|
+
* screen, and the screen stays reachable.
|
|
378
|
+
*/
|
|
379
|
+
const [supervisorOpen, setSupervisorOpen] = React.useState(openHomeOnBoot === true);
|
|
364
380
|
/** `/tree` opens the session family tree (pi's Session Tree): every rewind
|
|
365
381
|
* fork stitched back onto the message it diverged from, hover previews,
|
|
366
|
-
* and per-node rewind/fork/adopt actions. Like the
|
|
382
|
+
* and per-node rewind/fork/adopt actions. Like the supervisor, a screen. */
|
|
367
383
|
const [treeOpen, setTreeOpen] = React.useState(false);
|
|
368
|
-
/**
|
|
369
|
-
*
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
* returns to that conversation" return target), cleared on close. */
|
|
384
|
+
/**
|
|
385
|
+
* The session backgrounded when the screen opened via ←/`/bg` (the "Esc
|
|
386
|
+
* returns to that conversation" return target), cleared on close.
|
|
387
|
+
*/
|
|
373
388
|
const [agentViewReturnId, setAgentViewReturnId] = React.useState(undefined);
|
|
374
389
|
/** Live agent-view rows: the prompt footer's "← N agents" hint reads the
|
|
375
390
|
* needs-input count from here (cached snapshot in the channel). The
|
|
@@ -378,16 +393,16 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
378
393
|
const EMPTY_AGENT_VIEW_ROWS = [];
|
|
379
394
|
const agentViewRows = React.useSyncExternalStore(listener => channel.subscribeAgentView?.(listener) ?? (() => { }), () => channel.agentViewRows?.() ?? EMPTY_AGENT_VIEW_ROWS);
|
|
380
395
|
const backgroundAgentsNeedingInput = agentViewRows.filter(row => row.status === 'needs-input' && !row.current).length;
|
|
381
|
-
/** Background the attached session and open the
|
|
396
|
+
/** Background the attached session and open the supervisor
|
|
382
397
|
* (`/bg`, `/background`, and ← on an empty prompt all land here). The
|
|
383
|
-
* backgrounded session becomes the
|
|
398
|
+
* backgrounded session becomes the screen's return target (final Esc
|
|
384
399
|
* attaches back to it). */
|
|
385
400
|
const backgroundToAgentView = React.useCallback(() => {
|
|
386
401
|
void channel.backgroundCurrent().then((result) => {
|
|
387
402
|
if (result.ok) {
|
|
388
403
|
setAgentViewReturnId(result.backgroundedSessionId);
|
|
389
404
|
agentViewOpenSessionRef.current = channel.agentId;
|
|
390
|
-
|
|
405
|
+
setSupervisorOpen(true);
|
|
391
406
|
}
|
|
392
407
|
});
|
|
393
408
|
}, [channel]);
|
|
@@ -405,6 +420,10 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
405
420
|
const [themeName, setTheme] = useTheme();
|
|
406
421
|
const { rows: terminalRows } = useTerminalSize();
|
|
407
422
|
const [showAllMessages, setShowAllMessages] = React.useState(false);
|
|
423
|
+
/** Scope the fold to this question: an aborted ask can promote its queued
|
|
424
|
+
* successor without ever publishing an idle (null) snapshot. */
|
|
425
|
+
const [minimizedQuestionKey, setMinimizedQuestionKey] = React.useState(null);
|
|
426
|
+
const questionMinimized = questionSnapshot !== null && minimizedQuestionKey === questionSnapshot.key;
|
|
408
427
|
/** Fold state for the GoalTodoPanel todo section (ctrl/cmd+q or click). */
|
|
409
428
|
const [todoCollapsed, setTodoCollapsed] = React.useState(false);
|
|
410
429
|
const [thinkingVisible, setThinkingVisible] = React.useState(true);
|
|
@@ -615,6 +634,34 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
615
634
|
});
|
|
616
635
|
setSceneOpen(true);
|
|
617
636
|
}, []);
|
|
637
|
+
/**
|
|
638
|
+
* Leave the session supervisor for the conversation.
|
|
639
|
+
*
|
|
640
|
+
* The one-shot landing preference is written here rather than at boot: a
|
|
641
|
+
* process that dies before the user ever sees the screen (a config error, a
|
|
642
|
+
* crash during the first render) must not burn the installation's only
|
|
643
|
+
* first-launch landing. Writing on the way OUT means "the user has seen it".
|
|
644
|
+
*
|
|
645
|
+
* Leaving also honours `/bg`'s return target. `/background` moved the
|
|
646
|
+
* session the user was in to the background and opened this screen; a plain
|
|
647
|
+
* Esc out of it re-attaches to that session instead of silently leaving them
|
|
648
|
+
* on the fresh one, which is what "go back to what I was doing" means. Any
|
|
649
|
+
* explicit mount inside the screen clears the target first, so this can
|
|
650
|
+
* never undo a choice the user just made.
|
|
651
|
+
*/
|
|
652
|
+
const closeHome = React.useCallback(() => {
|
|
653
|
+
suppressLogoIntroRef.current = true;
|
|
654
|
+
markHomeSeen();
|
|
655
|
+
const returnTo = agentViewReturnId;
|
|
656
|
+
setAgentViewReturnId(undefined);
|
|
657
|
+
setSupervisorOpen(false);
|
|
658
|
+
if (returnTo !== undefined && returnTo !== channel.agentId) {
|
|
659
|
+
void channel.resumeTo(returnTo).then((result) => {
|
|
660
|
+
if (result.ok)
|
|
661
|
+
repaintTranscript();
|
|
662
|
+
}).catch(() => undefined);
|
|
663
|
+
}
|
|
664
|
+
}, [agentViewReturnId, channel, repaintTranscript]);
|
|
618
665
|
/** The startup summary gives way to transcript rows after the first local command or message. */
|
|
619
666
|
const loadedContextVisible = channel.rows.length === 0 && channel.loadedContext !== undefined;
|
|
620
667
|
/** Startup context panel: collapsed by default, toggled with Ctrl+P. */
|
|
@@ -851,12 +898,121 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
851
898
|
const exitTimerRef = React.useRef(null);
|
|
852
899
|
// Live view into the prompt's text for the Ctrl+C rule (clears text when
|
|
853
900
|
// non-empty; the double-press exit only arms on an empty input).
|
|
854
|
-
const
|
|
901
|
+
const ownPromptControllerRef = React.useRef(null);
|
|
902
|
+
const promptControllerRef = promptControllerRefProp ?? ownPromptControllerRef;
|
|
903
|
+
/**
|
|
904
|
+
* Owner of the unsent draft. Every screen this component renders INSTEAD of
|
|
905
|
+
* the conversation (the session screen, the tree, settings, the jobs and
|
|
906
|
+
* subagent panels, the trajectory scene) unmounts the composer, and the
|
|
907
|
+
* composer keeps its text in local state — so without this the half-written
|
|
908
|
+
* prompt died on the way in. The slot lives here, outlives that unmount, and
|
|
909
|
+
* is dropped the moment the attached session changes so no draft can follow
|
|
910
|
+
* the user into a different conversation.
|
|
911
|
+
*/
|
|
912
|
+
const promptDraftRef = React.useRef({ current: null });
|
|
913
|
+
/**
|
|
914
|
+
* Latest channel for the unmount release below: that effect must not re-run
|
|
915
|
+
* on a channel identity change, yet its cleanup must release against the
|
|
916
|
+
* channel of the last render.
|
|
917
|
+
*/
|
|
918
|
+
const channelRef = React.useRef(channel);
|
|
919
|
+
channelRef.current = channel;
|
|
920
|
+
/**
|
|
921
|
+
* Release the staged images a WAITING snapshot alone owns.
|
|
922
|
+
*
|
|
923
|
+
* While a draft waits in the slot for the composer to remount, the snapshot
|
|
924
|
+
* is the only owner of the capabilities behind its `[Image #N]` tokens. If
|
|
925
|
+
* Chat itself goes away first (leaving an early-return screen by exiting the
|
|
926
|
+
* TUI), nothing would ever restore or discard them — the session's
|
|
927
|
+
* 128-entry FIFO would evict live entries instead. The `hasStagedImage`
|
|
928
|
+
* guard keeps a capability the channel already recycled a no-op; both calls
|
|
929
|
+
* are idempotent.
|
|
930
|
+
*
|
|
931
|
+
* Scope, deliberately narrow (review round 7): a capability a QUEUED message
|
|
932
|
+
* still references (`channel.pending`) is never revoked here. The real
|
|
933
|
+
* double-hold path is paste an image → queue the draft with Tab while the
|
|
934
|
+
* model works → recall that line from input history with ↑ (same stageId
|
|
935
|
+
* rebound to the draft) → park the composer. Delivery resolves its refs from
|
|
936
|
+
* the enqueue-time capture, so a late revoke would only bite a host that
|
|
937
|
+
* re-resolves them afterwards — this keeps the rule identical to
|
|
938
|
+
* `stageIdIsRetained` instead of relying on that.
|
|
939
|
+
*
|
|
940
|
+
* A composer that is still MOUNTED when Chat unmounts is NOT covered: React
|
|
941
|
+
* runs this parent cleanup BEFORE the child's, so the child then writes its
|
|
942
|
+
* draft into the now-dead ref and those ids ride the channel's lifetime out
|
|
943
|
+
* (the #942 review's remaining P2). Neither unmount path loses anything
|
|
944
|
+
* user-visible — the channel dies with them.
|
|
945
|
+
*/
|
|
946
|
+
React.useEffect(() => {
|
|
947
|
+
return () => {
|
|
948
|
+
const snapshot = promptDraftRef.current.current;
|
|
949
|
+
promptDraftRef.current.current = null;
|
|
950
|
+
if (snapshot === null)
|
|
951
|
+
return;
|
|
952
|
+
const queued = new Set();
|
|
953
|
+
for (const item of channelRef.current.pending) {
|
|
954
|
+
// `?? []`: a foreign/embedded host may hand us a pending entry without
|
|
955
|
+
// images, and a throw inside an unmount cleanup escapes into the exit
|
|
956
|
+
// path — every other reader of this field guards it the same way.
|
|
957
|
+
for (const image of item.images ?? [])
|
|
958
|
+
queued.add(image.stageId);
|
|
959
|
+
}
|
|
960
|
+
for (const [, stageId] of snapshot.images) {
|
|
961
|
+
if (queued.has(stageId))
|
|
962
|
+
continue;
|
|
963
|
+
if (channelRef.current.hasStagedImage?.(stageId) === true) {
|
|
964
|
+
channelRef.current.discardStagedImage(stageId);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
};
|
|
968
|
+
}, []);
|
|
969
|
+
const draftSessionId = channel.agentId;
|
|
970
|
+
/** Session the effect below last reconciled against; a change is a switch. */
|
|
971
|
+
const draftSessionRef = React.useRef(draftSessionId);
|
|
972
|
+
/**
|
|
973
|
+
* The session a fill Chat itself requested belongs to, if one is in flight.
|
|
974
|
+
*
|
|
975
|
+
* A rewind's restored message arrives in the same commit that replaces the
|
|
976
|
+
* session, and it belongs to the NEW binding — the user picked it. The
|
|
977
|
+
* composer cannot tell, so Chat says so here, at the two call sites that ask
|
|
978
|
+
* for a fill.
|
|
979
|
+
*
|
|
980
|
+
* Keyed by the session id rather than a bare flag, so it can only ever excuse
|
|
981
|
+
* the switch it was written for. Do NOT clear it when the composer consumes
|
|
982
|
+
* the fill: a child's layout effects run before the parent's, so the fill is
|
|
983
|
+
* consumed in the very commit this effect judges, and clearing it there would
|
|
984
|
+
* wipe the message the user just got back.
|
|
985
|
+
*/
|
|
986
|
+
const pendingFillRef = React.useRef(null);
|
|
987
|
+
/**
|
|
988
|
+
* Drop the composer's text when the session underneath it is replaced.
|
|
989
|
+
*
|
|
990
|
+
* A LAYOUT effect, not a passive one: the clear has to land in the commit
|
|
991
|
+
* that swaps the session. A passive effect is flushed later, and anything
|
|
992
|
+
* typed in between (the tree's hand-off, a fast user) would be wiped with the
|
|
993
|
+
* old conversation's text. Which DRAFT the slot keeps is a separate question,
|
|
994
|
+
* answered by the snapshot's owner fields.
|
|
995
|
+
*/
|
|
996
|
+
React.useLayoutEffect(() => {
|
|
997
|
+
if (draftSessionRef.current === draftSessionId)
|
|
998
|
+
return;
|
|
999
|
+
draftSessionRef.current = draftSessionId;
|
|
1000
|
+
// A stored draft can only belong to the conversation being replaced: the
|
|
1001
|
+
// composer is the one that writes it, and it writes it on the way out.
|
|
1002
|
+
promptDraftRef.current.current = null;
|
|
1003
|
+
if (pendingFillRef.current === draftSessionId) {
|
|
1004
|
+
pendingFillRef.current = null;
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
promptControllerRef.current?.clear();
|
|
1008
|
+
}, [draftSessionId]);
|
|
855
1009
|
const previewGallery = activePreview === null ? [] : activePreview.peek
|
|
856
1010
|
? promptControllerRef.current?.previewImages?.() ?? [activePreview]
|
|
857
1011
|
: overlay.kind === 'image-preview' ? overlay.gallery ?? [activePreview] : [];
|
|
1012
|
+
// Peek entries are rebuilt from the prompt every render: match by
|
|
1013
|
+
// attachment id + token title, not facade identity.
|
|
858
1014
|
const previewIndex = activePreview?.peek
|
|
859
|
-
? previewGallery.findIndex(entry => entry.image === activePreview.image && entry.title === activePreview.title)
|
|
1015
|
+
? previewGallery.findIndex(entry => entry.image.id === activePreview.image.id && entry.title === activePreview.title)
|
|
860
1016
|
: overlay.kind === 'image-preview' ? overlay.index ?? 0 : -1;
|
|
861
1017
|
const stepPreview = (delta) => {
|
|
862
1018
|
if (!activePreview?.peek) {
|
|
@@ -943,7 +1099,11 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
943
1099
|
// fullscreen (<AlternateScreen> supplies mouse tracking); a no-op
|
|
944
1100
|
// subscription in inline mode, where selection belongs to the terminal.
|
|
945
1101
|
// The copy clears the highlight and posts a transient notification.
|
|
946
|
-
useCopyOnSelect(text => channel.notify(t('copied-chars', { n: text.length }), { timeoutMs: 1500 })
|
|
1102
|
+
useCopyOnSelect(text => channel.notify(t('copied-chars', { n: text.length }), { timeoutMs: 1500 }),
|
|
1103
|
+
// Stale-selection refusal: the highlighted rows were replaced in place
|
|
1104
|
+
// (streaming overwrite), so nothing was copied — say why instead of
|
|
1105
|
+
// letting the highlight vanish silently.
|
|
1106
|
+
() => channel.notify(t('copy-refused-stale'), { timeoutMs: 2500 }));
|
|
947
1107
|
const { clearSelection: clearMouseSelection, hasSelection: hasMouseSelection } = useSelection();
|
|
948
1108
|
React.useEffect(() => {
|
|
949
1109
|
if (!channel.working || !terminalFocused)
|
|
@@ -1611,27 +1771,31 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1611
1771
|
}
|
|
1612
1772
|
return true;
|
|
1613
1773
|
}
|
|
1614
|
-
case 'resume':
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1774
|
+
case 'resume':
|
|
1775
|
+
/**
|
|
1776
|
+
* `/resume`, `/home` and `/agentview` are one screen.
|
|
1777
|
+
*
|
|
1778
|
+
* They were three implementations of one domain and drifted apart: the
|
|
1779
|
+
* same session could be listed by all three, each with its own selection
|
|
1780
|
+
* model and its own idea of what opening one does. Keeping the three
|
|
1781
|
+
* commands is about muscle memory, not about three surfaces — every one
|
|
1782
|
+
* of them lands here, on the same runtime.
|
|
1783
|
+
*/
|
|
1784
|
+
case 'home':
|
|
1622
1785
|
case 'agentview': {
|
|
1623
|
-
// The agent view shows one screen for every session. It opens
|
|
1624
|
-
// immediately; the view reads its own rows (live + persisted).
|
|
1625
1786
|
setHelpOpen(false);
|
|
1787
|
+
// The screen opens immediately and loads its own list. Waiting for the
|
|
1788
|
+
// listing here would make it feel slower the more history a project
|
|
1789
|
+
// has, which is exactly backwards.
|
|
1626
1790
|
agentViewOpenSessionRef.current = channel.agentId;
|
|
1627
|
-
|
|
1791
|
+
setSupervisorOpen(true);
|
|
1628
1792
|
return true;
|
|
1629
1793
|
}
|
|
1630
1794
|
case 'bg':
|
|
1631
1795
|
case 'background': {
|
|
1632
1796
|
// `/background`: the attached session moves to the background
|
|
1633
1797
|
// (it keeps running in this process), the terminal lands on a fresh
|
|
1634
|
-
// session, and the
|
|
1798
|
+
// session, and the supervisor opens on top.
|
|
1635
1799
|
setHelpOpen(false);
|
|
1636
1800
|
backgroundToAgentView();
|
|
1637
1801
|
return true;
|
|
@@ -2256,6 +2420,10 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
2256
2420
|
const performRewind = async (row, mode = null) => {
|
|
2257
2421
|
const text = await channel.rewindTo(row, mode);
|
|
2258
2422
|
if (text !== null) {
|
|
2423
|
+
// The restored message belongs to the binding `rewindTo` just created,
|
|
2424
|
+
// not to the one it replaced: it is the user's choice, and the switch
|
|
2425
|
+
// effect must not treat it as the old conversation's leftovers.
|
|
2426
|
+
pendingFillRef.current = String(channel.agentId);
|
|
2259
2427
|
// Put the restored message back in the prompt for re-editing.
|
|
2260
2428
|
setHistoryFill(text);
|
|
2261
2429
|
channel.notify(t('rewind-done'));
|
|
@@ -2460,18 +2628,15 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
2460
2628
|
|| overlay.kind === 'tips'
|
|
2461
2629
|
|| (recap !== null && (!recap.auto || recap.expanded)))
|
|
2462
2630
|
return;
|
|
2463
|
-
//
|
|
2464
|
-
//
|
|
2465
|
-
// search box, which Chat would otherwise route into the prompt.
|
|
2466
|
-
if (browserOpen)
|
|
2467
|
-
return;
|
|
2468
|
-
// Same for the session tree: plain letters drive its search, clicks and
|
|
2469
|
-
// Enter drive its action menu.
|
|
2631
|
+
// The session tree owns the whole terminal while it is up: plain letters
|
|
2632
|
+
// drive its search, clicks and Enter drive its action menu.
|
|
2470
2633
|
if (treeOpen)
|
|
2471
2634
|
return;
|
|
2472
|
-
// The
|
|
2473
|
-
// its
|
|
2474
|
-
|
|
2635
|
+
// The session supervisor owns the whole terminal while it is up: its rail
|
|
2636
|
+
// and session list bind ↑/↓/Enter/Tab/Esc, its filter box takes the plain
|
|
2637
|
+
// letters that would otherwise reach the prompt, and the directory picker
|
|
2638
|
+
// and menus it opens are its own modal layers.
|
|
2639
|
+
if (supervisorOpen)
|
|
2475
2640
|
return;
|
|
2476
2641
|
// Same for the settings screen: plain letters (s save / d discard) and
|
|
2477
2642
|
// the field draft editor belong to it alone.
|
|
@@ -2575,8 +2740,17 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
2575
2740
|
// keyboard while one is pending (the panel's own useInput handles
|
|
2576
2741
|
// ↑/↓/Space/Tab/Enter/Esc; the prompt input is suspended, so nothing
|
|
2577
2742
|
// else should see these keys).
|
|
2578
|
-
if (
|
|
2743
|
+
if (approvalSnapshot !== null || dialogSnapshot !== null)
|
|
2744
|
+
return;
|
|
2745
|
+
if (questionSnapshot !== null) {
|
|
2746
|
+
// Only transcript navigation belongs here. The mounted questionnaire
|
|
2747
|
+
// owns fold/expand keys, including when it interrupts another screen.
|
|
2748
|
+
if (questionMinimized && !isSticky && (isPlainReturnInput(input, key) || key.end)) {
|
|
2749
|
+
handle?.scrollToBottom();
|
|
2750
|
+
event.stopImmediatePropagation();
|
|
2751
|
+
}
|
|
2579
2752
|
return;
|
|
2753
|
+
}
|
|
2580
2754
|
const returnCandidate = isPlainReturnInput(input, key);
|
|
2581
2755
|
const returnNow = Date.now();
|
|
2582
2756
|
const plainReturn = returnCandidate && returnNow - lastModalEnterAtRef.current >= 80;
|
|
@@ -3365,9 +3539,9 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
3365
3539
|
const approvalPanelNode = approvalSnapshot !== null ? (_jsx(ApprovalPanel, { approval: approvalSnapshot, background: approvalSnapshot.agentId !== channel.agentId, onDecide: outcome => approvals.decide(outcome) }, approvalSnapshot.key)) : null;
|
|
3366
3540
|
const questionPanelNode = questionSnapshot !== null ? (_jsx(AskUserQuestionPanel, { question: questionSnapshot.question, position: questionSnapshot.position, total: questionSnapshot.total, answered: questionSnapshot.answered, initialDraft: questionSnapshot.draft, onAnswer: selection => questionStore.answerCurrent(selection), onCancel: () => questionStore.cancelCurrent(), onBack: questionSnapshot.canGoBack
|
|
3367
3541
|
? draft => questionStore.backCurrent(draft)
|
|
3368
|
-
: undefined }, questionSnapshot.key)) : null;
|
|
3542
|
+
: undefined, collapsed: questionMinimized, onExpand: () => setMinimizedQuestionKey(null), onToggleFold: () => setMinimizedQuestionKey(previous => previous === questionSnapshot.key ? null : questionSnapshot.key), fullscreen: fullscreen }, questionSnapshot.key)) : null;
|
|
3369
3543
|
const interruptPanel = approvalPanelNode ?? questionPanelNode;
|
|
3370
|
-
const screenOpen = channel.pluginScene !== undefined ||
|
|
3544
|
+
const screenOpen = channel.pluginScene !== undefined || supervisorOpen || settingsOpen
|
|
3371
3545
|
|| subagentDetailId !== null || subagentDashboardOpen || sceneOpen;
|
|
3372
3546
|
if (interruptPanel !== null && screenOpen) {
|
|
3373
3547
|
const node = (_jsx(Box, { flexDirection: "column", width: "100%", paddingX: 1, children: interruptPanel }));
|
|
@@ -3394,37 +3568,62 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
3394
3568
|
}, children: renderScene ? renderScene(pluginScene.id, channel) : _jsxs(Text, { children: ["Scene unavailable: ", pluginScene.id] }) }));
|
|
3395
3569
|
return fullscreen ? node : _jsx(AlternateScreen, { children: node });
|
|
3396
3570
|
}
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3571
|
+
/**
|
|
3572
|
+
* The session supervisor: a screen in the same sense as the tree — an early
|
|
3573
|
+
* return after every hook above has run, so there is no transcript
|
|
3574
|
+
* underneath to repaint or bled through.
|
|
3575
|
+
*
|
|
3576
|
+
* It sits ABOVE the session tree because it is the surface a launch can
|
|
3577
|
+
* start on (`openHomeOnBoot`): a first launch has no conversation to come
|
|
3578
|
+
* back to, and every action it offers either mounts a session (which closes
|
|
3579
|
+
* it) or starts a new one.
|
|
3580
|
+
*
|
|
3581
|
+
* Behind it, every session this terminal hosts keeps running — that is the
|
|
3582
|
+
* runtime the screen describes, not an implementation detail of it. A turn
|
|
3583
|
+
* that was in flight when the user opened this screen is still in flight
|
|
3584
|
+
* while they read the list, which is why closing the screen only repaints
|
|
3585
|
+
* the transcript when the attached session actually changed.
|
|
3586
|
+
*/
|
|
3587
|
+
if (supervisorOpen) {
|
|
3588
|
+
/**
|
|
3589
|
+
* Live state per session, from the channel's own agent-view projection.
|
|
3590
|
+
* Reading the projection rather than a parallel source is what keeps this
|
|
3591
|
+
* screen and the attention hints in the composer footer from disagreeing
|
|
3592
|
+
* about which session is waiting for input.
|
|
3593
|
+
*/
|
|
3594
|
+
const agentRowOf = (sessionId) => agentViewRows.find(row => row.id === sessionId);
|
|
3595
|
+
const supervisorNode = (_jsx(SessionSupervisor, { channel: channel, home: homeDir(), onClose: closeHome, approval: approvalSnapshot, onApprove: outcome => approvals.decide(outcome), onOpenSession: async (sessionId) => {
|
|
3596
|
+
const result = await channel.resumeTo(sessionId);
|
|
3597
|
+
if (!result.ok) {
|
|
3598
|
+
const text = resumeFailureText(result);
|
|
3599
|
+
if (text !== undefined)
|
|
3600
|
+
channel.notify(text, { color: 'error', timeoutMs: 8000 });
|
|
3601
|
+
return false;
|
|
3602
|
+
}
|
|
3603
|
+
channel.notify(t('resume-resumed'));
|
|
3406
3604
|
suppressLogoIntroRef.current = true;
|
|
3407
|
-
const switched = agentViewOpenSessionRef.current !== undefined &&
|
|
3408
|
-
agentViewOpenSessionRef.current !== channel.agentId;
|
|
3409
|
-
if (switched)
|
|
3410
|
-
repaintTranscript();
|
|
3411
3605
|
setAgentViewReturnId(undefined);
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3606
|
+
setSupervisorOpen(false);
|
|
3607
|
+
repaintTranscript();
|
|
3608
|
+
return true;
|
|
3609
|
+
}, onNewSession: async (target) => {
|
|
3610
|
+
const ok = await channel.switchWorkspace(target);
|
|
3611
|
+
if (ok) {
|
|
3612
|
+
suppressLogoIntroRef.current = true;
|
|
3613
|
+
setAgentViewReturnId(undefined);
|
|
3614
|
+
setSupervisorOpen(false);
|
|
3615
|
+
repaintTranscript();
|
|
3616
|
+
}
|
|
3617
|
+
return ok;
|
|
3618
|
+
}, onStopSession: async (sessionId) => channel.stopBackgroundAgent?.(sessionId) ?? false, liveStateOf: (sessionId) => {
|
|
3619
|
+
const row = agentRowOf(sessionId);
|
|
3620
|
+
return row === undefined
|
|
3621
|
+
? undefined
|
|
3622
|
+
: { status: row.status, live: row.live, current: row.current, summary: row.summary };
|
|
3424
3623
|
} }));
|
|
3425
3624
|
// Inline hosts enter the alternate screen for the duration; full-screen
|
|
3426
3625
|
// hosts are already in it and must not nest a second one.
|
|
3427
|
-
return fullscreen ?
|
|
3626
|
+
return fullscreen ? supervisorNode : _jsx(AlternateScreen, { children: supervisorNode });
|
|
3428
3627
|
}
|
|
3429
3628
|
// The session tree follows the browser's rule exactly: it REPLACES the
|
|
3430
3629
|
// conversation (an early return after every hook above has run), so there
|
|
@@ -3432,6 +3631,10 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
3432
3631
|
// turn's prompt returns through the same fill path a rewind picker uses.
|
|
3433
3632
|
if (treeOpen) {
|
|
3434
3633
|
const tree = (_jsx(SessionTree, { channel: channel, currentSessionId: channel.agentId, onClose: () => setTreeOpen(false), onRestoreText: (text) => {
|
|
3634
|
+
// The tree rewound to a node and is handing that turn's prompt back,
|
|
3635
|
+
// exactly like the picker does. It belongs to the binding the tree
|
|
3636
|
+
// action just created.
|
|
3637
|
+
pendingFillRef.current = String(channel.agentId);
|
|
3435
3638
|
setHistoryFill(text);
|
|
3436
3639
|
} }));
|
|
3437
3640
|
return fullscreen ? tree : _jsx(AlternateScreen, { children: tree });
|
|
@@ -3568,7 +3771,7 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
3568
3771
|
// sessions keep the full intro; restored ones settle instantly.
|
|
3569
3772
|
// A remount after a whole screen closed also settles instantly
|
|
3570
3773
|
// (see suppressLogoIntroRef).
|
|
3571
|
-
skipIntro: suppressLogoIntroRef.current || channel.rows.length > 30 }, logoNonce), loadedContextVisible && (_jsx(LoadedContextPanel, { context: channel.loadedContext, open: loadedContextOpen, onToggle: toggleLoadedContext })), _jsx(MessageList, { rows: channel.rows, failureHintRowId: failureHintRowId, failureHint: t('traj-hint-failure', { key: `${modLabel}t` }), expanded: expanded, expandedRows: expandedRows, selectedId: selectionActive ? selectedId : null, onToggleRow: toggleRowExpanded, streamViewToggledRows: streamViewToggledRows, onToggleStreamView: toggleStreamView, model: channel.model, diffLayout: channel.diffLayout, thinkingFold: channel.thinkingFold, toolBackground: channel.toolBackground, foldTerminalCommand: channel.foldTerminalCommand, smoothStreaming: channel.smoothStreaming, activityFrames: channel.activityFrames, showAll: showAllMessages, thinkingVisible: thinkingVisible, historyPaintEnabled: !fullscreen, onToggleAll: () => { setShowAllMessages(previous => !previous); }, onLoadOlder: () => channel.loadOlder(), registerRowRef: registerRowRef, scrollHandle: handle, forceMountRowId: forceMountRowId, newSinceRowId: isSticky ? null : lastSeenRowIdRef.current, onUnseenCount: setUnseenCount, onTimeline: setTimeline, onOpenSubagent: setSubagentDetailId, onOpenJobs: openJobsPanel, onOpenFile: openFileActions, onPreviewImage: openImagePreview, suppressImageGraphics: activePreview !== null })] }), (() => {
|
|
3774
|
+
skipIntro: suppressLogoIntroRef.current || channel.rows.length > 30 }, logoNonce), loadedContextVisible && (_jsx(LoadedContextPanel, { context: channel.loadedContext, open: loadedContextOpen, onToggle: toggleLoadedContext })), _jsx(MessageList, { rows: channel.rows, failureHintRowId: failureHintRowId, failureHint: t('traj-hint-failure', { key: `${modLabel}t` }), expanded: expanded, expandedRows: expandedRows, selectedId: selectionActive ? selectedId : null, onToggleRow: toggleRowExpanded, streamViewToggledRows: streamViewToggledRows, onToggleStreamView: toggleStreamView, model: channel.model, diffLayout: channel.diffLayout, thinkingFold: channel.thinkingFold, toolBackground: channel.toolBackground, foldTerminalCommand: channel.foldTerminalCommand, smoothStreaming: channel.smoothStreaming, activityFrames: channel.activityFrames, showAll: showAllMessages, thinkingVisible: thinkingVisible, historyPaintEnabled: !fullscreen, onToggleAll: () => { setShowAllMessages(previous => !previous); }, onLoadOlder: () => channel.loadOlder(), registerRowRef: registerRowRef, scrollHandle: handle, forceMountRowId: forceMountRowId, newSinceRowId: isSticky ? null : lastSeenRowIdRef.current, onUnseenCount: setUnseenCount, onTimeline: setTimeline, onOpenSubagent: setSubagentDetailId, onOpenJobs: openJobsPanel, onOpenFile: openFileActions, sessionCwd: channel.cwd, onPreviewImage: openImagePreview, suppressImageGraphics: activePreview !== null })] }), (() => {
|
|
3572
3775
|
// Gutter mode (settings `dsh-tui.scrollGutter`): the timeline
|
|
3573
3776
|
// rail (default), the proportional scrollbar, or nothing. The
|
|
3574
3777
|
// slot keeps its 2 columns in both rendered modes (Qwen's
|
|
@@ -3617,7 +3820,16 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
3617
3820
|
void setClipboard(btw.answer ?? '').then(raw => { if (raw)
|
|
3618
3821
|
writeRaw?.(raw); });
|
|
3619
3822
|
channel.notify(t('copied-chars', { n: (btw.answer ?? '').length }), { timeoutMs: 1500 });
|
|
3620
|
-
} }) })) : questionPanelNode !== null ? (questionPanelNode) : null, _jsx(PromptInput, { channel: channel, suspended: promptReplacementOpen, helpOpen: helpOpen, onToggleHelp: () => { setHelpOpen(previous => !previous); }, onRunCommand: runCommand, selectionActive: promptSelectionActive, fillText: historyFill, onFillConsumed: () =>
|
|
3823
|
+
} }) })) : questionPanelNode !== null ? (questionPanelNode) : null, _jsx(PromptInput, { channel: channel, suspended: promptReplacementOpen, draftCache: promptDraftRef.current, helpOpen: helpOpen, onToggleHelp: () => { setHelpOpen(previous => !previous); }, onRunCommand: runCommand, selectionActive: promptSelectionActive, fillText: historyFill, onFillConsumed: () => setHistoryFill(null), onRewindRequest: openRewind, onBackgroundRequest: backgroundToAgentView,
|
|
3824
|
+
// The 🏠 at the head of the input row opens the same session screen
|
|
3825
|
+
// `/resume` and `/agentview` open — one surface, three doors. It is
|
|
3826
|
+
// gated on this prop rather than a setting, so hosts that mount the
|
|
3827
|
+
// prompt without a session screen (and the layout regressions that
|
|
3828
|
+
// pin the row's column budget) keep the row they had.
|
|
3829
|
+
onOpenSessions: () => {
|
|
3830
|
+
agentViewOpenSessionRef.current = channel.agentId;
|
|
3831
|
+
setSupervisorOpen(true);
|
|
3832
|
+
}, backgroundAgentsNeedingInput:
|
|
3621
3833
|
// Only the real channel supplies the seam; pre-agent-view test
|
|
3622
3834
|
// stubs must not grow the footer row (layout-dependent
|
|
3623
3835
|
// regressions pin the visible row count). The footer only
|