@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.
Files changed (85) hide show
  1. package/LICENSE +190 -0
  2. package/app/SessionApp.d.ts +42 -0
  3. package/app/SessionApp.d.ts.map +1 -0
  4. package/app/SessionApp.js +38 -0
  5. package/app/SessionApp.js.map +1 -0
  6. package/app/SessionView.d.ts +32 -0
  7. package/app/SessionView.d.ts.map +1 -0
  8. package/app/SessionView.js +58 -0
  9. package/app/SessionView.js.map +1 -0
  10. package/cli/stigmer-ink.d.ts +3 -0
  11. package/cli/stigmer-ink.d.ts.map +1 -0
  12. package/cli/stigmer-ink.js +117 -0
  13. package/cli/stigmer-ink.js.map +1 -0
  14. package/components/ApprovalPrompt.d.ts +21 -0
  15. package/components/ApprovalPrompt.d.ts.map +1 -0
  16. package/components/ApprovalPrompt.js +60 -0
  17. package/components/ApprovalPrompt.js.map +1 -0
  18. package/components/ExecutionProgress.d.ts +16 -0
  19. package/components/ExecutionProgress.d.ts.map +1 -0
  20. package/components/ExecutionProgress.js +47 -0
  21. package/components/ExecutionProgress.js.map +1 -0
  22. package/components/FollowUpInput.d.ts +19 -0
  23. package/components/FollowUpInput.d.ts.map +1 -0
  24. package/components/FollowUpInput.js +27 -0
  25. package/components/FollowUpInput.js.map +1 -0
  26. package/components/MessageEntry.d.ts +18 -0
  27. package/components/MessageEntry.d.ts.map +1 -0
  28. package/components/MessageEntry.js +42 -0
  29. package/components/MessageEntry.js.map +1 -0
  30. package/components/MessageThread.d.ts +31 -0
  31. package/components/MessageThread.d.ts.map +1 -0
  32. package/components/MessageThread.js +146 -0
  33. package/components/MessageThread.js.map +1 -0
  34. package/components/SubAgentBlock.d.ts +19 -0
  35. package/components/SubAgentBlock.d.ts.map +1 -0
  36. package/components/SubAgentBlock.js +73 -0
  37. package/components/SubAgentBlock.js.map +1 -0
  38. package/components/TodoList.d.ts +17 -0
  39. package/components/TodoList.d.ts.map +1 -0
  40. package/components/TodoList.js +43 -0
  41. package/components/TodoList.js.map +1 -0
  42. package/components/ToolCallGroup.d.ts +20 -0
  43. package/components/ToolCallGroup.d.ts.map +1 -0
  44. package/components/ToolCallGroup.js +51 -0
  45. package/components/ToolCallGroup.js.map +1 -0
  46. package/components/ToolCallItem.d.ts +14 -0
  47. package/components/ToolCallItem.d.ts.map +1 -0
  48. package/components/ToolCallItem.js +33 -0
  49. package/components/ToolCallItem.js.map +1 -0
  50. package/components/UsageWidget.d.ts +16 -0
  51. package/components/UsageWidget.d.ts.map +1 -0
  52. package/components/UsageWidget.js +18 -0
  53. package/components/UsageWidget.js.map +1 -0
  54. package/index.d.ts +16 -0
  55. package/index.d.ts.map +1 -0
  56. package/index.js +21 -0
  57. package/index.js.map +1 -0
  58. package/markdown.d.ts +21 -0
  59. package/markdown.d.ts.map +1 -0
  60. package/markdown.js +44 -0
  61. package/markdown.js.map +1 -0
  62. package/package.json +48 -0
  63. package/provider.d.ts +46 -0
  64. package/provider.d.ts.map +1 -0
  65. package/provider.js +33 -0
  66. package/provider.js.map +1 -0
  67. package/src/__tests__/components.test.tsx +162 -0
  68. package/src/__tests__/markdown.test.ts +46 -0
  69. package/src/app/SessionApp.tsx +74 -0
  70. package/src/app/SessionView.tsx +164 -0
  71. package/src/cli/stigmer-ink.tsx +148 -0
  72. package/src/components/ApprovalPrompt.tsx +139 -0
  73. package/src/components/ExecutionProgress.tsx +75 -0
  74. package/src/components/FollowUpInput.tsx +70 -0
  75. package/src/components/MessageEntry.tsx +80 -0
  76. package/src/components/MessageThread.tsx +264 -0
  77. package/src/components/SubAgentBlock.tsx +146 -0
  78. package/src/components/TodoList.tsx +75 -0
  79. package/src/components/ToolCallGroup.tsx +92 -0
  80. package/src/components/ToolCallItem.tsx +74 -0
  81. package/src/components/UsageWidget.tsx +35 -0
  82. package/src/index.ts +28 -0
  83. package/src/markdown.ts +48 -0
  84. package/src/provider.tsx +62 -0
  85. 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
+ }