@robota-sdk/agent-ui-terminal 3.0.0-beta.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1204 -0
- package/LICENSE +661 -0
- package/README.md +50 -0
- package/dist/node/index.d.ts +1224 -0
- package/dist/node/index.d.ts.map +1 -0
- package/dist/node/index.js +33 -0
- package/dist/node/index.js.map +1 -0
- package/package.json +88 -0
- package/src/App.tsx +157 -0
- package/src/AppPresentation.tsx +190 -0
- package/src/AppView.tsx +26 -0
- package/src/BackgroundTaskPanel.tsx +73 -0
- package/src/CjkTextInput.tsx +336 -0
- package/src/ConfirmPrompt.tsx +140 -0
- package/src/ContextWarningBanner.tsx +45 -0
- package/src/ExecutionWorkspaceDetailPane.tsx +85 -0
- package/src/ExecutionWorkspaceSwitcher.tsx +213 -0
- package/src/HistorySearchOverlay.tsx +150 -0
- package/src/InputArea.tsx +338 -0
- package/src/ListPicker.tsx +158 -0
- package/src/MenuSelect.tsx +159 -0
- package/src/MessageList.tsx +308 -0
- package/src/MultiSelectList.tsx +294 -0
- package/src/PendingActionPrompt.tsx +121 -0
- package/src/PermissionPrompt.tsx +236 -0
- package/src/PluginTUI.tsx +258 -0
- package/src/RoleLabel.tsx +72 -0
- package/src/SafeText.tsx +66 -0
- package/src/SessionEventNotices.tsx +27 -0
- package/src/SessionPicker.tsx +92 -0
- package/src/SessionStatusBar.tsx +80 -0
- package/src/SlashAutocomplete.tsx +154 -0
- package/src/StatusBar.tsx +308 -0
- package/src/StreamingIndicator.tsx +145 -0
- package/src/SupervisedSessionView.tsx +872 -0
- package/src/TextPrompt.tsx +112 -0
- package/src/ThemePicker.tsx +366 -0
- package/src/ToolCommandOutput.tsx +48 -0
- package/src/ToolDiffBlock.tsx +44 -0
- package/src/TransportTUI.tsx +166 -0
- package/src/TuiInteractionChannel.ts +530 -0
- package/src/UpdateNotice.tsx +18 -0
- package/src/UsageSummaryEntry.tsx +49 -0
- package/src/WaveText.tsx +70 -0
- package/src/__tests__/ListPicker.test.tsx +188 -0
- package/src/__tests__/MenuSelect.test.tsx +127 -0
- package/src/__tests__/PendingActionPrompt.test.tsx +193 -0
- package/src/__tests__/PluginTUI.test.tsx +167 -0
- package/src/__tests__/SlashAutocomplete.test.tsx +162 -0
- package/src/__tests__/SupervisedSessionView-render.test.tsx +52 -0
- package/src/__tests__/SupervisedSessionView.test.tsx +1112 -0
- package/src/__tests__/TextPrompt.test.tsx +112 -0
- package/src/__tests__/TransportTUI.test.tsx +124 -0
- package/src/__tests__/TuiInteractionChannel.askUser.test.ts +124 -0
- package/src/__tests__/TuiInteractionChannel.display-contract.test.ts +294 -0
- package/src/__tests__/TuiInteractionChannel.lifecycle.test.ts +650 -0
- package/src/__tests__/UpdateNotice.test.tsx +15 -0
- package/src/__tests__/__fixtures__/korean-text.json +17 -0
- package/src/__tests__/abort-after-permission.test.tsx +160 -0
- package/src/__tests__/abort-streaming-e2e.test.tsx +183 -0
- package/src/__tests__/attached-app-session-switch.test.tsx +317 -0
- package/src/__tests__/attention-bell-and-marks.test.ts +144 -0
- package/src/__tests__/autocomplete-editing-keys.test.tsx +127 -0
- package/src/__tests__/background-task-panel-countdown.test.tsx +75 -0
- package/src/__tests__/background-task-panel.test.tsx +98 -0
- package/src/__tests__/background-task-row-format.test.ts +152 -0
- package/src/__tests__/channel-factory-integration.test.ts +150 -0
- package/src/__tests__/checkpoint-store-per-channel.test.ts +81 -0
- package/src/__tests__/cjk-defer-submit.test.tsx +68 -0
- package/src/__tests__/cjk-fallback-render.test.tsx +89 -0
- package/src/__tests__/cjk-text-input-flow.test.ts +187 -0
- package/src/__tests__/cjk-text-input.test.ts +196 -0
- package/src/__tests__/command-handoff-pty-e2e.test.ts +107 -0
- package/src/__tests__/command-output-summary.test.ts +95 -0
- package/src/__tests__/command-result-handler.test.ts +171 -0
- package/src/__tests__/compact-event-bridge.test.ts +20 -0
- package/src/__tests__/confirm-permission-flow.test.ts +149 -0
- package/src/__tests__/confirm-prompt.test.tsx +103 -0
- package/src/__tests__/contextual-keybindings-input.test.tsx +158 -0
- package/src/__tests__/default-tui-cli-adapter-host-settings.test.ts +47 -0
- package/src/__tests__/effort-reaches-the-session.test.ts +41 -0
- package/src/__tests__/execution-workspace-switcher.test.tsx +147 -0
- package/src/__tests__/execution-workspace-view-model.test.ts +94 -0
- package/src/__tests__/external-prompt-notice.test.tsx +100 -0
- package/src/__tests__/fixtures/command-handoff-driver.tsx +114 -0
- package/src/__tests__/fixtures/fake-editor.sh +7 -0
- package/src/__tests__/fixtures/terminal-handoff-driver.tsx +90 -0
- package/src/__tests__/fork-attach.test.tsx +235 -0
- package/src/__tests__/grant-history-reaches-the-session.test.ts +31 -0
- package/src/__tests__/helpers/terminal-profiles.ts +152 -0
- package/src/__tests__/helpers/vt-cursor-interpreter.ts +175 -0
- package/src/__tests__/history-clear-broadcast.test.ts +66 -0
- package/src/__tests__/history-search-overlay.test.tsx +329 -0
- package/src/__tests__/humanize-tool-name.test.ts +30 -0
- package/src/__tests__/input-area-bottom-border.test.tsx +45 -0
- package/src/__tests__/input-area-flow.test.ts +199 -0
- package/src/__tests__/input-area-focus-handoff.test.tsx +77 -0
- package/src/__tests__/interaction-channel-charter.test.ts +13 -0
- package/src/__tests__/key-hint-consistency.test.tsx +249 -0
- package/src/__tests__/key-hint-footer.test.tsx +69 -0
- package/src/__tests__/live-trace-reaches-session.test.ts +14 -0
- package/src/__tests__/message-list-rendering.test.tsx +474 -0
- package/src/__tests__/numbered-selection-chunk.test.tsx +41 -0
- package/src/__tests__/org-policy-reaches-the-channel.test.ts +77 -0
- package/src/__tests__/output-style-reaches-session.test.ts +35 -0
- package/src/__tests__/palette-consistency.test.ts +329 -0
- package/src/__tests__/permission-prompt-arming.test.tsx +153 -0
- package/src/__tests__/permission-prompt-availability.test.tsx +23 -0
- package/src/__tests__/permission-prompt-peer.test.tsx +34 -0
- package/src/__tests__/permission-prompt-requester.test.tsx +34 -0
- package/src/__tests__/product-display-name.test.tsx +79 -0
- package/src/__tests__/prompt-queue.test.tsx +260 -0
- package/src/__tests__/provider-definitions-reach-the-session.test.ts +75 -0
- package/src/__tests__/pty/ask-user-question.ptytest.ts +72 -0
- package/src/__tests__/pty/background-work-switcher.ptytest.ts +53 -0
- package/src/__tests__/pty/behavior-2437-git.ptytest.ts +350 -0
- package/src/__tests__/pty/fixtures/ask-user-question.jsonl +3 -0
- package/src/__tests__/pty/fixtures/replay-conversation.jsonl +4 -0
- package/src/__tests__/pty/fixtures/screen-005-ask-shapes.jsonl +5 -0
- package/src/__tests__/pty/fixtures/screen-005-permission.jsonl +3 -0
- package/src/__tests__/pty/fixtures/screen-006-colors.jsonl +3 -0
- package/src/__tests__/pty/fixtures/screen-2002-themes.jsonl +5 -0
- package/src/__tests__/pty/flag-tui.ptytest.ts +56 -0
- package/src/__tests__/pty/flow-2006-deep-link.ptytest.ts +320 -0
- package/src/__tests__/pty/ime-cursor-tmux.ptytest.ts +211 -0
- package/src/__tests__/pty/ime-cursor.ptytest.ts +187 -0
- package/src/__tests__/pty/isolated-home.ts +97 -0
- package/src/__tests__/pty/org-policy.ptytest.ts +71 -0
- package/src/__tests__/pty/provider-setup.ptytest.ts +61 -0
- package/src/__tests__/pty/pty-driver.ts +155 -0
- package/src/__tests__/pty/replay-conversation.ptytest.ts +68 -0
- package/src/__tests__/pty/screen-005-prompt-footers.ptytest.ts +135 -0
- package/src/__tests__/pty/screen-006-no-color.ptytest.ts +140 -0
- package/src/__tests__/pty/screen-010-scrollback.ptytest.ts +123 -0
- package/src/__tests__/pty/screen-1993-scrollback.ptytest.ts +124 -0
- package/src/__tests__/pty/screen-2002-themes.ptytest.ts +292 -0
- package/src/__tests__/pty/screen-2670-prepark.ptytest.ts +228 -0
- package/src/__tests__/pty/screen-reader-mode.ptytest.ts +228 -0
- package/src/__tests__/pty/session-attach.ptytest.ts +182 -0
- package/src/__tests__/pty/settings-screen.ptytest.ts +49 -0
- package/src/__tests__/pty/spawn-pty.test.ts +120 -0
- package/src/__tests__/pty/spawn-pty.ts +229 -0
- package/src/__tests__/pty/terminal-handoff.ptytest.ts +79 -0
- package/src/__tests__/pty/trust-prompt.ptytest.ts +134 -0
- package/src/__tests__/pty/tui-pty.ptytest.ts +65 -0
- package/src/__tests__/pty-driver-dispose.test.ts +43 -0
- package/src/__tests__/real-cursor-positioning.test.tsx +264 -0
- package/src/__tests__/rename-broadcast-persistence.test.tsx +140 -0
- package/src/__tests__/render-channel-options.test.ts +201 -0
- package/src/__tests__/render-lifecycle.test.ts +36 -0
- package/src/__tests__/render-markdown.test.ts +164 -0
- package/src/__tests__/rendered-markdown-styling.test.tsx +98 -0
- package/src/__tests__/safe-text-boundary.test.tsx +75 -0
- package/src/__tests__/sandbox-reaches-the-session.test.ts +37 -0
- package/src/__tests__/screen-reader-input.test.tsx +106 -0
- package/src/__tests__/screen-reader-labels.test.ts +91 -0
- package/src/__tests__/screen-reader-menus.test.tsx +200 -0
- package/src/__tests__/screen-reader-pacing.test.ts +230 -0
- package/src/__tests__/screen-reader-prepark-echo.test.tsx +200 -0
- package/src/__tests__/screen-reader-render-options.test.ts +262 -0
- package/src/__tests__/screen-reader-stdout.test.ts +322 -0
- package/src/__tests__/screen-reader-turn-marks-port.test.tsx +85 -0
- package/src/__tests__/screen-reader-turn-signals.test.tsx +125 -0
- package/src/__tests__/sec-019-terminal-sanitizer.test.ts +276 -0
- package/src/__tests__/sec-019-tool-label-render.test.tsx +207 -0
- package/src/__tests__/selection-flow.test.ts +61 -0
- package/src/__tests__/session-event-notices.test.tsx +31 -0
- package/src/__tests__/session-init-poller.test.ts +102 -0
- package/src/__tests__/session-picker.test.tsx +93 -0
- package/src/__tests__/session-switch-channel.test.tsx +470 -0
- package/src/__tests__/shutdown-signal.test.ts +34 -0
- package/src/__tests__/status-activity.test.ts +71 -0
- package/src/__tests__/status-bar.test.tsx +290 -0
- package/src/__tests__/status-glyph.test.ts +30 -0
- package/src/__tests__/streaming-indicator.test.tsx +178 -0
- package/src/__tests__/terminal-capabilities.test.ts +153 -0
- package/src/__tests__/terminal-handoff-controller.test.ts +173 -0
- package/src/__tests__/terminal-handoff-pty-e2e.test.ts +77 -0
- package/src/__tests__/text-prompt-flow.test.ts +77 -0
- package/src/__tests__/theme-picker.test.tsx +345 -0
- package/src/__tests__/transport-boundary.test.ts +11 -0
- package/src/__tests__/tui-app-channel-port.test.ts +31 -0
- package/src/__tests__/tui-channel-init-failure.test.ts +70 -0
- package/src/__tests__/tui-channel-lifecycle-coordinator.test.ts +258 -0
- package/src/__tests__/tui-interaction-queues.test.ts +149 -0
- package/src/__tests__/tui-preset-capabilities.test.ts +33 -0
- package/src/__tests__/tui-state-manager.error-stall.test.ts +169 -0
- package/src/__tests__/tui-state-manager.test.ts +482 -0
- package/src/__tests__/ui-intent-rendering.test.tsx +291 -0
- package/src/__tests__/wave-text.test.tsx +133 -0
- package/src/__tests__/wire-tui-channel.test.ts +1457 -0
- package/src/__tests__/wire-tui-client-commands.test.ts +243 -0
- package/src/app-banner.tsx +42 -0
- package/src/app-static-items.ts +35 -0
- package/src/app-view-model.ts +140 -0
- package/src/attached-session-connection.ts +14 -0
- package/src/attention/__tests__/attention-coordinator.test.ts +166 -0
- package/src/attention/__tests__/attention-tracker.test.ts +103 -0
- package/src/attention/__tests__/countdown.test.ts +15 -0
- package/src/attention/__tests__/focus-input-filter.test.ts +93 -0
- package/src/attention/__tests__/interval-recap.test.ts +49 -0
- package/src/attention/attention-coordinator.ts +71 -0
- package/src/attention/attention-tracker.ts +124 -0
- package/src/attention/countdown.ts +24 -0
- package/src/attention/focus-input-filter.ts +131 -0
- package/src/attention/interval-recap.ts +148 -0
- package/src/attention/time-units.ts +6 -0
- package/src/attention-bell.ts +79 -0
- package/src/attributed-user-echo.ts +30 -0
- package/src/background-task-row-format.ts +99 -0
- package/src/command-interaction.ts +8 -0
- package/src/command-output-summary.ts +129 -0
- package/src/create-default-tui-cli-adapter.ts +45 -0
- package/src/execution-workspace-switcher-selection.ts +118 -0
- package/src/execution-workspace-view-model.ts +139 -0
- package/src/external-prompt-notice.tsx +38 -0
- package/src/flows/__tests__/background-focus-flow.test.ts +48 -0
- package/src/flows/__tests__/defer-submit.test.ts +88 -0
- package/src/flows/__tests__/real-cursor-flow.test.ts +139 -0
- package/src/flows/background-focus-flow.ts +44 -0
- package/src/flows/cjk-cursor-motion.ts +75 -0
- package/src/flows/cjk-text-deletion.ts +53 -0
- package/src/flows/cjk-text-input-flow.ts +248 -0
- package/src/flows/cjk-text-input-types.ts +24 -0
- package/src/flows/confirm-prompt-flow.ts +90 -0
- package/src/flows/defer-submit.ts +67 -0
- package/src/flows/fork-attach-flow.ts +62 -0
- package/src/flows/input-area-flow.ts +188 -0
- package/src/flows/permission-prompt-flow.ts +108 -0
- package/src/flows/real-cursor-flow.ts +83 -0
- package/src/flows/selection-flow.ts +205 -0
- package/src/flows/session-init-poller.ts +76 -0
- package/src/flows/text-prompt-flow.ts +93 -0
- package/src/history-search/__tests__/history-search-flow.test.ts +86 -0
- package/src/history-search/history-search-flow.ts +94 -0
- package/src/history-search/useHistorySearch.ts +336 -0
- package/src/hooks/__tests__/use-app-detach-key.test.tsx +72 -0
- package/src/hooks/__tests__/use-app-loop-escape.test.tsx +87 -0
- package/src/hooks/__tests__/use-plugin-callbacks.test.ts +18 -0
- package/src/hooks/__tests__/use-tui-channel-stability.test.tsx +100 -0
- package/src/hooks/__tests__/useAppThemeState.test.tsx +130 -0
- package/src/hooks/command-result-handler.ts +44 -0
- package/src/hooks/side-effects-types.ts +45 -0
- package/src/hooks/useAppController.ts +271 -0
- package/src/hooks/useAppInputBindings.ts +163 -0
- package/src/hooks/useAppInteractionState.ts +84 -0
- package/src/hooks/useAppLifecycleState.ts +81 -0
- package/src/hooks/useAppOverlays.ts +90 -0
- package/src/hooks/useAppScreenState.ts +81 -0
- package/src/hooks/useAppSubmissionState.ts +45 -0
- package/src/hooks/useAppThemeState.ts +157 -0
- package/src/hooks/useAppWorkspaceState.ts +149 -0
- package/src/hooks/useAppearanceSettings.ts +30 -0
- package/src/hooks/useAutocomplete.ts +101 -0
- package/src/hooks/useBackgroundPanel.ts +58 -0
- package/src/hooks/useCountdownTick.ts +24 -0
- package/src/hooks/useExecutionDetailPage.ts +69 -0
- package/src/hooks/useForkAttach.ts +60 -0
- package/src/hooks/useInputAreaHistorySearch.ts +72 -0
- package/src/hooks/useInputAreaKeys.ts +114 -0
- package/src/hooks/useNumberedSelection.ts +93 -0
- package/src/hooks/usePluginCallbacks.ts +38 -0
- package/src/hooks/usePluginScreenData.ts +85 -0
- package/src/hooks/useRealCursorPosition.ts +125 -0
- package/src/hooks/useRenderMarkdown.ts +38 -0
- package/src/hooks/useScreenReaderTurnSignals.ts +113 -0
- package/src/hooks/useSideEffects.ts +144 -0
- package/src/hooks/useStatusLineSettings.ts +50 -0
- package/src/hooks/useTerminalHandoffSuspension.ts +30 -0
- package/src/hooks/useTuiChannel.ts +141 -0
- package/src/humanize-tool-name.ts +51 -0
- package/src/index.ts +72 -0
- package/src/input-area-rules.tsx +71 -0
- package/src/json-value.ts +18 -0
- package/src/key-hint-footer.tsx +54 -0
- package/src/keybindings/__tests__/keybinding-registry.test.ts +228 -0
- package/src/keybindings/__tests__/node-keybindings-source.test.ts +118 -0
- package/src/keybindings/keybinding-catalogue.ts +171 -0
- package/src/keybindings/keybinding-registry.ts +303 -0
- package/src/keybindings/keybinding-syntax.ts +155 -0
- package/src/keybindings/keybindings-context.tsx +124 -0
- package/src/keybindings/node-keybindings-source.ts +189 -0
- package/src/numbered-list.tsx +113 -0
- package/src/plugin-tui-handlers.ts +163 -0
- package/src/product-display-name-context.tsx +30 -0
- package/src/render-markdown.ts +209 -0
- package/src/render.tsx +746 -0
- package/src/sanitize-terminal-text.ts +171 -0
- package/src/screen-reader-announcement.ts +52 -0
- package/src/screen-reader-context.tsx +32 -0
- package/src/screen-reader-labels.ts +64 -0
- package/src/screen-reader-pacing-context.tsx +47 -0
- package/src/screen-reader-pacing.ts +178 -0
- package/src/screen-reader-stdout.ts +328 -0
- package/src/short-session-id.ts +9 -0
- package/src/shutdown-signal.ts +34 -0
- package/src/slash-command-input.ts +11 -0
- package/src/status-activity.ts +70 -0
- package/src/status-glyph.ts +78 -0
- package/src/terminal-capabilities-context.tsx +27 -0
- package/src/terminal-capabilities.ts +77 -0
- package/src/terminal-focus-reporting.ts +50 -0
- package/src/terminal-handoff-controller.ts +98 -0
- package/src/terminal-marks.ts +63 -0
- package/src/theme/__tests__/built-in-themes.test.ts +117 -0
- package/src/theme/__tests__/color-vision.test.ts +82 -0
- package/src/theme/__tests__/theme-context.test.tsx +146 -0
- package/src/theme/__tests__/theme-document.test.ts +222 -0
- package/src/theme/__tests__/theme-registry.test.ts +138 -0
- package/src/theme/__tests__/theme-styles.test.ts +233 -0
- package/src/theme/built-in-themes.ts +342 -0
- package/src/theme/color-vision.ts +171 -0
- package/src/theme/index.ts +45 -0
- package/src/theme/theme-context.tsx +81 -0
- package/src/theme/theme-contracts.ts +139 -0
- package/src/theme/theme-document.ts +278 -0
- package/src/theme/theme-registry.ts +127 -0
- package/src/theme/theme-styles.ts +177 -0
- package/src/tool-summary-status.ts +59 -0
- package/src/tui-app-channel-port.ts +111 -0
- package/src/tui-channel-lifecycle-coordinator.ts +149 -0
- package/src/tui-channel-options.ts +184 -0
- package/src/tui-cli-adapter-context.tsx +13 -0
- package/src/tui-cli-adapter.ts +24 -0
- package/src/tui-interaction-queues.ts +171 -0
- package/src/tui-session-binding.ts +52 -0
- package/src/tui-session-event-projector.ts +149 -0
- package/src/tui-session-events.ts +89 -0
- package/src/tui-session-notice-store.ts +43 -0
- package/src/tui-session-options.ts +134 -0
- package/src/tui-state-manager.ts +347 -0
- package/src/types.ts +21 -0
- package/src/use-terminal-title.ts +29 -0
- package/src/utils/__tests__/edit-diff.test.ts +426 -0
- package/src/utils/__tests__/paste-detection.test.ts +116 -0
- package/src/utils/__tests__/paste-labels.test.ts +46 -0
- package/src/utils/__tests__/tool-call-extractor.test.ts +227 -0
- package/src/utils/__tests__/tool-diff-summary.test.ts +104 -0
- package/src/utils/edit-diff.ts +151 -0
- package/src/utils/input-top-border.ts +19 -0
- package/src/utils/paste-labels.ts +9 -0
- package/src/utils/tool-call-extractor.ts +92 -0
- package/src/utils/tool-diff-summary.ts +75 -0
- package/src/waiting-loop-stop-notice.ts +23 -0
- package/src/wire-history-sync.ts +159 -0
- package/src/wire-tui-channel.ts +963 -0
- package/src/wire-tui-client-commands.ts +142 -0
- package/src/wire-tui-projection.ts +136 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
countActiveBackgroundWorkspaceEntries,
|
|
5
|
+
getDefaultBackgroundWorkspaceEntries,
|
|
6
|
+
} from '../execution-workspace-view-model.js';
|
|
7
|
+
import { useBackgroundPanel } from './useBackgroundPanel.js';
|
|
8
|
+
import { resolveBackgroundFocusKey } from '../flows/background-focus-flow.js';
|
|
9
|
+
import { useKeybindingActions } from '../keybindings/keybindings-context.js';
|
|
10
|
+
|
|
11
|
+
import type { IAppBackgroundViewModel } from '../app-view-model.js';
|
|
12
|
+
import type { IHistoryEntry } from '@robota-sdk/agent-core';
|
|
13
|
+
import type {
|
|
14
|
+
IExecutionDetailPage,
|
|
15
|
+
IExecutionWorkspaceSnapshot,
|
|
16
|
+
} from '@robota-sdk/agent-interface-execution';
|
|
17
|
+
import type { IInteractiveSessionStore } from '@robota-sdk/agent-interface-session';
|
|
18
|
+
|
|
19
|
+
export interface IAppWorkspaceState {
|
|
20
|
+
readonly background: IAppBackgroundViewModel;
|
|
21
|
+
readonly activeBackgroundTaskCount: number;
|
|
22
|
+
readonly activeAgentLabel: string | undefined;
|
|
23
|
+
readonly mainThreadEntryId: string | undefined;
|
|
24
|
+
readonly isSelectedEntryInteractive: boolean;
|
|
25
|
+
readonly isBackgroundListFocused: boolean;
|
|
26
|
+
readonly openSwitcher: () => void;
|
|
27
|
+
readonly toggleSwitcher: () => void;
|
|
28
|
+
readonly focusBackgroundList: () => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface IOptions {
|
|
32
|
+
readonly snapshot: IExecutionWorkspaceSnapshot | null;
|
|
33
|
+
readonly selectedEntryId: string | undefined;
|
|
34
|
+
readonly select: (entryId: string) => void;
|
|
35
|
+
readonly read: (entryId: string) => Promise<IExecutionDetailPage>;
|
|
36
|
+
readonly sessionStore: IInteractiveSessionStore | undefined;
|
|
37
|
+
readonly onSessionSwitch: (sessionId: string) => Promise<void>;
|
|
38
|
+
readonly addEntry: (entry: IHistoryEntry) => void;
|
|
39
|
+
readonly navigationEnabled: boolean;
|
|
40
|
+
readonly switcherVisible: boolean;
|
|
41
|
+
readonly setSwitcherVisible: (visible: boolean | ((current: boolean) => boolean)) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface ISwitcherActions {
|
|
45
|
+
readonly open: () => void;
|
|
46
|
+
readonly toggle: () => void;
|
|
47
|
+
readonly close: () => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function useBackgroundListKeys(
|
|
51
|
+
focusedIndex: number | null,
|
|
52
|
+
entryIds: readonly string[],
|
|
53
|
+
setFocusedIndex: (index: number | null) => void,
|
|
54
|
+
select: (entryId: string) => void,
|
|
55
|
+
enabled: boolean,
|
|
56
|
+
): void {
|
|
57
|
+
useKeybindingActions(
|
|
58
|
+
'background-list',
|
|
59
|
+
(actions) => {
|
|
60
|
+
if (focusedIndex === null) return;
|
|
61
|
+
const action = resolveBackgroundFocusKey(focusedIndex, entryIds.length, {
|
|
62
|
+
upArrow: actions.includes('previous'),
|
|
63
|
+
downArrow: actions.includes('next'),
|
|
64
|
+
return: actions.includes('open'),
|
|
65
|
+
escape: actions.includes('close'),
|
|
66
|
+
});
|
|
67
|
+
if (action.type === 'move') setFocusedIndex(action.index);
|
|
68
|
+
if (action.type === 'open') {
|
|
69
|
+
const entryId = entryIds[action.index];
|
|
70
|
+
if (entryId) select(entryId);
|
|
71
|
+
setFocusedIndex(null);
|
|
72
|
+
}
|
|
73
|
+
if (action.type === 'exit') setFocusedIndex(null);
|
|
74
|
+
},
|
|
75
|
+
{ isActive: focusedIndex !== null && enabled },
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function useBackgroundFocus(
|
|
80
|
+
entryIds: readonly string[],
|
|
81
|
+
select: (entryId: string) => void,
|
|
82
|
+
enabled: boolean,
|
|
83
|
+
): { focusedIndex: number | null; focusList: () => void } {
|
|
84
|
+
const [focusedIndex, setFocusedIndex] = useState<number | null>(null);
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
setFocusedIndex((index) => {
|
|
87
|
+
if (index === null || entryIds.length === 0) return null;
|
|
88
|
+
return Math.min(index, entryIds.length - 1);
|
|
89
|
+
});
|
|
90
|
+
}, [entryIds.length]);
|
|
91
|
+
useBackgroundListKeys(focusedIndex, entryIds, setFocusedIndex, select, enabled);
|
|
92
|
+
const focusList = useCallback(() => {
|
|
93
|
+
if (entryIds.length > 0) setFocusedIndex(0);
|
|
94
|
+
}, [entryIds.length]);
|
|
95
|
+
return { focusedIndex, focusList };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function useSwitcherActions(setVisible: IOptions['setSwitcherVisible']): ISwitcherActions {
|
|
99
|
+
const open = useCallback(() => setVisible(true), [setVisible]);
|
|
100
|
+
const toggle = useCallback(() => setVisible((visible) => !visible), [setVisible]);
|
|
101
|
+
const close = useCallback(() => setVisible(false), [setVisible]);
|
|
102
|
+
return { open, toggle, close };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function useAppWorkspaceState(options: IOptions): IAppWorkspaceState {
|
|
106
|
+
const entries = useMemo(
|
|
107
|
+
() => getDefaultBackgroundWorkspaceEntries(options.snapshot),
|
|
108
|
+
[options.snapshot],
|
|
109
|
+
);
|
|
110
|
+
const { focusedIndex, focusList } = useBackgroundFocus(
|
|
111
|
+
entries.map((entry) => entry.id),
|
|
112
|
+
options.select,
|
|
113
|
+
options.navigationEnabled,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const panel = useBackgroundPanel({
|
|
117
|
+
selectedEntryId: options.selectedEntryId,
|
|
118
|
+
snapshot: options.snapshot,
|
|
119
|
+
read: options.read,
|
|
120
|
+
sessionStore: options.sessionStore,
|
|
121
|
+
onSessionSwitch: options.onSessionSwitch,
|
|
122
|
+
addEntry: options.addEntry,
|
|
123
|
+
});
|
|
124
|
+
const switcher = useSwitcherActions(options.setSwitcherVisible);
|
|
125
|
+
const selected = panel.entry;
|
|
126
|
+
return {
|
|
127
|
+
background: {
|
|
128
|
+
entries,
|
|
129
|
+
focusedIndex,
|
|
130
|
+
selectedEntry: selected,
|
|
131
|
+
detail: panel.detail,
|
|
132
|
+
switcherVisible: options.switcherVisible,
|
|
133
|
+
snapshot: options.snapshot,
|
|
134
|
+
selectedEntryId: options.selectedEntryId,
|
|
135
|
+
select: options.select,
|
|
136
|
+
closeSwitcher: switcher.close,
|
|
137
|
+
attachToFork: panel.attachToFork,
|
|
138
|
+
},
|
|
139
|
+
activeBackgroundTaskCount: countActiveBackgroundWorkspaceEntries(options.snapshot),
|
|
140
|
+
activeAgentLabel: selected?.kind === 'main_thread' ? undefined : selected?.title,
|
|
141
|
+
mainThreadEntryId: options.snapshot?.entries.find((entry) => entry.kind === 'main_thread')?.id,
|
|
142
|
+
isSelectedEntryInteractive:
|
|
143
|
+
!selected || selected.kind === 'main_thread' || selected.controls.includes('send'),
|
|
144
|
+
isBackgroundListFocused: focusedIndex !== null,
|
|
145
|
+
openSwitcher: switcher.open,
|
|
146
|
+
toggleSwitcher: switcher.toggle,
|
|
147
|
+
focusBackgroundList: focusList,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readAppearanceSettings } from '@robota-sdk/agent-framework';
|
|
2
|
+
import { useCallback, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
import { useTuiCliAdapter } from '../tui-cli-adapter-context.js';
|
|
5
|
+
|
|
6
|
+
import type { ITuiCliAdapter } from '../tui-cli-adapter.js';
|
|
7
|
+
import type { IAppearanceSettings } from '@robota-sdk/agent-interface-command';
|
|
8
|
+
|
|
9
|
+
function readFromAdapter(cliAdapter: ITuiCliAdapter): IAppearanceSettings {
|
|
10
|
+
return readAppearanceSettings(cliAdapter.readSettings(cliAdapter.getUserSettingsPath()));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* SCREEN-2002 — the persisted appearance, plus a from-disk `refresh()`.
|
|
15
|
+
*
|
|
16
|
+
* Refresh-on-result, the shape `useStatusLineSettings` already uses: the HOST applies
|
|
17
|
+
* `appearance-settings-patch` through the settings adapter, so the renderer learns about a change by
|
|
18
|
+
* RE-READING the document when a command result arrives. The TUI never writes these keys itself,
|
|
19
|
+
* which is what keeps one writer for one file.
|
|
20
|
+
*/
|
|
21
|
+
export function useAppearanceSettings(): [IAppearanceSettings, () => void] {
|
|
22
|
+
const cliAdapter = useTuiCliAdapter();
|
|
23
|
+
const [appearance, setAppearance] = useState<IAppearanceSettings>(() =>
|
|
24
|
+
readFromAdapter(cliAdapter),
|
|
25
|
+
);
|
|
26
|
+
const refresh = useCallback((): void => {
|
|
27
|
+
setAppearance(readFromAdapter(cliAdapter));
|
|
28
|
+
}, [cliAdapter]);
|
|
29
|
+
return [appearance, refresh];
|
|
30
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slash command autocomplete hook.
|
|
3
|
+
* Extracted from InputArea.tsx for single-responsibility.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useState, useMemo, useEffect } from 'react';
|
|
7
|
+
|
|
8
|
+
import type { ITuiCommandQueryPort } from '../tui-app-channel-port.js';
|
|
9
|
+
import type { ICommand } from '@robota-sdk/agent-interface-command';
|
|
10
|
+
|
|
11
|
+
/** Parse input to determine autocomplete state */
|
|
12
|
+
function parseSlashInput(value: string): {
|
|
13
|
+
isSlash: boolean;
|
|
14
|
+
parentCommand: string;
|
|
15
|
+
filter: string;
|
|
16
|
+
} {
|
|
17
|
+
if (!value.startsWith('/')) return { isSlash: false, parentCommand: '', filter: '' };
|
|
18
|
+
const afterSlash = value.slice(1);
|
|
19
|
+
const spaceIndex = afterSlash.indexOf(' ');
|
|
20
|
+
if (spaceIndex === -1) return { isSlash: true, parentCommand: '', filter: afterSlash };
|
|
21
|
+
const parent = afterSlash.slice(0, spaceIndex);
|
|
22
|
+
const rest = afterSlash.slice(spaceIndex + 1);
|
|
23
|
+
return { isSlash: true, parentCommand: parent, filter: rest };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Hook: manage autocomplete state */
|
|
27
|
+
export function useAutocomplete(
|
|
28
|
+
value: string,
|
|
29
|
+
commandQueryPort: ITuiCommandQueryPort | undefined,
|
|
30
|
+
): {
|
|
31
|
+
showPopup: boolean;
|
|
32
|
+
filteredCommands: ICommand[];
|
|
33
|
+
selectedIndex: number;
|
|
34
|
+
setSelectedIndex: React.Dispatch<React.SetStateAction<number>>;
|
|
35
|
+
isSubcommandMode: boolean;
|
|
36
|
+
setShowPopup: React.Dispatch<React.SetStateAction<boolean>>;
|
|
37
|
+
} {
|
|
38
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
39
|
+
const [dismissed, setDismissed] = useState(false);
|
|
40
|
+
|
|
41
|
+
// Reset dismissed when input changes
|
|
42
|
+
const prevValueRef = React.useRef(value);
|
|
43
|
+
if (prevValueRef.current !== value) {
|
|
44
|
+
prevValueRef.current = value;
|
|
45
|
+
if (dismissed) setDismissed(false);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const parsed = parseSlashInput(value);
|
|
49
|
+
const isSubcommandMode = parsed.isSlash && parsed.parentCommand.length > 0;
|
|
50
|
+
|
|
51
|
+
const previousParentCommandRef = React.useRef(parsed.parentCommand);
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (previousParentCommandRef.current === parsed.parentCommand) return;
|
|
54
|
+
previousParentCommandRef.current = parsed.parentCommand;
|
|
55
|
+
setSelectedIndex(0);
|
|
56
|
+
}, [parsed.parentCommand]);
|
|
57
|
+
|
|
58
|
+
const filteredCommands = useMemo(() => {
|
|
59
|
+
if (!commandQueryPort || !parsed.isSlash || dismissed) return [];
|
|
60
|
+
if (isSubcommandMode) {
|
|
61
|
+
const subs = commandQueryPort.getSubcommands(parsed.parentCommand);
|
|
62
|
+
if (subs.length === 0) return [];
|
|
63
|
+
if (!parsed.filter) return subs;
|
|
64
|
+
const lower = parsed.filter.toLowerCase();
|
|
65
|
+
return subs.filter((c) => c.name.toLowerCase().startsWith(lower));
|
|
66
|
+
}
|
|
67
|
+
return commandQueryPort.getCommands(parsed.filter);
|
|
68
|
+
}, [
|
|
69
|
+
commandQueryPort,
|
|
70
|
+
parsed.isSlash,
|
|
71
|
+
parsed.parentCommand,
|
|
72
|
+
parsed.filter,
|
|
73
|
+
dismissed,
|
|
74
|
+
isSubcommandMode,
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
const showPopup = parsed.isSlash && filteredCommands.length > 0 && !dismissed;
|
|
78
|
+
|
|
79
|
+
// Clamp selectedIndex
|
|
80
|
+
if (selectedIndex >= filteredCommands.length && filteredCommands.length > 0) {
|
|
81
|
+
setSelectedIndex(filteredCommands.length - 1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
showPopup,
|
|
86
|
+
filteredCommands,
|
|
87
|
+
selectedIndex,
|
|
88
|
+
setSelectedIndex,
|
|
89
|
+
isSubcommandMode,
|
|
90
|
+
setShowPopup: (val) => {
|
|
91
|
+
if (typeof val === 'function') {
|
|
92
|
+
setDismissed((prev) => {
|
|
93
|
+
const nextVal = (val as (prev: boolean) => boolean)(!prev);
|
|
94
|
+
return !nextVal;
|
|
95
|
+
});
|
|
96
|
+
} else {
|
|
97
|
+
setDismissed(!val);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The background panel's two data concerns as one call: what the selected entry's detail page says,
|
|
3
|
+
* and what happens when the operator attaches to a forked one.
|
|
4
|
+
*
|
|
5
|
+
* Both belong to the same surface and both are read by the same component, so `App` asks for them
|
|
6
|
+
* once rather than wiring two hooks whose inputs it has to keep in step. Split out because CLI-2004
|
|
7
|
+
* had already brought `App.tsx` to its size budget by extracting the detail read, and CLI-1994 adds
|
|
8
|
+
* the attach handler beside it — growing the file was the alternative the size rule forbids.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useMemo } from 'react';
|
|
12
|
+
|
|
13
|
+
import { useExecutionDetailPage } from './useExecutionDetailPage.js';
|
|
14
|
+
import { useForkAttach } from './useForkAttach.js';
|
|
15
|
+
|
|
16
|
+
import type { IExecutionDetailState } from './useExecutionDetailPage.js';
|
|
17
|
+
import type { IHistoryEntry } from '@robota-sdk/agent-core';
|
|
18
|
+
import type {
|
|
19
|
+
IExecutionDetailPage,
|
|
20
|
+
IExecutionWorkspaceEntry,
|
|
21
|
+
IExecutionWorkspaceSnapshot,
|
|
22
|
+
} from '@robota-sdk/agent-interface-execution';
|
|
23
|
+
import type { IInteractiveSessionStore } from '@robota-sdk/agent-interface-session';
|
|
24
|
+
|
|
25
|
+
export interface IUseBackgroundPanelOptions {
|
|
26
|
+
/** Which entry the switcher has selected; resolved against the snapshot below. */
|
|
27
|
+
readonly selectedEntryId: string | undefined;
|
|
28
|
+
/** The panel's data, and the re-read trigger — the detail follows a running task's output. */
|
|
29
|
+
readonly snapshot: IExecutionWorkspaceSnapshot | null;
|
|
30
|
+
readonly read: (entryId: string) => Promise<IExecutionDetailPage>;
|
|
31
|
+
/** Absent ⇒ this surface cannot open another session's record, and attach says so. */
|
|
32
|
+
readonly sessionStore: IInteractiveSessionStore | undefined;
|
|
33
|
+
readonly onSessionSwitch: (sessionId: string) => void;
|
|
34
|
+
readonly addEntry: (entry: IHistoryEntry) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface IBackgroundPanel {
|
|
38
|
+
/** The selected entry itself, which the switcher and the detail view both render. */
|
|
39
|
+
readonly entry: IExecutionWorkspaceEntry | undefined;
|
|
40
|
+
readonly detail: IExecutionDetailState;
|
|
41
|
+
readonly attachToFork: (entry: IExecutionWorkspaceEntry) => void;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Wire the background panel's detail read and its attach handler together. */
|
|
45
|
+
export function useBackgroundPanel(inputs: IUseBackgroundPanelOptions): IBackgroundPanel {
|
|
46
|
+
const { snapshot, selectedEntryId } = inputs;
|
|
47
|
+
const entry = useMemo(
|
|
48
|
+
() => snapshot?.entries.find((candidate) => candidate.id === selectedEntryId),
|
|
49
|
+
[snapshot, selectedEntryId],
|
|
50
|
+
);
|
|
51
|
+
const detail = useExecutionDetailPage({ entry, snapshot, read: inputs.read });
|
|
52
|
+
const attachToFork = useForkAttach({
|
|
53
|
+
sessionStore: inputs.sessionStore,
|
|
54
|
+
onSessionSwitch: inputs.onSessionSwitch,
|
|
55
|
+
addEntry: inputs.addEntry,
|
|
56
|
+
});
|
|
57
|
+
return { entry, detail, attachToFork };
|
|
58
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { MS_PER_SECOND } from '../attention/time-units.js';
|
|
4
|
+
|
|
5
|
+
/** SCREEN-1992: one second, the resolution a `in 59s` countdown is read at. */
|
|
6
|
+
const COUNTDOWN_TICK_MS = MS_PER_SECOND;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The clock a sleeping schedule's countdown is rendered against. Re-renders once a second only while
|
|
10
|
+
* `active` (an entry carries `nextFireAt`) and not in screen-reader mode — a reader would otherwise
|
|
11
|
+
* hear the row change every second. When not ticking the clock is still the CURRENT one on every
|
|
12
|
+
* render (never the mount-time seed): a reader gets a correct number, read once.
|
|
13
|
+
*/
|
|
14
|
+
export function useCountdownTick(active: boolean, screenReader: boolean): Date {
|
|
15
|
+
const ticking = active && !screenReader;
|
|
16
|
+
const [tickedAt, setTickedAt] = useState(() => new Date());
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!ticking) return undefined;
|
|
19
|
+
setTickedAt(new Date());
|
|
20
|
+
const timer = setInterval(() => setTickedAt(new Date()), COUNTDOWN_TICK_MS);
|
|
21
|
+
return () => clearInterval(timer);
|
|
22
|
+
}, [ticking]);
|
|
23
|
+
return ticking ? tickedAt : new Date();
|
|
24
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loads the inline detail page for the selected background-execution entry.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `App.tsx` (CLI-2004): three pieces of state and one async effect that exist only
|
|
5
|
+
* for the detail pane, sitting in the middle of the component's input handlers. One responsibility,
|
|
6
|
+
* one module — and the App keeps room for the concerns that actually belong to it.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { useEffect, useState } from 'react';
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
IExecutionDetailPage,
|
|
13
|
+
IExecutionWorkspaceEntry,
|
|
14
|
+
IExecutionWorkspaceSnapshot,
|
|
15
|
+
} from '@robota-sdk/agent-interface-execution';
|
|
16
|
+
|
|
17
|
+
export interface IExecutionDetailState {
|
|
18
|
+
page: IExecutionDetailPage | null;
|
|
19
|
+
error: string | undefined;
|
|
20
|
+
loading: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface IUseExecutionDetailPageInputs {
|
|
24
|
+
/** The selected entry, or undefined / the main thread when no detail is shown. */
|
|
25
|
+
entry: IExecutionWorkspaceEntry | undefined;
|
|
26
|
+
/** Re-read trigger: the snapshot the entry came from. */
|
|
27
|
+
snapshot: IExecutionWorkspaceSnapshot | null;
|
|
28
|
+
read: (entryId: string) => Promise<IExecutionDetailPage>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Load (and cancel-on-change) the detail page for the selected entry. */
|
|
32
|
+
export function useExecutionDetailPage(
|
|
33
|
+
inputs: IUseExecutionDetailPageInputs,
|
|
34
|
+
): IExecutionDetailState {
|
|
35
|
+
const [page, setPage] = useState<IExecutionDetailPage | null>(null);
|
|
36
|
+
const [error, setError] = useState<string | undefined>();
|
|
37
|
+
const [loading, setLoading] = useState(false);
|
|
38
|
+
const { entry, snapshot, read } = inputs;
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!entry || entry.kind === 'main_thread') {
|
|
42
|
+
setPage(null);
|
|
43
|
+
setError(undefined);
|
|
44
|
+
setLoading(false);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let isCurrent = true;
|
|
49
|
+
setLoading(true);
|
|
50
|
+
setError(undefined);
|
|
51
|
+
read(entry.id)
|
|
52
|
+
.then((loaded) => {
|
|
53
|
+
if (!isCurrent) return;
|
|
54
|
+
setPage(loaded);
|
|
55
|
+
setLoading(false);
|
|
56
|
+
})
|
|
57
|
+
.catch((cause: Error) => {
|
|
58
|
+
if (!isCurrent) return;
|
|
59
|
+
setError(cause.message);
|
|
60
|
+
setLoading(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return () => {
|
|
64
|
+
isCurrent = false;
|
|
65
|
+
};
|
|
66
|
+
}, [snapshot, read, entry]);
|
|
67
|
+
|
|
68
|
+
return { page, error, loading };
|
|
69
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI-1994: the App's `attach` handler, as a hook rather than another closure inside `App.tsx`.
|
|
3
|
+
*
|
|
4
|
+
* `App.tsx` is a file the size floor has already frozen as debt, where the rule is "split instead of
|
|
5
|
+
* extending" — so the handler lives beside the other App hooks. The decision itself is neither here
|
|
6
|
+
* nor there: `attachToForkedSession` asks the framework, which owns when `attach` is offered and
|
|
7
|
+
* when it is honoured.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createSystemMessage, messageToHistoryEntry } from '@robota-sdk/agent-core';
|
|
11
|
+
import { useCallback } from 'react';
|
|
12
|
+
|
|
13
|
+
import { attachToForkedSession } from '../flows/fork-attach-flow.js';
|
|
14
|
+
import { useTuiCliAdapter } from '../tui-cli-adapter-context.js';
|
|
15
|
+
|
|
16
|
+
import type { IHistoryEntry } from '@robota-sdk/agent-core';
|
|
17
|
+
import type { IExecutionWorkspaceEntry } from '@robota-sdk/agent-interface-execution';
|
|
18
|
+
import type { IInteractiveSessionStore } from '@robota-sdk/agent-interface-session';
|
|
19
|
+
|
|
20
|
+
export interface IUseForkAttachOptions {
|
|
21
|
+
/** Absent when the surface persists nothing; attach then refuses, saying so. */
|
|
22
|
+
readonly sessionStore: IInteractiveSessionStore | undefined;
|
|
23
|
+
/** The surface's session-switch path — the same one the session picker takes. */
|
|
24
|
+
readonly onSessionSwitch: (sessionId: string) => void;
|
|
25
|
+
/** Where a refusal is written, so it reaches the operator instead of disappearing. */
|
|
26
|
+
readonly addEntry: (entry: IHistoryEntry) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Attach to a forked conversation: point this terminal at the fork's own session record. A VIEW
|
|
31
|
+
* switch, never a merge — the parent's record is not read, written or joined either way.
|
|
32
|
+
*/
|
|
33
|
+
export function useForkAttach({
|
|
34
|
+
sessionStore,
|
|
35
|
+
onSessionSwitch,
|
|
36
|
+
addEntry,
|
|
37
|
+
}: IUseForkAttachOptions): (entry: IExecutionWorkspaceEntry) => void {
|
|
38
|
+
const cliAdapter = useTuiCliAdapter();
|
|
39
|
+
return useCallback(
|
|
40
|
+
(entry: IExecutionWorkspaceEntry): void => {
|
|
41
|
+
const notify = (message: string): void =>
|
|
42
|
+
addEntry(messageToHistoryEntry(createSystemMessage(message)));
|
|
43
|
+
// A surface composed without a session store cannot open another session's record. Said
|
|
44
|
+
// plainly rather than reported as "the record is missing", which is a different fact.
|
|
45
|
+
if (sessionStore === undefined) {
|
|
46
|
+
notify('This surface has no session store, so it cannot attach to a forked session.');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
attachToForkedSession(entry, {
|
|
50
|
+
hasSessionRecord: (sessionId) => sessionStore.load(sessionId).status === 'valid',
|
|
51
|
+
switchSession: onSessionSwitch,
|
|
52
|
+
notify,
|
|
53
|
+
...(cliAdapter.formatResumeCommand === undefined
|
|
54
|
+
? {}
|
|
55
|
+
: { formatResumeCommand: cliAdapter.formatResumeCommand }),
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
[addEntry, cliAdapter, onSessionSwitch, sessionStore],
|
|
59
|
+
);
|
|
60
|
+
}
|