@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,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SCREEN-2002 — the theme picker.
|
|
3
|
+
*
|
|
4
|
+
* Preview-on-move: the highlighted row IS the live region's theme while the picker is open, because
|
|
5
|
+
* a colour scheme cannot be judged from a swatch beside the thing it does not apply to. Escape
|
|
6
|
+
* restores what was persisted; selecting submits `/theme <id>` so the HOST writes the settings
|
|
7
|
+
* document and the picker never becomes a second writer.
|
|
8
|
+
*
|
|
9
|
+
* The transcript above the picker keeps the theme it was written in — Ink's `<Static>` emits an
|
|
10
|
+
* entry once — so a preview moves the input frame, the status bar and this overlay. That is the
|
|
11
|
+
* region a reader is looking at while choosing, and it is the property that makes a preview honest
|
|
12
|
+
* rather than partial.
|
|
13
|
+
*/
|
|
14
|
+
import { Box } from 'ink';
|
|
15
|
+
import React from 'react';
|
|
16
|
+
|
|
17
|
+
import { useNumberedSelection } from './hooks/useNumberedSelection.js';
|
|
18
|
+
import { KeyHintFooter, SELECTION_INDICATOR, SELECTION_INDICATOR_NONE } from './key-hint-footer.js';
|
|
19
|
+
import { useKeybindingActions, useKeybindingHints } from './keybindings/keybindings-context.js';
|
|
20
|
+
import { formatNumberedSelectionPrompt, numberedRowPrefix } from './numbered-list.js';
|
|
21
|
+
import { Text } from './SafeText.js';
|
|
22
|
+
import { useScreenReader } from './screen-reader-context.js';
|
|
23
|
+
import { isInteractiveColorTerminal } from './terminal-capabilities.js';
|
|
24
|
+
import { usePalette } from './theme/index.js';
|
|
25
|
+
|
|
26
|
+
import type { IAppThemePickerViewModel, IThemeToggles } from './hooks/useAppThemeState.js';
|
|
27
|
+
import type { ITuiTheme } from './theme/index.js';
|
|
28
|
+
import type { IThemeSkip } from './theme/theme-registry.js';
|
|
29
|
+
|
|
30
|
+
function describeTheme(theme: ITuiTheme, isActive: boolean): string {
|
|
31
|
+
const active = isActive ? ' (current)' : '';
|
|
32
|
+
return `${theme.name} — ${theme.appearance}, ${theme.source}${active}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function ThemeRow({
|
|
36
|
+
theme,
|
|
37
|
+
isFocused,
|
|
38
|
+
isActive,
|
|
39
|
+
rowNumber,
|
|
40
|
+
}: {
|
|
41
|
+
theme: ITuiTheme;
|
|
42
|
+
isFocused: boolean;
|
|
43
|
+
isActive: boolean;
|
|
44
|
+
rowNumber?: number | undefined;
|
|
45
|
+
}): React.ReactElement {
|
|
46
|
+
const palette = usePalette();
|
|
47
|
+
const marker =
|
|
48
|
+
rowNumber !== undefined
|
|
49
|
+
? numberedRowPrefix(rowNumber)
|
|
50
|
+
: isFocused
|
|
51
|
+
? SELECTION_INDICATOR
|
|
52
|
+
: SELECTION_INDICATOR_NONE;
|
|
53
|
+
return (
|
|
54
|
+
<Text
|
|
55
|
+
{...(isFocused && rowNumber === undefined ? { color: palette.text.accent, bold: true } : {})}
|
|
56
|
+
>
|
|
57
|
+
{marker}
|
|
58
|
+
{describeTheme(theme, isActive)}
|
|
59
|
+
</Text>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The typed-selection prompt, re-printed verbatim after an out-of-range answer. */
|
|
64
|
+
function ScreenReaderThemePrompt({
|
|
65
|
+
itemCount,
|
|
66
|
+
buffer,
|
|
67
|
+
invalid,
|
|
68
|
+
}: {
|
|
69
|
+
itemCount: number;
|
|
70
|
+
buffer: string;
|
|
71
|
+
invalid: boolean;
|
|
72
|
+
}): React.ReactElement {
|
|
73
|
+
const prompt = formatNumberedSelectionPrompt(itemCount, true);
|
|
74
|
+
return (
|
|
75
|
+
<Box flexDirection="column">
|
|
76
|
+
<Text>
|
|
77
|
+
{prompt}
|
|
78
|
+
{buffer.length > 0 ? ` ${buffer}` : ''}
|
|
79
|
+
</Text>
|
|
80
|
+
{invalid && <Text>{prompt}</Text>}
|
|
81
|
+
</Box>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function focusedIndexOf(themes: readonly ITuiTheme[], activeThemeId: string): number {
|
|
86
|
+
const index = themes.findIndex((theme) => theme.id === activeThemeId);
|
|
87
|
+
return index < 0 ? 0 : index;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Navigation PREVIEWS and only `select` applies, so the highlight and the live region move together
|
|
92
|
+
* without anything being written until the user says so.
|
|
93
|
+
*/
|
|
94
|
+
/**
|
|
95
|
+
* Pending, not applied: the toggles travel with the selection and are submitted WITH it, so escape
|
|
96
|
+
* abandons them exactly as it abandons the previewed theme. Applying them live would make escape
|
|
97
|
+
* mean two different things for two controls in the same overlay.
|
|
98
|
+
*/
|
|
99
|
+
function usePendingToggles(picker: IAppThemePickerViewModel): {
|
|
100
|
+
toggles: IThemeToggles;
|
|
101
|
+
setToggles: React.Dispatch<React.SetStateAction<IThemeToggles>>;
|
|
102
|
+
} {
|
|
103
|
+
const [toggles, setToggles] = React.useState<IThemeToggles>(() => ({
|
|
104
|
+
syntaxHighlighting: picker.syntaxHighlighting,
|
|
105
|
+
reducedMotion: picker.reducedMotion,
|
|
106
|
+
}));
|
|
107
|
+
return { toggles, setToggles };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* The highlight, and the preview that follows it. Moving WRAPS at both ends rather than stopping —
|
|
112
|
+
* four themes is a short list, and a dead end at the bottom reads as a broken key.
|
|
113
|
+
*/
|
|
114
|
+
function useFocusedRow(picker: IAppThemePickerViewModel): {
|
|
115
|
+
focusedIndex: number;
|
|
116
|
+
move: (delta: number) => void;
|
|
117
|
+
} {
|
|
118
|
+
const { themes, activeThemeId, preview } = picker;
|
|
119
|
+
const [focusedIndex, setFocusedIndex] = React.useState(() =>
|
|
120
|
+
focusedIndexOf(themes, activeThemeId),
|
|
121
|
+
);
|
|
122
|
+
const move = React.useCallback(
|
|
123
|
+
(delta: number): void => {
|
|
124
|
+
if (themes.length === 0) return;
|
|
125
|
+
setFocusedIndex((current) => {
|
|
126
|
+
const next = (current + delta + themes.length) % themes.length;
|
|
127
|
+
const theme = themes[next];
|
|
128
|
+
if (theme) preview(theme.id);
|
|
129
|
+
return next;
|
|
130
|
+
});
|
|
131
|
+
},
|
|
132
|
+
[preview, themes],
|
|
133
|
+
);
|
|
134
|
+
return { focusedIndex, move };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function useThemePickerSelection(
|
|
138
|
+
picker: IAppThemePickerViewModel,
|
|
139
|
+
screenReader: boolean,
|
|
140
|
+
): {
|
|
141
|
+
focusedIndex: number;
|
|
142
|
+
toggles: IThemeToggles;
|
|
143
|
+
numbered: ReturnType<typeof useNumberedSelection>;
|
|
144
|
+
} {
|
|
145
|
+
const { themes, select, cancel } = picker;
|
|
146
|
+
const { focusedIndex, move } = useFocusedRow(picker);
|
|
147
|
+
const { toggles, setToggles } = usePendingToggles(picker);
|
|
148
|
+
const togglesRef = React.useRef(toggles);
|
|
149
|
+
togglesRef.current = toggles;
|
|
150
|
+
const applyAt = React.useCallback(
|
|
151
|
+
(index: number): void => {
|
|
152
|
+
const theme = themes[index];
|
|
153
|
+
if (theme) select(theme.id, togglesRef.current);
|
|
154
|
+
},
|
|
155
|
+
[select, themes],
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// CLI-2004: an arrow-key menu gets numbers and a typed answer in the mode. It applies on SELECT
|
|
159
|
+
// rather than on navigation — there is no highlight to hear, so a preview per keystroke would
|
|
160
|
+
// just be repaints a reader has to sit through.
|
|
161
|
+
const numbered = useNumberedSelection({
|
|
162
|
+
enabled: screenReader && themes.length > 0,
|
|
163
|
+
itemCount: themes.length,
|
|
164
|
+
cancellable: true,
|
|
165
|
+
onSelect: applyAt,
|
|
166
|
+
onCancel: cancel,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const toggle = React.useCallback(
|
|
170
|
+
(key: keyof IThemeToggles): void => {
|
|
171
|
+
setToggles((current) => ({ ...current, [key]: !current[key] }));
|
|
172
|
+
},
|
|
173
|
+
[setToggles],
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
useKeybindingActions(
|
|
177
|
+
'theme-picker',
|
|
178
|
+
(actions) => {
|
|
179
|
+
for (const action of actions) {
|
|
180
|
+
if (action === 'previous') move(-1);
|
|
181
|
+
else if (action === 'next') move(1);
|
|
182
|
+
else if (action === 'select') applyAt(focusedIndex);
|
|
183
|
+
else if (action === 'toggle-syntax') toggle('syntaxHighlighting');
|
|
184
|
+
else if (action === 'toggle-motion') toggle('reducedMotion');
|
|
185
|
+
else if (action === 'cancel') cancel();
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
{ isActive: !screenReader },
|
|
189
|
+
);
|
|
190
|
+
return { focusedIndex, toggles, numbered };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function ThemeRows({
|
|
194
|
+
themes,
|
|
195
|
+
activeThemeId,
|
|
196
|
+
focusedIndex,
|
|
197
|
+
screenReader,
|
|
198
|
+
}: {
|
|
199
|
+
themes: readonly ITuiTheme[];
|
|
200
|
+
activeThemeId: string;
|
|
201
|
+
focusedIndex: number;
|
|
202
|
+
screenReader: boolean;
|
|
203
|
+
}): React.ReactElement {
|
|
204
|
+
if (themes.length === 0) return <Text dimColor>No themes installed</Text>;
|
|
205
|
+
return (
|
|
206
|
+
<>
|
|
207
|
+
{themes.map((theme, index) => (
|
|
208
|
+
<ThemeRow
|
|
209
|
+
key={theme.id}
|
|
210
|
+
theme={theme}
|
|
211
|
+
isFocused={index === focusedIndex}
|
|
212
|
+
isActive={theme.id === activeThemeId}
|
|
213
|
+
{...(screenReader ? { rowNumber: index } : {})}
|
|
214
|
+
/>
|
|
215
|
+
))}
|
|
216
|
+
</>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Under the colour gate a preview shows nothing, so the picker says so in words rather than letting
|
|
222
|
+
* the user conclude the highlight is broken. The gate is the degradation rule this package has
|
|
223
|
+
* always had; this is the one place that has to ADMIT it.
|
|
224
|
+
*/
|
|
225
|
+
function ColourGateNotice(): React.ReactElement {
|
|
226
|
+
if (isInteractiveColorTerminal()) return <></>;
|
|
227
|
+
return (
|
|
228
|
+
<Text dimColor>Colour is off for this terminal — themes apply, previews cannot show.</Text>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* The theme files this run found and refused. They are shown, never offered: a row a user cannot
|
|
234
|
+
* pick is still the only place that says why the theme they wrote is not in the list — without it
|
|
235
|
+
* the file just silently is not there, and the startup line has long since scrolled away.
|
|
236
|
+
*/
|
|
237
|
+
function SkippedRows({ skipped }: { skipped: readonly IThemeSkip[] }): React.ReactElement {
|
|
238
|
+
if (skipped.length === 0) return <></>;
|
|
239
|
+
return (
|
|
240
|
+
<Box flexDirection="column" marginTop={1}>
|
|
241
|
+
{/*
|
|
242
|
+
Keyed by POSITION, not by id. `IThemeSkip.id` is a LABEL and its contract says so: a
|
|
243
|
+
scope-level failure carries one id for a whole scope, and two scopes can both fail, so two
|
|
244
|
+
rows can share it. A duplicate React key would put a warning on stderr from inside the live
|
|
245
|
+
frame, corrupting the frame it is complaining about.
|
|
246
|
+
|
|
247
|
+
Recorded limit: no test pins this. Rendering two same-id rows under ink-testing-library
|
|
248
|
+
produced both rows and no warning on `console.error`, so a test asserting the warning's
|
|
249
|
+
ABSENCE is green either way — an unfalsifiable guard, which is worse than none. The reason
|
|
250
|
+
is written here instead of measured.
|
|
251
|
+
*/}
|
|
252
|
+
{skipped.map((skip, index) => (
|
|
253
|
+
<Text key={index} dimColor>
|
|
254
|
+
{`${SELECTION_INDICATOR_NONE}Skipped "${skip.fileName}" — ${skip.reason}`}
|
|
255
|
+
</Text>
|
|
256
|
+
))}
|
|
257
|
+
</Box>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function TogglesRow({
|
|
262
|
+
toggles,
|
|
263
|
+
reducedMotionPin,
|
|
264
|
+
screenReader,
|
|
265
|
+
}: {
|
|
266
|
+
toggles: IThemeToggles;
|
|
267
|
+
reducedMotionPin?: IAppThemePickerViewModel['reducedMotionPin'];
|
|
268
|
+
screenReader: boolean;
|
|
269
|
+
}): React.ReactElement {
|
|
270
|
+
const on = (value: boolean): string => (value ? 'on' : 'off');
|
|
271
|
+
// The pin is admitted here as it is by `/theme list` and `/theme motion` — and it names what THIS
|
|
272
|
+
// RUN does, not only who decided it. The tier alone is not enough: `--no-reduced-motion` is an
|
|
273
|
+
// override too and pins the opposite value, so a row carrying the chosen value beside the tier
|
|
274
|
+
// can read `motion on (pinned by flag)` on a visibly still run.
|
|
275
|
+
const pinned =
|
|
276
|
+
reducedMotionPin === undefined
|
|
277
|
+
? ''
|
|
278
|
+
: ` (this run: motion ${on(!reducedMotionPin.reducedMotion)}, pinned by ${reducedMotionPin.tier})`;
|
|
279
|
+
return (
|
|
280
|
+
<Box flexDirection="column">
|
|
281
|
+
<Text dimColor>
|
|
282
|
+
{`syntax ${on(toggles.syntaxHighlighting)} · motion ${on(!toggles.reducedMotion)}${pinned}`}
|
|
283
|
+
</Text>
|
|
284
|
+
{/*
|
|
285
|
+
The mode takes a typed answer, so `s` and `m` are not bound in it — but the state above is
|
|
286
|
+
still worth reading before choosing. So the state stays and the AFFORDANCE changes: this
|
|
287
|
+
names a route the mode can actually take, instead of leaving two keys silently inert.
|
|
288
|
+
*/}
|
|
289
|
+
{screenReader && (
|
|
290
|
+
<Text dimColor>Change with /theme syntax on|off or /theme motion on|off</Text>
|
|
291
|
+
)}
|
|
292
|
+
</Box>
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function ThemePickerFooter({
|
|
297
|
+
screenReader,
|
|
298
|
+
itemCount,
|
|
299
|
+
numbered,
|
|
300
|
+
}: {
|
|
301
|
+
screenReader: boolean;
|
|
302
|
+
itemCount: number;
|
|
303
|
+
numbered: ReturnType<typeof useNumberedSelection>;
|
|
304
|
+
}): React.ReactElement {
|
|
305
|
+
// Footer order: navigate → primary → dismiss, the same as every other picker in this package.
|
|
306
|
+
const footerHints = useKeybindingHints('theme-picker', [
|
|
307
|
+
[['previous', 'next'], 'Preview'],
|
|
308
|
+
['select', 'Apply'],
|
|
309
|
+
['toggle-syntax', 'Syntax'],
|
|
310
|
+
['toggle-motion', 'Motion'],
|
|
311
|
+
['cancel', 'Cancel'],
|
|
312
|
+
]);
|
|
313
|
+
if (!screenReader) return <KeyHintFooter hints={footerHints} />;
|
|
314
|
+
return (
|
|
315
|
+
<ScreenReaderThemePrompt
|
|
316
|
+
itemCount={itemCount}
|
|
317
|
+
buffer={numbered.buffer}
|
|
318
|
+
invalid={numbered.invalid}
|
|
319
|
+
/>
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export default function ThemePicker({
|
|
324
|
+
picker,
|
|
325
|
+
}: {
|
|
326
|
+
picker: IAppThemePickerViewModel;
|
|
327
|
+
}): React.ReactElement {
|
|
328
|
+
const palette = usePalette();
|
|
329
|
+
const screenReader = useScreenReader();
|
|
330
|
+
const { themes, activeThemeId } = picker;
|
|
331
|
+
const { focusedIndex, toggles, numbered } = useThemePickerSelection(picker, screenReader);
|
|
332
|
+
|
|
333
|
+
return (
|
|
334
|
+
<Box
|
|
335
|
+
flexDirection="column"
|
|
336
|
+
{...(screenReader
|
|
337
|
+
? {}
|
|
338
|
+
: { borderStyle: 'round' as const, borderColor: palette.border.focused })}
|
|
339
|
+
paddingX={1}
|
|
340
|
+
>
|
|
341
|
+
<Text color={palette.text.accent} bold>
|
|
342
|
+
Theme
|
|
343
|
+
</Text>
|
|
344
|
+
<ColourGateNotice />
|
|
345
|
+
<TogglesRow
|
|
346
|
+
toggles={toggles}
|
|
347
|
+
reducedMotionPin={picker.reducedMotionPin}
|
|
348
|
+
screenReader={screenReader}
|
|
349
|
+
/>
|
|
350
|
+
<Box flexDirection="column" marginTop={1}>
|
|
351
|
+
<ThemeRows
|
|
352
|
+
themes={themes}
|
|
353
|
+
activeThemeId={activeThemeId}
|
|
354
|
+
focusedIndex={focusedIndex}
|
|
355
|
+
screenReader={screenReader}
|
|
356
|
+
/>
|
|
357
|
+
</Box>
|
|
358
|
+
<SkippedRows skipped={picker.skipped} />
|
|
359
|
+
<ThemePickerFooter
|
|
360
|
+
screenReader={screenReader}
|
|
361
|
+
itemCount={themes.length}
|
|
362
|
+
numbered={numbered}
|
|
363
|
+
/>
|
|
364
|
+
</Box>
|
|
365
|
+
);
|
|
366
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Box } from 'ink';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import { formatCommandOutputSummary } from './command-output-summary.js';
|
|
5
|
+
import { Text } from './SafeText.js';
|
|
6
|
+
import { statusGlyphColor } from './status-glyph.js';
|
|
7
|
+
import { usePalette } from './theme/index.js';
|
|
8
|
+
|
|
9
|
+
import type { ICommandOutputInput } from './command-output-summary.js';
|
|
10
|
+
|
|
11
|
+
interface IProps {
|
|
12
|
+
tool: ICommandOutputInput;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function ToolCommandOutput({ tool }: IProps): React.ReactElement | null {
|
|
16
|
+
const palette = usePalette();
|
|
17
|
+
const summary = formatCommandOutputSummary(tool);
|
|
18
|
+
if (!summary) return null;
|
|
19
|
+
|
|
20
|
+
// A successful command with no output renders nothing here: the parent
|
|
21
|
+
// (MessageList ToolSummaryEntry) already shows a "✓ tool(args)" summary line for
|
|
22
|
+
// every tool, so a second "✓ ok" line was a duplicate success marker (SCREEN-007).
|
|
23
|
+
if (
|
|
24
|
+
summary.statusLabel === 'ok' &&
|
|
25
|
+
summary.previewLines.length === 0 &&
|
|
26
|
+
!summary.transcriptHint
|
|
27
|
+
) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const color =
|
|
31
|
+
summary.status === 'error' ? statusGlyphColor(palette, 'error') : palette.text.emphasis;
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<Box flexDirection="column" marginLeft={4}>
|
|
35
|
+
{summary.statusLabel !== 'ok' && (
|
|
36
|
+
<Text color={color} dimColor>
|
|
37
|
+
{summary.statusLabel}
|
|
38
|
+
</Text>
|
|
39
|
+
)}
|
|
40
|
+
{summary.previewLines.map((line, index) => (
|
|
41
|
+
<Text key={`${line}-${index}`} color={color} dimColor>
|
|
42
|
+
{line}
|
|
43
|
+
</Text>
|
|
44
|
+
))}
|
|
45
|
+
{summary.transcriptHint && <Text dimColor>{summary.transcriptHint}</Text>}
|
|
46
|
+
</Box>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Box } from 'ink';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
|
|
4
|
+
import { useRenderMarkdown } from './hooks/useRenderMarkdown.js';
|
|
5
|
+
import { RenderedText, Text } from './SafeText.js';
|
|
6
|
+
import { sanitizeTerminalText } from './sanitize-terminal-text.js';
|
|
7
|
+
import { useScreenReader } from './screen-reader-context.js';
|
|
8
|
+
import { usePalette } from './theme/index.js';
|
|
9
|
+
import { buildToolDiffSummary } from './utils/tool-diff-summary.js';
|
|
10
|
+
|
|
11
|
+
import type { IDiffLine } from './utils/edit-diff.js';
|
|
12
|
+
|
|
13
|
+
interface IProps {
|
|
14
|
+
file?: string;
|
|
15
|
+
lines: readonly IDiffLine[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function ToolDiffBlock({ file, lines }: IProps): React.ReactElement {
|
|
19
|
+
const palette = usePalette();
|
|
20
|
+
const renderMarkdown = useRenderMarkdown();
|
|
21
|
+
const summary = buildToolDiffSummary({ file, lines });
|
|
22
|
+
// CLI-2004: the `\u2502` gutter is a vertical rule a reader announces on every line it prefixes.
|
|
23
|
+
// The mode drops it — indentation already separates the block from the surrounding transcript.
|
|
24
|
+
const screenReader = useScreenReader();
|
|
25
|
+
const gutter = screenReader ? '' : '\u2502 ';
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Box flexDirection="column" marginLeft={4}>
|
|
29
|
+
{summary.file && (
|
|
30
|
+
<Text color={palette.text.emphasis} dimColor>
|
|
31
|
+
{gutter}
|
|
32
|
+
{sanitizeTerminalText(summary.file)}
|
|
33
|
+
</Text>
|
|
34
|
+
)}
|
|
35
|
+
{/* `renderMarkdown` sanitizes its input and then styles it; the SGR in its output is ours. */}
|
|
36
|
+
<RenderedText>{renderMarkdown(summary.markdown)}</RenderedText>
|
|
37
|
+
{summary.truncated && (
|
|
38
|
+
<Text color={palette.text.emphasis} dimColor>
|
|
39
|
+
{gutter}... and {summary.remainingLineCount} more lines
|
|
40
|
+
</Text>
|
|
41
|
+
)}
|
|
42
|
+
</Box>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TransportTUI — interactive overlay for transport enable/disable settings.
|
|
3
|
+
*
|
|
4
|
+
* Arrow keys navigate the list, space toggles enabled/disabled, enter/esc closes.
|
|
5
|
+
*
|
|
6
|
+
* TRANS-009: a toggle SAVES a setting. It does not start or stop anything — `registry.setEnabled`
|
|
7
|
+
* writes the settings file and returns, and `startAll` reads `getEnabled()` when a session starts, so
|
|
8
|
+
* the change takes effect at the next start. This component used to render `[enabled]` on the way
|
|
9
|
+
* back from that write, which told the user a transport was running when nothing had been started.
|
|
10
|
+
*
|
|
11
|
+
* The badge therefore names the SAVED setting and the footer says when it applies. And a failed write
|
|
12
|
+
* is rendered rather than swallowed: the previous `.catch(() => setSaving(false))` took the error and
|
|
13
|
+
* discarded it, so a settings file that could not be written looked exactly like a successful save
|
|
14
|
+
* with the row unchanged.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { Box } from 'ink';
|
|
18
|
+
import React, { useState, useCallback } from 'react';
|
|
19
|
+
|
|
20
|
+
import { KeyHintFooter } from './key-hint-footer.js';
|
|
21
|
+
import { useKeybindingActions, useKeybindingHints } from './keybindings/keybindings-context.js';
|
|
22
|
+
import { useProductDisplayName } from './product-display-name-context.js';
|
|
23
|
+
import { Text } from './SafeText.js';
|
|
24
|
+
import { usePalette } from './theme/index.js';
|
|
25
|
+
|
|
26
|
+
import type {
|
|
27
|
+
ITransportEntry,
|
|
28
|
+
ITransportSettingsRegistryView,
|
|
29
|
+
} from '@robota-sdk/agent-interface-transport';
|
|
30
|
+
|
|
31
|
+
const TRANSPORT_NAME_WIDTH = 18;
|
|
32
|
+
|
|
33
|
+
interface IEntryRowProps {
|
|
34
|
+
entry: ITransportEntry;
|
|
35
|
+
selected: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function TransportEntryRow({ entry, selected }: IEntryRowProps): React.ReactElement {
|
|
39
|
+
const palette = usePalette();
|
|
40
|
+
const enabled = entry.config.enabled;
|
|
41
|
+
const dot = enabled ? '●' : '○';
|
|
42
|
+
// The saved setting, not a running state — see the file header.
|
|
43
|
+
const badge = enabled ? '[on] ' : '[off]';
|
|
44
|
+
const portOpt = entry.config.options?.port;
|
|
45
|
+
const portHint = typeof portOpt === 'number' ? `port: ${portOpt}` : '';
|
|
46
|
+
return (
|
|
47
|
+
<Box>
|
|
48
|
+
<Text color={selected ? palette.text.accent : undefined} bold={selected}>
|
|
49
|
+
{`${dot} ${entry.transport.name.padEnd(TRANSPORT_NAME_WIDTH)} ${badge} ${portHint}`}
|
|
50
|
+
</Text>
|
|
51
|
+
</Box>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function useTransportInput(
|
|
56
|
+
entries: ITransportEntry[],
|
|
57
|
+
cursor: number,
|
|
58
|
+
saving: boolean,
|
|
59
|
+
registry: ITransportSettingsRegistryView,
|
|
60
|
+
setCursor: (fn: (c: number) => number) => void,
|
|
61
|
+
setSaving: (v: boolean) => void,
|
|
62
|
+
setError: (v: string | undefined) => void,
|
|
63
|
+
onClose: () => void,
|
|
64
|
+
refresh: () => void,
|
|
65
|
+
): void {
|
|
66
|
+
useKeybindingActions(
|
|
67
|
+
'transport-settings',
|
|
68
|
+
useCallback(
|
|
69
|
+
(actions: readonly ('previous' | 'next' | 'toggle' | 'close')[]) => {
|
|
70
|
+
if (saving) return;
|
|
71
|
+
if (actions.includes('previous')) {
|
|
72
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (actions.includes('next')) {
|
|
76
|
+
setCursor((c) => Math.min(entries.length - 1, c + 1));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (actions.includes('close')) {
|
|
80
|
+
onClose();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (actions.includes('toggle')) {
|
|
84
|
+
const entry = entries[cursor];
|
|
85
|
+
if (!entry) return;
|
|
86
|
+
setSaving(true);
|
|
87
|
+
setError(undefined);
|
|
88
|
+
registry
|
|
89
|
+
.setEnabled(entry.transport.name, !entry.config.enabled)
|
|
90
|
+
.then(() => {
|
|
91
|
+
refresh();
|
|
92
|
+
setSaving(false);
|
|
93
|
+
})
|
|
94
|
+
.catch((cause: unknown) => {
|
|
95
|
+
// The reason, not just the fact. A settings file that cannot be written and a
|
|
96
|
+
// transport that refuses configuration are different problems for the user.
|
|
97
|
+
setError(cause instanceof Error ? cause.message : 'could not save the setting');
|
|
98
|
+
setSaving(false);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
[saving, entries, cursor, registry, onClose, refresh, setCursor, setSaving, setError],
|
|
103
|
+
),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface IProps {
|
|
108
|
+
registry: ITransportSettingsRegistryView;
|
|
109
|
+
onClose: () => void;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export default function TransportTUI({ registry, onClose }: IProps): React.ReactElement {
|
|
113
|
+
const palette = usePalette();
|
|
114
|
+
const productDisplayName = useProductDisplayName();
|
|
115
|
+
const [entries, setEntries] = useState(() => registry.getAll());
|
|
116
|
+
const [cursor, setCursor] = useState(0);
|
|
117
|
+
const [saving, setSaving] = useState(false);
|
|
118
|
+
const [error, setError] = useState<string | undefined>(undefined);
|
|
119
|
+
const refresh = useCallback((): void => {
|
|
120
|
+
setEntries(registry.getAll());
|
|
121
|
+
}, [registry]);
|
|
122
|
+
const footerHints = useKeybindingHints('transport-settings', [
|
|
123
|
+
[['previous', 'next'], 'Select'],
|
|
124
|
+
['toggle', 'Toggle'],
|
|
125
|
+
['close', 'Close'],
|
|
126
|
+
]);
|
|
127
|
+
|
|
128
|
+
useTransportInput(
|
|
129
|
+
entries,
|
|
130
|
+
cursor,
|
|
131
|
+
saving,
|
|
132
|
+
registry,
|
|
133
|
+
setCursor,
|
|
134
|
+
setSaving,
|
|
135
|
+
setError,
|
|
136
|
+
onClose,
|
|
137
|
+
refresh,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<Box flexDirection="column" paddingX={2} paddingY={1}>
|
|
142
|
+
<Text bold>Settings › Transports</Text>
|
|
143
|
+
<Box marginTop={1} flexDirection="column">
|
|
144
|
+
{entries.map((entry, i) => (
|
|
145
|
+
<TransportEntryRow key={entry.transport.name} entry={entry} selected={i === cursor} />
|
|
146
|
+
))}
|
|
147
|
+
</Box>
|
|
148
|
+
<Box marginTop={1} flexDirection="column">
|
|
149
|
+
<KeyHintFooter hints={footerHints} />
|
|
150
|
+
<Text dimColor>
|
|
151
|
+
A toggle is saved now and applies the next time {productDisplayName} starts.
|
|
152
|
+
</Text>
|
|
153
|
+
</Box>
|
|
154
|
+
{saving && (
|
|
155
|
+
<Box marginTop={1}>
|
|
156
|
+
<Text color={palette.text.warning}>Saving…</Text>
|
|
157
|
+
</Box>
|
|
158
|
+
)}
|
|
159
|
+
{error !== undefined && (
|
|
160
|
+
<Box marginTop={1}>
|
|
161
|
+
<Text color={palette.text.error}>{`Not saved — ${error}`}</Text>
|
|
162
|
+
</Box>
|
|
163
|
+
)}
|
|
164
|
+
</Box>
|
|
165
|
+
);
|
|
166
|
+
}
|