pi-extension-observational-memory 0.1.2 → 0.1.3
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/index.ts +95 -0
- package/overlay.ts +387 -0
- package/package.json +4 -2
package/index.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
type SessionEntry,
|
|
28
28
|
serializeConversation,
|
|
29
29
|
} from "@mariozechner/pi-coding-agent";
|
|
30
|
+
import { ObservationMemoryOverlay, type ObservationMemoryOverlaySnapshot } from "./overlay.js";
|
|
30
31
|
|
|
31
32
|
const DETAILS_SCHEMA_VERSION = 2;
|
|
32
33
|
|
|
@@ -35,6 +36,7 @@ const OBS_REFLECT_COMMAND = "obs-reflect";
|
|
|
35
36
|
const OBS_AUTO_COMPACT_COMMAND = "obs-auto-compact";
|
|
36
37
|
const OBS_MODE_COMMAND = "obs-mode";
|
|
37
38
|
const OBS_VIEW_COMMAND = "obs-view";
|
|
39
|
+
const OBS_STATUS_SHORTCUT = "ctrl+shift+o";
|
|
38
40
|
|
|
39
41
|
const DEFAULT_RESERVE_TOKENS = 16384;
|
|
40
42
|
|
|
@@ -735,6 +737,7 @@ export default function observationalMemoryExtension(pi: ExtensionAPI) {
|
|
|
735
737
|
let rawTailRetainTokens = DEFAULT_RAW_TAIL_RETAIN_TOKENS;
|
|
736
738
|
let autoCompactInFlight = false;
|
|
737
739
|
let lastAutoCompactAt = 0;
|
|
740
|
+
let statusOverlayOpen = false;
|
|
738
741
|
|
|
739
742
|
pi.registerFlag("obs-auto-compact", {
|
|
740
743
|
description: "Enable observational auto observer trigger",
|
|
@@ -827,6 +830,86 @@ export default function observationalMemoryExtension(pi: ExtensionAPI) {
|
|
|
827
830
|
});
|
|
828
831
|
};
|
|
829
832
|
|
|
833
|
+
const buildStatusSnapshot = (ctx: ExtensionContext): ObservationMemoryOverlaySnapshot => {
|
|
834
|
+
const branchEntries = ctx.sessionManager.getBranch();
|
|
835
|
+
const lastCompaction = [...branchEntries].reverse().find((entry) => entry.type === "compaction");
|
|
836
|
+
const lastBranchSummary = [...branchEntries].reverse().find((entry) => entry.type === "branch_summary");
|
|
837
|
+
const rawTailTokens = estimateRawTailTokens(branchEntries);
|
|
838
|
+
const observationTokens = estimateObservationTokens(lastCompaction?.summary);
|
|
839
|
+
|
|
840
|
+
const compactionDetails =
|
|
841
|
+
lastCompaction && isObservationalCompactionDetails(lastCompaction.details)
|
|
842
|
+
? {
|
|
843
|
+
strategy: lastCompaction.details.strategy,
|
|
844
|
+
model: lastCompaction.details.model,
|
|
845
|
+
observationCount: lastCompaction.details.observationCount,
|
|
846
|
+
reflectorRan: lastCompaction.details.reflectorRan,
|
|
847
|
+
reflectionMode: lastCompaction.details.reflectionMode,
|
|
848
|
+
observationsDropped: lastCompaction.details.observationsDropped,
|
|
849
|
+
isSplitTurn: lastCompaction.details.isSplitTurn,
|
|
850
|
+
usedPreviousSummary: lastCompaction.details.usedPreviousSummary,
|
|
851
|
+
generatedAt: lastCompaction.details.generatedAt,
|
|
852
|
+
}
|
|
853
|
+
: undefined;
|
|
854
|
+
|
|
855
|
+
const branchSummaryDetails =
|
|
856
|
+
lastBranchSummary && isObservationalBranchDetails(lastBranchSummary.details)
|
|
857
|
+
? {
|
|
858
|
+
strategy: lastBranchSummary.details.strategy,
|
|
859
|
+
model: lastBranchSummary.details.model,
|
|
860
|
+
observationCount: lastBranchSummary.details.observationCount,
|
|
861
|
+
entryCount: lastBranchSummary.details.entryCount,
|
|
862
|
+
generatedAt: lastBranchSummary.details.generatedAt,
|
|
863
|
+
}
|
|
864
|
+
: undefined;
|
|
865
|
+
|
|
866
|
+
return {
|
|
867
|
+
autoObserverEnabled,
|
|
868
|
+
observerTriggerTokens,
|
|
869
|
+
rawTailTokens,
|
|
870
|
+
reflectorTriggerTokens,
|
|
871
|
+
observationTokens,
|
|
872
|
+
autoCompactInFlight,
|
|
873
|
+
forceReflectPending: forceReflectNextCompaction,
|
|
874
|
+
lastCompaction: lastCompaction
|
|
875
|
+
? {
|
|
876
|
+
id: lastCompaction.id,
|
|
877
|
+
timestamp: lastCompaction.timestamp,
|
|
878
|
+
tokensBefore: lastCompaction.tokensBefore,
|
|
879
|
+
fromExtension: lastCompaction.fromHook,
|
|
880
|
+
details: compactionDetails,
|
|
881
|
+
}
|
|
882
|
+
: undefined,
|
|
883
|
+
lastBranchSummary: lastBranchSummary
|
|
884
|
+
? {
|
|
885
|
+
id: lastBranchSummary.id,
|
|
886
|
+
timestamp: lastBranchSummary.timestamp,
|
|
887
|
+
details: branchSummaryDetails,
|
|
888
|
+
}
|
|
889
|
+
: undefined,
|
|
890
|
+
observations: lastCompaction?.summary ? stripFileTags(lastCompaction.summary) : undefined,
|
|
891
|
+
};
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
const showStatusOverlay = async (ctx: ExtensionContext): Promise<void> => {
|
|
895
|
+
if (!ctx.hasUI) return;
|
|
896
|
+
if (statusOverlayOpen) return;
|
|
897
|
+
|
|
898
|
+
const snapshot = buildStatusSnapshot(ctx);
|
|
899
|
+
statusOverlayOpen = true;
|
|
900
|
+
try {
|
|
901
|
+
await ctx.ui.custom<null>(
|
|
902
|
+
(_tui, _theme, _keys, done) => new ObservationMemoryOverlay(snapshot, done),
|
|
903
|
+
{ overlay: true },
|
|
904
|
+
);
|
|
905
|
+
} catch (error) {
|
|
906
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
907
|
+
ctx.ui.notify(`Unable to render obs overlay: ${message}`, "error");
|
|
908
|
+
} finally {
|
|
909
|
+
statusOverlayOpen = false;
|
|
910
|
+
}
|
|
911
|
+
};
|
|
912
|
+
|
|
830
913
|
pi.on("session_start", async (_event, ctx) => {
|
|
831
914
|
const enabledFlag = pi.getFlag("obs-auto-compact");
|
|
832
915
|
if (typeof enabledFlag === "boolean") {
|
|
@@ -1050,6 +1133,11 @@ export default function observationalMemoryExtension(pi: ExtensionAPI) {
|
|
|
1050
1133
|
pi.registerCommand(OBS_STATUS_COMMAND, {
|
|
1051
1134
|
description: "Show observational-memory compaction and tree summary status",
|
|
1052
1135
|
handler: async (_args, ctx) => {
|
|
1136
|
+
if (ctx.hasUI) {
|
|
1137
|
+
await showStatusOverlay(ctx);
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1053
1141
|
const branchEntries = ctx.sessionManager.getBranch();
|
|
1054
1142
|
const lastCompaction = [...branchEntries].reverse().find((entry) => entry.type === "compaction");
|
|
1055
1143
|
const lastBranchSummary = [...branchEntries].reverse().find((entry) => entry.type === "branch_summary");
|
|
@@ -1122,6 +1210,13 @@ export default function observationalMemoryExtension(pi: ExtensionAPI) {
|
|
|
1122
1210
|
},
|
|
1123
1211
|
});
|
|
1124
1212
|
|
|
1213
|
+
pi.registerShortcut(OBS_STATUS_SHORTCUT, {
|
|
1214
|
+
description: "Open observational-memory status overlay",
|
|
1215
|
+
handler: async (ctx) => {
|
|
1216
|
+
await showStatusOverlay(ctx);
|
|
1217
|
+
},
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1125
1220
|
pi.registerCommand(OBS_AUTO_COMPACT_COMMAND, {
|
|
1126
1221
|
description: "Show or set observer/reflector thresholds, mode, and raw-tail retention",
|
|
1127
1222
|
handler: async (args, ctx) => {
|
package/overlay.ts
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { Key, matchesKey } from "@mariozechner/pi-tui";
|
|
2
|
+
|
|
3
|
+
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
4
|
+
|
|
5
|
+
function color(code: string, text: string): string {
|
|
6
|
+
return `\x1b[${code}m${text}\x1b[0m`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function bold(text: string): string {
|
|
10
|
+
return `\x1b[1m${text}\x1b[22m`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function dim(text: string): string {
|
|
14
|
+
return color("2", text);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function visibleLength(text: string): number {
|
|
18
|
+
return text.replace(ANSI_RE, "").length;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function clipAnsi(text: string, width: number): string {
|
|
22
|
+
if (visibleLength(text) <= width) return text;
|
|
23
|
+
let visible = 0;
|
|
24
|
+
let i = 0;
|
|
25
|
+
let out = "";
|
|
26
|
+
while (i < text.length && visible < width) {
|
|
27
|
+
const ch = text[i];
|
|
28
|
+
if (ch === "\u001b" && text[i + 1] === "[") {
|
|
29
|
+
let j = i + 2;
|
|
30
|
+
while (j < text.length && text[j] !== "m") j++;
|
|
31
|
+
if (j < text.length) {
|
|
32
|
+
out += text.slice(i, j + 1);
|
|
33
|
+
i = j + 1;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
out += ch;
|
|
38
|
+
visible += 1;
|
|
39
|
+
i += 1;
|
|
40
|
+
}
|
|
41
|
+
return `${out}\x1b[0m`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function padRight(text: string, width: number): string {
|
|
45
|
+
const clipped = clipAnsi(text, width);
|
|
46
|
+
const pad = Math.max(0, width - visibleLength(clipped));
|
|
47
|
+
return clipped + " ".repeat(pad);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function wrapPlain(text: string, width: number): string[] {
|
|
51
|
+
if (!text) return [""];
|
|
52
|
+
if (text.length <= width) return [text];
|
|
53
|
+
|
|
54
|
+
const words = text.split(/\s+/).filter((word) => word.length > 0);
|
|
55
|
+
if (words.length === 0) return [""];
|
|
56
|
+
|
|
57
|
+
const lines: string[] = [];
|
|
58
|
+
let current = "";
|
|
59
|
+
|
|
60
|
+
for (const word of words) {
|
|
61
|
+
if (!current) {
|
|
62
|
+
if (word.length <= width) {
|
|
63
|
+
current = word;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
for (let i = 0; i < word.length; i += width) {
|
|
67
|
+
lines.push(word.slice(i, i + width));
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const candidate = `${current} ${word}`;
|
|
73
|
+
if (candidate.length <= width) {
|
|
74
|
+
current = candidate;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
lines.push(current);
|
|
79
|
+
if (word.length <= width) {
|
|
80
|
+
current = word;
|
|
81
|
+
} else {
|
|
82
|
+
for (let i = 0; i < word.length; i += width) {
|
|
83
|
+
lines.push(word.slice(i, i + width));
|
|
84
|
+
}
|
|
85
|
+
current = "";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (current) lines.push(current);
|
|
90
|
+
return lines;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function formatTokenCount(tokens: number): string {
|
|
94
|
+
return `${tokens.toLocaleString()} tokens`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function meter(current: number, total: number, width = 26): string {
|
|
98
|
+
const ratio = total > 0 ? Math.min(current / total, 1) : 0;
|
|
99
|
+
const filled = Math.max(0, Math.min(width, Math.round(ratio * width)));
|
|
100
|
+
const colorCode = ratio >= 0.95 ? "31" : ratio >= 0.7 ? "33" : "32";
|
|
101
|
+
return `${color("2", "[")}${color(colorCode, "█".repeat(filled))}${color("2", "░".repeat(width - filled))}${color("2", "]")} ${color(colorCode, `${Math.round(ratio * 100)}%`)}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
type Tab = "status" | "observations";
|
|
105
|
+
|
|
106
|
+
type Severity = "normal" | "heading" | "red" | "yellow" | "green" | "muted";
|
|
107
|
+
|
|
108
|
+
interface StyledLine {
|
|
109
|
+
text: string;
|
|
110
|
+
severity?: Severity;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
interface CompactionOverlayDetails {
|
|
114
|
+
strategy?: string;
|
|
115
|
+
model?: string;
|
|
116
|
+
observationCount?: number;
|
|
117
|
+
reflectorRan?: boolean;
|
|
118
|
+
reflectionMode?: string;
|
|
119
|
+
observationsDropped?: number;
|
|
120
|
+
isSplitTurn?: boolean;
|
|
121
|
+
usedPreviousSummary?: boolean;
|
|
122
|
+
generatedAt?: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
interface BranchOverlayDetails {
|
|
126
|
+
strategy?: string;
|
|
127
|
+
model?: string;
|
|
128
|
+
observationCount?: number;
|
|
129
|
+
entryCount?: number;
|
|
130
|
+
generatedAt?: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface ObservationMemoryOverlaySnapshot {
|
|
134
|
+
autoObserverEnabled: boolean;
|
|
135
|
+
observerTriggerTokens: number;
|
|
136
|
+
rawTailTokens: number;
|
|
137
|
+
reflectorTriggerTokens: number;
|
|
138
|
+
observationTokens: number;
|
|
139
|
+
autoCompactInFlight: boolean;
|
|
140
|
+
forceReflectPending: boolean;
|
|
141
|
+
lastCompaction?: {
|
|
142
|
+
id: string;
|
|
143
|
+
timestamp: number | string;
|
|
144
|
+
tokensBefore: number;
|
|
145
|
+
fromExtension: boolean;
|
|
146
|
+
details?: CompactionOverlayDetails;
|
|
147
|
+
};
|
|
148
|
+
lastBranchSummary?: {
|
|
149
|
+
id: string;
|
|
150
|
+
timestamp: number | string;
|
|
151
|
+
details?: BranchOverlayDetails;
|
|
152
|
+
};
|
|
153
|
+
observations?: string;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function styleLine(line: StyledLine): string {
|
|
157
|
+
switch (line.severity) {
|
|
158
|
+
case "heading":
|
|
159
|
+
return bold(color("36", line.text));
|
|
160
|
+
case "red":
|
|
161
|
+
return color("31", line.text);
|
|
162
|
+
case "yellow":
|
|
163
|
+
return color("33", line.text);
|
|
164
|
+
case "green":
|
|
165
|
+
return color("32", line.text);
|
|
166
|
+
case "muted":
|
|
167
|
+
return dim(line.text);
|
|
168
|
+
default:
|
|
169
|
+
return line.text;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function buildStatusLines(snapshot: ObservationMemoryOverlaySnapshot): StyledLine[] {
|
|
174
|
+
const lines: StyledLine[] = [
|
|
175
|
+
{ text: "Observer/Reflector" },
|
|
176
|
+
{ text: `Observer trigger: ${snapshot.autoObserverEnabled ? "on" : "off"}` },
|
|
177
|
+
{ text: `Observer threshold: ${formatTokenCount(snapshot.observerTriggerTokens)}` },
|
|
178
|
+
{ text: `Raw tail now: ${formatTokenCount(snapshot.rawTailTokens)}` },
|
|
179
|
+
{ text: meter(snapshot.rawTailTokens, snapshot.observerTriggerTokens), severity: "normal" },
|
|
180
|
+
{ text: "" },
|
|
181
|
+
{ text: `Reflector threshold: ${formatTokenCount(snapshot.reflectorTriggerTokens)}` },
|
|
182
|
+
{ text: `Observation block: ${formatTokenCount(snapshot.observationTokens)}` },
|
|
183
|
+
{ text: meter(snapshot.observationTokens, snapshot.reflectorTriggerTokens), severity: "normal" },
|
|
184
|
+
{ text: "" },
|
|
185
|
+
{ text: `Auto-compact in flight: ${snapshot.autoCompactInFlight ? "yes" : "no"}` },
|
|
186
|
+
{ text: `Force-reflect pending: ${snapshot.forceReflectPending ? "yes" : "no"}` },
|
|
187
|
+
{ text: "" },
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
if (snapshot.lastCompaction) {
|
|
191
|
+
lines.push(
|
|
192
|
+
{ text: "Last compaction", severity: "heading" },
|
|
193
|
+
{ text: `id: ${snapshot.lastCompaction.id}` },
|
|
194
|
+
{ text: `timestamp: ${new Date(snapshot.lastCompaction.timestamp).toLocaleString()}` },
|
|
195
|
+
{ text: `tokensBefore: ${snapshot.lastCompaction.tokensBefore.toLocaleString()}` },
|
|
196
|
+
{ text: `fromExtension: ${snapshot.lastCompaction.fromExtension ? "yes" : "no"}` },
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
if (snapshot.lastCompaction.details) {
|
|
200
|
+
const details = snapshot.lastCompaction.details;
|
|
201
|
+
lines.push(
|
|
202
|
+
{ text: `strategy: ${details.strategy ?? "unknown"}`, severity: "muted" },
|
|
203
|
+
{ text: `model: ${details.model ?? "unknown"}`, severity: "muted" },
|
|
204
|
+
{ text: `observations: ${details.observationCount ?? 0}`, severity: "muted" },
|
|
205
|
+
{
|
|
206
|
+
text: `reflector: ${details.reflectorRan ? "yes" : "no"}${details.reflectionMode ? ` (${details.reflectionMode})` : ""}`,
|
|
207
|
+
severity: "muted",
|
|
208
|
+
},
|
|
209
|
+
{ text: `dropped: ${details.observationsDropped ?? 0}`, severity: "muted" },
|
|
210
|
+
{ text: `splitTurn: ${details.isSplitTurn ? "yes" : "no"}`, severity: "muted" },
|
|
211
|
+
{ text: `usedPreviousSummary: ${details.usedPreviousSummary ? "yes" : "no"}`, severity: "muted" },
|
|
212
|
+
);
|
|
213
|
+
if (details.generatedAt) {
|
|
214
|
+
lines.push({ text: `generatedAt: ${details.generatedAt}`, severity: "muted" });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
lines.push({ text: "" });
|
|
219
|
+
} else {
|
|
220
|
+
lines.push({ text: "No compaction entries found in current branch.", severity: "yellow" }, { text: "" });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (snapshot.lastBranchSummary) {
|
|
224
|
+
lines.push(
|
|
225
|
+
{ text: "Last branch summary", severity: "heading" },
|
|
226
|
+
{ text: `id: ${snapshot.lastBranchSummary.id}` },
|
|
227
|
+
{ text: `timestamp: ${new Date(snapshot.lastBranchSummary.timestamp).toLocaleString()}` },
|
|
228
|
+
);
|
|
229
|
+
if (snapshot.lastBranchSummary.details) {
|
|
230
|
+
const details = snapshot.lastBranchSummary.details;
|
|
231
|
+
lines.push(
|
|
232
|
+
{ text: `strategy: ${details.strategy ?? "unknown"}`, severity: "muted" },
|
|
233
|
+
{ text: `model: ${details.model ?? "unknown"}`, severity: "muted" },
|
|
234
|
+
{ text: `observations: ${details.observationCount ?? 0}`, severity: "muted" },
|
|
235
|
+
{ text: `entryCount: ${details.entryCount ?? 0}`, severity: "muted" },
|
|
236
|
+
);
|
|
237
|
+
if (details.generatedAt) {
|
|
238
|
+
lines.push({ text: `generatedAt: ${details.generatedAt}`, severity: "muted" });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return lines;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function buildObservationLines(summary: string | undefined): StyledLine[] {
|
|
247
|
+
if (!summary || summary.trim().length === 0) {
|
|
248
|
+
return [{ text: "No observations in the latest compaction yet.", severity: "yellow" }];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return summary.split("\n").map((line) => {
|
|
252
|
+
if (line.startsWith("## ")) return { text: line, severity: "heading" as const };
|
|
253
|
+
if (line.startsWith("Date:")) return { text: line, severity: "muted" as const };
|
|
254
|
+
if (line.startsWith("- 🔴")) return { text: line, severity: "red" as const };
|
|
255
|
+
if (line.startsWith("- 🟡")) return { text: line, severity: "yellow" as const };
|
|
256
|
+
if (line.startsWith("- 🟢")) return { text: line, severity: "green" as const };
|
|
257
|
+
if (/^\d+\.\s+/.test(line)) return { text: line, severity: "green" as const };
|
|
258
|
+
if (line.trim().length === 0) return { text: "" };
|
|
259
|
+
return { text: line };
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function wrapStyledLines(lines: StyledLine[], width: number): string[] {
|
|
264
|
+
const wrapped: string[] = [];
|
|
265
|
+
for (const line of lines) {
|
|
266
|
+
const parts = wrapPlain(line.text, width);
|
|
267
|
+
for (const part of parts) {
|
|
268
|
+
wrapped.push(styleLine({ text: part, severity: line.severity }));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return wrapped;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export class ObservationMemoryOverlay {
|
|
275
|
+
private readonly maxWidth = 98;
|
|
276
|
+
private readonly contentRows = 20;
|
|
277
|
+
private tab: Tab = "status";
|
|
278
|
+
private scrollOffset = 0;
|
|
279
|
+
private cacheWidth = 0;
|
|
280
|
+
private statusLines: string[] = [];
|
|
281
|
+
private observationLines: string[] = [];
|
|
282
|
+
|
|
283
|
+
constructor(
|
|
284
|
+
private snapshot: ObservationMemoryOverlaySnapshot,
|
|
285
|
+
private done: (result: null) => void,
|
|
286
|
+
) {}
|
|
287
|
+
|
|
288
|
+
handleInput(data: string): void {
|
|
289
|
+
if (matchesKey(data, Key.escape) || data === "q") {
|
|
290
|
+
this.done(null);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (matchesKey(data, Key.tab) || data === "1" || data === "2") {
|
|
295
|
+
this.tab = data === "1" ? "status" : data === "2" ? "observations" : this.tab === "status" ? "observations" : "status";
|
|
296
|
+
this.scrollOffset = 0;
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (matchesKey(data, Key.up) || data === "k") {
|
|
301
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - 1);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (matchesKey(data, Key.down) || data === "j") {
|
|
305
|
+
this.scrollOffset += 1;
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (matchesKey(data, "pageup") || matchesKey(data, Key.ctrl("u"))) {
|
|
309
|
+
this.scrollOffset = Math.max(0, this.scrollOffset - 8);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (matchesKey(data, "pagedown") || matchesKey(data, Key.ctrl("d"))) {
|
|
313
|
+
this.scrollOffset += 8;
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (data === "g") {
|
|
317
|
+
this.scrollOffset = 0;
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (data === "G") {
|
|
321
|
+
const maxScroll = Math.max(0, this.activeLines().length - this.contentRows);
|
|
322
|
+
this.scrollOffset = maxScroll;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
render(width: number): string[] {
|
|
327
|
+
if (width < 20) {
|
|
328
|
+
return [padRight("Obs Memory", width)];
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const frameWidth = Math.min(this.maxWidth, width);
|
|
332
|
+
const innerWidth = frameWidth - 2;
|
|
333
|
+
const contentWidth = Math.max(1, innerWidth - 2);
|
|
334
|
+
|
|
335
|
+
if (this.cacheWidth !== contentWidth) {
|
|
336
|
+
this.cacheWidth = contentWidth;
|
|
337
|
+
this.statusLines = wrapStyledLines(buildStatusLines(this.snapshot), contentWidth);
|
|
338
|
+
this.observationLines = wrapStyledLines(buildObservationLines(this.snapshot.observations), contentWidth);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const lines = this.activeLines();
|
|
342
|
+
const maxScroll = Math.max(0, lines.length - this.contentRows);
|
|
343
|
+
if (this.scrollOffset > maxScroll) this.scrollOffset = maxScroll;
|
|
344
|
+
|
|
345
|
+
const visible = lines.slice(this.scrollOffset, this.scrollOffset + this.contentRows);
|
|
346
|
+
const statusTab = this.tab === "status" ? bold(color("36", "● Status")) : dim("○ Status");
|
|
347
|
+
const obsTab = this.tab === "observations" ? bold(color("36", "● Observations")) : dim("○ Observations");
|
|
348
|
+
|
|
349
|
+
const out: string[] = [];
|
|
350
|
+
const title = bold(color("36", " 🧠 Observational Memory "));
|
|
351
|
+
const sidePad = Math.max(0, innerWidth - visibleLength(title));
|
|
352
|
+
const leftPad = Math.floor(sidePad / 2);
|
|
353
|
+
const rightPad = sidePad - leftPad;
|
|
354
|
+
|
|
355
|
+
out.push(dim("╭") + dim("─".repeat(leftPad)) + title + dim("─".repeat(rightPad)) + dim("╮"));
|
|
356
|
+
out.push(dim("│") + " " + padRight(`${statusTab} ${obsTab}`, innerWidth - 1) + dim("│"));
|
|
357
|
+
out.push(dim("├") + dim("─".repeat(innerWidth)) + dim("┤"));
|
|
358
|
+
|
|
359
|
+
for (let i = 0; i < this.contentRows; i++) {
|
|
360
|
+
const line = visible[i] ?? "";
|
|
361
|
+
out.push(dim("│") + " " + padRight(line, innerWidth - 1) + dim("│"));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const rangeStart = lines.length === 0 ? 0 : this.scrollOffset + 1;
|
|
365
|
+
const rangeEnd = Math.min(lines.length, this.scrollOffset + this.contentRows);
|
|
366
|
+
const footer = dim(` ${rangeStart}-${rangeEnd} / ${lines.length} `);
|
|
367
|
+
const footerPad = Math.max(0, innerWidth - visibleLength(footer));
|
|
368
|
+
out.push(dim("├") + dim("─".repeat(Math.floor(footerPad / 2))) + footer + dim("─".repeat(Math.ceil(footerPad / 2))) + dim("┤"));
|
|
369
|
+
|
|
370
|
+
const hints = dim("↑↓/jk scroll PgUp/PgDn page tab switch esc close");
|
|
371
|
+
out.push(dim("│") + " " + padRight(hints, innerWidth - 1) + dim("│"));
|
|
372
|
+
out.push(dim("╰") + dim("─".repeat(innerWidth)) + dim("╯"));
|
|
373
|
+
return out;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
invalidate(): void {
|
|
377
|
+
this.cacheWidth = 0;
|
|
378
|
+
this.statusLines = [];
|
|
379
|
+
this.observationLines = [];
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
dispose(): void {}
|
|
383
|
+
|
|
384
|
+
private activeLines(): string[] {
|
|
385
|
+
return this.tab === "status" ? this.statusLines : this.observationLines;
|
|
386
|
+
}
|
|
387
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-extension-observational-memory",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Observational-memory compaction strategy for pi with observer/reflector token thresholds",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
],
|
|
16
16
|
"files": [
|
|
17
17
|
"index.ts",
|
|
18
|
+
"overlay.ts",
|
|
18
19
|
"README.md",
|
|
19
20
|
"DESIGN.md",
|
|
20
21
|
"package.json"
|
|
@@ -31,7 +32,8 @@
|
|
|
31
32
|
},
|
|
32
33
|
"peerDependencies": {
|
|
33
34
|
"@mariozechner/pi-ai": "*",
|
|
34
|
-
"@mariozechner/pi-coding-agent": "*"
|
|
35
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
36
|
+
"@mariozechner/pi-tui": "*"
|
|
35
37
|
},
|
|
36
38
|
"devDependencies": {
|
|
37
39
|
"@biomejs/biome": "^2.3.5"
|