@deepseek-harness-tui/dsh-tui 0.6.1 → 0.7.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 +74 -21
- package/bin/dsh-tui.js +115 -25
- package/cordis.patch.yml +69 -17
- package/cordis.yml +50 -12
- package/lib/types/activityPrefs.d.ts +1 -1
- package/lib/types/activityPrefs.js +3 -3
- package/lib/types/cc/figures.d.ts +27 -20
- package/lib/types/cc/figures.d.ts.map +1 -1
- package/lib/types/cc/figures.js +43 -34
- package/lib/types/cc/format.d.ts +18 -9
- package/lib/types/cc/format.d.ts.map +1 -1
- package/lib/types/cc/format.js +26 -18
- package/lib/types/cc/markdown.d.ts +31 -16
- package/lib/types/cc/markdown.d.ts.map +1 -1
- package/lib/types/cc/markdown.js +283 -260
- package/lib/types/cc/terminal.d.ts +10 -7
- package/lib/types/cc/terminal.d.ts.map +1 -1
- package/lib/types/cc/terminal.js +62 -50
- package/lib/types/command-trees.d.ts +3 -0
- package/lib/types/command-trees.d.ts.map +1 -0
- package/lib/types/command-trees.js +4 -0
- package/lib/types/commands.d.ts +37 -1
- package/lib/types/commands.d.ts.map +1 -1
- package/lib/types/commands.js +68 -3
- package/lib/types/components/ActivityLine.d.ts +1 -1
- package/lib/types/components/ActivityLine.d.ts.map +1 -1
- package/lib/types/components/ActivityPicker.d.ts.map +1 -1
- package/lib/types/components/ActivityPicker.js +3 -4
- package/lib/types/components/BtwPanel.d.ts +16 -0
- package/lib/types/components/BtwPanel.d.ts.map +1 -0
- package/lib/types/components/BtwPanel.js +47 -0
- package/lib/types/components/CommandSuggestions.d.ts +4 -2
- package/lib/types/components/CommandSuggestions.d.ts.map +1 -1
- package/lib/types/components/CommandSuggestions.js +2 -2
- package/lib/types/components/EffortSlider.d.ts +15 -0
- package/lib/types/components/EffortSlider.d.ts.map +1 -0
- package/lib/types/components/EffortSlider.js +17 -0
- package/lib/types/components/GoalTodoPanel.d.ts +1 -1
- package/lib/types/components/GoalTodoPanel.d.ts.map +1 -1
- package/lib/types/components/HelpMenu.d.ts +5 -2
- package/lib/types/components/HelpMenu.d.ts.map +1 -1
- package/lib/types/components/HelpMenu.js +8 -3
- package/lib/types/components/HistorySearchDialog.d.ts +1 -1
- package/lib/types/components/HistorySearchDialog.d.ts.map +1 -1
- package/lib/types/components/HistorySearchDialog.js +21 -10
- package/lib/types/components/InterruptedByUser.d.ts +2 -2
- package/lib/types/components/InterruptedByUser.d.ts.map +1 -1
- package/lib/types/components/InterruptedByUser.js +4 -3
- package/lib/types/components/LoadedContextPanel.d.ts +1 -1
- package/lib/types/components/LoadedContextPanel.d.ts.map +1 -1
- package/lib/types/components/LogoV2.d.ts.map +1 -1
- package/lib/types/components/LogoV2.js +2 -1
- package/lib/types/components/Markdown.d.ts +11 -11
- package/lib/types/components/Markdown.d.ts.map +1 -1
- package/lib/types/components/Markdown.js +63 -57
- package/lib/types/components/MarkdownTable.d.ts +1 -1
- package/lib/types/components/MarkdownTable.js +2 -2
- package/lib/types/components/MessageList.d.ts +10 -2
- package/lib/types/components/MessageList.d.ts.map +1 -1
- package/lib/types/components/MessageList.js +17 -10
- package/lib/types/components/ModelPicker.d.ts +4 -1
- package/lib/types/components/ModelPicker.d.ts.map +1 -1
- package/lib/types/components/ModelPicker.js +16 -4
- package/lib/types/components/OverlayAbove.d.ts +20 -0
- package/lib/types/components/OverlayAbove.d.ts.map +1 -0
- package/lib/types/components/OverlayAbove.js +18 -0
- package/lib/types/components/PresetPicker.d.ts +1 -1
- package/lib/types/components/PresetPicker.d.ts.map +1 -1
- package/lib/types/components/PresetPicker.js +2 -3
- package/lib/types/components/PromptInput.d.ts +5 -4
- package/lib/types/components/PromptInput.d.ts.map +1 -1
- package/lib/types/components/PromptInput.js +186 -39
- package/lib/types/components/ResumePicker.d.ts +15 -4
- package/lib/types/components/ResumePicker.d.ts.map +1 -1
- package/lib/types/components/ResumePicker.js +25 -9
- package/lib/types/components/RewindPicker.d.ts +1 -1
- package/lib/types/components/RewindPicker.d.ts.map +1 -1
- package/lib/types/components/RewindPicker.js +14 -5
- package/lib/types/components/SearchBox.d.ts +1 -1
- package/lib/types/components/SearchBox.js +1 -1
- package/lib/types/components/Spinner/GlimmerMessage.d.ts +1 -2
- package/lib/types/components/Spinner/GlimmerMessage.d.ts.map +1 -1
- package/lib/types/components/Spinner/GlimmerMessage.js +1 -2
- package/lib/types/components/Spinner/SpinnerAnimationRow.d.ts +1 -1
- package/lib/types/components/Spinner/SpinnerAnimationRow.js +2 -2
- package/lib/types/components/Spinner/SpinnerGlyph.d.ts +1 -1
- package/lib/types/components/Spinner/SpinnerGlyph.js +1 -1
- package/lib/types/components/Spinner/spinnerUtils.d.ts +13 -18
- package/lib/types/components/Spinner/spinnerUtils.d.ts.map +1 -1
- package/lib/types/components/Spinner/spinnerUtils.js +67 -71
- package/lib/types/components/Spinner/useStalledAnimation.d.ts +1 -2
- package/lib/types/components/Spinner/useStalledAnimation.d.ts.map +1 -1
- package/lib/types/components/Spinner/useStalledAnimation.js +1 -2
- package/lib/types/components/StreamingMarkdown.d.ts +1 -1
- package/lib/types/components/StreamingMarkdown.js +1 -1
- package/lib/types/components/ThemePicker.d.ts +13 -9
- package/lib/types/components/ThemePicker.d.ts.map +1 -1
- package/lib/types/components/ThemePicker.js +21 -15
- package/lib/types/components/ThinkingToggle.d.ts +1 -1
- package/lib/types/components/ThinkingToggle.d.ts.map +1 -1
- package/lib/types/components/ThinkingToggle.js +8 -8
- package/lib/types/components/ToolUseLoader.d.ts +1 -1
- package/lib/types/components/ToolUseLoader.js +1 -1
- package/lib/types/components/WorkingSpinner.d.ts +2 -2
- package/lib/types/components/WorkingSpinner.js +2 -2
- package/lib/types/components/WorkspaceFlowPicker.d.ts +15 -0
- package/lib/types/components/WorkspaceFlowPicker.d.ts.map +1 -0
- package/lib/types/components/WorkspaceFlowPicker.js +19 -0
- package/lib/types/components/WorkspacePicker.d.ts +9 -0
- package/lib/types/components/WorkspacePicker.d.ts.map +1 -0
- package/lib/types/components/WorkspacePicker.js +13 -0
- package/lib/types/components/approvals/ApprovalPanel.d.ts +1 -1
- package/lib/types/components/approvals/ApprovalPanel.d.ts.map +1 -1
- package/lib/types/components/approvals/ApprovalPanel.js +2 -1
- package/lib/types/components/design-system/Byline.d.ts +1 -1
- package/lib/types/components/design-system/Byline.js +1 -1
- package/lib/types/components/design-system/Divider.d.ts +1 -1
- package/lib/types/components/design-system/Divider.js +1 -1
- package/lib/types/components/design-system/HintLine.d.ts +12 -0
- package/lib/types/components/design-system/HintLine.d.ts.map +1 -0
- package/lib/types/components/design-system/HintLine.js +15 -0
- package/lib/types/components/design-system/KeyboardShortcutHint.d.ts +1 -1
- package/lib/types/components/design-system/KeyboardShortcutHint.js +1 -1
- package/lib/types/components/design-system/ListItem.d.ts +2 -2
- package/lib/types/components/design-system/ListItem.d.ts.map +1 -1
- package/lib/types/components/design-system/ListItem.js +35 -3
- package/lib/types/components/design-system/LoadingState.d.ts +1 -2
- package/lib/types/components/design-system/LoadingState.d.ts.map +1 -1
- package/lib/types/components/design-system/LoadingState.js +1 -2
- package/lib/types/components/design-system/Pane.d.ts +2 -2
- package/lib/types/components/design-system/Pane.js +2 -2
- package/lib/types/components/design-system/ProgressBar.d.ts +1 -2
- package/lib/types/components/design-system/ProgressBar.d.ts.map +1 -1
- package/lib/types/components/design-system/ProgressBar.js +1 -2
- package/lib/types/components/design-system/StatusIcon.d.ts +2 -2
- package/lib/types/components/design-system/StatusIcon.js +2 -2
- package/lib/types/components/design-system/ThemeProvider.d.ts +1 -1
- package/lib/types/components/design-system/ThemeProvider.d.ts.map +1 -1
- package/lib/types/components/design-system/ThemeProvider.js +87 -26
- package/lib/types/components/design-system/ThemedBox.d.ts +1 -1
- package/lib/types/components/design-system/ThemedBox.js +1 -1
- package/lib/types/components/design-system/ThemedText.d.ts +2 -2
- package/lib/types/components/design-system/ThemedText.js +2 -2
- package/lib/types/components/listWindow.d.ts +26 -0
- package/lib/types/components/listWindow.d.ts.map +1 -0
- package/lib/types/components/listWindow.js +47 -0
- package/lib/types/components/messages/AssistantTextMessage.d.ts +1 -2
- package/lib/types/components/messages/AssistantTextMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantTextMessage.js +1 -2
- package/lib/types/components/messages/AssistantThinkingMessage.d.ts +2 -2
- package/lib/types/components/messages/AssistantThinkingMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantThinkingMessage.js +6 -6
- package/lib/types/components/messages/AssistantToolUseMessage.d.ts +12 -4
- package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantToolUseMessage.js +9 -5
- package/lib/types/components/messages/MessageMetadata.d.ts +1 -1
- package/lib/types/components/messages/MessageMetadata.js +1 -1
- package/lib/types/components/messages/UserPromptMessage.d.ts +1 -1
- package/lib/types/components/messages/UserPromptMessage.js +1 -1
- package/lib/types/components/questions/AskUserQuestionPanel.d.ts +5 -1
- package/lib/types/components/questions/AskUserQuestionPanel.d.ts.map +1 -1
- package/lib/types/components/questions/AskUserQuestionPanel.js +11 -9
- package/lib/types/components/questions/PlanReviewPanel.d.ts +1 -1
- package/lib/types/components/questions/PlanReviewPanel.d.ts.map +1 -1
- package/lib/types/components/questions/PlanReviewPanel.js +3 -2
- package/lib/types/components/trajectory/HotspotView.d.ts +14 -0
- package/lib/types/components/trajectory/HotspotView.d.ts.map +1 -0
- package/lib/types/components/trajectory/HotspotView.js +83 -0
- package/lib/types/components/trajectory/Inspector.d.ts +31 -0
- package/lib/types/components/trajectory/Inspector.d.ts.map +1 -0
- package/lib/types/components/trajectory/Inspector.js +67 -0
- package/lib/types/components/trajectory/Ledger.d.ts +20 -0
- package/lib/types/components/trajectory/Ledger.d.ts.map +1 -0
- package/lib/types/components/trajectory/Ledger.js +112 -0
- package/lib/types/components/trajectory/MiniWake.d.ts +24 -0
- package/lib/types/components/trajectory/MiniWake.d.ts.map +1 -0
- package/lib/types/components/trajectory/MiniWake.js +99 -0
- package/lib/types/components/trajectory/WaveBand.d.ts +33 -0
- package/lib/types/components/trajectory/WaveBand.d.ts.map +1 -0
- package/lib/types/components/trajectory/WaveBand.js +145 -0
- package/lib/types/customTheme.d.ts +6 -6
- package/lib/types/customTheme.d.ts.map +1 -1
- package/lib/types/customTheme.js +15 -12
- package/lib/types/dsh-adapter/approvals.d.ts.map +1 -0
- package/lib/types/{channel.d.ts → dsh-adapter/channel.d.ts} +184 -43
- package/lib/types/dsh-adapter/channel.d.ts.map +1 -0
- package/lib/types/{channel.js → dsh-adapter/channel.js} +972 -201
- package/lib/types/dsh-adapter/childStderr.d.ts.map +1 -0
- package/lib/types/{childStderr.js → dsh-adapter/childStderr.js} +1 -1
- package/lib/types/dsh-adapter/command-trees.d.ts +27 -0
- package/lib/types/dsh-adapter/command-trees.d.ts.map +1 -0
- package/lib/types/dsh-adapter/command-trees.js +42 -0
- package/lib/types/dsh-adapter/compat/index.d.ts +22 -0
- package/lib/types/dsh-adapter/compat/index.d.ts.map +1 -0
- package/lib/types/{compat → dsh-adapter/compat}/index.js +7 -4
- package/lib/types/dsh-adapter/compat/sessionLog.d.ts +86 -0
- package/lib/types/dsh-adapter/compat/sessionLog.d.ts.map +1 -0
- package/lib/types/dsh-adapter/compat/sessionLog.js +347 -0
- package/lib/types/dsh-adapter/contract.d.ts +20 -0
- package/lib/types/dsh-adapter/contract.d.ts.map +1 -0
- package/lib/types/dsh-adapter/contract.js +109 -0
- package/lib/types/dsh-adapter/index.d.ts +80 -0
- package/lib/types/dsh-adapter/index.d.ts.map +1 -0
- package/lib/types/dsh-adapter/index.js +45 -0
- package/lib/types/dsh-adapter/invariant.d.ts.map +1 -0
- package/lib/types/dsh-adapter/packaged-skills.d.ts.map +1 -0
- package/lib/types/{plugin.d.ts → dsh-adapter/plugin.d.ts} +17 -0
- package/lib/types/dsh-adapter/plugin.d.ts.map +1 -0
- package/lib/types/{plugin.js → dsh-adapter/plugin.js} +280 -80
- package/lib/types/dsh-adapter/presets.d.ts.map +1 -0
- package/lib/types/dsh-adapter/providerWizard.d.ts +91 -0
- package/lib/types/dsh-adapter/providerWizard.d.ts.map +1 -0
- package/lib/types/dsh-adapter/providerWizard.js +332 -0
- package/lib/types/{questions.d.ts → dsh-adapter/questions.d.ts} +7 -2
- package/lib/types/dsh-adapter/questions.d.ts.map +1 -0
- package/lib/types/{questions.js → dsh-adapter/questions.js} +15 -7
- package/lib/types/dsh-adapter/sideQuestion.d.ts +38 -0
- package/lib/types/dsh-adapter/sideQuestion.d.ts.map +1 -0
- package/lib/types/dsh-adapter/sideQuestion.js +69 -0
- package/lib/types/dsh-adapter/trajectory/aggregate.d.ts +46 -0
- package/lib/types/dsh-adapter/trajectory/aggregate.d.ts.map +1 -0
- package/lib/types/dsh-adapter/trajectory/aggregate.js +207 -0
- package/lib/types/dsh-adapter/trajectory/guards.d.ts +177 -0
- package/lib/types/dsh-adapter/trajectory/guards.d.ts.map +1 -0
- package/lib/types/dsh-adapter/trajectory/guards.js +235 -0
- package/lib/types/dsh-adapter/trajectory/index.d.ts +16 -0
- package/lib/types/dsh-adapter/trajectory/index.d.ts.map +1 -0
- package/lib/types/dsh-adapter/trajectory/index.js +15 -0
- package/lib/types/dsh-adapter/trajectory/inspect.d.ts +40 -0
- package/lib/types/dsh-adapter/trajectory/inspect.d.ts.map +1 -0
- package/lib/types/dsh-adapter/trajectory/inspect.js +160 -0
- package/lib/types/dsh-adapter/trajectory/projection.d.ts +128 -0
- package/lib/types/dsh-adapter/trajectory/projection.d.ts.map +1 -0
- package/lib/types/dsh-adapter/trajectory/projection.js +653 -0
- package/lib/types/dsh-adapter/trajectory/types.d.ts +232 -0
- package/lib/types/dsh-adapter/trajectory/types.d.ts.map +1 -0
- package/lib/types/dsh-adapter/trajectory/types.js +70 -0
- package/lib/types/dsh-adapter/trajectory/wave.d.ts +45 -0
- package/lib/types/dsh-adapter/trajectory/wave.d.ts.map +1 -0
- package/lib/types/dsh-adapter/trajectory/wave.js +210 -0
- package/lib/types/dsh-adapter/types.d.ts +23 -0
- package/lib/types/dsh-adapter/types.d.ts.map +1 -0
- package/lib/types/dsh-adapter/types.js +1 -0
- package/lib/types/dsh-adapter/workspace.d.ts +22 -0
- package/lib/types/dsh-adapter/workspace.d.ts.map +1 -0
- package/lib/types/dsh-adapter/workspace.js +16 -0
- package/lib/types/dsh-adapter/workspaces.d.ts +118 -0
- package/lib/types/dsh-adapter/workspaces.d.ts.map +1 -0
- package/lib/types/dsh-adapter/workspaces.js +290 -0
- package/lib/types/effortPrefs.d.ts +1 -1
- package/lib/types/effortPrefs.js +3 -3
- package/lib/types/history.js +2 -2
- package/lib/types/hooks/useBlink.d.ts +1 -1
- package/lib/types/hooks/useBlink.js +1 -1
- package/lib/types/i18n.d.ts +977 -36
- package/lib/types/i18n.d.ts.map +1 -1
- package/lib/types/i18n.js +307 -30
- package/lib/types/index.d.ts +1 -69
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +3 -31
- package/lib/types/ink/components/App.d.ts.map +1 -1
- package/lib/types/ink/components/App.js +33 -19
- package/lib/types/ink/dom.d.ts +15 -0
- package/lib/types/ink/dom.d.ts.map +1 -1
- package/lib/types/ink/dom.js +32 -0
- package/lib/types/ink/hooks/use-input.d.ts.map +1 -1
- package/lib/types/ink/hooks/use-input.js +8 -0
- package/lib/types/ink/hooks/use-terminal-size.d.ts +1 -1
- package/lib/types/ink/hooks/use-terminal-size.js +1 -1
- package/lib/types/ink/ink.d.ts.map +1 -1
- package/lib/types/ink/ink.js +109 -28
- package/lib/types/ink/input-suppression.d.ts +20 -0
- package/lib/types/ink/input-suppression.d.ts.map +1 -0
- package/lib/types/ink/input-suppression.js +26 -0
- package/lib/types/ink/log-update.js +21 -6
- package/lib/types/ink/parse-keypress.d.ts +31 -0
- package/lib/types/ink/parse-keypress.d.ts.map +1 -1
- package/lib/types/ink/parse-keypress.js +442 -8
- package/lib/types/ink/terminal-querier.d.ts +5 -1
- package/lib/types/ink/terminal-querier.d.ts.map +1 -1
- package/lib/types/ink/terminal-querier.js +27 -3
- package/lib/types/ink/terminal.d.ts +18 -0
- package/lib/types/ink/terminal.d.ts.map +1 -1
- package/lib/types/ink/terminal.js +32 -4
- package/lib/types/ink/termio/csi.d.ts +10 -0
- package/lib/types/ink/termio/csi.d.ts.map +1 -1
- package/lib/types/ink/termio/csi.js +20 -0
- package/lib/types/ink/termio/tokenize.js +10 -0
- package/lib/types/modelPrefs.d.ts +1 -1
- package/lib/types/modelPrefs.js +3 -3
- package/lib/types/presetPrefs.d.ts +1 -1
- package/lib/types/presetPrefs.js +3 -3
- package/lib/types/screens/Chat.d.ts +20 -4
- package/lib/types/screens/Chat.d.ts.map +1 -1
- package/lib/types/screens/Chat.js +655 -55
- package/lib/types/screens/StatusLine.d.ts +17 -2
- package/lib/types/screens/StatusLine.d.ts.map +1 -1
- package/lib/types/screens/StatusLine.js +12 -4
- package/lib/types/screens/TrajectoryScene.d.ts +16 -0
- package/lib/types/screens/TrajectoryScene.d.ts.map +1 -0
- package/lib/types/screens/TrajectoryScene.js +305 -0
- package/lib/types/sessionHistory.d.ts +9 -1
- package/lib/types/sessionHistory.d.ts.map +1 -1
- package/lib/types/sessionHistory.js +56 -16
- package/lib/types/sessionModes.d.ts +29 -0
- package/lib/types/sessionModes.d.ts.map +1 -0
- package/lib/types/sessionModes.js +45 -0
- package/lib/types/theme.d.ts +27 -3
- package/lib/types/theme.d.ts.map +1 -1
- package/lib/types/theme.js +48 -4
- package/lib/types/themePrefs.d.ts +3 -3
- package/lib/types/themePrefs.js +5 -5
- package/lib/types/trajectory/format.d.ts +100 -0
- package/lib/types/trajectory/format.d.ts.map +1 -0
- package/lib/types/trajectory/format.js +213 -0
- package/lib/types/trajectory/motion.d.ts +99 -0
- package/lib/types/trajectory/motion.d.ts.map +1 -0
- package/lib/types/trajectory/motion.js +143 -0
- package/lib/types/trajectory/query.d.ts +83 -0
- package/lib/types/trajectory/query.d.ts.map +1 -0
- package/lib/types/trajectory/query.js +187 -0
- package/lib/types/trajectoryPrefs.d.ts +36 -0
- package/lib/types/trajectoryPrefs.d.ts.map +1 -0
- package/lib/types/trajectoryPrefs.js +65 -0
- package/lib/types/update.d.ts +3 -3
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +18 -7
- package/lib/types/utils/clipboard.d.ts +101 -29
- package/lib/types/utils/clipboard.d.ts.map +1 -1
- package/lib/types/utils/clipboard.js +533 -55
- package/lib/types/utils/debug.d.ts +1 -1
- package/lib/types/utils/debug.js +2 -2
- package/lib/types/utils/execFileNoThrow.d.ts.map +1 -1
- package/lib/types/utils/execFileNoThrow.js +16 -6
- package/lib/types/utils/externalEditor.d.ts +115 -0
- package/lib/types/utils/externalEditor.d.ts.map +1 -0
- package/lib/types/utils/externalEditor.js +270 -0
- package/lib/types/utils/fullscreen.d.ts +2 -2
- package/lib/types/utils/fullscreen.js +3 -3
- package/lib/types/utils/loaded-context.d.ts +1 -1
- package/lib/types/utils/loaded-context.d.ts.map +1 -1
- package/lib/types/utils/modifiers.d.ts +24 -0
- package/lib/types/utils/modifiers.d.ts.map +1 -1
- package/lib/types/utils/modifiers.js +27 -0
- package/lib/types/utils/paths.d.ts +45 -0
- package/lib/types/utils/paths.d.ts.map +1 -0
- package/lib/types/utils/paths.js +69 -0
- package/lib/types/utils/shellQuote.d.ts +14 -0
- package/lib/types/utils/shellQuote.d.ts.map +1 -0
- package/lib/types/utils/shellQuote.js +39 -0
- package/lib/types/utils/workspaceRoot.d.ts +21 -0
- package/lib/types/utils/workspaceRoot.d.ts.map +1 -0
- package/lib/types/utils/workspaceRoot.js +47 -0
- package/lib/types/workspaces.d.ts +3 -0
- package/lib/types/workspaces.d.ts.map +1 -0
- package/lib/types/workspaces.js +4 -0
- package/package.json +25 -7
- package/lib/types/approvals.d.ts.map +0 -1
- package/lib/types/channel.d.ts.map +0 -1
- package/lib/types/childStderr.d.ts.map +0 -1
- package/lib/types/compat/index.d.ts +0 -20
- package/lib/types/compat/index.d.ts.map +0 -1
- package/lib/types/compat/sessionLog.d.ts +0 -40
- package/lib/types/compat/sessionLog.d.ts.map +0 -1
- package/lib/types/compat/sessionLog.js +0 -226
- package/lib/types/invariant.d.ts.map +0 -1
- package/lib/types/packaged-skills.d.ts.map +0 -1
- package/lib/types/plugin.d.ts.map +0 -1
- package/lib/types/presets.d.ts.map +0 -1
- package/lib/types/questions.d.ts.map +0 -1
- /package/lib/types/{approvals.d.ts → dsh-adapter/approvals.d.ts} +0 -0
- /package/lib/types/{approvals.js → dsh-adapter/approvals.js} +0 -0
- /package/lib/types/{childStderr.d.ts → dsh-adapter/childStderr.d.ts} +0 -0
- /package/lib/types/{invariant.d.ts → dsh-adapter/invariant.d.ts} +0 -0
- /package/lib/types/{invariant.js → dsh-adapter/invariant.js} +0 -0
- /package/lib/types/{packaged-skills.d.ts → dsh-adapter/packaged-skills.d.ts} +0 -0
- /package/lib/types/{packaged-skills.js → dsh-adapter/packaged-skills.js} +0 -0
- /package/lib/types/{presets.d.ts → dsh-adapter/presets.d.ts} +0 -0
- /package/lib/types/{presets.js → dsh-adapter/presets.js} +0 -0
|
@@ -1,25 +1,31 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { assembleContextFor, installModelSelection } from '@deepseek-ai/dsh-agent';
|
|
3
|
+
import { isUserInvocable } from '@deepseek-ai/dsh-skill';
|
|
3
4
|
import { createUserMessage, isTokenDelta, MessageId, ReasoningEffortId, } from '@deepseek-ai/dsh-llm';
|
|
5
|
+
import { runSideQuestion, wrapSideQuestion } from './sideQuestion.js';
|
|
4
6
|
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
5
7
|
import { renderContextSections, renderPrompt } from '@deepseek-ai/dsh-system-prompt';
|
|
6
|
-
import {
|
|
7
|
-
import { isAbsolute, join } from 'node:path';
|
|
8
|
-
import { LOCAL_COMMANDS } from '
|
|
9
|
-
import { clearResumeTarget, readLastUsed, touchSession, writeResumeTarget } from '
|
|
10
|
-
import {
|
|
11
|
-
import { writeActivityFrames } from '
|
|
12
|
-
import { readEffortPref, writeEffortPref } from '
|
|
13
|
-
import { readModelPref, writeModelPref } from '
|
|
14
|
-
import { explicitModelRoute, recordedModelRoute, resolveModelRoute, validateModelRoute } from '
|
|
15
|
-
import { readPresetPref, writePresetPref } from '
|
|
8
|
+
import { loadBaselineInstructions } from '@deepseek-ai/dsh-agent-instructions';
|
|
9
|
+
import { extname, isAbsolute, join } from 'node:path';
|
|
10
|
+
import { completeCommands, LOCAL_COMMANDS } from '../commands.js';
|
|
11
|
+
import { clearResumeTarget, forgetSession, readLastUsed, readResumeTarget, touchSession, writeResumeTarget } from '../sessionHistory.js';
|
|
12
|
+
import { appendSessionTitle, deleteSessionLog, ensureLegacySessionEventTypes, readSessionTitleFromLog, sessionsRoots } from './compat/index.js';
|
|
13
|
+
import { writeActivityFrames } from '../activityPrefs.js';
|
|
14
|
+
import { readEffortPref, writeEffortPref } from '../effortPrefs.js';
|
|
15
|
+
import { readModelPref, writeModelPref } from '../modelPrefs.js';
|
|
16
|
+
import { explicitModelRoute, recordedModelRoute, resolveModelRoute, validateModelRoute } from '../modelRoute.js';
|
|
17
|
+
import { readPresetPref, writePresetPref } from '../presetPrefs.js';
|
|
16
18
|
import { composePreset, resolvePersistedPreset, rosterOf, runningPresetOf, serviceForAgent } from './presets.js';
|
|
17
|
-
import { isPresetName } from '
|
|
18
|
-
import { existsSync, writeFileSync } from 'node:fs';
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import { extractMentions } from '
|
|
22
|
-
import { t } from '
|
|
19
|
+
import { isPresetName } from '../components/activityFrames.js';
|
|
20
|
+
import { existsSync, statSync, writeFileSync } from 'node:fs';
|
|
21
|
+
import { logForDebugging } from '../utils/debug.js';
|
|
22
|
+
import { homeDir, LEGACY_DATA_DIR } from '../utils/paths.js';
|
|
23
|
+
import { extractMentions } from '../utils/mentions.js';
|
|
24
|
+
import { t } from '../i18n.js';
|
|
25
|
+
import { modeDisplayName, resolveSessionModes } from '../sessionModes.js';
|
|
26
|
+
import { ActivityTracker } from 'dsh-working-activity/status';
|
|
27
|
+
import { attachSessionToWorkspace } from './workspace.js';
|
|
28
|
+
import { createLocalWorkspaceRuntime } from './workspaces.js';
|
|
23
29
|
const ARGS_PREVIEW_LIMIT = 160;
|
|
24
30
|
const RESULT_PREVIEW_LIMIT = 240;
|
|
25
31
|
/** Local `!`-command output cap (mirrors the result preview limit). */
|
|
@@ -172,6 +178,21 @@ function restoreRowFromEvent(row, event) {
|
|
|
172
178
|
break;
|
|
173
179
|
}
|
|
174
180
|
}
|
|
181
|
+
/** Render the durable tool-result payload, including provider error details. */
|
|
182
|
+
function toolResultText(event) {
|
|
183
|
+
const block = event.data.message.content[0];
|
|
184
|
+
if (block === undefined || block.type !== 'tool-result')
|
|
185
|
+
return '';
|
|
186
|
+
return block.content.map(item => item.type === 'text' ? item.text : '').join('').trim();
|
|
187
|
+
}
|
|
188
|
+
function toolErrorText(event) {
|
|
189
|
+
const failure = event.data.error;
|
|
190
|
+
if (failure === undefined)
|
|
191
|
+
return '';
|
|
192
|
+
const identity = `${failure.name}: ${failure.code}`;
|
|
193
|
+
const detail = toolResultText(event);
|
|
194
|
+
return detail === '' || detail === identity ? identity : `${identity} — ${detail}`;
|
|
195
|
+
}
|
|
175
196
|
/** Restore a folded tool row's result text from its tool/result event. */
|
|
176
197
|
function restoreToolResult(row, event) {
|
|
177
198
|
if (row.tool === undefined)
|
|
@@ -179,12 +200,11 @@ function restoreToolResult(row, event) {
|
|
|
179
200
|
const failure = event.data.error;
|
|
180
201
|
if (failure !== undefined) {
|
|
181
202
|
row.tool.status = 'error';
|
|
182
|
-
row.tool.errorText =
|
|
203
|
+
row.tool.errorText = toolErrorText(event);
|
|
183
204
|
return;
|
|
184
205
|
}
|
|
185
206
|
row.tool.status = 'ok';
|
|
186
|
-
const
|
|
187
|
-
const result = block.content.map(b => b.type === 'text' ? b.text : '').join('').trim();
|
|
207
|
+
const result = toolResultText(event);
|
|
188
208
|
row.tool.resultFull = result || undefined;
|
|
189
209
|
}
|
|
190
210
|
/**
|
|
@@ -278,6 +298,14 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
278
298
|
// command/run + command/done records). Absent the service, only the
|
|
279
299
|
// built-in local commands exist.
|
|
280
300
|
const commandService = ctx.get('commands');
|
|
301
|
+
const workspaceService = ctx.tuiWorkspaces ?? createLocalWorkspaceRuntime();
|
|
302
|
+
const commandTrees = ctx.get('tuiCommandTrees');
|
|
303
|
+
// Shift+Tab session-mode cycle: cordis.yml `modes` wins; absent/empty/
|
|
304
|
+
// atom-less → the built-in default/plan/full cycle (sessionModes.ts).
|
|
305
|
+
const { modes: sessionModes, dropped: droppedModeIds } = resolveSessionModes(options.modes);
|
|
306
|
+
if (droppedModeIds.length > 0) {
|
|
307
|
+
ctx.logger.warn(`dsh-tui: session modes ${droppedModeIds.map(id => `"${id}"`).join(', ')} declare no plan/sandbox/approval atom; dropped from the Shift+Tab cycle`);
|
|
308
|
+
}
|
|
281
309
|
const listeners = new Set();
|
|
282
310
|
/** True while a frame-aligned stream notification is pending (emitStream). */
|
|
283
311
|
let streamNotifyScheduled = false;
|
|
@@ -292,7 +320,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
292
320
|
return;
|
|
293
321
|
contextWarned = true;
|
|
294
322
|
const percentLeft = Math.max(0, Math.round((remaining / state.contextWindow) * 100));
|
|
295
|
-
state.notify(
|
|
323
|
+
state.notify(t('context-low-warning', { percent: percentLeft }), { color: 'warning', timeoutMs: 8000 });
|
|
296
324
|
};
|
|
297
325
|
/**
|
|
298
326
|
* Register a submitted message as pending and notify the UI. The inbox
|
|
@@ -316,6 +344,12 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
316
344
|
* through this chain to keep the send order FIFO.
|
|
317
345
|
*/
|
|
318
346
|
let sendChain = Promise.resolve();
|
|
347
|
+
let stagedImageSequence = 0;
|
|
348
|
+
const stagedImages = new Map();
|
|
349
|
+
const clearStagedImages = () => {
|
|
350
|
+
stagedImages.clear();
|
|
351
|
+
stagedImageSequence = 0;
|
|
352
|
+
};
|
|
319
353
|
/**
|
|
320
354
|
* Expand the text's `@` mentions and deliver ONE user message: the typed
|
|
321
355
|
* text stays the first content block (the transcript bubble renders it —
|
|
@@ -324,7 +358,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
324
358
|
*/
|
|
325
359
|
const deliverUserText = (text, placement) => {
|
|
326
360
|
sendChain = sendChain.then(async () => {
|
|
327
|
-
const expansion = await expandMentions(mentionFs(ctx), state.cwd, text);
|
|
361
|
+
const expansion = await expandMentions(mentionFs(ctx), state.cwd, text, mentionAttachments(ctx), stagedImages);
|
|
328
362
|
const message = createUserMessage({
|
|
329
363
|
content: expansion.blocks,
|
|
330
364
|
source: { kind: 'user' },
|
|
@@ -393,51 +427,191 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
393
427
|
// provider default in effect.
|
|
394
428
|
}
|
|
395
429
|
};
|
|
396
|
-
/**
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
state.notify(t('effort-unavailable'), { color: 'error' });
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
|
-
let efforts;
|
|
406
|
-
let defaultEffort;
|
|
430
|
+
/** Resolve the live route's effort levels + adapter default through the
|
|
431
|
+
* llm runtime; 'unavailable' when the service is unmounted, 'error' when
|
|
432
|
+
* resolution throws (notified here). */
|
|
433
|
+
const resolveEfforts = async () => {
|
|
434
|
+
if (llmRuntime === undefined)
|
|
435
|
+
return 'unavailable';
|
|
407
436
|
try {
|
|
408
437
|
const info = await llmRuntime.resolveModelInfo(state.provider, state.model);
|
|
409
|
-
|
|
410
|
-
|
|
438
|
+
return {
|
|
439
|
+
efforts: info.reasoning?.efforts ?? [],
|
|
440
|
+
defaultEffort: info.reasoning?.defaultEffort,
|
|
441
|
+
};
|
|
411
442
|
}
|
|
412
443
|
catch (error) {
|
|
413
444
|
state.notify(t('effort-read-failed', { error: error instanceof Error ? error.message : String(error) }), {
|
|
414
445
|
color: 'error',
|
|
415
446
|
timeoutMs: 8000,
|
|
416
447
|
});
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
if (efforts.length <= 1) {
|
|
420
|
-
state.notify(efforts.length === 1
|
|
421
|
-
? t('effort-single-tier', { name: efforts[0].name })
|
|
422
|
-
: t('effort-unsupported'), { color: 'warning' });
|
|
423
|
-
return;
|
|
448
|
+
return 'error';
|
|
424
449
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
const next = efforts[(currentIndex + 1) % efforts.length];
|
|
450
|
+
};
|
|
451
|
+
/** Pin one validated effort level on the live route: reroutes the next
|
|
452
|
+
* request, persists the choice, and refreshes the StatusLine segment. */
|
|
453
|
+
const applyEffort = (effort) => {
|
|
430
454
|
selection.current = {
|
|
431
455
|
provider: state.provider,
|
|
432
456
|
model: state.model,
|
|
433
|
-
reasoningEffort: ReasoningEffortId(
|
|
457
|
+
reasoningEffort: ReasoningEffortId(effort.id),
|
|
434
458
|
};
|
|
435
|
-
preferredEffort =
|
|
436
|
-
state.reasoningEffort =
|
|
437
|
-
writeEffortPref(
|
|
438
|
-
state.notify(t('effort-switched', { name:
|
|
459
|
+
preferredEffort = effort.id;
|
|
460
|
+
state.reasoningEffort = effort.id;
|
|
461
|
+
writeEffortPref(effort.id);
|
|
462
|
+
state.notify(t('effort-switched', { name: effort.name }));
|
|
439
463
|
state.emit();
|
|
440
464
|
};
|
|
465
|
+
/** The live route's effort levels for the `/effort` slider; empty after
|
|
466
|
+
* notifying when the route is unsupported/unavailable/single-tier. */
|
|
467
|
+
const listEfforts = async () => {
|
|
468
|
+
const resolved = await resolveEfforts();
|
|
469
|
+
if (resolved === 'unavailable') {
|
|
470
|
+
state.notify(t('effort-unavailable'), { color: 'error' });
|
|
471
|
+
return { efforts: [], defaultEffort: undefined };
|
|
472
|
+
}
|
|
473
|
+
if (resolved === 'error')
|
|
474
|
+
return { efforts: [], defaultEffort: undefined };
|
|
475
|
+
if (resolved.efforts.length === 0) {
|
|
476
|
+
state.notify(t('effort-unsupported'), { color: 'warning' });
|
|
477
|
+
}
|
|
478
|
+
else if (resolved.efforts.length === 1) {
|
|
479
|
+
state.notify(t('effort-single-tier', { name: resolved.efforts[0].name }), { color: 'warning' });
|
|
480
|
+
}
|
|
481
|
+
return resolved;
|
|
482
|
+
};
|
|
483
|
+
/** Set one effort level by id (`/effort <id>` and the slider's live
|
|
484
|
+
* apply); false + a notify when the id is not offered by the route. */
|
|
485
|
+
const setEffort = async (id) => {
|
|
486
|
+
const resolved = await resolveEfforts();
|
|
487
|
+
if (resolved === 'unavailable') {
|
|
488
|
+
state.notify(t('effort-unavailable'), { color: 'error' });
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
if (resolved === 'error')
|
|
492
|
+
return false;
|
|
493
|
+
if (resolved.efforts.length === 0) {
|
|
494
|
+
state.notify(t('effort-unsupported'), { color: 'warning' });
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
const found = resolved.efforts.find(effort => effort.id === id);
|
|
498
|
+
if (!found) {
|
|
499
|
+
state.notify(t('effort-invalid', { id, ids: resolved.efforts.map(effort => effort.id).join(', ') }), { color: 'warning' });
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
applyEffort(found);
|
|
503
|
+
return true;
|
|
504
|
+
};
|
|
505
|
+
/** Run one DSH registry command (`/plan`, …) on the live agent; the text
|
|
506
|
+
* of its result, '' when the result is textless, undefined when the
|
|
507
|
+
* command is not registered, and the error message when it throws. */
|
|
508
|
+
const executeRegistryCommand = async (name, rawInput) => {
|
|
509
|
+
if (!commandService)
|
|
510
|
+
return undefined;
|
|
511
|
+
try {
|
|
512
|
+
const execution = await commandService.execute(agent, `/${name}${rawInput}`, new AbortController().signal);
|
|
513
|
+
// `undefined` = not registered; a handler error surfaces as its
|
|
514
|
+
// message so the user sees why the command failed.
|
|
515
|
+
return execution?.result.text ?? '';
|
|
516
|
+
}
|
|
517
|
+
catch (error) {
|
|
518
|
+
return error instanceof Error ? error.message : String(error);
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
// Session-mode folds: last-wins projections over the session log. The
|
|
522
|
+
// event types are registered by dsh-plan-mode / dsh-sandbox-policy /
|
|
523
|
+
// dsh-user-approval and are NOT in this package's typed SessionEvent
|
|
524
|
+
// union, so they are matched by name through casts — the same pattern as
|
|
525
|
+
// `agent-preset/selected` in renderEvent and the goal projection above.
|
|
526
|
+
const foldPlanActive = (events) => {
|
|
527
|
+
let active = false;
|
|
528
|
+
for (const event of events) {
|
|
529
|
+
if (event.type === 'plan/mode') {
|
|
530
|
+
active = event.data.active === true;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return active;
|
|
534
|
+
};
|
|
535
|
+
const foldSandboxMode = (events) => {
|
|
536
|
+
let mode;
|
|
537
|
+
for (const event of events) {
|
|
538
|
+
if (event.type === 'sandbox/mode') {
|
|
539
|
+
const value = event.data.mode;
|
|
540
|
+
if (typeof value === 'string')
|
|
541
|
+
mode = value;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return mode;
|
|
545
|
+
};
|
|
546
|
+
const foldApprovalPolicy = (events) => {
|
|
547
|
+
let policy;
|
|
548
|
+
for (const event of events) {
|
|
549
|
+
if (event.type === 'approval/policy') {
|
|
550
|
+
const value = event.data.policy;
|
|
551
|
+
if (typeof value === 'string')
|
|
552
|
+
policy = value;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return policy;
|
|
556
|
+
};
|
|
557
|
+
/** First configured mode whose declared atoms all match the folds;
|
|
558
|
+
* undeclared atoms are wildcards; no match → index 0 (the base mode).
|
|
559
|
+
* Matching is exact: a fresh session has no `approval/policy` event, so
|
|
560
|
+
* a mode declaring `approval: 'ask'` never falsely matches it. */
|
|
561
|
+
const deriveModeIndex = (events) => {
|
|
562
|
+
const index = sessionModes.findIndex(spec => (spec.plan === undefined || foldPlanActive(events) === spec.plan) &&
|
|
563
|
+
(spec.sandbox === undefined || foldSandboxMode(events) === spec.sandbox) &&
|
|
564
|
+
(spec.approval === undefined || foldApprovalPolicy(events) === spec.approval));
|
|
565
|
+
return index >= 0 ? index : 0;
|
|
566
|
+
};
|
|
567
|
+
/** Re-derive the current mode from the live session log (boot, every
|
|
568
|
+
* agent re-bind, and after mode-affecting session events). */
|
|
569
|
+
const refreshMode = () => {
|
|
570
|
+
state.modeIndex = deriveModeIndex(agent.session.events);
|
|
571
|
+
state.mode = sessionModes[state.modeIndex];
|
|
572
|
+
};
|
|
573
|
+
/** Apply one configured mode: each declared atom switches independently
|
|
574
|
+
* (plan via the registry `/plan` command; sandbox/approval via their
|
|
575
|
+
* durable session-log override events). A failing plan toggle aborts the
|
|
576
|
+
* whole switch so the session never lands in a half-applied mode. */
|
|
577
|
+
const applyMode = async (spec) => {
|
|
578
|
+
if (spec.plan !== undefined && foldPlanActive(agent.session.events) !== spec.plan) {
|
|
579
|
+
const text = await executeRegistryCommand('plan', spec.plan ? '' : ' off');
|
|
580
|
+
if (text === undefined) {
|
|
581
|
+
// The active preset registers no /plan.
|
|
582
|
+
state.notify(t('mode-plan-unavailable'), { color: 'warning' });
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
// The durable sandbox override is one session event (dsh-sandbox-policy's
|
|
587
|
+
// own write path); the session/event arm picks it up immediately.
|
|
588
|
+
if (spec.sandbox !== undefined && foldSandboxMode(agent.session.events) !== spec.sandbox) {
|
|
589
|
+
;
|
|
590
|
+
agent.session.append('sandbox/mode', { mode: spec.sandbox });
|
|
591
|
+
}
|
|
592
|
+
// Prefer the approval service (it narrates the switch to the model);
|
|
593
|
+
// the raw durable event is the fallback when it is unmounted.
|
|
594
|
+
if (spec.approval !== undefined && foldApprovalPolicy(agent.session.events) !== spec.approval) {
|
|
595
|
+
const approval = ctx.get('approval');
|
|
596
|
+
if (approval) {
|
|
597
|
+
approval.setPolicy(agent, spec.approval);
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
;
|
|
601
|
+
agent.session.append('approval/policy', { policy: spec.approval });
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
refreshMode();
|
|
605
|
+
state.notify(t('mode-switched', { name: modeDisplayName(state.mode) }));
|
|
606
|
+
state.emit();
|
|
607
|
+
};
|
|
608
|
+
/** Shift+Tab: advance to the next configured session mode. Cycling starts
|
|
609
|
+
* from the mode DERIVED from the session log (never a stored index), so
|
|
610
|
+
* manual `/plan` use can never desync the cycle. */
|
|
611
|
+
const cycleMode = async () => {
|
|
612
|
+
const index = deriveModeIndex(agent.session.events);
|
|
613
|
+
await applyMode(sessionModes[(index + 1) % sessionModes.length]);
|
|
614
|
+
};
|
|
441
615
|
const state = {
|
|
442
616
|
version: 0,
|
|
443
617
|
rows: [],
|
|
@@ -448,6 +622,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
448
622
|
provider: options.provider,
|
|
449
623
|
tokens: { input: 0, output: 0 },
|
|
450
624
|
cwd: options.cwd,
|
|
625
|
+
displayCwd: workspaceService.describe(options.cwd).description ?? options.cwd,
|
|
451
626
|
gitBranch: undefined,
|
|
452
627
|
working: false,
|
|
453
628
|
spinnerMode: 'requesting',
|
|
@@ -457,9 +632,13 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
457
632
|
lastUserText: '',
|
|
458
633
|
notifications: [],
|
|
459
634
|
contextWindow: undefined,
|
|
460
|
-
// Explicit cordis.yml `effort` wins; otherwise the persisted
|
|
635
|
+
// Explicit cordis.yml `effort` wins; otherwise the persisted /effort
|
|
461
636
|
// choice; the first request/header event re-asserts the adapter's truth.
|
|
462
637
|
reasoningEffort: options.effort ?? readEffortPref(),
|
|
638
|
+
// Session-mode seed; the first refreshMode() (bindAgent) re-derives it
|
|
639
|
+
// from the session log, so a resumed session lands on its recorded mode.
|
|
640
|
+
mode: sessionModes[0],
|
|
641
|
+
modeIndex: 0,
|
|
463
642
|
workingActivity: undefined,
|
|
464
643
|
activityFrames: options.activityFrames,
|
|
465
644
|
activityEnabled: options.activity !== false,
|
|
@@ -470,6 +649,29 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
470
649
|
loadedContext: undefined,
|
|
471
650
|
pending: [],
|
|
472
651
|
commandList: LOCAL_COMMANDS,
|
|
652
|
+
commandCompletions(input) {
|
|
653
|
+
return completeCommands(input, state.commandList, (path) => {
|
|
654
|
+
if (path.length === 1 && path[0] === 'workspace') {
|
|
655
|
+
const builtins = [
|
|
656
|
+
{ name: 'resume', description: 'Switch to another workspace', descriptionKey: 'cmd-desc-workspace-resume' },
|
|
657
|
+
{ name: 'rename', description: 'Rename the current workspace', descriptionKey: 'cmd-desc-workspace-rename' },
|
|
658
|
+
{ name: 'open', description: 'Open a path or workspace URI', descriptionKey: 'cmd-desc-workspace-open' },
|
|
659
|
+
];
|
|
660
|
+
const reserved = new Set(builtins.map(command => command.name));
|
|
661
|
+
return [
|
|
662
|
+
...builtins,
|
|
663
|
+
...workspaceService.commands()
|
|
664
|
+
.filter(command => !reserved.has(command.name.toLowerCase()))
|
|
665
|
+
.map(command => ({
|
|
666
|
+
name: command.name,
|
|
667
|
+
aliases: command.aliases,
|
|
668
|
+
description: command.description,
|
|
669
|
+
})),
|
|
670
|
+
];
|
|
671
|
+
}
|
|
672
|
+
return commandTrees?.children(path) ?? [];
|
|
673
|
+
});
|
|
674
|
+
},
|
|
473
675
|
lastUsage: undefined,
|
|
474
676
|
tps: undefined,
|
|
475
677
|
tpsSamples: [],
|
|
@@ -522,6 +724,30 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
522
724
|
state.emit();
|
|
523
725
|
return restored;
|
|
524
726
|
},
|
|
727
|
+
async stageImage(input) {
|
|
728
|
+
const attachments = mentionAttachments(ctx);
|
|
729
|
+
if (attachments === undefined)
|
|
730
|
+
throw new Error('image attachments are unavailable in this profile');
|
|
731
|
+
if (!attachments.imageLimits.mediaTypes.includes(input.mediaType)) {
|
|
732
|
+
throw new Error(`${input.mediaType} images are not accepted by this profile`);
|
|
733
|
+
}
|
|
734
|
+
if (input.data.byteLength > attachments.imageLimits.maxImageBytes) {
|
|
735
|
+
throw new Error(`image exceeds this profile's per-image size limit`);
|
|
736
|
+
}
|
|
737
|
+
const attachment = await attachments.saveImage(input);
|
|
738
|
+
stagedImageSequence += 1;
|
|
739
|
+
const token = `[Image #${stagedImageSequence}]`;
|
|
740
|
+
stagedImages.set(token, attachment);
|
|
741
|
+
// References are content-addressed and durable. This map only connects
|
|
742
|
+
// editable prompt placeholders to them; cap it to bound a long TUI run.
|
|
743
|
+
while (stagedImages.size > 128) {
|
|
744
|
+
const oldest = stagedImages.keys().next().value;
|
|
745
|
+
if (oldest === undefined)
|
|
746
|
+
break;
|
|
747
|
+
stagedImages.delete(oldest);
|
|
748
|
+
}
|
|
749
|
+
return token;
|
|
750
|
+
},
|
|
525
751
|
submit(text) {
|
|
526
752
|
const trimmed = text.trim();
|
|
527
753
|
if (!trimmed)
|
|
@@ -619,7 +845,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
619
845
|
const sessions = ctx.get('sessions');
|
|
620
846
|
const agents = ctx.get('agents');
|
|
621
847
|
if (!sessions || !agents) {
|
|
622
|
-
state.notify('
|
|
848
|
+
state.notify(t('rewind-unavailable'), { color: 'error' });
|
|
623
849
|
return null;
|
|
624
850
|
}
|
|
625
851
|
// Stop a running turn first and WAIT for its turn/end to land — fork
|
|
@@ -632,7 +858,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
632
858
|
if (wasWorking) {
|
|
633
859
|
const turnSettled = await waitForTurnEnd(agent.session, cancelSeq, 30000);
|
|
634
860
|
if (!turnSettled) {
|
|
635
|
-
state.notify('
|
|
861
|
+
state.notify(t('rewind-settling'), { color: 'error' });
|
|
636
862
|
return null;
|
|
637
863
|
}
|
|
638
864
|
}
|
|
@@ -669,7 +895,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
669
895
|
}
|
|
670
896
|
catch (error) {
|
|
671
897
|
const message = error instanceof Error ? error.message : String(error);
|
|
672
|
-
state.notify(
|
|
898
|
+
state.notify(t('rewind-fork-failed', { err: message }), { color: 'error' });
|
|
673
899
|
return null;
|
|
674
900
|
}
|
|
675
901
|
let handle;
|
|
@@ -684,7 +910,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
684
910
|
sessionId: childId,
|
|
685
911
|
seed,
|
|
686
912
|
meta: {
|
|
687
|
-
cwd:
|
|
913
|
+
cwd: state.cwd,
|
|
688
914
|
parentSession: agent.session.id,
|
|
689
915
|
seedLength: seed.length,
|
|
690
916
|
...(rewindComposed.agentPreset === undefined
|
|
@@ -696,9 +922,15 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
696
922
|
});
|
|
697
923
|
}
|
|
698
924
|
catch {
|
|
699
|
-
state.notify('
|
|
925
|
+
state.notify(t('rewind-create-failed'), { color: 'error' });
|
|
700
926
|
return null;
|
|
701
927
|
}
|
|
928
|
+
try {
|
|
929
|
+
await attachSessionToWorkspace(ctx, state.cwd, childId);
|
|
930
|
+
}
|
|
931
|
+
catch (error) {
|
|
932
|
+
state.notify(t('rewind-attach-failed', { err: error instanceof Error ? error.message : String(error) }), { color: 'warning', timeoutMs: 8000 });
|
|
933
|
+
}
|
|
702
934
|
// Replay the forked history into a fresh transcript (tokens/spinner
|
|
703
935
|
// counters land back at the rewind point, matching the fork).
|
|
704
936
|
streaming = undefined;
|
|
@@ -709,6 +941,11 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
709
941
|
// Goal/todo/title are session-scoped; the replay re-derives them for
|
|
710
942
|
// the session being entered (or leaves them empty).
|
|
711
943
|
state.todos = [];
|
|
944
|
+
// Queued-but-undelivered messages live in the OLD agent's inbox; the
|
|
945
|
+
// swap must drop their previews or they linger forever (unretirable —
|
|
946
|
+
// retire events are filtered to the new agent, unwithdrawable — the
|
|
947
|
+
// new inbox never heard of them).
|
|
948
|
+
state.pending = [];
|
|
712
949
|
state.goal = undefined;
|
|
713
950
|
state.sessionTitle = '';
|
|
714
951
|
state.tokens = { input: 0, output: 0 };
|
|
@@ -749,17 +986,24 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
749
986
|
async resumeTo(sessionId) {
|
|
750
987
|
// Switch the live agent to a persisted session: /resume picker Enter
|
|
751
988
|
// loads the history immediately (the `--resume` launcher path keeps
|
|
752
|
-
// resolving through
|
|
989
|
+
// resolving through DSH_TUI_RESUME_SESSION at boot).
|
|
753
990
|
if (state.working) {
|
|
754
|
-
state.notify('
|
|
991
|
+
state.notify(t('resume-while-working'), { color: 'warning' });
|
|
755
992
|
return false;
|
|
756
993
|
}
|
|
757
994
|
const agents = ctx.get('agents');
|
|
758
995
|
if (!agents) {
|
|
759
|
-
state.notify('
|
|
996
|
+
state.notify(t('resume-unavailable'), { color: 'error' });
|
|
760
997
|
return false;
|
|
761
998
|
}
|
|
762
999
|
let handle;
|
|
1000
|
+
// Compat boundary: register vouched-for legacy event types (e.g.
|
|
1001
|
+
// activity/status from pre-#143 logs) in every reachable dsh-session
|
|
1002
|
+
// copy before ANY strict read path (preset lookup below, then the
|
|
1003
|
+
// harness seed validation) loads the target — the plugin's #119
|
|
1004
|
+
// registration never ran in processes where it is unmounted (issue
|
|
1005
|
+
// #153). In-process only: the shared log is never rewritten.
|
|
1006
|
+
ensureLegacySessionEventTypes();
|
|
763
1007
|
// The target session's own preset (from its persisted log) — never the
|
|
764
1008
|
// current preference: a resume re-enters the composition its history
|
|
765
1009
|
// was produced under. Same rule for the route: only an explicit
|
|
@@ -773,9 +1017,6 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
773
1017
|
model: options.configuredModel,
|
|
774
1018
|
});
|
|
775
1019
|
try {
|
|
776
|
-
// Compat boundary: mark third-party event types in the target log
|
|
777
|
-
// ignorable before the harness seed-validates it (src/compat/).
|
|
778
|
-
await prepareSessionForResume(sessionId);
|
|
779
1020
|
handle = await agents.resume({
|
|
780
1021
|
resumeSessionId: SessionId(sessionId),
|
|
781
1022
|
agentOptions: { provider: resumeRoute?.provider, model: resumeRoute?.model },
|
|
@@ -784,9 +1025,18 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
784
1025
|
}
|
|
785
1026
|
catch (error) {
|
|
786
1027
|
const message = error instanceof Error ? error.message : String(error);
|
|
787
|
-
state.notify(
|
|
1028
|
+
state.notify(t('resume-failed', { err: message }), { color: 'error', timeoutMs: 8000 });
|
|
788
1029
|
return false;
|
|
789
1030
|
}
|
|
1031
|
+
try {
|
|
1032
|
+
// `/resume` is an explicit adoption of this persisted conversation.
|
|
1033
|
+
// This also repairs sessions created by TUI versions that predate the
|
|
1034
|
+
// separate workspace ownership ledger.
|
|
1035
|
+
await attachSessionToWorkspace(ctx, handle.agent.session.header.cwd ?? state.cwd, SessionId(sessionId));
|
|
1036
|
+
}
|
|
1037
|
+
catch (error) {
|
|
1038
|
+
state.notify(t('resume-attach-failed', { err: error instanceof Error ? error.message : String(error) }), { color: 'warning', timeoutMs: 8000 });
|
|
1039
|
+
}
|
|
790
1040
|
// Replay the persisted history into a fresh transcript (same reset as
|
|
791
1041
|
// rewindTo, plus the context window which the replay re-derives).
|
|
792
1042
|
streaming = undefined;
|
|
@@ -797,6 +1047,11 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
797
1047
|
// Goal/todo/title are session-scoped; the replay re-derives them for
|
|
798
1048
|
// the session being entered (or leaves them empty).
|
|
799
1049
|
state.todos = [];
|
|
1050
|
+
// Queued-but-undelivered messages live in the OLD agent's inbox; the
|
|
1051
|
+
// swap must drop their previews or they linger forever (unretirable —
|
|
1052
|
+
// retire events are filtered to the new agent, unwithdrawable — the
|
|
1053
|
+
// new inbox never heard of them).
|
|
1054
|
+
state.pending = [];
|
|
800
1055
|
state.goal = undefined;
|
|
801
1056
|
state.sessionTitle = '';
|
|
802
1057
|
state.tokens = { input: 0, output: 0 };
|
|
@@ -807,6 +1062,15 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
807
1062
|
state.spinnerMode = 'requesting';
|
|
808
1063
|
state.status = handle.agent.status;
|
|
809
1064
|
state.agentId = handle.agent.id;
|
|
1065
|
+
// Adopt the resumed session's persisted cwd (issue #96): pre-upgrade
|
|
1066
|
+
// sessions recorded the LAUNCH directory (often a repo subdirectory),
|
|
1067
|
+
// so keeping the freshly resolved root would split @ expansion / file
|
|
1068
|
+
// completion (state.cwd) from the agent's own workspace record — and
|
|
1069
|
+
// drop the session back out of the /resume filter. The branch
|
|
1070
|
+
// breadcrumb follows the adopted cwd.
|
|
1071
|
+
state.cwd = handle.agent.session.header.cwd ?? state.cwd;
|
|
1072
|
+
state.displayCwd = workspaceService.describe(state.cwd).description ?? state.cwd;
|
|
1073
|
+
refreshGitBranch();
|
|
810
1074
|
state.agentPreset = resumeComposed.agentPreset;
|
|
811
1075
|
// Status-line route follows the resumed session (review feedback): the
|
|
812
1076
|
// route it actually continues on — a complete cordis.yml pin, else the
|
|
@@ -845,6 +1109,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
845
1109
|
touchSession(sessionId);
|
|
846
1110
|
state.emit();
|
|
847
1111
|
void oldHandle?.dispose().catch(() => { });
|
|
1112
|
+
clearStagedImages();
|
|
848
1113
|
return true;
|
|
849
1114
|
},
|
|
850
1115
|
async newSession() {
|
|
@@ -852,14 +1117,14 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
852
1117
|
// transcript reset, the `--resume` marker forgotten (the old session
|
|
853
1118
|
// stays persisted for /resume). Same reset shape as rewindTo/resumeTo.
|
|
854
1119
|
if (state.working) {
|
|
855
|
-
state.notify('
|
|
1120
|
+
state.notify(t('new-session-while-working'), {
|
|
856
1121
|
color: 'warning',
|
|
857
1122
|
});
|
|
858
1123
|
return false;
|
|
859
1124
|
}
|
|
860
1125
|
const agents = ctx.get('agents');
|
|
861
1126
|
if (!agents) {
|
|
862
|
-
state.notify('
|
|
1127
|
+
state.notify(t('new-session-unavailable'), {
|
|
863
1128
|
color: 'error',
|
|
864
1129
|
});
|
|
865
1130
|
return false;
|
|
@@ -893,7 +1158,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
893
1158
|
handle = await agents.create({
|
|
894
1159
|
sessionId,
|
|
895
1160
|
meta: {
|
|
896
|
-
cwd:
|
|
1161
|
+
cwd: state.cwd,
|
|
897
1162
|
...(newComposed.agentPreset === undefined
|
|
898
1163
|
? {}
|
|
899
1164
|
: { agentPreset: newComposed.agentPreset }),
|
|
@@ -904,12 +1169,18 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
904
1169
|
}
|
|
905
1170
|
catch (error) {
|
|
906
1171
|
const message = error instanceof Error ? error.message : String(error);
|
|
907
|
-
state.notify(
|
|
1172
|
+
state.notify(t('new-session-failed', { err: message }), {
|
|
908
1173
|
color: 'error',
|
|
909
1174
|
timeoutMs: 8000,
|
|
910
1175
|
});
|
|
911
1176
|
return false;
|
|
912
1177
|
}
|
|
1178
|
+
try {
|
|
1179
|
+
await attachSessionToWorkspace(ctx, state.cwd, sessionId);
|
|
1180
|
+
}
|
|
1181
|
+
catch (error) {
|
|
1182
|
+
state.notify(t('new-session-attach-failed', { err: error instanceof Error ? error.message : String(error) }), { color: 'warning', timeoutMs: 8000 });
|
|
1183
|
+
}
|
|
913
1184
|
streaming = undefined;
|
|
914
1185
|
reasoning = undefined;
|
|
915
1186
|
toolCards.clear();
|
|
@@ -918,6 +1189,11 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
918
1189
|
// Goal/todo/title are session-scoped; the replay re-derives them for
|
|
919
1190
|
// the session being entered (or leaves them empty).
|
|
920
1191
|
state.todos = [];
|
|
1192
|
+
// Queued-but-undelivered messages live in the OLD agent's inbox; the
|
|
1193
|
+
// swap must drop their previews or they linger forever (unretirable —
|
|
1194
|
+
// retire events are filtered to the new agent, unwithdrawable — the
|
|
1195
|
+
// new inbox never heard of them).
|
|
1196
|
+
state.pending = [];
|
|
921
1197
|
state.goal = undefined;
|
|
922
1198
|
state.sessionTitle = '';
|
|
923
1199
|
state.tokens = { input: 0, output: 0 };
|
|
@@ -952,17 +1228,75 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
952
1228
|
clearResumeTarget();
|
|
953
1229
|
// The brand-new session becomes the most recently used.
|
|
954
1230
|
touchSession(handle.agent.id);
|
|
955
|
-
state.emit();
|
|
956
1231
|
void oldHandle?.dispose().catch(() => { });
|
|
1232
|
+
clearStagedImages();
|
|
957
1233
|
return true;
|
|
958
1234
|
},
|
|
1235
|
+
listWorkspaces() {
|
|
1236
|
+
return workspaceService.list(state.cwd);
|
|
1237
|
+
},
|
|
1238
|
+
resolveWorkspace(uri) {
|
|
1239
|
+
return workspaceService.resolve(uri, state.cwd);
|
|
1240
|
+
},
|
|
1241
|
+
async switchWorkspace(target) {
|
|
1242
|
+
if (state.working) {
|
|
1243
|
+
state.notify(t('workspace-switch-working'), { color: 'warning' });
|
|
1244
|
+
return false;
|
|
1245
|
+
}
|
|
1246
|
+
// Local targets must exist and be directories — creating a session in
|
|
1247
|
+
// a typo'd cwd "succeeds" and then every file tool errors per call.
|
|
1248
|
+
if (target.kind === 'local') {
|
|
1249
|
+
try {
|
|
1250
|
+
if (!statSync(target.cwd).isDirectory())
|
|
1251
|
+
throw new Error('not a directory');
|
|
1252
|
+
}
|
|
1253
|
+
catch {
|
|
1254
|
+
state.notify(t('workspace-open-invalid', { target: target.label }), { color: 'error', timeoutMs: 8000 });
|
|
1255
|
+
return false;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
const previousCwd = state.cwd;
|
|
1259
|
+
const previousDisplay = state.displayCwd;
|
|
1260
|
+
state.cwd = target.cwd;
|
|
1261
|
+
state.displayCwd = target.description ?? target.uri;
|
|
1262
|
+
const switched = await state.newSession();
|
|
1263
|
+
if (!switched) {
|
|
1264
|
+
state.cwd = previousCwd;
|
|
1265
|
+
state.displayCwd = previousDisplay;
|
|
1266
|
+
return false;
|
|
1267
|
+
}
|
|
1268
|
+
// The breadcrumb follows the adopted cwd, same as /resume (#96).
|
|
1269
|
+
refreshGitBranch();
|
|
1270
|
+
state.notify(t('workspace-switched', { target: target.label }));
|
|
1271
|
+
state.emit();
|
|
1272
|
+
return true;
|
|
1273
|
+
},
|
|
1274
|
+
async renameWorkspace(title) {
|
|
1275
|
+
try {
|
|
1276
|
+
const renamed = await workspaceService.rename(state.cwd, title);
|
|
1277
|
+
state.displayCwd = renamed.description ?? renamed.uri;
|
|
1278
|
+
state.notify(t('workspace-renamed', { title: renamed.label }));
|
|
1279
|
+
state.emit();
|
|
1280
|
+
return true;
|
|
1281
|
+
}
|
|
1282
|
+
catch (error) {
|
|
1283
|
+
state.notify(t('workspace-rename-failed', { err: error instanceof Error ? error.message : String(error) }), { color: 'error', timeoutMs: 8000 });
|
|
1284
|
+
return false;
|
|
1285
|
+
}
|
|
1286
|
+
},
|
|
1287
|
+
workspaceCommands() {
|
|
1288
|
+
return workspaceService.commands();
|
|
1289
|
+
},
|
|
1290
|
+
runWorkspaceCommand(name, input) {
|
|
1291
|
+
return workspaceService.runCommand(name, input, state.cwd);
|
|
1292
|
+
},
|
|
959
1293
|
async switchModel(provider, model) {
|
|
960
1294
|
// `/model` picker Enter — switch the live model by forking the
|
|
961
1295
|
// conversation at its current end and continuing with a new agent
|
|
962
1296
|
// routed to the chosen model. Same reset shape as rewindTo/resumeTo;
|
|
963
1297
|
// the history replays unchanged, only the request model changes.
|
|
964
1298
|
if (state.working) {
|
|
965
|
-
state.notify('
|
|
1299
|
+
state.notify(t('model-switch-while-working'), {
|
|
966
1300
|
color: 'warning',
|
|
967
1301
|
});
|
|
968
1302
|
return false;
|
|
@@ -970,7 +1304,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
970
1304
|
const sessions = ctx.get('sessions');
|
|
971
1305
|
const agents = ctx.get('agents');
|
|
972
1306
|
if (!sessions || !agents) {
|
|
973
|
-
state.notify('
|
|
1307
|
+
state.notify(t('model-switch-unavailable'), {
|
|
974
1308
|
color: 'error',
|
|
975
1309
|
});
|
|
976
1310
|
return false;
|
|
@@ -982,7 +1316,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
982
1316
|
}
|
|
983
1317
|
catch (error) {
|
|
984
1318
|
const message = error instanceof Error ? error.message : String(error);
|
|
985
|
-
state.notify(
|
|
1319
|
+
state.notify(t('model-switch-fork-failed', { err: message }), { color: 'error' });
|
|
986
1320
|
return false;
|
|
987
1321
|
}
|
|
988
1322
|
const childId = SessionId(randomUUID());
|
|
@@ -995,7 +1329,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
995
1329
|
sessionId: childId,
|
|
996
1330
|
seed,
|
|
997
1331
|
meta: {
|
|
998
|
-
cwd:
|
|
1332
|
+
cwd: state.cwd,
|
|
999
1333
|
parentSession: agent.session.id,
|
|
1000
1334
|
seedLength: seed.length,
|
|
1001
1335
|
...(modelComposed.agentPreset === undefined
|
|
@@ -1008,9 +1342,15 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1008
1342
|
}
|
|
1009
1343
|
catch (error) {
|
|
1010
1344
|
const message = error instanceof Error ? error.message : String(error);
|
|
1011
|
-
state.notify(
|
|
1345
|
+
state.notify(t('model-switch-failed', { err: message }), { color: 'error', timeoutMs: 8000 });
|
|
1012
1346
|
return false;
|
|
1013
1347
|
}
|
|
1348
|
+
try {
|
|
1349
|
+
await attachSessionToWorkspace(ctx, state.cwd, childId);
|
|
1350
|
+
}
|
|
1351
|
+
catch (error) {
|
|
1352
|
+
state.notify(t('model-switch-attach-failed', { err: error instanceof Error ? error.message : String(error) }), { color: 'warning', timeoutMs: 8000 });
|
|
1353
|
+
}
|
|
1014
1354
|
streaming = undefined;
|
|
1015
1355
|
reasoning = undefined;
|
|
1016
1356
|
toolCards.clear();
|
|
@@ -1019,6 +1359,11 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1019
1359
|
// Goal/todo/title are session-scoped; the replay re-derives them for
|
|
1020
1360
|
// the session being entered (or leaves them empty).
|
|
1021
1361
|
state.todos = [];
|
|
1362
|
+
// Queued-but-undelivered messages live in the OLD agent's inbox; the
|
|
1363
|
+
// swap must drop their previews or they linger forever (unretirable —
|
|
1364
|
+
// retire events are filtered to the new agent, unwithdrawable — the
|
|
1365
|
+
// new inbox never heard of them).
|
|
1366
|
+
state.pending = [];
|
|
1022
1367
|
state.goal = undefined;
|
|
1023
1368
|
state.sessionTitle = '';
|
|
1024
1369
|
state.tokens = { input: 0, output: 0 };
|
|
@@ -1058,7 +1403,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1058
1403
|
state.emit();
|
|
1059
1404
|
void oldHandle?.dispose().catch(() => { });
|
|
1060
1405
|
// Persist the choice so the next boot and `/new` start on it (same
|
|
1061
|
-
// contract as /preset and
|
|
1406
|
+
// contract as /preset and /effort; issues #14/#30). A failed
|
|
1062
1407
|
// write keeps the live switch but warns it will not survive a restart.
|
|
1063
1408
|
if (!writeModelPref(provider, model)) {
|
|
1064
1409
|
state.notify(t('model-pref-write-failed'), {
|
|
@@ -1067,7 +1412,9 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1067
1412
|
}
|
|
1068
1413
|
return true;
|
|
1069
1414
|
},
|
|
1070
|
-
|
|
1415
|
+
listEfforts,
|
|
1416
|
+
setEffort,
|
|
1417
|
+
cycleMode,
|
|
1071
1418
|
clear() {
|
|
1072
1419
|
state.rows.length = 0;
|
|
1073
1420
|
nextRowId = 0;
|
|
@@ -1210,6 +1557,105 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1210
1557
|
return Promise.all(providers.map(provider => llm.listModels(provider.id).catch(() => [])))
|
|
1211
1558
|
.then(lists => lists.flat());
|
|
1212
1559
|
},
|
|
1560
|
+
providerSetup() {
|
|
1561
|
+
// The `/provider` wizard's runtime surface, over the dsh-base seams:
|
|
1562
|
+
// settings (profile persistence), credentials (key storage) and the
|
|
1563
|
+
// llm runtime's configurable-provider directory + model discovery.
|
|
1564
|
+
// Structurally typed like the other optional seams in this file.
|
|
1565
|
+
const llm = ctx.get('llm');
|
|
1566
|
+
const settings = ctx.get('settings');
|
|
1567
|
+
const credentials = ctx.get('credentials');
|
|
1568
|
+
// Without dsh-llm-pi-ai there is no adapter watching the settings
|
|
1569
|
+
// section, so a written profile would never activate a route. The
|
|
1570
|
+
// adapter registers its `llm-pi-ai` settings namespace at mount, which
|
|
1571
|
+
// is the rc.6-observable mount signal (the newer
|
|
1572
|
+
// `listModelDiscoveryNamespaces()` does not exist in rc.6).
|
|
1573
|
+
if (!llm || !settings || !credentials
|
|
1574
|
+
|| !settings.describe().some(descriptor => descriptor.ns === 'llm-pi-ai')) {
|
|
1575
|
+
return undefined;
|
|
1576
|
+
}
|
|
1577
|
+
const revision = () => settings.describe().find(descriptor => descriptor.ns === 'llm-pi-ai')?.revision;
|
|
1578
|
+
return {
|
|
1579
|
+
listCatalogProviders() {
|
|
1580
|
+
// declared === true marks routes the adapter knows only because a
|
|
1581
|
+
// stored profile names them (user-added); the rest are activatable
|
|
1582
|
+
// catalog routes.
|
|
1583
|
+
return llm.listConfigurableProviders()
|
|
1584
|
+
.filter(entry => entry.settingsNs === 'llm-pi-ai' && entry.declared !== true)
|
|
1585
|
+
.map(entry => ({ provider: entry.provider, displayName: entry.displayName }));
|
|
1586
|
+
},
|
|
1587
|
+
routeExists(route) {
|
|
1588
|
+
const section = settings.get('llm-pi-ai');
|
|
1589
|
+
return section?.providers !== undefined && route in section.providers;
|
|
1590
|
+
},
|
|
1591
|
+
discoverModels(request) {
|
|
1592
|
+
return llm.discoverModels('llm-pi-ai', request);
|
|
1593
|
+
},
|
|
1594
|
+
envShadows(ref) {
|
|
1595
|
+
return process.env[ref] !== undefined;
|
|
1596
|
+
},
|
|
1597
|
+
async readCredential(ref) {
|
|
1598
|
+
const resolved = await credentials.resolve(ref);
|
|
1599
|
+
return resolved?.value;
|
|
1600
|
+
},
|
|
1601
|
+
writeCredential(ref, value) {
|
|
1602
|
+
return credentials.set(ref, value);
|
|
1603
|
+
},
|
|
1604
|
+
removeCredential(ref) {
|
|
1605
|
+
return credentials.unset(ref);
|
|
1606
|
+
},
|
|
1607
|
+
async writeProfile(route, profile) {
|
|
1608
|
+
const ops = [{ op: 'set', path: ['providers', route], value: profile }];
|
|
1609
|
+
try {
|
|
1610
|
+
await settings.mutate('llm-pi-ai', ops, revision());
|
|
1611
|
+
}
|
|
1612
|
+
catch (error) {
|
|
1613
|
+
// One retry on a stale-revision conflict (a concurrent write
|
|
1614
|
+
// landed between describe and mutate); anything else propagates
|
|
1615
|
+
// so the wizard can report and roll back the credential.
|
|
1616
|
+
const code = error?.code;
|
|
1617
|
+
if (code !== 'SETTINGS_CONFLICT')
|
|
1618
|
+
throw error;
|
|
1619
|
+
await settings.mutate('llm-pi-ai', ops, revision());
|
|
1620
|
+
}
|
|
1621
|
+
},
|
|
1622
|
+
};
|
|
1623
|
+
},
|
|
1624
|
+
async sideQuestion(question, options) {
|
|
1625
|
+
// CC /btw:无工具单轮辅助调用,重放 deriveMessages() 前缀 + 一条
|
|
1626
|
+
// 包装问题。tools 永不传(侧问无工具是核心语义);usage 不回收
|
|
1627
|
+
// (skipCacheWrite 同义——答案不进主上下文也不进 token 计数)。
|
|
1628
|
+
const llm = ctx.get('llm');
|
|
1629
|
+
if (!llm)
|
|
1630
|
+
return { answer: null, error: t('btw-llm-unavailable') };
|
|
1631
|
+
const header = agent.session.requestHeader();
|
|
1632
|
+
const config = header?.config;
|
|
1633
|
+
const messages = [
|
|
1634
|
+
...agent.session.deriveMessages(),
|
|
1635
|
+
createUserMessage({
|
|
1636
|
+
content: [{ type: 'text', text: wrapSideQuestion(question) }],
|
|
1637
|
+
source: { kind: 'plugin', plugin: 'dsh-tui/btw' },
|
|
1638
|
+
}),
|
|
1639
|
+
];
|
|
1640
|
+
const request = {
|
|
1641
|
+
provider: config?.provider ?? state.provider,
|
|
1642
|
+
model: config?.model ?? state.model,
|
|
1643
|
+
messages,
|
|
1644
|
+
...(header?.system !== undefined && { system: header.system }),
|
|
1645
|
+
...(config?.reasoningEffort !== undefined && { reasoningEffort: config.reasoningEffort }),
|
|
1646
|
+
...(config?.temperature !== undefined && { temperature: config.temperature }),
|
|
1647
|
+
...(config?.maxTokens !== undefined && { maxTokens: config.maxTokens }),
|
|
1648
|
+
...(config?.stop !== undefined && { stop: [...config.stop] }),
|
|
1649
|
+
sessionId: agent.session.id,
|
|
1650
|
+
...(options?.signal && { signal: options.signal }),
|
|
1651
|
+
};
|
|
1652
|
+
return runSideQuestion({
|
|
1653
|
+
stream: llm.stream.bind(llm),
|
|
1654
|
+
options: request,
|
|
1655
|
+
onText: options?.onText,
|
|
1656
|
+
signal: options?.signal,
|
|
1657
|
+
});
|
|
1658
|
+
},
|
|
1213
1659
|
listFiles() {
|
|
1214
1660
|
const fs = ctx.get('fs');
|
|
1215
1661
|
return listFilesDeep(fs, state.cwd);
|
|
@@ -1224,8 +1670,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1224
1670
|
const headers = await persistence.list();
|
|
1225
1671
|
// 按工作目录隔离(Claude Code 的项目维度):/resume 只列出本会话
|
|
1226
1672
|
// 目录启动的会话,别的项目的会话不出现在选择器里。
|
|
1227
|
-
const
|
|
1228
|
-
const local = headers.filter(header => (header.cwd ?? '').replace(/\/+$/, '') === cwd);
|
|
1673
|
+
const local = headers.filter(header => sessionCwdMatches(state.cwd, header.cwd ?? ''));
|
|
1229
1674
|
// MRU ordering: DSH headers carry only createdAt, so dsh-tui keeps its
|
|
1230
1675
|
// own last-used timestamps (touchSession on resume/submit/new) and
|
|
1231
1676
|
// falls back to createdAt for sessions never touched in this install.
|
|
@@ -1247,8 +1692,8 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1247
1692
|
// useful label. Read via the tolerant compat log reader instead of
|
|
1248
1693
|
// persistence.load: the backend validates every event against
|
|
1249
1694
|
// KNOWN_SESSION_EVENT_TYPES and throws the whole load on an unmarked
|
|
1250
|
-
// third-party type
|
|
1251
|
-
//
|
|
1695
|
+
// an unregistered third-party type, which would otherwise leave the
|
|
1696
|
+
// affected session titled with the cwd
|
|
1252
1697
|
// basename. An unreadable log keeps the basename fallback.
|
|
1253
1698
|
const empty = new Set();
|
|
1254
1699
|
for (const record of records.slice(0, SESSION_TITLE_DEPTH)) {
|
|
@@ -1284,6 +1729,41 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1284
1729
|
state.sessionTitle = title;
|
|
1285
1730
|
state.emit();
|
|
1286
1731
|
},
|
|
1732
|
+
async deleteSession(sessionId) {
|
|
1733
|
+
// The live session's log is still being appended by this process —
|
|
1734
|
+
// deleting it from under the writer is never offered in the picker
|
|
1735
|
+
// (the current session is filtered out), so refuse it here too.
|
|
1736
|
+
if (sessionId === agent.session.id)
|
|
1737
|
+
return false;
|
|
1738
|
+
if (deleteSessionLog(sessionId) !== 'deleted')
|
|
1739
|
+
return false;
|
|
1740
|
+
forgetSession(sessionId);
|
|
1741
|
+
// A resume marker naming the deleted session would make the next
|
|
1742
|
+
// `dsh-tui --resume` launch target a log that no longer exists.
|
|
1743
|
+
if (readResumeTarget() === sessionId)
|
|
1744
|
+
clearResumeTarget();
|
|
1745
|
+
return true;
|
|
1746
|
+
},
|
|
1747
|
+
async renameSessionTo(sessionId, title) {
|
|
1748
|
+
if (sessionId === agent.session.id) {
|
|
1749
|
+
// The live session renames through session.append so the firehose
|
|
1750
|
+
// updates the status line right away (same as /rename).
|
|
1751
|
+
agent.session.append('session/title', { title });
|
|
1752
|
+
state.sessionTitle = title;
|
|
1753
|
+
state.emit();
|
|
1754
|
+
return true;
|
|
1755
|
+
}
|
|
1756
|
+
if (appendSessionTitle(sessionId, title) !== 'appended')
|
|
1757
|
+
return false;
|
|
1758
|
+
// listSessions resolves persisted titles only for the MRU top
|
|
1759
|
+
// SESSION_TITLE_DEPTH; a rename does not change MRU by itself, so a
|
|
1760
|
+
// session beyond the window would keep showing the cwd-basename
|
|
1761
|
+
// fallback (in the next picker AND after restart) even though the
|
|
1762
|
+
// title event is durable. A rename IS user interaction with the
|
|
1763
|
+
// session — touching it pulls it into the title window.
|
|
1764
|
+
touchSession(sessionId);
|
|
1765
|
+
return true;
|
|
1766
|
+
},
|
|
1287
1767
|
compact() {
|
|
1288
1768
|
// DSH compaction service key: `ctx.compaction` (dsh-compaction's
|
|
1289
1769
|
// CompactionEngine; dsh-compaction-basic provides it in the example
|
|
@@ -1292,38 +1772,28 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1292
1772
|
// scope chain first (minimal composes NO compaction: stays unavailable).
|
|
1293
1773
|
const compactService = serviceForAgent(ctx, agent, 'compaction');
|
|
1294
1774
|
if (!compactService) {
|
|
1295
|
-
state.notify('
|
|
1775
|
+
state.notify(t('compact-unavailable'), {
|
|
1296
1776
|
color: 'warning',
|
|
1297
1777
|
});
|
|
1298
1778
|
return;
|
|
1299
1779
|
}
|
|
1300
1780
|
if (state.working) {
|
|
1301
|
-
state.notify('
|
|
1781
|
+
state.notify(t('compact-while-working'), { color: 'warning' });
|
|
1302
1782
|
return;
|
|
1303
1783
|
}
|
|
1304
1784
|
const signal = new AbortController().signal;
|
|
1305
|
-
state.notify('
|
|
1785
|
+
state.notify(t('compact-working'));
|
|
1306
1786
|
void compactService
|
|
1307
1787
|
.compactNow(agent, signal)
|
|
1308
1788
|
.then((result) => {
|
|
1309
|
-
state.notify(result ? '
|
|
1789
|
+
state.notify(result ? t('compact-done') : t('compact-nothing'));
|
|
1310
1790
|
})
|
|
1311
1791
|
.catch((error) => {
|
|
1312
|
-
state.notify(
|
|
1792
|
+
state.notify(t('compact-failed', { err: error instanceof Error ? error.message : String(error) }), { color: 'error', timeoutMs: 8000 });
|
|
1313
1793
|
});
|
|
1314
1794
|
},
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
return undefined;
|
|
1318
|
-
try {
|
|
1319
|
-
const execution = await commandService.execute(agent, `/${name}${rawInput}`, new AbortController().signal);
|
|
1320
|
-
// `undefined` = not registered; a handler error surfaces as its
|
|
1321
|
-
// message so the user sees why the command failed.
|
|
1322
|
-
return execution?.result.text ?? '';
|
|
1323
|
-
}
|
|
1324
|
-
catch (error) {
|
|
1325
|
-
return error instanceof Error ? error.message : String(error);
|
|
1326
|
-
}
|
|
1795
|
+
runExternalCommand(name, rawInput) {
|
|
1796
|
+
return executeRegistryCommand(name, rawInput);
|
|
1327
1797
|
},
|
|
1328
1798
|
pushLocal(title, lines) {
|
|
1329
1799
|
state.rows.push({ id: nextRowId++, kind: 'local', text: title });
|
|
@@ -1464,16 +1934,24 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1464
1934
|
lines.push(t('doctor-cwd', { cwd: state.cwd }));
|
|
1465
1935
|
lines.push(t('doctor-context-window', { window: state.contextWindow ?? t('doctor-unknown') }));
|
|
1466
1936
|
lines.push(`${t('doctor-session', { id: state.agentId })}${state.sessionTitle ? ' · ' + state.sessionTitle : ''}`);
|
|
1467
|
-
const userHome =
|
|
1937
|
+
const userHome = homeDir();
|
|
1468
1938
|
const configCandidates = [
|
|
1469
|
-
join(userHome, '.dsh-
|
|
1939
|
+
join(userHome, '.dsh-tui/cordis.yml'),
|
|
1470
1940
|
join(userHome, '.dsh/profiles/dsh-tui/cordis.patch.yml'),
|
|
1471
1941
|
];
|
|
1472
1942
|
for (const candidate of configCandidates) {
|
|
1473
1943
|
lines.push(`${t('doctor-config', { candidate, state: existsSync(candidate) ? '✓' : t('doctor-config-missing') })}`);
|
|
1474
1944
|
}
|
|
1475
|
-
|
|
1476
|
-
|
|
1945
|
+
// Session store candidates mirror the compat layer (sessionsRoots):
|
|
1946
|
+
// the active root depends on the composition (bare cordis.yml →
|
|
1947
|
+
// legacy ~/.dsh-tui, profile → $DSH_HOME/sessions), so list every
|
|
1948
|
+
// candidate with its own state instead of hardcoding one.
|
|
1949
|
+
for (const dir of sessionsRoots()) {
|
|
1950
|
+
lines.push(`${t('doctor-storage', { dir, state: existsSync(dir) ? '✓' : t('doctor-storage-uninit') })}`);
|
|
1951
|
+
}
|
|
1952
|
+
if (existsSync(LEGACY_DATA_DIR)) {
|
|
1953
|
+
lines.push(t('doctor-legacy-dir'));
|
|
1954
|
+
}
|
|
1477
1955
|
return lines;
|
|
1478
1956
|
},
|
|
1479
1957
|
async listSubagents() {
|
|
@@ -1495,6 +1973,11 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1495
1973
|
return [t('subagent-query-failed', { err: error instanceof Error ? error.message : String(error) })];
|
|
1496
1974
|
}
|
|
1497
1975
|
},
|
|
1976
|
+
traceEvents() {
|
|
1977
|
+
// Immutable per-append snapshot (dsh-session caches the frozen array);
|
|
1978
|
+
// reads follow agent swaps (/resume /rewind /new) automatically.
|
|
1979
|
+
return agent.session.events;
|
|
1980
|
+
},
|
|
1498
1981
|
};
|
|
1499
1982
|
/**
|
|
1500
1983
|
* Assemble the context a fresh conversation for the live agent will load,
|
|
@@ -1536,10 +2019,20 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1536
2019
|
tools.push({ name: tool.name, description: tool.description ?? '' });
|
|
1537
2020
|
}
|
|
1538
2021
|
}
|
|
1539
|
-
const
|
|
2022
|
+
const renderedInstructions = await loadBaselineInstructions({
|
|
2023
|
+
cwd: state.cwd,
|
|
2024
|
+
maxBytes: 1024 * 1024,
|
|
2025
|
+
maxSourceBytes: 1024 * 1024,
|
|
2026
|
+
}, ctx.get('fs'));
|
|
1540
2027
|
if (target !== agent)
|
|
1541
2028
|
return;
|
|
1542
|
-
|
|
2029
|
+
const instructionSources = renderedInstructions;
|
|
2030
|
+
const instructionPaths = new Set([
|
|
2031
|
+
...(instructionSources?.represented ?? []).map(file => file.displayPath),
|
|
2032
|
+
...(renderedInstructions?.omitted ?? []).map(file => file.displayPath),
|
|
2033
|
+
...(renderedInstructions?.truncated ?? []).map(file => file.displayPath),
|
|
2034
|
+
]);
|
|
2035
|
+
files.push(...[...instructionPaths].map(displayPath => ({ displayPath })));
|
|
1543
2036
|
// The skills registry is host-plane but scope-layered: preset rows
|
|
1544
2037
|
// (skill-filesystem) register into the preset's layer, so the catalog
|
|
1545
2038
|
// must be read through the agent's scope chain (serviceForAgent falls
|
|
@@ -1563,20 +2056,41 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1563
2056
|
state.emit();
|
|
1564
2057
|
};
|
|
1565
2058
|
/**
|
|
1566
|
-
* Rebuild the merged slash-command list
|
|
1567
|
-
*
|
|
1568
|
-
*
|
|
1569
|
-
*
|
|
2059
|
+
* Rebuild the merged slash-command list: built-in locals, then registry
|
|
2060
|
+
* commands (plan/goal/…), then user-invocable skills from the DSH skill
|
|
2061
|
+
* registry (issue #86 — filesystem-discovered skills must appear in the
|
|
2062
|
+
* `/` menu and Tab completion, like /audit and /review). Skill entries
|
|
2063
|
+
* are completion-only: dispatch falls through to the model as plain text,
|
|
2064
|
+
* where dsh-tool-skill's pre-step hook injects the skill body — the same
|
|
2065
|
+
* path a hand-typed `/skill-name` takes. Registry and skill reads are
|
|
2066
|
+
* scoped to the LIVE agent, so this runs on `commands/change` +
|
|
2067
|
+
* `skills/change` and again whenever the live agent is swapped
|
|
2068
|
+
* (rewind/resume/new/model). A failed skill read restores the last
|
|
2069
|
+
* successfully merged skill set for the same agent (last-good), so a
|
|
2070
|
+
* transient provider failure never makes known skills vanish.
|
|
2071
|
+
*/
|
|
2072
|
+
let commandListSeq = 0;
|
|
2073
|
+
/**
|
|
2074
|
+
* The last successfully merged skill entries, tagged with the agent whose
|
|
2075
|
+
* scope produced them. A failed catalog read restores these instead of
|
|
2076
|
+
* dropping skill entries from the menu until the next successful refresh
|
|
2077
|
+
* (last-good); the agent tag refuses cross-agent restores — a different
|
|
2078
|
+
* scope's skills may not exist for the live agent at all.
|
|
1570
2079
|
*/
|
|
2080
|
+
let lastGoodSkills;
|
|
1571
2081
|
const refreshCommandList = () => {
|
|
2082
|
+
const target = agent;
|
|
2083
|
+
const token = ++commandListSeq;
|
|
1572
2084
|
const merged = [...LOCAL_COMMANDS];
|
|
1573
2085
|
if (commandService) {
|
|
1574
|
-
for (const descriptor of commandService.list(
|
|
2086
|
+
for (const descriptor of commandService.list(target)) {
|
|
1575
2087
|
if (merged.some(command => command.name === descriptor.name))
|
|
1576
2088
|
continue;
|
|
2089
|
+
const descriptions = commandTrees?.descriptions(descriptor.name);
|
|
1577
2090
|
merged.push({
|
|
1578
2091
|
name: descriptor.name,
|
|
1579
2092
|
description: descriptor.description,
|
|
2093
|
+
...(descriptions === undefined ? {} : { descriptions }),
|
|
1580
2094
|
tag: descriptor.input?.hint,
|
|
1581
2095
|
external: true,
|
|
1582
2096
|
});
|
|
@@ -1584,8 +2098,73 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1584
2098
|
}
|
|
1585
2099
|
state.commandList = merged;
|
|
1586
2100
|
state.emit();
|
|
2101
|
+
// The skill catalog resolves asynchronously (filesystem providers scan
|
|
2102
|
+
// their roots), so skills append in a continuation; a newer refresh or
|
|
2103
|
+
// an agent swap supersedes this run (token/identity check, same rule as
|
|
2104
|
+
// refreshLoadedContext). Locals and registry commands win name
|
|
2105
|
+
// collisions — a skill named `plan` must not shadow the registry's.
|
|
2106
|
+
const skillsService = serviceForAgent(ctx, target, 'skills');
|
|
2107
|
+
if (skillsService === undefined)
|
|
2108
|
+
return;
|
|
2109
|
+
/** Last-good restore shared by the failed-read and incomplete-read
|
|
2110
|
+
* paths; the caller holds the staleness check. */
|
|
2111
|
+
const restoreLastGood = () => {
|
|
2112
|
+
const fallback = lastGoodSkills?.agent === target ? lastGoodSkills.commands : [];
|
|
2113
|
+
const restored = fallback.filter(entry => !merged.some(command => command.name === entry.name));
|
|
2114
|
+
if (restored.length === 0)
|
|
2115
|
+
return;
|
|
2116
|
+
state.commandList = [...merged, ...restored];
|
|
2117
|
+
state.emit();
|
|
2118
|
+
};
|
|
2119
|
+
// snapshot() over list(): only a COMPLETE observation is authoritative
|
|
2120
|
+
// — list() discards `complete`, so a provider failure or a rescan still
|
|
2121
|
+
// in flight would resolve as a partial/empty catalog and wrongly clear
|
|
2122
|
+
// the last-good set (dsh-skill's own consumer contract).
|
|
2123
|
+
void skillsService.snapshot({
|
|
2124
|
+
scope: target,
|
|
2125
|
+
cwd: target.session.header?.cwd ?? state.cwd,
|
|
2126
|
+
}).then((observation) => {
|
|
2127
|
+
if (token !== commandListSeq || target !== agent)
|
|
2128
|
+
return;
|
|
2129
|
+
if (!observation.complete) {
|
|
2130
|
+
// Incomplete (provider failure/rescan mid-flight): NOT authoritative
|
|
2131
|
+
// — never clear last-good or repopulate from the partial catalog.
|
|
2132
|
+
// The provider's next invalidate fires skills/change for the retry.
|
|
2133
|
+
ctx.logger.warn('skill command merge: incomplete catalog observation, keeping last-good skills');
|
|
2134
|
+
restoreLastGood();
|
|
2135
|
+
return;
|
|
2136
|
+
}
|
|
2137
|
+
const withSkills = [...merged];
|
|
2138
|
+
for (const skill of observation.skills) {
|
|
2139
|
+
if (!isUserInvocable(skill))
|
|
2140
|
+
continue;
|
|
2141
|
+
if (withSkills.some(command => command.name === skill.name))
|
|
2142
|
+
continue;
|
|
2143
|
+
withSkills.push({ name: skill.name, description: skill.description, skill: true });
|
|
2144
|
+
}
|
|
2145
|
+
const added = withSkills.slice(merged.length);
|
|
2146
|
+
lastGoodSkills = { agent: target, commands: added };
|
|
2147
|
+
// The sync phase already assigned `merged`; a complete read that adds
|
|
2148
|
+
// nothing leaves the state as-is (and authoritatively clears the
|
|
2149
|
+
// last-good set above).
|
|
2150
|
+
if (added.length === 0)
|
|
2151
|
+
return;
|
|
2152
|
+
state.commandList = withSkills;
|
|
2153
|
+
state.emit();
|
|
2154
|
+
}).catch((error) => {
|
|
2155
|
+
// A superseded read (a newer refresh or an agent swap beat it) says
|
|
2156
|
+
// nothing about the live menu: stay silent instead of logging a
|
|
2157
|
+
// misleading failure warning.
|
|
2158
|
+
if (token !== commandListSeq || target !== agent)
|
|
2159
|
+
return;
|
|
2160
|
+
ctx.logger.warn('skill command merge failed: %o', error);
|
|
2161
|
+
// Last-good: a transient provider failure (rescan error, permission
|
|
2162
|
+
// hiccup) must not make known skills vanish from completion.
|
|
2163
|
+
restoreLastGood();
|
|
2164
|
+
});
|
|
1587
2165
|
};
|
|
1588
2166
|
ctx.on('commands/change', refreshCommandList);
|
|
2167
|
+
ctx.on('skills/change', refreshCommandList);
|
|
1589
2168
|
refreshCommandList();
|
|
1590
2169
|
void refreshLoadedContext();
|
|
1591
2170
|
let nextRowId = 0;
|
|
@@ -1594,20 +2173,27 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1594
2173
|
* service registers under `ctx.shell` (ShellExecutor; dsh-bash-local and
|
|
1595
2174
|
* dsh-pwsh-local are the providers). */
|
|
1596
2175
|
const bash = ctx.get('shell');
|
|
1597
|
-
/** Claude Code's `!` mode:
|
|
1598
|
-
|
|
2176
|
+
/** Claude Code's `!` mode: execute in the current workspace provider and
|
|
2177
|
+
* render local-only transcript rows (never sent to the model). */
|
|
1599
2178
|
const runLocalCommand = async (command, includeInContext) => {
|
|
1600
|
-
|
|
2179
|
+
const workspace = workspaceService.describe(state.cwd);
|
|
2180
|
+
state.rows.push({
|
|
2181
|
+
id: nextRowId++,
|
|
2182
|
+
kind: 'local',
|
|
2183
|
+
text: command,
|
|
2184
|
+
executionTarget: workspace.kind === 'local' ? workspace.badge : `${workspace.badge} · ${workspace.label}`,
|
|
2185
|
+
});
|
|
1601
2186
|
state.emit();
|
|
1602
2187
|
let output = '(no output)';
|
|
1603
|
-
|
|
2188
|
+
const executionShell = await workspaceService.commandShell(state.cwd) ?? bash;
|
|
2189
|
+
if (executionShell) {
|
|
1604
2190
|
try {
|
|
1605
|
-
const spec =
|
|
2191
|
+
const spec = executionShell.resolve({
|
|
1606
2192
|
command,
|
|
1607
2193
|
workdir: state.cwd,
|
|
1608
2194
|
timeoutMs: 30000,
|
|
1609
2195
|
});
|
|
1610
|
-
const result = await
|
|
2196
|
+
const result = await executionShell.run(spec);
|
|
1611
2197
|
output =
|
|
1612
2198
|
result.stdout.text.trim() ||
|
|
1613
2199
|
result.stderr.text.trim() ||
|
|
@@ -2009,7 +2595,7 @@ ${output}
|
|
|
2009
2595
|
const failure = event.data.error;
|
|
2010
2596
|
if (failure !== undefined) {
|
|
2011
2597
|
card.tool.status = 'error';
|
|
2012
|
-
const errorText =
|
|
2598
|
+
const errorText = toolErrorText(event);
|
|
2013
2599
|
card.tool.errorText = errorText;
|
|
2014
2600
|
state.contextSegments.tools += estimateTokens(errorText);
|
|
2015
2601
|
}
|
|
@@ -2094,7 +2680,7 @@ ${output}
|
|
|
2094
2680
|
state.rows.push({
|
|
2095
2681
|
id: nextRowId,
|
|
2096
2682
|
kind: 'interrupt',
|
|
2097
|
-
text: '
|
|
2683
|
+
text: t('interrupted-by-user') + t('interrupted-ask-next'),
|
|
2098
2684
|
});
|
|
2099
2685
|
nextRowId += 1;
|
|
2100
2686
|
break;
|
|
@@ -2102,7 +2688,7 @@ ${output}
|
|
|
2102
2688
|
const detail = reason.kind === 'error' ? reason.error.message : '';
|
|
2103
2689
|
state.rows.push({ id: nextRowId, kind: 'notice', text: `turn ${reason.kind}${detail ? ` · ${detail}` : ''}` });
|
|
2104
2690
|
nextRowId += 1;
|
|
2105
|
-
state.notify(
|
|
2691
|
+
state.notify(t('turn-failed', { detail: detail ? ` · ${detail}` : '' }), { color: 'error', timeoutMs: 8000 });
|
|
2106
2692
|
break;
|
|
2107
2693
|
}
|
|
2108
2694
|
case 'request/context':
|
|
@@ -2137,7 +2723,7 @@ ${output}
|
|
|
2137
2723
|
// Logged preset switch (blank sessions only, issue #8): a transcript
|
|
2138
2724
|
// marker so a replayed log shows which composition produced the
|
|
2139
2725
|
// turns after it. Not in dsh-session's typed union — matched here by
|
|
2140
|
-
// name, the
|
|
2726
|
+
// name, like the other plugin-defined events above.
|
|
2141
2727
|
if (event.type === 'agent-preset/selected') {
|
|
2142
2728
|
const data = event.data;
|
|
2143
2729
|
state.rows.push({
|
|
@@ -2159,29 +2745,97 @@ ${output}
|
|
|
2159
2745
|
state.working = false;
|
|
2160
2746
|
state.status = agent.status;
|
|
2161
2747
|
state.emit();
|
|
2162
|
-
// Live subscription list, rebound to
|
|
2748
|
+
// Live subscription list and activity timer, rebound to every replacement
|
|
2749
|
+
// agent so no status from the previous session can leak across a swap.
|
|
2163
2750
|
let agentSubscriptions = [];
|
|
2751
|
+
let activityTracker = new ActivityTracker({
|
|
2752
|
+
phrases: true,
|
|
2753
|
+
detailLimit: 40,
|
|
2754
|
+
showIdle: false,
|
|
2755
|
+
});
|
|
2756
|
+
let activityTickTimer;
|
|
2757
|
+
const stopActivityTick = () => {
|
|
2758
|
+
if (activityTickTimer === undefined)
|
|
2759
|
+
return;
|
|
2760
|
+
clearInterval(activityTickTimer);
|
|
2761
|
+
activityTickTimer = undefined;
|
|
2762
|
+
};
|
|
2763
|
+
/** Render the current tracker into the TUI-only projection. */
|
|
2764
|
+
const renderWorkingActivity = () => {
|
|
2765
|
+
if (options.activity === false) {
|
|
2766
|
+
state.workingActivity = undefined;
|
|
2767
|
+
return undefined;
|
|
2768
|
+
}
|
|
2769
|
+
const rendered = activityTracker.render();
|
|
2770
|
+
state.workingActivity = rendered;
|
|
2771
|
+
return rendered;
|
|
2772
|
+
};
|
|
2164
2773
|
const bindAgent = () => {
|
|
2165
2774
|
for (const dispose of agentSubscriptions)
|
|
2166
2775
|
dispose();
|
|
2776
|
+
stopActivityTick();
|
|
2777
|
+
activityTracker = new ActivityTracker({
|
|
2778
|
+
phrases: true,
|
|
2779
|
+
detailLimit: 40,
|
|
2780
|
+
showIdle: false,
|
|
2781
|
+
});
|
|
2782
|
+
activityTracker.onAgentStatus(agent.status);
|
|
2783
|
+
renderWorkingActivity();
|
|
2784
|
+
activityTickTimer = setInterval(() => {
|
|
2785
|
+
const previous = state.workingActivity;
|
|
2786
|
+
const rendered = renderWorkingActivity();
|
|
2787
|
+
if (rendered === undefined)
|
|
2788
|
+
return;
|
|
2789
|
+
// Live phases deliberately wake at 500 ms even when the formatted line
|
|
2790
|
+
// has not crossed its next whole-second boundary: turnElapsedMs remains
|
|
2791
|
+
// a current state value, while line changes cover phrase rotation and
|
|
2792
|
+
// the short-lived completed-tool summary.
|
|
2793
|
+
if (rendered.phase === 'waiting' ||
|
|
2794
|
+
rendered.phase === 'thinking' ||
|
|
2795
|
+
rendered.phase === 'tool' ||
|
|
2796
|
+
previous?.phase !== rendered.phase ||
|
|
2797
|
+
previous.line !== rendered.line) {
|
|
2798
|
+
state.emit();
|
|
2799
|
+
}
|
|
2800
|
+
}, 500);
|
|
2801
|
+
activityTickTimer.unref();
|
|
2167
2802
|
// Re-couple the channel-owned model selection to the new agent's
|
|
2168
2803
|
// assembly/request waterfalls, then re-apply the persisted effort when
|
|
2169
2804
|
// this agent's route offers it (dsh-agent installModelSelection).
|
|
2170
2805
|
selection.current = undefined;
|
|
2171
2806
|
selection.assembled = undefined;
|
|
2807
|
+
// {{model}} backfill (issue #155): a resumed agent's route lives only in
|
|
2808
|
+
// its session's request/header records — agentOptions.model stays
|
|
2809
|
+
// undefined unless cordis.yml pins a COMPLETE provider+model pair — so
|
|
2810
|
+
// the assemble-time persona variable `{{model}}` was registered but
|
|
2811
|
+
// valueless, and dsh-system-prompt's interpolate() throws before any
|
|
2812
|
+
// model call. Seed the selection from the channel's display route (on
|
|
2813
|
+
// resume it already carries the session's recorded route; on create it
|
|
2814
|
+
// matches the route the agent was created with). Per
|
|
2815
|
+
// installModelSelection's contract an absent effort restores the
|
|
2816
|
+
// provider/default behavior, so seeding never pins an effort the route
|
|
2817
|
+
// did not ask for; applyPreferredEffort below still upgrades the seed
|
|
2818
|
+
// when the user has a persisted preference the route offers.
|
|
2819
|
+
if (agent.options?.model === undefined && state.provider !== '' && state.model !== '') {
|
|
2820
|
+
selection.current = { provider: state.provider, model: state.model };
|
|
2821
|
+
}
|
|
2172
2822
|
void applyPreferredEffort();
|
|
2823
|
+
refreshMode();
|
|
2173
2824
|
agentSubscriptions = [
|
|
2174
2825
|
installModelSelection(agent.ctx, selection),
|
|
2175
2826
|
ctx.on('agent/status', ({ agent: subject, status }) => {
|
|
2176
2827
|
if (subject !== agent)
|
|
2177
2828
|
return;
|
|
2178
2829
|
state.status = status;
|
|
2830
|
+
activityTracker.onAgentStatus(status);
|
|
2831
|
+
renderWorkingActivity();
|
|
2179
2832
|
state.emit();
|
|
2180
2833
|
}),
|
|
2181
2834
|
ctx.on('agent/disposed', ({ agent: subject }) => {
|
|
2182
2835
|
if (subject !== agent)
|
|
2183
2836
|
return;
|
|
2184
2837
|
state.status = 'disposed';
|
|
2838
|
+
stopActivityTick();
|
|
2185
2839
|
state.emit();
|
|
2186
2840
|
}),
|
|
2187
2841
|
// Pending delivery is driven by the agent inbox: a claimed message
|
|
@@ -2215,24 +2869,13 @@ ${output}
|
|
|
2215
2869
|
ctx.on('session/event', (session, event) => {
|
|
2216
2870
|
if (session !== agent.session)
|
|
2217
2871
|
return;
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
//
|
|
2221
|
-
//
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
state.workingActivity = {
|
|
2226
|
-
phase: data.phase,
|
|
2227
|
-
line: data.line,
|
|
2228
|
-
toolCount: data.toolCount ?? 0,
|
|
2229
|
-
turnElapsedMs: data.turnElapsedMs ?? 0,
|
|
2230
|
-
...(data.label === undefined ? {} : { label: data.label }),
|
|
2231
|
-
...(data.detail === undefined ? {} : { detail: data.detail }),
|
|
2232
|
-
...(data.phrase === undefined ? {} : { phrase: data.phrase }),
|
|
2233
|
-
};
|
|
2234
|
-
state.emit();
|
|
2235
|
-
return;
|
|
2872
|
+
activityTracker.onSessionEvent(event);
|
|
2873
|
+
renderWorkingActivity();
|
|
2874
|
+
// Mode-affecting atoms fold into the Shift+Tab mode indicator the
|
|
2875
|
+
// moment they land (whether appended by cycleMode or by hand).
|
|
2876
|
+
const eventType = event.type;
|
|
2877
|
+
if (eventType === 'plan/mode' || eventType === 'sandbox/mode' || eventType === 'approval/policy') {
|
|
2878
|
+
refreshMode();
|
|
2236
2879
|
}
|
|
2237
2880
|
renderEvent(event);
|
|
2238
2881
|
// Streaming deltas (one event per token) take the frame-aligned
|
|
@@ -2245,15 +2888,30 @@ ${output}
|
|
|
2245
2888
|
];
|
|
2246
2889
|
};
|
|
2247
2890
|
bindAgent();
|
|
2891
|
+
// Cordis owns the Channel lifetime. Rebinding handles the common case;
|
|
2892
|
+
// this effect closes the final timer when the Channel's context unloads.
|
|
2893
|
+
const effect = ctx.effect;
|
|
2894
|
+
effect?.call(ctx, () => () => { stopActivityTick(); }, 'dsh-tui activity timer');
|
|
2248
2895
|
// Statusline breadcrumb: current git branch of the session cwd (best-effort).
|
|
2249
|
-
|
|
2896
|
+
// Re-run when an agent swap adopts a different persisted cwd (/resume,
|
|
2897
|
+
// issue #96) so the breadcrumb never shows the previous workspace's branch.
|
|
2898
|
+
const refreshGitBranch = () => {
|
|
2899
|
+
state.gitBranch = undefined;
|
|
2900
|
+
if (!bash)
|
|
2901
|
+
return;
|
|
2902
|
+
// Capture the requested cwd: a /resume landing while this query is in
|
|
2903
|
+
// flight refreshes the branch for the NEW cwd, so a late reply from the
|
|
2904
|
+
// old workspace must be dropped (statusline staleness, issue #96 review).
|
|
2905
|
+
const requestedCwd = state.cwd;
|
|
2250
2906
|
void bash
|
|
2251
2907
|
.run(bash.resolve({
|
|
2252
2908
|
command: 'git branch --show-current',
|
|
2253
|
-
workdir:
|
|
2909
|
+
workdir: requestedCwd,
|
|
2254
2910
|
timeoutMs: 3000,
|
|
2255
2911
|
}))
|
|
2256
2912
|
.then((result) => {
|
|
2913
|
+
if (state.cwd !== requestedCwd)
|
|
2914
|
+
return;
|
|
2257
2915
|
const branch = result.stdout.text.trim();
|
|
2258
2916
|
if (branch !== '') {
|
|
2259
2917
|
state.gitBranch = branch;
|
|
@@ -2265,7 +2923,8 @@ ${output}
|
|
|
2265
2923
|
// backend may be unavailable (no confinement yet) or the cwd may
|
|
2266
2924
|
// not be a git repo. Either way the statusline simply stays blank.
|
|
2267
2925
|
});
|
|
2268
|
-
}
|
|
2926
|
+
};
|
|
2927
|
+
refreshGitBranch();
|
|
2269
2928
|
return state;
|
|
2270
2929
|
}
|
|
2271
2930
|
/** Path basename for the resume-list title (`C:/a/b` → `b`). */
|
|
@@ -2273,6 +2932,56 @@ function basename(path) {
|
|
|
2273
2932
|
const parts = path.split(/[\\/]/);
|
|
2274
2933
|
return parts[parts.length - 1] ?? path;
|
|
2275
2934
|
}
|
|
2935
|
+
/** Normalize a cwd for comparison: forward slashes, no trailing slash; case
|
|
2936
|
+
* folded when the platform's filesystem semantics are case-insensitive. */
|
|
2937
|
+
function normalizeCwd(path, caseInsensitive) {
|
|
2938
|
+
const normalized = path.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
2939
|
+
return caseInsensitive ? normalized.toLowerCase() : normalized;
|
|
2940
|
+
}
|
|
2941
|
+
/**
|
|
2942
|
+
* `/resume` project filter (issue #96): exact cwd match, PLUS sessions
|
|
2943
|
+
* recorded in a subdirectory — pre-upgrade launches recorded the launch
|
|
2944
|
+
* subdirectory as the header cwd, and with the cwd default now resolving to
|
|
2945
|
+
* the git worktree root an exact match would hide those sessions forever.
|
|
2946
|
+
* They belong to the same workspace, so they stay listed. Comparison follows
|
|
2947
|
+
* the platform's filesystem semantics (case-insensitive on Windows — a
|
|
2948
|
+
* pre-upgrade header may record `C:\Repo` where the current launch resolves
|
|
2949
|
+
* `c:\repo`). `caseInsensitive` is a parameter (not a platform read) so the
|
|
2950
|
+
* verifier can exercise both modes on any host. Exported for
|
|
2951
|
+
* scripts/verify-session-cwd.mjs.
|
|
2952
|
+
*
|
|
2953
|
+
* Boundary rule (issue #153): container directories are nobody's workspace.
|
|
2954
|
+
* $HOME and the Windows root forms — plain drive roots (`C:`), UNC share
|
|
2955
|
+
* roots (`//server/share`), and extended-length roots (`//?/C:`,
|
|
2956
|
+
* `//?/UNC/server/share`) — are ancestors of unrelated projects, so the
|
|
2957
|
+
* descendant rules below would list every session on the machine from `~`
|
|
2958
|
+
* (and every session on the drive/share from those roots). At these
|
|
2959
|
+
* boundaries, in either direction, only an exact match passes.
|
|
2960
|
+
*/
|
|
2961
|
+
export function sessionCwdMatches(stateCwd, headerCwd, caseInsensitive = process.platform === 'win32') {
|
|
2962
|
+
const cwd = normalizeCwd(stateCwd, caseInsensitive);
|
|
2963
|
+
const recorded = normalizeCwd(headerCwd, caseInsensitive);
|
|
2964
|
+
if (recorded === '' || cwd === '')
|
|
2965
|
+
return false;
|
|
2966
|
+
const home = normalizeCwd(homeDir(), caseInsensitive);
|
|
2967
|
+
// Paths below arrive backslash-normalized (`\\server\share` →
|
|
2968
|
+
// `//server/share`, `\\?\C:\` → `//?/C:`), trailing slashes stripped.
|
|
2969
|
+
const isContainer = (path) => (home !== '' && path === home) ||
|
|
2970
|
+
/^[a-z]:$/i.test(path) || // drive root: C:
|
|
2971
|
+
/^\/\/[^/]+\/[^/]+$/.test(path) || // UNC share root: //server/share
|
|
2972
|
+
/^\/\/\?\/[a-z]:$/i.test(path) || // extended drive root: //?/C:
|
|
2973
|
+
/^\/\/\?\/unc\/[^/]+\/[^/]+$/i.test(path); // extended UNC root: //?/UNC/server/share
|
|
2974
|
+
if (isContainer(cwd) || isContainer(recorded))
|
|
2975
|
+
return recorded === cwd;
|
|
2976
|
+
return (recorded === cwd ||
|
|
2977
|
+
// Pre-upgrade subdirectory session of this workspace.
|
|
2978
|
+
recorded.startsWith(`${cwd}/`) ||
|
|
2979
|
+
// Resumed INTO a pre-upgrade subdirectory session (state.cwd adopted its
|
|
2980
|
+
// recorded subdirectory): the workspace-root sessions it belongs with
|
|
2981
|
+
// must stay visible, or /resume looks like it lost them for the rest of
|
|
2982
|
+
// the process lifetime (review leftover).
|
|
2983
|
+
cwd.startsWith(`${recorded}/`));
|
|
2984
|
+
}
|
|
2276
2985
|
/** Context-bar token estimate (pi-nano-context: ~4 chars per token). */
|
|
2277
2986
|
function estimateTokens(text) {
|
|
2278
2987
|
return Math.ceil(text.length / 4);
|
|
@@ -2355,84 +3064,146 @@ const MENTION_MAX_DIR_ENTRIES = 200;
|
|
|
2355
3064
|
function mentionFs(ctx) {
|
|
2356
3065
|
return ctx.get('fs');
|
|
2357
3066
|
}
|
|
3067
|
+
function mentionAttachments(ctx) {
|
|
3068
|
+
return ctx.get('attachments');
|
|
3069
|
+
}
|
|
3070
|
+
const MENTION_IMAGE_MEDIA_TYPES = {
|
|
3071
|
+
'.png': 'image/png',
|
|
3072
|
+
'.jpg': 'image/jpeg',
|
|
3073
|
+
'.jpeg': 'image/jpeg',
|
|
3074
|
+
'.webp': 'image/webp',
|
|
3075
|
+
'.gif': 'image/gif',
|
|
3076
|
+
};
|
|
3077
|
+
function mentionImageMediaType(path) {
|
|
3078
|
+
return MENTION_IMAGE_MEDIA_TYPES[extname(path).toLowerCase()];
|
|
3079
|
+
}
|
|
2358
3080
|
/**
|
|
2359
3081
|
* Expand a submitted text's `@` mentions (issue #15) into model-facing
|
|
2360
|
-
* attachment blocks:
|
|
2361
|
-
*
|
|
2362
|
-
*
|
|
2363
|
-
*
|
|
2364
|
-
*
|
|
3082
|
+
* attachment blocks: supported image files become durable image blocks,
|
|
3083
|
+
* other files contribute capped text, and directories contribute a shallow
|
|
3084
|
+
* listing. Reads always go through the active fs service, so provider-owned
|
|
3085
|
+
* workspaces keep their routing semantics. The typed text stays first and
|
|
3086
|
+
* verbatim. Best-effort failures degrade to `missing`, never a failed send.
|
|
2365
3087
|
*/
|
|
2366
|
-
export async function expandMentions(fs, cwd, text) {
|
|
3088
|
+
export async function expandMentions(fs, cwd, text, attachments, stagedImages) {
|
|
2367
3089
|
const blocks = [{ type: 'text', text }];
|
|
2368
3090
|
const attached = [];
|
|
2369
3091
|
const missing = [];
|
|
2370
3092
|
const mentions = extractMentions(text);
|
|
2371
|
-
if (!fs || mentions.length === 0)
|
|
2372
|
-
return { blocks, attached, missing };
|
|
2373
3093
|
let budget = MENTION_MAX_TOTAL_CHARS;
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
catch {
|
|
2387
|
-
missing.push(mention.path);
|
|
2388
|
-
continue;
|
|
2389
|
-
}
|
|
2390
|
-
if (info?.type === 'file') {
|
|
3094
|
+
let imageCount = 0;
|
|
3095
|
+
let imageBytes = 0;
|
|
3096
|
+
if (fs !== undefined) {
|
|
3097
|
+
for (const mention of mentions) {
|
|
3098
|
+
const imageMediaType = mentionImageMediaType(mention.path);
|
|
3099
|
+
if (budget <= 0 && imageMediaType === undefined)
|
|
3100
|
+
break;
|
|
3101
|
+
// Mentions resolve against the session cwd, same as the model-facing fs
|
|
3102
|
+
// tools; absolute paths pass through untouched.
|
|
3103
|
+
const absolute = isAbsolute(mention.path) ? mention.path : join(cwd, mention.path);
|
|
3104
|
+
let target;
|
|
3105
|
+
let info;
|
|
2391
3106
|
try {
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
let truncated = false;
|
|
2395
|
-
if (content.length > cap) {
|
|
2396
|
-
content = content.slice(0, cap);
|
|
2397
|
-
truncated = true;
|
|
2398
|
-
}
|
|
2399
|
-
budget -= content.length;
|
|
2400
|
-
blocks.push({
|
|
2401
|
-
type: 'text',
|
|
2402
|
-
text: `<attached-file path="${mention.path}">\n${content}${truncated ? '\n[… truncated]' : ''}\n</attached-file>`,
|
|
2403
|
-
});
|
|
2404
|
-
attached.push(mention.path);
|
|
3107
|
+
target = await fs.resolve(absolute);
|
|
3108
|
+
info = await fs.stat(target);
|
|
2405
3109
|
}
|
|
2406
3110
|
catch {
|
|
2407
|
-
// Binary/undecodable or unreadable — report it like a miss.
|
|
2408
3111
|
missing.push(mention.path);
|
|
3112
|
+
continue;
|
|
2409
3113
|
}
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
3114
|
+
if (info?.type === 'file') {
|
|
3115
|
+
if (imageMediaType !== undefined && attachments !== undefined && fs.readBytes !== undefined) {
|
|
3116
|
+
const limits = attachments.imageLimits;
|
|
3117
|
+
if (!limits.mediaTypes.includes(imageMediaType) || imageCount >= limits.maxImagesPerMessage) {
|
|
3118
|
+
missing.push(mention.path);
|
|
3119
|
+
continue;
|
|
3120
|
+
}
|
|
3121
|
+
try {
|
|
3122
|
+
const data = await fs.readBytes(target, undefined, limits.maxImageBytes);
|
|
3123
|
+
if (imageBytes + data.byteLength > limits.maxMessageImageBytes) {
|
|
3124
|
+
missing.push(mention.path);
|
|
3125
|
+
continue;
|
|
3126
|
+
}
|
|
3127
|
+
const attachment = await attachments.saveImage({
|
|
3128
|
+
data,
|
|
3129
|
+
mediaType: imageMediaType,
|
|
3130
|
+
name: basename(target.displayPath),
|
|
3131
|
+
});
|
|
3132
|
+
blocks.push({ type: 'image', attachment });
|
|
3133
|
+
imageCount += 1;
|
|
3134
|
+
imageBytes += data.byteLength;
|
|
3135
|
+
attached.push(mention.path);
|
|
3136
|
+
}
|
|
3137
|
+
catch {
|
|
3138
|
+
missing.push(mention.path);
|
|
3139
|
+
}
|
|
3140
|
+
continue;
|
|
2420
3141
|
}
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
3142
|
+
try {
|
|
3143
|
+
const cap = Math.min(MENTION_MAX_FILE_CHARS, budget);
|
|
3144
|
+
let content = await fs.readText(target);
|
|
3145
|
+
let truncated = false;
|
|
3146
|
+
if (content.length > cap) {
|
|
3147
|
+
content = content.slice(0, cap);
|
|
3148
|
+
truncated = true;
|
|
3149
|
+
}
|
|
3150
|
+
budget -= content.length;
|
|
3151
|
+
blocks.push({
|
|
3152
|
+
type: 'text',
|
|
3153
|
+
text: `<attached-file path="${mention.path}">\n${content}${truncated ? '\n[… truncated]' : ''}\n</attached-file>`,
|
|
3154
|
+
});
|
|
3155
|
+
attached.push(mention.path);
|
|
3156
|
+
}
|
|
3157
|
+
catch {
|
|
3158
|
+
// Binary/undecodable or unreadable — report it like a miss.
|
|
3159
|
+
missing.push(mention.path);
|
|
3160
|
+
}
|
|
3161
|
+
continue;
|
|
2428
3162
|
}
|
|
2429
|
-
|
|
2430
|
-
|
|
3163
|
+
if (info?.type === 'directory') {
|
|
3164
|
+
try {
|
|
3165
|
+
const entries = await fs.listDir(target);
|
|
3166
|
+
const listing = entries
|
|
3167
|
+
.slice(0, MENTION_MAX_DIR_ENTRIES)
|
|
3168
|
+
.map(entry => (entry.type === 'directory' ? `${entry.name}/` : entry.name));
|
|
3169
|
+
if (entries.length > MENTION_MAX_DIR_ENTRIES) {
|
|
3170
|
+
listing.push(`… (${entries.length - MENTION_MAX_DIR_ENTRIES} more)`);
|
|
3171
|
+
}
|
|
3172
|
+
const body = listing.join('\n');
|
|
3173
|
+
budget -= body.length;
|
|
3174
|
+
blocks.push({
|
|
3175
|
+
type: 'text',
|
|
3176
|
+
text: `<attached-directory path="${mention.path}">\n${body}\n</attached-directory>`,
|
|
3177
|
+
});
|
|
3178
|
+
attached.push(mention.path);
|
|
3179
|
+
}
|
|
3180
|
+
catch {
|
|
3181
|
+
missing.push(mention.path);
|
|
3182
|
+
}
|
|
3183
|
+
continue;
|
|
2431
3184
|
}
|
|
2432
|
-
|
|
3185
|
+
// Absent (stat → undefined) or a special file.
|
|
3186
|
+
missing.push(mention.path);
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
if (attachments !== undefined && stagedImages !== undefined) {
|
|
3190
|
+
const limits = attachments.imageLimits;
|
|
3191
|
+
for (const [token, attachment] of stagedImages) {
|
|
3192
|
+
if (!text.includes(token))
|
|
3193
|
+
continue;
|
|
3194
|
+
// A referenced-but-dropped staged image must be loud: silently sending
|
|
3195
|
+
// the bare token would leave the user believing the image reached the
|
|
3196
|
+
// model. Reuse the missing-mention warning channel.
|
|
3197
|
+
if (imageCount >= limits.maxImagesPerMessage
|
|
3198
|
+
|| imageBytes + attachment.bytes > limits.maxMessageImageBytes
|
|
3199
|
+
|| !limits.mediaTypes.includes(attachment.mediaType)) {
|
|
3200
|
+
missing.push(token);
|
|
3201
|
+
continue;
|
|
3202
|
+
}
|
|
3203
|
+
blocks.push({ type: 'image', attachment });
|
|
3204
|
+
imageCount += 1;
|
|
3205
|
+
imageBytes += attachment.bytes;
|
|
2433
3206
|
}
|
|
2434
|
-
// Absent (stat → undefined) or a special file.
|
|
2435
|
-
missing.push(mention.path);
|
|
2436
3207
|
}
|
|
2437
3208
|
return { blocks, attached, missing };
|
|
2438
3209
|
}
|