@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
|
@@ -9,12 +9,16 @@ import { EffortInputBorder } from './EffortInputBorder.js';
|
|
|
9
9
|
import { EffortTierBadge } from './EffortTierBadge.js';
|
|
10
10
|
import { isLightThemeActive } from '../theme.js';
|
|
11
11
|
import { useDeclaredCursor } from '../ink/hooks/use-declared-cursor.js';
|
|
12
|
+
import { noteAuxNumber } from '../ink/geometry-trace.js';
|
|
13
|
+
import instances from '../ink/instances.js';
|
|
12
14
|
import { stringWidth } from '../ink/stringWidth.js';
|
|
15
|
+
import { getGraphemeSegmenter } from '../utils/intl.js';
|
|
13
16
|
import { formatClipboardInsert, readClipboard } from '../utils/clipboard.js';
|
|
14
17
|
import { editInExternalEditor } from '../utils/externalEditor.js';
|
|
15
18
|
import { isHiddenCommandName, parseCommandName } from '../commands.js';
|
|
16
19
|
import { appendHistory } from '../history.js';
|
|
17
20
|
import { mentionAtCaret } from '../utils/mentions.js';
|
|
21
|
+
import { preserveSelection } from '../utils/fileSuggestions.js';
|
|
18
22
|
import { isMod } from '../utils/modifiers.js';
|
|
19
23
|
import { CommandSuggestions } from './CommandSuggestions.js';
|
|
20
24
|
import { FileSuggestions } from './FileSuggestions.js';
|
|
@@ -51,6 +55,84 @@ function wordBoundaryRight(text, cursor) {
|
|
|
51
55
|
index++;
|
|
52
56
|
return index;
|
|
53
57
|
}
|
|
58
|
+
// --- grapheme-cluster geometry ---------------------------------------------
|
|
59
|
+
// The caret, editing keys, and wrapping MUST agree on one text unit. Mixing
|
|
60
|
+
// UTF-16 code units (arrows/backspace), code points (wrap), and display
|
|
61
|
+
// cells (stringWidth) lets the caret land inside a surrogate pair or a ZWJ
|
|
62
|
+
// emoji — the inverted caret then shows half a glyph, Backspace deletes
|
|
63
|
+
// half a character, and `line.slice()` splits clusters. All offsets below
|
|
64
|
+
// are UTF-16 indices snapped to grapheme boundaries via the shared
|
|
65
|
+
// Intl.Segmenter (utils/intl.ts).
|
|
66
|
+
/** Ascending grapheme boundary offsets of `text` (starts at 0, ends at
|
|
67
|
+
* `text.length`). Empty text yields `[0]`. */
|
|
68
|
+
function graphemeBoundaries(text) {
|
|
69
|
+
const bounds = [0];
|
|
70
|
+
for (const { index, segment } of getGraphemeSegmenter().segment(text)) {
|
|
71
|
+
const end = index + segment.length;
|
|
72
|
+
if (end > bounds[bounds.length - 1])
|
|
73
|
+
bounds.push(end);
|
|
74
|
+
}
|
|
75
|
+
return bounds;
|
|
76
|
+
}
|
|
77
|
+
/** Largest grapheme boundary `<= offset` (clamped into the text). Snaps a
|
|
78
|
+
* cursor that landed mid-cluster back onto a boundary. */
|
|
79
|
+
function boundaryAtOrBefore(bounds, offset) {
|
|
80
|
+
let lo = 0;
|
|
81
|
+
let hi = bounds.length - 1;
|
|
82
|
+
let ans = bounds[0];
|
|
83
|
+
while (lo <= hi) {
|
|
84
|
+
const mid = (lo + hi) >> 1;
|
|
85
|
+
if (bounds[mid] <= offset) {
|
|
86
|
+
ans = bounds[mid];
|
|
87
|
+
lo = mid + 1;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
hi = mid - 1;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return ans;
|
|
94
|
+
}
|
|
95
|
+
/** Largest grapheme boundary strictly before `offset` (0 when none). */
|
|
96
|
+
function previousGraphemeBoundary(bounds, offset) {
|
|
97
|
+
let lo = 0;
|
|
98
|
+
let hi = bounds.length - 1;
|
|
99
|
+
let ans = 0;
|
|
100
|
+
while (lo <= hi) {
|
|
101
|
+
const mid = (lo + hi) >> 1;
|
|
102
|
+
if (bounds[mid] < offset) {
|
|
103
|
+
ans = bounds[mid];
|
|
104
|
+
lo = mid + 1;
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
hi = mid - 1;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return ans;
|
|
111
|
+
}
|
|
112
|
+
/** Smallest grapheme boundary strictly after `offset` (text.length when
|
|
113
|
+
* none). Returns `offset` unchanged when it already is the last boundary. */
|
|
114
|
+
function nextGraphemeBoundary(bounds, offset) {
|
|
115
|
+
let lo = 0;
|
|
116
|
+
let hi = bounds.length - 1;
|
|
117
|
+
let ans = bounds[hi] ?? 0;
|
|
118
|
+
while (lo <= hi) {
|
|
119
|
+
const mid = (lo + hi) >> 1;
|
|
120
|
+
if (bounds[mid] > offset) {
|
|
121
|
+
ans = bounds[mid];
|
|
122
|
+
hi = mid - 1;
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
lo = mid + 1;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return ans;
|
|
129
|
+
}
|
|
130
|
+
/** Snap an arbitrary UTF-16 offset onto a grapheme boundary of `text`,
|
|
131
|
+
* clamping into range. The safety net under every cursor write. */
|
|
132
|
+
function normalizeCursorOffset(text, offset) {
|
|
133
|
+
const clamped = Math.max(0, Math.min(offset, text.length));
|
|
134
|
+
return boundaryAtOrBefore(graphemeBoundaries(text), clamped);
|
|
135
|
+
}
|
|
54
136
|
/**
|
|
55
137
|
* The empty input deliberately shows NO placeholder text: terminal emulators
|
|
56
138
|
* paint the IME preedit (pinyin) at the physical cursor, which the app parks
|
|
@@ -65,9 +147,10 @@ const MAX_VISIBLE_LINES = 5;
|
|
|
65
147
|
/**
|
|
66
148
|
* Claude Code style prompt input: rounded border box (top+bottom borders
|
|
67
149
|
* only), `❯ ` prompt char (dimmed while a turn is working), the text with a
|
|
68
|
-
* block cursor at the cursor position, and
|
|
69
|
-
* suggestion
|
|
70
|
-
*
|
|
150
|
+
* block cursor at the cursor position, and above it the slash-command /
|
|
151
|
+
* file-completion suggestion card (SuggestionCard: rounded panel with the
|
|
152
|
+
* selected row behind a `❯` pointer in the theme's `suggestion` color,
|
|
153
|
+
* mirroring Claude Code's PromptInputFooterSuggestions layout).
|
|
71
154
|
*
|
|
72
155
|
* Empty input: a solid block caret on a blank cell and nothing else — no
|
|
73
156
|
* placeholder text, so the terminal-painted IME preedit (pinyin) at the
|
|
@@ -128,10 +211,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
128
211
|
React.useEffect(() => {
|
|
129
212
|
if (fillText && fillText !== lastFill.current) {
|
|
130
213
|
lastFill.current = fillText;
|
|
131
|
-
|
|
132
|
-
cursorRef.current = fillText.length;
|
|
133
|
-
setValue(fillText);
|
|
134
|
-
setCursor(fillText.length);
|
|
214
|
+
setInput(fillText);
|
|
135
215
|
onFillConsumed?.();
|
|
136
216
|
}
|
|
137
217
|
}, [fillText, onFillConsumed]);
|
|
@@ -152,10 +232,20 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
152
232
|
}, []);
|
|
153
233
|
const { columns, rows: terminalRows } = useTerminalSize();
|
|
154
234
|
const helpScrollRef = React.useRef(null);
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
|
|
235
|
+
// Help viewport budget: the overlay anchors at the composer's top edge
|
|
236
|
+
// (OverlayAbove bottom:'100%') and grows UP, so its budget is the space
|
|
237
|
+
// ABOVE that anchor — smallest on an empty session, where the whale
|
|
238
|
+
// splash (~15 rows) sits between the screen top and the composer
|
|
239
|
+
// (terminalRows minus chrome only applies once the transcript fills the
|
|
240
|
+
// viewport). Take the conservative intersection: 15 rows of viewport (16
|
|
241
|
+
// with the hint + margin) fits the empty-session anchor on the default
|
|
242
|
+
// layout at any terminal size, and the renderer's bottom-anchored
|
|
243
|
+
// clipping for absolute overlays (no negative-y clamp) then never has
|
|
244
|
+
// to eat the overlay's FIRST rows — the shortcut-column headers. The
|
|
245
|
+
// command registry scrolls inside the viewport, so a taller terminal
|
|
246
|
+
// loses nothing functional. (PR #446; restored after the picker
|
|
247
|
+
// snapshot's cherry-pick resurrected the old formula.)
|
|
248
|
+
const helpViewportHeight = Math.max(3, Math.min(terminalRows - 7, 15));
|
|
159
249
|
const suggestions = value.startsWith('/') ? channel.commandCompletions(value) : [];
|
|
160
250
|
const overlayOpen = suggestions.length > 0 &&
|
|
161
251
|
!helpOpen &&
|
|
@@ -165,29 +255,31 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
165
255
|
// CARET, so `@` works mid-message (`看看 @src/a.ts 这个`), not only when it
|
|
166
256
|
// is the input's first character. The cwd listing loads when the trigger
|
|
167
257
|
// appears.
|
|
168
|
-
const [
|
|
258
|
+
const [fileMatches, setFileMatches] = React.useState([]);
|
|
169
259
|
const [fileSelected, setFileSelected] = React.useState(0);
|
|
170
260
|
const mention = mentionAtCaret(value, cursor);
|
|
171
261
|
const atTrigger = mention !== undefined;
|
|
262
|
+
const fileRequestId = React.useRef(0);
|
|
263
|
+
const selectedFile = fileMatches[fileSelected];
|
|
172
264
|
React.useEffect(() => {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (
|
|
186
|
-
return
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
})
|
|
190
|
-
|
|
265
|
+
const requestId = ++fileRequestId.current;
|
|
266
|
+
if (!mention) {
|
|
267
|
+
setFileMatches([]);
|
|
268
|
+
setFileSelected(0);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const previous = selectedFile;
|
|
272
|
+
// Deps key on `mention.query` (and trigger on/off) only: cursor movement
|
|
273
|
+
// within the same token must NOT refetch, and `selectedFile`/`fileSelected`
|
|
274
|
+
// are read as their render-time values only to seed selection preservation.
|
|
275
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
276
|
+
void channel.listFileCandidates(mention.query, { topK: 50 }).then(next => {
|
|
277
|
+
if (requestId !== fileRequestId.current)
|
|
278
|
+
return;
|
|
279
|
+
setFileMatches(next);
|
|
280
|
+
setFileSelected(preserveSelection(previous, next, fileSelected));
|
|
281
|
+
});
|
|
282
|
+
}, [channel, mention?.query, atTrigger]);
|
|
191
283
|
// Esc dismisses the overlay for the token being edited (it reopens once the
|
|
192
284
|
// text changes); it must NOT clear a mid-message input.
|
|
193
285
|
const fileEscRef = React.useRef(-1);
|
|
@@ -200,7 +292,11 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
200
292
|
fileEscRef.current !== mention?.start;
|
|
201
293
|
const setInput = (next, cursorOffset = next.length) => {
|
|
202
294
|
valueRef.current = next;
|
|
203
|
-
|
|
295
|
+
// Normalize onto a grapheme boundary (also clamps into range): every
|
|
296
|
+
// caller passes a caret they believe is on a character edge — paste
|
|
297
|
+
// merges, history fills, and IME composition can still hand back an
|
|
298
|
+
// offset inside a surrogate pair or combining cluster.
|
|
299
|
+
cursorRef.current = normalizeCursorOffset(next, cursorOffset);
|
|
204
300
|
setValue(next);
|
|
205
301
|
setCursor(cursorRef.current);
|
|
206
302
|
};
|
|
@@ -210,11 +306,12 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
210
306
|
* directory inserts `@dir/` without a trailing space so completion
|
|
211
307
|
* continues into it; a file completes the token with a space.
|
|
212
308
|
*/
|
|
213
|
-
const acceptFile = (
|
|
309
|
+
const acceptFile = (candidate) => {
|
|
214
310
|
if (!mention)
|
|
215
311
|
return;
|
|
312
|
+
const file = candidate.path;
|
|
216
313
|
const body = /\s/.test(file) ? `@"${file}"` : `@${file}`;
|
|
217
|
-
const insert =
|
|
314
|
+
const insert = candidate.kind === 'directory' ? body : `${body} `;
|
|
218
315
|
const next = value.slice(0, mention.start) + insert + value.slice(mention.end);
|
|
219
316
|
setInput(next, mention.start + insert.length);
|
|
220
317
|
setFileSelected(0);
|
|
@@ -384,6 +481,9 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
384
481
|
// the text/caret produced by the preceding event in that batch.
|
|
385
482
|
const value = valueRef.current;
|
|
386
483
|
const cursor = cursorRef.current;
|
|
484
|
+
// Grapheme boundaries of the current text: every caret move / delete
|
|
485
|
+
// below snaps onto one of these offsets (never mid-cluster).
|
|
486
|
+
const bounds = graphemeBoundaries(value);
|
|
387
487
|
/** Insert text at the caret (typing, paste) and dismiss overlays. */
|
|
388
488
|
const insertAtCaret = (text) => {
|
|
389
489
|
if (helpOpen)
|
|
@@ -747,23 +847,27 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
747
847
|
return;
|
|
748
848
|
}
|
|
749
849
|
if (key.leftArrow) {
|
|
750
|
-
|
|
850
|
+
// Grapheme-step: skip the whole cluster (surrogate pair, ZWJ emoji,
|
|
851
|
+
// combining mark) so the caret never sits inside one.
|
|
852
|
+
setInput(value, previousGraphemeBoundary(bounds, cursor));
|
|
751
853
|
return;
|
|
752
854
|
}
|
|
753
855
|
if (key.rightArrow) {
|
|
754
|
-
setInput(value,
|
|
856
|
+
setInput(value, nextGraphemeBoundary(bounds, cursor));
|
|
755
857
|
return;
|
|
756
858
|
}
|
|
757
859
|
if (key.backspace) {
|
|
758
860
|
if (cursor === 0)
|
|
759
861
|
return;
|
|
760
|
-
|
|
862
|
+
const start = previousGraphemeBoundary(bounds, cursor);
|
|
863
|
+
setInput(value.slice(0, start) + value.slice(cursor), start);
|
|
761
864
|
return;
|
|
762
865
|
}
|
|
763
866
|
if (key.delete) {
|
|
764
|
-
|
|
867
|
+
const end = nextGraphemeBoundary(bounds, cursor);
|
|
868
|
+
if (end === cursor)
|
|
765
869
|
return;
|
|
766
|
-
setInput(value.slice(0, cursor) + value.slice(
|
|
870
|
+
setInput(value.slice(0, cursor) + value.slice(end), cursor);
|
|
767
871
|
return;
|
|
768
872
|
}
|
|
769
873
|
if (key.home) {
|
|
@@ -890,7 +994,10 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
890
994
|
// === Render: hard-wrap every logical line at the input width, then show
|
|
891
995
|
// the window of visual lines with the caret row always visible (CC's
|
|
892
996
|
// maxVisibleLines behavior with automatic wrapping).
|
|
893
|
-
|
|
997
|
+
// Narrow terminals: the usable width follows the real terminal down to a
|
|
998
|
+
// single column — a fixed floor of 10 would wrap far too early and park
|
|
999
|
+
// the declared cursor past the value box's actual width.
|
|
1000
|
+
const inputWidth = Math.max(1, columns - 3);
|
|
894
1001
|
const visualLines = wrapToWidth(value, inputWidth);
|
|
895
1002
|
const caretVisualLine = wrapToWidth(value.slice(0, cursor), inputWidth).length - 1;
|
|
896
1003
|
const windowStart = Math.max(0, Math.min(caretVisualLine - MAX_VISIBLE_LINES + 1, visualLines.length - MAX_VISIBLE_LINES));
|
|
@@ -918,13 +1025,38 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
918
1025
|
if (absoluteLine !== caretVisualLine) {
|
|
919
1026
|
return (_jsx(Text, { wrap: "truncate-end", children: line }, absoluteLine));
|
|
920
1027
|
}
|
|
921
|
-
// Caret row: invert the
|
|
1028
|
+
// Caret row: invert the grapheme cluster at the caret column (solid
|
|
1029
|
+
// block). `col` is a cluster boundary by construction (the cursor is
|
|
1030
|
+
// normalized onto boundaries and wrapping only breaks between
|
|
1031
|
+
// graphemes), so [col, next boundary) covers the WHOLE cluster — a
|
|
1032
|
+
// surrogate pair or ZWJ emoji inverts as one glyph, never two broken
|
|
1033
|
+
// halves.
|
|
922
1034
|
const col = caretCharCol();
|
|
1035
|
+
const clusterEnd = nextGraphemeBoundary(graphemeBoundaries(line), col);
|
|
923
1036
|
const before = line.slice(0, col);
|
|
924
|
-
const at = line
|
|
925
|
-
const after = line.slice(
|
|
1037
|
+
const at = clusterEnd > col ? line.slice(col, clusterEnd) : ' ';
|
|
1038
|
+
const after = line.slice(clusterEnd);
|
|
926
1039
|
return (_jsxs(Text, { wrap: "truncate-end", children: [before, _jsx(Text, { inverse: true, children: at }), after] }, absoluteLine));
|
|
927
1040
|
});
|
|
1041
|
+
// Composer height shrink: clearing multi-line text (Enter/Esc/Ctrl+C/
|
|
1042
|
+
// Backspace) collapses the input area within one commit, shifting the
|
|
1043
|
+
// status line up and the whole chrome with it. The renderer's
|
|
1044
|
+
// full-damage pass (didLayoutShift) repaints the shifted siblings, but
|
|
1045
|
+
// inline mode's virtual↔scrollback correspondence needs the stronger
|
|
1046
|
+
// in-place viewport repaint — same treatment as Ctrl+O and the
|
|
1047
|
+
// loaded-context toggle (see Chat.tsx). One-shot, only on SHRINK:
|
|
1048
|
+
// growth scrolls the terminal naturally and needs no recovery.
|
|
1049
|
+
const contentRows = value.length === 0 ? 1 : visibleLines.length;
|
|
1050
|
+
noteAuxNumber('promptContentRows', contentRows);
|
|
1051
|
+
const prevContentRowsRef = React.useRef(contentRows);
|
|
1052
|
+
React.useLayoutEffect(() => {
|
|
1053
|
+
if (contentRows < prevContentRowsRef.current) {
|
|
1054
|
+
const ink = instances.get(process.stdout) ?? instances.values().next().value;
|
|
1055
|
+
ink?.invalidatePrevFrame();
|
|
1056
|
+
ink?.reanchorViewport();
|
|
1057
|
+
}
|
|
1058
|
+
prevContentRowsRef.current = contentRows;
|
|
1059
|
+
}, [contentRows]);
|
|
928
1060
|
const lastNotification = channel.notifications[channel.notifications.length - 1];
|
|
929
1061
|
// Park the native terminal cursor at the input caret (via the renderer's
|
|
930
1062
|
// cursor-declaration mechanism). Terminal emulators render IME preedit
|
|
@@ -935,27 +1067,73 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
935
1067
|
// relative to the value box the ref attaches to.
|
|
936
1068
|
const valueBoxRef = useDeclaredCursor({
|
|
937
1069
|
line: caretVisualLine - windowStart,
|
|
938
|
-
column:
|
|
1070
|
+
// Clamp the declared column to the wrap width: a grapheme wider than
|
|
1071
|
+
// the last remaining column (emoji at width 1) can push the visual
|
|
1072
|
+
// column past inputWidth, and the park must stay inside the value box.
|
|
1073
|
+
column: Math.min(caretVisualCol(), inputWidth),
|
|
939
1074
|
active: !selectionActive,
|
|
940
1075
|
});
|
|
1076
|
+
/**
|
|
1077
|
+
* Click-to-position the caret: map a click inside the value box (local
|
|
1078
|
+
* row/column relative to the box) to a UTF-16 cursor offset via the same
|
|
1079
|
+
* grapheme walk the renderer wraps with — exact under CJK widths, wrapped
|
|
1080
|
+
* rows and multi-codepoint clusters. Clicks land on the boundary nearest
|
|
1081
|
+
* the clicked cell (mid-grapheme snaps to its start).
|
|
1082
|
+
*/
|
|
1083
|
+
const handleValueClick = React.useCallback((e) => {
|
|
1084
|
+
const clickedVisual = windowStart + e.localRow;
|
|
1085
|
+
const clamped = Math.max(0, Math.min(clickedVisual, visualLines.length - 1));
|
|
1086
|
+
setCursor(clickToCursorOffset(value, inputWidth, clamped, e.localCol));
|
|
1087
|
+
}, [windowStart, visualLines.length, value, inputWidth]);
|
|
941
1088
|
// 浮层整体挂载条件:与内部面板可见条件精确同值。关闭时必须把整个
|
|
942
1089
|
// absolute 浮层移除——渲染器的 absolute-removed 检测只看被移除节点自身
|
|
943
1090
|
// 的 style.position,常驻浮层 + 移除普通子节点不会触发 blit 解毒,被
|
|
944
1091
|
// 覆盖的转录行会留空(见 Chat.tsx dialogOverlayOpen 注释)。
|
|
945
1092
|
const floatersOpen = helpOpen || channel.pending.length > 0 || fileOverlayOpen || overlayOpen;
|
|
946
|
-
|
|
1093
|
+
// 补全卡片边框与输入框 idle 边框同色(plan 模式下整套面板一起变 sage 绿)。
|
|
1094
|
+
const promptAccent = channel.mode.plan === true ? 'planMode' : 'promptBorder';
|
|
1095
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [floatersOpen && (_jsxs(OverlayAbove, { maxHeight: Math.max(terminalRows - 6, 1), children: [helpOpen && (_jsx(Box, { marginBottom: 1, children: _jsx(HelpMenu, { commands: channel.commandList, viewportHeight: helpViewportHeight, viewportWidth: columns, scrollRef: helpScrollRef, onCommandPick: (name) => {
|
|
1096
|
+
// 点击命令行 = 填入 /name 并关闭帮助(Tab 补全的鼠标等价)
|
|
1097
|
+
setInput(`/${name} `);
|
|
1098
|
+
onToggleHelp();
|
|
1099
|
+
} }) })), !helpOpen && channel.pending.length > 0 && (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, paddingBottom: 1, children: [channel.pending.some(item => item.placement === 'steer') && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["\u26A1 ", t('input-pending-steer-label')] }), channel.pending
|
|
947
1100
|
.filter(item => item.placement === 'steer')
|
|
948
1101
|
.map(item => (_jsxs(Text, { dimColor: true, wrap: "truncate", children: [' ', "\u21B3 ", item.text] }, item.id)))] })), channel.pending.some(item => item.placement === 'followup') && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { dimColor: true, children: ["\u23F3 ", t('input-pending-queue-label')] }), channel.pending
|
|
949
1102
|
.filter(item => item.placement === 'followup')
|
|
950
|
-
.map(item => (_jsxs(Text, { dimColor: true, wrap: "truncate", children: [' ', "\u21B3 ", item.text] }, item.id)))] })), _jsxs(Text, { dimColor: true, children: ["Alt+\u2191 ", t('input-pending-actions-hint')] })] })), fileOverlayOpen && (_jsx(
|
|
1103
|
+
.map(item => (_jsxs(Text, { dimColor: true, wrap: "truncate", children: [' ', "\u21B3 ", item.text] }, item.id)))] })), _jsxs(Text, { dimColor: true, children: ["Alt+\u2191 ", t('input-pending-actions-hint')] })] })), fileOverlayOpen && (_jsx(FileSuggestions, { files: fileMatches, selectedIndex: fileSelected, columns: columns, query: mention?.query ?? '', accent: promptAccent,
|
|
1104
|
+
// 点击行 = 接受该项(与 Enter 同路径)
|
|
1105
|
+
onPick: (index) => {
|
|
1106
|
+
const file = fileMatches[index];
|
|
1107
|
+
if (file)
|
|
1108
|
+
acceptFile(file);
|
|
1109
|
+
},
|
|
1110
|
+
// 滚轮 = 移动选中行(与 ↑/↓ 同路径,窗口跟随)
|
|
1111
|
+
onWheelStep: (step) => {
|
|
1112
|
+
setFileSelected(i => Math.max(0, Math.min(fileMatches.length - 1, i + step)));
|
|
1113
|
+
} })), overlayOpen && (_jsx(CommandSuggestions, { commands: suggestions, selectedIndex: selectedCommand, columns: columns, query: value, accent: promptAccent,
|
|
1114
|
+
// 点击行 = 运行该命令(与 Enter 同路径)
|
|
1115
|
+
onPick: (index) => {
|
|
1116
|
+
const command = suggestions[index];
|
|
1117
|
+
if (command)
|
|
1118
|
+
tryRunCommand(command.commandLine);
|
|
1119
|
+
},
|
|
1120
|
+
// 滚轮 = 移动选中行(与 ↑/↓ 同路径,窗口跟随)
|
|
1121
|
+
onWheelStep: (step) => {
|
|
1122
|
+
setSelectedCommand(i => Math.max(0, Math.min(suggestions.length - 1, i + step)));
|
|
1123
|
+
} }))] })), lastNotification && (_jsx(Box, { position: "absolute", marginTop: -1, height: 1, width: "100%", paddingLeft: 2, paddingRight: 1, flexDirection: "column", justifyContent: "flex-end", overflow: "hidden", children: _jsx(Box, { justifyContent: "flex-end", children: _jsx(Text, { color: lastNotification.color, dimColor: !lastNotification.color, wrap: "truncate", children: lastNotification.text }) }) })), _jsx(EffortInputBorder, { effort: channel.reasoningEffort, levels: channel.effortLevels, columns: columns, onLight: isLightThemeActive(themeName), idleColor: promptAccent, children: _jsxs(Box, { flexDirection: "row", alignItems: "flex-start", width: "100%", children: [_jsx(EffortChargeGlyph, { effort: channel.reasoningEffort, levels: channel.effortLevels, working: channel.working }), _jsx(Box, { ref: valueBoxRef, flexGrow: 1, flexShrink: 1, onClick: handleValueClick, children: value.length === 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { inverse: true, children: " " }), _jsx(EffortTierBadge, { effort: channel.reasoningEffort, levels: channel.effortLevels, onLight: isLightThemeActive(themeName), columns: columns, leadingColumns: 3 })] })) : (_jsx(Box, { flexDirection: "column", children: rendered })) })] }) })] }));
|
|
951
1124
|
}
|
|
952
1125
|
/**
|
|
953
1126
|
* Hard-wrap text into visual rows of at most `width` columns (CJK-aware via
|
|
954
1127
|
* stringWidth). Used by the input renderer so long lines wrap instead of
|
|
955
|
-
* truncating, with exact caret-row mapping.
|
|
1128
|
+
* truncating, with exact caret-row mapping. Wrapping only ever breaks
|
|
1129
|
+
* BETWEEN grapheme clusters: iterating code points would split ZWJ emoji
|
|
1130
|
+
* and combining sequences across rows, leaving a broken half at each edge
|
|
1131
|
+
* and desyncing the caret's row arithmetic (which walks cluster
|
|
1132
|
+
* boundaries).
|
|
956
1133
|
*/
|
|
957
1134
|
function wrapToWidth(text, width) {
|
|
958
1135
|
const rows = [];
|
|
1136
|
+
const segmenter = getGraphemeSegmenter();
|
|
959
1137
|
for (const line of text.split('\n')) {
|
|
960
1138
|
if (line === '') {
|
|
961
1139
|
rows.push('');
|
|
@@ -963,15 +1141,15 @@ function wrapToWidth(text, width) {
|
|
|
963
1141
|
}
|
|
964
1142
|
let current = '';
|
|
965
1143
|
let currentWidth = 0;
|
|
966
|
-
for (const
|
|
967
|
-
const w = stringWidth(
|
|
1144
|
+
for (const { segment } of segmenter.segment(line)) {
|
|
1145
|
+
const w = stringWidth(segment);
|
|
968
1146
|
if (currentWidth + w > width && current !== '') {
|
|
969
1147
|
rows.push(current);
|
|
970
|
-
current =
|
|
1148
|
+
current = segment;
|
|
971
1149
|
currentWidth = w;
|
|
972
1150
|
}
|
|
973
1151
|
else {
|
|
974
|
-
current +=
|
|
1152
|
+
current += segment;
|
|
975
1153
|
currentWidth += w;
|
|
976
1154
|
}
|
|
977
1155
|
}
|
|
@@ -979,3 +1157,48 @@ function wrapToWidth(text, width) {
|
|
|
979
1157
|
}
|
|
980
1158
|
return rows;
|
|
981
1159
|
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Inverse of {@link wrapToWidth}: map a click position (visual row index +
|
|
1162
|
+
* visual column) back to a UTF-16 offset in the original text. Walks the
|
|
1163
|
+
* same grapheme boundaries with the same break rule, so every visual row's
|
|
1164
|
+
* start offset is known exactly. Within the clicked row, the caret snaps to
|
|
1165
|
+
* the boundary nearest the click: a grapheme whose midpoint lies past the
|
|
1166
|
+
* click column takes the caret before it, otherwise after.
|
|
1167
|
+
*/
|
|
1168
|
+
function clickToCursorOffset(text, width, visualLine, visualCol) {
|
|
1169
|
+
const segmenter = getGraphemeSegmenter();
|
|
1170
|
+
let row = 0;
|
|
1171
|
+
let offset = 0;
|
|
1172
|
+
for (const line of text.split('\n')) {
|
|
1173
|
+
if (line === '') {
|
|
1174
|
+
if (row === visualLine)
|
|
1175
|
+
return offset;
|
|
1176
|
+
row++;
|
|
1177
|
+
offset++; // the '\n'
|
|
1178
|
+
continue;
|
|
1179
|
+
}
|
|
1180
|
+
let currentWidth = 0;
|
|
1181
|
+
for (const { segment } of segmenter.segment(line)) {
|
|
1182
|
+
const w = stringWidth(segment);
|
|
1183
|
+
if (currentWidth + w > width && currentWidth > 0) {
|
|
1184
|
+
if (row === visualLine)
|
|
1185
|
+
return offset; // end of the clicked wrapped row
|
|
1186
|
+
row++;
|
|
1187
|
+
currentWidth = 0;
|
|
1188
|
+
}
|
|
1189
|
+
if (row === visualLine) {
|
|
1190
|
+
if (currentWidth + w / 2 > visualCol)
|
|
1191
|
+
return offset;
|
|
1192
|
+
if (currentWidth + w > visualCol)
|
|
1193
|
+
return offset + segment.length;
|
|
1194
|
+
}
|
|
1195
|
+
currentWidth += w;
|
|
1196
|
+
offset += segment.length;
|
|
1197
|
+
}
|
|
1198
|
+
if (row === visualLine)
|
|
1199
|
+
return offset;
|
|
1200
|
+
row++;
|
|
1201
|
+
offset++; // the '\n'
|
|
1202
|
+
}
|
|
1203
|
+
return offset;
|
|
1204
|
+
}
|
|
@@ -12,7 +12,7 @@ import type { TuiRewindMode } from '../dsh-adapter/extension-events.js';
|
|
|
12
12
|
* a choice list whose option zero is always the built-in conversation-only
|
|
13
13
|
* rewind, followed by the plugin's modes (e.g. "also restore files").
|
|
14
14
|
*/
|
|
15
|
-
export declare function RewindPicker({ rows, focusIndex, confirmRow, modes, modeIndex, busy, }: {
|
|
15
|
+
export declare function RewindPicker({ rows, focusIndex, confirmRow, modes, modeIndex, busy, onPickRow, onConfirm, onPickMode, }: {
|
|
16
16
|
rows: readonly ChatRow[];
|
|
17
17
|
focusIndex: number;
|
|
18
18
|
confirmRow: ChatRow | null;
|
|
@@ -22,5 +22,16 @@ export declare function RewindPicker({ rows, focusIndex, confirmRow, modes, mode
|
|
|
22
22
|
modeIndex?: number;
|
|
23
23
|
/** True while the plugin decision is in flight. */
|
|
24
24
|
busy?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Mouse pick on a list row (fullscreen): sets focus only — rewind is a
|
|
27
|
+
* high-risk invisible-confirm operation, so stepping into the confirm
|
|
28
|
+
* state stays an explicit keyboard Enter.
|
|
29
|
+
*/
|
|
30
|
+
onPickRow?: (index: number) => void;
|
|
31
|
+
/** Mouse click on the plain confirm row: executes the rewind directly —
|
|
32
|
+
* the confirm pane itself is the explicit confirmation layer. */
|
|
33
|
+
onConfirm?: () => void;
|
|
34
|
+
/** Mouse pick on a modes-list option: executes that mode directly. */
|
|
35
|
+
onPickMode?: (index: number) => void;
|
|
25
36
|
}): React.ReactNode;
|
|
26
37
|
//# sourceMappingURL=RewindPicker.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RewindPicker.d.ts","sourceRoot":"","sources":["../../../src/components/RewindPicker.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAGzB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAA;AACxD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAA;AAMvE;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,EAC3B,IAAI,EACJ,UAAU,EACV,UAAU,EACV,KAAY,EACZ,SAAa,EACb,IAAY,
|
|
1
|
+
{"version":3,"file":"RewindPicker.d.ts","sourceRoot":"","sources":["../../../src/components/RewindPicker.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAGzB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAA;AACxD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oCAAoC,CAAA;AAMvE;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,EAC3B,IAAI,EACJ,UAAU,EACV,UAAU,EACV,KAAY,EACZ,SAAa,EACb,IAAY,EACZ,SAAS,EACT,SAAS,EACT,UAAU,GACX,EAAE;IACD,IAAI,EAAE,SAAS,OAAO,EAAE,CAAA;IACxB,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,OAAO,GAAG,IAAI,CAAA;IAC1B,6EAA6E;IAC7E,KAAK,CAAC,EAAE,SAAS,aAAa,EAAE,GAAG,IAAI,CAAA;IACvC,gEAAgE;IAChE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mDAAmD;IACnD,IAAI,CAAC,EAAE,OAAO,CAAA;IACd;;;;OAIG;IACH,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACnC;sEACkE;IAClE,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;IACtB,sEAAsE;IACtE,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CACrC,GAAG,KAAK,CAAC,SAAS,CAkHlB"}
|
|
@@ -16,7 +16,7 @@ import { listWindow } from './listWindow.js';
|
|
|
16
16
|
* a choice list whose option zero is always the built-in conversation-only
|
|
17
17
|
* rewind, followed by the plugin's modes (e.g. "also restore files").
|
|
18
18
|
*/
|
|
19
|
-
export function RewindPicker({ rows, focusIndex, confirmRow, modes = null, modeIndex = 0, busy = false, }) {
|
|
19
|
+
export function RewindPicker({ rows, focusIndex, confirmRow, modes = null, modeIndex = 0, busy = false, onPickRow, onConfirm, onPickMode, }) {
|
|
20
20
|
if (confirmRow !== null) {
|
|
21
21
|
if (modes !== null) {
|
|
22
22
|
// Plugin modes: a described choice list (one extra row per
|
|
@@ -34,10 +34,10 @@ export function RewindPicker({ rows, focusIndex, confirmRow, modes = null, modeI
|
|
|
34
34
|
];
|
|
35
35
|
return (_jsxs(Pane, { color: "permission", children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: "remember", bold: true, children: t('rewind-confirm-title') }), _jsx(Text, { dimColor: true, children: preview(confirmRow.text) })] }), options.slice(start, end).map((option, index) => {
|
|
36
36
|
const absoluteIndex = start + index;
|
|
37
|
-
return (_jsx(ListItem, { isFocused: absoluteIndex === modeIndex, description: option.description, showScrollUp: absoluteIndex === start && start > 0, showScrollDown: absoluteIndex === end - 1 && end < options.length, children: option.label }, option.key));
|
|
37
|
+
return (_jsx(ListItem, { isFocused: absoluteIndex === modeIndex, description: option.description, showScrollUp: absoluteIndex === start && start > 0, showScrollDown: absoluteIndex === end - 1 && end < options.length, onClick: onPickMode === undefined ? undefined : () => onPickMode(absoluteIndex), children: option.label }, option.key));
|
|
38
38
|
})] }), _jsx(Text, { dimColor: true, italic: true, children: _jsx(HintLine, { text: t('hint-select-exit') }) })] }));
|
|
39
39
|
}
|
|
40
|
-
return (_jsx(Pane, { color: "permission", children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "remember", bold: true, children: t('rewind-confirm-title') }) }), _jsx(ListItem, { isFocused: false, description: t('rewind-confirm-desc'), children: preview(confirmRow.text) }), _jsx(Text, { dimColor: true, italic: true, children: _jsx(HintLine, { text: t('hint-rewind-back') }) })] }) }));
|
|
40
|
+
return (_jsx(Pane, { color: "permission", children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "remember", bold: true, children: t('rewind-confirm-title') }) }), _jsx(ListItem, { isFocused: false, description: t('rewind-confirm-desc'), onClick: onConfirm, children: preview(confirmRow.text) }), _jsx(Text, { dimColor: true, italic: true, children: _jsx(HintLine, { text: t('hint-rewind-back') }) })] }) }));
|
|
41
41
|
}
|
|
42
42
|
const { rows: terminalRows } = useTerminalSize();
|
|
43
43
|
// 焦点窗口化按行预算:首项带 'last message' 描述占 2 行、其余 1 行
|
|
@@ -46,7 +46,9 @@ export function RewindPicker({ rows, focusIndex, confirmRow, modes = null, modeI
|
|
|
46
46
|
const { start, end } = listWindow(rows.map((_, i) => (i === 0 ? 2 : 1)), focusIndex, Math.max(terminalRows - 14, 2));
|
|
47
47
|
return (_jsxs(Pane, { color: "permission", children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { color: "remember", bold: true, children: t('rewind-title') }), _jsx(Text, { dimColor: true, children: t('rewind-subtitle') })] }), rows.length === 0 ? (_jsx(ListItem, { isFocused: false, children: t('rewind-empty') })) : (rows.slice(start, end).map((row, index) => {
|
|
48
48
|
const absoluteIndex = start + index;
|
|
49
|
-
return (_jsx(ListItem, { isFocused: absoluteIndex === focusIndex, description: absoluteIndex === 0 ? t('rewind-last-message') : undefined, showScrollUp: absoluteIndex === start && start > 0, showScrollDown: absoluteIndex === end - 1 && end < rows.length,
|
|
49
|
+
return (_jsx(ListItem, { isFocused: absoluteIndex === focusIndex, description: absoluteIndex === 0 ? t('rewind-last-message') : undefined, showScrollUp: absoluteIndex === start && start > 0, showScrollDown: absoluteIndex === end - 1 && end < rows.length, onClick: onPickRow !== undefined && !busy
|
|
50
|
+
? () => onPickRow(absoluteIndex)
|
|
51
|
+
: undefined, children: preview(row.text) }, row.id));
|
|
50
52
|
})), busy && _jsx(Text, { dimColor: true, children: t('rewind-waiting-plugins') })] }), _jsx(Text, { dimColor: true, italic: true, children: _jsx(HintLine, { text: t('hint-select-exit') }) })] }));
|
|
51
53
|
}
|
|
52
54
|
/** One-line preview of a message (newlines flattened, capped). */
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type ScrollBoxHandle } from '../ui.js';
|
|
3
|
+
/**
|
|
4
|
+
* Proportional scrollbar for the fullscreen transcript's gutter — the
|
|
5
|
+
* `scrollbar` option of the `dsh-tui.scrollGutter` setting (the timeline
|
|
6
|
+
* rail's sibling; same 2-column slot, same chrome rules):
|
|
7
|
+
*
|
|
8
|
+
* - the thumb (██) shows the visible window's position AND size over the
|
|
9
|
+
* whole content (viewport²/content tall, positioned by scrollTop);
|
|
10
|
+
* - clicking the track scrolls the clicked position to the viewport top
|
|
11
|
+
* (classic scrollbar semantics — the thumb centers under the click
|
|
12
|
+
* through the follow-up renders);
|
|
13
|
+
* - the gutter is permanent while scrollable (Qwen's rule: an
|
|
14
|
+
* auto-hiding gutter that changes content width rewraps everything);
|
|
15
|
+
* hidden below 60 terminal columns or when the content fits (inline
|
|
16
|
+
* mode keeps the terminal's native scrollback);
|
|
17
|
+
* - NoSelect fences the glyphs out of click-drag text selection; the
|
|
18
|
+
* wheel over the gutter scrolls the transcript.
|
|
19
|
+
*
|
|
20
|
+
* Thumb dragging is intentionally NOT implemented (Grok's MVP rule): a
|
|
21
|
+
* proportional thumb encodes position, and the tick rail remains the
|
|
22
|
+
* semantic navigator. Click-to-position covers the same need.
|
|
23
|
+
*/
|
|
24
|
+
export declare function ScrollbarGutter({ handle, terminalWidth, }: {
|
|
25
|
+
handle: ScrollBoxHandle | null;
|
|
26
|
+
terminalWidth: number;
|
|
27
|
+
}): React.ReactNode;
|
|
28
|
+
//# sourceMappingURL=ScrollbarGutter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ScrollbarGutter.d.ts","sourceRoot":"","sources":["../../../src/components/ScrollbarGutter.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AACzB,OAAO,EAAuB,KAAK,eAAe,EAAE,MAAM,UAAU,CAAA;AAOpE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,eAAe,CAAC,EAC9B,MAAM,EACN,aAAa,GACd,EAAE;IACD,MAAM,EAAE,eAAe,GAAG,IAAI,CAAA;IAC9B,aAAa,EAAE,MAAM,CAAA;CACtB,GAAG,KAAK,CAAC,SAAS,CA2DlB"}
|