@makefinks/daemon 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "module": "src/index.tsx",
30
30
  "type": "module",
31
- "version": "0.4.0",
31
+ "version": "0.5.0",
32
32
  "bin": {
33
33
  "daemon": "dist/cli.js"
34
34
  },
@@ -22,9 +22,10 @@ import type {
22
22
  ToolApprovalResponse,
23
23
  TranscriptionResult,
24
24
  } from "../types";
25
- import { debug } from "../utils/debug-logger";
25
+ import { debug, toolDebug } from "../utils/debug-logger";
26
26
  import { getOpenRouterReportedCost } from "../utils/openrouter-reported-cost";
27
27
  import { getWorkspacePath } from "../utils/workspace-manager";
28
+ import { extractFinalAssistantText } from "./message-utils";
28
29
  import { TRANSCRIPTION_MODEL, buildOpenRouterChatSettings, getResponseModel } from "./model-config";
29
30
  import { sanitizeMessagesForInput } from "./sanitize-messages";
30
31
  import { type InteractionMode, buildDaemonSystemPrompt } from "./system-prompt";
@@ -54,40 +55,6 @@ function normalizeStreamError(error: unknown): Error {
54
55
  return new Error(String(error));
55
56
  }
56
57
 
57
- /**
58
- * Extract the final text content from the last assistant message.
59
- * In multi-step agent loops, we only want to speak the final response, not intermediate text.
60
- */
61
- function extractFinalAssistantText(messages: ModelMessage[]): string {
62
- // Find the last assistant message
63
- for (let i = messages.length - 1; i >= 0; i--) {
64
- const msg = messages[i];
65
- if (msg?.role === "assistant") {
66
- const content = msg.content;
67
- if (Array.isArray(content)) {
68
- // Find the last text part. In some models/providers, intermediate
69
- // "thoughts" might be included as separate text blocks before the final answer.
70
- // We prioritize the last text block in the message for the final response.
71
- for (let j = content.length - 1; j >= 0; j--) {
72
- const part = content[j];
73
- if (
74
- part &&
75
- typeof part === "object" &&
76
- "type" in part &&
77
- part.type === "text" &&
78
- "text" in part &&
79
- typeof part.text === "string"
80
- ) {
81
- return part.text;
82
- }
83
- }
84
- // If this assistant message had no text parts, continue searching previous messages
85
- }
86
- }
87
- }
88
- return "";
89
- }
90
-
91
58
  /**
92
59
  * The DAEMON agent instance.
93
60
  * Handles the agent loop internally, allowing for multi-step tool usage.
@@ -239,6 +206,12 @@ export async function generateResponse(
239
206
  } else if (part.type === "tool-result") {
240
207
  callbacks.onToolResult?.(part.toolName, part.output, part.toolCallId);
241
208
  } else if (part.type === "tool-error") {
209
+ toolDebug.error("tool-error", {
210
+ toolName: part.toolName,
211
+ toolCallId: part.toolCallId,
212
+ input: part.input,
213
+ error: part.error,
214
+ });
242
215
  callbacks.onToolResult?.(part.toolName, { error: part.error, input: part.input }, part.toolCallId);
243
216
  } else if (part.type === "tool-approval-request") {
244
217
  const approvalRequest: ToolApprovalRequest = {
@@ -0,0 +1,26 @@
1
+ import type { ModelMessage } from "ai";
2
+
3
+ export function extractFinalAssistantText(messages: ModelMessage[]): string {
4
+ for (let i = messages.length - 1; i >= 0; i--) {
5
+ const msg = messages[i];
6
+ if (msg?.role === "assistant") {
7
+ const content = msg.content;
8
+ if (Array.isArray(content)) {
9
+ for (let j = content.length - 1; j >= 0; j--) {
10
+ const part = content[j];
11
+ if (
12
+ part &&
13
+ typeof part === "object" &&
14
+ "type" in part &&
15
+ part.type === "text" &&
16
+ "text" in part &&
17
+ typeof part.text === "string"
18
+ ) {
19
+ return part.text;
20
+ }
21
+ }
22
+ }
23
+ }
24
+ }
25
+ return "";
26
+ }
@@ -5,12 +5,13 @@
5
5
 
6
6
  import { createOpenRouter } from "@openrouter/ai-sdk-provider";
7
7
  import { tool } from "ai";
8
- import { ToolLoopAgent, stepCountIs } from "ai";
8
+ import { type ModelMessage, ToolLoopAgent, stepCountIs } from "ai";
9
9
  import type { ToolSet } from "ai";
10
10
  import { z } from "zod";
11
11
  import { getDaemonManager } from "../../state/daemon-state";
12
12
  import type { SubagentProgressEmitter } from "../../types";
13
13
  import { getOpenRouterReportedCost } from "../../utils/openrouter-reported-cost";
14
+ import { extractFinalAssistantText } from "../message-utils";
14
15
  import { buildOpenRouterChatSettings, getSubagentModel } from "../model-config";
15
16
  import { buildToolSet } from "./tool-registry";
16
17
 
@@ -60,7 +61,7 @@ RULES:
60
61
  - The final summary needs to be self contained and needs to provide enough information to the main agent so it is clear what you have done and what the results are.
61
62
 
62
63
  Today's date: ${new Date().toISOString().split("T")[0]}
63
- `;
64
+ `;
64
65
  }
65
66
 
66
67
  // Global emitter that will be set by the daemon-ai module
@@ -109,7 +110,6 @@ Provide a concise summary for display and a very specific task description (espe
109
110
  stopWhen: stepCountIs(MAX_SUBAGENT_STEPS),
110
111
  });
111
112
 
112
- let responseText = "";
113
113
  let costTotal = 0;
114
114
  let hasCost = false;
115
115
 
@@ -119,9 +119,7 @@ Provide a concise summary for display and a very specific task description (espe
119
119
  });
120
120
 
121
121
  for await (const part of stream.fullStream) {
122
- if (part.type === "text-delta") {
123
- responseText += part.text;
124
- } else if (part.type === "finish-step") {
122
+ if (part.type === "finish-step") {
125
123
  const reportedCost = getOpenRouterReportedCost(part.providerMetadata);
126
124
  if (reportedCost !== undefined) {
127
125
  costTotal += reportedCost;
@@ -145,6 +143,9 @@ Provide a concise summary for display and a very specific task description (espe
145
143
  }
146
144
  }
147
145
 
146
+ const responseMessages = await stream.response.then((response) => response.messages);
147
+ const finalResponse = extractFinalAssistantText(responseMessages);
148
+
148
149
  const streamUsage = await stream.usage;
149
150
  if (streamUsage) {
150
151
  progressEmitter?.onSubagentUsage({
@@ -163,7 +164,7 @@ Provide a concise summary for display and a very specific task description (espe
163
164
  return {
164
165
  success: true,
165
166
  summary,
166
- response: responseText || "Task completed but no text response generated.",
167
+ response: finalResponse || "Task completed but no text response generated.",
167
168
  };
168
169
  } catch (error) {
169
170
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -16,6 +16,8 @@ import { TypingInputBar } from "../../components/TypingInputBar";
16
16
  import type { ContentBlock, ConversationMessage, TokenUsage } from "../../types";
17
17
  import { DaemonState } from "../../types";
18
18
  import { COLORS, REASONING_MARKDOWN_STYLE } from "../../ui/constants";
19
+ import { renderReasoningTicker } from "../../ui/reasoning-ticker";
20
+ import { formatElapsedTime } from "../../utils/formatters";
19
21
  import type { ModelMetadata } from "../../utils/model-metadata";
20
22
 
21
23
  export interface ConversationDisplayState {
@@ -175,6 +177,8 @@ function ConversationPaneImpl(props: ConversationPaneProps) {
175
177
  const isReasoning =
176
178
  daemonState === DaemonState.RESPONDING &&
177
179
  (!conversation.currentResponse || !!reasoningDisplay || !!reasoningQueue);
180
+ const fullReasoningDurationLabel =
181
+ responseElapsedMs > 0 ? ` · ${formatElapsedTime(responseElapsedMs, { style: "detailed" })}` : "";
178
182
 
179
183
  return (
180
184
  <>
@@ -408,6 +412,7 @@ function ConversationPaneImpl(props: ConversationPaneProps) {
408
412
  >
409
413
  <text>
410
414
  <span fg={COLORS.REASONING}>{"REASONING"}</span>
415
+ <span fg={COLORS.REASONING_DIM}>{fullReasoningDurationLabel}</span>
411
416
  </text>
412
417
  <code
413
418
  content={fullReasoning}
@@ -418,12 +423,7 @@ function ConversationPaneImpl(props: ConversationPaneProps) {
418
423
  />
419
424
  </box>
420
425
  ) : reasoningDisplay ? (
421
- <text>
422
- <span fg={COLORS.REASONING_DIM}>
423
- {"⟡ "}
424
- {reasoningDisplay}
425
- </span>
426
- </text>
426
+ renderReasoningTicker(reasoningDisplay)
427
427
  ) : null}
428
428
  </box>
429
429
  )}
@@ -41,6 +41,10 @@ export function ContentBlockView({
41
41
 
42
42
  // Show full reasoning if enabled
43
43
  if (showFullReasoning) {
44
+ const durationLabel =
45
+ block.durationMs !== undefined
46
+ ? ` · ${formatElapsedTime(block.durationMs, { style: "detailed" })}`
47
+ : "";
44
48
  return (
45
49
  <box
46
50
  flexDirection="column"
@@ -51,6 +55,7 @@ export function ContentBlockView({
51
55
  >
52
56
  <text>
53
57
  <span fg={COLORS.REASONING}>{"REASONING"}</span>
58
+ <span fg={COLORS.REASONING_DIM}>{durationLabel}</span>
54
59
  </text>
55
60
  <code
56
61
  content={cleanedContent}
@@ -74,7 +79,7 @@ export function ContentBlockView({
74
79
  return (
75
80
  <text>
76
81
  <span fg={COLORS.REASONING_DIM}>
77
- {"// REASONING"}
82
+ {"REASONING"}
78
83
  {durationLabel}
79
84
  </span>
80
85
  </text>
@@ -1,18 +1,18 @@
1
1
  import { useMemo } from "react";
2
- import { COLORS } from "../ui/constants";
2
+ import { useToolApprovalForCall } from "../hooks/use-tool-approval";
3
3
  import type { ToolCall } from "../types";
4
+ import { COLORS } from "../ui/constants";
5
+ import { ApprovalPicker } from "./ApprovalPicker";
4
6
  import {
5
- getToolLayout,
7
+ ErrorPreviewView,
8
+ ResultPreviewView,
9
+ ToolBodyView,
10
+ ToolHeaderView,
6
11
  defaultToolLayout,
7
12
  getDefaultAbbreviation,
8
- ToolHeaderView,
9
- ToolBodyView,
10
- ResultPreviewView,
11
- ErrorPreviewView,
12
13
  getStatusBorderColor,
14
+ getToolLayout,
13
15
  } from "./tool-layouts";
14
- import { ApprovalPicker } from "./ApprovalPicker";
15
- import { useToolApprovalForCall } from "../hooks/use-tool-approval";
16
16
 
17
17
  interface ToolCallViewProps {
18
18
  call: ToolCall;
@@ -68,10 +68,7 @@ export function ToolCallView({ call, result, showOutput = true }: ToolCallViewPr
68
68
  const toolName = layout.abbreviation ?? getDefaultAbbreviation(call.name);
69
69
  const borderColor = getStatusBorderColor(call.status);
70
70
 
71
- const customBody = useMemo(() => {
72
- if (!layout.renderBody) return null;
73
- return layout.renderBody({ call, result, showOutput });
74
- }, [layout, call, result, showOutput]);
71
+ const customBody = layout.renderBody ? layout.renderBody({ call, result, showOutput }) : null;
75
72
 
76
73
  return (
77
74
  <box
@@ -1,7 +1,7 @@
1
1
  import { TextAttributes } from "@opentui/core";
2
- import { COLORS } from "../../ui/constants";
3
- import type { ToolHeader, ToolBody, ToolBodyLine } from "./types";
4
2
  import type { ToolCallStatus } from "../../types";
3
+ import { COLORS } from "../../ui/constants";
4
+ import type { ToolBody, ToolBodyLine, ToolHeader } from "./types";
5
5
 
6
6
  interface ToolHeaderViewProps {
7
7
  toolName: string;
@@ -11,11 +11,12 @@ interface ToolHeaderViewProps {
11
11
  }
12
12
 
13
13
  export function ToolHeaderView({ toolName, header, isRunning, toolColor }: ToolHeaderViewProps) {
14
+ const displayName = toolName.toUpperCase();
14
15
  return (
15
16
  <box flexDirection="row" alignItems="center" justifyContent="space-between" width="100%">
16
17
  <text>
17
18
  <span fg={toolColor}>{"↯ "}</span>
18
- <span fg={toolColor}>{toolName}</span>
19
+ <span fg={toolColor}>{displayName}</span>
19
20
  {header?.primary && <span fg={COLORS.TOOL_INPUT_TEXT}>{` ${header.primary}`}</span>}
20
21
  {header?.secondary && (
21
22
  <span
@@ -1,7 +1,8 @@
1
- import type { ToolLayoutConfig, ToolHeader, ToolLayoutRenderProps } from "../types";
2
1
  import type { ToolCallStatus } from "../../../types";
2
+ import { COLORS, REASONING_MARKDOWN_STYLE } from "../../../ui/constants";
3
+ import { formatMarkdownTables } from "../../../utils/markdown-tables";
3
4
  import { registerToolLayout } from "../registry";
4
- import { COLORS } from "../../../ui/constants";
5
+ import type { ToolHeader, ToolLayoutConfig, ToolLayoutRenderProps } from "../types";
5
6
 
6
7
  type UnknownRecord = Record<string, unknown>;
7
8
 
@@ -28,20 +29,91 @@ function extractSearchQuery(input: unknown): string | null {
28
29
  return null;
29
30
  }
30
31
 
32
+ function extractUrl(input: unknown): string | null {
33
+ if (!isRecord(input)) return null;
34
+ if ("url" in input && typeof input.url === "string") {
35
+ return input.url;
36
+ }
37
+ return null;
38
+ }
39
+
40
+ function extractPath(input: unknown): string | null {
41
+ if (!isRecord(input)) return null;
42
+ if ("path" in input && typeof input.path === "string") {
43
+ return input.path;
44
+ }
45
+ return null;
46
+ }
47
+
48
+ function extractCommand(input: unknown): string | null {
49
+ if (!isRecord(input)) return null;
50
+ if ("command" in input && typeof input.command === "string") {
51
+ return input.command;
52
+ }
53
+ return null;
54
+ }
55
+
56
+ function truncateLabel(text: string, maxLength: number): string {
57
+ if (text.length <= maxLength) return text;
58
+ if (maxLength <= 3) return text.slice(0, maxLength);
59
+ return `${text.slice(0, maxLength - 3)}...`;
60
+ }
61
+
31
62
  function abbreviateToolName(name: string): string {
32
63
  const abbreviations: Record<string, string> = {
33
64
  webSearch: "search",
34
65
  fetchUrls: "fetch",
35
66
  renderUrl: "render",
36
- getSystemInfo: "sys",
37
67
  runBash: "bash",
38
68
  todoManager: "todo",
39
69
  readFile: "read",
40
- groundingManager: "grounding",
41
70
  };
42
71
  return abbreviations[name] ?? name.slice(0, 8);
43
72
  }
44
73
 
74
+ function formatStepLabel(step: { toolName: string; input?: unknown }): string {
75
+ const toolLabel = abbreviateToolName(step.toolName);
76
+ const MAX_URL_LENGTH = 56;
77
+ const MAX_PATH_LENGTH = 56;
78
+ const MAX_COMMAND_LENGTH = 72;
79
+ const MAX_QUERY_LENGTH = 56;
80
+
81
+ if (step.toolName === "webSearch") {
82
+ const query = extractSearchQuery(step.input);
83
+ if (query) {
84
+ return `${toolLabel}: "${truncateLabel(query, MAX_QUERY_LENGTH)}"`;
85
+ }
86
+ return toolLabel;
87
+ }
88
+
89
+ if (step.toolName === "fetchUrls" || step.toolName === "renderUrl") {
90
+ const url = extractUrl(step.input);
91
+ if (url) {
92
+ return `${toolLabel}: ${truncateLabel(url, MAX_URL_LENGTH)}`;
93
+ }
94
+ return toolLabel;
95
+ }
96
+
97
+ if (step.toolName === "readFile") {
98
+ const path = extractPath(step.input);
99
+ if (path) {
100
+ return `${toolLabel}: ${truncateLabel(path, MAX_PATH_LENGTH)}`;
101
+ }
102
+ return toolLabel;
103
+ }
104
+
105
+ if (step.toolName === "runBash") {
106
+ const command = extractCommand(step.input);
107
+ if (command) {
108
+ const cleanCommand = command.replace(/\s+/g, " ").trim();
109
+ return `${toolLabel}: ${truncateLabel(cleanCommand, MAX_COMMAND_LENGTH)}`;
110
+ }
111
+ return toolLabel;
112
+ }
113
+
114
+ return toolLabel;
115
+ }
116
+
45
117
  function getStepStatusIcon(status: ToolCallStatus): string {
46
118
  switch (status) {
47
119
  case "running":
@@ -68,22 +140,50 @@ function getStepStatusColor(status: ToolCallStatus): string {
68
140
  }
69
141
  }
70
142
 
71
- function SubagentBody({ call }: ToolLayoutRenderProps) {
72
- if (!call.subagentSteps || call.subagentSteps.length === 0) {
143
+ function formatSubagentResponse(result: unknown): string | null {
144
+ if (!isRecord(result)) return null;
145
+ if (typeof result.response !== "string") return null;
146
+ const raw = result.response.trim();
147
+ if (!raw) return null;
148
+
149
+ const MAX_LINES = 6;
150
+ const MAX_CHARS = 160;
151
+ const lines = raw
152
+ .replace(/\r\n/g, "\n")
153
+ .split("\n")
154
+ .map((line) => line.trimEnd())
155
+ .filter((line) => line.length > 0);
156
+ if (lines.length === 0) return null;
157
+
158
+ const trimmed = lines.slice(0, MAX_LINES).map((line) => truncateLabel(line, MAX_CHARS));
159
+ if (lines.length > MAX_LINES && trimmed.length > 0) {
160
+ const lastIndex = trimmed.length - 1;
161
+ const lastLine = trimmed[lastIndex] ?? "";
162
+ trimmed[lastIndex] = lastLine.endsWith("...") ? lastLine : `${lastLine}...`;
163
+ }
164
+
165
+ return trimmed.join("\n");
166
+ }
167
+
168
+ function SubagentBody({ call, result }: ToolLayoutRenderProps) {
169
+ const steps = call.subagentSteps ?? [];
170
+ const responseText = formatSubagentResponse(result);
171
+ if (steps.length === 0 && !responseText) {
73
172
  return null;
74
173
  }
75
174
 
175
+ const maxWidth =
176
+ typeof process !== "undefined" && process.stdout?.columns ? process.stdout.columns : undefined;
177
+ const renderedResponse = responseText ? formatMarkdownTables(responseText, { maxWidth }) : "";
178
+
76
179
  return (
77
180
  <box flexDirection="column" paddingLeft={2} marginTop={0}>
78
- {call.subagentSteps.map((step, idx) => {
79
- const toolLabel = abbreviateToolName(step.toolName);
80
- let stepLabel = toolLabel;
81
- if (step.toolName === "webSearch") {
82
- const query = extractSearchQuery(step.input);
83
- if (query) {
84
- stepLabel = `${toolLabel}: "${query}"`;
85
- }
86
- }
181
+ {steps.map((step, idx) => {
182
+ const stepLabel = formatStepLabel(step);
183
+ const inputLabel = stepLabel.slice(stepLabel.indexOf(":") + 1).trim();
184
+ const toolLabel = stepLabel.includes(":")
185
+ ? stepLabel.slice(0, stepLabel.indexOf(":") + 1)
186
+ : stepLabel;
87
187
 
88
188
  return (
89
189
  <box key={`${step.toolName}-${idx}`} flexDirection="row" alignItems="center">
@@ -95,11 +195,35 @@ function SubagentBody({ call }: ToolLayoutRenderProps) {
95
195
  </text>
96
196
  )}
97
197
  <text marginLeft={1}>
98
- <span fg={COLORS.TOOL_INPUT_TEXT}>{stepLabel}</span>
198
+ <span fg={COLORS.TOOL_INPUT_TEXT}>{toolLabel}</span>
199
+ {stepLabel.includes(":") && <span fg={COLORS.REASONING_DIM}>{` ${inputLabel}`}</span>}
99
200
  </text>
100
201
  </box>
101
202
  );
102
203
  })}
204
+ {responseText && (
205
+ <box flexDirection="column" marginTop={steps.length > 0 ? 1 : 0}>
206
+ <text>
207
+ <span fg={COLORS.REASONING_DIM}>{"response"}</span>
208
+ </text>
209
+ <box
210
+ borderStyle="single"
211
+ borderColor={COLORS.TOOL_INPUT_BORDER}
212
+ paddingLeft={1}
213
+ paddingRight={1}
214
+ paddingTop={0}
215
+ paddingBottom={0}
216
+ >
217
+ <code
218
+ content={renderedResponse}
219
+ filetype="markdown"
220
+ syntaxStyle={REASONING_MARKDOWN_STYLE}
221
+ conceal={true}
222
+ drawUnstyledText={false}
223
+ />
224
+ </box>
225
+ </box>
226
+ )}
103
227
  </box>
104
228
  );
105
229
  }
@@ -22,7 +22,7 @@ import type {
22
22
  import { DaemonState } from "../types";
23
23
  import { REASONING_COLORS, STATE_COLORS } from "../types/theme";
24
24
  import { REASONING_ANIMATION } from "../ui/constants";
25
- import { debug } from "../utils/debug-logger";
25
+ import { debug, messageDebug } from "../utils/debug-logger";
26
26
  import { hasVisibleText } from "../utils/formatters";
27
27
  import {
28
28
  INTERRUPTED_TOOL_RESULT,
@@ -736,7 +736,7 @@ export function createCancelledHandler(
736
736
  const hasBlocks = refs.contentBlocksRef.current.length > 0;
737
737
  const contentBlocks = hasBlocks ? buildInterruptedContentBlocks(refs.contentBlocksRef.current) : [];
738
738
 
739
- debug.info("agent-turn-incomplete", {
739
+ messageDebug.info("agent-turn-incomplete", {
740
740
  userText,
741
741
  contentBlocks,
742
742
  });
@@ -753,7 +753,7 @@ export function createCancelledHandler(
753
753
  }
754
754
  : null;
755
755
 
756
- debug.info("agent-turn-incomplete-messages", {
756
+ messageDebug.info("agent-turn-incomplete-messages", {
757
757
  responseMessages,
758
758
  });
759
759
 
@@ -59,7 +59,7 @@ export function useReasoningAnimation(): UseReasoningAnimationReturn {
59
59
 
60
60
  const terminalWidth =
61
61
  typeof process !== "undefined" && process.stdout?.columns ? process.stdout.columns : undefined;
62
- const maxWidth = terminalWidth ? Math.max(20, terminalWidth - 12) : REASONING_ANIMATION.LINE_WIDTH;
62
+ const maxWidth = terminalWidth ? Math.max(20, terminalWidth - 14) : REASONING_ANIMATION.LINE_WIDTH;
63
63
  const lineWidth = Math.min(REASONING_ANIMATION.LINE_WIDTH, maxWidth);
64
64
 
65
65
  // Add to display, restart when reaching the line width
@@ -16,7 +16,7 @@ import type {
16
16
  } from "../types";
17
17
  import { DEFAULT_TOOL_TOGGLES } from "../types";
18
18
  import { DaemonState } from "../types";
19
- import { debug } from "../utils/debug-logger";
19
+ import { debug, messageDebug } from "../utils/debug-logger";
20
20
  import { SpeechController } from "../voice/tts/speech-controller";
21
21
  import { VoiceInputController } from "../voice/voice-input-controller";
22
22
  import { type DaemonStateEvents, daemonEvents } from "./daemon-events";
@@ -272,7 +272,7 @@ class DaemonStateManager {
272
272
  this.setState(DaemonState.RESPONDING);
273
273
  this._response = "";
274
274
  const turnId = ++this._turnId;
275
- debug.info("agent-turn-start", {
275
+ messageDebug.info("agent-turn-start", {
276
276
  turnId,
277
277
  text,
278
278
  mode: this._interactionMode,
@@ -319,7 +319,7 @@ class DaemonStateManager {
319
319
  return;
320
320
  }
321
321
 
322
- debug.info("agent-turn-complete", {
322
+ messageDebug.info("agent-turn-complete", {
323
323
  turnId,
324
324
  fullText: result.fullText,
325
325
  finalText: result.finalText,
@@ -28,7 +28,10 @@ export function renderReasoningTicker(reasoningDisplay: string) {
28
28
 
29
29
  return (
30
30
  <text>
31
- <span fg={REASONING_ANIMATION.PREFIX_COLOR}>{"// "}</span>
31
+ <span fg={COLORS.REASONING_DIM} attributes={TextAttributes.BOLD}>
32
+ {"REASONING"}
33
+ </span>
34
+ <span fg={REASONING_ANIMATION.PREFIX_COLOR}>{" | "}</span>
32
35
  {segments.map((segment, index) => (
33
36
  <span fg={segment.color} key={`reasoning-seg-${index}`} attributes={TextAttributes.ITALIC}>
34
37
  {segment.text}
@@ -6,19 +6,24 @@
6
6
  * import { debug } from "../utils/debug-logger";
7
7
  * debug.log("message", someObject);
8
8
  *
9
- * Then run `tail -f debug.log` in a separate terminal.
9
+ * Then run `tail -f ~/.config/daemon/logs/debug.log` in a separate terminal.
10
+ * Tool-specific logging uses `~/.config/daemon/logs/tools.log`.
11
+ * Message logging uses `~/.config/daemon/logs/messages.log`.
10
12
  */
11
13
 
12
14
  import fs from "node:fs";
13
15
  import path from "node:path";
14
16
  import { getAppConfigDir } from "./preferences";
15
17
 
16
- const LOG_FILE = path.join(getAppConfigDir(), "debug.log");
18
+ const LOG_DIR = path.join(getAppConfigDir(), "logs");
19
+ const LOG_FILE = path.join(LOG_DIR, "debug.log");
20
+ const TOOLS_LOG_FILE = path.join(LOG_DIR, "tools.log");
21
+ const MESSAGES_LOG_FILE = path.join(LOG_DIR, "messages.log");
17
22
  const ENABLED = process.env.DEBUG_LOG === "1" || process.env.DEBUG_LOG === "true";
18
23
 
19
- function ensureLogDir(): void {
24
+ function ensureLogDir(logDir: string): void {
20
25
  try {
21
- fs.mkdirSync(getAppConfigDir(), { recursive: true });
26
+ fs.mkdirSync(logDir, { recursive: true });
22
27
  } catch {
23
28
  // Silently fail if we can't create the directory
24
29
  }
@@ -35,7 +40,7 @@ function formatValue(value: unknown): string {
35
40
  }
36
41
  }
37
42
 
38
- function writeLog(level: string, args: unknown[]): void {
43
+ function writeLog(logFile: string, level: string, args: unknown[]): void {
39
44
  if (!ENABLED) return;
40
45
 
41
46
  const timestamp = new Date().toISOString();
@@ -43,27 +48,33 @@ function writeLog(level: string, args: unknown[]): void {
43
48
  const line = `[${timestamp}] [${level}] ${formatted}\n`;
44
49
 
45
50
  try {
46
- ensureLogDir();
47
- fs.appendFileSync(LOG_FILE, line);
51
+ ensureLogDir(LOG_DIR);
52
+ fs.appendFileSync(logFile, line);
48
53
  } catch {
49
54
  // Silently fail if we can't write
50
55
  }
51
56
  }
52
57
 
53
- export const debug = {
54
- log: (...args: unknown[]) => writeLog("LOG", args),
55
- info: (...args: unknown[]) => writeLog("INFO", args),
56
- warn: (...args: unknown[]) => writeLog("WARN", args),
57
- error: (...args: unknown[]) => writeLog("ERROR", args),
58
+ function createDebugLogger(logFile: string) {
59
+ return {
60
+ log: (...args: unknown[]) => writeLog(logFile, "LOG", args),
61
+ info: (...args: unknown[]) => writeLog(logFile, "INFO", args),
62
+ warn: (...args: unknown[]) => writeLog(logFile, "WARN", args),
63
+ error: (...args: unknown[]) => writeLog(logFile, "ERROR", args),
58
64
 
59
- /** Clear the log file */
60
- clear: () => {
61
- if (!ENABLED) return;
62
- try {
63
- ensureLogDir();
64
- fs.writeFileSync(LOG_FILE, "");
65
- } catch {
66
- // Silently fail
67
- }
68
- },
69
- };
65
+ /** Clear the log file */
66
+ clear: () => {
67
+ if (!ENABLED) return;
68
+ try {
69
+ ensureLogDir(LOG_DIR);
70
+ fs.writeFileSync(logFile, "");
71
+ } catch {
72
+ // Silently fail
73
+ }
74
+ },
75
+ };
76
+ }
77
+
78
+ export const debug = createDebugLogger(LOG_FILE);
79
+ export const toolDebug = createDebugLogger(TOOLS_LOG_FILE);
80
+ export const messageDebug = createDebugLogger(MESSAGES_LOG_FILE);