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/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(([, count2]) => count2 > 0).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
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 count2 = hours[hour] ?? 0;
135
- if (count2 > 0) sparse.push([hour, count2]);
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, count2] of counts) {
299
- if (key === skip || count2 <= 0) continue;
300
- if (count2 > bestCount || count2 === bestCount && best !== null && key < best) {
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 = count2;
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, count2] of value.days) {
315
+ for (const [date, count4] of value.days) {
311
316
  dates.add(date);
312
- collected.prompts.set(date, (collected.prompts.get(date) ?? 0) + count2);
317
+ collected.prompts.set(date, (collected.prompts.get(date) ?? 0) + count4);
313
318
  }
314
- for (const [hour, count2] of value.hours) collected.hours.set(hour, (collected.hours.get(hour) ?? 0) + count2);
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, count2] of collected.prompts) {
350
+ for (const [date, count4] of collected.prompts) {
346
351
  if (!inRange(date)) continue;
347
- messages += count2;
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 count2 = collected.prompts.get(date) ?? 0;
387
- if (count2 > 0) days.push([date, count2]);
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 count2 = collected.hours.get(hour) ?? 0;
401
- if (count2 > busyCount) {
405
+ const count4 = collected.hours.get(hour) ?? 0;
406
+ if (count4 > busyCount) {
402
407
  busyHour = hour;
403
- busyCount = count2;
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. Cap only cards taller than the free
7
- * space above the anchor so later content growth keeps its natural height.
8
- * MutationObserver and resize reapply placement when the host updates it.
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;