@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,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI-2004 — the typed-number input wiring shared by every numbered menu.
|
|
3
|
+
*
|
|
4
|
+
* The reducer lives in `flows/selection-flow.ts` (one owner of "what is selected"); this hook is the
|
|
5
|
+
* Ink adapter around it, so six components get the same behaviour without six copies of the same
|
|
6
|
+
* `useInput` block. Inactive when the mode is off, so the arrow-key handlers keep the terminal.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useInput } from 'ink';
|
|
10
|
+
import { useCallback, useRef, useState } from 'react';
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
applyNumericSelection,
|
|
14
|
+
createNumericSelectionState,
|
|
15
|
+
type INumericSelectionState,
|
|
16
|
+
} from '../flows/selection-flow.js';
|
|
17
|
+
|
|
18
|
+
export interface IUseNumberedSelectionInputs {
|
|
19
|
+
/** Screen-reader mode. Off ⇒ the hook reads nothing. */
|
|
20
|
+
enabled: boolean;
|
|
21
|
+
itemCount: number;
|
|
22
|
+
cancellable?: boolean;
|
|
23
|
+
/** A checklist: a number toggles a row, the menu stays open, and an empty Enter commits. */
|
|
24
|
+
multi?: boolean;
|
|
25
|
+
/** Keep a single-row selection open so the user can select another row later. */
|
|
26
|
+
repeatable?: boolean;
|
|
27
|
+
onSelect: (index: number) => void;
|
|
28
|
+
onCancel?: () => void;
|
|
29
|
+
/** Required with `multi` — what an empty Enter commits. */
|
|
30
|
+
onConfirm?: () => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface INumberedSelection {
|
|
34
|
+
buffer: string;
|
|
35
|
+
invalid: boolean;
|
|
36
|
+
clear: () => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Read a typed selection for a numbered list. */
|
|
40
|
+
export function useNumberedSelection(inputs: IUseNumberedSelectionInputs): INumberedSelection {
|
|
41
|
+
const [state, setState] = useState<INumericSelectionState>(createNumericSelectionState);
|
|
42
|
+
const stateRef = useRef(state);
|
|
43
|
+
const { enabled, itemCount, cancellable, multi, repeatable, onSelect, onCancel, onConfirm } = inputs;
|
|
44
|
+
|
|
45
|
+
const clear = useCallback((): void => {
|
|
46
|
+
const empty = createNumericSelectionState();
|
|
47
|
+
stateRef.current = empty;
|
|
48
|
+
setState(empty);
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
const handle = useCallback(
|
|
52
|
+
(input: string, key: Parameters<typeof applyNumericSelection>[2]): void => {
|
|
53
|
+
const result = applyNumericSelection(stateRef.current, input, key, {
|
|
54
|
+
itemCount,
|
|
55
|
+
...(cancellable === true ? { cancellable: true } : {}),
|
|
56
|
+
...(multi === true ? { multi: true } : {}),
|
|
57
|
+
...(repeatable === true ? { repeatable: true } : {}),
|
|
58
|
+
});
|
|
59
|
+
stateRef.current = result.state;
|
|
60
|
+
setState(result.state);
|
|
61
|
+
if (result.effect.type === 'select') onSelect(result.effect.index);
|
|
62
|
+
else if (result.effect.type === 'cancel') onCancel?.();
|
|
63
|
+
else if (result.effect.type === 'confirm') onConfirm?.();
|
|
64
|
+
},
|
|
65
|
+
[itemCount, cancellable, multi, repeatable, onSelect, onCancel, onConfirm],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
useInput(
|
|
69
|
+
(input, key) => {
|
|
70
|
+
handle(input, key);
|
|
71
|
+
},
|
|
72
|
+
{ isActive: enabled },
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return { buffer: state.buffer, invalid: state.invalid, clear };
|
|
76
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook: uses the host plugin adapter, or an explicit unsupported adapter when none is provided.
|
|
3
|
+
*
|
|
4
|
+
* A missing host capability must fail visibly. Returning successful no-ops would make the plugin
|
|
5
|
+
* screen claim mutations succeeded while changing nothing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useMemo } from 'react';
|
|
9
|
+
|
|
10
|
+
import type { ICommandPluginAdapter } from '@robota-sdk/agent-interface-command';
|
|
11
|
+
|
|
12
|
+
function unsupported(): Promise<never> {
|
|
13
|
+
return Promise.reject(new Error('Plugin management is unavailable in this host.'));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function createUnsupportedPluginAdapter(): ICommandPluginAdapter {
|
|
17
|
+
return {
|
|
18
|
+
listInstalled: unsupported,
|
|
19
|
+
listAvailablePlugins: unsupported,
|
|
20
|
+
install: unsupported,
|
|
21
|
+
uninstall: unsupported,
|
|
22
|
+
enable: unsupported,
|
|
23
|
+
disable: unsupported,
|
|
24
|
+
marketplaceAdd: unsupported,
|
|
25
|
+
marketplaceRemove: unsupported,
|
|
26
|
+
marketplaceUpdate: unsupported,
|
|
27
|
+
marketplaceList: unsupported,
|
|
28
|
+
reloadPlugins: unsupported,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function resolvePluginCallbacks(adapter?: ICommandPluginAdapter): ICommandPluginAdapter {
|
|
33
|
+
return adapter ?? createUnsupportedPluginAdapter();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function usePluginCallbacks(adapter?: ICommandPluginAdapter): ICommandPluginAdapter {
|
|
37
|
+
return useMemo(() => resolvePluginCallbacks(adapter), [adapter]);
|
|
38
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hook: fetch data for PluginTUI screens.
|
|
3
|
+
* Extracted from PluginTUI.tsx for single-responsibility.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useEffect } from 'react';
|
|
7
|
+
|
|
8
|
+
import type { IMenuSelectItem } from '../MenuSelect.js';
|
|
9
|
+
import type { ICommandPluginAdapter } from '@robota-sdk/agent-interface-command';
|
|
10
|
+
|
|
11
|
+
export function usePluginScreenData(
|
|
12
|
+
screen: string,
|
|
13
|
+
marketplace: string | undefined,
|
|
14
|
+
callbacks: ICommandPluginAdapter,
|
|
15
|
+
refreshCounter: number,
|
|
16
|
+
stackLength: number,
|
|
17
|
+
): { items: IMenuSelectItem[]; loading: boolean; error: string | undefined } {
|
|
18
|
+
const [items, setItems] = useState<IMenuSelectItem[]>([]);
|
|
19
|
+
const [loading, setLoading] = useState(false);
|
|
20
|
+
const [error, setError] = useState<string | undefined>();
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
setItems([]);
|
|
24
|
+
setError(undefined);
|
|
25
|
+
|
|
26
|
+
if (screen === 'marketplace-list') {
|
|
27
|
+
setLoading(true);
|
|
28
|
+
callbacks
|
|
29
|
+
.marketplaceList()
|
|
30
|
+
.then((sources) => {
|
|
31
|
+
const baseItems: IMenuSelectItem[] = [{ label: 'Add Marketplace', value: '__add__' }];
|
|
32
|
+
const sourceItems: IMenuSelectItem[] = sources.map((s) => ({
|
|
33
|
+
label: s.name,
|
|
34
|
+
value: s.name,
|
|
35
|
+
hint: s.type,
|
|
36
|
+
}));
|
|
37
|
+
setItems([...baseItems, ...sourceItems]);
|
|
38
|
+
setLoading(false);
|
|
39
|
+
})
|
|
40
|
+
.catch((err) => {
|
|
41
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
42
|
+
setLoading(false);
|
|
43
|
+
});
|
|
44
|
+
} else if (screen === 'marketplace-browse') {
|
|
45
|
+
const mp = marketplace ?? '';
|
|
46
|
+
setLoading(true);
|
|
47
|
+
callbacks
|
|
48
|
+
.listAvailablePlugins(mp)
|
|
49
|
+
.then((plugins) => {
|
|
50
|
+
setItems(
|
|
51
|
+
plugins.map((p) => ({
|
|
52
|
+
label: p.name,
|
|
53
|
+
value: p.name,
|
|
54
|
+
hint: p.installed ? 'installed' : p.description,
|
|
55
|
+
})),
|
|
56
|
+
);
|
|
57
|
+
setLoading(false);
|
|
58
|
+
})
|
|
59
|
+
.catch((err) => {
|
|
60
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
61
|
+
setLoading(false);
|
|
62
|
+
});
|
|
63
|
+
} else if (screen === 'installed-list') {
|
|
64
|
+
setLoading(true);
|
|
65
|
+
callbacks
|
|
66
|
+
.listInstalled()
|
|
67
|
+
.then((plugins) => {
|
|
68
|
+
setItems(
|
|
69
|
+
plugins.map((p) => ({
|
|
70
|
+
label: p.name,
|
|
71
|
+
value: p.name,
|
|
72
|
+
hint: p.description,
|
|
73
|
+
})),
|
|
74
|
+
);
|
|
75
|
+
setLoading(false);
|
|
76
|
+
})
|
|
77
|
+
.catch((err) => {
|
|
78
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
79
|
+
setLoading(false);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}, [stackLength, screen, marketplace, callbacks, refreshCounter]);
|
|
83
|
+
|
|
84
|
+
return { items, loading, error };
|
|
85
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI-062 — position the REAL terminal cursor at the CJK composition point.
|
|
3
|
+
*
|
|
4
|
+
* Mechanism (contract: .design/investigations/2026-07-25-cli-062-ime-cursor-design.md, POC-PASS):
|
|
5
|
+
* the input box's absolute frame-space origin comes from walking the yoga `parentNode` chain from
|
|
6
|
+
* a `<Box ref>` (the `5195e326b` approach, minus its debug logging); the cell is handed to ink's
|
|
7
|
+
* `useCursor().setCursorPosition`, which converts it into a relative `cursorUp` from the frame
|
|
8
|
+
* bottom INSIDE the same atomic synchronized frame write (invariant I3: never out-of-band — all
|
|
9
|
+
* cursor movement rides ink's ≤30fps frame writes; this hook itself writes NOTHING to any stream,
|
|
10
|
+
* the PoC row-accounting lesson).
|
|
11
|
+
*
|
|
12
|
+
* Measurement happens post-commit (layout is valid there) and is stored in state so the NEXT
|
|
13
|
+
* commit's insertion effect propagates a fresh position with its frame; `useBoxMetrics` provides
|
|
14
|
+
* the layout-change subscription so sibling-driven moves re-measure too. A stale origin self-heals
|
|
15
|
+
* within one frame (ink recomputes layout before onRender and notifies layout listeners).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { useBoxMetrics, useCursor, useWindowSize } from 'ink';
|
|
19
|
+
import { useEffect, useState } from 'react';
|
|
20
|
+
|
|
21
|
+
import { computeCursorCell, shouldPositionRealCursor } from '../flows/real-cursor-flow.js';
|
|
22
|
+
|
|
23
|
+
import type { DOMElement } from 'ink';
|
|
24
|
+
import type { RefObject } from 'react';
|
|
25
|
+
|
|
26
|
+
export interface IUseRealCursorPositionOptions {
|
|
27
|
+
/** Ref attached to the `<Box>` wrapping the input's text. */
|
|
28
|
+
boxRef: RefObject<DOMElement | null>;
|
|
29
|
+
/** Capability + focus gate: supportsImeCursorPositioning() && focus && showCursor. */
|
|
30
|
+
enabled: boolean;
|
|
31
|
+
/** Live input value (from the input's state ref — fresh at render time). */
|
|
32
|
+
value: string;
|
|
33
|
+
/** Live cursor character index. */
|
|
34
|
+
cursor: number;
|
|
35
|
+
/** Wrap width in columns (matches the input's own wrap math). */
|
|
36
|
+
availableWidth?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface IUseRealCursorPositionResult {
|
|
40
|
+
/** True while the hardware cursor is being positioned — the drawn cursor must be suppressed. */
|
|
41
|
+
realCursorActive: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface IMeasuredOrigin {
|
|
45
|
+
x: number;
|
|
46
|
+
y: number;
|
|
47
|
+
frameHeight: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Walk the yoga `parentNode` chain to the absolute frame-space origin, plus the root frame height. */
|
|
51
|
+
function measureAbsoluteOrigin(element: DOMElement): IMeasuredOrigin | undefined {
|
|
52
|
+
if (element.yogaNode === undefined) return undefined;
|
|
53
|
+
let x = 0;
|
|
54
|
+
let y = 0;
|
|
55
|
+
let current: DOMElement | undefined = element;
|
|
56
|
+
let root: DOMElement = element;
|
|
57
|
+
while (current !== undefined) {
|
|
58
|
+
const layout = current.yogaNode?.getComputedLayout();
|
|
59
|
+
x += layout?.left ?? 0;
|
|
60
|
+
y += layout?.top ?? 0;
|
|
61
|
+
root = current;
|
|
62
|
+
current = current.parentNode;
|
|
63
|
+
}
|
|
64
|
+
const frameHeight = root.yogaNode?.getComputedLayout().height ?? 0;
|
|
65
|
+
return { x, y, frameHeight };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function sameOrigin(a: IMeasuredOrigin | undefined, b: IMeasuredOrigin | undefined): boolean {
|
|
69
|
+
if (a === undefined || b === undefined) return a === b;
|
|
70
|
+
return a.x === b.x && a.y === b.y && a.frameHeight === b.frameHeight;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function useRealCursorPosition(
|
|
74
|
+
options: IUseRealCursorPositionOptions,
|
|
75
|
+
): IUseRealCursorPositionResult {
|
|
76
|
+
const { setCursorPosition } = useCursor();
|
|
77
|
+
// Layout-change subscription: useBoxMetrics re-renders this component whenever the tracked
|
|
78
|
+
// box's metrics change on a root layout commit, and exposes hasMeasured for invariant I1.
|
|
79
|
+
const metrics = useBoxMetrics(options.boxRef);
|
|
80
|
+
const { rows: viewportRows } = useWindowSize();
|
|
81
|
+
const [origin, setOrigin] = useState<IMeasuredOrigin | undefined>(undefined);
|
|
82
|
+
|
|
83
|
+
// I1: never a guessed row — measure the absolute origin ONLY from a committed yoga layout
|
|
84
|
+
// (post-commit effect; ink ran calculateLayout before onRender, passive effects run after).
|
|
85
|
+
// No dependency array on purpose: any render may follow a layout change, and the setState
|
|
86
|
+
// bails out when the origin is unchanged, so this settles instead of looping.
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
const element = options.boxRef.current;
|
|
89
|
+
const next = element === null ? undefined : measureAbsoluteOrigin(element);
|
|
90
|
+
setOrigin((previous) => (sameOrigin(previous, next) ? previous : next));
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const cell =
|
|
94
|
+
origin !== undefined && options.enabled
|
|
95
|
+
? computeCursorCell({
|
|
96
|
+
absX: origin.x,
|
|
97
|
+
absY: origin.y,
|
|
98
|
+
value: options.value,
|
|
99
|
+
cursor: options.cursor,
|
|
100
|
+
...(options.availableWidth !== undefined
|
|
101
|
+
? { availableWidth: options.availableWidth }
|
|
102
|
+
: {}),
|
|
103
|
+
})
|
|
104
|
+
: undefined;
|
|
105
|
+
|
|
106
|
+
const realCursorActive =
|
|
107
|
+
cell !== undefined &&
|
|
108
|
+
origin !== undefined &&
|
|
109
|
+
shouldPositionRealCursor({
|
|
110
|
+
hasMeasured: metrics.hasMeasured, // I1 — `origin` is already narrowed non-undefined above
|
|
111
|
+
y: cell.y,
|
|
112
|
+
frameHeight: origin.frameHeight, // I2 checked inside the guard
|
|
113
|
+
viewportRows,
|
|
114
|
+
capability: options.enabled, // I5 folded in via supportsImeCursorPositioning()
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Render-phase call is safe: useCursor only records the position in a ref; propagation happens
|
|
118
|
+
// in its insertion effect, inside ink's synchronized frame write (I3). I4: when the guard fails
|
|
119
|
+
// (blur, fullscreen frame, not measured yet) we pass `undefined` — ink hides the hardware cursor
|
|
120
|
+
// and the caller falls back to today's drawn-cursor rendering; on unmount, useCursor's cleanup
|
|
121
|
+
// propagates `undefined` by itself.
|
|
122
|
+
setCursorPosition(realCursorActive && cell !== undefined ? { x: cell.x, y: cell.y } : undefined);
|
|
123
|
+
|
|
124
|
+
return { realCursorActive };
|
|
125
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { renderMarkdown } from '../render-markdown.js';
|
|
2
|
+
import { useScreenReader } from '../screen-reader-context.js';
|
|
3
|
+
import { useSyntaxHighlighting, useTheme } from '../theme/index.js';
|
|
4
|
+
|
|
5
|
+
import type { IRenderMarkdownOptions } from '../render-markdown.js';
|
|
6
|
+
|
|
7
|
+
/** What a call site still decides. The three appearance inputs are no longer among them. */
|
|
8
|
+
export type TRenderMarkdownExtras = Omit<
|
|
9
|
+
IRenderMarkdownOptions,
|
|
10
|
+
'theme' | 'syntaxHighlighting' | 'screenReader'
|
|
11
|
+
>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* SCREEN-2002 — markdown rendered with this surface's resolved appearance.
|
|
15
|
+
*
|
|
16
|
+
* The three inputs every render site needs — the theme, the syntax-highlighting setting and
|
|
17
|
+
* screen-reader mode — are bound here instead of at each call. That is a deliberate removal of an
|
|
18
|
+
* obligation rather than a convenience.
|
|
19
|
+
*
|
|
20
|
+
* The obligation was forgotten once and guarded four times. `/theme syntax off` shipped persisting
|
|
21
|
+
* a setting no call site passed, so the command reported success and changed nothing; each guard
|
|
22
|
+
* written afterwards measured a PROXY for "every call site passes it" — the prop appears in the
|
|
23
|
+
* file, the file matched the regex, the identifier occurs N times — and each held for exactly one
|
|
24
|
+
* review round. A binding removes the thing being proxied: there is no per-call-site argument left
|
|
25
|
+
* to forget, and the floor collapses to a single-consumer ratchet (nothing outside this module
|
|
26
|
+
* imports `renderMarkdown`), which is a property rather than a proxy for one.
|
|
27
|
+
*
|
|
28
|
+
* Screen-reader mode is bound too, uniformly. `StreamingIndicator` previously omitted it, which was
|
|
29
|
+
* correct but only incidentally: it returns its collapsed line before reaching any markdown, so the
|
|
30
|
+
* flag never applied. Binding it removes that coincidence as something a reader has to reconstruct.
|
|
31
|
+
*/
|
|
32
|
+
export function useRenderMarkdown(): (md: string, extras?: TRenderMarkdownExtras) => string {
|
|
33
|
+
const theme = useTheme();
|
|
34
|
+
const syntaxHighlighting = useSyntaxHighlighting();
|
|
35
|
+
const screenReader = useScreenReader();
|
|
36
|
+
return (md, extras = {}) =>
|
|
37
|
+
renderMarkdown(md, { ...extras, theme, syntaxHighlighting, screenReader });
|
|
38
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI-2004 — the non-visual half of a turn: the bell and the OSC 133 boundary marks.
|
|
3
|
+
*
|
|
4
|
+
* Both are byte-stream events, not rendering, so they live in a hook rather than a component: the
|
|
5
|
+
* App says WHEN a turn starts, ends, or wants an answer, and this decides what reaches the terminal.
|
|
6
|
+
* When the mode is off both writers are inert, so the App has exactly one code path.
|
|
7
|
+
*
|
|
8
|
+
* One turn emits `A` (prompt start) → `B` (prompt end) → `C` (turn start) → `D` (turn end): the
|
|
9
|
+
* prompt is marked while the user is being asked for input, and the execution pair brackets the
|
|
10
|
+
* agent's work. That is the boundary a reader jumps between.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
14
|
+
|
|
15
|
+
import { createAttentionBell } from '../attention-bell.js';
|
|
16
|
+
import { useScreenReaderPacing } from '../screen-reader-pacing-context.js';
|
|
17
|
+
import { supportsTurnMarks } from '../terminal-capabilities.js';
|
|
18
|
+
import { useTerminalCapabilityOverrides } from '../terminal-capabilities-context.js';
|
|
19
|
+
import { createTurnMarkWriter } from '../terminal-marks.js';
|
|
20
|
+
|
|
21
|
+
import type { IAttentionBell } from '../attention-bell.js';
|
|
22
|
+
import type { IToolState } from '@robota-sdk/agent-interface-session';
|
|
23
|
+
|
|
24
|
+
export interface IUseScreenReaderTurnSignalsInputs {
|
|
25
|
+
enabled: boolean;
|
|
26
|
+
/** The agent is working (a turn is in flight). */
|
|
27
|
+
isThinking: boolean;
|
|
28
|
+
/** Tools currently reported as active by the session. */
|
|
29
|
+
activeTools: readonly IToolState[];
|
|
30
|
+
/** A permission ask or user-action dialog is mounted and waiting for an answer. */
|
|
31
|
+
awaitingAnswer: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Stable key for one tool run within a turn.
|
|
36
|
+
*
|
|
37
|
+
* `executionId` where the session supplies one; otherwise the position, which `onToolEnd` preserves
|
|
38
|
+
* (it replaces the entry at its index rather than removing it).
|
|
39
|
+
*/
|
|
40
|
+
function toolKey(tool: IToolState, index: number): string {
|
|
41
|
+
return tool.executionId ?? `${tool.toolName}#${index}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The keys of the tools currently RUNNING — not merely present in the array. */
|
|
45
|
+
function runningKeys(activeTools: readonly IToolState[]): Set<string> {
|
|
46
|
+
const running = new Set<string>();
|
|
47
|
+
activeTools.forEach((tool, index) => {
|
|
48
|
+
if (tool.isRunning) running.add(toolKey(tool, index));
|
|
49
|
+
});
|
|
50
|
+
return running;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Wire the bell and the turn marks to the App's turn lifecycle. */
|
|
54
|
+
export function useScreenReaderTurnSignals(inputs: IUseScreenReaderTurnSignalsInputs): void {
|
|
55
|
+
const { enabled, isThinking, activeTools, awaitingAnswer } = inputs;
|
|
56
|
+
const bell = useMemo<IAttentionBell>(() => createAttentionBell({ enabled }), [enabled]);
|
|
57
|
+
// SCREEN-2670: the marks are POSITIONAL — the terminal records the line they arrive on — so they
|
|
58
|
+
// travel through the owned write path, ordered with the commit they belong to. Outside a provider
|
|
59
|
+
// the port writes to the process's own stdout, which is exactly today's path.
|
|
60
|
+
const pacing = useScreenReaderPacing();
|
|
61
|
+
const terminalCapabilities = useTerminalCapabilityOverrides();
|
|
62
|
+
const marks = useMemo(
|
|
63
|
+
() =>
|
|
64
|
+
createTurnMarkWriter({
|
|
65
|
+
enabled,
|
|
66
|
+
supported: () => supportsTurnMarks({ override: terminalCapabilities.turnMarks }),
|
|
67
|
+
write: (text) => pacing.write(text),
|
|
68
|
+
}),
|
|
69
|
+
[enabled, pacing, terminalCapabilities.turnMarks],
|
|
70
|
+
);
|
|
71
|
+
const wasThinking = useRef(false);
|
|
72
|
+
const runningTools = useRef<ReadonlySet<string>>(new Set());
|
|
73
|
+
|
|
74
|
+
// Turn boundaries: B/C when work starts, D/A when it ends and the prompt comes back.
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (isThinking && !wasThinking.current) {
|
|
77
|
+
marks.emit('promptEnd');
|
|
78
|
+
marks.emit('turnStart');
|
|
79
|
+
} else if (!isThinking && wasThinking.current) {
|
|
80
|
+
marks.emit('turnEnd');
|
|
81
|
+
marks.emit('promptStart');
|
|
82
|
+
bell.replyCompleted();
|
|
83
|
+
}
|
|
84
|
+
wasThinking.current = isThinking;
|
|
85
|
+
}, [isThinking, bell, marks]);
|
|
86
|
+
|
|
87
|
+
// The very first prompt of the session is marked once, before anything has been asked.
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
marks.emit('promptStart');
|
|
90
|
+
}, [marks]);
|
|
91
|
+
|
|
92
|
+
// A prompt or dialog that wants an answer rings once on mount.
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (awaitingAnswer) bell.promptMounted();
|
|
95
|
+
}, [awaitingAnswer, bell]);
|
|
96
|
+
|
|
97
|
+
// Long-running tools: record each start, ring on the completions that outlived the threshold.
|
|
98
|
+
//
|
|
99
|
+
// Completion is the `isRunning` true→false transition, NOT the entry leaving the array. The
|
|
100
|
+
// session's `onToolEnd` replaces the finished entry in place (same index, same name, so the same
|
|
101
|
+
// key) and only `onThinking(false)` empties the array — so a membership test would report every
|
|
102
|
+
// tool as completing at turn end, judging its duration against the whole turn.
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
const running = runningKeys(activeTools);
|
|
105
|
+
for (const key of running) {
|
|
106
|
+
if (!runningTools.current.has(key)) bell.toolStarted(key);
|
|
107
|
+
}
|
|
108
|
+
for (const key of runningTools.current) {
|
|
109
|
+
if (!running.has(key)) bell.toolCompleted(key);
|
|
110
|
+
}
|
|
111
|
+
runningTools.current = running;
|
|
112
|
+
}, [activeTools, bell]);
|
|
113
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CMD-004 Phase 2 Stage C — the TUI as a pure renderer.
|
|
3
|
+
*
|
|
4
|
+
* The session layer (the host) executes every command host action via `ICommandHostAdapters`
|
|
5
|
+
* BEFORE the command result returns (language change, settings reset, exit/restart, rename,
|
|
6
|
+
* statusline patch, remote control — see `interactive-session-host-actions.ts`). This hook only:
|
|
7
|
+
*
|
|
8
|
+
* 1. renders the four UI screens from the requester-routed `ui_intent` session event
|
|
9
|
+
* (`show-plugin-manager` / `show-settings` / `show-session-picker` / `show-agent-switcher`) —
|
|
10
|
+
* the surface that issued the command renders the intent; intents stamped with another
|
|
11
|
+
* surface's driver id (or unattributed) are ignored;
|
|
12
|
+
* 2. reflects the broadcast `session_renamed` event into the rendered title (the host performed
|
|
13
|
+
* the rename — the TUI never mutates the session);
|
|
14
|
+
* 3. refreshes the statusline display after a slash-command result arrives by RE-READING the
|
|
15
|
+
* persisted settings (refresh-on-result — the host applied any patch via the settings adapter).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { OWNER_DRIVER_ID } from '@robota-sdk/agent-interface-session';
|
|
19
|
+
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
|
20
|
+
|
|
21
|
+
import type { IUseSideEffectsOptions, IUseSideEffectsResult } from './side-effects-types.js';
|
|
22
|
+
import type { ITuiSessionUiEventPort } from '../tui-app-channel-port.js';
|
|
23
|
+
import type { ISessionRenamedEvent, IUiIntentEvent } from '@robota-sdk/agent-interface-session';
|
|
24
|
+
|
|
25
|
+
interface IUiEventHandlers {
|
|
26
|
+
setSessionName: (name: string) => void;
|
|
27
|
+
refreshStatusLineSettings: () => void;
|
|
28
|
+
/** SCREEN-2002: re-read the persisted appearance after a slash command may have patched it. */
|
|
29
|
+
refreshAppearanceSettings: () => void;
|
|
30
|
+
openAgentSwitcher?: (() => void) | undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface IScreenSetters {
|
|
34
|
+
setShowPluginTUI: (show: boolean) => void;
|
|
35
|
+
setShowTransportTUI: (show: boolean) => void;
|
|
36
|
+
setShowSessionPicker: (show: boolean) => void;
|
|
37
|
+
setShowThemePicker: (show: boolean) => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Subscribe to `ui_intent` (requester-routed) + `session_renamed` (broadcast); returns cleanup. */
|
|
41
|
+
function subscribeToSessionUiEvents(
|
|
42
|
+
uiEventPort: ITuiSessionUiEventPort,
|
|
43
|
+
screens: IScreenSetters,
|
|
44
|
+
handlersRef: { current: IUiEventHandlers },
|
|
45
|
+
): () => void {
|
|
46
|
+
const onUiIntent = (event: IUiIntentEvent): void => {
|
|
47
|
+
// Requester-routed: this surface is the local operator (OWNER_DRIVER_ID). Intents issued by
|
|
48
|
+
// other surfaces — or unattributed ones — are theirs to render, not ours.
|
|
49
|
+
if (event.requesterDriverId !== OWNER_DRIVER_ID) return;
|
|
50
|
+
switch (event.intent.type) {
|
|
51
|
+
case 'show-plugin-manager':
|
|
52
|
+
screens.setShowPluginTUI(true);
|
|
53
|
+
return;
|
|
54
|
+
case 'show-settings':
|
|
55
|
+
screens.setShowTransportTUI(true);
|
|
56
|
+
return;
|
|
57
|
+
case 'show-session-picker':
|
|
58
|
+
screens.setShowSessionPicker(true);
|
|
59
|
+
return;
|
|
60
|
+
case 'show-agent-switcher':
|
|
61
|
+
handlersRef.current.openAgentSwitcher?.();
|
|
62
|
+
return;
|
|
63
|
+
case 'show-theme-picker':
|
|
64
|
+
screens.setShowThemePicker(true);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const onSessionRenamed = (event: ISessionRenamedEvent): void => {
|
|
69
|
+
handlersRef.current.setSessionName(event.name);
|
|
70
|
+
};
|
|
71
|
+
uiEventPort.on('ui_intent', onUiIntent);
|
|
72
|
+
uiEventPort.on('session_renamed', onSessionRenamed);
|
|
73
|
+
return () => {
|
|
74
|
+
uiEventPort.off('ui_intent', onUiIntent);
|
|
75
|
+
uiEventPort.off('session_renamed', onSessionRenamed);
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Latest-callback refs so the session subscription binds exactly once per session: App recreates
|
|
81
|
+
* some callbacks every render, and re-subscribing on each identity change would churn listeners.
|
|
82
|
+
*/
|
|
83
|
+
function useLatestHandlers(handlers: IUiEventHandlers): { current: IUiEventHandlers } {
|
|
84
|
+
const ref = useRef(handlers);
|
|
85
|
+
ref.current = handlers;
|
|
86
|
+
return ref;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function useSideEffects({
|
|
90
|
+
uiEventPort,
|
|
91
|
+
baseHandleSubmit,
|
|
92
|
+
setSessionName,
|
|
93
|
+
refreshStatusLineSettings,
|
|
94
|
+
refreshAppearanceSettings,
|
|
95
|
+
showSessionPickerOnStart,
|
|
96
|
+
openAgentSwitcher,
|
|
97
|
+
}: IUseSideEffectsOptions): IUseSideEffectsResult {
|
|
98
|
+
const [showPluginTUI, setShowPluginTUI] = useState(false);
|
|
99
|
+
const [showSessionPicker, setShowSessionPicker] = useState(showSessionPickerOnStart ?? false);
|
|
100
|
+
const [showTransportTUI, setShowTransportTUI] = useState(false);
|
|
101
|
+
const [showThemePicker, setShowThemePicker] = useState(false);
|
|
102
|
+
|
|
103
|
+
const handlersRef = useLatestHandlers({
|
|
104
|
+
setSessionName,
|
|
105
|
+
refreshStatusLineSettings,
|
|
106
|
+
refreshAppearanceSettings,
|
|
107
|
+
openAgentSwitcher,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
useLayoutEffect(
|
|
111
|
+
() =>
|
|
112
|
+
subscribeToSessionUiEvents(
|
|
113
|
+
uiEventPort,
|
|
114
|
+
{ setShowPluginTUI, setShowTransportTUI, setShowSessionPicker, setShowThemePicker },
|
|
115
|
+
handlersRef,
|
|
116
|
+
),
|
|
117
|
+
[uiEventPort],
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const handleSubmit = useCallback(
|
|
121
|
+
async (input: string): Promise<void> => {
|
|
122
|
+
await baseHandleSubmit(input);
|
|
123
|
+
// Refresh-on-result: a slash command's result has arrived; the host may have patched the
|
|
124
|
+
// persisted statusline settings — re-read them for the status bar.
|
|
125
|
+
if (input.trimStart().startsWith('/')) {
|
|
126
|
+
handlersRef.current.refreshStatusLineSettings();
|
|
127
|
+
handlersRef.current.refreshAppearanceSettings();
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
[baseHandleSubmit],
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
handleSubmit,
|
|
135
|
+
showPluginTUI,
|
|
136
|
+
showSessionPicker,
|
|
137
|
+
showTransportTUI,
|
|
138
|
+
showThemePicker,
|
|
139
|
+
setShowPluginTUI,
|
|
140
|
+
setShowSessionPicker,
|
|
141
|
+
setShowTransportTUI,
|
|
142
|
+
setShowThemePicker,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { DEFAULT_STATUS_LINE_COMMAND_SETTINGS } from '@robota-sdk/agent-framework';
|
|
2
|
+
import { useCallback, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
import { useTuiCliAdapter } from '../tui-cli-adapter-context.js';
|
|
5
|
+
|
|
6
|
+
import type { ITuiCliAdapter } from '../tui-cli-adapter.js';
|
|
7
|
+
import type { TUniversalValue } from '@robota-sdk/agent-core';
|
|
8
|
+
import type { IStatusLineCommandSettings } from '@robota-sdk/agent-interface-command';
|
|
9
|
+
|
|
10
|
+
function readStatusLineSettings(
|
|
11
|
+
settings: Record<string, TUniversalValue>,
|
|
12
|
+
): IStatusLineCommandSettings {
|
|
13
|
+
const defaults = { ...DEFAULT_STATUS_LINE_COMMAND_SETTINGS };
|
|
14
|
+
const raw = settings.statusline;
|
|
15
|
+
if (!isRecord(raw)) {
|
|
16
|
+
return defaults;
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
enabled: typeof raw.enabled === 'boolean' ? raw.enabled : defaults.enabled,
|
|
20
|
+
gitBranch: typeof raw.gitBranch === 'boolean' ? raw.gitBranch : defaults.gitBranch,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isRecord(value: TUniversalValue): value is Record<string, TUniversalValue> {
|
|
25
|
+
return (
|
|
26
|
+
value !== null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date)
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readFromAdapter(cliAdapter: ITuiCliAdapter): IStatusLineCommandSettings {
|
|
31
|
+
return readStatusLineSettings(cliAdapter.readSettings(cliAdapter.getUserSettingsPath()));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Statusline settings as persisted on disk, plus a from-disk `refresh()`.
|
|
36
|
+
*
|
|
37
|
+
* CMD-004 Phase 2 Stage C: the HOST applies `statusline-settings-patch` via the settings adapter
|
|
38
|
+
* (the TUI no longer writes settings), so the renderer refreshes by RE-READING the persisted
|
|
39
|
+
* document when a command result arrives (refresh-on-result).
|
|
40
|
+
*/
|
|
41
|
+
export function useStatusLineSettings(): [IStatusLineCommandSettings, () => void] {
|
|
42
|
+
const cliAdapter = useTuiCliAdapter();
|
|
43
|
+
const [settings, setSettings] = useState<IStatusLineCommandSettings>(() =>
|
|
44
|
+
readFromAdapter(cliAdapter),
|
|
45
|
+
);
|
|
46
|
+
const refresh = useCallback((): void => {
|
|
47
|
+
setSettings(readFromAdapter(cliAdapter));
|
|
48
|
+
}, [cliAdapter]);
|
|
49
|
+
return [settings, refresh];
|
|
50
|
+
}
|