@linxin666/dsh-pet 0.3.4 → 0.3.6
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/README.i18n.yaml +2 -2
- package/README.md +13 -10
- package/README.zh.md +13 -10
- package/contracts/voice-pack-v1.schema.json +53 -28
- package/cordis.patch.yml +1 -1
- package/lib/client.js +166 -30
- package/lib/client.js.map +1 -1
- package/lib/index.js +400 -478
- package/lib/invariant.js +1 -1
- package/lib/{state-DrMX22GL.js → state-NvDEoCln.js} +5 -2
- package/lib/types/access.d.ts +5 -4
- package/lib/types/access.d.ts.map +1 -1
- package/lib/types/access.js +2 -12
- package/lib/types/affinity.d.ts +2 -0
- package/lib/types/affinity.d.ts.map +1 -1
- package/lib/types/affinity.js +12 -0
- package/lib/types/chatter.d.ts +68 -46
- package/lib/types/chatter.d.ts.map +1 -1
- package/lib/types/chatter.js +243 -301
- package/lib/types/client/PetSprite.d.ts.map +1 -1
- package/lib/types/client/PetSprite.js +23 -18
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +26 -9
- package/lib/types/client/locales.d.ts +18 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +18 -0
- package/lib/types/client/renderers/frames2d.d.ts +12 -0
- package/lib/types/client/renderers/frames2d.d.ts.map +1 -1
- package/lib/types/client/renderers/frames2d.js +143 -18
- package/lib/types/event-projection.d.ts +9 -4
- package/lib/types/event-projection.d.ts.map +1 -1
- package/lib/types/event-projection.js +46 -14
- package/lib/types/ledger.d.ts +4 -2
- package/lib/types/ledger.d.ts.map +1 -1
- package/lib/types/ledger.js +4 -4
- package/lib/types/pair-access.d.ts +35 -0
- package/lib/types/pair-access.d.ts.map +1 -0
- package/lib/types/pair-access.js +19 -0
- package/lib/types/remarks.d.ts +1 -1
- package/lib/types/remarks.d.ts.map +1 -1
- package/lib/types/remarks.js +11 -2
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +8 -3
- package/lib/types/service.d.ts +7 -11
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +31 -20
- package/lib/types/voice-pack.d.ts +11 -7
- package/lib/types/voice-pack.d.ts.map +1 -1
- package/lib/types/voice-pack.js +80 -49
- package/package.json +1 -1
- package/src/access.ts +7 -27
- package/src/affinity.ts +13 -0
- package/src/chatter.test.ts +68 -38
- package/src/chatter.ts +269 -316
- package/src/client/PetSprite.test.tsx +50 -15
- package/src/client/PetSprite.tsx +30 -25
- package/src/client/index.test.tsx +10 -1
- package/src/client/index.ts +29 -10
- package/src/client/locales.ts +18 -0
- package/src/client/renderers/frames2d.test.ts +101 -0
- package/src/client/renderers/frames2d.ts +138 -18
- package/src/event-projection.ts +57 -16
- package/src/ledger.ts +6 -4
- package/src/pair-access.ts +49 -0
- package/src/remarks.test.ts +10 -0
- package/src/remarks.ts +9 -2
- package/src/routes.ts +9 -4
- package/src/service.ts +36 -30
- package/src/voice-pack.test.ts +87 -25
- package/src/voice-pack.ts +87 -49
|
@@ -360,14 +360,14 @@ export function PetSprite(props) {
|
|
|
360
360
|
const statusBubble = feedback === null && sessionBubbles.length === 0
|
|
361
361
|
? snapshot?.bubble
|
|
362
362
|
: undefined;
|
|
363
|
-
//
|
|
364
|
-
// woken by
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
// voices at once. Interaction feedback takes over the
|
|
368
|
-
// while it plays, so whispers yield to it like status
|
|
369
|
-
|
|
370
|
-
const bubblePresent = feedback !== null || sessionBubbles.length > 0 || statusBubble !== undefined
|
|
363
|
+
// Each session's inner whisper (碎碎念) rides its own bubble — short
|
|
364
|
+
// inner-voice copy woken by that session's activity, never the model's or
|
|
365
|
+
// another session's. Instead of a second bubble of its own, a fresh
|
|
366
|
+
// whisper takes over its session's bubble and re-tints it, so the pet
|
|
367
|
+
// never wears two voices at once. Interaction feedback takes over the
|
|
368
|
+
// whole bubble area while it plays, so whispers yield to it like status
|
|
369
|
+
// copy.
|
|
370
|
+
const bubblePresent = feedback !== null || sessionBubbles.length > 0 || statusBubble !== undefined;
|
|
371
371
|
const displayName = snapshot?.name ?? definition.displayName;
|
|
372
372
|
// The host-served status decoration (M5, #567); absent = text-only bubbles.
|
|
373
373
|
const decoration = snapshot?.decoration;
|
|
@@ -447,14 +447,13 @@ export function PetSprite(props) {
|
|
|
447
447
|
}
|
|
448
448
|
}
|
|
449
449
|
props.onPet();
|
|
450
|
-
}, role: "button", "aria-label": definition.displayName, children: props.visual }) }), props.hud, feedback !== null && (_jsx("div", { ref: bubbleRef, className: clsx(styles.bubble, feedback.kind === 'feed' ? styles.bubbleFeed : styles.bubblePet), children: feedback.text }, feedback.at)), feedback === null && (sessionBubbles.length > 0 || statusBubble !== undefined
|
|
451
|
-
//
|
|
452
|
-
//
|
|
453
|
-
//
|
|
454
|
-
//
|
|
455
|
-
|
|
456
|
-
const
|
|
457
|
-
const bubble = (_jsxs("button", { type: "button", className: clsx(styles.bubble, styles.bubbleStatus, styles.bubbleClickable, speaksWhisper && styles.bubbleWhisper), title: props.t('pet.openSessionHint'), onClick: () => { props.onOpenSession(session.sessionId); }, children: [index === 0 && !speaksWhisper && decoration !== undefined && (_jsx(StatusOrnament, { decoration: decoration, phase: phase })), speaksWhisper ? whisper : session.bubble] }, speaksWhisper ? 'whisper:' + whisper : session.sessionId));
|
|
450
|
+
}, role: "button", "aria-label": definition.displayName, children: props.visual }) }), props.hud, feedback !== null && (_jsx("div", { ref: bubbleRef, className: clsx(styles.bubble, feedback.kind === 'feed' ? styles.bubbleFeed : styles.bubblePet), children: feedback.text }, feedback.at)), feedback === null && (sessionBubbles.length > 0 || statusBubble !== undefined) && (_jsxs("div", { ref: bubbleRef, className: styles.bubbleStack, onPointerEnter: () => setStackPeek(true), onPointerLeave: () => setStackPeek(false), children: [visibleSessions.map((session, index) => {
|
|
451
|
+
// A session's whisper rides ITS OWN bubble (the stack lead when
|
|
452
|
+
// the current session has one, any bubble when the stack is
|
|
453
|
+
// expanded). The key swap restarts the entrance animation so the
|
|
454
|
+
// mood change reads as the bubble re-speaking.
|
|
455
|
+
const speaksWhisper = session.whisper !== undefined;
|
|
456
|
+
const bubble = (_jsxs("button", { type: "button", className: clsx(styles.bubble, styles.bubbleStatus, styles.bubbleClickable, speaksWhisper && styles.bubbleWhisper), title: props.t('pet.openSessionHint'), onClick: () => { props.onOpenSession(session.sessionId); }, children: [index === 0 && !speaksWhisper && decoration !== undefined && (_jsx(StatusOrnament, { decoration: decoration, phase: phase })), session.whisper ?? session.bubble] }, speaksWhisper ? 'whisper:' + session.whisper : session.sessionId));
|
|
458
457
|
// The primary bubble carries the '+N' badge while other sessions
|
|
459
458
|
// hide behind it; the badge toggles the pinned (touch) expansion.
|
|
460
459
|
if (index !== 0 || sessionBubbles.length <= 1)
|
|
@@ -467,7 +466,7 @@ export function PetSprite(props) {
|
|
|
467
466
|
e.stopPropagation();
|
|
468
467
|
setStackPinned(open => !open);
|
|
469
468
|
}, children: stackOpen ? '×' : '+' + String(sessionBubbles.length - 1) })] }, "primary"));
|
|
470
|
-
}), sessionBubbles.length === 0 &&
|
|
469
|
+
}), sessionBubbles.length === 0 && statusBubble !== undefined && (_jsxs("div", { className: clsx(styles.bubble, styles.bubbleStatus), role: "status", "aria-live": "polite", children: [decoration !== undefined && (_jsx(StatusOrnament, { decoration: decoration, phase: phase })), statusBubble] }, "status"))] })), hovered && dragRef.current === null && (_jsx("div", { ref: panelRef, className: clsx(styles.panel, panelAbove && styles.panelAbove), "data-placement": panelAbove ? 'above' : 'below', style: panelAbove && panelLift > 0
|
|
471
470
|
? { marginBottom: panelLift }
|
|
472
471
|
: undefined, onPointerEnter: () => {
|
|
473
472
|
// Reaching the panel (or its bridge) must cancel any hide timer
|
|
@@ -499,7 +498,13 @@ export function PetSprite(props) {
|
|
|
499
498
|
props.onRename(trimmed);
|
|
500
499
|
setRenaming(false);
|
|
501
500
|
}
|
|
502
|
-
}, children: panelLabel('confirm', props.t('pet.confirm')) })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: styles.rankRow, children: [_jsx("span", { className: styles.nameCell, children: displayName }), _jsx("span", { className: styles.statRank, children:
|
|
501
|
+
}, children: panelLabel('confirm', props.t('pet.confirm')) })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: styles.rankRow, children: [_jsx("span", { className: styles.nameCell, children: displayName }), _jsx("span", { className: styles.statRank, children: (() => {
|
|
502
|
+
const rawRank = snapshot?.affinity.rank ?? '?';
|
|
503
|
+
const rankKey = `pet.rank.name.${rawRank}`;
|
|
504
|
+
const localized = props.t(rankKey);
|
|
505
|
+
const rankName = localized !== rankKey ? localized : rawRank;
|
|
506
|
+
return panelStat('rank', 'pet.rank', { rank: rankName });
|
|
507
|
+
})() })] }), _jsxs("div", { className: styles.rankRow, children: [_jsx("span", { className: styles.statTreats, children: panelStat('treats', 'pet.treats', { n: snapshot?.treats.stocked ?? 0 }) }), _jsx("span", { className: styles.statPoints, children: panelStat('points', 'pet.points', { points: snapshot?.affinity.points ?? 0 }) })] }), _jsxs("div", { className: styles.actions, children: [panelShows('feed') && (_jsx("button", { type: "button", className: styles.action, onClick: props.onFeed, children: panelLabel('feed', props.t('pet.feed')) })), panelShows('rename') && (_jsx("button", { type: "button", className: styles.action, onClick: () => {
|
|
503
508
|
// Cancel any pending hide so the rename box cannot
|
|
504
509
|
// unmount right as the user starts typing (#303).
|
|
505
510
|
clearHideTimer();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAwB,aAAa,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/client/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAwB,aAAa,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAA;AA4EnI,wEAAwE;AACxE,eAAO,MAAM,MAAM,UAA2E,CAAA;AAE9F,qEAAqE;AACrE,YAAY,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AACxE,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AACrD,YAAY,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC7D,YAAY,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAA;AACtF,YAAY,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAA;AACpE,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf;;;;WAIG;QACH,aAAa,CAAC,EAAE;YAAE,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAAA;SAAE,CAAA;KAC1E;CACF;AAED;;;;;GAKG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAgT9C"}
|
|
@@ -37,7 +37,8 @@ async function petFetch(path, body) {
|
|
|
37
37
|
}
|
|
38
38
|
/** The live host API instance (always defined; failures surface per call). */
|
|
39
39
|
const petApi = {
|
|
40
|
-
state: () => petFetch('/api/pet/state'
|
|
40
|
+
state: (currentSessionId) => petFetch('/api/pet/state'
|
|
41
|
+
+ (currentSessionId === undefined ? '' : '?current=' + encodeURIComponent(currentSessionId))),
|
|
41
42
|
pets: () => petFetch('/api/pet/pets'),
|
|
42
43
|
interact: (kind) => petFetch('/api/pet/interact', { kind }),
|
|
43
44
|
setVisible: (visible) => petFetch('/api/pet/set-visible', { visible }),
|
|
@@ -142,6 +143,19 @@ export function apply(ctx) {
|
|
|
142
143
|
const setPets = petStore.actions.setPets;
|
|
143
144
|
const setState = petStore.actions.setState;
|
|
144
145
|
const setFeedback = petStore.actions.setFeedback;
|
|
146
|
+
// Clicking a session bubble jumps the GUI to that session; the same
|
|
147
|
+
// sessions face reports which session the user is currently on, so the
|
|
148
|
+
// host can lead the bubble stack with it. A bubble can outlive its
|
|
149
|
+
// disposed session by one poll tick, and the sessions service fails
|
|
150
|
+
// loud on unknown ids, so consult the live list first. The pet's type
|
|
151
|
+
// program also loads the host-side dsh-session package through the
|
|
152
|
+
// service types, whose Context merge declares a different 'sessions'
|
|
153
|
+
// face; pin the browser runtime's outward face here.
|
|
154
|
+
const sessions = ctx.sessions;
|
|
155
|
+
const currentSessionId = () => {
|
|
156
|
+
const current = sessions.list.getSnapshot().current;
|
|
157
|
+
return current === undefined ? undefined : String(current);
|
|
158
|
+
};
|
|
145
159
|
// The registry list is fetched lazily with retries baked into the poll
|
|
146
160
|
// cycle: until it lands, the dock entry renders nothing and every 2s
|
|
147
161
|
// tick tries again. After it lands, one list feeds both the sprite and
|
|
@@ -163,7 +177,7 @@ export function apply(ctx) {
|
|
|
163
177
|
}
|
|
164
178
|
const seq = stateSeq + 1;
|
|
165
179
|
stateSeq = seq;
|
|
166
|
-
petApi.state().then((snapshot) => {
|
|
180
|
+
petApi.state(currentSessionId()).then((snapshot) => {
|
|
167
181
|
if (seq !== stateSeq)
|
|
168
182
|
return;
|
|
169
183
|
setSnapshot(snapshot);
|
|
@@ -207,13 +221,16 @@ export function apply(ctx) {
|
|
|
207
221
|
document.removeEventListener('visibilitychange', onVisibility);
|
|
208
222
|
};
|
|
209
223
|
}, 'pet: poll');
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
224
|
+
// A current-session switch should re-order the bubble stack right away,
|
|
225
|
+
// not on the next 2s tick. Poll only while the tab is visible, as the
|
|
226
|
+
// poll loop itself does.
|
|
227
|
+
const disposeSessionWatch = ctx.effect(() => {
|
|
228
|
+
const unsubscribe = sessions.list.subscribe(() => {
|
|
229
|
+
if (document.visibilityState === 'visible')
|
|
230
|
+
pollNow();
|
|
231
|
+
});
|
|
232
|
+
return unsubscribe;
|
|
233
|
+
}, 'pet: current-session watch');
|
|
217
234
|
const openSession = (sessionId) => {
|
|
218
235
|
const list = sessions.list.getSnapshot();
|
|
219
236
|
if (list.byId[sessionId] === undefined)
|
|
@@ -13,6 +13,15 @@ export declare const zh: {
|
|
|
13
13
|
readonly 'pet.namePlaceholder': "输入新名字";
|
|
14
14
|
readonly 'pet.summon': "召唤{name}";
|
|
15
15
|
readonly 'pet.rank': "亲密度 {rank}";
|
|
16
|
+
readonly 'pet.rank.name.\u5E7C\u9CB8': "幼鲸";
|
|
17
|
+
readonly 'pet.rank.name.\u4F19\u4F34': "伙伴";
|
|
18
|
+
readonly 'pet.rank.name.\u631A\u53CB': "挚友";
|
|
19
|
+
readonly 'pet.rank.name.\u6DF1\u6D77\u7F81\u7ECA': "深海羁绊";
|
|
20
|
+
readonly 'pet.rank.name.\u5FC3\u6709\u7075\u7280': "心有灵犀";
|
|
21
|
+
readonly 'pet.rank.name.\u4F20\u8BF4\u7F81\u7ECA': "传说羁绊";
|
|
22
|
+
readonly 'pet.rank.name.\u795E\u8BDD\u7F81\u7ECA': "神话羁绊";
|
|
23
|
+
readonly 'pet.rank.name.\u6C38\u6052\u4E4B\u5951': "永恒之契";
|
|
24
|
+
readonly 'pet.rank.name.\u9CB8\u751F\u5171\u6E21': "鲸生共渡";
|
|
16
25
|
readonly 'pet.points': "{points} 点";
|
|
17
26
|
readonly 'pet.treats': "小鱼干 ×{n}";
|
|
18
27
|
readonly 'pet.state.loading': "宠物正在赶来…";
|
|
@@ -83,6 +92,15 @@ export declare const en: {
|
|
|
83
92
|
readonly 'pet.namePlaceholder': "Enter a new name";
|
|
84
93
|
readonly 'pet.summon': "Summon {name}";
|
|
85
94
|
readonly 'pet.rank': "Affinity {rank}";
|
|
95
|
+
readonly 'pet.rank.name.\u5E7C\u9CB8': "Baby Whale";
|
|
96
|
+
readonly 'pet.rank.name.\u4F19\u4F34': "Companion";
|
|
97
|
+
readonly 'pet.rank.name.\u631A\u53CB': "Close Friend";
|
|
98
|
+
readonly 'pet.rank.name.\u6DF1\u6D77\u7F81\u7ECA': "Deep Sea Bond";
|
|
99
|
+
readonly 'pet.rank.name.\u5FC3\u6709\u7075\u7280': "Kindred Spirit";
|
|
100
|
+
readonly 'pet.rank.name.\u4F20\u8BF4\u7F81\u7ECA': "Legendary Bond";
|
|
101
|
+
readonly 'pet.rank.name.\u795E\u8BDD\u7F81\u7ECA': "Mythic Bond";
|
|
102
|
+
readonly 'pet.rank.name.\u6C38\u6052\u4E4B\u5951': "Eternal Covenant";
|
|
103
|
+
readonly 'pet.rank.name.\u9CB8\u751F\u5171\u6E21': "Lifelong Companion";
|
|
86
104
|
readonly 'pet.points': "{points} pts";
|
|
87
105
|
readonly 'pet.treats': "Treats ×{n}";
|
|
88
106
|
readonly 'pet.state.loading': "The pet is on its way…";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,mDAAmD;AACnD,eAAO,MAAM,EAAE,QAAQ,CAAA;AAEvB,oBAAoB;AACpB,eAAO,MAAM,EAAE
|
|
1
|
+
{"version":3,"file":"locales.d.ts","sourceRoot":"","sources":["../../../src/client/locales.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,mDAAmD;AACnD,eAAO,MAAM,EAAE,QAAQ,CAAA;AAEvB,oBAAoB;AACpB,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+EL,CAAA;AAEV,oBAAoB;AACpB,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+EL,CAAA;AAEV,oCAAoC;AACpC,MAAM,MAAM,MAAM,GAAG,MAAM,OAAO,EAAE,CAAA;AAEpC,qDAAqD;AACrD,MAAM,MAAM,eAAe,GAAG,MAAM,CAAA;AAEpC;;;;;GAKG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAGnD;AAED;;;;;;;GAOG;AACH,wBAAgB,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAQvE;AAED,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,kBAAkB;QAC1B,uBAAuB;QACvB,GAAG,EAAE,MAAM,CAAA;KACZ;CACF"}
|
|
@@ -13,6 +13,15 @@ export const zh = {
|
|
|
13
13
|
'pet.namePlaceholder': '输入新名字',
|
|
14
14
|
'pet.summon': '召唤{name}',
|
|
15
15
|
'pet.rank': '亲密度 {rank}',
|
|
16
|
+
'pet.rank.name.幼鲸': '幼鲸',
|
|
17
|
+
'pet.rank.name.伙伴': '伙伴',
|
|
18
|
+
'pet.rank.name.挚友': '挚友',
|
|
19
|
+
'pet.rank.name.深海羁绊': '深海羁绊',
|
|
20
|
+
'pet.rank.name.心有灵犀': '心有灵犀',
|
|
21
|
+
'pet.rank.name.传说羁绊': '传说羁绊',
|
|
22
|
+
'pet.rank.name.神话羁绊': '神话羁绊',
|
|
23
|
+
'pet.rank.name.永恒之契': '永恒之契',
|
|
24
|
+
'pet.rank.name.鲸生共渡': '鲸生共渡',
|
|
16
25
|
'pet.points': '{points} 点',
|
|
17
26
|
'pet.treats': '小鱼干 ×{n}',
|
|
18
27
|
'pet.state.loading': '宠物正在赶来…',
|
|
@@ -85,6 +94,15 @@ export const en = {
|
|
|
85
94
|
'pet.namePlaceholder': 'Enter a new name',
|
|
86
95
|
'pet.summon': 'Summon {name}',
|
|
87
96
|
'pet.rank': 'Affinity {rank}',
|
|
97
|
+
'pet.rank.name.幼鲸': 'Baby Whale',
|
|
98
|
+
'pet.rank.name.伙伴': 'Companion',
|
|
99
|
+
'pet.rank.name.挚友': 'Close Friend',
|
|
100
|
+
'pet.rank.name.深海羁绊': 'Deep Sea Bond',
|
|
101
|
+
'pet.rank.name.心有灵犀': 'Kindred Spirit',
|
|
102
|
+
'pet.rank.name.传说羁绊': 'Legendary Bond',
|
|
103
|
+
'pet.rank.name.神话羁绊': 'Mythic Bond',
|
|
104
|
+
'pet.rank.name.永恒之契': 'Eternal Covenant',
|
|
105
|
+
'pet.rank.name.鲸生共渡': 'Lifelong Companion',
|
|
88
106
|
'pet.points': '{points} pts',
|
|
89
107
|
'pet.treats': 'Treats ×{n}',
|
|
90
108
|
'pet.state.loading': 'The pet is on its way…',
|
|
@@ -8,6 +8,18 @@
|
|
|
8
8
|
* phase-mapped track. Rendering never throws: a broken track list degrades
|
|
9
9
|
* to the first decodable frame, and the 1.2 s stall watchdog re-kicks the
|
|
10
10
|
* playback chain after timer throttling.
|
|
11
|
+
*
|
|
12
|
+
* Frame presentation has two modes picked once at mount by capability
|
|
13
|
+
* probing:
|
|
14
|
+
* - Canvas bitmap buffer (default where createImageBitmap/fetch/2D context
|
|
15
|
+
* exist): every frame is decoded exactly once into an ImageBitmap during
|
|
16
|
+
* the warm pass and drawn onto one <canvas> - steady-state playback issues
|
|
17
|
+
* zero DOM mutations and zero re-decodes (measured hotspot: swapping
|
|
18
|
+
* <img>.src per frame drove image decode + invalidation every tick).
|
|
19
|
+
* - Classic <img> fallback (jsdom/tests or missing APIs): identical to the
|
|
20
|
+
* historical behavior - cache-warm Image elements plus guarded src swaps,
|
|
21
|
+
* so environments without modern decoding keep working unchanged.
|
|
22
|
+
*
|
|
11
23
|
* @module @linxin666/dsh-pet/client/renderers/frames2d
|
|
12
24
|
*/
|
|
13
25
|
import { type PetRenderer, type PetRendererHandle } from '../../contracts/renderer.ts';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"frames2d.d.ts","sourceRoot":"","sources":["../../../../src/client/renderers/frames2d.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"frames2d.d.ts","sourceRoot":"","sources":["../../../../src/client/renderers/frames2d.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAA4B,KAAK,WAAW,EAA2B,KAAK,iBAAiB,EAAE,MAAM,6BAA6B,CAAA;AACzI,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAEnD,oEAAoE;AACpE,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,SAAS,EAAE,MAAM,EAAE,CAAA;IACnB,IAAI,EAAE,OAAO,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,8DAA8D;AAC9D,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAA;IAC3C,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC,GAAG;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;CAClE;AAED,MAAM,WAAW,sBAAuB,SAAQ,iBAAiB;IAC/D,gFAAgF;IAChF,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAA;IACzC,2DAA2D;IAC3D,YAAY,IAAI,MAAM,CAAA;CACvB;AA0CD,eAAO,MAAM,gBAAgB,EAAE,WAAW,CAAC,iBAAiB,CAsP3D,CAAA"}
|
|
@@ -8,6 +8,18 @@
|
|
|
8
8
|
* phase-mapped track. Rendering never throws: a broken track list degrades
|
|
9
9
|
* to the first decodable frame, and the 1.2 s stall watchdog re-kicks the
|
|
10
10
|
* playback chain after timer throttling.
|
|
11
|
+
*
|
|
12
|
+
* Frame presentation has two modes picked once at mount by capability
|
|
13
|
+
* probing:
|
|
14
|
+
* - Canvas bitmap buffer (default where createImageBitmap/fetch/2D context
|
|
15
|
+
* exist): every frame is decoded exactly once into an ImageBitmap during
|
|
16
|
+
* the warm pass and drawn onto one <canvas> - steady-state playback issues
|
|
17
|
+
* zero DOM mutations and zero re-decodes (measured hotspot: swapping
|
|
18
|
+
* <img>.src per frame drove image decode + invalidation every tick).
|
|
19
|
+
* - Classic <img> fallback (jsdom/tests or missing APIs): identical to the
|
|
20
|
+
* historical behavior - cache-warm Image elements plus guarded src swaps,
|
|
21
|
+
* so environments without modern decoding keep working unchanged.
|
|
22
|
+
*
|
|
11
23
|
* @module @linxin666/dsh-pet/client/renderers/frames2d
|
|
12
24
|
*/
|
|
13
25
|
import { PET_RENDERER_API_VERSION } from "../../contracts/renderer.js";
|
|
@@ -50,23 +62,83 @@ export const frames2dRenderer = {
|
|
|
50
62
|
const reducedMotion = typeof window !== 'undefined'
|
|
51
63
|
&& typeof window.matchMedia === 'function'
|
|
52
64
|
&& window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
img
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const pre = new Image();
|
|
67
|
-
pre.src = url;
|
|
65
|
+
// Capability probe: the canvas path needs decode-to-bitmap, same-origin
|
|
66
|
+
// fetch, and a real 2D context (jsdom returns null and falls back below).
|
|
67
|
+
let canvas = null;
|
|
68
|
+
let context2d = null;
|
|
69
|
+
let img = null;
|
|
70
|
+
try {
|
|
71
|
+
if (typeof createImageBitmap === 'function' && typeof fetch === 'function') {
|
|
72
|
+
const probe = document.createElement('canvas');
|
|
73
|
+
const c2d = probe.getContext('2d');
|
|
74
|
+
if (c2d !== null) {
|
|
75
|
+
canvas = probe;
|
|
76
|
+
context2d = c2d;
|
|
77
|
+
}
|
|
68
78
|
}
|
|
69
79
|
}
|
|
80
|
+
catch {
|
|
81
|
+
canvas = null;
|
|
82
|
+
context2d = null;
|
|
83
|
+
}
|
|
84
|
+
if (canvas !== null && context2d !== null) {
|
|
85
|
+
canvas.dataset.dshPetFrames2d = ctx.petId;
|
|
86
|
+
canvas.draggable = false;
|
|
87
|
+
canvas.style.width = '100%';
|
|
88
|
+
canvas.style.height = '100%';
|
|
89
|
+
canvas.style.objectFit = 'contain';
|
|
90
|
+
canvas.style.pointerEvents = 'none';
|
|
91
|
+
ctx.container.appendChild(canvas);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
img = document.createElement('img');
|
|
95
|
+
img.dataset.dshPetFrames2d = ctx.petId;
|
|
96
|
+
img.alt = '';
|
|
97
|
+
img.draggable = false;
|
|
98
|
+
img.style.width = '100%';
|
|
99
|
+
img.style.height = '100%';
|
|
100
|
+
img.style.objectFit = 'contain';
|
|
101
|
+
img.style.pointerEvents = 'none';
|
|
102
|
+
ctx.container.appendChild(img);
|
|
103
|
+
}
|
|
104
|
+
// Decode cache: one concurrent decode per URL, memoized forever (frames
|
|
105
|
+
// are tiny same-origin webp files; failures resolve undefined and keep
|
|
106
|
+
// the last painted frame instead of breaking playback).
|
|
107
|
+
const decoding = new Map();
|
|
108
|
+
const decodedAll = [];
|
|
109
|
+
const loadFrame = (url) => {
|
|
110
|
+
const cached = decoding.get(url);
|
|
111
|
+
if (cached !== undefined)
|
|
112
|
+
return cached;
|
|
113
|
+
const job = (async () => {
|
|
114
|
+
try {
|
|
115
|
+
const response = await fetch(url);
|
|
116
|
+
if (!response.ok)
|
|
117
|
+
throw new Error('http ' + response.status);
|
|
118
|
+
const bitmap = await createImageBitmap(await response.blob());
|
|
119
|
+
return { source: bitmap, width: bitmap.width, height: bitmap.height };
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// Fail-open: classic Image decode keeps non-modern runtimes alive.
|
|
123
|
+
return await new Promise((resolve) => {
|
|
124
|
+
try {
|
|
125
|
+
const pre = new Image();
|
|
126
|
+
pre.onload = () => {
|
|
127
|
+
resolve(pre.naturalWidth > 0 ? { source: pre, width: pre.naturalWidth, height: pre.naturalHeight } : undefined);
|
|
128
|
+
};
|
|
129
|
+
pre.onerror = () => resolve(undefined);
|
|
130
|
+
pre.src = url;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
resolve(undefined);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
})();
|
|
138
|
+
decoding.set(url, job);
|
|
139
|
+
decodedAll.push(job.then(() => undefined, () => undefined));
|
|
140
|
+
return job;
|
|
141
|
+
};
|
|
70
142
|
let disposed = false;
|
|
71
143
|
let timer;
|
|
72
144
|
let watchdog;
|
|
@@ -74,15 +146,45 @@ export const frames2dRenderer = {
|
|
|
74
146
|
let frameIndex = 0;
|
|
75
147
|
let lastAdvance = Date.now();
|
|
76
148
|
let override;
|
|
149
|
+
let drawToken = 0;
|
|
150
|
+
let lastDrawnUrl;
|
|
77
151
|
const trackForPhase = (phase) => {
|
|
78
152
|
const mapped = config.phases[phase];
|
|
79
153
|
return mapped !== undefined && config.tracks[mapped] !== undefined ? mapped : config.phases.idle;
|
|
80
154
|
};
|
|
155
|
+
/** Canvas path: paints the newest requested frame; stale draws drop out. */
|
|
156
|
+
const paintCanvas = (url) => {
|
|
157
|
+
if (context2d === null || canvas === null)
|
|
158
|
+
return;
|
|
159
|
+
const myToken = ++drawToken;
|
|
160
|
+
void loadFrame(url).then((frame) => {
|
|
161
|
+
if (disposed || frame === undefined || myToken !== drawToken)
|
|
162
|
+
return;
|
|
163
|
+
if (lastDrawnUrl === url)
|
|
164
|
+
return;
|
|
165
|
+
lastDrawnUrl = url;
|
|
166
|
+
// Resizing clears the canvas, so size only when it actually differs.
|
|
167
|
+
if (canvas.width !== frame.width || canvas.height !== frame.height) {
|
|
168
|
+
canvas.width = frame.width;
|
|
169
|
+
canvas.height = frame.height;
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
context2d.clearRect(0, 0, canvas.width, canvas.height);
|
|
173
|
+
}
|
|
174
|
+
context2d.drawImage(frame.source, 0, 0);
|
|
175
|
+
}).catch(() => { });
|
|
176
|
+
};
|
|
81
177
|
const show = (trackId, index) => {
|
|
82
178
|
const def = config.tracks[trackId];
|
|
83
179
|
const url = def?.frames[index];
|
|
84
|
-
if (url
|
|
85
|
-
|
|
180
|
+
if (url === undefined)
|
|
181
|
+
return;
|
|
182
|
+
if (img !== null) {
|
|
183
|
+
if (img.getAttribute('src') !== url)
|
|
184
|
+
img.src = url;
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
paintCanvas(url);
|
|
86
188
|
};
|
|
87
189
|
const schedule = (ms) => {
|
|
88
190
|
if (disposed)
|
|
@@ -155,6 +257,13 @@ export const frames2dRenderer = {
|
|
|
155
257
|
tick();
|
|
156
258
|
}, WATCHDOG_MS);
|
|
157
259
|
}
|
|
260
|
+
// Warm pass: decode every frame up front (tiny same-origin webp files)
|
|
261
|
+
// so loops and phase switches never wait on a first decode - same intent
|
|
262
|
+
// as the historical Image-cache warm loop, now feeding the decode cache.
|
|
263
|
+
for (const warmTrack of Object.values(config.tracks)) {
|
|
264
|
+
for (const warmUrl of warmTrack.frames)
|
|
265
|
+
void loadFrame(warmUrl);
|
|
266
|
+
}
|
|
158
267
|
play(track);
|
|
159
268
|
let disposedOnce = false;
|
|
160
269
|
const dispose = () => {
|
|
@@ -167,7 +276,23 @@ export const frames2dRenderer = {
|
|
|
167
276
|
clearTimeout(timer);
|
|
168
277
|
if (watchdog !== undefined)
|
|
169
278
|
clearInterval(watchdog);
|
|
170
|
-
|
|
279
|
+
// Release decoded bitmaps after pending decodes settle; close() is
|
|
280
|
+
// browser-only, so guard it for exotic hosts.
|
|
281
|
+
void Promise.allSettled(decodedAll).then(() => {
|
|
282
|
+
for (const job of decoding.values()) {
|
|
283
|
+
void job.then((frame) => {
|
|
284
|
+
try {
|
|
285
|
+
const maybeClose = frame?.source?.close;
|
|
286
|
+
if (typeof maybeClose === 'function' && frame !== undefined)
|
|
287
|
+
maybeClose.call(frame.source);
|
|
288
|
+
}
|
|
289
|
+
catch { /* already released */ }
|
|
290
|
+
}).catch(() => { });
|
|
291
|
+
}
|
|
292
|
+
decoding.clear();
|
|
293
|
+
});
|
|
294
|
+
canvas?.remove();
|
|
295
|
+
img?.remove();
|
|
171
296
|
};
|
|
172
297
|
ctx.onCleanup(dispose);
|
|
173
298
|
return {
|
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
* {@link ProjectionRuntime} per session and feed events in arrival order.
|
|
6
6
|
*
|
|
7
7
|
* Status copy comes from the chatter voice (big rotating pools, per-tool
|
|
8
|
-
* families, real-argument hints), and
|
|
9
|
-
*
|
|
8
|
+
* families, real-argument hints), and the projection feeds the murmur engine
|
|
9
|
+
* (碎碎念) with the SITUATION — thinking/writing during the stream, the
|
|
10
|
+
* running tool family on tool calls, and the STRUCTURED outcome on
|
|
11
|
+
* tool/result and turn/end — so the pet's inner voice always roughly knows
|
|
12
|
+
* what is going on and never mis-fires on output text. The wall clock is
|
|
10
13
|
* injected by the caller, keeping every projection reproducible.
|
|
11
14
|
* @module @linxin666/dsh-pet/event-projection
|
|
12
15
|
*/
|
|
@@ -22,18 +25,20 @@ export interface ActivityStatusEventLike {
|
|
|
22
25
|
/** Per-session facts needed to project the official event stream. */
|
|
23
26
|
export interface ProjectionRuntime {
|
|
24
27
|
activeTools: Set<string>;
|
|
28
|
+
/** callIds whose tool looked like a test run, marked at tool/call (pet M6). */
|
|
29
|
+
testCalls: Set<string>;
|
|
25
30
|
officialEventsSeen: boolean;
|
|
26
31
|
stepHadFailure: boolean;
|
|
27
32
|
/** Round-robin status copy voice (scene-stable, cadence-rotated). */
|
|
28
33
|
voice: StatusVoice;
|
|
29
|
-
/** Inner-whisper engine fed by the
|
|
34
|
+
/** Inner-whisper engine fed by the projection's situation and outcomes. */
|
|
30
35
|
whispers: WhisperEngine;
|
|
31
36
|
}
|
|
32
37
|
/** One official event projection, optionally carrying a completed turn reward. */
|
|
33
38
|
export interface PetActivityTransition {
|
|
34
39
|
input: PetStateInput;
|
|
35
40
|
completedTurn?: number;
|
|
36
|
-
/** A fresh inner whisper woken by this event
|
|
41
|
+
/** A fresh inner whisper woken by this event, when any. */
|
|
37
42
|
whisper?: string;
|
|
38
43
|
}
|
|
39
44
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event-projection.d.ts","sourceRoot":"","sources":["../../src/event-projection.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"event-projection.d.ts","sourceRoot":"","sources":["../../src/event-projection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAC5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAC/C,OAAO,EACL,WAAW,EAGX,aAAa,EAGb,KAAK,kBAAkB,EACxB,MAAM,cAAc,CAAA;AAErB,2DAA2D;AAC3D,MAAM,WAAW,uBAAuB;IACtC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,qEAAqE;AACrE,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACxB,+EAA+E;IAC/E,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IACtB,kBAAkB,EAAE,OAAO,CAAA;IAC3B,cAAc,EAAE,OAAO,CAAA;IACvB,qEAAqE;IACrE,KAAK,EAAE,WAAW,CAAA;IAClB,2EAA2E;IAC3E,QAAQ,EAAE,aAAa,CAAA;CACxB;AAED,kFAAkF;AAClF,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,aAAa,CAAA;IACpB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,CAAC,EAAE,kBAAkB,GAAG,iBAAiB,CASpF;AAQD,wEAAwE;AACxE,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,aAAa,CAAC,OAAO,CAAC,CAE9E;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,YAAY,EACnB,OAAO,EAAE,iBAAiB,EAC1B,KAAK,GAAE,MAAmB,GACzB,qBAAqB,GAAG,SAAS,CAmHnC"}
|
|
@@ -5,12 +5,15 @@
|
|
|
5
5
|
* {@link ProjectionRuntime} per session and feed events in arrival order.
|
|
6
6
|
*
|
|
7
7
|
* Status copy comes from the chatter voice (big rotating pools, per-tool
|
|
8
|
-
* families, real-argument hints), and
|
|
9
|
-
*
|
|
8
|
+
* families, real-argument hints), and the projection feeds the murmur engine
|
|
9
|
+
* (碎碎念) with the SITUATION — thinking/writing during the stream, the
|
|
10
|
+
* running tool family on tool calls, and the STRUCTURED outcome on
|
|
11
|
+
* tool/result and turn/end — so the pet's inner voice always roughly knows
|
|
12
|
+
* what is going on and never mis-fires on output text. The wall clock is
|
|
10
13
|
* injected by the caller, keeping every projection reproducible.
|
|
11
14
|
* @module @linxin666/dsh-pet/event-projection
|
|
12
15
|
*/
|
|
13
|
-
import { StatusVoice, toolArgHint, WhisperEngine } from "./chatter.js";
|
|
16
|
+
import { StatusVoice, toolArgHint, toolCategory, WhisperEngine, whisperCategoryOf, looksLikeTestTool, } from "./chatter.js";
|
|
14
17
|
/**
|
|
15
18
|
* Fresh projection runtime for a newly seen session. The optional voice-pack
|
|
16
19
|
* provider (pet-center M4, issue #677) hands both chatter engines their
|
|
@@ -20,6 +23,7 @@ import { StatusVoice, toolArgHint, WhisperEngine } from "./chatter.js";
|
|
|
20
23
|
export function emptyProjectionRuntime(pools) {
|
|
21
24
|
return {
|
|
22
25
|
activeTools: new Set(),
|
|
26
|
+
testCalls: new Set(),
|
|
23
27
|
officialEventsSeen: false,
|
|
24
28
|
stepHadFailure: false,
|
|
25
29
|
voice: new StatusVoice(pools),
|
|
@@ -44,6 +48,7 @@ export function projectOfficialEvent(event, runtime, nowMs = Date.now()) {
|
|
|
44
48
|
switch (event.type) {
|
|
45
49
|
case 'turn/start':
|
|
46
50
|
runtime.activeTools.clear();
|
|
51
|
+
runtime.testCalls.clear();
|
|
47
52
|
runtime.stepHadFailure = false;
|
|
48
53
|
return { input: { phase: 'waiting', line: runtime.voice.scene('prepare', nowMs) } };
|
|
49
54
|
case 'step/start':
|
|
@@ -53,14 +58,14 @@ export function projectOfficialEvent(event, runtime, nowMs = Date.now()) {
|
|
|
53
58
|
case 'assistant/chunk': {
|
|
54
59
|
const { chunk } = event.data;
|
|
55
60
|
if (chunk.type === 'reasoning-delta' && chunk.text.length > 0) {
|
|
56
|
-
const whisper = runtime.whispers.feed(
|
|
61
|
+
const whisper = runtime.whispers.feed('thinking', nowMs);
|
|
57
62
|
return {
|
|
58
63
|
input: { phase: 'thinking', line: runtime.voice.scene('thinking', nowMs) },
|
|
59
64
|
...(whisper === undefined ? {} : { whisper }),
|
|
60
65
|
};
|
|
61
66
|
}
|
|
62
67
|
if (chunk.type === 'text-delta' && chunk.text.length > 0) {
|
|
63
|
-
const whisper = runtime.whispers.feed(
|
|
68
|
+
const whisper = runtime.whispers.feed('writing', nowMs);
|
|
64
69
|
return {
|
|
65
70
|
input: { phase: 'review', line: runtime.voice.scene('review', nowMs) },
|
|
66
71
|
...(whisper === undefined ? {} : { whisper }),
|
|
@@ -70,40 +75,67 @@ export function projectOfficialEvent(event, runtime, nowMs = Date.now()) {
|
|
|
70
75
|
}
|
|
71
76
|
case 'assistant/message':
|
|
72
77
|
return { input: { phase: 'review', line: runtime.voice.scene('review', nowMs) } };
|
|
73
|
-
case 'tool/call':
|
|
74
|
-
|
|
78
|
+
case 'tool/call': {
|
|
79
|
+
const callId = String(event.data.callId);
|
|
80
|
+
runtime.activeTools.add(callId);
|
|
81
|
+
// A test-looking call is remembered so its result (the only place the
|
|
82
|
+
// outcome is known) can wake the test-green mood later.
|
|
83
|
+
if (looksLikeTestTool(event.data.name, event.data.arguments))
|
|
84
|
+
runtime.testCalls.add(callId);
|
|
85
|
+
const whisper = runtime.whispers.feed(whisperCategoryOf(toolCategory(event.data.name)), nowMs);
|
|
75
86
|
return {
|
|
76
87
|
input: {
|
|
77
88
|
phase: 'tool',
|
|
78
89
|
line: runtime.voice.tool(event.data.name, displayToolName(event.data.name), toolArgHint(event.data.name, event.data.arguments), nowMs),
|
|
79
90
|
},
|
|
91
|
+
...(whisper === undefined ? {} : { whisper }),
|
|
80
92
|
};
|
|
93
|
+
}
|
|
81
94
|
case 'tool/result': {
|
|
82
95
|
const block = event.data.message.content[0];
|
|
83
|
-
|
|
84
|
-
|
|
96
|
+
const callId = String(event.data.message.source.callId);
|
|
97
|
+
const failed = event.data.error !== undefined || block.isError === true;
|
|
98
|
+
const wasTest = runtime.testCalls.delete(callId);
|
|
99
|
+
runtime.activeTools.delete(callId);
|
|
100
|
+
runtime.stepHadFailure ||= failed;
|
|
101
|
+
const whisper = failed
|
|
102
|
+
? runtime.whispers.result('fail', nowMs)
|
|
103
|
+
: wasTest
|
|
104
|
+
? runtime.whispers.result('pass', nowMs)
|
|
105
|
+
: undefined;
|
|
106
|
+
const whisperSpread = whisper === undefined ? {} : { whisper };
|
|
85
107
|
if (runtime.activeTools.size > 0) {
|
|
86
108
|
return {
|
|
87
109
|
input: {
|
|
88
110
|
phase: 'tool',
|
|
89
111
|
line: runtime.voice.toolRemaining(runtime.activeTools.size, nowMs),
|
|
90
112
|
},
|
|
113
|
+
...whisperSpread,
|
|
91
114
|
};
|
|
92
115
|
}
|
|
93
116
|
return runtime.stepHadFailure
|
|
94
|
-
? { input: { phase: 'failed', line: runtime.voice.scene('toolFailed', nowMs) } }
|
|
95
|
-
: { input: { phase: 'thinking', line: runtime.voice.scene('toolResult', nowMs) } };
|
|
117
|
+
? { input: { phase: 'failed', line: runtime.voice.scene('toolFailed', nowMs) }, ...whisperSpread }
|
|
118
|
+
: { input: { phase: 'thinking', line: runtime.voice.scene('toolResult', nowMs) }, ...whisperSpread };
|
|
96
119
|
}
|
|
97
120
|
case 'turn/end': {
|
|
98
121
|
runtime.activeTools.clear();
|
|
122
|
+
runtime.testCalls.clear();
|
|
99
123
|
switch (event.data.reason.kind) {
|
|
100
|
-
case 'completed':
|
|
124
|
+
case 'completed': {
|
|
125
|
+
const whisper = runtime.whispers.result('done', nowMs);
|
|
101
126
|
return {
|
|
102
127
|
input: { phase: 'done', line: runtime.voice.scene('done', nowMs) },
|
|
103
128
|
completedTurn: event.data.turn,
|
|
129
|
+
...(whisper === undefined ? {} : { whisper }),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
case 'error': {
|
|
133
|
+
const whisper = runtime.whispers.result('fail', nowMs);
|
|
134
|
+
return {
|
|
135
|
+
input: { phase: 'failed', line: runtime.voice.scene('failed', nowMs) },
|
|
136
|
+
...(whisper === undefined ? {} : { whisper }),
|
|
104
137
|
};
|
|
105
|
-
|
|
106
|
-
return { input: { phase: 'failed', line: runtime.voice.scene('failed', nowMs) } };
|
|
138
|
+
}
|
|
107
139
|
case 'max-tokens':
|
|
108
140
|
return { input: { phase: 'failed', line: runtime.voice.scene('maxTokens', nowMs) } };
|
|
109
141
|
case 'interrupted':
|