@csmedeiros/codemax 1.0.3 → 1.0.7

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 (54) hide show
  1. package/README.md +4 -20
  2. package/launcher.js +28 -0
  3. package/package.json +15 -121
  4. package/LICENSE +0 -21
  5. package/dist/commands/cursorRules.d.ts +0 -15
  6. package/dist/commands/cursorRules.js +0 -118
  7. package/dist/commands/slashCommands.d.ts +0 -36
  8. package/dist/commands/slashCommands.js +0 -236
  9. package/dist/configuration/configManager.d.ts +0 -32
  10. package/dist/configuration/configManager.js +0 -71
  11. package/dist/configuration/modelContextWindows.d.ts +0 -3
  12. package/dist/configuration/modelContextWindows.js +0 -13
  13. package/dist/conversation/agentGraph.d.ts +0 -203
  14. package/dist/conversation/agentGraph.js +0 -433
  15. package/dist/conversation/agentTurn.d.ts +0 -40
  16. package/dist/conversation/agentTurn.js +0 -252
  17. package/dist/conversation/chatHistory.d.ts +0 -24
  18. package/dist/conversation/chatHistory.js +0 -251
  19. package/dist/conversation/compactionUtils.d.ts +0 -17
  20. package/dist/conversation/compactionUtils.js +0 -57
  21. package/dist/conversation/prompts/planPrompt.d.ts +0 -1
  22. package/dist/conversation/prompts/planPrompt.js +0 -12
  23. package/dist/conversation/prompts/systemPrompt.d.ts +0 -29
  24. package/dist/conversation/prompts/systemPrompt.js +0 -149
  25. package/dist/entry/cli.d.ts +0 -2
  26. package/dist/entry/cli.js +0 -24
  27. package/dist/observability/langfuseTracing.d.ts +0 -5
  28. package/dist/observability/langfuseTracing.js +0 -76
  29. package/dist/shared/types.d.ts +0 -25
  30. package/dist/shared/types.js +0 -1
  31. package/dist/terminal/app.d.ts +0 -2
  32. package/dist/terminal/app.js +0 -1236
  33. package/dist/terminal/components.d.ts +0 -18
  34. package/dist/terminal/components.js +0 -43
  35. package/dist/terminal/markdown.d.ts +0 -4
  36. package/dist/terminal/markdown.js +0 -47
  37. package/dist/terminal/screens/compactionSettings.d.ts +0 -6
  38. package/dist/terminal/screens/compactionSettings.js +0 -66
  39. package/dist/terminal/screens/modelSettings.d.ts +0 -10
  40. package/dist/terminal/screens/modelSettings.js +0 -76
  41. package/dist/terminal/textField.d.ts +0 -7
  42. package/dist/terminal/textField.js +0 -136
  43. package/dist/terminal/theme.d.ts +0 -23
  44. package/dist/terminal/theme.js +0 -23
  45. package/dist/tooling/mcpConfig.d.ts +0 -40
  46. package/dist/tooling/mcpConfig.js +0 -49
  47. package/dist/tooling/planControlChannel.d.ts +0 -2
  48. package/dist/tooling/planControlChannel.js +0 -21
  49. package/dist/tooling/toolConfig.d.ts +0 -42
  50. package/dist/tooling/toolConfig.js +0 -138
  51. package/dist/tooling/toolUiCallback.d.ts +0 -21
  52. package/dist/tooling/toolUiCallback.js +0 -268
  53. package/dist/tooling/tools.d.ts +0 -216
  54. package/dist/tooling/tools.js +0 -614
@@ -1,1236 +0,0 @@
1
- import React, { useEffect, useMemo, useRef, useState } from 'react';
2
- import { Box, Static, Text, useApp, useInput, useStdout } from 'ink';
3
- import { appendFileSync, readFileSync } from 'node:fs';
4
- import os from 'node:os';
5
- import pathModule from 'node:path';
6
- import chalk from 'chalk';
7
- import { streamAgentTurn, } from '../conversation/agentTurn.js';
8
- import { renderAssistantMarkdown } from './markdown.js';
9
- import { executeSlashCommand, getCommandSuggestions, getSkillSuggestions, loadProjectSkills, } from '../commands/slashCommands.js';
10
- import { createCodemaxSessionId, initLangfuseTracing, } from '../observability/langfuseTracing.js';
11
- import { isWriteOutput, parseWriteOutput } from '../tooling/toolUiCallback.js';
12
- import { updateModelConfig, triggerCompaction, } from '../conversation/agentGraph.js';
13
- import { CompactionSettingsScreen } from './screens/compactionSettings.js';
14
- import { MCP_STATUSES, reloadMcpServer } from '../tooling/tools.js';
15
- import { consumePlanNonce } from '../tooling/planControlChannel.js';
16
- import { THEME, DASHED_BORDER } from './theme.js';
17
- import { ModelSettingsScreen } from './screens/modelSettings.js';
18
- import { getConfig } from '../configuration/configManager.js';
19
- import { listChats, loadChatHistory, saveThreadName, generateThreadName, } from '../conversation/chatHistory.js';
20
- import { TodoPanel, SlashCommandModal, truncateToWidth, } from './components.js';
21
- function debugLog(message) {
22
- if (!getConfig().debugEvents)
23
- return;
24
- const ts = new Date().toISOString();
25
- const line = `[CodeMax][app][${ts}] ${message}\n`;
26
- try {
27
- const filePath = (process.env['CODEMAX_DEBUG_LOG_FILE'] ?? '').trim() ||
28
- '/tmp/codemax-debug.log';
29
- appendFileSync(filePath, line, { encoding: 'utf8' });
30
- }
31
- catch {
32
- // ignore
33
- }
34
- }
35
- const VERBS = [
36
- ['Baking', 'Baked'],
37
- ['Brewing', 'Brewed'],
38
- ['Crafting', 'Crafted'],
39
- ['Thinking', 'Thought'],
40
- ['Pondering', 'Pondered'],
41
- ['Cooking', 'Cooked'],
42
- ['Forging', 'Forged'],
43
- ['Conjuring', 'Conjured'],
44
- ['Assembling', 'Assembled'],
45
- ['Computing', 'Computed'],
46
- ['Synthesizing', 'Synthesized'],
47
- ['Distilling', 'Distilled'],
48
- ['Weaving', 'Woven'],
49
- ['Combulating', 'Combulated'],
50
- ['Processing', 'Processed'],
51
- ];
52
- function pickVerb() {
53
- return VERBS[Math.floor(Math.random() * VERBS.length)];
54
- }
55
- function nextId(prefix) {
56
- return `${prefix}_${Date.now().toString(36)}_${Math.random()
57
- .toString(36)
58
- .slice(2, 8)}`;
59
- }
60
- function normalizeNewlines(text) {
61
- return (text ?? '').replace(/\r\n/g, '\n');
62
- }
63
- function clamp(n, min, max) {
64
- return Math.max(min, Math.min(max, n));
65
- }
66
- function padRight(text, width) {
67
- if (text.length >= width)
68
- return text;
69
- return text + ' '.repeat(width - text.length);
70
- }
71
- function hr(width) {
72
- return '─'.repeat(clamp(width, 0, 2000));
73
- }
74
- function RobotAscii({ color }) {
75
- return (React.createElement(Text, { color: color }, [
76
- ' ▄▄▄ ',
77
- ' ▐▀█▀▌ ',
78
- ' ▐▄█▄▌ ',
79
- ' ███ ',
80
- ' ▄███▄ ',
81
- ' ▐█████▌ ',
82
- ].join('\n')));
83
- }
84
- function HomePanel({ width }) {
85
- // Two-column panel, collapses to one column on narrow terminals.
86
- const innerWidth = clamp(width - 4, 20, 2000);
87
- const twoCol = innerWidth >= 70;
88
- const colGap = twoCol ? 3 : 0;
89
- const leftW = twoCol ? Math.floor((innerWidth - colGap) * 0.44) : innerWidth;
90
- const rightW = twoCol ? innerWidth - colGap - leftW : innerWidth;
91
- const activity = [
92
- ['5m ago', 'Updated config.js'],
93
- ['15m ago', 'Fixed bug in api_handler.py'],
94
- ['2h ago', 'Deployed to staging'],
95
- ['1d ago', 'Created new component.vue'],
96
- ['…', '/log for more history'],
97
- ];
98
- const whatsNew = [
99
- '/scan for vulnerabilities',
100
- '/optimize-code for efficiency',
101
- '/test-suite for running tests',
102
- 'ctrl+h for help',
103
- '… /changelog for details',
104
- ];
105
- return (React.createElement(Box, { flexDirection: "column", borderStyle: DASHED_BORDER, borderColor: THEME.copper, paddingX: 2, paddingY: 1, width: clamp(width, 20, 2000) },
106
- React.createElement(Box, { flexDirection: twoCol ? 'row' : 'column', gap: twoCol ? 2 : 1 },
107
- React.createElement(Box, { flexDirection: "column", width: leftW },
108
- React.createElement(Text, { color: THEME.copper }, "CodeMax v1.0.0"),
109
- React.createElement(Text, null, " "),
110
- React.createElement(Text, { color: THEME.fg }, "Welcome back User!"),
111
- React.createElement(Text, null, " "),
112
- React.createElement(Box, { justifyContent: "center", paddingY: 1 },
113
- React.createElement(RobotAscii, { color: THEME.copper })),
114
- React.createElement(Text, null, " "),
115
- React.createElement(Text, { color: THEME.fg }, "Max-1 Engine \u2022 32k Context")),
116
- twoCol ? React.createElement(Text, { color: THEME.copper }, "\u2506") : null,
117
- React.createElement(Box, { flexDirection: "column", width: rightW },
118
- React.createElement(Text, { color: THEME.copper }, "Recent activity"),
119
- activity.map(([t, msg]) => (React.createElement(Text, { key: `${t}-${msg}`, color: THEME.fg },
120
- React.createElement(Text, { color: THEME.muted }, padRight(t, 7)),
121
- React.createElement(Text, null, " "),
122
- React.createElement(Text, null, truncateToWidth(msg, Math.max(10, rightW - 10)))))),
123
- React.createElement(Text, null, " "),
124
- React.createElement(Text, { color: THEME.copper }, "What's new"),
125
- whatsNew.map(x => (React.createElement(Text, { key: x, color: THEME.fg }, truncateToWidth(x, rightW))))))));
126
- }
127
- // Multi-char inputs that are escape sequence residuals, not paste content.
128
- // '[200~' is the bracketed-paste start marker (handled earlier in useInput).
129
- const ESCAPE_RESIDUALS = new Set([
130
- '[13;2u',
131
- 'OM',
132
- '[27;2;13~',
133
- '[1;3D',
134
- '[1;3C',
135
- '[200~',
136
- ]);
137
- function McpServerModal({ servers, selectedIndex, }) {
138
- if (!servers.length)
139
- return null;
140
- const maxNameLen = Math.max(0, ...servers.map(s => s.name.length));
141
- const statusColor = (s) => {
142
- if (s === 'connected')
143
- return 'green';
144
- if (s === 'unauthenticated')
145
- return 'yellow';
146
- return 'red';
147
- };
148
- const statusLabel = (s) => {
149
- if (s.status === 'connected')
150
- return `connected ${s.toolCount} tool${s.toolCount === 1 ? '' : 's'}`;
151
- if (s.status === 'unauthenticated')
152
- return 'unauthenticated';
153
- return 'unavailable';
154
- };
155
- return (React.createElement(Box, { flexDirection: "column", paddingX: 1 },
156
- React.createElement(Box, null,
157
- React.createElement(Text, { bold: true, color: THEME.copper }, 'MCP Servers'.padEnd(maxNameLen + 2)),
158
- React.createElement(Text, { bold: true, color: THEME.copper }, "Status")),
159
- servers.map((srv, i) => {
160
- const namePadded = srv.name.padEnd(maxNameLen + 2);
161
- const isSelected = i === selectedIndex;
162
- return (React.createElement(Box, { key: srv.name },
163
- React.createElement(Text, { inverse: isSelected, color: isSelected ? undefined : THEME.copper }, namePadded),
164
- React.createElement(Text, { inverse: isSelected, color: isSelected ? undefined : statusColor(srv.status) }, statusLabel(srv))));
165
- }),
166
- React.createElement(Box, null,
167
- React.createElement(Text, { color: THEME.muted }, " \u2193\u2191 navigate r reload Esc close"))));
168
- }
169
- function relativeTime(ms) {
170
- if (ms === 0)
171
- return '';
172
- const diff = Date.now() - ms;
173
- const mins = Math.floor(diff / 60000);
174
- if (mins < 1)
175
- return 'just now';
176
- if (mins < 60)
177
- return `${mins}m ago`;
178
- const hours = Math.floor(mins / 60);
179
- if (hours < 24)
180
- return `${hours}h ago`;
181
- return `${Math.floor(hours / 24)}d ago`;
182
- }
183
- const CHATS_WINDOW = 8;
184
- function ChatsModal({ chats, selectedIndex, }) {
185
- if (!chats.length)
186
- return (React.createElement(Box, { flexDirection: "column", paddingX: 1 },
187
- React.createElement(Box, null,
188
- React.createElement(Text, { bold: true, color: THEME.copper }, "Previous Conversations")),
189
- React.createElement(Box, null,
190
- React.createElement(Text, { color: THEME.muted }, " No previous conversations found.")),
191
- React.createElement(Box, null,
192
- React.createElement(Text, { color: THEME.muted }, " Esc close"))));
193
- const windowStart = Math.max(0, Math.min(selectedIndex - Math.floor(CHATS_WINDOW / 2), chats.length - CHATS_WINDOW));
194
- const windowEnd = Math.min(chats.length, windowStart + CHATS_WINDOW);
195
- const visible = chats.slice(windowStart, windowEnd);
196
- const maxNameLen = Math.max(0, ...visible.map(c => (c.name ?? c.threadId).length));
197
- const scrollInfo = chats.length > CHATS_WINDOW
198
- ? ` (${selectedIndex + 1}/${chats.length})`
199
- : '';
200
- return (React.createElement(Box, { flexDirection: "column", paddingX: 1 },
201
- React.createElement(Box, null,
202
- React.createElement(Text, { bold: true, color: THEME.copper }, `Conversations${scrollInfo}`.padEnd(maxNameLen + 2)),
203
- React.createElement(Text, { bold: true, color: THEME.copper }, "When")),
204
- windowStart > 0 && (React.createElement(Box, null,
205
- React.createElement(Text, { color: THEME.muted }, " \u2191 more above"))),
206
- visible.map((chat, vi) => {
207
- const i = windowStart + vi;
208
- const label = (chat.name ?? chat.threadId).padEnd(maxNameLen + 2);
209
- const when = relativeTime(chat.createdAt);
210
- const isSelected = i === selectedIndex;
211
- return (React.createElement(Box, { key: chat.threadId },
212
- React.createElement(Text, { inverse: isSelected, color: isSelected ? undefined : THEME.copper }, label),
213
- React.createElement(Text, { inverse: isSelected, color: isSelected ? undefined : THEME.muted }, when)));
214
- }),
215
- windowEnd < chats.length && (React.createElement(Box, null,
216
- React.createElement(Text, { color: THEME.muted }, " \u2193 more below"))),
217
- React.createElement(Box, null,
218
- React.createElement(Text, { color: THEME.muted }, " \u2193\u2191 navigate Enter open Esc close"))));
219
- }
220
- export default function App() {
221
- const { exit } = useApp();
222
- const { stdout } = useStdout();
223
- // Safe no-op if env keys are missing.
224
- const tracingEnabledRef = useRef(false);
225
- useEffect(() => {
226
- tracingEnabledRef.current = initLangfuseTracing();
227
- }, []);
228
- // Enable bracketed paste mode and kitty keyboard protocol (for Shift+Enter etc).
229
- useEffect(() => {
230
- stdout.write('\x1b[?2004h'); // bracketed paste
231
- stdout.write('\x1b[>1u'); // kitty keyboard protocol: disambiguate shift/ctrl/alt
232
- return () => {
233
- stdout.write('\x1b[?2004l');
234
- stdout.write('\x1b[<u'); // restore keyboard protocol
235
- };
236
- // eslint-disable-next-line react-hooks/exhaustive-deps
237
- }, [stdout]);
238
- useEffect(() => {
239
- const original = process.stdout.write.bind(process.stdout);
240
- process.stdout.write = (chunk, ...args) => {
241
- const text = typeof chunk === 'string' ? chunk : chunk.toString();
242
- // Control markers carry a per-server nonce: `<nonce>:<payload>`. We only
243
- // act when the nonce was registered by a real feedback server in THIS
244
- // process (consumePlanNonce). This prevents arbitrary tool/model output
245
- // that merely contains the marker string from triggering auto-mode or
246
- // reading files. Nonces are single-use.
247
- const feedbackMatch = /(?:^|\n)\s*Received user feedback: ([0-9a-f]{32}):(.*)/.exec(text);
248
- if (feedbackMatch && consumePlanNonce(feedbackMatch[1])) {
249
- const feedback = feedbackMatch[2].trim();
250
- setTimeout(() => {
251
- setTranscript(t => [
252
- ...t,
253
- {
254
- id: nextId('meta'),
255
- role: 'meta',
256
- text: `⊛ Plan feedback: ${feedback}`,
257
- },
258
- ]);
259
- setMessageQueue(q => [...q, feedback]);
260
- }, 0);
261
- }
262
- else {
263
- const acceptMatch = /(?:^|\n)\s*Plan accepted: ([0-9a-f]{32}):(.+)/.exec(text);
264
- if (acceptMatch &&
265
- /^[A-Za-z0-9._-]{1,64}$/.test(acceptMatch[2].trim()) &&
266
- consumePlanNonce(acceptMatch[1])) {
267
- const name = acceptMatch[2].trim();
268
- setTimeout(() => {
269
- setTranscript(t => [
270
- ...t,
271
- {
272
- id: nextId('meta'),
273
- role: 'meta',
274
- text: `✓ Plan accepted: ${name}`,
275
- },
276
- ]);
277
- const plansDir = pathModule.join(os.homedir(), '.codemax', 'plans');
278
- const planPath = pathModule.join(plansDir, `${name}.md`);
279
- if (!pathModule
280
- .resolve(planPath)
281
- .startsWith(pathModule.resolve(plansDir) + pathModule.sep)) {
282
- return;
283
- }
284
- try {
285
- const planContent = readFileSync(planPath, 'utf8');
286
- setToolExecutionMode('auto');
287
- setMessageQueue(q => [...q, `Build it:\n\n${planContent}`]);
288
- }
289
- catch { }
290
- }, 0);
291
- }
292
- }
293
- return original(chunk, ...args);
294
- };
295
- return () => {
296
- process.stdout.write = original;
297
- };
298
- }, []);
299
- // Bracketed paste state: accumulates content between \x1b[200~ and \x1b[201~.
300
- // Ink strips \x1b, so we see '[200~' and '[201~' as raw input strings.
301
- const pasteBufferRef = useRef('');
302
- const inPasteRef = useRef(false);
303
- const swallowNextDeleteRef = useRef(false);
304
- const [ctx, setCtx] = useState(() => ({
305
- threadId: createCodemaxSessionId('thread'),
306
- sessionId: createCodemaxSessionId('cli'),
307
- renderMarkdown: getConfig().renderMarkdown,
308
- screen: 'chat',
309
- }));
310
- const ctxRef = useRef(ctx);
311
- useEffect(() => {
312
- ctxRef.current = ctx;
313
- }, [ctx]);
314
- const [transcript, setTranscript] = useState(() => [
315
- {
316
- id: 'header',
317
- role: 'meta',
318
- text: 'CodeMax – OpenSource Code Agent',
319
- },
320
- ]);
321
- const [line, setLine] = useState('');
322
- const [cursor, setCursor] = useState(0);
323
- const [pastedContent, setPastedContent] = useState(null);
324
- const [todos, setTodos] = useState([]);
325
- const [pendingPrompt, setPendingPrompt] = useState(null);
326
- const [toolExecutionMode, setToolExecutionMode] = useState('ask');
327
- const [resumeResponse, setResumeResponse] = useState(null);
328
- const [isWaitingForApproval, setIsWaitingForApproval] = useState(false);
329
- const [pendingApproval, setPendingApproval] = useState(null);
330
- const [skills] = useState(() => loadProjectSkills(process.cwd()));
331
- const [activeSkill, setActiveSkill] = useState(undefined);
332
- const [suggestionIndex, setSuggestionIndex] = useState(-1);
333
- const mcpModalOpenRef = useRef(false);
334
- const [mcpModalIndex, setMcpModalIndex] = useState(0);
335
- const [mcpServers, setMcpServers] = useState(MCP_STATUSES);
336
- const mcpServersRef = useRef(MCP_STATUSES);
337
- useEffect(() => {
338
- mcpServersRef.current = mcpServers;
339
- }, [mcpServers]);
340
- const [mcpModalOpen, setMcpModalOpen] = useState(false);
341
- const openMcpModal = () => {
342
- mcpModalOpenRef.current = true;
343
- setMcpModalOpen(true);
344
- };
345
- const closeMcpModal = () => {
346
- mcpModalOpenRef.current = false;
347
- setMcpModalOpen(false);
348
- };
349
- const chatsModalOpenRef = useRef(false);
350
- const [chatsModalIndex, setChatsModalIndex] = useState(0);
351
- const [chats, setChats] = useState([]);
352
- const chatsRef = useRef([]);
353
- useEffect(() => {
354
- chatsRef.current = chats;
355
- }, [chats]);
356
- const chatsModalIndexRef = useRef(0);
357
- useEffect(() => {
358
- chatsModalIndexRef.current = chatsModalIndex;
359
- }, [chatsModalIndex]);
360
- const [chatsModalOpen, setChatsModalOpen] = useState(false);
361
- const openChatsModal = () => {
362
- chatsModalOpenRef.current = true;
363
- setChatsModalOpen(true);
364
- };
365
- const closeChatsModal = () => {
366
- chatsModalOpenRef.current = false;
367
- setChatsModalOpen(false);
368
- };
369
- const firstUserMessageRef = useRef(null);
370
- const threadNamedRef = useRef(false);
371
- const [messageQueue, setMessageQueue] = useState([]);
372
- const [isRunning, setIsRunning] = useState(false);
373
- const isRunningRef = useRef(false);
374
- useEffect(() => {
375
- isRunningRef.current = isRunning;
376
- }, [isRunning]);
377
- const [elapsedSecs, setElapsedSecs] = useState(0);
378
- const [bounceTick, setBounceTick] = useState(0);
379
- const turnStartRef = useRef(0);
380
- const currentVerbRef = useRef(['Thinking', 'Thought']);
381
- const abortControllerRef = useRef(null);
382
- const suggestions = useMemo(() => {
383
- const cmds = getCommandSuggestions(line);
384
- const skillSuggestions = getSkillSuggestions(line, skills);
385
- const combined = [...cmds, ...skillSuggestions];
386
- const seen = new Set();
387
- return combined
388
- .filter(c => {
389
- if (seen.has(c.name))
390
- return false;
391
- seen.add(c.name);
392
- return true;
393
- })
394
- .slice(0, 5);
395
- }, [line, skills]);
396
- useEffect(() => {
397
- setSuggestionIndex(-1);
398
- }, [suggestions]);
399
- const assistantBufferRef = useRef('');
400
- const [streamingText, setStreamingText] = useState('');
401
- const activeToolNameRef = useRef('');
402
- function applyPaste(pasted) {
403
- const lineCount = pasted.split('\n').length;
404
- const placeholder = `[Pasted ${lineCount} ${lineCount === 1 ? 'line' : 'lines'}]`;
405
- // Set line and cursor first without pastedContent to avoid layout issues
406
- setLine(placeholder);
407
- setCursor(placeholder.length);
408
- setPastedContent(pasted);
409
- }
410
- function append(item) {
411
- setTranscript(t => [...t, item]);
412
- }
413
- function appendSystem(text) {
414
- append({ id: nextId('system'), role: 'system', text });
415
- }
416
- function appendAssistant(text) {
417
- const now = new Date();
418
- const timestamp = now.toLocaleTimeString('en-US', {
419
- hour: '2-digit',
420
- minute: '2-digit',
421
- hour12: true,
422
- });
423
- append({ id: nextId('assistant'), role: 'assistant', text, timestamp });
424
- }
425
- function appendTool(text, kind, toolName = '', toolCall) {
426
- append({ id: nextId('tool'), role: 'tool', kind, text, toolName, toolCall });
427
- }
428
- function commitAssistantBuffer() {
429
- const buffered = assistantBufferRef.current;
430
- assistantBufferRef.current = '';
431
- setStreamingText('');
432
- if (!buffered.trim())
433
- return;
434
- appendAssistant(buffered);
435
- }
436
- function handleSubmit(rawLine) {
437
- const text = (pastedContent ?? rawLine).trimEnd();
438
- setPastedContent(null);
439
- setLine('');
440
- setCursor(0);
441
- if (!text.trim())
442
- return;
443
- if (isWaitingForApproval) {
444
- appendSystem('[CodeMax] Turn interrupted. Please approve (y) or deny (n) the pending tool.');
445
- return;
446
- }
447
- append({ id: nextId('user'), role: 'user', text });
448
- if (firstUserMessageRef.current === null) {
449
- firstUserMessageRef.current = text;
450
- }
451
- const result = executeSlashCommand(text, ctxRef.current, skills);
452
- if (result.handled) {
453
- if (result.systemMessage)
454
- appendSystem(normalizeNewlines(result.systemMessage));
455
- if (result.activateSkill !== undefined) {
456
- setActiveSkill(result.activateSkill);
457
- if (result.prompt) {
458
- setMessageQueue(q => [...q, result.prompt]);
459
- }
460
- }
461
- if (result.clearTranscript) {
462
- setTranscript([
463
- { id: 'header', role: 'meta', text: 'CodeMax – OpenSource Code Agent' },
464
- ]);
465
- setMessageQueue([]);
466
- setTodos([]);
467
- setActiveSkill(undefined);
468
- firstUserMessageRef.current = null;
469
- threadNamedRef.current = false;
470
- }
471
- if (result.nextState) {
472
- setCtx(s => ({ ...s, ...result.nextState }));
473
- if (!result.clearTranscript && result.nextState.threadId) {
474
- setMessageQueue([]);
475
- setActiveSkill(undefined);
476
- firstUserMessageRef.current = null;
477
- threadNamedRef.current = false;
478
- }
479
- }
480
- if (result.openMcpModal) {
481
- openMcpModal();
482
- setMcpModalIndex(0);
483
- return;
484
- }
485
- if (result.openChatsModal) {
486
- const chatList = listChats();
487
- setChats(chatList);
488
- chatsRef.current = chatList;
489
- openChatsModal();
490
- setChatsModalIndex(0);
491
- return;
492
- }
493
- if (result.forceCompact) {
494
- const tid = ctxRef.current.threadId;
495
- void triggerCompaction(tid)
496
- .then(() => {
497
- appendSystem('[CodeMax] Compaction complete.');
498
- })
499
- .catch((e) => {
500
- appendSystem(`[CodeMax] Compaction failed: ${e?.message || String(e)}`);
501
- });
502
- return;
503
- }
504
- if (result.shouldExit)
505
- exit();
506
- return;
507
- }
508
- setMessageQueue(q => [...q, text]);
509
- }
510
- useInput((input, key) => {
511
- if (ctxRef.current.screen !== 'chat')
512
- return;
513
- // ESC closes the MCP modal regardless of how the terminal encodes it:
514
- // - standard terminals: key.escape = true, input = ''
515
- // - raw ESC byte: input = '\x1b'
516
- // - kitty CSI-u disambiguate mode (`\x1b[>1u`): ESC arrives as '[27u' (no modifiers)
517
- // or '[27;1u' (explicit mods=1). Ink strips the leading \x1b.
518
- if (mcpModalOpenRef.current &&
519
- (key.escape || input === '\x1b' || input === '[27u' || input === '[27;1u')) {
520
- closeMcpModal();
521
- return;
522
- }
523
- if (chatsModalOpenRef.current &&
524
- (key.escape || input === '\x1b' || input === '[27u' || input === '[27;1u')) {
525
- closeChatsModal();
526
- return;
527
- }
528
- // Bracketed paste: Ink strips the leading \x1b but inner \x1b chars remain.
529
- // Full chunk arrives as '[200~<content>\x1b[201~' in a single useInput call.
530
- if (input.startsWith('[200~') || inPasteRef.current) {
531
- if (!inPasteRef.current) {
532
- inPasteRef.current = true;
533
- pasteBufferRef.current = input.slice(5); // strip '[200~'
534
- }
535
- else {
536
- pasteBufferRef.current += input;
537
- }
538
- // End marker still has \x1b intact inside the string.
539
- const endIdx = pasteBufferRef.current.indexOf('\x1b[201~');
540
- if (endIdx !== -1) {
541
- const pasted = pasteBufferRef.current.slice(0, endIdx);
542
- inPasteRef.current = false;
543
- pasteBufferRef.current = '';
544
- applyPaste(pasted);
545
- }
546
- return;
547
- }
548
- // Kitty keyboard protocol CSI-u: '[<codepoint>;<modifiers>u' (\x1b stripped by Ink).
549
- // modifiers bitmask offset by 1: 1=none, 2=Shift, 3=Alt, 5=Ctrl, 9=Super.
550
- // These are key events, never printable text — decode the few we act on, swallow the rest.
551
- const kittyMatch = /^\[(\d+)(?:;(\d+))?u$/.exec(input);
552
- if (kittyMatch) {
553
- const codepoint = Number(kittyMatch[1]);
554
- const mods = Number(kittyMatch[2] ?? '1') - 1;
555
- const ctrl = (mods & 4) !== 0;
556
- const shift = (mods & 1) !== 0;
557
- if (ctrl && codepoint === 99 && !inPasteRef.current) {
558
- exit();
559
- return;
560
- }
561
- if (shift && codepoint === 9) {
562
- setToolExecutionMode(m => {
563
- if (m === 'ask')
564
- return 'edits';
565
- if (m === 'edits')
566
- return 'auto';
567
- if (m === 'auto')
568
- return 'plan';
569
- return 'ask';
570
- });
571
- return;
572
- }
573
- // Escape (27) is decoded here regardless of modal state so it is never swallowed.
574
- if (codepoint === 27) {
575
- if (mcpModalOpenRef.current) {
576
- closeMcpModal();
577
- return;
578
- }
579
- if (chatsModalOpenRef.current) {
580
- closeChatsModal();
581
- return;
582
- }
583
- // fall through — let key.escape below handle non-modal cases
584
- return;
585
- }
586
- if (mcpModalOpenRef.current) {
587
- if (codepoint === 66) {
588
- setMcpModalIndex(i => (i + 1) % mcpServersRef.current.length);
589
- return;
590
- }
591
- if (codepoint === 65) {
592
- setMcpModalIndex(i => i <= 0 ? mcpServersRef.current.length - 1 : i - 1);
593
- return;
594
- }
595
- }
596
- if (chatsModalOpenRef.current) {
597
- if (codepoint === 66) {
598
- setChatsModalIndex(i => Math.min(i + 1, chatsRef.current.length - 1));
599
- return;
600
- }
601
- if (codepoint === 65) {
602
- setChatsModalIndex(i => Math.max(0, i - 1));
603
- return;
604
- }
605
- if (codepoint === 13) {
606
- if (isRunningRef.current)
607
- return;
608
- const selected = chatsRef.current[chatsModalIndexRef.current];
609
- if (selected) {
610
- closeChatsModal();
611
- setCtx(s => ({ ...s, threadId: selected.threadId }));
612
- setMessageQueue([]);
613
- setTodos([]);
614
- setActiveSkill(undefined);
615
- firstUserMessageRef.current = null;
616
- threadNamedRef.current = true;
617
- const historyItems = loadChatHistory(selected.threadId);
618
- setTranscript(t => [...t, ...historyItems]);
619
- }
620
- return;
621
- }
622
- return;
623
- }
624
- return;
625
- }
626
- if (key.ctrl && input === 'c' && !inPasteRef.current) {
627
- exit();
628
- return;
629
- }
630
- if (key.tab && key.shift) {
631
- setToolExecutionMode(m => {
632
- if (m === 'ask')
633
- return 'edits';
634
- if (m === 'edits')
635
- return 'auto';
636
- if (m === 'auto')
637
- return 'plan';
638
- return 'ask';
639
- });
640
- return;
641
- }
642
- if (isWaitingForApproval) {
643
- if (input.toLowerCase() === 'y' || input === '1') {
644
- setResumeResponse({ action: 'approve' });
645
- setPendingApproval(null);
646
- return;
647
- }
648
- if (input === '2') {
649
- setToolExecutionMode('auto');
650
- setResumeResponse({ action: 'approve_always' });
651
- setPendingApproval(null);
652
- return;
653
- }
654
- if (input.toLowerCase() === 'n' || input === '3') {
655
- setResumeResponse({ action: 'deny' });
656
- setPendingApproval(null);
657
- return;
658
- }
659
- }
660
- if (mcpModalOpenRef.current) {
661
- if (key.escape) {
662
- closeMcpModal();
663
- return;
664
- }
665
- if (key.downArrow) {
666
- setMcpModalIndex(i => (i + 1) % mcpServersRef.current.length);
667
- return;
668
- }
669
- if (key.upArrow) {
670
- setMcpModalIndex(i => i <= 0 ? mcpServersRef.current.length - 1 : i - 1);
671
- return;
672
- }
673
- if (input === 'r' || input === 'R') {
674
- setMcpModalIndex(idx => {
675
- const target = mcpServersRef.current[idx];
676
- if (target) {
677
- void reloadMcpServer(target.name).then(updated => {
678
- setMcpServers(prev => prev.map(s => (s.name === updated.name ? updated : s)));
679
- });
680
- }
681
- return idx;
682
- });
683
- return;
684
- }
685
- return;
686
- }
687
- if (chatsModalOpenRef.current) {
688
- if (key.escape) {
689
- closeChatsModal();
690
- return;
691
- }
692
- if (key.downArrow) {
693
- setChatsModalIndex(i => Math.min(i + 1, chatsRef.current.length - 1));
694
- return;
695
- }
696
- if (key.upArrow) {
697
- setChatsModalIndex(i => Math.max(0, i - 1));
698
- return;
699
- }
700
- if (key.return) {
701
- if (isRunningRef.current)
702
- return;
703
- const selected = chatsRef.current[chatsModalIndexRef.current];
704
- if (selected) {
705
- closeChatsModal();
706
- setCtx(s => ({ ...s, threadId: selected.threadId }));
707
- setMessageQueue([]);
708
- setTodos([]);
709
- setActiveSkill(undefined);
710
- firstUserMessageRef.current = null;
711
- threadNamedRef.current = true;
712
- const historyItems = loadChatHistory(selected.threadId);
713
- setTranscript(t => [...t, ...historyItems]);
714
- }
715
- return;
716
- }
717
- return;
718
- }
719
- if (key.escape) {
720
- if (suggestionIndex >= 0) {
721
- setSuggestionIndex(-1);
722
- return;
723
- }
724
- if (isRunning && abortControllerRef.current) {
725
- abortControllerRef.current.abort();
726
- }
727
- return;
728
- }
729
- // Shift+Enter: various terminals send different sequences.
730
- // Kitty protocol: \x1b[13;2u → Ink strips \x1b → '[13;2u'
731
- // SS3 Return: \x1bOM → Ink strips \x1b → 'OM'
732
- // VT sequence: \x1b[27;2;13~ → Ink strips \x1b → '[27;2;13~'
733
- // Warp/some xterms: \x1b[1;2F or similar
734
- if (input === '[13;2u' ||
735
- input === 'OM' ||
736
- input === '[27;2;13~' ||
737
- (key.return && key.shift)) {
738
- setLine(s => s.slice(0, cursor) + '\n' + s.slice(cursor));
739
- setCursor(c => c + 1);
740
- return;
741
- }
742
- if (key.return) {
743
- if (suggestionIndex >= 0 && suggestions[suggestionIndex]) {
744
- const completion = `/${suggestions[suggestionIndex].name} `;
745
- setLine(completion);
746
- setCursor(completion.length);
747
- setSuggestionIndex(-1);
748
- return;
749
- }
750
- handleSubmit(line);
751
- return;
752
- }
753
- if (key.tab) {
754
- if (suggestions.length) {
755
- const completion = `/${suggestions[0].name} `;
756
- setLine(completion);
757
- setCursor(completion.length);
758
- }
759
- return;
760
- }
761
- if (key.downArrow) {
762
- if (suggestions.length) {
763
- setSuggestionIndex(i => (i + 1) % suggestions.length);
764
- }
765
- return;
766
- }
767
- if (key.upArrow) {
768
- if (suggestions.length) {
769
- setSuggestionIndex(i => (i <= 0 ? suggestions.length - 1 : i - 1));
770
- }
771
- return;
772
- }
773
- // Alt+Left / Alt+Right: jump by word (macOS style).
774
- // macOS Terminal sends \x1b[1;3D / \x1b[1;3C; Ink strips leading \x1b → '[1;3D' / '[1;3C'.
775
- // Some terminals send \x1bb / \x1bf (emacs-style word motion) → 'b' / 'f' with key.meta.
776
- if (input === '[1;3D' || (key.meta && input === 'b')) {
777
- setCursor(c => {
778
- let i = c - 1;
779
- while (i > 0 && /\s/.test(line[i - 1]))
780
- i--;
781
- while (i > 0 && !/\s/.test(line[i - 1]))
782
- i--;
783
- return i;
784
- });
785
- return;
786
- }
787
- if (input === '[1;3C' || (key.meta && input === 'f')) {
788
- setCursor(c => {
789
- let i = c;
790
- while (i < line.length && /\s/.test(line[i]))
791
- i++;
792
- while (i < line.length && !/\s/.test(line[i]))
793
- i++;
794
- return i;
795
- });
796
- return;
797
- }
798
- if (key.leftArrow) {
799
- setCursor(c => Math.max(0, c - 1));
800
- return;
801
- }
802
- if (key.rightArrow) {
803
- setCursor(c => Math.min(line.length, c + 1));
804
- return;
805
- }
806
- // Alt+Backspace / Ctrl+W: delete word to the left.
807
- // macOS Terminal: \x1b+w → key.meta=true, input='w'.
808
- // VS Code terminal: Option+Backspace → two events: Ctrl+W (key.ctrl+input='w') then Delete.
809
- // The Delete that follows Ctrl+W must be swallowed; track it with a ref.
810
- if (swallowNextDeleteRef.current && key.delete && !key.meta && !key.ctrl) {
811
- swallowNextDeleteRef.current = false;
812
- return;
813
- }
814
- if ((key.meta && input === 'w') ||
815
- (key.meta && key.backspace) ||
816
- (key.meta && key.delete) ||
817
- (key.ctrl && input === 'w')) {
818
- swallowNextDeleteRef.current = key.ctrl && input === 'w';
819
- if (cursor <= 0)
820
- return;
821
- let i = cursor - 1;
822
- while (i > 0 && /\s/.test(line[i - 1]))
823
- i--;
824
- while (i > 0 && !/\s/.test(line[i - 1]))
825
- i--;
826
- setLine(s => s.slice(0, i) + s.slice(cursor));
827
- setCursor(i);
828
- return;
829
- }
830
- if (key.backspace || key.delete) {
831
- if (pastedContent !== null) {
832
- setPastedContent(null);
833
- setLine('');
834
- setCursor(0);
835
- return;
836
- }
837
- if (cursor <= 0)
838
- return;
839
- setLine(s => s.slice(0, cursor - 1) + s.slice(cursor));
840
- setCursor(c => Math.max(0, c - 1));
841
- return;
842
- }
843
- // Ignore unhandled ctrl/meta combos — they are not printable text.
844
- if (key.ctrl || key.meta)
845
- return;
846
- // Printable characters (Ink already filters escape sequences for us).
847
- // isPaste: multi-char input that is NOT a known escape sequence residual.
848
- const isPaste = input.length > 1 && !ESCAPE_RESIDUALS.has(input);
849
- if (input && (input.length === 1 || isPaste)) {
850
- if (input.length > 1) {
851
- applyPaste(input);
852
- }
853
- else {
854
- if (pastedContent !== null)
855
- setPastedContent(null);
856
- setLine(s => s.slice(0, cursor) + input + s.slice(cursor));
857
- setCursor(c => c + input.length);
858
- }
859
- }
860
- });
861
- useEffect(() => {
862
- if (!isRunning)
863
- return;
864
- const ticker = setInterval(() => {
865
- const secs = Math.round((Date.now() - turnStartRef.current) / 1000);
866
- setElapsedSecs(secs);
867
- if (secs > 0 && secs % 15 === 0) {
868
- currentVerbRef.current = pickVerb();
869
- }
870
- }, 1000);
871
- const bouncer = setInterval(() => {
872
- setBounceTick(t => (t + 1) % 3);
873
- }, 500);
874
- return () => {
875
- clearInterval(ticker);
876
- clearInterval(bouncer);
877
- };
878
- }, [isRunning]);
879
- useEffect(() => {
880
- if (isRunning || isWaitingForApproval || messageQueue.length === 0)
881
- return;
882
- const [next, ...rest] = messageQueue;
883
- setMessageQueue(rest);
884
- setPendingPrompt(next);
885
- // eslint-disable-next-line react-hooks/exhaustive-deps
886
- }, [isRunning, isWaitingForApproval, messageQueue]);
887
- useEffect(() => {
888
- if (!pendingPrompt && !resumeResponse)
889
- return;
890
- let cancelled = false;
891
- (async () => {
892
- if (pendingPrompt) {
893
- turnStartRef.current = Date.now();
894
- currentVerbRef.current = pickVerb();
895
- setElapsedSecs(0);
896
- assistantBufferRef.current = '';
897
- }
898
- setIsRunning(true);
899
- setIsWaitingForApproval(false);
900
- abortControllerRef.current = new AbortController();
901
- try {
902
- const options = {
903
- prompt: pendingPrompt || undefined,
904
- threadId: ctxRef.current.threadId,
905
- sessionId: ctxRef.current.sessionId,
906
- tracingEnabled: tracingEnabledRef.current,
907
- signal: abortControllerRef.current.signal,
908
- toolExecutionMode,
909
- resumeResponse: resumeResponse || undefined,
910
- activeSkill,
911
- };
912
- for await (const ev of streamAgentTurn(options)) {
913
- if (cancelled)
914
- break;
915
- applyAgentEvent(ev);
916
- }
917
- commitAssistantBuffer();
918
- if (!threadNamedRef.current && firstUserMessageRef.current) {
919
- threadNamedRef.current = true;
920
- const threadId = ctxRef.current.threadId;
921
- const msg = firstUserMessageRef.current;
922
- void generateThreadName(msg)
923
- .then(name => {
924
- saveThreadName(threadId, name);
925
- })
926
- .catch(() => {
927
- /* swallow */
928
- });
929
- }
930
- }
931
- finally {
932
- if (!cancelled) {
933
- setIsRunning(false);
934
- setPendingPrompt(null);
935
- setResumeResponse(null);
936
- abortControllerRef.current = null;
937
- }
938
- }
939
- })().catch((error) => {
940
- appendTool(`[CodeMax] Agent error: ${error instanceof Error ? error.message : String(error)}`, 'error');
941
- setIsRunning(false);
942
- setIsWaitingForApproval(false);
943
- setPendingPrompt(null);
944
- setResumeResponse(null);
945
- });
946
- return () => {
947
- cancelled = true;
948
- };
949
- // eslint-disable-next-line react-hooks/exhaustive-deps
950
- }, [pendingPrompt, resumeResponse]);
951
- function applyAgentEvent(ev) {
952
- debugLog(`event type=${ev.type}`);
953
- if (ev.type === 'status') {
954
- return;
955
- }
956
- if (ev.type === 'on_state_update') {
957
- setTodos(ev.todos);
958
- return;
959
- }
960
- if (ev.type === 'on_interrupt') {
961
- setIsWaitingForApproval(true);
962
- const toolName = ev.payload.toolCall.name;
963
- const args = ev.payload.toolCall.args || {};
964
- let target = '';
965
- if (toolName === 'writeFileTool' || toolName === 'editFileTool')
966
- target = args.filePath;
967
- if (toolName === 'shellTool')
968
- target = args.command;
969
- const question = target
970
- ? `Do you want to run ${toolName} on ${target}?`
971
- : `Do you want to run ${toolName}?`;
972
- setPendingApproval({ toolName, target, question });
973
- return;
974
- }
975
- if (ev.type === 'on_tool_start') {
976
- commitAssistantBuffer();
977
- activeToolNameRef.current = ev.toolName ?? '';
978
- appendTool(ev.input ?? `${ev.toolName ?? 'tool'}…`, 'start', ev.toolName ?? '');
979
- return;
980
- }
981
- if (ev.type === 'on_tool_end') {
982
- activeToolNameRef.current = '';
983
- appendTool(ev.output ?? '', 'end', ev.toolName ?? '');
984
- return;
985
- }
986
- if (ev.type === 'on_tool_error') {
987
- commitAssistantBuffer();
988
- const failedTool = activeToolNameRef.current;
989
- activeToolNameRef.current = '';
990
- const label = failedTool
991
- ? `${failedTool} failed`
992
- : ev.error ?? '[tool error]';
993
- appendTool(label, 'error', failedTool);
994
- return;
995
- }
996
- if (ev.type === 'on_chat_delta') {
997
- assistantBufferRef.current += ev.text;
998
- debugLog(`delta text="${ev.text.replace(/\n/g, '\\n')}" total_len=${assistantBufferRef.current.length}`);
999
- setStreamingText(assistantBufferRef.current);
1000
- return;
1001
- }
1002
- if (ev.type === 'on_chat_end') {
1003
- debugLog(`chat_end total_len=${assistantBufferRef.current.length}`);
1004
- commitAssistantBuffer();
1005
- const elapsed = Math.round((Date.now() - turnStartRef.current) / 1000);
1006
- const pastTense = currentVerbRef.current[1];
1007
- append({
1008
- id: nextId('meta'),
1009
- role: 'meta',
1010
- text: `${pastTense} for ${elapsed}s`,
1011
- });
1012
- }
1013
- }
1014
- // Never force a minimum wider than the terminal; otherwise Ink borders "break" on shrink.
1015
- const width = clamp(stdout?.columns ?? 80, 20, 1000);
1016
- const BOUNCE_FRAMES = ['⊛', '●', '◉'];
1017
- const bounceChar = BOUNCE_FRAMES[bounceTick % BOUNCE_FRAMES.length];
1018
- const promptPrefix = isRunning || isWaitingForApproval ? '…' : '>';
1019
- const left = line.slice(0, cursor);
1020
- const right = line.slice(cursor);
1021
- const renderedTranscript = useMemo(() => {
1022
- return transcript.map(item => {
1023
- if (item.role === 'assistant') {
1024
- const raw = normalizeNewlines(item.text);
1025
- const out = ctx.renderMarkdown ? renderAssistantMarkdown(raw) : raw;
1026
- return {
1027
- id: item.id,
1028
- label: 'assistant',
1029
- text: out,
1030
- timestamp: item.timestamp,
1031
- toolName: '',
1032
- kind: undefined,
1033
- };
1034
- }
1035
- if (item.role === 'user') {
1036
- return {
1037
- id: item.id,
1038
- label: 'user',
1039
- text: normalizeNewlines(item.text),
1040
- timestamp: '',
1041
- toolName: '',
1042
- kind: undefined,
1043
- };
1044
- }
1045
- if (item.role === 'tool') {
1046
- return {
1047
- id: item.id,
1048
- label: 'tool',
1049
- text: normalizeNewlines(item.text),
1050
- timestamp: '',
1051
- toolName: item.toolName,
1052
- kind: item.kind,
1053
- toolCall: item.toolCall,
1054
- };
1055
- }
1056
- if (item.role === 'meta') {
1057
- return {
1058
- id: item.id,
1059
- label: 'meta',
1060
- text: item.text,
1061
- timestamp: '',
1062
- toolName: '',
1063
- kind: undefined,
1064
- };
1065
- }
1066
- return {
1067
- id: item.id,
1068
- label: 'system',
1069
- text: normalizeNewlines(item.text),
1070
- timestamp: '',
1071
- toolName: '',
1072
- kind: undefined,
1073
- };
1074
- });
1075
- }, [ctx.renderMarkdown, transcript]);
1076
- const streamingTextOut = useMemo(() => {
1077
- if (!streamingText)
1078
- return '';
1079
- const raw = normalizeNewlines(streamingText);
1080
- return ctx.renderMarkdown ? renderAssistantMarkdown(raw) : raw;
1081
- }, [ctx.renderMarkdown, streamingText]);
1082
- const modeLabels = {
1083
- ask: 'Ask for Edits',
1084
- edits: 'Accept Edits',
1085
- auto: 'Auto-Accept',
1086
- plan: 'Plan Mode',
1087
- };
1088
- if (ctx.screen === 'model') {
1089
- return (React.createElement(ModelSettingsScreen, { width: width, onSave: cfg => {
1090
- updateModelConfig(cfg);
1091
- setCtx(prev => ({ ...prev, screen: 'chat' }));
1092
- appendSystem(`[CodeMax] Model updated: ${cfg.modelName}`);
1093
- }, onCancel: () => {
1094
- setCtx(prev => ({ ...prev, screen: 'chat' }));
1095
- } }));
1096
- }
1097
- if (ctx.screen === 'compaction') {
1098
- return (React.createElement(CompactionSettingsScreen, { width: width, onSave: pct => {
1099
- setCtx(prev => ({ ...prev, screen: 'chat' }));
1100
- appendSystem(`[CodeMax] Compaction threshold set to ${pct}%`);
1101
- }, onCancel: () => {
1102
- setCtx(prev => ({ ...prev, screen: 'chat' }));
1103
- } }));
1104
- }
1105
- return (React.createElement(Box, { flexDirection: "column", width: width },
1106
- transcript.length <= 1 ? (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
1107
- React.createElement(HomePanel, { width: width }),
1108
- React.createElement(Box, { marginTop: 1 },
1109
- React.createElement(TodoPanel, { todos: todos, width: width })),
1110
- React.createElement(Box, { marginTop: 1 },
1111
- React.createElement(Text, { color: THEME.muted }, hr(width))))) : (React.createElement(Box, { marginTop: 1 },
1112
- React.createElement(TodoPanel, { todos: todos, width: width }))),
1113
- React.createElement(Static, { items: renderedTranscript }, m => (React.createElement(Box, { key: m.id, flexDirection: "column", marginBottom: m.label === 'tool' && (m.kind === 'end' || m.kind === 'error')
1114
- ? 1
1115
- : 0 },
1116
- m.label === 'user' && (React.createElement(Box, { marginTop: 1 },
1117
- React.createElement(Text, { color: THEME.copper }, '> '),
1118
- React.createElement(Text, { color: THEME.fg }, m.text))),
1119
- m.label === 'assistant' && (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
1120
- React.createElement(Box, null,
1121
- React.createElement(Text, { color: THEME.green }, '• '),
1122
- React.createElement(Text, { color: THEME.fg }, m.text),
1123
- m.timestamp ? (React.createElement(Text, { color: THEME.muted },
1124
- " ",
1125
- m.timestamp)) : null))),
1126
- m.label === 'meta' && (React.createElement(Box, null,
1127
- React.createElement(Text, { color: THEME.muted },
1128
- '⊛ ',
1129
- m.text))),
1130
- m.label === 'tool' && m.kind === 'start' && (React.createElement(Box, { marginTop: 1 },
1131
- React.createElement(Text, null,
1132
- React.createElement(Text, { color: THEME.green }, '• '),
1133
- React.createElement(Text, { color: THEME.fg }, m.text.trim())))),
1134
- m.label === 'tool' &&
1135
- m.kind === 'end' &&
1136
- m.toolName !== 'shellTool' &&
1137
- isWriteOutput(m.text) && (React.createElement(Box, { flexDirection: "column", marginLeft: 1 }, (() => {
1138
- const { summary, entries, omitted } = parseWriteOutput(m.text);
1139
- return (React.createElement(React.Fragment, null,
1140
- React.createElement(Text, { color: THEME.muted },
1141
- '└ ',
1142
- summary),
1143
- React.createElement(Box, { flexDirection: "column", paddingLeft: 2 },
1144
- entries.map((entry, i) => (React.createElement(Box, { key: i },
1145
- React.createElement(Text, { color: THEME.muted }, padRight(String(entry.lineNum), 5)),
1146
- React.createElement(Box, { paddingX: 1 },
1147
- React.createElement(Text, { color: entry.type === 'add'
1148
- ? THEME.diffAddFg
1149
- : entry.type === 'remove'
1150
- ? THEME.diffRemoveFg
1151
- : THEME.fg, backgroundColor: entry.type === 'add'
1152
- ? THEME.diffAdd
1153
- : entry.type === 'remove'
1154
- ? THEME.diffRemove
1155
- : undefined },
1156
- entry.type === 'add'
1157
- ? '+ '
1158
- : entry.type === 'remove'
1159
- ? '- '
1160
- : ' ',
1161
- truncateToWidth(entry.text, Math.max(10, width - 15))))))),
1162
- omitted > 0 && (React.createElement(Text, { color: THEME.muted },
1163
- React.createElement(Text, null,
1164
- '... +',
1165
- omitted,
1166
- ' lines (ctrl+o to expand)'))))));
1167
- })())),
1168
- m.label === 'tool' &&
1169
- m.kind === 'end' &&
1170
- m.toolName === 'shellTool' &&
1171
- m.text.trim() && (React.createElement(Box, { flexDirection: "column", marginLeft: 1 }, m.text
1172
- .trim()
1173
- .split('\n')
1174
- .map((line, i) => (React.createElement(Text, { key: i, color: THEME.muted },
1175
- i === 0 ? '└─ ' : ' ',
1176
- truncateToWidth(line, Math.max(10, width - 7))))))),
1177
- m.label === 'tool' &&
1178
- m.kind === 'end' &&
1179
- m.toolName !== 'shellTool' &&
1180
- !isWriteOutput(m.text) &&
1181
- m.text.trim() && (React.createElement(Box, { marginLeft: 1 },
1182
- React.createElement(Text, { color: THEME.muted },
1183
- '└ ',
1184
- truncateToWidth(m.text.trim(), Math.max(10, width - 8))))),
1185
- m.label === 'tool' && m.kind === 'error' && (React.createElement(Text, { color: "#e06c75" },
1186
- React.createElement(Text, null, '• '),
1187
- React.createElement(Text, null, truncateToWidth(m.text.trim(), Math.max(10, width - 4))))),
1188
- m.label === 'system' && React.createElement(Text, { color: THEME.muted }, m.text)))),
1189
- streamingTextOut ? (React.createElement(Box, { marginTop: 1, flexDirection: "column" },
1190
- React.createElement(Box, null,
1191
- React.createElement(Text, { color: THEME.green }, '• '),
1192
- React.createElement(Text, { color: THEME.fg }, streamingTextOut)))) : null,
1193
- pendingApproval && (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: THEME.copper, marginTop: 1, paddingX: 1 },
1194
- React.createElement(Text, { color: THEME.fg }, pendingApproval.question),
1195
- React.createElement(Box, { flexDirection: "column", marginTop: 1 },
1196
- React.createElement(Text, { color: THEME.fg }, "1. Yes"),
1197
- React.createElement(Text, { color: THEME.fg }, "2. Yes, allow all edits during this session (shift+tab)"),
1198
- React.createElement(Text, { color: THEME.fg }, "3. No")))),
1199
- React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: THEME.muted, marginTop: pendingApproval ? 0 : 1, width: width },
1200
- React.createElement(Box, { justifyContent: "space-between", paddingX: 1 },
1201
- React.createElement(Box, null, isRunning ? (React.createElement(Text, { wrap: "truncate" },
1202
- React.createElement(Text, { color: THEME.copper },
1203
- bounceChar,
1204
- " "),
1205
- React.createElement(Text, { color: THEME.fg },
1206
- currentVerbRef.current[0],
1207
- "\u2026 "),
1208
- React.createElement(Text, { color: THEME.muted },
1209
- "(",
1210
- elapsedSecs,
1211
- "s)"),
1212
- messageQueue.length > 0 && (React.createElement(Text, { color: THEME.muted },
1213
- ' ',
1214
- "\u00B7 ",
1215
- messageQueue.length,
1216
- " queued")))) : isWaitingForApproval ? (React.createElement(Text, { color: THEME.muted }, "Waiting for decision\u2026")) : (React.createElement(Text, { color: THEME.muted, wrap: "truncate" },
1217
- '• Ready · thread ',
1218
- ctx.threadId))),
1219
- React.createElement(Box, null,
1220
- React.createElement(Text, { color: THEME.muted }, "[Shift+Tab] "),
1221
- React.createElement(Text, { color: THEME.copper, bold: true }, modeLabels[toolExecutionMode]))),
1222
- React.createElement(Text, { color: THEME.muted },
1223
- " ",
1224
- hr(width - 4)),
1225
- mcpModalOpen && (React.createElement(McpServerModal, { servers: mcpServers, selectedIndex: mcpModalIndex })),
1226
- chatsModalOpen && (React.createElement(ChatsModal, { chats: chats, selectedIndex: chatsModalIndex })),
1227
- React.createElement(SlashCommandModal, { suggestions: suggestions, selectedIndex: suggestionIndex }),
1228
- React.createElement(Box, { paddingX: 1, paddingBottom: 0 },
1229
- React.createElement(Text, { color: THEME.copper },
1230
- promptPrefix,
1231
- " "),
1232
- React.createElement(Text, { wrap: "wrap" }, left +
1233
- chalk.inverse(right.length ? right[0] : ' ') +
1234
- right.slice(1))),
1235
- React.createElement(Box, { paddingX: 1, paddingBottom: 1 }, transcript.length <= 1 && !line ? (React.createElement(Text, { color: THEME.muted }, "try \"refactor <file> to ...\"")) : null))));
1236
- }