@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,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TuiStateManager — pure TypeScript rendering state manager.
|
|
3
|
+
*
|
|
4
|
+
* Converts InteractiveSession events into rendering state without a React dependency.
|
|
5
|
+
* The React hook subscribes to onChange and reads state for rendering.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { TuiSessionNoticeStore } from './tui-session-notice-store.js';
|
|
9
|
+
|
|
10
|
+
import type { TTuiNoticeInput } from './tui-session-events.js';
|
|
11
|
+
import type { IContextWindowState, IHistoryEntry } from '@robota-sdk/agent-core';
|
|
12
|
+
import type { IExecutionWorkspaceSnapshot } from '@robota-sdk/agent-interface-execution';
|
|
13
|
+
import type {
|
|
14
|
+
IExecutionResult,
|
|
15
|
+
IToolState,
|
|
16
|
+
TInteractiveEventName,
|
|
17
|
+
} from '@robota-sdk/agent-interface-session';
|
|
18
|
+
|
|
19
|
+
/** Debounce interval for streaming text notify (limits renderMarkdown frequency) */
|
|
20
|
+
const STREAMING_DEBOUNCE_MS = 300;
|
|
21
|
+
/** ERR-001 G3: with no provider activity for this long while thinking, hint that the
|
|
22
|
+
* connection may be stalled (well under the 120s provider idle timeout). */
|
|
23
|
+
const STALL_HINT_MS = 15_000;
|
|
24
|
+
/**
|
|
25
|
+
* TUI view of the core context-window state. The token fields are derived from the agent-core
|
|
26
|
+
* SSOT (`IContextWindowState`) via `Pick` so they stay structurally tied to it; `percentage` is an
|
|
27
|
+
* explicit display-facing mirror of `IContextWindowState.usedPercentage`.
|
|
28
|
+
*/
|
|
29
|
+
export type TContextState = Pick<IContextWindowState, 'usedTokens' | 'maxTokens'> & {
|
|
30
|
+
/** Mirror of `IContextWindowState.usedPercentage`, named for the TUI display layer. */
|
|
31
|
+
percentage: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** Create a debounced notify — schedules at most one call per interval. */
|
|
35
|
+
function createDebouncedNotify(
|
|
36
|
+
notify: () => void,
|
|
37
|
+
ms: number,
|
|
38
|
+
): { schedule: () => void; flush: () => void } {
|
|
39
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
40
|
+
return {
|
|
41
|
+
schedule() {
|
|
42
|
+
if (!timer) {
|
|
43
|
+
timer = setTimeout(() => {
|
|
44
|
+
timer = null;
|
|
45
|
+
notify();
|
|
46
|
+
}, ms);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
flush() {
|
|
50
|
+
if (timer) {
|
|
51
|
+
clearTimeout(timer);
|
|
52
|
+
timer = null;
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class TuiStateManager {
|
|
59
|
+
// ── Rendering state ───────────────────────────────────────────
|
|
60
|
+
history: IHistoryEntry[] = [];
|
|
61
|
+
/**
|
|
62
|
+
* Notices this terminal added itself (command results, init errors). They are not in the session's
|
|
63
|
+
* history, so each remembers how many session entries preceded it and is re-inserted there on sync.
|
|
64
|
+
*/
|
|
65
|
+
private localNotices: { readonly entry: IHistoryEntry; readonly afterSessionEntries: number }[] =
|
|
66
|
+
[];
|
|
67
|
+
streamingText = '';
|
|
68
|
+
activeTools: IToolState[] = [];
|
|
69
|
+
isThinking = false;
|
|
70
|
+
isAborting = false;
|
|
71
|
+
pendingPrompt: string | null = null;
|
|
72
|
+
contextState: TContextState = { percentage: 0, usedTokens: 0, maxTokens: 0 };
|
|
73
|
+
executionWorkspaceSnapshot: IExecutionWorkspaceSnapshot | null = null;
|
|
74
|
+
selectedExecutionEntryId: string | undefined;
|
|
75
|
+
/** ERR-001 G2: humanized message of the last failed turn; cleared when the next turn starts. */
|
|
76
|
+
lastErrorMessage: string | null = null;
|
|
77
|
+
/** ERR-001 G3: no stream/tool activity for STALL_HINT_MS while thinking. */
|
|
78
|
+
isStalled = false;
|
|
79
|
+
private readonly sessionNoticeStore = new TuiSessionNoticeStore();
|
|
80
|
+
/** Bounded render-only projection; never merged into canonical session history. */
|
|
81
|
+
readonly sessionEventNotices = this.sessionNoticeStore.notices;
|
|
82
|
+
|
|
83
|
+
/** Called after any state change. React hook sets this to trigger re-render. */
|
|
84
|
+
onChange: (() => void) | null = null;
|
|
85
|
+
|
|
86
|
+
// ── Internal ──────────────────────────────────────────────────
|
|
87
|
+
private streamBuf = '';
|
|
88
|
+
private debouncedStreamNotify = createDebouncedNotify(() => this.notify(), STREAMING_DEBOUNCE_MS);
|
|
89
|
+
private stallTimer: ReturnType<typeof setTimeout> | null = null;
|
|
90
|
+
|
|
91
|
+
/** (Re)start the dead-air timer — any provider activity proves the connection is alive. */
|
|
92
|
+
private armStallTimer(): void {
|
|
93
|
+
this.clearStallTimer();
|
|
94
|
+
this.stallTimer = setTimeout(() => {
|
|
95
|
+
this.stallTimer = null;
|
|
96
|
+
this.isStalled = true;
|
|
97
|
+
this.notify();
|
|
98
|
+
}, STALL_HINT_MS);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private clearStallTimer(): void {
|
|
102
|
+
if (this.stallTimer) {
|
|
103
|
+
clearTimeout(this.stallTimer);
|
|
104
|
+
this.stallTimer = null;
|
|
105
|
+
}
|
|
106
|
+
if (this.isStalled) {
|
|
107
|
+
this.isStalled = false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private notify(): void {
|
|
112
|
+
this.onChange?.();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── Event handlers (InteractiveSession → state) ───────────────
|
|
116
|
+
|
|
117
|
+
onTextDelta = (delta: string): void => {
|
|
118
|
+
if (this.isThinking) this.armStallTimer();
|
|
119
|
+
this.streamBuf += delta;
|
|
120
|
+
this.streamingText = this.streamBuf;
|
|
121
|
+
this.debouncedStreamNotify.schedule();
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
onToolStart = (state: IToolState): void => {
|
|
125
|
+
// RUNTIME-39: a running tool is legitimate activity, not a stalled provider connection —
|
|
126
|
+
// suppress the dead-air hint for the duration of the tool run (re-armed in onToolEnd).
|
|
127
|
+
this.clearStallTimer();
|
|
128
|
+
this.activeTools = [...this.activeTools, state];
|
|
129
|
+
this.notify();
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
onToolEnd = (state: IToolState): void => {
|
|
133
|
+
// findLastIndex: when same tool runs concurrently, match the most recently started instance
|
|
134
|
+
const idx = this.activeTools.findLastIndex((t) => t.toolName === state.toolName && t.isRunning);
|
|
135
|
+
if (idx !== -1) {
|
|
136
|
+
const updated = [...this.activeTools];
|
|
137
|
+
updated[idx] = state;
|
|
138
|
+
this.activeTools = updated;
|
|
139
|
+
}
|
|
140
|
+
// RUNTIME-39: re-arm the dead-air watch only once no tool is still running and the turn is
|
|
141
|
+
// still in progress — otherwise the provider is again the thing we're waiting on.
|
|
142
|
+
if (this.isThinking && !this.activeTools.some((t) => t.isRunning)) {
|
|
143
|
+
this.armStallTimer();
|
|
144
|
+
}
|
|
145
|
+
this.notify();
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
onThinking = (thinking: boolean): void => {
|
|
149
|
+
this.isThinking = thinking;
|
|
150
|
+
if (thinking) {
|
|
151
|
+
// Clear at START of new execution (preserves previous result until next)
|
|
152
|
+
this.debouncedStreamNotify.flush();
|
|
153
|
+
this.streamBuf = '';
|
|
154
|
+
this.streamingText = '';
|
|
155
|
+
this.activeTools = [];
|
|
156
|
+
this.lastErrorMessage = null;
|
|
157
|
+
this.armStallTimer();
|
|
158
|
+
} else {
|
|
159
|
+
this.isAborting = false;
|
|
160
|
+
this.activeTools = [];
|
|
161
|
+
this.clearStallTimer();
|
|
162
|
+
}
|
|
163
|
+
this.notify();
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
onComplete = (result: IExecutionResult): void => {
|
|
167
|
+
this.clearStallTimer();
|
|
168
|
+
this.debouncedStreamNotify.flush();
|
|
169
|
+
this.streamBuf = '';
|
|
170
|
+
this.streamingText = '';
|
|
171
|
+
this.activeTools = [];
|
|
172
|
+
this.contextState = {
|
|
173
|
+
percentage: result.contextState.usedPercentage,
|
|
174
|
+
usedTokens: result.contextState.usedTokens,
|
|
175
|
+
maxTokens: result.contextState.maxTokens,
|
|
176
|
+
};
|
|
177
|
+
this.notify();
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
onInterrupted = (): void => {
|
|
181
|
+
this.clearStallTimer();
|
|
182
|
+
this.debouncedStreamNotify.flush();
|
|
183
|
+
this.streamBuf = '';
|
|
184
|
+
this.streamingText = '';
|
|
185
|
+
this.activeTools = [];
|
|
186
|
+
this.notify();
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
onError = (error?: Error): void => {
|
|
190
|
+
// The partial answer is preserved as an interrupted history entry by the framework
|
|
191
|
+
// (ERR-001); clearing the local stream buffer here no longer loses it.
|
|
192
|
+
this.clearStallTimer();
|
|
193
|
+
this.lastErrorMessage = error?.message ?? 'Unknown error';
|
|
194
|
+
this.debouncedStreamNotify.flush();
|
|
195
|
+
this.streamBuf = '';
|
|
196
|
+
this.streamingText = '';
|
|
197
|
+
this.activeTools = [];
|
|
198
|
+
this.notify();
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
onContextUpdate = (state: IContextWindowState): void => {
|
|
202
|
+
this.setContextState({
|
|
203
|
+
percentage: state.usedPercentage,
|
|
204
|
+
usedTokens: state.usedTokens,
|
|
205
|
+
maxTokens: state.maxTokens,
|
|
206
|
+
});
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// ── State updates from external sources ───────────────────────
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Sync history from InteractiveSession — authoritative SSOT replace (SCREEN-010: no front-windowing).
|
|
213
|
+
* The session history grows monotonically during a session, so Ink `<Static>` (count-based) renders
|
|
214
|
+
* only the newly-appended tail; committed lines already in terminal scrollback are never re-printed.
|
|
215
|
+
*/
|
|
216
|
+
syncHistory(entries: IHistoryEntry[]): void {
|
|
217
|
+
if (entries.length === 0) return;
|
|
218
|
+
// `<Static>` counts what it already printed, so a local notice dropped here would shift every
|
|
219
|
+
// later index by one and the next committed entry (the assistant's answer) would never print.
|
|
220
|
+
const merged: IHistoryEntry[] = [];
|
|
221
|
+
let next = 0;
|
|
222
|
+
for (const notice of this.localNotices) {
|
|
223
|
+
const upTo = Math.min(notice.afterSessionEntries, entries.length);
|
|
224
|
+
while (next < upTo) merged.push(entries[next++]!);
|
|
225
|
+
merged.push(notice.entry);
|
|
226
|
+
}
|
|
227
|
+
while (next < entries.length) merged.push(entries[next++]!);
|
|
228
|
+
this.history = merged;
|
|
229
|
+
this.notify();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Add a notice this terminal owns; it survives the next session-history sync in place. */
|
|
233
|
+
addEntry(entry: IHistoryEntry): void {
|
|
234
|
+
this.localNotices.push({
|
|
235
|
+
entry,
|
|
236
|
+
afterSessionEntries: this.history.length - this.localNotices.length,
|
|
237
|
+
});
|
|
238
|
+
this.history = [...this.history, entry];
|
|
239
|
+
this.notify();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Echo the user's prompt at once; the session's own user entry replaces it on the next sync. */
|
|
243
|
+
addUserEcho(entry: IHistoryEntry): void {
|
|
244
|
+
this.history = [...this.history, entry];
|
|
245
|
+
this.notify();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
clearHistory(): void {
|
|
249
|
+
this.history = [];
|
|
250
|
+
this.localNotices = [];
|
|
251
|
+
this.debouncedStreamNotify.flush();
|
|
252
|
+
this.streamBuf = '';
|
|
253
|
+
this.streamingText = '';
|
|
254
|
+
this.activeTools = [];
|
|
255
|
+
this.notify();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Update pending prompt state */
|
|
259
|
+
setPendingPrompt(prompt: string | null): void {
|
|
260
|
+
this.pendingPrompt = prompt;
|
|
261
|
+
this.notify();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Set aborting flag */
|
|
265
|
+
setAborting(aborting: boolean): void {
|
|
266
|
+
this.isAborting = aborting;
|
|
267
|
+
this.notify();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Update context state */
|
|
271
|
+
setContextState(state: TContextState): void {
|
|
272
|
+
this.contextState = state;
|
|
273
|
+
this.notify();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
addSessionEventNotice(input: TTuiNoticeInput): void {
|
|
277
|
+
this.sessionNoticeStore.add(input);
|
|
278
|
+
this.notify();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
addAttentionRecap(line: string): void {
|
|
282
|
+
this.sessionNoticeStore.addAttentionRecap(line);
|
|
283
|
+
this.notify();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
addSessionEventDeliveryError(error: Error, event: TInteractiveEventName): void {
|
|
287
|
+
this.sessionNoticeStore.addDeliveryError(error, event);
|
|
288
|
+
this.notify();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
syncExecutionWorkspaceSnapshot(snapshot: IExecutionWorkspaceSnapshot): void {
|
|
292
|
+
const currentSelection = this.selectedExecutionEntryId;
|
|
293
|
+
const hasCurrentSelection =
|
|
294
|
+
currentSelection !== undefined &&
|
|
295
|
+
snapshot.entries.some((entry) => entry.id === currentSelection);
|
|
296
|
+
const selectedExecutionEntryId = hasCurrentSelection
|
|
297
|
+
? currentSelection
|
|
298
|
+
: (snapshot.selectedEntryId ?? snapshot.entries[0]?.id);
|
|
299
|
+
this.executionWorkspaceSnapshot = {
|
|
300
|
+
...snapshot,
|
|
301
|
+
...(selectedExecutionEntryId ? { selectedEntryId: selectedExecutionEntryId } : {}),
|
|
302
|
+
};
|
|
303
|
+
this.selectedExecutionEntryId = selectedExecutionEntryId;
|
|
304
|
+
this.notify();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
selectExecutionWorkspaceEntry(entryId: string): void {
|
|
308
|
+
if (!this.executionWorkspaceSnapshot?.entries.some((entry) => entry.id === entryId)) return;
|
|
309
|
+
this.selectedExecutionEntryId = entryId;
|
|
310
|
+
this.executionWorkspaceSnapshot = {
|
|
311
|
+
...this.executionWorkspaceSnapshot,
|
|
312
|
+
selectedEntryId: entryId,
|
|
313
|
+
};
|
|
314
|
+
this.notify();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* RUNTIME-52: release every timer this manager owns and detach the render callback. Called by the
|
|
319
|
+
* channel's teardown (`stop()`); after dispose the manager must never fire `onChange` or trip the
|
|
320
|
+
* stall hint on a discarded session.
|
|
321
|
+
*/
|
|
322
|
+
dispose(): void {
|
|
323
|
+
this.clearStallTimer();
|
|
324
|
+
this.debouncedStreamNotify.flush();
|
|
325
|
+
this.onChange = null;
|
|
326
|
+
}
|
|
327
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** UI-layer permission types for the Ink TUI */
|
|
2
|
+
|
|
3
|
+
import type { TToolArgs } from '@robota-sdk/agent-core';
|
|
4
|
+
import type { TPermissionResultValue } from '@robota-sdk/agent-interface-session';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Permission result: true (allow once), false (deny), 'allow-session' (remember for session),
|
|
8
|
+
* or 'allow-project' (persist through the host-selected project settings writer).
|
|
9
|
+
*/
|
|
10
|
+
export type TPermissionResult = TPermissionResultValue; // issue #2052: owned by agent-interface-session
|
|
11
|
+
|
|
12
|
+
export interface IPendingPermissionRequest {
|
|
13
|
+
toolName: string;
|
|
14
|
+
toolArgs: TToolArgs;
|
|
15
|
+
canPersistProjectPermission?: boolean;
|
|
16
|
+
/** The peer session whose message this call serves, when a peer turn asked. */
|
|
17
|
+
requestedByPeer?: string;
|
|
18
|
+
resolve: (result: TPermissionResult) => void;
|
|
19
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
import { useProductDisplayName } from './product-display-name-context.js';
|
|
4
|
+
import { sanitizeTerminalText } from './sanitize-terminal-text.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Set the terminal's window title from the session name.
|
|
8
|
+
*
|
|
9
|
+
* Split out of `App.tsx` under SEC-019 (issue #2022) by responsibility: this is the one place the TUI
|
|
10
|
+
* writes an escape sequence to stdout directly rather than through Ink, which makes it the one place
|
|
11
|
+
* where a value is interpolated INTO terminal syntax rather than rendered as content. Keeping it
|
|
12
|
+
* inside a 600-line component is what let it sit unexamined next to code that never touches an
|
|
13
|
+
* escape.
|
|
14
|
+
*
|
|
15
|
+
* `sessionName` is untrusted — a session can be named from a prompt, and a name containing BEL or ESC
|
|
16
|
+
* terminates the OSC early, so everything after it is read by the terminal as its own command.
|
|
17
|
+
*
|
|
18
|
+
* Both the host-selected product name and the session name are sanitized separately from the
|
|
19
|
+
* `\x1b]0;` … `\x07` framing this module deliberately writes.
|
|
20
|
+
*/
|
|
21
|
+
export function useTerminalTitle(sessionName: string | undefined): void {
|
|
22
|
+
const productDisplayName = useProductDisplayName();
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const safeName = sanitizeTerminalText(sessionName ?? '');
|
|
25
|
+
const safeProductName = sanitizeTerminalText(productDisplayName);
|
|
26
|
+
const title = safeName ? `${safeProductName} — ${safeName}` : safeProductName;
|
|
27
|
+
process.stdout.write(`\x1b]0;${title}\x07`);
|
|
28
|
+
}, [sessionName, productDisplayName]);
|
|
29
|
+
}
|