@makefinks/daemon 0.9.1 → 0.11.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/README.md +60 -14
- package/package.json +4 -2
- package/src/ai/copilot-client.ts +775 -0
- package/src/ai/daemon-ai.ts +32 -234
- package/src/ai/model-config.ts +55 -14
- package/src/ai/providers/capabilities.ts +16 -0
- package/src/ai/providers/copilot-provider.ts +632 -0
- package/src/ai/providers/openrouter-provider.ts +217 -0
- package/src/ai/providers/registry.ts +14 -0
- package/src/ai/providers/types.ts +31 -0
- package/src/ai/system-prompt.ts +16 -0
- package/src/ai/tools/subagents.ts +1 -1
- package/src/ai/tools/tool-registry.ts +22 -1
- package/src/ai/tools/write-file.ts +51 -0
- package/src/app/components/AppOverlays.tsx +9 -1
- package/src/app/components/ConversationPane.tsx +8 -2
- package/src/components/ModelMenu.tsx +202 -140
- package/src/components/OnboardingOverlay.tsx +147 -1
- package/src/components/SettingsMenu.tsx +27 -1
- package/src/components/TokenUsageDisplay.tsx +5 -3
- package/src/components/tool-layouts/layouts/index.ts +1 -0
- package/src/components/tool-layouts/layouts/write-file.tsx +117 -0
- package/src/hooks/daemon-event-handlers.ts +61 -14
- package/src/hooks/keyboard-handlers.ts +109 -28
- package/src/hooks/use-app-callbacks.ts +141 -43
- package/src/hooks/use-app-context-builder.ts +5 -0
- package/src/hooks/use-app-controller.ts +31 -2
- package/src/hooks/use-app-copilot-models-loader.ts +45 -0
- package/src/hooks/use-app-display-state.ts +24 -2
- package/src/hooks/use-app-model.ts +103 -17
- package/src/hooks/use-app-preferences-bootstrap.ts +54 -10
- package/src/hooks/use-bootstrap-controller.ts +5 -0
- package/src/hooks/use-daemon-events.ts +8 -2
- package/src/hooks/use-daemon-keyboard.ts +19 -6
- package/src/hooks/use-daemon-runtime-controller.ts +4 -0
- package/src/hooks/use-menu-keyboard.ts +6 -1
- package/src/state/app-context.tsx +6 -0
- package/src/types/index.ts +24 -1
- package/src/utils/copilot-models.ts +77 -0
- package/src/utils/preferences.ts +3 -0
|
@@ -3,9 +3,10 @@ import { useKeyboard } from "@opentui/react";
|
|
|
3
3
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
4
4
|
import type { RefObject } from "react";
|
|
5
5
|
import { handleOnboardingKey } from "../hooks/keyboard-handlers";
|
|
6
|
-
import type { AppPreferences, AudioDevice, ModelOption, OnboardingStep } from "../types";
|
|
6
|
+
import type { AppPreferences, AudioDevice, LlmProvider, ModelOption, OnboardingStep } from "../types";
|
|
7
7
|
import { COLORS } from "../ui/constants";
|
|
8
8
|
import { formatContextWindowK, formatPrice } from "../utils/formatters";
|
|
9
|
+
import { ApiKeyInput } from "./ApiKeyInput";
|
|
9
10
|
import { ApiKeyStep } from "./ApiKeyStep";
|
|
10
11
|
|
|
11
12
|
const MODEL_COL_WIDTH = {
|
|
@@ -25,6 +26,13 @@ const API_KEY_CONFIGS = {
|
|
|
25
26
|
keyUrl: "https://openrouter.ai/keys",
|
|
26
27
|
optional: false,
|
|
27
28
|
},
|
|
29
|
+
copilot_auth: {
|
|
30
|
+
title: "GITHUB COPILOT LOGIN REQUIRED",
|
|
31
|
+
description: "Authenticate with GitHub/Copilot CLI, then validate from here.",
|
|
32
|
+
envVarName: "COPILOT_CLI_LOGIN",
|
|
33
|
+
keyUrl: "https://docs.github.com/en/copilot/how-tos/set-up/install-copilot-cli",
|
|
34
|
+
optional: false,
|
|
35
|
+
},
|
|
28
36
|
openai_key: {
|
|
29
37
|
title: "OPENAI API KEY (OPTIONAL)",
|
|
30
38
|
description: "OpenAI enables voice features (speech-to-text and text-to-speech).",
|
|
@@ -51,10 +59,13 @@ interface OnboardingOverlayProps {
|
|
|
51
59
|
currentDevice?: string;
|
|
52
60
|
currentOutputDevice?: string;
|
|
53
61
|
models: ModelOption[];
|
|
62
|
+
currentModelProvider: LlmProvider;
|
|
63
|
+
copilotAuthenticated: boolean;
|
|
54
64
|
currentModelId: string;
|
|
55
65
|
deviceLoadTimedOut?: boolean;
|
|
56
66
|
soxAvailable: boolean;
|
|
57
67
|
soxInstallHint: string;
|
|
68
|
+
setCurrentModelProvider: (provider: LlmProvider) => void;
|
|
58
69
|
setCurrentDevice: (deviceName: string | undefined) => void;
|
|
59
70
|
setCurrentOutputDevice: (deviceName: string | undefined) => void;
|
|
60
71
|
setCurrentModelId: (modelId: string) => void;
|
|
@@ -74,10 +85,13 @@ export function OnboardingOverlay({
|
|
|
74
85
|
currentDevice,
|
|
75
86
|
currentOutputDevice,
|
|
76
87
|
models,
|
|
88
|
+
currentModelProvider,
|
|
89
|
+
copilotAuthenticated,
|
|
77
90
|
currentModelId,
|
|
78
91
|
deviceLoadTimedOut,
|
|
79
92
|
soxAvailable,
|
|
80
93
|
soxInstallHint,
|
|
94
|
+
setCurrentModelProvider,
|
|
81
95
|
setCurrentDevice,
|
|
82
96
|
setCurrentOutputDevice,
|
|
83
97
|
setCurrentModelId,
|
|
@@ -87,9 +101,35 @@ export function OnboardingOverlay({
|
|
|
87
101
|
onKeySubmit,
|
|
88
102
|
apiKeyTextareaRef,
|
|
89
103
|
}: OnboardingOverlayProps) {
|
|
104
|
+
const [selectedProviderIdx, setSelectedProviderIdx] = useState(0);
|
|
90
105
|
const [selectedDeviceIdx, setSelectedDeviceIdx] = useState(0);
|
|
91
106
|
const [selectedModelIdx, setSelectedModelIdx] = useState(0);
|
|
92
107
|
|
|
108
|
+
const providerOptions = useMemo(
|
|
109
|
+
() => [
|
|
110
|
+
{
|
|
111
|
+
id: "openrouter" as const,
|
|
112
|
+
label: "OpenRouter",
|
|
113
|
+
description: "API key based provider routing",
|
|
114
|
+
},
|
|
115
|
+
...(copilotAuthenticated
|
|
116
|
+
? ([
|
|
117
|
+
{
|
|
118
|
+
id: "copilot" as const,
|
|
119
|
+
label: "GitHub Copilot",
|
|
120
|
+
description: "GitHub auth + Copilot subscription",
|
|
121
|
+
},
|
|
122
|
+
] as const)
|
|
123
|
+
: []),
|
|
124
|
+
],
|
|
125
|
+
[copilotAuthenticated]
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const initialProviderIdx = useMemo(() => {
|
|
129
|
+
const idx = providerOptions.findIndex((provider) => provider.id === currentModelProvider);
|
|
130
|
+
return idx >= 0 ? idx : 0;
|
|
131
|
+
}, [currentModelProvider, providerOptions]);
|
|
132
|
+
|
|
93
133
|
const initialDeviceIdx = useMemo(() => {
|
|
94
134
|
if (devices.length === 0) return 0;
|
|
95
135
|
const idx = currentDevice ? devices.findIndex((device) => device.name === currentDevice) : -1;
|
|
@@ -103,6 +143,11 @@ export function OnboardingOverlay({
|
|
|
103
143
|
return idx >= 0 ? idx : 0;
|
|
104
144
|
}, [models, currentModelId]);
|
|
105
145
|
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
if (step !== "provider") return;
|
|
148
|
+
setSelectedProviderIdx(initialProviderIdx);
|
|
149
|
+
}, [step, initialProviderIdx]);
|
|
150
|
+
|
|
106
151
|
useEffect(() => {
|
|
107
152
|
if (step !== "device") return;
|
|
108
153
|
setSelectedDeviceIdx(initialDeviceIdx);
|
|
@@ -117,13 +162,18 @@ export function OnboardingOverlay({
|
|
|
117
162
|
(key: KeyEvent) => {
|
|
118
163
|
const handled = handleOnboardingKey(key, {
|
|
119
164
|
step,
|
|
165
|
+
selectedProviderIdx,
|
|
120
166
|
devices,
|
|
121
167
|
models,
|
|
122
168
|
selectedDeviceIdx,
|
|
123
169
|
selectedModelIdx,
|
|
170
|
+
currentModelProvider,
|
|
171
|
+
copilotAuthenticated,
|
|
124
172
|
preferences,
|
|
173
|
+
setSelectedProviderIdx,
|
|
125
174
|
setSelectedDeviceIdx,
|
|
126
175
|
setSelectedModelIdx,
|
|
176
|
+
setCurrentModelProvider,
|
|
127
177
|
setCurrentDevice,
|
|
128
178
|
setCurrentOutputDevice,
|
|
129
179
|
setCurrentModelId,
|
|
@@ -140,11 +190,15 @@ export function OnboardingOverlay({
|
|
|
140
190
|
},
|
|
141
191
|
[
|
|
142
192
|
step,
|
|
193
|
+
selectedProviderIdx,
|
|
143
194
|
devices,
|
|
144
195
|
models,
|
|
145
196
|
selectedDeviceIdx,
|
|
146
197
|
selectedModelIdx,
|
|
198
|
+
currentModelProvider,
|
|
199
|
+
copilotAuthenticated,
|
|
147
200
|
preferences,
|
|
201
|
+
setCurrentModelProvider,
|
|
148
202
|
setCurrentDevice,
|
|
149
203
|
setCurrentOutputDevice,
|
|
150
204
|
setCurrentModelId,
|
|
@@ -209,6 +263,61 @@ export function OnboardingOverlay({
|
|
|
209
263
|
</>
|
|
210
264
|
)}
|
|
211
265
|
|
|
266
|
+
{step === "provider" && (
|
|
267
|
+
<>
|
|
268
|
+
<box marginBottom={1}>
|
|
269
|
+
<text>
|
|
270
|
+
<span fg={COLORS.DAEMON_LABEL}>[ SELECT MODEL PROVIDER ]</span>
|
|
271
|
+
</text>
|
|
272
|
+
</box>
|
|
273
|
+
<box marginBottom={1}>
|
|
274
|
+
<text>
|
|
275
|
+
<span fg={COLORS.REASONING_DIM}>Choose where DAEMON should run agent responses.</span>
|
|
276
|
+
</text>
|
|
277
|
+
</box>
|
|
278
|
+
<box marginBottom={1}>
|
|
279
|
+
<text>
|
|
280
|
+
<span fg={COLORS.USER_LABEL}>↑/↓ navigate, ENTER select</span>
|
|
281
|
+
</text>
|
|
282
|
+
</box>
|
|
283
|
+
<box flexDirection="column">
|
|
284
|
+
{providerOptions.map((provider, idx) => {
|
|
285
|
+
const isSelected = idx === selectedProviderIdx;
|
|
286
|
+
const isCurrent = currentModelProvider === provider.id;
|
|
287
|
+
return (
|
|
288
|
+
<box
|
|
289
|
+
key={provider.id}
|
|
290
|
+
backgroundColor={isSelected ? COLORS.MENU_SELECTED_BG : COLORS.MENU_BG}
|
|
291
|
+
paddingLeft={1}
|
|
292
|
+
paddingRight={1}
|
|
293
|
+
flexDirection="column"
|
|
294
|
+
>
|
|
295
|
+
<text>
|
|
296
|
+
<span fg={isSelected ? COLORS.DAEMON_LABEL : COLORS.MENU_TEXT}>
|
|
297
|
+
{isSelected ? "▶ " : " "}
|
|
298
|
+
{provider.label}
|
|
299
|
+
{isCurrent ? " ●" : ""}
|
|
300
|
+
</span>
|
|
301
|
+
</text>
|
|
302
|
+
<text>
|
|
303
|
+
<span fg={COLORS.REASONING_DIM}> {provider.description}</span>
|
|
304
|
+
</text>
|
|
305
|
+
</box>
|
|
306
|
+
);
|
|
307
|
+
})}
|
|
308
|
+
{!copilotAuthenticated && (
|
|
309
|
+
<box marginTop={1}>
|
|
310
|
+
<text>
|
|
311
|
+
<span fg={COLORS.REASONING_DIM}>
|
|
312
|
+
Copilot appears after `gh auth login` and `copilot login`.
|
|
313
|
+
</span>
|
|
314
|
+
</text>
|
|
315
|
+
</box>
|
|
316
|
+
)}
|
|
317
|
+
</box>
|
|
318
|
+
</>
|
|
319
|
+
)}
|
|
320
|
+
|
|
212
321
|
{step === "openrouter_key" && (
|
|
213
322
|
<ApiKeyStep
|
|
214
323
|
{...API_KEY_CONFIGS.openrouter_key}
|
|
@@ -217,6 +326,43 @@ export function OnboardingOverlay({
|
|
|
217
326
|
/>
|
|
218
327
|
)}
|
|
219
328
|
|
|
329
|
+
{step === "copilot_auth" && (
|
|
330
|
+
<>
|
|
331
|
+
<box marginBottom={1}>
|
|
332
|
+
<text>
|
|
333
|
+
<span fg={COLORS.DAEMON_LABEL}>[ {API_KEY_CONFIGS.copilot_auth.title} ]</span>
|
|
334
|
+
</text>
|
|
335
|
+
</box>
|
|
336
|
+
<box marginBottom={1}>
|
|
337
|
+
<text>
|
|
338
|
+
<span fg={COLORS.MENU_TEXT}>
|
|
339
|
+
Run `gh auth login` and `copilot login`, then press ENTER to validate.
|
|
340
|
+
</span>
|
|
341
|
+
</text>
|
|
342
|
+
</box>
|
|
343
|
+
<box marginBottom={1}>
|
|
344
|
+
<text>
|
|
345
|
+
<span fg={COLORS.REASONING_DIM}>
|
|
346
|
+
Copilot token auth is disabled in DAEMON; logged-in CLI session is required.
|
|
347
|
+
</span>
|
|
348
|
+
</text>
|
|
349
|
+
</box>
|
|
350
|
+
<box marginBottom={1}>
|
|
351
|
+
<text>
|
|
352
|
+
<span fg={COLORS.REASONING_DIM}>
|
|
353
|
+
Install/login guide: {API_KEY_CONFIGS.copilot_auth.keyUrl}
|
|
354
|
+
</span>
|
|
355
|
+
<span fg={COLORS.USER_LABEL}> · Shift+O to open</span>
|
|
356
|
+
</text>
|
|
357
|
+
</box>
|
|
358
|
+
<ApiKeyInput
|
|
359
|
+
onSubmit={onKeySubmit}
|
|
360
|
+
placeholder="Press ENTER to validate Copilot login..."
|
|
361
|
+
textareaRef={apiKeyTextareaRef}
|
|
362
|
+
/>
|
|
363
|
+
</>
|
|
364
|
+
)}
|
|
365
|
+
|
|
220
366
|
{step === "openai_key" && (
|
|
221
367
|
<ApiKeyStep
|
|
222
368
|
{...API_KEY_CONFIGS.openai_key}
|
|
@@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
|
|
|
4
4
|
import type {
|
|
5
5
|
AppPreferences,
|
|
6
6
|
InteractionMode,
|
|
7
|
+
LlmProvider,
|
|
7
8
|
VoiceInteractionType,
|
|
8
9
|
SpeechSpeed,
|
|
9
10
|
ReasoningEffort,
|
|
@@ -31,13 +32,17 @@ interface SettingsMenuProps {
|
|
|
31
32
|
speechSpeed: SpeechSpeed;
|
|
32
33
|
reasoningEffort: ReasoningEffort;
|
|
33
34
|
bashApprovalLevel: BashApprovalLevel;
|
|
35
|
+
modelProvider: LlmProvider;
|
|
34
36
|
supportsReasoning: boolean;
|
|
37
|
+
supportsReasoningXHigh: boolean;
|
|
38
|
+
copilotAvailable: boolean;
|
|
35
39
|
canEnableVoiceOutput: boolean;
|
|
36
40
|
showFullReasoning: boolean;
|
|
37
41
|
showToolOutput: boolean;
|
|
38
42
|
memoryEnabled: boolean;
|
|
39
43
|
onClose: () => void;
|
|
40
44
|
toggleInteractionMode: () => void;
|
|
45
|
+
cycleModelProvider: () => void;
|
|
41
46
|
setVoiceInteractionType: (type: VoiceInteractionType) => void;
|
|
42
47
|
setSpeechSpeed: (speed: SpeechSpeed) => void;
|
|
43
48
|
setReasoningEffort: (effort: ReasoningEffort) => void;
|
|
@@ -54,13 +59,17 @@ export function SettingsMenu({
|
|
|
54
59
|
speechSpeed,
|
|
55
60
|
reasoningEffort,
|
|
56
61
|
bashApprovalLevel,
|
|
62
|
+
modelProvider,
|
|
57
63
|
supportsReasoning,
|
|
64
|
+
supportsReasoningXHigh,
|
|
65
|
+
copilotAvailable,
|
|
58
66
|
canEnableVoiceOutput,
|
|
59
67
|
showFullReasoning,
|
|
60
68
|
showToolOutput,
|
|
61
69
|
memoryEnabled,
|
|
62
70
|
onClose,
|
|
63
71
|
toggleInteractionMode,
|
|
72
|
+
cycleModelProvider,
|
|
64
73
|
setVoiceInteractionType,
|
|
65
74
|
setSpeechSpeed,
|
|
66
75
|
setReasoningEffort,
|
|
@@ -93,6 +102,18 @@ export function SettingsMenu({
|
|
|
93
102
|
isToggle: true,
|
|
94
103
|
disabled: interactionModeLocked,
|
|
95
104
|
},
|
|
105
|
+
{
|
|
106
|
+
id: "model-provider",
|
|
107
|
+
label: "Model Provider",
|
|
108
|
+
value: modelProvider === "copilot" ? "COPILOT" : "OPENROUTER",
|
|
109
|
+
description:
|
|
110
|
+
!copilotAvailable && modelProvider === "openrouter"
|
|
111
|
+
? "OpenRouter API with provider routing (Copilot: run `gh auth login` + `copilot login`)"
|
|
112
|
+
: modelProvider === "copilot"
|
|
113
|
+
? "GitHub Copilot session runtime"
|
|
114
|
+
: "OpenRouter API with provider routing",
|
|
115
|
+
isToggle: true,
|
|
116
|
+
},
|
|
96
117
|
{
|
|
97
118
|
id: "voice-interaction-type",
|
|
98
119
|
label: "Voice Flow",
|
|
@@ -108,7 +129,9 @@ export function SettingsMenu({
|
|
|
108
129
|
label: "Reasoning Effort",
|
|
109
130
|
value: supportsReasoning ? REASONING_EFFORT_LABELS[reasoningEffort] : "N/A",
|
|
110
131
|
description: supportsReasoning
|
|
111
|
-
?
|
|
132
|
+
? supportsReasoningXHigh
|
|
133
|
+
? "Depth of reasoning (LOW / MEDIUM / HIGH / XHIGH)"
|
|
134
|
+
: "Depth of reasoning (LOW / MEDIUM / HIGH)"
|
|
112
135
|
: "Not supported by current model",
|
|
113
136
|
isCyclic: supportsReasoning,
|
|
114
137
|
},
|
|
@@ -187,17 +210,20 @@ export function SettingsMenu({
|
|
|
187
210
|
selectedIdx,
|
|
188
211
|
menuItemCount: selectableCount,
|
|
189
212
|
interactionMode,
|
|
213
|
+
modelProvider,
|
|
190
214
|
voiceInteractionType,
|
|
191
215
|
speechSpeed,
|
|
192
216
|
reasoningEffort,
|
|
193
217
|
bashApprovalLevel,
|
|
194
218
|
supportsReasoning,
|
|
219
|
+
supportsReasoningXHigh,
|
|
195
220
|
canEnableVoiceOutput,
|
|
196
221
|
showFullReasoning,
|
|
197
222
|
showToolOutput,
|
|
198
223
|
memoryEnabled,
|
|
199
224
|
setSelectedIdx,
|
|
200
225
|
toggleInteractionMode,
|
|
226
|
+
cycleModelProvider,
|
|
201
227
|
setVoiceInteractionType,
|
|
202
228
|
setSpeechSpeed,
|
|
203
229
|
setReasoningEffort,
|
|
@@ -10,15 +10,17 @@ import { type ModelMetadata, calculateCost, formatContextUsage, formatCost } fro
|
|
|
10
10
|
interface TokenUsageDisplayProps {
|
|
11
11
|
usage: TokenUsage;
|
|
12
12
|
modelMetadata?: ModelMetadata | null;
|
|
13
|
+
hideCost?: boolean;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
export function TokenUsageDisplay({ usage, modelMetadata }: TokenUsageDisplayProps) {
|
|
16
|
+
export function TokenUsageDisplay({ usage, modelMetadata, hideCost = false }: TokenUsageDisplayProps) {
|
|
16
17
|
const mainPromptTokens = usage.promptTokens;
|
|
17
18
|
const mainCompletionTokens = usage.completionTokens;
|
|
18
19
|
|
|
19
20
|
// Calculate cost if we have pricing info
|
|
20
|
-
const cost =
|
|
21
|
-
|
|
21
|
+
const cost = hideCost
|
|
22
|
+
? null
|
|
23
|
+
: typeof usage.cost === "number"
|
|
22
24
|
? usage.cost
|
|
23
25
|
: modelMetadata?.pricing
|
|
24
26
|
? calculateCost(
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { pathToFiletype } from "@opentui/core";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { COLORS, REASONING_MARKDOWN_STYLE } from "../../../ui/constants";
|
|
4
|
+
import { registerToolLayout } from "../registry";
|
|
5
|
+
import type { ToolHeader, ToolLayoutConfig, ToolLayoutRenderProps } from "../types";
|
|
6
|
+
|
|
7
|
+
type UnknownRecord = Record<string, unknown>;
|
|
8
|
+
|
|
9
|
+
function isRecord(value: unknown): value is UnknownRecord {
|
|
10
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function extractPath(input: unknown): string | null {
|
|
14
|
+
if (!isRecord(input)) return null;
|
|
15
|
+
if ("path" in input && typeof input.path === "string") {
|
|
16
|
+
return input.path;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function extractContent(input: unknown): string | null {
|
|
22
|
+
if (!isRecord(input)) return null;
|
|
23
|
+
if ("content" in input && typeof input.content === "string") {
|
|
24
|
+
return input.content;
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function extractAppend(input: unknown): boolean {
|
|
30
|
+
if (!isRecord(input)) return false;
|
|
31
|
+
if ("append" in input && typeof input.append === "boolean") {
|
|
32
|
+
return input.append;
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function WriteFileBody({ call, result }: ToolLayoutRenderProps) {
|
|
38
|
+
if (!isRecord(result)) return null;
|
|
39
|
+
if (result.success === false && typeof result.error === "string") {
|
|
40
|
+
return (
|
|
41
|
+
<box paddingLeft={2}>
|
|
42
|
+
<text>
|
|
43
|
+
<span fg={COLORS.STATUS_FAILED}>{`error: ${result.error}`}</span>
|
|
44
|
+
</text>
|
|
45
|
+
</box>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
if (result.success !== true) return null;
|
|
49
|
+
|
|
50
|
+
const content = extractContent(call.input) ?? "";
|
|
51
|
+
const path = extractPath(call.input) ?? "";
|
|
52
|
+
|
|
53
|
+
// Detect filetype from path for syntax highlighting
|
|
54
|
+
const filetype = pathToFiletype(path);
|
|
55
|
+
|
|
56
|
+
// Format content preview
|
|
57
|
+
let previewContent = "";
|
|
58
|
+
if (content.trim()) {
|
|
59
|
+
const MAX_LINES = 4;
|
|
60
|
+
const MAX_CHARS = 160;
|
|
61
|
+
const contentLines = content
|
|
62
|
+
.split("\n")
|
|
63
|
+
.slice(0, MAX_LINES)
|
|
64
|
+
.map((line) => (line.length > MAX_CHARS ? `${line.slice(0, MAX_CHARS - 1)}…` : line));
|
|
65
|
+
|
|
66
|
+
const totalLines = content.split("\n").length;
|
|
67
|
+
if (totalLines > MAX_LINES) {
|
|
68
|
+
contentLines.push(`... (${totalLines - MAX_LINES} more lines)`);
|
|
69
|
+
}
|
|
70
|
+
previewContent = contentLines.join("\n");
|
|
71
|
+
} else {
|
|
72
|
+
previewContent = "(empty file)";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<box flexDirection="column" paddingLeft={2} marginTop={0}>
|
|
77
|
+
<box
|
|
78
|
+
borderStyle="single"
|
|
79
|
+
borderColor={COLORS.TOOL_INPUT_BORDER}
|
|
80
|
+
paddingLeft={1}
|
|
81
|
+
paddingRight={1}
|
|
82
|
+
paddingTop={0}
|
|
83
|
+
paddingBottom={0}
|
|
84
|
+
>
|
|
85
|
+
<code
|
|
86
|
+
content={previewContent}
|
|
87
|
+
filetype={filetype}
|
|
88
|
+
syntaxStyle={REASONING_MARKDOWN_STYLE}
|
|
89
|
+
conceal={true}
|
|
90
|
+
drawUnstyledText={false}
|
|
91
|
+
/>
|
|
92
|
+
</box>
|
|
93
|
+
</box>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const writeFileLayout: ToolLayoutConfig = {
|
|
98
|
+
abbreviation: "write",
|
|
99
|
+
|
|
100
|
+
getHeader: (input): ToolHeader | null => {
|
|
101
|
+
const path = extractPath(input);
|
|
102
|
+
if (!path) return null;
|
|
103
|
+
const append = extractAppend(input);
|
|
104
|
+
const filetype = pathToFiletype(path);
|
|
105
|
+
|
|
106
|
+
const parts: string[] = [];
|
|
107
|
+
if (filetype) parts.push(filetype);
|
|
108
|
+
if (append) parts.push("append");
|
|
109
|
+
|
|
110
|
+
const secondary = parts.length > 0 ? parts.join(" · ") : undefined;
|
|
111
|
+
return { primary: path, secondary, secondaryStyle: "dim" };
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
renderBody: WriteFileBody,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
registerToolLayout("writeFile", writeFileLayout);
|
|
@@ -57,6 +57,31 @@ function clearAvatarToolEffects(avatar: DaemonAvatarRenderable | null): void {
|
|
|
57
57
|
avatar.setTypingMode(false);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
function normalizeToolCallId(toolCallId: string | undefined): string | undefined {
|
|
61
|
+
if (typeof toolCallId !== "string") return undefined;
|
|
62
|
+
const trimmed = toolCallId.trim();
|
|
63
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isInProgressToolCall(toolCall: ToolCall | undefined): boolean {
|
|
67
|
+
const status = toolCall?.status;
|
|
68
|
+
return status === "streaming" || status === "running" || status === "awaiting_approval";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function findExistingToolCall(
|
|
72
|
+
refs: EventHandlerRefs,
|
|
73
|
+
toolName: string,
|
|
74
|
+
toolCallId: string | undefined
|
|
75
|
+
): ToolCall | undefined {
|
|
76
|
+
if (toolCallId) {
|
|
77
|
+
return refs.toolCallsByIdRef.current.get(toolCallId);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return [...refs.toolCallsRef.current]
|
|
81
|
+
.reverse()
|
|
82
|
+
.find((call) => call.name === toolName && isInProgressToolCall(call));
|
|
83
|
+
}
|
|
84
|
+
|
|
60
85
|
export function createMemorySavedHandler() {
|
|
61
86
|
return (preview: MemoryToastPreview) => {
|
|
62
87
|
const description = preview.description?.trim();
|
|
@@ -401,15 +426,27 @@ export function createToolInputStartHandler(
|
|
|
401
426
|
blocks.pop();
|
|
402
427
|
}
|
|
403
428
|
|
|
429
|
+
const normalizedToolCallId = normalizeToolCallId(toolCallId);
|
|
430
|
+
const existingToolCall = findExistingToolCall(refs, toolName, normalizedToolCallId);
|
|
431
|
+
if (existingToolCall) {
|
|
432
|
+
if (existingToolCall.status === undefined || existingToolCall.status === "streaming") {
|
|
433
|
+
existingToolCall.status = "streaming";
|
|
434
|
+
}
|
|
435
|
+
setters.setCurrentContentBlocks([...blocks]);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
404
439
|
const toolCall: ToolCall = {
|
|
405
440
|
name: toolName,
|
|
406
441
|
input: undefined,
|
|
407
|
-
toolCallId,
|
|
442
|
+
toolCallId: normalizedToolCallId,
|
|
408
443
|
status: "streaming",
|
|
409
444
|
subagentSteps: toolName === "subagent" ? [] : undefined,
|
|
410
445
|
};
|
|
411
446
|
refs.toolCallsRef.current.push(toolCall);
|
|
412
|
-
|
|
447
|
+
if (normalizedToolCallId) {
|
|
448
|
+
refs.toolCallsByIdRef.current.set(normalizedToolCallId, toolCall);
|
|
449
|
+
}
|
|
413
450
|
|
|
414
451
|
blocks.push({ type: "tool", call: toolCall });
|
|
415
452
|
setters.setCurrentContentBlocks([...blocks]);
|
|
@@ -437,18 +474,28 @@ export function createToolInvocationHandler(
|
|
|
437
474
|
|
|
438
475
|
const blocks = refs.contentBlocksRef.current;
|
|
439
476
|
|
|
440
|
-
const
|
|
441
|
-
|
|
477
|
+
const normalizedToolCallId = normalizeToolCallId(toolCallId);
|
|
478
|
+
const existingToolCall = findExistingToolCall(refs, toolName, normalizedToolCallId);
|
|
479
|
+
if (existingToolCall) {
|
|
480
|
+
const shouldActivateToolAnimation =
|
|
481
|
+
existingToolCall.status === undefined ||
|
|
482
|
+
existingToolCall.status === "streaming" ||
|
|
483
|
+
existingToolCall.status === "awaiting_approval";
|
|
484
|
+
|
|
442
485
|
existingToolCall.input = input;
|
|
443
|
-
|
|
486
|
+
if (shouldActivateToolAnimation) {
|
|
487
|
+
existingToolCall.status = "running";
|
|
488
|
+
}
|
|
444
489
|
setters.setCurrentContentBlocks([...blocks]);
|
|
445
490
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
491
|
+
if (shouldActivateToolAnimation) {
|
|
492
|
+
const avatar = refs.avatarRef.current;
|
|
493
|
+
if (avatar) {
|
|
494
|
+
const category = getToolCategory(toolName);
|
|
495
|
+
avatar.triggerToolFlash(category as ToolCategory | undefined);
|
|
496
|
+
if (category !== "fast") {
|
|
497
|
+
avatar.setToolActive(true, category as ToolCategory | undefined);
|
|
498
|
+
}
|
|
452
499
|
}
|
|
453
500
|
}
|
|
454
501
|
return;
|
|
@@ -462,14 +509,14 @@ export function createToolInvocationHandler(
|
|
|
462
509
|
const toolCall: ToolCall = {
|
|
463
510
|
name: toolName,
|
|
464
511
|
input,
|
|
465
|
-
toolCallId,
|
|
512
|
+
toolCallId: normalizedToolCallId,
|
|
466
513
|
status: "running",
|
|
467
514
|
subagentSteps: toolName === "subagent" ? [] : undefined,
|
|
468
515
|
};
|
|
469
516
|
refs.toolCallsRef.current.push(toolCall);
|
|
470
517
|
|
|
471
|
-
if (
|
|
472
|
-
refs.toolCallsByIdRef.current.set(
|
|
518
|
+
if (normalizedToolCallId) {
|
|
519
|
+
refs.toolCallsByIdRef.current.set(normalizedToolCallId, toolCall);
|
|
473
520
|
}
|
|
474
521
|
|
|
475
522
|
blocks.push({ type: "tool", call: toolCall });
|