@ephemushroom/opencode-claude-mem 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -7
- package/dist/index.js +114 -0
- package/dist/tui-registration.d.ts +9 -0
- package/dist/tui.d.ts +11 -2
- package/dist/tui.js +156 -28
- package/dist/worker-client.d.ts +15 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -118,6 +118,7 @@ The worker endpoint is resolved in this order: `CLAUDE_MEM_WORKER_HOST` /
|
|
|
118
118
|
| `GET` | `/api/timeline?project={name}&anchor={id}` | Chronological context around an observation |
|
|
119
119
|
| `POST` | `/api/observations/batch` | Fetch full observation details by ID |
|
|
120
120
|
| `GET` | `/api/stats` + `/api/processing-status` | Sidebar status panel |
|
|
121
|
+
| `GET` | `/api/summaries` + `/api/observations` | Sidebar recent items (expanded only) |
|
|
121
122
|
|
|
122
123
|
Session and observation writes include `platformSource: "opencode"` so
|
|
123
124
|
Claude-Mem can attribute new OpenCode memories separately from Claude Code,
|
|
@@ -215,13 +216,28 @@ plugin, so a separately configured MCP server is not required.
|
|
|
215
216
|
|
|
216
217
|
## Sidebar Status
|
|
217
218
|
|
|
218
|
-
When running in the OpenCode TUI, the plugin registers a sidebar
|
|
219
|
-
**Memory** via its `./tui` export
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
219
|
+
When running in the OpenCode TUI, the plugin registers a sidebar section titled
|
|
220
|
+
**Memory** via its `./tui` export, styled after OpenCode's native MCP/Context
|
|
221
|
+
sections: a borderless, click-to-toggle collapsible block.
|
|
222
|
+
|
|
223
|
+
- **Collapsed (default)** — a single line: `▶ Memory (online, 11.7k obs)`.
|
|
224
|
+
The summary turns yellow when the worker is processing (`(queue N)`) and red
|
|
225
|
+
when it is offline (`(offline)`).
|
|
226
|
+
- **Expanded** — click the header to expand: database counts, processing
|
|
227
|
+
status, the last 3 session summaries (`Recent sessions`), and the last 3
|
|
228
|
+
observations (`Latest`, with the same type icons used in the injected
|
|
229
|
+
context: ◆ feature, ● bugfix, ⚖ decision, ○ discovery, …).
|
|
230
|
+
|
|
231
|
+
Recent items are only fetched while expanded, so the collapsed poll loop stays
|
|
232
|
+
cheap. The panel fails open — if the worker is offline it simply shows the
|
|
233
|
+
offline state and never blocks the TUI. Requires OpenCode's `@opentui/solid`
|
|
234
|
+
runtime; if that is unavailable the panel is skipped silently.
|
|
235
|
+
|
|
236
|
+
The plugin also self-heals `~/.config/opencode/tui.json` on load (like
|
|
237
|
+
oh-my-openagent does): if the plugin is registered as a server plugin in
|
|
238
|
+
`opencode.json` but missing from the TUI plugin list, it appends itself so the
|
|
239
|
+
sidebar loads without manual configuration. Symlinked `tui.json` files are
|
|
240
|
+
written through, preserving dotfiles setups.
|
|
225
241
|
|
|
226
242
|
## Key Implementation Details
|
|
227
243
|
|
package/dist/index.js
CHANGED
|
@@ -12647,6 +12647,51 @@ class WorkerClient {
|
|
|
12647
12647
|
return null;
|
|
12648
12648
|
}
|
|
12649
12649
|
}
|
|
12650
|
+
static async getRecentSummaries(project, limit) {
|
|
12651
|
+
try {
|
|
12652
|
+
const params = new URLSearchParams({ project, limit: String(limit) });
|
|
12653
|
+
const response = await fetch(`${this.BASE_URL}/api/summaries?${params.toString()}`);
|
|
12654
|
+
if (!response.ok) {
|
|
12655
|
+
return [];
|
|
12656
|
+
}
|
|
12657
|
+
const payload = await response.json();
|
|
12658
|
+
if (!Array.isArray(payload.items)) {
|
|
12659
|
+
return [];
|
|
12660
|
+
}
|
|
12661
|
+
return payload.items.flatMap((item) => {
|
|
12662
|
+
if (!isRecord(item) || typeof item["id"] !== "number") {
|
|
12663
|
+
return [];
|
|
12664
|
+
}
|
|
12665
|
+
const request = typeof item["request"] === "string" ? item["request"].trim() : "";
|
|
12666
|
+
return request ? [{ id: item["id"], request }] : [];
|
|
12667
|
+
});
|
|
12668
|
+
} catch {
|
|
12669
|
+
return [];
|
|
12670
|
+
}
|
|
12671
|
+
}
|
|
12672
|
+
static async getRecentObservations(project, limit) {
|
|
12673
|
+
try {
|
|
12674
|
+
const params = new URLSearchParams({ project, limit: String(limit) });
|
|
12675
|
+
const response = await fetch(`${this.BASE_URL}/api/observations?${params.toString()}`);
|
|
12676
|
+
if (!response.ok) {
|
|
12677
|
+
return [];
|
|
12678
|
+
}
|
|
12679
|
+
const payload = await response.json();
|
|
12680
|
+
if (!Array.isArray(payload.items)) {
|
|
12681
|
+
return [];
|
|
12682
|
+
}
|
|
12683
|
+
return payload.items.flatMap((item) => {
|
|
12684
|
+
if (!isRecord(item) || typeof item["id"] !== "number") {
|
|
12685
|
+
return [];
|
|
12686
|
+
}
|
|
12687
|
+
const title = typeof item["title"] === "string" ? item["title"].trim() : "";
|
|
12688
|
+
const type = typeof item["type"] === "string" ? item["type"] : "";
|
|
12689
|
+
return title ? [{ id: item["id"], type, title }] : [];
|
|
12690
|
+
});
|
|
12691
|
+
} catch {
|
|
12692
|
+
return [];
|
|
12693
|
+
}
|
|
12694
|
+
}
|
|
12650
12695
|
static async search(query, project, limit) {
|
|
12651
12696
|
try {
|
|
12652
12697
|
const params = new URLSearchParams({ query, project });
|
|
@@ -12669,6 +12714,74 @@ class WorkerClient {
|
|
|
12669
12714
|
}
|
|
12670
12715
|
}
|
|
12671
12716
|
|
|
12717
|
+
// src/tui-registration.ts
|
|
12718
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
12719
|
+
import { homedir as homedir2 } from "os";
|
|
12720
|
+
import { join as join2 } from "path";
|
|
12721
|
+
var PACKAGE_NAME = "@ephemushroom/opencode-claude-mem";
|
|
12722
|
+
function getConfigDir() {
|
|
12723
|
+
const custom2 = process.env["OPENCODE_CONFIG_DIR"]?.trim();
|
|
12724
|
+
return custom2 || join2(homedir2(), ".config", "opencode");
|
|
12725
|
+
}
|
|
12726
|
+
function readJson(filePath) {
|
|
12727
|
+
try {
|
|
12728
|
+
const parsed = JSON.parse(readFileSync2(filePath, "utf8"));
|
|
12729
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : null;
|
|
12730
|
+
} catch {
|
|
12731
|
+
return null;
|
|
12732
|
+
}
|
|
12733
|
+
}
|
|
12734
|
+
function pluginEntries(config2) {
|
|
12735
|
+
const { plugin } = config2;
|
|
12736
|
+
if (!Array.isArray(plugin)) {
|
|
12737
|
+
return [];
|
|
12738
|
+
}
|
|
12739
|
+
return plugin.filter((entry) => typeof entry === "string");
|
|
12740
|
+
}
|
|
12741
|
+
function isOwnEntry(entry) {
|
|
12742
|
+
return entry === PACKAGE_NAME || entry.startsWith(`${PACKAGE_NAME}@`);
|
|
12743
|
+
}
|
|
12744
|
+
function findServerEntry(configDir) {
|
|
12745
|
+
for (const name of ["opencode.json", "opencode.jsonc"]) {
|
|
12746
|
+
const filePath = join2(configDir, name);
|
|
12747
|
+
const config2 = existsSync2(filePath) ? readJson(filePath) : null;
|
|
12748
|
+
const entry = config2 ? pluginEntries(config2).find(isOwnEntry) : undefined;
|
|
12749
|
+
if (entry) {
|
|
12750
|
+
return entry;
|
|
12751
|
+
}
|
|
12752
|
+
}
|
|
12753
|
+
return null;
|
|
12754
|
+
}
|
|
12755
|
+
function ensureTuiPluginEntry() {
|
|
12756
|
+
try {
|
|
12757
|
+
const configDir = getConfigDir();
|
|
12758
|
+
const serverEntry = findServerEntry(configDir);
|
|
12759
|
+
if (!serverEntry) {
|
|
12760
|
+
return "no-server-entry";
|
|
12761
|
+
}
|
|
12762
|
+
const tuiJsonPath = join2(configDir, "tui.json");
|
|
12763
|
+
let config2 = {};
|
|
12764
|
+
if (existsSync2(tuiJsonPath)) {
|
|
12765
|
+
const parsed = readJson(tuiJsonPath);
|
|
12766
|
+
if (!parsed) {
|
|
12767
|
+
return "malformed";
|
|
12768
|
+
}
|
|
12769
|
+
config2 = parsed;
|
|
12770
|
+
}
|
|
12771
|
+
const plugins = pluginEntries(config2);
|
|
12772
|
+
if (plugins.some(isOwnEntry)) {
|
|
12773
|
+
return "already-present";
|
|
12774
|
+
}
|
|
12775
|
+
mkdirSync(configDir, { recursive: true });
|
|
12776
|
+
const next = { ...config2, plugin: [...plugins, serverEntry] };
|
|
12777
|
+
writeFileSync(tuiJsonPath, `${JSON.stringify(next, null, 2)}
|
|
12778
|
+
`);
|
|
12779
|
+
return "added";
|
|
12780
|
+
} catch {
|
|
12781
|
+
return "failed";
|
|
12782
|
+
}
|
|
12783
|
+
}
|
|
12784
|
+
|
|
12672
12785
|
// src/index.ts
|
|
12673
12786
|
var MAX_OBSERVATION_BYTES = 24 * 1024;
|
|
12674
12787
|
var MAX_TAG_REPLACEMENTS = 100;
|
|
@@ -12773,6 +12886,7 @@ function extractTextFromParts(parts) {
|
|
|
12773
12886
|
}
|
|
12774
12887
|
var ClaudeMemPlugin = async (ctx) => {
|
|
12775
12888
|
const { project, directory, client } = ctx;
|
|
12889
|
+
ensureTuiPluginEntry();
|
|
12776
12890
|
const projectRoot = directory || process.cwd();
|
|
12777
12891
|
const projectName = project?.worktree ? project.worktree.split(/[\\/]/).findLast(Boolean) || "unknown-project" : "unknown-project";
|
|
12778
12892
|
async function toast(message, variant = "info", duration3 = 3000) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type TuiRegistrationResult = 'added' | 'already-present' | 'no-server-entry' | 'malformed' | 'failed';
|
|
2
|
+
/**
|
|
3
|
+
* Self-heal ~/.config/opencode/tui.json: if this plugin is registered as an
|
|
4
|
+
* OpenCode server plugin but missing from the TUI plugin list, append it so
|
|
5
|
+
* the Memory sidebar loads without manual config. Uses writeFileSync (not
|
|
6
|
+
* atomic rename) so a symlinked tui.json keeps pointing at its dotfiles
|
|
7
|
+
* target instead of being replaced by a regular file.
|
|
8
|
+
*/
|
|
9
|
+
export declare function ensureTuiPluginEntry(): TuiRegistrationResult;
|
package/dist/tui.d.ts
CHANGED
|
@@ -6,6 +6,15 @@ export interface MemSidebarView {
|
|
|
6
6
|
readonly summaries: number | null;
|
|
7
7
|
readonly queueDepth: number | null;
|
|
8
8
|
readonly isProcessing: boolean;
|
|
9
|
+
readonly recentSummaries: readonly {
|
|
10
|
+
id: number;
|
|
11
|
+
request: string;
|
|
12
|
+
}[];
|
|
13
|
+
readonly recentObservations: readonly {
|
|
14
|
+
id: number;
|
|
15
|
+
type: string;
|
|
16
|
+
title: string;
|
|
17
|
+
}[];
|
|
9
18
|
}
|
|
10
19
|
interface ViewNode {
|
|
11
20
|
readonly kind: 'box' | 'text';
|
|
@@ -22,9 +31,9 @@ interface Theme {
|
|
|
22
31
|
readonly error: string;
|
|
23
32
|
readonly borderSubtle: string;
|
|
24
33
|
}
|
|
25
|
-
export declare function buildMemNodes(view: MemSidebarView, theme: Theme): ViewNode[];
|
|
34
|
+
export declare function buildMemNodes(view: MemSidebarView, theme: Theme, collapsed: boolean, onToggle: () => void): ViewNode[];
|
|
26
35
|
export declare function viewKey(view: MemSidebarView): string;
|
|
27
|
-
export declare function readMemView(project: string): Promise<MemSidebarView>;
|
|
36
|
+
export declare function readMemView(project: string, collapsed: boolean): Promise<MemSidebarView>;
|
|
28
37
|
declare const tuiModule: {
|
|
29
38
|
id: string;
|
|
30
39
|
tui: (api: any) => Promise<void>;
|
package/dist/tui.js
CHANGED
|
@@ -314,6 +314,51 @@ class WorkerClient {
|
|
|
314
314
|
return null;
|
|
315
315
|
}
|
|
316
316
|
}
|
|
317
|
+
static async getRecentSummaries(project, limit) {
|
|
318
|
+
try {
|
|
319
|
+
const params = new URLSearchParams({ project, limit: String(limit) });
|
|
320
|
+
const response = await fetch(`${this.BASE_URL}/api/summaries?${params.toString()}`);
|
|
321
|
+
if (!response.ok) {
|
|
322
|
+
return [];
|
|
323
|
+
}
|
|
324
|
+
const payload = await response.json();
|
|
325
|
+
if (!Array.isArray(payload.items)) {
|
|
326
|
+
return [];
|
|
327
|
+
}
|
|
328
|
+
return payload.items.flatMap((item) => {
|
|
329
|
+
if (!isRecord(item) || typeof item["id"] !== "number") {
|
|
330
|
+
return [];
|
|
331
|
+
}
|
|
332
|
+
const request = typeof item["request"] === "string" ? item["request"].trim() : "";
|
|
333
|
+
return request ? [{ id: item["id"], request }] : [];
|
|
334
|
+
});
|
|
335
|
+
} catch {
|
|
336
|
+
return [];
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
static async getRecentObservations(project, limit) {
|
|
340
|
+
try {
|
|
341
|
+
const params = new URLSearchParams({ project, limit: String(limit) });
|
|
342
|
+
const response = await fetch(`${this.BASE_URL}/api/observations?${params.toString()}`);
|
|
343
|
+
if (!response.ok) {
|
|
344
|
+
return [];
|
|
345
|
+
}
|
|
346
|
+
const payload = await response.json();
|
|
347
|
+
if (!Array.isArray(payload.items)) {
|
|
348
|
+
return [];
|
|
349
|
+
}
|
|
350
|
+
return payload.items.flatMap((item) => {
|
|
351
|
+
if (!isRecord(item) || typeof item["id"] !== "number") {
|
|
352
|
+
return [];
|
|
353
|
+
}
|
|
354
|
+
const title = typeof item["title"] === "string" ? item["title"].trim() : "";
|
|
355
|
+
const type = typeof item["type"] === "string" ? item["type"] : "";
|
|
356
|
+
return title ? [{ id: item["id"], type, title }] : [];
|
|
357
|
+
});
|
|
358
|
+
} catch {
|
|
359
|
+
return [];
|
|
360
|
+
}
|
|
361
|
+
}
|
|
317
362
|
static async search(query, project, limit) {
|
|
318
363
|
try {
|
|
319
364
|
const params = new URLSearchParams({ query, project });
|
|
@@ -338,15 +383,38 @@ class WorkerClient {
|
|
|
338
383
|
|
|
339
384
|
// src/tui.ts
|
|
340
385
|
var POLL_INTERVAL_MS = 5000;
|
|
341
|
-
var LABEL_MAX =
|
|
386
|
+
var LABEL_MAX = 40;
|
|
387
|
+
var RECENT_SUMMARIES = 3;
|
|
388
|
+
var RECENT_OBSERVATIONS = 3;
|
|
389
|
+
var OBSERVATION_TYPE_ICONS = {
|
|
390
|
+
bugfix: "\u25CF",
|
|
391
|
+
feature: "\u25C6",
|
|
392
|
+
refactor: "\u21BB",
|
|
393
|
+
change: "\u2713",
|
|
394
|
+
discovery: "\u25CB",
|
|
395
|
+
decision: "\u2696",
|
|
396
|
+
security_alert: "\u26A0",
|
|
397
|
+
security_note: "\u26B7"
|
|
398
|
+
};
|
|
342
399
|
function box(props, children) {
|
|
343
400
|
return { kind: "box", props, children };
|
|
344
401
|
}
|
|
345
402
|
function text(props, value) {
|
|
346
403
|
return { kind: "text", props, text: value };
|
|
347
404
|
}
|
|
405
|
+
var HTML_ENTITIES = {
|
|
406
|
+
"<": "<",
|
|
407
|
+
">": ">",
|
|
408
|
+
""": '"',
|
|
409
|
+
"'": "'",
|
|
410
|
+
"&": "&"
|
|
411
|
+
};
|
|
412
|
+
function decodeEntities(value) {
|
|
413
|
+
return value.replace(/&(?:lt|gt|quot|#39|amp);/g, (entity) => HTML_ENTITIES[entity] ?? entity);
|
|
414
|
+
}
|
|
348
415
|
function truncate(value) {
|
|
349
|
-
|
|
416
|
+
const decoded = decodeEntities(value);
|
|
417
|
+
return decoded.length <= LABEL_MAX ? decoded : `${decoded.slice(0, LABEL_MAX - 1)}\u2026`;
|
|
350
418
|
}
|
|
351
419
|
function formatCount(value) {
|
|
352
420
|
if (value === null) {
|
|
@@ -354,21 +422,64 @@ function formatCount(value) {
|
|
|
354
422
|
}
|
|
355
423
|
return value >= 1e4 ? `${(value / 1000).toFixed(1)}k` : String(value);
|
|
356
424
|
}
|
|
357
|
-
function
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
425
|
+
function summaryLine(view) {
|
|
426
|
+
if (!view.healthy) {
|
|
427
|
+
return { label: "(offline)", tone: "err" };
|
|
428
|
+
}
|
|
429
|
+
if (view.isProcessing || (view.queueDepth ?? 0) > 0) {
|
|
430
|
+
return { label: `(queue ${view.queueDepth ?? 0})`, tone: "warn" };
|
|
431
|
+
}
|
|
432
|
+
return { label: `(online, ${formatCount(view.observations)} obs)`, tone: "ok" };
|
|
433
|
+
}
|
|
434
|
+
function toneColor(tone, theme) {
|
|
435
|
+
if (tone === "err") {
|
|
436
|
+
return theme.error;
|
|
437
|
+
}
|
|
438
|
+
return tone === "warn" ? theme.warning : theme.textMuted;
|
|
439
|
+
}
|
|
440
|
+
function bulletRow(icon, iconColor, label, theme) {
|
|
441
|
+
return box({ flexDirection: "row", gap: 1 }, [
|
|
442
|
+
text({ fg: iconColor, flexShrink: 0 }, icon),
|
|
443
|
+
text({ fg: theme.text, wrapMode: "none" }, truncate(label))
|
|
444
|
+
]);
|
|
445
|
+
}
|
|
446
|
+
function expandedRows(view, theme) {
|
|
447
|
+
if (!view.healthy) {
|
|
448
|
+
return [bulletRow("\u2022", theme.error, "worker offline", theme)];
|
|
449
|
+
}
|
|
450
|
+
const rows = [
|
|
451
|
+
bulletRow("\u2022", theme.success, `obs ${formatCount(view.observations)} \xB7 sum ${formatCount(view.summaries)} \xB7 ses ${formatCount(view.sessions)}`, theme)
|
|
452
|
+
];
|
|
453
|
+
if (view.isProcessing || (view.queueDepth ?? 0) > 0) {
|
|
454
|
+
rows.push(bulletRow("\u2022", theme.warning, `processing \xB7 queue ${view.queueDepth ?? 0}`, theme));
|
|
455
|
+
}
|
|
456
|
+
if (view.recentSummaries.length > 0) {
|
|
457
|
+
rows.push(text({ fg: theme.textMuted }, "Recent sessions"));
|
|
458
|
+
for (const summary of view.recentSummaries) {
|
|
459
|
+
rows.push(bulletRow("\u2022", theme.textMuted, summary.request, theme));
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (view.recentObservations.length > 0) {
|
|
463
|
+
rows.push(text({ fg: theme.textMuted }, "Latest"));
|
|
464
|
+
for (const observation of view.recentObservations) {
|
|
465
|
+
const icon = OBSERVATION_TYPE_ICONS[observation.type] ?? "\u25CB";
|
|
466
|
+
rows.push(bulletRow(icon, theme.info, observation.title, theme));
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return rows;
|
|
470
|
+
}
|
|
471
|
+
function buildMemNodes(view, theme, collapsed, onToggle) {
|
|
472
|
+
const summary = summaryLine(view);
|
|
473
|
+
const header = box({ flexDirection: "row", gap: 1, onMouseDown: onToggle }, [
|
|
474
|
+
text({ fg: theme.text }, collapsed ? "\u25B6" : "\u25BC"),
|
|
475
|
+
text({ fg: theme.info }, "Memory"),
|
|
476
|
+
...collapsed ? [text({ fg: toneColor(summary.tone, theme) }, summary.label)] : []
|
|
477
|
+
]);
|
|
365
478
|
return [
|
|
366
|
-
box({
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
padding: 1
|
|
371
|
-
}, [text({ fg: theme.info }, "Memory"), ...rows])
|
|
479
|
+
box({ flexDirection: "column", paddingTop: 1 }, [
|
|
480
|
+
header,
|
|
481
|
+
...collapsed ? [] : [box({ flexDirection: "column", paddingLeft: 2 }, expandedRows(view, theme))]
|
|
482
|
+
])
|
|
372
483
|
];
|
|
373
484
|
}
|
|
374
485
|
function viewKey(view) {
|
|
@@ -377,7 +488,7 @@ function viewKey(view) {
|
|
|
377
488
|
function readCount(value) {
|
|
378
489
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
379
490
|
}
|
|
380
|
-
async function readMemView(project) {
|
|
491
|
+
async function readMemView(project, collapsed) {
|
|
381
492
|
const stats = await WorkerClient.getStats();
|
|
382
493
|
if (!stats) {
|
|
383
494
|
return {
|
|
@@ -387,9 +498,15 @@ async function readMemView(project) {
|
|
|
387
498
|
sessions: null,
|
|
388
499
|
summaries: null,
|
|
389
500
|
queueDepth: null,
|
|
390
|
-
isProcessing: false
|
|
501
|
+
isProcessing: false,
|
|
502
|
+
recentSummaries: [],
|
|
503
|
+
recentObservations: []
|
|
391
504
|
};
|
|
392
505
|
}
|
|
506
|
+
const [recentSummaries, recentObservations] = collapsed ? [[], []] : await Promise.all([
|
|
507
|
+
WorkerClient.getRecentSummaries(project, RECENT_SUMMARIES),
|
|
508
|
+
WorkerClient.getRecentObservations(project, RECENT_OBSERVATIONS)
|
|
509
|
+
]);
|
|
393
510
|
return {
|
|
394
511
|
healthy: true,
|
|
395
512
|
project,
|
|
@@ -397,7 +514,9 @@ async function readMemView(project) {
|
|
|
397
514
|
sessions: readCount(stats.database?.sessions),
|
|
398
515
|
summaries: readCount(stats.database?.summaries),
|
|
399
516
|
queueDepth: readCount(stats.processing?.queueDepth),
|
|
400
|
-
isProcessing: stats.processing?.isProcessing === true
|
|
517
|
+
isProcessing: stats.processing?.isProcessing === true,
|
|
518
|
+
recentSummaries,
|
|
519
|
+
recentObservations
|
|
401
520
|
};
|
|
402
521
|
}
|
|
403
522
|
function materializeNode(node, solid) {
|
|
@@ -432,15 +551,30 @@ var tuiModule = {
|
|
|
432
551
|
return;
|
|
433
552
|
}
|
|
434
553
|
const project = projectNameFromDirectory(String(api.state?.path?.directory ?? ""));
|
|
435
|
-
let
|
|
554
|
+
let collapsed = true;
|
|
555
|
+
let currentView = await readMemView(project, collapsed);
|
|
436
556
|
let currentKey = viewKey(currentView);
|
|
437
557
|
let disposed = false;
|
|
438
558
|
let inFlight = false;
|
|
439
559
|
let timer = null;
|
|
560
|
+
const refresh = async () => {
|
|
561
|
+
const nextView = await readMemView(project, collapsed);
|
|
562
|
+
const nextKey = viewKey(nextView);
|
|
563
|
+
if (nextKey !== currentKey) {
|
|
564
|
+
currentView = nextView;
|
|
565
|
+
currentKey = nextKey;
|
|
566
|
+
api.renderer.requestRender();
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
const onToggle = () => {
|
|
570
|
+
collapsed = !collapsed;
|
|
571
|
+
api.renderer.requestRender();
|
|
572
|
+
refresh().catch(() => {});
|
|
573
|
+
};
|
|
440
574
|
api.slots.register({
|
|
441
575
|
order: 910,
|
|
442
576
|
slots: {
|
|
443
|
-
sidebar_content: () => materialize(buildMemNodes(currentView, api.theme.current), solid)
|
|
577
|
+
sidebar_content: () => materialize(buildMemNodes(currentView, api.theme.current, collapsed, onToggle), solid)
|
|
444
578
|
}
|
|
445
579
|
});
|
|
446
580
|
api.renderer.requestRender();
|
|
@@ -456,13 +590,7 @@ var tuiModule = {
|
|
|
456
590
|
}
|
|
457
591
|
inFlight = true;
|
|
458
592
|
try {
|
|
459
|
-
|
|
460
|
-
const nextKey = viewKey(nextView);
|
|
461
|
-
if (nextKey !== currentKey) {
|
|
462
|
-
currentView = nextView;
|
|
463
|
-
currentKey = nextKey;
|
|
464
|
-
api.renderer.requestRender();
|
|
465
|
-
}
|
|
593
|
+
await refresh();
|
|
466
594
|
} catch {} finally {
|
|
467
595
|
inFlight = false;
|
|
468
596
|
if (!disposed) {
|
package/dist/worker-client.d.ts
CHANGED
|
@@ -95,5 +95,20 @@ export declare class WorkerClient {
|
|
|
95
95
|
queueDepth?: number;
|
|
96
96
|
};
|
|
97
97
|
} | null>;
|
|
98
|
+
/**
|
|
99
|
+
* Recent session summaries for the sidebar (GET /api/summaries).
|
|
100
|
+
*/
|
|
101
|
+
static getRecentSummaries(project: string, limit: number): Promise<{
|
|
102
|
+
id: number;
|
|
103
|
+
request: string;
|
|
104
|
+
}[]>;
|
|
105
|
+
/**
|
|
106
|
+
* Recent observations for the sidebar (GET /api/observations).
|
|
107
|
+
*/
|
|
108
|
+
static getRecentObservations(project: string, limit: number): Promise<{
|
|
109
|
+
id: number;
|
|
110
|
+
type: string;
|
|
111
|
+
title: string;
|
|
112
|
+
}[]>;
|
|
98
113
|
static search(query: string, project: string, limit?: number): Promise<string>;
|
|
99
114
|
}
|