dsh-claude-desktop-theme 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +54 -0
- package/README.md +197 -0
- package/cordis.patch.yml +5 -0
- package/lib/client.js +5492 -0
- package/lib/index.js +666 -0
- package/lib/types/client/apply.d.ts +25 -0
- package/lib/types/client/compat/account-menu.d.ts +52 -0
- package/lib/types/client/compat/adapter.d.ts +17 -0
- package/lib/types/client/compat/blank-session-rows.d.ts +74 -0
- package/lib/types/client/compat/composer-menus.d.ts +11 -0
- package/lib/types/client/compat/composer-placeholder.d.ts +3 -0
- package/lib/types/client/compat/composer-status-order.d.ts +56 -0
- package/lib/types/client/compat/conversation-fade.d.ts +37 -0
- package/lib/types/client/compat/dom.d.ts +2 -0
- package/lib/types/client/compat/header-labels.d.ts +24 -0
- package/lib/types/client/compat/host-builds.d.ts +101 -0
- package/lib/types/client/compat/host-dom.d.ts +247 -0
- package/lib/types/client/compat/open-target.d.ts +9 -0
- package/lib/types/client/compat/pet-anchor.d.ts +30 -0
- package/lib/types/client/compat/plugin-manager.d.ts +32 -0
- package/lib/types/client/compat/session-menu.d.ts +47 -0
- package/lib/types/client/compat/slots.d.ts +21 -0
- package/lib/types/client/compat/stats-values.d.ts +20 -0
- package/lib/types/client/compat/view-switch.d.ts +14 -0
- package/lib/types/client/compat/workspace-menu.d.ts +15 -0
- package/lib/types/client/contracts/feature.d.ts +22 -0
- package/lib/types/client/contracts/ports.d.ts +169 -0
- package/lib/types/client/core/cleanup.d.ts +10 -0
- package/lib/types/client/core/mount-features.d.ts +3 -0
- package/lib/types/client/features/composer-pet/BlankPet.d.ts +17 -0
- package/lib/types/client/features/composer-pet/ComposerPet.d.ts +7 -0
- package/lib/types/client/features/composer-pet/Whale.d.ts +18 -0
- package/lib/types/client/features/composer-pet/mount.d.ts +9 -0
- package/lib/types/client/features/composer-pet/reactions.d.ts +15 -0
- package/lib/types/client/features/conversation/header-actions/HeaderActionButton.d.ts +25 -0
- package/lib/types/client/features/conversation/header-actions/entries.d.ts +101 -0
- package/lib/types/client/features/conversation/header-actions/mount.d.ts +28 -0
- package/lib/types/client/features/conversation/header-actions/sidebar-view.d.ts +21 -0
- package/lib/types/client/features/conversation/index.d.ts +16 -0
- package/lib/types/client/features/model-controls/EffortPanel.d.ts +14 -0
- package/lib/types/client/features/model-controls/ModelControls.d.ts +11 -0
- package/lib/types/client/features/model-controls/mount.d.ts +5 -0
- package/lib/types/client/features/model-controls/pixels.d.ts +4 -0
- package/lib/types/client/features/model-controls/popup.d.ts +10 -0
- package/lib/types/client/features/model-controls/selection.d.ts +66 -0
- package/lib/types/client/features/new-session/index.d.ts +8 -0
- package/lib/types/client/features/settings/ConfigPage.d.ts +28 -0
- package/lib/types/client/features/settings/FontMenu.d.ts +37 -0
- package/lib/types/client/features/settings/edits.d.ts +39 -0
- package/lib/types/client/features/settings/fields.d.ts +83 -0
- package/lib/types/client/features/settings/fonts.d.ts +40 -0
- package/lib/types/client/features/settings/locales.d.ts +61 -0
- package/lib/types/client/features/settings/mount.d.ts +17 -0
- package/lib/types/client/features/settings/register.d.ts +17 -0
- package/lib/types/client/features/shell/index.d.ts +8 -0
- package/lib/types/client/features/sidebar/index.d.ts +14 -0
- package/lib/types/client/features/statistics/books.d.ts +34 -0
- package/lib/types/client/features/statistics/card.d.ts +143 -0
- package/lib/types/client/features/statistics/format.d.ts +36 -0
- package/lib/types/client/features/statistics/index.d.ts +8 -0
- package/lib/types/client/features/statistics/mount.d.ts +12 -0
- package/lib/types/client/features/statistics/source.d.ts +35 -0
- package/lib/types/client/features/statistics/view.d.ts +95 -0
- package/lib/types/client/features/tool-calls/index.d.ts +2 -0
- package/lib/types/client/index.d.ts +1 -0
- package/lib/types/client/theme/mount.d.ts +15 -0
- package/lib/types/client/theme/palette.d.ts +80 -0
- package/lib/types/client/theme/tokens.d.ts +27 -0
- package/lib/types/host/index.d.ts +106 -0
- package/lib/types/host/stats/aggregate.d.ts +38 -0
- package/lib/types/host/stats/service.d.ts +18 -0
- package/lib/types/host/stats/unit.d.ts +115 -0
- package/lib/types/shared/config.d.ts +61 -0
- package/lib/types/shared/identity.d.ts +5 -0
- package/lib/types/shared/stats.d.ts +133 -0
- package/package.json +116 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
// src/host/index.ts
|
|
2
|
+
import z from "@deepseek-ai/schemastery";
|
|
3
|
+
|
|
4
|
+
// src/shared/config.ts
|
|
5
|
+
var DEFAULT_FEATURES = Object.freeze({
|
|
6
|
+
shell: true,
|
|
7
|
+
sidebar: true,
|
|
8
|
+
"new-session": true,
|
|
9
|
+
conversation: true,
|
|
10
|
+
"tool-calls": false,
|
|
11
|
+
statistics: false,
|
|
12
|
+
"composer-pet": true
|
|
13
|
+
});
|
|
14
|
+
var DEFAULT_APPEARANCE = Object.freeze({
|
|
15
|
+
canvas: "",
|
|
16
|
+
sidebar: ""
|
|
17
|
+
});
|
|
18
|
+
var DEFAULT_FONTS = Object.freeze({
|
|
19
|
+
uiLatin: "",
|
|
20
|
+
uiCjk: "",
|
|
21
|
+
code: ""
|
|
22
|
+
});
|
|
23
|
+
var FONT_NAME_MAX_LENGTH = 64;
|
|
24
|
+
|
|
25
|
+
// src/shared/stats.ts
|
|
26
|
+
var STATS_PATH = "/api/ccd-stats";
|
|
27
|
+
var STATS_VERSION = 2;
|
|
28
|
+
var STATS_DAY_LIMIT = 366;
|
|
29
|
+
var STATS_RANGES = ["all", "30d", "7d"];
|
|
30
|
+
var STATS_RANGE_DAYS = {
|
|
31
|
+
all: null,
|
|
32
|
+
"30d": 30,
|
|
33
|
+
"7d": 7
|
|
34
|
+
};
|
|
35
|
+
function localDayKey(time) {
|
|
36
|
+
const date = new Date(time);
|
|
37
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
38
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
39
|
+
return `${date.getFullYear()}-${month}-${day}`;
|
|
40
|
+
}
|
|
41
|
+
function rangeStart(range, now) {
|
|
42
|
+
const days = STATS_RANGE_DAYS[range];
|
|
43
|
+
if (days === null) return null;
|
|
44
|
+
const date = new Date(now);
|
|
45
|
+
date.setHours(0, 0, 0, 0);
|
|
46
|
+
date.setDate(date.getDate() - (days - 1));
|
|
47
|
+
return localDayKey(date.getTime());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/shared/identity.ts
|
|
51
|
+
var PLUGIN_ID = "dsh-claude-desktop-theme";
|
|
52
|
+
|
|
53
|
+
// src/host/stats/unit.ts
|
|
54
|
+
var USAGE_KEY = "ccdUsage";
|
|
55
|
+
var USAGE_STATE_VERSION = 2;
|
|
56
|
+
var UNKNOWN_MODEL = "unknown";
|
|
57
|
+
var HOURS_PER_DAY = 24;
|
|
58
|
+
function rawTokens(usage) {
|
|
59
|
+
const total = usage.totalTokens;
|
|
60
|
+
if (typeof total === "number" && Number.isFinite(total) && total > 0) return total;
|
|
61
|
+
const sum = (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0) + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0);
|
|
62
|
+
return Number.isFinite(sum) && sum > 0 ? sum : 0;
|
|
63
|
+
}
|
|
64
|
+
function counter(value) {
|
|
65
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
|
|
66
|
+
}
|
|
67
|
+
function initUsageState(_header, inheritedEventCount) {
|
|
68
|
+
return {
|
|
69
|
+
seedSeq: inheritedEventCount,
|
|
70
|
+
prompts: 0,
|
|
71
|
+
tokens: 0,
|
|
72
|
+
days: {},
|
|
73
|
+
hours: new Array(HOURS_PER_DAY).fill(0),
|
|
74
|
+
models: {},
|
|
75
|
+
modelDays: {},
|
|
76
|
+
model: null
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function bumped(record, key, amount) {
|
|
80
|
+
return { ...record, [key]: (record[key] ?? 0) + amount };
|
|
81
|
+
}
|
|
82
|
+
function applyUsageEvent(state, event) {
|
|
83
|
+
if (event.seq < state.seedSeq) return state;
|
|
84
|
+
if (event.type === "user/message") {
|
|
85
|
+
if (event.data.source.kind !== "user") return state;
|
|
86
|
+
const hour = new Date(event.time).getHours();
|
|
87
|
+
const hours = state.hours.slice();
|
|
88
|
+
hours[hour] = (hours[hour] ?? 0) + 1;
|
|
89
|
+
return {
|
|
90
|
+
...state,
|
|
91
|
+
prompts: state.prompts + 1,
|
|
92
|
+
days: bumped(state.days, localDayKey(event.time), 1),
|
|
93
|
+
hours
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (event.type === "assistant/message") {
|
|
97
|
+
const usage = event.data.usage;
|
|
98
|
+
if (usage === void 0) return state;
|
|
99
|
+
const added = rawTokens(usage);
|
|
100
|
+
if (added === 0) return state;
|
|
101
|
+
const model = state.model ?? UNKNOWN_MODEL;
|
|
102
|
+
const day = localDayKey(event.time);
|
|
103
|
+
const previous = state.models[model] ?? { t: 0, i: 0, o: 0 };
|
|
104
|
+
return {
|
|
105
|
+
...state,
|
|
106
|
+
tokens: state.tokens + added,
|
|
107
|
+
models: {
|
|
108
|
+
...state.models,
|
|
109
|
+
[model]: {
|
|
110
|
+
t: previous.t + added,
|
|
111
|
+
i: previous.i + counter(usage.inputTokens),
|
|
112
|
+
o: previous.o + counter(usage.outputTokens)
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
modelDays: {
|
|
116
|
+
...state.modelDays,
|
|
117
|
+
[day]: bumped(state.modelDays[day] ?? {}, model, added)
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (event.type === "request/header") {
|
|
122
|
+
const config = event.data.header.config;
|
|
123
|
+
const model = `${config.provider}/${config.model}`;
|
|
124
|
+
return model === state.model ? state : { ...state, model };
|
|
125
|
+
}
|
|
126
|
+
return state;
|
|
127
|
+
}
|
|
128
|
+
function pairs(record) {
|
|
129
|
+
return Object.entries(record).filter(([, count2]) => count2 > 0).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
|
|
130
|
+
}
|
|
131
|
+
function hourPairs(hours) {
|
|
132
|
+
const sparse = [];
|
|
133
|
+
for (let hour = 0; hour < hours.length; hour += 1) {
|
|
134
|
+
const count2 = hours[hour] ?? 0;
|
|
135
|
+
if (count2 > 0) sparse.push([hour, count2]);
|
|
136
|
+
}
|
|
137
|
+
return sparse;
|
|
138
|
+
}
|
|
139
|
+
function modelPairs(models) {
|
|
140
|
+
return Object.entries(models).filter(([, usage]) => usage.t > 0).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([model, usage]) => [model, usage.t, usage.i, usage.o]);
|
|
141
|
+
}
|
|
142
|
+
function modelDayRows(modelDays, table) {
|
|
143
|
+
const index = new Map(table.map(([model], at) => [model, at]));
|
|
144
|
+
const rows = [];
|
|
145
|
+
for (const day of Object.keys(modelDays).sort()) {
|
|
146
|
+
const perModel = modelDays[day] ?? {};
|
|
147
|
+
for (const model of Object.keys(perModel).sort()) {
|
|
148
|
+
const at = index.get(model);
|
|
149
|
+
const tokens = perModel[model] ?? 0;
|
|
150
|
+
if (at === void 0 || tokens <= 0) continue;
|
|
151
|
+
rows.push([day, at, tokens]);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return rows;
|
|
155
|
+
}
|
|
156
|
+
var views = /* @__PURE__ */ new WeakMap();
|
|
157
|
+
function viewUsage(state) {
|
|
158
|
+
const cached = views.get(state);
|
|
159
|
+
if (cached !== void 0) return cached;
|
|
160
|
+
const models = modelPairs(state.models);
|
|
161
|
+
const view = {
|
|
162
|
+
prompts: state.prompts,
|
|
163
|
+
tokens: state.tokens,
|
|
164
|
+
days: pairs(state.days),
|
|
165
|
+
hours: hourPairs(state.hours),
|
|
166
|
+
models,
|
|
167
|
+
modelDays: modelDayRows(state.modelDays, models)
|
|
168
|
+
};
|
|
169
|
+
views.set(state, view);
|
|
170
|
+
return view;
|
|
171
|
+
}
|
|
172
|
+
function schemaOf(parse) {
|
|
173
|
+
return { parse };
|
|
174
|
+
}
|
|
175
|
+
function count(value) {
|
|
176
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
|
|
177
|
+
}
|
|
178
|
+
function readCounts(value) {
|
|
179
|
+
const counts = {};
|
|
180
|
+
if (typeof value !== "object" || value === null) return counts;
|
|
181
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
182
|
+
const parsed = count(entry);
|
|
183
|
+
if (parsed > 0) counts[key] = parsed;
|
|
184
|
+
}
|
|
185
|
+
return counts;
|
|
186
|
+
}
|
|
187
|
+
function readModels(value) {
|
|
188
|
+
const models = {};
|
|
189
|
+
if (typeof value !== "object" || value === null) return models;
|
|
190
|
+
for (const [model, entry] of Object.entries(value)) {
|
|
191
|
+
if (typeof entry === "number") {
|
|
192
|
+
const total2 = count(entry);
|
|
193
|
+
if (total2 > 0) models[model] = { t: total2, i: 0, o: 0 };
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
197
|
+
const raw = entry;
|
|
198
|
+
const total = count(raw["t"]);
|
|
199
|
+
if (total > 0) models[model] = { t: total, i: count(raw["i"]), o: count(raw["o"]) };
|
|
200
|
+
}
|
|
201
|
+
return models;
|
|
202
|
+
}
|
|
203
|
+
function readModelDays(value) {
|
|
204
|
+
const days = {};
|
|
205
|
+
if (typeof value !== "object" || value === null) return days;
|
|
206
|
+
for (const [day, entry] of Object.entries(value)) {
|
|
207
|
+
const perModel = readCounts(entry);
|
|
208
|
+
if (Object.keys(perModel).length > 0) days[day] = perModel;
|
|
209
|
+
}
|
|
210
|
+
return days;
|
|
211
|
+
}
|
|
212
|
+
function readUsageState(value) {
|
|
213
|
+
const raw = typeof value === "object" && value !== null ? value : {};
|
|
214
|
+
const hours = new Array(HOURS_PER_DAY).fill(0);
|
|
215
|
+
const rawHours = Array.isArray(raw["hours"]) ? raw["hours"] : [];
|
|
216
|
+
for (let hour = 0; hour < HOURS_PER_DAY; hour += 1) hours[hour] = count(rawHours[hour]);
|
|
217
|
+
const model = raw["model"];
|
|
218
|
+
return {
|
|
219
|
+
seedSeq: count(raw["seedSeq"]),
|
|
220
|
+
prompts: count(raw["prompts"]),
|
|
221
|
+
tokens: count(raw["tokens"]),
|
|
222
|
+
days: readCounts(raw["days"]),
|
|
223
|
+
hours,
|
|
224
|
+
models: readModels(raw["models"]),
|
|
225
|
+
modelDays: readModelDays(raw["modelDays"]),
|
|
226
|
+
model: typeof model === "string" && model !== "" ? model : null
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function readPairList(value, key) {
|
|
230
|
+
if (!Array.isArray(value)) return [];
|
|
231
|
+
const list = [];
|
|
232
|
+
for (const entry of value) {
|
|
233
|
+
if (!Array.isArray(entry) || entry.length !== 2) continue;
|
|
234
|
+
const [rawKey, rawCount] = entry;
|
|
235
|
+
const parsedKey = key(rawKey);
|
|
236
|
+
const parsedCount = count(rawCount);
|
|
237
|
+
if (parsedKey === null || parsedCount === 0) continue;
|
|
238
|
+
list.push([parsedKey, parsedCount]);
|
|
239
|
+
}
|
|
240
|
+
return list;
|
|
241
|
+
}
|
|
242
|
+
function readUsageView(value) {
|
|
243
|
+
const raw = typeof value === "object" && value !== null ? value : {};
|
|
244
|
+
const models = [];
|
|
245
|
+
if (Array.isArray(raw["models"])) {
|
|
246
|
+
for (const entry of raw["models"]) {
|
|
247
|
+
if (!Array.isArray(entry) || entry.length < 4) continue;
|
|
248
|
+
const [model, total, input, output] = entry;
|
|
249
|
+
if (typeof model !== "string" || model === "") continue;
|
|
250
|
+
const tokens = count(total);
|
|
251
|
+
if (tokens === 0) continue;
|
|
252
|
+
models.push([model, tokens, count(input), count(output)]);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const modelDays = [];
|
|
256
|
+
if (Array.isArray(raw["modelDays"])) {
|
|
257
|
+
for (const entry of raw["modelDays"]) {
|
|
258
|
+
if (!Array.isArray(entry) || entry.length !== 3) continue;
|
|
259
|
+
const [day, at, tokens] = entry;
|
|
260
|
+
if (typeof day !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(day)) continue;
|
|
261
|
+
if (typeof at !== "number" || !Number.isInteger(at) || at < 0 || at >= models.length) continue;
|
|
262
|
+
const counted = count(tokens);
|
|
263
|
+
if (counted === 0) continue;
|
|
264
|
+
modelDays.push([day, at, counted]);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
prompts: count(raw["prompts"]),
|
|
269
|
+
tokens: count(raw["tokens"]),
|
|
270
|
+
days: readPairList(raw["days"], (rawKey) => typeof rawKey === "string" && /^\d{4}-\d{2}-\d{2}$/.test(rawKey) ? rawKey : null),
|
|
271
|
+
/* Hour keys travel as numbers; a string is accepted for a hand-written row. */
|
|
272
|
+
hours: readPairList(raw["hours"], (rawKey) => {
|
|
273
|
+
const hour = typeof rawKey === "number" ? rawKey : Number(rawKey);
|
|
274
|
+
return Number.isInteger(hour) && hour >= 0 && hour < HOURS_PER_DAY ? hour : null;
|
|
275
|
+
}),
|
|
276
|
+
models,
|
|
277
|
+
modelDays
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
var usageProjection = {
|
|
281
|
+
key: USAGE_KEY,
|
|
282
|
+
stateSchema: schemaOf(readUsageState),
|
|
283
|
+
init: initUsageState,
|
|
284
|
+
apply: applyUsageEvent,
|
|
285
|
+
wire: {
|
|
286
|
+
viewSchema: schemaOf(readUsageView),
|
|
287
|
+
view: viewUsage
|
|
288
|
+
},
|
|
289
|
+
stateVersion: USAGE_STATE_VERSION
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
// src/host/stats/aggregate.ts
|
|
293
|
+
var byKey = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
294
|
+
var byTokens = (left, right) => right[1] - left[1] || byKey(left[0], right[0]);
|
|
295
|
+
function peak(counts, skip) {
|
|
296
|
+
let best = null;
|
|
297
|
+
let bestCount = 0;
|
|
298
|
+
for (const [key, count2] of counts) {
|
|
299
|
+
if (key === skip || count2 <= 0) continue;
|
|
300
|
+
if (count2 > bestCount || count2 === bestCount && best !== null && key < best) {
|
|
301
|
+
best = key;
|
|
302
|
+
bestCount = count2;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return best;
|
|
306
|
+
}
|
|
307
|
+
function collect(value, collected) {
|
|
308
|
+
if (value.prompts <= 0 && value.tokens <= 0) return;
|
|
309
|
+
const dates = /* @__PURE__ */ new Set();
|
|
310
|
+
for (const [date, count2] of value.days) {
|
|
311
|
+
dates.add(date);
|
|
312
|
+
collected.prompts.set(date, (collected.prompts.get(date) ?? 0) + count2);
|
|
313
|
+
}
|
|
314
|
+
for (const [hour, count2] of value.hours) collected.hours.set(hour, (collected.hours.get(hour) ?? 0) + count2);
|
|
315
|
+
for (const [model, tokens, input, output] of value.models) {
|
|
316
|
+
const previous = collected.modelUsage.get(model) ?? { t: 0, i: 0, o: 0 };
|
|
317
|
+
collected.modelUsage.set(model, {
|
|
318
|
+
t: previous.t + tokens,
|
|
319
|
+
i: previous.i + input,
|
|
320
|
+
o: previous.o + output
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
for (const [date, at, tokens] of value.modelDays) {
|
|
324
|
+
const model = value.models[at]?.[0];
|
|
325
|
+
if (model === void 0) continue;
|
|
326
|
+
dates.add(date);
|
|
327
|
+
const perModel = collected.modelDays.get(date) ?? /* @__PURE__ */ new Map();
|
|
328
|
+
perModel.set(model, (perModel.get(model) ?? 0) + tokens);
|
|
329
|
+
collected.modelDays.set(date, perModel);
|
|
330
|
+
}
|
|
331
|
+
collected.sessionDates.push(dates);
|
|
332
|
+
}
|
|
333
|
+
function totalsFor(collected, start) {
|
|
334
|
+
const inRange = (date) => start === null || date >= start;
|
|
335
|
+
let sessions = 0;
|
|
336
|
+
for (const dates of collected.sessionDates) {
|
|
337
|
+
for (const date of dates) {
|
|
338
|
+
if (!inRange(date)) continue;
|
|
339
|
+
sessions += 1;
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const active = /* @__PURE__ */ new Set();
|
|
344
|
+
let messages = 0;
|
|
345
|
+
for (const [date, count2] of collected.prompts) {
|
|
346
|
+
if (!inRange(date)) continue;
|
|
347
|
+
messages += count2;
|
|
348
|
+
active.add(date);
|
|
349
|
+
}
|
|
350
|
+
const models = /* @__PURE__ */ new Map();
|
|
351
|
+
let tokens = 0;
|
|
352
|
+
for (const [date, perModel] of collected.modelDays) {
|
|
353
|
+
if (!inRange(date)) continue;
|
|
354
|
+
active.add(date);
|
|
355
|
+
for (const [model, amount] of perModel) {
|
|
356
|
+
tokens += amount;
|
|
357
|
+
models.set(model, (models.get(model) ?? 0) + amount);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const list = [...models.entries()].filter(([model, amount]) => model !== UNKNOWN_MODEL && amount > 0).sort(byTokens);
|
|
361
|
+
return {
|
|
362
|
+
sessions,
|
|
363
|
+
messages,
|
|
364
|
+
tokens,
|
|
365
|
+
activeDays: active.size,
|
|
366
|
+
topModel: peak(models, UNKNOWN_MODEL),
|
|
367
|
+
models: list
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
function aggregateUsage(values, options) {
|
|
371
|
+
const collected = {
|
|
372
|
+
sessionDates: [],
|
|
373
|
+
prompts: /* @__PURE__ */ new Map(),
|
|
374
|
+
hours: /* @__PURE__ */ new Map(),
|
|
375
|
+
modelUsage: /* @__PURE__ */ new Map(),
|
|
376
|
+
modelDays: /* @__PURE__ */ new Map()
|
|
377
|
+
};
|
|
378
|
+
for (const value of values) collect(value, collected);
|
|
379
|
+
const ranges = {};
|
|
380
|
+
for (const range of STATS_RANGES) ranges[range] = totalsFor(collected, rangeStart(range, options.now));
|
|
381
|
+
const dates = [.../* @__PURE__ */ new Set([...collected.prompts.keys(), ...collected.modelDays.keys()])].sort(byKey);
|
|
382
|
+
const cutoff = dates.length > STATS_DAY_LIMIT ? dates[dates.length - STATS_DAY_LIMIT] ?? null : null;
|
|
383
|
+
const days = [];
|
|
384
|
+
for (const date of dates) {
|
|
385
|
+
if (cutoff !== null && date < cutoff) continue;
|
|
386
|
+
const count2 = collected.prompts.get(date) ?? 0;
|
|
387
|
+
if (count2 > 0) days.push([date, count2]);
|
|
388
|
+
}
|
|
389
|
+
const modelDays = [];
|
|
390
|
+
for (const [date, perModel] of [...collected.modelDays.entries()].sort(([left], [right]) => byKey(left, right))) {
|
|
391
|
+
if (cutoff !== null && date < cutoff) continue;
|
|
392
|
+
for (const [model, tokens] of [...perModel.entries()].sort(([left], [right]) => byKey(left, right))) {
|
|
393
|
+
if (tokens > 0) modelDays.push([date, model, tokens]);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const modelIO = [...collected.modelUsage.entries()].filter(([model, usage]) => model !== UNKNOWN_MODEL && usage.t > 0).sort(([left], [right]) => byKey(left, right)).map(([model, usage]) => [model, usage.i, usage.o]);
|
|
397
|
+
let busyHour = null;
|
|
398
|
+
let busyCount = 0;
|
|
399
|
+
for (let hour = 0; hour < HOURS_PER_DAY; hour += 1) {
|
|
400
|
+
const count2 = collected.hours.get(hour) ?? 0;
|
|
401
|
+
if (count2 > busyCount) {
|
|
402
|
+
busyHour = hour;
|
|
403
|
+
busyCount = count2;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return {
|
|
407
|
+
version: STATS_VERSION,
|
|
408
|
+
ranges,
|
|
409
|
+
busyHour,
|
|
410
|
+
days,
|
|
411
|
+
modelDays,
|
|
412
|
+
modelIO,
|
|
413
|
+
pending: options.pending,
|
|
414
|
+
missed: options.missed,
|
|
415
|
+
computedAt: options.now
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// src/host/stats/service.ts
|
|
420
|
+
var FOLD_CONCURRENCY = 4;
|
|
421
|
+
var RETRY_COOLDOWN_MS = 6e4;
|
|
422
|
+
var UsageAggregator = class {
|
|
423
|
+
ctx;
|
|
424
|
+
warn;
|
|
425
|
+
values = /* @__PURE__ */ new Map();
|
|
426
|
+
queued = [];
|
|
427
|
+
folding = /* @__PURE__ */ new Set();
|
|
428
|
+
failures = /* @__PURE__ */ new Map();
|
|
429
|
+
running = 0;
|
|
430
|
+
refreshRun = null;
|
|
431
|
+
snapshot = null;
|
|
432
|
+
disposed = false;
|
|
433
|
+
constructor(ctx, warn) {
|
|
434
|
+
this.ctx = ctx;
|
|
435
|
+
this.warn = warn;
|
|
436
|
+
}
|
|
437
|
+
/** The most recent snapshot; an empty one before the first refresh. */
|
|
438
|
+
get current() {
|
|
439
|
+
return this.snapshot ?? aggregateUsage([], { now: Date.now(), pending: 0, missed: 0 });
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Bring the snapshot up to date.
|
|
443
|
+
*
|
|
444
|
+
* The listing pass is cheap (projection reads only) and serialized, so
|
|
445
|
+
* several polling browsers share one pass; cold folds continue in the
|
|
446
|
+
* background and land in later snapshots.
|
|
447
|
+
*
|
|
448
|
+
* @returns the snapshot as of this pass.
|
|
449
|
+
*/
|
|
450
|
+
refresh() {
|
|
451
|
+
const running = this.refreshRun;
|
|
452
|
+
if (running !== null) return running;
|
|
453
|
+
const run = this.pass().finally(() => {
|
|
454
|
+
if (this.refreshRun === run) this.refreshRun = null;
|
|
455
|
+
});
|
|
456
|
+
this.refreshRun = run;
|
|
457
|
+
return run;
|
|
458
|
+
}
|
|
459
|
+
/** Stop folding; in-flight reads settle but no longer publish. */
|
|
460
|
+
dispose() {
|
|
461
|
+
this.disposed = true;
|
|
462
|
+
this.queued.length = 0;
|
|
463
|
+
}
|
|
464
|
+
async pass() {
|
|
465
|
+
try {
|
|
466
|
+
const candidates = await this.list();
|
|
467
|
+
const listed = new Set(candidates.map((candidate) => candidate.header.id));
|
|
468
|
+
for (const id of [...this.values.keys()]) if (!listed.has(id)) this.values.delete(id);
|
|
469
|
+
for (const id of [...this.failures.keys()]) if (!listed.has(id)) this.failures.delete(id);
|
|
470
|
+
for (const candidate of candidates) {
|
|
471
|
+
if (this.disposed) break;
|
|
472
|
+
const value = this.readSafely(candidate);
|
|
473
|
+
if (value === void 0) this.enqueue(candidate);
|
|
474
|
+
else this.values.set(candidate.header.id, value);
|
|
475
|
+
}
|
|
476
|
+
} catch (error) {
|
|
477
|
+
this.warn("session listing failed; the card keeps the last snapshot", error);
|
|
478
|
+
}
|
|
479
|
+
this.publish();
|
|
480
|
+
return this.current;
|
|
481
|
+
}
|
|
482
|
+
/** Every stored session the card counts, newest first. */
|
|
483
|
+
async list() {
|
|
484
|
+
const query = this.ctx.get("sessionQuery");
|
|
485
|
+
if (query !== void 0) {
|
|
486
|
+
const records = await query.listSessions();
|
|
487
|
+
return records.filter((record) => record.header.cwd !== void 0).map((record) => ({ header: record.header, live: record.live }));
|
|
488
|
+
}
|
|
489
|
+
const persistence = this.ctx.get("sessionPersistence");
|
|
490
|
+
if (persistence === void 0) return [];
|
|
491
|
+
const snapshots = await persistence.list();
|
|
492
|
+
return snapshots.filter((snapshot) => snapshot.header.cwd !== void 0).map((snapshot) => ({ header: snapshot.header, live: false }));
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Read one session's value without letting a single failure drop the pass.
|
|
496
|
+
*
|
|
497
|
+
* A live session's projection read can throw (a host-side projection bug, a
|
|
498
|
+
* log the registry cannot advance across); that must cost this session's
|
|
499
|
+
* numbers, not every session's.
|
|
500
|
+
*/
|
|
501
|
+
readSafely(candidate) {
|
|
502
|
+
try {
|
|
503
|
+
return this.read(candidate);
|
|
504
|
+
} catch (error) {
|
|
505
|
+
this.warn(`projection read failed for session ${candidate.header.id}`, error);
|
|
506
|
+
return void 0;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
/** Zero-I/O read: the live registry for a live session, the durable cache for a cold one. */
|
|
510
|
+
read(candidate) {
|
|
511
|
+
const projections = this.ctx.get("sessionProjections");
|
|
512
|
+
if (projections === void 0) return void 0;
|
|
513
|
+
if (candidate.live) {
|
|
514
|
+
const session = this.ctx.get("sessions")?.get(candidate.header.id);
|
|
515
|
+
if (session !== void 0) {
|
|
516
|
+
return projections.snapshot(session, [USAGE_KEY]).values[USAGE_KEY];
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
const cache = this.ctx.get("sessionProjectionCache");
|
|
520
|
+
return cache?.cachedSnapshot(candidate.header, [USAGE_KEY])?.values[USAGE_KEY];
|
|
521
|
+
}
|
|
522
|
+
/** Queue one cold fold unless it is already queued, running, or cooling down. */
|
|
523
|
+
enqueue(candidate) {
|
|
524
|
+
const id = candidate.header.id;
|
|
525
|
+
if (this.folding.has(id) || this.queued.some((entry) => entry.header.id === id)) return;
|
|
526
|
+
const failedAt = this.failures.get(id);
|
|
527
|
+
if (failedAt !== void 0 && Date.now() - failedAt < RETRY_COOLDOWN_MS) return;
|
|
528
|
+
this.queued.push(candidate);
|
|
529
|
+
this.pump();
|
|
530
|
+
}
|
|
531
|
+
/** Keep at most {@link FOLD_CONCURRENCY} folds in flight. */
|
|
532
|
+
pump() {
|
|
533
|
+
while (!this.disposed && this.running < FOLD_CONCURRENCY) {
|
|
534
|
+
const candidate = this.queued.shift();
|
|
535
|
+
if (candidate === void 0) return;
|
|
536
|
+
const id = candidate.header.id;
|
|
537
|
+
this.folding.add(id);
|
|
538
|
+
this.running += 1;
|
|
539
|
+
void this.fold(candidate).finally(() => {
|
|
540
|
+
this.folding.delete(id);
|
|
541
|
+
this.running -= 1;
|
|
542
|
+
this.publish();
|
|
543
|
+
this.pump();
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
/** Fold one cold session once, refreshing its durable checkpoint. */
|
|
548
|
+
async fold(candidate) {
|
|
549
|
+
const persistence = this.ctx.get("sessionPersistence");
|
|
550
|
+
const cache = this.ctx.get("sessionProjectionCache");
|
|
551
|
+
if (persistence === void 0 || cache === void 0) return;
|
|
552
|
+
let handle;
|
|
553
|
+
try {
|
|
554
|
+
handle = await persistence.open(candidate.header.id, "read");
|
|
555
|
+
const { events } = await handle.read(0);
|
|
556
|
+
const block = cache.coldSnapshot(handle.header, handle.inheritedEventCount, events);
|
|
557
|
+
const value = block.values[USAGE_KEY];
|
|
558
|
+
if (this.disposed) return;
|
|
559
|
+
if (value === void 0) {
|
|
560
|
+
this.failures.set(candidate.header.id, Date.now());
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
this.values.set(candidate.header.id, value);
|
|
564
|
+
this.failures.delete(candidate.header.id);
|
|
565
|
+
} catch (error) {
|
|
566
|
+
this.failures.set(candidate.header.id, Date.now());
|
|
567
|
+
this.warn(`fold failed for session ${candidate.header.id}`, error);
|
|
568
|
+
} finally {
|
|
569
|
+
if (handle !== void 0) {
|
|
570
|
+
try {
|
|
571
|
+
await handle.close();
|
|
572
|
+
} catch {
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
/** Recompute the snapshot from the values read so far. */
|
|
578
|
+
publish() {
|
|
579
|
+
if (this.disposed) return;
|
|
580
|
+
this.snapshot = aggregateUsage(this.values.values(), {
|
|
581
|
+
now: Date.now(),
|
|
582
|
+
pending: this.queued.length + this.running,
|
|
583
|
+
missed: this.failures.size
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
function mountStatistics(ctx, isEnabled) {
|
|
588
|
+
ctx.inject(["sessionProjections", "connection"], (injected) => {
|
|
589
|
+
if (!isEnabled()) return;
|
|
590
|
+
const projections = injected.get("sessionProjections");
|
|
591
|
+
const routes = injected.get("connection");
|
|
592
|
+
if (projections === void 0 || routes === void 0) return;
|
|
593
|
+
const warn = (message, error) => {
|
|
594
|
+
try {
|
|
595
|
+
console.warn(`[${PLUGIN_ID}] statistics: ${message}`, error);
|
|
596
|
+
} catch {
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
projections.register(usageProjection);
|
|
600
|
+
const aggregator = new UsageAggregator(injected, warn);
|
|
601
|
+
ctx.effect(() => routes.fetch.register({
|
|
602
|
+
path: STATS_PATH,
|
|
603
|
+
methods: ["GET"],
|
|
604
|
+
requestBody: "buffered",
|
|
605
|
+
fetch: async () => {
|
|
606
|
+
const snapshot = await aggregator.refresh();
|
|
607
|
+
return new Response(JSON.stringify(snapshot), {
|
|
608
|
+
headers: {
|
|
609
|
+
"content-type": "application/json; charset=utf-8",
|
|
610
|
+
"cache-control": "no-store"
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
}), "statistics: snapshot route");
|
|
615
|
+
ctx.effect(() => () => aggregator.dispose(), "statistics: aggregation");
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// src/host/index.ts
|
|
620
|
+
var HEX_COLOUR = /^(#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6})?$/;
|
|
621
|
+
var FONT_NAME = new RegExp(`^[\\p{L}\\p{N} ._-]{0,${FONT_NAME_MAX_LENGTH}}$`, "u");
|
|
622
|
+
var Config = z.object({
|
|
623
|
+
enabled: z.boolean().default(false).description("\u542F\u7528 CCD \u98CE\u683C\u754C\u9762").volatile(),
|
|
624
|
+
debug: z.boolean().default(false).description("\u5728\u63A7\u5236\u53F0\u8F93\u51FA\u8C03\u8BD5\u65E5\u5FD7").volatile(),
|
|
625
|
+
features: z.object({
|
|
626
|
+
shell: z.boolean().default(DEFAULT_FEATURES.shell),
|
|
627
|
+
sidebar: z.boolean().default(DEFAULT_FEATURES.sidebar),
|
|
628
|
+
"new-session": z.boolean().default(DEFAULT_FEATURES["new-session"]),
|
|
629
|
+
conversation: z.boolean().default(DEFAULT_FEATURES.conversation),
|
|
630
|
+
"tool-calls": z.boolean().default(DEFAULT_FEATURES["tool-calls"]),
|
|
631
|
+
statistics: z.boolean().default(DEFAULT_FEATURES.statistics),
|
|
632
|
+
"composer-pet": z.boolean().default(DEFAULT_FEATURES["composer-pet"])
|
|
633
|
+
}).default({ ...DEFAULT_FEATURES }).description("\u6309\u6A21\u5757\u542F\u7528").volatile(),
|
|
634
|
+
/* One volatile node per section, exactly like `features`: Cordis rejects a
|
|
635
|
+
volatile field nested inside another volatile field ("volatile fields
|
|
636
|
+
require a fixed object path without an enclosing volatile field"), and a
|
|
637
|
+
volatile object already makes its whole subtree editable. */
|
|
638
|
+
appearance: z.object({
|
|
639
|
+
canvas: z.string().default(DEFAULT_APPEARANCE.canvas).description("\u4F1A\u8BDD\u4E0E\u753B\u5E03\u80CC\u666F\u8272\uFF08#rrggbb\uFF0C\u7559\u7A7A\u7528\u5185\u7F6E\u503C\uFF09").pattern(HEX_COLOUR),
|
|
640
|
+
sidebar: z.string().default(DEFAULT_APPEARANCE.sidebar).description("\u4FA7\u680F\u80CC\u666F\u8272\uFF08#rrggbb\uFF0C\u7559\u7A7A\u7528\u5185\u7F6E\u503C\uFF09").pattern(HEX_COLOUR)
|
|
641
|
+
}).default({ ...DEFAULT_APPEARANCE }).description("\u80CC\u666F\u8272").volatile(),
|
|
642
|
+
fonts: z.object({
|
|
643
|
+
uiLatin: z.string().default(DEFAULT_FONTS.uiLatin).description("\u754C\u9762\u897F\u6587\u5B57\u4F53\u540D\uFF08\u7559\u7A7A\u7528\u7CFB\u7EDF\u6808\uFF09").pattern(FONT_NAME),
|
|
644
|
+
uiCjk: z.string().default(DEFAULT_FONTS.uiCjk).description("\u754C\u9762\u4E2D\u6587\u5B57\u4F53\u540D\uFF08\u7559\u7A7A\u7528\u7CFB\u7EDF\u6808\uFF09").pattern(FONT_NAME),
|
|
645
|
+
code: z.string().default(DEFAULT_FONTS.code).description("\u4EE3\u7801\u4E0E\u7B49\u5BBD\u5B57\u4F53\u540D\uFF08\u7559\u7A7A\u7528\u7CFB\u7EDF\u6808\uFF09").pattern(FONT_NAME)
|
|
646
|
+
}).default({ ...DEFAULT_FONTS }).description("\u5B57\u4F53").volatile()
|
|
647
|
+
});
|
|
648
|
+
function readValue(node, fallback) {
|
|
649
|
+
if (node === void 0) return fallback;
|
|
650
|
+
if (typeof node === "object" && node !== null && typeof node.get === "function") {
|
|
651
|
+
try {
|
|
652
|
+
return node.get();
|
|
653
|
+
} catch {
|
|
654
|
+
return fallback;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return node;
|
|
658
|
+
}
|
|
659
|
+
function apply(ctx, config) {
|
|
660
|
+
const host = typeof config === "object" && config !== null ? config : {};
|
|
661
|
+
mountStatistics(ctx, () => readValue(host.enabled, false) === true);
|
|
662
|
+
}
|
|
663
|
+
export {
|
|
664
|
+
Config,
|
|
665
|
+
apply
|
|
666
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
/**
|
|
3
|
+
* Cordis services, not manifest package-name edges, control activation.
|
|
4
|
+
*
|
|
5
|
+
* `sessions` is the Session Controller's catalog store: the sidebar column
|
|
6
|
+
* reads its `blank` flag to keep a provisional New Session row out of the list.
|
|
7
|
+
* It is a prerequisite of `uiWorkspace` already, and Cordis refuses to resolve
|
|
8
|
+
* a service that is not declared here, so the dependency is named rather than
|
|
9
|
+
* discovered at the point of use.
|
|
10
|
+
*/
|
|
11
|
+
export declare const inject: string[];
|
|
12
|
+
/**
|
|
13
|
+
* The only assembly point allowed to import multiple feature domains.
|
|
14
|
+
*
|
|
15
|
+
* The loader creates client entries without configuration, so this plugin reads
|
|
16
|
+
* its own Host-served section through the settings transport rather than a
|
|
17
|
+
* second `apply` argument: `configForms.get(entryId)`. That keeps one source of
|
|
18
|
+
* truth for the switch and lets enable/disable take effect without a reload.
|
|
19
|
+
* The configuration page is registered on the outer scope so it survives the
|
|
20
|
+
* switch being turned off — otherwise the interface could never be turned back
|
|
21
|
+
* on from inside the app.
|
|
22
|
+
*
|
|
23
|
+
* @param ctx - client root context carrying the injected services.
|
|
24
|
+
*/
|
|
25
|
+
export declare function apply(ctx: Context): void;
|