@dipertq/dsh-openviking-status 0.2.2 → 0.3.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.
package/lib/client.js CHANGED
@@ -16,6 +16,71 @@ 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 normalizeExtractionTask(raw) {
21
+ if (!raw || typeof raw !== "object") return null;
22
+ const taskId = typeof raw.task_id === "string" && raw.task_id || typeof raw.id === "string" && raw.id || "";
23
+ if (!taskId) return null;
24
+ const statusRaw = typeof raw.status === "string" ? raw.status : "";
25
+ const status = statusRaw === "running" || statusRaw === "pending" || statusRaw === "completed" || statusRaw === "failed" ? statusRaw : "pending";
26
+ const result = raw.result ?? {};
27
+ const extracted = result.memories_extracted ?? {};
28
+ const numberOf = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
29
+ const tokenUsageRaw = result.token_usage ?? raw.token_usage;
30
+ const tokenUsage = typeof tokenUsageRaw === "number" ? tokenUsageRaw : numberOf(
31
+ tokenUsageRaw?.total_tokens
32
+ );
33
+ const errorRaw = raw.error;
34
+ const errorMsg = typeof errorRaw === "string" ? errorRaw : typeof errorRaw?.message === "string" ? errorRaw.message : void 0;
35
+ return {
36
+ task_id: taskId,
37
+ status,
38
+ resource_id: typeof raw.resource_id === "string" ? raw.resource_id : void 0,
39
+ created_at: typeof raw.created_at === "string" ? raw.created_at : void 0,
40
+ updated_at: typeof raw.updated_at === "string" ? raw.updated_at : void 0,
41
+ memory_write: numberOf(extracted.memory_write ?? extracted.written),
42
+ memory_edit: numberOf(extracted.memory_edit ?? extracted.edited),
43
+ token_usage: tokenUsage,
44
+ error: errorMsg
45
+ };
46
+ }
47
+ function computeBreakdown(tasks) {
48
+ const b = {
49
+ running: 0,
50
+ pending: 0,
51
+ completed: 0,
52
+ failed: 0,
53
+ total: tasks.length,
54
+ firstRunning: null,
55
+ lastCompleted: null,
56
+ lastFailed: null
57
+ };
58
+ for (const t of tasks) {
59
+ if (t.status === "running") {
60
+ b.running += 1;
61
+ if (!b.firstRunning) b.firstRunning = t;
62
+ } else if (t.status === "pending") b.pending += 1;
63
+ else if (t.status === "completed") {
64
+ b.completed += 1;
65
+ if (!b.lastCompleted) b.lastCompleted = t;
66
+ } else if (t.status === "failed") {
67
+ b.failed += 1;
68
+ if (!b.lastFailed) b.lastFailed = t;
69
+ }
70
+ }
71
+ return b;
72
+ }
73
+ function normalizeExecutionEvent(raw) {
74
+ return {
75
+ seq: typeof raw.seq === "number" ? raw.seq : void 0,
76
+ recorded_at: typeof raw.recorded_at === "string" ? raw.recorded_at : void 0,
77
+ kind: typeof raw.kind === "string" ? raw.kind : void 0,
78
+ status: typeof raw.status === "string" ? raw.status : void 0,
79
+ stage: typeof raw.stage === "string" ? raw.stage : null,
80
+ operation: typeof raw.operation === "string" ? raw.operation : null,
81
+ error: typeof raw.error === "string" ? raw.error : null
82
+ };
83
+ }
19
84
  var DEFAULT_OPENVIKING_ENDPOINT = "http://127.0.0.1:1933";
20
85
  var PROXY_OPENVIKING_ENDPOINT = "/openviking-status/api";
21
86
  function resolveEndpoint(endpoint) {
@@ -324,7 +389,9 @@ var OpenVikingClient = class {
324
389
  const data = await res.json().catch(() => ({}));
325
390
  return {
326
391
  ok: data.ok === true,
327
- error: typeof data.error === "string" ? data.error : void 0
392
+ error: typeof data.error === "string" ? data.error : void 0,
393
+ task_id: typeof data.task_id === "string" ? data.task_id : void 0,
394
+ resource_id: typeof data.resource_id === "string" ? data.resource_id : void 0
328
395
  };
329
396
  } catch (err) {
330
397
  return {
@@ -356,7 +423,14 @@ var OpenVikingClient = class {
356
423
  return { ok: false, error: String(errorMsg) };
357
424
  }
358
425
  this.resolvedSessionIds.set(sessionId.trim(), candidateId);
359
- return { ok: true };
426
+ const okBody = await res.json().catch(() => ({}));
427
+ const container = okBody.result ?? okBody.data ?? okBody;
428
+ const taskId = container.task_id ?? container.id;
429
+ return {
430
+ ok: true,
431
+ task_id: typeof taskId === "string" && taskId.trim() ? taskId.trim() : void 0,
432
+ resource_id: candidateId
433
+ };
360
434
  } catch (err) {
361
435
  return {
362
436
  ok: false,
@@ -366,7 +440,193 @@ var OpenVikingClient = class {
366
440
  }
367
441
  return { ok: false, error: lastError };
368
442
  }
443
+ /**
444
+ * Список задач Phase 2 (Memory Extraction) текущей сессии со сводной
445
+ * разбивкой по статусам.
446
+ *
447
+ * Через прокси: один запрос `GET /tasks?session=<id>`, разбивка считается
448
+ * здесь. Напрямую: разрешаем `resource_id` через кандидатов и запрашиваем
449
+ * `GET /api/v1/tasks?resource_id=<id>&task_type=session_commit&limit=200`.
450
+ */
451
+ async listTasks(sessionId) {
452
+ if (!sessionId || !sessionId.trim()) {
453
+ return { status: "missing" };
454
+ }
455
+ if (this.isProxy()) {
456
+ try {
457
+ const res = await fetch(
458
+ `${this.endpoint}/tasks?session=${encodeURIComponent(
459
+ sessionId.trim()
460
+ )}&limit=${TASKS_LIMIT_CAP}`,
461
+ { method: "GET", headers: this.getHeaders() }
462
+ );
463
+ if (res.status === 401 || res.status === 403) {
464
+ return { status: "unauthorized" };
465
+ }
466
+ if (!res.ok) {
467
+ return { status: "error", detail: `HTTP ${res.status}` };
468
+ }
469
+ const data = await res.json();
470
+ if (data.status === "unauthorized") return { status: "unauthorized" };
471
+ if (data.status === "missing") return { status: "missing" };
472
+ if (data.status === "unreachable")
473
+ return {
474
+ status: "unreachable",
475
+ detail: typeof data.detail === "string" ? data.detail : void 0
476
+ };
477
+ if (data.status === "error")
478
+ return {
479
+ status: "error",
480
+ detail: typeof data.detail === "string" ? data.detail : void 0
481
+ };
482
+ const tasks = normalizeTaskList(data.tasks);
483
+ return { status: "ok", breakdown: computeBreakdown(tasks), tasks };
484
+ } catch (err) {
485
+ return {
486
+ status: "unreachable",
487
+ detail: err instanceof Error ? err.message : String(err)
488
+ };
489
+ }
490
+ }
491
+ const candidates = this.getCandidateSessionIds(sessionId);
492
+ let resourceId = null;
493
+ for (const candidateId of candidates) {
494
+ try {
495
+ const probe = await fetch(
496
+ `${this.endpoint}/api/v1/sessions/${encodeURIComponent(candidateId)}`,
497
+ { method: "GET", headers: this.getHeaders() }
498
+ );
499
+ if (probe.status === 404) continue;
500
+ if (probe.status === 401 || probe.status === 403) {
501
+ return { status: "unauthorized" };
502
+ }
503
+ if (probe.ok) {
504
+ resourceId = candidateId;
505
+ this.resolvedSessionIds.set(sessionId.trim(), candidateId);
506
+ break;
507
+ }
508
+ } catch (err) {
509
+ return {
510
+ status: "unreachable",
511
+ detail: err instanceof Error ? err.message : String(err)
512
+ };
513
+ }
514
+ }
515
+ if (!resourceId) return { status: "missing" };
516
+ try {
517
+ const query = new URLSearchParams({
518
+ resource_id: resourceId,
519
+ task_type: "session_commit",
520
+ limit: String(TASKS_LIMIT_CAP)
521
+ });
522
+ const res = await fetch(
523
+ `${this.endpoint}/api/v1/tasks?${query.toString()}`,
524
+ { method: "GET", headers: this.getHeaders() }
525
+ );
526
+ if (res.status === 401 || res.status === 403) {
527
+ return { status: "unauthorized" };
528
+ }
529
+ if (!res.ok) {
530
+ return { status: "error", detail: `HTTP ${res.status}` };
531
+ }
532
+ const data = await res.json();
533
+ const items = data?.items ?? data?.tasks ?? data?.result?.items ?? (Array.isArray(data) ? data : []);
534
+ const tasks = normalizeTaskList(items);
535
+ return { status: "ok", breakdown: computeBreakdown(tasks), tasks };
536
+ } catch (err) {
537
+ return {
538
+ status: "unreachable",
539
+ detail: err instanceof Error ? err.message : String(err)
540
+ };
541
+ }
542
+ }
543
+ /**
544
+ * Лениво загрузить одну задачу с лентой `execution_events` для «Show log».
545
+ */
546
+ async fetchTaskEvents(taskId) {
547
+ if (!taskId || !taskId.trim()) {
548
+ return { status: "missing" };
549
+ }
550
+ const buildOk = (taskRaw) => {
551
+ const task = normalizeExtractionTask(taskRaw);
552
+ const eventsContainer = taskRaw.execution_events ?? {};
553
+ const rawEvents = eventsContainer.items ?? (Array.isArray(taskRaw.execution_events) ? taskRaw.execution_events : []);
554
+ const events = (Array.isArray(rawEvents) ? rawEvents : []).filter(
555
+ (e) => !!e && typeof e === "object"
556
+ ).map(normalizeExecutionEvent);
557
+ if (!task) return { status: "error", detail: "malformed task" };
558
+ return { status: "ok", task, events };
559
+ };
560
+ if (this.isProxy()) {
561
+ try {
562
+ const res = await fetch(
563
+ `${this.endpoint}/task?id=${encodeURIComponent(
564
+ taskId.trim()
565
+ )}&events=1`,
566
+ { method: "GET", headers: this.getHeaders() }
567
+ );
568
+ if (res.status === 401 || res.status === 403) {
569
+ return { status: "unauthorized" };
570
+ }
571
+ if (!res.ok) {
572
+ return { status: "error", detail: `HTTP ${res.status}` };
573
+ }
574
+ const data = await res.json();
575
+ if (data.status === "unauthorized") return { status: "unauthorized" };
576
+ if (data.status === "missing") return { status: "missing" };
577
+ if (data.status === "unreachable")
578
+ return {
579
+ status: "unreachable",
580
+ detail: typeof data.detail === "string" ? data.detail : void 0
581
+ };
582
+ if (data.status === "error")
583
+ return {
584
+ status: "error",
585
+ detail: typeof data.detail === "string" ? data.detail : void 0
586
+ };
587
+ return buildOk(data.task ?? {});
588
+ } catch (err) {
589
+ return {
590
+ status: "unreachable",
591
+ detail: err instanceof Error ? err.message : String(err)
592
+ };
593
+ }
594
+ }
595
+ try {
596
+ const res = await fetch(
597
+ `${this.endpoint}/api/v1/tasks/${encodeURIComponent(
598
+ taskId.trim()
599
+ )}?include_events=true`,
600
+ { method: "GET", headers: this.getHeaders() }
601
+ );
602
+ if (res.status === 404) return { status: "missing" };
603
+ if (res.status === 401 || res.status === 403) {
604
+ return { status: "unauthorized" };
605
+ }
606
+ if (!res.ok) {
607
+ return { status: "error", detail: `HTTP ${res.status}` };
608
+ }
609
+ const data = await res.json();
610
+ const taskRaw = data?.result ?? data?.data ?? data;
611
+ return buildOk(taskRaw);
612
+ } catch (err) {
613
+ return {
614
+ status: "unreachable",
615
+ detail: err instanceof Error ? err.message : String(err)
616
+ };
617
+ }
618
+ }
369
619
  };
620
+ function normalizeTaskList(raw) {
621
+ if (!Array.isArray(raw)) return [];
622
+ const tasks = [];
623
+ for (const entry of raw) {
624
+ if (!entry || typeof entry !== "object") continue;
625
+ const task = normalizeExtractionTask(entry);
626
+ if (task) tasks.push(task);
627
+ }
628
+ return tasks;
629
+ }
370
630
  var defaultOpenVikingClient = new OpenVikingClient();
371
631
  function checkHealth(endpoint, apiKey) {
372
632
  const client = endpoint || apiKey ? new OpenVikingClient(endpoint, apiKey) : defaultOpenVikingClient;
@@ -740,6 +1000,39 @@ function dshIcon(name2) {
740
1000
  return null;
741
1001
  }
742
1002
  }
1003
+ function formatDuration(startIso, endIso) {
1004
+ if (!startIso || !endIso) return void 0;
1005
+ const start = new Date(startIso).getTime();
1006
+ const end = new Date(endIso).getTime();
1007
+ if (isNaN(start) || isNaN(end) || end < start) return void 0;
1008
+ const ms = end - start;
1009
+ const sec = ms / 1e3;
1010
+ if (sec < 60) {
1011
+ return `${sec < 10 ? sec.toFixed(1) : Math.round(sec)}s`;
1012
+ }
1013
+ const min = Math.floor(sec / 60);
1014
+ const rem = Math.round(sec % 60);
1015
+ return `${min}m ${rem}s`;
1016
+ }
1017
+ function formatExtractionSummary(task, now = Date.now()) {
1018
+ const write = task.memory_write ?? 0;
1019
+ const edit = task.memory_edit ?? 0;
1020
+ const parts = [`${write} written, ${edit} edited`];
1021
+ const duration = formatDuration(task.created_at, task.updated_at);
1022
+ if (duration) parts.push(duration);
1023
+ const rel = formatRelativeTime(task.updated_at, now);
1024
+ if (rel) parts.push(rel);
1025
+ return parts.join(" \xB7 ");
1026
+ }
1027
+ function formatBacklog(breakdown) {
1028
+ if (!breakdown) return null;
1029
+ const { pending, failed } = breakdown;
1030
+ if (pending <= 0 && failed <= 0) return null;
1031
+ const parts = [];
1032
+ if (pending > 0) parts.push(`${pending} pending`);
1033
+ if (failed > 0) parts.push(`${failed} failed`);
1034
+ return parts.join(" / ");
1035
+ }
743
1036
  function getProgressBarPercent(pendingTokens, threshold = COMMIT_THRESHOLD) {
744
1037
  if (!threshold || threshold <= 0) return 0;
745
1038
  const ratio = (pendingTokens || 0) / threshold;
@@ -806,6 +1099,229 @@ function handleEscapeKey(event, onClose) {
806
1099
  }
807
1100
  return false;
808
1101
  }
1102
+ function ExtractionSection({
1103
+ breakdown,
1104
+ client
1105
+ }) {
1106
+ const [showLog, setShowLog] = useState(false);
1107
+ const [events, setEvents] = useState(null);
1108
+ const [logError, setLogError] = useState(null);
1109
+ const [loadingLog, setLoadingLog] = useState(false);
1110
+ if (!breakdown) return null;
1111
+ const isRunning = breakdown.running >= 1;
1112
+ const lastCompleted = breakdown.lastCompleted;
1113
+ const lastFailed = breakdown.lastFailed;
1114
+ const backlog = formatBacklog(breakdown);
1115
+ const failedActive = !isRunning && breakdown.failed >= 1;
1116
+ const logTaskId = (isRunning ? breakdown.firstRunning?.task_id : null) ?? (failedActive ? lastFailed?.task_id : null) ?? lastCompleted?.task_id ?? lastFailed?.task_id ?? null;
1117
+ const mutedStyle = { color: themeVar("labelTertiary") };
1118
+ const toggleLog = useCallback(async () => {
1119
+ const next = !showLog;
1120
+ setShowLog(next);
1121
+ if (next && events === null && logTaskId && client) {
1122
+ setLoadingLog(true);
1123
+ setLogError(null);
1124
+ const res = await client.fetchTaskEvents(logTaskId);
1125
+ setLoadingLog(false);
1126
+ if (res.status === "ok") {
1127
+ setEvents(res.events);
1128
+ } else {
1129
+ setLogError(
1130
+ res.status === "unauthorized" ? "No access to task log" : "Could not load log"
1131
+ );
1132
+ }
1133
+ }
1134
+ }, [showLog, events, logTaskId, client]);
1135
+ if (!isRunning && !failedActive && !lastCompleted && !backlog) {
1136
+ return null;
1137
+ }
1138
+ return /* @__PURE__ */ jsxs("div", { "data-testid": "extraction-section", style: { marginBottom: "12px" }, children: [
1139
+ /* @__PURE__ */ jsx(
1140
+ "div",
1141
+ {
1142
+ style: {
1143
+ marginBottom: "4px",
1144
+ color: themeVar("labelPrimary"),
1145
+ fontWeight: 500
1146
+ },
1147
+ children: "Memory Extraction"
1148
+ }
1149
+ ),
1150
+ isRunning ? /* @__PURE__ */ jsxs("div", { children: [
1151
+ /* @__PURE__ */ jsxs(
1152
+ "div",
1153
+ {
1154
+ style: {
1155
+ display: "flex",
1156
+ alignItems: "center",
1157
+ gap: "6px",
1158
+ marginBottom: "4px"
1159
+ },
1160
+ children: [
1161
+ /* @__PURE__ */ jsx(
1162
+ "span",
1163
+ {
1164
+ "data-testid": "extraction-status-dot",
1165
+ "aria-hidden": "true",
1166
+ style: {
1167
+ width: "6px",
1168
+ height: "6px",
1169
+ borderRadius: "50%",
1170
+ background: themeVar("stateSuccess"),
1171
+ animation: "ov-pulse 1.2s ease-in-out infinite",
1172
+ flexShrink: 0
1173
+ }
1174
+ }
1175
+ ),
1176
+ /* @__PURE__ */ jsx("span", { "data-testid": "extraction-running-label", children: "Extracting\u2026" })
1177
+ ]
1178
+ }
1179
+ ),
1180
+ /* @__PURE__ */ jsx(
1181
+ "div",
1182
+ {
1183
+ "data-testid": "extraction-indeterminate-track",
1184
+ style: {
1185
+ width: "100%",
1186
+ height: "6px",
1187
+ borderRadius: "3px",
1188
+ backgroundColor: themeVar("insetSurface"),
1189
+ overflow: "hidden",
1190
+ position: "relative"
1191
+ },
1192
+ children: /* @__PURE__ */ jsx(
1193
+ "div",
1194
+ {
1195
+ "data-testid": "extraction-indeterminate-fill",
1196
+ style: {
1197
+ position: "absolute",
1198
+ left: 0,
1199
+ top: 0,
1200
+ height: "100%",
1201
+ width: "40%",
1202
+ borderRadius: "3px",
1203
+ backgroundColor: themeVar("stateSuccess"),
1204
+ animation: "ov-indeterminate 1.2s ease-in-out infinite"
1205
+ }
1206
+ }
1207
+ )
1208
+ }
1209
+ )
1210
+ ] }) : failedActive && lastFailed ? /* @__PURE__ */ jsxs(
1211
+ "div",
1212
+ {
1213
+ "data-testid": "extraction-failed-line",
1214
+ style: {
1215
+ display: "flex",
1216
+ alignItems: "center",
1217
+ gap: "6px",
1218
+ color: themeVar("stateWarning")
1219
+ },
1220
+ children: [
1221
+ /* @__PURE__ */ jsx(
1222
+ "span",
1223
+ {
1224
+ "aria-hidden": "true",
1225
+ style: { display: "inline-flex", flexShrink: 0 },
1226
+ children: /* @__PURE__ */ jsx(WarningGlyph, { size: 12, testId: "extraction-warning-glyph" })
1227
+ }
1228
+ ),
1229
+ /* @__PURE__ */ jsx("span", { "data-testid": "extraction-failed-text", children: lastFailed.error || "extraction failed" })
1230
+ ]
1231
+ }
1232
+ ) : lastCompleted ? /* @__PURE__ */ jsxs(
1233
+ "div",
1234
+ {
1235
+ style: {
1236
+ display: "flex",
1237
+ alignItems: "center",
1238
+ gap: "6px"
1239
+ },
1240
+ children: [
1241
+ /* @__PURE__ */ jsx(
1242
+ "span",
1243
+ {
1244
+ "data-testid": "extraction-status-dot",
1245
+ "aria-hidden": "true",
1246
+ style: {
1247
+ width: "6px",
1248
+ height: "6px",
1249
+ borderRadius: "50%",
1250
+ background: themeVar("stateSuccess"),
1251
+ flexShrink: 0
1252
+ }
1253
+ }
1254
+ ),
1255
+ /* @__PURE__ */ jsx("span", { "data-testid": "extraction-last-line", children: formatExtractionSummary(lastCompleted) })
1256
+ ]
1257
+ }
1258
+ ) : null,
1259
+ backlog && /* @__PURE__ */ jsx(
1260
+ "div",
1261
+ {
1262
+ "data-testid": "extraction-backlog-line",
1263
+ style: { ...mutedStyle, marginTop: "4px" },
1264
+ children: backlog
1265
+ }
1266
+ ),
1267
+ logTaskId && client && /* @__PURE__ */ jsxs("div", { style: { marginTop: "6px" }, children: [
1268
+ /* @__PURE__ */ jsx(
1269
+ "button",
1270
+ {
1271
+ type: "button",
1272
+ "data-testid": "extraction-show-log-btn",
1273
+ onClick: () => void toggleLog(),
1274
+ style: {
1275
+ background: "none",
1276
+ border: "none",
1277
+ padding: 0,
1278
+ cursor: "pointer",
1279
+ font: "inherit",
1280
+ color: themeVar("labelTertiary"),
1281
+ textDecoration: "underline"
1282
+ },
1283
+ "aria-expanded": showLog,
1284
+ children: showLog ? "Hide log" : "Show log"
1285
+ }
1286
+ ),
1287
+ showLog && /* @__PURE__ */ jsxs(
1288
+ "div",
1289
+ {
1290
+ "data-testid": "extraction-log",
1291
+ style: {
1292
+ marginTop: "6px",
1293
+ display: "flex",
1294
+ flexDirection: "column",
1295
+ gap: "2px",
1296
+ maxHeight: "120px",
1297
+ overflowY: "auto"
1298
+ },
1299
+ children: [
1300
+ loadingLog && /* @__PURE__ */ jsx("div", { "data-testid": "extraction-log-loading", style: mutedStyle, children: "Loading\u2026" }),
1301
+ logError && /* @__PURE__ */ jsx("div", { "data-testid": "extraction-log-error", style: mutedStyle, children: logError }),
1302
+ events && events.length === 0 && !loadingLog && !logError && /* @__PURE__ */ jsx("div", { "data-testid": "extraction-log-empty", style: mutedStyle, children: "No events" }),
1303
+ events?.map((ev, idx) => /* @__PURE__ */ jsxs(
1304
+ "div",
1305
+ {
1306
+ "data-testid": "extraction-log-event",
1307
+ style: {
1308
+ display: "flex",
1309
+ justifyContent: "space-between",
1310
+ gap: "8px"
1311
+ },
1312
+ children: [
1313
+ /* @__PURE__ */ jsx("span", { children: ev.status || ev.kind || "event" }),
1314
+ /* @__PURE__ */ jsx("span", { style: mutedStyle, children: formatRelativeTime(ev.recorded_at) || "" })
1315
+ ]
1316
+ },
1317
+ `${ev.seq ?? idx}-${ev.recorded_at ?? idx}`
1318
+ ))
1319
+ ]
1320
+ }
1321
+ )
1322
+ ] })
1323
+ ] });
1324
+ }
809
1325
  function OpenVikingStatusPopover({
810
1326
  sessionId,
811
1327
  health,
@@ -815,6 +1331,8 @@ function OpenVikingStatusPopover({
815
1331
  endpoint,
816
1332
  isCommitting = false,
817
1333
  commitError = null,
1334
+ breakdown = null,
1335
+ client,
818
1336
  onCommitNow,
819
1337
  onClose,
820
1338
  className,
@@ -895,7 +1413,7 @@ function OpenVikingStatusPopover({
895
1413
  ...style
896
1414
  },
897
1415
  children: [
898
- /* @__PURE__ */ jsx("style", { children: `@keyframes ov-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }` }),
1416
+ /* @__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
1417
  /* @__PURE__ */ jsxs(
900
1418
  "div",
901
1419
  {
@@ -1178,6 +1696,7 @@ function OpenVikingStatusPopover({
1178
1696
  }
1179
1697
  )
1180
1698
  ] }),
1699
+ /* @__PURE__ */ jsx(ExtractionSection, { breakdown, client }),
1181
1700
  commitError && /* @__PURE__ */ jsx(
1182
1701
  "div",
1183
1702
  {
@@ -1235,6 +1754,77 @@ function OpenVikingStatusPopover({
1235
1754
  // src/client/OpenVikingStatusChip.tsx
1236
1755
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1237
1756
  var COMMIT_THRESHOLD = 2e4;
1757
+ var POLL_INTERVAL_IDLE_MS = 15e3;
1758
+ var POLL_INTERVAL_ACTIVE_MS = 2500;
1759
+ function getChipDotState({
1760
+ isOnline,
1761
+ sessionUnreadable,
1762
+ breakdown
1763
+ }) {
1764
+ if (!isOnline) return "offline";
1765
+ if (sessionUnreadable) return "session-unreadable";
1766
+ if (breakdown && breakdown.running >= 1) return "busy";
1767
+ if (breakdown && breakdown.failed >= 1) return "extraction-failed";
1768
+ return "online-idle";
1769
+ }
1770
+ function WarningGlyph({
1771
+ size = 11,
1772
+ testId = "extraction-warning-glyph"
1773
+ }) {
1774
+ return /* @__PURE__ */ jsxs2(
1775
+ "svg",
1776
+ {
1777
+ "data-testid": testId,
1778
+ width: size,
1779
+ height: size,
1780
+ viewBox: "0 0 16 16",
1781
+ fill: "none",
1782
+ "aria-hidden": "true",
1783
+ style: { flexShrink: 0, display: "block" },
1784
+ children: [
1785
+ /* @__PURE__ */ jsx2("circle", { cx: "8", cy: "8", r: "6.5", stroke: "currentColor", strokeWidth: "1.5" }),
1786
+ /* @__PURE__ */ jsx2(
1787
+ "path",
1788
+ {
1789
+ d: "M8 5v3.5",
1790
+ stroke: "currentColor",
1791
+ strokeWidth: "1.5",
1792
+ strokeLinecap: "round"
1793
+ }
1794
+ ),
1795
+ /* @__PURE__ */ jsx2("circle", { cx: "8", cy: "11", r: "0.9", fill: "currentColor" })
1796
+ ]
1797
+ }
1798
+ );
1799
+ }
1800
+ function seedRunningTask(prev, taskId, resourceId) {
1801
+ const existing = prev && prev.status === "ok" ? prev.tasks : [];
1802
+ if (existing.some((t) => t.task_id === taskId)) {
1803
+ return prev;
1804
+ }
1805
+ const seeded = {
1806
+ task_id: taskId,
1807
+ status: "running",
1808
+ resource_id: resourceId,
1809
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
1810
+ };
1811
+ const tasks = [seeded, ...existing];
1812
+ return { status: "ok", breakdown: computeBreakdown(tasks), tasks };
1813
+ }
1814
+ function getDotColorForState(state) {
1815
+ switch (state) {
1816
+ case "offline":
1817
+ return themeVar("stateError");
1818
+ case "session-unreadable":
1819
+ case "extraction-failed":
1820
+ return themeVar("stateWarning");
1821
+ case "busy":
1822
+ return themeVar("stateSuccess");
1823
+ case "online-idle":
1824
+ default:
1825
+ return themeVar("stateSuccess");
1826
+ }
1827
+ }
1238
1828
  function formatPendingTokens(pendingTokens) {
1239
1829
  const k = Math.round((pendingTokens || 0) / 1e3);
1240
1830
  return `${k}k pend`;
@@ -1337,6 +1927,7 @@ function StatusChipView({
1337
1927
  initialHealth,
1338
1928
  initialSessionData,
1339
1929
  initialSessionRead,
1930
+ initialTasksRead,
1340
1931
  initialOpen = false
1341
1932
  }) {
1342
1933
  const [health, setHealth] = useState2(
@@ -1345,6 +1936,9 @@ function StatusChipView({
1345
1936
  const [sessionRead, setSessionRead] = useState2(
1346
1937
  initialSessionRead ?? (initialSessionData ? { status: "ok", session: initialSessionData } : null)
1347
1938
  );
1939
+ const [tasksRead, setTasksRead] = useState2(
1940
+ initialTasksRead ?? null
1941
+ );
1348
1942
  const [isOpen, setIsOpen] = useState2(initialOpen);
1349
1943
  const [isCommitting, setIsCommitting] = useState2(false);
1350
1944
  const [commitError, setCommitError] = useState2(null);
@@ -1380,16 +1974,24 @@ function StatusChipView({
1380
1974
  if (!healthRes.ok) {
1381
1975
  return;
1382
1976
  }
1383
- setSessionRead(await apiClient.readSession(sessionId));
1977
+ const [session, tasks] = await Promise.all([
1978
+ apiClient.readSession(sessionId),
1979
+ apiClient.listTasks(sessionId)
1980
+ ]);
1981
+ setSessionRead(session);
1982
+ setTasksRead(tasks);
1384
1983
  } catch {
1385
1984
  setHealth({ ok: false });
1386
1985
  }
1387
1986
  }, [sessionId, apiClient]);
1987
+ const breakdown = tasksRead?.status === "ok" ? tasksRead.breakdown : null;
1988
+ const hasRunning = (breakdown?.running ?? 0) >= 1;
1388
1989
  useEffect2(() => {
1389
1990
  fetchStatus();
1390
- const timer = setInterval(fetchStatus, 15e3);
1991
+ const interval = hasRunning ? POLL_INTERVAL_ACTIVE_MS : POLL_INTERVAL_IDLE_MS;
1992
+ const timer = setInterval(fetchStatus, interval);
1391
1993
  return () => clearInterval(timer);
1392
- }, [fetchStatus]);
1994
+ }, [fetchStatus, hasRunning]);
1393
1995
  useEffect2(() => {
1394
1996
  function handleClickOutside(event) {
1395
1997
  if (popoverRef.current && !popoverRef.current.contains(event.target)) {
@@ -1419,6 +2021,11 @@ function StatusChipView({
1419
2021
  keep_recent_count: 10
1420
2022
  });
1421
2023
  if (res.ok) {
2024
+ if (res.task_id) {
2025
+ setTasksRead(
2026
+ (prev) => seedRunningTask(prev, res.task_id, res.resource_id)
2027
+ );
2028
+ }
1422
2029
  await fetchStatus();
1423
2030
  onCommit?.();
1424
2031
  } else {
@@ -1437,11 +2044,15 @@ function StatusChipView({
1437
2044
  const sessionUnreadable = isOnline && sessionRead !== null && sessionRead.status !== "ok";
1438
2045
  const pendingTokens = sessionData?.pending_tokens ?? 0;
1439
2046
  const recalledCount = recalledResult.recalledCount;
1440
- const statusColor = getStatusIndicatorColor(
2047
+ const rawDotState = getChipDotState({
1441
2048
  isOnline,
1442
- isCommitting,
1443
- sessionUnreadable
1444
- );
2049
+ sessionUnreadable,
2050
+ breakdown
2051
+ });
2052
+ const dotState = isCommitting && (rawDotState === "online-idle" || rawDotState === "busy") ? "busy" : rawDotState;
2053
+ const dotBusy = dotState === "busy";
2054
+ const dotFailed = dotState === "extraction-failed";
2055
+ const statusColor = getDotColorForState(dotState);
1445
2056
  const tooltipTitle = formatTooltipTitle({
1446
2057
  isOnline,
1447
2058
  isCommitting,
@@ -1458,6 +2069,7 @@ function StatusChipView({
1458
2069
  style: { minWidth: 0, display: "inline-flex", position: "relative" },
1459
2070
  ref: popoverRef,
1460
2071
  children: [
2072
+ /* @__PURE__ */ jsx2("style", { children: `@keyframes ov-pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.35; transform: scale(0.72); } }` }),
1461
2073
  /* @__PURE__ */ jsxs2(
1462
2074
  "button",
1463
2075
  {
@@ -1488,17 +2100,32 @@ function StatusChipView({
1488
2100
  title: tooltipTitle,
1489
2101
  "aria-label": tooltipTitle,
1490
2102
  children: [
1491
- /* @__PURE__ */ jsx2(
2103
+ dotFailed ? /* @__PURE__ */ jsx2(
1492
2104
  "span",
1493
2105
  {
1494
2106
  "data-testid": "status-dot",
2107
+ "data-dot-state": dotState,
2108
+ "aria-hidden": "true",
2109
+ style: {
2110
+ display: "inline-flex",
2111
+ color: statusColor,
2112
+ flexShrink: 0
2113
+ },
2114
+ children: /* @__PURE__ */ jsx2(WarningGlyph, { size: 11, testId: "chip-warning-glyph" })
2115
+ }
2116
+ ) : /* @__PURE__ */ jsx2(
2117
+ "span",
2118
+ {
2119
+ "data-testid": "status-dot",
2120
+ "data-dot-state": dotBusy ? "busy" : dotState,
1495
2121
  "aria-hidden": "true",
1496
2122
  style: {
1497
2123
  width: "6px",
1498
2124
  height: "6px",
1499
2125
  borderRadius: "50%",
1500
2126
  background: statusColor,
1501
- flexShrink: 0
2127
+ flexShrink: 0,
2128
+ ...dotBusy ? { animation: "ov-pulse 1.2s ease-in-out infinite" } : {}
1502
2129
  }
1503
2130
  }
1504
2131
  ),
@@ -1517,6 +2144,8 @@ function StatusChipView({
1517
2144
  endpoint: apiClient.endpoint,
1518
2145
  isCommitting,
1519
2146
  commitError,
2147
+ breakdown,
2148
+ client: apiClient,
1520
2149
  onCommitNow: handleCommitNow,
1521
2150
  onClose: () => setIsOpen(false)
1522
2151
  }
@@ -2130,26 +2759,37 @@ function apply(ctx) {
2130
2759
  export {
2131
2760
  COMMIT_THRESHOLD,
2132
2761
  DEFAULT_OPENVIKING_ENDPOINT,
2762
+ ExtractionSection,
2133
2763
  OpenVikingClient,
2134
2764
  OpenVikingSettingsSection,
2135
2765
  OpenVikingStatusChip,
2136
2766
  OpenVikingStatusPopover,
2767
+ POLL_INTERVAL_ACTIVE_MS,
2768
+ POLL_INTERVAL_IDLE_MS,
2137
2769
  PROXY_OPENVIKING_ENDPOINT,
2770
+ TASKS_LIMIT_CAP,
2138
2771
  THEME,
2772
+ WarningGlyph,
2139
2773
  apply,
2140
2774
  chatNodesToText,
2141
2775
  checkHealth,
2142
2776
  commitSession,
2777
+ computeBreakdown,
2143
2778
  defaultOpenVikingClient,
2144
2779
  fetchSession,
2780
+ formatBacklog,
2145
2781
  formatDaemonVersion,
2782
+ formatDuration,
2146
2783
  formatEndpoint,
2784
+ formatExtractionSummary,
2147
2785
  formatMemoryLeafName,
2148
2786
  formatPendingTokens,
2149
2787
  formatRelativeTime,
2150
2788
  formatStatusLabel,
2151
2789
  formatTooltipTitle,
2152
2790
  getCategoryBadgeStyle,
2791
+ getChipDotState,
2792
+ getDotColorForState,
2153
2793
  getProgressBarColor,
2154
2794
  getProgressBarPercent,
2155
2795
  getSession,
@@ -2158,9 +2798,13 @@ export {
2158
2798
  inferCategory,
2159
2799
  inject,
2160
2800
  name,
2801
+ normalizeExecutionEvent,
2802
+ normalizeExtractionTask,
2803
+ normalizeTaskList,
2161
2804
  parseRecalledMemories,
2162
2805
  resolveApiKey,
2163
2806
  resolveEndpoint,
2807
+ seedRunningTask,
2164
2808
  themeVar,
2165
2809
  truncateSessionId
2166
2810
  };