@robota-sdk/agent-ui-terminal 3.0.0-beta.100
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/CHANGELOG.md +1204 -0
- package/LICENSE +661 -0
- package/README.md +50 -0
- package/dist/node/index.d.ts +1224 -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 +88 -0
- package/src/App.tsx +157 -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 +236 -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 +92 -0
- package/src/SessionStatusBar.tsx +80 -0
- package/src/SlashAutocomplete.tsx +154 -0
- package/src/StatusBar.tsx +308 -0
- package/src/StreamingIndicator.tsx +145 -0
- package/src/SupervisedSessionView.tsx +872 -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 +530 -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 +52 -0
- package/src/__tests__/SupervisedSessionView.test.tsx +1112 -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 +294 -0
- package/src/__tests__/TuiInteractionChannel.lifecycle.test.ts +650 -0
- package/src/__tests__/UpdateNotice.test.tsx +15 -0
- package/src/__tests__/__fixtures__/korean-text.json +17 -0
- package/src/__tests__/abort-after-permission.test.tsx +160 -0
- package/src/__tests__/abort-streaming-e2e.test.tsx +183 -0
- package/src/__tests__/attached-app-session-switch.test.tsx +317 -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__/checkpoint-store-per-channel.test.ts +81 -0
- package/src/__tests__/cjk-defer-submit.test.tsx +68 -0
- package/src/__tests__/cjk-fallback-render.test.tsx +89 -0
- package/src/__tests__/cjk-text-input-flow.test.ts +187 -0
- package/src/__tests__/cjk-text-input.test.ts +196 -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 +47 -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 +114 -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__/grant-history-reaches-the-session.test.ts +31 -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 +474 -0
- package/src/__tests__/numbered-selection-chunk.test.tsx +41 -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 +329 -0
- package/src/__tests__/permission-prompt-arming.test.tsx +153 -0
- package/src/__tests__/permission-prompt-availability.test.tsx +23 -0
- package/src/__tests__/permission-prompt-peer.test.tsx +34 -0
- package/src/__tests__/permission-prompt-requester.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 +350 -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 +320 -0
- package/src/__tests__/pty/ime-cursor-tmux.ptytest.ts +211 -0
- package/src/__tests__/pty/ime-cursor.ptytest.ts +187 -0
- package/src/__tests__/pty/isolated-home.ts +97 -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 +155 -0
- package/src/__tests__/pty/replay-conversation.ptytest.ts +68 -0
- package/src/__tests__/pty/screen-005-prompt-footers.ptytest.ts +135 -0
- package/src/__tests__/pty/screen-006-no-color.ptytest.ts +140 -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/session-attach.ptytest.ts +182 -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/trust-prompt.ptytest.ts +134 -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 +264 -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__/sandbox-reaches-the-session.test.ts +37 -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-picker.test.tsx +93 -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 +290 -0
- package/src/__tests__/status-glyph.test.ts +30 -0
- package/src/__tests__/streaming-indicator.test.tsx +178 -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 +70 -0
- package/src/__tests__/tui-channel-lifecycle-coordinator.test.ts +258 -0
- package/src/__tests__/tui-interaction-queues.test.ts +149 -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 +482 -0
- package/src/__tests__/ui-intent-rendering.test.tsx +291 -0
- package/src/__tests__/wave-text.test.tsx +133 -0
- package/src/__tests__/wire-tui-channel.test.ts +1457 -0
- package/src/__tests__/wire-tui-client-commands.test.ts +243 -0
- package/src/app-banner.tsx +42 -0
- package/src/app-static-items.ts +35 -0
- package/src/app-view-model.ts +140 -0
- package/src/attached-session-connection.ts +14 -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 +93 -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 +88 -0
- package/src/flows/__tests__/real-cursor-flow.test.ts +139 -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-detach-key.test.tsx +72 -0
- package/src/hooks/__tests__/use-app-loop-escape.test.tsx +87 -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 +271 -0
- package/src/hooks/useAppInputBindings.ts +163 -0
- package/src/hooks/useAppInteractionState.ts +84 -0
- package/src/hooks/useAppLifecycleState.ts +81 -0
- package/src/hooks/useAppOverlays.ts +90 -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 +93 -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 +141 -0
- package/src/humanize-tool-name.ts +51 -0
- package/src/index.ts +72 -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 +118 -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 +746 -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/short-session-id.ts +9 -0
- package/src/shutdown-signal.ts +34 -0
- package/src/slash-command-input.ts +11 -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 +111 -0
- package/src/tui-channel-lifecycle-coordinator.ts +149 -0
- package/src/tui-channel-options.ts +184 -0
- package/src/tui-cli-adapter-context.tsx +13 -0
- package/src/tui-cli-adapter.ts +24 -0
- package/src/tui-interaction-queues.ts +171 -0
- package/src/tui-session-binding.ts +52 -0
- package/src/tui-session-event-projector.ts +149 -0
- package/src/tui-session-events.ts +89 -0
- package/src/tui-session-notice-store.ts +43 -0
- package/src/tui-session-options.ts +134 -0
- package/src/tui-state-manager.ts +347 -0
- package/src/types.ts +21 -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
- package/src/waiting-loop-stop-notice.ts +23 -0
- package/src/wire-history-sync.ts +159 -0
- package/src/wire-tui-channel.ts +963 -0
- package/src/wire-tui-client-commands.ts +142 -0
- package/src/wire-tui-projection.ts +136 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TERM-002: React glue for the terminal-handoff controller.
|
|
3
|
+
*
|
|
4
|
+
* Registers the App's suspend/resume into the controller. On suspend the App renders nothing (see
|
|
5
|
+
* App.tsx) so Ink unmounts its input hooks and releases raw mode, handing the real terminal to the
|
|
6
|
+
* child process; on resume the App re-renders.
|
|
7
|
+
*/
|
|
8
|
+
import { useEffect, useState } from 'react';
|
|
9
|
+
|
|
10
|
+
import type { TerminalHandoffController } from '../terminal-handoff-controller.js';
|
|
11
|
+
|
|
12
|
+
/** Time for React/Ink to commit the empty render (unmount input, release raw mode) before handoff. */
|
|
13
|
+
const SUSPEND_COMMIT_DELAY_MS = 50;
|
|
14
|
+
|
|
15
|
+
export function useTerminalHandoffSuspension(
|
|
16
|
+
controller: TerminalHandoffController | undefined,
|
|
17
|
+
): boolean {
|
|
18
|
+
const [suspended, setSuspended] = useState(false);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!controller) return;
|
|
21
|
+
return controller.registerSuspendHooks({
|
|
22
|
+
suspend: async () => {
|
|
23
|
+
setSuspended(true);
|
|
24
|
+
await new Promise<void>((resolve) => setTimeout(resolve, SUSPEND_COMMIT_DELAY_MS));
|
|
25
|
+
},
|
|
26
|
+
resume: () => setSuspended(false),
|
|
27
|
+
});
|
|
28
|
+
}, [controller]);
|
|
29
|
+
return suspended;
|
|
30
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useTuiChannel — React hook that subscribes to the bounded TUI channel port.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
ITuiAppChannelPort,
|
|
9
|
+
ITuiCommandQueryPort,
|
|
10
|
+
ITuiRuntimeStatusSnapshot,
|
|
11
|
+
ITuiSessionUiEventPort,
|
|
12
|
+
} from '../tui-app-channel-port.js';
|
|
13
|
+
import type { ITuiSessionEventNotice } from '../tui-session-events.js';
|
|
14
|
+
import type { IPendingPermissionRequest } from '../types.js';
|
|
15
|
+
import type {
|
|
16
|
+
IActionRequest,
|
|
17
|
+
IHistoryEntry,
|
|
18
|
+
TActionResponse,
|
|
19
|
+
TPermissionMode,
|
|
20
|
+
TSessionEndReason,
|
|
21
|
+
} from '@robota-sdk/agent-core';
|
|
22
|
+
import type {
|
|
23
|
+
IExecutionDetailPage,
|
|
24
|
+
IExecutionWorkspaceSnapshot,
|
|
25
|
+
} from '@robota-sdk/agent-interface-execution';
|
|
26
|
+
import type { ISessionListingEntry, IToolState } from '@robota-sdk/agent-interface-session';
|
|
27
|
+
|
|
28
|
+
export interface ITuiChannelState {
|
|
29
|
+
uiEventPort: ITuiSessionUiEventPort;
|
|
30
|
+
commandQueryPort: ITuiCommandQueryPort;
|
|
31
|
+
history: readonly IHistoryEntry[];
|
|
32
|
+
addEntry: (entry: IHistoryEntry) => void;
|
|
33
|
+
streamingText: string;
|
|
34
|
+
activeTools: readonly IToolState[];
|
|
35
|
+
isThinking: boolean;
|
|
36
|
+
isAborting: boolean;
|
|
37
|
+
/** ERR-001 G2: humanized message of the last failed turn (null once the next turn starts). */
|
|
38
|
+
lastErrorMessage: string | null;
|
|
39
|
+
/** ERR-001 G3: no provider activity for a while during a turn — connection may be stalled. */
|
|
40
|
+
isStalled: boolean;
|
|
41
|
+
sessionEventNotices: readonly ITuiSessionEventNotice[];
|
|
42
|
+
isShuttingDown: boolean;
|
|
43
|
+
pendingPrompt: string | null;
|
|
44
|
+
/** REMOTE-014 E5: total queued turns across all drivers (owner + co-drivers); >1 means a co-driver is queued. */
|
|
45
|
+
pendingCount: number;
|
|
46
|
+
executionWorkspaceSnapshot: IExecutionWorkspaceSnapshot | null;
|
|
47
|
+
selectedExecutionEntryId?: string;
|
|
48
|
+
permissionRequest: IPendingPermissionRequest | null;
|
|
49
|
+
/** CMD-004: the unified action awaiting a user answer, or null. */
|
|
50
|
+
pendingUserAction: IActionRequest | null;
|
|
51
|
+
contextState: { percentage: number; usedTokens: number; maxTokens: number };
|
|
52
|
+
/** The host's sessions for the picker, when the channel's host keeps them. */
|
|
53
|
+
hostSessions: readonly ISessionListingEntry[] | undefined;
|
|
54
|
+
/** Changes when the transcript starts over for another session under this channel. */
|
|
55
|
+
transcriptGeneration: number;
|
|
56
|
+
/** This terminal only observes the session. */
|
|
57
|
+
readOnly: boolean;
|
|
58
|
+
/** This terminal is attached to a session a host runs; leaving only detaches it. */
|
|
59
|
+
attached: boolean;
|
|
60
|
+
handleSubmit: (input: string) => Promise<void>;
|
|
61
|
+
handleAbort: () => void;
|
|
62
|
+
handleCancelQueue: () => void;
|
|
63
|
+
handleStopWaitingLoop: () => Promise<void>;
|
|
64
|
+
handleShutdown: (reason?: TSessionEndReason) => Promise<void>;
|
|
65
|
+
sendAgentJob: (taskId: string, input: string) => Promise<void>;
|
|
66
|
+
resolveUserAction: (request: IActionRequest, response: TActionResponse) => void;
|
|
67
|
+
getRuntimeStatusSnapshot: (fallback: TPermissionMode) => ITuiRuntimeStatusSnapshot;
|
|
68
|
+
selectExecutionWorkspaceEntry: (entryId: string) => void;
|
|
69
|
+
readExecutionWorkspaceDetail: (entryId: string) => Promise<IExecutionDetailPage>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface IHistoryReadableSession {
|
|
73
|
+
getFullHistory(): IHistoryEntry[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface IHistorySyncManager {
|
|
77
|
+
syncHistory(entries: IHistoryEntry[]): void;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function applyCompactEventToManager(
|
|
81
|
+
interactiveSession: IHistoryReadableSession,
|
|
82
|
+
manager: IHistorySyncManager,
|
|
83
|
+
): void {
|
|
84
|
+
manager.syncHistory(interactiveSession.getFullHistory());
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function useTuiChannel(channel: ITuiAppChannelPort): ITuiChannelState {
|
|
88
|
+
const [, forceRender] = useState(0);
|
|
89
|
+
|
|
90
|
+
useEffect(() => channel.subscribe(() => forceRender((n) => n + 1)), [channel]);
|
|
91
|
+
const snapshot = channel.getSnapshot();
|
|
92
|
+
|
|
93
|
+
// SCREEN-014 fix: these are consumed in `useEffect` dependency arrays in App. `channel` is stable
|
|
94
|
+
// (created once), so memoize them — a fresh closure each render made the detail-loading effect
|
|
95
|
+
// re-run every render and `setState`-loop ("Maximum update depth exceeded") whenever a background
|
|
96
|
+
// entry was selected.
|
|
97
|
+
const selectExecutionWorkspaceEntry = useCallback(
|
|
98
|
+
(id: string) => channel.selectExecutionWorkspaceEntry(id),
|
|
99
|
+
[channel],
|
|
100
|
+
);
|
|
101
|
+
const readExecutionWorkspaceDetail = useCallback(
|
|
102
|
+
(id: string) => channel.readExecutionWorkspaceDetail(id),
|
|
103
|
+
[channel],
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
uiEventPort: channel.getSessionUiEventPort(),
|
|
108
|
+
commandQueryPort: channel.getCommandQueryPort(),
|
|
109
|
+
history: snapshot.history,
|
|
110
|
+
addEntry: (entry) => channel.addEntry(entry),
|
|
111
|
+
streamingText: snapshot.streamingText,
|
|
112
|
+
activeTools: snapshot.activeTools,
|
|
113
|
+
isThinking: snapshot.isThinking,
|
|
114
|
+
isAborting: snapshot.isAborting,
|
|
115
|
+
lastErrorMessage: snapshot.lastErrorMessage,
|
|
116
|
+
isStalled: snapshot.isStalled,
|
|
117
|
+
sessionEventNotices: snapshot.sessionEventNotices,
|
|
118
|
+
isShuttingDown: snapshot.isShuttingDown,
|
|
119
|
+
pendingPrompt: snapshot.pendingPrompt,
|
|
120
|
+
pendingCount: snapshot.pendingCount,
|
|
121
|
+
executionWorkspaceSnapshot: snapshot.executionWorkspaceSnapshot,
|
|
122
|
+
selectedExecutionEntryId: snapshot.selectedExecutionEntryId,
|
|
123
|
+
permissionRequest: snapshot.permissionRequest,
|
|
124
|
+
pendingUserAction: snapshot.pendingUserAction,
|
|
125
|
+
contextState: snapshot.contextState,
|
|
126
|
+
hostSessions: snapshot.hostSessions,
|
|
127
|
+
transcriptGeneration: snapshot.transcriptGeneration ?? 0,
|
|
128
|
+
readOnly: snapshot.readOnly === true,
|
|
129
|
+
attached: snapshot.attached === true,
|
|
130
|
+
handleSubmit: (input) => channel.handleInput(input),
|
|
131
|
+
handleAbort: () => channel.abort(),
|
|
132
|
+
handleCancelQueue: () => channel.cancelQueue(),
|
|
133
|
+
handleStopWaitingLoop: () => channel.stopWaitingSelfPacedLoop(),
|
|
134
|
+
handleShutdown: (reason) => channel.shutdown({ reason }),
|
|
135
|
+
sendAgentJob: (taskId, input) => channel.sendAgentJob(taskId, input),
|
|
136
|
+
resolveUserAction: (request, response) => channel.resolveUserAction(request, response),
|
|
137
|
+
getRuntimeStatusSnapshot: (fallback) => channel.getRuntimeStatusSnapshot(fallback),
|
|
138
|
+
selectExecutionWorkspaceEntry,
|
|
139
|
+
readExecutionWorkspaceDetail,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCREEN-012: turn a provider-safe projected tool id into the natural command name for display.
|
|
3
|
+
*
|
|
4
|
+
* Slash commands invoked by the model are projected to provider-safe tool ids prefixed with
|
|
5
|
+
* the host-selected prefix (or the neutral default), optionally suffixed with a `_<8 hex>` hash when
|
|
6
|
+
* the name would otherwise exceed the provider length limit (see model-command-tool-projection.ts).
|
|
7
|
+
* For display we recover the command name: strip the prefix and a trailing projection hash. Tool
|
|
8
|
+
* names that are not command projections (e.g. `Shell`, `Read`) are returned unchanged.
|
|
9
|
+
*
|
|
10
|
+
* ## SEC-019: this is where a tool label is made safe, not at each render site
|
|
11
|
+
*
|
|
12
|
+
* A tool name and its first argument come from the model, from an MCP server's manifest, or from a
|
|
13
|
+
* plugin — all untrusted — and they are put on the terminal as text. Sanitizing at the render sites
|
|
14
|
+
* was tried and failed within one review round: `MessageList` has two branches that show a tool name
|
|
15
|
+
* and only one of them got the call, so a name carrying OSC 52 still reached `<Text>` whenever the
|
|
16
|
+
* tool's content happened to parse as a structured summary. Three more sites had the same gap
|
|
17
|
+
* (`StreamingIndicator`, and the label `tool-summary-status` composes for the history view).
|
|
18
|
+
*
|
|
19
|
+
* The defect is not that a call was forgotten. It is that "make this displayable" and "make this
|
|
20
|
+
* safe to display" were two separate steps, so every new render site had to remember the second one.
|
|
21
|
+
* They are one step here, and a fifth caller is covered because it cannot obtain the display text
|
|
22
|
+
* without going through the function that sanitizes it.
|
|
23
|
+
*/
|
|
24
|
+
import { MODEL_COMMAND_TOOL_PREFIX } from '@robota-sdk/agent-framework';
|
|
25
|
+
|
|
26
|
+
import { sanitizeTerminalText } from './sanitize-terminal-text.js';
|
|
27
|
+
|
|
28
|
+
/** A trailing `_<8 lowercase hex>` projection hash appended to over-long command tool names. */
|
|
29
|
+
const PROJECTION_HASH_SUFFIX = /_[0-9a-f]{8}$/;
|
|
30
|
+
|
|
31
|
+
export function humanizeToolName(
|
|
32
|
+
toolName: string,
|
|
33
|
+
prefix: string = MODEL_COMMAND_TOOL_PREFIX,
|
|
34
|
+
): string {
|
|
35
|
+
if (!toolName.startsWith(prefix)) return sanitizeTerminalText(toolName);
|
|
36
|
+
const body = toolName.slice(prefix.length);
|
|
37
|
+
const withoutHash = body.replace(PROJECTION_HASH_SUFFIX, '');
|
|
38
|
+
return sanitizeTerminalText(withoutHash.length > 0 ? withoutHash : toolName);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The first argument of a tool call, as display text.
|
|
43
|
+
*
|
|
44
|
+
* A separate function rather than a bare `sanitizeTerminalText` at the two sites that show it: the
|
|
45
|
+
* argument is the other half of a tool label, it is equally untrusted — a path, a shell command, a
|
|
46
|
+
* URL the model chose — and pairing it with {@link humanizeToolName} is what makes the boundary
|
|
47
|
+
* findable. A render site that reaches for one naturally reaches for the other.
|
|
48
|
+
*/
|
|
49
|
+
export function humanizeToolArgument(firstArg: string | undefined): string {
|
|
50
|
+
return firstArg === undefined ? '' : sanitizeTerminalText(firstArg);
|
|
51
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export { renderApp, renderAttachedApp } from './render.js';
|
|
2
|
+
export { renderSupervisedSessionView } from './SupervisedSessionView.js';
|
|
3
|
+
export type {
|
|
4
|
+
IAttachedSessionConnection,
|
|
5
|
+
TAttachedSessionEnd,
|
|
6
|
+
} from './attached-session-connection.js';
|
|
7
|
+
export type {
|
|
8
|
+
ISupervisedAttachRequest,
|
|
9
|
+
ISupervisedViewRow,
|
|
10
|
+
ISupervisedSessionViewProps,
|
|
11
|
+
ISupervisedStartTrustQuestion,
|
|
12
|
+
TSupervisedStartTrustChoice,
|
|
13
|
+
TSupervisedViewExit,
|
|
14
|
+
} from './SupervisedSessionView.js';
|
|
15
|
+
export { TuiInteractionChannel } from './TuiInteractionChannel.js';
|
|
16
|
+
export type { ITuiInteractionChannelOptions } from './TuiInteractionChannel.js';
|
|
17
|
+
export type {
|
|
18
|
+
ITuiClientCommand,
|
|
19
|
+
ITuiClientCommands,
|
|
20
|
+
TTuiClientCommandHost,
|
|
21
|
+
} from './wire-tui-client-commands.js';
|
|
22
|
+
export type { IRenderAttachedAppOptions, IRenderOptions } from './render.js';
|
|
23
|
+
export type { ITerminalCapabilityOverrides } from './terminal-capabilities-context.js';
|
|
24
|
+
export type { TScreenReaderChannel } from './screen-reader-announcement.js';
|
|
25
|
+
export type { IScreenReaderPacingOverrides } from './screen-reader-pacing.js';
|
|
26
|
+
export type { ITuiCliAdapter } from './tui-cli-adapter.js';
|
|
27
|
+
export type { IDefaultTuiCliAdapterOptions } from './create-default-tui-cli-adapter.js';
|
|
28
|
+
export { createDefaultTuiCliAdapter } from './create-default-tui-cli-adapter.js';
|
|
29
|
+
export { createNodeKeybindingsSource } from './keybindings/node-keybindings-source.js';
|
|
30
|
+
// SCREEN-2002: the product shell assembles one registry and hands it to both `/theme` and
|
|
31
|
+
// `renderApp`. The theme DATA stays unexported — a caller asks the registry, never a built-in.
|
|
32
|
+
export { createThemeCataloguePort, createThemeRegistry } from './theme/theme-registry.js';
|
|
33
|
+
// The product shell composes the registry, so it must be able to enumerate the built-ins, parse the
|
|
34
|
+
// files that join them, and build its OWN diagnostics through the same escaping policy. The
|
|
35
|
+
// accessor comes from the module that USES it rather than from the data module, which the
|
|
36
|
+
// anti-drift floor forbids naming outside `src/theme/`.
|
|
37
|
+
export { listBuiltInThemes } from './theme/theme-registry.js';
|
|
38
|
+
export {
|
|
39
|
+
escapeThemeText,
|
|
40
|
+
parseThemeDocument,
|
|
41
|
+
quoteThemeText,
|
|
42
|
+
sanitizeThemeProse,
|
|
43
|
+
} from './theme/theme-document.js';
|
|
44
|
+
export type {
|
|
45
|
+
IThemeCataloguePortOptions,
|
|
46
|
+
IThemeRegistry,
|
|
47
|
+
IThemeResolution,
|
|
48
|
+
IThemeSkip,
|
|
49
|
+
} from './theme/theme-registry.js';
|
|
50
|
+
export type { IThemeDocumentInput, TThemeDocumentResult } from './theme/theme-document.js';
|
|
51
|
+
// `createThemeRegistry` takes themes, so a caller must be able to name one.
|
|
52
|
+
export type { ITuiTheme, TThemeSource } from './theme/theme-contracts.js';
|
|
53
|
+
export type {
|
|
54
|
+
IKeybindingsFilePort,
|
|
55
|
+
IKeybindingsSource,
|
|
56
|
+
INodeKeybindingsSourceOptions,
|
|
57
|
+
} from './keybindings/node-keybindings-source.js';
|
|
58
|
+
export type {
|
|
59
|
+
IKeybindingDiagnostic,
|
|
60
|
+
IKeybindingSnapshot,
|
|
61
|
+
IKeybindingWarning,
|
|
62
|
+
TKeybindingAction,
|
|
63
|
+
TKeybindingContext,
|
|
64
|
+
} from './keybindings/keybinding-registry.js';
|
|
65
|
+
export type {
|
|
66
|
+
TOnMissingArgsAction,
|
|
67
|
+
ITuiPickerItem,
|
|
68
|
+
ITuiCommandInteraction,
|
|
69
|
+
ITuiPickerInteraction,
|
|
70
|
+
ITuiConfirmInteraction,
|
|
71
|
+
TAnyTuiCommandInteraction,
|
|
72
|
+
} from './command-interaction.js';
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The input area's horizontal rules, and the deletion announcement that replaces them.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `InputArea.tsx` (CLI-2004). The rules are hand-drawn `<Text>` lines rather than a
|
|
5
|
+
* Yoga-synthesized `Box` border because a synthesized border drops glyphs during rapid re-render at
|
|
6
|
+
* full terminal height (SCREEN-003) — which is exactly why they are not covered by the `borderStyle`
|
|
7
|
+
* suppression the mode applies elsewhere and need their own branch.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React from 'react';
|
|
11
|
+
|
|
12
|
+
import { Text } from './SafeText.js';
|
|
13
|
+
import { usePalette } from './theme/index.js';
|
|
14
|
+
import { buildInputTopBorder } from './utils/input-top-border.js';
|
|
15
|
+
|
|
16
|
+
/** The announcement's exact shape, so the input and its test cannot disagree about it. */
|
|
17
|
+
export function formatDeletionAnnouncement(deleted: string): string {
|
|
18
|
+
return `[deleted: ${deleted}]`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Top rule, carrying the optional right-aligned session title. Nothing in screen-reader mode. */
|
|
22
|
+
export function InputTopRule({
|
|
23
|
+
screenReader,
|
|
24
|
+
innerWidth,
|
|
25
|
+
borderColor,
|
|
26
|
+
sessionName,
|
|
27
|
+
}: {
|
|
28
|
+
screenReader: boolean;
|
|
29
|
+
innerWidth: number;
|
|
30
|
+
borderColor: string;
|
|
31
|
+
sessionName?: string;
|
|
32
|
+
}): React.ReactElement | null {
|
|
33
|
+
const palette = usePalette();
|
|
34
|
+
if (screenReader) return null;
|
|
35
|
+
const topBorder = buildInputTopBorder(innerWidth, sessionName);
|
|
36
|
+
return (
|
|
37
|
+
<Text color={borderColor}>
|
|
38
|
+
{topBorder.left}
|
|
39
|
+
{topBorder.label ? (
|
|
40
|
+
<Text backgroundColor={borderColor} color={palette.text.onAccent} bold>
|
|
41
|
+
{topBorder.label}
|
|
42
|
+
</Text>
|
|
43
|
+
) : null}
|
|
44
|
+
{topBorder.right}
|
|
45
|
+
</Text>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Bottom rule (mirrors the top). Nothing in screen-reader mode. */
|
|
50
|
+
export function InputBottomRule({
|
|
51
|
+
screenReader,
|
|
52
|
+
innerWidth,
|
|
53
|
+
borderColor,
|
|
54
|
+
}: {
|
|
55
|
+
screenReader: boolean;
|
|
56
|
+
innerWidth: number;
|
|
57
|
+
borderColor: string;
|
|
58
|
+
}): React.ReactElement | null {
|
|
59
|
+
if (screenReader) return null;
|
|
60
|
+
return <Text color={borderColor}>{'─'.repeat(innerWidth)}</Text>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** `[deleted: <text>]`, rendered once for the most recent word/line delete. */
|
|
64
|
+
export function DeletionAnnouncement({
|
|
65
|
+
deleted,
|
|
66
|
+
}: {
|
|
67
|
+
deleted: string | null;
|
|
68
|
+
}): React.ReactElement | null {
|
|
69
|
+
if (deleted === null) return null;
|
|
70
|
+
return <Text>{formatDeletionAnnouncement(deleted)}</Text>;
|
|
71
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape of a parsed JSON document, and the one guard that narrows it.
|
|
3
|
+
*
|
|
4
|
+
* Two user-owned files are read as JSON in this package — the keybindings document and, since
|
|
5
|
+
* SCREEN-2002, a theme document — and both walk an arbitrary tree looking for the keys they know.
|
|
6
|
+
* A second copy of this type would be a second answer to "what can a parsed file hold", which is
|
|
7
|
+
* how two readers of the same kind of file start disagreeing about what a null or an array means.
|
|
8
|
+
*/
|
|
9
|
+
export type TJsonValue = string | number | boolean | null | TJsonValue[] | IJsonRecord;
|
|
10
|
+
|
|
11
|
+
export interface IJsonRecord {
|
|
12
|
+
[key: string]: TJsonValue;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** An object, not an array and not `null` — the distinction `typeof` alone does not make. */
|
|
16
|
+
export function isJsonRecord(value: TJsonValue): value is IJsonRecord {
|
|
17
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
18
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Key-hint footer SSOT (SCREEN-005).
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for prompt-footer key hints and the selection-row indicator across the
|
|
5
|
+
* package's Ink prompt/selection components. Every footer call site declares its hints as
|
|
6
|
+
* `IKeyHint[]` and renders them through `formatKeyHints` / `KeyHintFooter`, so exactly one grammar
|
|
7
|
+
* exists (no per-component footer dialects) and a drift cannot re-appear silently — the
|
|
8
|
+
* `key-hint-consistency` test asserts every declared hint set round-trips through this module.
|
|
9
|
+
*
|
|
10
|
+
* This module ships MECHANICS only: the separator, the pair grammar, the indicator constants.
|
|
11
|
+
* Verb vocabulary (Navigate/Select/Confirm/…) is supplied by callers — the package stays a
|
|
12
|
+
* content-neutral library ingredient. No config surface: per-call-site suppression is passing an
|
|
13
|
+
* empty hint list.
|
|
14
|
+
*
|
|
15
|
+
* Affordance contract (see docs/SPEC.md "Interaction affordance contract"): the footer lists
|
|
16
|
+
* exactly the keys that do something — a prompt that suppresses Esc in its flow omits Esc here.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import React from 'react';
|
|
20
|
+
|
|
21
|
+
import { Text } from './SafeText.js';
|
|
22
|
+
|
|
23
|
+
/** One key hint: the key(s) and the verb label, e.g. `{ keys: '↑↓', label: 'Navigate' }`. */
|
|
24
|
+
export interface IKeyHint {
|
|
25
|
+
keys: string;
|
|
26
|
+
label: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Separator between key-hint pairs. Changing the footer separator is a one-constant edit. */
|
|
30
|
+
export const KEY_HINT_SEPARATOR = ' · ';
|
|
31
|
+
|
|
32
|
+
/** Selection cursor rendered in front of the focused row. */
|
|
33
|
+
export const SELECTION_INDICATOR = '> ';
|
|
34
|
+
|
|
35
|
+
/** Same-width blank rendered in front of non-focused rows so columns stay aligned. */
|
|
36
|
+
export const SELECTION_INDICATOR_NONE = ' ';
|
|
37
|
+
|
|
38
|
+
/** Pure formatter: joins `keys label` pairs with {@link KEY_HINT_SEPARATOR}. Empty list → ''. */
|
|
39
|
+
export function formatKeyHints(hints: readonly IKeyHint[]): string {
|
|
40
|
+
return hints.map((hint) => `${hint.keys} ${hint.label}`).join(KEY_HINT_SEPARATOR);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Dim one-line footer: leading pad + formatted hints. Renders nothing for an empty hint list
|
|
45
|
+
* (per-call-site suppression without a config surface).
|
|
46
|
+
*/
|
|
47
|
+
export function KeyHintFooter({
|
|
48
|
+
hints,
|
|
49
|
+
}: {
|
|
50
|
+
hints: readonly IKeyHint[];
|
|
51
|
+
}): React.ReactElement | null {
|
|
52
|
+
if (hints.length === 0) return null;
|
|
53
|
+
return <Text dimColor> {formatKeyHints(hints)}</Text>;
|
|
54
|
+
}
|