@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,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCREEN-014 regression: the workspace callbacks must keep a stable identity across re-renders.
|
|
3
|
+
*
|
|
4
|
+
* A fresh `readExecutionWorkspaceDetail` closure every render made App's detail-loading `useEffect`
|
|
5
|
+
* re-run on every render and `setState`-loop ("Maximum update depth exceeded") the moment a
|
|
6
|
+
* background entry was selected. This test pins the callbacks to a stable identity.
|
|
7
|
+
*/
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import { render } from 'ink-testing-library';
|
|
10
|
+
import { describe, expect, it } from 'vitest';
|
|
11
|
+
|
|
12
|
+
import { useTuiChannel } from '../useTuiChannel.js';
|
|
13
|
+
|
|
14
|
+
import type { ITuiAppChannelPort } from '../../tui-app-channel-port.js';
|
|
15
|
+
|
|
16
|
+
function makeFakeChannel(): ITuiAppChannelPort & { emitChange: () => void } {
|
|
17
|
+
const manager = {
|
|
18
|
+
history: [],
|
|
19
|
+
streamingText: '',
|
|
20
|
+
activeTools: [],
|
|
21
|
+
isThinking: false,
|
|
22
|
+
isAborting: false,
|
|
23
|
+
pendingPrompt: null,
|
|
24
|
+
executionWorkspaceSnapshot: null,
|
|
25
|
+
selectedExecutionEntryId: undefined,
|
|
26
|
+
contextState: { percentage: 0, usedTokens: 0, maxTokens: 100_000 },
|
|
27
|
+
addEntry: () => undefined,
|
|
28
|
+
};
|
|
29
|
+
let onChange: (() => void) | null = null;
|
|
30
|
+
return {
|
|
31
|
+
terminalHandoffController: undefined,
|
|
32
|
+
sessionName: undefined,
|
|
33
|
+
start: () => Promise.resolve(),
|
|
34
|
+
stop: () => Promise.resolve(),
|
|
35
|
+
subscribe: (handler) => {
|
|
36
|
+
onChange = handler;
|
|
37
|
+
return () => {
|
|
38
|
+
if (onChange === handler) onChange = null;
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
emitChange: () => onChange?.(),
|
|
42
|
+
getSnapshot: () => ({
|
|
43
|
+
...manager,
|
|
44
|
+
lastErrorMessage: null,
|
|
45
|
+
isStalled: false,
|
|
46
|
+
sessionEventNotices: [],
|
|
47
|
+
isShuttingDown: false,
|
|
48
|
+
pendingCount: 0,
|
|
49
|
+
permissionRequest: null,
|
|
50
|
+
pendingUserAction: null,
|
|
51
|
+
}),
|
|
52
|
+
getSessionUiEventPort: () => ({ on: () => undefined, off: () => undefined }),
|
|
53
|
+
getCommandQueryPort: () => ({ getCommands: () => [], getSubcommands: () => [] }),
|
|
54
|
+
getRuntimeStatusSnapshot: (permissionMode) => ({ permissionMode, sessionId: '' }),
|
|
55
|
+
addEntry: manager.addEntry,
|
|
56
|
+
handleInput: () => Promise.resolve(),
|
|
57
|
+
abort: () => undefined,
|
|
58
|
+
cancelQueue: () => undefined,
|
|
59
|
+
stopWaitingSelfPacedLoop: () => Promise.resolve(),
|
|
60
|
+
shutdown: () => Promise.resolve(),
|
|
61
|
+
selectExecutionWorkspaceEntry: () => undefined,
|
|
62
|
+
readExecutionWorkspaceDetail: () => Promise.resolve({ entryId: 'x', records: [] }),
|
|
63
|
+
sendAgentJob: () => Promise.resolve(),
|
|
64
|
+
resolveUserAction: () => undefined,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function tick(ms = 25): Promise<void> {
|
|
69
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
describe('useTuiChannel callback stability (SCREEN-014 regression)', () => {
|
|
73
|
+
it('keeps readExecutionWorkspaceDetail + selectExecutionWorkspaceEntry stable across re-renders', async () => {
|
|
74
|
+
const channel = makeFakeChannel();
|
|
75
|
+
const seen: Array<{ read: unknown; select: unknown }> = [];
|
|
76
|
+
|
|
77
|
+
function Probe(): React.ReactElement | null {
|
|
78
|
+
const state = useTuiChannel(channel);
|
|
79
|
+
seen.push({
|
|
80
|
+
read: state.readExecutionWorkspaceDetail,
|
|
81
|
+
select: state.selectExecutionWorkspaceEntry,
|
|
82
|
+
});
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
render(<Probe />);
|
|
87
|
+
await tick();
|
|
88
|
+
|
|
89
|
+
// Force a re-render the way the channel does on every state change.
|
|
90
|
+
channel.emitChange();
|
|
91
|
+
channel.emitChange();
|
|
92
|
+
await tick();
|
|
93
|
+
|
|
94
|
+
expect(seen.length).toBeGreaterThanOrEqual(2);
|
|
95
|
+
const first = seen[0]!;
|
|
96
|
+
const last = seen[seen.length - 1]!;
|
|
97
|
+
expect(last.read).toBe(first.read);
|
|
98
|
+
expect(last.select).toBe(first.select);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCREEN-2002 TC-10 — what the picker submits.
|
|
3
|
+
*
|
|
4
|
+
* The picker does not write. It submits a `/theme` line, so the HOST stays the only writer of the
|
|
5
|
+
* settings document. What it submits therefore matters: sending all three keys every time would
|
|
6
|
+
* report "syntax highlighting on, motion on" as APPLIED to a user who touched neither, and on a run
|
|
7
|
+
* with a reduced-motion flag it would drag out the pinned-for-this-run notice for a setting they
|
|
8
|
+
* never asked about.
|
|
9
|
+
*/
|
|
10
|
+
import { render } from 'ink-testing-library';
|
|
11
|
+
import React from 'react';
|
|
12
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
13
|
+
|
|
14
|
+
import { useAppThemeState } from '../useAppThemeState.js';
|
|
15
|
+
|
|
16
|
+
import type { IThemeToggles } from '../useAppThemeState.js';
|
|
17
|
+
import type { IAppearanceSettings } from '@robota-sdk/agent-interface-command';
|
|
18
|
+
|
|
19
|
+
const PERSISTED: IAppearanceSettings = {
|
|
20
|
+
theme: 'dark',
|
|
21
|
+
syntaxHighlighting: true,
|
|
22
|
+
reducedMotion: false,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Drive the hook and capture what `select` submits. */
|
|
26
|
+
function submissionOf(toggles: IThemeToggles, appearance: IAppearanceSettings = PERSISTED): string {
|
|
27
|
+
const submit = vi.fn();
|
|
28
|
+
function Harness(): React.ReactElement {
|
|
29
|
+
const theme = useAppThemeState({
|
|
30
|
+
appearance,
|
|
31
|
+
visible: true,
|
|
32
|
+
setVisible: vi.fn(),
|
|
33
|
+
submit,
|
|
34
|
+
});
|
|
35
|
+
// One shot: a ref rather than an empty dependency array, so the intent is in the code instead
|
|
36
|
+
// of in a lint suppression for a rule this repo does not configure.
|
|
37
|
+
const fired = React.useRef(false);
|
|
38
|
+
React.useEffect(() => {
|
|
39
|
+
if (fired.current) return;
|
|
40
|
+
fired.current = true;
|
|
41
|
+
theme.picker.select('light', toggles);
|
|
42
|
+
});
|
|
43
|
+
return <></>;
|
|
44
|
+
}
|
|
45
|
+
render(<Harness />);
|
|
46
|
+
expect(submit).toHaveBeenCalledTimes(1);
|
|
47
|
+
return submit.mock.calls[0]?.[0] as string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The middle link. `createThemeSurface` is tested at the producer end and `ThemePicker` at the
|
|
52
|
+
* consumer end with an injected value — so deleting these three lines left the whole suite green,
|
|
53
|
+
* which is the same both-ends-pinned-middle-severed shape that made the pin dead wiring one round
|
|
54
|
+
* earlier, one hop further in.
|
|
55
|
+
*/
|
|
56
|
+
describe('the motion pin reaches the picker (SCREEN-2002 TC-08)', () => {
|
|
57
|
+
function pinSeenBy(
|
|
58
|
+
reducedMotionOverride?: 'flag' | 'environment' | 'screen-reader',
|
|
59
|
+
reducedMotion?: boolean,
|
|
60
|
+
): { tier: string; reducedMotion: boolean } | undefined {
|
|
61
|
+
let seen: { tier: string; reducedMotion: boolean } | undefined;
|
|
62
|
+
function Harness(): React.ReactElement {
|
|
63
|
+
const theme = useAppThemeState({
|
|
64
|
+
appearance: PERSISTED,
|
|
65
|
+
...(reducedMotionOverride === undefined ? {} : { reducedMotionOverride }),
|
|
66
|
+
...(reducedMotion === undefined ? {} : { reducedMotion }),
|
|
67
|
+
visible: true,
|
|
68
|
+
setVisible: vi.fn(),
|
|
69
|
+
submit: vi.fn(),
|
|
70
|
+
});
|
|
71
|
+
seen = theme.picker.reducedMotionPin;
|
|
72
|
+
return <></>;
|
|
73
|
+
}
|
|
74
|
+
render(<Harness />);
|
|
75
|
+
return seen;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
it('carries the tier AND what it pinned, which is not what is persisted', () => {
|
|
79
|
+
// The persisted value is `false`. A run pinned to `true` must report `true` — reading the
|
|
80
|
+
// persisted half here is the contradiction the pair exists to stop, and asserting only the
|
|
81
|
+
// tier leaves that rewrite green.
|
|
82
|
+
expect(PERSISTED.reducedMotion).toBe(false);
|
|
83
|
+
expect(pinSeenBy('flag', true)).toEqual({ tier: 'flag', reducedMotion: true });
|
|
84
|
+
// `--no-reduced-motion` over a persisted `true` is the opposite direction, which a tier alone
|
|
85
|
+
// cannot tell apart.
|
|
86
|
+
expect(pinSeenBy('environment', false)).toEqual({
|
|
87
|
+
tier: 'environment',
|
|
88
|
+
reducedMotion: false,
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('reports no pin at all when a caller supplies a tier without what it pinned', () => {
|
|
93
|
+
// Both halves or neither: pairing the tier with the PERSISTED value would name a direction the
|
|
94
|
+
// run may not have taken.
|
|
95
|
+
expect(pinSeenBy('flag')).toBeUndefined();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('leaves it absent when the settings decided', () => {
|
|
99
|
+
expect(pinSeenBy()).toBeUndefined();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('what the picker submits (SCREEN-2002 TC-10)', () => {
|
|
104
|
+
it('sends the theme ALONE when neither toggle was touched', () => {
|
|
105
|
+
expect(submissionOf({ syntaxHighlighting: true, reducedMotion: false })).toBe('/theme light');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('adds only the toggle that changed', () => {
|
|
109
|
+
expect(submissionOf({ syntaxHighlighting: false, reducedMotion: false })).toBe(
|
|
110
|
+
'/theme light syntax off',
|
|
111
|
+
);
|
|
112
|
+
expect(submissionOf({ syntaxHighlighting: true, reducedMotion: true })).toBe(
|
|
113
|
+
'/theme light motion off',
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('sends both when both changed, as ONE line — one patch, not three', () => {
|
|
118
|
+
expect(submissionOf({ syntaxHighlighting: false, reducedMotion: true })).toBe(
|
|
119
|
+
'/theme light syntax off motion off',
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('speaks the command s vocabulary: `motion on` means reducedMotion FALSE', () => {
|
|
124
|
+
const stillByDefault = { ...PERSISTED, reducedMotion: true };
|
|
125
|
+
|
|
126
|
+
expect(submissionOf({ syntaxHighlighting: true, reducedMotion: false }, stillByDefault)).toBe(
|
|
127
|
+
'/theme light motion on',
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renders a system-command result into the TUI state (CMD-004 Phase 2).
|
|
3
|
+
*
|
|
4
|
+
* Pure rendering only: host actions were already executed (and consumed) by the session layer,
|
|
5
|
+
* screen navigation arrives via the requester-routed `ui_intent` session event (`useSideEffects`),
|
|
6
|
+
* and state-change notifications arrive as broadcast session events (`session_renamed`,
|
|
7
|
+
* `history_cleared` — bound in `TuiInteractionChannel`). The only result-carried hint consumed
|
|
8
|
+
* here is `data.pluginRegistryReloaded` — the requester-local command-registry/autocomplete
|
|
9
|
+
* refresh (the semantic plugin reload already ran host-side inside the command).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createSystemMessage, messageToHistoryEntry } from '@robota-sdk/agent-core';
|
|
13
|
+
|
|
14
|
+
import type { TuiStateManager } from '../tui-state-manager.js';
|
|
15
|
+
import type { CommandRegistry } from '@robota-sdk/agent-framework';
|
|
16
|
+
import type { ICommandResult } from '@robota-sdk/agent-interface-command';
|
|
17
|
+
import type {
|
|
18
|
+
ISessionConversationRead,
|
|
19
|
+
ISessionLifecycle,
|
|
20
|
+
} from '@robota-sdk/agent-interface-session';
|
|
21
|
+
|
|
22
|
+
export interface ICommandResultSession extends ISessionLifecycle, ISessionConversationRead {}
|
|
23
|
+
|
|
24
|
+
export function applySystemCommandResult(
|
|
25
|
+
result: ICommandResult,
|
|
26
|
+
interactiveSession: ICommandResultSession,
|
|
27
|
+
registry: CommandRegistry,
|
|
28
|
+
manager: TuiStateManager,
|
|
29
|
+
reloadPluginCommandSource?: (registry: CommandRegistry) => void,
|
|
30
|
+
): void {
|
|
31
|
+
if (result.data?.['pluginRegistryReloaded'] === true) {
|
|
32
|
+
reloadPluginCommandSource?.(registry);
|
|
33
|
+
}
|
|
34
|
+
manager.addEntry(messageToHistoryEntry(createSystemMessage(result.message)));
|
|
35
|
+
|
|
36
|
+
if (interactiveSession.isInitialized) {
|
|
37
|
+
const ctx = interactiveSession.getContextState();
|
|
38
|
+
manager.setContextState({
|
|
39
|
+
percentage: ctx.usedPercentage,
|
|
40
|
+
usedTokens: ctx.usedTokens,
|
|
41
|
+
maxTokens: ctx.maxTokens,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ITuiSessionUiEventPort } from '../tui-app-channel-port.js';
|
|
2
|
+
import type { IStatusLineCommandSettings as TStatusLineSettings } from '@robota-sdk/agent-interface-command';
|
|
3
|
+
|
|
4
|
+
export type { TStatusLineSettings };
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* CMD-004 Phase 2 Stage C: the TUI is a pure renderer. This hook only manages the UI screens
|
|
8
|
+
* (driven by the requester-routed `ui_intent` session event), reflects the broadcast
|
|
9
|
+
* `session_renamed` title, and refreshes the statusline display after a command result — it
|
|
10
|
+
* executes no command semantics (the session layer applies host actions before the result returns).
|
|
11
|
+
*/
|
|
12
|
+
export interface IUseSideEffectsOptions {
|
|
13
|
+
uiEventPort: ITuiSessionUiEventPort;
|
|
14
|
+
baseHandleSubmit: (input: string) => Promise<void>;
|
|
15
|
+
/** Reflect the host-executed rename (broadcast `session_renamed`) into the rendered title. */
|
|
16
|
+
setSessionName: (name: string) => void;
|
|
17
|
+
/** Re-read the persisted statusline settings (the HOST applied any patch) — refresh-on-result. */
|
|
18
|
+
refreshStatusLineSettings: () => void;
|
|
19
|
+
/**
|
|
20
|
+
* SCREEN-2002: re-read the persisted appearance the same way. A `/theme` result has already been
|
|
21
|
+
* applied to the settings document by the host, so the live theme changes when this fires — which
|
|
22
|
+
* is what makes a theme switch take effect without a restart.
|
|
23
|
+
*/
|
|
24
|
+
refreshAppearanceSettings: () => void;
|
|
25
|
+
showSessionPickerOnStart?: boolean;
|
|
26
|
+
openAgentSwitcher?: () => void;
|
|
27
|
+
/**
|
|
28
|
+
* CLI-1994: honour a `switch-session` UI intent — point this terminal at another persisted
|
|
29
|
+
* session (a fork being attached to). The surface's existing session-switch path: a new channel
|
|
30
|
+
* from the factory with the previous one stopped first. A view switch, never a merge.
|
|
31
|
+
*/
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface IUseSideEffectsResult {
|
|
35
|
+
handleSubmit: (input: string) => Promise<void>;
|
|
36
|
+
showPluginTUI: boolean;
|
|
37
|
+
showSessionPicker: boolean;
|
|
38
|
+
showTransportTUI: boolean;
|
|
39
|
+
/** SCREEN-2002: the theme picker, opened by the `show-theme-picker` intent. */
|
|
40
|
+
showThemePicker: boolean;
|
|
41
|
+
setShowPluginTUI: (show: boolean) => void;
|
|
42
|
+
setShowSessionPicker: (show: boolean) => void;
|
|
43
|
+
setShowTransportTUI: (show: boolean) => void;
|
|
44
|
+
setShowThemePicker: (show: boolean) => void;
|
|
45
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { buildStaticItems } from '../app-static-items.js';
|
|
4
|
+
import { useAppInteractionState } from './useAppInteractionState.js';
|
|
5
|
+
import { useAppLifecycleState } from './useAppLifecycleState.js';
|
|
6
|
+
import { useAppScreenState } from './useAppScreenState.js';
|
|
7
|
+
import { useAppThemeState } from './useAppThemeState.js';
|
|
8
|
+
import { useTuiChannel } from './useTuiChannel.js';
|
|
9
|
+
|
|
10
|
+
import type { IBuildStaticItemsInputs } from '../app-static-items.js';
|
|
11
|
+
import type { IAppInputViewModel, IAppStatusViewModel, IAppViewModel } from '../app-view-model.js';
|
|
12
|
+
import type { ITuiAppChannelPort, ITuiRuntimeStatusSnapshot } from '../tui-app-channel-port.js';
|
|
13
|
+
import type { IAppInteractionState } from './useAppInteractionState.js';
|
|
14
|
+
import type { IAppLifecycleState } from './useAppLifecycleState.js';
|
|
15
|
+
import type { IAppScreenState } from './useAppScreenState.js';
|
|
16
|
+
import type { IAppThemeViewModel } from './useAppThemeState.js';
|
|
17
|
+
import type { ITuiChannelState } from './useTuiChannel.js';
|
|
18
|
+
import type { IThemeRegistry } from '../theme/theme-registry.js';
|
|
19
|
+
import type { TPermissionMode } from '@robota-sdk/agent-core';
|
|
20
|
+
import type { TReducedMotionOverride } from '@robota-sdk/agent-interface-command';
|
|
21
|
+
import type { ICommandPluginAdapter } from '@robota-sdk/agent-interface-command';
|
|
22
|
+
import type { IInteractiveSessionStore } from '@robota-sdk/agent-interface-session';
|
|
23
|
+
import type { IPromptHistorySource } from '@robota-sdk/agent-interface-session';
|
|
24
|
+
import type { ITransportRegistryView } from '@robota-sdk/agent-interface-transport';
|
|
25
|
+
|
|
26
|
+
export interface IUseAppControllerOptions {
|
|
27
|
+
cwd: string;
|
|
28
|
+
providerType?: string | undefined;
|
|
29
|
+
modelId?: string;
|
|
30
|
+
permissionMode?: TPermissionMode;
|
|
31
|
+
version?: string;
|
|
32
|
+
sessionStore?: IInteractiveSessionStore;
|
|
33
|
+
showSessionPickerOnStart?: boolean;
|
|
34
|
+
/** FLOW-2006: a deep link's prefill, handed to the composer exactly once. */
|
|
35
|
+
initialInput?: string;
|
|
36
|
+
initialInputOrigin?: 'external-link';
|
|
37
|
+
startupUpdateNotice?: Promise<string | undefined>;
|
|
38
|
+
transportRegistry?: ITransportRegistryView;
|
|
39
|
+
pluginAdapter?: ICommandPluginAdapter;
|
|
40
|
+
sessionSwitchError?: string;
|
|
41
|
+
sessionSwitchPending?: boolean;
|
|
42
|
+
channel: ITuiAppChannelPort;
|
|
43
|
+
onSessionSwitch: (sessionId: string) => Promise<void>;
|
|
44
|
+
onRetrySessionSwitch: () => void;
|
|
45
|
+
/** SCREEN-1993: the stored-prompt source and this run's project key; both or neither. */
|
|
46
|
+
promptHistorySource?: IPromptHistorySource;
|
|
47
|
+
promptHistoryProject?: string;
|
|
48
|
+
/** SCREEN-2002: the catalogue this surface renders from; absent ⇒ the built-ins. */
|
|
49
|
+
themeRegistry?: IThemeRegistry;
|
|
50
|
+
/** SCREEN-2002: reduced motion as the product shell resolved it (settings ← env ← flag). */
|
|
51
|
+
reducedMotion?: boolean | undefined;
|
|
52
|
+
/** SCREEN-2002: which tier decided it, when that was not the settings. */
|
|
53
|
+
reducedMotionOverride?: TReducedMotionOverride | undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface ICoordinationState {
|
|
57
|
+
readonly error: string | undefined;
|
|
58
|
+
readonly pending: boolean;
|
|
59
|
+
readonly blocked: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface IComposition {
|
|
63
|
+
readonly props: IUseAppControllerOptions;
|
|
64
|
+
readonly state: ITuiChannelState;
|
|
65
|
+
readonly lifecycle: IAppLifecycleState;
|
|
66
|
+
readonly shell: IAppScreenState;
|
|
67
|
+
readonly interaction: IAppInteractionState;
|
|
68
|
+
readonly staticItems: IAppViewModel['staticItems'];
|
|
69
|
+
readonly runtime: ITuiRuntimeStatusSnapshot;
|
|
70
|
+
readonly coordination: ICoordinationState;
|
|
71
|
+
readonly theme: IAppThemeViewModel;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getCoordinationState(
|
|
75
|
+
props: IUseAppControllerOptions,
|
|
76
|
+
lifecycle: IAppLifecycleState,
|
|
77
|
+
): ICoordinationState {
|
|
78
|
+
const error = props.sessionSwitchError ?? lifecycle.startError;
|
|
79
|
+
const pending = Boolean(props.sessionSwitchPending || lifecycle.startPending);
|
|
80
|
+
return { error, pending, blocked: error !== undefined || pending };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function buildHistorySearch(composition: IComposition): IAppInputViewModel['historySearch'] {
|
|
84
|
+
const { promptHistorySource, promptHistoryProject } = composition.props;
|
|
85
|
+
if (promptHistorySource === undefined || promptHistoryProject === undefined) return undefined;
|
|
86
|
+
return {
|
|
87
|
+
source: promptHistorySource,
|
|
88
|
+
project: promptHistoryProject,
|
|
89
|
+
sessionId: composition.runtime.sessionId,
|
|
90
|
+
onOpenChange: composition.shell.setHistorySearchOpen,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function buildInput(composition: IComposition): IAppInputViewModel {
|
|
95
|
+
const { screens, workspace } = composition.shell;
|
|
96
|
+
const state = composition.state;
|
|
97
|
+
const overlayOpen = Boolean(
|
|
98
|
+
state.permissionRequest ||
|
|
99
|
+
state.pendingUserAction ||
|
|
100
|
+
screens.showPluginTUI ||
|
|
101
|
+
screens.showTransportTUI ||
|
|
102
|
+
screens.showSessionPicker ||
|
|
103
|
+
screens.showThemePicker ||
|
|
104
|
+
workspace.background.switcherVisible,
|
|
105
|
+
);
|
|
106
|
+
const interactionBlocked =
|
|
107
|
+
composition.coordination.blocked ||
|
|
108
|
+
overlayOpen ||
|
|
109
|
+
state.isShuttingDown ||
|
|
110
|
+
!workspace.isSelectedEntryInteractive ||
|
|
111
|
+
workspace.isBackgroundListFocused;
|
|
112
|
+
return {
|
|
113
|
+
submit: composition.interaction.submission.submit,
|
|
114
|
+
cancelQueue: state.handleCancelQueue,
|
|
115
|
+
disabled: interactionBlocked || (state.isThinking && state.pendingPrompt !== null),
|
|
116
|
+
queueCancellationDisabled: interactionBlocked,
|
|
117
|
+
isAborting: state.isAborting,
|
|
118
|
+
pendingPrompt: state.pendingPrompt,
|
|
119
|
+
pendingCount: state.pendingCount,
|
|
120
|
+
commandQueryPort: state.commandQueryPort,
|
|
121
|
+
sessionName: composition.lifecycle.sessionName,
|
|
122
|
+
history: state.history,
|
|
123
|
+
focusBackgroundList: workspace.focusBackgroundList,
|
|
124
|
+
historySearch: buildHistorySearch(composition),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function buildStatus(composition: IComposition): IAppStatusViewModel {
|
|
129
|
+
const { props, runtime, state } = composition;
|
|
130
|
+
const workspace = composition.shell.workspace;
|
|
131
|
+
return {
|
|
132
|
+
cwd: props.cwd,
|
|
133
|
+
permissionMode: runtime.permissionMode,
|
|
134
|
+
modelId: props.modelId,
|
|
135
|
+
providerType: props.providerType,
|
|
136
|
+
sessionId: runtime.sessionId,
|
|
137
|
+
isThinking: state.isThinking,
|
|
138
|
+
activeToolCount: state.activeTools.length,
|
|
139
|
+
activeBackgroundTaskCount: workspace.activeBackgroundTaskCount,
|
|
140
|
+
hasPendingPrompt: state.pendingPrompt !== null,
|
|
141
|
+
contextState: state.contextState,
|
|
142
|
+
sessionName: composition.lifecycle.sessionName,
|
|
143
|
+
settings: composition.shell.statusSettings,
|
|
144
|
+
activeAgentLabel: workspace.activeAgentLabel,
|
|
145
|
+
activePresetId: runtime.activePresetId,
|
|
146
|
+
effort: runtime.effort,
|
|
147
|
+
gitRefreshToken: composition.interaction.submission.gitRefreshToken,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function buildViewModel(composition: IComposition): IAppViewModel {
|
|
152
|
+
const { interaction, lifecycle, shell, state } = composition;
|
|
153
|
+
return {
|
|
154
|
+
staticItems: composition.staticItems,
|
|
155
|
+
handoffSuspended: lifecycle.handoffSuspended,
|
|
156
|
+
updateNotice: lifecycle.updateNotice,
|
|
157
|
+
coordinationError: composition.coordination.error,
|
|
158
|
+
coordinationPending: composition.coordination.pending,
|
|
159
|
+
sessionEventNotices: state.sessionEventNotices,
|
|
160
|
+
isShuttingDown: state.isShuttingDown,
|
|
161
|
+
stream: {
|
|
162
|
+
text: state.streamingText,
|
|
163
|
+
activeTools: state.activeTools,
|
|
164
|
+
isThinking: state.isThinking,
|
|
165
|
+
isStalled: state.isStalled,
|
|
166
|
+
lastErrorMessage: state.lastErrorMessage,
|
|
167
|
+
},
|
|
168
|
+
background: shell.workspace.background,
|
|
169
|
+
permissionRequest: state.permissionRequest,
|
|
170
|
+
pendingUserAction: state.pendingUserAction,
|
|
171
|
+
resolveUserAction: state.resolveUserAction,
|
|
172
|
+
...interaction.overlays,
|
|
173
|
+
theme: composition.theme,
|
|
174
|
+
contextPercentage: state.contextState.percentage,
|
|
175
|
+
input: buildInput(composition),
|
|
176
|
+
status: buildStatus(composition),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** The live theme and its picker, composed from the shell's persisted appearance and the shell's props. */
|
|
181
|
+
function useControllerTheme(
|
|
182
|
+
props: IUseAppControllerOptions,
|
|
183
|
+
shell: IAppScreenState,
|
|
184
|
+
): IAppThemeViewModel {
|
|
185
|
+
return useAppThemeState({
|
|
186
|
+
appearance: shell.appearance,
|
|
187
|
+
registry: props.themeRegistry,
|
|
188
|
+
reducedMotion: props.reducedMotion,
|
|
189
|
+
reducedMotionOverride: props.reducedMotionOverride,
|
|
190
|
+
visible: shell.screens.showThemePicker,
|
|
191
|
+
setVisible: shell.screens.setShowThemePicker,
|
|
192
|
+
submit: (input) => void shell.screens.handleSubmit(input),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* FLOW-2006: the deep link's prefill is owned HERE, not by the composer's `useState` initializer.
|
|
198
|
+
* `AppPresentation` unmounts the prompt subtree while a terminal handoff is suspended, so a
|
|
199
|
+
* component-local initializer would re-seed a prompt the user had already cleared or submitted.
|
|
200
|
+
*/
|
|
201
|
+
function useExternalPrefill(
|
|
202
|
+
initialInput: string | undefined,
|
|
203
|
+
origin: IUseAppControllerOptions['initialInputOrigin'],
|
|
204
|
+
): Pick<IAppInputViewModel, 'initialValue' | 'consumeInitialValue' | 'externalPromptOrigin'> {
|
|
205
|
+
const [initialValue, setInitialValue] = useState(initialInput);
|
|
206
|
+
const consumeInitialValue = useCallback(() => setInitialValue(undefined), []);
|
|
207
|
+
return { initialValue, consumeInitialValue, externalPromptOrigin: origin === 'external-link' };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** The scrollback Ink commits once: the banner (unless suppressed) and the settled history. */
|
|
211
|
+
function useStaticItems(
|
|
212
|
+
history: IBuildStaticItemsInputs['history'],
|
|
213
|
+
version: IBuildStaticItemsInputs['version'],
|
|
214
|
+
screenReader: IBuildStaticItemsInputs['screenReader'],
|
|
215
|
+
): IAppViewModel['staticItems'] {
|
|
216
|
+
return useMemo(
|
|
217
|
+
() => buildStaticItems({ history, version, screenReader }),
|
|
218
|
+
[history, screenReader, version],
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Compose responsibility-specific hooks into the only view model accepted by presentation. */
|
|
223
|
+
export function useAppController(props: IUseAppControllerOptions): IAppViewModel {
|
|
224
|
+
const prefill = useExternalPrefill(props.initialInput, props.initialInputOrigin);
|
|
225
|
+
const state = useTuiChannel(props.channel);
|
|
226
|
+
const lifecycle = useAppLifecycleState(props.channel, props.startupUpdateNotice);
|
|
227
|
+
const coordination = getCoordinationState(props, lifecycle);
|
|
228
|
+
const shell = useAppScreenState({
|
|
229
|
+
state,
|
|
230
|
+
setSessionName: lifecycle.setSessionName,
|
|
231
|
+
sessionStore: props.sessionStore,
|
|
232
|
+
onSessionSwitch: props.onSessionSwitch,
|
|
233
|
+
showSessionPickerOnStart: props.showSessionPickerOnStart,
|
|
234
|
+
coordinationBlocked: coordination.blocked,
|
|
235
|
+
});
|
|
236
|
+
const interaction = useAppInteractionState({
|
|
237
|
+
cwd: props.cwd,
|
|
238
|
+
state,
|
|
239
|
+
shell,
|
|
240
|
+
sessionStore: props.sessionStore,
|
|
241
|
+
onSessionSwitch: props.onSessionSwitch,
|
|
242
|
+
pluginAdapter: props.pluginAdapter,
|
|
243
|
+
transportRegistry: props.transportRegistry,
|
|
244
|
+
recoveryError: coordination.error,
|
|
245
|
+
recoveryPending: coordination.pending,
|
|
246
|
+
coordinationBlocked: coordination.blocked,
|
|
247
|
+
retryRecovery:
|
|
248
|
+
props.sessionSwitchError !== undefined ? props.onRetrySessionSwitch : lifecycle.retryStart,
|
|
249
|
+
});
|
|
250
|
+
const staticItems = useStaticItems(state.history, props.version, interaction.screenReader);
|
|
251
|
+
const runtime = state.getRuntimeStatusSnapshot(props.permissionMode ?? 'default');
|
|
252
|
+
const theme = useControllerTheme(props, shell);
|
|
253
|
+
const viewModel = buildViewModel({
|
|
254
|
+
props,
|
|
255
|
+
state,
|
|
256
|
+
lifecycle,
|
|
257
|
+
shell,
|
|
258
|
+
interaction,
|
|
259
|
+
staticItems,
|
|
260
|
+
runtime,
|
|
261
|
+
coordination,
|
|
262
|
+
theme,
|
|
263
|
+
});
|
|
264
|
+
return { ...viewModel, input: { ...viewModel.input, ...prefill } };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export type { IAppViewModel } from '../app-view-model.js';
|