@stigmer/ink 0.0.88
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 +190 -0
- package/app/SessionApp.d.ts +42 -0
- package/app/SessionApp.d.ts.map +1 -0
- package/app/SessionApp.js +38 -0
- package/app/SessionApp.js.map +1 -0
- package/app/SessionView.d.ts +32 -0
- package/app/SessionView.d.ts.map +1 -0
- package/app/SessionView.js +58 -0
- package/app/SessionView.js.map +1 -0
- package/cli/stigmer-ink.d.ts +3 -0
- package/cli/stigmer-ink.d.ts.map +1 -0
- package/cli/stigmer-ink.js +117 -0
- package/cli/stigmer-ink.js.map +1 -0
- package/components/ApprovalPrompt.d.ts +21 -0
- package/components/ApprovalPrompt.d.ts.map +1 -0
- package/components/ApprovalPrompt.js +60 -0
- package/components/ApprovalPrompt.js.map +1 -0
- package/components/ExecutionProgress.d.ts +16 -0
- package/components/ExecutionProgress.d.ts.map +1 -0
- package/components/ExecutionProgress.js +47 -0
- package/components/ExecutionProgress.js.map +1 -0
- package/components/FollowUpInput.d.ts +19 -0
- package/components/FollowUpInput.d.ts.map +1 -0
- package/components/FollowUpInput.js +27 -0
- package/components/FollowUpInput.js.map +1 -0
- package/components/MessageEntry.d.ts +18 -0
- package/components/MessageEntry.d.ts.map +1 -0
- package/components/MessageEntry.js +42 -0
- package/components/MessageEntry.js.map +1 -0
- package/components/MessageThread.d.ts +31 -0
- package/components/MessageThread.d.ts.map +1 -0
- package/components/MessageThread.js +146 -0
- package/components/MessageThread.js.map +1 -0
- package/components/SubAgentBlock.d.ts +19 -0
- package/components/SubAgentBlock.d.ts.map +1 -0
- package/components/SubAgentBlock.js +73 -0
- package/components/SubAgentBlock.js.map +1 -0
- package/components/TodoList.d.ts +17 -0
- package/components/TodoList.d.ts.map +1 -0
- package/components/TodoList.js +43 -0
- package/components/TodoList.js.map +1 -0
- package/components/ToolCallGroup.d.ts +20 -0
- package/components/ToolCallGroup.d.ts.map +1 -0
- package/components/ToolCallGroup.js +51 -0
- package/components/ToolCallGroup.js.map +1 -0
- package/components/ToolCallItem.d.ts +14 -0
- package/components/ToolCallItem.d.ts.map +1 -0
- package/components/ToolCallItem.js +33 -0
- package/components/ToolCallItem.js.map +1 -0
- package/components/UsageWidget.d.ts +16 -0
- package/components/UsageWidget.d.ts.map +1 -0
- package/components/UsageWidget.js +18 -0
- package/components/UsageWidget.js.map +1 -0
- package/index.d.ts +16 -0
- package/index.d.ts.map +1 -0
- package/index.js +21 -0
- package/index.js.map +1 -0
- package/markdown.d.ts +21 -0
- package/markdown.d.ts.map +1 -0
- package/markdown.js +44 -0
- package/markdown.js.map +1 -0
- package/package.json +48 -0
- package/provider.d.ts +46 -0
- package/provider.d.ts.map +1 -0
- package/provider.js +33 -0
- package/provider.js.map +1 -0
- package/src/__tests__/components.test.tsx +162 -0
- package/src/__tests__/markdown.test.ts +46 -0
- package/src/app/SessionApp.tsx +74 -0
- package/src/app/SessionView.tsx +164 -0
- package/src/cli/stigmer-ink.tsx +148 -0
- package/src/components/ApprovalPrompt.tsx +139 -0
- package/src/components/ExecutionProgress.tsx +75 -0
- package/src/components/FollowUpInput.tsx +70 -0
- package/src/components/MessageEntry.tsx +80 -0
- package/src/components/MessageThread.tsx +264 -0
- package/src/components/SubAgentBlock.tsx +146 -0
- package/src/components/TodoList.tsx +75 -0
- package/src/components/ToolCallGroup.tsx +92 -0
- package/src/components/ToolCallItem.tsx +74 -0
- package/src/components/UsageWidget.tsx +35 -0
- package/src/index.ts +28 -0
- package/src/markdown.ts +48 -0
- package/src/provider.tsx +62 -0
- package/src/types/marked-terminal.d.ts +19 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import type { TokenProvider } from "@stigmer/sdk";
|
|
4
|
+
import { InkStigmerProvider } from "../provider.js";
|
|
5
|
+
import { createNodeClient, type NodeClientConfig } from "@stigmer/sdk/node";
|
|
6
|
+
import { SessionView } from "./SessionView.js";
|
|
7
|
+
|
|
8
|
+
/** Props for {@link SessionApp}. */
|
|
9
|
+
export interface SessionAppProps {
|
|
10
|
+
/** Session ID to display and converse in. */
|
|
11
|
+
readonly sessionId: string;
|
|
12
|
+
/** Organization slug for creating follow-up executions. */
|
|
13
|
+
readonly org: string;
|
|
14
|
+
/** Stigmer API server URL. */
|
|
15
|
+
readonly baseUrl: string;
|
|
16
|
+
/** Static API key for authentication. */
|
|
17
|
+
readonly apiKey?: string;
|
|
18
|
+
/** Dynamic token provider for authentication. */
|
|
19
|
+
readonly getAccessToken?: TokenProvider;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Self-contained top-level Ink application for viewing and
|
|
24
|
+
* interacting with a Stigmer agent session in the terminal.
|
|
25
|
+
*
|
|
26
|
+
* Creates a Node.js-compatible Stigmer client, wraps in
|
|
27
|
+
* `InkStigmerProvider`, and renders a {@link SessionView}.
|
|
28
|
+
*
|
|
29
|
+
* This is the highest-level component — platform builders who
|
|
30
|
+
* need more control should compose {@link InkStigmerProvider},
|
|
31
|
+
* {@link SessionView}, or the individual components directly.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```tsx
|
|
35
|
+
* import { render } from "ink";
|
|
36
|
+
* import { SessionApp } from "@stigmer/ink";
|
|
37
|
+
*
|
|
38
|
+
* render(
|
|
39
|
+
* <SessionApp
|
|
40
|
+
* sessionId="ses-abc123"
|
|
41
|
+
* org="my-org"
|
|
42
|
+
* baseUrl="https://api.stigmer.ai"
|
|
43
|
+
* apiKey={process.env.STIGMER_API_KEY}
|
|
44
|
+
* />
|
|
45
|
+
* );
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export function SessionApp({
|
|
49
|
+
sessionId,
|
|
50
|
+
org,
|
|
51
|
+
baseUrl,
|
|
52
|
+
apiKey,
|
|
53
|
+
getAccessToken,
|
|
54
|
+
}: SessionAppProps) {
|
|
55
|
+
const clientConfig: NodeClientConfig = { baseUrl, apiKey, getAccessToken };
|
|
56
|
+
|
|
57
|
+
const client = React.useMemo(
|
|
58
|
+
() => createNodeClient(clientConfig),
|
|
59
|
+
[baseUrl, apiKey, getAccessToken],
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<InkStigmerProvider client={client}>
|
|
64
|
+
<Box flexDirection="column">
|
|
65
|
+
<Box paddingLeft={1} paddingBottom={1}>
|
|
66
|
+
<Text dimColor>
|
|
67
|
+
Session {sessionId} · {org}
|
|
68
|
+
</Text>
|
|
69
|
+
</Box>
|
|
70
|
+
<SessionView sessionId={sessionId} org={org} />
|
|
71
|
+
</Box>
|
|
72
|
+
</InkStigmerProvider>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import Spinner from "ink-spinner";
|
|
4
|
+
import { useSessionConversation, resolvedSubject, PENDING_SUBJECT } from "@stigmer/react";
|
|
5
|
+
import { MessageThread } from "../components/MessageThread.js";
|
|
6
|
+
import { TodoList } from "../components/TodoList.js";
|
|
7
|
+
import { FollowUpInput } from "../components/FollowUpInput.js";
|
|
8
|
+
import { UsageWidget } from "../components/UsageWidget.js";
|
|
9
|
+
import { ExecutionProgress } from "../components/ExecutionProgress.js";
|
|
10
|
+
|
|
11
|
+
/** Props for {@link SessionView}. */
|
|
12
|
+
export interface SessionViewProps {
|
|
13
|
+
/** Session ID to display and converse in. */
|
|
14
|
+
readonly sessionId: string;
|
|
15
|
+
/** Organization slug for creating follow-up executions. */
|
|
16
|
+
readonly org: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Full-featured session conversation view for the terminal.
|
|
21
|
+
*
|
|
22
|
+
* Uses the headless {@link useSessionConversation} hook from
|
|
23
|
+
* `@stigmer/react` to manage the complete conversation lifecycle,
|
|
24
|
+
* then renders the thread and input using Ink terminal components.
|
|
25
|
+
*
|
|
26
|
+
* This is the main composition component that platform builders
|
|
27
|
+
* drop into their Ink apps for a complete agent conversation UI.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```tsx
|
|
31
|
+
* import { InkStigmerProvider, createNodeClient, SessionView } from "@stigmer/ink";
|
|
32
|
+
*
|
|
33
|
+
* const client = createNodeClient({ baseUrl: "...", apiKey: "..." });
|
|
34
|
+
*
|
|
35
|
+
* render(
|
|
36
|
+
* <InkStigmerProvider client={client}>
|
|
37
|
+
* <SessionView sessionId="ses-xxx" org="my-org" />
|
|
38
|
+
* </InkStigmerProvider>
|
|
39
|
+
* );
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export function SessionView({ sessionId, org }: SessionViewProps) {
|
|
43
|
+
const conv = useSessionConversation(sessionId, org);
|
|
44
|
+
const [expandTools, setExpandTools] = useState(false);
|
|
45
|
+
|
|
46
|
+
useInput((input, key) => {
|
|
47
|
+
if (key.ctrl && input === "o") {
|
|
48
|
+
setExpandTools((e) => !e);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (conv.isLoading) {
|
|
53
|
+
return (
|
|
54
|
+
<Box gap={1} paddingLeft={1}>
|
|
55
|
+
<Text color="cyan">
|
|
56
|
+
<Spinner type="dots" />
|
|
57
|
+
</Text>
|
|
58
|
+
<Text>Loading session...</Text>
|
|
59
|
+
</Box>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (conv.loadError) {
|
|
64
|
+
return (
|
|
65
|
+
<Box flexDirection="column" paddingLeft={1}>
|
|
66
|
+
<Text color="red" bold>
|
|
67
|
+
Failed to load session
|
|
68
|
+
</Text>
|
|
69
|
+
<Text color="red">{conv.loadError.message}</Text>
|
|
70
|
+
</Box>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const allExecutions = [
|
|
75
|
+
...conv.completedExecutions,
|
|
76
|
+
...(conv.activeStreamExecution ? [conv.activeStreamExecution] : []),
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const activeTodos = conv.activeStreamExecution?.status?.todos;
|
|
80
|
+
const contextInfo = conv.activeStreamExecution?.status?.contextInfo;
|
|
81
|
+
const summarizationCount = contextInfo?.summarizationEvents?.length ?? 0;
|
|
82
|
+
|
|
83
|
+
const subject = resolvedSubject(conv.session?.spec?.subject);
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<Box flexDirection="column">
|
|
87
|
+
{subject && subject !== PENDING_SUBJECT && (
|
|
88
|
+
<Box paddingLeft={1} marginBottom={1}>
|
|
89
|
+
<Text dimColor bold>{subject}</Text>
|
|
90
|
+
</Box>
|
|
91
|
+
)}
|
|
92
|
+
|
|
93
|
+
{conv.isConnecting && (
|
|
94
|
+
<Box gap={1} paddingLeft={1}>
|
|
95
|
+
<Text color="cyan">
|
|
96
|
+
<Spinner type="dots" />
|
|
97
|
+
</Text>
|
|
98
|
+
<Text dimColor>Connecting to stream...</Text>
|
|
99
|
+
</Box>
|
|
100
|
+
)}
|
|
101
|
+
|
|
102
|
+
{conv.streamError && (
|
|
103
|
+
<Box flexDirection="column" paddingLeft={1} marginBottom={1}>
|
|
104
|
+
<Box gap={1}>
|
|
105
|
+
<Text color="yellow" bold>Stream disconnected</Text>
|
|
106
|
+
<Text dimColor>— reconnecting...</Text>
|
|
107
|
+
</Box>
|
|
108
|
+
<Text color="red" dimColor>{conv.streamError.message}</Text>
|
|
109
|
+
</Box>
|
|
110
|
+
)}
|
|
111
|
+
|
|
112
|
+
{conv.activePhase != null && conv.activePhase !== 0 && (
|
|
113
|
+
<ExecutionProgress phase={conv.activePhase} />
|
|
114
|
+
)}
|
|
115
|
+
|
|
116
|
+
<MessageThread
|
|
117
|
+
executions={conv.completedExecutions}
|
|
118
|
+
activeStreamExecution={conv.activeStreamExecution}
|
|
119
|
+
pendingUserMessage={conv.pendingUserMessage}
|
|
120
|
+
onApprovalSubmit={conv.submitApproval}
|
|
121
|
+
submittingApprovalIds={conv.submittingApprovalIds}
|
|
122
|
+
expandToolCalls={expandTools}
|
|
123
|
+
/>
|
|
124
|
+
|
|
125
|
+
{summarizationCount > 0 && (
|
|
126
|
+
<Box paddingLeft={1} marginTop={1}>
|
|
127
|
+
<Text dimColor>
|
|
128
|
+
Context compacted ({summarizationCount} {summarizationCount === 1 ? "event" : "events"}, {Math.round(contextInfo!.utilizationPercent)}% utilization)
|
|
129
|
+
</Text>
|
|
130
|
+
</Box>
|
|
131
|
+
)}
|
|
132
|
+
|
|
133
|
+
{activeTodos && Object.keys(activeTodos).length > 0 && (
|
|
134
|
+
<Box marginTop={1} paddingLeft={1}>
|
|
135
|
+
<TodoList todos={activeTodos} />
|
|
136
|
+
</Box>
|
|
137
|
+
)}
|
|
138
|
+
|
|
139
|
+
{conv.approvalError && (
|
|
140
|
+
<Box paddingLeft={1}>
|
|
141
|
+
<Text color="red">
|
|
142
|
+
Approval error: {conv.approvalError.message}
|
|
143
|
+
</Text>
|
|
144
|
+
</Box>
|
|
145
|
+
)}
|
|
146
|
+
|
|
147
|
+
{conv.sendError && (
|
|
148
|
+
<Box paddingLeft={1}>
|
|
149
|
+
<Text color="red">
|
|
150
|
+
Send error: {conv.sendError.message}
|
|
151
|
+
</Text>
|
|
152
|
+
</Box>
|
|
153
|
+
)}
|
|
154
|
+
|
|
155
|
+
<UsageWidget executions={allExecutions} />
|
|
156
|
+
|
|
157
|
+
<FollowUpInput
|
|
158
|
+
onSubmit={(message) => conv.sendFollowUp(message)}
|
|
159
|
+
isSubmitting={conv.isSending}
|
|
160
|
+
disabled={!conv.canSendFollowUp}
|
|
161
|
+
/>
|
|
162
|
+
</Box>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { Readable } from "node:stream";
|
|
5
|
+
import { render } from "ink";
|
|
6
|
+
import { SessionApp } from "../app/SessionApp.js";
|
|
7
|
+
|
|
8
|
+
interface CliConfig {
|
|
9
|
+
sessionId: string;
|
|
10
|
+
org: string;
|
|
11
|
+
baseUrl: string;
|
|
12
|
+
apiKey?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function parseArgs(argv: string[]): CliConfig | null {
|
|
16
|
+
const args = argv.slice(2);
|
|
17
|
+
const config: Partial<CliConfig> = {};
|
|
18
|
+
|
|
19
|
+
for (let i = 0; i < args.length; i++) {
|
|
20
|
+
switch (args[i]) {
|
|
21
|
+
case "--session":
|
|
22
|
+
case "-s":
|
|
23
|
+
config.sessionId = args[++i];
|
|
24
|
+
break;
|
|
25
|
+
case "--org":
|
|
26
|
+
case "-o":
|
|
27
|
+
config.org = args[++i];
|
|
28
|
+
break;
|
|
29
|
+
case "--base-url":
|
|
30
|
+
case "-u":
|
|
31
|
+
config.baseUrl = args[++i];
|
|
32
|
+
break;
|
|
33
|
+
case "--api-key":
|
|
34
|
+
case "-k":
|
|
35
|
+
config.apiKey = args[++i];
|
|
36
|
+
break;
|
|
37
|
+
case "--help":
|
|
38
|
+
case "-h":
|
|
39
|
+
printUsage();
|
|
40
|
+
process.exit(0);
|
|
41
|
+
default:
|
|
42
|
+
if (args[i].startsWith("-")) {
|
|
43
|
+
console.error(`Unknown option: ${args[i]}`);
|
|
44
|
+
printUsage();
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
config.baseUrl ??= process.env.STIGMER_BASE_URL;
|
|
51
|
+
config.apiKey ??= process.env.STIGMER_API_KEY;
|
|
52
|
+
|
|
53
|
+
if (!config.sessionId || !config.org || !config.baseUrl) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return config as CliConfig;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function readStdinJson(): Promise<CliConfig | null> {
|
|
61
|
+
if (process.stdin.isTTY) return null;
|
|
62
|
+
|
|
63
|
+
return new Promise((resolve) => {
|
|
64
|
+
let data = "";
|
|
65
|
+
process.stdin.setEncoding("utf8");
|
|
66
|
+
process.stdin.on("data", (chunk) => (data += chunk));
|
|
67
|
+
process.stdin.on("end", () => {
|
|
68
|
+
try {
|
|
69
|
+
const parsed = JSON.parse(data);
|
|
70
|
+
if (parsed.sessionId && parsed.org && parsed.baseUrl) {
|
|
71
|
+
resolve(parsed as CliConfig);
|
|
72
|
+
} else {
|
|
73
|
+
resolve(null);
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
resolve(null);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
setTimeout(() => resolve(null), 1000);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function printUsage(): void {
|
|
84
|
+
console.log(`
|
|
85
|
+
stigmer-ink — Terminal session viewer for the Stigmer platform
|
|
86
|
+
|
|
87
|
+
Usage:
|
|
88
|
+
stigmer-ink --session <id> --org <slug> [options]
|
|
89
|
+
echo '{"sessionId":"...","org":"...","baseUrl":"..."}' | stigmer-ink
|
|
90
|
+
|
|
91
|
+
Options:
|
|
92
|
+
-s, --session <id> Session ID (required)
|
|
93
|
+
-o, --org <slug> Organization slug (required)
|
|
94
|
+
-u, --base-url <url> Stigmer API URL (or STIGMER_BASE_URL env)
|
|
95
|
+
-k, --api-key <key> API key (or STIGMER_API_KEY env)
|
|
96
|
+
-h, --help Show this help message
|
|
97
|
+
|
|
98
|
+
Environment:
|
|
99
|
+
STIGMER_BASE_URL Default API server URL
|
|
100
|
+
STIGMER_API_KEY Default API key
|
|
101
|
+
`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function main() {
|
|
105
|
+
let config = parseArgs(process.argv);
|
|
106
|
+
|
|
107
|
+
if (!config) {
|
|
108
|
+
config = await readStdinJson();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!config) {
|
|
112
|
+
console.error(
|
|
113
|
+
"Error: --session, --org, and --base-url are required.\n" +
|
|
114
|
+
"Run with --help for usage information.",
|
|
115
|
+
);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const isTTY = Boolean(process.stdin.isTTY);
|
|
120
|
+
const stdin = isTTY
|
|
121
|
+
? process.stdin
|
|
122
|
+
: new Readable({ read() {} }) as unknown as NodeJS.ReadStream;
|
|
123
|
+
|
|
124
|
+
const instance = render(
|
|
125
|
+
<SessionApp
|
|
126
|
+
sessionId={config.sessionId}
|
|
127
|
+
org={config.org}
|
|
128
|
+
baseUrl={config.baseUrl}
|
|
129
|
+
apiKey={config.apiKey}
|
|
130
|
+
/>,
|
|
131
|
+
{ stdin, exitOnCtrlC: isTTY, debug: !isTTY },
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
const cleanup = () => {
|
|
135
|
+
instance.unmount();
|
|
136
|
+
process.exit(0);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
process.on("SIGINT", cleanup);
|
|
140
|
+
process.on("SIGTERM", cleanup);
|
|
141
|
+
|
|
142
|
+
await instance.waitUntilExit();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
main().catch((err) => {
|
|
146
|
+
console.error("Fatal error:", err);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { Box, Text, useInput } from "ink";
|
|
3
|
+
import type { PendingApproval } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/approval_pb";
|
|
4
|
+
import { ApprovalAction } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
5
|
+
|
|
6
|
+
/** Props for {@link ApprovalPrompt}. */
|
|
7
|
+
export interface ApprovalPromptProps {
|
|
8
|
+
/** The pending approval request to render. */
|
|
9
|
+
readonly pendingApproval: PendingApproval;
|
|
10
|
+
/** Called when the user selects an action. */
|
|
11
|
+
readonly onSubmit: (action: ApprovalAction) => void;
|
|
12
|
+
/** Disables input while an approval submission is in flight. */
|
|
13
|
+
readonly isSubmitting?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ActionOption {
|
|
17
|
+
readonly label: string;
|
|
18
|
+
readonly action: ApprovalAction;
|
|
19
|
+
readonly color: string;
|
|
20
|
+
readonly shortcut: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const OPTIONS: readonly ActionOption[] = [
|
|
24
|
+
{
|
|
25
|
+
label: "Approve",
|
|
26
|
+
action: ApprovalAction.APPROVE,
|
|
27
|
+
color: "green",
|
|
28
|
+
shortcut: "y",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
label: "Reject",
|
|
32
|
+
action: ApprovalAction.REJECT,
|
|
33
|
+
color: "red",
|
|
34
|
+
shortcut: "n",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
label: "Skip",
|
|
38
|
+
action: ApprovalAction.SKIP,
|
|
39
|
+
color: "yellow",
|
|
40
|
+
shortcut: "s",
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* HITL approval prompt for tool call authorization.
|
|
46
|
+
*
|
|
47
|
+
* Displays the tool name and args preview, then presents
|
|
48
|
+
* Approve/Reject/Skip options navigable via arrow keys or
|
|
49
|
+
* shortcut keys (y/n/s). Press Enter to confirm the highlighted
|
|
50
|
+
* selection.
|
|
51
|
+
*/
|
|
52
|
+
export function ApprovalPrompt({
|
|
53
|
+
pendingApproval,
|
|
54
|
+
onSubmit,
|
|
55
|
+
isSubmitting = false,
|
|
56
|
+
}: ApprovalPromptProps) {
|
|
57
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
58
|
+
|
|
59
|
+
useInput(
|
|
60
|
+
(input, key) => {
|
|
61
|
+
if (isSubmitting) return;
|
|
62
|
+
|
|
63
|
+
if (key.upArrow || key.leftArrow) {
|
|
64
|
+
setSelectedIndex((i) => (i > 0 ? i - 1 : OPTIONS.length - 1));
|
|
65
|
+
} else if (key.downArrow || key.rightArrow) {
|
|
66
|
+
setSelectedIndex((i) => (i < OPTIONS.length - 1 ? i + 1 : 0));
|
|
67
|
+
} else if (key.return) {
|
|
68
|
+
onSubmit(OPTIONS[selectedIndex].action);
|
|
69
|
+
} else {
|
|
70
|
+
const shortcutMatch = OPTIONS.findIndex(
|
|
71
|
+
(o) => o.shortcut === input.toLowerCase(),
|
|
72
|
+
);
|
|
73
|
+
if (shortcutMatch >= 0) {
|
|
74
|
+
onSubmit(OPTIONS[shortcutMatch].action);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const serverSlug = pendingApproval.mcpServerSlug;
|
|
81
|
+
const toolLabel = serverSlug
|
|
82
|
+
? `${serverSlug}/${pendingApproval.toolName}`
|
|
83
|
+
: pendingApproval.toolName;
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<Box
|
|
87
|
+
flexDirection="column"
|
|
88
|
+
paddingLeft={2}
|
|
89
|
+
paddingTop={1}
|
|
90
|
+
paddingBottom={1}
|
|
91
|
+
borderStyle="round"
|
|
92
|
+
borderColor="yellow"
|
|
93
|
+
>
|
|
94
|
+
<Box gap={1}>
|
|
95
|
+
<Text color="yellow" bold>
|
|
96
|
+
⚠ Approval required
|
|
97
|
+
</Text>
|
|
98
|
+
{pendingApproval.fromSubAgent && (
|
|
99
|
+
<Text dimColor>
|
|
100
|
+
via {pendingApproval.subAgentSubject || pendingApproval.subAgentName}
|
|
101
|
+
</Text>
|
|
102
|
+
)}
|
|
103
|
+
</Box>
|
|
104
|
+
|
|
105
|
+
<Box paddingLeft={2} marginTop={1} flexDirection="column">
|
|
106
|
+
<Box gap={1}>
|
|
107
|
+
<Text dimColor>Tool:</Text>
|
|
108
|
+
<Text bold>{toolLabel}</Text>
|
|
109
|
+
</Box>
|
|
110
|
+
{pendingApproval.argsPreview && (
|
|
111
|
+
<Box gap={1}>
|
|
112
|
+
<Text dimColor>Args:</Text>
|
|
113
|
+
<Text wrap="truncate-end">{pendingApproval.argsPreview}</Text>
|
|
114
|
+
</Box>
|
|
115
|
+
)}
|
|
116
|
+
</Box>
|
|
117
|
+
|
|
118
|
+
<Box gap={2} marginTop={1} paddingLeft={2}>
|
|
119
|
+
{OPTIONS.map((opt, idx) => (
|
|
120
|
+
<Text
|
|
121
|
+
key={opt.shortcut}
|
|
122
|
+
color={idx === selectedIndex ? opt.color : undefined}
|
|
123
|
+
dimColor={idx !== selectedIndex}
|
|
124
|
+
bold={idx === selectedIndex}
|
|
125
|
+
>
|
|
126
|
+
{idx === selectedIndex ? "▸ " : " "}
|
|
127
|
+
[{opt.shortcut}] {opt.label}
|
|
128
|
+
</Text>
|
|
129
|
+
))}
|
|
130
|
+
</Box>
|
|
131
|
+
|
|
132
|
+
{isSubmitting && (
|
|
133
|
+
<Box paddingLeft={2} marginTop={1}>
|
|
134
|
+
<Text dimColor>Submitting...</Text>
|
|
135
|
+
</Box>
|
|
136
|
+
)}
|
|
137
|
+
</Box>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import Spinner from "ink-spinner";
|
|
4
|
+
import { ExecutionPhase } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
5
|
+
|
|
6
|
+
/** Props for {@link ExecutionProgress}. */
|
|
7
|
+
export interface ExecutionProgressProps {
|
|
8
|
+
/** Current execution phase. */
|
|
9
|
+
readonly phase: ExecutionPhase;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface PhaseDisplay {
|
|
13
|
+
readonly label: string;
|
|
14
|
+
readonly color?: string;
|
|
15
|
+
readonly showSpinner: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const PHASE_DISPLAY: ReadonlyMap<ExecutionPhase, PhaseDisplay> = new Map([
|
|
19
|
+
[ExecutionPhase.EXECUTION_PENDING, { label: "Pending", showSpinner: true }],
|
|
20
|
+
[
|
|
21
|
+
ExecutionPhase.EXECUTION_IN_PROGRESS,
|
|
22
|
+
{ label: "Running", color: "yellow", showSpinner: true },
|
|
23
|
+
],
|
|
24
|
+
[
|
|
25
|
+
ExecutionPhase.EXECUTION_COMPLETED,
|
|
26
|
+
{ label: "Completed", color: "green", showSpinner: false },
|
|
27
|
+
],
|
|
28
|
+
[
|
|
29
|
+
ExecutionPhase.EXECUTION_FAILED,
|
|
30
|
+
{ label: "Failed", color: "red", showSpinner: false },
|
|
31
|
+
],
|
|
32
|
+
[
|
|
33
|
+
ExecutionPhase.EXECUTION_CANCELLED,
|
|
34
|
+
{ label: "Cancelled", showSpinner: false },
|
|
35
|
+
],
|
|
36
|
+
[
|
|
37
|
+
ExecutionPhase.EXECUTION_TERMINATED,
|
|
38
|
+
{ label: "Terminated", color: "red", showSpinner: false },
|
|
39
|
+
],
|
|
40
|
+
[
|
|
41
|
+
ExecutionPhase.EXECUTION_WAITING_FOR_APPROVAL,
|
|
42
|
+
{ label: "Waiting for approval", color: "yellow", showSpinner: false },
|
|
43
|
+
],
|
|
44
|
+
[ExecutionPhase.EXECUTION_PAUSED, { label: "Paused", showSpinner: false }],
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Displays the current execution phase as a compact terminal badge.
|
|
49
|
+
*
|
|
50
|
+
* Shows a spinner for active phases (pending, in-progress) and
|
|
51
|
+
* static indicators for terminal phases.
|
|
52
|
+
*
|
|
53
|
+
* Renders nothing for unspecified phases.
|
|
54
|
+
*/
|
|
55
|
+
export function ExecutionProgress({ phase }: ExecutionProgressProps) {
|
|
56
|
+
const display = PHASE_DISPLAY.get(phase);
|
|
57
|
+
if (!display) return null;
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<Box gap={1} paddingLeft={1}>
|
|
61
|
+
{display.showSpinner ? (
|
|
62
|
+
<Text color={display.color ?? "cyan"}>
|
|
63
|
+
<Spinner type="dots" />
|
|
64
|
+
</Text>
|
|
65
|
+
) : (
|
|
66
|
+
<Text color={display.color}>
|
|
67
|
+
{phase === ExecutionPhase.EXECUTION_COMPLETED ? "✓" : "●"}
|
|
68
|
+
</Text>
|
|
69
|
+
)}
|
|
70
|
+
<Text color={display.color} dimColor={!display.color}>
|
|
71
|
+
{display.label}
|
|
72
|
+
</Text>
|
|
73
|
+
</Box>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { Box, Text, useStdin } from "ink";
|
|
3
|
+
import TextInput from "ink-text-input";
|
|
4
|
+
|
|
5
|
+
/** Props for {@link FollowUpInput}. */
|
|
6
|
+
export interface FollowUpInputProps {
|
|
7
|
+
/** Called when the user submits a message (Enter key). */
|
|
8
|
+
readonly onSubmit: (message: string) => void;
|
|
9
|
+
/** Shows a "sending" indicator and disables input. */
|
|
10
|
+
readonly isSubmitting?: boolean;
|
|
11
|
+
/** Disables the input entirely. */
|
|
12
|
+
readonly disabled?: boolean;
|
|
13
|
+
/** Placeholder text. Default: "Reply..." */
|
|
14
|
+
readonly placeholder?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Terminal text input for sending follow-up messages in a session.
|
|
19
|
+
*
|
|
20
|
+
* Renders a single-line text input with a submit hint. Press Enter
|
|
21
|
+
* to submit, Ctrl+C to exit the application.
|
|
22
|
+
*/
|
|
23
|
+
export function FollowUpInput({
|
|
24
|
+
onSubmit,
|
|
25
|
+
isSubmitting = false,
|
|
26
|
+
disabled = false,
|
|
27
|
+
placeholder = "Reply...",
|
|
28
|
+
}: FollowUpInputProps) {
|
|
29
|
+
const [value, setValue] = useState("");
|
|
30
|
+
const { isRawModeSupported } = useStdin();
|
|
31
|
+
const isDisabled = disabled || isSubmitting || !isRawModeSupported;
|
|
32
|
+
|
|
33
|
+
const handleSubmit = (input: string) => {
|
|
34
|
+
const trimmed = input.trim();
|
|
35
|
+
if (!trimmed) return;
|
|
36
|
+
onSubmit(trimmed);
|
|
37
|
+
setValue("");
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
if (isDisabled) {
|
|
41
|
+
return (
|
|
42
|
+
<Box paddingLeft={1} paddingTop={1}>
|
|
43
|
+
<Text dimColor>
|
|
44
|
+
{isSubmitting ? "Sending..." : placeholder}
|
|
45
|
+
</Text>
|
|
46
|
+
</Box>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<Box paddingLeft={1} paddingTop={1} flexDirection="column">
|
|
52
|
+
<Box gap={1}>
|
|
53
|
+
<Text color="cyan" bold>
|
|
54
|
+
❯
|
|
55
|
+
</Text>
|
|
56
|
+
<TextInput
|
|
57
|
+
value={value}
|
|
58
|
+
onChange={setValue}
|
|
59
|
+
onSubmit={handleSubmit}
|
|
60
|
+
placeholder={placeholder}
|
|
61
|
+
/>
|
|
62
|
+
</Box>
|
|
63
|
+
<Box paddingLeft={2}>
|
|
64
|
+
<Text dimColor>
|
|
65
|
+
Enter to send · Ctrl+C to exit
|
|
66
|
+
</Text>
|
|
67
|
+
</Box>
|
|
68
|
+
</Box>
|
|
69
|
+
);
|
|
70
|
+
}
|