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.
Files changed (50) hide show
  1. package/README.md +76 -54
  2. package/lib/client.js +9392 -2579
  3. package/lib/index.js +387 -20
  4. package/lib/types/client/compat/account-menu.d.ts +4 -4
  5. package/lib/types/client/compat/account-more.d.ts +3 -0
  6. package/lib/types/client/compat/account-theme.d.ts +3 -0
  7. package/lib/types/client/compat/adapter.d.ts +22 -1
  8. package/lib/types/client/compat/browser-header.d.ts +57 -0
  9. package/lib/types/client/compat/browser-menu.d.ts +16 -0
  10. package/lib/types/client/compat/composer-menus.d.ts +6 -3
  11. package/lib/types/client/compat/context-panel.d.ts +47 -0
  12. package/lib/types/client/compat/empty-groups.d.ts +46 -0
  13. package/lib/types/client/compat/files-pane.d.ts +78 -0
  14. package/lib/types/client/compat/header-menu-actions.d.ts +14 -0
  15. package/lib/types/client/compat/header-menus.d.ts +4 -0
  16. package/lib/types/client/compat/host-dom.d.ts +180 -0
  17. package/lib/types/client/compat/pane-chrome.d.ts +29 -0
  18. package/lib/types/client/compat/pane-icons.d.ts +23 -0
  19. package/lib/types/client/compat/pane-tiles.d.ts +37 -0
  20. package/lib/types/client/compat/permission-menu.d.ts +88 -0
  21. package/lib/types/client/compat/relative-file-path.d.ts +2 -0
  22. package/lib/types/client/compat/right-panes.d.ts +16 -0
  23. package/lib/types/client/compat/scroll-pill.d.ts +70 -0
  24. package/lib/types/client/compat/sidebar-navigation.d.ts +11 -0
  25. package/lib/types/client/compat/stats-values.d.ts +18 -1
  26. package/lib/types/client/compat/view-options.d.ts +58 -0
  27. package/lib/types/client/compat/view-switch.d.ts +46 -5
  28. package/lib/types/client/contracts/ports.d.ts +176 -1
  29. package/lib/types/client/features/context-panel/ContextPanel.d.ts +12 -0
  30. package/lib/types/client/features/context-panel/mount.d.ts +20 -0
  31. package/lib/types/client/features/conversation/index.d.ts +5 -3
  32. package/lib/types/client/features/conversation/view-switch-icons.d.ts +13 -0
  33. package/lib/types/client/features/settings/locales.d.ts +7 -0
  34. package/lib/types/client/features/shell/index.d.ts +9 -3
  35. package/lib/types/client/features/sidebar/index.d.ts +1 -1
  36. package/lib/types/client/features/sidebar/navigation-icons.d.ts +6 -0
  37. package/lib/types/client/features/statistics/books.d.ts +1 -2
  38. package/lib/types/client/features/statistics/format.d.ts +4 -4
  39. package/lib/types/client/features/view-options/ViewOptionsCard.d.ts +30 -0
  40. package/lib/types/client/features/view-options/mount.d.ts +25 -0
  41. package/lib/types/client/theme/mount.d.ts +2 -2
  42. package/lib/types/client/theme/palette.d.ts +1 -1
  43. package/lib/types/host/context/compaction.d.ts +26 -0
  44. package/lib/types/host/context/pricing.d.ts +99 -0
  45. package/lib/types/host/context/service.d.ts +24 -0
  46. package/lib/types/host/context/unit.d.ts +136 -0
  47. package/lib/types/host/index.d.ts +18 -0
  48. package/lib/types/shared/config.d.ts +26 -1
  49. package/lib/types/shared/context.d.ts +243 -0
  50. 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(([, 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,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: no row is added, renamed or reordered — the stylesheet only reshapes
42
- * the card and prints the name it was given.
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;
@@ -0,0 +1,3 @@
1
+ import type { Disposer } from '../contracts/ports.ts';
2
+ /** ui-settings-account's pinned Menu items are Settings, Contact, Sign in/out. */
3
+ export declare function mountAccountMore(menu: HTMLElement): Disposer;
@@ -0,0 +1,3 @@
1
+ import type { Disposer, ThemePreferencePort } from '../contracts/ports.ts';
2
+ /** A compact theme row sharing the settings page's native preference service. */
3
+ export declare function mountAccountTheme(menu: HTMLElement, 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. 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;