@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,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCREEN-2002 — one theme, every encoding.
|
|
3
|
+
*
|
|
4
|
+
* Ink reads `colors` directly. `marked-terminal` wants chalk STYLE FUNCTIONS and `cli-highlight` wants
|
|
5
|
+
* a token→function map, so both are built here from the same values. Ink's own `colorize` is this
|
|
6
|
+
* mapping (`chalk[name]` / `chalk.hex` / `chalk.ansi256` / `chalk.rgb`), which is why deriving them is
|
|
7
|
+
* not the "name→SGR layer" SCREEN-006 refused to invent — chalk is already the layer, and a direct
|
|
8
|
+
* dependency.
|
|
9
|
+
*
|
|
10
|
+
* The STRUCTURE of a markdown or syntax token is fixed here, not themed: a heading is bold, a first
|
|
11
|
+
* heading underlined and bold, a blockquote italic, a deletion dim and struck through, an href
|
|
12
|
+
* underlined, and a syntax `type` dim — exactly what the two dependencies do today. A theme changes
|
|
13
|
+
* the colour and nothing else, which is what keeps the `dark` theme's rendering the one users know.
|
|
14
|
+
*/
|
|
15
|
+
import chalk, { Chalk, backgroundColorNames, foregroundColorNames } from 'chalk';
|
|
16
|
+
|
|
17
|
+
import { THEME_SYNTAX_KEYS } from './theme-contracts.js';
|
|
18
|
+
|
|
19
|
+
import type { IThemeMarkdown, IThemeSyntax, ITuiTheme, TThemeColor } from './theme-contracts.js';
|
|
20
|
+
import type { ChalkInstance } from 'chalk';
|
|
21
|
+
|
|
22
|
+
const HEX = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/iu;
|
|
23
|
+
const ANSI256 = /^ansi256\((\d{1,3})\)$/iu;
|
|
24
|
+
const RGB = /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/iu;
|
|
25
|
+
const ANSI256_MAX = 255;
|
|
26
|
+
|
|
27
|
+
/** The grammar, written once: the validator, the refusal below and the file parser all quote it. */
|
|
28
|
+
export const THEME_COLOR_GRAMMAR = '<chalk name> | #rgb | #rrggbb | ansi256(n) | rgb(r,g,b)';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* chalk's own name tables, which it exports; its styles are prototype getters, so a table has to be
|
|
32
|
+
* built from the names rather than from `Object.keys(chalk)`. This is the same set Ink's `colorize`
|
|
33
|
+
* accepts, which is why the grammar needs no list of its own.
|
|
34
|
+
*
|
|
35
|
+
* A `Map`, not an object literal: an object built by `Object.fromEntries` inherits `Object.prototype`,
|
|
36
|
+
* so a lookup of `constructor`, `toString`, `valueOf` or `__proto__` answers TRUTHY and the
|
|
37
|
+
* `undefined` guard below never fires — a value that is not a colour name would be handed back as
|
|
38
|
+
* though it were a style. That value becomes reachable the moment a parsed user theme (work unit 3)
|
|
39
|
+
* reaches this builder.
|
|
40
|
+
*/
|
|
41
|
+
const CHALK_STYLES = new Map<string, ChalkInstance>(
|
|
42
|
+
[...foregroundColorNames, ...backgroundColorNames].map((name) => [name, chalk[name]]),
|
|
43
|
+
);
|
|
44
|
+
const FOREGROUND_NAMES = new Set<string>(foregroundColorNames);
|
|
45
|
+
|
|
46
|
+
/** A FOREGROUND chalk name Ink would accept — a `bg…` name is not a token value. */
|
|
47
|
+
function isChalkColorName(value: string): boolean {
|
|
48
|
+
return FOREGROUND_NAMES.has(value);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Whether a string is a colour in Ink's grammar. The one place the grammar is decided. */
|
|
52
|
+
export function isThemeColor(value: string): boolean {
|
|
53
|
+
if (HEX.test(value)) return true;
|
|
54
|
+
const ansi = ANSI256.exec(value);
|
|
55
|
+
if (ansi) return Number(ansi[1]) <= ANSI256_MAX;
|
|
56
|
+
const rgb = RGB.exec(value);
|
|
57
|
+
if (rgb) return rgb.slice(1).every((part) => Number(part) <= ANSI256_MAX);
|
|
58
|
+
return isChalkColorName(value);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A value that reached the builder without being a colour is a DEFECT IN THIS PACKAGE, not in a
|
|
63
|
+
* user's file: `parseThemeDocument` refuses a theme file whole, and every built-in value is checked
|
|
64
|
+
* by TC-01. Before work unit 3 this case had two different silent answers — `foreground` handed
|
|
65
|
+
* back its base, `background`'s helper handed back `chalk.reset` — so the same bad value rendered
|
|
66
|
+
* as unstyled text in one place and as a reset in another, and neither said anything. Throwing is
|
|
67
|
+
* the one behaviour that cannot render a theme nobody can see is wrong.
|
|
68
|
+
*/
|
|
69
|
+
function refuseThemeColor(value: string): never {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Theme colour ${JSON.stringify(value)} is not in Ink's colour grammar (${THEME_COLOR_GRAMMAR}).`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A foreground style for one theme value, optionally continuing a chain. The base matters: chalk
|
|
77
|
+
* emits its SGR in chain order, so `chalk.dim` + a colour is not the same bytes as a colour + `.dim`,
|
|
78
|
+
* and reproducing a dependency's own default means reproducing its order.
|
|
79
|
+
*/
|
|
80
|
+
export function foreground(color: TThemeColor, base: ChalkInstance = chalk): ChalkInstance {
|
|
81
|
+
if (HEX.test(color)) return base.hex(color);
|
|
82
|
+
const ansi = ANSI256.exec(color);
|
|
83
|
+
if (ansi) return base.ansi256(Number(ansi[1]));
|
|
84
|
+
const rgb = RGB.exec(color);
|
|
85
|
+
if (rgb) return base.rgb(Number(rgb[1]), Number(rgb[2]), Number(rgb[3]));
|
|
86
|
+
// Gated on the FOREGROUND names, not on the shared table: the table also holds the `bg…` entries
|
|
87
|
+
// that `background()` needs, so asking it alone would answer `foreground('bgRed')` with a
|
|
88
|
+
// background style — a name `isThemeColor` already refuses as a token value.
|
|
89
|
+
const named = isChalkColorName(color) ? CHALK_STYLES.get(color) : undefined;
|
|
90
|
+
if (named === undefined) refuseThemeColor(color);
|
|
91
|
+
// A named colour continues the chain through chalk's own getter on the base instance.
|
|
92
|
+
const chained = Reflect.get(base, color) as ChalkInstance | undefined;
|
|
93
|
+
return chained ?? named;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** A background style for one theme value. */
|
|
97
|
+
export function background(color: TThemeColor, base: ChalkInstance = chalk): ChalkInstance {
|
|
98
|
+
if (HEX.test(color)) return base.bgHex(color);
|
|
99
|
+
const ansi = ANSI256.exec(color);
|
|
100
|
+
if (ansi) return base.bgAnsi256(Number(ansi[1]));
|
|
101
|
+
const rgb = RGB.exec(color);
|
|
102
|
+
if (rgb) return base.bgRgb(Number(rgb[1]), Number(rgb[2]), Number(rgb[3]));
|
|
103
|
+
const capitalised = `bg${color.charAt(0).toUpperCase()}${color.slice(1)}`;
|
|
104
|
+
const chained = Reflect.get(base, capitalised) as ChalkInstance | undefined;
|
|
105
|
+
return chained ?? refuseThemeColor(color);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The diff rows are written by this package, not by a dependency, and their caller decides colour
|
|
110
|
+
* with its own `color` flag — so a caller that asked for colour gets it even where chalk's own
|
|
111
|
+
* detection says level 0. That is the behaviour the renderer had when it wrote the escapes by hand,
|
|
112
|
+
* and the reason `renderMarkdown(md, { color: true })` still emits them off a TTY.
|
|
113
|
+
*
|
|
114
|
+
* What is forced is colour, NOT depth. Overriding a DETECTED level would downsample the rest of the
|
|
115
|
+
* frame while these rows alone emit truecolor — visible on a 256-colour terminal with a hex-valued
|
|
116
|
+
* theme, where `#56b4e9` is `ESC[38;5;117m` at level 2 and `ESC[38;2;86;180;233m` at level 3. So the
|
|
117
|
+
* ambient level is kept whenever there is one, and only the OFF case is overridden.
|
|
118
|
+
*/
|
|
119
|
+
const TRUECOLOR = 3;
|
|
120
|
+
const FORCED_CHALK = new Chalk({ level: TRUECOLOR });
|
|
121
|
+
|
|
122
|
+
function diffRowBase(): ChalkInstance {
|
|
123
|
+
return chalk.level === 0 ? FORCED_CHALK : chalk;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The `marked-terminal` constructor options: theme colour + the structure that is not themed. */
|
|
127
|
+
export function markdownRendererOptions(markdown: IThemeMarkdown): Record<string, ChalkInstance> {
|
|
128
|
+
return {
|
|
129
|
+
// Each chain reproduces marked-terminal's own default order, so the `dark` theme is byte-identical.
|
|
130
|
+
heading: foreground(markdown.heading).bold,
|
|
131
|
+
firstHeading: foreground(markdown.firstHeading).underline.bold,
|
|
132
|
+
code: foreground(markdown.code),
|
|
133
|
+
codespan: foreground(markdown.codespan),
|
|
134
|
+
link: foreground(markdown.link),
|
|
135
|
+
href: foreground(markdown.href).underline,
|
|
136
|
+
blockquote: foreground(markdown.blockquote).italic,
|
|
137
|
+
del: foreground(markdown.del, chalk.dim).strikethrough,
|
|
138
|
+
html: foreground(markdown.html),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** The `cli-highlight` theme: every one of its seventeen coloured keys, so none falls back. */
|
|
143
|
+
export function syntaxHighlightTheme(syntax: IThemeSyntax): Record<string, ChalkInstance> {
|
|
144
|
+
const theme: Record<string, ChalkInstance> = {};
|
|
145
|
+
for (const key of THEME_SYNTAX_KEYS) {
|
|
146
|
+
// `type` is `<colour>.dim` in cli-highlight's own theme: the modifier is structure, the colour is
|
|
147
|
+
// the theme's, and the order is the dependency's, so the `dark` theme is byte-identical.
|
|
148
|
+
theme[key] = key === 'type' ? foreground(syntax[key]).dim : foreground(syntax[key]);
|
|
149
|
+
}
|
|
150
|
+
return theme;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The styles the diff renderer writes around a row: background and foreground composed per row. */
|
|
154
|
+
export interface IDiffRowStyles {
|
|
155
|
+
readonly added: (row: string) => string;
|
|
156
|
+
readonly removed: (row: string) => string;
|
|
157
|
+
readonly hunk: (row: string) => string;
|
|
158
|
+
/** Structure only: dim over the inherited foreground, never a theme colour. */
|
|
159
|
+
readonly meta: (row: string) => string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function diffRowStyles(theme: ITuiTheme): IDiffRowStyles {
|
|
163
|
+
const { markdown } = theme;
|
|
164
|
+
const base = diffRowBase();
|
|
165
|
+
const addedBackground = background(markdown.diffAddedBackground, base);
|
|
166
|
+
const addedForeground = foreground(markdown.diffAdded, base);
|
|
167
|
+
const removedBackground = background(markdown.diffRemovedBackground, base);
|
|
168
|
+
const removedForeground = foreground(markdown.diffRemoved, base);
|
|
169
|
+
return {
|
|
170
|
+
added: (row) => addedBackground(addedForeground(row)),
|
|
171
|
+
removed: (row) => removedBackground(removedForeground(row)),
|
|
172
|
+
hunk: foreground(markdown.diffHunk, base),
|
|
173
|
+
// Dim over the INHERITED foreground, as this renderer always wrote it — colouring it would be a
|
|
174
|
+
// rendering change dressed as a theme.
|
|
175
|
+
meta: (row) => base.dim(row),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persisted tool-summary → status-SSOT mapping (SCREEN-006).
|
|
3
|
+
*
|
|
4
|
+
* MessageList used to hand-roll its own glyph/color mapping for tool summaries, which drifted
|
|
5
|
+
* from `status-glyph.ts` (denied rendered plain yellow vs the SSOT's yellowBright). These
|
|
6
|
+
* helpers route the persisted summary items through `toolStateStatusKind` + `STATUS_SYMBOL`
|
|
7
|
+
* so tool status renders one way everywhere.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { humanizeToolArgument, humanizeToolName } from './humanize-tool-name.js';
|
|
11
|
+
import { STATUS_SYMBOL, toolStateStatusKind } from './status-glyph.js';
|
|
12
|
+
|
|
13
|
+
import type { TUiStatusKind } from './status-glyph.js';
|
|
14
|
+
import type { IToolCallSummary } from './utils/tool-call-extractor.js';
|
|
15
|
+
import type { IToolState } from '@robota-sdk/agent-interface-session';
|
|
16
|
+
|
|
17
|
+
/** A tool item as persisted in a `tool-summary` history entry (loosely-typed session data). */
|
|
18
|
+
export type TToolSummaryItem = {
|
|
19
|
+
toolName: string;
|
|
20
|
+
firstArg?: string;
|
|
21
|
+
isRunning?: boolean;
|
|
22
|
+
result?: string;
|
|
23
|
+
diffLines?: IToolCallSummary['diffLines'];
|
|
24
|
+
diffFile?: string;
|
|
25
|
+
toolResultData?: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Map a persisted tool-summary item to the semantic status kind — via the `status-glyph.ts`
|
|
30
|
+
* SSOT. A command-output error (non-zero exit surfaced by `formatCommandOutputSummary`)
|
|
31
|
+
* overrides the recorded result; the caller passes that flag so this module stays free of
|
|
32
|
+
* the command-output dependency.
|
|
33
|
+
*/
|
|
34
|
+
export function toolSummaryStatusKind(
|
|
35
|
+
tool: TToolSummaryItem,
|
|
36
|
+
hasCommandOutputError: boolean,
|
|
37
|
+
): TUiStatusKind {
|
|
38
|
+
if (hasCommandOutputError) return 'error';
|
|
39
|
+
const result: IToolState['result'] =
|
|
40
|
+
tool.result === 'success' || tool.result === 'error' || tool.result === 'denied'
|
|
41
|
+
? tool.result
|
|
42
|
+
: undefined;
|
|
43
|
+
return toolStateStatusKind({
|
|
44
|
+
toolName: tool.toolName,
|
|
45
|
+
firstArg: tool.firstArg ?? '',
|
|
46
|
+
isRunning: tool.isRunning ?? false,
|
|
47
|
+
result,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** One-line summary label: SSOT glyph + humanized tool name + first argument. */
|
|
52
|
+
export function getToolSummaryLabel(
|
|
53
|
+
tool: TToolSummaryItem,
|
|
54
|
+
kind: TUiStatusKind,
|
|
55
|
+
prefix?: string,
|
|
56
|
+
): string {
|
|
57
|
+
const argument = humanizeToolArgument(tool.firstArg);
|
|
58
|
+
return `${STATUS_SYMBOL[kind]} ${humanizeToolName(tool.toolName, prefix)}${argument ? `(${argument})` : ''}`;
|
|
59
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { TerminalHandoffController } from './terminal-handoff-controller.js';
|
|
2
|
+
import type { ITuiSessionEventNotice } from './tui-session-events.js';
|
|
3
|
+
import type { IPendingPermissionRequest } from './types.js';
|
|
4
|
+
import type {
|
|
5
|
+
IActionRequest,
|
|
6
|
+
IHistoryEntry,
|
|
7
|
+
TActionResponse,
|
|
8
|
+
TModelEffortSelection,
|
|
9
|
+
TSessionEndReason,
|
|
10
|
+
TPermissionMode,
|
|
11
|
+
} from '@robota-sdk/agent-core';
|
|
12
|
+
import type { ICommand } from '@robota-sdk/agent-interface-command';
|
|
13
|
+
import type {
|
|
14
|
+
IExecutionDetailPage,
|
|
15
|
+
IExecutionWorkspaceSnapshot,
|
|
16
|
+
} from '@robota-sdk/agent-interface-execution';
|
|
17
|
+
import type { IInteractiveSessionEvents, IToolState } from '@robota-sdk/agent-interface-session';
|
|
18
|
+
|
|
19
|
+
export interface ITuiCommandQueryPort {
|
|
20
|
+
getCommands(filter?: string): ICommand[];
|
|
21
|
+
getSubcommands(commandName: string): ICommand[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type TTuiSessionUiEventName = 'ui_intent' | 'session_renamed';
|
|
25
|
+
|
|
26
|
+
export interface ITuiSessionUiEventPort {
|
|
27
|
+
on<E extends TTuiSessionUiEventName>(event: E, handler: IInteractiveSessionEvents[E]): void;
|
|
28
|
+
off<E extends TTuiSessionUiEventName>(event: E, handler: IInteractiveSessionEvents[E]): void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ITuiRuntimeStatusSnapshot {
|
|
32
|
+
permissionMode: TPermissionMode;
|
|
33
|
+
sessionId: string;
|
|
34
|
+
activePresetId?: string;
|
|
35
|
+
effort?: TModelEffortSelection;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ITuiChannelSnapshot {
|
|
39
|
+
history: readonly IHistoryEntry[];
|
|
40
|
+
streamingText: string;
|
|
41
|
+
activeTools: readonly IToolState[];
|
|
42
|
+
isThinking: boolean;
|
|
43
|
+
isAborting: boolean;
|
|
44
|
+
lastErrorMessage: string | null;
|
|
45
|
+
isStalled: boolean;
|
|
46
|
+
sessionEventNotices: readonly ITuiSessionEventNotice[];
|
|
47
|
+
isShuttingDown: boolean;
|
|
48
|
+
pendingPrompt: string | null;
|
|
49
|
+
pendingCount: number;
|
|
50
|
+
executionWorkspaceSnapshot: IExecutionWorkspaceSnapshot | null;
|
|
51
|
+
selectedExecutionEntryId?: string;
|
|
52
|
+
permissionRequest: IPendingPermissionRequest | null;
|
|
53
|
+
pendingUserAction: IActionRequest | null;
|
|
54
|
+
contextState: { percentage: number; usedTokens: number; maxTokens: number };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Complete channel surface visible below the top-level App composition shell.
|
|
59
|
+
*
|
|
60
|
+
* Deliberately excludes the concrete-session, concrete-registry and mutable-state-manager
|
|
61
|
+
* compatibility escapes retained by TuiInteractionChannel for non-React consumers.
|
|
62
|
+
*/
|
|
63
|
+
export interface ITuiAppChannelPort {
|
|
64
|
+
readonly terminalHandoffController: TerminalHandoffController | undefined;
|
|
65
|
+
readonly sessionName: string | undefined;
|
|
66
|
+
start(): Promise<void>;
|
|
67
|
+
stop(): Promise<void>;
|
|
68
|
+
subscribe(onChange: () => void): () => void;
|
|
69
|
+
getSnapshot(): ITuiChannelSnapshot;
|
|
70
|
+
getCommandQueryPort(): ITuiCommandQueryPort;
|
|
71
|
+
getSessionUiEventPort(): ITuiSessionUiEventPort;
|
|
72
|
+
getRuntimeStatusSnapshot(fallbackPermissionMode: TPermissionMode): ITuiRuntimeStatusSnapshot;
|
|
73
|
+
addEntry(entry: IHistoryEntry): void;
|
|
74
|
+
handleInput(input: string): Promise<void>;
|
|
75
|
+
abort(): void;
|
|
76
|
+
cancelQueue(): void;
|
|
77
|
+
stopWaitingSelfPacedLoop(): Promise<void>;
|
|
78
|
+
shutdown(options?: { reason?: TSessionEndReason; timeoutMs?: number }): Promise<void>;
|
|
79
|
+
selectExecutionWorkspaceEntry(entryId: string): void;
|
|
80
|
+
readExecutionWorkspaceDetail(entryId: string): Promise<IExecutionDetailPage>;
|
|
81
|
+
sendAgentJob(taskId: string, input: string): Promise<void>;
|
|
82
|
+
resolveUserAction(request: IActionRequest, response: TActionResponse): void;
|
|
83
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { TSessionEndReason } from '@robota-sdk/agent-core';
|
|
2
|
+
|
|
3
|
+
export interface ITuiChannelShutdownRequest {
|
|
4
|
+
reason: TSessionEndReason;
|
|
5
|
+
message: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ITuiChannelLifecycleOperations {
|
|
9
|
+
start(): Promise<void>;
|
|
10
|
+
stop(): Promise<void>;
|
|
11
|
+
beginShutdown(): void;
|
|
12
|
+
shutdownSession(request: ITuiChannelShutdownRequest): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Marks a startup failure whose rollback also failed, so retrying cannot duplicate live resources. */
|
|
16
|
+
export class TuiChannelStartRollbackError extends AggregateError {}
|
|
17
|
+
|
|
18
|
+
/** Owns channel lifecycle transitions and bounds SDK shutdown without owning presentation state. */
|
|
19
|
+
export class TuiChannelLifecycleCoordinator {
|
|
20
|
+
private started = false;
|
|
21
|
+
private startPromise: Promise<void> | undefined;
|
|
22
|
+
private teardownBegun = false;
|
|
23
|
+
private stopped = false;
|
|
24
|
+
private shuttingDown = false;
|
|
25
|
+
private shutdownPromise: Promise<void> | undefined;
|
|
26
|
+
private stopPromise: Promise<void> | undefined;
|
|
27
|
+
|
|
28
|
+
constructor(
|
|
29
|
+
private readonly operations: ITuiChannelLifecycleOperations,
|
|
30
|
+
private readonly defaultShutdownTimeoutMs: number,
|
|
31
|
+
) {}
|
|
32
|
+
|
|
33
|
+
get isShuttingDown(): boolean {
|
|
34
|
+
return this.shuttingDown;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** False before startup and immediately when teardown begins, even if teardown later fails. */
|
|
38
|
+
get isActiveForPeerStatus(): boolean {
|
|
39
|
+
return this.started && !this.teardownBegun && !this.shuttingDown;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async start(): Promise<void> {
|
|
43
|
+
if (this.teardownBegun) {
|
|
44
|
+
throw new Error('Cannot start a channel after teardown has begun.');
|
|
45
|
+
}
|
|
46
|
+
if (this.started) return;
|
|
47
|
+
const pendingStart = (this.startPromise ??= this.operations.start());
|
|
48
|
+
try {
|
|
49
|
+
await pendingStart;
|
|
50
|
+
this.started = true;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error instanceof TuiChannelStartRollbackError) this.teardownBegun = true;
|
|
53
|
+
throw error;
|
|
54
|
+
} finally {
|
|
55
|
+
if (this.startPromise === pendingStart) this.startPromise = undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async stop(): Promise<void> {
|
|
60
|
+
this.teardownBegun = true;
|
|
61
|
+
if (this.stopped) return;
|
|
62
|
+
this.stopPromise ??= this.performStop();
|
|
63
|
+
try {
|
|
64
|
+
await this.stopPromise;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
// A failed teardown is retryable; claiming stopped here would strand owned resources.
|
|
67
|
+
this.stopPromise = undefined;
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private async performStop(): Promise<void> {
|
|
73
|
+
const pendingStart = this.startPromise;
|
|
74
|
+
let startError: Error | undefined;
|
|
75
|
+
if (pendingStart !== undefined) {
|
|
76
|
+
try {
|
|
77
|
+
await pendingStart;
|
|
78
|
+
} catch (cause) {
|
|
79
|
+
startError = cause instanceof Error ? cause : new Error(String(cause));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
let stopError: Error | undefined;
|
|
83
|
+
try {
|
|
84
|
+
await this.operations.stop();
|
|
85
|
+
} catch (error) {
|
|
86
|
+
stopError = error instanceof Error ? error : new Error(String(error));
|
|
87
|
+
}
|
|
88
|
+
this.shutdownPromise ??= this.shutdownSessionBounded(
|
|
89
|
+
{ reason: 'other', message: 'channel stopped' },
|
|
90
|
+
this.defaultShutdownTimeoutMs,
|
|
91
|
+
);
|
|
92
|
+
await this.shutdownPromise;
|
|
93
|
+
if (stopError !== undefined && startError !== undefined) {
|
|
94
|
+
throw new AggregateError(
|
|
95
|
+
[startError, stopError],
|
|
96
|
+
'TUI channel start and teardown both failed.',
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
if (stopError !== undefined) throw stopError;
|
|
100
|
+
this.started = false;
|
|
101
|
+
this.stopped = true;
|
|
102
|
+
if (startError !== undefined) throw startError;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async shutdown(options?: { reason?: TSessionEndReason; timeoutMs?: number }): Promise<void> {
|
|
106
|
+
this.teardownBegun = true;
|
|
107
|
+
if (!this.shuttingDown) {
|
|
108
|
+
this.shuttingDown = true;
|
|
109
|
+
this.operations.beginShutdown();
|
|
110
|
+
}
|
|
111
|
+
const pendingStop = this.stopPromise;
|
|
112
|
+
if (pendingStop !== undefined) {
|
|
113
|
+
await pendingStop.catch(() => undefined); // allow-fallback: the stop caller owns teardown failure
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
this.shutdownPromise ??= this.performShutdown(options);
|
|
117
|
+
await this.shutdownPromise;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private async performShutdown(options?: {
|
|
121
|
+
reason?: TSessionEndReason;
|
|
122
|
+
timeoutMs?: number;
|
|
123
|
+
}): Promise<void> {
|
|
124
|
+
const pendingStart = this.startPromise;
|
|
125
|
+
if (pendingStart !== undefined) {
|
|
126
|
+
await pendingStart.catch(() => undefined); // allow-fallback: the start caller owns startup failure
|
|
127
|
+
}
|
|
128
|
+
await this.shutdownSessionBounded(
|
|
129
|
+
{ reason: options?.reason ?? 'prompt_input_exit', message: 'CLI shutdown' },
|
|
130
|
+
options?.timeoutMs ?? this.defaultShutdownTimeoutMs,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private async shutdownSessionBounded(
|
|
135
|
+
request: ITuiChannelShutdownRequest,
|
|
136
|
+
timeoutMs: number,
|
|
137
|
+
): Promise<void> {
|
|
138
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
139
|
+
const timeout = new Promise<void>((resolve) => {
|
|
140
|
+
timer = setTimeout(resolve, timeoutMs);
|
|
141
|
+
timer.unref?.();
|
|
142
|
+
});
|
|
143
|
+
await Promise.race([
|
|
144
|
+
this.operations.shutdownSession(request).catch(() => undefined), // allow-fallback: shutdown is best-effort during process exit and remains timeout-bounded
|
|
145
|
+
timeout,
|
|
146
|
+
]);
|
|
147
|
+
if (timer) clearTimeout(timer);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { IAttentionSource } from './attention/attention-tracker.js';
|
|
2
|
+
import type { TerminalHandoffController } from './terminal-handoff-controller.js';
|
|
3
|
+
import type {
|
|
4
|
+
IAIProvider,
|
|
5
|
+
IProviderDefinition,
|
|
6
|
+
IToolWithEventService,
|
|
7
|
+
TPermissionMode,
|
|
8
|
+
} from '@robota-sdk/agent-core';
|
|
9
|
+
import type {
|
|
10
|
+
IAgentDefinition,
|
|
11
|
+
IAutomaticMemoryConfig,
|
|
12
|
+
IBackgroundTaskRunner,
|
|
13
|
+
ICommandHostAdapters,
|
|
14
|
+
IOutputStylePrompt,
|
|
15
|
+
IOrgPolicy,
|
|
16
|
+
IProviderErrorGuidance,
|
|
17
|
+
IProjectSettingsPath,
|
|
18
|
+
INodeHostSettingsSource,
|
|
19
|
+
ICommandModule,
|
|
20
|
+
ICreateSessionOptions,
|
|
21
|
+
ILivePromptTracePort,
|
|
22
|
+
EditCheckpointStore,
|
|
23
|
+
IMemoryStore,
|
|
24
|
+
IPromptHistoryOptions,
|
|
25
|
+
IPerTurnRecallConfig,
|
|
26
|
+
IRemoteCommandPolicy,
|
|
27
|
+
IToolCallHandoffPolicy,
|
|
28
|
+
TShellExecFn,
|
|
29
|
+
TSubagentRunnerFactory,
|
|
30
|
+
TWorkspaceProjectAccess,
|
|
31
|
+
} from '@robota-sdk/agent-framework';
|
|
32
|
+
import type { CommandRegistry } from '@robota-sdk/agent-framework';
|
|
33
|
+
import type {
|
|
34
|
+
IInteractiveSession,
|
|
35
|
+
IInteractiveSessionStore,
|
|
36
|
+
TInteractiveEventName,
|
|
37
|
+
} from '@robota-sdk/agent-interface-session';
|
|
38
|
+
import type { ITransportRegistryView } from '@robota-sdk/agent-interface-transport';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The TUI channel's option surface.
|
|
42
|
+
*
|
|
43
|
+
* ARCH-013: this lived inside the 698-line channel class, beside the code that consumes it. The
|
|
44
|
+
* projection FROM it — `buildTuiSessionOptions` — already had its own module, so the surface and its
|
|
45
|
+
* one projection now sit together and neither is buried in an implementation file. That matters for
|
|
46
|
+
* this item specifically: a field added to the resolved preset has to be added here to reach the
|
|
47
|
+
* interactive surface at all, and a surface nobody can find is a surface people forget.
|
|
48
|
+
*/
|
|
49
|
+
export interface ITuiInteractionChannelOptions {
|
|
50
|
+
livePromptTrace?: ILivePromptTracePort;
|
|
51
|
+
/**
|
|
52
|
+
* Provider definitions, forwarded to the session so `/provider switch` can construct the provider
|
|
53
|
+
* it switches TO.
|
|
54
|
+
*
|
|
55
|
+
* Not optional-by-accident: without it the session holds an empty list, and the hot-swap fails with
|
|
56
|
+
* "Unknown provider: <name>. Currently supported: " — an empty supported-list, which is both wrong
|
|
57
|
+
* and unactionable. The definitions live in the composition root; the session cannot discover them.
|
|
58
|
+
*/
|
|
59
|
+
providerDefinitions?: readonly IProviderDefinition[];
|
|
60
|
+
cwd: string;
|
|
61
|
+
provider: IAIProvider;
|
|
62
|
+
providerErrorGuidance?: IProviderErrorGuidance;
|
|
63
|
+
promptFileReferenceTag?: string;
|
|
64
|
+
modelCommandToolPrefix?: string;
|
|
65
|
+
subagentHookEnvironmentNames?: ICreateSessionOptions['subagentHookEnvironmentNames'];
|
|
66
|
+
observerFailureWarningCode?: ICreateSessionOptions['observerFailureWarningCode'];
|
|
67
|
+
commandHookShell?: string;
|
|
68
|
+
/** Skip instruction files and plugin discovery (`--safe-mode`). */
|
|
69
|
+
bare?: boolean;
|
|
70
|
+
/** Run no hook the settings layers declare (`--safe-mode`). */
|
|
71
|
+
skipConfiguredHooks?: boolean;
|
|
72
|
+
/** Resolved organization policy forwarded to the interactive session. */
|
|
73
|
+
orgPolicy?: IOrgPolicy;
|
|
74
|
+
projectAccess?: TWorkspaceProjectAccess;
|
|
75
|
+
projectSettingsPaths?: readonly IProjectSettingsPath[];
|
|
76
|
+
baselinePermissionAllow?: readonly string[];
|
|
77
|
+
/** Host-selected task-context root; absent means no framework task-directory scan. */
|
|
78
|
+
taskContext?: { readonly enabled?: boolean; readonly dir?: string };
|
|
79
|
+
contributionSources?: ICreateSessionOptions['contributionSources'];
|
|
80
|
+
skillRoots?: ICreateSessionOptions['skillRoots'];
|
|
81
|
+
userSettingsSources?: readonly INodeHostSettingsSource[];
|
|
82
|
+
/** Explicit authority- and permission-backed edit checkpoint capability. */
|
|
83
|
+
editCheckpointStore?: EditCheckpointStore;
|
|
84
|
+
/**
|
|
85
|
+
* CLI-076: the resolved model id (the same value the status line displays). Forwarded to the session so an
|
|
86
|
+
* explicit `--model` override reaches the provider chat call instead of being silently replaced by the
|
|
87
|
+
* session's default model.
|
|
88
|
+
*/
|
|
89
|
+
model?: string;
|
|
90
|
+
/** CLI-1988: resolved provider-neutral response style. */
|
|
91
|
+
outputStyle?: IOutputStylePrompt;
|
|
92
|
+
/** ARCH-013: resolved preset effort, threaded to the session's `effort` seam. */
|
|
93
|
+
effort?: ICreateSessionOptions['effort'];
|
|
94
|
+
temperature?: number;
|
|
95
|
+
maxOutputTokens?: number;
|
|
96
|
+
/** Preset prompt seed, distinct from the replacing `systemPrompt` option. */
|
|
97
|
+
presetSystemPrompt?: string;
|
|
98
|
+
responseFormat?: ICreateSessionOptions['responseFormat'];
|
|
99
|
+
permissionMode?: TPermissionMode;
|
|
100
|
+
maxTurns?: number;
|
|
101
|
+
sessionStore?: IInteractiveSessionStore;
|
|
102
|
+
disableSessionLoops?: boolean;
|
|
103
|
+
resolveDefaultLoopPrompt?: () => string;
|
|
104
|
+
resumeSessionId?: string;
|
|
105
|
+
forkSession?: boolean;
|
|
106
|
+
/** Issue #3081: this channel's session is the target of a `/cd` from this directory. */
|
|
107
|
+
workspaceMovedFrom?: string;
|
|
108
|
+
sessionName?: string;
|
|
109
|
+
onAutoNamed?: (name: string) => void;
|
|
110
|
+
backgroundTaskRunners?: IBackgroundTaskRunner[];
|
|
111
|
+
/** MCP-004: forwarded to `ICreateSessionOptions.toolCallHandoff`. */
|
|
112
|
+
toolCallHandoff?: IToolCallHandoffPolicy;
|
|
113
|
+
subagentRunnerFactory?: TSubagentRunnerFactory;
|
|
114
|
+
/** ARCH-005: composition-root-contributed subagent definitions (merged capability packs). */
|
|
115
|
+
agentDefinitions?: readonly IAgentDefinition[];
|
|
116
|
+
agentDefinitionRoots?: readonly string[];
|
|
117
|
+
pluginDirectories?: { readonly user?: string; readonly project?: string };
|
|
118
|
+
/**
|
|
119
|
+
* ARCH-006: tools contributed by the composition root (the capability packs `assembleProduct` merged)
|
|
120
|
+
* and, when the profile hands the packs the whole tool surface, the suppressed framework default tier
|
|
121
|
+
* (`defaultTools: []`). Forwarded to the session's tool-composition seam; absent ⇒ unchanged.
|
|
122
|
+
*/
|
|
123
|
+
additionalTools?: IToolWithEventService[];
|
|
124
|
+
defaultTools?: readonly IToolWithEventService[];
|
|
125
|
+
commandModules?: readonly ICommandModule[];
|
|
126
|
+
commandHostAdapters?: ICommandHostAdapters;
|
|
127
|
+
shellExec?: TShellExecFn;
|
|
128
|
+
/** REMOTE-006: optional remote-command policy (allow-by-default; a transport-origin command runs as a local one). */
|
|
129
|
+
remoteCommandPolicy?: IRemoteCommandPolicy;
|
|
130
|
+
transportRegistry?: ITransportRegistryView;
|
|
131
|
+
/** Bind concrete transports to each newly constructed session before registry startup. */
|
|
132
|
+
bindTransports?: (session: IInteractiveSession) => void | Promise<void>;
|
|
133
|
+
language?: string;
|
|
134
|
+
reloadPluginCommandSource?: (registry: CommandRegistry) => void;
|
|
135
|
+
agentName?: string;
|
|
136
|
+
/** Active preset id selected at startup (PRESET-011 runtime state). Defaults to 'default'. */
|
|
137
|
+
activePresetId?: string;
|
|
138
|
+
/** Preset persona block composed as a `source: 'persona'` system-prompt section (priority 5). */
|
|
139
|
+
persona?: string;
|
|
140
|
+
systemPrompt?: string;
|
|
141
|
+
appendSystemPrompt?: string;
|
|
142
|
+
allowedTools?: readonly string[];
|
|
143
|
+
deniedTools?: readonly string[];
|
|
144
|
+
/** Preset execution capability: activate agent runtime + subagent/background dispatch. */
|
|
145
|
+
enableParallelSubagents?: boolean;
|
|
146
|
+
/** Preset execution capability: run a post-task self-verification step. */
|
|
147
|
+
selfVerification?: boolean;
|
|
148
|
+
/** TERM-002: process-shared terminal-handoff controller (the TUI implementation of ITerminalHandoff). */
|
|
149
|
+
terminalHandoff?: TerminalHandoffController;
|
|
150
|
+
/** SCREEN-1992: the terminal's attention level; absent ⇒ no interval recap. */
|
|
151
|
+
attention?: IAttentionSource;
|
|
152
|
+
/**
|
|
153
|
+
* SELFHOST-008 P6: optional durable-memory store injected by the surface (agent-cli). Forwarded into
|
|
154
|
+
* `buildRuntimeSession`; absent ⇒ memory OFF (today's behavior). Enablement/policy is surface-owned.
|
|
155
|
+
*/
|
|
156
|
+
memoryStore?: IMemoryStore;
|
|
157
|
+
/** SELFHOST-008 P6: optional automatic post-turn capture policy (absent ⇒ capture OFF). */
|
|
158
|
+
automaticMemory?: IAutomaticMemoryConfig;
|
|
159
|
+
/** SELFHOST-008 P6: optional per-turn recall policy (absent ⇒ recall OFF, startup-only injection). */
|
|
160
|
+
recallMemory?: IPerTurnRecallConfig;
|
|
161
|
+
/** Observe TUI-owned session-event rendering failures without failing the committed operation. */
|
|
162
|
+
onSessionEventDeliveryError?: (error: Error, event: TInteractiveEventName) => void;
|
|
163
|
+
/**
|
|
164
|
+
* CLI-2004: the resolved screen-reader mode. A PRESENTATION field — it changes nothing about the
|
|
165
|
+
* session and is deliberately NOT projected into `buildTuiSessionOptions`; it is carried here so
|
|
166
|
+
* the render-options → channel-options projection is complete and the ARCH-110 drop hazard is
|
|
167
|
+
* asserted rather than assumed. Absent ⇒ OFF.
|
|
168
|
+
*/
|
|
169
|
+
screenReader?: boolean;
|
|
170
|
+
/** SCREEN-1993: the session-side prompt-history writer and project key; absent ⇒ nothing recorded. */
|
|
171
|
+
promptHistory?: IPromptHistoryOptions | undefined;
|
|
172
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { ITuiCliAdapter } from './tui-cli-adapter.js';
|
|
4
|
+
|
|
5
|
+
const TuiCliAdapterContext = createContext<ITuiCliAdapter | null>(null);
|
|
6
|
+
|
|
7
|
+
export const TuiCliAdapterProvider = TuiCliAdapterContext.Provider;
|
|
8
|
+
|
|
9
|
+
export function useTuiCliAdapter(): ITuiCliAdapter {
|
|
10
|
+
const adapter = useContext(TuiCliAdapterContext);
|
|
11
|
+
if (!adapter) throw new Error('TuiCliAdapterContext not provided');
|
|
12
|
+
return adapter;
|
|
13
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { TUniversalValue } from '@robota-sdk/agent-core';
|
|
2
|
+
import type { CommandRegistry } from '@robota-sdk/agent-framework';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* CLI seam injected into the renderer.
|
|
6
|
+
*
|
|
7
|
+
* CMD-004 Phase 2 Stage C: READ-ONLY toward settings — the renderer re-reads the persisted
|
|
8
|
+
* settings document for display (statusline refresh-on-result) but never writes it. All settings
|
|
9
|
+
* mutations are host actions executed by the session layer via `ICommandHostAdapters`.
|
|
10
|
+
*/
|
|
11
|
+
export interface ITuiCliAdapter {
|
|
12
|
+
getUserSettingsPath(): string;
|
|
13
|
+
readSettings(path: string): Record<string, TUniversalValue>;
|
|
14
|
+
reloadPluginCommandSource(registry: CommandRegistry): void;
|
|
15
|
+
applyActiveModelChange(
|
|
16
|
+
cwd: string,
|
|
17
|
+
modelId: string,
|
|
18
|
+
options?: { providerOverride?: string },
|
|
19
|
+
): { applied: boolean };
|
|
20
|
+
getGitBranch(cwd: string): string | undefined;
|
|
21
|
+
getProviderDisplayName(type: string): string;
|
|
22
|
+
/** Optional host-owned command shown when a completed fork can be reopened outside the TUI. */
|
|
23
|
+
formatResumeCommand?: (sessionId: string) => string;
|
|
24
|
+
}
|