@deepseek-harness-tui/dsh-tui 0.6.1 → 0.7.1
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 +77 -23
- package/bin/dsh-tui.js +132 -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} +198 -43
- package/lib/types/dsh-adapter/channel.d.ts.map +1 -0
- package/lib/types/{channel.js → dsh-adapter/channel.js} +1129 -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} +294 -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 +981 -36
- package/lib/types/i18n.d.ts.map +1 -1
- package/lib/types/i18n.js +308 -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 +31 -10
- package/lib/invariant.js +0 -28
- 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,36 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { assembleContextFor, installModelSelection } from '@deepseek-ai/dsh-agent';
|
|
3
|
+
import { isUserInvocable, renderSkillContent } 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, isLocalCommandName, LOCAL_COMMANDS, parseCommandName } 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';
|
|
29
|
+
/**
|
|
30
|
+
* Delay before re-reading a skill catalog that reported an incomplete
|
|
31
|
+
* observation (a provider whose directory watcher is still warming).
|
|
32
|
+
*/
|
|
33
|
+
const SKILL_COMMAND_RETRY_MS = 800;
|
|
23
34
|
const ARGS_PREVIEW_LIMIT = 160;
|
|
24
35
|
const RESULT_PREVIEW_LIMIT = 240;
|
|
25
36
|
/** Local `!`-command output cap (mirrors the result preview limit). */
|
|
@@ -172,6 +183,21 @@ function restoreRowFromEvent(row, event) {
|
|
|
172
183
|
break;
|
|
173
184
|
}
|
|
174
185
|
}
|
|
186
|
+
/** Render the durable tool-result payload, including provider error details. */
|
|
187
|
+
function toolResultText(event) {
|
|
188
|
+
const block = event.data.message.content[0];
|
|
189
|
+
if (block === undefined || block.type !== 'tool-result')
|
|
190
|
+
return '';
|
|
191
|
+
return block.content.map(item => item.type === 'text' ? item.text : '').join('').trim();
|
|
192
|
+
}
|
|
193
|
+
function toolErrorText(event) {
|
|
194
|
+
const failure = event.data.error;
|
|
195
|
+
if (failure === undefined)
|
|
196
|
+
return '';
|
|
197
|
+
const identity = `${failure.name}: ${failure.code}`;
|
|
198
|
+
const detail = toolResultText(event);
|
|
199
|
+
return detail === '' || detail === identity ? identity : `${identity} — ${detail}`;
|
|
200
|
+
}
|
|
175
201
|
/** Restore a folded tool row's result text from its tool/result event. */
|
|
176
202
|
function restoreToolResult(row, event) {
|
|
177
203
|
if (row.tool === undefined)
|
|
@@ -179,12 +205,11 @@ function restoreToolResult(row, event) {
|
|
|
179
205
|
const failure = event.data.error;
|
|
180
206
|
if (failure !== undefined) {
|
|
181
207
|
row.tool.status = 'error';
|
|
182
|
-
row.tool.errorText =
|
|
208
|
+
row.tool.errorText = toolErrorText(event);
|
|
183
209
|
return;
|
|
184
210
|
}
|
|
185
211
|
row.tool.status = 'ok';
|
|
186
|
-
const
|
|
187
|
-
const result = block.content.map(b => b.type === 'text' ? b.text : '').join('').trim();
|
|
212
|
+
const result = toolResultText(event);
|
|
188
213
|
row.tool.resultFull = result || undefined;
|
|
189
214
|
}
|
|
190
215
|
/**
|
|
@@ -278,6 +303,14 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
278
303
|
// command/run + command/done records). Absent the service, only the
|
|
279
304
|
// built-in local commands exist.
|
|
280
305
|
const commandService = ctx.get('commands');
|
|
306
|
+
const workspaceService = ctx.tuiWorkspaces ?? createLocalWorkspaceRuntime();
|
|
307
|
+
const commandTrees = ctx.get('tuiCommandTrees');
|
|
308
|
+
// Shift+Tab session-mode cycle: cordis.yml `modes` wins; absent/empty/
|
|
309
|
+
// atom-less → the built-in default/plan/full cycle (sessionModes.ts).
|
|
310
|
+
const { modes: sessionModes, dropped: droppedModeIds } = resolveSessionModes(options.modes);
|
|
311
|
+
if (droppedModeIds.length > 0) {
|
|
312
|
+
ctx.logger.warn(`dsh-tui: session modes ${droppedModeIds.map(id => `"${id}"`).join(', ')} declare no plan/sandbox/approval atom; dropped from the Shift+Tab cycle`);
|
|
313
|
+
}
|
|
281
314
|
const listeners = new Set();
|
|
282
315
|
/** True while a frame-aligned stream notification is pending (emitStream). */
|
|
283
316
|
let streamNotifyScheduled = false;
|
|
@@ -292,7 +325,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
292
325
|
return;
|
|
293
326
|
contextWarned = true;
|
|
294
327
|
const percentLeft = Math.max(0, Math.round((remaining / state.contextWindow) * 100));
|
|
295
|
-
state.notify(
|
|
328
|
+
state.notify(t('context-low-warning', { percent: percentLeft }), { color: 'warning', timeoutMs: 8000 });
|
|
296
329
|
};
|
|
297
330
|
/**
|
|
298
331
|
* Register a submitted message as pending and notify the UI. The inbox
|
|
@@ -316,6 +349,12 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
316
349
|
* through this chain to keep the send order FIFO.
|
|
317
350
|
*/
|
|
318
351
|
let sendChain = Promise.resolve();
|
|
352
|
+
let stagedImageSequence = 0;
|
|
353
|
+
const stagedImages = new Map();
|
|
354
|
+
const clearStagedImages = () => {
|
|
355
|
+
stagedImages.clear();
|
|
356
|
+
stagedImageSequence = 0;
|
|
357
|
+
};
|
|
319
358
|
/**
|
|
320
359
|
* Expand the text's `@` mentions and deliver ONE user message: the typed
|
|
321
360
|
* text stays the first content block (the transcript bubble renders it —
|
|
@@ -324,7 +363,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
324
363
|
*/
|
|
325
364
|
const deliverUserText = (text, placement) => {
|
|
326
365
|
sendChain = sendChain.then(async () => {
|
|
327
|
-
const expansion = await expandMentions(mentionFs(ctx), state.cwd, text);
|
|
366
|
+
const expansion = await expandMentions(mentionFs(ctx), state.cwd, text, mentionAttachments(ctx), stagedImages);
|
|
328
367
|
const message = createUserMessage({
|
|
329
368
|
content: expansion.blocks,
|
|
330
369
|
source: { kind: 'user' },
|
|
@@ -393,51 +432,191 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
393
432
|
// provider default in effect.
|
|
394
433
|
}
|
|
395
434
|
};
|
|
396
|
-
/**
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
state.notify(t('effort-unavailable'), { color: 'error' });
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
|
-
let efforts;
|
|
406
|
-
let defaultEffort;
|
|
435
|
+
/** Resolve the live route's effort levels + adapter default through the
|
|
436
|
+
* llm runtime; 'unavailable' when the service is unmounted, 'error' when
|
|
437
|
+
* resolution throws (notified here). */
|
|
438
|
+
const resolveEfforts = async () => {
|
|
439
|
+
if (llmRuntime === undefined)
|
|
440
|
+
return 'unavailable';
|
|
407
441
|
try {
|
|
408
442
|
const info = await llmRuntime.resolveModelInfo(state.provider, state.model);
|
|
409
|
-
|
|
410
|
-
|
|
443
|
+
return {
|
|
444
|
+
efforts: info.reasoning?.efforts ?? [],
|
|
445
|
+
defaultEffort: info.reasoning?.defaultEffort,
|
|
446
|
+
};
|
|
411
447
|
}
|
|
412
448
|
catch (error) {
|
|
413
449
|
state.notify(t('effort-read-failed', { error: error instanceof Error ? error.message : String(error) }), {
|
|
414
450
|
color: 'error',
|
|
415
451
|
timeoutMs: 8000,
|
|
416
452
|
});
|
|
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;
|
|
453
|
+
return 'error';
|
|
424
454
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
const next = efforts[(currentIndex + 1) % efforts.length];
|
|
455
|
+
};
|
|
456
|
+
/** Pin one validated effort level on the live route: reroutes the next
|
|
457
|
+
* request, persists the choice, and refreshes the StatusLine segment. */
|
|
458
|
+
const applyEffort = (effort) => {
|
|
430
459
|
selection.current = {
|
|
431
460
|
provider: state.provider,
|
|
432
461
|
model: state.model,
|
|
433
|
-
reasoningEffort: ReasoningEffortId(
|
|
462
|
+
reasoningEffort: ReasoningEffortId(effort.id),
|
|
434
463
|
};
|
|
435
|
-
preferredEffort =
|
|
436
|
-
state.reasoningEffort =
|
|
437
|
-
writeEffortPref(
|
|
438
|
-
state.notify(t('effort-switched', { name:
|
|
464
|
+
preferredEffort = effort.id;
|
|
465
|
+
state.reasoningEffort = effort.id;
|
|
466
|
+
writeEffortPref(effort.id);
|
|
467
|
+
state.notify(t('effort-switched', { name: effort.name }));
|
|
468
|
+
state.emit();
|
|
469
|
+
};
|
|
470
|
+
/** The live route's effort levels for the `/effort` slider; empty after
|
|
471
|
+
* notifying when the route is unsupported/unavailable/single-tier. */
|
|
472
|
+
const listEfforts = async () => {
|
|
473
|
+
const resolved = await resolveEfforts();
|
|
474
|
+
if (resolved === 'unavailable') {
|
|
475
|
+
state.notify(t('effort-unavailable'), { color: 'error' });
|
|
476
|
+
return { efforts: [], defaultEffort: undefined };
|
|
477
|
+
}
|
|
478
|
+
if (resolved === 'error')
|
|
479
|
+
return { efforts: [], defaultEffort: undefined };
|
|
480
|
+
if (resolved.efforts.length === 0) {
|
|
481
|
+
state.notify(t('effort-unsupported'), { color: 'warning' });
|
|
482
|
+
}
|
|
483
|
+
else if (resolved.efforts.length === 1) {
|
|
484
|
+
state.notify(t('effort-single-tier', { name: resolved.efforts[0].name }), { color: 'warning' });
|
|
485
|
+
}
|
|
486
|
+
return resolved;
|
|
487
|
+
};
|
|
488
|
+
/** Set one effort level by id (`/effort <id>` and the slider's live
|
|
489
|
+
* apply); false + a notify when the id is not offered by the route. */
|
|
490
|
+
const setEffort = async (id) => {
|
|
491
|
+
const resolved = await resolveEfforts();
|
|
492
|
+
if (resolved === 'unavailable') {
|
|
493
|
+
state.notify(t('effort-unavailable'), { color: 'error' });
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
if (resolved === 'error')
|
|
497
|
+
return false;
|
|
498
|
+
if (resolved.efforts.length === 0) {
|
|
499
|
+
state.notify(t('effort-unsupported'), { color: 'warning' });
|
|
500
|
+
return false;
|
|
501
|
+
}
|
|
502
|
+
const found = resolved.efforts.find(effort => effort.id === id);
|
|
503
|
+
if (!found) {
|
|
504
|
+
state.notify(t('effort-invalid', { id, ids: resolved.efforts.map(effort => effort.id).join(', ') }), { color: 'warning' });
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
applyEffort(found);
|
|
508
|
+
return true;
|
|
509
|
+
};
|
|
510
|
+
/** Run one DSH registry command (`/plan`, …) on the live agent; the text
|
|
511
|
+
* of its result, '' when the result is textless, undefined when the
|
|
512
|
+
* command is not registered, and the error message when it throws. */
|
|
513
|
+
const executeRegistryCommand = async (name, rawInput) => {
|
|
514
|
+
if (!commandService)
|
|
515
|
+
return undefined;
|
|
516
|
+
try {
|
|
517
|
+
const execution = await commandService.execute(agent, `/${name}${rawInput}`, new AbortController().signal);
|
|
518
|
+
// `undefined` = not registered; a handler error surfaces as its
|
|
519
|
+
// message so the user sees why the command failed.
|
|
520
|
+
return execution?.result.text ?? '';
|
|
521
|
+
}
|
|
522
|
+
catch (error) {
|
|
523
|
+
return error instanceof Error ? error.message : String(error);
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
// Session-mode folds: last-wins projections over the session log. The
|
|
527
|
+
// event types are registered by dsh-plan-mode / dsh-sandbox-policy /
|
|
528
|
+
// dsh-user-approval and are NOT in this package's typed SessionEvent
|
|
529
|
+
// union, so they are matched by name through casts — the same pattern as
|
|
530
|
+
// `agent-preset/selected` in renderEvent and the goal projection above.
|
|
531
|
+
const foldPlanActive = (events) => {
|
|
532
|
+
let active = false;
|
|
533
|
+
for (const event of events) {
|
|
534
|
+
if (event.type === 'plan/mode') {
|
|
535
|
+
active = event.data.active === true;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return active;
|
|
539
|
+
};
|
|
540
|
+
const foldSandboxMode = (events) => {
|
|
541
|
+
let mode;
|
|
542
|
+
for (const event of events) {
|
|
543
|
+
if (event.type === 'sandbox/mode') {
|
|
544
|
+
const value = event.data.mode;
|
|
545
|
+
if (typeof value === 'string')
|
|
546
|
+
mode = value;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return mode;
|
|
550
|
+
};
|
|
551
|
+
const foldApprovalPolicy = (events) => {
|
|
552
|
+
let policy;
|
|
553
|
+
for (const event of events) {
|
|
554
|
+
if (event.type === 'approval/policy') {
|
|
555
|
+
const value = event.data.policy;
|
|
556
|
+
if (typeof value === 'string')
|
|
557
|
+
policy = value;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return policy;
|
|
561
|
+
};
|
|
562
|
+
/** First configured mode whose declared atoms all match the folds;
|
|
563
|
+
* undeclared atoms are wildcards; no match → index 0 (the base mode).
|
|
564
|
+
* Matching is exact: a fresh session has no `approval/policy` event, so
|
|
565
|
+
* a mode declaring `approval: 'ask'` never falsely matches it. */
|
|
566
|
+
const deriveModeIndex = (events) => {
|
|
567
|
+
const index = sessionModes.findIndex(spec => (spec.plan === undefined || foldPlanActive(events) === spec.plan) &&
|
|
568
|
+
(spec.sandbox === undefined || foldSandboxMode(events) === spec.sandbox) &&
|
|
569
|
+
(spec.approval === undefined || foldApprovalPolicy(events) === spec.approval));
|
|
570
|
+
return index >= 0 ? index : 0;
|
|
571
|
+
};
|
|
572
|
+
/** Re-derive the current mode from the live session log (boot, every
|
|
573
|
+
* agent re-bind, and after mode-affecting session events). */
|
|
574
|
+
const refreshMode = () => {
|
|
575
|
+
state.modeIndex = deriveModeIndex(agent.session.events);
|
|
576
|
+
state.mode = sessionModes[state.modeIndex];
|
|
577
|
+
};
|
|
578
|
+
/** Apply one configured mode: each declared atom switches independently
|
|
579
|
+
* (plan via the registry `/plan` command; sandbox/approval via their
|
|
580
|
+
* durable session-log override events). A failing plan toggle aborts the
|
|
581
|
+
* whole switch so the session never lands in a half-applied mode. */
|
|
582
|
+
const applyMode = async (spec) => {
|
|
583
|
+
if (spec.plan !== undefined && foldPlanActive(agent.session.events) !== spec.plan) {
|
|
584
|
+
const text = await executeRegistryCommand('plan', spec.plan ? '' : ' off');
|
|
585
|
+
if (text === undefined) {
|
|
586
|
+
// The active preset registers no /plan.
|
|
587
|
+
state.notify(t('mode-plan-unavailable'), { color: 'warning' });
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
// The durable sandbox override is one session event (dsh-sandbox-policy's
|
|
592
|
+
// own write path); the session/event arm picks it up immediately.
|
|
593
|
+
if (spec.sandbox !== undefined && foldSandboxMode(agent.session.events) !== spec.sandbox) {
|
|
594
|
+
;
|
|
595
|
+
agent.session.append('sandbox/mode', { mode: spec.sandbox });
|
|
596
|
+
}
|
|
597
|
+
// Prefer the approval service (it narrates the switch to the model);
|
|
598
|
+
// the raw durable event is the fallback when it is unmounted.
|
|
599
|
+
if (spec.approval !== undefined && foldApprovalPolicy(agent.session.events) !== spec.approval) {
|
|
600
|
+
const approval = ctx.get('approval');
|
|
601
|
+
if (approval) {
|
|
602
|
+
approval.setPolicy(agent, spec.approval);
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
;
|
|
606
|
+
agent.session.append('approval/policy', { policy: spec.approval });
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
refreshMode();
|
|
610
|
+
state.notify(t('mode-switched', { name: modeDisplayName(state.mode) }));
|
|
439
611
|
state.emit();
|
|
440
612
|
};
|
|
613
|
+
/** Shift+Tab: advance to the next configured session mode. Cycling starts
|
|
614
|
+
* from the mode DERIVED from the session log (never a stored index), so
|
|
615
|
+
* manual `/plan` use can never desync the cycle. */
|
|
616
|
+
const cycleMode = async () => {
|
|
617
|
+
const index = deriveModeIndex(agent.session.events);
|
|
618
|
+
await applyMode(sessionModes[(index + 1) % sessionModes.length]);
|
|
619
|
+
};
|
|
441
620
|
const state = {
|
|
442
621
|
version: 0,
|
|
443
622
|
rows: [],
|
|
@@ -448,6 +627,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
448
627
|
provider: options.provider,
|
|
449
628
|
tokens: { input: 0, output: 0 },
|
|
450
629
|
cwd: options.cwd,
|
|
630
|
+
displayCwd: workspaceService.describe(options.cwd).description ?? options.cwd,
|
|
451
631
|
gitBranch: undefined,
|
|
452
632
|
working: false,
|
|
453
633
|
spinnerMode: 'requesting',
|
|
@@ -457,9 +637,13 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
457
637
|
lastUserText: '',
|
|
458
638
|
notifications: [],
|
|
459
639
|
contextWindow: undefined,
|
|
460
|
-
// Explicit cordis.yml `effort` wins; otherwise the persisted
|
|
640
|
+
// Explicit cordis.yml `effort` wins; otherwise the persisted /effort
|
|
461
641
|
// choice; the first request/header event re-asserts the adapter's truth.
|
|
462
642
|
reasoningEffort: options.effort ?? readEffortPref(),
|
|
643
|
+
// Session-mode seed; the first refreshMode() (bindAgent) re-derives it
|
|
644
|
+
// from the session log, so a resumed session lands on its recorded mode.
|
|
645
|
+
mode: sessionModes[0],
|
|
646
|
+
modeIndex: 0,
|
|
463
647
|
workingActivity: undefined,
|
|
464
648
|
activityFrames: options.activityFrames,
|
|
465
649
|
activityEnabled: options.activity !== false,
|
|
@@ -470,6 +654,29 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
470
654
|
loadedContext: undefined,
|
|
471
655
|
pending: [],
|
|
472
656
|
commandList: LOCAL_COMMANDS,
|
|
657
|
+
commandCompletions(input) {
|
|
658
|
+
return completeCommands(input, state.commandList, (path) => {
|
|
659
|
+
if (path.length === 1 && path[0] === 'workspace') {
|
|
660
|
+
const builtins = [
|
|
661
|
+
{ name: 'resume', description: 'Switch to another workspace', descriptionKey: 'cmd-desc-workspace-resume' },
|
|
662
|
+
{ name: 'rename', description: 'Rename the current workspace', descriptionKey: 'cmd-desc-workspace-rename' },
|
|
663
|
+
{ name: 'open', description: 'Open a path or workspace URI', descriptionKey: 'cmd-desc-workspace-open' },
|
|
664
|
+
];
|
|
665
|
+
const reserved = new Set(builtins.map(command => command.name));
|
|
666
|
+
return [
|
|
667
|
+
...builtins,
|
|
668
|
+
...workspaceService.commands()
|
|
669
|
+
.filter(command => !reserved.has(command.name.toLowerCase()))
|
|
670
|
+
.map(command => ({
|
|
671
|
+
name: command.name,
|
|
672
|
+
aliases: command.aliases,
|
|
673
|
+
description: command.description,
|
|
674
|
+
})),
|
|
675
|
+
];
|
|
676
|
+
}
|
|
677
|
+
return commandTrees?.children(path) ?? [];
|
|
678
|
+
});
|
|
679
|
+
},
|
|
473
680
|
lastUsage: undefined,
|
|
474
681
|
tps: undefined,
|
|
475
682
|
tpsSamples: [],
|
|
@@ -522,6 +729,30 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
522
729
|
state.emit();
|
|
523
730
|
return restored;
|
|
524
731
|
},
|
|
732
|
+
async stageImage(input) {
|
|
733
|
+
const attachments = mentionAttachments(ctx);
|
|
734
|
+
if (attachments === undefined)
|
|
735
|
+
throw new Error('image attachments are unavailable in this profile');
|
|
736
|
+
if (!attachments.imageLimits.mediaTypes.includes(input.mediaType)) {
|
|
737
|
+
throw new Error(`${input.mediaType} images are not accepted by this profile`);
|
|
738
|
+
}
|
|
739
|
+
if (input.data.byteLength > attachments.imageLimits.maxImageBytes) {
|
|
740
|
+
throw new Error(`image exceeds this profile's per-image size limit`);
|
|
741
|
+
}
|
|
742
|
+
const attachment = await attachments.saveImage(input);
|
|
743
|
+
stagedImageSequence += 1;
|
|
744
|
+
const token = `[Image #${stagedImageSequence}]`;
|
|
745
|
+
stagedImages.set(token, attachment);
|
|
746
|
+
// References are content-addressed and durable. This map only connects
|
|
747
|
+
// editable prompt placeholders to them; cap it to bound a long TUI run.
|
|
748
|
+
while (stagedImages.size > 128) {
|
|
749
|
+
const oldest = stagedImages.keys().next().value;
|
|
750
|
+
if (oldest === undefined)
|
|
751
|
+
break;
|
|
752
|
+
stagedImages.delete(oldest);
|
|
753
|
+
}
|
|
754
|
+
return token;
|
|
755
|
+
},
|
|
525
756
|
submit(text) {
|
|
526
757
|
const trimmed = text.trim();
|
|
527
758
|
if (!trimmed)
|
|
@@ -619,7 +850,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
619
850
|
const sessions = ctx.get('sessions');
|
|
620
851
|
const agents = ctx.get('agents');
|
|
621
852
|
if (!sessions || !agents) {
|
|
622
|
-
state.notify('
|
|
853
|
+
state.notify(t('rewind-unavailable'), { color: 'error' });
|
|
623
854
|
return null;
|
|
624
855
|
}
|
|
625
856
|
// Stop a running turn first and WAIT for its turn/end to land — fork
|
|
@@ -632,7 +863,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
632
863
|
if (wasWorking) {
|
|
633
864
|
const turnSettled = await waitForTurnEnd(agent.session, cancelSeq, 30000);
|
|
634
865
|
if (!turnSettled) {
|
|
635
|
-
state.notify('
|
|
866
|
+
state.notify(t('rewind-settling'), { color: 'error' });
|
|
636
867
|
return null;
|
|
637
868
|
}
|
|
638
869
|
}
|
|
@@ -669,7 +900,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
669
900
|
}
|
|
670
901
|
catch (error) {
|
|
671
902
|
const message = error instanceof Error ? error.message : String(error);
|
|
672
|
-
state.notify(
|
|
903
|
+
state.notify(t('rewind-fork-failed', { err: message }), { color: 'error' });
|
|
673
904
|
return null;
|
|
674
905
|
}
|
|
675
906
|
let handle;
|
|
@@ -684,7 +915,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
684
915
|
sessionId: childId,
|
|
685
916
|
seed,
|
|
686
917
|
meta: {
|
|
687
|
-
cwd:
|
|
918
|
+
cwd: state.cwd,
|
|
688
919
|
parentSession: agent.session.id,
|
|
689
920
|
seedLength: seed.length,
|
|
690
921
|
...(rewindComposed.agentPreset === undefined
|
|
@@ -696,9 +927,15 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
696
927
|
});
|
|
697
928
|
}
|
|
698
929
|
catch {
|
|
699
|
-
state.notify('
|
|
930
|
+
state.notify(t('rewind-create-failed'), { color: 'error' });
|
|
700
931
|
return null;
|
|
701
932
|
}
|
|
933
|
+
try {
|
|
934
|
+
await attachSessionToWorkspace(ctx, state.cwd, childId);
|
|
935
|
+
}
|
|
936
|
+
catch (error) {
|
|
937
|
+
state.notify(t('rewind-attach-failed', { err: error instanceof Error ? error.message : String(error) }), { color: 'warning', timeoutMs: 8000 });
|
|
938
|
+
}
|
|
702
939
|
// Replay the forked history into a fresh transcript (tokens/spinner
|
|
703
940
|
// counters land back at the rewind point, matching the fork).
|
|
704
941
|
streaming = undefined;
|
|
@@ -709,6 +946,11 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
709
946
|
// Goal/todo/title are session-scoped; the replay re-derives them for
|
|
710
947
|
// the session being entered (or leaves them empty).
|
|
711
948
|
state.todos = [];
|
|
949
|
+
// Queued-but-undelivered messages live in the OLD agent's inbox; the
|
|
950
|
+
// swap must drop their previews or they linger forever (unretirable —
|
|
951
|
+
// retire events are filtered to the new agent, unwithdrawable — the
|
|
952
|
+
// new inbox never heard of them).
|
|
953
|
+
state.pending = [];
|
|
712
954
|
state.goal = undefined;
|
|
713
955
|
state.sessionTitle = '';
|
|
714
956
|
state.tokens = { input: 0, output: 0 };
|
|
@@ -740,6 +982,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
740
982
|
bindAgent();
|
|
741
983
|
refreshCommandList();
|
|
742
984
|
void refreshLoadedContext();
|
|
985
|
+
void refreshSkillCommands();
|
|
743
986
|
// The forked session (rewind) becomes the most recently used.
|
|
744
987
|
touchSession(childId);
|
|
745
988
|
state.emit();
|
|
@@ -749,17 +992,24 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
749
992
|
async resumeTo(sessionId) {
|
|
750
993
|
// Switch the live agent to a persisted session: /resume picker Enter
|
|
751
994
|
// loads the history immediately (the `--resume` launcher path keeps
|
|
752
|
-
// resolving through
|
|
995
|
+
// resolving through DSH_TUI_RESUME_SESSION at boot).
|
|
753
996
|
if (state.working) {
|
|
754
|
-
state.notify('
|
|
997
|
+
state.notify(t('resume-while-working'), { color: 'warning' });
|
|
755
998
|
return false;
|
|
756
999
|
}
|
|
757
1000
|
const agents = ctx.get('agents');
|
|
758
1001
|
if (!agents) {
|
|
759
|
-
state.notify('
|
|
1002
|
+
state.notify(t('resume-unavailable'), { color: 'error' });
|
|
760
1003
|
return false;
|
|
761
1004
|
}
|
|
762
1005
|
let handle;
|
|
1006
|
+
// Compat boundary: register vouched-for legacy event types (e.g.
|
|
1007
|
+
// activity/status from pre-#143 logs) in every reachable dsh-session
|
|
1008
|
+
// copy before ANY strict read path (preset lookup below, then the
|
|
1009
|
+
// harness seed validation) loads the target — the plugin's #119
|
|
1010
|
+
// registration never ran in processes where it is unmounted (issue
|
|
1011
|
+
// #153). In-process only: the shared log is never rewritten.
|
|
1012
|
+
ensureLegacySessionEventTypes();
|
|
763
1013
|
// The target session's own preset (from its persisted log) — never the
|
|
764
1014
|
// current preference: a resume re-enters the composition its history
|
|
765
1015
|
// was produced under. Same rule for the route: only an explicit
|
|
@@ -773,9 +1023,6 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
773
1023
|
model: options.configuredModel,
|
|
774
1024
|
});
|
|
775
1025
|
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
1026
|
handle = await agents.resume({
|
|
780
1027
|
resumeSessionId: SessionId(sessionId),
|
|
781
1028
|
agentOptions: { provider: resumeRoute?.provider, model: resumeRoute?.model },
|
|
@@ -784,9 +1031,18 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
784
1031
|
}
|
|
785
1032
|
catch (error) {
|
|
786
1033
|
const message = error instanceof Error ? error.message : String(error);
|
|
787
|
-
state.notify(
|
|
1034
|
+
state.notify(t('resume-failed', { err: message }), { color: 'error', timeoutMs: 8000 });
|
|
788
1035
|
return false;
|
|
789
1036
|
}
|
|
1037
|
+
try {
|
|
1038
|
+
// `/resume` is an explicit adoption of this persisted conversation.
|
|
1039
|
+
// This also repairs sessions created by TUI versions that predate the
|
|
1040
|
+
// separate workspace ownership ledger.
|
|
1041
|
+
await attachSessionToWorkspace(ctx, handle.agent.session.header.cwd ?? state.cwd, SessionId(sessionId));
|
|
1042
|
+
}
|
|
1043
|
+
catch (error) {
|
|
1044
|
+
state.notify(t('resume-attach-failed', { err: error instanceof Error ? error.message : String(error) }), { color: 'warning', timeoutMs: 8000 });
|
|
1045
|
+
}
|
|
790
1046
|
// Replay the persisted history into a fresh transcript (same reset as
|
|
791
1047
|
// rewindTo, plus the context window which the replay re-derives).
|
|
792
1048
|
streaming = undefined;
|
|
@@ -797,6 +1053,11 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
797
1053
|
// Goal/todo/title are session-scoped; the replay re-derives them for
|
|
798
1054
|
// the session being entered (or leaves them empty).
|
|
799
1055
|
state.todos = [];
|
|
1056
|
+
// Queued-but-undelivered messages live in the OLD agent's inbox; the
|
|
1057
|
+
// swap must drop their previews or they linger forever (unretirable —
|
|
1058
|
+
// retire events are filtered to the new agent, unwithdrawable — the
|
|
1059
|
+
// new inbox never heard of them).
|
|
1060
|
+
state.pending = [];
|
|
800
1061
|
state.goal = undefined;
|
|
801
1062
|
state.sessionTitle = '';
|
|
802
1063
|
state.tokens = { input: 0, output: 0 };
|
|
@@ -807,6 +1068,15 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
807
1068
|
state.spinnerMode = 'requesting';
|
|
808
1069
|
state.status = handle.agent.status;
|
|
809
1070
|
state.agentId = handle.agent.id;
|
|
1071
|
+
// Adopt the resumed session's persisted cwd (issue #96): pre-upgrade
|
|
1072
|
+
// sessions recorded the LAUNCH directory (often a repo subdirectory),
|
|
1073
|
+
// so keeping the freshly resolved root would split @ expansion / file
|
|
1074
|
+
// completion (state.cwd) from the agent's own workspace record — and
|
|
1075
|
+
// drop the session back out of the /resume filter. The branch
|
|
1076
|
+
// breadcrumb follows the adopted cwd.
|
|
1077
|
+
state.cwd = handle.agent.session.header.cwd ?? state.cwd;
|
|
1078
|
+
state.displayCwd = workspaceService.describe(state.cwd).description ?? state.cwd;
|
|
1079
|
+
refreshGitBranch();
|
|
810
1080
|
state.agentPreset = resumeComposed.agentPreset;
|
|
811
1081
|
// Status-line route follows the resumed session (review feedback): the
|
|
812
1082
|
// route it actually continues on — a complete cordis.yml pin, else the
|
|
@@ -839,12 +1109,14 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
839
1109
|
bindAgent();
|
|
840
1110
|
refreshCommandList();
|
|
841
1111
|
void refreshLoadedContext();
|
|
1112
|
+
void refreshSkillCommands();
|
|
842
1113
|
// Keep the `--resume` launcher contract pointing at the same session.
|
|
843
1114
|
writeResumeTarget(sessionId);
|
|
844
1115
|
// The resumed session is now the most recently used.
|
|
845
1116
|
touchSession(sessionId);
|
|
846
1117
|
state.emit();
|
|
847
1118
|
void oldHandle?.dispose().catch(() => { });
|
|
1119
|
+
clearStagedImages();
|
|
848
1120
|
return true;
|
|
849
1121
|
},
|
|
850
1122
|
async newSession() {
|
|
@@ -852,14 +1124,14 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
852
1124
|
// transcript reset, the `--resume` marker forgotten (the old session
|
|
853
1125
|
// stays persisted for /resume). Same reset shape as rewindTo/resumeTo.
|
|
854
1126
|
if (state.working) {
|
|
855
|
-
state.notify('
|
|
1127
|
+
state.notify(t('new-session-while-working'), {
|
|
856
1128
|
color: 'warning',
|
|
857
1129
|
});
|
|
858
1130
|
return false;
|
|
859
1131
|
}
|
|
860
1132
|
const agents = ctx.get('agents');
|
|
861
1133
|
if (!agents) {
|
|
862
|
-
state.notify('
|
|
1134
|
+
state.notify(t('new-session-unavailable'), {
|
|
863
1135
|
color: 'error',
|
|
864
1136
|
});
|
|
865
1137
|
return false;
|
|
@@ -893,7 +1165,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
893
1165
|
handle = await agents.create({
|
|
894
1166
|
sessionId,
|
|
895
1167
|
meta: {
|
|
896
|
-
cwd:
|
|
1168
|
+
cwd: state.cwd,
|
|
897
1169
|
...(newComposed.agentPreset === undefined
|
|
898
1170
|
? {}
|
|
899
1171
|
: { agentPreset: newComposed.agentPreset }),
|
|
@@ -904,12 +1176,18 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
904
1176
|
}
|
|
905
1177
|
catch (error) {
|
|
906
1178
|
const message = error instanceof Error ? error.message : String(error);
|
|
907
|
-
state.notify(
|
|
1179
|
+
state.notify(t('new-session-failed', { err: message }), {
|
|
908
1180
|
color: 'error',
|
|
909
1181
|
timeoutMs: 8000,
|
|
910
1182
|
});
|
|
911
1183
|
return false;
|
|
912
1184
|
}
|
|
1185
|
+
try {
|
|
1186
|
+
await attachSessionToWorkspace(ctx, state.cwd, sessionId);
|
|
1187
|
+
}
|
|
1188
|
+
catch (error) {
|
|
1189
|
+
state.notify(t('new-session-attach-failed', { err: error instanceof Error ? error.message : String(error) }), { color: 'warning', timeoutMs: 8000 });
|
|
1190
|
+
}
|
|
913
1191
|
streaming = undefined;
|
|
914
1192
|
reasoning = undefined;
|
|
915
1193
|
toolCards.clear();
|
|
@@ -918,6 +1196,11 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
918
1196
|
// Goal/todo/title are session-scoped; the replay re-derives them for
|
|
919
1197
|
// the session being entered (or leaves them empty).
|
|
920
1198
|
state.todos = [];
|
|
1199
|
+
// Queued-but-undelivered messages live in the OLD agent's inbox; the
|
|
1200
|
+
// swap must drop their previews or they linger forever (unretirable —
|
|
1201
|
+
// retire events are filtered to the new agent, unwithdrawable — the
|
|
1202
|
+
// new inbox never heard of them).
|
|
1203
|
+
state.pending = [];
|
|
921
1204
|
state.goal = undefined;
|
|
922
1205
|
state.sessionTitle = '';
|
|
923
1206
|
state.tokens = { input: 0, output: 0 };
|
|
@@ -949,20 +1232,79 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
949
1232
|
bindAgent();
|
|
950
1233
|
refreshCommandList();
|
|
951
1234
|
void refreshLoadedContext();
|
|
1235
|
+
void refreshSkillCommands();
|
|
952
1236
|
clearResumeTarget();
|
|
953
1237
|
// The brand-new session becomes the most recently used.
|
|
954
1238
|
touchSession(handle.agent.id);
|
|
955
|
-
state.emit();
|
|
956
1239
|
void oldHandle?.dispose().catch(() => { });
|
|
1240
|
+
clearStagedImages();
|
|
1241
|
+
return true;
|
|
1242
|
+
},
|
|
1243
|
+
listWorkspaces() {
|
|
1244
|
+
return workspaceService.list(state.cwd);
|
|
1245
|
+
},
|
|
1246
|
+
resolveWorkspace(uri) {
|
|
1247
|
+
return workspaceService.resolve(uri, state.cwd);
|
|
1248
|
+
},
|
|
1249
|
+
async switchWorkspace(target) {
|
|
1250
|
+
if (state.working) {
|
|
1251
|
+
state.notify(t('workspace-switch-working'), { color: 'warning' });
|
|
1252
|
+
return false;
|
|
1253
|
+
}
|
|
1254
|
+
// Local targets must exist and be directories — creating a session in
|
|
1255
|
+
// a typo'd cwd "succeeds" and then every file tool errors per call.
|
|
1256
|
+
if (target.kind === 'local') {
|
|
1257
|
+
try {
|
|
1258
|
+
if (!statSync(target.cwd).isDirectory())
|
|
1259
|
+
throw new Error('not a directory');
|
|
1260
|
+
}
|
|
1261
|
+
catch {
|
|
1262
|
+
state.notify(t('workspace-open-invalid', { target: target.label }), { color: 'error', timeoutMs: 8000 });
|
|
1263
|
+
return false;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
const previousCwd = state.cwd;
|
|
1267
|
+
const previousDisplay = state.displayCwd;
|
|
1268
|
+
state.cwd = target.cwd;
|
|
1269
|
+
state.displayCwd = target.description ?? target.uri;
|
|
1270
|
+
const switched = await state.newSession();
|
|
1271
|
+
if (!switched) {
|
|
1272
|
+
state.cwd = previousCwd;
|
|
1273
|
+
state.displayCwd = previousDisplay;
|
|
1274
|
+
return false;
|
|
1275
|
+
}
|
|
1276
|
+
// The breadcrumb follows the adopted cwd, same as /resume (#96).
|
|
1277
|
+
refreshGitBranch();
|
|
1278
|
+
state.notify(t('workspace-switched', { target: target.label }));
|
|
1279
|
+
state.emit();
|
|
957
1280
|
return true;
|
|
958
1281
|
},
|
|
1282
|
+
async renameWorkspace(title) {
|
|
1283
|
+
try {
|
|
1284
|
+
const renamed = await workspaceService.rename(state.cwd, title);
|
|
1285
|
+
state.displayCwd = renamed.description ?? renamed.uri;
|
|
1286
|
+
state.notify(t('workspace-renamed', { title: renamed.label }));
|
|
1287
|
+
state.emit();
|
|
1288
|
+
return true;
|
|
1289
|
+
}
|
|
1290
|
+
catch (error) {
|
|
1291
|
+
state.notify(t('workspace-rename-failed', { err: error instanceof Error ? error.message : String(error) }), { color: 'error', timeoutMs: 8000 });
|
|
1292
|
+
return false;
|
|
1293
|
+
}
|
|
1294
|
+
},
|
|
1295
|
+
workspaceCommands() {
|
|
1296
|
+
return workspaceService.commands();
|
|
1297
|
+
},
|
|
1298
|
+
runWorkspaceCommand(name, input) {
|
|
1299
|
+
return workspaceService.runCommand(name, input, state.cwd);
|
|
1300
|
+
},
|
|
959
1301
|
async switchModel(provider, model) {
|
|
960
1302
|
// `/model` picker Enter — switch the live model by forking the
|
|
961
1303
|
// conversation at its current end and continuing with a new agent
|
|
962
1304
|
// routed to the chosen model. Same reset shape as rewindTo/resumeTo;
|
|
963
1305
|
// the history replays unchanged, only the request model changes.
|
|
964
1306
|
if (state.working) {
|
|
965
|
-
state.notify('
|
|
1307
|
+
state.notify(t('model-switch-while-working'), {
|
|
966
1308
|
color: 'warning',
|
|
967
1309
|
});
|
|
968
1310
|
return false;
|
|
@@ -970,7 +1312,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
970
1312
|
const sessions = ctx.get('sessions');
|
|
971
1313
|
const agents = ctx.get('agents');
|
|
972
1314
|
if (!sessions || !agents) {
|
|
973
|
-
state.notify('
|
|
1315
|
+
state.notify(t('model-switch-unavailable'), {
|
|
974
1316
|
color: 'error',
|
|
975
1317
|
});
|
|
976
1318
|
return false;
|
|
@@ -982,7 +1324,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
982
1324
|
}
|
|
983
1325
|
catch (error) {
|
|
984
1326
|
const message = error instanceof Error ? error.message : String(error);
|
|
985
|
-
state.notify(
|
|
1327
|
+
state.notify(t('model-switch-fork-failed', { err: message }), { color: 'error' });
|
|
986
1328
|
return false;
|
|
987
1329
|
}
|
|
988
1330
|
const childId = SessionId(randomUUID());
|
|
@@ -995,7 +1337,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
995
1337
|
sessionId: childId,
|
|
996
1338
|
seed,
|
|
997
1339
|
meta: {
|
|
998
|
-
cwd:
|
|
1340
|
+
cwd: state.cwd,
|
|
999
1341
|
parentSession: agent.session.id,
|
|
1000
1342
|
seedLength: seed.length,
|
|
1001
1343
|
...(modelComposed.agentPreset === undefined
|
|
@@ -1008,9 +1350,15 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1008
1350
|
}
|
|
1009
1351
|
catch (error) {
|
|
1010
1352
|
const message = error instanceof Error ? error.message : String(error);
|
|
1011
|
-
state.notify(
|
|
1353
|
+
state.notify(t('model-switch-failed', { err: message }), { color: 'error', timeoutMs: 8000 });
|
|
1012
1354
|
return false;
|
|
1013
1355
|
}
|
|
1356
|
+
try {
|
|
1357
|
+
await attachSessionToWorkspace(ctx, state.cwd, childId);
|
|
1358
|
+
}
|
|
1359
|
+
catch (error) {
|
|
1360
|
+
state.notify(t('model-switch-attach-failed', { err: error instanceof Error ? error.message : String(error) }), { color: 'warning', timeoutMs: 8000 });
|
|
1361
|
+
}
|
|
1014
1362
|
streaming = undefined;
|
|
1015
1363
|
reasoning = undefined;
|
|
1016
1364
|
toolCards.clear();
|
|
@@ -1019,6 +1367,11 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1019
1367
|
// Goal/todo/title are session-scoped; the replay re-derives them for
|
|
1020
1368
|
// the session being entered (or leaves them empty).
|
|
1021
1369
|
state.todos = [];
|
|
1370
|
+
// Queued-but-undelivered messages live in the OLD agent's inbox; the
|
|
1371
|
+
// swap must drop their previews or they linger forever (unretirable —
|
|
1372
|
+
// retire events are filtered to the new agent, unwithdrawable — the
|
|
1373
|
+
// new inbox never heard of them).
|
|
1374
|
+
state.pending = [];
|
|
1022
1375
|
state.goal = undefined;
|
|
1023
1376
|
state.sessionTitle = '';
|
|
1024
1377
|
state.tokens = { input: 0, output: 0 };
|
|
@@ -1053,12 +1406,13 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1053
1406
|
bindAgent();
|
|
1054
1407
|
refreshCommandList();
|
|
1055
1408
|
void refreshLoadedContext();
|
|
1409
|
+
void refreshSkillCommands();
|
|
1056
1410
|
// The model-switched fork becomes the most recently used.
|
|
1057
1411
|
touchSession(childId);
|
|
1058
1412
|
state.emit();
|
|
1059
1413
|
void oldHandle?.dispose().catch(() => { });
|
|
1060
1414
|
// Persist the choice so the next boot and `/new` start on it (same
|
|
1061
|
-
// contract as /preset and
|
|
1415
|
+
// contract as /preset and /effort; issues #14/#30). A failed
|
|
1062
1416
|
// write keeps the live switch but warns it will not survive a restart.
|
|
1063
1417
|
if (!writeModelPref(provider, model)) {
|
|
1064
1418
|
state.notify(t('model-pref-write-failed'), {
|
|
@@ -1067,7 +1421,9 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1067
1421
|
}
|
|
1068
1422
|
return true;
|
|
1069
1423
|
},
|
|
1070
|
-
|
|
1424
|
+
listEfforts,
|
|
1425
|
+
setEffort,
|
|
1426
|
+
cycleMode,
|
|
1071
1427
|
clear() {
|
|
1072
1428
|
state.rows.length = 0;
|
|
1073
1429
|
nextRowId = 0;
|
|
@@ -1210,6 +1566,105 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1210
1566
|
return Promise.all(providers.map(provider => llm.listModels(provider.id).catch(() => [])))
|
|
1211
1567
|
.then(lists => lists.flat());
|
|
1212
1568
|
},
|
|
1569
|
+
providerSetup() {
|
|
1570
|
+
// The `/provider` wizard's runtime surface, over the dsh-base seams:
|
|
1571
|
+
// settings (profile persistence), credentials (key storage) and the
|
|
1572
|
+
// llm runtime's configurable-provider directory + model discovery.
|
|
1573
|
+
// Structurally typed like the other optional seams in this file.
|
|
1574
|
+
const llm = ctx.get('llm');
|
|
1575
|
+
const settings = ctx.get('settings');
|
|
1576
|
+
const credentials = ctx.get('credentials');
|
|
1577
|
+
// Without dsh-llm-pi-ai there is no adapter watching the settings
|
|
1578
|
+
// section, so a written profile would never activate a route. The
|
|
1579
|
+
// adapter registers its `llm-pi-ai` settings namespace at mount, which
|
|
1580
|
+
// is the rc.6-observable mount signal (the newer
|
|
1581
|
+
// `listModelDiscoveryNamespaces()` does not exist in rc.6).
|
|
1582
|
+
if (!llm || !settings || !credentials
|
|
1583
|
+
|| !settings.describe().some(descriptor => descriptor.ns === 'llm-pi-ai')) {
|
|
1584
|
+
return undefined;
|
|
1585
|
+
}
|
|
1586
|
+
const revision = () => settings.describe().find(descriptor => descriptor.ns === 'llm-pi-ai')?.revision;
|
|
1587
|
+
return {
|
|
1588
|
+
listCatalogProviders() {
|
|
1589
|
+
// declared === true marks routes the adapter knows only because a
|
|
1590
|
+
// stored profile names them (user-added); the rest are activatable
|
|
1591
|
+
// catalog routes.
|
|
1592
|
+
return llm.listConfigurableProviders()
|
|
1593
|
+
.filter(entry => entry.settingsNs === 'llm-pi-ai' && entry.declared !== true)
|
|
1594
|
+
.map(entry => ({ provider: entry.provider, displayName: entry.displayName }));
|
|
1595
|
+
},
|
|
1596
|
+
routeExists(route) {
|
|
1597
|
+
const section = settings.get('llm-pi-ai');
|
|
1598
|
+
return section?.providers !== undefined && route in section.providers;
|
|
1599
|
+
},
|
|
1600
|
+
discoverModels(request) {
|
|
1601
|
+
return llm.discoverModels('llm-pi-ai', request);
|
|
1602
|
+
},
|
|
1603
|
+
envShadows(ref) {
|
|
1604
|
+
return process.env[ref] !== undefined;
|
|
1605
|
+
},
|
|
1606
|
+
async readCredential(ref) {
|
|
1607
|
+
const resolved = await credentials.resolve(ref);
|
|
1608
|
+
return resolved?.value;
|
|
1609
|
+
},
|
|
1610
|
+
writeCredential(ref, value) {
|
|
1611
|
+
return credentials.set(ref, value);
|
|
1612
|
+
},
|
|
1613
|
+
removeCredential(ref) {
|
|
1614
|
+
return credentials.unset(ref);
|
|
1615
|
+
},
|
|
1616
|
+
async writeProfile(route, profile) {
|
|
1617
|
+
const ops = [{ op: 'set', path: ['providers', route], value: profile }];
|
|
1618
|
+
try {
|
|
1619
|
+
await settings.mutate('llm-pi-ai', ops, revision());
|
|
1620
|
+
}
|
|
1621
|
+
catch (error) {
|
|
1622
|
+
// One retry on a stale-revision conflict (a concurrent write
|
|
1623
|
+
// landed between describe and mutate); anything else propagates
|
|
1624
|
+
// so the wizard can report and roll back the credential.
|
|
1625
|
+
const code = error?.code;
|
|
1626
|
+
if (code !== 'SETTINGS_CONFLICT')
|
|
1627
|
+
throw error;
|
|
1628
|
+
await settings.mutate('llm-pi-ai', ops, revision());
|
|
1629
|
+
}
|
|
1630
|
+
},
|
|
1631
|
+
};
|
|
1632
|
+
},
|
|
1633
|
+
async sideQuestion(question, options) {
|
|
1634
|
+
// CC /btw:无工具单轮辅助调用,重放 deriveMessages() 前缀 + 一条
|
|
1635
|
+
// 包装问题。tools 永不传(侧问无工具是核心语义);usage 不回收
|
|
1636
|
+
// (skipCacheWrite 同义——答案不进主上下文也不进 token 计数)。
|
|
1637
|
+
const llm = ctx.get('llm');
|
|
1638
|
+
if (!llm)
|
|
1639
|
+
return { answer: null, error: t('btw-llm-unavailable') };
|
|
1640
|
+
const header = agent.session.requestHeader();
|
|
1641
|
+
const config = header?.config;
|
|
1642
|
+
const messages = [
|
|
1643
|
+
...agent.session.deriveMessages(),
|
|
1644
|
+
createUserMessage({
|
|
1645
|
+
content: [{ type: 'text', text: wrapSideQuestion(question) }],
|
|
1646
|
+
source: { kind: 'plugin', plugin: 'dsh-tui/btw' },
|
|
1647
|
+
}),
|
|
1648
|
+
];
|
|
1649
|
+
const request = {
|
|
1650
|
+
provider: config?.provider ?? state.provider,
|
|
1651
|
+
model: config?.model ?? state.model,
|
|
1652
|
+
messages,
|
|
1653
|
+
...(header?.system !== undefined && { system: header.system }),
|
|
1654
|
+
...(config?.reasoningEffort !== undefined && { reasoningEffort: config.reasoningEffort }),
|
|
1655
|
+
...(config?.temperature !== undefined && { temperature: config.temperature }),
|
|
1656
|
+
...(config?.maxTokens !== undefined && { maxTokens: config.maxTokens }),
|
|
1657
|
+
...(config?.stop !== undefined && { stop: [...config.stop] }),
|
|
1658
|
+
sessionId: agent.session.id,
|
|
1659
|
+
...(options?.signal && { signal: options.signal }),
|
|
1660
|
+
};
|
|
1661
|
+
return runSideQuestion({
|
|
1662
|
+
stream: llm.stream.bind(llm),
|
|
1663
|
+
options: request,
|
|
1664
|
+
onText: options?.onText,
|
|
1665
|
+
signal: options?.signal,
|
|
1666
|
+
});
|
|
1667
|
+
},
|
|
1213
1668
|
listFiles() {
|
|
1214
1669
|
const fs = ctx.get('fs');
|
|
1215
1670
|
return listFilesDeep(fs, state.cwd);
|
|
@@ -1224,8 +1679,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1224
1679
|
const headers = await persistence.list();
|
|
1225
1680
|
// 按工作目录隔离(Claude Code 的项目维度):/resume 只列出本会话
|
|
1226
1681
|
// 目录启动的会话,别的项目的会话不出现在选择器里。
|
|
1227
|
-
const
|
|
1228
|
-
const local = headers.filter(header => (header.cwd ?? '').replace(/\/+$/, '') === cwd);
|
|
1682
|
+
const local = headers.filter(header => sessionCwdMatches(state.cwd, header.cwd ?? ''));
|
|
1229
1683
|
// MRU ordering: DSH headers carry only createdAt, so dsh-tui keeps its
|
|
1230
1684
|
// own last-used timestamps (touchSession on resume/submit/new) and
|
|
1231
1685
|
// falls back to createdAt for sessions never touched in this install.
|
|
@@ -1247,8 +1701,8 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1247
1701
|
// useful label. Read via the tolerant compat log reader instead of
|
|
1248
1702
|
// persistence.load: the backend validates every event against
|
|
1249
1703
|
// KNOWN_SESSION_EVENT_TYPES and throws the whole load on an unmarked
|
|
1250
|
-
// third-party type
|
|
1251
|
-
//
|
|
1704
|
+
// an unregistered third-party type, which would otherwise leave the
|
|
1705
|
+
// affected session titled with the cwd
|
|
1252
1706
|
// basename. An unreadable log keeps the basename fallback.
|
|
1253
1707
|
const empty = new Set();
|
|
1254
1708
|
for (const record of records.slice(0, SESSION_TITLE_DEPTH)) {
|
|
@@ -1284,6 +1738,41 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1284
1738
|
state.sessionTitle = title;
|
|
1285
1739
|
state.emit();
|
|
1286
1740
|
},
|
|
1741
|
+
async deleteSession(sessionId) {
|
|
1742
|
+
// The live session's log is still being appended by this process —
|
|
1743
|
+
// deleting it from under the writer is never offered in the picker
|
|
1744
|
+
// (the current session is filtered out), so refuse it here too.
|
|
1745
|
+
if (sessionId === agent.session.id)
|
|
1746
|
+
return false;
|
|
1747
|
+
if (deleteSessionLog(sessionId) !== 'deleted')
|
|
1748
|
+
return false;
|
|
1749
|
+
forgetSession(sessionId);
|
|
1750
|
+
// A resume marker naming the deleted session would make the next
|
|
1751
|
+
// `dsh-tui --resume` launch target a log that no longer exists.
|
|
1752
|
+
if (readResumeTarget() === sessionId)
|
|
1753
|
+
clearResumeTarget();
|
|
1754
|
+
return true;
|
|
1755
|
+
},
|
|
1756
|
+
async renameSessionTo(sessionId, title) {
|
|
1757
|
+
if (sessionId === agent.session.id) {
|
|
1758
|
+
// The live session renames through session.append so the firehose
|
|
1759
|
+
// updates the status line right away (same as /rename).
|
|
1760
|
+
agent.session.append('session/title', { title });
|
|
1761
|
+
state.sessionTitle = title;
|
|
1762
|
+
state.emit();
|
|
1763
|
+
return true;
|
|
1764
|
+
}
|
|
1765
|
+
if (appendSessionTitle(sessionId, title) !== 'appended')
|
|
1766
|
+
return false;
|
|
1767
|
+
// listSessions resolves persisted titles only for the MRU top
|
|
1768
|
+
// SESSION_TITLE_DEPTH; a rename does not change MRU by itself, so a
|
|
1769
|
+
// session beyond the window would keep showing the cwd-basename
|
|
1770
|
+
// fallback (in the next picker AND after restart) even though the
|
|
1771
|
+
// title event is durable. A rename IS user interaction with the
|
|
1772
|
+
// session — touching it pulls it into the title window.
|
|
1773
|
+
touchSession(sessionId);
|
|
1774
|
+
return true;
|
|
1775
|
+
},
|
|
1287
1776
|
compact() {
|
|
1288
1777
|
// DSH compaction service key: `ctx.compaction` (dsh-compaction's
|
|
1289
1778
|
// CompactionEngine; dsh-compaction-basic provides it in the example
|
|
@@ -1292,38 +1781,28 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1292
1781
|
// scope chain first (minimal composes NO compaction: stays unavailable).
|
|
1293
1782
|
const compactService = serviceForAgent(ctx, agent, 'compaction');
|
|
1294
1783
|
if (!compactService) {
|
|
1295
|
-
state.notify('
|
|
1784
|
+
state.notify(t('compact-unavailable'), {
|
|
1296
1785
|
color: 'warning',
|
|
1297
1786
|
});
|
|
1298
1787
|
return;
|
|
1299
1788
|
}
|
|
1300
1789
|
if (state.working) {
|
|
1301
|
-
state.notify('
|
|
1790
|
+
state.notify(t('compact-while-working'), { color: 'warning' });
|
|
1302
1791
|
return;
|
|
1303
1792
|
}
|
|
1304
1793
|
const signal = new AbortController().signal;
|
|
1305
|
-
state.notify('
|
|
1794
|
+
state.notify(t('compact-working'));
|
|
1306
1795
|
void compactService
|
|
1307
1796
|
.compactNow(agent, signal)
|
|
1308
1797
|
.then((result) => {
|
|
1309
|
-
state.notify(result ? '
|
|
1798
|
+
state.notify(result ? t('compact-done') : t('compact-nothing'));
|
|
1310
1799
|
})
|
|
1311
1800
|
.catch((error) => {
|
|
1312
|
-
state.notify(
|
|
1801
|
+
state.notify(t('compact-failed', { err: error instanceof Error ? error.message : String(error) }), { color: 'error', timeoutMs: 8000 });
|
|
1313
1802
|
});
|
|
1314
1803
|
},
|
|
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
|
-
}
|
|
1804
|
+
runExternalCommand(name, rawInput) {
|
|
1805
|
+
return executeRegistryCommand(name, rawInput);
|
|
1327
1806
|
},
|
|
1328
1807
|
pushLocal(title, lines) {
|
|
1329
1808
|
state.rows.push({ id: nextRowId++, kind: 'local', text: title });
|
|
@@ -1464,16 +1943,24 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1464
1943
|
lines.push(t('doctor-cwd', { cwd: state.cwd }));
|
|
1465
1944
|
lines.push(t('doctor-context-window', { window: state.contextWindow ?? t('doctor-unknown') }));
|
|
1466
1945
|
lines.push(`${t('doctor-session', { id: state.agentId })}${state.sessionTitle ? ' · ' + state.sessionTitle : ''}`);
|
|
1467
|
-
const userHome =
|
|
1946
|
+
const userHome = homeDir();
|
|
1468
1947
|
const configCandidates = [
|
|
1469
|
-
join(userHome, '.dsh-
|
|
1948
|
+
join(userHome, '.dsh-tui/cordis.yml'),
|
|
1470
1949
|
join(userHome, '.dsh/profiles/dsh-tui/cordis.patch.yml'),
|
|
1471
1950
|
];
|
|
1472
1951
|
for (const candidate of configCandidates) {
|
|
1473
1952
|
lines.push(`${t('doctor-config', { candidate, state: existsSync(candidate) ? '✓' : t('doctor-config-missing') })}`);
|
|
1474
1953
|
}
|
|
1475
|
-
|
|
1476
|
-
|
|
1954
|
+
// Session store candidates mirror the compat layer (sessionsRoots):
|
|
1955
|
+
// the active root depends on the composition (bare cordis.yml →
|
|
1956
|
+
// legacy ~/.dsh-tui, profile → $DSH_HOME/sessions), so list every
|
|
1957
|
+
// candidate with its own state instead of hardcoding one.
|
|
1958
|
+
for (const dir of sessionsRoots()) {
|
|
1959
|
+
lines.push(`${t('doctor-storage', { dir, state: existsSync(dir) ? '✓' : t('doctor-storage-uninit') })}`);
|
|
1960
|
+
}
|
|
1961
|
+
if (existsSync(LEGACY_DATA_DIR)) {
|
|
1962
|
+
lines.push(t('doctor-legacy-dir'));
|
|
1963
|
+
}
|
|
1477
1964
|
return lines;
|
|
1478
1965
|
},
|
|
1479
1966
|
async listSubagents() {
|
|
@@ -1495,6 +1982,14 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1495
1982
|
return [t('subagent-query-failed', { err: error instanceof Error ? error.message : String(error) })];
|
|
1496
1983
|
}
|
|
1497
1984
|
},
|
|
1985
|
+
releaseContributions() {
|
|
1986
|
+
releaseSkillCommands();
|
|
1987
|
+
},
|
|
1988
|
+
traceEvents() {
|
|
1989
|
+
// Immutable per-append snapshot (dsh-session caches the frozen array);
|
|
1990
|
+
// reads follow agent swaps (/resume /rewind /new) automatically.
|
|
1991
|
+
return agent.session.events;
|
|
1992
|
+
},
|
|
1498
1993
|
};
|
|
1499
1994
|
/**
|
|
1500
1995
|
* Assemble the context a fresh conversation for the live agent will load,
|
|
@@ -1536,10 +2031,20 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1536
2031
|
tools.push({ name: tool.name, description: tool.description ?? '' });
|
|
1537
2032
|
}
|
|
1538
2033
|
}
|
|
1539
|
-
const
|
|
2034
|
+
const renderedInstructions = await loadBaselineInstructions({
|
|
2035
|
+
cwd: state.cwd,
|
|
2036
|
+
maxBytes: 1024 * 1024,
|
|
2037
|
+
maxSourceBytes: 1024 * 1024,
|
|
2038
|
+
}, ctx.get('fs'));
|
|
1540
2039
|
if (target !== agent)
|
|
1541
2040
|
return;
|
|
1542
|
-
|
|
2041
|
+
const instructionSources = renderedInstructions;
|
|
2042
|
+
const instructionPaths = new Set([
|
|
2043
|
+
...(instructionSources?.represented ?? []).map(file => file.displayPath),
|
|
2044
|
+
...(renderedInstructions?.omitted ?? []).map(file => file.displayPath),
|
|
2045
|
+
...(renderedInstructions?.truncated ?? []).map(file => file.displayPath),
|
|
2046
|
+
]);
|
|
2047
|
+
files.push(...[...instructionPaths].map(displayPath => ({ displayPath })));
|
|
1543
2048
|
// The skills registry is host-plane but scope-layered: preset rows
|
|
1544
2049
|
// (skill-filesystem) register into the preset's layer, so the catalog
|
|
1545
2050
|
// must be read through the agent's scope chain (serviceForAgent falls
|
|
@@ -1563,51 +2068,289 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1563
2068
|
state.emit();
|
|
1564
2069
|
};
|
|
1565
2070
|
/**
|
|
1566
|
-
* Rebuild the merged slash-command list
|
|
1567
|
-
*
|
|
1568
|
-
*
|
|
1569
|
-
*
|
|
2071
|
+
* Rebuild the merged slash-command list: built-in locals, then registry
|
|
2072
|
+
* commands (plan/goal/…), then user-invocable skills from the DSH skill
|
|
2073
|
+
* registry (issue #86 — filesystem-discovered skills must appear in the
|
|
2074
|
+
* `/` menu and Tab completion, like /audit and /review). Skill entries
|
|
2075
|
+
* are completion-only: dispatch falls through to the model as plain text,
|
|
2076
|
+
* where dsh-tool-skill's pre-step hook injects the skill body — the same
|
|
2077
|
+
* path a hand-typed `/skill-name` takes. Registry and skill reads are
|
|
2078
|
+
* scoped to the LIVE agent, so this runs on `commands/change` +
|
|
2079
|
+
* `skills/change` and again whenever the live agent is swapped
|
|
2080
|
+
* (rewind/resume/new/model). A failed skill read restores the last
|
|
2081
|
+
* successfully merged skill set for the same agent (last-good), so a
|
|
2082
|
+
* transient provider failure never makes known skills vanish.
|
|
1570
2083
|
*/
|
|
2084
|
+
let commandListSeq = 0;
|
|
2085
|
+
/**
|
|
2086
|
+
* The last successfully merged skill entries, tagged with the agent whose
|
|
2087
|
+
* scope produced them. A failed catalog read restores these instead of
|
|
2088
|
+
* dropping skill entries from the menu until the next successful refresh
|
|
2089
|
+
* (last-good); the agent tag refuses cross-agent restores — a different
|
|
2090
|
+
* scope's skills may not exist for the live agent at all.
|
|
2091
|
+
*/
|
|
2092
|
+
let lastGoodSkills;
|
|
1571
2093
|
const refreshCommandList = () => {
|
|
2094
|
+
const target = agent;
|
|
2095
|
+
const token = ++commandListSeq;
|
|
1572
2096
|
const merged = [...LOCAL_COMMANDS];
|
|
1573
2097
|
if (commandService) {
|
|
1574
|
-
for (const descriptor of commandService.list(
|
|
2098
|
+
for (const descriptor of commandService.list(target)) {
|
|
1575
2099
|
if (merged.some(command => command.name === descriptor.name))
|
|
1576
2100
|
continue;
|
|
2101
|
+
const descriptions = commandTrees?.descriptions(descriptor.name);
|
|
1577
2102
|
merged.push({
|
|
1578
2103
|
name: descriptor.name,
|
|
1579
2104
|
description: descriptor.description,
|
|
2105
|
+
...(descriptions === undefined ? {} : { descriptions }),
|
|
1580
2106
|
tag: descriptor.input?.hint,
|
|
1581
2107
|
external: true,
|
|
2108
|
+
// Skills reach the registry as ordinary commands, so the menu would
|
|
2109
|
+
// lose the marker HelpMenu uses to keep them out of the chrome list.
|
|
2110
|
+
// This channel registered them and is the authority on which names
|
|
2111
|
+
// are skills.
|
|
2112
|
+
...(skillCommands.has(descriptor.name) ? { skill: true } : {}),
|
|
1582
2113
|
});
|
|
1583
2114
|
}
|
|
1584
2115
|
}
|
|
1585
2116
|
state.commandList = merged;
|
|
1586
2117
|
state.emit();
|
|
2118
|
+
// The skill catalog resolves asynchronously (filesystem providers scan
|
|
2119
|
+
// their roots), so skills append in a continuation; a newer refresh or
|
|
2120
|
+
// an agent swap supersedes this run (token/identity check, same rule as
|
|
2121
|
+
// refreshLoadedContext). Locals and registry commands win name
|
|
2122
|
+
// collisions — a skill named `plan` must not shadow the registry's.
|
|
2123
|
+
const skillsService = serviceForAgent(ctx, target, 'skills');
|
|
2124
|
+
if (skillsService === undefined)
|
|
2125
|
+
return;
|
|
2126
|
+
/** Last-good restore shared by the failed-read and incomplete-read
|
|
2127
|
+
* paths; the caller holds the staleness check. */
|
|
2128
|
+
const restoreLastGood = () => {
|
|
2129
|
+
const fallback = lastGoodSkills?.agent === target ? lastGoodSkills.commands : [];
|
|
2130
|
+
const restored = fallback.filter(entry => !merged.some(command => command.name === entry.name));
|
|
2131
|
+
if (restored.length === 0)
|
|
2132
|
+
return;
|
|
2133
|
+
state.commandList = [...merged, ...restored];
|
|
2134
|
+
state.emit();
|
|
2135
|
+
};
|
|
2136
|
+
// snapshot() over list(): only a COMPLETE observation is authoritative
|
|
2137
|
+
// — list() discards `complete`, so a provider failure or a rescan still
|
|
2138
|
+
// in flight would resolve as a partial/empty catalog and wrongly clear
|
|
2139
|
+
// the last-good set (dsh-skill's own consumer contract).
|
|
2140
|
+
void skillsService.snapshot({
|
|
2141
|
+
scope: target,
|
|
2142
|
+
cwd: target.session.header?.cwd ?? state.cwd,
|
|
2143
|
+
}).then((observation) => {
|
|
2144
|
+
if (token !== commandListSeq || target !== agent)
|
|
2145
|
+
return;
|
|
2146
|
+
if (!observation.complete) {
|
|
2147
|
+
// Incomplete (provider failure/rescan mid-flight): NOT authoritative
|
|
2148
|
+
// — never clear last-good or repopulate from the partial catalog.
|
|
2149
|
+
// The provider's next invalidate fires skills/change for the retry.
|
|
2150
|
+
ctx.logger.warn('skill command merge: incomplete catalog observation, keeping last-good skills');
|
|
2151
|
+
restoreLastGood();
|
|
2152
|
+
return;
|
|
2153
|
+
}
|
|
2154
|
+
const withSkills = [...merged];
|
|
2155
|
+
for (const skill of observation.skills) {
|
|
2156
|
+
if (!isUserInvocable(skill))
|
|
2157
|
+
continue;
|
|
2158
|
+
if (withSkills.some(command => command.name === skill.name))
|
|
2159
|
+
continue;
|
|
2160
|
+
withSkills.push({ name: skill.name, description: skill.description, skill: true });
|
|
2161
|
+
}
|
|
2162
|
+
const added = withSkills.slice(merged.length);
|
|
2163
|
+
lastGoodSkills = { agent: target, commands: added };
|
|
2164
|
+
// The sync phase already assigned `merged`; a complete read that adds
|
|
2165
|
+
// nothing leaves the state as-is (and authoritatively clears the
|
|
2166
|
+
// last-good set above).
|
|
2167
|
+
if (added.length === 0)
|
|
2168
|
+
return;
|
|
2169
|
+
state.commandList = withSkills;
|
|
2170
|
+
state.emit();
|
|
2171
|
+
}).catch((error) => {
|
|
2172
|
+
// A superseded read (a newer refresh or an agent swap beat it) says
|
|
2173
|
+
// nothing about the live menu: stay silent instead of logging a
|
|
2174
|
+
// misleading failure warning.
|
|
2175
|
+
if (token !== commandListSeq || target !== agent)
|
|
2176
|
+
return;
|
|
2177
|
+
ctx.logger.warn('skill command merge failed: %o', error);
|
|
2178
|
+
// Last-good: a transient provider failure (rescan error, permission
|
|
2179
|
+
// hiccup) must not make known skills vanish from completion.
|
|
2180
|
+
restoreLastGood();
|
|
2181
|
+
});
|
|
1587
2182
|
};
|
|
1588
2183
|
ctx.on('commands/change', refreshCommandList);
|
|
2184
|
+
ctx.on('skills/change', refreshCommandList);
|
|
2185
|
+
/**
|
|
2186
|
+
* The view a skill-catalog read must be taken through, as ONE value.
|
|
2187
|
+
*
|
|
2188
|
+
* The registry is host-plane but scope-LAYERED: a provider mounted by an
|
|
2189
|
+
* agent preset's standing composition files into that preset's layer, and a
|
|
2190
|
+
* read taken without the scope sees only the host layer. Passing the pair
|
|
2191
|
+
* together keeps a read from being taken half-scoped.
|
|
2192
|
+
*
|
|
2193
|
+
* @param target - the agent whose view is wanted.
|
|
2194
|
+
*/
|
|
2195
|
+
const skillViewOptions = (target) => ({
|
|
2196
|
+
scope: target,
|
|
2197
|
+
cwd: state.cwd,
|
|
2198
|
+
});
|
|
2199
|
+
/** The skill registry as the given agent sees it, or undefined when a boot
|
|
2200
|
+
* mounts none. `serviceForAgent` resolves through the agent's mount and
|
|
2201
|
+
* falls back to the host context. */
|
|
2202
|
+
const skillRegistryFor = (target) => serviceForAgent(ctx, target, 'skills');
|
|
2203
|
+
/**
|
|
2204
|
+
* Skill commands this channel owns, by skill name. The value keeps the
|
|
2205
|
+
* description the command was registered with so an edited SKILL.md
|
|
2206
|
+
* re-registers instead of leaving a stale menu entry.
|
|
2207
|
+
*/
|
|
2208
|
+
const skillCommands = new Map();
|
|
2209
|
+
/** Skill names the registry refused (name taken, or invalid) — warn once. */
|
|
2210
|
+
const skillCommandsRefused = new Set();
|
|
2211
|
+
/** Pending re-read after an incomplete catalog observation. */
|
|
2212
|
+
let skillCommandsRetry;
|
|
2213
|
+
/**
|
|
2214
|
+
* Publish every user-invocable skill as a slash command (issue #86).
|
|
2215
|
+
*
|
|
2216
|
+
* The completion menu already lists these skills, but a menu entry is not a
|
|
2217
|
+
* command: nothing dispatches it, so typing the name and pressing Enter does
|
|
2218
|
+
* nothing. Registering through the host command registry is what makes them
|
|
2219
|
+
* runnable, and buys three things the TUI would otherwise reimplement:
|
|
2220
|
+
* `register` emits `commands/change`, so the menu merge folds the entry in
|
|
2221
|
+
* on its own; Enter dispatches through the normal command path, so the
|
|
2222
|
+
* invocation is logged as a paired `command/run`/`command/done` like every
|
|
2223
|
+
* other command; and the handler runs host-side, so invoking a skill is
|
|
2224
|
+
* DETERMINISTIC — the body is injected here, instead of sending `/name` to
|
|
2225
|
+
* the model and depending on it to recognize the text and reach for its
|
|
2226
|
+
* skill loader.
|
|
2227
|
+
*
|
|
2228
|
+
* `userInvocable` covers "human-facing command catalogs AND loaders", so
|
|
2229
|
+
* discovery alone would honor half the flag.
|
|
2230
|
+
*/
|
|
2231
|
+
const refreshSkillCommands = async () => {
|
|
2232
|
+
if (commandService === undefined)
|
|
2233
|
+
return;
|
|
2234
|
+
const target = agent;
|
|
2235
|
+
const registry = skillRegistryFor(target);
|
|
2236
|
+
if (registry === undefined)
|
|
2237
|
+
return;
|
|
2238
|
+
let observation;
|
|
2239
|
+
try {
|
|
2240
|
+
observation = await registry.snapshot(skillViewOptions(target));
|
|
2241
|
+
}
|
|
2242
|
+
catch (error) {
|
|
2243
|
+
ctx.logger.warn('skill commands: catalog read failed: %o', error);
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
2246
|
+
if (target !== agent)
|
|
2247
|
+
return;
|
|
2248
|
+
// A provider still warming its watcher reports an incomplete observation;
|
|
2249
|
+
// re-read once so a cold start cannot leave the menu permanently short.
|
|
2250
|
+
if (!observation.complete && skillCommandsRetry === undefined) {
|
|
2251
|
+
skillCommandsRetry = setTimeout(() => {
|
|
2252
|
+
skillCommandsRetry = undefined;
|
|
2253
|
+
void refreshSkillCommands();
|
|
2254
|
+
}, SKILL_COMMAND_RETRY_MS);
|
|
2255
|
+
}
|
|
2256
|
+
const wanted = new Map(observation.skills
|
|
2257
|
+
.filter(skill => isUserInvocable(skill))
|
|
2258
|
+
// A name the TUI's own command grammar cannot parse would show in the
|
|
2259
|
+
// menu and then fail to dispatch when typed; ask the real parser
|
|
2260
|
+
// instead of restating its pattern here.
|
|
2261
|
+
.filter(skill => parseCommandName(`/${skill.name}`)?.name === skill.name)
|
|
2262
|
+
// Built-in locals win a name collision, exactly as they do over
|
|
2263
|
+
// plugin-registered commands in refreshCommandList.
|
|
2264
|
+
.filter(skill => !isLocalCommandName(skill.name))
|
|
2265
|
+
.map(skill => [skill.name, skill.description]));
|
|
2266
|
+
for (const [name, entry] of skillCommands) {
|
|
2267
|
+
if (wanted.get(name) === entry.description)
|
|
2268
|
+
continue;
|
|
2269
|
+
entry.dispose();
|
|
2270
|
+
skillCommands.delete(name);
|
|
2271
|
+
}
|
|
2272
|
+
for (const [name, description] of wanted) {
|
|
2273
|
+
if (skillCommands.has(name) || skillCommandsRefused.has(name))
|
|
2274
|
+
continue;
|
|
2275
|
+
// Another plugin already owns this name (plan/goal/…): leave it alone.
|
|
2276
|
+
if (commandService.find(target, name) !== undefined)
|
|
2277
|
+
continue;
|
|
2278
|
+
try {
|
|
2279
|
+
const dispose = commandService.register({
|
|
2280
|
+
name,
|
|
2281
|
+
description,
|
|
2282
|
+
// The injected body is the payload; recording the (empty) raw input
|
|
2283
|
+
// would only duplicate the command name into the session log.
|
|
2284
|
+
recordInput: false,
|
|
2285
|
+
handler: async ({ agent: invoker, signal }) => {
|
|
2286
|
+
const view = { ...skillViewOptions(invoker), signal };
|
|
2287
|
+
const skill = await skillRegistryFor(invoker)?.get(name, view);
|
|
2288
|
+
if (skill === undefined || !isUserInvocable(skill)) {
|
|
2289
|
+
return { kind: 'error', text: t('skill-unavailable', { name }) };
|
|
2290
|
+
}
|
|
2291
|
+
// The official user-explicit invocation shape (dsh-skill's
|
|
2292
|
+
// SkillInvocationSource): the rendered body rides as instructions
|
|
2293
|
+
// the model follows, and transcript consumers present it from the
|
|
2294
|
+
// source metadata instead of re-parsing model-facing text.
|
|
2295
|
+
invoker.followup(createUserMessage({
|
|
2296
|
+
content: [{ type: 'text', text: renderSkillContent(skill) }],
|
|
2297
|
+
source: { kind: 'skill-invocation', name, form: 'instructions' },
|
|
2298
|
+
}));
|
|
2299
|
+
// Silent success: the agent visibly starts working on the skill,
|
|
2300
|
+
// which is the feedback (CC shows no banner either).
|
|
2301
|
+
return { kind: 'success' };
|
|
2302
|
+
},
|
|
2303
|
+
});
|
|
2304
|
+
skillCommands.set(name, { dispose, description });
|
|
2305
|
+
}
|
|
2306
|
+
catch (error) {
|
|
2307
|
+
skillCommandsRefused.add(name);
|
|
2308
|
+
ctx.logger.warn(`skill commands: "${name}" not registrable: %o`, error);
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
};
|
|
2312
|
+
ctx.on('skills/change', () => {
|
|
2313
|
+
void refreshSkillCommands();
|
|
2314
|
+
});
|
|
2315
|
+
/** See {@link Channel.releaseContributions}. */
|
|
2316
|
+
const releaseSkillCommands = () => {
|
|
2317
|
+
if (skillCommandsRetry !== undefined)
|
|
2318
|
+
clearTimeout(skillCommandsRetry);
|
|
2319
|
+
skillCommandsRetry = undefined;
|
|
2320
|
+
for (const entry of skillCommands.values())
|
|
2321
|
+
entry.dispose();
|
|
2322
|
+
skillCommands.clear();
|
|
2323
|
+
};
|
|
1589
2324
|
refreshCommandList();
|
|
1590
2325
|
void refreshLoadedContext();
|
|
2326
|
+
void refreshSkillCommands();
|
|
1591
2327
|
let nextRowId = 0;
|
|
1592
2328
|
/** The leaf's bash executor (dsh-bash-local in the example leaf) — the DSH
|
|
1593
2329
|
* execution seam for local `!` commands and the git status breadcrumb. The
|
|
1594
2330
|
* service registers under `ctx.shell` (ShellExecutor; dsh-bash-local and
|
|
1595
2331
|
* dsh-pwsh-local are the providers). */
|
|
1596
2332
|
const bash = ctx.get('shell');
|
|
1597
|
-
/** Claude Code's `!` mode:
|
|
1598
|
-
|
|
2333
|
+
/** Claude Code's `!` mode: execute in the current workspace provider and
|
|
2334
|
+
* render local-only transcript rows (never sent to the model). */
|
|
1599
2335
|
const runLocalCommand = async (command, includeInContext) => {
|
|
1600
|
-
|
|
2336
|
+
const workspace = workspaceService.describe(state.cwd);
|
|
2337
|
+
state.rows.push({
|
|
2338
|
+
id: nextRowId++,
|
|
2339
|
+
kind: 'local',
|
|
2340
|
+
text: command,
|
|
2341
|
+
executionTarget: workspace.kind === 'local' ? workspace.badge : `${workspace.badge} · ${workspace.label}`,
|
|
2342
|
+
});
|
|
1601
2343
|
state.emit();
|
|
1602
2344
|
let output = '(no output)';
|
|
1603
|
-
|
|
2345
|
+
const executionShell = await workspaceService.commandShell(state.cwd) ?? bash;
|
|
2346
|
+
if (executionShell) {
|
|
1604
2347
|
try {
|
|
1605
|
-
const spec =
|
|
2348
|
+
const spec = executionShell.resolve({
|
|
1606
2349
|
command,
|
|
1607
2350
|
workdir: state.cwd,
|
|
1608
2351
|
timeoutMs: 30000,
|
|
1609
2352
|
});
|
|
1610
|
-
const result = await
|
|
2353
|
+
const result = await executionShell.run(spec);
|
|
1611
2354
|
output =
|
|
1612
2355
|
result.stdout.text.trim() ||
|
|
1613
2356
|
result.stderr.text.trim() ||
|
|
@@ -2009,7 +2752,7 @@ ${output}
|
|
|
2009
2752
|
const failure = event.data.error;
|
|
2010
2753
|
if (failure !== undefined) {
|
|
2011
2754
|
card.tool.status = 'error';
|
|
2012
|
-
const errorText =
|
|
2755
|
+
const errorText = toolErrorText(event);
|
|
2013
2756
|
card.tool.errorText = errorText;
|
|
2014
2757
|
state.contextSegments.tools += estimateTokens(errorText);
|
|
2015
2758
|
}
|
|
@@ -2094,7 +2837,7 @@ ${output}
|
|
|
2094
2837
|
state.rows.push({
|
|
2095
2838
|
id: nextRowId,
|
|
2096
2839
|
kind: 'interrupt',
|
|
2097
|
-
text: '
|
|
2840
|
+
text: t('interrupted-by-user') + t('interrupted-ask-next'),
|
|
2098
2841
|
});
|
|
2099
2842
|
nextRowId += 1;
|
|
2100
2843
|
break;
|
|
@@ -2102,7 +2845,7 @@ ${output}
|
|
|
2102
2845
|
const detail = reason.kind === 'error' ? reason.error.message : '';
|
|
2103
2846
|
state.rows.push({ id: nextRowId, kind: 'notice', text: `turn ${reason.kind}${detail ? ` · ${detail}` : ''}` });
|
|
2104
2847
|
nextRowId += 1;
|
|
2105
|
-
state.notify(
|
|
2848
|
+
state.notify(t('turn-failed', { detail: detail ? ` · ${detail}` : '' }), { color: 'error', timeoutMs: 8000 });
|
|
2106
2849
|
break;
|
|
2107
2850
|
}
|
|
2108
2851
|
case 'request/context':
|
|
@@ -2137,7 +2880,7 @@ ${output}
|
|
|
2137
2880
|
// Logged preset switch (blank sessions only, issue #8): a transcript
|
|
2138
2881
|
// marker so a replayed log shows which composition produced the
|
|
2139
2882
|
// turns after it. Not in dsh-session's typed union — matched here by
|
|
2140
|
-
// name, the
|
|
2883
|
+
// name, like the other plugin-defined events above.
|
|
2141
2884
|
if (event.type === 'agent-preset/selected') {
|
|
2142
2885
|
const data = event.data;
|
|
2143
2886
|
state.rows.push({
|
|
@@ -2159,29 +2902,97 @@ ${output}
|
|
|
2159
2902
|
state.working = false;
|
|
2160
2903
|
state.status = agent.status;
|
|
2161
2904
|
state.emit();
|
|
2162
|
-
// Live subscription list, rebound to
|
|
2905
|
+
// Live subscription list and activity timer, rebound to every replacement
|
|
2906
|
+
// agent so no status from the previous session can leak across a swap.
|
|
2163
2907
|
let agentSubscriptions = [];
|
|
2908
|
+
let activityTracker = new ActivityTracker({
|
|
2909
|
+
phrases: true,
|
|
2910
|
+
detailLimit: 40,
|
|
2911
|
+
showIdle: false,
|
|
2912
|
+
});
|
|
2913
|
+
let activityTickTimer;
|
|
2914
|
+
const stopActivityTick = () => {
|
|
2915
|
+
if (activityTickTimer === undefined)
|
|
2916
|
+
return;
|
|
2917
|
+
clearInterval(activityTickTimer);
|
|
2918
|
+
activityTickTimer = undefined;
|
|
2919
|
+
};
|
|
2920
|
+
/** Render the current tracker into the TUI-only projection. */
|
|
2921
|
+
const renderWorkingActivity = () => {
|
|
2922
|
+
if (options.activity === false) {
|
|
2923
|
+
state.workingActivity = undefined;
|
|
2924
|
+
return undefined;
|
|
2925
|
+
}
|
|
2926
|
+
const rendered = activityTracker.render();
|
|
2927
|
+
state.workingActivity = rendered;
|
|
2928
|
+
return rendered;
|
|
2929
|
+
};
|
|
2164
2930
|
const bindAgent = () => {
|
|
2165
2931
|
for (const dispose of agentSubscriptions)
|
|
2166
2932
|
dispose();
|
|
2933
|
+
stopActivityTick();
|
|
2934
|
+
activityTracker = new ActivityTracker({
|
|
2935
|
+
phrases: true,
|
|
2936
|
+
detailLimit: 40,
|
|
2937
|
+
showIdle: false,
|
|
2938
|
+
});
|
|
2939
|
+
activityTracker.onAgentStatus(agent.status);
|
|
2940
|
+
renderWorkingActivity();
|
|
2941
|
+
activityTickTimer = setInterval(() => {
|
|
2942
|
+
const previous = state.workingActivity;
|
|
2943
|
+
const rendered = renderWorkingActivity();
|
|
2944
|
+
if (rendered === undefined)
|
|
2945
|
+
return;
|
|
2946
|
+
// Live phases deliberately wake at 500 ms even when the formatted line
|
|
2947
|
+
// has not crossed its next whole-second boundary: turnElapsedMs remains
|
|
2948
|
+
// a current state value, while line changes cover phrase rotation and
|
|
2949
|
+
// the short-lived completed-tool summary.
|
|
2950
|
+
if (rendered.phase === 'waiting' ||
|
|
2951
|
+
rendered.phase === 'thinking' ||
|
|
2952
|
+
rendered.phase === 'tool' ||
|
|
2953
|
+
previous?.phase !== rendered.phase ||
|
|
2954
|
+
previous.line !== rendered.line) {
|
|
2955
|
+
state.emit();
|
|
2956
|
+
}
|
|
2957
|
+
}, 500);
|
|
2958
|
+
activityTickTimer.unref();
|
|
2167
2959
|
// Re-couple the channel-owned model selection to the new agent's
|
|
2168
2960
|
// assembly/request waterfalls, then re-apply the persisted effort when
|
|
2169
2961
|
// this agent's route offers it (dsh-agent installModelSelection).
|
|
2170
2962
|
selection.current = undefined;
|
|
2171
2963
|
selection.assembled = undefined;
|
|
2964
|
+
// {{model}} backfill (issue #155): a resumed agent's route lives only in
|
|
2965
|
+
// its session's request/header records — agentOptions.model stays
|
|
2966
|
+
// undefined unless cordis.yml pins a COMPLETE provider+model pair — so
|
|
2967
|
+
// the assemble-time persona variable `{{model}}` was registered but
|
|
2968
|
+
// valueless, and dsh-system-prompt's interpolate() throws before any
|
|
2969
|
+
// model call. Seed the selection from the channel's display route (on
|
|
2970
|
+
// resume it already carries the session's recorded route; on create it
|
|
2971
|
+
// matches the route the agent was created with). Per
|
|
2972
|
+
// installModelSelection's contract an absent effort restores the
|
|
2973
|
+
// provider/default behavior, so seeding never pins an effort the route
|
|
2974
|
+
// did not ask for; applyPreferredEffort below still upgrades the seed
|
|
2975
|
+
// when the user has a persisted preference the route offers.
|
|
2976
|
+
if (agent.options?.model === undefined && state.provider !== '' && state.model !== '') {
|
|
2977
|
+
selection.current = { provider: state.provider, model: state.model };
|
|
2978
|
+
}
|
|
2172
2979
|
void applyPreferredEffort();
|
|
2980
|
+
refreshMode();
|
|
2173
2981
|
agentSubscriptions = [
|
|
2174
2982
|
installModelSelection(agent.ctx, selection),
|
|
2175
2983
|
ctx.on('agent/status', ({ agent: subject, status }) => {
|
|
2176
2984
|
if (subject !== agent)
|
|
2177
2985
|
return;
|
|
2178
2986
|
state.status = status;
|
|
2987
|
+
activityTracker.onAgentStatus(status);
|
|
2988
|
+
renderWorkingActivity();
|
|
2179
2989
|
state.emit();
|
|
2180
2990
|
}),
|
|
2181
2991
|
ctx.on('agent/disposed', ({ agent: subject }) => {
|
|
2182
2992
|
if (subject !== agent)
|
|
2183
2993
|
return;
|
|
2184
2994
|
state.status = 'disposed';
|
|
2995
|
+
stopActivityTick();
|
|
2185
2996
|
state.emit();
|
|
2186
2997
|
}),
|
|
2187
2998
|
// Pending delivery is driven by the agent inbox: a claimed message
|
|
@@ -2215,24 +3026,13 @@ ${output}
|
|
|
2215
3026
|
ctx.on('session/event', (session, event) => {
|
|
2216
3027
|
if (session !== agent.session)
|
|
2217
3028
|
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;
|
|
3029
|
+
activityTracker.onSessionEvent(event);
|
|
3030
|
+
renderWorkingActivity();
|
|
3031
|
+
// Mode-affecting atoms fold into the Shift+Tab mode indicator the
|
|
3032
|
+
// moment they land (whether appended by cycleMode or by hand).
|
|
3033
|
+
const eventType = event.type;
|
|
3034
|
+
if (eventType === 'plan/mode' || eventType === 'sandbox/mode' || eventType === 'approval/policy') {
|
|
3035
|
+
refreshMode();
|
|
2236
3036
|
}
|
|
2237
3037
|
renderEvent(event);
|
|
2238
3038
|
// Streaming deltas (one event per token) take the frame-aligned
|
|
@@ -2245,15 +3045,30 @@ ${output}
|
|
|
2245
3045
|
];
|
|
2246
3046
|
};
|
|
2247
3047
|
bindAgent();
|
|
3048
|
+
// Cordis owns the Channel lifetime. Rebinding handles the common case;
|
|
3049
|
+
// this effect closes the final timer when the Channel's context unloads.
|
|
3050
|
+
const effect = ctx.effect;
|
|
3051
|
+
effect?.call(ctx, () => () => { stopActivityTick(); }, 'dsh-tui activity timer');
|
|
2248
3052
|
// Statusline breadcrumb: current git branch of the session cwd (best-effort).
|
|
2249
|
-
|
|
3053
|
+
// Re-run when an agent swap adopts a different persisted cwd (/resume,
|
|
3054
|
+
// issue #96) so the breadcrumb never shows the previous workspace's branch.
|
|
3055
|
+
const refreshGitBranch = () => {
|
|
3056
|
+
state.gitBranch = undefined;
|
|
3057
|
+
if (!bash)
|
|
3058
|
+
return;
|
|
3059
|
+
// Capture the requested cwd: a /resume landing while this query is in
|
|
3060
|
+
// flight refreshes the branch for the NEW cwd, so a late reply from the
|
|
3061
|
+
// old workspace must be dropped (statusline staleness, issue #96 review).
|
|
3062
|
+
const requestedCwd = state.cwd;
|
|
2250
3063
|
void bash
|
|
2251
3064
|
.run(bash.resolve({
|
|
2252
3065
|
command: 'git branch --show-current',
|
|
2253
|
-
workdir:
|
|
3066
|
+
workdir: requestedCwd,
|
|
2254
3067
|
timeoutMs: 3000,
|
|
2255
3068
|
}))
|
|
2256
3069
|
.then((result) => {
|
|
3070
|
+
if (state.cwd !== requestedCwd)
|
|
3071
|
+
return;
|
|
2257
3072
|
const branch = result.stdout.text.trim();
|
|
2258
3073
|
if (branch !== '') {
|
|
2259
3074
|
state.gitBranch = branch;
|
|
@@ -2265,7 +3080,8 @@ ${output}
|
|
|
2265
3080
|
// backend may be unavailable (no confinement yet) or the cwd may
|
|
2266
3081
|
// not be a git repo. Either way the statusline simply stays blank.
|
|
2267
3082
|
});
|
|
2268
|
-
}
|
|
3083
|
+
};
|
|
3084
|
+
refreshGitBranch();
|
|
2269
3085
|
return state;
|
|
2270
3086
|
}
|
|
2271
3087
|
/** Path basename for the resume-list title (`C:/a/b` → `b`). */
|
|
@@ -2273,6 +3089,56 @@ function basename(path) {
|
|
|
2273
3089
|
const parts = path.split(/[\\/]/);
|
|
2274
3090
|
return parts[parts.length - 1] ?? path;
|
|
2275
3091
|
}
|
|
3092
|
+
/** Normalize a cwd for comparison: forward slashes, no trailing slash; case
|
|
3093
|
+
* folded when the platform's filesystem semantics are case-insensitive. */
|
|
3094
|
+
function normalizeCwd(path, caseInsensitive) {
|
|
3095
|
+
const normalized = path.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
3096
|
+
return caseInsensitive ? normalized.toLowerCase() : normalized;
|
|
3097
|
+
}
|
|
3098
|
+
/**
|
|
3099
|
+
* `/resume` project filter (issue #96): exact cwd match, PLUS sessions
|
|
3100
|
+
* recorded in a subdirectory — pre-upgrade launches recorded the launch
|
|
3101
|
+
* subdirectory as the header cwd, and with the cwd default now resolving to
|
|
3102
|
+
* the git worktree root an exact match would hide those sessions forever.
|
|
3103
|
+
* They belong to the same workspace, so they stay listed. Comparison follows
|
|
3104
|
+
* the platform's filesystem semantics (case-insensitive on Windows — a
|
|
3105
|
+
* pre-upgrade header may record `C:\Repo` where the current launch resolves
|
|
3106
|
+
* `c:\repo`). `caseInsensitive` is a parameter (not a platform read) so the
|
|
3107
|
+
* verifier can exercise both modes on any host. Exported for
|
|
3108
|
+
* scripts/verify-session-cwd.mjs.
|
|
3109
|
+
*
|
|
3110
|
+
* Boundary rule (issue #153): container directories are nobody's workspace.
|
|
3111
|
+
* $HOME and the Windows root forms — plain drive roots (`C:`), UNC share
|
|
3112
|
+
* roots (`//server/share`), and extended-length roots (`//?/C:`,
|
|
3113
|
+
* `//?/UNC/server/share`) — are ancestors of unrelated projects, so the
|
|
3114
|
+
* descendant rules below would list every session on the machine from `~`
|
|
3115
|
+
* (and every session on the drive/share from those roots). At these
|
|
3116
|
+
* boundaries, in either direction, only an exact match passes.
|
|
3117
|
+
*/
|
|
3118
|
+
export function sessionCwdMatches(stateCwd, headerCwd, caseInsensitive = process.platform === 'win32') {
|
|
3119
|
+
const cwd = normalizeCwd(stateCwd, caseInsensitive);
|
|
3120
|
+
const recorded = normalizeCwd(headerCwd, caseInsensitive);
|
|
3121
|
+
if (recorded === '' || cwd === '')
|
|
3122
|
+
return false;
|
|
3123
|
+
const home = normalizeCwd(homeDir(), caseInsensitive);
|
|
3124
|
+
// Paths below arrive backslash-normalized (`\\server\share` →
|
|
3125
|
+
// `//server/share`, `\\?\C:\` → `//?/C:`), trailing slashes stripped.
|
|
3126
|
+
const isContainer = (path) => (home !== '' && path === home) ||
|
|
3127
|
+
/^[a-z]:$/i.test(path) || // drive root: C:
|
|
3128
|
+
/^\/\/[^/]+\/[^/]+$/.test(path) || // UNC share root: //server/share
|
|
3129
|
+
/^\/\/\?\/[a-z]:$/i.test(path) || // extended drive root: //?/C:
|
|
3130
|
+
/^\/\/\?\/unc\/[^/]+\/[^/]+$/i.test(path); // extended UNC root: //?/UNC/server/share
|
|
3131
|
+
if (isContainer(cwd) || isContainer(recorded))
|
|
3132
|
+
return recorded === cwd;
|
|
3133
|
+
return (recorded === cwd ||
|
|
3134
|
+
// Pre-upgrade subdirectory session of this workspace.
|
|
3135
|
+
recorded.startsWith(`${cwd}/`) ||
|
|
3136
|
+
// Resumed INTO a pre-upgrade subdirectory session (state.cwd adopted its
|
|
3137
|
+
// recorded subdirectory): the workspace-root sessions it belongs with
|
|
3138
|
+
// must stay visible, or /resume looks like it lost them for the rest of
|
|
3139
|
+
// the process lifetime (review leftover).
|
|
3140
|
+
cwd.startsWith(`${recorded}/`));
|
|
3141
|
+
}
|
|
2276
3142
|
/** Context-bar token estimate (pi-nano-context: ~4 chars per token). */
|
|
2277
3143
|
function estimateTokens(text) {
|
|
2278
3144
|
return Math.ceil(text.length / 4);
|
|
@@ -2355,84 +3221,146 @@ const MENTION_MAX_DIR_ENTRIES = 200;
|
|
|
2355
3221
|
function mentionFs(ctx) {
|
|
2356
3222
|
return ctx.get('fs');
|
|
2357
3223
|
}
|
|
3224
|
+
function mentionAttachments(ctx) {
|
|
3225
|
+
return ctx.get('attachments');
|
|
3226
|
+
}
|
|
3227
|
+
const MENTION_IMAGE_MEDIA_TYPES = {
|
|
3228
|
+
'.png': 'image/png',
|
|
3229
|
+
'.jpg': 'image/jpeg',
|
|
3230
|
+
'.jpeg': 'image/jpeg',
|
|
3231
|
+
'.webp': 'image/webp',
|
|
3232
|
+
'.gif': 'image/gif',
|
|
3233
|
+
};
|
|
3234
|
+
function mentionImageMediaType(path) {
|
|
3235
|
+
return MENTION_IMAGE_MEDIA_TYPES[extname(path).toLowerCase()];
|
|
3236
|
+
}
|
|
2358
3237
|
/**
|
|
2359
3238
|
* Expand a submitted text's `@` mentions (issue #15) into model-facing
|
|
2360
|
-
* attachment blocks:
|
|
2361
|
-
*
|
|
2362
|
-
*
|
|
2363
|
-
*
|
|
2364
|
-
*
|
|
3239
|
+
* attachment blocks: supported image files become durable image blocks,
|
|
3240
|
+
* other files contribute capped text, and directories contribute a shallow
|
|
3241
|
+
* listing. Reads always go through the active fs service, so provider-owned
|
|
3242
|
+
* workspaces keep their routing semantics. The typed text stays first and
|
|
3243
|
+
* verbatim. Best-effort failures degrade to `missing`, never a failed send.
|
|
2365
3244
|
*/
|
|
2366
|
-
export async function expandMentions(fs, cwd, text) {
|
|
3245
|
+
export async function expandMentions(fs, cwd, text, attachments, stagedImages) {
|
|
2367
3246
|
const blocks = [{ type: 'text', text }];
|
|
2368
3247
|
const attached = [];
|
|
2369
3248
|
const missing = [];
|
|
2370
3249
|
const mentions = extractMentions(text);
|
|
2371
|
-
if (!fs || mentions.length === 0)
|
|
2372
|
-
return { blocks, attached, missing };
|
|
2373
3250
|
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') {
|
|
3251
|
+
let imageCount = 0;
|
|
3252
|
+
let imageBytes = 0;
|
|
3253
|
+
if (fs !== undefined) {
|
|
3254
|
+
for (const mention of mentions) {
|
|
3255
|
+
const imageMediaType = mentionImageMediaType(mention.path);
|
|
3256
|
+
if (budget <= 0 && imageMediaType === undefined)
|
|
3257
|
+
break;
|
|
3258
|
+
// Mentions resolve against the session cwd, same as the model-facing fs
|
|
3259
|
+
// tools; absolute paths pass through untouched.
|
|
3260
|
+
const absolute = isAbsolute(mention.path) ? mention.path : join(cwd, mention.path);
|
|
3261
|
+
let target;
|
|
3262
|
+
let info;
|
|
2391
3263
|
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);
|
|
3264
|
+
target = await fs.resolve(absolute);
|
|
3265
|
+
info = await fs.stat(target);
|
|
2405
3266
|
}
|
|
2406
3267
|
catch {
|
|
2407
|
-
// Binary/undecodable or unreadable — report it like a miss.
|
|
2408
3268
|
missing.push(mention.path);
|
|
3269
|
+
continue;
|
|
2409
3270
|
}
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
3271
|
+
if (info?.type === 'file') {
|
|
3272
|
+
if (imageMediaType !== undefined && attachments !== undefined && fs.readBytes !== undefined) {
|
|
3273
|
+
const limits = attachments.imageLimits;
|
|
3274
|
+
if (!limits.mediaTypes.includes(imageMediaType) || imageCount >= limits.maxImagesPerMessage) {
|
|
3275
|
+
missing.push(mention.path);
|
|
3276
|
+
continue;
|
|
3277
|
+
}
|
|
3278
|
+
try {
|
|
3279
|
+
const data = await fs.readBytes(target, undefined, limits.maxImageBytes);
|
|
3280
|
+
if (imageBytes + data.byteLength > limits.maxMessageImageBytes) {
|
|
3281
|
+
missing.push(mention.path);
|
|
3282
|
+
continue;
|
|
3283
|
+
}
|
|
3284
|
+
const attachment = await attachments.saveImage({
|
|
3285
|
+
data,
|
|
3286
|
+
mediaType: imageMediaType,
|
|
3287
|
+
name: basename(target.displayPath),
|
|
3288
|
+
});
|
|
3289
|
+
blocks.push({ type: 'image', attachment });
|
|
3290
|
+
imageCount += 1;
|
|
3291
|
+
imageBytes += data.byteLength;
|
|
3292
|
+
attached.push(mention.path);
|
|
3293
|
+
}
|
|
3294
|
+
catch {
|
|
3295
|
+
missing.push(mention.path);
|
|
3296
|
+
}
|
|
3297
|
+
continue;
|
|
2420
3298
|
}
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
3299
|
+
try {
|
|
3300
|
+
const cap = Math.min(MENTION_MAX_FILE_CHARS, budget);
|
|
3301
|
+
let content = await fs.readText(target);
|
|
3302
|
+
let truncated = false;
|
|
3303
|
+
if (content.length > cap) {
|
|
3304
|
+
content = content.slice(0, cap);
|
|
3305
|
+
truncated = true;
|
|
3306
|
+
}
|
|
3307
|
+
budget -= content.length;
|
|
3308
|
+
blocks.push({
|
|
3309
|
+
type: 'text',
|
|
3310
|
+
text: `<attached-file path="${mention.path}">\n${content}${truncated ? '\n[… truncated]' : ''}\n</attached-file>`,
|
|
3311
|
+
});
|
|
3312
|
+
attached.push(mention.path);
|
|
3313
|
+
}
|
|
3314
|
+
catch {
|
|
3315
|
+
// Binary/undecodable or unreadable — report it like a miss.
|
|
3316
|
+
missing.push(mention.path);
|
|
3317
|
+
}
|
|
3318
|
+
continue;
|
|
2428
3319
|
}
|
|
2429
|
-
|
|
2430
|
-
|
|
3320
|
+
if (info?.type === 'directory') {
|
|
3321
|
+
try {
|
|
3322
|
+
const entries = await fs.listDir(target);
|
|
3323
|
+
const listing = entries
|
|
3324
|
+
.slice(0, MENTION_MAX_DIR_ENTRIES)
|
|
3325
|
+
.map(entry => (entry.type === 'directory' ? `${entry.name}/` : entry.name));
|
|
3326
|
+
if (entries.length > MENTION_MAX_DIR_ENTRIES) {
|
|
3327
|
+
listing.push(`… (${entries.length - MENTION_MAX_DIR_ENTRIES} more)`);
|
|
3328
|
+
}
|
|
3329
|
+
const body = listing.join('\n');
|
|
3330
|
+
budget -= body.length;
|
|
3331
|
+
blocks.push({
|
|
3332
|
+
type: 'text',
|
|
3333
|
+
text: `<attached-directory path="${mention.path}">\n${body}\n</attached-directory>`,
|
|
3334
|
+
});
|
|
3335
|
+
attached.push(mention.path);
|
|
3336
|
+
}
|
|
3337
|
+
catch {
|
|
3338
|
+
missing.push(mention.path);
|
|
3339
|
+
}
|
|
3340
|
+
continue;
|
|
2431
3341
|
}
|
|
2432
|
-
|
|
3342
|
+
// Absent (stat → undefined) or a special file.
|
|
3343
|
+
missing.push(mention.path);
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
if (attachments !== undefined && stagedImages !== undefined) {
|
|
3347
|
+
const limits = attachments.imageLimits;
|
|
3348
|
+
for (const [token, attachment] of stagedImages) {
|
|
3349
|
+
if (!text.includes(token))
|
|
3350
|
+
continue;
|
|
3351
|
+
// A referenced-but-dropped staged image must be loud: silently sending
|
|
3352
|
+
// the bare token would leave the user believing the image reached the
|
|
3353
|
+
// model. Reuse the missing-mention warning channel.
|
|
3354
|
+
if (imageCount >= limits.maxImagesPerMessage
|
|
3355
|
+
|| imageBytes + attachment.bytes > limits.maxMessageImageBytes
|
|
3356
|
+
|| !limits.mediaTypes.includes(attachment.mediaType)) {
|
|
3357
|
+
missing.push(token);
|
|
3358
|
+
continue;
|
|
3359
|
+
}
|
|
3360
|
+
blocks.push({ type: 'image', attachment });
|
|
3361
|
+
imageCount += 1;
|
|
3362
|
+
imageBytes += attachment.bytes;
|
|
2433
3363
|
}
|
|
2434
|
-
// Absent (stat → undefined) or a special file.
|
|
2435
|
-
missing.push(mention.path);
|
|
2436
3364
|
}
|
|
2437
3365
|
return { blocks, attached, missing };
|
|
2438
3366
|
}
|