@clowder-ai/companion 0.1.0-alpha.3 → 0.1.0-alpha.30
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/NOTICE +7 -2
- package/README.md +54 -5
- package/manifest.json +3 -3
- package/package.json +5 -3
- package/plugin.yaml +3 -3
- package/renderer/app-assist-controller.mjs +125 -0
- package/renderer/call-controls-presentation.mjs +179 -0
- package/renderer/call-duration.mjs +59 -0
- package/renderer/call-transcript-view.mjs +240 -0
- package/renderer/chat-unread.mjs +44 -0
- package/renderer/client.mjs +75 -3
- package/renderer/compact-size.mjs +8 -0
- package/renderer/companion-identity.mjs +42 -0
- package/renderer/conversation-draft.mjs +304 -0
- package/renderer/conversation-history.mjs +176 -0
- package/renderer/conversation-surface.mjs +202 -0
- package/renderer/conversation.mjs +155 -21
- package/renderer/decision-panel.mjs +122 -0
- package/renderer/decision-view.mjs +124 -0
- package/renderer/errors.mjs +4 -4
- package/renderer/index.html +202 -25
- package/renderer/living-body.mjs +147 -0
- package/renderer/living-edge.mjs +16 -0
- package/renderer/living-geometry.mjs +96 -0
- package/renderer/meeting-state.mjs +55 -0
- package/renderer/motion-facts.mjs +108 -0
- package/renderer/native-window-travel.mjs +89 -0
- package/renderer/native-work-motion.mjs +39 -0
- package/renderer/peer.mjs +1 -1
- package/renderer/pet-controls.mjs +139 -27
- package/renderer/pet-motion-assets.mjs +22 -0
- package/renderer/pet-motion.mjs +343 -0
- package/renderer/recent-bubble.mjs +93 -0
- package/renderer/reply-letter-controller.mjs +361 -0
- package/renderer/reply-letter-state.mjs +38 -0
- package/renderer/reply-letter-view.mjs +116 -0
- package/renderer/settings-controller.mjs +171 -0
- package/renderer/settings-view.mjs +241 -0
- package/renderer/skins/xianxian-codex.webp +0 -0
- package/renderer/skins/xianxian-living/clips/carry.webm +0 -0
- package/renderer/skins/xianxian-living/clips/peek.webm +0 -0
- package/renderer/skins/xianxian-living/clips/peek_fade.webm +0 -0
- package/renderer/skins/xianxian-living/clips/pounce.webm +0 -0
- package/renderer/skins/xianxian-living/clips/sleep.webm +0 -0
- package/renderer/skins/xianxian-living/clips/think.webm +0 -0
- package/renderer/skins/xianxian-living/clips/trot.webm +0 -0
- package/renderer/skins/xianxian-living/clips/wait.webm +0 -0
- package/renderer/skins/xianxian-living/clips/wake.webm +0 -0
- package/renderer/skins/xianxian-living/layers/body.webp +0 -0
- package/renderer/skins/xianxian-living/layers/closed.webp +0 -0
- package/renderer/skins/xianxian-living/layers/earL.webp +0 -0
- package/renderer/skins/xianxian-living/layers/earR.webp +0 -0
- package/renderer/skins/xianxian-living/layers/head.webp +0 -0
- package/renderer/skins/xianxian-living/layers/irisL.webp +0 -0
- package/renderer/skins/xianxian-living/layers/irisR.webp +0 -0
- package/renderer/skins/xianxian-living/layers/meta.json +88 -0
- package/renderer/skins/xianxian-living/layers/mouth.webp +0 -0
- package/renderer/skins/xianxian-living/layers/scared.webp +0 -0
- package/renderer/skins/xianxian-living/layers/tail.webp +0 -0
- package/renderer/skins/xianxian-living/layers/whiteL.webp +0 -0
- package/renderer/skins/xianxian-living/layers/whiteR.webp +0 -0
- package/renderer/skins/xianxian-sleeping-row.png +0 -0
- package/renderer/skins/xianxian-staged-thought-row.png +0 -0
- package/renderer/skins/xianxian-working-row.png +0 -0
- package/renderer/style.css +325 -27
- package/renderer/surface.mjs +309 -51
- package/renderer/transcript-view.mjs +117 -15
- package/renderer/visible-text-receipt.mjs +241 -0
- package/renderer/work-overview.mjs +95 -0
- package/source-lock.json +122 -2
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const validId = value => typeof value === 'string' && value.length > 0 && value.length <= 256;
|
|
2
|
+
const validExpiry = (value, now) => Number.isFinite(value) && value > now;
|
|
3
|
+
const workKinds = new Set(['reasoning', 'tool', 'workspace', 'screen_reading']);
|
|
4
|
+
const normalizeTravel = (travel, now, suppressed) => travel && validId(travel.eventId)
|
|
5
|
+
&& travel.status === 'active' && Number.isFinite(travel.dx) && Number.isFinite(travel.dy)
|
|
6
|
+
&& Math.hypot(travel.dx, travel.dy) >= 1 && validExpiry(travel.expiresAt, now)
|
|
7
|
+
&& !suppressed.has(travel.eventId)
|
|
8
|
+
? { eventId: travel.eventId, dx: travel.dx, dy: travel.dy, expiresAt: travel.expiresAt } : null;
|
|
9
|
+
|
|
10
|
+
/** A bounded, expiring projection of Host-owned motion facts. */
|
|
11
|
+
export class MotionFacts {
|
|
12
|
+
constructor({ now, setTimer, clearTimer, changed }) {
|
|
13
|
+
this.now = now;
|
|
14
|
+
this.setTimer = setTimer;
|
|
15
|
+
this.clearTimer = clearTimer;
|
|
16
|
+
this.changed = changed;
|
|
17
|
+
this.snapshot = { work: null, delivery: null, travel: null };
|
|
18
|
+
this.seenResults = new Map();
|
|
19
|
+
this.suppressedTravel = new Map();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
sync(value, suppressTravel = false) {
|
|
23
|
+
const now = this.now();
|
|
24
|
+
this.prune(now);
|
|
25
|
+
const input = value && typeof value === 'object' ? value : {};
|
|
26
|
+
const work = input.work;
|
|
27
|
+
const delivery = input.delivery;
|
|
28
|
+
const travel = input.travel;
|
|
29
|
+
this.snapshot = {
|
|
30
|
+
work: work && validId(work.taskId) && work.status === 'active' && workKinds.has(work.kind)
|
|
31
|
+
&& validExpiry(work.expiresAt, now)
|
|
32
|
+
? { taskId: work.taskId, kind: work.kind, expiresAt: work.expiresAt } : null,
|
|
33
|
+
delivery: delivery && validId(delivery.resultId) && delivery.status === 'applied'
|
|
34
|
+
&& validExpiry(delivery.expiresAt, now)
|
|
35
|
+
? { resultId: delivery.resultId, expiresAt: delivery.expiresAt } : null,
|
|
36
|
+
travel: normalizeTravel(travel, now, this.suppressedTravel),
|
|
37
|
+
};
|
|
38
|
+
if (suppressTravel) this.suppressTravel();
|
|
39
|
+
this.scheduleExpiry();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
syncNative(value) {
|
|
43
|
+
const now = this.now();
|
|
44
|
+
this.prune(now);
|
|
45
|
+
const input = value && typeof value === 'object' ? value : {};
|
|
46
|
+
const work = input.work;
|
|
47
|
+
const delivery = input.delivery;
|
|
48
|
+
const travel = this.snapshot.travel?.expiresAt > now ? this.snapshot.travel : null;
|
|
49
|
+
this.snapshot = {
|
|
50
|
+
work: work && validId(work.taskId) && work.status === 'active' && workKinds.has(work.kind)
|
|
51
|
+
&& validExpiry(work.expiresAt, now)
|
|
52
|
+
? { taskId: work.taskId, kind: work.kind, expiresAt: work.expiresAt } : null,
|
|
53
|
+
delivery: delivery && validId(delivery.resultId) && delivery.status === 'applied'
|
|
54
|
+
&& validExpiry(delivery.expiresAt, now)
|
|
55
|
+
? { resultId: delivery.resultId, expiresAt: delivery.expiresAt } : null,
|
|
56
|
+
travel,
|
|
57
|
+
};
|
|
58
|
+
this.scheduleExpiry();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
syncTravel(value, suppressTravel = false) {
|
|
62
|
+
const now = this.now();
|
|
63
|
+
this.prune(now);
|
|
64
|
+
this.snapshot.travel = normalizeTravel(value, now, this.suppressedTravel);
|
|
65
|
+
if (suppressTravel) this.suppressTravel();
|
|
66
|
+
this.scheduleExpiry();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
consumeDelivery() {
|
|
70
|
+
const delivery = this.snapshot.delivery;
|
|
71
|
+
if (!delivery || this.seenResults.has(delivery.resultId)) return null;
|
|
72
|
+
this.seenResults.set(delivery.resultId, this.now() + 3_600_000);
|
|
73
|
+
return delivery;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
suppressTravel() {
|
|
77
|
+
if (!this.snapshot.travel) return;
|
|
78
|
+
this.suppressedTravel.set(this.snapshot.travel.eventId, this.now() + 3_600_000);
|
|
79
|
+
this.snapshot.travel = null;
|
|
80
|
+
this.scheduleExpiry();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
close() {
|
|
84
|
+
if (this.timer !== undefined) this.clearTimer(this.timer);
|
|
85
|
+
this.timer = undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
scheduleExpiry() {
|
|
89
|
+
this.close();
|
|
90
|
+
const expiries = Object.values(this.snapshot).filter(Boolean).map(value => value.expiresAt);
|
|
91
|
+
if (!expiries.length) return;
|
|
92
|
+
this.timer = this.setTimer(() => {
|
|
93
|
+
this.timer = undefined;
|
|
94
|
+
const now = this.now();
|
|
95
|
+
for (const key of ['work', 'delivery', 'travel']) {
|
|
96
|
+
if (this.snapshot[key]?.expiresAt <= now) this.snapshot[key] = null;
|
|
97
|
+
}
|
|
98
|
+
this.scheduleExpiry();
|
|
99
|
+
this.changed();
|
|
100
|
+
}, Math.max(0, Math.min(...expiries) - this.now()));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
prune(now) {
|
|
104
|
+
for (const collection of [this.seenResults, this.suppressedTravel]) {
|
|
105
|
+
for (const [id, expiresAt] of collection) if (expiresAt <= now) collection.delete(id);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const validPosition = value => value && Number.isFinite(value.x) && Number.isFinite(value.y);
|
|
2
|
+
|
|
3
|
+
/** Observe the native window itself. Running animation follows measured desktop
|
|
4
|
+
* movement; renderer state never invents a target or claims a move succeeded. */
|
|
5
|
+
export class NativeWindowTravel {
|
|
6
|
+
constructor({
|
|
7
|
+
readPosition,
|
|
8
|
+
changed,
|
|
9
|
+
now = Date.now,
|
|
10
|
+
setTimer = globalThis.setTimeout,
|
|
11
|
+
clearTimer = globalThis.clearTimeout,
|
|
12
|
+
intervalMs = 80,
|
|
13
|
+
expiresMs = 240,
|
|
14
|
+
}) {
|
|
15
|
+
this.readPosition = readPosition;
|
|
16
|
+
this.changed = changed;
|
|
17
|
+
this.now = now;
|
|
18
|
+
this.setTimer = (callback, delay) => setTimer.call(globalThis, callback, delay);
|
|
19
|
+
this.clearTimer = id => clearTimer.call(globalThis, id);
|
|
20
|
+
this.intervalMs = Math.max(32, intervalMs);
|
|
21
|
+
this.expiresMs = Math.max(this.intervalMs, expiresMs);
|
|
22
|
+
this.sequence = 0;
|
|
23
|
+
this.manual = false;
|
|
24
|
+
this.closed = false;
|
|
25
|
+
this.active = null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
start() {
|
|
29
|
+
if (this.closed || this.timer !== undefined) return;
|
|
30
|
+
this.previous = this.read();
|
|
31
|
+
this.schedule();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
setManual(value) {
|
|
35
|
+
const manual = value === true;
|
|
36
|
+
if (manual === this.manual) return;
|
|
37
|
+
this.manual = manual;
|
|
38
|
+
this.previous = this.read();
|
|
39
|
+
if (manual) this.clearActive();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
tick() {
|
|
43
|
+
this.timer = undefined;
|
|
44
|
+
if (this.closed) return;
|
|
45
|
+
const next = this.read();
|
|
46
|
+
const now = this.now();
|
|
47
|
+
if (next && this.previous) {
|
|
48
|
+
const dx = next.x - this.previous.x;
|
|
49
|
+
const dy = next.y - this.previous.y;
|
|
50
|
+
if (!this.manual && Math.hypot(dx, dy) >= 1) {
|
|
51
|
+
const eventId = this.active?.eventId ?? `native-${Math.trunc(now)}-${++this.sequence}`;
|
|
52
|
+
this.active = { eventId, status: 'active', dx, dy, expiresAt: now + this.expiresMs };
|
|
53
|
+
this.changed({ ...this.active });
|
|
54
|
+
} else if (this.active && this.active.expiresAt <= now) {
|
|
55
|
+
this.clearActive();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
this.previous = next;
|
|
59
|
+
this.schedule();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
close() {
|
|
63
|
+
if (this.closed) return;
|
|
64
|
+
this.closed = true;
|
|
65
|
+
if (this.timer !== undefined) this.clearTimer(this.timer);
|
|
66
|
+
this.timer = undefined;
|
|
67
|
+
this.clearActive();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
read() {
|
|
71
|
+
try {
|
|
72
|
+
const value = this.readPosition();
|
|
73
|
+
return validPosition(value) ? { x: value.x, y: value.y } : null;
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
schedule() {
|
|
80
|
+
if (this.closed || this.timer !== undefined) return;
|
|
81
|
+
this.timer = this.setTimer(() => this.tick(), this.intervalMs);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
clearActive() {
|
|
85
|
+
if (!this.active) return;
|
|
86
|
+
this.active = null;
|
|
87
|
+
this.changed(null);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const priority = { reasoning: 1, tool: 2, workspace_fetch: 3, workspace_dispatch: 4, screen_read: 5 };
|
|
2
|
+
const semantic = {
|
|
3
|
+
reasoning: 'reasoning',
|
|
4
|
+
tool: 'tool',
|
|
5
|
+
workspace_fetch: 'workspace',
|
|
6
|
+
workspace_dispatch: 'workspace',
|
|
7
|
+
screen_read: 'screen_reading',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** Normalize the Host's bounded call snapshot; never infer work from chat text. */
|
|
11
|
+
export function normalizeNativeWork(value, now = Date.now()) {
|
|
12
|
+
if (!value || typeof value !== 'object' || !Array.isArray(value.active) || !Array.isArray(value.recent)) return null;
|
|
13
|
+
const active = value.active
|
|
14
|
+
.filter(entry => entry && semantic[entry.kind] && Number.isFinite(entry.expiresAt) && entry.expiresAt > now)
|
|
15
|
+
.sort((left, right) => priority[right.kind] - priority[left.kind] || right.startedAt - left.startedAt)[0];
|
|
16
|
+
const handed = [...value.recent].reverse().find(event => event?.kind === 'result'
|
|
17
|
+
&& event.phase === 'result_handed_to_voice' && typeof event.resultId === 'string'
|
|
18
|
+
&& event.resultId.length > 0 && Number.isFinite(event.expiresAt) && event.expiresAt > now);
|
|
19
|
+
return {
|
|
20
|
+
work: active ? { taskId: active.taskId, kind: semantic[active.kind], status: 'active', expiresAt: active.expiresAt } : null,
|
|
21
|
+
delivery: handed ? { resultId: handed.resultId, status: 'applied', expiresAt: handed.expiresAt } : null,
|
|
22
|
+
travel: null,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const workLabels = {
|
|
27
|
+
reasoning: '正在思考',
|
|
28
|
+
tool: '正在处理',
|
|
29
|
+
workspace: '正在处理家里的事务',
|
|
30
|
+
screen_reading: '正在查看共享画面',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Product wording for the same bounded work fact used by body motion. */
|
|
34
|
+
export function nativeWorkLabel(snapshot, nativeActivity = 'none') {
|
|
35
|
+
const kind = snapshot
|
|
36
|
+
? snapshot.work?.kind
|
|
37
|
+
: nativeActivity === 'reasoning' ? 'reasoning' : nativeActivity === 'tool_running' ? 'tool' : null;
|
|
38
|
+
return workLabels[kind] ?? '';
|
|
39
|
+
}
|
package/renderer/peer.mjs
CHANGED
|
@@ -10,7 +10,7 @@ export class VoicePeer {
|
|
|
10
10
|
}
|
|
11
11
|
});
|
|
12
12
|
}
|
|
13
|
-
async connect() { if (!this.closed) await this.client.connectAudio(); }
|
|
13
|
+
async connect(mode) { if (!this.closed) await this.client.connectAudio(mode); }
|
|
14
14
|
muteMic(muted) { void this.client.muteMicrophone(muted).catch(() => this.failed()); }
|
|
15
15
|
muteSpeaker(muted) { void this.client.muteSpeaker(muted).catch(() => this.failed()); }
|
|
16
16
|
failed() { if (!this.closed) this.onEvent({ type: 'error', code: 'unavailable' }); }
|
|
@@ -1,29 +1,113 @@
|
|
|
1
1
|
/** Transient controls. The Host owns native placement, movement and hiding. */
|
|
2
|
-
export function bindPetControls(client, {
|
|
2
|
+
export function bindPetControls(client, {
|
|
3
|
+
action, error, changed, moved = () => {}, activate = () => false, escape = () => false,
|
|
4
|
+
}) {
|
|
3
5
|
const $ = id => document.getElementById(id);
|
|
4
6
|
const body = $('pet');
|
|
5
|
-
|
|
7
|
+
const panelIds = ['actions', 'menu', 'chat', 'bubble', 'app-assist', 'decisions', 'transcript', 'settings'];
|
|
8
|
+
const callBubble = $('call-bubble');
|
|
9
|
+
const managedIds = [...panelIds, 'call-bubble'];
|
|
10
|
+
const desiredSizes = new WeakMap();
|
|
11
|
+
let panel = 'none', ambientPanel = 'none', layoutRevision = 0, layoutScheduled = false, drag, suppressClick = false;
|
|
12
|
+
const measure = node => {
|
|
13
|
+
if (!node) return { width: 120, height: 130 };
|
|
14
|
+
// Host placement is an applied cap, not the panel's desired geometry.
|
|
15
|
+
// Release it synchronously while measuring so CSS height and nested flex
|
|
16
|
+
// overflow can expose their natural demand, then restore it before paint.
|
|
17
|
+
const appliedMaxHeight = node.style.maxHeight;
|
|
18
|
+
node.style.maxHeight = '';
|
|
19
|
+
const desired = {
|
|
20
|
+
width: Math.min(420, Math.max(120, Math.ceil(Math.max(node.offsetWidth, node.scrollWidth)))),
|
|
21
|
+
height: Math.min(500, Math.max(32, Math.ceil(Math.max(node.offsetHeight, node.scrollHeight)))),
|
|
22
|
+
};
|
|
23
|
+
node.style.maxHeight = appliedMaxHeight;
|
|
24
|
+
return desired;
|
|
25
|
+
};
|
|
26
|
+
const hasCallBubble = () => callBubble?.querySelector('p:not([hidden])') !== null;
|
|
27
|
+
const callLayout = next => {
|
|
28
|
+
if (next !== 'actions') {
|
|
29
|
+
if (callBubble) callBubble.hidden = true;
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
const presentation = $('actions').dataset.presentation === 'controls' ? 'controls' : 'lite';
|
|
33
|
+
const call = { presentation };
|
|
34
|
+
if (callBubble && hasCallBubble()) {
|
|
35
|
+
callBubble.hidden = false;
|
|
36
|
+
call.bubble = measure(callBubble);
|
|
37
|
+
} else if (callBubble) callBubble.hidden = true;
|
|
38
|
+
return call;
|
|
39
|
+
};
|
|
40
|
+
const sizeKey = (desired, call) => [
|
|
41
|
+
desired.width, desired.height, call?.presentation ?? '',
|
|
42
|
+
call?.bubble?.width ?? '', call?.bubble?.height ?? '',
|
|
43
|
+
].join(':');
|
|
6
44
|
async function show(next) {
|
|
7
|
-
if (
|
|
8
|
-
|
|
9
|
-
for (const id of ['actions', 'menu', 'chat']) $(id).hidden = id !== next;
|
|
10
|
-
body.setAttribute('aria-expanded', String(next !== 'none'));
|
|
45
|
+
if (next === 'none' && ambientPanel !== 'none') next = ambientPanel;
|
|
46
|
+
const previousPanel = panel;
|
|
11
47
|
const node = $(next);
|
|
48
|
+
panel = next;
|
|
49
|
+
for (const id of panelIds) $(id).hidden = id !== next;
|
|
50
|
+
body.setAttribute('aria-expanded', String(!['none', 'bubble'].includes(next)));
|
|
51
|
+
const call = callLayout(next);
|
|
52
|
+
const desired = measure(node);
|
|
53
|
+
if (node) desiredSizes.set(node, sizeKey(desired, call));
|
|
12
54
|
const revision = ++layoutRevision;
|
|
13
55
|
try {
|
|
14
|
-
const placed = await client.layout(next,
|
|
56
|
+
const placed = await client.layout(next, desired.width, desired.height, call);
|
|
15
57
|
if (revision !== layoutRevision) return;
|
|
16
58
|
$('anchor').style.left = `${placed.pet.x}px`; $('anchor').style.top = `${placed.pet.y}px`;
|
|
17
59
|
if (node) { node.style.left = `${placed.panel.x}px`; node.style.top = `${placed.panel.y}px`; node.style.maxHeight = `${placed.panel.height}px`; }
|
|
18
|
-
if (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
60
|
+
if (callBubble) {
|
|
61
|
+
const visible = next === 'actions' && call?.bubble && placed.bubble;
|
|
62
|
+
callBubble.hidden = !visible;
|
|
63
|
+
if (visible) {
|
|
64
|
+
callBubble.style.left = `${placed.bubble.x}px`;
|
|
65
|
+
callBubble.style.top = `${placed.bubble.y}px`;
|
|
66
|
+
callBubble.style.maxHeight = `${placed.bubble.height}px`;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (previousPanel !== next) {
|
|
70
|
+
if (next === 'chat') {
|
|
71
|
+
const mode = $('chat').dataset.replyMode;
|
|
72
|
+
const target = mode === 'reply'
|
|
73
|
+
? ($('reply-close')?.hidden === false ? $('reply-close') : $('reply-retain'))
|
|
74
|
+
: mode === 'sent' ? $('reply-sent') : $('message');
|
|
75
|
+
target?.focus?.({ preventScroll: true });
|
|
76
|
+
}
|
|
77
|
+
if (next === 'transcript') $('call-message').focus({ preventScroll: true });
|
|
78
|
+
if (next === 'settings') $('settings-back').focus({ preventScroll: true });
|
|
79
|
+
if (next === 'decisions') $('decision-reload').focus({ preventScroll: true });
|
|
80
|
+
if (next === 'menu') $('menu').querySelector('button').focus({ preventScroll: true });
|
|
81
|
+
changed(next);
|
|
82
|
+
}
|
|
83
|
+
} catch (cause) {
|
|
84
|
+
if (revision !== layoutRevision) return;
|
|
85
|
+
panel = previousPanel;
|
|
86
|
+
for (const id of panelIds) $(id).hidden = id !== panel;
|
|
87
|
+
if (callBubble) callBubble.hidden = panel !== 'actions' || !hasCallBubble();
|
|
88
|
+
body.setAttribute('aria-expanded', String(!['none', 'bubble'].includes(panel)));
|
|
89
|
+
error(cause, { panel: next });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function remeasure() {
|
|
93
|
+
if (layoutScheduled) return;
|
|
94
|
+
layoutScheduled = true;
|
|
95
|
+
queueMicrotask(() => {
|
|
96
|
+
layoutScheduled = false;
|
|
97
|
+
const node = $(panel);
|
|
98
|
+
if (!node || node.hidden) return;
|
|
99
|
+
const call = callLayout(panel);
|
|
100
|
+
const desired = measure(node);
|
|
101
|
+
const key = sizeKey(desired, call);
|
|
102
|
+
if (desiredSizes.get(node) === key) return;
|
|
103
|
+
void show(panel);
|
|
104
|
+
});
|
|
22
105
|
}
|
|
23
106
|
body.onclick = event => {
|
|
24
107
|
if (suppressClick) { suppressClick = false; return; }
|
|
25
108
|
if (event.detail > 1) return;
|
|
26
|
-
|
|
109
|
+
if (activate('click')) return;
|
|
110
|
+
void show(panel === 'menu' ? 'none' : 'menu');
|
|
27
111
|
};
|
|
28
112
|
body.oncontextmenu = event => { event.preventDefault(); void show('menu'); };
|
|
29
113
|
body.onkeydown = event => {
|
|
@@ -31,37 +115,65 @@ export function bindPetControls(client, { action, error, changed }) {
|
|
|
31
115
|
};
|
|
32
116
|
body.onpointerdown = event => {
|
|
33
117
|
if (event.button !== 0) return;
|
|
34
|
-
|
|
118
|
+
const current = { id: event.pointerId, x: event.screenX, y: event.screenY, armed: false };
|
|
119
|
+
drag = current;
|
|
35
120
|
body.setPointerCapture(event.pointerId);
|
|
36
|
-
void client.drag('start').catch(error);
|
|
121
|
+
void client.drag('start').then(() => { if (drag === current) current.armed = true; }).catch(error);
|
|
37
122
|
};
|
|
38
123
|
body.onpointermove = event => {
|
|
39
|
-
if (drag
|
|
124
|
+
if (!drag) return;
|
|
125
|
+
const dx = event.screenX - drag.x, dy = event.screenY - drag.y;
|
|
126
|
+
if (Math.hypot(dx, dy) < 6) return;
|
|
127
|
+
suppressClick = true;
|
|
128
|
+
if (drag.armed) moved(dx, dy);
|
|
129
|
+
drag.x = event.screenX; drag.y = event.screenY;
|
|
40
130
|
};
|
|
41
131
|
for (const name of ['pointerup', 'pointercancel']) body.addEventListener(name, () => {
|
|
42
|
-
drag = undefined; void client.drag('end').catch(error);
|
|
132
|
+
drag = undefined; moved('stop'); void client.drag('end').catch(error);
|
|
43
133
|
});
|
|
44
134
|
document.addEventListener('pointerdown', event => {
|
|
45
135
|
if (!event.target.closest('#anchor, .floating')) void show('none');
|
|
46
136
|
});
|
|
47
137
|
document.addEventListener('keydown', event => {
|
|
48
|
-
if (event.key === 'Escape' && panel !== 'none') {
|
|
138
|
+
if (event.key === 'Escape' && panel !== 'none') {
|
|
139
|
+
event.preventDefault();
|
|
140
|
+
if (!escape(panel)) void show('none');
|
|
141
|
+
body.focus();
|
|
142
|
+
}
|
|
49
143
|
});
|
|
50
144
|
document.querySelectorAll('[data-action]').forEach(button => button.onclick = () => {
|
|
51
145
|
const kind = button.dataset.action;
|
|
52
|
-
if (kind === 'write' || kind === 'history') { void show('chat'); return; }
|
|
146
|
+
if (kind === 'write' || kind === 'history') { action(kind); void show('chat'); return; }
|
|
147
|
+
if (kind === 'transcript') { void show('transcript'); return; }
|
|
148
|
+
if (kind === 'settings') { void show('settings'); return; }
|
|
149
|
+
if (kind === 'decisions') { void show('decisions'); return; }
|
|
53
150
|
if (kind === 'dismiss') { void show('none'); return; }
|
|
54
151
|
action(kind);
|
|
55
152
|
});
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
153
|
+
const observer = new MutationObserver(records => {
|
|
154
|
+
const current = $(panel);
|
|
155
|
+
if (!current || current.hidden) return;
|
|
156
|
+
// Root visibility is already handled by show(). Descendant visibility and
|
|
157
|
+
// text/row mutations are content demand and may require a larger surface.
|
|
158
|
+
if (records.some(record => record.type !== 'attributes' || !managedIds.includes(record.target.id))) remeasure();
|
|
159
|
+
});
|
|
160
|
+
for (const id of panelIds) observer.observe($(id), {
|
|
161
|
+
subtree: true, childList: true, characterData: true, attributes: true, attributeFilter: ['hidden'],
|
|
162
|
+
});
|
|
163
|
+
if (callBubble) observer.observe(callBubble, {
|
|
164
|
+
subtree: true, childList: true, characterData: true, attributes: true, attributeFilter: ['hidden'],
|
|
63
165
|
});
|
|
64
|
-
for (const id of ['actions', 'menu', 'chat']) observer.observe($(id));
|
|
65
166
|
window.addEventListener('beforeunload', () => observer.disconnect());
|
|
66
|
-
return {
|
|
167
|
+
return {
|
|
168
|
+
get panel() { return panel; }, show, remeasure,
|
|
169
|
+
setAmbient(value) {
|
|
170
|
+
const next = value === true ? 'bubble' : value === false ? 'none' : value;
|
|
171
|
+
const ambientPanels = ['none', 'bubble', 'actions', 'app-assist'];
|
|
172
|
+
if (!ambientPanels.includes(next)) return Promise.resolve();
|
|
173
|
+
if (ambientPanel === next) return Promise.resolve();
|
|
174
|
+
ambientPanel = next;
|
|
175
|
+
return ambientPanels.includes(panel) ? show('none') : Promise.resolve();
|
|
176
|
+
},
|
|
177
|
+
dismiss() { if (drag) suppressClick = true; void show('none'); },
|
|
178
|
+
};
|
|
67
179
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// F229 pet.json frame durations, plus the three F258 rows pinned in source-lock.json.
|
|
2
|
+
export const atlas = {
|
|
3
|
+
idle: { row: 0, frames: [1200, 400, 400, 500, 500, 800] },
|
|
4
|
+
'running-right': { row: 1, frames: [120, 120, 120, 120, 120, 120, 120, 220] },
|
|
5
|
+
'running-left': { row: 2, frames: [120, 120, 120, 120, 120, 120, 120, 220] },
|
|
6
|
+
waving: { row: 3, frames: [140, 140, 140, 280] },
|
|
7
|
+
jumping: { row: 4, frames: [140, 140, 140, 140, 280] },
|
|
8
|
+
failed: { row: 5, frames: [140, 140, 140, 140, 140, 140, 140, 240] },
|
|
9
|
+
waiting: { row: 6, frames: [150, 150, 150, 150, 150, 260] },
|
|
10
|
+
running: { row: 7, frames: [120, 120, 120, 120, 120, 220] },
|
|
11
|
+
review: { row: 8, frames: [150, 150, 150, 150, 150, 280] },
|
|
12
|
+
};
|
|
13
|
+
export const additional = {
|
|
14
|
+
sleeping: { file: 'xianxian-sleeping-row.png', frames: [700, 700, 700, 700, 700, 700] },
|
|
15
|
+
working: { file: 'xianxian-working-row.png', frames: [520, 560, 520, 640, 520, 560, 520, 640] },
|
|
16
|
+
staged_thought: { file: 'xianxian-staged-thought-row.png', frames: [1200, 500, 700, 900, 1200, 500] },
|
|
17
|
+
};
|
|
18
|
+
export const visualAlias = { fetching: 'working', studying: 'staged_thought', delivering: 'review', held: 'idle' };
|
|
19
|
+
export const workAction = { reasoning: 'staged_thought', tool: 'working', workspace: 'fetching', screen_reading: 'studying' };
|
|
20
|
+
export const signals = { connected: 'waving', received: 'jumping', failed: 'failed' };
|
|
21
|
+
export const staticSkins = { 'ragdoll-v1': 'ragdoll-v1.png', 'yarn-ball': 'yarn-ball.png' };
|
|
22
|
+
export const validId = value => typeof value === 'string' && value.length > 0 && value.length <= 256;
|