@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,240 @@
|
|
|
1
|
+
const MAX_LIVE_ROWS = 8;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Projection of the Host-bound transcript for one trusted call.
|
|
5
|
+
*
|
|
6
|
+
* Durable rows are keyed only by Host messageId. Ephemeral audio is keyed by
|
|
7
|
+
* provider item/turn identity and is discarded when the Host projects the
|
|
8
|
+
* matching durable source. Text equality is deliberately never used.
|
|
9
|
+
*/
|
|
10
|
+
export class CallTranscriptView {
|
|
11
|
+
constructor(container, { onUnread = () => {} } = {}) {
|
|
12
|
+
this.container = container;
|
|
13
|
+
this.onUnread = onUnread;
|
|
14
|
+
this.durableRows = new Map();
|
|
15
|
+
this.liveRows = new Map();
|
|
16
|
+
this.liveSequence = 0;
|
|
17
|
+
this.optimisticIds = new Set();
|
|
18
|
+
this.unread = 0;
|
|
19
|
+
this.initialized = false;
|
|
20
|
+
container.addEventListener?.('scroll', () => {
|
|
21
|
+
if (this.nearBottom()) this.setUnread(0);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
setCall(callId) {
|
|
26
|
+
if (this.callId === callId) return;
|
|
27
|
+
this.callId = callId ?? null;
|
|
28
|
+
this.scope = undefined;
|
|
29
|
+
this.durableRows.clear();
|
|
30
|
+
this.liveRows.clear();
|
|
31
|
+
this.liveSequence = 0;
|
|
32
|
+
this.optimisticIds.clear();
|
|
33
|
+
this.initialized = false;
|
|
34
|
+
this.container.replaceChildren();
|
|
35
|
+
this.container.dataset.callId = this.callId ?? '';
|
|
36
|
+
this.container.dataset.hasMore = 'false';
|
|
37
|
+
this.setUnread(0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
reset() { this.setCall(null); }
|
|
41
|
+
|
|
42
|
+
nearBottom() {
|
|
43
|
+
return this.container.scrollHeight - this.container.scrollTop - this.container.clientHeight < 32;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
scrollToBottom() {
|
|
47
|
+
this.container.scrollTop = this.container.scrollHeight;
|
|
48
|
+
this.setUnread(0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
setUnread(count) {
|
|
52
|
+
if (this.unread === count) return;
|
|
53
|
+
this.unread = count;
|
|
54
|
+
this.onUnread(count);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
visibleAnchor() {
|
|
58
|
+
const viewport = this.container.getBoundingClientRect?.();
|
|
59
|
+
if (!viewport) return null;
|
|
60
|
+
for (const [id, row] of this.durableRows) {
|
|
61
|
+
const rect = row.getBoundingClientRect?.();
|
|
62
|
+
if (rect && rect.bottom > viewport.top && rect.top < viewport.bottom) {
|
|
63
|
+
return { id, offset: rect.top - viewport.top };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
createRow() {
|
|
70
|
+
return this.container.ownerDocument.createElement('p');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
updateDurable(row, value) {
|
|
74
|
+
row.className = `call-transcript-row ${value.source.kind} ${value.role}`;
|
|
75
|
+
row.textContent = value.text;
|
|
76
|
+
row.dataset.messageId = value.messageId;
|
|
77
|
+
row.dataset.source = value.source.kind;
|
|
78
|
+
row.dataset.role = value.role;
|
|
79
|
+
delete row.dataset.live;
|
|
80
|
+
delete row.dataset.itemId;
|
|
81
|
+
delete row.dataset.turnId;
|
|
82
|
+
delete row.dataset.optimistic;
|
|
83
|
+
if (value.source.kind === 'voice') {
|
|
84
|
+
row.dataset.itemId = value.source.nativeItemId;
|
|
85
|
+
if (value.source.nativeTurnId) row.dataset.turnId = value.source.nativeTurnId;
|
|
86
|
+
delete row.dataset.clientMessageId;
|
|
87
|
+
} else {
|
|
88
|
+
row.dataset.clientMessageId = value.source.clientMessageId;
|
|
89
|
+
}
|
|
90
|
+
row.setAttribute?.('aria-label', value.source.kind === 'typed'
|
|
91
|
+
? `你打字说:${value.text}`
|
|
92
|
+
: value.role === 'assistant' ? `猫猫:${value.text}` : `语音转写:${value.text}`);
|
|
93
|
+
return row;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
removeReconciledLive(source) {
|
|
97
|
+
if (source.kind !== 'voice') return false;
|
|
98
|
+
let removed = false;
|
|
99
|
+
for (const [key, row] of this.liveRows) {
|
|
100
|
+
if ((row.dataset.itemId && row.dataset.itemId === source.nativeItemId)
|
|
101
|
+
|| (source.nativeTurnId && row.dataset.turnId === source.nativeTurnId)) {
|
|
102
|
+
this.liveRows.delete(key);
|
|
103
|
+
removed = true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return removed;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
render() {
|
|
110
|
+
this.container.replaceChildren(...this.durableRows.values(), ...this.liveRows.values());
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
load(reply) {
|
|
114
|
+
if (!this.callId || reply?.kind !== 'transcript' || reply.scope?.callId !== this.callId) return false;
|
|
115
|
+
const pinned = this.nearBottom();
|
|
116
|
+
const readingPosition = this.container.scrollTop;
|
|
117
|
+
const anchor = pinned ? null : this.visibleAnchor();
|
|
118
|
+
const previous = this.durableRows;
|
|
119
|
+
const previousIds = new Set(previous.keys());
|
|
120
|
+
const next = new Map();
|
|
121
|
+
let added = 0;
|
|
122
|
+
|
|
123
|
+
for (const value of reply.rows) {
|
|
124
|
+
const reconciledLive = this.removeReconciledLive(value.source);
|
|
125
|
+
const row = previous.get(value.messageId) ?? this.createRow();
|
|
126
|
+
this.updateDurable(row, value);
|
|
127
|
+
this.optimisticIds.delete(value.messageId);
|
|
128
|
+
next.set(value.messageId, row);
|
|
129
|
+
if (this.initialized && !previousIds.has(value.messageId) && !reconciledLive) added += 1;
|
|
130
|
+
}
|
|
131
|
+
// An accepted delivery already has a durable Host messageId. A transcript
|
|
132
|
+
// read can race the MessageStore projection, so keep that exact row until
|
|
133
|
+
// the same id appears; never fall back to matching its text.
|
|
134
|
+
for (const id of this.optimisticIds) {
|
|
135
|
+
const row = previous.get(id);
|
|
136
|
+
if (row && !next.has(id)) next.set(id, row);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this.scope = reply.scope;
|
|
140
|
+
this.durableRows = next;
|
|
141
|
+
this.container.dataset.callId = reply.scope.callId;
|
|
142
|
+
this.container.dataset.hasMore = String(reply.hasMore);
|
|
143
|
+
this.render();
|
|
144
|
+
|
|
145
|
+
if (pinned) {
|
|
146
|
+
this.container.scrollTop = this.container.scrollHeight;
|
|
147
|
+
this.setUnread(0);
|
|
148
|
+
} else if (anchor) {
|
|
149
|
+
const row = this.durableRows.get(anchor.id) ?? this.durableRows.values().next().value;
|
|
150
|
+
const rect = row?.getBoundingClientRect?.();
|
|
151
|
+
const viewport = this.container.getBoundingClientRect?.();
|
|
152
|
+
this.container.scrollTop = rect && viewport
|
|
153
|
+
? this.container.scrollTop + rect.top - viewport.top - anchor.offset
|
|
154
|
+
: readingPosition;
|
|
155
|
+
if (added) this.setUnread(this.unread + added);
|
|
156
|
+
} else {
|
|
157
|
+
this.container.scrollTop = readingPosition;
|
|
158
|
+
if (added) this.setUnread(this.unread + added);
|
|
159
|
+
}
|
|
160
|
+
this.initialized = true;
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
liveKey(event) {
|
|
165
|
+
if (event.itemId) return `item:${event.itemId}`;
|
|
166
|
+
if (event.turnId) return `turn:${event.turnId}`;
|
|
167
|
+
return `ephemeral:${++this.liveSequence}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
appendLive(event) {
|
|
171
|
+
if (!this.callId || event?.type !== 'transcript' || event.callId !== this.callId) return false;
|
|
172
|
+
const pinned = this.nearBottom();
|
|
173
|
+
const key = this.liveKey(event);
|
|
174
|
+
let row = this.liveRows.get(key);
|
|
175
|
+
const isNew = !row;
|
|
176
|
+
if (!row) {
|
|
177
|
+
row = this.createRow();
|
|
178
|
+
row.className = `call-transcript-row voice ${event.role}`;
|
|
179
|
+
row.dataset.live = 'true';
|
|
180
|
+
row.dataset.source = 'voice';
|
|
181
|
+
row.dataset.role = event.role;
|
|
182
|
+
if (event.itemId) row.dataset.itemId = event.itemId;
|
|
183
|
+
if (event.turnId) row.dataset.turnId = event.turnId;
|
|
184
|
+
this.liveRows.set(key, row);
|
|
185
|
+
while (this.liveRows.size > MAX_LIVE_ROWS) this.liveRows.delete(this.liveRows.keys().next().value);
|
|
186
|
+
}
|
|
187
|
+
row.textContent = `${row.textContent}${event.text}`.slice(-16000);
|
|
188
|
+
row.setAttribute?.('aria-label', `${event.role === 'assistant' ? '猫猫' : '语音转写'},实时:${row.textContent}`);
|
|
189
|
+
this.render();
|
|
190
|
+
if (pinned) {
|
|
191
|
+
this.container.scrollTop = this.container.scrollHeight;
|
|
192
|
+
this.setUnread(0);
|
|
193
|
+
} else if (isNew) this.setUnread(this.unread + 1);
|
|
194
|
+
return row;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
rowForMessage(messageId) { return this.durableRows.get(messageId); }
|
|
198
|
+
|
|
199
|
+
appendTyped({ receipt, text }) {
|
|
200
|
+
if (!this.callId || receipt?.delivery !== 'accepted' || receipt.callId !== this.callId) return false;
|
|
201
|
+
const pinned = this.nearBottom();
|
|
202
|
+
let row = this.durableRows.get(receipt.messageId);
|
|
203
|
+
const isNew = !row;
|
|
204
|
+
if (!row) {
|
|
205
|
+
row = this.createRow();
|
|
206
|
+
this.durableRows.set(receipt.messageId, row);
|
|
207
|
+
}
|
|
208
|
+
this.updateDurable(row, {
|
|
209
|
+
messageId: receipt.messageId,
|
|
210
|
+
role: 'user',
|
|
211
|
+
text,
|
|
212
|
+
source: { kind: 'typed', clientMessageId: receipt.clientMessageId, callId: receipt.callId },
|
|
213
|
+
});
|
|
214
|
+
row.dataset.optimistic = 'true';
|
|
215
|
+
this.optimisticIds.add(receipt.messageId);
|
|
216
|
+
while (this.optimisticIds.size > MAX_LIVE_ROWS) {
|
|
217
|
+
const oldest = this.optimisticIds.values().next().value;
|
|
218
|
+
this.optimisticIds.delete(oldest);
|
|
219
|
+
if (this.durableRows.get(oldest)?.dataset.optimistic === 'true') this.durableRows.delete(oldest);
|
|
220
|
+
}
|
|
221
|
+
this.render();
|
|
222
|
+
if (pinned) {
|
|
223
|
+
this.container.scrollTop = this.container.scrollHeight;
|
|
224
|
+
this.setUnread(0);
|
|
225
|
+
} else if (isNew) this.setUnread(this.unread + 1);
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
finish(event) {
|
|
230
|
+
if (!this.callId || event?.type !== 'turn-done' || event.callId !== this.callId) return false;
|
|
231
|
+
for (const row of this.liveRows.values()) {
|
|
232
|
+
const matchesTurn = event.turnId && row.dataset.turnId === event.turnId;
|
|
233
|
+
const matchesUnkeyedRole = !event.turnId && event.role && !row.dataset.itemId
|
|
234
|
+
&& !row.dataset.turnId && row.dataset.role === event.role;
|
|
235
|
+
if (matchesTurn || matchesUnkeyedRole) row.dataset.complete = 'true';
|
|
236
|
+
}
|
|
237
|
+
this.render();
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Counts new assistant call-message identities only after the owner folds an opened chat. */
|
|
2
|
+
export class FoldedChatUnread {
|
|
3
|
+
constructor(changed = () => {}) {
|
|
4
|
+
this.changed = changed;
|
|
5
|
+
this.seen = new Set();
|
|
6
|
+
this.count = 0;
|
|
7
|
+
this.armed = false;
|
|
8
|
+
this.folded = false;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
publish(count) {
|
|
12
|
+
if (this.count === count) return;
|
|
13
|
+
this.count = count;
|
|
14
|
+
this.changed(count);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
open() {
|
|
18
|
+
this.armed = true;
|
|
19
|
+
this.folded = false;
|
|
20
|
+
this.publish(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
fold() {
|
|
24
|
+
if (this.armed) this.folded = true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
receive(message) {
|
|
28
|
+
const { messageId, role } = message ?? {};
|
|
29
|
+
if (!messageId || this.seen.has(messageId)) return;
|
|
30
|
+
this.seen.add(messageId);
|
|
31
|
+
if (role === 'assistant' && this.armed && this.folded) this.publish(this.count + 1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
receiveMany(messages) {
|
|
35
|
+
for (const message of messages) this.receive(message);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
reset() {
|
|
39
|
+
this.seen.clear();
|
|
40
|
+
this.armed = false;
|
|
41
|
+
this.folded = false;
|
|
42
|
+
this.publish(0);
|
|
43
|
+
}
|
|
44
|
+
}
|
package/renderer/client.mjs
CHANGED
|
@@ -16,16 +16,62 @@ export function createCompanionClient(bridge) {
|
|
|
16
16
|
throw new CompanionRequestError('unavailable');
|
|
17
17
|
return reply;
|
|
18
18
|
}
|
|
19
|
+
async function readConversation() {
|
|
20
|
+
const response = await invoke({ kind: 'conversation.read' }, 'conversation');
|
|
21
|
+
if ('v' in response)
|
|
22
|
+
throw new CompanionRequestError('unavailable');
|
|
23
|
+
return response;
|
|
24
|
+
}
|
|
25
|
+
async function readConversationPage(options = {}) {
|
|
26
|
+
const command = options.before === undefined
|
|
27
|
+
? options.limit === undefined
|
|
28
|
+
? { kind: 'conversation.read', v: 2 }
|
|
29
|
+
: { kind: 'conversation.read', v: 2, limit: options.limit }
|
|
30
|
+
: options.limit === undefined
|
|
31
|
+
? { kind: 'conversation.read', v: 2, before: options.before }
|
|
32
|
+
: { kind: 'conversation.read', v: 2, before: options.before, limit: options.limit };
|
|
33
|
+
const response = await invoke(command, 'conversation');
|
|
34
|
+
if (!('v' in response) || response.v !== 2)
|
|
35
|
+
throw new CompanionRequestError('unavailable');
|
|
36
|
+
return response;
|
|
37
|
+
}
|
|
38
|
+
async function readConversationBody(continuation) {
|
|
39
|
+
return invoke({ kind: 'conversation.body.read', v: 2, continuation }, 'conversation-body');
|
|
40
|
+
}
|
|
41
|
+
async function readConversationDraft() {
|
|
42
|
+
return invoke({ kind: 'conversation.draft.read', v: 1 }, 'conversation-draft');
|
|
43
|
+
}
|
|
44
|
+
async function saveConversationDraft(expectedRevision, text, scopeRef) {
|
|
45
|
+
return invoke({
|
|
46
|
+
kind: 'conversation.draft.save', v: 1, scopeRef, expectedRevision, text,
|
|
47
|
+
}, 'conversation-draft');
|
|
48
|
+
}
|
|
49
|
+
async function invokeDeliveryV2(command) {
|
|
50
|
+
const response = await invoke(command, 'delivery');
|
|
51
|
+
if (!('v' in response) || response.v !== 2)
|
|
52
|
+
throw new CompanionRequestError('unavailable');
|
|
53
|
+
return response;
|
|
54
|
+
}
|
|
55
|
+
async function submitConversationText(text, clientMessageId, submissionRevision, scopeRef) {
|
|
56
|
+
return invokeDeliveryV2({
|
|
57
|
+
kind: 'text', v: 2, scopeRef, text, clientMessageId, submissionRevision,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
async function readConversationDelivery(clientMessageId, scopeRef) {
|
|
61
|
+
return invokeDeliveryV2({ kind: 'conversation.delivery.read', v: 1, scopeRef, clientMessageId });
|
|
62
|
+
}
|
|
19
63
|
return {
|
|
20
64
|
state: () => invoke({ kind: 'state' }, 'state'),
|
|
21
65
|
prepare: () => invoke({ kind: 'prepare' }, 'state'),
|
|
22
|
-
connectAudio: () => invoke({ kind: 'audio.connect' }, 'ok'),
|
|
66
|
+
connectAudio: (mode) => invoke(mode === undefined ? { kind: 'audio.connect' } : { kind: 'audio.connect', mode }, 'ok'),
|
|
23
67
|
closeAudio: () => invoke({ kind: 'audio.close' }, 'ok'),
|
|
24
68
|
muteMicrophone: (muted) => invoke({ kind: 'audio.microphone', muted }, 'ok'),
|
|
25
69
|
muteSpeaker: (muted) => invoke({ kind: 'audio.speaker', muted }, 'ok'),
|
|
26
70
|
stop: () => invoke({ kind: 'stop' }, 'ok'),
|
|
27
71
|
text: async (text, clientMessageId) => {
|
|
28
72
|
const receipt = await invoke({ kind: 'text', text, clientMessageId }, 'delivery');
|
|
73
|
+
if ('v' in receipt)
|
|
74
|
+
throw new CompanionRequestError('unavailable');
|
|
29
75
|
if (receipt.delivery !== 'accepted')
|
|
30
76
|
throw new CompanionRequestError('unconfirmed');
|
|
31
77
|
return receipt;
|
|
@@ -36,11 +82,37 @@ export function createCompanionClient(bridge) {
|
|
|
36
82
|
screenFrame: (selectionId, frame) => invoke({ kind: 'screen.frame', selectionId, frame }, 'ok'),
|
|
37
83
|
screenClose: () => invoke({ kind: 'screen.close' }, 'ok'),
|
|
38
84
|
openConversation: () => invoke({ kind: 'conversation.open' }, 'navigation'),
|
|
85
|
+
observePresentation: (observation) => invoke({
|
|
86
|
+
kind: 'presentation.observe',
|
|
87
|
+
event: observation.event,
|
|
88
|
+
observedAt: observation.observedAt,
|
|
89
|
+
callId: observation.callId,
|
|
90
|
+
reference: observation.reference,
|
|
91
|
+
}, 'ok'),
|
|
92
|
+
readWork: () => invoke({ kind: 'work.read' }, 'work'),
|
|
93
|
+
openWork: (entryRef) => invoke({ kind: 'work.open', entryRef }, 'navigation'),
|
|
94
|
+
readDecisions: (offset = 0, limit = 20) => invoke({ kind: 'decisions.read', offset, limit }, 'decisions'),
|
|
95
|
+
openDecision: (variantRef, target) => invoke({ kind: 'decision.open', variantRef, target }, 'navigation'),
|
|
96
|
+
inspectF221: (proposalId) => invoke({ kind: 'f221.inspect', proposalId }, 'decision-trial'),
|
|
39
97
|
resize: (expanded) => invoke({ kind: 'view.resize', expanded }, 'ok'),
|
|
40
|
-
layout: (panel, width, height) => invoke(
|
|
98
|
+
layout: (panel, width, height, call) => invoke(call === undefined
|
|
99
|
+
? { kind: 'view.layout', panel, width, height }
|
|
100
|
+
: { kind: 'view.layout', panel, width, height, call }, 'layout'),
|
|
41
101
|
drag: (phase) => invoke({ kind: 'view.drag', phase }, 'ok'),
|
|
42
102
|
hide: () => invoke({ kind: 'view.hide' }, 'ok'),
|
|
43
|
-
|
|
103
|
+
resetPosition: () => invoke({ kind: 'view.reset' }, 'ok'),
|
|
104
|
+
readSettings: () => invoke({ kind: 'settings.read' }, 'settings'),
|
|
105
|
+
updateSetting: (field, value) => invoke({ kind: 'settings.update', field, value }, 'settings-update'),
|
|
106
|
+
settleAppAssist: (scopeRef, offerRef, action) => invoke({ kind: 'app-assist.settle', scopeRef, offerRef, action }, 'app-assist-settlement'),
|
|
107
|
+
disableCompanion: () => invoke({ kind: 'companion.disable' }, 'companion-lifecycle'),
|
|
108
|
+
readConversation,
|
|
109
|
+
readConversationPage,
|
|
110
|
+
readConversationBody,
|
|
111
|
+
readConversationDraft,
|
|
112
|
+
saveConversationDraft,
|
|
113
|
+
submitConversationText,
|
|
114
|
+
readConversationDelivery,
|
|
115
|
+
readTranscript: () => invoke({ kind: 'transcript.read' }, 'transcript'),
|
|
44
116
|
subscribe: (listener) => bridge.subscribe(listener),
|
|
45
117
|
};
|
|
46
118
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const COMPACT_BALL_SIZE = 43;
|
|
2
|
+
export const DEFAULT_BALL_SIZE = 72;
|
|
3
|
+
|
|
4
|
+
export function compactSizeAction(ballSize) {
|
|
5
|
+
return Number(ballSize) <= COMPACT_BALL_SIZE
|
|
6
|
+
? { label: '放大', value: DEFAULT_BALL_SIZE }
|
|
7
|
+
: { label: '缩小', value: COMPACT_BALL_SIZE };
|
|
8
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const named = value => typeof value === 'string' && value.trim().length > 0;
|
|
2
|
+
const actor = value => value && named(value.catId) && named(value.displayName);
|
|
3
|
+
const model = value => value === null || (named(value) && value.length <= 160);
|
|
4
|
+
const snapshotActor = value => actor(value) && value.catId.length <= 160 && value.displayName.length <= 160;
|
|
5
|
+
|
|
6
|
+
function validSnapshot(value) {
|
|
7
|
+
return value?.v === 1 && value.name === '猫猫球'
|
|
8
|
+
&& snapshotActor(value.partner) && named(value.partner.skin) && value.partner.skin.length <= 160
|
|
9
|
+
&& snapshotActor(value.live) && value.live.transport === 'gpt_live_v3' && model(value.live.verifiedModel)
|
|
10
|
+
&& snapshotActor(value.deep) && value.deep.catId === value.partner.catId && model(value.deep.verifiedModel);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function display(snapshot, partnerLabel) {
|
|
14
|
+
return {
|
|
15
|
+
title: snapshot.name,
|
|
16
|
+
companionKnown: true,
|
|
17
|
+
partnerLabel,
|
|
18
|
+
avatarCatId: snapshot.partner.catId,
|
|
19
|
+
skin: snapshot.partner.skin,
|
|
20
|
+
liveLabel: `Live 快端:${snapshot.live.displayName} · ${snapshot.live.verifiedModel ?? '型号未核实'}`,
|
|
21
|
+
deepLabel: `深思端:${snapshot.deep.displayName} · ${snapshot.deep.verifiedModel ?? '型号未核实'}`,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The current face comes from the Host's selected duty, never the background carrier. */
|
|
26
|
+
export function currentCompanionIdentity(state) {
|
|
27
|
+
if (!actor(state?.duty) || !actor(state?.carrier) || !named(state.skin)
|
|
28
|
+
|| state.liveTransport?.kind !== 'gpt_live_v3' || !model(state.liveTransport.verifiedModel)) return null;
|
|
29
|
+
const snapshot = {
|
|
30
|
+
v: 1, name: '猫猫球',
|
|
31
|
+
partner: { ...state.duty, skin: state.skin },
|
|
32
|
+
live: { ...state.carrier, transport: state.liveTransport.kind, verifiedModel: state.liveTransport.verifiedModel },
|
|
33
|
+
deep: { ...state.duty, verifiedModel: null },
|
|
34
|
+
};
|
|
35
|
+
return display(snapshot, `${snapshot.partner.displayName}陪伴中`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A missing old snapshot never borrows today's preference. Keep the message's real author visible. */
|
|
39
|
+
export function historicalCompanionIdentity(snapshot, authorLabel) {
|
|
40
|
+
if (!validSnapshot(snapshot)) return { authorLabel, companionKnown: false };
|
|
41
|
+
return { ...display(snapshot, `当时由${snapshot.partner.displayName}陪伴`), authorLabel };
|
|
42
|
+
}
|