@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.cjs CHANGED
@@ -37,25 +37,37 @@ var client_exports = {};
37
37
  __export(client_exports, {
38
38
  COMMIT_THRESHOLD: () => COMMIT_THRESHOLD,
39
39
  DEFAULT_OPENVIKING_ENDPOINT: () => DEFAULT_OPENVIKING_ENDPOINT,
40
+ ExtractionSection: () => ExtractionSection,
40
41
  OpenVikingClient: () => OpenVikingClient,
41
42
  OpenVikingSettingsSection: () => OpenVikingSettingsSection,
42
43
  OpenVikingStatusChip: () => OpenVikingStatusChip,
43
44
  OpenVikingStatusPopover: () => OpenVikingStatusPopover,
45
+ POLL_INTERVAL_ACTIVE_MS: () => POLL_INTERVAL_ACTIVE_MS,
46
+ POLL_INTERVAL_IDLE_MS: () => POLL_INTERVAL_IDLE_MS,
47
+ PROXY_OPENVIKING_ENDPOINT: () => PROXY_OPENVIKING_ENDPOINT,
48
+ TASKS_LIMIT_CAP: () => TASKS_LIMIT_CAP,
44
49
  THEME: () => THEME,
50
+ WarningGlyph: () => WarningGlyph,
45
51
  apply: () => apply,
46
52
  chatNodesToText: () => chatNodesToText,
47
53
  checkHealth: () => checkHealth,
48
54
  commitSession: () => commitSession,
55
+ computeBreakdown: () => computeBreakdown,
49
56
  defaultOpenVikingClient: () => defaultOpenVikingClient,
50
57
  fetchSession: () => fetchSession,
58
+ formatBacklog: () => formatBacklog,
51
59
  formatDaemonVersion: () => formatDaemonVersion,
60
+ formatDuration: () => formatDuration,
52
61
  formatEndpoint: () => formatEndpoint,
62
+ formatExtractionSummary: () => formatExtractionSummary,
53
63
  formatMemoryLeafName: () => formatMemoryLeafName,
54
64
  formatPendingTokens: () => formatPendingTokens,
55
65
  formatRelativeTime: () => formatRelativeTime,
56
66
  formatStatusLabel: () => formatStatusLabel,
57
67
  formatTooltipTitle: () => formatTooltipTitle,
58
68
  getCategoryBadgeStyle: () => getCategoryBadgeStyle,
69
+ getChipDotState: () => getChipDotState,
70
+ getDotColorForState: () => getDotColorForState,
59
71
  getProgressBarColor: () => getProgressBarColor,
60
72
  getProgressBarPercent: () => getProgressBarPercent,
61
73
  getSession: () => getSession,
@@ -64,9 +76,13 @@ __export(client_exports, {
64
76
  inferCategory: () => inferCategory,
65
77
  inject: () => inject,
66
78
  name: () => name,
79
+ normalizeExecutionEvent: () => normalizeExecutionEvent,
80
+ normalizeExtractionTask: () => normalizeExtractionTask,
81
+ normalizeTaskList: () => normalizeTaskList,
67
82
  parseRecalledMemories: () => parseRecalledMemories,
68
83
  resolveApiKey: () => resolveApiKey,
69
84
  resolveEndpoint: () => resolveEndpoint,
85
+ seedRunningTask: () => seedRunningTask,
70
86
  themeVar: () => themeVar,
71
87
  truncateSessionId: () => truncateSessionId
72
88
  });
@@ -77,7 +93,73 @@ var import_react2 = __toESM(require("react"), 1);
77
93
  var import_react_dom = __toESM(require("react-dom"), 1);
78
94
 
79
95
  // src/client/api.ts
96
+ var TASKS_LIMIT_CAP = 200;
97
+ function normalizeExtractionTask(raw) {
98
+ if (!raw || typeof raw !== "object") return null;
99
+ const taskId = typeof raw.task_id === "string" && raw.task_id || typeof raw.id === "string" && raw.id || "";
100
+ if (!taskId) return null;
101
+ const statusRaw = typeof raw.status === "string" ? raw.status : "";
102
+ const status = statusRaw === "running" || statusRaw === "pending" || statusRaw === "completed" || statusRaw === "failed" ? statusRaw : "pending";
103
+ const result = raw.result ?? {};
104
+ const extracted = result.memories_extracted ?? {};
105
+ const numberOf = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
106
+ const tokenUsageRaw = result.token_usage ?? raw.token_usage;
107
+ const tokenUsage = typeof tokenUsageRaw === "number" ? tokenUsageRaw : numberOf(
108
+ tokenUsageRaw?.total_tokens
109
+ );
110
+ const errorRaw = raw.error;
111
+ const errorMsg = typeof errorRaw === "string" ? errorRaw : typeof errorRaw?.message === "string" ? errorRaw.message : void 0;
112
+ return {
113
+ task_id: taskId,
114
+ status,
115
+ resource_id: typeof raw.resource_id === "string" ? raw.resource_id : void 0,
116
+ created_at: typeof raw.created_at === "string" ? raw.created_at : void 0,
117
+ updated_at: typeof raw.updated_at === "string" ? raw.updated_at : void 0,
118
+ memory_write: numberOf(extracted.memory_write ?? extracted.written),
119
+ memory_edit: numberOf(extracted.memory_edit ?? extracted.edited),
120
+ token_usage: tokenUsage,
121
+ error: errorMsg
122
+ };
123
+ }
124
+ function computeBreakdown(tasks) {
125
+ const b = {
126
+ running: 0,
127
+ pending: 0,
128
+ completed: 0,
129
+ failed: 0,
130
+ total: tasks.length,
131
+ firstRunning: null,
132
+ lastCompleted: null,
133
+ lastFailed: null
134
+ };
135
+ for (const t of tasks) {
136
+ if (t.status === "running") {
137
+ b.running += 1;
138
+ if (!b.firstRunning) b.firstRunning = t;
139
+ } else if (t.status === "pending") b.pending += 1;
140
+ else if (t.status === "completed") {
141
+ b.completed += 1;
142
+ if (!b.lastCompleted) b.lastCompleted = t;
143
+ } else if (t.status === "failed") {
144
+ b.failed += 1;
145
+ if (!b.lastFailed) b.lastFailed = t;
146
+ }
147
+ }
148
+ return b;
149
+ }
150
+ function normalizeExecutionEvent(raw) {
151
+ return {
152
+ seq: typeof raw.seq === "number" ? raw.seq : void 0,
153
+ recorded_at: typeof raw.recorded_at === "string" ? raw.recorded_at : void 0,
154
+ kind: typeof raw.kind === "string" ? raw.kind : void 0,
155
+ status: typeof raw.status === "string" ? raw.status : void 0,
156
+ stage: typeof raw.stage === "string" ? raw.stage : null,
157
+ operation: typeof raw.operation === "string" ? raw.operation : null,
158
+ error: typeof raw.error === "string" ? raw.error : null
159
+ };
160
+ }
80
161
  var DEFAULT_OPENVIKING_ENDPOINT = "http://127.0.0.1:1933";
162
+ var PROXY_OPENVIKING_ENDPOINT = "/openviking-status/api";
81
163
  function resolveEndpoint(endpoint) {
82
164
  if (endpoint && endpoint.trim().length > 0) {
83
165
  return endpoint.trim().replace(/\/+$/, "");
@@ -87,6 +169,7 @@ function resolveEndpoint(endpoint) {
87
169
  if (typeof win.__OPENVIKING_ENDPOINT__ === "string" && win.__OPENVIKING_ENDPOINT__.trim()) {
88
170
  return win.__OPENVIKING_ENDPOINT__.trim().replace(/\/+$/, "");
89
171
  }
172
+ return PROXY_OPENVIKING_ENDPOINT;
90
173
  }
91
174
  if (typeof localStorage !== "undefined") {
92
175
  try {
@@ -383,7 +466,9 @@ var OpenVikingClient = class {
383
466
  const data = await res.json().catch(() => ({}));
384
467
  return {
385
468
  ok: data.ok === true,
386
- error: typeof data.error === "string" ? data.error : void 0
469
+ error: typeof data.error === "string" ? data.error : void 0,
470
+ task_id: typeof data.task_id === "string" ? data.task_id : void 0,
471
+ resource_id: typeof data.resource_id === "string" ? data.resource_id : void 0
387
472
  };
388
473
  } catch (err) {
389
474
  return {
@@ -415,7 +500,14 @@ var OpenVikingClient = class {
415
500
  return { ok: false, error: String(errorMsg) };
416
501
  }
417
502
  this.resolvedSessionIds.set(sessionId.trim(), candidateId);
418
- return { ok: true };
503
+ const okBody = await res.json().catch(() => ({}));
504
+ const container = okBody.result ?? okBody.data ?? okBody;
505
+ const taskId = container.task_id ?? container.id;
506
+ return {
507
+ ok: true,
508
+ task_id: typeof taskId === "string" && taskId.trim() ? taskId.trim() : void 0,
509
+ resource_id: candidateId
510
+ };
419
511
  } catch (err) {
420
512
  return {
421
513
  ok: false,
@@ -425,7 +517,193 @@ var OpenVikingClient = class {
425
517
  }
426
518
  return { ok: false, error: lastError };
427
519
  }
520
+ /**
521
+ * Список задач Phase 2 (Memory Extraction) текущей сессии со сводной
522
+ * разбивкой по статусам.
523
+ *
524
+ * Через прокси: один запрос `GET /tasks?session=<id>`, разбивка считается
525
+ * здесь. Напрямую: разрешаем `resource_id` через кандидатов и запрашиваем
526
+ * `GET /api/v1/tasks?resource_id=<id>&task_type=session_commit&limit=200`.
527
+ */
528
+ async listTasks(sessionId) {
529
+ if (!sessionId || !sessionId.trim()) {
530
+ return { status: "missing" };
531
+ }
532
+ if (this.isProxy()) {
533
+ try {
534
+ const res = await fetch(
535
+ `${this.endpoint}/tasks?session=${encodeURIComponent(
536
+ sessionId.trim()
537
+ )}&limit=${TASKS_LIMIT_CAP}`,
538
+ { method: "GET", headers: this.getHeaders() }
539
+ );
540
+ if (res.status === 401 || res.status === 403) {
541
+ return { status: "unauthorized" };
542
+ }
543
+ if (!res.ok) {
544
+ return { status: "error", detail: `HTTP ${res.status}` };
545
+ }
546
+ const data = await res.json();
547
+ if (data.status === "unauthorized") return { status: "unauthorized" };
548
+ if (data.status === "missing") return { status: "missing" };
549
+ if (data.status === "unreachable")
550
+ return {
551
+ status: "unreachable",
552
+ detail: typeof data.detail === "string" ? data.detail : void 0
553
+ };
554
+ if (data.status === "error")
555
+ return {
556
+ status: "error",
557
+ detail: typeof data.detail === "string" ? data.detail : void 0
558
+ };
559
+ const tasks = normalizeTaskList(data.tasks);
560
+ return { status: "ok", breakdown: computeBreakdown(tasks), tasks };
561
+ } catch (err) {
562
+ return {
563
+ status: "unreachable",
564
+ detail: err instanceof Error ? err.message : String(err)
565
+ };
566
+ }
567
+ }
568
+ const candidates = this.getCandidateSessionIds(sessionId);
569
+ let resourceId = null;
570
+ for (const candidateId of candidates) {
571
+ try {
572
+ const probe = await fetch(
573
+ `${this.endpoint}/api/v1/sessions/${encodeURIComponent(candidateId)}`,
574
+ { method: "GET", headers: this.getHeaders() }
575
+ );
576
+ if (probe.status === 404) continue;
577
+ if (probe.status === 401 || probe.status === 403) {
578
+ return { status: "unauthorized" };
579
+ }
580
+ if (probe.ok) {
581
+ resourceId = candidateId;
582
+ this.resolvedSessionIds.set(sessionId.trim(), candidateId);
583
+ break;
584
+ }
585
+ } catch (err) {
586
+ return {
587
+ status: "unreachable",
588
+ detail: err instanceof Error ? err.message : String(err)
589
+ };
590
+ }
591
+ }
592
+ if (!resourceId) return { status: "missing" };
593
+ try {
594
+ const query = new URLSearchParams({
595
+ resource_id: resourceId,
596
+ task_type: "session_commit",
597
+ limit: String(TASKS_LIMIT_CAP)
598
+ });
599
+ const res = await fetch(
600
+ `${this.endpoint}/api/v1/tasks?${query.toString()}`,
601
+ { method: "GET", headers: this.getHeaders() }
602
+ );
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 items = data?.items ?? data?.tasks ?? data?.result?.items ?? (Array.isArray(data) ? data : []);
611
+ const tasks = normalizeTaskList(items);
612
+ return { status: "ok", breakdown: computeBreakdown(tasks), tasks };
613
+ } catch (err) {
614
+ return {
615
+ status: "unreachable",
616
+ detail: err instanceof Error ? err.message : String(err)
617
+ };
618
+ }
619
+ }
620
+ /**
621
+ * Лениво загрузить одну задачу с лентой `execution_events` для «Show log».
622
+ */
623
+ async fetchTaskEvents(taskId) {
624
+ if (!taskId || !taskId.trim()) {
625
+ return { status: "missing" };
626
+ }
627
+ const buildOk = (taskRaw) => {
628
+ const task = normalizeExtractionTask(taskRaw);
629
+ const eventsContainer = taskRaw.execution_events ?? {};
630
+ const rawEvents = eventsContainer.items ?? (Array.isArray(taskRaw.execution_events) ? taskRaw.execution_events : []);
631
+ const events = (Array.isArray(rawEvents) ? rawEvents : []).filter(
632
+ (e) => !!e && typeof e === "object"
633
+ ).map(normalizeExecutionEvent);
634
+ if (!task) return { status: "error", detail: "malformed task" };
635
+ return { status: "ok", task, events };
636
+ };
637
+ if (this.isProxy()) {
638
+ try {
639
+ const res = await fetch(
640
+ `${this.endpoint}/task?id=${encodeURIComponent(
641
+ taskId.trim()
642
+ )}&events=1`,
643
+ { method: "GET", headers: this.getHeaders() }
644
+ );
645
+ if (res.status === 401 || res.status === 403) {
646
+ return { status: "unauthorized" };
647
+ }
648
+ if (!res.ok) {
649
+ return { status: "error", detail: `HTTP ${res.status}` };
650
+ }
651
+ const data = await res.json();
652
+ if (data.status === "unauthorized") return { status: "unauthorized" };
653
+ if (data.status === "missing") return { status: "missing" };
654
+ if (data.status === "unreachable")
655
+ return {
656
+ status: "unreachable",
657
+ detail: typeof data.detail === "string" ? data.detail : void 0
658
+ };
659
+ if (data.status === "error")
660
+ return {
661
+ status: "error",
662
+ detail: typeof data.detail === "string" ? data.detail : void 0
663
+ };
664
+ return buildOk(data.task ?? {});
665
+ } catch (err) {
666
+ return {
667
+ status: "unreachable",
668
+ detail: err instanceof Error ? err.message : String(err)
669
+ };
670
+ }
671
+ }
672
+ try {
673
+ const res = await fetch(
674
+ `${this.endpoint}/api/v1/tasks/${encodeURIComponent(
675
+ taskId.trim()
676
+ )}?include_events=true`,
677
+ { method: "GET", headers: this.getHeaders() }
678
+ );
679
+ if (res.status === 404) return { status: "missing" };
680
+ if (res.status === 401 || res.status === 403) {
681
+ return { status: "unauthorized" };
682
+ }
683
+ if (!res.ok) {
684
+ return { status: "error", detail: `HTTP ${res.status}` };
685
+ }
686
+ const data = await res.json();
687
+ const taskRaw = data?.result ?? data?.data ?? data;
688
+ return buildOk(taskRaw);
689
+ } catch (err) {
690
+ return {
691
+ status: "unreachable",
692
+ detail: err instanceof Error ? err.message : String(err)
693
+ };
694
+ }
695
+ }
428
696
  };
697
+ function normalizeTaskList(raw) {
698
+ if (!Array.isArray(raw)) return [];
699
+ const tasks = [];
700
+ for (const entry of raw) {
701
+ if (!entry || typeof entry !== "object") continue;
702
+ const task = normalizeExtractionTask(entry);
703
+ if (task) tasks.push(task);
704
+ }
705
+ return tasks;
706
+ }
429
707
  var defaultOpenVikingClient = new OpenVikingClient();
430
708
  function checkHealth(endpoint, apiKey) {
431
709
  const client = endpoint || apiKey ? new OpenVikingClient(endpoint, apiKey) : defaultOpenVikingClient;
@@ -799,6 +1077,39 @@ function dshIcon(name2) {
799
1077
  return null;
800
1078
  }
801
1079
  }
1080
+ function formatDuration(startIso, endIso) {
1081
+ if (!startIso || !endIso) return void 0;
1082
+ const start = new Date(startIso).getTime();
1083
+ const end = new Date(endIso).getTime();
1084
+ if (isNaN(start) || isNaN(end) || end < start) return void 0;
1085
+ const ms = end - start;
1086
+ const sec = ms / 1e3;
1087
+ if (sec < 60) {
1088
+ return `${sec < 10 ? sec.toFixed(1) : Math.round(sec)}s`;
1089
+ }
1090
+ const min = Math.floor(sec / 60);
1091
+ const rem = Math.round(sec % 60);
1092
+ return `${min}m ${rem}s`;
1093
+ }
1094
+ function formatExtractionSummary(task, now = Date.now()) {
1095
+ const write = task.memory_write ?? 0;
1096
+ const edit = task.memory_edit ?? 0;
1097
+ const parts = [`${write} written, ${edit} edited`];
1098
+ const duration = formatDuration(task.created_at, task.updated_at);
1099
+ if (duration) parts.push(duration);
1100
+ const rel = formatRelativeTime(task.updated_at, now);
1101
+ if (rel) parts.push(rel);
1102
+ return parts.join(" \xB7 ");
1103
+ }
1104
+ function formatBacklog(breakdown) {
1105
+ if (!breakdown) return null;
1106
+ const { pending, failed } = breakdown;
1107
+ if (pending <= 0 && failed <= 0) return null;
1108
+ const parts = [];
1109
+ if (pending > 0) parts.push(`${pending} pending`);
1110
+ if (failed > 0) parts.push(`${failed} failed`);
1111
+ return parts.join(" / ");
1112
+ }
802
1113
  function getProgressBarPercent(pendingTokens, threshold = COMMIT_THRESHOLD) {
803
1114
  if (!threshold || threshold <= 0) return 0;
804
1115
  const ratio = (pendingTokens || 0) / threshold;
@@ -845,10 +1156,10 @@ function formatMemoryLeafName(uri) {
845
1156
  return segments[0] || clean;
846
1157
  }
847
1158
  function formatEndpoint(endpoint) {
848
- if (!endpoint || !endpoint.trim()) {
1159
+ if (!endpoint || !endpoint.trim() || endpoint.startsWith("/")) {
849
1160
  return "127.0.0.1:1933";
850
1161
  }
851
- return endpoint.trim().replace(/^https?:\/\//, "");
1162
+ return endpoint.trim().replace(/^https?:\/\//, "").replace(/\/+$/, "");
852
1163
  }
853
1164
  function truncateSessionId(id, maxLen = 16) {
854
1165
  if (!id) return "";
@@ -865,6 +1176,229 @@ function handleEscapeKey(event, onClose) {
865
1176
  }
866
1177
  return false;
867
1178
  }
1179
+ function ExtractionSection({
1180
+ breakdown,
1181
+ client
1182
+ }) {
1183
+ const [showLog, setShowLog] = (0, import_react.useState)(false);
1184
+ const [events, setEvents] = (0, import_react.useState)(null);
1185
+ const [logError, setLogError] = (0, import_react.useState)(null);
1186
+ const [loadingLog, setLoadingLog] = (0, import_react.useState)(false);
1187
+ if (!breakdown) return null;
1188
+ const isRunning = breakdown.running >= 1;
1189
+ const lastCompleted = breakdown.lastCompleted;
1190
+ const lastFailed = breakdown.lastFailed;
1191
+ const backlog = formatBacklog(breakdown);
1192
+ const failedActive = !isRunning && breakdown.failed >= 1;
1193
+ const logTaskId = (isRunning ? breakdown.firstRunning?.task_id : null) ?? (failedActive ? lastFailed?.task_id : null) ?? lastCompleted?.task_id ?? lastFailed?.task_id ?? null;
1194
+ const mutedStyle = { color: themeVar("labelTertiary") };
1195
+ const toggleLog = (0, import_react.useCallback)(async () => {
1196
+ const next = !showLog;
1197
+ setShowLog(next);
1198
+ if (next && events === null && logTaskId && client) {
1199
+ setLoadingLog(true);
1200
+ setLogError(null);
1201
+ const res = await client.fetchTaskEvents(logTaskId);
1202
+ setLoadingLog(false);
1203
+ if (res.status === "ok") {
1204
+ setEvents(res.events);
1205
+ } else {
1206
+ setLogError(
1207
+ res.status === "unauthorized" ? "No access to task log" : "Could not load log"
1208
+ );
1209
+ }
1210
+ }
1211
+ }, [showLog, events, logTaskId, client]);
1212
+ if (!isRunning && !failedActive && !lastCompleted && !backlog) {
1213
+ return null;
1214
+ }
1215
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { "data-testid": "extraction-section", style: { marginBottom: "12px" }, children: [
1216
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1217
+ "div",
1218
+ {
1219
+ style: {
1220
+ marginBottom: "4px",
1221
+ color: themeVar("labelPrimary"),
1222
+ fontWeight: 500
1223
+ },
1224
+ children: "Memory Extraction"
1225
+ }
1226
+ ),
1227
+ isRunning ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
1228
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1229
+ "div",
1230
+ {
1231
+ style: {
1232
+ display: "flex",
1233
+ alignItems: "center",
1234
+ gap: "6px",
1235
+ marginBottom: "4px"
1236
+ },
1237
+ children: [
1238
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1239
+ "span",
1240
+ {
1241
+ "data-testid": "extraction-status-dot",
1242
+ "aria-hidden": "true",
1243
+ style: {
1244
+ width: "6px",
1245
+ height: "6px",
1246
+ borderRadius: "50%",
1247
+ background: themeVar("stateSuccess"),
1248
+ animation: "ov-pulse 1.2s ease-in-out infinite",
1249
+ flexShrink: 0
1250
+ }
1251
+ }
1252
+ ),
1253
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "data-testid": "extraction-running-label", children: "Extracting\u2026" })
1254
+ ]
1255
+ }
1256
+ ),
1257
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1258
+ "div",
1259
+ {
1260
+ "data-testid": "extraction-indeterminate-track",
1261
+ style: {
1262
+ width: "100%",
1263
+ height: "6px",
1264
+ borderRadius: "3px",
1265
+ backgroundColor: themeVar("insetSurface"),
1266
+ overflow: "hidden",
1267
+ position: "relative"
1268
+ },
1269
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1270
+ "div",
1271
+ {
1272
+ "data-testid": "extraction-indeterminate-fill",
1273
+ style: {
1274
+ position: "absolute",
1275
+ left: 0,
1276
+ top: 0,
1277
+ height: "100%",
1278
+ width: "40%",
1279
+ borderRadius: "3px",
1280
+ backgroundColor: themeVar("stateSuccess"),
1281
+ animation: "ov-indeterminate 1.2s ease-in-out infinite"
1282
+ }
1283
+ }
1284
+ )
1285
+ }
1286
+ )
1287
+ ] }) : failedActive && lastFailed ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1288
+ "div",
1289
+ {
1290
+ "data-testid": "extraction-failed-line",
1291
+ style: {
1292
+ display: "flex",
1293
+ alignItems: "center",
1294
+ gap: "6px",
1295
+ color: themeVar("stateWarning")
1296
+ },
1297
+ children: [
1298
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1299
+ "span",
1300
+ {
1301
+ "aria-hidden": "true",
1302
+ style: { display: "inline-flex", flexShrink: 0 },
1303
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(WarningGlyph, { size: 12, testId: "extraction-warning-glyph" })
1304
+ }
1305
+ ),
1306
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "data-testid": "extraction-failed-text", children: lastFailed.error || "extraction failed" })
1307
+ ]
1308
+ }
1309
+ ) : lastCompleted ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1310
+ "div",
1311
+ {
1312
+ style: {
1313
+ display: "flex",
1314
+ alignItems: "center",
1315
+ gap: "6px"
1316
+ },
1317
+ children: [
1318
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1319
+ "span",
1320
+ {
1321
+ "data-testid": "extraction-status-dot",
1322
+ "aria-hidden": "true",
1323
+ style: {
1324
+ width: "6px",
1325
+ height: "6px",
1326
+ borderRadius: "50%",
1327
+ background: themeVar("stateSuccess"),
1328
+ flexShrink: 0
1329
+ }
1330
+ }
1331
+ ),
1332
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "data-testid": "extraction-last-line", children: formatExtractionSummary(lastCompleted) })
1333
+ ]
1334
+ }
1335
+ ) : null,
1336
+ backlog && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1337
+ "div",
1338
+ {
1339
+ "data-testid": "extraction-backlog-line",
1340
+ style: { ...mutedStyle, marginTop: "4px" },
1341
+ children: backlog
1342
+ }
1343
+ ),
1344
+ logTaskId && client && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { marginTop: "6px" }, children: [
1345
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1346
+ "button",
1347
+ {
1348
+ type: "button",
1349
+ "data-testid": "extraction-show-log-btn",
1350
+ onClick: () => void toggleLog(),
1351
+ style: {
1352
+ background: "none",
1353
+ border: "none",
1354
+ padding: 0,
1355
+ cursor: "pointer",
1356
+ font: "inherit",
1357
+ color: themeVar("labelTertiary"),
1358
+ textDecoration: "underline"
1359
+ },
1360
+ "aria-expanded": showLog,
1361
+ children: showLog ? "Hide log" : "Show log"
1362
+ }
1363
+ ),
1364
+ showLog && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1365
+ "div",
1366
+ {
1367
+ "data-testid": "extraction-log",
1368
+ style: {
1369
+ marginTop: "6px",
1370
+ display: "flex",
1371
+ flexDirection: "column",
1372
+ gap: "2px",
1373
+ maxHeight: "120px",
1374
+ overflowY: "auto"
1375
+ },
1376
+ children: [
1377
+ loadingLog && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-testid": "extraction-log-loading", style: mutedStyle, children: "Loading\u2026" }),
1378
+ logError && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-testid": "extraction-log-error", style: mutedStyle, children: logError }),
1379
+ events && events.length === 0 && !loadingLog && !logError && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { "data-testid": "extraction-log-empty", style: mutedStyle, children: "No events" }),
1380
+ events?.map((ev, idx) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1381
+ "div",
1382
+ {
1383
+ "data-testid": "extraction-log-event",
1384
+ style: {
1385
+ display: "flex",
1386
+ justifyContent: "space-between",
1387
+ gap: "8px"
1388
+ },
1389
+ children: [
1390
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: ev.status || ev.kind || "event" }),
1391
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: mutedStyle, children: formatRelativeTime(ev.recorded_at) || "" })
1392
+ ]
1393
+ },
1394
+ `${ev.seq ?? idx}-${ev.recorded_at ?? idx}`
1395
+ ))
1396
+ ]
1397
+ }
1398
+ )
1399
+ ] })
1400
+ ] });
1401
+ }
868
1402
  function OpenVikingStatusPopover({
869
1403
  sessionId,
870
1404
  health,
@@ -874,6 +1408,8 @@ function OpenVikingStatusPopover({
874
1408
  endpoint,
875
1409
  isCommitting = false,
876
1410
  commitError = null,
1411
+ breakdown = null,
1412
+ client,
877
1413
  onCommitNow,
878
1414
  onClose,
879
1415
  className,
@@ -954,7 +1490,7 @@ function OpenVikingStatusPopover({
954
1490
  ...style
955
1491
  },
956
1492
  children: [
957
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: `@keyframes ov-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }` }),
1493
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: `@keyframes ov-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @keyframes ov-pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.35; transform: scale(0.72); } } @keyframes ov-indeterminate { 0% { left: -40%; } 100% { left: 100%; } }` }),
958
1494
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
959
1495
  "div",
960
1496
  {
@@ -975,7 +1511,7 @@ function OpenVikingStatusPopover({
975
1511
  {
976
1512
  "data-testid": "endpoint-label",
977
1513
  style: { ...mutedStyle, ...monoStyle, fontWeight: 400 },
978
- children: formatEndpoint(endpoint)
1514
+ children: formatEndpoint(health?.endpoint || endpoint)
979
1515
  }
980
1516
  )
981
1517
  ] }),
@@ -1151,7 +1687,7 @@ function OpenVikingStatusPopover({
1151
1687
  marginBottom: "12px",
1152
1688
  color: themeVar("labelTertiary")
1153
1689
  },
1154
- children: unauthorized ? "The daemon requires an API key. Set openviking_api_key in localStorage to read session counters." : "Session counters are unavailable right now."
1690
+ children: unauthorized ? "The daemon requires an API key. Configure it in DSH Settings \u2192 OpenViking." : "Session counters are unavailable right now."
1155
1691
  }
1156
1692
  ),
1157
1693
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { marginBottom: "12px" }, children: [
@@ -1237,6 +1773,7 @@ function OpenVikingStatusPopover({
1237
1773
  }
1238
1774
  )
1239
1775
  ] }),
1776
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ExtractionSection, { breakdown, client }),
1240
1777
  commitError && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1241
1778
  "div",
1242
1779
  {
@@ -1294,6 +1831,77 @@ function OpenVikingStatusPopover({
1294
1831
  // src/client/OpenVikingStatusChip.tsx
1295
1832
  var import_jsx_runtime2 = require("react/jsx-runtime");
1296
1833
  var COMMIT_THRESHOLD = 2e4;
1834
+ var POLL_INTERVAL_IDLE_MS = 15e3;
1835
+ var POLL_INTERVAL_ACTIVE_MS = 2500;
1836
+ function getChipDotState({
1837
+ isOnline,
1838
+ sessionUnreadable,
1839
+ breakdown
1840
+ }) {
1841
+ if (!isOnline) return "offline";
1842
+ if (sessionUnreadable) return "session-unreadable";
1843
+ if (breakdown && breakdown.running >= 1) return "busy";
1844
+ if (breakdown && breakdown.failed >= 1) return "extraction-failed";
1845
+ return "online-idle";
1846
+ }
1847
+ function WarningGlyph({
1848
+ size = 11,
1849
+ testId = "extraction-warning-glyph"
1850
+ }) {
1851
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1852
+ "svg",
1853
+ {
1854
+ "data-testid": testId,
1855
+ width: size,
1856
+ height: size,
1857
+ viewBox: "0 0 16 16",
1858
+ fill: "none",
1859
+ "aria-hidden": "true",
1860
+ style: { flexShrink: 0, display: "block" },
1861
+ children: [
1862
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "8", cy: "8", r: "6.5", stroke: "currentColor", strokeWidth: "1.5" }),
1863
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1864
+ "path",
1865
+ {
1866
+ d: "M8 5v3.5",
1867
+ stroke: "currentColor",
1868
+ strokeWidth: "1.5",
1869
+ strokeLinecap: "round"
1870
+ }
1871
+ ),
1872
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("circle", { cx: "8", cy: "11", r: "0.9", fill: "currentColor" })
1873
+ ]
1874
+ }
1875
+ );
1876
+ }
1877
+ function seedRunningTask(prev, taskId, resourceId) {
1878
+ const existing = prev && prev.status === "ok" ? prev.tasks : [];
1879
+ if (existing.some((t) => t.task_id === taskId)) {
1880
+ return prev;
1881
+ }
1882
+ const seeded = {
1883
+ task_id: taskId,
1884
+ status: "running",
1885
+ resource_id: resourceId,
1886
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
1887
+ };
1888
+ const tasks = [seeded, ...existing];
1889
+ return { status: "ok", breakdown: computeBreakdown(tasks), tasks };
1890
+ }
1891
+ function getDotColorForState(state) {
1892
+ switch (state) {
1893
+ case "offline":
1894
+ return themeVar("stateError");
1895
+ case "session-unreadable":
1896
+ case "extraction-failed":
1897
+ return themeVar("stateWarning");
1898
+ case "busy":
1899
+ return themeVar("stateSuccess");
1900
+ case "online-idle":
1901
+ default:
1902
+ return themeVar("stateSuccess");
1903
+ }
1904
+ }
1297
1905
  function formatPendingTokens(pendingTokens) {
1298
1906
  const k = Math.round((pendingTokens || 0) / 1e3);
1299
1907
  return `${k}k pend`;
@@ -1396,6 +2004,7 @@ function StatusChipView({
1396
2004
  initialHealth,
1397
2005
  initialSessionData,
1398
2006
  initialSessionRead,
2007
+ initialTasksRead,
1399
2008
  initialOpen = false
1400
2009
  }) {
1401
2010
  const [health, setHealth] = (0, import_react2.useState)(
@@ -1404,6 +2013,9 @@ function StatusChipView({
1404
2013
  const [sessionRead, setSessionRead] = (0, import_react2.useState)(
1405
2014
  initialSessionRead ?? (initialSessionData ? { status: "ok", session: initialSessionData } : null)
1406
2015
  );
2016
+ const [tasksRead, setTasksRead] = (0, import_react2.useState)(
2017
+ initialTasksRead ?? null
2018
+ );
1407
2019
  const [isOpen, setIsOpen] = (0, import_react2.useState)(initialOpen);
1408
2020
  const [isCommitting, setIsCommitting] = (0, import_react2.useState)(false);
1409
2021
  const [commitError, setCommitError] = (0, import_react2.useState)(null);
@@ -1439,16 +2051,24 @@ function StatusChipView({
1439
2051
  if (!healthRes.ok) {
1440
2052
  return;
1441
2053
  }
1442
- setSessionRead(await apiClient.readSession(sessionId));
2054
+ const [session, tasks] = await Promise.all([
2055
+ apiClient.readSession(sessionId),
2056
+ apiClient.listTasks(sessionId)
2057
+ ]);
2058
+ setSessionRead(session);
2059
+ setTasksRead(tasks);
1443
2060
  } catch {
1444
2061
  setHealth({ ok: false });
1445
2062
  }
1446
2063
  }, [sessionId, apiClient]);
2064
+ const breakdown = tasksRead?.status === "ok" ? tasksRead.breakdown : null;
2065
+ const hasRunning = (breakdown?.running ?? 0) >= 1;
1447
2066
  (0, import_react2.useEffect)(() => {
1448
2067
  fetchStatus();
1449
- const timer = setInterval(fetchStatus, 15e3);
2068
+ const interval = hasRunning ? POLL_INTERVAL_ACTIVE_MS : POLL_INTERVAL_IDLE_MS;
2069
+ const timer = setInterval(fetchStatus, interval);
1450
2070
  return () => clearInterval(timer);
1451
- }, [fetchStatus]);
2071
+ }, [fetchStatus, hasRunning]);
1452
2072
  (0, import_react2.useEffect)(() => {
1453
2073
  function handleClickOutside(event) {
1454
2074
  if (popoverRef.current && !popoverRef.current.contains(event.target)) {
@@ -1478,6 +2098,11 @@ function StatusChipView({
1478
2098
  keep_recent_count: 10
1479
2099
  });
1480
2100
  if (res.ok) {
2101
+ if (res.task_id) {
2102
+ setTasksRead(
2103
+ (prev) => seedRunningTask(prev, res.task_id, res.resource_id)
2104
+ );
2105
+ }
1481
2106
  await fetchStatus();
1482
2107
  onCommit?.();
1483
2108
  } else {
@@ -1496,11 +2121,15 @@ function StatusChipView({
1496
2121
  const sessionUnreadable = isOnline && sessionRead !== null && sessionRead.status !== "ok";
1497
2122
  const pendingTokens = sessionData?.pending_tokens ?? 0;
1498
2123
  const recalledCount = recalledResult.recalledCount;
1499
- const statusColor = getStatusIndicatorColor(
2124
+ const rawDotState = getChipDotState({
1500
2125
  isOnline,
1501
- isCommitting,
1502
- sessionUnreadable
1503
- );
2126
+ sessionUnreadable,
2127
+ breakdown
2128
+ });
2129
+ const dotState = isCommitting && (rawDotState === "online-idle" || rawDotState === "busy") ? "busy" : rawDotState;
2130
+ const dotBusy = dotState === "busy";
2131
+ const dotFailed = dotState === "extraction-failed";
2132
+ const statusColor = getDotColorForState(dotState);
1504
2133
  const tooltipTitle = formatTooltipTitle({
1505
2134
  isOnline,
1506
2135
  isCommitting,
@@ -1517,6 +2146,7 @@ function StatusChipView({
1517
2146
  style: { minWidth: 0, display: "inline-flex", position: "relative" },
1518
2147
  ref: popoverRef,
1519
2148
  children: [
2149
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("style", { children: `@keyframes ov-pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.35; transform: scale(0.72); } }` }),
1520
2150
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1521
2151
  "button",
1522
2152
  {
@@ -1547,17 +2177,32 @@ function StatusChipView({
1547
2177
  title: tooltipTitle,
1548
2178
  "aria-label": tooltipTitle,
1549
2179
  children: [
1550
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2180
+ dotFailed ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1551
2181
  "span",
1552
2182
  {
1553
2183
  "data-testid": "status-dot",
2184
+ "data-dot-state": dotState,
2185
+ "aria-hidden": "true",
2186
+ style: {
2187
+ display: "inline-flex",
2188
+ color: statusColor,
2189
+ flexShrink: 0
2190
+ },
2191
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(WarningGlyph, { size: 11, testId: "chip-warning-glyph" })
2192
+ }
2193
+ ) : /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
2194
+ "span",
2195
+ {
2196
+ "data-testid": "status-dot",
2197
+ "data-dot-state": dotBusy ? "busy" : dotState,
1554
2198
  "aria-hidden": "true",
1555
2199
  style: {
1556
2200
  width: "6px",
1557
2201
  height: "6px",
1558
2202
  borderRadius: "50%",
1559
2203
  background: statusColor,
1560
- flexShrink: 0
2204
+ flexShrink: 0,
2205
+ ...dotBusy ? { animation: "ov-pulse 1.2s ease-in-out infinite" } : {}
1561
2206
  }
1562
2207
  }
1563
2208
  ),
@@ -1576,6 +2221,8 @@ function StatusChipView({
1576
2221
  endpoint: apiClient.endpoint,
1577
2222
  isCommitting,
1578
2223
  commitError,
2224
+ breakdown,
2225
+ client: apiClient,
1579
2226
  onCommitNow: handleCommitNow,
1580
2227
  onClose: () => setIsOpen(false)
1581
2228
  }