@makefinks/daemon 0.5.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.
@@ -0,0 +1,31 @@
1
+ import type { SceneElements } from "../scene/create-scene-elements";
2
+ import type { RigState } from "../state/rig-state";
3
+ import { lerpColor } from "../utils/math";
4
+
5
+ export function updateThemeColors(elements: SceneElements, state: RigState, dt: number): void {
6
+ const { theme, typing, tool } = state;
7
+
8
+ const t = dt * 4;
9
+ theme.current.primary = lerpColor(theme.current.primary, theme.target.primary, t);
10
+ theme.current.glow = lerpColor(theme.current.glow, theme.target.glow, t);
11
+ theme.current.eye = lerpColor(theme.current.eye, theme.target.eye, t);
12
+
13
+ let displayPrimary = theme.current.primary;
14
+ let displayEye = theme.current.eye;
15
+
16
+ if (typing.pulse > 0.01) {
17
+ const flashStrength = Math.pow(typing.pulse, 1.5) * 0.5;
18
+ displayPrimary = lerpColor(displayPrimary, 0xffffff, flashStrength);
19
+ displayEye = lerpColor(displayEye, 0xff8888, flashStrength * 0.3);
20
+ }
21
+
22
+ elements.glowMat.color.setHex(displayPrimary);
23
+ if (tool.flashTimer <= 0) {
24
+ elements.eyeMat.color.setHex(displayEye);
25
+ elements.pupilMat.color.setHex(displayEye);
26
+ }
27
+ elements.pointLight.color.setHex(theme.current.glow);
28
+
29
+ elements.rings.forEach((ring) => ring.material.color.setHex(displayPrimary));
30
+ elements.fragments.forEach((frag) => frag.material.color.setHex(displayPrimary));
31
+ }
@@ -0,0 +1,8 @@
1
+ export type ToolCategory = "web" | "file" | "bash" | "subagent";
2
+
3
+ export const TOOL_CATEGORY_COLORS: Record<ToolCategory, number> = {
4
+ web: 0x22d3ee,
5
+ file: 0x4ade80,
6
+ bash: 0xfbbf24,
7
+ subagent: 0xa78bfa,
8
+ };
@@ -0,0 +1,32 @@
1
+ import type { SceneElements } from "../scene/create-scene-elements";
2
+ import type { RigState } from "../state/rig-state";
3
+ import { clamp01 } from "../utils/math";
4
+
5
+ export function updateCore(elements: SceneElements, state: RigState, dt: number, intensity: number): void {
6
+ const { phase, audio, reasoning } = state;
7
+
8
+ const coreSpeed = 0.1 + intensity * 0.9;
9
+ elements.glowMesh.rotation.y += dt * coreSpeed;
10
+ elements.glowMesh.rotation.x += dt * coreSpeed * 0.6;
11
+
12
+ const glowScale = 1 + intensity * 0.25;
13
+ elements.glowMesh.scale.setScalar(glowScale);
14
+ elements.glowMat.opacity = clamp01(0.4 + intensity * 0.4 + audio.current * 0.25);
15
+ const normalCorePulseSpeed = 1 + intensity * 4;
16
+ const reasoningCorePulseSpeed = 0.4;
17
+ const corePulseSpeed =
18
+ normalCorePulseSpeed * (1 - reasoning.blend) + reasoningCorePulseSpeed * reasoning.blend;
19
+ phase.corePulse += dt * corePulseSpeed;
20
+
21
+ const normalCorePulseAmount = 0.01 + intensity * 0.15;
22
+ const reasoningCorePulseAmount = 0.08;
23
+ const corePulseAmount =
24
+ normalCorePulseAmount * (1 - reasoning.blend) + reasoningCorePulseAmount * reasoning.blend;
25
+ const corePulse = 1 + Math.sin(phase.corePulse) * corePulseAmount;
26
+
27
+ elements.coreGroup.scale.setScalar(corePulse * (1 + audio.current * 0.18));
28
+
29
+ const squashStretch = 1 + audio.current * 0.15;
30
+ elements.coreMesh.scale.set(1, squashStretch, 1);
31
+ elements.glowMesh.scale.y *= squashStretch;
32
+ }
@@ -0,0 +1,31 @@
1
+ import type { SceneElements } from "../scene/create-scene-elements";
2
+ import type { RigState } from "../state/rig-state";
3
+ import { clamp01, lerpColor } from "../utils/math";
4
+
5
+ export function updateEye(elements: SceneElements, state: RigState, dt: number, intensity: number): void {
6
+ const { phase, audio, typing, reasoning, tool, theme } = state;
7
+
8
+ const eyeSpeed = 1.5 + intensity * 4;
9
+ phase.eyePulse += dt * eyeSpeed;
10
+ const eyePulseAmount = 0.1 + intensity * 0.3 + typing.pulse * 0.1;
11
+ const eyePulse = 0.9 + Math.sin(phase.eyePulse) * eyePulseAmount;
12
+ elements.eye.scale.setScalar(eyePulse * (1 + audio.current * 0.1));
13
+
14
+ const pupilSpeed = 2 + intensity * 5;
15
+ phase.pupilPulse += dt * pupilSpeed;
16
+ const pupilPulseAmount = 0.15 + intensity * 0.35 + typing.pulse * 0.15;
17
+ const normalPupilBase = 0.8 + Math.sin(phase.pupilPulse) * pupilPulseAmount;
18
+ const reasoningPupilDilation = 1.4;
19
+ const pupilBase = normalPupilBase * (1 - reasoning.blend) + reasoningPupilDilation * reasoning.blend;
20
+ elements.pupil.scale.setScalar(pupilBase * (1 + audio.current * 0.08));
21
+
22
+ if (tool.flashTimer > 0) {
23
+ tool.flashTimer -= dt;
24
+ const flashIntensity = clamp01(tool.flashTimer / 0.15);
25
+ elements.eyeMat.color.setHex(lerpColor(theme.current.eye, tool.flashColor, flashIntensity));
26
+ elements.pupilMat.color.setHex(lerpColor(theme.current.eye, tool.flashColor, flashIntensity));
27
+ }
28
+
29
+ elements.eyeMat.opacity = clamp01(0.85 + intensity * 0.15 + audio.current * 0.15);
30
+ elements.pupilMat.opacity = clamp01(0.9 + intensity * 0.1 + audio.current * 0.1);
31
+ }
@@ -0,0 +1,46 @@
1
+ import type { SceneElements } from "../scene/create-scene-elements";
2
+ import type { RigState } from "../state/rig-state";
3
+ import { clamp01 } from "../utils/math";
4
+
5
+ export function updateFragments(
6
+ elements: SceneElements,
7
+ state: RigState,
8
+ dt: number,
9
+ intensity: number
10
+ ): void {
11
+ const { audio, tool, reasoning, intensity: intensityState } = state;
12
+
13
+ tool.fragmentScatterBoost += (0 - tool.fragmentScatterBoost) * dt * 6;
14
+ tool.settleTimer = Math.max(0, tool.settleTimer - dt);
15
+
16
+ const settleContraction = tool.settleTimer > 0 ? Math.sin(tool.settleTimer * 20) * 0.08 : 0;
17
+ const reasoningContraction = reasoning.blend * 0.25;
18
+ const fragmentScale =
19
+ 0.5 + intensity * 0.6 + tool.fragmentScatterBoost - settleContraction - reasoningContraction;
20
+ elements.fragmentGroup.scale.setScalar(fragmentScale);
21
+
22
+ elements.fragments.forEach((frag) => {
23
+ const reasoningOrbitSlowdown = 1 - reasoning.blend * 0.6;
24
+ const audioOrbitBoost = audio.current * 0.4;
25
+ const orbitSpeed =
26
+ frag.orbitSpeed * (0.4 + intensity * 1.2 + audioOrbitBoost) * reasoningOrbitSlowdown +
27
+ intensityState.spinBoost * 0.5;
28
+ frag.orbitAngle += dt * orbitSpeed;
29
+
30
+ const dynamicRadius = frag.orbitRadius;
31
+ const bobAmount = 0.08 + intensity * 0.2;
32
+ const bobSpeed = frag.bobSpeed * (0.5 + intensity * 1.0);
33
+ frag.bobPhase += dt * bobSpeed;
34
+
35
+ const bob = Math.sin(frag.bobPhase) * bobAmount;
36
+ frag.mesh.position.x = Math.cos(frag.orbitAngle) * dynamicRadius;
37
+ frag.mesh.position.z = Math.sin(frag.orbitAngle) * dynamicRadius;
38
+ frag.mesh.position.y += (bob - frag.mesh.position.y) * dt * 3;
39
+
40
+ const tumbleSpeed = 0.1 + intensity * 0.5;
41
+ frag.mesh.rotation.x += dt * tumbleSpeed;
42
+ frag.mesh.rotation.y += dt * tumbleSpeed * 1.5;
43
+
44
+ frag.material.opacity = clamp01(0.45 + intensity * 0.4 + audio.current * 0.22);
45
+ });
46
+ }
@@ -0,0 +1,64 @@
1
+ import type { SceneElements } from "../scene/create-scene-elements";
2
+ import type { RigState } from "../state/rig-state";
3
+
4
+ export function updateGlitchBehavior(
5
+ elements: SceneElements,
6
+ state: RigState,
7
+ dt: number,
8
+ intensity: number,
9
+ allowGlitch: boolean
10
+ ): void {
11
+ const { glitch } = state;
12
+
13
+ if (!allowGlitch) {
14
+ glitch.timer = 0;
15
+ if (glitch.isActive) {
16
+ glitch.isActive = false;
17
+ elements.coreGroup.position.set(0, 0, 0);
18
+ elements.fragmentGroup.scale.set(1, 1, 1);
19
+ }
20
+ return;
21
+ }
22
+
23
+ glitch.timer += dt;
24
+
25
+ const intensityFactor = Math.max(0.1, intensity);
26
+ const baseInterval = 3.0 / intensityFactor;
27
+ const randomFactor = 0.5 + Math.random();
28
+ const glitchInterval = baseInterval * randomFactor;
29
+
30
+ if (!glitch.isActive && glitch.timer > glitchInterval) {
31
+ glitch.isActive = true;
32
+ glitch.duration = 0.05 + Math.random() * 0.1 + intensity * 0.05;
33
+ glitch.timer = 0;
34
+ }
35
+
36
+ if (glitch.isActive) {
37
+ glitch.duration -= dt;
38
+
39
+ const displaceMult = intensity * 0.5;
40
+ elements.coreGroup.position.set(
41
+ (Math.random() - 0.5) * 0.05 * displaceMult,
42
+ (Math.random() - 0.5) * 0.05 * displaceMult,
43
+ (Math.random() - 0.5) * 0.03 * displaceMult
44
+ );
45
+
46
+ const scatterAmount = 1.0 + (Math.random() - 0.5) * intensity * 0.15;
47
+ elements.fragmentGroup.scale.setScalar(scatterAmount);
48
+
49
+ elements.rings.forEach((ring, i) => {
50
+ const baseOpacity = 0.5 + i * 0.15;
51
+ const flickerRange = intensity * 0.2;
52
+ ring.material.opacity = baseOpacity * (1 - flickerRange + Math.random() * flickerRange * 2);
53
+ });
54
+
55
+ if (glitch.duration <= 0) {
56
+ glitch.isActive = false;
57
+ elements.coreGroup.position.set(0, 0, 0);
58
+ elements.fragmentGroup.scale.set(1, 1, 1);
59
+ elements.rings.forEach((ring, i) => {
60
+ ring.material.opacity = 0.5 + i * 0.15;
61
+ });
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,95 @@
1
+ import type { SceneElements } from "../scene/create-scene-elements";
2
+ import type { RigState } from "../state/rig-state";
3
+
4
+ export function updateIdleAmbience(
5
+ elements: SceneElements,
6
+ state: RigState,
7
+ dt: number,
8
+ isIdle: boolean
9
+ ): void {
10
+ const { idleMicroGlitch, eyeDrift, particlePulse, coreDrift, typing } = state;
11
+
12
+ if (isIdle) {
13
+ idleMicroGlitch.timer += dt;
14
+ if (!idleMicroGlitch.active && idleMicroGlitch.timer > idleMicroGlitch.cooldown) {
15
+ idleMicroGlitch.active = true;
16
+ idleMicroGlitch.duration = 0.08 + Math.random() * 0.12;
17
+ idleMicroGlitch.timer = 0;
18
+ idleMicroGlitch.cooldown = 3 + Math.random() * 5;
19
+ }
20
+ if (idleMicroGlitch.active) {
21
+ idleMicroGlitch.duration -= dt;
22
+ const jitterAmount = 0.06;
23
+ elements.coreGroup.position.x += (Math.random() - 0.5) * jitterAmount;
24
+ elements.coreGroup.position.y += (Math.random() - 0.5) * jitterAmount;
25
+ if (idleMicroGlitch.duration <= 0) {
26
+ idleMicroGlitch.active = false;
27
+ elements.coreGroup.position.x = 0;
28
+ elements.coreGroup.position.y = 0;
29
+ }
30
+ }
31
+ } else {
32
+ idleMicroGlitch.timer = 0;
33
+ idleMicroGlitch.active = false;
34
+ }
35
+
36
+ if (typing.active) {
37
+ typing.eyeScanTimer += dt;
38
+ if (typing.eyeScanTimer > typing.eyeScanInterval) {
39
+ typing.eyeScanTimer = 0;
40
+ const scanWidth = 0.2;
41
+ eyeDrift.targetX = (Math.random() - 0.5) * scanWidth;
42
+ eyeDrift.targetY = (Math.random() - 0.5) * 0.05;
43
+ typing.eyeScanInterval = 0.3 + Math.random() * 0.8;
44
+ }
45
+ const trackSpeed = 5;
46
+ eyeDrift.x += (eyeDrift.targetX - eyeDrift.x) * dt * trackSpeed;
47
+ eyeDrift.y += (eyeDrift.targetY - eyeDrift.y) * dt * trackSpeed;
48
+ } else if (isIdle) {
49
+ eyeDrift.timer += dt;
50
+ if (eyeDrift.timer > eyeDrift.interval) {
51
+ eyeDrift.timer = 0;
52
+ const isFast = Math.random() > 0.4;
53
+ eyeDrift.interval = isFast ? 0.15 + Math.random() * 0.25 : 2.0 + Math.random() * 3.0;
54
+ eyeDrift.targetX = (Math.random() - 0.5) * 0.25;
55
+ eyeDrift.targetY = (Math.random() - 0.5) * 0.15;
56
+ }
57
+ const interpSpeed = eyeDrift.interval < 0.5 ? 30 : 1.5;
58
+ eyeDrift.x += (eyeDrift.targetX - eyeDrift.x) * dt * interpSpeed;
59
+ eyeDrift.y += (eyeDrift.targetY - eyeDrift.y) * dt * interpSpeed;
60
+ } else {
61
+ eyeDrift.x += (0 - eyeDrift.x) * dt * 4;
62
+ eyeDrift.y += (0 - eyeDrift.y) * dt * 4;
63
+ }
64
+
65
+ elements.eye.position.x = eyeDrift.x;
66
+ elements.eye.position.y = eyeDrift.y;
67
+ elements.pupil.position.x = eyeDrift.x;
68
+ elements.pupil.position.y = eyeDrift.y;
69
+
70
+ if (isIdle) {
71
+ particlePulse.timer += dt;
72
+ if (particlePulse.timer > particlePulse.interval) {
73
+ particlePulse.timer = 0;
74
+ particlePulse.interval = 1 + Math.random() * 2;
75
+ particlePulse.brightness = 1.0;
76
+ }
77
+ }
78
+ particlePulse.brightness = Math.max(0, particlePulse.brightness - dt * 1.5);
79
+
80
+ const coreDriftSpeed = 0.4;
81
+ const coreDriftAmount = 0.08;
82
+ coreDrift.phaseX += dt * coreDriftSpeed;
83
+ coreDrift.phaseY += dt * coreDriftSpeed * 0.7;
84
+ coreDrift.phaseZ += dt * coreDriftSpeed * 0.5;
85
+
86
+ const targetDriftX = Math.sin(coreDrift.phaseX) * coreDriftAmount;
87
+ const targetDriftY = Math.sin(coreDrift.phaseY) * coreDriftAmount;
88
+ const targetDriftZ = Math.sin(coreDrift.phaseZ) * coreDriftAmount * 0.5;
89
+
90
+ coreDrift.x += (targetDriftX - coreDrift.x) * dt * 2;
91
+ coreDrift.y += (targetDriftY - coreDrift.y) * dt * 2;
92
+ coreDrift.z += (targetDriftZ - coreDrift.z) * dt * 2;
93
+
94
+ elements.mainAnchor.position.set(coreDrift.x, coreDrift.y, coreDrift.z);
95
+ }
@@ -0,0 +1,16 @@
1
+ import type { RigState } from "../state/rig-state";
2
+
3
+ export function updateIntensityAndAudio(state: RigState, dt: number): number {
4
+ const { intensity, audio, typing, reasoning } = state;
5
+
6
+ typing.pulse = Math.max(0, typing.pulse - dt * 5);
7
+
8
+ const intensityRate = intensity.target > intensity.current ? 10 : 8;
9
+ intensity.current += (intensity.target - intensity.current) * dt * intensityRate;
10
+
11
+ intensity.spinBoost += (0 - intensity.spinBoost) * dt * 2.5;
12
+ audio.current += (audio.target - audio.current) * dt * 25;
13
+ reasoning.blend += ((reasoning.active ? 1 : 0) - reasoning.blend) * dt * 6;
14
+
15
+ return intensity.current;
16
+ }
@@ -0,0 +1,20 @@
1
+ import type { SceneElements } from "../scene/create-scene-elements";
2
+ import type { RigState } from "../state/rig-state";
3
+
4
+ export function updateMainAnchor(
5
+ elements: SceneElements,
6
+ state: RigState,
7
+ dt: number,
8
+ intensity: number
9
+ ): void {
10
+ const { phase, audio } = state;
11
+
12
+ const driftSpeed = 0.08 + intensity * 0.3;
13
+ phase.drift += dt * driftSpeed;
14
+ const driftAmount = 0.03 + intensity * 0.12;
15
+
16
+ elements.mainAnchor.rotation.y = Math.sin(phase.drift) * driftAmount;
17
+ elements.mainAnchor.rotation.x = Math.sin(phase.drift * 0.7) * driftAmount * 0.5;
18
+ elements.mainAnchor.rotation.z = Math.sin(phase.drift * 0.5) * intensity * 0.08;
19
+ elements.mainAnchor.scale.setScalar(1 + audio.current * 0.12);
20
+ }
@@ -0,0 +1,49 @@
1
+ import type { SceneElements } from "../scene/create-scene-elements";
2
+ import type { RigState } from "../state/rig-state";
3
+ import { clamp01 } from "../utils/math";
4
+
5
+ export function updateParticles(
6
+ elements: SceneElements,
7
+ state: RigState,
8
+ dt: number,
9
+ intensity: number,
10
+ allowGlitch: boolean
11
+ ): void {
12
+ const { audio, particlePulse } = state;
13
+ const particleCount = elements.particleVelocities.length;
14
+
15
+ const glitchChance = allowGlitch ? 0.0002 + intensity * 0.006 : 0;
16
+ const audioJitterBoost = 1 + audio.current * 0.8;
17
+ const particleSpeedMult = (0.3 + intensity * 1.2) * audioJitterBoost;
18
+
19
+ for (let i = 0; i < particleCount; i++) {
20
+ const vel = elements.particleVelocities[i]!;
21
+ let x = elements.particlePos.getX(i) + vel.x * dt * particleSpeedMult;
22
+ let y = elements.particlePos.getY(i) + vel.y * dt * particleSpeedMult;
23
+ let z = elements.particlePos.getZ(i) + vel.z * dt * particleSpeedMult;
24
+
25
+ if (Math.random() < glitchChance) {
26
+ const r = 1.5 + Math.random() * 2;
27
+ const theta = Math.random() * Math.PI * 2;
28
+ const phi = Math.acos(Math.random() * 2 - 1);
29
+ x = r * Math.sin(phi) * Math.cos(theta);
30
+ y = r * Math.sin(phi) * Math.sin(theta);
31
+ z = r * Math.cos(phi);
32
+ }
33
+
34
+ const distSq = x * x + y * y + z * z;
35
+ if (distSq > 20) {
36
+ x *= -0.8;
37
+ y *= -0.8;
38
+ z *= -0.8;
39
+ }
40
+
41
+ elements.particlePos.setXYZ(i, x, y, z);
42
+ }
43
+ elements.particlePos.needsUpdate = true;
44
+
45
+ const idleParticleBoost = particlePulse.brightness * 0.4;
46
+ elements.particleMat.opacity = clamp01(0.3 + intensity * 0.4 + audio.current * 0.25 + idleParticleBoost);
47
+ elements.particleMat.size =
48
+ 0.02 + intensity * 0.015 + audio.current * 0.01 + particlePulse.brightness * 0.02;
49
+ }
@@ -0,0 +1,35 @@
1
+ import type { SceneElements } from "../scene/create-scene-elements";
2
+ import type { RigState } from "../state/rig-state";
3
+ import { clamp01 } from "../utils/math";
4
+
5
+ export function updateRings(elements: SceneElements, state: RigState, dt: number, intensity: number): void {
6
+ const { audio, intensity: intensityState } = state;
7
+
8
+ const orbitScale = 0.75 + intensity * 0.4;
9
+ elements.orbitGroup.scale.setScalar(orbitScale);
10
+
11
+ elements.rings.forEach((ring, i) => {
12
+ const ringIntensity = Math.pow(intensity, 1.35);
13
+ const ringSpeed = ring.speed * (0.4 + ringIntensity * 1.5) + intensityState.spinBoost * (1 + i * 0.2);
14
+ ring.mesh.rotateOnAxis(ring.axis, dt * ringSpeed);
15
+
16
+ const wobbleSpeed = 3 + i * 0.5;
17
+ ring.wobblePhase += dt * wobbleSpeed;
18
+ const wobbleAmount = audio.current * 0.06;
19
+ const wobbleX = Math.sin(ring.wobblePhase) * wobbleAmount;
20
+ const wobbleZ = Math.cos(ring.wobblePhase * 1.3) * wobbleAmount;
21
+ ring.mesh.rotation.x += wobbleX;
22
+ ring.mesh.rotation.z += wobbleZ;
23
+
24
+ const phaseSpeed = 1 + intensity * 3;
25
+ ring.phase += dt * phaseSpeed;
26
+
27
+ const baseOpacity = 0.4 + intensity * 0.4 + i * 0.1;
28
+ if (intensity > 0.1) {
29
+ const wave = Math.sin(ring.phase + i * 1.5) * 0.2 * intensity;
30
+ ring.material.opacity = clamp01(baseOpacity + wave + audio.current * 0.25);
31
+ } else {
32
+ ring.material.opacity = clamp01(baseOpacity + audio.current * 0.25);
33
+ }
34
+ });
35
+ }
@@ -0,0 +1,26 @@
1
+ import type { SceneElements } from "../scene/create-scene-elements";
2
+ import type { RigState } from "../state/rig-state";
3
+ import { clamp01 } from "../utils/math";
4
+
5
+ export function updateSigils(elements: SceneElements, state: RigState, dt: number, intensity: number): void {
6
+ const { phase, audio, tool } = state;
7
+
8
+ for (let i = 0; i < elements.fragments.length; i++) {
9
+ const curr = elements.fragments[i]!;
10
+ const next = elements.fragments[(i + 1) % elements.fragments.length]!;
11
+ elements.sigilPos.setXYZ(i * 2, curr.mesh.position.x, curr.mesh.position.y, curr.mesh.position.z);
12
+ elements.sigilPos.setXYZ(i * 2 + 1, next.mesh.position.x, next.mesh.position.y, next.mesh.position.z);
13
+ }
14
+ elements.sigilPos.needsUpdate = true;
15
+
16
+ const sigilPulseSpeed = 1 + intensity * 2;
17
+ phase.sigilPulse += dt * sigilPulseSpeed;
18
+
19
+ tool.sigilBrightnessBoost += ((tool.active ? 1 : 0) - tool.sigilBrightnessBoost) * dt * 8;
20
+
21
+ const sigilBaseOpacity = 0.2 + intensity * 0.2 + tool.sigilBrightnessBoost * 0.5;
22
+ const sigilPulseAmount = 0.05 + intensity * 0.12;
23
+ elements.sigilMat.opacity = clamp01(
24
+ sigilBaseOpacity + Math.sin(phase.sigilPulse) * sigilPulseAmount + audio.current * 0.2
25
+ );
26
+ }
@@ -0,0 +1,83 @@
1
+ import type { SceneElements } from "../scene/create-scene-elements";
2
+ import type { RigState } from "../state/rig-state";
3
+
4
+ import { STARTUP_BANNER_DURATION_S } from "../../../ui/startup";
5
+
6
+ const SPAWN_DURATION = STARTUP_BANNER_DURATION_S;
7
+
8
+ /** Easing for jitter intensity - high at start, fades quickly */
9
+ function jitterEasing(t: number): number {
10
+ return Math.pow(1 - t, 2);
11
+ }
12
+
13
+ /**
14
+ * Updates the spawn animation state and applies visual effects.
15
+ * Returns the current spawn progress (0-1) for other systems to use.
16
+ */
17
+ export function updateSpawn(elements: SceneElements, state: RigState, dt: number): number {
18
+ const easedProgress = advanceSpawn(state, dt);
19
+ applySpawn(elements, state);
20
+ return easedProgress;
21
+ }
22
+
23
+ /**
24
+ * Advances spawn timers/state (no rendering side effects).
25
+ * Returns the eased progress (0-1).
26
+ */
27
+ export function advanceSpawn(state: RigState, dt: number): number {
28
+ if (state.spawn.complete) return 1;
29
+
30
+ state.spawn.elapsed += dt;
31
+ const rawProgress = Math.min(1, state.spawn.elapsed / SPAWN_DURATION);
32
+ state.spawn.progress = rawProgress;
33
+ state.spawn.glitchIntensity = jitterEasing(rawProgress);
34
+
35
+ // Mark complete when done
36
+ if (rawProgress >= 1) {
37
+ state.spawn.complete = true;
38
+ state.spawn.progress = 1;
39
+ state.spawn.glitchIntensity = 0;
40
+ return 1;
41
+ }
42
+
43
+ return rawProgress;
44
+ }
45
+
46
+ /**
47
+ * Applies spawn animation effects to scene elements using current state.
48
+ * Safe to call multiple times per frame.
49
+ */
50
+ export function applySpawn(elements: SceneElements, state: RigState): void {
51
+ // Minimal startup effect: jitter only (synced to banner reveal duration).
52
+ if (state.spawn.complete) {
53
+ elements.coreGroup.position.x = 0;
54
+ elements.coreGroup.position.y = 0;
55
+ return;
56
+ }
57
+
58
+ const jitterAmount = state.spawn.glitchIntensity * 0.15;
59
+ const jitterX = (Math.random() - 0.5) * jitterAmount;
60
+ const jitterY = (Math.random() - 0.5) * jitterAmount;
61
+ elements.coreGroup.position.x = jitterX;
62
+ elements.coreGroup.position.y = jitterY;
63
+ }
64
+
65
+ /**
66
+ * Resets spawn state for a fresh spawn animation.
67
+ */
68
+ export function resetSpawnState(state: RigState): void {
69
+ state.spawn.progress = 0;
70
+ state.spawn.elapsed = 0;
71
+ state.spawn.complete = false;
72
+ state.spawn.glitchIntensity = 1;
73
+ }
74
+
75
+ /**
76
+ * Skips spawn animation and sets everything to fully spawned.
77
+ */
78
+ export function skipSpawnAnimation(state: RigState): void {
79
+ state.spawn.progress = 1;
80
+ state.spawn.elapsed = SPAWN_DURATION;
81
+ state.spawn.complete = true;
82
+ state.spawn.glitchIntensity = 0;
83
+ }
@@ -0,0 +1,17 @@
1
+ export const clamp = (value: number, min: number, max: number): number => Math.max(min, Math.min(max, value));
2
+
3
+ export const clamp01 = (value: number): number => clamp(value, 0, 1);
4
+
5
+ export function lerpColor(current: number, target: number, t: number): number {
6
+ const cr = (current >> 16) & 0xff;
7
+ const cg = (current >> 8) & 0xff;
8
+ const cb = current & 0xff;
9
+ const tr = (target >> 16) & 0xff;
10
+ const tg = (target >> 8) & 0xff;
11
+ const tb = target & 0xff;
12
+ return (
13
+ (Math.round(cr + (tr - cr) * t) << 16) |
14
+ (Math.round(cg + (tg - cg) * t) << 8) |
15
+ Math.round(cb + (tb - cb) * t)
16
+ );
17
+ }
@@ -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;