@dipertq/dsh-openviking-status 0.2.1 → 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,7 +16,73 @@ 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";
85
+ var PROXY_OPENVIKING_ENDPOINT = "/openviking-status/api";
20
86
  function resolveEndpoint(endpoint) {
21
87
  if (endpoint && endpoint.trim().length > 0) {
22
88
  return endpoint.trim().replace(/\/+$/, "");
@@ -26,6 +92,7 @@ function resolveEndpoint(endpoint) {
26
92
  if (typeof win.__OPENVIKING_ENDPOINT__ === "string" && win.__OPENVIKING_ENDPOINT__.trim()) {
27
93
  return win.__OPENVIKING_ENDPOINT__.trim().replace(/\/+$/, "");
28
94
  }
95
+ return PROXY_OPENVIKING_ENDPOINT;
29
96
  }
30
97
  if (typeof localStorage !== "undefined") {
31
98
  try {
@@ -322,7 +389,9 @@ var OpenVikingClient = class {
322
389
  const data = await res.json().catch(() => ({}));
323
390
  return {
324
391
  ok: data.ok === true,
325
- 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
326
395
  };
327
396
  } catch (err) {
328
397
  return {
@@ -354,7 +423,14 @@ var OpenVikingClient = class {
354
423
  return { ok: false, error: String(errorMsg) };
355
424
  }
356
425
  this.resolvedSessionIds.set(sessionId.trim(), candidateId);
357
- 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
+ };
358
434
  } catch (err) {
359
435
  return {
360
436
  ok: false,
@@ -364,7 +440,193 @@ var OpenVikingClient = class {
364
440
  }
365
441
  return { ok: false, error: lastError };
366
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
+ }
367
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
+ }
368
630
  var defaultOpenVikingClient = new OpenVikingClient();
369
631
  function checkHealth(endpoint, apiKey) {
370
632
  const client = endpoint || apiKey ? new OpenVikingClient(endpoint, apiKey) : defaultOpenVikingClient;
@@ -738,6 +1000,39 @@ function dshIcon(name2) {
738
1000
  return null;
739
1001
  }
740
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
+ }
741
1036
  function getProgressBarPercent(pendingTokens, threshold = COMMIT_THRESHOLD) {
742
1037
  if (!threshold || threshold <= 0) return 0;
743
1038
  const ratio = (pendingTokens || 0) / threshold;
@@ -784,10 +1079,10 @@ function formatMemoryLeafName(uri) {
784
1079
  return segments[0] || clean;
785
1080
  }
786
1081
  function formatEndpoint(endpoint) {
787
- if (!endpoint || !endpoint.trim()) {
1082
+ if (!endpoint || !endpoint.trim() || endpoint.startsWith("/")) {
788
1083
  return "127.0.0.1:1933";
789
1084
  }
790
- return endpoint.trim().replace(/^https?:\/\//, "");
1085
+ return endpoint.trim().replace(/^https?:\/\//, "").replace(/\/+$/, "");
791
1086
  }
792
1087
  function truncateSessionId(id, maxLen = 16) {
793
1088
  if (!id) return "";
@@ -804,6 +1099,229 @@ function handleEscapeKey(event, onClose) {
804
1099
  }
805
1100
  return false;
806
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
+ }
807
1325
  function OpenVikingStatusPopover({
808
1326
  sessionId,
809
1327
  health,
@@ -813,6 +1331,8 @@ function OpenVikingStatusPopover({
813
1331
  endpoint,
814
1332
  isCommitting = false,
815
1333
  commitError = null,
1334
+ breakdown = null,
1335
+ client,
816
1336
  onCommitNow,
817
1337
  onClose,
818
1338
  className,
@@ -893,7 +1413,7 @@ function OpenVikingStatusPopover({
893
1413
  ...style
894
1414
  },
895
1415
  children: [
896
- /* @__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%; } }` }),
897
1417
  /* @__PURE__ */ jsxs(
898
1418
  "div",
899
1419
  {
@@ -914,7 +1434,7 @@ function OpenVikingStatusPopover({
914
1434
  {
915
1435
  "data-testid": "endpoint-label",
916
1436
  style: { ...mutedStyle, ...monoStyle, fontWeight: 400 },
917
- children: formatEndpoint(endpoint)
1437
+ children: formatEndpoint(health?.endpoint || endpoint)
918
1438
  }
919
1439
  )
920
1440
  ] }),
@@ -1090,7 +1610,7 @@ function OpenVikingStatusPopover({
1090
1610
  marginBottom: "12px",
1091
1611
  color: themeVar("labelTertiary")
1092
1612
  },
1093
- children: unauthorized ? "The daemon requires an API key. Set openviking_api_key in localStorage to read session counters." : "Session counters are unavailable right now."
1613
+ children: unauthorized ? "The daemon requires an API key. Configure it in DSH Settings \u2192 OpenViking." : "Session counters are unavailable right now."
1094
1614
  }
1095
1615
  ),
1096
1616
  /* @__PURE__ */ jsxs("div", { style: { marginBottom: "12px" }, children: [
@@ -1176,6 +1696,7 @@ function OpenVikingStatusPopover({
1176
1696
  }
1177
1697
  )
1178
1698
  ] }),
1699
+ /* @__PURE__ */ jsx(ExtractionSection, { breakdown, client }),
1179
1700
  commitError && /* @__PURE__ */ jsx(
1180
1701
  "div",
1181
1702
  {
@@ -1233,6 +1754,77 @@ function OpenVikingStatusPopover({
1233
1754
  // src/client/OpenVikingStatusChip.tsx
1234
1755
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1235
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
+ }
1236
1828
  function formatPendingTokens(pendingTokens) {
1237
1829
  const k = Math.round((pendingTokens || 0) / 1e3);
1238
1830
  return `${k}k pend`;
@@ -1335,6 +1927,7 @@ function StatusChipView({
1335
1927
  initialHealth,
1336
1928
  initialSessionData,
1337
1929
  initialSessionRead,
1930
+ initialTasksRead,
1338
1931
  initialOpen = false
1339
1932
  }) {
1340
1933
  const [health, setHealth] = useState2(
@@ -1343,6 +1936,9 @@ function StatusChipView({
1343
1936
  const [sessionRead, setSessionRead] = useState2(
1344
1937
  initialSessionRead ?? (initialSessionData ? { status: "ok", session: initialSessionData } : null)
1345
1938
  );
1939
+ const [tasksRead, setTasksRead] = useState2(
1940
+ initialTasksRead ?? null
1941
+ );
1346
1942
  const [isOpen, setIsOpen] = useState2(initialOpen);
1347
1943
  const [isCommitting, setIsCommitting] = useState2(false);
1348
1944
  const [commitError, setCommitError] = useState2(null);
@@ -1378,16 +1974,24 @@ function StatusChipView({
1378
1974
  if (!healthRes.ok) {
1379
1975
  return;
1380
1976
  }
1381
- 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);
1382
1983
  } catch {
1383
1984
  setHealth({ ok: false });
1384
1985
  }
1385
1986
  }, [sessionId, apiClient]);
1987
+ const breakdown = tasksRead?.status === "ok" ? tasksRead.breakdown : null;
1988
+ const hasRunning = (breakdown?.running ?? 0) >= 1;
1386
1989
  useEffect2(() => {
1387
1990
  fetchStatus();
1388
- const timer = setInterval(fetchStatus, 15e3);
1991
+ const interval = hasRunning ? POLL_INTERVAL_ACTIVE_MS : POLL_INTERVAL_IDLE_MS;
1992
+ const timer = setInterval(fetchStatus, interval);
1389
1993
  return () => clearInterval(timer);
1390
- }, [fetchStatus]);
1994
+ }, [fetchStatus, hasRunning]);
1391
1995
  useEffect2(() => {
1392
1996
  function handleClickOutside(event) {
1393
1997
  if (popoverRef.current && !popoverRef.current.contains(event.target)) {
@@ -1417,6 +2021,11 @@ function StatusChipView({
1417
2021
  keep_recent_count: 10
1418
2022
  });
1419
2023
  if (res.ok) {
2024
+ if (res.task_id) {
2025
+ setTasksRead(
2026
+ (prev) => seedRunningTask(prev, res.task_id, res.resource_id)
2027
+ );
2028
+ }
1420
2029
  await fetchStatus();
1421
2030
  onCommit?.();
1422
2031
  } else {
@@ -1435,11 +2044,15 @@ function StatusChipView({
1435
2044
  const sessionUnreadable = isOnline && sessionRead !== null && sessionRead.status !== "ok";
1436
2045
  const pendingTokens = sessionData?.pending_tokens ?? 0;
1437
2046
  const recalledCount = recalledResult.recalledCount;
1438
- const statusColor = getStatusIndicatorColor(
2047
+ const rawDotState = getChipDotState({
1439
2048
  isOnline,
1440
- isCommitting,
1441
- sessionUnreadable
1442
- );
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);
1443
2056
  const tooltipTitle = formatTooltipTitle({
1444
2057
  isOnline,
1445
2058
  isCommitting,
@@ -1456,6 +2069,7 @@ function StatusChipView({
1456
2069
  style: { minWidth: 0, display: "inline-flex", position: "relative" },
1457
2070
  ref: popoverRef,
1458
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); } }` }),
1459
2073
  /* @__PURE__ */ jsxs2(
1460
2074
  "button",
1461
2075
  {
@@ -1486,17 +2100,32 @@ function StatusChipView({
1486
2100
  title: tooltipTitle,
1487
2101
  "aria-label": tooltipTitle,
1488
2102
  children: [
1489
- /* @__PURE__ */ jsx2(
2103
+ dotFailed ? /* @__PURE__ */ jsx2(
1490
2104
  "span",
1491
2105
  {
1492
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,
1493
2121
  "aria-hidden": "true",
1494
2122
  style: {
1495
2123
  width: "6px",
1496
2124
  height: "6px",
1497
2125
  borderRadius: "50%",
1498
2126
  background: statusColor,
1499
- flexShrink: 0
2127
+ flexShrink: 0,
2128
+ ...dotBusy ? { animation: "ov-pulse 1.2s ease-in-out infinite" } : {}
1500
2129
  }
1501
2130
  }
1502
2131
  ),
@@ -1515,6 +2144,8 @@ function StatusChipView({
1515
2144
  endpoint: apiClient.endpoint,
1516
2145
  isCommitting,
1517
2146
  commitError,
2147
+ breakdown,
2148
+ client: apiClient,
1518
2149
  onCommitNow: handleCommitNow,
1519
2150
  onClose: () => setIsOpen(false)
1520
2151
  }
@@ -2128,25 +2759,37 @@ function apply(ctx) {
2128
2759
  export {
2129
2760
  COMMIT_THRESHOLD,
2130
2761
  DEFAULT_OPENVIKING_ENDPOINT,
2762
+ ExtractionSection,
2131
2763
  OpenVikingClient,
2132
2764
  OpenVikingSettingsSection,
2133
2765
  OpenVikingStatusChip,
2134
2766
  OpenVikingStatusPopover,
2767
+ POLL_INTERVAL_ACTIVE_MS,
2768
+ POLL_INTERVAL_IDLE_MS,
2769
+ PROXY_OPENVIKING_ENDPOINT,
2770
+ TASKS_LIMIT_CAP,
2135
2771
  THEME,
2772
+ WarningGlyph,
2136
2773
  apply,
2137
2774
  chatNodesToText,
2138
2775
  checkHealth,
2139
2776
  commitSession,
2777
+ computeBreakdown,
2140
2778
  defaultOpenVikingClient,
2141
2779
  fetchSession,
2780
+ formatBacklog,
2142
2781
  formatDaemonVersion,
2782
+ formatDuration,
2143
2783
  formatEndpoint,
2784
+ formatExtractionSummary,
2144
2785
  formatMemoryLeafName,
2145
2786
  formatPendingTokens,
2146
2787
  formatRelativeTime,
2147
2788
  formatStatusLabel,
2148
2789
  formatTooltipTitle,
2149
2790
  getCategoryBadgeStyle,
2791
+ getChipDotState,
2792
+ getDotColorForState,
2150
2793
  getProgressBarColor,
2151
2794
  getProgressBarPercent,
2152
2795
  getSession,
@@ -2155,9 +2798,13 @@ export {
2155
2798
  inferCategory,
2156
2799
  inject,
2157
2800
  name,
2801
+ normalizeExecutionEvent,
2802
+ normalizeExtractionTask,
2803
+ normalizeTaskList,
2158
2804
  parseRecalledMemories,
2159
2805
  resolveApiKey,
2160
2806
  resolveEndpoint,
2807
+ seedRunningTask,
2161
2808
  themeVar,
2162
2809
  truncateSessionId
2163
2810
  };