@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
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { t, getLang, setLang, isLang, writeLangPref, subscribeLang } from '../i18n.js';
|
|
3
|
+
import { t, getLang, setLang, isLang, writeLangPref, readLangPref, subscribeLang, LANGS } from '../i18n.js';
|
|
4
|
+
import { readThemePref } from '../themePrefs.js';
|
|
5
|
+
import { readPresetPref } from '../presetPrefs.js';
|
|
6
|
+
import { readModelPref } from '../modelPrefs.js';
|
|
7
|
+
import { readActivityFrames } from '../activityPrefs.js';
|
|
8
|
+
import { envThemeOverride } from '../components/design-system/ThemeProvider.js';
|
|
9
|
+
import { hasPath } from '../dsh-adapter/settingsEditor.js';
|
|
10
|
+
import { planReload } from '../reload.js';
|
|
4
11
|
import { AlternateScreen, Box, Text, useInput, ScrollBox, useTheme, useTerminalSize } from '../ui.js';
|
|
5
12
|
import * as tuiKit from '../ui.js';
|
|
6
13
|
import { POINTER } from '../cc/figures.js';
|
|
7
|
-
import {
|
|
14
|
+
import { isPlainReturnInput, modLabel } from '../utils/modifiers.js';
|
|
15
|
+
import { actionMatches } from '../utils/keymap.js';
|
|
8
16
|
import { formatTokens } from '../cc/format.js';
|
|
9
17
|
import { homeDir } from '../utils/paths.js';
|
|
18
|
+
import { deriveModelGroups, modelPickerLanding, recentCatalogModels, RECENTS_GROUP_PROVIDER, } from '../modelGroups.js';
|
|
19
|
+
import { readModelRecents, recordModelUse } from '../modelRecents.js';
|
|
10
20
|
import { sessionCwdMatches } from '../dsh-adapter/channel.js';
|
|
11
21
|
import { TuiDialogStore } from '../dsh-adapter/dialogs.js';
|
|
12
22
|
import { TuiStatusStore } from '../dsh-adapter/status.js';
|
|
@@ -22,9 +32,13 @@ import { useCopyOnSelect } from '../ink/hooks/use-copy-on-select.js';
|
|
|
22
32
|
import { useSelection } from '../ink/hooks/use-selection.js';
|
|
23
33
|
import { NoSelect } from '../ink/components/NoSelect.js';
|
|
24
34
|
import { LogoHeader, MessageList } from '../components/MessageList.js';
|
|
35
|
+
import { TimelineRail } from '../components/TimelineRail.js';
|
|
36
|
+
import { ScrollbarGutter } from '../components/ScrollbarGutter.js';
|
|
37
|
+
import { normalizeScrollGutter } from '../tuiDisplayPrefs.js';
|
|
25
38
|
import { OverlayAbove } from '../components/OverlayAbove.js';
|
|
26
39
|
import { PromptInput } from '../components/PromptInput.js';
|
|
27
40
|
import { GoalTodoPanel } from '../components/GoalTodoPanel.js';
|
|
41
|
+
import { AutoRecapRow } from '../components/AutoRecapRow.js';
|
|
28
42
|
import { LoadedContextPanel } from '../components/LoadedContextPanel.js';
|
|
29
43
|
import { StatusLine } from './StatusLine.js';
|
|
30
44
|
import { WorkingSpinner, useThinkingStatus } from '../components/WorkingSpinner.js';
|
|
@@ -33,12 +47,18 @@ import { ModelPicker } from '../components/ModelPicker.js';
|
|
|
33
47
|
import { PluginSceneBoundary } from '../components/PluginSceneBoundary.js';
|
|
34
48
|
import { SkillsPicker, SkillsPickerLoading } from '../components/SkillsPicker.js';
|
|
35
49
|
import { SessionBrowser } from './SessionBrowser.js';
|
|
50
|
+
import { SessionTree } from './SessionTree.js';
|
|
36
51
|
import { Settings } from './Settings.js';
|
|
37
52
|
import { WorkspacePicker } from '../components/WorkspacePicker.js';
|
|
53
|
+
import { WorkspaceMenuPicker } from '../components/WorkspaceMenuPicker.js';
|
|
38
54
|
import { WorkspaceFlowPicker } from '../components/WorkspaceFlowPicker.js';
|
|
39
55
|
import { ActivityPicker } from '../components/ActivityPicker.js';
|
|
56
|
+
import { ColorPicker } from '../components/ColorPicker.js';
|
|
40
57
|
import { EffortSlider } from '../components/EffortSlider.js';
|
|
41
58
|
import { PresetPicker } from '../components/PresetPicker.js';
|
|
59
|
+
import { PermissionsPicker, PERMISSION_PRESET_IDS } from '../components/PermissionsPicker.js';
|
|
60
|
+
import { PlanPicker } from '../components/PlanPicker.js';
|
|
61
|
+
import { LangPicker } from '../components/LangPicker.js';
|
|
42
62
|
import { ThemePicker, getThemeOptions } from '../components/ThemePicker.js';
|
|
43
63
|
import { AUTO_THEME_NAME, getAutoThemeBase } from '../theme.js';
|
|
44
64
|
import { FRAME_PRESETS, PRESET_NAMES } from '../components/activityFrames.js';
|
|
@@ -46,9 +66,15 @@ import { ThinkingToggle } from '../components/ThinkingToggle.js';
|
|
|
46
66
|
import { HistorySearchDialog } from '../components/HistorySearchDialog.js';
|
|
47
67
|
import { RewindPicker } from '../components/RewindPicker.js';
|
|
48
68
|
import { BtwPanel } from '../components/BtwPanel.js';
|
|
69
|
+
import { RecapPanel } from '../components/RecapPanel.js';
|
|
70
|
+
import { isValidSessionColor, SESSION_COLOR_NAMES } from '../cc/sessionColors.js';
|
|
49
71
|
import { TipsPanel } from '../components/TipsPanel.js';
|
|
50
72
|
import { SubagentDashboard } from '../components/SubagentDashboard.js';
|
|
51
73
|
import { SubagentDetailScene } from '../components/SubagentDetailScene.js';
|
|
74
|
+
import { FileActionsPanel, FILE_ACTION_COUNT } from '../components/FileActionsPanel.js';
|
|
75
|
+
import { openExternal, openFile, revealInFileManager } from '../utils/openExternal.js';
|
|
76
|
+
import { fileUrlToPath, parseFileLinkUrl, resolveTargetPath } from '../utils/fileTarget.js';
|
|
77
|
+
import { statSync } from 'node:fs';
|
|
52
78
|
import { setClipboard } from '../ink/termio/osc.js';
|
|
53
79
|
import { TerminalWriteContext } from '../ink/useTerminalNotification.js';
|
|
54
80
|
import instances from '../ink/instances.js';
|
|
@@ -61,6 +87,7 @@ import { LoadingState } from '../components/design-system/LoadingState.js';
|
|
|
61
87
|
import { Pane } from '../components/design-system/Pane.js';
|
|
62
88
|
import { loadHistory } from '../history.js';
|
|
63
89
|
import { formatLoadedContextReport } from '../utils/loaded-context.js';
|
|
90
|
+
import { NO_OVERLAY, chatOverlayReducer, dialogOverlayVisible, wrapIndex, } from './chatOverlay.js';
|
|
64
91
|
/** Shared empty snapshot for hosts whose channel has no event log. */
|
|
65
92
|
const NO_EVENTS = [];
|
|
66
93
|
/** Row kinds the message-selection cursor can land on. */
|
|
@@ -113,8 +140,11 @@ function searchableText(row) {
|
|
|
113
140
|
}
|
|
114
141
|
/**
|
|
115
142
|
* Main chat screen in the Claude Code layout: a scrollable transcript
|
|
116
|
-
* (with the
|
|
117
|
-
* up
|
|
143
|
+
* (with the user message the viewport is showing pinned above the transcript
|
|
144
|
+
* while scrolled up, and a 1-column minimap scrollbar with one node per
|
|
145
|
+
* user message — the current message's node is highlighted, clicking a node
|
|
146
|
+
* jumps to it), transient notifications, the working spinner, the bordered
|
|
147
|
+
* prompt
|
|
118
148
|
* input (with slash-command overlay) and the status line pinned at the
|
|
119
149
|
* bottom.
|
|
120
150
|
*
|
|
@@ -137,7 +167,7 @@ let fallbackApprovalStore;
|
|
|
137
167
|
*/
|
|
138
168
|
let fallbackDialogStore;
|
|
139
169
|
let fallbackStatusStore;
|
|
140
|
-
export function Chat({ channel, questionStore, approvalStore, extensionDialogs, extensionStatus, extensionShortcuts, onExit, onUpdate, fullscreen = false, trajectorySeen: trajectorySeenProp, }) {
|
|
170
|
+
export function Chat({ channel, questionStore, approvalStore, extensionDialogs, extensionStatus, extensionShortcuts, onExit, onUpdate, onRestart, fullscreen = false, trajectorySeen: trajectorySeenProp, }) {
|
|
141
171
|
const writeRaw = React.useContext(TerminalWriteContext);
|
|
142
172
|
// Re-render whenever the channel mutates; rows/status are read fresh below.
|
|
143
173
|
React.useSyncExternalStore(channel.subscribe, () => channel.version);
|
|
@@ -188,70 +218,110 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
188
218
|
const [expanded, setExpanded] = React.useState(false);
|
|
189
219
|
const [helpOpen, setHelpOpen] = React.useState(false);
|
|
190
220
|
const [handle, setHandle] = React.useState(null);
|
|
221
|
+
/**
|
|
222
|
+
* Conversation timeline snapshot (reported by MessageList): one entry
|
|
223
|
+
* per user turn plus the viewport-derived navigation targets. The
|
|
224
|
+
* ACTIVE turn — the one whose content owns the viewport top row — pins
|
|
225
|
+
* the sticky prompt header AND highlights the transcript rail's tick,
|
|
226
|
+
* from one report so the two can never disagree; upId/downId drive the
|
|
227
|
+
* rail's ▲/▼. Null activeId while pinned to the bottom only when there
|
|
228
|
+
* are no turns (header hidden there anyway).
|
|
229
|
+
*/
|
|
230
|
+
const [timeline, setTimeline] = React.useState({
|
|
231
|
+
turns: [],
|
|
232
|
+
activeId: null,
|
|
233
|
+
upId: null,
|
|
234
|
+
downId: null,
|
|
235
|
+
});
|
|
191
236
|
const [selectionActive, setSelectionActive] = React.useState(false);
|
|
192
237
|
const [selectedId, setSelectedId] = React.useState(null);
|
|
193
238
|
const [expandedRows, setExpandedRows] = React.useState(() => new Set());
|
|
194
|
-
|
|
239
|
+
/** 流式 reasoning 行的用户折叠(点击/进入折叠态)。与 expandedRows 分开:
|
|
240
|
+
* 流式默认展开,用户点一下 = 折叠(preview ticker 或单行头);落定后
|
|
241
|
+
* 默认折叠,此集合不再参与——两种默认互不翻转。 */
|
|
242
|
+
const [streamFoldedRows, setStreamFoldedRows] = React.useState(() => new Set());
|
|
243
|
+
/**
|
|
244
|
+
* The transient-dialog layer (every picker/dialog `<OverlayAbove>` hosts,
|
|
245
|
+
* plus /tips) as ONE value: mutual exclusion between the panels is
|
|
246
|
+
* structural instead of emerging from "an open picker makes the prompt
|
|
247
|
+
* inert". Transitions live in the pure reducer (chatOverlay.ts), which
|
|
248
|
+
* scripts/verify-chat-overlay.ts pins without a renderer. Async data the
|
|
249
|
+
* pickers show (model list, preset roster, …) stays in the caches below —
|
|
250
|
+
* it persists across open/close so a reopened picker paints the previous
|
|
251
|
+
* list while the fresh one loads, exactly as the boolean era did.
|
|
252
|
+
*/
|
|
253
|
+
const [overlay, dispatchOverlay] = React.useReducer(chatOverlayReducer, NO_OVERLAY);
|
|
195
254
|
const [models, setModels] = React.useState([]);
|
|
196
|
-
|
|
255
|
+
/** Provider display identities for the /model group level; refreshed alongside `models`. */
|
|
256
|
+
const [providerInfos, setProviderInfos] = React.useState([]);
|
|
257
|
+
/** /model 最近使用分组:成功切换即记录(去重置顶,上限 10),重启保留。 */
|
|
258
|
+
const [modelRecents, setModelRecents] = React.useState(() => readModelRecents());
|
|
259
|
+
/** Two-level /model: the drilled-in provider route; undefined = group level.
|
|
260
|
+
* Reset on open; stale ids resolve back to the group level via `activeModelGroup`. */
|
|
261
|
+
const [modelGroup, setModelGroup] = React.useState(undefined);
|
|
262
|
+
/** True while the picker sits in the single-provider fast path (drilled in
|
|
263
|
+
* at open, the group level never shown): Esc closes directly and no back
|
|
264
|
+
* hint renders — a pinned recents pseudo-group must not fake a two-level
|
|
265
|
+
* walk the user never saw (issue #527 regression: repro-picker-windowing). */
|
|
266
|
+
const [modelPickerDirect, setModelPickerDirect] = React.useState(false);
|
|
267
|
+
/** Group rows over the current catalog, first-appearance (registry) order,
|
|
268
|
+
* with the pinned recents pseudo-group first when any entry is catalogued. */
|
|
269
|
+
const modelGroups = React.useMemo(() => deriveModelGroups(models, providerInfos, modelRecents), [models, providerInfos, modelRecents]);
|
|
270
|
+
/** The drilled-in group, but only while it still exists in the catalog. */
|
|
271
|
+
const activeModelGroup = modelGroup !== undefined && modelGroups.some(group => group.provider === modelGroup)
|
|
272
|
+
? modelGroup
|
|
273
|
+
: undefined;
|
|
274
|
+
const groupModels = React.useMemo(() => {
|
|
275
|
+
if (activeModelGroup === undefined)
|
|
276
|
+
return [];
|
|
277
|
+
if (activeModelGroup === RECENTS_GROUP_PROVIDER)
|
|
278
|
+
return recentCatalogModels(modelRecents, models);
|
|
279
|
+
return models.filter(model => model.provider === activeModelGroup);
|
|
280
|
+
}, [models, modelRecents, activeModelGroup]);
|
|
281
|
+
/** Switch + record: every successful switch feeds the /model recents group
|
|
282
|
+
* (picker Enter/click, `/model provider/id`, the wizard's live switch,
|
|
283
|
+
* and /reload's applied model all ride this one path). */
|
|
284
|
+
const switchModelRecorded = (provider, id, name) => {
|
|
285
|
+
if (name !== undefined)
|
|
286
|
+
channel.notify(t('model-switching', { name }));
|
|
287
|
+
return channel.switchModel(provider, id).then((ok) => {
|
|
288
|
+
if (!ok)
|
|
289
|
+
return ok;
|
|
290
|
+
if (name !== undefined)
|
|
291
|
+
channel.notify(t('model-switched', { name }));
|
|
292
|
+
setModelRecents(recordModelUse({ provider, id }));
|
|
293
|
+
return ok;
|
|
294
|
+
});
|
|
295
|
+
};
|
|
197
296
|
/** `/skills` 技能目录(issue #204):null = 注册表快照在途。 */
|
|
198
|
-
const [skillsPickerOpen, setSkillsPickerOpen] = React.useState(false);
|
|
199
297
|
const [skillsList, setSkillsList] = React.useState(null);
|
|
200
|
-
const [skillsIndex, setSkillsIndex] = React.useState(0);
|
|
201
298
|
/** `/resume` opens the session browser, a screen rather than a panel. It
|
|
202
299
|
* owns its own selection, filters and keyboard — Chat only opens it. */
|
|
203
300
|
const [browserOpen, setBrowserOpen] = React.useState(false);
|
|
301
|
+
/** `/tree` opens the session family tree (pi's Session Tree): every rewind
|
|
302
|
+
* fork stitched back onto the message it diverged from, hover previews,
|
|
303
|
+
* and per-node rewind/fork/adopt actions. Like the browser, a screen. */
|
|
304
|
+
const [treeOpen, setTreeOpen] = React.useState(false);
|
|
204
305
|
/** `/settings` opens the plugin settings screen (issue #165) — like the
|
|
205
306
|
* browser, a screen rather than a panel: it owns its own focus, staged
|
|
206
307
|
* drafts and keyboard; Chat only opens it. */
|
|
207
308
|
const [settingsOpen, setSettingsOpen] = React.useState(false);
|
|
208
|
-
const [workspacePickerOpen, setWorkspacePickerOpen] = React.useState(false);
|
|
209
309
|
const [workspaceTargets, setWorkspaceTargets] = React.useState([]);
|
|
210
|
-
const [workspaceIndex, setWorkspaceIndex] = React.useState(0);
|
|
211
|
-
const [workspaceFlow, setWorkspaceFlow] = React.useState(null);
|
|
212
|
-
const [workspaceFlowIndex, setWorkspaceFlowIndex] = React.useState(0);
|
|
213
|
-
const [workspaceFlowBusy, setWorkspaceFlowBusy] = React.useState(false);
|
|
214
|
-
const [workspaceFlowInput, setWorkspaceFlowInput] = React.useState(null);
|
|
215
310
|
const workspaceFlowRequestRef = React.useRef(0);
|
|
216
311
|
const workspaceFlowAbortRef = React.useRef(null);
|
|
217
|
-
/** `/
|
|
218
|
-
const [activityPickerOpen, setActivityPickerOpen] = React.useState(false);
|
|
219
|
-
const [activityIndex, setActivityIndex] = React.useState(0);
|
|
220
|
-
/** `/preset` agent-preset picker (issue #8): roster list loads async. */
|
|
221
|
-
const [presetPickerOpen, setPresetPickerOpen] = React.useState(false);
|
|
312
|
+
/** `/preset` agent-preset roster (issue #8): loads async, persists. */
|
|
222
313
|
const [presetOptions, setPresetOptions] = React.useState([]);
|
|
223
|
-
|
|
224
|
-
/** `/effort` rheostat slider: adapter levels load async, focus moves ←/→. */
|
|
225
|
-
const [effortSliderOpen, setEffortSliderOpen] = React.useState(false);
|
|
314
|
+
/** `/effort` adapter levels: load async before the slider opens. */
|
|
226
315
|
const [effortOptions, setEffortOptions] = React.useState([]);
|
|
227
|
-
const [effortIndex, setEffortIndex] = React.useState(0);
|
|
228
|
-
/** `/theme` color-theme picker (built-ins + ~/.dsh-tui/themes user themes). */
|
|
229
|
-
const [themePickerOpen, setThemePickerOpen] = React.useState(false);
|
|
230
|
-
const [themeIndex, setThemeIndex] = React.useState(0);
|
|
231
316
|
const [themeName, setTheme] = useTheme();
|
|
232
317
|
const { rows: terminalRows } = useTerminalSize();
|
|
233
318
|
const [showAllMessages, setShowAllMessages] = React.useState(false);
|
|
319
|
+
/** Fold state for the GoalTodoPanel todo section (ctrl/cmd+q or click). */
|
|
320
|
+
const [todoCollapsed, setTodoCollapsed] = React.useState(false);
|
|
234
321
|
const [thinkingVisible, setThinkingVisible] = React.useState(true);
|
|
235
|
-
|
|
236
|
-
const [thinkingFocus, setThinkingFocus] = React.useState(0);
|
|
237
|
-
/** ctrl+r history search dialog (ported from CC's HistorySearchDialog). */
|
|
238
|
-
const [historyOpen, setHistoryOpen] = React.useState(false);
|
|
239
|
-
const [historyQuery, setHistoryQuery] = React.useState('');
|
|
240
|
-
const [historyCursor, setHistoryCursor] = React.useState(0);
|
|
241
|
-
const [historyFocus, setHistoryFocus] = React.useState(0);
|
|
322
|
+
/** ctrl+r history-search entries (loaded on open, persists). */
|
|
242
323
|
const [historyEntries, setHistoryEntries] = React.useState([]);
|
|
243
324
|
const [historyFill, setHistoryFill] = React.useState(null);
|
|
244
|
-
/** Double-Esc rewind picker (CC rewind): open state + focused row + confirm. */
|
|
245
|
-
const [rewindOpen, setRewindOpen] = React.useState(false);
|
|
246
|
-
const [rewindIndex, setRewindIndex] = React.useState(0);
|
|
247
|
-
const [rewindConfirm, setRewindConfirm] = React.useState(null);
|
|
248
|
-
/** Plugin rewind modes (tui/rewind-prompt seam): extra choices offered in
|
|
249
|
-
* the confirm pane; null = the plain conversation-only confirm. */
|
|
250
|
-
const [rewindModes, setRewindModes] = React.useState(null);
|
|
251
|
-
const [rewindModeIndex, setRewindModeIndex] = React.useState(0);
|
|
252
|
-
/** True while the tui/rewind-prompt decision is in flight (a plugin may be
|
|
253
|
-
* showing its own dialog); keys except Esc are swallowed meanwhile. */
|
|
254
|
-
const [rewindBusy, setRewindBusy] = React.useState(false);
|
|
255
325
|
/** Monotonic token: only the latest rewind decision may land (a slow
|
|
256
326
|
* plugin answering after the user moved on must not open a confirm for
|
|
257
327
|
* a row they are no longer looking at). */
|
|
@@ -265,8 +335,68 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
265
335
|
btwAbortRef.current = null;
|
|
266
336
|
setBtw(null);
|
|
267
337
|
};
|
|
268
|
-
/** /
|
|
269
|
-
|
|
338
|
+
/** /recap overlay (pi-recap semantics): pure UI state like /btw — the
|
|
339
|
+
* summary never enters the transcript or session log; applying the
|
|
340
|
+
* proposed title goes through the normal /rename path. `auto` marks the
|
|
341
|
+
* recapOnOpen-triggered run (rendered as the dim AutoRecapRow until
|
|
342
|
+
* expanded); `expanded` lifts an auto recap into the full RecapPanel;
|
|
343
|
+
* `rowsAtTrigger` is the last user-row id when the auto run started —
|
|
344
|
+
* a newer user row (the user starts a new message) retires the recap. */
|
|
345
|
+
const [recap, setRecap] = React.useState(null);
|
|
346
|
+
const recapAbortRef = React.useRef(null);
|
|
347
|
+
const closeRecap = () => {
|
|
348
|
+
recapAbortRef.current?.abort();
|
|
349
|
+
recapAbortRef.current = null;
|
|
350
|
+
setRecap(null);
|
|
351
|
+
};
|
|
352
|
+
// Auto-recap (`dsh-tui.recapOnOpen`): every time the session switches
|
|
353
|
+
// (mount = open/resume, rewind/fork included), summarize its tail into
|
|
354
|
+
// the dim AutoRecapRow. Failures stay silent in auto mode — `/recap`
|
|
355
|
+
// surfaces them; the summary never enters the transcript or session log.
|
|
356
|
+
const autoRecapSessionId = channel.agentId;
|
|
357
|
+
React.useEffect(() => {
|
|
358
|
+
// A session switch retires the previous recap outright — an old
|
|
359
|
+
// session's 回顾 has no place above a new conversation.
|
|
360
|
+
setRecap(null);
|
|
361
|
+
if (!channel.autoRecapOnOpen)
|
|
362
|
+
return;
|
|
363
|
+
// No conversation yet (/new): nothing to recap, don't even fire.
|
|
364
|
+
if (!channel.rows.some(row => row.kind === 'user' || row.kind === 'assistant'))
|
|
365
|
+
return;
|
|
366
|
+
recapAbortRef.current?.abort();
|
|
367
|
+
const controller = new AbortController();
|
|
368
|
+
recapAbortRef.current = controller;
|
|
369
|
+
const lastUserId = channel.rows.filter(row => row.kind === 'user').at(-1)?.id ?? -1;
|
|
370
|
+
setRecap({ raw: '', summary: '', error: undefined, done: false, titleApplied: false, auto: true, expanded: false, rowsAtTrigger: lastUserId });
|
|
371
|
+
void channel.recapRecent({
|
|
372
|
+
signal: controller.signal,
|
|
373
|
+
onText: delta => setRecap(prev => (prev ? { ...prev, raw: prev.raw + delta } : prev)),
|
|
374
|
+
}).then(result => {
|
|
375
|
+
if (controller.signal.aborted)
|
|
376
|
+
return;
|
|
377
|
+
setRecap(prev => {
|
|
378
|
+
if (prev === null || !prev.auto)
|
|
379
|
+
return prev;
|
|
380
|
+
// Auto mode stays quiet on failure (no activity / llm missing / error).
|
|
381
|
+
if (result.summary === null)
|
|
382
|
+
return null;
|
|
383
|
+
return { ...prev, summary: result.summary, title: result.title, error: result.error, done: true };
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
return () => controller.abort();
|
|
387
|
+
}, [autoRecapSessionId]);
|
|
388
|
+
// The user starts a new message → the auto recap has served its purpose
|
|
389
|
+
// (catching them up) and bows out. A newer user row is the signal; the
|
|
390
|
+
// assistant's own streamed rows don't count.
|
|
391
|
+
const lastUserRowId = channel.rows.filter(row => row.kind === 'user').at(-1)?.id ?? -1;
|
|
392
|
+
React.useEffect(() => {
|
|
393
|
+
if (recap !== null &&
|
|
394
|
+
recap.auto &&
|
|
395
|
+
recap.rowsAtTrigger !== undefined &&
|
|
396
|
+
lastUserRowId > recap.rowsAtTrigger) {
|
|
397
|
+
closeRecap();
|
|
398
|
+
}
|
|
399
|
+
}, [lastUserRowId, recap]);
|
|
270
400
|
/** Subagent dashboard (Ctrl+A): displays active/completed subagents. */
|
|
271
401
|
const [subagentDashboardOpen, setSubagentDashboardOpen] = React.useState(false);
|
|
272
402
|
/** Detail view for a specific subagent (opened from dashboard). */
|
|
@@ -277,6 +407,7 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
277
407
|
*/
|
|
278
408
|
const [logoNonce, setLogoNonce] = React.useState(0);
|
|
279
409
|
React.useEffect(() => () => btwAbortRef.current?.abort(), []);
|
|
410
|
+
React.useEffect(() => () => recapAbortRef.current?.abort(), []);
|
|
280
411
|
/**
|
|
281
412
|
* The trajectory scene (issue #80 evolution). Unlike every other overlay
|
|
282
413
|
* here it is not a panel but a whole screen: while open, Chat renders the
|
|
@@ -321,8 +452,72 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
321
452
|
ink?.invalidatePrevFrame();
|
|
322
453
|
ink?.reanchorViewport();
|
|
323
454
|
}, []);
|
|
324
|
-
/**
|
|
325
|
-
|
|
455
|
+
/**
|
|
456
|
+
* Click-to-act targets: the Ink instance's hyperlink-open callback (wired
|
|
457
|
+
* in the effect below) resolves every clickable target the transcript
|
|
458
|
+
* renders — http(s) links open the browser, `dsh-file:`/`file://` paths
|
|
459
|
+
* open the file-action menu. `dsh-file:` payloads are RAW display paths
|
|
460
|
+
* (possibly relative), so they resolve against the CURRENT channel cwd
|
|
461
|
+
* at click time (read through a ref so this callback keeps a stable
|
|
462
|
+
* identity — it is threaded into memoized row components).
|
|
463
|
+
*/
|
|
464
|
+
const cwdRef = React.useRef(channel.cwd);
|
|
465
|
+
React.useEffect(() => {
|
|
466
|
+
cwdRef.current = channel.cwd;
|
|
467
|
+
}, [channel.cwd]);
|
|
468
|
+
const openFileActions = React.useCallback((rawPath) => {
|
|
469
|
+
const resolved = resolveTargetPath(rawPath, cwdRef.current);
|
|
470
|
+
// Whether the target is a directory decides the first menu row's label
|
|
471
|
+
// ("open file" vs "open folder"). Missing paths count as files.
|
|
472
|
+
let isDir = false;
|
|
473
|
+
try {
|
|
474
|
+
isDir = statSync(resolved).isDirectory();
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
isDir = false;
|
|
478
|
+
}
|
|
479
|
+
dispatchOverlay({ type: 'open', overlay: { kind: 'file-actions', path: resolved, index: 0, isDir } });
|
|
480
|
+
}, []);
|
|
481
|
+
/** Run one file-action menu row: 0 = open file, 1 = reveal in file
|
|
482
|
+
* manager, 2 = copy absolute path. */
|
|
483
|
+
const runFileAction = React.useCallback((index, path) => {
|
|
484
|
+
if (index === 0)
|
|
485
|
+
openFile(path);
|
|
486
|
+
else if (index === 1)
|
|
487
|
+
revealInFileManager(path);
|
|
488
|
+
else
|
|
489
|
+
void setClipboard(path);
|
|
490
|
+
}, []);
|
|
491
|
+
const handleOpenTarget = React.useCallback((url) => {
|
|
492
|
+
const rawPath = parseFileLinkUrl(url);
|
|
493
|
+
if (rawPath !== undefined) {
|
|
494
|
+
openFileActions(rawPath);
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const filePath = fileUrlToPath(url);
|
|
498
|
+
if (filePath !== undefined) {
|
|
499
|
+
openFileActions(filePath);
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
openExternal(url);
|
|
503
|
+
}, [openFileActions]);
|
|
504
|
+
// Wire the click-to-open callback into the Ink instance (the field is
|
|
505
|
+
// otherwise never set — clicking links was a no-op). Re-wired whenever
|
|
506
|
+
// the handler changes (cwd moves), cleared on unmount.
|
|
507
|
+
React.useEffect(() => {
|
|
508
|
+
const ink = instances.get(process.stdout) ?? instances.values().next().value;
|
|
509
|
+
if (ink)
|
|
510
|
+
ink.onHyperlinkClick = handleOpenTarget;
|
|
511
|
+
return () => {
|
|
512
|
+
const current = instances.get(process.stdout) ?? instances.values().next().value;
|
|
513
|
+
if (current)
|
|
514
|
+
current.onHyperlinkClick = undefined;
|
|
515
|
+
};
|
|
516
|
+
}, [handleOpenTarget]);
|
|
517
|
+
/** `/` transcript search (less-style incsearch, ported from CC's REPL).
|
|
518
|
+
* Only the bar's open/closed mode lives in `overlay`; the query and match
|
|
519
|
+
* counters persist past the bar closing so n/N keep walking the matches. */
|
|
520
|
+
const searchActive = overlay.kind === 'search';
|
|
326
521
|
const [searchQuery, setSearchQuery] = React.useState('');
|
|
327
522
|
const [searchCursor, setSearchCursor] = React.useState(0);
|
|
328
523
|
const [searchCount, setSearchCount] = React.useState(0);
|
|
@@ -353,7 +548,10 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
353
548
|
: -1;
|
|
354
549
|
}
|
|
355
550
|
}, [isSticky, channel.rows]);
|
|
356
|
-
|
|
551
|
+
// The pill shows whenever the view is off the bottom (one-click return
|
|
552
|
+
// home): with unseen rows it counts them, otherwise it is the plain
|
|
553
|
+
// "return to bottom" affordance (Enter/End/click all land it).
|
|
554
|
+
const showPill = !isSticky;
|
|
357
555
|
// Idle Ctrl+C: first press arms an exit, second press exits (CC's
|
|
358
556
|
// double-press semantics, simplified). Under Windows ConPTY the key
|
|
359
557
|
// arrives as stdin data (key.ctrl && input === 'c') — the useInput
|
|
@@ -423,26 +621,30 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
423
621
|
useTerminalTitle(`${titlePrefix} 🐋 ${channel.sessionTitle}`);
|
|
424
622
|
const handleWorkspaceResult = (result) => {
|
|
425
623
|
workspaceFlowAbortRef.current = null;
|
|
426
|
-
setWorkspaceFlowBusy(false);
|
|
427
|
-
setWorkspaceFlowInput(null);
|
|
428
624
|
if (result.kind === 'target') {
|
|
429
|
-
|
|
625
|
+
dispatchOverlay({ type: 'close-if', kind: 'workspace-flow' });
|
|
430
626
|
void channel.switchWorkspace(result.target);
|
|
431
627
|
return;
|
|
432
628
|
}
|
|
433
629
|
if (result.choices.length === 0) {
|
|
434
|
-
|
|
630
|
+
dispatchOverlay({ type: 'close-if', kind: 'workspace-flow' });
|
|
435
631
|
channel.notify(t('workspace-command-empty'));
|
|
436
632
|
return;
|
|
437
633
|
}
|
|
438
|
-
|
|
439
|
-
|
|
634
|
+
// open-if: 'workspace-flow' stays allowed so an in-flow action can
|
|
635
|
+
// transition to its next stage; a picker the user opened after leaving
|
|
636
|
+
// the menu wins over a late command result.
|
|
637
|
+
dispatchOverlay({
|
|
638
|
+
type: 'open-if',
|
|
639
|
+
overlay: { kind: 'workspace-flow', flow: result, index: 0, busy: false, input: null },
|
|
640
|
+
when: ['none', 'workspace-flow'],
|
|
641
|
+
});
|
|
440
642
|
};
|
|
441
643
|
const runWorkspaceFlowAction = (action) => {
|
|
442
644
|
const request = ++workspaceFlowRequestRef.current;
|
|
443
645
|
const controller = new AbortController();
|
|
444
646
|
workspaceFlowAbortRef.current = controller;
|
|
445
|
-
|
|
647
|
+
dispatchOverlay({ type: 'flow-busy', busy: true });
|
|
446
648
|
void Promise.resolve()
|
|
447
649
|
.then(() => action(controller.signal))
|
|
448
650
|
.then((result) => {
|
|
@@ -453,10 +655,25 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
453
655
|
if (request !== workspaceFlowRequestRef.current)
|
|
454
656
|
return;
|
|
455
657
|
workspaceFlowAbortRef.current = null;
|
|
456
|
-
|
|
658
|
+
dispatchOverlay({ type: 'flow-busy', busy: false });
|
|
457
659
|
channel.notify(t('workspace-command-failed', { err: error instanceof Error ? error.message : String(error) }), { color: 'error', timeoutMs: 8000 });
|
|
458
660
|
});
|
|
459
661
|
};
|
|
662
|
+
/** Bare `/workspace` menu rows: built-in subcommands first, then the
|
|
663
|
+
* dynamically registered extensions (same reserved-name filter the Tab
|
|
664
|
+
* completion applies). Recomputed per render — the extension list is
|
|
665
|
+
* live. */
|
|
666
|
+
const workspaceMenuOptions = [
|
|
667
|
+
{ id: 'resume', label: 'resume', description: t('workspace-menu-resume-desc') },
|
|
668
|
+
{ id: 'rename', label: 'rename', description: t('workspace-menu-rename-desc') },
|
|
669
|
+
{ id: 'open', label: 'open', description: t('workspace-menu-open-desc') },
|
|
670
|
+
// Optional call: verify/repro scripts and embedders stub the channel
|
|
671
|
+
// without the workspace-commands API — render must not throw for them
|
|
672
|
+
// (a thrown render unmounts the whole Ink root).
|
|
673
|
+
...(channel.workspaceCommands?.() ?? [])
|
|
674
|
+
.filter(command => !['resume', 'rename', 'open'].includes(command.name.toLowerCase()))
|
|
675
|
+
.map(command => ({ id: command.name, label: command.name, description: command.description })),
|
|
676
|
+
];
|
|
460
677
|
const openWorkspaceTarget = (reference) => {
|
|
461
678
|
void channel.resolveWorkspace(reference).then((target) => {
|
|
462
679
|
if (target === undefined) {
|
|
@@ -475,12 +692,45 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
475
692
|
return;
|
|
476
693
|
}
|
|
477
694
|
setWorkspaceTargets(targets);
|
|
478
|
-
|
|
479
|
-
|
|
695
|
+
// open-if: the listing is async — whatever the user opened meanwhile wins.
|
|
696
|
+
dispatchOverlay({
|
|
697
|
+
type: 'open-if',
|
|
698
|
+
overlay: {
|
|
699
|
+
kind: 'workspace-picker',
|
|
700
|
+
index: Math.max(0, targets.findIndex(target => target.cwd === channel.cwd)),
|
|
701
|
+
},
|
|
702
|
+
when: ['none'],
|
|
703
|
+
});
|
|
480
704
|
}).catch((error) => {
|
|
481
705
|
channel.notify(t('workspace-list-failed', { err: error instanceof Error ? error.message : String(error) }), { color: 'error' });
|
|
482
706
|
});
|
|
483
707
|
};
|
|
708
|
+
/**
|
|
709
|
+
* Run one /workspace menu row (Enter path, shared with the mouse click):
|
|
710
|
+
* built-ins dispatch locally, extension commands go through the channel.
|
|
711
|
+
*/
|
|
712
|
+
const runWorkspaceMenuOption = (option) => {
|
|
713
|
+
dispatchOverlay({ type: 'close-if', kind: 'workspace-menu' });
|
|
714
|
+
if (option === undefined)
|
|
715
|
+
return;
|
|
716
|
+
if (option.id === 'resume') {
|
|
717
|
+
openWorkspaceResume();
|
|
718
|
+
}
|
|
719
|
+
else if (option.id === 'rename') {
|
|
720
|
+
channel.notify(t('workspace-rename-usage'));
|
|
721
|
+
}
|
|
722
|
+
else if (option.id === 'open') {
|
|
723
|
+
channel.notify(t('workspace-open-usage'));
|
|
724
|
+
}
|
|
725
|
+
else {
|
|
726
|
+
void channel.runWorkspaceCommand(option.id, '').then((result) => {
|
|
727
|
+
if (result !== undefined)
|
|
728
|
+
handleWorkspaceResult(result);
|
|
729
|
+
}).catch((error) => {
|
|
730
|
+
channel.notify(t('workspace-command-failed', { err: error instanceof Error ? error.message : String(error) }), { color: 'error', timeoutMs: 8000 });
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
};
|
|
484
734
|
/**
|
|
485
735
|
* Dispatch a slash command; false lets the input flow to the model.
|
|
486
736
|
* Built-in names run the local switch; anything registered by a DSH
|
|
@@ -488,6 +738,41 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
488
738
|
* result text lands as a notification. `rawInput` carries the text after
|
|
489
739
|
* the command name (`/plan off` → ` off`).
|
|
490
740
|
*/
|
|
741
|
+
/** Localized display name of a sandbox mode id (`read-only` /
|
|
742
|
+
* `workspace-write` / `danger-full-access`), for `/permission status`. */
|
|
743
|
+
const permissionPresetName = (id) => {
|
|
744
|
+
switch (id) {
|
|
745
|
+
case 'read-only': return t('permission-preset-readonly');
|
|
746
|
+
case 'workspace-write': return t('permission-preset-workspace-write');
|
|
747
|
+
case 'danger-full-access': return t('permission-preset-full-access');
|
|
748
|
+
default: return id ?? '—';
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
/** Hot-swap the UI language (`/lang <id>` and the LangPicker both land
|
|
752
|
+
* here): persist to ~/.dsh-tui/lang.json and mirror into the dsh-tui
|
|
753
|
+
* settings namespace when it is served (best effort). */
|
|
754
|
+
const applyLang = (lang) => {
|
|
755
|
+
const ok = writeLangPref(lang);
|
|
756
|
+
setLang(lang);
|
|
757
|
+
const settingsHost = channel.settingsHost();
|
|
758
|
+
const tuiView = settingsHost?.listNamespaces().find(entry => entry.ns === 'dsh-tui');
|
|
759
|
+
if (settingsHost !== undefined && tuiView !== undefined) {
|
|
760
|
+
void settingsHost
|
|
761
|
+
.write('dsh-tui', [{ op: 'set', path: ['lang'], value: lang }], tuiView.revision)
|
|
762
|
+
.catch(() => { });
|
|
763
|
+
}
|
|
764
|
+
channel.notify(ok ? t('lang-switched', { lang }) : t('lang-switch-failed', { lang }), { color: ok ? 'success' : 'error' });
|
|
765
|
+
};
|
|
766
|
+
/** Localized label of one /reload surface, for the change report. */
|
|
767
|
+
const reloadKindLabel = (kind) => {
|
|
768
|
+
switch (kind) {
|
|
769
|
+
case 'theme': return t('reload-kind-theme');
|
|
770
|
+
case 'lang': return t('reload-kind-lang');
|
|
771
|
+
case 'preset': return t('reload-kind-preset');
|
|
772
|
+
case 'model': return t('reload-kind-model');
|
|
773
|
+
case 'activity': return t('reload-kind-activity');
|
|
774
|
+
}
|
|
775
|
+
};
|
|
491
776
|
const runCommand = (name, rawInput = '') => {
|
|
492
777
|
switch (name) {
|
|
493
778
|
case 'activity': {
|
|
@@ -524,8 +809,13 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
524
809
|
return true;
|
|
525
810
|
}
|
|
526
811
|
setHelpOpen(false);
|
|
527
|
-
|
|
528
|
-
|
|
812
|
+
dispatchOverlay({
|
|
813
|
+
type: 'open',
|
|
814
|
+
overlay: {
|
|
815
|
+
kind: 'activity',
|
|
816
|
+
index: Math.max(0, PRESET_NAMES.indexOf(channel.activityFrames ?? 'random')),
|
|
817
|
+
},
|
|
818
|
+
});
|
|
529
819
|
return true;
|
|
530
820
|
}
|
|
531
821
|
case 'preset': {
|
|
@@ -552,16 +842,26 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
552
842
|
return true;
|
|
553
843
|
}
|
|
554
844
|
setHelpOpen(false);
|
|
555
|
-
|
|
845
|
+
// The picker opens immediately over the cached roster (no loading
|
|
846
|
+
// pane — deliberate contrast with /model); the fresh list lands with
|
|
847
|
+
// the authoritative focus. Both loader writes are kind-guarded, so a
|
|
848
|
+
// picker the user already left is not resurrected or re-focused.
|
|
849
|
+
dispatchOverlay({
|
|
850
|
+
type: 'open',
|
|
851
|
+
overlay: {
|
|
852
|
+
kind: 'preset',
|
|
853
|
+
index: Math.max(0, presetOptions.findIndex(preset => preset.id === channel.agentPreset)),
|
|
854
|
+
},
|
|
855
|
+
});
|
|
556
856
|
void channel.listPresets().then((list) => {
|
|
557
857
|
if (list.length === 0) {
|
|
558
|
-
|
|
858
|
+
dispatchOverlay({ type: 'close-if', kind: 'preset' });
|
|
559
859
|
channel.notify(t('preset-roster-unmounted'), { color: 'warning' });
|
|
560
860
|
return;
|
|
561
861
|
}
|
|
562
862
|
setPresetOptions(list);
|
|
563
863
|
const index = list.findIndex(preset => preset.id === channel.agentPreset);
|
|
564
|
-
|
|
864
|
+
dispatchOverlay({ type: 'set-index', kind: 'preset', index: index >= 0 ? index : 0 });
|
|
565
865
|
});
|
|
566
866
|
return true;
|
|
567
867
|
}
|
|
@@ -592,16 +892,22 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
592
892
|
setEffortOptions(efforts);
|
|
593
893
|
const current = channel.reasoningEffort ?? defaultEffort;
|
|
594
894
|
const index = efforts.findIndex(effort => effort.id === current);
|
|
595
|
-
|
|
596
|
-
|
|
895
|
+
// open-if: a picker the user opened during the round trip wins
|
|
896
|
+
// over this late-arriving slider.
|
|
897
|
+
dispatchOverlay({
|
|
898
|
+
type: 'open-if',
|
|
899
|
+
overlay: { kind: 'effort', index: index >= 0 ? index : 0 },
|
|
900
|
+
when: ['none'],
|
|
901
|
+
});
|
|
597
902
|
});
|
|
598
903
|
return true;
|
|
599
904
|
}
|
|
600
905
|
case 'lang': {
|
|
601
906
|
// `/lang` shows the current UI language, `/lang en|zh` switches
|
|
602
|
-
// (hot-swap, persisted to ~/.dsh-tui/lang.json)
|
|
603
|
-
// launch: DSH_TUI_LANG >
|
|
604
|
-
// `lang` > the persisted
|
|
907
|
+
// (hot-swap, persisted to ~/.dsh-tui/lang.json), bare `/lang` opens
|
|
908
|
+
// the en/zh picker. Precedence on next launch: DSH_TUI_LANG >
|
|
909
|
+
// settings.yaml `dsh-tui.lang` > cordis.yml `lang` > the persisted
|
|
910
|
+
// choice.
|
|
605
911
|
const parts = rawInput.trim().split(/\s+/).filter(Boolean);
|
|
606
912
|
if (parts[0] === 'status') {
|
|
607
913
|
setHelpOpen(false);
|
|
@@ -615,19 +921,7 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
615
921
|
if (parts.length > 0) {
|
|
616
922
|
setHelpOpen(false);
|
|
617
923
|
if (isLang(parts[0])) {
|
|
618
|
-
|
|
619
|
-
setLang(parts[0]);
|
|
620
|
-
// Mirror into the dsh-tui settings namespace when it is served,
|
|
621
|
-
// so /settings and the next boot see the same last-write-wins
|
|
622
|
-
// choice (best effort; lang.json stays the fallback).
|
|
623
|
-
const settingsHost = channel.settingsHost();
|
|
624
|
-
const tuiView = settingsHost?.listNamespaces().find(entry => entry.ns === 'dsh-tui');
|
|
625
|
-
if (settingsHost !== undefined && tuiView !== undefined) {
|
|
626
|
-
void settingsHost
|
|
627
|
-
.write('dsh-tui', [{ op: 'set', path: ['lang'], value: parts[0] }], tuiView.revision)
|
|
628
|
-
.catch(() => { });
|
|
629
|
-
}
|
|
630
|
-
channel.notify(ok ? t('lang-switched', { lang: parts[0] }) : t('lang-switch-failed', { lang: parts[0] }), { color: ok ? 'success' : 'error' });
|
|
924
|
+
applyLang(parts[0]);
|
|
631
925
|
}
|
|
632
926
|
else {
|
|
633
927
|
channel.notify(t('lang-unknown', { lang: parts[0] }), { color: 'error' });
|
|
@@ -635,11 +929,10 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
635
929
|
return true;
|
|
636
930
|
}
|
|
637
931
|
setHelpOpen(false);
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
]);
|
|
932
|
+
dispatchOverlay({
|
|
933
|
+
type: 'open',
|
|
934
|
+
overlay: { kind: 'lang', index: getLang() === 'zh' ? 0 : 1 },
|
|
935
|
+
});
|
|
643
936
|
return true;
|
|
644
937
|
}
|
|
645
938
|
case 'theme': {
|
|
@@ -672,15 +965,59 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
672
965
|
return true;
|
|
673
966
|
}
|
|
674
967
|
setHelpOpen(false);
|
|
675
|
-
|
|
676
|
-
|
|
968
|
+
dispatchOverlay({
|
|
969
|
+
type: 'open',
|
|
970
|
+
overlay: {
|
|
971
|
+
kind: 'theme',
|
|
972
|
+
index: Math.max(0, getThemeOptions().findIndex(option => option.value === themeName)),
|
|
973
|
+
},
|
|
974
|
+
});
|
|
975
|
+
return true;
|
|
976
|
+
}
|
|
977
|
+
case 'color': {
|
|
978
|
+
// `/color`(CC accent,按会话持久化):无参打开调色板选择器,
|
|
979
|
+
// `/color <name>` 直接设置,`/color status` 显示当前,`/color
|
|
980
|
+
// reset` 清除回主题默认。颜色经 `session/color` 事件按会话保存
|
|
981
|
+
// ——resume/rewind 后仍是这个会话自己的颜色(见 channel.ts)。
|
|
982
|
+
setHelpOpen(false);
|
|
983
|
+
const parts = rawInput.trim().split(/\s+/).filter(Boolean);
|
|
984
|
+
if (parts.length === 0) {
|
|
985
|
+
dispatchOverlay({
|
|
986
|
+
type: 'open',
|
|
987
|
+
overlay: {
|
|
988
|
+
kind: 'color',
|
|
989
|
+
index: Math.max(0, SESSION_COLOR_NAMES.indexOf(channel.sessionColor)),
|
|
990
|
+
},
|
|
991
|
+
});
|
|
992
|
+
return true;
|
|
993
|
+
}
|
|
994
|
+
if (parts[0] === 'status') {
|
|
995
|
+
channel.pushLocal('/color', [
|
|
996
|
+
channel.sessionColor === ''
|
|
997
|
+
? t('color-current-none')
|
|
998
|
+
: t('color-current', { name: channel.sessionColor }),
|
|
999
|
+
t('color-usage', { list: SESSION_COLOR_NAMES.join('/') }),
|
|
1000
|
+
]);
|
|
1001
|
+
return true;
|
|
1002
|
+
}
|
|
1003
|
+
if (parts[0] === 'reset') {
|
|
1004
|
+
channel.setSessionColor('');
|
|
1005
|
+
channel.notify(t('color-reset'));
|
|
1006
|
+
return true;
|
|
1007
|
+
}
|
|
1008
|
+
const colorName = parts[0].toLowerCase();
|
|
1009
|
+
if (!isValidSessionColor(colorName)) {
|
|
1010
|
+
channel.notify(t('color-unknown', { name: colorName, list: SESSION_COLOR_NAMES.join(' · ') }), { color: 'error' });
|
|
1011
|
+
return true;
|
|
1012
|
+
}
|
|
1013
|
+
channel.setSessionColor(colorName);
|
|
1014
|
+
channel.notify(t('color-set', { name: colorName }), { color: 'success' });
|
|
677
1015
|
return true;
|
|
678
1016
|
}
|
|
679
1017
|
case 'new': {
|
|
680
1018
|
// One-shot `/new` (issue #25): the old session stays persisted and
|
|
681
1019
|
// is recoverable via /resume, so discarding the live view is
|
|
682
|
-
// non-destructive — no CC-style "press /new again" confirmation.
|
|
683
|
-
setHelpOpen(false);
|
|
1020
|
+
// non-destructive — no CC-style "press /new again" confirmation. setHelpOpen(false)
|
|
684
1021
|
void channel.newSession().then((ok) => {
|
|
685
1022
|
if (!ok)
|
|
686
1023
|
return;
|
|
@@ -735,32 +1072,102 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
735
1072
|
case 'help':
|
|
736
1073
|
setHelpOpen(true);
|
|
737
1074
|
return true;
|
|
738
|
-
case 'model':
|
|
1075
|
+
case 'model': {
|
|
1076
|
+
// `/model <provider/model>` switches directly (same live-fork path
|
|
1077
|
+
// as the picker's Enter), bare `/model` opens the picker.
|
|
1078
|
+
const parts = rawInput.trim().split(/\s+/).filter(Boolean);
|
|
1079
|
+
if (parts.length > 0) {
|
|
1080
|
+
setHelpOpen(false);
|
|
1081
|
+
const spec = parts[0];
|
|
1082
|
+
const slash = spec.indexOf('/');
|
|
1083
|
+
const provider = slash >= 0 ? spec.slice(0, slash) : undefined;
|
|
1084
|
+
const id = slash >= 0 ? spec.slice(slash + 1) : spec;
|
|
1085
|
+
if (provider === undefined || id.length === 0 || provider.length === 0) {
|
|
1086
|
+
channel.notify(t('model-usage'), { color: 'warning' });
|
|
1087
|
+
return true;
|
|
1088
|
+
}
|
|
1089
|
+
void channel.listModels().then((list) => {
|
|
1090
|
+
const model = list.find(m => m.provider === provider && m.id === id);
|
|
1091
|
+
if (model === undefined) {
|
|
1092
|
+
channel.notify(t('model-unknown', { spec }), { color: 'error', timeoutMs: 8000 });
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
void switchModelRecorded(provider, id, model.name);
|
|
1096
|
+
});
|
|
1097
|
+
return true;
|
|
1098
|
+
}
|
|
739
1099
|
setHelpOpen(false);
|
|
740
|
-
|
|
1100
|
+
// Opens over the cached catalog (empty cache shows the loading
|
|
1101
|
+
// pane); the fresh list lands with the authoritative focus, and the
|
|
1102
|
+
// kind-guarded set-index cannot re-focus a picker the user left.
|
|
1103
|
+
// Seed-on-open: the model in use IS a use — recording it here means
|
|
1104
|
+
// the recents group exists before the first post-update switch, and
|
|
1105
|
+
// switching A→B keeps A in the list (the file records what was
|
|
1106
|
+
// used, not only switches made after the file appeared).
|
|
1107
|
+
let recentsNow = modelRecents;
|
|
1108
|
+
if (channel.provider !== '' && channel.model !== ''
|
|
1109
|
+
&& !recentsNow.some(ref => ref.provider === channel.provider && ref.id === channel.model)) {
|
|
1110
|
+
recentsNow = recordModelUse({ provider: channel.provider, id: channel.model });
|
|
1111
|
+
setModelRecents(recentsNow);
|
|
1112
|
+
}
|
|
1113
|
+
// Two-level landing: recents (when catalogued) focus their pinned
|
|
1114
|
+
// row; else multi-provider catalogs focus the current provider's
|
|
1115
|
+
// group row; a single-provider catalog without a meaningful recents
|
|
1116
|
+
// list drills straight into its model list (pre-grouping UX).
|
|
1117
|
+
{
|
|
1118
|
+
const landing = modelPickerLanding(models, channel.provider, channel.model, recentsNow);
|
|
1119
|
+
setModelGroup(landing.group);
|
|
1120
|
+
setModelPickerDirect(landing.group !== undefined);
|
|
1121
|
+
dispatchOverlay({ type: 'open', overlay: { kind: 'model', index: landing.index } });
|
|
1122
|
+
}
|
|
741
1123
|
void channel.listModels().then((list) => {
|
|
742
1124
|
setModels(list);
|
|
743
|
-
const
|
|
744
|
-
|
|
1125
|
+
const landing = modelPickerLanding(list, channel.provider, channel.model, recentsNow);
|
|
1126
|
+
setModelGroup(landing.group);
|
|
1127
|
+
setModelPickerDirect(landing.group !== undefined);
|
|
1128
|
+
dispatchOverlay({ type: 'set-index', kind: 'model', index: landing.index });
|
|
745
1129
|
});
|
|
1130
|
+
void channel.listProviders().then(setProviderInfos).catch(() => setProviderInfos([]));
|
|
746
1131
|
return true;
|
|
747
|
-
|
|
1132
|
+
}
|
|
1133
|
+
case 'skills': {
|
|
748
1134
|
// issue #204: 列出当前 agent 的完整技能目录(名称 + 来源 + 简述),
|
|
749
1135
|
// Enter 把可直调技能以 `/name ` 填回输入行(completion-only 分发的
|
|
750
1136
|
// 同一路径)。注册表读取走 channel(快照 scoped 到 live agent)。
|
|
1137
|
+
// `/skills <name>` 直达同一个填回动作,跳过选择器。
|
|
1138
|
+
const parts = rawInput.trim().split(/\s+/).filter(Boolean);
|
|
1139
|
+
if (parts.length > 0) {
|
|
1140
|
+
setHelpOpen(false);
|
|
1141
|
+
void channel.listSkills().then((list) => {
|
|
1142
|
+
if (list === undefined) {
|
|
1143
|
+
channel.notify(t('skills-load-failed'), { color: 'error' });
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
const skill = list.find(s => s.name === parts[0]);
|
|
1147
|
+
if (skill === undefined) {
|
|
1148
|
+
channel.notify(t('skills-unknown', { name: parts[0] }), { color: 'error' });
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
if (skill.userInvocable)
|
|
1152
|
+
setHistoryFill(`/${skill.name} `);
|
|
1153
|
+
else
|
|
1154
|
+
channel.notify(t('skills-not-invocable', { name: parts[0] }), { color: 'warning' });
|
|
1155
|
+
});
|
|
1156
|
+
return true;
|
|
1157
|
+
}
|
|
751
1158
|
setHelpOpen(false);
|
|
752
1159
|
setSkillsList(null);
|
|
753
|
-
|
|
754
|
-
setSkillsPickerOpen(true);
|
|
1160
|
+
dispatchOverlay({ type: 'open', overlay: { kind: 'skills', index: 0 } });
|
|
755
1161
|
void channel.listSkills().then((list) => {
|
|
756
1162
|
if (list === undefined) {
|
|
757
|
-
|
|
1163
|
+
dispatchOverlay({ type: 'close-if', kind: 'skills' });
|
|
758
1164
|
channel.notify(t('skills-load-failed'), { color: 'error' });
|
|
759
1165
|
return;
|
|
760
1166
|
}
|
|
761
1167
|
setSkillsList(list);
|
|
762
1168
|
});
|
|
763
1169
|
return true;
|
|
1170
|
+
}
|
|
764
1171
|
case 'provider': {
|
|
765
1172
|
// Interactive add-provider wizard (/provider): drives the shared
|
|
766
1173
|
// question panel, persists profile + key via the channel's settings/
|
|
@@ -777,7 +1184,7 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
777
1184
|
notify: (text, options) => channel.notify(text, options),
|
|
778
1185
|
pushLocal: (title, lines) => channel.pushLocal(title, lines),
|
|
779
1186
|
working: () => channel.working,
|
|
780
|
-
switchModel: (provider, model) =>
|
|
1187
|
+
switchModel: (provider, model) => switchModelRecorded(provider, model),
|
|
781
1188
|
}).catch(() => {
|
|
782
1189
|
// The wizard notifies on every handled failure; this only swallows
|
|
783
1190
|
// an unexpected reject so it never surfaces as an unhandled promise.
|
|
@@ -786,8 +1193,10 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
786
1193
|
}
|
|
787
1194
|
case 'thinking':
|
|
788
1195
|
setHelpOpen(false);
|
|
789
|
-
|
|
790
|
-
|
|
1196
|
+
dispatchOverlay({
|
|
1197
|
+
type: 'open',
|
|
1198
|
+
overlay: { kind: 'thinking', focus: thinkingVisible ? 0 : 1 },
|
|
1199
|
+
});
|
|
791
1200
|
return true;
|
|
792
1201
|
case 'tokens': {
|
|
793
1202
|
const usage = t('tokens-usage', { in: formatTokens(channel.tokens.input), out: formatTokens(channel.tokens.output) });
|
|
@@ -815,10 +1224,9 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
815
1224
|
const subcommand = (separator < 0 ? trimmed : trimmed.slice(0, separator)).toLowerCase();
|
|
816
1225
|
const input = separator < 0 ? '' : trimmed.slice(separator).trim();
|
|
817
1226
|
if (subcommand === '') {
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
channel.pushLocal('/workspace', [t('workspace-command-usage', { commands: extensions })]);
|
|
1227
|
+
// Bare `/workspace` opens the action menu (resume / rename / open
|
|
1228
|
+
// plus any registered extensions) instead of a text usage line.
|
|
1229
|
+
dispatchOverlay({ type: 'open', overlay: { kind: 'workspace-menu', index: 0 } });
|
|
822
1230
|
}
|
|
823
1231
|
else if (subcommand === 'resume') {
|
|
824
1232
|
openWorkspaceResume();
|
|
@@ -869,6 +1277,21 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
869
1277
|
setHelpOpen(false);
|
|
870
1278
|
openRewind();
|
|
871
1279
|
return true;
|
|
1280
|
+
case 'tree': {
|
|
1281
|
+
// The session family tree (pi's Session Tree): every fork branch
|
|
1282
|
+
// stitched back, hover previews, per-node rewind/fork/adopt.
|
|
1283
|
+
setHelpOpen(false);
|
|
1284
|
+
setTreeOpen(true);
|
|
1285
|
+
return true;
|
|
1286
|
+
}
|
|
1287
|
+
case 'fork': {
|
|
1288
|
+
// Tip fork (kimi-code semantics): a persisted copy of the whole
|
|
1289
|
+
// conversation the user enters via /resume — the live session and
|
|
1290
|
+
// its running turn stay untouched.
|
|
1291
|
+
setHelpOpen(false);
|
|
1292
|
+
void channel.forkSession();
|
|
1293
|
+
return true;
|
|
1294
|
+
}
|
|
872
1295
|
case 'exit':
|
|
873
1296
|
case 'quit':
|
|
874
1297
|
case 'q':
|
|
@@ -972,12 +1395,16 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
972
1395
|
setHelpOpen(false);
|
|
973
1396
|
void channel.describeCredential('DEEPSEEK_API_KEY')
|
|
974
1397
|
.catch(() => undefined)
|
|
975
|
-
.then(status => {
|
|
1398
|
+
.then(async (status) => {
|
|
976
1399
|
const keyStatus = status === undefined
|
|
977
1400
|
? t('login-credentials-unavailable')
|
|
978
1401
|
: status.configured
|
|
979
1402
|
? t('login-key-configured', { ref: 'DEEPSEEK_API_KEY' })
|
|
980
1403
|
: t('login-key-missing');
|
|
1404
|
+
// OAuth account states ride along only while a dsh-auth-style
|
|
1405
|
+
// plugin is mounted; absent it the lines below are exactly the
|
|
1406
|
+
// pre-plugin set.
|
|
1407
|
+
const oauth = await channel.oauthProviderStatuses().catch(() => undefined);
|
|
981
1408
|
channel.pushLocal('/login', [
|
|
982
1409
|
t('login-api-key', { status: keyStatus }),
|
|
983
1410
|
...(status === undefined
|
|
@@ -989,6 +1416,20 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
989
1416
|
}),
|
|
990
1417
|
]),
|
|
991
1418
|
t('login-base-url', { url: process.env.DEEPSEEK_BASE_URL ?? t('login-official-endpoint') }),
|
|
1419
|
+
...(oauth === undefined
|
|
1420
|
+
? []
|
|
1421
|
+
: [
|
|
1422
|
+
t('login-oauth-heading'),
|
|
1423
|
+
...oauth.map(row => t('login-oauth-row', {
|
|
1424
|
+
provider: row.provider,
|
|
1425
|
+
state: row.signedIn
|
|
1426
|
+
? t('login-oauth-in', { time: new Date(row.expiresAt ?? 0).toISOString() })
|
|
1427
|
+
: row.expired
|
|
1428
|
+
? t('login-oauth-expired')
|
|
1429
|
+
: t('login-oauth-signed-out'),
|
|
1430
|
+
})),
|
|
1431
|
+
t('login-oauth-hint'),
|
|
1432
|
+
]),
|
|
992
1433
|
]);
|
|
993
1434
|
});
|
|
994
1435
|
return true;
|
|
@@ -996,25 +1437,87 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
996
1437
|
case 'logout':
|
|
997
1438
|
channel.notify(t('login-logout-hint'));
|
|
998
1439
|
return true;
|
|
999
|
-
case '
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
])
|
|
1012
|
-
|
|
1440
|
+
case 'permission': {
|
|
1441
|
+
// The command itself is registered by dsh-sandbox-policy (dsh-base
|
|
1442
|
+
// permission-presets row): bare `/permission` opens the preset
|
|
1443
|
+
// picker (read-only / workspace-write / danger-full-access) and
|
|
1444
|
+
// Enter dispatches `/permission <preset>` through the same
|
|
1445
|
+
// external-command path a hand-typed argument takes. `/permission
|
|
1446
|
+
// status` prints the policy explainer (absorbed from the removed
|
|
1447
|
+
// `/permissions` command); other arguments pass through verbatim.
|
|
1448
|
+
// When the row is not mounted the default external path (or the
|
|
1449
|
+
// model, when nothing is registered) wins.
|
|
1450
|
+
const mounted = channel.commandList.some(command => command.external && command.name === 'permission');
|
|
1451
|
+
const parts = rawInput.trim().split(/\s+/).filter(Boolean);
|
|
1452
|
+
if (mounted && parts[0] === 'status') {
|
|
1453
|
+
setHelpOpen(false);
|
|
1454
|
+
channel.pushLocal('/permission', [
|
|
1455
|
+
t('permission-current', { name: permissionPresetName(channel.mode.sandbox) }),
|
|
1456
|
+
t('permission-policy-hint'),
|
|
1457
|
+
t('permission-approval-hint'),
|
|
1458
|
+
t('permission-root-hint', { cwd: channel.cwd }),
|
|
1459
|
+
t('permission-path-hint'),
|
|
1460
|
+
]);
|
|
1461
|
+
return true;
|
|
1462
|
+
}
|
|
1463
|
+
if (mounted && parts.length === 0) {
|
|
1464
|
+
setHelpOpen(false);
|
|
1465
|
+
const index = PERMISSION_PRESET_IDS.indexOf(channel.mode.sandbox ?? '');
|
|
1466
|
+
dispatchOverlay({
|
|
1467
|
+
type: 'open',
|
|
1468
|
+
overlay: { kind: 'permission', index: index >= 0 ? index : 1 },
|
|
1469
|
+
});
|
|
1470
|
+
return true;
|
|
1471
|
+
}
|
|
1472
|
+
if (mounted) {
|
|
1473
|
+
setHelpOpen(false);
|
|
1474
|
+
void channel.runExternalCommand('permission', rawInput).then((text) => {
|
|
1475
|
+
if (text !== undefined && text !== '') {
|
|
1476
|
+
channel.notify(text);
|
|
1477
|
+
}
|
|
1478
|
+
else if (text === undefined) {
|
|
1479
|
+
channel.notify(t('command-not-found', { name: 'permission' }), { color: 'error' });
|
|
1480
|
+
}
|
|
1481
|
+
});
|
|
1482
|
+
return true;
|
|
1483
|
+
}
|
|
1484
|
+
return false;
|
|
1485
|
+
}
|
|
1486
|
+
case 'plan': {
|
|
1487
|
+
// Registered by dsh-plan-mode: bare `/plan` opens an on/off picker
|
|
1488
|
+
// marked with the current state instead of toggling blindly; Enter
|
|
1489
|
+
// dispatches `/plan` or `/plan off`. Arguments pass through verbatim
|
|
1490
|
+
// (`/plan off`), and an unmounted row falls back to the default
|
|
1491
|
+
// external path.
|
|
1492
|
+
const mounted = channel.commandList.some(command => command.external && command.name === 'plan');
|
|
1493
|
+
const parts = rawInput.trim().split(/\s+/).filter(Boolean);
|
|
1494
|
+
if (mounted && parts.length === 0) {
|
|
1495
|
+
setHelpOpen(false);
|
|
1496
|
+
dispatchOverlay({
|
|
1497
|
+
type: 'open',
|
|
1498
|
+
overlay: { kind: 'plan', index: channel.mode.plan === true ? 0 : 1 },
|
|
1499
|
+
});
|
|
1500
|
+
return true;
|
|
1501
|
+
}
|
|
1502
|
+
if (mounted) {
|
|
1503
|
+
setHelpOpen(false);
|
|
1504
|
+
void channel.runExternalCommand('plan', rawInput).then((text) => {
|
|
1505
|
+
if (text !== undefined && text !== '') {
|
|
1506
|
+
channel.notify(text);
|
|
1507
|
+
}
|
|
1508
|
+
else if (text === undefined) {
|
|
1509
|
+
channel.notify(t('command-not-found', { name: 'plan' }), { color: 'error' });
|
|
1510
|
+
}
|
|
1511
|
+
});
|
|
1512
|
+
return true;
|
|
1513
|
+
}
|
|
1514
|
+
return false;
|
|
1515
|
+
}
|
|
1013
1516
|
case 'add-dir':
|
|
1014
1517
|
setHelpOpen(false);
|
|
1015
1518
|
channel.pushLocal('/add-dir', [
|
|
1016
|
-
t('
|
|
1017
|
-
t('
|
|
1519
|
+
t('permission-root-hint', { cwd: channel.cwd }),
|
|
1520
|
+
t('permission-path-hint'),
|
|
1018
1521
|
]);
|
|
1019
1522
|
return true;
|
|
1020
1523
|
case 'hooks':
|
|
@@ -1041,6 +1544,100 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1041
1544
|
onUpdate();
|
|
1042
1545
|
}
|
|
1043
1546
|
return true;
|
|
1547
|
+
case 'reload': {
|
|
1548
|
+
// pi-style soft reload: re-read the persisted preference files
|
|
1549
|
+
// (~/.dsh-tui/{theme,lang,agent-preset,model,working-activity}.json)
|
|
1550
|
+
// and re-apply live, honoring the boot-time precedence (env >
|
|
1551
|
+
// cordis.yml > settings user layer > pref). The dsh-tui settings
|
|
1552
|
+
// namespace is NOT re-read here — its watch applies edits live and
|
|
1553
|
+
// the platform watcher hot-reloads settings.yaml itself. What no
|
|
1554
|
+
// reload can re-read (cordis.yml root config, frozen fullscreen,
|
|
1555
|
+
// newly built code) is listed in the footer and served by /restart.
|
|
1556
|
+
setHelpOpen(false);
|
|
1557
|
+
const tuiNamespace = channel.settingsHost()
|
|
1558
|
+
?.listNamespaces()
|
|
1559
|
+
.find(entry => entry.ns === 'dsh-tui');
|
|
1560
|
+
const plan = planReload({
|
|
1561
|
+
envTheme: envThemeOverride(),
|
|
1562
|
+
envLang: isLang(process.env.DSH_TUI_LANG) ? process.env.DSH_TUI_LANG : undefined,
|
|
1563
|
+
themePref: readThemePref(),
|
|
1564
|
+
currentTheme: themeName,
|
|
1565
|
+
langPref: readLangPref(),
|
|
1566
|
+
currentLang: getLang(),
|
|
1567
|
+
langOverriddenBySettings: tuiNamespace !== undefined && hasPath(tuiNamespace.user, ['lang']),
|
|
1568
|
+
configuredLang: channel.configuredLang,
|
|
1569
|
+
configuredPreset: channel.configuredPreset,
|
|
1570
|
+
presetPref: readPresetPref(),
|
|
1571
|
+
currentPreset: channel.agentPreset,
|
|
1572
|
+
configuredModel: {
|
|
1573
|
+
provider: channel.configuredProvider,
|
|
1574
|
+
model: channel.configuredModel,
|
|
1575
|
+
},
|
|
1576
|
+
modelPref: readModelPref(),
|
|
1577
|
+
currentModel: { provider: channel.provider, model: channel.model },
|
|
1578
|
+
configuredActivity: channel.configuredActivityFrames,
|
|
1579
|
+
activityPref: readActivityFrames(),
|
|
1580
|
+
currentActivity: channel.activityFrames,
|
|
1581
|
+
});
|
|
1582
|
+
for (const item of plan.apply) {
|
|
1583
|
+
switch (item.kind) {
|
|
1584
|
+
case 'theme':
|
|
1585
|
+
setTheme(item.to);
|
|
1586
|
+
break;
|
|
1587
|
+
case 'lang':
|
|
1588
|
+
applyLang(item.to);
|
|
1589
|
+
break;
|
|
1590
|
+
case 'preset':
|
|
1591
|
+
void channel.switchPreset(item.to);
|
|
1592
|
+
break;
|
|
1593
|
+
case 'model':
|
|
1594
|
+
if (item.route !== undefined) {
|
|
1595
|
+
void switchModelRecorded(item.route.provider, item.route.model);
|
|
1596
|
+
}
|
|
1597
|
+
break;
|
|
1598
|
+
case 'activity':
|
|
1599
|
+
channel.setActivityFrames(item.to);
|
|
1600
|
+
break;
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
const lines = [t('reload-header')];
|
|
1604
|
+
for (const item of plan.apply) {
|
|
1605
|
+
lines.push(t('reload-applied', {
|
|
1606
|
+
kind: reloadKindLabel(item.kind),
|
|
1607
|
+
from: item.from,
|
|
1608
|
+
to: item.to,
|
|
1609
|
+
}));
|
|
1610
|
+
}
|
|
1611
|
+
for (const kind of plan.unchanged) {
|
|
1612
|
+
lines.push(t('reload-unchanged', { kind: reloadKindLabel(kind) }));
|
|
1613
|
+
}
|
|
1614
|
+
for (const skip of plan.skipped) {
|
|
1615
|
+
const key = skip.reason === 'env-wins'
|
|
1616
|
+
? 'reload-skipped-env'
|
|
1617
|
+
: skip.reason === 'config-wins' ? 'reload-skipped-config' : 'reload-skipped-invalid';
|
|
1618
|
+
lines.push(t(key, { kind: reloadKindLabel(skip.kind) }));
|
|
1619
|
+
}
|
|
1620
|
+
lines.push(t('reload-footer'));
|
|
1621
|
+
channel.pushLocal('/reload', lines);
|
|
1622
|
+
return true;
|
|
1623
|
+
}
|
|
1624
|
+
case 'restart':
|
|
1625
|
+
// pi-style reload tail: /reload cannot re-read boot-time-only state
|
|
1626
|
+
// (cordis.yml root config, frozen fullscreen layout, newly built
|
|
1627
|
+
// code), so /restart respawns the process with the original argv and
|
|
1628
|
+
// resumes this session — the /update handoff minus the pnpm step.
|
|
1629
|
+
setHelpOpen(false);
|
|
1630
|
+
if (onRestart === undefined) {
|
|
1631
|
+
channel.notify(t('restart-unavailable'), { color: 'warning' });
|
|
1632
|
+
}
|
|
1633
|
+
else if (channel.working) {
|
|
1634
|
+
channel.notify(t('update-working'), { color: 'warning' });
|
|
1635
|
+
}
|
|
1636
|
+
else {
|
|
1637
|
+
channel.notify(t('restart-starting'));
|
|
1638
|
+
onRestart();
|
|
1639
|
+
}
|
|
1640
|
+
return true;
|
|
1044
1641
|
case 'vim':
|
|
1045
1642
|
channel.notify(t('vim-not-implemented'));
|
|
1046
1643
|
return true;
|
|
@@ -1051,6 +1648,33 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1051
1648
|
t('terminal-paste-hint', { mod: modLabel }),
|
|
1052
1649
|
]);
|
|
1053
1650
|
return true;
|
|
1651
|
+
case 'recap': {
|
|
1652
|
+
// `/recap`(pi-recap 语义):对会话最近活动做一次无工具单轮
|
|
1653
|
+
// 调用,生成一行摘要 + 建议标题。摘要是纯 UI 状态(不进 transcript
|
|
1654
|
+
// 也不进 session log);建议标题经「应用」按钮走 /rename 路径。
|
|
1655
|
+
setHelpOpen(false);
|
|
1656
|
+
recapAbortRef.current?.abort();
|
|
1657
|
+
const controller = new AbortController();
|
|
1658
|
+
recapAbortRef.current = controller;
|
|
1659
|
+
setRecap({ raw: '', summary: '', error: undefined, done: false, titleApplied: false, auto: false, expanded: true });
|
|
1660
|
+
void channel.recapRecent({
|
|
1661
|
+
signal: controller.signal,
|
|
1662
|
+
onText: delta => setRecap(prev => (prev ? { ...prev, raw: prev.raw + delta } : prev)),
|
|
1663
|
+
}).then(result => {
|
|
1664
|
+
if (controller.signal.aborted)
|
|
1665
|
+
return;
|
|
1666
|
+
setRecap(prev => (prev
|
|
1667
|
+
? {
|
|
1668
|
+
...prev,
|
|
1669
|
+
summary: result.summary ?? prev.raw,
|
|
1670
|
+
title: result.title,
|
|
1671
|
+
error: result.error,
|
|
1672
|
+
done: true,
|
|
1673
|
+
}
|
|
1674
|
+
: prev));
|
|
1675
|
+
});
|
|
1676
|
+
return true;
|
|
1677
|
+
}
|
|
1054
1678
|
case 'btw': {
|
|
1055
1679
|
// CC /btw:单轮无工具侧问,overlay 态纯 UI,不打断主回合、不写
|
|
1056
1680
|
// 会话历史。空参数只提示用法。
|
|
@@ -1088,7 +1712,7 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1088
1712
|
}
|
|
1089
1713
|
case 'tips':
|
|
1090
1714
|
setHelpOpen(false);
|
|
1091
|
-
|
|
1715
|
+
dispatchOverlay({ type: 'open', overlay: { kind: 'tips' } });
|
|
1092
1716
|
return true;
|
|
1093
1717
|
case 'connect':
|
|
1094
1718
|
setHelpOpen(false);
|
|
@@ -1142,6 +1766,9 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1142
1766
|
? channel.rows.filter(row => SELECTABLE_KINDS.has(row.kind))
|
|
1143
1767
|
: NO_ROWS;
|
|
1144
1768
|
// ctrl+r history search: substring match on the query, newest first.
|
|
1769
|
+
// The draft lives in the overlay variant; the derived '' while closed
|
|
1770
|
+
// keeps the memo inputs stable (nothing renders the matches then).
|
|
1771
|
+
const historyQuery = overlay.kind === 'history' ? overlay.query : '';
|
|
1145
1772
|
const historyMatches = React.useMemo(() => {
|
|
1146
1773
|
const q = historyQuery.trim().toLowerCase();
|
|
1147
1774
|
return q ? historyEntries.filter(e => e.text.toLowerCase().includes(q)) : historyEntries;
|
|
@@ -1150,15 +1777,15 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1150
1777
|
// selectable user turns; steering side-questions are excluded). Computed
|
|
1151
1778
|
// per render while the picker is open — `channel.rows` is a live in-place
|
|
1152
1779
|
// array (see selectableRows).
|
|
1153
|
-
const rewindRows =
|
|
1780
|
+
const rewindRows = overlay.kind === 'rewind'
|
|
1154
1781
|
? channel.rows
|
|
1155
1782
|
.filter(row => row.kind === 'user' && row.label === undefined)
|
|
1156
1783
|
.reverse()
|
|
1157
1784
|
: NO_ROWS;
|
|
1158
1785
|
/** Open the rewind picker (from PromptInput's double-Esc on an empty input). */
|
|
1159
1786
|
const openRewind = () => {
|
|
1160
|
-
//
|
|
1161
|
-
// directly instead of reading the gated list.
|
|
1787
|
+
// The overlay is not 'rewind' yet this render, so rewindRows is empty —
|
|
1788
|
+
// scan directly instead of reading the gated list.
|
|
1162
1789
|
const candidates = channel.rows
|
|
1163
1790
|
.filter(row => row.kind === 'user' && row.label === undefined)
|
|
1164
1791
|
.reverse();
|
|
@@ -1166,12 +1793,11 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1166
1793
|
channel.notify(t('rewind-none'));
|
|
1167
1794
|
return;
|
|
1168
1795
|
}
|
|
1169
|
-
setRewindIndex(0);
|
|
1170
|
-
setRewindConfirm(null);
|
|
1171
|
-
setRewindModes(null);
|
|
1172
|
-
setRewindBusy(false);
|
|
1173
1796
|
rewindRequestRef.current += 1;
|
|
1174
|
-
|
|
1797
|
+
dispatchOverlay({
|
|
1798
|
+
type: 'open',
|
|
1799
|
+
overlay: { kind: 'rewind', index: 0, confirm: null, modes: null, modeIndex: 0, busy: false },
|
|
1800
|
+
});
|
|
1175
1801
|
};
|
|
1176
1802
|
/**
|
|
1177
1803
|
* Enter on a rewind candidate: ask the plugins first (tui/rewind-prompt).
|
|
@@ -1180,16 +1806,15 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1180
1806
|
*/
|
|
1181
1807
|
const requestRewindConfirm = async (row) => {
|
|
1182
1808
|
const token = ++rewindRequestRef.current;
|
|
1183
|
-
|
|
1809
|
+
dispatchOverlay({ type: 'rewind-busy', busy: true });
|
|
1184
1810
|
const decision = await channel.promptRewind(row);
|
|
1185
1811
|
if (token !== rewindRequestRef.current)
|
|
1186
1812
|
return;
|
|
1187
|
-
|
|
1188
|
-
|
|
1813
|
+
if (decision === 'cancel') {
|
|
1814
|
+
dispatchOverlay({ type: 'rewind-busy', busy: false });
|
|
1189
1815
|
return;
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
setRewindModeIndex(0);
|
|
1816
|
+
}
|
|
1817
|
+
dispatchOverlay({ type: 'rewind-decision', confirm: row, modes: decision?.modes ?? null });
|
|
1193
1818
|
};
|
|
1194
1819
|
/** Execute the confirmed rewind; the message comes back into the input. */
|
|
1195
1820
|
const performRewind = async (row, mode = null) => {
|
|
@@ -1277,13 +1902,31 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1277
1902
|
}
|
|
1278
1903
|
setForceMountRowId(rowId);
|
|
1279
1904
|
};
|
|
1905
|
+
/**
|
|
1906
|
+
* Reveal-and-seek for a row folded behind the recent-rows window (the
|
|
1907
|
+
* rail's tick for an old turn, the doc's revealAndSeekRow): expand the
|
|
1908
|
+
* fold first, then the ordinary seek takes over — the completion effect
|
|
1909
|
+
* below force-mounts the row and scrollToElement lands it once its ref
|
|
1910
|
+
* (and Yoga top) exist. The fold toggle is idempotent, so calling this
|
|
1911
|
+
* for an already-revealed row is harmless.
|
|
1912
|
+
*/
|
|
1913
|
+
const revealAndSeekRow = (rowId) => {
|
|
1914
|
+
if (!showAllMessages)
|
|
1915
|
+
setShowAllMessages(true);
|
|
1916
|
+
seekRow(rowId);
|
|
1917
|
+
};
|
|
1280
1918
|
React.useLayoutEffect(() => {
|
|
1281
1919
|
if (forceMountRowId === null)
|
|
1282
1920
|
return;
|
|
1283
1921
|
const el = rowRefsRef.current.get(forceMountRowId);
|
|
1284
1922
|
if (el) {
|
|
1285
1923
|
handle?.scrollToElement(el);
|
|
1286
|
-
|
|
1924
|
+
// Clear deferred to a macrotask: clearing here would let React's
|
|
1925
|
+
// synchronous re-render narrow the virtualization window and unmount
|
|
1926
|
+
// the row BEFORE the renderer's deferred pass reads its Yoga top
|
|
1927
|
+
// (scrollAnchor processing runs in a microtask) — the seek would
|
|
1928
|
+
// silently no-op (detached anchor element).
|
|
1929
|
+
setTimeout(() => setForceMountRowId(null), 0);
|
|
1287
1930
|
}
|
|
1288
1931
|
});
|
|
1289
1932
|
// `/` transcript search: rows whose searchable text contains the query.
|
|
@@ -1300,7 +1943,7 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1300
1943
|
// Incsearch: highlight all matches (screen-space overlay) and keep the
|
|
1301
1944
|
// current match row in view as the query changes (CC semantics).
|
|
1302
1945
|
React.useEffect(() => {
|
|
1303
|
-
if (!
|
|
1946
|
+
if (!searchActive)
|
|
1304
1947
|
return;
|
|
1305
1948
|
setHighlight(searchQuery);
|
|
1306
1949
|
const count = searchMatches.length;
|
|
@@ -1312,10 +1955,10 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1312
1955
|
if (target) {
|
|
1313
1956
|
seekRow(target.row.id);
|
|
1314
1957
|
}
|
|
1315
|
-
}, [searchQuery,
|
|
1958
|
+
}, [searchQuery, searchActive]);
|
|
1316
1959
|
// n/N navigation: move the current match into view.
|
|
1317
1960
|
React.useEffect(() => {
|
|
1318
|
-
if (!
|
|
1961
|
+
if (!searchActive)
|
|
1319
1962
|
return;
|
|
1320
1963
|
const target = searchMatches[searchCurrent];
|
|
1321
1964
|
// oxlint-disable-next-line typescript/no-unnecessary-condition -- runtime guard: out-of-range index on an empty/filtered list
|
|
@@ -1352,6 +1995,16 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1352
1995
|
return next;
|
|
1353
1996
|
});
|
|
1354
1997
|
}, []);
|
|
1998
|
+
const toggleStreamFolded = React.useCallback((rowId) => {
|
|
1999
|
+
setStreamFoldedRows((previous) => {
|
|
2000
|
+
const next = new Set(previous);
|
|
2001
|
+
if (next.has(rowId))
|
|
2002
|
+
next.delete(rowId);
|
|
2003
|
+
else
|
|
2004
|
+
next.add(rowId);
|
|
2005
|
+
return next;
|
|
2006
|
+
});
|
|
2007
|
+
}, []);
|
|
1355
2008
|
const registerRowRef = React.useCallback((rowId, el) => {
|
|
1356
2009
|
if (el)
|
|
1357
2010
|
rowRefsRef.current.set(rowId, el);
|
|
@@ -1372,6 +2025,10 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1372
2025
|
// search box, which Chat would otherwise route into the prompt.
|
|
1373
2026
|
if (browserOpen)
|
|
1374
2027
|
return;
|
|
2028
|
+
// Same for the session tree: plain letters drive its search, clicks and
|
|
2029
|
+
// Enter drive its action menu.
|
|
2030
|
+
if (treeOpen)
|
|
2031
|
+
return;
|
|
1375
2032
|
// Same for the settings screen: plain letters (s save / d discard) and
|
|
1376
2033
|
// the field draft editor belong to it alone.
|
|
1377
2034
|
if (settingsOpen)
|
|
@@ -1388,13 +2045,30 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1388
2045
|
// Mouse wheel scrolls the transcript even while a question/approval/
|
|
1389
2046
|
// dialog panel is open — those panels own arrow/Enter/Esc keys, but the
|
|
1390
2047
|
// transcript above them should still be scrollable in fullscreen mode.
|
|
1391
|
-
//
|
|
1392
|
-
//
|
|
2048
|
+
//
|
|
2049
|
+
// Wheel routing is position-first: events landing over a ScrollBox
|
|
2050
|
+
// (transcript, help, subagent panels…) are consumed by that box in
|
|
2051
|
+
// App's input batch (onWheelAt) and never reach this branch. What
|
|
2052
|
+
// arrives here is the fallback: wheel over non-scroll areas (prompt,
|
|
2053
|
+
// status bar) or over floating overlays.
|
|
2054
|
+
// - Help stays yielded: PromptInput's help ScrollBox handles the
|
|
2055
|
+
// remaining global wheel while help is open (both covered layers
|
|
2056
|
+
// must not move).
|
|
2057
|
+
// - Pickers/dialogs are modal: wheel that fell through over them
|
|
2058
|
+
// must NOT scroll the transcript behind (the audit's
|
|
2059
|
+
// pass-through gap), so yield like the keyboard guards above.
|
|
1393
2060
|
// Events only arrive with mouse tracking on; inline mode never sees
|
|
1394
2061
|
// them, so this is a no-op there.
|
|
1395
2062
|
if (key.wheelUp || key.wheelDown) {
|
|
1396
2063
|
if (helpOpen)
|
|
1397
2064
|
return;
|
|
2065
|
+
// Any open transient dialog is modal to the wheel; the one exception
|
|
2066
|
+
// mirrors the render gate — a workspace picker whose target list has
|
|
2067
|
+
// not landed paints nothing, so wheel-through keeps scrolling.
|
|
2068
|
+
const overlayModal = overlay.kind !== 'none' &&
|
|
2069
|
+
(overlay.kind !== 'workspace-picker' || workspaceTargets.length > 0);
|
|
2070
|
+
if (overlayModal)
|
|
2071
|
+
return;
|
|
1398
2072
|
handle?.scrollBy(key.wheelUp ? -3 : 3);
|
|
1399
2073
|
event.stopImmediatePropagation();
|
|
1400
2074
|
return;
|
|
@@ -1424,11 +2098,11 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1424
2098
|
event.stopImmediatePropagation();
|
|
1425
2099
|
return;
|
|
1426
2100
|
}
|
|
1427
|
-
if (
|
|
2101
|
+
if (overlay.kind === 'search') {
|
|
1428
2102
|
// Transcript search bar (less-style): edit the query, Enter commits
|
|
1429
2103
|
// (query persists for n/N), Esc/ctrl+c cancels back to the anchor.
|
|
1430
2104
|
if (key.escape || (key.ctrl && input === 'c')) {
|
|
1431
|
-
|
|
2105
|
+
dispatchOverlay({ type: 'close' });
|
|
1432
2106
|
setHighlight('');
|
|
1433
2107
|
handle?.scrollTo(searchAnchorRef.current);
|
|
1434
2108
|
}
|
|
@@ -1436,7 +2110,7 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1436
2110
|
// Enter commits; 0-match junk queries don't persist (CC behavior).
|
|
1437
2111
|
if (searchCount === 0)
|
|
1438
2112
|
setSearchQuery('');
|
|
1439
|
-
|
|
2113
|
+
dispatchOverlay({ type: 'close' });
|
|
1440
2114
|
}
|
|
1441
2115
|
else if (key.backspace) {
|
|
1442
2116
|
if (searchCursor > 0) {
|
|
@@ -1482,41 +2156,41 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1482
2156
|
event.stopImmediatePropagation();
|
|
1483
2157
|
return;
|
|
1484
2158
|
}
|
|
1485
|
-
if (
|
|
2159
|
+
if (overlay.kind === 'thinking') {
|
|
1486
2160
|
if (key.upArrow || key.downArrow) {
|
|
1487
|
-
|
|
2161
|
+
dispatchOverlay({ type: 'move', delta: key.upArrow ? -1 : 1, count: 2 });
|
|
1488
2162
|
}
|
|
1489
2163
|
else if (plainReturn) {
|
|
1490
|
-
const visible =
|
|
2164
|
+
const visible = overlay.focus === 0;
|
|
1491
2165
|
setThinkingVisible(visible);
|
|
1492
|
-
|
|
2166
|
+
dispatchOverlay({ type: 'close' });
|
|
1493
2167
|
channel.notify(t('thinking-toggled', { state: visible ? t('thinking-on') : t('thinking-off') }));
|
|
1494
2168
|
}
|
|
1495
2169
|
else if (key.escape) {
|
|
1496
|
-
|
|
2170
|
+
dispatchOverlay({ type: 'close' });
|
|
1497
2171
|
}
|
|
1498
2172
|
return;
|
|
1499
2173
|
}
|
|
1500
|
-
if (
|
|
2174
|
+
if (overlay.kind === 'workspace-flow') {
|
|
2175
|
+
const { flow, busy, input: flowInput } = overlay;
|
|
1501
2176
|
if (key.escape) {
|
|
1502
|
-
if (
|
|
1503
|
-
|
|
2177
|
+
if (flowInput !== null && !busy) {
|
|
2178
|
+
dispatchOverlay({ type: 'flow-input', input: null });
|
|
1504
2179
|
return;
|
|
1505
2180
|
}
|
|
1506
2181
|
workspaceFlowAbortRef.current?.abort();
|
|
1507
2182
|
workspaceFlowAbortRef.current = null;
|
|
1508
2183
|
workspaceFlowRequestRef.current += 1;
|
|
1509
|
-
|
|
1510
|
-
setWorkspaceFlow(null);
|
|
2184
|
+
dispatchOverlay({ type: 'close' });
|
|
1511
2185
|
return;
|
|
1512
2186
|
}
|
|
1513
|
-
if (
|
|
2187
|
+
if (busy)
|
|
1514
2188
|
return;
|
|
1515
|
-
if (
|
|
1516
|
-
const choice =
|
|
2189
|
+
if (flowInput !== null) {
|
|
2190
|
+
const choice = flow.choices.find(candidate => candidate.id === flowInput.choiceId);
|
|
1517
2191
|
const editor = choice?.input;
|
|
1518
2192
|
if (plainReturn) {
|
|
1519
|
-
const value =
|
|
2193
|
+
const value = flowInput.value.trim();
|
|
1520
2194
|
if (value.length === 0) {
|
|
1521
2195
|
channel.notify(t('workspace-flow-input-empty'), { color: 'warning' });
|
|
1522
2196
|
}
|
|
@@ -1524,127 +2198,158 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1524
2198
|
runWorkspaceFlowAction(signal => editor.submit(value, signal));
|
|
1525
2199
|
}
|
|
1526
2200
|
}
|
|
1527
|
-
else if (key.backspace &&
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
value:
|
|
1531
|
-
cursor:
|
|
2201
|
+
else if (key.backspace && flowInput.cursor > 0) {
|
|
2202
|
+
dispatchOverlay({
|
|
2203
|
+
type: 'flow-input-edit',
|
|
2204
|
+
value: flowInput.value.slice(0, flowInput.cursor - 1) + flowInput.value.slice(flowInput.cursor),
|
|
2205
|
+
cursor: flowInput.cursor - 1,
|
|
1532
2206
|
});
|
|
1533
2207
|
}
|
|
1534
|
-
else if (key.delete &&
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
value:
|
|
2208
|
+
else if (key.delete && flowInput.cursor < flowInput.value.length) {
|
|
2209
|
+
dispatchOverlay({
|
|
2210
|
+
type: 'flow-input-edit',
|
|
2211
|
+
value: flowInput.value.slice(0, flowInput.cursor) + flowInput.value.slice(flowInput.cursor + 1),
|
|
2212
|
+
cursor: flowInput.cursor,
|
|
1538
2213
|
});
|
|
1539
2214
|
}
|
|
1540
2215
|
else if (key.leftArrow) {
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
2216
|
+
dispatchOverlay({
|
|
2217
|
+
type: 'flow-input-edit',
|
|
2218
|
+
value: flowInput.value,
|
|
2219
|
+
cursor: Math.max(0, flowInput.cursor - 1),
|
|
1544
2220
|
});
|
|
1545
2221
|
}
|
|
1546
2222
|
else if (key.rightArrow) {
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
2223
|
+
dispatchOverlay({
|
|
2224
|
+
type: 'flow-input-edit',
|
|
2225
|
+
value: flowInput.value,
|
|
2226
|
+
cursor: Math.min(flowInput.value.length, flowInput.cursor + 1),
|
|
1550
2227
|
});
|
|
1551
2228
|
}
|
|
1552
2229
|
else if (input.length > 0 && !key.ctrl && !key.meta && !key.super && !key.tab) {
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
value:
|
|
1556
|
-
cursor:
|
|
2230
|
+
dispatchOverlay({
|
|
2231
|
+
type: 'flow-input-edit',
|
|
2232
|
+
value: flowInput.value.slice(0, flowInput.cursor) + input + flowInput.value.slice(flowInput.cursor),
|
|
2233
|
+
cursor: flowInput.cursor + input.length,
|
|
1557
2234
|
});
|
|
1558
2235
|
}
|
|
1559
2236
|
return;
|
|
1560
2237
|
}
|
|
1561
|
-
if (key.upArrow) {
|
|
1562
|
-
|
|
1563
|
-
}
|
|
1564
|
-
else if (key.downArrow) {
|
|
1565
|
-
setWorkspaceFlowIndex(index => (index >= workspaceFlow.choices.length - 1 ? 0 : index + 1));
|
|
2238
|
+
if (key.upArrow || key.downArrow) {
|
|
2239
|
+
dispatchOverlay({ type: 'move', delta: key.upArrow ? -1 : 1, count: flow.choices.length });
|
|
1566
2240
|
}
|
|
1567
2241
|
else if (key.tab && !key.shift) {
|
|
1568
|
-
const choice =
|
|
2242
|
+
const choice = flow.choices[overlay.index];
|
|
1569
2243
|
if (choice?.input !== undefined) {
|
|
1570
2244
|
const value = choice.input.initialValue ?? '';
|
|
1571
|
-
|
|
2245
|
+
const flowInputNext = {
|
|
1572
2246
|
choiceId: choice.id,
|
|
1573
2247
|
value,
|
|
1574
2248
|
cursor: value.length,
|
|
1575
2249
|
...(choice.input.placeholder === undefined ? {} : { placeholder: choice.input.placeholder }),
|
|
1576
|
-
}
|
|
2250
|
+
};
|
|
2251
|
+
dispatchOverlay({ type: 'flow-input', input: flowInputNext });
|
|
1577
2252
|
}
|
|
1578
2253
|
}
|
|
1579
2254
|
else if (plainReturn) {
|
|
1580
|
-
const choice =
|
|
2255
|
+
const choice = flow.choices[overlay.index];
|
|
1581
2256
|
if (choice !== undefined) {
|
|
1582
2257
|
runWorkspaceFlowAction(signal => choice.choose(signal));
|
|
1583
2258
|
}
|
|
1584
2259
|
}
|
|
1585
2260
|
return;
|
|
1586
2261
|
}
|
|
1587
|
-
if (
|
|
1588
|
-
if (key.upArrow) {
|
|
1589
|
-
|
|
1590
|
-
}
|
|
1591
|
-
else if (key.downArrow) {
|
|
1592
|
-
setWorkspaceIndex(index => (index >= workspaceTargets.length - 1 ? 0 : index + 1));
|
|
2262
|
+
if (overlay.kind === 'workspace-picker') {
|
|
2263
|
+
if (key.upArrow || key.downArrow) {
|
|
2264
|
+
dispatchOverlay({ type: 'move', delta: key.upArrow ? -1 : 1, count: workspaceTargets.length });
|
|
1593
2265
|
}
|
|
1594
2266
|
else if (plainReturn) {
|
|
1595
|
-
const target = workspaceTargets[
|
|
1596
|
-
|
|
2267
|
+
const target = workspaceTargets[overlay.index];
|
|
2268
|
+
dispatchOverlay({ type: 'close' });
|
|
1597
2269
|
if (target !== undefined)
|
|
1598
2270
|
void channel.switchWorkspace(target);
|
|
1599
2271
|
}
|
|
1600
2272
|
else if (key.escape) {
|
|
1601
|
-
|
|
2273
|
+
dispatchOverlay({ type: 'close' });
|
|
1602
2274
|
}
|
|
1603
2275
|
return;
|
|
1604
2276
|
}
|
|
1605
|
-
if (
|
|
1606
|
-
|
|
1607
|
-
|
|
2277
|
+
if (overlay.kind === 'workspace-menu') {
|
|
2278
|
+
const menu = workspaceMenuOptions;
|
|
2279
|
+
if (key.upArrow || key.downArrow) {
|
|
2280
|
+
dispatchOverlay({ type: 'move', delta: key.upArrow ? -1 : 1, count: menu.length });
|
|
1608
2281
|
}
|
|
1609
|
-
else if (
|
|
1610
|
-
|
|
2282
|
+
else if (plainReturn) {
|
|
2283
|
+
const option = menu[overlay.index];
|
|
2284
|
+
runWorkspaceMenuOption(option);
|
|
2285
|
+
}
|
|
2286
|
+
else if (key.escape) {
|
|
2287
|
+
dispatchOverlay({ type: 'close' });
|
|
2288
|
+
}
|
|
2289
|
+
return;
|
|
2290
|
+
}
|
|
2291
|
+
if (overlay.kind === 'model') {
|
|
2292
|
+
// Two-level picker: group rows at the top (Enter drills in), one
|
|
2293
|
+
// provider's models below (Enter switches, the same live-fork path as
|
|
2294
|
+
// the flat picker always had). Esc/⌫ climbs one level and only closes
|
|
2295
|
+
// at the top; a single-group catalog never shows the group level, so
|
|
2296
|
+
// Esc there closes directly.
|
|
2297
|
+
const rowCount = activeModelGroup === undefined ? modelGroups.length : groupModels.length;
|
|
2298
|
+
if (key.upArrow || key.downArrow) {
|
|
2299
|
+
dispatchOverlay({ type: 'move', delta: key.upArrow ? -1 : 1, count: rowCount });
|
|
1611
2300
|
}
|
|
1612
2301
|
else if (plainReturn) {
|
|
1613
|
-
|
|
2302
|
+
if (activeModelGroup === undefined) {
|
|
2303
|
+
const group = modelGroups[overlay.index];
|
|
2304
|
+
if (!group) {
|
|
2305
|
+
dispatchOverlay({ type: 'close' });
|
|
2306
|
+
return;
|
|
2307
|
+
}
|
|
2308
|
+
setModelGroup(group.provider);
|
|
2309
|
+
// The recents group opens on its most-recent entry; a provider
|
|
2310
|
+
// group on its current model when it owns one, else its first row.
|
|
2311
|
+
if (group.provider === RECENTS_GROUP_PROVIDER) {
|
|
2312
|
+
dispatchOverlay({ type: 'set-index', kind: 'model', index: 0 });
|
|
2313
|
+
return;
|
|
2314
|
+
}
|
|
2315
|
+
const landing = modelPickerLanding(models.filter(model => model.provider === group.provider), channel.provider, channel.model);
|
|
2316
|
+
dispatchOverlay({ type: 'set-index', kind: 'model', index: landing.index });
|
|
2317
|
+
return;
|
|
2318
|
+
}
|
|
2319
|
+
const model = groupModels[overlay.index];
|
|
1614
2320
|
// oxlint-disable-next-line typescript/no-unnecessary-condition -- runtime guard: out-of-range index on an empty list
|
|
1615
2321
|
if (model) {
|
|
1616
2322
|
// Enter switches the live model right away: the conversation is
|
|
1617
2323
|
// forked at its end and continued with an agent routed to the new
|
|
1618
|
-
// model (history replays unchanged).
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
void channel.switchModel(model.provider, model.id).then((ok) => {
|
|
1622
|
-
if (ok)
|
|
1623
|
-
channel.notify(t('model-switched', { name: model.name }));
|
|
1624
|
-
});
|
|
2324
|
+
// model (history replays unchanged) — and feeds the recents group.
|
|
2325
|
+
dispatchOverlay({ type: 'close' });
|
|
2326
|
+
void switchModelRecorded(model.provider, model.id, model.name);
|
|
1625
2327
|
}
|
|
1626
2328
|
else {
|
|
1627
|
-
|
|
2329
|
+
dispatchOverlay({ type: 'close' });
|
|
1628
2330
|
}
|
|
1629
2331
|
}
|
|
1630
|
-
else if (key.escape) {
|
|
1631
|
-
|
|
2332
|
+
else if (key.escape || key.backspace) {
|
|
2333
|
+
if (activeModelGroup !== undefined && modelGroups.length > 1 && !modelPickerDirect) {
|
|
2334
|
+
setModelGroup(undefined);
|
|
2335
|
+
const groupIndex = Math.max(0, modelGroups.findIndex(group => group.provider === activeModelGroup));
|
|
2336
|
+
dispatchOverlay({ type: 'set-index', kind: 'model', index: groupIndex });
|
|
2337
|
+
}
|
|
2338
|
+
else {
|
|
2339
|
+
dispatchOverlay({ type: 'close' });
|
|
2340
|
+
}
|
|
1632
2341
|
}
|
|
1633
2342
|
return;
|
|
1634
2343
|
}
|
|
1635
|
-
if (
|
|
2344
|
+
if (overlay.kind === 'skills') {
|
|
1636
2345
|
const list = skillsList ?? [];
|
|
1637
|
-
if (key.upArrow) {
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
}
|
|
1641
|
-
else if (key.downArrow) {
|
|
1642
|
-
if (list.length > 0)
|
|
1643
|
-
setSkillsIndex(index => (index >= list.length - 1 ? 0 : index + 1));
|
|
2346
|
+
if (key.upArrow || key.downArrow) {
|
|
2347
|
+
// count 0 (snapshot still loading) is a no-op inside the reducer.
|
|
2348
|
+
dispatchOverlay({ type: 'move', delta: key.upArrow ? -1 : 1, count: list.length });
|
|
1644
2349
|
}
|
|
1645
2350
|
else if (plainReturn) {
|
|
1646
|
-
const skill = list[
|
|
1647
|
-
|
|
2351
|
+
const skill = list[overlay.index];
|
|
2352
|
+
dispatchOverlay({ type: 'close' });
|
|
1648
2353
|
// 可直调技能 Enter 填入 `/name `——与 / 菜单选中技能同一条
|
|
1649
2354
|
// completion-only 分发路径;模型专用技能(userInvocable=false)只关闭。
|
|
1650
2355
|
// oxlint-disable-next-line typescript/no-unnecessary-condition -- runtime guard: out-of-range index on an empty list
|
|
@@ -1652,239 +2357,313 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1652
2357
|
setHistoryFill(`/${skill.name} `);
|
|
1653
2358
|
}
|
|
1654
2359
|
else if (key.escape) {
|
|
1655
|
-
|
|
2360
|
+
dispatchOverlay({ type: 'close' });
|
|
1656
2361
|
}
|
|
1657
2362
|
return;
|
|
1658
2363
|
}
|
|
1659
|
-
if (
|
|
1660
|
-
if (key.upArrow) {
|
|
1661
|
-
|
|
1662
|
-
}
|
|
1663
|
-
else if (key.downArrow) {
|
|
1664
|
-
setActivityIndex(index => (index >= PRESET_NAMES.length - 1 ? 0 : index + 1));
|
|
2364
|
+
if (overlay.kind === 'activity') {
|
|
2365
|
+
if (key.upArrow || key.downArrow) {
|
|
2366
|
+
dispatchOverlay({ type: 'move', delta: key.upArrow ? -1 : 1, count: PRESET_NAMES.length });
|
|
1665
2367
|
}
|
|
1666
2368
|
else if (plainReturn) {
|
|
1667
|
-
const name = PRESET_NAMES[
|
|
1668
|
-
|
|
2369
|
+
const name = PRESET_NAMES[overlay.index];
|
|
2370
|
+
dispatchOverlay({ type: 'close' });
|
|
1669
2371
|
if (name)
|
|
1670
2372
|
channel.setActivityFrames(name);
|
|
1671
2373
|
}
|
|
1672
2374
|
else if (key.escape) {
|
|
1673
|
-
|
|
2375
|
+
dispatchOverlay({ type: 'close' });
|
|
1674
2376
|
}
|
|
1675
2377
|
return;
|
|
1676
2378
|
}
|
|
1677
|
-
if (
|
|
2379
|
+
if (overlay.kind === 'color') {
|
|
2380
|
+
if (key.upArrow || key.downArrow) {
|
|
2381
|
+
dispatchOverlay({ type: 'move', delta: key.upArrow ? -1 : 1, count: SESSION_COLOR_NAMES.length });
|
|
2382
|
+
}
|
|
2383
|
+
else if (plainReturn) {
|
|
2384
|
+
const name = SESSION_COLOR_NAMES[overlay.index];
|
|
2385
|
+
dispatchOverlay({ type: 'close' });
|
|
2386
|
+
if (name) {
|
|
2387
|
+
channel.setSessionColor(name);
|
|
2388
|
+
channel.notify(t('color-set', { name }), { color: 'success' });
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
else if (key.escape) {
|
|
2392
|
+
dispatchOverlay({ type: 'close' });
|
|
2393
|
+
}
|
|
2394
|
+
return;
|
|
2395
|
+
}
|
|
2396
|
+
if (overlay.kind === 'effort') {
|
|
1678
2397
|
if (key.leftArrow || key.rightArrow) {
|
|
1679
2398
|
const delta = key.leftArrow ? -1 : 1;
|
|
1680
|
-
|
|
1681
|
-
|
|
2399
|
+
// The same wrap rule the reducer applies — computed here too so the
|
|
2400
|
+
// newly focused level is applied in this very keystroke.
|
|
2401
|
+
const next = wrapIndex(overlay.index, delta, effortOptions.length);
|
|
2402
|
+
dispatchOverlay({ type: 'move', delta, count: effortOptions.length });
|
|
1682
2403
|
const option = effortOptions[next];
|
|
1683
2404
|
// Live-apply: the slider IS the control; Esc does not revert.
|
|
1684
2405
|
if (option)
|
|
1685
2406
|
void channel.setEffort(option.id);
|
|
1686
2407
|
}
|
|
1687
2408
|
else if (plainReturn || key.escape) {
|
|
1688
|
-
|
|
2409
|
+
dispatchOverlay({ type: 'close' });
|
|
1689
2410
|
}
|
|
1690
2411
|
return;
|
|
1691
2412
|
}
|
|
1692
|
-
if (
|
|
1693
|
-
if (key.upArrow) {
|
|
1694
|
-
|
|
1695
|
-
}
|
|
1696
|
-
else if (key.downArrow) {
|
|
1697
|
-
setPresetIndex(index => (index >= presetOptions.length - 1 ? 0 : index + 1));
|
|
2413
|
+
if (overlay.kind === 'preset') {
|
|
2414
|
+
if (key.upArrow || key.downArrow) {
|
|
2415
|
+
dispatchOverlay({ type: 'move', delta: key.upArrow ? -1 : 1, count: presetOptions.length });
|
|
1698
2416
|
}
|
|
1699
2417
|
else if (plainReturn) {
|
|
1700
|
-
const option = presetOptions[
|
|
1701
|
-
|
|
2418
|
+
const option = presetOptions[overlay.index];
|
|
2419
|
+
dispatchOverlay({ type: 'close' });
|
|
1702
2420
|
if (option)
|
|
1703
2421
|
void channel.switchPreset(option.id);
|
|
1704
2422
|
}
|
|
1705
2423
|
else if (key.escape) {
|
|
1706
|
-
|
|
2424
|
+
dispatchOverlay({ type: 'close' });
|
|
1707
2425
|
}
|
|
1708
2426
|
return;
|
|
1709
2427
|
}
|
|
1710
|
-
if (
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
setThemeIndex(index => (index <= 0 ? options.length - 1 : index - 1));
|
|
2428
|
+
if (overlay.kind === 'permission') {
|
|
2429
|
+
if (key.upArrow || key.downArrow) {
|
|
2430
|
+
dispatchOverlay({ type: 'move', delta: key.upArrow ? -1 : 1, count: PERMISSION_PRESET_IDS.length });
|
|
1714
2431
|
}
|
|
1715
|
-
else if (
|
|
1716
|
-
|
|
2432
|
+
else if (plainReturn) {
|
|
2433
|
+
const id = PERMISSION_PRESET_IDS[overlay.index];
|
|
2434
|
+
dispatchOverlay({ type: 'close' });
|
|
2435
|
+
if (id !== undefined) {
|
|
2436
|
+
void channel.runExternalCommand('permission', ` ${id}`).then((text) => {
|
|
2437
|
+
if (text !== undefined && text !== '')
|
|
2438
|
+
channel.notify(text);
|
|
2439
|
+
});
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
else if (key.escape) {
|
|
2443
|
+
dispatchOverlay({ type: 'close' });
|
|
2444
|
+
}
|
|
2445
|
+
return;
|
|
2446
|
+
}
|
|
2447
|
+
if (overlay.kind === 'plan') {
|
|
2448
|
+
if (key.upArrow || key.downArrow) {
|
|
2449
|
+
dispatchOverlay({ type: 'move', delta: key.upArrow ? -1 : 1, count: 2 });
|
|
2450
|
+
}
|
|
2451
|
+
else if (plainReturn) {
|
|
2452
|
+
const on = overlay.index === 0;
|
|
2453
|
+
dispatchOverlay({ type: 'close' });
|
|
2454
|
+
void channel.runExternalCommand('plan', on ? '' : ' off').then((text) => {
|
|
2455
|
+
if (text !== undefined && text !== '')
|
|
2456
|
+
channel.notify(text);
|
|
2457
|
+
});
|
|
2458
|
+
}
|
|
2459
|
+
else if (key.escape) {
|
|
2460
|
+
dispatchOverlay({ type: 'close' });
|
|
2461
|
+
}
|
|
2462
|
+
return;
|
|
2463
|
+
}
|
|
2464
|
+
if (overlay.kind === 'lang') {
|
|
2465
|
+
if (key.upArrow || key.downArrow) {
|
|
2466
|
+
dispatchOverlay({ type: 'move', delta: key.upArrow ? -1 : 1, count: 2 });
|
|
2467
|
+
}
|
|
2468
|
+
else if (plainReturn) {
|
|
2469
|
+
const lang = LANGS[overlay.index];
|
|
2470
|
+
dispatchOverlay({ type: 'close' });
|
|
2471
|
+
if (lang !== undefined)
|
|
2472
|
+
applyLang(lang);
|
|
2473
|
+
}
|
|
2474
|
+
else if (key.escape) {
|
|
2475
|
+
dispatchOverlay({ type: 'close' });
|
|
2476
|
+
}
|
|
2477
|
+
return;
|
|
2478
|
+
}
|
|
2479
|
+
if (overlay.kind === 'theme') {
|
|
2480
|
+
const options = getThemeOptions();
|
|
2481
|
+
if (key.upArrow || key.downArrow) {
|
|
2482
|
+
dispatchOverlay({ type: 'move', delta: key.upArrow ? -1 : 1, count: options.length });
|
|
1717
2483
|
}
|
|
1718
2484
|
else if (plainReturn) {
|
|
1719
|
-
|
|
1720
|
-
const name = options[
|
|
2485
|
+
dispatchOverlay({ type: 'close' });
|
|
2486
|
+
const name = options[overlay.index]?.value;
|
|
1721
2487
|
if (name !== undefined) {
|
|
1722
2488
|
const ok = setTheme(name);
|
|
1723
2489
|
channel.notify(ok ? t('theme-switched-saved', { name }) : t('theme-switch-failed', { name }), { color: ok ? 'success' : 'error' });
|
|
1724
2490
|
}
|
|
1725
2491
|
}
|
|
1726
2492
|
else if (key.escape) {
|
|
1727
|
-
|
|
2493
|
+
dispatchOverlay({ type: 'close' });
|
|
1728
2494
|
}
|
|
1729
2495
|
return;
|
|
1730
2496
|
}
|
|
1731
|
-
if (
|
|
2497
|
+
if (overlay.kind === 'history') {
|
|
2498
|
+
const { query, cursor, focus } = overlay;
|
|
1732
2499
|
if (key.escape) {
|
|
1733
|
-
|
|
2500
|
+
dispatchOverlay({ type: 'close' });
|
|
1734
2501
|
}
|
|
1735
2502
|
else if (key.ctrl && (input === 'c' || input === 'd')) {
|
|
1736
2503
|
// CC's history search cancels on ctrl+c/ctrl+d too.
|
|
1737
|
-
|
|
2504
|
+
dispatchOverlay({ type: 'close' });
|
|
1738
2505
|
}
|
|
1739
2506
|
else if (plainReturn) {
|
|
1740
|
-
const entry = historyMatches[
|
|
2507
|
+
const entry = historyMatches[focus];
|
|
1741
2508
|
// oxlint-disable-next-line typescript/no-unnecessary-condition -- runtime guard: out-of-range index on an empty match list
|
|
1742
2509
|
if (entry) {
|
|
1743
2510
|
setHistoryFill(entry.text);
|
|
1744
|
-
|
|
2511
|
+
dispatchOverlay({ type: 'close' });
|
|
1745
2512
|
}
|
|
1746
2513
|
}
|
|
1747
2514
|
else if (key.upArrow) {
|
|
1748
|
-
|
|
2515
|
+
if (historyMatches.length > 0) {
|
|
2516
|
+
dispatchOverlay({ type: 'move', delta: -1, count: historyMatches.length });
|
|
2517
|
+
}
|
|
1749
2518
|
}
|
|
1750
|
-
else if (key.downArrow || (
|
|
1751
|
-
// CC's historySearch:next — ↓ and
|
|
1752
|
-
|
|
2519
|
+
else if (key.downArrow || actionMatches('history', input, key)) {
|
|
2520
|
+
// CC's historySearch:next — ↓ and the history key (default Ctrl+R)
|
|
2521
|
+
// walk to the next match.
|
|
2522
|
+
if (historyMatches.length > 0) {
|
|
2523
|
+
dispatchOverlay({ type: 'move', delta: 1, count: historyMatches.length });
|
|
2524
|
+
}
|
|
1753
2525
|
}
|
|
1754
2526
|
else if (key.backspace) {
|
|
1755
|
-
if (
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
2527
|
+
if (cursor > 0) {
|
|
2528
|
+
dispatchOverlay({
|
|
2529
|
+
type: 'history-edit',
|
|
2530
|
+
query: query.slice(0, cursor - 1) + query.slice(cursor),
|
|
2531
|
+
cursor: cursor - 1,
|
|
2532
|
+
focus: 0,
|
|
2533
|
+
});
|
|
1760
2534
|
}
|
|
1761
2535
|
}
|
|
1762
2536
|
else if (key.delete) {
|
|
1763
|
-
if (
|
|
1764
|
-
|
|
1765
|
-
|
|
2537
|
+
if (cursor < query.length) {
|
|
2538
|
+
dispatchOverlay({
|
|
2539
|
+
type: 'history-edit',
|
|
2540
|
+
query: query.slice(0, cursor) + query.slice(cursor + 1),
|
|
2541
|
+
focus: 0,
|
|
2542
|
+
});
|
|
1766
2543
|
}
|
|
1767
2544
|
}
|
|
1768
2545
|
else if (key.leftArrow) {
|
|
1769
2546
|
// Step by code point, not UTF-16 unit: an emoji is two units, and
|
|
1770
2547
|
// a mid-pair caret offset would split it in the SearchBox render.
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
return cursor - ch.length;
|
|
1776
|
-
});
|
|
2548
|
+
if (cursor > 0) {
|
|
2549
|
+
const ch = [...query.slice(0, cursor)].pop();
|
|
2550
|
+
dispatchOverlay({ type: 'history-edit', cursor: cursor - ch.length });
|
|
2551
|
+
}
|
|
1777
2552
|
}
|
|
1778
2553
|
else if (key.rightArrow) {
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
return cursor + ch.length;
|
|
1784
|
-
});
|
|
2554
|
+
if (cursor < query.length) {
|
|
2555
|
+
const ch = [...query.slice(cursor)][0];
|
|
2556
|
+
dispatchOverlay({ type: 'history-edit', cursor: cursor + ch.length });
|
|
2557
|
+
}
|
|
1785
2558
|
}
|
|
1786
2559
|
else if (key.home) {
|
|
1787
|
-
|
|
2560
|
+
dispatchOverlay({ type: 'history-edit', cursor: 0 });
|
|
1788
2561
|
}
|
|
1789
2562
|
else if (key.end) {
|
|
1790
|
-
|
|
2563
|
+
dispatchOverlay({ type: 'history-edit', cursor: query.length });
|
|
1791
2564
|
}
|
|
1792
2565
|
else if (!key.ctrl && !key.meta && !key.super && input) {
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
2566
|
+
dispatchOverlay({
|
|
2567
|
+
type: 'history-edit',
|
|
2568
|
+
query: query.slice(0, cursor) + input + query.slice(cursor),
|
|
2569
|
+
cursor: cursor + input.length,
|
|
2570
|
+
focus: 0,
|
|
2571
|
+
});
|
|
1797
2572
|
}
|
|
1798
2573
|
return;
|
|
1799
2574
|
}
|
|
1800
|
-
if (
|
|
2575
|
+
if (overlay.kind === 'rewind') {
|
|
1801
2576
|
// While the plugin decision is in flight the picker is read-only;
|
|
1802
2577
|
// Esc abandons the wait (the stale answer is dropped by the token).
|
|
1803
|
-
if (
|
|
2578
|
+
if (overlay.busy) {
|
|
1804
2579
|
if (key.escape) {
|
|
1805
2580
|
rewindRequestRef.current += 1;
|
|
1806
|
-
|
|
2581
|
+
dispatchOverlay({ type: 'rewind-busy', busy: false });
|
|
1807
2582
|
}
|
|
1808
2583
|
return;
|
|
1809
2584
|
}
|
|
1810
|
-
if (
|
|
1811
|
-
|
|
2585
|
+
if (overlay.confirm !== null) {
|
|
2586
|
+
const row = overlay.confirm;
|
|
2587
|
+
if (overlay.modes !== null) {
|
|
1812
2588
|
// Plugin offered modes: the confirm pane is a choice list —
|
|
1813
2589
|
// option 0 is always the built-in conversation-only rewind.
|
|
1814
|
-
const optionCount =
|
|
1815
|
-
if (key.upArrow) {
|
|
1816
|
-
|
|
1817
|
-
}
|
|
1818
|
-
else if (key.downArrow) {
|
|
1819
|
-
setRewindModeIndex(index => (index >= optionCount - 1 ? 0 : index + 1));
|
|
2590
|
+
const optionCount = overlay.modes.length + 1;
|
|
2591
|
+
if (key.upArrow || key.downArrow) {
|
|
2592
|
+
dispatchOverlay({ type: 'move', delta: key.upArrow ? -1 : 1, count: optionCount });
|
|
1820
2593
|
}
|
|
1821
2594
|
else if (plainReturn) {
|
|
1822
|
-
const
|
|
1823
|
-
|
|
1824
|
-
setRewindOpen(false);
|
|
1825
|
-
setRewindConfirm(null);
|
|
1826
|
-
setRewindModes(null);
|
|
2595
|
+
const mode = overlay.modeIndex === 0 ? null : (overlay.modes[overlay.modeIndex - 1]?.id ?? null);
|
|
2596
|
+
dispatchOverlay({ type: 'close' });
|
|
1827
2597
|
void performRewind(row, mode);
|
|
1828
2598
|
}
|
|
1829
2599
|
else if (key.escape) {
|
|
1830
|
-
|
|
1831
|
-
setRewindModes(null);
|
|
2600
|
+
dispatchOverlay({ type: 'rewind-back' });
|
|
1832
2601
|
}
|
|
1833
2602
|
return;
|
|
1834
2603
|
}
|
|
1835
2604
|
// Confirmation state: Enter rewinds, Esc backs out to the list.
|
|
1836
2605
|
if (plainReturn) {
|
|
1837
|
-
|
|
1838
|
-
setRewindOpen(false);
|
|
1839
|
-
setRewindConfirm(null);
|
|
2606
|
+
dispatchOverlay({ type: 'close' });
|
|
1840
2607
|
void performRewind(row);
|
|
1841
2608
|
}
|
|
1842
2609
|
else if (key.escape) {
|
|
1843
|
-
|
|
2610
|
+
dispatchOverlay({ type: 'rewind-back' });
|
|
1844
2611
|
}
|
|
1845
2612
|
}
|
|
1846
|
-
else if (key.upArrow) {
|
|
1847
|
-
|
|
1848
|
-
}
|
|
1849
|
-
else if (key.downArrow) {
|
|
1850
|
-
setRewindIndex(index => (index >= rewindRows.length - 1 ? 0 : index + 1));
|
|
2613
|
+
else if (key.upArrow || key.downArrow) {
|
|
2614
|
+
dispatchOverlay({ type: 'move', delta: key.upArrow ? -1 : 1, count: rewindRows.length });
|
|
1851
2615
|
}
|
|
1852
2616
|
else if (plainReturn) {
|
|
1853
|
-
const row = rewindRows[
|
|
2617
|
+
const row = rewindRows[overlay.index];
|
|
1854
2618
|
// oxlint-disable-next-line typescript/no-unnecessary-condition -- runtime guard: out-of-range index on an empty list
|
|
1855
2619
|
if (row)
|
|
1856
2620
|
void requestRewindConfirm(row);
|
|
1857
2621
|
}
|
|
1858
2622
|
else if (key.escape) {
|
|
1859
|
-
|
|
1860
|
-
setRewindConfirm(null);
|
|
1861
|
-
setRewindModes(null);
|
|
2623
|
+
dispatchOverlay({ type: 'close' });
|
|
1862
2624
|
}
|
|
1863
2625
|
return;
|
|
1864
2626
|
}
|
|
1865
|
-
if (
|
|
1866
|
-
//
|
|
2627
|
+
if (overlay.kind === 'file-actions') {
|
|
2628
|
+
// Click-to-act file menu: ↑/↓ move, Enter runs the focused action,
|
|
2629
|
+
// Esc closes.
|
|
2630
|
+
if (key.upArrow || key.downArrow) {
|
|
2631
|
+
dispatchOverlay({ type: 'move', delta: key.upArrow ? -1 : 1, count: FILE_ACTION_COUNT });
|
|
2632
|
+
}
|
|
2633
|
+
else if (plainReturn) {
|
|
2634
|
+
const path = overlay.path;
|
|
2635
|
+
dispatchOverlay({ type: 'close' });
|
|
2636
|
+
runFileAction(overlay.index, path);
|
|
2637
|
+
}
|
|
2638
|
+
else if (key.escape) {
|
|
2639
|
+
dispatchOverlay({ type: 'close' });
|
|
2640
|
+
}
|
|
2641
|
+
return;
|
|
2642
|
+
}
|
|
2643
|
+
if (actionMatches('trajectory', input, key)) {
|
|
2644
|
+
// The trajectory scene key (default Ctrl+T) opens it at any point in
|
|
2645
|
+
// the session.
|
|
1867
2646
|
openScene();
|
|
1868
2647
|
return;
|
|
1869
2648
|
}
|
|
1870
|
-
if (
|
|
1871
|
-
// Ctrl+A opens the
|
|
2649
|
+
if (actionMatches('dashboard', input, key)) {
|
|
2650
|
+
// The subagent dashboard key (default Ctrl+A) opens the dashboard.
|
|
1872
2651
|
setSubagentDashboardOpen(true);
|
|
1873
2652
|
return;
|
|
1874
2653
|
}
|
|
1875
|
-
if (
|
|
1876
|
-
//
|
|
1877
|
-
// screen (transcript still empty); once rows take
|
|
1878
|
-
// panel disappears the key has nothing left to do.
|
|
2654
|
+
if (actionMatches('contextPanel', input, key) && loadedContextVisible) {
|
|
2655
|
+
// The loaded-context panel key (default Ctrl+P) toggles the startup
|
|
2656
|
+
// panel while it is on screen (transcript still empty); once rows take
|
|
2657
|
+
// over and the panel disappears the key has nothing left to do.
|
|
1879
2658
|
toggleLoadedContext();
|
|
1880
2659
|
return;
|
|
1881
2660
|
}
|
|
1882
|
-
if (
|
|
1883
|
-
setHistoryQuery('');
|
|
1884
|
-
setHistoryCursor(0);
|
|
1885
|
-
setHistoryFocus(0);
|
|
2661
|
+
if (actionMatches('history', input, key) && !helpOpen) {
|
|
1886
2662
|
setHistoryEntries(loadHistory());
|
|
1887
|
-
|
|
2663
|
+
dispatchOverlay({
|
|
2664
|
+
type: 'open',
|
|
2665
|
+
overlay: { kind: 'history', query: '', cursor: 0, focus: 0 },
|
|
2666
|
+
});
|
|
1888
2667
|
return;
|
|
1889
2668
|
}
|
|
1890
2669
|
if (key.shift && key.upArrow && !selectionActive && !helpOpen) {
|
|
@@ -1921,11 +2700,12 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1921
2700
|
}
|
|
1922
2701
|
event.stopImmediatePropagation();
|
|
1923
2702
|
}
|
|
1924
|
-
else if (
|
|
1925
|
-
// Leaving transcript mode (Ctrl+O) — search was already
|
|
1926
|
-
// Help is modal: toggling this state behind the
|
|
1927
|
-
// then the next `/` unexpectedly opens
|
|
1928
|
-
// slash-command completion after Help
|
|
2703
|
+
else if (actionMatches('transcript', input, key) && !helpOpen) {
|
|
2704
|
+
// Leaving transcript mode (default Ctrl+O) — search was already
|
|
2705
|
+
// handled above. Help is modal: toggling this state behind the
|
|
2706
|
+
// overlay is invisible, then the next `/` unexpectedly opens
|
|
2707
|
+
// transcript search instead of slash-command completion after Help
|
|
2708
|
+
// closes.
|
|
1929
2709
|
setExpanded(previous => !previous);
|
|
1930
2710
|
// The toggle rewrites every thinking row's layout at once. The
|
|
1931
2711
|
// ordinary scroll-based diff pushes rows into terminal scrollback on
|
|
@@ -1947,7 +2727,7 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1947
2727
|
setSearchCursor(0);
|
|
1948
2728
|
setSearchCurrent(0);
|
|
1949
2729
|
setSearchCount(0);
|
|
1950
|
-
|
|
2730
|
+
dispatchOverlay({ type: 'open', overlay: { kind: 'search' } });
|
|
1951
2731
|
event.stopImmediatePropagation();
|
|
1952
2732
|
}
|
|
1953
2733
|
}
|
|
@@ -1970,16 +2750,34 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1970
2750
|
requestExit();
|
|
1971
2751
|
}
|
|
1972
2752
|
}
|
|
1973
|
-
else if (
|
|
1974
|
-
// CC's app:redraw — clear the physical terminal and
|
|
2753
|
+
else if (actionMatches('redraw', input, key)) {
|
|
2754
|
+
// CC's app:redraw (default Ctrl+L) — clear the physical terminal and
|
|
2755
|
+
// repaint.
|
|
1975
2756
|
instances.get(process.stdout)?.forceRedraw();
|
|
1976
2757
|
}
|
|
1977
|
-
else if (
|
|
2758
|
+
else if (actionMatches('showAll', input, key)) {
|
|
1978
2759
|
setShowAllMessages(previous => !previous);
|
|
1979
2760
|
}
|
|
1980
|
-
else if (
|
|
2761
|
+
else if (actionMatches('todoFold', input, key)) {
|
|
2762
|
+
// Fold/unfold the GoalTodoPanel todo section (default Ctrl+Q) — works
|
|
2763
|
+
// mid-turn too: the collapsed line keeps the done/total count and the
|
|
2764
|
+
// live task preview, so long todo lists stop crowding the prompt.
|
|
2765
|
+
setTodoCollapsed(previous => !previous);
|
|
2766
|
+
}
|
|
2767
|
+
else if (plainReturn && !isSticky) {
|
|
2768
|
+
// Enter while scrolled up returns to the bottom (CC's pill: the
|
|
2769
|
+
// affordance now exists whenever the view is off the bottom, not
|
|
2770
|
+
// only with unseen rows).
|
|
1981
2771
|
handle?.scrollToBottom();
|
|
1982
2772
|
}
|
|
2773
|
+
else if (key.end && !isSticky) {
|
|
2774
|
+
// End = jump to bottom, less/vim semantics. Global on the chat
|
|
2775
|
+
// screen (search/history overlays consume their own End first —
|
|
2776
|
+
// cursor-to-line-end there). At the bottom already: no-op, so the
|
|
2777
|
+
// key stays harmless in muscle memory.
|
|
2778
|
+
handle?.scrollToBottom();
|
|
2779
|
+
event.stopImmediatePropagation();
|
|
2780
|
+
}
|
|
1983
2781
|
else if (extensionShortcuts !== undefined && extensionShortcuts.dispatch(input, key)) {
|
|
1984
2782
|
// Plugin shortcut (tuiShortcuts seam): matched only after every
|
|
1985
2783
|
// built-in global binding above declined — locals always win, and the
|
|
@@ -2029,6 +2827,16 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
2029
2827
|
// hosts are already in it and must not nest a second one.
|
|
2030
2828
|
return fullscreen ? browser : _jsx(AlternateScreen, { children: browser });
|
|
2031
2829
|
}
|
|
2830
|
+
// The session tree follows the browser's rule exactly: it REPLACES the
|
|
2831
|
+
// conversation (an early return after every hook above has run), so there
|
|
2832
|
+
// is no transcript underneath to be repainted or bled through. The dropped
|
|
2833
|
+
// turn's prompt returns through the same fill path a rewind picker uses.
|
|
2834
|
+
if (treeOpen) {
|
|
2835
|
+
const tree = (_jsx(SessionTree, { channel: channel, currentSessionId: channel.agentId, onClose: () => setTreeOpen(false), onRestoreText: (text) => {
|
|
2836
|
+
setHistoryFill(text);
|
|
2837
|
+
} }));
|
|
2838
|
+
return fullscreen ? tree : _jsx(AlternateScreen, { children: tree });
|
|
2839
|
+
}
|
|
2032
2840
|
// The settings screen follows the browser's rule exactly: it REPLACES the
|
|
2033
2841
|
// conversation (an early return after every hook above has run), so there
|
|
2034
2842
|
// is no transcript underneath to be repainted or bled through.
|
|
@@ -2061,10 +2869,10 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
2061
2869
|
}, onClose: () => setSubagentDashboardOpen(false) }));
|
|
2062
2870
|
return fullscreen ? dashboard : _jsx(AlternateScreen, { children: dashboard });
|
|
2063
2871
|
}
|
|
2064
|
-
/** Prompt input is inert while a modal dialog owns the keyboard.
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2872
|
+
/** Prompt input is inert while a modal dialog owns the keyboard. The
|
|
2873
|
+
* overlay union covers every picker/dialog and /tips in one check;
|
|
2874
|
+
* message-selection mode and the /btw panel live outside it. */
|
|
2875
|
+
const promptSelectionActive = selectionActive || overlay.kind !== 'none' || btw !== null;
|
|
2068
2876
|
// The trajectory scene replaces the conversation for as long as it is open.
|
|
2069
2877
|
// Rendering it INSTEAD of (not above) the transcript is what makes it a
|
|
2070
2878
|
// screen rather than an overlay: it owns the full viewport, and the
|
|
@@ -2077,24 +2885,60 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
2077
2885
|
const scene = _jsx(TrajectoryScene, { channel: channel, build: trajectory, onClose: closeScene });
|
|
2078
2886
|
return fullscreen ? scene : _jsx(AlternateScreen, { children: scene });
|
|
2079
2887
|
}
|
|
2080
|
-
//
|
|
2081
|
-
//
|
|
2082
|
-
//
|
|
2083
|
-
//
|
|
2084
|
-
// blit-skip
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2888
|
+
// 浮层整体挂载条件:与内部各面板的可见条件同值(数据门在
|
|
2889
|
+
// dialogOverlayVisible 里逐面板镜像)。关闭时把整个 absolute 浮层从树里
|
|
2890
|
+
// 移除——渲染器的"移除 absolute 节点"检测只看被移除子树自身的
|
|
2891
|
+
// style.position(dom.ts collectRemovedRects),若浮层常驻、只移除其
|
|
2892
|
+
// 普通子节点,blit 解毒不触发,被覆盖的转录行会在 blit-skip 后留空
|
|
2893
|
+
// (Esc 关 picker 一片空白的根因)。
|
|
2894
|
+
const dialogOverlayOpen = dialogOverlayVisible(overlay, {
|
|
2895
|
+
workspaceTargetCount: workspaceTargets.length,
|
|
2896
|
+
effortOptionCount: effortOptions.length,
|
|
2897
|
+
presetOptionCount: presetOptions.length,
|
|
2898
|
+
});
|
|
2899
|
+
// The sticky header pins the turn owning the viewport top row
|
|
2900
|
+
// (timeline.activeId, reported by MessageList) — scrolled up to an old
|
|
2901
|
+
// turn, it carries THAT turn's prompt, not the latest one.
|
|
2902
|
+
// channel.rows is a live in-place array, so the lookup is per-render.
|
|
2903
|
+
const anchorUserRowId = timeline.activeId;
|
|
2904
|
+
const anchorUserText = anchorUserRowId === null
|
|
2905
|
+
? null
|
|
2906
|
+
: channel.rows.find(row => row.id === anchorUserRowId)?.text ?? null;
|
|
2907
|
+
return (_jsxs(Box, { ref: wakeTickRef, flexDirection: "column", flexGrow: 1, width: "100%", children: [!isSticky && anchorUserText && (_jsx(StickyPromptHeader, { text: anchorUserText, onClick: () => {
|
|
2908
|
+
// Click snaps the pinned prompt to the viewport top (CC's
|
|
2909
|
+
// StickyPromptHeader). Jump by the SAME content coordinate the
|
|
2910
|
+
// rail's tick uses (timeline turn top = the prompt TEXT top):
|
|
2911
|
+
// the element-based seek lands the row wrapper's margin at the
|
|
2912
|
+
// top instead — one row shy of the text top the anchor rule
|
|
2913
|
+
// compares against — and the header would flip to the previous
|
|
2914
|
+
// turn immediately after the click.
|
|
2915
|
+
const turn = timeline.turns.find(t => t.id === anchorUserRowId);
|
|
2916
|
+
if (turn)
|
|
2917
|
+
handle?.scrollTo(turn.top);
|
|
2918
|
+
else if (anchorUserRowId !== null)
|
|
2919
|
+
seekRow(anchorUserRowId);
|
|
2095
2920
|
else
|
|
2096
2921
|
handle?.scrollToBottom();
|
|
2097
|
-
} })), _jsxs(
|
|
2922
|
+
} })), _jsxs(Box, { flexDirection: "row", flexGrow: 1, flexShrink: 1, width: "100%", children: [_jsxs(ScrollBox, { ref: setHandle, flexDirection: "column", flexGrow: 1, flexShrink: 1, stickyScroll: true, children: [_jsx(LogoHeader, { model: channel.model, effort: channel.reasoningEffort, cwd: channel.displayCwd, whale: channel.whale,
|
|
2923
|
+
// Resuming a long session skips the ~3.4s opening animation: it
|
|
2924
|
+
// keeps firing low-frequency React commits that compete with the
|
|
2925
|
+
// transcript mount batches (and the first wheel events) for the
|
|
2926
|
+
// frame budget right when the user wants to read history. Fresh
|
|
2927
|
+
// sessions keep the full intro; restored ones settle instantly.
|
|
2928
|
+
skipIntro: channel.rows.length > 30 }, logoNonce), loadedContextVisible && (_jsx(LoadedContextPanel, { context: channel.loadedContext, open: loadedContextOpen, onToggle: toggleLoadedContext })), _jsx(MessageList, { rows: channel.rows, failureHintRowId: failureHintRowId, failureHint: t('traj-hint-failure', { key: `${modLabel}t` }), expanded: expanded, expandedRows: expandedRows, selectedId: selectionActive ? selectedId : null, onToggleRow: toggleRowExpanded, streamFoldedRows: streamFoldedRows, onToggleStreamFold: toggleStreamFolded, model: channel.model, diffLayout: channel.diffLayout, thinkingFold: channel.thinkingFold, toolBackground: channel.toolBackground, activityFrames: channel.activityFrames, showAll: showAllMessages, thinkingVisible: thinkingVisible, historyPaintEnabled: !fullscreen, onToggleAll: () => { setShowAllMessages(previous => !previous); }, onLoadOlder: () => channel.loadOlder(), registerRowRef: registerRowRef, scrollHandle: handle, forceMountRowId: forceMountRowId, newSinceRowId: isSticky ? null : lastSeenRowIdRef.current, onUnseenCount: setUnseenCount, onTimeline: setTimeline, onOpenSubagent: (agentId) => setSubagentDetailId(agentId), onOpenFile: openFileActions })] }), (() => {
|
|
2929
|
+
// Gutter mode (settings `dsh-tui.scrollGutter`): the timeline
|
|
2930
|
+
// rail (default), the proportional scrollbar, or nothing. The
|
|
2931
|
+
// slot keeps its 2 columns in both rendered modes (Qwen's
|
|
2932
|
+
// permanent-gutter rule — an appearing/disappearing gutter
|
|
2933
|
+
// changes the transcript width and rewraps everything).
|
|
2934
|
+
const gutter = normalizeScrollGutter(channel.scrollGutter);
|
|
2935
|
+
if (gutter === 'hidden')
|
|
2936
|
+
return null;
|
|
2937
|
+
if (gutter === 'scrollbar') {
|
|
2938
|
+
return _jsx(ScrollbarGutter, { handle: handle, terminalWidth: terminalColumns });
|
|
2939
|
+
}
|
|
2940
|
+
return (_jsx(TimelineRail, { handle: handle, turns: timeline.turns, activeId: timeline.activeId, upId: timeline.upId, downId: timeline.downId, terminalWidth: terminalColumns, hoverEnabled: !promptSelectionActive, onRevealTurn: revealAndSeekRow }));
|
|
2941
|
+
})()] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [showPill && (_jsx(NewMessagesPill, { count: unseenCount, onClick: () => handle?.scrollToBottom() })), channel.working &&
|
|
2098
2942
|
(channel.activityEnabled &&
|
|
2099
2943
|
!channel.minimal &&
|
|
2100
2944
|
channel.workingActivity !== undefined &&
|
|
@@ -2104,22 +2948,182 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
2104
2948
|
// the animated chars/4 estimate, matching the classic
|
|
2105
2949
|
// spinner's counter (the suffix used raw chars before,
|
|
2106
2950
|
// inflating the reading next to a real upload number).
|
|
2107
|
-
suffix: `${lastUploadTokens > 0 ? ` · ↑ ${formatTokens(lastUploadTokens)}` : ''} · ↓ ${formatTokens(Math.round(channel.responseChars / 4))} tokens` }) })) : (_jsx(WorkingSpinner, { mode: channel.spinnerMode, hasActiveTools: channel.activeToolCount > 0, responseLengthRef: responseLengthRef, uploadTokensRef: uploadTokensRef, loadingStartTimeRef: loadingStartTimeRef, totalPausedMsRef: totalPausedMsRef, pauseStartTimeRef: pauseStartTimeRef, thinkingStatus: thinkingStatus }))), _jsx(GoalTodoPanel, { channel: channel }), statusEntries.length > 0 && (_jsx(Text, { dimColor: true, wrap: "truncate", children: statusEntries.map(entry => entry.text).join(' · ') })), approvalSnapshot !== null ? (_jsx(ApprovalPanel, { approval: approvalSnapshot, onDecide: outcome => approvals.decide(outcome) }, approvalSnapshot.key)) : dialogSnapshot !== null ? (_jsx(ExtensionDialog, { dialog: dialogSnapshot, onDecide: value => dialogs.decide(dialogSnapshot.key, value), onCancel: () => dialogs.cancel(dialogSnapshot.key) }, dialogSnapshot.key)) :
|
|
2951
|
+
suffix: `${lastUploadTokens > 0 ? ` · ↑ ${formatTokens(lastUploadTokens)}` : ''} · ↓ ${formatTokens(Math.round(channel.responseChars / 4))} tokens` }) })) : (_jsx(WorkingSpinner, { mode: channel.spinnerMode, hasActiveTools: channel.activeToolCount > 0, responseLengthRef: responseLengthRef, uploadTokensRef: uploadTokensRef, loadingStartTimeRef: loadingStartTimeRef, totalPausedMsRef: totalPausedMsRef, pauseStartTimeRef: pauseStartTimeRef, thinkingStatus: thinkingStatus }))), _jsx(GoalTodoPanel, { channel: channel, collapsed: todoCollapsed, onToggle: () => setTodoCollapsed(previous => !previous) }), recap !== null && recap.auto && !recap.expanded && (_jsx(AutoRecapRow, { summary: recap.summary, streaming: !recap.done, onExpand: () => setRecap(prev => (prev ? { ...prev, expanded: true } : prev)), onDismiss: () => closeRecap() })), statusEntries.length > 0 && (_jsx(Text, { dimColor: true, wrap: "truncate", children: statusEntries.map(entry => entry.text).join(' · ') })), approvalSnapshot !== null ? (_jsx(ApprovalPanel, { approval: approvalSnapshot, onDecide: outcome => approvals.decide(outcome) }, approvalSnapshot.key)) : dialogSnapshot !== null ? (_jsx(ExtensionDialog, { dialog: dialogSnapshot, onDecide: value => dialogs.decide(dialogSnapshot.key, value), onCancel: () => dialogs.cancel(dialogSnapshot.key) }, dialogSnapshot.key)) : overlay.kind === 'tips' ? (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(TipsPanel, { onClose: () => dispatchOverlay({ type: 'close-if', kind: 'tips' }) }) })) : recap !== null && (!recap.auto || recap.expanded) ? (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(RecapPanel, { summary: recap.summary, title: recap.title, error: recap.error, streaming: !recap.done, titleApplied: recap.titleApplied, onClose: () => {
|
|
2952
|
+
// An expanded auto recap collapses back to its dim row;
|
|
2953
|
+
// a manual /recap closes outright.
|
|
2954
|
+
if (recap.auto) {
|
|
2955
|
+
setRecap(prev => (prev ? { ...prev, expanded: false } : prev));
|
|
2956
|
+
}
|
|
2957
|
+
else {
|
|
2958
|
+
closeRecap();
|
|
2959
|
+
}
|
|
2960
|
+
}, onCopy: () => {
|
|
2961
|
+
void setClipboard(recap.summary ?? '').then(raw => { if (raw)
|
|
2962
|
+
writeRaw?.(raw); });
|
|
2963
|
+
channel.notify(t('copied-chars', { n: (recap.summary ?? '').length }), { timeoutMs: 1500 });
|
|
2964
|
+
}, onApplyTitle: () => {
|
|
2965
|
+
if (recap.title === undefined || recap.titleApplied)
|
|
2966
|
+
return;
|
|
2967
|
+
channel.renameSession(recap.title);
|
|
2968
|
+
setRecap(prev => (prev ? { ...prev, titleApplied: true } : prev));
|
|
2969
|
+
channel.notify(t('recap-title-applied-notify', { title: recap.title }), { color: 'success' });
|
|
2970
|
+
} }) })) : btw !== null ? (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(BtwPanel, { question: btw.question, answer: btw.answer, error: btw.error, streaming: !btw.done, onClose: closeBtw, onCopy: () => {
|
|
2108
2971
|
void setClipboard(btw.answer ?? '').then(raw => { if (raw)
|
|
2109
2972
|
writeRaw?.(raw); });
|
|
2110
2973
|
channel.notify(t('copied-chars', { n: (btw.answer ?? '').length }), { timeoutMs: 1500 });
|
|
2111
|
-
} }) })) : questionSnapshot !== null ? (_jsx(AskUserQuestionPanel, { question: questionSnapshot.question, position: questionSnapshot.position, total: questionSnapshot.total, answered: questionSnapshot.answered, onAnswer: selection => questionStore.answerCurrent(selection), onCancel: () => questionStore.cancelCurrent()
|
|
2974
|
+
} }) })) : questionSnapshot !== null ? (_jsx(AskUserQuestionPanel, { question: questionSnapshot.question, position: questionSnapshot.position, total: questionSnapshot.total, answered: questionSnapshot.answered, initialDraft: questionSnapshot.draft, onAnswer: selection => questionStore.answerCurrent(selection), onCancel: () => questionStore.cancelCurrent(), onBack: questionSnapshot.canGoBack
|
|
2975
|
+
? draft => questionStore.backCurrent(draft)
|
|
2976
|
+
: undefined }, questionSnapshot.key)) : (_jsx(PromptInput, { channel: channel, helpOpen: helpOpen, onToggleHelp: () => { setHelpOpen(previous => !previous); }, onRunCommand: runCommand, selectionActive: promptSelectionActive, fillText: historyFill, onFillConsumed: () => { setHistoryFill(null); }, onRewindRequest: openRewind, controllerRef: promptControllerRef })), _jsx(StatusLine, { channel: channel, selectionActive: selectionActive, helpOpen: helpOpen, wake: wakeBand === undefined
|
|
2112
2977
|
? undefined
|
|
2113
2978
|
: {
|
|
2114
2979
|
band: wakeBand,
|
|
2115
2980
|
hint: trajectorySeen ? undefined : `${modLabel}t`,
|
|
2116
2981
|
tick: Math.floor(wakeTime / 120),
|
|
2117
|
-
} }), dialogOverlayOpen && (_jsxs(OverlayAbove, { maxHeight: Math.max(terminalRows - 8,
|
|
2982
|
+
} }), dialogOverlayOpen && (_jsxs(OverlayAbove, { maxHeight: Math.max(terminalRows - 8, 1), children: [overlay.kind === 'thinking' && (_jsx(ThinkingToggle, { currentValue: thinkingVisible, focusIndex: overlay.focus, onPick: (index) => {
|
|
2983
|
+
// 点击行 = 设焦点 + 应用(与 Enter 同一条路径)
|
|
2984
|
+
const visible = index === 0;
|
|
2985
|
+
setThinkingVisible(visible);
|
|
2986
|
+
dispatchOverlay({ type: 'close' });
|
|
2987
|
+
channel.notify(t('thinking-toggled', { state: visible ? t('thinking-on') : t('thinking-off') }));
|
|
2988
|
+
} })), overlay.kind === 'workspace-picker' && workspaceTargets.length > 0 && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(WorkspacePicker, { targets: workspaceTargets, focusIndex: overlay.index, currentCwd: channel.cwd, onPick: (index) => {
|
|
2989
|
+
// 点击行 = 切换该行目标(与 Enter 同一条路径)
|
|
2990
|
+
const target = workspaceTargets[index];
|
|
2991
|
+
dispatchOverlay({ type: 'close' });
|
|
2992
|
+
if (target !== undefined)
|
|
2993
|
+
void channel.switchWorkspace(target);
|
|
2994
|
+
} }) })), overlay.kind === 'workspace-menu' && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(WorkspaceMenuPicker, { options: workspaceMenuOptions, focusIndex: overlay.index, onPick: (index) => {
|
|
2995
|
+
// 点击行 = 执行该行(与 Enter 同一条路径)
|
|
2996
|
+
runWorkspaceMenuOption(workspaceMenuOptions[index]);
|
|
2997
|
+
} }) })), overlay.kind === 'workspace-flow' && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(WorkspaceFlowPicker, { title: overlay.flow.title, choices: overlay.flow.choices, focusIndex: overlay.index, busy: overlay.busy, input: overlay.input, onPick: (index) => {
|
|
2998
|
+
// 点击行 = 设焦点 + 执行分支(与 Enter 同一条路径);
|
|
2999
|
+
// busy/输入态在组件侧禁点
|
|
3000
|
+
const choice = overlay.flow.choices[index];
|
|
3001
|
+
if (choice === undefined)
|
|
3002
|
+
return;
|
|
3003
|
+
dispatchOverlay({ type: 'set-index', kind: 'workspace-flow', index });
|
|
3004
|
+
runWorkspaceFlowAction(signal => choice.choose(signal));
|
|
3005
|
+
} }) })), overlay.kind === 'model' && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: models.length === 0 ? (_jsx(ModelPickerLoading, {})) : activeModelGroup === undefined ? (_jsx(ModelPicker, { groups: modelGroups, focusIndex: overlay.index, currentProvider: channel.provider, onPick: (index) => {
|
|
3006
|
+
// 点击分组行 = 进入该组(与 Enter 同一条路径)
|
|
3007
|
+
const group = modelGroups[index];
|
|
3008
|
+
if (!group)
|
|
3009
|
+
return;
|
|
3010
|
+
setModelGroup(group.provider);
|
|
3011
|
+
if (group.provider === RECENTS_GROUP_PROVIDER) {
|
|
3012
|
+
dispatchOverlay({ type: 'set-index', kind: 'model', index: 0 });
|
|
3013
|
+
return;
|
|
3014
|
+
}
|
|
3015
|
+
const landing = modelPickerLanding(models.filter(model => model.provider === group.provider), channel.provider, channel.model);
|
|
3016
|
+
dispatchOverlay({ type: 'set-index', kind: 'model', index: landing.index });
|
|
3017
|
+
} })) : (_jsx(ModelPicker, { models: groupModels, groupLabel: activeModelGroup === RECENTS_GROUP_PROVIDER
|
|
3018
|
+
? t('picker-group-recent')
|
|
3019
|
+
: modelGroups.find(group => group.provider === activeModelGroup)?.label, showBack: modelGroups.length > 1 && !modelPickerDirect, showProviderPrefix: activeModelGroup === RECENTS_GROUP_PROVIDER, focusIndex: overlay.index, currentModel: `${channel.provider}/${channel.model}`, onPick: (index) => {
|
|
3020
|
+
// 点击行 = 应用该行模型(与 Enter 同一条路径)
|
|
3021
|
+
const model = groupModels[index];
|
|
3022
|
+
if (!model)
|
|
3023
|
+
return;
|
|
3024
|
+
dispatchOverlay({ type: 'close' });
|
|
3025
|
+
void switchModelRecorded(model.provider, model.id, model.name);
|
|
3026
|
+
} })) })), overlay.kind === 'skills' && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: skillsList === null ? (_jsx(SkillsPickerLoading, {})) : (_jsx(SkillsPicker, { skills: skillsList, focusIndex: overlay.index, onPick: (index) => {
|
|
3027
|
+
const skill = skillsList[index];
|
|
3028
|
+
if (!skill)
|
|
3029
|
+
return;
|
|
3030
|
+
dispatchOverlay({ type: 'close' });
|
|
3031
|
+
if (skill.userInvocable)
|
|
3032
|
+
setHistoryFill(`/${skill.name} `);
|
|
3033
|
+
} })) })), overlay.kind === 'activity' && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(ActivityPicker, { focusIndex: overlay.index, currentPreset: channel.activityFrames, onPick: (index) => {
|
|
3034
|
+
dispatchOverlay({ type: 'close' });
|
|
3035
|
+
const name = PRESET_NAMES[index];
|
|
3036
|
+
if (name)
|
|
3037
|
+
channel.setActivityFrames(name);
|
|
3038
|
+
} }) })), overlay.kind === 'color' && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(ColorPicker, { focusIndex: overlay.index, currentColor: channel.sessionColor, onPick: (index) => {
|
|
3039
|
+
dispatchOverlay({ type: 'close' });
|
|
3040
|
+
const name = SESSION_COLOR_NAMES[index];
|
|
3041
|
+
if (name) {
|
|
3042
|
+
channel.setSessionColor(name);
|
|
3043
|
+
channel.notify(t('color-set', { name }), { color: 'success' });
|
|
3044
|
+
}
|
|
3045
|
+
} }) })), overlay.kind === 'effort' && effortOptions.length > 1 && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(EffortSlider, { options: effortOptions, focusIndex: overlay.index, currentId: channel.reasoningEffort,
|
|
3046
|
+
// 点击档位 = 移到该档并即时应用(与 ←/→ 同语义)
|
|
3047
|
+
onPick: (index) => {
|
|
3048
|
+
dispatchOverlay({ type: 'set-index', kind: 'effort', index });
|
|
3049
|
+
const option = effortOptions[index];
|
|
3050
|
+
if (option)
|
|
3051
|
+
void channel.setEffort(option.id);
|
|
3052
|
+
} }) })), overlay.kind === 'preset' && presetOptions.length > 0 && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(PresetPicker, { presets: presetOptions, focusIndex: overlay.index, currentPreset: channel.agentPreset, onPick: (index) => {
|
|
3053
|
+
dispatchOverlay({ type: 'close' });
|
|
3054
|
+
const option = presetOptions[index];
|
|
3055
|
+
if (option)
|
|
3056
|
+
void channel.switchPreset(option.id);
|
|
3057
|
+
} }) })), overlay.kind === 'permission' && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(PermissionsPicker, { focusIndex: overlay.index, currentMode: channel.mode.sandbox, cwd: channel.cwd, onPick: (index) => {
|
|
3058
|
+
dispatchOverlay({ type: 'close' });
|
|
3059
|
+
const id = PERMISSION_PRESET_IDS[index];
|
|
3060
|
+
if (id !== undefined) {
|
|
3061
|
+
void channel.runExternalCommand('permission', ` ${id}`).then((text) => {
|
|
3062
|
+
if (text !== undefined && text !== '')
|
|
3063
|
+
channel.notify(text);
|
|
3064
|
+
});
|
|
3065
|
+
}
|
|
3066
|
+
} }) })), overlay.kind === 'plan' && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(PlanPicker, { focusIndex: overlay.index, currentOn: channel.mode.plan === true, onPick: (index) => {
|
|
3067
|
+
dispatchOverlay({ type: 'close' });
|
|
3068
|
+
const on = index === 0;
|
|
3069
|
+
void channel.runExternalCommand('plan', on ? '' : ' off').then((text) => {
|
|
3070
|
+
if (text !== undefined && text !== '')
|
|
3071
|
+
channel.notify(text);
|
|
3072
|
+
});
|
|
3073
|
+
} }) })), overlay.kind === 'lang' && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(LangPicker, { focusIndex: overlay.index, currentLang: getLang(), onPick: (index) => {
|
|
3074
|
+
const lang = LANGS[index];
|
|
3075
|
+
if (lang === undefined)
|
|
3076
|
+
return;
|
|
3077
|
+
dispatchOverlay({ type: 'close' });
|
|
3078
|
+
applyLang(lang);
|
|
3079
|
+
} }) })), overlay.kind === 'theme' && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(ThemePicker, { focusIndex: overlay.index, currentTheme: themeName, onPick: (index) => {
|
|
3080
|
+
dispatchOverlay({ type: 'close' });
|
|
3081
|
+
const name = getThemeOptions()[index]?.value;
|
|
3082
|
+
if (name !== undefined) {
|
|
3083
|
+
const ok = setTheme(name);
|
|
3084
|
+
channel.notify(ok ? t('theme-switched-saved', { name }) : t('theme-switch-failed', { name }), { color: ok ? 'success' : 'error' });
|
|
3085
|
+
}
|
|
3086
|
+
} }) })), overlay.kind === 'history' && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(HistorySearchDialog, { query: overlay.query, cursorOffset: overlay.cursor, matches: historyMatches, focusIndex: overlay.focus, onPick: (index) => {
|
|
3087
|
+
// 点击行 = 填入该历史命令(与 Enter 同路径)
|
|
3088
|
+
const entry = historyMatches[index];
|
|
3089
|
+
if (entry) {
|
|
3090
|
+
setHistoryFill(entry.text);
|
|
3091
|
+
dispatchOverlay({ type: 'close' });
|
|
3092
|
+
}
|
|
3093
|
+
} }) })), overlay.kind === 'rewind' && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(RewindPicker, { rows: rewindRows, focusIndex: overlay.index, confirmRow: overlay.confirm, modes: overlay.modes, modeIndex: overlay.modeIndex, busy: overlay.busy, onPickRow: (index) => {
|
|
3094
|
+
// 列表页点击只选中:进入确认态保留键盘 Enter 显式触发
|
|
3095
|
+
dispatchOverlay({ type: 'set-index', kind: 'rewind', index });
|
|
3096
|
+
}, onConfirm: () => {
|
|
3097
|
+
// 确认页即显式确认层,点击直接执行(与 Enter 同路径)
|
|
3098
|
+
const row = overlay.confirm;
|
|
3099
|
+
if (row === null)
|
|
3100
|
+
return;
|
|
3101
|
+
dispatchOverlay({ type: 'close' });
|
|
3102
|
+
void performRewind(row);
|
|
3103
|
+
}, onPickMode: (index) => {
|
|
3104
|
+
// 模式列表点击直接执行该模式(与 Enter 同路径)
|
|
3105
|
+
const row = overlay.confirm;
|
|
3106
|
+
if (row === null)
|
|
3107
|
+
return;
|
|
3108
|
+
// 模式页仅当 modes 非空才渲染,这里空安全取值
|
|
3109
|
+
const mode = index === 0 ? null : (overlay.modes?.[index - 1]?.id ?? null);
|
|
3110
|
+
dispatchOverlay({ type: 'close' });
|
|
3111
|
+
void performRewind(row, mode);
|
|
3112
|
+
} }) })), overlay.kind === 'file-actions' && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(FileActionsPanel, { path: overlay.path, isDir: overlay.isDir, focusIndex: overlay.index, onPick: (index) => {
|
|
3113
|
+
// 点击行直接执行该动作(与 Enter 同路径)
|
|
3114
|
+
const path = overlay.path;
|
|
3115
|
+
dispatchOverlay({ type: 'close' });
|
|
3116
|
+
runFileAction(index, path);
|
|
3117
|
+
} }) })), overlay.kind === 'search' && _jsx(TranscriptSearchBar, { query: searchQuery, cursorOffset: searchCursor, count: searchCount, current: searchCurrent })] }))] })] }));
|
|
2118
3118
|
}
|
|
2119
3119
|
/**
|
|
2120
3120
|
* The pinned prompt header shown above the ScrollBox while the user has
|
|
2121
3121
|
* scrolled up (mirroring Claude Code's FullscreenLayout.StickyPromptHeader).
|
|
2122
|
-
*
|
|
3122
|
+
* Pins the user message the transcript viewport is currently showing — the
|
|
3123
|
+
* topmost visible user message, or the nearest one above when only assistant
|
|
3124
|
+
* content fills the view — so it tracks which turn the user is reading
|
|
3125
|
+
* instead of always carrying the latest prompt. Fixed at 1 row so the
|
|
3126
|
+
* ScrollBox never shifts when the text changes.
|
|
2123
3127
|
*/
|
|
2124
3128
|
function StickyPromptHeader({ text, onClick, }) {
|
|
2125
3129
|
return (_jsx(Box, { flexShrink: 0, width: "100%", height: 1, paddingRight: 1, onClick: onClick, children: _jsxs(Text, { color: "briefLabelYou", bold: true, wrap: "truncate-end", children: [POINTER, " ", text] }) }));
|
|
@@ -2127,7 +3131,9 @@ function StickyPromptHeader({ text, onClick, }) {
|
|
|
2127
3131
|
/** The `↓ N new messages` pill shown while scrolled up with new content. */
|
|
2128
3132
|
function NewMessagesPill({ count, onClick, }) {
|
|
2129
3133
|
const [hover, setHover] = React.useState(false);
|
|
2130
|
-
return (_jsx(Box, { paddingX: 2, paddingTop: 1, children: _jsx(Box, { backgroundColor: hover ? 'userMessageBackgroundHover' : 'background', onClick: onClick, onMouseEnter: () => { setHover(true); }, onMouseLeave: () => { setHover(false); }, children: _jsxs(Text, { color: "inverseText", bold: true, children: [' ',
|
|
3134
|
+
return (_jsx(Box, { paddingX: 2, paddingTop: 1, children: _jsx(Box, { backgroundColor: hover ? 'userMessageBackgroundHover' : 'background', onClick: onClick, onMouseEnter: () => { setHover(true); }, onMouseLeave: () => { setHover(false); }, children: _jsxs(Text, { color: "inverseText", bold: true, children: [' ', count > 0
|
|
3135
|
+
? t(count === 1 ? 'new-message' : 'new-messages', { n: count })
|
|
3136
|
+
: t('back-to-bottom'), ' '] }) }) }));
|
|
2131
3137
|
}
|
|
2132
3138
|
/** /model while the provider catalog is still loading (CC's LoadingState). */
|
|
2133
3139
|
function ModelPickerLoading() {
|