dsh-claude-desktop-theme 0.2.0 → 0.4.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 +76 -54
- package/lib/client.js +9392 -2579
- package/lib/index.js +387 -20
- package/lib/types/client/compat/account-menu.d.ts +4 -4
- package/lib/types/client/compat/account-more.d.ts +3 -0
- package/lib/types/client/compat/account-theme.d.ts +3 -0
- package/lib/types/client/compat/adapter.d.ts +22 -1
- package/lib/types/client/compat/browser-header.d.ts +57 -0
- package/lib/types/client/compat/browser-menu.d.ts +16 -0
- 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/files-pane.d.ts +78 -0
- package/lib/types/client/compat/header-menu-actions.d.ts +14 -0
- package/lib/types/client/compat/header-menus.d.ts +4 -0
- package/lib/types/client/compat/host-dom.d.ts +180 -0
- package/lib/types/client/compat/pane-chrome.d.ts +29 -0
- package/lib/types/client/compat/pane-icons.d.ts +23 -0
- package/lib/types/client/compat/pane-tiles.d.ts +37 -0
- package/lib/types/client/compat/permission-menu.d.ts +88 -0
- package/lib/types/client/compat/relative-file-path.d.ts +2 -0
- package/lib/types/client/compat/right-panes.d.ts +16 -0
- package/lib/types/client/compat/scroll-pill.d.ts +70 -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/compat/view-switch.d.ts +46 -5
- package/lib/types/client/contracts/ports.d.ts +176 -1
- 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/conversation/index.d.ts +5 -3
- package/lib/types/client/features/conversation/view-switch-icons.d.ts +13 -0
- package/lib/types/client/features/settings/locales.d.ts +7 -0
- package/lib/types/client/features/shell/index.d.ts +9 -3
- package/lib/types/client/features/sidebar/index.d.ts +1 -1
- package/lib/types/client/features/sidebar/navigation-icons.d.ts +6 -0
- package/lib/types/client/features/statistics/books.d.ts +1 -2
- package/lib/types/client/features/statistics/format.d.ts +4 -4
- 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/client/theme/mount.d.ts +2 -2
- package/lib/types/client/theme/palette.d.ts +1 -1
- 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,4 +1,4 @@
|
|
|
1
|
-
import type { Disposer } from '../contracts/ports.ts';
|
|
1
|
+
import type { Disposer, ThemePreferencePort } from '../contracts/ports.ts';
|
|
2
2
|
/** Marks the portalled account menu for the stylesheet. */
|
|
3
3
|
export declare const ACCOUNT_MENU_ATTRIBUTE = "data-ccd-account-menu";
|
|
4
4
|
/**
|
|
@@ -38,8 +38,8 @@ export declare function accountNameToken(name: string): string;
|
|
|
38
38
|
* the account itself (the account name over a workspace line). DSH's account
|
|
39
39
|
* menu is a portalled `Menu` of plain rows and the signed-in name only exists on
|
|
40
40
|
* the sidebar trigger, so this mirrors that real value onto the open menu and
|
|
41
|
-
* tags it
|
|
42
|
-
*
|
|
41
|
+
* tags it. The contact row becomes a More submenu whose feedback action still
|
|
42
|
+
* invokes the original host button.
|
|
43
43
|
*
|
|
44
44
|
* The menu is recognised structurally (a fixed-position `[role="menu"]` that is
|
|
45
45
|
* open while the trigger reports `aria-expanded`), so no menu class hash is
|
|
@@ -49,4 +49,4 @@ export declare function accountNameToken(name: string): string;
|
|
|
49
49
|
* @param report - sink for observer failures; the native menu stays untouched.
|
|
50
50
|
* @returns disposer that disconnects the observers and clears every mark.
|
|
51
51
|
*/
|
|
52
|
-
export declare function mountAccountMenu(document: Document, report: (error: unknown) => void): Disposer;
|
|
52
|
+
export declare function mountAccountMenu(document: Document, report: (error: unknown) => void, theme?: ThemePreferencePort | null): Disposer;
|
|
@@ -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;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Disposer } from '../contracts/ports.ts';
|
|
2
|
+
/**
|
|
3
|
+
* The browsing region's header line, and the word that names it.
|
|
4
|
+
*
|
|
5
|
+
* The reference draws this region as ONE line: the group's name at the inline
|
|
6
|
+
* start and, at the trailing edge, the plus that starts a Session, Search and
|
|
7
|
+
* the view options. DSH renders those controls on a section header of its own,
|
|
8
|
+
* above a tree whose first row is the first Workspace's, so `sidebar.css` parks
|
|
9
|
+
* the header on that row's line — and this module is what tells it when. The
|
|
10
|
+
* merge holds only while the tree actually leads with a Workspace row: flat
|
|
11
|
+
* mode renders no group at all, and `compat/empty-groups.ts` takes every group
|
|
12
|
+
* off screen when there is nothing to show, so in both the header stays in the
|
|
13
|
+
* flow and is the region's own title line.
|
|
14
|
+
*
|
|
15
|
+
* That title is a word the host serves (`section.workspaces` when the tree is
|
|
16
|
+
* grouped, `section.sessions` when it is a flat list) and the reference's is
|
|
17
|
+
* 最近会话 / Recents. The word is rewritten the way the sidebar's navigation
|
|
18
|
+
* labels are: only a string the host itself rendered is replaced — a locale
|
|
19
|
+
* this module does not know keeps the host's own word — and every write is
|
|
20
|
+
* undone on release. The grouped word takes the reference's name only while the
|
|
21
|
+
* plugin's no-history prompt is up, which `empty-groups.ts` marks
|
|
22
|
+
* `data-ccd-empty-state="new"`; a region that still has groups to show keeps
|
|
23
|
+
* the host's own heading.
|
|
24
|
+
*
|
|
25
|
+
* The state is one marker attribute on the browsing region rather than a style
|
|
26
|
+
* per element, so a single answer covers every re-render the host makes and
|
|
27
|
+
* dropping the attribute restores the host's own two-row layout.
|
|
28
|
+
*
|
|
29
|
+
* The parked band is locked to the tree's content, not to the region's top
|
|
30
|
+
* edge: the header sits on the first Workspace row's line, so it has to leave
|
|
31
|
+
* with that row when the tree scrolls. No attribute reports a scroll, and the
|
|
32
|
+
* scroller is a sibling of the header rather than an ancestor, so CSS cannot
|
|
33
|
+
* see it (a scroll-driven animation timeline resolves against an ancestor);
|
|
34
|
+
* this module therefore listens to the scroller and publishes its distance as
|
|
35
|
+
* `--ccd-header-scroll` for the band's `top`. That is one deliberate listener
|
|
36
|
+
* beside the observers — the same exception `view-switch.ts` records for the
|
|
37
|
+
* column's width — and it writes only when the value changes.
|
|
38
|
+
*/
|
|
39
|
+
/** Marker on the browsing region while its header shares the first row's line. */
|
|
40
|
+
export declare const BROWSER_HEADER_ATTRIBUTE = "data-ccd-header-merged";
|
|
41
|
+
/** Custom property carrying the tree's scrolled distance, in px. */
|
|
42
|
+
export declare const HEADER_SCROLL_PROPERTY = "--ccd-header-scroll";
|
|
43
|
+
/**
|
|
44
|
+
* The name the browsing region's own title line draws.
|
|
45
|
+
* @param text - the host's word, exactly as it rendered it.
|
|
46
|
+
* @param noHistory - whether the plugin's no-history prompt is on screen.
|
|
47
|
+
* @returns the reference's name, or null to leave the host's own word.
|
|
48
|
+
*/
|
|
49
|
+
export declare function browserHeaderWord(text: string, noHistory: boolean): string | null;
|
|
50
|
+
/**
|
|
51
|
+
* Publish the browsing region's merged-header state, and name its title line.
|
|
52
|
+
*
|
|
53
|
+
* @param document - renderer document carrying the sidebar.
|
|
54
|
+
* @param report - sink for observer failures; the host's two-row layout stays.
|
|
55
|
+
* @returns disposer that drops the marker and restores every label.
|
|
56
|
+
*/
|
|
57
|
+
export declare function mountBrowserHeader(document: Document, report: (error: unknown) => void): Disposer;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Browser capture menu backed by the native webview. */
|
|
2
|
+
export interface BrowserPopupLayout {
|
|
3
|
+
readonly width: number;
|
|
4
|
+
readonly align: 'start' | 'end';
|
|
5
|
+
}
|
|
6
|
+
interface BrowserMenuActions {
|
|
7
|
+
popup(anchor: HTMLElement, content: HTMLElement, layout: BrowserPopupLayout): void;
|
|
8
|
+
dismiss(): void;
|
|
9
|
+
notice(anchor: HTMLElement, text: string): void;
|
|
10
|
+
}
|
|
11
|
+
export interface BrowserMenus {
|
|
12
|
+
more(anchor: HTMLElement): void;
|
|
13
|
+
release(): void;
|
|
14
|
+
}
|
|
15
|
+
export declare function mountBrowserMenus(node: HTMLElement, actions: BrowserMenuActions): BrowserMenus;
|
|
16
|
+
export {};
|
|
@@ -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;
|