dsh-watcher 0.6.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.
@@ -1,7 +1,12 @@
1
1
  import { z } from "zod";
2
2
  import { createHash } from "node:crypto";
3
3
  //#region src/insights/engine.mjs
4
- /** Read-only DSH 0.1.7-rc.1 log fold. No prompt, reasoning or tool body is retained. */
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,18 @@
1
+ import type { InsightsView } from '../insights/projection.ts';
2
+ type Evidence = {
3
+ turn: number;
4
+ steps: number[];
5
+ seqs: number[];
6
+ };
7
+ export declare function SessionInsights({ value, now, running, waiting, onEvidence }: {
8
+ value: InsightsView | undefined;
9
+ now: number;
10
+ running: boolean;
11
+ waiting: boolean;
12
+ onEvidence: (e: Evidence) => void;
13
+ }): import("react").JSX.Element | null;
14
+ export declare function InsightsSettings(props: {
15
+ remote?: any;
16
+ }): import("react").JSX.Element;
17
+ export {};
18
+ //# sourceMappingURL=Insights.d.ts.map
@@ -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
@@ -0,0 +1,7 @@
1
+ import type { InsightsView } from '../insights/projection.ts';
2
+ export declare function TimingPanel({ stats, scope, tokensPerSecond, }: {
3
+ stats: InsightsView['totals'];
4
+ scope: 'turn' | 'session';
5
+ tokensPerSecond?: number | null;
6
+ }): import("react").JSX.Element;
7
+ //# sourceMappingURL=TimingPanel.d.ts.map
@@ -0,0 +1,56 @@
1
+ import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
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';
7
+ export interface WatcherInjected {
8
+ loadAllHistory: (signal: AbortSignal) => Promise<CompleteHistoryResult>;
9
+ }
10
+ export type WatcherProps = PropsRuntime<'conversation.session.header.utilities'> & WatcherInjected;
11
+ /** Native session-header utility: exact work picture, typed evidence, no steering. */
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;
56
+ //# sourceMappingURL=Watcher.d.ts.map
@@ -0,0 +1,22 @@
1
+ export type DisclosureDepth = 'overview' | 'detail';
2
+ export type DisclosureLayer = 'phase' | 'step' | 'cluster' | 'model' | 'reasoning';
3
+ type LayerOverrides = Readonly<Record<DisclosureLayer, Readonly<Record<string, boolean>>>>;
4
+ export interface DisclosureState {
5
+ readonly depth: DisclosureDepth;
6
+ readonly turns: Readonly<Record<number, boolean>>;
7
+ readonly layers: LayerOverrides;
8
+ }
9
+ export declare function createDisclosureState(depth?: DisclosureDepth): DisclosureState;
10
+ /** Overview keeps the automatic Turn policy; detail opens every Turn by default. */
11
+ export declare function turnDisclosureOpen(state: DisclosureState, turn: number, overviewDefaultOpen: boolean): boolean;
12
+ /** Overview stops at phase headers; detail opens every nested level by default. */
13
+ export declare function layerDisclosureOpen(state: DisclosureState, layer: DisclosureLayer, key: string): boolean;
14
+ export declare function setLayerDisclosure(state: DisclosureState, layer: DisclosureLayer, key: string, open: boolean): DisclosureState;
15
+ export declare function toggleTurnDisclosure(state: DisclosureState, turn: number, overviewDefaultOpen: boolean): DisclosureState;
16
+ export declare function toggleLayerDisclosure(state: DisclosureState, layer: DisclosureLayer, key: string): DisclosureState;
17
+ /** Choosing a depth applies it immediately instead of inheriting stale manual folds. */
18
+ export declare function chooseDisclosureDepth(depth: DisclosureDepth): DisclosureState;
19
+ /** Session-specific ids may change, while the user's chosen depth remains useful. */
20
+ export declare function resetDisclosureOverrides(state: DisclosureState): DisclosureState;
21
+ export {};
22
+ //# sourceMappingURL=disclosure-depth.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
@@ -0,0 +1,9 @@
1
+ import type { Context as ClientContext } from '@deepseek-ai/cordis';
2
+ export declare const name = "dsh-watcher-client";
3
+ export declare const inject: string[];
4
+ /**
5
+ * Native session-header utility. Order 50 sits after Session log (0)
6
+ * and before the files-panel toggle (110). No overlay glyph.
7
+ */
8
+ export declare function apply(ctx: ClientContext): void;
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,11 @@
1
+ import type { Context as ClientContext } from '@deepseek-ai/cordis';
2
+ import { type ModelStepTrace } from '../observation/model-trace.ts';
3
+ declare module '@deepseek-ai/dsh-client-ui-conversation/client' {
4
+ interface ConversationStepDataMap {
5
+ /** Provider-exposed model activity retained for one Step. */
6
+ 'dsh-watcher-model-stage': ModelStepTrace;
7
+ }
8
+ }
9
+ /** Register the Step-scoped, read-only model-stage projection. */
10
+ export declare function registerModelTraceDefinition(ctx: ClientContext): void;
11
+ //# sourceMappingURL=model-trace-definition.d.ts.map
@@ -0,0 +1,15 @@
1
+ import type { WorkItem, WorkStep } from '../observation/fold.ts';
2
+ import type { ModelStepTrace } from '../observation/model-trace.ts';
3
+ type StepTimelineOccurrence = {
4
+ readonly kind: 'occurrence';
5
+ readonly item: WorkItem;
6
+ readonly occurrenceIndex: number;
7
+ };
8
+ export type StepTimelineEntry = StepTimelineOccurrence | {
9
+ readonly kind: 'model';
10
+ readonly trace: ModelStepTrace;
11
+ };
12
+ /** Compose the visible rows owned by one authoritative DSH Step. */
13
+ export declare function stepTimelineEntries(step: Pick<WorkStep, 'items' | 'model'>): readonly StepTimelineEntry[];
14
+ export {};
15
+ //# sourceMappingURL=step-timeline.d.ts.map
@@ -0,0 +1,6 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ export declare const name = "dsh-watcher";
3
+ export declare const inject: string[];
4
+ /** Host-owned replayable statistics; no new transport or model-facing writes. */
5
+ export declare function apply(ctx: Context): void;
6
+ //# sourceMappingURL=dsh-watcher.d.ts.map
@@ -0,0 +1,40 @@
1
+ import type { WorkItem, WorkStatus } from '../observation/fold.ts';
2
+ export type WorkClusterBasis = 'mutable-target' | 'shared-target' | 'exact-call' | 'single';
3
+ export interface WorkStatusCounts {
4
+ running: number;
5
+ waiting: number;
6
+ success: number;
7
+ failure: number;
8
+ returned: number;
9
+ interrupted: number;
10
+ unknown: number;
11
+ }
12
+ export type NonEmptyWorkItems = readonly [WorkItem, ...WorkItem[]];
13
+ /** A reversible analysis projection. `items` always retains the source records. */
14
+ export interface WorkCluster {
15
+ id: string;
16
+ basis: WorkClusterBasis;
17
+ title: string;
18
+ items: NonEmptyWorkItems;
19
+ executionCount: number;
20
+ stepCount: number;
21
+ retryCount: number;
22
+ iterationCount: number;
23
+ latestStatus: WorkStatus;
24
+ statusCounts: Readonly<WorkStatusCounts>;
25
+ }
26
+ /**
27
+ * Group one phase for analysis without changing evidence identity or order.
28
+ *
29
+ * - Mutable calls may group by operation + target so changed inputs remain
30
+ * comparable as iterations.
31
+ * - Reads may group by one exact file target so different line windows stay
32
+ * comparable.
33
+ * - Search, Glob, Grep, Bash, and every other tool require exact normalized
34
+ * arguments. Sharing a cwd, broad path, tool name, or translated title is
35
+ * never enough.
36
+ * - Messages and otherwise unsigned records remain singletons.
37
+ */
38
+ export declare function clusterWorkItems(items: readonly WorkItem[]): readonly WorkCluster[];
39
+ export declare function clusterOutcomeSummary(cluster: WorkCluster): string;
40
+ //# sourceMappingURL=aggregation.d.ts.map
@@ -0,0 +1,29 @@
1
+ import type { WorkPicture } from '../observation/fold.ts';
2
+ export interface FollowSnapshot {
3
+ follow: boolean;
4
+ unread: number;
5
+ selectedId: string | null;
6
+ }
7
+ export interface RailScrollState {
8
+ atBottom: boolean;
9
+ }
10
+ /**
11
+ * Follow versus pin state for the work rail.
12
+ *
13
+ * The rail follows the newest recorded occurrence by default. Selecting
14
+ * history or scrolling away pins the viewport; later occurrences and result
15
+ * updates increment `unread` without yanking the reader away from evidence.
16
+ */
17
+ export declare function createFollow(): {
18
+ snapshot: () => FollowSnapshot;
19
+ /** Counts appended occurrences or a settled live result while pinned. */
20
+ onPicture(picture: Pick<WorkPicture, "nodes">): FollowSnapshot;
21
+ onSelect(id: string): FollowSnapshot;
22
+ onScroll({ atBottom }: RailScrollState): FollowSnapshot;
23
+ setFollow(next: boolean): FollowSnapshot;
24
+ backToLatest(): FollowSnapshot;
25
+ /** Drop a pinned selection without re-arming follow: closing the inspector keeps the rail where the user left it. */
26
+ clearSelection(): FollowSnapshot;
27
+ reset(): FollowSnapshot;
28
+ };
29
+ //# sourceMappingURL=follow.d.ts.map
@@ -0,0 +1,30 @@
1
+ /** Minimal public facts needed to page one shared RC8 Session window. */
2
+ export interface HistoryWindowState {
3
+ hasMore: boolean;
4
+ loadingOlder: boolean;
5
+ headKey: string;
6
+ }
7
+ export type CompleteHistoryResult = {
8
+ kind: 'complete';
9
+ pages: number;
10
+ } | {
11
+ kind: 'cancelled';
12
+ pages: number;
13
+ } | {
14
+ kind: 'blocked';
15
+ pages: number;
16
+ reason: 'busy' | 'no-progress' | 'page-limit';
17
+ };
18
+ export interface CompleteHistoryOptions {
19
+ read: () => HistoryWindowState;
20
+ loadOlder: () => Promise<void>;
21
+ signal: AbortSignal;
22
+ maxPages?: number;
23
+ }
24
+ /**
25
+ * Pull every older RC8 page in order. The Session remains the sole owner of
26
+ * history continuity; this helper only repeats its public, read-only paging
27
+ * verb and fails closed if one request does not advance the visible head.
28
+ */
29
+ export declare function loadCompleteHistory({ read, loadOlder, signal, maxPages, }: CompleteHistoryOptions): Promise<CompleteHistoryResult>;
30
+ //# sourceMappingURL=history.d.ts.map
@@ -0,0 +1,10 @@
1
+ import type { WorkGroup, WorkStatus, WorkTurn } from '../observation/fold.ts';
2
+ /** Overview progress is deliberately separate from execution evidence status. */
3
+ export type OverviewState = 'active' | 'current' | 'waiting' | 'failure' | 'interrupted' | 'settled' | 'partial';
4
+ export declare const OVERVIEW_STATE_LABEL: Readonly<Record<OverviewState, string>>;
5
+ /** Project evidence status into the one question overview markers answer: where should the user look? */
6
+ export declare function overviewStateOf(status: WorkStatus, isLatest: boolean): OverviewState;
7
+ export declare function groupOverviewSummary(group: WorkGroup): string;
8
+ export declare function turnOverviewSummary(turn: WorkTurn): string;
9
+ export declare function turnNeedsDefaultDisclosure(state: OverviewState, isLatest: boolean): boolean;
10
+ //# sourceMappingURL=overview.d.ts.map
@@ -0,0 +1,4 @@
1
+ /** Stable model entry for consumers that need Watcher's truthful work picture. */
2
+ export { EMPTY_PICTURE, foldEvents, foldSnapshot, normalizedSignature, pairTools, parseJsonl, } from '../observation/fold.ts';
3
+ export type { WorkGroup, WorkItem, WorkPhase, WorkPicture, WorkPresentation, WorkStatus, WorkStep, WorkTurn, } from '../observation/fold.ts';
4
+ //# sourceMappingURL=watcher.d.ts.map