@oh-my-pi/pi-coding-agent 3.15.1 → 3.20.1
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 +60 -0
- package/docs/extensions.md +1055 -0
- package/docs/rpc.md +69 -13
- package/docs/session-tree-plan.md +1 -1
- package/examples/extensions/README.md +141 -0
- package/examples/extensions/api-demo.ts +87 -0
- package/examples/extensions/chalk-logger.ts +26 -0
- package/examples/extensions/hello.ts +33 -0
- package/examples/extensions/pirate.ts +44 -0
- package/examples/extensions/plan-mode.ts +551 -0
- package/examples/extensions/subagent/agents/reviewer.md +35 -0
- package/examples/extensions/todo.ts +299 -0
- package/examples/extensions/tools.ts +145 -0
- package/examples/extensions/with-deps/index.ts +36 -0
- package/examples/extensions/with-deps/package-lock.json +31 -0
- package/examples/extensions/with-deps/package.json +16 -0
- package/examples/sdk/02-custom-model.ts +3 -3
- package/examples/sdk/05-tools.ts +7 -3
- package/examples/sdk/06-extensions.ts +81 -0
- package/examples/sdk/06-hooks.ts +14 -13
- package/examples/sdk/08-prompt-templates.ts +42 -0
- package/examples/sdk/08-slash-commands.ts +17 -12
- package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
- package/examples/sdk/12-full-control.ts +6 -6
- package/package.json +11 -7
- package/src/capability/extension-module.ts +34 -0
- package/src/cli/args.ts +22 -7
- package/src/cli/file-processor.ts +38 -67
- package/src/cli/list-models.ts +1 -1
- package/src/config.ts +25 -14
- package/src/core/agent-session.ts +505 -242
- package/src/core/auth-storage.ts +33 -21
- package/src/core/compaction/branch-summarization.ts +4 -4
- package/src/core/compaction/compaction.ts +3 -3
- package/src/core/custom-commands/bundled/wt/index.ts +430 -0
- package/src/core/custom-commands/loader.ts +9 -0
- package/src/core/custom-tools/wrapper.ts +5 -0
- package/src/core/event-bus.ts +59 -0
- package/src/core/export-html/vendor/highlight.min.js +1213 -0
- package/src/core/export-html/vendor/marked.min.js +6 -0
- package/src/core/extensions/index.ts +100 -0
- package/src/core/extensions/loader.ts +501 -0
- package/src/core/extensions/runner.ts +477 -0
- package/src/core/extensions/types.ts +712 -0
- package/src/core/extensions/wrapper.ts +147 -0
- package/src/core/hooks/types.ts +2 -2
- package/src/core/index.ts +10 -21
- package/src/core/keybindings.ts +199 -0
- package/src/core/messages.ts +26 -7
- package/src/core/model-registry.ts +123 -46
- package/src/core/model-resolver.ts +7 -5
- package/src/core/prompt-templates.ts +242 -0
- package/src/core/sdk.ts +378 -295
- package/src/core/session-manager.ts +72 -58
- package/src/core/settings-manager.ts +118 -22
- package/src/core/system-prompt.ts +24 -1
- package/src/core/terminal-notify.ts +37 -0
- package/src/core/tools/context.ts +4 -4
- package/src/core/tools/exa/mcp-client.ts +5 -4
- package/src/core/tools/exa/render.ts +176 -131
- package/src/core/tools/find.ts +7 -1
- package/src/core/tools/gemini-image.ts +361 -0
- package/src/core/tools/git.ts +216 -0
- package/src/core/tools/index.ts +28 -15
- package/src/core/tools/ls.ts +9 -2
- package/src/core/tools/lsp/config.ts +5 -4
- package/src/core/tools/lsp/index.ts +17 -12
- package/src/core/tools/lsp/render.ts +39 -47
- package/src/core/tools/read.ts +66 -29
- package/src/core/tools/render-utils.ts +268 -0
- package/src/core/tools/renderers.ts +243 -225
- package/src/core/tools/task/discovery.ts +2 -2
- package/src/core/tools/task/executor.ts +66 -58
- package/src/core/tools/task/index.ts +29 -10
- package/src/core/tools/task/model-resolver.ts +8 -13
- package/src/core/tools/task/omp-command.ts +24 -0
- package/src/core/tools/task/render.ts +37 -62
- package/src/core/tools/task/types.ts +3 -0
- package/src/core/tools/web-fetch.ts +29 -28
- package/src/core/tools/web-search/index.ts +6 -5
- package/src/core/tools/web-search/providers/exa.ts +6 -5
- package/src/core/tools/web-search/render.ts +66 -111
- package/src/core/voice-controller.ts +135 -0
- package/src/core/voice-supervisor.ts +1003 -0
- package/src/core/voice.ts +308 -0
- package/src/discovery/builtin.ts +75 -1
- package/src/discovery/claude.ts +47 -1
- package/src/discovery/codex.ts +54 -2
- package/src/discovery/gemini.ts +55 -2
- package/src/discovery/helpers.ts +100 -1
- package/src/discovery/index.ts +2 -0
- package/src/index.ts +14 -9
- package/src/lib/worktree/collapse.ts +179 -0
- package/src/lib/worktree/constants.ts +14 -0
- package/src/lib/worktree/errors.ts +23 -0
- package/src/lib/worktree/git.ts +110 -0
- package/src/lib/worktree/index.ts +23 -0
- package/src/lib/worktree/operations.ts +216 -0
- package/src/lib/worktree/session.ts +114 -0
- package/src/lib/worktree/stats.ts +67 -0
- package/src/main.ts +61 -37
- package/src/migrations.ts +37 -7
- package/src/modes/interactive/components/bash-execution.ts +6 -4
- package/src/modes/interactive/components/custom-editor.ts +55 -0
- package/src/modes/interactive/components/custom-message.ts +95 -0
- package/src/modes/interactive/components/extensions/extension-list.ts +5 -0
- package/src/modes/interactive/components/extensions/inspector-panel.ts +18 -12
- package/src/modes/interactive/components/extensions/state-manager.ts +12 -0
- package/src/modes/interactive/components/extensions/types.ts +1 -0
- package/src/modes/interactive/components/footer.ts +324 -0
- package/src/modes/interactive/components/hook-selector.ts +3 -3
- package/src/modes/interactive/components/model-selector.ts +7 -6
- package/src/modes/interactive/components/oauth-selector.ts +3 -3
- package/src/modes/interactive/components/settings-defs.ts +55 -6
- package/src/modes/interactive/components/status-line.ts +45 -37
- package/src/modes/interactive/components/tool-execution.ts +95 -23
- package/src/modes/interactive/interactive-mode.ts +643 -113
- package/src/modes/interactive/theme/defaults/index.ts +16 -16
- package/src/modes/print-mode.ts +14 -72
- package/src/modes/rpc/rpc-client.ts +23 -9
- package/src/modes/rpc/rpc-mode.ts +137 -125
- package/src/modes/rpc/rpc-types.ts +46 -24
- package/src/prompts/task.md +1 -0
- package/src/prompts/tools/gemini-image.md +4 -0
- package/src/prompts/tools/git.md +9 -0
- package/src/prompts/voice-summary.md +12 -0
- package/src/utils/image-convert.ts +26 -0
- package/src/utils/image-resize.ts +215 -0
- package/src/utils/shell-snapshot.ts +22 -20
|
@@ -1,19 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
|
-
appendFileSync,
|
|
3
2
|
closeSync,
|
|
4
3
|
createWriteStream,
|
|
5
4
|
existsSync,
|
|
6
5
|
fsyncSync,
|
|
7
6
|
mkdirSync,
|
|
8
7
|
openSync,
|
|
9
|
-
readdirSync,
|
|
10
8
|
readFileSync,
|
|
11
9
|
readSync,
|
|
12
10
|
renameSync,
|
|
13
11
|
statSync,
|
|
14
12
|
unlinkSync,
|
|
15
13
|
type WriteStream,
|
|
16
|
-
writeFileSync,
|
|
17
14
|
} from "node:fs";
|
|
18
15
|
import { basename, join, resolve } from "node:path";
|
|
19
16
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
@@ -23,13 +20,14 @@ import sharp from "sharp";
|
|
|
23
20
|
import { getAgentDir as getDefaultAgentDir } from "../config";
|
|
24
21
|
import {
|
|
25
22
|
type BashExecutionMessage,
|
|
23
|
+
type CustomMessage,
|
|
26
24
|
createBranchSummaryMessage,
|
|
27
25
|
createCompactionSummaryMessage,
|
|
28
|
-
|
|
26
|
+
createCustomMessage,
|
|
29
27
|
type HookMessage,
|
|
30
28
|
} from "./messages";
|
|
31
29
|
|
|
32
|
-
export const CURRENT_SESSION_VERSION =
|
|
30
|
+
export const CURRENT_SESSION_VERSION = 3;
|
|
33
31
|
|
|
34
32
|
export interface SessionHeader {
|
|
35
33
|
type: "session";
|
|
@@ -75,27 +73,27 @@ export interface CompactionEntry<T = unknown> extends SessionEntryBase {
|
|
|
75
73
|
summary: string;
|
|
76
74
|
firstKeptEntryId: string;
|
|
77
75
|
tokensBefore: number;
|
|
78
|
-
/**
|
|
76
|
+
/** Extension-specific data (e.g., ArtifactIndex, version markers for structured compaction) */
|
|
79
77
|
details?: T;
|
|
80
|
-
/** True if generated by
|
|
81
|
-
|
|
78
|
+
/** True if generated by an extension, undefined/false if pi-generated (backward compatible) */
|
|
79
|
+
fromExtension?: boolean;
|
|
82
80
|
}
|
|
83
81
|
|
|
84
82
|
export interface BranchSummaryEntry<T = unknown> extends SessionEntryBase {
|
|
85
83
|
type: "branch_summary";
|
|
86
84
|
fromId: string;
|
|
87
85
|
summary: string;
|
|
88
|
-
/**
|
|
86
|
+
/** Extension-specific data (not sent to LLM) */
|
|
89
87
|
details?: T;
|
|
90
|
-
/** True if generated by
|
|
91
|
-
|
|
88
|
+
/** True if generated by an extension, false if pi-generated */
|
|
89
|
+
fromExtension?: boolean;
|
|
92
90
|
}
|
|
93
91
|
|
|
94
92
|
/**
|
|
95
|
-
* Custom entry for
|
|
96
|
-
* Use customType to identify your
|
|
93
|
+
* Custom entry for extensions to store extension-specific data in the session.
|
|
94
|
+
* Use customType to identify your extension's entries.
|
|
97
95
|
*
|
|
98
|
-
* Purpose: Persist
|
|
96
|
+
* Purpose: Persist extension state across session reloads. On reload, extensions can
|
|
99
97
|
* scan entries for their customType and reconstruct internal state.
|
|
100
98
|
*
|
|
101
99
|
* Does NOT participate in LLM context (ignored by buildSessionContext).
|
|
@@ -122,12 +120,12 @@ export interface TtsrInjectionEntry extends SessionEntryBase {
|
|
|
122
120
|
}
|
|
123
121
|
|
|
124
122
|
/**
|
|
125
|
-
* Custom message entry for
|
|
126
|
-
* Use customType to identify your
|
|
123
|
+
* Custom message entry for extensions to inject messages into LLM context.
|
|
124
|
+
* Use customType to identify your extension's entries.
|
|
127
125
|
*
|
|
128
126
|
* Unlike CustomEntry, this DOES participate in LLM context.
|
|
129
127
|
* The content is converted to a user message in buildSessionContext().
|
|
130
|
-
* Use details for
|
|
128
|
+
* Use details for extension-specific metadata (not sent to LLM).
|
|
131
129
|
*
|
|
132
130
|
* display controls TUI rendering:
|
|
133
131
|
* - false: hidden entirely
|
|
@@ -239,8 +237,22 @@ function migrateV1ToV2(entries: FileEntry[]): void {
|
|
|
239
237
|
}
|
|
240
238
|
}
|
|
241
239
|
|
|
242
|
-
|
|
243
|
-
|
|
240
|
+
/** Migrate v2 → v3: rename hookMessage role to custom. Mutates in place. */
|
|
241
|
+
function migrateV2ToV3(entries: FileEntry[]): void {
|
|
242
|
+
for (const entry of entries) {
|
|
243
|
+
if (entry.type === "session") {
|
|
244
|
+
entry.version = 3;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (entry.type === "message") {
|
|
249
|
+
const msg = entry.message as { role?: string };
|
|
250
|
+
if (msg.role === "hookMessage") {
|
|
251
|
+
(entry.message as { role: string }).role = "custom";
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
244
256
|
|
|
245
257
|
/**
|
|
246
258
|
* Run all necessary migrations to bring entries to current version.
|
|
@@ -253,7 +265,7 @@ function migrateToCurrentVersion(entries: FileEntry[]): boolean {
|
|
|
253
265
|
if (version >= CURRENT_SESSION_VERSION) return false;
|
|
254
266
|
|
|
255
267
|
if (version < 2) migrateV1ToV2(entries);
|
|
256
|
-
|
|
268
|
+
if (version < 3) migrateV2ToV3(entries);
|
|
257
269
|
|
|
258
270
|
return true;
|
|
259
271
|
}
|
|
@@ -380,7 +392,7 @@ export function buildSessionContext(
|
|
|
380
392
|
messages.push(entry.message);
|
|
381
393
|
} else if (entry.type === "custom_message") {
|
|
382
394
|
messages.push(
|
|
383
|
-
|
|
395
|
+
createCustomMessage(entry.customType, entry.content, entry.display, entry.details, entry.timestamp),
|
|
384
396
|
);
|
|
385
397
|
} else if (entry.type === "branch_summary" && entry.summary) {
|
|
386
398
|
messages.push(createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp));
|
|
@@ -438,7 +450,7 @@ function getDefaultSessionDir(cwd: string): string {
|
|
|
438
450
|
export function loadEntriesFromFile(filePath: string): FileEntry[] {
|
|
439
451
|
if (!existsSync(filePath)) return [];
|
|
440
452
|
|
|
441
|
-
const content = readFileSync(filePath, "
|
|
453
|
+
const content = readFileSync(filePath, "utf-8");
|
|
442
454
|
const entries: FileEntry[] = [];
|
|
443
455
|
const lines = content.trim().split("\n");
|
|
444
456
|
|
|
@@ -535,10 +547,9 @@ function getSortedSessions(sessionDir: string): RecentSessionInfo[] {
|
|
|
535
547
|
return header;
|
|
536
548
|
};
|
|
537
549
|
|
|
538
|
-
return
|
|
550
|
+
return Array.from(new Bun.Glob("*.jsonl").scanSync(sessionDir))
|
|
539
551
|
.map((f) => {
|
|
540
552
|
try {
|
|
541
|
-
if (!f.endsWith(".jsonl")) return null;
|
|
542
553
|
const path = join(sessionDir, f);
|
|
543
554
|
const fd = openSync(path, "r");
|
|
544
555
|
try {
|
|
@@ -927,8 +938,8 @@ export class SessionManager {
|
|
|
927
938
|
}
|
|
928
939
|
|
|
929
940
|
/** Initialize with a specific session file (used by factory methods) */
|
|
930
|
-
private
|
|
931
|
-
|
|
941
|
+
private _initSessionFile(sessionFile: string): void {
|
|
942
|
+
this.setSessionFile(sessionFile);
|
|
932
943
|
}
|
|
933
944
|
|
|
934
945
|
/** Initialize with a new session (used by factory methods) */
|
|
@@ -937,23 +948,25 @@ export class SessionManager {
|
|
|
937
948
|
}
|
|
938
949
|
|
|
939
950
|
/** Switch to a different session file (used for resume and branching) */
|
|
940
|
-
|
|
941
|
-
|
|
951
|
+
setSessionFile(sessionFile: string): void {
|
|
952
|
+
void this._closePersistWriter();
|
|
942
953
|
this.persistError = undefined;
|
|
943
954
|
this.persistErrorReported = false;
|
|
944
955
|
this.sessionFile = resolve(sessionFile);
|
|
945
956
|
if (existsSync(this.sessionFile)) {
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
957
|
+
void (async () => {
|
|
958
|
+
this.fileEntries = await loadEntriesFromFile(this.sessionFile!);
|
|
959
|
+
const header = this.fileEntries.find((e) => e.type === "session") as SessionHeader | undefined;
|
|
960
|
+
this.sessionId = header?.id ?? nanoid();
|
|
961
|
+
this.sessionTitle = header?.title;
|
|
962
|
+
|
|
963
|
+
if (migrateToCurrentVersion(this.fileEntries)) {
|
|
964
|
+
await this._rewriteFile();
|
|
965
|
+
}
|
|
954
966
|
|
|
955
|
-
|
|
956
|
-
|
|
967
|
+
this._buildIndex();
|
|
968
|
+
this.flushed = true;
|
|
969
|
+
})();
|
|
957
970
|
} else {
|
|
958
971
|
this._newSessionSync();
|
|
959
972
|
}
|
|
@@ -1178,7 +1191,7 @@ export class SessionManager {
|
|
|
1178
1191
|
fileHeader.title = title;
|
|
1179
1192
|
lines[0] = JSON.stringify(fileHeader);
|
|
1180
1193
|
const tempPath = join(resolve(sessionFile, ".."), `.${basename(sessionFile)}.${nanoid(6)}.tmp`);
|
|
1181
|
-
|
|
1194
|
+
await Bun.write(tempPath, lines.join("\n"));
|
|
1182
1195
|
const fd = openSync(tempPath, "r");
|
|
1183
1196
|
try {
|
|
1184
1197
|
fsyncSync(fd);
|
|
@@ -1265,7 +1278,7 @@ export class SessionManager {
|
|
|
1265
1278
|
* so it is easier to find them.
|
|
1266
1279
|
* These need to be appended via appendCompaction() and appendBranchSummary() methods.
|
|
1267
1280
|
*/
|
|
1268
|
-
appendMessage(message: Message | HookMessage | BashExecutionMessage): string {
|
|
1281
|
+
appendMessage(message: Message | CustomMessage | HookMessage | BashExecutionMessage): string {
|
|
1269
1282
|
const entry: SessionMessageEntry = {
|
|
1270
1283
|
type: "message",
|
|
1271
1284
|
id: generateId(this.byId),
|
|
@@ -1314,7 +1327,7 @@ export class SessionManager {
|
|
|
1314
1327
|
firstKeptEntryId: string,
|
|
1315
1328
|
tokensBefore: number,
|
|
1316
1329
|
details?: T,
|
|
1317
|
-
|
|
1330
|
+
fromExtension?: boolean,
|
|
1318
1331
|
): string {
|
|
1319
1332
|
const entry: CompactionEntry<T> = {
|
|
1320
1333
|
type: "compaction",
|
|
@@ -1325,13 +1338,13 @@ export class SessionManager {
|
|
|
1325
1338
|
firstKeptEntryId,
|
|
1326
1339
|
tokensBefore,
|
|
1327
1340
|
details,
|
|
1328
|
-
|
|
1341
|
+
fromExtension,
|
|
1329
1342
|
};
|
|
1330
1343
|
this._appendEntry(entry);
|
|
1331
1344
|
return entry.id;
|
|
1332
1345
|
}
|
|
1333
1346
|
|
|
1334
|
-
/** Append a custom entry (for
|
|
1347
|
+
/** Append a custom entry (for extensions) as child of current leaf, then advance leaf. Returns entry id. */
|
|
1335
1348
|
appendCustomEntry(customType: string, data?: unknown): string {
|
|
1336
1349
|
const entry: CustomEntry = {
|
|
1337
1350
|
type: "custom",
|
|
@@ -1346,11 +1359,11 @@ export class SessionManager {
|
|
|
1346
1359
|
}
|
|
1347
1360
|
|
|
1348
1361
|
/**
|
|
1349
|
-
* Append a custom message entry (for
|
|
1362
|
+
* Append a custom message entry (for extensions) that participates in LLM context.
|
|
1350
1363
|
* @param customType Hook identifier for filtering on reload
|
|
1351
1364
|
* @param content Message content (string or TextContent/ImageContent array)
|
|
1352
1365
|
* @param display Whether to show in TUI (true = styled display, false = hidden)
|
|
1353
|
-
* @param details Optional
|
|
1366
|
+
* @param details Optional extension-specific metadata (not sent to LLM)
|
|
1354
1367
|
* @returns Entry id
|
|
1355
1368
|
*/
|
|
1356
1369
|
appendCustomMessageEntry<T = unknown>(
|
|
@@ -1589,7 +1602,7 @@ export class SessionManager {
|
|
|
1589
1602
|
* Same as branch(), but also appends a branch_summary entry that captures
|
|
1590
1603
|
* context from the abandoned conversation path.
|
|
1591
1604
|
*/
|
|
1592
|
-
branchWithSummary(branchFromId: string | null, summary: string, details?: unknown,
|
|
1605
|
+
branchWithSummary(branchFromId: string | null, summary: string, details?: unknown, fromExtension?: boolean): string {
|
|
1593
1606
|
if (branchFromId !== null && !this.byId.has(branchFromId)) {
|
|
1594
1607
|
throw new Error(`Entry ${branchFromId} not found`);
|
|
1595
1608
|
}
|
|
@@ -1602,7 +1615,7 @@ export class SessionManager {
|
|
|
1602
1615
|
fromId: branchFromId ?? "root",
|
|
1603
1616
|
summary,
|
|
1604
1617
|
details,
|
|
1605
|
-
|
|
1618
|
+
fromExtension,
|
|
1606
1619
|
};
|
|
1607
1620
|
this._appendEntry(entry);
|
|
1608
1621
|
return entry.id;
|
|
@@ -1646,9 +1659,11 @@ export class SessionManager {
|
|
|
1646
1659
|
}
|
|
1647
1660
|
|
|
1648
1661
|
if (this.persist) {
|
|
1649
|
-
|
|
1662
|
+
const file = Bun.file(newSessionFile);
|
|
1663
|
+
const writer = file.writer();
|
|
1664
|
+
writer.write(`${JSON.stringify(header)}\n`);
|
|
1650
1665
|
for (const entry of pathWithoutLabels) {
|
|
1651
|
-
|
|
1666
|
+
writer.write(`${JSON.stringify(entry)}\n`);
|
|
1652
1667
|
}
|
|
1653
1668
|
// Write fresh label entries at the end
|
|
1654
1669
|
const lastEntryId = pathWithoutLabels[pathWithoutLabels.length - 1]?.id || null;
|
|
@@ -1663,11 +1678,12 @@ export class SessionManager {
|
|
|
1663
1678
|
targetId,
|
|
1664
1679
|
label,
|
|
1665
1680
|
};
|
|
1666
|
-
|
|
1681
|
+
writer.write(`${JSON.stringify(labelEntry)}\n`);
|
|
1667
1682
|
pathEntryIds.add(labelEntry.id);
|
|
1668
1683
|
labelEntries.push(labelEntry);
|
|
1669
1684
|
parentId = labelEntry.id;
|
|
1670
1685
|
}
|
|
1686
|
+
writer.end();
|
|
1671
1687
|
this.fileEntries = [header, ...pathWithoutLabels, ...labelEntries];
|
|
1672
1688
|
this.sessionId = newSessionId;
|
|
1673
1689
|
this._buildIndex();
|
|
@@ -1714,13 +1730,13 @@ export class SessionManager {
|
|
|
1714
1730
|
*/
|
|
1715
1731
|
static async open(path: string, sessionDir?: string): Promise<SessionManager> {
|
|
1716
1732
|
// Extract cwd from session header if possible, otherwise use process.cwd()
|
|
1717
|
-
const entries = loadEntriesFromFile(path);
|
|
1733
|
+
const entries = await loadEntriesFromFile(path);
|
|
1718
1734
|
const header = entries.find((e) => e.type === "session") as SessionHeader | undefined;
|
|
1719
1735
|
const cwd = header?.cwd ?? process.cwd();
|
|
1720
1736
|
// If no sessionDir provided, derive from file's parent directory
|
|
1721
1737
|
const dir = sessionDir ?? resolve(path, "..");
|
|
1722
1738
|
const manager = new SessionManager(cwd, dir, true);
|
|
1723
|
-
|
|
1739
|
+
manager._initSessionFile(path);
|
|
1724
1740
|
return manager;
|
|
1725
1741
|
}
|
|
1726
1742
|
|
|
@@ -1729,12 +1745,12 @@ export class SessionManager {
|
|
|
1729
1745
|
* @param cwd Working directory
|
|
1730
1746
|
* @param sessionDir Optional session directory. If omitted, uses default (~/.omp/agent/sessions/<encoded-cwd>/).
|
|
1731
1747
|
*/
|
|
1732
|
-
static
|
|
1748
|
+
static continueRecent(cwd: string, sessionDir?: string): SessionManager {
|
|
1733
1749
|
const dir = sessionDir ?? getDefaultSessionDir(cwd);
|
|
1734
1750
|
const mostRecent = findMostRecentSession(dir);
|
|
1735
1751
|
const manager = new SessionManager(cwd, dir, true);
|
|
1736
1752
|
if (mostRecent) {
|
|
1737
|
-
|
|
1753
|
+
manager._initSessionFile(mostRecent);
|
|
1738
1754
|
} else {
|
|
1739
1755
|
manager._initNewSession();
|
|
1740
1756
|
}
|
|
@@ -1758,13 +1774,11 @@ export class SessionManager {
|
|
|
1758
1774
|
const sessions: SessionInfo[] = [];
|
|
1759
1775
|
|
|
1760
1776
|
try {
|
|
1761
|
-
const files =
|
|
1762
|
-
.filter((f) => f.endsWith(".jsonl"))
|
|
1763
|
-
.map((f) => join(dir, f));
|
|
1777
|
+
const files = Array.from(new Bun.Glob("*.jsonl").scanSync(dir)).map((f) => join(dir, f));
|
|
1764
1778
|
|
|
1765
1779
|
for (const file of files) {
|
|
1766
1780
|
try {
|
|
1767
|
-
const content = readFileSync(file, "
|
|
1781
|
+
const content = readFileSync(file, "utf-8");
|
|
1768
1782
|
const lines = content.trim().split("\n");
|
|
1769
1783
|
if (lines.length === 0) continue;
|
|
1770
1784
|
|
|
@@ -42,6 +42,16 @@ export interface TerminalSettings {
|
|
|
42
42
|
showImages?: boolean; // default: true (only relevant if terminal supports images)
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
export interface ImageSettings {
|
|
46
|
+
autoResize?: boolean; // default: true (resize images to 2000x2000 max for better model compatibility)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type NotificationMethod = "bell" | "osc99" | "osc9" | "auto" | "off";
|
|
50
|
+
|
|
51
|
+
export interface NotificationSettings {
|
|
52
|
+
onComplete?: NotificationMethod; // default: "auto"
|
|
53
|
+
}
|
|
54
|
+
|
|
45
55
|
export interface ExaSettings {
|
|
46
56
|
enabled?: boolean; // default: true (master toggle for all Exa tools)
|
|
47
57
|
enableSearch?: boolean; // default: true (search, deep, code, crawl)
|
|
@@ -81,6 +91,15 @@ export interface TtsrSettings {
|
|
|
81
91
|
repeatGap?: number; // default: 10
|
|
82
92
|
}
|
|
83
93
|
|
|
94
|
+
export interface VoiceSettings {
|
|
95
|
+
enabled?: boolean; // default: false
|
|
96
|
+
transcriptionModel?: string; // default: "whisper-1"
|
|
97
|
+
transcriptionLanguage?: string; // optional language hint (e.g., "en")
|
|
98
|
+
ttsModel?: string; // default: "gpt-4o-mini-tts"
|
|
99
|
+
ttsVoice?: string; // default: "alloy"
|
|
100
|
+
ttsFormat?: "wav" | "mp3" | "opus" | "aac" | "flac"; // default: "wav"
|
|
101
|
+
}
|
|
102
|
+
|
|
84
103
|
export type StatusLineSegmentId =
|
|
85
104
|
| "pi"
|
|
86
105
|
| "model"
|
|
@@ -125,7 +144,9 @@ export interface Settings {
|
|
|
125
144
|
/** Model roles map: { default: "provider/modelId", small: "provider/modelId", ... } */
|
|
126
145
|
modelRoles?: Record<string, string>;
|
|
127
146
|
defaultThinkingLevel?: "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
128
|
-
|
|
147
|
+
steeringMode?: "all" | "one-at-a-time";
|
|
148
|
+
followUpMode?: "all" | "one-at-a-time";
|
|
149
|
+
queueMode?: "all" | "one-at-a-time"; // legacy
|
|
129
150
|
interruptMode?: "immediate" | "wait";
|
|
130
151
|
theme?: string;
|
|
131
152
|
symbolPreset?: SymbolPreset; // default: uses theme's preset or "unicode"
|
|
@@ -135,11 +156,13 @@ export interface Settings {
|
|
|
135
156
|
hideThinkingBlock?: boolean;
|
|
136
157
|
shellPath?: string; // Custom shell path (e.g., for Cygwin users on Windows)
|
|
137
158
|
collapseChangelog?: boolean; // Show condensed changelog after update (use /changelog for full)
|
|
138
|
-
|
|
139
|
-
|
|
159
|
+
doubleEscapeAction?: "branch" | "tree"; // Action for double-escape with empty editor (default: "tree")
|
|
160
|
+
extensions?: string[]; // Array of extension file paths
|
|
140
161
|
skills?: SkillsSettings;
|
|
141
162
|
commands?: CommandsSettings;
|
|
142
163
|
terminal?: TerminalSettings;
|
|
164
|
+
images?: ImageSettings;
|
|
165
|
+
notifications?: NotificationSettings;
|
|
143
166
|
enabledModels?: string[]; // Model patterns for cycling (same format as --models CLI flag)
|
|
144
167
|
exa?: ExaSettings;
|
|
145
168
|
bashInterceptor?: BashInterceptorSettings;
|
|
@@ -147,6 +170,7 @@ export interface Settings {
|
|
|
147
170
|
lsp?: LspSettings;
|
|
148
171
|
edit?: EditSettings;
|
|
149
172
|
ttsr?: TtsrSettings;
|
|
173
|
+
voice?: VoiceSettings;
|
|
150
174
|
disabledProviders?: string[]; // Discovery provider IDs that are disabled
|
|
151
175
|
disabledExtensions?: string[]; // Individual extension IDs that are disabled (e.g., "skill:commit")
|
|
152
176
|
statusLine?: StatusLineSettings; // Status line configuration
|
|
@@ -232,13 +256,24 @@ export class SettingsManager {
|
|
|
232
256
|
}
|
|
233
257
|
try {
|
|
234
258
|
const content = readFileSync(path, "utf-8");
|
|
235
|
-
|
|
259
|
+
const settings = JSON.parse(content);
|
|
260
|
+
return SettingsManager.migrateSettings(settings as Record<string, unknown>);
|
|
236
261
|
} catch (error) {
|
|
237
262
|
console.error(`Warning: Could not read settings file ${path}: ${error}`);
|
|
238
263
|
return {};
|
|
239
264
|
}
|
|
240
265
|
}
|
|
241
266
|
|
|
267
|
+
/** Migrate old settings format to new format */
|
|
268
|
+
private static migrateSettings(settings: Record<string, unknown>): Settings {
|
|
269
|
+
// Migrate queueMode -> steeringMode
|
|
270
|
+
if ("queueMode" in settings && !("steeringMode" in settings)) {
|
|
271
|
+
settings.steeringMode = settings.queueMode;
|
|
272
|
+
delete settings.queueMode;
|
|
273
|
+
}
|
|
274
|
+
return settings as Settings;
|
|
275
|
+
}
|
|
276
|
+
|
|
242
277
|
private loadProjectSettings(): Settings {
|
|
243
278
|
if (!this.cwd) return {};
|
|
244
279
|
|
|
@@ -253,7 +288,7 @@ export class SettingsManager {
|
|
|
253
288
|
}
|
|
254
289
|
}
|
|
255
290
|
|
|
256
|
-
return merged;
|
|
291
|
+
return SettingsManager.migrateSettings(merged as Record<string, unknown>);
|
|
257
292
|
}
|
|
258
293
|
|
|
259
294
|
/** Apply additional overrides on top of current settings */
|
|
@@ -315,12 +350,21 @@ export class SettingsManager {
|
|
|
315
350
|
return { ...this.settings.modelRoles };
|
|
316
351
|
}
|
|
317
352
|
|
|
318
|
-
|
|
319
|
-
return this.settings.
|
|
353
|
+
getSteeringMode(): "all" | "one-at-a-time" {
|
|
354
|
+
return this.settings.steeringMode || "one-at-a-time";
|
|
320
355
|
}
|
|
321
356
|
|
|
322
|
-
|
|
323
|
-
this.globalSettings.
|
|
357
|
+
setSteeringMode(mode: "all" | "one-at-a-time"): void {
|
|
358
|
+
this.globalSettings.steeringMode = mode;
|
|
359
|
+
this.save();
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
getFollowUpMode(): "all" | "one-at-a-time" {
|
|
363
|
+
return this.settings.followUpMode || "one-at-a-time";
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
setFollowUpMode(mode: "all" | "one-at-a-time"): void {
|
|
367
|
+
this.globalSettings.followUpMode = mode;
|
|
324
368
|
this.save();
|
|
325
369
|
}
|
|
326
370
|
|
|
@@ -441,21 +485,12 @@ export class SettingsManager {
|
|
|
441
485
|
this.save();
|
|
442
486
|
}
|
|
443
487
|
|
|
444
|
-
|
|
445
|
-
return [...(this.settings.
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
setHookPaths(paths: string[]): void {
|
|
449
|
-
this.globalSettings.hooks = paths;
|
|
450
|
-
this.save();
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
getCustomToolPaths(): string[] {
|
|
454
|
-
return [...(this.settings.customTools ?? [])];
|
|
488
|
+
getExtensionPaths(): string[] {
|
|
489
|
+
return [...(this.settings.extensions ?? [])];
|
|
455
490
|
}
|
|
456
491
|
|
|
457
|
-
|
|
458
|
-
this.globalSettings.
|
|
492
|
+
setExtensionPaths(paths: string[]): void {
|
|
493
|
+
this.globalSettings.extensions = paths;
|
|
459
494
|
this.save();
|
|
460
495
|
}
|
|
461
496
|
|
|
@@ -504,6 +539,30 @@ export class SettingsManager {
|
|
|
504
539
|
this.save();
|
|
505
540
|
}
|
|
506
541
|
|
|
542
|
+
getNotificationOnComplete(): NotificationMethod {
|
|
543
|
+
return this.settings.notifications?.onComplete ?? "auto";
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
setNotificationOnComplete(method: NotificationMethod): void {
|
|
547
|
+
if (!this.globalSettings.notifications) {
|
|
548
|
+
this.globalSettings.notifications = {};
|
|
549
|
+
}
|
|
550
|
+
this.globalSettings.notifications.onComplete = method;
|
|
551
|
+
this.save();
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
getImageAutoResize(): boolean {
|
|
555
|
+
return this.settings.images?.autoResize ?? true;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
setImageAutoResize(enabled: boolean): void {
|
|
559
|
+
if (!this.globalSettings.images) {
|
|
560
|
+
this.globalSettings.images = {};
|
|
561
|
+
}
|
|
562
|
+
this.globalSettings.images.autoResize = enabled;
|
|
563
|
+
this.save();
|
|
564
|
+
}
|
|
565
|
+
|
|
507
566
|
getEnabledModels(): string[] | undefined {
|
|
508
567
|
return this.settings.enabledModels;
|
|
509
568
|
}
|
|
@@ -737,6 +796,34 @@ export class SettingsManager {
|
|
|
737
796
|
this.save();
|
|
738
797
|
}
|
|
739
798
|
|
|
799
|
+
getVoiceSettings(): Required<VoiceSettings> {
|
|
800
|
+
return {
|
|
801
|
+
enabled: this.settings.voice?.enabled ?? false,
|
|
802
|
+
transcriptionModel: this.settings.voice?.transcriptionModel ?? "whisper-1",
|
|
803
|
+
transcriptionLanguage: this.settings.voice?.transcriptionLanguage ?? "",
|
|
804
|
+
ttsModel: this.settings.voice?.ttsModel ?? "tts-1",
|
|
805
|
+
ttsVoice: this.settings.voice?.ttsVoice ?? "alloy",
|
|
806
|
+
ttsFormat: this.settings.voice?.ttsFormat ?? "wav",
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
setVoiceSettings(settings: VoiceSettings): void {
|
|
811
|
+
this.globalSettings.voice = { ...this.globalSettings.voice, ...settings };
|
|
812
|
+
this.save();
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
getVoiceEnabled(): boolean {
|
|
816
|
+
return this.settings.voice?.enabled ?? false;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
setVoiceEnabled(enabled: boolean): void {
|
|
820
|
+
if (!this.globalSettings.voice) {
|
|
821
|
+
this.globalSettings.voice = {};
|
|
822
|
+
}
|
|
823
|
+
this.globalSettings.voice.enabled = enabled;
|
|
824
|
+
this.save();
|
|
825
|
+
}
|
|
826
|
+
|
|
740
827
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
741
828
|
// Status Line Settings
|
|
742
829
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -857,4 +944,13 @@ export class SettingsManager {
|
|
|
857
944
|
this.globalSettings.statusLine.showHookStatus = show;
|
|
858
945
|
this.save();
|
|
859
946
|
}
|
|
947
|
+
|
|
948
|
+
getDoubleEscapeAction(): "branch" | "tree" {
|
|
949
|
+
return this.settings.doubleEscapeAction ?? "tree";
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
setDoubleEscapeAction(action: "branch" | "tree"): void {
|
|
953
|
+
this.globalSettings.doubleEscapeAction = action;
|
|
954
|
+
this.save();
|
|
955
|
+
}
|
|
860
956
|
}
|
|
@@ -77,9 +77,12 @@ const toolDescriptions: Record<ToolName, string> = {
|
|
|
77
77
|
ls: "List directory contents",
|
|
78
78
|
lsp: "PREFERRED for semantic code queries: go-to-definition, find-all-references, hover (type info), call hierarchy. Returns precise, deterministic results. Use BEFORE grep for symbol lookups.",
|
|
79
79
|
notebook: "Edit Jupyter notebook cells",
|
|
80
|
+
output: "Output structured data to the user (bypasses tool result formatting)",
|
|
80
81
|
task: "Spawn a sub-agent to handle complex tasks",
|
|
81
82
|
web_fetch: "Fetch and render URLs into clean text for LLM consumption",
|
|
82
83
|
web_search: "Search the web for information",
|
|
84
|
+
report_finding: "Report a finding during code review",
|
|
85
|
+
submit_review: "Submit the final code review with all findings",
|
|
83
86
|
};
|
|
84
87
|
|
|
85
88
|
/**
|
|
@@ -231,6 +234,8 @@ export interface BuildSystemPromptOptions {
|
|
|
231
234
|
customPrompt?: string;
|
|
232
235
|
/** Tools to include in prompt. Default: [read, bash, edit, write] */
|
|
233
236
|
selectedTools?: ToolName[];
|
|
237
|
+
/** Extra tool descriptions to include in prompt (non built-in tools). */
|
|
238
|
+
extraToolDescriptions?: Array<{ name: string; description: string }>;
|
|
234
239
|
/** Text to append to system prompt. */
|
|
235
240
|
appendSystemPrompt?: string;
|
|
236
241
|
/** Skills settings for discovery. */
|
|
@@ -250,6 +255,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
|
|
|
250
255
|
const {
|
|
251
256
|
customPrompt,
|
|
252
257
|
selectedTools,
|
|
258
|
+
extraToolDescriptions = [],
|
|
253
259
|
appendSystemPrompt,
|
|
254
260
|
skillsSettings,
|
|
255
261
|
cwd,
|
|
@@ -304,6 +310,12 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
|
|
|
304
310
|
}
|
|
305
311
|
}
|
|
306
312
|
|
|
313
|
+
// Append custom tool descriptions if provided
|
|
314
|
+
if (extraToolDescriptions.length > 0) {
|
|
315
|
+
prompt += "\n\n# Additional Tools\n\n";
|
|
316
|
+
prompt += extraToolDescriptions.map((tool) => `- ${tool.name}: ${tool.description}`).join("\n");
|
|
317
|
+
}
|
|
318
|
+
|
|
307
319
|
// Append git context if in a git repo
|
|
308
320
|
const gitContext = loadGitContext(resolvedCwd);
|
|
309
321
|
if (gitContext) {
|
|
@@ -335,7 +347,12 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
|
|
|
335
347
|
|
|
336
348
|
// Build tools list based on selected tools
|
|
337
349
|
const tools = selectedTools || (["read", "bash", "edit", "write"] as ToolName[]);
|
|
338
|
-
const
|
|
350
|
+
const builtInToolsList = tools.map((t) => `- ${t}: ${toolDescriptions[t]}`).join("\n");
|
|
351
|
+
const extraToolsList =
|
|
352
|
+
extraToolDescriptions.length > 0
|
|
353
|
+
? extraToolDescriptions.map((tool) => `- ${tool.name}: ${tool.description}`).join("\n")
|
|
354
|
+
: "";
|
|
355
|
+
const toolsList = [builtInToolsList, extraToolsList].filter(Boolean).join("\n");
|
|
339
356
|
|
|
340
357
|
// Generate anti-bash rules (returns null if not applicable)
|
|
341
358
|
const antiBashSection = generateAntiBashRules(tools);
|
|
@@ -413,6 +430,12 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
|
|
|
413
430
|
}
|
|
414
431
|
}
|
|
415
432
|
|
|
433
|
+
// Append custom tool descriptions if provided
|
|
434
|
+
if (extraToolDescriptions.length > 0) {
|
|
435
|
+
prompt += "\n\n# Additional Tools\n\n";
|
|
436
|
+
prompt += extraToolDescriptions.map((tool) => `- ${tool.name}: ${tool.description}`).join("\n");
|
|
437
|
+
}
|
|
438
|
+
|
|
416
439
|
// Append git context if in a git repo
|
|
417
440
|
const gitContext = loadGitContext(resolvedCwd);
|
|
418
441
|
if (gitContext) {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type NotificationProtocol = "bell" | "osc99" | "osc9";
|
|
2
|
+
|
|
3
|
+
export function detectNotificationProtocol(): NotificationProtocol {
|
|
4
|
+
const termProgram = process.env.TERM_PROGRAM?.toLowerCase() || "";
|
|
5
|
+
const term = process.env.TERM?.toLowerCase() || "";
|
|
6
|
+
|
|
7
|
+
if (process.env.KITTY_WINDOW_ID || termProgram === "kitty") {
|
|
8
|
+
return "osc99";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (process.env.GHOSTTY_RESOURCES_DIR || termProgram === "ghostty" || term.includes("ghostty")) {
|
|
12
|
+
return "osc9";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (process.env.WEZTERM_PANE || termProgram === "wezterm") {
|
|
16
|
+
return "osc9";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (process.env.ITERM_SESSION_ID || termProgram === "iterm.app") {
|
|
20
|
+
return "osc9";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return "bell";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function sendNotification(protocol: NotificationProtocol, message: string): void {
|
|
27
|
+
const payload =
|
|
28
|
+
protocol === "osc99" ? `\x1b]99;;${message}\x1b\\` : protocol === "osc9" ? `\x1b]9;${message}\x1b\\` : "\x07";
|
|
29
|
+
|
|
30
|
+
process.stdout.write(payload);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isNotificationSuppressed(): boolean {
|
|
34
|
+
const value = process.env.OMP_NOTIFICATIONS?.trim().toLowerCase();
|
|
35
|
+
if (!value) return false;
|
|
36
|
+
return value === "off" || value === "0" || value === "false";
|
|
37
|
+
}
|