pi-ui-extend 0.1.13 → 0.1.15

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.
Files changed (92) hide show
  1. package/README.md +1 -1
  2. package/dist/app/app.d.ts +5 -0
  3. package/dist/app/app.js +82 -12
  4. package/dist/app/commands/command-controller.js +1 -0
  5. package/dist/app/commands/command-host.d.ts +3 -0
  6. package/dist/app/commands/command-model-actions.d.ts +2 -0
  7. package/dist/app/commands/command-model-actions.js +40 -4
  8. package/dist/app/commands/command-navigation-actions.js +3 -0
  9. package/dist/app/commands/command-registry.d.ts +1 -0
  10. package/dist/app/commands/command-registry.js +8 -0
  11. package/dist/app/extensions/extension-ui-controller.d.ts +16 -5
  12. package/dist/app/extensions/extension-ui-controller.js +99 -61
  13. package/dist/app/input/input-action-controller.d.ts +1 -0
  14. package/dist/app/input/input-action-controller.js +8 -2
  15. package/dist/app/logger.d.ts +25 -0
  16. package/dist/app/logger.js +90 -0
  17. package/dist/app/model/model-usage-status.js +30 -15
  18. package/dist/app/popup/menu-items-controller.d.ts +2 -0
  19. package/dist/app/popup/menu-items-controller.js +45 -6
  20. package/dist/app/popup/popup-action-controller.d.ts +2 -1
  21. package/dist/app/popup/popup-action-controller.js +7 -4
  22. package/dist/app/popup/popup-menu-controller.d.ts +36 -23
  23. package/dist/app/popup/popup-menu-controller.js +68 -322
  24. package/dist/app/rendering/conversation-entry-renderer.js +3 -3
  25. package/dist/app/rendering/conversation-viewport.d.ts +10 -2
  26. package/dist/app/rendering/conversation-viewport.js +157 -16
  27. package/dist/app/rendering/editor-panels.js +4 -2
  28. package/dist/app/rendering/popup-menu-renderer.d.ts +50 -0
  29. package/dist/app/rendering/popup-menu-renderer.js +307 -0
  30. package/dist/app/rendering/render-controller.js +5 -13
  31. package/dist/app/rendering/status-line-renderer.d.ts +1 -1
  32. package/dist/app/rendering/status-line-renderer.js +27 -24
  33. package/dist/app/rendering/toast-controller.d.ts +11 -3
  34. package/dist/app/rendering/toast-controller.js +53 -12
  35. package/dist/app/runtime.d.ts +2 -1
  36. package/dist/app/runtime.js +20 -10
  37. package/dist/app/screen/mouse-controller.d.ts +2 -2
  38. package/dist/app/screen/mouse-controller.js +27 -48
  39. package/dist/app/screen/screen-styler.d.ts +1 -1
  40. package/dist/app/screen/screen-styler.js +9 -7
  41. package/dist/app/screen/scroll-controller.d.ts +11 -9
  42. package/dist/app/screen/scroll-controller.js +50 -45
  43. package/dist/app/session/lazy-session-manager.d.ts +11 -0
  44. package/dist/app/session/lazy-session-manager.js +539 -0
  45. package/dist/app/session/pix-system-message.d.ts +16 -0
  46. package/dist/app/session/pix-system-message.js +64 -0
  47. package/dist/app/session/session-event-controller.d.ts +11 -0
  48. package/dist/app/session/session-event-controller.js +58 -2
  49. package/dist/app/session/session-history.d.ts +18 -0
  50. package/dist/app/session/session-history.js +72 -3
  51. package/dist/app/session/session-lifecycle-controller.d.ts +6 -2
  52. package/dist/app/session/session-lifecycle-controller.js +7 -2
  53. package/dist/app/session/tabs-controller.d.ts +13 -1
  54. package/dist/app/session/tabs-controller.js +248 -27
  55. package/dist/app/todo/todo-model.d.ts +3 -1
  56. package/dist/app/todo/todo-model.js +14 -2
  57. package/dist/app/types.d.ts +5 -2
  58. package/dist/app/workspace/workspace-actions-controller.d.ts +2 -0
  59. package/dist/app/workspace/workspace-actions-controller.js +12 -0
  60. package/dist/config.d.ts +5 -1
  61. package/dist/config.js +73 -25
  62. package/dist/default-pix-config.js +2 -0
  63. package/dist/schemas/pi-tools-suite-schema.d.ts +1 -0
  64. package/dist/schemas/pi-tools-suite-schema.js +1 -0
  65. package/dist/schemas/pix-schema.d.ts +2 -1
  66. package/dist/schemas/pix-schema.js +5 -4
  67. package/dist/terminal-width.d.ts +2 -0
  68. package/dist/terminal-width.js +64 -3
  69. package/external/pi-tools-suite/README.md +1 -0
  70. package/external/pi-tools-suite/src/antigravity-auth/auth-store.ts +12 -3
  71. package/external/pi-tools-suite/src/antigravity-auth/commands.ts +2 -4
  72. package/external/pi-tools-suite/src/antigravity-auth/constants.ts +2 -2
  73. package/external/pi-tools-suite/src/antigravity-auth/index.ts +8 -2
  74. package/external/pi-tools-suite/src/antigravity-auth/oauth.ts +102 -50
  75. package/external/pi-tools-suite/src/antigravity-auth/status.ts +81 -2
  76. package/external/pi-tools-suite/src/antigravity-auth/stream.ts +29 -8
  77. package/external/pi-tools-suite/src/config.ts +8 -0
  78. package/external/pi-tools-suite/src/dcp/index.ts +16 -1
  79. package/external/pi-tools-suite/src/dcp/state.ts +35 -0
  80. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +3 -0
  81. package/external/pi-tools-suite/src/todo/index.ts +181 -11
  82. package/external/pi-tools-suite/src/todo/state/state-reducer.ts +23 -10
  83. package/external/pi-tools-suite/src/todo/todo.ts +10 -5
  84. package/external/pi-tools-suite/src/todo/tool/response-envelope.ts +33 -6
  85. package/external/pi-tools-suite/src/todo/tool/types.ts +9 -1
  86. package/external/pi-tools-suite/src/todo/view/format.ts +2 -1
  87. package/external/pi-tools-suite/src/tool-descriptions.ts +2 -1
  88. package/external/pi-tools-suite/src/usage/index.ts +5 -2
  89. package/external/pi-tools-suite/src/usage/lib/google.ts +6 -13
  90. package/package.json +1 -1
  91. package/schemas/pi-tools-suite.json +4 -0
  92. package/schemas/pix.json +6 -2
@@ -3,6 +3,7 @@ export class AppScrollController {
3
3
  host;
4
4
  scrollFromBottom = 0;
5
5
  detachedScrollStart;
6
+ olderHistoryThresholdLines = 8;
6
7
  constructor(host) {
7
8
  this.host = host;
8
9
  }
@@ -37,20 +38,6 @@ export class AppScrollController {
37
38
  }
38
39
  return { bodyHeight, viewportColumns, conversationLineCount, maxScroll, start };
39
40
  }
40
- scrollBarForMetrics(metrics) {
41
- if (metrics.bodyHeight <= 0 || metrics.maxScroll <= 0 || metrics.conversationLineCount <= metrics.bodyHeight)
42
- return undefined;
43
- const thumbSize = Math.max(1, Math.min(metrics.bodyHeight, Math.round((metrics.bodyHeight * metrics.bodyHeight) / metrics.conversationLineCount)));
44
- const travel = Math.max(0, metrics.bodyHeight - thumbSize);
45
- const thumbOffset = travel === 0 ? 0 : Math.round((metrics.start / metrics.maxScroll) * travel);
46
- return {
47
- thumbStartRow: thumbOffset + 1,
48
- thumbEndRow: thumbOffset + thumbSize,
49
- };
50
- }
51
- scrollBarMetrics(columns, bodyHeight) {
52
- return this.scrollBarForMetrics(this.scrollMetrics(columns, bodyHeight));
53
- }
54
41
  scrollByPage(direction) {
55
42
  const rows = this.host.terminalRows();
56
43
  this.scrollByLines(direction * Math.max(1, editorLayoutRows(rows, this.host.tabPanelRows(rows)) - 4));
@@ -62,37 +49,64 @@ export class AppScrollController {
62
49
  const rows = editorLayoutRows(terminalRows, this.host.tabPanelRows(terminalRows));
63
50
  const { bodyHeight } = this.host.editorLayoutRenderer().computeLayout(columns, rows);
64
51
  const metrics = this.scrollMetrics(columns, bodyHeight);
52
+ const shouldLoadOlderHistory = this.shouldLoadOlderHistory(delta, metrics);
65
53
  const { conversationLineCount, maxScroll } = metrics;
66
54
  const nextScrollFromBottom = Math.max(0, Math.min(maxScroll, this.scrollFromBottom + -delta));
55
+ let changed = false;
67
56
  if (nextScrollFromBottom === this.scrollFromBottom) {
68
57
  if (nextScrollFromBottom === 0 && this.detachedScrollStart !== undefined && delta > 0) {
69
58
  this.detachedScrollStart = undefined;
70
- if (shouldRender)
71
- this.host.render();
72
- return true;
59
+ changed = true;
60
+ }
61
+ else if (!shouldLoadOlderHistory) {
62
+ return false;
73
63
  }
74
- return false;
75
64
  }
76
- this.scrollFromBottom = nextScrollFromBottom;
77
- this.detachedScrollStart = nextScrollFromBottom === 0
78
- ? undefined
79
- : Math.max(0, conversationLineCount - bodyHeight - nextScrollFromBottom);
65
+ else {
66
+ this.scrollFromBottom = nextScrollFromBottom;
67
+ this.detachedScrollStart = nextScrollFromBottom === 0
68
+ ? undefined
69
+ : Math.max(0, conversationLineCount - bodyHeight - nextScrollFromBottom);
70
+ changed = true;
71
+ }
72
+ if (shouldLoadOlderHistory)
73
+ this.loadOlderHistoryAnchored(metrics, { render: shouldRender });
80
74
  if (shouldRender)
81
75
  this.host.render();
82
- return true;
76
+ return changed || shouldLoadOlderHistory;
83
77
  }
84
- scrollToScrollbarPosition(bodyRow) {
85
- const columns = this.host.terminalColumns();
86
- const terminalRows = this.host.terminalRows();
87
- const rows = editorLayoutRows(terminalRows, this.host.tabPanelRows(terminalRows));
88
- const { bodyHeight } = this.host.editorLayoutRenderer().computeLayout(columns, rows);
89
- const metrics = this.scrollMetrics(columns, bodyHeight);
90
- if (!this.scrollBarForMetrics(metrics))
78
+ shouldLoadOlderHistory(delta, metrics) {
79
+ if (delta >= 0)
80
+ return false;
81
+ if (metrics.start > this.olderHistoryThresholdLines)
82
+ return false;
83
+ if (this.host.hasOlderSessionHistory?.() !== true)
84
+ return false;
85
+ if (this.host.isLoadingOlderSessionHistory?.() === true)
91
86
  return false;
92
- const clampedRow = Math.max(0, Math.min(Math.max(0, bodyHeight - 1), bodyRow));
93
- const ratio = bodyHeight <= 1 ? 0 : clampedRow / (bodyHeight - 1);
94
- const start = Math.round(metrics.maxScroll * ratio);
95
- return this.scrollToStart(start, metrics);
87
+ return true;
88
+ }
89
+ loadOlderHistoryAnchored(metrics, options) {
90
+ void this.host.loadOlderSessionHistory?.({
91
+ render: false,
92
+ onPrependedEntries: (entries) => {
93
+ const prependedLineCount = this.host.conversationViewport().measuredLineCountForEntries(metrics.viewportColumns, entries.map((entry) => entry.id));
94
+ if (prependedLineCount > 0 && this.detachedScrollStart !== undefined)
95
+ this.detachedScrollStart += prependedLineCount;
96
+ },
97
+ }).then((loaded) => {
98
+ if (loaded && options.render)
99
+ this.host.render();
100
+ });
101
+ }
102
+ adjustForHistoryWindowPrune(edge, lineCount) {
103
+ if (lineCount <= 0)
104
+ return;
105
+ if (edge !== "top")
106
+ return;
107
+ if (this.detachedScrollStart === undefined)
108
+ return;
109
+ this.detachedScrollStart = Math.max(0, this.detachedScrollStart - lineCount);
96
110
  }
97
111
  scrollToConversationEntry(entryId) {
98
112
  const columns = this.host.terminalColumns();
@@ -143,12 +157,6 @@ export class AppScrollController {
143
157
  }
144
158
  return false;
145
159
  }
146
- scrollToStart(start, metrics) {
147
- if (!this.setScrollStart(start, metrics))
148
- return false;
149
- this.host.render();
150
- return true;
151
- }
152
160
  setScrollStart(start, metrics) {
153
161
  const nextStart = Math.max(0, Math.min(metrics.maxScroll, start));
154
162
  const nextScrollFromBottom = Math.max(0, metrics.conversationLineCount - metrics.bodyHeight - nextStart);
@@ -158,12 +166,9 @@ export class AppScrollController {
158
166
  this.detachedScrollStart = nextDetachedScrollStart;
159
167
  return changed;
160
168
  }
161
- viewportColumns(columns, bodyHeight) {
169
+ viewportColumns(columns, _bodyHeight) {
162
170
  const safeColumns = Math.max(1, columns);
163
- if (safeColumns <= 1 || bodyHeight <= 0)
164
- return safeColumns;
165
- const lineCountWithoutScrollbar = this.host.conversationViewport().lineCount(safeColumns);
166
- return lineCountWithoutScrollbar > bodyHeight ? safeColumns - 1 : safeColumns;
171
+ return safeColumns;
167
172
  }
168
173
  }
169
174
  function editorLayoutRows(terminalRows, tabPanelRows) {
@@ -0,0 +1,11 @@
1
+ import { SessionManager, type SessionEntry } from "@earendil-works/pi-coding-agent";
2
+ export type LazySessionManagerOptions = {
3
+ cwdOverride?: string;
4
+ sessionDir?: string;
5
+ tailEntryCount?: number;
6
+ };
7
+ export type LazySessionHistoryReader = {
8
+ hasOlder(): boolean;
9
+ readOlder(limit: number): Promise<SessionEntry[]>;
10
+ };
11
+ export declare function openLazySessionManager(sessionPath: string, options?: LazySessionManagerOptions): SessionManager;
@@ -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
+ }