@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,213 @@
|
|
|
1
|
+
import { Box } from 'ink';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import { useWorkspaceSwitcherSelection } from './execution-workspace-switcher-selection.js';
|
|
5
|
+
import { formatExecutionWorkspaceEntryRow } from './execution-workspace-view-model.js';
|
|
6
|
+
import { useNumberedSelection } from './hooks/useNumberedSelection.js';
|
|
7
|
+
import {
|
|
8
|
+
KeyHintFooter,
|
|
9
|
+
SELECTION_INDICATOR,
|
|
10
|
+
SELECTION_INDICATOR_NONE,
|
|
11
|
+
type IKeyHint,
|
|
12
|
+
} from './key-hint-footer.js';
|
|
13
|
+
import { useKeybindingActions, useKeybindingHints } from './keybindings/keybindings-context.js';
|
|
14
|
+
import { formatNumberedSelectionPrompt, numberedRowPrefix } from './numbered-list.js';
|
|
15
|
+
import { Text } from './SafeText.js';
|
|
16
|
+
import { useScreenReader } from './screen-reader-context.js';
|
|
17
|
+
import { statusGlyphColor } from './status-glyph.js';
|
|
18
|
+
import { usePalette } from './theme/index.js';
|
|
19
|
+
|
|
20
|
+
import type {
|
|
21
|
+
IExecutionWorkspaceEntry,
|
|
22
|
+
IExecutionWorkspaceSnapshot,
|
|
23
|
+
} from '@robota-sdk/agent-interface-execution';
|
|
24
|
+
|
|
25
|
+
/** Footer for the workspace switcher (order: navigate → primary → dismiss). */
|
|
26
|
+
export const EXECUTION_WORKSPACE_SWITCHER_FOOTER_HINTS: readonly IKeyHint[] = [
|
|
27
|
+
{ keys: '↑↓', label: 'Navigate' },
|
|
28
|
+
{ keys: 'Enter', label: 'Switch' },
|
|
29
|
+
{ keys: 'Ctrl+B/Esc', label: 'Close' },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* CLI-1994: the extra hint shown only while a forked entry is focused. Appended rather than always
|
|
34
|
+
* present, because a key that does nothing on most rows reads as a broken key, not an unused one.
|
|
35
|
+
*/
|
|
36
|
+
export const EXECUTION_WORKSPACE_ATTACH_HINT: IKeyHint = { keys: 'a', label: 'Attach' };
|
|
37
|
+
|
|
38
|
+
interface IProps {
|
|
39
|
+
snapshot: IExecutionWorkspaceSnapshot | null;
|
|
40
|
+
selectedEntryId?: string;
|
|
41
|
+
onSelect: (entryId: string) => void;
|
|
42
|
+
onClose: () => void;
|
|
43
|
+
/**
|
|
44
|
+
* CLI-1994: attach to the focused entry's forked session — a VIEW switch onto that record, never
|
|
45
|
+
* a merge. Absent when the surface cannot switch sessions; the control is then not offered.
|
|
46
|
+
*/
|
|
47
|
+
onAttach?: (entry: IExecutionWorkspaceEntry) => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** CLI-1994: `attach` is offered by the projection; the switcher only reads what it was handed. */
|
|
51
|
+
function offersAttach(entry: IExecutionWorkspaceEntry | undefined): boolean {
|
|
52
|
+
return entry?.controls.includes('attach') === true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export default function ExecutionWorkspaceSwitcher({
|
|
56
|
+
snapshot,
|
|
57
|
+
selectedEntryId,
|
|
58
|
+
onSelect,
|
|
59
|
+
onClose,
|
|
60
|
+
onAttach,
|
|
61
|
+
}: IProps): React.ReactElement {
|
|
62
|
+
const palette = usePalette();
|
|
63
|
+
const entries = [...(snapshot?.entries ?? [])];
|
|
64
|
+
const { normalized, visibleEntries, applyAction } = useWorkspaceSwitcherSelection({
|
|
65
|
+
entries,
|
|
66
|
+
selectedEntryId,
|
|
67
|
+
onSelect,
|
|
68
|
+
onClose,
|
|
69
|
+
});
|
|
70
|
+
const focusedEntry = entries[normalized.selectedIndex];
|
|
71
|
+
const canAttach = onAttach !== undefined && offersAttach(focusedEntry);
|
|
72
|
+
|
|
73
|
+
// CLI-2004: the switcher is an arrow-key menu, so the mode gives it numbers and a typed answer.
|
|
74
|
+
const screenReader = useScreenReader();
|
|
75
|
+
const numbered = useNumberedSelection({
|
|
76
|
+
enabled: screenReader && entries.length > 0,
|
|
77
|
+
itemCount: entries.length,
|
|
78
|
+
cancellable: true,
|
|
79
|
+
onSelect: (index) => {
|
|
80
|
+
const entry = entries[index];
|
|
81
|
+
if (entry) onSelect(entry.id);
|
|
82
|
+
},
|
|
83
|
+
onCancel: onClose,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
useKeybindingActions(
|
|
87
|
+
'workspace-switcher',
|
|
88
|
+
(actions) => {
|
|
89
|
+
for (const action of actions) {
|
|
90
|
+
if (action === 'attach' && canAttach && focusedEntry) {
|
|
91
|
+
// Attaching replaces what the terminal is looking at, so the switcher has nothing left to
|
|
92
|
+
// switch between — it closes itself rather than making every caller remember to.
|
|
93
|
+
onAttach(focusedEntry);
|
|
94
|
+
onClose();
|
|
95
|
+
} else if (action === 'previous' || action === 'next' || action === 'select') {
|
|
96
|
+
applyAction(action);
|
|
97
|
+
} else if (action === 'close') {
|
|
98
|
+
applyAction('cancel');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
{ isActive: !screenReader },
|
|
103
|
+
);
|
|
104
|
+
const footerHints = useKeybindingHints('workspace-switcher', [
|
|
105
|
+
[['previous', 'next'], 'Navigate'],
|
|
106
|
+
['select', 'Switch'],
|
|
107
|
+
['close', 'Close'],
|
|
108
|
+
]);
|
|
109
|
+
const attachHint = useKeybindingHints('workspace-switcher', [['attach', 'Attach']]);
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<Box
|
|
113
|
+
flexDirection="column"
|
|
114
|
+
{...(screenReader
|
|
115
|
+
? {}
|
|
116
|
+
: { borderStyle: 'round' as const, borderColor: palette.border.focused })}
|
|
117
|
+
paddingX={1}
|
|
118
|
+
>
|
|
119
|
+
<Text color={palette.text.accent} bold>
|
|
120
|
+
Execution workspace
|
|
121
|
+
</Text>
|
|
122
|
+
<Box flexDirection="column" marginTop={1}>
|
|
123
|
+
{visibleEntries.length === 0 ? (
|
|
124
|
+
<Text dimColor>No workspace entries</Text>
|
|
125
|
+
) : screenReader ? (
|
|
126
|
+
entries.map((entry, index) => (
|
|
127
|
+
<ExecutionWorkspaceSwitcherRow
|
|
128
|
+
key={entry.id}
|
|
129
|
+
entry={entry}
|
|
130
|
+
isFocused={false}
|
|
131
|
+
selectedEntryId={selectedEntryId}
|
|
132
|
+
rowNumber={index}
|
|
133
|
+
/>
|
|
134
|
+
))
|
|
135
|
+
) : (
|
|
136
|
+
visibleEntries.map((entry, index) => (
|
|
137
|
+
<ExecutionWorkspaceSwitcherRow
|
|
138
|
+
key={entry.id}
|
|
139
|
+
entry={entry}
|
|
140
|
+
isFocused={normalized.scrollOffset + index === normalized.selectedIndex}
|
|
141
|
+
selectedEntryId={selectedEntryId}
|
|
142
|
+
/>
|
|
143
|
+
))
|
|
144
|
+
)}
|
|
145
|
+
</Box>
|
|
146
|
+
{screenReader ? (
|
|
147
|
+
<ScreenReaderSelectionPrompt
|
|
148
|
+
itemCount={entries.length}
|
|
149
|
+
buffer={numbered.buffer}
|
|
150
|
+
invalid={numbered.invalid}
|
|
151
|
+
/>
|
|
152
|
+
) : (
|
|
153
|
+
<KeyHintFooter hints={canAttach ? [...footerHints, ...attachHint] : footerHints} />
|
|
154
|
+
)}
|
|
155
|
+
</Box>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The typed-selection prompt, re-printed verbatim after an out-of-range answer. */
|
|
160
|
+
function ScreenReaderSelectionPrompt({
|
|
161
|
+
itemCount,
|
|
162
|
+
buffer,
|
|
163
|
+
invalid,
|
|
164
|
+
}: {
|
|
165
|
+
itemCount: number;
|
|
166
|
+
buffer: string;
|
|
167
|
+
invalid: boolean;
|
|
168
|
+
}): React.ReactElement {
|
|
169
|
+
const prompt = formatNumberedSelectionPrompt(itemCount, true);
|
|
170
|
+
return (
|
|
171
|
+
<Box flexDirection="column">
|
|
172
|
+
<Text>
|
|
173
|
+
{prompt}
|
|
174
|
+
{buffer.length > 0 ? ` ${buffer}` : ''}
|
|
175
|
+
</Text>
|
|
176
|
+
{invalid && <Text>{prompt}</Text>}
|
|
177
|
+
</Box>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function ExecutionWorkspaceSwitcherRow({
|
|
182
|
+
entry,
|
|
183
|
+
isFocused,
|
|
184
|
+
selectedEntryId,
|
|
185
|
+
rowNumber,
|
|
186
|
+
}: {
|
|
187
|
+
entry: IExecutionWorkspaceEntry;
|
|
188
|
+
isFocused: boolean;
|
|
189
|
+
selectedEntryId?: string;
|
|
190
|
+
rowNumber?: number;
|
|
191
|
+
}): React.ReactElement {
|
|
192
|
+
const palette = usePalette();
|
|
193
|
+
const row = formatExecutionWorkspaceEntryRow(entry, { selectedEntryId });
|
|
194
|
+
return (
|
|
195
|
+
<Text>
|
|
196
|
+
<Text color={isFocused ? palette.text.accent : undefined} bold={isFocused}>
|
|
197
|
+
{rowNumber !== undefined
|
|
198
|
+
? numberedRowPrefix(rowNumber)
|
|
199
|
+
: isFocused
|
|
200
|
+
? SELECTION_INDICATOR
|
|
201
|
+
: SELECTION_INDICATOR_NONE}
|
|
202
|
+
</Text>
|
|
203
|
+
<Text color={statusGlyphColor(palette, row.statusKind)}>{`${row.radio} ${row.state}`}</Text>
|
|
204
|
+
<Text
|
|
205
|
+
color={isFocused ? palette.text.accent : undefined}
|
|
206
|
+
bold={isFocused}
|
|
207
|
+
>{` ${row.title}`}</Text>
|
|
208
|
+
<Text dimColor>{` · ${row.statusLabel}`}</Text>
|
|
209
|
+
{row.subtitle ? <Text dimColor>{` · ${row.subtitle}`}</Text> : null}
|
|
210
|
+
{row.preview ? <Text dimColor>{` · ${row.preview}`}</Text> : null}
|
|
211
|
+
</Text>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCREEN-1993 — the reverse prompt-history search overlay (Ctrl+R), rendered inside the input area
|
|
3
|
+
* on the `SlashAutocomplete` pattern. The query line, the scope label, the matches newest-first with
|
|
4
|
+
* the query highlighted, the load state and the footer of live keys.
|
|
5
|
+
*
|
|
6
|
+
* The highlight survives `NO_COLOR`: when the process emits no colour, a matched run is wrapped in
|
|
7
|
+
* visible brackets instead of relying on an SGR attribute nothing would render.
|
|
8
|
+
*/
|
|
9
|
+
import { Box } from 'ink';
|
|
10
|
+
import React from 'react';
|
|
11
|
+
|
|
12
|
+
import { splitByRanges, type IHistorySearchMatch } from './history-search/history-search-flow.js';
|
|
13
|
+
import {
|
|
14
|
+
KeyHintFooter,
|
|
15
|
+
SELECTION_INDICATOR,
|
|
16
|
+
SELECTION_INDICATOR_NONE,
|
|
17
|
+
type IKeyHint,
|
|
18
|
+
} from './key-hint-footer.js';
|
|
19
|
+
import { useKeybindingHints } from './keybindings/keybindings-context.js';
|
|
20
|
+
import { numberedRowPrefix } from './numbered-list.js';
|
|
21
|
+
import { Text } from './SafeText.js';
|
|
22
|
+
import { useScreenReader } from './screen-reader-context.js';
|
|
23
|
+
import { isInteractiveColorTerminal } from './terminal-capabilities.js';
|
|
24
|
+
import { usePalette } from './theme/index.js';
|
|
25
|
+
|
|
26
|
+
import type { IHistorySearchView } from './history-search/useHistorySearch.js';
|
|
27
|
+
|
|
28
|
+
/** Footer for the overlay, in the default bindings. */
|
|
29
|
+
export const HISTORY_SEARCH_FOOTER_HINTS: readonly IKeyHint[] = [
|
|
30
|
+
{ keys: '↑↓', label: 'Navigate' },
|
|
31
|
+
{ keys: 'Ctrl+S', label: 'Scope' },
|
|
32
|
+
{ keys: 'Enter/Tab', label: 'Insert' },
|
|
33
|
+
{ keys: 'Ctrl+E', label: 'Run' },
|
|
34
|
+
{ keys: 'Esc', label: 'Close' },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const MAX_VISIBLE = 8;
|
|
38
|
+
|
|
39
|
+
function scrollOffset(selectedIndex: number, total: number): number {
|
|
40
|
+
if (total <= MAX_VISIBLE || selectedIndex < MAX_VISIBLE) return 0;
|
|
41
|
+
return Math.min(selectedIndex - MAX_VISIBLE + 1, total - MAX_VISIBLE);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** `1 unreadable line skipped` / `3 unreadable lines skipped`. */
|
|
45
|
+
function formatSkippedLines(count: number): string {
|
|
46
|
+
return `${count} unreadable ${count === 1 ? 'line' : 'lines'} skipped`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function MatchRow(props: {
|
|
50
|
+
readonly match: IHistorySearchMatch;
|
|
51
|
+
readonly selected: boolean;
|
|
52
|
+
readonly rowNumber: number | undefined;
|
|
53
|
+
readonly color: boolean;
|
|
54
|
+
}): React.ReactElement {
|
|
55
|
+
const palette = usePalette();
|
|
56
|
+
const { match, selected, rowNumber, color } = props;
|
|
57
|
+
const indicator =
|
|
58
|
+
rowNumber !== undefined
|
|
59
|
+
? numberedRowPrefix(rowNumber)
|
|
60
|
+
: selected
|
|
61
|
+
? SELECTION_INDICATOR
|
|
62
|
+
: SELECTION_INDICATOR_NONE;
|
|
63
|
+
// A stored prompt may span lines; one visible glyph per newline keeps the row a row and the
|
|
64
|
+
// match ranges aligned (same length).
|
|
65
|
+
const runs = splitByRanges(
|
|
66
|
+
match.entry.text.replace(/\r\n|[\r\n]/gu, (br) => (br.length === 2 ? ' ↵' : '↵')),
|
|
67
|
+
match.ranges,
|
|
68
|
+
);
|
|
69
|
+
return (
|
|
70
|
+
<Text
|
|
71
|
+
color={selected ? palette.text.accent : undefined}
|
|
72
|
+
dimColor={!selected}
|
|
73
|
+
wrap="truncate-end"
|
|
74
|
+
>
|
|
75
|
+
{indicator}
|
|
76
|
+
{runs.map((run, index) =>
|
|
77
|
+
run.matched ? (
|
|
78
|
+
<Text key={index} bold inverse={color}>
|
|
79
|
+
{color ? run.text : `[${run.text}]`}
|
|
80
|
+
</Text>
|
|
81
|
+
) : (
|
|
82
|
+
<Text key={index}>{run.text}</Text>
|
|
83
|
+
),
|
|
84
|
+
)}
|
|
85
|
+
</Text>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function StateLine({ view }: { readonly view: IHistorySearchView }): React.ReactElement {
|
|
90
|
+
const palette = usePalette();
|
|
91
|
+
if (view.error !== undefined) {
|
|
92
|
+
return <Text color={palette.text.error}>History could not be read: {view.error}</Text>;
|
|
93
|
+
}
|
|
94
|
+
const parts: string[] = [];
|
|
95
|
+
if (view.matches.length === 0) {
|
|
96
|
+
parts.push(
|
|
97
|
+
view.loading ? 'loading…' : view.query.length === 0 ? 'no stored prompts yet' : 'no match',
|
|
98
|
+
);
|
|
99
|
+
} else {
|
|
100
|
+
parts.push(`${view.matches.length} match${view.matches.length === 1 ? '' : 'es'}`);
|
|
101
|
+
if (view.loading) parts.push('loading…');
|
|
102
|
+
}
|
|
103
|
+
if (view.skippedLines > 0) parts.push(formatSkippedLines(view.skippedLines));
|
|
104
|
+
return <Text dimColor>{parts.join(' · ')}</Text>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export default function HistorySearchOverlay({
|
|
108
|
+
view,
|
|
109
|
+
}: {
|
|
110
|
+
readonly view: IHistorySearchView;
|
|
111
|
+
}): React.ReactElement | null {
|
|
112
|
+
const palette = usePalette();
|
|
113
|
+
const screenReader = useScreenReader();
|
|
114
|
+
const color = isInteractiveColorTerminal();
|
|
115
|
+
const footerHints = useKeybindingHints('history-search', [
|
|
116
|
+
[['previous', 'next'], 'Navigate'],
|
|
117
|
+
['cycle-scope', 'Scope'],
|
|
118
|
+
['insert', 'Insert'],
|
|
119
|
+
['execute', 'Run'],
|
|
120
|
+
['cancel', 'Close'],
|
|
121
|
+
]);
|
|
122
|
+
if (!view.open) return null;
|
|
123
|
+
const offset = scrollOffset(view.selectedIndex, view.matches.length);
|
|
124
|
+
const visible = view.matches.slice(offset, offset + MAX_VISIBLE);
|
|
125
|
+
return (
|
|
126
|
+
<Box
|
|
127
|
+
flexDirection="column"
|
|
128
|
+
{...(screenReader
|
|
129
|
+
? {}
|
|
130
|
+
: { borderStyle: 'round' as const, borderColor: palette.border.muted })}
|
|
131
|
+
paddingX={1}
|
|
132
|
+
>
|
|
133
|
+
<Text>
|
|
134
|
+
<Text color={palette.text.accent}>(reverse-i-search)</Text> scope: {view.scope} · query:{' '}
|
|
135
|
+
{view.query}
|
|
136
|
+
</Text>
|
|
137
|
+
{visible.map((match, index) => (
|
|
138
|
+
<MatchRow
|
|
139
|
+
key={`${match.entry.at}:${offset + index}`}
|
|
140
|
+
match={match}
|
|
141
|
+
selected={offset + index === view.selectedIndex}
|
|
142
|
+
rowNumber={screenReader ? offset + index : undefined}
|
|
143
|
+
color={color}
|
|
144
|
+
/>
|
|
145
|
+
))}
|
|
146
|
+
<StateLine view={view} />
|
|
147
|
+
<KeyHintFooter hints={footerHints} />
|
|
148
|
+
</Box>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { Box, useWindowSize } from 'ink';
|
|
2
|
+
import React, { useState, useCallback, useEffect, useRef, useMemo } from 'react';
|
|
3
|
+
|
|
4
|
+
import CjkTextInput from './CjkTextInput.js';
|
|
5
|
+
import ExternalPromptNotice from './external-prompt-notice.js';
|
|
6
|
+
import {
|
|
7
|
+
appendPromptHistory,
|
|
8
|
+
createPasteLabelChange,
|
|
9
|
+
createPromptHistoryNavigationState,
|
|
10
|
+
extractPromptHistory,
|
|
11
|
+
resolveEnterCommandSelection,
|
|
12
|
+
resolveTabCompletion,
|
|
13
|
+
shouldSubmitInput,
|
|
14
|
+
} from './flows/input-area-flow.js';
|
|
15
|
+
import HistorySearchOverlay from './HistorySearchOverlay.js';
|
|
16
|
+
import { useAutocomplete } from './hooks/useAutocomplete.js';
|
|
17
|
+
import { useInputAreaHistorySearch } from './hooks/useInputAreaHistorySearch.js';
|
|
18
|
+
import { useInputAreaKeys } from './hooks/useInputAreaKeys.js';
|
|
19
|
+
import { DeletionAnnouncement, InputBottomRule, InputTopRule } from './input-area-rules.js';
|
|
20
|
+
import { KeyHintFooter } from './key-hint-footer.js';
|
|
21
|
+
import { useKeybindingHints } from './keybindings/keybindings-context.js';
|
|
22
|
+
import { Text } from './SafeText.js';
|
|
23
|
+
import { useScreenReader } from './screen-reader-context.js';
|
|
24
|
+
import SlashAutocomplete from './SlashAutocomplete.js';
|
|
25
|
+
import { usePalette } from './theme/index.js';
|
|
26
|
+
import { expandPasteLabels } from './utils/paste-labels.js';
|
|
27
|
+
import WaveText from './WaveText.js';
|
|
28
|
+
|
|
29
|
+
import type { IInputAreaHistorySearch } from './hooks/useInputAreaHistorySearch.js';
|
|
30
|
+
import type { ITuiCommandQueryPort } from './tui-app-channel-port.js';
|
|
31
|
+
import type { IHistoryEntry } from '@robota-sdk/agent-core';
|
|
32
|
+
import type { ICommand } from '@robota-sdk/agent-interface-command';
|
|
33
|
+
|
|
34
|
+
const PENDING_PROMPT_DISPLAY_MAX = 50;
|
|
35
|
+
const PENDING_PROMPT_TAIL_KEEP = 47;
|
|
36
|
+
|
|
37
|
+
interface IProps {
|
|
38
|
+
/** FLOW-2006: a deep link's prefill, seeded once. */
|
|
39
|
+
initialValue?: string | undefined;
|
|
40
|
+
/** Told to the controller the first time the prefill is taken. */
|
|
41
|
+
consumeInitialValue?: (() => void) | undefined;
|
|
42
|
+
/** True while the composer's text came from a deep link: renders the provenance notice. */
|
|
43
|
+
externalPromptOrigin?: boolean | undefined;
|
|
44
|
+
onSubmit: (value: string) => void;
|
|
45
|
+
onCancelQueue?: () => void;
|
|
46
|
+
isDisabled: boolean;
|
|
47
|
+
isQueueCancellationDisabled?: boolean;
|
|
48
|
+
isAborting?: boolean;
|
|
49
|
+
pendingPrompt?: string | null;
|
|
50
|
+
/** REMOTE-014 E5: total queued turns (owner + co-drivers); >1 surfaces a co-driver-queued hint. */
|
|
51
|
+
pendingCount?: number;
|
|
52
|
+
commandQueryPort?: ITuiCommandQueryPort;
|
|
53
|
+
sessionName?: string;
|
|
54
|
+
history?: readonly IHistoryEntry[];
|
|
55
|
+
/**
|
|
56
|
+
* SCREEN-014: called when ↓ is pressed on an empty input at the bottom of prompt history, so the
|
|
57
|
+
* parent can move focus into the background-work list (a no-op for the input today).
|
|
58
|
+
*/
|
|
59
|
+
onRequestFocusBackgroundList?: () => void;
|
|
60
|
+
/** SCREEN-1993: the stored-prompt search surface; absent ⇒ `ctrl+r` is inert. */
|
|
61
|
+
historySearch?: IInputAreaHistorySearch | undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Known limitation: Korean IME last character may be dropped on Enter.
|
|
66
|
+
* This is an Ink raw mode limitation — no compositionstart/compositionend
|
|
67
|
+
* events are available in terminal raw mode.
|
|
68
|
+
* Reference: https://github.com/anthropics/claude-code/issues/3045
|
|
69
|
+
*/
|
|
70
|
+
/**
|
|
71
|
+
* Layout constants for InputArea (columns).
|
|
72
|
+
* Used to compute available text width from terminal columns.
|
|
73
|
+
*
|
|
74
|
+
* Side borders removed — only top/bottom horizontal lines remain.
|
|
75
|
+
* paddingLeft={1} adds 1 column inside the box.
|
|
76
|
+
* Prompt "> " takes 2 columns.
|
|
77
|
+
*/
|
|
78
|
+
const BORDER_HORIZONTAL = 0;
|
|
79
|
+
const PADDING_LEFT = 1;
|
|
80
|
+
const PROMPT_WIDTH = 2;
|
|
81
|
+
const INPUT_AREA_OVERHEAD = BORDER_HORIZONTAL + PADDING_LEFT + PROMPT_WIDTH;
|
|
82
|
+
const DEFAULT_TERMINAL_COLUMNS = 80;
|
|
83
|
+
|
|
84
|
+
export default function InputArea({
|
|
85
|
+
onSubmit,
|
|
86
|
+
onCancelQueue,
|
|
87
|
+
isDisabled,
|
|
88
|
+
isQueueCancellationDisabled,
|
|
89
|
+
isAborting,
|
|
90
|
+
pendingPrompt,
|
|
91
|
+
pendingCount,
|
|
92
|
+
commandQueryPort,
|
|
93
|
+
sessionName,
|
|
94
|
+
history,
|
|
95
|
+
onRequestFocusBackgroundList,
|
|
96
|
+
historySearch,
|
|
97
|
+
initialValue,
|
|
98
|
+
consumeInitialValue,
|
|
99
|
+
externalPromptOrigin,
|
|
100
|
+
}: IProps): React.ReactElement {
|
|
101
|
+
const palette = usePalette();
|
|
102
|
+
// FLOW-2006: the deep link's prefill arrives as a prop and is taken exactly once; the controller
|
|
103
|
+
// then drops it, so a remount (the handoff-suspend path) never re-seeds a cleared prompt.
|
|
104
|
+
const [value, setValue] = useState(initialValue ?? '');
|
|
105
|
+
// The text the link supplied, held for as long as it is still what the composer holds. The
|
|
106
|
+
// provenance belongs to THIS TEXT, not to the session: once it is sent or cleared, the next
|
|
107
|
+
// prompt is the user's own and must not be labelled as if a link had written it.
|
|
108
|
+
const [seededValue] = useState(externalPromptOrigin === true ? (initialValue ?? '') : '');
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (initialValue === undefined || initialValue.length === 0) return;
|
|
111
|
+
consumeInitialValue?.();
|
|
112
|
+
}, [initialValue, consumeInitialValue]);
|
|
113
|
+
const [cursorHint, setCursorHint] = useState<number | null>(null);
|
|
114
|
+
// CLI-2004: the text the last word/line delete removed, announced once and cleared on the next
|
|
115
|
+
// ordinary edit. A reader announces the line it is ON — what left it is otherwise unrecoverable.
|
|
116
|
+
const screenReader = useScreenReader();
|
|
117
|
+
const [deletedText, setDeletedText] = useState<string | null>(null);
|
|
118
|
+
const [historyState, setHistoryState] = useState(createPromptHistoryNavigationState);
|
|
119
|
+
const [localPromptHistory, setLocalPromptHistory] = useState<string[]>([]);
|
|
120
|
+
const restoredPromptHistory = useMemo(() => extractPromptHistory(history ?? []), [history]);
|
|
121
|
+
const promptHistory = useMemo(
|
|
122
|
+
() =>
|
|
123
|
+
localPromptHistory.reduce<string[]>(
|
|
124
|
+
(prompts, prompt) => appendPromptHistory(prompts, prompt),
|
|
125
|
+
restoredPromptHistory,
|
|
126
|
+
),
|
|
127
|
+
[restoredPromptHistory, localPromptHistory],
|
|
128
|
+
);
|
|
129
|
+
const pasteStore = useRef<Map<number, string>>(new Map());
|
|
130
|
+
const { columns } = useWindowSize();
|
|
131
|
+
const terminalColumns = columns > 0 ? columns : DEFAULT_TERMINAL_COLUMNS;
|
|
132
|
+
const availableWidth = Math.max(1, terminalColumns - INPUT_AREA_OVERHEAD);
|
|
133
|
+
const pasteIdRef = useRef(0);
|
|
134
|
+
const submitHint = useKeybindingHints('chat-input', [['submit', 'Submit']]);
|
|
135
|
+
|
|
136
|
+
const {
|
|
137
|
+
showPopup,
|
|
138
|
+
filteredCommands,
|
|
139
|
+
selectedIndex,
|
|
140
|
+
setSelectedIndex,
|
|
141
|
+
isSubcommandMode,
|
|
142
|
+
setShowPopup,
|
|
143
|
+
} = useAutocomplete(value, commandQueryPort);
|
|
144
|
+
|
|
145
|
+
const handlePaste = useCallback((text: string, cursorPosition: number) => {
|
|
146
|
+
pasteIdRef.current += 1;
|
|
147
|
+
const id = pasteIdRef.current;
|
|
148
|
+
pasteStore.current.set(id, text);
|
|
149
|
+
setValue((prev) => {
|
|
150
|
+
const change = createPasteLabelChange(prev, cursorPosition, id, text);
|
|
151
|
+
setCursorHint(change.cursorHint);
|
|
152
|
+
return change.value;
|
|
153
|
+
});
|
|
154
|
+
}, []);
|
|
155
|
+
|
|
156
|
+
const resetHistoryNavigation = useCallback(() => {
|
|
157
|
+
setHistoryState(createPromptHistoryNavigationState());
|
|
158
|
+
}, []);
|
|
159
|
+
|
|
160
|
+
const recordPromptHistory = useCallback((prompt: string): void => {
|
|
161
|
+
setLocalPromptHistory((prev) => appendPromptHistory(prev, prompt));
|
|
162
|
+
}, []);
|
|
163
|
+
|
|
164
|
+
const submitPrompt = useCallback(
|
|
165
|
+
(prompt: string): void => {
|
|
166
|
+
recordPromptHistory(prompt);
|
|
167
|
+
resetHistoryNavigation();
|
|
168
|
+
onSubmit(prompt);
|
|
169
|
+
},
|
|
170
|
+
[onSubmit, recordPromptHistory, resetHistoryNavigation],
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
/** Tab: insert command into input field without executing */
|
|
174
|
+
const tabCompleteCommand = useCallback(
|
|
175
|
+
(cmd: ICommand): void => {
|
|
176
|
+
const result = resolveTabCompletion(value, cmd);
|
|
177
|
+
if (result.type === 'insert') {
|
|
178
|
+
setValue(result.value);
|
|
179
|
+
if (result.selectedIndex !== undefined) {
|
|
180
|
+
setSelectedIndex(result.selectedIndex);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
[value, setSelectedIndex],
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
/** Enter: insert and execute command immediately */
|
|
188
|
+
const enterSelectCommand = useCallback(
|
|
189
|
+
(cmd: ICommand): void => {
|
|
190
|
+
const result = resolveEnterCommandSelection(value, cmd);
|
|
191
|
+
if (result.type === 'insert') {
|
|
192
|
+
setValue(result.value);
|
|
193
|
+
if (result.selectedIndex !== undefined) {
|
|
194
|
+
setSelectedIndex(result.selectedIndex);
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (result.type === 'submit') {
|
|
199
|
+
setValue('');
|
|
200
|
+
submitPrompt(result.value);
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
[value, submitPrompt, setSelectedIndex],
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const handleSubmit = useCallback(
|
|
207
|
+
(text: string): void => {
|
|
208
|
+
if (!shouldSubmitInput(text)) return;
|
|
209
|
+
|
|
210
|
+
if (showPopup && filteredCommands[selectedIndex]) {
|
|
211
|
+
enterSelectCommand(filteredCommands[selectedIndex]);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Expand paste labels before submitting
|
|
216
|
+
const expanded = expandPasteLabels(text.trim(), pasteStore.current);
|
|
217
|
+
|
|
218
|
+
setValue('');
|
|
219
|
+
// Reset paste state
|
|
220
|
+
pasteStore.current.clear();
|
|
221
|
+
pasteIdRef.current = 0;
|
|
222
|
+
|
|
223
|
+
submitPrompt(expanded);
|
|
224
|
+
},
|
|
225
|
+
[showPopup, filteredCommands, selectedIndex, enterSelectCommand, submitPrompt],
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const search = useInputAreaHistorySearch({
|
|
229
|
+
historySearch,
|
|
230
|
+
sessionPrompts: promptHistory,
|
|
231
|
+
composerActive: !showPopup && !isDisabled && !pendingPrompt,
|
|
232
|
+
setValue,
|
|
233
|
+
setCursorHint,
|
|
234
|
+
submitPrompt,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
useInputAreaKeys({
|
|
238
|
+
value,
|
|
239
|
+
isDisabled,
|
|
240
|
+
searchOpen: search.open,
|
|
241
|
+
isQueueCancellationDisabled: isQueueCancellationDisabled ?? isDisabled,
|
|
242
|
+
pendingPrompt,
|
|
243
|
+
showPopup,
|
|
244
|
+
setShowPopup,
|
|
245
|
+
filteredCommands,
|
|
246
|
+
selectedIndex,
|
|
247
|
+
setSelectedIndex,
|
|
248
|
+
tabCompleteCommand,
|
|
249
|
+
promptHistory,
|
|
250
|
+
historyState,
|
|
251
|
+
setValue,
|
|
252
|
+
setCursorHint,
|
|
253
|
+
setHistoryState,
|
|
254
|
+
onRequestFocusBackgroundList,
|
|
255
|
+
onCancelQueue,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const borderColor = isAborting
|
|
259
|
+
? palette.border.attention
|
|
260
|
+
: pendingPrompt
|
|
261
|
+
? palette.border.focused
|
|
262
|
+
: isDisabled
|
|
263
|
+
? palette.border.muted
|
|
264
|
+
: palette.border.active;
|
|
265
|
+
const innerWidth = Math.max(1, terminalColumns - BORDER_HORIZONTAL);
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<Box flexDirection="column">
|
|
269
|
+
{showPopup && (
|
|
270
|
+
<SlashAutocomplete
|
|
271
|
+
commands={filteredCommands}
|
|
272
|
+
selectedIndex={selectedIndex}
|
|
273
|
+
visible={showPopup}
|
|
274
|
+
isSubcommandMode={isSubcommandMode}
|
|
275
|
+
/>
|
|
276
|
+
)}
|
|
277
|
+
<HistorySearchOverlay view={search} />
|
|
278
|
+
<DeletionAnnouncement deleted={screenReader ? deletedText : null} />
|
|
279
|
+
<InputTopRule
|
|
280
|
+
screenReader={screenReader}
|
|
281
|
+
innerWidth={innerWidth}
|
|
282
|
+
borderColor={borderColor}
|
|
283
|
+
{...(sessionName !== undefined ? { sessionName } : {})}
|
|
284
|
+
/>
|
|
285
|
+
<Box paddingLeft={1}>
|
|
286
|
+
{isAborting ? (
|
|
287
|
+
<Text color={palette.text.warning}> Interrupting...</Text>
|
|
288
|
+
) : pendingPrompt ? (
|
|
289
|
+
<Text color={palette.text.accent}>
|
|
290
|
+
{' '}
|
|
291
|
+
Queued:{' '}
|
|
292
|
+
{pendingPrompt.length > PENDING_PROMPT_DISPLAY_MAX
|
|
293
|
+
? pendingPrompt.slice(0, PENDING_PROMPT_TAIL_KEEP) + '...'
|
|
294
|
+
: pendingPrompt}{' '}
|
|
295
|
+
{typeof pendingCount === 'number' && pendingCount > 1 && (
|
|
296
|
+
<Text dimColor>(+{pendingCount - 1} co-driver queued) </Text>
|
|
297
|
+
)}
|
|
298
|
+
<Text dimColor>(Backspace to cancel)</Text>
|
|
299
|
+
</Text>
|
|
300
|
+
) : isDisabled ? (
|
|
301
|
+
<WaveText text=" Waiting for response... (ESC to interrupt)" />
|
|
302
|
+
) : (
|
|
303
|
+
<Box>
|
|
304
|
+
<Text color={palette.text.success} bold>
|
|
305
|
+
{'> '}
|
|
306
|
+
</Text>
|
|
307
|
+
<CjkTextInput
|
|
308
|
+
value={value}
|
|
309
|
+
onChange={(v) => {
|
|
310
|
+
setValue(v);
|
|
311
|
+
resetHistoryNavigation();
|
|
312
|
+
setCursorHint(null); // reset after normal typing
|
|
313
|
+
}}
|
|
314
|
+
onDeletedText={setDeletedText}
|
|
315
|
+
onSubmit={handleSubmit}
|
|
316
|
+
onPaste={handlePaste}
|
|
317
|
+
placeholder="Type a message or /help"
|
|
318
|
+
availableWidth={availableWidth}
|
|
319
|
+
cursorHint={cursorHint}
|
|
320
|
+
enableVerticalNavigation={false}
|
|
321
|
+
keybindingContext={showPopup ? 'autocomplete-menu' : 'chat-input'}
|
|
322
|
+
focus={!search.open}
|
|
323
|
+
/>
|
|
324
|
+
</Box>
|
|
325
|
+
)}
|
|
326
|
+
</Box>
|
|
327
|
+
{!screenReader && !isDisabled && !pendingPrompt && !search.open && (
|
|
328
|
+
<KeyHintFooter hints={submitHint} />
|
|
329
|
+
)}
|
|
330
|
+
<InputBottomRule
|
|
331
|
+
screenReader={screenReader}
|
|
332
|
+
innerWidth={innerWidth}
|
|
333
|
+
borderColor={borderColor}
|
|
334
|
+
/>
|
|
335
|
+
{seededValue.length > 0 && value === seededValue && <ExternalPromptNotice value={value} />}
|
|
336
|
+
</Box>
|
|
337
|
+
);
|
|
338
|
+
}
|