@calliopelabs/cli 0.4.7 → 0.6.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.
Files changed (111) hide show
  1. package/README.md +258 -105
  2. package/dist/branching.d.ts +56 -0
  3. package/dist/branching.d.ts.map +1 -0
  4. package/dist/branching.js +211 -0
  5. package/dist/branching.js.map +1 -0
  6. package/dist/cli.d.ts.map +1 -1
  7. package/dist/cli.js +235 -2
  8. package/dist/cli.js.map +1 -1
  9. package/dist/completion.d.ts +75 -0
  10. package/dist/completion.d.ts.map +1 -0
  11. package/dist/completion.js +234 -0
  12. package/dist/completion.js.map +1 -0
  13. package/dist/config.d.ts +36 -0
  14. package/dist/config.d.ts.map +1 -1
  15. package/dist/config.js +90 -0
  16. package/dist/config.js.map +1 -1
  17. package/dist/diff.d.ts +73 -0
  18. package/dist/diff.d.ts.map +1 -0
  19. package/dist/diff.js +317 -0
  20. package/dist/diff.js.map +1 -0
  21. package/dist/errors.d.ts +41 -0
  22. package/dist/errors.d.ts.map +1 -0
  23. package/dist/errors.js +199 -0
  24. package/dist/errors.js.map +1 -0
  25. package/dist/file-watcher.d.ts +91 -0
  26. package/dist/file-watcher.d.ts.map +1 -0
  27. package/dist/file-watcher.js +269 -0
  28. package/dist/file-watcher.js.map +1 -0
  29. package/dist/files.d.ts +49 -0
  30. package/dist/files.d.ts.map +1 -0
  31. package/dist/files.js +191 -0
  32. package/dist/files.js.map +1 -0
  33. package/dist/fuzzy-search.d.ts +75 -0
  34. package/dist/fuzzy-search.d.ts.map +1 -0
  35. package/dist/fuzzy-search.js +240 -0
  36. package/dist/fuzzy-search.js.map +1 -0
  37. package/dist/hooks.d.ts +79 -0
  38. package/dist/hooks.d.ts.map +1 -0
  39. package/dist/hooks.js +271 -0
  40. package/dist/hooks.js.map +1 -0
  41. package/dist/keyboard.d.ts +57 -0
  42. package/dist/keyboard.d.ts.map +1 -0
  43. package/dist/keyboard.js +265 -0
  44. package/dist/keyboard.js.map +1 -0
  45. package/dist/markdown.d.ts +14 -0
  46. package/dist/markdown.d.ts.map +1 -0
  47. package/dist/markdown.js +248 -0
  48. package/dist/markdown.js.map +1 -0
  49. package/dist/mcp.d.ts +90 -0
  50. package/dist/mcp.d.ts.map +1 -0
  51. package/dist/mcp.js +290 -0
  52. package/dist/mcp.js.map +1 -0
  53. package/dist/memory.d.ts +104 -0
  54. package/dist/memory.d.ts.map +1 -0
  55. package/dist/memory.js +394 -0
  56. package/dist/memory.js.map +1 -0
  57. package/dist/model-router.d.ts +67 -0
  58. package/dist/model-router.d.ts.map +1 -0
  59. package/dist/model-router.js +289 -0
  60. package/dist/model-router.js.map +1 -0
  61. package/dist/parallel-tools.d.ts +51 -0
  62. package/dist/parallel-tools.d.ts.map +1 -0
  63. package/dist/parallel-tools.js +278 -0
  64. package/dist/parallel-tools.js.map +1 -0
  65. package/dist/project-config.d.ts +84 -0
  66. package/dist/project-config.d.ts.map +1 -0
  67. package/dist/project-config.js +250 -0
  68. package/dist/project-config.js.map +1 -0
  69. package/dist/providers.d.ts +10 -2
  70. package/dist/providers.d.ts.map +1 -1
  71. package/dist/providers.js +240 -38
  72. package/dist/providers.js.map +1 -1
  73. package/dist/risk.d.ts +31 -0
  74. package/dist/risk.d.ts.map +1 -0
  75. package/dist/risk.js +367 -0
  76. package/dist/risk.js.map +1 -0
  77. package/dist/sandbox.d.ts +49 -0
  78. package/dist/sandbox.d.ts.map +1 -0
  79. package/dist/sandbox.js +347 -0
  80. package/dist/sandbox.js.map +1 -0
  81. package/dist/skills.d.ts +71 -0
  82. package/dist/skills.d.ts.map +1 -0
  83. package/dist/skills.js +383 -0
  84. package/dist/skills.js.map +1 -0
  85. package/dist/storage.d.ts +139 -0
  86. package/dist/storage.d.ts.map +1 -0
  87. package/dist/storage.js +508 -0
  88. package/dist/storage.js.map +1 -0
  89. package/dist/streaming.d.ts +94 -0
  90. package/dist/streaming.d.ts.map +1 -0
  91. package/dist/streaming.js +305 -0
  92. package/dist/streaming.js.map +1 -0
  93. package/dist/summarization.d.ts +76 -0
  94. package/dist/summarization.d.ts.map +1 -0
  95. package/dist/summarization.js +242 -0
  96. package/dist/summarization.js.map +1 -0
  97. package/dist/themes.d.ts +110 -0
  98. package/dist/themes.d.ts.map +1 -0
  99. package/dist/themes.js +329 -0
  100. package/dist/themes.js.map +1 -0
  101. package/dist/tools.d.ts.map +1 -1
  102. package/dist/tools.js +335 -1
  103. package/dist/tools.js.map +1 -1
  104. package/dist/types.d.ts +56 -1
  105. package/dist/types.d.ts.map +1 -1
  106. package/dist/types.js +105 -0
  107. package/dist/types.js.map +1 -1
  108. package/dist/ui-cli.d.ts.map +1 -1
  109. package/dist/ui-cli.js +1144 -22
  110. package/dist/ui-cli.js.map +1 -1
  111. package/package.json +1 -1
package/dist/ui-cli.js CHANGED
@@ -14,12 +14,25 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
14
14
  import { useState, useCallback, useRef, useEffect } from 'react';
15
15
  import { render, Box, Text, useInput, useApp, useStdout, Static } from 'ink';
16
16
  import TextInput from 'ink-text-input';
17
+ import * as fs from 'fs';
17
18
  import * as config from './config.js';
18
19
  import { chat, getAvailableProviders, selectProvider } from './providers.js';
19
20
  import { TOOLS, executeTool } from './tools.js';
20
- import { getSystemPrompt, DEFAULT_MODELS } from './types.js';
21
+ import { getSystemPrompt, DEFAULT_MODELS, MODE_CONFIG, RISK_CONFIG, supportsVision, calculateCost } from './types.js';
21
22
  import { getVersion, getLatestVersion, performUpgrade } from './version-check.js';
22
23
  import { getAvailableModels } from './model-detection.js';
24
+ import { assessToolRisk } from './risk.js';
25
+ import { formatError } from './errors.js';
26
+ import * as storage from './storage.js';
27
+ import { parseFileReferences, processFilesForMessage, formatFileInfo } from './files.js';
28
+ import { renderMarkdown } from './markdown.js';
29
+ import * as mcp from './mcp.js';
30
+ import * as skills from './skills.js';
31
+ import * as memory from './memory.js';
32
+ import * as hooks from './hooks.js';
33
+ import * as modelRouter from './model-router.js';
34
+ import * as summarization from './summarization.js';
35
+ import { requiresConfirmation } from './risk.js';
23
36
  // ============================================================================
24
37
  // Constants
25
38
  // ============================================================================
@@ -38,6 +51,10 @@ const TOOL_ICONS = {
38
51
  write_file: 'āœļø',
39
52
  list_files: 'šŸ“',
40
53
  think: 'šŸ’­',
54
+ execute_code: 'ā–¶ļø',
55
+ web_search: 'šŸ”',
56
+ git: 'šŸ”€',
57
+ mermaid: 'šŸ“Š',
41
58
  };
42
59
  // ============================================================================
43
60
  // Utility Components
@@ -47,6 +64,17 @@ function Separator() {
47
64
  const width = stdout?.columns || 80;
48
65
  return _jsx(Text, { dimColor: true, children: '─'.repeat(width) });
49
66
  }
67
+ function ThinkingDisplay({ state }) {
68
+ const [frame, setFrame] = useState(0);
69
+ useEffect(() => {
70
+ const timer = setInterval(() => {
71
+ setFrame(f => (f + 1) % SPINNER_FRAMES.length);
72
+ }, 80);
73
+ return () => clearInterval(timer);
74
+ }, []);
75
+ return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: SPINNER_FRAMES[frame] }), _jsxs(Text, { children: [" ", state.status] }), state.iteration && state.maxIterations && (_jsxs(Text, { dimColor: true, children: [" (", state.iteration, "/", state.maxIterations, ")"] }))] }), state.detail && (_jsx(Box, { marginLeft: 2, children: _jsxs(Text, { dimColor: true, children: ["\u21B3 ", state.detail] }) })), state.thinking && (_jsxs(Box, { flexDirection: "column", marginLeft: 2, marginTop: 1, children: [_jsx(Text, { color: "magenta", children: "\uD83D\uDCAD Thinking:" }), state.thinking.split('\n').slice(0, 5).map((line, i) => (_jsxs(Text, { dimColor: true, children: [" ", line.substring(0, 80)] }, i))), state.thinking.split('\n').length > 5 && (_jsx(Text, { dimColor: true, children: " ..." }))] }))] }));
76
+ }
77
+ // Legacy simple indicator for non-agent operations
50
78
  function ProcessingIndicator({ label }) {
51
79
  const [frame, setFrame] = useState(0);
52
80
  useEffect(() => {
@@ -65,7 +93,9 @@ function MessageItem({ msg }) {
65
93
  case 'user':
66
94
  return (_jsx(Box, { flexDirection: "column", marginTop: 1, children: _jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "\u203A" }), " ", msg.content] }) }));
67
95
  case 'assistant': {
68
- const lines = msg.content.split('\n');
96
+ // Render markdown with syntax highlighting
97
+ const rendered = renderMarkdown(msg.content);
98
+ const lines = rendered.split('\n');
69
99
  return (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, children: [_jsx(Text, { color: "cyan", children: "\u2727 Calliope:" }), _jsx(Text, { children: " " }), lines.map((line, i) => (_jsxs(Text, { children: [_jsx(Text, { color: "blue", children: "\u2502" }), " ", line] }, i)))] }));
70
100
  }
71
101
  case 'tool': {
@@ -78,7 +108,29 @@ function MessageItem({ msg }) {
78
108
  return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u256D\u2500" }), " ", icon, " ", _jsx(Text, { color: "yellow", children: toolName })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u2502" }), " ", _jsx(Text, { dimColor: true, children: preview })] })] }));
79
109
  }
80
110
  }
81
- // Tool result
111
+ // Check for diff output from write_file
112
+ const isDiff = msg.content.startsWith('DIFF:');
113
+ if (isDiff) {
114
+ const lines = msg.content.split('\n');
115
+ const header = lines[0];
116
+ const isNewFile = header.includes('NEW_FILE:');
117
+ const filePath = isNewFile
118
+ ? header.replace('DIFF:NEW_FILE:', '')
119
+ : header.replace('DIFF:', '');
120
+ const diffLines = lines.slice(1, 12);
121
+ const hasMore = lines.length > 12;
122
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u251C\u2500\u2500" }), _jsxs(Text, { color: "yellow", children: [" ", isNewFile ? '(new file)' : '(modified)'] })] }), diffLines.map((line, i) => {
123
+ let color;
124
+ if (line.startsWith('+ '))
125
+ color = 'green';
126
+ else if (line.startsWith('- '))
127
+ color = 'red';
128
+ else if (line.startsWith('@@'))
129
+ color = 'cyan';
130
+ return (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u2502" }), _jsxs(Text, { color: color, children: [" ", line.substring(0, 80)] })] }, i));
131
+ }), hasMore && _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u2502" }), " ", _jsx(Text, { dimColor: true, children: "..." })] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "\u2570\u2500" }), " ", _jsx(Text, { color: "green", children: "\u2713" }), " ", _jsx(Text, { dimColor: true, children: filePath })] })] }));
132
+ }
133
+ // Regular tool result
82
134
  const lines = msg.content.split('\n').slice(0, 5);
83
135
  const hasMore = msg.content.split('\n').length > 5;
84
136
  const hasError = msg.content.toLowerCase().includes('error');
@@ -130,19 +182,62 @@ function UpgradePrompt({ currentVersion, latestVersion, onConfirm, onCancel }) {
130
182
  });
131
183
  return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsxs(Text, { color: "yellow", children: ["Update available: v", currentVersion, " \u2192 ", _jsxs(Text, { color: "green", children: ["v", latestVersion] })] }), _jsxs(Text, { children: ["Upgrade now? ", _jsx(Text, { color: "cyan", children: "(y/N)" })] })] }));
132
184
  }
185
+ function ToolConfirmation({ toolCall, riskLevel, reason, onConfirm, onDeny }) {
186
+ useInput((input, key) => {
187
+ if (input === 'y' || input === 'Y')
188
+ onConfirm();
189
+ else if (input === 'n' || input === 'N' || key.escape)
190
+ onDeny();
191
+ });
192
+ const args = toolCall.arguments;
193
+ const preview = String(args.command || args.path || args.operation || '...');
194
+ const riskColor = riskLevel === 'critical' ? 'red' : 'yellow';
195
+ const riskIcon = riskLevel === 'critical' ? 'āš ļø' : '⚔';
196
+ return (_jsxs(Box, { flexDirection: "column", marginY: 1, borderStyle: "round", borderColor: riskColor, paddingX: 1, children: [_jsxs(Text, { color: riskColor, bold: true, children: [riskIcon, " ", riskLevel.toUpperCase(), " RISK OPERATION"] }), _jsx(Text, { children: " " }), _jsxs(Text, { children: ["Tool: ", _jsx(Text, { color: "cyan", children: toolCall.name })] }), _jsxs(Text, { children: ["Command: ", _jsx(Text, { dimColor: true, children: preview.substring(0, 60) })] }), _jsxs(Text, { children: ["Reason: ", _jsx(Text, { dimColor: true, children: reason })] }), _jsx(Text, { children: " " }), _jsxs(Text, { children: ["Execute this operation? ", _jsx(Text, { color: "cyan", children: "(y/N)" })] })] }));
197
+ }
133
198
  // ============================================================================
134
199
  // Input Components
135
200
  // ============================================================================
136
- function ChatInput({ value, onChange, onSubmit, disabled }) {
201
+ function ChatInput({ value, onChange, onSubmit, disabled, mode }) {
137
202
  if (disabled)
138
203
  return null;
139
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Separator, {}), _jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: "calliope" }), _jsx(Text, { dimColor: true, children: "> " }), _jsx(TextInput, { value: value, onChange: onChange, onSubmit: onSubmit })] })] }));
204
+ const modeConfig = MODE_CONFIG[mode];
205
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Separator, {}), _jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: "calliope " }), _jsx(Text, { children: modeConfig.icon }), _jsx(Text, { dimColor: true, children: "> " }), _jsx(TextInput, { value: value, onChange: onChange, onSubmit: onSubmit })] })] }));
206
+ }
207
+ // Context window limits by model (approximate)
208
+ const CONTEXT_LIMITS = {
209
+ 'claude-sonnet-4': 200000,
210
+ 'claude-opus-4': 200000,
211
+ 'claude-3': 200000,
212
+ 'gpt-4o': 128000,
213
+ 'gpt-4-turbo': 128000,
214
+ 'gpt-4': 8192,
215
+ 'gemini-2': 1000000,
216
+ 'gemini-1.5-pro': 1000000,
217
+ 'gemini-1.5-flash': 1000000,
218
+ 'llama-3.3': 128000,
219
+ 'llama-3.1': 128000,
220
+ 'mistral-large': 128000,
221
+ 'default': 32000,
222
+ };
223
+ function getContextLimit(model) {
224
+ for (const [key, limit] of Object.entries(CONTEXT_LIMITS)) {
225
+ if (model.toLowerCase().includes(key.toLowerCase())) {
226
+ return limit;
227
+ }
228
+ }
229
+ return CONTEXT_LIMITS.default;
140
230
  }
141
- function StatusBar({ provider, model, stats }) {
231
+ function StatusBar({ provider, model, stats, mode, contextTokens, }) {
142
232
  const formatTokens = (n) => n >= 1000 ? `${(n / 1000).toFixed(1)}K` : String(n);
143
233
  const formatCost = (c) => c < 0.01 ? '<$0.01' : `$${c.toFixed(2)}`;
144
234
  const displayModel = model.length > 25 ? model.slice(0, 22) + '...' : model;
145
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Separator, {}), _jsxs(Text, { dimColor: true, children: [provider, ":", displayModel, ' │ ', formatTokens(stats.inputTokens + stats.outputTokens), " tokens", ' │ ', formatCost(stats.cost)] })] }));
235
+ const modeConfig = MODE_CONFIG[mode];
236
+ // Context usage indicator
237
+ const contextLimit = getContextLimit(model);
238
+ const contextPct = Math.min(100, Math.round((contextTokens / contextLimit) * 100));
239
+ const contextColor = contextPct > 80 ? 'red' : contextPct > 50 ? 'yellow' : 'green';
240
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Separator, {}), _jsxs(Text, { dimColor: true, children: [modeConfig.icon, " ", modeConfig.label, ' │ ', provider, ":", displayModel, ' │ ', _jsxs(Text, { color: contextColor, children: [formatTokens(contextTokens), "/", formatTokens(contextLimit)] }), ' │ ', formatTokens(stats.inputTokens + stats.outputTokens), " used", ' │ ', formatCost(stats.cost), ' │ ', _jsx(Text, { dimColor: true, children: "Esc: exit" })] })] }));
146
241
  }
147
242
  // ============================================================================
148
243
  // Main Chat Component
@@ -155,12 +250,17 @@ function TerminalChat() {
155
250
  const [input, setInput] = useState('');
156
251
  const [messages, setMessages] = useState([]);
157
252
  const [isProcessing, setIsProcessing] = useState(false);
253
+ const [thinkingState, setThinkingState] = useState(null);
254
+ const [streamingResponse, setStreamingResponse] = useState('');
158
255
  // Config state
159
256
  const [provider, setProvider] = useState(config.get('defaultProvider'));
160
257
  const [model, setModel] = useState(config.get('defaultModel'));
161
258
  const [persona, setPersona] = useState(config.get('persona'));
259
+ const [mode, setMode] = useState('hybrid'); // Default to hybrid mode
260
+ const [confirmMode, setConfirmMode] = useState(true); // Require confirmation for risky ops
162
261
  // Modal state
163
262
  const [modalMode, setModalMode] = useState('none');
263
+ const [pendingToolCall, setPendingToolCall] = useState(null);
164
264
  const [availableModels, setAvailableModels] = useState([]);
165
265
  const [latestVersion, setLatestVersion] = useState(null);
166
266
  // Stats
@@ -170,10 +270,61 @@ function TerminalChat() {
170
270
  cost: 0,
171
271
  messageCount: 0,
172
272
  });
273
+ const [contextTokens, setContextTokens] = useState(0);
173
274
  // LLM conversation history
174
275
  const llmMessages = useRef([
175
276
  { role: 'system', content: getSystemPrompt(persona) }
176
277
  ]);
278
+ // Estimate context tokens (rough: ~4 chars per token)
279
+ const estimateContextTokens = useCallback(() => {
280
+ let chars = 0;
281
+ for (const msg of llmMessages.current) {
282
+ if (typeof msg.content === 'string') {
283
+ chars += msg.content.length;
284
+ }
285
+ else if (Array.isArray(msg.content)) {
286
+ for (const block of msg.content) {
287
+ if (block.type === 'text') {
288
+ chars += block.text.length;
289
+ }
290
+ else if (block.type === 'image') {
291
+ chars += 1000; // Images count as ~250 tokens
292
+ }
293
+ }
294
+ }
295
+ }
296
+ return Math.round(chars / 4);
297
+ }, []);
298
+ // Session state
299
+ const sessionRef = useRef(null);
300
+ const [autoRoute, setAutoRoute] = useState(false); // Auto model routing
301
+ const [memoryLoaded, setMemoryLoaded] = useState(false);
302
+ // Initialize session and load memory on mount
303
+ useEffect(() => {
304
+ const session = storage.getOrCreateSession(process.cwd());
305
+ sessionRef.current = session;
306
+ // Load memory context into system prompt
307
+ if (!memoryLoaded) {
308
+ const cwd = process.cwd();
309
+ const memoryContext = memory.buildMemoryContext(cwd);
310
+ if (memoryContext.trim()) {
311
+ // Append memory context to system prompt
312
+ const currentSystem = llmMessages.current[0];
313
+ if (currentSystem && currentSystem.role === 'system') {
314
+ const systemContent = typeof currentSystem.content === 'string'
315
+ ? currentSystem.content
316
+ : '';
317
+ llmMessages.current[0] = {
318
+ role: 'system',
319
+ content: systemContent + '\n\n--- Project Context ---\n' + memoryContext,
320
+ };
321
+ }
322
+ }
323
+ setMemoryLoaded(true);
324
+ // Execute session start hooks
325
+ hooks.executeHooks('session-start', {}).catch(() => { });
326
+ }
327
+ }, [memoryLoaded]);
177
328
  // Derived values
178
329
  const actualProvider = selectProvider(provider);
179
330
  const actualModel = model || DEFAULT_MODELS[actualProvider];
@@ -193,7 +344,42 @@ function TerminalChat() {
193
344
  switch (command) {
194
345
  case '/help':
195
346
  case '/h':
196
- addMessage('system', 'Commands: /help /provider /model /models /persona /clear /status /config /upgrade /exit\nLoop mode: use --legacy flag');
347
+ addMessage('system', `Commands:
348
+ /mode [plan|hybrid|work] - Switch modes (Shift+Tab to cycle)
349
+ /provider [name] - Switch AI provider
350
+ /model [name] - Switch model
351
+ /route [on|off|test] - Auto model routing by complexity
352
+ /persona [name] - Switch personality
353
+ /todo [add|done|list] - Manage TODOs
354
+ /plans [list|view] - View plan history
355
+ /session [list|info] - Session management
356
+ /history [search] - Chat history
357
+ /context [load|summary] - Context management
358
+ /summarize [context|compact] - Summarize/compact context
359
+ /clear - Clear conversation
360
+ /copy - Copy last response to clipboard
361
+ /export [file.md] - Export conversation to markdown
362
+ /edit - Edit and resend last message
363
+ /undo - Remove last exchange
364
+ /confirm [on|off] - Toggle risky op confirmation
365
+ /profile [name|save|del] - Switch/save/delete profiles
366
+ /mcp [add|remove|tools] - Manage MCP servers
367
+ /skills [add|remove] - Manage agent skills
368
+ /memory [init|add|show] - Project memory (CALLIOPE.md)
369
+ /project [init|show|run] - Project config (.calliope)
370
+ /find <pattern> - Fuzzy file search
371
+ /branch [new|switch] - Conversation branches
372
+ /theme [name|list] - Color themes
373
+ /hooks [list|add] - Pre/post tool hooks
374
+ /search <query> - Search conversation
375
+ /status - Show status
376
+ /config - Show config
377
+ /upgrade - Check for updates
378
+ /exit - Exit
379
+
380
+ File references: @filename, ./path, /absolute/path
381
+ Modes: šŸ“‹ Plan | šŸ”„ Hybrid | šŸ”§ Work
382
+ Auto-route: ${autoRoute ? 'ON' : 'OFF'}`);
197
383
  break;
198
384
  case '/provider':
199
385
  case '/p':
@@ -245,6 +431,17 @@ function TerminalChat() {
245
431
  addMessage('error', `Failed to fetch models: ${e instanceof Error ? e.message : String(e)}`);
246
432
  }
247
433
  break;
434
+ case '/mode':
435
+ if (parts[1] && ['plan', 'hybrid', 'work'].includes(parts[1])) {
436
+ const m = parts[1];
437
+ setMode(m);
438
+ addMessage('system', `Mode: ${MODE_CONFIG[m].icon} ${MODE_CONFIG[m].label} - ${MODE_CONFIG[m].description}`);
439
+ }
440
+ else {
441
+ const currentConfig = MODE_CONFIG[mode];
442
+ addMessage('system', `Mode: ${currentConfig.icon} ${currentConfig.label}\nOptions: plan (šŸ“‹), hybrid (šŸ”„), work (šŸ”§)\nUse Shift+Tab to cycle`);
443
+ }
444
+ break;
248
445
  case '/persona':
249
446
  if (parts[1] && ['calliope', 'professional', 'minimal'].includes(parts[1])) {
250
447
  const p = parts[1];
@@ -262,6 +459,113 @@ function TerminalChat() {
262
459
  llmMessages.current = [{ role: 'system', content: getSystemPrompt(persona) }];
263
460
  setStats({ inputTokens: 0, outputTokens: 0, cost: 0, messageCount: 0 });
264
461
  break;
462
+ case '/copy': {
463
+ // Copy last assistant response to clipboard
464
+ const lastAssistant = [...messages].reverse().find(m => m.type === 'assistant');
465
+ if (lastAssistant) {
466
+ try {
467
+ const { execSync } = await import('child_process');
468
+ // Try different clipboard commands based on platform
469
+ const content = lastAssistant.content;
470
+ if (process.platform === 'darwin') {
471
+ execSync('pbcopy', { input: content });
472
+ }
473
+ else if (process.platform === 'win32') {
474
+ execSync('clip', { input: content });
475
+ }
476
+ else {
477
+ // Linux - try xclip, xsel, or wl-copy
478
+ try {
479
+ execSync('xclip -selection clipboard', { input: content });
480
+ }
481
+ catch {
482
+ try {
483
+ execSync('xsel --clipboard --input', { input: content });
484
+ }
485
+ catch {
486
+ execSync('wl-copy', { input: content });
487
+ }
488
+ }
489
+ }
490
+ addMessage('system', 'āœ“ Copied to clipboard');
491
+ }
492
+ catch (e) {
493
+ addMessage('error', `Clipboard not available: ${e instanceof Error ? e.message : String(e)}`);
494
+ }
495
+ }
496
+ else {
497
+ addMessage('system', 'No assistant message to copy');
498
+ }
499
+ break;
500
+ }
501
+ case '/export': {
502
+ // Export conversation to markdown
503
+ const filename = parts[1] || `calliope-export-${Date.now()}.md`;
504
+ const fs = await import('fs');
505
+ const path = await import('path');
506
+ let markdown = `# Calliope Conversation Export\n\n`;
507
+ markdown += `**Date:** ${new Date().toLocaleString()}\n`;
508
+ markdown += `**Provider:** ${actualProvider}\n`;
509
+ markdown += `**Model:** ${actualModel}\n\n---\n\n`;
510
+ for (const msg of messages) {
511
+ if (msg.type === 'user') {
512
+ markdown += `## šŸ‘¤ User\n\n${msg.content}\n\n`;
513
+ }
514
+ else if (msg.type === 'assistant') {
515
+ markdown += `## šŸ¤– Assistant\n\n${msg.content}\n\n`;
516
+ }
517
+ else if (msg.type === 'tool') {
518
+ markdown += `> šŸ”§ Tool: ${msg.content}\n\n`;
519
+ }
520
+ else if (msg.type === 'system') {
521
+ markdown += `> ā„¹ļø ${msg.content}\n\n`;
522
+ }
523
+ else if (msg.type === 'error') {
524
+ markdown += `> āš ļø Error: ${msg.content}\n\n`;
525
+ }
526
+ }
527
+ const filepath = path.resolve(process.cwd(), filename);
528
+ fs.writeFileSync(filepath, markdown);
529
+ addMessage('system', `āœ“ Exported to ${filename}`);
530
+ break;
531
+ }
532
+ case '/edit': {
533
+ // Edit last user message
534
+ const lastUserIdx = [...messages].reverse().findIndex(m => m.type === 'user');
535
+ if (lastUserIdx >= 0) {
536
+ const lastUser = messages[messages.length - 1 - lastUserIdx];
537
+ setInput(lastUser.content);
538
+ addMessage('system', 'Edit the message above and press Enter to resend');
539
+ }
540
+ else {
541
+ addMessage('system', 'No user message to edit');
542
+ }
543
+ break;
544
+ }
545
+ case '/undo': {
546
+ // Remove last exchange (user message + assistant response)
547
+ let removed = 0;
548
+ const newMessages = [...messages];
549
+ // Remove from the end until we've removed a user message
550
+ while (newMessages.length > 0 && removed < 10) {
551
+ const last = newMessages.pop();
552
+ removed++;
553
+ if (last?.type === 'user')
554
+ break;
555
+ }
556
+ // Also remove from LLM context
557
+ while (llmMessages.current.length > 1) {
558
+ const last = llmMessages.current[llmMessages.current.length - 1];
559
+ if (last.role === 'user') {
560
+ llmMessages.current.pop();
561
+ break;
562
+ }
563
+ llmMessages.current.pop();
564
+ }
565
+ setMessages(newMessages);
566
+ addMessage('system', `āœ“ Removed last ${removed} message(s)`);
567
+ break;
568
+ }
265
569
  case '/status':
266
570
  case '/s':
267
571
  addMessage('system', `${actualProvider}:${actualModel} | ${stats.messageCount} msgs | ${stats.inputTokens + stats.outputTokens} tokens`);
@@ -273,6 +577,509 @@ function TerminalChat() {
273
577
  case '/loop':
274
578
  addMessage('system', 'This feature requires legacy CLI. Run: calliope --legacy');
275
579
  break;
580
+ case '/confirm':
581
+ if (parts[1] === 'on') {
582
+ setConfirmMode(true);
583
+ addMessage('system', 'āœ“ Confirmation mode ON - will ask before risky operations');
584
+ }
585
+ else if (parts[1] === 'off') {
586
+ setConfirmMode(false);
587
+ addMessage('system', 'āš ļø Confirmation mode OFF - risky operations will auto-execute');
588
+ }
589
+ else {
590
+ addMessage('system', `Confirm mode: ${confirmMode ? 'ON' : 'OFF'}\nUsage: /confirm [on|off]`);
591
+ }
592
+ break;
593
+ case '/profile': {
594
+ const subCmd = parts[1];
595
+ if (subCmd === 'list' || !subCmd) {
596
+ const profiles = config.listProfiles();
597
+ const active = config.getActiveProfile();
598
+ const list = profiles.map(p => {
599
+ const marker = p.name === active ? '→ ' : ' ';
600
+ const tag = p.builtin ? '(built-in)' : '(custom)';
601
+ return `${marker}${p.name}: ${p.profile.provider}/${p.profile.model || 'default'} ${tag}`;
602
+ }).join('\n');
603
+ addMessage('system', `Profiles:\n${list}\n\nUsage: /profile <name> | /profile save <name>`);
604
+ }
605
+ else if (subCmd === 'save' && parts[2]) {
606
+ const name = parts[2];
607
+ config.saveProfile(name, {
608
+ provider: provider,
609
+ model: model,
610
+ persona: persona,
611
+ confirmMode: confirmMode,
612
+ });
613
+ addMessage('system', `āœ“ Saved profile: ${name}`);
614
+ }
615
+ else if (subCmd === 'delete' && parts[2]) {
616
+ const name = parts[2];
617
+ if (config.deleteProfile(name)) {
618
+ addMessage('system', `āœ“ Deleted profile: ${name}`);
619
+ }
620
+ else {
621
+ addMessage('error', `Cannot delete profile: ${name} (built-in or not found)`);
622
+ }
623
+ }
624
+ else {
625
+ // Load profile
626
+ const profile = config.getProfile(subCmd);
627
+ if (profile) {
628
+ setProvider(profile.provider);
629
+ if (profile.model)
630
+ setModel(profile.model);
631
+ setPersona(profile.persona);
632
+ if (profile.confirmMode !== undefined)
633
+ setConfirmMode(profile.confirmMode);
634
+ config.setActiveProfile(subCmd);
635
+ addMessage('system', `āœ“ Loaded profile: ${subCmd} (${profile.provider}/${profile.model || 'default'})`);
636
+ }
637
+ else {
638
+ addMessage('error', `Profile not found: ${subCmd}\nBuilt-in: fast, smart, cheap, local`);
639
+ }
640
+ }
641
+ break;
642
+ }
643
+ case '/mcp': {
644
+ const subCmd = parts[1];
645
+ if (subCmd === 'list' || !subCmd) {
646
+ const servers = mcp.listServers();
647
+ if (servers.length === 0) {
648
+ addMessage('system', 'No MCP servers registered.\n\nUsage:\n /mcp add <url> - Register MCP server\n /mcp remove <id> - Remove server');
649
+ }
650
+ else {
651
+ const list = servers.map(s => {
652
+ const status = s.status === 'connected' ? '🟢' : s.status === 'error' ? 'šŸ”“' : '⚪';
653
+ return `${status} ${s.name} (${s.tools.length} tools)\n ${s.url}`;
654
+ }).join('\n\n');
655
+ addMessage('system', `MCP Servers:\n\n${list}`);
656
+ }
657
+ }
658
+ else if (subCmd === 'add' && parts[2]) {
659
+ const url = parts[2];
660
+ addMessage('system', `Registering MCP server: ${url}...`);
661
+ try {
662
+ const server = await mcp.registerServer(url);
663
+ addMessage('system', `āœ“ Registered: ${server.name} (${server.tools.length} tools)`);
664
+ }
665
+ catch (e) {
666
+ addMessage('error', `Failed to register: ${e instanceof Error ? e.message : String(e)}`);
667
+ }
668
+ }
669
+ else if ((subCmd === 'remove' || subCmd === 'rm') && parts[2]) {
670
+ if (mcp.unregisterServer(parts[2])) {
671
+ addMessage('system', 'āœ“ Server removed');
672
+ }
673
+ else {
674
+ addMessage('error', 'Server not found');
675
+ }
676
+ }
677
+ else if (subCmd === 'refresh') {
678
+ const servers = mcp.listServers();
679
+ let connected = 0;
680
+ for (const s of servers) {
681
+ const updated = await mcp.refreshServer(s.id);
682
+ if (updated?.status === 'connected')
683
+ connected++;
684
+ }
685
+ addMessage('system', `Refreshed ${servers.length} servers (${connected} connected)`);
686
+ }
687
+ else if (subCmd === 'tools') {
688
+ const tools = mcp.getMCPTools();
689
+ if (tools.length === 0) {
690
+ addMessage('system', 'No MCP tools available. Add servers with /mcp add <url>');
691
+ }
692
+ else {
693
+ const list = tools.map(t => `• ${t.name}\n ${t.description}`).join('\n\n');
694
+ addMessage('system', `MCP Tools:\n\n${list}`);
695
+ }
696
+ }
697
+ else {
698
+ addMessage('system', 'Usage: /mcp [list|add <url>|remove <id>|refresh|tools]');
699
+ }
700
+ break;
701
+ }
702
+ case '/skills': {
703
+ const subCmd = parts[1];
704
+ if (subCmd === 'list' || !subCmd) {
705
+ const allSkills = skills.getSkills();
706
+ if (allSkills.length === 0) {
707
+ addMessage('system', 'No skills installed.\n\nUsage:\n /skills add <name> - Install from agentskills.io\n /skills add <github-url> - Install from GitHub\n /skills add <path> - Install from local directory');
708
+ }
709
+ else {
710
+ const list = allSkills.map(s => {
711
+ const src = s.source === 'github' ? '(GitHub)' : s.source === 'registry' ? '(agentskills.io)' : '(local)';
712
+ return `• ${s.metadata.name} ${src}\n ${s.metadata.description.substring(0, 80)}...`;
713
+ }).join('\n\n');
714
+ addMessage('system', `Installed Skills:\n\n${list}`);
715
+ }
716
+ }
717
+ else if (subCmd === 'add' && parts[2]) {
718
+ const source = parts[2];
719
+ addMessage('system', `Installing skill: ${source}...`);
720
+ try {
721
+ let skill;
722
+ if (source.startsWith('http')) {
723
+ skill = await skills.installFromGithub(source);
724
+ }
725
+ else if (fs.existsSync(source)) {
726
+ skill = skills.installLocalSkill(source);
727
+ }
728
+ else {
729
+ skill = await skills.installFromRegistry(source);
730
+ }
731
+ if (skill) {
732
+ addMessage('system', `āœ“ Installed: ${skill.metadata.name}`);
733
+ }
734
+ else {
735
+ addMessage('error', 'Failed to install skill');
736
+ }
737
+ }
738
+ catch (e) {
739
+ addMessage('error', `Failed: ${e instanceof Error ? e.message : String(e)}`);
740
+ }
741
+ }
742
+ else if ((subCmd === 'remove' || subCmd === 'rm') && parts[2]) {
743
+ if (skills.uninstallSkill(parts[2])) {
744
+ addMessage('system', 'āœ“ Skill removed');
745
+ }
746
+ else {
747
+ addMessage('error', 'Skill not found');
748
+ }
749
+ }
750
+ else if (subCmd === 'info' && parts[2]) {
751
+ const skill = skills.getSkill(parts[2]);
752
+ if (skill) {
753
+ let info = `# ${skill.metadata.name}\n\n`;
754
+ info += `${skill.metadata.description}\n\n`;
755
+ if (skill.metadata.compatibility)
756
+ info += `Compatibility: ${skill.metadata.compatibility}\n`;
757
+ if (skill.metadata.license)
758
+ info += `License: ${skill.metadata.license}\n`;
759
+ if (skill.sourceUrl)
760
+ info += `Source: ${skill.sourceUrl}\n`;
761
+ addMessage('system', info);
762
+ }
763
+ else {
764
+ addMessage('error', 'Skill not found');
765
+ }
766
+ }
767
+ else {
768
+ addMessage('system', 'Usage: /skills [list|add <source>|remove <name>|info <name>]');
769
+ }
770
+ break;
771
+ }
772
+ case '/memory': {
773
+ const memory = await import('./memory.js');
774
+ const subCmd = parts[1];
775
+ const cwd = process.cwd();
776
+ if (subCmd === 'init') {
777
+ const memPath = memory.initProjectMemory(cwd);
778
+ addMessage('system', `Created: ${memPath}\nEdit the file to add context and preferences.`);
779
+ }
780
+ else if (subCmd === 'show' || !subCmd) {
781
+ const memPath = memory.findProjectMemory(cwd);
782
+ if (!memPath) {
783
+ addMessage('system', 'No CALLIOPE.md found.\nRun /memory init to create one.');
784
+ }
785
+ else {
786
+ const mem = memory.loadMemory(memPath);
787
+ let info = `Memory: ${memPath}\n\n`;
788
+ if (mem.context.length)
789
+ info += `**Context:**\n${mem.context.map(c => ` - ${c}`).join('\n')}\n\n`;
790
+ if (mem.preferences.length)
791
+ info += `**Preferences:**\n${mem.preferences.map(p => ` - ${p}`).join('\n')}\n\n`;
792
+ if (mem.history.length)
793
+ info += `**History:**\n${mem.history.slice(-5).map(h => ` - ${h}`).join('\n')}\n`;
794
+ addMessage('system', info);
795
+ }
796
+ }
797
+ else if (subCmd === 'add' && parts[2]) {
798
+ const type = parts[2];
799
+ const content = parts.slice(3).join(' ');
800
+ if (!content) {
801
+ addMessage('error', 'Usage: /memory add <type> <content>');
802
+ }
803
+ else {
804
+ let memPath = memory.findProjectMemory(cwd);
805
+ if (!memPath) {
806
+ memPath = memory.initProjectMemory(cwd);
807
+ }
808
+ memory.addMemoryEntry(memPath, {
809
+ type,
810
+ content,
811
+ timestamp: new Date().toISOString().split('T')[0],
812
+ });
813
+ addMessage('system', `Added ${type}: ${content}`);
814
+ }
815
+ }
816
+ else if (subCmd === 'remove' && parts[2]) {
817
+ const type = parts[2];
818
+ const content = parts.slice(3).join(' ');
819
+ const memPath = memory.findProjectMemory(cwd);
820
+ if (memPath && memory.removeMemoryEntry(memPath, type, content)) {
821
+ addMessage('system', `Removed matching ${type}`);
822
+ }
823
+ else {
824
+ addMessage('error', 'Entry not found');
825
+ }
826
+ }
827
+ else if (subCmd === 'global') {
828
+ const globalMem = memory.getGlobalMemory();
829
+ let info = 'Global Memory:\n\n';
830
+ if (globalMem.preferences.length)
831
+ info += `**Preferences:**\n${globalMem.preferences.map(p => ` - ${p}`).join('\n')}\n`;
832
+ if (globalMem.notes.length)
833
+ info += `**Notes:**\n${globalMem.notes.map(n => ` - ${n}`).join('\n')}\n`;
834
+ addMessage('system', info || 'No global memories yet.');
835
+ }
836
+ else {
837
+ addMessage('system', 'Usage: /memory [init|show|add <type> <text>|remove <type> <text>|global]');
838
+ }
839
+ break;
840
+ }
841
+ case '/find': {
842
+ const fuzzy = await import('./fuzzy-search.js');
843
+ const query = parts.slice(1).join(' ');
844
+ if (!query) {
845
+ addMessage('system', 'Usage: /find <pattern>\nFuzzy search for files');
846
+ }
847
+ else {
848
+ const results = fuzzy.searchWithHighlight(process.cwd(), query, { maxResults: 20 });
849
+ if (results.length === 0) {
850
+ addMessage('system', 'No files found');
851
+ }
852
+ else {
853
+ const list = results.map((r, i) => `${i + 1}. ${r.highlighted}`).join('\n');
854
+ addMessage('system', `Found ${results.length} files:\n\n${list}`);
855
+ }
856
+ }
857
+ break;
858
+ }
859
+ case '/branch': {
860
+ const branching = await import('./branching.js');
861
+ const subCmd = parts[1];
862
+ const sessionId = `session_${Date.now()}`; // Would use actual session ID
863
+ if (subCmd === 'list' || !subCmd) {
864
+ const tree = branching.getBranchTree(sessionId);
865
+ addMessage('system', `Branches:\n${tree}`);
866
+ }
867
+ else if (subCmd === 'new' && parts[2]) {
868
+ const branch = branching.createBranch(sessionId, parts[2], llmMessages.current, parts.slice(3).join(' '));
869
+ addMessage('system', `Created branch: ${branch.name}`);
870
+ }
871
+ else if (subCmd === 'switch' && parts[2]) {
872
+ const msgs = branching.switchBranch(sessionId, parts[2], llmMessages.current);
873
+ if (msgs) {
874
+ llmMessages.current = msgs;
875
+ addMessage('system', `Switched to branch: ${parts[2]}`);
876
+ }
877
+ else {
878
+ addMessage('error', 'Branch not found');
879
+ }
880
+ }
881
+ else if (subCmd === 'delete' && parts[2]) {
882
+ if (branching.deleteBranch(sessionId, parts[2])) {
883
+ addMessage('system', 'Branch deleted');
884
+ }
885
+ else {
886
+ addMessage('error', 'Cannot delete branch');
887
+ }
888
+ }
889
+ else {
890
+ addMessage('system', 'Usage: /branch [list|new <name>|switch <name>|delete <name>]');
891
+ }
892
+ break;
893
+ }
894
+ case '/theme': {
895
+ const themes = await import('./themes.js');
896
+ const subCmd = parts[1];
897
+ if (subCmd === 'list' || !subCmd) {
898
+ const list = themes.listThemes();
899
+ const current = themes.getCurrentThemeName();
900
+ const formatted = list.map(t => {
901
+ const marker = t.name === current ? ' *' : '';
902
+ const custom = t.custom ? ' (custom)' : '';
903
+ return ` ${t.name}${marker}${custom} - ${t.description || 'No description'}`;
904
+ }).join('\n');
905
+ addMessage('system', `Available themes:\n${formatted}`);
906
+ }
907
+ else if (themes.setCurrentTheme(subCmd)) {
908
+ themes.clearThemeCache();
909
+ addMessage('system', `Theme set to: ${subCmd}`);
910
+ }
911
+ else {
912
+ addMessage('error', `Theme not found: ${subCmd}`);
913
+ }
914
+ break;
915
+ }
916
+ case '/hooks': {
917
+ const hooks = await import('./hooks.js');
918
+ const subCmd = parts[1];
919
+ if (subCmd === 'list' || !subCmd) {
920
+ addMessage('system', hooks.listHooksFormatted());
921
+ }
922
+ else if (subCmd === 'add' && parts[2]) {
923
+ const event = parts[2];
924
+ const command = parts.slice(3).join(' ');
925
+ if (!command) {
926
+ addMessage('system', 'Usage: /hooks add <event> <command>');
927
+ }
928
+ else {
929
+ hooks.addHook({ event, name: `Hook for ${event}`, command, enabled: true, async: false });
930
+ addMessage('system', 'Hook added');
931
+ }
932
+ }
933
+ else if (subCmd === 'init') {
934
+ hooks.initDefaultHooks();
935
+ addMessage('system', 'Default hooks initialized');
936
+ }
937
+ else {
938
+ addMessage('system', 'Usage: /hooks [list|add <event> <command>|init]');
939
+ }
940
+ break;
941
+ }
942
+ case '/search': {
943
+ const query = parts.slice(1).join(' ');
944
+ if (!query) {
945
+ addMessage('system', 'Usage: /search <query>\nSearch conversation history');
946
+ }
947
+ else {
948
+ const lower = query.toLowerCase();
949
+ const matches = messages.filter(m => m.content.toLowerCase().includes(lower));
950
+ if (matches.length === 0) {
951
+ addMessage('system', 'No matches found');
952
+ }
953
+ else {
954
+ const results = matches.slice(-10).map(m => {
955
+ const preview = m.content.slice(0, 100).replace(/\n/g, ' ');
956
+ return `[${m.type}] ${preview}...`;
957
+ }).join('\n\n');
958
+ addMessage('system', `Found ${matches.length} matches:\n\n${results}`);
959
+ }
960
+ }
961
+ break;
962
+ }
963
+ case '/project': {
964
+ const projectConfig = await import('./project-config.js');
965
+ const subCmd = parts[1];
966
+ const cwd = process.cwd();
967
+ if (subCmd === 'init') {
968
+ const configPath = projectConfig.createProjectConfig(cwd);
969
+ addMessage('system', `Created project config: ${configPath}\nEdit the file to customize settings.`);
970
+ }
971
+ else if (subCmd === 'show' || !subCmd) {
972
+ const configPath = projectConfig.findProjectConfig(cwd);
973
+ if (!configPath) {
974
+ addMessage('system', 'No project config found.\nRun /project init to create one.');
975
+ }
976
+ else {
977
+ const cfg = projectConfig.loadProjectConfig(configPath);
978
+ if (cfg) {
979
+ let info = `Config: ${configPath}\n\n`;
980
+ if (cfg.project)
981
+ info += `Project: ${cfg.project}\n`;
982
+ if (cfg.provider)
983
+ info += `Provider: ${cfg.provider}\n`;
984
+ if (cfg.model)
985
+ info += `Model: ${cfg.model}\n`;
986
+ if (cfg.tech?.length)
987
+ info += `Tech: ${cfg.tech.join(', ')}\n`;
988
+ if (cfg.conventions?.length)
989
+ info += `\nConventions:\n${cfg.conventions.map(c => ` - ${c}`).join('\n')}\n`;
990
+ if (cfg.commands)
991
+ info += `\nCommands: ${Object.keys(cfg.commands).join(', ')}\n`;
992
+ addMessage('system', info);
993
+ }
994
+ else {
995
+ addMessage('error', 'Failed to parse config');
996
+ }
997
+ }
998
+ }
999
+ else if (subCmd === 'run' && parts[2]) {
1000
+ const configPath = projectConfig.findProjectConfig(cwd);
1001
+ const cfg = configPath ? projectConfig.loadProjectConfig(configPath) : null;
1002
+ const cmdName = parts[2];
1003
+ if (cfg?.commands?.[cmdName]) {
1004
+ addMessage('system', `Running: ${cfg.commands[cmdName]}`);
1005
+ // Queue the command to run
1006
+ const { spawn } = await import('child_process');
1007
+ const proc = spawn('sh', ['-c', cfg.commands[cmdName]], { cwd, stdio: 'pipe' });
1008
+ let output = '';
1009
+ proc.stdout?.on('data', (d) => output += d.toString());
1010
+ proc.stderr?.on('data', (d) => output += d.toString());
1011
+ proc.on('close', (code) => {
1012
+ addMessage('system', `Exit ${code}\n${output}`);
1013
+ });
1014
+ }
1015
+ else {
1016
+ addMessage('error', `Command not found: ${cmdName}`);
1017
+ }
1018
+ }
1019
+ else {
1020
+ addMessage('system', 'Usage: /project [init|show|run <cmd>]');
1021
+ }
1022
+ break;
1023
+ }
1024
+ case '/route':
1025
+ case '/autoroute': {
1026
+ if (parts[1] === 'on') {
1027
+ setAutoRoute(true);
1028
+ addMessage('system', 'āœ“ Auto-routing ON - model selected based on task complexity');
1029
+ }
1030
+ else if (parts[1] === 'off') {
1031
+ setAutoRoute(false);
1032
+ addMessage('system', 'āœ“ Auto-routing OFF - using fixed model');
1033
+ }
1034
+ else if (parts[1] === 'test' && parts[2]) {
1035
+ const testMsg = parts.slice(2).join(' ');
1036
+ const decision = modelRouter.routeRequest(testMsg, actualProvider);
1037
+ addMessage('system', `Route test: ${decision.tier} tier (${decision.complexity})\nModel: ${decision.model.model}\nReason: ${decision.reason}\nConfidence: ${Math.round(decision.confidence * 100)}%`);
1038
+ }
1039
+ else {
1040
+ const tiers = modelRouter.getAllTiers(actualProvider);
1041
+ addMessage('system', `Auto-route: ${autoRoute ? 'ON' : 'OFF'}\n\nModel tiers for ${actualProvider}:\n fast: ${tiers.fast.model}\n balanced: ${tiers.balanced.model}\n smart: ${tiers.smart.model}\n\nUsage: /route [on|off|test <message>]`);
1042
+ }
1043
+ break;
1044
+ }
1045
+ case '/summarize': {
1046
+ const subCmd = parts[1];
1047
+ if (subCmd === 'context' || !subCmd) {
1048
+ const msgCount = llmMessages.current.length;
1049
+ if (msgCount < 5) {
1050
+ addMessage('system', 'Not enough messages to summarize.');
1051
+ }
1052
+ else {
1053
+ const summary = summarization.extractKeyInfo(llmMessages.current);
1054
+ let info = 'Context Summary:\n\n';
1055
+ if (summary.topics.length)
1056
+ info += `**Topics:** ${summary.topics.join(', ')}\n`;
1057
+ if (summary.decisions.length)
1058
+ info += `**Decisions:**\n${summary.decisions.map(d => ` - ${d}`).join('\n')}\n`;
1059
+ if (summary.actions.length)
1060
+ info += `**Actions:**\n${summary.actions.map(a => ` - ${a}`).join('\n')}\n`;
1061
+ if (summary.codeChanges.length)
1062
+ info += `**Code Changes:**\n${summary.codeChanges.slice(0, 5).map(c => ` - ${c}`).join('\n')}\n`;
1063
+ addMessage('system', info || 'No key information extracted.');
1064
+ }
1065
+ }
1066
+ else if (subCmd === 'compact') {
1067
+ // Summarize and compact the conversation
1068
+ const result = summarization.summarizeConversation(llmMessages.current, { maxTokens: 50000 });
1069
+ if (result.summarizedCount > 0) {
1070
+ llmMessages.current = result.messages;
1071
+ setContextTokens(estimateContextTokens());
1072
+ addMessage('system', `āœ“ Compacted ${result.summarizedCount} messages (${result.originalTokens} → ${result.reducedTokens} tokens)`);
1073
+ }
1074
+ else {
1075
+ addMessage('system', 'Context already within limits, no compaction needed.');
1076
+ }
1077
+ }
1078
+ else {
1079
+ addMessage('system', 'Usage: /summarize [context|compact]');
1080
+ }
1081
+ break;
1082
+ }
276
1083
  case '/upgrade':
277
1084
  addMessage('system', 'Checking for updates...');
278
1085
  try {
@@ -297,6 +1104,166 @@ function TerminalChat() {
297
1104
  addMessage('error', `Failed to check for updates: ${e instanceof Error ? e.message : String(e)}`);
298
1105
  }
299
1106
  break;
1107
+ case '/session':
1108
+ if (parts[1] === 'list') {
1109
+ const sessions = storage.listSessions(5);
1110
+ if (sessions.length === 0) {
1111
+ addMessage('system', 'No previous sessions found.');
1112
+ }
1113
+ else {
1114
+ const list = sessions.map(s => `${s.projectName} (${new Date(s.lastAccessedAt).toLocaleDateString()}) - ${s.messageCount} msgs`).join('\n');
1115
+ addMessage('system', `Recent sessions:\n${list}`);
1116
+ }
1117
+ }
1118
+ else if (parts[1] === 'info') {
1119
+ const session = sessionRef.current;
1120
+ if (session) {
1121
+ addMessage('system', `Session: ${session.projectName}\nCreated: ${new Date(session.createdAt).toLocaleString()}\nMessages: ${session.messageCount}`);
1122
+ }
1123
+ else {
1124
+ addMessage('system', 'No active session.');
1125
+ }
1126
+ }
1127
+ else {
1128
+ addMessage('system', 'Usage: /session [list|info]');
1129
+ }
1130
+ break;
1131
+ case '/todo': {
1132
+ const subCommand = parts[1];
1133
+ if (subCommand === 'add' && parts.length > 2) {
1134
+ const content = parts.slice(2).join(' ');
1135
+ const isGlobal = content.includes('--global');
1136
+ const isHigh = content.includes('--priority') && content.includes('high');
1137
+ const cleanContent = content.replace(/--global|--priority\s*\w+/g, '').trim();
1138
+ const todo = storage.addTodo(cleanContent, {
1139
+ global: isGlobal,
1140
+ priority: isHigh ? 'high' : 'normal',
1141
+ });
1142
+ addMessage('system', `āœ“ TODO added (#${todo.id.slice(-4)}${isGlobal ? ', global' : ''})`);
1143
+ }
1144
+ else if (subCommand === 'done' && parts[2]) {
1145
+ const id = parts[2];
1146
+ const todos = [...storage.getSessionTodos(), ...storage.getGlobalTodos()];
1147
+ const todo = todos.find(t => t.id.endsWith(id) || t.id === id);
1148
+ if (todo) {
1149
+ storage.updateTodo(todo.id, { status: 'completed' });
1150
+ addMessage('system', `āœ“ TODO #${id} marked done`);
1151
+ }
1152
+ else {
1153
+ addMessage('error', `TODO #${id} not found`);
1154
+ }
1155
+ }
1156
+ else if (subCommand === 'list' || !subCommand) {
1157
+ const sessionTodos = storage.getSessionTodos();
1158
+ const globalTodos = storage.getGlobalTodos();
1159
+ const pending = [...sessionTodos, ...globalTodos].filter(t => t.status !== 'completed');
1160
+ const completed = [...sessionTodos, ...globalTodos].filter(t => t.status === 'completed').slice(-3);
1161
+ if (pending.length === 0 && completed.length === 0) {
1162
+ addMessage('system', 'No TODOs. Use /todo add <task> to create one.');
1163
+ }
1164
+ else {
1165
+ let output = 'šŸ“‹ TODOs:\n';
1166
+ if (pending.length > 0) {
1167
+ output += pending.map(t => ` ${t.priority === 'high' ? '!' : 'ā–”'} #${t.id.slice(-4)} ${t.content}`).join('\n');
1168
+ }
1169
+ if (completed.length > 0) {
1170
+ output += '\n\nCompleted:\n' + completed.map(t => ` āœ“ #${t.id.slice(-4)} ${t.content}`).join('\n');
1171
+ }
1172
+ addMessage('system', output);
1173
+ }
1174
+ }
1175
+ else {
1176
+ addMessage('system', 'Usage: /todo [add <task>|done <id>|list]');
1177
+ }
1178
+ break;
1179
+ }
1180
+ case '/plans': {
1181
+ const subCommand = parts[1];
1182
+ if (subCommand === 'list' || !subCommand) {
1183
+ const plans = storage.getPlans();
1184
+ if (plans.length === 0) {
1185
+ addMessage('system', 'No plans yet. Plans are created in hybrid mode.');
1186
+ }
1187
+ else {
1188
+ const list = plans.slice(0, 5).map(p => `${p.status === 'completed' ? 'āœ“' : 'ā—‹'} ${p.id.slice(-4)}: ${p.title}`).join('\n');
1189
+ addMessage('system', `šŸ“‹ Plans:\n${list}`);
1190
+ }
1191
+ }
1192
+ else if (subCommand === 'view' && parts[2]) {
1193
+ const plans = storage.getPlans();
1194
+ const plan = plans.find(p => p.id.endsWith(parts[2]) || p.id === parts[2]);
1195
+ if (plan) {
1196
+ const phases = plan.phases.map(ph => ` ${ph.status === 'completed' ? 'āœ“' : 'ā—‹'} ${ph.name} (${ph.risk} risk)`).join('\n');
1197
+ addMessage('system', `Plan: ${plan.title}\nStatus: ${plan.status}\n\nPhases:\n${phases}`);
1198
+ }
1199
+ else {
1200
+ addMessage('error', `Plan #${parts[2]} not found`);
1201
+ }
1202
+ }
1203
+ else {
1204
+ addMessage('system', 'Usage: /plans [list|view <id>]');
1205
+ }
1206
+ break;
1207
+ }
1208
+ case '/history': {
1209
+ const subCommand = parts[1];
1210
+ if (subCommand === 'search' && parts[2]) {
1211
+ const query = parts.slice(2).join(' ');
1212
+ const results = storage.searchChatHistory(query);
1213
+ if (results.length === 0) {
1214
+ addMessage('system', `No matches for "${query}"`);
1215
+ }
1216
+ else {
1217
+ const list = results.slice(-5).map(m => `${new Date(m.timestamp).toLocaleTimeString()}: ${m.content.substring(0, 60)}...`).join('\n');
1218
+ addMessage('system', `šŸ” Found ${results.length} matches:\n${list}`);
1219
+ }
1220
+ }
1221
+ else if (subCommand === 'clear') {
1222
+ addMessage('system', 'History is preserved per session. Start a new session for fresh history.');
1223
+ }
1224
+ else {
1225
+ const history = storage.getChatHistory(5);
1226
+ if (history.length === 0) {
1227
+ addMessage('system', 'No chat history yet.');
1228
+ }
1229
+ else {
1230
+ const list = history.map(m => `${m.role}: ${m.content.substring(0, 50)}...`).join('\n');
1231
+ addMessage('system', `Recent history:\n${list}\n\nUse /history search <query> to search.`);
1232
+ }
1233
+ }
1234
+ break;
1235
+ }
1236
+ case '/context': {
1237
+ const subCommand = parts[1];
1238
+ if (subCommand === 'load') {
1239
+ const limit = parseInt(parts[2]) || 20;
1240
+ const history = storage.getChatHistory(limit);
1241
+ if (history.length > 0) {
1242
+ // Load history into LLM context
1243
+ for (const msg of history) {
1244
+ if (msg.role === 'user' || msg.role === 'assistant') {
1245
+ llmMessages.current.push({
1246
+ role: msg.role,
1247
+ content: msg.content,
1248
+ });
1249
+ }
1250
+ }
1251
+ addMessage('system', `āœ“ Loaded ${history.length} messages into context`);
1252
+ }
1253
+ else {
1254
+ addMessage('system', 'No history to load.');
1255
+ }
1256
+ }
1257
+ else if (subCommand === 'summary') {
1258
+ const msgCount = llmMessages.current.length;
1259
+ const estTokens = llmMessages.current.reduce((sum, m) => sum + (m.content?.length || 0) / 4, 0);
1260
+ addMessage('system', `Context: ${msgCount} messages (~${Math.round(estTokens)} tokens)`);
1261
+ }
1262
+ else {
1263
+ addMessage('system', 'Usage: /context [load [n]|summary]');
1264
+ }
1265
+ break;
1266
+ }
300
1267
  case '/exit':
301
1268
  case '/quit':
302
1269
  case '/q':
@@ -307,19 +1274,55 @@ function TerminalChat() {
307
1274
  }
308
1275
  }, [actualProvider, actualModel, persona, stats, addMessage, exit]);
309
1276
  // Run agent with user prompt
310
- const runAgent = useCallback(async (prompt) => {
311
- llmMessages.current.push({ role: 'user', content: prompt });
1277
+ const runAgent = useCallback(async (content) => {
1278
+ llmMessages.current.push({ role: 'user', content });
312
1279
  setStats(s => ({ ...s, messageCount: s.messageCount + 1 }));
1280
+ setStreamingResponse('');
1281
+ // Auto-route to appropriate model based on task complexity
1282
+ let effectiveModel = model;
1283
+ if (autoRoute && typeof content === 'string') {
1284
+ const routeDecision = modelRouter.routeRequest(content, provider, {
1285
+ messageCount: stats.messageCount,
1286
+ hasCode: content.includes('```') || /\.(ts|js|py|go|rs|java)/.test(content),
1287
+ });
1288
+ effectiveModel = routeDecision.model.model;
1289
+ if (effectiveModel !== model) {
1290
+ addMessage('system', `[Auto-route: ${routeDecision.tier} tier - ${routeDecision.reason}]`);
1291
+ }
1292
+ }
313
1293
  const maxIterations = config.get('maxIterations');
314
1294
  for (let i = 0; i < maxIterations; i++) {
315
1295
  try {
316
- const response = await chat(provider, llmMessages.current, TOOLS, model);
317
- // Update token stats
1296
+ // Update thinking state for LLM call
1297
+ setThinkingState({
1298
+ status: i === 0 ? 'Analyzing request...' : 'Processing response...',
1299
+ detail: `Iteration ${i + 1}/${maxIterations}`,
1300
+ iteration: i + 1,
1301
+ maxIterations,
1302
+ });
1303
+ // Streaming callback for final response
1304
+ const onToken = (token) => {
1305
+ setThinkingState(null); // Clear thinking when streaming starts
1306
+ setStreamingResponse(prev => prev + token);
1307
+ };
1308
+ // Retry callback for error recovery
1309
+ const onRetry = (attempt, error, delayMs) => {
1310
+ setThinkingState({
1311
+ status: `Retrying... (attempt ${attempt + 1})`,
1312
+ detail: `${error.message.substring(0, 40)}... Waiting ${Math.round(delayMs / 1000)}s`,
1313
+ iteration: i + 1,
1314
+ maxIterations,
1315
+ });
1316
+ };
1317
+ const response = await chat(provider, llmMessages.current, TOOLS, effectiveModel, onToken, onRetry);
1318
+ // Update token stats and cost
318
1319
  if (response.usage) {
1320
+ const usageCost = calculateCost(model || DEFAULT_MODELS[provider], response.usage.inputTokens, response.usage.outputTokens);
319
1321
  setStats(s => ({
320
1322
  ...s,
321
1323
  inputTokens: s.inputTokens + response.usage.inputTokens,
322
1324
  outputTokens: s.outputTokens + response.usage.outputTokens,
1325
+ cost: s.cost + usageCost,
323
1326
  }));
324
1327
  }
325
1328
  // Handle tool calls
@@ -330,8 +1333,76 @@ function TerminalChat() {
330
1333
  toolCalls: response.toolCalls,
331
1334
  });
332
1335
  for (const toolCall of response.toolCalls) {
333
- addMessage('tool', `⚔ ${toolCall.name}: ${toolCall.arguments.command || toolCall.arguments.path || '...'}`);
1336
+ const args = toolCall.arguments;
1337
+ const toolPreview = String(args.command || args.path || '...');
1338
+ // Assess risk
1339
+ const risk = assessToolRisk(toolCall);
1340
+ const riskConfig = RISK_CONFIG[risk.level];
1341
+ const riskDisplay = risk.level !== 'none' ? ` [${riskConfig.bar}]` : '';
1342
+ // Special handling for think tool
1343
+ if (toolCall.name === 'think') {
1344
+ const thought = String(args.thought || '');
1345
+ setThinkingState({
1346
+ status: 'Reasoning...',
1347
+ detail: thought.substring(0, 60) + (thought.length > 60 ? '...' : ''),
1348
+ thinking: thought,
1349
+ iteration: i + 1,
1350
+ maxIterations,
1351
+ });
1352
+ }
1353
+ else {
1354
+ setThinkingState({
1355
+ status: `Executing ${toolCall.name}...`,
1356
+ detail: toolPreview.substring(0, 60),
1357
+ thinking: undefined,
1358
+ iteration: i + 1,
1359
+ maxIterations,
1360
+ });
1361
+ }
1362
+ // In plan mode, don't execute tools (except think)
1363
+ if (mode === 'plan' && toolCall.name !== 'think') {
1364
+ addMessage('tool', `šŸ“‹ ${toolCall.name}: ${toolPreview}${riskDisplay} (plan mode - not executed)`);
1365
+ llmMessages.current.push({
1366
+ role: 'tool',
1367
+ content: '[Plan mode: Tool not executed. Describe what this would do.]',
1368
+ toolCallId: toolCall.id,
1369
+ });
1370
+ continue;
1371
+ }
1372
+ // Check if confirmation is required for risky operations
1373
+ if (confirmMode && requiresConfirmation(risk, false) && toolCall.name !== 'think') {
1374
+ // Show warning and skip execution
1375
+ const riskIcon = risk.level === 'critical' ? 'šŸ›‘' : 'āš ļø';
1376
+ addMessage('tool', `${riskIcon} ${toolCall.name}: ${toolPreview}${riskDisplay}\n → Requires confirmation (use /confirm off to disable)`);
1377
+ llmMessages.current.push({
1378
+ role: 'tool',
1379
+ content: `[Operation blocked - ${risk.level} risk: ${risk.reason}. User confirmation required.]`,
1380
+ toolCallId: toolCall.id,
1381
+ });
1382
+ continue;
1383
+ }
1384
+ addMessage('tool', `⚔ ${toolCall.name}: ${toolPreview}${riskDisplay}`);
1385
+ // Execute pre-tool hooks
1386
+ const preHookResult = await hooks.checkHooksAllow('pre-tool', {
1387
+ tool: toolCall.name,
1388
+ toolArgs: args,
1389
+ });
1390
+ if (!preHookResult.allowed) {
1391
+ addMessage('tool', `šŸ›‘ Blocked by hook: ${preHookResult.reason}`);
1392
+ llmMessages.current.push({
1393
+ role: 'tool',
1394
+ content: `[Blocked by hook: ${preHookResult.reason}]`,
1395
+ toolCallId: toolCall.id,
1396
+ });
1397
+ continue;
1398
+ }
334
1399
  const result = await executeTool(toolCall, process.cwd());
1400
+ // Execute post-tool hooks
1401
+ hooks.executeHooks('post-tool', {
1402
+ tool: toolCall.name,
1403
+ toolArgs: args,
1404
+ toolResult: result.result,
1405
+ }).catch(() => { });
335
1406
  const preview = result.result.split('\n').slice(0, 3).join('\n');
336
1407
  addMessage('tool', preview + (result.result.split('\n').length > 3 ? '\n...' : ''));
337
1408
  llmMessages.current.push({
@@ -342,17 +1413,30 @@ function TerminalChat() {
342
1413
  }
343
1414
  continue;
344
1415
  }
345
- // Final response
1416
+ // Final response - move streaming content to message history
1417
+ setThinkingState(null);
346
1418
  llmMessages.current.push({ role: 'assistant', content: response.content });
347
1419
  addMessage('assistant', response.content);
1420
+ setStreamingResponse('');
1421
+ setContextTokens(estimateContextTokens());
1422
+ // Auto-continue if response was truncated due to length
1423
+ if (response.finishReason === 'length') {
1424
+ addMessage('system', '(auto-continuing...)');
1425
+ llmMessages.current.push({ role: 'user', content: 'Please continue where you left off.' });
1426
+ continue; // Loop again to get continuation
1427
+ }
348
1428
  break;
349
1429
  }
350
1430
  catch (error) {
351
- addMessage('error', `Error: ${error instanceof Error ? error.message : String(error)}`);
1431
+ setThinkingState(null);
1432
+ setStreamingResponse('');
1433
+ addMessage('error', formatError(error));
352
1434
  break;
353
1435
  }
354
1436
  }
355
- }, [provider, model, addMessage]);
1437
+ // Update context tokens after agent run
1438
+ setContextTokens(estimateContextTokens());
1439
+ }, [provider, model, addMessage, mode, estimateContextTokens]);
356
1440
  // Handle input submission
357
1441
  const handleSubmit = useCallback(async (value) => {
358
1442
  const trimmed = value.trim();
@@ -363,15 +1447,40 @@ function TerminalChat() {
363
1447
  await handleCommand(trimmed);
364
1448
  return;
365
1449
  }
366
- addMessage('user', trimmed);
1450
+ // Parse file references from input
1451
+ const { text: cleanText, files } = parseFileReferences(trimmed, process.cwd());
1452
+ // Show user message (with file info if any)
1453
+ if (files.length > 0) {
1454
+ const fileInfo = formatFileInfo(files);
1455
+ addMessage('user', `${cleanText}\nšŸ“Ž ${fileInfo}`);
1456
+ }
1457
+ else {
1458
+ addMessage('user', trimmed);
1459
+ }
367
1460
  setIsProcessing(true);
368
1461
  try {
369
- await runAgent(trimmed);
1462
+ // Build message content (with file/image support)
1463
+ let messageContent;
1464
+ if (files.length > 0) {
1465
+ const visionSupported = supportsVision(provider, model);
1466
+ const { content, warnings } = processFilesForMessage(cleanText || trimmed, files, visionSupported);
1467
+ // Show any warnings about files
1468
+ for (const warning of warnings) {
1469
+ addMessage('system', warning);
1470
+ }
1471
+ messageContent = content;
1472
+ }
1473
+ else {
1474
+ messageContent = trimmed;
1475
+ }
1476
+ await runAgent(messageContent);
370
1477
  }
371
1478
  finally {
372
1479
  setIsProcessing(false);
1480
+ setThinkingState(null);
1481
+ setStreamingResponse('');
373
1482
  }
374
- }, [isProcessing, handleCommand, runAgent, addMessage]);
1483
+ }, [isProcessing, handleCommand, runAgent, addMessage, provider, model]);
375
1484
  // Modal handlers
376
1485
  const handleModelSelect = useCallback((selectedModel) => {
377
1486
  setModel(selectedModel);
@@ -408,13 +1517,26 @@ function TerminalChat() {
408
1517
  }
409
1518
  setLatestVersion(null);
410
1519
  }, [addMessage]);
411
- // Escape to exit (but not in modal)
412
- useInput((_, key) => {
1520
+ // Cycle through modes
1521
+ const cycleMode = useCallback(() => {
1522
+ setMode(current => {
1523
+ const modes = ['plan', 'hybrid', 'work'];
1524
+ const idx = modes.indexOf(current);
1525
+ const next = modes[(idx + 1) % modes.length];
1526
+ return next;
1527
+ });
1528
+ }, []);
1529
+ // Keyboard shortcuts (Escape to exit, Shift+Tab to cycle mode)
1530
+ useInput((input, key) => {
413
1531
  if (key.escape && !isModalActive)
414
1532
  exit();
1533
+ // Shift+Tab to cycle mode (key.shift && key.tab)
1534
+ if (key.shift && key.tab && !isProcessing) {
1535
+ cycleMode();
1536
+ }
415
1537
  }, { isActive: !isModalActive });
416
1538
  // Render
417
- return (_jsxs(Box, { flexDirection: "column", width: width, children: [_jsx(MessageHistory, { messages: messages }), isProcessing && _jsx(ProcessingIndicator, { label: "Thinking..." }), modalMode === 'model' && availableModels.length > 0 && (_jsx(ModelSelector, { models: availableModels, onSelect: handleModelSelect, onCancel: handleModalCancel })), modalMode === 'upgrade' && latestVersion && (_jsx(UpgradePrompt, { currentVersion: getVersion(), latestVersion: latestVersion, onConfirm: handleUpgradeConfirm, onCancel: handleModalCancel })), _jsx(ChatInput, { value: input, onChange: setInput, onSubmit: handleSubmit, disabled: isModalActive || isProcessing }), _jsx(StatusBar, { provider: actualProvider, model: actualModel, stats: stats })] }));
1539
+ return (_jsxs(Box, { flexDirection: "column", width: width, children: [_jsx(MessageHistory, { messages: messages }), streamingResponse && (_jsxs(Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, children: [_jsx(Text, { color: "cyan", children: "\u2727 Calliope:" }), _jsx(Text, { children: " " }), streamingResponse.split('\n').map((line, i) => (_jsxs(Text, { children: [_jsx(Text, { color: "blue", children: "\u2502" }), " ", line] }, i))), _jsx(Text, { color: "blue", children: "\u2502" }), _jsx(Text, { color: "cyan", children: "\u258C" })] })), isProcessing && thinkingState && !streamingResponse && _jsx(ThinkingDisplay, { state: thinkingState }), isProcessing && !thinkingState && !streamingResponse && _jsx(ProcessingIndicator, { label: "Processing..." }), modalMode === 'model' && availableModels.length > 0 && (_jsx(ModelSelector, { models: availableModels, onSelect: handleModelSelect, onCancel: handleModalCancel })), modalMode === 'upgrade' && latestVersion && (_jsx(UpgradePrompt, { currentVersion: getVersion(), latestVersion: latestVersion, onConfirm: handleUpgradeConfirm, onCancel: handleModalCancel })), _jsx(ChatInput, { value: input, onChange: setInput, onSubmit: handleSubmit, disabled: isModalActive || isProcessing, mode: mode }), _jsx(StatusBar, { provider: actualProvider, model: actualModel, mode: mode, stats: stats, contextTokens: contextTokens })] }));
418
1540
  }
419
1541
  // ============================================================================
420
1542
  // App Wrapper & Entry Point