@deepseek-harness-tui/dsh-tui 0.8.8 → 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 +27 -8
- 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 +24 -7
- package/lib/types/components/FileSuggestions.d.ts.map +1 -1
- package/lib/types/components/FileSuggestions.js +39 -12
- package/lib/types/components/GoalTodoPanel.d.ts +26 -2
- package/lib/types/components/GoalTodoPanel.d.ts.map +1 -1
- package/lib/types/components/GoalTodoPanel.js +86 -17
- package/lib/types/components/HelpMenu.d.ts +7 -1
- package/lib/types/components/HelpMenu.d.ts.map +1 -1
- package/lib/types/components/HelpMenu.js +12 -4
- package/lib/types/components/HistorySearchDialog.d.ts +4 -1
- package/lib/types/components/HistorySearchDialog.d.ts.map +1 -1
- package/lib/types/components/HistorySearchDialog.js +2 -2
- package/lib/types/components/LangPicker.d.ts +14 -0
- package/lib/types/components/LangPicker.d.ts.map +1 -0
- package/lib/types/components/LangPicker.js +17 -0
- package/lib/types/components/LoadedContextPanel.d.ts.map +1 -1
- package/lib/types/components/LoadedContextPanel.js +3 -1
- package/lib/types/components/Markdown.d.ts.map +1 -1
- package/lib/types/components/Markdown.js +12 -3
- package/lib/types/components/MessageList.d.ts +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 +248 -29
- 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 +16 -5
- package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel.js +332 -29
- 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 +4 -1
- package/lib/types/dsh-adapter/plugin.d.ts +0 -9
- package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
- package/lib/types/dsh-adapter/plugin.js +136 -6
- package/lib/types/i18n.d.ts +176 -15
- package/lib/types/i18n.d.ts.map +1 -1
- package/lib/types/i18n.js +51 -8
- package/lib/types/ink/components/AlternateScreen.d.ts.map +1 -1
- package/lib/types/ink/components/AlternateScreen.js +13 -1
- package/lib/types/ink/components/App.d.ts +12 -1
- package/lib/types/ink/components/App.d.ts.map +1 -1
- package/lib/types/ink/components/App.js +101 -4
- package/lib/types/ink/components/ClockContext.d.ts.map +1 -1
- package/lib/types/ink/components/ClockContext.js +2 -0
- package/lib/types/ink/components/ScrollBox.d.ts.map +1 -1
- package/lib/types/ink/components/ScrollBox.js +170 -91
- package/lib/types/ink/constants.d.ts +9 -0
- package/lib/types/ink/constants.d.ts.map +1 -1
- package/lib/types/ink/constants.js +9 -0
- package/lib/types/ink/events/click-event.d.ts +7 -16
- package/lib/types/ink/events/click-event.d.ts.map +1 -1
- package/lib/types/ink/events/click-event.js +16 -19
- package/lib/types/ink/events/event-handlers.d.ts +11 -1
- package/lib/types/ink/events/event-handlers.d.ts.map +1 -1
- package/lib/types/ink/events/event-handlers.js +2 -0
- package/lib/types/ink/events/input-event.d.ts +9 -0
- package/lib/types/ink/events/input-event.d.ts.map +1 -1
- package/lib/types/ink/events/input-event.js +4 -0
- package/lib/types/ink/events/pointer-event.d.ts +61 -0
- package/lib/types/ink/events/pointer-event.d.ts.map +1 -0
- package/lib/types/ink/events/pointer-event.js +61 -0
- package/lib/types/ink/events/wheel-event.d.ts +17 -0
- package/lib/types/ink/events/wheel-event.d.ts.map +1 -0
- package/lib/types/ink/events/wheel-event.js +20 -0
- package/lib/types/ink/geometry-trace.d.ts +51 -0
- package/lib/types/ink/geometry-trace.d.ts.map +1 -0
- package/lib/types/ink/geometry-trace.js +83 -0
- package/lib/types/ink/hit-test.d.ts +56 -1
- package/lib/types/ink/hit-test.d.ts.map +1 -1
- package/lib/types/ink/hit-test.js +146 -8
- package/lib/types/ink/ink.d.ts +58 -1
- package/lib/types/ink/ink.d.ts.map +1 -1
- package/lib/types/ink/ink.js +230 -29
- package/lib/types/ink/node-cache.d.ts +5 -0
- package/lib/types/ink/node-cache.d.ts.map +1 -1
- package/lib/types/ink/parse-keypress.d.ts +8 -0
- package/lib/types/ink/parse-keypress.d.ts.map +1 -1
- package/lib/types/ink/parse-keypress.js +101 -12
- package/lib/types/ink/render-node-to-output.d.ts +10 -0
- package/lib/types/ink/render-node-to-output.d.ts.map +1 -1
- package/lib/types/ink/render-node-to-output.js +111 -18
- package/lib/types/ink/timeline-rail.d.ts +138 -0
- package/lib/types/ink/timeline-rail.d.ts.map +1 -0
- package/lib/types/ink/timeline-rail.js +179 -0
- package/lib/types/ink/wrap-text.d.ts.map +1 -1
- package/lib/types/ink/wrap-text.js +47 -5
- package/lib/types/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 +42 -6
- 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/paths.d.ts.map +1 -1
- package/lib/types/utils/paths.js +1 -0
- package/package.json +4 -2
|
@@ -9,9 +9,12 @@ import type { LlmModelInfo } from '../dsh-adapter/types.js';
|
|
|
9
9
|
* 长列表按焦点窗口化(Select 同款):picker 经 OverlayAbove 浮层挂载后有
|
|
10
10
|
* maxHeight 裁剪,全量渲染会让焦点行被裁掉(看不到焦点按 Enter)。
|
|
11
11
|
*/
|
|
12
|
-
export declare function ModelPicker({ models, focusIndex, currentModel, }: {
|
|
12
|
+
export declare function ModelPicker({ models, focusIndex, currentModel, onPick, }: {
|
|
13
13
|
models: readonly LlmModelInfo[];
|
|
14
14
|
focusIndex: number;
|
|
15
15
|
currentModel: string;
|
|
16
|
+
/** Mouse pick (fullscreen): reports the clicked row's absolute index —
|
|
17
|
+
* Chat applies it with the same code path as the keyboard Enter. */
|
|
18
|
+
onPick?: (index: number) => void;
|
|
16
19
|
}): React.ReactNode;
|
|
17
20
|
//# sourceMappingURL=ModelPicker.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ModelPicker.d.ts","sourceRoot":"","sources":["../../../src/components/ModelPicker.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAGzB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AAM3D;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,EAC1B,MAAM,EACN,UAAU,EACV,YAAY,
|
|
1
|
+
{"version":3,"file":"ModelPicker.d.ts","sourceRoot":"","sources":["../../../src/components/ModelPicker.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAGzB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAA;AAM3D;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,EAC1B,MAAM,EACN,UAAU,EACV,YAAY,EACZ,MAAM,GACP,EAAE;IACD,MAAM,EAAE,SAAS,YAAY,EAAE,CAAA;IAC/B,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB;yEACqE;IACrE,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CACjC,GAAG,KAAK,CAAC,SAAS,CAwClB"}
|
|
@@ -14,7 +14,7 @@ import { listWindow } from './listWindow.js';
|
|
|
14
14
|
* 长列表按焦点窗口化(Select 同款):picker 经 OverlayAbove 浮层挂载后有
|
|
15
15
|
* maxHeight 裁剪,全量渲染会让焦点行被裁掉(看不到焦点按 Enter)。
|
|
16
16
|
*/
|
|
17
|
-
export function ModelPicker({ models, focusIndex, currentModel, }) {
|
|
17
|
+
export function ModelPicker({ models, focusIndex, currentModel, onPick, }) {
|
|
18
18
|
const { rows: terminalRows } = useTerminalSize();
|
|
19
19
|
// 焦点窗口化按行预算:ListItem 带 description 时占 2 行(正文+描述,均
|
|
20
20
|
// truncate 成单行),只数项数会把焦点裁出浮层(二次审查实证)。
|
|
@@ -22,6 +22,6 @@ export function ModelPicker({ models, focusIndex, currentModel, }) {
|
|
|
22
22
|
const { start, end } = listWindow(models.map(m => (m.description ? 2 : 1)), focusIndex, Math.max(terminalRows - 14, 2));
|
|
23
23
|
return (_jsxs(Pane, { color: "permission", children: [_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "remember", bold: true, children: t('picker-title-model') }) }), models.slice(start, end).map((model, index) => {
|
|
24
24
|
const absoluteIndex = start + index;
|
|
25
|
-
return (_jsxs(ListItem, { isFocused: absoluteIndex === focusIndex, isSelected: `${model.provider}/${model.id}` === currentModel, description: model.description, showScrollUp: absoluteIndex === start && start > 0, showScrollDown: absoluteIndex === end - 1 && end < models.length, children: [model.provider, " / ", model.name] }, `${model.provider}/${model.id}`));
|
|
25
|
+
return (_jsxs(ListItem, { isFocused: absoluteIndex === focusIndex, isSelected: `${model.provider}/${model.id}` === currentModel, description: model.description, showScrollUp: absoluteIndex === start && start > 0, showScrollDown: absoluteIndex === end - 1 && end < models.length, onClick: onPick ? () => onPick(absoluteIndex) : undefined, children: [model.provider, " / ", model.name] }, `${model.provider}/${model.id}`));
|
|
26
26
|
})] }), _jsx(Text, { dimColor: true, italic: true, children: _jsx(HintLine, { text: t('hint-confirm-exit') }) })] }));
|
|
27
27
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* `/permission` sandbox-preset picker. The command itself is registered by
|
|
4
|
+
* the harness side (dsh-sandbox-policy / dsh-base permission-presets row);
|
|
5
|
+
* bare `/permission` opens this picker and Enter dispatches
|
|
6
|
+
* `/permission <preset>` through the same external-command path a hand-typed
|
|
7
|
+
* argument takes. The list mirrors the `sandbox/mode` vocabulary the session
|
|
8
|
+
* modes use (`sessionModes.ts`), so the ✓ mark stays consistent with the
|
|
9
|
+
* mode the TUI derives from the session log.
|
|
10
|
+
*/
|
|
11
|
+
export declare const PERMISSION_PRESET_IDS: readonly ["read-only", "workspace-write", "danger-full-access"];
|
|
12
|
+
export type PermissionPresetId = (typeof PERMISSION_PRESET_IDS)[number];
|
|
13
|
+
export declare function PermissionsPicker({ focusIndex, currentMode, cwd, onPick, }: {
|
|
14
|
+
focusIndex: number;
|
|
15
|
+
currentMode: string | undefined;
|
|
16
|
+
cwd: string;
|
|
17
|
+
/** Mouse pick (fullscreen): clicked row's absolute index (Chat applies
|
|
18
|
+
* the same code path as the keyboard Enter). */
|
|
19
|
+
onPick?: (index: number) => void;
|
|
20
|
+
}): React.ReactNode;
|
|
21
|
+
//# sourceMappingURL=PermissionsPicker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PermissionsPicker.d.ts","sourceRoot":"","sources":["../../../src/components/PermissionsPicker.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAOzB;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,iEAIxB,CAAA;AAEV,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAA;AAEvE,wBAAgB,iBAAiB,CAAC,EAChC,UAAU,EACV,WAAW,EACX,GAAG,EACH,MAAM,GACP,EAAE;IACD,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,MAAM,GAAG,SAAS,CAAA;IAC/B,GAAG,EAAE,MAAM,CAAA;IACX;qDACiD;IACjD,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CACjC,GAAG,KAAK,CAAC,SAAS,CA4BlB"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { t } from '../i18n.js';
|
|
3
|
+
import { Box, Text } from '../ui.js';
|
|
4
|
+
import { Pane } from './design-system/Pane.js';
|
|
5
|
+
import { Select } from './Select.js';
|
|
6
|
+
import { HintLine } from './design-system/HintLine.js';
|
|
7
|
+
/**
|
|
8
|
+
* `/permission` sandbox-preset picker. The command itself is registered by
|
|
9
|
+
* the harness side (dsh-sandbox-policy / dsh-base permission-presets row);
|
|
10
|
+
* bare `/permission` opens this picker and Enter dispatches
|
|
11
|
+
* `/permission <preset>` through the same external-command path a hand-typed
|
|
12
|
+
* argument takes. The list mirrors the `sandbox/mode` vocabulary the session
|
|
13
|
+
* modes use (`sessionModes.ts`), so the ✓ mark stays consistent with the
|
|
14
|
+
* mode the TUI derives from the session log.
|
|
15
|
+
*/
|
|
16
|
+
export const PERMISSION_PRESET_IDS = [
|
|
17
|
+
'read-only',
|
|
18
|
+
'workspace-write',
|
|
19
|
+
'danger-full-access',
|
|
20
|
+
];
|
|
21
|
+
export function PermissionsPicker({ focusIndex, currentMode, cwd, onPick, }) {
|
|
22
|
+
return (_jsx(Pane, { color: "permission", children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "remember", bold: true, children: t('permission-picker-title') }) }), _jsx(Select, { options: [
|
|
23
|
+
{ value: 'read-only', label: t('permission-preset-readonly'), description: t('permission-preset-readonly-desc') },
|
|
24
|
+
{ value: 'workspace-write', label: t('permission-preset-workspace-write'), description: t('permission-preset-workspace-write-desc') },
|
|
25
|
+
{ value: 'danger-full-access', label: t('permission-preset-full-access'), description: t('permission-preset-full-access-desc') },
|
|
26
|
+
], focusIndex: focusIndex, selectedValue: currentMode, onPick: onPick ? index => onPick(index) : undefined }), _jsx(Text, { dimColor: true, italic: true, children: _jsx(HintLine, { text: t('hint-confirm-exit') }) }), _jsx(Text, { dimColor: true, children: t('permission-root-hint', { cwd }) })] }) }));
|
|
27
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* `/plan` on/off picker. The command itself is registered by dsh-plan-mode
|
|
4
|
+
* on the harness side; bare `/plan` opens this picker (with the current
|
|
5
|
+
* state marked) and Enter dispatches `/plan` or `/plan off` through the
|
|
6
|
+
* same external-command path a hand-typed argument takes.
|
|
7
|
+
*/
|
|
8
|
+
export declare function PlanPicker({ focusIndex, currentOn, onPick, }: {
|
|
9
|
+
focusIndex: number;
|
|
10
|
+
currentOn: boolean;
|
|
11
|
+
/** Mouse pick (fullscreen): clicked row's absolute index (Chat applies
|
|
12
|
+
* the same code path as the keyboard Enter). */
|
|
13
|
+
onPick?: (index: number) => void;
|
|
14
|
+
}): React.ReactNode;
|
|
15
|
+
//# sourceMappingURL=PlanPicker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PlanPicker.d.ts","sourceRoot":"","sources":["../../../src/components/PlanPicker.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAOzB;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,EACzB,UAAU,EACV,SAAS,EACT,MAAM,GACP,EAAE;IACD,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,OAAO,CAAA;IAClB;qDACiD;IACjD,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CACjC,GAAG,KAAK,CAAC,SAAS,CAwBlB"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { t } from '../i18n.js';
|
|
3
|
+
import { Box, Text } from '../ui.js';
|
|
4
|
+
import { Pane } from './design-system/Pane.js';
|
|
5
|
+
import { Select } from './Select.js';
|
|
6
|
+
import { HintLine } from './design-system/HintLine.js';
|
|
7
|
+
/**
|
|
8
|
+
* `/plan` on/off picker. The command itself is registered by dsh-plan-mode
|
|
9
|
+
* on the harness side; bare `/plan` opens this picker (with the current
|
|
10
|
+
* state marked) and Enter dispatches `/plan` or `/plan off` through the
|
|
11
|
+
* same external-command path a hand-typed argument takes.
|
|
12
|
+
*/
|
|
13
|
+
export function PlanPicker({ focusIndex, currentOn, onPick, }) {
|
|
14
|
+
return (_jsx(Pane, { color: "permission", children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "remember", bold: true, children: t('plan-picker-title') }) }), _jsx(Select, { options: [
|
|
15
|
+
{ value: 'on', label: t('plan-mode-on'), description: t('plan-mode-on-desc') },
|
|
16
|
+
{ value: 'off', label: t('plan-mode-off'), description: t('plan-mode-off-desc') },
|
|
17
|
+
], focusIndex: focusIndex, selectedValue: currentOn ? 'on' : 'off', onPick: onPick ? index => onPick(index) : undefined }), _jsx(Text, { dimColor: true, italic: true, children: _jsx(HintLine, { text: t('hint-confirm-exit') }) })] }) }));
|
|
18
|
+
}
|
|
@@ -9,9 +9,12 @@ import type { PresetOption } from '../dsh-adapter/channel.js';
|
|
|
9
9
|
* roster's discovery contract) but marked with their reason; the roster
|
|
10
10
|
* default is tagged.
|
|
11
11
|
*/
|
|
12
|
-
export declare function PresetPicker({ presets, focusIndex, currentPreset, }: {
|
|
12
|
+
export declare function PresetPicker({ presets, focusIndex, currentPreset, onPick, }: {
|
|
13
13
|
presets: readonly PresetOption[];
|
|
14
14
|
focusIndex: number;
|
|
15
15
|
currentPreset: string | undefined;
|
|
16
|
+
/** Mouse pick (fullscreen): clicked row's absolute index (Chat applies
|
|
17
|
+
* the same code path as the keyboard Enter). */
|
|
18
|
+
onPick?: (index: number) => void;
|
|
16
19
|
}): React.ReactNode;
|
|
17
20
|
//# sourceMappingURL=PresetPicker.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PresetPicker.d.ts","sourceRoot":"","sources":["../../../src/components/PresetPicker.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAMzB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAA;AAE7D;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,EAC3B,OAAO,EACP,UAAU,EACV,aAAa,
|
|
1
|
+
{"version":3,"file":"PresetPicker.d.ts","sourceRoot":"","sources":["../../../src/components/PresetPicker.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAMzB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAA;AAE7D;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,EAC3B,OAAO,EACP,UAAU,EACV,aAAa,EACb,MAAM,GACP,EAAE;IACD,OAAO,EAAE,SAAS,YAAY,EAAE,CAAA;IAChC,UAAU,EAAE,MAAM,CAAA;IAClB,aAAa,EAAE,MAAM,GAAG,SAAS,CAAA;IACjC;qDACiD;IACjD,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;CACjC,GAAG,KAAK,CAAC,SAAS,CA4BlB"}
|
|
@@ -13,12 +13,12 @@ import { HintLine } from './design-system/HintLine.js';
|
|
|
13
13
|
* roster's discovery contract) but marked with their reason; the roster
|
|
14
14
|
* default is tagged.
|
|
15
15
|
*/
|
|
16
|
-
export function PresetPicker({ presets, focusIndex, currentPreset, }) {
|
|
16
|
+
export function PresetPicker({ presets, focusIndex, currentPreset, onPick, }) {
|
|
17
17
|
return (_jsx(Pane, { color: "permission", children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: "remember", bold: true, children: "Agent preset" }) }), _jsx(Select, { options: presets.map(preset => ({
|
|
18
18
|
value: preset.id,
|
|
19
19
|
label: (preset.name ?? preset.id) +
|
|
20
20
|
(preset.isDefault ? t('preset-default-tag') : '') +
|
|
21
21
|
(preset.broken !== undefined ? t('preset-broken-tag') : ''),
|
|
22
22
|
description: preset.broken ?? preset.description ?? preset.id,
|
|
23
|
-
})), focusIndex: focusIndex, selectedValue: currentPreset }), _jsx(Text, { dimColor: true, italic: true, children: _jsx(HintLine, { text: t('hint-confirm-exit') }) })] }) }));
|
|
23
|
+
})), focusIndex: focusIndex, selectedValue: currentPreset, onPick: onPick ? index => onPick(index) : undefined }), _jsx(Text, { dimColor: true, italic: true, children: _jsx(HintLine, { text: t('hint-confirm-exit') }) })] }) }));
|
|
24
24
|
}
|
|
@@ -37,9 +37,10 @@ export interface PromptInputProps {
|
|
|
37
37
|
/**
|
|
38
38
|
* Claude Code style prompt input: rounded border box (top+bottom borders
|
|
39
39
|
* only), `❯ ` prompt char (dimmed while a turn is working), the text with a
|
|
40
|
-
* block cursor at the cursor position, and
|
|
41
|
-
* suggestion
|
|
42
|
-
*
|
|
40
|
+
* block cursor at the cursor position, and above it the slash-command /
|
|
41
|
+
* file-completion suggestion card (SuggestionCard: rounded panel with the
|
|
42
|
+
* selected row behind a `❯` pointer in the theme's `suggestion` color,
|
|
43
|
+
* mirroring Claude Code's PromptInputFooterSuggestions layout).
|
|
43
44
|
*
|
|
44
45
|
* Empty input: a solid block caret on a blank cell and nothing else — no
|
|
45
46
|
* placeholder text, so the terminal-painted IME preedit (pinyin) at the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PromptInput.d.ts","sourceRoot":"","sources":["../../../src/components/PromptInput.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"PromptInput.d.ts","sourceRoot":"","sources":["../../../src/components/PromptInput.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAiBzB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAA;AAmIxD;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,IAAI,OAAO,CAAA;IAClB,KAAK,IAAI,IAAI,CAAA;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAA;IAChB,0EAA0E;IAC1E,QAAQ,EAAE,OAAO,CAAA;IACjB,YAAY,IAAI,IAAI,CAAA;IACpB;;;OAGG;IACH,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAA;IACrD,6EAA6E;IAC7E,eAAe,EAAE,OAAO,CAAA;IACxB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,cAAc,CAAC,IAAI,IAAI,CAAA;IACvB,8EAA8E;IAC9E,eAAe,CAAC,IAAI,IAAI,CAAA;IACxB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAA;CACzD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,QAAQ,EACR,cAAc,EACd,eAAe,EACf,aAAa,GACd,EAAE,gBAAgB,qBAolClB"}
|
|
@@ -9,7 +9,10 @@ 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';
|
|
@@ -52,6 +55,84 @@ function wordBoundaryRight(text, cursor) {
|
|
|
52
55
|
index++;
|
|
53
56
|
return index;
|
|
54
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
|
+
}
|
|
55
136
|
/**
|
|
56
137
|
* The empty input deliberately shows NO placeholder text: terminal emulators
|
|
57
138
|
* paint the IME preedit (pinyin) at the physical cursor, which the app parks
|
|
@@ -66,9 +147,10 @@ const MAX_VISIBLE_LINES = 5;
|
|
|
66
147
|
/**
|
|
67
148
|
* Claude Code style prompt input: rounded border box (top+bottom borders
|
|
68
149
|
* only), `❯ ` prompt char (dimmed while a turn is working), the text with a
|
|
69
|
-
* block cursor at the cursor position, and
|
|
70
|
-
* suggestion
|
|
71
|
-
*
|
|
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).
|
|
72
154
|
*
|
|
73
155
|
* Empty input: a solid block caret on a blank cell and nothing else — no
|
|
74
156
|
* placeholder text, so the terminal-painted IME preedit (pinyin) at the
|
|
@@ -129,10 +211,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
129
211
|
React.useEffect(() => {
|
|
130
212
|
if (fillText && fillText !== lastFill.current) {
|
|
131
213
|
lastFill.current = fillText;
|
|
132
|
-
|
|
133
|
-
cursorRef.current = fillText.length;
|
|
134
|
-
setValue(fillText);
|
|
135
|
-
setCursor(fillText.length);
|
|
214
|
+
setInput(fillText);
|
|
136
215
|
onFillConsumed?.();
|
|
137
216
|
}
|
|
138
217
|
}, [fillText, onFillConsumed]);
|
|
@@ -153,10 +232,20 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
153
232
|
}, []);
|
|
154
233
|
const { columns, rows: terminalRows } = useTerminalSize();
|
|
155
234
|
const helpScrollRef = React.useRef(null);
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
|
|
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));
|
|
160
249
|
const suggestions = value.startsWith('/') ? channel.commandCompletions(value) : [];
|
|
161
250
|
const overlayOpen = suggestions.length > 0 &&
|
|
162
251
|
!helpOpen &&
|
|
@@ -203,7 +292,11 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
203
292
|
fileEscRef.current !== mention?.start;
|
|
204
293
|
const setInput = (next, cursorOffset = next.length) => {
|
|
205
294
|
valueRef.current = next;
|
|
206
|
-
|
|
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);
|
|
207
300
|
setValue(next);
|
|
208
301
|
setCursor(cursorRef.current);
|
|
209
302
|
};
|
|
@@ -388,6 +481,9 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
388
481
|
// the text/caret produced by the preceding event in that batch.
|
|
389
482
|
const value = valueRef.current;
|
|
390
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);
|
|
391
487
|
/** Insert text at the caret (typing, paste) and dismiss overlays. */
|
|
392
488
|
const insertAtCaret = (text) => {
|
|
393
489
|
if (helpOpen)
|
|
@@ -751,23 +847,27 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
751
847
|
return;
|
|
752
848
|
}
|
|
753
849
|
if (key.leftArrow) {
|
|
754
|
-
|
|
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));
|
|
755
853
|
return;
|
|
756
854
|
}
|
|
757
855
|
if (key.rightArrow) {
|
|
758
|
-
setInput(value,
|
|
856
|
+
setInput(value, nextGraphemeBoundary(bounds, cursor));
|
|
759
857
|
return;
|
|
760
858
|
}
|
|
761
859
|
if (key.backspace) {
|
|
762
860
|
if (cursor === 0)
|
|
763
861
|
return;
|
|
764
|
-
|
|
862
|
+
const start = previousGraphemeBoundary(bounds, cursor);
|
|
863
|
+
setInput(value.slice(0, start) + value.slice(cursor), start);
|
|
765
864
|
return;
|
|
766
865
|
}
|
|
767
866
|
if (key.delete) {
|
|
768
|
-
|
|
867
|
+
const end = nextGraphemeBoundary(bounds, cursor);
|
|
868
|
+
if (end === cursor)
|
|
769
869
|
return;
|
|
770
|
-
setInput(value.slice(0, cursor) + value.slice(
|
|
870
|
+
setInput(value.slice(0, cursor) + value.slice(end), cursor);
|
|
771
871
|
return;
|
|
772
872
|
}
|
|
773
873
|
if (key.home) {
|
|
@@ -894,7 +994,10 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
894
994
|
// === Render: hard-wrap every logical line at the input width, then show
|
|
895
995
|
// the window of visual lines with the caret row always visible (CC's
|
|
896
996
|
// maxVisibleLines behavior with automatic wrapping).
|
|
897
|
-
|
|
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);
|
|
898
1001
|
const visualLines = wrapToWidth(value, inputWidth);
|
|
899
1002
|
const caretVisualLine = wrapToWidth(value.slice(0, cursor), inputWidth).length - 1;
|
|
900
1003
|
const windowStart = Math.max(0, Math.min(caretVisualLine - MAX_VISIBLE_LINES + 1, visualLines.length - MAX_VISIBLE_LINES));
|
|
@@ -922,13 +1025,38 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
922
1025
|
if (absoluteLine !== caretVisualLine) {
|
|
923
1026
|
return (_jsx(Text, { wrap: "truncate-end", children: line }, absoluteLine));
|
|
924
1027
|
}
|
|
925
|
-
// 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.
|
|
926
1034
|
const col = caretCharCol();
|
|
1035
|
+
const clusterEnd = nextGraphemeBoundary(graphemeBoundaries(line), col);
|
|
927
1036
|
const before = line.slice(0, col);
|
|
928
|
-
const at = line
|
|
929
|
-
const after = line.slice(
|
|
1037
|
+
const at = clusterEnd > col ? line.slice(col, clusterEnd) : ' ';
|
|
1038
|
+
const after = line.slice(clusterEnd);
|
|
930
1039
|
return (_jsxs(Text, { wrap: "truncate-end", children: [before, _jsx(Text, { inverse: true, children: at }), after] }, absoluteLine));
|
|
931
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]);
|
|
932
1060
|
const lastNotification = channel.notifications[channel.notifications.length - 1];
|
|
933
1061
|
// Park the native terminal cursor at the input caret (via the renderer's
|
|
934
1062
|
// cursor-declaration mechanism). Terminal emulators render IME preedit
|
|
@@ -939,27 +1067,73 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
939
1067
|
// relative to the value box the ref attaches to.
|
|
940
1068
|
const valueBoxRef = useDeclaredCursor({
|
|
941
1069
|
line: caretVisualLine - windowStart,
|
|
942
|
-
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),
|
|
943
1074
|
active: !selectionActive,
|
|
944
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]);
|
|
945
1088
|
// 浮层整体挂载条件:与内部面板可见条件精确同值。关闭时必须把整个
|
|
946
1089
|
// absolute 浮层移除——渲染器的 absolute-removed 检测只看被移除节点自身
|
|
947
1090
|
// 的 style.position,常驻浮层 + 移除普通子节点不会触发 blit 解毒,被
|
|
948
1091
|
// 覆盖的转录行会留空(见 Chat.tsx dialogOverlayOpen 注释)。
|
|
949
1092
|
const floatersOpen = helpOpen || channel.pending.length > 0 || fileOverlayOpen || overlayOpen;
|
|
950
|
-
|
|
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
|
|
951
1100
|
.filter(item => item.placement === 'steer')
|
|
952
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
|
|
953
1102
|
.filter(item => item.placement === 'followup')
|
|
954
|
-
.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 })) })] }) })] }));
|
|
955
1124
|
}
|
|
956
1125
|
/**
|
|
957
1126
|
* Hard-wrap text into visual rows of at most `width` columns (CJK-aware via
|
|
958
1127
|
* stringWidth). Used by the input renderer so long lines wrap instead of
|
|
959
|
-
* 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).
|
|
960
1133
|
*/
|
|
961
1134
|
function wrapToWidth(text, width) {
|
|
962
1135
|
const rows = [];
|
|
1136
|
+
const segmenter = getGraphemeSegmenter();
|
|
963
1137
|
for (const line of text.split('\n')) {
|
|
964
1138
|
if (line === '') {
|
|
965
1139
|
rows.push('');
|
|
@@ -967,15 +1141,15 @@ function wrapToWidth(text, width) {
|
|
|
967
1141
|
}
|
|
968
1142
|
let current = '';
|
|
969
1143
|
let currentWidth = 0;
|
|
970
|
-
for (const
|
|
971
|
-
const w = stringWidth(
|
|
1144
|
+
for (const { segment } of segmenter.segment(line)) {
|
|
1145
|
+
const w = stringWidth(segment);
|
|
972
1146
|
if (currentWidth + w > width && current !== '') {
|
|
973
1147
|
rows.push(current);
|
|
974
|
-
current =
|
|
1148
|
+
current = segment;
|
|
975
1149
|
currentWidth = w;
|
|
976
1150
|
}
|
|
977
1151
|
else {
|
|
978
|
-
current +=
|
|
1152
|
+
current += segment;
|
|
979
1153
|
currentWidth += w;
|
|
980
1154
|
}
|
|
981
1155
|
}
|
|
@@ -983,3 +1157,48 @@ function wrapToWidth(text, width) {
|
|
|
983
1157
|
}
|
|
984
1158
|
return rows;
|
|
985
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
|