@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
package/package.json
CHANGED
package/src/ai/daemon-ai.ts
CHANGED
|
@@ -22,9 +22,10 @@ import type {
|
|
|
22
22
|
ToolApprovalResponse,
|
|
23
23
|
TranscriptionResult,
|
|
24
24
|
} from "../types";
|
|
25
|
-
import { debug } from "../utils/debug-logger";
|
|
25
|
+
import { debug, toolDebug } from "../utils/debug-logger";
|
|
26
26
|
import { getOpenRouterReportedCost } from "../utils/openrouter-reported-cost";
|
|
27
27
|
import { getWorkspacePath } from "../utils/workspace-manager";
|
|
28
|
+
import { extractFinalAssistantText } from "./message-utils";
|
|
28
29
|
import { TRANSCRIPTION_MODEL, buildOpenRouterChatSettings, getResponseModel } from "./model-config";
|
|
29
30
|
import { sanitizeMessagesForInput } from "./sanitize-messages";
|
|
30
31
|
import { type InteractionMode, buildDaemonSystemPrompt } from "./system-prompt";
|
|
@@ -54,40 +55,6 @@ function normalizeStreamError(error: unknown): Error {
|
|
|
54
55
|
return new Error(String(error));
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
/**
|
|
58
|
-
* Extract the final text content from the last assistant message.
|
|
59
|
-
* In multi-step agent loops, we only want to speak the final response, not intermediate text.
|
|
60
|
-
*/
|
|
61
|
-
function extractFinalAssistantText(messages: ModelMessage[]): string {
|
|
62
|
-
// Find the last assistant message
|
|
63
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
64
|
-
const msg = messages[i];
|
|
65
|
-
if (msg?.role === "assistant") {
|
|
66
|
-
const content = msg.content;
|
|
67
|
-
if (Array.isArray(content)) {
|
|
68
|
-
// Find the last text part. In some models/providers, intermediate
|
|
69
|
-
// "thoughts" might be included as separate text blocks before the final answer.
|
|
70
|
-
// We prioritize the last text block in the message for the final response.
|
|
71
|
-
for (let j = content.length - 1; j >= 0; j--) {
|
|
72
|
-
const part = content[j];
|
|
73
|
-
if (
|
|
74
|
-
part &&
|
|
75
|
-
typeof part === "object" &&
|
|
76
|
-
"type" in part &&
|
|
77
|
-
part.type === "text" &&
|
|
78
|
-
"text" in part &&
|
|
79
|
-
typeof part.text === "string"
|
|
80
|
-
) {
|
|
81
|
-
return part.text;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
// If this assistant message had no text parts, continue searching previous messages
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return "";
|
|
89
|
-
}
|
|
90
|
-
|
|
91
58
|
/**
|
|
92
59
|
* The DAEMON agent instance.
|
|
93
60
|
* Handles the agent loop internally, allowing for multi-step tool usage.
|
|
@@ -239,6 +206,12 @@ export async function generateResponse(
|
|
|
239
206
|
} else if (part.type === "tool-result") {
|
|
240
207
|
callbacks.onToolResult?.(part.toolName, part.output, part.toolCallId);
|
|
241
208
|
} else if (part.type === "tool-error") {
|
|
209
|
+
toolDebug.error("tool-error", {
|
|
210
|
+
toolName: part.toolName,
|
|
211
|
+
toolCallId: part.toolCallId,
|
|
212
|
+
input: part.input,
|
|
213
|
+
error: part.error,
|
|
214
|
+
});
|
|
242
215
|
callbacks.onToolResult?.(part.toolName, { error: part.error, input: part.input }, part.toolCallId);
|
|
243
216
|
} else if (part.type === "tool-approval-request") {
|
|
244
217
|
const approvalRequest: ToolApprovalRequest = {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ModelMessage } from "ai";
|
|
2
|
+
|
|
3
|
+
export function extractFinalAssistantText(messages: ModelMessage[]): string {
|
|
4
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
5
|
+
const msg = messages[i];
|
|
6
|
+
if (msg?.role === "assistant") {
|
|
7
|
+
const content = msg.content;
|
|
8
|
+
if (Array.isArray(content)) {
|
|
9
|
+
for (let j = content.length - 1; j >= 0; j--) {
|
|
10
|
+
const part = content[j];
|
|
11
|
+
if (
|
|
12
|
+
part &&
|
|
13
|
+
typeof part === "object" &&
|
|
14
|
+
"type" in part &&
|
|
15
|
+
part.type === "text" &&
|
|
16
|
+
"text" in part &&
|
|
17
|
+
typeof part.text === "string"
|
|
18
|
+
) {
|
|
19
|
+
return part.text;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return "";
|
|
26
|
+
}
|
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
|
|
6
6
|
import { createOpenRouter } from "@openrouter/ai-sdk-provider";
|
|
7
7
|
import { tool } from "ai";
|
|
8
|
-
import { ToolLoopAgent, stepCountIs } from "ai";
|
|
8
|
+
import { type ModelMessage, ToolLoopAgent, stepCountIs } from "ai";
|
|
9
9
|
import type { ToolSet } from "ai";
|
|
10
10
|
import { z } from "zod";
|
|
11
11
|
import { getDaemonManager } from "../../state/daemon-state";
|
|
12
12
|
import type { SubagentProgressEmitter } from "../../types";
|
|
13
13
|
import { getOpenRouterReportedCost } from "../../utils/openrouter-reported-cost";
|
|
14
|
+
import { extractFinalAssistantText } from "../message-utils";
|
|
14
15
|
import { buildOpenRouterChatSettings, getSubagentModel } from "../model-config";
|
|
15
16
|
import { buildToolSet } from "./tool-registry";
|
|
16
17
|
|
|
@@ -60,7 +61,7 @@ RULES:
|
|
|
60
61
|
- The final summary needs to be self contained and needs to provide enough information to the main agent so it is clear what you have done and what the results are.
|
|
61
62
|
|
|
62
63
|
Today's date: ${new Date().toISOString().split("T")[0]}
|
|
63
|
-
`;
|
|
64
|
+
`;
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
// Global emitter that will be set by the daemon-ai module
|
|
@@ -109,7 +110,6 @@ Provide a concise summary for display and a very specific task description (espe
|
|
|
109
110
|
stopWhen: stepCountIs(MAX_SUBAGENT_STEPS),
|
|
110
111
|
});
|
|
111
112
|
|
|
112
|
-
let responseText = "";
|
|
113
113
|
let costTotal = 0;
|
|
114
114
|
let hasCost = false;
|
|
115
115
|
|
|
@@ -119,9 +119,7 @@ Provide a concise summary for display and a very specific task description (espe
|
|
|
119
119
|
});
|
|
120
120
|
|
|
121
121
|
for await (const part of stream.fullStream) {
|
|
122
|
-
if (part.type === "
|
|
123
|
-
responseText += part.text;
|
|
124
|
-
} else if (part.type === "finish-step") {
|
|
122
|
+
if (part.type === "finish-step") {
|
|
125
123
|
const reportedCost = getOpenRouterReportedCost(part.providerMetadata);
|
|
126
124
|
if (reportedCost !== undefined) {
|
|
127
125
|
costTotal += reportedCost;
|
|
@@ -145,6 +143,9 @@ Provide a concise summary for display and a very specific task description (espe
|
|
|
145
143
|
}
|
|
146
144
|
}
|
|
147
145
|
|
|
146
|
+
const responseMessages = await stream.response.then((response) => response.messages);
|
|
147
|
+
const finalResponse = extractFinalAssistantText(responseMessages);
|
|
148
|
+
|
|
148
149
|
const streamUsage = await stream.usage;
|
|
149
150
|
if (streamUsage) {
|
|
150
151
|
progressEmitter?.onSubagentUsage({
|
|
@@ -163,7 +164,7 @@ Provide a concise summary for display and a very specific task description (espe
|
|
|
163
164
|
return {
|
|
164
165
|
success: true,
|
|
165
166
|
summary,
|
|
166
|
-
response:
|
|
167
|
+
response: finalResponse || "Task completed but no text response generated.",
|
|
167
168
|
};
|
|
168
169
|
} catch (error) {
|
|
169
170
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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";
|
|
@@ -16,6 +15,8 @@ import { TypingInputBar } from "../../components/TypingInputBar";
|
|
|
16
15
|
import type { ContentBlock, ConversationMessage, TokenUsage } from "../../types";
|
|
17
16
|
import { DaemonState } from "../../types";
|
|
18
17
|
import { COLORS, REASONING_MARKDOWN_STYLE } from "../../ui/constants";
|
|
18
|
+
import { renderReasoningTicker } from "../../ui/reasoning-ticker";
|
|
19
|
+
import { formatElapsedTime } from "../../utils/formatters";
|
|
19
20
|
import type { ModelMetadata } from "../../utils/model-metadata";
|
|
20
21
|
|
|
21
22
|
export interface ConversationDisplayState {
|
|
@@ -76,6 +77,7 @@ export interface ConversationPaneProps {
|
|
|
76
77
|
modelName?: string;
|
|
77
78
|
sessionTitle?: string;
|
|
78
79
|
isVoiceOutputEnabled?: boolean;
|
|
80
|
+
startupIntroDone?: boolean;
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
function ConversationPaneImpl(props: ConversationPaneProps) {
|
|
@@ -96,6 +98,7 @@ function ConversationPaneImpl(props: ConversationPaneProps) {
|
|
|
96
98
|
modelName,
|
|
97
99
|
sessionTitle,
|
|
98
100
|
isVoiceOutputEnabled,
|
|
101
|
+
startupIntroDone = true,
|
|
99
102
|
} = props;
|
|
100
103
|
|
|
101
104
|
const { conversationHistory, currentTranscription, currentContentBlocks } = conversation;
|
|
@@ -175,6 +178,8 @@ function ConversationPaneImpl(props: ConversationPaneProps) {
|
|
|
175
178
|
const isReasoning =
|
|
176
179
|
daemonState === DaemonState.RESPONDING &&
|
|
177
180
|
(!conversation.currentResponse || !!reasoningDisplay || !!reasoningQueue);
|
|
181
|
+
const fullReasoningDurationLabel =
|
|
182
|
+
responseElapsedMs > 0 ? ` · ${formatElapsedTime(responseElapsedMs, { style: "detailed" })}` : "";
|
|
178
183
|
|
|
179
184
|
return (
|
|
180
185
|
<>
|
|
@@ -189,7 +194,7 @@ function ConversationPaneImpl(props: ConversationPaneProps) {
|
|
|
189
194
|
/>
|
|
190
195
|
)}
|
|
191
196
|
|
|
192
|
-
{!hasInteracted && (
|
|
197
|
+
{!hasInteracted && startupIntroDone && (
|
|
193
198
|
<box
|
|
194
199
|
position="absolute"
|
|
195
200
|
left={0}
|
|
@@ -408,6 +413,7 @@ function ConversationPaneImpl(props: ConversationPaneProps) {
|
|
|
408
413
|
>
|
|
409
414
|
<text>
|
|
410
415
|
<span fg={COLORS.REASONING}>{"REASONING"}</span>
|
|
416
|
+
<span fg={COLORS.REASONING_DIM}>{fullReasoningDurationLabel}</span>
|
|
411
417
|
</text>
|
|
412
418
|
<code
|
|
413
419
|
content={fullReasoning}
|
|
@@ -418,12 +424,7 @@ function ConversationPaneImpl(props: ConversationPaneProps) {
|
|
|
418
424
|
/>
|
|
419
425
|
</box>
|
|
420
426
|
) : reasoningDisplay ? (
|
|
421
|
-
|
|
422
|
-
<span fg={COLORS.REASONING_DIM}>
|
|
423
|
-
{"⟡ "}
|
|
424
|
-
{reasoningDisplay}
|
|
425
|
-
</span>
|
|
426
|
-
</text>
|
|
427
|
+
renderReasoningTicker(reasoningDisplay)
|
|
427
428
|
) : null}
|
|
428
429
|
</box>
|
|
429
430
|
)}
|
|
@@ -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,
|
|
@@ -340,4 +349,18 @@ export class DaemonAvatarRenderable extends FrameBufferRenderable {
|
|
|
340
349
|
this.rig.triggerTypingPulse();
|
|
341
350
|
}
|
|
342
351
|
}
|
|
352
|
+
|
|
353
|
+
public resetSpawn(): void {
|
|
354
|
+
this.pendingSpawnAction = "reset";
|
|
355
|
+
if (this.rig) {
|
|
356
|
+
this.rig.resetSpawn();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
public skipSpawn(): void {
|
|
361
|
+
this.pendingSpawnAction = "skip";
|
|
362
|
+
if (this.rig) {
|
|
363
|
+
this.rig.skipSpawn();
|
|
364
|
+
}
|
|
365
|
+
}
|
|
343
366
|
}
|