@selesai/code 0.13.26 → 0.13.27
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/CHANGELOG.md +74 -0
- package/dist/cli/args.js +1 -0
- package/dist/cli/session-picker.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/core/agent-session-auto-handoff.test.js +2 -1
- package/dist/core/agent-session-length-continuation.test.js +2 -1
- package/dist/core/agent-session.d.ts +58 -7
- package/dist/core/agent-session.js +334 -167
- package/dist/core/bug-report-upload.d.ts +11 -0
- package/dist/core/bug-report-upload.js +21 -0
- package/dist/core/bug-report.d.ts +175 -0
- package/dist/core/bug-report.js +291 -0
- package/dist/core/cache-stats.js +12 -1
- package/dist/core/cache-warmer.d.ts +101 -0
- package/dist/core/cache-warmer.js +341 -0
- package/dist/core/compaction/branch-summarization.js +2 -2
- package/dist/core/compaction/compaction.d.ts +6 -5
- package/dist/core/compaction/compaction.js +49 -42
- package/dist/core/crash-log.d.ts +21 -0
- package/dist/core/crash-log.js +70 -0
- package/dist/core/experimental.d.ts +0 -4
- package/dist/core/experimental.js +0 -4
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/jiti-loader.d.ts +1 -0
- package/dist/core/extensions/jiti-loader.js +3 -0
- package/dist/core/extensions/jiti-static-loader.d.ts +1 -0
- package/dist/core/extensions/jiti-static-loader.js +3 -0
- package/dist/core/extensions/loader.js +39 -49
- package/dist/core/extensions/runner.d.ts +10 -7
- package/dist/core/extensions/runner.js +85 -67
- package/dist/core/extensions/types.d.ts +66 -52
- package/dist/core/extensions/virtual-modules.d.ts +2 -0
- package/dist/core/extensions/virtual-modules.js +39 -0
- package/dist/core/extensions/wrapper.js +1 -20
- package/dist/core/index.d.ts +2 -1
- package/dist/core/keybindings.d.ts +1 -1
- package/dist/core/keybindings.js +1 -1
- package/dist/core/messages.js +1 -0
- package/dist/core/model-config.d.ts +101 -20
- package/dist/core/model-config.js +25 -18
- package/dist/core/model-resolver.js +2 -1
- package/dist/core/model-runtime.js +5 -3
- package/dist/core/provider-composer.d.ts +7 -2
- package/dist/core/provider-composer.js +10 -5
- package/dist/core/radius.d.ts +3 -0
- package/dist/core/radius.js +6 -0
- package/dist/core/sdk.js +60 -38
- package/dist/core/session-export.d.ts +6 -2
- package/dist/core/session-export.js +14 -13
- package/dist/core/session-format.test.d.ts +1 -0
- package/dist/core/session-format.test.js +96 -0
- package/dist/core/session-manager.d.ts +29 -6
- package/dist/core/session-manager.js +159 -96
- package/dist/core/settings-manager.d.ts +22 -5
- package/dist/core/settings-manager.js +56 -23
- package/dist/core/slash-commands.js +1 -0
- package/dist/core/system-prompt.d.ts +49 -4
- package/dist/core/system-prompt.js +155 -102
- package/dist/core/system-prompt.test.js +61 -23
- package/dist/core/tools/bash.d.ts +2 -1
- package/dist/core/tools/bash.js +10 -4
- package/dist/core/tools/edit.js +1 -2
- package/dist/core/tools/read.js +1 -2
- package/dist/core/tools/renderers/bash.js +9 -1
- package/dist/core/tools/write.js +1 -2
- package/dist/core/usage-totals.d.ts +1 -1
- package/dist/core/usage-totals.js +5 -1
- package/dist/extensions/pi-web-agent/src/backends/factory.ts +14 -6
- package/dist/extensions/pi-web-agent/src/extension.ts +3 -1
- package/dist/extensions/pi-web-agent/src/orchestration/answer-synthesizer.ts +7 -3
- package/dist/extensions/pi-web-agent/src/orchestration/research-orchestrator.ts +3 -0
- package/dist/extensions/pi-web-agent/src/orchestration/research-types.ts +2 -0
- package/dist/extensions/pi-web-agent/src/presentation/explore-presentation.ts +4 -2
- package/dist/extensions/pi-web-agent/src/search/tokenin.ts +4 -1
- package/dist/extensions/pi-web-agent/src/tools/web-explore.ts +2 -1
- package/dist/extensions/pi-web-agent/src/types.ts +4 -0
- package/dist/index.d.ts +3 -2
- package/dist/main.js +13 -9
- package/dist/migrations.js +2 -2
- package/dist/modes/interactive/bug-report.d.ts +15 -0
- package/dist/modes/interactive/bug-report.js +205 -0
- package/dist/modes/interactive/chat-viewport.js +1 -1
- package/dist/modes/interactive/components/branch-summary-message.js +12 -5
- package/dist/modes/interactive/components/compaction-summary-message.js +12 -5
- package/dist/modes/interactive/components/custom-editor.d.ts +3 -3
- package/dist/modes/interactive/components/extension-editor.d.ts +4 -1
- package/dist/modes/interactive/components/extension-editor.js +7 -2
- package/dist/modes/interactive/components/extension-input.d.ts +2 -0
- package/dist/modes/interactive/components/extension-input.js +6 -0
- package/dist/modes/interactive/components/extension-selector.d.ts +1 -0
- package/dist/modes/interactive/components/extension-selector.js +4 -0
- package/dist/modes/interactive/components/footer.js +4 -1
- package/dist/modes/interactive/components/session-selector.d.ts +5 -5
- package/dist/modes/interactive/components/session-selector.js +74 -48
- package/dist/modes/interactive/components/settings-selector.d.ts +3 -1
- package/dist/modes/interactive/components/settings-selector.js +11 -0
- package/dist/modes/interactive/components/skill-invocation-message.js +11 -4
- package/dist/modes/interactive/components/status-indicator.d.ts +2 -2
- package/dist/modes/interactive/components/status-indicator.js +7 -7
- package/dist/modes/interactive/components/tool-execution.js +18 -9
- package/dist/modes/interactive/components/tree-selector.js +2 -0
- package/dist/modes/interactive/interactive-mode.d.ts +15 -2
- package/dist/modes/interactive/interactive-mode.js +257 -81
- package/dist/modes/interactive/session-share.d.ts +2 -0
- package/dist/modes/interactive/session-share.js +8 -4
- package/dist/modes/interactive/tui-renderer.js +2 -2
- package/dist/modes/rpc/rpc-mode.js +2 -2
- package/dist/utils/clipboard-command.d.ts +6 -0
- package/dist/utils/clipboard-command.js +44 -0
- package/dist/utils/clipboard-image.js +55 -89
- package/dist/utils/clipboard.d.ts +1 -1
- package/dist/utils/clipboard.js +118 -108
- package/dist/utils/wsl.d.ts +2 -0
- package/dist/utils/wsl.js +14 -0
- package/dist/utils/zip.d.ts +6 -0
- package/dist/utils/zip.js +59 -0
- package/package.json +9 -9
- package/dist/utils/clipboard-native.d.ts +0 -10
- package/dist/utils/clipboard-native.js +0 -19
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { uuidv7 } from "@earendil-works/pi-ai";
|
|
1
|
+
import { getCurrentSystemMessage, uuidv7, } from "@earendil-works/pi-ai";
|
|
2
2
|
import { randomUUID } from "crypto";
|
|
3
3
|
import { appendFileSync, closeSync, createReadStream, existsSync, mkdirSync, openSync, readdirSync, readSync, statSync, writeFileSync, } from "fs";
|
|
4
4
|
import { readdir, stat } from "fs/promises";
|
|
5
|
-
import { join, resolve } from "path";
|
|
5
|
+
import { basename, join, resolve } from "path";
|
|
6
6
|
import { createInterface } from "readline";
|
|
7
7
|
import { StringDecoder } from "string_decoder";
|
|
8
8
|
import { APP_NAME, getAgentDir as getDefaultAgentDir, getSessionsDir } from "../config.js";
|
|
@@ -168,6 +168,8 @@ export function sessionEntryToContextMessages(entry) {
|
|
|
168
168
|
const message = entry.message;
|
|
169
169
|
// Session files are parsed without validation; old versions, forks, or
|
|
170
170
|
// hand-edited files can contain messages with null/missing content.
|
|
171
|
+
if (message.role === "system" && message.content == null)
|
|
172
|
+
return [{ ...message, content: "" }];
|
|
171
173
|
if ((message.role === "user" || message.role === "assistant" || message.role === "toolResult") &&
|
|
172
174
|
message.content == null) {
|
|
173
175
|
return [{ ...message, content: [] }];
|
|
@@ -183,7 +185,8 @@ export function sessionEntryToContextMessages(entry) {
|
|
|
183
185
|
return [createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp)];
|
|
184
186
|
}
|
|
185
187
|
if (entry.type === "compaction") {
|
|
186
|
-
|
|
188
|
+
const summary = createCompactionSummaryMessage(entry.summary, entry.tokensBefore, entry.timestamp);
|
|
189
|
+
return entry.systemMessage ? [entry.systemMessage, summary] : [summary];
|
|
187
190
|
}
|
|
188
191
|
return [];
|
|
189
192
|
}
|
|
@@ -217,7 +220,7 @@ export function buildContextEntries(entries, leafId, byId) {
|
|
|
217
220
|
if (entry.id === compaction.firstKeptEntryId) {
|
|
218
221
|
foundFirstKept = true;
|
|
219
222
|
}
|
|
220
|
-
if (foundFirstKept) {
|
|
223
|
+
if (foundFirstKept && !(entry.type === "message" && entry.message.role === "system")) {
|
|
221
224
|
contextEntries.push(entry);
|
|
222
225
|
}
|
|
223
226
|
}
|
|
@@ -399,14 +402,16 @@ export function findMostRecentSession(sessionDir, cwd) {
|
|
|
399
402
|
const resolvedCwd = cwd ? resolvePath(cwd) : undefined;
|
|
400
403
|
try {
|
|
401
404
|
const files = readdirSync(resolvedSessionDir)
|
|
402
|
-
.filter((
|
|
403
|
-
.map((
|
|
404
|
-
.map((path) => ({ path,
|
|
405
|
-
.
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
405
|
+
.filter((file) => file.endsWith(".jsonl"))
|
|
406
|
+
.map((file) => join(resolvedSessionDir, file))
|
|
407
|
+
.map((path) => ({ path, mtime: statSync(path).mtimeMs }))
|
|
408
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
409
|
+
for (const { path } of files) {
|
|
410
|
+
const header = readSessionHeaderForDiscovery(path);
|
|
411
|
+
if (header && (!resolvedCwd || sessionCwdMatches(getSessionHeaderCwd(header), resolvedCwd)))
|
|
412
|
+
return path;
|
|
413
|
+
}
|
|
414
|
+
return null;
|
|
410
415
|
}
|
|
411
416
|
catch {
|
|
412
417
|
// Directory access and stat races make recent-session discovery unavailable.
|
|
@@ -439,9 +444,9 @@ function getMessageActivityTime(entry) {
|
|
|
439
444
|
const t = new Date(entry.timestamp).getTime();
|
|
440
445
|
return Number.isNaN(t) ? undefined : t;
|
|
441
446
|
}
|
|
442
|
-
async function buildSessionInfo(filePath) {
|
|
447
|
+
async function buildSessionInfo(filePath, signal, fileStats) {
|
|
443
448
|
try {
|
|
444
|
-
const stats = await stat(filePath);
|
|
449
|
+
const stats = fileStats ?? (await stat(filePath));
|
|
445
450
|
let header = null;
|
|
446
451
|
let messageCount = 0;
|
|
447
452
|
let firstMessage = "";
|
|
@@ -449,7 +454,7 @@ async function buildSessionInfo(filePath) {
|
|
|
449
454
|
let name;
|
|
450
455
|
let lastActivityTime;
|
|
451
456
|
const rl = createInterface({
|
|
452
|
-
input: createReadStream(filePath, { encoding: "utf8" }),
|
|
457
|
+
input: createReadStream(filePath, { encoding: "utf8", signal }),
|
|
453
458
|
crlfDelay: Infinity,
|
|
454
459
|
});
|
|
455
460
|
for await (const line of rl) {
|
|
@@ -510,67 +515,63 @@ async function buildSessionInfo(filePath) {
|
|
|
510
515
|
};
|
|
511
516
|
}
|
|
512
517
|
catch {
|
|
518
|
+
signal?.throwIfAborted();
|
|
513
519
|
return null;
|
|
514
520
|
}
|
|
515
521
|
}
|
|
516
522
|
const MAX_CONCURRENT_SESSION_INFO_LOADS = 10;
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
523
|
+
const MAX_CONCURRENT_SESSION_DISCOVERY_LOADS = 64;
|
|
524
|
+
const CURRENT_SESSION_LIST_PUBLISH_INTERVAL = 10;
|
|
525
|
+
const ALL_SESSION_LIST_PUBLISH_INTERVAL = 100;
|
|
526
|
+
async function mapWithConcurrency(items, limit, map, signal) {
|
|
527
|
+
const results = new Array(items.length);
|
|
520
528
|
let nextIndex = 0;
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
let task;
|
|
527
|
-
task = buildSessionInfo(file)
|
|
528
|
-
.then((info) => {
|
|
529
|
-
results[index] = info;
|
|
530
|
-
})
|
|
531
|
-
.catch(() => {
|
|
532
|
-
results[index] = null;
|
|
533
|
-
})
|
|
534
|
-
.finally(() => {
|
|
535
|
-
inFlight.delete(task);
|
|
536
|
-
onLoaded();
|
|
537
|
-
});
|
|
538
|
-
inFlight.add(task);
|
|
539
|
-
};
|
|
540
|
-
while (nextIndex < files.length || inFlight.size > 0) {
|
|
541
|
-
while (nextIndex < files.length && inFlight.size < MAX_CONCURRENT_SESSION_INFO_LOADS) {
|
|
542
|
-
startNext();
|
|
543
|
-
}
|
|
544
|
-
if (inFlight.size > 0) {
|
|
545
|
-
await Promise.race(inFlight);
|
|
529
|
+
const worker = async () => {
|
|
530
|
+
while (nextIndex < items.length) {
|
|
531
|
+
signal?.throwIfAborted();
|
|
532
|
+
const index = nextIndex++;
|
|
533
|
+
results[index] = await map(items[index], index);
|
|
546
534
|
}
|
|
547
|
-
}
|
|
535
|
+
};
|
|
536
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker()));
|
|
548
537
|
return results;
|
|
549
538
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
539
|
+
function sortSessionInfos(sessions) {
|
|
540
|
+
return sessions.sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
|
541
|
+
}
|
|
542
|
+
function buildSessionInfosWithConcurrency(files, onLoaded, signal) {
|
|
543
|
+
return mapWithConcurrency(files, MAX_CONCURRENT_SESSION_INFO_LOADS, async (file, index) => {
|
|
544
|
+
const info = await buildSessionInfo(file.path, signal, file.stats);
|
|
545
|
+
onLoaded(info, index);
|
|
546
|
+
return info;
|
|
547
|
+
}, signal);
|
|
548
|
+
}
|
|
549
|
+
async function listSessionsFromDir(dir, onProgress, signal) {
|
|
550
|
+
signal?.throwIfAborted();
|
|
551
|
+
if (!existsSync(dir))
|
|
552
|
+
return [];
|
|
555
553
|
try {
|
|
556
554
|
const dirEntries = await readdir(dir);
|
|
557
|
-
const files = dirEntries
|
|
558
|
-
|
|
555
|
+
const files = dirEntries
|
|
556
|
+
.filter((file) => file.endsWith(".jsonl"))
|
|
557
|
+
.sort((a, b) => b.localeCompare(a))
|
|
558
|
+
.map((file) => ({ path: join(dir, file) }));
|
|
559
|
+
const total = files.length;
|
|
560
|
+
const partialSessions = [];
|
|
559
561
|
let loaded = 0;
|
|
560
|
-
const results = await buildSessionInfosWithConcurrency(files, () => {
|
|
562
|
+
const results = await buildSessionInfosWithConcurrency(files, (info) => {
|
|
561
563
|
loaded++;
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
}
|
|
564
|
+
if (info)
|
|
565
|
+
partialSessions.push(info);
|
|
566
|
+
const publishPartial = loaded === 1 || loaded % CURRENT_SESSION_LIST_PUBLISH_INTERVAL === 0 || loaded === files.length;
|
|
567
|
+
onProgress?.(loaded, total, publishPartial ? sortSessionInfos([...partialSessions]) : undefined);
|
|
568
|
+
}, signal);
|
|
569
|
+
return results.filter((info) => info !== null);
|
|
569
570
|
}
|
|
570
571
|
catch {
|
|
571
|
-
|
|
572
|
+
signal?.throwIfAborted();
|
|
573
|
+
return [];
|
|
572
574
|
}
|
|
573
|
-
return sessions;
|
|
574
575
|
}
|
|
575
576
|
/**
|
|
576
577
|
* Manages conversation sessions as append-only trees stored in JSONL files.
|
|
@@ -814,19 +815,38 @@ export class SessionManager {
|
|
|
814
815
|
this._appendEntry(entry);
|
|
815
816
|
return entry.id;
|
|
816
817
|
}
|
|
818
|
+
/** Append model-attributed usage that does not participate in LLM context. Returns the appended entry. */
|
|
819
|
+
appendUsage(kind, provider, model, usage, note) {
|
|
820
|
+
const entry = {
|
|
821
|
+
type: "usage",
|
|
822
|
+
id: generateId(this.byId),
|
|
823
|
+
parentId: this.leafId,
|
|
824
|
+
timestamp: new Date().toISOString(),
|
|
825
|
+
kind,
|
|
826
|
+
provider,
|
|
827
|
+
model,
|
|
828
|
+
usage,
|
|
829
|
+
...(note ? { note } : {}),
|
|
830
|
+
};
|
|
831
|
+
this._appendEntry(entry);
|
|
832
|
+
return entry;
|
|
833
|
+
}
|
|
817
834
|
/** Append a compaction summary as child of current leaf, then advance leaf. Returns entry id. */
|
|
818
835
|
appendCompaction(summary, firstKeptEntryId, tokensBefore, details, fromHook, usage) {
|
|
836
|
+
const timestamp = new Date().toISOString();
|
|
837
|
+
const systemMessage = getCurrentSystemMessage(this.buildSessionContext().messages);
|
|
819
838
|
const entry = {
|
|
820
839
|
type: "compaction",
|
|
821
840
|
id: generateId(this.byId),
|
|
822
841
|
parentId: this.leafId,
|
|
823
|
-
timestamp
|
|
842
|
+
timestamp,
|
|
824
843
|
summary,
|
|
825
844
|
firstKeptEntryId,
|
|
826
845
|
tokensBefore,
|
|
827
846
|
details,
|
|
828
847
|
usage,
|
|
829
848
|
fromHook,
|
|
849
|
+
...(systemMessage ? { systemMessage: { ...systemMessage, timestamp: new Date(timestamp).getTime() } } : {}),
|
|
830
850
|
};
|
|
831
851
|
this._appendEntry(entry);
|
|
832
852
|
return entry.id;
|
|
@@ -1301,65 +1321,108 @@ export class SessionManager {
|
|
|
1301
1321
|
}
|
|
1302
1322
|
return new SessionManager(resolvedTargetCwd, dir, newSessionFile, true);
|
|
1303
1323
|
}
|
|
1324
|
+
/**
|
|
1325
|
+
* Find an exact session ID without loading transcript bodies.
|
|
1326
|
+
* @param cwd Working directory (used to compute default session directory)
|
|
1327
|
+
* @param id Exact session ID
|
|
1328
|
+
* @param sessionDir Optional session directory. If omitted, uses default (~/.selesai/agent/sessions/<encoded-cwd>/).
|
|
1329
|
+
*/
|
|
1330
|
+
static findById(cwd, id, sessionDir) {
|
|
1331
|
+
const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(cwd);
|
|
1332
|
+
const filterCwd = sessionDir !== undefined && dir !== getDefaultSessionDirPath(cwd);
|
|
1333
|
+
const resolvedCwd = resolvePath(cwd);
|
|
1334
|
+
try {
|
|
1335
|
+
for (const file of readdirSync(dir)) {
|
|
1336
|
+
if (!file.endsWith(".jsonl"))
|
|
1337
|
+
continue;
|
|
1338
|
+
const path = join(dir, file);
|
|
1339
|
+
const header = readSessionHeaderForDiscovery(path);
|
|
1340
|
+
if (header?.id !== id)
|
|
1341
|
+
continue;
|
|
1342
|
+
if (filterCwd && !sessionCwdMatches(getSessionHeaderCwd(header), resolvedCwd))
|
|
1343
|
+
continue;
|
|
1344
|
+
return path;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
catch {
|
|
1348
|
+
// Exact session discovery is best-effort, matching list().
|
|
1349
|
+
}
|
|
1350
|
+
return undefined;
|
|
1351
|
+
}
|
|
1304
1352
|
/**
|
|
1305
1353
|
* List all sessions for a directory.
|
|
1306
1354
|
* @param cwd Working directory (used to compute default session directory)
|
|
1307
1355
|
* @param sessionDir Optional session directory. If omitted, uses the configured agent sessions directory.
|
|
1308
1356
|
* @param onProgress Optional callback for progress updates (loaded, total)
|
|
1309
1357
|
*/
|
|
1310
|
-
static async list(cwd, sessionDir, onProgress) {
|
|
1358
|
+
static async list(cwd, sessionDir, onProgress, signal) {
|
|
1311
1359
|
const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(cwd);
|
|
1312
1360
|
const filterCwd = sessionDir !== undefined && dir !== getDefaultSessionDirPath(cwd);
|
|
1313
1361
|
const resolvedCwd = resolvePath(cwd);
|
|
1314
|
-
const
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1362
|
+
const includeSession = (session) => !filterCwd || sessionCwdMatches(session.cwd, resolvedCwd);
|
|
1363
|
+
const progress = onProgress
|
|
1364
|
+
? (loaded, total, partialSessions) => onProgress(loaded, total, partialSessions?.filter(includeSession))
|
|
1365
|
+
: undefined;
|
|
1366
|
+
const sessions = (await listSessionsFromDir(dir, progress, signal)).filter(includeSession);
|
|
1367
|
+
return sortSessionInfos(sessions);
|
|
1368
|
+
}
|
|
1369
|
+
static async listAll(sessionDirOrOnProgress, onProgressOrSignal, signal) {
|
|
1319
1370
|
const customSessionDir = typeof sessionDirOrOnProgress === "string" ? normalizePath(sessionDirOrOnProgress) : undefined;
|
|
1320
|
-
const progress = typeof sessionDirOrOnProgress === "function"
|
|
1371
|
+
const progress = typeof sessionDirOrOnProgress === "function"
|
|
1372
|
+
? sessionDirOrOnProgress
|
|
1373
|
+
: typeof onProgressOrSignal === "function"
|
|
1374
|
+
? onProgressOrSignal
|
|
1375
|
+
: undefined;
|
|
1376
|
+
const abortSignal = typeof sessionDirOrOnProgress === "string" || typeof onProgressOrSignal === "function"
|
|
1377
|
+
? signal
|
|
1378
|
+
: (onProgressOrSignal ?? signal);
|
|
1379
|
+
abortSignal?.throwIfAborted();
|
|
1321
1380
|
if (customSessionDir) {
|
|
1322
|
-
|
|
1323
|
-
sessions.sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
|
1324
|
-
return sessions;
|
|
1381
|
+
return sortSessionInfos(await listSessionsFromDir(customSessionDir, progress, abortSignal));
|
|
1325
1382
|
}
|
|
1326
1383
|
const sessionsDir = getSessionsDir();
|
|
1327
1384
|
try {
|
|
1328
|
-
if (!existsSync(sessionsDir))
|
|
1385
|
+
if (!existsSync(sessionsDir))
|
|
1329
1386
|
return [];
|
|
1330
|
-
}
|
|
1331
1387
|
const entries = await readdir(sessionsDir, { withFileTypes: true });
|
|
1332
1388
|
const dirs = entries.filter((e) => e.isDirectory()).map((e) => join(sessionsDir, e.name));
|
|
1333
|
-
|
|
1334
|
-
let totalFiles = 0;
|
|
1335
|
-
const dirFiles = [];
|
|
1336
|
-
for (const dir of dirs) {
|
|
1389
|
+
const dirFiles = await mapWithConcurrency(dirs, MAX_CONCURRENT_SESSION_DISCOVERY_LOADS, async (dir) => {
|
|
1337
1390
|
try {
|
|
1338
|
-
|
|
1339
|
-
dirFiles.push(files.map((f) => join(dir, f)));
|
|
1340
|
-
totalFiles += files.length;
|
|
1391
|
+
return (await readdir(dir)).filter((file) => file.endsWith(".jsonl")).map((file) => join(dir, file));
|
|
1341
1392
|
}
|
|
1342
1393
|
catch {
|
|
1343
|
-
|
|
1394
|
+
return [];
|
|
1344
1395
|
}
|
|
1345
|
-
}
|
|
1346
|
-
// Process all files with progress tracking
|
|
1347
|
-
let loaded = 0;
|
|
1348
|
-
const sessions = [];
|
|
1396
|
+
}, abortSignal);
|
|
1349
1397
|
const allFiles = dirFiles.flat();
|
|
1350
|
-
const
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
});
|
|
1354
|
-
for (const info of results) {
|
|
1355
|
-
if (info) {
|
|
1356
|
-
sessions.push(info);
|
|
1398
|
+
const candidates = await mapWithConcurrency(allFiles, MAX_CONCURRENT_SESSION_DISCOVERY_LOADS, async (path) => {
|
|
1399
|
+
try {
|
|
1400
|
+
return { path, stats: await stat(path) };
|
|
1357
1401
|
}
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1402
|
+
catch {
|
|
1403
|
+
return { path };
|
|
1404
|
+
}
|
|
1405
|
+
}, abortSignal);
|
|
1406
|
+
candidates.sort((a, b) => (b.stats?.mtimeMs ?? Number.NEGATIVE_INFINITY) - (a.stats?.mtimeMs ?? Number.NEGATIVE_INFINITY) ||
|
|
1407
|
+
basename(b.path).localeCompare(basename(a.path)));
|
|
1408
|
+
const totalFiles = candidates.length;
|
|
1409
|
+
let loaded = 0;
|
|
1410
|
+
let firstCandidateLoaded = false;
|
|
1411
|
+
const partialSessions = [];
|
|
1412
|
+
const results = await buildSessionInfosWithConcurrency(candidates, (info, index) => {
|
|
1413
|
+
loaded++;
|
|
1414
|
+
if (index === 0)
|
|
1415
|
+
firstCandidateLoaded = true;
|
|
1416
|
+
if (info)
|
|
1417
|
+
partialSessions.push(info);
|
|
1418
|
+
const publishPartial = firstCandidateLoaded &&
|
|
1419
|
+
(index === 0 || loaded % ALL_SESSION_LIST_PUBLISH_INTERVAL === 0 || loaded === totalFiles);
|
|
1420
|
+
progress?.(loaded, totalFiles, publishPartial ? sortSessionInfos([...partialSessions]) : undefined);
|
|
1421
|
+
}, abortSignal);
|
|
1422
|
+
return sortSessionInfos(results.filter((info) => info !== null));
|
|
1361
1423
|
}
|
|
1362
1424
|
catch {
|
|
1425
|
+
abortSignal?.throwIfAborted();
|
|
1363
1426
|
return [];
|
|
1364
1427
|
}
|
|
1365
1428
|
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
|
-
import type
|
|
3
|
-
import type {
|
|
2
|
+
import { type Model, type Transport } from "@earendil-works/pi-ai";
|
|
3
|
+
import type { TuiMode as RendererTuiMode, ScrollViewScrollbar, TerminalCapabilities } from "@earendil-works/pi-tui";
|
|
4
|
+
export interface CompactionModelOverride {
|
|
5
|
+
reserveTokens?: number;
|
|
6
|
+
keepRecentTokens?: number;
|
|
7
|
+
}
|
|
4
8
|
export interface CompactionSettings {
|
|
5
9
|
enabled?: boolean;
|
|
6
10
|
reserveTokens?: number;
|
|
7
11
|
keepRecentTokens?: number;
|
|
12
|
+
modelOverrides?: Record<string, CompactionModelOverride>;
|
|
8
13
|
}
|
|
9
14
|
export interface BranchSummarySettings {
|
|
10
15
|
reserveTokens?: number;
|
|
@@ -19,6 +24,7 @@ export interface RetrySettings {
|
|
|
19
24
|
enabled?: boolean;
|
|
20
25
|
maxRetries?: number;
|
|
21
26
|
baseDelayMs?: number;
|
|
27
|
+
maxAgentDelayMs?: number;
|
|
22
28
|
provider?: ProviderRetrySettings;
|
|
23
29
|
}
|
|
24
30
|
export type TuiMode = RendererTuiMode;
|
|
@@ -44,6 +50,10 @@ export interface ThinkingBudgetsSettings {
|
|
|
44
50
|
medium?: number;
|
|
45
51
|
high?: number;
|
|
46
52
|
}
|
|
53
|
+
export type MermaidRenderingMode = "off" | "final" | "streaming";
|
|
54
|
+
/** Cache-warming profile. "idle" also warms between agent runs. */
|
|
55
|
+
export declare const CACHE_WARMING_MODES: readonly ["off", "streaming", "idle"];
|
|
56
|
+
export type CacheWarmingMode = (typeof CACHE_WARMING_MODES)[number];
|
|
47
57
|
export interface MarkdownSettings {
|
|
48
58
|
codeBlockIndent?: string;
|
|
49
59
|
}
|
|
@@ -124,6 +134,7 @@ export interface Settings {
|
|
|
124
134
|
extensionHost?: Record<string, "selesai" | "pi">;
|
|
125
135
|
httpProxy?: string;
|
|
126
136
|
httpIdleTimeoutMs?: number;
|
|
137
|
+
cacheWarming?: CacheWarmingMode;
|
|
127
138
|
websocketConnectTimeoutMs?: number;
|
|
128
139
|
}
|
|
129
140
|
export type SettingsScope = "global" | "project";
|
|
@@ -234,9 +245,11 @@ export declare class SettingsManager {
|
|
|
234
245
|
setTransport(transport: TransportSetting): void;
|
|
235
246
|
getCompactionEnabled(): boolean;
|
|
236
247
|
setCompactionEnabled(enabled: boolean): void;
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
248
|
+
private getCompactionTokenSetting;
|
|
249
|
+
getCompactionReserveTokens(model?: Pick<Model<string>, "provider" | "id">): number;
|
|
250
|
+
getCompactionKeepRecentTokens(model?: Pick<Model<string>, "provider" | "id">): number;
|
|
251
|
+
/** Resolve each token setting through model override, ordinary setting, then built-in default. */
|
|
252
|
+
getCompactionSettings(model?: Pick<Model<string>, "provider" | "id">): {
|
|
240
253
|
enabled: boolean;
|
|
241
254
|
reserveTokens: number;
|
|
242
255
|
keepRecentTokens: number;
|
|
@@ -252,9 +265,13 @@ export declare class SettingsManager {
|
|
|
252
265
|
enabled: boolean;
|
|
253
266
|
maxRetries: number;
|
|
254
267
|
baseDelayMs: number;
|
|
268
|
+
maxAgentDelayMs: number;
|
|
255
269
|
};
|
|
256
270
|
getHttpIdleTimeoutMs(): number;
|
|
257
271
|
setHttpIdleTimeoutMs(timeoutMs: number): void;
|
|
272
|
+
/** Read from global settings only because warming costs money. */
|
|
273
|
+
getCacheWarmingMode(): CacheWarmingMode;
|
|
274
|
+
setCacheWarmingMode(mode: CacheWarmingMode): void;
|
|
258
275
|
getProviderRetrySettings(): {
|
|
259
276
|
timeoutMs?: number;
|
|
260
277
|
maxRetries?: number;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DEFAULT_MAX_AGENT_RETRY_DELAY_MS } from "@earendil-works/pi-ai";
|
|
1
2
|
import { randomUUID } from "crypto";
|
|
2
3
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
4
|
import { dirname, join } from "path";
|
|
@@ -5,35 +6,38 @@ import lockfile from "proper-lockfile";
|
|
|
5
6
|
import { CONFIG_DIR_NAME, getAgentDir } from "../config.js";
|
|
6
7
|
import { normalizePath, resolvePath } from "../utils/paths.js";
|
|
7
8
|
import { DEFAULT_HTTP_IDLE_TIMEOUT_MS, parseHttpIdleTimeoutMs } from "./http-dispatcher.js";
|
|
9
|
+
const DEFAULT_COMPACTION_TOKEN_SETTINGS = {
|
|
10
|
+
reserveTokens: 16384,
|
|
11
|
+
keepRecentTokens: 20000,
|
|
12
|
+
};
|
|
13
|
+
/** Cache-warming profile. "idle" also warms between agent runs. */
|
|
14
|
+
export const CACHE_WARMING_MODES = ["off", "streaming", "idle"];
|
|
8
15
|
const DEFAULT_AUTO_HANDOFF_SETTINGS = {
|
|
9
16
|
enabled: false,
|
|
10
17
|
thresholdTokens: 128_000,
|
|
11
18
|
};
|
|
12
|
-
|
|
13
|
-
|
|
19
|
+
function isMergeableObject(value) {
|
|
20
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
21
|
+
}
|
|
22
|
+
function deepMergeObjects(base, overrides) {
|
|
14
23
|
const result = { ...base };
|
|
15
24
|
for (const key of Object.keys(overrides)) {
|
|
16
25
|
const overrideValue = overrides[key];
|
|
17
|
-
const baseValue = base[key];
|
|
18
26
|
if (overrideValue === undefined) {
|
|
19
27
|
continue;
|
|
20
28
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
baseValue !== null &&
|
|
27
|
-
!Array.isArray(baseValue)) {
|
|
28
|
-
result[key] = { ...baseValue, ...overrideValue };
|
|
29
|
-
}
|
|
30
|
-
else {
|
|
31
|
-
// For primitives and arrays, override value wins
|
|
32
|
-
result[key] = overrideValue;
|
|
33
|
-
}
|
|
29
|
+
const baseValue = base[key];
|
|
30
|
+
result[key] =
|
|
31
|
+
isMergeableObject(baseValue) && isMergeableObject(overrideValue)
|
|
32
|
+
? deepMergeObjects(baseValue, overrideValue)
|
|
33
|
+
: overrideValue;
|
|
34
34
|
}
|
|
35
35
|
return result;
|
|
36
36
|
}
|
|
37
|
+
/** Deep merge settings: project/overrides take precedence, nested objects merge recursively */
|
|
38
|
+
function deepMergeSettings(base, overrides) {
|
|
39
|
+
return deepMergeObjects(base, overrides);
|
|
40
|
+
}
|
|
37
41
|
function parseTimeoutSetting(value, settingName) {
|
|
38
42
|
const timeoutMs = parseHttpIdleTimeoutMs(value);
|
|
39
43
|
if (timeoutMs !== undefined) {
|
|
@@ -604,17 +608,35 @@ export class SettingsManager {
|
|
|
604
608
|
this.markModified("compaction", "enabled");
|
|
605
609
|
this.save();
|
|
606
610
|
}
|
|
607
|
-
|
|
608
|
-
|
|
611
|
+
getCompactionTokenSetting(field, model) {
|
|
612
|
+
const compaction = this.settings.compaction;
|
|
613
|
+
const ordinary = compaction?.[field];
|
|
614
|
+
if (ordinary !== undefined && (typeof ordinary !== "number" || !Number.isSafeInteger(ordinary) || ordinary < 0)) {
|
|
615
|
+
throw new Error(`Invalid compaction.${field} setting: ${String(ordinary)}. Expected a non-negative safe integer.`);
|
|
616
|
+
}
|
|
617
|
+
const modelKey = model ? `${model.provider}/${model.id}` : undefined;
|
|
618
|
+
const entry = modelKey !== undefined ? compaction?.modelOverrides?.[modelKey] : undefined;
|
|
619
|
+
if (entry !== undefined && !isMergeableObject(entry)) {
|
|
620
|
+
throw new Error(`Invalid compaction.modelOverrides["${modelKey}"] setting: ${String(entry)}. Expected an object.`);
|
|
621
|
+
}
|
|
622
|
+
const override = entry?.[field];
|
|
623
|
+
if (override !== undefined && (typeof override !== "number" || !Number.isSafeInteger(override) || override < 0)) {
|
|
624
|
+
throw new Error(`Invalid compaction.modelOverrides["${modelKey}"].${field} setting: ${String(override)}. Expected a non-negative safe integer.`);
|
|
625
|
+
}
|
|
626
|
+
return override ?? ordinary ?? DEFAULT_COMPACTION_TOKEN_SETTINGS[field];
|
|
609
627
|
}
|
|
610
|
-
|
|
611
|
-
return this.
|
|
628
|
+
getCompactionReserveTokens(model) {
|
|
629
|
+
return this.getCompactionTokenSetting("reserveTokens", model);
|
|
612
630
|
}
|
|
613
|
-
|
|
631
|
+
getCompactionKeepRecentTokens(model) {
|
|
632
|
+
return this.getCompactionTokenSetting("keepRecentTokens", model);
|
|
633
|
+
}
|
|
634
|
+
/** Resolve each token setting through model override, ordinary setting, then built-in default. */
|
|
635
|
+
getCompactionSettings(model) {
|
|
614
636
|
return {
|
|
615
637
|
enabled: this.getCompactionEnabled(),
|
|
616
|
-
reserveTokens: this.getCompactionReserveTokens(),
|
|
617
|
-
keepRecentTokens: this.getCompactionKeepRecentTokens(),
|
|
638
|
+
reserveTokens: this.getCompactionReserveTokens(model),
|
|
639
|
+
keepRecentTokens: this.getCompactionKeepRecentTokens(model),
|
|
618
640
|
};
|
|
619
641
|
}
|
|
620
642
|
getBranchSummarySettings() {
|
|
@@ -642,6 +664,7 @@ export class SettingsManager {
|
|
|
642
664
|
enabled: this.getRetryEnabled(),
|
|
643
665
|
maxRetries: this.settings.retry?.maxRetries ?? 3,
|
|
644
666
|
baseDelayMs: this.settings.retry?.baseDelayMs ?? 2000,
|
|
667
|
+
maxAgentDelayMs: this.settings.retry?.maxAgentDelayMs ?? DEFAULT_MAX_AGENT_RETRY_DELAY_MS,
|
|
645
668
|
};
|
|
646
669
|
}
|
|
647
670
|
getHttpIdleTimeoutMs() {
|
|
@@ -655,6 +678,16 @@ export class SettingsManager {
|
|
|
655
678
|
this.markModified("httpIdleTimeoutMs");
|
|
656
679
|
this.save();
|
|
657
680
|
}
|
|
681
|
+
/** Read from global settings only because warming costs money. */
|
|
682
|
+
getCacheWarmingMode() {
|
|
683
|
+
const mode = this.globalSettings.cacheWarming;
|
|
684
|
+
return mode !== undefined && CACHE_WARMING_MODES.includes(mode) ? mode : "streaming";
|
|
685
|
+
}
|
|
686
|
+
setCacheWarmingMode(mode) {
|
|
687
|
+
this.globalSettings.cacheWarming = mode;
|
|
688
|
+
this.markModified("cacheWarming");
|
|
689
|
+
this.save();
|
|
690
|
+
}
|
|
658
691
|
getProviderRetrySettings() {
|
|
659
692
|
return {
|
|
660
693
|
timeoutMs: this.settings.retry?.provider?.timeoutMs,
|
|
@@ -7,6 +7,7 @@ export const BUILTIN_SLASH_COMMANDS = [
|
|
|
7
7
|
{ name: "export", description: "Export session (HTML default, or specify path: .html/.jsonl)" },
|
|
8
8
|
{ name: "import", description: "Import and resume a session from a JSONL file" },
|
|
9
9
|
{ name: "share", description: "Share session as a secret GitHub gist" },
|
|
10
|
+
{ name: "bug", description: "Report a bug to the Pi developers", argumentHint: "<description>" },
|
|
10
11
|
{ name: "copy", description: "Copy last agent message to clipboard" },
|
|
11
12
|
{ name: "name", description: "Set session display name" },
|
|
12
13
|
{ name: "session", description: "Show session info and stats" },
|
|
@@ -4,16 +4,22 @@
|
|
|
4
4
|
import { type AgentPersona } from "./agents.ts";
|
|
5
5
|
import { type Skill } from "./skills.ts";
|
|
6
6
|
export interface BuildSystemPromptOptions {
|
|
7
|
-
/** Custom system prompt (replaces default). */
|
|
7
|
+
/** Custom system prompt (replaces the default prefix). */
|
|
8
8
|
customPrompt?: string;
|
|
9
|
-
/**
|
|
9
|
+
/** Exact full prompt replacement set by a before_agent_start handler. */
|
|
10
|
+
forceSystemPrompt?: string;
|
|
11
|
+
/** Tools to include in prompt. Default: [read, bash, edit, write] */
|
|
10
12
|
selectedTools?: string[];
|
|
11
13
|
/** Optional one-line tool snippets keyed by tool name. */
|
|
12
14
|
toolSnippets?: Record<string, string>;
|
|
15
|
+
/** Guideline bullets contributed by each tool, keyed by tool name. */
|
|
16
|
+
toolGuidelines?: Record<string, string[]>;
|
|
13
17
|
/** Additional guideline bullets appended to the default system prompt guidelines. */
|
|
14
18
|
promptGuidelines?: string[];
|
|
15
19
|
/** Text to append to system prompt. */
|
|
16
20
|
appendSystemPrompt?: string;
|
|
21
|
+
/** Additional XML-wrapped prompt sections keyed by tag name. */
|
|
22
|
+
sections?: Record<string, string>;
|
|
17
23
|
/** Working directory. */
|
|
18
24
|
cwd: string;
|
|
19
25
|
/** Pre-loaded context files. */
|
|
@@ -26,5 +32,44 @@ export interface BuildSystemPromptOptions {
|
|
|
26
32
|
/** Pre-loaded subagent personas. */
|
|
27
33
|
agents?: AgentPersona[];
|
|
28
34
|
}
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
export type NormalizedBuildSystemPromptOptions = BuildSystemPromptOptions & {
|
|
36
|
+
selectedTools: string[];
|
|
37
|
+
toolSnippets: Record<string, string>;
|
|
38
|
+
toolGuidelines: Record<string, string[]>;
|
|
39
|
+
promptGuidelines: string[];
|
|
40
|
+
appendSystemPrompt: string;
|
|
41
|
+
sections: Record<string, string>;
|
|
42
|
+
contextFiles: Array<{
|
|
43
|
+
path: string;
|
|
44
|
+
content: string;
|
|
45
|
+
}>;
|
|
46
|
+
skills: Skill[];
|
|
47
|
+
agents: AgentPersona[];
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Ordered system prompt sections, keyed by name. `preamble` is untagged text; every other
|
|
51
|
+
* section is wrapped in a tag of the same name so the model can match later updates to it.
|
|
52
|
+
* These become `SystemMessage.sections` in the transcript.
|
|
53
|
+
*/
|
|
54
|
+
export type SystemPromptSections = Record<string, string>;
|
|
55
|
+
/** Normalize prompt input into the mutable, collection-complete shape exposed to extensions. */
|
|
56
|
+
export declare function normalizeBuildSystemPromptOptions(input: BuildSystemPromptOptions): NormalizedBuildSystemPromptOptions;
|
|
57
|
+
export declare function buildRules(selectedTools: string[], toolGuidelines: Record<string, string[]>, promptGuidelines: string[]): string;
|
|
58
|
+
/** Build the ordered, independently replaceable sections of the structured system prompt. */
|
|
59
|
+
export declare function buildSystemPromptSections(input: BuildSystemPromptOptions): SystemPromptSections;
|
|
60
|
+
/**
|
|
61
|
+
* The complete prompt state for `input`. A forced prompt is opaque and lives in `content`
|
|
62
|
+
* with no sections; otherwise `content` is empty and the structured sections carry the prompt.
|
|
63
|
+
*/
|
|
64
|
+
export declare function buildSystemPromptState(input: BuildSystemPromptOptions): {
|
|
65
|
+
content: string;
|
|
66
|
+
sections?: SystemPromptSections;
|
|
67
|
+
};
|
|
68
|
+
/** Build the system prompt text, rendered exactly as the transcript's system message replays it. */
|
|
69
|
+
export declare function buildSystemPrompt(input: BuildSystemPromptOptions): string;
|
|
70
|
+
/**
|
|
71
|
+
* Diff the sections the model currently has (replayed from the transcript, so never null)
|
|
72
|
+
* against the desired ones. Returns a `SystemMessage.sections` patch, or undefined when
|
|
73
|
+
* nothing changed.
|
|
74
|
+
*/
|
|
75
|
+
export declare function diffSystemPromptSections(previous: Record<string, string | null>, current: SystemPromptSections): Record<string, string | null> | undefined;
|