dsh-claude-desktop-theme 0.4.2 → 0.4.4

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
@@ -24,7 +24,7 @@ var DEFAULT_FONTS = Object.freeze({
24
24
  });
25
25
  var DEFAULT_VIEW = Object.freeze({
26
26
  showEmptyGroups: false,
27
- groupByStatus: false,
27
+ groupByStatus: true,
28
28
  showHeaderSubagents: false,
29
29
  showHeaderTeam: false
30
30
  });
@@ -431,12 +431,14 @@ var UsageAggregator = class {
431
431
  ctx;
432
432
  warn;
433
433
  values = /* @__PURE__ */ new Map();
434
- queued = [];
434
+ queued = /* @__PURE__ */ new Map();
435
435
  folding = /* @__PURE__ */ new Set();
436
436
  failures = /* @__PURE__ */ new Map();
437
437
  running = 0;
438
438
  refreshRun = null;
439
439
  snapshot = null;
440
+ dirty = true;
441
+ listed = /* @__PURE__ */ new Set();
440
442
  disposed = false;
441
443
  constructor(ctx, warn) {
442
444
  this.ctx = ctx;
@@ -444,7 +446,15 @@ var UsageAggregator = class {
444
446
  }
445
447
  /** The most recent snapshot; an empty one before the first refresh. */
446
448
  get current() {
447
- return this.snapshot ?? aggregateUsage([], { now: Date.now(), pending: 0, missed: 0 });
449
+ if (this.snapshot === null || this.dirty) {
450
+ this.snapshot = aggregateUsage(this.values.values(), {
451
+ now: Date.now(),
452
+ pending: this.queued.size + this.running,
453
+ missed: this.failures.size
454
+ });
455
+ this.dirty = false;
456
+ }
457
+ return this.snapshot;
448
458
  }
449
459
  /**
450
460
  * Bring the snapshot up to date.
@@ -456,6 +466,7 @@ var UsageAggregator = class {
456
466
  * @returns the snapshot as of this pass.
457
467
  */
458
468
  refresh() {
469
+ if (this.disposed) return Promise.resolve(this.current);
459
470
  const running = this.refreshRun;
460
471
  if (running !== null) return running;
461
472
  const run = this.pass().finally(() => {
@@ -467,24 +478,34 @@ var UsageAggregator = class {
467
478
  /** Stop folding; in-flight reads settle but no longer publish. */
468
479
  dispose() {
469
480
  this.disposed = true;
470
- this.queued.length = 0;
481
+ this.queued.clear();
482
+ this.values.clear();
483
+ this.failures.clear();
484
+ this.listed.clear();
485
+ this.snapshot = null;
471
486
  }
472
487
  async pass() {
473
488
  try {
474
489
  const candidates = await this.list();
490
+ if (this.disposed) return this.current;
475
491
  const listed = new Set(candidates.map((candidate) => candidate.header.id));
492
+ this.listed = listed;
476
493
  for (const id of [...this.values.keys()]) if (!listed.has(id)) this.values.delete(id);
477
494
  for (const id of [...this.failures.keys()]) if (!listed.has(id)) this.failures.delete(id);
495
+ for (const id of this.queued.keys()) if (!listed.has(id)) this.queued.delete(id);
478
496
  for (const candidate of candidates) {
479
497
  if (this.disposed) break;
480
498
  const value = this.readSafely(candidate);
481
499
  if (value === void 0) this.enqueue(candidate);
482
- else this.values.set(candidate.header.id, value);
500
+ else {
501
+ this.queued.delete(candidate.header.id);
502
+ this.values.set(candidate.header.id, value);
503
+ }
483
504
  }
484
505
  } catch (error) {
485
506
  this.warn("session listing failed; the card keeps the last snapshot", error);
486
507
  }
487
- this.publish();
508
+ this.invalidate();
488
509
  return this.current;
489
510
  }
490
511
  /** Every stored session the card counts, newest first. */
@@ -530,24 +551,25 @@ var UsageAggregator = class {
530
551
  /** Queue one cold fold unless it is already queued, running, or cooling down. */
531
552
  enqueue(candidate) {
532
553
  const id = candidate.header.id;
533
- if (this.folding.has(id) || this.queued.some((entry) => entry.header.id === id)) return;
554
+ if (this.disposed || this.folding.has(id) || this.queued.has(id)) return;
534
555
  const failedAt = this.failures.get(id);
535
556
  if (failedAt !== void 0 && Date.now() - failedAt < RETRY_COOLDOWN_MS) return;
536
- this.queued.push(candidate);
557
+ this.queued.set(id, candidate);
537
558
  this.pump();
538
559
  }
539
560
  /** Keep at most {@link FOLD_CONCURRENCY} folds in flight. */
540
561
  pump() {
541
562
  while (!this.disposed && this.running < FOLD_CONCURRENCY) {
542
- const candidate = this.queued.shift();
563
+ const candidate = this.queued.values().next().value;
543
564
  if (candidate === void 0) return;
544
565
  const id = candidate.header.id;
566
+ this.queued.delete(id);
545
567
  this.folding.add(id);
546
568
  this.running += 1;
547
569
  void this.fold(candidate).finally(() => {
548
570
  this.folding.delete(id);
549
571
  this.running -= 1;
550
- this.publish();
572
+ this.invalidate();
551
573
  this.pump();
552
574
  });
553
575
  }
@@ -560,7 +582,9 @@ var UsageAggregator = class {
560
582
  let handle;
561
583
  try {
562
584
  handle = await persistence.open(candidate.header.id, "read");
585
+ if (this.disposed || !this.listed.has(candidate.header.id)) return;
563
586
  const { events } = await handle.read(0);
587
+ if (this.disposed || !this.listed.has(candidate.header.id)) return;
564
588
  const block = cache.coldSnapshot(handle.header, handle.inheritedEventCount, events);
565
589
  const value = block.values[USAGE_KEY];
566
590
  if (this.disposed) return;
@@ -571,6 +595,7 @@ var UsageAggregator = class {
571
595
  this.values.set(candidate.header.id, value);
572
596
  this.failures.delete(candidate.header.id);
573
597
  } catch (error) {
598
+ if (this.disposed || !this.listed.has(candidate.header.id)) return;
574
599
  this.failures.set(candidate.header.id, Date.now());
575
600
  this.warn(`fold failed for session ${candidate.header.id}`, error);
576
601
  } finally {
@@ -582,14 +607,10 @@ var UsageAggregator = class {
582
607
  }
583
608
  }
584
609
  }
585
- /** Recompute the snapshot from the values read so far. */
586
- publish() {
610
+ /** Fold completions invalidate totals; only a route read pays for aggregation. */
611
+ invalidate() {
587
612
  if (this.disposed) return;
588
- this.snapshot = aggregateUsage(this.values.values(), {
589
- now: Date.now(),
590
- pending: this.queued.length + this.running,
591
- missed: this.failures.size
592
- });
613
+ this.dirty = true;
593
614
  }
594
615
  };
595
616
  function mountStatistics(ctx, isEnabled) {
@@ -0,0 +1,8 @@
1
+ import type { Disposer } from '../contracts/ports.ts';
2
+ /**
3
+ * Reveal process containers through the host's own handlers. Changing CSS alone
4
+ * leaves hidden reasoning and process members under the host's collapse policy.
5
+ * Leaf reasoning/tool disclosures keep their independent state. Stable hooks
6
+ * come from ui-chat's TurnProcessNodeView and ChatGroupSeat in DSH rc.2.
7
+ */
8
+ export declare function mountConversationProcess(document: Document, report: (error: unknown) => void): Disposer;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Coalesce DOM work into one paint frame.
3
+ *
4
+ * A mutation observer delivers one batch per microtask, and several documents
5
+ * (a panel drag, a streaming transcript, a slider writing its own inline
6
+ * style) can produce many batches between two paints. Doing a full geometry
7
+ * pass per batch multiplies the layout reads by the batch count; doing it once
8
+ * per frame is what the screen can actually use.
9
+ *
10
+ * The fallbacks are deliberate: an occluded Electron window throttles animation
11
+ * frames, and a frame without `requestAnimationFrame` at all (some test hosts)
12
+ * must still settle. In both cases the work runs on a timeout instead, so the
13
+ * published values are never left stale.
14
+ */
15
+ /** One coalescing scheduler; `schedule` runs `task` once before the next paint. */
16
+ export interface FrameScheduler {
17
+ /** Queue the task; repeated calls before it runs collapse into one. */
18
+ schedule(): void;
19
+ /** Drop a queued task without running it. */
20
+ cancel(): void;
21
+ }
22
+ /**
23
+ * Build a scheduler over one task.
24
+ *
25
+ * @param task - the read/write pass to run at most once per frame.
26
+ * @param win - the window supplying animation frames; defaults to the global.
27
+ * @returns the scheduler; its task never runs after `cancel`.
28
+ */
29
+ export declare function frameScheduler(task: () => void, win?: Window | null): FrameScheduler;
@@ -0,0 +1,9 @@
1
+ import type { Disposer } from '../contracts/ports.ts';
2
+ /** Measured hero row width, also reserved in the Composer tools group. */
3
+ export declare const HERO_CHIP_WIDTH_PROPERTY = "--ccd-workspace-seat";
4
+ /**
5
+ * Measure the complete workspace/preset row, whose chips size to their content.
6
+ * Reserve that width plus one gap in the tools group to separate the permission
7
+ * button. Observe label, font and viewport-driven size changes.
8
+ */
9
+ export declare function mountHeroChips(document: Document, report: (error: unknown) => void): Disposer;
@@ -163,6 +163,9 @@ export declare const HOST: Readonly<{
163
163
  readonly browserList: '._7514NG_list';
164
164
  /** FlatList is the only native body the status presentation reorders. */
165
165
  readonly browserFlatList: '._7514NG_flatList';
166
+ /** StateDot's stable state attributes, used when root sessionStatus is absent. */
167
+ readonly sessionNeedsInput: '[aria-hidden="true"][data-state="warning"]';
168
+ readonly sessionWorking: '[aria-hidden="true"][data-state="ongoing"]';
166
169
  /** Rows.module.css: one Workspace's row, the line the merged header shares,
167
170
  and its trailing action group (`sidebar.css` parks both). */
168
171
  readonly workspaceRow: '.jJkEga_projectRow';
@@ -347,6 +350,9 @@ export declare const HOST: Readonly<{
347
350
  * means the frame is not the pinned one and the feature must stay off.
348
351
  */
349
352
  export declare const ANCHOR: Readonly<{
353
+ /** ui-chat TurnProcessNodeView / ChatGroupSeat: process containers, not leaf details. */
354
+ readonly turnProcess: 'button[data-turn-process]';
355
+ readonly stepProcessToggle: '[data-step-process] > div > button[data-process-activity]';
350
356
  readonly frame: '[data-slot="root"] > div';
351
357
  readonly sidebar: '[data-slot="sidebar"]';
352
358
  readonly workspaces: '[data-slot="sidebar.workspaces"]';
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Whether a mutation batch can concern one region of the page.
3
+ *
4
+ * Several modules here observe the whole body because the host can move or
5
+ * rebuild their anchor at any time, then pay a geometry pass on every batch.
6
+ * Most batches, though, come from somewhere else — a drag in the right pane, a
7
+ * streaming transcript on another page, another plugin writing its own markup.
8
+ * This predicate lets a module skip those batches without narrowing what it
9
+ * observes, so an anchor that is replaced still reaches it.
10
+ *
11
+ * The answer is deliberately conservative: a region that is missing, detached
12
+ * or cannot answer `contains` makes every batch relevant, so a module that
13
+ * cannot yet measure is never starved of the mutation that would let it.
14
+ */
15
+ /**
16
+ * Whether any record in a batch can move something inside `region`.
17
+ *
18
+ * `region` is the element the module measures — the composer, the scroller,
19
+ * the conversation root. A change inside it, on it, or on any of its ancestors
20
+ * counts, because those are the writes that can change its box. A `null`
21
+ * region, or one the host has already detached, makes the batch relevant:
22
+ * that is exactly the state in which the module has to look again.
23
+ *
24
+ * @param records - the observer's batch.
25
+ * @param region - the element being measured, or null while it is unknown.
26
+ * @returns true when the batch is worth a pass.
27
+ */
28
+ export declare function recordsTouch(records: readonly MutationRecord[], region: Element | null): boolean;
29
+ /**
30
+ * Whether a batch inserted a subtree the selector names.
31
+ *
32
+ * For modules that watch the whole body only because their anchor can be
33
+ * replaced: an insertion that matches the selector is the one structural event
34
+ * worth a pass, and it costs one `matches`/`querySelector` per inserted element
35
+ * rather than a scan of the document. The predicate is also conservative: a
36
+ * node from another realm, or one without the selector helpers, counts as a
37
+ * match.
38
+ *
39
+ * @param records - the observer's batch.
40
+ * @param selector - the marker whose arrival matters.
41
+ * @returns true when an inserted element is, or contains, a match.
42
+ */
43
+ export declare function recordsIntroduce(records: readonly MutationRecord[], selector: string): boolean;
@@ -7,6 +7,7 @@ export interface PaneChromeOptions {
7
7
  readonly onNew: () => void;
8
8
  readonly onSelect: (id: string) => void;
9
9
  readonly onClose: (id: string) => void;
10
+ readonly onCloseGroup?: () => void;
10
11
  readonly closeTabs: readonly string[];
11
12
  readonly terminals: readonly {
12
13
  id: string;
@@ -16,6 +17,7 @@ export interface PaneChromeOptions {
16
17
  readonly files: readonly {
17
18
  id: string;
18
19
  title: string;
20
+ path: string;
19
21
  selected: boolean;
20
22
  }[];
21
23
  readonly panel: HTMLElement;
@@ -0,0 +1,6 @@
1
+ import type { Disposer } from '../contracts/ports.ts';
2
+ /** The host marks only the latest turn ending in a reply as always revealed.
3
+ * Starting another turn clears that marker before its response is complete. */
4
+ export declare const REPLY_SIGNATURE_SEAT = "[data-turn-tail][data-actions-reveal=\"always\"]:has(> [data-clock=\"end\"])";
5
+ /** Keep one playful signature at the latest settled reply, following host swaps. */
6
+ export declare function mountReplySignature(document: Document, report: (error: unknown) => void): Disposer;
@@ -21,7 +21,7 @@ export declare const VIEW_GLYPH_ATTRIBUTE = "data-ccd-view-glyph";
21
21
  /**
22
22
  * The parked pill's inset from the sidebar column's right edge, in px. Measured
23
23
  * at @2x on the reference: the column's divider sits 16 device px right of the
24
- * pill's right edge (docs/issues/img/todo-sidebar-sessions-target.webp).
24
+ * pill's right edge (DESIGN.md §6.5).
25
25
  */
26
26
  export declare const VIEW_DOCK_INSET = 8;
27
27
  /** Bundled artwork, one per shipped View; see features/conversation/view-switch-icons.ts. */
@@ -82,6 +82,8 @@ export interface SidebarRightPort {
82
82
  }): void;
83
83
  /** Focus a tab and the pane holding it; a missing tab is left alone. */
84
84
  focus(tabId: string): void;
85
+ /** Close a tab in the on-screen Session through the host's cleanup path. */
86
+ close?(tabId: string): void;
85
87
  /**
86
88
  * Open tab metadata across adopted Sessions — and, in the shipped face, the
87
89
  * snapshot store behind it: the plugin's pane layer re-reads the layout
@@ -11,8 +11,8 @@ export interface HeaderActionProps {
11
11
  /**
12
12
  * One view button for the conversation header.
13
13
  *
14
- * All three sit in the utilities group beside the native actions, led by the
15
- * project folder; the corner seat past them holds nothing (the panel's shipped
14
+ * All three sit in the utilities group beside the native actions, in terminal,
15
+ * browser, project folder order; the corner seat past them holds nothing (the panel's shipped
16
16
  * expand control is retired there and its action moves into the Session menu).
17
17
  * All three are the same control: the box size, hover surface and spacing
18
18
  * belong to the seat's own stylesheet
@@ -18,28 +18,24 @@ export declare const FILES_KIND = "files";
18
18
  /**
19
19
  * List orders inside `conversation.session.header.utilities`.
20
20
  *
21
- * The project folder leads the row, so it carries the lowest order of every
22
- * entry there is — below the retired open-in-app control (-10) and the native
23
- * scheduled-tasks entry (-5), not only below this plugin's own views: the
24
- * reference draws the folder as the cluster's first control, whatever a Session
25
- * happens to carry. The two views then land directly before the native session
26
- * menu at 0 and keep it as the cluster's closing entry.
21
+ * Terminal, browser and project folder sit directly before the native session
22
+ * menu at 0, in that order.
27
23
  */
28
- export declare const FOLDER_ORDER = -11;
29
- export declare const TERMINAL_ORDER = -2;
30
- export declare const BROWSER_ORDER = -1;
24
+ export declare const TERMINAL_ORDER = -3;
25
+ export declare const BROWSER_ORDER = -2;
26
+ export declare const FOLDER_ORDER = -1;
31
27
  /** The views this plugin exposes in the utilities group, in cluster order. */
32
28
  export declare const VIEWS: readonly [{
33
- readonly id: 'ccd-folder';
34
- readonly kind: "files";
35
- readonly order: -11;
36
- }, {
37
29
  readonly id: 'ccd-terminal';
38
30
  readonly kind: 'terminal';
39
- readonly order: -2;
31
+ readonly order: -3;
40
32
  }, {
41
33
  readonly id: 'ccd-browser';
42
34
  readonly kind: 'browser';
35
+ readonly order: -2;
36
+ }, {
37
+ readonly id: 'ccd-folder';
38
+ readonly kind: "files";
43
39
  readonly order: -1;
44
40
  }];
45
41
  /**
@@ -64,7 +60,7 @@ export type OpenViewKind = (kind: string, sessionId: string) => void;
64
60
  export declare function utilitiesEntry(view: (typeof VIEWS)[number], labels: Record<(typeof VIEWS)[number]['kind'] | typeof FILES_KIND, string>, open: OpenViewKind): {
65
61
  name: "conversation.session.header.utilities";
66
62
  id: "ccd-browser" | "ccd-folder" | "ccd-terminal";
67
- order: -11 | -2 | -1;
63
+ order: -3 | -2 | -1;
68
64
  inject: (sessionId: string) => HeaderActionProps;
69
65
  };
70
66
  /**
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * The two View switcher glyphs, bundled as trusted static markup.
3
3
  *
4
- * Drawn to the reference's own segments (docs/issues/img/todo-sidebar-sessions-target.webp,
5
- * @2x): a pair of speech bubbles for the chat View and the `</>` pair for the
4
+ * Drawn to the reference's own segments (DESIGN.md §6.5, @2x): a pair of
5
+ * speech bubbles for the chat View and the `</>` pair for the
6
6
  * trajectory View, both on the CCD 16 grid — 1–1.2px stroke (each glyph's own
7
7
  * weight read off the reference), round caps and joins, `currentColor` ink,
8
8
  * ink box ≈ 12.5 × 11px inside the 16px artwork.
@@ -1,4 +1,23 @@
1
1
  import type { RefObject } from 'react';
2
+ interface Size {
3
+ width: number;
4
+ height: number;
5
+ }
6
+ /**
7
+ * The panel's border box, kept between placements.
8
+ *
9
+ * Its size changes only when its own content does, which the resize observer
10
+ * below already reports, so each placement can read the trigger alone instead
11
+ * of forcing a second layout of the surface. The first read is the seeding one:
12
+ * the panel has been shown by the time `place()` runs, so its box is real.
13
+ *
14
+ * @param surface - the popover element.
15
+ * @returns the cached size, and a way to re-read it when the observer fires.
16
+ */
17
+ export declare function cachedSize(surface: Element): {
18
+ current: () => Size;
19
+ refresh: () => void;
20
+ };
2
21
  /** Viewport-clamped top-layer surface; focus and observers die with the panel. */
3
22
  export declare function usePopup(anchor: RefObject<HTMLButtonElement>, close: () => void): {
4
23
  panel: RefObject<HTMLDivElement>;
@@ -8,3 +27,4 @@ export declare function usePopup(anchor: RefObject<HTMLButtonElement>, close: ()
8
27
  visibility: 'hidden' | 'visible';
9
28
  };
10
29
  };
30
+ export {};
@@ -1,8 +1,8 @@
1
1
  import type { HeroRippleImpulse } from '../../contracts/hero-canvas.ts';
2
- /** One character's stable grain and time-varying light, before the edge fade. */
2
+ /** One cell's independently changing character and moving light, before edge fade. */
3
3
  export declare function rippleCell(x: number, y: number, elapsed: number, width: number, height: number, impulses?: readonly HeroRippleImpulse[]): {
4
4
  alpha: number;
5
5
  glyph: string;
6
6
  };
7
- /** Stationary ASCII rows lit by an autonomous, slowly swelling wave field. */
7
+ /** Shimmering ASCII rows lit by an autonomous, slowly swelling wave field. */
8
8
  export declare function drawHeroRipples(canvas: HTMLCanvasElement, elapsed: number, impulses?: readonly HeroRippleImpulse[]): void;
@@ -10,7 +10,8 @@
10
10
  * are still being folded (`pending > 0`) the card is genuinely incomplete, so
11
11
  * it refreshes every couple of seconds; once the snapshot is complete it drops
12
12
  * to a slow poll. A missing route (the host half is off) hides the card rather
13
- * than failing the mount.
13
+ * than failing the mount. Hidden documents pause reads and resume immediately
14
+ * when visible, keeping background windows from repeatedly aggregating history.
14
15
  */
15
16
  import { type StatsSnapshot } from '../../../shared/stats.ts';
16
17
  import type { Logger } from '../../contracts/ports.ts';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-claude-desktop-theme",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "license": "MIT",
5
5
  "description": "Claude Code Desktop-style theme for DeepSeek Harness (DSH) Desktop on macOS and Windows, with light/dark themes, model/effort pickers and an interactive pixel whale",
6
6
  "author": "zkforge",
@@ -85,7 +85,6 @@
85
85
  "check": "npm run typecheck && npm run check:architecture && npm test && npm run build && npm run check:package",
86
86
  "prepack": "npm run check",
87
87
  "pack:local": "npm pack --pack-destination artifacts",
88
- "preview:install": "node scripts/install-local.mjs",
89
88
  "pet:gifs": "node scripts/pet-gifs.mjs",
90
89
  "effort:gifs": "node scripts/effort-gif.mjs"
91
90
  },