@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.js CHANGED
@@ -16,6 +16,98 @@ import React2, {
16
16
  import ReactDOM from "react-dom";
17
17
 
18
18
  // src/client/api.ts
19
+ var TASKS_LIMIT_CAP = 200;
20
+ function normalizeTimestamp(ts, isoFallback) {
21
+ if (typeof isoFallback === "string" && isoFallback.trim()) {
22
+ return isoFallback.trim();
23
+ }
24
+ if (typeof ts === "string" && ts.trim()) {
25
+ return ts.trim();
26
+ }
27
+ if (typeof ts === "number" && Number.isFinite(ts) && ts > 0) {
28
+ const ms = ts < 1e11 ? ts * 1e3 : ts;
29
+ return new Date(ms).toISOString();
30
+ }
31
+ return void 0;
32
+ }
33
+ function extractTaskList(data) {
34
+ if (!data || typeof data !== "object") return [];
35
+ if (Array.isArray(data)) return data;
36
+ const obj = data;
37
+ if (Array.isArray(obj.result)) return obj.result;
38
+ if (Array.isArray(obj.items)) return obj.items;
39
+ if (Array.isArray(obj.tasks)) return obj.tasks;
40
+ if (obj.result && typeof obj.result === "object") {
41
+ const res = obj.result;
42
+ if (Array.isArray(res.items)) return res.items;
43
+ if (Array.isArray(res.tasks)) return res.tasks;
44
+ }
45
+ return [];
46
+ }
47
+ function normalizeExtractionTask(raw) {
48
+ if (!raw || typeof raw !== "object") return null;
49
+ const taskId = typeof raw.task_id === "string" && raw.task_id || typeof raw.id === "string" && raw.id || "";
50
+ if (!taskId) return null;
51
+ const statusRaw = typeof raw.status === "string" ? raw.status : "";
52
+ const status = statusRaw === "running" || statusRaw === "pending" || statusRaw === "completed" || statusRaw === "failed" ? statusRaw : "pending";
53
+ const result = raw.result ?? {};
54
+ const extracted = result.memories_extracted ?? {};
55
+ const numberOf = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
56
+ const tokenUsageRaw = result.token_usage ?? raw.token_usage;
57
+ const tokenUsage = typeof tokenUsageRaw === "number" ? tokenUsageRaw : numberOf(
58
+ tokenUsageRaw?.total_tokens
59
+ );
60
+ const errorRaw = raw.error;
61
+ const errorMsg = typeof errorRaw === "string" ? errorRaw : typeof errorRaw?.message === "string" ? errorRaw.message : void 0;
62
+ return {
63
+ task_id: taskId,
64
+ status,
65
+ resource_id: typeof raw.resource_id === "string" ? raw.resource_id : void 0,
66
+ created_at: normalizeTimestamp(raw.created_at, raw.created_at_iso),
67
+ updated_at: normalizeTimestamp(raw.updated_at, raw.updated_at_iso),
68
+ memory_write: numberOf(extracted.memory_write ?? extracted.written),
69
+ memory_edit: numberOf(extracted.memory_edit ?? extracted.edited),
70
+ token_usage: tokenUsage,
71
+ error: errorMsg
72
+ };
73
+ }
74
+ function computeBreakdown(tasks) {
75
+ const b = {
76
+ running: 0,
77
+ pending: 0,
78
+ completed: 0,
79
+ failed: 0,
80
+ total: tasks.length,
81
+ firstRunning: null,
82
+ lastCompleted: null,
83
+ lastFailed: null
84
+ };
85
+ for (const t of tasks) {
86
+ if (t.status === "running") {
87
+ b.running += 1;
88
+ if (!b.firstRunning) b.firstRunning = t;
89
+ } else if (t.status === "pending") b.pending += 1;
90
+ else if (t.status === "completed") {
91
+ b.completed += 1;
92
+ if (!b.lastCompleted) b.lastCompleted = t;
93
+ } else if (t.status === "failed") {
94
+ b.failed += 1;
95
+ if (!b.lastFailed) b.lastFailed = t;
96
+ }
97
+ }
98
+ return b;
99
+ }
100
+ function normalizeExecutionEvent(raw) {
101
+ return {
102
+ seq: typeof raw.seq === "number" ? raw.seq : void 0,
103
+ recorded_at: normalizeTimestamp(raw.recorded_at, raw.recorded_at_iso),
104
+ kind: typeof raw.kind === "string" ? raw.kind : void 0,
105
+ status: typeof raw.status === "string" ? raw.status : void 0,
106
+ stage: typeof raw.stage === "string" ? raw.stage : null,
107
+ operation: typeof raw.operation === "string" ? raw.operation : null,
108
+ error: typeof raw.error === "string" ? raw.error : null
109
+ };
110
+ }
19
111
  var DEFAULT_OPENVIKING_ENDPOINT = "http://127.0.0.1:1933";
20
112
  var PROXY_OPENVIKING_ENDPOINT = "/openviking-status/api";
21
113
  function resolveEndpoint(endpoint) {
@@ -324,7 +416,9 @@ var OpenVikingClient = class {
324
416
  const data = await res.json().catch(() => ({}));
325
417
  return {
326
418
  ok: data.ok === true,
327
- error: typeof data.error === "string" ? data.error : void 0
419
+ error: typeof data.error === "string" ? data.error : void 0,
420
+ task_id: typeof data.task_id === "string" ? data.task_id : void 0,
421
+ resource_id: typeof data.resource_id === "string" ? data.resource_id : void 0
328
422
  };
329
423
  } catch (err) {
330
424
  return {
@@ -356,7 +450,14 @@ var OpenVikingClient = class {
356
450
  return { ok: false, error: String(errorMsg) };
357
451
  }
358
452
  this.resolvedSessionIds.set(sessionId.trim(), candidateId);
359
- return { ok: true };
453
+ const okBody = await res.json().catch(() => ({}));
454
+ const container = okBody.result ?? okBody.data ?? okBody;
455
+ const taskId = container.task_id ?? container.id;
456
+ return {
457
+ ok: true,
458
+ task_id: typeof taskId === "string" && taskId.trim() ? taskId.trim() : void 0,
459
+ resource_id: candidateId
460
+ };
360
461
  } catch (err) {
361
462
  return {
362
463
  ok: false,
@@ -366,7 +467,192 @@ var OpenVikingClient = class {
366
467
  }
367
468
  return { ok: false, error: lastError };
368
469
  }
470
+ /**
471
+ * Список задач Phase 2 (Memory Extraction) текущей сессии со сводной
472
+ * разбивкой по статусам.
473
+ *
474
+ * Через прокси: один запрос `GET /tasks?session=<id>`, разбивка считается
475
+ * здесь. Напрямую: разрешаем `resource_id` через кандидатов и запрашиваем
476
+ * `GET /api/v1/tasks?resource_id=<id>&task_type=session_commit&limit=200`.
477
+ */
478
+ async listTasks(sessionId) {
479
+ if (!sessionId || !sessionId.trim()) {
480
+ return { status: "missing" };
481
+ }
482
+ if (this.isProxy()) {
483
+ try {
484
+ const res = await fetch(
485
+ `${this.endpoint}/tasks?session=${encodeURIComponent(
486
+ sessionId.trim()
487
+ )}&limit=${TASKS_LIMIT_CAP}`,
488
+ { method: "GET", headers: this.getHeaders() }
489
+ );
490
+ if (res.status === 401 || res.status === 403) {
491
+ return { status: "unauthorized" };
492
+ }
493
+ if (!res.ok) {
494
+ return { status: "error", detail: `HTTP ${res.status}` };
495
+ }
496
+ const data = await res.json();
497
+ if (data.status === "unauthorized") return { status: "unauthorized" };
498
+ if (data.status === "missing") return { status: "missing" };
499
+ if (data.status === "unreachable")
500
+ return {
501
+ status: "unreachable",
502
+ detail: typeof data.detail === "string" ? data.detail : void 0
503
+ };
504
+ if (data.status === "error")
505
+ return {
506
+ status: "error",
507
+ detail: typeof data.detail === "string" ? data.detail : void 0
508
+ };
509
+ const tasks = normalizeTaskList(extractTaskList(data.tasks ?? data));
510
+ return { status: "ok", breakdown: computeBreakdown(tasks), tasks };
511
+ } catch (err) {
512
+ return {
513
+ status: "unreachable",
514
+ detail: err instanceof Error ? err.message : String(err)
515
+ };
516
+ }
517
+ }
518
+ const candidates = this.getCandidateSessionIds(sessionId);
519
+ let resourceId = null;
520
+ for (const candidateId of candidates) {
521
+ try {
522
+ const probe = await fetch(
523
+ `${this.endpoint}/api/v1/sessions/${encodeURIComponent(candidateId)}`,
524
+ { method: "GET", headers: this.getHeaders() }
525
+ );
526
+ if (probe.status === 404) continue;
527
+ if (probe.status === 401 || probe.status === 403) {
528
+ return { status: "unauthorized" };
529
+ }
530
+ if (probe.ok) {
531
+ resourceId = candidateId;
532
+ this.resolvedSessionIds.set(sessionId.trim(), candidateId);
533
+ break;
534
+ }
535
+ } catch (err) {
536
+ return {
537
+ status: "unreachable",
538
+ detail: err instanceof Error ? err.message : String(err)
539
+ };
540
+ }
541
+ }
542
+ if (!resourceId) return { status: "missing" };
543
+ try {
544
+ const query = new URLSearchParams({
545
+ resource_id: resourceId,
546
+ task_type: "session_commit",
547
+ limit: String(TASKS_LIMIT_CAP)
548
+ });
549
+ const res = await fetch(
550
+ `${this.endpoint}/api/v1/tasks?${query.toString()}`,
551
+ { method: "GET", headers: this.getHeaders() }
552
+ );
553
+ if (res.status === 401 || res.status === 403) {
554
+ return { status: "unauthorized" };
555
+ }
556
+ if (!res.ok) {
557
+ return { status: "error", detail: `HTTP ${res.status}` };
558
+ }
559
+ const data = await res.json();
560
+ const tasks = normalizeTaskList(extractTaskList(data));
561
+ return { status: "ok", breakdown: computeBreakdown(tasks), tasks };
562
+ } catch (err) {
563
+ return {
564
+ status: "unreachable",
565
+ detail: err instanceof Error ? err.message : String(err)
566
+ };
567
+ }
568
+ }
569
+ /**
570
+ * Лениво загрузить одну задачу с лентой `execution_events` для «Show log».
571
+ */
572
+ async fetchTaskEvents(taskId) {
573
+ if (!taskId || !taskId.trim()) {
574
+ return { status: "missing" };
575
+ }
576
+ const buildOk = (taskRaw) => {
577
+ const task = normalizeExtractionTask(taskRaw);
578
+ const eventsContainer = taskRaw.execution_events ?? {};
579
+ const rawEvents = eventsContainer.items ?? (Array.isArray(taskRaw.execution_events) ? taskRaw.execution_events : []);
580
+ const events = (Array.isArray(rawEvents) ? rawEvents : []).filter(
581
+ (e) => !!e && typeof e === "object"
582
+ ).map(normalizeExecutionEvent);
583
+ if (!task) return { status: "error", detail: "malformed task" };
584
+ return { status: "ok", task, events };
585
+ };
586
+ if (this.isProxy()) {
587
+ try {
588
+ const res = await fetch(
589
+ `${this.endpoint}/task?id=${encodeURIComponent(
590
+ taskId.trim()
591
+ )}&events=1`,
592
+ { method: "GET", headers: this.getHeaders() }
593
+ );
594
+ if (res.status === 401 || res.status === 403) {
595
+ return { status: "unauthorized" };
596
+ }
597
+ if (!res.ok) {
598
+ return { status: "error", detail: `HTTP ${res.status}` };
599
+ }
600
+ const data = await res.json();
601
+ if (data.status === "unauthorized") return { status: "unauthorized" };
602
+ if (data.status === "missing") return { status: "missing" };
603
+ if (data.status === "unreachable")
604
+ return {
605
+ status: "unreachable",
606
+ detail: typeof data.detail === "string" ? data.detail : void 0
607
+ };
608
+ if (data.status === "error")
609
+ return {
610
+ status: "error",
611
+ detail: typeof data.detail === "string" ? data.detail : void 0
612
+ };
613
+ return buildOk(data.task ?? {});
614
+ } catch (err) {
615
+ return {
616
+ status: "unreachable",
617
+ detail: err instanceof Error ? err.message : String(err)
618
+ };
619
+ }
620
+ }
621
+ try {
622
+ const res = await fetch(
623
+ `${this.endpoint}/api/v1/tasks/${encodeURIComponent(
624
+ taskId.trim()
625
+ )}?include_events=true`,
626
+ { method: "GET", headers: this.getHeaders() }
627
+ );
628
+ if (res.status === 404) return { status: "missing" };
629
+ if (res.status === 401 || res.status === 403) {
630
+ return { status: "unauthorized" };
631
+ }
632
+ if (!res.ok) {
633
+ return { status: "error", detail: `HTTP ${res.status}` };
634
+ }
635
+ const data = await res.json();
636
+ const taskRaw = data?.result ?? data?.data ?? data;
637
+ return buildOk(taskRaw);
638
+ } catch (err) {
639
+ return {
640
+ status: "unreachable",
641
+ detail: err instanceof Error ? err.message : String(err)
642
+ };
643
+ }
644
+ }
369
645
  };
646
+ function normalizeTaskList(raw) {
647
+ if (!Array.isArray(raw)) return [];
648
+ const tasks = [];
649
+ for (const entry of raw) {
650
+ if (!entry || typeof entry !== "object") continue;
651
+ const task = normalizeExtractionTask(entry);
652
+ if (task) tasks.push(task);
653
+ }
654
+ return tasks;
655
+ }
370
656
  var defaultOpenVikingClient = new OpenVikingClient();
371
657
  function checkHealth(endpoint, apiKey) {
372
658
  const client = endpoint || apiKey ? new OpenVikingClient(endpoint, apiKey) : defaultOpenVikingClient;
@@ -740,6 +1026,39 @@ function dshIcon(name2) {
740
1026
  return null;
741
1027
  }
742
1028
  }
1029
+ function formatDuration(startIso, endIso) {
1030
+ if (!startIso || !endIso) return void 0;
1031
+ const start = new Date(startIso).getTime();
1032
+ const end = new Date(endIso).getTime();
1033
+ if (isNaN(start) || isNaN(end) || end < start) return void 0;
1034
+ const ms = end - start;
1035
+ const sec = ms / 1e3;
1036
+ if (sec < 60) {
1037
+ return `${sec < 10 ? sec.toFixed(1) : Math.round(sec)}s`;
1038
+ }
1039
+ const min = Math.floor(sec / 60);
1040
+ const rem = Math.round(sec % 60);
1041
+ return `${min}m ${rem}s`;
1042
+ }
1043
+ function formatExtractionSummary(task, now = Date.now()) {
1044
+ const write = task.memory_write ?? 0;
1045
+ const edit = task.memory_edit ?? 0;
1046
+ const parts = [`${write} written, ${edit} edited`];
1047
+ const duration = formatDuration(task.created_at, task.updated_at);
1048
+ if (duration) parts.push(duration);
1049
+ const rel = formatRelativeTime(task.updated_at, now);
1050
+ if (rel) parts.push(rel);
1051
+ return parts.join(" \xB7 ");
1052
+ }
1053
+ function formatBacklog(breakdown) {
1054
+ if (!breakdown) return null;
1055
+ const { pending, failed } = breakdown;
1056
+ if (pending <= 0 && failed <= 0) return null;
1057
+ const parts = [];
1058
+ if (pending > 0) parts.push(`${pending} pending`);
1059
+ if (failed > 0) parts.push(`${failed} failed`);
1060
+ return parts.join(" / ");
1061
+ }
743
1062
  function getProgressBarPercent(pendingTokens, threshold = COMMIT_THRESHOLD) {
744
1063
  if (!threshold || threshold <= 0) return 0;
745
1064
  const ratio = (pendingTokens || 0) / threshold;
@@ -806,6 +1125,229 @@ function handleEscapeKey(event, onClose) {
806
1125
  }
807
1126
  return false;
808
1127
  }
1128
+ function ExtractionSection({
1129
+ breakdown,
1130
+ client
1131
+ }) {
1132
+ const [showLog, setShowLog] = useState(false);
1133
+ const [events, setEvents] = useState(null);
1134
+ const [logError, setLogError] = useState(null);
1135
+ const [loadingLog, setLoadingLog] = useState(false);
1136
+ if (!breakdown) return null;
1137
+ const isRunning = breakdown.running >= 1;
1138
+ const lastCompleted = breakdown.lastCompleted;
1139
+ const lastFailed = breakdown.lastFailed;
1140
+ const backlog = formatBacklog(breakdown);
1141
+ const failedActive = !isRunning && breakdown.failed >= 1;
1142
+ const logTaskId = (isRunning ? breakdown.firstRunning?.task_id : null) ?? (failedActive ? lastFailed?.task_id : null) ?? lastCompleted?.task_id ?? lastFailed?.task_id ?? null;
1143
+ const mutedStyle = { color: themeVar("labelTertiary") };
1144
+ const toggleLog = useCallback(async () => {
1145
+ const next = !showLog;
1146
+ setShowLog(next);
1147
+ if (next && events === null && logTaskId && client) {
1148
+ setLoadingLog(true);
1149
+ setLogError(null);
1150
+ const res = await client.fetchTaskEvents(logTaskId);
1151
+ setLoadingLog(false);
1152
+ if (res.status === "ok") {
1153
+ setEvents(res.events);
1154
+ } else {
1155
+ setLogError(
1156
+ res.status === "unauthorized" ? "No access to task log" : "Could not load log"
1157
+ );
1158
+ }
1159
+ }
1160
+ }, [showLog, events, logTaskId, client]);
1161
+ if (!isRunning && !failedActive && !lastCompleted && !backlog) {
1162
+ return null;
1163
+ }
1164
+ return /* @__PURE__ */ jsxs("div", { "data-testid": "extraction-section", style: { marginBottom: "12px" }, children: [
1165
+ /* @__PURE__ */ jsx(
1166
+ "div",
1167
+ {
1168
+ style: {
1169
+ marginBottom: "4px",
1170
+ color: themeVar("labelPrimary"),
1171
+ fontWeight: 500
1172
+ },
1173
+ children: "Memory Extraction"
1174
+ }
1175
+ ),
1176
+ isRunning ? /* @__PURE__ */ jsxs("div", { children: [
1177
+ /* @__PURE__ */ jsxs(
1178
+ "div",
1179
+ {
1180
+ style: {
1181
+ display: "flex",
1182
+ alignItems: "center",
1183
+ gap: "6px",
1184
+ marginBottom: "4px"
1185
+ },
1186
+ children: [
1187
+ /* @__PURE__ */ jsx(
1188
+ "span",
1189
+ {
1190
+ "data-testid": "extraction-status-dot",
1191
+ "aria-hidden": "true",
1192
+ style: {
1193
+ width: "6px",
1194
+ height: "6px",
1195
+ borderRadius: "50%",
1196
+ background: themeVar("stateSuccess"),
1197
+ animation: "ov-pulse 1.2s ease-in-out infinite",
1198
+ flexShrink: 0
1199
+ }
1200
+ }
1201
+ ),
1202
+ /* @__PURE__ */ jsx("span", { "data-testid": "extraction-running-label", children: "Extracting\u2026" })
1203
+ ]
1204
+ }
1205
+ ),
1206
+ /* @__PURE__ */ jsx(
1207
+ "div",
1208
+ {
1209
+ "data-testid": "extraction-indeterminate-track",
1210
+ style: {
1211
+ width: "100%",
1212
+ height: "6px",
1213
+ borderRadius: "3px",
1214
+ backgroundColor: themeVar("insetSurface"),
1215
+ overflow: "hidden",
1216
+ position: "relative"
1217
+ },
1218
+ children: /* @__PURE__ */ jsx(
1219
+ "div",
1220
+ {
1221
+ "data-testid": "extraction-indeterminate-fill",
1222
+ style: {
1223
+ position: "absolute",
1224
+ left: 0,
1225
+ top: 0,
1226
+ height: "100%",
1227
+ width: "40%",
1228
+ borderRadius: "3px",
1229
+ backgroundColor: themeVar("stateSuccess"),
1230
+ animation: "ov-indeterminate 1.2s ease-in-out infinite"
1231
+ }
1232
+ }
1233
+ )
1234
+ }
1235
+ )
1236
+ ] }) : failedActive && lastFailed ? /* @__PURE__ */ jsxs(
1237
+ "div",
1238
+ {
1239
+ "data-testid": "extraction-failed-line",
1240
+ style: {
1241
+ display: "flex",
1242
+ alignItems: "center",
1243
+ gap: "6px",
1244
+ color: themeVar("stateWarning")
1245
+ },
1246
+ children: [
1247
+ /* @__PURE__ */ jsx(
1248
+ "span",
1249
+ {
1250
+ "aria-hidden": "true",
1251
+ style: { display: "inline-flex", flexShrink: 0 },
1252
+ children: /* @__PURE__ */ jsx(WarningGlyph, { size: 12, testId: "extraction-warning-glyph" })
1253
+ }
1254
+ ),
1255
+ /* @__PURE__ */ jsx("span", { "data-testid": "extraction-failed-text", children: lastFailed.error || "extraction failed" })
1256
+ ]
1257
+ }
1258
+ ) : lastCompleted ? /* @__PURE__ */ jsxs(
1259
+ "div",
1260
+ {
1261
+ style: {
1262
+ display: "flex",
1263
+ alignItems: "center",
1264
+ gap: "6px"
1265
+ },
1266
+ children: [
1267
+ /* @__PURE__ */ 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
+ flexShrink: 0
1278
+ }
1279
+ }
1280
+ ),
1281
+ /* @__PURE__ */ jsx("span", { "data-testid": "extraction-last-line", children: formatExtractionSummary(lastCompleted) })
1282
+ ]
1283
+ }
1284
+ ) : null,
1285
+ backlog && /* @__PURE__ */ jsx(
1286
+ "div",
1287
+ {
1288
+ "data-testid": "extraction-backlog-line",
1289
+ style: { ...mutedStyle, marginTop: "4px" },
1290
+ children: backlog
1291
+ }
1292
+ ),
1293
+ logTaskId && client && /* @__PURE__ */ jsxs("div", { style: { marginTop: "6px" }, children: [
1294
+ /* @__PURE__ */ jsx(
1295
+ "button",
1296
+ {
1297
+ type: "button",
1298
+ "data-testid": "extraction-show-log-btn",
1299
+ onClick: () => void toggleLog(),
1300
+ style: {
1301
+ background: "none",
1302
+ border: "none",
1303
+ padding: 0,
1304
+ cursor: "pointer",
1305
+ font: "inherit",
1306
+ color: themeVar("labelTertiary"),
1307
+ textDecoration: "underline"
1308
+ },
1309
+ "aria-expanded": showLog,
1310
+ children: showLog ? "Hide log" : "Show log"
1311
+ }
1312
+ ),
1313
+ showLog && /* @__PURE__ */ jsxs(
1314
+ "div",
1315
+ {
1316
+ "data-testid": "extraction-log",
1317
+ style: {
1318
+ marginTop: "6px",
1319
+ display: "flex",
1320
+ flexDirection: "column",
1321
+ gap: "2px",
1322
+ maxHeight: "120px",
1323
+ overflowY: "auto"
1324
+ },
1325
+ children: [
1326
+ loadingLog && /* @__PURE__ */ jsx("div", { "data-testid": "extraction-log-loading", style: mutedStyle, children: "Loading\u2026" }),
1327
+ logError && /* @__PURE__ */ jsx("div", { "data-testid": "extraction-log-error", style: mutedStyle, children: logError }),
1328
+ events && events.length === 0 && !loadingLog && !logError && /* @__PURE__ */ jsx("div", { "data-testid": "extraction-log-empty", style: mutedStyle, children: "No events" }),
1329
+ events?.map((ev, idx) => /* @__PURE__ */ jsxs(
1330
+ "div",
1331
+ {
1332
+ "data-testid": "extraction-log-event",
1333
+ style: {
1334
+ display: "flex",
1335
+ justifyContent: "space-between",
1336
+ gap: "8px"
1337
+ },
1338
+ children: [
1339
+ /* @__PURE__ */ jsx("span", { children: ev.status || ev.kind || "event" }),
1340
+ /* @__PURE__ */ jsx("span", { style: mutedStyle, children: formatRelativeTime(ev.recorded_at) || "" })
1341
+ ]
1342
+ },
1343
+ `${ev.seq ?? idx}-${ev.recorded_at ?? idx}`
1344
+ ))
1345
+ ]
1346
+ }
1347
+ )
1348
+ ] })
1349
+ ] });
1350
+ }
809
1351
  function OpenVikingStatusPopover({
810
1352
  sessionId,
811
1353
  health,
@@ -815,6 +1357,8 @@ function OpenVikingStatusPopover({
815
1357
  endpoint,
816
1358
  isCommitting = false,
817
1359
  commitError = null,
1360
+ breakdown = null,
1361
+ client,
818
1362
  onCommitNow,
819
1363
  onClose,
820
1364
  className,
@@ -895,7 +1439,7 @@ function OpenVikingStatusPopover({
895
1439
  ...style
896
1440
  },
897
1441
  children: [
898
- /* @__PURE__ */ jsx("style", { children: `@keyframes ov-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }` }),
1442
+ /* @__PURE__ */ 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%; } }` }),
899
1443
  /* @__PURE__ */ jsxs(
900
1444
  "div",
901
1445
  {
@@ -1178,6 +1722,7 @@ function OpenVikingStatusPopover({
1178
1722
  }
1179
1723
  )
1180
1724
  ] }),
1725
+ /* @__PURE__ */ jsx(ExtractionSection, { breakdown, client }),
1181
1726
  commitError && /* @__PURE__ */ jsx(
1182
1727
  "div",
1183
1728
  {
@@ -1235,6 +1780,81 @@ function OpenVikingStatusPopover({
1235
1780
  // src/client/OpenVikingStatusChip.tsx
1236
1781
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1237
1782
  var COMMIT_THRESHOLD = 2e4;
1783
+ var POLL_INTERVAL_IDLE_MS = 15e3;
1784
+ var POLL_INTERVAL_ACTIVE_MS = 2500;
1785
+ function shouldPollActive(breakdown) {
1786
+ if (!breakdown) return false;
1787
+ return breakdown.running >= 1 || breakdown.pending >= 1;
1788
+ }
1789
+ function getChipDotState({
1790
+ isOnline,
1791
+ sessionUnreadable,
1792
+ breakdown
1793
+ }) {
1794
+ if (!isOnline) return "offline";
1795
+ if (sessionUnreadable) return "session-unreadable";
1796
+ if (breakdown && breakdown.running >= 1) return "busy";
1797
+ if (breakdown && breakdown.failed >= 1) return "extraction-failed";
1798
+ return "online-idle";
1799
+ }
1800
+ function WarningGlyph({
1801
+ size = 11,
1802
+ testId = "extraction-warning-glyph"
1803
+ }) {
1804
+ return /* @__PURE__ */ jsxs2(
1805
+ "svg",
1806
+ {
1807
+ "data-testid": testId,
1808
+ width: size,
1809
+ height: size,
1810
+ viewBox: "0 0 16 16",
1811
+ fill: "none",
1812
+ "aria-hidden": "true",
1813
+ style: { flexShrink: 0, display: "block" },
1814
+ children: [
1815
+ /* @__PURE__ */ jsx2("circle", { cx: "8", cy: "8", r: "6.5", stroke: "currentColor", strokeWidth: "1.5" }),
1816
+ /* @__PURE__ */ jsx2(
1817
+ "path",
1818
+ {
1819
+ d: "M8 5v3.5",
1820
+ stroke: "currentColor",
1821
+ strokeWidth: "1.5",
1822
+ strokeLinecap: "round"
1823
+ }
1824
+ ),
1825
+ /* @__PURE__ */ jsx2("circle", { cx: "8", cy: "11", r: "0.9", fill: "currentColor" })
1826
+ ]
1827
+ }
1828
+ );
1829
+ }
1830
+ function seedRunningTask(prev, taskId, resourceId) {
1831
+ const existing = prev && prev.status === "ok" ? prev.tasks : [];
1832
+ if (existing.some((t) => t.task_id === taskId)) {
1833
+ return prev;
1834
+ }
1835
+ const seeded = {
1836
+ task_id: taskId,
1837
+ status: "running",
1838
+ resource_id: resourceId,
1839
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
1840
+ };
1841
+ const tasks = [seeded, ...existing];
1842
+ return { status: "ok", breakdown: computeBreakdown(tasks), tasks };
1843
+ }
1844
+ function getDotColorForState(state) {
1845
+ switch (state) {
1846
+ case "offline":
1847
+ return themeVar("stateError");
1848
+ case "session-unreadable":
1849
+ case "extraction-failed":
1850
+ return themeVar("stateWarning");
1851
+ case "busy":
1852
+ return themeVar("stateSuccess");
1853
+ case "online-idle":
1854
+ default:
1855
+ return themeVar("stateSuccess");
1856
+ }
1857
+ }
1238
1858
  function formatPendingTokens(pendingTokens) {
1239
1859
  const k = Math.round((pendingTokens || 0) / 1e3);
1240
1860
  return `${k}k pend`;
@@ -1337,6 +1957,7 @@ function StatusChipView({
1337
1957
  initialHealth,
1338
1958
  initialSessionData,
1339
1959
  initialSessionRead,
1960
+ initialTasksRead,
1340
1961
  initialOpen = false
1341
1962
  }) {
1342
1963
  const [health, setHealth] = useState2(
@@ -1345,6 +1966,9 @@ function StatusChipView({
1345
1966
  const [sessionRead, setSessionRead] = useState2(
1346
1967
  initialSessionRead ?? (initialSessionData ? { status: "ok", session: initialSessionData } : null)
1347
1968
  );
1969
+ const [tasksRead, setTasksRead] = useState2(
1970
+ initialTasksRead ?? null
1971
+ );
1348
1972
  const [isOpen, setIsOpen] = useState2(initialOpen);
1349
1973
  const [isCommitting, setIsCommitting] = useState2(false);
1350
1974
  const [commitError, setCommitError] = useState2(null);
@@ -1380,16 +2004,24 @@ function StatusChipView({
1380
2004
  if (!healthRes.ok) {
1381
2005
  return;
1382
2006
  }
1383
- setSessionRead(await apiClient.readSession(sessionId));
2007
+ const [session, tasks] = await Promise.all([
2008
+ apiClient.readSession(sessionId),
2009
+ apiClient.listTasks(sessionId)
2010
+ ]);
2011
+ setSessionRead(session);
2012
+ setTasksRead(tasks);
1384
2013
  } catch {
1385
2014
  setHealth({ ok: false });
1386
2015
  }
1387
2016
  }, [sessionId, apiClient]);
2017
+ const breakdown = tasksRead?.status === "ok" ? tasksRead.breakdown : null;
2018
+ const hasActivePhase = shouldPollActive(breakdown);
1388
2019
  useEffect2(() => {
1389
2020
  fetchStatus();
1390
- const timer = setInterval(fetchStatus, 15e3);
2021
+ const interval = hasActivePhase ? POLL_INTERVAL_ACTIVE_MS : POLL_INTERVAL_IDLE_MS;
2022
+ const timer = setInterval(fetchStatus, interval);
1391
2023
  return () => clearInterval(timer);
1392
- }, [fetchStatus]);
2024
+ }, [fetchStatus, hasActivePhase]);
1393
2025
  useEffect2(() => {
1394
2026
  function handleClickOutside(event) {
1395
2027
  if (popoverRef.current && !popoverRef.current.contains(event.target)) {
@@ -1419,6 +2051,11 @@ function StatusChipView({
1419
2051
  keep_recent_count: 10
1420
2052
  });
1421
2053
  if (res.ok) {
2054
+ if (res.task_id) {
2055
+ setTasksRead(
2056
+ (prev) => seedRunningTask(prev, res.task_id, res.resource_id)
2057
+ );
2058
+ }
1422
2059
  await fetchStatus();
1423
2060
  onCommit?.();
1424
2061
  } else {
@@ -1437,11 +2074,15 @@ function StatusChipView({
1437
2074
  const sessionUnreadable = isOnline && sessionRead !== null && sessionRead.status !== "ok";
1438
2075
  const pendingTokens = sessionData?.pending_tokens ?? 0;
1439
2076
  const recalledCount = recalledResult.recalledCount;
1440
- const statusColor = getStatusIndicatorColor(
2077
+ const rawDotState = getChipDotState({
1441
2078
  isOnline,
1442
- isCommitting,
1443
- sessionUnreadable
1444
- );
2079
+ sessionUnreadable,
2080
+ breakdown
2081
+ });
2082
+ const dotState = isCommitting && (rawDotState === "online-idle" || rawDotState === "busy") ? "busy" : rawDotState;
2083
+ const dotBusy = dotState === "busy";
2084
+ const dotFailed = dotState === "extraction-failed";
2085
+ const statusColor = getDotColorForState(dotState);
1445
2086
  const tooltipTitle = formatTooltipTitle({
1446
2087
  isOnline,
1447
2088
  isCommitting,
@@ -1458,6 +2099,7 @@ function StatusChipView({
1458
2099
  style: { minWidth: 0, display: "inline-flex", position: "relative" },
1459
2100
  ref: popoverRef,
1460
2101
  children: [
2102
+ /* @__PURE__ */ jsx2("style", { children: `@keyframes ov-pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.35; transform: scale(0.72); } }` }),
1461
2103
  /* @__PURE__ */ jsxs2(
1462
2104
  "button",
1463
2105
  {
@@ -1488,17 +2130,32 @@ function StatusChipView({
1488
2130
  title: tooltipTitle,
1489
2131
  "aria-label": tooltipTitle,
1490
2132
  children: [
1491
- /* @__PURE__ */ jsx2(
2133
+ dotFailed ? /* @__PURE__ */ jsx2(
1492
2134
  "span",
1493
2135
  {
1494
2136
  "data-testid": "status-dot",
2137
+ "data-dot-state": dotState,
2138
+ "aria-hidden": "true",
2139
+ style: {
2140
+ display: "inline-flex",
2141
+ color: statusColor,
2142
+ flexShrink: 0
2143
+ },
2144
+ children: /* @__PURE__ */ jsx2(WarningGlyph, { size: 11, testId: "chip-warning-glyph" })
2145
+ }
2146
+ ) : /* @__PURE__ */ jsx2(
2147
+ "span",
2148
+ {
2149
+ "data-testid": "status-dot",
2150
+ "data-dot-state": dotBusy ? "busy" : dotState,
1495
2151
  "aria-hidden": "true",
1496
2152
  style: {
1497
2153
  width: "6px",
1498
2154
  height: "6px",
1499
2155
  borderRadius: "50%",
1500
2156
  background: statusColor,
1501
- flexShrink: 0
2157
+ flexShrink: 0,
2158
+ ...dotBusy ? { animation: "ov-pulse 1.2s ease-in-out infinite" } : {}
1502
2159
  }
1503
2160
  }
1504
2161
  ),
@@ -1517,6 +2174,8 @@ function StatusChipView({
1517
2174
  endpoint: apiClient.endpoint,
1518
2175
  isCommitting,
1519
2176
  commitError,
2177
+ breakdown,
2178
+ client: apiClient,
1520
2179
  onCommitNow: handleCommitNow,
1521
2180
  onClose: () => setIsOpen(false)
1522
2181
  }
@@ -2130,26 +2789,38 @@ function apply(ctx) {
2130
2789
  export {
2131
2790
  COMMIT_THRESHOLD,
2132
2791
  DEFAULT_OPENVIKING_ENDPOINT,
2792
+ ExtractionSection,
2133
2793
  OpenVikingClient,
2134
2794
  OpenVikingSettingsSection,
2135
2795
  OpenVikingStatusChip,
2136
2796
  OpenVikingStatusPopover,
2797
+ POLL_INTERVAL_ACTIVE_MS,
2798
+ POLL_INTERVAL_IDLE_MS,
2137
2799
  PROXY_OPENVIKING_ENDPOINT,
2800
+ TASKS_LIMIT_CAP,
2138
2801
  THEME,
2802
+ WarningGlyph,
2139
2803
  apply,
2140
2804
  chatNodesToText,
2141
2805
  checkHealth,
2142
2806
  commitSession,
2807
+ computeBreakdown,
2143
2808
  defaultOpenVikingClient,
2809
+ extractTaskList,
2144
2810
  fetchSession,
2811
+ formatBacklog,
2145
2812
  formatDaemonVersion,
2813
+ formatDuration,
2146
2814
  formatEndpoint,
2815
+ formatExtractionSummary,
2147
2816
  formatMemoryLeafName,
2148
2817
  formatPendingTokens,
2149
2818
  formatRelativeTime,
2150
2819
  formatStatusLabel,
2151
2820
  formatTooltipTitle,
2152
2821
  getCategoryBadgeStyle,
2822
+ getChipDotState,
2823
+ getDotColorForState,
2153
2824
  getProgressBarColor,
2154
2825
  getProgressBarPercent,
2155
2826
  getSession,
@@ -2158,9 +2829,15 @@ export {
2158
2829
  inferCategory,
2159
2830
  inject,
2160
2831
  name,
2832
+ normalizeExecutionEvent,
2833
+ normalizeExtractionTask,
2834
+ normalizeTaskList,
2835
+ normalizeTimestamp,
2161
2836
  parseRecalledMemories,
2162
2837
  resolveApiKey,
2163
2838
  resolveEndpoint,
2839
+ seedRunningTask,
2840
+ shouldPollActive,
2164
2841
  themeVar,
2165
2842
  truncateSessionId
2166
2843
  };