@fastino-ai/pioneer-cli 0.1.0 → 0.2.0

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.
@@ -0,0 +1,701 @@
1
+ /**
2
+ * ChatApp - Main interactive chat interface using Ink
3
+ */
4
+
5
+ import React, { useState, useEffect, useCallback, useRef } from "react";
6
+ import { Box, Text, useApp, useInput, useStdin } from "ink";
7
+ import TextInput from "ink-text-input";
8
+ import Spinner from "ink-spinner";
9
+
10
+ import { Agent, AgentEvents } from "../agent/Agent.js";
11
+ import type { Message, ToolCall, ToolResult, AgentConfig } from "../agent/types.js";
12
+ import type { FileReference } from "../agent/FileResolver.js";
13
+ import { createBashTool } from "../tools/bash.js";
14
+ import { createFilesystemTools } from "../tools/filesystem.js";
15
+ import { createSandboxTool } from "../tools/sandbox.js";
16
+
17
+ // ─────────────────────────────────────────────────────────────────────────────
18
+ // Types
19
+ // ─────────────────────────────────────────────────────────────────────────────
20
+
21
+ interface ChatMessage {
22
+ role: "user" | "assistant" | "system" | "tool";
23
+ content: string;
24
+ timestamp: Date;
25
+ toolName?: string;
26
+ isStreaming?: boolean;
27
+ }
28
+
29
+ interface ChatState {
30
+ messages: ChatMessage[];
31
+ isProcessing: boolean;
32
+ streamingContent: string;
33
+ currentToolCall: string | null;
34
+ fileReferences: FileReference[];
35
+ error: string | null;
36
+ }
37
+
38
+ // ─────────────────────────────────────────────────────────────────────────────
39
+ // Message Display Components
40
+ // ─────────────────────────────────────────────────────────────────────────────
41
+
42
+ const MessageBubble: React.FC<{ message: ChatMessage }> = ({ message }) => {
43
+ const roleColors: Record<string, string> = {
44
+ user: "cyan",
45
+ assistant: "green",
46
+ system: "yellow",
47
+ tool: "magenta",
48
+ };
49
+
50
+ const roleLabels: Record<string, string> = {
51
+ user: "You",
52
+ assistant: "Agent",
53
+ system: "System",
54
+ tool: `Tool${message.toolName ? `: ${message.toolName}` : ""}`,
55
+ };
56
+
57
+ const color = roleColors[message.role] || "white";
58
+ const label = roleLabels[message.role] || message.role;
59
+
60
+ // Truncate long tool outputs
61
+ let content = message.content;
62
+ if (message.role === "tool" && content.length > 1000) {
63
+ content = content.slice(0, 1000) + "\n... (truncated)";
64
+ }
65
+
66
+ return (
67
+ <Box flexDirection="column" marginBottom={1}>
68
+ <Text color={color} bold>
69
+ {label}:
70
+ </Text>
71
+ <Box marginLeft={2}>
72
+ <Text wrap="wrap">{content}</Text>
73
+ </Box>
74
+ </Box>
75
+ );
76
+ };
77
+
78
+ const StreamingMessage: React.FC<{ content: string }> = ({ content }) => (
79
+ <Box flexDirection="column" marginBottom={1}>
80
+ <Text color="green" bold>
81
+ Agent: <Text color="gray">(streaming...)</Text>
82
+ </Text>
83
+ <Box marginLeft={2}>
84
+ <Text wrap="wrap">{content || "..."}</Text>
85
+ </Box>
86
+ </Box>
87
+ );
88
+
89
+ const ToolCallIndicator: React.FC<{ toolName: string }> = ({ toolName }) => (
90
+ <Box marginBottom={1}>
91
+ <Text color="magenta">
92
+ <Spinner type="dots" /> Calling tool: {toolName}
93
+ </Text>
94
+ </Box>
95
+ );
96
+
97
+ const FileReferencesIndicator: React.FC<{ refs: FileReference[] }> = ({ refs }) => {
98
+ if (refs.length === 0) return null;
99
+
100
+ return (
101
+ <Box flexDirection="column" marginBottom={1}>
102
+ <Text color="blue" bold>📎 Referenced files:</Text>
103
+ {refs.map((ref, i) => (
104
+ <Box key={i} marginLeft={2}>
105
+ <Text color={ref.exists ? "blue" : "red"}>
106
+ {ref.exists ? "✓" : "✗"} {ref.relativePath}
107
+ {ref.isDirectory ? "/" : ""}
108
+ {ref.error ? ` (${ref.error})` : ""}
109
+ </Text>
110
+ </Box>
111
+ ))}
112
+ </Box>
113
+ );
114
+ };
115
+
116
+ // File autocomplete dropdown
117
+ const FileSuggestions: React.FC<{
118
+ suggestions: string[];
119
+ selectedIndex: number;
120
+ searchPath: string;
121
+ }> = ({ suggestions, selectedIndex, searchPath }) => {
122
+ if (suggestions.length === 0) return null;
123
+
124
+ return (
125
+ <Box flexDirection="column" borderStyle="round" borderColor="blue" paddingX={1} marginBottom={1}>
126
+ <Text color="blue" dimColor>
127
+ 📁 {searchPath || "./"} <Text color="gray">(↑↓ navigate, Tab select, Esc close)</Text>
128
+ </Text>
129
+ {suggestions.slice(0, 10).map((suggestion, i) => (
130
+ <Box key={i}>
131
+ <Text
132
+ color={i === selectedIndex ? "black" : "white"}
133
+ backgroundColor={i === selectedIndex ? "blue" : undefined}
134
+ >
135
+ {" "}{suggestion.endsWith("/") ? "📁" : "📄"} {suggestion}{" "}
136
+ </Text>
137
+ </Box>
138
+ ))}
139
+ {suggestions.length > 10 && (
140
+ <Text dimColor> ... and {suggestions.length - 10} more</Text>
141
+ )}
142
+ </Box>
143
+ );
144
+ };
145
+
146
+ // Extract @path from end of input
147
+ function extractAtPath(input: string): { path: string; start: number } | null {
148
+ // Match @ followed by path characters at end of string
149
+ const match = input.match(/@([\w\-.\/]*)$/);
150
+ if (!match) return null;
151
+ return { path: match[1], start: input.length - match[0].length };
152
+ }
153
+
154
+ const StatusBar: React.FC<{ budgetSummary: string; isProcessing: boolean }> = ({
155
+ budgetSummary,
156
+ isProcessing,
157
+ }) => (
158
+ <Box
159
+ borderStyle="single"
160
+ borderColor="gray"
161
+ paddingX={1}
162
+ marginTop={1}
163
+ >
164
+ <Text dimColor>
165
+ {isProcessing ? "Processing... | " : ""}
166
+ {budgetSummary}
167
+ </Text>
168
+ </Box>
169
+ );
170
+
171
+ // ─────────────────────────────────────────────────────────────────────────────
172
+ // Main Chat Component
173
+ // ─────────────────────────────────────────────────────────────────────────────
174
+
175
+ export interface ChatAppProps {
176
+ config: AgentConfig;
177
+ initialMessage?: string;
178
+ }
179
+
180
+ export const ChatApp: React.FC<ChatAppProps> = ({ config, initialMessage }) => {
181
+ const { exit } = useApp();
182
+ const { isRawModeSupported } = useStdin();
183
+
184
+ const [state, setState] = useState<ChatState>({
185
+ messages: [],
186
+ isProcessing: false,
187
+ streamingContent: "",
188
+ currentToolCall: null,
189
+ fileReferences: [],
190
+ error: null,
191
+ });
192
+
193
+ const [input, setInput] = useState("");
194
+ const [agent, setAgent] = useState<Agent | null>(null);
195
+ const [budgetSummary, setBudgetSummary] = useState("Initializing...");
196
+
197
+ // Autocomplete state
198
+ const [suggestions, setSuggestions] = useState<string[]>([]);
199
+ const [selectedIndex, setSelectedIndex] = useState(0);
200
+ const [showSuggestions, setShowSuggestions] = useState(false);
201
+ const [currentAtPath, setCurrentAtPath] = useState<{ path: string; start: number } | null>(null);
202
+
203
+ // Streaming content refs for throttled updates
204
+ const streamBufferRef = useRef("");
205
+ const streamUpdateTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
206
+
207
+ // Initialize agent
208
+ useEffect(() => {
209
+ const newAgent = new Agent(config, {
210
+ onStream: (chunk) => {
211
+ // Accumulate in buffer
212
+ streamBufferRef.current += chunk;
213
+
214
+ // Throttle UI updates to every 50ms
215
+ if (!streamUpdateTimerRef.current) {
216
+ streamUpdateTimerRef.current = setTimeout(() => {
217
+ setState((s) => ({
218
+ ...s,
219
+ streamingContent: streamBufferRef.current,
220
+ }));
221
+ streamUpdateTimerRef.current = null;
222
+ }, 50);
223
+ }
224
+ },
225
+ onToolCall: (toolCall) => {
226
+ setState((s) => ({
227
+ ...s,
228
+ currentToolCall: toolCall.name,
229
+ }));
230
+ },
231
+ onToolResult: (result) => {
232
+ setState((s) => ({
233
+ ...s,
234
+ currentToolCall: null,
235
+ messages: [
236
+ ...s.messages,
237
+ {
238
+ role: "tool",
239
+ content: result.output || result.error || "(no output)",
240
+ timestamp: new Date(),
241
+ toolName: s.currentToolCall || undefined,
242
+ },
243
+ ],
244
+ }));
245
+ },
246
+ onBudgetWarning: (warnings) => {
247
+ setState((s) => ({
248
+ ...s,
249
+ messages: [
250
+ ...s.messages,
251
+ {
252
+ role: "system",
253
+ content: `⚠️ Budget warning: ${warnings.join(", ")}`,
254
+ timestamp: new Date(),
255
+ },
256
+ ],
257
+ }));
258
+ },
259
+ onFileReference: (refs) => {
260
+ setState((s) => ({
261
+ ...s,
262
+ fileReferences: refs,
263
+ }));
264
+ },
265
+ onError: (error) => {
266
+ setState((s) => ({
267
+ ...s,
268
+ error: error.message,
269
+ isProcessing: false,
270
+ }));
271
+ },
272
+ });
273
+
274
+ // Register tools
275
+ newAgent.registerTool(createBashTool({ cwd: process.cwd() }));
276
+ newAgent.registerTools(createFilesystemTools());
277
+ newAgent.registerTool(createSandboxTool());
278
+
279
+ setAgent(newAgent);
280
+
281
+ // Update budget summary - only when agent is initialized
282
+ setBudgetSummary(newAgent.getBudgetStatus().summary);
283
+
284
+ return () => { };
285
+ }, [config]);
286
+
287
+ // Update file suggestions when input changes
288
+ useEffect(() => {
289
+ if (!agent || state.isProcessing) {
290
+ // Only update if currently showing suggestions
291
+ if (showSuggestions) {
292
+ setSuggestions([]);
293
+ setShowSuggestions(false);
294
+ }
295
+ return;
296
+ }
297
+
298
+ const atPath = extractAtPath(input);
299
+
300
+ // Only update currentAtPath if it changed
301
+ if (JSON.stringify(atPath) !== JSON.stringify(currentAtPath)) {
302
+ setCurrentAtPath(atPath);
303
+ }
304
+
305
+ if (atPath) {
306
+ const newSuggestions = agent.getFileSuggestions(atPath.path);
307
+ setSuggestions(newSuggestions);
308
+ setShowSuggestions(newSuggestions.length > 0);
309
+ setSelectedIndex(0);
310
+ } else if (showSuggestions) {
311
+ // Only clear if currently showing
312
+ setSuggestions([]);
313
+ setShowSuggestions(false);
314
+ }
315
+ }, [input, agent, state.isProcessing]);
316
+
317
+ // Handle initial message if provided
318
+ useEffect(() => {
319
+ if (agent && initialMessage && state.messages.length === 0) {
320
+ handleSubmit(initialMessage);
321
+ }
322
+ }, [agent, initialMessage]);
323
+
324
+ const handleSubmit = useCallback(
325
+ async (value: string) => {
326
+ if (!agent || !value.trim() || state.isProcessing) return;
327
+
328
+ const userMessage = value.trim();
329
+ setInput("");
330
+
331
+ // Handle special commands
332
+ if (userMessage === "/exit" || userMessage === "/quit") {
333
+ exit();
334
+ return;
335
+ }
336
+
337
+ if (userMessage === "/clear") {
338
+ agent.clearHistory();
339
+ streamBufferRef.current = "";
340
+ setState({
341
+ messages: [],
342
+ isProcessing: false,
343
+ streamingContent: "",
344
+ currentToolCall: null,
345
+ fileReferences: [],
346
+ error: null,
347
+ });
348
+ return;
349
+ }
350
+
351
+ if (userMessage === "/help") {
352
+ setState((s) => ({
353
+ ...s,
354
+ messages: [
355
+ ...s.messages,
356
+ {
357
+ role: "system",
358
+ content: `Available commands:
359
+ /clear - Clear conversation history
360
+ /help - Show this help
361
+ /exit - Exit the chat
362
+ /tools - List available tools
363
+ /model - Show/switch model
364
+ /budget - Show budget status
365
+
366
+ Budget adjustment:
367
+ /budget tokens <n> - Set max tokens (0=unlimited)
368
+ /budget cost <n> - Set max cost in USD
369
+ /budget time <n> - Set max time in seconds
370
+ /budget tools <n> - Set max tool calls per turn
371
+ /budget unlimited - Remove all limits
372
+
373
+ File references:
374
+ @file.ts - Include file contents
375
+ @src/ - Include directory listing
376
+ @package.json - Include any file by path`,
377
+ timestamp: new Date(),
378
+ },
379
+ ],
380
+ }));
381
+ return;
382
+ }
383
+
384
+ if (userMessage === "/tools") {
385
+ const tools = agent.getTools();
386
+ const toolList = tools
387
+ .map((t) => ` • ${t.name}: ${t.description}`)
388
+ .join("\n");
389
+ setState((s) => ({
390
+ ...s,
391
+ messages: [
392
+ ...s.messages,
393
+ {
394
+ role: "system",
395
+ content: `Available tools:\n${toolList}`,
396
+ timestamp: new Date(),
397
+ },
398
+ ],
399
+ }));
400
+ return;
401
+ }
402
+
403
+ if (userMessage === "/model") {
404
+ const info = agent.getModelInfo();
405
+ const modelHelp = `🤖 Current Model
406
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
407
+ Provider: ${info.provider}
408
+ Model: ${info.model}
409
+
410
+ Switch with /model <name>. Examples:
411
+ Anthropic (Claude 4.5):
412
+ claude-opus-4-5-20251101 (strongest)
413
+ claude-sonnet-4-5-20250929 (balanced)
414
+ claude-haiku-4-5-20251001 (fast/cheap)
415
+ OpenAI:
416
+ gpt-4o, gpt-4o-mini, o1, o3-mini`;
417
+ setState((s) => ({
418
+ ...s,
419
+ messages: [...s.messages, { role: "system", content: modelHelp, timestamp: new Date() }],
420
+ }));
421
+ return;
422
+ }
423
+
424
+ if (userMessage.startsWith("/model ")) {
425
+ const newModel = userMessage.slice(7).trim();
426
+ if (newModel) {
427
+ agent.setModel(newModel);
428
+ setState((s) => ({
429
+ ...s,
430
+ messages: [...s.messages, { role: "system", content: `✓ Switched to model: ${newModel}`, timestamp: new Date() }],
431
+ }));
432
+ }
433
+ return;
434
+ }
435
+
436
+ if (userMessage === "/budget") {
437
+ const status = agent.getBudgetStatus();
438
+ const maxTools = agent.getMaxToolCalls();
439
+ const budgetInfo = `📊 Budget Status
440
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
441
+ ${status.summary}
442
+ Max tool calls: ${maxTools}
443
+
444
+ ⚙️ Adjust Limits
445
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
446
+ /budget tokens <n> Set max tokens (0 = unlimited)
447
+ /budget cost <n> Set max cost in USD (0 = unlimited)
448
+ /budget time <n> Set max time in seconds (0 = unlimited)
449
+ /budget tools <n> Set max tool calls per turn (0 = unlimited)
450
+ /budget unlimited Remove all limits`;
451
+
452
+ setState((s) => ({
453
+ ...s,
454
+ messages: [
455
+ ...s.messages,
456
+ { role: "system", content: budgetInfo, timestamp: new Date() },
457
+ ],
458
+ }));
459
+ return;
460
+ }
461
+
462
+ // Budget adjustment commands
463
+ if (userMessage.startsWith("/budget ")) {
464
+ const parts = userMessage.split(" ");
465
+ const subCmd = parts[1];
466
+ const value = parts[2];
467
+
468
+ let message = "";
469
+ if (subCmd === "unlimited") {
470
+ agent.updateBudget({ maxTokens: undefined, maxCost: undefined, maxTime: undefined, maxIterations: undefined });
471
+ agent.setMaxToolCalls(1000);
472
+ message = "✓ All limits removed";
473
+ } else if (subCmd === "tokens" && value) {
474
+ const val = parseInt(value, 10);
475
+ agent.updateBudget({ maxTokens: val === 0 ? undefined : val });
476
+ message = val === 0 ? "✓ Token limit removed" : `✓ Token limit set to ${val.toLocaleString()}`;
477
+ } else if (subCmd === "cost" && value) {
478
+ const val = parseFloat(value);
479
+ agent.updateBudget({ maxCost: val === 0 ? undefined : val });
480
+ message = val === 0 ? "✓ Cost limit removed" : `✓ Cost limit set to $${val}`;
481
+ } else if (subCmd === "time" && value) {
482
+ const val = parseInt(value, 10);
483
+ agent.updateBudget({ maxTime: val === 0 ? undefined : val });
484
+ message = val === 0 ? "✓ Time limit removed" : `✓ Time limit set to ${val}s`;
485
+ } else if (subCmd === "tools" && value) {
486
+ const val = parseInt(value, 10);
487
+ agent.setMaxToolCalls(val === 0 ? 1000 : val);
488
+ message = val === 0 ? "✓ Tool call limit removed" : `✓ Max tool calls set to ${val}`;
489
+ } else {
490
+ message = "Usage: /budget [tokens|cost|time|tools] <value> or /budget unlimited";
491
+ }
492
+
493
+ setState((s) => ({
494
+ ...s,
495
+ messages: [...s.messages, { role: "system", content: message, timestamp: new Date() }],
496
+ }));
497
+ setBudgetSummary(agent.getBudgetStatus().summary);
498
+ return;
499
+ }
500
+
501
+ // Add user message
502
+ streamBufferRef.current = "";
503
+ if (streamUpdateTimerRef.current) {
504
+ clearTimeout(streamUpdateTimerRef.current);
505
+ streamUpdateTimerRef.current = null;
506
+ }
507
+ setState((s) => ({
508
+ ...s,
509
+ messages: [
510
+ ...s.messages,
511
+ { role: "user", content: userMessage, timestamp: new Date() },
512
+ ],
513
+ isProcessing: true,
514
+ streamingContent: "",
515
+ fileReferences: [],
516
+ error: null,
517
+ }));
518
+
519
+ try {
520
+ const response = await agent.chat(userMessage, true);
521
+
522
+ // Clear any pending stream update
523
+ if (streamUpdateTimerRef.current) {
524
+ clearTimeout(streamUpdateTimerRef.current);
525
+ streamUpdateTimerRef.current = null;
526
+ }
527
+ streamBufferRef.current = "";
528
+
529
+ setState((s) => ({
530
+ ...s,
531
+ messages: [
532
+ ...s.messages,
533
+ { role: "assistant", content: response, timestamp: new Date() },
534
+ ],
535
+ isProcessing: false,
536
+ streamingContent: "",
537
+ }));
538
+ } catch (error) {
539
+ streamBufferRef.current = "";
540
+ setState((s) => ({
541
+ ...s,
542
+ error: error instanceof Error ? error.message : String(error),
543
+ isProcessing: false,
544
+ }));
545
+ }
546
+
547
+ setBudgetSummary(agent.getBudgetStatus().summary);
548
+ },
549
+ [agent, state.isProcessing, exit]
550
+ );
551
+
552
+ // Insert selected suggestion into input
553
+ const insertSuggestion = useCallback((suggestion: string) => {
554
+ if (!currentAtPath) return;
555
+ const before = input.slice(0, currentAtPath.start);
556
+ const newInput = before + "@" + suggestion;
557
+ setInput(newInput);
558
+ setShowSuggestions(false);
559
+ setSuggestions([]);
560
+ }, [input, currentAtPath]);
561
+
562
+ // Stop the agent
563
+ const handleStop = useCallback(() => {
564
+ if (agent && state.isProcessing) {
565
+ agent.stop();
566
+ setState((s) => ({
567
+ ...s,
568
+ isProcessing: false,
569
+ currentToolCall: null,
570
+ }));
571
+ }
572
+ }, [agent, state.isProcessing]);
573
+
574
+ // Handle keyboard shortcuts
575
+ useInput((char, key) => {
576
+ if (key.ctrl && char === "c") {
577
+ exit();
578
+ return;
579
+ }
580
+
581
+ // Escape to stop agent when processing
582
+ if (key.escape && state.isProcessing) {
583
+ handleStop();
584
+ return;
585
+ }
586
+
587
+ // Handle suggestion navigation when suggestions are shown
588
+ if (showSuggestions && suggestions.length > 0) {
589
+ if (key.downArrow) {
590
+ setSelectedIndex((i) => Math.min(i + 1, suggestions.length - 1));
591
+ return;
592
+ }
593
+ if (key.upArrow) {
594
+ setSelectedIndex((i) => Math.max(i - 1, 0));
595
+ return;
596
+ }
597
+ if (key.tab) {
598
+ insertSuggestion(suggestions[selectedIndex]);
599
+ return;
600
+ }
601
+ if (key.escape) {
602
+ setShowSuggestions(false);
603
+ return;
604
+ }
605
+ }
606
+ });
607
+
608
+ if (!agent) {
609
+ return (
610
+ <Box>
611
+ <Text color="yellow">
612
+ <Spinner type="dots" />
613
+ </Text>
614
+ <Text> Initializing agent...</Text>
615
+ </Box>
616
+ );
617
+ }
618
+
619
+ return (
620
+ <Box flexDirection="column" paddingX={1}>
621
+ {/* Header */}
622
+ <Box marginBottom={1}>
623
+ <Text color="cyan" bold>
624
+ Pioneer Agent
625
+ </Text>
626
+ <Text dimColor> — /help for commands, @ for files, Ctrl+C to exit</Text>
627
+ </Box>
628
+
629
+ {/* Messages */}
630
+ <Box flexDirection="column" flexGrow={1}>
631
+ {state.messages.map((msg, i) => (
632
+ <MessageBubble key={i} message={msg} />
633
+ ))}
634
+
635
+ {/* Streaming content */}
636
+ {state.isProcessing && state.streamingContent && (
637
+ <StreamingMessage content={state.streamingContent} />
638
+ )}
639
+
640
+ {/* File references indicator */}
641
+ {state.fileReferences.length > 0 && (
642
+ <FileReferencesIndicator refs={state.fileReferences} />
643
+ )}
644
+
645
+ {/* Tool call indicator */}
646
+ {state.currentToolCall && (
647
+ <ToolCallIndicator toolName={state.currentToolCall} />
648
+ )}
649
+
650
+ {/* Error display */}
651
+ {state.error && (
652
+ <Box marginBottom={1}>
653
+ <Text color="red">Error: {state.error}</Text>
654
+ </Box>
655
+ )}
656
+ </Box>
657
+
658
+ {/* File suggestions dropdown */}
659
+ {showSuggestions && !state.isProcessing && (
660
+ <FileSuggestions
661
+ suggestions={suggestions}
662
+ selectedIndex={selectedIndex}
663
+ searchPath={currentAtPath?.path || ""}
664
+ />
665
+ )}
666
+
667
+ {/* Input area */}
668
+ <Box marginTop={1}>
669
+ {state.isProcessing ? (
670
+ <Box>
671
+ <Text color="yellow">
672
+ <Spinner type="dots" />
673
+ </Text>
674
+ <Text> Processing... </Text>
675
+ <Text dimColor>(Esc to stop)</Text>
676
+ </Box>
677
+ ) : (
678
+ <Box>
679
+ <Text color="cyan" bold>
680
+ {">"}{" "}
681
+ </Text>
682
+ {isRawModeSupported ? (
683
+ <TextInput
684
+ value={input}
685
+ onChange={setInput}
686
+ onSubmit={handleSubmit}
687
+ placeholder="Type a message... (@ for files)"
688
+ />
689
+ ) : (
690
+ <Text dimColor>Raw mode not supported in this terminal</Text>
691
+ )}
692
+ </Box>
693
+ )}
694
+ </Box>
695
+
696
+ {/* Status bar */}
697
+ <StatusBar budgetSummary={budgetSummary} isProcessing={state.isProcessing} />
698
+ </Box>
699
+ );
700
+ };
701
+
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Chat module exports
3
+ */
4
+
5
+ export { ChatApp } from "./ChatApp.js";
6
+ export type { ChatAppProps } from "./ChatApp.js";
7
+