@dipertq/dsh-openviking-status 0.1.5

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 ADDED
@@ -0,0 +1,1431 @@
1
+ // src/client/OpenVikingStatusChip.tsx
2
+ import {
3
+ useState as useState2,
4
+ useEffect as useEffect2,
5
+ useCallback as useCallback2,
6
+ useRef as useRef2,
7
+ useMemo
8
+ } from "react";
9
+
10
+ // src/client/api.ts
11
+ var DEFAULT_OPENVIKING_ENDPOINT = "http://127.0.0.1:1933";
12
+ function resolveEndpoint(endpoint) {
13
+ if (endpoint && endpoint.trim().length > 0) {
14
+ return endpoint.trim().replace(/\/+$/, "");
15
+ }
16
+ if (typeof window !== "undefined") {
17
+ const win = window;
18
+ if (typeof win.__OPENVIKING_ENDPOINT__ === "string" && win.__OPENVIKING_ENDPOINT__.trim()) {
19
+ return win.__OPENVIKING_ENDPOINT__.trim().replace(/\/+$/, "");
20
+ }
21
+ }
22
+ if (typeof localStorage !== "undefined") {
23
+ try {
24
+ const stored = localStorage.getItem("openviking_endpoint") || localStorage.getItem("OPENVIKING_ENDPOINT");
25
+ if (stored && stored.trim()) {
26
+ return stored.trim().replace(/\/+$/, "");
27
+ }
28
+ } catch {
29
+ }
30
+ }
31
+ return DEFAULT_OPENVIKING_ENDPOINT;
32
+ }
33
+ function resolveApiKey(apiKey) {
34
+ if (apiKey && apiKey.trim().length > 0) {
35
+ return apiKey.trim();
36
+ }
37
+ if (typeof window !== "undefined") {
38
+ const win = window;
39
+ if (typeof win.__OPENVIKING_API_KEY__ === "string" && win.__OPENVIKING_API_KEY__.trim()) {
40
+ return win.__OPENVIKING_API_KEY__.trim();
41
+ }
42
+ }
43
+ if (typeof localStorage !== "undefined") {
44
+ try {
45
+ const stored = localStorage.getItem("openviking_api_key") || localStorage.getItem("OPENVIKING_API_KEY");
46
+ if (stored && stored.trim()) {
47
+ return stored.trim();
48
+ }
49
+ } catch {
50
+ }
51
+ }
52
+ return void 0;
53
+ }
54
+ var OpenVikingClient = class {
55
+ endpoint;
56
+ apiKey;
57
+ resolvedSessionIds = /* @__PURE__ */ new Map();
58
+ constructor(endpoint, apiKey) {
59
+ this.endpoint = resolveEndpoint(endpoint);
60
+ this.apiKey = resolveApiKey(apiKey);
61
+ }
62
+ /**
63
+ * Формирование заголовков запроса, включая опциональный заголовок авторизации
64
+ */
65
+ getHeaders(customHeaders) {
66
+ const headers = {
67
+ "Content-Type": "application/json",
68
+ ...customHeaders
69
+ };
70
+ if (this.apiKey) {
71
+ headers["Authorization"] = `Bearer ${this.apiKey}`;
72
+ }
73
+ return headers;
74
+ }
75
+ /**
76
+ * Генерация кандидатов ID сессии для fallback-разрешения:
77
+ * dsh-session-${id} <-> dsh-${id}
78
+ */
79
+ getCandidateSessionIds(sessionId) {
80
+ const raw = sessionId.trim();
81
+ const candidates = [];
82
+ const cached = this.resolvedSessionIds.get(raw);
83
+ if (cached) {
84
+ candidates.push(cached);
85
+ }
86
+ if (raw.startsWith("dsh-session-")) {
87
+ const suffix = raw.slice("dsh-session-".length);
88
+ candidates.push(raw, `dsh-${suffix}`);
89
+ } else if (raw.startsWith("dsh-")) {
90
+ const suffix = raw.slice("dsh-".length);
91
+ candidates.push(raw, `dsh-session-${suffix}`);
92
+ } else {
93
+ candidates.push(`dsh-session-${raw}`, `dsh-${raw}`, raw);
94
+ }
95
+ return Array.from(new Set(candidates));
96
+ }
97
+ /**
98
+ * Проверка доступности и состояния сервиса OpenViking
99
+ */
100
+ async checkHealth() {
101
+ try {
102
+ const res = await fetch(`${this.endpoint}/health`, {
103
+ method: "GET",
104
+ headers: this.getHeaders()
105
+ });
106
+ if (!res.ok) {
107
+ return {
108
+ ok: false,
109
+ error: `HTTP ${res.status}: ${res.statusText}`
110
+ };
111
+ }
112
+ const body = await res.json().catch(() => ({}));
113
+ const isOk = body.ok !== false && body.status !== "error" && (body.ok === true || body.status === "ok" || body.status === "healthy" || res.ok);
114
+ return {
115
+ ok: isOk,
116
+ version: typeof body.version === "string" ? body.version : void 0,
117
+ storage: typeof body.storage === "string" ? body.storage : void 0
118
+ };
119
+ } catch (err) {
120
+ return {
121
+ ok: false,
122
+ error: err instanceof Error ? err.message : String(err)
123
+ };
124
+ }
125
+ }
126
+ /**
127
+ * Получение метаданных сессии по идентификатору с автоматическим разрешением префикса.
128
+ * При сетевых сбоях или ошибках авторизации возвращает null, не выбрасывая исключений.
129
+ */
130
+ async fetchSession(sessionId) {
131
+ if (!sessionId || !sessionId.trim()) {
132
+ return null;
133
+ }
134
+ const candidates = this.getCandidateSessionIds(sessionId);
135
+ for (const candidateId of candidates) {
136
+ try {
137
+ const res = await fetch(
138
+ `${this.endpoint}/api/v1/sessions/${encodeURIComponent(candidateId)}`,
139
+ {
140
+ method: "GET",
141
+ headers: this.getHeaders()
142
+ }
143
+ );
144
+ if (res.status === 404) {
145
+ continue;
146
+ }
147
+ if (!res.ok) {
148
+ return null;
149
+ }
150
+ const data = await res.json();
151
+ const raw = data?.result ?? data?.data ?? data;
152
+ if (!raw || typeof raw !== "object") {
153
+ return null;
154
+ }
155
+ this.resolvedSessionIds.set(sessionId.trim(), candidateId);
156
+ return {
157
+ session_id: typeof raw.session_id === "string" ? raw.session_id : candidateId,
158
+ peer_id: typeof raw.peer_id === "string" ? raw.peer_id : void 0,
159
+ pending_tokens: typeof raw.pending_tokens === "number" ? raw.pending_tokens : 0,
160
+ message_count: typeof raw.message_count === "number" ? raw.message_count : void 0,
161
+ commit_count: typeof raw.commit_count === "number" ? raw.commit_count : void 0,
162
+ last_commit_at: typeof raw.last_commit_at === "string" ? raw.last_commit_at : typeof raw.last_commit === "string" ? raw.last_commit : void 0,
163
+ created_at: typeof raw.created_at === "string" ? raw.created_at : void 0,
164
+ updated_at: typeof raw.updated_at === "string" ? raw.updated_at : void 0
165
+ };
166
+ } catch {
167
+ return null;
168
+ }
169
+ }
170
+ return null;
171
+ }
172
+ /**
173
+ * Алиас для fetchSession
174
+ */
175
+ async getSession(sessionId) {
176
+ return this.fetchSession(sessionId);
177
+ }
178
+ /**
179
+ * Инициация фиксации (коммита) накопленных токенов сессии в долговременную память
180
+ */
181
+ async commitSession(sessionId, options) {
182
+ if (!sessionId || !sessionId.trim()) {
183
+ return { ok: false, error: "Missing sessionId" };
184
+ }
185
+ const candidates = this.getCandidateSessionIds(sessionId);
186
+ const bodyPayload = JSON.stringify(options ?? { keep_recent_count: 10 });
187
+ let lastError = "Session commit failed";
188
+ for (const candidateId of candidates) {
189
+ try {
190
+ const res = await fetch(
191
+ `${this.endpoint}/api/v1/sessions/${encodeURIComponent(candidateId)}/commit`,
192
+ {
193
+ method: "POST",
194
+ headers: this.getHeaders(),
195
+ body: bodyPayload
196
+ }
197
+ );
198
+ if (res.status === 404) {
199
+ lastError = `Session not found: ${candidateId}`;
200
+ continue;
201
+ }
202
+ if (!res.ok) {
203
+ const errBody = await res.json().catch(() => null);
204
+ const errorMsg = errBody?.error?.message || errBody?.message || errBody?.error || `HTTP ${res.status}: ${res.statusText}`;
205
+ return { ok: false, error: String(errorMsg) };
206
+ }
207
+ this.resolvedSessionIds.set(sessionId.trim(), candidateId);
208
+ return { ok: true };
209
+ } catch (err) {
210
+ return {
211
+ ok: false,
212
+ error: err instanceof Error ? err.message : String(err)
213
+ };
214
+ }
215
+ }
216
+ return { ok: false, error: lastError };
217
+ }
218
+ };
219
+ var defaultOpenVikingClient = new OpenVikingClient();
220
+ function checkHealth(endpoint, apiKey) {
221
+ const client = endpoint || apiKey ? new OpenVikingClient(endpoint, apiKey) : defaultOpenVikingClient;
222
+ return client.checkHealth();
223
+ }
224
+ function fetchSession(sessionId, endpoint, apiKey) {
225
+ const client = endpoint || apiKey ? new OpenVikingClient(endpoint, apiKey) : defaultOpenVikingClient;
226
+ return client.fetchSession(sessionId);
227
+ }
228
+ function getSession(sessionId, endpoint, apiKey) {
229
+ return fetchSession(sessionId, endpoint, apiKey);
230
+ }
231
+ function commitSession(sessionId, options, endpoint, apiKey) {
232
+ const client = endpoint || apiKey ? new OpenVikingClient(endpoint, apiKey) : defaultOpenVikingClient;
233
+ return client.commitSession(sessionId, options);
234
+ }
235
+
236
+ // src/client/recallParser.ts
237
+ var KNOWN_CATEGORIES = /* @__PURE__ */ new Set([
238
+ "preferences",
239
+ "entities",
240
+ "events",
241
+ "skills",
242
+ "resources",
243
+ "profile"
244
+ ]);
245
+ function inferCategory(uri) {
246
+ if (!uri || typeof uri !== "string") return void 0;
247
+ const memoriesMatch = uri.match(/\/memories\/([^/#?]+)/i);
248
+ if (memoriesMatch && memoriesMatch[1]) {
249
+ return memoriesMatch[1].toLowerCase();
250
+ }
251
+ const cleanUri = uri.replace(/^viking:\/\/?/i, "");
252
+ const segments = cleanUri.split("/").filter(Boolean);
253
+ for (const seg of segments) {
254
+ const lower = seg.toLowerCase();
255
+ if (KNOWN_CATEGORIES.has(lower)) {
256
+ return lower;
257
+ }
258
+ }
259
+ if (segments[0]?.toLowerCase() === "user" && segments.length >= 3) {
260
+ return segments[2].toLowerCase();
261
+ }
262
+ if (segments.length >= 2) {
263
+ return segments[0].toLowerCase();
264
+ }
265
+ return void 0;
266
+ }
267
+ function extractAllText(input) {
268
+ if (input == null) return "";
269
+ if (typeof input === "string") return input;
270
+ if (typeof input === "number" || typeof input === "boolean")
271
+ return String(input);
272
+ if (Array.isArray(input)) {
273
+ return input.map(extractAllText).filter(Boolean).join("\n");
274
+ }
275
+ if (typeof input === "object") {
276
+ const obj = input;
277
+ const parts = [];
278
+ if ("content" in obj && obj.content != null) {
279
+ parts.push(extractAllText(obj.content));
280
+ }
281
+ if ("text" in obj && obj.text != null) {
282
+ parts.push(extractAllText(obj.text));
283
+ }
284
+ if ("message" in obj && obj.message != null) {
285
+ parts.push(extractAllText(obj.message));
286
+ }
287
+ if (parts.length > 0) {
288
+ return parts.join("\n");
289
+ }
290
+ try {
291
+ return Object.values(obj).map(extractAllText).filter(Boolean).join("\n");
292
+ } catch {
293
+ return "";
294
+ }
295
+ }
296
+ return "";
297
+ }
298
+ function findContextBlocks(text) {
299
+ const blocks = [];
300
+ const openTagRegex = /<openviking-context\b([^>]*)>/gi;
301
+ let match;
302
+ const tagPositions = [];
303
+ while ((match = openTagRegex.exec(text)) !== null) {
304
+ tagPositions.push({
305
+ index: match.index,
306
+ endIndex: match.index + match[0].length,
307
+ attrs: match[1] || ""
308
+ });
309
+ }
310
+ if (tagPositions.length === 0) {
311
+ if (/<available-memories\b/i.test(text)) {
312
+ blocks.push({
313
+ isProfile: true,
314
+ attributes: 'source="profile"',
315
+ content: text
316
+ });
317
+ } else if (/<memory\b/i.test(text)) {
318
+ blocks.push({
319
+ isProfile: false,
320
+ attributes: "",
321
+ content: text
322
+ });
323
+ }
324
+ return blocks;
325
+ }
326
+ for (let i = 0; i < tagPositions.length; i++) {
327
+ const current = tagPositions[i];
328
+ const nextStart = i + 1 < tagPositions.length ? tagPositions[i + 1].index : text.length;
329
+ const blockText = text.slice(current.endIndex, nextStart);
330
+ const closeIndex = blockText.search(/<\/openviking-context>/i);
331
+ const content = closeIndex !== -1 ? blockText.slice(0, closeIndex) : blockText;
332
+ const isProfile = /\bsource\s*=\s*["']profile["']/i.test(current.attrs);
333
+ blocks.push({
334
+ isProfile,
335
+ attributes: current.attrs,
336
+ content
337
+ });
338
+ }
339
+ return blocks;
340
+ }
341
+ function normalizeUri(uri) {
342
+ let clean = uri.trim();
343
+ if (clean.endsWith("/") && !clean.endsWith("://")) {
344
+ clean = clean.slice(0, -1);
345
+ }
346
+ return clean;
347
+ }
348
+ function parseProfileItems(blockContent) {
349
+ const items = [];
350
+ let memContent = blockContent;
351
+ const availMatch = blockContent.match(
352
+ /<available-memories\b[^>]*>([\s\S]*?)(?:<\/available-memories>|$)/i
353
+ );
354
+ if (availMatch && availMatch[1]) {
355
+ memContent = availMatch[1];
356
+ }
357
+ const lines = memContent.split(/\r?\n/);
358
+ let currentBaseUri = null;
359
+ for (const rawLine of lines) {
360
+ const trimmed = rawLine.trim();
361
+ if (!trimmed) continue;
362
+ const baseMatch = trimmed.match(
363
+ /^[-*•]?\s*(viking:\/\/[^\s<>"'`]+?\/)\s*$/i
364
+ );
365
+ if (baseMatch) {
366
+ currentBaseUri = baseMatch[1];
367
+ continue;
368
+ }
369
+ const bulletMatch = trimmed.match(/^[-*•]\s+([^\s<>"'`].*?)$/);
370
+ if (bulletMatch && currentBaseUri && !bulletMatch[1].startsWith("viking://")) {
371
+ let relPath = bulletMatch[1].trim();
372
+ relPath = relPath.replace(/^\[([^\]]+)\](?:\([^)]*\))?/, "$1");
373
+ relPath = relPath.replace(/[.,;:)\]]+$/, "").trim();
374
+ if (relPath) {
375
+ const cleanBase = currentBaseUri.endsWith("/") ? currentBaseUri : currentBaseUri + "/";
376
+ const fullUri = normalizeUri(cleanBase + relPath.replace(/^\/+/, ""));
377
+ items.push({
378
+ uri: fullUri,
379
+ category: inferCategory(fullUri),
380
+ source: "profile"
381
+ });
382
+ continue;
383
+ }
384
+ }
385
+ const fullBulletMatch = trimmed.match(
386
+ /^[-*•]\s+(viking:\/\/[^<>"'`\r\n]+)/i
387
+ );
388
+ if (fullBulletMatch) {
389
+ let uri = fullBulletMatch[1].trim().replace(/[.,;:)\]]+$/, "");
390
+ if (uri.endsWith("/")) {
391
+ currentBaseUri = uri;
392
+ } else {
393
+ uri = normalizeUri(uri);
394
+ items.push({
395
+ uri,
396
+ category: inferCategory(uri),
397
+ source: "profile"
398
+ });
399
+ continue;
400
+ }
401
+ }
402
+ const inlineMatches = trimmed.matchAll(/viking:\/\/[^\s<>"'`]+/gi);
403
+ for (const m of inlineMatches) {
404
+ let uri = m[0].replace(/[.,;:)\]]+$/, "");
405
+ if (uri.endsWith("/")) {
406
+ currentBaseUri = uri;
407
+ } else {
408
+ uri = normalizeUri(uri);
409
+ items.push({
410
+ uri,
411
+ category: inferCategory(uri),
412
+ source: "profile"
413
+ });
414
+ }
415
+ }
416
+ }
417
+ return items;
418
+ }
419
+ function parseRecallItems(blockContent) {
420
+ const items = [];
421
+ const memoryTagRegex = /<memory\b([^>]*?)(?:\/>|>([\s\S]*?)(?:<\/memory>|(?=<memory\b)|$))/gi;
422
+ let memMatch;
423
+ while ((memMatch = memoryTagRegex.exec(blockContent)) !== null) {
424
+ const attrsString = memMatch[1] || "";
425
+ const innerContent = memMatch[2] || "";
426
+ const uriMatch = attrsString.match(
427
+ /\buri\s*=\s*(?:["'](viking:\/\/[^"']+)["']|(viking:\/\/[^\s>]+))/i
428
+ );
429
+ if (!uriMatch) continue;
430
+ const rawUri = uriMatch[1] || uriMatch[2];
431
+ const uri = normalizeUri(rawUri);
432
+ const scoreMatch = attrsString.match(
433
+ /\bscore\s*=\s*(?:["']([^"']+)["']|([0-9.]+))/i
434
+ );
435
+ let score;
436
+ const scoreStr = scoreMatch ? scoreMatch[1] || scoreMatch[2] : void 0;
437
+ if (scoreStr) {
438
+ const parsed = parseFloat(scoreStr);
439
+ if (Number.isFinite(parsed)) {
440
+ score = parsed;
441
+ }
442
+ }
443
+ const typeMatch = attrsString.match(
444
+ /\b(?:type|category)\s*=\s*["']([^"']+)["']/i
445
+ );
446
+ const explicitCategory = typeMatch ? typeMatch[1].trim() : void 0;
447
+ const category = inferCategory(uri) || explicitCategory;
448
+ let abstractText;
449
+ const abstractAttrMatch = attrsString.match(
450
+ /\babstract\s*=\s*["']([^"']+)["']/i
451
+ );
452
+ if (abstractAttrMatch) {
453
+ abstractText = abstractAttrMatch[1].trim();
454
+ } else {
455
+ const trimmedContent = innerContent.trim();
456
+ if (trimmedContent) {
457
+ abstractText = trimmedContent;
458
+ }
459
+ }
460
+ items.push({
461
+ uri,
462
+ category,
463
+ source: "recall",
464
+ score,
465
+ abstract: abstractText
466
+ });
467
+ }
468
+ return items;
469
+ }
470
+ function parseRecalledMemories(input) {
471
+ if (input == null) {
472
+ return {
473
+ recalledCount: 0,
474
+ items: [],
475
+ profileItems: [],
476
+ recallItems: []
477
+ };
478
+ }
479
+ const text = extractAllText(input);
480
+ if (!text.trim()) {
481
+ return {
482
+ recalledCount: 0,
483
+ items: [],
484
+ profileItems: [],
485
+ recallItems: []
486
+ };
487
+ }
488
+ const blocks = findContextBlocks(text);
489
+ const rawProfileItems = [];
490
+ const rawRecallItems = [];
491
+ for (const block of blocks) {
492
+ if (block.isProfile) {
493
+ rawProfileItems.push(...parseProfileItems(block.content));
494
+ } else {
495
+ rawRecallItems.push(...parseRecallItems(block.content));
496
+ }
497
+ }
498
+ const recallMap = /* @__PURE__ */ new Map();
499
+ for (const item of rawRecallItems) {
500
+ const existing = recallMap.get(item.uri);
501
+ if (!existing) {
502
+ recallMap.set(item.uri, { ...item });
503
+ } else {
504
+ if (existing.score === void 0 && item.score !== void 0) {
505
+ existing.score = item.score;
506
+ } else if (existing.score !== void 0 && item.score !== void 0) {
507
+ existing.score = Math.max(existing.score, item.score);
508
+ }
509
+ if (!existing.abstract && item.abstract) {
510
+ existing.abstract = item.abstract;
511
+ }
512
+ if (!existing.category && item.category) {
513
+ existing.category = item.category;
514
+ }
515
+ }
516
+ }
517
+ const profileMap = /* @__PURE__ */ new Map();
518
+ for (const item of rawProfileItems) {
519
+ if (!profileMap.has(item.uri)) {
520
+ profileMap.set(item.uri, { ...item });
521
+ }
522
+ }
523
+ const itemsMap = /* @__PURE__ */ new Map();
524
+ for (const [uri, item] of profileMap.entries()) {
525
+ itemsMap.set(uri, item);
526
+ }
527
+ for (const [uri, item] of recallMap.entries()) {
528
+ itemsMap.set(uri, item);
529
+ }
530
+ const items = Array.from(itemsMap.values());
531
+ const profileItems = items.filter((it) => it.source === "profile");
532
+ const recallItems = items.filter((it) => it.source === "recall");
533
+ return {
534
+ recalledCount: items.length,
535
+ items,
536
+ profileItems,
537
+ recallItems
538
+ };
539
+ }
540
+
541
+ // src/client/OpenVikingStatusPopover.tsx
542
+ import { useState, useEffect, useCallback, useRef } from "react";
543
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
544
+ function getProgressBarPercent(pendingTokens, threshold = COMMIT_THRESHOLD) {
545
+ if (!threshold || threshold <= 0) return 0;
546
+ const ratio = (pendingTokens || 0) / threshold;
547
+ return Math.min(100, Math.max(0, Math.round(ratio * 100)));
548
+ }
549
+ function getProgressBarColor(percent) {
550
+ if (percent >= 80) {
551
+ return "var(--dsw-status-warning, #fbbf24)";
552
+ }
553
+ return "var(--dsw-status-success, #34d399)";
554
+ }
555
+ function formatRelativeTime(isoOrTimestamp, now = Date.now()) {
556
+ if (!isoOrTimestamp) return void 0;
557
+ const date = new Date(isoOrTimestamp);
558
+ const time = date.getTime();
559
+ if (isNaN(time)) return String(isoOrTimestamp);
560
+ const diffMs = now - time;
561
+ if (diffMs < 0) return "just now";
562
+ const diffSec = Math.floor(diffMs / 1e3);
563
+ if (diffSec < 60) return "just now";
564
+ const diffMin = Math.floor(diffSec / 60);
565
+ if (diffMin < 60) return `${diffMin}m ago`;
566
+ const diffHour = Math.floor(diffMin / 60);
567
+ if (diffHour < 24) return `${diffHour}h ago`;
568
+ const diffDay = Math.floor(diffHour / 24);
569
+ return `${diffDay}d ago`;
570
+ }
571
+ function formatMemoryLeafName(uri) {
572
+ if (!uri) return "";
573
+ const clean = uri.replace(/^viking:\/\/?/i, "");
574
+ const memMatch = clean.match(/memories\/[^/]+\/(.+)$/i);
575
+ if (memMatch && memMatch[1]) {
576
+ return memMatch[1];
577
+ }
578
+ const segments = clean.split("/").filter(Boolean);
579
+ if (segments.length >= 2) {
580
+ return segments.slice(-2).join("/");
581
+ }
582
+ return segments[0] || clean;
583
+ }
584
+ function formatEndpoint(endpoint) {
585
+ if (!endpoint || !endpoint.trim()) {
586
+ return "127.0.0.1:1933";
587
+ }
588
+ return endpoint.trim().replace(/^https?:\/\//, "");
589
+ }
590
+ function truncateSessionId(id, maxLen = 16) {
591
+ if (!id) return "";
592
+ if (id.length <= maxLen) return id;
593
+ return `${id.slice(0, maxLen)}...`;
594
+ }
595
+ function getCategoryBadgeStyle(category) {
596
+ switch (category?.toLowerCase()) {
597
+ case "preferences":
598
+ return {
599
+ backgroundColor: "rgba(168, 85, 247, 0.15)",
600
+ color: "var(--dsw-status-purple, #c084fc)"
601
+ };
602
+ case "entities":
603
+ return {
604
+ backgroundColor: "rgba(59, 130, 246, 0.15)",
605
+ color: "var(--dsw-status-info, #60a5fa)"
606
+ };
607
+ case "skills":
608
+ return {
609
+ backgroundColor: "rgba(236, 72, 153, 0.15)",
610
+ color: "var(--dsw-status-pink, #f472b6)"
611
+ };
612
+ case "events":
613
+ return {
614
+ backgroundColor: "rgba(245, 158, 11, 0.15)",
615
+ color: "var(--dsw-status-warning, #fbbf24)"
616
+ };
617
+ case "resources":
618
+ return {
619
+ backgroundColor: "rgba(20, 184, 166, 0.15)",
620
+ color: "var(--dsw-status-teal, #2dd4bf)"
621
+ };
622
+ default:
623
+ return {
624
+ backgroundColor: "rgba(148, 163, 184, 0.15)",
625
+ color: "var(--dsw-text-muted, #94a3b8)"
626
+ };
627
+ }
628
+ }
629
+ function handleEscapeKey(event, onClose) {
630
+ if (event.key === "Escape") {
631
+ onClose?.();
632
+ return true;
633
+ }
634
+ return false;
635
+ }
636
+ function OpenVikingStatusPopover({
637
+ sessionId,
638
+ health,
639
+ sessionData,
640
+ recalledResult,
641
+ endpoint,
642
+ isCommitting = false,
643
+ commitError = null,
644
+ onCommitNow,
645
+ onClose,
646
+ className,
647
+ style
648
+ }) {
649
+ const [copied, setCopied] = useState(false);
650
+ const copyTimeoutRef = useRef(null);
651
+ useEffect(() => {
652
+ return () => {
653
+ if (copyTimeoutRef.current) {
654
+ clearTimeout(copyTimeoutRef.current);
655
+ }
656
+ };
657
+ }, []);
658
+ const isOnline = health?.ok === true;
659
+ const statusColor = isOnline ? "var(--dsw-status-success, #34d399)" : "var(--dsw-status-error, #f87171)";
660
+ const displaySessionId = sessionData?.session_id || sessionId || "";
661
+ const pendingTokens = sessionData?.pending_tokens ?? 0;
662
+ const progressPercent = getProgressBarPercent(pendingTokens);
663
+ const progressBarColor = getProgressBarColor(progressPercent);
664
+ const memoryItems = recalledResult?.items || [];
665
+ const recalledCount = recalledResult?.recalledCount ?? memoryItems.length;
666
+ const handleCopySessionId = useCallback(() => {
667
+ if (!displaySessionId) return;
668
+ if (typeof navigator !== "undefined" && navigator.clipboard) {
669
+ navigator.clipboard.writeText(displaySessionId).catch(() => {
670
+ });
671
+ setCopied(true);
672
+ if (copyTimeoutRef.current) {
673
+ clearTimeout(copyTimeoutRef.current);
674
+ }
675
+ copyTimeoutRef.current = setTimeout(() => {
676
+ setCopied(false);
677
+ copyTimeoutRef.current = null;
678
+ }, 1500);
679
+ }
680
+ }, [displaySessionId]);
681
+ const isCommitDisabled = isCommitting || !isOnline || pendingTokens === 0;
682
+ return /* @__PURE__ */ jsxs(
683
+ "div",
684
+ {
685
+ role: "dialog",
686
+ "aria-label": "OpenViking Memory Details",
687
+ className,
688
+ style: {
689
+ position: "absolute",
690
+ bottom: "calc(100% + 8px)",
691
+ right: 0,
692
+ width: "320px",
693
+ backgroundColor: "var(--dsw-surface-overlay, #1e293b)",
694
+ border: "1px solid var(--dsw-border-default, #334155)",
695
+ borderRadius: "8px",
696
+ padding: "12px",
697
+ boxShadow: "0 10px 25px -5px rgba(0, 0, 0, 0.5)",
698
+ zIndex: 1e3,
699
+ fontSize: "12px",
700
+ color: "var(--dsw-text-default, #f1f5f9)",
701
+ fontFamily: "var(--dsw-font-sans, system-ui, sans-serif)",
702
+ boxSizing: "border-box",
703
+ ...style
704
+ },
705
+ children: [
706
+ /* @__PURE__ */ jsx("style", { children: `@keyframes ov-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }` }),
707
+ /* @__PURE__ */ jsxs(
708
+ "div",
709
+ {
710
+ style: {
711
+ display: "flex",
712
+ justifyContent: "space-between",
713
+ alignItems: "flex-start",
714
+ marginBottom: "10px",
715
+ paddingBottom: "8px",
716
+ borderBottom: "1px solid var(--dsw-border-subtle, rgba(255, 255, 255, 0.08))"
717
+ },
718
+ children: [
719
+ /* @__PURE__ */ jsxs("div", { children: [
720
+ /* @__PURE__ */ jsx(
721
+ "div",
722
+ {
723
+ style: {
724
+ fontWeight: 600,
725
+ fontSize: "13px",
726
+ color: "var(--dsw-text-default, #f1f5f9)"
727
+ },
728
+ children: "OpenViking Memory"
729
+ }
730
+ ),
731
+ /* @__PURE__ */ jsx(
732
+ "div",
733
+ {
734
+ "data-testid": "endpoint-label",
735
+ style: {
736
+ fontSize: "10px",
737
+ color: "var(--dsw-text-muted, #94a3b8)",
738
+ fontFamily: "var(--dsw-font-mono, monospace)",
739
+ marginTop: "1px"
740
+ },
741
+ children: formatEndpoint(endpoint)
742
+ }
743
+ )
744
+ ] }),
745
+ /* @__PURE__ */ jsxs(
746
+ "div",
747
+ {
748
+ "data-testid": "status-badge",
749
+ style: {
750
+ display: "inline-flex",
751
+ alignItems: "center",
752
+ gap: "5px",
753
+ fontSize: "10px",
754
+ padding: "2px 7px",
755
+ borderRadius: "4px",
756
+ backgroundColor: isOnline ? "rgba(52, 211, 153, 0.15)" : "rgba(248, 113, 113, 0.15)",
757
+ color: statusColor,
758
+ fontWeight: 600
759
+ },
760
+ children: [
761
+ /* @__PURE__ */ jsx(
762
+ "span",
763
+ {
764
+ "data-testid": "status-badge-dot",
765
+ style: {
766
+ width: "6px",
767
+ height: "6px",
768
+ borderRadius: "50%",
769
+ backgroundColor: statusColor,
770
+ flexShrink: 0
771
+ }
772
+ }
773
+ ),
774
+ /* @__PURE__ */ jsx("span", { children: isOnline ? health?.version ? `ONLINE v${health.version}` : "ONLINE" : "OFFLINE" })
775
+ ]
776
+ }
777
+ )
778
+ ]
779
+ }
780
+ ),
781
+ /* @__PURE__ */ jsxs(
782
+ "div",
783
+ {
784
+ style: {
785
+ display: "flex",
786
+ flexDirection: "column",
787
+ gap: "6px",
788
+ marginBottom: "10px"
789
+ },
790
+ children: [
791
+ /* @__PURE__ */ jsxs(
792
+ "div",
793
+ {
794
+ style: {
795
+ display: "flex",
796
+ justifyContent: "space-between",
797
+ alignItems: "center"
798
+ },
799
+ children: [
800
+ /* @__PURE__ */ jsx("span", { style: { color: "var(--dsw-text-muted, #94a3b8)" }, children: "Session ID:" }),
801
+ /* @__PURE__ */ jsxs(
802
+ "div",
803
+ {
804
+ style: {
805
+ display: "flex",
806
+ alignItems: "center",
807
+ gap: "4px"
808
+ },
809
+ children: [
810
+ /* @__PURE__ */ jsx(
811
+ "span",
812
+ {
813
+ "data-testid": "session-id-value",
814
+ role: "button",
815
+ tabIndex: 0,
816
+ onKeyDown: (e) => {
817
+ if (e.key === "Enter" || e.key === " ") {
818
+ e.preventDefault();
819
+ handleCopySessionId();
820
+ }
821
+ },
822
+ "aria-label": "Click to copy Session ID",
823
+ style: {
824
+ fontFamily: "var(--dsw-font-mono, monospace)",
825
+ cursor: "pointer"
826
+ },
827
+ title: displaySessionId,
828
+ onClick: handleCopySessionId,
829
+ children: truncateSessionId(displaySessionId)
830
+ }
831
+ ),
832
+ /* @__PURE__ */ jsx(
833
+ "button",
834
+ {
835
+ type: "button",
836
+ "data-testid": "copy-session-btn",
837
+ onClick: handleCopySessionId,
838
+ title: copied ? "Copied!" : "Copy Session ID",
839
+ "aria-label": copied ? "Copied!" : "Copy Session ID",
840
+ style: {
841
+ background: "none",
842
+ border: "none",
843
+ cursor: "pointer",
844
+ padding: "2px 4px",
845
+ fontSize: "10px",
846
+ color: copied ? "var(--dsw-status-success, #34d399)" : "var(--dsw-text-muted, #94a3b8)",
847
+ borderRadius: "3px"
848
+ },
849
+ children: copied ? "\u2713" : "\u{1F4CB}"
850
+ }
851
+ )
852
+ ]
853
+ }
854
+ )
855
+ ]
856
+ }
857
+ ),
858
+ sessionData?.peer_id && /* @__PURE__ */ jsxs(
859
+ "div",
860
+ {
861
+ style: {
862
+ display: "flex",
863
+ justifyContent: "space-between",
864
+ alignItems: "center"
865
+ },
866
+ children: [
867
+ /* @__PURE__ */ jsx("span", { style: { color: "var(--dsw-text-muted, #94a3b8)" }, children: "Peer ID:" }),
868
+ /* @__PURE__ */ jsx(
869
+ "span",
870
+ {
871
+ "data-testid": "peer-id-value",
872
+ style: {
873
+ maxWidth: "180px",
874
+ overflow: "hidden",
875
+ textOverflow: "ellipsis",
876
+ whiteSpace: "nowrap",
877
+ fontFamily: "var(--dsw-font-mono, monospace)"
878
+ },
879
+ title: sessionData.peer_id,
880
+ children: sessionData.peer_id
881
+ }
882
+ )
883
+ ]
884
+ }
885
+ ),
886
+ sessionData?.last_commit_at && /* @__PURE__ */ jsxs(
887
+ "div",
888
+ {
889
+ style: {
890
+ display: "flex",
891
+ justifyContent: "space-between",
892
+ alignItems: "center"
893
+ },
894
+ children: [
895
+ /* @__PURE__ */ jsx("span", { style: { color: "var(--dsw-text-muted, #94a3b8)" }, children: "Last Commit:" }),
896
+ /* @__PURE__ */ jsx(
897
+ "span",
898
+ {
899
+ "data-testid": "last-commit-value",
900
+ title: sessionData.last_commit_at,
901
+ style: { color: "var(--dsw-text-default, #e2e8f0)" },
902
+ children: formatRelativeTime(sessionData.last_commit_at) || sessionData.last_commit_at
903
+ }
904
+ )
905
+ ]
906
+ }
907
+ )
908
+ ]
909
+ }
910
+ ),
911
+ /* @__PURE__ */ jsxs("div", { style: { marginBottom: "12px" }, children: [
912
+ /* @__PURE__ */ jsxs(
913
+ "div",
914
+ {
915
+ style: {
916
+ display: "flex",
917
+ justifyContent: "space-between",
918
+ marginBottom: "4px"
919
+ },
920
+ children: [
921
+ /* @__PURE__ */ jsx("span", { style: { color: "var(--dsw-text-muted, #94a3b8)" }, children: "Pending Tokens:" }),
922
+ /* @__PURE__ */ jsx("span", { "data-testid": "pending-tokens-label", style: { fontWeight: 500 }, children: `${pendingTokens.toLocaleString()} / ${COMMIT_THRESHOLD.toLocaleString()}` })
923
+ ]
924
+ }
925
+ ),
926
+ /* @__PURE__ */ jsx(
927
+ "div",
928
+ {
929
+ "data-testid": "progress-bar-track",
930
+ style: {
931
+ width: "100%",
932
+ height: "6px",
933
+ borderRadius: "3px",
934
+ backgroundColor: "rgba(255, 255, 255, 0.1)",
935
+ overflow: "hidden"
936
+ },
937
+ children: /* @__PURE__ */ jsx(
938
+ "div",
939
+ {
940
+ "data-testid": "progress-bar-fill",
941
+ style: {
942
+ width: `${progressPercent}%`,
943
+ height: "100%",
944
+ backgroundColor: progressBarColor,
945
+ transition: "width 0.3s ease, background-color 0.3s ease"
946
+ }
947
+ }
948
+ )
949
+ }
950
+ )
951
+ ] }),
952
+ /* @__PURE__ */ jsxs("div", { style: { marginBottom: "12px" }, children: [
953
+ /* @__PURE__ */ jsx(
954
+ "div",
955
+ {
956
+ style: {
957
+ display: "flex",
958
+ justifyContent: "space-between",
959
+ alignItems: "center",
960
+ marginBottom: "4px"
961
+ },
962
+ children: /* @__PURE__ */ jsx("span", { style: { fontWeight: 600, fontSize: "11px" }, children: `Recalled Memories (${recalledCount})` })
963
+ }
964
+ ),
965
+ memoryItems.length === 0 ? /* @__PURE__ */ jsx(
966
+ "div",
967
+ {
968
+ "data-testid": "empty-memories-message",
969
+ style: {
970
+ padding: "8px 0",
971
+ color: "var(--dsw-text-muted, #94a3b8)",
972
+ fontStyle: "italic",
973
+ fontSize: "11px",
974
+ textAlign: "center"
975
+ },
976
+ children: "No memories recalled in this session"
977
+ }
978
+ ) : /* @__PURE__ */ jsx(
979
+ "div",
980
+ {
981
+ "data-testid": "recalled-memories-list",
982
+ style: {
983
+ maxHeight: "140px",
984
+ overflowY: "auto",
985
+ display: "flex",
986
+ flexDirection: "column",
987
+ gap: "4px",
988
+ paddingRight: "2px"
989
+ },
990
+ children: memoryItems.map((item, idx) => /* @__PURE__ */ jsxs(
991
+ "div",
992
+ {
993
+ "data-testid": "recalled-memory-item",
994
+ title: item.uri,
995
+ style: {
996
+ display: "flex",
997
+ alignItems: "center",
998
+ gap: "6px",
999
+ padding: "4px 6px",
1000
+ borderRadius: "4px",
1001
+ backgroundColor: "rgba(255, 255, 255, 0.04)",
1002
+ border: "1px solid var(--dsw-border-subtle, rgba(255, 255, 255, 0.05))",
1003
+ fontSize: "11px"
1004
+ },
1005
+ children: [
1006
+ /* @__PURE__ */ jsx(
1007
+ "span",
1008
+ {
1009
+ "data-testid": "memory-category-badge",
1010
+ style: {
1011
+ fontSize: "9px",
1012
+ fontWeight: 600,
1013
+ padding: "1px 4px",
1014
+ borderRadius: "3px",
1015
+ textTransform: "uppercase",
1016
+ flexShrink: 0,
1017
+ ...getCategoryBadgeStyle(item.category)
1018
+ },
1019
+ children: item.category || "memory"
1020
+ }
1021
+ ),
1022
+ /* @__PURE__ */ jsx(
1023
+ "span",
1024
+ {
1025
+ "data-testid": "memory-source-badge",
1026
+ style: {
1027
+ fontSize: "9px",
1028
+ fontWeight: 600,
1029
+ padding: "1px 4px",
1030
+ borderRadius: "3px",
1031
+ textTransform: "uppercase",
1032
+ flexShrink: 0,
1033
+ ...item.source === "profile" ? {
1034
+ backgroundColor: "rgba(99, 102, 241, 0.15)",
1035
+ color: "#818cf8"
1036
+ } : {
1037
+ backgroundColor: "rgba(16, 185, 129, 0.15)",
1038
+ color: "var(--dsw-status-success, #34d399)"
1039
+ }
1040
+ },
1041
+ children: item.source
1042
+ }
1043
+ ),
1044
+ /* @__PURE__ */ jsx(
1045
+ "span",
1046
+ {
1047
+ "data-testid": "memory-leaf-name",
1048
+ style: {
1049
+ flex: 1,
1050
+ overflow: "hidden",
1051
+ textOverflow: "ellipsis",
1052
+ whiteSpace: "nowrap",
1053
+ fontFamily: "var(--dsw-font-mono, monospace)",
1054
+ color: "var(--dsw-text-default, #f1f5f9)"
1055
+ },
1056
+ children: formatMemoryLeafName(item.uri)
1057
+ }
1058
+ )
1059
+ ]
1060
+ },
1061
+ `${item.uri}-${idx}`
1062
+ ))
1063
+ }
1064
+ )
1065
+ ] }),
1066
+ commitError && /* @__PURE__ */ jsx(
1067
+ "div",
1068
+ {
1069
+ "data-testid": "commit-error-message",
1070
+ style: {
1071
+ marginBottom: "8px",
1072
+ padding: "6px 8px",
1073
+ borderRadius: "4px",
1074
+ backgroundColor: "rgba(248, 113, 113, 0.1)",
1075
+ border: "1px solid var(--dsw-status-error, #f87171)",
1076
+ color: "var(--dsw-status-error, #f87171)",
1077
+ fontSize: "11px",
1078
+ wordBreak: "break-word"
1079
+ },
1080
+ children: commitError
1081
+ }
1082
+ ),
1083
+ /* @__PURE__ */ jsx(
1084
+ "button",
1085
+ {
1086
+ type: "button",
1087
+ "data-testid": "commit-now-btn",
1088
+ onClick: () => onCommitNow?.(),
1089
+ disabled: isCommitDisabled,
1090
+ style: {
1091
+ width: "100%",
1092
+ padding: "7px 0",
1093
+ borderRadius: "6px",
1094
+ border: "1px solid var(--dsw-border-default, #475569)",
1095
+ backgroundColor: isCommitting ? "var(--dsw-surface-active, #334155)" : isCommitDisabled ? "rgba(255, 255, 255, 0.03)" : "var(--dsw-surface-base, #1e293b)",
1096
+ color: isCommitDisabled ? "var(--dsw-text-muted, #64748b)" : "var(--dsw-text-default, #f8fafc)",
1097
+ cursor: isCommitDisabled ? "not-allowed" : "pointer",
1098
+ fontWeight: 600,
1099
+ fontSize: "12px",
1100
+ transition: "all 0.15s ease",
1101
+ display: "flex",
1102
+ alignItems: "center",
1103
+ justifyContent: "center",
1104
+ gap: "6px"
1105
+ },
1106
+ children: isCommitting ? /* @__PURE__ */ jsxs(Fragment, { children: [
1107
+ /* @__PURE__ */ jsx(
1108
+ "span",
1109
+ {
1110
+ style: {
1111
+ display: "inline-block",
1112
+ width: "10px",
1113
+ height: "10px",
1114
+ borderRadius: "50%",
1115
+ border: "2px solid var(--dsw-status-warning, #fbbf24)",
1116
+ borderTopColor: "transparent",
1117
+ animation: "ov-spin 1s linear infinite"
1118
+ }
1119
+ }
1120
+ ),
1121
+ /* @__PURE__ */ jsx("span", { children: "Committing..." })
1122
+ ] }) : /* @__PURE__ */ jsx("span", { children: "Commit To Memory Now" })
1123
+ }
1124
+ )
1125
+ ]
1126
+ }
1127
+ );
1128
+ }
1129
+
1130
+ // src/client/OpenVikingStatusChip.tsx
1131
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
1132
+ var COMMIT_THRESHOLD = 2e4;
1133
+ function formatPendingTokens(pendingTokens) {
1134
+ const k = Math.round((pendingTokens || 0) / 1e3);
1135
+ return `${k}k pend`;
1136
+ }
1137
+ function formatStatusLabel(isOnline, recalledCount, pendingTokens) {
1138
+ if (!isOnline) {
1139
+ return "OV offline";
1140
+ }
1141
+ return `OV: ${recalledCount} rec \xB7 ${formatPendingTokens(pendingTokens)}`;
1142
+ }
1143
+ function formatTooltipTitle({
1144
+ isOnline,
1145
+ isCommitting = false,
1146
+ recalledCount,
1147
+ pendingTokens
1148
+ }) {
1149
+ if (!isOnline) {
1150
+ return "OpenViking: Offline";
1151
+ }
1152
+ const countLabel = `${recalledCount} recalled`;
1153
+ const tokenLabel = `${(pendingTokens || 0).toLocaleString()} pending tokens`;
1154
+ if (isCommitting) {
1155
+ return `OpenViking: Committing... (${countLabel}, ${tokenLabel})`;
1156
+ }
1157
+ return `OpenViking: Online (${countLabel}, ${tokenLabel})`;
1158
+ }
1159
+ function getStatusIndicatorColor(isOnline, isCommitting = false) {
1160
+ if (!isOnline) {
1161
+ return "var(--dsw-status-error, #f87171)";
1162
+ }
1163
+ if (isCommitting) {
1164
+ return "var(--dsw-status-warning, #fbbf24)";
1165
+ }
1166
+ return "var(--dsw-status-success, #34d399)";
1167
+ }
1168
+ function getStatusGlow(isOnline, isCommitting = false) {
1169
+ if (isOnline && !isCommitting) {
1170
+ return "0 0 6px var(--dsw-status-success, #34d399)";
1171
+ }
1172
+ return "none";
1173
+ }
1174
+ function getFallbackSessionMessages(sessionId) {
1175
+ if (!sessionId || typeof window === "undefined") {
1176
+ return void 0;
1177
+ }
1178
+ const win = window;
1179
+ if (win.__DSH_STORE__?.getState) {
1180
+ const state = win.__DSH_STORE__.getState();
1181
+ return state?.conversations?.[sessionId]?.messages || state?.sessions?.[sessionId]?.messages;
1182
+ }
1183
+ if (win.__DSH_SESSION_MESSAGES__?.[sessionId]) {
1184
+ return win.__DSH_SESSION_MESSAGES__[sessionId];
1185
+ }
1186
+ return void 0;
1187
+ }
1188
+ function OpenVikingStatusChip({
1189
+ sessionId,
1190
+ messages,
1191
+ contextText,
1192
+ client,
1193
+ onCommit,
1194
+ className,
1195
+ initialHealth,
1196
+ initialSessionData,
1197
+ initialOpen = false
1198
+ }) {
1199
+ const [health, setHealth] = useState2(
1200
+ initialHealth ?? null
1201
+ );
1202
+ const [sessionData, setSessionData] = useState2(
1203
+ initialSessionData ?? null
1204
+ );
1205
+ const [isOpen, setIsOpen] = useState2(initialOpen);
1206
+ const [isCommitting, setIsCommitting] = useState2(false);
1207
+ const [commitError, setCommitError] = useState2(null);
1208
+ const popoverRef = useRef2(null);
1209
+ const apiClient = client ?? defaultOpenVikingClient;
1210
+ const fallbackMessages = useMemo(() => {
1211
+ if (messages || contextText) return void 0;
1212
+ return getFallbackSessionMessages(sessionId);
1213
+ }, [sessionId, messages, contextText]);
1214
+ const inputForParser = useMemo(() => {
1215
+ if (contextText && messages) {
1216
+ return [contextText, ...messages];
1217
+ }
1218
+ return contextText ?? messages ?? fallbackMessages;
1219
+ }, [contextText, messages, fallbackMessages]);
1220
+ const recalledResult = useMemo(
1221
+ () => parseRecalledMemories(inputForParser),
1222
+ [inputForParser]
1223
+ );
1224
+ const fetchStatus = useCallback2(async () => {
1225
+ try {
1226
+ const healthRes = await apiClient.checkHealth();
1227
+ setHealth(healthRes);
1228
+ if (!healthRes.ok) {
1229
+ return;
1230
+ }
1231
+ const session = await apiClient.fetchSession(sessionId);
1232
+ setSessionData(session);
1233
+ } catch {
1234
+ setHealth({ ok: false });
1235
+ }
1236
+ }, [sessionId, apiClient]);
1237
+ useEffect2(() => {
1238
+ fetchStatus();
1239
+ const timer = setInterval(fetchStatus, 15e3);
1240
+ return () => clearInterval(timer);
1241
+ }, [fetchStatus]);
1242
+ useEffect2(() => {
1243
+ function handleClickOutside(event) {
1244
+ if (popoverRef.current && !popoverRef.current.contains(event.target)) {
1245
+ setIsOpen(false);
1246
+ }
1247
+ }
1248
+ function handleKeyDown(event) {
1249
+ if (event.key === "Escape") {
1250
+ setIsOpen(false);
1251
+ }
1252
+ }
1253
+ if (isOpen) {
1254
+ document.addEventListener("mousedown", handleClickOutside);
1255
+ document.addEventListener("keydown", handleKeyDown);
1256
+ }
1257
+ return () => {
1258
+ document.removeEventListener("mousedown", handleClickOutside);
1259
+ document.removeEventListener("keydown", handleKeyDown);
1260
+ };
1261
+ }, [isOpen]);
1262
+ const handleCommitNow = async () => {
1263
+ if (isCommitting) return;
1264
+ setIsCommitting(true);
1265
+ setCommitError(null);
1266
+ try {
1267
+ const res = await apiClient.commitSession(sessionId, {
1268
+ keep_recent_count: 10
1269
+ });
1270
+ if (res.ok) {
1271
+ await fetchStatus();
1272
+ onCommit?.();
1273
+ } else {
1274
+ setCommitError(res.error || "Commit failed");
1275
+ }
1276
+ } catch (e) {
1277
+ const msg = e instanceof Error ? e.message : String(e);
1278
+ setCommitError(msg || "Failed to commit session");
1279
+ console.error("[dsh-openviking-status] Failed to commit session:", e);
1280
+ } finally {
1281
+ setIsCommitting(false);
1282
+ }
1283
+ };
1284
+ const isOnline = health?.ok === true;
1285
+ const pendingTokens = sessionData?.pending_tokens ?? 0;
1286
+ const pendingTokensK = Math.round(pendingTokens / 1e3);
1287
+ const recalledCount = recalledResult.recalledCount;
1288
+ const statusColor = getStatusIndicatorColor(isOnline, isCommitting);
1289
+ const statusGlow = getStatusGlow(isOnline, isCommitting);
1290
+ const tooltipTitle = formatTooltipTitle({
1291
+ isOnline,
1292
+ isCommitting,
1293
+ recalledCount,
1294
+ pendingTokens
1295
+ });
1296
+ return /* @__PURE__ */ jsxs2(
1297
+ "div",
1298
+ {
1299
+ className,
1300
+ style: {
1301
+ position: "relative",
1302
+ display: "inline-flex",
1303
+ alignItems: "center"
1304
+ },
1305
+ ref: popoverRef,
1306
+ children: [
1307
+ /* @__PURE__ */ jsxs2(
1308
+ "button",
1309
+ {
1310
+ type: "button",
1311
+ onClick: () => setIsOpen(!isOpen),
1312
+ "aria-expanded": isOpen,
1313
+ "aria-haspopup": "dialog",
1314
+ style: {
1315
+ display: "inline-flex",
1316
+ alignItems: "center",
1317
+ gap: "6px",
1318
+ height: "26px",
1319
+ padding: "0 8px",
1320
+ fontSize: "12px",
1321
+ fontFamily: "var(--dsw-font-mono, monospace)",
1322
+ borderRadius: "6px",
1323
+ background: "var(--dsw-surface-base, rgba(255, 255, 255, 0.05))",
1324
+ border: "1px solid var(--dsw-border-subtle, rgba(255, 255, 255, 0.1))",
1325
+ color: "var(--dsw-text-muted, #94a3b8)",
1326
+ cursor: "pointer",
1327
+ transition: "all 0.15s ease",
1328
+ userSelect: "none"
1329
+ },
1330
+ title: tooltipTitle,
1331
+ "aria-label": tooltipTitle,
1332
+ children: [
1333
+ /* @__PURE__ */ jsx2(
1334
+ "span",
1335
+ {
1336
+ "data-testid": "status-dot",
1337
+ style: {
1338
+ width: "7px",
1339
+ height: "7px",
1340
+ borderRadius: "50%",
1341
+ backgroundColor: statusColor,
1342
+ boxShadow: statusGlow,
1343
+ flexShrink: 0
1344
+ }
1345
+ }
1346
+ ),
1347
+ /* @__PURE__ */ jsx2(
1348
+ "span",
1349
+ {
1350
+ style: { fontWeight: 600, color: "var(--dsw-text-default, #e2e8f0)" },
1351
+ children: isOnline ? "OV:" : "OV"
1352
+ }
1353
+ ),
1354
+ " ",
1355
+ /* @__PURE__ */ jsx2("span", { children: isOnline ? `${recalledCount} rec \xB7 ${pendingTokensK}k pend` : "offline" })
1356
+ ]
1357
+ }
1358
+ ),
1359
+ isOpen && /* @__PURE__ */ jsx2(
1360
+ OpenVikingStatusPopover,
1361
+ {
1362
+ sessionId,
1363
+ health,
1364
+ sessionData,
1365
+ recalledResult,
1366
+ endpoint: apiClient.endpoint,
1367
+ isCommitting,
1368
+ commitError,
1369
+ onCommitNow: handleCommitNow,
1370
+ onClose: () => setIsOpen(false)
1371
+ }
1372
+ )
1373
+ ]
1374
+ }
1375
+ );
1376
+ }
1377
+
1378
+ // src/client/index.tsx
1379
+ var name = "@dipertq/dsh-openviking-status";
1380
+ var inject = ["slots"];
1381
+ function apply(ctx) {
1382
+ ctx.effect(
1383
+ () => ctx.slots.inject(
1384
+ "conversation.input.right",
1385
+ () => ctx.slots.register(
1386
+ {
1387
+ name: "conversation.input.right",
1388
+ id: "openviking-status",
1389
+ order: 50,
1390
+ label: "OpenViking"
1391
+ },
1392
+ OpenVikingStatusChip
1393
+ )
1394
+ ),
1395
+ "openviking-status: composer chip"
1396
+ );
1397
+ }
1398
+ export {
1399
+ COMMIT_THRESHOLD,
1400
+ DEFAULT_OPENVIKING_ENDPOINT,
1401
+ OpenVikingClient,
1402
+ OpenVikingStatusChip,
1403
+ OpenVikingStatusPopover,
1404
+ apply,
1405
+ checkHealth,
1406
+ commitSession,
1407
+ defaultOpenVikingClient,
1408
+ fetchSession,
1409
+ formatEndpoint,
1410
+ formatMemoryLeafName,
1411
+ formatPendingTokens,
1412
+ formatRelativeTime,
1413
+ formatStatusLabel,
1414
+ formatTooltipTitle,
1415
+ getCategoryBadgeStyle,
1416
+ getFallbackSessionMessages,
1417
+ getProgressBarColor,
1418
+ getProgressBarPercent,
1419
+ getSession,
1420
+ getStatusGlow,
1421
+ getStatusIndicatorColor,
1422
+ handleEscapeKey,
1423
+ inferCategory,
1424
+ inject,
1425
+ name,
1426
+ parseRecalledMemories,
1427
+ resolveApiKey,
1428
+ resolveEndpoint,
1429
+ truncateSessionId
1430
+ };
1431
+ //# sourceMappingURL=client.js.map