@daniel156161/prism 0.2.90 → 0.2.92

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.
@@ -8,7 +8,7 @@
8
8
  import { APP_NAME } from "./config.js";
9
9
  import { configureHttpDispatcher } from "./core/http-dispatcher.js";
10
10
  import { main } from "./main.js";
11
- process.title = APP_NAME;
11
+ process.title = "▲";
12
12
  process.env.PI_CODING_AGENT = "true";
13
13
  process.env.AI_AGENT = "pi";
14
14
  process.emitWarning = (() => { });
@@ -321,10 +321,9 @@ export function getThemesDir() {
321
321
  if (isBunBinary) {
322
322
  return join(getPackageDir(), "theme");
323
323
  }
324
- // Theme is in modes/interactive/theme/ relative to src/ or dist/
325
- const packageDir = getPackageDir();
326
- const srcOrDist = existsSync(join(packageDir, "src")) ? "src" : "dist";
327
- return join(packageDir, srcOrDist, "modes", "interactive", "theme");
324
+ // Prism: use __dirname so built-in themes resolve from the actual dist/,
325
+ // not PI_PACKAGE_DIR which may point to a config-only dir without dist/.
326
+ return join(__dirname, "modes", "interactive", "theme");
328
327
  }
329
328
  /**
330
329
  * Get path to HTML export template directory (shipped with package)
@@ -336,9 +335,7 @@ export function getExportTemplateDir() {
336
335
  if (isBunBinary) {
337
336
  return join(getPackageDir(), "export-html");
338
337
  }
339
- const packageDir = getPackageDir();
340
- const srcOrDist = existsSync(join(packageDir, "src")) ? "src" : "dist";
341
- return join(packageDir, srcOrDist, "core", "export-html");
338
+ return join(__dirname, "core", "export-html");
342
339
  }
343
340
  /** Get path to package.json */
344
341
  export function getPackageJsonPath() {
@@ -370,9 +367,7 @@ export function getInteractiveAssetsDir() {
370
367
  if (isBunBinary) {
371
368
  return join(getPackageDir(), "assets");
372
369
  }
373
- const packageDir = getPackageDir();
374
- const srcOrDist = existsSync(join(packageDir, "src")) ? "src" : "dist";
375
- return join(packageDir, srcOrDist, "modes", "interactive", "assets");
370
+ return join(__dirname, "modes", "interactive", "assets");
376
371
  }
377
372
  /** Get path to a bundled interactive asset */
378
373
  export function getBundledInteractiveAssetPath(name) {
@@ -426,7 +421,7 @@ export function getModelsPath() {
426
421
  }
427
422
  /** Get path to auth.json */
428
423
  export function getAuthPath() {
429
- return join(getAgentDir(), "auth.json");
424
+ return process.env.PRISM_AUTH_PATH || process.env.PI_AUTH_PATH || join(getAgentDir(), "auth.json");
430
425
  }
431
426
  /** Get path to settings.json */
432
427
  export function getSettingsPath() {
@@ -55,7 +55,7 @@ export async function createAgentSessionServices(options) {
55
55
  const agentDir = options.agentDir ? resolvePath(options.agentDir) : getAgentDir();
56
56
  const modelRuntime = options.modelRuntime ??
57
57
  (await ModelRuntime.create({
58
- authPath: join(agentDir, "auth.json"),
58
+ authPath: process.env.PRISM_AUTH_PATH || process.env.PI_AUTH_PATH || join(agentDir, "auth.json"),
59
59
  modelsPath: join(agentDir, "models.json"),
60
60
  signal: options.modelRuntimeSignal,
61
61
  }));
@@ -2084,6 +2084,11 @@ export class AgentSession {
2084
2084
  // Context overflow is handled by compaction, not retry.
2085
2085
  if (isContextOverflow(message, this.model?.contextWindow ?? 0))
2086
2086
  return false;
2087
+ // Prism: provider account/quota usage limits need a user decision (wait, switch model, or cancel).
2088
+ // Do not auto-retry them with the generic exponential backoff, because provider reset
2089
+ // windows are often much longer than the retry delay.
2090
+ if (/GoUsageLimitError|FreeUsageLimitError|Monthly usage limit reached|available balance|insufficient_quota|out of budget|quota exceeded|billing|usage[_\s-]*limit|usage_limit_reached/i.test(message.errorMessage || ""))
2091
+ return false;
2087
2092
  return isRetryableAssistantError(message);
2088
2093
  }
2089
2094
  /**
@@ -14,7 +14,7 @@ const AUTH_FILE_WRITE_OPTIONS = { encoding: "utf-8", mode: 0o600 };
14
14
  let sharedAuthFileReadState;
15
15
  export class FileAuthStorageBackend {
16
16
  authPath;
17
- constructor(authPath = join(getAgentDir(), "auth.json")) {
17
+ constructor(authPath = (process.env.PRISM_AUTH_PATH || process.env.PI_AUTH_PATH || join(getAgentDir(), "auth.json"))) {
18
18
  this.authPath = normalizePath(authPath);
19
19
  }
20
20
  ensureParentDir() {
@@ -9,6 +9,8 @@ import { fileURLToPath } from "node:url";
9
9
  import * as _bundledPiAgentCore from "@earendil-works/pi-agent-core";
10
10
  import * as _bundledPiAiCompat from "@earendil-works/pi-ai/compat";
11
11
  import * as _bundledPiAiOauth from "@earendil-works/pi-ai/oauth";
12
+ import * as _prismPiAiOpenAICompletions from "@earendil-works/pi-ai/api/openai-completions";
13
+ import * as _prismPiAiOpenAIResponses from "@earendil-works/pi-ai/api/openai-responses";
12
14
  import * as _bundledPiAiProviders from "@earendil-works/pi-ai/providers/all";
13
15
  import * as _bundledPiTui from "@earendil-works/pi-tui";
14
16
  import { createJiti } from "jiti/static";
@@ -21,12 +23,14 @@ import * as _bundledTypeboxValue from "typebox/value";
21
23
  import { CONFIG_DIR_NAME, getAgentDir, isBunBinary } from "../../config.js";
22
24
  // NOTE: This import works because loader.ts exports are NOT re-exported from index.ts,
23
25
  // avoiding a circular dependency. Extensions can import from @earendil-works/pi-coding-agent.
24
- import * as _bundledPiCodingAgent from "../../index.js";
26
+ import * as _bundledPiCodingAgent from "./index.js";
25
27
  import { resolvePath } from "../../utils/paths.js";
26
28
  import { createEventBus } from "../event-bus.js";
27
29
  import { execCommand } from "../exec.js";
28
30
  import { readPiManifest } from "../pi-manifest.js";
29
31
  import { createSyntheticSourceInfo } from "../source-info.js";
32
+ globalThis.__PRISM_PI_AI_OPENAI_COMPLETIONS__ = _prismPiAiOpenAICompletions;
33
+ globalThis.__PRISM_PI_AI_OPENAI_RESPONSES__ = _prismPiAiOpenAIResponses;
30
34
  import { time } from "../timings.js";
31
35
  /** Modules available to extensions via virtualModules (for compiled Bun binary) */
32
36
  const VIRTUAL_MODULES = {
@@ -0,0 +1,128 @@
1
+ import { createRequire } from "node:module";
2
+ import { mkdirSync } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import { homedir } from "node:os";
5
+ const require = createRequire(import.meta.url);
6
+ export function getSessionBackend() {
7
+ return process.env.PRISM_SESSION_BACKEND || "db";
8
+ }
9
+ export function findMostRecentSessionFromDb(sessionDir) {
10
+ try {
11
+ const db = openDb();
12
+ const row = db.prepare("SELECT file_path FROM sessions WHERE session_dir = ? ORDER BY updated_at DESC LIMIT 1").get(sessionDir);
13
+ db.close();
14
+ return row?.file_path || null;
15
+ } catch { return null; }
16
+ }
17
+ export function deleteSessionFromDb(filePath) {
18
+ if (!filePath) return;
19
+ try {
20
+ const db = openDb();
21
+ db.prepare("DELETE FROM sessions WHERE file_path = ?").run(filePath);
22
+ db.close();
23
+ } catch { }
24
+ }
25
+ function openDb() {
26
+ const prismDir = process.env.PRISM_CODING_AGENT_DIR || process.env.PI_CODING_AGENT_DIR || join(homedir(), ".prism");
27
+ const dbPath = join(prismDir, "sessions.db");
28
+ mkdirSync(dirname(dbPath), { recursive: true });
29
+ const { DatabaseSync } = require("node:sqlite");
30
+ const db = new DatabaseSync(dbPath);
31
+ db.exec("PRAGMA journal_mode=WAL");
32
+ db.exec("PRAGMA synchronous=NORMAL");
33
+ db.exec("CREATE TABLE IF NOT EXISTS sessions (id TEXT PRIMARY KEY, file_path TEXT UNIQUE, cwd TEXT, session_dir TEXT, created_at TEXT, updated_at TEXT, parent_session TEXT, version INTEGER, header_json TEXT NOT NULL)");
34
+ db.exec("CREATE TABLE IF NOT EXISTS entries (session_id TEXT NOT NULL, entry_id TEXT NOT NULL, idx INTEGER NOT NULL, parent_id TEXT, type TEXT, timestamp TEXT, data_json TEXT NOT NULL, PRIMARY KEY (session_id, entry_id), FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE)");
35
+ db.exec("CREATE INDEX IF NOT EXISTS idx_entries_session_idx ON entries(session_id, idx)");
36
+ db.exec("CREATE INDEX IF NOT EXISTS idx_sessions_updated ON sessions(updated_at)");
37
+ return db;
38
+ }
39
+ export function mirrorSessionFileToDb(filePath, entries, cwd, sessionDir) {
40
+ if (!filePath) return;
41
+ const header = entries.find((entry) => entry?.type === "session" && typeof entry.id === "string");
42
+ if (!header) return;
43
+ const rows = entries.filter((entry) => entry !== header);
44
+ const db = openDb();
45
+ const updatedAt = rows.at(-1)?.timestamp || header.timestamp || new Date().toISOString();
46
+ const insertSession = db.prepare("INSERT INTO sessions (id, file_path, cwd, session_dir, created_at, updated_at, parent_session, version, header_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET file_path=excluded.file_path, cwd=excluded.cwd, session_dir=excluded.session_dir, updated_at=excluded.updated_at, parent_session=excluded.parent_session, version=excluded.version, header_json=excluded.header_json");
47
+ const insertEntry = db.prepare("INSERT INTO entries (session_id, entry_id, idx, parent_id, type, timestamp, data_json) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(session_id, entry_id) DO UPDATE SET idx=excluded.idx, parent_id=excluded.parent_id, type=excluded.type, timestamp=excluded.timestamp, data_json=excluded.data_json");
48
+ const deleteEntries = db.prepare("DELETE FROM entries WHERE session_id = ?");
49
+ db.exec("BEGIN");
50
+ try {
51
+ insertSession.run(header.id, filePath, header.cwd || cwd || "", sessionDir || dirname(filePath), header.timestamp || "", updatedAt, header.parentSession || null, header.version || null, JSON.stringify(header));
52
+ deleteEntries.run(header.id);
53
+ rows.forEach((entry, idx) => insertEntry.run(header.id, typeof entry.id === "string" ? entry.id : String(idx), idx, entry.parentId ?? null, entry.type || null, entry.timestamp || null, JSON.stringify(entry)));
54
+ db.exec("COMMIT");
55
+ } catch (error) {
56
+ db.exec("ROLLBACK");
57
+ db.close();
58
+ throw error;
59
+ }
60
+ db.close();
61
+ }
62
+ export function mirrorEntryToDb(filePath, header, entry, cwd, sessionDir, index) {
63
+ if (!filePath || !header?.id || !entry) return;
64
+ const db = openDb();
65
+ const insertSession = db.prepare("INSERT INTO sessions (id, file_path, cwd, session_dir, created_at, updated_at, parent_session, version, header_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET file_path=excluded.file_path, cwd=excluded.cwd, session_dir=excluded.session_dir, updated_at=excluded.updated_at, parent_session=excluded.parent_session, version=excluded.version, header_json=excluded.header_json");
66
+ const insertEntry = db.prepare("INSERT INTO entries (session_id, entry_id, idx, parent_id, type, timestamp, data_json) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(session_id, entry_id) DO UPDATE SET idx=excluded.idx, parent_id=excluded.parent_id, type=excluded.type, timestamp=excluded.timestamp, data_json=excluded.data_json");
67
+ insertSession.run(header.id, filePath, header.cwd || cwd || "", sessionDir || dirname(filePath), header.timestamp || "", entry.timestamp || header.timestamp || new Date().toISOString(), header.parentSession || null, header.version || null, JSON.stringify(header));
68
+ insertEntry.run(header.id, typeof entry.id === "string" ? entry.id : String(index ?? 0), Number.isInteger(index) ? index : 0, entry.parentId ?? null, entry.type || null, entry.timestamp || null, JSON.stringify(entry));
69
+ db.close();
70
+ }
71
+ export function loadEntriesFromDb(filePath) {
72
+ if (!filePath) return null;
73
+ try {
74
+ const db = openDb();
75
+ const session = db.prepare("SELECT id, header_json FROM sessions WHERE file_path = ?").get(filePath);
76
+ if (!session) { db.close(); return null; }
77
+ const entryRows = db.prepare("SELECT data_json FROM entries WHERE session_id = ? ORDER BY idx ASC").all(session.id);
78
+ db.close();
79
+ return [JSON.parse(session.header_json), ...entryRows.map((r) => JSON.parse(r.data_json))];
80
+ } catch { return null; }
81
+ }
82
+ async function buildSessionsFromRows(db, sessionRows, onProgress) {
83
+ const sessions = [];
84
+ let loaded = 0;
85
+ const total = sessionRows.length;
86
+ for (const row of sessionRows) {
87
+ try {
88
+ const entryRows = db.prepare("SELECT data_json FROM entries WHERE session_id = ? ORDER BY idx ASC").all(row.id);
89
+ let messageCount = 0, firstMessage = "";
90
+ const allMessages = [];
91
+ let name;
92
+ for (const { data_json } of entryRows) {
93
+ const e = JSON.parse(data_json);
94
+ if (e.type === "session_info" && e.name?.trim()) name = e.name.trim();
95
+ if (e.type !== "message") continue;
96
+ messageCount++;
97
+ const msg = e.message;
98
+ if (!msg?.content || (msg.role !== "user" && msg.role !== "assistant")) continue;
99
+ const text = [].concat(msg.content).filter((p) => p?.type === "text").map((p) => p.text).join(" ").trim();
100
+ if (!text) continue;
101
+ allMessages.push(text);
102
+ if (!firstMessage && msg.role === "user") firstMessage = text;
103
+ }
104
+ sessions.push({ path: row.file_path || row.id, id: row.id, cwd: row.cwd || "", name, parentSessionPath: row.parent_session || undefined, created: new Date(row.created_at || 0), modified: new Date(row.updated_at || row.created_at || 0), messageCount, firstMessage: firstMessage || "(no messages)", allMessagesText: allMessages.join(" ") });
105
+ } catch {}
106
+ loaded++;
107
+ onProgress?.(loaded, total);
108
+ }
109
+ return sessions;
110
+ }
111
+ export async function listSessionsFromDb(dir, onProgress) {
112
+ try {
113
+ const db = openDb();
114
+ const sessionRows = db.prepare("SELECT id, file_path, cwd, created_at, updated_at, parent_session FROM sessions WHERE session_dir = ? ORDER BY updated_at DESC").all(dir);
115
+ const sessions = await buildSessionsFromRows(db, sessionRows, onProgress);
116
+ db.close();
117
+ return sessions;
118
+ } catch { return []; }
119
+ }
120
+ export async function listAllSessionsFromDb(onProgress) {
121
+ try {
122
+ const db = openDb();
123
+ const sessionRows = db.prepare("SELECT id, file_path, cwd, created_at, updated_at, parent_session FROM sessions ORDER BY updated_at DESC").all();
124
+ const sessions = await buildSessionsFromRows(db, sessionRows, onProgress);
125
+ db.close();
126
+ return sessions;
127
+ } catch { return []; }
128
+ }
@@ -2,7 +2,7 @@ import { APP_NAME, CONFIG_DIR_NAME } from "../config.js";
2
2
  import { emitProjectTrustEvent } from "./extensions/runner.js";
3
3
  import { getProjectTrustOptions, hasTrustRequiringProjectResources, } from "./trust-manager.js";
4
4
  function formatProjectTrustPrompt(cwd) {
5
- return `Trust project folder?\n${cwd}\n\nThis allows ${APP_NAME} to load ${CONFIG_DIR_NAME} settings and resources, install missing project packages, and execute project extensions.`;
5
+ return `Trust project folder?\n${cwd}\n\nThis allows Prism to load ${CONFIG_DIR_NAME} settings and resources, install missing project packages, and execute project extensions.`;
6
6
  }
7
7
  async function selectProjectTrustOption(cwd, ctx) {
8
8
  const options = getProjectTrustOptions(cwd, { includeSessionOnly: true });
@@ -67,7 +67,7 @@ export async function createAgentSession(options = {}) {
67
67
  const cwd = resolvePath(options.cwd ?? options.sessionManager?.getCwd() ?? process.cwd());
68
68
  const agentDir = options.agentDir ? resolvePath(options.agentDir) : getDefaultAgentDir();
69
69
  let resourceLoader = options.resourceLoader;
70
- const authPath = options.agentDir ? join(agentDir, "auth.json") : undefined;
70
+ const authPath = process.env.PRISM_AUTH_PATH || process.env.PI_AUTH_PATH || (options.agentDir ? join(agentDir, "auth.json") : undefined);
71
71
  const modelsPath = options.agentDir ? join(agentDir, "models.json") : undefined;
72
72
  const modelRuntime = options.modelRuntime ?? (await ModelRuntime.create({ authPath, modelsPath }));
73
73
  const settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);
@@ -3,6 +3,7 @@ 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
5
  import { join, resolve } from "path";
6
+ import { findMostRecentSessionFromDb, getSessionBackend, listAllSessionsFromDb, listSessionsFromDb, loadEntriesFromDb, mirrorEntryToDb, mirrorSessionFileToDb } from "./prism-session-db.js";
6
7
  import { createInterface } from "readline";
7
8
  import { StringDecoder } from "string_decoder";
8
9
  import { APP_NAME, getAgentDir as getDefaultAgentDir, getSessionsDir } from "../config.js";
@@ -634,12 +635,23 @@ export class SessionManager {
634
635
  this._rewriteFile();
635
636
  }
636
637
  this._buildIndex();
638
+ if (getSessionBackend() !== "file") { try { mirrorSessionFileToDb(this.sessionFile, this.fileEntries, this.cwd, this.sessionDir); } catch { } }
637
639
  this.flushed = true;
638
640
  }
639
641
  else {
640
- const explicitPath = this.sessionFile;
641
- this.newSession();
642
- this.sessionFile = explicitPath; // preserve explicit path from --session flag
642
+ const dbEntries = getSessionBackend() !== "file" ? loadEntriesFromDb(this.sessionFile) : null;
643
+ if (dbEntries) {
644
+ this.fileEntries = dbEntries;
645
+ const header = this.fileEntries.find((e) => e.type === "session");
646
+ this.sessionId = header?.id ?? createSessionId();
647
+ this._buildIndex();
648
+ if (getSessionBackend() !== "file") { try { mirrorSessionFileToDb(this.sessionFile, this.fileEntries, this.cwd, this.sessionDir); } catch {} }
649
+ this.flushed = true;
650
+ } else {
651
+ const explicitPath = this.sessionFile;
652
+ this.newSession();
653
+ this.sessionFile = explicitPath;
654
+ }
643
655
  }
644
656
  }
645
657
  newSession(options) {
@@ -693,6 +705,8 @@ export class SessionManager {
693
705
  _rewriteFile() {
694
706
  if (!this.persist || !this.sessionFile)
695
707
  return;
708
+ if (getSessionBackend() !== "file") { try { mirrorSessionFileToDb(this.sessionFile, this.fileEntries, this.cwd, this.sessionDir); } catch { } }
709
+ if (getSessionBackend() === "db") return;
696
710
  const fd = openSync(this.sessionFile, "w");
697
711
  try {
698
712
  for (const entry of this.fileEntries) {
@@ -724,30 +738,14 @@ export class SessionManager {
724
738
  _persist(entry) {
725
739
  if (!this.persist || !this.sessionFile)
726
740
  return;
727
- const hasAssistant = this.fileEntries.some((e) => e.type === "message" && e.message.role === "assistant");
728
- if (!hasAssistant) {
729
- if (this.flushed) {
730
- appendFileSync(this.sessionFile, `${JSON.stringify(entry)}\n`);
731
- }
732
- else {
733
- // Mark as not flushed so when assistant arrives, all entries get written
734
- this.flushed = false;
735
- }
736
- return;
737
- }
741
+ // Prism: persist every entry immediately. Pi defers session writes until the
742
+ // first assistant message, which can lose user prompts if multiple harnesses
743
+ // run at once or a process exits before the assistant response is recorded.
738
744
  if (!this.flushed) {
739
- const fd = openSync(this.sessionFile, "wx");
740
- try {
741
- for (const e of this.fileEntries) {
742
- writeFileSync(fd, `${JSON.stringify(e)}\n`);
743
- }
744
- }
745
- finally {
746
- closeSync(fd);
747
- }
745
+ if (getSessionBackend() !== "db") { const fd = openSync(this.sessionFile, "wx"); try { for (const e of this.fileEntries) { writeFileSync(fd, `${JSON.stringify(e)}\n`); } } finally { closeSync(fd); } }
748
746
  this.flushed = true;
749
747
  }
750
- else {
748
+ else if (getSessionBackend() !== "db") {
751
749
  appendFileSync(this.sessionFile, `${JSON.stringify(entry)}\n`);
752
750
  }
753
751
  }
@@ -756,6 +754,7 @@ export class SessionManager {
756
754
  this.byId.set(entry.id, entry);
757
755
  this.leafId = entry.id;
758
756
  this._persist(entry);
757
+ if (getSessionBackend() !== "file") { try { mirrorEntryToDb(this.sessionFile, this.fileEntries.find((e) => e.type === "session"), entry, this.cwd, this.sessionDir, this.fileEntries.length - 2); } catch { } }
759
758
  }
760
759
  /** Append a message as child of current leaf, then advance leaf. Returns entry id.
761
760
  * Does not allow writing CompactionSummaryMessage and BranchSummaryMessage directly.
@@ -1201,6 +1200,7 @@ export class SessionManager {
1201
1200
  header = firstEntry?.type === "session" ? firstEntry : null;
1202
1201
  }
1203
1202
  }
1203
+ if (!header && getSessionBackend() !== "file") { try { preloadedFileEntries = loadEntriesFromDb(resolvedPath) ?? undefined; header = preloadedFileEntries?.find((e) => e.type === "session") ?? null; } catch {} }
1204
1204
  const cwd = cwdOverride ?? (header ? getSessionHeaderCwd(header) : undefined) ?? process.cwd();
1205
1205
  // If no sessionDir provided, derive from file's parent directory
1206
1206
  const dir = sessionDir ? normalizePath(sessionDir) : resolve(resolvedPath, "..");
@@ -1214,7 +1214,7 @@ export class SessionManager {
1214
1214
  static continueRecent(cwd, sessionDir) {
1215
1215
  const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(cwd);
1216
1216
  const filterCwd = sessionDir !== undefined && dir !== getDefaultSessionDirPath(cwd);
1217
- const mostRecent = findMostRecentSession(dir, filterCwd ? cwd : undefined);
1217
+ const mostRecent = getSessionBackend() === "file" ? findMostRecentSession(dir, filterCwd ? cwd : undefined) : findMostRecentSessionFromDb(dir);
1218
1218
  if (mostRecent) {
1219
1219
  return new SessionManager(cwd, dir, mostRecent, true);
1220
1220
  }
@@ -1234,7 +1234,8 @@ export class SessionManager {
1234
1234
  static forkFrom(sourcePath, targetCwd, sessionDir, options) {
1235
1235
  const resolvedSourcePath = resolvePath(sourcePath);
1236
1236
  const resolvedTargetCwd = resolvePath(targetCwd);
1237
- const sourceEntries = loadEntriesFromFile(resolvedSourcePath);
1237
+ let sourceEntries = loadEntriesFromFile(resolvedSourcePath);
1238
+ if (sourceEntries.length === 0 && getSessionBackend() !== "file") { try { sourceEntries = loadEntriesFromDb(resolvedSourcePath) ?? []; } catch {} }
1238
1239
  if (sourceEntries.length === 0) {
1239
1240
  throw new Error(`Cannot fork: source session file is empty or invalid: ${resolvedSourcePath}`);
1240
1241
  }
@@ -1263,12 +1264,10 @@ export class SessionManager {
1263
1264
  cwd: resolvedTargetCwd,
1264
1265
  parentSession: resolvedSourcePath,
1265
1266
  };
1266
- writeFileSync(newSessionFile, `${JSON.stringify(newHeader)}\n`, { flag: "wx" });
1267
- // Copy all non-header entries from source
1268
- for (const entry of sourceEntries) {
1269
- if (entry.type !== "session") {
1270
- appendFileSync(newSessionFile, `${JSON.stringify(entry)}\n`);
1271
- }
1267
+ if (getSessionBackend() !== "file") { try { mirrorSessionFileToDb(newSessionFile, [newHeader, ...sourceEntries.filter((e) => e.type !== "session")], resolvedTargetCwd, dir); } catch {} }
1268
+ if (getSessionBackend() !== "db") {
1269
+ writeFileSync(newSessionFile, `${JSON.stringify(newHeader)}\n`, { flag: "wx" });
1270
+ for (const entry of sourceEntries) { if (entry.type !== "session") appendFileSync(newSessionFile, `${JSON.stringify(entry)}\n`); }
1272
1271
  }
1273
1272
  return new SessionManager(resolvedTargetCwd, dir, newSessionFile, true);
1274
1273
  }
@@ -1280,11 +1279,14 @@ export class SessionManager {
1280
1279
  */
1281
1280
  static async list(cwd, sessionDir, onProgress) {
1282
1281
  const dir = sessionDir ? normalizePath(sessionDir) : getDefaultSessionDir(cwd);
1283
- const filterCwd = sessionDir !== undefined && dir !== getDefaultSessionDirPath(cwd);
1284
- const resolvedCwd = resolvePath(cwd);
1285
- const sessions = (await listSessionsFromDir(dir, onProgress)).filter((session) => !filterCwd || sessionCwdMatches(session.cwd, resolvedCwd));
1286
- sessions.sort((a, b) => b.modified.getTime() - a.modified.getTime());
1287
- return sessions;
1282
+ if (getSessionBackend() === "file") {
1283
+ const filterCwd = sessionDir !== undefined && dir !== getDefaultSessionDirPath(cwd);
1284
+ const resolvedCwd = resolvePath(cwd);
1285
+ const sessions = (await listSessionsFromDir(dir, onProgress)).filter((session) => !filterCwd || sessionCwdMatches(session.cwd, resolvedCwd));
1286
+ sessions.sort((a, b) => b.modified.getTime() - a.modified.getTime());
1287
+ return sessions;
1288
+ }
1289
+ return listSessionsFromDb(dir, onProgress);
1288
1290
  }
1289
1291
  static async listAll(sessionDirOrOnProgress, onProgress) {
1290
1292
  const customSessionDir = typeof sessionDirOrOnProgress === "string" ? normalizePath(sessionDirOrOnProgress) : undefined;
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * System prompt construction and project context loading
3
3
  */
4
- import { getDocsPath, getExamplesPath, getReadmePath } from "../config.js";
4
+ import { existsSync } from "node:fs";
5
+ import { join } from "node:path";
5
6
  import { formatSkillsForPrompt } from "./skills.js";
6
7
  /** Build the system prompt with tools, guidelines, and context */
7
8
  export function buildSystemPrompt(options) {
@@ -32,10 +33,22 @@ export function buildSystemPrompt(options) {
32
33
  prompt += `\nCurrent working directory: ${promptCwd}\n`;
33
34
  return prompt;
34
35
  }
35
- // Get absolute paths to documentation and examples
36
- const readmePath = getReadmePath();
37
- const docsPath = getDocsPath();
38
- const examplesPath = getExamplesPath();
36
+ // Get absolute paths to project documentation and examples.
37
+ // Only project docs are advertised here. Harness/pi docs can be linked into the project docs
38
+ // tree (for example docs/pi) when a project wants them in the prompt.
39
+ const resolvedCwd = cwd;
40
+ const existingPath = (filePath) => existsSync(filePath) ? filePath : undefined;
41
+ const firstExistingPath = (filePaths) => filePaths.find((filePath) => existsSync(filePath));
42
+ const projectReadmePath = firstExistingPath(["README.md", "README.MD", "readme.md"].map((name) => join(resolvedCwd, name)));
43
+ const projectDocsPath = existingPath(join(resolvedCwd, "docs"));
44
+ const projectExamplesPath = existingPath(join(resolvedCwd, "examples"));
45
+ const documentationLines = [];
46
+ if (projectReadmePath) documentationLines.push(`- Main documentation: ${projectReadmePath}`);
47
+ if (projectDocsPath) documentationLines.push(`- Additional docs: ${projectDocsPath}`);
48
+ if (projectExamplesPath) documentationLines.push(`- Examples: ${projectExamplesPath}`);
49
+ if (projectDocsPath || projectExamplesPath) documentationLines.push("- When reading these docs or examples, resolve docs/... under Additional docs and examples/... under Examples, not the current working directory");
50
+ if (projectDocsPath) documentationLines.push("- When working on this project, read the relevant docs and follow .md cross-references before implementing");
51
+ const documentationSection = documentationLines.length > 0 ? `\n\nProject documentation (read only when relevant):\n${documentationLines.join("\n")}` : "";
39
52
  // Build tools list based on selected tools.
40
53
  // A tool appears in Available tools only when the caller provides a one-line snippet.
41
54
  const tools = selectedTools || ["read", "bash", "edit", "write"];
@@ -70,7 +83,7 @@ export function buildSystemPrompt(options) {
70
83
  addGuideline("Be concise in your responses");
71
84
  addGuideline("Show file paths clearly when working with files");
72
85
  const guidelines = guidelinesList.map((g) => `- ${g}`).join("\n");
73
- let prompt = `You are an expert coding assistant operating inside pi, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.
86
+ let prompt = `You are an expert coding assistant operating inside prism, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.
74
87
 
75
88
  Available tools:
76
89
  ${toolsList}
@@ -78,16 +91,7 @@ ${toolsList}
78
91
  In addition to the tools above, you may have access to other custom tools depending on the project.
79
92
 
80
93
  Guidelines:
81
- ${guidelines}
82
-
83
- Pi documentation (read only when the user asks about pi itself, its SDK, extensions, themes, skills, or TUI):
84
- - Main documentation: ${readmePath}
85
- - Additional docs: ${docsPath}
86
- - Examples: ${examplesPath} (extensions, custom tools, SDK)
87
- - When reading pi docs or examples, resolve docs/... under Additional docs and examples/... under Examples, not the current working directory
88
- - When asked about: extensions (docs/extensions.md, examples/extensions/), themes (docs/themes.md), skills (docs/skills.md), prompt templates (docs/prompt-templates.md), TUI components (docs/tui.md), keybindings (docs/keybindings.md), SDK integrations (docs/sdk.md), custom providers (docs/custom-provider.md), adding models (docs/models.md), pi packages (docs/packages.md), environment variables (docs/environment-variables.md)
89
- - When working on pi topics, read the docs and examples, and follow .md cross-references before implementing
90
- - Always read pi .md files completely and follow links to related docs (e.g., tui.md for TUI API details)`;
94
+ ${guidelines}${documentationSection}`;
91
95
  if (appendSection) {
92
96
  prompt += appendSection;
93
97
  }
@@ -1,4 +1,5 @@
1
1
  import { spawnSync } from "node:child_process";
2
+ import { deleteSessionFromDb } from "../../../core/prism-session-db.js";
2
3
  import { existsSync } from "node:fs";
3
4
  import { unlink } from "node:fs/promises";
4
5
  import * as os from "node:os";
@@ -539,6 +540,9 @@ class SessionList {
539
540
  * Delete a session file, trying the `trash` CLI first, then falling back to unlink
540
541
  */
541
542
  async function deleteSessionFile(sessionPath) {
543
+ const _backend = process.env.PRISM_SESSION_BACKEND || "db";
544
+ if (_backend !== "file") { try { deleteSessionFromDb(sessionPath); } catch {} }
545
+ if (_backend === "db" && !existsSync(sessionPath)) return { ok: true, method: "trash" };
542
546
  // Try `trash` first (if installed)
543
547
  const trashArgs = sessionPath.startsWith("-") ? ["--", sessionPath] : [sessionPath];
544
548
  const trashResult = spawnSync("trash", trashArgs, { encoding: "utf-8" });
@@ -484,12 +484,15 @@ export class InteractiveMode {
484
484
  }));
485
485
  // Convert extension commands to SlashCommand format
486
486
  const builtinCommandNames = new Set(slashCommands.map((c) => c.name));
487
+ const prismInternalCommandExtensions = new Set(["caveman-mode.ts", "clear-session.ts", "eval-command.ts", "git-commit.ts", "plan-command.ts", "prompt-review-command.ts", "self-improve-command.ts", "voice-command.ts", "vote-command.ts", "obsidian-memory.ts", "logseq-memory.ts", "honcho-memory.ts", "research-tools.ts", "ai-memory-system.ts", "rp-memory.ts", "manifest-provider.ts", "ollama-provider.ts", "toolbox.ts", "self-improvement-tools.ts", "context-usage.ts", "prism-status.ts", "toolbar.ts", "pi-mcp-adapter"]);
488
+ const prismInternalCommandNpmPackages = new Set(["pi-mcp-adapter"]);
489
+ const isPrismInternalCommand = (cmd) => (() => { const prismExtensionPath = String(cmd.sourceInfo?.path || "").replaceAll("\\", "/"); const prismExtensionSource = String(cmd.sourceInfo?.source || ""); return prismInternalCommandExtensions.has(path.basename(prismExtensionPath)) || Array.from(prismInternalCommandNpmPackages).some((pkg) => prismExtensionSource === "npm:" + pkg || prismExtensionPath.includes("/node_modules/" + pkg + "/")); })();
487
490
  const extensionCommands = this.session.extensionRunner
488
491
  .getRegisteredCommands()
489
492
  .filter((cmd) => !builtinCommandNames.has(cmd.name))
490
493
  .map((cmd) => ({
491
494
  name: cmd.invocationName,
492
- description: this.prefixAutocompleteDescription(cmd.description, cmd.sourceInfo),
495
+ description: isPrismInternalCommand(cmd) ? cmd.description : this.prefixAutocompleteDescription(cmd.description, cmd.sourceInfo),
493
496
  getArgumentCompletions: cmd.getArgumentCompletions,
494
497
  }));
495
498
  // Build skill commands from session.skills (if enabled)
@@ -676,7 +679,7 @@ export class InteractiveMode {
676
679
  await this.themeController.applyFromSettings();
677
680
  // Add header with keybindings from config (unless silenced)
678
681
  if (this.options.verbose || !this.settingsManager.getQuietStartup()) {
679
- const logo = theme.bold(theme.fg("accent", APP_NAME)) + theme.fg("dim", ` v${this.version}`);
682
+ const logo = " \u001b[38;5;196mp\u001b[38;5;202mr\u001b[38;5;226mi\u001b[38;5;46ms\u001b[38;5;51mm\u001b[0m v0.2.92";
680
683
  // Build startup instructions using keybinding hint helpers
681
684
  const hint = (keybinding, description) => keyHint(keybinding, description);
682
685
  const expandedInstructions = [
@@ -708,7 +711,7 @@ export class InteractiveMode {
708
711
  hint("app.tools.expand", "more"),
709
712
  ].join(theme.fg("muted", " · "));
710
713
  const compactOnboarding = theme.fg("dim", `Press ${keyText("app.tools.expand")} to show full startup help and loaded resources.`);
711
- const onboarding = theme.fg("dim", `Pi can explain its own features and look up its docs. Ask it how to use or extend Pi.`);
714
+ const onboarding = theme.fg("dim", `Prism can explain its own features and look up its docs. Ask it how to use or extend Prism.`);
712
715
  this.builtInHeader = new ExpandableText(() => `${logo}\n${compactInstructions}\n${compactOnboarding}\n\n${onboarding}`, () => `${logo}\n${expandedInstructions}\n\n${onboarding}`, this.getStartupExpansionState(), 1, 0);
713
716
  // Setup UI layout
714
717
  this.headerContainer.addChild(new Spacer(1));
@@ -757,10 +760,10 @@ export class InteractiveMode {
757
760
  const cwdBasename = path.basename(this.sessionManager.getCwd());
758
761
  const sessionName = this.sessionManager.getSessionName();
759
762
  if (sessionName) {
760
- this.ui.terminal.setTitle(`${APP_TITLE} - ${sessionName} - ${cwdBasename}`);
763
+ this.ui.terminal.setTitle(`▲ - ${sessionName} - ${cwdBasename}`);
761
764
  }
762
765
  else {
763
- this.ui.terminal.setTitle(`${APP_TITLE} - ${cwdBasename}`);
766
+ this.ui.terminal.setTitle(`▲ - ${cwdBasename}`);
764
767
  }
765
768
  }
766
769
  /**
@@ -823,6 +826,7 @@ export class InteractiveMode {
823
826
  }
824
827
  catch (error) {
825
828
  const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
829
+ errorMessage = this.humanizeUsageLimitError(errorMessage);
826
830
  this.showError(errorMessage);
827
831
  }
828
832
  }
@@ -833,7 +837,8 @@ export class InteractiveMode {
833
837
  }
834
838
  catch (error) {
835
839
  const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
836
- this.showError(errorMessage);
840
+ errorMessage = this.humanizeUsageLimitError(errorMessage);
841
+ this.showError(errorMessage);
837
842
  }
838
843
  }
839
844
  }
@@ -845,6 +850,7 @@ export class InteractiveMode {
845
850
  }
846
851
  catch (error) {
847
852
  const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
853
+ errorMessage = this.humanizeUsageLimitError(errorMessage);
848
854
  this.showError(errorMessage);
849
855
  }
850
856
  }
@@ -921,14 +927,19 @@ export class InteractiveMode {
921
927
  const entries = parseChangelog(changelogPath);
922
928
  if (!lastVersion) {
923
929
  // Fresh install - record the version, send telemetry, don't show changelog
924
- this.settingsManager.setLastChangelogVersion(VERSION);
925
- this.reportInstallTelemetry(VERSION);
930
+ this.settingsManager.setLastChangelogVersion("0.84.2");
931
+ this.reportInstallTelemetry("0.84.2");
926
932
  return undefined;
927
933
  }
928
934
  const newEntries = getNewEntries(entries, lastVersion);
935
+ if (entries.length > 0 && newEntries.length === entries.length) {
936
+ this.settingsManager.setLastChangelogVersion("0.84.2");
937
+ this.reportInstallTelemetry("0.84.2");
938
+ return normalizeChangelogLinks(entries[0].content, entries[0]);
939
+ }
929
940
  if (newEntries.length > 0) {
930
- this.settingsManager.setLastChangelogVersion(VERSION);
931
- this.reportInstallTelemetry(VERSION);
941
+ this.settingsManager.setLastChangelogVersion("0.84.2");
942
+ this.reportInstallTelemetry("0.84.2");
932
943
  return newEntries.map((e) => normalizeChangelogLinks(e.content, e)).join("\n\n");
933
944
  }
934
945
  return undefined;
@@ -1266,7 +1277,7 @@ export class InteractiveMode {
1266
1277
  const skillsResult = this.session.resourceLoader.getSkills();
1267
1278
  const promptsResult = this.session.resourceLoader.getPrompts();
1268
1279
  const themesResult = this.session.resourceLoader.getThemes();
1269
- const extensions = options?.extensions ??
1280
+ let extensions = options?.extensions ??
1270
1281
  this.session.resourceLoader
1271
1282
  .getExtensions()
1272
1283
  .extensions.filter((extension) => !extension.hidden)
@@ -1337,6 +1348,9 @@ export class InteractiveMode {
1337
1348
  const promptCompactList = formatCompactList(templates.map((template) => `/${template.name}`));
1338
1349
  addLoadedSection("Prompts", promptCompactList, templateList);
1339
1350
  }
1351
+ const prismInternalExtensions = new Set(["caveman-mode.ts", "clear-session.ts", "eval-command.ts", "git-commit.ts", "plan-command.ts", "prompt-review-command.ts", "self-improve-command.ts", "voice-command.ts", "vote-command.ts", "obsidian-memory.ts", "logseq-memory.ts", "honcho-memory.ts", "research-tools.ts", "ai-memory-system.ts", "rp-memory.ts", "manifest-provider.ts", "ollama-provider.ts", "toolbox.ts", "self-improvement-tools.ts", "context-usage.ts", "prism-status.ts", "toolbar.ts", "pi-mcp-adapter"]);
1352
+ const prismInternalNpmPackages = new Set(["pi-mcp-adapter"]);
1353
+ extensions = extensions.filter((extension) => !(() => { const prismExtensionPath = String(extension.path || "").replaceAll("\\", "/"); const prismExtensionSource = String(extension.sourceInfo?.source || ""); return prismInternalExtensions.has(path.basename(prismExtensionPath)) || Array.from(prismInternalNpmPackages).some((pkg) => prismExtensionSource === "npm:" + pkg || prismExtensionPath.includes("/node_modules/" + pkg + "/")); })());
1340
1354
  if (extensions.length > 0) {
1341
1355
  const groups = this.buildScopeGroups(extensions);
1342
1356
  const extList = this.formatScopeGroups(groups, {
@@ -1348,7 +1362,8 @@ export class InteractiveMode {
1348
1362
  }
1349
1363
  // Show loaded themes (excluding built-in)
1350
1364
  const loadedThemes = themesResult.themes;
1351
- const customThemes = loadedThemes.filter((t) => t.sourcePath);
1365
+ const prismInternalThemes = new Set(["soft-green"]);
1366
+ const customThemes = loadedThemes.filter((t) => t.sourcePath && !prismInternalThemes.has(t.name));
1352
1367
  if (customThemes.length > 0) {
1353
1368
  const groups = this.buildScopeGroups(customThemes.map((loadedTheme) => ({
1354
1369
  path: loadedTheme.sourcePath,
@@ -2428,6 +2443,27 @@ export class InteractiveMode {
2428
2443
  await this.handleCompactCommand(customInstructions);
2429
2444
  return;
2430
2445
  }
2446
+ if (text === "/plan" || text.startsWith("/plan ")) {
2447
+ const task = text.startsWith("/plan ") ? text.slice(6).trim() : "";
2448
+ this.editor.setText("");
2449
+ await this.startPrismPlanMode(task);
2450
+ return;
2451
+ }
2452
+ if (text === "/plan:approve") {
2453
+ this.editor.setText("");
2454
+ await this.approvePrismPlan();
2455
+ return;
2456
+ }
2457
+ if (text === "/plan:reject") {
2458
+ this.editor.setText("");
2459
+ this.rejectPrismPlan();
2460
+ return;
2461
+ }
2462
+ if (text === "/plan:cancel") {
2463
+ this.editor.setText("");
2464
+ this.cancelPrismPlan();
2465
+ return;
2466
+ }
2431
2467
  if (text === "/reload") {
2432
2468
  this.editor.setText("");
2433
2469
  await this.handleReloadCommand();
@@ -2692,6 +2728,8 @@ export class InteractiveMode {
2692
2728
  break;
2693
2729
  case "agent_settled":
2694
2730
  await this.checkShutdownRequested();
2731
+ void this.maybeShowUsageLimitRecoveryMenu(event);
2732
+ void this.maybeShowPrismPlanApprovalMenu(event);
2695
2733
  break;
2696
2734
  case "compaction_start": {
2697
2735
  if (this.settingsManager.getShowTerminalProgress()) {
@@ -2790,6 +2828,189 @@ export class InteractiveMode {
2790
2828
  }
2791
2829
  }
2792
2830
  }
2831
+ formatUsageLimitDuration(ms) {
2832
+ const totalMinutes = Math.ceil(ms / 60000);
2833
+ const hours = Math.floor(totalMinutes / 60);
2834
+ const minutes = totalMinutes % 60;
2835
+ const parts = [];
2836
+ if (hours > 0) parts.push(hours + "h");
2837
+ if (minutes > 0 || parts.length === 0) parts.push(minutes + "min");
2838
+ return parts.join(" ");
2839
+ }
2840
+ humanizeUsageLimitError(errorMessage) {
2841
+ return String(errorMessage || "").replace(/(~|about\s*)?(\d+)\s*min\b/gi, (_match, prefix, minutes) => {
2842
+ return (prefix || "") + this.formatUsageLimitDuration(Number(minutes) * 60 * 1000);
2843
+ });
2844
+ }
2845
+ extractUsageLimitWaitMs(errorMessage) {
2846
+ const text = String(errorMessage || "");
2847
+ const parsedSeconds = text.match(/"(?:resets_in_seconds|X-Codex-Primary-Reset-After-Seconds|Retry-After)"\s*:?\s*"?(\d+)"?/i)?.[1];
2848
+ if (parsedSeconds) return Number(parsedSeconds) * 1000;
2849
+ const resetAt = text.match(/"(?:resets_at|X-Codex-Primary-Reset-At)"\s*:?\s*"?(\d+)"?/i)?.[1];
2850
+ if (resetAt) {
2851
+ const resetMs = Number(resetAt) * 1000 - Date.now();
2852
+ if (Number.isFinite(resetMs) && resetMs > 0) return resetMs;
2853
+ }
2854
+ const hours = text.match(/(?:~|about\s*)?(\d+)\s*h(?:our)?/i)?.[1];
2855
+ if (hours) return Number(hours) * 60 * 60 * 1000;
2856
+ const minutes = text.match(/(?:~|about\s*)?(\d+)\s*min/i)?.[1];
2857
+ if (minutes) return Number(minutes) * 60 * 1000;
2858
+ const seconds = text.match(/(?:~|about\s*)?(\d+)\s*s(?:ec|econd)?/i)?.[1];
2859
+ if (seconds) return Number(seconds) * 1000;
2860
+ return 5 * 60 * 1000;
2861
+ }
2862
+ getUsageLimitError(event) {
2863
+ const last = event?.messages?.[event.messages.length - 1];
2864
+ let errorMessage = last?.errorMessage || last?.content?.find?.((block) => block?.type === "text")?.text || "";
2865
+ if (last?.role !== "assistant" || last?.stopReason !== "error") return undefined;
2866
+ errorMessage = this.humanizeUsageLimitError(errorMessage);
2867
+ return /usage limit|try again in/i.test(errorMessage) ? String(errorMessage) : undefined;
2868
+ }
2869
+ removeLastAssistantErrorFromState() {
2870
+ const messages = this.agent.state.messages;
2871
+ const last = messages[messages.length - 1];
2872
+ if (last?.role === "assistant" && last?.stopReason === "error") this.agent.state.messages = messages.slice(0, -1);
2873
+ }
2874
+ async continueAfterUsageLimitRecovery() {
2875
+ try {
2876
+ await this.agent.continue();
2877
+ } catch (error) {
2878
+ this.showError(error instanceof Error ? error.message : String(error));
2879
+ }
2880
+ }
2881
+ maybeShowUsageLimitRecoveryMenu(event) {
2882
+ const errorMessage = this.getUsageLimitError(event);
2883
+ if (!errorMessage) return;
2884
+ const delayMs = this.extractUsageLimitWaitMs(errorMessage);
2885
+ const waitLabel = "Wait " + this.formatUsageLimitDuration(delayMs) + " then retry";
2886
+ this.showSelector((done) => {
2887
+ const selector = new ExtensionSelectorComponent("Usage limit hit", [waitLabel, "Change model and retry", "Cancel"], async (selected) => {
2888
+ done();
2889
+ if (selected === "Cancel") return;
2890
+ this.removeLastAssistantErrorFromState();
2891
+ if (selected === waitLabel) {
2892
+ const endTime = Date.now() + delayMs;
2893
+ const updateCountdown = () => {
2894
+ const remaining = Math.max(0, endTime - Date.now());
2895
+ this.showStatus("Waiting " + this.formatUsageLimitDuration(remaining) + " for usage limit, then retrying...");
2896
+ this.ui.requestRender();
2897
+ };
2898
+ updateCountdown();
2899
+ const countdownInterval = setInterval(updateCountdown, 60000);
2900
+ setTimeout(() => { clearInterval(countdownInterval); void this.continueAfterUsageLimitRecovery(); }, delayMs);
2901
+ return;
2902
+ }
2903
+ this.showUsageLimitModelSelector();
2904
+ }, () => {
2905
+ done();
2906
+ this.ui.requestRender();
2907
+ }, { tui: this.ui });
2908
+ return { component: selector, focus: selector };
2909
+ });
2910
+ }
2911
+ showUsageLimitModelSelector(initialSearchInput) {
2912
+ this.showSelector((done) => {
2913
+ const selector = new ModelSelectorComponent(this.ui, this.session.model, this.settingsManager, this.session.modelRegistry, this.session.scopedModels, async (model) => {
2914
+ try {
2915
+ await this.session.setModel(model);
2916
+ this.footer.invalidate();
2917
+ this.updateEditorBorderColor();
2918
+ done();
2919
+ this.showStatus("Model: " + model.id + "; retrying...");
2920
+ void this.maybeWarnAboutAnthropicSubscriptionAuth(model);
2921
+ this.checkDaxnutsEasterEgg(model);
2922
+ void this.continueAfterUsageLimitRecovery();
2923
+ } catch (error) {
2924
+ done();
2925
+ this.showError(error instanceof Error ? error.message : String(error));
2926
+ }
2927
+ }, () => {
2928
+ done();
2929
+ this.ui.requestRender();
2930
+ }, initialSearchInput);
2931
+ return { component: selector, focus: selector };
2932
+ });
2933
+ }
2934
+ getPrismReadOnlyToolNames() {
2935
+ const readOnly = new Set(["read", "grep", "find", "ls", "obsidian_memory_read", "obsidian_memory_search", "obsidian_memory_list", "web_search", "web_fetch"]);
2936
+ return this.session.getActiveToolNames().filter((name) => readOnly.has(name));
2937
+ }
2938
+ setPrismPlanStatus(enabled) {
2939
+ this.prismPlanMode = enabled;
2940
+ this.setExtensionStatus("prism-mode", enabled ? "[Plan]" : undefined);
2941
+ this.footer.invalidate();
2942
+ }
2943
+ async startPrismPlanMode(task) {
2944
+ if (!this.prismPlanPreviousTools) this.prismPlanPreviousTools = this.session.getActiveToolNames();
2945
+ if (this.prismPlanLeafId === undefined) this.prismPlanLeafId = this.session.sessionManager.getLeafId();
2946
+ this.session.setActiveToolsByName(this.getPrismReadOnlyToolNames());
2947
+ this.setPrismPlanStatus(true);
2948
+ globalThis.__PRISM_MANIFEST_TIER_OVERRIDE__ = "plan";
2949
+ const prompt = "PLAN MODE ACTIVE. You must only inspect and plan. Do not modify files or run mutating commands. Ask concise clarification questions if needed. When ready, write the full implementation plan, then end with exactly: /plan:ready\n\nTask: " + (task || "Create an implementation plan for the user's next change.");
2950
+ await this.session.prompt(prompt);
2951
+ }
2952
+ getPrismLastAssistantText(event) {
2953
+ const last = event?.messages?.[event.messages.length - 1];
2954
+ if (last?.role !== "assistant") return "";
2955
+ return (last.content || []).map((block) => block?.type === "text" ? block.text || "" : "").join("\n");
2956
+ }
2957
+ maybeShowPrismPlanApprovalMenu(event) {
2958
+ if (!this.prismPlanMode) return;
2959
+ const text = this.getPrismLastAssistantText(event);
2960
+ if (!text.includes("/plan:ready")) return;
2961
+ this.prismPlanText = text.replace("/plan:ready", "").trim();
2962
+ this.showSelector((done) => {
2963
+ const selector = new ExtensionSelectorComponent("Plan ready", ["Approve -> make changes", "Reject -> add more context", "Cancel plan"], async (selected) => {
2964
+ done();
2965
+ if (selected.startsWith("Approve")) return void this.approvePrismPlan();
2966
+ if (selected.startsWith("Reject")) return this.rejectPrismPlan();
2967
+ this.cancelPrismPlan();
2968
+ }, () => {
2969
+ done();
2970
+ this.ui.requestRender();
2971
+ }, { tui: this.ui });
2972
+ return { component: selector, focus: selector };
2973
+ });
2974
+ }
2975
+ restorePrismPlanTools() {
2976
+ if (this.prismPlanPreviousTools) this.session.setActiveToolsByName(this.prismPlanPreviousTools);
2977
+ this.prismPlanPreviousTools = undefined;
2978
+ if (globalThis.__PRISM_MANIFEST_TIER_OVERRIDE__ === "plan") globalThis.__PRISM_MANIFEST_TIER_OVERRIDE__ = undefined;
2979
+ this.setPrismPlanStatus(false);
2980
+ }
2981
+ restorePrismPlanBranch() {
2982
+ if (this.prismPlanLeafId === undefined) return;
2983
+ if (this.prismPlanLeafId === null) {
2984
+ this.session.sessionManager.resetLeaf();
2985
+ } else {
2986
+ this.session.sessionManager.branch(this.prismPlanLeafId);
2987
+ }
2988
+ this.agent.state.messages = this.session.sessionManager.buildSessionContext().messages;
2989
+ this.prismPlanLeafId = undefined;
2990
+ }
2991
+ async approvePrismPlan() {
2992
+ if (!this.prismPlanMode) return this.showWarning("No active plan to approve.");
2993
+ const planText = this.prismPlanText || "";
2994
+ this.prismPlanText = undefined;
2995
+ this.restorePrismPlanBranch();
2996
+ this.restorePrismPlanTools();
2997
+ await this.session.prompt("Implement this plan:\n\n" + planText);
2998
+ }
2999
+ rejectPrismPlan() {
3000
+ if (!this.prismPlanMode) return this.showWarning("No active plan to reject.");
3001
+ this.prismPlanText = undefined;
3002
+ this.editor.setText("/plan ");
3003
+ this.showStatus("Add more context after /plan and submit.");
3004
+ this.ui.requestRender();
3005
+ }
3006
+ cancelPrismPlan() {
3007
+ if (!this.prismPlanMode) return this.showWarning("No active plan to cancel.");
3008
+ this.restorePrismPlanBranch();
3009
+ this.prismPlanText = undefined;
3010
+ this.restorePrismPlanTools();
3011
+ this.showStatus("Plan cancelled");
3012
+ this.ui.requestRender();
3013
+ }
2793
3014
  /** Extract text content from a user message */
2794
3015
  getUserMessageText(message) {
2795
3016
  if (message.role !== "user")
@@ -202,7 +202,7 @@ export class CombinedAutocompleteProvider {
202
202
  prefix: atPrefix,
203
203
  };
204
204
  }
205
- if (!options.force && textBeforeCursor.startsWith("/")) {
205
+ if (textBeforeCursor.startsWith("/")) {
206
206
  const spaceIndex = textBeforeCursor.indexOf(" ");
207
207
  if (spaceIndex === -1) {
208
208
  const prefix = textBeforeCursor.slice(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daniel156161/prism",
3
- "version": "0.2.90",
3
+ "version": "0.2.92",
4
4
  "description": "Prism-branded wrapper around pi that stores config in ~/.prism",
5
5
  "type": "module",
6
6
  "engines": {
@@ -27,9 +27,7 @@
27
27
  "dependencies": {
28
28
  "@earendil-works/pi-ai": "^0.84.2",
29
29
  "@earendil-works/pi-coding-agent": "^0.84.2",
30
- "eventsource": "^5.1.0",
31
30
  "pi-mcp-adapter": "^2.26.0",
32
- "pkce-challenge": "^6.0.0",
33
31
  "typebox": "^1.3.14"
34
32
  },
35
33
  "bundledDependencies": [