@deepseek-harness-tui/dsh-tui 0.8.7 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -10
- package/bin/dsh-tui.js +1 -1
- package/lib/types/commands.d.ts.map +1 -1
- package/lib/types/commands.js +4 -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/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/CommandSuggestions.d.ts +21 -4
- package/lib/types/components/CommandSuggestions.d.ts.map +1 -1
- package/lib/types/components/CommandSuggestions.js +41 -14
- 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/FileSuggestions.d.ts +26 -5
- package/lib/types/components/FileSuggestions.d.ts.map +1 -1
- package/lib/types/components/FileSuggestions.js +53 -16
- 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/LogoV2.d.ts +5 -1
- package/lib/types/components/LogoV2.d.ts.map +1 -1
- package/lib/types/components/LogoV2.js +12 -3
- 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 +43 -2
- package/lib/types/components/MessageList.d.ts.map +1 -1
- package/lib/types/components/MessageList.js +553 -41
- package/lib/types/components/ModelPicker.d.ts +4 -1
- package/lib/types/components/ModelPicker.d.ts.map +1 -1
- package/lib/types/components/ModelPicker.js +2 -2
- 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 +273 -50
- 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 +68 -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 +6 -1
- 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 +63 -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 +190 -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/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 +8 -1
- package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantToolUseMessage.js +4 -2
- 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.map +1 -1
- package/lib/types/components/questions/AskUserQuestionPanel.js +34 -2
- 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 +27 -6
- package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel.js +432 -66
- package/lib/types/dsh-adapter/contract.d.ts +58 -18
- package/lib/types/dsh-adapter/contract.d.ts.map +1 -1
- package/lib/types/dsh-adapter/contract.js +124 -36
- package/lib/types/dsh-adapter/index.d.ts +10 -2
- package/lib/types/dsh-adapter/index.d.ts.map +1 -1
- package/lib/types/dsh-adapter/index.js +10 -13
- 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 +136 -6
- package/lib/types/i18n.d.ts +194 -16
- package/lib/types/i18n.d.ts.map +1 -1
- package/lib/types/i18n.js +73 -10
- 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 +268 -52
- 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 +16 -3
- package/lib/types/ink/render-node-to-output.d.ts.map +1 -1
- package/lib/types/ink/render-node-to-output.js +144 -25
- package/lib/types/ink/selection.d.ts +29 -4
- package/lib/types/ink/selection.d.ts.map +1 -1
- package/lib/types/ink/selection.js +41 -5
- 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/screens/Chat.d.ts.map +1 -1
- package/lib/types/screens/Chat.js +819 -337
- package/lib/types/screens/SessionBrowser.d.ts.map +1 -1
- package/lib/types/screens/SessionBrowser.js +56 -29
- 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 +2 -1
- package/lib/types/screens/StatusLine.d.ts.map +1 -1
- package/lib/types/screens/StatusLine.js +17 -1
- 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 +221 -0
- package/lib/types/screens/chatOverlay.d.ts.map +1 -0
- package/lib/types/screens/chatOverlay.js +114 -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/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/fileSuggestions.d.ts +19 -0
- package/lib/types/utils/fileSuggestions.d.ts.map +1 -0
- package/lib/types/utils/fileSuggestions.js +40 -0
- package/lib/types/utils/mentions.d.ts.map +1 -1
- package/lib/types/utils/mentions.js +3 -1
- package/lib/types/utils/paths.d.ts.map +1 -1
- package/lib/types/utils/paths.js +1 -0
- package/package.json +69 -66
|
@@ -1,6 +1,6 @@
|
|
|
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, subscribeLang, LANGS } from '../i18n.js';
|
|
4
4
|
import { AlternateScreen, Box, Text, useInput, ScrollBox, useTheme, useTerminalSize } from '../ui.js';
|
|
5
5
|
import * as tuiKit from '../ui.js';
|
|
6
6
|
import { POINTER } from '../cc/figures.js';
|
|
@@ -22,6 +22,9 @@ import { useCopyOnSelect } from '../ink/hooks/use-copy-on-select.js';
|
|
|
22
22
|
import { useSelection } from '../ink/hooks/use-selection.js';
|
|
23
23
|
import { NoSelect } from '../ink/components/NoSelect.js';
|
|
24
24
|
import { LogoHeader, MessageList } from '../components/MessageList.js';
|
|
25
|
+
import { TimelineRail } from '../components/TimelineRail.js';
|
|
26
|
+
import { ScrollbarGutter } from '../components/ScrollbarGutter.js';
|
|
27
|
+
import { normalizeScrollGutter } from '../tuiDisplayPrefs.js';
|
|
25
28
|
import { OverlayAbove } from '../components/OverlayAbove.js';
|
|
26
29
|
import { PromptInput } from '../components/PromptInput.js';
|
|
27
30
|
import { GoalTodoPanel } from '../components/GoalTodoPanel.js';
|
|
@@ -35,10 +38,14 @@ import { SkillsPicker, SkillsPickerLoading } from '../components/SkillsPicker.js
|
|
|
35
38
|
import { SessionBrowser } from './SessionBrowser.js';
|
|
36
39
|
import { Settings } from './Settings.js';
|
|
37
40
|
import { WorkspacePicker } from '../components/WorkspacePicker.js';
|
|
41
|
+
import { WorkspaceMenuPicker } from '../components/WorkspaceMenuPicker.js';
|
|
38
42
|
import { WorkspaceFlowPicker } from '../components/WorkspaceFlowPicker.js';
|
|
39
43
|
import { ActivityPicker } from '../components/ActivityPicker.js';
|
|
40
44
|
import { EffortSlider } from '../components/EffortSlider.js';
|
|
41
45
|
import { PresetPicker } from '../components/PresetPicker.js';
|
|
46
|
+
import { PermissionsPicker, PERMISSION_PRESET_IDS } from '../components/PermissionsPicker.js';
|
|
47
|
+
import { PlanPicker } from '../components/PlanPicker.js';
|
|
48
|
+
import { LangPicker } from '../components/LangPicker.js';
|
|
42
49
|
import { ThemePicker, getThemeOptions } from '../components/ThemePicker.js';
|
|
43
50
|
import { AUTO_THEME_NAME, getAutoThemeBase } from '../theme.js';
|
|
44
51
|
import { FRAME_PRESETS, PRESET_NAMES } from '../components/activityFrames.js';
|
|
@@ -61,6 +68,7 @@ import { LoadingState } from '../components/design-system/LoadingState.js';
|
|
|
61
68
|
import { Pane } from '../components/design-system/Pane.js';
|
|
62
69
|
import { loadHistory } from '../history.js';
|
|
63
70
|
import { formatLoadedContextReport } from '../utils/loaded-context.js';
|
|
71
|
+
import { NO_OVERLAY, chatOverlayReducer, dialogOverlayVisible, wrapIndex, } from './chatOverlay.js';
|
|
64
72
|
/** Shared empty snapshot for hosts whose channel has no event log. */
|
|
65
73
|
const NO_EVENTS = [];
|
|
66
74
|
/** Row kinds the message-selection cursor can land on. */
|
|
@@ -113,8 +121,11 @@ function searchableText(row) {
|
|
|
113
121
|
}
|
|
114
122
|
/**
|
|
115
123
|
* Main chat screen in the Claude Code layout: a scrollable transcript
|
|
116
|
-
* (with the
|
|
117
|
-
* up
|
|
124
|
+
* (with the user message the viewport is showing pinned above the transcript
|
|
125
|
+
* while scrolled up, and a 1-column minimap scrollbar with one node per
|
|
126
|
+
* user message — the current message's node is highlighted, clicking a node
|
|
127
|
+
* jumps to it), transient notifications, the working spinner, the bordered
|
|
128
|
+
* prompt
|
|
118
129
|
* input (with slash-command overlay) and the status line pinned at the
|
|
119
130
|
* bottom.
|
|
120
131
|
*
|
|
@@ -188,16 +199,42 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
188
199
|
const [expanded, setExpanded] = React.useState(false);
|
|
189
200
|
const [helpOpen, setHelpOpen] = React.useState(false);
|
|
190
201
|
const [handle, setHandle] = React.useState(null);
|
|
202
|
+
/**
|
|
203
|
+
* Conversation timeline snapshot (reported by MessageList): one entry
|
|
204
|
+
* per user turn plus the viewport-derived navigation targets. The
|
|
205
|
+
* ACTIVE turn — the one whose content owns the viewport top row — pins
|
|
206
|
+
* the sticky prompt header AND highlights the transcript rail's tick,
|
|
207
|
+
* from one report so the two can never disagree; upId/downId drive the
|
|
208
|
+
* rail's ▲/▼. Null activeId while pinned to the bottom only when there
|
|
209
|
+
* are no turns (header hidden there anyway).
|
|
210
|
+
*/
|
|
211
|
+
const [timeline, setTimeline] = React.useState({
|
|
212
|
+
turns: [],
|
|
213
|
+
activeId: null,
|
|
214
|
+
upId: null,
|
|
215
|
+
downId: null,
|
|
216
|
+
});
|
|
191
217
|
const [selectionActive, setSelectionActive] = React.useState(false);
|
|
192
218
|
const [selectedId, setSelectedId] = React.useState(null);
|
|
193
219
|
const [expandedRows, setExpandedRows] = React.useState(() => new Set());
|
|
194
|
-
|
|
220
|
+
/** 流式 reasoning 行的用户折叠(点击/进入折叠态)。与 expandedRows 分开:
|
|
221
|
+
* 流式默认展开,用户点一下 = 折叠(preview ticker 或单行头);落定后
|
|
222
|
+
* 默认折叠,此集合不再参与——两种默认互不翻转。 */
|
|
223
|
+
const [streamFoldedRows, setStreamFoldedRows] = React.useState(() => new Set());
|
|
224
|
+
/**
|
|
225
|
+
* The transient-dialog layer (every picker/dialog `<OverlayAbove>` hosts,
|
|
226
|
+
* plus /tips) as ONE value: mutual exclusion between the panels is
|
|
227
|
+
* structural instead of emerging from "an open picker makes the prompt
|
|
228
|
+
* inert". Transitions live in the pure reducer (chatOverlay.ts), which
|
|
229
|
+
* scripts/verify-chat-overlay.ts pins without a renderer. Async data the
|
|
230
|
+
* pickers show (model list, preset roster, …) stays in the caches below —
|
|
231
|
+
* it persists across open/close so a reopened picker paints the previous
|
|
232
|
+
* list while the fresh one loads, exactly as the boolean era did.
|
|
233
|
+
*/
|
|
234
|
+
const [overlay, dispatchOverlay] = React.useReducer(chatOverlayReducer, NO_OVERLAY);
|
|
195
235
|
const [models, setModels] = React.useState([]);
|
|
196
|
-
const [modelIndex, setModelIndex] = React.useState(0);
|
|
197
236
|
/** `/skills` 技能目录(issue #204):null = 注册表快照在途。 */
|
|
198
|
-
const [skillsPickerOpen, setSkillsPickerOpen] = React.useState(false);
|
|
199
237
|
const [skillsList, setSkillsList] = React.useState(null);
|
|
200
|
-
const [skillsIndex, setSkillsIndex] = React.useState(0);
|
|
201
238
|
/** `/resume` opens the session browser, a screen rather than a panel. It
|
|
202
239
|
* owns its own selection, filters and keyboard — Chat only opens it. */
|
|
203
240
|
const [browserOpen, setBrowserOpen] = React.useState(false);
|
|
@@ -205,53 +242,22 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
205
242
|
* browser, a screen rather than a panel: it owns its own focus, staged
|
|
206
243
|
* drafts and keyboard; Chat only opens it. */
|
|
207
244
|
const [settingsOpen, setSettingsOpen] = React.useState(false);
|
|
208
|
-
const [workspacePickerOpen, setWorkspacePickerOpen] = React.useState(false);
|
|
209
245
|
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
246
|
const workspaceFlowRequestRef = React.useRef(0);
|
|
216
247
|
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);
|
|
248
|
+
/** `/preset` agent-preset roster (issue #8): loads async, persists. */
|
|
222
249
|
const [presetOptions, setPresetOptions] = React.useState([]);
|
|
223
|
-
|
|
224
|
-
/** `/effort` rheostat slider: adapter levels load async, focus moves ←/→. */
|
|
225
|
-
const [effortSliderOpen, setEffortSliderOpen] = React.useState(false);
|
|
250
|
+
/** `/effort` adapter levels: load async before the slider opens. */
|
|
226
251
|
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
252
|
const [themeName, setTheme] = useTheme();
|
|
232
253
|
const { rows: terminalRows } = useTerminalSize();
|
|
233
254
|
const [showAllMessages, setShowAllMessages] = React.useState(false);
|
|
255
|
+
/** Fold state for the GoalTodoPanel todo section (ctrl/cmd+q or click). */
|
|
256
|
+
const [todoCollapsed, setTodoCollapsed] = React.useState(false);
|
|
234
257
|
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);
|
|
258
|
+
/** ctrl+r history-search entries (loaded on open, persists). */
|
|
242
259
|
const [historyEntries, setHistoryEntries] = React.useState([]);
|
|
243
260
|
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
261
|
/** Monotonic token: only the latest rewind decision may land (a slow
|
|
256
262
|
* plugin answering after the user moved on must not open a confirm for
|
|
257
263
|
* a row they are no longer looking at). */
|
|
@@ -265,8 +271,6 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
265
271
|
btwAbortRef.current = null;
|
|
266
272
|
setBtw(null);
|
|
267
273
|
};
|
|
268
|
-
/** /tips usage-tips overlay: pure UI state, no session side effects. */
|
|
269
|
-
const [tipsOpen, setTipsOpen] = React.useState(false);
|
|
270
274
|
/** Subagent dashboard (Ctrl+A): displays active/completed subagents. */
|
|
271
275
|
const [subagentDashboardOpen, setSubagentDashboardOpen] = React.useState(false);
|
|
272
276
|
/** Detail view for a specific subagent (opened from dashboard). */
|
|
@@ -321,8 +325,10 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
321
325
|
ink?.invalidatePrevFrame();
|
|
322
326
|
ink?.reanchorViewport();
|
|
323
327
|
}, []);
|
|
324
|
-
/** `/` transcript search (less-style incsearch, ported from CC's REPL).
|
|
325
|
-
|
|
328
|
+
/** `/` transcript search (less-style incsearch, ported from CC's REPL).
|
|
329
|
+
* Only the bar's open/closed mode lives in `overlay`; the query and match
|
|
330
|
+
* counters persist past the bar closing so n/N keep walking the matches. */
|
|
331
|
+
const searchActive = overlay.kind === 'search';
|
|
326
332
|
const [searchQuery, setSearchQuery] = React.useState('');
|
|
327
333
|
const [searchCursor, setSearchCursor] = React.useState(0);
|
|
328
334
|
const [searchCount, setSearchCount] = React.useState(0);
|
|
@@ -353,7 +359,10 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
353
359
|
: -1;
|
|
354
360
|
}
|
|
355
361
|
}, [isSticky, channel.rows]);
|
|
356
|
-
|
|
362
|
+
// The pill shows whenever the view is off the bottom (one-click return
|
|
363
|
+
// home): with unseen rows it counts them, otherwise it is the plain
|
|
364
|
+
// "return to bottom" affordance (Enter/End/click all land it).
|
|
365
|
+
const showPill = !isSticky;
|
|
357
366
|
// Idle Ctrl+C: first press arms an exit, second press exits (CC's
|
|
358
367
|
// double-press semantics, simplified). Under Windows ConPTY the key
|
|
359
368
|
// arrives as stdin data (key.ctrl && input === 'c') — the useInput
|
|
@@ -423,26 +432,30 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
423
432
|
useTerminalTitle(`${titlePrefix} 🐋 ${channel.sessionTitle}`);
|
|
424
433
|
const handleWorkspaceResult = (result) => {
|
|
425
434
|
workspaceFlowAbortRef.current = null;
|
|
426
|
-
setWorkspaceFlowBusy(false);
|
|
427
|
-
setWorkspaceFlowInput(null);
|
|
428
435
|
if (result.kind === 'target') {
|
|
429
|
-
|
|
436
|
+
dispatchOverlay({ type: 'close-if', kind: 'workspace-flow' });
|
|
430
437
|
void channel.switchWorkspace(result.target);
|
|
431
438
|
return;
|
|
432
439
|
}
|
|
433
440
|
if (result.choices.length === 0) {
|
|
434
|
-
|
|
441
|
+
dispatchOverlay({ type: 'close-if', kind: 'workspace-flow' });
|
|
435
442
|
channel.notify(t('workspace-command-empty'));
|
|
436
443
|
return;
|
|
437
444
|
}
|
|
438
|
-
|
|
439
|
-
|
|
445
|
+
// open-if: 'workspace-flow' stays allowed so an in-flow action can
|
|
446
|
+
// transition to its next stage; a picker the user opened after leaving
|
|
447
|
+
// the menu wins over a late command result.
|
|
448
|
+
dispatchOverlay({
|
|
449
|
+
type: 'open-if',
|
|
450
|
+
overlay: { kind: 'workspace-flow', flow: result, index: 0, busy: false, input: null },
|
|
451
|
+
when: ['none', 'workspace-flow'],
|
|
452
|
+
});
|
|
440
453
|
};
|
|
441
454
|
const runWorkspaceFlowAction = (action) => {
|
|
442
455
|
const request = ++workspaceFlowRequestRef.current;
|
|
443
456
|
const controller = new AbortController();
|
|
444
457
|
workspaceFlowAbortRef.current = controller;
|
|
445
|
-
|
|
458
|
+
dispatchOverlay({ type: 'flow-busy', busy: true });
|
|
446
459
|
void Promise.resolve()
|
|
447
460
|
.then(() => action(controller.signal))
|
|
448
461
|
.then((result) => {
|
|
@@ -453,10 +466,25 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
453
466
|
if (request !== workspaceFlowRequestRef.current)
|
|
454
467
|
return;
|
|
455
468
|
workspaceFlowAbortRef.current = null;
|
|
456
|
-
|
|
469
|
+
dispatchOverlay({ type: 'flow-busy', busy: false });
|
|
457
470
|
channel.notify(t('workspace-command-failed', { err: error instanceof Error ? error.message : String(error) }), { color: 'error', timeoutMs: 8000 });
|
|
458
471
|
});
|
|
459
472
|
};
|
|
473
|
+
/** Bare `/workspace` menu rows: built-in subcommands first, then the
|
|
474
|
+
* dynamically registered extensions (same reserved-name filter the Tab
|
|
475
|
+
* completion applies). Recomputed per render — the extension list is
|
|
476
|
+
* live. */
|
|
477
|
+
const workspaceMenuOptions = [
|
|
478
|
+
{ id: 'resume', label: 'resume', description: t('workspace-menu-resume-desc') },
|
|
479
|
+
{ id: 'rename', label: 'rename', description: t('workspace-menu-rename-desc') },
|
|
480
|
+
{ id: 'open', label: 'open', description: t('workspace-menu-open-desc') },
|
|
481
|
+
// Optional call: verify/repro scripts and embedders stub the channel
|
|
482
|
+
// without the workspace-commands API — render must not throw for them
|
|
483
|
+
// (a thrown render unmounts the whole Ink root).
|
|
484
|
+
...(channel.workspaceCommands?.() ?? [])
|
|
485
|
+
.filter(command => !['resume', 'rename', 'open'].includes(command.name.toLowerCase()))
|
|
486
|
+
.map(command => ({ id: command.name, label: command.name, description: command.description })),
|
|
487
|
+
];
|
|
460
488
|
const openWorkspaceTarget = (reference) => {
|
|
461
489
|
void channel.resolveWorkspace(reference).then((target) => {
|
|
462
490
|
if (target === undefined) {
|
|
@@ -475,12 +503,45 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
475
503
|
return;
|
|
476
504
|
}
|
|
477
505
|
setWorkspaceTargets(targets);
|
|
478
|
-
|
|
479
|
-
|
|
506
|
+
// open-if: the listing is async — whatever the user opened meanwhile wins.
|
|
507
|
+
dispatchOverlay({
|
|
508
|
+
type: 'open-if',
|
|
509
|
+
overlay: {
|
|
510
|
+
kind: 'workspace-picker',
|
|
511
|
+
index: Math.max(0, targets.findIndex(target => target.cwd === channel.cwd)),
|
|
512
|
+
},
|
|
513
|
+
when: ['none'],
|
|
514
|
+
});
|
|
480
515
|
}).catch((error) => {
|
|
481
516
|
channel.notify(t('workspace-list-failed', { err: error instanceof Error ? error.message : String(error) }), { color: 'error' });
|
|
482
517
|
});
|
|
483
518
|
};
|
|
519
|
+
/**
|
|
520
|
+
* Run one /workspace menu row (Enter path, shared with the mouse click):
|
|
521
|
+
* built-ins dispatch locally, extension commands go through the channel.
|
|
522
|
+
*/
|
|
523
|
+
const runWorkspaceMenuOption = (option) => {
|
|
524
|
+
dispatchOverlay({ type: 'close-if', kind: 'workspace-menu' });
|
|
525
|
+
if (option === undefined)
|
|
526
|
+
return;
|
|
527
|
+
if (option.id === 'resume') {
|
|
528
|
+
openWorkspaceResume();
|
|
529
|
+
}
|
|
530
|
+
else if (option.id === 'rename') {
|
|
531
|
+
channel.notify(t('workspace-rename-usage'));
|
|
532
|
+
}
|
|
533
|
+
else if (option.id === 'open') {
|
|
534
|
+
channel.notify(t('workspace-open-usage'));
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
void channel.runWorkspaceCommand(option.id, '').then((result) => {
|
|
538
|
+
if (result !== undefined)
|
|
539
|
+
handleWorkspaceResult(result);
|
|
540
|
+
}).catch((error) => {
|
|
541
|
+
channel.notify(t('workspace-command-failed', { err: error instanceof Error ? error.message : String(error) }), { color: 'error', timeoutMs: 8000 });
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
};
|
|
484
545
|
/**
|
|
485
546
|
* Dispatch a slash command; false lets the input flow to the model.
|
|
486
547
|
* Built-in names run the local switch; anything registered by a DSH
|
|
@@ -488,6 +549,31 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
488
549
|
* result text lands as a notification. `rawInput` carries the text after
|
|
489
550
|
* the command name (`/plan off` → ` off`).
|
|
490
551
|
*/
|
|
552
|
+
/** Localized display name of a sandbox mode id (`read-only` /
|
|
553
|
+
* `workspace-write` / `danger-full-access`), for `/permission status`. */
|
|
554
|
+
const permissionPresetName = (id) => {
|
|
555
|
+
switch (id) {
|
|
556
|
+
case 'read-only': return t('permission-preset-readonly');
|
|
557
|
+
case 'workspace-write': return t('permission-preset-workspace-write');
|
|
558
|
+
case 'danger-full-access': return t('permission-preset-full-access');
|
|
559
|
+
default: return id ?? '—';
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
/** Hot-swap the UI language (`/lang <id>` and the LangPicker both land
|
|
563
|
+
* here): persist to ~/.dsh-tui/lang.json and mirror into the dsh-tui
|
|
564
|
+
* settings namespace when it is served (best effort). */
|
|
565
|
+
const applyLang = (lang) => {
|
|
566
|
+
const ok = writeLangPref(lang);
|
|
567
|
+
setLang(lang);
|
|
568
|
+
const settingsHost = channel.settingsHost();
|
|
569
|
+
const tuiView = settingsHost?.listNamespaces().find(entry => entry.ns === 'dsh-tui');
|
|
570
|
+
if (settingsHost !== undefined && tuiView !== undefined) {
|
|
571
|
+
void settingsHost
|
|
572
|
+
.write('dsh-tui', [{ op: 'set', path: ['lang'], value: lang }], tuiView.revision)
|
|
573
|
+
.catch(() => { });
|
|
574
|
+
}
|
|
575
|
+
channel.notify(ok ? t('lang-switched', { lang }) : t('lang-switch-failed', { lang }), { color: ok ? 'success' : 'error' });
|
|
576
|
+
};
|
|
491
577
|
const runCommand = (name, rawInput = '') => {
|
|
492
578
|
switch (name) {
|
|
493
579
|
case 'activity': {
|
|
@@ -524,8 +610,13 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
524
610
|
return true;
|
|
525
611
|
}
|
|
526
612
|
setHelpOpen(false);
|
|
527
|
-
|
|
528
|
-
|
|
613
|
+
dispatchOverlay({
|
|
614
|
+
type: 'open',
|
|
615
|
+
overlay: {
|
|
616
|
+
kind: 'activity',
|
|
617
|
+
index: Math.max(0, PRESET_NAMES.indexOf(channel.activityFrames ?? 'random')),
|
|
618
|
+
},
|
|
619
|
+
});
|
|
529
620
|
return true;
|
|
530
621
|
}
|
|
531
622
|
case 'preset': {
|
|
@@ -552,16 +643,26 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
552
643
|
return true;
|
|
553
644
|
}
|
|
554
645
|
setHelpOpen(false);
|
|
555
|
-
|
|
646
|
+
// The picker opens immediately over the cached roster (no loading
|
|
647
|
+
// pane — deliberate contrast with /model); the fresh list lands with
|
|
648
|
+
// the authoritative focus. Both loader writes are kind-guarded, so a
|
|
649
|
+
// picker the user already left is not resurrected or re-focused.
|
|
650
|
+
dispatchOverlay({
|
|
651
|
+
type: 'open',
|
|
652
|
+
overlay: {
|
|
653
|
+
kind: 'preset',
|
|
654
|
+
index: Math.max(0, presetOptions.findIndex(preset => preset.id === channel.agentPreset)),
|
|
655
|
+
},
|
|
656
|
+
});
|
|
556
657
|
void channel.listPresets().then((list) => {
|
|
557
658
|
if (list.length === 0) {
|
|
558
|
-
|
|
659
|
+
dispatchOverlay({ type: 'close-if', kind: 'preset' });
|
|
559
660
|
channel.notify(t('preset-roster-unmounted'), { color: 'warning' });
|
|
560
661
|
return;
|
|
561
662
|
}
|
|
562
663
|
setPresetOptions(list);
|
|
563
664
|
const index = list.findIndex(preset => preset.id === channel.agentPreset);
|
|
564
|
-
|
|
665
|
+
dispatchOverlay({ type: 'set-index', kind: 'preset', index: index >= 0 ? index : 0 });
|
|
565
666
|
});
|
|
566
667
|
return true;
|
|
567
668
|
}
|
|
@@ -592,16 +693,22 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
592
693
|
setEffortOptions(efforts);
|
|
593
694
|
const current = channel.reasoningEffort ?? defaultEffort;
|
|
594
695
|
const index = efforts.findIndex(effort => effort.id === current);
|
|
595
|
-
|
|
596
|
-
|
|
696
|
+
// open-if: a picker the user opened during the round trip wins
|
|
697
|
+
// over this late-arriving slider.
|
|
698
|
+
dispatchOverlay({
|
|
699
|
+
type: 'open-if',
|
|
700
|
+
overlay: { kind: 'effort', index: index >= 0 ? index : 0 },
|
|
701
|
+
when: ['none'],
|
|
702
|
+
});
|
|
597
703
|
});
|
|
598
704
|
return true;
|
|
599
705
|
}
|
|
600
706
|
case 'lang': {
|
|
601
707
|
// `/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
|
|
708
|
+
// (hot-swap, persisted to ~/.dsh-tui/lang.json), bare `/lang` opens
|
|
709
|
+
// the en/zh picker. Precedence on next launch: DSH_TUI_LANG >
|
|
710
|
+
// settings.yaml `dsh-tui.lang` > cordis.yml `lang` > the persisted
|
|
711
|
+
// choice.
|
|
605
712
|
const parts = rawInput.trim().split(/\s+/).filter(Boolean);
|
|
606
713
|
if (parts[0] === 'status') {
|
|
607
714
|
setHelpOpen(false);
|
|
@@ -615,19 +722,7 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
615
722
|
if (parts.length > 0) {
|
|
616
723
|
setHelpOpen(false);
|
|
617
724
|
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' });
|
|
725
|
+
applyLang(parts[0]);
|
|
631
726
|
}
|
|
632
727
|
else {
|
|
633
728
|
channel.notify(t('lang-unknown', { lang: parts[0] }), { color: 'error' });
|
|
@@ -635,11 +730,10 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
635
730
|
return true;
|
|
636
731
|
}
|
|
637
732
|
setHelpOpen(false);
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
]);
|
|
733
|
+
dispatchOverlay({
|
|
734
|
+
type: 'open',
|
|
735
|
+
overlay: { kind: 'lang', index: getLang() === 'zh' ? 0 : 1 },
|
|
736
|
+
});
|
|
643
737
|
return true;
|
|
644
738
|
}
|
|
645
739
|
case 'theme': {
|
|
@@ -672,8 +766,13 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
672
766
|
return true;
|
|
673
767
|
}
|
|
674
768
|
setHelpOpen(false);
|
|
675
|
-
|
|
676
|
-
|
|
769
|
+
dispatchOverlay({
|
|
770
|
+
type: 'open',
|
|
771
|
+
overlay: {
|
|
772
|
+
kind: 'theme',
|
|
773
|
+
index: Math.max(0, getThemeOptions().findIndex(option => option.value === themeName)),
|
|
774
|
+
},
|
|
775
|
+
});
|
|
677
776
|
return true;
|
|
678
777
|
}
|
|
679
778
|
case 'new': {
|
|
@@ -735,32 +834,90 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
735
834
|
case 'help':
|
|
736
835
|
setHelpOpen(true);
|
|
737
836
|
return true;
|
|
738
|
-
case 'model':
|
|
837
|
+
case 'model': {
|
|
838
|
+
// `/model <provider/model>` switches directly (same live-fork path
|
|
839
|
+
// as the picker's Enter), bare `/model` opens the picker.
|
|
840
|
+
const parts = rawInput.trim().split(/\s+/).filter(Boolean);
|
|
841
|
+
if (parts.length > 0) {
|
|
842
|
+
setHelpOpen(false);
|
|
843
|
+
const spec = parts[0];
|
|
844
|
+
const slash = spec.indexOf('/');
|
|
845
|
+
const provider = slash >= 0 ? spec.slice(0, slash) : undefined;
|
|
846
|
+
const id = slash >= 0 ? spec.slice(slash + 1) : spec;
|
|
847
|
+
if (provider === undefined || id.length === 0 || provider.length === 0) {
|
|
848
|
+
channel.notify(t('model-usage'), { color: 'warning' });
|
|
849
|
+
return true;
|
|
850
|
+
}
|
|
851
|
+
void channel.listModels().then((list) => {
|
|
852
|
+
const model = list.find(m => m.provider === provider && m.id === id);
|
|
853
|
+
if (model === undefined) {
|
|
854
|
+
channel.notify(t('model-unknown', { spec }), { color: 'error', timeoutMs: 8000 });
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
channel.notify(t('model-switching', { name: model.name }));
|
|
858
|
+
void channel.switchModel(provider, id).then((ok) => {
|
|
859
|
+
if (ok)
|
|
860
|
+
channel.notify(t('model-switched', { name: model.name }));
|
|
861
|
+
});
|
|
862
|
+
});
|
|
863
|
+
return true;
|
|
864
|
+
}
|
|
739
865
|
setHelpOpen(false);
|
|
740
|
-
|
|
866
|
+
// Opens over the cached catalog (empty cache shows the loading
|
|
867
|
+
// pane); the fresh list lands with the authoritative focus, and the
|
|
868
|
+
// kind-guarded set-index cannot re-focus a picker the user left.
|
|
869
|
+
dispatchOverlay({
|
|
870
|
+
type: 'open',
|
|
871
|
+
overlay: {
|
|
872
|
+
kind: 'model',
|
|
873
|
+
index: Math.max(0, models.findIndex(model => model.provider === channel.provider && model.id === channel.model)),
|
|
874
|
+
},
|
|
875
|
+
});
|
|
741
876
|
void channel.listModels().then((list) => {
|
|
742
877
|
setModels(list);
|
|
743
878
|
const index = list.findIndex(model => model.provider === channel.provider && model.id === channel.model);
|
|
744
|
-
|
|
879
|
+
dispatchOverlay({ type: 'set-index', kind: 'model', index: index >= 0 ? index : 0 });
|
|
745
880
|
});
|
|
746
881
|
return true;
|
|
747
|
-
|
|
882
|
+
}
|
|
883
|
+
case 'skills': {
|
|
748
884
|
// issue #204: 列出当前 agent 的完整技能目录(名称 + 来源 + 简述),
|
|
749
885
|
// Enter 把可直调技能以 `/name ` 填回输入行(completion-only 分发的
|
|
750
886
|
// 同一路径)。注册表读取走 channel(快照 scoped 到 live agent)。
|
|
887
|
+
// `/skills <name>` 直达同一个填回动作,跳过选择器。
|
|
888
|
+
const parts = rawInput.trim().split(/\s+/).filter(Boolean);
|
|
889
|
+
if (parts.length > 0) {
|
|
890
|
+
setHelpOpen(false);
|
|
891
|
+
void channel.listSkills().then((list) => {
|
|
892
|
+
if (list === undefined) {
|
|
893
|
+
channel.notify(t('skills-load-failed'), { color: 'error' });
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
const skill = list.find(s => s.name === parts[0]);
|
|
897
|
+
if (skill === undefined) {
|
|
898
|
+
channel.notify(t('skills-unknown', { name: parts[0] }), { color: 'error' });
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
if (skill.userInvocable)
|
|
902
|
+
setHistoryFill(`/${skill.name} `);
|
|
903
|
+
else
|
|
904
|
+
channel.notify(t('skills-not-invocable', { name: parts[0] }), { color: 'warning' });
|
|
905
|
+
});
|
|
906
|
+
return true;
|
|
907
|
+
}
|
|
751
908
|
setHelpOpen(false);
|
|
752
909
|
setSkillsList(null);
|
|
753
|
-
|
|
754
|
-
setSkillsPickerOpen(true);
|
|
910
|
+
dispatchOverlay({ type: 'open', overlay: { kind: 'skills', index: 0 } });
|
|
755
911
|
void channel.listSkills().then((list) => {
|
|
756
912
|
if (list === undefined) {
|
|
757
|
-
|
|
913
|
+
dispatchOverlay({ type: 'close-if', kind: 'skills' });
|
|
758
914
|
channel.notify(t('skills-load-failed'), { color: 'error' });
|
|
759
915
|
return;
|
|
760
916
|
}
|
|
761
917
|
setSkillsList(list);
|
|
762
918
|
});
|
|
763
919
|
return true;
|
|
920
|
+
}
|
|
764
921
|
case 'provider': {
|
|
765
922
|
// Interactive add-provider wizard (/provider): drives the shared
|
|
766
923
|
// question panel, persists profile + key via the channel's settings/
|
|
@@ -786,8 +943,10 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
786
943
|
}
|
|
787
944
|
case 'thinking':
|
|
788
945
|
setHelpOpen(false);
|
|
789
|
-
|
|
790
|
-
|
|
946
|
+
dispatchOverlay({
|
|
947
|
+
type: 'open',
|
|
948
|
+
overlay: { kind: 'thinking', focus: thinkingVisible ? 0 : 1 },
|
|
949
|
+
});
|
|
791
950
|
return true;
|
|
792
951
|
case 'tokens': {
|
|
793
952
|
const usage = t('tokens-usage', { in: formatTokens(channel.tokens.input), out: formatTokens(channel.tokens.output) });
|
|
@@ -815,10 +974,9 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
815
974
|
const subcommand = (separator < 0 ? trimmed : trimmed.slice(0, separator)).toLowerCase();
|
|
816
975
|
const input = separator < 0 ? '' : trimmed.slice(separator).trim();
|
|
817
976
|
if (subcommand === '') {
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
channel.pushLocal('/workspace', [t('workspace-command-usage', { commands: extensions })]);
|
|
977
|
+
// Bare `/workspace` opens the action menu (resume / rename / open
|
|
978
|
+
// plus any registered extensions) instead of a text usage line.
|
|
979
|
+
dispatchOverlay({ type: 'open', overlay: { kind: 'workspace-menu', index: 0 } });
|
|
822
980
|
}
|
|
823
981
|
else if (subcommand === 'resume') {
|
|
824
982
|
openWorkspaceResume();
|
|
@@ -996,25 +1154,87 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
996
1154
|
case 'logout':
|
|
997
1155
|
channel.notify(t('login-logout-hint'));
|
|
998
1156
|
return true;
|
|
999
|
-
case '
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
])
|
|
1012
|
-
|
|
1157
|
+
case 'permission': {
|
|
1158
|
+
// The command itself is registered by dsh-sandbox-policy (dsh-base
|
|
1159
|
+
// permission-presets row): bare `/permission` opens the preset
|
|
1160
|
+
// picker (read-only / workspace-write / danger-full-access) and
|
|
1161
|
+
// Enter dispatches `/permission <preset>` through the same
|
|
1162
|
+
// external-command path a hand-typed argument takes. `/permission
|
|
1163
|
+
// status` prints the policy explainer (absorbed from the removed
|
|
1164
|
+
// `/permissions` command); other arguments pass through verbatim.
|
|
1165
|
+
// When the row is not mounted the default external path (or the
|
|
1166
|
+
// model, when nothing is registered) wins.
|
|
1167
|
+
const mounted = channel.commandList.some(command => command.external && command.name === 'permission');
|
|
1168
|
+
const parts = rawInput.trim().split(/\s+/).filter(Boolean);
|
|
1169
|
+
if (mounted && parts[0] === 'status') {
|
|
1170
|
+
setHelpOpen(false);
|
|
1171
|
+
channel.pushLocal('/permission', [
|
|
1172
|
+
t('permission-current', { name: permissionPresetName(channel.mode.sandbox) }),
|
|
1173
|
+
t('permission-policy-hint'),
|
|
1174
|
+
t('permission-approval-hint'),
|
|
1175
|
+
t('permission-root-hint', { cwd: channel.cwd }),
|
|
1176
|
+
t('permission-path-hint'),
|
|
1177
|
+
]);
|
|
1178
|
+
return true;
|
|
1179
|
+
}
|
|
1180
|
+
if (mounted && parts.length === 0) {
|
|
1181
|
+
setHelpOpen(false);
|
|
1182
|
+
const index = PERMISSION_PRESET_IDS.indexOf(channel.mode.sandbox ?? '');
|
|
1183
|
+
dispatchOverlay({
|
|
1184
|
+
type: 'open',
|
|
1185
|
+
overlay: { kind: 'permission', index: index >= 0 ? index : 1 },
|
|
1186
|
+
});
|
|
1187
|
+
return true;
|
|
1188
|
+
}
|
|
1189
|
+
if (mounted) {
|
|
1190
|
+
setHelpOpen(false);
|
|
1191
|
+
void channel.runExternalCommand('permission', rawInput).then((text) => {
|
|
1192
|
+
if (text !== undefined && text !== '') {
|
|
1193
|
+
channel.notify(text);
|
|
1194
|
+
}
|
|
1195
|
+
else if (text === undefined) {
|
|
1196
|
+
channel.notify(t('command-not-found', { name: 'permission' }), { color: 'error' });
|
|
1197
|
+
}
|
|
1198
|
+
});
|
|
1199
|
+
return true;
|
|
1200
|
+
}
|
|
1201
|
+
return false;
|
|
1202
|
+
}
|
|
1203
|
+
case 'plan': {
|
|
1204
|
+
// Registered by dsh-plan-mode: bare `/plan` opens an on/off picker
|
|
1205
|
+
// marked with the current state instead of toggling blindly; Enter
|
|
1206
|
+
// dispatches `/plan` or `/plan off`. Arguments pass through verbatim
|
|
1207
|
+
// (`/plan off`), and an unmounted row falls back to the default
|
|
1208
|
+
// external path.
|
|
1209
|
+
const mounted = channel.commandList.some(command => command.external && command.name === 'plan');
|
|
1210
|
+
const parts = rawInput.trim().split(/\s+/).filter(Boolean);
|
|
1211
|
+
if (mounted && parts.length === 0) {
|
|
1212
|
+
setHelpOpen(false);
|
|
1213
|
+
dispatchOverlay({
|
|
1214
|
+
type: 'open',
|
|
1215
|
+
overlay: { kind: 'plan', index: channel.mode.plan === true ? 0 : 1 },
|
|
1216
|
+
});
|
|
1217
|
+
return true;
|
|
1218
|
+
}
|
|
1219
|
+
if (mounted) {
|
|
1220
|
+
setHelpOpen(false);
|
|
1221
|
+
void channel.runExternalCommand('plan', rawInput).then((text) => {
|
|
1222
|
+
if (text !== undefined && text !== '') {
|
|
1223
|
+
channel.notify(text);
|
|
1224
|
+
}
|
|
1225
|
+
else if (text === undefined) {
|
|
1226
|
+
channel.notify(t('command-not-found', { name: 'plan' }), { color: 'error' });
|
|
1227
|
+
}
|
|
1228
|
+
});
|
|
1229
|
+
return true;
|
|
1230
|
+
}
|
|
1231
|
+
return false;
|
|
1232
|
+
}
|
|
1013
1233
|
case 'add-dir':
|
|
1014
1234
|
setHelpOpen(false);
|
|
1015
1235
|
channel.pushLocal('/add-dir', [
|
|
1016
|
-
t('
|
|
1017
|
-
t('
|
|
1236
|
+
t('permission-root-hint', { cwd: channel.cwd }),
|
|
1237
|
+
t('permission-path-hint'),
|
|
1018
1238
|
]);
|
|
1019
1239
|
return true;
|
|
1020
1240
|
case 'hooks':
|
|
@@ -1088,7 +1308,7 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1088
1308
|
}
|
|
1089
1309
|
case 'tips':
|
|
1090
1310
|
setHelpOpen(false);
|
|
1091
|
-
|
|
1311
|
+
dispatchOverlay({ type: 'open', overlay: { kind: 'tips' } });
|
|
1092
1312
|
return true;
|
|
1093
1313
|
case 'connect':
|
|
1094
1314
|
setHelpOpen(false);
|
|
@@ -1142,6 +1362,9 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1142
1362
|
? channel.rows.filter(row => SELECTABLE_KINDS.has(row.kind))
|
|
1143
1363
|
: NO_ROWS;
|
|
1144
1364
|
// ctrl+r history search: substring match on the query, newest first.
|
|
1365
|
+
// The draft lives in the overlay variant; the derived '' while closed
|
|
1366
|
+
// keeps the memo inputs stable (nothing renders the matches then).
|
|
1367
|
+
const historyQuery = overlay.kind === 'history' ? overlay.query : '';
|
|
1145
1368
|
const historyMatches = React.useMemo(() => {
|
|
1146
1369
|
const q = historyQuery.trim().toLowerCase();
|
|
1147
1370
|
return q ? historyEntries.filter(e => e.text.toLowerCase().includes(q)) : historyEntries;
|
|
@@ -1150,15 +1373,15 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1150
1373
|
// selectable user turns; steering side-questions are excluded). Computed
|
|
1151
1374
|
// per render while the picker is open — `channel.rows` is a live in-place
|
|
1152
1375
|
// array (see selectableRows).
|
|
1153
|
-
const rewindRows =
|
|
1376
|
+
const rewindRows = overlay.kind === 'rewind'
|
|
1154
1377
|
? channel.rows
|
|
1155
1378
|
.filter(row => row.kind === 'user' && row.label === undefined)
|
|
1156
1379
|
.reverse()
|
|
1157
1380
|
: NO_ROWS;
|
|
1158
1381
|
/** Open the rewind picker (from PromptInput's double-Esc on an empty input). */
|
|
1159
1382
|
const openRewind = () => {
|
|
1160
|
-
//
|
|
1161
|
-
// directly instead of reading the gated list.
|
|
1383
|
+
// The overlay is not 'rewind' yet this render, so rewindRows is empty —
|
|
1384
|
+
// scan directly instead of reading the gated list.
|
|
1162
1385
|
const candidates = channel.rows
|
|
1163
1386
|
.filter(row => row.kind === 'user' && row.label === undefined)
|
|
1164
1387
|
.reverse();
|
|
@@ -1166,12 +1389,11 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1166
1389
|
channel.notify(t('rewind-none'));
|
|
1167
1390
|
return;
|
|
1168
1391
|
}
|
|
1169
|
-
setRewindIndex(0);
|
|
1170
|
-
setRewindConfirm(null);
|
|
1171
|
-
setRewindModes(null);
|
|
1172
|
-
setRewindBusy(false);
|
|
1173
1392
|
rewindRequestRef.current += 1;
|
|
1174
|
-
|
|
1393
|
+
dispatchOverlay({
|
|
1394
|
+
type: 'open',
|
|
1395
|
+
overlay: { kind: 'rewind', index: 0, confirm: null, modes: null, modeIndex: 0, busy: false },
|
|
1396
|
+
});
|
|
1175
1397
|
};
|
|
1176
1398
|
/**
|
|
1177
1399
|
* Enter on a rewind candidate: ask the plugins first (tui/rewind-prompt).
|
|
@@ -1180,16 +1402,15 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1180
1402
|
*/
|
|
1181
1403
|
const requestRewindConfirm = async (row) => {
|
|
1182
1404
|
const token = ++rewindRequestRef.current;
|
|
1183
|
-
|
|
1405
|
+
dispatchOverlay({ type: 'rewind-busy', busy: true });
|
|
1184
1406
|
const decision = await channel.promptRewind(row);
|
|
1185
1407
|
if (token !== rewindRequestRef.current)
|
|
1186
1408
|
return;
|
|
1187
|
-
|
|
1188
|
-
|
|
1409
|
+
if (decision === 'cancel') {
|
|
1410
|
+
dispatchOverlay({ type: 'rewind-busy', busy: false });
|
|
1189
1411
|
return;
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
setRewindModeIndex(0);
|
|
1412
|
+
}
|
|
1413
|
+
dispatchOverlay({ type: 'rewind-decision', confirm: row, modes: decision?.modes ?? null });
|
|
1193
1414
|
};
|
|
1194
1415
|
/** Execute the confirmed rewind; the message comes back into the input. */
|
|
1195
1416
|
const performRewind = async (row, mode = null) => {
|
|
@@ -1277,13 +1498,31 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1277
1498
|
}
|
|
1278
1499
|
setForceMountRowId(rowId);
|
|
1279
1500
|
};
|
|
1501
|
+
/**
|
|
1502
|
+
* Reveal-and-seek for a row folded behind the recent-rows window (the
|
|
1503
|
+
* rail's tick for an old turn, the doc's revealAndSeekRow): expand the
|
|
1504
|
+
* fold first, then the ordinary seek takes over — the completion effect
|
|
1505
|
+
* below force-mounts the row and scrollToElement lands it once its ref
|
|
1506
|
+
* (and Yoga top) exist. The fold toggle is idempotent, so calling this
|
|
1507
|
+
* for an already-revealed row is harmless.
|
|
1508
|
+
*/
|
|
1509
|
+
const revealAndSeekRow = (rowId) => {
|
|
1510
|
+
if (!showAllMessages)
|
|
1511
|
+
setShowAllMessages(true);
|
|
1512
|
+
seekRow(rowId);
|
|
1513
|
+
};
|
|
1280
1514
|
React.useLayoutEffect(() => {
|
|
1281
1515
|
if (forceMountRowId === null)
|
|
1282
1516
|
return;
|
|
1283
1517
|
const el = rowRefsRef.current.get(forceMountRowId);
|
|
1284
1518
|
if (el) {
|
|
1285
1519
|
handle?.scrollToElement(el);
|
|
1286
|
-
|
|
1520
|
+
// Clear deferred to a macrotask: clearing here would let React's
|
|
1521
|
+
// synchronous re-render narrow the virtualization window and unmount
|
|
1522
|
+
// the row BEFORE the renderer's deferred pass reads its Yoga top
|
|
1523
|
+
// (scrollAnchor processing runs in a microtask) — the seek would
|
|
1524
|
+
// silently no-op (detached anchor element).
|
|
1525
|
+
setTimeout(() => setForceMountRowId(null), 0);
|
|
1287
1526
|
}
|
|
1288
1527
|
});
|
|
1289
1528
|
// `/` transcript search: rows whose searchable text contains the query.
|
|
@@ -1300,7 +1539,7 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1300
1539
|
// Incsearch: highlight all matches (screen-space overlay) and keep the
|
|
1301
1540
|
// current match row in view as the query changes (CC semantics).
|
|
1302
1541
|
React.useEffect(() => {
|
|
1303
|
-
if (!
|
|
1542
|
+
if (!searchActive)
|
|
1304
1543
|
return;
|
|
1305
1544
|
setHighlight(searchQuery);
|
|
1306
1545
|
const count = searchMatches.length;
|
|
@@ -1312,10 +1551,10 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1312
1551
|
if (target) {
|
|
1313
1552
|
seekRow(target.row.id);
|
|
1314
1553
|
}
|
|
1315
|
-
}, [searchQuery,
|
|
1554
|
+
}, [searchQuery, searchActive]);
|
|
1316
1555
|
// n/N navigation: move the current match into view.
|
|
1317
1556
|
React.useEffect(() => {
|
|
1318
|
-
if (!
|
|
1557
|
+
if (!searchActive)
|
|
1319
1558
|
return;
|
|
1320
1559
|
const target = searchMatches[searchCurrent];
|
|
1321
1560
|
// oxlint-disable-next-line typescript/no-unnecessary-condition -- runtime guard: out-of-range index on an empty/filtered list
|
|
@@ -1352,6 +1591,16 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1352
1591
|
return next;
|
|
1353
1592
|
});
|
|
1354
1593
|
}, []);
|
|
1594
|
+
const toggleStreamFolded = React.useCallback((rowId) => {
|
|
1595
|
+
setStreamFoldedRows((previous) => {
|
|
1596
|
+
const next = new Set(previous);
|
|
1597
|
+
if (next.has(rowId))
|
|
1598
|
+
next.delete(rowId);
|
|
1599
|
+
else
|
|
1600
|
+
next.add(rowId);
|
|
1601
|
+
return next;
|
|
1602
|
+
});
|
|
1603
|
+
}, []);
|
|
1355
1604
|
const registerRowRef = React.useCallback((rowId, el) => {
|
|
1356
1605
|
if (el)
|
|
1357
1606
|
rowRefsRef.current.set(rowId, el);
|
|
@@ -1388,13 +1637,30 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1388
1637
|
// Mouse wheel scrolls the transcript even while a question/approval/
|
|
1389
1638
|
// dialog panel is open — those panels own arrow/Enter/Esc keys, but the
|
|
1390
1639
|
// transcript above them should still be scrollable in fullscreen mode.
|
|
1391
|
-
//
|
|
1392
|
-
//
|
|
1640
|
+
//
|
|
1641
|
+
// Wheel routing is position-first: events landing over a ScrollBox
|
|
1642
|
+
// (transcript, help, subagent panels…) are consumed by that box in
|
|
1643
|
+
// App's input batch (onWheelAt) and never reach this branch. What
|
|
1644
|
+
// arrives here is the fallback: wheel over non-scroll areas (prompt,
|
|
1645
|
+
// status bar) or over floating overlays.
|
|
1646
|
+
// - Help stays yielded: PromptInput's help ScrollBox handles the
|
|
1647
|
+
// remaining global wheel while help is open (both covered layers
|
|
1648
|
+
// must not move).
|
|
1649
|
+
// - Pickers/dialogs are modal: wheel that fell through over them
|
|
1650
|
+
// must NOT scroll the transcript behind (the audit's
|
|
1651
|
+
// pass-through gap), so yield like the keyboard guards above.
|
|
1393
1652
|
// Events only arrive with mouse tracking on; inline mode never sees
|
|
1394
1653
|
// them, so this is a no-op there.
|
|
1395
1654
|
if (key.wheelUp || key.wheelDown) {
|
|
1396
1655
|
if (helpOpen)
|
|
1397
1656
|
return;
|
|
1657
|
+
// Any open transient dialog is modal to the wheel; the one exception
|
|
1658
|
+
// mirrors the render gate — a workspace picker whose target list has
|
|
1659
|
+
// not landed paints nothing, so wheel-through keeps scrolling.
|
|
1660
|
+
const overlayModal = overlay.kind !== 'none' &&
|
|
1661
|
+
(overlay.kind !== 'workspace-picker' || workspaceTargets.length > 0);
|
|
1662
|
+
if (overlayModal)
|
|
1663
|
+
return;
|
|
1398
1664
|
handle?.scrollBy(key.wheelUp ? -3 : 3);
|
|
1399
1665
|
event.stopImmediatePropagation();
|
|
1400
1666
|
return;
|
|
@@ -1424,11 +1690,11 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1424
1690
|
event.stopImmediatePropagation();
|
|
1425
1691
|
return;
|
|
1426
1692
|
}
|
|
1427
|
-
if (
|
|
1693
|
+
if (overlay.kind === 'search') {
|
|
1428
1694
|
// Transcript search bar (less-style): edit the query, Enter commits
|
|
1429
1695
|
// (query persists for n/N), Esc/ctrl+c cancels back to the anchor.
|
|
1430
1696
|
if (key.escape || (key.ctrl && input === 'c')) {
|
|
1431
|
-
|
|
1697
|
+
dispatchOverlay({ type: 'close' });
|
|
1432
1698
|
setHighlight('');
|
|
1433
1699
|
handle?.scrollTo(searchAnchorRef.current);
|
|
1434
1700
|
}
|
|
@@ -1436,7 +1702,7 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1436
1702
|
// Enter commits; 0-match junk queries don't persist (CC behavior).
|
|
1437
1703
|
if (searchCount === 0)
|
|
1438
1704
|
setSearchQuery('');
|
|
1439
|
-
|
|
1705
|
+
dispatchOverlay({ type: 'close' });
|
|
1440
1706
|
}
|
|
1441
1707
|
else if (key.backspace) {
|
|
1442
1708
|
if (searchCursor > 0) {
|
|
@@ -1482,41 +1748,41 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1482
1748
|
event.stopImmediatePropagation();
|
|
1483
1749
|
return;
|
|
1484
1750
|
}
|
|
1485
|
-
if (
|
|
1751
|
+
if (overlay.kind === 'thinking') {
|
|
1486
1752
|
if (key.upArrow || key.downArrow) {
|
|
1487
|
-
|
|
1753
|
+
dispatchOverlay({ type: 'move', delta: key.upArrow ? -1 : 1, count: 2 });
|
|
1488
1754
|
}
|
|
1489
1755
|
else if (plainReturn) {
|
|
1490
|
-
const visible =
|
|
1756
|
+
const visible = overlay.focus === 0;
|
|
1491
1757
|
setThinkingVisible(visible);
|
|
1492
|
-
|
|
1758
|
+
dispatchOverlay({ type: 'close' });
|
|
1493
1759
|
channel.notify(t('thinking-toggled', { state: visible ? t('thinking-on') : t('thinking-off') }));
|
|
1494
1760
|
}
|
|
1495
1761
|
else if (key.escape) {
|
|
1496
|
-
|
|
1762
|
+
dispatchOverlay({ type: 'close' });
|
|
1497
1763
|
}
|
|
1498
1764
|
return;
|
|
1499
1765
|
}
|
|
1500
|
-
if (
|
|
1766
|
+
if (overlay.kind === 'workspace-flow') {
|
|
1767
|
+
const { flow, busy, input: flowInput } = overlay;
|
|
1501
1768
|
if (key.escape) {
|
|
1502
|
-
if (
|
|
1503
|
-
|
|
1769
|
+
if (flowInput !== null && !busy) {
|
|
1770
|
+
dispatchOverlay({ type: 'flow-input', input: null });
|
|
1504
1771
|
return;
|
|
1505
1772
|
}
|
|
1506
1773
|
workspaceFlowAbortRef.current?.abort();
|
|
1507
1774
|
workspaceFlowAbortRef.current = null;
|
|
1508
1775
|
workspaceFlowRequestRef.current += 1;
|
|
1509
|
-
|
|
1510
|
-
setWorkspaceFlow(null);
|
|
1776
|
+
dispatchOverlay({ type: 'close' });
|
|
1511
1777
|
return;
|
|
1512
1778
|
}
|
|
1513
|
-
if (
|
|
1779
|
+
if (busy)
|
|
1514
1780
|
return;
|
|
1515
|
-
if (
|
|
1516
|
-
const choice =
|
|
1781
|
+
if (flowInput !== null) {
|
|
1782
|
+
const choice = flow.choices.find(candidate => candidate.id === flowInput.choiceId);
|
|
1517
1783
|
const editor = choice?.input;
|
|
1518
1784
|
if (plainReturn) {
|
|
1519
|
-
const value =
|
|
1785
|
+
const value = flowInput.value.trim();
|
|
1520
1786
|
if (value.length === 0) {
|
|
1521
1787
|
channel.notify(t('workspace-flow-input-empty'), { color: 'warning' });
|
|
1522
1788
|
}
|
|
@@ -1524,99 +1790,108 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1524
1790
|
runWorkspaceFlowAction(signal => editor.submit(value, signal));
|
|
1525
1791
|
}
|
|
1526
1792
|
}
|
|
1527
|
-
else if (key.backspace &&
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
value:
|
|
1531
|
-
cursor:
|
|
1793
|
+
else if (key.backspace && flowInput.cursor > 0) {
|
|
1794
|
+
dispatchOverlay({
|
|
1795
|
+
type: 'flow-input-edit',
|
|
1796
|
+
value: flowInput.value.slice(0, flowInput.cursor - 1) + flowInput.value.slice(flowInput.cursor),
|
|
1797
|
+
cursor: flowInput.cursor - 1,
|
|
1532
1798
|
});
|
|
1533
1799
|
}
|
|
1534
|
-
else if (key.delete &&
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
value:
|
|
1800
|
+
else if (key.delete && flowInput.cursor < flowInput.value.length) {
|
|
1801
|
+
dispatchOverlay({
|
|
1802
|
+
type: 'flow-input-edit',
|
|
1803
|
+
value: flowInput.value.slice(0, flowInput.cursor) + flowInput.value.slice(flowInput.cursor + 1),
|
|
1804
|
+
cursor: flowInput.cursor,
|
|
1538
1805
|
});
|
|
1539
1806
|
}
|
|
1540
1807
|
else if (key.leftArrow) {
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1808
|
+
dispatchOverlay({
|
|
1809
|
+
type: 'flow-input-edit',
|
|
1810
|
+
value: flowInput.value,
|
|
1811
|
+
cursor: Math.max(0, flowInput.cursor - 1),
|
|
1544
1812
|
});
|
|
1545
1813
|
}
|
|
1546
1814
|
else if (key.rightArrow) {
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1815
|
+
dispatchOverlay({
|
|
1816
|
+
type: 'flow-input-edit',
|
|
1817
|
+
value: flowInput.value,
|
|
1818
|
+
cursor: Math.min(flowInput.value.length, flowInput.cursor + 1),
|
|
1550
1819
|
});
|
|
1551
1820
|
}
|
|
1552
1821
|
else if (input.length > 0 && !key.ctrl && !key.meta && !key.super && !key.tab) {
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
value:
|
|
1556
|
-
cursor:
|
|
1822
|
+
dispatchOverlay({
|
|
1823
|
+
type: 'flow-input-edit',
|
|
1824
|
+
value: flowInput.value.slice(0, flowInput.cursor) + input + flowInput.value.slice(flowInput.cursor),
|
|
1825
|
+
cursor: flowInput.cursor + input.length,
|
|
1557
1826
|
});
|
|
1558
1827
|
}
|
|
1559
1828
|
return;
|
|
1560
1829
|
}
|
|
1561
|
-
if (key.upArrow) {
|
|
1562
|
-
|
|
1563
|
-
}
|
|
1564
|
-
else if (key.downArrow) {
|
|
1565
|
-
setWorkspaceFlowIndex(index => (index >= workspaceFlow.choices.length - 1 ? 0 : index + 1));
|
|
1830
|
+
if (key.upArrow || key.downArrow) {
|
|
1831
|
+
dispatchOverlay({ type: 'move', delta: key.upArrow ? -1 : 1, count: flow.choices.length });
|
|
1566
1832
|
}
|
|
1567
1833
|
else if (key.tab && !key.shift) {
|
|
1568
|
-
const choice =
|
|
1834
|
+
const choice = flow.choices[overlay.index];
|
|
1569
1835
|
if (choice?.input !== undefined) {
|
|
1570
1836
|
const value = choice.input.initialValue ?? '';
|
|
1571
|
-
|
|
1837
|
+
const flowInputNext = {
|
|
1572
1838
|
choiceId: choice.id,
|
|
1573
1839
|
value,
|
|
1574
1840
|
cursor: value.length,
|
|
1575
1841
|
...(choice.input.placeholder === undefined ? {} : { placeholder: choice.input.placeholder }),
|
|
1576
|
-
}
|
|
1842
|
+
};
|
|
1843
|
+
dispatchOverlay({ type: 'flow-input', input: flowInputNext });
|
|
1577
1844
|
}
|
|
1578
1845
|
}
|
|
1579
1846
|
else if (plainReturn) {
|
|
1580
|
-
const choice =
|
|
1847
|
+
const choice = flow.choices[overlay.index];
|
|
1581
1848
|
if (choice !== undefined) {
|
|
1582
1849
|
runWorkspaceFlowAction(signal => choice.choose(signal));
|
|
1583
1850
|
}
|
|
1584
1851
|
}
|
|
1585
1852
|
return;
|
|
1586
1853
|
}
|
|
1587
|
-
if (
|
|
1588
|
-
if (key.upArrow) {
|
|
1589
|
-
|
|
1590
|
-
}
|
|
1591
|
-
else if (key.downArrow) {
|
|
1592
|
-
setWorkspaceIndex(index => (index >= workspaceTargets.length - 1 ? 0 : index + 1));
|
|
1854
|
+
if (overlay.kind === 'workspace-picker') {
|
|
1855
|
+
if (key.upArrow || key.downArrow) {
|
|
1856
|
+
dispatchOverlay({ type: 'move', delta: key.upArrow ? -1 : 1, count: workspaceTargets.length });
|
|
1593
1857
|
}
|
|
1594
1858
|
else if (plainReturn) {
|
|
1595
|
-
const target = workspaceTargets[
|
|
1596
|
-
|
|
1859
|
+
const target = workspaceTargets[overlay.index];
|
|
1860
|
+
dispatchOverlay({ type: 'close' });
|
|
1597
1861
|
if (target !== undefined)
|
|
1598
1862
|
void channel.switchWorkspace(target);
|
|
1599
1863
|
}
|
|
1600
1864
|
else if (key.escape) {
|
|
1601
|
-
|
|
1865
|
+
dispatchOverlay({ type: 'close' });
|
|
1602
1866
|
}
|
|
1603
1867
|
return;
|
|
1604
1868
|
}
|
|
1605
|
-
if (
|
|
1606
|
-
|
|
1607
|
-
|
|
1869
|
+
if (overlay.kind === 'workspace-menu') {
|
|
1870
|
+
const menu = workspaceMenuOptions;
|
|
1871
|
+
if (key.upArrow || key.downArrow) {
|
|
1872
|
+
dispatchOverlay({ type: 'move', delta: key.upArrow ? -1 : 1, count: menu.length });
|
|
1608
1873
|
}
|
|
1609
|
-
else if (
|
|
1610
|
-
|
|
1874
|
+
else if (plainReturn) {
|
|
1875
|
+
const option = menu[overlay.index];
|
|
1876
|
+
runWorkspaceMenuOption(option);
|
|
1877
|
+
}
|
|
1878
|
+
else if (key.escape) {
|
|
1879
|
+
dispatchOverlay({ type: 'close' });
|
|
1880
|
+
}
|
|
1881
|
+
return;
|
|
1882
|
+
}
|
|
1883
|
+
if (overlay.kind === 'model') {
|
|
1884
|
+
if (key.upArrow || key.downArrow) {
|
|
1885
|
+
dispatchOverlay({ type: 'move', delta: key.upArrow ? -1 : 1, count: models.length });
|
|
1611
1886
|
}
|
|
1612
1887
|
else if (plainReturn) {
|
|
1613
|
-
const model = models[
|
|
1888
|
+
const model = models[overlay.index];
|
|
1614
1889
|
// oxlint-disable-next-line typescript/no-unnecessary-condition -- runtime guard: out-of-range index on an empty list
|
|
1615
1890
|
if (model) {
|
|
1616
1891
|
// Enter switches the live model right away: the conversation is
|
|
1617
1892
|
// forked at its end and continued with an agent routed to the new
|
|
1618
1893
|
// model (history replays unchanged).
|
|
1619
|
-
|
|
1894
|
+
dispatchOverlay({ type: 'close' });
|
|
1620
1895
|
channel.notify(t('model-switching', { name: model.name }));
|
|
1621
1896
|
void channel.switchModel(model.provider, model.id).then((ok) => {
|
|
1622
1897
|
if (ok)
|
|
@@ -1624,27 +1899,23 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1624
1899
|
});
|
|
1625
1900
|
}
|
|
1626
1901
|
else {
|
|
1627
|
-
|
|
1902
|
+
dispatchOverlay({ type: 'close' });
|
|
1628
1903
|
}
|
|
1629
1904
|
}
|
|
1630
1905
|
else if (key.escape) {
|
|
1631
|
-
|
|
1906
|
+
dispatchOverlay({ type: 'close' });
|
|
1632
1907
|
}
|
|
1633
1908
|
return;
|
|
1634
1909
|
}
|
|
1635
|
-
if (
|
|
1910
|
+
if (overlay.kind === 'skills') {
|
|
1636
1911
|
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));
|
|
1912
|
+
if (key.upArrow || key.downArrow) {
|
|
1913
|
+
// count 0 (snapshot still loading) is a no-op inside the reducer.
|
|
1914
|
+
dispatchOverlay({ type: 'move', delta: key.upArrow ? -1 : 1, count: list.length });
|
|
1644
1915
|
}
|
|
1645
1916
|
else if (plainReturn) {
|
|
1646
|
-
const skill = list[
|
|
1647
|
-
|
|
1917
|
+
const skill = list[overlay.index];
|
|
1918
|
+
dispatchOverlay({ type: 'close' });
|
|
1648
1919
|
// 可直调技能 Enter 填入 `/name `——与 / 菜单选中技能同一条
|
|
1649
1920
|
// completion-only 分发路径;模型专用技能(userInvocable=false)只关闭。
|
|
1650
1921
|
// oxlint-disable-next-line typescript/no-unnecessary-condition -- runtime guard: out-of-range index on an empty list
|
|
@@ -1652,213 +1923,252 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1652
1923
|
setHistoryFill(`/${skill.name} `);
|
|
1653
1924
|
}
|
|
1654
1925
|
else if (key.escape) {
|
|
1655
|
-
|
|
1926
|
+
dispatchOverlay({ type: 'close' });
|
|
1656
1927
|
}
|
|
1657
1928
|
return;
|
|
1658
1929
|
}
|
|
1659
|
-
if (
|
|
1660
|
-
if (key.upArrow) {
|
|
1661
|
-
|
|
1662
|
-
}
|
|
1663
|
-
else if (key.downArrow) {
|
|
1664
|
-
setActivityIndex(index => (index >= PRESET_NAMES.length - 1 ? 0 : index + 1));
|
|
1930
|
+
if (overlay.kind === 'activity') {
|
|
1931
|
+
if (key.upArrow || key.downArrow) {
|
|
1932
|
+
dispatchOverlay({ type: 'move', delta: key.upArrow ? -1 : 1, count: PRESET_NAMES.length });
|
|
1665
1933
|
}
|
|
1666
1934
|
else if (plainReturn) {
|
|
1667
|
-
const name = PRESET_NAMES[
|
|
1668
|
-
|
|
1935
|
+
const name = PRESET_NAMES[overlay.index];
|
|
1936
|
+
dispatchOverlay({ type: 'close' });
|
|
1669
1937
|
if (name)
|
|
1670
1938
|
channel.setActivityFrames(name);
|
|
1671
1939
|
}
|
|
1672
1940
|
else if (key.escape) {
|
|
1673
|
-
|
|
1941
|
+
dispatchOverlay({ type: 'close' });
|
|
1674
1942
|
}
|
|
1675
1943
|
return;
|
|
1676
1944
|
}
|
|
1677
|
-
if (
|
|
1945
|
+
if (overlay.kind === 'effort') {
|
|
1678
1946
|
if (key.leftArrow || key.rightArrow) {
|
|
1679
1947
|
const delta = key.leftArrow ? -1 : 1;
|
|
1680
|
-
|
|
1681
|
-
|
|
1948
|
+
// The same wrap rule the reducer applies — computed here too so the
|
|
1949
|
+
// newly focused level is applied in this very keystroke.
|
|
1950
|
+
const next = wrapIndex(overlay.index, delta, effortOptions.length);
|
|
1951
|
+
dispatchOverlay({ type: 'move', delta, count: effortOptions.length });
|
|
1682
1952
|
const option = effortOptions[next];
|
|
1683
1953
|
// Live-apply: the slider IS the control; Esc does not revert.
|
|
1684
1954
|
if (option)
|
|
1685
1955
|
void channel.setEffort(option.id);
|
|
1686
1956
|
}
|
|
1687
1957
|
else if (plainReturn || key.escape) {
|
|
1688
|
-
|
|
1958
|
+
dispatchOverlay({ type: 'close' });
|
|
1689
1959
|
}
|
|
1690
1960
|
return;
|
|
1691
1961
|
}
|
|
1692
|
-
if (
|
|
1693
|
-
if (key.upArrow) {
|
|
1694
|
-
|
|
1695
|
-
}
|
|
1696
|
-
else if (key.downArrow) {
|
|
1697
|
-
setPresetIndex(index => (index >= presetOptions.length - 1 ? 0 : index + 1));
|
|
1962
|
+
if (overlay.kind === 'preset') {
|
|
1963
|
+
if (key.upArrow || key.downArrow) {
|
|
1964
|
+
dispatchOverlay({ type: 'move', delta: key.upArrow ? -1 : 1, count: presetOptions.length });
|
|
1698
1965
|
}
|
|
1699
1966
|
else if (plainReturn) {
|
|
1700
|
-
const option = presetOptions[
|
|
1701
|
-
|
|
1967
|
+
const option = presetOptions[overlay.index];
|
|
1968
|
+
dispatchOverlay({ type: 'close' });
|
|
1702
1969
|
if (option)
|
|
1703
1970
|
void channel.switchPreset(option.id);
|
|
1704
1971
|
}
|
|
1705
1972
|
else if (key.escape) {
|
|
1706
|
-
|
|
1973
|
+
dispatchOverlay({ type: 'close' });
|
|
1707
1974
|
}
|
|
1708
1975
|
return;
|
|
1709
1976
|
}
|
|
1710
|
-
if (
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
setThemeIndex(index => (index <= 0 ? options.length - 1 : index - 1));
|
|
1977
|
+
if (overlay.kind === 'permission') {
|
|
1978
|
+
if (key.upArrow || key.downArrow) {
|
|
1979
|
+
dispatchOverlay({ type: 'move', delta: key.upArrow ? -1 : 1, count: PERMISSION_PRESET_IDS.length });
|
|
1714
1980
|
}
|
|
1715
|
-
else if (
|
|
1716
|
-
|
|
1981
|
+
else if (plainReturn) {
|
|
1982
|
+
const id = PERMISSION_PRESET_IDS[overlay.index];
|
|
1983
|
+
dispatchOverlay({ type: 'close' });
|
|
1984
|
+
if (id !== undefined) {
|
|
1985
|
+
void channel.runExternalCommand('permission', ` ${id}`).then((text) => {
|
|
1986
|
+
if (text !== undefined && text !== '')
|
|
1987
|
+
channel.notify(text);
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
else if (key.escape) {
|
|
1992
|
+
dispatchOverlay({ type: 'close' });
|
|
1993
|
+
}
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
if (overlay.kind === 'plan') {
|
|
1997
|
+
if (key.upArrow || key.downArrow) {
|
|
1998
|
+
dispatchOverlay({ type: 'move', delta: key.upArrow ? -1 : 1, count: 2 });
|
|
1717
1999
|
}
|
|
1718
2000
|
else if (plainReturn) {
|
|
1719
|
-
|
|
1720
|
-
|
|
2001
|
+
const on = overlay.index === 0;
|
|
2002
|
+
dispatchOverlay({ type: 'close' });
|
|
2003
|
+
void channel.runExternalCommand('plan', on ? '' : ' off').then((text) => {
|
|
2004
|
+
if (text !== undefined && text !== '')
|
|
2005
|
+
channel.notify(text);
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
2008
|
+
else if (key.escape) {
|
|
2009
|
+
dispatchOverlay({ type: 'close' });
|
|
2010
|
+
}
|
|
2011
|
+
return;
|
|
2012
|
+
}
|
|
2013
|
+
if (overlay.kind === 'lang') {
|
|
2014
|
+
if (key.upArrow || key.downArrow) {
|
|
2015
|
+
dispatchOverlay({ type: 'move', delta: key.upArrow ? -1 : 1, count: 2 });
|
|
2016
|
+
}
|
|
2017
|
+
else if (plainReturn) {
|
|
2018
|
+
const lang = LANGS[overlay.index];
|
|
2019
|
+
dispatchOverlay({ type: 'close' });
|
|
2020
|
+
if (lang !== undefined)
|
|
2021
|
+
applyLang(lang);
|
|
2022
|
+
}
|
|
2023
|
+
else if (key.escape) {
|
|
2024
|
+
dispatchOverlay({ type: 'close' });
|
|
2025
|
+
}
|
|
2026
|
+
return;
|
|
2027
|
+
}
|
|
2028
|
+
if (overlay.kind === 'theme') {
|
|
2029
|
+
const options = getThemeOptions();
|
|
2030
|
+
if (key.upArrow || key.downArrow) {
|
|
2031
|
+
dispatchOverlay({ type: 'move', delta: key.upArrow ? -1 : 1, count: options.length });
|
|
2032
|
+
}
|
|
2033
|
+
else if (plainReturn) {
|
|
2034
|
+
dispatchOverlay({ type: 'close' });
|
|
2035
|
+
const name = options[overlay.index]?.value;
|
|
1721
2036
|
if (name !== undefined) {
|
|
1722
2037
|
const ok = setTheme(name);
|
|
1723
2038
|
channel.notify(ok ? t('theme-switched-saved', { name }) : t('theme-switch-failed', { name }), { color: ok ? 'success' : 'error' });
|
|
1724
2039
|
}
|
|
1725
2040
|
}
|
|
1726
2041
|
else if (key.escape) {
|
|
1727
|
-
|
|
2042
|
+
dispatchOverlay({ type: 'close' });
|
|
1728
2043
|
}
|
|
1729
2044
|
return;
|
|
1730
2045
|
}
|
|
1731
|
-
if (
|
|
2046
|
+
if (overlay.kind === 'history') {
|
|
2047
|
+
const { query, cursor, focus } = overlay;
|
|
1732
2048
|
if (key.escape) {
|
|
1733
|
-
|
|
2049
|
+
dispatchOverlay({ type: 'close' });
|
|
1734
2050
|
}
|
|
1735
2051
|
else if (key.ctrl && (input === 'c' || input === 'd')) {
|
|
1736
2052
|
// CC's history search cancels on ctrl+c/ctrl+d too.
|
|
1737
|
-
|
|
2053
|
+
dispatchOverlay({ type: 'close' });
|
|
1738
2054
|
}
|
|
1739
2055
|
else if (plainReturn) {
|
|
1740
|
-
const entry = historyMatches[
|
|
2056
|
+
const entry = historyMatches[focus];
|
|
1741
2057
|
// oxlint-disable-next-line typescript/no-unnecessary-condition -- runtime guard: out-of-range index on an empty match list
|
|
1742
2058
|
if (entry) {
|
|
1743
2059
|
setHistoryFill(entry.text);
|
|
1744
|
-
|
|
2060
|
+
dispatchOverlay({ type: 'close' });
|
|
1745
2061
|
}
|
|
1746
2062
|
}
|
|
1747
2063
|
else if (key.upArrow) {
|
|
1748
|
-
|
|
2064
|
+
if (historyMatches.length > 0) {
|
|
2065
|
+
dispatchOverlay({ type: 'move', delta: -1, count: historyMatches.length });
|
|
2066
|
+
}
|
|
1749
2067
|
}
|
|
1750
2068
|
else if (key.downArrow || (isMod(key) && input === 'r')) {
|
|
1751
2069
|
// CC's historySearch:next — ↓ and repeat ctrl+r walk to the next match.
|
|
1752
|
-
|
|
2070
|
+
if (historyMatches.length > 0) {
|
|
2071
|
+
dispatchOverlay({ type: 'move', delta: 1, count: historyMatches.length });
|
|
2072
|
+
}
|
|
1753
2073
|
}
|
|
1754
2074
|
else if (key.backspace) {
|
|
1755
|
-
if (
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
2075
|
+
if (cursor > 0) {
|
|
2076
|
+
dispatchOverlay({
|
|
2077
|
+
type: 'history-edit',
|
|
2078
|
+
query: query.slice(0, cursor - 1) + query.slice(cursor),
|
|
2079
|
+
cursor: cursor - 1,
|
|
2080
|
+
focus: 0,
|
|
2081
|
+
});
|
|
1760
2082
|
}
|
|
1761
2083
|
}
|
|
1762
2084
|
else if (key.delete) {
|
|
1763
|
-
if (
|
|
1764
|
-
|
|
1765
|
-
|
|
2085
|
+
if (cursor < query.length) {
|
|
2086
|
+
dispatchOverlay({
|
|
2087
|
+
type: 'history-edit',
|
|
2088
|
+
query: query.slice(0, cursor) + query.slice(cursor + 1),
|
|
2089
|
+
focus: 0,
|
|
2090
|
+
});
|
|
1766
2091
|
}
|
|
1767
2092
|
}
|
|
1768
2093
|
else if (key.leftArrow) {
|
|
1769
2094
|
// Step by code point, not UTF-16 unit: an emoji is two units, and
|
|
1770
2095
|
// a mid-pair caret offset would split it in the SearchBox render.
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
return cursor - ch.length;
|
|
1776
|
-
});
|
|
2096
|
+
if (cursor > 0) {
|
|
2097
|
+
const ch = [...query.slice(0, cursor)].pop();
|
|
2098
|
+
dispatchOverlay({ type: 'history-edit', cursor: cursor - ch.length });
|
|
2099
|
+
}
|
|
1777
2100
|
}
|
|
1778
2101
|
else if (key.rightArrow) {
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
return cursor + ch.length;
|
|
1784
|
-
});
|
|
2102
|
+
if (cursor < query.length) {
|
|
2103
|
+
const ch = [...query.slice(cursor)][0];
|
|
2104
|
+
dispatchOverlay({ type: 'history-edit', cursor: cursor + ch.length });
|
|
2105
|
+
}
|
|
1785
2106
|
}
|
|
1786
2107
|
else if (key.home) {
|
|
1787
|
-
|
|
2108
|
+
dispatchOverlay({ type: 'history-edit', cursor: 0 });
|
|
1788
2109
|
}
|
|
1789
2110
|
else if (key.end) {
|
|
1790
|
-
|
|
2111
|
+
dispatchOverlay({ type: 'history-edit', cursor: query.length });
|
|
1791
2112
|
}
|
|
1792
2113
|
else if (!key.ctrl && !key.meta && !key.super && input) {
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
2114
|
+
dispatchOverlay({
|
|
2115
|
+
type: 'history-edit',
|
|
2116
|
+
query: query.slice(0, cursor) + input + query.slice(cursor),
|
|
2117
|
+
cursor: cursor + input.length,
|
|
2118
|
+
focus: 0,
|
|
2119
|
+
});
|
|
1797
2120
|
}
|
|
1798
2121
|
return;
|
|
1799
2122
|
}
|
|
1800
|
-
if (
|
|
2123
|
+
if (overlay.kind === 'rewind') {
|
|
1801
2124
|
// While the plugin decision is in flight the picker is read-only;
|
|
1802
2125
|
// Esc abandons the wait (the stale answer is dropped by the token).
|
|
1803
|
-
if (
|
|
2126
|
+
if (overlay.busy) {
|
|
1804
2127
|
if (key.escape) {
|
|
1805
2128
|
rewindRequestRef.current += 1;
|
|
1806
|
-
|
|
2129
|
+
dispatchOverlay({ type: 'rewind-busy', busy: false });
|
|
1807
2130
|
}
|
|
1808
2131
|
return;
|
|
1809
2132
|
}
|
|
1810
|
-
if (
|
|
1811
|
-
|
|
2133
|
+
if (overlay.confirm !== null) {
|
|
2134
|
+
const row = overlay.confirm;
|
|
2135
|
+
if (overlay.modes !== null) {
|
|
1812
2136
|
// Plugin offered modes: the confirm pane is a choice list —
|
|
1813
2137
|
// 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));
|
|
2138
|
+
const optionCount = overlay.modes.length + 1;
|
|
2139
|
+
if (key.upArrow || key.downArrow) {
|
|
2140
|
+
dispatchOverlay({ type: 'move', delta: key.upArrow ? -1 : 1, count: optionCount });
|
|
1820
2141
|
}
|
|
1821
2142
|
else if (plainReturn) {
|
|
1822
|
-
const
|
|
1823
|
-
|
|
1824
|
-
setRewindOpen(false);
|
|
1825
|
-
setRewindConfirm(null);
|
|
1826
|
-
setRewindModes(null);
|
|
2143
|
+
const mode = overlay.modeIndex === 0 ? null : (overlay.modes[overlay.modeIndex - 1]?.id ?? null);
|
|
2144
|
+
dispatchOverlay({ type: 'close' });
|
|
1827
2145
|
void performRewind(row, mode);
|
|
1828
2146
|
}
|
|
1829
2147
|
else if (key.escape) {
|
|
1830
|
-
|
|
1831
|
-
setRewindModes(null);
|
|
2148
|
+
dispatchOverlay({ type: 'rewind-back' });
|
|
1832
2149
|
}
|
|
1833
2150
|
return;
|
|
1834
2151
|
}
|
|
1835
2152
|
// Confirmation state: Enter rewinds, Esc backs out to the list.
|
|
1836
2153
|
if (plainReturn) {
|
|
1837
|
-
|
|
1838
|
-
setRewindOpen(false);
|
|
1839
|
-
setRewindConfirm(null);
|
|
2154
|
+
dispatchOverlay({ type: 'close' });
|
|
1840
2155
|
void performRewind(row);
|
|
1841
2156
|
}
|
|
1842
2157
|
else if (key.escape) {
|
|
1843
|
-
|
|
2158
|
+
dispatchOverlay({ type: 'rewind-back' });
|
|
1844
2159
|
}
|
|
1845
2160
|
}
|
|
1846
|
-
else if (key.upArrow) {
|
|
1847
|
-
|
|
1848
|
-
}
|
|
1849
|
-
else if (key.downArrow) {
|
|
1850
|
-
setRewindIndex(index => (index >= rewindRows.length - 1 ? 0 : index + 1));
|
|
2161
|
+
else if (key.upArrow || key.downArrow) {
|
|
2162
|
+
dispatchOverlay({ type: 'move', delta: key.upArrow ? -1 : 1, count: rewindRows.length });
|
|
1851
2163
|
}
|
|
1852
2164
|
else if (plainReturn) {
|
|
1853
|
-
const row = rewindRows[
|
|
2165
|
+
const row = rewindRows[overlay.index];
|
|
1854
2166
|
// oxlint-disable-next-line typescript/no-unnecessary-condition -- runtime guard: out-of-range index on an empty list
|
|
1855
2167
|
if (row)
|
|
1856
2168
|
void requestRewindConfirm(row);
|
|
1857
2169
|
}
|
|
1858
2170
|
else if (key.escape) {
|
|
1859
|
-
|
|
1860
|
-
setRewindConfirm(null);
|
|
1861
|
-
setRewindModes(null);
|
|
2171
|
+
dispatchOverlay({ type: 'close' });
|
|
1862
2172
|
}
|
|
1863
2173
|
return;
|
|
1864
2174
|
}
|
|
@@ -1880,11 +2190,11 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1880
2190
|
return;
|
|
1881
2191
|
}
|
|
1882
2192
|
if (isMod(key) && input === 'r' && !helpOpen) {
|
|
1883
|
-
setHistoryQuery('');
|
|
1884
|
-
setHistoryCursor(0);
|
|
1885
|
-
setHistoryFocus(0);
|
|
1886
2193
|
setHistoryEntries(loadHistory());
|
|
1887
|
-
|
|
2194
|
+
dispatchOverlay({
|
|
2195
|
+
type: 'open',
|
|
2196
|
+
overlay: { kind: 'history', query: '', cursor: 0, focus: 0 },
|
|
2197
|
+
});
|
|
1888
2198
|
return;
|
|
1889
2199
|
}
|
|
1890
2200
|
if (key.shift && key.upArrow && !selectionActive && !helpOpen) {
|
|
@@ -1947,7 +2257,7 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1947
2257
|
setSearchCursor(0);
|
|
1948
2258
|
setSearchCurrent(0);
|
|
1949
2259
|
setSearchCount(0);
|
|
1950
|
-
|
|
2260
|
+
dispatchOverlay({ type: 'open', overlay: { kind: 'search' } });
|
|
1951
2261
|
event.stopImmediatePropagation();
|
|
1952
2262
|
}
|
|
1953
2263
|
}
|
|
@@ -1977,9 +2287,26 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
1977
2287
|
else if (isMod(key) && input === 'e') {
|
|
1978
2288
|
setShowAllMessages(previous => !previous);
|
|
1979
2289
|
}
|
|
1980
|
-
else if (
|
|
2290
|
+
else if (isMod(key) && input === 'q') {
|
|
2291
|
+
// Fold/unfold the GoalTodoPanel todo section — works mid-turn too:
|
|
2292
|
+
// the collapsed line keeps the done/total count and the live task
|
|
2293
|
+
// preview, so long todo lists stop crowding the prompt.
|
|
2294
|
+
setTodoCollapsed(previous => !previous);
|
|
2295
|
+
}
|
|
2296
|
+
else if (plainReturn && !isSticky) {
|
|
2297
|
+
// Enter while scrolled up returns to the bottom (CC's pill: the
|
|
2298
|
+
// affordance now exists whenever the view is off the bottom, not
|
|
2299
|
+
// only with unseen rows).
|
|
1981
2300
|
handle?.scrollToBottom();
|
|
1982
2301
|
}
|
|
2302
|
+
else if (key.end && !isSticky) {
|
|
2303
|
+
// End = jump to bottom, less/vim semantics. Global on the chat
|
|
2304
|
+
// screen (search/history overlays consume their own End first —
|
|
2305
|
+
// cursor-to-line-end there). At the bottom already: no-op, so the
|
|
2306
|
+
// key stays harmless in muscle memory.
|
|
2307
|
+
handle?.scrollToBottom();
|
|
2308
|
+
event.stopImmediatePropagation();
|
|
2309
|
+
}
|
|
1983
2310
|
else if (extensionShortcuts !== undefined && extensionShortcuts.dispatch(input, key)) {
|
|
1984
2311
|
// Plugin shortcut (tuiShortcuts seam): matched only after every
|
|
1985
2312
|
// built-in global binding above declined — locals always win, and the
|
|
@@ -2061,10 +2388,10 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
2061
2388
|
}, onClose: () => setSubagentDashboardOpen(false) }));
|
|
2062
2389
|
return fullscreen ? dashboard : _jsx(AlternateScreen, { children: dashboard });
|
|
2063
2390
|
}
|
|
2064
|
-
/** Prompt input is inert while a modal dialog owns the keyboard.
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2391
|
+
/** Prompt input is inert while a modal dialog owns the keyboard. The
|
|
2392
|
+
* overlay union covers every picker/dialog and /tips in one check;
|
|
2393
|
+
* message-selection mode and the /btw panel live outside it. */
|
|
2394
|
+
const promptSelectionActive = selectionActive || overlay.kind !== 'none' || btw !== null;
|
|
2068
2395
|
// The trajectory scene replaces the conversation for as long as it is open.
|
|
2069
2396
|
// Rendering it INSTEAD of (not above) the transcript is what makes it a
|
|
2070
2397
|
// screen rather than an overlay: it owns the full viewport, and the
|
|
@@ -2077,24 +2404,60 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
2077
2404
|
const scene = _jsx(TrajectoryScene, { channel: channel, build: trajectory, onClose: closeScene });
|
|
2078
2405
|
return fullscreen ? scene : _jsx(AlternateScreen, { children: scene });
|
|
2079
2406
|
}
|
|
2080
|
-
//
|
|
2081
|
-
//
|
|
2082
|
-
//
|
|
2083
|
-
//
|
|
2084
|
-
// blit-skip
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2407
|
+
// 浮层整体挂载条件:与内部各面板的可见条件同值(数据门在
|
|
2408
|
+
// dialogOverlayVisible 里逐面板镜像)。关闭时把整个 absolute 浮层从树里
|
|
2409
|
+
// 移除——渲染器的"移除 absolute 节点"检测只看被移除子树自身的
|
|
2410
|
+
// style.position(dom.ts collectRemovedRects),若浮层常驻、只移除其
|
|
2411
|
+
// 普通子节点,blit 解毒不触发,被覆盖的转录行会在 blit-skip 后留空
|
|
2412
|
+
// (Esc 关 picker 一片空白的根因)。
|
|
2413
|
+
const dialogOverlayOpen = dialogOverlayVisible(overlay, {
|
|
2414
|
+
workspaceTargetCount: workspaceTargets.length,
|
|
2415
|
+
effortOptionCount: effortOptions.length,
|
|
2416
|
+
presetOptionCount: presetOptions.length,
|
|
2417
|
+
});
|
|
2418
|
+
// The sticky header pins the turn owning the viewport top row
|
|
2419
|
+
// (timeline.activeId, reported by MessageList) — scrolled up to an old
|
|
2420
|
+
// turn, it carries THAT turn's prompt, not the latest one.
|
|
2421
|
+
// channel.rows is a live in-place array, so the lookup is per-render.
|
|
2422
|
+
const anchorUserRowId = timeline.activeId;
|
|
2423
|
+
const anchorUserText = anchorUserRowId === null
|
|
2424
|
+
? null
|
|
2425
|
+
: channel.rows.find(row => row.id === anchorUserRowId)?.text ?? null;
|
|
2426
|
+
return (_jsxs(Box, { ref: wakeTickRef, flexDirection: "column", flexGrow: 1, width: "100%", children: [!isSticky && anchorUserText && (_jsx(StickyPromptHeader, { text: anchorUserText, onClick: () => {
|
|
2427
|
+
// Click snaps the pinned prompt to the viewport top (CC's
|
|
2428
|
+
// StickyPromptHeader). Jump by the SAME content coordinate the
|
|
2429
|
+
// rail's tick uses (timeline turn top = the prompt TEXT top):
|
|
2430
|
+
// the element-based seek lands the row wrapper's margin at the
|
|
2431
|
+
// top instead — one row shy of the text top the anchor rule
|
|
2432
|
+
// compares against — and the header would flip to the previous
|
|
2433
|
+
// turn immediately after the click.
|
|
2434
|
+
const turn = timeline.turns.find(t => t.id === anchorUserRowId);
|
|
2435
|
+
if (turn)
|
|
2436
|
+
handle?.scrollTo(turn.top);
|
|
2437
|
+
else if (anchorUserRowId !== null)
|
|
2438
|
+
seekRow(anchorUserRowId);
|
|
2095
2439
|
else
|
|
2096
2440
|
handle?.scrollToBottom();
|
|
2097
|
-
} })), _jsxs(
|
|
2441
|
+
} })), _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,
|
|
2442
|
+
// Resuming a long session skips the ~3.4s opening animation: it
|
|
2443
|
+
// keeps firing low-frequency React commits that compete with the
|
|
2444
|
+
// transcript mount batches (and the first wheel events) for the
|
|
2445
|
+
// frame budget right when the user wants to read history. Fresh
|
|
2446
|
+
// sessions keep the full intro; restored ones settle instantly.
|
|
2447
|
+
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) })] }), (() => {
|
|
2448
|
+
// Gutter mode (settings `dsh-tui.scrollGutter`): the timeline
|
|
2449
|
+
// rail (default), the proportional scrollbar, or nothing. The
|
|
2450
|
+
// slot keeps its 2 columns in both rendered modes (Qwen's
|
|
2451
|
+
// permanent-gutter rule — an appearing/disappearing gutter
|
|
2452
|
+
// changes the transcript width and rewraps everything).
|
|
2453
|
+
const gutter = normalizeScrollGutter(channel.scrollGutter);
|
|
2454
|
+
if (gutter === 'hidden')
|
|
2455
|
+
return null;
|
|
2456
|
+
if (gutter === 'scrollbar') {
|
|
2457
|
+
return _jsx(ScrollbarGutter, { handle: handle, terminalWidth: terminalColumns });
|
|
2458
|
+
}
|
|
2459
|
+
return (_jsx(TimelineRail, { handle: handle, turns: timeline.turns, activeId: timeline.activeId, upId: timeline.upId, downId: timeline.downId, terminalWidth: terminalColumns, hoverEnabled: !promptSelectionActive, onRevealTurn: revealAndSeekRow }));
|
|
2460
|
+
})()] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [showPill && (_jsx(NewMessagesPill, { count: unseenCount, onClick: () => handle?.scrollToBottom() })), channel.working &&
|
|
2098
2461
|
(channel.activityEnabled &&
|
|
2099
2462
|
!channel.minimal &&
|
|
2100
2463
|
channel.workingActivity !== undefined &&
|
|
@@ -2104,7 +2467,7 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
2104
2467
|
// the animated chars/4 estimate, matching the classic
|
|
2105
2468
|
// spinner's counter (the suffix used raw chars before,
|
|
2106
2469
|
// 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)) :
|
|
2470
|
+
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) }), 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' }) }) })) : 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
2471
|
void setClipboard(btw.answer ?? '').then(raw => { if (raw)
|
|
2109
2472
|
writeRaw?.(raw); });
|
|
2110
2473
|
channel.notify(t('copied-chars', { n: (btw.answer ?? '').length }), { timeoutMs: 1500 });
|
|
@@ -2114,12 +2477,129 @@ export function Chat({ channel, questionStore, approvalStore, extensionDialogs,
|
|
|
2114
2477
|
band: wakeBand,
|
|
2115
2478
|
hint: trajectorySeen ? undefined : `${modLabel}t`,
|
|
2116
2479
|
tick: Math.floor(wakeTime / 120),
|
|
2117
|
-
} }), dialogOverlayOpen && (_jsxs(OverlayAbove, { maxHeight: Math.max(terminalRows - 8,
|
|
2480
|
+
} }), dialogOverlayOpen && (_jsxs(OverlayAbove, { maxHeight: Math.max(terminalRows - 8, 1), children: [overlay.kind === 'thinking' && (_jsx(ThinkingToggle, { currentValue: thinkingVisible, focusIndex: overlay.focus, onPick: (index) => {
|
|
2481
|
+
// 点击行 = 设焦点 + 应用(与 Enter 同一条路径)
|
|
2482
|
+
const visible = index === 0;
|
|
2483
|
+
setThinkingVisible(visible);
|
|
2484
|
+
dispatchOverlay({ type: 'close' });
|
|
2485
|
+
channel.notify(t('thinking-toggled', { state: visible ? t('thinking-on') : t('thinking-off') }));
|
|
2486
|
+
} })), 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) => {
|
|
2487
|
+
// 点击行 = 切换该行目标(与 Enter 同一条路径)
|
|
2488
|
+
const target = workspaceTargets[index];
|
|
2489
|
+
dispatchOverlay({ type: 'close' });
|
|
2490
|
+
if (target !== undefined)
|
|
2491
|
+
void channel.switchWorkspace(target);
|
|
2492
|
+
} }) })), overlay.kind === 'workspace-menu' && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(WorkspaceMenuPicker, { options: workspaceMenuOptions, focusIndex: overlay.index, onPick: (index) => {
|
|
2493
|
+
// 点击行 = 执行该行(与 Enter 同一条路径)
|
|
2494
|
+
runWorkspaceMenuOption(workspaceMenuOptions[index]);
|
|
2495
|
+
} }) })), 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) => {
|
|
2496
|
+
// 点击行 = 设焦点 + 执行分支(与 Enter 同一条路径);
|
|
2497
|
+
// busy/输入态在组件侧禁点
|
|
2498
|
+
const choice = overlay.flow.choices[index];
|
|
2499
|
+
if (choice === undefined)
|
|
2500
|
+
return;
|
|
2501
|
+
dispatchOverlay({ type: 'set-index', kind: 'workspace-flow', index });
|
|
2502
|
+
runWorkspaceFlowAction(signal => choice.choose(signal));
|
|
2503
|
+
} }) })), overlay.kind === 'model' && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: models.length === 0 ? (_jsx(ModelPickerLoading, {})) : (_jsx(ModelPicker, { models: models, focusIndex: overlay.index, currentModel: `${channel.provider}/${channel.model}`, onPick: (index) => {
|
|
2504
|
+
// 点击行 = 应用该行模型(与 Enter 同一条路径)
|
|
2505
|
+
const model = models[index];
|
|
2506
|
+
if (!model)
|
|
2507
|
+
return;
|
|
2508
|
+
dispatchOverlay({ type: 'close' });
|
|
2509
|
+
channel.notify(t('model-switching', { name: model.name }));
|
|
2510
|
+
void channel.switchModel(model.provider, model.id).then((ok) => {
|
|
2511
|
+
if (ok)
|
|
2512
|
+
channel.notify(t('model-switched', { name: model.name }));
|
|
2513
|
+
});
|
|
2514
|
+
} })) })), overlay.kind === 'skills' && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: skillsList === null ? (_jsx(SkillsPickerLoading, {})) : (_jsx(SkillsPicker, { skills: skillsList, focusIndex: overlay.index, onPick: (index) => {
|
|
2515
|
+
const skill = skillsList[index];
|
|
2516
|
+
if (!skill)
|
|
2517
|
+
return;
|
|
2518
|
+
dispatchOverlay({ type: 'close' });
|
|
2519
|
+
if (skill.userInvocable)
|
|
2520
|
+
setHistoryFill(`/${skill.name} `);
|
|
2521
|
+
} })) })), overlay.kind === 'activity' && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(ActivityPicker, { focusIndex: overlay.index, currentPreset: channel.activityFrames, onPick: (index) => {
|
|
2522
|
+
dispatchOverlay({ type: 'close' });
|
|
2523
|
+
const name = PRESET_NAMES[index];
|
|
2524
|
+
if (name)
|
|
2525
|
+
channel.setActivityFrames(name);
|
|
2526
|
+
} }) })), overlay.kind === 'effort' && effortOptions.length > 1 && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(EffortSlider, { options: effortOptions, focusIndex: overlay.index, currentId: channel.reasoningEffort,
|
|
2527
|
+
// 点击档位 = 移到该档并即时应用(与 ←/→ 同语义)
|
|
2528
|
+
onPick: (index) => {
|
|
2529
|
+
dispatchOverlay({ type: 'set-index', kind: 'effort', index });
|
|
2530
|
+
const option = effortOptions[index];
|
|
2531
|
+
if (option)
|
|
2532
|
+
void channel.setEffort(option.id);
|
|
2533
|
+
} }) })), 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) => {
|
|
2534
|
+
dispatchOverlay({ type: 'close' });
|
|
2535
|
+
const option = presetOptions[index];
|
|
2536
|
+
if (option)
|
|
2537
|
+
void channel.switchPreset(option.id);
|
|
2538
|
+
} }) })), overlay.kind === 'permission' && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(PermissionsPicker, { focusIndex: overlay.index, currentMode: channel.mode.sandbox, cwd: channel.cwd, onPick: (index) => {
|
|
2539
|
+
dispatchOverlay({ type: 'close' });
|
|
2540
|
+
const id = PERMISSION_PRESET_IDS[index];
|
|
2541
|
+
if (id !== undefined) {
|
|
2542
|
+
void channel.runExternalCommand('permission', ` ${id}`).then((text) => {
|
|
2543
|
+
if (text !== undefined && text !== '')
|
|
2544
|
+
channel.notify(text);
|
|
2545
|
+
});
|
|
2546
|
+
}
|
|
2547
|
+
} }) })), overlay.kind === 'plan' && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(PlanPicker, { focusIndex: overlay.index, currentOn: channel.mode.plan === true, onPick: (index) => {
|
|
2548
|
+
dispatchOverlay({ type: 'close' });
|
|
2549
|
+
const on = index === 0;
|
|
2550
|
+
void channel.runExternalCommand('plan', on ? '' : ' off').then((text) => {
|
|
2551
|
+
if (text !== undefined && text !== '')
|
|
2552
|
+
channel.notify(text);
|
|
2553
|
+
});
|
|
2554
|
+
} }) })), overlay.kind === 'lang' && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(LangPicker, { focusIndex: overlay.index, currentLang: getLang(), onPick: (index) => {
|
|
2555
|
+
const lang = LANGS[index];
|
|
2556
|
+
if (lang === undefined)
|
|
2557
|
+
return;
|
|
2558
|
+
dispatchOverlay({ type: 'close' });
|
|
2559
|
+
applyLang(lang);
|
|
2560
|
+
} }) })), overlay.kind === 'theme' && (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsx(ThemePicker, { focusIndex: overlay.index, currentTheme: themeName, onPick: (index) => {
|
|
2561
|
+
dispatchOverlay({ type: 'close' });
|
|
2562
|
+
const name = getThemeOptions()[index]?.value;
|
|
2563
|
+
if (name !== undefined) {
|
|
2564
|
+
const ok = setTheme(name);
|
|
2565
|
+
channel.notify(ok ? t('theme-switched-saved', { name }) : t('theme-switch-failed', { name }), { color: ok ? 'success' : 'error' });
|
|
2566
|
+
}
|
|
2567
|
+
} }) })), 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) => {
|
|
2568
|
+
// 点击行 = 填入该历史命令(与 Enter 同路径)
|
|
2569
|
+
const entry = historyMatches[index];
|
|
2570
|
+
if (entry) {
|
|
2571
|
+
setHistoryFill(entry.text);
|
|
2572
|
+
dispatchOverlay({ type: 'close' });
|
|
2573
|
+
}
|
|
2574
|
+
} }) })), 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) => {
|
|
2575
|
+
// 列表页点击只选中:进入确认态保留键盘 Enter 显式触发
|
|
2576
|
+
dispatchOverlay({ type: 'set-index', kind: 'rewind', index });
|
|
2577
|
+
}, onConfirm: () => {
|
|
2578
|
+
// 确认页即显式确认层,点击直接执行(与 Enter 同路径)
|
|
2579
|
+
const row = overlay.confirm;
|
|
2580
|
+
if (row === null)
|
|
2581
|
+
return;
|
|
2582
|
+
dispatchOverlay({ type: 'close' });
|
|
2583
|
+
void performRewind(row);
|
|
2584
|
+
}, onPickMode: (index) => {
|
|
2585
|
+
// 模式列表点击直接执行该模式(与 Enter 同路径)
|
|
2586
|
+
const row = overlay.confirm;
|
|
2587
|
+
if (row === null)
|
|
2588
|
+
return;
|
|
2589
|
+
// 模式页仅当 modes 非空才渲染,这里空安全取值
|
|
2590
|
+
const mode = index === 0 ? null : (overlay.modes?.[index - 1]?.id ?? null);
|
|
2591
|
+
dispatchOverlay({ type: 'close' });
|
|
2592
|
+
void performRewind(row, mode);
|
|
2593
|
+
} }) })), overlay.kind === 'search' && _jsx(TranscriptSearchBar, { query: searchQuery, cursorOffset: searchCursor, count: searchCount, current: searchCurrent })] }))] })] }));
|
|
2118
2594
|
}
|
|
2119
2595
|
/**
|
|
2120
2596
|
* The pinned prompt header shown above the ScrollBox while the user has
|
|
2121
2597
|
* scrolled up (mirroring Claude Code's FullscreenLayout.StickyPromptHeader).
|
|
2122
|
-
*
|
|
2598
|
+
* Pins the user message the transcript viewport is currently showing — the
|
|
2599
|
+
* topmost visible user message, or the nearest one above when only assistant
|
|
2600
|
+
* content fills the view — so it tracks which turn the user is reading
|
|
2601
|
+
* instead of always carrying the latest prompt. Fixed at 1 row so the
|
|
2602
|
+
* ScrollBox never shifts when the text changes.
|
|
2123
2603
|
*/
|
|
2124
2604
|
function StickyPromptHeader({ text, onClick, }) {
|
|
2125
2605
|
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 +2607,9 @@ function StickyPromptHeader({ text, onClick, }) {
|
|
|
2127
2607
|
/** The `↓ N new messages` pill shown while scrolled up with new content. */
|
|
2128
2608
|
function NewMessagesPill({ count, onClick, }) {
|
|
2129
2609
|
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: [' ',
|
|
2610
|
+
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
|
|
2611
|
+
? t(count === 1 ? 'new-message' : 'new-messages', { n: count })
|
|
2612
|
+
: t('back-to-bottom'), ' '] }) }) }));
|
|
2131
2613
|
}
|
|
2132
2614
|
/** /model while the provider catalog is still loading (CC's LoadingState). */
|
|
2133
2615
|
function ModelPickerLoading() {
|