dsh-claude-desktop-theme 0.2.0 → 0.3.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/README.md +69 -51
- package/lib/client.js +2342 -270
- package/lib/index.js +387 -20
- package/lib/types/client/compat/adapter.d.ts +22 -1
- package/lib/types/client/compat/composer-menus.d.ts +6 -3
- package/lib/types/client/compat/context-panel.d.ts +47 -0
- package/lib/types/client/compat/empty-groups.d.ts +46 -0
- package/lib/types/client/compat/host-dom.d.ts +64 -0
- package/lib/types/client/compat/permission-menu.d.ts +88 -0
- package/lib/types/client/compat/sidebar-navigation.d.ts +11 -0
- package/lib/types/client/compat/stats-values.d.ts +18 -1
- package/lib/types/client/compat/view-options.d.ts +58 -0
- package/lib/types/client/contracts/ports.d.ts +147 -0
- package/lib/types/client/features/context-panel/ContextPanel.d.ts +12 -0
- package/lib/types/client/features/context-panel/mount.d.ts +20 -0
- package/lib/types/client/features/settings/locales.d.ts +7 -0
- package/lib/types/client/features/sidebar/navigation-icons.d.ts +6 -0
- package/lib/types/client/features/view-options/ViewOptionsCard.d.ts +30 -0
- package/lib/types/client/features/view-options/mount.d.ts +25 -0
- package/lib/types/host/context/compaction.d.ts +26 -0
- package/lib/types/host/context/pricing.d.ts +99 -0
- package/lib/types/host/context/service.d.ts +24 -0
- package/lib/types/host/context/unit.d.ts +136 -0
- package/lib/types/host/index.d.ts +18 -0
- package/lib/types/shared/config.d.ts +26 -1
- package/lib/types/shared/context.d.ts +243 -0
- package/package.json +6 -3
package/lib/index.js
CHANGED
|
@@ -9,7 +9,9 @@ var DEFAULT_FEATURES = Object.freeze({
|
|
|
9
9
|
conversation: true,
|
|
10
10
|
"tool-calls": false,
|
|
11
11
|
statistics: false,
|
|
12
|
-
"composer-pet": true
|
|
12
|
+
"composer-pet": true,
|
|
13
|
+
"composer-stats": false,
|
|
14
|
+
"context-panel": true
|
|
13
15
|
});
|
|
14
16
|
var DEFAULT_APPEARANCE = Object.freeze({
|
|
15
17
|
canvas: "",
|
|
@@ -20,6 +22,9 @@ var DEFAULT_FONTS = Object.freeze({
|
|
|
20
22
|
uiCjk: "",
|
|
21
23
|
code: ""
|
|
22
24
|
});
|
|
25
|
+
var DEFAULT_VIEW = Object.freeze({
|
|
26
|
+
showEmptyGroups: false
|
|
27
|
+
});
|
|
23
28
|
var FONT_NAME_MAX_LENGTH = 64;
|
|
24
29
|
|
|
25
30
|
// src/shared/stats.ts
|
|
@@ -126,13 +131,13 @@ function applyUsageEvent(state, event) {
|
|
|
126
131
|
return state;
|
|
127
132
|
}
|
|
128
133
|
function pairs(record) {
|
|
129
|
-
return Object.entries(record).filter(([,
|
|
134
|
+
return Object.entries(record).filter(([, count4]) => count4 > 0).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
|
|
130
135
|
}
|
|
131
136
|
function hourPairs(hours) {
|
|
132
137
|
const sparse = [];
|
|
133
138
|
for (let hour = 0; hour < hours.length; hour += 1) {
|
|
134
|
-
const
|
|
135
|
-
if (
|
|
139
|
+
const count4 = hours[hour] ?? 0;
|
|
140
|
+
if (count4 > 0) sparse.push([hour, count4]);
|
|
136
141
|
}
|
|
137
142
|
return sparse;
|
|
138
143
|
}
|
|
@@ -295,11 +300,11 @@ var byTokens = (left, right) => right[1] - left[1] || byKey(left[0], right[0]);
|
|
|
295
300
|
function peak(counts, skip) {
|
|
296
301
|
let best = null;
|
|
297
302
|
let bestCount = 0;
|
|
298
|
-
for (const [key,
|
|
299
|
-
if (key === skip ||
|
|
300
|
-
if (
|
|
303
|
+
for (const [key, count4] of counts) {
|
|
304
|
+
if (key === skip || count4 <= 0) continue;
|
|
305
|
+
if (count4 > bestCount || count4 === bestCount && best !== null && key < best) {
|
|
301
306
|
best = key;
|
|
302
|
-
bestCount =
|
|
307
|
+
bestCount = count4;
|
|
303
308
|
}
|
|
304
309
|
}
|
|
305
310
|
return best;
|
|
@@ -307,11 +312,11 @@ function peak(counts, skip) {
|
|
|
307
312
|
function collect(value, collected) {
|
|
308
313
|
if (value.prompts <= 0 && value.tokens <= 0) return;
|
|
309
314
|
const dates = /* @__PURE__ */ new Set();
|
|
310
|
-
for (const [date,
|
|
315
|
+
for (const [date, count4] of value.days) {
|
|
311
316
|
dates.add(date);
|
|
312
|
-
collected.prompts.set(date, (collected.prompts.get(date) ?? 0) +
|
|
317
|
+
collected.prompts.set(date, (collected.prompts.get(date) ?? 0) + count4);
|
|
313
318
|
}
|
|
314
|
-
for (const [hour,
|
|
319
|
+
for (const [hour, count4] of value.hours) collected.hours.set(hour, (collected.hours.get(hour) ?? 0) + count4);
|
|
315
320
|
for (const [model, tokens, input, output] of value.models) {
|
|
316
321
|
const previous = collected.modelUsage.get(model) ?? { t: 0, i: 0, o: 0 };
|
|
317
322
|
collected.modelUsage.set(model, {
|
|
@@ -342,9 +347,9 @@ function totalsFor(collected, start) {
|
|
|
342
347
|
}
|
|
343
348
|
const active = /* @__PURE__ */ new Set();
|
|
344
349
|
let messages = 0;
|
|
345
|
-
for (const [date,
|
|
350
|
+
for (const [date, count4] of collected.prompts) {
|
|
346
351
|
if (!inRange(date)) continue;
|
|
347
|
-
messages +=
|
|
352
|
+
messages += count4;
|
|
348
353
|
active.add(date);
|
|
349
354
|
}
|
|
350
355
|
const models = /* @__PURE__ */ new Map();
|
|
@@ -383,8 +388,8 @@ function aggregateUsage(values, options) {
|
|
|
383
388
|
const days = [];
|
|
384
389
|
for (const date of dates) {
|
|
385
390
|
if (cutoff !== null && date < cutoff) continue;
|
|
386
|
-
const
|
|
387
|
-
if (
|
|
391
|
+
const count4 = collected.prompts.get(date) ?? 0;
|
|
392
|
+
if (count4 > 0) days.push([date, count4]);
|
|
388
393
|
}
|
|
389
394
|
const modelDays = [];
|
|
390
395
|
for (const [date, perModel] of [...collected.modelDays.entries()].sort(([left], [right]) => byKey(left, right))) {
|
|
@@ -397,10 +402,10 @@ function aggregateUsage(values, options) {
|
|
|
397
402
|
let busyHour = null;
|
|
398
403
|
let busyCount = 0;
|
|
399
404
|
for (let hour = 0; hour < HOURS_PER_DAY; hour += 1) {
|
|
400
|
-
const
|
|
401
|
-
if (
|
|
405
|
+
const count4 = collected.hours.get(hour) ?? 0;
|
|
406
|
+
if (count4 > busyCount) {
|
|
402
407
|
busyHour = hour;
|
|
403
|
-
busyCount =
|
|
408
|
+
busyCount = count4;
|
|
404
409
|
}
|
|
405
410
|
}
|
|
406
411
|
return {
|
|
@@ -616,6 +621,362 @@ function mountStatistics(ctx, isEnabled) {
|
|
|
616
621
|
});
|
|
617
622
|
}
|
|
618
623
|
|
|
624
|
+
// src/shared/context.ts
|
|
625
|
+
var CONTEXT_KEY = "ccdContext";
|
|
626
|
+
var CONTEXT_STATE_VERSION = 1;
|
|
627
|
+
var CHARS_PER_TOKEN = 4;
|
|
628
|
+
var BLOCK_OVERHEAD = 4;
|
|
629
|
+
var DEFAULT_THRESHOLD_RATIO = 0.8;
|
|
630
|
+
var DEFAULT_HEADROOM_TOKENS = 65536;
|
|
631
|
+
var CONTEXT_ENTRY_LIMIT = 200;
|
|
632
|
+
function count2(value) {
|
|
633
|
+
return Number.isFinite(value) && value > 0 ? value : 0;
|
|
634
|
+
}
|
|
635
|
+
function readCount(value) {
|
|
636
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : null;
|
|
637
|
+
}
|
|
638
|
+
function readEntries(value) {
|
|
639
|
+
if (!Array.isArray(value)) return [];
|
|
640
|
+
const entries = [];
|
|
641
|
+
for (const entry of value) {
|
|
642
|
+
if (!Array.isArray(entry) || entry.length < 2) continue;
|
|
643
|
+
const [name, tokens] = entry;
|
|
644
|
+
if (typeof name !== "string" || name === "") continue;
|
|
645
|
+
const priced = typeof tokens === "number" && Number.isFinite(tokens) && tokens > 0 ? tokens : 0;
|
|
646
|
+
if (priced === 0) continue;
|
|
647
|
+
entries.push([name, priced]);
|
|
648
|
+
}
|
|
649
|
+
return entries;
|
|
650
|
+
}
|
|
651
|
+
function readContextView(value) {
|
|
652
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
653
|
+
const raw = value;
|
|
654
|
+
return {
|
|
655
|
+
tools: readEntries(raw["tools"]),
|
|
656
|
+
files: readEntries(raw["files"]),
|
|
657
|
+
skills: readEntries(raw["skills"]),
|
|
658
|
+
skillTokens: count2(typeof raw["skillTokens"] === "number" ? raw["skillTokens"] : 0),
|
|
659
|
+
fileTokens: count2(typeof raw["fileTokens"] === "number" ? raw["fileTokens"] : 0),
|
|
660
|
+
compaction: readCount(raw["compaction"])
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
function emptyContextView() {
|
|
664
|
+
return { tools: [], files: [], skills: [], skillTokens: 0, fileTokens: 0, compaction: null };
|
|
665
|
+
}
|
|
666
|
+
function compactionThreshold(window, reserved, ratio = DEFAULT_THRESHOLD_RATIO, headroom = DEFAULT_HEADROOM_TOKENS) {
|
|
667
|
+
const capacity = count2(window);
|
|
668
|
+
if (capacity === 0 || reserved === null || !Number.isFinite(reserved) || reserved < 0) return null;
|
|
669
|
+
const pressureBudget = capacity - reserved - count2(headroom);
|
|
670
|
+
if (pressureBudget <= 0) return null;
|
|
671
|
+
return Math.floor(Math.min(capacity * ratio, pressureBudget));
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// src/host/context/compaction.ts
|
|
675
|
+
var DEFAULT_BACKEND = "@deepseek-ai/dsh-compaction-basic";
|
|
676
|
+
function ratioOf(value) {
|
|
677
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= 1 ? value : null;
|
|
678
|
+
}
|
|
679
|
+
function headroomOf(value) {
|
|
680
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : null;
|
|
681
|
+
}
|
|
682
|
+
function policyOf(entry) {
|
|
683
|
+
const config = entry.options?.config;
|
|
684
|
+
const record = typeof config === "object" && config !== null ? config : {};
|
|
685
|
+
const ratio = ratioOf(record["thresholdRatio"]);
|
|
686
|
+
const headroom = headroomOf(record["headroomTokens"]);
|
|
687
|
+
const named = entry.options?.name === DEFAULT_BACKEND;
|
|
688
|
+
if (ratio === null && headroom === null && !named) return null;
|
|
689
|
+
return {
|
|
690
|
+
ratio: ratio ?? DEFAULT_THRESHOLD_RATIO,
|
|
691
|
+
headroom: headroom ?? DEFAULT_HEADROOM_TOKENS
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
function readCompactionPolicy(ctx) {
|
|
695
|
+
const loader = ctx.get("loader");
|
|
696
|
+
if (loader === void 0 || typeof loader.entries !== "function") return null;
|
|
697
|
+
try {
|
|
698
|
+
let found = null;
|
|
699
|
+
for (const entry of loader.entries()) {
|
|
700
|
+
if (entry.disabled === true) continue;
|
|
701
|
+
const policy = policyOf(entry);
|
|
702
|
+
if (policy !== null) found = policy;
|
|
703
|
+
}
|
|
704
|
+
return found;
|
|
705
|
+
} catch {
|
|
706
|
+
return null;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// src/host/context/pricing.ts
|
|
711
|
+
var SURFACE_TYPES = /* @__PURE__ */ new Set([
|
|
712
|
+
"system/message",
|
|
713
|
+
"developer/message",
|
|
714
|
+
"user/message",
|
|
715
|
+
"assistant/message",
|
|
716
|
+
"tool/result"
|
|
717
|
+
]);
|
|
718
|
+
function priceContent(blocks) {
|
|
719
|
+
let tokens = 0;
|
|
720
|
+
for (const block of blocks) {
|
|
721
|
+
if (block.type === "text" || block.type === "reasoning") {
|
|
722
|
+
tokens += Math.ceil((block.text ?? "").length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD;
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
if (block.type === "tool-call") {
|
|
726
|
+
tokens += Math.ceil((block.name ?? "").length / CHARS_PER_TOKEN) + Math.ceil((block.arguments ?? "").length / CHARS_PER_TOKEN) + BLOCK_OVERHEAD;
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
tokens += BLOCK_OVERHEAD + Math.ceil(JSON.stringify(block).length / CHARS_PER_TOKEN);
|
|
730
|
+
}
|
|
731
|
+
return tokens;
|
|
732
|
+
}
|
|
733
|
+
function priceMessage(message) {
|
|
734
|
+
if (message === null) return 0;
|
|
735
|
+
if (message.role === "system") {
|
|
736
|
+
if (message.content.length === 0) return 0;
|
|
737
|
+
let characters = 0;
|
|
738
|
+
for (const block of message.content) {
|
|
739
|
+
characters += block.type === "text" ? (block.text ?? "").length : JSON.stringify(block).length;
|
|
740
|
+
}
|
|
741
|
+
return Math.ceil(characters / CHARS_PER_TOKEN) + BLOCK_OVERHEAD;
|
|
742
|
+
}
|
|
743
|
+
return priceContent(message.content) + BLOCK_OVERHEAD;
|
|
744
|
+
}
|
|
745
|
+
function derivedMessage(event) {
|
|
746
|
+
switch (event.type) {
|
|
747
|
+
case "user/message":
|
|
748
|
+
return { content: event.data?.content ?? [] };
|
|
749
|
+
case "system/message":
|
|
750
|
+
case "developer/message":
|
|
751
|
+
case "assistant/message": {
|
|
752
|
+
const message = event.data?.message;
|
|
753
|
+
if (message === void 0 || message.content.length === 0) return null;
|
|
754
|
+
return message;
|
|
755
|
+
}
|
|
756
|
+
case "tool/result":
|
|
757
|
+
return event.data?.message ?? null;
|
|
758
|
+
default:
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
function isSurfaceEvent(event) {
|
|
763
|
+
return SURFACE_TYPES.has(event.type) && event.surfaceOp !== void 0;
|
|
764
|
+
}
|
|
765
|
+
function priceTool(tool) {
|
|
766
|
+
return Math.ceil(JSON.stringify(tool).length / CHARS_PER_TOKEN);
|
|
767
|
+
}
|
|
768
|
+
function allocate(total, weights) {
|
|
769
|
+
if (weights.length === 0) return [];
|
|
770
|
+
const safe = weights.map((weight) => Number.isFinite(weight) && weight > 0 ? weight : 1);
|
|
771
|
+
const sum = safe.reduce((running, weight) => running + weight, 0);
|
|
772
|
+
const out = [];
|
|
773
|
+
let placed = 0;
|
|
774
|
+
for (let index = 0; index < safe.length - 1; index += 1) {
|
|
775
|
+
const share = Math.round(total * safe[index] / sum);
|
|
776
|
+
out.push(share);
|
|
777
|
+
placed += share;
|
|
778
|
+
}
|
|
779
|
+
out.push(Math.max(0, total - placed));
|
|
780
|
+
return out;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// src/host/context/unit.ts
|
|
784
|
+
var KIND_OTHER = 0;
|
|
785
|
+
var KIND_INSTRUCTIONS = 1;
|
|
786
|
+
var KIND_SKILLS = 2;
|
|
787
|
+
function initContextState() {
|
|
788
|
+
return { nodes: [], tools: [], window: 0, reserved: null };
|
|
789
|
+
}
|
|
790
|
+
function count3(value) {
|
|
791
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
|
|
792
|
+
}
|
|
793
|
+
function optionalCount(value) {
|
|
794
|
+
return readCount(value);
|
|
795
|
+
}
|
|
796
|
+
function textOf(event) {
|
|
797
|
+
return (event.data?.content ?? []).map((block) => block.type === "text" ? block.text ?? "" : "").join("\n");
|
|
798
|
+
}
|
|
799
|
+
function sectionStart(text, path) {
|
|
800
|
+
const needles = [
|
|
801
|
+
`Instructions from: ${path}`,
|
|
802
|
+
`Additional instructions from: ${path}`,
|
|
803
|
+
`Updated instructions from: ${path}`
|
|
804
|
+
];
|
|
805
|
+
let found = -1;
|
|
806
|
+
for (const needle of needles) {
|
|
807
|
+
const at = text.indexOf(needle);
|
|
808
|
+
if (at >= 0 && (found < 0 || at < found)) found = at;
|
|
809
|
+
}
|
|
810
|
+
return found;
|
|
811
|
+
}
|
|
812
|
+
function nextSection(text, from) {
|
|
813
|
+
const marker = /^(?:Additional |Updated )?[Ii]nstructions from: /m;
|
|
814
|
+
const rest = text.slice(from);
|
|
815
|
+
const at = marker.exec(rest);
|
|
816
|
+
return at === null ? text.length : from + at.index;
|
|
817
|
+
}
|
|
818
|
+
function instructionEntries(event, tokens) {
|
|
819
|
+
const text = textOf(event);
|
|
820
|
+
const weights = /* @__PURE__ */ new Map();
|
|
821
|
+
for (const change of event.data?.source?.changes ?? []) {
|
|
822
|
+
const path = change.path;
|
|
823
|
+
if (typeof path !== "string" || path === "" || change.action === "remove") continue;
|
|
824
|
+
const at = sectionStart(text, path);
|
|
825
|
+
const weight = at < 0 ? 1 : Math.max(1, nextSection(text, at + 1) - at);
|
|
826
|
+
weights.set(path, (weights.get(path) ?? 0) + weight);
|
|
827
|
+
}
|
|
828
|
+
const paths = [...weights.keys()];
|
|
829
|
+
if (paths.length === 0) return [];
|
|
830
|
+
const shares = allocate(tokens, paths.map((path) => weights.get(path) ?? 1));
|
|
831
|
+
return paths.map((path, index) => [path, shares[index] ?? 0]);
|
|
832
|
+
}
|
|
833
|
+
function skillEntries(event, tokens) {
|
|
834
|
+
const listed = event.data?.source?.entries ?? [];
|
|
835
|
+
const named = [];
|
|
836
|
+
for (const entry of listed) {
|
|
837
|
+
const name = entry.name;
|
|
838
|
+
if (typeof name !== "string" || name === "") continue;
|
|
839
|
+
const weight = name.length + (entry.description?.length ?? 0);
|
|
840
|
+
const seen = named.find((candidate) => candidate.name === name);
|
|
841
|
+
if (seen === void 0) named.push({ name, weight });
|
|
842
|
+
else seen.weight += weight;
|
|
843
|
+
}
|
|
844
|
+
if (named.length === 0) return [];
|
|
845
|
+
const shares = allocate(tokens, named.map((entry) => entry.weight));
|
|
846
|
+
return named.map((entry, index) => [entry.name, shares[index] ?? 0]);
|
|
847
|
+
}
|
|
848
|
+
function kindOf(event) {
|
|
849
|
+
if (event.type !== "user/message") return KIND_OTHER;
|
|
850
|
+
const kind = event.data?.source?.kind;
|
|
851
|
+
if (kind === "agent-instructions") return KIND_INSTRUCTIONS;
|
|
852
|
+
if (kind === "skill-catalog") return KIND_SKILLS;
|
|
853
|
+
return KIND_OTHER;
|
|
854
|
+
}
|
|
855
|
+
function pricedTools(tools) {
|
|
856
|
+
if (tools === void 0 || tools.length === 0) return [];
|
|
857
|
+
return tools.map((tool) => [tool.name, priceTool(tool)]).sort((left, right) => right[1] - left[1] || (left[0] < right[0] ? -1 : left[0] > right[0] ? 1 : 0)).slice(0, CONTEXT_ENTRY_LIMIT);
|
|
858
|
+
}
|
|
859
|
+
function sameTools(left, right) {
|
|
860
|
+
if (left.length !== right.length) return false;
|
|
861
|
+
return left.every((entry, index) => entry[0] === right[index]?.[0] && entry[1] === right[index]?.[1]);
|
|
862
|
+
}
|
|
863
|
+
function applyContextEvent(state, event) {
|
|
864
|
+
if (event.type === "request/header") {
|
|
865
|
+
const header = event.data?.header;
|
|
866
|
+
const tools = pricedTools(header?.tools);
|
|
867
|
+
const reserved = optionalCount(header?.config?.maxTokens);
|
|
868
|
+
return sameTools(tools, state.tools) && reserved === state.reserved ? state : { ...state, tools, reserved };
|
|
869
|
+
}
|
|
870
|
+
if (event.type === "request/context") {
|
|
871
|
+
const window = count3(event.data?.contextWindow);
|
|
872
|
+
return window === state.window ? state : { ...state, window };
|
|
873
|
+
}
|
|
874
|
+
if (!isSurfaceEvent(event)) return state;
|
|
875
|
+
const tokens = priceMessage(derivedMessage(event));
|
|
876
|
+
const kind = kindOf(event);
|
|
877
|
+
const entries = kind === KIND_INSTRUCTIONS ? instructionEntries(event, tokens) : kind === KIND_SKILLS ? skillEntries(event, tokens) : [];
|
|
878
|
+
const node = [event.seq, tokens, kind, entries];
|
|
879
|
+
const op = event.surfaceOp;
|
|
880
|
+
if (op === void 0 || op === "append") {
|
|
881
|
+
return { ...state, nodes: [...state.nodes, node] };
|
|
882
|
+
}
|
|
883
|
+
const start = state.nodes.findIndex((candidate) => candidate[0] === op.startSeq);
|
|
884
|
+
const end = state.nodes.findIndex((candidate) => candidate[0] === op.endSeq);
|
|
885
|
+
if (start < 0 || end < 0 || start > end) return { ...state, nodes: [...state.nodes, node] };
|
|
886
|
+
const nodes = state.nodes.slice();
|
|
887
|
+
nodes.splice(start, end - start + 1, node);
|
|
888
|
+
return { ...state, nodes };
|
|
889
|
+
}
|
|
890
|
+
function mergeEntries(nodes, kind) {
|
|
891
|
+
const merged = /* @__PURE__ */ new Map();
|
|
892
|
+
for (const node of nodes) {
|
|
893
|
+
if (node[2] !== kind) continue;
|
|
894
|
+
for (const [name, tokens] of node[3]) merged.set(name, (merged.get(name) ?? 0) + tokens);
|
|
895
|
+
}
|
|
896
|
+
return [...merged].filter(([, tokens]) => tokens > 0).sort((left, right) => right[1] - left[1] || (left[0] < right[0] ? -1 : left[0] > right[0] ? 1 : 0)).slice(0, CONTEXT_ENTRY_LIMIT).map(([name, tokens]) => [name, tokens]);
|
|
897
|
+
}
|
|
898
|
+
function sumKind(nodes, kind) {
|
|
899
|
+
let tokens = 0;
|
|
900
|
+
for (const node of nodes) if (node[2] === kind) tokens += node[1];
|
|
901
|
+
return tokens;
|
|
902
|
+
}
|
|
903
|
+
function viewContext(state, policy) {
|
|
904
|
+
return {
|
|
905
|
+
tools: state.tools,
|
|
906
|
+
files: mergeEntries(state.nodes, KIND_INSTRUCTIONS),
|
|
907
|
+
skills: mergeEntries(state.nodes, KIND_SKILLS),
|
|
908
|
+
skillTokens: sumKind(state.nodes, KIND_SKILLS),
|
|
909
|
+
fileTokens: sumKind(state.nodes, KIND_INSTRUCTIONS),
|
|
910
|
+
compaction: policy === null ? null : compactionThreshold(state.window, state.reserved, policy.ratio, policy.headroom)
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
function schemaOf2(parse) {
|
|
914
|
+
return { parse };
|
|
915
|
+
}
|
|
916
|
+
function readContextState(value) {
|
|
917
|
+
const raw = typeof value === "object" && value !== null ? value : {};
|
|
918
|
+
const nodes = [];
|
|
919
|
+
if (Array.isArray(raw["nodes"])) {
|
|
920
|
+
for (const entry of raw["nodes"]) {
|
|
921
|
+
if (!Array.isArray(entry) || entry.length < 3) continue;
|
|
922
|
+
const [seq, tokens, kind, entries] = entry;
|
|
923
|
+
if (typeof seq !== "number" || !Number.isInteger(seq) || seq < 0) continue;
|
|
924
|
+
nodes.push([
|
|
925
|
+
seq,
|
|
926
|
+
count3(tokens),
|
|
927
|
+
typeof kind === "number" && Number.isInteger(kind) ? kind : KIND_OTHER,
|
|
928
|
+
readEntries(entries)
|
|
929
|
+
]);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
return {
|
|
933
|
+
nodes,
|
|
934
|
+
tools: readEntries(raw["tools"]),
|
|
935
|
+
window: count3(raw["window"]),
|
|
936
|
+
reserved: optionalCount(raw["reserved"])
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
function readContextView2(value) {
|
|
940
|
+
return readContextView(value) ?? emptyContextView();
|
|
941
|
+
}
|
|
942
|
+
function createContextProjection(policy) {
|
|
943
|
+
const views2 = /* @__PURE__ */ new WeakMap();
|
|
944
|
+
return {
|
|
945
|
+
key: CONTEXT_KEY,
|
|
946
|
+
stateSchema: schemaOf2(readContextState),
|
|
947
|
+
init: () => initContextState(),
|
|
948
|
+
apply: (state, event) => applyContextEvent(state, event),
|
|
949
|
+
wire: {
|
|
950
|
+
viewSchema: schemaOf2(readContextView2),
|
|
951
|
+
view(state) {
|
|
952
|
+
const cached = views2.get(state);
|
|
953
|
+
if (cached !== void 0) return cached;
|
|
954
|
+
const view = viewContext(state, policy());
|
|
955
|
+
views2.set(state, view);
|
|
956
|
+
return view;
|
|
957
|
+
}
|
|
958
|
+
},
|
|
959
|
+
stateVersion: CONTEXT_STATE_VERSION
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// src/host/context/service.ts
|
|
964
|
+
function mountContextBreakdown(ctx, isEnabled) {
|
|
965
|
+
ctx.inject(["sessionProjections"], (injected) => {
|
|
966
|
+
if (!isEnabled()) return;
|
|
967
|
+
const projections = injected.get("sessionProjections");
|
|
968
|
+
if (projections === void 0) return;
|
|
969
|
+
try {
|
|
970
|
+
projections.register(createContextProjection(() => readCompactionPolicy(injected)));
|
|
971
|
+
} catch (error) {
|
|
972
|
+
try {
|
|
973
|
+
console.warn(`[${PLUGIN_ID}] context: the projection unit could not be registered`, error);
|
|
974
|
+
} catch {
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
|
|
619
980
|
// src/host/index.ts
|
|
620
981
|
var HEX_COLOUR = /^(#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6})?$/;
|
|
621
982
|
var FONT_NAME = new RegExp(`^[\\p{L}\\p{N} ._-]{0,${FONT_NAME_MAX_LENGTH}}$`, "u");
|
|
@@ -629,7 +990,9 @@ var Config = z.object({
|
|
|
629
990
|
conversation: z.boolean().default(DEFAULT_FEATURES.conversation),
|
|
630
991
|
"tool-calls": z.boolean().default(DEFAULT_FEATURES["tool-calls"]),
|
|
631
992
|
statistics: z.boolean().default(DEFAULT_FEATURES.statistics),
|
|
632
|
-
"composer-pet": z.boolean().default(DEFAULT_FEATURES["composer-pet"])
|
|
993
|
+
"composer-pet": z.boolean().default(DEFAULT_FEATURES["composer-pet"]),
|
|
994
|
+
"composer-stats": z.boolean().default(DEFAULT_FEATURES["composer-stats"]),
|
|
995
|
+
"context-panel": z.boolean().default(DEFAULT_FEATURES["context-panel"])
|
|
633
996
|
}).default({ ...DEFAULT_FEATURES }).description("\u6309\u6A21\u5757\u542F\u7528").volatile(),
|
|
634
997
|
/* One volatile node per section, exactly like `features`: Cordis rejects a
|
|
635
998
|
volatile field nested inside another volatile field ("volatile fields
|
|
@@ -643,7 +1006,10 @@ var Config = z.object({
|
|
|
643
1006
|
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
1007
|
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
1008
|
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()
|
|
1009
|
+
}).default({ ...DEFAULT_FONTS }).description("\u5B57\u4F53").volatile(),
|
|
1010
|
+
view: z.object({
|
|
1011
|
+
showEmptyGroups: z.boolean().default(DEFAULT_VIEW.showEmptyGroups).description("\u663E\u793A\u7A7A\u5206\u7EC4\uFF08\u5173\u6389\u65F6\u6CA1\u6709\u4F1A\u8BDD\u7684\u5DE5\u4F5C\u533A\u4E0D\u518D\u663E\u793A\uFF09")
|
|
1012
|
+
}).default({ ...DEFAULT_VIEW }).description("\u89C6\u56FE\u9009\u9879").volatile()
|
|
647
1013
|
});
|
|
648
1014
|
function readValue(node, fallback) {
|
|
649
1015
|
if (node === void 0) return fallback;
|
|
@@ -659,6 +1025,7 @@ function readValue(node, fallback) {
|
|
|
659
1025
|
function apply(ctx, config) {
|
|
660
1026
|
const host = typeof config === "object" && config !== null ? config : {};
|
|
661
1027
|
mountStatistics(ctx, () => readValue(host.enabled, false) === true);
|
|
1028
|
+
mountContextBreakdown(ctx, () => readValue(host.enabled, false) === true);
|
|
662
1029
|
}
|
|
663
1030
|
export {
|
|
664
1031
|
Config,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
-
import type { HostServices, ThemePreferencePort } from '../contracts/ports.ts';
|
|
2
|
+
import type { ContextProjectionPort, HostServices, SidebarSessionsPort, ThemePreferencePort } from '../contracts/ports.ts';
|
|
3
3
|
/**
|
|
4
4
|
* Build the theme service's preference face.
|
|
5
5
|
*
|
|
@@ -13,5 +13,26 @@ import type { HostServices, ThemePreferencePort } from '../contracts/ports.ts';
|
|
|
13
13
|
* @returns the port, or null when this build cannot read or write the preference.
|
|
14
14
|
*/
|
|
15
15
|
export declare function createThemePreferencePort(ctx: Context): ThemePreferencePort | null;
|
|
16
|
+
/** Read collapsed membership from the same two catalogs the host browser uses. */
|
|
17
|
+
export declare function createSidebarSessionsPort(ctx: Context): SidebarSessionsPort | null;
|
|
18
|
+
/**
|
|
19
|
+
* Build the context ring's data face over the client session list.
|
|
20
|
+
*
|
|
21
|
+
* `ctx.sessions.list` is the Session Controller's catalog store and the one
|
|
22
|
+
* place the browser sees host-computed projections: `contextPressure` is the
|
|
23
|
+
* ring's own occupancy, `contextBreakdown` its three-way composition, and
|
|
24
|
+
* `ccdContext` this plugin's rows — all of them wire values the host shipped,
|
|
25
|
+
* carried for sessions that were never opened as well as the live one. Reading
|
|
26
|
+
* them here rather than recomputing them in the browser is what keeps the
|
|
27
|
+
* panel's header identical to the ring beside it.
|
|
28
|
+
*
|
|
29
|
+
* A session with no capacity yet is null: the host's own `ContextMeter` renders
|
|
30
|
+
* nothing in that state either, so the ring the panel hangs from is not there
|
|
31
|
+
* to open.
|
|
32
|
+
*
|
|
33
|
+
* @param ctx - client context carrying the session service.
|
|
34
|
+
* @returns the port, or null when this build does not publish a session list.
|
|
35
|
+
*/
|
|
36
|
+
export declare function createContextProjections(ctx: Context): ContextProjectionPort | null;
|
|
16
37
|
/** This compatibility boundary targets the pinned SDK, not arbitrary DSH versions. */
|
|
17
38
|
export declare function createHostServices(ctx: Context): HostServices;
|
|
@@ -3,9 +3,12 @@ import type { Disposer } from '../contracts/ports.ts';
|
|
|
3
3
|
export declare const FLIPPED_MENU_ATTRIBUTE = "data-ccd-menu-flipped";
|
|
4
4
|
/**
|
|
5
5
|
* Place bottom-edge Composer and hero-chip menus above their anchors, using
|
|
6
|
-
* the Menu primitive's side=top geometry.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* the Menu primitive's side=top geometry. The side is decided from the two free
|
|
7
|
+
* spaces alone — the room above wins whenever it is the larger one — so a card
|
|
8
|
+
* short enough to fit below does not stay under a control the reference opens
|
|
9
|
+
* upwards from. Cap only cards taller than the free space above the anchor so
|
|
10
|
+
* later content growth keeps its natural height. MutationObserver and resize
|
|
11
|
+
* reapply placement when the host updates it.
|
|
9
12
|
* @returns disposer that restores each menu and releases the observers.
|
|
10
13
|
*/
|
|
11
14
|
export declare function mountComposerMenuPlacement(document: Document, report: (error: unknown) => void): Disposer;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ContextPanelPort, ContextProjectionPort } from '../contracts/ports.ts';
|
|
2
|
+
/**
|
|
3
|
+
* The context ring's breakdown panel, driven instead of replaced.
|
|
4
|
+
*
|
|
5
|
+
* DSH opens its own 264px panel from the ring's `onClick` and publishes no face
|
|
6
|
+
* that reads or drives it, so this module owns the interaction outright: it
|
|
7
|
+
* intercepts the trigger's click, holds the open and expanded state, and reads
|
|
8
|
+
* the numbers off the session list — where the host's own `contextPressure` and
|
|
9
|
+
* `contextBreakdown` projections, and this plugin's `ccdContext`, all arrive as
|
|
10
|
+
* wire values.
|
|
11
|
+
*
|
|
12
|
+
* Three details decide how it is written:
|
|
13
|
+
*
|
|
14
|
+
* - **The trigger's click is intercepted in the capture phase.** DSH's handler
|
|
15
|
+
* is React's `onClick`, delegated at the renderer root; stopping the event on
|
|
16
|
+
* the button before it bubbles keeps that handler from running, so the host's
|
|
17
|
+
* panel never opens. This is the same device `compat/view-options.ts` uses for
|
|
18
|
+
* the sidebar's menu, and it is why the host's open state never has to be
|
|
19
|
+
* driven — a driven panel would be dismissed by the host's own
|
|
20
|
+
* `useDismissOnOutsidePointer` the moment the reader clicked inside ours.
|
|
21
|
+
* - **The document element carries the open marker.** `composer.css` keys the
|
|
22
|
+
* host panel's removal off `[data-ccd-context-open]`, so a click that somehow
|
|
23
|
+
* reaches DSH's handler still leaves the reader looking at one panel. The
|
|
24
|
+
* marker is belt to the interception's braces, not the mechanism.
|
|
25
|
+
* - **A trigger that goes away closes the panel.** The ring is drawn by the
|
|
26
|
+
* Conversation page, so a session switch, a page change or a compaction that
|
|
27
|
+
* empties the reading all replace it; each of those has to put the panel away
|
|
28
|
+
* rather than leave it hanging over a control that is no longer there.
|
|
29
|
+
*/
|
|
30
|
+
/** Marks the host popup this module has taken over; the sheet keeps it off screen. */
|
|
31
|
+
export declare const OPEN_MARKER = "data-ccd-context-open";
|
|
32
|
+
/** Marks the ring trigger while this plugin's panel is the one on screen. */
|
|
33
|
+
export declare const OPEN_ATTRIBUTE = "data-ccd-context-panel";
|
|
34
|
+
/** The driver's own face, plus the release the mounting scope holds. */
|
|
35
|
+
export interface ContextPanelDriver {
|
|
36
|
+
readonly port: ContextPanelPort;
|
|
37
|
+
dispose(): void;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Watch the context ring and open this plugin's panel from it.
|
|
41
|
+
*
|
|
42
|
+
* @param document - renderer document carrying the Conversation page.
|
|
43
|
+
* @param projections - the host's context readings, off the session list.
|
|
44
|
+
* @param report - sink for observer failures; the ring keeps DSH's own panel.
|
|
45
|
+
* @returns the port the card reads through, and the release of every observer.
|
|
46
|
+
*/
|
|
47
|
+
export declare function createContextPanelDriver(document: Document, projections: ContextProjectionPort, report: (error: unknown) => void): ContextPanelDriver;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Disposer, SidebarSessionsPort, ViewOptionsSettingsPort } from '../contracts/ports.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Workspaces with nothing to show, kept out of the sidebar.
|
|
4
|
+
*
|
|
5
|
+
* The `显示空分组` row in the view-options card writes `view.showEmptyGroups`,
|
|
6
|
+
* whose default is off: a Workspace whose rows are all hidden by the current
|
|
7
|
+
* filter disappears from the column instead of standing there as a heading over
|
|
8
|
+
* nothing. With no durable history, all headings give way to a welcome prompt
|
|
9
|
+
* even if empty groups are enabled. DSH has no such setting of its own — the host draws one empty state
|
|
10
|
+
* for the whole tree (`EmptySessions`, `data-row-key="empty"`, rendered only
|
|
11
|
+
* when there are no groups at all), never one per Workspace — so this is the
|
|
12
|
+
* plugin's own behaviour and this module is where it is decided.
|
|
13
|
+
*
|
|
14
|
+
* A group is the `WorkspaceBrowser` box that holds one Workspace's row, its
|
|
15
|
+
* nested groups and its Session rows (`._7514NG_groupSection`). It is empty
|
|
16
|
+
* when nothing under it is a row the reader can see:
|
|
17
|
+
*
|
|
18
|
+
* - a Session row keeps it (except one `compat/blank-session-rows.ts` already
|
|
19
|
+
* hides — a provisional New Session is not something to show a group for);
|
|
20
|
+
* - the `overflow:` row ("Show {n} more sessions") keeps it, because it is the
|
|
21
|
+
* host saying there are more rows behind it;
|
|
22
|
+
* - a nested group's Sessions count, which is why the test walks the whole
|
|
23
|
+
* subtree rather than the direct children;
|
|
24
|
+
* - collapsed membership comes from the host catalogs and native filter;
|
|
25
|
+
* while those are loading, an unknown collapsed Workspace stays reachable.
|
|
26
|
+
*
|
|
27
|
+
* The marker is an attribute the sidebar sheet turns into `display: none`. It is
|
|
28
|
+
* written after the commit that drew the rows, which is the one thing about this
|
|
29
|
+
* module worth knowing: the host measures its keyed rows inside that commit
|
|
30
|
+
* (`blank-session-rows.ts` records the 200 ms glide that follows a late change),
|
|
31
|
+
* so a group hidden here can arrive with the rows below it already placed. The
|
|
32
|
+
* plugin's own default makes this the common path, so the first paint is checked
|
|
33
|
+
* on a real machine; if it shows, the fix is to write the same attribute from
|
|
34
|
+
* the previous pass's answer before the rows mount, not to move this test.
|
|
35
|
+
*
|
|
36
|
+
* @param document - renderer document carrying the sidebar.
|
|
37
|
+
* @param hideEmpty - whether empty groups are being kept out (the config's
|
|
38
|
+
* `view.showEmptyGroups`, inverted).
|
|
39
|
+
* @param report - sink for observer failures; the column stays as the host drew it.
|
|
40
|
+
* @returns disposer that disconnects the observer and clears every marker.
|
|
41
|
+
*/
|
|
42
|
+
export declare function mountEmptyGroups(document: Document, hideEmpty: boolean, report: (error: unknown) => void, options?: {
|
|
43
|
+
source?: SidebarSessionsPort | null;
|
|
44
|
+
settings?: ViewOptionsSettingsPort;
|
|
45
|
+
showAll?: () => void;
|
|
46
|
+
}): Disposer;
|