@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,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-select checklist (CMD-004): arrow-key navigation, Space toggles, Enter confirms once the
|
|
3
|
+
* selection count is within [minSelect, maxSelect], Esc cancels. Used by PendingActionPrompt for an
|
|
4
|
+
* IActionRequest whose `maxSelect` > 1.
|
|
5
|
+
*
|
|
6
|
+
* Note: unlike ListPicker, this renders all options without a scroll viewport — `IActionRequest`'s
|
|
7
|
+
* optional `maxVisible` hint is not yet honored here (multi-select lists are typically short). Adding a
|
|
8
|
+
* viewport is a follow-up if long multi-select lists appear.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Box } from 'ink';
|
|
12
|
+
import React, { useState } from 'react';
|
|
13
|
+
|
|
14
|
+
import { useNumberedSelection } from './hooks/useNumberedSelection.js';
|
|
15
|
+
import {
|
|
16
|
+
KeyHintFooter,
|
|
17
|
+
SELECTION_INDICATOR,
|
|
18
|
+
SELECTION_INDICATOR_NONE,
|
|
19
|
+
type IKeyHint,
|
|
20
|
+
} from './key-hint-footer.js';
|
|
21
|
+
import { useKeybindingActions, useKeybindingHints } from './keybindings/keybindings-context.js';
|
|
22
|
+
import { NumberedSelectionPrompt, numberedRowPrefix } from './numbered-list.js';
|
|
23
|
+
import { Text } from './SafeText.js';
|
|
24
|
+
import { useScreenReader } from './screen-reader-context.js';
|
|
25
|
+
import { usePalette } from './theme/index.js';
|
|
26
|
+
|
|
27
|
+
import type { IActionOption } from '@robota-sdk/agent-core';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Footer hints for the multi-select checklist. The Enter hint carries a dynamic `(min N)` segment
|
|
31
|
+
* until the selection is confirmable.
|
|
32
|
+
*/
|
|
33
|
+
export function getMultiSelectFooterHints(input: {
|
|
34
|
+
canConfirm: boolean;
|
|
35
|
+
minSelect: number;
|
|
36
|
+
}): readonly IKeyHint[] {
|
|
37
|
+
return [
|
|
38
|
+
{ keys: '↑↓', label: 'Navigate' },
|
|
39
|
+
{ keys: 'Space', label: 'Toggle' },
|
|
40
|
+
{ keys: 'Enter', label: input.canConfirm ? 'Confirm' : `Confirm (min ${input.minSelect})` },
|
|
41
|
+
{ keys: 'Esc', label: 'Cancel' },
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* One checklist row. The mode swaps the position cue — a spoken number for the `> ` cursor and its
|
|
47
|
+
* colour — and nothing else: the checkbox is what says selected, in both.
|
|
48
|
+
*/
|
|
49
|
+
function ChecklistRow({
|
|
50
|
+
label,
|
|
51
|
+
index,
|
|
52
|
+
isCursor,
|
|
53
|
+
isChecked,
|
|
54
|
+
screenReader,
|
|
55
|
+
}: {
|
|
56
|
+
label: string;
|
|
57
|
+
index: number;
|
|
58
|
+
isCursor: boolean;
|
|
59
|
+
isChecked: boolean;
|
|
60
|
+
screenReader: boolean;
|
|
61
|
+
}): React.ReactElement {
|
|
62
|
+
const palette = usePalette();
|
|
63
|
+
const box = isChecked ? '[x] ' : '[ ] ';
|
|
64
|
+
if (screenReader) {
|
|
65
|
+
return (
|
|
66
|
+
<Text>
|
|
67
|
+
{numberedRowPrefix(index)}
|
|
68
|
+
{box}
|
|
69
|
+
{label}
|
|
70
|
+
</Text>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return (
|
|
74
|
+
<Text color={isCursor ? palette.text.accent : undefined}>
|
|
75
|
+
{isCursor ? SELECTION_INDICATOR : SELECTION_INDICATOR_NONE}
|
|
76
|
+
{box}
|
|
77
|
+
{label}
|
|
78
|
+
</Text>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface IMultiSelectListProps {
|
|
83
|
+
title: string;
|
|
84
|
+
description?: string;
|
|
85
|
+
options: readonly IActionOption[];
|
|
86
|
+
minSelect: number;
|
|
87
|
+
maxSelect: number;
|
|
88
|
+
defaultValues?: readonly string[];
|
|
89
|
+
onConfirm: (values: string[]) => void;
|
|
90
|
+
onCancel: () => void;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The arrow-key driver: cursor motion, Space toggles, Enter confirms. Inactive in the mode. */
|
|
94
|
+
function useChecklistArrowKeys(inputs: {
|
|
95
|
+
active: boolean;
|
|
96
|
+
itemCount: number;
|
|
97
|
+
setCursor: React.Dispatch<React.SetStateAction<number>>;
|
|
98
|
+
onToggleAtCursor: () => void;
|
|
99
|
+
onConfirm: () => void;
|
|
100
|
+
onCancel: () => void;
|
|
101
|
+
}): void {
|
|
102
|
+
const { active, itemCount, setCursor } = inputs;
|
|
103
|
+
useKeybindingActions(
|
|
104
|
+
'multi-select',
|
|
105
|
+
(actions) => {
|
|
106
|
+
for (const action of actions) {
|
|
107
|
+
if (action === 'previous') {
|
|
108
|
+
setCursor((c) => (c <= 0 ? itemCount - 1 : c - 1));
|
|
109
|
+
} else if (action === 'next') {
|
|
110
|
+
setCursor((c) => (c >= itemCount - 1 ? 0 : c + 1));
|
|
111
|
+
} else if (action === 'toggle') {
|
|
112
|
+
inputs.onToggleAtCursor();
|
|
113
|
+
} else if (action === 'confirm') {
|
|
114
|
+
inputs.onConfirm();
|
|
115
|
+
} else if (action === 'cancel') {
|
|
116
|
+
inputs.onCancel();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
{ isActive: active },
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** How the checklist asks for an answer: a spoken range in the mode, the key hints otherwise. */
|
|
125
|
+
function ChecklistAnswer(props: {
|
|
126
|
+
screenReader: boolean;
|
|
127
|
+
itemCount: number;
|
|
128
|
+
numbered: { buffer: string; invalid: boolean };
|
|
129
|
+
canConfirm: boolean;
|
|
130
|
+
minSelect: number;
|
|
131
|
+
}): React.ReactElement {
|
|
132
|
+
const hints = useKeybindingHints('multi-select', [
|
|
133
|
+
[['previous', 'next'], 'Navigate'],
|
|
134
|
+
['toggle', 'Toggle'],
|
|
135
|
+
['confirm', props.canConfirm ? 'Confirm' : `Confirm (min ${props.minSelect})`],
|
|
136
|
+
['cancel', 'Cancel'],
|
|
137
|
+
]);
|
|
138
|
+
if (props.screenReader) {
|
|
139
|
+
return (
|
|
140
|
+
<NumberedSelectionPrompt
|
|
141
|
+
itemCount={props.itemCount}
|
|
142
|
+
cancellable
|
|
143
|
+
buffer={props.numbered.buffer}
|
|
144
|
+
invalid={props.numbered.invalid}
|
|
145
|
+
/>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return <KeyHintFooter hints={hints} />;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** The rendered checklist: the rows, and whichever answer affordance the mode calls for. */
|
|
152
|
+
function ChecklistView(props: {
|
|
153
|
+
title: string;
|
|
154
|
+
description?: string;
|
|
155
|
+
options: readonly IActionOption[];
|
|
156
|
+
cursor: number;
|
|
157
|
+
selected: ReadonlySet<string>;
|
|
158
|
+
screenReader: boolean;
|
|
159
|
+
canConfirm: boolean;
|
|
160
|
+
minSelect: number;
|
|
161
|
+
numbered: { buffer: string; invalid: boolean };
|
|
162
|
+
}): React.ReactElement {
|
|
163
|
+
const palette = usePalette();
|
|
164
|
+
const { title, description, options, cursor, selected, screenReader, numbered } = props;
|
|
165
|
+
return (
|
|
166
|
+
<Box
|
|
167
|
+
flexDirection="column"
|
|
168
|
+
{...(screenReader
|
|
169
|
+
? {}
|
|
170
|
+
: { borderStyle: 'round' as const, borderColor: palette.border.attention })}
|
|
171
|
+
paddingX={1}
|
|
172
|
+
>
|
|
173
|
+
<Text color={palette.text.warning} bold>
|
|
174
|
+
{title}
|
|
175
|
+
</Text>
|
|
176
|
+
{description !== undefined && description.length > 0 && <Text dimColor>{description}</Text>}
|
|
177
|
+
{options.map((option, index) => (
|
|
178
|
+
<ChecklistRow
|
|
179
|
+
key={option.value}
|
|
180
|
+
label={option.label}
|
|
181
|
+
index={index}
|
|
182
|
+
isCursor={index === cursor}
|
|
183
|
+
isChecked={selected.has(option.value)}
|
|
184
|
+
screenReader={screenReader}
|
|
185
|
+
/>
|
|
186
|
+
))}
|
|
187
|
+
<ChecklistAnswer
|
|
188
|
+
screenReader={screenReader}
|
|
189
|
+
itemCount={options.length}
|
|
190
|
+
numbered={numbered}
|
|
191
|
+
canConfirm={props.canConfirm}
|
|
192
|
+
minSelect={props.minSelect}
|
|
193
|
+
/>
|
|
194
|
+
</Box>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** The checklist's own state: which row the cursor is on, what is ticked, and how it commits. */
|
|
199
|
+
function useChecklistSelection(inputs: {
|
|
200
|
+
options: readonly IActionOption[];
|
|
201
|
+
minSelect: number;
|
|
202
|
+
maxSelect: number;
|
|
203
|
+
defaultValues?: readonly string[];
|
|
204
|
+
onConfirm: (values: string[]) => void;
|
|
205
|
+
}): {
|
|
206
|
+
cursor: number;
|
|
207
|
+
setCursor: React.Dispatch<React.SetStateAction<number>>;
|
|
208
|
+
selected: ReadonlySet<string>;
|
|
209
|
+
toggleAt: (index: number) => void;
|
|
210
|
+
confirmIfAllowed: () => void;
|
|
211
|
+
} {
|
|
212
|
+
const { options, minSelect, maxSelect, defaultValues, onConfirm } = inputs;
|
|
213
|
+
const [cursor, setCursor] = useState(0);
|
|
214
|
+
const [selected, setSelected] = useState<ReadonlySet<string>>(() => new Set(defaultValues ?? []));
|
|
215
|
+
|
|
216
|
+
const toggleAt = (index: number): void => {
|
|
217
|
+
const option = options[index];
|
|
218
|
+
if (option === undefined) return;
|
|
219
|
+
setSelected((prev) => {
|
|
220
|
+
const next = new Set(prev);
|
|
221
|
+
if (next.has(option.value)) {
|
|
222
|
+
next.delete(option.value);
|
|
223
|
+
} else if (next.size < maxSelect) {
|
|
224
|
+
next.add(option.value);
|
|
225
|
+
}
|
|
226
|
+
return next;
|
|
227
|
+
});
|
|
228
|
+
};
|
|
229
|
+
const confirmIfAllowed = (): void => {
|
|
230
|
+
if (selected.size >= minSelect) onConfirm([...selected]);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
return { cursor, setCursor, selected, toggleAt, confirmIfAllowed };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export default function MultiSelectList({
|
|
237
|
+
title,
|
|
238
|
+
description,
|
|
239
|
+
options,
|
|
240
|
+
minSelect,
|
|
241
|
+
maxSelect,
|
|
242
|
+
defaultValues,
|
|
243
|
+
onConfirm,
|
|
244
|
+
onCancel,
|
|
245
|
+
}: IMultiSelectListProps): React.ReactElement {
|
|
246
|
+
// CLI-2004: the checklist keeps its own reducer (Space toggles, Enter confirms) — what the mode
|
|
247
|
+
// changes is the ROW: a spoken number replaces the `> ` cursor, and typing that number toggles it.
|
|
248
|
+
const screenReader = useScreenReader();
|
|
249
|
+
const { cursor, setCursor, selected, toggleAt, confirmIfAllowed } = useChecklistSelection({
|
|
250
|
+
options,
|
|
251
|
+
minSelect,
|
|
252
|
+
maxSelect,
|
|
253
|
+
defaultValues,
|
|
254
|
+
onConfirm,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// CLI-2004: the checklist answers by number in the mode, through the SAME reducer every other
|
|
258
|
+
// numbered menu uses — `multi` is what keeps it open after a toggle and makes an empty Enter the
|
|
259
|
+
// commit. A local digit branch would have capped it at nine rows and skipped the shared prompt.
|
|
260
|
+
const numbered = useNumberedSelection({
|
|
261
|
+
enabled: screenReader,
|
|
262
|
+
itemCount: options.length,
|
|
263
|
+
cancellable: true,
|
|
264
|
+
multi: true,
|
|
265
|
+
onSelect: toggleAt,
|
|
266
|
+
onConfirm: confirmIfAllowed,
|
|
267
|
+
onCancel,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
useChecklistArrowKeys({
|
|
271
|
+
active: !screenReader,
|
|
272
|
+
itemCount: options.length,
|
|
273
|
+
setCursor,
|
|
274
|
+
onToggleAtCursor: () => toggleAt(cursor),
|
|
275
|
+
onConfirm: confirmIfAllowed,
|
|
276
|
+
onCancel,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const canConfirm = selected.size >= minSelect;
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
<ChecklistView
|
|
283
|
+
title={title}
|
|
284
|
+
{...(description !== undefined ? { description } : {})}
|
|
285
|
+
options={options}
|
|
286
|
+
cursor={cursor}
|
|
287
|
+
selected={selected}
|
|
288
|
+
screenReader={screenReader}
|
|
289
|
+
canConfirm={canConfirm}
|
|
290
|
+
minSelect={minSelect}
|
|
291
|
+
numbered={numbered}
|
|
292
|
+
/>
|
|
293
|
+
);
|
|
294
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PendingActionPrompt (CMD-004) — the TUI renderer for the unified `IActionRequest`. It derives the
|
|
3
|
+
* presentation from the request's fields (not a kind switch):
|
|
4
|
+
* - no options → free-text (TextPrompt, honoring `masked`/`placeholder`/`allowEmpty`)
|
|
5
|
+
* - options, `maxSelect` > 1 → multi-select checklist (MultiSelectList)
|
|
6
|
+
* - options, `maxSelect` <= 1 → single-select list (ListPicker); with `allowFreeText`, a synthetic
|
|
7
|
+
* "Type a custom answer…" entry switches to a text field
|
|
8
|
+
*
|
|
9
|
+
* It always resolves the caller's `onAnswer` with a `TActionResponse` (answer or cancelled).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Box } from 'ink';
|
|
13
|
+
import React, { useState } from 'react';
|
|
14
|
+
|
|
15
|
+
import { SELECTION_INDICATOR, SELECTION_INDICATOR_NONE } from './key-hint-footer.js';
|
|
16
|
+
import ListPicker from './ListPicker.js';
|
|
17
|
+
import MultiSelectList from './MultiSelectList.js';
|
|
18
|
+
import { Text } from './SafeText.js';
|
|
19
|
+
import TextPrompt from './TextPrompt.js';
|
|
20
|
+
import { usePalette } from './theme/index.js';
|
|
21
|
+
|
|
22
|
+
import type { IActionOption, IActionRequest, TActionResponse } from '@robota-sdk/agent-core';
|
|
23
|
+
|
|
24
|
+
interface IPendingActionPromptProps {
|
|
25
|
+
request: IActionRequest;
|
|
26
|
+
onAnswer: (response: TActionResponse) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Synthetic "type a custom answer" entry for a single-select with free text. Routed by object identity
|
|
31
|
+
* (reference equality), not by a magic value — so it can never collide with a real option's `value`.
|
|
32
|
+
*/
|
|
33
|
+
const FREE_TEXT_OPTION: IActionOption = { value: '', label: 'Type a custom answer…' };
|
|
34
|
+
|
|
35
|
+
export default function PendingActionPrompt({
|
|
36
|
+
request,
|
|
37
|
+
onAnswer,
|
|
38
|
+
}: IPendingActionPromptProps): React.ReactElement {
|
|
39
|
+
const palette = usePalette();
|
|
40
|
+
const [freeTextMode, setFreeTextMode] = useState(false);
|
|
41
|
+
|
|
42
|
+
const options = request.options ?? [];
|
|
43
|
+
const hasOptions = options.length > 0;
|
|
44
|
+
const maxSelect = request.maxSelect ?? 1;
|
|
45
|
+
const cancel = (): void => onAnswer({ type: 'cancelled' });
|
|
46
|
+
|
|
47
|
+
// Pure free-text (no options) — or the user chose "type a custom answer" on a single-select.
|
|
48
|
+
if (!hasOptions || freeTextMode) {
|
|
49
|
+
return (
|
|
50
|
+
<TextPrompt
|
|
51
|
+
key={`text:${request.id}`}
|
|
52
|
+
title={request.title}
|
|
53
|
+
description={request.description}
|
|
54
|
+
placeholder={request.placeholder}
|
|
55
|
+
allowEmpty={request.allowEmpty}
|
|
56
|
+
masked={request.masked}
|
|
57
|
+
onSubmit={(value) => onAnswer({ type: 'answer', values: [], text: value })}
|
|
58
|
+
onCancel={freeTextMode ? () => setFreeTextMode(false) : cancel}
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Multi-select.
|
|
64
|
+
if (maxSelect > 1) {
|
|
65
|
+
return (
|
|
66
|
+
<MultiSelectList
|
|
67
|
+
title={request.title}
|
|
68
|
+
description={request.description}
|
|
69
|
+
options={options}
|
|
70
|
+
minSelect={request.minSelect ?? 1}
|
|
71
|
+
maxSelect={maxSelect}
|
|
72
|
+
defaultValues={request.default?.values}
|
|
73
|
+
onConfirm={(values) => onAnswer({ type: 'answer', values })}
|
|
74
|
+
onCancel={cancel}
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Single-select, optionally with a "type your own" entry (routed by identity, see FREE_TEXT_OPTION).
|
|
80
|
+
const pickerItems: IActionOption[] = request.allowFreeText
|
|
81
|
+
? [...options, FREE_TEXT_OPTION]
|
|
82
|
+
: [...options];
|
|
83
|
+
// BEHAVIOR-2437: a request's declared default is where the highlight STARTS — for a write
|
|
84
|
+
// confirmation built with `defaultYes: false`, a bare Enter must answer No, not Yes.
|
|
85
|
+
const defaultValue = request.default?.values?.[0];
|
|
86
|
+
const initialIndex =
|
|
87
|
+
defaultValue === undefined
|
|
88
|
+
? 0
|
|
89
|
+
: Math.max(
|
|
90
|
+
0,
|
|
91
|
+
pickerItems.findIndex((option) => option.value === defaultValue),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<Box flexDirection="column">
|
|
96
|
+
<Text bold>{request.title}</Text>
|
|
97
|
+
{request.description !== undefined && request.description.length > 0 && (
|
|
98
|
+
<Text dimColor>{request.description}</Text>
|
|
99
|
+
)}
|
|
100
|
+
<ListPicker<IActionOption>
|
|
101
|
+
items={pickerItems}
|
|
102
|
+
initialIndex={initialIndex}
|
|
103
|
+
maxVisible={request.maxVisible}
|
|
104
|
+
renderItem={(option, isSelected) => (
|
|
105
|
+
<Text color={isSelected ? palette.text.accent : undefined}>
|
|
106
|
+
{isSelected ? SELECTION_INDICATOR : SELECTION_INDICATOR_NONE}
|
|
107
|
+
{option.label}
|
|
108
|
+
</Text>
|
|
109
|
+
)}
|
|
110
|
+
onSelect={(option) => {
|
|
111
|
+
if (option === FREE_TEXT_OPTION) {
|
|
112
|
+
setFreeTextMode(true);
|
|
113
|
+
} else {
|
|
114
|
+
onAnswer({ type: 'answer', values: [option.value] });
|
|
115
|
+
}
|
|
116
|
+
}}
|
|
117
|
+
onCancel={cancel}
|
|
118
|
+
/>
|
|
119
|
+
</Box>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { consentScopeFor } from '@robota-sdk/agent-framework';
|
|
2
|
+
import { Box } from 'ink';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
applyPermissionPromptInput,
|
|
7
|
+
permissionPromptOptionsFor,
|
|
8
|
+
type TPermissionPromptInputAction,
|
|
9
|
+
} from './flows/permission-prompt-flow.js';
|
|
10
|
+
import { createSelectionFlowState, type ISelectionFlowState } from './flows/selection-flow.js';
|
|
11
|
+
import { useNumberedSelection } from './hooks/useNumberedSelection.js';
|
|
12
|
+
import {
|
|
13
|
+
KeyHintFooter,
|
|
14
|
+
SELECTION_INDICATOR,
|
|
15
|
+
SELECTION_INDICATOR_NONE,
|
|
16
|
+
type IKeyHint,
|
|
17
|
+
} from './key-hint-footer.js';
|
|
18
|
+
import { useKeybindingActions, useKeybindingHints } from './keybindings/keybindings-context.js';
|
|
19
|
+
import { NumberedList } from './numbered-list.js';
|
|
20
|
+
import { Text } from './SafeText.js';
|
|
21
|
+
import { useScreenReader } from './screen-reader-context.js';
|
|
22
|
+
import { SCREEN_READER_LABELS } from './screen-reader-labels.js';
|
|
23
|
+
import { usePalette } from './theme/index.js';
|
|
24
|
+
|
|
25
|
+
import type { IPendingPermissionRequest } from './types.js';
|
|
26
|
+
import type { TToolArgs } from '@robota-sdk/agent-core';
|
|
27
|
+
|
|
28
|
+
interface IProps {
|
|
29
|
+
request: IPendingPermissionRequest;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Footer for the permission prompt — identical to the confirm prompt's (same horizontal row, same
|
|
34
|
+
* reducer). Esc is deliberately suppressed by the flow (`escape: false` — a permission ask must
|
|
35
|
+
* resolve explicitly; Esc-dismissal would be an implicit deny), so the footer omits it.
|
|
36
|
+
*/
|
|
37
|
+
export const PERMISSION_PROMPT_FOOTER_HINTS: readonly IKeyHint[] = [
|
|
38
|
+
{ keys: '←→', label: 'Navigate' },
|
|
39
|
+
{ keys: 'Enter', label: 'Confirm' },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
function formatArgs(args: TToolArgs): string {
|
|
43
|
+
const entries = Object.entries(args);
|
|
44
|
+
if (entries.length === 0) return '(no arguments)';
|
|
45
|
+
return entries
|
|
46
|
+
.map(([k, v]) => `${k}: ${typeof v === 'string' ? v : JSON.stringify(v)}`)
|
|
47
|
+
.join(', ');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default function PermissionPrompt({ request }: IProps): React.ReactElement {
|
|
51
|
+
const palette = usePalette();
|
|
52
|
+
const canPersistProjectPermission = request.canPersistProjectPermission !== false;
|
|
53
|
+
const [state, setState] = React.useState<ISelectionFlowState>(() => createSelectionFlowState());
|
|
54
|
+
const stateRef = React.useRef(state);
|
|
55
|
+
const prevRequestRef = React.useRef(request);
|
|
56
|
+
|
|
57
|
+
if (prevRequestRef.current !== request) {
|
|
58
|
+
prevRequestRef.current = request;
|
|
59
|
+
const nextState = createSelectionFlowState();
|
|
60
|
+
stateRef.current = nextState;
|
|
61
|
+
setState(nextState);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const applyAction = React.useCallback(
|
|
65
|
+
(action: TPermissionPromptInputAction): void => {
|
|
66
|
+
const result = applyPermissionPromptInput(
|
|
67
|
+
stateRef.current,
|
|
68
|
+
action,
|
|
69
|
+
canPersistProjectPermission,
|
|
70
|
+
);
|
|
71
|
+
stateRef.current = result.state;
|
|
72
|
+
setState(result.state);
|
|
73
|
+
if (result.effect.type === 'resolve') {
|
|
74
|
+
request.resolve(result.effect.decision);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
[request, canPersistProjectPermission],
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const screenReader = useScreenReader();
|
|
81
|
+
const options = permissionPromptOptionsFor(
|
|
82
|
+
consentScopeFor(request.toolName, request.toolArgs),
|
|
83
|
+
canPersistProjectPermission,
|
|
84
|
+
);
|
|
85
|
+
const numbered = useNumberedSelection({
|
|
86
|
+
enabled: screenReader,
|
|
87
|
+
itemCount: options.length,
|
|
88
|
+
onSelect: (index) => {
|
|
89
|
+
const result = applyPermissionPromptInput(
|
|
90
|
+
stateRef.current,
|
|
91
|
+
{
|
|
92
|
+
type: 'shortcut',
|
|
93
|
+
index,
|
|
94
|
+
},
|
|
95
|
+
canPersistProjectPermission,
|
|
96
|
+
);
|
|
97
|
+
stateRef.current = result.state;
|
|
98
|
+
setState(result.state);
|
|
99
|
+
if (result.effect.type === 'resolve') request.resolve(result.effect.decision);
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
useKeybindingActions(
|
|
104
|
+
'permission-prompt',
|
|
105
|
+
(actions) => {
|
|
106
|
+
const shortcutIndex = {
|
|
107
|
+
'allow-once': 0,
|
|
108
|
+
'allow-session': 1,
|
|
109
|
+
'allow-project': 2,
|
|
110
|
+
deny: 3,
|
|
111
|
+
} as const;
|
|
112
|
+
for (const action of actions) {
|
|
113
|
+
if (action in shortcutIndex) {
|
|
114
|
+
applyAction({
|
|
115
|
+
type: 'shortcut',
|
|
116
|
+
index: shortcutIndex[action as keyof typeof shortcutIndex],
|
|
117
|
+
});
|
|
118
|
+
} else if (action === 'previous' || action === 'next' || action === 'confirm') {
|
|
119
|
+
applyAction(action === 'confirm' ? 'select' : action);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
{ isActive: !screenReader },
|
|
124
|
+
);
|
|
125
|
+
const footerHints = useKeybindingHints('permission-prompt', [
|
|
126
|
+
[['previous', 'next'], 'Navigate'],
|
|
127
|
+
['confirm', 'Confirm'],
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
if (screenReader) {
|
|
131
|
+
return (
|
|
132
|
+
<NumberedList
|
|
133
|
+
title={SCREEN_READER_LABELS.permissionRequired}
|
|
134
|
+
description={`${request.toolName} — ${formatArgs(request.toolArgs)}${
|
|
135
|
+
request.requestedByPeer !== undefined
|
|
136
|
+
? ` (requested by another session: ${request.requestedByPeer})`
|
|
137
|
+
: ''
|
|
138
|
+
}`}
|
|
139
|
+
options={options}
|
|
140
|
+
buffer={numbered.buffer}
|
|
141
|
+
invalid={numbered.invalid}
|
|
142
|
+
/>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<Box
|
|
148
|
+
flexDirection="column"
|
|
149
|
+
borderStyle="round"
|
|
150
|
+
borderColor={palette.border.attention}
|
|
151
|
+
paddingX={1}
|
|
152
|
+
>
|
|
153
|
+
<Text color={palette.text.warning} bold>
|
|
154
|
+
[Permission Required]
|
|
155
|
+
</Text>
|
|
156
|
+
<Text>
|
|
157
|
+
Tool:{' '}
|
|
158
|
+
<Text color={palette.text.accent} bold>
|
|
159
|
+
{request.toolName}
|
|
160
|
+
</Text>
|
|
161
|
+
</Text>
|
|
162
|
+
<Text dimColor> {formatArgs(request.toolArgs)}</Text>
|
|
163
|
+
{request.requestedByPeer !== undefined ? (
|
|
164
|
+
<Text color={palette.text.warning}>
|
|
165
|
+
Requested by another session: {request.requestedByPeer}
|
|
166
|
+
</Text>
|
|
167
|
+
) : null}
|
|
168
|
+
{/* Issue #2351: the "always" options carry the consent scope, so they no longer fit one row
|
|
169
|
+
of the prompt box — a row wrapped `Allow [y]` across two lines. One option per line. */}
|
|
170
|
+
<Box marginTop={1} flexDirection="column">
|
|
171
|
+
{options.map((opt, i) => (
|
|
172
|
+
<Box key={opt}>
|
|
173
|
+
<Text
|
|
174
|
+
color={i === state.selectedIndex ? palette.text.accent : undefined}
|
|
175
|
+
bold={i === state.selectedIndex}
|
|
176
|
+
>
|
|
177
|
+
{i === state.selectedIndex ? SELECTION_INDICATOR : SELECTION_INDICATOR_NONE}
|
|
178
|
+
{opt}
|
|
179
|
+
</Text>
|
|
180
|
+
</Box>
|
|
181
|
+
))}
|
|
182
|
+
</Box>
|
|
183
|
+
<KeyHintFooter hints={footerHints} />
|
|
184
|
+
</Box>
|
|
185
|
+
);
|
|
186
|
+
}
|