@linxin666/dsh-pet 0.1.14 → 0.1.15
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/lib/client.js +29 -16
- package/lib/client.js.map +1 -1
- package/lib/index.js +355 -251
- package/lib/invariant.js +1 -1
- package/lib/{state-P-wsVJ6O.js → state-CFyJv0sQ.js} +15 -1
- package/lib/types/affinity.d.ts +15 -0
- package/lib/types/affinity.d.ts.map +1 -1
- package/lib/types/affinity.js +14 -0
- package/lib/types/client/PluginSettingsCard.d.ts +26 -11
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +27 -7
- package/lib/types/client/index.d.ts +1 -1
- package/lib/types/client/index.js +3 -3
- package/lib/types/client/settings-form.d.ts +14 -5
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +38 -10
- package/lib/types/dsh-home.d.ts +17 -0
- package/lib/types/dsh-home.d.ts.map +1 -0
- package/lib/types/dsh-home.js +34 -0
- package/lib/types/event-projection.d.ts +36 -0
- package/lib/types/event-projection.d.ts.map +1 -0
- package/lib/types/event-projection.js +98 -0
- package/lib/types/ledger.d.ts +77 -0
- package/lib/types/ledger.d.ts.map +1 -0
- package/lib/types/ledger.js +139 -0
- package/lib/types/persist.d.ts +5 -1
- package/lib/types/persist.d.ts.map +1 -1
- package/lib/types/persist.js +7 -3
- package/lib/types/service.d.ts +15 -46
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +47 -204
- package/package.json +1 -1
- package/src/affinity.ts +33 -0
- package/src/client/PluginSettingsCard.tsx +83 -17
- package/src/client/index.ts +3 -3
- package/src/client/settings-card.module.css +6 -0
- package/src/client/settings-form.ts +41 -9
- package/src/dsh-home.test.ts +35 -0
- package/src/dsh-home.ts +36 -0
- package/src/event-projection.ts +128 -0
- package/src/ledger.test.ts +67 -0
- package/src/ledger.ts +183 -0
- package/src/persist.ts +7 -3
- package/src/service.ts +60 -274
package/lib/types/service.js
CHANGED
|
@@ -1,136 +1,44 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pet host service — the `pet.*` RPC domain.
|
|
3
|
-
*
|
|
4
|
-
* affinity
|
|
5
|
-
*
|
|
6
|
-
* `pet.setVisible` / `pet.setConfig` for
|
|
2
|
+
* Pet host service — the `pet.*` RPC domain. A composition facade: it wires
|
|
3
|
+
* the pure event projection (`event-projection`) onto the state machine,
|
|
4
|
+
* delegates the affinity economy to the ledger (`ledger`), and routes
|
|
5
|
+
* persistence through `persist`. The API gateway maps these methods onto
|
|
6
|
+
* `pet.state` / `pet.interact` / `pet.setVisible` / `pet.setConfig` for
|
|
7
|
+
* browser consumers.
|
|
7
8
|
* @module @linxin666/dsh-pet/service
|
|
8
9
|
*/
|
|
9
10
|
import { Service } from '@deepseek-ai/cordis';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
11
|
+
import { emptyProjectionRuntime, isActivityPhase, projectOfficialEvent, } from "./event-projection.js";
|
|
12
|
+
import { PetLedger } from "./ledger.js";
|
|
13
|
+
import { DISPLAY_INSET_MAX, DISPLAY_SIZE_MAX, DISPLAY_SIZE_MIN, PET_NAME_MAX_LENGTH, loadPetPersist, petHomeDir, savePetPersist, } from "./persist.js";
|
|
13
14
|
import { defaultPetStateConfig, PetStateMachine, } from "./state.js";
|
|
14
15
|
/** Settings namespace of the pet capability. Spelled here rather than imported: the browser half spells the same value. */
|
|
15
16
|
export const PET_SETTINGS_NAMESPACE = 'pet';
|
|
16
|
-
/** Keep tool names readable inside the compact status bubble. */
|
|
17
|
-
function displayToolName(name) {
|
|
18
|
-
const compact = name.replace(/\s+/g, ' ').trim() || '工具';
|
|
19
|
-
return compact.length <= 24 ? compact : `${compact.slice(0, 21)}...`;
|
|
20
|
-
}
|
|
21
|
-
/** Whether a legacy phase is part of the pet's supported vocabulary. */
|
|
22
|
-
function isActivityPhase(phase) {
|
|
23
|
-
return ['idle', 'waiting', 'thinking', 'tool', 'review', 'done', 'failed'].includes(phase);
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Project the durable DSH session vocabulary into the pet's visual phases.
|
|
27
|
-
* Unknown and log-only events do not disturb the last meaningful activity.
|
|
28
|
-
*/
|
|
29
|
-
function projectOfficialEvent(event, runtime) {
|
|
30
|
-
switch (event.type) {
|
|
31
|
-
case 'turn/start':
|
|
32
|
-
runtime.activeTools.clear();
|
|
33
|
-
runtime.stepHadFailure = false;
|
|
34
|
-
return { input: { phase: 'waiting', line: '准备开始' } };
|
|
35
|
-
case 'step/start':
|
|
36
|
-
runtime.activeTools.clear();
|
|
37
|
-
runtime.stepHadFailure = false;
|
|
38
|
-
return { input: { phase: 'waiting', line: '等待模型响应' } };
|
|
39
|
-
case 'assistant/chunk': {
|
|
40
|
-
const { chunk } = event.data;
|
|
41
|
-
if (chunk.type === 'reasoning-delta' && chunk.text.length > 0) {
|
|
42
|
-
return { input: { phase: 'thinking', line: '正在思考' } };
|
|
43
|
-
}
|
|
44
|
-
if (chunk.type === 'text-delta' && chunk.text.length > 0) {
|
|
45
|
-
return { input: { phase: 'review', line: '整理回复中' } };
|
|
46
|
-
}
|
|
47
|
-
return undefined;
|
|
48
|
-
}
|
|
49
|
-
case 'assistant/message':
|
|
50
|
-
return { input: { phase: 'review', line: '整理回复中' } };
|
|
51
|
-
case 'tool/call':
|
|
52
|
-
runtime.activeTools.add(String(event.data.callId));
|
|
53
|
-
return {
|
|
54
|
-
input: {
|
|
55
|
-
phase: 'tool',
|
|
56
|
-
line: `正在使用 ${displayToolName(event.data.name)}`,
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
case 'tool/result': {
|
|
60
|
-
const block = event.data.message.content[0];
|
|
61
|
-
runtime.activeTools.delete(String(event.data.message.source.callId));
|
|
62
|
-
runtime.stepHadFailure ||= event.data.error !== undefined || block.isError === true;
|
|
63
|
-
if (runtime.activeTools.size > 0) {
|
|
64
|
-
return {
|
|
65
|
-
input: {
|
|
66
|
-
phase: 'tool',
|
|
67
|
-
line: `还有 ${runtime.activeTools.size} 个工具运行中`,
|
|
68
|
-
},
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
return runtime.stepHadFailure
|
|
72
|
-
? { input: { phase: 'failed', line: '工具执行失败' } }
|
|
73
|
-
: { input: { phase: 'thinking', line: '处理工具结果' } };
|
|
74
|
-
}
|
|
75
|
-
case 'turn/end': {
|
|
76
|
-
runtime.activeTools.clear();
|
|
77
|
-
switch (event.data.reason.kind) {
|
|
78
|
-
case 'completed':
|
|
79
|
-
return {
|
|
80
|
-
input: { phase: 'done', line: '完成啦' },
|
|
81
|
-
completedTurn: event.data.turn,
|
|
82
|
-
};
|
|
83
|
-
case 'error':
|
|
84
|
-
return { input: { phase: 'failed', line: '执行失败' } };
|
|
85
|
-
case 'max-tokens':
|
|
86
|
-
return { input: { phase: 'failed', line: '达到输出上限' } };
|
|
87
|
-
case 'interrupted':
|
|
88
|
-
return { input: { phase: 'failed', line: '执行意外中断' } };
|
|
89
|
-
case 'blocked':
|
|
90
|
-
return { input: { phase: 'waiting', line: '等待继续' } };
|
|
91
|
-
case 'aborted':
|
|
92
|
-
return { input: { phase: 'idle', line: '已停止' } };
|
|
93
|
-
default:
|
|
94
|
-
// TurnEndReasonMap is merge-extensible; a newer ending must not
|
|
95
|
-
// leave the pet showing stale in-progress work.
|
|
96
|
-
return { input: { phase: 'idle' } };
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
default:
|
|
100
|
-
return undefined;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
17
|
/**
|
|
104
18
|
* Cordis service exposing the pet RPC domain. Lazy: nothing is scanned or
|
|
105
|
-
* written until
|
|
106
|
-
* in-memory state, and persistence happens on
|
|
107
|
-
*
|
|
19
|
+
* written until an economic event or interaction arrives; event listeners
|
|
20
|
+
* update only in-memory state, and persistence happens on economic changes
|
|
21
|
+
* (turn rewards, feeds, config/name changes) — never on a read.
|
|
108
22
|
*/
|
|
109
23
|
export class PetService extends Service {
|
|
110
24
|
static inject = [];
|
|
111
25
|
machine;
|
|
112
|
-
|
|
113
|
-
treatConfig;
|
|
26
|
+
ledger;
|
|
114
27
|
persistDir;
|
|
115
|
-
persist;
|
|
116
|
-
/** Completed turns already rewarded, per session (turn numbers are per-session). */
|
|
117
|
-
rewardedTurns = new Map();
|
|
118
28
|
enabled;
|
|
119
29
|
disposeActivity;
|
|
120
30
|
/** Session whose most recent meaningful event currently drives the global pet. */
|
|
121
31
|
displaySession;
|
|
122
32
|
sessionActivity = new WeakMap();
|
|
123
|
-
lastLegacyTurnRewardAt = 0;
|
|
124
33
|
constructor(ctx, config = {}) {
|
|
125
34
|
super(ctx, 'pet');
|
|
126
35
|
this.persistDir = config.persistDir ?? petHomeDir();
|
|
127
|
-
|
|
128
|
-
this.
|
|
36
|
+
const ledgerConfig = { affinity: config.affinity, treats: config.treats };
|
|
37
|
+
this.ledger = new PetLedger(loadPetPersist(this.persistDir), ledgerConfig);
|
|
129
38
|
this.machine = new PetStateMachine({
|
|
130
39
|
...defaultPetStateConfig,
|
|
131
40
|
...(config.state ?? {}),
|
|
132
41
|
});
|
|
133
|
-
this.persist = loadPetPersist(this.persistDir);
|
|
134
42
|
this.enabled = config.enabled ?? true;
|
|
135
43
|
this.syncActivity();
|
|
136
44
|
}
|
|
@@ -144,11 +52,11 @@ export class PetService extends Service {
|
|
|
144
52
|
}
|
|
145
53
|
/** Current persisted display config (read-only view). */
|
|
146
54
|
display() {
|
|
147
|
-
return { ...this.
|
|
55
|
+
return { ...this.ledger.snapshot.display };
|
|
148
56
|
}
|
|
149
57
|
/** Current persisted pet name (read-only view). */
|
|
150
58
|
petName() {
|
|
151
|
-
return this.
|
|
59
|
+
return this.ledger.snapshot.name;
|
|
152
60
|
}
|
|
153
61
|
/** Start or stop the session-activity listeners that drive the pet. */
|
|
154
62
|
setEnabled(enabled) {
|
|
@@ -210,11 +118,7 @@ export class PetService extends Service {
|
|
|
210
118
|
activityRuntime(session) {
|
|
211
119
|
let runtime = this.sessionActivity.get(session);
|
|
212
120
|
if (runtime === undefined) {
|
|
213
|
-
runtime =
|
|
214
|
-
activeTools: new Set(),
|
|
215
|
-
officialEventsSeen: false,
|
|
216
|
-
stepHadFailure: false,
|
|
217
|
-
};
|
|
121
|
+
runtime = emptyProjectionRuntime();
|
|
218
122
|
this.sessionActivity.set(session, runtime);
|
|
219
123
|
}
|
|
220
124
|
return runtime;
|
|
@@ -228,52 +132,28 @@ export class PetService extends Service {
|
|
|
228
132
|
/** RPC: pet or feed the pet. */
|
|
229
133
|
async interact(kind) {
|
|
230
134
|
const nowMs = Date.now();
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
// BEFORE spending stock — a feed inside the cooldown must not burn a
|
|
234
|
-
// treat for nothing.
|
|
235
|
-
if (kind === 'feed')
|
|
236
|
-
this.settleTreats(nowMs);
|
|
237
|
-
const outcome = applyInteraction(this.persist.affinity, kind, nowMs, this.affinityConfig);
|
|
238
|
-
if (kind === 'feed' && !outcome.accepted) {
|
|
239
|
-
return { reaction: outcome.reaction, delta: 0, affinity: this.affinityView(this.persist.affinity) };
|
|
240
|
-
}
|
|
241
|
-
if (kind === 'feed') {
|
|
242
|
-
const consume = consumeTreat(this.persist.treats);
|
|
243
|
-
if (!consume.ok) {
|
|
244
|
-
const affinity = this.affinityView(this.persist.affinity);
|
|
245
|
-
return {
|
|
246
|
-
reaction: '没有小鱼干了,多陪鲸鱼娘工作一会儿吧~',
|
|
247
|
-
delta: 0,
|
|
248
|
-
affinity,
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
this.persist = { ...this.persist, treats: consume.ledger };
|
|
252
|
-
}
|
|
253
|
-
if (outcome.accepted) {
|
|
254
|
-
this.persist = { ...this.persist, affinity: outcome.affinity };
|
|
135
|
+
const result = this.ledger.interact(kind, nowMs);
|
|
136
|
+
if (this.ledger.takeDirty())
|
|
255
137
|
this.flush();
|
|
256
|
-
|
|
257
|
-
const affinity = this.affinityView(outcome.affinity);
|
|
258
|
-
return { reaction: outcome.reaction, delta: outcome.delta, affinity };
|
|
138
|
+
return result;
|
|
259
139
|
}
|
|
260
140
|
/** RPC: show or hide the pet. */
|
|
261
141
|
async setVisible(visible) {
|
|
262
|
-
this.
|
|
142
|
+
this.ledger.setDisplay({ ...this.ledger.snapshot.display, visible });
|
|
263
143
|
this.flush();
|
|
264
144
|
this.syncSettingsFromPet();
|
|
265
|
-
return { ok: true, display: this.
|
|
145
|
+
return { ok: true, display: this.ledger.snapshot.display };
|
|
266
146
|
}
|
|
267
147
|
/** RPC: update display config (size / position). Values are clamped to whole pixels. */
|
|
268
148
|
async setConfig(patch) {
|
|
269
|
-
const next = { ...this.
|
|
149
|
+
const next = { ...this.ledger.snapshot.display, ...patch };
|
|
270
150
|
next.size = Math.round(Math.min(DISPLAY_SIZE_MAX, Math.max(DISPLAY_SIZE_MIN, next.size)));
|
|
271
151
|
next.right = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, next.right)));
|
|
272
152
|
next.bottom = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, next.bottom)));
|
|
273
|
-
this.
|
|
153
|
+
this.ledger.setDisplay(next);
|
|
274
154
|
this.flush();
|
|
275
155
|
this.syncSettingsFromPet();
|
|
276
|
-
return { ok: true, display: this.
|
|
156
|
+
return { ok: true, display: this.ledger.snapshot.display };
|
|
277
157
|
}
|
|
278
158
|
/** RPC: rename the pet (trimmed, 1–20 chars). */
|
|
279
159
|
async setName(name) {
|
|
@@ -282,7 +162,7 @@ export class PetService extends Service {
|
|
|
282
162
|
return { ok: false, error: 'name-empty' };
|
|
283
163
|
if (trimmed.length > PET_NAME_MAX_LENGTH)
|
|
284
164
|
return { ok: false, error: 'name-too-long' };
|
|
285
|
-
this.
|
|
165
|
+
this.ledger.setName(trimmed);
|
|
286
166
|
this.flush();
|
|
287
167
|
this.syncSettingsFromPet();
|
|
288
168
|
return { ok: true, name: trimmed };
|
|
@@ -294,12 +174,13 @@ export class PetService extends Service {
|
|
|
294
174
|
* @param section - the resolved settings section.
|
|
295
175
|
*/
|
|
296
176
|
applySettingsSection(section) {
|
|
297
|
-
const next = { ...this.
|
|
177
|
+
const next = { ...this.ledger.snapshot.display };
|
|
298
178
|
next.visible = section.visible && (section.enabled ?? true);
|
|
299
179
|
next.size = Math.round(Math.min(DISPLAY_SIZE_MAX, Math.max(DISPLAY_SIZE_MIN, section.size)));
|
|
300
180
|
next.right = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, section.right)));
|
|
301
181
|
next.bottom = Math.round(Math.min(DISPLAY_INSET_MAX, Math.max(0, section.bottom)));
|
|
302
|
-
this.
|
|
182
|
+
this.ledger.setDisplay(next);
|
|
183
|
+
this.ledger.setName(section.name.trim());
|
|
303
184
|
this.flush();
|
|
304
185
|
}
|
|
305
186
|
/** Mirror the persisted display config into the settings document (best-effort). */
|
|
@@ -307,86 +188,48 @@ export class PetService extends Service {
|
|
|
307
188
|
const settings = this.ctx.get('settings', false);
|
|
308
189
|
if (settings === undefined)
|
|
309
190
|
return;
|
|
191
|
+
const snapshot = this.ledger.snapshot;
|
|
310
192
|
void settings.update(PET_SETTINGS_NAMESPACE, {
|
|
311
|
-
visible:
|
|
312
|
-
size:
|
|
313
|
-
right:
|
|
314
|
-
bottom:
|
|
315
|
-
name:
|
|
193
|
+
visible: snapshot.display.visible,
|
|
194
|
+
size: snapshot.display.size,
|
|
195
|
+
right: snapshot.display.right,
|
|
196
|
+
bottom: snapshot.display.bottom,
|
|
197
|
+
name: snapshot.name,
|
|
316
198
|
}).catch(() => {
|
|
317
199
|
// A settings write failure must not break the pet's own persistence.
|
|
318
200
|
});
|
|
319
201
|
}
|
|
320
202
|
/** Award the turn reward once per completed turn (idempotent per session + turn). */
|
|
321
203
|
rewardTurn(sessionId, turn) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
return;
|
|
325
|
-
this.rewardedTurns.set(sessionId, turn);
|
|
326
|
-
this.applyTurnReward();
|
|
204
|
+
if (this.ledger.rewardTurn(sessionId, turn, Date.now()))
|
|
205
|
+
this.flush();
|
|
327
206
|
}
|
|
328
207
|
/** Preserve turn rewards for installations that only emit legacy activity. */
|
|
329
208
|
rewardLegacyTurn() {
|
|
330
|
-
|
|
331
|
-
// A legacy `done` snapshot may repeat during the celebration window.
|
|
332
|
-
if (nowMs - this.lastLegacyTurnRewardAt < 5_000)
|
|
333
|
-
return;
|
|
334
|
-
this.lastLegacyTurnRewardAt = nowMs;
|
|
335
|
-
this.applyTurnReward();
|
|
336
|
-
}
|
|
337
|
-
/** Persist one accepted completed-turn reward. */
|
|
338
|
-
applyTurnReward() {
|
|
339
|
-
this.persist = { ...this.persist, affinity: applyTurnReward(this.persist.affinity, this.affinityConfig) };
|
|
340
|
-
this.flush();
|
|
341
|
-
}
|
|
342
|
-
/**
|
|
343
|
-
* Settle the treat economy (work + time output since the last settlement)
|
|
344
|
-
* and persist whenever the ledger changed. A zero-gain first settlement
|
|
345
|
-
* still starts the time clock (anchor write), which is what lets the
|
|
346
|
-
* 30-minute time output ever accrue.
|
|
347
|
-
*/
|
|
348
|
-
settleTreats(nowMs) {
|
|
349
|
-
const settlement = settleTreatGrants(this.persist.treats, this.persist.affinity.turns, nowMs, this.treatConfig);
|
|
350
|
-
if (settlement.ledger !== this.persist.treats) {
|
|
351
|
-
this.persist = { ...this.persist, treats: settlement.ledger };
|
|
209
|
+
if (this.ledger.rewardLegacyTurn(Date.now()))
|
|
352
210
|
this.flush();
|
|
353
|
-
}
|
|
354
211
|
}
|
|
355
212
|
view() {
|
|
356
213
|
const snapshot = this.machine.render();
|
|
357
|
-
//
|
|
358
|
-
|
|
214
|
+
// Read-only: the ledger settles on economic events only, never on a read,
|
|
215
|
+
// so polling the state cannot trigger pet.json writes.
|
|
359
216
|
return {
|
|
360
217
|
animation: snapshot.animation,
|
|
361
218
|
...(snapshot.bubble === undefined ? {} : { bubble: snapshot.bubble }),
|
|
362
219
|
phase: snapshot.phase,
|
|
363
220
|
sessionActive: snapshot.sessionActive,
|
|
364
|
-
affinity: this.affinityView(
|
|
365
|
-
display: { ...this.
|
|
366
|
-
name: this.
|
|
221
|
+
affinity: this.ledger.affinityView(Date.now()),
|
|
222
|
+
display: { ...this.ledger.snapshot.display },
|
|
223
|
+
name: this.ledger.snapshot.name,
|
|
367
224
|
treats: {
|
|
368
|
-
stocked: this.
|
|
369
|
-
max: this.
|
|
225
|
+
stocked: this.ledger.snapshot.treats.treats,
|
|
226
|
+
max: this.ledger.treatMax,
|
|
370
227
|
},
|
|
371
228
|
};
|
|
372
229
|
}
|
|
373
|
-
affinityView(affinity) {
|
|
374
|
-
const nowMs = Date.now();
|
|
375
|
-
const rank = rankOf(affinity.points);
|
|
376
|
-
return {
|
|
377
|
-
points: affinity.points,
|
|
378
|
-
rank: rank.name,
|
|
379
|
-
rankEmoji: rank.emoji,
|
|
380
|
-
pets: affinity.pets,
|
|
381
|
-
feeds: affinity.feeds,
|
|
382
|
-
turns: affinity.turns,
|
|
383
|
-
petCooldown: nowMs - affinity.lastPetAt < this.affinityConfig.petCooldownMs,
|
|
384
|
-
feedCooldown: nowMs - affinity.lastFeedAt < this.affinityConfig.feedCooldownMs,
|
|
385
|
-
};
|
|
386
|
-
}
|
|
387
230
|
flush() {
|
|
388
231
|
try {
|
|
389
|
-
savePetPersist(this.
|
|
232
|
+
savePetPersist(this.ledger.snapshot, this.persistDir);
|
|
390
233
|
}
|
|
391
234
|
catch {
|
|
392
235
|
// Persistence is best-effort; the in-memory ledger keeps working.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-pet",
|
|
3
3
|
"description": "鲸鱼娘宠物插件 for the dsh web GUI: a soft healing whale-girl companion that reacts to model activity (idle/waiting/thinking/tool/done), with petting/feeding interactions and an affinity score",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.15",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
package/src/affinity.ts
CHANGED
|
@@ -84,6 +84,39 @@ export function rankOf(points: number): (typeof AFFINITY_RANKS)[number] {
|
|
|
84
84
|
return rank
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/** Read-only affinity snapshot suited for the RPC view shape. */
|
|
88
|
+
export interface PetAffinityView {
|
|
89
|
+
points: number
|
|
90
|
+
rank: string
|
|
91
|
+
rankEmoji: string
|
|
92
|
+
pets: number
|
|
93
|
+
feeds: number
|
|
94
|
+
turns: number
|
|
95
|
+
/** True while the pet interaction is inside its cooldown. */
|
|
96
|
+
petCooldown: boolean
|
|
97
|
+
/** True while the feed is inside its cooldown. */
|
|
98
|
+
feedCooldown: boolean
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Derive the read-only view of one affinity state at a wall-clock instant. */
|
|
102
|
+
export function affinityViewOf(
|
|
103
|
+
state: AffinityState,
|
|
104
|
+
nowMs: number,
|
|
105
|
+
config: AffinityConfig = defaultAffinityConfig,
|
|
106
|
+
): PetAffinityView {
|
|
107
|
+
const rank = rankOf(state.points)
|
|
108
|
+
return {
|
|
109
|
+
points: state.points,
|
|
110
|
+
rank: rank.name,
|
|
111
|
+
rankEmoji: rank.emoji,
|
|
112
|
+
pets: state.pets,
|
|
113
|
+
feeds: state.feeds,
|
|
114
|
+
turns: state.turns,
|
|
115
|
+
petCooldown: nowMs - state.lastPetAt < config.petCooldownMs,
|
|
116
|
+
feedCooldown: nowMs - state.lastFeedAt < config.feedCooldownMs,
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
87
120
|
function clamp(points: number): number {
|
|
88
121
|
return Math.min(AFFINITY_MAX, Math.max(0, points))
|
|
89
122
|
}
|
|
@@ -1,25 +1,44 @@
|
|
|
1
|
+
// Generated by scripts/sync-shared.mjs from shared/client/settings/PluginSettingsCard.tsx. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
|
|
1
2
|
/**
|
|
2
|
-
*
|
|
3
|
-
* plugin and what its settings govern, the controls inside, and the save
|
|
4
|
-
* writes them. Renders nothing while the namespace is unavailable — a
|
|
3
|
+
* Family-shared chrome for plugin settings cards: a disclosure header naming
|
|
4
|
+
* the plugin and what its settings govern, the controls inside, and the save
|
|
5
|
+
* that writes them. Renders nothing while the namespace is unavailable — a
|
|
5
6
|
* deployment that does not compose the owning plugin should show no trace of
|
|
6
|
-
* it.
|
|
7
|
-
*
|
|
7
|
+
* it. Inlined into each consumer's client bundle; mirrors the official
|
|
8
|
+
* ui-plugin-config PluginCard in a self-contained slice.
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
import { useState, type ReactNode } from 'react'
|
|
11
12
|
import type { CardShell } from './settings-form.ts'
|
|
12
|
-
import type { SettingsCardKey } from './locales.ts'
|
|
13
13
|
import css from './settings-card.module.css'
|
|
14
14
|
|
|
15
|
+
/** Copy keys the card chrome itself reads; every consumer locale carries this shared vocabulary. */
|
|
16
|
+
export const CARD_COPY_KEYS = [
|
|
17
|
+
'settings.collapse',
|
|
18
|
+
'settings.expand',
|
|
19
|
+
'settings.notExposed',
|
|
20
|
+
'settings.unsaved',
|
|
21
|
+
'settings.readOnly',
|
|
22
|
+
'settings.saveFailed',
|
|
23
|
+
'settings.discard',
|
|
24
|
+
'settings.save',
|
|
25
|
+
'settings.saving',
|
|
26
|
+
] as const
|
|
27
|
+
|
|
28
|
+
/** Copy key the card chrome itself reads. */
|
|
29
|
+
export type CardCopyKey = (typeof CARD_COPY_KEYS)[number]
|
|
30
|
+
|
|
31
|
+
/** Key domain for the plugin's own copy (inferred per consumer). */
|
|
32
|
+
export type SettingsCardKey<TKey extends string = string> = TKey | CardCopyKey
|
|
33
|
+
|
|
15
34
|
/** Card chrome shared by every plugin settings card. */
|
|
16
|
-
export interface PluginSettingsCardProps {
|
|
35
|
+
export interface PluginSettingsCardProps<TKey extends string = string> {
|
|
17
36
|
/** Locale reader for this card's copy. */
|
|
18
|
-
t: (key: SettingsCardKey) => string
|
|
37
|
+
t: (key: SettingsCardKey<TKey>, params?: Record<string, string | number>) => string
|
|
19
38
|
/** Locale key of the plugin's name. */
|
|
20
|
-
titleKey:
|
|
39
|
+
titleKey: TKey
|
|
21
40
|
/** Locale key of the line describing what this plugin's settings govern. */
|
|
22
|
-
descriptionKey:
|
|
41
|
+
descriptionKey: TKey
|
|
23
42
|
/** The card's form state: availability, writability, and what a save would do. */
|
|
24
43
|
state: CardShell
|
|
25
44
|
/** Write every staged edit. */
|
|
@@ -35,11 +54,12 @@ export interface PluginSettingsCardProps {
|
|
|
35
54
|
* @param props - the plugin's copy keys, its form state, and its controls.
|
|
36
55
|
* @returns the card, or nothing while the namespace is still loading.
|
|
37
56
|
*/
|
|
38
|
-
export function PluginSettingsCard(props: PluginSettingsCardProps) {
|
|
57
|
+
export function PluginSettingsCard<TKey extends string = string>(props: PluginSettingsCardProps<TKey>) {
|
|
39
58
|
const [open, setOpen] = useState(false)
|
|
40
59
|
const { state } = props
|
|
41
60
|
if (!state.available) return null
|
|
42
61
|
const title = props.t(props.titleKey)
|
|
62
|
+
const description = props.t(props.descriptionKey)
|
|
43
63
|
const blocked = !state.dirty || state.invalid || state.saving
|
|
44
64
|
const cardClass = open ? `${css.cardOpen} ${css.card}` : css.card
|
|
45
65
|
// The namespace exists but the Host does not serve it to this client (the
|
|
@@ -57,8 +77,8 @@ export function PluginSettingsCard(props: PluginSettingsCardProps) {
|
|
|
57
77
|
onClick={() => { setOpen(!open) }}
|
|
58
78
|
>
|
|
59
79
|
<span className={css.headText}>
|
|
60
|
-
<span className={css.name}>{title}</span>
|
|
61
|
-
<span className={css.description}>{
|
|
80
|
+
<span className={css.name} title={title}>{title}</span>
|
|
81
|
+
<span className={css.description} title={description}>{description}</span>
|
|
62
82
|
</span>
|
|
63
83
|
<svg
|
|
64
84
|
width="14"
|
|
@@ -94,10 +114,10 @@ export function PluginSettingsCard(props: PluginSettingsCardProps) {
|
|
|
94
114
|
onClick={() => { setOpen(!open) }}
|
|
95
115
|
>
|
|
96
116
|
<span className={css.headText}>
|
|
97
|
-
<span className={css.name}>{title}</span>
|
|
98
|
-
<span className={css.description}>{
|
|
117
|
+
<span className={css.name} title={title}>{title}</span>
|
|
118
|
+
<span className={css.description} title={description}>{description}</span>
|
|
99
119
|
</span>
|
|
100
|
-
{state.dirty ? <span className={css.pending}>{props.t('settings.unsaved')}</span> : null}
|
|
120
|
+
{state.dirty ? <span className={css.pending} title={props.t('settings.unsaved')}>{props.t('settings.unsaved')}</span> : null}
|
|
101
121
|
<svg
|
|
102
122
|
width="14"
|
|
103
123
|
height="14"
|
|
@@ -259,4 +279,50 @@ export function BooleanField(props: FieldProps & {
|
|
|
259
279
|
<p className={css.hint}>{props.hint}</p>
|
|
260
280
|
</div>
|
|
261
281
|
)
|
|
262
|
-
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** A staged enumerated field rendered as a select. */
|
|
285
|
+
export function ChoiceField(props: FieldProps & {
|
|
286
|
+
/** Copy for the inherit option (draft text is the empty string). */
|
|
287
|
+
inheritLabel: string
|
|
288
|
+
/** Choices rendered in order; `value` is the draft/stored text. */
|
|
289
|
+
choices: ReadonlyArray<{ value: string; label: string }>
|
|
290
|
+
}) {
|
|
291
|
+
return (
|
|
292
|
+
<div className={css.field}>
|
|
293
|
+
<div className={css.head}>
|
|
294
|
+
<label className={css.label} htmlFor={props.id}>{props.label}</label>
|
|
295
|
+
{props.overridden
|
|
296
|
+
? (
|
|
297
|
+
<span className={css.badges}>
|
|
298
|
+
<span className={css.badge}>{props.overriddenLabel}</span>
|
|
299
|
+
<button
|
|
300
|
+
type="button"
|
|
301
|
+
className={css.reset}
|
|
302
|
+
disabled={props.disabled}
|
|
303
|
+
onClick={props.onReset}
|
|
304
|
+
>
|
|
305
|
+
{props.resetLabel}
|
|
306
|
+
</button>
|
|
307
|
+
</span>
|
|
308
|
+
)
|
|
309
|
+
: null}
|
|
310
|
+
</div>
|
|
311
|
+
<select
|
|
312
|
+
id={props.id}
|
|
313
|
+
className={css.select}
|
|
314
|
+
value={props.text}
|
|
315
|
+
disabled={props.disabled}
|
|
316
|
+
onChange={(event) => { props.onEdit(event.target.value) }}
|
|
317
|
+
>
|
|
318
|
+
<option value="">{props.inheritLabel}</option>
|
|
319
|
+
{props.choices.map(choice => (
|
|
320
|
+
<option key={choice.value} value={choice.value}>{choice.label}</option>
|
|
321
|
+
))}
|
|
322
|
+
</select>
|
|
323
|
+
<p className={props.invalid ? css.invalid : css.hint}>
|
|
324
|
+
{props.invalid ? props.invalidLabel : props.hint}
|
|
325
|
+
</p>
|
|
326
|
+
</div>
|
|
327
|
+
)
|
|
328
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-pet browser half — mounts the whale-girl as a global floating surface
|
|
3
3
|
* and drives it from the host's same-origin `/api/pet/*` JSON endpoints: poll
|
|
4
|
-
* the host snapshot (~
|
|
4
|
+
* the host snapshot (~2 s), forward interactions, persist drag positions.
|
|
5
5
|
* The pet is host-global (no session dimension), so it mounts directly onto
|
|
6
6
|
* `document.body` via a single React root rather than a session-scoped slot —
|
|
7
7
|
* on the new-conversation screen no session exists, and a dock-mounted pet
|
|
@@ -61,7 +61,7 @@ const petApi: PetHttpApi = {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/** Poll interval for the host snapshot. */
|
|
64
|
-
const POLL_MS =
|
|
64
|
+
const POLL_MS = 2000
|
|
65
65
|
|
|
66
66
|
/** Settings namespace the pet settings card edits (the Host plugin registers it). */
|
|
67
67
|
const PET_SETTINGS_NS = 'pet'
|
|
@@ -161,7 +161,7 @@ export function apply(ctx: ClientContext): void {
|
|
|
161
161
|
// change while the page is hidden, so a background interval would
|
|
162
162
|
// only burn RPCs (browser throttling is an unreliable backstop).
|
|
163
163
|
// Coming back to the tab refreshes the pet immediately instead of
|
|
164
|
-
// waiting out the next
|
|
164
|
+
// waiting out the next 2 s cycle.
|
|
165
165
|
let timer: number | undefined
|
|
166
166
|
const stop = (): void => {
|
|
167
167
|
if (timer !== undefined) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* Generated by scripts/sync-shared.mjs from shared/client/settings/settings-card.module.css. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs". */
|
|
1
2
|
/* Plugin settings card chrome + staged form fields.
|
|
2
3
|
* Aligned with the official ui-settings-plugins PluginCard / fields CSS:
|
|
3
4
|
* same semantic tokens, radius, typography and states so family cards read
|
|
@@ -229,6 +230,11 @@
|
|
|
229
230
|
outline-offset: 2px;
|
|
230
231
|
}
|
|
231
232
|
|
|
233
|
+
.reset:focus-visible {
|
|
234
|
+
outline: 2px solid var(--dsw-alias-brand-primary);
|
|
235
|
+
outline-offset: 2px;
|
|
236
|
+
}
|
|
237
|
+
|
|
232
238
|
.input,
|
|
233
239
|
.select {
|
|
234
240
|
border: 1px solid var(--dsw-alias-border-l2);
|