@robota-sdk/agent-transport 3.0.0-beta.64
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 +21 -0
- package/dist/node/headless/index.cjs +1 -0
- package/dist/node/headless/index.d.ts +2 -0
- package/dist/node/headless/index.js +1 -0
- package/dist/node/headless-CWEpJXFK.js +7 -0
- package/dist/node/headless-CWEpJXFK.js.map +1 -0
- package/dist/node/headless-CsZFelG9.cjs +6 -0
- package/dist/node/http/index.cjs +1 -0
- package/dist/node/http/index.d.ts +2 -0
- package/dist/node/http/index.js +1 -0
- package/dist/node/http-CM3TJhrF.cjs +1 -0
- package/dist/node/http-DwO1AHG-.js +2 -0
- package/dist/node/http-DwO1AHG-.js.map +1 -0
- package/dist/node/index--Ti9NzQX.d.ts +64 -0
- package/dist/node/index--Ti9NzQX.d.ts.map +1 -0
- package/dist/node/index-B_rcr14p.d.ts +47 -0
- package/dist/node/index-B_rcr14p.d.ts.map +1 -0
- package/dist/node/index-C9LWCL4l.d.ts +34 -0
- package/dist/node/index-C9LWCL4l.d.ts.map +1 -0
- package/dist/node/index-CAr3ioVh.d.ts +64 -0
- package/dist/node/index-CAr3ioVh.d.ts.map +1 -0
- package/dist/node/index-CEs25wVk.d.ts +213 -0
- package/dist/node/index-CEs25wVk.d.ts.map +1 -0
- package/dist/node/index-CvXLpjJO.d.ts +213 -0
- package/dist/node/index-CvXLpjJO.d.ts.map +1 -0
- package/dist/node/index-D34WUfFH.d.ts +26 -0
- package/dist/node/index-D34WUfFH.d.ts.map +1 -0
- package/dist/node/index-Y0zHb1Bz.d.ts +47 -0
- package/dist/node/index-Y0zHb1Bz.d.ts.map +1 -0
- package/dist/node/index-k3TUjA-T.d.ts +26 -0
- package/dist/node/index-k3TUjA-T.d.ts.map +1 -0
- package/dist/node/index-nBlMTFkZ.d.ts +34 -0
- package/dist/node/index-nBlMTFkZ.d.ts.map +1 -0
- package/dist/node/index.cjs +1 -0
- package/dist/node/index.d.ts +6 -0
- package/dist/node/index.js +1 -0
- package/dist/node/mcp/index.cjs +1 -0
- package/dist/node/mcp/index.d.ts +2 -0
- package/dist/node/mcp/index.js +1 -0
- package/dist/node/mcp-BXBwF6Wu.js +2 -0
- package/dist/node/mcp-BXBwF6Wu.js.map +1 -0
- package/dist/node/mcp-DcHuGokt.cjs +1 -0
- package/dist/node/tui/index.cjs +1 -0
- package/dist/node/tui/index.d.ts +2 -0
- package/dist/node/tui/index.js +1 -0
- package/dist/node/tui-CeD_6rSo.cjs +24 -0
- package/dist/node/tui-zmDTPk4b.js +25 -0
- package/dist/node/tui-zmDTPk4b.js.map +1 -0
- package/dist/node/ws/index.cjs +1 -0
- package/dist/node/ws/index.d.ts +2 -0
- package/dist/node/ws/index.js +1 -0
- package/dist/node/ws-B-oRccFl.js +2 -0
- package/dist/node/ws-B-oRccFl.js.map +1 -0
- package/dist/node/ws-COnIgnmn.cjs +1 -0
- package/package.json +141 -0
- package/src/headless/__tests__/headless-runner-initialization.test.ts +45 -0
- package/src/headless/__tests__/headless-runner.test.ts +484 -0
- package/src/headless/__tests__/headless-skill-activation.integration.test.ts +430 -0
- package/src/headless/__tests__/headless-transport.test.ts +268 -0
- package/src/headless/headless-runner.ts +141 -0
- package/src/headless/headless-stream-json.ts +142 -0
- package/src/headless/headless-transport.ts +43 -0
- package/src/headless/index.ts +4 -0
- package/src/http/__tests__/http-transport.test.ts +55 -0
- package/src/http/__tests__/routes.test.ts +168 -0
- package/src/http/http-transport.ts +42 -0
- package/src/http/index.ts +4 -0
- package/src/http/routes.ts +151 -0
- package/src/index.ts +5 -0
- package/src/mcp/__tests__/mcp-server.test.ts +66 -0
- package/src/mcp/__tests__/mcp-transport.test.ts +46 -0
- package/src/mcp/index.ts +4 -0
- package/src/mcp/mcp-server.ts +162 -0
- package/src/mcp/mcp-transport.ts +48 -0
- package/src/tui/App.tsx +478 -0
- package/src/tui/BackgroundTaskPanel.tsx +34 -0
- package/src/tui/CjkTextInput.tsx +204 -0
- package/src/tui/ConfirmPrompt.tsx +69 -0
- package/src/tui/ExecutionWorkspaceDetailPane.tsx +62 -0
- package/src/tui/ExecutionWorkspaceSwitcher.tsx +185 -0
- package/src/tui/InkTerminal.ts +42 -0
- package/src/tui/InputArea.tsx +298 -0
- package/src/tui/InteractivePrompt.tsx +57 -0
- package/src/tui/ListPicker.tsx +94 -0
- package/src/tui/MenuSelect.tsx +103 -0
- package/src/tui/MessageList.tsx +282 -0
- package/src/tui/PermissionPrompt.tsx +84 -0
- package/src/tui/PluginTUI.tsx +256 -0
- package/src/tui/SessionPicker.tsx +66 -0
- package/src/tui/SessionStatusBar.tsx +66 -0
- package/src/tui/SlashAutocomplete.tsx +110 -0
- package/src/tui/StatusBar.tsx +213 -0
- package/src/tui/StreamingIndicator.tsx +91 -0
- package/src/tui/TextPrompt.tsx +80 -0
- package/src/tui/ToolCommandOutput.tsx +37 -0
- package/src/tui/ToolDiffBlock.tsx +30 -0
- package/src/tui/TransportTUI.tsx +116 -0
- package/src/tui/UpdateNotice.tsx +14 -0
- package/src/tui/UsageSummaryEntry.tsx +38 -0
- package/src/tui/WaveText.tsx +44 -0
- package/src/tui/__tests__/InteractivePrompt.test.tsx +82 -0
- package/src/tui/__tests__/ListPicker.test.tsx +159 -0
- package/src/tui/__tests__/MenuSelect.test.tsx +103 -0
- package/src/tui/__tests__/PluginTUI.test.tsx +167 -0
- package/src/tui/__tests__/SlashAutocomplete.test.tsx +140 -0
- package/src/tui/__tests__/TextPrompt.test.tsx +98 -0
- package/src/tui/__tests__/UpdateNotice.test.tsx +15 -0
- package/src/tui/__tests__/abort-after-permission.test.tsx +169 -0
- package/src/tui/__tests__/abort-streaming-e2e.test.tsx +183 -0
- package/src/tui/__tests__/background-task-panel.test.tsx +53 -0
- package/src/tui/__tests__/background-task-row-format.test.ts +59 -0
- package/src/tui/__tests__/cjk-text-input-flow.test.ts +109 -0
- package/src/tui/__tests__/cjk-text-input.test.ts +191 -0
- package/src/tui/__tests__/command-effect-handler.test.ts +128 -0
- package/src/tui/__tests__/command-output-summary.test.ts +95 -0
- package/src/tui/__tests__/compact-event-bridge.test.ts +20 -0
- package/src/tui/__tests__/confirm-permission-flow.test.ts +91 -0
- package/src/tui/__tests__/confirm-prompt.test.tsx +87 -0
- package/src/tui/__tests__/execution-workspace-switcher.test.tsx +110 -0
- package/src/tui/__tests__/execution-workspace-view-model.test.ts +93 -0
- package/src/tui/__tests__/fixtures/provider-setup-prompt-driver.tsx +122 -0
- package/src/tui/__tests__/input-area-flow.test.ts +152 -0
- package/src/tui/__tests__/message-list-rendering.test.tsx +353 -0
- package/src/tui/__tests__/model-change-side-effect.test.ts +91 -0
- package/src/tui/__tests__/prompt-queue.test.tsx +255 -0
- package/src/tui/__tests__/provider-setup-pty-e2e.test.ts +233 -0
- package/src/tui/__tests__/render-markdown.test.ts +72 -0
- package/src/tui/__tests__/selection-flow.test.ts +61 -0
- package/src/tui/__tests__/slash-routing-effects.test.ts +225 -0
- package/src/tui/__tests__/status-activity.test.ts +71 -0
- package/src/tui/__tests__/status-bar.test.tsx +157 -0
- package/src/tui/__tests__/streaming-indicator.test.tsx +137 -0
- package/src/tui/__tests__/text-prompt-flow.test.ts +77 -0
- package/src/tui/__tests__/tui-state-manager.test.ts +401 -0
- package/src/tui/background-task-row-format.ts +52 -0
- package/src/tui/command-output-summary.ts +122 -0
- package/src/tui/execution-workspace-view-model.ts +123 -0
- package/src/tui/flows/cjk-text-input-flow.ts +285 -0
- package/src/tui/flows/confirm-prompt-flow.ts +45 -0
- package/src/tui/flows/input-area-flow.ts +186 -0
- package/src/tui/flows/permission-prompt-flow.ts +76 -0
- package/src/tui/flows/selection-flow.ts +126 -0
- package/src/tui/flows/text-prompt-flow.ts +98 -0
- package/src/tui/hooks/command-effect-handler.ts +98 -0
- package/src/tui/hooks/command-effect-queue.ts +39 -0
- package/src/tui/hooks/model-change-side-effect.ts +63 -0
- package/src/tui/hooks/side-effects-types.ts +38 -0
- package/src/tui/hooks/use-interactive-session-init.ts +50 -0
- package/src/tui/hooks/useAutocomplete.ts +85 -0
- package/src/tui/hooks/useInteractiveSession.ts +273 -0
- package/src/tui/hooks/usePermissionQueue.ts +51 -0
- package/src/tui/hooks/usePluginCallbacks.ts +30 -0
- package/src/tui/hooks/usePluginScreenData.ts +84 -0
- package/src/tui/hooks/useSideEffects.ts +210 -0
- package/src/tui/hooks/useSlashRouting.ts +117 -0
- package/src/tui/hooks/useStatusLineSettings.ts +35 -0
- package/src/tui/index.ts +3 -0
- package/src/tui/plugin-tui-handlers.ts +163 -0
- package/src/tui/render-markdown.ts +129 -0
- package/src/tui/render.tsx +60 -0
- package/src/tui/status-activity.ts +63 -0
- package/src/tui/tui-cli-adapter-context.tsx +12 -0
- package/src/tui/tui-cli-adapter.ts +25 -0
- package/src/tui/tui-state-manager.ts +225 -0
- package/src/tui/tui-transport.ts +32 -0
- package/src/tui/types.ts +14 -0
- package/src/tui/utils/__tests__/edit-diff.test.ts +426 -0
- package/src/tui/utils/__tests__/paste-detection.test.ts +116 -0
- package/src/tui/utils/__tests__/paste-labels.test.ts +46 -0
- package/src/tui/utils/__tests__/tool-call-extractor.test.ts +227 -0
- package/src/tui/utils/__tests__/tool-diff-summary.test.ts +104 -0
- package/src/tui/utils/edit-diff.ts +152 -0
- package/src/tui/utils/paste-labels.ts +9 -0
- package/src/tui/utils/tool-call-extractor.ts +91 -0
- package/src/tui/utils/tool-diff-summary.ts +75 -0
- package/src/ws/__tests__/ws-handler.test.ts +407 -0
- package/src/ws/__tests__/ws-transport.test.ts +53 -0
- package/src/ws/index.ts +13 -0
- package/src/ws/ws-background-messages.ts +170 -0
- package/src/ws/ws-handler.ts +279 -0
- package/src/ws/ws-protocol.ts +76 -0
- package/src/ws/ws-transport-configurable.ts +123 -0
- package/src/ws/ws-transport.ts +42 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TransportTUI — interactive overlay for transport enable/disable settings.
|
|
3
|
+
*
|
|
4
|
+
* Arrow keys navigate the list, space toggles enabled/disabled, enter/esc closes.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, { useState, useCallback } from 'react';
|
|
8
|
+
import { Box, Text, useInput } from 'ink';
|
|
9
|
+
import type {
|
|
10
|
+
ITransportEntry,
|
|
11
|
+
ITransportRegistryView,
|
|
12
|
+
} from '@robota-sdk/agent-interface-transport';
|
|
13
|
+
import type { IInteractiveSession } from '@robota-sdk/agent-framework';
|
|
14
|
+
|
|
15
|
+
const TRANSPORT_NAME_WIDTH = 18;
|
|
16
|
+
|
|
17
|
+
interface IEntryRowProps {
|
|
18
|
+
entry: ITransportEntry<IInteractiveSession>;
|
|
19
|
+
selected: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function TransportEntryRow({ entry, selected }: IEntryRowProps): React.ReactElement {
|
|
23
|
+
const enabled = entry.config.enabled;
|
|
24
|
+
const dot = enabled ? '●' : '○';
|
|
25
|
+
const badge = enabled ? '[enabled] ' : '[disabled]';
|
|
26
|
+
const portOpt = entry.config.options?.port;
|
|
27
|
+
const portHint = typeof portOpt === 'number' ? `port: ${portOpt}` : '';
|
|
28
|
+
return (
|
|
29
|
+
<Box>
|
|
30
|
+
<Text color={selected ? 'cyan' : undefined} bold={selected}>
|
|
31
|
+
{`${dot} ${entry.transport.name.padEnd(TRANSPORT_NAME_WIDTH)} ${badge} ${portHint}`}
|
|
32
|
+
</Text>
|
|
33
|
+
</Box>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type TKey = { upArrow: boolean; downArrow: boolean; escape: boolean; return: boolean };
|
|
38
|
+
|
|
39
|
+
function useTransportInput(
|
|
40
|
+
entries: ITransportEntry<IInteractiveSession>[],
|
|
41
|
+
cursor: number,
|
|
42
|
+
saving: boolean,
|
|
43
|
+
registry: ITransportRegistryView<IInteractiveSession>,
|
|
44
|
+
setCursor: (fn: (c: number) => number) => void,
|
|
45
|
+
setSaving: (v: boolean) => void,
|
|
46
|
+
onClose: () => void,
|
|
47
|
+
refresh: () => void,
|
|
48
|
+
): void {
|
|
49
|
+
useInput(
|
|
50
|
+
useCallback(
|
|
51
|
+
(_input: string, key: TKey) => {
|
|
52
|
+
if (saving) return;
|
|
53
|
+
if (key.upArrow) {
|
|
54
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (key.downArrow) {
|
|
58
|
+
setCursor((c) => Math.min(entries.length - 1, c + 1));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (key.escape || key.return) {
|
|
62
|
+
onClose();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (_input === ' ') {
|
|
66
|
+
const entry = entries[cursor];
|
|
67
|
+
if (!entry) return;
|
|
68
|
+
setSaving(true);
|
|
69
|
+
registry
|
|
70
|
+
.setEnabled(entry.transport.name, !entry.config.enabled)
|
|
71
|
+
.then(() => {
|
|
72
|
+
refresh();
|
|
73
|
+
setSaving(false);
|
|
74
|
+
})
|
|
75
|
+
.catch(() => setSaving(false));
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
[saving, entries, cursor, registry, onClose, refresh, setCursor, setSaving],
|
|
79
|
+
),
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface IProps {
|
|
84
|
+
registry: ITransportRegistryView<IInteractiveSession>;
|
|
85
|
+
onClose: () => void;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export default function TransportTUI({ registry, onClose }: IProps): React.ReactElement {
|
|
89
|
+
const [entries, setEntries] = useState(() => registry.getAll());
|
|
90
|
+
const [cursor, setCursor] = useState(0);
|
|
91
|
+
const [saving, setSaving] = useState(false);
|
|
92
|
+
const refresh = useCallback((): void => {
|
|
93
|
+
setEntries(registry.getAll());
|
|
94
|
+
}, [registry]);
|
|
95
|
+
|
|
96
|
+
useTransportInput(entries, cursor, saving, registry, setCursor, setSaving, onClose, refresh);
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<Box flexDirection="column" paddingX={2} paddingY={1}>
|
|
100
|
+
<Text bold>Settings › Transports</Text>
|
|
101
|
+
<Box marginTop={1} flexDirection="column">
|
|
102
|
+
{entries.map((entry, i) => (
|
|
103
|
+
<TransportEntryRow key={entry.transport.name} entry={entry} selected={i === cursor} />
|
|
104
|
+
))}
|
|
105
|
+
</Box>
|
|
106
|
+
<Box marginTop={1}>
|
|
107
|
+
<Text dimColor>↑↓ select space toggle enter/esc close</Text>
|
|
108
|
+
</Box>
|
|
109
|
+
{saving && (
|
|
110
|
+
<Box marginTop={1}>
|
|
111
|
+
<Text color="yellow">Saving…</Text>
|
|
112
|
+
</Box>
|
|
113
|
+
)}
|
|
114
|
+
</Box>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
|
|
4
|
+
interface IProps {
|
|
5
|
+
message: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default function UpdateNotice({ message }: IProps): React.ReactElement {
|
|
9
|
+
return (
|
|
10
|
+
<Box paddingX={1} marginBottom={1}>
|
|
11
|
+
<Text color="yellow">{message}</Text>
|
|
12
|
+
</Box>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import type { IHistoryEntry } from '@robota-sdk/agent-core';
|
|
4
|
+
import { formatTokenCount } from '@robota-sdk/agent-core';
|
|
5
|
+
import type { IUsageSnapshot } from '@robota-sdk/agent-framework';
|
|
6
|
+
|
|
7
|
+
const TOKEN_COMPACT_THRESHOLD = 1000;
|
|
8
|
+
|
|
9
|
+
export default function UsageSummaryEntry({ entry }: { entry: IHistoryEntry }): React.ReactElement {
|
|
10
|
+
const usage = entry.data as IUsageSnapshot | undefined;
|
|
11
|
+
if (!usage) return <></>;
|
|
12
|
+
const prompt = usage.promptTokens !== undefined ? formatUsageTokenCount(usage.promptTokens) : '?';
|
|
13
|
+
const completion =
|
|
14
|
+
usage.completionTokens !== undefined ? formatUsageTokenCount(usage.completionTokens) : '?';
|
|
15
|
+
const total = formatUsageTokenCount(usage.totalTokens);
|
|
16
|
+
const context = `${Math.round(usage.contextUsedPercentage)}% (${formatTokenCount(
|
|
17
|
+
usage.contextUsedTokens,
|
|
18
|
+
)}/${formatTokenCount(usage.contextMaxTokens)})`;
|
|
19
|
+
const costLabel = usage.costStatus === 'unknown' ? 'cost unknown' : `cost ${usage.costStatus}`;
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
23
|
+
<Box>
|
|
24
|
+
<Text color="white" bold>
|
|
25
|
+
Usage:{' '}
|
|
26
|
+
</Text>
|
|
27
|
+
<Text dimColor>
|
|
28
|
+
{usage.kind} {total} tokens (in {prompt} / out {completion}) · Context {context} ·{' '}
|
|
29
|
+
{costLabel}
|
|
30
|
+
</Text>
|
|
31
|
+
</Box>
|
|
32
|
+
</Box>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function formatUsageTokenCount(tokens: number): string {
|
|
37
|
+
return tokens < TOKEN_COMPACT_THRESHOLD ? tokens.toLocaleString() : formatTokenCount(tokens);
|
|
38
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WaveText — renders text with a subtle wave color animation.
|
|
3
|
+
* Groups of 3-4 characters share the same color, creating a soft flowing effect.
|
|
4
|
+
* Colors stay in a narrow range (dim grays) to avoid harsh contrast.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, { useState, useEffect } from 'react';
|
|
8
|
+
import { Text } from 'ink';
|
|
9
|
+
|
|
10
|
+
// Subtle gray tones — minimal contrast, soft wave
|
|
11
|
+
const WAVE_COLORS = ['#666666', '#888888', '#aaaaaa', '#888888'] as const;
|
|
12
|
+
const INTERVAL_MS = 400;
|
|
13
|
+
const CHARS_PER_GROUP = 4;
|
|
14
|
+
|
|
15
|
+
interface IProps {
|
|
16
|
+
text: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default function WaveText({ text }: IProps): React.ReactElement {
|
|
20
|
+
const [tick, setTick] = useState(0);
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const timer = setInterval(() => {
|
|
24
|
+
setTick((prev) => prev + 1);
|
|
25
|
+
}, INTERVAL_MS);
|
|
26
|
+
return () => clearInterval(timer);
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
const chars = [...text];
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Text>
|
|
33
|
+
{chars.map((char, i) => {
|
|
34
|
+
const group = Math.floor(i / CHARS_PER_GROUP);
|
|
35
|
+
const colorIndex = (tick + group) % WAVE_COLORS.length;
|
|
36
|
+
return (
|
|
37
|
+
<Text key={i} color={WAVE_COLORS[colorIndex]}>
|
|
38
|
+
{char}
|
|
39
|
+
</Text>
|
|
40
|
+
);
|
|
41
|
+
})}
|
|
42
|
+
</Text>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from 'ink-testing-library';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import InteractivePrompt from '../InteractivePrompt.js';
|
|
5
|
+
|
|
6
|
+
const delay = () => new Promise((resolve) => setTimeout(resolve, 20));
|
|
7
|
+
|
|
8
|
+
describe('InteractivePrompt', () => {
|
|
9
|
+
it('renders a generic choice prompt and submits the selected value', async () => {
|
|
10
|
+
const onSubmit = vi.fn();
|
|
11
|
+
const { stdin, lastFrame } = render(
|
|
12
|
+
<InteractivePrompt
|
|
13
|
+
prompt={{
|
|
14
|
+
kind: 'choice',
|
|
15
|
+
title: 'Select item',
|
|
16
|
+
options: [
|
|
17
|
+
{ value: 'first', label: 'First item' },
|
|
18
|
+
{ value: 'second', label: 'Second item' },
|
|
19
|
+
],
|
|
20
|
+
}}
|
|
21
|
+
onSubmit={onSubmit}
|
|
22
|
+
onCancel={() => {}}
|
|
23
|
+
/>,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
expect(lastFrame()!).toContain('Select item');
|
|
27
|
+
expect(lastFrame()!).toContain('First item');
|
|
28
|
+
|
|
29
|
+
stdin.write('\u001B[B');
|
|
30
|
+
await delay();
|
|
31
|
+
stdin.write('\r');
|
|
32
|
+
await delay();
|
|
33
|
+
|
|
34
|
+
expect(onSubmit).toHaveBeenCalledWith('second');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('renders a generic text prompt and validates submitted values', async () => {
|
|
38
|
+
const onSubmit = vi.fn();
|
|
39
|
+
const { stdin, lastFrame } = render(
|
|
40
|
+
<InteractivePrompt
|
|
41
|
+
prompt={{
|
|
42
|
+
kind: 'text',
|
|
43
|
+
title: 'Secret',
|
|
44
|
+
masked: true,
|
|
45
|
+
validate: (value) => (value.length === 0 ? 'Required' : undefined),
|
|
46
|
+
}}
|
|
47
|
+
onSubmit={onSubmit}
|
|
48
|
+
onCancel={() => {}}
|
|
49
|
+
/>,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
stdin.write('\r');
|
|
53
|
+
await delay();
|
|
54
|
+
expect(onSubmit).not.toHaveBeenCalled();
|
|
55
|
+
expect(lastFrame()!).toContain('Required');
|
|
56
|
+
|
|
57
|
+
stdin.write('abc');
|
|
58
|
+
await delay();
|
|
59
|
+
stdin.write('\r');
|
|
60
|
+
await delay();
|
|
61
|
+
expect(onSubmit).toHaveBeenCalledWith('abc');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('renders generic prompt descriptions without command-specific UI branches', () => {
|
|
65
|
+
const { lastFrame } = render(
|
|
66
|
+
<InteractivePrompt
|
|
67
|
+
prompt={{
|
|
68
|
+
kind: 'text',
|
|
69
|
+
title: 'OpenAI API key',
|
|
70
|
+
description:
|
|
71
|
+
'Setup help: API key: OpenAI API keys - https://platform.openai.com/api-keys',
|
|
72
|
+
}}
|
|
73
|
+
onSubmit={() => {}}
|
|
74
|
+
onCancel={() => {}}
|
|
75
|
+
/>,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
expect(lastFrame()!).toContain('OpenAI API key');
|
|
79
|
+
expect(lastFrame()!).toContain('OpenAI API keys');
|
|
80
|
+
expect(lastFrame()!).toContain('https://platform.openai.com/api-keys');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from 'ink-testing-library';
|
|
3
|
+
import { Text } from 'ink';
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import ListPicker from '../ListPicker.js';
|
|
6
|
+
|
|
7
|
+
describe('ListPicker', () => {
|
|
8
|
+
it('renders all items with first selected by default', () => {
|
|
9
|
+
const items = ['Alpha', 'Beta', 'Gamma'];
|
|
10
|
+
const { lastFrame } = render(
|
|
11
|
+
<ListPicker
|
|
12
|
+
items={items}
|
|
13
|
+
renderItem={(item, isSelected) => (
|
|
14
|
+
<Text>
|
|
15
|
+
{isSelected ? '> ' : ' '}
|
|
16
|
+
{item}
|
|
17
|
+
</Text>
|
|
18
|
+
)}
|
|
19
|
+
onSelect={() => {}}
|
|
20
|
+
onCancel={() => {}}
|
|
21
|
+
/>,
|
|
22
|
+
);
|
|
23
|
+
const frame = lastFrame()!;
|
|
24
|
+
expect(frame).toContain('> Alpha');
|
|
25
|
+
expect(frame).toContain(' Beta');
|
|
26
|
+
expect(frame).toContain(' Gamma');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('renders empty state when no items', () => {
|
|
30
|
+
const { lastFrame } = render(
|
|
31
|
+
<ListPicker
|
|
32
|
+
items={[]}
|
|
33
|
+
renderItem={(item, isSelected) => (
|
|
34
|
+
<Text>
|
|
35
|
+
{isSelected ? '> ' : ' '}
|
|
36
|
+
{String(item)}
|
|
37
|
+
</Text>
|
|
38
|
+
)}
|
|
39
|
+
onSelect={() => {}}
|
|
40
|
+
onCancel={() => {}}
|
|
41
|
+
/>,
|
|
42
|
+
);
|
|
43
|
+
// Should render without crashing
|
|
44
|
+
expect(lastFrame()).toBeDefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('calls onSelect with item on Enter', () => {
|
|
48
|
+
let selected = '';
|
|
49
|
+
const items = ['Alpha', 'Beta'];
|
|
50
|
+
const { stdin } = render(
|
|
51
|
+
<ListPicker
|
|
52
|
+
items={items}
|
|
53
|
+
renderItem={(item, isSelected) => (
|
|
54
|
+
<Text>
|
|
55
|
+
{isSelected ? '> ' : ' '}
|
|
56
|
+
{item}
|
|
57
|
+
</Text>
|
|
58
|
+
)}
|
|
59
|
+
onSelect={(item) => {
|
|
60
|
+
selected = item;
|
|
61
|
+
}}
|
|
62
|
+
onCancel={() => {}}
|
|
63
|
+
/>,
|
|
64
|
+
);
|
|
65
|
+
stdin.write('\r');
|
|
66
|
+
expect(selected).toBe('Alpha');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('navigates down with arrow key and selects', () => {
|
|
70
|
+
let selected = '';
|
|
71
|
+
const items = ['Alpha', 'Beta', 'Gamma'];
|
|
72
|
+
const { stdin } = render(
|
|
73
|
+
<ListPicker
|
|
74
|
+
items={items}
|
|
75
|
+
renderItem={(item, isSelected) => (
|
|
76
|
+
<Text>
|
|
77
|
+
{isSelected ? '> ' : ' '}
|
|
78
|
+
{item}
|
|
79
|
+
</Text>
|
|
80
|
+
)}
|
|
81
|
+
onSelect={(item) => {
|
|
82
|
+
selected = item;
|
|
83
|
+
}}
|
|
84
|
+
onCancel={() => {}}
|
|
85
|
+
/>,
|
|
86
|
+
);
|
|
87
|
+
stdin.write('\x1B[B'); // Down arrow
|
|
88
|
+
stdin.write('\r');
|
|
89
|
+
expect(selected).toBe('Beta');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('calls onCancel on Escape', async () => {
|
|
93
|
+
const delay = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
94
|
+
let cancelled = false;
|
|
95
|
+
const { stdin } = render(
|
|
96
|
+
<ListPicker
|
|
97
|
+
items={['Alpha']}
|
|
98
|
+
renderItem={(item, isSelected) => (
|
|
99
|
+
<Text>
|
|
100
|
+
{isSelected ? '> ' : ' '}
|
|
101
|
+
{item}
|
|
102
|
+
</Text>
|
|
103
|
+
)}
|
|
104
|
+
onSelect={() => {}}
|
|
105
|
+
onCancel={() => {
|
|
106
|
+
cancelled = true;
|
|
107
|
+
}}
|
|
108
|
+
/>,
|
|
109
|
+
);
|
|
110
|
+
stdin.write('\x1B');
|
|
111
|
+
await delay(50);
|
|
112
|
+
expect(cancelled).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('does not go above first item', () => {
|
|
116
|
+
const items = ['Alpha', 'Beta'];
|
|
117
|
+
const { stdin, lastFrame } = render(
|
|
118
|
+
<ListPicker
|
|
119
|
+
items={items}
|
|
120
|
+
renderItem={(item, isSelected) => (
|
|
121
|
+
<Text>
|
|
122
|
+
{isSelected ? '> ' : ' '}
|
|
123
|
+
{item}
|
|
124
|
+
</Text>
|
|
125
|
+
)}
|
|
126
|
+
onSelect={() => {}}
|
|
127
|
+
onCancel={() => {}}
|
|
128
|
+
/>,
|
|
129
|
+
);
|
|
130
|
+
stdin.write('\x1B[A'); // Up arrow (already at 0)
|
|
131
|
+
const frame = lastFrame()!;
|
|
132
|
+
expect(frame).toContain('> Alpha');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('does not go below last item', () => {
|
|
136
|
+
let selected = '';
|
|
137
|
+
const items = ['Alpha', 'Beta'];
|
|
138
|
+
const { stdin } = render(
|
|
139
|
+
<ListPicker
|
|
140
|
+
items={items}
|
|
141
|
+
renderItem={(item, isSelected) => (
|
|
142
|
+
<Text>
|
|
143
|
+
{isSelected ? '> ' : ' '}
|
|
144
|
+
{item}
|
|
145
|
+
</Text>
|
|
146
|
+
)}
|
|
147
|
+
onSelect={(item) => {
|
|
148
|
+
selected = item;
|
|
149
|
+
}}
|
|
150
|
+
onCancel={() => {}}
|
|
151
|
+
/>,
|
|
152
|
+
);
|
|
153
|
+
stdin.write('\x1B[B'); // Down
|
|
154
|
+
stdin.write('\x1B[B'); // Down (should stay at Beta)
|
|
155
|
+
stdin.write('\x1B[B'); // Down (should stay at Beta)
|
|
156
|
+
stdin.write('\r');
|
|
157
|
+
expect(selected).toBe('Beta');
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from 'ink-testing-library';
|
|
3
|
+
import { describe, it, expect } from 'vitest';
|
|
4
|
+
import MenuSelect from '../MenuSelect.js';
|
|
5
|
+
|
|
6
|
+
const delay = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
7
|
+
|
|
8
|
+
describe('MenuSelect', () => {
|
|
9
|
+
const items = [
|
|
10
|
+
{ label: 'Option A', value: 'a' },
|
|
11
|
+
{ label: 'Option B', value: 'b', hint: 'some hint' },
|
|
12
|
+
{ label: 'Option C', value: 'c' },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
it('renders title and all items', () => {
|
|
16
|
+
const { lastFrame } = render(
|
|
17
|
+
<MenuSelect title="Test Menu" items={items} onSelect={() => {}} onBack={() => {}} />,
|
|
18
|
+
);
|
|
19
|
+
const frame = lastFrame()!;
|
|
20
|
+
expect(frame).toContain('Test Menu');
|
|
21
|
+
expect(frame).toContain('Option A');
|
|
22
|
+
expect(frame).toContain('Option B');
|
|
23
|
+
expect(frame).toContain('Option C');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('renders hint text when provided', () => {
|
|
27
|
+
const { lastFrame } = render(
|
|
28
|
+
<MenuSelect title="Test" items={items} onSelect={() => {}} onBack={() => {}} />,
|
|
29
|
+
);
|
|
30
|
+
expect(lastFrame()!).toContain('some hint');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('highlights first item by default with > prefix', () => {
|
|
34
|
+
const { lastFrame } = render(
|
|
35
|
+
<MenuSelect title="Test" items={items} onSelect={() => {}} onBack={() => {}} />,
|
|
36
|
+
);
|
|
37
|
+
expect(lastFrame()!).toContain('>');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('shows loading state', () => {
|
|
41
|
+
const { lastFrame } = render(
|
|
42
|
+
<MenuSelect title="Test" items={[]} onSelect={() => {}} onBack={() => {}} loading />,
|
|
43
|
+
);
|
|
44
|
+
expect(lastFrame()!).toContain('Loading');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('shows error state', () => {
|
|
48
|
+
const { lastFrame } = render(
|
|
49
|
+
<MenuSelect title="Test" items={[]} onSelect={() => {}} onBack={() => {}} error="Failed" />,
|
|
50
|
+
);
|
|
51
|
+
expect(lastFrame()!).toContain('Failed');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('calls onSelect with value on Enter', () => {
|
|
55
|
+
let selected = '';
|
|
56
|
+
const { stdin } = render(
|
|
57
|
+
<MenuSelect
|
|
58
|
+
title="Test"
|
|
59
|
+
items={items}
|
|
60
|
+
onSelect={(v) => {
|
|
61
|
+
selected = v;
|
|
62
|
+
}}
|
|
63
|
+
onBack={() => {}}
|
|
64
|
+
/>,
|
|
65
|
+
);
|
|
66
|
+
stdin.write('\r');
|
|
67
|
+
expect(selected).toBe('a');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('calls onBack on Escape', async () => {
|
|
71
|
+
let backed = false;
|
|
72
|
+
const { stdin } = render(
|
|
73
|
+
<MenuSelect
|
|
74
|
+
title="Test"
|
|
75
|
+
items={items}
|
|
76
|
+
onSelect={() => {}}
|
|
77
|
+
onBack={() => {
|
|
78
|
+
backed = true;
|
|
79
|
+
}}
|
|
80
|
+
/>,
|
|
81
|
+
);
|
|
82
|
+
stdin.write('\x1B');
|
|
83
|
+
await delay(50);
|
|
84
|
+
expect(backed).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('navigates down with arrow key', () => {
|
|
88
|
+
let selected = '';
|
|
89
|
+
const { stdin } = render(
|
|
90
|
+
<MenuSelect
|
|
91
|
+
title="Test"
|
|
92
|
+
items={items}
|
|
93
|
+
onSelect={(v) => {
|
|
94
|
+
selected = v;
|
|
95
|
+
}}
|
|
96
|
+
onBack={() => {}}
|
|
97
|
+
/>,
|
|
98
|
+
);
|
|
99
|
+
stdin.write('\x1B[B'); // Down arrow
|
|
100
|
+
stdin.write('\r');
|
|
101
|
+
expect(selected).toBe('b');
|
|
102
|
+
});
|
|
103
|
+
});
|