@dipertq/dsh-openviking-status 0.2.2 → 0.3.1

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/client.cjs CHANGED
@@ -37,26 +37,38 @@ var client_exports = {};
37
37
  __export(client_exports, {
38
38
  COMMIT_THRESHOLD: () => COMMIT_THRESHOLD,
39
39
  DEFAULT_OPENVIKING_ENDPOINT: () => DEFAULT_OPENVIKING_ENDPOINT,
40
+ ExtractionSection: () => ExtractionSection,
40
41
  OpenVikingClient: () => OpenVikingClient,
41
42
  OpenVikingSettingsSection: () => OpenVikingSettingsSection,
42
43
  OpenVikingStatusChip: () => OpenVikingStatusChip,
43
44
  OpenVikingStatusPopover: () => OpenVikingStatusPopover,
45
+ POLL_INTERVAL_ACTIVE_MS: () => POLL_INTERVAL_ACTIVE_MS,
46
+ POLL_INTERVAL_IDLE_MS: () => POLL_INTERVAL_IDLE_MS,
44
47
  PROXY_OPENVIKING_ENDPOINT: () => PROXY_OPENVIKING_ENDPOINT,
48
+ TASKS_LIMIT_CAP: () => TASKS_LIMIT_CAP,
45
49
  THEME: () => THEME,
50
+ WarningGlyph: () => WarningGlyph,
46
51
  apply: () => apply,
47
52
  chatNodesToText: () => chatNodesToText,
48
53
  checkHealth: () => checkHealth,
49
54
  commitSession: () => commitSession,
55
+ computeBreakdown: () => computeBreakdown,
50
56
  defaultOpenVikingClient: () => defaultOpenVikingClient,
57
+ extractTaskList: () => extractTaskList,
51
58
  fetchSession: () => fetchSession,
59
+ formatBacklog: () => formatBacklog,
52
60
  formatDaemonVersion: () => formatDaemonVersion,
61
+ formatDuration: () => formatDuration,
53
62
  formatEndpoint: () => formatEndpoint,
63
+ formatExtractionSummary: () => formatExtractionSummary,
54
64
  formatMemoryLeafName: () => formatMemoryLeafName,
55
65
  formatPendingTokens: () => formatPendingTokens,
56
66
  formatRelativeTime: () => formatRelativeTime,
57
67
  formatStatusLabel: () => formatStatusLabel,
58
68
  formatTooltipTitle: () => formatTooltipTitle,
59
69
  getCategoryBadgeStyle: () => getCategoryBadgeStyle,
70
+ getChipDotState: () => getChipDotState,
71
+ getDotColorForState: () => getDotColorForState,
60
72
  getProgressBarColor: () => getProgressBarColor,
61
73
  getProgressBarPercent: () => getProgressBarPercent,
62
74
  getSession: () => getSession,
@@ -65,9 +77,15 @@ __export(client_exports, {
65
77
  inferCategory: () => inferCategory,
66
78
  inject: () => inject,
67
79
  name: () => name,
80
+ normalizeExecutionEvent: () => normalizeExecutionEvent,
81
+ normalizeExtractionTask: () => normalizeExtractionTask,
82
+ normalizeTaskList: () => normalizeTaskList,
83
+ normalizeTimestamp: () => normalizeTimestamp,
68
84
  parseRecalledMemories: () => parseRecalledMemories,
69
85
  resolveApiKey: () => resolveApiKey,
70
86
  resolveEndpoint: () => resolveEndpoint,
87
+ seedRunningTask: () => seedRunningTask,
88
+ shouldPollActive: () => shouldPollActive,
71
89
  themeVar: () => themeVar,
72
90
  truncateSessionId: () => truncateSessionId
73
91
  });
@@ -78,6 +96,98 @@ var import_react2 = __toESM(require("react"), 1);
78
96
  var import_react_dom = __toESM(require("react-dom"), 1);
79
97
 
80
98
  // src/client/api.ts
99
+ var TASKS_LIMIT_CAP = 200;
100
+ function normalizeTimestamp(ts, isoFallback) {
101
+ if (typeof isoFallback === "string" && isoFallback.trim()) {
102
+ return isoFallback.trim();
103
+ }
104
+ if (typeof ts === "string" && ts.trim()) {
105
+ return ts.trim();
106
+ }
107
+ if (typeof ts === "number" && Number.isFinite(ts) && ts > 0) {
108
+ const ms = ts < 1e11 ? ts * 1e3 : ts;
109
+ return new Date(ms).toISOString();
110
+ }
111
+ return void 0;
112
+ }
113
+ function extractTaskList(data) {
114
+ if (!data || typeof data !== "object") return [];
115
+ if (Array.isArray(data)) return data;
116
+ const obj = data;
117
+ if (Array.isArray(obj.result)) return obj.result;
118
+ if (Array.isArray(obj.items)) return obj.items;
119
+ if (Array.isArray(obj.tasks)) return obj.tasks;
120
+ if (obj.result && typeof obj.result === "object") {
121
+ const res = obj.result;
122
+ if (Array.isArray(res.items)) return res.items;
123
+ if (Array.isArray(res.tasks)) return res.tasks;
124
+ }
125
+ return [];
126
+ }
127
+ function normalizeExtractionTask(raw) {
128
+ if (!raw || typeof raw !== "object") return null;
129
+ const taskId = typeof raw.task_id === "string" && raw.task_id || typeof raw.id === "string" && raw.id || "";
130
+ if (!taskId) return null;
131
+ const statusRaw = typeof raw.status === "string" ? raw.status : "";
132
+ const status = statusRaw === "running" || statusRaw === "pending" || statusRaw === "completed" || statusRaw === "failed" ? statusRaw : "pending";
133
+ const result = raw.result ?? {};
134
+ const extracted = result.memories_extracted ?? {};
135
+ const numberOf = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
136
+ const tokenUsageRaw = result.token_usage ?? raw.token_usage;
137
+ const tokenUsage = typeof tokenUsageRaw === "number" ? tokenUsageRaw : numberOf(
138
+ tokenUsageRaw?.total_tokens
139
+ );
140
+ const errorRaw = raw.error;
141
+ const errorMsg = typeof errorRaw === "string" ? errorRaw : typeof errorRaw?.message === "string" ? errorRaw.message : void 0;
142
+ return {
143
+ task_id: taskId,
144
+ status,
145
+ resource_id: typeof raw.resource_id === "string" ? raw.resource_id : void 0,
146
+ created_at: normalizeTimestamp(raw.created_at, raw.created_at_iso),
147
+ updated_at: normalizeTimestamp(raw.updated_at, raw.updated_at_iso),
148
+ memory_write: numberOf(extracted.memory_write ?? extracted.written),
149
+ memory_edit: numberOf(extracted.memory_edit ?? extracted.edited),
150
+ token_usage: tokenUsage,
151
+ error: errorMsg
152
+ };
153
+ }
154
+ function computeBreakdown(tasks) {
155
+ const b = {
156
+ running: 0,
157
+ pending: 0,
158
+ completed: 0,
159
+ failed: 0,
160
+ total: tasks.length,
161
+ firstRunning: null,
162
+ lastCompleted: null,
163
+ lastFailed: null
164
+ };
165
+ for (const t of tasks) {
166
+ if (t.status === "running") {
167
+ b.running += 1;
168
+ if (!b.firstRunning) b.firstRunning = t;
169
+ } else if (t.status === "pending") b.pending += 1;
170
+ else if (t.status === "completed") {
171
+ b.completed += 1;
172
+ if (!b.lastCompleted) b.lastCompleted = t;
173
+ } else if (t.status === "failed") {
174
+ b.failed += 1;
175
+ if (!b.lastFailed) b.lastFailed = t;
176
+ }
177
+ }
178
+ return b;
179
+ }
180
+ function normalizeExecutionEvent(raw) {
181
+ return {
182
+ seq: typeof raw.seq === "number" ? raw.seq : void 0,
183
+ recorded_at: normalizeTimestamp(raw.recorded_at, raw.recorded_at_iso),
184
+ kind: typeof raw.kind === "string" ? raw.kind : void 0,
185
+ status: typeof raw.status === "string" ? raw.status : void 0,
186
+ stage: typeof raw.stage === "string" ? raw.stage : null,
187
+ operation: typeof raw.operation === "string" ? raw.operation : null,
188
+ error: typeof raw.error === "string" ? raw.error : null
189
+ };
190
+ }
81
191
  var DEFAULT_OPENVIKING_ENDPOINT = "http://127.0.0.1:1933";
82
192
  var PROXY_OPENVIKING_ENDPOINT = "/openviking-status/api";
83
193
  function resolveEndpoint(endpoint) {
@@ -386,7 +496,9 @@ var OpenVikingClient = class {
386
496
  const data = await res.json().catch(() => ({}));
387
497
  return {
388
498
  ok: data.ok === true,
389
- error: typeof data.error === "string" ? data.error : void 0
499
+ error: typeof data.error === "string" ? data.error : void 0,
500
+ task_id: typeof data.task_id === "string" ? data.task_id : void 0,
501
+ resource_id: typeof data.resource_id === "string" ? data.resource_id : void 0
390
502
  };
391
503
  } catch (err) {
392
504
  return {
@@ -418,7 +530,14 @@ var OpenVikingClient = class {
418
530
  return { ok: false, error: String(errorMsg) };
419
531
  }
420
532
  this.resolvedSessionIds.set(sessionId.trim(), candidateId);
421
- return { ok: true };
533
+ const okBody = await res.json().catch(() => ({}));
534
+ const container = okBody.result ?? okBody.data ?? okBody;
535
+ const taskId = container.task_id ?? container.id;
536
+ return {
537
+ ok: true,
538
+ task_id: typeof taskId === "string" && taskId.trim() ? taskId.trim() : void 0,
539
+ resource_id: candidateId
540
+ };
422
541
  } catch (err) {
423
542
  return {
424
543
  ok: false,
@@ -428,7 +547,192 @@ var OpenVikingClient = class {
428
547
  }
429
548
  return { ok: false, error: lastError };
430
549
  }
550
+ /**
551
+ * Список задач Phase 2 (Memory Extraction) текущей сессии со сводной
552
+ * разбивкой по статусам.
553
+ *
554
+ * Через прокси: один запрос `GET /tasks?session=<id>`, разбивка считается
555
+ * здесь. Напрямую: разрешаем `resource_id` через кандидатов и запрашиваем
556
+ * `GET /api/v1/tasks?resource_id=<id>&task_type=session_commit&limit=200`.
557
+ */
558
+ async listTasks(sessionId) {
559
+ if (!sessionId || !sessionId.trim()) {
560
+ return { status: "missing" };
561
+ }
562
+ if (this.isProxy()) {
563
+ try {
564
+ const res = await fetch(
565
+ `${this.endpoint}/tasks?session=${encodeURIComponent(
566
+ sessionId.trim()
567
+ )}&limit=${TASKS_LIMIT_CAP}`,
568
+ { method: "GET", headers: this.getHeaders() }
569
+ );
570
+ if (res.status === 401 || res.status === 403) {
571
+ return { status: "unauthorized" };
572
+ }
573
+ if (!res.ok) {
574
+ return { status: "error", detail: `HTTP ${res.status}` };
575
+ }
576
+ const data = await res.json();
577
+ if (data.status === "unauthorized") return { status: "unauthorized" };
578
+ if (data.status === "missing") return { status: "missing" };
579
+ if (data.status === "unreachable")
580
+ return {
581
+ status: "unreachable",
582
+ detail: typeof data.detail === "string" ? data.detail : void 0
583
+ };
584
+ if (data.status === "error")
585
+ return {
586
+ status: "error",
587
+ detail: typeof data.detail === "string" ? data.detail : void 0
588
+ };
589
+ const tasks = normalizeTaskList(extractTaskList(data.tasks ?? data));
590
+ return { status: "ok", breakdown: computeBreakdown(tasks), tasks };
591
+ } catch (err) {
592
+ return {
593
+ status: "unreachable",
594
+ detail: err instanceof Error ? err.message : String(err)
595
+ };
596
+ }
597
+ }
598
+ const candidates = this.getCandidateSessionIds(sessionId);
599
+ let resourceId = null;
600
+ for (const candidateId of candidates) {
601
+ try {
602
+ const probe = await fetch(
603
+ `${this.endpoint}/api/v1/sessions/${encodeURIComponent(candidateId)}`,
604
+ { method: "GET", headers: this.getHeaders() }
605
+ );
606
+ if (probe.status === 404) continue;
607
+ if (probe.status === 401 || probe.status === 403) {
608
+ return { status: "unauthorized" };
609
+ }
610
+ if (probe.ok) {
611
+ resourceId = candidateId;
612
+ this.resolvedSessionIds.set(sessionId.trim(), candidateId);
613
+ break;
614
+ }
615
+ } catch (err) {
616
+ return {
617
+ status: "unreachable",
618
+ detail: err instanceof Error ? err.message : String(err)
619
+ };
620
+ }
621
+ }
622
+ if (!resourceId) return { status: "missing" };
623
+ try {
624
+ const query = new URLSearchParams({
625
+ resource_id: resourceId,
626
+ task_type: "session_commit",
627
+ limit: String(TASKS_LIMIT_CAP)
628
+ });
629
+ const res = await fetch(
630
+ `${this.endpoint}/api/v1/tasks?${query.toString()}`,
631
+ { method: "GET", headers: this.getHeaders() }
632
+ );
633
+ if (res.status === 401 || res.status === 403) {
634
+ return { status: "unauthorized" };
635
+ }
636
+ if (!res.ok) {
637
+ return { status: "error", detail: `HTTP ${res.status}` };
638
+ }
639
+ const data = await res.json();
640
+ const tasks = normalizeTaskList(extractTaskList(data));
641
+ return { status: "ok", breakdown: computeBreakdown(tasks), tasks };
642
+ } catch (err) {
643
+ return {
644
+ status: "unreachable",
645
+ detail: err instanceof Error ? err.message : String(err)
646
+ };
647
+ }
648
+ }
649
+ /**
650
+ * Лениво загрузить одну задачу с лентой `execution_events` для «Show log».
651
+ */
652
+ async fetchTaskEvents(taskId) {
653
+ if (!taskId || !taskId.trim()) {
654
+ return { status: "missing" };
655
+ }
656
+ const buildOk = (taskRaw) => {
657
+ const task = normalizeExtractionTask(taskRaw);
658
+ const eventsContainer = taskRaw.execution_events ?? {};
659
+ const rawEvents = eventsContainer.items ?? (Array.isArray(taskRaw.execution_events) ? taskRaw.execution_events : []);
660
+ const events = (Array.isArray(rawEvents) ? rawEvents : []).filter(
661
+ (e) => !!e && typeof e === "object"
662
+ ).map(normalizeExecutionEvent);
663
+ if (!task) return { status: "error", detail: "malformed task" };
664
+ return { status: "ok", task, events };
665
+ };
666
+ if (this.isProxy()) {
667
+ try {
668
+ const res = await fetch(
669
+ `${this.endpoint}/task?id=${encodeURIComponent(
670
+ taskId.trim()
671
+ )}&events=1`,
672
+ { method: "GET", headers: this.getHeaders() }
673
+ );
674
+ if (res.status === 401 || res.status === 403) {
675
+ return { status: "unauthorized" };
676
+ }
677
+ if (!res.ok) {
678
+ return { status: "error", detail: `HTTP ${res.status}` };
679
+ }
680
+ const data = await res.json();
681
+ if (data.status === "unauthorized") return { status: "unauthorized" };
682
+ if (data.status === "missing") return { status: "missing" };
683
+ if (data.status === "unreachable")
684
+ return {
685
+ status: "unreachable",
686
+ detail: typeof data.detail === "string" ? data.detail : void 0
687
+ };
688
+ if (data.status === "error")
689
+ return {
690
+ status: "error",
691
+ detail: typeof data.detail === "string" ? data.detail : void 0
692
+ };
693
+ return buildOk(data.task ?? {});
694
+ } catch (err) {
695
+ return {
696
+ status: "unreachable",
697
+ detail: err instanceof Error ? err.message : String(err)
698
+ };
699
+ }
700
+ }
701
+ try {
702
+ const res = await fetch(
703
+ `${this.endpoint}/api/v1/tasks/${encodeURIComponent(
704
+ taskId.trim()
705
+ )}?include_events=true`,
706
+ { method: "GET", headers: this.getHeaders() }
707
+ );
708
+ if (res.status === 404) return { status: "missing" };
709
+ if (res.status === 401 || res.status === 403) {
710
+ return { status: "unauthorized" };
711
+ }
712
+ if (!res.ok) {
713
+ return { status: "error", detail: `HTTP ${res.status}` };
714
+ }
715
+ const data = await res.json();
716
+ const taskRaw = data?.result ?? data?.data ?? data;
717
+ return buildOk(taskRaw);
718
+ } catch (err) {
719
+ return {
720
+ status: "unreachable",
721
+ detail: err instanceof Error ? err.message : String(err)
722
+ };
723
+ }
724
+ }
431
725
  };
726
+ function normalizeTaskList(raw) {
727
+ if (!Array.isArray(raw)) return [];
728
+ const tasks = [];
729
+ for (const entry of raw) {
730
+ if (!entry || typeof entry !== "object") continue;
731
+ const task = normalizeExtractionTask(entry);
732
+ if (task) tasks.push(task);
733
+ }
734
+ return tasks;
735
+ }
432
736
  var defaultOpenVikingClient = new OpenVikingClient();
433
737
  function checkHealth(endpoint, apiKey) {
434
738
  const client = endpoint || apiKey ? new OpenVikingClient(endpoint, apiKey) : defaultOpenVikingClient;
@@ -802,6 +1106,39 @@ function dshIcon(name2) {
802
1106
  return null;
803
1107
  }
804
1108
  }
1109
+ function formatDuration(startIso, endIso) {
1110
+ if (!startIso || !endIso) return void 0;
1111
+ const start = new Date(startIso).getTime();
1112
+ const end = new Date(endIso).getTime();
1113
+ if (isNaN(start) || isNaN(end) || end < start) return void 0;
1114
+ const ms = end - start;
1115
+ const sec = ms / 1e3;
1116
+ if (sec < 60) {
1117
+ return `${sec < 10 ? sec.toFixed(1) : Math.round(sec)}s`;
1118
+ }
1119
+ const min = Math.floor(sec / 60);
1120
+ const rem = Math.round(sec % 60);
1121
+ return `${min}m ${rem}s`;
1122
+ }
1123
+ function formatExtractionSummary(task, now = Date.now()) {
1124
+ const write = task.memory_write ?? 0;
1125
+ const edit = task.memory_edit ?? 0;
1126
+ const parts = [`${write} written, ${edit} edited`];
1127
+ const duration = formatDuration(task.created_at, task.updated_at);
1128
+ if (duration) parts.push(duration);
1129
+ const rel = formatRelativeTime(task.updated_at, now);
1130
+ if (rel) parts.push(rel);
1131
+ return parts.join(" \xB7 ");
1132
+ }
1133
+ function formatBacklog(breakdown) {
1134
+ if (!breakdown) return null;
1135
+ const { pending, failed } = breakdown;
1136
+ if (pending <= 0 && failed <= 0) return null;
1137
+ const parts = [];
1138
+ if (pending > 0) parts.push(`${pending} pending`);
1139
+ if (failed > 0) parts.push(`${failed} failed`);
1140
+ return parts.join(" / ");
1141
+ }
805
1142
  function getProgressBarPercent(pendingTokens, threshold = COMMIT_THRESHOLD) {
806
1143
  if (!threshold || threshold <= 0) return 0;
807
1144
  const ratio = (pendingTokens || 0) / threshold;
@@ -868,6 +1205,229 @@ function handleEscapeKey(event, onClose) {
868
1205
  }
869
1206
  return false;
870
1207
  }
1208
+ function ExtractionSection({
1209
+ breakdown,
1210
+ client
1211
+ }) {
1212
+ const [showLog, setShowLog] = (0, import_react.useState)(false);
1213
+ const [events, setEvents] = (0, import_react.useState)(null);
1214
+ const [logError, setLogError] = (0, import_react.useState)(null);
1215
+ const [loadingLog, setLoadingLog] = (0, import_react.useState)(false);
1216
+ if (!breakdown) return null;
1217
+ const isRunning = breakdown.running >= 1;
1218
+ const lastCompleted = breakdown.lastCompleted;
1219
+ const lastFailed = breakdown.lastFailed;
1220
+ const backlog = formatBacklog(breakdown);
1221
+ const failedActive = !isRunning && breakdown.failed >= 1;
1222
+ const logTaskId = (isRunning ? breakdown.firstRunning?.task_id : null) ?? (failedActive ? lastFailed?.task_id : null) ?? lastCompleted?.task_id ?? lastFailed?.task_id ?? null;
1223
+ const mutedStyle = { color: themeVar("labelTertiary") };
1224
+ const toggleLog = (0, import_react.useCallback)(async () => {
1225
+ const next = !showLog;
1226
+ setShowLog(next);
1227
+ if (next && events === null && logTaskId && client) {
1228
+ setLoadingLog(true);
1229
+ setLogError(null);
1230
+ const res = await client.fetchTaskEvents(logTaskId);
1231
+ setLoadingLog(false);
1232
+ if (res.status === "ok") {
1233
+ setEvents(res.events);
1234
+ } else {
1235
+ setLogError(
1236
+ res.status === "unauthorized" ? "No access to task log" : "Could not load log"
1237
+ );
1238
+ }
1239
+ }
1240
+ }, [showLog, events, logTaskId, client]);
1241
+ if (!isRunning && !failedActive && !lastCompleted && !backlog) {
1242
+ return null;
1243
+ }
1244
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { "data-testid": "extraction-section", style: { marginBottom: "12px" }, children: [
1245
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1246
+ "div",
1247
+ {
1248
+ style: {
1249
+ marginBottom: "4px",
1250
+ color: themeVar("labelPrimary"),
1251
+ fontWeight: 500
1252
+ },
1253
+ children: "Memory Extraction"
1254
+ }
1255
+ ),
1256
+ isRunning ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
1257
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1258
+ "div",
1259
+ {
1260
+ style: {
1261
+ display: "flex",
1262
+ alignItems: "center",
1263
+ gap: "6px",
1264
+ marginBottom: "4px"
1265
+ },
1266
+ children: [
1267
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1268
+ "span",
1269
+ {
1270
+ "data-testid": "extraction-status-dot",
1271
+ "aria-hidden": "true",
1272
+ style: {
1273
+ width: "6px",
1274
+ height: "6px",
1275
+ borderRadius: "50%",
1276
+ background: themeVar("stateSuccess"),
1277
+ animation: "ov-pulse 1.2s ease-in-out infinite",
1278
+ flexShrink: 0
1279
+ }
1280
+ }
1281
+ ),
1282
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "data-testid": "extraction-running-label", children: "Extracting\u2026" })
1283
+ ]
1284
+ }
1285
+ ),
1286
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1287
+ "div",
1288
+ {
1289
+ "data-testid": "extraction-indeterminate-track",
1290
+ style: {
1291
+ width: "100%",
1292
+ height: "6px",
1293
+ borderRadius: "3px",
1294
+ backgroundColor: themeVar("insetSurface"),
1295
+ overflow: "hidden",
1296
+ position: "relative"
1297
+ },
1298
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1299
+ "div",
1300
+ {
1301
+ "data-testid": "extraction-indeterminate-fill",
1302
+ style: {
1303
+ position: "absolute",
1304
+ left: 0,
1305
+ top: 0,
1306
+ height: "100%",
1307
+ width: "40%",
1308
+ borderRadius: "3px",
1309
+ backgroundColor: themeVar("stateSuccess"),
1310
+ animation: "ov-indeterminate 1.2s ease-in-out infinite"
1311
+ }
1312
+ }
1313
+ )
1314
+ }
1315
+ )
1316
+ ] }) : failedActive && lastFailed ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1317
+ "div",
1318
+ {
1319
+ "data-testid": "extraction-failed-line",
1320
+ style: {
1321
+ display: "flex",
1322
+ alignItems: "center",
1323
+ gap: "6px",
1324
+ color: themeVar("stateWarning")
1325
+ },
1326
+ children: [
1327
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1328
+ "span",
1329
+ {
1330
+ "aria-hidden": "true",
1331
+ style: { display: "inline-flex", flexShrink: 0 },
1332
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(WarningGlyph, { size: 12, testId: "extraction-warning-glyph" })
1333
+ }
1334
+ ),
1335
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "data-testid": "extraction-failed-text", children: lastFailed.error || "extraction failed" })
1336
+ ]
1337
+ }
1338
+ ) : lastCompleted ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1339
+ "div",
1340
+ {
1341
+ style: {
1342
+ display: "flex",
1343
+ alignItems: "center",
1344
+ gap: "6px"
1345
+ },
1346
+ children: [
1347
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1348
+ "span",
1349
+ {
1350
+ "data-testid": "extraction-status-dot",
1351
+ "aria-hidden": "true",
1352
+ style: {
1353
+ width: "6px",
1354
+ height: "6px",
1355
+ borderRadius: "50%",
1356
+ background: themeVar("stateSuccess"),
1357
+ flexShrink: 0
1358
+ }
1359
+ }
1360
+ ),
1361
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "data-testid": "extraction-last-line", children: formatExtractionSummary(lastCompleted) })
1362
+ ]
1363
+ }
1364
+ ) : null,
1365
+ backlog && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1366
+ "div",
1367
+ {
1368
+ "data-testid": "extraction-backlog-line",
1369
+ style: { ...mutedStyle, marginTop: "4px" },
1370
+ children: backlog
1371
+ }
1372
+ ),
1373
+ logTaskId && client && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { marginTop: "6px" }, children: [
1374
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1375
+ "button",
1376
+ {
1377
+ type: "button",
1378
+ "data-testid": "extraction-show-log-btn",
1379
+ onClick: () => void toggleLog(),
1380
+ style: {
1381
+ background: "none",
1382
+ border: "none",
1383
+ padding: 0,
1384
+ cursor: "pointer",
1385
+ font: "inherit",
1386
+ color: themeVar("labelTertiary"),
1387
+ textDecoration: "underline"
1388
+ },
1389
+ "aria-expanded": showLog,
1390
+ children: showLog ? "Hide log" : "Show log"
1391
+ }
1392
+ ),
1393
+ showLog && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1394
+ "div",
1395
+ {
1396
+ "data-testid": "extraction-log",
1397
+ style: {
1398
+ marginTop: "6px",
1399
+ display: "flex",
1400
+ flexDirection: "column",
1401
+ gap: "2px",
1402
+ maxHeight: "120px",
1403
+ overflowY: "auto"
1404
+ },
1405
+ children: [
1406
+ loadingLog && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-testid": "extraction-log-loading", style: mutedStyle, children: "Loading\u2026" }),
1407
+ logError && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-testid": "extraction-log-error", style: mutedStyle, children: logError }),
1408
+ events && events.length === 0 && !loadingLog && !logError && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-testid": "extraction-log-empty", style: mutedStyle, children: "No events" }),
1409
+ events?.map((ev, idx) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1410
+ "div",
1411
+ {
1412
+ "data-testid": "extraction-log-event",
1413
+ style: {
1414
+ display: "flex",
1415
+ justifyContent: "space-between",
1416
+ gap: "8px"
1417
+ },
1418
+ children: [
1419
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: ev.status || ev.kind || "event" }),
1420
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: mutedStyle, children: formatRelativeTime(ev.recorded_at) || "" })
1421
+ ]
1422
+ },
1423
+ `${ev.seq ?? idx}-${ev.recorded_at ?? idx}`
1424
+ ))
1425
+ ]
1426
+ }
1427
+ )
1428
+ ] })
1429
+ ] });
1430
+ }
871
1431
  function OpenVikingStatusPopover({
872
1432
  sessionId,
873
1433
  health,
@@ -877,6 +1437,8 @@ function OpenVikingStatusPopover({
877
1437
  endpoint,
878
1438
  isCommitting = false,
879
1439
  commitError = null,
1440
+ breakdown = null,
1441
+ client,
880
1442
  onCommitNow,
881
1443
  onClose,
882
1444
  className,
@@ -957,7 +1519,7 @@ function OpenVikingStatusPopover({
957
1519
  ...style
958
1520
  },
959
1521
  children: [
960
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: `@keyframes ov-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }` }),
1522
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: `@keyframes ov-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @keyframes ov-pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.35; transform: scale(0.72); } } @keyframes ov-indeterminate { 0% { left: -40%; } 100% { left: 100%; } }` }),
961
1523
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
962
1524
  "div",
963
1525
  {
@@ -1240,6 +1802,7 @@ function OpenVikingStatusPopover({
1240
1802
  }
1241
1803
  )
1242
1804
  ] }),
1805
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ExtractionSection, { breakdown, client }),
1243
1806
  commitError && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1244
1807
  "div",
1245
1808
  {
@@ -1297,6 +1860,81 @@ function OpenVikingStatusPopover({
1297
1860
  // src/client/OpenVikingStatusChip.tsx
1298
1861
  var import_jsx_runtime2 = require("react/jsx-runtime");
1299
1862
  var COMMIT_THRESHOLD = 2e4;
1863
+ var POLL_INTERVAL_IDLE_MS = 15e3;
1864
+ var POLL_INTERVAL_ACTIVE_MS = 2500;
1865
+ function shouldPollActive(breakdown) {
1866
+ if (!breakdown) return false;
1867
+ return breakdown.running >= 1 || breakdown.pending >= 1;
1868
+ }
1869
+ function getChipDotState({
1870
+ isOnline,
1871
+ sessionUnreadable,
1872
+ breakdown
1873
+ }) {
1874
+ if (!isOnline) return "offline";
1875
+ if (sessionUnreadable) return "session-unreadable";
1876
+ if (breakdown && breakdown.running >= 1) return "busy";
1877
+ if (breakdown && breakdown.failed >= 1) return "extraction-failed";
1878
+ return "online-idle";
1879
+ }
1880
+ function WarningGlyph({
1881
+ size = 11,
1882
+ testId = "extraction-warning-glyph"
1883
+ }) {
1884
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1885
+ "svg",
1886
+ {
1887
+ "data-testid": testId,
1888
+ width: size,
1889
+ height: size,
1890
+ viewBox: "0 0 16 16",
1891
+ fill: "none",
1892
+ "aria-hidden": "true",
1893
+ style: { flexShrink: 0, display: "block" },
1894
+ children: [
1895
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "8", cy: "8", r: "6.5", stroke: "currentColor", strokeWidth: "1.5" }),
1896
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1897
+ "path",
1898
+ {
1899
+ d: "M8 5v3.5",
1900
+ stroke: "currentColor",
1901
+ strokeWidth: "1.5",
1902
+ strokeLinecap: "round"
1903
+ }
1904
+ ),
1905
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "8", cy: "11", r: "0.9", fill: "currentColor" })
1906
+ ]
1907
+ }
1908
+ );
1909
+ }
1910
+ function seedRunningTask(prev, taskId, resourceId) {
1911
+ const existing = prev && prev.status === "ok" ? prev.tasks : [];
1912
+ if (existing.some((t) => t.task_id === taskId)) {
1913
+ return prev;
1914
+ }
1915
+ const seeded = {
1916
+ task_id: taskId,
1917
+ status: "running",
1918
+ resource_id: resourceId,
1919
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
1920
+ };
1921
+ const tasks = [seeded, ...existing];
1922
+ return { status: "ok", breakdown: computeBreakdown(tasks), tasks };
1923
+ }
1924
+ function getDotColorForState(state) {
1925
+ switch (state) {
1926
+ case "offline":
1927
+ return themeVar("stateError");
1928
+ case "session-unreadable":
1929
+ case "extraction-failed":
1930
+ return themeVar("stateWarning");
1931
+ case "busy":
1932
+ return themeVar("stateSuccess");
1933
+ case "online-idle":
1934
+ default:
1935
+ return themeVar("stateSuccess");
1936
+ }
1937
+ }
1300
1938
  function formatPendingTokens(pendingTokens) {
1301
1939
  const k = Math.round((pendingTokens || 0) / 1e3);
1302
1940
  return `${k}k pend`;
@@ -1399,6 +2037,7 @@ function StatusChipView({
1399
2037
  initialHealth,
1400
2038
  initialSessionData,
1401
2039
  initialSessionRead,
2040
+ initialTasksRead,
1402
2041
  initialOpen = false
1403
2042
  }) {
1404
2043
  const [health, setHealth] = (0, import_react2.useState)(
@@ -1407,6 +2046,9 @@ function StatusChipView({
1407
2046
  const [sessionRead, setSessionRead] = (0, import_react2.useState)(
1408
2047
  initialSessionRead ?? (initialSessionData ? { status: "ok", session: initialSessionData } : null)
1409
2048
  );
2049
+ const [tasksRead, setTasksRead] = (0, import_react2.useState)(
2050
+ initialTasksRead ?? null
2051
+ );
1410
2052
  const [isOpen, setIsOpen] = (0, import_react2.useState)(initialOpen);
1411
2053
  const [isCommitting, setIsCommitting] = (0, import_react2.useState)(false);
1412
2054
  const [commitError, setCommitError] = (0, import_react2.useState)(null);
@@ -1442,16 +2084,24 @@ function StatusChipView({
1442
2084
  if (!healthRes.ok) {
1443
2085
  return;
1444
2086
  }
1445
- setSessionRead(await apiClient.readSession(sessionId));
2087
+ const [session, tasks] = await Promise.all([
2088
+ apiClient.readSession(sessionId),
2089
+ apiClient.listTasks(sessionId)
2090
+ ]);
2091
+ setSessionRead(session);
2092
+ setTasksRead(tasks);
1446
2093
  } catch {
1447
2094
  setHealth({ ok: false });
1448
2095
  }
1449
2096
  }, [sessionId, apiClient]);
2097
+ const breakdown = tasksRead?.status === "ok" ? tasksRead.breakdown : null;
2098
+ const hasActivePhase = shouldPollActive(breakdown);
1450
2099
  (0, import_react2.useEffect)(() => {
1451
2100
  fetchStatus();
1452
- const timer = setInterval(fetchStatus, 15e3);
2101
+ const interval = hasActivePhase ? POLL_INTERVAL_ACTIVE_MS : POLL_INTERVAL_IDLE_MS;
2102
+ const timer = setInterval(fetchStatus, interval);
1453
2103
  return () => clearInterval(timer);
1454
- }, [fetchStatus]);
2104
+ }, [fetchStatus, hasActivePhase]);
1455
2105
  (0, import_react2.useEffect)(() => {
1456
2106
  function handleClickOutside(event) {
1457
2107
  if (popoverRef.current && !popoverRef.current.contains(event.target)) {
@@ -1481,6 +2131,11 @@ function StatusChipView({
1481
2131
  keep_recent_count: 10
1482
2132
  });
1483
2133
  if (res.ok) {
2134
+ if (res.task_id) {
2135
+ setTasksRead(
2136
+ (prev) => seedRunningTask(prev, res.task_id, res.resource_id)
2137
+ );
2138
+ }
1484
2139
  await fetchStatus();
1485
2140
  onCommit?.();
1486
2141
  } else {
@@ -1499,11 +2154,15 @@ function StatusChipView({
1499
2154
  const sessionUnreadable = isOnline && sessionRead !== null && sessionRead.status !== "ok";
1500
2155
  const pendingTokens = sessionData?.pending_tokens ?? 0;
1501
2156
  const recalledCount = recalledResult.recalledCount;
1502
- const statusColor = getStatusIndicatorColor(
2157
+ const rawDotState = getChipDotState({
1503
2158
  isOnline,
1504
- isCommitting,
1505
- sessionUnreadable
1506
- );
2159
+ sessionUnreadable,
2160
+ breakdown
2161
+ });
2162
+ const dotState = isCommitting && (rawDotState === "online-idle" || rawDotState === "busy") ? "busy" : rawDotState;
2163
+ const dotBusy = dotState === "busy";
2164
+ const dotFailed = dotState === "extraction-failed";
2165
+ const statusColor = getDotColorForState(dotState);
1507
2166
  const tooltipTitle = formatTooltipTitle({
1508
2167
  isOnline,
1509
2168
  isCommitting,
@@ -1520,6 +2179,7 @@ function StatusChipView({
1520
2179
  style: { minWidth: 0, display: "inline-flex", position: "relative" },
1521
2180
  ref: popoverRef,
1522
2181
  children: [
2182
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("style", { children: `@keyframes ov-pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.35; transform: scale(0.72); } }` }),
1523
2183
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1524
2184
  "button",
1525
2185
  {
@@ -1550,17 +2210,32 @@ function StatusChipView({
1550
2210
  title: tooltipTitle,
1551
2211
  "aria-label": tooltipTitle,
1552
2212
  children: [
1553
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2213
+ dotFailed ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1554
2214
  "span",
1555
2215
  {
1556
2216
  "data-testid": "status-dot",
2217
+ "data-dot-state": dotState,
2218
+ "aria-hidden": "true",
2219
+ style: {
2220
+ display: "inline-flex",
2221
+ color: statusColor,
2222
+ flexShrink: 0
2223
+ },
2224
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(WarningGlyph, { size: 11, testId: "chip-warning-glyph" })
2225
+ }
2226
+ ) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2227
+ "span",
2228
+ {
2229
+ "data-testid": "status-dot",
2230
+ "data-dot-state": dotBusy ? "busy" : dotState,
1557
2231
  "aria-hidden": "true",
1558
2232
  style: {
1559
2233
  width: "6px",
1560
2234
  height: "6px",
1561
2235
  borderRadius: "50%",
1562
2236
  background: statusColor,
1563
- flexShrink: 0
2237
+ flexShrink: 0,
2238
+ ...dotBusy ? { animation: "ov-pulse 1.2s ease-in-out infinite" } : {}
1564
2239
  }
1565
2240
  }
1566
2241
  ),
@@ -1579,6 +2254,8 @@ function StatusChipView({
1579
2254
  endpoint: apiClient.endpoint,
1580
2255
  isCommitting,
1581
2256
  commitError,
2257
+ breakdown,
2258
+ client: apiClient,
1582
2259
  onCommitNow: handleCommitNow,
1583
2260
  onClose: () => setIsOpen(false)
1584
2261
  }