@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,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCREEN-006 anti-drift consistency floor — the mechanical floor for the color-token SSOT.
|
|
3
|
+
*
|
|
4
|
+
* Every component must consume the resolved theme (`usePalette()` / `useMotionTokens()`, whose data
|
|
5
|
+
* lives in `src/theme/`) instead of spelling Ink color names or hex values inline. This scan reads
|
|
6
|
+
* every source file in the package (excluding `src/theme/` itself and tests) and fails on any
|
|
7
|
+
* `color="…"` / `borderColor="…"` / `backgroundColor="…"` JSX string literal and on any
|
|
8
|
+
* `#rrggbb` hex literal. Precedent: `key-hint-consistency.test.tsx` (the SCREEN-005 floor).
|
|
9
|
+
*
|
|
10
|
+
* SCREEN-2002 added the two ratchets the SCREEN-006 limit predicted. The deferred case — "a future
|
|
11
|
+
* TS helper returning a bare color-name string" — recurred twice (`getContextColor`, the status
|
|
12
|
+
* glyph's colour), so the floor now also refuses any file outside `src/theme/` that names the
|
|
13
|
+
* built-in theme data or calls a chalk COLOUR function. `chalk.inverse` (a modifier, the
|
|
14
|
+
* drawn cursor) and `chalk.level` (the colour gate) are deliberately untouched: they choose no colour.
|
|
15
|
+
*
|
|
16
|
+
* What the floor still cannot see: a colour-name string returned from a helper that neither imports
|
|
17
|
+
* the theme data nor calls chalk — e.g. a literal `'cyan'` returned by a pure function. The type
|
|
18
|
+
* system covers the tokens it flows into; this limit is recorded rather than claimed away.
|
|
19
|
+
*/
|
|
20
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
21
|
+
import { join, relative } from 'node:path';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
|
|
24
|
+
import { describe, expect, it } from 'vitest';
|
|
25
|
+
|
|
26
|
+
const SRC_ROOT = fileURLToPath(new URL('..', import.meta.url));
|
|
27
|
+
|
|
28
|
+
/** `src/theme/` holds the token data and the one chalk boundary; nothing else may. */
|
|
29
|
+
const THEME_DIR = 'theme';
|
|
30
|
+
const EXCLUDED_DIRS = new Set(['__tests__']);
|
|
31
|
+
const SOURCE_EXTENSIONS = ['.ts', '.tsx'];
|
|
32
|
+
|
|
33
|
+
const COLOR_ATTR_LITERAL = /\b(?:color|borderColor|backgroundColor)="[^"]*"/g;
|
|
34
|
+
const HEX_LITERAL = /#[0-9a-fA-F]{6}\b/g;
|
|
35
|
+
/**
|
|
36
|
+
* SCREEN-2002: the built-in colour DATA may be imported only from inside `src/theme/`. The SYMBOL is
|
|
37
|
+
* what the ratchet matches, not the module path — matching the path alone let the `src/theme/index.js`
|
|
38
|
+
* barrel re-export the data straight past the floor, which is exactly the route every migrated
|
|
39
|
+
* component already imports through.
|
|
40
|
+
*
|
|
41
|
+
* Recorded limit (widened by SCREEN-2002 work unit 3): the package now EXPORTS `listBuiltInThemes`,
|
|
42
|
+
* so a caller can obtain a built-in's colour values through the accessor without naming a constant
|
|
43
|
+
* or the data module — the floor does not see that, exactly as it does not see a colour-name string
|
|
44
|
+
* returned by a pure helper. The accessor exists for one composition root, which puts the built-ins
|
|
45
|
+
* in a registry; reading a colour off it is the case this floor cannot catch, and it is written down
|
|
46
|
+
* rather than claimed away.
|
|
47
|
+
*
|
|
48
|
+
* Recorded limit: a bare word matches anywhere, so PROSE naming a theme constant in a comment
|
|
49
|
+
* outside `src/theme/` fails the floor too. That is the strict direction — it fails loudly and is
|
|
50
|
+
* rewritten in a sentence, rather than passing silently — so it is left as is.
|
|
51
|
+
*/
|
|
52
|
+
const BUILT_IN_THEME_IMPORT =
|
|
53
|
+
/\b(?:BUILT_IN_THEMES|DARK_THEME|LIGHT_THEME|DARK_DALTONIZED_THEME|LIGHT_DALTONIZED_THEME)\b|from\s+'[^']*built-in-themes\.js'/g;
|
|
54
|
+
/** SCREEN-2002: a chalk COLOUR call outside `src/theme/` decides a colour the theme should own. */
|
|
55
|
+
const CHALK_COLOR_CALL = /\bchalk\.(?!inverse\b|level\b)[A-Za-z]+[.(]/g;
|
|
56
|
+
/** SCREEN-2002: the three inputs `<ThemeProvider>` must be fed from the resolved view model. */
|
|
57
|
+
const THEME_PROVIDER_PROPS = ['theme', 'reducedMotion', 'syntaxHighlighting'] as const;
|
|
58
|
+
|
|
59
|
+
interface IFinding {
|
|
60
|
+
file: string;
|
|
61
|
+
line: number;
|
|
62
|
+
match: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function collectSourceFiles(dir: string): string[] {
|
|
66
|
+
const files: string[] = [];
|
|
67
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
68
|
+
if (entry.isDirectory()) {
|
|
69
|
+
if (EXCLUDED_DIRS.has(entry.name)) continue;
|
|
70
|
+
files.push(...collectSourceFiles(join(dir, entry.name)));
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (!SOURCE_EXTENSIONS.some((ext) => entry.name.endsWith(ext))) continue;
|
|
74
|
+
files.push(join(dir, entry.name));
|
|
75
|
+
}
|
|
76
|
+
return files;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Blank out comment CONTENT before scanning, keeping every newline and every character position so
|
|
81
|
+
* a finding's line number still points at the real line.
|
|
82
|
+
*
|
|
83
|
+
* The floor is about CODE. Matching a bare symbol anywhere was recorded as a known limit when these
|
|
84
|
+
* ratchets were written — "it fails loudly rather than passing silently, so leave it" — and then it
|
|
85
|
+
* failed on a comment that correctly EXPLAINS the rule it was named in (`useMotion()` in a docblock
|
|
86
|
+
* about the motion gate). A guard that fires on correct work is how a guard stops being read, so
|
|
87
|
+
* the limit is closed rather than re-recorded.
|
|
88
|
+
*/
|
|
89
|
+
function blankComments(source: string): string {
|
|
90
|
+
const blank = (match: string): string => match.replace(/[^\n]/gu, ' ');
|
|
91
|
+
return source
|
|
92
|
+
.replace(/\/\*[\s\S]*?\*\//gu, blank)
|
|
93
|
+
.replace(
|
|
94
|
+
/(^|[^:])\/\/[^\n]*/gu,
|
|
95
|
+
(match, prefix: string) => prefix + blank(match.slice(prefix.length)),
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function scanFile(filePath: string, pattern: RegExp): IFinding[] {
|
|
100
|
+
const findings: IFinding[] = [];
|
|
101
|
+
const lines = blankComments(readFileSync(filePath, 'utf8')).split('\n');
|
|
102
|
+
lines.forEach((line, index) => {
|
|
103
|
+
for (const match of line.matchAll(pattern)) {
|
|
104
|
+
findings.push({
|
|
105
|
+
file: relative(SRC_ROOT, filePath),
|
|
106
|
+
line: index + 1,
|
|
107
|
+
match: match[0],
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
return findings;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function formatFindings(findings: IFinding[]): string {
|
|
115
|
+
return findings.map((f) => ` ${f.file}:${f.line} ${f.match}`).join('\n');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
describe('SCREEN-006 palette consistency floor', () => {
|
|
119
|
+
const allFiles = collectSourceFiles(SRC_ROOT);
|
|
120
|
+
// `src/theme/` is the one place a colour value or a chalk colour call may appear.
|
|
121
|
+
const sourceFiles = allFiles.filter(
|
|
122
|
+
(file) => !relative(SRC_ROOT, file).split('/').includes(THEME_DIR),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
it('scans a non-empty source inventory', () => {
|
|
126
|
+
expect(sourceFiles.length).toBeGreaterThan(0);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('has zero color/borderColor/backgroundColor JSX string literals outside the token modules', () => {
|
|
130
|
+
const findings = sourceFiles.flatMap((file) => scanFile(file, COLOR_ATTR_LITERAL));
|
|
131
|
+
expect(
|
|
132
|
+
findings,
|
|
133
|
+
`Inline color attribute literals found — read the theme with usePalette():\n${formatFindings(findings)}`,
|
|
134
|
+
).toEqual([]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('has zero #rrggbb hex literals outside the token modules', () => {
|
|
138
|
+
const findings = sourceFiles.flatMap((file) => scanFile(file, HEX_LITERAL));
|
|
139
|
+
expect(
|
|
140
|
+
findings,
|
|
141
|
+
`Inline hex color literals found — put the value in src/theme/built-in-themes.ts:\n${formatFindings(findings)}`,
|
|
142
|
+
).toEqual([]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('SCREEN-2002: only src/theme/ imports the built-in theme data', () => {
|
|
146
|
+
const findings = sourceFiles.flatMap((file) => scanFile(file, BUILT_IN_THEME_IMPORT));
|
|
147
|
+
expect(
|
|
148
|
+
findings,
|
|
149
|
+
`The dark theme's values are data, not a palette — read the live theme instead:\n${formatFindings(findings)}`,
|
|
150
|
+
).toEqual([]);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('SCREEN-2002: only src/theme/ calls a chalk colour', () => {
|
|
154
|
+
const findings = sourceFiles.flatMap((file) => scanFile(file, CHALK_COLOR_CALL));
|
|
155
|
+
expect(
|
|
156
|
+
findings,
|
|
157
|
+
`A chalk colour call decides a colour the theme owns — go through src/theme/:\n${formatFindings(findings)}`,
|
|
158
|
+
).toEqual([]);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* SCREEN-2002 TC-06 carve-out, made mechanical. The motion gate has exactly ONE consumer, and the
|
|
163
|
+
* carve-outs depend on that: the sleeping-task countdown still advances under reduced motion
|
|
164
|
+
* (it reports a fact, it is not decoration) and `StreamingIndicator`'s collapse follows
|
|
165
|
+
* screen-reader mode alone. A second consumer would quietly widen what the setting turns off.
|
|
166
|
+
*/
|
|
167
|
+
it('SCREEN-2002: the motion gate has one consumer', () => {
|
|
168
|
+
const consumers = sourceFiles.filter((file) =>
|
|
169
|
+
/\buseMotion\(\)/u.test(blankComments(readFileSync(file, 'utf8'))),
|
|
170
|
+
);
|
|
171
|
+
expect(consumers.map((file) => relative(SRC_ROOT, file)).sort()).toEqual(['WaveText.tsx']);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* SCREEN-2002: the provider's three inputs must all be WIRED, and every markdown render site must
|
|
176
|
+
* READ the one that is not a colour.
|
|
177
|
+
*
|
|
178
|
+
* `/theme syntax off` shipped in this package's first draft persisting a setting that reached no
|
|
179
|
+
* render site — the command reported success for a no-op, and every context-level test still
|
|
180
|
+
* passed, because the context was fine; it was the one line feeding it that was missing.
|
|
181
|
+
*
|
|
182
|
+
* The first version of this floor was itself half-real: it matched `prop={viewModel.theme.` on the
|
|
183
|
+
* RAW file, so a commented-out provider stayed green and so did moving all three props onto a
|
|
184
|
+
* different element. Both halves below are anchored and comment-blind, and the fixture test at the
|
|
185
|
+
* end proves each one fires.
|
|
186
|
+
*/
|
|
187
|
+
const themeProviderElement = (source: string): string =>
|
|
188
|
+
/<ThemeProvider\b[\s\S]*?>/u.exec(blankComments(source))?.[0] ?? '';
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* The consumer end, as a PROPERTY rather than a proxy for one.
|
|
192
|
+
*
|
|
193
|
+
* `/theme syntax off` shipped persisting a setting no render site passed, so the command reported
|
|
194
|
+
* success and changed nothing. Four guards were written for it, and each measured something
|
|
195
|
+
* standing in for "every call site passes the setting" — the prop appears in the file, the file
|
|
196
|
+
* matched the regex, the identifier occurs N times. Each held for exactly one review round,
|
|
197
|
+
* because a closer proxy is still a proxy.
|
|
198
|
+
*
|
|
199
|
+
* So the obligation is gone instead: `useRenderMarkdown()` binds the theme, the setting and
|
|
200
|
+
* screen-reader mode, and there is no per-call-site argument left to forget. What remains to
|
|
201
|
+
* check is that nothing goes around it — the single-consumer shape the motion gate already uses
|
|
202
|
+
* below.
|
|
203
|
+
*/
|
|
204
|
+
const RENDER_MARKDOWN_OWNER = 'hooks/useRenderMarkdown.ts';
|
|
205
|
+
const RAW_RENDERER_SPECIFIER = /(?:from|import\()\s*'[^']*render-markdown\.js'/u;
|
|
206
|
+
/**
|
|
207
|
+
* `from` AND `import(` — a dynamic import reaches the same function, and a check whose whole job
|
|
208
|
+
* is that there is no way around it must see one. A TYPE-only import is exempt: it reaches no
|
|
209
|
+
* call, and a guard that fires on correct work is how a guard stops being read.
|
|
210
|
+
*/
|
|
211
|
+
const importsRawRenderer = (source: string): boolean =>
|
|
212
|
+
source
|
|
213
|
+
.split('\n')
|
|
214
|
+
.some(
|
|
215
|
+
(line) =>
|
|
216
|
+
RAW_RENDERER_SPECIFIER.test(line) && !/^\s*(?:import|export)\s+type\b/u.test(line),
|
|
217
|
+
);
|
|
218
|
+
const RAW_RENDERER_IMPORT = /(?<!import type )(?:from|import\()\s*'[^']*render-markdown\.js'/u;
|
|
219
|
+
|
|
220
|
+
it('SCREEN-2002: AppView feeds the theme provider all three resolved inputs', () => {
|
|
221
|
+
const element = themeProviderElement(readFileSync(join(SRC_ROOT, 'AppView.tsx'), 'utf8'));
|
|
222
|
+
|
|
223
|
+
expect(element, 'AppView must render a <ThemeProvider> — it is the only live one').not.toBe('');
|
|
224
|
+
for (const prop of THEME_PROVIDER_PROPS) {
|
|
225
|
+
expect(
|
|
226
|
+
new RegExp(`${prop}=\\{viewModel\\.theme\\.`, 'u').test(element),
|
|
227
|
+
`<ThemeProvider> must be passed ${prop} — the resolved value reaches components only here`,
|
|
228
|
+
).toBe(true);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('SCREEN-2002: the markdown renderer has one consumer, which binds the appearance', () => {
|
|
233
|
+
const importers = sourceFiles
|
|
234
|
+
.map((file) => ({
|
|
235
|
+
file: relative(SRC_ROOT, file),
|
|
236
|
+
source: blankComments(readFileSync(file, 'utf8')),
|
|
237
|
+
}))
|
|
238
|
+
.filter(({ file }) => file !== 'render-markdown.ts')
|
|
239
|
+
// `from` AND `import(` — a dynamic import is a way around a check whose whole job is that
|
|
240
|
+
// there is no way around it. A TYPE-only import is exempt: it reaches no call, and a guard
|
|
241
|
+
// that fires on correct work is how a guard stops being read (the reason `blankComments`
|
|
242
|
+
// exists a few lines up).
|
|
243
|
+
.filter(({ source }) => importsRawRenderer(source))
|
|
244
|
+
.map(({ file }) => file)
|
|
245
|
+
.sort();
|
|
246
|
+
|
|
247
|
+
expect(
|
|
248
|
+
importers,
|
|
249
|
+
`Only ${RENDER_MARKDOWN_OWNER} may import the raw renderer — everything else goes through ` +
|
|
250
|
+
`\`useRenderMarkdown()\`, which binds the theme, the syntax-highlighting setting and ` +
|
|
251
|
+
`screen-reader mode. A direct import is a call site that can forget one of them.`,
|
|
252
|
+
).toEqual([RENDER_MARKDOWN_OWNER]);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('the wiring floors fire on a violating fixture', () => {
|
|
256
|
+
// (a) A commented-out provider is not a provider.
|
|
257
|
+
expect(themeProviderElement('// <ThemeProvider theme={viewModel.theme.resolved}>')).toBe('');
|
|
258
|
+
expect(themeProviderElement('/* <ThemeProvider theme={viewModel.theme.resolved}> */')).toBe('');
|
|
259
|
+
// (b) The props must be on the PROVIDER, not on whatever element happens to follow it.
|
|
260
|
+
const moved = [
|
|
261
|
+
'<ThemeProvider>',
|
|
262
|
+
' <AppPresentation theme={viewModel.theme.resolved} />',
|
|
263
|
+
'</ThemeProvider>',
|
|
264
|
+
].join('\n');
|
|
265
|
+
expect(/theme=\{viewModel\.theme\./u.test(themeProviderElement(moved))).toBe(false);
|
|
266
|
+
// (c) The real shape passes.
|
|
267
|
+
const wired = [
|
|
268
|
+
'<ThemeProvider',
|
|
269
|
+
' theme={viewModel.theme.resolved}',
|
|
270
|
+
' reducedMotion={viewModel.theme.reducedMotion}',
|
|
271
|
+
' syntaxHighlighting={viewModel.theme.syntaxHighlighting}',
|
|
272
|
+
'>',
|
|
273
|
+
].join('\n');
|
|
274
|
+
const element = themeProviderElement(wired);
|
|
275
|
+
for (const prop of THEME_PROVIDER_PROPS) {
|
|
276
|
+
expect(new RegExp(`${prop}=\\{viewModel\\.theme\\.`, 'u').test(element)).toBe(true);
|
|
277
|
+
}
|
|
278
|
+
// (d) The consumer half is an IMPORT check, so it cannot be satisfied by an identifier that
|
|
279
|
+
// merely occurs — a dependency array, a destructure, a sibling component's props.
|
|
280
|
+
const shapes: [string, boolean][] = [
|
|
281
|
+
["import { renderMarkdown } from './render-markdown.js';", true],
|
|
282
|
+
["import * as md from './render-markdown.js';", true],
|
|
283
|
+
["import { renderMarkdown } from '../../render-markdown.js';", true],
|
|
284
|
+
["export { renderMarkdown } from './render-markdown.js';", true],
|
|
285
|
+
// A dynamic import reaches the same function; the check must see it.
|
|
286
|
+
["const { renderMarkdown } = await import('./render-markdown.js');", true],
|
|
287
|
+
// A TYPE-only import reaches no call, so flagging it would be a false red.
|
|
288
|
+
["import type { IRenderMarkdownOptions } from './render-markdown.js';", false],
|
|
289
|
+
["import { useRenderMarkdown } from './hooks/useRenderMarkdown.js';", false],
|
|
290
|
+
];
|
|
291
|
+
for (const [shape, flagged] of shapes) {
|
|
292
|
+
expect(importsRawRenderer(shape), shape).toBe(flagged);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('both ratchets actually fire on a violating fixture', () => {
|
|
297
|
+
const violation = [
|
|
298
|
+
"import { DARK_THEME } from './theme/built-in-themes.js';",
|
|
299
|
+
"const label = chalk.cyan('x');",
|
|
300
|
+
].join('\n');
|
|
301
|
+
// Two matches: the symbol and the path. Either alone is a violation.
|
|
302
|
+
expect([...violation.matchAll(BUILT_IN_THEME_IMPORT)]).toHaveLength(2);
|
|
303
|
+
expect([...violation.matchAll(CHALK_COLOR_CALL)]).toHaveLength(1);
|
|
304
|
+
// The route the path-only ratchet could not see: the barrel re-exporting the same data.
|
|
305
|
+
const throughTheBarrel = "import { DARK_THEME } from './theme/index.js';";
|
|
306
|
+
expect([...throughTheBarrel.matchAll(BUILT_IN_THEME_IMPORT)]).toHaveLength(1);
|
|
307
|
+
// The two deliberate exemptions — and only those two — stay green.
|
|
308
|
+
expect([...'chalk.inverse(c) + (chalk.level = 0)'.matchAll(CHALK_COLOR_CALL)]).toHaveLength(0);
|
|
309
|
+
// `chalk.reset` is NOT among them: it clears whatever style the caller established, which is a
|
|
310
|
+
// rendering decision, not a colour-free modifier.
|
|
311
|
+
expect([...'chalk.reset(row)'.matchAll(CHALK_COLOR_CALL)]).toHaveLength(1);
|
|
312
|
+
// `resolveTheme` is the sanctioned way to ask for a theme one did not receive.
|
|
313
|
+
expect([...'resolveTheme(options.theme)'.matchAll(BUILT_IN_THEME_IMPORT)]).toHaveLength(0);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('reads CODE, not the comments that explain it', () => {
|
|
317
|
+
const explained = [
|
|
318
|
+
'// `useMotion()` gates on the colour gate above this.',
|
|
319
|
+
'/* DARK_THEME is the default; see built-in-themes.ts. */',
|
|
320
|
+
'const label = value; // chalk.cyan(x) would be a violation here',
|
|
321
|
+
].join('\n');
|
|
322
|
+
const code = blankComments(explained);
|
|
323
|
+
|
|
324
|
+
expect([...code.matchAll(BUILT_IN_THEME_IMPORT)]).toHaveLength(0);
|
|
325
|
+
expect([...code.matchAll(CHALK_COLOR_CALL)]).toHaveLength(0);
|
|
326
|
+
expect(/\buseMotion\(\)/u.test(code)).toBe(false);
|
|
327
|
+
// Line positions survive, so a finding still points at the line it came from.
|
|
328
|
+
expect(code.split('\n')).toHaveLength(3);
|
|
329
|
+
});
|
|
330
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from 'ink-testing-library';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import PermissionPrompt from '../PermissionPrompt.js';
|
|
6
|
+
|
|
7
|
+
describe('project permission availability in the terminal', () => {
|
|
8
|
+
it('labels the project choice unavailable before selection', () => {
|
|
9
|
+
const frame = render(
|
|
10
|
+
<PermissionPrompt
|
|
11
|
+
request={{
|
|
12
|
+
toolName: 'Bash',
|
|
13
|
+
toolArgs: { command: 'git status' },
|
|
14
|
+
canPersistProjectPermission: false,
|
|
15
|
+
resolve: () => {},
|
|
16
|
+
}}
|
|
17
|
+
/>,
|
|
18
|
+
).lastFrame();
|
|
19
|
+
|
|
20
|
+
expect(frame).toContain('Project-wide approval unavailable');
|
|
21
|
+
expect(frame).not.toContain('always (this project)');
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from 'ink-testing-library';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import PermissionPrompt from '../PermissionPrompt.js';
|
|
6
|
+
|
|
7
|
+
describe('a permission asked for in a peer turn', () => {
|
|
8
|
+
it('names the session whose message the call is serving, and shows the full reply', () => {
|
|
9
|
+
const text = 'The project is called Lumen and its token budget is 4096.';
|
|
10
|
+
const frame = render(
|
|
11
|
+
<PermissionPrompt
|
|
12
|
+
request={{
|
|
13
|
+
toolName: 'peer_reply',
|
|
14
|
+
toolArgs: { text },
|
|
15
|
+
requestedByPeer: 'peer:session-a',
|
|
16
|
+
resolve: () => {},
|
|
17
|
+
}}
|
|
18
|
+
/>,
|
|
19
|
+
).lastFrame();
|
|
20
|
+
|
|
21
|
+
expect(frame).toContain('peer:session-a');
|
|
22
|
+
expect(frame?.replace(/\s+/g, ' ')).toContain(text);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('says nothing about a peer for the operator’s own call', () => {
|
|
26
|
+
const frame = render(
|
|
27
|
+
<PermissionPrompt
|
|
28
|
+
request={{ toolName: 'Bash', toolArgs: { command: 'ls' }, resolve: () => {} }}
|
|
29
|
+
/>,
|
|
30
|
+
).lastFrame();
|
|
31
|
+
|
|
32
|
+
expect(frame).not.toContain('peer');
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from 'ink-testing-library';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { ProductDisplayNameProvider } from '../product-display-name-context.js';
|
|
6
|
+
import { RoleLabel } from '../RoleLabel.js';
|
|
7
|
+
import StreamingIndicator from '../StreamingIndicator.js';
|
|
8
|
+
import TransportTUI from '../TransportTUI.js';
|
|
9
|
+
import { useTerminalTitle } from '../use-terminal-title.js';
|
|
10
|
+
|
|
11
|
+
describe('host-selected terminal display name', () => {
|
|
12
|
+
it('uses the host name for history and streaming labels', () => {
|
|
13
|
+
const history = render(
|
|
14
|
+
<ProductDisplayNameProvider name="Atlas">
|
|
15
|
+
<RoleLabel role="assistant" />
|
|
16
|
+
</ProductDisplayNameProvider>,
|
|
17
|
+
);
|
|
18
|
+
const streaming = render(
|
|
19
|
+
<ProductDisplayNameProvider name="Atlas">
|
|
20
|
+
<StreamingIndicator text="Hello" activeTools={[]} />
|
|
21
|
+
</ProductDisplayNameProvider>,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
expect(history.lastFrame()).toContain('Atlas:');
|
|
25
|
+
expect(streaming.lastFrame()).toContain('Atlas:');
|
|
26
|
+
expect(history.lastFrame()).not.toContain('Robota:');
|
|
27
|
+
expect(streaming.lastFrame()).not.toContain('Robota:');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('uses a neutral name when the host does not choose one', () => {
|
|
31
|
+
const { lastFrame } = render(<RoleLabel role="assistant" />);
|
|
32
|
+
expect(lastFrame()).toContain('Assistant:');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('renders a projected command using the host-selected prefix', () => {
|
|
36
|
+
const { lastFrame } = render(
|
|
37
|
+
<ProductDisplayNameProvider name="Atlas" modelCommandToolPrefix="robota_command_">
|
|
38
|
+
<StreamingIndicator
|
|
39
|
+
text=""
|
|
40
|
+
activeTools={[{ toolName: 'robota_command_echo', firstArg: '', isRunning: true }]}
|
|
41
|
+
/>
|
|
42
|
+
</ProductDisplayNameProvider>,
|
|
43
|
+
);
|
|
44
|
+
expect(lastFrame()).toContain('echo(');
|
|
45
|
+
expect(lastFrame()).not.toContain('robota_command_echo');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('uses the host name in transport restart copy', () => {
|
|
49
|
+
const { lastFrame } = render(
|
|
50
|
+
<ProductDisplayNameProvider name="Atlas">
|
|
51
|
+
<TransportTUI
|
|
52
|
+
registry={{ getAll: () => [], setEnabled: vi.fn(), setOptions: vi.fn() }}
|
|
53
|
+
onClose={vi.fn()}
|
|
54
|
+
/>
|
|
55
|
+
</ProductDisplayNameProvider>,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
expect(lastFrame()).toContain('applies the next time Atlas starts');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('sanitizes the host name before writing the terminal title', () => {
|
|
62
|
+
const write = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
63
|
+
function Title(): null {
|
|
64
|
+
useTerminalTitle('session');
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const mounted = render(
|
|
69
|
+
<ProductDisplayNameProvider name={'Atlas\x07\x1b]52;c;evil\x07'}>
|
|
70
|
+
<Title />
|
|
71
|
+
</ProductDisplayNameProvider>,
|
|
72
|
+
);
|
|
73
|
+
expect(write).toHaveBeenCalledWith('\x1b]0;Atlas — session\x07');
|
|
74
|
+
mounted.unmount();
|
|
75
|
+
} finally {
|
|
76
|
+
write.mockRestore();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
});
|