frogo 0.1.0 → 0.1.1

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.
@@ -1,14 +1,15 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
1
2
  import "dotenv/config";
2
- import readline from "node:readline/promises";
3
3
  import crypto from "node:crypto";
4
- import chalk from "chalk";
5
- import { stdin as input, stdout as output } from "node:process";
4
+ import { render } from "ink";
6
5
  import { ToolLoopAgent, stepCountIs } from "ai";
7
6
  import { createOpenAI } from "@ai-sdk/openai";
8
7
  import { createAnthropic } from "@ai-sdk/anthropic";
9
8
  import { createMCPClient } from "@ai-sdk/mcp";
10
9
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
11
10
  import { loadConfig } from "../config/load.js";
11
+ import { getSentryOAuthProvider, hasSentryTokens } from "../mcp/sentry-auth.js";
12
+ import { FrogoChatApp } from "./ui.js";
12
13
  const DEFAULT_SYSTEM_PROMPT = "You are Frogo, a deterministic incident investigator. Respond concisely, cite evidence, and avoid hallucinations. " +
13
14
  "If the user says 'do a pass' or 'anything you can get', fetch a minimal overview: projects, recent runs, datasets, and prompts (if available) " +
14
15
  "using MCP tools, then summarize what you found.";
@@ -87,6 +88,61 @@ async function buildLangSmithToolContext(config) {
87
88
  return null;
88
89
  }
89
90
  }
91
+ async function createSentryClient(sentry) {
92
+ const { provider } = await getSentryOAuthProvider();
93
+ return await createMCPClient({
94
+ transport: {
95
+ type: "http",
96
+ url: sentry.mcpUrl,
97
+ authProvider: provider
98
+ }
99
+ });
100
+ }
101
+ async function buildSentryToolContext(config) {
102
+ const sentry = config.sentry;
103
+ if (!sentry?.mcpUrl) {
104
+ return null;
105
+ }
106
+ try {
107
+ const hasTokens = await hasSentryTokens();
108
+ if (!hasTokens) {
109
+ console.log("↳ Sentry not authenticated. Run `frogo configure` to complete OAuth.");
110
+ return null;
111
+ }
112
+ const client = await createSentryClient(sentry);
113
+ const tools = await client.tools();
114
+ return { name: "Sentry", tools, client };
115
+ }
116
+ catch (error) {
117
+ console.error("Sentry MCP initialization failed:", error);
118
+ return null;
119
+ }
120
+ }
121
+ async function buildTriggerMcpToolContext(config) {
122
+ const triggerMcp = config.triggerMcp;
123
+ if (!triggerMcp?.command || !triggerMcp?.args || triggerMcp.args.length === 0) {
124
+ return null;
125
+ }
126
+ try {
127
+ const transport = new StdioClientTransport({
128
+ command: triggerMcp.command,
129
+ args: triggerMcp.args,
130
+ env: { ...process.env },
131
+ stderr: "inherit"
132
+ });
133
+ const client = await createMCPClient({
134
+ transport,
135
+ name: "frogo-trigger",
136
+ version: "0.1.0"
137
+ });
138
+ const tools = await client.tools();
139
+ return { name: "Trigger", tools, client };
140
+ }
141
+ catch (error) {
142
+ console.error("Trigger MCP initialization failed:", error);
143
+ return null;
144
+ }
145
+ }
90
146
  async function buildDatadogToolContext(config) {
91
147
  const datadog = config.datadog;
92
148
  if (!datadog?.apiKey || !datadog?.appKey) {
@@ -132,6 +188,14 @@ async function buildMcpToolContexts(config) {
132
188
  if (langsmithContext) {
133
189
  contexts.push(langsmithContext);
134
190
  }
191
+ const sentryContext = await buildSentryToolContext(config);
192
+ if (sentryContext) {
193
+ contexts.push(sentryContext);
194
+ }
195
+ const triggerMcpContext = await buildTriggerMcpToolContext(config);
196
+ if (triggerMcpContext) {
197
+ contexts.push(triggerMcpContext);
198
+ }
135
199
  const datadogContext = await buildDatadogToolContext(config);
136
200
  if (datadogContext) {
137
201
  contexts.push(datadogContext);
@@ -151,86 +215,9 @@ async function cleanupMcpContexts(contexts) {
151
215
  }
152
216
  }));
153
217
  }
154
- function createSpinner(label = "thinking") {
155
- const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧"];
156
- let index = 0;
157
- let handle;
158
- const isTty = Boolean(process.stdout.isTTY);
159
- const render = () => {
160
- const frame = frames[index];
161
- index = (index + 1) % frames.length;
162
- process.stdout.write(`\r${chalk.gray(frame)} ${chalk.dim(label)}`);
163
- };
164
- return {
165
- start() {
166
- if (!isTty) {
167
- process.stdout.write(`${chalk.dim(label)}...\n`);
168
- return;
169
- }
170
- render();
171
- handle = setInterval(render, 80);
172
- },
173
- stop() {
174
- if (!isTty) {
175
- return;
176
- }
177
- if (handle) {
178
- clearInterval(handle);
179
- handle = undefined;
180
- }
181
- process.stdout.write("\r");
182
- process.stdout.write(" ".repeat(32));
183
- process.stdout.write("\r");
184
- }
185
- };
186
- }
187
218
  function cleanAssistantOutput(content) {
188
219
  return content.replace(/^Agent:\s*/i, "").trim();
189
220
  }
190
- function renderBanner(context, mcpContexts) {
191
- const sessionId = crypto.randomBytes(8).toString("hex");
192
- const workdir = process.cwd();
193
- const providerName = context.languageModel.modelId ?? context.languageModel?.model ?? "unknown";
194
- const providerLabel = context.languageModel?.provider ?? "openai";
195
- const mcpLabel = mcpContexts.length ? mcpContexts.map((item) => item.name).join(", ") : "none";
196
- const header = "🐸 Frogo CLI";
197
- const headerLine = `│ ● ${header} │`;
198
- const sessionLines = [
199
- `│ session: ${sessionId} │`,
200
- `│ ↳ workdir: ${workdir} │`,
201
- `│ ↳ model: ${providerName} │`,
202
- `│ ↳ provider: ${providerLabel} │`,
203
- `│ ↳ mcp: ${mcpLabel} │`
204
- ];
205
- const headerWidth = Math.max(headerLine.length, ...sessionLines.map((line) => line.length));
206
- const drawLine = (text) => {
207
- const padded = text.padEnd(headerWidth - 1, " ");
208
- return `${padded}│`;
209
- };
210
- const top = `╭${"─".repeat(headerWidth - 1)}╮`;
211
- const bottom = `╰${"─".repeat(headerWidth - 1)}╯`;
212
- console.log(chalk.gray(top));
213
- console.log(chalk.gray(drawLine(headerLine)));
214
- console.log(chalk.gray(bottom));
215
- console.log(chalk.gray(top));
216
- sessionLines.forEach((line) => console.log(chalk.gray(drawLine(line))));
217
- console.log(chalk.gray(bottom));
218
- }
219
- function renderAssistantPrefix() {
220
- process.stdout.write(`${chalk.green("🐸")} `);
221
- }
222
- function formatToolPayload(payload, max = 400) {
223
- try {
224
- const raw = JSON.stringify(payload);
225
- if (raw.length <= max) {
226
- return raw;
227
- }
228
- return `${raw.slice(0, max)}…`;
229
- }
230
- catch {
231
- return "[unserializable]";
232
- }
233
- }
234
221
  export async function runAgentChat() {
235
222
  const config = await loadConfig();
236
223
  const context = buildProviderContext(config);
@@ -238,7 +225,6 @@ export async function runAgentChat() {
238
225
  process.exit(1);
239
226
  }
240
227
  const mcpContexts = await buildMcpToolContexts(config);
241
- renderBanner(context, mcpContexts);
242
228
  const toolSet = combineToolSets(mcpContexts);
243
229
  const agent = new ToolLoopAgent({
244
230
  id: "frogo-agent",
@@ -247,138 +233,11 @@ export async function runAgentChat() {
247
233
  tools: Object.keys(toolSet).length ? toolSet : undefined,
248
234
  stopWhen: stepCountIs(1000)
249
235
  });
250
- const rl = readline.createInterface({ input, output });
251
- const spinner = createSpinner();
252
- const conversation = [];
253
- let activeAbort = null;
254
- let isGenerating = false;
255
- console.log(chalk.dim("Type your question and press enter (Ctrl+C to exit)."));
256
- try {
257
- rl.on("SIGINT", () => {
258
- if (isGenerating && activeAbort) {
259
- activeAbort.abort();
260
- process.stdout.write(`\n${chalk.dim("↳ you canceled the current response. Ask another question when ready.")}\n`);
261
- return;
262
- }
263
- rl.close();
264
- });
265
- while (true) {
266
- let question;
267
- try {
268
- question = await rl.question(`${chalk.cyan("›")} `);
269
- }
270
- catch (error) {
271
- if (error instanceof Error && error.message === "SIGINT") {
272
- break;
273
- }
274
- throw error;
275
- }
276
- const trimmed = question.trim();
277
- if (!trimmed) {
278
- continue;
279
- }
280
- conversation.push({ role: "user", content: trimmed });
281
- spinner.start();
282
- isGenerating = true;
283
- activeAbort = new AbortController();
284
- let spinnerActive = true;
285
- let assistantReply = "";
286
- let prefixPrinted = false;
287
- let lineOpen = false;
288
- const ensurePrefix = () => {
289
- if (!prefixPrinted) {
290
- renderAssistantPrefix();
291
- prefixPrinted = true;
292
- lineOpen = true;
293
- }
294
- };
295
- const ensureNewline = () => {
296
- if (lineOpen) {
297
- process.stdout.write("\n");
298
- lineOpen = false;
299
- prefixPrinted = false;
300
- }
301
- };
302
- try {
303
- const streamResult = await agent.stream({
304
- messages: conversation,
305
- abortSignal: activeAbort.signal
306
- });
307
- for await (const part of streamResult.fullStream) {
308
- switch (part.type) {
309
- case "text-delta": {
310
- if (spinnerActive) {
311
- spinner.stop();
312
- spinnerActive = false;
313
- ensurePrefix();
314
- }
315
- ensurePrefix();
316
- process.stdout.write(part.text);
317
- assistantReply += part.text;
318
- break;
319
- }
320
- case "tool-call": {
321
- if (spinnerActive) {
322
- spinner.stop();
323
- spinnerActive = false;
324
- }
325
- ensureNewline();
326
- const payload = formatToolPayload(part.input);
327
- process.stdout.write(`${chalk.dim("↳ tool call")} ${chalk.cyan(part.toolName)} ${chalk.dim(payload)}\n`);
328
- break;
329
- }
330
- case "tool-result": {
331
- ensureNewline();
332
- const payload = formatToolPayload(part.output);
333
- process.stdout.write(`${chalk.dim("↳ tool result")} ${chalk.cyan(part.toolName)} ${chalk.dim(payload)}\n`);
334
- break;
335
- }
336
- case "tool-error": {
337
- ensureNewline();
338
- process.stdout.write(`${chalk.red("↳ tool error")} ${chalk.cyan(part.toolName)} ${chalk.dim(String(part.error))}\n`);
339
- break;
340
- }
341
- case "tool-approval-request": {
342
- ensureNewline();
343
- process.stdout.write(`${chalk.yellow("↳ tool approval")} ${chalk.cyan(part.toolCall.toolName)}\n`);
344
- break;
345
- }
346
- case "tool-output-denied": {
347
- ensureNewline();
348
- process.stdout.write(`${chalk.yellow("↳ tool output denied")} ${chalk.cyan(part.toolName)}\n`);
349
- break;
350
- }
351
- default:
352
- break;
353
- }
354
- }
355
- if (spinnerActive) {
356
- spinner.stop();
357
- }
358
- process.stdout.write("\n");
359
- const cleaned = cleanAssistantOutput(assistantReply);
360
- if (cleaned) {
361
- conversation.push({ role: "assistant", content: cleaned });
362
- }
363
- }
364
- catch (error) {
365
- spinner.stop();
366
- if (activeAbort?.signal.aborted) {
367
- console.log(`\n${chalk.dim("↳ you canceled the current response. Ask another question when ready.")}`);
368
- }
369
- else {
370
- console.error("Agent call failed:", error);
371
- break;
372
- }
373
- }
374
- finally {
375
- isGenerating = false;
376
- activeAbort = null;
377
- }
378
- }
379
- }
380
- finally {
381
- rl.close();
382
- await cleanupMcpContexts(mcpContexts);
383
- }
236
+ const sessionId = crypto.randomBytes(8).toString("hex");
237
+ const workdir = process.cwd();
238
+ const modelLabel = context.languageModel.modelId ?? context.languageModel?.model ?? "unknown";
239
+ const providerLabel = context.languageModel?.provider ?? "openai";
240
+ const app = render(_jsx(FrogoChatApp, { agent: agent, systemPrompt: context.systemPrompt, mcpContexts: mcpContexts, modelLabel: modelLabel, providerLabel: providerLabel, workdir: workdir, sessionId: sessionId }));
241
+ await app.waitUntilExit();
242
+ await cleanupMcpContexts(mcpContexts);
384
243
  }
@@ -0,0 +1,232 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
+ import { Box, Text, useApp, useInput } from "ink";
4
+ import TextInput from "ink-text-input";
5
+ import Spinner from "ink-spinner";
6
+ const makeId = (() => {
7
+ let i = 0;
8
+ return () => `${Date.now()}-${i++}`;
9
+ })();
10
+ function formatToolPayload(payload, max = 400) {
11
+ try {
12
+ const raw = JSON.stringify(payload);
13
+ if (raw.length <= max) {
14
+ return raw;
15
+ }
16
+ return `${raw.slice(0, max)}…`;
17
+ }
18
+ catch {
19
+ return "[unserializable]";
20
+ }
21
+ }
22
+ function renderHelpLines() {
23
+ return [
24
+ "Slash commands:",
25
+ "/mcp List MCP connections and tools",
26
+ "/mcp list Same as /mcp",
27
+ "/help Show help",
28
+ "/exit Exit Frogo"
29
+ ];
30
+ }
31
+ export function FrogoChatApp(props) {
32
+ const { agent, mcpContexts, modelLabel, providerLabel, workdir, sessionId } = props;
33
+ const { exit } = useApp();
34
+ const [input, setInput] = useState("");
35
+ const [messages, setMessages] = useState([]);
36
+ const [isGenerating, setIsGenerating] = useState(false);
37
+ const [slashHint, setSlashHint] = useState(false);
38
+ const [slashIndex, setSlashIndex] = useState(0);
39
+ const abortRef = useRef(null);
40
+ const conversationRef = useRef([]);
41
+ const slashOptions = useMemo(() => ["/mcp", "/help", "/exit"], []);
42
+ const toolSummary = useMemo(() => {
43
+ if (!mcpContexts.length) {
44
+ return "none";
45
+ }
46
+ return mcpContexts.map((ctx) => ctx.name).join(", ");
47
+ }, [mcpContexts]);
48
+ const addMessage = useCallback((msg) => {
49
+ setMessages((prev) => [...prev, msg]);
50
+ }, []);
51
+ const updateMessage = useCallback((id, content) => {
52
+ setMessages((prev) => prev.map((msg) => (msg.id === id ? { ...msg, content } : msg)));
53
+ }, []);
54
+ useEffect(() => {
55
+ setSlashHint(input === "/");
56
+ if (input !== "/") {
57
+ setSlashIndex(0);
58
+ }
59
+ }, [input]);
60
+ useInput((_, key) => {
61
+ if (!key)
62
+ return;
63
+ const keyName = key.name;
64
+ if (key.ctrl && keyName === "c") {
65
+ if (isGenerating && abortRef.current) {
66
+ abortRef.current.abort();
67
+ addMessage({
68
+ id: makeId(),
69
+ role: "system",
70
+ content: "↳ you canceled the current response. Ask another question when ready."
71
+ });
72
+ return;
73
+ }
74
+ exit();
75
+ }
76
+ if (!isGenerating && slashHint) {
77
+ if (key.upArrow || keyName === "up") {
78
+ setSlashIndex((prev) => (prev - 1 + slashOptions.length) % slashOptions.length);
79
+ setInput("/");
80
+ }
81
+ else if (key.downArrow || keyName === "down") {
82
+ setSlashIndex((prev) => (prev + 1) % slashOptions.length);
83
+ setInput("/");
84
+ }
85
+ }
86
+ });
87
+ const handleSlashCommand = useCallback((command) => {
88
+ if (command === "/" || command === "/help") {
89
+ renderHelpLines().forEach((line) => {
90
+ addMessage({ id: makeId(), role: "system", content: line });
91
+ });
92
+ return true;
93
+ }
94
+ if (command === "/mcp" || command === "/mcp list") {
95
+ if (!mcpContexts.length) {
96
+ addMessage({ id: makeId(), role: "system", content: "No MCP integrations connected." });
97
+ return true;
98
+ }
99
+ mcpContexts.forEach((ctx) => {
100
+ const tools = Object.keys(ctx.tools);
101
+ addMessage({
102
+ id: makeId(),
103
+ role: "system",
104
+ content: `${ctx.name} (${tools.length} tools)`
105
+ });
106
+ if (tools.length) {
107
+ addMessage({
108
+ id: makeId(),
109
+ role: "system",
110
+ content: `tools: ${tools.join(", ")}`
111
+ });
112
+ }
113
+ });
114
+ return true;
115
+ }
116
+ if (command === "/exit") {
117
+ exit();
118
+ return true;
119
+ }
120
+ addMessage({ id: makeId(), role: "system", content: `Unknown command: ${command}` });
121
+ return true;
122
+ }, [addMessage, exit, mcpContexts]);
123
+ const handleSubmit = useCallback(async (value) => {
124
+ const trimmed = value.trim();
125
+ setInput("");
126
+ if (!trimmed) {
127
+ return;
128
+ }
129
+ if (trimmed === "/" && slashHint) {
130
+ handleSlashCommand(slashOptions[slashIndex]);
131
+ return;
132
+ }
133
+ if (slashHint) {
134
+ handleSlashCommand(trimmed);
135
+ return;
136
+ }
137
+ if (trimmed.startsWith("/")) {
138
+ handleSlashCommand(trimmed);
139
+ return;
140
+ }
141
+ addMessage({ id: makeId(), role: "user", content: trimmed });
142
+ conversationRef.current.push({ role: "user", content: trimmed });
143
+ const assistantId = makeId();
144
+ addMessage({ id: assistantId, role: "assistant", content: "" });
145
+ abortRef.current = new AbortController();
146
+ setIsGenerating(true);
147
+ try {
148
+ const streamResult = await agent.stream({
149
+ messages: conversationRef.current,
150
+ abortSignal: abortRef.current.signal
151
+ });
152
+ let assistantContent = "";
153
+ for await (const part of streamResult.fullStream) {
154
+ if (part.type === "text-delta") {
155
+ assistantContent += part.text;
156
+ updateMessage(assistantId, assistantContent);
157
+ }
158
+ else if (part.type === "tool-call") {
159
+ addMessage({
160
+ id: makeId(),
161
+ role: "tool",
162
+ content: `↳ tool call ${part.toolName} ${formatToolPayload(part.input)}`
163
+ });
164
+ }
165
+ else if (part.type === "tool-result") {
166
+ addMessage({
167
+ id: makeId(),
168
+ role: "tool",
169
+ content: `↳ tool result ${part.toolName} ${formatToolPayload(part.output)}`
170
+ });
171
+ }
172
+ else if (part.type === "tool-error") {
173
+ addMessage({
174
+ id: makeId(),
175
+ role: "tool",
176
+ content: `↳ tool error ${part.toolName} ${String(part.error)}`
177
+ });
178
+ }
179
+ else if (part.type === "tool-output-denied") {
180
+ const toolName = part.toolName ?? "tool";
181
+ addMessage({
182
+ id: makeId(),
183
+ role: "tool",
184
+ content: `↳ tool output denied ${toolName}`
185
+ });
186
+ }
187
+ else if (part.type === "tool-approval-request") {
188
+ addMessage({
189
+ id: makeId(),
190
+ role: "tool",
191
+ content: `↳ tool approval ${part.toolCall.toolName}`
192
+ });
193
+ }
194
+ }
195
+ const cleaned = assistantContent.replace(/^Agent:\\s*/i, "").trim();
196
+ updateMessage(assistantId, cleaned);
197
+ conversationRef.current.push({ role: "assistant", content: cleaned });
198
+ }
199
+ catch (error) {
200
+ if (abortRef.current?.signal.aborted) {
201
+ addMessage({
202
+ id: makeId(),
203
+ role: "system",
204
+ content: "↳ you canceled the current response. Ask another question when ready."
205
+ });
206
+ }
207
+ else {
208
+ addMessage({
209
+ id: makeId(),
210
+ role: "system",
211
+ content: `Agent call failed: ${error instanceof Error ? error.message : String(error)}`
212
+ });
213
+ }
214
+ }
215
+ finally {
216
+ abortRef.current = null;
217
+ setIsGenerating(false);
218
+ }
219
+ }, [addMessage, agent, handleSlashCommand, slashHint, slashIndex, slashOptions, updateMessage]);
220
+ return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "gray", children: "\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E" }), _jsx(Text, { color: "gray", children: "\u2502 \u25CF \uD83D\uDC38 Frogo CLI \u2502 \u2502" }), _jsx(Text, { color: "gray", children: "\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F" }), _jsx(Text, { color: "gray", children: "\u256D\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256E" }), _jsxs(Text, { color: "gray", children: ["\u2502 session: ", sessionId, " \u2502"] }), _jsxs(Text, { color: "gray", children: ["\u2502 \u21B3 workdir: ", workdir, " \u2502"] }), _jsxs(Text, { color: "gray", children: ["\u2502 \u21B3 model: ", modelLabel, " \u2502"] }), _jsxs(Text, { color: "gray", children: ["\u2502 \u21B3 provider: ", providerLabel, " \u2502"] }), _jsxs(Text, { color: "gray", children: ["\u2502 \u21B3 mcp: ", toolSummary, " \u2502"] }), _jsx(Text, { color: "gray", children: "\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256F" })] }), _jsx(Box, { flexDirection: "column", flexGrow: 1, marginTop: 1, children: messages.map((msg) => {
221
+ if (msg.role === "user") {
222
+ return (_jsxs(Text, { color: "cyan", children: ["\u203A ", msg.content] }, msg.id));
223
+ }
224
+ if (msg.role === "assistant") {
225
+ return (_jsxs(Text, { color: "green", children: ["\uD83D\uDC38 ", msg.content] }, msg.id));
226
+ }
227
+ if (msg.role === "tool") {
228
+ return (_jsx(Text, { color: "gray", children: msg.content }, msg.id));
229
+ }
230
+ return (_jsx(Text, { color: "gray", children: msg.content }, msg.id));
231
+ }) }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: "cyan", children: "\u203A " }), _jsx(TextInput, { value: input, onChange: setInput, onSubmit: handleSubmit }), isGenerating ? (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { color: "gray", children: [_jsx(Spinner, { type: "dots" }), " thinking"] }) })) : null] }), slashHint ? (_jsx(Box, { marginTop: 1, flexDirection: "column", children: slashOptions.map((option, idx) => (_jsxs(Text, { color: idx === slashIndex ? "cyan" : "gray", children: [idx === slashIndex ? "› " : " ", option] }, option))) })) : null] }));
232
+ }