@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,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCREEN-2670 TC-10 — the echo exemption FIRES through the real render path.
|
|
3
|
+
*
|
|
4
|
+
* TC-03 arms the flag by hand, which proves the queue honours a stamp and nothing about whether
|
|
5
|
+
* anything ever sets one. This drives a keystroke through Ink's own input path into the composed
|
|
6
|
+
* tree — keybindings, the pacing context, the composer's key handler — with the proxy as Ink's
|
|
7
|
+
* stdout, and asserts BOTH halves: the keystroke's commit leaves unparked, and the next commit that
|
|
8
|
+
* is not a keystroke (a prop change from outside the composer, and Enter — whose commit appends the
|
|
9
|
+
* prompt line) is parked. A proxy that stamped everything would pass the first half and fail the
|
|
10
|
+
* second, which is why they sit in one test.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { EventEmitter } from 'node:events';
|
|
14
|
+
|
|
15
|
+
import { Box, render, Text } from 'ink';
|
|
16
|
+
import React, { useState } from 'react';
|
|
17
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
18
|
+
|
|
19
|
+
import CjkTextInput from '../CjkTextInput.js';
|
|
20
|
+
import { KeybindingsProvider } from '../keybindings/keybindings-context.js';
|
|
21
|
+
import { parseKeybindingsDocument } from '../keybindings/keybinding-registry.js';
|
|
22
|
+
import { ScreenReaderPacingProvider } from '../screen-reader-pacing-context.js';
|
|
23
|
+
import { createParkedStdout } from '../screen-reader-stdout.js';
|
|
24
|
+
|
|
25
|
+
import type { IKeybindingsSource } from '../keybindings/node-keybindings-source.js';
|
|
26
|
+
import type { IScreenReaderPacingPort } from '../screen-reader-pacing-context.js';
|
|
27
|
+
import type { TParkedStdoutSource } from '../screen-reader-stdout.js';
|
|
28
|
+
|
|
29
|
+
const PARK_MS = 400;
|
|
30
|
+
|
|
31
|
+
class FakeTtyStdout extends EventEmitter {
|
|
32
|
+
readonly isTTY = true;
|
|
33
|
+
readonly columns = 80;
|
|
34
|
+
readonly rows = 24;
|
|
35
|
+
readonly destroyed = false;
|
|
36
|
+
readonly writable = true;
|
|
37
|
+
readonly writableEnded = false;
|
|
38
|
+
readonly writableLength = 0;
|
|
39
|
+
readonly writableNeedDrain = false;
|
|
40
|
+
readonly frames: Array<{ text: string; at: number }> = [];
|
|
41
|
+
write = (chunk: string | Uint8Array, ...rest: unknown[]): boolean => {
|
|
42
|
+
const callback = rest.find((argument) => typeof argument === 'function') as
|
|
43
|
+
((error?: Error | null) => void) | undefined;
|
|
44
|
+
this.frames.push({ text: String(chunk), at: Date.now() });
|
|
45
|
+
callback?.(null);
|
|
46
|
+
return true;
|
|
47
|
+
};
|
|
48
|
+
asSource(): TParkedStdoutSource {
|
|
49
|
+
return this as unknown as TParkedStdoutSource;
|
|
50
|
+
}
|
|
51
|
+
textSince(index: number): string {
|
|
52
|
+
return this.frames
|
|
53
|
+
.slice(index)
|
|
54
|
+
.map((frame) => frame.text)
|
|
55
|
+
.join('');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
class FakeStdin extends EventEmitter {
|
|
60
|
+
readonly isTTY = true;
|
|
61
|
+
private pending: string | null = null;
|
|
62
|
+
setEncoding(): void {}
|
|
63
|
+
setRawMode(): void {}
|
|
64
|
+
resume(): void {}
|
|
65
|
+
pause(): void {}
|
|
66
|
+
ref(): void {}
|
|
67
|
+
unref(): void {}
|
|
68
|
+
read(): string | null {
|
|
69
|
+
const data = this.pending;
|
|
70
|
+
this.pending = null;
|
|
71
|
+
return data;
|
|
72
|
+
}
|
|
73
|
+
send(data: string): void {
|
|
74
|
+
this.pending = data;
|
|
75
|
+
this.emit('readable');
|
|
76
|
+
this.emit('data', data);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function source(): IKeybindingsSource {
|
|
81
|
+
const parsed = parseKeybindingsDocument(
|
|
82
|
+
JSON.stringify({ version: 1, bindings: {} }),
|
|
83
|
+
'/tmp/keybindings.json',
|
|
84
|
+
);
|
|
85
|
+
if (!parsed.ok) throw new Error(parsed.diagnostic.message);
|
|
86
|
+
return {
|
|
87
|
+
filePath: '/tmp/keybindings.json',
|
|
88
|
+
start: async () => undefined,
|
|
89
|
+
isStarted: () => true,
|
|
90
|
+
ensureFile: async () => '/tmp/keybindings.json',
|
|
91
|
+
getSnapshot: () => parsed.snapshot,
|
|
92
|
+
subscribe: () => () => undefined,
|
|
93
|
+
dispose: () => undefined,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function Harness({
|
|
98
|
+
banner,
|
|
99
|
+
onSubmit,
|
|
100
|
+
}: {
|
|
101
|
+
banner: string;
|
|
102
|
+
onSubmit: (value: string) => void;
|
|
103
|
+
}): React.ReactElement {
|
|
104
|
+
const [value, setValue] = useState('');
|
|
105
|
+
return (
|
|
106
|
+
<Box flexDirection="column">
|
|
107
|
+
<Text>{banner}</Text>
|
|
108
|
+
<Box>
|
|
109
|
+
<Text>{'> '}</Text>
|
|
110
|
+
<CjkTextInput
|
|
111
|
+
value={value}
|
|
112
|
+
onChange={setValue}
|
|
113
|
+
onSubmit={(submitted) => {
|
|
114
|
+
// Like the real composer: submitting clears the line, so the commit repaints.
|
|
115
|
+
setValue('');
|
|
116
|
+
onSubmit(submitted);
|
|
117
|
+
}}
|
|
118
|
+
keybindingContext="chat-input"
|
|
119
|
+
/>
|
|
120
|
+
</Box>
|
|
121
|
+
</Box>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));
|
|
126
|
+
|
|
127
|
+
/** In screen-reader mode Ink linearises each Text node; compare visible characters only. */
|
|
128
|
+
const visible = (text: string): string =>
|
|
129
|
+
// eslint-disable-next-line no-control-regex -- stripping the escapes IS the point here
|
|
130
|
+
text.replace(/\x1b\[[0-9;?]*[A-Za-z]|\x1b\][^\x07]*\x07/g, '').replace(/\s+/g, ' ');
|
|
131
|
+
|
|
132
|
+
let unmount: (() => void) | undefined;
|
|
133
|
+
|
|
134
|
+
afterEach(() => {
|
|
135
|
+
unmount?.();
|
|
136
|
+
unmount = undefined;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('TC-10: a keystroke through the composed tree is released unparked; a non-composer commit is parked', () => {
|
|
140
|
+
it('echo leaves at once, a banner change and Enter each wait out the interval', async () => {
|
|
141
|
+
const stdout = new FakeTtyStdout();
|
|
142
|
+
const stdin = new FakeStdin();
|
|
143
|
+
const parked = createParkedStdout({ stdout: stdout.asSource(), preparkMs: PARK_MS });
|
|
144
|
+
const port: IScreenReaderPacingPort = {
|
|
145
|
+
armEchoRelease: () => parked.armEchoRelease(),
|
|
146
|
+
write: (text) => {
|
|
147
|
+
parked.write(text);
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
const submitted: string[] = [];
|
|
151
|
+
const tree = (banner: string): React.ReactElement => (
|
|
152
|
+
<KeybindingsProvider source={source()}>
|
|
153
|
+
<ScreenReaderPacingProvider port={port}>
|
|
154
|
+
<Harness banner={banner} onSubmit={(value) => submitted.push(value)} />
|
|
155
|
+
</ScreenReaderPacingProvider>
|
|
156
|
+
</KeybindingsProvider>
|
|
157
|
+
);
|
|
158
|
+
const instance = render(tree('banner 1'), {
|
|
159
|
+
stdout: parked.asInkStdout(),
|
|
160
|
+
stdin: stdin as unknown as NodeJS.ReadStream,
|
|
161
|
+
isScreenReaderEnabled: true,
|
|
162
|
+
// Ink treats a CI environment as non-interactive and defers every frame to unmount; the
|
|
163
|
+
// park only exists for interactive terminals, so say so explicitly (as the real-cursor
|
|
164
|
+
// suite does) rather than let the runner's CI variable decide what is under test.
|
|
165
|
+
interactive: true,
|
|
166
|
+
exitOnCtrlC: false,
|
|
167
|
+
patchConsole: false,
|
|
168
|
+
});
|
|
169
|
+
unmount = instance.unmount;
|
|
170
|
+
|
|
171
|
+
// First frames: the first batch is never parked; let the mount settle past one interval.
|
|
172
|
+
await sleep(PARK_MS + 100);
|
|
173
|
+
expect(stdout.textSince(0)).toContain('banner 1');
|
|
174
|
+
|
|
175
|
+
// A keystroke: its echo must be on the underlying stream well inside the interval.
|
|
176
|
+
const beforeKey = stdout.frames.length;
|
|
177
|
+
stdin.send('hey');
|
|
178
|
+
await sleep(80);
|
|
179
|
+
expect(visible(stdout.textSince(beforeKey))).toContain('hey');
|
|
180
|
+
|
|
181
|
+
// A commit that is not a keystroke, arriving inside the interval after the echo: parked until
|
|
182
|
+
// the interval since that last printable release has elapsed, then released.
|
|
183
|
+
const beforeBanner = stdout.frames.length;
|
|
184
|
+
instance.rerender(tree('banner 2'));
|
|
185
|
+
await sleep(80);
|
|
186
|
+
expect(visible(stdout.textSince(beforeBanner))).not.toContain('banner 2');
|
|
187
|
+
await sleep(PARK_MS);
|
|
188
|
+
expect(visible(stdout.textSince(beforeBanner))).toContain('banner 2');
|
|
189
|
+
|
|
190
|
+
// Enter runs through the same key handler and must NOT arm: arriving right after the banner's
|
|
191
|
+
// release, the submit commit is parked like any other transcript commit.
|
|
192
|
+
const beforeEnter = stdout.frames.length;
|
|
193
|
+
stdin.send('\r');
|
|
194
|
+
await sleep(80);
|
|
195
|
+
expect(submitted).toEqual(['hey']);
|
|
196
|
+
expect(stdout.frames.length).toBe(beforeEnter);
|
|
197
|
+
await sleep(PARK_MS);
|
|
198
|
+
expect(stdout.frames.length).toBeGreaterThan(beforeEnter);
|
|
199
|
+
}, 10_000);
|
|
200
|
+
});
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI-2004 TC-02 / TC-03 / TC-17 / TC-18 — the mode's entry point.
|
|
3
|
+
*
|
|
4
|
+
* TC-02 asserts BOTH halves of the threading, because they fail independently: Ink's own
|
|
5
|
+
* `isScreenReaderEnabled` option, and the hand-maintained `toChannelOptions` projection that
|
|
6
|
+
* ARCH-110 records as able to drop an option silently.
|
|
7
|
+
* TC-03/TC-18 assert the first line the process prints — the confirmation, the advisory, and the
|
|
8
|
+
* two cases where nothing at all is printed.
|
|
9
|
+
* TC-17 asserts the banner suppression, which TC-11's box-drawing sweep cannot see because the
|
|
10
|
+
* banner is ASCII art, not box-drawing characters.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import React from 'react';
|
|
14
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
15
|
+
|
|
16
|
+
import { BANNER_GLYPHS, BANNER_ART } from '../app-banner.js';
|
|
17
|
+
import { buildStaticItems } from '../app-static-items.js';
|
|
18
|
+
import {
|
|
19
|
+
screenReaderAnnouncement,
|
|
20
|
+
writeScreenReaderAnnouncement,
|
|
21
|
+
} from '../screen-reader-announcement.js';
|
|
22
|
+
import { toChannelOptions } from '../render.js';
|
|
23
|
+
|
|
24
|
+
import type { IRenderOptions } from '../render.js';
|
|
25
|
+
import type { IKeybindingsSource } from '../keybindings/node-keybindings-source.js';
|
|
26
|
+
import type { IHistoryEntry, IAIProvider } from '@robota-sdk/agent-core';
|
|
27
|
+
import type { ITuiCliAdapter } from '../tui-cli-adapter.js';
|
|
28
|
+
|
|
29
|
+
const inkRender = vi.hoisted(() =>
|
|
30
|
+
vi.fn(
|
|
31
|
+
(
|
|
32
|
+
_node: unknown,
|
|
33
|
+
_options?: { isScreenReaderEnabled?: boolean; stdout?: NodeJS.WriteStream },
|
|
34
|
+
) => ({
|
|
35
|
+
waitUntilExit: async (): Promise<void> => {},
|
|
36
|
+
}),
|
|
37
|
+
),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
vi.mock('ink', async (importOriginal) => {
|
|
41
|
+
const actual = await importOriginal<typeof import('ink')>();
|
|
42
|
+
return { ...actual, render: inkRender };
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// The App boots a channel and starts I/O; the mode's threading is what is under test, not the App.
|
|
46
|
+
vi.mock('../App.js', () => ({
|
|
47
|
+
default: (): React.ReactElement => React.createElement('robota-app'),
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
function baseOptions(): IRenderOptions {
|
|
51
|
+
return {
|
|
52
|
+
cwd: '/tmp/project',
|
|
53
|
+
provider: {} as IAIProvider,
|
|
54
|
+
cliAdapter: {} as ITuiCliAdapter,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('TC-02: the mode reaches Ink AND the channel projection', () => {
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
inkRender.mockClear();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
afterEach(() => {
|
|
64
|
+
vi.unstubAllEnvs();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('renderApp passes isScreenReaderEnabled: true to Ink render when the mode is on', async () => {
|
|
68
|
+
const { renderApp } = await import('../render.js');
|
|
69
|
+
await renderApp({ ...baseOptions(), screenReader: true });
|
|
70
|
+
|
|
71
|
+
expect(inkRender).toHaveBeenCalledTimes(1);
|
|
72
|
+
expect(inkRender.mock.calls[0]?.[1]?.isScreenReaderEnabled).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('renderApp passes isScreenReaderEnabled: false when no mode input is present (default-off)', async () => {
|
|
76
|
+
const { renderApp } = await import('../render.js');
|
|
77
|
+
await renderApp(baseOptions());
|
|
78
|
+
|
|
79
|
+
expect(inkRender.mock.calls[0]?.[1]?.isScreenReaderEnabled).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('toChannelOptions projects screenReader — the ARCH-110 drop hazard', () => {
|
|
83
|
+
expect(toChannelOptions({ ...baseOptions(), screenReader: true }).screenReader).toBe(true);
|
|
84
|
+
expect(toChannelOptions({ ...baseOptions(), screenReader: false }).screenReader).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('toChannelOptions leaves screenReader undefined when the option is absent', () => {
|
|
88
|
+
expect(toChannelOptions(baseOptions()).screenReader).toBeUndefined();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('BEHAVIOR-2003: keybinding source lifecycle', () => {
|
|
93
|
+
it('disposes the source when initialization fails before Ink starts', async () => {
|
|
94
|
+
const startupFailure = new Error('watcher startup failed');
|
|
95
|
+
const dispose = vi.fn();
|
|
96
|
+
const source = {
|
|
97
|
+
start: vi.fn().mockRejectedValue(startupFailure),
|
|
98
|
+
dispose,
|
|
99
|
+
} as unknown as IKeybindingsSource;
|
|
100
|
+
inkRender.mockClear();
|
|
101
|
+
|
|
102
|
+
const { renderApp } = await import('../render.js');
|
|
103
|
+
await expect(renderApp({ ...baseOptions(), keybindingsSource: source })).rejects.toBe(
|
|
104
|
+
startupFailure,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
expect(dispose).toHaveBeenCalledTimes(1);
|
|
108
|
+
expect(inkRender).not.toHaveBeenCalled();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('TC-03: the confirmation line names the channel', () => {
|
|
113
|
+
it('prints the channel that actually turned the mode on', () => {
|
|
114
|
+
expect(screenReaderAnnouncement({ enabled: true, channel: 'flag' })).toBe(
|
|
115
|
+
'[Screen reader mode: on via flag]',
|
|
116
|
+
);
|
|
117
|
+
expect(screenReaderAnnouncement({ enabled: true, channel: 'env' })).toBe(
|
|
118
|
+
'[Screen reader mode: on via env]',
|
|
119
|
+
);
|
|
120
|
+
expect(screenReaderAnnouncement({ enabled: true, channel: 'settings' })).toBe(
|
|
121
|
+
'[Screen reader mode: on via settings]',
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('prints nothing when the mode is off and no reader hint is present', () => {
|
|
126
|
+
expect(screenReaderAnnouncement({ enabled: false })).toBeUndefined();
|
|
127
|
+
const written: string[] = [];
|
|
128
|
+
writeScreenReaderAnnouncement({ enabled: false }, (text) => written.push(text));
|
|
129
|
+
expect(written).toEqual([]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('renderApp writes the confirmation line to stdout before Ink is called', async () => {
|
|
133
|
+
const written: string[] = [];
|
|
134
|
+
const spy = vi
|
|
135
|
+
.spyOn(process.stdout, 'write')
|
|
136
|
+
.mockImplementation((chunk: string | Uint8Array): boolean => {
|
|
137
|
+
written.push(String(chunk));
|
|
138
|
+
return true;
|
|
139
|
+
});
|
|
140
|
+
inkRender.mockClear();
|
|
141
|
+
inkRender.mockImplementation(() => {
|
|
142
|
+
// Ink is reached only after the line is already on stdout.
|
|
143
|
+
expect(written.join('')).toContain('[Screen reader mode: on via flag]');
|
|
144
|
+
return { waitUntilExit: async (): Promise<void> => {} };
|
|
145
|
+
});
|
|
146
|
+
try {
|
|
147
|
+
const { renderApp } = await import('../render.js');
|
|
148
|
+
await renderApp({ ...baseOptions(), screenReader: true, screenReaderChannel: 'flag' });
|
|
149
|
+
} finally {
|
|
150
|
+
spy.mockRestore();
|
|
151
|
+
inkRender.mockImplementation(() => ({ waitUntilExit: async (): Promise<void> => {} }));
|
|
152
|
+
}
|
|
153
|
+
expect(written.join('')).toContain('[Screen reader mode: on via flag]\n');
|
|
154
|
+
expect(inkRender).toHaveBeenCalledTimes(1);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('TC-18: the advisory line, and its two negatives', () => {
|
|
159
|
+
it('is the first line when the mode is off and a reader-shaped hint is present', () => {
|
|
160
|
+
const written: string[] = [];
|
|
161
|
+
writeScreenReaderAnnouncement({ enabled: false, hint: true }, (text) => written.push(text));
|
|
162
|
+
expect(written.join('').split('\n')[0]).toBe(
|
|
163
|
+
'[Screen reader mode: off — run with --screen-reader]',
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('is absent when the mode is off and there is no hint', () => {
|
|
168
|
+
expect(screenReaderAnnouncement({ enabled: false, hint: false })).toBeUndefined();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('is replaced by the confirmation line when the mode is on, never printed alongside it', () => {
|
|
172
|
+
const written: string[] = [];
|
|
173
|
+
writeScreenReaderAnnouncement({ enabled: true, channel: 'env', hint: true }, (text) =>
|
|
174
|
+
written.push(text),
|
|
175
|
+
);
|
|
176
|
+
const output = written.join('');
|
|
177
|
+
expect(output).toBe('[Screen reader mode: on via env]\n');
|
|
178
|
+
expect(output).not.toContain('run with --screen-reader');
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe('TC-17: the ASCII banner is not committed in the mode', () => {
|
|
183
|
+
const history = [{ id: 'h1' } as unknown as IHistoryEntry];
|
|
184
|
+
|
|
185
|
+
it('omits the banner item entirely when the mode is on', () => {
|
|
186
|
+
const items = buildStaticItems({ history, version: '1.2.3', screenReader: true });
|
|
187
|
+
expect(items.some((item) => item.kind === 'banner')).toBe(false);
|
|
188
|
+
expect(items).toEqual([{ kind: 'entry', entry: history[0] }]);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('keeps the banner item when the mode is off', () => {
|
|
192
|
+
const items = buildStaticItems({ history, version: '1.2.3', screenReader: false });
|
|
193
|
+
expect(items[0]).toEqual({ kind: 'banner', version: '1.2.3' });
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('the suppressed art is what carries the glyphs a box-drawing sweep would miss', () => {
|
|
197
|
+
for (const glyph of BANNER_GLYPHS) {
|
|
198
|
+
expect(BANNER_ART).toContain(glyph);
|
|
199
|
+
}
|
|
200
|
+
// None of them is a box-drawing character — hence the separate criterion.
|
|
201
|
+
expect(BANNER_ART).not.toMatch(/[│─┌┐└┘├┤┬┴┼╭╮╰╯]/u);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe('SCREEN-2670 TC-05: with the mode off, render() is called with no stdout key at all', () => {
|
|
206
|
+
beforeEach(() => {
|
|
207
|
+
inkRender.mockClear();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
afterEach(() => {
|
|
211
|
+
vi.unstubAllEnvs();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('passes no stdout when the mode is off — Ink defaults to process.stdout and keys its map by it', async () => {
|
|
215
|
+
const { renderApp } = await import('../render.js');
|
|
216
|
+
await renderApp(baseOptions());
|
|
217
|
+
const options = inkRender.mock.calls[0]?.[1] ?? {};
|
|
218
|
+
expect(Object.keys(options)).not.toContain('stdout');
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('passes no stdout when the mode is on but the park is disabled with 0', async () => {
|
|
222
|
+
const { renderApp } = await import('../render.js');
|
|
223
|
+
await renderApp({
|
|
224
|
+
...baseOptions(),
|
|
225
|
+
screenReader: true,
|
|
226
|
+
screenReaderPacing: {
|
|
227
|
+
startupQuiet: { raw: '0', label: 'ACME_STARTUP_QUIET_MS' },
|
|
228
|
+
prepark: { raw: '0', label: 'ACME_PREPARK_MS' },
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
expect(Object.keys(inkRender.mock.calls[0]?.[1] ?? {})).not.toContain('stdout');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('ignores an ambient Robota park override when the host supplies no park choice', async () => {
|
|
235
|
+
vi.stubEnv('ROBOTA_SCREEN_READER_PREPARK_MS', '0');
|
|
236
|
+
const { renderApp } = await import('../render.js');
|
|
237
|
+
await renderApp({
|
|
238
|
+
...baseOptions(),
|
|
239
|
+
screenReader: true,
|
|
240
|
+
screenReaderPacing: {
|
|
241
|
+
startupQuiet: { raw: '0', label: 'ACME_STARTUP_QUIET_MS' },
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
expect(inkRender.mock.calls[0]?.[1]?.stdout).toBeDefined();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('hands Ink the owned stdout when the mode is on and the park is enabled', async () => {
|
|
248
|
+
const { renderApp } = await import('../render.js');
|
|
249
|
+
await renderApp({
|
|
250
|
+
...baseOptions(),
|
|
251
|
+
screenReader: true,
|
|
252
|
+
screenReaderPacing: {
|
|
253
|
+
startupQuiet: { raw: '0', label: 'ACME_STARTUP_QUIET_MS' },
|
|
254
|
+
prepark: { raw: '50', label: 'ACME_PREPARK_MS' },
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
const stdout = inkRender.mock.calls[0]?.[1]?.stdout;
|
|
258
|
+
expect(stdout).toBeDefined();
|
|
259
|
+
expect(stdout).not.toBe(process.stdout);
|
|
260
|
+
expect(typeof stdout?.write).toBe('function');
|
|
261
|
+
});
|
|
262
|
+
});
|