@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,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI-062 — real-terminal-cursor positioning through ink's actual interactive render path.
|
|
3
|
+
*
|
|
4
|
+
* Renders CjkTextInput inside a TransportTUI-shaped harness against a fake TTY stdout
|
|
5
|
+
* (`interactive: true`), then interprets the raw ANSI stream with the same VT logic a terminal
|
|
6
|
+
* emulator (and the OS IME) applies. This is the component-level half of the CLI-062 contract
|
|
7
|
+
* (.design/investigations/2026-07-25-cli-062-ime-cursor-design.md): the hardware cursor must be
|
|
8
|
+
* shown ON the input row at the composition column, track CJK width growth, disappear when the
|
|
9
|
+
* input is unfocused (I4), propagate `undefined` on unmount (I4), and the production code must
|
|
10
|
+
* write NOTHING to the real process streams (the PoC row-accounting lesson, I3).
|
|
11
|
+
*
|
|
12
|
+
* The host override opts the capability gate in explicitly — the vitest process has no TTY,
|
|
13
|
+
* so the default gate would (correctly) refuse.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { EventEmitter } from 'node:events';
|
|
17
|
+
|
|
18
|
+
import chalk from 'chalk';
|
|
19
|
+
import { Box, Text, render } from 'ink';
|
|
20
|
+
import React, { useState } from 'react';
|
|
21
|
+
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
|
|
22
|
+
|
|
23
|
+
import { interpretVtStream } from './helpers/vt-cursor-interpreter.js';
|
|
24
|
+
import CjkTextInput from '../CjkTextInput.js';
|
|
25
|
+
import { TerminalCapabilitiesProvider } from '../terminal-capabilities-context.js';
|
|
26
|
+
import korean from './__fixtures__/korean-text.json';
|
|
27
|
+
|
|
28
|
+
const COLS = 60;
|
|
29
|
+
const ROWS = 24;
|
|
30
|
+
const PROMPT = '> ';
|
|
31
|
+
|
|
32
|
+
class FakeTtyStdout extends EventEmitter {
|
|
33
|
+
readonly isTTY = true;
|
|
34
|
+
readonly columns = COLS;
|
|
35
|
+
readonly rows = ROWS;
|
|
36
|
+
private chunks: string[] = [];
|
|
37
|
+
write = (chunk: string, callback?: () => void): boolean => {
|
|
38
|
+
this.chunks.push(String(chunk));
|
|
39
|
+
callback?.();
|
|
40
|
+
return true;
|
|
41
|
+
};
|
|
42
|
+
stream(): string {
|
|
43
|
+
return this.chunks.join('');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class FakeStdin extends EventEmitter {
|
|
48
|
+
readonly isTTY = true;
|
|
49
|
+
private pending: string | null = null;
|
|
50
|
+
setEncoding(): void {}
|
|
51
|
+
setRawMode(): void {}
|
|
52
|
+
resume(): void {}
|
|
53
|
+
pause(): void {}
|
|
54
|
+
ref(): void {}
|
|
55
|
+
unref(): void {}
|
|
56
|
+
read(): string | null {
|
|
57
|
+
const data = this.pending;
|
|
58
|
+
this.pending = null;
|
|
59
|
+
return data;
|
|
60
|
+
}
|
|
61
|
+
send(data: string): void {
|
|
62
|
+
this.pending = data;
|
|
63
|
+
this.emit('readable');
|
|
64
|
+
this.emit('data', data);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function Harness({
|
|
69
|
+
focus = true,
|
|
70
|
+
bannerLines = 1,
|
|
71
|
+
}: {
|
|
72
|
+
focus?: boolean;
|
|
73
|
+
/** Rows rendered above the input — the layout the measured y must follow. */
|
|
74
|
+
bannerLines?: number;
|
|
75
|
+
}): React.ReactElement {
|
|
76
|
+
const [value, setValue] = useState('');
|
|
77
|
+
return (
|
|
78
|
+
<Box flexDirection="column">
|
|
79
|
+
{Array.from({ length: bannerLines }, (_, index) => (
|
|
80
|
+
<Text key={index}>banner line {index}</Text>
|
|
81
|
+
))}
|
|
82
|
+
<Box>
|
|
83
|
+
<Text>{PROMPT}</Text>
|
|
84
|
+
<CjkTextInput value={value} onChange={setValue} availableWidth={COLS - 2} focus={focus} />
|
|
85
|
+
</Box>
|
|
86
|
+
<Text>status line</Text>
|
|
87
|
+
</Box>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface IScenario {
|
|
92
|
+
stdout: FakeTtyStdout;
|
|
93
|
+
stdin: FakeStdin;
|
|
94
|
+
unmount: () => void;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function renderScenario(focus?: boolean, bannerLines?: number): IScenario {
|
|
98
|
+
const stdout = new FakeTtyStdout();
|
|
99
|
+
const stdin = new FakeStdin();
|
|
100
|
+
const instance = render(
|
|
101
|
+
<TerminalCapabilitiesProvider overrides={{ imeCursorPositioning: true }}>
|
|
102
|
+
<Harness
|
|
103
|
+
{...(focus === undefined ? {} : { focus })}
|
|
104
|
+
{...(bannerLines === undefined ? {} : { bannerLines })}
|
|
105
|
+
/>
|
|
106
|
+
</TerminalCapabilitiesProvider>,
|
|
107
|
+
{
|
|
108
|
+
// Cast: ink wants real process streams; the fakes implement the parts its render loop uses.
|
|
109
|
+
stdout: stdout as unknown as NodeJS.WriteStream,
|
|
110
|
+
stdin: stdin as unknown as NodeJS.ReadStream,
|
|
111
|
+
interactive: true,
|
|
112
|
+
exitOnCtrlC: false,
|
|
113
|
+
patchConsole: false,
|
|
114
|
+
},
|
|
115
|
+
);
|
|
116
|
+
return { stdout, stdin, unmount: instance.unmount };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Ink throttles frames (≤30fps) and React effects settle asynchronously — allow both to flush. */
|
|
120
|
+
const settle = (): Promise<void> => new Promise((r) => setTimeout(r, 120));
|
|
121
|
+
|
|
122
|
+
const INPUT_ROW = 1; // banner is row 0, input row is row 1 in the harness frame
|
|
123
|
+
|
|
124
|
+
describe('CLI-062 — real cursor positioning (interactive ink render)', () => {
|
|
125
|
+
const ORIGINAL_CHALK_LEVEL = chalk.level;
|
|
126
|
+
|
|
127
|
+
beforeAll(() => {
|
|
128
|
+
chalk.level = 3; // make the drawn inverse cursor observable in bytes, so suppression is provable
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
afterAll(() => {
|
|
132
|
+
vi.unstubAllEnvs();
|
|
133
|
+
chalk.level = ORIGINAL_CHALK_LEVEL;
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
afterEach(() => {
|
|
137
|
+
vi.restoreAllMocks();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('shows the hardware cursor on the input row at the composition column and tracks CJK growth', async () => {
|
|
141
|
+
const stdoutSpy = vi.spyOn(process.stdout, 'write');
|
|
142
|
+
const stderrSpy = vi.spyOn(process.stderr, 'write');
|
|
143
|
+
|
|
144
|
+
const { stdout, stdin, unmount } = renderScenario();
|
|
145
|
+
await settle();
|
|
146
|
+
|
|
147
|
+
stdin.send(korean.greetingFirstTwoSyllables);
|
|
148
|
+
await settle();
|
|
149
|
+
|
|
150
|
+
const afterTwo = interpretVtStream(stdout.stream(), ROWS, COLS);
|
|
151
|
+
const showsAfterTwo = afterTwo.showEvents;
|
|
152
|
+
expect(showsAfterTwo.length).toBeGreaterThan(0);
|
|
153
|
+
// Every show lands on the input row (never the banner/top region — the crash geometry).
|
|
154
|
+
for (const event of showsAfterTwo) {
|
|
155
|
+
expect(event.row).toBe(INPUT_ROW);
|
|
156
|
+
}
|
|
157
|
+
// Composition column: '> ' (2 cols) + two syllables (2 wide chars = 4 cols).
|
|
158
|
+
expect(showsAfterTwo.at(-1)).toMatchObject({ row: INPUT_ROW, col: 6 });
|
|
159
|
+
|
|
160
|
+
stdin.send(korean.greetingThirdSyllable); // one more wide char → +2 columns
|
|
161
|
+
await settle();
|
|
162
|
+
|
|
163
|
+
const afterThree = interpretVtStream(stdout.stream(), ROWS, COLS);
|
|
164
|
+
expect(afterThree.showEvents.at(-1)).toMatchObject({ row: INPUT_ROW, col: 8 });
|
|
165
|
+
|
|
166
|
+
// Drawn-cursor suppression: before the first measurement the fallback legitimately draws the
|
|
167
|
+
// inverse block (I1: no guessed rows), but from the first positioned show onward the hardware
|
|
168
|
+
// cursor and the inverse block must never coexist.
|
|
169
|
+
const firstShowOffset = afterThree.showEvents[0]!.offset;
|
|
170
|
+
expect(stdout.stream().slice(firstShowOffset)).not.toContain('\u001b[7m');
|
|
171
|
+
|
|
172
|
+
// I3/PoC lesson: the production path writes NOTHING to the real process streams.
|
|
173
|
+
expect(stdoutSpy).not.toHaveBeenCalled();
|
|
174
|
+
expect(stderrSpy).not.toHaveBeenCalled();
|
|
175
|
+
|
|
176
|
+
unmount();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* SIGSEGV regression guard, as a test rather than an argument.
|
|
181
|
+
*
|
|
182
|
+
* The Terminal.app Korean-IME crash was root-caused to `setCursorPosition({x, y: 0})` — a
|
|
183
|
+
* CONSTANT row that pointed the hardware cursor at the logo area. The mechanical statement that
|
|
184
|
+
* the bug class is gone is not "the literal is absent from the source" but "the emitted row is a
|
|
185
|
+
* function of the layout": move the input box down and the positioned row must move with it. A
|
|
186
|
+
* hardcoded `y: 0` — or any other constant — cannot satisfy both halves of this test.
|
|
187
|
+
*/
|
|
188
|
+
it('SIGSEGV guard: the positioned row FOLLOWS the layout, so a constant y (the y:0 bug) is unrepresentable', async () => {
|
|
189
|
+
const rowsSeen: number[] = [];
|
|
190
|
+
for (const bannerLines of [1, 4]) {
|
|
191
|
+
const { stdout, stdin, unmount } = renderScenario(true, bannerLines);
|
|
192
|
+
await settle();
|
|
193
|
+
stdin.send(korean.greetingFirstTwoSyllables);
|
|
194
|
+
await settle();
|
|
195
|
+
|
|
196
|
+
const vt = interpretVtStream(stdout.stream(), ROWS, COLS);
|
|
197
|
+
expect(vt.showEvents.length, `banner=${bannerLines}`).toBeGreaterThan(0);
|
|
198
|
+
const row = vt.showEvents.at(-1)!.row;
|
|
199
|
+
// The input box starts immediately below the banner, so its measured row IS bannerLines.
|
|
200
|
+
expect(row, `banner=${bannerLines}`).toBe(bannerLines);
|
|
201
|
+
// ...and never the top of the frame while any banner occupies it (the crash geometry).
|
|
202
|
+
expect(row, `banner=${bannerLines}`).toBeGreaterThan(0);
|
|
203
|
+
rowsSeen.push(row);
|
|
204
|
+
unmount();
|
|
205
|
+
await settle();
|
|
206
|
+
}
|
|
207
|
+
// Two different layouts must yield two different rows — a constant cannot pass both.
|
|
208
|
+
expect(new Set(rowsSeen).size).toBe(2);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('I4: unfocused input never positions the cursor (and keeps the fallback drawn-cursor path off)', async () => {
|
|
212
|
+
const { stdout, stdin, unmount } = renderScenario(false);
|
|
213
|
+
await settle();
|
|
214
|
+
stdin.send('x'); // ignored — not focused
|
|
215
|
+
await settle();
|
|
216
|
+
|
|
217
|
+
const vt = interpretVtStream(stdout.stream(), ROWS, COLS);
|
|
218
|
+
expect(vt.showEvents).toEqual([]);
|
|
219
|
+
unmount();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('I4: blur withdraws the position (no shows after focus loss) and unmount leaves the cursor visible', async () => {
|
|
223
|
+
const stdout = new FakeTtyStdout();
|
|
224
|
+
const stdin = new FakeStdin();
|
|
225
|
+
const instance = render(
|
|
226
|
+
<TerminalCapabilitiesProvider overrides={{ imeCursorPositioning: true }}>
|
|
227
|
+
<Harness />
|
|
228
|
+
</TerminalCapabilitiesProvider>,
|
|
229
|
+
{
|
|
230
|
+
stdout: stdout as unknown as NodeJS.WriteStream,
|
|
231
|
+
stdin: stdin as unknown as NodeJS.ReadStream,
|
|
232
|
+
interactive: true,
|
|
233
|
+
exitOnCtrlC: false,
|
|
234
|
+
patchConsole: false,
|
|
235
|
+
},
|
|
236
|
+
);
|
|
237
|
+
await settle();
|
|
238
|
+
stdin.send(korean.greetingFirstSyllable);
|
|
239
|
+
await settle();
|
|
240
|
+
expect(interpretVtStream(stdout.stream(), ROWS, COLS).showEvents.length).toBeGreaterThan(0);
|
|
241
|
+
|
|
242
|
+
// Blur: the guard fails → setCursorPosition(undefined) → ink hides the hardware cursor and
|
|
243
|
+
// emits NO further positioned shows (the I4 fallback path).
|
|
244
|
+
instance.rerender(
|
|
245
|
+
<TerminalCapabilitiesProvider overrides={{ imeCursorPositioning: true }}>
|
|
246
|
+
<Harness focus={false} />
|
|
247
|
+
</TerminalCapabilitiesProvider>,
|
|
248
|
+
);
|
|
249
|
+
await settle();
|
|
250
|
+
const afterBlurMark = stdout.stream().length;
|
|
251
|
+
stdin.send('x'); // ignored — not focused; also must not resurrect the cursor
|
|
252
|
+
await settle();
|
|
253
|
+
const blurShows = interpretVtStream(stdout.stream(), ROWS, COLS).showEvents.filter(
|
|
254
|
+
(event) => event.offset >= afterBlurMark,
|
|
255
|
+
);
|
|
256
|
+
expect(blurShows).toEqual([]);
|
|
257
|
+
|
|
258
|
+
// Unmount: useCursor's cleanup propagates `undefined`; ink's teardown restores a bare,
|
|
259
|
+
// visible cursor (never a freshly positioned one).
|
|
260
|
+
instance.unmount();
|
|
261
|
+
await settle();
|
|
262
|
+
expect(interpretVtStream(stdout.stream(), ROWS, COLS).visible).toBe(true);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CMD-004 Phase 2 Stage C — TC-10 (/rename persistence proof, TUI half).
|
|
3
|
+
*
|
|
4
|
+
* The spec gates deleting the TUI `renameSession` mutation on a red-first proof that `/rename`
|
|
5
|
+
* (1) persists HOST-side — the session executes the rename, no TUI handler involved — and
|
|
6
|
+
* (2) updates the TUI title via the broadcast `session_renamed` event.
|
|
7
|
+
*
|
|
8
|
+
* Runs the REAL TuiInteractionChannel over a real InteractiveSession (scripted provider) with a
|
|
9
|
+
* command module mirroring the real `/rename` contract (returns the `session-rename` host action
|
|
10
|
+
* on the split contract — Stage E deleted the legacy effect union).
|
|
11
|
+
*
|
|
12
|
+
* Red-first evidence: against pre-Stage-C code the broadcast half FAILED (the TUI only reacted to
|
|
13
|
+
* the legacy effect, which Stage B strips after host execution — so the title never updated).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { Text } from 'ink';
|
|
17
|
+
import { render } from 'ink-testing-library';
|
|
18
|
+
import React from 'react';
|
|
19
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
20
|
+
|
|
21
|
+
import { createScriptedProvider } from '@robota-sdk/agent-core/testing';
|
|
22
|
+
import { useSideEffects } from '../hooks/useSideEffects.js';
|
|
23
|
+
import { TuiCliAdapterProvider } from '../tui-cli-adapter-context.js';
|
|
24
|
+
import { TuiInteractionChannel } from '../TuiInteractionChannel.js';
|
|
25
|
+
|
|
26
|
+
import type { IUseSideEffectsOptions } from '../hooks/side-effects-types.js';
|
|
27
|
+
import type { ITuiCliAdapter } from '../tui-cli-adapter.js';
|
|
28
|
+
import type { ICommandModule } from '@robota-sdk/agent-framework';
|
|
29
|
+
|
|
30
|
+
const READY_DEADLINE_MS = 10_000;
|
|
31
|
+
const POLL_MS = 25;
|
|
32
|
+
|
|
33
|
+
async function waitUntil(predicate: () => boolean, deadlineMs = READY_DEADLINE_MS): Promise<void> {
|
|
34
|
+
const deadline = Date.now() + deadlineMs;
|
|
35
|
+
while (Date.now() < deadline) {
|
|
36
|
+
if (predicate()) return;
|
|
37
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_MS));
|
|
38
|
+
}
|
|
39
|
+
expect(predicate()).toBe(true);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Mirrors the real session-command `/rename` contract: NO mutation in the command, host action only. */
|
|
43
|
+
const renameModule: ICommandModule = {
|
|
44
|
+
name: 'stage-c-rename-proof',
|
|
45
|
+
systemCommands: [
|
|
46
|
+
{
|
|
47
|
+
name: 'rename',
|
|
48
|
+
description: 'Rename the session (test mirror of the real /rename contract)',
|
|
49
|
+
requiresPermission: false,
|
|
50
|
+
execute: (_context, args) => ({
|
|
51
|
+
success: true,
|
|
52
|
+
message: `Session renamed to "${args}".`,
|
|
53
|
+
hostActions: [{ type: 'session-rename', name: args }],
|
|
54
|
+
}),
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function fakeCliAdapter(): ITuiCliAdapter {
|
|
60
|
+
return {
|
|
61
|
+
getUserSettingsPath: () => '/tmp/fake-settings.json',
|
|
62
|
+
readSettings: () => ({}),
|
|
63
|
+
reloadPluginCommandSource: vi.fn(),
|
|
64
|
+
applyActiveModelChange: vi.fn().mockReturnValue({ applied: true }),
|
|
65
|
+
getGitBranch: vi.fn().mockReturnValue(undefined),
|
|
66
|
+
getProviderDisplayName: vi.fn((type: string) => type),
|
|
67
|
+
} as unknown as ITuiCliAdapter;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
describe('TC-10 — /rename persists host-side and the TUI title follows the broadcast', () => {
|
|
71
|
+
let channel: TuiInteractionChannel | undefined;
|
|
72
|
+
|
|
73
|
+
afterEach(async () => {
|
|
74
|
+
await channel?.stop();
|
|
75
|
+
channel = undefined;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('host executes the rename; the TUI updates its title from session_renamed only', async () => {
|
|
79
|
+
const scripted = createScriptedProvider([{ text: 'unused in this test' }]);
|
|
80
|
+
channel = new TuiInteractionChannel({
|
|
81
|
+
cwd: process.cwd(),
|
|
82
|
+
provider: scripted.provider,
|
|
83
|
+
sessionName: 'Before Rename',
|
|
84
|
+
commandModules: [renameModule],
|
|
85
|
+
});
|
|
86
|
+
await channel.start();
|
|
87
|
+
const session = channel.getSession();
|
|
88
|
+
await waitUntil(() => {
|
|
89
|
+
try {
|
|
90
|
+
session.getContextState();
|
|
91
|
+
return true;
|
|
92
|
+
} catch {
|
|
93
|
+
return false; // allow-fallback: session init is asynchronous; poll until it is ready
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const setSessionName = vi.fn();
|
|
98
|
+
const options = {
|
|
99
|
+
cwd: process.cwd(),
|
|
100
|
+
uiEventPort: session,
|
|
101
|
+
commandEffectQueue: {
|
|
102
|
+
enqueueEffects: () => undefined,
|
|
103
|
+
drain: () => undefined,
|
|
104
|
+
clear: () => undefined,
|
|
105
|
+
},
|
|
106
|
+
addEntry: vi.fn(),
|
|
107
|
+
baseHandleSubmit: (input: string) => channel!.handleInput(input),
|
|
108
|
+
setSessionName,
|
|
109
|
+
setStatusLineSettings: vi.fn(),
|
|
110
|
+
refreshStatusLineSettings: vi.fn(),
|
|
111
|
+
refreshAppearanceSettings: vi.fn(),
|
|
112
|
+
showSessionPickerOnStart: false,
|
|
113
|
+
} as unknown as IUseSideEffectsOptions;
|
|
114
|
+
|
|
115
|
+
const submitRef: { current: ((input: string) => Promise<void>) | null } = { current: null };
|
|
116
|
+
function Probe(): React.ReactElement {
|
|
117
|
+
const state = useSideEffects(options);
|
|
118
|
+
submitRef.current = state.handleSubmit;
|
|
119
|
+
return <Text>probe</Text>;
|
|
120
|
+
}
|
|
121
|
+
const instance = render(
|
|
122
|
+
<TuiCliAdapterProvider value={fakeCliAdapter()}>
|
|
123
|
+
<Probe />
|
|
124
|
+
</TuiCliAdapterProvider>,
|
|
125
|
+
);
|
|
126
|
+
await waitUntil(() => submitRef.current !== null);
|
|
127
|
+
|
|
128
|
+
await submitRef.current!('/rename Stage C Proof');
|
|
129
|
+
|
|
130
|
+
// (1) HOST persistence: the session itself performed the rename — no TUI mutation handler.
|
|
131
|
+
expect(session.getName()).toBe('Stage C Proof');
|
|
132
|
+
|
|
133
|
+
// (2) Title update via the broadcast `session_renamed` (the dual-carry legacy effect is
|
|
134
|
+
// stripped by the host applier, so this is the ONLY path that can update the title).
|
|
135
|
+
await waitUntil(() => setSessionName.mock.calls.length > 0, 3000);
|
|
136
|
+
expect(setSessionName).toHaveBeenCalledWith('Stage C Proof');
|
|
137
|
+
|
|
138
|
+
instance.unmount();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import type { IAIProvider } from '@robota-sdk/agent-core';
|
|
3
|
+
import { createNodeHostSettingsSource } from '@robota-sdk/agent-framework';
|
|
4
|
+
import { createNodeHostContributionSource } from '@robota-sdk/agent-framework';
|
|
5
|
+
import type { IPromptHistoryOptions } from '@robota-sdk/agent-framework';
|
|
6
|
+
import type { ITuiCliAdapter } from '../tui-cli-adapter.js';
|
|
7
|
+
import { toChannelOptions } from '../render.js';
|
|
8
|
+
import { buildTuiSessionOptions } from '../tui-session-options.js';
|
|
9
|
+
import type { IRenderOptions } from '../render.js';
|
|
10
|
+
|
|
11
|
+
describe('toChannelOptions', () => {
|
|
12
|
+
it('keeps host model identifiers through render, channel, and session', () => {
|
|
13
|
+
const channel = toChannelOptions({
|
|
14
|
+
cwd: '/tmp/project',
|
|
15
|
+
provider: {} as IAIProvider,
|
|
16
|
+
cliAdapter: {} as ITuiCliAdapter,
|
|
17
|
+
promptFileReferenceTag: 'acme_files',
|
|
18
|
+
modelCommandToolPrefix: 'acme_command_',
|
|
19
|
+
observerFailureWarningCode: 'ACME_BACKGROUND_OBSERVER_FAILURE',
|
|
20
|
+
subagentHookEnvironmentNames: { agentId: 'ACME_AGENT_ID', agentType: 'ACME_AGENT_TYPE' },
|
|
21
|
+
commandHookShell: '/bin/bash',
|
|
22
|
+
});
|
|
23
|
+
const session = buildTuiSessionOptions(channel);
|
|
24
|
+
expect(session.promptFileReferenceTag).toBe('acme_files');
|
|
25
|
+
expect(session.modelCommandToolPrefix).toBe('acme_command_');
|
|
26
|
+
expect(session.observerFailureWarningCode).toBe('ACME_BACKGROUND_OBSERVER_FAILURE');
|
|
27
|
+
expect(session.subagentHookEnvironmentNames).toEqual({
|
|
28
|
+
agentId: 'ACME_AGENT_ID',
|
|
29
|
+
agentType: 'ACME_AGENT_TYPE',
|
|
30
|
+
});
|
|
31
|
+
expect('commandHookShell' in session && session.commandHookShell).toBe('/bin/bash');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('keeps safe mode (no instructions, plugins or settings hooks) through render, channel, and session', () => {
|
|
35
|
+
const channel = toChannelOptions({
|
|
36
|
+
cwd: '/tmp/project',
|
|
37
|
+
provider: {} as IAIProvider,
|
|
38
|
+
cliAdapter: {} as ITuiCliAdapter,
|
|
39
|
+
bare: true,
|
|
40
|
+
skipConfiguredHooks: true,
|
|
41
|
+
});
|
|
42
|
+
const session = buildTuiSessionOptions(channel);
|
|
43
|
+
expect('bare' in session && session.bare).toBe(true);
|
|
44
|
+
expect('skipConfiguredHooks' in session && session.skipConfiguredHooks).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('preserves host permission baselines through render, channel, and session', () => {
|
|
48
|
+
const baselinePermissionAllow = ['Read(custom/**)'];
|
|
49
|
+
const channel = toChannelOptions({
|
|
50
|
+
cwd: '/tmp/project',
|
|
51
|
+
provider: {} as IAIProvider,
|
|
52
|
+
cliAdapter: {} as ITuiCliAdapter,
|
|
53
|
+
baselinePermissionAllow,
|
|
54
|
+
});
|
|
55
|
+
const session = buildTuiSessionOptions(channel);
|
|
56
|
+
if (!('baselinePermissionAllow' in session)) {
|
|
57
|
+
throw new Error('TUI standard session options must preserve the host permission baseline.');
|
|
58
|
+
}
|
|
59
|
+
expect(session.baselinePermissionAllow).toBe(baselinePermissionAllow);
|
|
60
|
+
});
|
|
61
|
+
it('keeps the host task-context selection through render, channel, and session', () => {
|
|
62
|
+
const taskContext = { enabled: true, dir: 'custom/tasks' };
|
|
63
|
+
const channel = toChannelOptions({
|
|
64
|
+
cwd: '/test-project',
|
|
65
|
+
provider: {} as IAIProvider,
|
|
66
|
+
cliAdapter: {} as ITuiCliAdapter,
|
|
67
|
+
taskContext,
|
|
68
|
+
});
|
|
69
|
+
expect(channel.taskContext).toBe(taskContext);
|
|
70
|
+
const session = buildTuiSessionOptions(channel);
|
|
71
|
+
if (!('taskContext' in session)) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
'TUI standard session options must preserve the host task-context selection.',
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
expect(session.taskContext).toBe(taskContext);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('keeps the host contribution sources and skill roots through render, channel, and session', () => {
|
|
80
|
+
const sources = [createNodeHostContributionSource('/test-project')];
|
|
81
|
+
const skillRoots = [{ root: 'custom/skills', kind: 'skills' as const }];
|
|
82
|
+
const channel = toChannelOptions({
|
|
83
|
+
cwd: '/test-project',
|
|
84
|
+
provider: {} as IAIProvider,
|
|
85
|
+
cliAdapter: {} as ITuiCliAdapter,
|
|
86
|
+
contributionSources: sources,
|
|
87
|
+
skillRoots,
|
|
88
|
+
});
|
|
89
|
+
const session = buildTuiSessionOptions(channel);
|
|
90
|
+
if (!('contributionSources' in session) || !('skillRoots' in session)) {
|
|
91
|
+
throw new Error('TUI standard session options must preserve host skill discovery settings.');
|
|
92
|
+
}
|
|
93
|
+
expect(session.contributionSources).toBe(sources);
|
|
94
|
+
expect(session.skillRoots).toBe(skillRoots);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('keeps the host user settings sources through render, channel, and session', () => {
|
|
98
|
+
const sources = [createNodeHostSettingsSource('user', '/test-home/settings.json')];
|
|
99
|
+
const channel = toChannelOptions({
|
|
100
|
+
cwd: '/tmp/project',
|
|
101
|
+
provider: {} as IAIProvider,
|
|
102
|
+
cliAdapter: {} as ITuiCliAdapter,
|
|
103
|
+
userSettingsSources: sources,
|
|
104
|
+
});
|
|
105
|
+
expect(buildTuiSessionOptions(channel).userSettingsSources).toBe(sources);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('forwards the loop kill switch through the channel to the session', () => {
|
|
109
|
+
const options = toChannelOptions({
|
|
110
|
+
cwd: '/tmp/project',
|
|
111
|
+
provider: {} as IAIProvider,
|
|
112
|
+
cliAdapter: {} as ITuiCliAdapter,
|
|
113
|
+
disableSessionLoops: true,
|
|
114
|
+
});
|
|
115
|
+
expect(options.disableSessionLoops).toBe(true);
|
|
116
|
+
expect(buildTuiSessionOptions(options).disableSessionLoops).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('TC-02: threads allowedTools and deniedTools into the channel options', () => {
|
|
120
|
+
const renderOptions: IRenderOptions = {
|
|
121
|
+
cwd: '/tmp/project',
|
|
122
|
+
provider: {} as IAIProvider,
|
|
123
|
+
cliAdapter: {} as ITuiCliAdapter,
|
|
124
|
+
allowedTools: ['Read'],
|
|
125
|
+
deniedTools: ['Bash'],
|
|
126
|
+
};
|
|
127
|
+
const channelOptions = toChannelOptions(renderOptions, 'session-1');
|
|
128
|
+
expect(channelOptions.allowedTools).toEqual(['Read']);
|
|
129
|
+
expect(channelOptions.deniedTools).toEqual(['Bash']);
|
|
130
|
+
expect(channelOptions.resumeSessionId).toBe('session-1');
|
|
131
|
+
expect(channelOptions.cwd).toBe('/tmp/project');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('leaves tool filters undefined when not provided', () => {
|
|
135
|
+
const channelOptions = toChannelOptions({
|
|
136
|
+
cwd: '/tmp/project',
|
|
137
|
+
provider: {} as IAIProvider,
|
|
138
|
+
cliAdapter: {} as ITuiCliAdapter,
|
|
139
|
+
});
|
|
140
|
+
expect(channelOptions.allowedTools).toBeUndefined();
|
|
141
|
+
expect(channelOptions.deniedTools).toBeUndefined();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('CLI-076: threads the display modelId into the channel model override', () => {
|
|
145
|
+
const channelOptions = toChannelOptions({
|
|
146
|
+
cwd: '/tmp/project',
|
|
147
|
+
provider: {} as IAIProvider,
|
|
148
|
+
cliAdapter: {} as ITuiCliAdapter,
|
|
149
|
+
modelId: 'claude-haiku-4-5-20251001',
|
|
150
|
+
});
|
|
151
|
+
// The status-line model id IS the session's model override (header == the model actually called).
|
|
152
|
+
expect(channelOptions.model).toBe('claude-haiku-4-5-20251001');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('CLI-076: leaves model undefined when no modelId is resolved', () => {
|
|
156
|
+
const channelOptions = toChannelOptions({
|
|
157
|
+
cwd: '/tmp/project',
|
|
158
|
+
provider: {} as IAIProvider,
|
|
159
|
+
cliAdapter: {} as ITuiCliAdapter,
|
|
160
|
+
});
|
|
161
|
+
expect(channelOptions.model).toBeUndefined();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('carries the host external-event verifier factory to the session options', () => {
|
|
165
|
+
const factory = () => ({ verify: async () => ({ admitted: true as const }) });
|
|
166
|
+
const channelOptions = toChannelOptions({
|
|
167
|
+
cwd: '/tmp/project',
|
|
168
|
+
provider: {} as IAIProvider,
|
|
169
|
+
cliAdapter: {} as ITuiCliAdapter,
|
|
170
|
+
externalEventVerifierFactory: factory,
|
|
171
|
+
});
|
|
172
|
+
expect(channelOptions.externalEventVerifierFactory).toBe(factory);
|
|
173
|
+
expect(buildTuiSessionOptions(channelOptions).externalEventVerifierFactory).toBe(factory);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('SCREEN-1993: projects the prompt-history writer to the channel and on to the session options', () => {
|
|
177
|
+
const promptHistory = { writer: { append: () => undefined }, project: '/p' };
|
|
178
|
+
const channelOptions = toChannelOptions({
|
|
179
|
+
cwd: '/tmp/project',
|
|
180
|
+
provider: {} as IAIProvider,
|
|
181
|
+
cliAdapter: {} as ITuiCliAdapter,
|
|
182
|
+
promptHistory,
|
|
183
|
+
});
|
|
184
|
+
expect(channelOptions.promptHistory).toBe(promptHistory);
|
|
185
|
+
// The session-options union hides standard-only fields; read the projected field by name.
|
|
186
|
+
const session = (
|
|
187
|
+
options: ReturnType<typeof toChannelOptions>,
|
|
188
|
+
): { promptHistory?: IPromptHistoryOptions } =>
|
|
189
|
+
buildTuiSessionOptions(options) as { promptHistory?: IPromptHistoryOptions };
|
|
190
|
+
expect(session(channelOptions).promptHistory).toBe(promptHistory);
|
|
191
|
+
expect(
|
|
192
|
+
session(
|
|
193
|
+
toChannelOptions({
|
|
194
|
+
cwd: '/tmp/project',
|
|
195
|
+
provider: {} as IAIProvider,
|
|
196
|
+
cliAdapter: {} as ITuiCliAdapter,
|
|
197
|
+
}),
|
|
198
|
+
).promptHistory,
|
|
199
|
+
).toBeUndefined();
|
|
200
|
+
});
|
|
201
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { waitForRenderAndStop } from '../render.js';
|
|
4
|
+
|
|
5
|
+
describe('renderApp lifecycle ownership', () => {
|
|
6
|
+
it('awaits active channel teardown after Ink exits', async () => {
|
|
7
|
+
let release: (() => void) | undefined;
|
|
8
|
+
const stop = vi.fn(() => new Promise<void>((resolve) => (release = resolve)));
|
|
9
|
+
let settled = false;
|
|
10
|
+
|
|
11
|
+
const outcome = waitForRenderAndStop(
|
|
12
|
+
async () => {},
|
|
13
|
+
() => ({ stop }),
|
|
14
|
+
).then(() => {
|
|
15
|
+
settled = true;
|
|
16
|
+
});
|
|
17
|
+
await Promise.resolve();
|
|
18
|
+
|
|
19
|
+
expect(stop).toHaveBeenCalledTimes(1);
|
|
20
|
+
expect(settled).toBe(false);
|
|
21
|
+
release?.();
|
|
22
|
+
await outcome;
|
|
23
|
+
expect(settled).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('propagates channel teardown failure to the renderApp caller', async () => {
|
|
27
|
+
const stopError = new Error('transport stop failed');
|
|
28
|
+
|
|
29
|
+
await expect(
|
|
30
|
+
waitForRenderAndStop(
|
|
31
|
+
async () => {},
|
|
32
|
+
() => ({ stop: vi.fn().mockRejectedValue(stopError) }),
|
|
33
|
+
),
|
|
34
|
+
).rejects.toBe(stopError);
|
|
35
|
+
});
|
|
36
|
+
});
|