dodo-ai 0.1.0 → 0.1.4

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.
package/dist/index.mjs ADDED
@@ -0,0 +1,3763 @@
1
+ // src/index.tsx
2
+ import fs5 from "fs";
3
+ import path4 from "path";
4
+ import process2 from "process";
5
+ import { spawn } from "child_process";
6
+ import net from "net";
7
+ import { render } from "ink";
8
+
9
+ // src/ui/app.tsx
10
+ import path3 from "path";
11
+ import { useEffect as useEffect11, useState as useState13, useCallback as useCallback10 } from "react";
12
+ import { Box as Box20, useApp as useApp6 } from "ink";
13
+
14
+ // src/hooks/useEngineConnection.ts
15
+ import { useState as useState5, useCallback as useCallback6, useMemo as useMemo2 } from "react";
16
+
17
+ // src/hooks/useConversation.ts
18
+ import { useCallback, useState } from "react";
19
+
20
+ // src/contexts/ConversationContext.tsx
21
+ import { createContext, useContext } from "react";
22
+ import { jsx } from "react/jsx-runtime";
23
+ var ConversationContext = createContext(void 0);
24
+ function ConversationProvider({ children }) {
25
+ const conversation = useConversationState();
26
+ return /* @__PURE__ */ jsx(ConversationContext.Provider, { value: conversation, children });
27
+ }
28
+ function useConversationContext() {
29
+ const context = useContext(ConversationContext);
30
+ if (!context) {
31
+ throw new Error("useConversationContext must be used within a ConversationProvider");
32
+ }
33
+ return context;
34
+ }
35
+
36
+ // src/hooks/useConversation.ts
37
+ function isMetadataEqual(a, b) {
38
+ if (a === b) return true;
39
+ if (!a || !b) return a === b;
40
+ const keysA = Object.keys(a);
41
+ const keysB = Object.keys(b);
42
+ if (keysA.length !== keysB.length) return false;
43
+ return keysA.every((key) => a[key] === b[key]);
44
+ }
45
+ function useConversationState() {
46
+ const [turns, setTurns] = useState([]);
47
+ const pushTurn = useCallback((userMessage) => {
48
+ const id = `${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 6)}`;
49
+ setTurns((prev) => [...prev, {
50
+ id,
51
+ user: userMessage,
52
+ assistant: "",
53
+ done: false,
54
+ collapsed: false,
55
+ activities: [],
56
+ timelineSteps: [],
57
+ logLines: []
58
+ }]);
59
+ }, []);
60
+ const appendAssistantContent = useCallback((content, replace = false) => {
61
+ setTurns((prev) => {
62
+ if (prev.length === 0) return prev;
63
+ const lastIndex = prev.length - 1;
64
+ const last = prev[lastIndex];
65
+ const newAssistant = replace ? content : last.assistant + content;
66
+ if (last.assistant === newAssistant) return prev;
67
+ const updated = [...prev];
68
+ updated[lastIndex] = { ...last, assistant: newAssistant };
69
+ return updated;
70
+ });
71
+ }, []);
72
+ const markLastTurnDone = useCallback((summary) => {
73
+ setTurns((prev) => {
74
+ if (prev.length === 0) return prev;
75
+ const lastIndex = prev.length - 1;
76
+ const last = prev[lastIndex];
77
+ const updated = [...prev];
78
+ updated[lastIndex] = { ...last, done: true, summary: summary || last.summary };
79
+ return updated;
80
+ });
81
+ }, []);
82
+ const addActivity = useCallback((turnId, activity) => {
83
+ setTurns((prev) => {
84
+ if (prev.length === 0) return prev;
85
+ const lastIndex = prev.length - 1;
86
+ if (prev[lastIndex].id === turnId) {
87
+ const updated = [...prev];
88
+ const last = prev[lastIndex];
89
+ updated[lastIndex] = {
90
+ ...last,
91
+ activities: [...last.activities, activity]
92
+ };
93
+ return updated;
94
+ }
95
+ return prev.map((turn) => {
96
+ if (turn.id === turnId) {
97
+ return { ...turn, activities: [...turn.activities, activity] };
98
+ }
99
+ return turn;
100
+ });
101
+ });
102
+ }, []);
103
+ const updateActivity = useCallback(
104
+ (turnId, activityId, updates) => {
105
+ setTurns((prev) => {
106
+ if (prev.length === 0) return prev;
107
+ const lastIndex = prev.length - 1;
108
+ const updateTurnActivities = (turn) => {
109
+ const updatedActivities = turn.activities.map((activity) => {
110
+ if (activity.id === activityId) {
111
+ const hasChanges = Object.entries(updates).some(([key, value]) => {
112
+ if (key === "metadata") return !isMetadataEqual(activity.metadata, value);
113
+ return activity[key] !== value;
114
+ });
115
+ if (!hasChanges) return activity;
116
+ return { ...activity, ...updates };
117
+ }
118
+ return activity;
119
+ });
120
+ const activitiesChanged = updatedActivities.some((a, i) => a !== turn.activities[i]);
121
+ if (!activitiesChanged) return turn;
122
+ return { ...turn, activities: updatedActivities };
123
+ };
124
+ if (prev[lastIndex].id === turnId) {
125
+ const updated = [...prev];
126
+ updated[lastIndex] = updateTurnActivities(prev[lastIndex]);
127
+ return updated;
128
+ }
129
+ return prev.map((turn) => {
130
+ if (turn.id === turnId) {
131
+ return updateTurnActivities(turn);
132
+ }
133
+ return turn;
134
+ });
135
+ });
136
+ },
137
+ []
138
+ );
139
+ const toggleTurnCollapsed = useCallback((turnId) => {
140
+ setTurns((prev) => {
141
+ return prev.map((turn) => {
142
+ if (turn.id === turnId) {
143
+ return { ...turn, collapsed: !turn.collapsed };
144
+ }
145
+ return turn;
146
+ });
147
+ });
148
+ }, []);
149
+ const addTimelineStep = useCallback((turnId, step) => {
150
+ setTurns((prev) => {
151
+ if (prev.length === 0) return prev;
152
+ const lastIndex = prev.length - 1;
153
+ if (prev[lastIndex].id === turnId) {
154
+ const updated = [...prev];
155
+ const last = prev[lastIndex];
156
+ updated[lastIndex] = {
157
+ ...last,
158
+ timelineSteps: [...last.timelineSteps || [], step]
159
+ };
160
+ return updated;
161
+ }
162
+ return prev.map((turn) => {
163
+ if (turn.id === turnId) {
164
+ return { ...turn, timelineSteps: [...turn.timelineSteps || [], step] };
165
+ }
166
+ return turn;
167
+ });
168
+ });
169
+ }, []);
170
+ const updateTimelineStep = useCallback(
171
+ (turnId, stepId, updates) => {
172
+ setTurns((prev) => {
173
+ if (prev.length === 0) return prev;
174
+ const lastIndex = prev.length - 1;
175
+ const updateTurnSteps = (turn) => {
176
+ const updatedSteps = (turn.timelineSteps || []).map((step) => {
177
+ if (step.id === stepId) {
178
+ const hasChanges = Object.entries(updates).some(([key, value]) => {
179
+ if (key === "metadata") return !isMetadataEqual(step.metadata, value);
180
+ return step[key] !== value;
181
+ });
182
+ if (!hasChanges) return step;
183
+ return { ...step, ...updates };
184
+ }
185
+ return step;
186
+ });
187
+ const stepsChanged = updatedSteps.some((s, i) => s !== (turn.timelineSteps || [])[i]);
188
+ if (!stepsChanged) return turn;
189
+ return { ...turn, timelineSteps: updatedSteps };
190
+ };
191
+ if (prev[lastIndex].id === turnId) {
192
+ const updated = [...prev];
193
+ updated[lastIndex] = updateTurnSteps(prev[lastIndex]);
194
+ return updated;
195
+ }
196
+ return prev.map((turn) => {
197
+ if (turn.id === turnId) {
198
+ return updateTurnSteps(turn);
199
+ }
200
+ return turn;
201
+ });
202
+ });
203
+ },
204
+ []
205
+ );
206
+ const appendLogLine = useCallback((turnId, line) => {
207
+ setTurns((prev) => {
208
+ if (prev.length === 0) return prev;
209
+ const lastIndex = prev.length - 1;
210
+ if (prev[lastIndex].id === turnId) {
211
+ const updated = [...prev];
212
+ const last = prev[lastIndex];
213
+ updated[lastIndex] = {
214
+ ...last,
215
+ logLines: [...last.logLines || [], line]
216
+ };
217
+ return updated;
218
+ }
219
+ return prev.map((turn) => {
220
+ if (turn.id === turnId) {
221
+ return { ...turn, logLines: [...turn.logLines || [], line] };
222
+ }
223
+ return turn;
224
+ });
225
+ });
226
+ }, []);
227
+ const appendToolOutput = useCallback(
228
+ (turnId, invocationId, output, stream) => {
229
+ if (!output) return;
230
+ setTurns((prev) => {
231
+ if (prev.length === 0) return prev;
232
+ const lastIndex = prev.length - 1;
233
+ const updateTurnOutput = (turn) => {
234
+ const updatedSteps = (turn.timelineSteps || []).map((step) => {
235
+ if (step.id === invocationId) {
236
+ const currentMetadata = step.metadata || {};
237
+ const currentStream = currentMetadata[stream] || "";
238
+ return {
239
+ ...step,
240
+ metadata: {
241
+ ...currentMetadata,
242
+ [stream]: currentStream + output
243
+ }
244
+ };
245
+ }
246
+ return step;
247
+ });
248
+ return { ...turn, timelineSteps: updatedSteps };
249
+ };
250
+ if (prev[lastIndex].id === turnId) {
251
+ const updated = [...prev];
252
+ updated[lastIndex] = updateTurnOutput(prev[lastIndex]);
253
+ return updated;
254
+ }
255
+ return prev.map((turn) => {
256
+ if (turn.id === turnId) {
257
+ return updateTurnOutput(turn);
258
+ }
259
+ return turn;
260
+ });
261
+ });
262
+ },
263
+ []
264
+ );
265
+ const addContextEvent = useCallback((turnId, event) => {
266
+ const step = {
267
+ id: `ctx-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
268
+ toolName: "context",
269
+ label: event.description,
270
+ status: "done",
271
+ startedAt: /* @__PURE__ */ new Date(),
272
+ finishedAt: /* @__PURE__ */ new Date(),
273
+ type: "context_event",
274
+ contextEventType: event.kind,
275
+ metadata: {
276
+ before: event.before,
277
+ after: event.after
278
+ }
279
+ };
280
+ addTimelineStep(turnId, step);
281
+ }, [addTimelineStep]);
282
+ const getCurrentTurnId = useCallback(() => {
283
+ if (turns.length === 0) return null;
284
+ return turns[turns.length - 1].id;
285
+ }, [turns]);
286
+ const clearTurns = useCallback(() => {
287
+ setTurns([]);
288
+ }, []);
289
+ return {
290
+ turns,
291
+ pushTurn,
292
+ appendAssistantContent,
293
+ markLastTurnDone,
294
+ addActivity,
295
+ updateActivity,
296
+ toggleTurnCollapsed,
297
+ addTimelineStep,
298
+ updateTimelineStep,
299
+ appendLogLine,
300
+ appendToolOutput,
301
+ addContextEvent,
302
+ getCurrentTurnId,
303
+ clearTurns
304
+ };
305
+ }
306
+ function useConversation() {
307
+ return useConversationContext();
308
+ }
309
+
310
+ // src/utils/logger.ts
311
+ import fs from "fs";
312
+ import path from "path";
313
+ var LOG_FILE = path.resolve(process.cwd(), "ui_debug.log");
314
+ function safeStringify(obj, space = 2) {
315
+ const seen = /* @__PURE__ */ new WeakSet();
316
+ return JSON.stringify(obj, (key, value) => {
317
+ if (typeof value === "object" && value !== null) {
318
+ if (seen.has(value)) {
319
+ return "[Circular]";
320
+ }
321
+ seen.add(value);
322
+ }
323
+ return value;
324
+ }, space);
325
+ }
326
+ var STATE_LOG_FILE = path.resolve(process.cwd(), "ui_state.log");
327
+ var lastStateLogTime = 0;
328
+ var STATE_LOG_THROTTLE_MS = 1e3;
329
+ var logQueue = [];
330
+ var isProcessing = false;
331
+ async function processQueue() {
332
+ if (isProcessing || logQueue.length === 0) return;
333
+ isProcessing = true;
334
+ while (logQueue.length > 0) {
335
+ const item = logQueue.shift();
336
+ if (item) {
337
+ try {
338
+ await fs.promises.appendFile(item.file, item.message);
339
+ } catch (e) {
340
+ }
341
+ }
342
+ }
343
+ isProcessing = false;
344
+ }
345
+ function queueLog(file, message) {
346
+ logQueue.push({ file, message });
347
+ void processQueue();
348
+ }
349
+ var logger = {
350
+ log: (message, data) => {
351
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
352
+ const logMessage = `[${timestamp}] [INFO] ${message} ${data ? safeStringify(data, 0) : ""}
353
+ `;
354
+ queueLog(LOG_FILE, logMessage);
355
+ },
356
+ error: (message, error) => {
357
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
358
+ let errorDetails = "";
359
+ if (error instanceof Error) {
360
+ errorDetails = safeStringify({
361
+ message: error.message,
362
+ stack: error.stack,
363
+ name: error.name
364
+ });
365
+ } else if (typeof error === "object" && error !== null) {
366
+ try {
367
+ errorDetails = safeStringify(error);
368
+ if (errorDetails === "{}") {
369
+ const props = Object.getOwnPropertyNames(error).reduce((acc, key) => {
370
+ acc[key] = error[key];
371
+ return acc;
372
+ }, {});
373
+ errorDetails = safeStringify(props);
374
+ }
375
+ } catch (e) {
376
+ errorDetails = String(error);
377
+ }
378
+ } else {
379
+ errorDetails = String(error);
380
+ }
381
+ const errorMessage = `[${timestamp}] [ERROR] ${message} ${errorDetails}
382
+ `;
383
+ queueLog(LOG_FILE, errorMessage);
384
+ },
385
+ clear: () => {
386
+ try {
387
+ fs.writeFileSync(LOG_FILE, "");
388
+ fs.writeFileSync(STATE_LOG_FILE, "");
389
+ } catch (_) {
390
+ }
391
+ },
392
+ snapshot: (name, data) => {
393
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
394
+ const snapshotData = safeStringify(data, 2);
395
+ const logMessage = `
396
+ [${timestamp}] [SNAPSHOT] === ${name} ===
397
+ ${snapshotData}
398
+ =====================================
399
+ `;
400
+ queueLog(LOG_FILE, logMessage);
401
+ },
402
+ state: (message, data, immediate = false) => {
403
+ const now = Date.now();
404
+ if (!immediate && now - lastStateLogTime < STATE_LOG_THROTTLE_MS) {
405
+ return;
406
+ }
407
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
408
+ const logMessage = `[${timestamp}] [STATE] ${message} ${data ? safeStringify(data, 0) : ""}
409
+ `;
410
+ queueLog(STATE_LOG_FILE, logMessage);
411
+ if (!immediate) {
412
+ lastStateLogTime = now;
413
+ }
414
+ }
415
+ };
416
+
417
+ // src/hooks/useEngineConnection.ts
418
+ import { useApp as useApp2 } from "ink";
419
+
420
+ // src/hooks/useSessionLifecycle.ts
421
+ import { useState as useState2, useRef as useRef2, useEffect as useEffect2, useCallback as useCallback3 } from "react";
422
+
423
+ // src/hooks/useEngineEvents.ts
424
+ import { useCallback as useCallback2, useEffect, useRef } from "react";
425
+ function useEngineEvents(client, callbacks) {
426
+ const mountedRef = useRef(true);
427
+ useEffect(() => {
428
+ mountedRef.current = true;
429
+ return () => {
430
+ mountedRef.current = false;
431
+ };
432
+ }, []);
433
+ const callbacksRef = useRef(callbacks);
434
+ useEffect(() => {
435
+ callbacksRef.current = callbacks;
436
+ });
437
+ const handleEvent = useCallback2(
438
+ (event) => {
439
+ if (!mountedRef.current) return;
440
+ const currentCallbacks = callbacksRef.current;
441
+ switch (event.type) {
442
+ case "status":
443
+ if (event.status === "session_ready" && event.session_id) {
444
+ if (currentCallbacks.onSessionReady) currentCallbacks.onSessionReady(event.session_id);
445
+ } else if (event.status === "engine_ready") {
446
+ } else if (event.status === "setup_complete") {
447
+ if (currentCallbacks.onSetupComplete) currentCallbacks.onSetupComplete();
448
+ const status = mapStatusToUiStatus(event.status);
449
+ if (currentCallbacks.onStatusChange) currentCallbacks.onStatusChange(status, event.detail ?? event.status);
450
+ } else {
451
+ const status = mapStatusToUiStatus(event.status);
452
+ if (currentCallbacks.onStatusChange) currentCallbacks.onStatusChange(status, event.detail ?? event.status);
453
+ }
454
+ break;
455
+ case "assistant_text":
456
+ if (currentCallbacks.onAssistantText) {
457
+ currentCallbacks.onAssistantText(
458
+ event.content,
459
+ false,
460
+ // Never replace, always append for assistant text
461
+ event.final ?? false
462
+ );
463
+ }
464
+ break;
465
+ case "tool_event":
466
+ if (currentCallbacks.onToolEvent) {
467
+ currentCallbacks.onToolEvent({
468
+ tool: event.tool,
469
+ phase: event.phase,
470
+ success: event.success,
471
+ details: event.details,
472
+ timestamp: /* @__PURE__ */ new Date()
473
+ });
474
+ }
475
+ break;
476
+ case "files_changed":
477
+ if (currentCallbacks.onFilesChanged) currentCallbacks.onFilesChanged(event.files);
478
+ break;
479
+ case "done":
480
+ if (currentCallbacks.onDone) currentCallbacks.onDone(event.summary, event.files_changed);
481
+ if (currentCallbacks.onStatusChange) currentCallbacks.onStatusChange("ready", event.summary ?? "Done");
482
+ break;
483
+ case "error":
484
+ if (currentCallbacks.onError) currentCallbacks.onError(event.message);
485
+ if (currentCallbacks.onStatusChange) currentCallbacks.onStatusChange("error", event.message);
486
+ break;
487
+ case "token_usage":
488
+ if (currentCallbacks.onTokenUsage) {
489
+ currentCallbacks.onTokenUsage({
490
+ prompt: event.prompt_tokens,
491
+ limit: event.limit,
492
+ total: event.total
493
+ });
494
+ }
495
+ break;
496
+ case "project_plan":
497
+ if (currentCallbacks.onProjectPlan) currentCallbacks.onProjectPlan(event.content, event.source);
498
+ break;
499
+ case "context":
500
+ if (currentCallbacks.onContext) {
501
+ currentCallbacks.onContext({
502
+ kind: event.kind,
503
+ description: event.description,
504
+ before: event.before,
505
+ after: event.after
506
+ });
507
+ }
508
+ break;
509
+ case "tool_output":
510
+ if (event.stream === "stdout" || event.stream === "stderr") {
511
+ if (currentCallbacks.onToolOutput) {
512
+ currentCallbacks.onToolOutput(
513
+ event.invocation_id,
514
+ event.tool,
515
+ event.output,
516
+ event.stream
517
+ );
518
+ }
519
+ }
520
+ break;
521
+ case "activity":
522
+ const activity = {
523
+ id: event.activity_id,
524
+ type: mapActivityType(event.activity_type),
525
+ tool: event.tool,
526
+ target: event.target,
527
+ metadata: event.metadata,
528
+ status: mapActivityStatus(event.status),
529
+ timestamp: /* @__PURE__ */ new Date(),
530
+ // Fallback if start_time not parsed
531
+ startTime: event.start_time ? new Date(event.start_time) : void 0,
532
+ endTime: event.end_time ? new Date(event.end_time) : void 0,
533
+ durationMs: event.duration_ms,
534
+ invocationId: event.invocation_id,
535
+ command: event.command,
536
+ codeChange: event.code_change ? {
537
+ file: event.code_change.file,
538
+ before: event.code_change.before,
539
+ after: event.code_change.after,
540
+ startLine: event.code_change.start_line,
541
+ endLine: event.code_change.end_line
542
+ } : void 0
543
+ };
544
+ if (currentCallbacks.onActivity) currentCallbacks.onActivity(activity);
545
+ break;
546
+ case "setup_required":
547
+ if (currentCallbacks.onSetupRequired) currentCallbacks.onSetupRequired();
548
+ break;
549
+ case "config_loaded":
550
+ if (currentCallbacks.onConfigLoaded) currentCallbacks.onConfigLoaded(event.config);
551
+ break;
552
+ case "config_reloaded":
553
+ if (currentCallbacks.onConfigReloaded) currentCallbacks.onConfigReloaded(event.provider, event.model_name);
554
+ break;
555
+ case "cancelled":
556
+ if (currentCallbacks.onCancelled) currentCallbacks.onCancelled(event.reason);
557
+ if (currentCallbacks.onStatusChange) currentCallbacks.onStatusChange("ready", event.reason || "Cancelled");
558
+ break;
559
+ case "session_history":
560
+ if (currentCallbacks.onSessionHistory) {
561
+ currentCallbacks.onSessionHistory(event.title, event.summary || "", event.messages || []);
562
+ }
563
+ break;
564
+ case "project_permission_required":
565
+ if (currentCallbacks.onProjectPermissionRequired) {
566
+ currentCallbacks.onProjectPermissionRequired(event.repo_root);
567
+ }
568
+ break;
569
+ }
570
+ },
571
+ []
572
+ );
573
+ const handleError = useCallback2(
574
+ (err) => {
575
+ if (!mountedRef.current) return;
576
+ const currentCallbacks = callbacksRef.current;
577
+ if (currentCallbacks.onError) currentCallbacks.onError(err.message);
578
+ if (currentCallbacks.onStatusChange) currentCallbacks.onStatusChange("error", err.message);
579
+ },
580
+ []
581
+ );
582
+ const handleClose = useCallback2(() => {
583
+ if (!mountedRef.current) return;
584
+ const currentCallbacks = callbacksRef.current;
585
+ if (currentCallbacks.onStatusChange) currentCallbacks.onStatusChange("disconnected", "Engine connection closed");
586
+ }, []);
587
+ useEffect(() => {
588
+ client.on("event", handleEvent);
589
+ client.on("error", handleError);
590
+ client.on("close", handleClose);
591
+ return () => {
592
+ client.off("event", handleEvent);
593
+ client.off("error", handleError);
594
+ client.off("close", handleClose);
595
+ };
596
+ }, [client, handleEvent, handleError, handleClose]);
597
+ }
598
+ function mapStatusToUiStatus(status) {
599
+ switch (status) {
600
+ case "engine_ready":
601
+ return "connecting";
602
+ case "thinking":
603
+ return "thinking";
604
+ case "step_start":
605
+ return "running_tools";
606
+ case "done":
607
+ return "ready";
608
+ case "retry":
609
+ case "budget_exceeded":
610
+ return "running_tools";
611
+ default:
612
+ return "ready";
613
+ }
614
+ }
615
+ function mapActivityType(type) {
616
+ switch (type) {
617
+ case "thinking":
618
+ return "thinking";
619
+ case "reasoning":
620
+ return "reasoning";
621
+ case "edit":
622
+ return "edit";
623
+ case "tool":
624
+ return "tool";
625
+ default:
626
+ return "tool";
627
+ }
628
+ }
629
+ function mapActivityStatus(status) {
630
+ switch (status) {
631
+ case "started":
632
+ return "active";
633
+ case "completed":
634
+ return "completed";
635
+ case "failed":
636
+ return "failed";
637
+ default:
638
+ return "active";
639
+ }
640
+ }
641
+
642
+ // src/utils/debugLogger.ts
643
+ import fs2 from "fs";
644
+ try {
645
+ fs2.appendFileSync("/tmp/dodo_debug.log", `[${(/* @__PURE__ */ new Date()).toISOString()}] debugLogger.ts loaded, DODO_DEBUG=${process.env.DODO_DEBUG}
646
+ `);
647
+ } catch {
648
+ }
649
+ var DebugLogger = class {
650
+ constructor() {
651
+ this.logPath = "/tmp/dodo_debug.log";
652
+ }
653
+ /**
654
+ * Check if debug mode is enabled (checked dynamically each time)
655
+ */
656
+ isEnabled() {
657
+ return process.env.DODO_DEBUG === "true";
658
+ }
659
+ /**
660
+ * Write a structured log entry
661
+ */
662
+ async log(entry) {
663
+ if (!this.isEnabled()) return;
664
+ const fullEntry = {
665
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
666
+ ...entry
667
+ };
668
+ try {
669
+ await fs2.promises.appendFile(this.logPath, JSON.stringify(fullEntry) + "\n");
670
+ } catch {
671
+ }
672
+ }
673
+ /**
674
+ * Log an event received from the backend
675
+ */
676
+ event(component, eventType, data) {
677
+ this.log({
678
+ category: "event",
679
+ component,
680
+ message: `Received event: ${eventType}`,
681
+ data
682
+ });
683
+ }
684
+ /**
685
+ * Log a command sent to the backend
686
+ */
687
+ command(component, cmdType, data) {
688
+ this.log({
689
+ category: "command",
690
+ component,
691
+ message: `Sent command: ${cmdType}`,
692
+ data
693
+ });
694
+ }
695
+ /**
696
+ * Log a state change
697
+ */
698
+ state(component, change, before, after) {
699
+ this.log({
700
+ category: "state",
701
+ component,
702
+ message: change,
703
+ data: { before, after }
704
+ });
705
+ }
706
+ /**
707
+ * Log component lifecycle events
708
+ */
709
+ lifecycle(component, phase, details) {
710
+ this.log({
711
+ category: "lifecycle",
712
+ component,
713
+ message: `${phase}${details ? `: ${details}` : ""}`
714
+ });
715
+ }
716
+ /**
717
+ * Log an error with context
718
+ */
719
+ error(component, error, context) {
720
+ this.log({
721
+ category: "error",
722
+ component,
723
+ message: error instanceof Error ? error.message : error,
724
+ data: {
725
+ ...error instanceof Error ? { stack: error.stack } : {},
726
+ ...context
727
+ }
728
+ });
729
+ }
730
+ /**
731
+ * Clear the debug log file
732
+ */
733
+ clear() {
734
+ try {
735
+ fs2.writeFileSync(this.logPath, "");
736
+ } catch {
737
+ }
738
+ }
739
+ };
740
+ var debugLog = new DebugLogger();
741
+
742
+ // src/hooks/useSessionLifecycle.ts
743
+ import { useApp } from "ink";
744
+ function useSessionLifecycle(client, repoPath, requestedSessionId, engineExited, skipConnection = false) {
745
+ const { exit } = useApp();
746
+ const [sessionId, setSessionId] = useState2();
747
+ const [status, setStatus] = useState2(engineExited ? "disconnected" : "connecting");
748
+ const [infoMessage, setInfoMessage] = useState2("Connecting to engine...");
749
+ const [error, setError] = useState2();
750
+ const [tokenUsage, setTokenUsage] = useState2();
751
+ const [loadedConfig, setLoadedConfig] = useState2();
752
+ const [isSetupRequired, setIsSetupRequired] = useState2(false);
753
+ const [isProjectPermissionRequired, setIsProjectPermissionRequired] = useState2(false);
754
+ const [pendingRepoRoot, setPendingRepoRoot] = useState2(null);
755
+ const statusRef = useRef2(status);
756
+ useEffect2(() => {
757
+ statusRef.current = status;
758
+ }, [status]);
759
+ useEffect2(() => {
760
+ if (skipConnection) {
761
+ return;
762
+ }
763
+ if (engineExited) {
764
+ statusRef.current = "disconnected";
765
+ setStatus("disconnected");
766
+ setInfoMessage("Engine process exited");
767
+ return;
768
+ }
769
+ client.startSession({ repoRoot: repoPath, sessionId: requestedSessionId }).catch((err) => {
770
+ setError(err instanceof Error ? err.message : String(err));
771
+ statusRef.current = "error";
772
+ setStatus("error");
773
+ });
774
+ }, [client, repoPath, requestedSessionId, engineExited, skipConnection]);
775
+ useEffect2(() => {
776
+ if (engineExited) {
777
+ setInfoMessage(
778
+ `Engine exited${engineExited.code !== null ? ` (code ${engineExited.code})` : ""}`
779
+ );
780
+ const timer = setTimeout(() => {
781
+ process.stdout.write("\x1B[?1049l");
782
+ exit();
783
+ }, 500);
784
+ return () => clearTimeout(timer);
785
+ }
786
+ }, [engineExited, exit]);
787
+ useEngineEvents(client, {
788
+ onStatusChange: useCallback3((newStatus, message) => {
789
+ if (statusRef.current !== newStatus) {
790
+ statusRef.current = newStatus;
791
+ setStatus(newStatus);
792
+ setInfoMessage(message);
793
+ } else {
794
+ setInfoMessage((prevMsg) => prevMsg !== message ? message : prevMsg);
795
+ }
796
+ }, []),
797
+ onSessionReady: useCallback3((id) => {
798
+ setSessionId(id);
799
+ statusRef.current = "ready";
800
+ setStatus("ready");
801
+ setInfoMessage(`Session ready (${id.slice(0, 8)})`);
802
+ setError(void 0);
803
+ client.sendCommand({ type: "get_config" });
804
+ }, [client]),
805
+ onError: useCallback3((message) => {
806
+ setError(message);
807
+ statusRef.current = "error";
808
+ setStatus("error");
809
+ }, []),
810
+ onTokenUsage: useCallback3((usage) => {
811
+ setTokenUsage({
812
+ used: usage.prompt,
813
+ total: usage.limit,
814
+ percentage: usage.limit > 0 ? usage.prompt / usage.limit * 100 : 0,
815
+ sessionTotal: usage.total
816
+ });
817
+ }, []),
818
+ onSetupRequired: useCallback3(() => {
819
+ setIsSetupRequired(true);
820
+ }, []),
821
+ onProjectPermissionRequired: useCallback3((repoRoot) => {
822
+ setIsProjectPermissionRequired(true);
823
+ setPendingRepoRoot(repoRoot);
824
+ }, []),
825
+ onConfigLoaded: useCallback3((config) => {
826
+ setLoadedConfig(config);
827
+ }, []),
828
+ onConfigReloaded: useCallback3((provider, modelName) => {
829
+ statusRef.current = "ready";
830
+ setStatus("ready");
831
+ setInfoMessage(`Config reloaded: ${provider} (${modelName})`);
832
+ setLoadedConfig((prev) => prev ? {
833
+ ...prev,
834
+ llm_provider: provider,
835
+ model: modelName
836
+ } : { llm_provider: provider, model: modelName });
837
+ debugLog.event("useSessionLifecycle", "config_reloaded", { provider, modelName });
838
+ }, []),
839
+ onCancelled: useCallback3((reason) => {
840
+ logger.log(`[useSessionLifecycle] onCancelled received. reason=${reason}`);
841
+ statusRef.current = "ready";
842
+ setStatus("ready");
843
+ setInfoMessage(reason || "Task cancelled");
844
+ }, [])
845
+ });
846
+ const reloadSession = useCallback3(() => {
847
+ debugLog.command("useSessionLifecycle", "reloadSession", { sessionId });
848
+ if (!sessionId) {
849
+ debugLog.error("useSessionLifecycle", "No sessionId, cannot reload");
850
+ return;
851
+ }
852
+ debugLog.command("useSessionLifecycle", "reload_config", { sessionId });
853
+ client.sendCommand({
854
+ type: "reload_config",
855
+ session_id: sessionId
856
+ });
857
+ setInfoMessage("Configuration reloaded!");
858
+ statusRef.current = "ready";
859
+ setStatus("ready");
860
+ debugLog.state("useSessionLifecycle", "reloadSession completed", void 0, { status: "ready" });
861
+ }, [sessionId, client]);
862
+ return {
863
+ sessionId,
864
+ status,
865
+ statusRef,
866
+ infoMessage,
867
+ setInfoMessage,
868
+ setStatus,
869
+ error,
870
+ setError,
871
+ tokenUsage,
872
+ loadedConfig,
873
+ isSetupRequired,
874
+ setIsSetupRequired,
875
+ isProjectPermissionRequired,
876
+ setIsProjectPermissionRequired,
877
+ pendingRepoRoot,
878
+ reloadSession
879
+ };
880
+ }
881
+
882
+ // src/hooks/useResponseStream.ts
883
+ import { useState as useState3, useCallback as useCallback4, useRef as useRef3, useEffect as useEffect3 } from "react";
884
+ function useResponseStream(client, appendAssistantContent, markLastTurnDone) {
885
+ const [currentThought, setCurrentThought] = useState3("");
886
+ const bufferRef = useRef3("");
887
+ const timerRef = useRef3(null);
888
+ const lastUpdateRef = useRef3(0);
889
+ const THROTTLE_MS = 50;
890
+ const flushBuffer = useCallback4((final) => {
891
+ if (timerRef.current) {
892
+ clearTimeout(timerRef.current);
893
+ timerRef.current = null;
894
+ }
895
+ const textToFlush = bufferRef.current;
896
+ bufferRef.current = "";
897
+ lastUpdateRef.current = Date.now();
898
+ if (textToFlush) {
899
+ setCurrentThought((prev) => prev + textToFlush);
900
+ appendAssistantContent(textToFlush, false);
901
+ }
902
+ if (final) {
903
+ setCurrentThought("");
904
+ markLastTurnDone();
905
+ }
906
+ }, [appendAssistantContent, markLastTurnDone]);
907
+ useEffect3(() => {
908
+ return () => {
909
+ if (timerRef.current) clearTimeout(timerRef.current);
910
+ };
911
+ }, []);
912
+ const handleAssistantText = useCallback4(
913
+ (content, replace, final) => {
914
+ if (replace) {
915
+ if (timerRef.current) clearTimeout(timerRef.current);
916
+ timerRef.current = null;
917
+ bufferRef.current = "";
918
+ lastUpdateRef.current = Date.now();
919
+ setCurrentThought(final ? "" : content);
920
+ appendAssistantContent(content, true);
921
+ if (final) markLastTurnDone();
922
+ return;
923
+ }
924
+ bufferRef.current += content;
925
+ const now = Date.now();
926
+ if (final || now - lastUpdateRef.current > THROTTLE_MS) {
927
+ flushBuffer(final);
928
+ } else if (!timerRef.current) {
929
+ timerRef.current = setTimeout(() => flushBuffer(false), THROTTLE_MS);
930
+ }
931
+ },
932
+ [flushBuffer, appendAssistantContent, markLastTurnDone]
933
+ );
934
+ useEngineEvents(client, {
935
+ onAssistantText: handleAssistantText
936
+ });
937
+ return {
938
+ currentThought,
939
+ setCurrentThought
940
+ };
941
+ }
942
+
943
+ // src/hooks/useToolActivities.ts
944
+ import { useState as useState4, useRef as useRef4, useCallback as useCallback5, useEffect as useEffect4 } from "react";
945
+ function useToolActivities(client, conversation) {
946
+ const [projectPlan, setProjectPlan] = useState4("");
947
+ const [showProjectPlan, setShowProjectPlan] = useState4(false);
948
+ const activeStepIdsRef = useRef4(/* @__PURE__ */ new Set());
949
+ const {
950
+ addActivity,
951
+ addTimelineStep,
952
+ updateTimelineStep,
953
+ getCurrentTurnId,
954
+ addContextEvent,
955
+ appendToolOutput
956
+ } = conversation;
957
+ const THROTTLE_MS = 50;
958
+ const activityBufferRef = useRef4([]);
959
+ const activityThrottleTimerRef = useRef4(null);
960
+ const lastActivityFlushRef = useRef4(0);
961
+ const flushActivityBuffer = useCallback5(() => {
962
+ if (activityThrottleTimerRef.current) {
963
+ clearTimeout(activityThrottleTimerRef.current);
964
+ activityThrottleTimerRef.current = null;
965
+ }
966
+ const currentTurnId = getCurrentTurnId();
967
+ if (!currentTurnId) {
968
+ activityBufferRef.current = [];
969
+ return;
970
+ }
971
+ activityBufferRef.current.forEach((activity) => {
972
+ addActivity(currentTurnId, activity);
973
+ if (activity.type === "tool" || activity.type === "reasoning") {
974
+ const stepId = activity.invocationId || activity.id;
975
+ if (activity.status === "active") {
976
+ if (!activeStepIdsRef.current.has(stepId)) {
977
+ addTimelineStep(currentTurnId, {
978
+ id: stepId,
979
+ toolName: activity.tool || "unknown",
980
+ label: activity.tool || "unknown",
981
+ status: "running",
982
+ startedAt: activity.timestamp,
983
+ type: "tool",
984
+ command: activity.command,
985
+ metadata: activity.metadata
986
+ });
987
+ activeStepIdsRef.current.add(stepId);
988
+ } else {
989
+ updateTimelineStep(currentTurnId, stepId, {
990
+ metadata: activity.metadata,
991
+ command: activity.command || void 0
992
+ });
993
+ }
994
+ } else {
995
+ updateTimelineStep(currentTurnId, stepId, {
996
+ status: activity.status === "completed" ? "done" : "failed",
997
+ finishedAt: activity.timestamp,
998
+ metadata: activity.metadata,
999
+ command: activity.command || void 0,
1000
+ durationMs: activity.durationMs
1001
+ });
1002
+ activeStepIdsRef.current.delete(stepId);
1003
+ }
1004
+ }
1005
+ });
1006
+ activityBufferRef.current = [];
1007
+ lastActivityFlushRef.current = Date.now();
1008
+ }, [addActivity, addTimelineStep, updateTimelineStep, getCurrentTurnId]);
1009
+ const outputBufferRef = useRef4(/* @__PURE__ */ new Map());
1010
+ const throttleTimerRef = useRef4(null);
1011
+ const lastFlushRef = useRef4(0);
1012
+ const flushOutputBuffer = useCallback5(() => {
1013
+ if (throttleTimerRef.current) {
1014
+ clearTimeout(throttleTimerRef.current);
1015
+ throttleTimerRef.current = null;
1016
+ }
1017
+ const turnId = getCurrentTurnId();
1018
+ if (!turnId) return;
1019
+ outputBufferRef.current.forEach((data, key) => {
1020
+ const [invocationId] = key.split(":");
1021
+ appendToolOutput(turnId, invocationId, data.output, data.stream);
1022
+ });
1023
+ outputBufferRef.current.clear();
1024
+ lastFlushRef.current = Date.now();
1025
+ }, [getCurrentTurnId, appendToolOutput]);
1026
+ useEffect4(() => {
1027
+ return () => {
1028
+ if (throttleTimerRef.current) clearTimeout(throttleTimerRef.current);
1029
+ if (activityThrottleTimerRef.current) clearTimeout(activityThrottleTimerRef.current);
1030
+ };
1031
+ }, []);
1032
+ useEngineEvents(client, {
1033
+ onActivity: useCallback5((activity) => {
1034
+ activityBufferRef.current.push(activity);
1035
+ const now = Date.now();
1036
+ if (now - lastActivityFlushRef.current > THROTTLE_MS) {
1037
+ flushActivityBuffer();
1038
+ } else if (!activityThrottleTimerRef.current) {
1039
+ activityThrottleTimerRef.current = setTimeout(flushActivityBuffer, THROTTLE_MS);
1040
+ }
1041
+ }, [flushActivityBuffer]),
1042
+ onProjectPlan: useCallback5((content, source) => {
1043
+ setProjectPlan(content);
1044
+ if (content && !showProjectPlan) {
1045
+ setShowProjectPlan(true);
1046
+ }
1047
+ }, [showProjectPlan]),
1048
+ onContext: useCallback5((event) => {
1049
+ const currentTurnId = getCurrentTurnId();
1050
+ if (currentTurnId) {
1051
+ addContextEvent(currentTurnId, event);
1052
+ }
1053
+ }, [getCurrentTurnId, addContextEvent]),
1054
+ onToolOutput: useCallback5((invocationId, tool, output, stream) => {
1055
+ const key = `${invocationId}:${stream}`;
1056
+ const existing = outputBufferRef.current.get(key);
1057
+ if (existing) {
1058
+ existing.output += output;
1059
+ } else {
1060
+ outputBufferRef.current.set(key, { output, stream, tool });
1061
+ }
1062
+ const now = Date.now();
1063
+ if (now - lastFlushRef.current > THROTTLE_MS) {
1064
+ flushOutputBuffer();
1065
+ } else if (!throttleTimerRef.current) {
1066
+ throttleTimerRef.current = setTimeout(flushOutputBuffer, THROTTLE_MS);
1067
+ }
1068
+ }, [flushOutputBuffer])
1069
+ });
1070
+ return {
1071
+ projectPlan,
1072
+ showProjectPlan,
1073
+ setShowProjectPlan,
1074
+ activeStepIdsRef
1075
+ // Exported if needed by parent, e.g. to clear on new turn
1076
+ };
1077
+ }
1078
+
1079
+ // src/hooks/useEngineConnection.ts
1080
+ function useEngineConnection(client, repoPath, requestedSessionId, engineExited, skipConnection = false) {
1081
+ const { exit } = useApp2();
1082
+ const [errorCount, setErrorCount] = useState5(0);
1083
+ const {
1084
+ turns,
1085
+ pushTurn,
1086
+ appendAssistantContent,
1087
+ markLastTurnDone,
1088
+ addActivity,
1089
+ updateActivity,
1090
+ toggleTurnCollapsed,
1091
+ addTimelineStep,
1092
+ updateTimelineStep,
1093
+ getCurrentTurnId,
1094
+ appendLogLine,
1095
+ appendToolOutput,
1096
+ addContextEvent,
1097
+ clearTurns
1098
+ } = useConversation();
1099
+ const {
1100
+ sessionId,
1101
+ status,
1102
+ statusRef,
1103
+ infoMessage,
1104
+ setInfoMessage,
1105
+ setStatus,
1106
+ error: lifecycleError,
1107
+ setError: setLifecycleError,
1108
+ tokenUsage,
1109
+ loadedConfig,
1110
+ isSetupRequired,
1111
+ setIsSetupRequired,
1112
+ isProjectPermissionRequired,
1113
+ setIsProjectPermissionRequired,
1114
+ pendingRepoRoot,
1115
+ reloadSession
1116
+ } = useSessionLifecycle(client, repoPath, requestedSessionId, engineExited, skipConnection);
1117
+ const {
1118
+ currentThought,
1119
+ setCurrentThought
1120
+ } = useResponseStream(client, appendAssistantContent, markLastTurnDone);
1121
+ const {
1122
+ projectPlan,
1123
+ showProjectPlan,
1124
+ activeStepIdsRef
1125
+ } = useToolActivities(client, {
1126
+ addActivity,
1127
+ addTimelineStep,
1128
+ updateTimelineStep,
1129
+ getCurrentTurnId,
1130
+ addContextEvent,
1131
+ appendToolOutput
1132
+ });
1133
+ useEngineEvents(client, {
1134
+ onSessionHistory: useCallback6((title, summary, _messages) => {
1135
+ if (summary) {
1136
+ const resumeTurnId = `resume-${Date.now()}`;
1137
+ pushTurn(`Resumed: ${title}`);
1138
+ const timer = setTimeout(() => {
1139
+ markLastTurnDone(summary);
1140
+ }, 100);
1141
+ return () => clearTimeout(timer);
1142
+ }
1143
+ }, [pushTurn, markLastTurnDone])
1144
+ });
1145
+ const submitQuery = useCallback6((query) => {
1146
+ if (!sessionId) return;
1147
+ logger.state("User Query Submitted", { query, sessionId });
1148
+ pushTurn(query);
1149
+ activeStepIdsRef.current.clear();
1150
+ setStatus("thinking");
1151
+ setLifecycleError(void 0);
1152
+ client.sendUserMessage(sessionId, query);
1153
+ }, [sessionId, pushTurn, client, setStatus, setLifecycleError]);
1154
+ const isRunning = status === "thinking" || status === "running_tools";
1155
+ const cancelRequest = useCallback6(() => {
1156
+ logger.log(`[useEngineConnection] cancelRequest called. sessionId=${sessionId}, status=${status}, isRunning=${isRunning}`);
1157
+ if (!sessionId) {
1158
+ logger.error("[useEngineConnection] No sessionId, cannot cancel");
1159
+ return false;
1160
+ }
1161
+ if (!isRunning) {
1162
+ logger.log("[useEngineConnection] Cancel skipped - not running");
1163
+ return false;
1164
+ }
1165
+ logger.log("[useEngineConnection] Sending cancel_request");
1166
+ client.sendCommand({
1167
+ type: "cancel_request",
1168
+ session_id: sessionId
1169
+ });
1170
+ setInfoMessage("Stopping...");
1171
+ return true;
1172
+ }, [sessionId, isRunning, status, client, setInfoMessage]);
1173
+ const { currentTimelineSteps, currentLogLines, currentRunningStep } = useMemo2(() => {
1174
+ const currentTurn = turns.length > 0 ? turns[turns.length - 1] : null;
1175
+ const timelineSteps = currentTurn?.timelineSteps || [];
1176
+ const logLines = currentTurn?.logLines || [];
1177
+ const runningStep = timelineSteps.find((s) => s.status === "running");
1178
+ return {
1179
+ currentTimelineSteps: timelineSteps,
1180
+ currentLogLines: logLines,
1181
+ currentRunningStep: runningStep
1182
+ };
1183
+ }, [turns]);
1184
+ return {
1185
+ sessionId,
1186
+ status,
1187
+ infoMessage,
1188
+ isRunning,
1189
+ error: lifecycleError,
1190
+ tokenUsage,
1191
+ projectPlan,
1192
+ showProjectPlan,
1193
+ errorCount,
1194
+ currentThought,
1195
+ turns,
1196
+ currentTimelineSteps,
1197
+ currentRunningStepId: currentRunningStep?.id,
1198
+ toggleTurnCollapsed,
1199
+ submitQuery,
1200
+ setInput: () => {
1201
+ },
1202
+ sendCommand: client.sendCommand.bind(client),
1203
+ isSetupRequired,
1204
+ setIsSetupRequired,
1205
+ isProjectPermissionRequired,
1206
+ setIsProjectPermissionRequired,
1207
+ pendingRepoRoot,
1208
+ loadedConfig,
1209
+ reloadSession,
1210
+ cancelRequest,
1211
+ clearTurns
1212
+ };
1213
+ }
1214
+
1215
+ // src/components/layout/AppLayout.tsx
1216
+ import React7, { useRef as useRef7 } from "react";
1217
+ import { Box as Box15, Text as Text16, useApp as useApp3, useInput as useInput2 } from "ink";
1218
+
1219
+ // src/components/layout/Header.tsx
1220
+ import { Box, Text } from "ink";
1221
+ import { createRequire } from "module";
1222
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
1223
+ var require2 = createRequire(import.meta.url);
1224
+ var pkg = require2("../../../package.json");
1225
+ var Header = () => {
1226
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, marginBottom: 1, children: [
1227
+ /* @__PURE__ */ jsx2(Box, { flexDirection: "row", alignItems: "center", children: /* @__PURE__ */ jsx2(Box, { paddingY: 1, marginRight: 4, children: /* @__PURE__ */ jsx2(Text, { color: "cyan", bold: true, children: `
1228
+ \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588\u2588\u2588
1229
+ \u2591\u2591\u2588\u2588\u2588\u2591\u2591\u2591\u2591\u2588\u2588\u2588 \u2588\u2588\u2588\u2591\u2591\u2591\u2591\u2591\u2588\u2588\u2588 \u2591\u2591\u2588\u2588\u2588\u2591\u2591\u2591\u2591\u2588\u2588\u2588 \u2588\u2588\u2588\u2591\u2591\u2591\u2591\u2591\u2588\u2588\u2588
1230
+ \u2591\u2588\u2588\u2588 \u2591\u2591\u2588\u2588\u2588 \u2588\u2588\u2588 \u2591\u2591\u2588\u2588\u2588 \u2591\u2588\u2588\u2588 \u2591\u2591\u2588\u2588\u2588 \u2588\u2588\u2588 \u2591\u2591\u2588\u2588\u2588
1231
+ \u2591\u2588\u2588\u2588 \u2591\u2588\u2588\u2588\u2591\u2588\u2588\u2588 \u2591\u2588\u2588\u2588 \u2591\u2588\u2588\u2588 \u2591\u2588\u2588\u2588\u2591\u2588\u2588\u2588 \u2591\u2588\u2588\u2588
1232
+ \u2591\u2588\u2588\u2588 \u2591\u2588\u2588\u2588\u2591\u2588\u2588\u2588 \u2591\u2588\u2588\u2588 \u2591\u2588\u2588\u2588 \u2591\u2588\u2588\u2588\u2591\u2588\u2588\u2588 \u2591\u2588\u2588\u2588
1233
+ \u2591\u2588\u2588\u2588 \u2588\u2588\u2588 \u2591\u2591\u2588\u2588\u2588 \u2588\u2588\u2588 \u2591\u2588\u2588\u2588 \u2588\u2588\u2588 \u2591\u2591\u2588\u2588\u2588 \u2588\u2588\u2588
1234
+ \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2591\u2591\u2591\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2591 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2591\u2591\u2591\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2591
1235
+ \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 \u2591\u2591\u2591\u2591\u2591\u2591\u2591 \u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591 \u2591\u2591\u2591\u2591\u2591\u2591\u2591 v${pkg.version}
1236
+ ` }) }) }),
1237
+ /* @__PURE__ */ jsx2(Text, { color: "yellow", italic: true, children: "The Agentic AI Coding Assistant" })
1238
+ ] });
1239
+ };
1240
+
1241
+ // src/components/layout/Footer.tsx
1242
+ import { Box as Box4, Text as Text5 } from "ink";
1243
+
1244
+ // src/components/common/Input.tsx
1245
+ import { useState as useState6, useEffect as useEffect6, useRef as useRef6 } from "react";
1246
+ import { Text as Text2, Box as Box2, useInput } from "ink";
1247
+ import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
1248
+ var Input = ({
1249
+ value,
1250
+ onChange,
1251
+ onSubmit,
1252
+ placeholder = "",
1253
+ isDisabled = false,
1254
+ onHistoryUp,
1255
+ onHistoryDown
1256
+ }) => {
1257
+ const [cursorPos, setCursorPos] = useState6(value.length);
1258
+ useEffect6(() => {
1259
+ if (cursorPos > value.length) {
1260
+ setCursorPos(value.length);
1261
+ }
1262
+ }, [value, cursorPos]);
1263
+ const stateRef = useRef6({ value, cursorPos, onChange, onSubmit, isDisabled, onHistoryUp, onHistoryDown });
1264
+ stateRef.current = { value, cursorPos, onChange, onSubmit, isDisabled, onHistoryUp, onHistoryDown };
1265
+ useInput((input, key) => {
1266
+ const { value: value2, cursorPos: cursorPos2, onChange: onChange2, onSubmit: onSubmit2, isDisabled: isDisabled2, onHistoryUp: onHistoryUp2, onHistoryDown: onHistoryDown2 } = stateRef.current;
1267
+ if (isDisabled2) return;
1268
+ if (key.return) {
1269
+ if (key.shift) {
1270
+ const newValue = value2.slice(0, cursorPos2) + "\n" + value2.slice(cursorPos2);
1271
+ onChange2(newValue);
1272
+ setCursorPos((p) => p + 1);
1273
+ return;
1274
+ }
1275
+ onSubmit2(value2);
1276
+ return;
1277
+ }
1278
+ if (key.leftArrow) {
1279
+ setCursorPos((p) => Math.max(0, p - 1));
1280
+ return;
1281
+ }
1282
+ if (key.rightArrow) {
1283
+ setCursorPos((p) => Math.min(value2.length, p + 1));
1284
+ return;
1285
+ }
1286
+ if (key.upArrow) {
1287
+ onHistoryUp2?.();
1288
+ return;
1289
+ }
1290
+ if (key.downArrow) {
1291
+ onHistoryDown2?.();
1292
+ return;
1293
+ }
1294
+ if (key.backspace || key.delete || input === "\x7F" || input === "\b" || key.ctrl && input === "h") {
1295
+ if (cursorPos2 > 0) {
1296
+ const newValue = value2.slice(0, cursorPos2 - 1) + value2.slice(cursorPos2);
1297
+ onChange2(newValue);
1298
+ setCursorPos((p) => Math.max(0, p - 1));
1299
+ }
1300
+ return;
1301
+ }
1302
+ if (input === "\x1B[H" || input === "\x1BOH") {
1303
+ setCursorPos(0);
1304
+ return;
1305
+ }
1306
+ if (input === "\x1B[F" || input === "\x1BOF") {
1307
+ setCursorPos(value2.length);
1308
+ return;
1309
+ }
1310
+ if (input === "\x1B[3~" || key.ctrl && input === "d") {
1311
+ if (cursorPos2 < value2.length) {
1312
+ const newValue = value2.slice(0, cursorPos2) + value2.slice(cursorPos2 + 1);
1313
+ onChange2(newValue);
1314
+ }
1315
+ return;
1316
+ }
1317
+ if (input && !key.ctrl && !key.meta) {
1318
+ if (input.includes("\n") || input.includes("\r")) {
1319
+ const normalizedInput = input.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1320
+ const newValue = value2.slice(0, cursorPos2) + normalizedInput + value2.slice(cursorPos2);
1321
+ onChange2(newValue);
1322
+ setCursorPos((p) => p + normalizedInput.length);
1323
+ return;
1324
+ }
1325
+ const charCode = input.charCodeAt(0);
1326
+ if (input.length === 1 && (charCode >= 32 && charCode <= 126 || // Printable ASCII
1327
+ charCode === 9 || // Tab
1328
+ charCode > 127)) {
1329
+ const newValue = value2.slice(0, cursorPos2) + input + value2.slice(cursorPos2);
1330
+ onChange2(newValue);
1331
+ setCursorPos((p) => p + 1);
1332
+ }
1333
+ }
1334
+ }, { isActive: !isDisabled });
1335
+ if (!value && placeholder) {
1336
+ return /* @__PURE__ */ jsx3(Text2, { color: "gray", children: placeholder });
1337
+ }
1338
+ const lines = value.split("\n");
1339
+ let currentPos = 0;
1340
+ return /* @__PURE__ */ jsx3(Box2, { flexDirection: "column", children: lines.map((line, lineIdx) => {
1341
+ const isLastLine = lineIdx === lines.length - 1;
1342
+ const lineStart = currentPos;
1343
+ const lineEnd = lineStart + line.length;
1344
+ const element = /* @__PURE__ */ jsx3(Text2, { children: cursorPos >= lineStart && cursorPos <= lineEnd ? /* @__PURE__ */ jsxs2(Fragment, { children: [
1345
+ /* @__PURE__ */ jsx3(Text2, { color: "green", children: line.slice(0, cursorPos - lineStart) }),
1346
+ /* @__PURE__ */ jsx3(Text2, { inverse: true, color: "green", children: value[cursorPos] === "\n" ? "\u21B5" : line[cursorPos - lineStart] || " " }),
1347
+ /* @__PURE__ */ jsx3(Text2, { color: "green", children: line.slice(cursorPos - lineStart + 1) })
1348
+ ] }) : /* @__PURE__ */ jsx3(Text2, { color: "green", children: line }) }, lineIdx);
1349
+ currentPos = lineEnd + 1;
1350
+ return element;
1351
+ }) });
1352
+ };
1353
+
1354
+ // src/components/common/StatusBadge.tsx
1355
+ import { Box as Box3, Text as Text4 } from "ink";
1356
+
1357
+ // src/components/common/Spinner.tsx
1358
+ import { Text as Text3 } from "ink";
1359
+
1360
+ // src/contexts/AnimationContext.tsx
1361
+ import { createContext as createContext2, useContext as useContext3, useState as useState7, useEffect as useEffect7 } from "react";
1362
+ import { jsx as jsx4 } from "react/jsx-runtime";
1363
+ var AnimationContext = createContext2(void 0);
1364
+ var useAnimation = () => {
1365
+ const context = useContext3(AnimationContext);
1366
+ if (!context) {
1367
+ throw new Error("useAnimation must be used within an AnimationProvider");
1368
+ }
1369
+ return context;
1370
+ };
1371
+ var AnimationProvider = ({ children, interval = 80 }) => {
1372
+ const [frameIndex, setFrameIndex] = useState7(0);
1373
+ useEffect7(() => {
1374
+ const timer = setInterval(() => {
1375
+ setFrameIndex((prev) => (prev + 1) % 1e3);
1376
+ }, interval);
1377
+ return () => clearInterval(timer);
1378
+ }, [interval]);
1379
+ return /* @__PURE__ */ jsx4(AnimationContext.Provider, { value: { frameIndex }, children });
1380
+ };
1381
+
1382
+ // src/components/common/Spinner.tsx
1383
+ import { jsx as jsx5 } from "react/jsx-runtime";
1384
+ var SPINNER_FRAMES = {
1385
+ dots: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"]
1386
+ };
1387
+ var Spinner = ({ type = "dots", color = "yellow" }) => {
1388
+ const { frameIndex } = useAnimation();
1389
+ const frames = SPINNER_FRAMES[type];
1390
+ const frame = frames[frameIndex % frames.length];
1391
+ return /* @__PURE__ */ jsx5(Text3, { color, children: frame });
1392
+ };
1393
+
1394
+ // src/components/common/StatusBadge.tsx
1395
+ import { jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
1396
+ var StatusBadge = ({ status, message }) => {
1397
+ let color;
1398
+ let label;
1399
+ let showSpinner = false;
1400
+ switch (status) {
1401
+ case "pending":
1402
+ case "booting":
1403
+ case "connecting":
1404
+ color = "gray";
1405
+ label = status.toUpperCase();
1406
+ break;
1407
+ case "ready":
1408
+ color = "green";
1409
+ label = "READY";
1410
+ break;
1411
+ case "running":
1412
+ case "thinking":
1413
+ case "running_tools":
1414
+ color = "yellow";
1415
+ label = "RUNNING";
1416
+ showSpinner = true;
1417
+ break;
1418
+ case "done":
1419
+ color = "green";
1420
+ label = "DONE";
1421
+ break;
1422
+ case "failed":
1423
+ case "error":
1424
+ case "disconnected":
1425
+ color = "red";
1426
+ label = status === "error" ? "ERROR" : status.toUpperCase();
1427
+ break;
1428
+ default:
1429
+ color = "gray";
1430
+ label = "UNKNOWN";
1431
+ }
1432
+ return /* @__PURE__ */ jsxs3(Box3, { children: [
1433
+ showSpinner && /* @__PURE__ */ jsx6(Box3, { marginRight: 1, children: /* @__PURE__ */ jsx6(Spinner, {}) }),
1434
+ /* @__PURE__ */ jsx6(Text4, { color, bold: true, children: label }),
1435
+ message && /* @__PURE__ */ jsxs3(Text4, { color: "gray", children: [
1436
+ " ",
1437
+ message
1438
+ ] })
1439
+ ] });
1440
+ };
1441
+
1442
+ // src/contexts/SessionContext.tsx
1443
+ import { createContext as createContext3, useContext as useContext4 } from "react";
1444
+ import { jsx as jsx7 } from "react/jsx-runtime";
1445
+ var SessionContext = createContext3(void 0);
1446
+ function SessionProvider({
1447
+ children,
1448
+ value
1449
+ }) {
1450
+ return /* @__PURE__ */ jsx7(SessionContext.Provider, { value, children });
1451
+ }
1452
+ function useSessionContext() {
1453
+ const context = useContext4(SessionContext);
1454
+ if (!context) {
1455
+ throw new Error("useSessionContext must be used within a SessionProvider");
1456
+ }
1457
+ return context;
1458
+ }
1459
+
1460
+ // src/components/layout/Footer.tsx
1461
+ import { jsx as jsx8, jsxs as jsxs4 } from "react/jsx-runtime";
1462
+ var Footer = ({
1463
+ input,
1464
+ onChange,
1465
+ onSubmit,
1466
+ onHistoryUp,
1467
+ onHistoryDown,
1468
+ repoLabel
1469
+ }) => {
1470
+ const {
1471
+ sessionId: sessionLabel,
1472
+ status,
1473
+ infoMessage,
1474
+ isRunning,
1475
+ tokenUsage,
1476
+ errorCount,
1477
+ currentThought,
1478
+ loadedConfig
1479
+ } = useSessionContext();
1480
+ const canSubmit = !isRunning && status !== "connecting" && status !== "disconnected";
1481
+ const modelLabel = loadedConfig?.model;
1482
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", borderStyle: "round", paddingX: 1, flexGrow: 1, children: [
1483
+ /* @__PURE__ */ jsx8(Box4, { children: /* @__PURE__ */ jsxs4(Text5, { color: "gray", children: [
1484
+ "Tokens:",
1485
+ " ",
1486
+ tokenUsage ? /* @__PURE__ */ jsxs4(Text5, { color: tokenUsage.percentage > 80 ? "red" : tokenUsage.percentage > 50 ? "yellow" : "green", children: [
1487
+ "Context: ",
1488
+ tokenUsage.used,
1489
+ " / ",
1490
+ tokenUsage.total,
1491
+ " (",
1492
+ tokenUsage.percentage.toFixed(2),
1493
+ "%)",
1494
+ tokenUsage.sessionTotal !== void 0 && ` | Session: ${tokenUsage.sessionTotal}`
1495
+ ] }) : "\u2014",
1496
+ isRunning && /* @__PURE__ */ jsx8(Text5, { color: "yellow", children: " \u2502 ESC to stop" })
1497
+ ] }) }),
1498
+ /* @__PURE__ */ jsx8(Box4, { children: currentThought ? /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginBottom: 0, children: [
1499
+ /* @__PURE__ */ jsx8(Box4, { children: /* @__PURE__ */ jsxs4(Text5, { color: "gray", dimColor: true, children: [
1500
+ /* @__PURE__ */ jsx8(Spinner, {}),
1501
+ " Thinking..."
1502
+ ] }) }),
1503
+ /* @__PURE__ */ jsxs4(Box4, { children: [
1504
+ /* @__PURE__ */ jsx8(Text5, { color: "cyan", children: "> " }),
1505
+ canSubmit ? /* @__PURE__ */ jsx8(
1506
+ Input,
1507
+ {
1508
+ value: input,
1509
+ onChange,
1510
+ onSubmit,
1511
+ placeholder: "Describe your task, or type /help...",
1512
+ onHistoryUp,
1513
+ onHistoryDown
1514
+ }
1515
+ ) : /* @__PURE__ */ jsxs4(Text5, { color: "gray", children: [
1516
+ "Processing... ",
1517
+ /* @__PURE__ */ jsx8(Text5, { color: "yellow", children: "(ESC to stop)" })
1518
+ ] })
1519
+ ] })
1520
+ ] }) : /* @__PURE__ */ jsxs4(Box4, { children: [
1521
+ /* @__PURE__ */ jsx8(Text5, { color: "cyan", children: "> " }),
1522
+ canSubmit ? /* @__PURE__ */ jsx8(
1523
+ Input,
1524
+ {
1525
+ value: input,
1526
+ onChange,
1527
+ onSubmit,
1528
+ placeholder: "Describe your task, or type /help...",
1529
+ onHistoryUp,
1530
+ onHistoryDown
1531
+ }
1532
+ ) : /* @__PURE__ */ jsxs4(Box4, { children: [
1533
+ /* @__PURE__ */ jsx8(Text5, { color: "yellow", children: /* @__PURE__ */ jsx8(Spinner, {}) }),
1534
+ /* @__PURE__ */ jsxs4(Text5, { color: "gray", children: [
1535
+ " ",
1536
+ isRunning ? "Processing... " : "Connecting to engine..."
1537
+ ] }),
1538
+ isRunning && /* @__PURE__ */ jsx8(Text5, { color: "yellow", children: "(ESC to stop)" })
1539
+ ] })
1540
+ ] }) }),
1541
+ /* @__PURE__ */ jsxs4(Box4, { borderStyle: "single", borderTop: true, borderBottom: false, borderLeft: false, borderRight: false, borderColor: "gray", marginTop: 0, children: [
1542
+ /* @__PURE__ */ jsx8(Box4, { flexGrow: 1, children: /* @__PURE__ */ jsxs4(Text5, { children: [
1543
+ "Repo: ",
1544
+ /* @__PURE__ */ jsx8(Text5, { color: "blue", children: repoLabel }),
1545
+ " \u2502 Session: ",
1546
+ /* @__PURE__ */ jsx8(Text5, { color: "magenta", children: sessionLabel }),
1547
+ modelLabel && /* @__PURE__ */ jsxs4(Text5, { children: [
1548
+ " \u2502 Model: ",
1549
+ /* @__PURE__ */ jsx8(Text5, { color: "green", children: modelLabel })
1550
+ ] })
1551
+ ] }) }),
1552
+ /* @__PURE__ */ jsxs4(Box4, { children: [
1553
+ errorCount !== void 0 && errorCount > 0 && /* @__PURE__ */ jsxs4(Text5, { color: "red", children: [
1554
+ "Errors: ",
1555
+ errorCount,
1556
+ " \u2502 "
1557
+ ] }),
1558
+ /* @__PURE__ */ jsx8(StatusBadge, { status, message: infoMessage })
1559
+ ] })
1560
+ ] })
1561
+ ] });
1562
+ };
1563
+
1564
+ // src/components/conversation/Conversation.tsx
1565
+ import React6, { useMemo as useMemo3 } from "react";
1566
+ import { Box as Box14, Text as Text15 } from "ink";
1567
+
1568
+ // src/config.ts
1569
+ var UI_CONFIG = {
1570
+ // Layout settings
1571
+ LAYOUT: {
1572
+ STATUS_BAR_HEIGHT: 1
1573
+ },
1574
+ // Turn settings
1575
+ TURN: {
1576
+ HEADER_COLOR_USER: "gray",
1577
+ HEADER_COLOR_ASSISTANT: "green",
1578
+ SUMMARY_PREFIX: "\u{1F4DD} "
1579
+ },
1580
+ // Conversation display
1581
+ CONVERSATION: {
1582
+ // Tools to hide from timeline (internal/noisy tools)
1583
+ HIDDEN_TOOLS: ["think"],
1584
+ // Prefixes for compact conversation display
1585
+ PREFIXES: {
1586
+ USER: ">",
1587
+ ASSISTANT: "$"
1588
+ },
1589
+ // Whether to show reasoning steps (think, internal reasoning)
1590
+ SHOW_REASONING: false
1591
+ },
1592
+ // Tool Component Layouts (Height in lines)
1593
+ TOOLS: {
1594
+ COMMON: {
1595
+ MARGIN_Y: 1
1596
+ // Spacing between steps
1597
+ },
1598
+ RUN_CMD: {
1599
+ HEADER_HEIGHT: 4,
1600
+ // Header box (Border + Content + Padding + Margin)
1601
+ BORDER_HEIGHT: 2,
1602
+ // Top/Bottom borders
1603
+ MARGIN_INNER: 1,
1604
+ // Padding
1605
+ FOOTER_HEIGHT: 4,
1606
+ // Exit code box (Margin + Border + Padding + Content)
1607
+ MAX_OUTPUT_LINES: 20
1608
+ },
1609
+ THINK: {
1610
+ HEADER_HEIGHT: 1,
1611
+ BORDER_HEIGHT: 2,
1612
+ MARGIN_INNER: 1
1613
+ },
1614
+ READ_FILE: {
1615
+ HEADER_HEIGHT: 4,
1616
+ // Header box (Border + Content + Padding + Margin)
1617
+ BORDER_HEIGHT: 2,
1618
+ MARGIN_INNER: 1,
1619
+ MAX_CONTENT_LINES: 30
1620
+ },
1621
+ RESPOND: {
1622
+ HEADER_HEIGHT: 0,
1623
+ BORDER_HEIGHT: 0,
1624
+ MARGIN_INNER: 0
1625
+ },
1626
+ CONTEXT: {
1627
+ HEIGHT: 1
1628
+ }
1629
+ }
1630
+ };
1631
+
1632
+ // src/utils/flatten.ts
1633
+ function flattenTurn(turn, steps, currentRunningStepId) {
1634
+ const items = [];
1635
+ const { PREFIXES, HIDDEN_TOOLS, SHOW_REASONING } = UI_CONFIG.CONVERSATION;
1636
+ items.push({
1637
+ type: "user_message",
1638
+ id: `turn-${turn.id}-user`,
1639
+ prefix: PREFIXES.USER,
1640
+ text: turn.user
1641
+ });
1642
+ if (turn.collapsed) {
1643
+ if (turn.summary) {
1644
+ items.push({
1645
+ type: "text_chunk",
1646
+ id: `turn-${turn.id}-summary`,
1647
+ text: `\u{1F4DD} ${turn.summary}`,
1648
+ color: "gray"
1649
+ });
1650
+ }
1651
+ const activeCount = steps.filter((s) => s.status === "running").length;
1652
+ const completedCount = steps.filter((s) => s.status === "done").length;
1653
+ if (steps.length > 0) {
1654
+ items.push({
1655
+ type: "text_chunk",
1656
+ id: `turn-${turn.id}-status`,
1657
+ text: `${activeCount > 0 ? `\u23F3 ${activeCount} running \u2022 ` : ""}\u2705 ${completedCount} completed`,
1658
+ color: "gray"
1659
+ });
1660
+ }
1661
+ } else {
1662
+ if (turn.assistant) {
1663
+ items.push({
1664
+ type: "assistant_message",
1665
+ id: `turn-${turn.id}-assistant`,
1666
+ prefix: PREFIXES.ASSISTANT,
1667
+ text: turn.assistant
1668
+ });
1669
+ } else if (!turn.done) {
1670
+ items.push({
1671
+ type: "assistant_message",
1672
+ id: `turn-${turn.id}-assistant-placeholder`,
1673
+ prefix: PREFIXES.ASSISTANT,
1674
+ text: "..."
1675
+ });
1676
+ }
1677
+ const visibleSteps = steps.filter((step) => {
1678
+ if (step.toolName && HIDDEN_TOOLS.includes(step.toolName)) return false;
1679
+ const isReasoning = (!step.toolName || step.toolName === "") && step.metadata?.content;
1680
+ const isThinking = step.toolName === "think" && step.metadata?.reasoning;
1681
+ if (!SHOW_REASONING && (isReasoning || isThinking)) return false;
1682
+ return true;
1683
+ });
1684
+ visibleSteps.forEach((step) => {
1685
+ items.push({
1686
+ type: "timeline_step",
1687
+ id: `step-${step.id}`,
1688
+ step,
1689
+ turnId: turn.id
1690
+ });
1691
+ if (step.toolName === "edit") {
1692
+ const matchingEdit = turn.activities?.find(
1693
+ (a) => a.type === "edit" && a.codeChange && (a.invocationId === step.id || step.metadata?.path && a.codeChange.file === step.metadata.path)
1694
+ );
1695
+ if (matchingEdit?.codeChange) {
1696
+ items.push({
1697
+ type: "code_change",
1698
+ id: `edit-inline-${matchingEdit.id}`,
1699
+ codeChange: matchingEdit.codeChange
1700
+ });
1701
+ }
1702
+ }
1703
+ });
1704
+ if (visibleSteps.length > 0) {
1705
+ items.push({ type: "divider", id: `turn-${turn.id}-divider-steps` });
1706
+ }
1707
+ if (turn.summary) {
1708
+ items.push({
1709
+ type: "text_chunk",
1710
+ id: `turn-${turn.id}-summary-footer`,
1711
+ text: turn.summary,
1712
+ color: "gray"
1713
+ });
1714
+ }
1715
+ }
1716
+ items.push({ type: "divider", id: `turn-${turn.id}-end-spacing` });
1717
+ return items;
1718
+ }
1719
+
1720
+ // src/components/timeline/TimelineItem.tsx
1721
+ import React5 from "react";
1722
+ import { Box as Box11, Text as Text12 } from "ink";
1723
+
1724
+ // src/utils/toolConfig.ts
1725
+ var TOOL_CONFIGS = {
1726
+ // Execution tools
1727
+ run_cmd: {
1728
+ icon: "",
1729
+ label: (m) => {
1730
+ const cmd = m.command || m.cmd || "";
1731
+ if (cmd.length > 50) {
1732
+ return `run: ${cmd.substring(0, 47)}...`;
1733
+ }
1734
+ return cmd ? `run: ${cmd}` : "Run command";
1735
+ },
1736
+ color: "cyan",
1737
+ category: "execution"
1738
+ },
1739
+ run_tests: {
1740
+ icon: "",
1741
+ label: () => "Run tests",
1742
+ color: "green",
1743
+ category: "execution"
1744
+ },
1745
+ run_build: {
1746
+ icon: "",
1747
+ label: () => "Build",
1748
+ color: "yellow",
1749
+ category: "execution"
1750
+ },
1751
+ run_terminal_cmd: {
1752
+ icon: "",
1753
+ label: (m) => {
1754
+ const cmd = m.command || "";
1755
+ if (cmd.length > 50) {
1756
+ return `run: ${cmd.substring(0, 47)}...`;
1757
+ }
1758
+ return cmd ? `run: ${cmd}` : "Run terminal command";
1759
+ },
1760
+ color: "cyan",
1761
+ category: "execution"
1762
+ },
1763
+ // Editing tools
1764
+ search_replace: {
1765
+ icon: "",
1766
+ label: (m) => {
1767
+ const file = m.file || m.file_path || "";
1768
+ return file ? `Edit ${file}` : "Edit file";
1769
+ },
1770
+ color: "blue",
1771
+ category: "editing"
1772
+ },
1773
+ write: {
1774
+ icon: "",
1775
+ label: (m) => {
1776
+ const file = m.file || m.file_path || "";
1777
+ return file ? `Write ${file}` : "Write file";
1778
+ },
1779
+ color: "blue",
1780
+ category: "editing"
1781
+ },
1782
+ write_file: {
1783
+ icon: "",
1784
+ label: (m) => {
1785
+ const file = m.file || m.path || "";
1786
+ return file ? `Write ${file}` : "Write file";
1787
+ },
1788
+ color: "blue",
1789
+ category: "editing"
1790
+ },
1791
+ propose_diff: {
1792
+ icon: "",
1793
+ label: (m) => {
1794
+ const file = m.file || m.file_path || "";
1795
+ return file ? `Propose diff for ${file}` : "Propose diff";
1796
+ },
1797
+ color: "blue",
1798
+ category: "editing"
1799
+ },
1800
+ // Search tools
1801
+ codebase_search: {
1802
+ icon: "",
1803
+ label: (m) => {
1804
+ const query = m.query || "";
1805
+ if (query.length > 40) {
1806
+ return `Search: ${query.substring(0, 37)}...`;
1807
+ }
1808
+ return query ? `Search: ${query}` : "Codebase search";
1809
+ },
1810
+ color: "magenta",
1811
+ category: "search"
1812
+ },
1813
+ grep: {
1814
+ icon: "",
1815
+ label: (m) => {
1816
+ const pattern = m.pattern || "";
1817
+ if (pattern.length > 40) {
1818
+ return `Grep: ${pattern.substring(0, 37)}...`;
1819
+ }
1820
+ return pattern ? `Grep: ${pattern}` : "Grep";
1821
+ },
1822
+ color: "magenta",
1823
+ category: "search"
1824
+ },
1825
+ // Filesystem tools
1826
+ read_file: {
1827
+ icon: "",
1828
+ label: (m) => {
1829
+ const path5 = m.path || "";
1830
+ return path5 ? `Read ${path5}` : "Read file";
1831
+ },
1832
+ color: "gray",
1833
+ category: "filesystem"
1834
+ },
1835
+ read_span: {
1836
+ icon: "",
1837
+ label: (m) => {
1838
+ const path5 = m.path || "";
1839
+ const start = m.start_line || m.start;
1840
+ const end = m.end_line || m.end;
1841
+ if (path5 && start && end) {
1842
+ return `Read ${path5}:${start}-${end}`;
1843
+ }
1844
+ return path5 ? `Read ${path5}` : "Read span";
1845
+ },
1846
+ color: "gray",
1847
+ category: "filesystem"
1848
+ },
1849
+ delete_file: {
1850
+ icon: "",
1851
+ label: (m) => {
1852
+ const path5 = m.path || "";
1853
+ return path5 ? `Delete ${path5}` : "Delete file";
1854
+ },
1855
+ color: "red",
1856
+ category: "filesystem"
1857
+ },
1858
+ list_files: {
1859
+ icon: "",
1860
+ label: (m) => {
1861
+ const path5 = m.path || "";
1862
+ return path5 ? `List ${path5}` : "List files";
1863
+ },
1864
+ color: "gray",
1865
+ category: "filesystem"
1866
+ },
1867
+ // Meta tools
1868
+ think: {
1869
+ icon: "",
1870
+ label: (m) => {
1871
+ const summary = m.summary || "";
1872
+ return summary || "Planning next action";
1873
+ },
1874
+ color: "dim",
1875
+ category: "meta"
1876
+ },
1877
+ respond: {
1878
+ icon: "",
1879
+ label: () => "Final answer",
1880
+ color: "green",
1881
+ category: "meta"
1882
+ },
1883
+ plan: {
1884
+ icon: "",
1885
+ label: () => "Create execution plan",
1886
+ color: "blue",
1887
+ category: "meta"
1888
+ },
1889
+ revise_plan: {
1890
+ icon: "",
1891
+ label: () => "Revise plan",
1892
+ color: "blue",
1893
+ category: "meta"
1894
+ }
1895
+ };
1896
+ function getToolConfig(toolName) {
1897
+ if (!toolName || toolName === "") {
1898
+ return {
1899
+ icon: "",
1900
+ label: () => "Reasoning",
1901
+ color: "blue",
1902
+ category: "reasoning"
1903
+ };
1904
+ }
1905
+ return TOOL_CONFIGS[toolName] || {
1906
+ icon: "",
1907
+ label: () => toolName,
1908
+ color: "gray",
1909
+ category: "unknown"
1910
+ };
1911
+ }
1912
+
1913
+ // src/components/timeline/PlanContent.tsx
1914
+ import { Box as Box5, Text as Text6 } from "ink";
1915
+ import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
1916
+ var PlanContent = ({
1917
+ content,
1918
+ summary,
1919
+ steps,
1920
+ targetAreas,
1921
+ risks
1922
+ }) => {
1923
+ if (summary || steps) {
1924
+ return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", marginTop: 1, borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
1925
+ summary && /* @__PURE__ */ jsxs5(Box5, { marginBottom: 1, children: [
1926
+ /* @__PURE__ */ jsx9(Text6, { color: "white", bold: true, children: "Plan Summary:" }),
1927
+ /* @__PURE__ */ jsx9(Box5, { marginLeft: 1, marginTop: 0, children: /* @__PURE__ */ jsx9(Text6, { color: "white", children: summary }) })
1928
+ ] }),
1929
+ steps && steps.length > 0 && /* @__PURE__ */ jsxs5(Box5, { marginBottom: 1, flexDirection: "column", children: [
1930
+ /* @__PURE__ */ jsxs5(Text6, { color: "white", bold: true, children: [
1931
+ "Steps (",
1932
+ steps.length,
1933
+ "):"
1934
+ ] }),
1935
+ steps.map((step, index) => {
1936
+ const stepId = step.id || `step - ${index + 1} `;
1937
+ const description = step.description || "";
1938
+ const targetFiles = step.target_files || [];
1939
+ const status = step.status || "pending";
1940
+ return /* @__PURE__ */ jsxs5(Box5, { marginLeft: 1, marginTop: 0, flexDirection: "column", children: [
1941
+ /* @__PURE__ */ jsxs5(Box5, { flexDirection: "row", children: [
1942
+ /* @__PURE__ */ jsxs5(Text6, { color: "cyan", bold: true, children: [
1943
+ index + 1,
1944
+ "."
1945
+ ] }),
1946
+ /* @__PURE__ */ jsx9(Box5, { marginLeft: 1, flexGrow: 1, children: /* @__PURE__ */ jsx9(Text6, { color: "white", children: description }) }),
1947
+ status === "completed" && /* @__PURE__ */ jsx9(Text6, { color: "green", children: " \u2713" }),
1948
+ status === "skipped" && /* @__PURE__ */ jsx9(Text6, { color: "gray", children: " \u2298" })
1949
+ ] }),
1950
+ targetFiles.length > 0 && /* @__PURE__ */ jsx9(Box5, { marginLeft: 2, marginTop: 0, children: /* @__PURE__ */ jsxs5(Text6, { color: "gray", dimColor: true, children: [
1951
+ "Files: ",
1952
+ targetFiles.join(", ")
1953
+ ] }) })
1954
+ ] }, stepId);
1955
+ })
1956
+ ] }),
1957
+ targetAreas && targetAreas.length > 0 && /* @__PURE__ */ jsxs5(Box5, { marginBottom: 1, children: [
1958
+ /* @__PURE__ */ jsx9(Text6, { color: "white", bold: true, children: "Target Areas:" }),
1959
+ /* @__PURE__ */ jsx9(Box5, { marginLeft: 1, marginTop: 0, children: /* @__PURE__ */ jsx9(Text6, { color: "gray", children: targetAreas.join(", ") }) })
1960
+ ] }),
1961
+ risks && risks.length > 0 && /* @__PURE__ */ jsxs5(Box5, { marginBottom: 1, children: [
1962
+ /* @__PURE__ */ jsx9(Text6, { color: "red", bold: true, children: "Risks:" }),
1963
+ risks.map((risk, index) => /* @__PURE__ */ jsx9(Box5, { marginLeft: 1, marginTop: 0, children: /* @__PURE__ */ jsxs5(Text6, { color: "yellow", children: [
1964
+ "\u26A0 ",
1965
+ risk
1966
+ ] }) }, index))
1967
+ ] })
1968
+ ] });
1969
+ }
1970
+ if (content) {
1971
+ const lines = content.split("\n");
1972
+ return /* @__PURE__ */ jsx9(Box5, { flexDirection: "column", marginTop: 1, children: lines.map((line, index) => {
1973
+ const trimmed = line.trim();
1974
+ if (!trimmed) {
1975
+ return /* @__PURE__ */ jsx9(Box5, { height: 1 }, index);
1976
+ }
1977
+ if (trimmed.startsWith("\u2705")) {
1978
+ return /* @__PURE__ */ jsx9(Text6, { color: "green", bold: true, children: trimmed }, index);
1979
+ }
1980
+ if (trimmed.startsWith("Plan Summary:") || trimmed.startsWith("Plan:")) {
1981
+ return /* @__PURE__ */ jsx9(Box5, { marginTop: index > 0 ? 1 : 0, children: /* @__PURE__ */ jsx9(Text6, { color: "cyan", bold: true, children: trimmed }) }, index);
1982
+ }
1983
+ if (/^\s*\d+\.\s*\[/.test(trimmed)) {
1984
+ const hasCheckmark = trimmed.includes("\u2713") || trimmed.includes("\u2705");
1985
+ return /* @__PURE__ */ jsx9(Box5, { marginLeft: 1, children: /* @__PURE__ */ jsx9(Text6, { color: hasCheckmark ? "green" : "white", children: trimmed }) }, index);
1986
+ }
1987
+ if (/^\d+\./.test(trimmed)) {
1988
+ return /* @__PURE__ */ jsx9(Box5, { marginLeft: 1, children: /* @__PURE__ */ jsx9(Text6, { color: "white", children: trimmed }) }, index);
1989
+ }
1990
+ if (trimmed.startsWith("You can now") || trimmed.startsWith("Use 'revise_plan'")) {
1991
+ return /* @__PURE__ */ jsx9(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text6, { color: "gray", dimColor: true, children: trimmed }) }, index);
1992
+ }
1993
+ return /* @__PURE__ */ jsx9(Text6, { color: "white", children: trimmed }, index);
1994
+ }) });
1995
+ }
1996
+ return null;
1997
+ };
1998
+
1999
+ // src/components/timeline/ExecutionStep.tsx
2000
+ import { Box as Box6, Text as Text7 } from "ink";
2001
+
2002
+ // src/utils/formatting.ts
2003
+ function truncateOutput(content, maxLines) {
2004
+ const lines = content.split("\n");
2005
+ if (lines.length <= maxLines) {
2006
+ return content;
2007
+ }
2008
+ return lines.slice(0, maxLines).join("\n") + `
2009
+ ... (${lines.length - maxLines} lines hidden)`;
2010
+ }
2011
+
2012
+ // src/components/timeline/ExecutionStep.tsx
2013
+ import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
2014
+ var ExecutionStep = ({ step }) => {
2015
+ const stdout = step.metadata?.stdout;
2016
+ const stderr = step.metadata?.stderr;
2017
+ const exitCode = step.metadata?.exit_code;
2018
+ const command = step.command || step.toolName;
2019
+ const isSuccess = exitCode === 0 || exitCode === void 0 && step.status === "done";
2020
+ const isError = exitCode !== void 0 && exitCode !== 0;
2021
+ const isRunning = step.status === "running";
2022
+ const borderColor = isError ? "red" : isSuccess ? "green" : isRunning ? "yellow" : "gray";
2023
+ const headerColor = isError ? "red" : isSuccess ? "green" : isRunning ? "yellow" : "gray";
2024
+ return /* @__PURE__ */ jsx10(Box6, { marginTop: 1, marginLeft: 1, flexDirection: "column", children: /* @__PURE__ */ jsxs6(
2025
+ Box6,
2026
+ {
2027
+ flexDirection: "column",
2028
+ borderStyle: "round",
2029
+ borderColor,
2030
+ paddingX: 1,
2031
+ children: [
2032
+ /* @__PURE__ */ jsx10(
2033
+ Box6,
2034
+ {
2035
+ marginBottom: 1,
2036
+ borderStyle: "single",
2037
+ borderBottom: true,
2038
+ borderLeft: false,
2039
+ borderRight: false,
2040
+ borderTop: false,
2041
+ borderColor,
2042
+ paddingBottom: 1,
2043
+ children: /* @__PURE__ */ jsxs6(Text7, { color: headerColor, bold: true, children: [
2044
+ "\u279C ",
2045
+ command
2046
+ ] })
2047
+ }
2048
+ ),
2049
+ stdout && /* @__PURE__ */ jsx10(Box6, { marginBottom: stderr ? 1 : 0, flexDirection: "column", children: /* @__PURE__ */ jsx10(Text7, { color: isError ? "red" : "white", children: truncateOutput(stdout, 20) }) }),
2050
+ stderr && /* @__PURE__ */ jsx10(Box6, { flexDirection: "column", marginTop: stdout ? 1 : 0, children: /* @__PURE__ */ jsx10(Text7, { color: "red", children: truncateOutput(stderr, 20) }) }),
2051
+ !stdout && !stderr && isRunning && /* @__PURE__ */ jsx10(Box6, { children: /* @__PURE__ */ jsx10(Text7, { color: "gray", dimColor: true, children: "Running..." }) }),
2052
+ !stdout && !stderr && !isRunning && /* @__PURE__ */ jsx10(Box6, { children: /* @__PURE__ */ jsx10(Text7, { color: "gray", dimColor: true, children: "(No output)" }) }),
2053
+ exitCode !== void 0 && /* @__PURE__ */ jsx10(
2054
+ Box6,
2055
+ {
2056
+ marginTop: 1,
2057
+ borderStyle: "single",
2058
+ borderBottom: false,
2059
+ borderLeft: false,
2060
+ borderRight: false,
2061
+ borderTop: true,
2062
+ borderColor,
2063
+ paddingTop: 1,
2064
+ children: /* @__PURE__ */ jsxs6(Text7, { color: isError ? "red" : "green", bold: true, children: [
2065
+ "Exit code: ",
2066
+ exitCode
2067
+ ] })
2068
+ }
2069
+ )
2070
+ ]
2071
+ }
2072
+ ) });
2073
+ };
2074
+
2075
+ // src/components/timeline/ReasoningStep.tsx
2076
+ import { Box as Box7, Text as Text8 } from "ink";
2077
+ import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
2078
+ var ReasoningStep = ({ step }) => {
2079
+ const isThinkTool = step.toolName === "think";
2080
+ const reasoning = step.metadata?.reasoning;
2081
+ const isReasoning = (!step.toolName || step.toolName === "") && step.metadata?.content;
2082
+ const reasoningContent = step.metadata?.content;
2083
+ if (isThinkTool && reasoning && step.status === "done") {
2084
+ return /* @__PURE__ */ jsxs7(Box7, { marginTop: 1, marginLeft: 1, marginBottom: 1, flexDirection: "column", borderStyle: "round", borderColor: "blue", paddingX: 1, children: [
2085
+ /* @__PURE__ */ jsx11(Box7, { marginBottom: 1, children: /* @__PURE__ */ jsx11(Text8, { color: "blue", bold: true, children: "Thinking Process:" }) }),
2086
+ /* @__PURE__ */ jsx11(Text8, { color: "white", children: reasoning })
2087
+ ] });
2088
+ }
2089
+ if (isReasoning && reasoningContent) {
2090
+ return /* @__PURE__ */ jsx11(Box7, { marginTop: 1, marginLeft: 1, flexDirection: "column", children: /* @__PURE__ */ jsx11(Text8, { color: "blue", dimColor: true, children: reasoningContent }) });
2091
+ }
2092
+ return null;
2093
+ };
2094
+
2095
+ // src/components/timeline/ContextStep.tsx
2096
+ import { Box as Box8, Text as Text9 } from "ink";
2097
+ import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
2098
+ var ContextStep = ({ step }) => {
2099
+ const contextBefore = step.metadata?.before;
2100
+ const contextAfter = step.metadata?.after;
2101
+ const contextSaved = contextBefore && contextAfter ? contextBefore - contextAfter : 0;
2102
+ const contextPercent = contextBefore && contextSaved ? Math.round(contextSaved / contextBefore * 100) : 0;
2103
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "row", alignItems: "center", children: [
2104
+ /* @__PURE__ */ jsxs8(Text9, { color: "gray", children: [
2105
+ "\u{1F9E0} ",
2106
+ step.label
2107
+ ] }),
2108
+ contextSaved > 0 && /* @__PURE__ */ jsx12(Box8, { marginLeft: 1, children: /* @__PURE__ */ jsxs8(Text9, { color: "dim", dimColor: true, children: [
2109
+ "(saved ",
2110
+ contextSaved,
2111
+ " tokens, ",
2112
+ contextPercent,
2113
+ "%)"
2114
+ ] }) })
2115
+ ] });
2116
+ };
2117
+
2118
+ // src/components/timeline/RespondStep.tsx
2119
+ import { Box as Box9, Text as Text10 } from "ink";
2120
+ import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
2121
+ var RespondStep = ({ step }) => {
2122
+ const respondSummary = step.metadata?.summary;
2123
+ const respondFiles = step.metadata?.files_changed;
2124
+ if (step.status !== "done") return null;
2125
+ return /* @__PURE__ */ jsxs9(Box9, { marginTop: 0, marginLeft: 2, flexDirection: "column", children: [
2126
+ /* @__PURE__ */ jsxs9(Box9, { children: [
2127
+ /* @__PURE__ */ jsx13(Text10, { color: "green", children: "\u2714 " }),
2128
+ /* @__PURE__ */ jsx13(Text10, { color: "white", children: respondSummary || "Task completed" })
2129
+ ] }),
2130
+ respondFiles && respondFiles.length > 0 && /* @__PURE__ */ jsx13(Box9, { marginLeft: 2, children: /* @__PURE__ */ jsxs9(Text10, { color: "gray", dimColor: true, children: [
2131
+ "Files: ",
2132
+ respondFiles.join(", ")
2133
+ ] }) })
2134
+ ] });
2135
+ };
2136
+
2137
+ // src/components/timeline/ReadFileStep.tsx
2138
+ import { Box as Box10, Text as Text11 } from "ink";
2139
+ import { jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
2140
+ var ReadFileStep = ({ step }) => {
2141
+ const { status, metadata } = step;
2142
+ const isError = status === "failed" || metadata?.error;
2143
+ const filePath = metadata?.path || metadata?.file_path || metadata?.params?.file_path || metadata?.params?.path || "Unknown file";
2144
+ const fileName = filePath.split("/").pop() || filePath;
2145
+ const statusIcon = isError ? "\u2717" : status === "running" ? "\u22EF" : "\u2713";
2146
+ const statusColor = isError ? "red" : status === "running" ? "gray" : "green";
2147
+ return /* @__PURE__ */ jsxs10(Box10, { children: [
2148
+ /* @__PURE__ */ jsxs10(Text11, { color: statusColor, children: [
2149
+ statusIcon,
2150
+ " "
2151
+ ] }),
2152
+ /* @__PURE__ */ jsx14(Text11, { color: "gray", children: "Reading " }),
2153
+ /* @__PURE__ */ jsx14(Text11, { color: "cyan", children: fileName })
2154
+ ] });
2155
+ };
2156
+
2157
+ // src/components/timeline/TimelineItem.tsx
2158
+ import { jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
2159
+ var TimelineItem = React5.memo(({ step, isActive = false }) => {
2160
+ const config = getToolConfig(step.toolName);
2161
+ let durationText = "";
2162
+ if (step.durationMs !== void 0 && step.durationMs > 0) {
2163
+ const seconds = (step.durationMs / 1e3).toFixed(2);
2164
+ durationText = ` (${seconds}s)`;
2165
+ }
2166
+ const isPlanTool = step.toolName === "plan";
2167
+ const planContent = step.metadata?.plan_content;
2168
+ const planSummary = step.metadata?.plan_summary;
2169
+ const planSteps = step.metadata?.plan_steps;
2170
+ const planTargetAreas = step.metadata?.plan_target_areas;
2171
+ const planRisks = step.metadata?.plan_risks;
2172
+ const isExecutionTool = step.toolName === "run_cmd" || step.toolName === "run_tests" || step.toolName === "run_build";
2173
+ const isThinkTool = step.toolName === "think";
2174
+ const isReasoning = (!step.toolName || step.toolName === "") && step.metadata?.content;
2175
+ const errorMessage = step.metadata?.error;
2176
+ const errorResult = step.metadata?.error_result;
2177
+ const isFailed = step.status === "failed";
2178
+ const displayError = errorMessage || errorResult;
2179
+ const isRespondTool = step.toolName === "respond";
2180
+ const isReadFileTool = step.toolName === "read_file";
2181
+ const isContextEvent = step.type === "context_event";
2182
+ return /* @__PURE__ */ jsxs11(
2183
+ Box11,
2184
+ {
2185
+ flexDirection: "column",
2186
+ marginLeft: 1,
2187
+ children: [
2188
+ isContextEvent && /* @__PURE__ */ jsx15(ContextStep, { step }),
2189
+ isReadFileTool && /* @__PURE__ */ jsx15(ReadFileStep, { step }),
2190
+ !isContextEvent && !isReadFileTool && /* @__PURE__ */ jsxs11(Box11, { flexDirection: "row", alignItems: "center", justifyContent: "space-between", children: [
2191
+ /* @__PURE__ */ jsxs11(Box11, { flexDirection: "row", alignItems: "center", flexGrow: 1, children: [
2192
+ /* @__PURE__ */ jsx15(Text12, { color: config.color, bold: true, children: config.icon }),
2193
+ step.toolName && /* @__PURE__ */ jsx15(Box11, { marginLeft: 1, children: /* @__PURE__ */ jsxs11(Text12, { color: config.color, bold: true, children: [
2194
+ "[",
2195
+ step.toolName.toUpperCase(),
2196
+ "]"
2197
+ ] }) }),
2198
+ /* @__PURE__ */ jsx15(Box11, { marginLeft: 1, flexGrow: 1, children: /* @__PURE__ */ jsx15(Text12, { children: step.label }) })
2199
+ ] }),
2200
+ /* @__PURE__ */ jsx15(Box11, { marginLeft: 2, children: /* @__PURE__ */ jsx15(StatusBadge, { status: step.status }) })
2201
+ ] }),
2202
+ step.command && /* @__PURE__ */ jsx15(Box11, { marginTop: 0, marginLeft: 1, children: /* @__PURE__ */ jsxs11(Text12, { color: "dim", dimColor: true, children: [
2203
+ "$ ",
2204
+ step.command
2205
+ ] }) }),
2206
+ isPlanTool && step.status === "done" && (planContent || planSummary || planSteps) && /* @__PURE__ */ jsx15(Box11, { marginTop: 1, marginLeft: 1, flexDirection: "column", children: /* @__PURE__ */ jsx15(
2207
+ PlanContent,
2208
+ {
2209
+ content: planContent,
2210
+ summary: planSummary,
2211
+ steps: planSteps,
2212
+ targetAreas: planTargetAreas,
2213
+ risks: planRisks
2214
+ }
2215
+ ) }),
2216
+ isFailed && displayError && /* @__PURE__ */ jsxs11(Box11, { marginTop: 1, marginLeft: 1, flexDirection: "column", children: [
2217
+ /* @__PURE__ */ jsx15(Text12, { color: "red", bold: true, children: "Error:" }),
2218
+ /* @__PURE__ */ jsx15(Box11, { marginLeft: 1, children: /* @__PURE__ */ jsx15(Text12, { color: "red", children: displayError }) })
2219
+ ] }),
2220
+ isExecutionTool && /* @__PURE__ */ jsx15(ExecutionStep, { step }),
2221
+ (isThinkTool || isReasoning) && /* @__PURE__ */ jsx15(ReasoningStep, { step }),
2222
+ isRespondTool && /* @__PURE__ */ jsx15(RespondStep, { step })
2223
+ ]
2224
+ }
2225
+ );
2226
+ }, (prevProps, nextProps) => {
2227
+ return prevProps.step.id === nextProps.step.id && prevProps.step.label === nextProps.step.label && prevProps.step.status === nextProps.step.status && prevProps.step.metadata === nextProps.step.metadata && prevProps.step.finishedAt?.getTime() === nextProps.step.finishedAt?.getTime() && prevProps.isActive === nextProps.isActive;
2228
+ });
2229
+
2230
+ // src/components/timeline/CodeDiff.tsx
2231
+ import { Box as Box12, Text as Text13 } from "ink";
2232
+ import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
2233
+ var formatDiffLines = (text, prefix, color, maxLines = 10) => {
2234
+ const lines = text.split("\n").slice(0, maxLines);
2235
+ const hasMore = text.split("\n").length > maxLines;
2236
+ const elements = lines.map((line, idx) => /* @__PURE__ */ jsx16(Box12, { children: /* @__PURE__ */ jsxs12(Text13, { color, children: [
2237
+ prefix,
2238
+ " ",
2239
+ line || "(empty line)"
2240
+ ] }) }, idx));
2241
+ if (hasMore) {
2242
+ elements.push(
2243
+ /* @__PURE__ */ jsx16(Box12, { children: /* @__PURE__ */ jsx16(Text13, { color: "gray", children: " ..." }) }, "more")
2244
+ );
2245
+ }
2246
+ return elements;
2247
+ };
2248
+ var CodeDiff = ({ codeChange }) => {
2249
+ const { file, before, after, startLine, endLine } = codeChange;
2250
+ let location = file;
2251
+ if (startLine && endLine) {
2252
+ location += ` (L${startLine}-${endLine})`;
2253
+ } else if (startLine) {
2254
+ location += ` (L${startLine})`;
2255
+ }
2256
+ return /* @__PURE__ */ jsxs12(Box12, { flexDirection: "column", marginLeft: 2, marginTop: 1, marginBottom: 1, children: [
2257
+ /* @__PURE__ */ jsx16(Box12, { children: /* @__PURE__ */ jsxs12(Text13, { color: "magenta", children: [
2258
+ "\u{1F4DD} ",
2259
+ location
2260
+ ] }) }),
2261
+ before && /* @__PURE__ */ jsx16(Box12, { flexDirection: "column", marginLeft: 1, children: formatDiffLines(before, "-", "red", 5) }),
2262
+ after && /* @__PURE__ */ jsx16(Box12, { flexDirection: "column", marginLeft: 1, children: formatDiffLines(after, "+", "green", 5) })
2263
+ ] });
2264
+ };
2265
+
2266
+ // src/components/common/FormattedText.tsx
2267
+ import { Box as Box13, Text as Text14 } from "ink";
2268
+ import { lexer } from "marked";
2269
+ import { jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
2270
+ var RenderInline = ({ tokens }) => {
2271
+ if (!tokens) return null;
2272
+ return /* @__PURE__ */ jsx17(Text14, { children: tokens.map((token, i) => {
2273
+ if (token.type === "text") {
2274
+ if (token.tokens) {
2275
+ return /* @__PURE__ */ jsx17(RenderInline, { tokens: token.tokens }, i);
2276
+ }
2277
+ return /* @__PURE__ */ jsx17(Text14, { children: token.text }, i);
2278
+ }
2279
+ if (token.type === "strong") {
2280
+ return /* @__PURE__ */ jsx17(Text14, { bold: true, children: /* @__PURE__ */ jsx17(RenderInline, { tokens: token.tokens }) }, i);
2281
+ }
2282
+ if (token.type === "em") {
2283
+ return /* @__PURE__ */ jsx17(Text14, { italic: true, children: /* @__PURE__ */ jsx17(RenderInline, { tokens: token.tokens }) }, i);
2284
+ }
2285
+ if (token.type === "codespan") {
2286
+ return /* @__PURE__ */ jsx17(Text14, { color: "blue", backgroundColor: "black", children: token.text }, i);
2287
+ }
2288
+ if (token.type === "link") {
2289
+ return /* @__PURE__ */ jsx17(Text14, { color: "blue", underline: true, children: token.text }, i);
2290
+ }
2291
+ return /* @__PURE__ */ jsx17(Text14, { children: token.raw }, i);
2292
+ }) });
2293
+ };
2294
+ var FormattedText = ({ content }) => {
2295
+ if (!content) return null;
2296
+ const tokens = lexer(content);
2297
+ return /* @__PURE__ */ jsx17(Box13, { flexDirection: "column", children: tokens.map((token, index) => {
2298
+ if (token.type === "paragraph") {
2299
+ return /* @__PURE__ */ jsx17(Box13, { marginBottom: 1, children: /* @__PURE__ */ jsx17(RenderInline, { tokens: token.tokens }) }, index);
2300
+ }
2301
+ if (token.type === "code") {
2302
+ return /* @__PURE__ */ jsx17(
2303
+ Box13,
2304
+ {
2305
+ flexDirection: "column",
2306
+ marginBottom: 1,
2307
+ borderStyle: "round",
2308
+ borderColor: "gray",
2309
+ paddingX: 1,
2310
+ children: /* @__PURE__ */ jsx17(Text14, { color: "yellow", children: token.text })
2311
+ },
2312
+ index
2313
+ );
2314
+ }
2315
+ if (token.type === "list") {
2316
+ return /* @__PURE__ */ jsx17(Box13, { flexDirection: "column", marginBottom: 1, children: token.items.map((item, i) => /* @__PURE__ */ jsxs13(Box13, { marginLeft: 1, children: [
2317
+ /* @__PURE__ */ jsx17(Text14, { color: "green", children: "\u2022 " }),
2318
+ /* @__PURE__ */ jsx17(RenderInline, { tokens: item.tokens })
2319
+ ] }, i)) }, index);
2320
+ }
2321
+ if (token.type === "heading") {
2322
+ return /* @__PURE__ */ jsx17(Box13, { marginBottom: 1, marginTop: 1, children: /* @__PURE__ */ jsx17(Text14, { bold: true, underline: true, children: token.text }) }, index);
2323
+ }
2324
+ if (token.type === "space") {
2325
+ return null;
2326
+ }
2327
+ return /* @__PURE__ */ jsx17(Box13, { marginBottom: 1, children: token.text && /* @__PURE__ */ jsx17(Text14, { children: token.text }) }, index);
2328
+ }) });
2329
+ };
2330
+
2331
+ // src/components/conversation/Conversation.tsx
2332
+ import { jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
2333
+ var Conversation = ({
2334
+ isRunning,
2335
+ timelineSteps = [],
2336
+ currentRunningStepId
2337
+ }) => {
2338
+ const { turns } = useConversation();
2339
+ const turnCacheRef = React6.useRef(/* @__PURE__ */ new Map());
2340
+ const items = useMemo3(() => {
2341
+ const result = [];
2342
+ turns.forEach((turn, index) => {
2343
+ const isLastTurn = index === turns.length - 1;
2344
+ let turnItems;
2345
+ if (isLastTurn || !turnCacheRef.current.has(turn.id)) {
2346
+ const steps = isLastTurn && timelineSteps.length > 0 ? timelineSteps : turn.timelineSteps;
2347
+ turnItems = flattenTurn(turn, steps, currentRunningStepId);
2348
+ if (turn.done && !isLastTurn) {
2349
+ turnCacheRef.current.set(turn.id, turnItems);
2350
+ }
2351
+ } else {
2352
+ turnItems = turnCacheRef.current.get(turn.id);
2353
+ }
2354
+ result.push(...turnItems);
2355
+ });
2356
+ return result;
2357
+ }, [turns, timelineSteps, currentRunningStepId]);
2358
+ if (turns.length === 0) {
2359
+ return /* @__PURE__ */ jsx18(Box14, { flexDirection: "column", children: /* @__PURE__ */ jsx18(Text15, { color: "gray", children: "Describe a task below to get started." }) });
2360
+ }
2361
+ return /* @__PURE__ */ jsx18(Box14, { flexDirection: "column", children: items.map((item) => /* @__PURE__ */ jsx18(
2362
+ RenderItemComponent,
2363
+ {
2364
+ item,
2365
+ currentRunningStepId
2366
+ },
2367
+ item.id
2368
+ )) });
2369
+ };
2370
+ var RenderItemComponent = React6.memo(({ item, currentRunningStepId }) => {
2371
+ switch (item.type) {
2372
+ case "user_message":
2373
+ return /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", children: [
2374
+ /* @__PURE__ */ jsxs14(Box14, { children: [
2375
+ /* @__PURE__ */ jsxs14(Text15, { color: "gray", bold: true, children: [
2376
+ item.prefix,
2377
+ " "
2378
+ ] }),
2379
+ /* @__PURE__ */ jsx18(Text15, { color: "gray", children: item.text.split("\n")[0] })
2380
+ ] }),
2381
+ item.text.split("\n").slice(1).map((line, i) => /* @__PURE__ */ jsx18(Box14, { marginLeft: item.prefix.length + 1, children: /* @__PURE__ */ jsx18(Text15, { color: "gray", children: line }) }, i))
2382
+ ] });
2383
+ case "label":
2384
+ return /* @__PURE__ */ jsx18(Box14, { children: /* @__PURE__ */ jsx18(Text15, { color: item.color, bold: true, dimColor: item.dimColor, children: item.text }) });
2385
+ case "text_chunk":
2386
+ return /* @__PURE__ */ jsx18(Box14, { marginLeft: 1, children: item.dimColor ? /* @__PURE__ */ jsx18(Text15, { color: "gray", dimColor: true, children: item.text }) : /* @__PURE__ */ jsx18(FormattedText, { content: item.text }) });
2387
+ case "divider":
2388
+ return null;
2389
+ case "timeline_step":
2390
+ return /* @__PURE__ */ jsx18(
2391
+ TimelineItem,
2392
+ {
2393
+ step: item.step,
2394
+ isActive: item.step.id === currentRunningStepId || item.step.status === "running"
2395
+ }
2396
+ );
2397
+ case "assistant_message":
2398
+ return /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", children: [
2399
+ /* @__PURE__ */ jsxs14(Box14, { children: [
2400
+ /* @__PURE__ */ jsxs14(Text15, { color: "green", bold: true, children: [
2401
+ item.prefix,
2402
+ " "
2403
+ ] }),
2404
+ /* @__PURE__ */ jsx18(FormattedText, { content: item.text.split("\n")[0] })
2405
+ ] }),
2406
+ item.text.split("\n").length > 1 && /* @__PURE__ */ jsx18(Box14, { flexDirection: "column", marginLeft: item.prefix.length + 1, children: /* @__PURE__ */ jsx18(FormattedText, { content: item.text.split("\n").slice(1).join("\n") }) })
2407
+ ] });
2408
+ case "code_change":
2409
+ return /* @__PURE__ */ jsx18(Box14, { paddingX: 1, children: /* @__PURE__ */ jsx18(CodeDiff, { codeChange: item.codeChange }) });
2410
+ default:
2411
+ return null;
2412
+ }
2413
+ });
2414
+
2415
+ // src/components/layout/AppLayout.tsx
2416
+ import { jsx as jsx19, jsxs as jsxs15 } from "react/jsx-runtime";
2417
+ var AppLayout = React7.memo(({
2418
+ terminalRows,
2419
+ terminalColumns,
2420
+ error,
2421
+ showProjectPlan,
2422
+ projectPlan,
2423
+ currentTimelineSteps,
2424
+ currentRunningStepId,
2425
+ isRunning,
2426
+ footerProps,
2427
+ onCancelRequest,
2428
+ helpModal,
2429
+ onHelp
2430
+ }) => {
2431
+ const { exit } = useApp3();
2432
+ const handlerRef = useRef7({
2433
+ exit,
2434
+ isRunning,
2435
+ onCancelRequest,
2436
+ onHelp,
2437
+ helpModal: !!helpModal
2438
+ });
2439
+ handlerRef.current = {
2440
+ exit,
2441
+ isRunning,
2442
+ onCancelRequest,
2443
+ onHelp,
2444
+ helpModal: !!helpModal
2445
+ };
2446
+ useInput2((input, key) => {
2447
+ const { exit: exit2, isRunning: isRunning2, onCancelRequest: onCancelRequest2, onHelp: onHelp2, helpModal: helpModal2 } = handlerRef.current;
2448
+ if (key.ctrl && input === "c") {
2449
+ exit2();
2450
+ return;
2451
+ }
2452
+ if (key.escape) {
2453
+ if (helpModal2) {
2454
+ return;
2455
+ }
2456
+ if (onCancelRequest2) {
2457
+ onCancelRequest2();
2458
+ }
2459
+ return;
2460
+ }
2461
+ if (input === "\x1BOP") {
2462
+ onHelp2?.();
2463
+ return;
2464
+ }
2465
+ }, { isActive: true });
2466
+ return /* @__PURE__ */ jsxs15(Box15, { flexDirection: "column", children: [
2467
+ /* @__PURE__ */ jsx19(Box15, { paddingX: 1, flexShrink: 0, children: /* @__PURE__ */ jsx19(Header, {}) }),
2468
+ error && /* @__PURE__ */ jsx19(Box15, { paddingX: 1, children: /* @__PURE__ */ jsx19(Box15, { borderStyle: "round", borderColor: "red", paddingX: 1, children: /* @__PURE__ */ jsxs15(Text16, { color: "red", children: [
2469
+ "Error: ",
2470
+ error
2471
+ ] }) }) }),
2472
+ /* @__PURE__ */ jsx19(Box15, { flexGrow: 1, flexDirection: "column", paddingX: 1, children: /* @__PURE__ */ jsx19(
2473
+ Conversation,
2474
+ {
2475
+ isRunning,
2476
+ timelineSteps: currentTimelineSteps,
2477
+ currentRunningStepId
2478
+ }
2479
+ ) }),
2480
+ helpModal && /* @__PURE__ */ jsx19(Box15, { position: "absolute", width: "100%", height: "100%", alignItems: "center", justifyContent: "center", children: helpModal }),
2481
+ !helpModal && /* @__PURE__ */ jsx19(Box15, { flexShrink: 0, paddingX: 1, children: /* @__PURE__ */ jsx19(Footer, { ...footerProps }) })
2482
+ ] });
2483
+ });
2484
+
2485
+ // src/components/SetupWizard.tsx
2486
+ import React8, { useState as useState8 } from "react";
2487
+ import { Box as Box16, Text as Text17, useApp as useApp4, useInput as useInput3 } from "ink";
2488
+ import { jsx as jsx20, jsxs as jsxs16 } from "react/jsx-runtime";
2489
+ var PROVIDERS = ["openai", "anthropic", "kimi", "gemini", "deepseek", "groq", "lmstudio", "ollama", "glm", "minimax"];
2490
+ var UncontrolledInput = ({ placeholder, onSubmit }) => {
2491
+ const [value, setValue] = useState8("");
2492
+ return /* @__PURE__ */ jsx20(
2493
+ Input,
2494
+ {
2495
+ value,
2496
+ onChange: setValue,
2497
+ onSubmit,
2498
+ placeholder
2499
+ }
2500
+ );
2501
+ };
2502
+ var PROVIDER_MODELS = {
2503
+ "openai": ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo"],
2504
+ "anthropic": ["claude-3-5-sonnet-20240620", "claude-3-opus-20240229", "claude-3-sonnet-20240229", "claude-3-haiku-20240307"],
2505
+ "kimi": ["kimi-k2-250711"]
2506
+ };
2507
+ var SetupWizard = ({ sendCommand, onComplete, isUpdate = false, initialConfig }) => {
2508
+ const { exit } = useApp4();
2509
+ const [step, setStep] = useState8("intro");
2510
+ const [isReady, setIsReady] = useState8(false);
2511
+ const [config, setConfig] = useState8({
2512
+ llm_provider: "openai",
2513
+ api_key: "",
2514
+ model: "gpt-4o",
2515
+ auto_index: true,
2516
+ embedding_key: ""
2517
+ });
2518
+ React8.useEffect(() => {
2519
+ if (initialConfig) {
2520
+ setConfig((prev) => ({
2521
+ ...prev,
2522
+ llm_provider: initialConfig.llm_provider || prev.llm_provider,
2523
+ api_key: initialConfig.api_key || prev.api_key,
2524
+ model: initialConfig.model || prev.model,
2525
+ auto_index: initialConfig.auto_index === "true"
2526
+ }));
2527
+ }
2528
+ }, [initialConfig]);
2529
+ const [selectedProviderIndex, setSelectedProviderIndex] = useState8(0);
2530
+ const [selectedModelIndex, setSelectedModelIndex] = useState8(0);
2531
+ const lastStepChangeRef = React8.useRef(Date.now());
2532
+ React8.useEffect(() => {
2533
+ debugLog.lifecycle("SetupWizard", "mount", `isUpdate=${isUpdate}`);
2534
+ const timer = setTimeout(() => setIsReady(true), 1e3);
2535
+ return () => {
2536
+ debugLog.lifecycle("SetupWizard", "unmount");
2537
+ clearTimeout(timer);
2538
+ };
2539
+ }, [isUpdate]);
2540
+ useInput3((input, key) => {
2541
+ debugLog.event("SetupWizard", "keypress", { step, input, key });
2542
+ if (!isReady) return;
2543
+ if (Date.now() - lastStepChangeRef.current < 500) {
2544
+ return;
2545
+ }
2546
+ if (key.ctrl && input === "c") {
2547
+ exit();
2548
+ return;
2549
+ }
2550
+ if (step === "intro" && key.return) {
2551
+ setStep("provider");
2552
+ lastStepChangeRef.current = Date.now();
2553
+ return;
2554
+ }
2555
+ if (step === "provider") {
2556
+ if (key.upArrow) {
2557
+ setSelectedProviderIndex((prev) => Math.max(0, prev - 1));
2558
+ }
2559
+ if (key.downArrow) {
2560
+ setSelectedProviderIndex((prev) => Math.min(PROVIDERS.length - 1, prev + 1));
2561
+ }
2562
+ if (key.return) {
2563
+ const provider = PROVIDERS[selectedProviderIndex];
2564
+ setConfig((prev) => ({ ...prev, llm_provider: provider, model: PROVIDER_MODELS[provider][0] }));
2565
+ setSelectedModelIndex(0);
2566
+ setStep("model");
2567
+ lastStepChangeRef.current = Date.now();
2568
+ }
2569
+ return;
2570
+ }
2571
+ if (step === "model") {
2572
+ const models = PROVIDER_MODELS[config.llm_provider] || [];
2573
+ if (key.upArrow) {
2574
+ setSelectedModelIndex((prev) => Math.max(0, prev - 1));
2575
+ }
2576
+ if (key.downArrow) {
2577
+ setSelectedModelIndex((prev) => Math.min(models.length - 1, prev + 1));
2578
+ }
2579
+ if (key.return) {
2580
+ setConfig((prev) => ({ ...prev, model: models[selectedModelIndex] }));
2581
+ setStep("api_key");
2582
+ lastStepChangeRef.current = Date.now();
2583
+ }
2584
+ return;
2585
+ }
2586
+ if (step === "auto_index") {
2587
+ if (input === "y" || input === "Y" || key.return) {
2588
+ setConfig((prev) => ({ ...prev, auto_index: true }));
2589
+ setStep("summary");
2590
+ lastStepChangeRef.current = Date.now();
2591
+ return;
2592
+ }
2593
+ if (input === "n" || input === "N") {
2594
+ setConfig((prev) => ({ ...prev, auto_index: false }));
2595
+ setStep("summary");
2596
+ lastStepChangeRef.current = Date.now();
2597
+ return;
2598
+ }
2599
+ }
2600
+ if (step === "summary") {
2601
+ if (key.return) {
2602
+ handleSave();
2603
+ lastStepChangeRef.current = Date.now();
2604
+ return;
2605
+ }
2606
+ if (key.escape || key.backspace || key.delete) {
2607
+ setStep("intro");
2608
+ return;
2609
+ }
2610
+ }
2611
+ if (step === "complete" && key.return) {
2612
+ debugLog.lifecycle("SetupWizard", "update", "onComplete triggered");
2613
+ onComplete();
2614
+ }
2615
+ }, { isActive: true });
2616
+ const handleSave = () => {
2617
+ setStep("saving");
2618
+ const payload = {
2619
+ ...config,
2620
+ auto_index: config.auto_index ? "true" : "false"
2621
+ };
2622
+ sendCommand("save_config", payload);
2623
+ setTimeout(() => {
2624
+ setStep("complete");
2625
+ }, 1e3);
2626
+ };
2627
+ if (step === "intro") {
2628
+ return /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "cyan", children: [
2629
+ /* @__PURE__ */ jsx20(Text17, { bold: true, color: "cyan", children: isUpdate ? "Update Configuration" : "Welcome to Dodo! \u{1F9A4}" }),
2630
+ /* @__PURE__ */ jsx20(Text17, { children: isUpdate ? "Let's update your Dodo agent settings." : "It looks like this is your first time running Dodo." }),
2631
+ !isUpdate && /* @__PURE__ */ jsx20(Text17, { children: "Let's get you set up in a few seconds." }),
2632
+ isUpdate && /* @__PURE__ */ jsx20(Box16, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx20(Text17, { color: "gray", children: "You are currently re-configuring the agent." }) }),
2633
+ /* @__PURE__ */ jsx20(Box16, { marginTop: 1, children: /* @__PURE__ */ jsx20(Text17, { color: "green", children: "Press Enter to start..." }) })
2634
+ ] });
2635
+ }
2636
+ if (step === "provider") {
2637
+ return /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "blue", children: [
2638
+ /* @__PURE__ */ jsx20(Text17, { bold: true, children: "Select your LLM Provider:" }),
2639
+ PROVIDERS.map((p, i) => /* @__PURE__ */ jsxs16(Text17, { color: i === selectedProviderIndex ? "green" : "white", children: [
2640
+ i === selectedProviderIndex ? "> " : " ",
2641
+ " ",
2642
+ p
2643
+ ] }, p)),
2644
+ /* @__PURE__ */ jsx20(Box16, { marginTop: 1, children: /* @__PURE__ */ jsx20(Text17, { color: "gray", children: "Use Up/Down arrows, Enter to select" }) })
2645
+ ] });
2646
+ }
2647
+ if (step === "model") {
2648
+ const models = PROVIDER_MODELS[config.llm_provider] || [];
2649
+ return /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "blue", children: [
2650
+ /* @__PURE__ */ jsxs16(Text17, { bold: true, children: [
2651
+ "Select Default Model for ",
2652
+ config.llm_provider,
2653
+ ":"
2654
+ ] }),
2655
+ models.map((m, i) => /* @__PURE__ */ jsxs16(Text17, { color: i === selectedModelIndex ? "green" : "white", children: [
2656
+ i === selectedModelIndex ? "> " : " ",
2657
+ " ",
2658
+ m
2659
+ ] }, m)),
2660
+ /* @__PURE__ */ jsx20(Box16, { marginTop: 1, children: /* @__PURE__ */ jsx20(Text17, { color: "gray", children: "Use Up/Down arrows, Enter to select" }) })
2661
+ ] });
2662
+ }
2663
+ if (step === "api_key") {
2664
+ return /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "blue", children: [
2665
+ /* @__PURE__ */ jsxs16(Text17, { bold: true, children: [
2666
+ "Enter your API Key for ",
2667
+ config.llm_provider,
2668
+ ":"
2669
+ ] }),
2670
+ /* @__PURE__ */ jsx20(
2671
+ UncontrolledInput,
2672
+ {
2673
+ placeholder: "sk-...",
2674
+ onSubmit: (val) => {
2675
+ if (!val.trim()) return;
2676
+ setConfig((prev) => ({ ...prev, api_key: val }));
2677
+ if (config.llm_provider === "openai") {
2678
+ setStep("auto_index");
2679
+ } else {
2680
+ setStep("embedding_key");
2681
+ }
2682
+ }
2683
+ },
2684
+ step
2685
+ )
2686
+ ] });
2687
+ }
2688
+ if (step === "embedding_key") {
2689
+ return /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "blue", children: [
2690
+ /* @__PURE__ */ jsx20(Text17, { bold: true, children: "Optional: Enter OpenAI API Key for Embeddings" }),
2691
+ /* @__PURE__ */ jsx20(Text17, { color: "gray", children: "Semantic search requires OpenAI embeddings. Press Enter to skip." }),
2692
+ /* @__PURE__ */ jsx20(
2693
+ UncontrolledInput,
2694
+ {
2695
+ placeholder: "sk-... (optional)",
2696
+ onSubmit: (val) => {
2697
+ setConfig((prev) => ({ ...prev, embedding_key: val.trim() }));
2698
+ setStep("auto_index");
2699
+ }
2700
+ },
2701
+ step
2702
+ )
2703
+ ] });
2704
+ }
2705
+ if (step === "auto_index") {
2706
+ return /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "blue", children: [
2707
+ /* @__PURE__ */ jsx20(Text17, { bold: true, children: "Enable Auto-Indexing for new projects? (Y/n)" }),
2708
+ /* @__PURE__ */ jsx20(Text17, { color: "gray", children: "This allows Dodo to understand your codebase using semantic search." })
2709
+ ] });
2710
+ }
2711
+ if (step === "summary") {
2712
+ return /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "yellow", children: [
2713
+ /* @__PURE__ */ jsx20(Text17, { bold: true, color: "yellow", children: "Configuration Summary" }),
2714
+ /* @__PURE__ */ jsxs16(Box16, { marginY: 1, flexDirection: "column", children: [
2715
+ /* @__PURE__ */ jsxs16(Text17, { children: [
2716
+ "Provider: ",
2717
+ /* @__PURE__ */ jsx20(Text17, { bold: true, color: "white", children: config.llm_provider })
2718
+ ] }),
2719
+ /* @__PURE__ */ jsxs16(Text17, { children: [
2720
+ "Model: ",
2721
+ /* @__PURE__ */ jsx20(Text17, { bold: true, color: "white", children: config.model })
2722
+ ] }),
2723
+ /* @__PURE__ */ jsxs16(Text17, { children: [
2724
+ "API Key: ",
2725
+ /* @__PURE__ */ jsx20(Text17, { bold: true, color: "white", children: config.api_key ? "********" + config.api_key.slice(-4) : "(Not set)" })
2726
+ ] }),
2727
+ config.llm_provider !== "openai" && /* @__PURE__ */ jsxs16(Text17, { children: [
2728
+ "Embed Key: ",
2729
+ /* @__PURE__ */ jsx20(Text17, { bold: true, color: "white", children: config.embedding_key ? "********" + config.embedding_key.slice(-4) : "(Not set - no semantic search)" })
2730
+ ] }),
2731
+ /* @__PURE__ */ jsxs16(Text17, { children: [
2732
+ "Auto-Index: ",
2733
+ /* @__PURE__ */ jsx20(Text17, { bold: true, color: "white", children: config.auto_index ? "Yes" : "No" })
2734
+ ] })
2735
+ ] }),
2736
+ /* @__PURE__ */ jsxs16(Text17, { children: [
2737
+ "Press ",
2738
+ /* @__PURE__ */ jsx20(Text17, { bold: true, color: "green", children: "Enter" }),
2739
+ " to Save, or ",
2740
+ /* @__PURE__ */ jsx20(Text17, { bold: true, color: "red", children: "Esc" }),
2741
+ " to Restart"
2742
+ ] })
2743
+ ] });
2744
+ }
2745
+ if (step === "saving") {
2746
+ return /* @__PURE__ */ jsx20(Box16, { flexDirection: "column", padding: 1, children: /* @__PURE__ */ jsx20(Text17, { color: "yellow", children: "Saving configuration..." }) });
2747
+ }
2748
+ if (step === "complete") {
2749
+ return /* @__PURE__ */ jsxs16(Box16, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "green", children: [
2750
+ /* @__PURE__ */ jsx20(Text17, { bold: true, color: "green", children: "Setup Complete! \u{1F389}" }),
2751
+ /* @__PURE__ */ jsx20(Text17, { children: "Configuration saved successfully." }),
2752
+ /* @__PURE__ */ jsx20(Box16, { marginTop: 1, children: /* @__PURE__ */ jsx20(Text17, { color: "gray", children: "Press Enter to return..." }) })
2753
+ ] });
2754
+ }
2755
+ return null;
2756
+ };
2757
+
2758
+ // src/components/ProjectPermissionPrompt.tsx
2759
+ import { useState as useState9, useEffect as useEffect8 } from "react";
2760
+ import { Box as Box17, Text as Text18, useInput as useInput4 } from "ink";
2761
+ import { jsx as jsx21, jsxs as jsxs17 } from "react/jsx-runtime";
2762
+ var ProjectPermissionPrompt = ({
2763
+ repoRoot,
2764
+ onResponse
2765
+ }) => {
2766
+ const [isReady, setIsReady] = useState9(false);
2767
+ useEffect8(() => {
2768
+ debugLog.lifecycle("ProjectPermissionPrompt", "mount", `repo=${repoRoot}`);
2769
+ const timer = setTimeout(() => setIsReady(true), 500);
2770
+ return () => {
2771
+ debugLog.lifecycle("ProjectPermissionPrompt", "unmount");
2772
+ clearTimeout(timer);
2773
+ };
2774
+ }, [repoRoot]);
2775
+ useInput4((input, key) => {
2776
+ if (!isReady) return;
2777
+ if (input === "y" || input === "Y" || key.return) {
2778
+ debugLog.command("ProjectPermissionPrompt", "response", { enabled: true });
2779
+ onResponse(true);
2780
+ } else if (input === "n" || input === "N") {
2781
+ debugLog.command("ProjectPermissionPrompt", "response", { enabled: false });
2782
+ onResponse(false);
2783
+ }
2784
+ });
2785
+ const projectName = repoRoot.split("/").pop() || repoRoot;
2786
+ return /* @__PURE__ */ jsxs17(Box17, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "cyan", children: [
2787
+ /* @__PURE__ */ jsx21(Text18, { bold: true, color: "cyan", children: "Project Indexing Setup \u{1F4CA}" }),
2788
+ /* @__PURE__ */ jsxs17(Box17, { marginTop: 1, children: [
2789
+ /* @__PURE__ */ jsx21(Text18, { children: "Would you like to enable semantic indexing for " }),
2790
+ /* @__PURE__ */ jsx21(Text18, { bold: true, color: "white", children: projectName }),
2791
+ /* @__PURE__ */ jsx21(Text18, { children: "?" })
2792
+ ] }),
2793
+ /* @__PURE__ */ jsxs17(Box17, { marginTop: 1, flexDirection: "column", children: [
2794
+ /* @__PURE__ */ jsx21(Text18, { color: "gray", children: "This allows Dodo to understand your codebase deeply for:" }),
2795
+ /* @__PURE__ */ jsx21(Text18, { color: "gray", children: " \u2022 Semantic code search" }),
2796
+ /* @__PURE__ */ jsx21(Text18, { color: "gray", children: " \u2022 Context-aware suggestions" }),
2797
+ /* @__PURE__ */ jsx21(Text18, { color: "gray", children: " \u2022 Better code understanding" })
2798
+ ] }),
2799
+ /* @__PURE__ */ jsxs17(Box17, { marginTop: 1, children: [
2800
+ /* @__PURE__ */ jsx21(Text18, { color: "gray", children: "You can add custom rules in " }),
2801
+ /* @__PURE__ */ jsx21(Text18, { color: "yellow", children: ".dodo/rules" })
2802
+ ] }),
2803
+ /* @__PURE__ */ jsxs17(Box17, { marginTop: 1, children: [
2804
+ /* @__PURE__ */ jsx21(Text18, { color: "green", bold: true, children: "(Y)" }),
2805
+ /* @__PURE__ */ jsx21(Text18, { children: "es / " }),
2806
+ /* @__PURE__ */ jsx21(Text18, { color: "red", bold: true, children: "(N)" }),
2807
+ /* @__PURE__ */ jsx21(Text18, { children: "o" })
2808
+ ] })
2809
+ ] });
2810
+ };
2811
+
2812
+ // src/components/SessionPicker.tsx
2813
+ import { useState as useState10, useEffect as useEffect9 } from "react";
2814
+ import { Box as Box18, Text as Text19, useApp as useApp5, useInput as useInput5 } from "ink";
2815
+ import fs3 from "fs";
2816
+ import path2 from "path";
2817
+ import os from "os";
2818
+ import crypto from "crypto";
2819
+ import { jsx as jsx22, jsxs as jsxs18 } from "react/jsx-runtime";
2820
+ var formatDate = (date) => {
2821
+ const now = /* @__PURE__ */ new Date();
2822
+ const diff = now.getTime() - date.getTime();
2823
+ const days = Math.floor(diff / (1e3 * 60 * 60 * 24));
2824
+ if (days === 0) {
2825
+ return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
2826
+ } else if (days === 1) {
2827
+ return "Yesterday";
2828
+ } else {
2829
+ return date.toLocaleDateString();
2830
+ }
2831
+ };
2832
+ var SessionPicker = ({ repoPath, onSelect }) => {
2833
+ const [sessions, setSessions] = useState10([]);
2834
+ const [selectedIndex, setSelectedIndex] = useState10(0);
2835
+ const [loading, setLoading] = useState10(true);
2836
+ useEffect9(() => {
2837
+ const loadSessions = async () => {
2838
+ try {
2839
+ const hash = crypto.createHash("sha256").update(path2.resolve(repoPath)).digest("hex").substring(0, 12);
2840
+ const sessionDir = path2.join(os.homedir(), ".dodo", "sessions", hash);
2841
+ if (!fs3.existsSync(sessionDir)) {
2842
+ setSessions([]);
2843
+ return;
2844
+ }
2845
+ const files = fs3.readdirSync(sessionDir);
2846
+ const loadedSessions = [];
2847
+ for (const file of files) {
2848
+ if (!file.endsWith(".json")) continue;
2849
+ try {
2850
+ const content = fs3.readFileSync(path2.join(sessionDir, file), "utf-8");
2851
+ const data = JSON.parse(content);
2852
+ loadedSessions.push({
2853
+ id: data.id,
2854
+ title: data.title || "Untitled Session",
2855
+ updatedAt: new Date(data.updated_at),
2856
+ summary: data.summary
2857
+ });
2858
+ } catch (e) {
2859
+ }
2860
+ }
2861
+ loadedSessions.sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime());
2862
+ setSessions(loadedSessions);
2863
+ if (loadedSessions.length === 0) {
2864
+ onSelect(void 0);
2865
+ return;
2866
+ }
2867
+ } catch (err) {
2868
+ } finally {
2869
+ setLoading(false);
2870
+ }
2871
+ };
2872
+ loadSessions();
2873
+ }, [repoPath, onSelect]);
2874
+ const options = [
2875
+ { id: "new", title: "+ Start New Session", updatedAt: /* @__PURE__ */ new Date(), summary: "" },
2876
+ ...sessions
2877
+ ];
2878
+ const { exit } = useApp5();
2879
+ useInput5((input, key) => {
2880
+ if (loading) return;
2881
+ if (key.upArrow) {
2882
+ setSelectedIndex((prev) => Math.max(0, prev - 1));
2883
+ }
2884
+ if (key.downArrow) {
2885
+ setSelectedIndex((prev) => Math.min(options.length - 1, prev + 1));
2886
+ }
2887
+ if (key.return) {
2888
+ if (options[selectedIndex]) {
2889
+ const selected = options[selectedIndex];
2890
+ onSelect(selected.id === "new" ? void 0 : selected.id);
2891
+ }
2892
+ }
2893
+ if (input === "q" || key.ctrl && input === "c") {
2894
+ exit();
2895
+ }
2896
+ }, { isActive: !loading });
2897
+ if (loading) {
2898
+ return /* @__PURE__ */ jsx22(Box18, { padding: 1, children: /* @__PURE__ */ jsx22(Text19, { children: "Loading sessions..." }) });
2899
+ }
2900
+ return /* @__PURE__ */ jsxs18(Box18, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "blue", children: [
2901
+ /* @__PURE__ */ jsx22(Box18, { marginBottom: 1, children: /* @__PURE__ */ jsx22(Text19, { bold: true, color: "cyan", children: "Select a Session" }) }),
2902
+ options.map((option, index) => {
2903
+ const isSelected = index === selectedIndex;
2904
+ return /* @__PURE__ */ jsxs18(Box18, { flexDirection: "column", marginBottom: 0, children: [
2905
+ /* @__PURE__ */ jsxs18(Box18, { children: [
2906
+ /* @__PURE__ */ jsx22(Text19, { color: isSelected ? "green" : void 0, children: isSelected ? "> " : " " }),
2907
+ /* @__PURE__ */ jsx22(Text19, { bold: isSelected, color: isSelected ? "white" : "gray", children: option.title }),
2908
+ /* @__PURE__ */ jsx22(Box18, { marginLeft: 2, children: option.id !== "new" && /* @__PURE__ */ jsxs18(Text19, { color: "gray", children: [
2909
+ "(",
2910
+ formatDate(option.updatedAt),
2911
+ ")"
2912
+ ] }) })
2913
+ ] }),
2914
+ isSelected && option.summary && /* @__PURE__ */ jsx22(Box18, { marginLeft: 2, marginBottom: 1, children: /* @__PURE__ */ jsxs18(Text19, { color: "gray", italic: true, children: [
2915
+ "\u2514\u2500 ",
2916
+ option.summary.length > 80 ? option.summary.substring(0, 80) + "..." : option.summary
2917
+ ] }) })
2918
+ ] }, option.id);
2919
+ }),
2920
+ /* @__PURE__ */ jsxs18(Box18, { marginTop: 1, children: [
2921
+ /* @__PURE__ */ jsx22(Text19, { color: "gray", children: "Use UP/DOWN to navigate, ENTER to select" }),
2922
+ /* @__PURE__ */ jsx22(Text19, { color: "gray", children: "Press 'q' or 'Ctrl+C' to quit" })
2923
+ ] })
2924
+ ] });
2925
+ };
2926
+
2927
+ // src/components/modal/HelpModal.tsx
2928
+ import { Box as Box19, Text as Text20, useInput as useInput6 } from "ink";
2929
+ import { jsx as jsx23, jsxs as jsxs19 } from "react/jsx-runtime";
2930
+ var SECTIONS = [
2931
+ {
2932
+ title: "System Commands",
2933
+ items: [
2934
+ { command: "/help", description: "Show this help menu" },
2935
+ { command: "/exit", description: "Exit the application" },
2936
+ { command: "/clear", description: "Clear conversation history" },
2937
+ { command: "/stop", description: "Stop current running task" }
2938
+ ]
2939
+ },
2940
+ {
2941
+ title: "Agent Capabilities",
2942
+ items: [
2943
+ { command: "Run Tests", description: "Agent can run project tests" },
2944
+ { command: "Edit Files", description: "Agent can search & edit code" },
2945
+ { command: "Search", description: "Agent can search codebase" }
2946
+ ]
2947
+ },
2948
+ {
2949
+ title: "Keyboard Shortcuts",
2950
+ items: [
2951
+ { command: "Esc", description: "Close modal / cancel task" },
2952
+ { command: "Ctrl+C", description: "Force exit application" },
2953
+ { command: "\u2191 / \u2193", description: "Navigate history" },
2954
+ { command: "F1", description: "Toggle Help" }
2955
+ ]
2956
+ }
2957
+ ];
2958
+ var HelpModal = ({ onClose }) => {
2959
+ useInput6((_input, key) => {
2960
+ if (key.escape) {
2961
+ onClose();
2962
+ }
2963
+ }, { isActive: true });
2964
+ return /* @__PURE__ */ jsxs19(
2965
+ Box19,
2966
+ {
2967
+ borderStyle: "double",
2968
+ borderColor: "cyan",
2969
+ paddingX: 2,
2970
+ paddingY: 1,
2971
+ flexDirection: "column",
2972
+ alignSelf: "center",
2973
+ children: [
2974
+ /* @__PURE__ */ jsx23(Box19, { marginBottom: 1, justifyContent: "center", children: /* @__PURE__ */ jsx23(Text20, { bold: true, color: "cyan", children: "Dodo Help & Commands" }) }),
2975
+ SECTIONS.map((section, idx) => /* @__PURE__ */ jsxs19(Box19, { flexDirection: "column", marginBottom: idx === SECTIONS.length - 1 ? 0 : 1, children: [
2976
+ /* @__PURE__ */ jsx23(Text20, { bold: true, underline: true, color: "white", children: section.title }),
2977
+ section.items.map((item) => /* @__PURE__ */ jsxs19(Box19, { marginLeft: 2, children: [
2978
+ /* @__PURE__ */ jsx23(Box19, { width: 15, children: /* @__PURE__ */ jsx23(Text20, { color: "green", children: item.command }) }),
2979
+ /* @__PURE__ */ jsx23(Text20, { children: item.description })
2980
+ ] }, item.command))
2981
+ ] }, section.title)),
2982
+ /* @__PURE__ */ jsx23(Box19, { marginTop: 1, justifyContent: "center", borderStyle: "single", borderTop: false, borderLeft: false, borderRight: false, borderBottom: true, borderColor: "gray" }),
2983
+ /* @__PURE__ */ jsx23(Box19, { marginTop: 1, justifyContent: "center", children: /* @__PURE__ */ jsxs19(Text20, { color: "gray", children: [
2984
+ "Press ",
2985
+ /* @__PURE__ */ jsx23(Text20, { bold: true, color: "white", children: "ESC" }),
2986
+ " to close"
2987
+ ] }) })
2988
+ ]
2989
+ }
2990
+ );
2991
+ };
2992
+
2993
+ // src/hooks/useHistory.ts
2994
+ import { useState as useState11, useCallback as useCallback8 } from "react";
2995
+ function useHistory() {
2996
+ const [history, setHistory] = useState11([]);
2997
+ const [historyIndex, setHistoryIndex] = useState11(-1);
2998
+ const addToHistory = useCallback8((command) => {
2999
+ setHistory((prev) => {
3000
+ const last = prev[prev.length - 1];
3001
+ if (last !== command) {
3002
+ return [...prev, command];
3003
+ }
3004
+ return prev;
3005
+ });
3006
+ setHistoryIndex(-1);
3007
+ }, []);
3008
+ const resetHistoryIndex = useCallback8(() => setHistoryIndex(-1), []);
3009
+ const navigateHistory = useCallback8(
3010
+ (direction) => {
3011
+ let newIndex = 0;
3012
+ let returnValue = void 0;
3013
+ setHistoryIndex((prev) => {
3014
+ newIndex = prev;
3015
+ if (direction === "up") {
3016
+ newIndex = Math.min(prev + 1, history.length - 1);
3017
+ } else {
3018
+ newIndex = Math.max(prev - 1, -1);
3019
+ }
3020
+ return newIndex;
3021
+ });
3022
+ },
3023
+ [history]
3024
+ );
3025
+ const navigate = useCallback8((direction, currentHistoryIndex, currentHistory) => {
3026
+ let newIndex = currentHistoryIndex;
3027
+ if (direction === "up") {
3028
+ newIndex = Math.min(currentHistoryIndex + 1, currentHistory.length - 1);
3029
+ } else {
3030
+ newIndex = Math.max(currentHistoryIndex - 1, -1);
3031
+ }
3032
+ let newValue = void 0;
3033
+ if (newIndex >= 0 && newIndex < currentHistory.length) {
3034
+ newValue = currentHistory[currentHistory.length - 1 - newIndex];
3035
+ }
3036
+ return { index: newIndex, value: newValue };
3037
+ }, []);
3038
+ return {
3039
+ history,
3040
+ historyIndex,
3041
+ addToHistory,
3042
+ navigateHistory: (direction) => {
3043
+ const res = navigate(direction, historyIndex, history);
3044
+ setHistoryIndex(res.index);
3045
+ return res;
3046
+ },
3047
+ resetHistoryIndex
3048
+ };
3049
+ }
3050
+
3051
+ // src/hooks/useTerminal.ts
3052
+ import { useState as useState12, useEffect as useEffect10 } from "react";
3053
+
3054
+ // src/utils/ansi.ts
3055
+ var ANSI = {
3056
+ // Screen / Buffer
3057
+ ALTERNATE_BUFFER_ENTER: "\x1B[?1049h",
3058
+ ALTERNATE_BUFFER_EXIT: "\x1B[?1049l",
3059
+ CLEAR_SCREEN: "\x1B[2J",
3060
+ CURSOR_HOME: "\x1B[H",
3061
+ // Mouse Tracking
3062
+ MOUSE_TRACKING_ENABLE: "\x1B[?1002h\x1B[?1006h",
3063
+ MOUSE_TRACKING_DISABLE: "\x1B[?1006l\x1B[?1002l",
3064
+ // Bracketed Paste
3065
+ BRACKETED_PASTE_ENABLE: "\x1B[?2004h",
3066
+ BRACKETED_PASTE_DISABLE: "\x1B[?2004l"
3067
+ };
3068
+
3069
+ // src/hooks/useTerminal.ts
3070
+ function useTerminal(options = {}) {
3071
+ const { enableAlternateBuffer = true, defaultRows = 24 } = options;
3072
+ const [terminalRows, setTerminalRows] = useState12(() => {
3073
+ return process.stdout?.rows || defaultRows;
3074
+ });
3075
+ const [terminalColumns, setTerminalColumns] = useState12(() => {
3076
+ return process.stdout?.columns || 80;
3077
+ });
3078
+ useEffect10(() => {
3079
+ const updateDimensions = () => {
3080
+ if (process.stdout?.rows) {
3081
+ setTerminalRows(process.stdout.rows);
3082
+ }
3083
+ if (process.stdout?.columns) {
3084
+ setTerminalColumns(process.stdout.columns);
3085
+ }
3086
+ };
3087
+ process.stdout?.on("resize", updateDimensions);
3088
+ updateDimensions();
3089
+ return () => {
3090
+ process.stdout?.off("resize", updateDimensions);
3091
+ };
3092
+ }, []);
3093
+ useEffect10(() => {
3094
+ if (!enableAlternateBuffer || !process.stdout?.write) return;
3095
+ process.stdout.write(ANSI.ALTERNATE_BUFFER_ENTER);
3096
+ process.stdout.write(ANSI.BRACKETED_PASTE_ENABLE);
3097
+ process.stdout.write(ANSI.CLEAR_SCREEN + ANSI.CURSOR_HOME);
3098
+ return () => {
3099
+ process.stdout.write(ANSI.BRACKETED_PASTE_DISABLE);
3100
+ process.stdout.write(ANSI.ALTERNATE_BUFFER_EXIT);
3101
+ };
3102
+ }, [enableAlternateBuffer]);
3103
+ return { terminalRows, terminalColumns };
3104
+ }
3105
+
3106
+ // src/hooks/useCommandProcessor.ts
3107
+ import { useCallback as useCallback9 } from "react";
3108
+
3109
+ // src/utils/diagnostics.ts
3110
+ import fs4 from "fs";
3111
+ function generateDiagnostics(params) {
3112
+ return {
3113
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3114
+ session: {
3115
+ id: params.sessionId,
3116
+ status: params.status,
3117
+ isRunning: params.isRunning
3118
+ },
3119
+ config: params.loadedConfig,
3120
+ environment: {
3121
+ LLM_PROVIDER: process.env.LLM_PROVIDER,
3122
+ DODO_DEBUG: process.env.DODO_DEBUG,
3123
+ NODE_ENV: process.env.NODE_ENV
3124
+ },
3125
+ componentStates: {
3126
+ isSetupRequired: params.isSetupRequired,
3127
+ isUpdateMode: params.isUpdateMode
3128
+ }
3129
+ };
3130
+ }
3131
+ function writeDiagnostics(diagnostics) {
3132
+ const filePath = "/tmp/dodo_diagnostics.json";
3133
+ try {
3134
+ fs4.writeFileSync(filePath, JSON.stringify(diagnostics, null, 2));
3135
+ return filePath;
3136
+ } catch (error) {
3137
+ return `Error writing diagnostics: ${error}`;
3138
+ }
3139
+ }
3140
+
3141
+ // src/hooks/useCommandProcessor.ts
3142
+ function useCommandProcessor(ctx) {
3143
+ const processCommand = useCallback9((input) => {
3144
+ const trimmed = input.trim();
3145
+ if (!trimmed.startsWith("/")) return false;
3146
+ if (trimmed === "/help") {
3147
+ ctx.setActiveModal("help");
3148
+ ctx.setInput("");
3149
+ return true;
3150
+ }
3151
+ if (trimmed === "/exit" || trimmed === "/quit") {
3152
+ process.stdout.write("\x1B[?1049l");
3153
+ process.exit(0);
3154
+ return true;
3155
+ }
3156
+ if (trimmed === "/clear") {
3157
+ ctx.clearTurns();
3158
+ ctx.setInput("");
3159
+ return true;
3160
+ }
3161
+ if (trimmed === "/debug") {
3162
+ const diagnostics = generateDiagnostics({
3163
+ sessionId: ctx.sessionId,
3164
+ status: ctx.status,
3165
+ isRunning: ctx.isRunning,
3166
+ loadedConfig: ctx.loadedConfig,
3167
+ isSetupRequired: ctx.isSetupRequired,
3168
+ isUpdateMode: ctx.isUpdateMode
3169
+ });
3170
+ const filePath = writeDiagnostics(diagnostics);
3171
+ debugLog.command("App", "/debug", { outputPath: filePath });
3172
+ ctx.setInput("");
3173
+ return true;
3174
+ }
3175
+ if (trimmed === "/stop") {
3176
+ if (ctx.isRunning) {
3177
+ ctx.cancelRequest();
3178
+ debugLog.command("App", "/stop", { sessionId: ctx.sessionId });
3179
+ }
3180
+ ctx.setInput("");
3181
+ return true;
3182
+ }
3183
+ if (trimmed === "/configure") {
3184
+ ctx.setIsUpdateMode(true);
3185
+ ctx.setIsSetupRequired(true);
3186
+ ctx.sendCommand({ type: "get_config" });
3187
+ ctx.setInput("");
3188
+ return true;
3189
+ }
3190
+ return false;
3191
+ return false;
3192
+ }, [ctx]);
3193
+ return { processCommand };
3194
+ }
3195
+
3196
+ // src/ui/app.tsx
3197
+ import { jsx as jsx24 } from "react/jsx-runtime";
3198
+ debugLog.lifecycle("App", "mount", "module loaded");
3199
+ var App = ({
3200
+ client,
3201
+ repoPath,
3202
+ requestedSessionId,
3203
+ engineCommand,
3204
+ engineExited
3205
+ }) => {
3206
+ const [input, setInput] = useState13("");
3207
+ const [activeModal, setActiveModal] = useState13("none");
3208
+ const [targetSessionId, setTargetSessionId] = useState13(requestedSessionId);
3209
+ const [isSessionSelected, setIsSessionSelected] = useState13(!!requestedSessionId);
3210
+ useEffect11(() => {
3211
+ if (isSessionSelected && process.stdout?.write) {
3212
+ process.stdout.write("\x1B[2J\x1B[H");
3213
+ }
3214
+ }, [isSessionSelected]);
3215
+ const { history, historyIndex, addToHistory, navigateHistory, resetHistoryIndex } = useHistory();
3216
+ const { terminalRows, terminalColumns } = useTerminal({ enableAlternateBuffer: false });
3217
+ const {
3218
+ sessionId,
3219
+ status,
3220
+ infoMessage,
3221
+ isRunning,
3222
+ error,
3223
+ tokenUsage,
3224
+ projectPlan,
3225
+ showProjectPlan,
3226
+ errorCount,
3227
+ currentThought,
3228
+ turns,
3229
+ currentTimelineSteps,
3230
+ currentRunningStepId,
3231
+ toggleTurnCollapsed,
3232
+ submitQuery,
3233
+ sendCommand,
3234
+ isSetupRequired,
3235
+ setIsSetupRequired,
3236
+ isProjectPermissionRequired,
3237
+ setIsProjectPermissionRequired,
3238
+ pendingRepoRoot,
3239
+ reloadSession,
3240
+ loadedConfig,
3241
+ cancelRequest,
3242
+ clearTurns
3243
+ } = useEngineConnection(client, repoPath, targetSessionId, engineExited, !isSessionSelected);
3244
+ const canSubmit = Boolean(sessionId) && !isRunning && status !== "connecting" && status !== "disconnected";
3245
+ const { exit } = useApp6();
3246
+ const [isUpdateMode, setIsUpdateMode] = useState13(false);
3247
+ useEffect11(() => {
3248
+ if (!isSetupRequired) {
3249
+ setIsUpdateMode(false);
3250
+ }
3251
+ }, [isSetupRequired]);
3252
+ const { processCommand } = useCommandProcessor({
3253
+ sessionId,
3254
+ status,
3255
+ isRunning,
3256
+ loadedConfig,
3257
+ isSetupRequired,
3258
+ isUpdateMode,
3259
+ clearTurns,
3260
+ cancelRequest,
3261
+ sendCommand,
3262
+ setIsSetupRequired,
3263
+ setIsUpdateMode,
3264
+ setActiveModal,
3265
+ setInput
3266
+ });
3267
+ const handleSubmit = useCallback10(async () => {
3268
+ if (!canSubmit) return;
3269
+ const trimmed = input.trim();
3270
+ if (!trimmed) return;
3271
+ const handled = processCommand(trimmed);
3272
+ if (handled) return;
3273
+ addToHistory(trimmed);
3274
+ await submitQuery(trimmed);
3275
+ setInput("");
3276
+ }, [canSubmit, input, submitQuery, addToHistory, processCommand]);
3277
+ const navigate = (dir) => {
3278
+ const res = navigateHistory(dir);
3279
+ if (res.index === -1) {
3280
+ setInput("");
3281
+ } else if (res.value !== void 0) {
3282
+ setInput(res.value);
3283
+ }
3284
+ };
3285
+ if (!isSessionSelected) {
3286
+ return /* @__PURE__ */ jsx24(AnimationProvider, { children: /* @__PURE__ */ jsx24(Box20, { flexDirection: "column", height: terminalRows, width: "100%", justifyContent: "center", alignItems: "center", children: /* @__PURE__ */ jsx24(
3287
+ SessionPicker,
3288
+ {
3289
+ repoPath,
3290
+ onSelect: (id) => {
3291
+ setTargetSessionId(id);
3292
+ setIsSessionSelected(true);
3293
+ }
3294
+ }
3295
+ ) }) });
3296
+ }
3297
+ if (isProjectPermissionRequired && sessionId) {
3298
+ return /* @__PURE__ */ jsx24(AnimationProvider, { children: /* @__PURE__ */ jsx24(Box20, { flexDirection: "column", height: terminalRows, width: "100%", justifyContent: "center", alignItems: "center", children: /* @__PURE__ */ jsx24(
3299
+ ProjectPermissionPrompt,
3300
+ {
3301
+ repoRoot: pendingRepoRoot || repoPath,
3302
+ onResponse: (enabled) => {
3303
+ process.stdout.write("\x1B[2J\x1B[H");
3304
+ sendCommand({
3305
+ type: "project_permission",
3306
+ session_id: sessionId,
3307
+ indexing_enabled: enabled
3308
+ });
3309
+ setIsProjectPermissionRequired(false);
3310
+ }
3311
+ }
3312
+ ) }) });
3313
+ }
3314
+ if (isSetupRequired) {
3315
+ return /* @__PURE__ */ jsx24(AnimationProvider, { children: /* @__PURE__ */ jsx24(Box20, { flexDirection: "column", height: terminalRows, width: "100%", justifyContent: "center", alignItems: "center", children: /* @__PURE__ */ jsx24(
3316
+ SetupWizard,
3317
+ {
3318
+ isUpdate: isUpdateMode,
3319
+ initialConfig: loadedConfig,
3320
+ sendCommand: (type, payload) => {
3321
+ if (type === "save_config") {
3322
+ sendCommand({
3323
+ type: "save_config",
3324
+ config: payload
3325
+ });
3326
+ }
3327
+ },
3328
+ onComplete: () => {
3329
+ debugLog.lifecycle("App", "update", "SetupWizard onComplete");
3330
+ setIsSetupRequired(false);
3331
+ setIsUpdateMode(false);
3332
+ debugLog.command("App", "reloadSession", { sessionId });
3333
+ reloadSession();
3334
+ }
3335
+ }
3336
+ ) }) });
3337
+ }
3338
+ return /* @__PURE__ */ jsx24(AnimationProvider, { children: /* @__PURE__ */ jsx24(SessionProvider, { value: {
3339
+ sessionId,
3340
+ status,
3341
+ infoMessage,
3342
+ isRunning,
3343
+ error,
3344
+ tokenUsage,
3345
+ errorCount,
3346
+ currentThought,
3347
+ loadedConfig,
3348
+ isSetupRequired
3349
+ }, children: /* @__PURE__ */ jsx24(
3350
+ AppLayout,
3351
+ {
3352
+ terminalRows,
3353
+ terminalColumns,
3354
+ error,
3355
+ showProjectPlan,
3356
+ projectPlan,
3357
+ currentTimelineSteps,
3358
+ currentRunningStepId,
3359
+ isRunning,
3360
+ footerProps: {
3361
+ input,
3362
+ onChange: (val) => {
3363
+ setInput(val);
3364
+ resetHistoryIndex();
3365
+ },
3366
+ onSubmit: handleSubmit,
3367
+ repoLabel: repoPath ? process.cwd() === repoPath ? path3.basename(repoPath) : repoPath : "No Repo",
3368
+ // Pass history handlers to Footer
3369
+ onHistoryUp: () => navigate("up"),
3370
+ onHistoryDown: () => navigate("down")
3371
+ },
3372
+ onCancelRequest: cancelRequest,
3373
+ helpModal: activeModal === "help" ? /* @__PURE__ */ jsx24(HelpModal, { onClose: () => setActiveModal("none") }) : void 0,
3374
+ onHelp: () => setActiveModal("help")
3375
+ }
3376
+ ) }) });
3377
+ };
3378
+ var app_default = App;
3379
+
3380
+ // src/engineClient.ts
3381
+ import { EventEmitter } from "events";
3382
+ import readline from "readline";
3383
+
3384
+ // src/protocol.ts
3385
+ var serializeCommand = (command) => {
3386
+ return JSON.stringify(command);
3387
+ };
3388
+
3389
+ // src/engineClient.ts
3390
+ var EngineClient = class extends EventEmitter {
3391
+ constructor(stdin, stdout) {
3392
+ super();
3393
+ this.stdin = stdin;
3394
+ this.closed = false;
3395
+ debugLog.state("EngineClient", "initialized");
3396
+ this.rl = readline.createInterface({ input: stdout });
3397
+ this.rl.on("line", (line) => {
3398
+ if (!line.trim() || this.closed) {
3399
+ return;
3400
+ }
3401
+ debugLog.log({ category: "traffic", component: "EngineClient", message: "IN", data: { line } });
3402
+ try {
3403
+ const evt = JSON.parse(line);
3404
+ this.handleEvent(evt);
3405
+ this.emit("event", evt);
3406
+ } catch (err) {
3407
+ const error = err instanceof Error ? err : new Error(`Failed to parse engine event: ${String(err)}`);
3408
+ debugLog.error("EngineClient", "ParseError", error);
3409
+ this.emit("error", error);
3410
+ }
3411
+ });
3412
+ this.rl.on("close", () => {
3413
+ this.closed = true;
3414
+ debugLog.state("EngineClient", "closed");
3415
+ if (this.pendingSession) {
3416
+ const err = new Error("Engine closed before session became ready");
3417
+ this.pendingSession.reject(err);
3418
+ this.pendingSession = void 0;
3419
+ }
3420
+ this.emit("close");
3421
+ });
3422
+ }
3423
+ /**
3424
+ * Logs a user-facing issue or error.
3425
+ */
3426
+ logIssue(message) {
3427
+ debugLog.error("EngineClient", "Issue", new Error(message));
3428
+ }
3429
+ /**
3430
+ * Closes the client and the underlying readline interface.
3431
+ */
3432
+ close() {
3433
+ if (this.closed) return;
3434
+ this.closed = true;
3435
+ if (this.rl) {
3436
+ this.rl.close();
3437
+ this.rl = void 0;
3438
+ }
3439
+ }
3440
+ /**
3441
+ * Starts a new session with the engine.
3442
+ * Resolves when the session is ready.
3443
+ */
3444
+ async startSession(opts) {
3445
+ if (this.pendingSession) {
3446
+ throw new Error("Session already starting");
3447
+ }
3448
+ if (!opts.sessionId) {
3449
+ this.currentSessionId = void 0;
3450
+ }
3451
+ const command = {
3452
+ type: "start_session",
3453
+ session_id: opts.sessionId,
3454
+ repo_root: opts.repoRoot,
3455
+ meta: {}
3456
+ };
3457
+ debugLog.command("EngineClient", "start_session", command);
3458
+ const sessionId = await new Promise((resolve, reject) => {
3459
+ this.pendingSession = { resolve, reject };
3460
+ this.sendCommand(command).catch((err) => {
3461
+ this.pendingSession = void 0;
3462
+ reject(err);
3463
+ });
3464
+ });
3465
+ this.currentSessionId = sessionId;
3466
+ return sessionId;
3467
+ }
3468
+ /**
3469
+ * Sends a user message to the engine.
3470
+ */
3471
+ sendUserMessage(sessionId, message) {
3472
+ const command = {
3473
+ type: "user_message",
3474
+ session_id: sessionId,
3475
+ message
3476
+ };
3477
+ debugLog.command("EngineClient", "user_message", { sessionId });
3478
+ void this.sendCommand(command);
3479
+ }
3480
+ /**
3481
+ * Returns the current session ID, if any.
3482
+ */
3483
+ getSessionId() {
3484
+ return this.currentSessionId;
3485
+ }
3486
+ /**
3487
+ * Low-level method to send a command object.
3488
+ */
3489
+ async sendCommand(command) {
3490
+ if (this.closed) {
3491
+ throw new Error("Engine client is closed");
3492
+ }
3493
+ const payload = serializeCommand(command);
3494
+ debugLog.log({ category: "traffic", component: "EngineClient", message: "OUT", data: { payload } });
3495
+ await new Promise((resolve, reject) => {
3496
+ this.stdin.write(payload + "\n", (err) => {
3497
+ if (err) {
3498
+ debugLog.error("EngineClient", "WriteError", err);
3499
+ reject(err);
3500
+ } else {
3501
+ resolve();
3502
+ }
3503
+ });
3504
+ });
3505
+ }
3506
+ handleEvent(event) {
3507
+ if (event.type === "status") {
3508
+ this.handleStatusEvent(event);
3509
+ }
3510
+ }
3511
+ handleStatusEvent(event) {
3512
+ if (event.status === "session_ready" && event.session_id) {
3513
+ if (this.pendingSession) {
3514
+ debugLog.state("EngineClient", "session_ready", { sessionId: event.session_id });
3515
+ this.pendingSession.resolve(event.session_id);
3516
+ this.pendingSession = void 0;
3517
+ }
3518
+ }
3519
+ }
3520
+ };
3521
+
3522
+ // src/components/common/ErrorBoundary.tsx
3523
+ import { Component } from "react";
3524
+ import { Box as Box21, Text as Text21 } from "ink";
3525
+ import { jsx as jsx25, jsxs as jsxs20 } from "react/jsx-runtime";
3526
+ var ErrorBoundary = class extends Component {
3527
+ constructor() {
3528
+ super(...arguments);
3529
+ this.state = {
3530
+ hasError: false,
3531
+ error: null
3532
+ };
3533
+ }
3534
+ static getDerivedStateFromError(error) {
3535
+ return { hasError: true, error };
3536
+ }
3537
+ componentDidCatch(error, errorInfo) {
3538
+ logger.error("Uncaught error in UI:", {
3539
+ message: error.message,
3540
+ stack: error.stack,
3541
+ componentStack: errorInfo.componentStack
3542
+ });
3543
+ }
3544
+ render() {
3545
+ if (this.state.hasError) {
3546
+ return /* @__PURE__ */ jsxs20(Box21, { flexDirection: "column", padding: 1, borderColor: "red", borderStyle: "round", children: [
3547
+ /* @__PURE__ */ jsx25(Text21, { color: "red", bold: true, children: "Something went wrong." }),
3548
+ /* @__PURE__ */ jsx25(Text21, { color: "red", children: this.state.error?.message }),
3549
+ /* @__PURE__ */ jsx25(Text21, { color: "gray", children: "Check ui_debug.log for details." })
3550
+ ] });
3551
+ }
3552
+ return this.props.children;
3553
+ }
3554
+ };
3555
+
3556
+ // src/index.tsx
3557
+ import { jsx as jsx26 } from "react/jsx-runtime";
3558
+ async function main() {
3559
+ const parseArgs = (argv) => {
3560
+ const result = {};
3561
+ for (let i = 2; i < argv.length; i++) {
3562
+ const arg = argv[i];
3563
+ if (!arg.startsWith("--")) {
3564
+ continue;
3565
+ }
3566
+ const key = arg.slice(2);
3567
+ const next = argv[i + 1];
3568
+ if (!next || next.startsWith("--")) {
3569
+ result[key] = true;
3570
+ } else {
3571
+ result[key] = next;
3572
+ i++;
3573
+ }
3574
+ }
3575
+ return result;
3576
+ };
3577
+ const args = parseArgs(process2.argv);
3578
+ const repoPath = path4.resolve(
3579
+ typeof args.repo === "string" ? args.repo : process2.cwd()
3580
+ );
3581
+ const engineCwd = path4.resolve(
3582
+ typeof args["engine-cwd"] === "string" ? args["engine-cwd"] : path4.resolve(process2.cwd(), "..")
3583
+ );
3584
+ const enginePath = typeof args.engine === "string" ? args.engine : void 0;
3585
+ const engineAddr = typeof args["engine-addr"] === "string" ? args["engine-addr"] : void 0;
3586
+ const requestedSessionId = typeof args["session-id"] === "string" ? args["session-id"] : void 0;
3587
+ const logFile = typeof args["log-file"] === "string" ? args["log-file"] : void 0;
3588
+ if (logFile) {
3589
+ logger.log(`Logging started. Repository: ${repoPath}`);
3590
+ }
3591
+ if (!fs5.existsSync(repoPath)) {
3592
+ console.error("\n\u274C Error: Repository path does not exist");
3593
+ console.error(` Path: ${repoPath}
3594
+ `);
3595
+ console.error("Usage:");
3596
+ console.error(" npm run dev -- --repo <path-to-your-repo> --engine <path-to-dodo-binary> [--log-file <path>]\n");
3597
+ console.error("Example:");
3598
+ console.error(" npm run dev -- --repo ../../dodo_tasks/my-project --engine ../repl --log-file debug.log\n");
3599
+ process2.exit(1);
3600
+ }
3601
+ const repoStat = fs5.statSync(repoPath);
3602
+ if (!repoStat.isDirectory()) {
3603
+ console.error("\n\u274C Error: Repository path is not a directory");
3604
+ console.error(` Path: ${repoPath}
3605
+ `);
3606
+ console.error("Please provide a valid directory path.\n");
3607
+ process2.exit(1);
3608
+ }
3609
+ let client;
3610
+ let childProcess;
3611
+ let engineExited;
3612
+ if (engineAddr) {
3613
+ try {
3614
+ fs5.writeFileSync("e2e_debug.log", `[${(/* @__PURE__ */ new Date()).toISOString()}] Index.tsx starting. EngineAddr: ${engineAddr}
3615
+ `);
3616
+ } catch (_) {
3617
+ }
3618
+ const [host, portStr] = engineAddr.split(":");
3619
+ const port = parseInt(portStr, 10);
3620
+ const socket = net.createConnection({ host, port });
3621
+ socket.on("connect", () => {
3622
+ try {
3623
+ fs5.appendFileSync("e2e_debug.log", `[${(/* @__PURE__ */ new Date()).toISOString()}] Socket connected to ${host}:${port}!
3624
+ `);
3625
+ } catch (_) {
3626
+ }
3627
+ });
3628
+ socket.on("error", (err) => {
3629
+ try {
3630
+ fs5.appendFileSync("e2e_debug.log", `[${(/* @__PURE__ */ new Date()).toISOString()}] Socket error: ${err.message}
3631
+ `);
3632
+ } catch (_) {
3633
+ }
3634
+ });
3635
+ socket.on("close", () => {
3636
+ try {
3637
+ fs5.appendFileSync("e2e_debug.log", `[${(/* @__PURE__ */ new Date()).toISOString()}] Socket closed.
3638
+ `);
3639
+ } catch (_) {
3640
+ }
3641
+ });
3642
+ client = new EngineClient(socket, socket);
3643
+ } else {
3644
+ const resolveEngineCommand = () => {
3645
+ const baseArgs = ["engine", "--stdio", "--repo", repoPath];
3646
+ if (enginePath) {
3647
+ const resolvedEnginePath = path4.resolve(enginePath);
3648
+ if (!fs5.existsSync(resolvedEnginePath)) {
3649
+ console.error("\n\u274C Error: Engine binary not found");
3650
+ console.error(` Path: ${resolvedEnginePath}
3651
+ `);
3652
+ console.error("Please build the engine first:");
3653
+ console.error(" cd <dodo-repo> && go build -o dodo ./cmd/repl\n");
3654
+ process2.exit(1);
3655
+ }
3656
+ return { command: resolvedEnginePath, args: baseArgs, cwd: path4.dirname(resolvedEnginePath) };
3657
+ }
3658
+ const envEnginePath = process2.env.DODO_ENGINE_PATH;
3659
+ if (envEnginePath && fs5.existsSync(envEnginePath)) {
3660
+ return { command: envEnginePath, args: baseArgs, cwd: path4.dirname(envEnginePath) };
3661
+ }
3662
+ const homeDir = process2.env.HOME || process2.env.USERPROFILE || "";
3663
+ const searchPaths = [
3664
+ path4.join(homeDir, ".local", "bin", "dodo"),
3665
+ path4.join(homeDir, "bin", "dodo"),
3666
+ "/usr/local/bin/dodo",
3667
+ path4.join(engineCwd, "dodo")
3668
+ ];
3669
+ for (const candidate of searchPaths) {
3670
+ if (fs5.existsSync(candidate)) {
3671
+ return { command: candidate, args: baseArgs, cwd: path4.dirname(candidate) };
3672
+ }
3673
+ }
3674
+ const dodoDir = process2.env.DODO_DIR || engineCwd;
3675
+ if (fs5.existsSync(path4.join(dodoDir, "cmd", "repl"))) {
3676
+ return { command: "go", args: ["run", "./cmd/repl", ...baseArgs], cwd: dodoDir };
3677
+ }
3678
+ console.error("\n\u274C Error: Could not find dodo engine");
3679
+ console.error("Options:");
3680
+ console.error(" 1. Set DODO_ENGINE_PATH=/path/to/dodo binary");
3681
+ console.error(" 2. Set DODO_DIR=/path/to/dodo/source (for go run)");
3682
+ console.error(" 3. Install dodo to ~/.local/bin/dodo\n");
3683
+ process2.exit(1);
3684
+ };
3685
+ const { command, args: engineArgs, cwd: engineWorkDir } = resolveEngineCommand();
3686
+ try {
3687
+ fs5.appendFileSync("/tmp/dodo_ui_debug.log", `${(/* @__PURE__ */ new Date()).toISOString()} Spawning engine: ${command} ${engineArgs.join(" ")} (cwd: ${engineWorkDir})
3688
+ `);
3689
+ } catch (_) {
3690
+ }
3691
+ childProcess = spawn(command, engineArgs, {
3692
+ cwd: engineWorkDir,
3693
+ stdio: ["pipe", "pipe", "pipe"]
3694
+ });
3695
+ childProcess.on("error", (err) => {
3696
+ try {
3697
+ fs5.appendFileSync("/tmp/dodo_ui_debug.log", `${(/* @__PURE__ */ new Date()).toISOString()} Engine spawn error: ${err.message}
3698
+ `);
3699
+ } catch (_) {
3700
+ }
3701
+ });
3702
+ if (!childProcess.stdin || !childProcess.stdout) {
3703
+ console.error("Failed to spawn engine: stdin/stdout missing");
3704
+ process2.exit(1);
3705
+ }
3706
+ childProcess.on("exit", (code, signal) => {
3707
+ engineExited = { code, signal };
3708
+ });
3709
+ if (childProcess.stderr) {
3710
+ childProcess.stderr.on("data", (data) => {
3711
+ const line = data.toString();
3712
+ try {
3713
+ fs5.appendFileSync("/tmp/dodo_backend_stderr.log", line);
3714
+ } catch (_) {
3715
+ }
3716
+ logger.log(`[BACKEND] ${line.trim()}`);
3717
+ });
3718
+ }
3719
+ client = new EngineClient(childProcess.stdin, childProcess.stdout);
3720
+ }
3721
+ const displayCommand = childProcess ? "Local Engine" : `Remote Engine (${engineAddr})`;
3722
+ const inkApp = render(
3723
+ /* @__PURE__ */ jsx26(ErrorBoundary, { children: /* @__PURE__ */ jsx26(ConversationProvider, { children: /* @__PURE__ */ jsx26(
3724
+ app_default,
3725
+ {
3726
+ client,
3727
+ repoPath,
3728
+ requestedSessionId,
3729
+ engineCommand: displayCommand
3730
+ }
3731
+ ) }) }),
3732
+ {
3733
+ stdin: process2.stdin,
3734
+ patchConsole: true
3735
+ }
3736
+ );
3737
+ const cleanup = (code = 0) => {
3738
+ process2.stdout.write("\x1B[?1049l");
3739
+ try {
3740
+ client.close();
3741
+ if (childProcess && !childProcess.killed) {
3742
+ childProcess.kill("SIGINT");
3743
+ }
3744
+ inkApp.unmount();
3745
+ } catch (err) {
3746
+ } finally {
3747
+ setTimeout(() => {
3748
+ process2.exit(code);
3749
+ }, 100);
3750
+ }
3751
+ };
3752
+ if (childProcess) {
3753
+ childProcess.on("exit", (code, signal) => {
3754
+ cleanup(code ?? (signal ? 1 : 0));
3755
+ });
3756
+ }
3757
+ process2.on("SIGINT", () => cleanup(0));
3758
+ process2.on("SIGTERM", () => cleanup(0));
3759
+ }
3760
+ main().catch((err) => {
3761
+ console.error("Fatal error:", err);
3762
+ process2.exit(1);
3763
+ });