@robota-sdk/agent-ui-terminal 3.0.0-beta.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +27 -0
- package/dist/node/index.cjs +32 -0
- package/dist/node/index.d.cts +1066 -0
- package/dist/node/index.d.cts.map +1 -0
- package/dist/node/index.d.ts +1066 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +33 -0
- package/dist/node/index.js.map +1 -0
- package/package.json +93 -0
- package/src/App.tsx +146 -0
- package/src/AppPresentation.tsx +190 -0
- package/src/AppView.tsx +26 -0
- package/src/BackgroundTaskPanel.tsx +73 -0
- package/src/CjkTextInput.tsx +336 -0
- package/src/ConfirmPrompt.tsx +140 -0
- package/src/ContextWarningBanner.tsx +45 -0
- package/src/ExecutionWorkspaceDetailPane.tsx +85 -0
- package/src/ExecutionWorkspaceSwitcher.tsx +213 -0
- package/src/HistorySearchOverlay.tsx +150 -0
- package/src/InputArea.tsx +338 -0
- package/src/ListPicker.tsx +158 -0
- package/src/MenuSelect.tsx +159 -0
- package/src/MessageList.tsx +308 -0
- package/src/MultiSelectList.tsx +294 -0
- package/src/PendingActionPrompt.tsx +121 -0
- package/src/PermissionPrompt.tsx +186 -0
- package/src/PluginTUI.tsx +258 -0
- package/src/RoleLabel.tsx +72 -0
- package/src/SafeText.tsx +66 -0
- package/src/SessionEventNotices.tsx +27 -0
- package/src/SessionPicker.tsx +72 -0
- package/src/SessionStatusBar.tsx +76 -0
- package/src/SlashAutocomplete.tsx +154 -0
- package/src/StatusBar.tsx +284 -0
- package/src/StreamingIndicator.tsx +145 -0
- package/src/SupervisedSessionView.tsx +455 -0
- package/src/TextPrompt.tsx +112 -0
- package/src/ThemePicker.tsx +366 -0
- package/src/ToolCommandOutput.tsx +48 -0
- package/src/ToolDiffBlock.tsx +44 -0
- package/src/TransportTUI.tsx +166 -0
- package/src/TuiInteractionChannel.ts +567 -0
- package/src/UpdateNotice.tsx +18 -0
- package/src/UsageSummaryEntry.tsx +49 -0
- package/src/WaveText.tsx +70 -0
- package/src/__tests__/ListPicker.test.tsx +188 -0
- package/src/__tests__/MenuSelect.test.tsx +127 -0
- package/src/__tests__/PendingActionPrompt.test.tsx +193 -0
- package/src/__tests__/PluginTUI.test.tsx +167 -0
- package/src/__tests__/SlashAutocomplete.test.tsx +162 -0
- package/src/__tests__/SupervisedSessionView-render.test.tsx +24 -0
- package/src/__tests__/SupervisedSessionView.test.tsx +629 -0
- package/src/__tests__/TextPrompt.test.tsx +112 -0
- package/src/__tests__/TransportTUI.test.tsx +124 -0
- package/src/__tests__/TuiInteractionChannel.askUser.test.ts +124 -0
- package/src/__tests__/TuiInteractionChannel.display-contract.test.ts +284 -0
- package/src/__tests__/TuiInteractionChannel.lifecycle.test.ts +642 -0
- package/src/__tests__/UpdateNotice.test.tsx +15 -0
- package/src/__tests__/abort-after-permission.test.tsx +160 -0
- package/src/__tests__/abort-streaming-e2e.test.tsx +183 -0
- package/src/__tests__/attention-bell-and-marks.test.ts +144 -0
- package/src/__tests__/autocomplete-editing-keys.test.tsx +127 -0
- package/src/__tests__/background-task-panel-countdown.test.tsx +75 -0
- package/src/__tests__/background-task-panel.test.tsx +98 -0
- package/src/__tests__/background-task-row-format.test.ts +152 -0
- package/src/__tests__/channel-factory-integration.test.ts +150 -0
- package/src/__tests__/cjk-defer-submit.test.tsx +67 -0
- package/src/__tests__/cjk-fallback-render.test.tsx +78 -0
- package/src/__tests__/cjk-text-input-flow.test.ts +186 -0
- package/src/__tests__/cjk-text-input.test.ts +191 -0
- package/src/__tests__/command-handoff-pty-e2e.test.ts +107 -0
- package/src/__tests__/command-output-summary.test.ts +95 -0
- package/src/__tests__/command-result-handler.test.ts +171 -0
- package/src/__tests__/compact-event-bridge.test.ts +20 -0
- package/src/__tests__/confirm-permission-flow.test.ts +149 -0
- package/src/__tests__/confirm-prompt.test.tsx +103 -0
- package/src/__tests__/contextual-keybindings-input.test.tsx +158 -0
- package/src/__tests__/default-tui-cli-adapter-host-settings.test.ts +45 -0
- package/src/__tests__/effort-reaches-the-session.test.ts +41 -0
- package/src/__tests__/execution-workspace-switcher.test.tsx +147 -0
- package/src/__tests__/execution-workspace-view-model.test.ts +94 -0
- package/src/__tests__/external-prompt-notice.test.tsx +100 -0
- package/src/__tests__/fixtures/command-handoff-driver.tsx +111 -0
- package/src/__tests__/fixtures/fake-editor.sh +7 -0
- package/src/__tests__/fixtures/terminal-handoff-driver.tsx +90 -0
- package/src/__tests__/fork-attach.test.tsx +235 -0
- package/src/__tests__/helpers/terminal-profiles.ts +152 -0
- package/src/__tests__/helpers/vt-cursor-interpreter.ts +175 -0
- package/src/__tests__/history-clear-broadcast.test.ts +66 -0
- package/src/__tests__/history-search-overlay.test.tsx +329 -0
- package/src/__tests__/humanize-tool-name.test.ts +30 -0
- package/src/__tests__/input-area-bottom-border.test.tsx +45 -0
- package/src/__tests__/input-area-flow.test.ts +199 -0
- package/src/__tests__/input-area-focus-handoff.test.tsx +77 -0
- package/src/__tests__/interaction-channel-charter.test.ts +13 -0
- package/src/__tests__/key-hint-consistency.test.tsx +249 -0
- package/src/__tests__/key-hint-footer.test.tsx +69 -0
- package/src/__tests__/live-trace-reaches-session.test.ts +14 -0
- package/src/__tests__/message-list-rendering.test.tsx +473 -0
- package/src/__tests__/org-policy-reaches-the-channel.test.ts +77 -0
- package/src/__tests__/output-style-reaches-session.test.ts +35 -0
- package/src/__tests__/palette-consistency.test.ts +330 -0
- package/src/__tests__/permission-prompt-availability.test.tsx +23 -0
- package/src/__tests__/permission-prompt-peer.test.tsx +34 -0
- package/src/__tests__/product-display-name.test.tsx +79 -0
- package/src/__tests__/prompt-queue.test.tsx +260 -0
- package/src/__tests__/provider-definitions-reach-the-session.test.ts +75 -0
- package/src/__tests__/pty/ask-user-question.ptytest.ts +72 -0
- package/src/__tests__/pty/background-work-switcher.ptytest.ts +53 -0
- package/src/__tests__/pty/behavior-2437-git.ptytest.ts +342 -0
- package/src/__tests__/pty/fixtures/ask-user-question.jsonl +3 -0
- package/src/__tests__/pty/fixtures/replay-conversation.jsonl +4 -0
- package/src/__tests__/pty/fixtures/screen-005-ask-shapes.jsonl +5 -0
- package/src/__tests__/pty/fixtures/screen-005-permission.jsonl +3 -0
- package/src/__tests__/pty/fixtures/screen-006-colors.jsonl +3 -0
- package/src/__tests__/pty/fixtures/screen-2002-themes.jsonl +5 -0
- package/src/__tests__/pty/flag-tui.ptytest.ts +56 -0
- package/src/__tests__/pty/flow-2006-deep-link.ptytest.ts +291 -0
- package/src/__tests__/pty/ime-cursor-tmux.ptytest.ts +165 -0
- package/src/__tests__/pty/ime-cursor.ptytest.ts +186 -0
- package/src/__tests__/pty/isolated-home.ts +64 -0
- package/src/__tests__/pty/org-policy.ptytest.ts +71 -0
- package/src/__tests__/pty/provider-setup.ptytest.ts +61 -0
- package/src/__tests__/pty/pty-driver.ts +153 -0
- package/src/__tests__/pty/replay-conversation.ptytest.ts +68 -0
- package/src/__tests__/pty/screen-005-prompt-footers.ptytest.ts +134 -0
- package/src/__tests__/pty/screen-006-no-color.ptytest.ts +138 -0
- package/src/__tests__/pty/screen-010-scrollback.ptytest.ts +123 -0
- package/src/__tests__/pty/screen-1993-scrollback.ptytest.ts +124 -0
- package/src/__tests__/pty/screen-2002-themes.ptytest.ts +292 -0
- package/src/__tests__/pty/screen-2670-prepark.ptytest.ts +228 -0
- package/src/__tests__/pty/screen-reader-mode.ptytest.ts +228 -0
- package/src/__tests__/pty/settings-screen.ptytest.ts +49 -0
- package/src/__tests__/pty/spawn-pty.test.ts +120 -0
- package/src/__tests__/pty/spawn-pty.ts +229 -0
- package/src/__tests__/pty/terminal-handoff.ptytest.ts +79 -0
- package/src/__tests__/pty/tui-pty.ptytest.ts +65 -0
- package/src/__tests__/pty-driver-dispose.test.ts +43 -0
- package/src/__tests__/real-cursor-positioning.test.tsx +263 -0
- package/src/__tests__/rename-broadcast-persistence.test.tsx +140 -0
- package/src/__tests__/render-channel-options.test.ts +201 -0
- package/src/__tests__/render-lifecycle.test.ts +36 -0
- package/src/__tests__/render-markdown.test.ts +164 -0
- package/src/__tests__/rendered-markdown-styling.test.tsx +98 -0
- package/src/__tests__/safe-text-boundary.test.tsx +75 -0
- package/src/__tests__/screen-reader-input.test.tsx +106 -0
- package/src/__tests__/screen-reader-labels.test.ts +91 -0
- package/src/__tests__/screen-reader-menus.test.tsx +200 -0
- package/src/__tests__/screen-reader-pacing.test.ts +230 -0
- package/src/__tests__/screen-reader-prepark-echo.test.tsx +200 -0
- package/src/__tests__/screen-reader-render-options.test.ts +262 -0
- package/src/__tests__/screen-reader-stdout.test.ts +322 -0
- package/src/__tests__/screen-reader-turn-marks-port.test.tsx +85 -0
- package/src/__tests__/screen-reader-turn-signals.test.tsx +125 -0
- package/src/__tests__/sec-019-terminal-sanitizer.test.ts +276 -0
- package/src/__tests__/sec-019-tool-label-render.test.tsx +207 -0
- package/src/__tests__/selection-flow.test.ts +61 -0
- package/src/__tests__/session-event-notices.test.tsx +31 -0
- package/src/__tests__/session-init-poller.test.ts +102 -0
- package/src/__tests__/session-switch-channel.test.tsx +470 -0
- package/src/__tests__/shutdown-signal.test.ts +34 -0
- package/src/__tests__/status-activity.test.ts +71 -0
- package/src/__tests__/status-bar.test.tsx +282 -0
- package/src/__tests__/status-glyph.test.ts +30 -0
- package/src/__tests__/streaming-indicator.test.tsx +177 -0
- package/src/__tests__/terminal-capabilities.test.ts +153 -0
- package/src/__tests__/terminal-handoff-controller.test.ts +173 -0
- package/src/__tests__/terminal-handoff-pty-e2e.test.ts +77 -0
- package/src/__tests__/text-prompt-flow.test.ts +77 -0
- package/src/__tests__/theme-picker.test.tsx +345 -0
- package/src/__tests__/transport-boundary.test.ts +11 -0
- package/src/__tests__/tui-app-channel-port.test.ts +31 -0
- package/src/__tests__/tui-channel-init-failure.test.ts +62 -0
- package/src/__tests__/tui-channel-lifecycle-coordinator.test.ts +258 -0
- package/src/__tests__/tui-interaction-queues.test.ts +139 -0
- package/src/__tests__/tui-preset-capabilities.test.ts +33 -0
- package/src/__tests__/tui-state-manager.error-stall.test.ts +169 -0
- package/src/__tests__/tui-state-manager.test.ts +451 -0
- package/src/__tests__/ui-intent-rendering.test.tsx +291 -0
- package/src/__tests__/wave-text.test.tsx +133 -0
- package/src/app-banner.tsx +42 -0
- package/src/app-static-items.ts +35 -0
- package/src/app-view-model.ts +136 -0
- package/src/attention/__tests__/attention-coordinator.test.ts +166 -0
- package/src/attention/__tests__/attention-tracker.test.ts +103 -0
- package/src/attention/__tests__/countdown.test.ts +15 -0
- package/src/attention/__tests__/focus-input-filter.test.ts +92 -0
- package/src/attention/__tests__/interval-recap.test.ts +49 -0
- package/src/attention/attention-coordinator.ts +71 -0
- package/src/attention/attention-tracker.ts +124 -0
- package/src/attention/countdown.ts +24 -0
- package/src/attention/focus-input-filter.ts +131 -0
- package/src/attention/interval-recap.ts +148 -0
- package/src/attention/time-units.ts +6 -0
- package/src/attention-bell.ts +79 -0
- package/src/attributed-user-echo.ts +30 -0
- package/src/background-task-row-format.ts +99 -0
- package/src/command-interaction.ts +8 -0
- package/src/command-output-summary.ts +129 -0
- package/src/create-default-tui-cli-adapter.ts +45 -0
- package/src/execution-workspace-switcher-selection.ts +118 -0
- package/src/execution-workspace-view-model.ts +139 -0
- package/src/external-prompt-notice.tsx +38 -0
- package/src/flows/__tests__/background-focus-flow.test.ts +48 -0
- package/src/flows/__tests__/defer-submit.test.ts +87 -0
- package/src/flows/__tests__/real-cursor-flow.test.ts +124 -0
- package/src/flows/background-focus-flow.ts +44 -0
- package/src/flows/cjk-cursor-motion.ts +75 -0
- package/src/flows/cjk-text-deletion.ts +53 -0
- package/src/flows/cjk-text-input-flow.ts +248 -0
- package/src/flows/cjk-text-input-types.ts +24 -0
- package/src/flows/confirm-prompt-flow.ts +90 -0
- package/src/flows/defer-submit.ts +67 -0
- package/src/flows/fork-attach-flow.ts +62 -0
- package/src/flows/input-area-flow.ts +188 -0
- package/src/flows/permission-prompt-flow.ts +108 -0
- package/src/flows/real-cursor-flow.ts +83 -0
- package/src/flows/selection-flow.ts +205 -0
- package/src/flows/session-init-poller.ts +76 -0
- package/src/flows/text-prompt-flow.ts +93 -0
- package/src/history-search/__tests__/history-search-flow.test.ts +86 -0
- package/src/history-search/history-search-flow.ts +94 -0
- package/src/history-search/useHistorySearch.ts +336 -0
- package/src/hooks/__tests__/use-app-loop-escape.test.tsx +74 -0
- package/src/hooks/__tests__/use-plugin-callbacks.test.ts +18 -0
- package/src/hooks/__tests__/use-tui-channel-stability.test.tsx +100 -0
- package/src/hooks/__tests__/useAppThemeState.test.tsx +130 -0
- package/src/hooks/command-result-handler.ts +44 -0
- package/src/hooks/side-effects-types.ts +45 -0
- package/src/hooks/useAppController.ts +267 -0
- package/src/hooks/useAppInputBindings.ts +145 -0
- package/src/hooks/useAppInteractionState.ts +81 -0
- package/src/hooks/useAppLifecycleState.ts +81 -0
- package/src/hooks/useAppOverlays.ts +74 -0
- package/src/hooks/useAppScreenState.ts +81 -0
- package/src/hooks/useAppSubmissionState.ts +45 -0
- package/src/hooks/useAppThemeState.ts +157 -0
- package/src/hooks/useAppWorkspaceState.ts +149 -0
- package/src/hooks/useAppearanceSettings.ts +30 -0
- package/src/hooks/useAutocomplete.ts +101 -0
- package/src/hooks/useBackgroundPanel.ts +58 -0
- package/src/hooks/useCountdownTick.ts +24 -0
- package/src/hooks/useExecutionDetailPage.ts +69 -0
- package/src/hooks/useForkAttach.ts +60 -0
- package/src/hooks/useInputAreaHistorySearch.ts +72 -0
- package/src/hooks/useInputAreaKeys.ts +114 -0
- package/src/hooks/useNumberedSelection.ts +76 -0
- package/src/hooks/usePluginCallbacks.ts +38 -0
- package/src/hooks/usePluginScreenData.ts +85 -0
- package/src/hooks/useRealCursorPosition.ts +125 -0
- package/src/hooks/useRenderMarkdown.ts +38 -0
- package/src/hooks/useScreenReaderTurnSignals.ts +113 -0
- package/src/hooks/useSideEffects.ts +144 -0
- package/src/hooks/useStatusLineSettings.ts +50 -0
- package/src/hooks/useTerminalHandoffSuspension.ts +30 -0
- package/src/hooks/useTuiChannel.ts +129 -0
- package/src/humanize-tool-name.ts +51 -0
- package/src/index.ts +56 -0
- package/src/input-area-rules.tsx +71 -0
- package/src/json-value.ts +18 -0
- package/src/key-hint-footer.tsx +54 -0
- package/src/keybindings/__tests__/keybinding-registry.test.ts +228 -0
- package/src/keybindings/__tests__/node-keybindings-source.test.ts +111 -0
- package/src/keybindings/keybinding-catalogue.ts +171 -0
- package/src/keybindings/keybinding-registry.ts +303 -0
- package/src/keybindings/keybinding-syntax.ts +155 -0
- package/src/keybindings/keybindings-context.tsx +124 -0
- package/src/keybindings/node-keybindings-source.ts +189 -0
- package/src/numbered-list.tsx +113 -0
- package/src/plugin-tui-handlers.ts +163 -0
- package/src/product-display-name-context.tsx +30 -0
- package/src/render-markdown.ts +209 -0
- package/src/render.tsx +544 -0
- package/src/sanitize-terminal-text.ts +171 -0
- package/src/screen-reader-announcement.ts +52 -0
- package/src/screen-reader-context.tsx +32 -0
- package/src/screen-reader-labels.ts +64 -0
- package/src/screen-reader-pacing-context.tsx +47 -0
- package/src/screen-reader-pacing.ts +178 -0
- package/src/screen-reader-stdout.ts +328 -0
- package/src/shutdown-signal.ts +34 -0
- package/src/status-activity.ts +70 -0
- package/src/status-glyph.ts +78 -0
- package/src/terminal-capabilities-context.tsx +27 -0
- package/src/terminal-capabilities.ts +77 -0
- package/src/terminal-focus-reporting.ts +50 -0
- package/src/terminal-handoff-controller.ts +98 -0
- package/src/terminal-marks.ts +63 -0
- package/src/theme/__tests__/built-in-themes.test.ts +117 -0
- package/src/theme/__tests__/color-vision.test.ts +82 -0
- package/src/theme/__tests__/theme-context.test.tsx +146 -0
- package/src/theme/__tests__/theme-document.test.ts +222 -0
- package/src/theme/__tests__/theme-registry.test.ts +138 -0
- package/src/theme/__tests__/theme-styles.test.ts +233 -0
- package/src/theme/built-in-themes.ts +342 -0
- package/src/theme/color-vision.ts +171 -0
- package/src/theme/index.ts +45 -0
- package/src/theme/theme-context.tsx +81 -0
- package/src/theme/theme-contracts.ts +139 -0
- package/src/theme/theme-document.ts +278 -0
- package/src/theme/theme-registry.ts +127 -0
- package/src/theme/theme-styles.ts +177 -0
- package/src/tool-summary-status.ts +59 -0
- package/src/tui-app-channel-port.ts +83 -0
- package/src/tui-channel-lifecycle-coordinator.ts +149 -0
- package/src/tui-channel-options.ts +172 -0
- package/src/tui-cli-adapter-context.tsx +13 -0
- package/src/tui-cli-adapter.ts +24 -0
- package/src/tui-interaction-queues.ts +163 -0
- package/src/tui-session-binding.ts +52 -0
- package/src/tui-session-event-projector.ts +140 -0
- package/src/tui-session-events.ts +82 -0
- package/src/tui-session-notice-store.ts +43 -0
- package/src/tui-session-options.ts +118 -0
- package/src/tui-state-manager.ts +327 -0
- package/src/types.ts +19 -0
- package/src/use-terminal-title.ts +29 -0
- package/src/utils/__tests__/edit-diff.test.ts +426 -0
- package/src/utils/__tests__/paste-detection.test.ts +116 -0
- package/src/utils/__tests__/paste-labels.test.ts +46 -0
- package/src/utils/__tests__/tool-call-extractor.test.ts +227 -0
- package/src/utils/__tests__/tool-diff-summary.test.ts +104 -0
- package/src/utils/edit-diff.ts +151 -0
- package/src/utils/input-top-border.ts +19 -0
- package/src/utils/paste-labels.ts +9 -0
- package/src/utils/tool-call-extractor.ts +92 -0
- package/src/utils/tool-diff-summary.ts +75 -0
|
@@ -0,0 +1,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 { EditCheckpointStore, 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('threads the explicit checkpoint capability without deriving it from cwd', () => {
|
|
165
|
+
const editCheckpointStore = {} as EditCheckpointStore;
|
|
166
|
+
const channelOptions = toChannelOptions({
|
|
167
|
+
cwd: '/tmp/project',
|
|
168
|
+
provider: {} as IAIProvider,
|
|
169
|
+
cliAdapter: {} as ITuiCliAdapter,
|
|
170
|
+
editCheckpointStore,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
expect(channelOptions.editCheckpointStore).toBe(editCheckpointStore);
|
|
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
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { renderMarkdown } from '../render-markdown.js';
|
|
5
|
+
import { DARK_DALTONIZED_THEME, DARK_THEME } from '../theme/built-in-themes.js';
|
|
6
|
+
import { foreground } from '../theme/index.js';
|
|
7
|
+
|
|
8
|
+
const ANSI_LIGHT_RED = '\u001b[38;5;210m';
|
|
9
|
+
const ANSI_LIGHT_GREEN = '\u001b[38;5;120m';
|
|
10
|
+
const ANSI_DARK_RED_BACKGROUND = '\u001b[48;5;52m';
|
|
11
|
+
const ANSI_DARK_GREEN_BACKGROUND = '\u001b[48;5;22m';
|
|
12
|
+
// SCREEN-2002: the rows are styled through chalk now, which closes a background+foreground pair with
|
|
13
|
+
// its own paired resets instead of the blanket `ESC[0m` the hand-written escapes used. The colours
|
|
14
|
+
// and their order are unchanged; this is the one recorded byte difference.
|
|
15
|
+
const ANSI_RESET_FOREGROUND = '\u001b[39m';
|
|
16
|
+
const ANSI_RESET_BACKGROUND = '\u001b[49m';
|
|
17
|
+
const CODE_BLOCK_INDENT = ' ';
|
|
18
|
+
/** Any SGR introducer — a plain block must carry none. */
|
|
19
|
+
const SGR_ANY = '\u001b[';
|
|
20
|
+
|
|
21
|
+
describe('renderMarkdown', () => {
|
|
22
|
+
it('renders diff fenced code blocks with addition and removal colors', () => {
|
|
23
|
+
const output = renderMarkdown(
|
|
24
|
+
['Before', '', '```diff', '- const oldValue = true;', '+ const newValue = true;', '```'].join(
|
|
25
|
+
'\n',
|
|
26
|
+
),
|
|
27
|
+
{ color: true },
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
expect(output).toContain(`${ANSI_DARK_RED_BACKGROUND}${ANSI_LIGHT_RED}`);
|
|
31
|
+
expect(output).toContain(`${CODE_BLOCK_INDENT}- const oldValue = true;`);
|
|
32
|
+
expect(output).toContain(`${ANSI_DARK_GREEN_BACKGROUND}${ANSI_LIGHT_GREEN}`);
|
|
33
|
+
expect(output).toContain(`${CODE_BLOCK_INDENT}+ const newValue = true;`);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('pads added and removed diff rows before applying background colors', () => {
|
|
37
|
+
const codeBlockWidth = 24;
|
|
38
|
+
const removedRow = `${CODE_BLOCK_INDENT}- removed`.padEnd(codeBlockWidth);
|
|
39
|
+
const addedRow = `${CODE_BLOCK_INDENT}+ added`.padEnd(codeBlockWidth);
|
|
40
|
+
const output = renderMarkdown(['```diff', '- removed', '+ added', '```'].join('\n'), {
|
|
41
|
+
color: true,
|
|
42
|
+
codeBlockWidth,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(output).toContain(
|
|
46
|
+
`${ANSI_DARK_RED_BACKGROUND}${ANSI_LIGHT_RED}${removedRow}${ANSI_RESET_FOREGROUND}${ANSI_RESET_BACKGROUND}`,
|
|
47
|
+
);
|
|
48
|
+
expect(output).toContain(
|
|
49
|
+
`${ANSI_DARK_GREEN_BACKGROUND}${ANSI_LIGHT_GREEN}${addedRow}${ANSI_RESET_FOREGROUND}${ANSI_RESET_BACKGROUND}`,
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('keeps diff fenced code block content readable when color is disabled', () => {
|
|
54
|
+
const output = renderMarkdown(
|
|
55
|
+
['```diff', '- removed line', '+ added line', ' unchanged line', '```'].join('\n'),
|
|
56
|
+
{ color: false },
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
expect(output).toContain('- removed line');
|
|
60
|
+
expect(output).toContain('+ added line');
|
|
61
|
+
expect(output).toContain(' unchanged line');
|
|
62
|
+
expect(output).not.toContain(ANSI_LIGHT_RED);
|
|
63
|
+
expect(output).not.toContain(ANSI_LIGHT_GREEN);
|
|
64
|
+
expect(output).not.toContain(ANSI_DARK_RED_BACKGROUND);
|
|
65
|
+
expect(output).not.toContain(ANSI_DARK_GREEN_BACKGROUND);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('keeps regular fenced code blocks as code output', () => {
|
|
69
|
+
const output = renderMarkdown(['```ts', 'const value: string = "ok";', '```'].join('\n'), {
|
|
70
|
+
color: false,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
expect(output).toContain('const value: string = "ok";');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* SCREEN-2002 TC-03 — the theme reaches `cli-highlight`.
|
|
78
|
+
*
|
|
79
|
+
* This is the unit's most dependency-fragile claim, and the failure it guards against is SILENT:
|
|
80
|
+
* `cli-highlight` falls back PER KEY to its own `DEFAULT_THEME`, whose `keyword` is red and whose
|
|
81
|
+
* `addition` is green. If the theme stopped reaching it, a daltonized run would quietly get the
|
|
82
|
+
* red/green pair back with every other surface still daltonized, and nothing else in the suite
|
|
83
|
+
* would notice.
|
|
84
|
+
*/
|
|
85
|
+
describe('SCREEN-2002 TC-03: syntax highlighting follows the theme', () => {
|
|
86
|
+
const TS_BLOCK = ['```ts', 'const value = 1;', '```'].join('\n');
|
|
87
|
+
const TRUECOLOR = 3;
|
|
88
|
+
const previousLevel = chalk.level;
|
|
89
|
+
afterEach(() => {
|
|
90
|
+
chalk.level = previousLevel;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const openCode = (color: string): string => foreground(color)('x').split('x')[0] ?? '';
|
|
94
|
+
|
|
95
|
+
it('colours a keyword with the theme s value, not cli-highlight s default', () => {
|
|
96
|
+
chalk.level = TRUECOLOR;
|
|
97
|
+
const daltonized = renderMarkdown(TS_BLOCK, {
|
|
98
|
+
color: true,
|
|
99
|
+
theme: DARK_DALTONIZED_THEME,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(daltonized).toContain(openCode(DARK_DALTONIZED_THEME.syntax.keyword));
|
|
103
|
+
// Recorded, because it bounds what this test can prove: `cli-highlight` depends on chalk 4 and
|
|
104
|
+
// resolves to its OWN instance, so `chalk.level` set here does not reach it and its per-key
|
|
105
|
+
// fallback emits nothing in-process — it is only coloured in a real terminal. The positive
|
|
106
|
+
// assertion above is therefore the whole guard; a `not.toContain` on the fallback's colour
|
|
107
|
+
// would be inert. The end-to-end colour is evidenced by the PTY scenario instead.
|
|
108
|
+
expect(DARK_THEME.syntax.keyword).not.toBe(DARK_DALTONIZED_THEME.syntax.keyword);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('renders a code block as plain indented text when highlighting is off', () => {
|
|
112
|
+
chalk.level = TRUECOLOR;
|
|
113
|
+
const plain = renderMarkdown(TS_BLOCK, {
|
|
114
|
+
color: true,
|
|
115
|
+
theme: DARK_DALTONIZED_THEME,
|
|
116
|
+
syntaxHighlighting: false,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(plain).toContain(`${CODE_BLOCK_INDENT}const value = 1;`);
|
|
120
|
+
expect(plain).not.toContain(openCode(DARK_DALTONIZED_THEME.syntax.keyword));
|
|
121
|
+
expect(plain).not.toContain(SGR_ANY);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* CLI-2004 TC-04 — the `Header: value` flattening, asserted at the single choke point every
|
|
127
|
+
* render path shares. A box-drawn grid is read aloud as its rules; the pairing is the content.
|
|
128
|
+
*/
|
|
129
|
+
describe('CLI-2004 TC-04: screen-reader table flattening', () => {
|
|
130
|
+
const TABLE = ['| A | B |', '|---|---|', '| 1 | 2 |'].join('\n');
|
|
131
|
+
const BOX_DRAWING = /[\u2502\u2500\u250c\u2510\u2514\u2518\u251c\u2524\u252c\u2534\u253c]/u;
|
|
132
|
+
|
|
133
|
+
it('flattens a table to one `Header: value` line per cell in the mode', () => {
|
|
134
|
+
const output = renderMarkdown(TABLE, { color: false, screenReader: true });
|
|
135
|
+
|
|
136
|
+
const lines = output.split('\n').map((line) => line.trim());
|
|
137
|
+
expect(lines).toContain('A: 1');
|
|
138
|
+
expect(lines).toContain('B: 2');
|
|
139
|
+
expect(lines.indexOf('A: 1')).toBeLessThan(lines.indexOf('B: 2'));
|
|
140
|
+
expect(output).not.toMatch(BOX_DRAWING);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('separates rows with a blank line so the row boundary is audible', () => {
|
|
144
|
+
const twoRows = ['| A | B |', '|---|---|', '| 1 | 2 |', '| 3 | 4 |'].join('\n');
|
|
145
|
+
const output = renderMarkdown(twoRows, { color: false, screenReader: true });
|
|
146
|
+
|
|
147
|
+
expect(output).toContain('A: 1\nB: 2\n\nA: 3\nB: 4');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('leaves the box-drawn grid exactly as it is outside the mode', () => {
|
|
151
|
+
const output = renderMarkdown(TABLE, { color: false });
|
|
152
|
+
|
|
153
|
+
expect(output).toMatch(BOX_DRAWING);
|
|
154
|
+
expect(output).not.toContain('A: 1');
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('keeps inline markdown formatting readable', () => {
|
|
159
|
+
const output = renderMarkdown('Use **bold** and `code` here.', { color: false });
|
|
160
|
+
|
|
161
|
+
expect(output).toContain('bold');
|
|
162
|
+
expect(output).toContain('code');
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCREEN-006 × #2222: the markdown renderer's own SGR must SURVIVE to the frame.
|
|
3
|
+
*
|
|
4
|
+
* `renderMarkdown` sanitizes untrusted markdown BEFORE `marked-terminal` styles it, so the escape
|
|
5
|
+
* codes in its OUTPUT are this package's own — the theme's diff pairs, heading emphasis.
|
|
6
|
+
* Routing that output back through `SafeText` sanitizes it a second time and strips exactly those
|
|
7
|
+
* codes, and it does so silently: the text still renders, just colourless. Every assertion in
|
|
8
|
+
* `streaming-indicator.test.tsx` and the tool-diff suite is on TEXT, so all of them stay green
|
|
9
|
+
* while the colour is gone — which is how the regression reached a release.
|
|
10
|
+
*
|
|
11
|
+
* These are the assertions that go red for that, one per render site that renders markdown.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import React from 'react';
|
|
15
|
+
import { render } from 'ink-testing-library';
|
|
16
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
17
|
+
|
|
18
|
+
import StreamingIndicator from '../StreamingIndicator.js';
|
|
19
|
+
import ToolDiffBlock from '../ToolDiffBlock.js';
|
|
20
|
+
import { DARK_THEME } from '../theme/built-in-themes.js';
|
|
21
|
+
import { foreground } from '../theme/index.js';
|
|
22
|
+
|
|
23
|
+
import type { IDiffLine } from '@robota-sdk/agent-interface-session';
|
|
24
|
+
|
|
25
|
+
/** The opening SGR a theme colour produces, for an assertion about the rendered bytes. */
|
|
26
|
+
function openCode(color: string): string {
|
|
27
|
+
const [open] = foreground(color)('x').split('x');
|
|
28
|
+
return open ?? '';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const DIFF_LINES: IDiffLine[] = [
|
|
32
|
+
{ type: 'hunk', text: '@@ -1,2 +1,2 @@', lineNumber: 1 },
|
|
33
|
+
{ type: 'remove', text: 'const a = 1;', lineNumber: 1 },
|
|
34
|
+
{ type: 'add', text: 'const a = 2;', lineNumber: 1 },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const DIFF_MARKDOWN = [
|
|
38
|
+
'```diff',
|
|
39
|
+
'@@ -1,2 +1,2 @@',
|
|
40
|
+
'- const a = 1;',
|
|
41
|
+
'+ const a = 2;',
|
|
42
|
+
'```',
|
|
43
|
+
].join('\n');
|
|
44
|
+
|
|
45
|
+
describe('markdown styling survives the render site (SCREEN-006)', () => {
|
|
46
|
+
let originalForceColor: string | undefined;
|
|
47
|
+
let originalNoColor: string | undefined;
|
|
48
|
+
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
// `renderMarkdown` colours only for an interactive colour terminal; the test process is not one.
|
|
51
|
+
originalForceColor = process.env.FORCE_COLOR;
|
|
52
|
+
originalNoColor = process.env.NO_COLOR;
|
|
53
|
+
delete process.env.NO_COLOR;
|
|
54
|
+
process.env.FORCE_COLOR = '1';
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
if (originalForceColor === undefined) delete process.env.FORCE_COLOR;
|
|
59
|
+
else process.env.FORCE_COLOR = originalForceColor;
|
|
60
|
+
if (originalNoColor === undefined) delete process.env.NO_COLOR;
|
|
61
|
+
else process.env.NO_COLOR = originalNoColor;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('ToolDiffBlock keeps the added/removed diff colours', () => {
|
|
65
|
+
const { lastFrame } = render(<ToolDiffBlock file="src/a.ts" lines={DIFF_LINES} />);
|
|
66
|
+
const frame = lastFrame() ?? '';
|
|
67
|
+
|
|
68
|
+
expect(frame).toContain('const a = 2;');
|
|
69
|
+
// SCREEN-2002: the diff colours come from the resolved theme; the default theme's values are the
|
|
70
|
+
// ones this suite pinned before the theme existed.
|
|
71
|
+
expect(frame).toContain(openCode(DARK_THEME.markdown.diffAdded));
|
|
72
|
+
expect(frame).toContain(openCode(DARK_THEME.markdown.diffRemoved));
|
|
73
|
+
expect(frame).toContain(openCode(DARK_THEME.markdown.diffHunk)); // the @@ hunk header
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('StreamingIndicator keeps the colours of a diff block in streamed text', () => {
|
|
77
|
+
const { lastFrame } = render(<StreamingIndicator text={DIFF_MARKDOWN} activeTools={[]} />);
|
|
78
|
+
const frame = lastFrame() ?? '';
|
|
79
|
+
|
|
80
|
+
expect(frame).toContain('const a = 2;');
|
|
81
|
+
expect(frame).toContain(openCode(DARK_THEME.markdown.diffAdded));
|
|
82
|
+
expect(frame).toContain(openCode(DARK_THEME.markdown.diffRemoved));
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('StreamingIndicator still sanitizes what arrives from OUTSIDE the renderer', () => {
|
|
86
|
+
// The other direction of the same boundary: `renderMarkdown` sanitizes its INPUT, so an escape
|
|
87
|
+
// sequence in the model's text is gone before `marked-terminal` ever sees it. Switching this
|
|
88
|
+
// site to `RenderedText` must not reopen that.
|
|
89
|
+
const { lastFrame } = render(
|
|
90
|
+
<StreamingIndicator text={`before]8;;https://evil.exampleclick]8;;after`} activeTools={[]} />,
|
|
91
|
+
);
|
|
92
|
+
const frame = lastFrame() ?? '';
|
|
93
|
+
|
|
94
|
+
expect(frame).not.toContain('evil.example');
|
|
95
|
+
expect(frame).not.toContain(']8;;');
|
|
96
|
+
expect(frame).toContain('before');
|
|
97
|
+
});
|
|
98
|
+
});
|