pi-ui-extend 0.1.13 → 0.1.17
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 +1 -1
- package/dist/app/app.d.ts +7 -0
- package/dist/app/app.js +102 -17
- package/dist/app/commands/command-controller.js +2 -0
- package/dist/app/commands/command-host.d.ts +5 -0
- package/dist/app/commands/command-model-actions.d.ts +2 -0
- package/dist/app/commands/command-model-actions.js +40 -4
- package/dist/app/commands/command-navigation-actions.d.ts +9 -0
- package/dist/app/commands/command-navigation-actions.js +62 -0
- package/dist/app/commands/command-registry.d.ts +2 -0
- package/dist/app/commands/command-registry.js +16 -0
- package/dist/app/constants.d.ts +0 -1
- package/dist/app/constants.js +0 -1
- package/dist/app/extensions/extension-ui-controller.d.ts +16 -5
- package/dist/app/extensions/extension-ui-controller.js +99 -61
- package/dist/app/icons.d.ts +1 -0
- package/dist/app/icons.js +2 -0
- package/dist/app/input/input-action-controller.d.ts +2 -0
- package/dist/app/input/input-action-controller.js +8 -1
- package/dist/app/logger.d.ts +25 -0
- package/dist/app/logger.js +90 -0
- package/dist/app/model/model-usage-status.js +30 -15
- package/dist/app/popup/menu-items-controller.d.ts +4 -0
- package/dist/app/popup/menu-items-controller.js +68 -6
- package/dist/app/popup/popup-action-controller.d.ts +2 -1
- package/dist/app/popup/popup-action-controller.js +7 -4
- package/dist/app/popup/popup-menu-controller.d.ts +36 -23
- package/dist/app/popup/popup-menu-controller.js +97 -326
- package/dist/app/rendering/conversation-entry-renderer.js +3 -3
- package/dist/app/rendering/conversation-viewport.d.ts +10 -2
- package/dist/app/rendering/conversation-viewport.js +157 -16
- package/dist/app/rendering/editor-panels.js +22 -9
- package/dist/app/rendering/popup-menu-renderer.d.ts +62 -0
- package/dist/app/rendering/popup-menu-renderer.js +405 -0
- package/dist/app/rendering/render-controller.js +30 -28
- package/dist/app/rendering/render-text.js +5 -2
- package/dist/app/rendering/status-line-renderer.d.ts +8 -1
- package/dist/app/rendering/status-line-renderer.js +217 -117
- package/dist/app/rendering/toast-controller.d.ts +12 -3
- package/dist/app/rendering/toast-controller.js +70 -12
- package/dist/app/runtime.d.ts +2 -1
- package/dist/app/runtime.js +20 -10
- package/dist/app/screen/mouse-controller.d.ts +2 -2
- package/dist/app/screen/mouse-controller.js +27 -48
- package/dist/app/screen/screen-styler.d.ts +1 -1
- package/dist/app/screen/screen-styler.js +9 -7
- package/dist/app/screen/scroll-controller.d.ts +12 -9
- package/dist/app/screen/scroll-controller.js +56 -45
- package/dist/app/screen/status-controller.js +2 -1
- package/dist/app/session/lazy-session-manager.d.ts +11 -0
- package/dist/app/session/lazy-session-manager.js +539 -0
- package/dist/app/session/pix-system-message.d.ts +16 -0
- package/dist/app/session/pix-system-message.js +64 -0
- package/dist/app/session/request-history.d.ts +4 -0
- package/dist/app/session/request-history.js +11 -0
- package/dist/app/session/session-event-controller.d.ts +11 -0
- package/dist/app/session/session-event-controller.js +58 -2
- package/dist/app/session/session-history.d.ts +18 -0
- package/dist/app/session/session-history.js +72 -3
- package/dist/app/session/session-lifecycle-controller.d.ts +6 -2
- package/dist/app/session/session-lifecycle-controller.js +7 -2
- package/dist/app/session/session-search.js +10 -0
- package/dist/app/session/tabs-controller.d.ts +17 -5
- package/dist/app/session/tabs-controller.js +308 -29
- package/dist/app/todo/todo-model.d.ts +4 -2
- package/dist/app/todo/todo-model.js +23 -13
- package/dist/app/types.d.ts +17 -6
- package/dist/app/workspace/workspace-actions-controller.d.ts +2 -0
- package/dist/app/workspace/workspace-actions-controller.js +12 -0
- package/dist/config.d.ts +6 -1
- package/dist/config.js +82 -25
- package/dist/default-pix-config.js +4 -0
- package/dist/fuzzy.d.ts +2 -0
- package/dist/fuzzy.js +27 -7
- package/dist/input-editor.d.ts +9 -0
- package/dist/input-editor.js +52 -0
- package/dist/schemas/pi-tools-suite-schema.d.ts +1 -0
- package/dist/schemas/pi-tools-suite-schema.js +1 -0
- package/dist/schemas/pix-schema.d.ts +3 -1
- package/dist/schemas/pix-schema.js +6 -4
- package/dist/terminal-width.d.ts +2 -0
- package/dist/terminal-width.js +64 -3
- package/dist/theme.js +6 -6
- package/dist/ui.d.ts +8 -0
- package/external/pi-tools-suite/README.md +3 -2
- package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +52 -8
- package/external/pi-tools-suite/src/antigravity-auth/commands.ts +3 -41
- package/external/pi-tools-suite/src/antigravity-auth/constants.ts +0 -2
- package/external/pi-tools-suite/src/antigravity-auth/index.ts +11 -18
- package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +129 -61
- package/external/pi-tools-suite/src/antigravity-auth/status.ts +82 -3
- package/external/pi-tools-suite/src/antigravity-auth/stream.ts +20 -7
- package/external/pi-tools-suite/src/antigravity-auth/types.ts +21 -0
- package/external/pi-tools-suite/src/config.ts +8 -0
- package/external/pi-tools-suite/src/dcp/index.ts +16 -1
- package/external/pi-tools-suite/src/dcp/state.ts +35 -0
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +3 -0
- package/external/pi-tools-suite/src/todo/index.ts +123 -14
- package/external/pi-tools-suite/src/todo/state/persistence.ts +0 -1
- package/external/pi-tools-suite/src/todo/state/state-reducer.ts +26 -43
- package/external/pi-tools-suite/src/todo/todo.ts +12 -23
- package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +34 -16
- package/external/pi-tools-suite/src/todo/tool/types.ts +7 -28
- package/external/pi-tools-suite/src/todo/view/format.ts +2 -3
- package/external/pi-tools-suite/src/tool-descriptions.ts +6 -4
- package/external/pi-tools-suite/src/usage/index.ts +5 -2
- package/external/pi-tools-suite/src/usage/lib/google.ts +53 -40
- package/external/pi-tools-suite/src/usage/lib/types.ts +12 -2
- package/package.json +1 -1
- package/schemas/pi-tools-suite.json +4 -0
- package/schemas/pix.json +11 -2
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { appendFileSync, closeSync, createReadStream, existsSync, mkdirSync, openSync, readSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { open as openFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { createInterface } from "node:readline";
|
|
6
|
+
import { buildSessionContext, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { isRecord } from "../guards.js";
|
|
8
|
+
const CURRENT_SESSION_VERSION = 3;
|
|
9
|
+
const DEFAULT_TAIL_ENTRY_COUNT = 180;
|
|
10
|
+
const INITIAL_TAIL_BYTES = 256 * 1024;
|
|
11
|
+
const MAX_TAIL_BYTES = 16 * 1024 * 1024;
|
|
12
|
+
export function openLazySessionManager(sessionPath, options = {}) {
|
|
13
|
+
return new LazySessionManager(sessionPath, options);
|
|
14
|
+
}
|
|
15
|
+
class LazySessionManager {
|
|
16
|
+
sessionFilePath;
|
|
17
|
+
sessionDirPath;
|
|
18
|
+
cwdPath;
|
|
19
|
+
header;
|
|
20
|
+
entries = [];
|
|
21
|
+
byId = new Map();
|
|
22
|
+
labelsById = new Map();
|
|
23
|
+
labelTimestampsById = new Map();
|
|
24
|
+
leafId = null;
|
|
25
|
+
hydrated;
|
|
26
|
+
tailEntryCount;
|
|
27
|
+
tailStartOffset = 0;
|
|
28
|
+
constructor(sessionPath, options = {}) {
|
|
29
|
+
this.sessionFilePath = resolve(sessionPath);
|
|
30
|
+
this.sessionDirPath = resolve(options.sessionDir ?? dirname(this.sessionFilePath));
|
|
31
|
+
this.tailEntryCount = Math.max(1, Math.floor(options.tailEntryCount ?? DEFAULT_TAIL_ENTRY_COUNT));
|
|
32
|
+
this.header = this.loadHeader(options.cwdOverride);
|
|
33
|
+
this.cwdPath = resolve(options.cwdOverride ?? this.header.cwd ?? process.cwd());
|
|
34
|
+
this.loadTailEntries();
|
|
35
|
+
}
|
|
36
|
+
setSessionFile(sessionFile) {
|
|
37
|
+
if (this.hydrated) {
|
|
38
|
+
this.hydrated.setSessionFile(sessionFile);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
this.sessionFilePath = resolve(sessionFile);
|
|
42
|
+
this.sessionDirPath = dirname(this.sessionFilePath);
|
|
43
|
+
this.header = this.loadHeader(this.cwdPath);
|
|
44
|
+
this.cwdPath = resolve(this.header.cwd || this.cwdPath);
|
|
45
|
+
this.loadTailEntries();
|
|
46
|
+
}
|
|
47
|
+
newSession(options) {
|
|
48
|
+
if (this.hydrated)
|
|
49
|
+
return this.hydrated.newSession(options);
|
|
50
|
+
const timestamp = new Date().toISOString();
|
|
51
|
+
const sessionId = options?.id ?? createSessionId();
|
|
52
|
+
const header = {
|
|
53
|
+
type: "session",
|
|
54
|
+
version: CURRENT_SESSION_VERSION,
|
|
55
|
+
id: sessionId,
|
|
56
|
+
timestamp,
|
|
57
|
+
cwd: this.cwdPath,
|
|
58
|
+
};
|
|
59
|
+
if (options?.parentSession !== undefined)
|
|
60
|
+
header.parentSession = options.parentSession;
|
|
61
|
+
this.header = header;
|
|
62
|
+
this.entries = [];
|
|
63
|
+
this.byId.clear();
|
|
64
|
+
this.labelsById.clear();
|
|
65
|
+
this.labelTimestampsById.clear();
|
|
66
|
+
this.leafId = null;
|
|
67
|
+
mkdirSync(this.sessionDirPath, { recursive: true });
|
|
68
|
+
this.sessionFilePath = join(this.sessionDirPath, `${timestamp.replace(/[:.]/g, "-")}_${sessionId}.jsonl`);
|
|
69
|
+
writeFileSync(this.sessionFilePath, `${JSON.stringify(header)}\n`, "utf8");
|
|
70
|
+
return this.sessionFilePath;
|
|
71
|
+
}
|
|
72
|
+
isPersisted() {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
getCwd() {
|
|
76
|
+
return this.hydrated?.getCwd() ?? this.cwdPath;
|
|
77
|
+
}
|
|
78
|
+
getSessionDir() {
|
|
79
|
+
return this.hydrated?.getSessionDir() ?? this.sessionDirPath;
|
|
80
|
+
}
|
|
81
|
+
usesDefaultSessionDir() {
|
|
82
|
+
return this.hydrated?.usesDefaultSessionDir() ?? false;
|
|
83
|
+
}
|
|
84
|
+
getSessionId() {
|
|
85
|
+
return this.hydrated?.getSessionId() ?? this.header.id;
|
|
86
|
+
}
|
|
87
|
+
getSessionFile() {
|
|
88
|
+
return this.hydrated?.getSessionFile() ?? this.sessionFilePath;
|
|
89
|
+
}
|
|
90
|
+
getHeader() {
|
|
91
|
+
return this.hydrated?.getHeader() ?? this.header;
|
|
92
|
+
}
|
|
93
|
+
getEntries() {
|
|
94
|
+
return this.hydrated?.getEntries() ?? [...this.entries];
|
|
95
|
+
}
|
|
96
|
+
getBranch(fromId) {
|
|
97
|
+
if (this.hydrated)
|
|
98
|
+
return this.hydrated.getBranch(fromId);
|
|
99
|
+
if (fromId === undefined)
|
|
100
|
+
return [...this.entries];
|
|
101
|
+
if (fromId !== undefined && !this.byId.has(fromId))
|
|
102
|
+
return this.hydrate().getBranch(fromId);
|
|
103
|
+
return [...this.entries];
|
|
104
|
+
}
|
|
105
|
+
createHistoryReader() {
|
|
106
|
+
if (this.hydrated || this.tailStartOffset <= 0)
|
|
107
|
+
return undefined;
|
|
108
|
+
let cursorOffset = this.tailStartOffset;
|
|
109
|
+
const firstEntryOffset = readFirstSessionEntryOffset(this.sessionFilePath);
|
|
110
|
+
return {
|
|
111
|
+
hasOlder: () => cursorOffset > firstEntryOffset,
|
|
112
|
+
readOlder: async (limit) => {
|
|
113
|
+
if (cursorOffset <= firstEntryOffset)
|
|
114
|
+
return [];
|
|
115
|
+
const result = await readSessionEntriesBeforeOffset(this.sessionFilePath, cursorOffset, Math.max(1, Math.floor(limit)));
|
|
116
|
+
cursorOffset = result.startOffset;
|
|
117
|
+
if (result.entries.length === 0)
|
|
118
|
+
cursorOffset = firstEntryOffset;
|
|
119
|
+
return result.entries;
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
async readFullBranchEntries() {
|
|
124
|
+
if (this.hydrated)
|
|
125
|
+
return this.hydrated.getBranch();
|
|
126
|
+
const entries = await readAllSessionEntries(this.sessionFilePath);
|
|
127
|
+
return branchEntries(entries, this.leafId ?? entries.at(-1)?.id);
|
|
128
|
+
}
|
|
129
|
+
buildSessionContext() {
|
|
130
|
+
if (this.hydrated)
|
|
131
|
+
return this.hydrated.buildSessionContext();
|
|
132
|
+
const entries = this.contextEntries();
|
|
133
|
+
const byId = new Map(entries.map((entry) => [entry.id, entry]));
|
|
134
|
+
return buildSessionContext(entries, entries.at(-1)?.id ?? null, byId);
|
|
135
|
+
}
|
|
136
|
+
getSessionName() {
|
|
137
|
+
if (this.hydrated)
|
|
138
|
+
return this.hydrated.getSessionName();
|
|
139
|
+
for (let index = this.entries.length - 1; index >= 0; index -= 1) {
|
|
140
|
+
const entry = this.entries[index];
|
|
141
|
+
if (entry?.type === "session_info")
|
|
142
|
+
return entry.name?.trim() || undefined;
|
|
143
|
+
}
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
getLeafId() {
|
|
147
|
+
return this.hydrated?.getLeafId() ?? this.leafId;
|
|
148
|
+
}
|
|
149
|
+
getLeafEntry() {
|
|
150
|
+
if (this.hydrated)
|
|
151
|
+
return this.hydrated.getLeafEntry();
|
|
152
|
+
return this.leafId ? this.byId.get(this.leafId) : undefined;
|
|
153
|
+
}
|
|
154
|
+
getEntry(id) {
|
|
155
|
+
if (this.hydrated)
|
|
156
|
+
return this.hydrated.getEntry(id);
|
|
157
|
+
return this.byId.get(id) ?? this.hydrate().getEntry(id);
|
|
158
|
+
}
|
|
159
|
+
getChildren(parentId) {
|
|
160
|
+
if (this.hydrated)
|
|
161
|
+
return this.hydrated.getChildren(parentId);
|
|
162
|
+
return this.entries.filter((entry) => entry.parentId === parentId);
|
|
163
|
+
}
|
|
164
|
+
getLabel(id) {
|
|
165
|
+
return this.hydrated?.getLabel(id) ?? this.labelsById.get(id);
|
|
166
|
+
}
|
|
167
|
+
getTree() {
|
|
168
|
+
return this.hydrate().getTree();
|
|
169
|
+
}
|
|
170
|
+
branch(branchFromId) {
|
|
171
|
+
if (!this.byId.has(branchFromId)) {
|
|
172
|
+
this.hydrate().branch(branchFromId);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
this.leafId = branchFromId;
|
|
176
|
+
}
|
|
177
|
+
resetLeaf() {
|
|
178
|
+
if (this.hydrated) {
|
|
179
|
+
this.hydrated.resetLeaf();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
this.leafId = null;
|
|
183
|
+
}
|
|
184
|
+
createBranchedSession(leafId) {
|
|
185
|
+
return this.hydrate().createBranchedSession(leafId);
|
|
186
|
+
}
|
|
187
|
+
branchWithSummary(branchFromId, summary, details, fromHook) {
|
|
188
|
+
return this.hydrate().branchWithSummary(branchFromId, summary, details, fromHook);
|
|
189
|
+
}
|
|
190
|
+
appendLabelChange(targetId, label) {
|
|
191
|
+
if (this.hydrated)
|
|
192
|
+
return this.hydrated.appendLabelChange(targetId, label);
|
|
193
|
+
if (!this.byId.has(targetId))
|
|
194
|
+
return this.hydrate().appendLabelChange(targetId, label);
|
|
195
|
+
const entry = this.newEntry("label", { targetId, label });
|
|
196
|
+
this.appendEntry(entry);
|
|
197
|
+
if (label) {
|
|
198
|
+
this.labelsById.set(targetId, label);
|
|
199
|
+
this.labelTimestampsById.set(targetId, entry.timestamp);
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
this.labelsById.delete(targetId);
|
|
203
|
+
this.labelTimestampsById.delete(targetId);
|
|
204
|
+
}
|
|
205
|
+
return entry.id;
|
|
206
|
+
}
|
|
207
|
+
appendMessage(message) {
|
|
208
|
+
if (this.hydrated)
|
|
209
|
+
return this.hydrated.appendMessage(message);
|
|
210
|
+
return this.appendEntry(this.newEntry("message", { message }));
|
|
211
|
+
}
|
|
212
|
+
appendThinkingLevelChange(thinkingLevel) {
|
|
213
|
+
if (this.hydrated)
|
|
214
|
+
return this.hydrated.appendThinkingLevelChange(thinkingLevel);
|
|
215
|
+
return this.appendEntry(this.newEntry("thinking_level_change", { thinkingLevel }));
|
|
216
|
+
}
|
|
217
|
+
appendModelChange(provider, modelId) {
|
|
218
|
+
if (this.hydrated)
|
|
219
|
+
return this.hydrated.appendModelChange(provider, modelId);
|
|
220
|
+
return this.appendEntry(this.newEntry("model_change", { provider, modelId }));
|
|
221
|
+
}
|
|
222
|
+
appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromHook) {
|
|
223
|
+
if (this.hydrated)
|
|
224
|
+
return this.hydrated.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromHook);
|
|
225
|
+
const payload = { summary, firstKeptEntryId, tokensBefore };
|
|
226
|
+
if (details !== undefined)
|
|
227
|
+
payload.details = details;
|
|
228
|
+
if (fromHook !== undefined)
|
|
229
|
+
payload.fromHook = fromHook;
|
|
230
|
+
return this.appendEntry(this.newEntry("compaction", payload));
|
|
231
|
+
}
|
|
232
|
+
appendCustomEntry(customType, data) {
|
|
233
|
+
if (this.hydrated)
|
|
234
|
+
return this.hydrated.appendCustomEntry(customType, data);
|
|
235
|
+
const payload = { customType };
|
|
236
|
+
if (data !== undefined)
|
|
237
|
+
payload.data = data;
|
|
238
|
+
return this.appendEntry(this.newEntry("custom", payload));
|
|
239
|
+
}
|
|
240
|
+
appendSessionInfo(name) {
|
|
241
|
+
if (this.hydrated)
|
|
242
|
+
return this.hydrated.appendSessionInfo(name);
|
|
243
|
+
return this.appendEntry(this.newEntry("session_info", { name: name.trim() }));
|
|
244
|
+
}
|
|
245
|
+
appendCustomMessageEntry(customType, content, display, details) {
|
|
246
|
+
if (this.hydrated)
|
|
247
|
+
return this.hydrated.appendCustomMessageEntry(customType, content, display, details);
|
|
248
|
+
const payload = { customType, content, display };
|
|
249
|
+
if (details !== undefined)
|
|
250
|
+
payload.details = details;
|
|
251
|
+
return this.appendEntry(this.newEntry("custom_message", payload));
|
|
252
|
+
}
|
|
253
|
+
hydrate() {
|
|
254
|
+
if (!this.hydrated) {
|
|
255
|
+
this.hydrated = SessionManager.open(this.sessionFilePath, this.sessionDirPath, this.cwdPath);
|
|
256
|
+
}
|
|
257
|
+
return this.hydrated;
|
|
258
|
+
}
|
|
259
|
+
loadHeader(cwdOverride) {
|
|
260
|
+
if (!existsSync(this.sessionFilePath)) {
|
|
261
|
+
mkdirSync(dirname(this.sessionFilePath), { recursive: true });
|
|
262
|
+
const header = createSessionHeader(resolve(cwdOverride ?? process.cwd()));
|
|
263
|
+
writeFileSync(this.sessionFilePath, `${JSON.stringify(header)}\n`, "utf8");
|
|
264
|
+
return header;
|
|
265
|
+
}
|
|
266
|
+
const header = readSessionHeaderFast(this.sessionFilePath);
|
|
267
|
+
return header ?? createSessionHeader(resolve(cwdOverride ?? process.cwd()));
|
|
268
|
+
}
|
|
269
|
+
loadTailEntries() {
|
|
270
|
+
const result = readTailSessionEntries(this.sessionFilePath, this.tailEntryCount);
|
|
271
|
+
this.entries = result.entries;
|
|
272
|
+
this.tailStartOffset = result.startOffset;
|
|
273
|
+
this.rebuildIndexes();
|
|
274
|
+
}
|
|
275
|
+
rebuildIndexes() {
|
|
276
|
+
this.byId.clear();
|
|
277
|
+
this.labelsById.clear();
|
|
278
|
+
this.labelTimestampsById.clear();
|
|
279
|
+
this.leafId = null;
|
|
280
|
+
for (const entry of this.entries) {
|
|
281
|
+
this.byId.set(entry.id, entry);
|
|
282
|
+
this.leafId = entry.id;
|
|
283
|
+
if (entry.type === "label") {
|
|
284
|
+
if (entry.label) {
|
|
285
|
+
this.labelsById.set(entry.targetId, entry.label);
|
|
286
|
+
this.labelTimestampsById.set(entry.targetId, entry.timestamp);
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
this.labelsById.delete(entry.targetId);
|
|
290
|
+
this.labelTimestampsById.delete(entry.targetId);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
appendEntry(entry) {
|
|
296
|
+
this.entries.push(entry);
|
|
297
|
+
this.byId.set(entry.id, entry);
|
|
298
|
+
this.leafId = entry.id;
|
|
299
|
+
appendFileSync(this.sessionFilePath, `${JSON.stringify(entry)}\n`, "utf8");
|
|
300
|
+
return entry.id;
|
|
301
|
+
}
|
|
302
|
+
newEntry(type, payload) {
|
|
303
|
+
return {
|
|
304
|
+
type,
|
|
305
|
+
id: this.createEntryId(),
|
|
306
|
+
parentId: this.leafId,
|
|
307
|
+
timestamp: new Date().toISOString(),
|
|
308
|
+
...payload,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
createEntryId() {
|
|
312
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
313
|
+
const id = randomUUID().slice(0, 8);
|
|
314
|
+
if (!this.byId.has(id))
|
|
315
|
+
return id;
|
|
316
|
+
}
|
|
317
|
+
return randomUUID();
|
|
318
|
+
}
|
|
319
|
+
contextEntries() {
|
|
320
|
+
const entries = this.entries.filter((entry) => entry.type !== "label");
|
|
321
|
+
const start = contextStartIndex(entries);
|
|
322
|
+
const selected = entries.slice(start);
|
|
323
|
+
return selected.map((entry, index) => ({
|
|
324
|
+
...entry,
|
|
325
|
+
parentId: index === 0 ? null : selected[index - 1].id,
|
|
326
|
+
}));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function createSessionHeader(cwd) {
|
|
330
|
+
return {
|
|
331
|
+
type: "session",
|
|
332
|
+
version: CURRENT_SESSION_VERSION,
|
|
333
|
+
id: createSessionId(),
|
|
334
|
+
timestamp: new Date().toISOString(),
|
|
335
|
+
cwd,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function createSessionId() {
|
|
339
|
+
return randomUUID();
|
|
340
|
+
}
|
|
341
|
+
function readSessionHeaderFast(filePath) {
|
|
342
|
+
const line = readFirstLine(filePath, 64 * 1024);
|
|
343
|
+
if (!line)
|
|
344
|
+
return undefined;
|
|
345
|
+
try {
|
|
346
|
+
const parsed = JSON.parse(line);
|
|
347
|
+
if (!isRecord(parsed) || parsed.type !== "session" || typeof parsed.id !== "string")
|
|
348
|
+
return undefined;
|
|
349
|
+
const header = parsed;
|
|
350
|
+
return typeof header.cwd === "string" ? header : { ...header, cwd: process.cwd() };
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
return undefined;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function readFirstLine(filePath, maxBytes) {
|
|
357
|
+
let fd;
|
|
358
|
+
try {
|
|
359
|
+
fd = openSync(filePath, "r");
|
|
360
|
+
const buffer = Buffer.alloc(maxBytes);
|
|
361
|
+
const bytesRead = readSync(fd, buffer, 0, buffer.length, 0);
|
|
362
|
+
const text = buffer.toString("utf8", 0, bytesRead);
|
|
363
|
+
return text.split("\n")[0];
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
return undefined;
|
|
367
|
+
}
|
|
368
|
+
finally {
|
|
369
|
+
if (fd !== undefined)
|
|
370
|
+
closeSync(fd);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
function readFirstSessionEntryOffset(filePath) {
|
|
374
|
+
let fd;
|
|
375
|
+
try {
|
|
376
|
+
fd = openSync(filePath, "r");
|
|
377
|
+
const buffer = Buffer.alloc(64 * 1024);
|
|
378
|
+
const bytesRead = readSync(fd, buffer, 0, buffer.length, 0);
|
|
379
|
+
const entries = parseSessionEntryBufferLines(buffer.subarray(0, bytesRead), 0);
|
|
380
|
+
return entries[0]?.offset ?? 0;
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
return 0;
|
|
384
|
+
}
|
|
385
|
+
finally {
|
|
386
|
+
if (fd !== undefined)
|
|
387
|
+
closeSync(fd);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
function readTailSessionEntries(filePath, limit) {
|
|
391
|
+
if (!existsSync(filePath))
|
|
392
|
+
return { entries: [], startOffset: 0 };
|
|
393
|
+
const size = statSync(filePath).size;
|
|
394
|
+
if (size <= 0)
|
|
395
|
+
return { entries: [], startOffset: 0 };
|
|
396
|
+
let byteCount = Math.min(size, INITIAL_TAIL_BYTES);
|
|
397
|
+
const maxBytes = Math.min(size, MAX_TAIL_BYTES);
|
|
398
|
+
while (byteCount <= maxBytes) {
|
|
399
|
+
const result = readTailSessionEntriesWithByteCount(filePath, byteCount, limit);
|
|
400
|
+
if (result.entries.length >= limit || byteCount >= maxBytes || byteCount >= size)
|
|
401
|
+
return result;
|
|
402
|
+
byteCount = Math.min(size, Math.max(byteCount + 1, byteCount * 2));
|
|
403
|
+
}
|
|
404
|
+
return { entries: [], startOffset: 0 };
|
|
405
|
+
}
|
|
406
|
+
function readTailSessionEntriesWithByteCount(filePath, byteCount, limit) {
|
|
407
|
+
let fd;
|
|
408
|
+
try {
|
|
409
|
+
const size = statSync(filePath).size;
|
|
410
|
+
const start = Math.max(0, size - byteCount);
|
|
411
|
+
const buffer = Buffer.alloc(size - start);
|
|
412
|
+
fd = openSync(filePath, "r");
|
|
413
|
+
readSync(fd, buffer, 0, buffer.length, start);
|
|
414
|
+
let parseStart = 0;
|
|
415
|
+
if (start > 0) {
|
|
416
|
+
const firstNewline = buffer.indexOf(10);
|
|
417
|
+
parseStart = firstNewline >= 0 ? firstNewline + 1 : buffer.length;
|
|
418
|
+
}
|
|
419
|
+
return selectLastSessionEntries(parseSessionEntryBufferLines(buffer.subarray(parseStart), start + parseStart), limit, size);
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
return { entries: [], startOffset: 0 };
|
|
423
|
+
}
|
|
424
|
+
finally {
|
|
425
|
+
if (fd !== undefined)
|
|
426
|
+
closeSync(fd);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
async function readSessionEntriesBeforeOffset(filePath, endOffset, limit) {
|
|
430
|
+
if (!existsSync(filePath) || endOffset <= 0)
|
|
431
|
+
return { entries: [], startOffset: 0 };
|
|
432
|
+
let byteCount = Math.min(endOffset, INITIAL_TAIL_BYTES);
|
|
433
|
+
const maxBytes = Math.min(endOffset, MAX_TAIL_BYTES);
|
|
434
|
+
while (byteCount <= maxBytes) {
|
|
435
|
+
const result = await readSessionEntriesBeforeOffsetWithByteCount(filePath, endOffset, byteCount, limit);
|
|
436
|
+
if (result.entries.length >= limit || byteCount >= maxBytes || byteCount >= endOffset)
|
|
437
|
+
return result;
|
|
438
|
+
byteCount = Math.min(endOffset, Math.max(byteCount + 1, byteCount * 2));
|
|
439
|
+
}
|
|
440
|
+
return { entries: [], startOffset: 0 };
|
|
441
|
+
}
|
|
442
|
+
async function readSessionEntriesBeforeOffsetWithByteCount(filePath, endOffset, byteCount, limit) {
|
|
443
|
+
let file;
|
|
444
|
+
try {
|
|
445
|
+
const start = Math.max(0, endOffset - byteCount);
|
|
446
|
+
const buffer = Buffer.alloc(endOffset - start);
|
|
447
|
+
file = await openFile(filePath, "r");
|
|
448
|
+
await file.read(buffer, 0, buffer.length, start);
|
|
449
|
+
let parseStart = 0;
|
|
450
|
+
if (start > 0) {
|
|
451
|
+
const firstNewline = buffer.indexOf(10);
|
|
452
|
+
parseStart = firstNewline >= 0 ? firstNewline + 1 : buffer.length;
|
|
453
|
+
}
|
|
454
|
+
return selectLastSessionEntries(parseSessionEntryBufferLines(buffer.subarray(parseStart), start + parseStart), limit, start);
|
|
455
|
+
}
|
|
456
|
+
catch {
|
|
457
|
+
return { entries: [], startOffset: 0 };
|
|
458
|
+
}
|
|
459
|
+
finally {
|
|
460
|
+
await file?.close();
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function selectLastSessionEntries(parsedEntries, limit, emptyStartOffset) {
|
|
464
|
+
const selected = parsedEntries.slice(-limit);
|
|
465
|
+
return {
|
|
466
|
+
entries: selected.map((item) => item.entry),
|
|
467
|
+
startOffset: selected[0]?.offset ?? emptyStartOffset,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
function parseSessionEntryBufferLines(buffer, baseOffset) {
|
|
471
|
+
const entries = [];
|
|
472
|
+
let lineStart = 0;
|
|
473
|
+
for (let index = 0; index <= buffer.length; index += 1) {
|
|
474
|
+
if (index < buffer.length && buffer[index] !== 10)
|
|
475
|
+
continue;
|
|
476
|
+
const lineEnd = index > lineStart && buffer[index - 1] === 13 ? index - 1 : index;
|
|
477
|
+
const entry = parseSessionEntryLine(buffer.toString("utf8", lineStart, lineEnd));
|
|
478
|
+
if (entry)
|
|
479
|
+
entries.push({ entry, offset: baseOffset + lineStart });
|
|
480
|
+
lineStart = index + 1;
|
|
481
|
+
}
|
|
482
|
+
return entries;
|
|
483
|
+
}
|
|
484
|
+
function parseSessionEntryLine(line) {
|
|
485
|
+
const trimmed = line.trim();
|
|
486
|
+
if (!trimmed)
|
|
487
|
+
return undefined;
|
|
488
|
+
try {
|
|
489
|
+
const parsed = JSON.parse(trimmed);
|
|
490
|
+
if (!isRecord(parsed) || parsed.type === "session" || typeof parsed.id !== "string")
|
|
491
|
+
return undefined;
|
|
492
|
+
return parsed;
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
return undefined;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
async function readAllSessionEntries(filePath) {
|
|
499
|
+
if (!existsSync(filePath))
|
|
500
|
+
return [];
|
|
501
|
+
const entries = [];
|
|
502
|
+
const lines = createInterface({ input: createReadStream(filePath, { encoding: "utf8" }), crlfDelay: Infinity });
|
|
503
|
+
for await (const line of lines) {
|
|
504
|
+
const entry = parseSessionEntryLine(line);
|
|
505
|
+
if (entry)
|
|
506
|
+
entries.push(entry);
|
|
507
|
+
}
|
|
508
|
+
return entries;
|
|
509
|
+
}
|
|
510
|
+
function branchEntries(entries, leafId) {
|
|
511
|
+
if (!leafId)
|
|
512
|
+
return [...entries];
|
|
513
|
+
const byId = new Map(entries.map((entry) => [entry.id, entry]));
|
|
514
|
+
const branch = [];
|
|
515
|
+
const seen = new Set();
|
|
516
|
+
let cursor = leafId;
|
|
517
|
+
while (cursor && !seen.has(cursor)) {
|
|
518
|
+
seen.add(cursor);
|
|
519
|
+
const entry = byId.get(cursor);
|
|
520
|
+
if (!entry)
|
|
521
|
+
break;
|
|
522
|
+
branch.push(entry);
|
|
523
|
+
cursor = entry.parentId;
|
|
524
|
+
}
|
|
525
|
+
return branch.reverse();
|
|
526
|
+
}
|
|
527
|
+
function contextStartIndex(entries) {
|
|
528
|
+
const userIndex = entries.findIndex((entry) => entry.type === "message" && entry.message.role === "user");
|
|
529
|
+
if (userIndex < 0)
|
|
530
|
+
return 0;
|
|
531
|
+
let start = userIndex;
|
|
532
|
+
while (start > 0) {
|
|
533
|
+
const previous = entries[start - 1];
|
|
534
|
+
if (!previous || (previous.type !== "model_change" && previous.type !== "thinking_level_change" && previous.type !== "compaction" && previous.type !== "branch_summary" && previous.type !== "custom"))
|
|
535
|
+
break;
|
|
536
|
+
start -= 1;
|
|
537
|
+
}
|
|
538
|
+
return start;
|
|
539
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AgentSession, SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
export declare const PIX_SYSTEM_MESSAGE_CUSTOM_TYPE = "pix-system";
|
|
3
|
+
export declare const PIX_SYSTEM_DISPLAY_ENTRY_CUSTOM_TYPE = "pix:system_message";
|
|
4
|
+
export declare const PIX_SESSION_ENTRY_ID_FIELD = "__pixSessionEntryId";
|
|
5
|
+
export declare function appendPixSystemDisplayEntry(session: AgentSession, text: string): void;
|
|
6
|
+
export declare function sessionHistoryDisplayMessages(session: AgentSession): readonly unknown[];
|
|
7
|
+
export type SessionHistoryOlderMessagesReader = {
|
|
8
|
+
hasOlder(): boolean;
|
|
9
|
+
readOlder(limit: number): Promise<readonly unknown[]>;
|
|
10
|
+
};
|
|
11
|
+
export declare function sessionHistoryOlderMessagesReader(session: AgentSession): SessionHistoryOlderMessagesReader | undefined;
|
|
12
|
+
export declare function sessionHistoryDisplayMessagesFromEntries(branch: readonly SessionEntry[]): readonly unknown[];
|
|
13
|
+
export type FullBranchSessionEntryReader = {
|
|
14
|
+
readFullBranchEntries(): Promise<readonly SessionEntry[]>;
|
|
15
|
+
};
|
|
16
|
+
export declare function sessionHistoryFullBranchEntries(session: AgentSession): Promise<readonly SessionEntry[]>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { isRecord } from "../guards.js";
|
|
2
|
+
export const PIX_SYSTEM_MESSAGE_CUSTOM_TYPE = "pix-system";
|
|
3
|
+
export const PIX_SYSTEM_DISPLAY_ENTRY_CUSTOM_TYPE = "pix:system_message";
|
|
4
|
+
export const PIX_SESSION_ENTRY_ID_FIELD = "__pixSessionEntryId";
|
|
5
|
+
export function appendPixSystemDisplayEntry(session, text) {
|
|
6
|
+
const trimmed = text.trim();
|
|
7
|
+
if (!trimmed)
|
|
8
|
+
return;
|
|
9
|
+
session.sessionManager.appendCustomEntry(PIX_SYSTEM_DISPLAY_ENTRY_CUSTOM_TYPE, { text: trimmed });
|
|
10
|
+
}
|
|
11
|
+
export function sessionHistoryDisplayMessages(session) {
|
|
12
|
+
const branch = session.sessionManager.getBranch();
|
|
13
|
+
if (branch.length === 0)
|
|
14
|
+
return session.messages;
|
|
15
|
+
return sessionHistoryDisplayMessagesFromEntries(branch);
|
|
16
|
+
}
|
|
17
|
+
export function sessionHistoryOlderMessagesReader(session) {
|
|
18
|
+
const reader = session.sessionManager.createHistoryReader?.();
|
|
19
|
+
if (!reader)
|
|
20
|
+
return undefined;
|
|
21
|
+
return {
|
|
22
|
+
hasOlder: () => reader.hasOlder(),
|
|
23
|
+
readOlder: async (limit) => sessionHistoryDisplayMessagesFromEntries(await reader.readOlder(limit)),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function sessionHistoryDisplayMessagesFromEntries(branch) {
|
|
27
|
+
const messages = [];
|
|
28
|
+
for (const entry of branch) {
|
|
29
|
+
if (entry.type === "message") {
|
|
30
|
+
messages.push(withSessionEntryId(entry.message, entry.id));
|
|
31
|
+
}
|
|
32
|
+
else if (entry.type === "custom_message") {
|
|
33
|
+
messages.push({
|
|
34
|
+
role: "custom",
|
|
35
|
+
customType: entry.customType,
|
|
36
|
+
content: entry.content,
|
|
37
|
+
display: entry.display,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
else if (isPixSystemDisplayEntry(entry)) {
|
|
41
|
+
messages.push({
|
|
42
|
+
role: "custom",
|
|
43
|
+
customType: PIX_SYSTEM_MESSAGE_CUSTOM_TYPE,
|
|
44
|
+
content: entry.data.text,
|
|
45
|
+
display: true,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return messages;
|
|
50
|
+
}
|
|
51
|
+
export async function sessionHistoryFullBranchEntries(session) {
|
|
52
|
+
const reader = session.sessionManager;
|
|
53
|
+
return await reader.readFullBranchEntries?.() ?? session.sessionManager.getBranch();
|
|
54
|
+
}
|
|
55
|
+
function withSessionEntryId(message, entryId) {
|
|
56
|
+
return isRecord(message) ? { ...message, [PIX_SESSION_ENTRY_ID_FIELD]: entryId } : message;
|
|
57
|
+
}
|
|
58
|
+
function isPixSystemDisplayEntry(entry) {
|
|
59
|
+
return entry.type === "custom"
|
|
60
|
+
&& entry.customType === PIX_SYSTEM_DISPLAY_ENTRY_CUSTOM_TYPE
|
|
61
|
+
&& isRecord(entry.data)
|
|
62
|
+
&& typeof entry.data.text === "string"
|
|
63
|
+
&& entry.data.text.trim().length > 0;
|
|
64
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type FuzzyMatch } from "../../fuzzy.js";
|
|
1
2
|
export type RequestHistoryHost = {
|
|
2
3
|
readonly noSession: boolean;
|
|
3
4
|
getInput(): string;
|
|
@@ -5,6 +6,7 @@ export type RequestHistoryHost = {
|
|
|
5
6
|
resetInputMenuDismissals(): void;
|
|
6
7
|
render(): void;
|
|
7
8
|
};
|
|
9
|
+
export type RequestHistorySearchMatch = FuzzyMatch<string>;
|
|
8
10
|
export declare class AppRequestHistory {
|
|
9
11
|
private readonly host;
|
|
10
12
|
private entries;
|
|
@@ -13,6 +15,8 @@ export declare class AppRequestHistory {
|
|
|
13
15
|
constructor(host: RequestHistoryHost);
|
|
14
16
|
load(): Promise<void>;
|
|
15
17
|
add(text: string): void;
|
|
18
|
+
search(query: string, limit?: number): string[];
|
|
19
|
+
searchMatches(query: string, limit?: number): RequestHistorySearchMatch[];
|
|
16
20
|
resetNavigation(): void;
|
|
17
21
|
navigate(direction: -1 | 1): boolean;
|
|
18
22
|
private save;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { fuzzySearch } from "../../fuzzy.js";
|
|
4
5
|
import { REQUEST_HISTORY_MAX_BYTES, REQUEST_HISTORY_MAX_ENTRIES, REQUEST_HISTORY_MAX_ENTRY_BYTES, REQUEST_HISTORY_VERSION, } from "../constants.js";
|
|
5
6
|
import { isRecord } from "../guards.js";
|
|
6
7
|
export class AppRequestHistory {
|
|
@@ -40,6 +41,16 @@ export class AppRequestHistory {
|
|
|
40
41
|
this.entries = this.limited([...this.entries, normalized]);
|
|
41
42
|
void this.save();
|
|
42
43
|
}
|
|
44
|
+
search(query, limit = 50) {
|
|
45
|
+
return this.searchMatches(query, limit).map((match) => match.value);
|
|
46
|
+
}
|
|
47
|
+
searchMatches(query, limit = 50) {
|
|
48
|
+
const items = [...this.entries].reverse().map((entry) => ({
|
|
49
|
+
value: entry,
|
|
50
|
+
label: entry,
|
|
51
|
+
}));
|
|
52
|
+
return fuzzySearch(items, query, { limit, minScorePerCharacter: 8, preferKeyboardLayoutMatches: true });
|
|
53
|
+
}
|
|
43
54
|
resetNavigation() {
|
|
44
55
|
this.cursor = undefined;
|
|
45
56
|
this.draft = "";
|