@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,164 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { renderMarkdown } from '../render-markdown.js';
|
|
5
|
+
import { DARK_DALTONIZED_THEME, DARK_THEME } from '../theme/built-in-themes.js';
|
|
6
|
+
import { foreground } from '../theme/index.js';
|
|
7
|
+
|
|
8
|
+
const ANSI_LIGHT_RED = '\u001b[38;5;210m';
|
|
9
|
+
const ANSI_LIGHT_GREEN = '\u001b[38;5;120m';
|
|
10
|
+
const ANSI_DARK_RED_BACKGROUND = '\u001b[48;5;52m';
|
|
11
|
+
const ANSI_DARK_GREEN_BACKGROUND = '\u001b[48;5;22m';
|
|
12
|
+
// SCREEN-2002: the rows are styled through chalk now, which closes a background+foreground pair with
|
|
13
|
+
// its own paired resets instead of the blanket `ESC[0m` the hand-written escapes used. The colours
|
|
14
|
+
// and their order are unchanged; this is the one recorded byte difference.
|
|
15
|
+
const ANSI_RESET_FOREGROUND = '\u001b[39m';
|
|
16
|
+
const ANSI_RESET_BACKGROUND = '\u001b[49m';
|
|
17
|
+
const CODE_BLOCK_INDENT = ' ';
|
|
18
|
+
/** Any SGR introducer — a plain block must carry none. */
|
|
19
|
+
const SGR_ANY = '\u001b[';
|
|
20
|
+
|
|
21
|
+
describe('renderMarkdown', () => {
|
|
22
|
+
it('renders diff fenced code blocks with addition and removal colors', () => {
|
|
23
|
+
const output = renderMarkdown(
|
|
24
|
+
['Before', '', '```diff', '- const oldValue = true;', '+ const newValue = true;', '```'].join(
|
|
25
|
+
'\n',
|
|
26
|
+
),
|
|
27
|
+
{ color: true },
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
expect(output).toContain(`${ANSI_DARK_RED_BACKGROUND}${ANSI_LIGHT_RED}`);
|
|
31
|
+
expect(output).toContain(`${CODE_BLOCK_INDENT}- const oldValue = true;`);
|
|
32
|
+
expect(output).toContain(`${ANSI_DARK_GREEN_BACKGROUND}${ANSI_LIGHT_GREEN}`);
|
|
33
|
+
expect(output).toContain(`${CODE_BLOCK_INDENT}+ const newValue = true;`);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('pads added and removed diff rows before applying background colors', () => {
|
|
37
|
+
const codeBlockWidth = 24;
|
|
38
|
+
const removedRow = `${CODE_BLOCK_INDENT}- removed`.padEnd(codeBlockWidth);
|
|
39
|
+
const addedRow = `${CODE_BLOCK_INDENT}+ added`.padEnd(codeBlockWidth);
|
|
40
|
+
const output = renderMarkdown(['```diff', '- removed', '+ added', '```'].join('\n'), {
|
|
41
|
+
color: true,
|
|
42
|
+
codeBlockWidth,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(output).toContain(
|
|
46
|
+
`${ANSI_DARK_RED_BACKGROUND}${ANSI_LIGHT_RED}${removedRow}${ANSI_RESET_FOREGROUND}${ANSI_RESET_BACKGROUND}`,
|
|
47
|
+
);
|
|
48
|
+
expect(output).toContain(
|
|
49
|
+
`${ANSI_DARK_GREEN_BACKGROUND}${ANSI_LIGHT_GREEN}${addedRow}${ANSI_RESET_FOREGROUND}${ANSI_RESET_BACKGROUND}`,
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('keeps diff fenced code block content readable when color is disabled', () => {
|
|
54
|
+
const output = renderMarkdown(
|
|
55
|
+
['```diff', '- removed line', '+ added line', ' unchanged line', '```'].join('\n'),
|
|
56
|
+
{ color: false },
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
expect(output).toContain('- removed line');
|
|
60
|
+
expect(output).toContain('+ added line');
|
|
61
|
+
expect(output).toContain(' unchanged line');
|
|
62
|
+
expect(output).not.toContain(ANSI_LIGHT_RED);
|
|
63
|
+
expect(output).not.toContain(ANSI_LIGHT_GREEN);
|
|
64
|
+
expect(output).not.toContain(ANSI_DARK_RED_BACKGROUND);
|
|
65
|
+
expect(output).not.toContain(ANSI_DARK_GREEN_BACKGROUND);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('keeps regular fenced code blocks as code output', () => {
|
|
69
|
+
const output = renderMarkdown(['```ts', 'const value: string = "ok";', '```'].join('\n'), {
|
|
70
|
+
color: false,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(output).toContain('const value: string = "ok";');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* SCREEN-2002 TC-03 — the theme reaches `cli-highlight`.
|
|
78
|
+
*
|
|
79
|
+
* This is the unit's most dependency-fragile claim, and the failure it guards against is SILENT:
|
|
80
|
+
* `cli-highlight` falls back PER KEY to its own `DEFAULT_THEME`, whose `keyword` is red and whose
|
|
81
|
+
* `addition` is green. If the theme stopped reaching it, a daltonized run would quietly get the
|
|
82
|
+
* red/green pair back with every other surface still daltonized, and nothing else in the suite
|
|
83
|
+
* would notice.
|
|
84
|
+
*/
|
|
85
|
+
describe('SCREEN-2002 TC-03: syntax highlighting follows the theme', () => {
|
|
86
|
+
const TS_BLOCK = ['```ts', 'const value = 1;', '```'].join('\n');
|
|
87
|
+
const TRUECOLOR = 3;
|
|
88
|
+
const previousLevel = chalk.level;
|
|
89
|
+
afterEach(() => {
|
|
90
|
+
chalk.level = previousLevel;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const openCode = (color: string): string => foreground(color)('x').split('x')[0] ?? '';
|
|
94
|
+
|
|
95
|
+
it('colours a keyword with the theme s value, not cli-highlight s default', () => {
|
|
96
|
+
chalk.level = TRUECOLOR;
|
|
97
|
+
const daltonized = renderMarkdown(TS_BLOCK, {
|
|
98
|
+
color: true,
|
|
99
|
+
theme: DARK_DALTONIZED_THEME,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(daltonized).toContain(openCode(DARK_DALTONIZED_THEME.syntax.keyword));
|
|
103
|
+
// Recorded, because it bounds what this test can prove: `cli-highlight` depends on chalk 4 and
|
|
104
|
+
// resolves to its OWN instance, so `chalk.level` set here does not reach it and its per-key
|
|
105
|
+
// fallback emits nothing in-process — it is only coloured in a real terminal. The positive
|
|
106
|
+
// assertion above is therefore the whole guard; a `not.toContain` on the fallback's colour
|
|
107
|
+
// would be inert. The end-to-end colour is evidenced by the PTY scenario instead.
|
|
108
|
+
expect(DARK_THEME.syntax.keyword).not.toBe(DARK_DALTONIZED_THEME.syntax.keyword);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('renders a code block as plain indented text when highlighting is off', () => {
|
|
112
|
+
chalk.level = TRUECOLOR;
|
|
113
|
+
const plain = renderMarkdown(TS_BLOCK, {
|
|
114
|
+
color: true,
|
|
115
|
+
theme: DARK_DALTONIZED_THEME,
|
|
116
|
+
syntaxHighlighting: false,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(plain).toContain(`${CODE_BLOCK_INDENT}const value = 1;`);
|
|
120
|
+
expect(plain).not.toContain(openCode(DARK_DALTONIZED_THEME.syntax.keyword));
|
|
121
|
+
expect(plain).not.toContain(SGR_ANY);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* CLI-2004 TC-04 — the `Header: value` flattening, asserted at the single choke point every
|
|
127
|
+
* render path shares. A box-drawn grid is read aloud as its rules; the pairing is the content.
|
|
128
|
+
*/
|
|
129
|
+
describe('CLI-2004 TC-04: screen-reader table flattening', () => {
|
|
130
|
+
const TABLE = ['| A | B |', '|---|---|', '| 1 | 2 |'].join('\n');
|
|
131
|
+
const BOX_DRAWING = /[\u2502\u2500\u250c\u2510\u2514\u2518\u251c\u2524\u252c\u2534\u253c]/u;
|
|
132
|
+
|
|
133
|
+
it('flattens a table to one `Header: value` line per cell in the mode', () => {
|
|
134
|
+
const output = renderMarkdown(TABLE, { color: false, screenReader: true });
|
|
135
|
+
|
|
136
|
+
const lines = output.split('\n').map((line) => line.trim());
|
|
137
|
+
expect(lines).toContain('A: 1');
|
|
138
|
+
expect(lines).toContain('B: 2');
|
|
139
|
+
expect(lines.indexOf('A: 1')).toBeLessThan(lines.indexOf('B: 2'));
|
|
140
|
+
expect(output).not.toMatch(BOX_DRAWING);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('separates rows with a blank line so the row boundary is audible', () => {
|
|
144
|
+
const twoRows = ['| A | B |', '|---|---|', '| 1 | 2 |', '| 3 | 4 |'].join('\n');
|
|
145
|
+
const output = renderMarkdown(twoRows, { color: false, screenReader: true });
|
|
146
|
+
|
|
147
|
+
expect(output).toContain('A: 1\nB: 2\n\nA: 3\nB: 4');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('leaves the box-drawn grid exactly as it is outside the mode', () => {
|
|
151
|
+
const output = renderMarkdown(TABLE, { color: false });
|
|
152
|
+
|
|
153
|
+
expect(output).toMatch(BOX_DRAWING);
|
|
154
|
+
expect(output).not.toContain('A: 1');
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('keeps inline markdown formatting readable', () => {
|
|
159
|
+
const output = renderMarkdown('Use **bold** and `code` here.', { color: false });
|
|
160
|
+
|
|
161
|
+
expect(output).toContain('bold');
|
|
162
|
+
expect(output).toContain('code');
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCREEN-006 × #2222: the markdown renderer's own SGR must SURVIVE to the frame.
|
|
3
|
+
*
|
|
4
|
+
* `renderMarkdown` sanitizes untrusted markdown BEFORE `marked-terminal` styles it, so the escape
|
|
5
|
+
* codes in its OUTPUT are this package's own — the theme's diff pairs, heading emphasis.
|
|
6
|
+
* Routing that output back through `SafeText` sanitizes it a second time and strips exactly those
|
|
7
|
+
* codes, and it does so silently: the text still renders, just colourless. Every assertion in
|
|
8
|
+
* `streaming-indicator.test.tsx` and the tool-diff suite is on TEXT, so all of them stay green
|
|
9
|
+
* while the colour is gone — which is how the regression reached a release.
|
|
10
|
+
*
|
|
11
|
+
* These are the assertions that go red for that, one per render site that renders markdown.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import React from 'react';
|
|
15
|
+
import { render } from 'ink-testing-library';
|
|
16
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
17
|
+
|
|
18
|
+
import StreamingIndicator from '../StreamingIndicator.js';
|
|
19
|
+
import ToolDiffBlock from '../ToolDiffBlock.js';
|
|
20
|
+
import { DARK_THEME } from '../theme/built-in-themes.js';
|
|
21
|
+
import { foreground } from '../theme/index.js';
|
|
22
|
+
|
|
23
|
+
import type { IDiffLine } from '@robota-sdk/agent-interface-session';
|
|
24
|
+
|
|
25
|
+
/** The opening SGR a theme colour produces, for an assertion about the rendered bytes. */
|
|
26
|
+
function openCode(color: string): string {
|
|
27
|
+
const [open] = foreground(color)('x').split('x');
|
|
28
|
+
return open ?? '';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const DIFF_LINES: IDiffLine[] = [
|
|
32
|
+
{ type: 'hunk', text: '@@ -1,2 +1,2 @@', lineNumber: 1 },
|
|
33
|
+
{ type: 'remove', text: 'const a = 1;', lineNumber: 1 },
|
|
34
|
+
{ type: 'add', text: 'const a = 2;', lineNumber: 1 },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const DIFF_MARKDOWN = [
|
|
38
|
+
'```diff',
|
|
39
|
+
'@@ -1,2 +1,2 @@',
|
|
40
|
+
'- const a = 1;',
|
|
41
|
+
'+ const a = 2;',
|
|
42
|
+
'```',
|
|
43
|
+
].join('\n');
|
|
44
|
+
|
|
45
|
+
describe('markdown styling survives the render site (SCREEN-006)', () => {
|
|
46
|
+
let originalForceColor: string | undefined;
|
|
47
|
+
let originalNoColor: string | undefined;
|
|
48
|
+
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
// `renderMarkdown` colours only for an interactive colour terminal; the test process is not one.
|
|
51
|
+
originalForceColor = process.env.FORCE_COLOR;
|
|
52
|
+
originalNoColor = process.env.NO_COLOR;
|
|
53
|
+
delete process.env.NO_COLOR;
|
|
54
|
+
process.env.FORCE_COLOR = '1';
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
if (originalForceColor === undefined) delete process.env.FORCE_COLOR;
|
|
59
|
+
else process.env.FORCE_COLOR = originalForceColor;
|
|
60
|
+
if (originalNoColor === undefined) delete process.env.NO_COLOR;
|
|
61
|
+
else process.env.NO_COLOR = originalNoColor;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('ToolDiffBlock keeps the added/removed diff colours', () => {
|
|
65
|
+
const { lastFrame } = render(<ToolDiffBlock file="src/a.ts" lines={DIFF_LINES} />);
|
|
66
|
+
const frame = lastFrame() ?? '';
|
|
67
|
+
|
|
68
|
+
expect(frame).toContain('const a = 2;');
|
|
69
|
+
// SCREEN-2002: the diff colours come from the resolved theme; the default theme's values are the
|
|
70
|
+
// ones this suite pinned before the theme existed.
|
|
71
|
+
expect(frame).toContain(openCode(DARK_THEME.markdown.diffAdded));
|
|
72
|
+
expect(frame).toContain(openCode(DARK_THEME.markdown.diffRemoved));
|
|
73
|
+
expect(frame).toContain(openCode(DARK_THEME.markdown.diffHunk)); // the @@ hunk header
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('StreamingIndicator keeps the colours of a diff block in streamed text', () => {
|
|
77
|
+
const { lastFrame } = render(<StreamingIndicator text={DIFF_MARKDOWN} activeTools={[]} />);
|
|
78
|
+
const frame = lastFrame() ?? '';
|
|
79
|
+
|
|
80
|
+
expect(frame).toContain('const a = 2;');
|
|
81
|
+
expect(frame).toContain(openCode(DARK_THEME.markdown.diffAdded));
|
|
82
|
+
expect(frame).toContain(openCode(DARK_THEME.markdown.diffRemoved));
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('StreamingIndicator still sanitizes what arrives from OUTSIDE the renderer', () => {
|
|
86
|
+
// The other direction of the same boundary: `renderMarkdown` sanitizes its INPUT, so an escape
|
|
87
|
+
// sequence in the model's text is gone before `marked-terminal` ever sees it. Switching this
|
|
88
|
+
// site to `RenderedText` must not reopen that.
|
|
89
|
+
const { lastFrame } = render(
|
|
90
|
+
<StreamingIndicator text={`before]8;;https://evil.exampleclick]8;;after`} activeTools={[]} />,
|
|
91
|
+
);
|
|
92
|
+
const frame = lastFrame() ?? '';
|
|
93
|
+
|
|
94
|
+
expect(frame).not.toContain('evil.example');
|
|
95
|
+
expect(frame).not.toContain(']8;;');
|
|
96
|
+
expect(frame).toContain('before');
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #2222: the SafeText boundary, asserted against the BYTES Ink writes to a tty — not against
|
|
3
|
+
* `lastFrame()`. Measured on PR #2212: Ink itself strips OSC 52 / OSC 0 / CSI / DCS / APC from a
|
|
4
|
+
* frame and passes SGR, OSC 8 and a bare CR, so a frame assertion is green whether or not the code
|
|
5
|
+
* sanitizes. Only the stream a real terminal would receive tells the two apart.
|
|
6
|
+
*
|
|
7
|
+
* The raw-`Text` case below is REQUIRED to leak: it is what proves the detector can answer "yes".
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Writable } from 'node:stream';
|
|
11
|
+
|
|
12
|
+
import { Text as RawInkText, render } from 'ink';
|
|
13
|
+
import React from 'react';
|
|
14
|
+
import { describe, expect, it } from 'vitest';
|
|
15
|
+
|
|
16
|
+
import { SafeText, Text } from '../SafeText.js';
|
|
17
|
+
|
|
18
|
+
import type { ReactElement } from 'react';
|
|
19
|
+
|
|
20
|
+
/** OSC 8 hyperlink (Ink passes it through) and a bare CR — the two markers a frame cannot show. */
|
|
21
|
+
const OSC8 = ']8;;https://evil.exampleclick]8;;';
|
|
22
|
+
const POISON = `before${OSC8}\rafter`;
|
|
23
|
+
|
|
24
|
+
/** A stream that claims to be a tty, so Ink writes what a terminal would receive. */
|
|
25
|
+
function createTtyCapture(): { stdout: NodeJS.WriteStream; bytes: () => string } {
|
|
26
|
+
const chunks: string[] = [];
|
|
27
|
+
const stream = new Writable({
|
|
28
|
+
write(chunk: Buffer | string, _encoding, callback) {
|
|
29
|
+
chunks.push(chunk.toString());
|
|
30
|
+
callback();
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
const stdout = Object.assign(stream, {
|
|
34
|
+
isTTY: true,
|
|
35
|
+
columns: 80,
|
|
36
|
+
rows: 24,
|
|
37
|
+
}) as unknown as NodeJS.WriteStream;
|
|
38
|
+
return { stdout, bytes: () => chunks.join('') };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function renderToTty(element: ReactElement): Promise<string> {
|
|
42
|
+
const capture = createTtyCapture();
|
|
43
|
+
const instance = render(element, { stdout: capture.stdout, patchConsole: false });
|
|
44
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
45
|
+
instance.unmount();
|
|
46
|
+
return capture.bytes();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('SafeText boundary (#2222)', () => {
|
|
50
|
+
it('a raw ink Text render LEAKS the markers to the tty stream (detector proof)', async () => {
|
|
51
|
+
const bytes = await renderToTty(<RawInkText>{POISON}</RawInkText>);
|
|
52
|
+
expect(bytes).toContain(']8;;https://evil.example');
|
|
53
|
+
expect(bytes).toContain('before');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('SafeText removes the markers from the same payload, keeping the visible text', async () => {
|
|
57
|
+
const bytes = await renderToTty(<SafeText>{POISON}</SafeText>);
|
|
58
|
+
expect(bytes).not.toContain('evil.example');
|
|
59
|
+
expect(bytes).not.toContain(']8;;');
|
|
60
|
+
expect(bytes).toContain('before');
|
|
61
|
+
expect(bytes).toContain('after');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('the `Text` alias call sites use is the boundary, and nested children are covered', async () => {
|
|
65
|
+
const bytes = await renderToTty(
|
|
66
|
+
<Text>
|
|
67
|
+
{'label: '}
|
|
68
|
+
<Text bold>{POISON}</Text>
|
|
69
|
+
{[POISON, 42]}
|
|
70
|
+
</Text>,
|
|
71
|
+
);
|
|
72
|
+
expect(bytes).not.toContain('evil.example');
|
|
73
|
+
expect(bytes).toContain('42');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue #3242: the TUI session consults the same sandbox the shell tools run under, so a confined
|
|
3
|
+
* command the sandbox approves runs without a prompt.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, it } from 'vitest';
|
|
7
|
+
|
|
8
|
+
import { toChannelOptions } from '../render.js';
|
|
9
|
+
import { buildTuiSessionOptions } from '../tui-session-options.js';
|
|
10
|
+
|
|
11
|
+
import type { IRenderOptions } from '../render.js';
|
|
12
|
+
|
|
13
|
+
type TSandboxClient = NonNullable<IRenderOptions['sandboxClient']>;
|
|
14
|
+
|
|
15
|
+
const sandboxClient = { filesystem: 'shared', autoApproves: () => true } as unknown as TSandboxClient;
|
|
16
|
+
|
|
17
|
+
function renderOptions(extra: Partial<IRenderOptions> = {}): IRenderOptions {
|
|
18
|
+
return { cwd: '/work', provider: {} as never, ...extra } as IRenderOptions;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('the TUI projection carries the sandbox client', () => {
|
|
22
|
+
it('passes the supplied sandbox through the channel into the session', () => {
|
|
23
|
+
const channel = toChannelOptions(renderOptions({ sandboxClient }));
|
|
24
|
+
|
|
25
|
+
expect((buildTuiSessionOptions(channel) as { sandboxClient?: unknown }).sandboxClient).toBe(
|
|
26
|
+
sandboxClient,
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('carries none when none was supplied', () => {
|
|
31
|
+
const channel = toChannelOptions(renderOptions());
|
|
32
|
+
|
|
33
|
+
expect(
|
|
34
|
+
(buildTuiSessionOptions(channel) as { sandboxClient?: unknown }).sandboxClient,
|
|
35
|
+
).toBeUndefined();
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI-2004 TC-10 — the input area in screen-reader mode.
|
|
3
|
+
*
|
|
4
|
+
* Two things, and they fail for the same reason. The hand-drawn rules above and below the input are
|
|
5
|
+
* repainted with the line, so a reader hears a wall of dashes on every keystroke; and a word
|
|
6
|
+
* deletion leaves a reader announcing what REMAINS, which is precisely not the information wanted.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { render } from 'ink-testing-library';
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
12
|
+
|
|
13
|
+
import InputArea from '../InputArea.js';
|
|
14
|
+
import { formatDeletionAnnouncement } from '../input-area-rules.js';
|
|
15
|
+
import { ScreenReaderProvider } from '../screen-reader-context.js';
|
|
16
|
+
|
|
17
|
+
async function tick(): Promise<void> {
|
|
18
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function renderInput(enabled: boolean): ReturnType<typeof render> {
|
|
22
|
+
return render(
|
|
23
|
+
<ScreenReaderProvider enabled={enabled}>
|
|
24
|
+
<InputArea onSubmit={vi.fn()} isDisabled={false} />
|
|
25
|
+
</ScreenReaderProvider>,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('TC-10: the input area chrome', () => {
|
|
30
|
+
it('renders no rule line and no border in the mode', async () => {
|
|
31
|
+
const { lastFrame, unmount } = renderInput(true);
|
|
32
|
+
await tick();
|
|
33
|
+
const frame = lastFrame() ?? '';
|
|
34
|
+
unmount();
|
|
35
|
+
|
|
36
|
+
expect(frame).not.toContain('─');
|
|
37
|
+
expect(frame).not.toMatch(/[╭╮╰╯│┌┐└┘]/u);
|
|
38
|
+
expect(frame).toContain('Type a message');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('still renders the rules when the mode is off', async () => {
|
|
42
|
+
const { lastFrame, unmount } = renderInput(false);
|
|
43
|
+
await tick();
|
|
44
|
+
const frame = lastFrame() ?? '';
|
|
45
|
+
unmount();
|
|
46
|
+
|
|
47
|
+
expect(frame).toContain('─');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('TC-10: the deletion announcement', () => {
|
|
52
|
+
it('prints `[deleted: <word>]` exactly once after a word deletion', async () => {
|
|
53
|
+
const { stdin, lastFrame, unmount } = renderInput(true);
|
|
54
|
+
await tick();
|
|
55
|
+
|
|
56
|
+
for (const char of 'hello world') {
|
|
57
|
+
stdin.write(char);
|
|
58
|
+
}
|
|
59
|
+
await tick();
|
|
60
|
+
// Ctrl+W — the word delete.
|
|
61
|
+
stdin.write('\x17');
|
|
62
|
+
await tick();
|
|
63
|
+
|
|
64
|
+
const frame = lastFrame() ?? '';
|
|
65
|
+
unmount();
|
|
66
|
+
|
|
67
|
+
const announcement = formatDeletionAnnouncement('world');
|
|
68
|
+
expect(frame).toContain(announcement);
|
|
69
|
+
expect(frame.split(announcement).length - 1).toBe(1);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('announces the whole line for a line deletion', async () => {
|
|
73
|
+
const { stdin, lastFrame, unmount } = renderInput(true);
|
|
74
|
+
await tick();
|
|
75
|
+
|
|
76
|
+
for (const char of 'abc') {
|
|
77
|
+
stdin.write(char);
|
|
78
|
+
}
|
|
79
|
+
await tick();
|
|
80
|
+
// Ctrl+U — the line delete.
|
|
81
|
+
stdin.write('\x15');
|
|
82
|
+
await tick();
|
|
83
|
+
|
|
84
|
+
const frame = lastFrame() ?? '';
|
|
85
|
+
unmount();
|
|
86
|
+
|
|
87
|
+
expect(frame).toContain(formatDeletionAnnouncement('abc'));
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('announces nothing when the mode is off', async () => {
|
|
91
|
+
const { stdin, lastFrame, unmount } = renderInput(false);
|
|
92
|
+
await tick();
|
|
93
|
+
|
|
94
|
+
for (const char of 'hello world') {
|
|
95
|
+
stdin.write(char);
|
|
96
|
+
}
|
|
97
|
+
await tick();
|
|
98
|
+
stdin.write('\x17');
|
|
99
|
+
await tick();
|
|
100
|
+
|
|
101
|
+
const frame = lastFrame() ?? '';
|
|
102
|
+
unmount();
|
|
103
|
+
|
|
104
|
+
expect(frame).not.toContain('[deleted:');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI-2004 TC-05 — the role-derived, provider-invariant label vocabulary.
|
|
3
|
+
*
|
|
4
|
+
* The point of the negative assertion below is not tidiness: a label naming the vendor would make
|
|
5
|
+
* the transcript differ between `agent-provider-*` packages for the same conversation, and a reader
|
|
6
|
+
* grepping their scrollback for "assistant:" would find nothing after switching providers.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { render } from 'ink-testing-library';
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import { describe, expect, it } from 'vitest';
|
|
12
|
+
|
|
13
|
+
import { RoleLabel } from '../RoleLabel.js';
|
|
14
|
+
import { ScreenReaderProvider } from '../screen-reader-context.js';
|
|
15
|
+
import {
|
|
16
|
+
SCREEN_READER_LABELS,
|
|
17
|
+
screenReaderLabelForRole,
|
|
18
|
+
screenReaderLabelValues,
|
|
19
|
+
} from '../screen-reader-labels.js';
|
|
20
|
+
|
|
21
|
+
import type { TUniversalMessage } from '@robota-sdk/agent-core';
|
|
22
|
+
|
|
23
|
+
/** Every provider package this repo ships, plus the vendor names behind them. */
|
|
24
|
+
const VENDOR_WORDS = /(claude|anthropic|openai|gpt|gemini|google|llama|mistral|copilot)/i;
|
|
25
|
+
|
|
26
|
+
function renderLabel(role: TUniversalMessage['role'], enabled: boolean, driverId?: string): string {
|
|
27
|
+
const label = React.createElement(
|
|
28
|
+
RoleLabel,
|
|
29
|
+
driverId === undefined ? { role } : { role, driverId },
|
|
30
|
+
);
|
|
31
|
+
const { lastFrame, unmount } = render(
|
|
32
|
+
React.createElement(ScreenReaderProvider, { enabled, children: label }),
|
|
33
|
+
);
|
|
34
|
+
const frame = lastFrame() ?? '';
|
|
35
|
+
unmount();
|
|
36
|
+
return frame;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('TC-05: the label vocabulary', () => {
|
|
40
|
+
it('covers all nine label kinds', () => {
|
|
41
|
+
expect(Object.keys(SCREEN_READER_LABELS)).toHaveLength(9);
|
|
42
|
+
expect(screenReaderLabelValues()).toEqual([
|
|
43
|
+
'you:',
|
|
44
|
+
'assistant:',
|
|
45
|
+
'thinking:',
|
|
46
|
+
'tool:',
|
|
47
|
+
'tool error:',
|
|
48
|
+
'error:',
|
|
49
|
+
'warning:',
|
|
50
|
+
'permission required:',
|
|
51
|
+
'cost:',
|
|
52
|
+
]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('is entirely lowercase', () => {
|
|
56
|
+
for (const label of screenReaderLabelValues()) {
|
|
57
|
+
expect(label).toBe(label.toLowerCase());
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('names no provider or vendor', () => {
|
|
62
|
+
for (const label of screenReaderLabelValues()) {
|
|
63
|
+
expect(label).not.toMatch(VENDOR_WORDS);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('maps every transcript role onto the vocabulary', () => {
|
|
68
|
+
expect(screenReaderLabelForRole('user')).toBe('you:');
|
|
69
|
+
expect(screenReaderLabelForRole('assistant')).toBe('assistant:');
|
|
70
|
+
expect(screenReaderLabelForRole('tool')).toBe('tool:');
|
|
71
|
+
expect(screenReaderLabelForRole('system')).toBe('warning:');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('TC-05: RoleLabel in the mode', () => {
|
|
76
|
+
it('renders `assistant:` for an assistant message — the same under every provider', () => {
|
|
77
|
+
const frame = renderLabel('assistant', true);
|
|
78
|
+
expect(frame).toContain('assistant:');
|
|
79
|
+
expect(frame).not.toMatch(VENDOR_WORDS);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('renders `you:` for the operator, and the driver id for a co-driven turn', () => {
|
|
83
|
+
expect(renderLabel('user', true)).toContain('you:');
|
|
84
|
+
expect(renderLabel('user', true, 'peer:s-1')).toContain('peer:s-1:');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('uses neutral visual labels when the mode is off', () => {
|
|
88
|
+
expect(renderLabel('assistant', false)).toContain('Assistant:');
|
|
89
|
+
expect(renderLabel('user', false)).toContain('You:');
|
|
90
|
+
});
|
|
91
|
+
});
|