@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,145 @@
|
|
|
1
|
+
import { useApp, useInput } from 'ink';
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
|
|
4
|
+
import { useKeybindingActions } from '../keybindings/keybindings-context.js';
|
|
5
|
+
import { useScreenReader } from '../screen-reader-context.js';
|
|
6
|
+
import { handleInterrupt } from '../shutdown-signal.js';
|
|
7
|
+
import { useScreenReaderTurnSignals } from './useScreenReaderTurnSignals.js';
|
|
8
|
+
|
|
9
|
+
import type { IPendingPermissionRequest } from '../types.js';
|
|
10
|
+
import type { IActionRequest, TSessionEndReason } from '@robota-sdk/agent-core';
|
|
11
|
+
import type { IExecutionWorkspaceEntry } from '@robota-sdk/agent-interface-execution';
|
|
12
|
+
import type { IToolState } from '@robota-sdk/agent-interface-session';
|
|
13
|
+
|
|
14
|
+
interface IOptions {
|
|
15
|
+
readonly isThinking: boolean;
|
|
16
|
+
readonly isShuttingDown: boolean;
|
|
17
|
+
readonly permissionRequest: IPendingPermissionRequest | null;
|
|
18
|
+
readonly pendingUserAction: IActionRequest | null;
|
|
19
|
+
readonly pluginVisible: boolean;
|
|
20
|
+
readonly transportVisible: boolean;
|
|
21
|
+
readonly sessionPickerVisible: boolean;
|
|
22
|
+
readonly workspaceSwitcherVisible: boolean;
|
|
23
|
+
/** SCREEN-1993: the history-search overlay owns Esc and the switcher key while open. */
|
|
24
|
+
readonly historySearchOpen: boolean;
|
|
25
|
+
readonly selectedEntry: IExecutionWorkspaceEntry | undefined;
|
|
26
|
+
readonly backgroundListFocused: boolean;
|
|
27
|
+
readonly mainThreadEntryId: string | undefined;
|
|
28
|
+
readonly activeTools: readonly IToolState[];
|
|
29
|
+
readonly abort: () => void;
|
|
30
|
+
readonly stopWaitingLoop: () => Promise<void>;
|
|
31
|
+
readonly toggleWorkspaceSwitcher: () => void;
|
|
32
|
+
readonly selectWorkspaceEntry: (entryId: string) => void;
|
|
33
|
+
readonly shutdown: (reason?: TSessionEndReason) => Promise<void>;
|
|
34
|
+
readonly recoveryError: string | undefined;
|
|
35
|
+
readonly recoveryPending: boolean;
|
|
36
|
+
readonly coordinationBlocked: boolean;
|
|
37
|
+
/** SCREEN-2002: the theme picker owns its keys while it is open, like every sibling overlay. */
|
|
38
|
+
readonly themePickerVisible: boolean;
|
|
39
|
+
readonly retryRecovery: () => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function overlaysBlockKeys(options: IOptions): boolean {
|
|
43
|
+
return Boolean(
|
|
44
|
+
options.permissionRequest ||
|
|
45
|
+
options.pendingUserAction ||
|
|
46
|
+
options.pluginVisible ||
|
|
47
|
+
options.transportVisible ||
|
|
48
|
+
options.sessionPickerVisible ||
|
|
49
|
+
options.workspaceSwitcherVisible ||
|
|
50
|
+
options.historySearchOpen ||
|
|
51
|
+
options.themePickerVisible ||
|
|
52
|
+
options.coordinationBlocked,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function useEscapeBindings(options: IOptions): void {
|
|
57
|
+
const context = options.isThinking ? 'thinking' : 'background-detail';
|
|
58
|
+
useKeybindingActions(context, (actions) => {
|
|
59
|
+
if (overlaysBlockKeys(options)) return;
|
|
60
|
+
if (actions.includes('abort') && options.isThinking) {
|
|
61
|
+
options.abort();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const selected = options.selectedEntry;
|
|
65
|
+
if (
|
|
66
|
+
actions.includes('return-to-main') &&
|
|
67
|
+
selected &&
|
|
68
|
+
selected.kind !== 'main_thread' &&
|
|
69
|
+
options.mainThreadEntryId
|
|
70
|
+
) {
|
|
71
|
+
options.selectWorkspaceEntry(options.mainThreadEntryId);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (actions.includes('return-to-main') && !options.backgroundListFocused) {
|
|
75
|
+
void options.stopWaitingLoop();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function useWorkspaceSwitcherBinding(options: IOptions): void {
|
|
81
|
+
useKeybindingActions('app', (actions) => {
|
|
82
|
+
if (!actions.includes('open-workspace-switcher')) return;
|
|
83
|
+
if (
|
|
84
|
+
options.permissionRequest ||
|
|
85
|
+
options.pendingUserAction ||
|
|
86
|
+
options.pluginVisible ||
|
|
87
|
+
options.sessionPickerVisible ||
|
|
88
|
+
options.historySearchOpen ||
|
|
89
|
+
options.themePickerVisible ||
|
|
90
|
+
options.coordinationBlocked ||
|
|
91
|
+
options.isShuttingDown
|
|
92
|
+
)
|
|
93
|
+
return;
|
|
94
|
+
options.toggleWorkspaceSwitcher();
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function useRecoveryBinding(options: IOptions): void {
|
|
99
|
+
useKeybindingActions('recovery', (actions) => {
|
|
100
|
+
if (
|
|
101
|
+
options.recoveryError !== undefined &&
|
|
102
|
+
!options.recoveryPending &&
|
|
103
|
+
actions.includes('retry')
|
|
104
|
+
) {
|
|
105
|
+
options.retryRecovery();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function useShutdownBindings(options: IOptions): void {
|
|
111
|
+
const { exit } = useApp();
|
|
112
|
+
const requestShutdown = (reason: TSessionEndReason): void => {
|
|
113
|
+
handleInterrupt({
|
|
114
|
+
isShuttingDown: options.isShuttingDown,
|
|
115
|
+
graceful: () => void options.shutdown(reason).finally(() => exit()),
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
useInput((input, key) => {
|
|
119
|
+
if (key.ctrl && input === 'c') requestShutdown('prompt_input_exit');
|
|
120
|
+
});
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
const onSignal = (): void => requestShutdown('other');
|
|
123
|
+
process.on('SIGINT', onSignal);
|
|
124
|
+
process.on('SIGTERM', onSignal);
|
|
125
|
+
return () => {
|
|
126
|
+
process.off('SIGINT', onSignal);
|
|
127
|
+
process.off('SIGTERM', onSignal);
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function useAppInputBindings(options: IOptions): boolean {
|
|
133
|
+
useEscapeBindings(options);
|
|
134
|
+
useRecoveryBinding(options);
|
|
135
|
+
useWorkspaceSwitcherBinding(options);
|
|
136
|
+
useShutdownBindings(options);
|
|
137
|
+
const screenReader = useScreenReader();
|
|
138
|
+
useScreenReaderTurnSignals({
|
|
139
|
+
enabled: screenReader,
|
|
140
|
+
isThinking: options.isThinking,
|
|
141
|
+
activeTools: options.activeTools,
|
|
142
|
+
awaitingAnswer: options.permissionRequest !== null || options.pendingUserAction !== null,
|
|
143
|
+
});
|
|
144
|
+
return screenReader;
|
|
145
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { useAppInputBindings } from './useAppInputBindings.js';
|
|
2
|
+
import { useAppOverlays } from './useAppOverlays.js';
|
|
3
|
+
import { useAppSubmissionState } from './useAppSubmissionState.js';
|
|
4
|
+
|
|
5
|
+
import type { IAppOverlays } from './useAppOverlays.js';
|
|
6
|
+
import type { IAppScreenState } from './useAppScreenState.js';
|
|
7
|
+
import type { IAppSubmissionState } from './useAppSubmissionState.js';
|
|
8
|
+
import type { ITuiChannelState } from './useTuiChannel.js';
|
|
9
|
+
import type { ICommandPluginAdapter } from '@robota-sdk/agent-interface-command';
|
|
10
|
+
import type { IInteractiveSessionStore } from '@robota-sdk/agent-interface-session';
|
|
11
|
+
import type { ITransportRegistryView } from '@robota-sdk/agent-interface-transport';
|
|
12
|
+
|
|
13
|
+
interface IOptions {
|
|
14
|
+
readonly cwd: string;
|
|
15
|
+
readonly state: ITuiChannelState;
|
|
16
|
+
readonly shell: IAppScreenState;
|
|
17
|
+
readonly sessionStore: IInteractiveSessionStore | undefined;
|
|
18
|
+
readonly onSessionSwitch: (sessionId: string) => Promise<void>;
|
|
19
|
+
readonly pluginAdapter: ICommandPluginAdapter | undefined;
|
|
20
|
+
readonly transportRegistry: ITransportRegistryView | undefined;
|
|
21
|
+
readonly recoveryError: string | undefined;
|
|
22
|
+
readonly recoveryPending: boolean;
|
|
23
|
+
readonly coordinationBlocked: boolean;
|
|
24
|
+
readonly retryRecovery: () => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface IAppInteractionState {
|
|
28
|
+
readonly submission: IAppSubmissionState;
|
|
29
|
+
readonly overlays: IAppOverlays;
|
|
30
|
+
readonly screenReader: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function useAppInteractionState(options: IOptions): IAppInteractionState {
|
|
34
|
+
const { screens, workspace } = options.shell;
|
|
35
|
+
const submission = useAppSubmissionState({
|
|
36
|
+
selectedEntry: workspace.background.selectedEntry,
|
|
37
|
+
submit: screens.handleSubmit,
|
|
38
|
+
sendAgentJob: options.state.sendAgentJob,
|
|
39
|
+
isThinking: options.state.isThinking,
|
|
40
|
+
});
|
|
41
|
+
const screenReader = useAppInputBindings({
|
|
42
|
+
isThinking: options.state.isThinking,
|
|
43
|
+
isShuttingDown: options.state.isShuttingDown,
|
|
44
|
+
permissionRequest: options.state.permissionRequest,
|
|
45
|
+
pendingUserAction: options.state.pendingUserAction,
|
|
46
|
+
pluginVisible: screens.showPluginTUI,
|
|
47
|
+
transportVisible: screens.showTransportTUI,
|
|
48
|
+
sessionPickerVisible: screens.showSessionPicker,
|
|
49
|
+
workspaceSwitcherVisible: workspace.background.switcherVisible,
|
|
50
|
+
historySearchOpen: options.shell.historySearchOpen,
|
|
51
|
+
themePickerVisible: screens.showThemePicker,
|
|
52
|
+
selectedEntry: workspace.background.selectedEntry,
|
|
53
|
+
backgroundListFocused: workspace.isBackgroundListFocused,
|
|
54
|
+
mainThreadEntryId: workspace.mainThreadEntryId,
|
|
55
|
+
activeTools: options.state.activeTools,
|
|
56
|
+
abort: options.state.handleAbort,
|
|
57
|
+
stopWaitingLoop: options.state.handleStopWaitingLoop,
|
|
58
|
+
toggleWorkspaceSwitcher: workspace.toggleSwitcher,
|
|
59
|
+
selectWorkspaceEntry: options.state.selectExecutionWorkspaceEntry,
|
|
60
|
+
shutdown: options.state.handleShutdown,
|
|
61
|
+
recoveryError: options.recoveryError,
|
|
62
|
+
recoveryPending: options.recoveryPending,
|
|
63
|
+
coordinationBlocked: options.coordinationBlocked,
|
|
64
|
+
retryRecovery: options.retryRecovery,
|
|
65
|
+
});
|
|
66
|
+
const overlays = useAppOverlays({
|
|
67
|
+
cwd: options.cwd,
|
|
68
|
+
pluginAdapter: options.pluginAdapter,
|
|
69
|
+
pluginVisible: screens.showPluginTUI,
|
|
70
|
+
setPluginVisible: screens.setShowPluginTUI,
|
|
71
|
+
transportRegistry: options.transportRegistry,
|
|
72
|
+
transportVisible: screens.showTransportTUI,
|
|
73
|
+
setTransportVisible: screens.setShowTransportTUI,
|
|
74
|
+
sessionStore: options.sessionStore,
|
|
75
|
+
sessionPickerVisible: screens.showSessionPicker,
|
|
76
|
+
setSessionPickerVisible: screens.setShowSessionPicker,
|
|
77
|
+
onSessionSwitch: options.onSessionSwitch,
|
|
78
|
+
addEntry: options.state.addEntry,
|
|
79
|
+
});
|
|
80
|
+
return { submission, overlays, screenReader };
|
|
81
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { useTerminalTitle } from '../use-terminal-title.js';
|
|
4
|
+
import { useTerminalHandoffSuspension } from './useTerminalHandoffSuspension.js';
|
|
5
|
+
|
|
6
|
+
import type { ITuiAppChannelPort } from '../tui-app-channel-port.js';
|
|
7
|
+
|
|
8
|
+
export interface IAppLifecycleState {
|
|
9
|
+
readonly sessionName: string | undefined;
|
|
10
|
+
readonly setSessionName: (name: string) => void;
|
|
11
|
+
readonly handoffSuspended: boolean;
|
|
12
|
+
readonly updateNotice: string | undefined;
|
|
13
|
+
readonly startError: string | undefined;
|
|
14
|
+
readonly startPending: boolean;
|
|
15
|
+
readonly retryStart: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function useChannelLifecycle(channel: ITuiAppChannelPort): {
|
|
19
|
+
startError: string | undefined;
|
|
20
|
+
startPending: boolean;
|
|
21
|
+
retryStart: () => void;
|
|
22
|
+
} {
|
|
23
|
+
const [startError, setStartError] = useState<string | undefined>();
|
|
24
|
+
const [startPending, setStartPending] = useState(true);
|
|
25
|
+
const retryStart = useCallback((): void => {
|
|
26
|
+
setStartPending(true);
|
|
27
|
+
void channel
|
|
28
|
+
.start()
|
|
29
|
+
.then(() => setStartError(undefined))
|
|
30
|
+
.catch((cause) => {
|
|
31
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
32
|
+
setStartError(`TUI start failed: ${message}`);
|
|
33
|
+
})
|
|
34
|
+
.finally(() => setStartPending(false));
|
|
35
|
+
}, [channel]);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
retryStart();
|
|
38
|
+
}, [channel, retryStart]);
|
|
39
|
+
return { startError, startPending, retryStart };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function useStartupUpdateNotice(source?: Promise<string | undefined>): string | undefined {
|
|
43
|
+
const [notice, setNotice] = useState<string | undefined>();
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
let mounted = true;
|
|
46
|
+
source
|
|
47
|
+
?.then((value) => {
|
|
48
|
+
if (mounted && value !== undefined) setNotice(value);
|
|
49
|
+
})
|
|
50
|
+
.catch(() => {
|
|
51
|
+
// Startup update checks are best-effort and must not disrupt the TUI.
|
|
52
|
+
});
|
|
53
|
+
return () => {
|
|
54
|
+
mounted = false;
|
|
55
|
+
};
|
|
56
|
+
}, [source]);
|
|
57
|
+
return notice;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function useAppLifecycleState(
|
|
61
|
+
channel: ITuiAppChannelPort,
|
|
62
|
+
startupUpdateNotice?: Promise<string | undefined>,
|
|
63
|
+
): IAppLifecycleState {
|
|
64
|
+
const channelLifecycle = useChannelLifecycle(channel);
|
|
65
|
+
const [sessionName, setSessionName] = useState<string | undefined>(channel.sessionName);
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (channel.sessionName !== undefined && channel.sessionName !== sessionName) {
|
|
68
|
+
setSessionName(channel.sessionName);
|
|
69
|
+
}
|
|
70
|
+
}, [channel, channel.sessionName, sessionName]);
|
|
71
|
+
useTerminalTitle(sessionName);
|
|
72
|
+
const handoffSuspended = useTerminalHandoffSuspension(channel.terminalHandoffController);
|
|
73
|
+
const updateNotice = useStartupUpdateNotice(startupUpdateNotice);
|
|
74
|
+
return {
|
|
75
|
+
sessionName,
|
|
76
|
+
setSessionName,
|
|
77
|
+
handoffSuspended,
|
|
78
|
+
updateNotice,
|
|
79
|
+
...channelLifecycle,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { createSystemMessage, messageToHistoryEntry } from '@robota-sdk/agent-core';
|
|
2
|
+
import { listResumableSessionSummaries } from '@robota-sdk/agent-framework';
|
|
3
|
+
import { useCallback } from 'react';
|
|
4
|
+
|
|
5
|
+
import { usePluginCallbacks } from './usePluginCallbacks.js';
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
IAppPluginViewModel,
|
|
9
|
+
IAppSessionPickerViewModel,
|
|
10
|
+
IAppTransportViewModel,
|
|
11
|
+
} from '../app-view-model.js';
|
|
12
|
+
import type { IHistoryEntry } from '@robota-sdk/agent-core';
|
|
13
|
+
import type { ICommandPluginAdapter } from '@robota-sdk/agent-interface-command';
|
|
14
|
+
import type { IInteractiveSessionStore } from '@robota-sdk/agent-interface-session';
|
|
15
|
+
import type { ITransportRegistryView } from '@robota-sdk/agent-interface-transport';
|
|
16
|
+
|
|
17
|
+
interface IOptions {
|
|
18
|
+
readonly cwd: string;
|
|
19
|
+
readonly pluginAdapter: ICommandPluginAdapter | undefined;
|
|
20
|
+
readonly pluginVisible: boolean;
|
|
21
|
+
readonly setPluginVisible: (visible: boolean) => void;
|
|
22
|
+
readonly transportRegistry: ITransportRegistryView | undefined;
|
|
23
|
+
readonly transportVisible: boolean;
|
|
24
|
+
readonly setTransportVisible: (visible: boolean) => void;
|
|
25
|
+
readonly sessionStore: IInteractiveSessionStore | undefined;
|
|
26
|
+
readonly sessionPickerVisible: boolean;
|
|
27
|
+
readonly setSessionPickerVisible: (visible: boolean) => void;
|
|
28
|
+
readonly onSessionSwitch: (sessionId: string) => Promise<void>;
|
|
29
|
+
readonly addEntry: (entry: IHistoryEntry) => void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface IAppOverlays {
|
|
33
|
+
readonly plugin: IAppPluginViewModel;
|
|
34
|
+
readonly transport: IAppTransportViewModel;
|
|
35
|
+
readonly sessionPicker: IAppSessionPickerViewModel;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function useAppOverlays(options: IOptions): IAppOverlays {
|
|
39
|
+
const pluginCallbacks = usePluginCallbacks(options.pluginAdapter);
|
|
40
|
+
const addPluginMessage = useCallback(
|
|
41
|
+
(content: string) => options.addEntry(messageToHistoryEntry(createSystemMessage(content))),
|
|
42
|
+
[options.addEntry],
|
|
43
|
+
);
|
|
44
|
+
const selectSession = useCallback(
|
|
45
|
+
(sessionId: string) => {
|
|
46
|
+
options.setSessionPickerVisible(false);
|
|
47
|
+
void options.onSessionSwitch(sessionId);
|
|
48
|
+
},
|
|
49
|
+
[options.onSessionSwitch, options.setSessionPickerVisible],
|
|
50
|
+
);
|
|
51
|
+
const cancelSessionPicker = useCallback(() => {
|
|
52
|
+
options.setSessionPickerVisible(false);
|
|
53
|
+
options.addEntry(messageToHistoryEntry(createSystemMessage('Session resume cancelled.')));
|
|
54
|
+
}, [options.addEntry, options.setSessionPickerVisible]);
|
|
55
|
+
return {
|
|
56
|
+
plugin: {
|
|
57
|
+
visible: options.pluginVisible,
|
|
58
|
+
callbacks: pluginCallbacks,
|
|
59
|
+
close: () => options.setPluginVisible(false),
|
|
60
|
+
addMessage: addPluginMessage,
|
|
61
|
+
},
|
|
62
|
+
transport: {
|
|
63
|
+
visible: options.transportVisible,
|
|
64
|
+
registry: options.transportRegistry,
|
|
65
|
+
close: () => options.setTransportVisible(false),
|
|
66
|
+
},
|
|
67
|
+
sessionPicker: {
|
|
68
|
+
visible: options.sessionPickerVisible,
|
|
69
|
+
sessions: listResumableSessionSummaries(options.sessionStore, options.cwd),
|
|
70
|
+
select: selectSession,
|
|
71
|
+
cancel: cancelSessionPicker,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { useAppearanceSettings } from './useAppearanceSettings.js';
|
|
4
|
+
import { useAppWorkspaceState } from './useAppWorkspaceState.js';
|
|
5
|
+
import { useSideEffects } from './useSideEffects.js';
|
|
6
|
+
import { useStatusLineSettings } from './useStatusLineSettings.js';
|
|
7
|
+
|
|
8
|
+
import type { IUseSideEffectsResult } from './side-effects-types.js';
|
|
9
|
+
import type { IAppWorkspaceState } from './useAppWorkspaceState.js';
|
|
10
|
+
import type { ITuiChannelState } from './useTuiChannel.js';
|
|
11
|
+
import type {
|
|
12
|
+
IAppearanceSettings,
|
|
13
|
+
IStatusLineCommandSettings,
|
|
14
|
+
} from '@robota-sdk/agent-interface-command';
|
|
15
|
+
import type { IInteractiveSessionStore } from '@robota-sdk/agent-interface-session';
|
|
16
|
+
|
|
17
|
+
interface IOptions {
|
|
18
|
+
readonly state: ITuiChannelState;
|
|
19
|
+
readonly setSessionName: (name: string) => void;
|
|
20
|
+
readonly sessionStore: IInteractiveSessionStore | undefined;
|
|
21
|
+
readonly onSessionSwitch: (sessionId: string) => Promise<void>;
|
|
22
|
+
readonly showSessionPickerOnStart: boolean | undefined;
|
|
23
|
+
readonly coordinationBlocked: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface IAppScreenState {
|
|
27
|
+
readonly screens: IUseSideEffectsResult;
|
|
28
|
+
readonly workspace: IAppWorkspaceState;
|
|
29
|
+
readonly statusSettings: IStatusLineCommandSettings;
|
|
30
|
+
/** SCREEN-2002: the persisted appearance, re-read after every slash-command result. */
|
|
31
|
+
readonly appearance: IAppearanceSettings;
|
|
32
|
+
/** SCREEN-1993: the input area's history-search overlay is open and owns the keys. */
|
|
33
|
+
readonly historySearchOpen: boolean;
|
|
34
|
+
readonly setHistorySearchOpen: (open: boolean) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function useAppScreenState(options: IOptions): IAppScreenState {
|
|
38
|
+
const [workspaceSwitcherVisible, setWorkspaceSwitcherVisible] = useState(false);
|
|
39
|
+
const [historySearchOpen, setHistorySearchOpen] = useState(false);
|
|
40
|
+
const [statusSettings, refreshStatusSettings] = useStatusLineSettings();
|
|
41
|
+
const [appearance, refreshAppearance] = useAppearanceSettings();
|
|
42
|
+
const screens = useSideEffects({
|
|
43
|
+
uiEventPort: options.state.uiEventPort,
|
|
44
|
+
baseHandleSubmit: options.state.handleSubmit,
|
|
45
|
+
setSessionName: options.setSessionName,
|
|
46
|
+
refreshStatusLineSettings: refreshStatusSettings,
|
|
47
|
+
refreshAppearanceSettings: refreshAppearance,
|
|
48
|
+
showSessionPickerOnStart: options.showSessionPickerOnStart,
|
|
49
|
+
openAgentSwitcher: () => setWorkspaceSwitcherVisible(true),
|
|
50
|
+
});
|
|
51
|
+
const navigationEnabled =
|
|
52
|
+
options.state.permissionRequest === null &&
|
|
53
|
+
options.state.pendingUserAction === null &&
|
|
54
|
+
!screens.showPluginTUI &&
|
|
55
|
+
!screens.showTransportTUI &&
|
|
56
|
+
!screens.showSessionPicker &&
|
|
57
|
+
!screens.showThemePicker &&
|
|
58
|
+
!workspaceSwitcherVisible &&
|
|
59
|
+
!historySearchOpen &&
|
|
60
|
+
!options.coordinationBlocked;
|
|
61
|
+
const workspace = useAppWorkspaceState({
|
|
62
|
+
snapshot: options.state.executionWorkspaceSnapshot,
|
|
63
|
+
selectedEntryId: options.state.selectedExecutionEntryId,
|
|
64
|
+
select: options.state.selectExecutionWorkspaceEntry,
|
|
65
|
+
read: options.state.readExecutionWorkspaceDetail,
|
|
66
|
+
sessionStore: options.sessionStore,
|
|
67
|
+
onSessionSwitch: options.onSessionSwitch,
|
|
68
|
+
addEntry: options.state.addEntry,
|
|
69
|
+
navigationEnabled,
|
|
70
|
+
switcherVisible: workspaceSwitcherVisible,
|
|
71
|
+
setSwitcherVisible: setWorkspaceSwitcherVisible,
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
screens,
|
|
75
|
+
workspace,
|
|
76
|
+
statusSettings,
|
|
77
|
+
appearance,
|
|
78
|
+
historySearchOpen,
|
|
79
|
+
setHistorySearchOpen,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { IExecutionWorkspaceEntry } from '@robota-sdk/agent-interface-execution';
|
|
4
|
+
|
|
5
|
+
interface IOptions {
|
|
6
|
+
readonly selectedEntry: IExecutionWorkspaceEntry | undefined;
|
|
7
|
+
readonly submit: (input: string) => Promise<void>;
|
|
8
|
+
readonly sendAgentJob: (taskId: string, input: string) => Promise<void>;
|
|
9
|
+
readonly isThinking: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface IAppSubmissionState {
|
|
13
|
+
readonly submit: (input: string) => Promise<void>;
|
|
14
|
+
readonly gitRefreshToken: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function useAppSubmissionState(options: IOptions): IAppSubmissionState {
|
|
18
|
+
const [gitRefreshToken, setGitRefreshToken] = useState(0);
|
|
19
|
+
const routedSubmit = useCallback(
|
|
20
|
+
async (input: string): Promise<void> => {
|
|
21
|
+
const entry = options.selectedEntry;
|
|
22
|
+
if (entry && entry.kind !== 'main_thread' && entry.controls.includes('send')) {
|
|
23
|
+
await options.sendAgentJob(entry.sourceId, input);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
await options.submit(input);
|
|
27
|
+
},
|
|
28
|
+
[options.selectedEntry, options.sendAgentJob, options.submit],
|
|
29
|
+
);
|
|
30
|
+
const submit = useCallback(
|
|
31
|
+
async (input: string): Promise<void> => {
|
|
32
|
+
setGitRefreshToken((token) => token + 1);
|
|
33
|
+
await routedSubmit(input);
|
|
34
|
+
},
|
|
35
|
+
[routedSubmit],
|
|
36
|
+
);
|
|
37
|
+
const wasThinkingRef = useRef(false);
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (wasThinkingRef.current && !options.isThinking) {
|
|
40
|
+
setGitRefreshToken((token) => token + 1);
|
|
41
|
+
}
|
|
42
|
+
wasThinkingRef.current = options.isThinking;
|
|
43
|
+
}, [options.isThinking]);
|
|
44
|
+
return { submit, gitRefreshToken };
|
|
45
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { createThemeRegistry, formatUnknownThemeNotice } from '../theme/theme-registry.js';
|
|
4
|
+
|
|
5
|
+
import type { ITuiTheme } from '../theme/theme-contracts.js';
|
|
6
|
+
import type { IThemeRegistry, IThemeSkip } from '../theme/theme-registry.js';
|
|
7
|
+
import type {
|
|
8
|
+
IAppearanceSettings,
|
|
9
|
+
IThemeAppearanceState,
|
|
10
|
+
TReducedMotionOverride,
|
|
11
|
+
} from '@robota-sdk/agent-interface-command';
|
|
12
|
+
|
|
13
|
+
/** The tier that pinned motion for this run and what it pinned — the contract's own pair. */
|
|
14
|
+
type IReducedMotionPin = NonNullable<IThemeAppearanceState['reducedMotionPin']>;
|
|
15
|
+
|
|
16
|
+
export interface IThemeToggles {
|
|
17
|
+
readonly syntaxHighlighting: boolean;
|
|
18
|
+
readonly reducedMotion: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface IAppThemePickerViewModel {
|
|
22
|
+
readonly visible: boolean;
|
|
23
|
+
readonly themes: readonly ITuiTheme[];
|
|
24
|
+
/** Theme files that were found and refused — shown as rows that cannot be chosen. */
|
|
25
|
+
readonly skipped: readonly IThemeSkip[];
|
|
26
|
+
/** The id that is PERSISTED — the row the picker returns to when it is cancelled. */
|
|
27
|
+
readonly activeThemeId: string;
|
|
28
|
+
/** The PERSISTED toggles the picker seeds itself from and submits alongside the theme. */
|
|
29
|
+
readonly syntaxHighlighting: boolean;
|
|
30
|
+
readonly reducedMotion: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Set when a tier above the settings pinned motion for this run — the tier AND what it pinned,
|
|
33
|
+
* as one value, for the reason `IThemeAppearanceState` states: the tier alone cannot say which
|
|
34
|
+
* way a run went.
|
|
35
|
+
*/
|
|
36
|
+
readonly reducedMotionPin?: IReducedMotionPin | undefined;
|
|
37
|
+
/** Move the highlight: the live region re-renders in that theme, nothing is written. */
|
|
38
|
+
readonly preview: (id: string) => void;
|
|
39
|
+
/**
|
|
40
|
+
* Commit: submits `/theme <id> syntax <on|off> motion <on|off>` — ONE patch through the command
|
|
41
|
+
* path, because the HOST owns writing the settings document. Three separate submissions would be
|
|
42
|
+
* three chances to half-apply what the user saw as one decision.
|
|
43
|
+
*/
|
|
44
|
+
readonly select: (id: string, toggles: IThemeToggles) => void;
|
|
45
|
+
/** Leave: the preview is dropped and the persisted theme is what remains. */
|
|
46
|
+
readonly cancel: () => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface IAppThemeViewModel {
|
|
50
|
+
/** The theme every component renders with right now — the preview when one is open. */
|
|
51
|
+
readonly resolved: ITuiTheme;
|
|
52
|
+
/** Whether motion is suppressed for this run, after the flag/env/settings chain. */
|
|
53
|
+
readonly reducedMotion: boolean;
|
|
54
|
+
/** Whether fenced code blocks are highlighted. Every markdown render site reads this. */
|
|
55
|
+
readonly syntaxHighlighting: boolean;
|
|
56
|
+
/** Present once, when the persisted id names a theme this build does not have. */
|
|
57
|
+
readonly unknownThemeNotice?: string;
|
|
58
|
+
readonly picker: IAppThemePickerViewModel;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface IOptions {
|
|
62
|
+
readonly appearance: IAppearanceSettings;
|
|
63
|
+
readonly registry?: IThemeRegistry | undefined;
|
|
64
|
+
readonly reducedMotion?: boolean | undefined;
|
|
65
|
+
/** Which tier decided it, when that was not the settings. */
|
|
66
|
+
readonly reducedMotionOverride?: TReducedMotionOverride | undefined;
|
|
67
|
+
readonly visible: boolean;
|
|
68
|
+
readonly setVisible: (visible: boolean) => void;
|
|
69
|
+
readonly submit: (input: string) => void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* SCREEN-2002 — which theme the live region renders with, and the picker over it.
|
|
74
|
+
*
|
|
75
|
+
* Two inputs, one answer. The PERSISTED appearance is the baseline; a preview is a local overlay
|
|
76
|
+
* that exists only while the picker is open. Selecting does not write — it submits `/theme <id>`,
|
|
77
|
+
* so the host applies the patch and the refresh-on-result path re-reads it. That keeps ONE writer
|
|
78
|
+
* for the settings document and means the picker cannot persist something `/theme` would refuse.
|
|
79
|
+
*
|
|
80
|
+
* `reducedMotion` arrives already resolved from the CLI (settings ← env ← flag). It is not
|
|
81
|
+
* recomputed here, because a second place deciding the same question is how the two start
|
|
82
|
+
* disagreeing — and it is still not the final say: `useMotion()` gates on the colour gate and
|
|
83
|
+
* screen-reader mode above it.
|
|
84
|
+
*/
|
|
85
|
+
export function useAppThemeState(options: IOptions): IAppThemeViewModel {
|
|
86
|
+
const registry = useMemo(() => options.registry ?? createThemeRegistry(), [options.registry]);
|
|
87
|
+
const [previewId, setPreviewId] = useState<string | undefined>(undefined);
|
|
88
|
+
const resolution = useMemo(
|
|
89
|
+
() => registry.resolve(previewId ?? options.appearance.theme),
|
|
90
|
+
[registry, previewId, options.appearance.theme],
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const { setVisible, submit } = options;
|
|
94
|
+
const cancel = useCallback((): void => {
|
|
95
|
+
setPreviewId(undefined);
|
|
96
|
+
setVisible(false);
|
|
97
|
+
}, [setVisible]);
|
|
98
|
+
const { syntaxHighlighting, reducedMotion } = options.appearance;
|
|
99
|
+
const select = useCallback(
|
|
100
|
+
(id: string, toggles: IThemeToggles): void => {
|
|
101
|
+
setPreviewId(undefined);
|
|
102
|
+
setVisible(false);
|
|
103
|
+
const parts = [`/theme ${id}`];
|
|
104
|
+
// Only what the user actually CHANGED. Sending all three would make choosing a theme report
|
|
105
|
+
// "syntax highlighting on, motion on" as applied to someone who touched neither — and on a
|
|
106
|
+
// run with a reduced-motion flag it would also drag out the pinned-for-this-run notice for a
|
|
107
|
+
// setting they never asked about.
|
|
108
|
+
if (toggles.syntaxHighlighting !== syntaxHighlighting) {
|
|
109
|
+
parts.push(`syntax ${toggles.syntaxHighlighting ? 'on' : 'off'}`);
|
|
110
|
+
}
|
|
111
|
+
if (toggles.reducedMotion !== reducedMotion) {
|
|
112
|
+
// `motion on` means ANIMATE, which is `reducedMotion: false` — the inversion lives in the
|
|
113
|
+
// command's parser, and this is the one place that has to speak its vocabulary.
|
|
114
|
+
parts.push(`motion ${toggles.reducedMotion ? 'off' : 'on'}`);
|
|
115
|
+
}
|
|
116
|
+
submit(parts.join(' '));
|
|
117
|
+
},
|
|
118
|
+
[reducedMotion, setVisible, submit, syntaxHighlighting],
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// A preview of an unknown id is impossible (the rows come from the registry), so the notice can
|
|
122
|
+
// only be about what is PERSISTED — and it is suppressed while previewing so it does not read as
|
|
123
|
+
// a complaint about the row the user is looking at.
|
|
124
|
+
const unknownId = previewId === undefined ? resolution.unknownId : undefined;
|
|
125
|
+
// One expression for "what motion IS for this run", read by both the view model and the picker:
|
|
126
|
+
// two copies of the same `??` chain are two things to keep in step.
|
|
127
|
+
const reducedMotionForRun = options.reducedMotion ?? options.appearance.reducedMotion;
|
|
128
|
+
return {
|
|
129
|
+
resolved: resolution.theme,
|
|
130
|
+
reducedMotion: reducedMotionForRun,
|
|
131
|
+
syntaxHighlighting: options.appearance.syntaxHighlighting,
|
|
132
|
+
...(unknownId === undefined ? {} : { unknownThemeNotice: formatUnknownThemeNotice(unknownId) }),
|
|
133
|
+
picker: {
|
|
134
|
+
visible: options.visible,
|
|
135
|
+
themes: registry.list(),
|
|
136
|
+
skipped: registry.skipped(),
|
|
137
|
+
activeThemeId: options.appearance.theme,
|
|
138
|
+
syntaxHighlighting: options.appearance.syntaxHighlighting,
|
|
139
|
+
reducedMotion: options.appearance.reducedMotion,
|
|
140
|
+
// Both halves or neither. A tier with no resolved value would have to be paired with the
|
|
141
|
+
// PERSISTED one, which is the contradiction this pair exists to stop — so a caller that
|
|
142
|
+
// supplies only the tier gets no pin rather than a wrong one, and the row simply says
|
|
143
|
+
// nothing about a pin instead of naming the wrong direction.
|
|
144
|
+
...(options.reducedMotionOverride === undefined || options.reducedMotion === undefined
|
|
145
|
+
? {}
|
|
146
|
+
: {
|
|
147
|
+
reducedMotionPin: {
|
|
148
|
+
tier: options.reducedMotionOverride,
|
|
149
|
+
reducedMotion: options.reducedMotion,
|
|
150
|
+
},
|
|
151
|
+
}),
|
|
152
|
+
preview: setPreviewId,
|
|
153
|
+
select,
|
|
154
|
+
cancel,
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|