@robota-sdk/agent-ui-terminal 3.0.0-beta.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1204 -0
- package/LICENSE +661 -0
- package/README.md +50 -0
- package/dist/node/index.d.ts +1224 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +33 -0
- package/dist/node/index.js.map +1 -0
- package/package.json +88 -0
- package/src/App.tsx +157 -0
- package/src/AppPresentation.tsx +190 -0
- package/src/AppView.tsx +26 -0
- package/src/BackgroundTaskPanel.tsx +73 -0
- package/src/CjkTextInput.tsx +336 -0
- package/src/ConfirmPrompt.tsx +140 -0
- package/src/ContextWarningBanner.tsx +45 -0
- package/src/ExecutionWorkspaceDetailPane.tsx +85 -0
- package/src/ExecutionWorkspaceSwitcher.tsx +213 -0
- package/src/HistorySearchOverlay.tsx +150 -0
- package/src/InputArea.tsx +338 -0
- package/src/ListPicker.tsx +158 -0
- package/src/MenuSelect.tsx +159 -0
- package/src/MessageList.tsx +308 -0
- package/src/MultiSelectList.tsx +294 -0
- package/src/PendingActionPrompt.tsx +121 -0
- package/src/PermissionPrompt.tsx +236 -0
- package/src/PluginTUI.tsx +258 -0
- package/src/RoleLabel.tsx +72 -0
- package/src/SafeText.tsx +66 -0
- package/src/SessionEventNotices.tsx +27 -0
- package/src/SessionPicker.tsx +92 -0
- package/src/SessionStatusBar.tsx +80 -0
- package/src/SlashAutocomplete.tsx +154 -0
- package/src/StatusBar.tsx +308 -0
- package/src/StreamingIndicator.tsx +145 -0
- package/src/SupervisedSessionView.tsx +872 -0
- package/src/TextPrompt.tsx +112 -0
- package/src/ThemePicker.tsx +366 -0
- package/src/ToolCommandOutput.tsx +48 -0
- package/src/ToolDiffBlock.tsx +44 -0
- package/src/TransportTUI.tsx +166 -0
- package/src/TuiInteractionChannel.ts +530 -0
- package/src/UpdateNotice.tsx +18 -0
- package/src/UsageSummaryEntry.tsx +49 -0
- package/src/WaveText.tsx +70 -0
- package/src/__tests__/ListPicker.test.tsx +188 -0
- package/src/__tests__/MenuSelect.test.tsx +127 -0
- package/src/__tests__/PendingActionPrompt.test.tsx +193 -0
- package/src/__tests__/PluginTUI.test.tsx +167 -0
- package/src/__tests__/SlashAutocomplete.test.tsx +162 -0
- package/src/__tests__/SupervisedSessionView-render.test.tsx +52 -0
- package/src/__tests__/SupervisedSessionView.test.tsx +1112 -0
- package/src/__tests__/TextPrompt.test.tsx +112 -0
- package/src/__tests__/TransportTUI.test.tsx +124 -0
- package/src/__tests__/TuiInteractionChannel.askUser.test.ts +124 -0
- package/src/__tests__/TuiInteractionChannel.display-contract.test.ts +294 -0
- package/src/__tests__/TuiInteractionChannel.lifecycle.test.ts +650 -0
- package/src/__tests__/UpdateNotice.test.tsx +15 -0
- package/src/__tests__/__fixtures__/korean-text.json +17 -0
- package/src/__tests__/abort-after-permission.test.tsx +160 -0
- package/src/__tests__/abort-streaming-e2e.test.tsx +183 -0
- package/src/__tests__/attached-app-session-switch.test.tsx +317 -0
- package/src/__tests__/attention-bell-and-marks.test.ts +144 -0
- package/src/__tests__/autocomplete-editing-keys.test.tsx +127 -0
- package/src/__tests__/background-task-panel-countdown.test.tsx +75 -0
- package/src/__tests__/background-task-panel.test.tsx +98 -0
- package/src/__tests__/background-task-row-format.test.ts +152 -0
- package/src/__tests__/channel-factory-integration.test.ts +150 -0
- package/src/__tests__/checkpoint-store-per-channel.test.ts +81 -0
- package/src/__tests__/cjk-defer-submit.test.tsx +68 -0
- package/src/__tests__/cjk-fallback-render.test.tsx +89 -0
- package/src/__tests__/cjk-text-input-flow.test.ts +187 -0
- package/src/__tests__/cjk-text-input.test.ts +196 -0
- package/src/__tests__/command-handoff-pty-e2e.test.ts +107 -0
- package/src/__tests__/command-output-summary.test.ts +95 -0
- package/src/__tests__/command-result-handler.test.ts +171 -0
- package/src/__tests__/compact-event-bridge.test.ts +20 -0
- package/src/__tests__/confirm-permission-flow.test.ts +149 -0
- package/src/__tests__/confirm-prompt.test.tsx +103 -0
- package/src/__tests__/contextual-keybindings-input.test.tsx +158 -0
- package/src/__tests__/default-tui-cli-adapter-host-settings.test.ts +47 -0
- package/src/__tests__/effort-reaches-the-session.test.ts +41 -0
- package/src/__tests__/execution-workspace-switcher.test.tsx +147 -0
- package/src/__tests__/execution-workspace-view-model.test.ts +94 -0
- package/src/__tests__/external-prompt-notice.test.tsx +100 -0
- package/src/__tests__/fixtures/command-handoff-driver.tsx +114 -0
- package/src/__tests__/fixtures/fake-editor.sh +7 -0
- package/src/__tests__/fixtures/terminal-handoff-driver.tsx +90 -0
- package/src/__tests__/fork-attach.test.tsx +235 -0
- package/src/__tests__/grant-history-reaches-the-session.test.ts +31 -0
- package/src/__tests__/helpers/terminal-profiles.ts +152 -0
- package/src/__tests__/helpers/vt-cursor-interpreter.ts +175 -0
- package/src/__tests__/history-clear-broadcast.test.ts +66 -0
- package/src/__tests__/history-search-overlay.test.tsx +329 -0
- package/src/__tests__/humanize-tool-name.test.ts +30 -0
- package/src/__tests__/input-area-bottom-border.test.tsx +45 -0
- package/src/__tests__/input-area-flow.test.ts +199 -0
- package/src/__tests__/input-area-focus-handoff.test.tsx +77 -0
- package/src/__tests__/interaction-channel-charter.test.ts +13 -0
- package/src/__tests__/key-hint-consistency.test.tsx +249 -0
- package/src/__tests__/key-hint-footer.test.tsx +69 -0
- package/src/__tests__/live-trace-reaches-session.test.ts +14 -0
- package/src/__tests__/message-list-rendering.test.tsx +474 -0
- package/src/__tests__/numbered-selection-chunk.test.tsx +41 -0
- package/src/__tests__/org-policy-reaches-the-channel.test.ts +77 -0
- package/src/__tests__/output-style-reaches-session.test.ts +35 -0
- package/src/__tests__/palette-consistency.test.ts +329 -0
- package/src/__tests__/permission-prompt-arming.test.tsx +153 -0
- package/src/__tests__/permission-prompt-availability.test.tsx +23 -0
- package/src/__tests__/permission-prompt-peer.test.tsx +34 -0
- package/src/__tests__/permission-prompt-requester.test.tsx +34 -0
- package/src/__tests__/product-display-name.test.tsx +79 -0
- package/src/__tests__/prompt-queue.test.tsx +260 -0
- package/src/__tests__/provider-definitions-reach-the-session.test.ts +75 -0
- package/src/__tests__/pty/ask-user-question.ptytest.ts +72 -0
- package/src/__tests__/pty/background-work-switcher.ptytest.ts +53 -0
- package/src/__tests__/pty/behavior-2437-git.ptytest.ts +350 -0
- package/src/__tests__/pty/fixtures/ask-user-question.jsonl +3 -0
- package/src/__tests__/pty/fixtures/replay-conversation.jsonl +4 -0
- package/src/__tests__/pty/fixtures/screen-005-ask-shapes.jsonl +5 -0
- package/src/__tests__/pty/fixtures/screen-005-permission.jsonl +3 -0
- package/src/__tests__/pty/fixtures/screen-006-colors.jsonl +3 -0
- package/src/__tests__/pty/fixtures/screen-2002-themes.jsonl +5 -0
- package/src/__tests__/pty/flag-tui.ptytest.ts +56 -0
- package/src/__tests__/pty/flow-2006-deep-link.ptytest.ts +320 -0
- package/src/__tests__/pty/ime-cursor-tmux.ptytest.ts +211 -0
- package/src/__tests__/pty/ime-cursor.ptytest.ts +187 -0
- package/src/__tests__/pty/isolated-home.ts +97 -0
- package/src/__tests__/pty/org-policy.ptytest.ts +71 -0
- package/src/__tests__/pty/provider-setup.ptytest.ts +61 -0
- package/src/__tests__/pty/pty-driver.ts +155 -0
- package/src/__tests__/pty/replay-conversation.ptytest.ts +68 -0
- package/src/__tests__/pty/screen-005-prompt-footers.ptytest.ts +135 -0
- package/src/__tests__/pty/screen-006-no-color.ptytest.ts +140 -0
- package/src/__tests__/pty/screen-010-scrollback.ptytest.ts +123 -0
- package/src/__tests__/pty/screen-1993-scrollback.ptytest.ts +124 -0
- package/src/__tests__/pty/screen-2002-themes.ptytest.ts +292 -0
- package/src/__tests__/pty/screen-2670-prepark.ptytest.ts +228 -0
- package/src/__tests__/pty/screen-reader-mode.ptytest.ts +228 -0
- package/src/__tests__/pty/session-attach.ptytest.ts +182 -0
- package/src/__tests__/pty/settings-screen.ptytest.ts +49 -0
- package/src/__tests__/pty/spawn-pty.test.ts +120 -0
- package/src/__tests__/pty/spawn-pty.ts +229 -0
- package/src/__tests__/pty/terminal-handoff.ptytest.ts +79 -0
- package/src/__tests__/pty/trust-prompt.ptytest.ts +134 -0
- package/src/__tests__/pty/tui-pty.ptytest.ts +65 -0
- package/src/__tests__/pty-driver-dispose.test.ts +43 -0
- package/src/__tests__/real-cursor-positioning.test.tsx +264 -0
- package/src/__tests__/rename-broadcast-persistence.test.tsx +140 -0
- package/src/__tests__/render-channel-options.test.ts +201 -0
- package/src/__tests__/render-lifecycle.test.ts +36 -0
- package/src/__tests__/render-markdown.test.ts +164 -0
- package/src/__tests__/rendered-markdown-styling.test.tsx +98 -0
- package/src/__tests__/safe-text-boundary.test.tsx +75 -0
- package/src/__tests__/sandbox-reaches-the-session.test.ts +37 -0
- package/src/__tests__/screen-reader-input.test.tsx +106 -0
- package/src/__tests__/screen-reader-labels.test.ts +91 -0
- package/src/__tests__/screen-reader-menus.test.tsx +200 -0
- package/src/__tests__/screen-reader-pacing.test.ts +230 -0
- package/src/__tests__/screen-reader-prepark-echo.test.tsx +200 -0
- package/src/__tests__/screen-reader-render-options.test.ts +262 -0
- package/src/__tests__/screen-reader-stdout.test.ts +322 -0
- package/src/__tests__/screen-reader-turn-marks-port.test.tsx +85 -0
- package/src/__tests__/screen-reader-turn-signals.test.tsx +125 -0
- package/src/__tests__/sec-019-terminal-sanitizer.test.ts +276 -0
- package/src/__tests__/sec-019-tool-label-render.test.tsx +207 -0
- package/src/__tests__/selection-flow.test.ts +61 -0
- package/src/__tests__/session-event-notices.test.tsx +31 -0
- package/src/__tests__/session-init-poller.test.ts +102 -0
- package/src/__tests__/session-picker.test.tsx +93 -0
- package/src/__tests__/session-switch-channel.test.tsx +470 -0
- package/src/__tests__/shutdown-signal.test.ts +34 -0
- package/src/__tests__/status-activity.test.ts +71 -0
- package/src/__tests__/status-bar.test.tsx +290 -0
- package/src/__tests__/status-glyph.test.ts +30 -0
- package/src/__tests__/streaming-indicator.test.tsx +178 -0
- package/src/__tests__/terminal-capabilities.test.ts +153 -0
- package/src/__tests__/terminal-handoff-controller.test.ts +173 -0
- package/src/__tests__/terminal-handoff-pty-e2e.test.ts +77 -0
- package/src/__tests__/text-prompt-flow.test.ts +77 -0
- package/src/__tests__/theme-picker.test.tsx +345 -0
- package/src/__tests__/transport-boundary.test.ts +11 -0
- package/src/__tests__/tui-app-channel-port.test.ts +31 -0
- package/src/__tests__/tui-channel-init-failure.test.ts +70 -0
- package/src/__tests__/tui-channel-lifecycle-coordinator.test.ts +258 -0
- package/src/__tests__/tui-interaction-queues.test.ts +149 -0
- package/src/__tests__/tui-preset-capabilities.test.ts +33 -0
- package/src/__tests__/tui-state-manager.error-stall.test.ts +169 -0
- package/src/__tests__/tui-state-manager.test.ts +482 -0
- package/src/__tests__/ui-intent-rendering.test.tsx +291 -0
- package/src/__tests__/wave-text.test.tsx +133 -0
- package/src/__tests__/wire-tui-channel.test.ts +1457 -0
- package/src/__tests__/wire-tui-client-commands.test.ts +243 -0
- package/src/app-banner.tsx +42 -0
- package/src/app-static-items.ts +35 -0
- package/src/app-view-model.ts +140 -0
- package/src/attached-session-connection.ts +14 -0
- package/src/attention/__tests__/attention-coordinator.test.ts +166 -0
- package/src/attention/__tests__/attention-tracker.test.ts +103 -0
- package/src/attention/__tests__/countdown.test.ts +15 -0
- package/src/attention/__tests__/focus-input-filter.test.ts +93 -0
- package/src/attention/__tests__/interval-recap.test.ts +49 -0
- package/src/attention/attention-coordinator.ts +71 -0
- package/src/attention/attention-tracker.ts +124 -0
- package/src/attention/countdown.ts +24 -0
- package/src/attention/focus-input-filter.ts +131 -0
- package/src/attention/interval-recap.ts +148 -0
- package/src/attention/time-units.ts +6 -0
- package/src/attention-bell.ts +79 -0
- package/src/attributed-user-echo.ts +30 -0
- package/src/background-task-row-format.ts +99 -0
- package/src/command-interaction.ts +8 -0
- package/src/command-output-summary.ts +129 -0
- package/src/create-default-tui-cli-adapter.ts +45 -0
- package/src/execution-workspace-switcher-selection.ts +118 -0
- package/src/execution-workspace-view-model.ts +139 -0
- package/src/external-prompt-notice.tsx +38 -0
- package/src/flows/__tests__/background-focus-flow.test.ts +48 -0
- package/src/flows/__tests__/defer-submit.test.ts +88 -0
- package/src/flows/__tests__/real-cursor-flow.test.ts +139 -0
- package/src/flows/background-focus-flow.ts +44 -0
- package/src/flows/cjk-cursor-motion.ts +75 -0
- package/src/flows/cjk-text-deletion.ts +53 -0
- package/src/flows/cjk-text-input-flow.ts +248 -0
- package/src/flows/cjk-text-input-types.ts +24 -0
- package/src/flows/confirm-prompt-flow.ts +90 -0
- package/src/flows/defer-submit.ts +67 -0
- package/src/flows/fork-attach-flow.ts +62 -0
- package/src/flows/input-area-flow.ts +188 -0
- package/src/flows/permission-prompt-flow.ts +108 -0
- package/src/flows/real-cursor-flow.ts +83 -0
- package/src/flows/selection-flow.ts +205 -0
- package/src/flows/session-init-poller.ts +76 -0
- package/src/flows/text-prompt-flow.ts +93 -0
- package/src/history-search/__tests__/history-search-flow.test.ts +86 -0
- package/src/history-search/history-search-flow.ts +94 -0
- package/src/history-search/useHistorySearch.ts +336 -0
- package/src/hooks/__tests__/use-app-detach-key.test.tsx +72 -0
- package/src/hooks/__tests__/use-app-loop-escape.test.tsx +87 -0
- package/src/hooks/__tests__/use-plugin-callbacks.test.ts +18 -0
- package/src/hooks/__tests__/use-tui-channel-stability.test.tsx +100 -0
- package/src/hooks/__tests__/useAppThemeState.test.tsx +130 -0
- package/src/hooks/command-result-handler.ts +44 -0
- package/src/hooks/side-effects-types.ts +45 -0
- package/src/hooks/useAppController.ts +271 -0
- package/src/hooks/useAppInputBindings.ts +163 -0
- package/src/hooks/useAppInteractionState.ts +84 -0
- package/src/hooks/useAppLifecycleState.ts +81 -0
- package/src/hooks/useAppOverlays.ts +90 -0
- package/src/hooks/useAppScreenState.ts +81 -0
- package/src/hooks/useAppSubmissionState.ts +45 -0
- package/src/hooks/useAppThemeState.ts +157 -0
- package/src/hooks/useAppWorkspaceState.ts +149 -0
- package/src/hooks/useAppearanceSettings.ts +30 -0
- package/src/hooks/useAutocomplete.ts +101 -0
- package/src/hooks/useBackgroundPanel.ts +58 -0
- package/src/hooks/useCountdownTick.ts +24 -0
- package/src/hooks/useExecutionDetailPage.ts +69 -0
- package/src/hooks/useForkAttach.ts +60 -0
- package/src/hooks/useInputAreaHistorySearch.ts +72 -0
- package/src/hooks/useInputAreaKeys.ts +114 -0
- package/src/hooks/useNumberedSelection.ts +93 -0
- package/src/hooks/usePluginCallbacks.ts +38 -0
- package/src/hooks/usePluginScreenData.ts +85 -0
- package/src/hooks/useRealCursorPosition.ts +125 -0
- package/src/hooks/useRenderMarkdown.ts +38 -0
- package/src/hooks/useScreenReaderTurnSignals.ts +113 -0
- package/src/hooks/useSideEffects.ts +144 -0
- package/src/hooks/useStatusLineSettings.ts +50 -0
- package/src/hooks/useTerminalHandoffSuspension.ts +30 -0
- package/src/hooks/useTuiChannel.ts +141 -0
- package/src/humanize-tool-name.ts +51 -0
- package/src/index.ts +72 -0
- package/src/input-area-rules.tsx +71 -0
- package/src/json-value.ts +18 -0
- package/src/key-hint-footer.tsx +54 -0
- package/src/keybindings/__tests__/keybinding-registry.test.ts +228 -0
- package/src/keybindings/__tests__/node-keybindings-source.test.ts +118 -0
- package/src/keybindings/keybinding-catalogue.ts +171 -0
- package/src/keybindings/keybinding-registry.ts +303 -0
- package/src/keybindings/keybinding-syntax.ts +155 -0
- package/src/keybindings/keybindings-context.tsx +124 -0
- package/src/keybindings/node-keybindings-source.ts +189 -0
- package/src/numbered-list.tsx +113 -0
- package/src/plugin-tui-handlers.ts +163 -0
- package/src/product-display-name-context.tsx +30 -0
- package/src/render-markdown.ts +209 -0
- package/src/render.tsx +746 -0
- package/src/sanitize-terminal-text.ts +171 -0
- package/src/screen-reader-announcement.ts +52 -0
- package/src/screen-reader-context.tsx +32 -0
- package/src/screen-reader-labels.ts +64 -0
- package/src/screen-reader-pacing-context.tsx +47 -0
- package/src/screen-reader-pacing.ts +178 -0
- package/src/screen-reader-stdout.ts +328 -0
- package/src/short-session-id.ts +9 -0
- package/src/shutdown-signal.ts +34 -0
- package/src/slash-command-input.ts +11 -0
- package/src/status-activity.ts +70 -0
- package/src/status-glyph.ts +78 -0
- package/src/terminal-capabilities-context.tsx +27 -0
- package/src/terminal-capabilities.ts +77 -0
- package/src/terminal-focus-reporting.ts +50 -0
- package/src/terminal-handoff-controller.ts +98 -0
- package/src/terminal-marks.ts +63 -0
- package/src/theme/__tests__/built-in-themes.test.ts +117 -0
- package/src/theme/__tests__/color-vision.test.ts +82 -0
- package/src/theme/__tests__/theme-context.test.tsx +146 -0
- package/src/theme/__tests__/theme-document.test.ts +222 -0
- package/src/theme/__tests__/theme-registry.test.ts +138 -0
- package/src/theme/__tests__/theme-styles.test.ts +233 -0
- package/src/theme/built-in-themes.ts +342 -0
- package/src/theme/color-vision.ts +171 -0
- package/src/theme/index.ts +45 -0
- package/src/theme/theme-context.tsx +81 -0
- package/src/theme/theme-contracts.ts +139 -0
- package/src/theme/theme-document.ts +278 -0
- package/src/theme/theme-registry.ts +127 -0
- package/src/theme/theme-styles.ts +177 -0
- package/src/tool-summary-status.ts +59 -0
- package/src/tui-app-channel-port.ts +111 -0
- package/src/tui-channel-lifecycle-coordinator.ts +149 -0
- package/src/tui-channel-options.ts +184 -0
- package/src/tui-cli-adapter-context.tsx +13 -0
- package/src/tui-cli-adapter.ts +24 -0
- package/src/tui-interaction-queues.ts +171 -0
- package/src/tui-session-binding.ts +52 -0
- package/src/tui-session-event-projector.ts +149 -0
- package/src/tui-session-events.ts +89 -0
- package/src/tui-session-notice-store.ts +43 -0
- package/src/tui-session-options.ts +134 -0
- package/src/tui-state-manager.ts +347 -0
- package/src/types.ts +21 -0
- package/src/use-terminal-title.ts +29 -0
- package/src/utils/__tests__/edit-diff.test.ts +426 -0
- package/src/utils/__tests__/paste-detection.test.ts +116 -0
- package/src/utils/__tests__/paste-labels.test.ts +46 -0
- package/src/utils/__tests__/tool-call-extractor.test.ts +227 -0
- package/src/utils/__tests__/tool-diff-summary.test.ts +104 -0
- package/src/utils/edit-diff.ts +151 -0
- package/src/utils/input-top-border.ts +19 -0
- package/src/utils/paste-labels.ts +9 -0
- package/src/utils/tool-call-extractor.ts +92 -0
- package/src/utils/tool-diff-summary.ts +75 -0
- package/src/waiting-loop-stop-notice.ts +23 -0
- package/src/wire-history-sync.ts +159 -0
- package/src/wire-tui-channel.ts +963 -0
- package/src/wire-tui-client-commands.ts +142 -0
- package/src/wire-tui-projection.ts +136 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for CjkTextInput pure utility functions.
|
|
3
|
+
*
|
|
4
|
+
* These test the input filtering and insertion logic extracted from
|
|
5
|
+
* the React component, without requiring Ink/React rendering.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect } from 'vitest';
|
|
9
|
+
import korean from './__fixtures__/korean-text.json';
|
|
10
|
+
import {
|
|
11
|
+
filterPrintable,
|
|
12
|
+
insertAtCursor,
|
|
13
|
+
displayOffset,
|
|
14
|
+
charIndexAtDisplayOffset,
|
|
15
|
+
} from '../flows/cjk-text-input-flow.js';
|
|
16
|
+
|
|
17
|
+
describe('filterPrintable', () => {
|
|
18
|
+
it('returns empty string for null input', () => {
|
|
19
|
+
expect(filterPrintable(null)).toBe('');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('returns empty string for undefined input', () => {
|
|
23
|
+
expect(filterPrintable(undefined)).toBe('');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('returns empty string for empty string', () => {
|
|
27
|
+
expect(filterPrintable('')).toBe('');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('passes through normal ASCII text', () => {
|
|
31
|
+
expect(filterPrintable('hello')).toBe('hello');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('passes through Korean characters', () => {
|
|
35
|
+
expect(filterPrintable(korean.greeting)).toBe(korean.greeting);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('passes through emoji', () => {
|
|
39
|
+
expect(filterPrintable('🎉')).toBe('🎉');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('filters out null byte', () => {
|
|
43
|
+
expect(filterPrintable('\x00')).toBe('');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('filters out escape character', () => {
|
|
47
|
+
expect(filterPrintable('\x1b')).toBe('');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('filters out DEL character', () => {
|
|
51
|
+
expect(filterPrintable('\x7f')).toBe('');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('filters out mixed control characters, keeps printable', () => {
|
|
55
|
+
expect(filterPrintable('\x01hello\x02world\x7f')).toBe('helloworld');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('returns empty when input is only control characters', () => {
|
|
59
|
+
expect(filterPrintable('\x00\x01\x02\x03\x1f\x7f')).toBe('');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('handles tab character (0x09) — filtered as control', () => {
|
|
63
|
+
expect(filterPrintable('\t')).toBe('');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('handles newline (0x0a) — filtered as control', () => {
|
|
67
|
+
expect(filterPrintable('\n')).toBe('');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('insertAtCursor', () => {
|
|
72
|
+
it('inserts at the beginning', () => {
|
|
73
|
+
const result = insertAtCursor('world', 0, 'hello ');
|
|
74
|
+
expect(result.value).toBe('hello world');
|
|
75
|
+
expect(result.cursor).toBe(6);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('inserts at the end', () => {
|
|
79
|
+
const result = insertAtCursor('hello', 5, ' world');
|
|
80
|
+
expect(result.value).toBe('hello world');
|
|
81
|
+
expect(result.cursor).toBe(11);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('inserts in the middle', () => {
|
|
85
|
+
const result = insertAtCursor('helo', 3, 'l');
|
|
86
|
+
expect(result.value).toBe('hello');
|
|
87
|
+
expect(result.cursor).toBe(4);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('inserts into empty string', () => {
|
|
91
|
+
const result = insertAtCursor('', 0, 'abc');
|
|
92
|
+
expect(result.value).toBe('abc');
|
|
93
|
+
expect(result.cursor).toBe(3);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('handles multi-character paste', () => {
|
|
97
|
+
const result = insertAtCursor('ac', 1, 'bb');
|
|
98
|
+
expect(result.value).toBe('abbc');
|
|
99
|
+
expect(result.cursor).toBe(3);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('handles Korean character insertion', () => {
|
|
103
|
+
const result = insertAtCursor(
|
|
104
|
+
korean.greetingWithoutMiddle,
|
|
105
|
+
1,
|
|
106
|
+
korean.greetingMiddleTwoSyllables,
|
|
107
|
+
);
|
|
108
|
+
expect(result.value).toBe(korean.greeting);
|
|
109
|
+
expect(result.cursor).toBe(3);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('handles emoji insertion', () => {
|
|
113
|
+
const result = insertAtCursor('hello world', 6, '🎉 ');
|
|
114
|
+
expect(result.value).toBe('hello 🎉 world');
|
|
115
|
+
expect(result.cursor).toBe(9);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('displayOffset', () => {
|
|
120
|
+
it('returns 0 for charIndex 0', () => {
|
|
121
|
+
expect(displayOffset([...'hello'], 0, 10)).toBe(0);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('returns char count for ASCII within one line', () => {
|
|
125
|
+
expect(displayOffset([...'hello'], 3, 10)).toBe(3);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('accumulates across wrap boundary', () => {
|
|
129
|
+
// "abcde" width 3: offset at index 4 = 4 (abc on line 0, de on line 1)
|
|
130
|
+
expect(displayOffset([...'abcde'], 4, 3)).toBe(4);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('cursor before CJK char is at end of previous content', () => {
|
|
134
|
+
// "abcd" + one wide syllable, width 5: cursor before the syllable is at col 4 on line 0
|
|
135
|
+
// gap happens when the syllable renders, not at cursor position
|
|
136
|
+
expect(displayOffset([...`abcd${korean.hanSyllable}`], 4, 5)).toBe(4);
|
|
137
|
+
// cursor after the syllable: 4(gap included) + 2 = 7 → but via offset: gap is at render
|
|
138
|
+
expect(displayOffset([...`abcd${korean.hanSyllable}`], 5, 5)).toBe(7);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('handles pure CJK', () => {
|
|
142
|
+
// two wide syllables, width 5: 2 + 2 → total 4, fits on one line
|
|
143
|
+
expect(displayOffset([...korean.hangulWord], 2, 5)).toBe(4);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('handles CJK wrapping', () => {
|
|
147
|
+
// three wide syllables, width 5: cursor before the third is after the second (offset 4)
|
|
148
|
+
// the third syllable doesn't fit at col 4, but cursor is BEFORE the wrap
|
|
149
|
+
expect(displayOffset([...korean.hangulWordPlusTe], 2, 5)).toBe(4);
|
|
150
|
+
// cursor after the third syllable: gap(1) + 2 = offset 7
|
|
151
|
+
expect(displayOffset([...korean.hangulWordPlusTe], 3, 5)).toBe(7);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('charIndexAtDisplayOffset', () => {
|
|
156
|
+
it('returns 0 for offset 0', () => {
|
|
157
|
+
expect(charIndexAtDisplayOffset([...'hello'], 0, 10)).toBe(0);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('finds correct index for ASCII', () => {
|
|
161
|
+
expect(charIndexAtDisplayOffset([...'hello'], 3, 10)).toBe(3);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('returns text length when offset exceeds text', () => {
|
|
165
|
+
expect(charIndexAtDisplayOffset([...'hi'], 100, 10)).toBe(2);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('round-trips with displayOffset for ASCII', () => {
|
|
169
|
+
const text = 'abcdefghij';
|
|
170
|
+
const chars = [...text];
|
|
171
|
+
const width = 4;
|
|
172
|
+
for (let i = 0; i <= chars.length; i++) {
|
|
173
|
+
const off = displayOffset(chars, i, width);
|
|
174
|
+
expect(charIndexAtDisplayOffset(chars, off, width)).toBe(i);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('round-trips with displayOffset for CJK', () => {
|
|
179
|
+
const text = korean.hangulWordPlusTest;
|
|
180
|
+
const chars = [...text];
|
|
181
|
+
const width = 5;
|
|
182
|
+
for (let i = 0; i <= chars.length; i++) {
|
|
183
|
+
const off = displayOffset(chars, i, width);
|
|
184
|
+
expect(charIndexAtDisplayOffset(chars, off, width)).toBe(i);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('up arrow simulation: offset - width gives previous line position', () => {
|
|
189
|
+
// "abcdef" width 3: line 0 = "abc", line 1 = "def"
|
|
190
|
+
// cursor at index 4 (line 1, col 1) → offset = 4
|
|
191
|
+
// offset - 3 = 1 → charIndex 1 (line 0, col 1)
|
|
192
|
+
const chars = [...'abcdef'];
|
|
193
|
+
const off = displayOffset(chars, 4, 3);
|
|
194
|
+
expect(charIndexAtDisplayOffset(chars, off - 3, 3)).toBe(1);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TEST-007 / TERM-003 + TERM-004: `/shell` and `/editor` through a real terminal handoff, end-to-end
|
|
3
|
+
* in a PTY. Drives `fixtures/command-handoff-driver.tsx` (real executors + real
|
|
4
|
+
* `TerminalHandoffController`). Proves on a real TTY that the child (subshell / `$EDITOR`) receives
|
|
5
|
+
* the driver's keystrokes and the command returns cleanly — the behavior only a PTY can validate
|
|
6
|
+
* (the session command-pipeline path is covered by the framework functional tests with a fake
|
|
7
|
+
* handoff).
|
|
8
|
+
*/
|
|
9
|
+
import { mkdtempSync, readFileSync, rmSync, realpathSync } from 'node:fs';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
|
|
14
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
15
|
+
|
|
16
|
+
import { createPtyEnv } from './pty/isolated-home.js';
|
|
17
|
+
import { spawnPtyFixture } from './pty/spawn-pty.js';
|
|
18
|
+
|
|
19
|
+
import type { IPtyRunSession } from './pty/spawn-pty.js';
|
|
20
|
+
|
|
21
|
+
const FIXTURE = fileURLToPath(new URL('./fixtures/command-handoff-driver.tsx', import.meta.url));
|
|
22
|
+
const FAKE_EDITOR = fileURLToPath(new URL('./fixtures/fake-editor.sh', import.meta.url));
|
|
23
|
+
const PACKAGE_DIR = fileURLToPath(new URL('../..', import.meta.url));
|
|
24
|
+
const TEST_TIMEOUT_MS = 30_000;
|
|
25
|
+
|
|
26
|
+
const tempDirs: string[] = [];
|
|
27
|
+
const sessions: IPtyRunSession[] = [];
|
|
28
|
+
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
for (const session of sessions.splice(0)) session.dispose();
|
|
31
|
+
for (const dir of tempDirs.splice(0)) rmSync(dir, { recursive: true, force: true });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
interface IResult {
|
|
35
|
+
success: boolean;
|
|
36
|
+
message: string;
|
|
37
|
+
exitCode?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readResult(outputPath: string): IResult {
|
|
41
|
+
return JSON.parse(readFileSync(outputPath, 'utf8')) as IResult;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe('command handoff PTY E2E', () => {
|
|
45
|
+
it(
|
|
46
|
+
'/shell runs a one-shot command on the real terminal and returns its exit code',
|
|
47
|
+
async () => {
|
|
48
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-test-shell-pty-')));
|
|
49
|
+
tempDirs.push(dir);
|
|
50
|
+
const outputPath = join(dir, 'result.json');
|
|
51
|
+
const session = spawnPtyFixture(FIXTURE, {
|
|
52
|
+
argv: ['shell', outputPath],
|
|
53
|
+
cwd: PACKAGE_DIR,
|
|
54
|
+
// HARNESS-025: isolated HOME — the subshell must not read the developer's `~` (a real
|
|
55
|
+
// shell rc there would otherwise change what the handoff child does).
|
|
56
|
+
env: createPtyEnv(),
|
|
57
|
+
});
|
|
58
|
+
sessions.push(session);
|
|
59
|
+
|
|
60
|
+
await session.waitFor('READY canHandoff=true', 10_000);
|
|
61
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
62
|
+
await session.sendKeys('shell-input', 10);
|
|
63
|
+
session.write('\r');
|
|
64
|
+
await session.waitFor('CMD_DONE', 10_000);
|
|
65
|
+
session.write('r');
|
|
66
|
+
await session.waitFor('INPUT_AFTER_HANDOFF r', 10_000);
|
|
67
|
+
expect(await session.expectExit(5_000)).toBe(0);
|
|
68
|
+
|
|
69
|
+
expect(session.snapshot()).toContain('SHELL_GOT:[shell-input]');
|
|
70
|
+
const result = readResult(outputPath);
|
|
71
|
+
expect(result.success).toBe(true);
|
|
72
|
+
expect(result.exitCode).toBe(0);
|
|
73
|
+
},
|
|
74
|
+
TEST_TIMEOUT_MS,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
it(
|
|
78
|
+
'/editor opens $EDITOR on the real terminal and round-trips the composed text',
|
|
79
|
+
async () => {
|
|
80
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), 'agent-test-editor-pty-')));
|
|
81
|
+
tempDirs.push(dir);
|
|
82
|
+
const outputPath = join(dir, 'result.json');
|
|
83
|
+
const session = spawnPtyFixture(FIXTURE, {
|
|
84
|
+
argv: ['editor', outputPath],
|
|
85
|
+
cwd: PACKAGE_DIR,
|
|
86
|
+
// HARNESS-025: isolated HOME — only the EDITOR override comes from the test, so a real
|
|
87
|
+
// `~/.selected_editor` (or similar) on the host cannot influence the assertion.
|
|
88
|
+
env: createPtyEnv({ EDITOR: `sh ${FAKE_EDITOR}` }),
|
|
89
|
+
});
|
|
90
|
+
sessions.push(session);
|
|
91
|
+
|
|
92
|
+
await session.waitFor('READY canHandoff=true', 10_000);
|
|
93
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
94
|
+
await session.sendKeys('composed-in-editor', 10);
|
|
95
|
+
session.write('\r');
|
|
96
|
+
await session.waitFor('CMD_DONE', 10_000);
|
|
97
|
+
session.write('r');
|
|
98
|
+
await session.waitFor('INPUT_AFTER_HANDOFF r', 10_000);
|
|
99
|
+
expect(await session.expectExit(5_000)).toBe(0);
|
|
100
|
+
|
|
101
|
+
const result = readResult(outputPath);
|
|
102
|
+
expect(result.success).toBe(true);
|
|
103
|
+
expect(result.message).toBe('composed-in-editor');
|
|
104
|
+
},
|
|
105
|
+
TEST_TIMEOUT_MS,
|
|
106
|
+
);
|
|
107
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { formatCommandOutputSummary } from '../command-output-summary.js';
|
|
3
|
+
|
|
4
|
+
describe('formatCommandOutputSummary', () => {
|
|
5
|
+
it('renders short command output inline without transcript decoration', () => {
|
|
6
|
+
const summary = formatCommandOutputSummary({
|
|
7
|
+
toolName: 'Bash',
|
|
8
|
+
firstArg: 'echo hello',
|
|
9
|
+
isRunning: false,
|
|
10
|
+
result: 'success',
|
|
11
|
+
toolResultData: JSON.stringify({ success: true, output: 'hello\n', exitCode: 0 }),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
expect(summary).toMatchObject({
|
|
15
|
+
status: 'success',
|
|
16
|
+
statusLabel: 'ok',
|
|
17
|
+
previewLines: ['hello'],
|
|
18
|
+
omittedLineCount: 0,
|
|
19
|
+
transcriptHint: undefined,
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('renders long command output with a bounded preview and transcript hint', () => {
|
|
24
|
+
const output = Array.from({ length: 8 }, (_, index) => `line-${index + 1}`).join('\n');
|
|
25
|
+
const summary = formatCommandOutputSummary({
|
|
26
|
+
toolName: 'Bash',
|
|
27
|
+
firstArg: 'pnpm test',
|
|
28
|
+
isRunning: false,
|
|
29
|
+
result: 'success',
|
|
30
|
+
toolResultData: JSON.stringify({ success: true, output, exitCode: 0 }),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
expect(summary?.previewLines).toEqual(['line-1', 'line-2', 'line-3', 'line-4']);
|
|
34
|
+
expect(summary?.omittedLineCount).toBe(4);
|
|
35
|
+
expect(summary?.transcriptHint).toBe('... +4 lines (full output in session transcript)');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('keeps stderr distinct when stdout and stderr are structured separately', () => {
|
|
39
|
+
const summary = formatCommandOutputSummary({
|
|
40
|
+
toolName: 'Bash',
|
|
41
|
+
firstArg: 'node script.js',
|
|
42
|
+
isRunning: false,
|
|
43
|
+
result: 'success',
|
|
44
|
+
toolResultData: JSON.stringify({
|
|
45
|
+
success: true,
|
|
46
|
+
stdout: 'ok',
|
|
47
|
+
stderr: 'warn',
|
|
48
|
+
exitCode: 0,
|
|
49
|
+
}),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(summary?.previewLines).toEqual(['ok', '[stderr] warn']);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('marks non-zero exit codes as failed even when the tool transport succeeded', () => {
|
|
56
|
+
const summary = formatCommandOutputSummary({
|
|
57
|
+
toolName: 'Bash',
|
|
58
|
+
firstArg: 'exit 42',
|
|
59
|
+
isRunning: false,
|
|
60
|
+
result: 'success',
|
|
61
|
+
toolResultData: JSON.stringify({ success: true, output: '', exitCode: 42 }),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(summary?.status).toBe('error');
|
|
65
|
+
expect(summary?.statusLabel).toBe('exit 42');
|
|
66
|
+
expect(summary?.previewLines).toEqual([]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('renders no-output commands without dangling transcript hints', () => {
|
|
70
|
+
const summary = formatCommandOutputSummary({
|
|
71
|
+
toolName: 'Bash',
|
|
72
|
+
firstArg: 'true',
|
|
73
|
+
isRunning: false,
|
|
74
|
+
result: 'success',
|
|
75
|
+
toolResultData: JSON.stringify({ success: true, output: '', exitCode: 0 }),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(summary?.status).toBe('success');
|
|
79
|
+
expect(summary?.previewLines).toEqual([]);
|
|
80
|
+
expect(summary?.omittedLineCount).toBe(0);
|
|
81
|
+
expect(summary?.transcriptHint).toBeUndefined();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('ignores non-command tools', () => {
|
|
85
|
+
const summary = formatCommandOutputSummary({
|
|
86
|
+
toolName: 'Read',
|
|
87
|
+
firstArg: 'file.ts',
|
|
88
|
+
isRunning: false,
|
|
89
|
+
result: 'success',
|
|
90
|
+
toolResultData: 'large file content',
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(summary).toBeUndefined();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CMD-004 Phase 2: `applySystemCommandResult` is pure rendering. Host actions are applied (and
|
|
3
|
+
* consumed) by the session, UI intents arrive via the `ui_intent` session event, and state-change
|
|
4
|
+
* notifications arrive as broadcast session events (`session_renamed`, `history_cleared`). The
|
|
5
|
+
* only result-carried hint consumed here is `data.pluginRegistryReloaded` — the requester-local
|
|
6
|
+
* command-registry/autocomplete refresh.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createTestInteractiveSession } from '@robota-sdk/agent-interface-session/testing';
|
|
10
|
+
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
import { mkdirSync, mkdtempSync, writeFileSync, realpathSync } from 'node:fs';
|
|
13
|
+
import { tmpdir } from 'node:os';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import { describe, expect, it } from 'vitest';
|
|
16
|
+
import { afterEach, vi } from 'vitest';
|
|
17
|
+
import {
|
|
18
|
+
CommandRegistry,
|
|
19
|
+
BundlePluginLoader,
|
|
20
|
+
PluginCommandSource,
|
|
21
|
+
} from '@robota-sdk/agent-framework';
|
|
22
|
+
import type { IInteractiveSession } from '@robota-sdk/agent-interface-session';
|
|
23
|
+
import { TuiStateManager } from '../tui-state-manager.js';
|
|
24
|
+
import { applySystemCommandResult } from '../hooks/command-result-handler.js';
|
|
25
|
+
|
|
26
|
+
const PLUGIN_SOURCE_NAME = 'plugin';
|
|
27
|
+
|
|
28
|
+
function reloadPluginCommandSource(registry: CommandRegistry): void {
|
|
29
|
+
const pluginsDir = join(process.env.HOME ?? homedir(), 'state', 'plugins');
|
|
30
|
+
const loader = new BundlePluginLoader(pluginsDir);
|
|
31
|
+
try {
|
|
32
|
+
// allow-fallback: test helper — empty registry on load error is safe
|
|
33
|
+
const plugins = loader.loadPluginsSync();
|
|
34
|
+
if (plugins.length === 0) {
|
|
35
|
+
registry.replaceSource(PLUGIN_SOURCE_NAME);
|
|
36
|
+
} else {
|
|
37
|
+
registry.replaceSource(PLUGIN_SOURCE_NAME, new PluginCommandSource(plugins));
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
// allow-fallback: test helper — empty registry on load error is safe
|
|
41
|
+
registry.replaceSource(PLUGIN_SOURCE_NAME);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe('applySystemCommandResult', () => {
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
vi.unstubAllEnvs();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
function createSession(): IInteractiveSession {
|
|
51
|
+
return Object.assign(createTestInteractiveSession(), {
|
|
52
|
+
getContextState: () => ({ usedPercentage: 0, usedTokens: 0, maxTokens: 0 }),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
it('renders the message only — UI intents are delivered via the ui_intent session event', () => {
|
|
57
|
+
const manager = new TuiStateManager();
|
|
58
|
+
|
|
59
|
+
applySystemCommandResult(
|
|
60
|
+
{
|
|
61
|
+
success: true,
|
|
62
|
+
message: 'Opening plugin manager...',
|
|
63
|
+
uiIntents: [{ type: 'show-plugin-manager' }],
|
|
64
|
+
},
|
|
65
|
+
createSession(),
|
|
66
|
+
new CommandRegistry(),
|
|
67
|
+
manager,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Pure rendering: the message lands in history; the legacy screen effect does nothing here.
|
|
71
|
+
expect(manager.history).toHaveLength(1);
|
|
72
|
+
expect(manager.history[0]?.data).toMatchObject({ content: 'Opening plugin manager...' });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('renders a host-actioned result as message only (host applied it; TUI refreshes on result)', () => {
|
|
76
|
+
const manager = new TuiStateManager();
|
|
77
|
+
|
|
78
|
+
applySystemCommandResult(
|
|
79
|
+
{
|
|
80
|
+
success: true,
|
|
81
|
+
message: 'Status line disabled.',
|
|
82
|
+
},
|
|
83
|
+
createSession(),
|
|
84
|
+
new CommandRegistry(),
|
|
85
|
+
manager,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
expect(manager.history).toHaveLength(1);
|
|
89
|
+
expect(manager.history[0]?.data).toMatchObject({ content: 'Status line disabled.' });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('appends the /clear result message AFTER the broadcast-driven clear (event fired during executeCommand)', () => {
|
|
93
|
+
const manager = new TuiStateManager();
|
|
94
|
+
manager.addEntry({
|
|
95
|
+
id: 'old',
|
|
96
|
+
timestamp: new Date('2026-05-03T00:00:00.000Z'),
|
|
97
|
+
category: 'chat',
|
|
98
|
+
type: 'user',
|
|
99
|
+
data: { role: 'user', content: 'old message' },
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Stage E: the transcript clear itself rides the broadcast `history_cleared` session event
|
|
103
|
+
// (bound in TuiInteractionChannel and emitted while the command executes — before the result
|
|
104
|
+
// returns), so by the time this renderer runs the history is already empty.
|
|
105
|
+
manager.clearHistory();
|
|
106
|
+
applySystemCommandResult(
|
|
107
|
+
{
|
|
108
|
+
success: true,
|
|
109
|
+
message: 'Conversation cleared.',
|
|
110
|
+
},
|
|
111
|
+
createSession(),
|
|
112
|
+
new CommandRegistry(),
|
|
113
|
+
manager,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
expect(manager.history).toHaveLength(1);
|
|
117
|
+
expect(manager.history[0]?.type).toBe('system');
|
|
118
|
+
expect(manager.history[0]?.data).toMatchObject({ content: 'Conversation cleared.' });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('reloads plugin command source immediately when requested', () => {
|
|
122
|
+
const home = realpathSync(mkdtempSync(join(tmpdir(), 'agent-test-plugin-reload-')));
|
|
123
|
+
const pluginDir = join(
|
|
124
|
+
home,
|
|
125
|
+
'state',
|
|
126
|
+
'plugins',
|
|
127
|
+
'cache',
|
|
128
|
+
'community',
|
|
129
|
+
'fresh-plugin',
|
|
130
|
+
'1.0.0',
|
|
131
|
+
);
|
|
132
|
+
mkdirSync(join(pluginDir, '.claude-plugin'), { recursive: true });
|
|
133
|
+
mkdirSync(join(pluginDir, 'skills', 'fresh-skill'), { recursive: true });
|
|
134
|
+
writeFileSync(
|
|
135
|
+
join(pluginDir, '.claude-plugin', 'plugin.json'),
|
|
136
|
+
JSON.stringify({
|
|
137
|
+
name: 'fresh-plugin',
|
|
138
|
+
version: '1.0.0',
|
|
139
|
+
description: 'Fresh plugin',
|
|
140
|
+
features: { skills: true },
|
|
141
|
+
}),
|
|
142
|
+
'utf8',
|
|
143
|
+
);
|
|
144
|
+
writeFileSync(
|
|
145
|
+
join(pluginDir, 'skills', 'fresh-skill', 'SKILL.md'),
|
|
146
|
+
'---\ndescription: Fresh skill\n---\n# Fresh Skill\n',
|
|
147
|
+
'utf8',
|
|
148
|
+
);
|
|
149
|
+
vi.stubEnv('HOME', home);
|
|
150
|
+
const registry = new CommandRegistry();
|
|
151
|
+
registry.addSource({
|
|
152
|
+
name: 'plugin',
|
|
153
|
+
getCommands: () => [{ name: 'stale-skill', description: 'Stale', source: 'plugin' }],
|
|
154
|
+
});
|
|
155
|
+
const manager = new TuiStateManager();
|
|
156
|
+
|
|
157
|
+
applySystemCommandResult(
|
|
158
|
+
{
|
|
159
|
+
success: true,
|
|
160
|
+
message: 'Reloaded 1 plugin resource.',
|
|
161
|
+
data: { pluginRegistryReloaded: true },
|
|
162
|
+
},
|
|
163
|
+
createSession(),
|
|
164
|
+
registry,
|
|
165
|
+
manager,
|
|
166
|
+
reloadPluginCommandSource,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
expect(registry.getCommands().map((command) => command.name)).toEqual(['fresh-skill']);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createSystemMessage, messageToHistoryEntry } from '@robota-sdk/agent-core';
|
|
3
|
+
import { TuiStateManager } from '../tui-state-manager.js';
|
|
4
|
+
import { applyCompactEventToManager } from '../hooks/useTuiChannel.js';
|
|
5
|
+
|
|
6
|
+
describe('compact event bridge', () => {
|
|
7
|
+
it('syncs session history so automatic compaction notifications render', () => {
|
|
8
|
+
const manager = new TuiStateManager();
|
|
9
|
+
const notification = messageToHistoryEntry(
|
|
10
|
+
createSystemMessage('Auto compacted context: 84% -> 35%'),
|
|
11
|
+
);
|
|
12
|
+
const session = {
|
|
13
|
+
getFullHistory: () => [notification],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
applyCompactEventToManager(session, manager);
|
|
17
|
+
|
|
18
|
+
expect(manager.history).toEqual([notification]);
|
|
19
|
+
});
|
|
20
|
+
});
|