@deepseek-harness-tui/dsh-tui 0.8.8 → 0.9.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 +60 -238
- package/bin/dsh-tui.js +172 -5
- package/cordis.patch.yml +16 -4
- package/dsh-ecosystem-spec/protocols/profile-definitions.d.ts +13 -13
- package/dsh-ecosystem-spec/protocols/profile-definitions.js +59 -59
- package/dsh-ecosystem-spec/protocols/tui-channel-http.d.ts +15 -15
- package/dsh-ecosystem-spec/protocols/tui-channel-http.js +32 -32
- package/dsh-ecosystem-spec/protocols/tui-channel.d.ts +59 -59
- package/dsh-ecosystem-spec/protocols/tui-channel.js +166 -166
- package/dsh-ecosystem-spec/protocols/tui-contributions.d.ts +42 -42
- package/dsh-ecosystem-spec/protocols/tui-contributions.js +143 -143
- package/dsh-ecosystem-spec/registry/README.md +14 -14
- package/dsh-ecosystem-spec/registry/host-descriptor.tui.example.json +56 -56
- package/dsh-ecosystem-spec/registry/permissions-0.1.json +13 -13
- package/dsh-ecosystem-spec/registry/registry-0.15.json +149 -149
- package/lib/types/cc/hyperlink.d.ts +7 -1
- package/lib/types/cc/hyperlink.d.ts.map +1 -1
- package/lib/types/cc/hyperlink.js +3 -2
- package/lib/types/cc/markdown.d.ts.map +1 -1
- package/lib/types/cc/markdown.js +33 -3
- package/lib/types/cc/sessionColors.d.ts +27 -0
- package/lib/types/cc/sessionColors.d.ts.map +1 -0
- package/lib/types/cc/sessionColors.js +39 -0
- package/lib/types/commands.d.ts.map +1 -1
- package/lib/types/commands.js +10 -2
- package/lib/types/components/ActivityPicker.d.ts +4 -1
- package/lib/types/components/ActivityPicker.d.ts.map +1 -1
- package/lib/types/components/ActivityPicker.js +2 -2
- package/lib/types/components/AutoRecapRow.d.ts +23 -0
- package/lib/types/components/AutoRecapRow.d.ts.map +1 -0
- package/lib/types/components/AutoRecapRow.js +23 -0
- package/lib/types/components/BtwPanel.d.ts.map +1 -1
- package/lib/types/components/BtwPanel.js +1 -1
- package/lib/types/components/Chat/SubagentMessage.d.ts +2 -1
- package/lib/types/components/Chat/SubagentMessage.d.ts.map +1 -1
- package/lib/types/components/Chat/SubagentMessage.js +11 -3
- package/lib/types/components/ColorPicker.d.ts +17 -0
- package/lib/types/components/ColorPicker.d.ts.map +1 -0
- package/lib/types/components/ColorPicker.js +20 -0
- package/lib/types/components/CommandSuggestions.d.ts +23 -4
- package/lib/types/components/CommandSuggestions.d.ts.map +1 -1
- package/lib/types/components/CommandSuggestions.js +41 -14
- package/lib/types/components/ContextBarView.d.ts +33 -0
- package/lib/types/components/ContextBarView.d.ts.map +1 -0
- package/lib/types/components/ContextBarView.js +46 -0
- package/lib/types/components/EffortInputBorder.d.ts +12 -1
- package/lib/types/components/EffortInputBorder.d.ts.map +1 -1
- package/lib/types/components/EffortInputBorder.js +22 -5
- package/lib/types/components/EffortSlider.d.ts +4 -1
- package/lib/types/components/EffortSlider.d.ts.map +1 -1
- package/lib/types/components/EffortSlider.js +4 -2
- package/lib/types/components/ExtensionDialog.js +6 -2
- package/lib/types/components/FileActionsPanel.d.ts +22 -0
- package/lib/types/components/FileActionsPanel.d.ts.map +1 -0
- package/lib/types/components/FileActionsPanel.js +28 -0
- package/lib/types/components/FileSuggestions.d.ts +26 -7
- package/lib/types/components/FileSuggestions.d.ts.map +1 -1
- package/lib/types/components/FileSuggestions.js +39 -12
- package/lib/types/components/GoalTodoPanel.d.ts +26 -2
- package/lib/types/components/GoalTodoPanel.d.ts.map +1 -1
- package/lib/types/components/GoalTodoPanel.js +86 -17
- package/lib/types/components/HelpMenu.d.ts +7 -1
- package/lib/types/components/HelpMenu.d.ts.map +1 -1
- package/lib/types/components/HelpMenu.js +12 -4
- package/lib/types/components/HistorySearchDialog.d.ts +4 -1
- package/lib/types/components/HistorySearchDialog.d.ts.map +1 -1
- package/lib/types/components/HistorySearchDialog.js +2 -2
- package/lib/types/components/LangPicker.d.ts +14 -0
- package/lib/types/components/LangPicker.d.ts.map +1 -0
- package/lib/types/components/LangPicker.js +17 -0
- package/lib/types/components/LoadedContextPanel.d.ts.map +1 -1
- package/lib/types/components/LoadedContextPanel.js +3 -1
- package/lib/types/components/Markdown.d.ts.map +1 -1
- package/lib/types/components/Markdown.js +12 -3
- package/lib/types/components/MessageList.d.ts +45 -2
- package/lib/types/components/MessageList.d.ts.map +1 -1
- package/lib/types/components/MessageList.js +560 -41
- package/lib/types/components/ModelPicker.d.ts +27 -2
- package/lib/types/components/ModelPicker.d.ts.map +1 -1
- package/lib/types/components/ModelPicker.js +26 -6
- package/lib/types/components/PermissionsPicker.d.ts +21 -0
- package/lib/types/components/PermissionsPicker.d.ts.map +1 -0
- package/lib/types/components/PermissionsPicker.js +27 -0
- package/lib/types/components/PlanPicker.d.ts +15 -0
- package/lib/types/components/PlanPicker.d.ts.map +1 -0
- package/lib/types/components/PlanPicker.js +18 -0
- package/lib/types/components/PresetPicker.d.ts +4 -1
- package/lib/types/components/PresetPicker.d.ts.map +1 -1
- package/lib/types/components/PresetPicker.js +2 -2
- package/lib/types/components/PromptInput.d.ts +4 -3
- package/lib/types/components/PromptInput.d.ts.map +1 -1
- package/lib/types/components/PromptInput.js +602 -75
- package/lib/types/components/RecapPanel.d.ts +25 -0
- package/lib/types/components/RecapPanel.d.ts.map +1 -0
- package/lib/types/components/RecapPanel.js +55 -0
- package/lib/types/components/RewindPicker.d.ts +12 -1
- package/lib/types/components/RewindPicker.d.ts.map +1 -1
- package/lib/types/components/RewindPicker.js +6 -4
- package/lib/types/components/ScrollbarGutter.d.ts +28 -0
- package/lib/types/components/ScrollbarGutter.d.ts.map +1 -0
- package/lib/types/components/ScrollbarGutter.js +119 -0
- package/lib/types/components/Select.d.ts +8 -1
- package/lib/types/components/Select.d.ts.map +1 -1
- package/lib/types/components/Select.js +4 -2
- package/lib/types/components/SkillsPicker.d.ts +4 -1
- package/lib/types/components/SkillsPicker.d.ts.map +1 -1
- package/lib/types/components/SkillsPicker.js +2 -2
- package/lib/types/components/SubagentCard.d.ts +2 -1
- package/lib/types/components/SubagentCard.d.ts.map +1 -1
- package/lib/types/components/SubagentCard.js +9 -2
- package/lib/types/components/SubagentDashboard.d.ts +4 -0
- package/lib/types/components/SubagentDashboard.d.ts.map +1 -1
- package/lib/types/components/SubagentDashboard.js +9 -1
- package/lib/types/components/SubagentDetailScene.d.ts.map +1 -1
- package/lib/types/components/SubagentDetailScene.js +5 -4
- package/lib/types/components/SuggestionCard.d.ts +65 -0
- package/lib/types/components/SuggestionCard.d.ts.map +1 -0
- package/lib/types/components/SuggestionCard.js +77 -0
- package/lib/types/components/ThemePicker.d.ts +4 -1
- package/lib/types/components/ThemePicker.d.ts.map +1 -1
- package/lib/types/components/ThemePicker.js +2 -2
- package/lib/types/components/ThinkingToggle.d.ts +4 -1
- package/lib/types/components/ThinkingToggle.d.ts.map +1 -1
- package/lib/types/components/ThinkingToggle.js +2 -2
- package/lib/types/components/TimelineRail.d.ts +53 -0
- package/lib/types/components/TimelineRail.d.ts.map +1 -0
- package/lib/types/components/TimelineRail.js +197 -0
- package/lib/types/components/WorkspaceFlowPicker.d.ts +6 -1
- package/lib/types/components/WorkspaceFlowPicker.d.ts.map +1 -1
- package/lib/types/components/WorkspaceFlowPicker.js +4 -2
- package/lib/types/components/WorkspaceMenuPicker.d.ts +19 -0
- package/lib/types/components/WorkspaceMenuPicker.d.ts.map +1 -0
- package/lib/types/components/WorkspaceMenuPicker.js +19 -0
- package/lib/types/components/WorkspacePicker.d.ts +4 -1
- package/lib/types/components/WorkspacePicker.d.ts.map +1 -1
- package/lib/types/components/WorkspacePicker.js +2 -2
- package/lib/types/components/approvals/ApprovalPanel.d.ts.map +1 -1
- package/lib/types/components/approvals/ApprovalPanel.js +5 -1
- package/lib/types/components/design-system/ListItem.d.ts +8 -1
- package/lib/types/components/design-system/ListItem.d.ts.map +1 -1
- package/lib/types/components/design-system/ListItem.js +8 -3
- package/lib/types/components/design-system/ThemeProvider.d.ts +7 -0
- package/lib/types/components/design-system/ThemeProvider.d.ts.map +1 -1
- package/lib/types/components/design-system/ThemeProvider.js +2 -1
- package/lib/types/components/messages/AssistantTextMessage.d.ts +5 -2
- package/lib/types/components/messages/AssistantTextMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantTextMessage.js +6 -2
- package/lib/types/components/messages/AssistantThinkingMessage.d.ts +2 -1
- package/lib/types/components/messages/AssistantThinkingMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantThinkingMessage.js +10 -4
- package/lib/types/components/messages/AssistantToolUseMessage.d.ts +15 -1
- package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantToolUseMessage.js +37 -7
- package/lib/types/components/messages/UserPromptMessage.d.ts +2 -1
- package/lib/types/components/messages/UserPromptMessage.d.ts.map +1 -1
- package/lib/types/components/questions/AskUserQuestionPanel.d.ts +7 -3
- package/lib/types/components/questions/AskUserQuestionPanel.d.ts.map +1 -1
- package/lib/types/components/questions/AskUserQuestionPanel.js +75 -11
- package/lib/types/components/questions/PlanReviewPanel.d.ts.map +1 -1
- package/lib/types/components/questions/PlanReviewPanel.js +13 -2
- package/lib/types/components/sessions/SessionListRow.d.ts +4 -1
- package/lib/types/components/sessions/SessionListRow.d.ts.map +1 -1
- package/lib/types/components/sessions/SessionListRow.js +4 -2
- package/lib/types/components/trajectory/HotspotView.d.ts +7 -1
- package/lib/types/components/trajectory/HotspotView.d.ts.map +1 -1
- package/lib/types/components/trajectory/HotspotView.js +8 -2
- package/lib/types/components/trajectory/Ledger.d.ts +9 -1
- package/lib/types/components/trajectory/Ledger.d.ts.map +1 -1
- package/lib/types/components/trajectory/Ledger.js +23 -5
- package/lib/types/components/trajectory/WaveBand.d.ts +10 -1
- package/lib/types/components/trajectory/WaveBand.d.ts.map +1 -1
- package/lib/types/components/trajectory/WaveBand.js +7 -2
- package/lib/types/dsh-adapter/channel.d.ts +117 -7
- package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel.js +1301 -122
- package/lib/types/dsh-adapter/compat/index.d.ts +8 -6
- package/lib/types/dsh-adapter/compat/index.d.ts.map +1 -1
- package/lib/types/dsh-adapter/compat/index.js +8 -6
- package/lib/types/dsh-adapter/compat/sessionLog.d.ts +150 -0
- package/lib/types/dsh-adapter/compat/sessionLog.d.ts.map +1 -1
- package/lib/types/dsh-adapter/compat/sessionLog.js +518 -41
- package/lib/types/dsh-adapter/contract.d.ts +1 -1
- package/lib/types/dsh-adapter/contract.d.ts.map +1 -1
- package/lib/types/dsh-adapter/contract.js +1 -0
- package/lib/types/dsh-adapter/extension-events.d.ts +1 -1
- package/lib/types/dsh-adapter/extension-events.d.ts.map +1 -1
- package/lib/types/dsh-adapter/index.d.ts +20 -2
- package/lib/types/dsh-adapter/index.d.ts.map +1 -1
- package/lib/types/dsh-adapter/index.js +9 -1
- package/lib/types/dsh-adapter/plugin.d.ts +0 -9
- package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
- package/lib/types/dsh-adapter/plugin.js +453 -14
- package/lib/types/dsh-adapter/presets.d.ts +18 -0
- package/lib/types/dsh-adapter/presets.d.ts.map +1 -1
- package/lib/types/dsh-adapter/presets.js +31 -0
- package/lib/types/dsh-adapter/providerWizard.d.ts +29 -1
- package/lib/types/dsh-adapter/providerWizard.d.ts.map +1 -1
- package/lib/types/dsh-adapter/providerWizard.js +89 -5
- package/lib/types/dsh-adapter/questions.d.ts +19 -2
- package/lib/types/dsh-adapter/questions.d.ts.map +1 -1
- package/lib/types/dsh-adapter/questions.js +53 -10
- package/lib/types/dsh-adapter/recap.d.ts +49 -0
- package/lib/types/dsh-adapter/recap.d.ts.map +1 -0
- package/lib/types/dsh-adapter/recap.js +121 -0
- package/lib/types/dsh-adapter/sessionTree.d.ts +314 -0
- package/lib/types/dsh-adapter/sessionTree.d.ts.map +1 -0
- package/lib/types/dsh-adapter/sessionTree.js +954 -0
- package/lib/types/dsh-adapter/shortcuts.d.ts +15 -44
- package/lib/types/dsh-adapter/shortcuts.d.ts.map +1 -1
- package/lib/types/dsh-adapter/shortcuts.js +28 -165
- package/lib/types/dsh-adapter/types.d.ts +1 -1
- package/lib/types/dsh-adapter/types.d.ts.map +1 -1
- package/lib/types/herdr.js +1 -1
- package/lib/types/i18n.d.ts +770 -181
- package/lib/types/i18n.d.ts.map +1 -1
- package/lib/types/i18n.js +257 -80
- package/lib/types/ink/components/AlternateScreen.d.ts.map +1 -1
- package/lib/types/ink/components/AlternateScreen.js +13 -1
- package/lib/types/ink/components/App.d.ts +12 -1
- package/lib/types/ink/components/App.d.ts.map +1 -1
- package/lib/types/ink/components/App.js +101 -4
- package/lib/types/ink/components/ClockContext.d.ts.map +1 -1
- package/lib/types/ink/components/ClockContext.js +2 -0
- package/lib/types/ink/components/ScrollBox.d.ts.map +1 -1
- package/lib/types/ink/components/ScrollBox.js +170 -91
- package/lib/types/ink/constants.d.ts +9 -0
- package/lib/types/ink/constants.d.ts.map +1 -1
- package/lib/types/ink/constants.js +9 -0
- package/lib/types/ink/events/click-event.d.ts +7 -16
- package/lib/types/ink/events/click-event.d.ts.map +1 -1
- package/lib/types/ink/events/click-event.js +16 -19
- package/lib/types/ink/events/event-handlers.d.ts +11 -1
- package/lib/types/ink/events/event-handlers.d.ts.map +1 -1
- package/lib/types/ink/events/event-handlers.js +2 -0
- package/lib/types/ink/events/input-event.d.ts +9 -0
- package/lib/types/ink/events/input-event.d.ts.map +1 -1
- package/lib/types/ink/events/input-event.js +4 -0
- package/lib/types/ink/events/pointer-event.d.ts +61 -0
- package/lib/types/ink/events/pointer-event.d.ts.map +1 -0
- package/lib/types/ink/events/pointer-event.js +61 -0
- package/lib/types/ink/events/wheel-event.d.ts +17 -0
- package/lib/types/ink/events/wheel-event.d.ts.map +1 -0
- package/lib/types/ink/events/wheel-event.js +20 -0
- package/lib/types/ink/geometry-trace.d.ts +51 -0
- package/lib/types/ink/geometry-trace.d.ts.map +1 -0
- package/lib/types/ink/geometry-trace.js +83 -0
- package/lib/types/ink/hit-test.d.ts +56 -1
- package/lib/types/ink/hit-test.d.ts.map +1 -1
- package/lib/types/ink/hit-test.js +146 -8
- package/lib/types/ink/ink.d.ts +58 -1
- package/lib/types/ink/ink.d.ts.map +1 -1
- package/lib/types/ink/ink.js +230 -29
- package/lib/types/ink/log-update.d.ts.map +1 -1
- package/lib/types/ink/log-update.js +21 -0
- package/lib/types/ink/node-cache.d.ts +5 -0
- package/lib/types/ink/node-cache.d.ts.map +1 -1
- package/lib/types/ink/parse-keypress.d.ts +8 -0
- package/lib/types/ink/parse-keypress.d.ts.map +1 -1
- package/lib/types/ink/parse-keypress.js +101 -12
- package/lib/types/ink/render-node-to-output.d.ts +10 -0
- package/lib/types/ink/render-node-to-output.d.ts.map +1 -1
- package/lib/types/ink/render-node-to-output.js +111 -18
- package/lib/types/ink/timeline-rail.d.ts +138 -0
- package/lib/types/ink/timeline-rail.d.ts.map +1 -0
- package/lib/types/ink/timeline-rail.js +179 -0
- package/lib/types/ink/wrap-text.d.ts.map +1 -1
- package/lib/types/ink/wrap-text.js +47 -5
- package/lib/types/migrationPrefs.d.ts +60 -0
- package/lib/types/migrationPrefs.d.ts.map +1 -0
- package/lib/types/migrationPrefs.js +97 -0
- package/lib/types/modelGroups.d.ts +72 -0
- package/lib/types/modelGroups.d.ts.map +1 -0
- package/lib/types/modelGroups.js +97 -0
- package/lib/types/modelRecents.d.ts +39 -0
- package/lib/types/modelRecents.d.ts.map +1 -0
- package/lib/types/modelRecents.js +97 -0
- package/lib/types/native-ts/yoga-layout/index.d.ts +11 -0
- package/lib/types/native-ts/yoga-layout/index.d.ts.map +1 -1
- package/lib/types/native-ts/yoga-layout/index.js +31 -1
- package/lib/types/oauth.d.ts +15 -0
- package/lib/types/oauth.d.ts.map +1 -0
- package/lib/types/oauth.js +14 -0
- package/lib/types/reload.d.ts +109 -0
- package/lib/types/reload.d.ts.map +1 -0
- package/lib/types/reload.js +118 -0
- package/lib/types/screens/Chat.d.ts +3 -1
- package/lib/types/screens/Chat.d.ts.map +1 -1
- package/lib/types/screens/Chat.js +1377 -371
- package/lib/types/screens/SessionBrowser.d.ts.map +1 -1
- package/lib/types/screens/SessionBrowser.js +56 -29
- package/lib/types/screens/SessionTree.d.ts +27 -0
- package/lib/types/screens/SessionTree.d.ts.map +1 -0
- package/lib/types/screens/SessionTree.js +555 -0
- package/lib/types/screens/Settings.d.ts.map +1 -1
- package/lib/types/screens/Settings.js +64 -22
- package/lib/types/screens/StatusLine.d.ts +0 -8
- package/lib/types/screens/StatusLine.d.ts.map +1 -1
- package/lib/types/screens/StatusLine.js +224 -45
- package/lib/types/screens/StatusMetrics.d.ts +35 -3
- package/lib/types/screens/StatusMetrics.d.ts.map +1 -1
- package/lib/types/screens/StatusMetrics.js +63 -8
- package/lib/types/screens/TrajectoryScene.d.ts.map +1 -1
- package/lib/types/screens/TrajectoryScene.js +81 -14
- package/lib/types/screens/chatOverlay.d.ts +237 -0
- package/lib/types/screens/chatOverlay.d.ts.map +1 -0
- package/lib/types/screens/chatOverlay.js +116 -0
- package/lib/types/tips.d.ts.map +1 -1
- package/lib/types/tips.js +64 -10
- package/lib/types/tuiDisplayPrefs.d.ts +15 -0
- package/lib/types/tuiDisplayPrefs.d.ts.map +1 -1
- package/lib/types/tuiDisplayPrefs.js +9 -0
- package/lib/types/update.d.ts +139 -0
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +382 -30
- package/lib/types/utils/debug.d.ts +10 -0
- package/lib/types/utils/debug.d.ts.map +1 -1
- package/lib/types/utils/debug.js +24 -0
- package/lib/types/utils/fileTarget.d.ts +46 -0
- package/lib/types/utils/fileTarget.d.ts.map +1 -0
- package/lib/types/utils/fileTarget.js +178 -0
- package/lib/types/utils/keymap.d.ts +134 -0
- package/lib/types/utils/keymap.d.ts.map +1 -0
- package/lib/types/utils/keymap.js +311 -0
- package/lib/types/utils/mentions.d.ts +25 -2
- package/lib/types/utils/mentions.d.ts.map +1 -1
- package/lib/types/utils/mentions.js +69 -10
- package/lib/types/utils/openExternal.d.ts +50 -0
- package/lib/types/utils/openExternal.d.ts.map +1 -0
- package/lib/types/utils/openExternal.js +170 -0
- package/lib/types/utils/paths.d.ts.map +1 -1
- package/lib/types/utils/paths.js +1 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/LICENSE +21 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/README.md +140 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/cordis.patch.yml +11 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/dsh-plugin.json +44 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/command.d.ts +14 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/command.js +67 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/credentials.d.ts +64 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/credentials.js +170 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/index.d.ts +79 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/index.js +196 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/interaction.d.ts +68 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/interaction.js +193 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/opener.d.ts +32 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/opener.js +101 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/profiles.d.ts +31 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/profiles.js +68 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/service.d.ts +69 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/service.js +129 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/package.json +70 -0
- package/node_modules/@dsh-std/command/CHANGELOG.md +8 -8
- package/node_modules/@dsh-std/command/LICENSE +21 -21
- package/node_modules/@dsh-std/command/README.md +26 -26
- package/node_modules/@dsh-std/command/README.zh.md +28 -28
- package/node_modules/@dsh-std/command/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/command/package.json +22 -22
- package/node_modules/@dsh-std/connection/CHANGELOG.md +8 -8
- package/node_modules/@dsh-std/connection/LICENSE +21 -21
- package/node_modules/@dsh-std/connection/README.md +42 -42
- package/node_modules/@dsh-std/connection/README.zh.md +42 -42
- package/node_modules/@dsh-std/connection/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/connection/lib/memory.js.map +1 -1
- package/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js.map +1 -1
- package/node_modules/@dsh-std/connection/package.json +20 -20
- package/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/node_modules/@dsh-std/core/LICENSE +21 -21
- package/node_modules/@dsh-std/core/README.md +13 -13
- package/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/core/package.json +21 -21
- package/node_modules/@dsh-std/manifest/CHANGELOG.md +9 -9
- package/node_modules/@dsh-std/manifest/LICENSE +21 -21
- package/node_modules/@dsh-std/manifest/README.md +5 -5
- package/node_modules/@dsh-std/manifest/README.zh.md +5 -5
- package/node_modules/@dsh-std/manifest/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/manifest/package.json +20 -20
- package/node_modules/@dsh-std/manifest/schema/dsh-plugin-0.15.schema.json +144 -144
- package/node_modules/@dsh-std/messages/CHANGELOG.md +8 -8
- package/node_modules/@dsh-std/messages/LICENSE +21 -21
- package/node_modules/@dsh-std/messages/README.md +5 -5
- package/node_modules/@dsh-std/messages/README.zh.md +7 -7
- package/node_modules/@dsh-std/messages/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/messages/package.json +20 -20
- package/node_modules/@dsh-std/messages/schema/message-observer-envelope-0.15.schema.json +54 -54
- package/node_modules/@dsh-std/presentation/CHANGELOG.md +9 -9
- package/node_modules/@dsh-std/presentation/LICENSE +21 -21
- package/node_modules/@dsh-std/presentation/README.md +27 -27
- package/node_modules/@dsh-std/presentation/README.zh.md +29 -29
- package/node_modules/@dsh-std/presentation/lib/callback.js.map +1 -1
- package/node_modules/@dsh-std/presentation/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/presentation/package.json +25 -25
- package/node_modules/@dsh-std/storage/CHANGELOG.md +8 -8
- package/node_modules/@dsh-std/storage/LICENSE +21 -21
- package/node_modules/@dsh-std/storage/README.md +5 -5
- package/node_modules/@dsh-std/storage/README.zh.md +7 -7
- package/node_modules/@dsh-std/storage/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/storage/package.json +20 -20
- package/node_modules/@dsh-std/storage/schema/local-storage-0.15.schema.json +56 -56
- package/package.json +19 -5
- package/presets/liangshen/.dsh-tui-managed.json +1 -1
- package/presets/liangshen/instruction-hint.mjs +8 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/LICENSE +21 -21
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/README.md +42 -42
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/README.zh.md +42 -42
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/memory.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/package.json +20 -20
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/LICENSE +21 -21
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/README.md +13 -13
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/package.json +21 -21
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/CHANGELOG.md +9 -9
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/LICENSE +21 -21
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/README.md +5 -5
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/README.zh.md +5 -5
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/package.json +20 -20
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/schema/dsh-plugin-0.15.schema.json +144 -144
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/CHANGELOG.md +9 -9
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/LICENSE +21 -21
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/README.md +27 -27
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/README.zh.md +29 -29
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/callback.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/package.json +25 -25
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/LICENSE +21 -21
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/README.md +13 -13
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/package.json +21 -21
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/LICENSE +21 -21
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/README.md +13 -13
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/package.json +21 -21
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/LICENSE +21 -21
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/README.md +13 -13
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/package.json +21 -21
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/LICENSE +21 -21
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/README.md +42 -42
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/README.zh.md +42 -42
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/memory.js.map +1 -1
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js.map +1 -1
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/package.json +20 -20
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/LICENSE +21 -21
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/README.md +13 -13
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/package.json +21 -21
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/LICENSE +21 -21
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/README.md +13 -13
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/package.json +21 -21
|
@@ -3,29 +3,35 @@ import { assembleContextFor, installModelSelection } from '@deepseek-ai/dsh-agen
|
|
|
3
3
|
import { isUserInvocable, renderSkillContent } from '@deepseek-ai/dsh-skill';
|
|
4
4
|
import { createUserMessage, isTokenDelta, MessageId, ReasoningEffortId, } from '@deepseek-ai/dsh-llm';
|
|
5
5
|
import { runSideQuestion, wrapSideQuestion } from './sideQuestion.js';
|
|
6
|
+
import { collectRecentActivity, parseRecapResponse, RECAP_RECENT_CHARS, wrapRecapPrompt } from './recap.js';
|
|
7
|
+
import { SESSION_COLOR_NAMES } from '../cc/sessionColors.js';
|
|
6
8
|
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
7
9
|
import { renderContextSections, renderPrompt } from '@deepseek-ai/dsh-system-prompt';
|
|
8
10
|
import { loadBaselineInstructions } from '@deepseek-ai/dsh-agent-instructions';
|
|
9
11
|
import { extname, isAbsolute, join } from 'node:path';
|
|
10
12
|
import { completeCommands, HIDDEN_COMMAND_NAMES, isLocalCommandName, LOCAL_COMMANDS, parseCommandName } from '../commands.js';
|
|
11
13
|
import { clearResumeTarget, forgetSession, readResumeTarget, touchSession, writeResumeTarget } from '../sessionHistory.js';
|
|
12
|
-
import { appendSessionTitle, deleteSessionLog, ensureLegacySessionEventTypes, sessionsRoots } from './compat/index.js';
|
|
13
|
-
import {
|
|
14
|
+
import { appendSessionTitle, defaultMaxScanned, deleteSessionLog, ensureLegacySessionEventTypes, readSessionEventsFromFile, readSessionEventsFromLog, sessionsRoots } from './compat/index.js';
|
|
15
|
+
import { buildSessionTree, forkTarget, liveTailWindow, rewindTarget, turnUserText, } from './sessionTree.js';
|
|
16
|
+
import { resolveDshProfileName } from '../update.js';
|
|
17
|
+
import { listSummaries, locateSession, noteBranch, previewSession, readHeader, } from './sessions/index.js';
|
|
14
18
|
import { writeActivityFrames } from '../activityPrefs.js';
|
|
15
19
|
import { isPathLikeQuery, rankFileCandidates } from '../utils/fileSuggestions.js';
|
|
16
20
|
import { readEffortPref, writeEffortPref } from '../effortPrefs.js';
|
|
17
21
|
import { readModelPref, writeModelPref } from '../modelPrefs.js';
|
|
18
22
|
import { explicitModelRoute, recordedModelRoute, resolveModelRoute, validateModelRoute } from '../modelRoute.js';
|
|
19
23
|
import { readPresetPref, writePresetPref } from '../presetPrefs.js';
|
|
20
|
-
import { composePreset, resolvePersistedPreset, rosterOf, runningPresetOf, serviceForAgent } from './presets.js';
|
|
21
|
-
import { isPresetName } from '../components/activityFrames.js';
|
|
24
|
+
import { composePreset, resolvePersistedPreset, resolvePersistedRoute, rosterOf, runningPresetOf, serviceForAgent } from './presets.js';
|
|
25
|
+
import { isPresetName, PRESET_NAMES } from '../components/activityFrames.js';
|
|
22
26
|
import { existsSync, statSync, writeFileSync } from 'node:fs';
|
|
23
27
|
import { logForDebugging } from '../utils/debug.js';
|
|
24
28
|
import { homeDir, LEGACY_DATA_DIR } from '../utils/paths.js';
|
|
25
29
|
import { extractMentions } from '../utils/mentions.js';
|
|
26
|
-
import { t } from '../i18n.js';
|
|
30
|
+
import { getLang, LANGS, t } from '../i18n.js';
|
|
31
|
+
import { AUTO_THEME_NAME, THEME_NAMES } from '../theme.js';
|
|
32
|
+
import { listCustomThemes } from '../customTheme.js';
|
|
27
33
|
import { modeDisplayName, resolveSessionModes } from '../sessionModes.js';
|
|
28
|
-
import { normalizeStatusBar, normalizeToolBackground } from '../tuiDisplayPrefs.js';
|
|
34
|
+
import { normalizeScrollGutter, normalizeStatusBar, normalizeToolBackground } from '../tuiDisplayPrefs.js';
|
|
29
35
|
import { SubagentActivityStore } from './subagents.js';
|
|
30
36
|
import { ActivityTracker } from 'dsh-working-activity/status';
|
|
31
37
|
import { featureOn } from 'dsh-working-activity/config';
|
|
@@ -328,6 +334,72 @@ function toolResultText(event) {
|
|
|
328
334
|
return '';
|
|
329
335
|
return block.content.map(item => item.type === 'text' ? item.text : '').join('').trim();
|
|
330
336
|
}
|
|
337
|
+
/** Phase badge for the harness goal card — mirrors the panel's PhaseBadge. */
|
|
338
|
+
const GOAL_RESULT_BADGE = {
|
|
339
|
+
active: '● active',
|
|
340
|
+
paused: '⏸ paused',
|
|
341
|
+
blocked: '⛔ blocked',
|
|
342
|
+
complete: '✓ complete',
|
|
343
|
+
};
|
|
344
|
+
/**
|
|
345
|
+
* Summary cards for the harness's goal/todo tools. Their results are machine
|
|
346
|
+
* JSON (`{"goal":{…}}` / `{"todos":[…]}`) that would otherwise dump under the
|
|
347
|
+
* tool card as a raw `⎿ {"goal":…}` line. Matched by tool-name substring AND
|
|
348
|
+
* payload shape, so unrelated tools and plain-text results pass through to
|
|
349
|
+
* the registry/raw-text path untouched. Runs on the live stream and replay.
|
|
350
|
+
*/
|
|
351
|
+
function harnessToolResultView(name, data) {
|
|
352
|
+
const lower = name.toLowerCase();
|
|
353
|
+
const isGoalTool = lower.includes('goal');
|
|
354
|
+
const isTodoTool = lower.includes('todo');
|
|
355
|
+
if (!isGoalTool && !isTodoTool)
|
|
356
|
+
return undefined;
|
|
357
|
+
const block = data.message.content[0];
|
|
358
|
+
if (block === undefined || block.type !== 'tool-result')
|
|
359
|
+
return undefined;
|
|
360
|
+
const text = block.content.map(item => item.type === 'text' ? item.text : '').join('').trim();
|
|
361
|
+
if (text === '' || !text.startsWith('{'))
|
|
362
|
+
return undefined;
|
|
363
|
+
let parsed;
|
|
364
|
+
try {
|
|
365
|
+
parsed = JSON.parse(text);
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
return undefined;
|
|
369
|
+
}
|
|
370
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
371
|
+
return undefined;
|
|
372
|
+
const record = parsed;
|
|
373
|
+
if (isGoalTool && typeof record.goal === 'object' && record.goal !== null) {
|
|
374
|
+
const goal = record.goal;
|
|
375
|
+
if (typeof goal.objective === 'string' && typeof goal.phase === 'string') {
|
|
376
|
+
const badge = GOAL_RESULT_BADGE[goal.phase] ?? goal.phase;
|
|
377
|
+
const rounds = typeof goal.roundsStarted === 'number' && typeof goal.maxGoalRounds === 'number'
|
|
378
|
+
? ` · ${goal.roundsStarted}/${goal.maxGoalRounds}`
|
|
379
|
+
: '';
|
|
380
|
+
const activation = typeof record.activation === 'string' ? ` · ${record.activation}` : '';
|
|
381
|
+
const lines = [`🎯 ${goal.objective}`, `${badge}${rounds}${activation}`];
|
|
382
|
+
const blocked = goal.blockedReason?.message;
|
|
383
|
+
if (typeof blocked === 'string')
|
|
384
|
+
lines.push(`⛔ ${blocked}`);
|
|
385
|
+
return { card: 'generic', content: lines.map(line => ({ type: 'text', text: line })) };
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (isTodoTool && Array.isArray(record.todos)) {
|
|
389
|
+
const rows = record.todos;
|
|
390
|
+
const done = rows.filter(row => row.status === 'completed').length;
|
|
391
|
+
const lines = [`todos ✓ ${done}/${rows.length}`];
|
|
392
|
+
for (const row of rows) {
|
|
393
|
+
if (row.status !== 'in_progress' || typeof row.content !== 'string')
|
|
394
|
+
continue;
|
|
395
|
+
lines.push(`● ${row.content}`);
|
|
396
|
+
if (lines.length >= 4)
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
return { card: 'generic', content: lines.map(line => ({ type: 'text', text: line })) };
|
|
400
|
+
}
|
|
401
|
+
return undefined;
|
|
402
|
+
}
|
|
331
403
|
function toolErrorText(event) {
|
|
332
404
|
const failure = event.data.error;
|
|
333
405
|
if (failure === undefined)
|
|
@@ -433,8 +505,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
433
505
|
try {
|
|
434
506
|
runtime.interrupt(target, { kind: 'ancestor', agent });
|
|
435
507
|
subagentStore.onCancelled(agentId, 'interrupted');
|
|
436
|
-
|
|
437
|
-
syncSubagentRows();
|
|
508
|
+
syncSubagentsNow();
|
|
438
509
|
state.emit();
|
|
439
510
|
return true;
|
|
440
511
|
}
|
|
@@ -467,9 +538,12 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
467
538
|
/**
|
|
468
539
|
* Sync subagentStore state into ChatRows (insert/update in state.rows).
|
|
469
540
|
* Called whenever subagent state changes (spawned/completed/failed/output).
|
|
541
|
+
* Accepts a caller-taken snapshot to avoid the double copy on the hot path
|
|
542
|
+
* (session/event fires per subagent token: snapshot here + snapshot in the
|
|
543
|
+
* caller = two full state copies before emitStream's 16ms throttle).
|
|
470
544
|
*/
|
|
471
|
-
const syncSubagentRows = () => {
|
|
472
|
-
const snapshot = subagentStore.snapshot();
|
|
545
|
+
const syncSubagentRows = (preSnapshot) => {
|
|
546
|
+
const snapshot = preSnapshot ?? subagentStore.snapshot();
|
|
473
547
|
for (const sub of snapshot) {
|
|
474
548
|
let row = subagentRowsByAgentId.get(sub.agentId);
|
|
475
549
|
if (!row) {
|
|
@@ -546,6 +620,32 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
546
620
|
const listeners = new Set();
|
|
547
621
|
/** True while a frame-aligned stream notification is pending (emitStream). */
|
|
548
622
|
let streamNotifyScheduled = false;
|
|
623
|
+
/** True while subagent assistant/chunk deltas have deferred their
|
|
624
|
+
* snapshot+projection to the frame-aligned flush (emitStream's timer).
|
|
625
|
+
* Chunks arrive at token rate (100-300 events/s) and the projection is a
|
|
626
|
+
* full deep state copy (SubagentActivityStore.snapshot) plus a SubagentRow
|
|
627
|
+
* rebuild per tracked agent — running that per token sits BEFORE
|
|
628
|
+
* emitStream's 16ms coalescing and defeats it. Non-chunk events
|
|
629
|
+
* (tool/call, subagent/end, interrupt) project immediately and clear
|
|
630
|
+
* this flag, so lifecycle transitions stay synchronous. */
|
|
631
|
+
let subagentStreamDirty = false;
|
|
632
|
+
/** Deferred projection for the frame-aligned flush: runs INSIDE the
|
|
633
|
+
* emitStream timer, before listeners wake, so React always reads fully
|
|
634
|
+
* projected rows. No-op unless a chunk marked the projection dirty. */
|
|
635
|
+
const flushSubagentStream = () => {
|
|
636
|
+
if (!subagentStreamDirty)
|
|
637
|
+
return;
|
|
638
|
+
subagentStreamDirty = false;
|
|
639
|
+
state.subagents = subagentStore.snapshot();
|
|
640
|
+
syncSubagentRows(state.subagents);
|
|
641
|
+
};
|
|
642
|
+
/** Immediate projection; supersedes any pending deferred flush (the fresh
|
|
643
|
+
* snapshot already contains everything the deferred pass would project). */
|
|
644
|
+
const syncSubagentsNow = () => {
|
|
645
|
+
subagentStreamDirty = false;
|
|
646
|
+
state.subagents = subagentStore.snapshot();
|
|
647
|
+
syncSubagentRows(state.subagents);
|
|
648
|
+
};
|
|
549
649
|
// foldRows incremental cursor (see foldRows): rows only append past the
|
|
550
650
|
// fold line, so each pass touches only newly-eligible rows.
|
|
551
651
|
const foldCursor = { rows: null, index: 0 };
|
|
@@ -770,6 +870,79 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
770
870
|
ctx.logger.warn('dsh-tui: tui/session-switched dispatch failed: %o', error);
|
|
771
871
|
}
|
|
772
872
|
};
|
|
873
|
+
/**
|
|
874
|
+
* Swap the live agent for a freshly created fork (rewindTo and the session
|
|
875
|
+
* tree's rewindToNode share this tail): reset every session-scoped
|
|
876
|
+
* projection, replay the fork's seed into a fresh transcript (tokens/
|
|
877
|
+
* spinner counters land back at the rewind point, matching the fork),
|
|
878
|
+
* rebind subscriptions to the new agent, and free the replaced handle.
|
|
879
|
+
* Returns the source session's id (for the session-switched notification).
|
|
880
|
+
*/
|
|
881
|
+
const adoptForkedAgent = (handle, seed, agentPreset, childId) => {
|
|
882
|
+
// Replay the forked history into a fresh transcript (tokens/spinner
|
|
883
|
+
// counters land back at the rewind point, matching the fork).
|
|
884
|
+
streaming = undefined;
|
|
885
|
+
reasoning = undefined;
|
|
886
|
+
// Stale sealed/thinking bookkeeping belongs to the OLD agent's rows;
|
|
887
|
+
// keep it out of the next turn's settle logs and revive cache.
|
|
888
|
+
sealedReasoning.length = 0;
|
|
889
|
+
lastReasoningRow = undefined;
|
|
890
|
+
toolCards.clear();
|
|
891
|
+
nextRowId = 0;
|
|
892
|
+
state.rows.length = 0;
|
|
893
|
+
// Goal/todo/title are session-scoped; the replay re-derives them for
|
|
894
|
+
// the session being entered (or leaves them empty).
|
|
895
|
+
state.todos = [];
|
|
896
|
+
// Queued-but-undelivered messages live in the OLD agent's inbox; the
|
|
897
|
+
// swap must drop their previews or they linger forever (unretirable —
|
|
898
|
+
// retire events are filtered to the new agent, unwithdrawable — the
|
|
899
|
+
// new inbox never heard of them).
|
|
900
|
+
state.pending = [];
|
|
901
|
+
state.goal = undefined;
|
|
902
|
+
state.sessionTitle = '';
|
|
903
|
+
state.sessionColor = '';
|
|
904
|
+
state.tokens = { input: 0, output: 0 };
|
|
905
|
+
state.responseChars = 0;
|
|
906
|
+
state.activeToolCount = 0;
|
|
907
|
+
state.lastUserText = '';
|
|
908
|
+
state.working = false;
|
|
909
|
+
state.spinnerMode = 'requesting';
|
|
910
|
+
state.status = handle.agent.status;
|
|
911
|
+
state.agentId = handle.agent.id;
|
|
912
|
+
state.agentPreset = agentPreset;
|
|
913
|
+
state.tps = undefined;
|
|
914
|
+
state.tpsSamples = [];
|
|
915
|
+
state.lastUsage = undefined;
|
|
916
|
+
state.workingActivity = undefined;
|
|
917
|
+
state.contextSegments = {
|
|
918
|
+
system: 0,
|
|
919
|
+
prompt: 0,
|
|
920
|
+
assistant: 0,
|
|
921
|
+
thinking: 0,
|
|
922
|
+
tools: 0,
|
|
923
|
+
};
|
|
924
|
+
replayEvents(seed);
|
|
925
|
+
settleStreaming();
|
|
926
|
+
// A seed ending mid-turn replays a turn/start that set working=true;
|
|
927
|
+
// the boot path resets this after replay — mirror it here so an idle
|
|
928
|
+
// rewound agent doesn't sit with a live spinner (a still-running
|
|
929
|
+
// agent re-asserts on its next event).
|
|
930
|
+
state.working = handle.agent.status === 'running';
|
|
931
|
+
// Rebind subscriptions to the new agent, then free the old one.
|
|
932
|
+
const oldHandle = currentHandle;
|
|
933
|
+
const sourceSessionId = String(agent.session.id);
|
|
934
|
+
agent = handle.agent;
|
|
935
|
+
currentHandle = handle;
|
|
936
|
+
bindAgent();
|
|
937
|
+
refreshCommandList();
|
|
938
|
+
void refreshLoadedContext();
|
|
939
|
+
void refreshSkillCommands();
|
|
940
|
+
// The forked session (rewind) becomes the most recently used.
|
|
941
|
+
touchSession(childId);
|
|
942
|
+
state.emit();
|
|
943
|
+
void oldHandle?.dispose().catch(() => { });
|
|
944
|
+
return sourceSessionId;
|
|
945
|
+
};
|
|
773
946
|
/** Monotonic token: only the latest `interruptAndDeliver` re-queues, so a
|
|
774
947
|
* second interrupt while the abort settles cannot double-deliver. */
|
|
775
948
|
let interruptSeq = 0;
|
|
@@ -1136,12 +1309,86 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1136
1309
|
// it is keyed by cwd so a /workspace switch or resumed session never reuses
|
|
1137
1310
|
// another directory's listing.
|
|
1138
1311
|
const fileCandidateCache = { cwd: '', load: undefined };
|
|
1312
|
+
// `/model <provider/id>` completion: the model catalog is async (one llm
|
|
1313
|
+
// listModels per provider), so the first keystrokes that could be heading
|
|
1314
|
+
// for /model warm a session-lifetime cache — the shared promise dedupes
|
|
1315
|
+
// concurrent triggers, children() synchronously serves whatever has landed,
|
|
1316
|
+
// and the arrival state.emit() reopens the menu mid-typing. switchModel's
|
|
1317
|
+
// success path drops the cache so the [current] tag re-resolves against
|
|
1318
|
+
// the new route.
|
|
1319
|
+
const modelNodeCache = {
|
|
1320
|
+
nodes: undefined,
|
|
1321
|
+
load: undefined,
|
|
1322
|
+
};
|
|
1323
|
+
const warmModelNodes = () => {
|
|
1324
|
+
if (modelNodeCache.load !== undefined)
|
|
1325
|
+
return;
|
|
1326
|
+
modelNodeCache.load = state.listModels().then((list) => {
|
|
1327
|
+
modelNodeCache.nodes = list.map((model) => ({
|
|
1328
|
+
name: `${model.provider}/${model.id}`,
|
|
1329
|
+
description: model.name,
|
|
1330
|
+
...(state.provider === model.provider && state.model === model.id
|
|
1331
|
+
? { tag: 'current' }
|
|
1332
|
+
: {}),
|
|
1333
|
+
}));
|
|
1334
|
+
state.emit();
|
|
1335
|
+
}).catch(() => {
|
|
1336
|
+
// listModels already swallows per-provider failures; this only fires
|
|
1337
|
+
// when the llm service shape itself is missing — settle on an empty
|
|
1338
|
+
// menu rather than retrying on every keystroke.
|
|
1339
|
+
modelNodeCache.nodes = [];
|
|
1340
|
+
});
|
|
1341
|
+
};
|
|
1342
|
+
// `/preset <id>` completion: same warm-cache pattern as models. The
|
|
1343
|
+
// current/default tags resolve at children() time (sync state reads), so
|
|
1344
|
+
// no cache invalidation is needed on switch.
|
|
1345
|
+
const presetOptionCache = {
|
|
1346
|
+
list: undefined,
|
|
1347
|
+
load: undefined,
|
|
1348
|
+
};
|
|
1349
|
+
const warmPresetOptions = () => {
|
|
1350
|
+
if (presetOptionCache.load !== undefined)
|
|
1351
|
+
return;
|
|
1352
|
+
presetOptionCache.load = state.listPresets().then((list) => {
|
|
1353
|
+
presetOptionCache.list = list;
|
|
1354
|
+
state.emit();
|
|
1355
|
+
}).catch(() => {
|
|
1356
|
+
presetOptionCache.list = [];
|
|
1357
|
+
});
|
|
1358
|
+
};
|
|
1359
|
+
// `/effort <id>` completion: state.effortLevels is the sync vocabulary
|
|
1360
|
+
// (populated on route changes); when still unknown, one best-effort
|
|
1361
|
+
// resolveEfforts warms it. `tried` caps the retry — resolveEfforts
|
|
1362
|
+
// notifies on hard errors, so keystroke-time retries would spam.
|
|
1363
|
+
const effortWarm = { tried: false };
|
|
1364
|
+
const warmEffortLevels = () => {
|
|
1365
|
+
if (state.effortLevels !== undefined || effortWarm.tried)
|
|
1366
|
+
return;
|
|
1367
|
+
effortWarm.tried = true;
|
|
1368
|
+
void resolveEfforts().then((resolved) => {
|
|
1369
|
+
if (resolved === 'unavailable' || resolved === 'error')
|
|
1370
|
+
return;
|
|
1371
|
+
effortWarm.tried = false;
|
|
1372
|
+
state.emit();
|
|
1373
|
+
}).catch(() => { });
|
|
1374
|
+
};
|
|
1139
1375
|
const state = {
|
|
1140
1376
|
effortLevels: undefined,
|
|
1141
1377
|
version: 0,
|
|
1142
1378
|
rows: [],
|
|
1143
1379
|
status: 'starting',
|
|
1144
1380
|
sessionTitle: '',
|
|
1381
|
+
sessionColor: '',
|
|
1382
|
+
get autoRecapOnOpen() {
|
|
1383
|
+
// Live read (not a boot snapshot): a /settings change applies on the
|
|
1384
|
+
// next session switch. No settings service → off (framework absent,
|
|
1385
|
+
// e.g. headless fixtures — nothing to configure and no llm route).
|
|
1386
|
+
const settings = ctx.get('settings');
|
|
1387
|
+
if (settings === undefined)
|
|
1388
|
+
return false;
|
|
1389
|
+
const ns = settings.describe({ redactSecrets: true }).find(entry => entry.ns === 'dsh-tui');
|
|
1390
|
+
return ns?.value?.recapOnOpen !== false;
|
|
1391
|
+
},
|
|
1145
1392
|
agentId: agent.id,
|
|
1146
1393
|
model: options.model,
|
|
1147
1394
|
provider: options.provider,
|
|
@@ -1166,9 +1413,16 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1166
1413
|
modeIndex: 0,
|
|
1167
1414
|
workingActivity: undefined,
|
|
1168
1415
|
activityFrames: options.activityFrames,
|
|
1416
|
+
configuredProvider: options.configuredProvider,
|
|
1417
|
+
configuredModel: options.configuredModel,
|
|
1418
|
+
configuredPreset: options.configuredPreset,
|
|
1419
|
+
configuredActivityFrames: options.configuredActivityFrames,
|
|
1420
|
+
configuredLang: options.configuredLang,
|
|
1169
1421
|
diffLayout: options.diffLayout ?? 'auto',
|
|
1170
1422
|
thinkingFold: options.thinkingFold ?? 'preview',
|
|
1171
1423
|
toolBackground: normalizeToolBackground(options.toolBackground),
|
|
1424
|
+
scrollGutter: normalizeScrollGutter(options.scrollGutter),
|
|
1425
|
+
promptSessionLabel: options.promptSessionLabel === true,
|
|
1172
1426
|
statusBar: normalizeStatusBar(options.statusBar),
|
|
1173
1427
|
whale: options.whale !== false,
|
|
1174
1428
|
minimal: options.minimal === true,
|
|
@@ -1181,7 +1435,106 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1181
1435
|
pending: [],
|
|
1182
1436
|
commandList: LOCAL_COMMANDS,
|
|
1183
1437
|
commandCompletions(input) {
|
|
1438
|
+
// Warm the async vocabularies as soon as the input could be heading
|
|
1439
|
+
// for their commands (`/m`, `/pre`, …) — by the time a trailing space
|
|
1440
|
+
// asks children() for nodes, the fetch has usually landed.
|
|
1441
|
+
const head = input.slice(1).split(/[\t ]/)[0]?.toLowerCase() ?? '';
|
|
1442
|
+
if (head !== '') {
|
|
1443
|
+
if ('model'.startsWith(head))
|
|
1444
|
+
warmModelNodes();
|
|
1445
|
+
if ('preset'.startsWith(head))
|
|
1446
|
+
warmPresetOptions();
|
|
1447
|
+
if ('effort'.startsWith(head))
|
|
1448
|
+
warmEffortLevels();
|
|
1449
|
+
}
|
|
1184
1450
|
return completeCommands(input, state.commandList, (path) => {
|
|
1451
|
+
if (path.length === 1 && path[0] === 'model') {
|
|
1452
|
+
// provider/id specs, current model tagged; see modelNodeCache.
|
|
1453
|
+
warmModelNodes();
|
|
1454
|
+
return modelNodeCache.nodes ?? [];
|
|
1455
|
+
}
|
|
1456
|
+
if (path.length === 1 && path[0] === 'lang') {
|
|
1457
|
+
return [
|
|
1458
|
+
{ name: 'status', description: 'Show the current UI language', descriptionKey: 'sugg-status-desc' },
|
|
1459
|
+
...LANGS.map((lang) => ({
|
|
1460
|
+
name: lang,
|
|
1461
|
+
description: `Switch the UI language to ${lang}`,
|
|
1462
|
+
descriptionKey: lang === 'zh' ? 'sugg-lang-zh-desc' : 'sugg-lang-en-desc',
|
|
1463
|
+
...(getLang() === lang ? { tag: 'current' } : {}),
|
|
1464
|
+
})),
|
|
1465
|
+
];
|
|
1466
|
+
}
|
|
1467
|
+
if (path.length === 1 && path[0] === 'theme') {
|
|
1468
|
+
return [
|
|
1469
|
+
{ name: 'status', description: 'Show the current theme', descriptionKey: 'sugg-status-desc' },
|
|
1470
|
+
{ name: AUTO_THEME_NAME, description: 'Follow the terminal background', descriptionKey: 'sugg-theme-auto-desc' },
|
|
1471
|
+
...THEME_NAMES.map((name) => ({
|
|
1472
|
+
name,
|
|
1473
|
+
description: `Built-in theme ${name}`,
|
|
1474
|
+
descriptionKey: 'sugg-theme-builtin-desc',
|
|
1475
|
+
})),
|
|
1476
|
+
...listCustomThemes()
|
|
1477
|
+
.filter((spec) => spec.name !== AUTO_THEME_NAME)
|
|
1478
|
+
.map((spec) => ({
|
|
1479
|
+
name: spec.name,
|
|
1480
|
+
description: `User theme (${spec.base} base)`,
|
|
1481
|
+
descriptionKey: 'sugg-theme-user-desc',
|
|
1482
|
+
})),
|
|
1483
|
+
];
|
|
1484
|
+
}
|
|
1485
|
+
if (path.length === 1 && path[0] === 'color') {
|
|
1486
|
+
return [
|
|
1487
|
+
{ name: 'status', description: 'Show the current session color', descriptionKey: 'sugg-status-desc' },
|
|
1488
|
+
{ name: 'reset', description: 'Clear the session color', descriptionKey: 'sugg-color-reset-desc' },
|
|
1489
|
+
...SESSION_COLOR_NAMES.map((name) => ({
|
|
1490
|
+
name,
|
|
1491
|
+
description: 'Session accent color',
|
|
1492
|
+
descriptionKey: 'sugg-color-name-desc',
|
|
1493
|
+
...(state.sessionColor === name ? { tag: 'current' } : {}),
|
|
1494
|
+
})),
|
|
1495
|
+
];
|
|
1496
|
+
}
|
|
1497
|
+
if (path.length === 1 && path[0] === 'effort') {
|
|
1498
|
+
warmEffortLevels();
|
|
1499
|
+
return [
|
|
1500
|
+
{ name: 'status', description: 'Show the current reasoning effort', descriptionKey: 'sugg-status-desc' },
|
|
1501
|
+
...(state.effortLevels ?? []).map((id) => ({
|
|
1502
|
+
name: id,
|
|
1503
|
+
description: 'Reasoning effort level',
|
|
1504
|
+
descriptionKey: 'sugg-effort-level-desc',
|
|
1505
|
+
...(state.reasoningEffort === id ? { tag: 'current' } : {}),
|
|
1506
|
+
})),
|
|
1507
|
+
];
|
|
1508
|
+
}
|
|
1509
|
+
if (path.length === 1 && path[0] === 'preset') {
|
|
1510
|
+
warmPresetOptions();
|
|
1511
|
+
return [
|
|
1512
|
+
{ name: 'status', description: 'Show the current agent preset', descriptionKey: 'sugg-status-desc' },
|
|
1513
|
+
...(presetOptionCache.list ?? []).map((preset) => ({
|
|
1514
|
+
name: preset.id,
|
|
1515
|
+
description: preset.description ?? preset.name ?? preset.id,
|
|
1516
|
+
...(preset.id === state.agentPreset
|
|
1517
|
+
? { tag: 'current' }
|
|
1518
|
+
: preset.isDefault
|
|
1519
|
+
? { tag: 'default' }
|
|
1520
|
+
: {}),
|
|
1521
|
+
})),
|
|
1522
|
+
];
|
|
1523
|
+
}
|
|
1524
|
+
if (path.length === 1 && path[0] === 'activity') {
|
|
1525
|
+
return [
|
|
1526
|
+
{ name: 'status', description: 'Show the current activity preset', descriptionKey: 'sugg-status-desc' },
|
|
1527
|
+
{ name: 'frames', description: 'List or switch frame presets', descriptionKey: 'sugg-activity-frames-desc' },
|
|
1528
|
+
];
|
|
1529
|
+
}
|
|
1530
|
+
if (path.length === 2 && path[0] === 'activity' && path[1] === 'frames') {
|
|
1531
|
+
return PRESET_NAMES.map((name) => ({
|
|
1532
|
+
name,
|
|
1533
|
+
description: 'Animation frame preset',
|
|
1534
|
+
descriptionKey: 'sugg-activity-frame-desc',
|
|
1535
|
+
...(state.activityFrames === name ? { tag: 'current' } : {}),
|
|
1536
|
+
}));
|
|
1537
|
+
}
|
|
1185
1538
|
if (path.length === 1 && path[0] === 'workspace') {
|
|
1186
1539
|
const builtins = [
|
|
1187
1540
|
{ name: 'resume', description: 'Switch to another workspace', descriptionKey: 'cmd-desc-workspace-resume' },
|
|
@@ -1200,6 +1553,21 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1200
1553
|
})),
|
|
1201
1554
|
];
|
|
1202
1555
|
}
|
|
1556
|
+
if (path.length === 1 && path[0] === 'permission') {
|
|
1557
|
+
// Sandbox-preset vocabulary of the `/permission` picker — Tab
|
|
1558
|
+
// completes it for keyboard users, Enter still dispatches.
|
|
1559
|
+
return [
|
|
1560
|
+
{ name: 'read-only', description: 'Read-only session: no file writes, no commands', descriptionKey: 'permission-preset-readonly-desc' },
|
|
1561
|
+
{ name: 'workspace-write', description: 'Read/write inside the workspace; writes need a prior read', descriptionKey: 'permission-preset-workspace-write-desc' },
|
|
1562
|
+
{ name: 'danger-full-access', description: 'Unrestricted access, no approvals', descriptionKey: 'permission-preset-full-access-desc' },
|
|
1563
|
+
];
|
|
1564
|
+
}
|
|
1565
|
+
if (path.length === 1 && path[0] === 'plan') {
|
|
1566
|
+
return [
|
|
1567
|
+
{ name: 'on', description: 'Enter plan mode: read-only, plan before acting', descriptionKey: 'plan-mode-on-desc' },
|
|
1568
|
+
{ name: 'off', description: 'Exit plan mode, back to normal execution', descriptionKey: 'plan-mode-off-desc' },
|
|
1569
|
+
];
|
|
1570
|
+
}
|
|
1203
1571
|
return commandTrees?.children(path) ?? [];
|
|
1204
1572
|
});
|
|
1205
1573
|
},
|
|
@@ -1240,6 +1608,9 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1240
1608
|
streamNotifyScheduled = true;
|
|
1241
1609
|
const timer = setTimeout(() => {
|
|
1242
1610
|
streamNotifyScheduled = false;
|
|
1611
|
+
// Deferred subagent projection rides this same frame: flush BEFORE
|
|
1612
|
+
// the listeners wake so React reads fully projected rows.
|
|
1613
|
+
flushSubagentStream();
|
|
1243
1614
|
foldRows(state.rows, MAX_ROWS, foldCursor);
|
|
1244
1615
|
for (const listener of listeners)
|
|
1245
1616
|
listener();
|
|
@@ -1509,67 +1880,9 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1509
1880
|
catch (error) {
|
|
1510
1881
|
state.notify(t('rewind-attach-failed', { err: error instanceof Error ? error.message : String(error) }), { color: 'warning', timeoutMs: 8000 });
|
|
1511
1882
|
}
|
|
1512
|
-
//
|
|
1513
|
-
//
|
|
1514
|
-
|
|
1515
|
-
reasoning = undefined;
|
|
1516
|
-
// Stale sealed/thinking bookkeeping belongs to the OLD agent's rows;
|
|
1517
|
-
// keep it out of the next turn's settle logs and revive cache.
|
|
1518
|
-
sealedReasoning.length = 0;
|
|
1519
|
-
lastReasoningRow = undefined;
|
|
1520
|
-
toolCards.clear();
|
|
1521
|
-
nextRowId = 0;
|
|
1522
|
-
state.rows.length = 0;
|
|
1523
|
-
// Goal/todo/title are session-scoped; the replay re-derives them for
|
|
1524
|
-
// the session being entered (or leaves them empty).
|
|
1525
|
-
state.todos = [];
|
|
1526
|
-
// Queued-but-undelivered messages live in the OLD agent's inbox; the
|
|
1527
|
-
// swap must drop their previews or they linger forever (unretirable —
|
|
1528
|
-
// retire events are filtered to the new agent, unwithdrawable — the
|
|
1529
|
-
// new inbox never heard of them).
|
|
1530
|
-
state.pending = [];
|
|
1531
|
-
state.goal = undefined;
|
|
1532
|
-
state.sessionTitle = '';
|
|
1533
|
-
state.tokens = { input: 0, output: 0 };
|
|
1534
|
-
state.responseChars = 0;
|
|
1535
|
-
state.activeToolCount = 0;
|
|
1536
|
-
state.lastUserText = '';
|
|
1537
|
-
state.working = false;
|
|
1538
|
-
state.spinnerMode = 'requesting';
|
|
1539
|
-
state.status = handle.agent.status;
|
|
1540
|
-
state.agentId = handle.agent.id;
|
|
1541
|
-
state.agentPreset = rewindComposed.agentPreset;
|
|
1542
|
-
state.tps = undefined;
|
|
1543
|
-
state.tpsSamples = [];
|
|
1544
|
-
state.lastUsage = undefined;
|
|
1545
|
-
state.workingActivity = undefined;
|
|
1546
|
-
state.contextSegments = {
|
|
1547
|
-
system: 0,
|
|
1548
|
-
prompt: 0,
|
|
1549
|
-
assistant: 0,
|
|
1550
|
-
thinking: 0,
|
|
1551
|
-
tools: 0,
|
|
1552
|
-
};
|
|
1553
|
-
replayEvents(seed);
|
|
1554
|
-
settleStreaming();
|
|
1555
|
-
// A seed ending mid-turn replays a turn/start that set working=true;
|
|
1556
|
-
// the boot path resets this after replay — mirror it here so an idle
|
|
1557
|
-
// rewound agent doesn't sit with a live spinner (a still-running
|
|
1558
|
-
// agent re-asserts on its next event).
|
|
1559
|
-
state.working = handle.agent.status === 'running';
|
|
1560
|
-
// Rebind subscriptions to the new agent, then free the old one.
|
|
1561
|
-
const oldHandle = currentHandle;
|
|
1562
|
-
const sourceSessionId = String(agent.session.id);
|
|
1563
|
-
agent = handle.agent;
|
|
1564
|
-
currentHandle = handle;
|
|
1565
|
-
bindAgent();
|
|
1566
|
-
refreshCommandList();
|
|
1567
|
-
void refreshLoadedContext();
|
|
1568
|
-
void refreshSkillCommands();
|
|
1569
|
-
// The forked session (rewind) becomes the most recently used.
|
|
1570
|
-
touchSession(childId);
|
|
1571
|
-
state.emit();
|
|
1572
|
-
void oldHandle?.dispose().catch(() => { });
|
|
1883
|
+
// Swap the live agent for the fork (shared with rewindToNode): replay
|
|
1884
|
+
// the seed, rebind, and free the replaced handle.
|
|
1885
|
+
const sourceSessionId = adoptForkedAgent(handle, seed, rewindComposed.agentPreset, childId);
|
|
1573
1886
|
// Decision-event pair around the completed rewind: `tui/rewind-done`
|
|
1574
1887
|
// (the first non-empty string is toasted as the post-rewind summary,
|
|
1575
1888
|
// e.g. a plugin reporting restored files) and the generic
|
|
@@ -1609,6 +1922,685 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1609
1922
|
notifySessionSwitched('rewind', String(childId), sourceSessionId);
|
|
1610
1923
|
return row.text;
|
|
1611
1924
|
},
|
|
1925
|
+
async buildSessionTree() {
|
|
1926
|
+
const persistence = ctx.get('sessionPersistence');
|
|
1927
|
+
if (!persistence) {
|
|
1928
|
+
state.notify(t('tree-unavailable'), { color: 'error' });
|
|
1929
|
+
return null;
|
|
1930
|
+
}
|
|
1931
|
+
// Pin the live session snapshot NOW: every await below (list/inspect)
|
|
1932
|
+
// is a window in which a fire-and-forget switch (/new, /resume,
|
|
1933
|
+
// /model) can swap `agent`. Reading agent.session piecemeal would
|
|
1934
|
+
// stitch the NEW session's events under the OLD session's id — a
|
|
1935
|
+
// confirm would then rewind from the wrong persisted log. Everything
|
|
1936
|
+
// below reads this snapshot, and the result is discarded if the live
|
|
1937
|
+
// session moved on before the build finished.
|
|
1938
|
+
const liveSession = agent.session;
|
|
1939
|
+
const currentId = String(liveSession.id);
|
|
1940
|
+
// Same enumerate as the /resume listing (snapshots when the backend
|
|
1941
|
+
// offers revisions, plain list otherwise), each header narrowed through
|
|
1942
|
+
// the sessions reader — one malformed header costs that session its
|
|
1943
|
+
// metadata, never the whole tree. `raw` stays the backend's own header
|
|
1944
|
+
// object for locate() below.
|
|
1945
|
+
let listed = [];
|
|
1946
|
+
try {
|
|
1947
|
+
if (typeof persistence.listSnapshots === 'function') {
|
|
1948
|
+
const snapshots = await persistence.listSnapshots();
|
|
1949
|
+
listed = snapshots.flatMap(snapshot => {
|
|
1950
|
+
const raw = snapshot?.header;
|
|
1951
|
+
const header = readHeader(raw);
|
|
1952
|
+
return header === undefined ? [] : [{ header, raw }];
|
|
1953
|
+
});
|
|
1954
|
+
}
|
|
1955
|
+
else if (typeof persistence.list === 'function') {
|
|
1956
|
+
const headers = await persistence.list();
|
|
1957
|
+
listed = headers.flatMap(raw => {
|
|
1958
|
+
const header = readHeader(raw);
|
|
1959
|
+
return header === undefined ? [] : [{ header, raw }];
|
|
1960
|
+
});
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
catch {
|
|
1964
|
+
// A listing failure degrades the tree to the live session only.
|
|
1965
|
+
}
|
|
1966
|
+
// Same cwd scoping as /resume (Claude Code's project dimension): forks
|
|
1967
|
+
// inherit cwd, so the family never crosses projects — and the match is
|
|
1968
|
+
// the project-aware one /resume uses, so a pre-upgrade subdirectory
|
|
1969
|
+
// path, Windows separators, or a case variant on one header cannot
|
|
1970
|
+
// quietly amputate the ancestors and siblings it records. Subagent
|
|
1971
|
+
// child sessions carry parentSession too, but they are delegation
|
|
1972
|
+
// artifacts, not rewind branches — exclude them from the family.
|
|
1973
|
+
const local = listed.filter(entry => sessionCwdMatches(state.cwd, entry.header.cwd ?? '') &&
|
|
1974
|
+
entry.header.origin !== 'subagent' &&
|
|
1975
|
+
(entry.header.delegationDepth ?? 0) === 0);
|
|
1976
|
+
const headerById = new Map(local.map(entry => [entry.header.id, entry]));
|
|
1977
|
+
// The live session's header may not be materialized in list() yet
|
|
1978
|
+
// (the jsonl backend writes on first append) — overlay the in-memory
|
|
1979
|
+
// header so the ancestor walk below still finds a fresh fork's parent.
|
|
1980
|
+
const liveMeta = liveSession.header;
|
|
1981
|
+
if (!headerById.has(currentId) && liveMeta !== undefined) {
|
|
1982
|
+
headerById.set(currentId, { header: readHeader(liveMeta) ?? { id: currentId, cwd: undefined, createdAt: undefined, parentSession: undefined, origin: undefined, delegationDepth: undefined, seedLength: undefined, agentPreset: undefined }, raw: liveMeta });
|
|
1983
|
+
}
|
|
1984
|
+
// Family = the live session's ancestor chain PLUS every descendant of
|
|
1985
|
+
// its topmost known ancestor (siblings and cousins included).
|
|
1986
|
+
const childrenByParent = new Map();
|
|
1987
|
+
for (const entry of local) {
|
|
1988
|
+
if (entry.header.parentSession === undefined)
|
|
1989
|
+
continue;
|
|
1990
|
+
const list = childrenByParent.get(entry.header.parentSession);
|
|
1991
|
+
if (list === undefined)
|
|
1992
|
+
childrenByParent.set(entry.header.parentSession, [entry.header.id]);
|
|
1993
|
+
else
|
|
1994
|
+
list.push(entry.header.id);
|
|
1995
|
+
}
|
|
1996
|
+
const ancestorIds = [];
|
|
1997
|
+
{
|
|
1998
|
+
const visited = new Set([currentId]);
|
|
1999
|
+
let cursor = headerById.get(currentId);
|
|
2000
|
+
while (cursor?.header.parentSession !== undefined) {
|
|
2001
|
+
const parentId = cursor.header.parentSession;
|
|
2002
|
+
if (visited.has(parentId))
|
|
2003
|
+
break;
|
|
2004
|
+
visited.add(parentId);
|
|
2005
|
+
if (!headerById.has(parentId))
|
|
2006
|
+
break;
|
|
2007
|
+
ancestorIds.push(parentId);
|
|
2008
|
+
cursor = headerById.get(parentId);
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
// BFS from the topmost ancestor. The scan must NOT be gated by family
|
|
2012
|
+
// membership: ancestor-chain nodes are already in the family, and
|
|
2013
|
+
// skipping them here would never enumerate their other children —
|
|
2014
|
+
// siblings/cousins forking off a MIDDLE ancestor would be lost.
|
|
2015
|
+
const family = new Set([currentId, ...ancestorIds]);
|
|
2016
|
+
{
|
|
2017
|
+
const scanned = new Set();
|
|
2018
|
+
const queue = [ancestorIds.at(-1) ?? currentId];
|
|
2019
|
+
while (queue.length > 0) {
|
|
2020
|
+
const id = queue.shift();
|
|
2021
|
+
if (scanned.has(id))
|
|
2022
|
+
continue;
|
|
2023
|
+
scanned.add(id);
|
|
2024
|
+
family.add(id);
|
|
2025
|
+
for (const child of childrenByParent.get(id) ?? []) {
|
|
2026
|
+
queue.push(child);
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
// Processing order is TOPOLOGICAL (a parent before its children): the
|
|
2031
|
+
// coverage bookkeeping below — which seq range each chain already
|
|
2032
|
+
// shows — feeds the next read's inherited-prefix skip, so a parent
|
|
2033
|
+
// must be read before its forks. Within each sibling group the live
|
|
2034
|
+
// chain wins, then newest first (the same priority the read budget
|
|
2035
|
+
// always had).
|
|
2036
|
+
const ancestorSet = new Set(ancestorIds);
|
|
2037
|
+
const priorityOf = (a, b) => {
|
|
2038
|
+
const aChain = a === currentId || ancestorSet.has(a);
|
|
2039
|
+
const bChain = b === currentId || ancestorSet.has(b);
|
|
2040
|
+
if (aChain !== bChain)
|
|
2041
|
+
return aChain ? -1 : 1;
|
|
2042
|
+
return (headerById.get(b)?.header.createdAt ?? 0) - (headerById.get(a)?.header.createdAt ?? 0);
|
|
2043
|
+
};
|
|
2044
|
+
const kidsOf = new Map();
|
|
2045
|
+
const familyRoots = [];
|
|
2046
|
+
for (const id of family) {
|
|
2047
|
+
const parentId = headerById.get(id)?.header.parentSession;
|
|
2048
|
+
if (parentId !== undefined && parentId !== id && family.has(parentId)) {
|
|
2049
|
+
const list = kidsOf.get(parentId);
|
|
2050
|
+
if (list === undefined)
|
|
2051
|
+
kidsOf.set(parentId, [id]);
|
|
2052
|
+
else
|
|
2053
|
+
list.push(id);
|
|
2054
|
+
}
|
|
2055
|
+
else {
|
|
2056
|
+
familyRoots.push(id);
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
familyRoots.sort(priorityOf);
|
|
2060
|
+
for (const list of kidsOf.values())
|
|
2061
|
+
list.sort(priorityOf);
|
|
2062
|
+
const ordered = [];
|
|
2063
|
+
{
|
|
2064
|
+
const seen = new Set();
|
|
2065
|
+
const stack = [...familyRoots].reverse();
|
|
2066
|
+
while (stack.length > 0) {
|
|
2067
|
+
const id = stack.pop();
|
|
2068
|
+
if (seen.has(id))
|
|
2069
|
+
continue;
|
|
2070
|
+
seen.add(id);
|
|
2071
|
+
ordered.push(id);
|
|
2072
|
+
const kids = kidsOf.get(id);
|
|
2073
|
+
if (kids !== undefined) {
|
|
2074
|
+
for (let i = kids.length - 1; i >= 0; i--)
|
|
2075
|
+
stack.push(kids[i]);
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
// Cycle-broken leftovers (corrupt parent headers) — never drop one.
|
|
2079
|
+
for (const id of [...family].sort(priorityOf)) {
|
|
2080
|
+
if (!seen.has(id))
|
|
2081
|
+
ordered.push(id);
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
// Caps: the ancestor chain + live session ALWAYS stay selected — the
|
|
2085
|
+
// structural invariant (the live branch must reach the family root)
|
|
2086
|
+
// outranks the session cap, which therefore evicts only non-ancestors.
|
|
2087
|
+
// The event budget bounds the READ cost too: non-live logs decode
|
|
2088
|
+
// lazily and stop at the remaining budget (see below), the live
|
|
2089
|
+
// session keeps only its tail.
|
|
2090
|
+
const MAX_TREE_SESSIONS = 24;
|
|
2091
|
+
const MAX_TREE_EVENTS = 200_000;
|
|
2092
|
+
// The event budget alone does NOT bound read cost: skipped envelopes
|
|
2093
|
+
// (ignorable frames, headers) are paid for but never collected, so a
|
|
2094
|
+
// noisy log can return ZERO events and leave the next log a full scan
|
|
2095
|
+
// allowance — 23 logs × ~800k envelopes would block the TUI for
|
|
2096
|
+
// seconds. scanBudget caps the TOTAL envelopes inspected across all
|
|
2097
|
+
// logs (the reader reports its real scanned count); per-log caps
|
|
2098
|
+
// derive from the event budget as before, and the smaller of the two
|
|
2099
|
+
// applies, so one flood cannot starve every later sibling either.
|
|
2100
|
+
const MAX_TREE_SCANNED = defaultMaxScanned(MAX_TREE_EVENTS);
|
|
2101
|
+
let scanBudget = MAX_TREE_SCANNED;
|
|
2102
|
+
const selected = new Set();
|
|
2103
|
+
let slots = MAX_TREE_SESSIONS;
|
|
2104
|
+
for (const id of ordered) {
|
|
2105
|
+
const chain = id === currentId || ancestorSet.has(id);
|
|
2106
|
+
if (chain || slots > 0) {
|
|
2107
|
+
selected.add(id);
|
|
2108
|
+
if (!chain)
|
|
2109
|
+
slots -= 1;
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
// The live session's events come from memory (its header may not be
|
|
2113
|
+
// materialized yet — the jsonl backend writes on first append).
|
|
2114
|
+
const liveHeader = headerById.get(currentId);
|
|
2115
|
+
const familySessions = [];
|
|
2116
|
+
let truncated = selected.size < family.size;
|
|
2117
|
+
let eventBudget = 0;
|
|
2118
|
+
// Coverage bookkeeping: coveredThrough(S) = the highest K such that
|
|
2119
|
+
// [0..K] is already displayed by S's chain or an ancestor's. A fork's
|
|
2120
|
+
// inherited seed prefix duplicates that range, so non-live reads SKIP
|
|
2121
|
+
// it (the reader's skipBelowSeq): the prefix still costs scan budget
|
|
2122
|
+
// (its bytes are read and parsed) but NOT the event budget — a fork
|
|
2123
|
+
// of a huge parent pays only for its OWN events, so two small forks
|
|
2124
|
+
// of a 70k-event parent both stay visible. Unreadable/unloaded
|
|
2125
|
+
// sessions are transparent: they claim nothing beyond what their own
|
|
2126
|
+
// ancestors covered, so a fork of a dead branch dedups against the
|
|
2127
|
+
// grandparent instead of hiding its self-contained history.
|
|
2128
|
+
const coveredThrough = new Map();
|
|
2129
|
+
for (const id of ordered) {
|
|
2130
|
+
if (!selected.has(id))
|
|
2131
|
+
continue;
|
|
2132
|
+
const entry = headerById.get(id);
|
|
2133
|
+
if (id === currentId) {
|
|
2134
|
+
const liveParentId = liveHeader?.header.parentSession ?? liveMeta?.parentSession;
|
|
2135
|
+
const liveParent = liveParentId !== undefined ? String(liveParentId) : undefined;
|
|
2136
|
+
const parentCovered = liveParent !== undefined
|
|
2137
|
+
? (coveredThrough.get(liveParent) ?? -1)
|
|
2138
|
+
: -1;
|
|
2139
|
+
const liveEvents = liveSession.events;
|
|
2140
|
+
const remaining = Math.max(0, MAX_TREE_EVENTS - eventBudget);
|
|
2141
|
+
// The live session's in-memory log is SELF-CONTAINED: a fork's
|
|
2142
|
+
// events still carry the inherited seed prefix, which the parent's
|
|
2143
|
+
// chain already displays (and already charged to the budget).
|
|
2144
|
+
// Skipping it exactly like the non-live reads do keeps a live fork
|
|
2145
|
+
// of a huge parent from spending the whole family budget on
|
|
2146
|
+
// duplicated history and evicting its own siblings.
|
|
2147
|
+
const liveSeed = liveHeader?.header.seedLength ?? liveMeta?.seedLength;
|
|
2148
|
+
const skipBelow = liveParent !== undefined && liveSeed !== undefined
|
|
2149
|
+
? Math.min(liveSeed, parentCovered + 1)
|
|
2150
|
+
: 0;
|
|
2151
|
+
const own = skipBelow > 0 ? liveEvents.filter(event => event.seq >= skipBelow) : liveEvents;
|
|
2152
|
+
// A live session larger than the remaining budget keeps its TAIL,
|
|
2153
|
+
// aligned to whole turns (sessionTree.liveTailWindow): leftover
|
|
2154
|
+
// entries of a turn whose turn/start was cut away render as
|
|
2155
|
+
// selectable rows that can never rewind; a window holding no
|
|
2156
|
+
// turn/start at all (one oversized LAST turn spans the budget)
|
|
2157
|
+
// retries over the earlier complete turns instead of blacking the
|
|
2158
|
+
// session out. Rewind itself never reads this copy (rewindToNode
|
|
2159
|
+
// forks the real session), so the slice only narrows what the tree
|
|
2160
|
+
// can display.
|
|
2161
|
+
const events = liveTailWindow(own, remaining);
|
|
2162
|
+
// Charge the KEPT tail, not the in-memory length: extraction only
|
|
2163
|
+
// ever touches `events`, and charging the full log would black out
|
|
2164
|
+
// every other family member's budget behind a discarded prefix.
|
|
2165
|
+
eventBudget += events.length;
|
|
2166
|
+
if (events.length !== own.length)
|
|
2167
|
+
truncated = true;
|
|
2168
|
+
familySessions.push({
|
|
2169
|
+
id,
|
|
2170
|
+
createdAt: liveHeader?.header.createdAt ?? liveMeta?.createdAt ?? Date.now(),
|
|
2171
|
+
...(liveParent !== undefined ? { parentSession: liveParent } : {}),
|
|
2172
|
+
...(liveHeader?.header.seedLength !== undefined || liveMeta?.seedLength !== undefined
|
|
2173
|
+
? { seedLength: liveHeader?.header.seedLength ?? liveMeta.seedLength }
|
|
2174
|
+
: {}),
|
|
2175
|
+
events,
|
|
2176
|
+
live: true,
|
|
2177
|
+
// The in-memory log always reaches the tip (liveTailWindow trims
|
|
2178
|
+
// the head only), so the adopt/warning UX facts are derivable.
|
|
2179
|
+
tailComplete: true,
|
|
2180
|
+
});
|
|
2181
|
+
// A kept tail cut off the front connects to nothing — coverage
|
|
2182
|
+
// stays at the parent's (a fork of the live session re-reads the
|
|
2183
|
+
// hidden prefix from its own log).
|
|
2184
|
+
const firstKept = events.length > 0 ? events[0].seq : Number.POSITIVE_INFINITY;
|
|
2185
|
+
const lastKept = events.length > 0 ? events[events.length - 1].seq : -1;
|
|
2186
|
+
coveredThrough.set(id, firstKept <= parentCovered + 1 ? Math.max(parentCovered, lastKept) : parentCovered);
|
|
2187
|
+
continue;
|
|
2188
|
+
}
|
|
2189
|
+
const header = entry?.header;
|
|
2190
|
+
const parentId = header?.parentSession;
|
|
2191
|
+
const parentCovered = parentId !== undefined ? (coveredThrough.get(parentId) ?? -1) : -1;
|
|
2192
|
+
// Never skip past the seed prefix: events beyond it are this
|
|
2193
|
+
// session's OWN — no ancestor can show them. A parent that was never
|
|
2194
|
+
// read (evicted, or outside the family) covers nothing (skip 0).
|
|
2195
|
+
const skipBelow = parentId !== undefined && header?.seedLength !== undefined
|
|
2196
|
+
? Math.min(header.seedLength, parentCovered + 1)
|
|
2197
|
+
: 0;
|
|
2198
|
+
const facts = {
|
|
2199
|
+
id,
|
|
2200
|
+
createdAt: header?.createdAt ?? 0,
|
|
2201
|
+
...(parentId !== undefined ? { parentSession: parentId } : {}),
|
|
2202
|
+
...(header?.seedLength !== undefined ? { seedLength: header.seedLength } : {}),
|
|
2203
|
+
};
|
|
2204
|
+
if (eventBudget >= MAX_TREE_EVENTS || scanBudget <= 0) {
|
|
2205
|
+
// Budget spent: keep the STRUCTURE — the session degrades to an
|
|
2206
|
+
// unloaded placeholder so its branch (and any ancestor chain
|
|
2207
|
+
// through it) stays visible instead of vanishing from the tree.
|
|
2208
|
+
truncated = true;
|
|
2209
|
+
familySessions.push({ ...facts, events: [], live: false, unloaded: true });
|
|
2210
|
+
coveredThrough.set(id, parentCovered);
|
|
2211
|
+
continue;
|
|
2212
|
+
}
|
|
2213
|
+
// Read-only, tolerant, bounded: the compat reader decodes frames
|
|
2214
|
+
// lazily and stops at the remaining event budget. Browsing the tree
|
|
2215
|
+
// must never REWRITE history logs (the ignorable-marking repair
|
|
2216
|
+
// stays on the explicit resume/rewind path), and the strict backend
|
|
2217
|
+
// inspect would both reject third-party event types wholesale and
|
|
2218
|
+
// parse chunk-heavy logs whole. Header facts come from list().
|
|
2219
|
+
const remaining = MAX_TREE_EVENTS - eventBudget;
|
|
2220
|
+
// Source precedence, all read-only:
|
|
2221
|
+
// 1. persistence.locate — the backend's OWN artifact resolution is
|
|
2222
|
+
// authoritative (custom root, workspace-key scheme). When it
|
|
2223
|
+
// names a path, ONLY that file is read: falling back to a
|
|
2224
|
+
// same-id copy under the stock root could surface a STALE log
|
|
2225
|
+
// from another backend configuration. A locate miss or an ABSENT
|
|
2226
|
+
// file falls through to inspect, never to the stock scan.
|
|
2227
|
+
// 2. Stock root scan — only for backends WITHOUT locate (fakes,
|
|
2228
|
+
// older custom implementations).
|
|
2229
|
+
// 3. inspect — the backend's strict read (non-file backends), with
|
|
2230
|
+
// the same budget enforced on what we keep — and ONLY when the
|
|
2231
|
+
// file read found NOTHING (undefined). A read that failed on a
|
|
2232
|
+
// safety cap or corruption (failed) must never escalate here:
|
|
2233
|
+
// inspect parses the WHOLE log up front, so falling through
|
|
2234
|
+
// would re-read unboundedly exactly the logs the caps exist to
|
|
2235
|
+
// bound (64 MiB frames, decode bombs) — degrade to a placeholder
|
|
2236
|
+
// instead.
|
|
2237
|
+
let events;
|
|
2238
|
+
let complete = true;
|
|
2239
|
+
let failed = false;
|
|
2240
|
+
// First seq the chosen source actually covers: the file readers start
|
|
2241
|
+
// at the inherited-prefix skip, inspect always hands the whole log.
|
|
2242
|
+
let readFrom = 0;
|
|
2243
|
+
// Per-log scan allowance: the usual 4×-of-remaining derivation,
|
|
2244
|
+
// clamped to what the tree-level scan budget still has.
|
|
2245
|
+
const scanAllowance = Math.min(defaultMaxScanned(remaining), scanBudget);
|
|
2246
|
+
const locate = persistence.locate;
|
|
2247
|
+
const hasLocate = typeof locate === 'function';
|
|
2248
|
+
if (hasLocate && entry !== undefined) {
|
|
2249
|
+
let locatedPath;
|
|
2250
|
+
try {
|
|
2251
|
+
const location = locate.call(persistence, entry.raw);
|
|
2252
|
+
// Only the jsonl kind enters the compat file layer — a foreign
|
|
2253
|
+
// kind's artifact is the backend's own format (inspect below).
|
|
2254
|
+
if (location !== null && typeof location === 'object') {
|
|
2255
|
+
const record = location;
|
|
2256
|
+
if (record.kind === 'jsonl' && typeof record.path === 'string') {
|
|
2257
|
+
locatedPath = record.path;
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
catch {
|
|
2262
|
+
// Best effort — a locate hiccup falls through to inspect.
|
|
2263
|
+
}
|
|
2264
|
+
if (locatedPath !== undefined) {
|
|
2265
|
+
const viaPath = readSessionEventsFromFile(locatedPath, remaining, scanAllowance, skipBelow);
|
|
2266
|
+
if (viaPath !== undefined) {
|
|
2267
|
+
scanBudget -= viaPath.scanned;
|
|
2268
|
+
if (viaPath.failed === true)
|
|
2269
|
+
failed = true;
|
|
2270
|
+
else {
|
|
2271
|
+
events = viaPath.events;
|
|
2272
|
+
complete = viaPath.complete;
|
|
2273
|
+
readFrom = skipBelow;
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
else if (!hasLocate) {
|
|
2279
|
+
const read = readSessionEventsFromLog(id, remaining, scanAllowance, skipBelow);
|
|
2280
|
+
if (read !== undefined) {
|
|
2281
|
+
scanBudget -= read.scanned;
|
|
2282
|
+
if (read.failed === true)
|
|
2283
|
+
failed = true;
|
|
2284
|
+
else {
|
|
2285
|
+
events = read.events;
|
|
2286
|
+
complete = read.complete;
|
|
2287
|
+
readFrom = skipBelow;
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
if (!failed && events === undefined && typeof persistence.inspect === 'function') {
|
|
2292
|
+
try {
|
|
2293
|
+
const inspection = await persistence.inspect(SessionId(id));
|
|
2294
|
+
// inspect parses the WHOLE log up front: charge the full length
|
|
2295
|
+
// to the scan budget (may overdraw; the next iterations skip).
|
|
2296
|
+
scanBudget -= inspection.events.length;
|
|
2297
|
+
// Non-file backends hand back the self-contained log from seq 0:
|
|
2298
|
+
// the inherited-prefix skip the file readers got must apply here
|
|
2299
|
+
// too, or a long prefix would fill the slice and the branch's OWN
|
|
2300
|
+
// events — the only ones nobody else displays — would be cut.
|
|
2301
|
+
const all = skipBelow > 0 ? inspection.events.filter(event => event.seq >= skipBelow) : inspection.events;
|
|
2302
|
+
readFrom = skipBelow;
|
|
2303
|
+
events = all;
|
|
2304
|
+
if (events.length > remaining) {
|
|
2305
|
+
events = events.slice(0, remaining);
|
|
2306
|
+
complete = false;
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
catch {
|
|
2310
|
+
events = undefined;
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
if (failed || events === undefined) {
|
|
2314
|
+
// An unreadable log keeps the branch structure, no entries — and
|
|
2315
|
+
// stays transparent for coverage, so a fork of this branch dedups
|
|
2316
|
+
// against the grandparent instead of hiding its own history.
|
|
2317
|
+
familySessions.push({ ...facts, events: [], live: false, unreadable: true });
|
|
2318
|
+
coveredThrough.set(id, parentCovered);
|
|
2319
|
+
continue;
|
|
2320
|
+
}
|
|
2321
|
+
eventBudget += events.length;
|
|
2322
|
+
if (!complete)
|
|
2323
|
+
truncated = true;
|
|
2324
|
+
// tailComplete gates the branch-adopt target and the drop-turn
|
|
2325
|
+
// warning: a budget-sliced read lost the tail, and a tip computed
|
|
2326
|
+
// from it would fork mid-branch while claiming to keep everything.
|
|
2327
|
+
familySessions.push({ ...facts, events, live: false, ...(complete ? { tailComplete: true } : {}) });
|
|
2328
|
+
const lastRead = events.length > 0 ? events[events.length - 1].seq : -1;
|
|
2329
|
+
coveredThrough.set(id, readFrom <= parentCovered + 1 ? Math.max(parentCovered, lastRead) : parentCovered);
|
|
2330
|
+
}
|
|
2331
|
+
// A session swap mid-build invalidates the whole assembly (it mixes
|
|
2332
|
+
// the snapshot's lineage with headers listed for the OLD cwd state):
|
|
2333
|
+
// drop it silently — the reopened tree rebuilds on the new session.
|
|
2334
|
+
if (agent.session !== liveSession)
|
|
2335
|
+
return null;
|
|
2336
|
+
return buildSessionTree(familySessions, currentId, truncated);
|
|
2337
|
+
},
|
|
2338
|
+
async rewindToNode(sessionId, seq, mode = 'rewind') {
|
|
2339
|
+
const agents = ctx.get('agents');
|
|
2340
|
+
if (!agents) {
|
|
2341
|
+
state.notify(t('rewind-unavailable'), { color: 'error' });
|
|
2342
|
+
return null;
|
|
2343
|
+
}
|
|
2344
|
+
// Pin the entry-time session: the awaits below (log load, preset
|
|
2345
|
+
// compose, agent create) are windows in which a queued switch
|
|
2346
|
+
// (/new, /resume, /model) can swap `agent` — the mutation queue only
|
|
2347
|
+
// serializes the REPLACING entries, so the boundary and restored text
|
|
2348
|
+
// derive from THIS session's log and any swap along the way aborts
|
|
2349
|
+
// the rewind (forking or disposing whatever agent happens to be
|
|
2350
|
+
// current at the end would rewind the wrong session).
|
|
2351
|
+
const entrySession = agent.session;
|
|
2352
|
+
const currentId = String(entrySession.id);
|
|
2353
|
+
const childId = SessionId(randomUUID());
|
|
2354
|
+
// Source events: the live session from memory; any other family member
|
|
2355
|
+
// from its durable log (legacy event types registered first — the same
|
|
2356
|
+
// in-process compat seam as resumeTo, since load validates known types).
|
|
2357
|
+
let sourceEvents;
|
|
2358
|
+
let sourceCwd = state.cwd;
|
|
2359
|
+
let forkFromLive = true;
|
|
2360
|
+
if (sessionId === currentId) {
|
|
2361
|
+
sourceEvents = entrySession.events;
|
|
2362
|
+
}
|
|
2363
|
+
else {
|
|
2364
|
+
forkFromLive = false;
|
|
2365
|
+
const persistence = ctx.get('sessionPersistence');
|
|
2366
|
+
if (!persistence || typeof persistence.load !== 'function') {
|
|
2367
|
+
state.notify(t('rewind-no-persistence'), { color: 'error' });
|
|
2368
|
+
return null;
|
|
2369
|
+
}
|
|
2370
|
+
try {
|
|
2371
|
+
ensureLegacySessionEventTypes();
|
|
2372
|
+
const loaded = await persistence.load(SessionId(sessionId));
|
|
2373
|
+
sourceEvents = loaded.events;
|
|
2374
|
+
sourceCwd = loaded.meta.cwd ?? state.cwd;
|
|
2375
|
+
}
|
|
2376
|
+
catch (error) {
|
|
2377
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2378
|
+
state.notify(t('rewind-load-failed', { err: message }), { color: 'error' });
|
|
2379
|
+
return null;
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
// DSH event order is `turn/start → user/message → … → turn/end`, and a
|
|
2383
|
+
// fork seed must not end inside an open turn. pi's navigateTree
|
|
2384
|
+
// semantics mapped onto that constraint (sessionTree.rewindTarget): a
|
|
2385
|
+
// USER message drops its turn — the boundary sits just before the
|
|
2386
|
+
// turn/start and the prompt comes back into the input for re-editing;
|
|
2387
|
+
// any OTHER entry keeps through its enclosing STEP — a mid-turn cut at
|
|
2388
|
+
// the step/end with the turn closed synthetically (DSH agentic turns
|
|
2389
|
+
// span thousands of events, so turn-granular keeping would barely move
|
|
2390
|
+
// the visible history). Fork mode (pi's /fork) instead KEEPS the
|
|
2391
|
+
// picked entry: a user message cuts right after itself (the turn's
|
|
2392
|
+
// reply drops) and never returns text to the input.
|
|
2393
|
+
const target = mode === 'fork'
|
|
2394
|
+
? forkTarget(sourceEvents, seq)
|
|
2395
|
+
: rewindTarget(sourceEvents, seq);
|
|
2396
|
+
const boundary = target.boundary;
|
|
2397
|
+
if (boundary < 0) {
|
|
2398
|
+
state.notify(t('rewind-first-message'), { color: 'error' });
|
|
2399
|
+
return null;
|
|
2400
|
+
}
|
|
2401
|
+
// Keeping the entry can still be a NO-OP: when nothing message-bearing
|
|
2402
|
+
// follows the boundary (only a turn/end, or nothing at all), the fork's
|
|
2403
|
+
// transcript would be identical to the live one. pi truncates to right
|
|
2404
|
+
// after the entry; DSH's step/turn-closed seed cannot always express
|
|
2405
|
+
// that, so the honest answer is to say there is nothing to rewind. A
|
|
2406
|
+
// DEAD session's tip still forks: that adopts the branch, a real
|
|
2407
|
+
// switch.
|
|
2408
|
+
if (forkFromLive && !sourceEvents.some(event => event.seq > boundary &&
|
|
2409
|
+
(event.type === 'user/message' || event.type === 'assistant/message' ||
|
|
2410
|
+
event.type === 'tool/call' || event.type === 'tool/result'))) {
|
|
2411
|
+
state.notify(t('rewind-noop'), { color: 'warning' });
|
|
2412
|
+
return null;
|
|
2413
|
+
}
|
|
2414
|
+
// The dropped turn's own prompt text, restored into the input after
|
|
2415
|
+
// the swap ('' whenever the entry was kept — fork mode included — or
|
|
2416
|
+
// the turn had no human-typed text to restore).
|
|
2417
|
+
const restoredText = mode === 'fork' ? '' : turnUserText(sourceEvents, seq);
|
|
2418
|
+
// The fork continues under the source session's own preset: switches
|
|
2419
|
+
// are blank-only, so every `agent-preset/selected` event predates any
|
|
2420
|
+
// rewind boundary and the source log resolves the exact composition.
|
|
2421
|
+
// The route likewise stays the live one — a rewind continues the same
|
|
2422
|
+
// conversation, so a `/model` switch must survive it (issue #30).
|
|
2423
|
+
const sourcePreset = forkFromLive
|
|
2424
|
+
? runningPresetOf(entrySession)
|
|
2425
|
+
: ((await resolvePersistedPreset(ctx, SessionId(sessionId))) ?? runningPresetOf(entrySession));
|
|
2426
|
+
const rewindComposed = await composePreset(ctx, sourcePreset);
|
|
2427
|
+
// Everything fallible is done — only NOW stop a running turn (a load
|
|
2428
|
+
// or preset failure above must not kill it). But bail first when the
|
|
2429
|
+
// live session was swapped during those awaits: cancelling/forking
|
|
2430
|
+
// now would hit the NEW session with THIS session's boundary.
|
|
2431
|
+
if (agent.session !== entrySession) {
|
|
2432
|
+
state.notify(t('rewind-session-changed'), { color: 'error' });
|
|
2433
|
+
return null;
|
|
2434
|
+
}
|
|
2435
|
+
// Stop a running turn first and WAIT for its turn/end to land: fork
|
|
2436
|
+
// rejects boundaries inside open turns, and Agent.cancel() closes the
|
|
2437
|
+
// turn asynchronously (a long thinking turn can take seconds to
|
|
2438
|
+
// settle). Cross-session rewinds need this too: the live agent is
|
|
2439
|
+
// about to be disposed, and its turn must close cleanly.
|
|
2440
|
+
const wasWorking = state.working;
|
|
2441
|
+
const cancelSeq = agent.session.seq;
|
|
2442
|
+
if (wasWorking)
|
|
2443
|
+
agent.cancel({ kind: 'user' });
|
|
2444
|
+
if (wasWorking) {
|
|
2445
|
+
const turnSettled = await waitForTurnEnd(agent.session, cancelSeq, 30000);
|
|
2446
|
+
if (!turnSettled) {
|
|
2447
|
+
state.notify(t('rewind-settling'), { color: 'error' });
|
|
2448
|
+
return null;
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
// Slice the seed from the PINNED event snapshot. Never sessions.fork
|
|
2452
|
+
// here: fork() rejects a boundary inside an open turn, which is
|
|
2453
|
+
// exactly where a keep-style cut lands (closeTurn set) — close it
|
|
2454
|
+
// with the exact event a real user interrupt writes instead (the
|
|
2455
|
+
// persistence layer closes crash-orphaned turns the same way).
|
|
2456
|
+
// agents.create validates the result itself (contiguous from seq 0,
|
|
2457
|
+
// no open turns).
|
|
2458
|
+
const seed = sourceEvents.filter(event => event.seq <= boundary);
|
|
2459
|
+
if (target.closeTurn !== undefined) {
|
|
2460
|
+
const last = seed[seed.length - 1];
|
|
2461
|
+
if (last !== undefined) {
|
|
2462
|
+
seed.push({
|
|
2463
|
+
type: 'turn/end',
|
|
2464
|
+
seq: last.seq + 1,
|
|
2465
|
+
time: last.time + 1,
|
|
2466
|
+
data: { turn: target.closeTurn, reason: { kind: 'aborted', reason: { kind: 'user' } } },
|
|
2467
|
+
});
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
let handle;
|
|
2471
|
+
try {
|
|
2472
|
+
handle = await agents.create({
|
|
2473
|
+
sessionId: childId,
|
|
2474
|
+
seed,
|
|
2475
|
+
meta: {
|
|
2476
|
+
cwd: sourceCwd,
|
|
2477
|
+
parentSession: SessionId(sessionId),
|
|
2478
|
+
seedLength: seed.length,
|
|
2479
|
+
...(rewindComposed.agentPreset === undefined
|
|
2480
|
+
? {}
|
|
2481
|
+
: { agentPreset: rewindComposed.agentPreset }),
|
|
2482
|
+
},
|
|
2483
|
+
agentOptions: { provider: state.provider, model: state.model },
|
|
2484
|
+
...(rewindComposed.setup === undefined ? {} : { setup: rewindComposed.setup }),
|
|
2485
|
+
});
|
|
2486
|
+
}
|
|
2487
|
+
catch {
|
|
2488
|
+
state.notify(t('rewind-create-failed'), { color: 'error' });
|
|
2489
|
+
return null;
|
|
2490
|
+
}
|
|
2491
|
+
try {
|
|
2492
|
+
await attachSessionToWorkspace(ctx, sourceCwd, childId);
|
|
2493
|
+
}
|
|
2494
|
+
catch (error) {
|
|
2495
|
+
state.notify(t('rewind-attach-failed', { err: error instanceof Error ? error.message : String(error) }), { color: 'warning', timeoutMs: 8000 });
|
|
2496
|
+
}
|
|
2497
|
+
// The create await was another swap window: adopting now would dispose
|
|
2498
|
+
// the NEW session's agent. Free the fork we just made and bail.
|
|
2499
|
+
if (agent.session !== entrySession) {
|
|
2500
|
+
void handle.dispose().catch(() => { });
|
|
2501
|
+
state.notify(t('rewind-session-changed'), { color: 'error' });
|
|
2502
|
+
return null;
|
|
2503
|
+
}
|
|
2504
|
+
// Replay the forked history into a fresh transcript (the same swap
|
|
2505
|
+
// tail rewindTo runs), then announce the session switch.
|
|
2506
|
+
const sourceSessionId = adoptForkedAgent(handle, seed, rewindComposed.agentPreset, childId);
|
|
2507
|
+
notifySessionSwitched(mode === 'fork' ? 'fork' : 'rewind', String(childId), sourceSessionId);
|
|
2508
|
+
return restoredText;
|
|
2509
|
+
},
|
|
2510
|
+
async forkSession() {
|
|
2511
|
+
const sessions = ctx.get('sessions');
|
|
2512
|
+
const agents = ctx.get('agents');
|
|
2513
|
+
if (!sessions || !agents) {
|
|
2514
|
+
state.notify(t('fork-unavailable'), { color: 'error' });
|
|
2515
|
+
return false;
|
|
2516
|
+
}
|
|
2517
|
+
// kimi-code /fork semantics: refuse mid-turn instead of cancelling —
|
|
2518
|
+
// the fork must not surprise the user by killing their running turn,
|
|
2519
|
+
// and sessions.fork rejects an open-turn log anyway.
|
|
2520
|
+
if (state.working) {
|
|
2521
|
+
state.notify(t('fork-while-working'), { color: 'warning' });
|
|
2522
|
+
return false;
|
|
2523
|
+
}
|
|
2524
|
+
const source = agent.session;
|
|
2525
|
+
const childId = SessionId(randomUUID());
|
|
2526
|
+
// No boundary: the whole (turn-closed) log. Slice via sessions.fork for
|
|
2527
|
+
// the same validation the rewind path gets, never sessions.fork's
|
|
2528
|
+
// session-storing sibling — agents.create must own the new session.
|
|
2529
|
+
let seed;
|
|
2530
|
+
try {
|
|
2531
|
+
seed = sessions.fork(source).events;
|
|
2532
|
+
}
|
|
2533
|
+
catch (error) {
|
|
2534
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2535
|
+
state.notify(t('fork-failed', { err: message }), { color: 'error' });
|
|
2536
|
+
return false;
|
|
2537
|
+
}
|
|
2538
|
+
// Same preset/route rule as a rewind fork: the source log's own
|
|
2539
|
+
// composition, the live route (a /model switch survives forking).
|
|
2540
|
+
const forkComposed = await composePreset(ctx, runningPresetOf(source));
|
|
2541
|
+
let handle;
|
|
2542
|
+
try {
|
|
2543
|
+
handle = await agents.create({
|
|
2544
|
+
sessionId: childId,
|
|
2545
|
+
seed,
|
|
2546
|
+
meta: {
|
|
2547
|
+
cwd: state.cwd,
|
|
2548
|
+
// NO parentSession: a /fork copy is an independent conversation
|
|
2549
|
+
// (kimi-code semantics — a copy of the message list under a new
|
|
2550
|
+
// root session, like /new plus the history), not a rewind branch.
|
|
2551
|
+
// Recording lineage would fold it into the source's family in
|
|
2552
|
+
// /resume and the user would never find it.
|
|
2553
|
+
seedLength: seed.length,
|
|
2554
|
+
...(forkComposed.agentPreset === undefined
|
|
2555
|
+
? {}
|
|
2556
|
+
: { agentPreset: forkComposed.agentPreset }),
|
|
2557
|
+
},
|
|
2558
|
+
agentOptions: { provider: state.provider, model: state.model },
|
|
2559
|
+
...(forkComposed.setup === undefined ? {} : { setup: forkComposed.setup }),
|
|
2560
|
+
});
|
|
2561
|
+
}
|
|
2562
|
+
catch {
|
|
2563
|
+
state.notify(t('fork-create-failed'), { color: 'error' });
|
|
2564
|
+
return false;
|
|
2565
|
+
}
|
|
2566
|
+
// STAY in the source session: adopting the fork would dispose the live
|
|
2567
|
+
// agent (killing its in-flight turn and background tasks) — the fork is
|
|
2568
|
+
// an independent copy the user enters via /resume or the printed resume
|
|
2569
|
+
// command. The teardown order matters:
|
|
2570
|
+
// 1. attach while the fork's agent is still LIVE — the workspace's
|
|
2571
|
+
// header read resolves live sessions from the registry, so attaching
|
|
2572
|
+
// after dispose races the persistence index and can fail with
|
|
2573
|
+
// "cannot validate session".
|
|
2574
|
+
// 2. await the dispose so the seed log finishes flushing…
|
|
2575
|
+
// 3. …then append the Fork: title — appending mid-flush races the
|
|
2576
|
+
// writer and the frame is silently dropped.
|
|
2577
|
+
try {
|
|
2578
|
+
await attachSessionToWorkspace(ctx, state.cwd, childId);
|
|
2579
|
+
}
|
|
2580
|
+
catch (error) {
|
|
2581
|
+
state.notify(t('fork-attach-failed', { err: error instanceof Error ? error.message : String(error) }), { color: 'warning', timeoutMs: 8000 });
|
|
2582
|
+
}
|
|
2583
|
+
try {
|
|
2584
|
+
await handle.dispose();
|
|
2585
|
+
}
|
|
2586
|
+
catch (error) {
|
|
2587
|
+
ctx.logger.warn('dsh-tui: forked session dispose failed: %o', error);
|
|
2588
|
+
}
|
|
2589
|
+
// kimi's naming convention: the fork wears `Fork: <source title>` (the
|
|
2590
|
+
// prefix stays English in both locales). Best effort — a backend whose
|
|
2591
|
+
// log the compat layer cannot reach just leaves the fork untitled.
|
|
2592
|
+
const sourceTitle = state.sessionTitle.trim();
|
|
2593
|
+
appendSessionTitle(String(childId), `Fork: ${sourceTitle === '' ? String(source.id).slice(0, 8) : sourceTitle}`);
|
|
2594
|
+
// The same resume-command shape the exit hint prints (plugin.ts
|
|
2595
|
+
// resumeCommand): DSH_TUI_RESUME_SESSION + the boot profile.
|
|
2596
|
+
const profile = resolveDshProfileName();
|
|
2597
|
+
const boot = profile === undefined ? 'dsh --config cordis.yml' : `dsh --profile ${profile}`;
|
|
2598
|
+
const command = process.platform === 'win32'
|
|
2599
|
+
? `dsh-tui --resume ${childId}`
|
|
2600
|
+
: `DSH_TUI_RESUME_SESSION=${childId} ${boot}`;
|
|
2601
|
+
state.notify(t('fork-done', { id: String(childId), command }), { timeoutMs: 8000 });
|
|
2602
|
+
return true;
|
|
2603
|
+
},
|
|
1612
2604
|
async resumeTo(sessionId) {
|
|
1613
2605
|
// Switch the live agent to a persisted session: /resume picker Enter
|
|
1614
2606
|
// loads the history immediately (the `--resume` launcher path keeps
|
|
@@ -1647,10 +2639,20 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1647
2639
|
provider: options.configuredProvider,
|
|
1648
2640
|
model: options.configuredModel,
|
|
1649
2641
|
});
|
|
2642
|
+
// The recorded route feeds back into agentOptions too — not just the
|
|
2643
|
+
// status line below: a provider-only cordis.yml pin (issue #67) leaves
|
|
2644
|
+
// agentOptions.model undefined on resume, which breaks the `{{model}}`
|
|
2645
|
+
// persona variable for the resumed agent's own assembly AND for every
|
|
2646
|
+
// subagent it spawns (dsh-subagent's resolveChildAgentOptions inherits
|
|
2647
|
+
// `parent.options.model`).
|
|
2648
|
+
const recordedRoute = await resolvePersistedRoute(ctx, SessionId(sessionId));
|
|
1650
2649
|
try {
|
|
1651
2650
|
handle = await agents.resume({
|
|
1652
2651
|
resumeSessionId: SessionId(sessionId),
|
|
1653
|
-
agentOptions: {
|
|
2652
|
+
agentOptions: {
|
|
2653
|
+
provider: resumeRoute?.provider ?? recordedRoute?.provider,
|
|
2654
|
+
model: resumeRoute?.model ?? recordedRoute?.model,
|
|
2655
|
+
},
|
|
1654
2656
|
...(resumeComposed.setup === undefined ? {} : { setup: resumeComposed.setup }),
|
|
1655
2657
|
});
|
|
1656
2658
|
}
|
|
@@ -1689,6 +2691,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1689
2691
|
state.pending = [];
|
|
1690
2692
|
state.goal = undefined;
|
|
1691
2693
|
state.sessionTitle = '';
|
|
2694
|
+
state.sessionColor = '';
|
|
1692
2695
|
state.tokens = { input: 0, output: 0 };
|
|
1693
2696
|
state.responseChars = 0;
|
|
1694
2697
|
state.activeToolCount = 0;
|
|
@@ -1856,6 +2859,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1856
2859
|
state.pending = [];
|
|
1857
2860
|
state.goal = undefined;
|
|
1858
2861
|
state.sessionTitle = '';
|
|
2862
|
+
state.sessionColor = '';
|
|
1859
2863
|
state.tokens = { input: 0, output: 0 };
|
|
1860
2864
|
state.responseChars = 0;
|
|
1861
2865
|
state.activeToolCount = 0;
|
|
@@ -2039,6 +3043,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
2039
3043
|
state.pending = [];
|
|
2040
3044
|
state.goal = undefined;
|
|
2041
3045
|
state.sessionTitle = '';
|
|
3046
|
+
state.sessionColor = '';
|
|
2042
3047
|
state.tokens = { input: 0, output: 0 };
|
|
2043
3048
|
state.responseChars = 0;
|
|
2044
3049
|
state.activeToolCount = 0;
|
|
@@ -2050,6 +3055,11 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
2050
3055
|
state.agentPreset = modelComposed.agentPreset;
|
|
2051
3056
|
state.model = model;
|
|
2052
3057
|
state.provider = provider;
|
|
3058
|
+
// /model completion cache: the [current] tag was resolved at fetch
|
|
3059
|
+
// time — drop the cache so the next `/model ` refetches for the new
|
|
3060
|
+
// route.
|
|
3061
|
+
modelNodeCache.nodes = undefined;
|
|
3062
|
+
modelNodeCache.load = undefined;
|
|
2053
3063
|
state.tps = undefined;
|
|
2054
3064
|
state.tpsSamples = [];
|
|
2055
3065
|
state.lastUsage = undefined;
|
|
@@ -2160,6 +3170,19 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
2160
3170
|
state.toolBackground = normalized;
|
|
2161
3171
|
state.emit();
|
|
2162
3172
|
},
|
|
3173
|
+
setScrollGutter(mode) {
|
|
3174
|
+
const normalized = normalizeScrollGutter(mode);
|
|
3175
|
+
if (normalized === state.scrollGutter)
|
|
3176
|
+
return;
|
|
3177
|
+
state.scrollGutter = normalized;
|
|
3178
|
+
state.emit();
|
|
3179
|
+
},
|
|
3180
|
+
setPromptSessionLabel(enabled) {
|
|
3181
|
+
if (enabled === state.promptSessionLabel)
|
|
3182
|
+
return;
|
|
3183
|
+
state.promptSessionLabel = enabled;
|
|
3184
|
+
state.emit();
|
|
3185
|
+
},
|
|
2163
3186
|
setStatusBar(config) {
|
|
2164
3187
|
const next = normalizeStatusBar({ ...state.statusBar, ...config });
|
|
2165
3188
|
const changed = Object.keys(next).some(key => next[key] !== state.statusBar[key]);
|
|
@@ -2290,6 +3313,12 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
2290
3313
|
return Promise.all(providers.map(provider => llm.listModels(provider.id).catch(() => [])))
|
|
2291
3314
|
.then(lists => lists.flat());
|
|
2292
3315
|
},
|
|
3316
|
+
listProviders() {
|
|
3317
|
+
// Group labels for the two-level /model picker: the registry's own
|
|
3318
|
+
// display names, detached so a registry swap cannot leak through.
|
|
3319
|
+
const llm = ctx.get('llm');
|
|
3320
|
+
return Promise.resolve(llm === undefined ? [] : llm.listProviders().map(info => ({ ...info })));
|
|
3321
|
+
},
|
|
2293
3322
|
async listSkills() {
|
|
2294
3323
|
// snapshot() over list(): only a COMPLETE observation is authoritative
|
|
2295
3324
|
// (same contract as the skill-command merge above) — a partial catalog
|
|
@@ -2386,7 +3415,12 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
2386
3415
|
return undefined;
|
|
2387
3416
|
}
|
|
2388
3417
|
const revision = () => settings.describe().find(descriptor => descriptor.ns === 'llm-pi-ai')?.revision;
|
|
3418
|
+
// The OAuth sign-in surface (dsh-auth-style plugin), structural and
|
|
3419
|
+
// optional: mounting the plugin lights up the wizard's OAuth branch,
|
|
3420
|
+
// and without it the wizard is exactly what it was before.
|
|
3421
|
+
const oauthApi = ctx.get('dshAuth')?.api;
|
|
2389
3422
|
return {
|
|
3423
|
+
...(oauthApi === undefined ? {} : { oauth: oauthApi }),
|
|
2390
3424
|
listCatalogProviders() {
|
|
2391
3425
|
// declared === true marks routes the adapter knows only because a
|
|
2392
3426
|
// stored profile names them (user-added); the rest are activatable
|
|
@@ -2432,6 +3466,12 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
2432
3466
|
},
|
|
2433
3467
|
};
|
|
2434
3468
|
},
|
|
3469
|
+
async oauthProviderStatuses() {
|
|
3470
|
+
// Same optional seam the wizard's OAuth branch reads: absent plugin →
|
|
3471
|
+
// undefined, and `/login` renders exactly its pre-plugin lines.
|
|
3472
|
+
const api = ctx.get('dshAuth')?.api;
|
|
3473
|
+
return api === undefined ? undefined : api.providers();
|
|
3474
|
+
},
|
|
2435
3475
|
async sideQuestion(question, options) {
|
|
2436
3476
|
// CC /btw:无工具单轮辅助调用,重放 deriveMessages() 前缀 + 一条
|
|
2437
3477
|
// 包装问题。tools 永不传(侧问无工具是核心语义);usage 不回收
|
|
@@ -2524,6 +3564,61 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
2524
3564
|
state.sessionTitle = title;
|
|
2525
3565
|
state.emit();
|
|
2526
3566
|
},
|
|
3567
|
+
setSessionColor(color) {
|
|
3568
|
+
// `session/color` is a dsh-tui plugin event — not in dsh-session's
|
|
3569
|
+
// typed union, so appended through the same cast applyMode uses for
|
|
3570
|
+
// its sandbox/approval overrides. It replays on resume/rewind like
|
|
3571
|
+
// session/title, keeping each session's accent color its own.
|
|
3572
|
+
;
|
|
3573
|
+
agent.session
|
|
3574
|
+
.append('session/color', { color });
|
|
3575
|
+
state.sessionColor = color;
|
|
3576
|
+
state.emit();
|
|
3577
|
+
},
|
|
3578
|
+
async recapRecent(options) {
|
|
3579
|
+
// `/recap` (pi-recap semantics): one tool-less LLM call over the
|
|
3580
|
+
// session's TAIL exchanges — unlike /btw it does not replay the full
|
|
3581
|
+
// derived history (the excerpt IS the payload), so it stays cheap.
|
|
3582
|
+
// The answer is pure UI state: never appended to the session log.
|
|
3583
|
+
const llm = ctx.get('llm');
|
|
3584
|
+
if (!llm)
|
|
3585
|
+
return { summary: null, error: t('recap-llm-unavailable') };
|
|
3586
|
+
const header = agent.session.requestHeader();
|
|
3587
|
+
const config = header?.config;
|
|
3588
|
+
const activity = collectRecentActivity(agent.session.events, RECAP_RECENT_CHARS);
|
|
3589
|
+
if (activity === '')
|
|
3590
|
+
return { summary: null, error: t('recap-no-activity') };
|
|
3591
|
+
const messages = [
|
|
3592
|
+
createUserMessage({
|
|
3593
|
+
content: [{ type: 'text', text: wrapRecapPrompt(activity) }],
|
|
3594
|
+
source: { kind: 'plugin', plugin: 'dsh-tui/recap' },
|
|
3595
|
+
}),
|
|
3596
|
+
];
|
|
3597
|
+
const request = {
|
|
3598
|
+
provider: config?.provider ?? state.provider,
|
|
3599
|
+
model: config?.model ?? state.model,
|
|
3600
|
+
messages,
|
|
3601
|
+
...(header?.system !== undefined && { system: header.system }),
|
|
3602
|
+
...(config?.reasoningEffort !== undefined && { reasoningEffort: config.reasoningEffort }),
|
|
3603
|
+
...(config?.temperature !== undefined && { temperature: config.temperature }),
|
|
3604
|
+
...(config?.maxTokens !== undefined && { maxTokens: config.maxTokens }),
|
|
3605
|
+
...(config?.stop !== undefined && { stop: [...config.stop] }),
|
|
3606
|
+
sessionId: agent.session.id,
|
|
3607
|
+
...(options?.signal && { signal: options.signal }),
|
|
3608
|
+
};
|
|
3609
|
+
const outcome = await runSideQuestion({
|
|
3610
|
+
stream: llm.stream.bind(llm),
|
|
3611
|
+
options: request,
|
|
3612
|
+
onText: options?.onText,
|
|
3613
|
+
signal: options?.signal,
|
|
3614
|
+
});
|
|
3615
|
+
if (outcome.answer === null)
|
|
3616
|
+
return { summary: null, error: outcome.error };
|
|
3617
|
+
const parsed = parseRecapResponse(outcome.answer);
|
|
3618
|
+
return parsed.title === undefined
|
|
3619
|
+
? { summary: parsed.summary }
|
|
3620
|
+
: { summary: parsed.summary, title: parsed.title };
|
|
3621
|
+
},
|
|
2527
3622
|
async deleteSession(sessionId) {
|
|
2528
3623
|
// The live session's log is still being appended by this process —
|
|
2529
3624
|
// deleting it from under the writer is never offered in the picker
|
|
@@ -2619,7 +3714,13 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
2619
3714
|
.catch((error) => {
|
|
2620
3715
|
state.notify(t('compact-failed', { err: error instanceof Error ? error.message : String(error) }), { color: 'error', timeoutMs: 8000 });
|
|
2621
3716
|
});
|
|
2622
|
-
})()
|
|
3717
|
+
})().catch((error) => {
|
|
3718
|
+
// Sync throws from compactNow (e.g. runMaintenance rejecting a
|
|
3719
|
+
// non-idle agent right after /resume) reject this IIFE itself;
|
|
3720
|
+
// uncaught, that is an unhandled rejection and Node exits the
|
|
3721
|
+
// whole TUI. Surface it as the same failure notification.
|
|
3722
|
+
state.notify(t('compact-failed', { err: error instanceof Error ? error.message : String(error) }), { color: 'error', timeoutMs: 8000 });
|
|
3723
|
+
});
|
|
2623
3724
|
},
|
|
2624
3725
|
runExternalCommand(name, rawInput) {
|
|
2625
3726
|
return executeRegistryCommand(name, rawInput);
|
|
@@ -3341,6 +4442,12 @@ ${output}
|
|
|
3341
4442
|
* its result-time contextual diff back from here). */
|
|
3342
4443
|
const presentResultView = (name, rawArgs, data) => {
|
|
3343
4444
|
try {
|
|
4445
|
+
// Harness goal/todo tools first: their raw JSON reads as noise in the
|
|
4446
|
+
// transcript — fold recognizable shapes into a summary card before the
|
|
4447
|
+
// registry gets a chance to (not) know them.
|
|
4448
|
+
const local = harnessToolResultView(name, data);
|
|
4449
|
+
if (local !== undefined)
|
|
4450
|
+
return local;
|
|
3344
4451
|
const tool = toolsRegistry?.get(name, agent);
|
|
3345
4452
|
if (tool?.presentResult === undefined)
|
|
3346
4453
|
return undefined;
|
|
@@ -3473,9 +4580,21 @@ ${output}
|
|
|
3473
4580
|
state.spinnerMode = 'requesting';
|
|
3474
4581
|
}
|
|
3475
4582
|
};
|
|
4583
|
+
/** Fold one goal mutation into the channel's goal projection. */
|
|
4584
|
+
const applyGoalChange = (change) => {
|
|
4585
|
+
if (change.operation === 'clear') {
|
|
4586
|
+
state.goal = undefined;
|
|
4587
|
+
}
|
|
4588
|
+
else if (change.goal !== undefined) {
|
|
4589
|
+
state.goal = {
|
|
4590
|
+
...change.goal,
|
|
4591
|
+
roundsStarted: change.roundsStarted ?? state.goal?.roundsStarted ?? 0,
|
|
4592
|
+
};
|
|
4593
|
+
}
|
|
4594
|
+
};
|
|
3476
4595
|
/**
|
|
3477
4596
|
* Fold one goal-sourced message into the channel's goal projection.
|
|
3478
|
-
* Round-zero goal messages carry the full durable snapshot (or a clear
|
|
4597
|
+
* Round-zero goal messages may carry the full durable snapshot (or a clear
|
|
3479
4598
|
* tombstone) in their source; positive-round messages are admitted
|
|
3480
4599
|
* continuation prompts that only advance the rounds counter.
|
|
3481
4600
|
*/
|
|
@@ -3495,15 +4614,7 @@ ${output}
|
|
|
3495
4614
|
// oxlint-disable-next-line typescript/no-unnecessary-condition -- durable replay data may not match the static type
|
|
3496
4615
|
if (change === undefined || change.kind !== 'goal/change')
|
|
3497
4616
|
return;
|
|
3498
|
-
|
|
3499
|
-
state.goal = undefined;
|
|
3500
|
-
}
|
|
3501
|
-
else if (change.goal !== undefined) {
|
|
3502
|
-
state.goal = {
|
|
3503
|
-
...change.goal,
|
|
3504
|
-
roundsStarted: change.roundsStarted ?? state.goal?.roundsStarted ?? 0,
|
|
3505
|
-
};
|
|
3506
|
-
}
|
|
4617
|
+
applyGoalChange(change);
|
|
3507
4618
|
};
|
|
3508
4619
|
/** True while the durable transcript is being replayed (boot /resume /
|
|
3509
4620
|
* rewind / model-switch fork). The assistant/message reasoning-rebuild
|
|
@@ -3531,6 +4642,15 @@ ${output}
|
|
|
3531
4642
|
}
|
|
3532
4643
|
};
|
|
3533
4644
|
const renderEvent = (event) => {
|
|
4645
|
+
// Top-level `goal/change` events are how the goal service actually
|
|
4646
|
+
// records durable goal mutations (create/edit/pause/resume/complete/
|
|
4647
|
+
// block/clear) — confirmed in production logs. The pinned peer's
|
|
4648
|
+
// SessionEvent union predates the type, so admit it structurally: the
|
|
4649
|
+
// goal chip and panel stay dark without this fold.
|
|
4650
|
+
if (event.type === 'goal/change') {
|
|
4651
|
+
applyGoalChange(event.data);
|
|
4652
|
+
return;
|
|
4653
|
+
}
|
|
3534
4654
|
switch (event.type) {
|
|
3535
4655
|
case 'user/message': {
|
|
3536
4656
|
// Compaction checkpoint: `source = { kind: 'plugin', plugin:
|
|
@@ -3573,10 +4693,13 @@ ${output}
|
|
|
3573
4693
|
contextWarned = false;
|
|
3574
4694
|
break;
|
|
3575
4695
|
}
|
|
3576
|
-
// Same-session goal domain:
|
|
3577
|
-
//
|
|
3578
|
-
//
|
|
3579
|
-
//
|
|
4696
|
+
// Same-session goal domain: goal-sourced messages are the round
|
|
4697
|
+
// driver's continuation prompts (positive rounds advance the
|
|
4698
|
+
// counter); some hosts also inline the durable snapshot in a
|
|
4699
|
+
// round-zero source. They are not transcript bubbles — they drive
|
|
4700
|
+
// the goal panel's live projection (replayed on resume/rewind like
|
|
4701
|
+
// every other event; the snapshot itself arrives as the top-level
|
|
4702
|
+
// `goal/change` event admitted above).
|
|
3580
4703
|
if (event.data.source.kind === 'goal') {
|
|
3581
4704
|
applyGoalEvent(event);
|
|
3582
4705
|
break;
|
|
@@ -3976,6 +5099,13 @@ ${output}
|
|
|
3976
5099
|
nextRowId += 1;
|
|
3977
5100
|
break;
|
|
3978
5101
|
}
|
|
5102
|
+
// `/color` accent (dsh-tui plugin event, replayed on resume/rewind
|
|
5103
|
+
// like session/title): last write wins, '' clears to the default.
|
|
5104
|
+
if (event.type === 'session/color') {
|
|
5105
|
+
const data = event.data;
|
|
5106
|
+
state.sessionColor = typeof data.color === 'string' ? data.color : '';
|
|
5107
|
+
break;
|
|
5108
|
+
}
|
|
3979
5109
|
// Custom plugin events (tuiRenderers seam): a registered renderer
|
|
3980
5110
|
// maps the payload to text rows — title as a local row, body as
|
|
3981
5111
|
// preview-clipped local-output rows, same shape pushLocal uses.
|
|
@@ -4158,12 +5288,18 @@ ${output}
|
|
|
4158
5288
|
const subagentId = subagentStore.getSubagentIdBySession(session);
|
|
4159
5289
|
if (subagentId) {
|
|
4160
5290
|
subagentStore.onSessionEvent(subagentId, event);
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
5291
|
+
if (event.type === 'assistant/chunk') {
|
|
5292
|
+
// Token-rate path (100-300 events/s): the store append stays
|
|
5293
|
+
// synchronous (cheap); the expensive snapshot + row projection
|
|
5294
|
+
// defers to the frame-aligned flush inside emitStream's 16ms
|
|
5295
|
+
// timer, so it coalesces exactly like the main-agent stream.
|
|
5296
|
+
subagentStreamDirty = true;
|
|
4164
5297
|
state.emitStream();
|
|
4165
|
-
|
|
5298
|
+
}
|
|
5299
|
+
else {
|
|
5300
|
+
syncSubagentsNow();
|
|
4166
5301
|
state.emit();
|
|
5302
|
+
}
|
|
4167
5303
|
return;
|
|
4168
5304
|
}
|
|
4169
5305
|
// Otherwise handle main agent session
|
|
@@ -4226,8 +5362,7 @@ ${output}
|
|
|
4226
5362
|
catch {
|
|
4227
5363
|
// Session discovery is best-effort and must not break the parent turn.
|
|
4228
5364
|
}
|
|
4229
|
-
|
|
4230
|
-
syncSubagentRows();
|
|
5365
|
+
syncSubagentsNow();
|
|
4231
5366
|
state.emit();
|
|
4232
5367
|
});
|
|
4233
5368
|
const disposeEnd = ctx.on('subagent/end', (info) => {
|
|
@@ -4249,8 +5384,7 @@ ${output}
|
|
|
4249
5384
|
subagentStore.onCancelled(info.id, info.stopReason, output);
|
|
4250
5385
|
else
|
|
4251
5386
|
subagentStore.onFailed(info.id, info.stopReason || 'Unknown error');
|
|
4252
|
-
|
|
4253
|
-
syncSubagentRows();
|
|
5387
|
+
syncSubagentsNow();
|
|
4254
5388
|
state.emit();
|
|
4255
5389
|
});
|
|
4256
5390
|
return () => {
|
|
@@ -4519,6 +5653,20 @@ const MENTION_IMAGE_MEDIA_TYPES = {
|
|
|
4519
5653
|
function mentionImageMediaType(path) {
|
|
4520
5654
|
return MENTION_IMAGE_MEDIA_TYPES[extname(path).toLowerCase()];
|
|
4521
5655
|
}
|
|
5656
|
+
/** Resolve+stat one candidate path; undefined when it throws OR stats
|
|
5657
|
+
* absent — both are a miss for strip-first mention resolution (issue #359). */
|
|
5658
|
+
async function tryResolveMention(fs, absolute) {
|
|
5659
|
+
try {
|
|
5660
|
+
const target = await fs.resolve(absolute);
|
|
5661
|
+
const info = await fs.stat(target);
|
|
5662
|
+
if (info === undefined)
|
|
5663
|
+
return undefined;
|
|
5664
|
+
return { target, info };
|
|
5665
|
+
}
|
|
5666
|
+
catch {
|
|
5667
|
+
return undefined;
|
|
5668
|
+
}
|
|
5669
|
+
}
|
|
4522
5670
|
/**
|
|
4523
5671
|
* Expand a submitted text's `@` mentions (issue #15) into model-facing
|
|
4524
5672
|
* attachment blocks: supported image files become durable image blocks,
|
|
@@ -4537,68 +5685,99 @@ export async function expandMentions(fs, cwd, text, attachments, stagedImages) {
|
|
|
4537
5685
|
let imageBytes = 0;
|
|
4538
5686
|
if (fs !== undefined) {
|
|
4539
5687
|
for (const mention of mentions) {
|
|
5688
|
+
const display = mention.literal ?? mention.path;
|
|
4540
5689
|
const imageMediaType = mentionImageMediaType(mention.path);
|
|
4541
5690
|
if (budget <= 0 && imageMediaType === undefined)
|
|
4542
5691
|
break;
|
|
4543
5692
|
// Mentions resolve against the session cwd, same as the model-facing fs
|
|
4544
|
-
// tools; absolute paths pass through untouched.
|
|
5693
|
+
// tools; absolute paths pass through untouched. A `#L12-14` line suffix
|
|
5694
|
+
// (issue #359) is stripped before resolution; when the stripped path
|
|
5695
|
+
// misses, the typed literal (suffix intact) gets ONE fallback try so
|
|
5696
|
+
// filenames genuinely containing `#L…` still resolve as whole files.
|
|
4545
5697
|
const absolute = isAbsolute(mention.path) ? mention.path : join(cwd, mention.path);
|
|
4546
|
-
let
|
|
4547
|
-
let
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
5698
|
+
let resolved = await tryResolveMention(fs, absolute);
|
|
5699
|
+
let literalFallback = false;
|
|
5700
|
+
if (resolved === undefined && mention.literal !== undefined) {
|
|
5701
|
+
const literalPath = isAbsolute(mention.literal) ? mention.literal : join(cwd, mention.literal);
|
|
5702
|
+
resolved = await tryResolveMention(fs, literalPath);
|
|
5703
|
+
literalFallback = resolved !== undefined;
|
|
5704
|
+
}
|
|
5705
|
+
if (resolved === undefined) {
|
|
5706
|
+
missing.push(display);
|
|
4554
5707
|
continue;
|
|
4555
5708
|
}
|
|
5709
|
+
const { target, info } = resolved;
|
|
5710
|
+
// On a literal-fallback hit the attached file IS the typed name — the
|
|
5711
|
+
// model must see that path, not the suffix-stripped one.
|
|
5712
|
+
const shownPath = literalFallback ? display : mention.path;
|
|
5713
|
+
// …and judge image-ness by the typed extension in that case too.
|
|
5714
|
+
const imageType = literalFallback && mention.literal !== undefined
|
|
5715
|
+
? mentionImageMediaType(mention.literal)
|
|
5716
|
+
: imageMediaType;
|
|
4556
5717
|
if (info?.type === 'file') {
|
|
4557
|
-
if (
|
|
5718
|
+
if (imageType !== undefined && attachments !== undefined && fs.readBytes !== undefined) {
|
|
4558
5719
|
const limits = attachments.imageLimits;
|
|
4559
|
-
if (!limits.mediaTypes.includes(
|
|
4560
|
-
missing.push(
|
|
5720
|
+
if (!limits.mediaTypes.includes(imageType) || imageCount >= limits.maxImagesPerMessage) {
|
|
5721
|
+
missing.push(display);
|
|
4561
5722
|
continue;
|
|
4562
5723
|
}
|
|
4563
5724
|
try {
|
|
4564
5725
|
const data = await fs.readBytes(target, undefined, limits.maxImageBytes);
|
|
4565
5726
|
if (imageBytes + data.byteLength > limits.maxMessageImageBytes) {
|
|
4566
|
-
missing.push(
|
|
5727
|
+
missing.push(display);
|
|
4567
5728
|
continue;
|
|
4568
5729
|
}
|
|
4569
5730
|
const attachment = await attachments.saveImage({
|
|
4570
5731
|
data,
|
|
4571
|
-
mediaType:
|
|
5732
|
+
mediaType: imageType,
|
|
4572
5733
|
name: basename(target.displayPath),
|
|
4573
5734
|
});
|
|
4574
5735
|
blocks.push({ type: 'image', attachment });
|
|
4575
5736
|
imageCount += 1;
|
|
4576
5737
|
imageBytes += data.byteLength;
|
|
4577
|
-
attached.push(
|
|
5738
|
+
attached.push(display);
|
|
4578
5739
|
}
|
|
4579
5740
|
catch {
|
|
4580
|
-
missing.push(
|
|
5741
|
+
missing.push(display);
|
|
4581
5742
|
}
|
|
4582
5743
|
continue;
|
|
4583
5744
|
}
|
|
4584
5745
|
try {
|
|
4585
5746
|
const cap = Math.min(MENTION_MAX_FILE_CHARS, budget);
|
|
4586
|
-
|
|
5747
|
+
const content = await fs.readText(target);
|
|
5748
|
+
let body = content;
|
|
4587
5749
|
let truncated = false;
|
|
4588
|
-
|
|
4589
|
-
|
|
5750
|
+
let header = `<attached-file path="${shownPath}">`;
|
|
5751
|
+
if (mention.startLine !== undefined && !literalFallback) {
|
|
5752
|
+
// Line-range slice (issue #359): 1-based inclusive. An endLine
|
|
5753
|
+
// past EOF clamps to the file; a startLine past EOF falls back
|
|
5754
|
+
// to the whole file with an in-band note — never a silent
|
|
5755
|
+
// empty attach. Line ranges never apply to literal-fallback
|
|
5756
|
+
// hits (those files really are named `…#L…`, no suffix typed).
|
|
5757
|
+
const lines = content.split('\n');
|
|
5758
|
+
if (mention.startLine > lines.length) {
|
|
5759
|
+
header = `<attached-file path="${shownPath}" lines="${mention.startLine}-${mention.endLine}" note="requested lines beyond EOF (file has ${lines.length} line${lines.length === 1 ? '' : 's'}); whole file attached">`;
|
|
5760
|
+
}
|
|
5761
|
+
else {
|
|
5762
|
+
const endLine = Math.min(mention.endLine ?? mention.startLine, lines.length);
|
|
5763
|
+
header = `<attached-file path="${shownPath}" lines="${mention.startLine}${endLine === mention.startLine ? '' : `-${endLine}`}">`;
|
|
5764
|
+
body = lines.slice(mention.startLine - 1, endLine).join('\n');
|
|
5765
|
+
}
|
|
5766
|
+
}
|
|
5767
|
+
if (body.length > cap) {
|
|
5768
|
+
body = body.slice(0, cap);
|
|
4590
5769
|
truncated = true;
|
|
4591
5770
|
}
|
|
4592
|
-
budget -=
|
|
5771
|
+
budget -= body.length;
|
|
4593
5772
|
blocks.push({
|
|
4594
5773
|
type: 'text',
|
|
4595
|
-
text:
|
|
5774
|
+
text: `${header}\n${body}${truncated ? '\n[… truncated]' : ''}\n</attached-file>`,
|
|
4596
5775
|
});
|
|
4597
|
-
attached.push(
|
|
5776
|
+
attached.push(display);
|
|
4598
5777
|
}
|
|
4599
5778
|
catch {
|
|
4600
5779
|
// Binary/undecodable or unreadable — report it like a miss.
|
|
4601
|
-
missing.push(
|
|
5780
|
+
missing.push(display);
|
|
4602
5781
|
}
|
|
4603
5782
|
continue;
|
|
4604
5783
|
}
|
|
@@ -4615,17 +5794,17 @@ export async function expandMentions(fs, cwd, text, attachments, stagedImages) {
|
|
|
4615
5794
|
budget -= body.length;
|
|
4616
5795
|
blocks.push({
|
|
4617
5796
|
type: 'text',
|
|
4618
|
-
text: `<attached-directory path="${
|
|
5797
|
+
text: `<attached-directory path="${shownPath}">\n${body}\n</attached-directory>`,
|
|
4619
5798
|
});
|
|
4620
|
-
attached.push(
|
|
5799
|
+
attached.push(display);
|
|
4621
5800
|
}
|
|
4622
5801
|
catch {
|
|
4623
|
-
missing.push(
|
|
5802
|
+
missing.push(display);
|
|
4624
5803
|
}
|
|
4625
5804
|
continue;
|
|
4626
5805
|
}
|
|
4627
5806
|
// Absent (stat → undefined) or a special file.
|
|
4628
|
-
missing.push(
|
|
5807
|
+
missing.push(display);
|
|
4629
5808
|
}
|
|
4630
5809
|
}
|
|
4631
5810
|
if (attachments !== undefined && stagedImages !== undefined) {
|