@mstar-harness/dsh 3.7.3 → 3.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,42 +1,114 @@
1
1
  /**
2
- * Data hook for the workflow panel (spec §5): subscribes to the active
3
- * session's conversation log through the session standard kit's chat target
4
- * (`useChat`, a uSES selector hook over the `chat` conversation view target)
5
- * and scans the log for the latest `mstar-engine-status` catalog row.
2
+ * Data hook for the workflow panel (spec §5): the panel's ONE data path.
6
3
  *
7
- * Node discriminator (spec §2.4): `kind === 'context'` + `form === 'catalog'`
8
- * + `source.kind === 'mstar-engine-status'`; the LATEST row wins (snapshot
9
- * order tail). Refresh = snapshot subscription: a digest/TTL re-emission on
10
- * the server only produces a new log line, whose snapshot bump re-runs this
11
- * selection — no manual reload, no polling.
4
+ * The persisted catalog row is an ANCHOR, not a data source: since the source
5
+ * reduction the row carries exactly `{ kind: 'plugin', plugin:
6
+ * 'mstar-engine-status', form: 'catalog' }`, and the structured payload lives
7
+ * in the host's per-session snapshot store. So the hook
12
8
  *
13
- * Contract: `{ source: MstarEngineStatusSource | null; lastUpdated: number |
14
- * null }` — `source` null while no catalog row is logged yet (waiting empty
15
- * state); `lastUpdated` carries the catalog message node `time` (Unix ms) for
16
- * the freshness marker. The hook never throws: a snapshot the selector cannot
17
- * read, or an absent session face, degrades to the explicit empty signal
18
- * instead of bubbling a crash (spec §5 degradation path; the strict-session
19
- * slot normally guarantees a session — the guard is belt-and-suspenders).
9
+ * 1. finds the LATEST anchor row in the active session's conversation log
10
+ * (`kind === 'context'` + `form === 'catalog'` + the first-party `plugin`
11
+ * arm with this plugin's identity) and takes its message time as the
12
+ * refresh key — a re-emission appends a new row, the snapshot bump re-runs
13
+ * this selection, and the newest anchor asks for the newest snapshot;
14
+ * 2. reads the session's workspace directory from the session standard kit's
15
+ * session feed (the host cross-checks the asserted `cwd` against the
16
+ * session AND the stored snapshot, so it must be the real one);
17
+ * 3. asks {@link MstarEngineStatusClient} for that session's snapshot over the
18
+ * host's shared `/api` typert gateway, and renders the validated result.
19
+ *
20
+ * Every degraded path is an EXPLICIT state (spec §5): `waiting` (no anchor row
21
+ * — the plugin never ran on this session), `loading` (anchor seen, no answer
22
+ * for it yet), `unavailable` (a reason: transport failure, an unknown session
23
+ * directory, no connection, a malformed answer, or the host's own reason).
24
+ * None of them renders an empty plans list, an empty event log or a zeroed
25
+ * counter as if it were data. A VALID snapshot that says "empty" still renders
26
+ * as data — the distinction between "no snapshot" and "a snapshot that says
27
+ * empty" survives.
28
+ *
29
+ * Freshness is the served snapshot's own `at`, never "live".
30
+ *
31
+ * The hook never throws: a throwing seat or a missing client degrades to an
32
+ * explicit state instead of bubbling a crash (spec §5 degradation path).
20
33
  */
34
+ import type { ConversationNode, ContextMessageNode } from '@deepseek-ai/dsh-client-ui-conversation/client';
21
35
  import type { ChatSnapshot } from '@deepseek-ai/dsh-client-ui-chat/client';
22
36
  import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots';
23
- import type { MstarEngineStatusSource } from '../../types.ts';
24
- /** The hook result: the latest catalog row plus its message time (spec §5). */
25
- export interface MstarEngineStatusView {
26
- source: MstarEngineStatusSource | null;
27
- /** Unix ms of the catalog message node; null while no row is present. */
28
- lastUpdated: number | null;
37
+ import type { SessionListState } from '@deepseek-ai/dsh-api-session-controller/client';
38
+ import type { SessionId } from '@deepseek-ai/dsh-session';
39
+ import type { MstarEngineStatusPayload } from '../../types.ts';
40
+ import type { MstarEngineStatusClient } from './engine-status-client.ts';
41
+ /** Selector hook over the Host session list (the `useSessions` standard seat). */
42
+ export type UseSessions = SnapshotSelectorHook<SessionListState>;
43
+ /** The panel's render state — a closed set: no shape can be both data and empty. */
44
+ export type MstarEngineStatusView =
45
+ /** No anchor row: this session never ran the plugin (today's empty state). */
46
+ {
47
+ readonly state: 'waiting';
48
+ readonly anchorTime: null;
49
+ readonly payload: null;
50
+ readonly at: null;
51
+ readonly turn: null;
52
+ readonly reason: null;
53
+ }
54
+ /** Anchor row seen, no answer for it yet — pending, never rendered as data. */
55
+ | {
56
+ readonly state: 'loading';
57
+ readonly anchorTime: number;
58
+ readonly payload: null;
59
+ readonly at: null;
60
+ readonly turn: null;
61
+ readonly reason: null;
62
+ }
63
+ /**
64
+ * The validated snapshot: the payload plus the snapshot's OWN emission
65
+ * identity (`at` timestamp and the agent turn it was written for). Both are
66
+ * the host's records of the stored entry, so the panel can name which
67
+ * emission it is showing rather than implying it is the newest one.
68
+ */
69
+ | {
70
+ readonly state: 'ok';
71
+ readonly anchorTime: number;
72
+ readonly payload: MstarEngineStatusPayload;
73
+ readonly at: string;
74
+ readonly turn: number;
75
+ readonly reason: null;
76
+ }
77
+ /** An explicit degraded answer — always WITH a reason, never silently empty. */
78
+ | {
79
+ readonly state: 'unavailable';
80
+ readonly anchorTime: number;
81
+ readonly payload: null;
82
+ readonly at: null;
83
+ readonly turn: null;
84
+ readonly reason: string;
85
+ };
86
+ /** The seats the hook needs: the session standard kit + the plugin's client. */
87
+ export interface MstarEngineStatusSeats {
88
+ /** Selector hook over the chat target snapshot (the `conversation.view` kit). */
89
+ useChat: SnapshotSelectorHook<ChatSnapshot>;
90
+ /**
91
+ * Selector hook over the Host session list (the global standard seat). The
92
+ * view ring always hands it to a `conversation.view` entry; it stays
93
+ * optional here because a program without the ui-session adapter does not
94
+ * see the declaration merge (the panel then reports `session-cwd-unknown`).
95
+ */
96
+ useSessions: UseSessions | undefined;
97
+ /** Current session identity (the strict-session slot's own prop). */
98
+ sessionId: SessionId | undefined;
99
+ /** The plugin's engine-status client (absent in a composition without one). */
100
+ engineStatus: MstarEngineStatusClient | undefined;
29
101
  }
102
+ /** Latest `mstar-engine-status` anchor row in snapshot order, or null. */
103
+ export declare function latestEngineStatusRow(nodes: readonly ConversationNode[]): ContextMessageNode | null;
104
+ /** Anchor row message time, or null when the log carries no anchor row. */
105
+ export declare function selectAnchorTime(snapshot: ChatSnapshot): number | null;
106
+ /** One session's workspace directory from the session list, or null when unknown. */
107
+ export declare function selectSessionCwd(sessionId: SessionId | undefined): (state: SessionListState) => string | null;
30
108
  /**
31
- * `useMstarEngineStatus(useChat): MstarEngineStatusView` — the panel's data
32
- * hook (spec §5). The session standard kit's `useChat` is passed in (the
33
- * view ring hands it to every `conversation.view` entry); the hook rides it as
34
- * a selector over the chat target snapshot, so a snapshot bump (new catalog
35
- * row) re-runs the selection and refreshes the panel.
109
+ * The panel's data hook (spec §5).
36
110
  *
37
- * The hook never throws (spec §5 degradation path; Task 3 contract): a
38
- * throwing session face or an absent one degrades to the explicit empty
39
- * signal instead of bubbling a crash — the strict-session slot normally
40
- * guarantees a session, the guard is belt-and-suspenders.
111
+ * @param seats - the session standard kit + the plugin's engine-status client.
112
+ * @returns the explicit render state (never a throw, never a half-parsed view).
41
113
  */
42
- export declare function useMstarEngineStatus(useChat: SnapshotSelectorHook<ChatSnapshot>): MstarEngineStatusView;
114
+ export declare function useMstarEngineStatus(seats: MstarEngineStatusSeats): MstarEngineStatusView;
package/dist/client.js CHANGED
@@ -72,6 +72,222 @@ __export(exports_client, {
72
72
  });
73
73
  module.exports = __toCommonJS(exports_client);
74
74
 
75
+ // src/engine-status-wire.ts
76
+ var ENGINE_STATUS_CHANNEL = "/api";
77
+ var ENGINE_STATUS_ENDPOINT = "mstar/engineStatus";
78
+
79
+ // src/client/panel/guards.ts
80
+ function str(value) {
81
+ return typeof value === "string" && value.length > 0 ? value : null;
82
+ }
83
+ function bool(value) {
84
+ return typeof value === "boolean" ? value : null;
85
+ }
86
+ function count(value) {
87
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
88
+ }
89
+ function record(value) {
90
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
91
+ }
92
+ function unavailable(reason) {
93
+ return { status: "unavailable", reason };
94
+ }
95
+ function parseEngineStatusResult(raw, sessionId, cwd) {
96
+ const envelope = record(raw);
97
+ if (envelope === null)
98
+ return unavailable("malformed-response");
99
+ if (envelope.ok !== true) {
100
+ const error = record(envelope.error);
101
+ const code = str(error?.code);
102
+ return unavailable(code === null ? "transport-error" : `transport-error:${code}`);
103
+ }
104
+ const value = record(envelope.value);
105
+ if (value === null)
106
+ return unavailable("malformed-response");
107
+ if (value.status === "unavailable")
108
+ return unavailable(str(value.reason) ?? "unavailable");
109
+ if (value.status !== "ok")
110
+ return unavailable("malformed-response");
111
+ if (value.sessionId !== sessionId)
112
+ return unavailable("session-mismatch");
113
+ if (cwd !== undefined && value.cwd !== cwd)
114
+ return unavailable("cwd-mismatch");
115
+ const at = str(value.at);
116
+ if (at === null)
117
+ return unavailable("malformed-snapshot");
118
+ const turn = count(value.turn);
119
+ if (turn === null)
120
+ return unavailable("malformed-snapshot");
121
+ const payload = record(value.payload);
122
+ if (payload === null)
123
+ return unavailable("malformed-payload");
124
+ return { status: "ok", payload, at, turn };
125
+ }
126
+
127
+ // src/client/panel/engine-status-client.ts
128
+ var ENGINE_STATUS_REQUEST_TIMEOUT_MS = 5000;
129
+ var ENGINE_STATUS_REFRESH_INTERVAL_MS = 30000;
130
+ var EMPTY = { entries: new Map };
131
+
132
+ class MstarEngineStatusClient {
133
+ connection;
134
+ snapshot = EMPTY;
135
+ listeners = new Set;
136
+ inFlight = new Map;
137
+ abort = new AbortController;
138
+ refreshing = new Set;
139
+ timeoutMs;
140
+ refreshIntervalMs;
141
+ refreshTimer = null;
142
+ generationDisposer = null;
143
+ generation = 0;
144
+ disposed = false;
145
+ constructor(connection, options = {}) {
146
+ this.connection = connection;
147
+ this.timeoutMs = options.timeoutMs ?? ENGINE_STATUS_REQUEST_TIMEOUT_MS;
148
+ this.refreshIntervalMs = options.refreshIntervalMs ?? ENGINE_STATUS_REFRESH_INTERVAL_MS;
149
+ try {
150
+ this.generationDisposer = connection?.generation?.subscribe(() => {
151
+ this.invalidate();
152
+ }) ?? null;
153
+ } catch {
154
+ this.generationDisposer = null;
155
+ }
156
+ this.startRefreshTimer();
157
+ }
158
+ getSnapshot = () => this.snapshot;
159
+ subscribe = (listener) => {
160
+ this.listeners.add(listener);
161
+ return () => {
162
+ this.listeners.delete(listener);
163
+ };
164
+ };
165
+ ensure(sessionId, cwd, anchorTime) {
166
+ if (this.disposed)
167
+ return;
168
+ if (this.snapshot.entries.get(sessionId)?.anchorTime === anchorTime)
169
+ return;
170
+ const key = `${sessionId}\x00${anchorTime}`;
171
+ if (this.inFlight.has(key))
172
+ return;
173
+ const request = this.fetch(sessionId, cwd, anchorTime).finally(() => {
174
+ this.inFlight.delete(key);
175
+ });
176
+ this.inFlight.set(key, request);
177
+ }
178
+ invalidate() {
179
+ if (this.disposed)
180
+ return;
181
+ this.generation += 1;
182
+ this.inFlight.clear();
183
+ if (this.snapshot.entries.size === 0)
184
+ return;
185
+ this.snapshot = EMPTY;
186
+ this.notify();
187
+ }
188
+ dispose() {
189
+ this.disposed = true;
190
+ this.generationDisposer?.();
191
+ this.generationDisposer = null;
192
+ if (this.refreshTimer !== null) {
193
+ clearInterval(this.refreshTimer);
194
+ this.refreshTimer = null;
195
+ }
196
+ this.abort.abort();
197
+ this.listeners.clear();
198
+ }
199
+ startRefreshTimer() {
200
+ if (this.disposed || this.refreshIntervalMs <= 0)
201
+ return;
202
+ if (typeof setInterval !== "function")
203
+ return;
204
+ this.refreshTimer = setInterval(() => {
205
+ this.refresh();
206
+ }, this.refreshIntervalMs);
207
+ const timer = this.refreshTimer;
208
+ timer.unref?.();
209
+ }
210
+ refresh() {
211
+ if (this.disposed)
212
+ return;
213
+ const now = Date.now();
214
+ for (const [sessionId, entry] of [...this.snapshot.entries]) {
215
+ if (this.disposed)
216
+ return;
217
+ if (now - entry.fetchedAt < this.refreshIntervalMs)
218
+ continue;
219
+ if (this.refreshing.has(sessionId))
220
+ continue;
221
+ if (this.snapshot.entries.get(sessionId)?.anchorTime !== entry.anchorTime)
222
+ continue;
223
+ this.refreshing.add(sessionId);
224
+ this.fetch(sessionId, entry.cwd, entry.anchorTime).finally(() => {
225
+ this.refreshing.delete(sessionId);
226
+ });
227
+ }
228
+ }
229
+ async fetch(sessionId, cwd, anchorTime) {
230
+ const generation = this.generation;
231
+ let answer;
232
+ if (this.connection === null || this.connection === undefined || typeof this.connection.rpc?.call !== "function") {
233
+ answer = { status: "unavailable", reason: "no-connection" };
234
+ } else {
235
+ try {
236
+ const raw = await this.withDeadline((signal) => this.connection.rpc.call(ENGINE_STATUS_CHANNEL, ENGINE_STATUS_ENDPOINT, { args: { sessionId, cwd } }, signal));
237
+ answer = parseEngineStatusResult(raw, sessionId, cwd);
238
+ } catch (error) {
239
+ answer = { status: "unavailable", reason: `transport-error:${error?.message ?? "unknown"}` };
240
+ }
241
+ }
242
+ this.write(generation, sessionId, cwd, anchorTime, answer);
243
+ }
244
+ async withDeadline(call) {
245
+ if (this.timeoutMs <= 0 || typeof setTimeout !== "function")
246
+ return await call(this.abort.signal);
247
+ const controller = new AbortController;
248
+ const relay = () => {
249
+ controller.abort();
250
+ };
251
+ this.abort.signal.addEventListener("abort", relay);
252
+ let timer = null;
253
+ const deadline = new Promise((_resolve, reject) => {
254
+ timer = setTimeout(() => {
255
+ controller.abort();
256
+ reject(new Error(`timeout:${this.timeoutMs}ms`));
257
+ }, this.timeoutMs);
258
+ });
259
+ try {
260
+ return await Promise.race([call(controller.signal), deadline]);
261
+ } finally {
262
+ if (timer !== null)
263
+ clearTimeout(timer);
264
+ this.abort.signal.removeEventListener("abort", relay);
265
+ }
266
+ }
267
+ write(generation, sessionId, cwd, anchorTime, fetch) {
268
+ if (this.disposed)
269
+ return;
270
+ if (generation !== this.generation)
271
+ return;
272
+ if ((this.snapshot.entries.get(sessionId)?.anchorTime ?? anchorTime) > anchorTime)
273
+ return;
274
+ this.writeEntry(sessionId, { anchorTime, cwd, fetchedAt: Date.now(), fetch });
275
+ }
276
+ writeEntry(sessionId, entry) {
277
+ const entries = new Map(this.snapshot.entries);
278
+ entries.set(sessionId, entry);
279
+ this.snapshot = { entries };
280
+ this.notify();
281
+ }
282
+ notify() {
283
+ for (const listener of [...this.listeners]) {
284
+ try {
285
+ listener();
286
+ } catch {}
287
+ }
288
+ }
289
+ }
290
+
75
291
  // src/client/panel/locale.ts
76
292
  var NS = "mstar-panel";
77
293
  var zh = {
@@ -111,6 +327,8 @@ var zh = {
111
327
  "event-log.yes": "是",
112
328
  "event-log.no": "否",
113
329
  "empty.waiting": "等待首条 engine-status catalog…",
330
+ "empty.loading": "正在读取该会话的 engine-status 快照…",
331
+ "empty.unavailable": "engine-status 快照不可用({reason})",
114
332
  "empty.no-harness": "未检测到 Morning Star harness",
115
333
  "empty.no-harness-hint": "当前工作区未发现 .mstar/ harness 目录,详细面板保持未激活;检测到 harness 后自动呈现",
116
334
  "watermark.version": "mstar {version}",
@@ -192,8 +410,8 @@ var zh = {
192
410
  "state.policy.worktree": "worktree",
193
411
  "state.policy.control-worktree": "control worktree",
194
412
  "state.knowledge.docs": "{count} 篇文档",
195
- "freshness.last-updated": "最后更新 {time}",
196
- "freshness.refresh-note": "刷新跟随 catalog 重发(约 ≤1 分钟)"
413
+ "freshness.last-updated": "快照 {time} · 第 {turn} 轮",
414
+ "freshness.refresh-note": "该会话已存储的快照——不是实时值"
197
415
  };
198
416
  var en = {
199
417
  "view.mstar-workflow": "MStar Workflow",
@@ -232,6 +450,8 @@ var en = {
232
450
  "event-log.yes": "yes",
233
451
  "event-log.no": "no",
234
452
  "empty.waiting": "Waiting for the first engine-status catalog…",
453
+ "empty.loading": "Reading this session’s engine-status snapshot…",
454
+ "empty.unavailable": "Engine-status snapshot unavailable ({reason})",
235
455
  "empty.no-harness": "No Morning Star harness detected",
236
456
  "empty.no-harness-hint": "No .mstar/ harness directory found in this workspace — the detail panel stays inactive and activates automatically once a harness is detected",
237
457
  "watermark.version": "mstar {version}",
@@ -313,12 +533,12 @@ var en = {
313
533
  "state.policy.worktree": "worktree",
314
534
  "state.policy.control-worktree": "control worktree",
315
535
  "state.knowledge.docs": "{count} docs",
316
- "freshness.last-updated": "last updated {time}",
317
- "freshness.refresh-note": "refreshes with catalog re-emission (≤~1 min)"
536
+ "freshness.last-updated": "snapshot {time} · turn {turn}",
537
+ "freshness.refresh-note": "the stored snapshot for this session — never a live value"
318
538
  };
319
539
 
320
540
  // src/client/panel/PanelView.tsx
321
- var import_react3 = require("react");
541
+ var import_react4 = require("react");
322
542
 
323
543
  // src/client/panel/panel.module.css
324
544
  var css = `
@@ -1053,17 +1273,6 @@ if (typeof document !== "undefined" && document.querySelector("style[data-plugin
1053
1273
  }
1054
1274
  var panel_module_default = { root: "20fd0e45_root", emptyRoot: "f83e09fa_emptyRoot", noHarnessCard: "58245a20_noHarnessCard", noHarnessIcon: "ffb65da7_noHarnessIcon", noHarnessTitle: "81e1817c_noHarnessTitle", noHarnessHint: "a2289da7_noHarnessHint", sidebar: "c87acfc7_sidebar", iterationHeadSplit: "77aea5a6_iterationHeadSplit", iterationBranches: "c3b8dcb0_iterationBranches", main: "ea90e208_main", tabNav: "4bb38bc9_tabNav", tab: "98f72e4c_tab", tabActive: "59e199de_tabActive", content: "90bec3c2_content", iterationPage: "3b1c4147_iterationPage", iterationHead: "8a629a8e_iterationHead", iterationSummary: "2d1bee34_iterationSummary", iterationSummaryId: "17162a99_iterationSummaryId", iterationSummaryVerdict: "d3d74135_iterationSummaryVerdict", iterationSummaryStatus: "d9a886be_iterationSummaryStatus", iterationSummaryStatusMuted: "13bf2f41_iterationSummaryStatusMuted", iterationSummaryHint: "ede5708d_iterationSummaryHint", iterationHeadBody: "64d1da62_iterationHeadBody", iterationStepsRow: "c52f7b85_iterationStepsRow", iterationStepItem: "81013a9d_iterationStepItem", iterationStepBadge: "7befde45_iterationStepBadge", iterationStepPhase: "d6cd23a5_iterationStepPhase", iterationStepChip: "de293024_iterationStepChip", iterationVerdictSeat: "afea5850_iterationVerdictSeat", iterationVerdict: "ebe6bbcd_iterationVerdict", iterationBranchesTitle: "e6921da6_iterationBranchesTitle", iterationBranchList: "8a2cd65c_iterationBranchList", iterationBranchRow: "997b0230_iterationBranchRow", iterationBranchLabel: "6cafc032_iterationBranchLabel", iterationBranchValue: "cdcd06ed_iterationBranchValue", iterationTasks: "87f7efbe_iterationTasks", sidebarScroll: "a0b80516_sidebarScroll", meta: "81fe4f58_meta", metaLine: "0fc275e2_metaLine", section: "fcdd0ccc_section", sectionTitle: "aded23ea_sectionTitle", subTitle: "b7190159_subTitle", defList: "26763614_defList", defTerm: "670df156_defTerm", defValue: "3f1b3a05_defValue", planList: "a129f16e_planList", residualList: "113e0aa2_residualList", leaseList: "33dfcc85_leaseList", violationList: "999efdc0_violationList", planId: "744bf63f_planId", leasePlan: "560663a6_leasePlan", leaseHolder: "a68a6d03_leaseHolder", leaseWorktree: "db027b44_leaseWorktree", findingSeverity: "1797fd51_findingSeverity", findingTitle: "c80b7bb0_findingTitle", findingPlan: "616667ad_findingPlan", violationCode: "e42c0bfd_violationCode", knowledgeCategories: "e2098247_knowledgeCategories", planStatus: "a3021d7c_planStatus", truncated: "1849d129_truncated", knowledge: "3eb5feeb_knowledge", direction: "df6dc76a_direction", selection: "4364e615_selection", selectionId: "0ae21b10_selectionId", selectionMeta: "5e43d3a8_selectionMeta", selectionWarning: "da64723f_selectionWarning", selectionCode: "ac988a64_selectionCode", empty: "18a7beee_empty", freshness: "c75c59b0_freshness" };
1055
1275
 
1056
- // src/client/panel/guards.ts
1057
- function str(value) {
1058
- return typeof value === "string" && value.length > 0 ? value : null;
1059
- }
1060
- function bool(value) {
1061
- return typeof value === "boolean" ? value : null;
1062
- }
1063
- function count(value) {
1064
- return typeof value === "number" && Number.isFinite(value) ? value : null;
1065
- }
1066
-
1067
1276
  // src/client/panel/plan-sort.ts
1068
1277
  var PLAN_CAP = 5;
1069
1278
  var FINDINGS_CAP = 10;
@@ -1679,7 +1888,7 @@ function pairingKeyOf(row) {
1679
1888
  return [row.agent ?? "", row.role ?? "", row.planId ?? "", row.taskId ?? ""].join("\x00");
1680
1889
  }
1681
1890
  function pairSettleStatus(rows) {
1682
- const record = new Map;
1891
+ const record2 = new Map;
1683
1892
  const lastDispatch = new Map;
1684
1893
  for (let i = rows.length - 1;i >= 0; i--) {
1685
1894
  const entry = rows[i];
@@ -1689,10 +1898,10 @@ function pairSettleStatus(rows) {
1689
1898
  } else if (entry.paired === true && entry.agent !== null) {
1690
1899
  const paired = lastDispatch.get(pairingKeyOf(entry));
1691
1900
  if (paired !== undefined)
1692
- record.set(paired, entry.status ?? "ok");
1901
+ record2.set(paired, entry.status ?? "ok");
1693
1902
  }
1694
1903
  }
1695
- return record;
1904
+ return record2;
1696
1905
  }
1697
1906
  function pairSettleIndexes(rows) {
1698
1907
  return new Set(pairSettleStatus(rows).keys());
@@ -3424,11 +3633,11 @@ function EntityCard({ entity, t, box }) {
3424
3633
  const running = entity.status === "running";
3425
3634
  const done = entity.status === "settled" && entity.emphasis !== "off";
3426
3635
  const title = entity.idle ? entity.name : entity.role !== "" ? entity.role : entity.name;
3427
- const record = [];
3636
+ const record2 = [];
3428
3637
  if (entity.agent !== null)
3429
- record.push(entity.agent);
3638
+ record2.push(entity.agent);
3430
3639
  if (entity.task !== null)
3431
- record.push(entity.task);
3640
+ record2.push(entity.task);
3432
3641
  return /* @__PURE__ */ jsx_runtime7.jsxs("li", {
3433
3642
  className: entity.idle ? `${agent_canvas_module_default.agentCard} ${agent_canvas_module_default.agentCardIdle}` : running ? `${agent_canvas_module_default.agentCard} ${agent_canvas_module_default.agentCardRunning}` : done ? `${agent_canvas_module_default.agentCard} ${agent_canvas_module_default.agentCardDone}` : agent_canvas_module_default.agentCard,
3434
3643
  style: { left: box.x, top: box.y, width: box.w, height: box.h },
@@ -3490,10 +3699,10 @@ function EntityCard({ entity, t, box }) {
3490
3699
  "data-agent-role": entity.role,
3491
3700
  children: entity.role
3492
3701
  }),
3493
- record.length > 0 && /* @__PURE__ */ jsx_runtime7.jsx("span", {
3702
+ record2.length > 0 && /* @__PURE__ */ jsx_runtime7.jsx("span", {
3494
3703
  className: agent_canvas_module_default.agentRecord,
3495
3704
  "data-agent-record": true,
3496
- children: record.join(" · ")
3705
+ children: record2.join(" · ")
3497
3706
  })
3498
3707
  ]
3499
3708
  });
@@ -4668,44 +4877,87 @@ function IterationTaskPage({ view, t }) {
4668
4877
  }
4669
4878
 
4670
4879
  // src/client/panel/use-mstar-engine-status.ts
4671
- var EMPTY = { source: null, lastUpdated: null };
4880
+ var import_react3 = require("react");
4881
+ var EMPTY_SNAPSHOT = { entries: new Map };
4882
+ var NO_SUBSCRIBE = () => () => {};
4883
+ var readEmptySnapshot = () => EMPTY_SNAPSHOT;
4672
4884
  function latestEngineStatusRow(nodes) {
4673
4885
  for (let i = nodes.length - 1;i >= 0; i--) {
4674
4886
  const node = nodes[i];
4675
4887
  if (node.kind !== "context" || node.form !== "catalog")
4676
4888
  continue;
4677
4889
  const source = node.source;
4678
- if (source?.kind === "mstar-engine-status")
4890
+ if (source?.kind === "plugin" && source.plugin === "mstar-engine-status")
4679
4891
  return node;
4680
4892
  }
4681
4893
  return null;
4682
4894
  }
4683
- function sameView(a, b) {
4684
- return a.source === b.source && a.lastUpdated === b.lastUpdated;
4685
- }
4686
- function selectEngineStatus(snapshot) {
4895
+ function selectAnchorTime(snapshot) {
4687
4896
  try {
4688
- const row = latestEngineStatusRow(snapshot.legacy.nodes);
4689
- if (row === null)
4690
- return EMPTY;
4691
- return { source: row.source, lastUpdated: row.time };
4897
+ const row = latestEngineStatusRow(snapshot?.legacy?.nodes ?? []);
4898
+ return row === null ? null : row.time;
4692
4899
  } catch {
4693
- return EMPTY;
4900
+ return null;
4694
4901
  }
4695
4902
  }
4696
- function useMstarEngineStatus(useChat) {
4903
+ function selectSessionCwd(sessionId) {
4904
+ return (state) => {
4905
+ try {
4906
+ if (sessionId === undefined)
4907
+ return null;
4908
+ const cwd = state?.byId?.[sessionId]?.cwd;
4909
+ return typeof cwd === "string" && cwd !== "" ? cwd : null;
4910
+ } catch {
4911
+ return null;
4912
+ }
4913
+ };
4914
+ }
4915
+ var WAITING = { state: "waiting", anchorTime: null, payload: null, at: null, turn: null, reason: null };
4916
+ function unavailable2(anchorTime, reason) {
4917
+ return { state: "unavailable", anchorTime, payload: null, at: null, turn: null, reason };
4918
+ }
4919
+ function useMstarEngineStatus(seats) {
4920
+ const { useChat, useSessions, sessionId, engineStatus } = seats;
4921
+ let anchorTime = null;
4922
+ let cwd = null;
4923
+ try {
4924
+ anchorTime = useChat(selectAnchorTime);
4925
+ } catch {
4926
+ anchorTime = null;
4927
+ }
4697
4928
  try {
4698
- const view = useChat(selectEngineStatus, sameView);
4699
- return view ?? EMPTY;
4929
+ cwd = useSessions === undefined ? null : useSessions(selectSessionCwd(sessionId));
4700
4930
  } catch {
4701
- return EMPTY;
4931
+ cwd = null;
4932
+ }
4933
+ const snapshot = import_react3.useSyncExternalStore(engineStatus?.subscribe ?? NO_SUBSCRIBE, engineStatus?.getSnapshot ?? readEmptySnapshot, engineStatus?.getSnapshot ?? readEmptySnapshot);
4934
+ if (anchorTime === null)
4935
+ return WAITING;
4936
+ if (engineStatus !== undefined && sessionId !== undefined && cwd !== null) {
4937
+ try {
4938
+ engineStatus.ensure(String(sessionId), cwd, anchorTime);
4939
+ } catch {}
4940
+ }
4941
+ const entry = sessionId === undefined ? undefined : snapshot.entries.get(String(sessionId));
4942
+ if (entry === undefined) {
4943
+ if (engineStatus === undefined)
4944
+ return unavailable2(anchorTime, "no-connection");
4945
+ if (sessionId === undefined)
4946
+ return unavailable2(anchorTime, "session-unknown");
4947
+ if (cwd === null)
4948
+ return unavailable2(anchorTime, "session-cwd-unknown");
4949
+ return { state: "loading", anchorTime, payload: null, at: null, turn: null, reason: null };
4702
4950
  }
4951
+ if (entry.fetch.status === "unavailable")
4952
+ return unavailable2(anchorTime, entry.fetch.reason);
4953
+ return { state: "ok", anchorTime, payload: entry.fetch.payload, at: entry.fetch.at, turn: entry.fetch.turn, reason: null };
4703
4954
  }
4704
4955
 
4705
4956
  // src/client/panel/PanelView.tsx
4706
4957
  var jsx_runtime12 = require("react/jsx-runtime");
4707
- function formatTime(ms) {
4708
- return new Date(ms).toLocaleTimeString("en-GB");
4958
+ function formatSnapshotTime(at) {
4959
+ const ms = Date.parse(at);
4960
+ return Number.isNaN(ms) ? null : new Date(ms).toLocaleTimeString("en-GB");
4709
4961
  }
4710
4962
  function PanelContent({ tab, source, t }) {
4711
4963
  if (tab === "agents") {
@@ -4726,10 +4978,10 @@ function PanelContent({ tab, source, t }) {
4726
4978
  t
4727
4979
  });
4728
4980
  }
4729
- function PanelView({ t, useChat }) {
4730
- const { source, lastUpdated } = useMstarEngineStatus(useChat);
4731
- const [tab, setTab] = import_react3.useState("tasks");
4732
- if (source === null || source === undefined) {
4981
+ function PanelView({ t, useChat, useSessions, sessionId, engineStatus }) {
4982
+ const view = useMstarEngineStatus({ useChat, useSessions, sessionId, engineStatus });
4983
+ const [tab, setTab] = import_react4.useState("tasks");
4984
+ if (view.state === "waiting") {
4733
4985
  return /* @__PURE__ */ jsx_runtime12.jsx("div", {
4734
4986
  className: panel_module_default.emptyRoot,
4735
4987
  "data-mstar-panel": "waiting",
@@ -4741,14 +4993,43 @@ function PanelView({ t, useChat }) {
4741
4993
  })
4742
4994
  });
4743
4995
  }
4996
+ if (view.state === "loading") {
4997
+ return /* @__PURE__ */ jsx_runtime12.jsx("div", {
4998
+ className: panel_module_default.emptyRoot,
4999
+ "data-mstar-panel": "loading",
5000
+ "data-conversation-composer-overlay": "",
5001
+ children: /* @__PURE__ */ jsx_runtime12.jsx("p", {
5002
+ className: panel_module_default.empty,
5003
+ "data-mstar-empty": "loading",
5004
+ children: t("empty.loading")
5005
+ })
5006
+ });
5007
+ }
5008
+ if (view.state === "unavailable") {
5009
+ return /* @__PURE__ */ jsx_runtime12.jsx("div", {
5010
+ className: panel_module_default.emptyRoot,
5011
+ "data-mstar-panel": "unavailable",
5012
+ "data-conversation-composer-overlay": "",
5013
+ children: /* @__PURE__ */ jsx_runtime12.jsx("p", {
5014
+ className: panel_module_default.empty,
5015
+ "data-mstar-empty": "unavailable",
5016
+ "data-mstar-unavailable-reason": view.reason,
5017
+ children: t("empty.unavailable", { reason: view.reason })
5018
+ })
5019
+ });
5020
+ }
5021
+ const source = view.payload;
5022
+ const snapshotTime = formatSnapshotTime(view.at);
4744
5023
  const noHarness = source.harnessDir === null && source.state === null && source.iteration == null;
4745
5024
  const freshness = /* @__PURE__ */ jsx_runtime12.jsxs("footer", {
4746
5025
  className: panel_module_default.freshness,
4747
5026
  "data-mstar-freshness": true,
4748
5027
  children: [
4749
- typeof lastUpdated === "number" ? /* @__PURE__ */ jsx_runtime12.jsx("span", {
4750
- children: t("freshness.last-updated", { time: formatTime(lastUpdated) })
4751
- }) : null,
5028
+ snapshotTime === null ? null : /* @__PURE__ */ jsx_runtime12.jsx("span", {
5029
+ "data-mstar-freshness-at": view.at,
5030
+ "data-mstar-freshness-turn": String(view.turn),
5031
+ children: t("freshness.last-updated", { time: snapshotTime, turn: String(view.turn) })
5032
+ }),
4752
5033
  /* @__PURE__ */ jsx_runtime12.jsx("span", {
4753
5034
  children: t("freshness.refresh-note")
4754
5035
  })
@@ -4844,8 +5125,10 @@ function PanelView({ t, useChat }) {
4844
5125
  }
4845
5126
 
4846
5127
  // src/client/index.ts
4847
- var inject = ["slots", "sessions", "locale"];
5128
+ var inject = ["slots", "sessions", "locale", "connection"];
4848
5129
  function apply(ctx) {
5130
+ const engineStatus = new MstarEngineStatusClient(ctx.connection);
5131
+ ctx.effect(() => () => engineStatus.dispose(), "ui-mstar-panel: engine-status client");
4849
5132
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), "ui-mstar-panel: dictionaries");
4850
5133
  ctx.slots.inject("conversation.view", () => ctx.slots.register({
4851
5134
  name: "conversation.view",
@@ -4853,7 +5136,7 @@ function apply(ctx) {
4853
5136
  order: 20,
4854
5137
  label: () => ctx.locale.bind(NS)("view.mstar-workflow"),
4855
5138
  locale: NS
4856
- }, PanelView));
5139
+ }, (props) => PanelView({ ...props, engineStatus })));
4857
5140
  }
4858
5141
 
4859
5142
  return module.exports; } });