@deepseek-harness-tui/dsh-tui 0.8.8 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -238
- package/bin/dsh-tui.js +172 -5
- package/cordis.patch.yml +16 -4
- package/dsh-ecosystem-spec/protocols/profile-definitions.d.ts +13 -13
- package/dsh-ecosystem-spec/protocols/profile-definitions.js +59 -59
- package/dsh-ecosystem-spec/protocols/tui-channel-http.d.ts +15 -15
- package/dsh-ecosystem-spec/protocols/tui-channel-http.js +32 -32
- package/dsh-ecosystem-spec/protocols/tui-channel.d.ts +59 -59
- package/dsh-ecosystem-spec/protocols/tui-channel.js +166 -166
- package/dsh-ecosystem-spec/protocols/tui-contributions.d.ts +42 -42
- package/dsh-ecosystem-spec/protocols/tui-contributions.js +143 -143
- package/dsh-ecosystem-spec/registry/README.md +14 -14
- package/dsh-ecosystem-spec/registry/host-descriptor.tui.example.json +56 -56
- package/dsh-ecosystem-spec/registry/permissions-0.1.json +13 -13
- package/dsh-ecosystem-spec/registry/registry-0.15.json +149 -149
- package/lib/types/cc/hyperlink.d.ts +7 -1
- package/lib/types/cc/hyperlink.d.ts.map +1 -1
- package/lib/types/cc/hyperlink.js +3 -2
- package/lib/types/cc/markdown.d.ts.map +1 -1
- package/lib/types/cc/markdown.js +33 -3
- package/lib/types/cc/sessionColors.d.ts +27 -0
- package/lib/types/cc/sessionColors.d.ts.map +1 -0
- package/lib/types/cc/sessionColors.js +39 -0
- package/lib/types/commands.d.ts.map +1 -1
- package/lib/types/commands.js +10 -2
- package/lib/types/components/ActivityPicker.d.ts +4 -1
- package/lib/types/components/ActivityPicker.d.ts.map +1 -1
- package/lib/types/components/ActivityPicker.js +2 -2
- package/lib/types/components/AutoRecapRow.d.ts +23 -0
- package/lib/types/components/AutoRecapRow.d.ts.map +1 -0
- package/lib/types/components/AutoRecapRow.js +23 -0
- package/lib/types/components/BtwPanel.d.ts.map +1 -1
- package/lib/types/components/BtwPanel.js +1 -1
- package/lib/types/components/Chat/SubagentMessage.d.ts +2 -1
- package/lib/types/components/Chat/SubagentMessage.d.ts.map +1 -1
- package/lib/types/components/Chat/SubagentMessage.js +11 -3
- package/lib/types/components/ColorPicker.d.ts +17 -0
- package/lib/types/components/ColorPicker.d.ts.map +1 -0
- package/lib/types/components/ColorPicker.js +20 -0
- package/lib/types/components/CommandSuggestions.d.ts +23 -4
- package/lib/types/components/CommandSuggestions.d.ts.map +1 -1
- package/lib/types/components/CommandSuggestions.js +41 -14
- package/lib/types/components/ContextBarView.d.ts +33 -0
- package/lib/types/components/ContextBarView.d.ts.map +1 -0
- package/lib/types/components/ContextBarView.js +46 -0
- package/lib/types/components/EffortInputBorder.d.ts +12 -1
- package/lib/types/components/EffortInputBorder.d.ts.map +1 -1
- package/lib/types/components/EffortInputBorder.js +22 -5
- package/lib/types/components/EffortSlider.d.ts +4 -1
- package/lib/types/components/EffortSlider.d.ts.map +1 -1
- package/lib/types/components/EffortSlider.js +4 -2
- package/lib/types/components/ExtensionDialog.js +6 -2
- package/lib/types/components/FileActionsPanel.d.ts +22 -0
- package/lib/types/components/FileActionsPanel.d.ts.map +1 -0
- package/lib/types/components/FileActionsPanel.js +28 -0
- package/lib/types/components/FileSuggestions.d.ts +26 -7
- package/lib/types/components/FileSuggestions.d.ts.map +1 -1
- package/lib/types/components/FileSuggestions.js +39 -12
- package/lib/types/components/GoalTodoPanel.d.ts +26 -2
- package/lib/types/components/GoalTodoPanel.d.ts.map +1 -1
- package/lib/types/components/GoalTodoPanel.js +86 -17
- package/lib/types/components/HelpMenu.d.ts +7 -1
- package/lib/types/components/HelpMenu.d.ts.map +1 -1
- package/lib/types/components/HelpMenu.js +12 -4
- package/lib/types/components/HistorySearchDialog.d.ts +4 -1
- package/lib/types/components/HistorySearchDialog.d.ts.map +1 -1
- package/lib/types/components/HistorySearchDialog.js +2 -2
- package/lib/types/components/LangPicker.d.ts +14 -0
- package/lib/types/components/LangPicker.d.ts.map +1 -0
- package/lib/types/components/LangPicker.js +17 -0
- package/lib/types/components/LoadedContextPanel.d.ts.map +1 -1
- package/lib/types/components/LoadedContextPanel.js +3 -1
- package/lib/types/components/Markdown.d.ts.map +1 -1
- package/lib/types/components/Markdown.js +12 -3
- package/lib/types/components/MessageList.d.ts +45 -2
- package/lib/types/components/MessageList.d.ts.map +1 -1
- package/lib/types/components/MessageList.js +560 -41
- package/lib/types/components/ModelPicker.d.ts +27 -2
- package/lib/types/components/ModelPicker.d.ts.map +1 -1
- package/lib/types/components/ModelPicker.js +26 -6
- package/lib/types/components/PermissionsPicker.d.ts +21 -0
- package/lib/types/components/PermissionsPicker.d.ts.map +1 -0
- package/lib/types/components/PermissionsPicker.js +27 -0
- package/lib/types/components/PlanPicker.d.ts +15 -0
- package/lib/types/components/PlanPicker.d.ts.map +1 -0
- package/lib/types/components/PlanPicker.js +18 -0
- package/lib/types/components/PresetPicker.d.ts +4 -1
- package/lib/types/components/PresetPicker.d.ts.map +1 -1
- package/lib/types/components/PresetPicker.js +2 -2
- package/lib/types/components/PromptInput.d.ts +4 -3
- package/lib/types/components/PromptInput.d.ts.map +1 -1
- package/lib/types/components/PromptInput.js +602 -75
- package/lib/types/components/RecapPanel.d.ts +25 -0
- package/lib/types/components/RecapPanel.d.ts.map +1 -0
- package/lib/types/components/RecapPanel.js +55 -0
- package/lib/types/components/RewindPicker.d.ts +12 -1
- package/lib/types/components/RewindPicker.d.ts.map +1 -1
- package/lib/types/components/RewindPicker.js +6 -4
- package/lib/types/components/ScrollbarGutter.d.ts +28 -0
- package/lib/types/components/ScrollbarGutter.d.ts.map +1 -0
- package/lib/types/components/ScrollbarGutter.js +119 -0
- package/lib/types/components/Select.d.ts +8 -1
- package/lib/types/components/Select.d.ts.map +1 -1
- package/lib/types/components/Select.js +4 -2
- package/lib/types/components/SkillsPicker.d.ts +4 -1
- package/lib/types/components/SkillsPicker.d.ts.map +1 -1
- package/lib/types/components/SkillsPicker.js +2 -2
- package/lib/types/components/SubagentCard.d.ts +2 -1
- package/lib/types/components/SubagentCard.d.ts.map +1 -1
- package/lib/types/components/SubagentCard.js +9 -2
- package/lib/types/components/SubagentDashboard.d.ts +4 -0
- package/lib/types/components/SubagentDashboard.d.ts.map +1 -1
- package/lib/types/components/SubagentDashboard.js +9 -1
- package/lib/types/components/SubagentDetailScene.d.ts.map +1 -1
- package/lib/types/components/SubagentDetailScene.js +5 -4
- package/lib/types/components/SuggestionCard.d.ts +65 -0
- package/lib/types/components/SuggestionCard.d.ts.map +1 -0
- package/lib/types/components/SuggestionCard.js +77 -0
- package/lib/types/components/ThemePicker.d.ts +4 -1
- package/lib/types/components/ThemePicker.d.ts.map +1 -1
- package/lib/types/components/ThemePicker.js +2 -2
- package/lib/types/components/ThinkingToggle.d.ts +4 -1
- package/lib/types/components/ThinkingToggle.d.ts.map +1 -1
- package/lib/types/components/ThinkingToggle.js +2 -2
- package/lib/types/components/TimelineRail.d.ts +53 -0
- package/lib/types/components/TimelineRail.d.ts.map +1 -0
- package/lib/types/components/TimelineRail.js +197 -0
- package/lib/types/components/WorkspaceFlowPicker.d.ts +6 -1
- package/lib/types/components/WorkspaceFlowPicker.d.ts.map +1 -1
- package/lib/types/components/WorkspaceFlowPicker.js +4 -2
- package/lib/types/components/WorkspaceMenuPicker.d.ts +19 -0
- package/lib/types/components/WorkspaceMenuPicker.d.ts.map +1 -0
- package/lib/types/components/WorkspaceMenuPicker.js +19 -0
- package/lib/types/components/WorkspacePicker.d.ts +4 -1
- package/lib/types/components/WorkspacePicker.d.ts.map +1 -1
- package/lib/types/components/WorkspacePicker.js +2 -2
- package/lib/types/components/approvals/ApprovalPanel.d.ts.map +1 -1
- package/lib/types/components/approvals/ApprovalPanel.js +5 -1
- package/lib/types/components/design-system/ListItem.d.ts +8 -1
- package/lib/types/components/design-system/ListItem.d.ts.map +1 -1
- package/lib/types/components/design-system/ListItem.js +8 -3
- package/lib/types/components/design-system/ThemeProvider.d.ts +7 -0
- package/lib/types/components/design-system/ThemeProvider.d.ts.map +1 -1
- package/lib/types/components/design-system/ThemeProvider.js +2 -1
- package/lib/types/components/messages/AssistantTextMessage.d.ts +5 -2
- package/lib/types/components/messages/AssistantTextMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantTextMessage.js +6 -2
- package/lib/types/components/messages/AssistantThinkingMessage.d.ts +2 -1
- package/lib/types/components/messages/AssistantThinkingMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantThinkingMessage.js +10 -4
- package/lib/types/components/messages/AssistantToolUseMessage.d.ts +15 -1
- package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantToolUseMessage.js +37 -7
- package/lib/types/components/messages/UserPromptMessage.d.ts +2 -1
- package/lib/types/components/messages/UserPromptMessage.d.ts.map +1 -1
- package/lib/types/components/questions/AskUserQuestionPanel.d.ts +7 -3
- package/lib/types/components/questions/AskUserQuestionPanel.d.ts.map +1 -1
- package/lib/types/components/questions/AskUserQuestionPanel.js +75 -11
- package/lib/types/components/questions/PlanReviewPanel.d.ts.map +1 -1
- package/lib/types/components/questions/PlanReviewPanel.js +13 -2
- package/lib/types/components/sessions/SessionListRow.d.ts +4 -1
- package/lib/types/components/sessions/SessionListRow.d.ts.map +1 -1
- package/lib/types/components/sessions/SessionListRow.js +4 -2
- package/lib/types/components/trajectory/HotspotView.d.ts +7 -1
- package/lib/types/components/trajectory/HotspotView.d.ts.map +1 -1
- package/lib/types/components/trajectory/HotspotView.js +8 -2
- package/lib/types/components/trajectory/Ledger.d.ts +9 -1
- package/lib/types/components/trajectory/Ledger.d.ts.map +1 -1
- package/lib/types/components/trajectory/Ledger.js +23 -5
- package/lib/types/components/trajectory/WaveBand.d.ts +10 -1
- package/lib/types/components/trajectory/WaveBand.d.ts.map +1 -1
- package/lib/types/components/trajectory/WaveBand.js +7 -2
- package/lib/types/dsh-adapter/channel.d.ts +117 -7
- package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel.js +1301 -122
- package/lib/types/dsh-adapter/compat/index.d.ts +8 -6
- package/lib/types/dsh-adapter/compat/index.d.ts.map +1 -1
- package/lib/types/dsh-adapter/compat/index.js +8 -6
- package/lib/types/dsh-adapter/compat/sessionLog.d.ts +150 -0
- package/lib/types/dsh-adapter/compat/sessionLog.d.ts.map +1 -1
- package/lib/types/dsh-adapter/compat/sessionLog.js +518 -41
- package/lib/types/dsh-adapter/contract.d.ts +1 -1
- package/lib/types/dsh-adapter/contract.d.ts.map +1 -1
- package/lib/types/dsh-adapter/contract.js +1 -0
- package/lib/types/dsh-adapter/extension-events.d.ts +1 -1
- package/lib/types/dsh-adapter/extension-events.d.ts.map +1 -1
- package/lib/types/dsh-adapter/index.d.ts +20 -2
- package/lib/types/dsh-adapter/index.d.ts.map +1 -1
- package/lib/types/dsh-adapter/index.js +9 -1
- package/lib/types/dsh-adapter/plugin.d.ts +0 -9
- package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
- package/lib/types/dsh-adapter/plugin.js +453 -14
- package/lib/types/dsh-adapter/presets.d.ts +18 -0
- package/lib/types/dsh-adapter/presets.d.ts.map +1 -1
- package/lib/types/dsh-adapter/presets.js +31 -0
- package/lib/types/dsh-adapter/providerWizard.d.ts +29 -1
- package/lib/types/dsh-adapter/providerWizard.d.ts.map +1 -1
- package/lib/types/dsh-adapter/providerWizard.js +89 -5
- package/lib/types/dsh-adapter/questions.d.ts +19 -2
- package/lib/types/dsh-adapter/questions.d.ts.map +1 -1
- package/lib/types/dsh-adapter/questions.js +53 -10
- package/lib/types/dsh-adapter/recap.d.ts +49 -0
- package/lib/types/dsh-adapter/recap.d.ts.map +1 -0
- package/lib/types/dsh-adapter/recap.js +121 -0
- package/lib/types/dsh-adapter/sessionTree.d.ts +314 -0
- package/lib/types/dsh-adapter/sessionTree.d.ts.map +1 -0
- package/lib/types/dsh-adapter/sessionTree.js +954 -0
- package/lib/types/dsh-adapter/shortcuts.d.ts +15 -44
- package/lib/types/dsh-adapter/shortcuts.d.ts.map +1 -1
- package/lib/types/dsh-adapter/shortcuts.js +28 -165
- package/lib/types/dsh-adapter/types.d.ts +1 -1
- package/lib/types/dsh-adapter/types.d.ts.map +1 -1
- package/lib/types/herdr.js +1 -1
- package/lib/types/i18n.d.ts +770 -181
- package/lib/types/i18n.d.ts.map +1 -1
- package/lib/types/i18n.js +257 -80
- package/lib/types/ink/components/AlternateScreen.d.ts.map +1 -1
- package/lib/types/ink/components/AlternateScreen.js +13 -1
- package/lib/types/ink/components/App.d.ts +12 -1
- package/lib/types/ink/components/App.d.ts.map +1 -1
- package/lib/types/ink/components/App.js +101 -4
- package/lib/types/ink/components/ClockContext.d.ts.map +1 -1
- package/lib/types/ink/components/ClockContext.js +2 -0
- package/lib/types/ink/components/ScrollBox.d.ts.map +1 -1
- package/lib/types/ink/components/ScrollBox.js +170 -91
- package/lib/types/ink/constants.d.ts +9 -0
- package/lib/types/ink/constants.d.ts.map +1 -1
- package/lib/types/ink/constants.js +9 -0
- package/lib/types/ink/events/click-event.d.ts +7 -16
- package/lib/types/ink/events/click-event.d.ts.map +1 -1
- package/lib/types/ink/events/click-event.js +16 -19
- package/lib/types/ink/events/event-handlers.d.ts +11 -1
- package/lib/types/ink/events/event-handlers.d.ts.map +1 -1
- package/lib/types/ink/events/event-handlers.js +2 -0
- package/lib/types/ink/events/input-event.d.ts +9 -0
- package/lib/types/ink/events/input-event.d.ts.map +1 -1
- package/lib/types/ink/events/input-event.js +4 -0
- package/lib/types/ink/events/pointer-event.d.ts +61 -0
- package/lib/types/ink/events/pointer-event.d.ts.map +1 -0
- package/lib/types/ink/events/pointer-event.js +61 -0
- package/lib/types/ink/events/wheel-event.d.ts +17 -0
- package/lib/types/ink/events/wheel-event.d.ts.map +1 -0
- package/lib/types/ink/events/wheel-event.js +20 -0
- package/lib/types/ink/geometry-trace.d.ts +51 -0
- package/lib/types/ink/geometry-trace.d.ts.map +1 -0
- package/lib/types/ink/geometry-trace.js +83 -0
- package/lib/types/ink/hit-test.d.ts +56 -1
- package/lib/types/ink/hit-test.d.ts.map +1 -1
- package/lib/types/ink/hit-test.js +146 -8
- package/lib/types/ink/ink.d.ts +58 -1
- package/lib/types/ink/ink.d.ts.map +1 -1
- package/lib/types/ink/ink.js +230 -29
- package/lib/types/ink/log-update.d.ts.map +1 -1
- package/lib/types/ink/log-update.js +21 -0
- package/lib/types/ink/node-cache.d.ts +5 -0
- package/lib/types/ink/node-cache.d.ts.map +1 -1
- package/lib/types/ink/parse-keypress.d.ts +8 -0
- package/lib/types/ink/parse-keypress.d.ts.map +1 -1
- package/lib/types/ink/parse-keypress.js +101 -12
- package/lib/types/ink/render-node-to-output.d.ts +10 -0
- package/lib/types/ink/render-node-to-output.d.ts.map +1 -1
- package/lib/types/ink/render-node-to-output.js +111 -18
- package/lib/types/ink/timeline-rail.d.ts +138 -0
- package/lib/types/ink/timeline-rail.d.ts.map +1 -0
- package/lib/types/ink/timeline-rail.js +179 -0
- package/lib/types/ink/wrap-text.d.ts.map +1 -1
- package/lib/types/ink/wrap-text.js +47 -5
- package/lib/types/migrationPrefs.d.ts +60 -0
- package/lib/types/migrationPrefs.d.ts.map +1 -0
- package/lib/types/migrationPrefs.js +97 -0
- package/lib/types/modelGroups.d.ts +72 -0
- package/lib/types/modelGroups.d.ts.map +1 -0
- package/lib/types/modelGroups.js +97 -0
- package/lib/types/modelRecents.d.ts +39 -0
- package/lib/types/modelRecents.d.ts.map +1 -0
- package/lib/types/modelRecents.js +97 -0
- package/lib/types/native-ts/yoga-layout/index.d.ts +11 -0
- package/lib/types/native-ts/yoga-layout/index.d.ts.map +1 -1
- package/lib/types/native-ts/yoga-layout/index.js +31 -1
- package/lib/types/oauth.d.ts +15 -0
- package/lib/types/oauth.d.ts.map +1 -0
- package/lib/types/oauth.js +14 -0
- package/lib/types/reload.d.ts +109 -0
- package/lib/types/reload.d.ts.map +1 -0
- package/lib/types/reload.js +118 -0
- package/lib/types/screens/Chat.d.ts +3 -1
- package/lib/types/screens/Chat.d.ts.map +1 -1
- package/lib/types/screens/Chat.js +1377 -371
- package/lib/types/screens/SessionBrowser.d.ts.map +1 -1
- package/lib/types/screens/SessionBrowser.js +56 -29
- package/lib/types/screens/SessionTree.d.ts +27 -0
- package/lib/types/screens/SessionTree.d.ts.map +1 -0
- package/lib/types/screens/SessionTree.js +555 -0
- package/lib/types/screens/Settings.d.ts.map +1 -1
- package/lib/types/screens/Settings.js +64 -22
- package/lib/types/screens/StatusLine.d.ts +0 -8
- package/lib/types/screens/StatusLine.d.ts.map +1 -1
- package/lib/types/screens/StatusLine.js +224 -45
- package/lib/types/screens/StatusMetrics.d.ts +35 -3
- package/lib/types/screens/StatusMetrics.d.ts.map +1 -1
- package/lib/types/screens/StatusMetrics.js +63 -8
- package/lib/types/screens/TrajectoryScene.d.ts.map +1 -1
- package/lib/types/screens/TrajectoryScene.js +81 -14
- package/lib/types/screens/chatOverlay.d.ts +237 -0
- package/lib/types/screens/chatOverlay.d.ts.map +1 -0
- package/lib/types/screens/chatOverlay.js +116 -0
- package/lib/types/tips.d.ts.map +1 -1
- package/lib/types/tips.js +64 -10
- package/lib/types/tuiDisplayPrefs.d.ts +15 -0
- package/lib/types/tuiDisplayPrefs.d.ts.map +1 -1
- package/lib/types/tuiDisplayPrefs.js +9 -0
- package/lib/types/update.d.ts +139 -0
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +382 -30
- package/lib/types/utils/debug.d.ts +10 -0
- package/lib/types/utils/debug.d.ts.map +1 -1
- package/lib/types/utils/debug.js +24 -0
- package/lib/types/utils/fileTarget.d.ts +46 -0
- package/lib/types/utils/fileTarget.d.ts.map +1 -0
- package/lib/types/utils/fileTarget.js +178 -0
- package/lib/types/utils/keymap.d.ts +134 -0
- package/lib/types/utils/keymap.d.ts.map +1 -0
- package/lib/types/utils/keymap.js +311 -0
- package/lib/types/utils/mentions.d.ts +25 -2
- package/lib/types/utils/mentions.d.ts.map +1 -1
- package/lib/types/utils/mentions.js +69 -10
- package/lib/types/utils/openExternal.d.ts +50 -0
- package/lib/types/utils/openExternal.d.ts.map +1 -0
- package/lib/types/utils/openExternal.js +170 -0
- package/lib/types/utils/paths.d.ts.map +1 -1
- package/lib/types/utils/paths.js +1 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/LICENSE +21 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/README.md +140 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/cordis.patch.yml +11 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/dsh-plugin.json +44 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/command.d.ts +14 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/command.js +67 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/credentials.d.ts +64 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/credentials.js +170 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/index.d.ts +79 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/index.js +196 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/interaction.d.ts +68 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/interaction.js +193 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/opener.d.ts +32 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/opener.js +101 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/profiles.d.ts +31 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/profiles.js +68 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/service.d.ts +69 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/service.js +129 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/package.json +70 -0
- package/node_modules/@dsh-std/command/CHANGELOG.md +8 -8
- package/node_modules/@dsh-std/command/LICENSE +21 -21
- package/node_modules/@dsh-std/command/README.md +26 -26
- package/node_modules/@dsh-std/command/README.zh.md +28 -28
- package/node_modules/@dsh-std/command/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/command/package.json +22 -22
- package/node_modules/@dsh-std/connection/CHANGELOG.md +8 -8
- package/node_modules/@dsh-std/connection/LICENSE +21 -21
- package/node_modules/@dsh-std/connection/README.md +42 -42
- package/node_modules/@dsh-std/connection/README.zh.md +42 -42
- package/node_modules/@dsh-std/connection/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/connection/lib/memory.js.map +1 -1
- package/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js.map +1 -1
- package/node_modules/@dsh-std/connection/package.json +20 -20
- package/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/node_modules/@dsh-std/core/LICENSE +21 -21
- package/node_modules/@dsh-std/core/README.md +13 -13
- package/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/core/package.json +21 -21
- package/node_modules/@dsh-std/manifest/CHANGELOG.md +9 -9
- package/node_modules/@dsh-std/manifest/LICENSE +21 -21
- package/node_modules/@dsh-std/manifest/README.md +5 -5
- package/node_modules/@dsh-std/manifest/README.zh.md +5 -5
- package/node_modules/@dsh-std/manifest/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/manifest/package.json +20 -20
- package/node_modules/@dsh-std/manifest/schema/dsh-plugin-0.15.schema.json +144 -144
- package/node_modules/@dsh-std/messages/CHANGELOG.md +8 -8
- package/node_modules/@dsh-std/messages/LICENSE +21 -21
- package/node_modules/@dsh-std/messages/README.md +5 -5
- package/node_modules/@dsh-std/messages/README.zh.md +7 -7
- package/node_modules/@dsh-std/messages/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/messages/package.json +20 -20
- package/node_modules/@dsh-std/messages/schema/message-observer-envelope-0.15.schema.json +54 -54
- package/node_modules/@dsh-std/presentation/CHANGELOG.md +9 -9
- package/node_modules/@dsh-std/presentation/LICENSE +21 -21
- package/node_modules/@dsh-std/presentation/README.md +27 -27
- package/node_modules/@dsh-std/presentation/README.zh.md +29 -29
- package/node_modules/@dsh-std/presentation/lib/callback.js.map +1 -1
- package/node_modules/@dsh-std/presentation/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/presentation/package.json +25 -25
- package/node_modules/@dsh-std/storage/CHANGELOG.md +8 -8
- package/node_modules/@dsh-std/storage/LICENSE +21 -21
- package/node_modules/@dsh-std/storage/README.md +5 -5
- package/node_modules/@dsh-std/storage/README.zh.md +7 -7
- package/node_modules/@dsh-std/storage/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/storage/package.json +20 -20
- package/node_modules/@dsh-std/storage/schema/local-storage-0.15.schema.json +56 -56
- package/package.json +19 -5
- package/presets/liangshen/.dsh-tui-managed.json +1 -1
- package/presets/liangshen/instruction-hint.mjs +8 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/LICENSE +21 -21
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/README.md +42 -42
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/README.zh.md +42 -42
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/memory.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/package.json +20 -20
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/LICENSE +21 -21
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/README.md +13 -13
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/package.json +21 -21
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/CHANGELOG.md +9 -9
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/LICENSE +21 -21
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/README.md +5 -5
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/README.zh.md +5 -5
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/package.json +20 -20
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/schema/dsh-plugin-0.15.schema.json +144 -144
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/CHANGELOG.md +9 -9
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/LICENSE +21 -21
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/README.md +27 -27
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/README.zh.md +29 -29
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/callback.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/package.json +25 -25
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/LICENSE +21 -21
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/README.md +13 -13
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/package.json +21 -21
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/LICENSE +21 -21
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/README.md +13 -13
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/package.json +21 -21
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/LICENSE +21 -21
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/README.md +13 -13
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/package.json +21 -21
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/LICENSE +21 -21
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/README.md +42 -42
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/README.zh.md +42 -42
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/memory.js.map +1 -1
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js.map +1 -1
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/package.json +20 -20
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/LICENSE +21 -21
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/README.md +13 -13
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/package.json +21 -21
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/LICENSE +21 -21
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/README.md +13 -13
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/package.json +21 -21
|
@@ -8,8 +8,13 @@ import { EffortChargeGlyph } from './EffortChargeGlyph.js';
|
|
|
8
8
|
import { EffortInputBorder } from './EffortInputBorder.js';
|
|
9
9
|
import { EffortTierBadge } from './EffortTierBadge.js';
|
|
10
10
|
import { isLightThemeActive } from '../theme.js';
|
|
11
|
+
import { sessionColorHex } from '../cc/sessionColors.js';
|
|
11
12
|
import { useDeclaredCursor } from '../ink/hooks/use-declared-cursor.js';
|
|
13
|
+
import { noteAuxNumber } from '../ink/geometry-trace.js';
|
|
14
|
+
import instances from '../ink/instances.js';
|
|
12
15
|
import { stringWidth } from '../ink/stringWidth.js';
|
|
16
|
+
import { truncateToWidth } from '../ink/truncateToWidth.js';
|
|
17
|
+
import { getGraphemeSegmenter } from '../utils/intl.js';
|
|
13
18
|
import { formatClipboardInsert, readClipboard } from '../utils/clipboard.js';
|
|
14
19
|
import { editInExternalEditor } from '../utils/externalEditor.js';
|
|
15
20
|
import { isHiddenCommandName, parseCommandName } from '../commands.js';
|
|
@@ -17,11 +22,25 @@ import { appendHistory } from '../history.js';
|
|
|
17
22
|
import { mentionAtCaret } from '../utils/mentions.js';
|
|
18
23
|
import { preserveSelection } from '../utils/fileSuggestions.js';
|
|
19
24
|
import { isMod } from '../utils/modifiers.js';
|
|
25
|
+
import { actionMatches } from '../utils/keymap.js';
|
|
20
26
|
import { CommandSuggestions } from './CommandSuggestions.js';
|
|
21
27
|
import { FileSuggestions } from './FileSuggestions.js';
|
|
22
28
|
import { HelpMenu } from './HelpMenu.js';
|
|
23
29
|
import { OverlayAbove } from './OverlayAbove.js';
|
|
30
|
+
import { SuggestionCard, cardContentWidth } from './SuggestionCard.js';
|
|
24
31
|
const HISTORY_LIMIT = 50;
|
|
32
|
+
/**
|
|
33
|
+
* Paste fold (CC-style collapse with a visible preview, no black box):
|
|
34
|
+
* a paste that leaves the input this big folds into a one-line chip
|
|
35
|
+
* showing the line/char count PLUS the first line of content. Hover peeks
|
|
36
|
+
* at the full text (window pinned to the head); clicking the chip — or
|
|
37
|
+
* the `▾` prefix on the first row while expanded — toggles the fold; Esc
|
|
38
|
+
* or any editing key unfolds first. Enter still submits the FULL text:
|
|
39
|
+
* folding never drops data.
|
|
40
|
+
*/
|
|
41
|
+
const FOLD_MIN_LINES = 6;
|
|
42
|
+
const FOLD_MIN_CHARS = 600;
|
|
43
|
+
const isBigInput = (text) => text.split('\n').length >= FOLD_MIN_LINES || text.length >= FOLD_MIN_CHARS;
|
|
25
44
|
function clipboardImageMediaType(path) {
|
|
26
45
|
if (/\.png$/iu.test(path))
|
|
27
46
|
return 'image/png';
|
|
@@ -52,6 +71,84 @@ function wordBoundaryRight(text, cursor) {
|
|
|
52
71
|
index++;
|
|
53
72
|
return index;
|
|
54
73
|
}
|
|
74
|
+
// --- grapheme-cluster geometry ---------------------------------------------
|
|
75
|
+
// The caret, editing keys, and wrapping MUST agree on one text unit. Mixing
|
|
76
|
+
// UTF-16 code units (arrows/backspace), code points (wrap), and display
|
|
77
|
+
// cells (stringWidth) lets the caret land inside a surrogate pair or a ZWJ
|
|
78
|
+
// emoji — the inverted caret then shows half a glyph, Backspace deletes
|
|
79
|
+
// half a character, and `line.slice()` splits clusters. All offsets below
|
|
80
|
+
// are UTF-16 indices snapped to grapheme boundaries via the shared
|
|
81
|
+
// Intl.Segmenter (utils/intl.ts).
|
|
82
|
+
/** Ascending grapheme boundary offsets of `text` (starts at 0, ends at
|
|
83
|
+
* `text.length`). Empty text yields `[0]`. */
|
|
84
|
+
function graphemeBoundaries(text) {
|
|
85
|
+
const bounds = [0];
|
|
86
|
+
for (const { index, segment } of getGraphemeSegmenter().segment(text)) {
|
|
87
|
+
const end = index + segment.length;
|
|
88
|
+
if (end > bounds[bounds.length - 1])
|
|
89
|
+
bounds.push(end);
|
|
90
|
+
}
|
|
91
|
+
return bounds;
|
|
92
|
+
}
|
|
93
|
+
/** Largest grapheme boundary `<= offset` (clamped into the text). Snaps a
|
|
94
|
+
* cursor that landed mid-cluster back onto a boundary. */
|
|
95
|
+
function boundaryAtOrBefore(bounds, offset) {
|
|
96
|
+
let lo = 0;
|
|
97
|
+
let hi = bounds.length - 1;
|
|
98
|
+
let ans = bounds[0];
|
|
99
|
+
while (lo <= hi) {
|
|
100
|
+
const mid = (lo + hi) >> 1;
|
|
101
|
+
if (bounds[mid] <= offset) {
|
|
102
|
+
ans = bounds[mid];
|
|
103
|
+
lo = mid + 1;
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
hi = mid - 1;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return ans;
|
|
110
|
+
}
|
|
111
|
+
/** Largest grapheme boundary strictly before `offset` (0 when none). */
|
|
112
|
+
function previousGraphemeBoundary(bounds, offset) {
|
|
113
|
+
let lo = 0;
|
|
114
|
+
let hi = bounds.length - 1;
|
|
115
|
+
let ans = 0;
|
|
116
|
+
while (lo <= hi) {
|
|
117
|
+
const mid = (lo + hi) >> 1;
|
|
118
|
+
if (bounds[mid] < offset) {
|
|
119
|
+
ans = bounds[mid];
|
|
120
|
+
lo = mid + 1;
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
hi = mid - 1;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return ans;
|
|
127
|
+
}
|
|
128
|
+
/** Smallest grapheme boundary strictly after `offset` (text.length when
|
|
129
|
+
* none). Returns `offset` unchanged when it already is the last boundary. */
|
|
130
|
+
function nextGraphemeBoundary(bounds, offset) {
|
|
131
|
+
let lo = 0;
|
|
132
|
+
let hi = bounds.length - 1;
|
|
133
|
+
let ans = bounds[hi] ?? 0;
|
|
134
|
+
while (lo <= hi) {
|
|
135
|
+
const mid = (lo + hi) >> 1;
|
|
136
|
+
if (bounds[mid] > offset) {
|
|
137
|
+
ans = bounds[mid];
|
|
138
|
+
hi = mid - 1;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
lo = mid + 1;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return ans;
|
|
145
|
+
}
|
|
146
|
+
/** Snap an arbitrary UTF-16 offset onto a grapheme boundary of `text`,
|
|
147
|
+
* clamping into range. The safety net under every cursor write. */
|
|
148
|
+
function normalizeCursorOffset(text, offset) {
|
|
149
|
+
const clamped = Math.max(0, Math.min(offset, text.length));
|
|
150
|
+
return boundaryAtOrBefore(graphemeBoundaries(text), clamped);
|
|
151
|
+
}
|
|
55
152
|
/**
|
|
56
153
|
* The empty input deliberately shows NO placeholder text: terminal emulators
|
|
57
154
|
* paint the IME preedit (pinyin) at the physical cursor, which the app parks
|
|
@@ -66,9 +163,10 @@ const MAX_VISIBLE_LINES = 5;
|
|
|
66
163
|
/**
|
|
67
164
|
* Claude Code style prompt input: rounded border box (top+bottom borders
|
|
68
165
|
* only), `❯ ` prompt char (dimmed while a turn is working), the text with a
|
|
69
|
-
* block cursor at the cursor position, and
|
|
70
|
-
* suggestion
|
|
71
|
-
*
|
|
166
|
+
* block cursor at the cursor position, and above it the slash-command /
|
|
167
|
+
* file-completion suggestion card (SuggestionCard: rounded panel with the
|
|
168
|
+
* selected row behind a `❯` pointer in the theme's `suggestion` color,
|
|
169
|
+
* mirroring Claude Code's PromptInputFooterSuggestions layout).
|
|
72
170
|
*
|
|
73
171
|
* Empty input: a solid block caret on a blank cell and nothing else — no
|
|
74
172
|
* placeholder text, so the terminal-painted IME preedit (pinyin) at the
|
|
@@ -97,6 +195,30 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
97
195
|
const [themeName] = useTheme();
|
|
98
196
|
const [value, setValue] = React.useState('');
|
|
99
197
|
const [cursor, setCursor] = React.useState(0);
|
|
198
|
+
/**
|
|
199
|
+
* CC-style fold block: the [start, end) span of `value` that renders as
|
|
200
|
+
* a one-line chip while the text around it stays fully editable. Created
|
|
201
|
+
* by a big paste; only an EXPLICIT expand (chip/card click, Esc) or
|
|
202
|
+
* delete removes it — typing NEVER unfolds the block.
|
|
203
|
+
*/
|
|
204
|
+
const [foldBlock, setFoldBlock] = React.useState(null);
|
|
205
|
+
/** Pointer over the input box (drives the hover peek card). */
|
|
206
|
+
const [hovered, setHovered] = React.useState(false);
|
|
207
|
+
/** 120ms grace so the pointer crossing the input border row from the
|
|
208
|
+
* chip up onto the peek card never flickers the card. */
|
|
209
|
+
const hoverLeaveTimerRef = React.useRef(null);
|
|
210
|
+
const hoverEnter = React.useCallback(() => {
|
|
211
|
+
if (hoverLeaveTimerRef.current) {
|
|
212
|
+
clearTimeout(hoverLeaveTimerRef.current);
|
|
213
|
+
hoverLeaveTimerRef.current = null;
|
|
214
|
+
}
|
|
215
|
+
setHovered(true);
|
|
216
|
+
}, []);
|
|
217
|
+
const hoverLeave = React.useCallback(() => {
|
|
218
|
+
if (hoverLeaveTimerRef.current)
|
|
219
|
+
clearTimeout(hoverLeaveTimerRef.current);
|
|
220
|
+
hoverLeaveTimerRef.current = setTimeout(() => setHovered(false), 120);
|
|
221
|
+
}, []);
|
|
100
222
|
const valueRef = React.useRef(value);
|
|
101
223
|
const cursorRef = React.useRef(cursor);
|
|
102
224
|
valueRef.current = value;
|
|
@@ -129,19 +251,17 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
129
251
|
React.useEffect(() => {
|
|
130
252
|
if (fillText && fillText !== lastFill.current) {
|
|
131
253
|
lastFill.current = fillText;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
setValue(fillText);
|
|
135
|
-
setCursor(fillText.length);
|
|
254
|
+
updateFoldBlock(null);
|
|
255
|
+
setInput(fillText);
|
|
136
256
|
onFillConsumed?.();
|
|
137
257
|
}
|
|
138
258
|
}, [fillText, onFillConsumed]);
|
|
139
259
|
// Double-tap Esc to clear (CC semantics).
|
|
140
260
|
const escPendingRef = React.useRef(false);
|
|
141
261
|
const escTimerRef = React.useRef(null);
|
|
142
|
-
/** True while a
|
|
262
|
+
/** True while a clipboard paste read is in flight (ignore repeat keys). */
|
|
143
263
|
const clipboardBusyRef = React.useRef(false);
|
|
144
|
-
/** True while the external editor owns the terminal (
|
|
264
|
+
/** True while the external editor owns the terminal (editor-key round-trip). */
|
|
145
265
|
const editorBusyRef = React.useRef(false);
|
|
146
266
|
/** Enter dedupe window: cmd pipelines can deliver one Enter as `\r`+`\n`. */
|
|
147
267
|
const lastEnterAtRef = React.useRef(0);
|
|
@@ -149,14 +269,26 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
149
269
|
return () => {
|
|
150
270
|
if (escTimerRef.current)
|
|
151
271
|
clearTimeout(escTimerRef.current);
|
|
272
|
+
if (hoverLeaveTimerRef.current)
|
|
273
|
+
clearTimeout(hoverLeaveTimerRef.current);
|
|
152
274
|
};
|
|
153
275
|
}, []);
|
|
154
276
|
const { columns, rows: terminalRows } = useTerminalSize();
|
|
155
277
|
const helpScrollRef = React.useRef(null);
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
|
|
278
|
+
// Help viewport budget: the overlay anchors at the composer's top edge
|
|
279
|
+
// (OverlayAbove bottom:'100%') and grows UP, so its budget is the space
|
|
280
|
+
// ABOVE that anchor — smallest on an empty session, where the whale
|
|
281
|
+
// splash (~15 rows) sits between the screen top and the composer
|
|
282
|
+
// (terminalRows minus chrome only applies once the transcript fills the
|
|
283
|
+
// viewport). Take the conservative intersection: 15 rows of viewport (16
|
|
284
|
+
// with the hint + margin) fits the empty-session anchor on the default
|
|
285
|
+
// layout at any terminal size, and the renderer's bottom-anchored
|
|
286
|
+
// clipping for absolute overlays (no negative-y clamp) then never has
|
|
287
|
+
// to eat the overlay's FIRST rows — the shortcut-column headers. The
|
|
288
|
+
// command registry scrolls inside the viewport, so a taller terminal
|
|
289
|
+
// loses nothing functional. (PR #446; restored after the picker
|
|
290
|
+
// snapshot's cherry-pick resurrected the old formula.)
|
|
291
|
+
const helpViewportHeight = Math.max(3, Math.min(terminalRows - 7, 15));
|
|
160
292
|
const suggestions = value.startsWith('/') ? channel.commandCompletions(value) : [];
|
|
161
293
|
const overlayOpen = suggestions.length > 0 &&
|
|
162
294
|
!helpOpen &&
|
|
@@ -201,24 +333,77 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
201
333
|
!helpOpen &&
|
|
202
334
|
!selectionActive &&
|
|
203
335
|
fileEscRef.current !== mention?.start;
|
|
336
|
+
/** Fold-block state + a synchronous mirror (setInput reads the ref).
|
|
337
|
+
* Creating a block also drags a caret that sits inside it out to the
|
|
338
|
+
* block's end (the block is atomic; typing continues after it). */
|
|
339
|
+
const foldBlockRef = React.useRef(null);
|
|
340
|
+
const updateFoldBlock = (block) => {
|
|
341
|
+
foldBlockRef.current = block;
|
|
342
|
+
setFoldBlock(block);
|
|
343
|
+
if (block) {
|
|
344
|
+
const c = cursorRef.current;
|
|
345
|
+
if (c > block.start && c < block.end) {
|
|
346
|
+
cursorRef.current = block.end;
|
|
347
|
+
setCursor(block.end);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
};
|
|
204
351
|
const setInput = (next, cursorOffset = next.length) => {
|
|
352
|
+
const prev = valueRef.current;
|
|
353
|
+
const prevCursor = cursorRef.current;
|
|
354
|
+
const block = foldBlockRef.current;
|
|
355
|
+
// Normalize onto a grapheme boundary (also clamps into range): every
|
|
356
|
+
// caller passes a caret they believe is on a character edge — paste
|
|
357
|
+
// merges, history fills, and IME composition can still hand back an
|
|
358
|
+
// offset inside a surrogate pair or combining cluster.
|
|
359
|
+
let offset = normalizeCursorOffset(next, cursorOffset);
|
|
360
|
+
if (block) {
|
|
361
|
+
// Fold block is atomic: the caret never lands INSIDE [start, end).
|
|
362
|
+
if (offset > block.start && offset < block.end) {
|
|
363
|
+
offset = offset - block.start < block.end - offset ? block.start : block.end;
|
|
364
|
+
}
|
|
365
|
+
// Every real edit happens AT the caret, so a block that starts at or
|
|
366
|
+
// after the caret shifts with the value delta; one fully past the
|
|
367
|
+
// caret is untouched. Whole-value replacements (fill/history/editor)
|
|
368
|
+
// clear the block explicitly at their call sites.
|
|
369
|
+
const delta = next.length - prev.length;
|
|
370
|
+
let start = block.start;
|
|
371
|
+
let end = block.end;
|
|
372
|
+
if (prevCursor <= block.start) {
|
|
373
|
+
start += delta;
|
|
374
|
+
end += delta;
|
|
375
|
+
}
|
|
376
|
+
start = Math.max(0, Math.min(start, next.length));
|
|
377
|
+
end = Math.max(start, Math.min(end, next.length));
|
|
378
|
+
if (start >= end)
|
|
379
|
+
updateFoldBlock(null);
|
|
380
|
+
else if (start !== block.start || end !== block.end)
|
|
381
|
+
updateFoldBlock({ start, end });
|
|
382
|
+
}
|
|
383
|
+
// The synchronous mirrors are what batch-dispatched events (one stdin
|
|
384
|
+
// read → several keys, no render in between) read on their next turn.
|
|
205
385
|
valueRef.current = next;
|
|
206
|
-
cursorRef.current =
|
|
386
|
+
cursorRef.current = offset;
|
|
207
387
|
setValue(next);
|
|
208
|
-
setCursor(
|
|
388
|
+
setCursor(offset);
|
|
209
389
|
};
|
|
210
390
|
/**
|
|
211
391
|
* Accept the selected file suggestion: replace ONLY the mention token at
|
|
212
392
|
* the caret (prefix/suffix text survives), quoting whitespace paths. A
|
|
213
393
|
* directory inserts `@dir/` without a trailing space so completion
|
|
214
|
-
* continues into it; a file completes the token with a space.
|
|
394
|
+
* continues into it; a file completes the token with a space. A typed
|
|
395
|
+
* `#L12-14` suffix is NOT part of the replacement — completion ends at
|
|
396
|
+
* `pathEnd` so the line range survives acceptance (issue #359).
|
|
215
397
|
*/
|
|
216
398
|
const acceptFile = (candidate) => {
|
|
217
399
|
if (!mention)
|
|
218
400
|
return;
|
|
219
401
|
const file = candidate.path;
|
|
220
402
|
const body = /\s/.test(file) ? `@"${file}"` : `@${file}`;
|
|
221
|
-
|
|
403
|
+
// A typed `#L12-14` suffix rides along AFTER the completed body (before
|
|
404
|
+
// the trailing space) — quoting a whitespace path must not detach it.
|
|
405
|
+
const suffix = mention.pathEnd === undefined ? '' : value.slice(mention.pathEnd, mention.end);
|
|
406
|
+
const insert = candidate.kind === 'directory' ? `${body}${suffix}` : `${body}${suffix} `;
|
|
222
407
|
const next = value.slice(0, mention.start) + insert + value.slice(mention.end);
|
|
223
408
|
setInput(next, mention.start + insert.length);
|
|
224
409
|
setFileSelected(0);
|
|
@@ -295,6 +480,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
295
480
|
return;
|
|
296
481
|
}
|
|
297
482
|
setInput(item.text);
|
|
483
|
+
updateFoldBlock(null);
|
|
298
484
|
setSelectedCommand(0);
|
|
299
485
|
setFileSelected(0);
|
|
300
486
|
channel.notify(t('input-retracted'), { timeoutMs: 2000 });
|
|
@@ -356,13 +542,17 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
356
542
|
return handled;
|
|
357
543
|
};
|
|
358
544
|
/** Clipboard reads are asynchronous; insert against the latest render so
|
|
359
|
-
* typing while PowerShell owns the clipboard never gets overwritten.
|
|
545
|
+
* typing while PowerShell owns the clipboard never gets overwritten.
|
|
546
|
+
* Returns the resulting value and the insertion offset so callers can
|
|
547
|
+
* apply the paste fold. */
|
|
360
548
|
const insertClipboardAtCaret = (text) => {
|
|
361
549
|
const current = valueRef.current;
|
|
362
550
|
const position = cursorRef.current;
|
|
363
|
-
|
|
551
|
+
const next = current.slice(0, position) + text + current.slice(position);
|
|
552
|
+
setInput(next, position + text.length);
|
|
364
553
|
setSelectedCommand(0);
|
|
365
554
|
setFileSelected(0);
|
|
555
|
+
return { next, at: position };
|
|
366
556
|
};
|
|
367
557
|
/** Line index of the cursor; -1 when the cursor is at the very end. */
|
|
368
558
|
const cursorLine = (text, cursorOffset) => {
|
|
@@ -388,6 +578,42 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
388
578
|
// the text/caret produced by the preceding event in that batch.
|
|
389
579
|
const value = valueRef.current;
|
|
390
580
|
const cursor = cursorRef.current;
|
|
581
|
+
// Fold block (CC-style): Esc expands it — it must NEVER clear text
|
|
582
|
+
// that LOOKS like one line; Backspace at the block's tail / Delete at
|
|
583
|
+
// its head deletes the WHOLE block in one key; ←/→ jump over the
|
|
584
|
+
// atomic block. Typing NEVER expands it — the caret lives outside the
|
|
585
|
+
// block and edits land in the visible text around it.
|
|
586
|
+
const block = foldBlockRef.current;
|
|
587
|
+
// Line-level moves/edits (↑/↓/Home/End/Ctrl+A/E/U/K/W) treat the
|
|
588
|
+
// block as an atomic row: boundaries that would land inside it are
|
|
589
|
+
// clamped to its edges.
|
|
590
|
+
const clampRowStart = (pos) => block && cursor >= block.end ? Math.max(pos, block.end) : pos;
|
|
591
|
+
const clampRowEnd = (pos) => block && cursor <= block.start ? Math.min(pos, block.start) : pos;
|
|
592
|
+
if (block) {
|
|
593
|
+
if (key.escape) {
|
|
594
|
+
updateFoldBlock(null);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
if ((key.backspace && cursor === block.end) || (key.delete && cursor === block.start)) {
|
|
598
|
+
const next = value.slice(0, block.start) + value.slice(block.end);
|
|
599
|
+
updateFoldBlock(null);
|
|
600
|
+
setInput(next, block.start);
|
|
601
|
+
setSelectedCommand(0);
|
|
602
|
+
setFileSelected(0);
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
if (key.leftArrow && cursor === block.end) {
|
|
606
|
+
setInput(value, block.start);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (key.rightArrow && cursor === block.start) {
|
|
610
|
+
setInput(value, block.end);
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
// Grapheme boundaries of the current text: every caret move / delete
|
|
615
|
+
// below snaps onto one of these offsets (never mid-cluster).
|
|
616
|
+
const bounds = graphemeBoundaries(value);
|
|
391
617
|
/** Insert text at the caret (typing, paste) and dismiss overlays. */
|
|
392
618
|
const insertAtCaret = (text) => {
|
|
393
619
|
if (helpOpen)
|
|
@@ -401,13 +627,20 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
401
627
|
// verbatim at the caret. Paste content may contain newlines — that is
|
|
402
628
|
// NOT Enter — so this branch runs before the whole-line submit rule.
|
|
403
629
|
if (event?.isPasted && input.length > 0) {
|
|
404
|
-
|
|
630
|
+
const text = input.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
631
|
+
insertAtCaret(text);
|
|
632
|
+
// A big paste becomes a CC-style fold block right away (hover peeks
|
|
633
|
+
// at it); an existing block is replaced by the new paste's span.
|
|
634
|
+
if (isBigInput(text))
|
|
635
|
+
updateFoldBlock({ start: cursor, end: cursor + text.length });
|
|
405
636
|
return;
|
|
406
637
|
}
|
|
407
|
-
// Ctrl+V / Cmd+V
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
|
|
638
|
+
// Clipboard paste (default Ctrl+V / Cmd+V, plus the Alt+V alias for
|
|
639
|
+
// terminals that intercept Ctrl+V — combos are user-remappable via
|
|
640
|
+
// /settings): raw mode hands the key to the app, so the clipboard is
|
|
641
|
+
// read here — text, file paths when the file manager copied files, or
|
|
642
|
+
// an exported temp-file path when the clipboard holds a raw image.
|
|
643
|
+
if (actionMatches('paste', input, key)) {
|
|
411
644
|
if (clipboardBusyRef.current)
|
|
412
645
|
return;
|
|
413
646
|
// Match insertAtCaret's overlay/selection dismissal up front: the
|
|
@@ -451,7 +684,11 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
451
684
|
// Insert against the LIVE input state: the read above resolved
|
|
452
685
|
// asynchronously and the user may have typed while waiting.
|
|
453
686
|
const text = formatClipboardInsert(content);
|
|
454
|
-
insertClipboardAtCaret(text);
|
|
687
|
+
const { at } = insertClipboardAtCaret(text);
|
|
688
|
+
// Same fold as bracketed paste: big clipboard text becomes a
|
|
689
|
+
// fold block covering exactly the pasted span.
|
|
690
|
+
if (isBigInput(text))
|
|
691
|
+
updateFoldBlock({ start: at, end: at + text.length });
|
|
455
692
|
})
|
|
456
693
|
.catch(() => {
|
|
457
694
|
channel.notify(t('input-clipboard-read-failed'), { color: 'warning' });
|
|
@@ -463,27 +700,30 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
463
700
|
});
|
|
464
701
|
return;
|
|
465
702
|
}
|
|
466
|
-
// Help is modal for modified keys and every Enter variant.
|
|
467
|
-
// is the intentional exception: paste closes Help and
|
|
703
|
+
// Help is modal for modified keys and every Enter variant. The paste
|
|
704
|
+
// branch above is the intentional exception: paste closes Help and
|
|
705
|
+
// inserts visibly.
|
|
468
706
|
// Swallow here before editor/submit/interrupt branches can mutate hidden
|
|
469
707
|
// composer or working-turn state; plain typing still dismisses Help below.
|
|
470
708
|
if (helpOpen && !key.escape && (key.ctrl || key.meta || key.super || key.return || input.includes('\n') || input.includes('\r'))) {
|
|
471
709
|
event.stopImmediatePropagation();
|
|
472
710
|
return;
|
|
473
711
|
}
|
|
474
|
-
// Ctrl+G: edit the current draft in
|
|
475
|
-
// readline's edit-and-execute-command). The
|
|
476
|
-
// file, the terminal is handed to the editor
|
|
477
|
-
// and the saved text replaces the input when
|
|
478
|
-
// every failure to an outcome, but the
|
|
479
|
-
// guarantee: a rejected promise must
|
|
480
|
-
// busy flag must always clear or
|
|
481
|
-
|
|
712
|
+
// Ctrl+G (remappable via /settings): edit the current draft in
|
|
713
|
+
// $VISUAL/$EDITOR (issue #123, readline's edit-and-execute-command). The
|
|
714
|
+
// draft is written to a temp file, the terminal is handed to the editor
|
|
715
|
+
// (Ink's alt-screen handoff), and the saved text replaces the input when
|
|
716
|
+
// it differs. The util maps every failure to an outcome, but the
|
|
717
|
+
// catch/finally here is the hard guarantee: a rejected promise must
|
|
718
|
+
// never kill the process, and the busy flag must always clear or the
|
|
719
|
+
// editor key stays locked forever.
|
|
720
|
+
if (actionMatches('editor', input, key)) {
|
|
482
721
|
editorBusyRef.current = true;
|
|
483
722
|
void (async () => {
|
|
484
723
|
try {
|
|
485
724
|
const outcome = await editInExternalEditor(value);
|
|
486
725
|
if (outcome.kind === 'edited') {
|
|
726
|
+
updateFoldBlock(null);
|
|
487
727
|
setInput(outcome.text);
|
|
488
728
|
setSelectedCommand(0);
|
|
489
729
|
setFileSelected(0);
|
|
@@ -628,8 +868,10 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
628
868
|
}
|
|
629
869
|
if (key.tab && overlayOpen) {
|
|
630
870
|
const command = suggestions[selectedCommand];
|
|
631
|
-
if (command)
|
|
871
|
+
if (command) {
|
|
872
|
+
updateFoldBlock(null);
|
|
632
873
|
setInput(command.replacement);
|
|
874
|
+
}
|
|
633
875
|
return;
|
|
634
876
|
}
|
|
635
877
|
// Tab while the model is working = queue for AFTER the turn (followup),
|
|
@@ -683,6 +925,22 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
683
925
|
setFileSelected(index => index <= 0 ? fileMatches.length - 1 : index - 1);
|
|
684
926
|
return;
|
|
685
927
|
}
|
|
928
|
+
// With a fold block the caret in the tail walks the TAIL's lines
|
|
929
|
+
// (the block is one atomic row); the first tail line steps over the
|
|
930
|
+
// block to its head.
|
|
931
|
+
if (block && cursor >= block.end) {
|
|
932
|
+
const tailCursor = cursor - block.end;
|
|
933
|
+
const line = cursorLine(tail, tailCursor);
|
|
934
|
+
if (line > 0) {
|
|
935
|
+
const upToLineStart = tail.lastIndexOf('\n', tailCursor - 1);
|
|
936
|
+
const prevLineStart = upToLineStart === -1 ? 0 : tail.lastIndexOf('\n', upToLineStart - 1) + 1;
|
|
937
|
+
const prevLine = tail.slice(prevLineStart, upToLineStart);
|
|
938
|
+
setInput(value, block.end + prevLineStart + Math.min(cursorColumn(tail, tailCursor), prevLine.length));
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
setInput(value, block.start);
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
686
944
|
const line = cursorLine(value, cursor);
|
|
687
945
|
if (line > 0) {
|
|
688
946
|
// Move to the previous line, clamping to its length.
|
|
@@ -706,6 +964,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
706
964
|
historyIndex.current = Math.max(0, historyIndex.current - 1);
|
|
707
965
|
}
|
|
708
966
|
const entry = history.current[historyIndex.current] ?? '';
|
|
967
|
+
updateFoldBlock(null);
|
|
709
968
|
setInput(entry);
|
|
710
969
|
return;
|
|
711
970
|
}
|
|
@@ -714,6 +973,36 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
714
973
|
setFileSelected(index => index >= fileMatches.length - 1 ? 0 : index + 1);
|
|
715
974
|
return;
|
|
716
975
|
}
|
|
976
|
+
// Mirror of the tail-side ↑ walk: with a fold block the caret walks
|
|
977
|
+
// the TAIL's lines; past its last line it falls through to the
|
|
978
|
+
// overlay/history handling below.
|
|
979
|
+
if (block && cursor >= block.end) {
|
|
980
|
+
const tailCursor = cursor - block.end;
|
|
981
|
+
const line = cursorLine(tail, tailCursor);
|
|
982
|
+
const lines = tail.split('\n');
|
|
983
|
+
if (line < lines.length - 1) {
|
|
984
|
+
const nextLineStart = tail.indexOf('\n', tailCursor) + 1;
|
|
985
|
+
const nextLineEnd = tail.indexOf('\n', nextLineStart);
|
|
986
|
+
const nextLine = tail.slice(nextLineStart, nextLineEnd === -1 ? tail.length : nextLineEnd);
|
|
987
|
+
setInput(value, block.end + nextLineStart + Math.min(cursorColumn(tail, tailCursor), nextLine.length));
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
else if (block && cursor <= block.start) {
|
|
992
|
+
// Head side: walk the HEAD's lines; its last line steps over the
|
|
993
|
+
// block to the tail (never into history for a multi-row input).
|
|
994
|
+
const line = cursorLine(head, cursor);
|
|
995
|
+
const lines = head.split('\n');
|
|
996
|
+
if (line < lines.length - 1) {
|
|
997
|
+
const nextLineStart = head.indexOf('\n', cursor) + 1;
|
|
998
|
+
const nextLineEnd = head.indexOf('\n', nextLineStart);
|
|
999
|
+
const nextLine = head.slice(nextLineStart, nextLineEnd === -1 ? head.length : nextLineEnd);
|
|
1000
|
+
setInput(value, nextLineStart + Math.min(cursorColumn(head, cursor), nextLine.length));
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
setInput(value, block.end);
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
717
1006
|
const line = cursorLine(value, cursor);
|
|
718
1007
|
const lines = value.split('\n');
|
|
719
1008
|
if (line < lines.length - 1) {
|
|
@@ -731,6 +1020,7 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
731
1020
|
return;
|
|
732
1021
|
if (historyIndex.current >= history.current.length - 1) {
|
|
733
1022
|
historyIndex.current = -1;
|
|
1023
|
+
updateFoldBlock(null);
|
|
734
1024
|
setInput(historyDraft.current);
|
|
735
1025
|
}
|
|
736
1026
|
else {
|
|
@@ -751,63 +1041,68 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
751
1041
|
return;
|
|
752
1042
|
}
|
|
753
1043
|
if (key.leftArrow) {
|
|
754
|
-
|
|
1044
|
+
// Grapheme-step: skip the whole cluster (surrogate pair, ZWJ emoji,
|
|
1045
|
+
// combining mark) so the caret never sits inside one.
|
|
1046
|
+
setInput(value, previousGraphemeBoundary(bounds, cursor));
|
|
755
1047
|
return;
|
|
756
1048
|
}
|
|
757
1049
|
if (key.rightArrow) {
|
|
758
|
-
setInput(value,
|
|
1050
|
+
setInput(value, nextGraphemeBoundary(bounds, cursor));
|
|
759
1051
|
return;
|
|
760
1052
|
}
|
|
761
1053
|
if (key.backspace) {
|
|
762
1054
|
if (cursor === 0)
|
|
763
1055
|
return;
|
|
764
|
-
|
|
1056
|
+
const start = previousGraphemeBoundary(bounds, cursor);
|
|
1057
|
+
setInput(value.slice(0, start) + value.slice(cursor), start);
|
|
765
1058
|
return;
|
|
766
1059
|
}
|
|
767
1060
|
if (key.delete) {
|
|
768
|
-
|
|
1061
|
+
const end = nextGraphemeBoundary(bounds, cursor);
|
|
1062
|
+
if (end === cursor)
|
|
769
1063
|
return;
|
|
770
|
-
setInput(value.slice(0, cursor) + value.slice(
|
|
1064
|
+
setInput(value.slice(0, cursor) + value.slice(end), cursor);
|
|
771
1065
|
return;
|
|
772
1066
|
}
|
|
773
1067
|
if (key.home) {
|
|
774
|
-
// Start of the current line.
|
|
775
|
-
const lineStart = value.lastIndexOf('\n', cursor - 1) + 1;
|
|
1068
|
+
// Start of the current line (the block is one atomic row).
|
|
1069
|
+
const lineStart = clampRowStart(value.lastIndexOf('\n', cursor - 1) + 1);
|
|
776
1070
|
setInput(value, lineStart);
|
|
777
1071
|
return;
|
|
778
1072
|
}
|
|
779
1073
|
if (key.end) {
|
|
780
1074
|
// End of the current line.
|
|
781
1075
|
const nextLine = value.indexOf('\n', cursor);
|
|
782
|
-
setInput(value, nextLine === -1 ? value.length : nextLine);
|
|
1076
|
+
setInput(value, nextLine === -1 ? value.length : clampRowEnd(nextLine));
|
|
783
1077
|
return;
|
|
784
1078
|
}
|
|
785
1079
|
if (isMod(key) && input === 'a') {
|
|
786
|
-
const lineStart = value.lastIndexOf('\n', cursor - 1) + 1;
|
|
1080
|
+
const lineStart = clampRowStart(value.lastIndexOf('\n', cursor - 1) + 1);
|
|
787
1081
|
setInput(value, lineStart);
|
|
788
1082
|
return;
|
|
789
1083
|
}
|
|
790
1084
|
if (isMod(key) && input === 'e') {
|
|
791
1085
|
const nextLine = value.indexOf('\n', cursor);
|
|
792
|
-
setInput(value, nextLine === -1 ? value.length : nextLine);
|
|
1086
|
+
setInput(value, nextLine === -1 ? value.length : clampRowEnd(nextLine));
|
|
793
1087
|
return;
|
|
794
1088
|
}
|
|
795
1089
|
if (isMod(key) && input === 'u') {
|
|
796
|
-
// Delete to start of line.
|
|
797
|
-
const lineStart = value.lastIndexOf('\n', cursor - 1) + 1;
|
|
1090
|
+
// Delete to start of line (never into the block).
|
|
1091
|
+
const lineStart = clampRowStart(value.lastIndexOf('\n', cursor - 1) + 1);
|
|
798
1092
|
setInput(value.slice(0, lineStart) + value.slice(cursor), lineStart);
|
|
799
1093
|
return;
|
|
800
1094
|
}
|
|
801
1095
|
if (isMod(key) && input === 'k') {
|
|
802
|
-
// Delete to end of line.
|
|
1096
|
+
// Delete to end of line (never into the block).
|
|
803
1097
|
const nextLine = value.indexOf('\n', cursor);
|
|
804
|
-
const end = nextLine === -1 ? value.length : nextLine;
|
|
1098
|
+
const end = nextLine === -1 ? value.length : clampRowEnd(nextLine);
|
|
805
1099
|
setInput(value.slice(0, cursor) + value.slice(end), cursor);
|
|
806
1100
|
return;
|
|
807
1101
|
}
|
|
808
1102
|
if (isMod(key) && input === 'w') {
|
|
809
1103
|
// Delete the word before the cursor (CC/readline behavior): skip
|
|
810
|
-
// trailing whitespace, then the whitespace-delimited word.
|
|
1104
|
+
// trailing whitespace, then the whitespace-delimited word. The
|
|
1105
|
+
// deletion start never crosses into the block.
|
|
811
1106
|
const before = value.slice(0, cursor);
|
|
812
1107
|
let end = before.length;
|
|
813
1108
|
while (end > 0 && /\s/.test(before[end - 1]))
|
|
@@ -815,7 +1110,8 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
815
1110
|
let start = end;
|
|
816
1111
|
while (start > 0 && !/\s/.test(before[start - 1]))
|
|
817
1112
|
start--;
|
|
818
|
-
|
|
1113
|
+
const clipped = clampRowStart(start);
|
|
1114
|
+
setInput(value.slice(0, clipped) + value.slice(cursor), clipped);
|
|
819
1115
|
return;
|
|
820
1116
|
}
|
|
821
1117
|
if (key.escape) {
|
|
@@ -848,8 +1144,17 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
848
1144
|
return;
|
|
849
1145
|
}
|
|
850
1146
|
// A single Esc clears the current input (if any); the double-tap
|
|
851
|
-
// path below handles rewind/clear on an already-empty input.
|
|
1147
|
+
// path below handles rewind/clear on an already-empty input. A BIG
|
|
1148
|
+
// input folds into a block instead (Esc = the fold toggle; the
|
|
1149
|
+
// expand → fold → expand cycle is lossless, and clearing a big draft
|
|
1150
|
+
// stays reachable via the block-delete Backspace or Ctrl+C).
|
|
852
1151
|
if (value.length > 0) {
|
|
1152
|
+
if (isBigInput(value)) {
|
|
1153
|
+
updateFoldBlock({ start: 0, end: value.length });
|
|
1154
|
+
setSelectedCommand(0);
|
|
1155
|
+
setFileSelected(0);
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
853
1158
|
setInput('', 0);
|
|
854
1159
|
setSelectedCommand(0);
|
|
855
1160
|
setFileSelected(0);
|
|
@@ -894,9 +1199,37 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
894
1199
|
// === Render: hard-wrap every logical line at the input width, then show
|
|
895
1200
|
// the window of visual lines with the caret row always visible (CC's
|
|
896
1201
|
// maxVisibleLines behavior with automatic wrapping).
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
1202
|
+
// Narrow terminals: the usable width follows the real terminal down to a
|
|
1203
|
+
// single column — a fixed floor of 10 would wrap far too early and park
|
|
1204
|
+
// the declared cursor past the value box's actual width.
|
|
1205
|
+
const inputWidth = Math.max(1, columns - 3);
|
|
1206
|
+
const block = foldBlock;
|
|
1207
|
+
// Fold-block model: the value renders as [head rows][chip row][tail rows]
|
|
1208
|
+
// — the block is ONE atomic visual row regardless of its text size; text
|
|
1209
|
+
// before/after it stays fully editable and never unfolds it.
|
|
1210
|
+
const foldText = block ? value.slice(block.start, block.end) : value;
|
|
1211
|
+
const head = block ? value.slice(0, block.start) : '';
|
|
1212
|
+
const tail = block ? value.slice(block.end) : '';
|
|
1213
|
+
const headRows = block ? wrapToWidth(head, inputWidth) : [];
|
|
1214
|
+
const tailRows = block ? wrapToWidth(tail, inputWidth) : [];
|
|
1215
|
+
const chipRow = block ? headRows.length : -1;
|
|
1216
|
+
const visualLines = block
|
|
1217
|
+
? [...headRows, '', ...tailRows]
|
|
1218
|
+
: wrapToWidth(value, inputWidth);
|
|
1219
|
+
const caretVisualLine = block
|
|
1220
|
+
? cursor <= block.start
|
|
1221
|
+
? wrapToWidth(value.slice(0, cursor), inputWidth).length - 1
|
|
1222
|
+
: headRows.length + 1 + wrapToWidth(value.slice(block.end, cursor), inputWidth).length - 1
|
|
1223
|
+
: wrapToWidth(value.slice(0, cursor), inputWidth).length - 1;
|
|
1224
|
+
// Fold stats describe the BLOCK (or the whole input when expanded and
|
|
1225
|
+
// the ▾ prefix offers a manual whole-input fold). The chip shows the
|
|
1226
|
+
// block's own line/char count + first-line preview — the preview text
|
|
1227
|
+
// around it is unaffected.
|
|
1228
|
+
const big = isBigInput(foldText);
|
|
1229
|
+
const stats = big
|
|
1230
|
+
? t('input-fold-stats', { lines: foldText.split('\n').length, chars: foldText.length })
|
|
1231
|
+
: '';
|
|
1232
|
+
const peekOpen = block !== null && hovered && !selectionActive;
|
|
900
1233
|
const windowStart = Math.max(0, Math.min(caretVisualLine - MAX_VISIBLE_LINES + 1, visualLines.length - MAX_VISIBLE_LINES));
|
|
901
1234
|
const visibleLines = visualLines.slice(windowStart, windowStart + MAX_VISIBLE_LINES);
|
|
902
1235
|
// Caret position in the caret's visual row, in two units:
|
|
@@ -906,29 +1239,94 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
906
1239
|
// cursor mid-character and Windows Terminal would paint the IME
|
|
907
1240
|
// preedit (pinyin) over the surrounding text).
|
|
908
1241
|
const caretCharCol = () => {
|
|
909
|
-
const before = value.slice(0, cursor);
|
|
1242
|
+
const before = block && cursor >= block.end ? value.slice(block.end, cursor) : value.slice(0, cursor);
|
|
910
1243
|
const rows = wrapToWidth(before, inputWidth);
|
|
911
1244
|
const last = rows[rows.length - 1] ?? '';
|
|
912
1245
|
return last.length;
|
|
913
1246
|
};
|
|
914
1247
|
const caretVisualCol = () => {
|
|
915
|
-
const before = value.slice(0, cursor);
|
|
1248
|
+
const before = block && cursor >= block.end ? value.slice(block.end, cursor) : value.slice(0, cursor);
|
|
916
1249
|
const rows = wrapToWidth(before, inputWidth);
|
|
917
1250
|
const last = rows[rows.length - 1] ?? '';
|
|
918
1251
|
return stringWidth(last);
|
|
919
1252
|
};
|
|
1253
|
+
// Folded chip content: block stats + first-line preview + hover hint,
|
|
1254
|
+
// all pre-truncated to the input width (the row is one line, always).
|
|
1255
|
+
const foldBadge = `▸ ${stats}`;
|
|
1256
|
+
const foldHint = t('input-fold-hover');
|
|
1257
|
+
const foldPreviewWidth = inputWidth - stringWidth(foldBadge) - stringWidth(` · ${foldHint}`) - 6;
|
|
1258
|
+
const foldPreview = foldPreviewWidth >= 8
|
|
1259
|
+
? truncateToWidth(foldText.split('\n')[0] ?? '', foldPreviewWidth)
|
|
1260
|
+
: '';
|
|
1261
|
+
// Expanded-state fold affordance: a `▾` prefix at the start of the FIRST
|
|
1262
|
+
// row (only while the window is at the top and no block exists); its
|
|
1263
|
+
// cells fold the whole input into a block again on click.
|
|
1264
|
+
const prefixLabel = `▾ ${stats} · `;
|
|
1265
|
+
const prefixCols = !block && big && windowStart === 0 ? stringWidth(prefixLabel) : 0;
|
|
920
1266
|
const rendered = visibleLines.map((line, index) => {
|
|
921
1267
|
const absoluteLine = windowStart + index;
|
|
1268
|
+
if (block && absoluteLine === chipRow) {
|
|
1269
|
+
// The fold block's atomic chip row: click expands it; hover pops the
|
|
1270
|
+
// peek card. The row is one line no matter how big the block is.
|
|
1271
|
+
return (_jsxs(Box, { flexDirection: "row", onClick: (event) => {
|
|
1272
|
+
event.stopImmediatePropagation();
|
|
1273
|
+
updateFoldBlock(null);
|
|
1274
|
+
}, onMouseEnter: hoverEnter, onMouseLeave: hoverLeave, children: [_jsx(Text, { dimColor: true, children: foldBadge }), foldPreview !== '' && _jsx(Text, { dimColor: true, children: " \u00B7 " }), foldPreview !== '' && _jsx(Text, { wrap: "truncate-end", children: foldPreview }), _jsx(Text, { dimColor: true, children: ` · ${foldHint}` })] }, `fold-${absoluteLine}`));
|
|
1275
|
+
}
|
|
1276
|
+
const withPrefix = absoluteLine === 0 && prefixCols > 0;
|
|
1277
|
+
const text = withPrefix ? truncateToWidth(line, inputWidth - prefixCols) : line;
|
|
1278
|
+
const prefix = withPrefix ? _jsx(Text, { dimColor: true, children: prefixLabel }) : null;
|
|
922
1279
|
if (absoluteLine !== caretVisualLine) {
|
|
923
|
-
return (
|
|
924
|
-
}
|
|
925
|
-
// Caret row: invert the
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1280
|
+
return (_jsxs(Text, { wrap: "truncate-end", children: [prefix, text] }, absoluteLine));
|
|
1281
|
+
}
|
|
1282
|
+
// Caret row: invert the grapheme cluster at the caret column (solid
|
|
1283
|
+
// block). `col` is a cluster boundary by construction (the cursor is
|
|
1284
|
+
// normalized onto boundaries and wrapping only breaks between
|
|
1285
|
+
// graphemes), so [col, next boundary) covers the WHOLE cluster — a
|
|
1286
|
+
// surrogate pair or ZWJ emoji inverts as one glyph, never two broken
|
|
1287
|
+
// halves. Clamped into the prefix-shortened row text so the block
|
|
1288
|
+
// caret never renders under the fold prefix.
|
|
1289
|
+
const col = Math.min(caretCharCol(), text.length);
|
|
1290
|
+
const clusterEnd = nextGraphemeBoundary(graphemeBoundaries(text), col);
|
|
1291
|
+
const before = text.slice(0, col);
|
|
1292
|
+
const at = clusterEnd > col ? text.slice(col, clusterEnd) : ' ';
|
|
1293
|
+
const after = text.slice(clusterEnd);
|
|
1294
|
+
return (_jsxs(Text, { wrap: "truncate-end", children: [prefix, before, _jsx(Text, { inverse: true, children: at }), after] }, absoluteLine));
|
|
931
1295
|
});
|
|
1296
|
+
// Peek card content: the BLOCK's text wrapped to the card's inner width,
|
|
1297
|
+
// capped at PEEK_MAX_ROWS visual rows (a preview — click to expand and
|
|
1298
|
+
// edit the real input). The footer reports the clipped remainder.
|
|
1299
|
+
const PEEK_MAX_ROWS = 10;
|
|
1300
|
+
const peekVisualLines = [];
|
|
1301
|
+
for (const line of foldText.split('\n')) {
|
|
1302
|
+
for (const row of wrapToWidth(line, cardContentWidth(columns))) {
|
|
1303
|
+
if (peekVisualLines.length >= PEEK_MAX_ROWS)
|
|
1304
|
+
break;
|
|
1305
|
+
peekVisualLines.push(row);
|
|
1306
|
+
}
|
|
1307
|
+
if (peekVisualLines.length >= PEEK_MAX_ROWS)
|
|
1308
|
+
break;
|
|
1309
|
+
}
|
|
1310
|
+
const peekClipped = peekVisualLines.length >= PEEK_MAX_ROWS;
|
|
1311
|
+
// Composer height shrink: clearing multi-line text (Enter/Esc/Ctrl+C/
|
|
1312
|
+
// Backspace) collapses the input area within one commit, shifting the
|
|
1313
|
+
// status line up and the whole chrome with it. The renderer's
|
|
1314
|
+
// full-damage pass (didLayoutShift) repaints the shifted siblings, but
|
|
1315
|
+
// inline mode's virtual↔scrollback correspondence needs the stronger
|
|
1316
|
+
// in-place viewport repaint — same treatment as Ctrl+O and the
|
|
1317
|
+
// loaded-context toggle (see Chat.tsx). One-shot, only on SHRINK:
|
|
1318
|
+
// growth scrolls the terminal naturally and needs no recovery.
|
|
1319
|
+
const contentRows = value.length === 0 ? 1 : visibleLines.length;
|
|
1320
|
+
noteAuxNumber('promptContentRows', contentRows);
|
|
1321
|
+
const prevContentRowsRef = React.useRef(contentRows);
|
|
1322
|
+
React.useLayoutEffect(() => {
|
|
1323
|
+
if (contentRows < prevContentRowsRef.current) {
|
|
1324
|
+
const ink = instances.get(process.stdout) ?? instances.values().next().value;
|
|
1325
|
+
ink?.invalidatePrevFrame();
|
|
1326
|
+
ink?.reanchorViewport();
|
|
1327
|
+
}
|
|
1328
|
+
prevContentRowsRef.current = contentRows;
|
|
1329
|
+
}, [contentRows]);
|
|
932
1330
|
const lastNotification = channel.notifications[channel.notifications.length - 1];
|
|
933
1331
|
// Park the native terminal cursor at the input caret (via the renderer's
|
|
934
1332
|
// cursor-declaration mechanism). Terminal emulators render IME preedit
|
|
@@ -939,27 +1337,111 @@ export function PromptInput({ channel, helpOpen, onToggleHelp, onRunCommand, sel
|
|
|
939
1337
|
// relative to the value box the ref attaches to.
|
|
940
1338
|
const valueBoxRef = useDeclaredCursor({
|
|
941
1339
|
line: caretVisualLine - windowStart,
|
|
942
|
-
column:
|
|
1340
|
+
// Clamp the declared column to the wrap width: a grapheme wider than
|
|
1341
|
+
// the last remaining column (emoji at width 1) can push the visual
|
|
1342
|
+
// column past inputWidth, and the park must stay inside the value box.
|
|
1343
|
+
column: Math.min(caretVisualCol() + (caretVisualLine === 0 && prefixCols > 0 ? prefixCols : 0), inputWidth),
|
|
943
1344
|
active: !selectionActive,
|
|
944
1345
|
});
|
|
1346
|
+
/**
|
|
1347
|
+
* Click-to-position the caret: map a click inside the value box (local
|
|
1348
|
+
* row/column relative to the box) to a UTF-16 cursor offset via the same
|
|
1349
|
+
* grapheme walk the renderer wraps with — exact under CJK widths, wrapped
|
|
1350
|
+
* rows and multi-codepoint clusters. Clicks land on the boundary nearest
|
|
1351
|
+
* the clicked cell (mid-grapheme snaps to its start). With a fold block,
|
|
1352
|
+
* clicks map into the head/tail text (the chip row has its own expand
|
|
1353
|
+
* onClick); without one, the fold prefix's cells fold the whole input.
|
|
1354
|
+
*/
|
|
1355
|
+
const handleValueClick = React.useCallback((e) => {
|
|
1356
|
+
const clickedVisual = windowStart + e.localRow;
|
|
1357
|
+
const clamped = Math.max(0, Math.min(clickedVisual, visualLines.length - 1));
|
|
1358
|
+
if (block) {
|
|
1359
|
+
if (clamped === chipRow)
|
|
1360
|
+
return;
|
|
1361
|
+
if (clamped < chipRow) {
|
|
1362
|
+
setCursor(clickToCursorOffset(head, inputWidth, clamped, e.localCol));
|
|
1363
|
+
}
|
|
1364
|
+
else {
|
|
1365
|
+
setCursor(block.end + clickToCursorOffset(tail, inputWidth, clamped - chipRow - 1, e.localCol));
|
|
1366
|
+
}
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
if (clamped === 0 && prefixCols > 0 && e.localCol < prefixCols) {
|
|
1370
|
+
updateFoldBlock({ start: 0, end: value.length });
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
const col = clamped === 0 && prefixCols > 0 ? Math.max(0, e.localCol - prefixCols) : e.localCol;
|
|
1374
|
+
setCursor(clickToCursorOffset(value, clamped === 0 && prefixCols > 0 ? inputWidth - prefixCols : inputWidth, clamped, col));
|
|
1375
|
+
}, [windowStart, visualLines.length, value, inputWidth, prefixCols, block, chipRow, head, tail]);
|
|
945
1376
|
// 浮层整体挂载条件:与内部面板可见条件精确同值。关闭时必须把整个
|
|
946
1377
|
// absolute 浮层移除——渲染器的 absolute-removed 检测只看被移除节点自身
|
|
947
1378
|
// 的 style.position,常驻浮层 + 移除普通子节点不会触发 blit 解毒,被
|
|
948
1379
|
// 覆盖的转录行会留空(见 Chat.tsx dialogOverlayOpen 注释)。
|
|
949
|
-
const floatersOpen = helpOpen || channel.pending.length > 0 || fileOverlayOpen || overlayOpen;
|
|
950
|
-
|
|
1380
|
+
const floatersOpen = helpOpen || channel.pending.length > 0 || fileOverlayOpen || overlayOpen || peekOpen;
|
|
1381
|
+
// 补全卡片边框与输入框 idle 边框同色(plan 模式下整套面板一起变 sage 绿)。
|
|
1382
|
+
// `/color` 会话强调色优先于主题 promptBorder(plan 模式仍整体走 sage 绿)。
|
|
1383
|
+
// `?? ''` 防御最小 mock channel(只声明用到的字段的回归脚本)。
|
|
1384
|
+
const sessionAccent = sessionColorHex(channel.sessionColor ?? '');
|
|
1385
|
+
const promptAccent = channel.mode.plan === true ? 'planMode' : (sessionAccent ?? 'promptBorder');
|
|
1386
|
+
// 顶边框右侧的会话名标签(CC 风格 chip):色随强调色;超宽截断,宽度
|
|
1387
|
+
// 随终端列数伸缩但不超过 28 显示单元。默认关闭——`/settings` 的
|
|
1388
|
+
// 「会话名标签」开关(dsh-tui.promptSessionLabel)开启后显示。
|
|
1389
|
+
const sessionTitle = channel.sessionTitle ?? '';
|
|
1390
|
+
const topRightLabel = channel.promptSessionLabel === true && sessionTitle !== ''
|
|
1391
|
+
? {
|
|
1392
|
+
text: truncateToWidth(sessionTitle, Math.max(8, Math.min(28, columns - 8))),
|
|
1393
|
+
color: channel.mode.plan === true ? 'planMode' : (sessionAccent ?? 'claude'),
|
|
1394
|
+
ink: 'inverseText',
|
|
1395
|
+
}
|
|
1396
|
+
: undefined;
|
|
1397
|
+
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) => {
|
|
1398
|
+
// 点击命令行 = 填入 /name 并关闭帮助(Tab 补全的鼠标等价)
|
|
1399
|
+
updateFoldBlock(null);
|
|
1400
|
+
setInput(`/${name} `);
|
|
1401
|
+
onToggleHelp();
|
|
1402
|
+
} }) })), !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
1403
|
.filter(item => item.placement === 'steer')
|
|
952
1404
|
.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
1405
|
.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(
|
|
1406
|
+
.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,
|
|
1407
|
+
// 点击行 = 接受该项(与 Enter 同路径)
|
|
1408
|
+
onPick: (index) => {
|
|
1409
|
+
const file = fileMatches[index];
|
|
1410
|
+
if (file)
|
|
1411
|
+
acceptFile(file);
|
|
1412
|
+
},
|
|
1413
|
+
// 滚轮 = 移动选中行(与 ↑/↓ 同路径,窗口跟随)
|
|
1414
|
+
onWheelStep: (step) => {
|
|
1415
|
+
setFileSelected(i => Math.max(0, Math.min(fileMatches.length - 1, i + step)));
|
|
1416
|
+
} })), overlayOpen && (_jsx(CommandSuggestions, { commands: suggestions, selectedIndex: selectedCommand, columns: columns, query: value, accent: promptAccent,
|
|
1417
|
+
// 点击行 = 运行该命令(与 Enter 同路径)
|
|
1418
|
+
onPick: (index) => {
|
|
1419
|
+
const command = suggestions[index];
|
|
1420
|
+
if (command)
|
|
1421
|
+
tryRunCommand(command.commandLine);
|
|
1422
|
+
},
|
|
1423
|
+
// 滚轮 = 移动选中行(与 ↑/↓ 同路径,窗口跟随)
|
|
1424
|
+
onWheelStep: (step) => {
|
|
1425
|
+
setSelectedCommand(i => Math.max(0, Math.min(suggestions.length - 1, i + step)));
|
|
1426
|
+
} })), peekOpen && (_jsx(Box, { onMouseEnter: hoverEnter, onMouseLeave: hoverLeave, children: _jsx(SuggestionCard, { title: stats, columns: columns, accent: promptAccent, footer: peekClipped
|
|
1427
|
+
? t('input-fold-peek-footer', { lines: foldText.split('\n').length })
|
|
1428
|
+
: undefined, rows: peekVisualLines.map((row, index) => (_jsx(Text, { wrap: "truncate-end", children: row }, index))), onRowPick: () => {
|
|
1429
|
+
updateFoldBlock(null);
|
|
1430
|
+
setHovered(false);
|
|
1431
|
+
} }) }))] })), 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, topRightLabel: topRightLabel, 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
1432
|
}
|
|
956
1433
|
/**
|
|
957
1434
|
* Hard-wrap text into visual rows of at most `width` columns (CJK-aware via
|
|
958
1435
|
* stringWidth). Used by the input renderer so long lines wrap instead of
|
|
959
|
-
* truncating, with exact caret-row mapping.
|
|
1436
|
+
* truncating, with exact caret-row mapping. Wrapping only ever breaks
|
|
1437
|
+
* BETWEEN grapheme clusters: iterating code points would split ZWJ emoji
|
|
1438
|
+
* and combining sequences across rows, leaving a broken half at each edge
|
|
1439
|
+
* and desyncing the caret's row arithmetic (which walks cluster
|
|
1440
|
+
* boundaries).
|
|
960
1441
|
*/
|
|
961
1442
|
function wrapToWidth(text, width) {
|
|
962
1443
|
const rows = [];
|
|
1444
|
+
const segmenter = getGraphemeSegmenter();
|
|
963
1445
|
for (const line of text.split('\n')) {
|
|
964
1446
|
if (line === '') {
|
|
965
1447
|
rows.push('');
|
|
@@ -967,15 +1449,15 @@ function wrapToWidth(text, width) {
|
|
|
967
1449
|
}
|
|
968
1450
|
let current = '';
|
|
969
1451
|
let currentWidth = 0;
|
|
970
|
-
for (const
|
|
971
|
-
const w = stringWidth(
|
|
1452
|
+
for (const { segment } of segmenter.segment(line)) {
|
|
1453
|
+
const w = stringWidth(segment);
|
|
972
1454
|
if (currentWidth + w > width && current !== '') {
|
|
973
1455
|
rows.push(current);
|
|
974
|
-
current =
|
|
1456
|
+
current = segment;
|
|
975
1457
|
currentWidth = w;
|
|
976
1458
|
}
|
|
977
1459
|
else {
|
|
978
|
-
current +=
|
|
1460
|
+
current += segment;
|
|
979
1461
|
currentWidth += w;
|
|
980
1462
|
}
|
|
981
1463
|
}
|
|
@@ -983,3 +1465,48 @@ function wrapToWidth(text, width) {
|
|
|
983
1465
|
}
|
|
984
1466
|
return rows;
|
|
985
1467
|
}
|
|
1468
|
+
/**
|
|
1469
|
+
* Inverse of {@link wrapToWidth}: map a click position (visual row index +
|
|
1470
|
+
* visual column) back to a UTF-16 offset in the original text. Walks the
|
|
1471
|
+
* same grapheme boundaries with the same break rule, so every visual row's
|
|
1472
|
+
* start offset is known exactly. Within the clicked row, the caret snaps to
|
|
1473
|
+
* the boundary nearest the click: a grapheme whose midpoint lies past the
|
|
1474
|
+
* click column takes the caret before it, otherwise after.
|
|
1475
|
+
*/
|
|
1476
|
+
function clickToCursorOffset(text, width, visualLine, visualCol) {
|
|
1477
|
+
const segmenter = getGraphemeSegmenter();
|
|
1478
|
+
let row = 0;
|
|
1479
|
+
let offset = 0;
|
|
1480
|
+
for (const line of text.split('\n')) {
|
|
1481
|
+
if (line === '') {
|
|
1482
|
+
if (row === visualLine)
|
|
1483
|
+
return offset;
|
|
1484
|
+
row++;
|
|
1485
|
+
offset++; // the '\n'
|
|
1486
|
+
continue;
|
|
1487
|
+
}
|
|
1488
|
+
let currentWidth = 0;
|
|
1489
|
+
for (const { segment } of segmenter.segment(line)) {
|
|
1490
|
+
const w = stringWidth(segment);
|
|
1491
|
+
if (currentWidth + w > width && currentWidth > 0) {
|
|
1492
|
+
if (row === visualLine)
|
|
1493
|
+
return offset; // end of the clicked wrapped row
|
|
1494
|
+
row++;
|
|
1495
|
+
currentWidth = 0;
|
|
1496
|
+
}
|
|
1497
|
+
if (row === visualLine) {
|
|
1498
|
+
if (currentWidth + w / 2 > visualCol)
|
|
1499
|
+
return offset;
|
|
1500
|
+
if (currentWidth + w > visualCol)
|
|
1501
|
+
return offset + segment.length;
|
|
1502
|
+
}
|
|
1503
|
+
currentWidth += w;
|
|
1504
|
+
offset += segment.length;
|
|
1505
|
+
}
|
|
1506
|
+
if (row === visualLine)
|
|
1507
|
+
return offset;
|
|
1508
|
+
row++;
|
|
1509
|
+
offset++; // the '\n'
|
|
1510
|
+
}
|
|
1511
|
+
return offset;
|
|
1512
|
+
}
|