@makefinks/daemon 0.4.0 → 0.6.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/App.tsx +3 -0
- package/src/app/components/AvatarLayer.tsx +96 -24
- package/src/app/components/ConversationPane.tsx +9 -8
- package/src/avatar/DaemonAvatarRenderable.ts +26 -3
- package/src/avatar/daemon-avatar-rig.ts +10 -1159
- package/src/avatar/rig/core/rig-engine.ts +202 -0
- package/src/avatar/rig/core/rig-types.ts +17 -0
- package/src/avatar/rig/scene/create-scene-elements.ts +298 -0
- package/src/avatar/rig/state/rig-state.ts +193 -0
- package/src/avatar/rig/theme/rig-theme.ts +31 -0
- package/src/avatar/rig/tools/rig-tools.ts +8 -0
- package/src/avatar/rig/update/update-core.ts +32 -0
- package/src/avatar/rig/update/update-eye.ts +31 -0
- package/src/avatar/rig/update/update-fragments.ts +46 -0
- package/src/avatar/rig/update/update-glitch.ts +64 -0
- package/src/avatar/rig/update/update-idle.ts +95 -0
- package/src/avatar/rig/update/update-intensity.ts +16 -0
- package/src/avatar/rig/update/update-main-anchor.ts +20 -0
- package/src/avatar/rig/update/update-particles.ts +49 -0
- package/src/avatar/rig/update/update-rings.ts +35 -0
- package/src/avatar/rig/update/update-sigils.ts +26 -0
- package/src/avatar/rig/update/update-spawn.ts +83 -0
- package/src/avatar/rig/utils/math.ts +17 -0
- package/src/components/ContentBlockView.tsx +6 -1
- 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-controller.ts +51 -1
- package/src/hooks/use-app-display-state.ts +1 -1
- package/src/hooks/use-glitchy-banner.ts +175 -0
- package/src/hooks/use-reasoning-animation.ts +1 -1
- package/src/state/daemon-state.ts +3 -3
- package/src/ui/reasoning-ticker.tsx +4 -1
- package/src/ui/startup.ts +5 -0
- package/src/utils/debug-logger.ts +34 -23
|
@@ -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,4 +1,4 @@
|
|
|
1
|
-
import { useRenderer } from "@opentui/react";
|
|
1
|
+
import { useOnResize, useRenderer } from "@opentui/react";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
3
3
|
|
|
4
4
|
import type { ConversationPaneProps } from "../app/components/ConversationPane";
|
|
@@ -21,6 +21,7 @@ import { useSessionController } from "./use-session-controller";
|
|
|
21
21
|
import { getDaemonManager } from "../state/daemon-state";
|
|
22
22
|
import { deleteSession } from "../state/session-store";
|
|
23
23
|
import { DaemonState } from "../types";
|
|
24
|
+
import { STARTUP_BANNER_DURATION_MS, STARTUP_IDLE_CHROME_LEAD_MS } from "../ui/startup";
|
|
24
25
|
|
|
25
26
|
export interface AppControllerResult {
|
|
26
27
|
handleCopyOnSelectMouseUp: () => void;
|
|
@@ -31,6 +32,9 @@ export interface AppControllerResult {
|
|
|
31
32
|
width: number;
|
|
32
33
|
height: number;
|
|
33
34
|
zIndex: number;
|
|
35
|
+
showBanner: boolean;
|
|
36
|
+
animateBanner: boolean;
|
|
37
|
+
startupAnimationActive: boolean;
|
|
34
38
|
};
|
|
35
39
|
isListeningDim: boolean;
|
|
36
40
|
listeningDimTop: number;
|
|
@@ -54,6 +58,18 @@ export function useAppController({
|
|
|
54
58
|
const { handleCopyOnSelectMouseUp } = useCopyOnSelect();
|
|
55
59
|
|
|
56
60
|
const [preferencesLoaded, setPreferencesLoaded] = useState(false);
|
|
61
|
+
const [terminalSize, setTerminalSize] = useState({
|
|
62
|
+
width: renderer.terminalWidth,
|
|
63
|
+
height: renderer.terminalHeight,
|
|
64
|
+
});
|
|
65
|
+
// Track if this is initial app load for startup animation
|
|
66
|
+
const [isInitialLoad, setIsInitialLoad] = useState(true);
|
|
67
|
+
const [startupIntroDone, setStartupIntroDone] = useState(false);
|
|
68
|
+
|
|
69
|
+
// Update terminal size state on resize to trigger re-render
|
|
70
|
+
useOnResize((width, height) => {
|
|
71
|
+
setTerminalSize({ width, height });
|
|
72
|
+
});
|
|
57
73
|
|
|
58
74
|
const menus = useAppMenus();
|
|
59
75
|
const {
|
|
@@ -119,6 +135,19 @@ export function useAppController({
|
|
|
119
135
|
preferencesLoaded,
|
|
120
136
|
showDeviceMenu,
|
|
121
137
|
});
|
|
138
|
+
const onboardingComplete = preferencesLoaded && !bootstrap.onboardingActive;
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (!onboardingComplete) {
|
|
142
|
+
setStartupIntroDone(false);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// Delay idle UI chrome (status/hotkeys) so the banner can resolve first.
|
|
146
|
+
const delayMs = Math.max(0, STARTUP_BANNER_DURATION_MS - STARTUP_IDLE_CHROME_LEAD_MS);
|
|
147
|
+
setStartupIntroDone(false);
|
|
148
|
+
const t = setTimeout(() => setStartupIntroDone(true), delayMs);
|
|
149
|
+
return () => clearTimeout(t);
|
|
150
|
+
}, [onboardingComplete]);
|
|
122
151
|
|
|
123
152
|
const daemon = useDaemonRuntimeController({
|
|
124
153
|
currentModelId,
|
|
@@ -373,6 +402,21 @@ export function useAppController({
|
|
|
373
402
|
}
|
|
374
403
|
}, [daemon.daemonState]);
|
|
375
404
|
|
|
405
|
+
// Turn off initial load state once user interacts (banner animation is one-time only)
|
|
406
|
+
useEffect(() => {
|
|
407
|
+
if (daemon.hasInteracted && isInitialLoad) {
|
|
408
|
+
setIsInitialLoad(false);
|
|
409
|
+
}
|
|
410
|
+
}, [daemon.hasInteracted, isInitialLoad]);
|
|
411
|
+
|
|
412
|
+
useEffect(() => {
|
|
413
|
+
if (daemon.hasInteracted && !startupIntroDone) {
|
|
414
|
+
setStartupIntroDone(true);
|
|
415
|
+
}
|
|
416
|
+
}, [daemon.hasInteracted, startupIntroDone]);
|
|
417
|
+
|
|
418
|
+
const startupAnimationActive = onboardingComplete && isInitialLoad;
|
|
419
|
+
|
|
376
420
|
const appContextValue = useAppContextBuilder({
|
|
377
421
|
menus: {
|
|
378
422
|
showDeviceMenu,
|
|
@@ -485,6 +529,11 @@ export function useAppController({
|
|
|
485
529
|
width: avatarWidth,
|
|
486
530
|
height: avatarHeight,
|
|
487
531
|
zIndex: isListening && daemon.hasInteracted ? 2 : 0,
|
|
532
|
+
// Show banner only when idle, not interacted, and terminal is large enough
|
|
533
|
+
showBanner:
|
|
534
|
+
onboardingComplete && !daemon.hasInteracted && terminalSize.height >= 30 && terminalSize.width >= 100,
|
|
535
|
+
animateBanner: startupAnimationActive,
|
|
536
|
+
startupAnimationActive,
|
|
488
537
|
},
|
|
489
538
|
isListeningDim,
|
|
490
539
|
listeningDimTop: statusBarHeight,
|
|
@@ -536,6 +585,7 @@ export function useAppController({
|
|
|
536
585
|
modelName: modelName ?? "",
|
|
537
586
|
sessionTitle: sessionTitle ?? "",
|
|
538
587
|
isVoiceOutputEnabled: interactionMode === "voice",
|
|
588
|
+
startupIntroDone,
|
|
539
589
|
},
|
|
540
590
|
appContextValue,
|
|
541
591
|
overlaysProps: {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import { DaemonState } from "../types";
|
|
3
3
|
import type { ContentBlock, SessionInfo } from "../types";
|
|
4
|
-
import type { ModelMetadata } from "../utils/model-metadata";
|
|
5
4
|
import { COLORS, STATE_COLOR_HEX, STATUS_TEXT } from "../ui/constants";
|
|
6
5
|
import { formatElapsedTime } from "../utils/formatters";
|
|
6
|
+
import type { ModelMetadata } from "../utils/model-metadata";
|
|
7
7
|
|
|
8
8
|
export interface UseAppDisplayStateParams {
|
|
9
9
|
daemonState: DaemonState;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import { STARTUP_BANNER_DURATION_MS } from "../ui/startup";
|
|
4
|
+
|
|
5
|
+
const DAEMON_BANNER_LINES = [
|
|
6
|
+
"88888888ba, db 88888888888 88b d88 ,ad8888ba, 888b 88",
|
|
7
|
+
'88 `"8b d88b 88 888b d888 d8"\' `"8b 8888b 88',
|
|
8
|
+
"88 `8b d8'`8b 88 88`8b d8'88 d8' `8b 88 `8b 88",
|
|
9
|
+
"88 88 d8' `8b 88aaaaa 88 `8b d8' 88 88 88 88 `8b 88",
|
|
10
|
+
'88 88 d8YaaaaY8b 88""""" 88 `8b d8\' 88 88 88 88 `8b 88',
|
|
11
|
+
'88 8P d8""""""""8b 88 88 `8b d8\' 88 Y8, ,8P 88 `8b 88',
|
|
12
|
+
"88 .a8P d8' `8b 88 88 `888' 88 Y8a. .a8P 88 `8888",
|
|
13
|
+
"88888888Y\"' d8' `8b 88888888888 88 `8' 88 `\"Y8888Y\"' 88 `888",
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const BANNER_GRADIENT = [
|
|
17
|
+
"#8a3434",
|
|
18
|
+
"#7a2f2f",
|
|
19
|
+
"#692929",
|
|
20
|
+
"#582323",
|
|
21
|
+
"#481c1c",
|
|
22
|
+
"#371515",
|
|
23
|
+
"#260f0f",
|
|
24
|
+
"#160808",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// glitch chars
|
|
28
|
+
const GLITCH_CHARS = "!@#$%^&*()_+-=[]{}|;:',.<>?/\\`~01";
|
|
29
|
+
|
|
30
|
+
const BANNER_ANIMATION_DURATION = STARTUP_BANNER_DURATION_MS;
|
|
31
|
+
const LINE_STAGGER_MS = 80;
|
|
32
|
+
|
|
33
|
+
export interface GlitchyBannerState {
|
|
34
|
+
lines: string[];
|
|
35
|
+
colors: string[];
|
|
36
|
+
progress: number;
|
|
37
|
+
complete: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Generates a glitched version of a string.
|
|
42
|
+
* @param original The original string
|
|
43
|
+
* @param glitchAmount 0-1, where 1 = fully glitched
|
|
44
|
+
* @param revealProgress 0-1, where 1 = fully revealed from left
|
|
45
|
+
*/
|
|
46
|
+
function glitchString(original: string, glitchAmount: number, revealProgress: number): string {
|
|
47
|
+
const revealedLength = Math.floor(original.length * revealProgress);
|
|
48
|
+
let result = "";
|
|
49
|
+
|
|
50
|
+
for (let i = 0; i < original.length; i++) {
|
|
51
|
+
const char = original[i];
|
|
52
|
+
|
|
53
|
+
if (i >= revealedLength) {
|
|
54
|
+
// Not yet revealed - either empty or glitch
|
|
55
|
+
if (Math.random() < glitchAmount * 0.7) {
|
|
56
|
+
result += GLITCH_CHARS[Math.floor(Math.random() * GLITCH_CHARS.length)];
|
|
57
|
+
} else {
|
|
58
|
+
result += " ";
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
// Revealed area - occasional glitch corruption
|
|
62
|
+
if (char !== " " && Math.random() < glitchAmount * 0.15) {
|
|
63
|
+
result += GLITCH_CHARS[Math.floor(Math.random() * GLITCH_CHARS.length)];
|
|
64
|
+
} else {
|
|
65
|
+
result += char;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Generates glitched color - subtle brightness/saturation shifts within red spectrum
|
|
75
|
+
*/
|
|
76
|
+
function glitchColor(baseColor: string, glitchAmount: number): string {
|
|
77
|
+
if (glitchAmount > 0.2 && Math.random() < glitchAmount * 0.4) {
|
|
78
|
+
const hex = baseColor.replace("#", "");
|
|
79
|
+
const r = parseInt(hex.substring(0, 2), 16);
|
|
80
|
+
const g = parseInt(hex.substring(2, 4), 16);
|
|
81
|
+
const b = parseInt(hex.substring(4, 6), 16);
|
|
82
|
+
|
|
83
|
+
// Subtle variation: shift brightness and add slight color temperature changes
|
|
84
|
+
const brightnessShift = (Math.random() - 0.3) * glitchAmount * 80;
|
|
85
|
+
const newR = Math.max(0, Math.min(255, r + brightnessShift + Math.random() * 30));
|
|
86
|
+
const newG = Math.max(0, Math.min(255, g + brightnessShift * 0.3));
|
|
87
|
+
const newB = Math.max(0, Math.min(255, b + brightnessShift * 0.2));
|
|
88
|
+
|
|
89
|
+
return `#${Math.round(newR).toString(16).padStart(2, "0")}${Math.round(newG).toString(16).padStart(2, "0")}${Math.round(newB).toString(16).padStart(2, "0")}`;
|
|
90
|
+
}
|
|
91
|
+
return baseColor;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function useGlitchyBanner(isActive: boolean): GlitchyBannerState {
|
|
95
|
+
const [state, setState] = useState<GlitchyBannerState>({
|
|
96
|
+
lines: DAEMON_BANNER_LINES.map(() => ""),
|
|
97
|
+
colors: BANNER_GRADIENT.map(() => "#000000"),
|
|
98
|
+
progress: 0,
|
|
99
|
+
complete: false,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Animation loop
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (!isActive) {
|
|
105
|
+
setState({
|
|
106
|
+
lines: DAEMON_BANNER_LINES.map(() => ""),
|
|
107
|
+
colors: BANNER_GRADIENT.map(() => "#000000"),
|
|
108
|
+
progress: 0,
|
|
109
|
+
complete: false,
|
|
110
|
+
});
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const startTime = performance.now();
|
|
115
|
+
const animate = () => {
|
|
116
|
+
const elapsed = performance.now() - startTime;
|
|
117
|
+
const progress = Math.min(1, elapsed / BANNER_ANIMATION_DURATION);
|
|
118
|
+
|
|
119
|
+
if (progress >= 1) {
|
|
120
|
+
// Animation complete, show final state
|
|
121
|
+
setState({
|
|
122
|
+
lines: [...DAEMON_BANNER_LINES],
|
|
123
|
+
colors: [...BANNER_GRADIENT],
|
|
124
|
+
progress: 1,
|
|
125
|
+
complete: true,
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Calculate glitch intensity (high at start, fades out)
|
|
131
|
+
const glitchIntensity = Math.pow(1 - progress, 2);
|
|
132
|
+
|
|
133
|
+
// Generate glitched lines with staggered reveal
|
|
134
|
+
const lines = DAEMON_BANNER_LINES.map((line, i) => {
|
|
135
|
+
const lineStartTime = i * LINE_STAGGER_MS;
|
|
136
|
+
const lineElapsed = Math.max(0, elapsed - lineStartTime);
|
|
137
|
+
const lineProgress = Math.min(1, lineElapsed / (BANNER_ANIMATION_DURATION - i * LINE_STAGGER_MS));
|
|
138
|
+
|
|
139
|
+
// Reveal from left + glitch effect
|
|
140
|
+
const revealProgress = Math.pow(lineProgress, 0.7); // Ease out
|
|
141
|
+
return glitchString(line, glitchIntensity, revealProgress);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Generate colors with occasional glitch flash
|
|
145
|
+
const colors = BANNER_GRADIENT.map((color, i) => {
|
|
146
|
+
const lineStartTime = i * LINE_STAGGER_MS;
|
|
147
|
+
const lineElapsed = Math.max(0, elapsed - lineStartTime);
|
|
148
|
+
const lineProgress = Math.min(1, lineElapsed / (BANNER_ANIMATION_DURATION - i * LINE_STAGGER_MS));
|
|
149
|
+
|
|
150
|
+
if (lineProgress < 0.1) {
|
|
151
|
+
return "#000000";
|
|
152
|
+
}
|
|
153
|
+
return glitchColor(color, glitchIntensity);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
setState({
|
|
157
|
+
lines,
|
|
158
|
+
colors,
|
|
159
|
+
progress,
|
|
160
|
+
complete: false,
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// Run at ~30fps for glitchy effect
|
|
165
|
+
const intervalId = setInterval(animate, 33);
|
|
166
|
+
// Run once immediately
|
|
167
|
+
animate();
|
|
168
|
+
|
|
169
|
+
return () => clearInterval(intervalId);
|
|
170
|
+
}, [isActive]);
|
|
171
|
+
|
|
172
|
+
return state;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export { DAEMON_BANNER_LINES, BANNER_GRADIENT };
|
|
@@ -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 -
|
|
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
|
-
|
|
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,
|
|
@@ -28,7 +28,10 @@ export function renderReasoningTicker(reasoningDisplay: string) {
|
|
|
28
28
|
|
|
29
29
|
return (
|
|
30
30
|
<text>
|
|
31
|
-
<span fg={
|
|
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
|
|
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);
|