@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.
Files changed (40) hide show
  1. package/package.json +1 -1
  2. package/src/ai/daemon-ai.ts +8 -35
  3. package/src/ai/message-utils.ts +26 -0
  4. package/src/ai/tools/subagents.ts +8 -7
  5. package/src/app/App.tsx +3 -0
  6. package/src/app/components/AvatarLayer.tsx +96 -24
  7. package/src/app/components/ConversationPane.tsx +9 -8
  8. package/src/avatar/DaemonAvatarRenderable.ts +26 -3
  9. package/src/avatar/daemon-avatar-rig.ts +10 -1159
  10. package/src/avatar/rig/core/rig-engine.ts +202 -0
  11. package/src/avatar/rig/core/rig-types.ts +17 -0
  12. package/src/avatar/rig/scene/create-scene-elements.ts +298 -0
  13. package/src/avatar/rig/state/rig-state.ts +193 -0
  14. package/src/avatar/rig/theme/rig-theme.ts +31 -0
  15. package/src/avatar/rig/tools/rig-tools.ts +8 -0
  16. package/src/avatar/rig/update/update-core.ts +32 -0
  17. package/src/avatar/rig/update/update-eye.ts +31 -0
  18. package/src/avatar/rig/update/update-fragments.ts +46 -0
  19. package/src/avatar/rig/update/update-glitch.ts +64 -0
  20. package/src/avatar/rig/update/update-idle.ts +95 -0
  21. package/src/avatar/rig/update/update-intensity.ts +16 -0
  22. package/src/avatar/rig/update/update-main-anchor.ts +20 -0
  23. package/src/avatar/rig/update/update-particles.ts +49 -0
  24. package/src/avatar/rig/update/update-rings.ts +35 -0
  25. package/src/avatar/rig/update/update-sigils.ts +26 -0
  26. package/src/avatar/rig/update/update-spawn.ts +83 -0
  27. package/src/avatar/rig/utils/math.ts +17 -0
  28. package/src/components/ContentBlockView.tsx +6 -1
  29. package/src/components/ToolCallView.tsx +9 -12
  30. package/src/components/tool-layouts/components.tsx +4 -3
  31. package/src/components/tool-layouts/layouts/subagent.tsx +140 -16
  32. package/src/hooks/daemon-event-handlers.ts +3 -3
  33. package/src/hooks/use-app-controller.ts +51 -1
  34. package/src/hooks/use-app-display-state.ts +1 -1
  35. package/src/hooks/use-glitchy-banner.ts +175 -0
  36. package/src/hooks/use-reasoning-animation.ts +1 -1
  37. package/src/state/daemon-state.ts +3 -3
  38. package/src/ui/reasoning-ticker.tsx +4 -1
  39. package/src/ui/startup.ts +5 -0
  40. 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 { 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
 
@@ -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 - 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}
@@ -0,0 +1,5 @@
1
+ export const STARTUP_BANNER_DURATION_MS = 1800;
2
+
3
+ export const STARTUP_IDLE_CHROME_LEAD_MS = 250;
4
+
5
+ export const STARTUP_BANNER_DURATION_S = STARTUP_BANNER_DURATION_MS / 1000;
@@ -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);