@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,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCREEN-2002 TC-10 — the theme picker.
|
|
3
|
+
*
|
|
4
|
+
* Preview-on-move is the behaviour worth pinning: the highlighted row IS the live region's theme
|
|
5
|
+
* while the picker is open, so moving the highlight must call `preview` and LEAVING must not
|
|
6
|
+
* persist it. Selecting does not write — it submits `/theme <id>` through the normal command path,
|
|
7
|
+
* which is what keeps the host the only writer of the settings document.
|
|
8
|
+
*/
|
|
9
|
+
import { render } from 'ink-testing-library';
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
12
|
+
|
|
13
|
+
import { ScreenReaderProvider } from '../screen-reader-context.js';
|
|
14
|
+
|
|
15
|
+
const gateMock = vi.hoisted(() => ({ value: true }));
|
|
16
|
+
// Spread the real module and override ONE export: a hand-listed factory silently drops whatever
|
|
17
|
+
// the module gains later, and the failure lands as "not a function" in an unrelated file.
|
|
18
|
+
vi.mock('../terminal-capabilities.js', async (importOriginal) => ({
|
|
19
|
+
...(await importOriginal<typeof import('../terminal-capabilities.js')>()),
|
|
20
|
+
isInteractiveColorTerminal: (): boolean => gateMock.value,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
gateMock.value = true;
|
|
25
|
+
});
|
|
26
|
+
import { DARK_THEME, LIGHT_THEME, listBuiltInThemes } from '../theme/built-in-themes.js';
|
|
27
|
+
import ThemePicker from '../ThemePicker.js';
|
|
28
|
+
|
|
29
|
+
import type { IAppThemePickerViewModel } from '../hooks/useAppThemeState.js';
|
|
30
|
+
|
|
31
|
+
const ARROW_DOWN = '\x1b[B';
|
|
32
|
+
const ARROW_UP = '\x1b[A';
|
|
33
|
+
const ENTER = '\r';
|
|
34
|
+
const ESCAPE = '\x1b';
|
|
35
|
+
|
|
36
|
+
function picker(overrides: Partial<IAppThemePickerViewModel> = {}): IAppThemePickerViewModel {
|
|
37
|
+
return {
|
|
38
|
+
visible: true,
|
|
39
|
+
themes: listBuiltInThemes(),
|
|
40
|
+
skipped: [],
|
|
41
|
+
activeThemeId: 'dark',
|
|
42
|
+
syntaxHighlighting: true,
|
|
43
|
+
reducedMotion: false,
|
|
44
|
+
preview: vi.fn(),
|
|
45
|
+
select: vi.fn(),
|
|
46
|
+
cancel: vi.fn(),
|
|
47
|
+
...overrides,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function tick(ms = 20): Promise<void> {
|
|
52
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe('the theme picker (SCREEN-2002 TC-10)', () => {
|
|
56
|
+
it('lists every theme with its appearance and source, and marks the active one', () => {
|
|
57
|
+
const frame = render(<ThemePicker picker={picker()} />).lastFrame() ?? '';
|
|
58
|
+
|
|
59
|
+
expect(frame).toContain(`${DARK_THEME.name} — dark, built-in (current)`);
|
|
60
|
+
expect(frame).toContain(`${LIGHT_THEME.name} — light, built-in`);
|
|
61
|
+
expect(frame).not.toContain(`${LIGHT_THEME.name} — light, built-in (current)`);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('starts focused on the ACTIVE theme, not on the first row', async () => {
|
|
65
|
+
const view = picker({ activeThemeId: 'dark-daltonized' });
|
|
66
|
+
const { stdin } = render(<ThemePicker picker={view} />);
|
|
67
|
+
await tick();
|
|
68
|
+
|
|
69
|
+
stdin.write(ARROW_DOWN);
|
|
70
|
+
await tick();
|
|
71
|
+
|
|
72
|
+
// From `dark-daltonized` (index 2), down lands on `light-daltonized` — not on `light`.
|
|
73
|
+
expect(view.preview).toHaveBeenCalledWith('light-daltonized');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('previews on move, and does not select or cancel while moving', async () => {
|
|
77
|
+
const view = picker();
|
|
78
|
+
const { stdin } = render(<ThemePicker picker={view} />);
|
|
79
|
+
await tick();
|
|
80
|
+
|
|
81
|
+
stdin.write(ARROW_DOWN);
|
|
82
|
+
await tick();
|
|
83
|
+
|
|
84
|
+
expect(view.preview).toHaveBeenCalledWith('light');
|
|
85
|
+
expect(view.select).not.toHaveBeenCalled();
|
|
86
|
+
expect(view.cancel).not.toHaveBeenCalled();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('wraps at both ends rather than stopping', async () => {
|
|
90
|
+
const view = picker();
|
|
91
|
+
const { stdin } = render(<ThemePicker picker={view} />);
|
|
92
|
+
await tick();
|
|
93
|
+
|
|
94
|
+
stdin.write(ARROW_UP);
|
|
95
|
+
await tick();
|
|
96
|
+
|
|
97
|
+
expect(view.preview).toHaveBeenCalledWith('light-daltonized');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('selects the focused row, and selects the PREVIEWED one after moving', async () => {
|
|
101
|
+
const view = picker();
|
|
102
|
+
const { stdin } = render(<ThemePicker picker={view} />);
|
|
103
|
+
await tick();
|
|
104
|
+
|
|
105
|
+
stdin.write(ARROW_DOWN);
|
|
106
|
+
await tick();
|
|
107
|
+
stdin.write(ENTER);
|
|
108
|
+
await tick();
|
|
109
|
+
|
|
110
|
+
expect(view.select).toHaveBeenCalledWith('light', {
|
|
111
|
+
syntaxHighlighting: true,
|
|
112
|
+
reducedMotion: false,
|
|
113
|
+
});
|
|
114
|
+
expect(view.select).toHaveBeenCalledTimes(1);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('cancels on escape WITHOUT selecting — the persisted theme is what remains', async () => {
|
|
118
|
+
const view = picker();
|
|
119
|
+
const { stdin } = render(<ThemePicker picker={view} />);
|
|
120
|
+
await tick();
|
|
121
|
+
|
|
122
|
+
stdin.write(ARROW_DOWN);
|
|
123
|
+
await tick();
|
|
124
|
+
stdin.write(ESCAPE);
|
|
125
|
+
await tick();
|
|
126
|
+
|
|
127
|
+
expect(view.cancel).toHaveBeenCalledTimes(1);
|
|
128
|
+
expect(view.select).not.toHaveBeenCalled();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('toggles syntax and motion locally, and submits them WITH the theme as one patch', async () => {
|
|
132
|
+
const view = picker();
|
|
133
|
+
const { stdin, lastFrame } = render(<ThemePicker picker={view} />);
|
|
134
|
+
await tick();
|
|
135
|
+
|
|
136
|
+
expect(lastFrame() ?? '').toContain('syntax on · motion on');
|
|
137
|
+
stdin.write('s');
|
|
138
|
+
await tick();
|
|
139
|
+
stdin.write('m');
|
|
140
|
+
await tick();
|
|
141
|
+
expect(lastFrame() ?? '').toContain('syntax off · motion off');
|
|
142
|
+
// Nothing is written until select — a toggle is pending, like the previewed theme.
|
|
143
|
+
expect(view.select).not.toHaveBeenCalled();
|
|
144
|
+
|
|
145
|
+
stdin.write(ENTER);
|
|
146
|
+
await tick();
|
|
147
|
+
|
|
148
|
+
expect(view.select).toHaveBeenCalledWith('dark', {
|
|
149
|
+
syntaxHighlighting: false,
|
|
150
|
+
reducedMotion: true,
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('abandons pending toggles on escape, exactly as it abandons the preview', async () => {
|
|
155
|
+
const view = picker();
|
|
156
|
+
const { stdin } = render(<ThemePicker picker={view} />);
|
|
157
|
+
await tick();
|
|
158
|
+
|
|
159
|
+
stdin.write('s');
|
|
160
|
+
await tick();
|
|
161
|
+
stdin.write(ESCAPE);
|
|
162
|
+
await tick();
|
|
163
|
+
|
|
164
|
+
expect(view.cancel).toHaveBeenCalledTimes(1);
|
|
165
|
+
expect(view.select).not.toHaveBeenCalled();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('seeds the toggles from what is PERSISTED, not from the defaults', async () => {
|
|
169
|
+
const view = picker({ syntaxHighlighting: false, reducedMotion: true });
|
|
170
|
+
const { lastFrame } = render(<ThemePicker picker={view} />);
|
|
171
|
+
await tick();
|
|
172
|
+
|
|
173
|
+
expect(lastFrame() ?? '').toContain('syntax off · motion off');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('admits a motion pin, so the row cannot read `motion on` on a visibly still run', () => {
|
|
177
|
+
const frame =
|
|
178
|
+
render(
|
|
179
|
+
<ThemePicker
|
|
180
|
+
picker={picker({
|
|
181
|
+
reducedMotion: false,
|
|
182
|
+
reducedMotionPin: { tier: 'flag', reducedMotion: true },
|
|
183
|
+
})}
|
|
184
|
+
/>,
|
|
185
|
+
).lastFrame() ?? '';
|
|
186
|
+
|
|
187
|
+
// The saved choice and what THIS RUN does are different facts and are shown as two. A tier
|
|
188
|
+
// alone cannot say which way it went — `--no-reduced-motion` is an override that pins the
|
|
189
|
+
// opposite value — so rendering the saved value beside the tier is how the row ends up reading
|
|
190
|
+
// `motion on (pinned by flag)` on a visibly still run.
|
|
191
|
+
expect(frame).toContain('motion on (this run: motion off, pinned by flag)');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('shows the opposite pin the same way, so the two runs are told apart', () => {
|
|
195
|
+
const frame =
|
|
196
|
+
render(
|
|
197
|
+
<ThemePicker
|
|
198
|
+
picker={picker({
|
|
199
|
+
reducedMotion: true,
|
|
200
|
+
reducedMotionPin: { tier: 'environment', reducedMotion: false },
|
|
201
|
+
})}
|
|
202
|
+
/>,
|
|
203
|
+
).lastFrame() ?? '';
|
|
204
|
+
|
|
205
|
+
expect(frame).toContain('motion off (this run: motion on, pinned by environment)');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('says previews cannot show when colour is off, rather than looking broken', () => {
|
|
209
|
+
gateMock.value = false;
|
|
210
|
+
|
|
211
|
+
const frame = render(<ThemePicker picker={picker()} />).lastFrame() ?? '';
|
|
212
|
+
|
|
213
|
+
expect(frame).toContain('Colour is off for this terminal');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('renders the empty state rather than an empty box', () => {
|
|
217
|
+
const frame = render(<ThemePicker picker={picker({ themes: [] })} />).lastFrame() ?? '';
|
|
218
|
+
|
|
219
|
+
expect(frame).toContain('No themes installed');
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe('the theme picker in screen-reader mode (SCREEN-2002 TC-10)', () => {
|
|
224
|
+
function renderInMode(view: IAppThemePickerViewModel): ReturnType<typeof render> {
|
|
225
|
+
return render(
|
|
226
|
+
<ScreenReaderProvider enabled>
|
|
227
|
+
<ThemePicker picker={view} />
|
|
228
|
+
</ScreenReaderProvider>,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
it('numbers the rows and prompts for a typed answer instead of drawing a highlight', () => {
|
|
233
|
+
const frame = renderInMode(picker()).lastFrame() ?? '';
|
|
234
|
+
|
|
235
|
+
expect(frame).toContain('1.');
|
|
236
|
+
expect(frame).toContain('4.');
|
|
237
|
+
// No box-drawing chrome in the mode.
|
|
238
|
+
expect(frame).not.toMatch(/[─-╿]/u);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('applies on SELECT and never previews per keystroke — there is no highlight to hear', async () => {
|
|
242
|
+
const view = picker();
|
|
243
|
+
const { stdin } = renderInMode(view);
|
|
244
|
+
await tick();
|
|
245
|
+
|
|
246
|
+
stdin.write('2');
|
|
247
|
+
await tick();
|
|
248
|
+
stdin.write(ENTER);
|
|
249
|
+
await tick();
|
|
250
|
+
|
|
251
|
+
expect(view.select).toHaveBeenCalledWith('light', {
|
|
252
|
+
syntaxHighlighting: true,
|
|
253
|
+
reducedMotion: false,
|
|
254
|
+
});
|
|
255
|
+
expect(view.preview).not.toHaveBeenCalled();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('cancels on the mode s own cancel path', async () => {
|
|
259
|
+
const view = picker();
|
|
260
|
+
const { stdin } = renderInMode(view);
|
|
261
|
+
await tick();
|
|
262
|
+
|
|
263
|
+
stdin.write(ESCAPE);
|
|
264
|
+
await tick();
|
|
265
|
+
|
|
266
|
+
expect(view.cancel).toHaveBeenCalledTimes(1);
|
|
267
|
+
expect(view.select).not.toHaveBeenCalled();
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe('the theme picker over files it refused (SCREEN-2002 TC-11)', () => {
|
|
272
|
+
const skipped = [
|
|
273
|
+
{ id: 'custom:broken', fileName: 'broken.json', reason: '$.overrides.colors.text.accent: bad' },
|
|
274
|
+
];
|
|
275
|
+
|
|
276
|
+
it('shows a refused file as a row that carries its reason and cannot be chosen', async () => {
|
|
277
|
+
const view = picker({ skipped });
|
|
278
|
+
const { lastFrame, stdin } = render(<ThemePicker picker={view} />);
|
|
279
|
+
const frame = lastFrame() ?? '';
|
|
280
|
+
|
|
281
|
+
expect(frame).toContain('broken.json');
|
|
282
|
+
expect(frame).toContain('$.overrides.colors.text.accent: bad');
|
|
283
|
+
|
|
284
|
+
// Walking past the end of the applicable rows wraps to the first one; it never lands on a file
|
|
285
|
+
// the run has already refused, because selecting it could only fail.
|
|
286
|
+
await tick();
|
|
287
|
+
for (let index = 0; index <= listBuiltInThemes().length; index += 1) {
|
|
288
|
+
stdin.write(ARROW_DOWN);
|
|
289
|
+
await tick();
|
|
290
|
+
}
|
|
291
|
+
stdin.write(ENTER);
|
|
292
|
+
await tick();
|
|
293
|
+
|
|
294
|
+
expect(view.select).toHaveBeenCalledTimes(1);
|
|
295
|
+
const [selectedId] = (view.select as ReturnType<typeof vi.fn>).mock.calls[0] ?? [];
|
|
296
|
+
expect(listBuiltInThemes().map((theme) => theme.id)).toContain(selectedId);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('says nothing about skipped files when there are none', () => {
|
|
300
|
+
expect(render(<ThemePicker picker={picker()} />).lastFrame() ?? '').not.toContain('Skipped');
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('numbers only the applicable rows in screen-reader mode', async () => {
|
|
304
|
+
const view = picker({ skipped });
|
|
305
|
+
const { lastFrame, stdin } = render(
|
|
306
|
+
<ScreenReaderProvider enabled>
|
|
307
|
+
<ThemePicker picker={view} />
|
|
308
|
+
</ScreenReaderProvider>,
|
|
309
|
+
);
|
|
310
|
+
const frame = lastFrame() ?? '';
|
|
311
|
+
|
|
312
|
+
expect(frame).toContain('broken.json');
|
|
313
|
+
expect(frame).not.toMatch(/5\.\s*Skipped/u);
|
|
314
|
+
|
|
315
|
+
await tick();
|
|
316
|
+
stdin.write('5');
|
|
317
|
+
await tick();
|
|
318
|
+
stdin.write(ENTER);
|
|
319
|
+
await tick();
|
|
320
|
+
|
|
321
|
+
expect(view.select).not.toHaveBeenCalled();
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
describe('the toggles row names a route the mode can take (SCREEN-2002 TC-11)', () => {
|
|
326
|
+
it('names the commands in screen-reader mode, where `s` and `m` are not bound', () => {
|
|
327
|
+
const frame =
|
|
328
|
+
render(
|
|
329
|
+
<ScreenReaderProvider enabled>
|
|
330
|
+
<ThemePicker picker={picker()} />
|
|
331
|
+
</ScreenReaderProvider>,
|
|
332
|
+
).lastFrame() ?? '';
|
|
333
|
+
|
|
334
|
+
expect(frame).toContain('syntax on · motion on');
|
|
335
|
+
expect(frame).toContain('/theme syntax on|off');
|
|
336
|
+
expect(frame).toContain('/theme motion on|off');
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('does NOT name them in sighted mode, where the keys are bound and hinted', () => {
|
|
340
|
+
const frame = render(<ThemePicker picker={picker()} />).lastFrame() ?? '';
|
|
341
|
+
|
|
342
|
+
expect(frame).toContain('syntax on · motion on');
|
|
343
|
+
expect(frame).not.toContain('/theme syntax');
|
|
344
|
+
});
|
|
345
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as tui from '../index.js';
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
describe('TUI presentation boundary (ARCH-011)', () => {
|
|
6
|
+
it('exports presentation hosts without claiming borrowed-session transport conformance', () => {
|
|
7
|
+
expect(tui).not.toHaveProperty('TuiTransport');
|
|
8
|
+
expect(tui).toHaveProperty('renderApp');
|
|
9
|
+
expect(tui).toHaveProperty('createDefaultTuiCliAdapter');
|
|
10
|
+
});
|
|
11
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expectTypeOf, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
ITuiAppChannelPort,
|
|
5
|
+
ITuiCommandQueryPort,
|
|
6
|
+
ITuiSessionUiEventPort,
|
|
7
|
+
} from '../tui-app-channel-port.js';
|
|
8
|
+
|
|
9
|
+
describe('TUI React capability boundary', () => {
|
|
10
|
+
it('excludes concrete channel and state-manager escape hatches', () => {
|
|
11
|
+
expectTypeOf<ITuiAppChannelPort>().not.toHaveProperty('getSession');
|
|
12
|
+
expectTypeOf<ITuiAppChannelPort>().not.toHaveProperty('getRegistry');
|
|
13
|
+
expectTypeOf<ITuiAppChannelPort>().not.toHaveProperty('stateManager');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('limits autocomplete to command queries', () => {
|
|
17
|
+
expectTypeOf<ITuiCommandQueryPort>().toHaveProperty('getCommands');
|
|
18
|
+
expectTypeOf<ITuiCommandQueryPort>().toHaveProperty('getSubcommands');
|
|
19
|
+
expectTypeOf<ITuiCommandQueryPort>().not.toHaveProperty('addModule');
|
|
20
|
+
expectTypeOf<ITuiCommandQueryPort>().not.toHaveProperty('execute');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('limits React event effects to the two UI event names', () => {
|
|
24
|
+
expectTypeOf<Parameters<ITuiSessionUiEventPort['on']>[0]>().toEqualTypeOf<
|
|
25
|
+
'ui_intent' | 'session_renamed'
|
|
26
|
+
>();
|
|
27
|
+
expectTypeOf<Parameters<ITuiSessionUiEventPort['off']>[0]>().toEqualTypeOf<
|
|
28
|
+
'ui_intent' | 'session_renamed'
|
|
29
|
+
>();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import type { IAIProvider } from '@robota-sdk/agent-core';
|
|
7
|
+
|
|
8
|
+
vi.mock('@robota-sdk/agent-framework', async (importOriginal) => {
|
|
9
|
+
const mod = await importOriginal<typeof import('@robota-sdk/agent-framework')>();
|
|
10
|
+
|
|
11
|
+
class FakeInteractiveSession {
|
|
12
|
+
on(): void {}
|
|
13
|
+
off(): void {}
|
|
14
|
+
getFullHistory(): unknown[] {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
getContextState(): never {
|
|
18
|
+
throw new Error('ENOENT: session store unreadable');
|
|
19
|
+
}
|
|
20
|
+
getName(): string | undefined {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
getSession(): { getSessionId: () => string } {
|
|
24
|
+
return { getSessionId: () => 'test-session' };
|
|
25
|
+
}
|
|
26
|
+
async shutdown(): Promise<void> {}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// RUNTIME-001: construction flows through buildRuntimeSession (wraps InteractiveSession in agent-framework).
|
|
30
|
+
return {
|
|
31
|
+
...mod,
|
|
32
|
+
InteractiveSession: FakeInteractiveSession,
|
|
33
|
+
buildRuntimeSession: () => new FakeInteractiveSession(),
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
import { TuiInteractionChannel } from '../TuiInteractionChannel.js';
|
|
38
|
+
|
|
39
|
+
/** The session's working directory: private to this run, never a fixed name under /tmp. */
|
|
40
|
+
const PROJECT_DIR = mkdtempSync(join(tmpdir(), 'agent-test-tui-init-failure-'));
|
|
41
|
+
afterAll(() => rmSync(PROJECT_DIR, { recursive: true, force: true }));
|
|
42
|
+
|
|
43
|
+
describe('TuiInteractionChannel init failure surfacing', () => {
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
vi.useFakeTimers();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
vi.useRealTimers();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('TC-05: a real init error records a session-init-error entry and sets error state', async () => {
|
|
53
|
+
const channel = new TuiInteractionChannel({
|
|
54
|
+
cwd: PROJECT_DIR,
|
|
55
|
+
provider: {} as IAIProvider,
|
|
56
|
+
});
|
|
57
|
+
await channel.start();
|
|
58
|
+
|
|
59
|
+
vi.advanceTimersByTime(400);
|
|
60
|
+
|
|
61
|
+
const entries = channel.stateManager.history;
|
|
62
|
+
const initError = entries.find((e) => e.type === 'session-init-error');
|
|
63
|
+
expect(initError).toBeDefined();
|
|
64
|
+
const message = (initError?.data as { message?: string } | undefined)?.message ?? '';
|
|
65
|
+
expect(message).toContain('Session initialization failed');
|
|
66
|
+
expect(message).toContain('ENOENT');
|
|
67
|
+
|
|
68
|
+
await channel.stop();
|
|
69
|
+
});
|
|
70
|
+
});
|