friday-code 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,629 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+ import { Box, Text, useApp, useStdout } from 'ink';
4
+ import { colors, icons } from '../theme/theme.js';
5
+ import { CollapsedRunCard, HeaderBar, HelpView, KeyboardBar, MessageBubble, RunTimelineCard, StatusBar, Toast, WelcomeScreen, } from '../components/components.js';
6
+ import { InputBox } from './InputBox.js';
7
+ import { ModelSelector } from './ModelSelector.js';
8
+ import { AgentEngine } from '../../core/engine/agent.js';
9
+ import { getSetting, setSetting } from '../../core/providers/registry.js';
10
+ import { existsSync, statSync } from 'fs';
11
+ import { resolve } from 'path';
12
+ const App = ({ initialDirectory }) => {
13
+ const { exit } = useApp();
14
+ const { stdout } = useStdout();
15
+ const [messages, setMessages] = useState([]);
16
+ const [isGenerating, setIsGenerating] = useState(false);
17
+ const [showModelSelector, setShowModelSelector] = useState(false);
18
+ const [activeModel, setActiveModel] = useState(getSetting('active_model') || '');
19
+ const [activeProvider, setActiveProvider] = useState(getSetting('active_provider') || '');
20
+ const [workingDirectory, setWorkingDirectory] = useState(initialDirectory || process.cwd());
21
+ const [tokenUsage, setTokenUsage] = useState();
22
+ const [statusText, setStatusText] = useState('');
23
+ const [engineStatus, setEngineStatus] = useState('idle');
24
+ const [showWelcome, setShowWelcome] = useState(true);
25
+ const [termSize, setTermSize] = useState({ cols: stdout?.columns || 80, rows: stdout?.rows || 24 });
26
+ const engineRef = useRef(null);
27
+ const activeRunIdRef = useRef(null);
28
+ const lastUserMessageRef = useRef('');
29
+ const [pendingApproval, setPendingApproval] = useState(null);
30
+ useEffect(() => {
31
+ const onResize = () => {
32
+ if (stdout) {
33
+ setTermSize({ cols: stdout.columns, rows: stdout.rows });
34
+ }
35
+ };
36
+ stdout?.on('resize', onResize);
37
+ return () => {
38
+ stdout?.off('resize', onResize);
39
+ };
40
+ }, [stdout]);
41
+ useEffect(() => {
42
+ engineRef.current = new AgentEngine({
43
+ workingDirectory,
44
+ });
45
+ engineRef.current.ensureConversation();
46
+ }, []);
47
+ const compact = termSize.cols < 100 || termSize.rows < 24;
48
+ // Each past turn (user + collapsed run) ≈ 3 visual lines.
49
+ // Reserve ~16 lines for current run + header + footer.
50
+ const maxPastTurns = Math.max(1, Math.floor((termSize.rows - 16) / 3));
51
+ const transcriptLimit = Math.max(3, maxPastTurns * 2 + 2); // pairs + current user+run
52
+ const displayScope = fitPath(workingDirectory, Math.max(20, termSize.cols - 24));
53
+ const visibleMessages = messages.slice(-transcriptLimit);
54
+ const hasUserMessages = useMemo(() => messages.some(message => message.type === 'user'), [messages]);
55
+ const showLaunchpad = showWelcome && !hasUserMessages;
56
+ const showKeyboardBar = termSize.rows >= 16;
57
+ const showStatusBar = termSize.rows >= 14;
58
+ const handleCommand = useCallback((input) => {
59
+ const parts = input.trim().split(/\s+/);
60
+ const command = parts[0]?.toLowerCase();
61
+ switch (command) {
62
+ case '/help':
63
+ setMessages(previous => [...previous, { type: 'help' }]);
64
+ return true;
65
+ case '/model':
66
+ case '/provider':
67
+ setShowModelSelector(true);
68
+ return true;
69
+ case '/clear':
70
+ setMessages([
71
+ {
72
+ type: 'toast',
73
+ message: 'Conversation cleared',
74
+ toastType: 'success',
75
+ },
76
+ ]);
77
+ setTokenUsage(undefined);
78
+ setStatusText('');
79
+ setEngineStatus('idle');
80
+ setShowWelcome(true);
81
+ engineRef.current?.clearHistory();
82
+ engineRef.current?.ensureConversation();
83
+ return true;
84
+ case '/new':
85
+ setMessages([
86
+ {
87
+ type: 'toast',
88
+ message: 'Fresh run ready',
89
+ toastType: 'success',
90
+ },
91
+ ]);
92
+ setTokenUsage(undefined);
93
+ setStatusText('');
94
+ setEngineStatus('idle');
95
+ setShowWelcome(true);
96
+ engineRef.current?.clearHistory();
97
+ engineRef.current?.ensureConversation();
98
+ return true;
99
+ case '/scope': {
100
+ const nextPath = parts[1];
101
+ if (!nextPath) {
102
+ setMessages(previous => [
103
+ ...previous,
104
+ {
105
+ type: 'toast',
106
+ message: `Current scope: ${workingDirectory}`,
107
+ toastType: 'info',
108
+ },
109
+ ]);
110
+ return true;
111
+ }
112
+ const resolved = resolve(workingDirectory, nextPath);
113
+ if (existsSync(resolved) && statSync(resolved).isDirectory()) {
114
+ setWorkingDirectory(resolved);
115
+ engineRef.current?.setWorkingDirectory(resolved);
116
+ setMessages(previous => [
117
+ ...previous,
118
+ {
119
+ type: 'toast',
120
+ message: `Scope → ${resolved}`,
121
+ toastType: 'success',
122
+ },
123
+ ]);
124
+ }
125
+ else {
126
+ setMessages(previous => [
127
+ ...previous,
128
+ {
129
+ type: 'toast',
130
+ message: `Directory not found: ${resolved}`,
131
+ toastType: 'error',
132
+ },
133
+ ]);
134
+ }
135
+ return true;
136
+ }
137
+ case '/history':
138
+ setMessages(previous => [
139
+ ...previous,
140
+ {
141
+ type: 'toast',
142
+ message: `${engineRef.current?.getMessageCount() || 0} messages in history`,
143
+ toastType: 'info',
144
+ },
145
+ ]);
146
+ return true;
147
+ case '/config': {
148
+ const key = parts[1];
149
+ const value = parts.slice(2).join(' ');
150
+ if (!key) {
151
+ // Show all config
152
+ const maxSteps = getSetting('max_steps') || '25';
153
+ const approval = engineRef.current?.getApprovalMode() ? 'on' : 'off';
154
+ const model = getSetting('active_model') || 'none';
155
+ const provider = getSetting('active_provider') || 'none';
156
+ setMessages(previous => [
157
+ ...previous,
158
+ { type: 'toast', message: `maxSteps: ${maxSteps} · approval: ${approval} · model: ${provider}/${model}`, toastType: 'info' },
159
+ ]);
160
+ return true;
161
+ }
162
+ const configKeys = {
163
+ maxSteps: () => {
164
+ const num = parseInt(value);
165
+ if (isNaN(num) || num < 1 || num > 100) {
166
+ setMessages(p => [...p, { type: 'toast', message: 'maxSteps must be 1-100', toastType: 'error' }]);
167
+ return;
168
+ }
169
+ setSetting('max_steps', String(num));
170
+ setMessages(p => [...p, { type: 'toast', message: `maxSteps → ${num}`, toastType: 'success' }]);
171
+ },
172
+ approval: () => {
173
+ const enabled = value === 'on' || value === 'true' || value === '1';
174
+ engineRef.current?.setApprovalMode(enabled);
175
+ setMessages(p => [...p, { type: 'toast', message: `Tool approval → ${enabled ? 'on' : 'off'}`, toastType: 'success' }]);
176
+ },
177
+ };
178
+ const handler = configKeys[key];
179
+ if (handler) {
180
+ handler();
181
+ }
182
+ else {
183
+ setMessages(p => [...p, { type: 'toast', message: `Unknown config: ${key}. Available: maxSteps, approval`, toastType: 'error' }]);
184
+ }
185
+ return true;
186
+ }
187
+ case '/exit':
188
+ exit();
189
+ return true;
190
+ default:
191
+ return false;
192
+ }
193
+ }, [exit, workingDirectory]);
194
+ const handleSubmit = useCallback(async (input) => {
195
+ if (showWelcome) {
196
+ setShowWelcome(false);
197
+ }
198
+ if (input.startsWith('/') && handleCommand(input)) {
199
+ return;
200
+ }
201
+ if (!engineRef.current) {
202
+ return;
203
+ }
204
+ lastUserMessageRef.current = input;
205
+ setIsGenerating(true);
206
+ setEngineStatus('running');
207
+ setStatusText('Working...');
208
+ setTokenUsage(undefined);
209
+ setPendingApproval(null);
210
+ const runId = createMessageId('run');
211
+ activeRunIdRef.current = runId;
212
+ setMessages(previous => [
213
+ ...previous,
214
+ { type: 'user', content: input },
215
+ createRunMessage(runId),
216
+ ]);
217
+ try {
218
+ for await (const event of engineRef.current.run(input)) {
219
+ if (event.type === 'tool-approval-request') {
220
+ // Show approval prompt in timeline
221
+ setMessages(previous => updateRunMessage(previous, runId, run => appendRunNode(run, {
222
+ id: `${run.id}-approval-${event.toolCallId}`,
223
+ kind: 'approval',
224
+ label: event.toolName,
225
+ detail: formatToolArgs(event.toolName, event.args ? JSON.stringify(event.args) : ''),
226
+ stepNumber: event.stepNumber,
227
+ toolCallId: event.toolCallId,
228
+ status: 'running',
229
+ })));
230
+ setPendingApproval({
231
+ toolCallId: event.toolCallId,
232
+ toolName: event.toolName,
233
+ args: event.args,
234
+ });
235
+ setStatusText(`Approve ${event.toolName}?`);
236
+ continue;
237
+ }
238
+ handleStreamEvent(event, {
239
+ runId,
240
+ setMessages,
241
+ setStatusText,
242
+ setTokenUsage,
243
+ setEngineStatus,
244
+ });
245
+ }
246
+ }
247
+ catch (error) {
248
+ setEngineStatus('error');
249
+ setMessages(previous => markRunErrored(previous, runId, error.message || 'Unknown error'));
250
+ }
251
+ finally {
252
+ setIsGenerating(false);
253
+ setStatusText('');
254
+ activeRunIdRef.current = null;
255
+ setPendingApproval(null);
256
+ }
257
+ }, [handleCommand, showWelcome]);
258
+ const handleCancel = useCallback(() => {
259
+ engineRef.current?.abort();
260
+ setIsGenerating(false);
261
+ setStatusText('');
262
+ setEngineStatus('idle');
263
+ setMessages(previous => markRunCancelled(previous, activeRunIdRef.current));
264
+ setMessages(previous => [
265
+ ...previous,
266
+ {
267
+ type: 'toast',
268
+ message: 'Generation cancelled',
269
+ toastType: 'warning',
270
+ },
271
+ ]);
272
+ }, []);
273
+ const handleApproval = useCallback((approved) => {
274
+ if (!pendingApproval || !engineRef.current)
275
+ return;
276
+ const runId = activeRunIdRef.current;
277
+ engineRef.current.respondToApproval(approved);
278
+ // Update approval node in timeline
279
+ if (runId) {
280
+ setMessages(previous => updateRunMessage(previous, runId, run => {
281
+ const approvalNodeId = `${run.id}-approval-${pendingApproval.toolCallId}`;
282
+ return {
283
+ ...run,
284
+ nodes: run.nodes.map(n => n.id === approvalNodeId
285
+ ? { ...n, status: approved ? 'done' : 'error' }
286
+ : n),
287
+ };
288
+ }));
289
+ }
290
+ setPendingApproval(null);
291
+ setStatusText(approved ? 'Continuing...' : 'Tool denied');
292
+ }, [pendingApproval]);
293
+ const handleRetry = useCallback(() => {
294
+ if (engineStatus !== 'error' || !lastUserMessageRef.current)
295
+ return;
296
+ handleSubmit(lastUserMessageRef.current);
297
+ }, [engineStatus, handleSubmit]);
298
+ const handleModelSelect = useCallback((providerId, modelId) => {
299
+ setActiveProvider(providerId);
300
+ setActiveModel(modelId);
301
+ setShowModelSelector(false);
302
+ setMessages(previous => [
303
+ ...previous,
304
+ {
305
+ type: 'toast',
306
+ message: `Model → ${providerId}/${modelId}`,
307
+ toastType: 'success',
308
+ },
309
+ ]);
310
+ }, []);
311
+ return (_jsxs(Box, { flexDirection: "column", width: termSize.cols, height: termSize.rows, children: [_jsx(HeaderBar, { provider: activeProvider, model: activeModel || 'none', scope: displayScope, status: engineStatus, compact: compact }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [showLaunchpad ? (_jsx(WelcomeScreen, { model: activeModel, provider: activeProvider, scope: displayScope, compact: compact })) : null, _jsx(Box, { flexDirection: "column", marginTop: showLaunchpad ? 1 : 0, children: visibleMessages.map((message, index) => {
312
+ switch (message.type) {
313
+ case 'user':
314
+ return _jsx(MessageBubble, { role: "user", content: message.content }, index);
315
+ case 'run': {
316
+ // Find if this is the last (most recent) run in the transcript
317
+ const isLastRun = !visibleMessages.slice(index + 1).some(m => m.type === 'run');
318
+ if (!isLastRun && message.status !== 'running') {
319
+ // Collapse past completed runs to save viewport space
320
+ return _jsx(CollapsedRunCard, { nodes: message.nodes, status: message.status }, message.id);
321
+ }
322
+ return (_jsx(RunTimelineCard, { status: message.status, nodes: message.nodes, isStreaming: message.isStreaming, compact: compact, viewportWidth: termSize.cols, viewportHeight: termSize.rows }, message.id));
323
+ }
324
+ case 'help':
325
+ return _jsx(HelpView, {}, index);
326
+ case 'toast':
327
+ return _jsx(Toast, { message: message.message, type: message.toastType }, index);
328
+ case 'error':
329
+ return _jsx(Text, { color: colors.red, children: ` ${icons.fail} ${message.message}` }, index);
330
+ default:
331
+ return null;
332
+ }
333
+ }) })] }), showModelSelector ? (_jsx(Box, { marginTop: 1, children: _jsx(ModelSelector, { onSelect: handleModelSelect, onCancel: () => setShowModelSelector(false) }) })) : null, !showModelSelector ? (_jsx(InputBox, { onSubmit: handleSubmit, isGenerating: isGenerating, onCancel: handleCancel, onApprove: () => handleApproval(true), onDeny: () => handleApproval(false), onRetry: handleRetry, pendingApproval: !!pendingApproval, canRetry: engineStatus === 'error', workingDirectory: workingDirectory, phase: engineStatus, viewportWidth: termSize.cols })) : null, showKeyboardBar ? (_jsx(KeyboardBar, { isGenerating: isGenerating, compact: compact, pendingApproval: !!pendingApproval, canRetry: engineStatus === 'error' })) : null, showStatusBar ? (_jsx(StatusBar, { tokens: tokenUsage, status: statusText, engineStatus: engineStatus, compact: compact })) : null] }));
334
+ };
335
+ function handleStreamEvent(event, handlers) {
336
+ const { runId, setMessages, setStatusText, setTokenUsage, setEngineStatus } = handlers;
337
+ switch (event.type) {
338
+ case 'status':
339
+ setStatusText(event.message);
340
+ break;
341
+ case 'step-start':
342
+ // No separate tracking needed — timeline nodes handle everything
343
+ break;
344
+ case 'reasoning':
345
+ setMessages(previous => updateRunMessage(previous, runId, run => appendRunNode(run, {
346
+ id: `${run.id}-thinking-${event.stepNumber}`,
347
+ kind: 'thinking',
348
+ label: `thinking`,
349
+ detail: event.text,
350
+ stepNumber: event.stepNumber,
351
+ status: 'running',
352
+ })));
353
+ break;
354
+ case 'tool-call-streaming-start':
355
+ setMessages(previous => updateRunMessage(previous, runId, run => appendRunNode(run, {
356
+ id: `${run.id}-tool-call-${event.toolCallId}`,
357
+ kind: 'tool-call',
358
+ label: event.toolName,
359
+ detail: '',
360
+ stepNumber: event.stepNumber,
361
+ toolCallId: event.toolCallId,
362
+ status: 'running',
363
+ })));
364
+ setStatusText(`Running ${event.toolName}`);
365
+ break;
366
+ case 'tool-call-delta':
367
+ // Don't update UI on every delta — wait for full tool-call
368
+ break;
369
+ case 'tool-call':
370
+ setMessages(previous => updateRunMessage(previous, runId, run => appendRunNode(run, {
371
+ id: `${run.id}-tool-call-${event.toolCallId}`,
372
+ kind: 'tool-call',
373
+ label: event.toolName,
374
+ detail: formatToolArgs(event.toolName, event.args ? JSON.stringify(event.args) : ''),
375
+ stepNumber: event.stepNumber,
376
+ toolCallId: event.toolCallId,
377
+ status: 'running',
378
+ })));
379
+ setStatusText(`Running ${event.toolName}`);
380
+ break;
381
+ case 'tool-result':
382
+ // Update tool-call node status, then add result node
383
+ setMessages(previous => updateRunMessage(previous, runId, run => {
384
+ const nodes = run.nodes.map(node => (node.kind === 'tool-call' && node.toolCallId === event.toolCallId
385
+ ? { ...node, status: hasError(event.result) ? 'error' : 'done' }
386
+ : node));
387
+ return appendRunNode({ ...run, nodes }, {
388
+ id: `${run.id}-tool-result-${event.toolCallId}`,
389
+ kind: 'tool-result',
390
+ label: event.toolName,
391
+ detail: formatToolResult(event.result),
392
+ stepNumber: event.stepNumber,
393
+ toolCallId: event.toolCallId,
394
+ status: hasError(event.result) ? 'error' : 'done',
395
+ });
396
+ }));
397
+ break;
398
+ case 'step-finish':
399
+ // No plan system to update — step transitions handled naturally
400
+ break;
401
+ case 'text-delta':
402
+ setMessages(previous => updateRunMessage(previous, runId, run => appendRunNode(run, {
403
+ id: `${run.id}-text-${event.stepNumber}`,
404
+ kind: 'text',
405
+ label: 'response',
406
+ detail: event.text,
407
+ stepNumber: event.stepNumber,
408
+ status: 'running',
409
+ })));
410
+ break;
411
+ case 'finish': {
412
+ const stepLabel = `${event.totalSteps} step${event.totalSteps === 1 ? '' : 's'}`;
413
+ const tokLabel = event.usage
414
+ ? `${event.usage.promptTokens}+${event.usage.completionTokens} tok`
415
+ : '';
416
+ const doneDetail = [stepLabel, tokLabel].filter(Boolean).join(' · ');
417
+ setMessages(previous => updateRunMessage(previous, runId, run => appendRunNode({
418
+ ...run,
419
+ status: 'complete',
420
+ isStreaming: false,
421
+ }, {
422
+ id: `${run.id}-done`,
423
+ kind: 'done',
424
+ label: 'done',
425
+ detail: doneDetail || undefined,
426
+ status: 'done',
427
+ })));
428
+ setTokenUsage(event.usage
429
+ ? { prompt: event.usage.promptTokens, completion: event.usage.completionTokens }
430
+ : undefined);
431
+ setEngineStatus('complete');
432
+ setStatusText(stepLabel);
433
+ break;
434
+ }
435
+ case 'error':
436
+ setEngineStatus('error');
437
+ setMessages(previous => markRunErrored(previous, runId, event.error));
438
+ break;
439
+ }
440
+ }
441
+ // ═══════════════════════════════════════════════════
442
+ // RUN MESSAGE HELPERS
443
+ // ═══════════════════════════════════════════════════
444
+ function createRunMessage(id) {
445
+ return {
446
+ type: 'run',
447
+ id,
448
+ status: 'running',
449
+ isStreaming: true,
450
+ nodes: [],
451
+ };
452
+ }
453
+ function markRunErrored(messages, runId, error) {
454
+ return updateRunMessage(messages, runId, run => appendRunNode({
455
+ ...run,
456
+ status: 'error',
457
+ isStreaming: false,
458
+ }, {
459
+ id: `${run.id}-error-${run.nodes.length}`,
460
+ kind: 'phase',
461
+ label: error,
462
+ status: 'error',
463
+ }));
464
+ }
465
+ function markRunCancelled(messages, runId) {
466
+ if (!runId) {
467
+ return messages;
468
+ }
469
+ return updateRunMessage(messages, runId, run => ({
470
+ ...run,
471
+ status: 'cancelled',
472
+ isStreaming: false,
473
+ }));
474
+ }
475
+ function updateRunMessage(messages, runId, updater) {
476
+ return messages.map(message => {
477
+ if (message.type !== 'run' || message.id !== runId) {
478
+ return message;
479
+ }
480
+ return updater(message);
481
+ });
482
+ }
483
+ function appendRunNode(run, node) {
484
+ // Merge text/thinking deltas into existing node of same kind + step.
485
+ // Search backward (not just last node) because smoothStream can delay
486
+ // text-deltas past step-finish/done nodes.
487
+ if (node.kind === 'thinking' || node.kind === 'text') {
488
+ for (let i = run.nodes.length - 1; i >= Math.max(0, run.nodes.length - 6); i--) {
489
+ const existing = run.nodes[i];
490
+ if (existing.kind === node.kind && existing.stepNumber === node.stepNumber) {
491
+ return {
492
+ ...run,
493
+ nodes: [
494
+ ...run.nodes.slice(0, i),
495
+ {
496
+ ...existing,
497
+ detail: (existing.detail || '') + (node.detail || ''),
498
+ status: node.status ?? existing.status,
499
+ },
500
+ ...run.nodes.slice(i + 1),
501
+ ],
502
+ };
503
+ }
504
+ }
505
+ }
506
+ // Upsert tool-call nodes (same toolCallId)
507
+ if (node.kind === 'tool-call') {
508
+ const existingIndex = run.nodes.findIndex(existing => existing.kind === 'tool-call' && existing.toolCallId === node.toolCallId);
509
+ if (existingIndex !== -1) {
510
+ const nodes = [...run.nodes];
511
+ const existing = nodes[existingIndex];
512
+ nodes[existingIndex] = {
513
+ ...existing,
514
+ label: node.label,
515
+ detail: node.detail || existing.detail,
516
+ status: node.status ?? existing.status,
517
+ };
518
+ return { ...run, nodes };
519
+ }
520
+ }
521
+ // Deduplicate identical phase nodes
522
+ const lastNode = run.nodes[run.nodes.length - 1];
523
+ if (node.kind === 'phase' && lastNode?.kind === 'phase' && lastNode.label === node.label) {
524
+ return run;
525
+ }
526
+ return {
527
+ ...run,
528
+ nodes: [...run.nodes, node],
529
+ };
530
+ }
531
+ // ═══════════════════════════════════════════════════
532
+ // FORMATTING HELPERS
533
+ // ═══════════════════════════════════════════════════
534
+ function createMessageId(prefix) {
535
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
536
+ }
537
+ function fitPath(pathname, limit) {
538
+ const home = process.env.HOME || '';
539
+ const shortened = home && pathname.startsWith(home) ? `~${pathname.slice(home.length)}` : pathname;
540
+ if (shortened.length <= limit) {
541
+ return shortened;
542
+ }
543
+ const parts = shortened.split('/').filter(Boolean);
544
+ if (parts.length <= 2) {
545
+ return `${shortened.slice(0, Math.max(0, limit - 1))}…`;
546
+ }
547
+ const prefix = shortened.startsWith('~') ? '~' : '';
548
+ const tail = parts.slice(-2).join('/');
549
+ const compact = `${prefix}/…/${tail}`.replace('//', '/');
550
+ if (compact.length <= limit) {
551
+ return compact;
552
+ }
553
+ return `…${shortened.slice(-(limit - 1))}`;
554
+ }
555
+ function formatToolArgs(toolName, argsText) {
556
+ try {
557
+ const args = JSON.parse(argsText);
558
+ if (typeof args === 'object' && args !== null) {
559
+ const entries = Object.entries(args);
560
+ if (entries.length === 0)
561
+ return `${toolName}()`;
562
+ const parts = entries.map(([k, v]) => {
563
+ const val = typeof v === 'string' ? (v.length > 40 ? `${v.slice(0, 37)}…` : v) : JSON.stringify(v);
564
+ return `${k}: ${val}`;
565
+ });
566
+ return parts.join(', ');
567
+ }
568
+ return argsText;
569
+ }
570
+ catch {
571
+ return argsText;
572
+ }
573
+ }
574
+ function formatToolResult(result) {
575
+ if (typeof result === 'string') {
576
+ try {
577
+ const parsed = JSON.parse(result);
578
+ return formatToolResultObject(parsed);
579
+ }
580
+ catch {
581
+ return result.length > 120 ? `${result.slice(0, 117)}…` : result;
582
+ }
583
+ }
584
+ if (typeof result === 'object' && result !== null) {
585
+ return formatToolResultObject(result);
586
+ }
587
+ return String(result).slice(0, 120);
588
+ }
589
+ function formatToolResultObject(obj) {
590
+ if (!obj || typeof obj !== 'object')
591
+ return String(obj).slice(0, 120);
592
+ const record = obj;
593
+ // Directory listing: { entries: [...] }
594
+ if (record.entries && Array.isArray(record.entries)) {
595
+ const names = record.entries.slice(0, 5).map((e) => {
596
+ const entry = e;
597
+ return entry.name || entry.path || '?';
598
+ });
599
+ const suffix = record.entries.length > 5 ? ` +${record.entries.length - 5} more` : '';
600
+ return `${record.entries.length} items: ${names.join(', ')}${suffix}`;
601
+ }
602
+ // Success result: { success: true, path: "..." }
603
+ if (record.success === true) {
604
+ if (record.path)
605
+ return String(record.path);
606
+ if (record.output && typeof record.output === 'string') {
607
+ return record.output.length > 120 ? `${record.output.slice(0, 117)}…` : record.output;
608
+ }
609
+ return 'ok';
610
+ }
611
+ // Error result
612
+ if (record.error) {
613
+ return `error: ${String(record.error).slice(0, 100)}`;
614
+ }
615
+ // File content
616
+ if (record.content && typeof record.content === 'string') {
617
+ return record.content.length > 120 ? `${record.content.slice(0, 117)}…` : record.content;
618
+ }
619
+ if (record.output && typeof record.output === 'string') {
620
+ return record.output.length > 120 ? `${record.output.slice(0, 117)}…` : record.output;
621
+ }
622
+ const json = JSON.stringify(obj);
623
+ return json.length > 120 ? `${json.slice(0, 117)}…` : json;
624
+ }
625
+ function hasError(result) {
626
+ return typeof result === 'object' && result !== null && 'error' in result && Boolean(result.error);
627
+ }
628
+ export default App;
629
+ //# sourceMappingURL=App.js.map