@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,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CMD-004 Phase 2 Stage C (TC-04): the TUI is a pure renderer — the four UI screens open from the
|
|
3
|
+
* requester-routed `ui_intent` session event (never from legacy `result.effects` branches), the
|
|
4
|
+
* session title updates from the broadcast `session_renamed` event (never by the TUI mutating the
|
|
5
|
+
* session), and the statusline re-reads its persisted settings when a command result arrives
|
|
6
|
+
* (refresh-on-result — the HOST applies the patch; the TUI no longer writes settings).
|
|
7
|
+
*
|
|
8
|
+
* Red-first evidence: written against the pre-Stage-C hook (which still consumed legacy effects and
|
|
9
|
+
* had no session subscription) — every behavioral case below failed there.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Text } from 'ink';
|
|
13
|
+
import { render } from 'ink-testing-library';
|
|
14
|
+
import React from 'react';
|
|
15
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
16
|
+
|
|
17
|
+
import { useSideEffects } from '../hooks/useSideEffects.js';
|
|
18
|
+
import { useStatusLineSettings } from '../hooks/useStatusLineSettings.js';
|
|
19
|
+
import { TuiCliAdapterProvider } from '../tui-cli-adapter-context.js';
|
|
20
|
+
import { TUI_SESSION_EVENT_CLASSIFICATION } from '../tui-session-events.js';
|
|
21
|
+
|
|
22
|
+
import type { IUseSideEffectsOptions, IUseSideEffectsResult } from '../hooks/side-effects-types.js';
|
|
23
|
+
import type { ITuiCliAdapter } from '../tui-cli-adapter.js';
|
|
24
|
+
import type { TUniversalValue } from '@robota-sdk/agent-core';
|
|
25
|
+
|
|
26
|
+
const FRAME_DEADLINE_MS = 3000;
|
|
27
|
+
|
|
28
|
+
async function waitUntil(predicate: () => boolean): Promise<void> {
|
|
29
|
+
const deadline = Date.now() + FRAME_DEADLINE_MS;
|
|
30
|
+
while (Date.now() < deadline) {
|
|
31
|
+
if (predicate()) return;
|
|
32
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
33
|
+
}
|
|
34
|
+
expect(predicate()).toBe(true);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type TSessionListener = (payload: unknown) => void;
|
|
38
|
+
|
|
39
|
+
/** Minimal InteractiveSession stand-in: a typed event emitter (`on`/`off`/`emit`). */
|
|
40
|
+
class FakeSessionEmitter {
|
|
41
|
+
private readonly listeners = new Map<string, Set<TSessionListener>>();
|
|
42
|
+
|
|
43
|
+
on(event: string, handler: TSessionListener): void {
|
|
44
|
+
const set = this.listeners.get(event) ?? new Set<TSessionListener>();
|
|
45
|
+
set.add(handler);
|
|
46
|
+
this.listeners.set(event, set);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
off(event: string, handler: TSessionListener): void {
|
|
50
|
+
this.listeners.get(event)?.delete(handler);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
emit(event: string, payload: unknown): void {
|
|
54
|
+
for (const handler of [...(this.listeners.get(event) ?? [])]) handler(payload);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
listenerCount(event: string): number {
|
|
58
|
+
return this.listeners.get(event)?.size ?? 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
subscribedEvents(): string[] {
|
|
62
|
+
return [...this.listeners].filter(([, handlers]) => handlers.size > 0).map(([event]) => event);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
setName = vi.fn();
|
|
66
|
+
getName = (): string | undefined => undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function fakeCliAdapter(store: () => Record<string, TUniversalValue>): ITuiCliAdapter {
|
|
70
|
+
return {
|
|
71
|
+
getUserSettingsPath: () => '/tmp/fake-settings.json',
|
|
72
|
+
readSettings: () => store(),
|
|
73
|
+
reloadPluginCommandSource: vi.fn(),
|
|
74
|
+
applyActiveModelChange: vi.fn().mockReturnValue({ applied: true }),
|
|
75
|
+
getGitBranch: vi.fn().mockReturnValue(undefined),
|
|
76
|
+
getProviderDisplayName: vi.fn((type: string) => type),
|
|
77
|
+
} as unknown as ITuiCliAdapter;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface IHarness {
|
|
81
|
+
session: FakeSessionEmitter;
|
|
82
|
+
result: () => IUseSideEffectsResult;
|
|
83
|
+
lastFrame: () => string | undefined;
|
|
84
|
+
unmount: () => void;
|
|
85
|
+
spies: {
|
|
86
|
+
setSessionName: ReturnType<typeof vi.fn>;
|
|
87
|
+
refreshStatusLineSettings: ReturnType<typeof vi.fn>;
|
|
88
|
+
refreshAppearanceSettings: ReturnType<typeof vi.fn>;
|
|
89
|
+
openAgentSwitcher: ReturnType<typeof vi.fn>;
|
|
90
|
+
baseHandleSubmit: ReturnType<typeof vi.fn>;
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function mountHarness(): IHarness {
|
|
95
|
+
const session = new FakeSessionEmitter();
|
|
96
|
+
const spies = {
|
|
97
|
+
setSessionName: vi.fn(),
|
|
98
|
+
refreshStatusLineSettings: vi.fn(),
|
|
99
|
+
refreshAppearanceSettings: vi.fn(),
|
|
100
|
+
openAgentSwitcher: vi.fn(),
|
|
101
|
+
baseHandleSubmit: vi.fn(async () => {}),
|
|
102
|
+
};
|
|
103
|
+
const resultRef: { current: IUseSideEffectsResult | null } = { current: null };
|
|
104
|
+
|
|
105
|
+
// Superset options object: carries both the pre-Stage-C fields (queue, addEntry, …) and the
|
|
106
|
+
// Stage-C fields so the SAME test file compiled red (pre) and green (post).
|
|
107
|
+
const options = {
|
|
108
|
+
cwd: '/tmp',
|
|
109
|
+
uiEventPort: session,
|
|
110
|
+
commandEffectQueue: {
|
|
111
|
+
enqueueEffects: () => undefined,
|
|
112
|
+
drain: () => undefined,
|
|
113
|
+
clear: () => undefined,
|
|
114
|
+
},
|
|
115
|
+
addEntry: vi.fn(),
|
|
116
|
+
baseHandleSubmit: spies.baseHandleSubmit,
|
|
117
|
+
setSessionName: spies.setSessionName,
|
|
118
|
+
setStatusLineSettings: vi.fn(),
|
|
119
|
+
refreshStatusLineSettings: spies.refreshStatusLineSettings,
|
|
120
|
+
refreshAppearanceSettings: spies.refreshAppearanceSettings,
|
|
121
|
+
showSessionPickerOnStart: false,
|
|
122
|
+
openAgentSwitcher: spies.openAgentSwitcher,
|
|
123
|
+
} as unknown as IUseSideEffectsOptions;
|
|
124
|
+
|
|
125
|
+
function Probe(): React.ReactElement {
|
|
126
|
+
const state = useSideEffects(options);
|
|
127
|
+
resultRef.current = state;
|
|
128
|
+
return (
|
|
129
|
+
<Text>
|
|
130
|
+
{`plugin:${state.showPluginTUI ? 1 : 0} settings:${state.showTransportTUI ? 1 : 0} picker:${state.showSessionPicker ? 1 : 0}`}
|
|
131
|
+
</Text>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const instance = render(
|
|
136
|
+
<TuiCliAdapterProvider value={fakeCliAdapter(() => ({}))}>
|
|
137
|
+
<Probe />
|
|
138
|
+
</TuiCliAdapterProvider>,
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
session,
|
|
143
|
+
result: () => {
|
|
144
|
+
if (!resultRef.current) throw new Error('useSideEffects result not captured');
|
|
145
|
+
return resultRef.current;
|
|
146
|
+
},
|
|
147
|
+
lastFrame: instance.lastFrame,
|
|
148
|
+
unmount: instance.unmount,
|
|
149
|
+
spies,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
describe('ui_intent → TUI screens (requester-routed, CMD-004 Stage C)', () => {
|
|
154
|
+
it('show-settings with the owner requester opens the settings screen', async () => {
|
|
155
|
+
const h = mountHarness();
|
|
156
|
+
h.session.emit('ui_intent', { intent: { type: 'show-settings' }, requesterDriverId: 'owner' });
|
|
157
|
+
await waitUntil(() => (h.lastFrame() ?? '').includes('settings:1'));
|
|
158
|
+
h.unmount();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('show-plugin-manager with the owner requester opens the plugin screen', async () => {
|
|
162
|
+
const h = mountHarness();
|
|
163
|
+
h.session.emit('ui_intent', {
|
|
164
|
+
intent: { type: 'show-plugin-manager' },
|
|
165
|
+
requesterDriverId: 'owner',
|
|
166
|
+
});
|
|
167
|
+
await waitUntil(() => (h.lastFrame() ?? '').includes('plugin:1'));
|
|
168
|
+
h.unmount();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('show-session-picker with the owner requester opens the session picker', async () => {
|
|
172
|
+
const h = mountHarness();
|
|
173
|
+
h.session.emit('ui_intent', {
|
|
174
|
+
intent: { type: 'show-session-picker' },
|
|
175
|
+
requesterDriverId: 'owner',
|
|
176
|
+
});
|
|
177
|
+
await waitUntil(() => (h.lastFrame() ?? '').includes('picker:1'));
|
|
178
|
+
h.unmount();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('show-agent-switcher with the owner requester invokes the switcher callback', async () => {
|
|
182
|
+
const h = mountHarness();
|
|
183
|
+
h.session.emit('ui_intent', {
|
|
184
|
+
intent: { type: 'show-agent-switcher' },
|
|
185
|
+
requesterDriverId: 'owner',
|
|
186
|
+
});
|
|
187
|
+
await waitUntil(() => h.spies.openAgentSwitcher.mock.calls.length === 1);
|
|
188
|
+
h.unmount();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('an intent stamped with ANOTHER surface driver id is ignored (requester-routed)', async () => {
|
|
192
|
+
const h = mountHarness();
|
|
193
|
+
h.session.emit('ui_intent', {
|
|
194
|
+
intent: { type: 'show-settings' },
|
|
195
|
+
requesterDriverId: 'device-remote-1',
|
|
196
|
+
});
|
|
197
|
+
h.session.emit('ui_intent', {
|
|
198
|
+
intent: { type: 'show-agent-switcher' },
|
|
199
|
+
requesterDriverId: 'device-remote-1',
|
|
200
|
+
});
|
|
201
|
+
// Give the (wrong) render a chance to happen, then assert it did not.
|
|
202
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
203
|
+
expect(h.lastFrame()).toContain('settings:0');
|
|
204
|
+
expect(h.spies.openAgentSwitcher).not.toHaveBeenCalled();
|
|
205
|
+
h.unmount();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('an unattributed intent (no requester id) is ignored by this surface', async () => {
|
|
209
|
+
const h = mountHarness();
|
|
210
|
+
h.session.emit('ui_intent', { intent: { type: 'show-settings' } });
|
|
211
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
212
|
+
expect(h.lastFrame()).toContain('settings:0');
|
|
213
|
+
h.unmount();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('unmounting unsubscribes the ui_intent and session_renamed listeners', async () => {
|
|
217
|
+
const h = mountHarness();
|
|
218
|
+
const expected = Object.entries(TUI_SESSION_EVENT_CLASSIFICATION)
|
|
219
|
+
.filter(([, classification]) => classification === 'react-side-effect')
|
|
220
|
+
.map(([event]) => event)
|
|
221
|
+
.sort();
|
|
222
|
+
await waitUntil(() => h.session.subscribedEvents().sort().join() === expected.join());
|
|
223
|
+
h.unmount();
|
|
224
|
+
await waitUntil(() => h.session.subscribedEvents().length === 0);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe('session_renamed broadcast → TUI title (CMD-004 Stage C / TC-10)', () => {
|
|
229
|
+
it('updates the session name from the broadcast without mutating the session itself', async () => {
|
|
230
|
+
const h = mountHarness();
|
|
231
|
+
h.session.emit('session_renamed', { name: 'Renamed via broadcast' });
|
|
232
|
+
await waitUntil(() => h.spies.setSessionName.mock.calls.length === 1);
|
|
233
|
+
expect(h.spies.setSessionName).toHaveBeenCalledWith('Renamed via broadcast');
|
|
234
|
+
// Pure renderer: the TUI reflects the host-executed rename; it never performs it.
|
|
235
|
+
expect(h.session.setName).not.toHaveBeenCalled();
|
|
236
|
+
h.unmount();
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe('statusline refresh-on-result (CMD-004 Stage C)', () => {
|
|
241
|
+
it('re-reads statusline settings after a slash-command result arrives', async () => {
|
|
242
|
+
const h = mountHarness();
|
|
243
|
+
await h.result().handleSubmit('/statusline off');
|
|
244
|
+
expect(h.spies.baseHandleSubmit).toHaveBeenCalledWith('/statusline off');
|
|
245
|
+
expect(h.spies.refreshStatusLineSettings).toHaveBeenCalledTimes(1);
|
|
246
|
+
// SCREEN-2002: the appearance is re-read on the same signal — a `/theme` result has already
|
|
247
|
+
// been written to the settings document by the host when this fires.
|
|
248
|
+
expect(h.spies.refreshAppearanceSettings).toHaveBeenCalledTimes(1);
|
|
249
|
+
h.unmount();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('does not re-read for a plain (non-command) submit', async () => {
|
|
253
|
+
const h = mountHarness();
|
|
254
|
+
await h.result().handleSubmit('hello there');
|
|
255
|
+
expect(h.spies.refreshStatusLineSettings).not.toHaveBeenCalled();
|
|
256
|
+
expect(h.spies.refreshAppearanceSettings).not.toHaveBeenCalled();
|
|
257
|
+
h.unmount();
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
describe('useStatusLineSettings exposes a from-disk refresh', () => {
|
|
262
|
+
it('refresh() re-reads the persisted settings document', async () => {
|
|
263
|
+
let doc: Record<string, TUniversalValue> = { statusline: { enabled: true, gitBranch: true } };
|
|
264
|
+
const adapter = fakeCliAdapter(() => doc);
|
|
265
|
+
const captured: {
|
|
266
|
+
settings: { enabled: boolean; gitBranch: boolean } | null;
|
|
267
|
+
refresh: (() => void) | null;
|
|
268
|
+
} = { settings: null, refresh: null };
|
|
269
|
+
|
|
270
|
+
function Probe(): React.ReactElement {
|
|
271
|
+
const [settings, refresh] = useStatusLineSettings();
|
|
272
|
+
captured.settings = settings;
|
|
273
|
+
captured.refresh = refresh as unknown as () => void;
|
|
274
|
+
return <Text>{`enabled:${settings.enabled ? 1 : 0}`}</Text>;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const instance = render(
|
|
278
|
+
<TuiCliAdapterProvider value={adapter}>
|
|
279
|
+
<Probe />
|
|
280
|
+
</TuiCliAdapterProvider>,
|
|
281
|
+
);
|
|
282
|
+
await waitUntil(() => (instance.lastFrame() ?? '').includes('enabled:1'));
|
|
283
|
+
|
|
284
|
+
// The HOST persisted a patch (Stage B applies it via the settings adapter); the TUI refreshes.
|
|
285
|
+
doc = { statusline: { enabled: false, gitBranch: true } };
|
|
286
|
+
captured.refresh?.();
|
|
287
|
+
await waitUntil(() => (instance.lastFrame() ?? '').includes('enabled:0'));
|
|
288
|
+
expect(captured.settings?.enabled).toBe(false);
|
|
289
|
+
instance.unmount();
|
|
290
|
+
});
|
|
291
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCREEN-006 WaveText token-sourcing tests.
|
|
3
|
+
*
|
|
4
|
+
* WaveText's color ramp comes from the resolved theme's motion tokens (its cadence is its own) in
|
|
5
|
+
* `src/theme/built-in-themes.ts` (not component-private literals), and the gated/static
|
|
6
|
+
* frame must use the canonical muted token. Reduced-motion behavior is
|
|
7
|
+
* re-asserted against the token source: when `isInteractiveColorTerminal()` is
|
|
8
|
+
* false there is no interval and no color churn.
|
|
9
|
+
*/
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import { render } from 'ink-testing-library';
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
|
|
14
|
+
|
|
15
|
+
import WaveText from '../WaveText.js';
|
|
16
|
+
import { ScreenReaderProvider } from '../screen-reader-context.js';
|
|
17
|
+
import { DARK_THEME } from '../theme/built-in-themes.js';
|
|
18
|
+
import { foreground } from '../theme/index.js';
|
|
19
|
+
|
|
20
|
+
/** Cadence is not themed; the component owns it (SCREEN-2002). */
|
|
21
|
+
const WAVE_INTERVAL_MS = 400;
|
|
22
|
+
|
|
23
|
+
const gateMock = vi.hoisted(() => ({ value: true }));
|
|
24
|
+
|
|
25
|
+
vi.mock('../terminal-capabilities.js', () => ({
|
|
26
|
+
isInteractiveColorTerminal: (): boolean => gateMock.value,
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
/** Extract every SGR open-sequence used in a frame (e.g. "38;2;85;85;85"). */
|
|
30
|
+
function sgrCodes(frame: string): string[] {
|
|
31
|
+
// eslint-disable-next-line no-control-regex -- asserting on raw SGR escape bytes by design
|
|
32
|
+
return [...frame.matchAll(/\x1b\[([0-9;]+)m/g)].map((m) => m[1]);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The SGR open code chalk emits for a color, at the level ink uses in this env. */
|
|
36
|
+
function chalkOpenCodes(colorize: (s: string) => string): string[] {
|
|
37
|
+
return sgrCodes(colorize('x'));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const ORIGINAL_CHALK_LEVEL = chalk.level;
|
|
41
|
+
|
|
42
|
+
beforeAll(() => {
|
|
43
|
+
// Pin color bytes regardless of the test process's TTY detection (cjk-fallback precedent).
|
|
44
|
+
chalk.level = 3;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterAll(() => {
|
|
48
|
+
chalk.level = ORIGINAL_CHALK_LEVEL;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
gateMock.value = true;
|
|
53
|
+
vi.useRealTimers();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('WaveText motion tokens (SCREEN-006)', () => {
|
|
57
|
+
it('animated frames draw only from the theme’s wave ramp', () => {
|
|
58
|
+
gateMock.value = true;
|
|
59
|
+
const allowed = new Set<string>(['39', '0']);
|
|
60
|
+
for (const stop of DARK_THEME.motion.wave) {
|
|
61
|
+
for (const code of chalkOpenCodes((s) => chalk.hex(stop)(s))) {
|
|
62
|
+
allowed.add(code);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const { lastFrame, unmount } = render(<WaveText text="Thinking" />);
|
|
67
|
+
const frame = lastFrame() ?? '';
|
|
68
|
+
unmount();
|
|
69
|
+
|
|
70
|
+
const used = sgrCodes(frame);
|
|
71
|
+
expect(used.length).toBeGreaterThan(0);
|
|
72
|
+
const outside = used.filter((code) => !allowed.has(code));
|
|
73
|
+
expect(outside).toEqual([]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('gated/static frame renders with the theme’s muted token and no color churn', () => {
|
|
77
|
+
gateMock.value = false;
|
|
78
|
+
vi.useFakeTimers();
|
|
79
|
+
|
|
80
|
+
const { lastFrame, unmount } = render(<WaveText text="Thinking" />);
|
|
81
|
+
const first = lastFrame() ?? '';
|
|
82
|
+
|
|
83
|
+
// The muted token (not a wave hex stop) styles the static frame.
|
|
84
|
+
const mutedCodes = chalkOpenCodes((s) => foreground(DARK_THEME.colors.text.muted)(s));
|
|
85
|
+
for (const code of mutedCodes) {
|
|
86
|
+
expect(first).toContain(`\u001b[${code}m`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// No interval ⇒ no motion: advancing time produces the identical frame.
|
|
90
|
+
vi.advanceTimersByTime(WAVE_INTERVAL_MS * 5);
|
|
91
|
+
const later = lastFrame() ?? '';
|
|
92
|
+
unmount();
|
|
93
|
+
expect(later).toBe(first);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* CLI-2004 TC-19 — a spinner is a repaint per frame, and a repaint is an announcement. In the mode
|
|
99
|
+
* the text is written once and no interval is ever scheduled.
|
|
100
|
+
*/
|
|
101
|
+
describe('CLI-2004 TC-19: WaveText in screen-reader mode', () => {
|
|
102
|
+
it('renders its text once and schedules no interval', () => {
|
|
103
|
+
gateMock.value = true; // the colour gate is OPEN — the mode alone must stop the animation
|
|
104
|
+
vi.useFakeTimers();
|
|
105
|
+
|
|
106
|
+
const { lastFrame, unmount } = render(
|
|
107
|
+
<ScreenReaderProvider enabled={true}>
|
|
108
|
+
<WaveText text="Thinking" />
|
|
109
|
+
</ScreenReaderProvider>,
|
|
110
|
+
);
|
|
111
|
+
const first = lastFrame() ?? '';
|
|
112
|
+
|
|
113
|
+
// No pending timer at all — not merely a frame that happens to look the same.
|
|
114
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
115
|
+
|
|
116
|
+
vi.advanceTimersByTime(WAVE_INTERVAL_MS * 5);
|
|
117
|
+
const later = lastFrame() ?? '';
|
|
118
|
+
unmount();
|
|
119
|
+
|
|
120
|
+
expect(first).toContain('Thinking');
|
|
121
|
+
expect(later).toBe(first);
|
|
122
|
+
expect(sgrCodes(first).filter((code) => code !== '0' && code !== '39')).toEqual([]);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('leaves the 400 ms ramp exactly as it is outside the mode', () => {
|
|
126
|
+
gateMock.value = true;
|
|
127
|
+
vi.useFakeTimers();
|
|
128
|
+
|
|
129
|
+
const { unmount } = render(<WaveText text="Thinking" />);
|
|
130
|
+
expect(vi.getTimerCount()).toBeGreaterThan(0);
|
|
131
|
+
unmount();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The startup banner, committed once to the terminal scrollback.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `App.tsx` under CLI-2004: the art is ASCII, not box-drawing, so a
|
|
5
|
+
* no-box-drawing sweep passes over it unchanged — it needs its own suppression AND its own
|
|
6
|
+
* assertion, and neither is reviewable while the glyphs live inline in a 600-line component.
|
|
7
|
+
* `BANNER_GLYPHS` is the set a test asserts is absent in screen-reader mode.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { Box } from 'ink';
|
|
11
|
+
import React from 'react';
|
|
12
|
+
|
|
13
|
+
import { Text } from './SafeText.js';
|
|
14
|
+
import { usePalette } from './theme/index.js';
|
|
15
|
+
|
|
16
|
+
/** The ASCII art itself. */
|
|
17
|
+
export const BANNER_ART = `
|
|
18
|
+
____ ___ ____ ___ _____ _
|
|
19
|
+
| _ \\ / _ \\| __ ) / _ \\_ _|/ \\
|
|
20
|
+
| |_) | | | | _ \\| | | || | / _ \\
|
|
21
|
+
| _ <| |_| | |_) | |_| || |/ ___ \\
|
|
22
|
+
|_| \\_\\\\___/|____/ \\___/ |_/_/ \\_\\
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The characters the art is drawn from. None is a box-drawing character, which is exactly why the
|
|
27
|
+
* banner needs a suppression test of its own.
|
|
28
|
+
*/
|
|
29
|
+
export const BANNER_GLYPHS: readonly string[] = ['_', '|', '\\', '/', '<'];
|
|
30
|
+
|
|
31
|
+
/** The banner as it is committed to scrollback: the art plus the version line. */
|
|
32
|
+
export function AppBanner({ version }: { version: string }): React.ReactElement {
|
|
33
|
+
const palette = usePalette();
|
|
34
|
+
return (
|
|
35
|
+
<Box flexDirection="column" paddingX={1} marginBottom={1}>
|
|
36
|
+
<Text color={palette.text.accent} bold>
|
|
37
|
+
{BANNER_ART}
|
|
38
|
+
</Text>
|
|
39
|
+
<Text dimColor> v{version}</Text>
|
|
40
|
+
</Box>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCREEN-010 / CLI-2004 — what gets committed to the terminal's native scrollback.
|
|
3
|
+
*
|
|
4
|
+
* The App renders exactly this list inside a single Ink `<Static>`, so each item is written once and
|
|
5
|
+
* never repainted. Building the list is a pure function so the ONE decision that varies — whether
|
|
6
|
+
* the startup banner is committed at all — is assertable without mounting the App.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { IHistoryEntry } from '@robota-sdk/agent-core';
|
|
10
|
+
|
|
11
|
+
/** A committed item: the startup banner, or one conversation entry. */
|
|
12
|
+
export type TStaticItem =
|
|
13
|
+
| { readonly kind: 'banner'; readonly version: string }
|
|
14
|
+
| { readonly kind: 'entry'; readonly entry: IHistoryEntry };
|
|
15
|
+
|
|
16
|
+
export interface IBuildStaticItemsInputs {
|
|
17
|
+
history: readonly IHistoryEntry[];
|
|
18
|
+
version: string | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Screen-reader mode. The banner is ASCII art with no spoken meaning — a reader announces it
|
|
21
|
+
* character by character before the session has said anything — so the mode omits the item
|
|
22
|
+
* entirely rather than styling it differently.
|
|
23
|
+
*/
|
|
24
|
+
screenReader: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const FALLBACK_VERSION = '0.0.0';
|
|
28
|
+
|
|
29
|
+
/** Build the `<Static>` item list. */
|
|
30
|
+
export function buildStaticItems(inputs: IBuildStaticItemsInputs): TStaticItem[] {
|
|
31
|
+
const banner: TStaticItem[] = inputs.screenReader
|
|
32
|
+
? []
|
|
33
|
+
: [{ kind: 'banner', version: inputs.version ?? FALLBACK_VERSION }];
|
|
34
|
+
return [...banner, ...inputs.history.map((entry): TStaticItem => ({ kind: 'entry', entry }))];
|
|
35
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { TStaticItem } from './app-static-items.js';
|
|
2
|
+
import type { IAppThemeViewModel } from './hooks/useAppThemeState.js';
|
|
3
|
+
import type { IInputAreaHistorySearch } from './hooks/useInputAreaHistorySearch.js';
|
|
4
|
+
import type { ITuiCommandQueryPort } from './tui-app-channel-port.js';
|
|
5
|
+
import type { ITuiSessionEventNotice } from './tui-session-events.js';
|
|
6
|
+
import type { IPendingPermissionRequest } from './types.js';
|
|
7
|
+
import type {
|
|
8
|
+
IActionRequest,
|
|
9
|
+
IHistoryEntry,
|
|
10
|
+
TActionResponse,
|
|
11
|
+
TModelEffortSelection,
|
|
12
|
+
TPermissionMode,
|
|
13
|
+
} from '@robota-sdk/agent-core';
|
|
14
|
+
import type {
|
|
15
|
+
ICommandPluginAdapter,
|
|
16
|
+
IStatusLineCommandSettings,
|
|
17
|
+
} from '@robota-sdk/agent-interface-command';
|
|
18
|
+
import type {
|
|
19
|
+
IExecutionDetailPage,
|
|
20
|
+
IExecutionWorkspaceEntry,
|
|
21
|
+
IExecutionWorkspaceSnapshot,
|
|
22
|
+
} from '@robota-sdk/agent-interface-execution';
|
|
23
|
+
import type {
|
|
24
|
+
IResumableSessionSummary,
|
|
25
|
+
IToolState,
|
|
26
|
+
} from '@robota-sdk/agent-interface-session';
|
|
27
|
+
import type { ITransportRegistryView } from '@robota-sdk/agent-interface-transport';
|
|
28
|
+
|
|
29
|
+
export interface IAppViewModel {
|
|
30
|
+
readonly staticItems: TStaticItem[];
|
|
31
|
+
readonly handoffSuspended: boolean;
|
|
32
|
+
readonly updateNotice: string | undefined;
|
|
33
|
+
readonly coordinationError: string | undefined;
|
|
34
|
+
readonly coordinationPending: boolean;
|
|
35
|
+
readonly sessionEventNotices: readonly ITuiSessionEventNotice[];
|
|
36
|
+
readonly isShuttingDown: boolean;
|
|
37
|
+
readonly stream: {
|
|
38
|
+
readonly text: string;
|
|
39
|
+
readonly activeTools: readonly IToolState[];
|
|
40
|
+
readonly isThinking: boolean;
|
|
41
|
+
readonly isStalled: boolean;
|
|
42
|
+
readonly lastErrorMessage: string | null;
|
|
43
|
+
};
|
|
44
|
+
readonly background: IAppBackgroundViewModel;
|
|
45
|
+
readonly permissionRequest: IPendingPermissionRequest | null;
|
|
46
|
+
readonly pendingUserAction: IActionRequest | null;
|
|
47
|
+
readonly resolveUserAction: (request: IActionRequest, response: TActionResponse) => void;
|
|
48
|
+
readonly plugin: IAppPluginViewModel;
|
|
49
|
+
readonly transport: IAppTransportViewModel;
|
|
50
|
+
readonly sessionPicker: IAppSessionPickerViewModel;
|
|
51
|
+
/** SCREEN-2002: the live theme, the resolved motion setting, and the picker over them. */
|
|
52
|
+
readonly theme: IAppThemeViewModel;
|
|
53
|
+
readonly contextPercentage: number;
|
|
54
|
+
readonly input: IAppInputViewModel;
|
|
55
|
+
readonly status: IAppStatusViewModel;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type { IAppThemePickerViewModel, IAppThemeViewModel } from './hooks/useAppThemeState.js';
|
|
59
|
+
|
|
60
|
+
export interface IAppBackgroundViewModel {
|
|
61
|
+
readonly entries: IExecutionWorkspaceEntry[];
|
|
62
|
+
readonly focusedIndex: number | null;
|
|
63
|
+
readonly selectedEntry: IExecutionWorkspaceEntry | undefined;
|
|
64
|
+
readonly detail: {
|
|
65
|
+
readonly page: IExecutionDetailPage | null;
|
|
66
|
+
readonly loading: boolean;
|
|
67
|
+
readonly error: string | undefined;
|
|
68
|
+
};
|
|
69
|
+
readonly switcherVisible: boolean;
|
|
70
|
+
readonly snapshot: IExecutionWorkspaceSnapshot | null;
|
|
71
|
+
readonly selectedEntryId: string | undefined;
|
|
72
|
+
readonly select: (entryId: string) => void;
|
|
73
|
+
readonly closeSwitcher: () => void;
|
|
74
|
+
readonly attachToFork: (entry: IExecutionWorkspaceEntry) => void;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface IAppPluginViewModel {
|
|
78
|
+
readonly visible: boolean;
|
|
79
|
+
readonly callbacks: ICommandPluginAdapter;
|
|
80
|
+
readonly close: () => void;
|
|
81
|
+
readonly addMessage: (content: string) => void;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface IAppTransportViewModel {
|
|
85
|
+
readonly visible: boolean;
|
|
86
|
+
readonly registry: ITransportRegistryView | undefined;
|
|
87
|
+
readonly close: () => void;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface IAppSessionPickerViewModel {
|
|
91
|
+
readonly visible: boolean;
|
|
92
|
+
readonly sessions: readonly IResumableSessionSummary[];
|
|
93
|
+
readonly select: (sessionId: string) => void;
|
|
94
|
+
readonly cancel: () => void;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface IAppInputViewModel {
|
|
98
|
+
/** FLOW-2006: the deep link's prefill, present only until the composer takes it. */
|
|
99
|
+
readonly initialValue?: string | undefined;
|
|
100
|
+
/** Called by the composer the first time it seeds itself, so a remount does not re-seed. */
|
|
101
|
+
readonly consumeInitialValue?: (() => void) | undefined;
|
|
102
|
+
/** True while the composer's text is the one a deep link supplied. */
|
|
103
|
+
readonly externalPromptOrigin?: boolean | undefined;
|
|
104
|
+
readonly submit: (input: string) => Promise<void>;
|
|
105
|
+
readonly cancelQueue: () => void;
|
|
106
|
+
readonly disabled: boolean;
|
|
107
|
+
readonly queueCancellationDisabled: boolean;
|
|
108
|
+
readonly isAborting: boolean;
|
|
109
|
+
readonly pendingPrompt: string | null;
|
|
110
|
+
readonly pendingCount: number;
|
|
111
|
+
readonly commandQueryPort: ITuiCommandQueryPort;
|
|
112
|
+
readonly sessionName: string | undefined;
|
|
113
|
+
readonly history: readonly IHistoryEntry[];
|
|
114
|
+
readonly focusBackgroundList: () => void;
|
|
115
|
+
/** SCREEN-1993: the stored-prompt search surface; absent ⇒ the feature is off. */
|
|
116
|
+
readonly historySearch: IInputAreaHistorySearch | undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface IAppStatusViewModel {
|
|
120
|
+
readonly cwd: string;
|
|
121
|
+
readonly permissionMode: TPermissionMode;
|
|
122
|
+
readonly modelId: string | undefined;
|
|
123
|
+
readonly providerType: string | undefined;
|
|
124
|
+
readonly sessionId: string;
|
|
125
|
+
readonly isThinking: boolean;
|
|
126
|
+
readonly activeToolCount: number;
|
|
127
|
+
readonly activeBackgroundTaskCount: number;
|
|
128
|
+
readonly hasPendingPrompt: boolean;
|
|
129
|
+
readonly contextState: { percentage: number; usedTokens: number; maxTokens: number };
|
|
130
|
+
readonly sessionName: string | undefined;
|
|
131
|
+
readonly settings: IStatusLineCommandSettings;
|
|
132
|
+
readonly activeAgentLabel: string | undefined;
|
|
133
|
+
readonly activePresetId: string | undefined;
|
|
134
|
+
readonly effort: TModelEffortSelection | undefined;
|
|
135
|
+
readonly gitRefreshToken: number;
|
|
136
|
+
}
|