@makefinks/daemon 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "module": "src/index.tsx",
30
30
  "type": "module",
31
- "version": "0.5.0",
31
+ "version": "0.6.1",
32
32
  "bin": {
33
33
  "daemon": "dist/cli.js"
34
34
  },
package/src/app/App.tsx CHANGED
@@ -70,6 +70,9 @@ export function App() {
70
70
  width={controller.avatarLayerProps.width}
71
71
  height={controller.avatarLayerProps.height}
72
72
  zIndex={controller.avatarLayerProps.zIndex}
73
+ showBanner={controller.avatarLayerProps.showBanner}
74
+ animateBanner={controller.avatarLayerProps.animateBanner}
75
+ startupAnimationActive={controller.avatarLayerProps.startupAnimationActive}
73
76
  />
74
77
 
75
78
  {controller.isListeningDim ? (
@@ -1,6 +1,7 @@
1
- import { memo, useCallback } from "react";
1
+ import { memo, useCallback, useEffect, useRef } from "react";
2
2
  import type { RefObject } from "react";
3
3
  import type { DaemonAvatarRenderable } from "../../avatar/DaemonAvatarRenderable";
4
+ import { BANNER_GRADIENT, DAEMON_BANNER_LINES, useGlitchyBanner } from "../../hooks/use-glitchy-banner";
4
5
  import { DaemonState } from "../../types";
5
6
 
6
7
  export interface AvatarLayerProps {
@@ -10,41 +11,112 @@ export interface AvatarLayerProps {
10
11
  width: number;
11
12
  height: number;
12
13
  zIndex?: number;
14
+ showBanner?: boolean;
15
+ animateBanner?: boolean;
16
+ startupAnimationActive?: boolean;
13
17
  }
14
18
 
15
19
  function AvatarLayerImpl(props: AvatarLayerProps) {
16
- const { avatarRef, daemonState, applyAvatarForState, width, height, zIndex = 0 } = props;
20
+ const {
21
+ avatarRef,
22
+ daemonState,
23
+ applyAvatarForState,
24
+ width,
25
+ height,
26
+ zIndex = 0,
27
+ showBanner = false,
28
+ animateBanner = false,
29
+ startupAnimationActive = false,
30
+ } = props;
31
+
32
+ // Use glitchy banner animation when animateBanner is true
33
+ const glitchyBanner = useGlitchyBanner(showBanner && animateBanner);
34
+
35
+ // Determine which lines/colors to use
36
+ const bannerLines = animateBanner ? glitchyBanner.lines : DAEMON_BANNER_LINES;
37
+ const bannerColors = animateBanner ? glitchyBanner.colors : BANNER_GRADIENT;
38
+
39
+ // Keep a stable callback ref so we don't detach/reattach on daemonState changes.
40
+ const daemonStateRef = useRef(daemonState);
41
+ const applyAvatarForStateRef = useRef(applyAvatarForState);
42
+ const startupAnimationActiveRef = useRef(startupAnimationActive);
43
+
44
+ useEffect(() => {
45
+ daemonStateRef.current = daemonState;
46
+ }, [daemonState]);
47
+ useEffect(() => {
48
+ applyAvatarForStateRef.current = applyAvatarForState;
49
+ }, [applyAvatarForState]);
50
+ useEffect(() => {
51
+ startupAnimationActiveRef.current = startupAnimationActive;
52
+ }, [startupAnimationActive]);
17
53
 
18
54
  const handleAvatarRef = useCallback(
19
55
  (ref: DaemonAvatarRenderable | null) => {
56
+ if (ref === avatarRef.current) return;
20
57
  avatarRef.current = ref;
21
- if (ref) {
22
- applyAvatarForState(daemonState);
58
+ if (!ref) return;
59
+
60
+ applyAvatarForStateRef.current(daemonStateRef.current);
61
+ if (startupAnimationActiveRef.current) {
62
+ ref.resetSpawn();
63
+ } else {
64
+ ref.skipSpawn();
23
65
  }
24
66
  },
25
- [avatarRef, applyAvatarForState, daemonState]
67
+ [avatarRef]
26
68
  );
27
69
 
70
+ useEffect(() => {
71
+ const ref = avatarRef.current;
72
+ if (!ref) return;
73
+ if (startupAnimationActive) {
74
+ ref.resetSpawn();
75
+ } else {
76
+ ref.skipSpawn();
77
+ }
78
+ }, [avatarRef, startupAnimationActive]);
79
+
28
80
  return (
29
- <box
30
- position="absolute"
31
- top={0}
32
- left={0}
33
- width="100%"
34
- height="100%"
35
- alignItems="center"
36
- justifyContent="center"
37
- zIndex={zIndex}
38
- >
39
- <daemon-avatar
40
- id="daemon-avatar"
41
- live
42
- width={width}
43
- height={height}
44
- respectAlpha={true}
45
- ref={handleAvatarRef}
46
- />
47
- </box>
81
+ <>
82
+ {showBanner && (
83
+ <box
84
+ position="absolute"
85
+ top={6}
86
+ left={0}
87
+ width="100%"
88
+ alignItems="center"
89
+ justifyContent="center"
90
+ flexDirection="column"
91
+ zIndex={10}
92
+ >
93
+ {bannerLines.map((line, i) => (
94
+ <text key={i}>
95
+ <span fg={bannerColors[i]}>{line}</span>
96
+ </text>
97
+ ))}
98
+ </box>
99
+ )}
100
+ <box
101
+ position="absolute"
102
+ top={0}
103
+ left={0}
104
+ width="100%"
105
+ height="100%"
106
+ alignItems="center"
107
+ justifyContent="center"
108
+ zIndex={zIndex}
109
+ >
110
+ <daemon-avatar
111
+ id="daemon-avatar"
112
+ live
113
+ width={width}
114
+ height={height}
115
+ respectAlpha={true}
116
+ ref={handleAvatarRef}
117
+ />
118
+ </box>
119
+ </>
48
120
  );
49
121
  }
50
122
 
@@ -7,7 +7,6 @@ import {
7
7
  isLastTextBlockInList,
8
8
  shouldHideContentBlock,
9
9
  } from "../../components/ContentBlockView";
10
- import { DaemonText } from "../../components/DaemonText";
11
10
  import { GroundingBadge } from "../../components/GroundingBadge";
12
11
  import { InlineStatusIndicator } from "../../components/InlineStatusIndicator";
13
12
  import { StatusBar } from "../../components/StatusBar";
@@ -78,6 +77,7 @@ export interface ConversationPaneProps {
78
77
  modelName?: string;
79
78
  sessionTitle?: string;
80
79
  isVoiceOutputEnabled?: boolean;
80
+ startupIntroDone?: boolean;
81
81
  }
82
82
 
83
83
  function ConversationPaneImpl(props: ConversationPaneProps) {
@@ -98,6 +98,7 @@ function ConversationPaneImpl(props: ConversationPaneProps) {
98
98
  modelName,
99
99
  sessionTitle,
100
100
  isVoiceOutputEnabled,
101
+ startupIntroDone = true,
101
102
  } = props;
102
103
 
103
104
  const { conversationHistory, currentTranscription, currentContentBlocks } = conversation;
@@ -193,7 +194,7 @@ function ConversationPaneImpl(props: ConversationPaneProps) {
193
194
  />
194
195
  )}
195
196
 
196
- {!hasInteracted && (
197
+ {!hasInteracted && startupIntroDone && (
197
198
  <box
198
199
  position="absolute"
199
200
  left={0}
@@ -1,16 +1,16 @@
1
1
  import {
2
+ type CliRenderer,
2
3
  FrameBufferRenderable,
4
+ OptimizedBuffer,
3
5
  RGBA,
4
6
  TextAttributes,
5
- OptimizedBuffer,
6
- type CliRenderer,
7
7
  } from "@opentui/core";
8
8
  import { SuperSampleType, ThreeCliRenderer } from "@opentui/core/3d";
9
9
  import {
10
- createDaemonRig,
11
10
  type DaemonColorTheme,
12
11
  type DaemonRig,
13
12
  type ToolCategory,
13
+ createDaemonRig,
14
14
  } from "./daemon-avatar-rig";
15
15
 
16
16
  export type { ToolCategory } from "./daemon-avatar-rig";
@@ -30,6 +30,7 @@ export class DaemonAvatarRenderable extends FrameBufferRenderable {
30
30
  private pendingTheme: DaemonColorTheme | null = null;
31
31
  private pendingIntensity: { value: number; immediate: boolean } | null = null;
32
32
  private pendingAudioLevel: { value: number; immediate: boolean } | null = null;
33
+ private pendingSpawnAction: "reset" | "skip" | null = null;
33
34
 
34
35
  private getDesiredAspectRatio(width: number, height: number): number {
35
36
  if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return 1;
@@ -138,6 +139,14 @@ export class DaemonAvatarRenderable extends FrameBufferRenderable {
138
139
  if (this.pendingAudioLevel) {
139
140
  this.rig.setAudioLevel(this.pendingAudioLevel.value, { immediate: this.pendingAudioLevel.immediate });
140
141
  }
142
+ if (this.pendingSpawnAction) {
143
+ if (this.pendingSpawnAction === "reset") {
144
+ this.rig.resetSpawn();
145
+ } else {
146
+ this.rig.skipSpawn();
147
+ }
148
+ this.pendingSpawnAction = null;
149
+ }
141
150
  this.renderBuffer = OptimizedBuffer.create(
142
151
  this.frameBuffer.width,
143
152
  this.frameBuffer.height,
@@ -232,28 +241,6 @@ export class DaemonAvatarRenderable extends FrameBufferRenderable {
232
241
  const w = fb.width;
233
242
  const h = fb.height;
234
243
  fb.clear(RGBA.fromValues(0, 0, 0, 0));
235
-
236
- const glyph = w >= 18 ? "⟡  SUMMONING  ⟡" : "SUMMONING";
237
- const gx = Math.max(0, Math.floor(w / 2) - Math.floor(glyph.length / 2));
238
- const gy = Math.floor(h / 2);
239
- fb.drawText(
240
- glyph,
241
- gx,
242
- Math.max(0, Math.min(h - 1, gy)),
243
- RGBA.fromInts(180, 255, 245, 230),
244
- RGBA.fromInts(0, 0, 0, 0),
245
- TextAttributes.DIM
246
- );
247
- if (h >= 2) {
248
- fb.drawText(
249
- "DAEMON",
250
- Math.max(0, Math.floor(w / 2) - 3),
251
- Math.max(0, Math.min(h - 1, gy + 1)),
252
- RGBA.fromInts(130, 190, 255, 200),
253
- RGBA.fromInts(0, 0, 0, 0),
254
- TextAttributes.BOLD
255
- );
256
- }
257
244
  } else {
258
245
  this.kickRenderFrame();
259
246
  }
@@ -340,4 +327,18 @@ export class DaemonAvatarRenderable extends FrameBufferRenderable {
340
327
  this.rig.triggerTypingPulse();
341
328
  }
342
329
  }
330
+
331
+ public resetSpawn(): void {
332
+ this.pendingSpawnAction = "reset";
333
+ if (this.rig) {
334
+ this.rig.resetSpawn();
335
+ }
336
+ }
337
+
338
+ public skipSpawn(): void {
339
+ this.pendingSpawnAction = "skip";
340
+ if (this.rig) {
341
+ this.rig.skipSpawn();
342
+ }
343
+ }
343
344
  }