@makefinks/daemon 0.3.1 → 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 +1 -1
- package/src/ai/daemon-ai.ts +8 -35
- package/src/ai/message-utils.ts +26 -0
- package/src/ai/tools/subagents.ts +8 -7
- package/src/app/components/ConversationPane.tsx +6 -6
- package/src/components/ContentBlockView.tsx +6 -1
- package/src/components/ModelMenu.tsx +59 -13
- package/src/components/ToolCallView.tsx +9 -12
- package/src/components/tool-layouts/components.tsx +4 -3
- package/src/components/tool-layouts/layouts/subagent.tsx +140 -16
- package/src/hooks/daemon-event-handlers.ts +3 -3
- package/src/hooks/use-app-preferences-bootstrap.ts +3 -6
- package/src/hooks/use-reasoning-animation.ts +4 -4
- package/src/state/daemon-state.ts +3 -3
- package/src/ui/reasoning-ticker.tsx +6 -2
- package/src/utils/debug-logger.ts +34 -23
package/package.json
CHANGED
package/src/ai/daemon-ai.ts
CHANGED
|
@@ -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 === "
|
|
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:
|
|
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
|
-
|
|
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
|
-
{"
|
|
82
|
+
{"REASONING"}
|
|
78
83
|
{durationLabel}
|
|
79
84
|
</span>
|
|
80
85
|
</text>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { ScrollBoxRenderable, TextareaRenderable } from "@opentui/core";
|
|
2
2
|
import { useKeyboard } from "@opentui/react";
|
|
3
3
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
-
import type { ModelOption } from "../types";
|
|
5
4
|
import { useMenuKeyboard } from "../hooks/use-menu-keyboard";
|
|
5
|
+
import type { ModelOption } from "../types";
|
|
6
6
|
import { COLORS } from "../ui/constants";
|
|
7
7
|
import { formatContextWindowK, formatPrice } from "../utils/formatters";
|
|
8
8
|
|
|
@@ -58,15 +58,26 @@ export function ModelMenu({
|
|
|
58
58
|
|
|
59
59
|
const curatedIdSet = useMemo(() => new Set(sortedCurated.map((model) => model.id)), [sortedCurated]);
|
|
60
60
|
|
|
61
|
+
const savedModel = useMemo(() => {
|
|
62
|
+
if (!currentModelId) return null;
|
|
63
|
+
if (curatedIdSet.has(currentModelId)) return null;
|
|
64
|
+
const match = allModels.find((model) => model.id === currentModelId);
|
|
65
|
+
return match ?? { id: currentModelId, name: currentModelId };
|
|
66
|
+
}, [allModels, curatedIdSet, currentModelId]);
|
|
67
|
+
|
|
68
|
+
const savedModels = useMemo(() => (savedModel ? [savedModel] : []), [savedModel]);
|
|
69
|
+
|
|
61
70
|
const allModelsWithFallback = useMemo(() => {
|
|
62
71
|
if (!currentModelId) return allModels;
|
|
63
72
|
if (curatedIdSet.has(currentModelId)) return allModels;
|
|
64
|
-
if (
|
|
73
|
+
if (savedModel) return allModels;
|
|
65
74
|
return [...allModels, { id: currentModelId, name: currentModelId }];
|
|
66
|
-
}, [allModels, curatedIdSet, currentModelId]);
|
|
75
|
+
}, [allModels, curatedIdSet, currentModelId, savedModel]);
|
|
67
76
|
|
|
68
77
|
const filteredAllModels = useMemo(() => {
|
|
69
|
-
const filtered = allModelsWithFallback.filter(
|
|
78
|
+
const filtered = allModelsWithFallback.filter(
|
|
79
|
+
(model) => !curatedIdSet.has(model.id) && model.id !== savedModel?.id
|
|
80
|
+
);
|
|
70
81
|
const query = searchQuery.trim().toLowerCase();
|
|
71
82
|
if (query.length < MIN_ALL_MODEL_QUERY_LENGTH) {
|
|
72
83
|
return [];
|
|
@@ -78,28 +89,44 @@ export function ModelMenu({
|
|
|
78
89
|
: filtered;
|
|
79
90
|
|
|
80
91
|
return matching.sort((a, b) => a.name.localeCompare(b.name));
|
|
81
|
-
}, [allModelsWithFallback, curatedIdSet, searchQuery]);
|
|
92
|
+
}, [allModelsWithFallback, curatedIdSet, savedModel?.id, searchQuery]);
|
|
82
93
|
|
|
83
|
-
const totalItems = sortedCurated.length + filteredAllModels.length;
|
|
94
|
+
const totalItems = sortedCurated.length + savedModels.length + filteredAllModels.length;
|
|
84
95
|
|
|
85
96
|
const initialIndex = useMemo(() => {
|
|
86
97
|
if (totalItems === 0) return 0;
|
|
87
98
|
const curatedIdx = sortedCurated.findIndex((model) => model.id === currentModelId);
|
|
88
99
|
if (curatedIdx >= 0) return curatedIdx;
|
|
100
|
+
const savedIdx = savedModels.findIndex((model) => model.id === currentModelId);
|
|
101
|
+
if (savedIdx >= 0) return sortedCurated.length + savedIdx;
|
|
89
102
|
const allIdx = filteredAllModels.findIndex((model) => model.id === currentModelId);
|
|
90
|
-
if (allIdx >= 0) return sortedCurated.length + allIdx;
|
|
103
|
+
if (allIdx >= 0) return sortedCurated.length + savedModels.length + allIdx;
|
|
91
104
|
return 0;
|
|
92
|
-
}, [sortedCurated, filteredAllModels, currentModelId, totalItems]);
|
|
105
|
+
}, [sortedCurated, savedModels, filteredAllModels, currentModelId, totalItems]);
|
|
93
106
|
|
|
94
107
|
const { selectedIndex } = useMenuKeyboard({
|
|
95
108
|
itemCount: totalItems,
|
|
96
109
|
initialIndex,
|
|
97
110
|
onClose,
|
|
98
111
|
onSelect: (selectedIdx) => {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
112
|
+
if (selectedIdx < sortedCurated.length) {
|
|
113
|
+
const model = sortedCurated[selectedIdx];
|
|
114
|
+
if (model) {
|
|
115
|
+
onSelect(model);
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const afterCurated = selectedIdx - sortedCurated.length;
|
|
121
|
+
if (afterCurated < savedModels.length) {
|
|
122
|
+
const model = savedModels[afterCurated];
|
|
123
|
+
if (model) {
|
|
124
|
+
onSelect(model);
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const model = filteredAllModels[afterCurated - savedModels.length];
|
|
103
130
|
if (model) {
|
|
104
131
|
onSelect(model);
|
|
105
132
|
}
|
|
@@ -124,7 +151,7 @@ export function ModelMenu({
|
|
|
124
151
|
}
|
|
125
152
|
});
|
|
126
153
|
|
|
127
|
-
const allSelectedIndex = selectedIndex - sortedCurated.length;
|
|
154
|
+
const allSelectedIndex = selectedIndex - sortedCurated.length - savedModels.length;
|
|
128
155
|
const isAllSectionSelected = allSelectedIndex >= 0;
|
|
129
156
|
|
|
130
157
|
const scrollRef = useRef<ScrollBoxRenderable | null>(null);
|
|
@@ -345,6 +372,25 @@ export function ModelMenu({
|
|
|
345
372
|
</>
|
|
346
373
|
)}
|
|
347
374
|
|
|
375
|
+
{savedModels.length > 0 ? (
|
|
376
|
+
<>
|
|
377
|
+
<box marginBottom={1} marginTop={1}>
|
|
378
|
+
<text>
|
|
379
|
+
<span fg={COLORS.DAEMON_LABEL}>[ SAVED ]</span>
|
|
380
|
+
</text>
|
|
381
|
+
</box>
|
|
382
|
+
<box flexDirection="column">
|
|
383
|
+
{savedModels.map((model, idx) =>
|
|
384
|
+
renderModelRow(
|
|
385
|
+
model,
|
|
386
|
+
sortedCurated.length + idx === selectedIndex,
|
|
387
|
+
model.id === currentModelId
|
|
388
|
+
)
|
|
389
|
+
)}
|
|
390
|
+
</box>
|
|
391
|
+
</>
|
|
392
|
+
) : null}
|
|
393
|
+
|
|
348
394
|
<box marginBottom={1} marginTop={1}>
|
|
349
395
|
<text>
|
|
350
396
|
<span fg={COLORS.DAEMON_LABEL}>[ ALL MODELS ]</span>
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
|
-
import {
|
|
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
|
-
|
|
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 =
|
|
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}>{
|
|
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 {
|
|
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
|
|
72
|
-
if (!
|
|
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
|
-
{
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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}>{
|
|
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
|
-
|
|
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
|
-
|
|
756
|
+
messageDebug.info("agent-turn-incomplete-messages", {
|
|
757
757
|
responseMessages,
|
|
758
758
|
});
|
|
759
759
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { setOpenRouterProviderTag, setResponseModel } from "../ai/model-config";
|
|
3
3
|
import type {
|
|
4
4
|
AppPreferences,
|
|
5
5
|
BashApprovalLevel,
|
|
@@ -94,11 +94,8 @@ export function useAppPreferencesBootstrap(
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
if (prefs?.modelId) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
setResponseModel(prefs.modelId);
|
|
100
|
-
setCurrentModelId(prefs.modelId);
|
|
101
|
-
}
|
|
97
|
+
setResponseModel(prefs.modelId);
|
|
98
|
+
setCurrentModelId(prefs.modelId);
|
|
102
99
|
}
|
|
103
100
|
|
|
104
101
|
if (prefs?.openRouterProviderTag) {
|
|
@@ -59,14 +59,14 @@ 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 -
|
|
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
|
-
// Add to display,
|
|
65
|
+
// Add to display, restart when reaching the line width
|
|
66
66
|
setReasoningDisplay((display: string) => {
|
|
67
67
|
const newDisplay = display + movedChars;
|
|
68
|
-
if (newDisplay.length
|
|
69
|
-
return
|
|
68
|
+
if (newDisplay.length >= lineWidth) {
|
|
69
|
+
return movedChars;
|
|
70
70
|
}
|
|
71
71
|
return newDisplay;
|
|
72
72
|
});
|
|
@@ -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
|
-
|
|
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
|
-
|
|
322
|
+
messageDebug.info("agent-turn-complete", {
|
|
323
323
|
turnId,
|
|
324
324
|
fullText: result.fullText,
|
|
325
325
|
finalText: result.finalText,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TextAttributes } from "@opentui/core";
|
|
1
2
|
import { COLORS, REASONING_ANIMATION } from "./constants";
|
|
2
3
|
|
|
3
4
|
export function renderReasoningTicker(reasoningDisplay: string) {
|
|
@@ -27,9 +28,12 @@ export function renderReasoningTicker(reasoningDisplay: string) {
|
|
|
27
28
|
|
|
28
29
|
return (
|
|
29
30
|
<text>
|
|
30
|
-
<span fg={
|
|
31
|
+
<span fg={COLORS.REASONING_DIM} attributes={TextAttributes.BOLD}>
|
|
32
|
+
{"REASONING"}
|
|
33
|
+
</span>
|
|
34
|
+
<span fg={REASONING_ANIMATION.PREFIX_COLOR}>{" | "}</span>
|
|
31
35
|
{segments.map((segment, index) => (
|
|
32
|
-
<span fg={segment.color} key={`reasoning-seg-${index}`}>
|
|
36
|
+
<span fg={segment.color} key={`reasoning-seg-${index}`} attributes={TextAttributes.ITALIC}>
|
|
33
37
|
{segment.text}
|
|
34
38
|
</span>
|
|
35
39
|
))}
|
|
@@ -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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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);
|