@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
package/src/render.tsx
ADDED
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ink render entry point.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { render } from 'ink';
|
|
7
|
+
import React from 'react';
|
|
8
|
+
|
|
9
|
+
import App from './App.js';
|
|
10
|
+
import { AttentionTracker } from './attention/attention-tracker.js';
|
|
11
|
+
import { FocusReportingStdin } from './attention/focus-input-filter.js';
|
|
12
|
+
import { KeybindingsProvider } from './keybindings/keybindings-context.js';
|
|
13
|
+
import { ProductDisplayNameProvider } from './product-display-name-context.js';
|
|
14
|
+
import { writeScreenReaderAnnouncement } from './screen-reader-announcement.js';
|
|
15
|
+
import { ScreenReaderProvider } from './screen-reader-context.js';
|
|
16
|
+
import { ScreenReaderPacingProvider } from './screen-reader-pacing-context.js';
|
|
17
|
+
import { awaitStartupQuietPeriod, resolvePacing } from './screen-reader-pacing.js';
|
|
18
|
+
import type { IScreenReaderPacingOverrides } from './screen-reader-pacing.js';
|
|
19
|
+
import { createParkedStdout, toPacingPort } from './screen-reader-stdout.js';
|
|
20
|
+
import { isInteractiveColorTerminal, supportsFocusReporting } from './terminal-capabilities.js';
|
|
21
|
+
import { TerminalCapabilitiesProvider } from './terminal-capabilities-context.js';
|
|
22
|
+
import type { ITerminalCapabilityOverrides } from './terminal-capabilities-context.js';
|
|
23
|
+
import { createFocusReportingWriter } from './terminal-focus-reporting.js';
|
|
24
|
+
import { TerminalHandoffController } from './terminal-handoff-controller.js';
|
|
25
|
+
import { TuiInteractionChannel } from './TuiInteractionChannel.js';
|
|
26
|
+
import { WireTuiChannel } from './wire-tui-channel.js';
|
|
27
|
+
|
|
28
|
+
import type {
|
|
29
|
+
IAttachedSessionConnection,
|
|
30
|
+
TAttachedSessionEnd,
|
|
31
|
+
} from './attached-session-connection.js';
|
|
32
|
+
import type { IAttentionSource } from './attention/attention-tracker.js';
|
|
33
|
+
import type { IKeybindingsSource } from './keybindings/node-keybindings-source.js';
|
|
34
|
+
import type { TScreenReaderChannel } from './screen-reader-announcement.js';
|
|
35
|
+
import type { IThemeRegistry } from './theme/theme-registry.js';
|
|
36
|
+
import type { ITuiAppChannelPort } from './tui-app-channel-port.js';
|
|
37
|
+
import type { ITuiCliAdapter } from './tui-cli-adapter.js';
|
|
38
|
+
import type { ITuiInteractionChannelOptions } from './TuiInteractionChannel.js';
|
|
39
|
+
import type { ITuiClientCommands } from './wire-tui-client-commands.js';
|
|
40
|
+
import type {
|
|
41
|
+
IAIProvider,
|
|
42
|
+
TProviderCredentialResolver,
|
|
43
|
+
IToolWithEventService,
|
|
44
|
+
IProviderDefinition,
|
|
45
|
+
} from '@robota-sdk/agent-core';
|
|
46
|
+
import type { TPermissionMode } from '@robota-sdk/agent-core';
|
|
47
|
+
import type {
|
|
48
|
+
IBackgroundTaskRunner,
|
|
49
|
+
ICommandHostAdapters,
|
|
50
|
+
IOutputStylePrompt,
|
|
51
|
+
ICommandModule,
|
|
52
|
+
IRemoteCommandPolicy,
|
|
53
|
+
TSubagentRunnerFactory,
|
|
54
|
+
IAgentDefinition,
|
|
55
|
+
TShellExecFn,
|
|
56
|
+
CommandRegistry,
|
|
57
|
+
IMemoryStore,
|
|
58
|
+
IPromptHistoryOptions,
|
|
59
|
+
IAutomaticMemoryConfig,
|
|
60
|
+
IPerTurnRecallConfig,
|
|
61
|
+
TWorkspaceProjectAccess,
|
|
62
|
+
EditCheckpointStore,
|
|
63
|
+
TInteractiveSessionOptions,
|
|
64
|
+
IOrgPolicy,
|
|
65
|
+
IProviderErrorGuidance,
|
|
66
|
+
IProjectSettingsPath,
|
|
67
|
+
IContributionSource,
|
|
68
|
+
ISkillRootDescriptor,
|
|
69
|
+
INodeHostSettingsSource,
|
|
70
|
+
IToolCallHandoffPolicy,
|
|
71
|
+
ICreateSessionOptions,
|
|
72
|
+
ILivePromptTracePort,
|
|
73
|
+
} from '@robota-sdk/agent-framework';
|
|
74
|
+
import type { TReducedMotionOverride } from '@robota-sdk/agent-interface-command';
|
|
75
|
+
import type {
|
|
76
|
+
IInteractiveSessionStore,
|
|
77
|
+
IPromptHistorySource,
|
|
78
|
+
} from '@robota-sdk/agent-interface-session';
|
|
79
|
+
import type { ITransportRegistryView } from '@robota-sdk/agent-interface-transport';
|
|
80
|
+
|
|
81
|
+
export interface IRenderOptions {
|
|
82
|
+
cwd: string;
|
|
83
|
+
livePromptTrace?: ILivePromptTracePort;
|
|
84
|
+
/** Product identity for terminal labels, title, and host-facing copy. */
|
|
85
|
+
productDisplayName?: string;
|
|
86
|
+
modelCommandToolPrefix?: string;
|
|
87
|
+
subagentHookEnvironmentNames?: ICreateSessionOptions['subagentHookEnvironmentNames'];
|
|
88
|
+
observerFailureWarningCode?: ICreateSessionOptions['observerFailureWarningCode'];
|
|
89
|
+
commandHookShell?: string;
|
|
90
|
+
/** Skip instruction files and plugin discovery (`--safe-mode`). */
|
|
91
|
+
bare?: boolean;
|
|
92
|
+
/** Run no hook the settings layers declare (`--safe-mode`). */
|
|
93
|
+
skipConfiguredHooks?: boolean;
|
|
94
|
+
promptFileReferenceTag?: string;
|
|
95
|
+
provider: IAIProvider;
|
|
96
|
+
providerErrorGuidance?: IProviderErrorGuidance;
|
|
97
|
+
projectAccess?: TWorkspaceProjectAccess;
|
|
98
|
+
projectSettingsPaths?: readonly IProjectSettingsPath[];
|
|
99
|
+
baselinePermissionAllow?: readonly string[];
|
|
100
|
+
/** Host-selected task-context root; absent means no framework task-directory scan. */
|
|
101
|
+
taskContext?: { readonly enabled?: boolean; readonly dir?: string };
|
|
102
|
+
contributionSources?: readonly IContributionSource[];
|
|
103
|
+
skillRoots?: readonly ISkillRootDescriptor[];
|
|
104
|
+
userSettingsSources?: readonly INodeHostSettingsSource[];
|
|
105
|
+
environment?: Readonly<Record<string, string | undefined>>;
|
|
106
|
+
resolveProviderCredential?: TProviderCredentialResolver;
|
|
107
|
+
/**
|
|
108
|
+
* CLI-083 (issue #2287) — the org policy, forwarded to the session so `blockedCommands` is
|
|
109
|
+
* enforced on the direct CLI path as well as under `--serve`.
|
|
110
|
+
*
|
|
111
|
+
* DECLARED, not merely spread through. The shell forwards this with
|
|
112
|
+
* `...(orgPolicy === null ? {} : { orgPolicy })`, and a spread bypasses TypeScript's
|
|
113
|
+
* excess-property check — so before this field existed the value compiled, arrived, and was
|
|
114
|
+
* dropped by `toChannelOptions` below, which copies field by field. The idiom that looked safest
|
|
115
|
+
* is what disabled the one check that would have caught the missing declaration.
|
|
116
|
+
*/
|
|
117
|
+
orgPolicy?: IOrgPolicy | undefined;
|
|
118
|
+
/**
|
|
119
|
+
* Builds the authority- and permission-backed edit checkpoint store for one session; absent, no
|
|
120
|
+
* session keeps checkpoints. A factory, not a store: a session switch builds the next channel
|
|
121
|
+
* while the old session may still be finishing a turn, and a store holds one turn in progress.
|
|
122
|
+
*/
|
|
123
|
+
createEditCheckpointStore?: () => EditCheckpointStore;
|
|
124
|
+
/** The host's way to build each external-event grant's verifier; absent, no grant opens. */
|
|
125
|
+
externalEventVerifierFactory?: TInteractiveSessionOptions['externalEventVerifierFactory'];
|
|
126
|
+
/**
|
|
127
|
+
* #3189: the run's grant history, handed to every session this TUI builds so a switch replays no
|
|
128
|
+
* spent token and resets no rate. DECLARED for the reason `orgPolicy` above is.
|
|
129
|
+
*/
|
|
130
|
+
externalEventGrantHistory?: TInteractiveSessionOptions['externalEventGrantHistory'];
|
|
131
|
+
providerOverride?: string | undefined;
|
|
132
|
+
/**
|
|
133
|
+
* #1844: forwarded to the session so `/provider switch` can construct the provider it switches TO.
|
|
134
|
+
*
|
|
135
|
+
* The session cannot discover these — they are assembled at the composition root from the provider
|
|
136
|
+
* packages. Without them the hot-swap throws with an empty supported-list, which is the failure
|
|
137
|
+
* this option exists to prevent rather than a nicety.
|
|
138
|
+
*/
|
|
139
|
+
providerDefinitions?: readonly IProviderDefinition[];
|
|
140
|
+
providerType?: string | undefined;
|
|
141
|
+
modelId?: string;
|
|
142
|
+
/** CLI-1988: resolved provider-neutral response style. */
|
|
143
|
+
outputStyle?: IOutputStylePrompt;
|
|
144
|
+
/** ARCH-013: resolved preset effort, forwarded to the session's `effort` seam. */
|
|
145
|
+
effort?: ITuiInteractionChannelOptions['effort'];
|
|
146
|
+
temperature?: number;
|
|
147
|
+
maxOutputTokens?: number;
|
|
148
|
+
/** Preset prompt seed, distinct from the replacing `systemPrompt` option. */
|
|
149
|
+
presetSystemPrompt?: string;
|
|
150
|
+
/** CLI-sourced additive prompt text, composed before the TUI is rendered. */
|
|
151
|
+
appendSystemPrompt?: string;
|
|
152
|
+
responseFormat?: ITuiInteractionChannelOptions['responseFormat'];
|
|
153
|
+
language?: string;
|
|
154
|
+
permissionMode?: TPermissionMode;
|
|
155
|
+
maxTurns?: number;
|
|
156
|
+
allowedTools?: readonly string[];
|
|
157
|
+
deniedTools?: readonly string[];
|
|
158
|
+
version?: string;
|
|
159
|
+
sessionStore?: IInteractiveSessionStore;
|
|
160
|
+
disableSessionLoops?: boolean;
|
|
161
|
+
resolveDefaultLoopPrompt?: () => string;
|
|
162
|
+
resumeSessionId?: string;
|
|
163
|
+
showSessionPickerOnStart?: boolean;
|
|
164
|
+
/** FLOW-2006: text a deep link prefilled into the composer. Never submitted on its own. */
|
|
165
|
+
initialInput?: string;
|
|
166
|
+
/** Where that text came from; `external-link` renders the provenance notice. */
|
|
167
|
+
initialInputOrigin?: 'external-link';
|
|
168
|
+
forkSession?: boolean;
|
|
169
|
+
/** Issue #3081: the startup session is the target of a `/cd` from this directory. */
|
|
170
|
+
workspaceMovedFrom?: string;
|
|
171
|
+
sessionName?: string;
|
|
172
|
+
backgroundTaskRunners?: IBackgroundTaskRunner[];
|
|
173
|
+
/** MCP-004: the tool-call handoff policy the composition root computed for this runtime. */
|
|
174
|
+
toolCallHandoff?: IToolCallHandoffPolicy;
|
|
175
|
+
subagentRunnerFactory?: TSubagentRunnerFactory;
|
|
176
|
+
/**
|
|
177
|
+
* ARCH-005: subagent definitions contributed by the composition root (the capability packs
|
|
178
|
+
* `assembleProduct` merged). Forwarded to the session's `agentDefinitions` seam; absent ⇒ unchanged.
|
|
179
|
+
*/
|
|
180
|
+
agentDefinitions?: readonly IAgentDefinition[];
|
|
181
|
+
agentDefinitionRoots?: readonly string[];
|
|
182
|
+
pluginDirectories?: { readonly user?: string; readonly project?: string };
|
|
183
|
+
/**
|
|
184
|
+
* ARCH-006: tools contributed by the composition root (the capability packs `assembleProduct` merged)
|
|
185
|
+
* and, when the profile hands the packs the whole tool surface, the suppressed framework default tier
|
|
186
|
+
* (`defaultTools: []`). Forwarded to the session's tool-composition seam; absent ⇒ unchanged.
|
|
187
|
+
*/
|
|
188
|
+
additionalTools?: IToolWithEventService[];
|
|
189
|
+
defaultTools?: readonly IToolWithEventService[];
|
|
190
|
+
/** The sandbox the shell tools run under, so the session can let a confined command skip the prompt. */
|
|
191
|
+
sandboxClient?: ICreateSessionOptions['sandboxClient'];
|
|
192
|
+
toolExecutionPolicy?: ICreateSessionOptions['toolExecutionPolicy'];
|
|
193
|
+
commandModules?: readonly ICommandModule[];
|
|
194
|
+
commandHostAdapters?: ICommandHostAdapters;
|
|
195
|
+
shellExec?: TShellExecFn;
|
|
196
|
+
/** REMOTE-006: optional remote-command policy (allow-by-default; local == remote). */
|
|
197
|
+
remoteCommandPolicy?: IRemoteCommandPolicy;
|
|
198
|
+
startupUpdateNotice?: Promise<string | undefined>;
|
|
199
|
+
transportRegistry?: ITransportRegistryView;
|
|
200
|
+
bindTransports?: ITuiInteractionChannelOptions['bindTransports'];
|
|
201
|
+
cliAdapter: ITuiCliAdapter;
|
|
202
|
+
reloadPluginCommandSource?: (registry: CommandRegistry) => void;
|
|
203
|
+
agentName?: string;
|
|
204
|
+
/** Active preset id selected at startup (PRESET-011 runtime state). Defaults to 'default'. */
|
|
205
|
+
activePresetId?: string;
|
|
206
|
+
/** Preset persona block composed as a `source: 'persona'` system-prompt section (priority 5). */
|
|
207
|
+
persona?: string;
|
|
208
|
+
/** Preset execution capability: activate agent runtime + subagent/background dispatch. */
|
|
209
|
+
enableParallelSubagents?: boolean;
|
|
210
|
+
/** Preset execution capability: run a post-task self-verification step. */
|
|
211
|
+
selfVerification?: boolean;
|
|
212
|
+
/**
|
|
213
|
+
* Called with each live channel (including session-switch re-creations). Lets the embedding
|
|
214
|
+
* product wire process-level concerns (ERR-001 G1: error routing into the live session).
|
|
215
|
+
*/
|
|
216
|
+
onChannelReady?: (channel: TuiInteractionChannel) => void;
|
|
217
|
+
/**
|
|
218
|
+
* SELFHOST-008 P6: optional durable-memory store injected by the surface (agent-cli). Forwarded to the
|
|
219
|
+
* channel → `buildRuntimeSession`; absent ⇒ memory OFF (today's behavior). Enablement is surface-owned.
|
|
220
|
+
*/
|
|
221
|
+
memoryStore?: IMemoryStore;
|
|
222
|
+
/** SELFHOST-008 P6: optional automatic post-turn capture policy (absent ⇒ capture OFF). */
|
|
223
|
+
automaticMemory?: IAutomaticMemoryConfig;
|
|
224
|
+
/** SELFHOST-008 P6: optional per-turn recall policy (absent ⇒ recall OFF, startup-only injection). */
|
|
225
|
+
recallMemory?: IPerTurnRecallConfig;
|
|
226
|
+
/**
|
|
227
|
+
* CLI-2004: the resolved screen-reader mode. DECLARED, not spread through — `toChannelOptions`
|
|
228
|
+
* below copies field by field, so an undeclared field compiles, arrives and is silently dropped
|
|
229
|
+
* (the ARCH-110 containment hazard the `orgPolicy` comment above records). Absent ⇒ mode OFF and
|
|
230
|
+
* today's byte stream is unchanged.
|
|
231
|
+
*/
|
|
232
|
+
screenReader?: boolean | undefined;
|
|
233
|
+
/** Host-selected raw timing overrides; absent values use the renderer's defaults. */
|
|
234
|
+
screenReaderPacing?: IScreenReaderPacingOverrides;
|
|
235
|
+
/** Host-selected cursor and turn-mark overrides; absent values use terminal detection. */
|
|
236
|
+
terminalCapabilities?: ITerminalCapabilityOverrides;
|
|
237
|
+
/** CLI-2004: which input turned the mode on — printed in the confirmation line. */
|
|
238
|
+
screenReaderChannel?: TScreenReaderChannel | undefined;
|
|
239
|
+
/** CLI-2004: mode off, but the environment suggests a reader is running ⇒ one advisory line. */
|
|
240
|
+
screenReaderHint?: boolean | undefined;
|
|
241
|
+
/** BEHAVIOR-2003: one watched source shared with the optional `/keybindings` command. */
|
|
242
|
+
keybindingsSource?: IKeybindingsSource;
|
|
243
|
+
/**
|
|
244
|
+
* SCREEN-2002: the theme catalogue this run renders from, shared with the optional `/theme`
|
|
245
|
+
* command. DECLARED, not spread — see the `screenReader` note above. Absent ⇒ the built-ins.
|
|
246
|
+
*/
|
|
247
|
+
themeRegistry?: IThemeRegistry;
|
|
248
|
+
/** SCREEN-2002: reduced motion as the product shell resolved it (settings ← env ← flag). */
|
|
249
|
+
reducedMotion?: boolean | undefined;
|
|
250
|
+
/** SCREEN-2002: which tier decided it, when that was not the settings. */
|
|
251
|
+
reducedMotionOverride?: TReducedMotionOverride | undefined;
|
|
252
|
+
/**
|
|
253
|
+
* SCREEN-1992: the focus-reporting override the product shell resolved from its own environment.
|
|
254
|
+
* `true` forces DECSET 1004 on, `false` is the kill switch, absent ⇒ on for an interactive TTY.
|
|
255
|
+
*/
|
|
256
|
+
focusReporting?: boolean | undefined;
|
|
257
|
+
/**
|
|
258
|
+
* SCREEN-1993: prompt history, resolved by the product shell. `promptHistory` is the session-side
|
|
259
|
+
* writer (forwarded to the channel like `memoryStore`); `promptHistorySource` and
|
|
260
|
+
* `promptHistoryProject` feed the input area's Ctrl+R search. DECLARED, not spread through — the
|
|
261
|
+
* projection below copies field by field. Absent ⇒ nothing is written and `ctrl+r` is inert.
|
|
262
|
+
*/
|
|
263
|
+
promptHistory?: IPromptHistoryOptions;
|
|
264
|
+
promptHistorySource?: IPromptHistorySource;
|
|
265
|
+
promptHistoryProject?: string;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Map render options to TuiInteractionChannel constructor options. */
|
|
269
|
+
export function toChannelOptions(
|
|
270
|
+
options: IRenderOptions,
|
|
271
|
+
resumeSessionId?: string,
|
|
272
|
+
): ConstructorParameters<typeof TuiInteractionChannel>[0] {
|
|
273
|
+
return {
|
|
274
|
+
cwd: options.cwd,
|
|
275
|
+
...(options.livePromptTrace ? { livePromptTrace: options.livePromptTrace } : {}),
|
|
276
|
+
provider: options.provider,
|
|
277
|
+
...(options.providerErrorGuidance !== undefined
|
|
278
|
+
? { providerErrorGuidance: options.providerErrorGuidance }
|
|
279
|
+
: {}),
|
|
280
|
+
...(options.promptFileReferenceTag !== undefined
|
|
281
|
+
? { promptFileReferenceTag: options.promptFileReferenceTag }
|
|
282
|
+
: {}),
|
|
283
|
+
...(options.modelCommandToolPrefix !== undefined
|
|
284
|
+
? { modelCommandToolPrefix: options.modelCommandToolPrefix }
|
|
285
|
+
: {}),
|
|
286
|
+
...(options.subagentHookEnvironmentNames !== undefined
|
|
287
|
+
? { subagentHookEnvironmentNames: options.subagentHookEnvironmentNames }
|
|
288
|
+
: {}),
|
|
289
|
+
...(options.observerFailureWarningCode !== undefined
|
|
290
|
+
? { observerFailureWarningCode: options.observerFailureWarningCode }
|
|
291
|
+
: {}),
|
|
292
|
+
...(options.commandHookShell !== undefined
|
|
293
|
+
? { commandHookShell: options.commandHookShell }
|
|
294
|
+
: {}),
|
|
295
|
+
...(options.bare === true ? { bare: true } : {}),
|
|
296
|
+
...(options.skipConfiguredHooks === true ? { skipConfiguredHooks: true } : {}),
|
|
297
|
+
...(options.projectAccess !== undefined ? { projectAccess: options.projectAccess } : {}),
|
|
298
|
+
...(options.projectSettingsPaths !== undefined
|
|
299
|
+
? { projectSettingsPaths: options.projectSettingsPaths }
|
|
300
|
+
: {}),
|
|
301
|
+
...(options.baselinePermissionAllow !== undefined
|
|
302
|
+
? { baselinePermissionAllow: options.baselinePermissionAllow }
|
|
303
|
+
: {}),
|
|
304
|
+
...(options.taskContext !== undefined ? { taskContext: options.taskContext } : {}),
|
|
305
|
+
...(options.contributionSources !== undefined
|
|
306
|
+
? { contributionSources: options.contributionSources }
|
|
307
|
+
: {}),
|
|
308
|
+
...(options.skillRoots !== undefined ? { skillRoots: options.skillRoots } : {}),
|
|
309
|
+
...(options.userSettingsSources !== undefined
|
|
310
|
+
? { userSettingsSources: options.userSettingsSources }
|
|
311
|
+
: {}),
|
|
312
|
+
...(options.environment !== undefined ? { environment: options.environment } : {}),
|
|
313
|
+
...(options.resolveProviderCredential !== undefined
|
|
314
|
+
? { resolveProviderCredential: options.resolveProviderCredential }
|
|
315
|
+
: {}),
|
|
316
|
+
...(options.orgPolicy !== undefined ? { orgPolicy: options.orgPolicy } : {}),
|
|
317
|
+
...(options.externalEventVerifierFactory !== undefined
|
|
318
|
+
? { externalEventVerifierFactory: options.externalEventVerifierFactory }
|
|
319
|
+
: {}),
|
|
320
|
+
...(options.externalEventGrantHistory !== undefined
|
|
321
|
+
? { externalEventGrantHistory: options.externalEventGrantHistory }
|
|
322
|
+
: {}),
|
|
323
|
+
...(options.providerDefinitions ? { providerDefinitions: options.providerDefinitions } : {}),
|
|
324
|
+
// CLI-076: the display model id doubles as the session's model override so `--model` actually reaches
|
|
325
|
+
// the provider chat call (header/status line == the model actually called).
|
|
326
|
+
...(options.modelId !== undefined ? { model: options.modelId } : {}),
|
|
327
|
+
...(options.effort !== undefined ? { effort: options.effort } : {}),
|
|
328
|
+
...(options.temperature !== undefined ? { temperature: options.temperature } : {}),
|
|
329
|
+
...(options.maxOutputTokens !== undefined ? { maxOutputTokens: options.maxOutputTokens } : {}),
|
|
330
|
+
...(options.presetSystemPrompt !== undefined
|
|
331
|
+
? { presetSystemPrompt: options.presetSystemPrompt }
|
|
332
|
+
: {}),
|
|
333
|
+
...(options.appendSystemPrompt !== undefined
|
|
334
|
+
? { appendSystemPrompt: options.appendSystemPrompt }
|
|
335
|
+
: {}),
|
|
336
|
+
...(options.responseFormat !== undefined ? { responseFormat: options.responseFormat } : {}),
|
|
337
|
+
...(options.outputStyle !== undefined ? { outputStyle: options.outputStyle } : {}),
|
|
338
|
+
permissionMode: options.permissionMode,
|
|
339
|
+
maxTurns: options.maxTurns,
|
|
340
|
+
allowedTools: options.allowedTools,
|
|
341
|
+
deniedTools: options.deniedTools,
|
|
342
|
+
sessionStore: options.sessionStore,
|
|
343
|
+
disableSessionLoops: options.disableSessionLoops,
|
|
344
|
+
resolveDefaultLoopPrompt: options.resolveDefaultLoopPrompt,
|
|
345
|
+
resumeSessionId,
|
|
346
|
+
forkSession: options.forkSession,
|
|
347
|
+
sessionName: options.sessionName,
|
|
348
|
+
backgroundTaskRunners: options.backgroundTaskRunners,
|
|
349
|
+
...(options.toolCallHandoff !== undefined ? { toolCallHandoff: options.toolCallHandoff } : {}),
|
|
350
|
+
subagentRunnerFactory: options.subagentRunnerFactory,
|
|
351
|
+
...(options.agentDefinitions !== undefined
|
|
352
|
+
? { agentDefinitions: options.agentDefinitions }
|
|
353
|
+
: {}),
|
|
354
|
+
...(options.agentDefinitionRoots !== undefined
|
|
355
|
+
? { agentDefinitionRoots: options.agentDefinitionRoots }
|
|
356
|
+
: {}),
|
|
357
|
+
...(options.pluginDirectories !== undefined
|
|
358
|
+
? { pluginDirectories: options.pluginDirectories }
|
|
359
|
+
: {}),
|
|
360
|
+
...(options.additionalTools !== undefined ? { additionalTools: options.additionalTools } : {}),
|
|
361
|
+
...(options.defaultTools !== undefined ? { defaultTools: options.defaultTools } : {}),
|
|
362
|
+
...(options.toolExecutionPolicy !== undefined
|
|
363
|
+
? { toolExecutionPolicy: options.toolExecutionPolicy }
|
|
364
|
+
: {}),
|
|
365
|
+
...(options.sandboxClient !== undefined ? { sandboxClient: options.sandboxClient } : {}),
|
|
366
|
+
commandModules: options.commandModules,
|
|
367
|
+
commandHostAdapters: options.commandHostAdapters,
|
|
368
|
+
shellExec: options.shellExec,
|
|
369
|
+
remoteCommandPolicy: options.remoteCommandPolicy,
|
|
370
|
+
transportRegistry: options.transportRegistry,
|
|
371
|
+
bindTransports: options.bindTransports,
|
|
372
|
+
language: options.language,
|
|
373
|
+
reloadPluginCommandSource: options.reloadPluginCommandSource,
|
|
374
|
+
agentName: options.agentName,
|
|
375
|
+
activePresetId: options.activePresetId,
|
|
376
|
+
persona: options.persona,
|
|
377
|
+
enableParallelSubagents: options.enableParallelSubagents,
|
|
378
|
+
selfVerification: options.selfVerification,
|
|
379
|
+
// SELFHOST-008 P6: forward the surface-resolved memory fields (absent ⇒ OFF).
|
|
380
|
+
...(options.memoryStore ? { memoryStore: options.memoryStore } : {}),
|
|
381
|
+
...(options.automaticMemory ? { automaticMemory: options.automaticMemory } : {}),
|
|
382
|
+
...(options.recallMemory ? { recallMemory: options.recallMemory } : {}),
|
|
383
|
+
// CLI-2004: declared above AND projected here — the hand-maintained copy is the exact place an
|
|
384
|
+
// undeclared option disappears, so both halves are asserted by one test.
|
|
385
|
+
...(options.screenReader !== undefined ? { screenReader: options.screenReader } : {}),
|
|
386
|
+
// SCREEN-1993: the session-side writer (absent ⇒ nothing is recorded).
|
|
387
|
+
promptHistory: options.promptHistory,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export async function waitForRenderAndStop<TResult>(
|
|
392
|
+
waitUntilExit: () => Promise<TResult>,
|
|
393
|
+
getActiveChannel: () => Pick<ITuiAppChannelPort, 'stop'> | undefined,
|
|
394
|
+
): Promise<void> {
|
|
395
|
+
let renderFailure: Error | undefined;
|
|
396
|
+
try {
|
|
397
|
+
await waitUntilExit();
|
|
398
|
+
} catch (cause) {
|
|
399
|
+
renderFailure = cause instanceof Error ? cause : new Error(String(cause));
|
|
400
|
+
}
|
|
401
|
+
let stopFailure: Error | undefined;
|
|
402
|
+
try {
|
|
403
|
+
await getActiveChannel()?.stop();
|
|
404
|
+
} catch (cause) {
|
|
405
|
+
stopFailure = cause instanceof Error ? cause : new Error(String(cause));
|
|
406
|
+
}
|
|
407
|
+
if (renderFailure !== undefined && stopFailure !== undefined) {
|
|
408
|
+
throw new AggregateError([renderFailure, stopFailure], 'TUI render and teardown both failed.');
|
|
409
|
+
}
|
|
410
|
+
if (renderFailure !== undefined) throw renderFailure;
|
|
411
|
+
if (stopFailure !== undefined) throw stopFailure;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export async function renderApp(options: IRenderOptions): Promise<void> {
|
|
415
|
+
// ERR-001 / Library Neutrality Rule: NO process-level error policy here — process survival
|
|
416
|
+
// is the product assembly's boundary (agent-cli installs the guards via onChannelReady).
|
|
417
|
+
try {
|
|
418
|
+
await options.keybindingsSource?.start();
|
|
419
|
+
await renderStartedApp(options, (services) => ({
|
|
420
|
+
createChannel: createInProcessChannelFactory(options, services),
|
|
421
|
+
}));
|
|
422
|
+
} finally {
|
|
423
|
+
options.keybindingsSource?.dispose();
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* What the full TUI needs to attach to a session a host runs (a daemon) instead of building one.
|
|
429
|
+
* Only presentation options: everything else belongs to the host's session.
|
|
430
|
+
*/
|
|
431
|
+
export interface IRenderAttachedAppOptions extends Pick<
|
|
432
|
+
IRenderOptions,
|
|
433
|
+
| 'cwd'
|
|
434
|
+
| 'productDisplayName'
|
|
435
|
+
| 'version'
|
|
436
|
+
| 'cliAdapter'
|
|
437
|
+
| 'screenReader'
|
|
438
|
+
| 'screenReaderChannel'
|
|
439
|
+
| 'screenReaderHint'
|
|
440
|
+
| 'screenReaderPacing'
|
|
441
|
+
| 'terminalCapabilities'
|
|
442
|
+
| 'keybindingsSource'
|
|
443
|
+
| 'themeRegistry'
|
|
444
|
+
| 'reducedMotion'
|
|
445
|
+
| 'reducedMotionOverride'
|
|
446
|
+
| 'focusReporting'
|
|
447
|
+
| 'promptHistorySource'
|
|
448
|
+
| 'promptHistoryProject'
|
|
449
|
+
> {
|
|
450
|
+
/** The session protocol connection; closing it belongs to the caller. */
|
|
451
|
+
readonly connection: IAttachedSessionConnection;
|
|
452
|
+
/** Shown as the session's label until the session reports its own name. */
|
|
453
|
+
readonly sessionLabel: string;
|
|
454
|
+
/** This terminal's driver id, from the attach handshake. */
|
|
455
|
+
readonly driverId: string;
|
|
456
|
+
/** Commands this terminal runs itself (`/shell`, `/theme`, ...); the host never sees them. */
|
|
457
|
+
readonly clientCommands?: ITuiClientCommands;
|
|
458
|
+
/**
|
|
459
|
+
* How this terminal is on the session: `'drive'` (the default) sends prompts and answers questions;
|
|
460
|
+
* `'observe'` only watches, and the host refuses anything else from it.
|
|
461
|
+
*/
|
|
462
|
+
readonly mode?: 'drive' | 'observe';
|
|
463
|
+
/** Print the screen-reader line; false when this process already printed it. Default true. */
|
|
464
|
+
readonly announce?: boolean;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Render the full TUI on a session the terminal is attached to over the session protocol. Leaving
|
|
469
|
+
* only detaches: the session keeps running. Resolves `'user'` when the user leaves (`/exit`,
|
|
470
|
+
* Ctrl-C) and `'closed'` when the connection closed.
|
|
471
|
+
*/
|
|
472
|
+
export async function renderAttachedApp(
|
|
473
|
+
options: IRenderAttachedAppOptions,
|
|
474
|
+
): Promise<TAttachedSessionEnd> {
|
|
475
|
+
let end: TAttachedSessionEnd = 'user';
|
|
476
|
+
let endApp: () => void = () => undefined;
|
|
477
|
+
const ended = new Promise<void>((resolve) => {
|
|
478
|
+
endApp = resolve;
|
|
479
|
+
});
|
|
480
|
+
try {
|
|
481
|
+
await options.keybindingsSource?.start();
|
|
482
|
+
await renderStartedApp(options, (services) => {
|
|
483
|
+
let channel: WireTuiChannel | undefined;
|
|
484
|
+
return {
|
|
485
|
+
// One channel for the App's life: a session switch is the host's, and the channel follows it.
|
|
486
|
+
createChannel: () =>
|
|
487
|
+
(channel ??= new WireTuiChannel({
|
|
488
|
+
connection: options.connection,
|
|
489
|
+
driverId: options.driverId,
|
|
490
|
+
sessionName: options.sessionLabel,
|
|
491
|
+
role: options.mode ?? 'drive',
|
|
492
|
+
terminalHandoff: services.terminalHandoff,
|
|
493
|
+
attention: services.attention,
|
|
494
|
+
...(options.clientCommands !== undefined
|
|
495
|
+
? { clientCommands: options.clientCommands, cwd: options.cwd }
|
|
496
|
+
: { clientCommands: undefined }),
|
|
497
|
+
onEnd: (reason) => {
|
|
498
|
+
end = reason;
|
|
499
|
+
endApp();
|
|
500
|
+
},
|
|
501
|
+
})),
|
|
502
|
+
requestSessionSwitch: async (sessionId) => channel?.requestSessionSwitch(sessionId),
|
|
503
|
+
ended,
|
|
504
|
+
};
|
|
505
|
+
});
|
|
506
|
+
} finally {
|
|
507
|
+
options.keybindingsSource?.dispose();
|
|
508
|
+
}
|
|
509
|
+
return end;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/** Presentation options the App shell reads, whichever kind of channel it renders. */
|
|
513
|
+
type TAppShellOptions = Pick<
|
|
514
|
+
IRenderOptions,
|
|
515
|
+
| 'cwd'
|
|
516
|
+
| 'productDisplayName'
|
|
517
|
+
| 'modelCommandToolPrefix'
|
|
518
|
+
| 'providerOverride'
|
|
519
|
+
| 'providerType'
|
|
520
|
+
| 'modelId'
|
|
521
|
+
| 'permissionMode'
|
|
522
|
+
| 'version'
|
|
523
|
+
| 'sessionStore'
|
|
524
|
+
| 'resumeSessionId'
|
|
525
|
+
| 'showSessionPickerOnStart'
|
|
526
|
+
| 'initialInput'
|
|
527
|
+
| 'initialInputOrigin'
|
|
528
|
+
| 'startupUpdateNotice'
|
|
529
|
+
| 'transportRegistry'
|
|
530
|
+
| 'commandHostAdapters'
|
|
531
|
+
| 'cliAdapter'
|
|
532
|
+
| 'promptHistorySource'
|
|
533
|
+
| 'promptHistoryProject'
|
|
534
|
+
| 'themeRegistry'
|
|
535
|
+
| 'reducedMotion'
|
|
536
|
+
| 'reducedMotionOverride'
|
|
537
|
+
| 'screenReader'
|
|
538
|
+
| 'screenReaderPacing'
|
|
539
|
+
| 'terminalCapabilities'
|
|
540
|
+
| 'screenReaderChannel'
|
|
541
|
+
| 'screenReaderHint'
|
|
542
|
+
| 'keybindingsSource'
|
|
543
|
+
| 'focusReporting'
|
|
544
|
+
> & {
|
|
545
|
+
/** Print the screen-reader line. Default true; false when this process already printed it. */
|
|
546
|
+
readonly announce?: boolean;
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
/** Terminal-wide services a channel is built with; they outlive every channel. */
|
|
550
|
+
export interface IChannelServices {
|
|
551
|
+
readonly terminalHandoff: TerminalHandoffController;
|
|
552
|
+
readonly attention: IAttentionSource;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** Where the App's channels come from, and how a session switch and an app end are decided. */
|
|
556
|
+
interface IAppChannelComposition {
|
|
557
|
+
readonly createChannel: (resumeSessionId?: string) => ITuiAppChannelPort;
|
|
558
|
+
/** Present when the host switches sessions and the channel follows it. */
|
|
559
|
+
readonly requestSessionSwitch?: (sessionId: string) => Promise<void>;
|
|
560
|
+
/** Settles when the app ends without the user closing it (the connection closed). */
|
|
561
|
+
readonly ended?: Promise<void>;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* The in-process factory: every channel builds its own session, and its own checkpoint store, from
|
|
566
|
+
* the render options. A resumed session's store restores its active branch from the persisted pointer.
|
|
567
|
+
*/
|
|
568
|
+
export function createInProcessChannelFactory(
|
|
569
|
+
options: IRenderOptions,
|
|
570
|
+
services: IChannelServices,
|
|
571
|
+
): (resumeSessionId?: string) => ITuiAppChannelPort {
|
|
572
|
+
// Issue #3081: the move is announced by the FIRST session only — a later switch to another
|
|
573
|
+
// session (a `/fork` attach) did not move anywhere.
|
|
574
|
+
let pendingWorkspaceMovedFrom = options.workspaceMovedFrom;
|
|
575
|
+
return (resumeSessionId) => {
|
|
576
|
+
const workspaceMovedFrom = pendingWorkspaceMovedFrom;
|
|
577
|
+
pendingWorkspaceMovedFrom = undefined;
|
|
578
|
+
const channel = new TuiInteractionChannel({
|
|
579
|
+
...toChannelOptions(options, resumeSessionId),
|
|
580
|
+
...(options.createEditCheckpointStore !== undefined
|
|
581
|
+
? { editCheckpointStore: options.createEditCheckpointStore() }
|
|
582
|
+
: {}),
|
|
583
|
+
...(workspaceMovedFrom !== undefined ? { workspaceMovedFrom } : {}),
|
|
584
|
+
terminalHandoff: services.terminalHandoff,
|
|
585
|
+
attention: services.attention,
|
|
586
|
+
});
|
|
587
|
+
// Expose each live channel (incl. session-switch re-creations) to the embedding product,
|
|
588
|
+
// e.g. for process-level error routing (ERR-001 G1).
|
|
589
|
+
options.onChannelReady?.(channel);
|
|
590
|
+
return channel;
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
async function renderStartedApp(
|
|
595
|
+
options: TAppShellOptions,
|
|
596
|
+
compose: (services: IChannelServices) => IAppChannelComposition,
|
|
597
|
+
): Promise<void> {
|
|
598
|
+
// CLI-2004: one resolved boolean drives Ink's own screen-reader support AND the React context
|
|
599
|
+
// every component reads. Both are set here so they can never disagree.
|
|
600
|
+
const screenReader = options.screenReader === true;
|
|
601
|
+
if (options.announce !== false) {
|
|
602
|
+
writeScreenReaderAnnouncement({
|
|
603
|
+
enabled: screenReader,
|
|
604
|
+
channel: options.screenReaderChannel,
|
|
605
|
+
hint: options.screenReaderHint,
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
const pacing = resolvePacing({ enabled: screenReader, overrides: options.screenReaderPacing });
|
|
609
|
+
// SCREEN-2670: the pre-write park. Constructed only when the mode is on AND the interval is
|
|
610
|
+
// non-zero, so with the mode off `stdout` is not passed at all and Ink defaults to
|
|
611
|
+
// `process.stdout` — the object it keys its instance map by — exactly as today.
|
|
612
|
+
const parked =
|
|
613
|
+
screenReader && pacing.preparkMs > 0
|
|
614
|
+
? createParkedStdout({ stdout: process.stdout, preparkMs: pacing.preparkMs })
|
|
615
|
+
: undefined;
|
|
616
|
+
|
|
617
|
+
// SCREEN-006: chalk (ink's styling engine) does not implement the NO_COLOR convention itself
|
|
618
|
+
// (verified: chalk 5's vendored supports-color reads only FORCE_COLOR/TTY/TERM), so on a real
|
|
619
|
+
// TTY `NO_COLOR=1` would still color every component. Sync chalk once with the package's single
|
|
620
|
+
// color gate (`terminal-capabilities.ts` — the SSOT that DOES honor NO_COLOR) so gate-off means
|
|
621
|
+
// zero SGR color output. Gate-on changes nothing: chalk's own level detection stays authoritative.
|
|
622
|
+
if (!isInteractiveColorTerminal()) {
|
|
623
|
+
chalk.level = 0;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// TERM-002: one terminal-handoff controller per process (one Ink instance / App). Shared across
|
|
627
|
+
// channel re-creations (session switch) so the handoff capability survives a session swap.
|
|
628
|
+
const handoffController = new TerminalHandoffController();
|
|
629
|
+
|
|
630
|
+
// SCREEN-1992: focus reporting is negotiated once for the process; the tracker outlives channels.
|
|
631
|
+
// Screen-reader mode keeps the recap (a plain notice line) — only the row countdown tick is off.
|
|
632
|
+
const focusReportingSupported = supportsFocusReporting({ override: options.focusReporting });
|
|
633
|
+
const focusReporting = createFocusReportingWriter({ supported: () => focusReportingSupported });
|
|
634
|
+
const attention = new AttentionTracker({
|
|
635
|
+
focusReporting: focusReportingSupported,
|
|
636
|
+
now: Date.now,
|
|
637
|
+
});
|
|
638
|
+
handoffController.setTerminalModeHooks({
|
|
639
|
+
preSuspend: async () => {
|
|
640
|
+
focusReporting.disable();
|
|
641
|
+
// SCREEN-2670: nothing parked may land on top of the child's output.
|
|
642
|
+
await parked?.drain();
|
|
643
|
+
},
|
|
644
|
+
postResume: () => focusReporting.enable(),
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
// Concrete framework creation has one composition boundary. React receives only the bounded port;
|
|
648
|
+
// App owns which narrowed channel is active, while each channel owns its own lifecycle.
|
|
649
|
+
const composition = compose({ terminalHandoff: handoffController, attention });
|
|
650
|
+
let activeChannel: ITuiAppChannelPort | undefined;
|
|
651
|
+
const createChannel = (resumeSessionId?: string): ITuiAppChannelPort => {
|
|
652
|
+
activeChannel = composition.createChannel(resumeSessionId);
|
|
653
|
+
return activeChannel;
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
// The startup quiet period sits between the confirmation line and the first frame, so a reader
|
|
657
|
+
// finishes announcing the mode before the prompt lands. A keypress ends it early; `0` skips it.
|
|
658
|
+
await awaitStartupQuietPeriod(pacing.startupQuietMs, process.stdin);
|
|
659
|
+
|
|
660
|
+
// The filtering proxy is attached only now: the quiet period consumed its own keystroke above, and
|
|
661
|
+
// from here on every byte reaches Ink through the proxy, minus the focus sequences.
|
|
662
|
+
const stdin = new FocusReportingStdin(process.stdin, {
|
|
663
|
+
negotiated: () => focusReporting.negotiated,
|
|
664
|
+
onFocusIn: () => attention.focusIn(),
|
|
665
|
+
onFocusOut: () => attention.focusOut(),
|
|
666
|
+
onKeystroke: () => attention.keystroke(),
|
|
667
|
+
});
|
|
668
|
+
process.stdin.resume();
|
|
669
|
+
// Requested only now, so a focus event the terminal answers with lands in the proxy, never in
|
|
670
|
+
// the quiet period's keypress wait; the tracker's idle source runs until the first one arrives.
|
|
671
|
+
focusReporting.enable();
|
|
672
|
+
attention.start();
|
|
673
|
+
|
|
674
|
+
const pacingPort = parked === undefined ? undefined : toPacingPort(parked);
|
|
675
|
+
const tree = (
|
|
676
|
+
<ProductDisplayNameProvider
|
|
677
|
+
name={options.productDisplayName}
|
|
678
|
+
modelCommandToolPrefix={options.modelCommandToolPrefix}
|
|
679
|
+
>
|
|
680
|
+
<KeybindingsProvider source={options.keybindingsSource}>
|
|
681
|
+
<ScreenReaderProvider enabled={screenReader}>
|
|
682
|
+
<TerminalCapabilitiesProvider overrides={options.terminalCapabilities}>
|
|
683
|
+
<App
|
|
684
|
+
cwd={options.cwd}
|
|
685
|
+
createChannel={createChannel}
|
|
686
|
+
{...(composition.requestSessionSwitch !== undefined
|
|
687
|
+
? { requestSessionSwitch: composition.requestSessionSwitch }
|
|
688
|
+
: {})}
|
|
689
|
+
providerOverride={options.providerOverride}
|
|
690
|
+
providerType={options.providerType}
|
|
691
|
+
modelId={options.modelId}
|
|
692
|
+
permissionMode={options.permissionMode}
|
|
693
|
+
version={options.version}
|
|
694
|
+
sessionStore={options.sessionStore}
|
|
695
|
+
resumeSessionId={options.resumeSessionId}
|
|
696
|
+
showSessionPickerOnStart={options.showSessionPickerOnStart}
|
|
697
|
+
initialInput={options.initialInput}
|
|
698
|
+
initialInputOrigin={options.initialInputOrigin}
|
|
699
|
+
startupUpdateNotice={options.startupUpdateNotice}
|
|
700
|
+
transportRegistry={options.transportRegistry}
|
|
701
|
+
pluginAdapter={options.commandHostAdapters?.plugin}
|
|
702
|
+
cliAdapter={options.cliAdapter}
|
|
703
|
+
promptHistorySource={options.promptHistorySource}
|
|
704
|
+
promptHistoryProject={options.promptHistoryProject}
|
|
705
|
+
themeRegistry={options.themeRegistry}
|
|
706
|
+
reducedMotion={options.reducedMotion}
|
|
707
|
+
reducedMotionOverride={options.reducedMotionOverride}
|
|
708
|
+
/>
|
|
709
|
+
</TerminalCapabilitiesProvider>
|
|
710
|
+
</ScreenReaderProvider>
|
|
711
|
+
</KeybindingsProvider>
|
|
712
|
+
</ProductDisplayNameProvider>
|
|
713
|
+
);
|
|
714
|
+
const instance = render(
|
|
715
|
+
pacingPort === undefined ? (
|
|
716
|
+
tree
|
|
717
|
+
) : (
|
|
718
|
+
<ScreenReaderPacingProvider port={pacingPort}>{tree}</ScreenReaderPacingProvider>
|
|
719
|
+
),
|
|
720
|
+
{
|
|
721
|
+
exitOnCtrlC: false,
|
|
722
|
+
isScreenReaderEnabled: screenReader,
|
|
723
|
+
stdin: stdin.asInkStdin(),
|
|
724
|
+
...(parked === undefined ? {} : { stdout: parked.asInkStdout() }),
|
|
725
|
+
},
|
|
726
|
+
);
|
|
727
|
+
// The controller needs the Ink instance to clear the frame before a handoff.
|
|
728
|
+
handoffController.setInkInstance(instance);
|
|
729
|
+
// Unmounting resolves `waitUntilExit`, so an app the host ended leaves the way a user exit does.
|
|
730
|
+
void composition.ended?.then(() => instance.unmount());
|
|
731
|
+
try {
|
|
732
|
+
await waitForRenderAndStop(
|
|
733
|
+
() => instance.waitUntilExit(),
|
|
734
|
+
() => activeChannel,
|
|
735
|
+
);
|
|
736
|
+
} finally {
|
|
737
|
+
// The last frame of a session must not be the one the park loses: drain before restoring.
|
|
738
|
+
await parked?.flush();
|
|
739
|
+
// Leave the terminal as it was found: no focus sequences after exit, no reader on stdin.
|
|
740
|
+
handoffController.setTerminalModeHooks(undefined);
|
|
741
|
+
focusReporting.disable();
|
|
742
|
+
attention.dispose();
|
|
743
|
+
stdin.detach();
|
|
744
|
+
process.stdin.pause();
|
|
745
|
+
}
|
|
746
|
+
}
|