@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,98 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { render } from 'ink-testing-library';
|
|
4
|
+
import type { IExecutionWorkspaceEntry } from '@robota-sdk/agent-interface-execution';
|
|
5
|
+
import BackgroundTaskPanel from '../BackgroundTaskPanel.js';
|
|
6
|
+
|
|
7
|
+
function makeEntry(overrides: Partial<IExecutionWorkspaceEntry>): IExecutionWorkspaceEntry {
|
|
8
|
+
return {
|
|
9
|
+
id: 'task:agent_1',
|
|
10
|
+
sourceId: 'agent_1',
|
|
11
|
+
kind: 'background_task',
|
|
12
|
+
origin: { kind: 'slash_command', sessionId: 'session_1', commandName: 'agent' },
|
|
13
|
+
taskKind: 'agent',
|
|
14
|
+
status: 'running',
|
|
15
|
+
state: 'working',
|
|
16
|
+
title: 'general-purpose',
|
|
17
|
+
subtitle: 'agent',
|
|
18
|
+
preview: 'Analyze backlog',
|
|
19
|
+
unread: false,
|
|
20
|
+
attention: 'none',
|
|
21
|
+
visibility: 'default',
|
|
22
|
+
updatedAt: '2026-05-09T00:00:00.000Z',
|
|
23
|
+
controls: ['select', 'cancel'],
|
|
24
|
+
...overrides,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('BackgroundTaskPanel', () => {
|
|
29
|
+
it('renders SDK workspace entries with compact markers instead of raw task ids', () => {
|
|
30
|
+
const { lastFrame } = render(
|
|
31
|
+
<BackgroundTaskPanel
|
|
32
|
+
entries={[
|
|
33
|
+
makeEntry({ id: 'task:agent_1', status: 'running' }),
|
|
34
|
+
makeEntry({
|
|
35
|
+
id: 'task:agent_2',
|
|
36
|
+
status: 'completed',
|
|
37
|
+
state: 'completed',
|
|
38
|
+
preview: 'Done',
|
|
39
|
+
}),
|
|
40
|
+
makeEntry({
|
|
41
|
+
id: 'task:agent_3',
|
|
42
|
+
status: 'failed',
|
|
43
|
+
state: 'failed',
|
|
44
|
+
attention: 'failed',
|
|
45
|
+
preview: 'Timed out',
|
|
46
|
+
}),
|
|
47
|
+
]}
|
|
48
|
+
/>,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const frame = lastFrame()!;
|
|
52
|
+
expect(frame).toContain('Background work');
|
|
53
|
+
// SCREEN-1992: the state word sits beside the glyph, so a reader hears it and no-color sees it.
|
|
54
|
+
expect(frame).toContain('├ ⟳ working general-purpose agent');
|
|
55
|
+
expect(frame).toContain('├ ✓ completed general-purpose agent · completed');
|
|
56
|
+
expect(frame).toContain('└ ✗ failed general-purpose agent · failed');
|
|
57
|
+
expect(frame).not.toContain('agent_1');
|
|
58
|
+
expect(frame).not.toContain('agent_2');
|
|
59
|
+
expect(frame).not.toContain('agent_3');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('advertises the keyboard drill-in when not focused (SCREEN-013/014)', () => {
|
|
63
|
+
const { lastFrame } = render(
|
|
64
|
+
<BackgroundTaskPanel entries={[makeEntry({ id: 'task:agent_1' })]} />,
|
|
65
|
+
);
|
|
66
|
+
const frame = lastFrame()!;
|
|
67
|
+
// Entry affordance: ↓ to select, Ctrl+B for the full switcher.
|
|
68
|
+
expect(frame).toContain('select');
|
|
69
|
+
expect(frame).toContain('Ctrl+B');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('shows the move/open affordance when a row is focused (SCREEN-014)', () => {
|
|
73
|
+
const { lastFrame } = render(
|
|
74
|
+
<BackgroundTaskPanel
|
|
75
|
+
entries={[makeEntry({ id: 'task:agent_1' }), makeEntry({ id: 'task:agent_2' })]}
|
|
76
|
+
focusedIndex={0}
|
|
77
|
+
/>,
|
|
78
|
+
);
|
|
79
|
+
const frame = lastFrame()!;
|
|
80
|
+
expect(frame).toContain('Enter open');
|
|
81
|
+
expect(frame).toContain('Esc back');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('keeps a long-preview row on a single line (SCREEN-011)', () => {
|
|
85
|
+
const longPreview =
|
|
86
|
+
'Read each of the following backlog files and analyze the current status, implementation ' +
|
|
87
|
+
'difficulty, dependencies, and estimated effort for every item; output a structured summary';
|
|
88
|
+
const { lastFrame } = render(
|
|
89
|
+
<BackgroundTaskPanel entries={[makeEntry({ id: 'task:agent_1', preview: longPreview })]} />,
|
|
90
|
+
);
|
|
91
|
+
const frame = lastFrame()!;
|
|
92
|
+
// The row with the connector must be exactly one line — no wrapped continuation line.
|
|
93
|
+
const rowLines = frame.split('\n').filter((line) => line.includes('⟳'));
|
|
94
|
+
expect(rowLines).toHaveLength(1);
|
|
95
|
+
// The connector + glyph still lead the single row line.
|
|
96
|
+
expect(rowLines[0]).toMatch(/└ ⟳ working general-purpose agent/);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { IExecutionWorkspaceEntry } from '@robota-sdk/agent-interface-execution';
|
|
3
|
+
import {
|
|
4
|
+
BACKGROUND_TASK_CONNECTORS,
|
|
5
|
+
formatBackgroundTaskRow,
|
|
6
|
+
} from '../background-task-row-format.js';
|
|
7
|
+
|
|
8
|
+
function makeEntry(overrides: Partial<IExecutionWorkspaceEntry>): IExecutionWorkspaceEntry {
|
|
9
|
+
return {
|
|
10
|
+
id: 'task:agent_1',
|
|
11
|
+
sourceId: 'agent_1',
|
|
12
|
+
kind: 'background_task',
|
|
13
|
+
origin: { kind: 'slash_command', sessionId: 'session_1', commandName: 'agent' },
|
|
14
|
+
taskKind: 'agent',
|
|
15
|
+
status: 'running',
|
|
16
|
+
state: 'working',
|
|
17
|
+
title: 'Explore',
|
|
18
|
+
subtitle: 'general-purpose',
|
|
19
|
+
preview: 'Analyze backlog',
|
|
20
|
+
unread: false,
|
|
21
|
+
attention: 'none',
|
|
22
|
+
visibility: 'default',
|
|
23
|
+
updatedAt: '2026-05-09T00:00:00.000Z',
|
|
24
|
+
controls: ['select', 'cancel'],
|
|
25
|
+
...overrides,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('formatBackgroundTaskRow', () => {
|
|
30
|
+
it('formats running SDK workspace entries without raw task ids', () => {
|
|
31
|
+
const row = formatBackgroundTaskRow(makeEntry({ id: 'task:agent_1' }), { isLast: true });
|
|
32
|
+
|
|
33
|
+
expect(row.connector).toBe('└');
|
|
34
|
+
expect(row.marker).toBe('⟳'); // running → shared status glyph (SCREEN-005)
|
|
35
|
+
// marker symbol AND colour come from the same glyph now (SCREEN-007): the running
|
|
36
|
+
// glyph is yellow, so the row colour is yellow (was cyan via the old view-model rule).
|
|
37
|
+
expect(row.statusKind).toBe('running');
|
|
38
|
+
expect(row.label).toBe('Explore agent');
|
|
39
|
+
expect(row.segments).toEqual(['running', 'agent · general-purpose']);
|
|
40
|
+
expect(row.preview).toBe('Analyze backlog');
|
|
41
|
+
expect(row.accessibleText).not.toContain('agent_1');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('formats failed and completed rows from SDK-owned status and attention', () => {
|
|
45
|
+
const failed = formatBackgroundTaskRow(
|
|
46
|
+
makeEntry({
|
|
47
|
+
id: 'task:agent_2',
|
|
48
|
+
status: 'failed',
|
|
49
|
+
attention: 'failed',
|
|
50
|
+
preview: 'Timed out',
|
|
51
|
+
}),
|
|
52
|
+
{ isLast: false },
|
|
53
|
+
);
|
|
54
|
+
const completed = formatBackgroundTaskRow(
|
|
55
|
+
makeEntry({ id: 'task:agent_3', status: 'completed', preview: 'Summary ready' }),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
expect(failed.connector).toBe('├');
|
|
59
|
+
expect(failed.marker).toBe('✗'); // failed → error glyph (SCREEN-005)
|
|
60
|
+
expect(failed.statusKind).toBe('error');
|
|
61
|
+
expect(failed.preview).toBe('Timed out');
|
|
62
|
+
expect(completed.marker).toBe('✓'); // completed → success glyph (SCREEN-005)
|
|
63
|
+
expect(completed.statusKind).toBe('success');
|
|
64
|
+
expect(completed.preview).toBe('Summary ready');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
/** CLI-2004: the tree connectors are drawing, and a reader announces them on every row. */
|
|
69
|
+
describe('formatBackgroundTaskRow in screen-reader mode', () => {
|
|
70
|
+
it('uses the plain connector instead of the box-drawing branch/last glyphs', () => {
|
|
71
|
+
const branch = formatBackgroundTaskRow(makeEntry({}), { isLast: false, screenReader: true });
|
|
72
|
+
const last = formatBackgroundTaskRow(makeEntry({}), { isLast: true, screenReader: true });
|
|
73
|
+
|
|
74
|
+
expect(branch.connector).toBe(BACKGROUND_TASK_CONNECTORS.plain);
|
|
75
|
+
expect(last.connector).toBe(BACKGROUND_TASK_CONNECTORS.plain);
|
|
76
|
+
expect(branch.accessibleText.startsWith(BACKGROUND_TASK_CONNECTORS.plain)).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('keeps the tree connectors outside the mode', () => {
|
|
80
|
+
expect(formatBackgroundTaskRow(makeEntry({}), { isLast: false }).connector).toBe(
|
|
81
|
+
BACKGROUND_TASK_CONNECTORS.branch,
|
|
82
|
+
);
|
|
83
|
+
expect(formatBackgroundTaskRow(makeEntry({}), { isLast: true }).connector).toBe(
|
|
84
|
+
BACKGROUND_TASK_CONNECTORS.last,
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
/** SCREEN-1992 TC-07: the state word beside the glyph, the headline and the countdown. */
|
|
90
|
+
describe('formatBackgroundTaskRow state, headline and countdown', () => {
|
|
91
|
+
const now = new Date('2026-01-01T00:00:00.000Z');
|
|
92
|
+
|
|
93
|
+
it('places the state word beside the glyph and carries it in the accessible text', () => {
|
|
94
|
+
const row = formatBackgroundTaskRow(makeEntry({ status: 'cancelled', state: 'stopped' }), {
|
|
95
|
+
isLast: true,
|
|
96
|
+
now,
|
|
97
|
+
});
|
|
98
|
+
expect(row.state).toBe('stopped');
|
|
99
|
+
expect(row.accessibleText.startsWith('└ ⊗ stopped Explore agent')).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('adds the headline only when it says more than the preview, marking a question', () => {
|
|
103
|
+
const same = formatBackgroundTaskRow(
|
|
104
|
+
makeEntry({ headline: { kind: 'activity', text: 'Analyze backlog' } }),
|
|
105
|
+
{ now },
|
|
106
|
+
);
|
|
107
|
+
expect(same.headline).toBeUndefined();
|
|
108
|
+
const question = formatBackgroundTaskRow(
|
|
109
|
+
makeEntry({
|
|
110
|
+
status: 'waiting_permission',
|
|
111
|
+
state: 'needs-input',
|
|
112
|
+
attention: 'permission',
|
|
113
|
+
headline: { kind: 'question', text: 'Allow Bash?' },
|
|
114
|
+
}),
|
|
115
|
+
{ now },
|
|
116
|
+
);
|
|
117
|
+
expect(question.headline).toBe('? Allow Bash?');
|
|
118
|
+
expect(question.accessibleText).toContain('needs-input');
|
|
119
|
+
expect(question.accessibleText).toContain('? Allow Bash?');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('renders a countdown against the given clock for a sleeping schedule only', () => {
|
|
123
|
+
const sleeping = formatBackgroundTaskRow(
|
|
124
|
+
makeEntry({
|
|
125
|
+
taskKind: 'scheduled',
|
|
126
|
+
status: 'sleeping',
|
|
127
|
+
state: 'working',
|
|
128
|
+
nextFireAt: '2026-01-01T00:04:59.000Z',
|
|
129
|
+
}),
|
|
130
|
+
{ now },
|
|
131
|
+
);
|
|
132
|
+
expect(sleeping.countdown).toBe('in 4m 59s');
|
|
133
|
+
expect(sleeping.accessibleText.endsWith('in 4m 59s')).toBe(true);
|
|
134
|
+
expect(formatBackgroundTaskRow(makeEntry({}), { now }).countdown).toBeUndefined();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('formatBackgroundTaskRow headline dedup against the subtitle (PR #2749 review)', () => {
|
|
139
|
+
it('drops a headline the subtitle already carries, such as a wake instruction', () => {
|
|
140
|
+
const row = formatBackgroundTaskRow(
|
|
141
|
+
makeEntry({
|
|
142
|
+
taskKind: 'scheduled',
|
|
143
|
+
status: 'sleeping',
|
|
144
|
+
subtitle: '↻ wake "say hello"',
|
|
145
|
+
preview: undefined,
|
|
146
|
+
headline: { kind: 'activity', text: 'say hello' },
|
|
147
|
+
}),
|
|
148
|
+
{ now: new Date('2026-01-01T00:00:00.000Z') },
|
|
149
|
+
);
|
|
150
|
+
expect(row.headline).toBeUndefined();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI-B11 TC-02: real-store channel factory integration.
|
|
3
|
+
*
|
|
4
|
+
* The official CI equivalent of real-resume-verify-v3.mjs: build the channel
|
|
5
|
+
* exactly the way render.tsx does (toChannelOptions + TuiInteractionChannel)
|
|
6
|
+
* over a REAL project session store with a persisted conversation, and assert
|
|
7
|
+
* the restored model context is non-empty. No store/session mocks.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { mkdtempSync, rmSync, realpathSync } from 'node:fs';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
|
|
14
|
+
import { createNodeHostSessionStore } from '@robota-sdk/agent-framework';
|
|
15
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
16
|
+
|
|
17
|
+
import { createScriptedProvider } from '@robota-sdk/agent-core/testing';
|
|
18
|
+
import { toChannelOptions, type IRenderOptions } from '../render.js';
|
|
19
|
+
import { TuiInteractionChannel } from '../TuiInteractionChannel.js';
|
|
20
|
+
|
|
21
|
+
import type { ITuiCliAdapter } from '../tui-cli-adapter.js';
|
|
22
|
+
import type { TUniversalMessage } from '@robota-sdk/agent-core';
|
|
23
|
+
import type { IInteractiveSessionStore } from '@robota-sdk/agent-interface-session';
|
|
24
|
+
|
|
25
|
+
const RESTORE_DEADLINE_MS = 10_000;
|
|
26
|
+
const POLL_MS = 50;
|
|
27
|
+
|
|
28
|
+
function fakeCliAdapter(settingsPath: string): ITuiCliAdapter {
|
|
29
|
+
return {
|
|
30
|
+
getUserSettingsPath: () => settingsPath,
|
|
31
|
+
readSettings: () => ({}),
|
|
32
|
+
reloadPluginCommandSource: vi.fn(),
|
|
33
|
+
applyActiveModelChange: vi.fn().mockReturnValue({ applied: true }),
|
|
34
|
+
getGitBranch: vi.fn().mockReturnValue(undefined),
|
|
35
|
+
getProviderDisplayName: vi.fn((type: string) => type),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function persistConversation(store: IInteractiveSessionStore, id: string, cwd: string): void {
|
|
40
|
+
// TRANS-007: the store decodes on load, so a message must satisfy the contract. These were cast
|
|
41
|
+
// stubs with only `role` and `content`; the store accepted them because it never looked.
|
|
42
|
+
const at = new Date('2026-06-13T00:00:00.000Z');
|
|
43
|
+
const messages: TUniversalMessage[] = [
|
|
44
|
+
{
|
|
45
|
+
id: 'm-0',
|
|
46
|
+
role: 'user',
|
|
47
|
+
content: 'Remember the number 42.',
|
|
48
|
+
timestamp: at,
|
|
49
|
+
state: 'complete',
|
|
50
|
+
},
|
|
51
|
+
{ id: 'm-1', role: 'assistant', content: 'Noted: 42.', timestamp: at, state: 'complete' },
|
|
52
|
+
{
|
|
53
|
+
id: 'm-2',
|
|
54
|
+
role: 'user',
|
|
55
|
+
content: 'And the city is Busan.',
|
|
56
|
+
timestamp: at,
|
|
57
|
+
state: 'complete',
|
|
58
|
+
},
|
|
59
|
+
{ id: 'm-3', role: 'assistant', content: 'Noted: Busan.', timestamp: at, state: 'complete' },
|
|
60
|
+
];
|
|
61
|
+
store.save({
|
|
62
|
+
id,
|
|
63
|
+
cwd,
|
|
64
|
+
createdAt: '2026-06-13T00:00:00.000Z',
|
|
65
|
+
updatedAt: '2026-06-13T00:00:00.000Z',
|
|
66
|
+
messages,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
describe('channel factory restores persisted context (CLI-B11 TC-02)', () => {
|
|
71
|
+
let cwd: string;
|
|
72
|
+
let channel: TuiInteractionChannel | undefined;
|
|
73
|
+
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
cwd = realpathSync(mkdtempSync(join(tmpdir(), 'robota-b11-int-')));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
afterEach(async () => {
|
|
79
|
+
await channel?.stop();
|
|
80
|
+
channel = undefined;
|
|
81
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('createChannel(sessionId) over a real FileSessionStore yields usedTokens > 0', async () => {
|
|
85
|
+
const store = createNodeHostSessionStore(join(cwd, '.robota', 'sessions'));
|
|
86
|
+
const sessionId = 'b11-restore-session';
|
|
87
|
+
persistConversation(store, sessionId, cwd);
|
|
88
|
+
|
|
89
|
+
// Exactly the render.tsx factory: toChannelOptions(options, resumeSessionId).
|
|
90
|
+
const scripted = createScriptedProvider([{ text: 'unused in this test' }]);
|
|
91
|
+
const options: IRenderOptions = {
|
|
92
|
+
cwd,
|
|
93
|
+
provider: scripted.provider,
|
|
94
|
+
sessionStore: store,
|
|
95
|
+
cliAdapter: fakeCliAdapter(join(cwd, 'settings.json')),
|
|
96
|
+
};
|
|
97
|
+
channel = new TuiInteractionChannel(toChannelOptions(options, sessionId));
|
|
98
|
+
await channel.start();
|
|
99
|
+
|
|
100
|
+
// Restoration is asynchronous (pendingRestoreMessages inject after init).
|
|
101
|
+
const deadline = Date.now() + RESTORE_DEADLINE_MS;
|
|
102
|
+
let usedTokens = 0;
|
|
103
|
+
while (Date.now() < deadline) {
|
|
104
|
+
try {
|
|
105
|
+
// allow-fallback: session init is asynchronous; poll until it is ready
|
|
106
|
+
usedTokens = channel.getSession().getContextState().usedTokens;
|
|
107
|
+
if (usedTokens > 0) break;
|
|
108
|
+
} catch {
|
|
109
|
+
// allow-fallback: session init is asynchronous; poll until it is ready
|
|
110
|
+
}
|
|
111
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_MS));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// The persisted messages were injected into the model context — the exact
|
|
115
|
+
// signal that was 0 in the 2026-05-31 bug. (getFullHistory() is the display
|
|
116
|
+
// log restored from record.history, which this record intentionally omits.)
|
|
117
|
+
expect(usedTokens).toBeGreaterThan(0);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('a channel created WITHOUT resumeSessionId starts with an empty context (control)', async () => {
|
|
121
|
+
const store = createNodeHostSessionStore(join(cwd, '.robota', 'sessions'));
|
|
122
|
+
persistConversation(store, 'b11-other-session', cwd);
|
|
123
|
+
|
|
124
|
+
const scripted = createScriptedProvider([{ text: 'unused' }]);
|
|
125
|
+
const options: IRenderOptions = {
|
|
126
|
+
cwd,
|
|
127
|
+
provider: scripted.provider,
|
|
128
|
+
sessionStore: store,
|
|
129
|
+
cliAdapter: fakeCliAdapter(join(cwd, 'settings.json')),
|
|
130
|
+
};
|
|
131
|
+
channel = new TuiInteractionChannel(toChannelOptions(options, undefined));
|
|
132
|
+
await channel.start();
|
|
133
|
+
|
|
134
|
+
const deadline = Date.now() + RESTORE_DEADLINE_MS;
|
|
135
|
+
let ready = false;
|
|
136
|
+
while (Date.now() < deadline && !ready) {
|
|
137
|
+
try {
|
|
138
|
+
// allow-fallback: session init is asynchronous; poll until it is ready
|
|
139
|
+
channel.getSession().getContextState();
|
|
140
|
+
ready = true;
|
|
141
|
+
} catch {
|
|
142
|
+
// allow-fallback: session init is asynchronous; poll until it is ready
|
|
143
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_MS));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
expect(ready).toBe(true);
|
|
148
|
+
expect(channel.getSession().getFullHistory()).toHaveLength(0);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { render } from 'ink-testing-library';
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import CjkTextInput from '../CjkTextInput.js';
|
|
6
|
+
import { IME_SUBMIT_DEFER_MS } from '../flows/defer-submit.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* CLI-061 — integration proof that the CjkTextInput defer-submit includes a trailing IME character whose stdin
|
|
10
|
+
* event arrives JUST AFTER Enter. This exercises the real handler + stateRef + defer wiring (not the pure
|
|
11
|
+
* reducer, which by construction only sees the value captured at Enter and would be accidental-green).
|
|
12
|
+
*
|
|
13
|
+
* Real timers + a real wait > IME_SUBMIT_DEFER_MS so the deferred submit actually fires (fake timers conflict
|
|
14
|
+
* with Ink's own render scheduling).
|
|
15
|
+
*
|
|
16
|
+
* That wait is DERIVED from the constant rather than written next to a comment quoting it. Before, this file
|
|
17
|
+
* waited a hard-coded 90ms and said `// > 50ms defer` in prose — so raising IME_SUBMIT_DEFER_MS past 90 would
|
|
18
|
+
* have left the test waiting less than the defer it exists to outlast, and the comment asserting otherwise.
|
|
19
|
+
* The constant was also, by that arrangement, exported and named by nothing but a comment.
|
|
20
|
+
*/
|
|
21
|
+
const tick = (): Promise<void> => new Promise((r) => setTimeout(r, 0));
|
|
22
|
+
const DEFER_WAIT_MARGIN_MS = 40;
|
|
23
|
+
const waitDefer = (): Promise<void> =>
|
|
24
|
+
new Promise((r) => setTimeout(r, IME_SUBMIT_DEFER_MS + DEFER_WAIT_MARGIN_MS));
|
|
25
|
+
|
|
26
|
+
function Harness({ onSubmit }: { onSubmit: (value: string) => void }): React.ReactElement {
|
|
27
|
+
const [value, setValue] = useState('');
|
|
28
|
+
return <CjkTextInput value={value} onChange={setValue} onSubmit={onSubmit} focus />;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('CLI-061 — CjkTextInput defer-submit (integration)', () => {
|
|
32
|
+
it('includes a trailing syllable that arrives after Enter (안녕하세 + Enter + 요 → 안녕하세요)', async () => {
|
|
33
|
+
const onSubmit = vi.fn();
|
|
34
|
+
const { stdin } = render(<Harness onSubmit={onSubmit} />);
|
|
35
|
+
await tick();
|
|
36
|
+
|
|
37
|
+
stdin.write('안녕하세'); // composed so far
|
|
38
|
+
await tick();
|
|
39
|
+
stdin.write('\r'); // Enter — schedules the deferred submit, does NOT submit synchronously
|
|
40
|
+
await tick();
|
|
41
|
+
expect(onSubmit).not.toHaveBeenCalled(); // deferred, not immediate
|
|
42
|
+
|
|
43
|
+
stdin.write('요'); // the final syllable's stdin event lands after Enter; the pipeline is still live
|
|
44
|
+
await tick();
|
|
45
|
+
|
|
46
|
+
await waitDefer(); // the defer window elapses → the deferred submit fires, re-reading the LATEST value
|
|
47
|
+
expect(onSubmit).toHaveBeenCalledTimes(1);
|
|
48
|
+
expect(onSubmit).toHaveBeenCalledWith('안녕하세요');
|
|
49
|
+
}, 10_000);
|
|
50
|
+
|
|
51
|
+
it('a second Enter within the window does not double-submit', async () => {
|
|
52
|
+
const onSubmit = vi.fn();
|
|
53
|
+
const { stdin } = render(<Harness onSubmit={onSubmit} />);
|
|
54
|
+
await tick();
|
|
55
|
+
|
|
56
|
+
stdin.write('hi');
|
|
57
|
+
await tick();
|
|
58
|
+
stdin.write('\r');
|
|
59
|
+
await tick();
|
|
60
|
+
stdin.write('\r'); // second Enter within the defer window
|
|
61
|
+
await tick();
|
|
62
|
+
|
|
63
|
+
await waitDefer();
|
|
64
|
+
expect(onSubmit).toHaveBeenCalledTimes(1);
|
|
65
|
+
expect(onSubmit).toHaveBeenCalledWith('hi');
|
|
66
|
+
}, 10_000);
|
|
67
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI-062 — fallback-path pinning guard.
|
|
3
|
+
*
|
|
4
|
+
* When real-cursor positioning is NOT active (capability off — the default in this test process:
|
|
5
|
+
* stdout is not a TTY and ROBOTA_IME_CURSOR is unset), CjkTextInput's rendering must stay
|
|
6
|
+
* byte-identical to the pre-CLI-062 behavior (invariant I4: guard fail → today's visuals, drawn
|
|
7
|
+
* inverse cursor included). The expected strings below are literal captures of the component's
|
|
8
|
+
* output BEFORE the CLI-062 change (chalk.level forced to 3 so the inverse-cursor bytes are pinned,
|
|
9
|
+
* not stripped). If this test breaks, the `<Box ref>` wrapper or the drawn-cursor gating changed
|
|
10
|
+
* the fallback rendering — that is a regression, not a snapshot to refresh.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import chalk from 'chalk';
|
|
14
|
+
import { render } from 'ink-testing-library';
|
|
15
|
+
import React from 'react';
|
|
16
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
17
|
+
|
|
18
|
+
import CjkTextInput from '../CjkTextInput.js';
|
|
19
|
+
|
|
20
|
+
const noop = (): void => {};
|
|
21
|
+
const tick = (): Promise<void> => new Promise((r) => setTimeout(r, 30));
|
|
22
|
+
|
|
23
|
+
const ORIGINAL_CHALK_LEVEL = chalk.level;
|
|
24
|
+
|
|
25
|
+
describe('CLI-062 — fallback rendering is byte-identical to pre-change output', () => {
|
|
26
|
+
beforeAll(() => {
|
|
27
|
+
chalk.level = 3;
|
|
28
|
+
});
|
|
29
|
+
afterAll(() => {
|
|
30
|
+
chalk.level = ORIGINAL_CHALK_LEVEL;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('sanity: capability is off in this test process (fallback path is the one being pinned)', () => {
|
|
34
|
+
expect(process.env['ROBOTA_IME_CURSOR']).toBeUndefined();
|
|
35
|
+
expect(Boolean(process.stdout.isTTY)).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('ASCII value with drawn end-of-line cursor', async () => {
|
|
39
|
+
const { lastFrame, unmount } = render(<CjkTextInput value="hello" onChange={noop} />);
|
|
40
|
+
await tick();
|
|
41
|
+
expect(lastFrame()).toBe('hello\u001b[7m \u001b[27m');
|
|
42
|
+
unmount();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('CJK value with drawn end-of-line cursor', async () => {
|
|
46
|
+
const { lastFrame, unmount } = render(<CjkTextInput value="안녕하세요" onChange={noop} />);
|
|
47
|
+
await tick();
|
|
48
|
+
expect(lastFrame()).toBe('안녕하세요\u001b[7m \u001b[27m');
|
|
49
|
+
unmount();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('empty value renders placeholder with inverse first character', async () => {
|
|
53
|
+
const { lastFrame, unmount } = render(
|
|
54
|
+
<CjkTextInput value="" onChange={noop} placeholder="Type a message or /help" />,
|
|
55
|
+
);
|
|
56
|
+
await tick();
|
|
57
|
+
expect(lastFrame()).toBe('\u001b[7mT\u001b[27m\u001b[90mype a message or /help\u001b[39m');
|
|
58
|
+
unmount();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('unfocused input renders plain text without a drawn cursor', async () => {
|
|
62
|
+
const { lastFrame, unmount } = render(
|
|
63
|
+
<CjkTextInput value="hello" onChange={noop} focus={false} />,
|
|
64
|
+
);
|
|
65
|
+
await tick();
|
|
66
|
+
expect(lastFrame()).toBe('hello');
|
|
67
|
+
unmount();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('long value wraps identically (the <Box ref> wrapper must not change layout)', async () => {
|
|
71
|
+
const { lastFrame, unmount } = render(
|
|
72
|
+
<CjkTextInput value={'a'.repeat(150) + '안녕'} onChange={noop} availableWidth={58} />,
|
|
73
|
+
);
|
|
74
|
+
await tick();
|
|
75
|
+
expect(lastFrame()).toBe('a'.repeat(100) + '\n' + 'a'.repeat(50) + '안녕\u001b[7m \u001b[27m');
|
|
76
|
+
unmount();
|
|
77
|
+
});
|
|
78
|
+
});
|