dsh-watcher 0.7.0 → 0.8.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.
@@ -2,6 +2,11 @@ import { z } from "zod";
2
2
  import { createHash } from "node:crypto";
3
3
  //#region src/insights/engine.mjs
4
4
  /** Read-only DSH 0.2.0-rc.2 log fold. No prompt, reasoning or tool body is retained. */
5
+ const DAY_CAP = 400;
6
+ const localDayKey = (ms) => {
7
+ const d = new Date(ms);
8
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
9
+ };
5
10
  const record = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
6
11
  const count = (v) => Number.isSafeInteger(v) && v >= 0;
7
12
  const elapsed = (start, end) => start === null ? 0 : Math.max(0, end - start);
@@ -31,7 +36,7 @@ function emptyStats() {
31
36
  }
32
37
  function initialState(header = {}, inherited = 0) {
33
38
  return {
34
- version: 1,
39
+ version: 2,
35
40
  sessionId: String(header.id ?? ""),
36
41
  skip: inherited,
37
42
  seq: -1,
@@ -42,6 +47,7 @@ function initialState(header = {}, inherited = 0) {
42
47
  },
43
48
  totals: emptyStats(),
44
49
  models: [],
50
+ days: [],
45
51
  turn: null,
46
52
  open: null,
47
53
  tools: [],
@@ -112,6 +118,33 @@ function books(s, route = s.route) {
112
118
  ...s.turn ? [s.turn.stats] : []
113
119
  ];
114
120
  }
121
+ function creditDay(s, time, route, u) {
122
+ const key = localDayKey(time);
123
+ let day = s.days.find((d) => d.key === key);
124
+ if (!day) {
125
+ day = {
126
+ key,
127
+ tokens: 0,
128
+ models: []
129
+ };
130
+ s.days.push(day);
131
+ s.days.sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
132
+ }
133
+ day.tokens += u.tokens;
134
+ const effort = typeof route?.effort === "string" ? route.effort : null;
135
+ let row = day.models.find((m) => m.provider === route.provider && m.model === route.model && (m.effort ?? null) === effort);
136
+ if (!row) {
137
+ row = {
138
+ provider: route.provider,
139
+ model: route.model,
140
+ ...effort ? { effort } : {},
141
+ tokens: 0
142
+ };
143
+ day.models.push(row);
144
+ }
145
+ row.tokens += u.tokens;
146
+ if (s.days.length > DAY_CAP) s.days = s.days.slice(s.days.length - DAY_CAP);
147
+ }
115
148
  function begin(s, d, time, uncertain = false) {
116
149
  s.open = {
117
150
  turn: d.turn,
@@ -162,6 +195,7 @@ function settle(s, time, usage, source) {
162
195
  }
163
196
  }
164
197
  }
198
+ if (u) creditDay(s, time, route, u);
165
199
  s.open = null;
166
200
  }
167
201
  function applyChunk(open, chunk, time) {
@@ -368,6 +402,7 @@ function buildView(s) {
368
402
  updatedAt: s.updatedAt,
369
403
  totals: s.totals,
370
404
  models: s.models,
405
+ days: s.days,
371
406
  turn: s.turn,
372
407
  pending: s.open ? {
373
408
  turn: s.open.turn,
@@ -445,8 +480,19 @@ const pending = z.object({
445
480
  reasoningLast: nullableTime,
446
481
  lastContentAt: nullableTime
447
482
  }).nullable();
483
+ const dayModels = z.array(z.object({
484
+ provider: z.string(),
485
+ model: z.string(),
486
+ effort: z.string().optional(),
487
+ tokens: number
488
+ }));
489
+ const days = z.array(z.object({
490
+ key: z.string(),
491
+ tokens: number,
492
+ models: dayModels
493
+ }));
448
494
  const viewSchema = z.object({
449
- version: z.literal(1),
495
+ version: z.literal(2),
450
496
  sessionId: z.string(),
451
497
  seq: z.number().int().min(-1),
452
498
  updatedAt: z.number().finite(),
@@ -456,6 +502,7 @@ const viewSchema = z.object({
456
502
  model: z.string(),
457
503
  effort: z.string().optional()
458
504
  })),
505
+ days,
459
506
  turn,
460
507
  pending,
461
508
  findings: z.array(finding).max(30),
@@ -471,7 +518,7 @@ const usage = z.object({
471
518
  exact: z.boolean()
472
519
  }).nullable();
473
520
  const stateSchema = z.object({
474
- version: z.literal(1),
521
+ version: z.literal(2),
475
522
  sessionId: z.string(),
476
523
  skip: number,
477
524
  seq: z.number().int().min(-1),
@@ -482,6 +529,7 @@ const stateSchema = z.object({
482
529
  provider: z.string(),
483
530
  model: z.string()
484
531
  })),
532
+ days,
485
533
  turn,
486
534
  open: z.object({
487
535
  turn: number,
@@ -516,7 +564,7 @@ const stateSchema = z.object({
516
564
  function installProjection(ctx) {
517
565
  ctx.sessionProjections.register({
518
566
  key: "watcherInsights",
519
- stateVersion: 1,
567
+ stateVersion: 2,
520
568
  stateSchema,
521
569
  init: (header, inherited) => stateSchema.parse(initialState(header, inherited)),
522
570
  apply: (state, event) => reduceEvent(state, event),
@@ -0,0 +1,52 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { Context as ClientContext } from '@deepseek-ai/cordis';
3
+ import type { SessionId } from '@deepseek-ai/dsh-session/types';
4
+ /**
5
+ * Minimal structural description of the optional `dsh-better-sidebar` tab
6
+ * registration service (published as `ctx.betterSidebar`, see the peer's
7
+ * `client/service.d.ts`). Declared locally on purpose: this package must
8
+ * typecheck and build without the peer installed, so neither values nor types
9
+ * may be imported from it. Semantics mirrored here: `registerTab` takes one
10
+ * descriptor and returns a disposer that unregisters the tab type — hand it to
11
+ * `ctx.effect` so hot reload cannot register it twice.
12
+ */
13
+ export interface WatcherSidebarDescriptor {
14
+ /** Unique tab type id; also the host tab's `type` value. */
15
+ id: string;
16
+ title: string;
17
+ /** Single-instance sugar: opening the tab focuses an existing one. */
18
+ single?: boolean;
19
+ /** + menu sort order (ascending); the host default is 100. */
20
+ order?: number;
21
+ component: (props: WatcherSidebarTabProps) => ReactNode;
22
+ }
23
+ /** The subset of the host's tab component props the Watcher page reads. */
24
+ export interface WatcherSidebarTabProps {
25
+ ctx: ClientContext;
26
+ scope: {
27
+ sessionId: string;
28
+ };
29
+ /** Whether this tab is the active one AND the sidebar panel is open. */
30
+ visible: boolean;
31
+ }
32
+ /** Structural face of `ctx.betterSidebar` relevant to this plugin. */
33
+ export interface BetterSidebarService {
34
+ registerTab?: (descriptor: WatcherSidebarDescriptor) => () => void;
35
+ }
36
+ /** Read the optional service off a context without importing the peer. */
37
+ export declare function betterSidebarServiceOf(ctx: ClientContext): BetterSidebarService | undefined;
38
+ /** The Watcher tab descriptor registered into dsh-better-sidebar. */
39
+ export declare function watcherSidebarDescriptor(): WatcherSidebarDescriptor;
40
+ /**
41
+ * The Better Sidebar tab body: the session-header overlay's work picture,
42
+ * ported onto `scope.sessionId` without the portal, the trigger, or the drag
43
+ * frame. Reads the same Session/Conversation stores through their binding
44
+ * faces; the cost/performance HUD and whole-history paging stay explicit (and
45
+ * `visible: false` never starts a load — only the button does).
46
+ */
47
+ export declare function WatcherSidebarTab({ ctx, sessionId, visible }: {
48
+ ctx: ClientContext;
49
+ sessionId: SessionId;
50
+ visible: boolean;
51
+ }): ReactNode;
52
+ //# sourceMappingURL=SidebarTab.d.ts.map
@@ -1,9 +1,56 @@
1
1
  import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
2
2
  import type { CompleteHistoryResult } from '../hub/history.ts';
3
+ import { type WorkPicture, type WatcherSnapshot } from '../observation/fold.ts';
4
+ import type { SessionId } from '@deepseek-ai/dsh-session/types';
5
+ import type { InsightsView } from '../insights/projection.ts';
6
+ import { type TurnPerformance } from '../observation/performance.ts';
3
7
  export interface WatcherInjected {
4
8
  loadAllHistory: (signal: AbortSignal) => Promise<CompleteHistoryResult>;
5
9
  }
6
10
  export type WatcherProps = PropsRuntime<'conversation.session.header.utilities'> & WatcherInjected;
7
11
  /** Native session-header utility: exact work picture, typed evidence, no steering. */
8
12
  export declare function Watcher(props: WatcherProps): import("react").JSX.Element;
13
+ /**
14
+ * Folds one Session snapshot into the work picture, merging observations that
15
+ * outlive the loaded window. Shared by the session-header overlay and the
16
+ * Better Sidebar tab so both surfaces read the same projection.
17
+ */
18
+ export declare function useObservedPicture(sessionId: string, snapshot: WatcherSnapshot): {
19
+ picture: WorkPicture;
20
+ performanceByTurn: ReadonlyMap<number, TurnPerformance>;
21
+ };
22
+ export interface WorkPicturePanelProps {
23
+ /** Owning Session identity; changing it resets follow, selection, and history state. */
24
+ sessionId: SessionId;
25
+ /** Folded work picture from {@link useObservedPicture}. */
26
+ picture: WorkPicture;
27
+ performanceByTurn: ReadonlyMap<number, TurnPerformance>;
28
+ /** Whether older history pages remain on the Session. */
29
+ hasMore: boolean;
30
+ /** Whole-session projection totals, when the Host projection is available. */
31
+ stats: {
32
+ turns: number;
33
+ steps: number;
34
+ } | undefined;
35
+ /** Cost/performance HUD input; the header overlay always renders it. */
36
+ insights: InsightsView | undefined;
37
+ /** Never mount the cost/performance HUD (the sidebar tab omits it). */
38
+ hideInsights?: boolean;
39
+ /** Explicit whole-history loader; absent when no Session face is available. */
40
+ loadAllHistory?: (signal: AbortSignal) => Promise<CompleteHistoryResult>;
41
+ /** Gates the live clock: the overlay's open state, or the sidebar tab's visibility. */
42
+ live: boolean;
43
+ /** Overlay-only: freeze the floating panel frame across inspector transitions. */
44
+ onInspectorFrameChange?: () => void;
45
+ /** DOM id prefix for turn landmarks; coexisting surfaces must not share ids. */
46
+ domIdPrefix?: string;
47
+ }
48
+ /**
49
+ * The work picture itself: the docked inspector column plus the turns rail.
50
+ * Extracted from the session-header overlay so the Better Sidebar tab can
51
+ * render the same picture without the portal, the trigger, or the drag frame.
52
+ * Owns every picture-local concern (follow, disclosure, observation mode,
53
+ * history load, inspector lifecycle); positioning stays with the host.
54
+ */
55
+ export declare function WorkPicturePanel({ sessionId, picture, performanceByTurn, hasMore, stats, insights, hideInsights, loadAllHistory, live, onInspectorFrameChange, domIdPrefix, }: WorkPicturePanelProps): import("react").JSX.Element;
9
56
  //# sourceMappingURL=Watcher.d.ts.map
@@ -0,0 +1,23 @@
1
+ import type { Context as ClientContext } from '@deepseek-ai/cordis';
2
+ import type { SessionId } from '@deepseek-ai/dsh-session/types';
3
+ import { type CompleteHistoryResult } from '../hub/history.ts';
4
+ /**
5
+ * The paging verbs the shared loader needs from one Session face. The
6
+ * session-header injection and the Better Sidebar tab both satisfy it from
7
+ * `sessions.binding(id).session`.
8
+ */
9
+ export interface WatcherHistorySession {
10
+ loadOlder: () => Promise<void>;
11
+ getSnapshot: () => {
12
+ hasMore: boolean;
13
+ loadingOlder: boolean;
14
+ };
15
+ }
16
+ /**
17
+ * Build the explicit whole-history loader for one Session: repeat the
18
+ * Session's public, read-only paging verb until the head stops moving, using
19
+ * the same conversation head key the header overlay reports. Nothing starts
20
+ * it automatically — only a user action does.
21
+ */
22
+ export declare function createCompleteHistoryLoader(ctx: ClientContext, sessionId: SessionId, session: WatcherHistorySession): (signal: AbortSignal) => Promise<CompleteHistoryResult>;
23
+ //# sourceMappingURL=history-loader.d.ts.map
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import type { Context } from '@deepseek-ai/cordis';
3
3
  export declare const viewSchema: z.ZodObject<{
4
- version: z.ZodLiteral<1>;
4
+ version: z.ZodLiteral<2>;
5
5
  sessionId: z.ZodString;
6
6
  seq: z.ZodNumber;
7
7
  updatedAt: z.ZodNumber;
@@ -52,6 +52,16 @@ export declare const viewSchema: z.ZodObject<{
52
52
  model: z.ZodString;
53
53
  effort: z.ZodOptional<z.ZodString>;
54
54
  }, z.core.$strip>>;
55
+ days: z.ZodArray<z.ZodObject<{
56
+ key: z.ZodString;
57
+ tokens: z.ZodNumber;
58
+ models: z.ZodArray<z.ZodObject<{
59
+ provider: z.ZodString;
60
+ model: z.ZodString;
61
+ effort: z.ZodOptional<z.ZodString>;
62
+ tokens: z.ZodNumber;
63
+ }, z.core.$strip>>;
64
+ }, z.core.$strip>>;
55
65
  turn: z.ZodNullable<z.ZodObject<{
56
66
  number: z.ZodNumber;
57
67
  startedAt: z.ZodNumber;
@@ -105,7 +115,7 @@ export declare const viewSchema: z.ZodObject<{
105
115
  findingCount: z.ZodNumber;
106
116
  }, z.core.$strip>;
107
117
  declare const stateSchema: z.ZodObject<{
108
- version: z.ZodLiteral<1>;
118
+ version: z.ZodLiteral<2>;
109
119
  sessionId: z.ZodString;
110
120
  skip: z.ZodNumber;
111
121
  seq: z.ZodNumber;
@@ -161,6 +171,16 @@ declare const stateSchema: z.ZodObject<{
161
171
  provider: z.ZodString;
162
172
  model: z.ZodString;
163
173
  }, z.core.$strip>>;
174
+ days: z.ZodArray<z.ZodObject<{
175
+ key: z.ZodString;
176
+ tokens: z.ZodNumber;
177
+ models: z.ZodArray<z.ZodObject<{
178
+ provider: z.ZodString;
179
+ model: z.ZodString;
180
+ effort: z.ZodOptional<z.ZodString>;
181
+ tokens: z.ZodNumber;
182
+ }, z.core.$strip>>;
183
+ }, z.core.$strip>>;
164
184
  turn: z.ZodNullable<z.ZodObject<{
165
185
  number: z.ZodNumber;
166
186
  startedAt: z.ZodNumber;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-watcher",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Read-only DSH session insights and local model usage statistics",
5
5
  "type": "module",
6
6
  "main": "lib/dsh-watcher.js",