@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,154 @@
|
|
|
1
|
+
import { Box, useStdout } from 'ink';
|
|
2
|
+
import React, { useState, useEffect } from 'react';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
KeyHintFooter,
|
|
6
|
+
SELECTION_INDICATOR,
|
|
7
|
+
SELECTION_INDICATOR_NONE,
|
|
8
|
+
type IKeyHint,
|
|
9
|
+
} from './key-hint-footer.js';
|
|
10
|
+
import { useKeybindingHints } from './keybindings/keybindings-context.js';
|
|
11
|
+
import { numberedRowPrefix } from './numbered-list.js';
|
|
12
|
+
import { Text } from './SafeText.js';
|
|
13
|
+
import { useScreenReader } from './screen-reader-context.js';
|
|
14
|
+
import { usePalette } from './theme/index.js';
|
|
15
|
+
|
|
16
|
+
import type { ICommand } from '@robota-sdk/agent-interface-command';
|
|
17
|
+
|
|
18
|
+
/** Footer for the autocomplete popup. */
|
|
19
|
+
export const SLASH_AUTOCOMPLETE_FOOTER_HINTS: readonly IKeyHint[] = [
|
|
20
|
+
{ keys: '↑↓', label: 'Navigate' },
|
|
21
|
+
{ keys: 'Tab', label: 'Complete' },
|
|
22
|
+
{ keys: 'Enter', label: 'Select' },
|
|
23
|
+
{ keys: 'Esc', label: 'Close' },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
interface IProps {
|
|
27
|
+
/** Filtered list of commands to display */
|
|
28
|
+
commands: ICommand[];
|
|
29
|
+
/** Currently highlighted item index */
|
|
30
|
+
selectedIndex: number;
|
|
31
|
+
/** Whether to show the popup */
|
|
32
|
+
visible: boolean;
|
|
33
|
+
/** Whether showing subcommands (no slash prefix) */
|
|
34
|
+
isSubcommandMode?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const MAX_VISIBLE = 8;
|
|
38
|
+
// border(1×2) + paddingX(1×2) consumed by outer box
|
|
39
|
+
const OUTER_CHROME = 4;
|
|
40
|
+
const MIN_ROW_WIDTH = 40;
|
|
41
|
+
const NAME_COL_MAX = 20;
|
|
42
|
+
|
|
43
|
+
function useRowWidth(): number {
|
|
44
|
+
const { stdout } = useStdout();
|
|
45
|
+
const measure = (): number => Math.max(MIN_ROW_WIDTH, (stdout.columns ?? 80) - OUTER_CHROME);
|
|
46
|
+
const [width, setWidth] = useState(measure);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
const onResize = (): void => setWidth(measure());
|
|
50
|
+
stdout.on('resize', onResize);
|
|
51
|
+
return () => {
|
|
52
|
+
stdout.off('resize', onResize);
|
|
53
|
+
};
|
|
54
|
+
}, [stdout]);
|
|
55
|
+
|
|
56
|
+
return width;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function capName(name: string, colWidth: number): string {
|
|
60
|
+
return name.length > colWidth ? `${name.slice(0, colWidth - 1)}…` : name.padEnd(colWidth);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Render a single command row */
|
|
64
|
+
function CommandRow(props: {
|
|
65
|
+
cmd: ICommand;
|
|
66
|
+
isSelected: boolean;
|
|
67
|
+
showSlash: boolean;
|
|
68
|
+
rowWidth: number;
|
|
69
|
+
nameColWidth: number;
|
|
70
|
+
/** CLI-2004: row number in the mode, replacing the `> ` cursor. */
|
|
71
|
+
rowNumber?: number;
|
|
72
|
+
}): React.ReactElement {
|
|
73
|
+
const palette = usePalette();
|
|
74
|
+
const { cmd, isSelected, showSlash, rowWidth, nameColWidth, rowNumber } = props;
|
|
75
|
+
const indicator =
|
|
76
|
+
rowNumber !== undefined
|
|
77
|
+
? numberedRowPrefix(rowNumber)
|
|
78
|
+
: isSelected
|
|
79
|
+
? SELECTION_INDICATOR
|
|
80
|
+
: SELECTION_INDICATOR_NONE;
|
|
81
|
+
const nameColor = isSelected ? palette.text.accent : undefined;
|
|
82
|
+
const dimmed = !isSelected;
|
|
83
|
+
const namePart = capName(cmd.name, nameColWidth);
|
|
84
|
+
const text = showSlash
|
|
85
|
+
? `${indicator}/${namePart} ${cmd.description ?? ''}`
|
|
86
|
+
: `${indicator}${namePart} ${cmd.description ?? ''}`;
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<Box width={rowWidth}>
|
|
90
|
+
<Text color={nameColor} dimColor={dimmed} wrap="truncate-end">
|
|
91
|
+
{text}
|
|
92
|
+
</Text>
|
|
93
|
+
</Box>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Autocomplete popup showing matching slash commands */
|
|
98
|
+
export default function SlashAutocomplete({
|
|
99
|
+
commands,
|
|
100
|
+
selectedIndex,
|
|
101
|
+
visible,
|
|
102
|
+
isSubcommandMode,
|
|
103
|
+
}: IProps): React.ReactElement | null {
|
|
104
|
+
const palette = usePalette();
|
|
105
|
+
const rowWidth = useRowWidth();
|
|
106
|
+
const screenReader = useScreenReader();
|
|
107
|
+
const footerHints = useKeybindingHints('autocomplete-menu', [
|
|
108
|
+
[['previous', 'next'], 'Navigate'],
|
|
109
|
+
['accept', 'Complete'],
|
|
110
|
+
['execute', 'Select'],
|
|
111
|
+
['close', 'Close'],
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
if (!visible || commands.length === 0) return null;
|
|
115
|
+
|
|
116
|
+
const scrollOffset = computeScrollOffset(selectedIndex, commands.length);
|
|
117
|
+
const visibleCommands = commands.slice(scrollOffset, scrollOffset + MAX_VISIBLE);
|
|
118
|
+
|
|
119
|
+
const nameColWidth = Math.min(
|
|
120
|
+
NAME_COL_MAX,
|
|
121
|
+
Math.max(...visibleCommands.map((c) => c.name.length)),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<Box
|
|
126
|
+
flexDirection="column"
|
|
127
|
+
{...(screenReader
|
|
128
|
+
? {}
|
|
129
|
+
: { borderStyle: 'round' as const, borderColor: palette.border.muted })}
|
|
130
|
+
paddingX={1}
|
|
131
|
+
>
|
|
132
|
+
{visibleCommands.map((cmd, i) => (
|
|
133
|
+
<CommandRow
|
|
134
|
+
key={cmd.name}
|
|
135
|
+
cmd={cmd}
|
|
136
|
+
isSelected={scrollOffset + i === selectedIndex}
|
|
137
|
+
showSlash={!isSubcommandMode}
|
|
138
|
+
rowWidth={rowWidth}
|
|
139
|
+
nameColWidth={nameColWidth}
|
|
140
|
+
{...(screenReader ? { rowNumber: scrollOffset + i } : {})}
|
|
141
|
+
/>
|
|
142
|
+
))}
|
|
143
|
+
<KeyHintFooter hints={footerHints} />
|
|
144
|
+
</Box>
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Compute scroll offset to keep selectedIndex visible */
|
|
149
|
+
function computeScrollOffset(selectedIndex: number, total: number): number {
|
|
150
|
+
if (total <= MAX_VISIBLE) return 0;
|
|
151
|
+
if (selectedIndex < MAX_VISIBLE) return 0;
|
|
152
|
+
const maxOffset = total - MAX_VISIBLE;
|
|
153
|
+
return Math.min(selectedIndex - MAX_VISIBLE + 1, maxOffset);
|
|
154
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { formatTokenCount } from '@robota-sdk/agent-core';
|
|
2
|
+
import { Box } from 'ink';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
import { Text } from './SafeText.js';
|
|
6
|
+
import { useScreenReader } from './screen-reader-context.js';
|
|
7
|
+
import { formatStatusActivity } from './status-activity.js';
|
|
8
|
+
import { usePalette } from './theme/index.js';
|
|
9
|
+
|
|
10
|
+
import type { IThemeColors } from './theme/index.js';
|
|
11
|
+
import type { TModelEffortSelection, TPermissionMode } from '@robota-sdk/agent-core';
|
|
12
|
+
|
|
13
|
+
/** Threshold boundaries for context percentage color coding */
|
|
14
|
+
const CONTEXT_YELLOW_THRESHOLD = 70;
|
|
15
|
+
const CONTEXT_RED_THRESHOLD = 90;
|
|
16
|
+
|
|
17
|
+
/** Segment separator between status-bar fields (SCREEN-006: single SSOT, was repeated inline 6x). */
|
|
18
|
+
const SEP = ' | ';
|
|
19
|
+
|
|
20
|
+
interface IProps {
|
|
21
|
+
permissionMode: TPermissionMode;
|
|
22
|
+
modelName: string;
|
|
23
|
+
providerDisplayName?: string | undefined;
|
|
24
|
+
sessionId: string;
|
|
25
|
+
isThinking: boolean;
|
|
26
|
+
activeToolCount?: number;
|
|
27
|
+
activeBackgroundTaskCount?: number;
|
|
28
|
+
hasPendingPrompt?: boolean;
|
|
29
|
+
contextPercentage: number;
|
|
30
|
+
contextUsedTokens: number;
|
|
31
|
+
contextMaxTokens: number;
|
|
32
|
+
sessionName?: string;
|
|
33
|
+
gitBranch?: string;
|
|
34
|
+
showGitBranch?: boolean;
|
|
35
|
+
activeAgentLabel?: string;
|
|
36
|
+
activePresetId?: string;
|
|
37
|
+
effort?: TModelEffortSelection;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface IStatusLeftProps {
|
|
41
|
+
permissionMode: TPermissionMode;
|
|
42
|
+
modelName: string;
|
|
43
|
+
providerDisplayName?: string | undefined;
|
|
44
|
+
isThinking: boolean;
|
|
45
|
+
activeToolCount: number;
|
|
46
|
+
activeBackgroundTaskCount: number;
|
|
47
|
+
hasPendingPrompt: boolean;
|
|
48
|
+
contextPercentage: number;
|
|
49
|
+
contextUsedTokens: number;
|
|
50
|
+
contextMaxTokens: number;
|
|
51
|
+
sessionName?: string;
|
|
52
|
+
gitBranch?: string;
|
|
53
|
+
showGitBranch: boolean;
|
|
54
|
+
activePresetId?: string;
|
|
55
|
+
effort?: TModelEffortSelection;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Return the color for the context percentage indicator */
|
|
59
|
+
function getContextColor(percentage: number, palette: IThemeColors): string {
|
|
60
|
+
if (percentage >= CONTEXT_RED_THRESHOLD) return palette.text.error;
|
|
61
|
+
if (percentage >= CONTEXT_YELLOW_THRESHOLD) return palette.text.warning;
|
|
62
|
+
return palette.text.success;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function StatusActivityText({
|
|
66
|
+
isThinking,
|
|
67
|
+
activeToolCount,
|
|
68
|
+
activeBackgroundTaskCount,
|
|
69
|
+
hasPendingPrompt,
|
|
70
|
+
}: Pick<
|
|
71
|
+
IStatusLeftProps,
|
|
72
|
+
'isThinking' | 'activeToolCount' | 'activeBackgroundTaskCount' | 'hasPendingPrompt'
|
|
73
|
+
>): React.ReactElement {
|
|
74
|
+
const palette = usePalette();
|
|
75
|
+
const activity = formatStatusActivity({
|
|
76
|
+
isThinking,
|
|
77
|
+
activeToolCount,
|
|
78
|
+
activeBackgroundTaskCount,
|
|
79
|
+
hasPendingPrompt,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<Text color={palette.text[activity.tone]} bold={activity.kind !== 'idle'}>
|
|
84
|
+
{activity.text}
|
|
85
|
+
</Text>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function ContextText({
|
|
90
|
+
percentage,
|
|
91
|
+
usedTokens,
|
|
92
|
+
maxTokens,
|
|
93
|
+
}: {
|
|
94
|
+
percentage: number;
|
|
95
|
+
usedTokens: number;
|
|
96
|
+
maxTokens: number;
|
|
97
|
+
}): React.ReactElement {
|
|
98
|
+
const palette = usePalette();
|
|
99
|
+
return (
|
|
100
|
+
<Text color={getContextColor(percentage, palette)}>
|
|
101
|
+
Context: {Math.round(percentage)}% ({formatTokenCount(usedTokens)}/
|
|
102
|
+
{formatTokenCount(maxTokens)} tokens)
|
|
103
|
+
</Text>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function ModeText({ permissionMode }: { permissionMode: TPermissionMode }): React.ReactElement {
|
|
108
|
+
const palette = usePalette();
|
|
109
|
+
return (
|
|
110
|
+
<>
|
|
111
|
+
<Text color={palette.text.accent} bold>
|
|
112
|
+
Mode:
|
|
113
|
+
</Text>{' '}
|
|
114
|
+
<Text>{permissionMode}</Text>
|
|
115
|
+
</>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* CLI-2004 verdict (l): outside the mode the `default` mode is hidden as noise. In the mode it is
|
|
121
|
+
* ALWAYS rendered — with no key-based cycling to announce, the status line is the only place a
|
|
122
|
+
* reader can find which permission mode it is in, and a hidden `default` leaves nothing to find.
|
|
123
|
+
*/
|
|
124
|
+
function shouldShowPermissionMode(permissionMode: TPermissionMode, screenReader: boolean): boolean {
|
|
125
|
+
return screenReader || permissionMode !== 'default';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function PresetText({ activePresetId }: { activePresetId: string }): React.ReactElement {
|
|
129
|
+
const palette = usePalette();
|
|
130
|
+
return (
|
|
131
|
+
<>
|
|
132
|
+
<Text color={palette.text.accent} bold>
|
|
133
|
+
Preset:
|
|
134
|
+
</Text>{' '}
|
|
135
|
+
<Text>{activePresetId}</Text>
|
|
136
|
+
</>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function shouldShowActivePreset(activePresetId: string | undefined): activePresetId is string {
|
|
141
|
+
return activePresetId !== undefined && activePresetId !== 'default';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function ProviderText({
|
|
145
|
+
modelName,
|
|
146
|
+
providerDisplayName,
|
|
147
|
+
effort,
|
|
148
|
+
}: {
|
|
149
|
+
modelName: string;
|
|
150
|
+
providerDisplayName?: string | undefined;
|
|
151
|
+
effort?: TModelEffortSelection;
|
|
152
|
+
}): React.ReactElement {
|
|
153
|
+
const provider =
|
|
154
|
+
providerDisplayName !== undefined ? `${providerDisplayName} ${modelName}` : modelName;
|
|
155
|
+
if (providerDisplayName !== undefined) {
|
|
156
|
+
return (
|
|
157
|
+
<Text dimColor>
|
|
158
|
+
{provider}
|
|
159
|
+
{effort !== undefined ? ` Effort: ${effort}` : ''}
|
|
160
|
+
</Text>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
return (
|
|
164
|
+
<Text dimColor>
|
|
165
|
+
{provider}
|
|
166
|
+
{effort !== undefined ? ` Effort: ${effort}` : ''}
|
|
167
|
+
</Text>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function StatusLeft(props: IStatusLeftProps): React.ReactElement {
|
|
172
|
+
const palette = usePalette();
|
|
173
|
+
const shouldShowGitBranch =
|
|
174
|
+
props.showGitBranch && props.gitBranch !== undefined && props.gitBranch.length > 0;
|
|
175
|
+
// CLI-2004 § Solution 13: one rule applied to fields of different VOLATILITY. The activity text
|
|
176
|
+
// and the context percentage re-render on their own cadence and on every token, so a reader
|
|
177
|
+
// reviewing the status line would re-announce them continuously while the operator types — they
|
|
178
|
+
// are suppressed. The permission mode, the preset and the model change only when the operator
|
|
179
|
+
// changes them: those are anchors, and they stay.
|
|
180
|
+
const screenReader = useScreenReader();
|
|
181
|
+
const showPermissionMode = shouldShowPermissionMode(props.permissionMode, screenReader);
|
|
182
|
+
const activePresetId = props.activePresetId;
|
|
183
|
+
const showActivePreset = shouldShowActivePreset(activePresetId);
|
|
184
|
+
return (
|
|
185
|
+
<Text>
|
|
186
|
+
{!screenReader && (
|
|
187
|
+
<StatusActivityText
|
|
188
|
+
isThinking={props.isThinking}
|
|
189
|
+
activeToolCount={props.activeToolCount}
|
|
190
|
+
activeBackgroundTaskCount={props.activeBackgroundTaskCount}
|
|
191
|
+
hasPendingPrompt={props.hasPendingPrompt}
|
|
192
|
+
/>
|
|
193
|
+
)}
|
|
194
|
+
{showPermissionMode && (
|
|
195
|
+
<>
|
|
196
|
+
{SEP}
|
|
197
|
+
<ModeText permissionMode={props.permissionMode} />
|
|
198
|
+
</>
|
|
199
|
+
)}
|
|
200
|
+
{showActivePreset && (
|
|
201
|
+
<>
|
|
202
|
+
{SEP}
|
|
203
|
+
<PresetText activePresetId={activePresetId} />
|
|
204
|
+
</>
|
|
205
|
+
)}
|
|
206
|
+
{props.sessionName && (
|
|
207
|
+
<>
|
|
208
|
+
{SEP}
|
|
209
|
+
<Text color={palette.text.session}>{props.sessionName}</Text>
|
|
210
|
+
</>
|
|
211
|
+
)}
|
|
212
|
+
{shouldShowGitBranch && (
|
|
213
|
+
<>
|
|
214
|
+
{SEP}
|
|
215
|
+
<Text dimColor>git: {props.gitBranch}</Text>
|
|
216
|
+
</>
|
|
217
|
+
)}
|
|
218
|
+
{SEP}
|
|
219
|
+
<ProviderText
|
|
220
|
+
modelName={props.modelName}
|
|
221
|
+
providerDisplayName={props.providerDisplayName}
|
|
222
|
+
effort={props.effort}
|
|
223
|
+
/>
|
|
224
|
+
{!screenReader && (
|
|
225
|
+
<>
|
|
226
|
+
{SEP}
|
|
227
|
+
<ContextText
|
|
228
|
+
percentage={props.contextPercentage}
|
|
229
|
+
usedTokens={props.contextUsedTokens}
|
|
230
|
+
maxTokens={props.contextMaxTokens}
|
|
231
|
+
/>
|
|
232
|
+
</>
|
|
233
|
+
)}
|
|
234
|
+
</Text>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export default function StatusBar({
|
|
239
|
+
permissionMode,
|
|
240
|
+
modelName,
|
|
241
|
+
providerDisplayName,
|
|
242
|
+
sessionId: _sessionId,
|
|
243
|
+
isThinking,
|
|
244
|
+
activeToolCount = 0,
|
|
245
|
+
activeBackgroundTaskCount = 0,
|
|
246
|
+
hasPendingPrompt = false,
|
|
247
|
+
contextPercentage,
|
|
248
|
+
contextUsedTokens,
|
|
249
|
+
contextMaxTokens,
|
|
250
|
+
sessionName,
|
|
251
|
+
gitBranch,
|
|
252
|
+
showGitBranch = true,
|
|
253
|
+
activeAgentLabel,
|
|
254
|
+
activePresetId,
|
|
255
|
+
effort,
|
|
256
|
+
}: IProps): React.ReactElement {
|
|
257
|
+
const palette = usePalette();
|
|
258
|
+
return (
|
|
259
|
+
<Box paddingLeft={1} paddingRight={1} justifyContent="space-between">
|
|
260
|
+
<StatusLeft
|
|
261
|
+
permissionMode={permissionMode}
|
|
262
|
+
modelName={modelName}
|
|
263
|
+
providerDisplayName={providerDisplayName}
|
|
264
|
+
isThinking={isThinking}
|
|
265
|
+
activeToolCount={activeToolCount}
|
|
266
|
+
activeBackgroundTaskCount={activeBackgroundTaskCount}
|
|
267
|
+
hasPendingPrompt={hasPendingPrompt}
|
|
268
|
+
contextPercentage={contextPercentage}
|
|
269
|
+
contextUsedTokens={contextUsedTokens}
|
|
270
|
+
contextMaxTokens={contextMaxTokens}
|
|
271
|
+
sessionName={sessionName}
|
|
272
|
+
gitBranch={gitBranch}
|
|
273
|
+
showGitBranch={showGitBranch}
|
|
274
|
+
activePresetId={activePresetId}
|
|
275
|
+
effort={effort}
|
|
276
|
+
/>
|
|
277
|
+
{activeAgentLabel !== undefined && (
|
|
278
|
+
<Text color={palette.text.warning} bold>
|
|
279
|
+
[{activeAgentLabel}]
|
|
280
|
+
</Text>
|
|
281
|
+
)}
|
|
282
|
+
</Box>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming indicator — shows real-time tool execution and AI response text.
|
|
3
|
+
* Displayed during session.run() execution.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Box } from 'ink';
|
|
7
|
+
import React from 'react';
|
|
8
|
+
|
|
9
|
+
import { useRenderMarkdown } from './hooks/useRenderMarkdown.js';
|
|
10
|
+
import { humanizeToolArgument, humanizeToolName } from './humanize-tool-name.js';
|
|
11
|
+
import { RenderedText, Text } from './SafeText.js';
|
|
12
|
+
import {
|
|
13
|
+
useModelCommandToolPrefix,
|
|
14
|
+
useProductDisplayName,
|
|
15
|
+
} from './product-display-name-context.js';
|
|
16
|
+
import { useScreenReader } from './screen-reader-context.js';
|
|
17
|
+
import { SCREEN_READER_LABELS } from './screen-reader-labels.js';
|
|
18
|
+
import { STATUS_SYMBOL, statusGlyphColor, toolStateStatusKind } from './status-glyph.js';
|
|
19
|
+
import { usePalette } from './theme/index.js';
|
|
20
|
+
import ToolDiffBlock from './ToolDiffBlock.js';
|
|
21
|
+
|
|
22
|
+
import type { IThemeColors } from './theme/index.js';
|
|
23
|
+
import type { IToolState } from '@robota-sdk/agent-interface-session';
|
|
24
|
+
|
|
25
|
+
function getToolStyle(
|
|
26
|
+
t: IToolState,
|
|
27
|
+
palette: IThemeColors,
|
|
28
|
+
): {
|
|
29
|
+
color: string;
|
|
30
|
+
icon: string;
|
|
31
|
+
strikethrough: boolean;
|
|
32
|
+
} {
|
|
33
|
+
const kind = toolStateStatusKind(t);
|
|
34
|
+
return {
|
|
35
|
+
color: statusGlyphColor(palette, kind),
|
|
36
|
+
icon: STATUS_SYMBOL[kind],
|
|
37
|
+
strikethrough: kind === 'error' || kind === 'denied',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface IProps {
|
|
42
|
+
text: string;
|
|
43
|
+
activeTools: readonly IToolState[];
|
|
44
|
+
isThinking?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function ThinkingFallback({ isThinking }: { isThinking: boolean }): React.ReactElement {
|
|
48
|
+
const palette = usePalette();
|
|
49
|
+
if (!isThinking) return <></>;
|
|
50
|
+
return (
|
|
51
|
+
<Box marginBottom={1}>
|
|
52
|
+
<Text color={palette.text.warning}>Thinking...</Text>
|
|
53
|
+
</Box>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function ActiveTools({ activeTools }: { activeTools: readonly IToolState[] }): React.ReactElement {
|
|
58
|
+
const palette = usePalette();
|
|
59
|
+
const modelCommandToolPrefix = useModelCommandToolPrefix();
|
|
60
|
+
return (
|
|
61
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
62
|
+
<Text color={palette.text.emphasis} bold>
|
|
63
|
+
Tools:
|
|
64
|
+
</Text>
|
|
65
|
+
<Text> </Text>
|
|
66
|
+
{activeTools.map((t, i) => {
|
|
67
|
+
const { color, icon, strikethrough } = getToolStyle(t, palette);
|
|
68
|
+
return (
|
|
69
|
+
<Box key={`${t.toolName}-${i}`} flexDirection="column">
|
|
70
|
+
<Text color={color} strikethrough={strikethrough}>
|
|
71
|
+
{' '}
|
|
72
|
+
{icon} {humanizeToolName(t.toolName, modelCommandToolPrefix)}({humanizeToolArgument(t.firstArg)})
|
|
73
|
+
</Text>
|
|
74
|
+
{t.diffLines && t.diffLines.length > 0 && (
|
|
75
|
+
<ToolDiffBlock file={t.diffFile} lines={t.diffLines} />
|
|
76
|
+
)}
|
|
77
|
+
</Box>
|
|
78
|
+
);
|
|
79
|
+
})}
|
|
80
|
+
</Box>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* CLI-2004: the whole progress indicator collapses to ONE static line. A spinner is a repaint per
|
|
86
|
+
* frame, and a repaint is an announcement — the reader would hear the same line forever. The line
|
|
87
|
+
* still carries what changed (which tools are running, how far the reply has got), just once per
|
|
88
|
+
* actual change instead of once per frame.
|
|
89
|
+
*/
|
|
90
|
+
function renderScreenReaderStatus(
|
|
91
|
+
text: string,
|
|
92
|
+
activeTools: readonly IToolState[],
|
|
93
|
+
isThinking: boolean,
|
|
94
|
+
modelCommandToolPrefix?: string,
|
|
95
|
+
): React.ReactElement {
|
|
96
|
+
const running = activeTools
|
|
97
|
+
.map((tool) => humanizeToolName(tool.toolName, modelCommandToolPrefix))
|
|
98
|
+
.join(', ');
|
|
99
|
+
const parts = [
|
|
100
|
+
running.length > 0 ? `${SCREEN_READER_LABELS.tool} ${running}` : undefined,
|
|
101
|
+
text.length > 0 ? text : isThinking ? SCREEN_READER_LABELS.thinking : undefined,
|
|
102
|
+
].filter((part): part is string => part !== undefined);
|
|
103
|
+
if (parts.length === 0) return <></>;
|
|
104
|
+
return <Text>{parts.join(' — ')}</Text>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export default function StreamingIndicator({
|
|
108
|
+
text,
|
|
109
|
+
activeTools,
|
|
110
|
+
isThinking = false,
|
|
111
|
+
}: IProps): React.ReactElement {
|
|
112
|
+
const palette = usePalette();
|
|
113
|
+
const renderMarkdown = useRenderMarkdown();
|
|
114
|
+
const hasTools = activeTools.length > 0;
|
|
115
|
+
const hasText = text.length > 0;
|
|
116
|
+
const screenReader = useScreenReader();
|
|
117
|
+
const productDisplayName = useProductDisplayName();
|
|
118
|
+
const modelCommandToolPrefix = useModelCommandToolPrefix();
|
|
119
|
+
|
|
120
|
+
if (screenReader) {
|
|
121
|
+
return renderScreenReaderStatus(text, activeTools, isThinking, modelCommandToolPrefix);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!hasTools && !hasText) {
|
|
125
|
+
return <ThinkingFallback isThinking={isThinking} />;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<Box flexDirection="column">
|
|
130
|
+
{hasTools && <ActiveTools activeTools={activeTools} />}
|
|
131
|
+
{hasText && (
|
|
132
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
133
|
+
<Text color={palette.text.accent} bold>
|
|
134
|
+
{productDisplayName}:
|
|
135
|
+
</Text>
|
|
136
|
+
<Text> </Text>
|
|
137
|
+
<Box marginLeft={2}>
|
|
138
|
+
{/* `renderMarkdown` sanitizes its input and then styles it; the SGR in its output is ours. */}
|
|
139
|
+
<RenderedText wrap="wrap">{renderMarkdown(text)}</RenderedText>
|
|
140
|
+
</Box>
|
|
141
|
+
</Box>
|
|
142
|
+
)}
|
|
143
|
+
</Box>
|
|
144
|
+
);
|
|
145
|
+
}
|