pi-chalin 0.1.0 → 0.2.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/README.md +47 -4
- package/package.json +1 -1
- package/src/autoroute.ts +2 -2
- package/src/child-tools.ts +5 -4
- package/src/commands.ts +74 -7
- package/src/config.ts +58 -0
- package/src/index.ts +2 -0
- package/src/kernel.ts +7 -11
- package/src/memory-provider.ts +701 -0
- package/src/memory.ts +18 -1
- package/src/runner.ts +3 -2
- package/src/schemas.ts +1 -0
- package/src/tools.ts +7 -6
- package/src/ui.ts +355 -2
package/src/memory.ts
CHANGED
|
@@ -38,6 +38,19 @@ export interface MemoryRevisionInput {
|
|
|
38
38
|
reason?: string;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
export interface MemoryStoreLike {
|
|
42
|
+
submitCandidates(candidates: MemoryCandidate[]): Promise<MemoryRecord[]>;
|
|
43
|
+
list(status?: MemoryRecord["status"]): Promise<MemoryRecord[]>;
|
|
44
|
+
pendingCount(): Promise<number>;
|
|
45
|
+
approve(id: string): Promise<MemoryRecord | undefined>;
|
|
46
|
+
reject(id: string): Promise<MemoryRecord | undefined>;
|
|
47
|
+
delete(id: string): Promise<boolean>;
|
|
48
|
+
search(query: string, limit?: number): Promise<MemorySearchResult[]>;
|
|
49
|
+
retrieve(request: MemoryContextRequest): Promise<MemoryContextBundle>;
|
|
50
|
+
revise(id: string, input: MemoryRevisionInput): Promise<MemoryRecord | undefined>;
|
|
51
|
+
events(recordId?: string): Promise<MemoryAuditEvent[]>;
|
|
52
|
+
}
|
|
53
|
+
|
|
41
54
|
type SqlJsStatic = any;
|
|
42
55
|
type SqlJsDatabase = any;
|
|
43
56
|
|
|
@@ -52,7 +65,7 @@ export class MemoryStore {
|
|
|
52
65
|
|
|
53
66
|
async submitCandidates(candidates: MemoryCandidate[]): Promise<MemoryRecord[]> {
|
|
54
67
|
const now = new Date().toISOString();
|
|
55
|
-
const records =
|
|
68
|
+
const records = prepareMemoryRecords(candidates, now);
|
|
56
69
|
|
|
57
70
|
await this.withDb(true, (db) => {
|
|
58
71
|
const existingRecords = selectRows(db, "SELECT * FROM memory_records ORDER BY createdAt DESC")
|
|
@@ -317,6 +330,10 @@ export function createMemoryCandidate(input: Omit<MemoryCandidate, "id" | "creat
|
|
|
317
330
|
};
|
|
318
331
|
}
|
|
319
332
|
|
|
333
|
+
export function prepareMemoryRecords(candidates: MemoryCandidate[], now = new Date().toISOString()): MemoryRecord[] {
|
|
334
|
+
return dedupeCandidates(candidates).map((candidate) => buildMemoryRecord(candidate, now));
|
|
335
|
+
}
|
|
336
|
+
|
|
320
337
|
async function getSqlModule(): Promise<SqlJsStatic> {
|
|
321
338
|
sqlModulePromise ??= initSqlJs();
|
|
322
339
|
return sqlModulePromise;
|
package/src/runner.ts
CHANGED
|
@@ -2,7 +2,8 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import type { AgentDefinition, AgentThinkingLevel } from "./schemas.ts";
|
|
3
3
|
import { evaluateBudgetUsage, policyForStep, recordBudgetCheckpoint, summarizeToolUtility } from "./budget.ts";
|
|
4
4
|
import type { ChalinPathsOptions } from "./paths.ts";
|
|
5
|
-
import { createMemoryCandidate
|
|
5
|
+
import { createMemoryCandidate } from "./memory.ts";
|
|
6
|
+
import { createConfiguredMemoryStore } from "./memory-provider.ts";
|
|
6
7
|
import type { AgentOutput, AgentStep, MemoryCandidate, RouteDecision, RoutePlan, RunState, RunStepMetrics, RunStepState, TokenUsageSummary } from "./schemas.ts";
|
|
7
8
|
import { createChildToolPolicy, createChildTools, type ChildToolActivity, type ChildToolPolicy } from "./child-tools.ts";
|
|
8
9
|
import { createChalinChildSessionManager } from "./child-sessions.ts";
|
|
@@ -544,7 +545,7 @@ function buildPromptOptionsForStep(run: RunState, step: RunStepState, agent: Age
|
|
|
544
545
|
async function compactMemoryContextForStep(cwd: string, step: RunStepState, agent: AgentDefinition | undefined, previous?: string): Promise<string | undefined> {
|
|
545
546
|
if (!agent?.memory.read || !agent.capabilities.includes("memory-read")) return undefined;
|
|
546
547
|
const query = [step.task, previous ? `Previous handoff: ${previous.slice(0, 700)}` : ""].filter(Boolean).join("\n");
|
|
547
|
-
const bundle = await
|
|
548
|
+
const bundle = await createConfiguredMemoryStore({ cwd }).retrieve({
|
|
548
549
|
query,
|
|
549
550
|
sourceAgent: step.agent,
|
|
550
551
|
agentConcern: agent.concern,
|
package/src/schemas.ts
CHANGED
package/src/tools.ts
CHANGED
|
@@ -6,7 +6,8 @@ import { AgentCatalog } from "./agents.ts";
|
|
|
6
6
|
import { ArtifactStore } from "./artifacts.ts";
|
|
7
7
|
import { loadEffectiveConfig } from "./config.ts";
|
|
8
8
|
import { ChalinKernel, routeFromPlan } from "./kernel.ts";
|
|
9
|
-
import { createMemoryCandidate
|
|
9
|
+
import { createMemoryCandidate } from "./memory.ts";
|
|
10
|
+
import { createConfiguredMemoryStore } from "./memory-provider.ts";
|
|
10
11
|
import { formatInterviewResult, runChalinInterview, type InterviewRequestInput } from "./interview.ts";
|
|
11
12
|
import { loadResumableRunState } from "./runner-state.ts";
|
|
12
13
|
import { beginChalinRouteInvocation, finishChalinRouteInvocation, setLatestRun } from "./runtime-state.ts";
|
|
@@ -218,7 +219,7 @@ export function registerChalinTools(pi: ExtensionAPI): void {
|
|
|
218
219
|
async execute(_toolCallId, params: ChalinRouteToolParams, signal, onUpdate, ctx) {
|
|
219
220
|
const loaded = loadEffectiveConfig({ cwd: ctx.cwd });
|
|
220
221
|
const catalog = AgentCatalog.load({ cwd: ctx.cwd });
|
|
221
|
-
const memory =
|
|
222
|
+
const memory = createConfiguredMemoryStore({ cwd: ctx.cwd }, loaded.config);
|
|
222
223
|
const kernel = new ChalinKernel({
|
|
223
224
|
cwd: ctx.cwd,
|
|
224
225
|
config: loaded.config,
|
|
@@ -349,7 +350,7 @@ export function registerChalinTools(pi: ExtensionAPI): void {
|
|
|
349
350
|
const run = loadResumableRunState({ cwd: ctx.cwd, runId: params.runId });
|
|
350
351
|
if (!run) return textResult(params.runId ? `No resumable pi-chalin run found for '${params.runId}'.` : "No paused or stale pi-chalin run found to resume.", { runId: params.runId });
|
|
351
352
|
const catalog = AgentCatalog.load({ cwd: ctx.cwd });
|
|
352
|
-
const memory =
|
|
353
|
+
const memory = createConfiguredMemoryStore({ cwd: ctx.cwd }, loaded.config);
|
|
353
354
|
const kernel = new ChalinKernel({
|
|
354
355
|
cwd: ctx.cwd,
|
|
355
356
|
config: loaded.config,
|
|
@@ -411,7 +412,7 @@ export function registerChalinTools(pi: ExtensionAPI): void {
|
|
|
411
412
|
],
|
|
412
413
|
parameters: ChalinMemorySearchParams,
|
|
413
414
|
async execute(_toolCallId, params: ChalinMemorySearchToolParams, _signal, _onUpdate, ctx) {
|
|
414
|
-
const memory =
|
|
415
|
+
const memory = createConfiguredMemoryStore({ cwd: ctx.cwd });
|
|
415
416
|
const bundle = await memory.retrieve({
|
|
416
417
|
query: params.query,
|
|
417
418
|
sourceAgent: "primary-pi",
|
|
@@ -437,7 +438,7 @@ export function registerChalinTools(pi: ExtensionAPI): void {
|
|
|
437
438
|
async execute(_toolCallId, params: ChalinMemoryWriteToolParams, _signal, _onUpdate, ctx) {
|
|
438
439
|
const content = params.content.trim();
|
|
439
440
|
if (content.length < 24) return textResult("memory rejected: content is too short to be durable.", { status: "rejected" });
|
|
440
|
-
const memory =
|
|
441
|
+
const memory = createConfiguredMemoryStore({ cwd: ctx.cwd });
|
|
441
442
|
const [record] = await memory.submitCandidates([createMemoryCandidate({
|
|
442
443
|
category: params.category,
|
|
443
444
|
content,
|
|
@@ -466,7 +467,7 @@ export function registerChalinTools(pi: ExtensionAPI): void {
|
|
|
466
467
|
async execute(_toolCallId, params: ChalinMemoryReviseToolParams, _signal, _onUpdate, ctx) {
|
|
467
468
|
const content = params.content.trim();
|
|
468
469
|
if (content.length < 24) return textResult("memory revision rejected: content is too short to be durable.", { status: "rejected" });
|
|
469
|
-
const memory =
|
|
470
|
+
const memory = createConfiguredMemoryStore({ cwd: ctx.cwd });
|
|
470
471
|
const record = await memory.revise(params.id, {
|
|
471
472
|
content,
|
|
472
473
|
category: params.category,
|
package/src/ui.ts
CHANGED
|
@@ -46,6 +46,7 @@ export function summarizeChalinHome(state: ChalinRuntimeState, agentCount: numbe
|
|
|
46
46
|
`agents: ${agentCount}`,
|
|
47
47
|
`activity: ${summarizeActivity(state)}`,
|
|
48
48
|
`guards: ${summarizeGuardHealth(state.lastRun)}`,
|
|
49
|
+
`memory: ${state.memoryBackend ?? "pi-chalin local"}`,
|
|
49
50
|
`memory candidates: ${state.pendingMemoryCandidates}`,
|
|
50
51
|
`approvals: ${state.pendingApprovals}`,
|
|
51
52
|
];
|
|
@@ -63,6 +64,7 @@ export async function openSmartPanel(
|
|
|
63
64
|
onSelectMemory(): Promise<void>;
|
|
64
65
|
onSelectArtifacts?(): Promise<void>;
|
|
65
66
|
onSelectWebFetch?(): Promise<void>;
|
|
67
|
+
onSelectSettings?(): Promise<void>;
|
|
66
68
|
},
|
|
67
69
|
): Promise<void> {
|
|
68
70
|
const lines = summarizeChalinHome(options.state, options.agents.length);
|
|
@@ -85,6 +87,7 @@ export async function openSmartPanel(
|
|
|
85
87
|
options.pendingMemories.length > 0 ? `Memory · ${options.pendingMemories.length} pending` : "Memory",
|
|
86
88
|
...(options.onSelectArtifacts ? ["Artifacts"] : []),
|
|
87
89
|
...(options.onSelectWebFetch ? ["WebFetch"] : []),
|
|
90
|
+
...(options.onSelectSettings ? ["Settings"] : []),
|
|
88
91
|
"Status",
|
|
89
92
|
...(options.diagnostics.length > 0 ? ["Diagnostics"] : []),
|
|
90
93
|
"Close",
|
|
@@ -95,6 +98,7 @@ export async function openSmartPanel(
|
|
|
95
98
|
if (selected?.startsWith("Memory")) return options.onSelectMemory();
|
|
96
99
|
if (selected === "Artifacts") return options.onSelectArtifacts?.();
|
|
97
100
|
if (selected === "WebFetch") return options.onSelectWebFetch?.();
|
|
101
|
+
if (selected === "Settings") return options.onSelectSettings?.();
|
|
98
102
|
if (selected === "Diagnostics") return void ctx.ui.notify(options.diagnostics.join("\n"), "warning");
|
|
99
103
|
if (selected === "Status") ctx.ui.notify(lines.join("\n"), "info");
|
|
100
104
|
}
|
|
@@ -258,9 +262,10 @@ export async function openMemoryReview(
|
|
|
258
262
|
ctx: ExtensionContext,
|
|
259
263
|
memories: MemoryRecord[],
|
|
260
264
|
actions: { approve(id: string): void; reject(id: string): void; delete(id: string): void },
|
|
265
|
+
options: { title?: string; emptyMessage?: string } = {},
|
|
261
266
|
): Promise<void> {
|
|
262
267
|
if (memories.length === 0) {
|
|
263
|
-
ctx.ui.notify("No memory records found.", "info");
|
|
268
|
+
ctx.ui.notify(options.emptyMessage ?? "No memory records found.", "info");
|
|
264
269
|
return;
|
|
265
270
|
}
|
|
266
271
|
const sorted = [...memories].sort((a, b) => memoryStatusRank(a.status) - memoryStatusRank(b.status) || b.createdAt.localeCompare(a.createdAt));
|
|
@@ -269,7 +274,22 @@ export async function openMemoryReview(
|
|
|
269
274
|
ctx.ui.notify(sorted.map(format).join("\n"), "info");
|
|
270
275
|
return;
|
|
271
276
|
}
|
|
272
|
-
|
|
277
|
+
if (typeof ctx.ui.custom === "function") {
|
|
278
|
+
const result = await openMemoryReviewOverlay(ctx, sorted, options.title ?? "pi-chalin Memory");
|
|
279
|
+
if (!result) return;
|
|
280
|
+
const record = sorted.find((candidate) => candidate.id === result.id);
|
|
281
|
+
if (!record) return;
|
|
282
|
+
if (result.action === "details") {
|
|
283
|
+
ctx.ui.notify(formatMemoryDetail(record), "info");
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (result.action === "approve") actions.approve(record.id);
|
|
287
|
+
if (result.action === "reject") actions.reject(record.id);
|
|
288
|
+
if (result.action === "delete") actions.delete(record.id);
|
|
289
|
+
ctx.ui.notify(`Memory ${memoryActionPast(result.action)}: ${memoryTitle(record)}`, "info");
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const selected = await ctx.ui.select(options.title ?? "pi-chalin Memory", [...sorted.map(format), "Close"]);
|
|
273
293
|
if (!selected || selected === "Close") return;
|
|
274
294
|
const record = sorted.find((candidate) => selected === format(candidate));
|
|
275
295
|
if (!record) return;
|
|
@@ -290,6 +310,339 @@ export async function openMemoryReview(
|
|
|
290
310
|
if (action && action !== "Close") ctx.ui.notify(`Memory ${action.toLowerCase().replace(/\s+.*/, "")}d: ${memoryTitle(record)}`, "info");
|
|
291
311
|
}
|
|
292
312
|
|
|
313
|
+
type MemoryOverlayAction = "details" | "approve" | "reject" | "delete";
|
|
314
|
+
type MemoryOverlayResult = { action: MemoryOverlayAction; id: string } | undefined;
|
|
315
|
+
|
|
316
|
+
async function openMemoryReviewOverlay(ctx: ExtensionContext, memories: MemoryRecord[], title: string): Promise<MemoryOverlayResult> {
|
|
317
|
+
return ctx.ui.custom<MemoryOverlayResult>(
|
|
318
|
+
(tui, theme, _keybindings, done) => new MemoryReviewOverlay(tui, theme, title, memories, done),
|
|
319
|
+
{
|
|
320
|
+
overlay: true,
|
|
321
|
+
overlayOptions: {
|
|
322
|
+
anchor: "center",
|
|
323
|
+
width: "94%",
|
|
324
|
+
maxHeight: "88%",
|
|
325
|
+
margin: 1,
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
class MemoryReviewOverlay implements Component, Focusable {
|
|
332
|
+
focused = false;
|
|
333
|
+
private query = "";
|
|
334
|
+
private filterIndex = 0;
|
|
335
|
+
private selectedIndex = 0;
|
|
336
|
+
private readonly filters: Array<MemoryRecord["status"] | "all">;
|
|
337
|
+
|
|
338
|
+
constructor(
|
|
339
|
+
private readonly tui: TUI,
|
|
340
|
+
private readonly theme: Theme,
|
|
341
|
+
private readonly title: string,
|
|
342
|
+
private readonly memories: MemoryRecord[],
|
|
343
|
+
private readonly done: (result: MemoryOverlayResult) => void,
|
|
344
|
+
) {
|
|
345
|
+
const statuses = Array.from(new Set(memories.map((record) => record.status)));
|
|
346
|
+
this.filters = ["all", ...(["pending", "active", "quarantined", "stale", "superseded", "rejected"] as const).filter((status) => statuses.includes(status))];
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
handleInput(data: string): void {
|
|
350
|
+
if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
|
|
351
|
+
this.done(undefined);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (matchesKey(data, "tab") || matchesKey(data, "right")) {
|
|
355
|
+
this.filterIndex = (this.filterIndex + 1) % this.filters.length;
|
|
356
|
+
this.selectedIndex = 0;
|
|
357
|
+
this.tui.requestRender();
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (matchesKey(data, "left")) {
|
|
361
|
+
this.filterIndex = (this.filterIndex - 1 + this.filters.length) % this.filters.length;
|
|
362
|
+
this.selectedIndex = 0;
|
|
363
|
+
this.tui.requestRender();
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (matchesKey(data, "up")) {
|
|
367
|
+
this.moveSelection(-1);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (matchesKey(data, "down")) {
|
|
371
|
+
this.moveSelection(1);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (matchesKey(data, "pageUp")) {
|
|
375
|
+
this.moveSelection(-MEMORY_OVERLAY_VISIBLE_ROWS);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (matchesKey(data, "pageDown")) {
|
|
379
|
+
this.moveSelection(MEMORY_OVERLAY_VISIBLE_ROWS);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (matchesKey(data, "home")) {
|
|
383
|
+
this.selectedIndex = 0;
|
|
384
|
+
this.tui.requestRender();
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (matchesKey(data, "end")) {
|
|
388
|
+
this.selectedIndex = Math.max(0, this.filteredMemories().length - 1);
|
|
389
|
+
this.tui.requestRender();
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (matchesKey(data, "enter")) {
|
|
393
|
+
this.finish("details");
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (data === "A" || (data === "a" && this.query.length === 0)) {
|
|
397
|
+
this.finish("approve");
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (data === "R" || (data === "r" && this.query.length === 0)) {
|
|
401
|
+
this.finish("reject");
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (data === "D" || (data === "d" && this.query.length === 0)) {
|
|
405
|
+
this.finish("delete");
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (isBackspace(data)) {
|
|
409
|
+
this.query = this.query.slice(0, -1);
|
|
410
|
+
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredMemories().length - 1));
|
|
411
|
+
this.tui.requestRender();
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (matchesKey(data, "ctrl+u")) {
|
|
415
|
+
this.query = "";
|
|
416
|
+
this.selectedIndex = 0;
|
|
417
|
+
this.tui.requestRender();
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (isPrintableInput(data)) {
|
|
421
|
+
this.query += data;
|
|
422
|
+
this.selectedIndex = 0;
|
|
423
|
+
this.tui.requestRender();
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
render(width: number): string[] {
|
|
428
|
+
const overlayWidth = Math.max(1, width);
|
|
429
|
+
const innerWidth = Math.max(1, overlayWidth - 2);
|
|
430
|
+
const filtered = this.filteredMemories();
|
|
431
|
+
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, filtered.length - 1));
|
|
432
|
+
const selected = filtered[this.selectedIndex];
|
|
433
|
+
const border = (text: string) => this.theme.fg("border", text);
|
|
434
|
+
const row = (content = "") => `${border("|")}${padAnsi(content, innerWidth)}${border("|")}`;
|
|
435
|
+
const filter = this.filters[this.filterIndex] ?? "all";
|
|
436
|
+
const header = ` ${this.theme.fg("accent", this.theme.bold(this.title))} ${this.theme.fg("dim", `${filtered.length}/${this.memories.length} records · ${memoryFilterLabel(filter)}`)}`;
|
|
437
|
+
const search = this.query ? `search: ${this.query}` : "type to search";
|
|
438
|
+
const body = innerWidth >= 86
|
|
439
|
+
? renderMemoryOverlayWide(filtered, selected, this.selectedIndex, this.theme, innerWidth)
|
|
440
|
+
: renderMemoryOverlayNarrow(filtered, selected, this.selectedIndex, this.theme, innerWidth);
|
|
441
|
+
const lines = [
|
|
442
|
+
border(`+${"-".repeat(innerWidth)}+`),
|
|
443
|
+
row(header),
|
|
444
|
+
row(` ${renderMemoryFilterTabs(this.filters, filter, this.theme, Math.max(20, innerWidth - 2))}`),
|
|
445
|
+
row(` ${this.theme.fg("dim", `${search} · tab filter · up/down navigate · enter details · A/R/D action · esc close`)}`),
|
|
446
|
+
row(""),
|
|
447
|
+
...body.map((line) => row(line)),
|
|
448
|
+
border(`+${"-".repeat(innerWidth)}+`),
|
|
449
|
+
];
|
|
450
|
+
return clampRenderedLines(lines, overlayWidth);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
invalidate(): void {}
|
|
454
|
+
|
|
455
|
+
private moveSelection(delta: number): void {
|
|
456
|
+
const max = Math.max(0, this.filteredMemories().length - 1);
|
|
457
|
+
this.selectedIndex = Math.max(0, Math.min(max, this.selectedIndex + delta));
|
|
458
|
+
this.tui.requestRender();
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private finish(action: MemoryOverlayAction): void {
|
|
462
|
+
const record = this.filteredMemories()[this.selectedIndex];
|
|
463
|
+
if (!record) return;
|
|
464
|
+
if ((action === "approve" || action === "reject") && record.status !== "pending") {
|
|
465
|
+
this.tui.requestRender();
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
this.done({ action, id: record.id });
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
private filteredMemories(): MemoryRecord[] {
|
|
472
|
+
const filter = this.filters[this.filterIndex] ?? "all";
|
|
473
|
+
const terms = normalizeSearch(this.query).split(" ").filter(Boolean);
|
|
474
|
+
return this.memories.filter((record) => {
|
|
475
|
+
if (filter !== "all" && record.status !== filter) return false;
|
|
476
|
+
if (terms.length === 0) return true;
|
|
477
|
+
const haystack = normalizeSearch(memorySearchText(record));
|
|
478
|
+
return terms.every((term) => haystack.includes(term));
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const MEMORY_OVERLAY_VISIBLE_ROWS = 18;
|
|
484
|
+
|
|
485
|
+
function renderMemoryOverlayWide(
|
|
486
|
+
memories: MemoryRecord[],
|
|
487
|
+
selected: MemoryRecord | undefined,
|
|
488
|
+
selectedIndex: number,
|
|
489
|
+
theme: Theme,
|
|
490
|
+
width: number,
|
|
491
|
+
): string[] {
|
|
492
|
+
const listWidth = Math.max(36, Math.min(62, Math.floor(width * 0.48)));
|
|
493
|
+
const detailWidth = Math.max(24, width - listWidth - 3);
|
|
494
|
+
const list = renderMemoryListWindow(memories, selectedIndex, theme, listWidth);
|
|
495
|
+
const detail = renderMemoryDetailPane(selected, theme, detailWidth, MEMORY_OVERLAY_VISIBLE_ROWS);
|
|
496
|
+
const rows = Math.max(MEMORY_OVERLAY_VISIBLE_ROWS, list.length, detail.length);
|
|
497
|
+
const lines: string[] = [];
|
|
498
|
+
for (let index = 0; index < rows; index += 1) {
|
|
499
|
+
lines.push(`${padAnsi(list[index] ?? "", listWidth)} ${theme.fg("border", "|")} ${padAnsi(detail[index] ?? "", detailWidth)}`);
|
|
500
|
+
}
|
|
501
|
+
return lines;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function renderMemoryOverlayNarrow(
|
|
505
|
+
memories: MemoryRecord[],
|
|
506
|
+
selected: MemoryRecord | undefined,
|
|
507
|
+
selectedIndex: number,
|
|
508
|
+
theme: Theme,
|
|
509
|
+
width: number,
|
|
510
|
+
): string[] {
|
|
511
|
+
return [
|
|
512
|
+
...renderMemoryListWindow(memories, selectedIndex, theme, width),
|
|
513
|
+
"",
|
|
514
|
+
...renderMemoryDetailPane(selected, theme, width, 8),
|
|
515
|
+
];
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function renderMemoryListWindow(memories: MemoryRecord[], selectedIndex: number, theme: Theme, width: number): string[] {
|
|
519
|
+
if (memories.length === 0) return [theme.fg("muted", "No matching memories.")];
|
|
520
|
+
const startIndex = Math.max(
|
|
521
|
+
0,
|
|
522
|
+
Math.min(selectedIndex - Math.floor(MEMORY_OVERLAY_VISIBLE_ROWS / 2), memories.length - MEMORY_OVERLAY_VISIBLE_ROWS),
|
|
523
|
+
);
|
|
524
|
+
const endIndex = Math.min(startIndex + MEMORY_OVERLAY_VISIBLE_ROWS, memories.length);
|
|
525
|
+
const lines: string[] = [];
|
|
526
|
+
for (let index = startIndex; index < endIndex; index += 1) {
|
|
527
|
+
const record = memories[index];
|
|
528
|
+
if (!record) continue;
|
|
529
|
+
const active = index === selectedIndex;
|
|
530
|
+
const line = formatMemoryOverlayRow(record, width - 2);
|
|
531
|
+
const prefix = active ? theme.fg("accent", "> ") : " ";
|
|
532
|
+
const colored = active ? theme.bg("selectedBg", theme.fg("text", line)) : colorMemoryRow(theme, record, line);
|
|
533
|
+
lines.push(prefix + colored);
|
|
534
|
+
}
|
|
535
|
+
if (memories.length > MEMORY_OVERLAY_VISIBLE_ROWS) {
|
|
536
|
+
lines.push(theme.fg("dim", ` ${selectedIndex + 1}/${memories.length}`));
|
|
537
|
+
}
|
|
538
|
+
return lines;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function renderMemoryDetailPane(record: MemoryRecord | undefined, theme: Theme, width: number, maxLines: number): string[] {
|
|
542
|
+
if (!record) return [theme.fg("muted", "Select a memory to inspect details.")];
|
|
543
|
+
const title = stripMemoryPrefix(record.content) || record.category;
|
|
544
|
+
const metadata = [
|
|
545
|
+
record.status,
|
|
546
|
+
record.category,
|
|
547
|
+
record.scope,
|
|
548
|
+
record.sourceAgent,
|
|
549
|
+
record.topicKey,
|
|
550
|
+
].filter(Boolean).join(" · ");
|
|
551
|
+
const contentLines = wrapPlainText(record.content, Math.max(12, width)).slice(0, Math.max(2, maxLines - 7));
|
|
552
|
+
return [
|
|
553
|
+
theme.fg("accent", theme.bold(truncateToWidth(title, width, "...", false))),
|
|
554
|
+
theme.fg("dim", truncateToWidth(metadata, width, "...", false)),
|
|
555
|
+
"",
|
|
556
|
+
...contentLines.map((line) => theme.fg("text", line)),
|
|
557
|
+
"",
|
|
558
|
+
theme.fg("dim", `confidence ${Math.round(record.confidence * 100)}% · importance ${record.importance}`),
|
|
559
|
+
theme.fg("dim", `seen ${record.duplicateCount} · used ${record.useCount ?? 0} · revisions ${record.revisionCount}`),
|
|
560
|
+
record.evidence ? theme.fg("muted", truncateToWidth(`evidence: ${record.evidence}`, width, "...", false)) : "",
|
|
561
|
+
].filter((line) => line !== "");
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function formatMemoryOverlayRow(record: MemoryRecord, width: number): string {
|
|
565
|
+
const status = `${memoryStatusIcon(record.status)} ${record.status}`.padEnd(13);
|
|
566
|
+
const category = truncateUi(record.category, 14).padEnd(14);
|
|
567
|
+
const source = truncateUi(record.sourceAgent, 15).padEnd(15);
|
|
568
|
+
const titleWidth = Math.max(16, width - visibleWidth(status) - visibleWidth(category) - visibleWidth(source) - 6);
|
|
569
|
+
return truncateToWidth(`${status} ${category} ${source} ${truncateUi(stripMemoryPrefix(record.content), titleWidth)}`, width, "...", false);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function renderMemoryFilterTabs(filters: Array<MemoryRecord["status"] | "all">, selected: MemoryRecord["status"] | "all", theme: Theme, width: number): string {
|
|
573
|
+
const text = filters.map((filter) => {
|
|
574
|
+
const label = ` ${memoryFilterLabel(filter)} `;
|
|
575
|
+
return filter === selected ? theme.bg("selectedBg", theme.fg("text", label)) : theme.fg("dim", label);
|
|
576
|
+
}).join(" ");
|
|
577
|
+
return truncateToWidth(text, width, "...", true);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function memoryFilterLabel(filter: MemoryRecord["status"] | "all"): string {
|
|
581
|
+
return filter === "all" ? "all" : filter;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function colorMemoryRow(theme: Theme, record: MemoryRecord, line: string): string {
|
|
585
|
+
if (record.status === "pending") return theme.fg("accent", line);
|
|
586
|
+
if (record.status === "active") return theme.fg("text", line);
|
|
587
|
+
if (record.status === "rejected") return theme.fg("muted", line);
|
|
588
|
+
return theme.fg("dim", line);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function memorySearchText(record: MemoryRecord): string {
|
|
592
|
+
return [
|
|
593
|
+
record.id,
|
|
594
|
+
record.status,
|
|
595
|
+
record.category,
|
|
596
|
+
record.sourceAgent,
|
|
597
|
+
record.scope,
|
|
598
|
+
record.topicKey,
|
|
599
|
+
record.content,
|
|
600
|
+
record.evidence,
|
|
601
|
+
].filter(Boolean).join(" ");
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function normalizeSearch(value: string): string {
|
|
605
|
+
return value.toLowerCase().replace(/\s+/g, " ").trim();
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function wrapPlainText(text: string, width: number): string[] {
|
|
609
|
+
const normalized = text.replace(/\r/g, "").split("\n").flatMap((line) => line.trim() ? [line.trim()] : [""]);
|
|
610
|
+
const result: string[] = [];
|
|
611
|
+
for (const line of normalized) {
|
|
612
|
+
if (!line) {
|
|
613
|
+
result.push("");
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
let current = "";
|
|
617
|
+
for (const word of line.split(/\s+/)) {
|
|
618
|
+
const next = current ? `${current} ${word}` : word;
|
|
619
|
+
if (visibleWidth(next) <= width) {
|
|
620
|
+
current = next;
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
if (current) result.push(current);
|
|
624
|
+
current = visibleWidth(word) > width ? truncateToWidth(word, width, "", false) : word;
|
|
625
|
+
}
|
|
626
|
+
if (current) result.push(current);
|
|
627
|
+
}
|
|
628
|
+
return result.length ? result : [""];
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function isBackspace(data: string): boolean {
|
|
632
|
+
return data === "\b" || data === "\x7f" || matchesKey(data, "backspace");
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function isPrintableInput(data: string): boolean {
|
|
636
|
+
return data.length === 1 && data >= " " && data !== "\x7f";
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function memoryActionPast(action: MemoryOverlayAction): string {
|
|
640
|
+
if (action === "approve") return "approved";
|
|
641
|
+
if (action === "reject") return "rejected";
|
|
642
|
+
if (action === "delete") return "deleted";
|
|
643
|
+
return "opened";
|
|
644
|
+
}
|
|
645
|
+
|
|
293
646
|
function formatMemoryListItem(record: MemoryRecord): string {
|
|
294
647
|
return `${memoryStatusIcon(record.status)} ${record.status} · ${record.category} · ${record.sourceAgent} · ${memoryTitle(record)}`;
|
|
295
648
|
}
|