@robota-sdk/agent-ui-terminal 3.0.0-beta.81
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/LICENSE +661 -0
- package/README.md +27 -0
- package/dist/node/index.cjs +32 -0
- package/dist/node/index.d.cts +1066 -0
- package/dist/node/index.d.cts.map +1 -0
- package/dist/node/index.d.ts +1066 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +33 -0
- package/dist/node/index.js.map +1 -0
- package/package.json +93 -0
- package/src/App.tsx +146 -0
- package/src/AppPresentation.tsx +190 -0
- package/src/AppView.tsx +26 -0
- package/src/BackgroundTaskPanel.tsx +73 -0
- package/src/CjkTextInput.tsx +336 -0
- package/src/ConfirmPrompt.tsx +140 -0
- package/src/ContextWarningBanner.tsx +45 -0
- package/src/ExecutionWorkspaceDetailPane.tsx +85 -0
- package/src/ExecutionWorkspaceSwitcher.tsx +213 -0
- package/src/HistorySearchOverlay.tsx +150 -0
- package/src/InputArea.tsx +338 -0
- package/src/ListPicker.tsx +158 -0
- package/src/MenuSelect.tsx +159 -0
- package/src/MessageList.tsx +308 -0
- package/src/MultiSelectList.tsx +294 -0
- package/src/PendingActionPrompt.tsx +121 -0
- package/src/PermissionPrompt.tsx +186 -0
- package/src/PluginTUI.tsx +258 -0
- package/src/RoleLabel.tsx +72 -0
- package/src/SafeText.tsx +66 -0
- package/src/SessionEventNotices.tsx +27 -0
- package/src/SessionPicker.tsx +72 -0
- package/src/SessionStatusBar.tsx +76 -0
- package/src/SlashAutocomplete.tsx +154 -0
- package/src/StatusBar.tsx +284 -0
- package/src/StreamingIndicator.tsx +145 -0
- package/src/SupervisedSessionView.tsx +455 -0
- package/src/TextPrompt.tsx +112 -0
- package/src/ThemePicker.tsx +366 -0
- package/src/ToolCommandOutput.tsx +48 -0
- package/src/ToolDiffBlock.tsx +44 -0
- package/src/TransportTUI.tsx +166 -0
- package/src/TuiInteractionChannel.ts +567 -0
- package/src/UpdateNotice.tsx +18 -0
- package/src/UsageSummaryEntry.tsx +49 -0
- package/src/WaveText.tsx +70 -0
- package/src/__tests__/ListPicker.test.tsx +188 -0
- package/src/__tests__/MenuSelect.test.tsx +127 -0
- package/src/__tests__/PendingActionPrompt.test.tsx +193 -0
- package/src/__tests__/PluginTUI.test.tsx +167 -0
- package/src/__tests__/SlashAutocomplete.test.tsx +162 -0
- package/src/__tests__/SupervisedSessionView-render.test.tsx +24 -0
- package/src/__tests__/SupervisedSessionView.test.tsx +629 -0
- package/src/__tests__/TextPrompt.test.tsx +112 -0
- package/src/__tests__/TransportTUI.test.tsx +124 -0
- package/src/__tests__/TuiInteractionChannel.askUser.test.ts +124 -0
- package/src/__tests__/TuiInteractionChannel.display-contract.test.ts +284 -0
- package/src/__tests__/TuiInteractionChannel.lifecycle.test.ts +642 -0
- package/src/__tests__/UpdateNotice.test.tsx +15 -0
- package/src/__tests__/abort-after-permission.test.tsx +160 -0
- package/src/__tests__/abort-streaming-e2e.test.tsx +183 -0
- package/src/__tests__/attention-bell-and-marks.test.ts +144 -0
- package/src/__tests__/autocomplete-editing-keys.test.tsx +127 -0
- package/src/__tests__/background-task-panel-countdown.test.tsx +75 -0
- package/src/__tests__/background-task-panel.test.tsx +98 -0
- package/src/__tests__/background-task-row-format.test.ts +152 -0
- package/src/__tests__/channel-factory-integration.test.ts +150 -0
- package/src/__tests__/cjk-defer-submit.test.tsx +67 -0
- package/src/__tests__/cjk-fallback-render.test.tsx +78 -0
- package/src/__tests__/cjk-text-input-flow.test.ts +186 -0
- package/src/__tests__/cjk-text-input.test.ts +191 -0
- package/src/__tests__/command-handoff-pty-e2e.test.ts +107 -0
- package/src/__tests__/command-output-summary.test.ts +95 -0
- package/src/__tests__/command-result-handler.test.ts +171 -0
- package/src/__tests__/compact-event-bridge.test.ts +20 -0
- package/src/__tests__/confirm-permission-flow.test.ts +149 -0
- package/src/__tests__/confirm-prompt.test.tsx +103 -0
- package/src/__tests__/contextual-keybindings-input.test.tsx +158 -0
- package/src/__tests__/default-tui-cli-adapter-host-settings.test.ts +45 -0
- package/src/__tests__/effort-reaches-the-session.test.ts +41 -0
- package/src/__tests__/execution-workspace-switcher.test.tsx +147 -0
- package/src/__tests__/execution-workspace-view-model.test.ts +94 -0
- package/src/__tests__/external-prompt-notice.test.tsx +100 -0
- package/src/__tests__/fixtures/command-handoff-driver.tsx +111 -0
- package/src/__tests__/fixtures/fake-editor.sh +7 -0
- package/src/__tests__/fixtures/terminal-handoff-driver.tsx +90 -0
- package/src/__tests__/fork-attach.test.tsx +235 -0
- package/src/__tests__/helpers/terminal-profiles.ts +152 -0
- package/src/__tests__/helpers/vt-cursor-interpreter.ts +175 -0
- package/src/__tests__/history-clear-broadcast.test.ts +66 -0
- package/src/__tests__/history-search-overlay.test.tsx +329 -0
- package/src/__tests__/humanize-tool-name.test.ts +30 -0
- package/src/__tests__/input-area-bottom-border.test.tsx +45 -0
- package/src/__tests__/input-area-flow.test.ts +199 -0
- package/src/__tests__/input-area-focus-handoff.test.tsx +77 -0
- package/src/__tests__/interaction-channel-charter.test.ts +13 -0
- package/src/__tests__/key-hint-consistency.test.tsx +249 -0
- package/src/__tests__/key-hint-footer.test.tsx +69 -0
- package/src/__tests__/live-trace-reaches-session.test.ts +14 -0
- package/src/__tests__/message-list-rendering.test.tsx +473 -0
- package/src/__tests__/org-policy-reaches-the-channel.test.ts +77 -0
- package/src/__tests__/output-style-reaches-session.test.ts +35 -0
- package/src/__tests__/palette-consistency.test.ts +330 -0
- package/src/__tests__/permission-prompt-availability.test.tsx +23 -0
- package/src/__tests__/permission-prompt-peer.test.tsx +34 -0
- package/src/__tests__/product-display-name.test.tsx +79 -0
- package/src/__tests__/prompt-queue.test.tsx +260 -0
- package/src/__tests__/provider-definitions-reach-the-session.test.ts +75 -0
- package/src/__tests__/pty/ask-user-question.ptytest.ts +72 -0
- package/src/__tests__/pty/background-work-switcher.ptytest.ts +53 -0
- package/src/__tests__/pty/behavior-2437-git.ptytest.ts +342 -0
- package/src/__tests__/pty/fixtures/ask-user-question.jsonl +3 -0
- package/src/__tests__/pty/fixtures/replay-conversation.jsonl +4 -0
- package/src/__tests__/pty/fixtures/screen-005-ask-shapes.jsonl +5 -0
- package/src/__tests__/pty/fixtures/screen-005-permission.jsonl +3 -0
- package/src/__tests__/pty/fixtures/screen-006-colors.jsonl +3 -0
- package/src/__tests__/pty/fixtures/screen-2002-themes.jsonl +5 -0
- package/src/__tests__/pty/flag-tui.ptytest.ts +56 -0
- package/src/__tests__/pty/flow-2006-deep-link.ptytest.ts +291 -0
- package/src/__tests__/pty/ime-cursor-tmux.ptytest.ts +165 -0
- package/src/__tests__/pty/ime-cursor.ptytest.ts +186 -0
- package/src/__tests__/pty/isolated-home.ts +64 -0
- package/src/__tests__/pty/org-policy.ptytest.ts +71 -0
- package/src/__tests__/pty/provider-setup.ptytest.ts +61 -0
- package/src/__tests__/pty/pty-driver.ts +153 -0
- package/src/__tests__/pty/replay-conversation.ptytest.ts +68 -0
- package/src/__tests__/pty/screen-005-prompt-footers.ptytest.ts +134 -0
- package/src/__tests__/pty/screen-006-no-color.ptytest.ts +138 -0
- package/src/__tests__/pty/screen-010-scrollback.ptytest.ts +123 -0
- package/src/__tests__/pty/screen-1993-scrollback.ptytest.ts +124 -0
- package/src/__tests__/pty/screen-2002-themes.ptytest.ts +292 -0
- package/src/__tests__/pty/screen-2670-prepark.ptytest.ts +228 -0
- package/src/__tests__/pty/screen-reader-mode.ptytest.ts +228 -0
- package/src/__tests__/pty/settings-screen.ptytest.ts +49 -0
- package/src/__tests__/pty/spawn-pty.test.ts +120 -0
- package/src/__tests__/pty/spawn-pty.ts +229 -0
- package/src/__tests__/pty/terminal-handoff.ptytest.ts +79 -0
- package/src/__tests__/pty/tui-pty.ptytest.ts +65 -0
- package/src/__tests__/pty-driver-dispose.test.ts +43 -0
- package/src/__tests__/real-cursor-positioning.test.tsx +263 -0
- package/src/__tests__/rename-broadcast-persistence.test.tsx +140 -0
- package/src/__tests__/render-channel-options.test.ts +201 -0
- package/src/__tests__/render-lifecycle.test.ts +36 -0
- package/src/__tests__/render-markdown.test.ts +164 -0
- package/src/__tests__/rendered-markdown-styling.test.tsx +98 -0
- package/src/__tests__/safe-text-boundary.test.tsx +75 -0
- package/src/__tests__/screen-reader-input.test.tsx +106 -0
- package/src/__tests__/screen-reader-labels.test.ts +91 -0
- package/src/__tests__/screen-reader-menus.test.tsx +200 -0
- package/src/__tests__/screen-reader-pacing.test.ts +230 -0
- package/src/__tests__/screen-reader-prepark-echo.test.tsx +200 -0
- package/src/__tests__/screen-reader-render-options.test.ts +262 -0
- package/src/__tests__/screen-reader-stdout.test.ts +322 -0
- package/src/__tests__/screen-reader-turn-marks-port.test.tsx +85 -0
- package/src/__tests__/screen-reader-turn-signals.test.tsx +125 -0
- package/src/__tests__/sec-019-terminal-sanitizer.test.ts +276 -0
- package/src/__tests__/sec-019-tool-label-render.test.tsx +207 -0
- package/src/__tests__/selection-flow.test.ts +61 -0
- package/src/__tests__/session-event-notices.test.tsx +31 -0
- package/src/__tests__/session-init-poller.test.ts +102 -0
- package/src/__tests__/session-switch-channel.test.tsx +470 -0
- package/src/__tests__/shutdown-signal.test.ts +34 -0
- package/src/__tests__/status-activity.test.ts +71 -0
- package/src/__tests__/status-bar.test.tsx +282 -0
- package/src/__tests__/status-glyph.test.ts +30 -0
- package/src/__tests__/streaming-indicator.test.tsx +177 -0
- package/src/__tests__/terminal-capabilities.test.ts +153 -0
- package/src/__tests__/terminal-handoff-controller.test.ts +173 -0
- package/src/__tests__/terminal-handoff-pty-e2e.test.ts +77 -0
- package/src/__tests__/text-prompt-flow.test.ts +77 -0
- package/src/__tests__/theme-picker.test.tsx +345 -0
- package/src/__tests__/transport-boundary.test.ts +11 -0
- package/src/__tests__/tui-app-channel-port.test.ts +31 -0
- package/src/__tests__/tui-channel-init-failure.test.ts +62 -0
- package/src/__tests__/tui-channel-lifecycle-coordinator.test.ts +258 -0
- package/src/__tests__/tui-interaction-queues.test.ts +139 -0
- package/src/__tests__/tui-preset-capabilities.test.ts +33 -0
- package/src/__tests__/tui-state-manager.error-stall.test.ts +169 -0
- package/src/__tests__/tui-state-manager.test.ts +451 -0
- package/src/__tests__/ui-intent-rendering.test.tsx +291 -0
- package/src/__tests__/wave-text.test.tsx +133 -0
- package/src/app-banner.tsx +42 -0
- package/src/app-static-items.ts +35 -0
- package/src/app-view-model.ts +136 -0
- package/src/attention/__tests__/attention-coordinator.test.ts +166 -0
- package/src/attention/__tests__/attention-tracker.test.ts +103 -0
- package/src/attention/__tests__/countdown.test.ts +15 -0
- package/src/attention/__tests__/focus-input-filter.test.ts +92 -0
- package/src/attention/__tests__/interval-recap.test.ts +49 -0
- package/src/attention/attention-coordinator.ts +71 -0
- package/src/attention/attention-tracker.ts +124 -0
- package/src/attention/countdown.ts +24 -0
- package/src/attention/focus-input-filter.ts +131 -0
- package/src/attention/interval-recap.ts +148 -0
- package/src/attention/time-units.ts +6 -0
- package/src/attention-bell.ts +79 -0
- package/src/attributed-user-echo.ts +30 -0
- package/src/background-task-row-format.ts +99 -0
- package/src/command-interaction.ts +8 -0
- package/src/command-output-summary.ts +129 -0
- package/src/create-default-tui-cli-adapter.ts +45 -0
- package/src/execution-workspace-switcher-selection.ts +118 -0
- package/src/execution-workspace-view-model.ts +139 -0
- package/src/external-prompt-notice.tsx +38 -0
- package/src/flows/__tests__/background-focus-flow.test.ts +48 -0
- package/src/flows/__tests__/defer-submit.test.ts +87 -0
- package/src/flows/__tests__/real-cursor-flow.test.ts +124 -0
- package/src/flows/background-focus-flow.ts +44 -0
- package/src/flows/cjk-cursor-motion.ts +75 -0
- package/src/flows/cjk-text-deletion.ts +53 -0
- package/src/flows/cjk-text-input-flow.ts +248 -0
- package/src/flows/cjk-text-input-types.ts +24 -0
- package/src/flows/confirm-prompt-flow.ts +90 -0
- package/src/flows/defer-submit.ts +67 -0
- package/src/flows/fork-attach-flow.ts +62 -0
- package/src/flows/input-area-flow.ts +188 -0
- package/src/flows/permission-prompt-flow.ts +108 -0
- package/src/flows/real-cursor-flow.ts +83 -0
- package/src/flows/selection-flow.ts +205 -0
- package/src/flows/session-init-poller.ts +76 -0
- package/src/flows/text-prompt-flow.ts +93 -0
- package/src/history-search/__tests__/history-search-flow.test.ts +86 -0
- package/src/history-search/history-search-flow.ts +94 -0
- package/src/history-search/useHistorySearch.ts +336 -0
- package/src/hooks/__tests__/use-app-loop-escape.test.tsx +74 -0
- package/src/hooks/__tests__/use-plugin-callbacks.test.ts +18 -0
- package/src/hooks/__tests__/use-tui-channel-stability.test.tsx +100 -0
- package/src/hooks/__tests__/useAppThemeState.test.tsx +130 -0
- package/src/hooks/command-result-handler.ts +44 -0
- package/src/hooks/side-effects-types.ts +45 -0
- package/src/hooks/useAppController.ts +267 -0
- package/src/hooks/useAppInputBindings.ts +145 -0
- package/src/hooks/useAppInteractionState.ts +81 -0
- package/src/hooks/useAppLifecycleState.ts +81 -0
- package/src/hooks/useAppOverlays.ts +74 -0
- package/src/hooks/useAppScreenState.ts +81 -0
- package/src/hooks/useAppSubmissionState.ts +45 -0
- package/src/hooks/useAppThemeState.ts +157 -0
- package/src/hooks/useAppWorkspaceState.ts +149 -0
- package/src/hooks/useAppearanceSettings.ts +30 -0
- package/src/hooks/useAutocomplete.ts +101 -0
- package/src/hooks/useBackgroundPanel.ts +58 -0
- package/src/hooks/useCountdownTick.ts +24 -0
- package/src/hooks/useExecutionDetailPage.ts +69 -0
- package/src/hooks/useForkAttach.ts +60 -0
- package/src/hooks/useInputAreaHistorySearch.ts +72 -0
- package/src/hooks/useInputAreaKeys.ts +114 -0
- package/src/hooks/useNumberedSelection.ts +76 -0
- package/src/hooks/usePluginCallbacks.ts +38 -0
- package/src/hooks/usePluginScreenData.ts +85 -0
- package/src/hooks/useRealCursorPosition.ts +125 -0
- package/src/hooks/useRenderMarkdown.ts +38 -0
- package/src/hooks/useScreenReaderTurnSignals.ts +113 -0
- package/src/hooks/useSideEffects.ts +144 -0
- package/src/hooks/useStatusLineSettings.ts +50 -0
- package/src/hooks/useTerminalHandoffSuspension.ts +30 -0
- package/src/hooks/useTuiChannel.ts +129 -0
- package/src/humanize-tool-name.ts +51 -0
- package/src/index.ts +56 -0
- package/src/input-area-rules.tsx +71 -0
- package/src/json-value.ts +18 -0
- package/src/key-hint-footer.tsx +54 -0
- package/src/keybindings/__tests__/keybinding-registry.test.ts +228 -0
- package/src/keybindings/__tests__/node-keybindings-source.test.ts +111 -0
- package/src/keybindings/keybinding-catalogue.ts +171 -0
- package/src/keybindings/keybinding-registry.ts +303 -0
- package/src/keybindings/keybinding-syntax.ts +155 -0
- package/src/keybindings/keybindings-context.tsx +124 -0
- package/src/keybindings/node-keybindings-source.ts +189 -0
- package/src/numbered-list.tsx +113 -0
- package/src/plugin-tui-handlers.ts +163 -0
- package/src/product-display-name-context.tsx +30 -0
- package/src/render-markdown.ts +209 -0
- package/src/render.tsx +544 -0
- package/src/sanitize-terminal-text.ts +171 -0
- package/src/screen-reader-announcement.ts +52 -0
- package/src/screen-reader-context.tsx +32 -0
- package/src/screen-reader-labels.ts +64 -0
- package/src/screen-reader-pacing-context.tsx +47 -0
- package/src/screen-reader-pacing.ts +178 -0
- package/src/screen-reader-stdout.ts +328 -0
- package/src/shutdown-signal.ts +34 -0
- package/src/status-activity.ts +70 -0
- package/src/status-glyph.ts +78 -0
- package/src/terminal-capabilities-context.tsx +27 -0
- package/src/terminal-capabilities.ts +77 -0
- package/src/terminal-focus-reporting.ts +50 -0
- package/src/terminal-handoff-controller.ts +98 -0
- package/src/terminal-marks.ts +63 -0
- package/src/theme/__tests__/built-in-themes.test.ts +117 -0
- package/src/theme/__tests__/color-vision.test.ts +82 -0
- package/src/theme/__tests__/theme-context.test.tsx +146 -0
- package/src/theme/__tests__/theme-document.test.ts +222 -0
- package/src/theme/__tests__/theme-registry.test.ts +138 -0
- package/src/theme/__tests__/theme-styles.test.ts +233 -0
- package/src/theme/built-in-themes.ts +342 -0
- package/src/theme/color-vision.ts +171 -0
- package/src/theme/index.ts +45 -0
- package/src/theme/theme-context.tsx +81 -0
- package/src/theme/theme-contracts.ts +139 -0
- package/src/theme/theme-document.ts +278 -0
- package/src/theme/theme-registry.ts +127 -0
- package/src/theme/theme-styles.ts +177 -0
- package/src/tool-summary-status.ts +59 -0
- package/src/tui-app-channel-port.ts +83 -0
- package/src/tui-channel-lifecycle-coordinator.ts +149 -0
- package/src/tui-channel-options.ts +172 -0
- package/src/tui-cli-adapter-context.tsx +13 -0
- package/src/tui-cli-adapter.ts +24 -0
- package/src/tui-interaction-queues.ts +163 -0
- package/src/tui-session-binding.ts +52 -0
- package/src/tui-session-event-projector.ts +140 -0
- package/src/tui-session-events.ts +82 -0
- package/src/tui-session-notice-store.ts +43 -0
- package/src/tui-session-options.ts +118 -0
- package/src/tui-state-manager.ts +327 -0
- package/src/types.ts +19 -0
- package/src/use-terminal-title.ts +29 -0
- package/src/utils/__tests__/edit-diff.test.ts +426 -0
- package/src/utils/__tests__/paste-detection.test.ts +116 -0
- package/src/utils/__tests__/paste-labels.test.ts +46 -0
- package/src/utils/__tests__/tool-call-extractor.test.ts +227 -0
- package/src/utils/__tests__/tool-diff-summary.test.ts +104 -0
- package/src/utils/edit-diff.ts +151 -0
- package/src/utils/input-top-border.ts +19 -0
- package/src/utils/paste-labels.ts +9 -0
- package/src/utils/tool-call-extractor.ts +92 -0
- package/src/utils/tool-diff-summary.ts +75 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI-2004 — deletion, and what was deleted.
|
|
3
|
+
*
|
|
4
|
+
* A reader announces the line it is on. When a whole word or the whole line disappears, the only
|
|
5
|
+
* thing it can say is what is LEFT — which is exactly the information you do not need. These helpers
|
|
6
|
+
* return the removed text alongside the new value so the input can say what went.
|
|
7
|
+
*
|
|
8
|
+
* Word and line deletion are added here as well as reported: the flow had only single-character
|
|
9
|
+
* backspace, so `Ctrl+W`/`Ctrl+U` previously fell through to the printable-input path.
|
|
10
|
+
*
|
|
11
|
+
* Extracted from `cjk-text-input-flow.ts` by responsibility — that module routes keystrokes, this
|
|
12
|
+
* one computes edits.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** The result of one deletion: the new buffer, the new cursor, and the text that was removed. */
|
|
16
|
+
export interface ICjkDeletion {
|
|
17
|
+
value: string;
|
|
18
|
+
cursor: number;
|
|
19
|
+
deleted: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Remove the character before the cursor. `undefined` at the start of the buffer. */
|
|
23
|
+
export function deleteCharBeforeCursor(value: string, cursor: number): ICjkDeletion | undefined {
|
|
24
|
+
if (cursor <= 0) return undefined;
|
|
25
|
+
return {
|
|
26
|
+
value: value.slice(0, cursor - 1) + value.slice(cursor),
|
|
27
|
+
cursor: cursor - 1,
|
|
28
|
+
deleted: value.slice(cursor - 1, cursor),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Remove the word before the cursor — trailing spaces first, then the run of non-space characters,
|
|
34
|
+
* which is the behaviour every shell's `Ctrl+W` has.
|
|
35
|
+
*/
|
|
36
|
+
export function deleteWordBeforeCursor(value: string, cursor: number): ICjkDeletion | undefined {
|
|
37
|
+
if (cursor <= 0) return undefined;
|
|
38
|
+
let start = cursor;
|
|
39
|
+
while (start > 0 && /\s/u.test(value[start - 1] ?? '')) start -= 1;
|
|
40
|
+
while (start > 0 && !/\s/u.test(value[start - 1] ?? '')) start -= 1;
|
|
41
|
+
if (start === cursor) return undefined;
|
|
42
|
+
return {
|
|
43
|
+
value: value.slice(0, start) + value.slice(cursor),
|
|
44
|
+
cursor: start,
|
|
45
|
+
deleted: value.slice(start, cursor),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Remove everything before the cursor (`Ctrl+U`). */
|
|
50
|
+
export function deleteLineBeforeCursor(value: string, cursor: number): ICjkDeletion | undefined {
|
|
51
|
+
if (cursor <= 0) return undefined;
|
|
52
|
+
return { value: value.slice(cursor), cursor: 0, deleted: value.slice(0, cursor) };
|
|
53
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { moveCursorHorizontally, moveCursorVertically } from './cjk-cursor-motion.js';
|
|
2
|
+
import {
|
|
3
|
+
deleteCharBeforeCursor,
|
|
4
|
+
deleteLineBeforeCursor,
|
|
5
|
+
deleteWordBeforeCursor,
|
|
6
|
+
type ICjkDeletion,
|
|
7
|
+
} from './cjk-text-deletion.js';
|
|
8
|
+
import type {
|
|
9
|
+
ICjkTextInputFlowState,
|
|
10
|
+
TCjkDeletionScope,
|
|
11
|
+
TCjkTextInputEffect,
|
|
12
|
+
} from './cjk-text-input-types.js';
|
|
13
|
+
|
|
14
|
+
const PASTE_START = '[200~';
|
|
15
|
+
const PASTE_END = '[201~';
|
|
16
|
+
const LAST_ASCII_CONTROL_CODE = 0x1f;
|
|
17
|
+
const DELETE_CONTROL_CODE = 0x7f;
|
|
18
|
+
|
|
19
|
+
export { charIndexAtDisplayOffset, displayOffset } from './cjk-cursor-motion.js';
|
|
20
|
+
export type {
|
|
21
|
+
ICjkTextInputFlowState,
|
|
22
|
+
TCjkDeletionScope,
|
|
23
|
+
TCjkTextInputEffect,
|
|
24
|
+
} from './cjk-text-input-types.js';
|
|
25
|
+
|
|
26
|
+
export interface ICjkTextInputKey {
|
|
27
|
+
ctrl?: boolean;
|
|
28
|
+
tab?: boolean;
|
|
29
|
+
shift?: boolean;
|
|
30
|
+
upArrow?: boolean;
|
|
31
|
+
downArrow?: boolean;
|
|
32
|
+
return?: boolean;
|
|
33
|
+
leftArrow?: boolean;
|
|
34
|
+
rightArrow?: boolean;
|
|
35
|
+
backspace?: boolean;
|
|
36
|
+
delete?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ICjkTextInputFlowOptions {
|
|
40
|
+
availableWidth?: number;
|
|
41
|
+
canPaste: boolean;
|
|
42
|
+
enableVerticalNavigation?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface ICjkTextInputFlowResult {
|
|
46
|
+
state: ICjkTextInputFlowState;
|
|
47
|
+
effect: TCjkTextInputEffect;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function createCjkTextInputFlowState(value: string): ICjkTextInputFlowState {
|
|
51
|
+
return { value, cursor: value.length, isPasting: false, pasteBuffer: '' };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function syncCjkTextInputFlowState(
|
|
55
|
+
state: ICjkTextInputFlowState,
|
|
56
|
+
value: string,
|
|
57
|
+
cursorHint: number | null,
|
|
58
|
+
): ICjkTextInputFlowState {
|
|
59
|
+
if (value === state.value) {
|
|
60
|
+
return state;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
...state,
|
|
64
|
+
value,
|
|
65
|
+
cursor: cursorHint != null ? Math.min(cursorHint, value.length) : value.length,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function applyCjkTextInput(
|
|
70
|
+
state: ICjkTextInputFlowState,
|
|
71
|
+
input: string,
|
|
72
|
+
key: ICjkTextInputKey,
|
|
73
|
+
options: ICjkTextInputFlowOptions,
|
|
74
|
+
): ICjkTextInputFlowResult {
|
|
75
|
+
const pasteResult = applyPasteBoundaryInput(state, input, options);
|
|
76
|
+
if (pasteResult !== undefined) return pasteResult;
|
|
77
|
+
const controlResult = applyControlInput(state, input, key, options);
|
|
78
|
+
if (controlResult !== undefined) return controlResult;
|
|
79
|
+
const cursorResult = applyCursorInput(state, key, options);
|
|
80
|
+
if (cursorResult !== undefined) return cursorResult;
|
|
81
|
+
return insertPrintableInput(state, input);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function applyCjkTextPaste(
|
|
85
|
+
state: ICjkTextInputFlowState,
|
|
86
|
+
text: string,
|
|
87
|
+
options: ICjkTextInputFlowOptions,
|
|
88
|
+
): ICjkTextInputFlowResult {
|
|
89
|
+
const normalizedText = text.replace(/\r\n?/g, '\n');
|
|
90
|
+
if (normalizedText.length === 0) {
|
|
91
|
+
return { state, effect: { type: 'none' } };
|
|
92
|
+
}
|
|
93
|
+
if (normalizedText.includes('\n') && options.canPaste) {
|
|
94
|
+
return { state, effect: { type: 'paste', text: normalizedText, cursor: state.cursor } };
|
|
95
|
+
}
|
|
96
|
+
return insertPrintableInput(state, normalizedText);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function applyPasteBoundaryInput(
|
|
100
|
+
state: ICjkTextInputFlowState,
|
|
101
|
+
input: string,
|
|
102
|
+
options: ICjkTextInputFlowOptions,
|
|
103
|
+
): ICjkTextInputFlowResult | undefined {
|
|
104
|
+
if (input === PASTE_START || input.startsWith(PASTE_START)) {
|
|
105
|
+
return startBracketedPaste(state, input);
|
|
106
|
+
}
|
|
107
|
+
if (state.isPasting) {
|
|
108
|
+
return continueBracketedPaste(state, input, options);
|
|
109
|
+
}
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function applyControlInput(
|
|
114
|
+
state: ICjkTextInputFlowState,
|
|
115
|
+
input: string,
|
|
116
|
+
key: ICjkTextInputKey,
|
|
117
|
+
options: ICjkTextInputFlowOptions,
|
|
118
|
+
): ICjkTextInputFlowResult | undefined {
|
|
119
|
+
if ((key.ctrl === true && input === 'c') || key.tab === true) {
|
|
120
|
+
return { state, effect: { type: 'none' } };
|
|
121
|
+
}
|
|
122
|
+
if (key.ctrl === true && (input === 'w' || input === 'u')) {
|
|
123
|
+
const scope: TCjkDeletionScope = input === 'w' ? 'word' : 'line';
|
|
124
|
+
const remove = scope === 'word' ? deleteWordBeforeCursor : deleteLineBeforeCursor;
|
|
125
|
+
return applyDeletion(state, remove(state.value, state.cursor), scope);
|
|
126
|
+
}
|
|
127
|
+
if (key.return === true) {
|
|
128
|
+
return { state, effect: { type: 'submit', value: state.value } };
|
|
129
|
+
}
|
|
130
|
+
if (input.length > 1 && (input.includes('\n') || input.includes('\r')) && options.canPaste) {
|
|
131
|
+
return {
|
|
132
|
+
state,
|
|
133
|
+
effect: { type: 'paste', text: input.replace(/\r\n?/g, '\n'), cursor: state.cursor },
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function applyCursorInput(
|
|
140
|
+
state: ICjkTextInputFlowState,
|
|
141
|
+
key: ICjkTextInputKey,
|
|
142
|
+
options: ICjkTextInputFlowOptions,
|
|
143
|
+
): ICjkTextInputFlowResult | undefined {
|
|
144
|
+
if (key.upArrow === true || key.downArrow === true) {
|
|
145
|
+
if (options.enableVerticalNavigation === false) {
|
|
146
|
+
return { state, effect: { type: 'none' } };
|
|
147
|
+
}
|
|
148
|
+
return moveCursorVertically(
|
|
149
|
+
state,
|
|
150
|
+
key.upArrow === true ? 'up' : 'down',
|
|
151
|
+
options.availableWidth,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
if (key.leftArrow === true) {
|
|
155
|
+
return moveCursorHorizontally(state, 'left');
|
|
156
|
+
}
|
|
157
|
+
if (key.rightArrow === true) {
|
|
158
|
+
return moveCursorHorizontally(state, 'right');
|
|
159
|
+
}
|
|
160
|
+
if (key.backspace === true || key.delete === true) {
|
|
161
|
+
return applyDeletion(state, deleteCharBeforeCursor(state.value, state.cursor));
|
|
162
|
+
}
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function filterPrintable(input: string | null | undefined): string {
|
|
167
|
+
if (!input || input.length === 0) return '';
|
|
168
|
+
let output = '';
|
|
169
|
+
for (const char of input) {
|
|
170
|
+
const code = char.charCodeAt(0);
|
|
171
|
+
if (code > LAST_ASCII_CONTROL_CODE && code !== DELETE_CONTROL_CODE) {
|
|
172
|
+
output += char;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return output;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function insertAtCursor(
|
|
179
|
+
value: string,
|
|
180
|
+
cursor: number,
|
|
181
|
+
input: string,
|
|
182
|
+
): { value: string; cursor: number } {
|
|
183
|
+
const next = value.slice(0, cursor) + input + value.slice(cursor);
|
|
184
|
+
return { value: next, cursor: cursor + input.length };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function startBracketedPaste(
|
|
188
|
+
state: ICjkTextInputFlowState,
|
|
189
|
+
input: string,
|
|
190
|
+
): ICjkTextInputFlowResult {
|
|
191
|
+
return {
|
|
192
|
+
state: { ...state, isPasting: true, pasteBuffer: input.slice(PASTE_START.length) },
|
|
193
|
+
effect: { type: 'none' },
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function continueBracketedPaste(
|
|
198
|
+
state: ICjkTextInputFlowState,
|
|
199
|
+
input: string,
|
|
200
|
+
options: ICjkTextInputFlowOptions,
|
|
201
|
+
): ICjkTextInputFlowResult {
|
|
202
|
+
if (input !== PASTE_END && !input.includes(PASTE_END)) {
|
|
203
|
+
return {
|
|
204
|
+
state: { ...state, pasteBuffer: state.pasteBuffer + input },
|
|
205
|
+
effect: { type: 'none' },
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
const beforeMarker = input.split(PASTE_END)[0] ?? '';
|
|
209
|
+
const nextState = { ...state, isPasting: false, pasteBuffer: '' };
|
|
210
|
+
return applyCjkTextPaste(nextState, state.pasteBuffer + beforeMarker, options);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Apply a deletion result. A word/line delete carries the removed text so the caller can announce
|
|
215
|
+
* it; a character delete does not — the reader already heard the character go.
|
|
216
|
+
*/
|
|
217
|
+
function applyDeletion(
|
|
218
|
+
state: ICjkTextInputFlowState,
|
|
219
|
+
deletion: ICjkDeletion | undefined,
|
|
220
|
+
scope?: TCjkDeletionScope,
|
|
221
|
+
): ICjkTextInputFlowResult {
|
|
222
|
+
if (deletion === undefined) {
|
|
223
|
+
return { state, effect: { type: 'none' } };
|
|
224
|
+
}
|
|
225
|
+
return {
|
|
226
|
+
state: { ...state, value: deletion.value, cursor: deletion.cursor },
|
|
227
|
+
effect: {
|
|
228
|
+
type: 'change',
|
|
229
|
+
value: deletion.value,
|
|
230
|
+
...(scope !== undefined ? { deleted: deletion.deleted, deletedScope: scope } : {}),
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function insertPrintableInput(
|
|
236
|
+
state: ICjkTextInputFlowState,
|
|
237
|
+
input: string,
|
|
238
|
+
): ICjkTextInputFlowResult {
|
|
239
|
+
const printable = filterPrintable(input);
|
|
240
|
+
if (printable.length === 0) {
|
|
241
|
+
return { state, effect: { type: 'none' } };
|
|
242
|
+
}
|
|
243
|
+
const result = insertAtCursor(state.value, state.cursor, printable);
|
|
244
|
+
return {
|
|
245
|
+
state: { ...state, value: result.value, cursor: result.cursor },
|
|
246
|
+
effect: { type: 'change', value: result.value },
|
|
247
|
+
};
|
|
248
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Leaf type module for the CJK text-input flow state and effect union.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `cjk-text-input-flow.ts` so `cjk-cursor-motion.ts` can depend on these types
|
|
5
|
+
* without importing back from `cjk-text-input-flow.ts`, which previously created an import cycle
|
|
6
|
+
* between the two (`cjk-cursor-motion.ts` -> `cjk-text-input-flow.ts` -> `cjk-cursor-motion.ts`).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface ICjkTextInputFlowState {
|
|
10
|
+
value: string;
|
|
11
|
+
cursor: number;
|
|
12
|
+
isPasting: boolean;
|
|
13
|
+
pasteBuffer: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** CLI-2004: a word/line delete is announced; a single-character backspace is not (it is heard). */
|
|
17
|
+
export type TCjkDeletionScope = 'word' | 'line';
|
|
18
|
+
|
|
19
|
+
export type TCjkTextInputEffect =
|
|
20
|
+
| { type: 'none' }
|
|
21
|
+
| { type: 'change'; value: string; deleted?: string; deletedScope?: TCjkDeletionScope }
|
|
22
|
+
| { type: 'submit'; value: string }
|
|
23
|
+
| { type: 'paste'; text: string; cursor: number }
|
|
24
|
+
| { type: 'render' };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applySelectionInput,
|
|
3
|
+
getDirectionalSelectionInputAction,
|
|
4
|
+
type ISelectionFlowState,
|
|
5
|
+
type ISelectionInputKey,
|
|
6
|
+
type TSelectionEffect,
|
|
7
|
+
type TSelectionInputAction,
|
|
8
|
+
} from './selection-flow.js';
|
|
9
|
+
|
|
10
|
+
export type TConfirmPromptInputAction = TSelectionInputAction | { type: 'shortcut'; index: number };
|
|
11
|
+
|
|
12
|
+
export function getConfirmPromptInputAction(
|
|
13
|
+
input: string,
|
|
14
|
+
key: ISelectionInputKey,
|
|
15
|
+
optionCount: number,
|
|
16
|
+
): TConfirmPromptInputAction | undefined {
|
|
17
|
+
const action = getDirectionalSelectionInputAction({ ...key, escape: false });
|
|
18
|
+
if (action !== undefined) {
|
|
19
|
+
return action;
|
|
20
|
+
}
|
|
21
|
+
if (optionCount === 2 && input === 'y') {
|
|
22
|
+
return { type: 'shortcut', index: 0 };
|
|
23
|
+
}
|
|
24
|
+
if (optionCount === 2 && input === 'n') {
|
|
25
|
+
return { type: 'shortcut', index: 1 };
|
|
26
|
+
}
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* CLI-2004 — the typed yes/no answer.
|
|
32
|
+
*
|
|
33
|
+
* A two-option arrow menu is the worst shape for a reader: both rows are announced, and which one is
|
|
34
|
+
* selected differs only by a cursor glyph and a colour. In screen-reader mode the prompt asks for a
|
|
35
|
+
* typed answer instead — `y`, `n`, `yes` or `no`, then Enter. Anything else clears the buffer and
|
|
36
|
+
* resolves nothing, so a mistyped answer cannot be read as consent.
|
|
37
|
+
*/
|
|
38
|
+
export interface ITypedConfirmState {
|
|
39
|
+
buffer: string;
|
|
40
|
+
resolved: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createTypedConfirmState(): ITypedConfirmState {
|
|
44
|
+
return { buffer: '', resolved: false };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const YES_ANSWERS = ['y', 'yes'];
|
|
48
|
+
const NO_ANSWERS = ['n', 'no'];
|
|
49
|
+
|
|
50
|
+
export function applyTypedConfirmInput(
|
|
51
|
+
state: ITypedConfirmState,
|
|
52
|
+
input: string,
|
|
53
|
+
key: ISelectionInputKey & { backspace?: boolean; delete?: boolean },
|
|
54
|
+
): { state: ITypedConfirmState; effect: TSelectionEffect } {
|
|
55
|
+
if (state.resolved) return { state, effect: { type: 'none' } };
|
|
56
|
+
if (key.backspace === true || key.delete === true) {
|
|
57
|
+
return { state: { ...state, buffer: state.buffer.slice(0, -1) }, effect: { type: 'none' } };
|
|
58
|
+
}
|
|
59
|
+
if (key.return === true) {
|
|
60
|
+
const answer = state.buffer.trim().toLowerCase();
|
|
61
|
+
if (YES_ANSWERS.includes(answer)) {
|
|
62
|
+
return { state: { buffer: '', resolved: true }, effect: { type: 'select', index: 0 } };
|
|
63
|
+
}
|
|
64
|
+
if (NO_ANSWERS.includes(answer)) {
|
|
65
|
+
return { state: { buffer: '', resolved: true }, effect: { type: 'select', index: 1 } };
|
|
66
|
+
}
|
|
67
|
+
return { state: { ...state, buffer: '' }, effect: { type: 'none' } };
|
|
68
|
+
}
|
|
69
|
+
if (/^[a-zA-Z]+$/.test(input)) {
|
|
70
|
+
return { state: { ...state, buffer: state.buffer + input }, effect: { type: 'none' } };
|
|
71
|
+
}
|
|
72
|
+
return { state, effect: { type: 'none' } };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function applyConfirmPromptInput(
|
|
76
|
+
state: ISelectionFlowState,
|
|
77
|
+
action: TConfirmPromptInputAction,
|
|
78
|
+
optionCount: number,
|
|
79
|
+
): { state: ISelectionFlowState; effect: TSelectionEffect } {
|
|
80
|
+
if (state.resolved) {
|
|
81
|
+
return { state, effect: { type: 'none' } };
|
|
82
|
+
}
|
|
83
|
+
if (typeof action !== 'string') {
|
|
84
|
+
return {
|
|
85
|
+
state: { ...state, selectedIndex: action.index, resolved: true },
|
|
86
|
+
effect: { type: 'select', index: action.index },
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return applySelectionInput(state, action, { itemCount: optionCount });
|
|
90
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI-061 — deferred submit for the CJK-IME last-character-drop race.
|
|
3
|
+
*
|
|
4
|
+
* IME composition is finalized by the terminal, which writes the final composed character into the pty as its
|
|
5
|
+
* OWN stdin event that arrives JUST AFTER the confirming `\r`. If we submit synchronously on Enter, the trailing
|
|
6
|
+
* syllable has not yet been applied to the input state, so it is dropped (`안녕하세요` → `안녕하세`). Deferring the
|
|
7
|
+
* submit a few milliseconds lets that trailing event land, after which we re-read the LATEST value and submit it.
|
|
8
|
+
*
|
|
9
|
+
* This is the same app-layer fix Gemini CLI applied on the same Ink (PR #4987/#7556); no framework change and
|
|
10
|
+
* no upstream Ink dependency (Ink #759 stays a watch-only item).
|
|
11
|
+
*
|
|
12
|
+
* Injectable `schedule`/`clear` (defaulting to the real timer) keep this unit deterministically testable —
|
|
13
|
+
* there is NO fake timer shipped in `src`; a test injects its own scheduler.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Defer window before a submit fires. Larger = safer against a slow/remote pty delivering the trailing event
|
|
18
|
+
* late, but adds this latency to EVERY submit (including plain ASCII). 50ms matches Gemini's default — long
|
|
19
|
+
* enough for a local IME finalize, short enough to feel instant. This NARROWS the race; on a very slow pty the
|
|
20
|
+
* trailing event can still land after the window (residual, accepted).
|
|
21
|
+
*/
|
|
22
|
+
export const IME_SUBMIT_DEFER_MS = 50;
|
|
23
|
+
|
|
24
|
+
type TTimer = ReturnType<typeof setTimeout>;
|
|
25
|
+
|
|
26
|
+
export interface IDeferSubmitState {
|
|
27
|
+
timer: TTimer | null;
|
|
28
|
+
/** Gates a SECOND submit / Enter only — never the input pipeline (the trailing char must still be processed). */
|
|
29
|
+
isSubmitting: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createDeferSubmitState(): IDeferSubmitState {
|
|
33
|
+
return { timer: null, isSubmitting: false };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Schedule a deferred submit. At fire time it reads `readLatest()` (the LIVE input value, not a value captured
|
|
38
|
+
* at Enter time) and submits it. A submit already in flight is ignored (no double-submit) — but note the caller
|
|
39
|
+
* MUST keep its input pipeline live during the window so `readLatest()` sees the trailing character.
|
|
40
|
+
*/
|
|
41
|
+
export function scheduleDeferredSubmit(
|
|
42
|
+
state: IDeferSubmitState,
|
|
43
|
+
readLatest: () => string,
|
|
44
|
+
submit: (value: string) => void,
|
|
45
|
+
deferMs: number = IME_SUBMIT_DEFER_MS,
|
|
46
|
+
schedule: (fn: () => void, ms: number) => TTimer = setTimeout,
|
|
47
|
+
): void {
|
|
48
|
+
if (state.isSubmitting) return; // second Enter within the window — ignore, do not double-submit
|
|
49
|
+
state.isSubmitting = true;
|
|
50
|
+
state.timer = schedule(() => {
|
|
51
|
+
state.timer = null;
|
|
52
|
+
state.isSubmitting = false;
|
|
53
|
+
submit(readLatest());
|
|
54
|
+
}, deferMs);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Cancel a pending deferred submit (call on unmount so no submit fires after teardown and no timer leaks). */
|
|
58
|
+
export function cancelDeferredSubmit(
|
|
59
|
+
state: IDeferSubmitState,
|
|
60
|
+
clear: (timer: TTimer) => void = clearTimeout,
|
|
61
|
+
): void {
|
|
62
|
+
if (state.timer !== null) {
|
|
63
|
+
clear(state.timer);
|
|
64
|
+
state.timer = null;
|
|
65
|
+
}
|
|
66
|
+
state.isSubmitting = false;
|
|
67
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI-1994: the TUI half of `attach` — turning the workspace entry the operator picked into either
|
|
3
|
+
* a session switch or a stated refusal.
|
|
4
|
+
*
|
|
5
|
+
* The DECISION is not made here. `resolveExecutionAttach` is the framework's, beside the projection
|
|
6
|
+
* that decides when the control is offered, so the surface cannot invent a second answer to "may
|
|
7
|
+
* this be attached to". What this module owns is the surface's part: the refusal reaches the
|
|
8
|
+
* operator as a message, and the switch goes through the SAME session-switch path the session
|
|
9
|
+
* picker uses — a new channel from the factory, the previous one stopped first.
|
|
10
|
+
*
|
|
11
|
+
* A switch is a VIEW change. Nothing is merged: the parent session's record is not read or written,
|
|
12
|
+
* and the two records stay separate exactly as `/fork` left them.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { resolveExecutionAttach } from '@robota-sdk/agent-framework';
|
|
16
|
+
|
|
17
|
+
import type { IExecutionWorkspaceEntry } from '@robota-sdk/agent-interface-execution';
|
|
18
|
+
|
|
19
|
+
/** The `switch-session` member of the command layer's UI-intent union, narrowed to itself. */
|
|
20
|
+
/**
|
|
21
|
+
* What an attach did: the session this terminal was pointed at.
|
|
22
|
+
*
|
|
23
|
+
* Deliberately NOT a `TCommandUiIntent` member. Attach starts at a keypress in this surface's own
|
|
24
|
+
* background panel, not at a command, so routing it through the requester-routed intent bus would
|
|
25
|
+
* add a hop with no second consumer at the end of it — and the intent's `useSideEffects` case had no
|
|
26
|
+
* emitter, which is a reader with no writer.
|
|
27
|
+
*/
|
|
28
|
+
export interface IForkAttachOutcome {
|
|
29
|
+
readonly sessionId: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface IForkAttachDeps {
|
|
33
|
+
/** Whether the forked record is still in the store this surface would switch onto. */
|
|
34
|
+
readonly hasSessionRecord: (sessionId: string) => boolean;
|
|
35
|
+
/** The surface's existing session-switch path — the one the session picker calls. */
|
|
36
|
+
readonly switchSession: (sessionId: string) => void;
|
|
37
|
+
/** How a refusal reaches the operator (a system entry in the transcript). */
|
|
38
|
+
readonly notify: (message: string) => void;
|
|
39
|
+
/** Optional host-owned command for reopening a completed fork outside the attach view. */
|
|
40
|
+
readonly formatResumeCommand?: (sessionId: string) => string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Attach to `entry`'s forked session, or tell the operator why not.
|
|
45
|
+
*
|
|
46
|
+
* Returns the UI intent that was emitted, so a caller — and a test — can see the switch as the
|
|
47
|
+
* command layer's own vocabulary rather than as a side effect. `undefined` means refused, and the
|
|
48
|
+
* refusal has already been delivered through `notify`.
|
|
49
|
+
*/
|
|
50
|
+
export function attachToForkedSession(
|
|
51
|
+
entry: IExecutionWorkspaceEntry,
|
|
52
|
+
deps: IForkAttachDeps,
|
|
53
|
+
): IForkAttachOutcome | undefined {
|
|
54
|
+
const outcome = resolveExecutionAttach(entry, { hasSessionRecord: deps.hasSessionRecord });
|
|
55
|
+
if (outcome.type === 'refused') {
|
|
56
|
+
const resumeCommand = outcome.resumeSessionId && deps.formatResumeCommand?.(outcome.resumeSessionId);
|
|
57
|
+
deps.notify(resumeCommand ? `${outcome.reason} Open it with: ${resumeCommand}` : outcome.reason);
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
deps.switchSession(outcome.sessionId);
|
|
61
|
+
return { sessionId: outcome.sessionId };
|
|
62
|
+
}
|