tau-coding-agent 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/extensions/answer.ts +48 -42
  3. package/extensions/branch-term.ts +8 -13
  4. package/extensions/btw.ts +4 -4
  5. package/extensions/ghostty.ts +64 -11
  6. package/extensions/insights.ts +4 -3
  7. package/extensions/loop.ts +37 -9
  8. package/extensions/memory.ts +5 -10
  9. package/extensions/notify.ts +24 -5
  10. package/extensions/openai-fast.ts +6 -4
  11. package/extensions/openai-verbosity.ts +6 -4
  12. package/extensions/review/fix.ts +239 -0
  13. package/extensions/review/git.ts +517 -0
  14. package/extensions/review/index.ts +232 -0
  15. package/extensions/review/message-queue.ts +249 -0
  16. package/extensions/review/models.ts +589 -0
  17. package/extensions/review/prompts.ts +337 -0
  18. package/extensions/review/renderers/inline.ts +126 -0
  19. package/extensions/review/request.ts +504 -0
  20. package/extensions/review/review.ts +1305 -0
  21. package/extensions/review/runner.ts +522 -0
  22. package/extensions/review/runtime.ts +275 -0
  23. package/extensions/review/schema.ts +160 -0
  24. package/extensions/review/submit-review-tool.ts +66 -0
  25. package/extensions/review/submit-triage-tool.ts +59 -0
  26. package/extensions/review/triage.ts +746 -0
  27. package/extensions/sandbox/index.ts +399 -102
  28. package/extensions/{interlude.ts → stash.ts} +13 -11
  29. package/extensions/tool-display-mode.ts +2 -3
  30. package/extensions/usage/index.ts +8 -4
  31. package/extensions/websearch/README.md +1 -1
  32. package/extensions/websearch/browser/chromium.ts +42 -31
  33. package/extensions/websearch/browser/discovery.ts +11 -9
  34. package/extensions/websearch/browser/firefox.ts +10 -7
  35. package/extensions/websearch/browser/fs.ts +10 -0
  36. package/extensions/websearch/browser/sqlite.ts +22 -15
  37. package/extensions/websearch/config.ts +2 -2
  38. package/extensions/websearch/index.ts +6 -6
  39. package/extensions/worktree.ts +33 -33
  40. package/package.json +3 -3
  41. package/skills/git-clean-history/SKILL.md +1 -0
  42. package/skills/git-commit/SKILL.md +2 -2
  43. package/skills/oracle/scripts/oracle +28 -11
  44. package/themes/tau-dark.json +2 -1
  45. package/extensions/review.ts +0 -4420
@@ -1,16 +1,19 @@
1
1
  import fs from "node:fs";
2
- import os from "node:os";
3
2
  import path from "node:path";
4
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import {
4
+ getAgentDir,
5
+ type ExtensionAPI,
6
+ type ExtensionContext,
7
+ } from "@earendil-works/pi-coding-agent";
5
8
 
6
9
  const DEFAULT_SHORTCUTS = ["alt+x"] as const;
7
- const STATUS_KEY = "interlude";
8
- const KEYBINDINGS_PATH = path.join(os.homedir(), ".pi", "agent", "keybindings.json");
10
+ const STATUS_KEY = "stash";
11
+ const KEYBINDINGS_PATH = path.join(getAgentDir(), "keybindings.json");
9
12
 
10
13
  type ShortcutConfig = string | string[] | undefined;
11
14
 
12
15
  type KeybindingsConfig = {
13
- interlude?: ShortcutConfig;
16
+ stash?: ShortcutConfig;
14
17
  [key: string]: unknown;
15
18
  };
16
19
 
@@ -37,9 +40,9 @@ function normalizeShortcuts(value: ShortcutConfig): string[] {
37
40
  return normalized.length > 0 ? [...new Set(normalized)] : [...DEFAULT_SHORTCUTS];
38
41
  }
39
42
 
40
- export default function interludeExtension(pi: ExtensionAPI): void {
43
+ export default function stashExtension(pi: ExtensionAPI): void {
41
44
  const keybindings = readKeybindings(KEYBINDINGS_PATH);
42
- const shortcuts = normalizeShortcuts(keybindings.interlude);
45
+ const shortcuts = normalizeShortcuts(keybindings.stash);
43
46
 
44
47
  let stashedDraft: string | null = null;
45
48
  let armed = false;
@@ -52,7 +55,7 @@ export default function interludeExtension(pi: ExtensionAPI): void {
52
55
  if (!ctx.hasUI) return;
53
56
 
54
57
  if (armed && stashedDraft !== null) {
55
- ctx.ui.setStatus(STATUS_KEY, `interlude (${shortcutLabel()})`);
58
+ ctx.ui.setStatus(STATUS_KEY, `stash (${shortcutLabel()})`);
56
59
  return;
57
60
  }
58
61
 
@@ -80,7 +83,7 @@ export default function interludeExtension(pi: ExtensionAPI): void {
80
83
 
81
84
  if (armed && stashedDraft !== null) {
82
85
  restoreDraft(ctx);
83
- ctx.ui.notify("Interlude draft restored", "info");
86
+ ctx.ui.notify("Stashed draft restored", "info");
84
87
  return;
85
88
  }
86
89
 
@@ -101,8 +104,7 @@ export default function interludeExtension(pi: ExtensionAPI): void {
101
104
 
102
105
  for (const shortcut of shortcuts) {
103
106
  pi.registerShortcut(shortcut as never, {
104
- description:
105
- "Stash the current message draft, send one interlude message, then restore the draft",
107
+ description: "Stash the current message draft, send one message, then restore it",
106
108
  handler: async (ctx) => {
107
109
  stashOrRestore(ctx);
108
110
  },
@@ -1,7 +1,7 @@
1
1
  import { mkdir, readFile, writeFile } from "node:fs/promises";
2
- import os from "node:os";
3
2
  import path from "node:path";
4
3
  import {
4
+ getAgentDir,
5
5
  type AppKeybinding,
6
6
  CustomEditor,
7
7
  createBashToolDefinition,
@@ -77,8 +77,7 @@ function emptyConfig(): ToolDisplayModeConfig {
77
77
  }
78
78
 
79
79
  function getConfigPath(): string {
80
- const agentDir = process.env.PI_CODING_AGENT_DIR ?? path.join(os.homedir(), ".pi", "agent");
81
- return path.join(agentDir, CONFIG_FILE);
80
+ return path.join(getAgentDir(), CONFIG_FILE);
82
81
  }
83
82
 
84
83
  function isObject(value: unknown): value is JsonObject {
@@ -1,5 +1,9 @@
1
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { BorderedLoader } from "@earendil-works/pi-coding-agent";
1
+ import {
2
+ BorderedLoader,
3
+ getAgentDir,
4
+ type ExtensionAPI,
5
+ type ExtensionContext,
6
+ } from "@earendil-works/pi-coding-agent";
3
7
  import {
4
8
  Key,
5
9
  matchesKey,
@@ -31,7 +35,7 @@ import type {
31
35
  } from "./types.js";
32
36
 
33
37
  const ALL_TAB_ID = "all";
34
- const SESSION_ROOT = path.join(os.homedir(), ".pi", "agent", "sessions");
38
+ const SESSION_ROOT = path.join(getAgentDir(), "sessions");
35
39
  const RANGE_DAYS = [7, 30, 90] as const;
36
40
  const FILE_PARSE_CONCURRENCY = 16;
37
41
 
@@ -2205,7 +2209,7 @@ export default function usageBreakdownExtension(pi: ExtensionAPI) {
2205
2209
  handler: async (_args, ctx: ExtensionContext) => {
2206
2210
  const liveUsageAvailability = await getLiveUsageAvailabilityMap(ctx);
2207
2211
 
2208
- if (!ctx.hasUI) {
2212
+ if (ctx.mode !== "tui") {
2209
2213
  const [data, liveUsage] = await Promise.all([
2210
2214
  computeBreakdown(liveUsageAvailability, undefined),
2211
2215
  fetchLiveUsage(SUPPORTED_PROVIDERS, liveUsageAvailability, ctx, undefined),
@@ -16,7 +16,7 @@ Current routes:
16
16
 
17
17
  Optional config file:
18
18
 
19
- - `~/.pi/websearch.json`
19
+ - `~/.pi/agent/websearch.json`
20
20
 
21
21
  Example:
22
22
 
@@ -1,11 +1,13 @@
1
- import { execFileSync } from "node:child_process";
2
- import { createDecipheriv, createHash, pbkdf2Sync } from "node:crypto";
3
- import { existsSync, readdirSync } from "node:fs";
1
+ import { execFile } from "node:child_process";
2
+ import { createDecipheriv, createHash, pbkdf2 } from "node:crypto";
3
+ import { readdir } from "node:fs/promises";
4
4
  import { DatabaseSync } from "node:sqlite";
5
5
  import os from "node:os";
6
6
  import path from "node:path";
7
+ import { promisify } from "node:util";
7
8
 
8
9
  import type { BrowserCookie, BrowserProfile } from "../types.js";
10
+ import { pathExists } from "./fs.js";
9
11
  import { withSqliteSnapshot } from "./sqlite.js";
10
12
 
11
13
  const MACOS_BROWSER_CONFIGS: ChromiumBrowserConfig[] = [
@@ -72,6 +74,8 @@ interface ChromiumBrowserConfig {
72
74
  secretToolApp?: string;
73
75
  }
74
76
 
77
+ const execFileAsync = promisify(execFile);
78
+ const pbkdf2Async = promisify(pbkdf2);
75
79
  const passwordCache = new Map<string, string | null>();
76
80
 
77
81
  function browserConfigs(): ChromiumBrowserConfig[] {
@@ -80,13 +84,15 @@ function browserConfigs(): ChromiumBrowserConfig[] {
80
84
  return [];
81
85
  }
82
86
 
83
- export function discoverChromiumProfiles(preferredProfileName?: string): BrowserProfile[] {
87
+ export async function discoverChromiumProfiles(
88
+ preferredProfileName?: string,
89
+ ): Promise<BrowserProfile[]> {
84
90
  const profiles: BrowserProfile[] = [];
85
91
 
86
92
  for (const config of browserConfigs()) {
87
- if (!existsSync(config.baseDir)) continue;
93
+ if (!(await pathExists(config.baseDir))) continue;
88
94
 
89
- const candidates = ["Default", ...discoverProfileDirectories(config.baseDir)];
95
+ const candidates = ["Default", ...(await discoverProfileDirectories(config.baseDir))];
90
96
  const seen = new Set<string>();
91
97
  for (const directoryName of candidates) {
92
98
  if (!directoryName || seen.has(directoryName)) continue;
@@ -94,7 +100,7 @@ export function discoverChromiumProfiles(preferredProfileName?: string): Browser
94
100
 
95
101
  const profilePath = path.join(config.baseDir, directoryName);
96
102
  const cookiesPath = path.join(profilePath, "Cookies");
97
- if (!existsSync(cookiesPath)) continue;
103
+ if (!(await pathExists(cookiesPath))) continue;
98
104
 
99
105
  profiles.push({
100
106
  family: "chromium",
@@ -136,9 +142,9 @@ function isPreferredChromiumProfile(
136
142
  return profile.profileName === preferredProfileName;
137
143
  }
138
144
 
139
- function discoverProfileDirectories(baseDir: string): string[] {
145
+ async function discoverProfileDirectories(baseDir: string): Promise<string[]> {
140
146
  try {
141
- return readdirSync(baseDir, { withFileTypes: true })
147
+ return (await readdir(baseDir, { withFileTypes: true }))
142
148
  .filter((entry) => entry.isDirectory())
143
149
  .map((entry) => entry.name)
144
150
  .filter((name) => name === "Default" || /^Profile\s+\d+$/.test(name));
@@ -147,27 +153,28 @@ function discoverProfileDirectories(baseDir: string): string[] {
147
153
  }
148
154
  }
149
155
 
150
- export function loadChromiumCookies(profile: BrowserProfile): BrowserCookie[] {
156
+ export async function loadChromiumCookies(profile: BrowserProfile): Promise<BrowserCookie[]> {
151
157
  const browserConfig = browserConfigs().find((config) =>
152
158
  profile.profilePath.startsWith(config.baseDir),
153
159
  );
154
160
  if (!browserConfig) return [];
155
161
 
156
- const password = readBrowserPassword(browserConfig);
162
+ const cookiesPath = path.join(profile.profilePath, "Cookies");
163
+ if (!(await pathExists(cookiesPath))) return [];
164
+
165
+ const password = await readBrowserPassword(browserConfig);
157
166
  if (!password) return [];
158
167
 
159
- const keyMaterial = pbkdf2Sync(
168
+ const keyMaterial = await pbkdf2Async(
160
169
  password,
161
170
  "saltysalt",
162
171
  process.platform === "darwin" ? 1003 : 1,
163
172
  16,
164
173
  "sha1",
165
174
  );
166
- const cookiesPath = path.join(profile.profilePath, "Cookies");
167
- if (!existsSync(cookiesPath)) return [];
168
175
 
169
176
  try {
170
- return withSqliteSnapshot(cookiesPath, "websearch-chromium", (tempDbPath) => {
177
+ return await withSqliteSnapshot(cookiesPath, "websearch-chromium", (tempDbPath) => {
171
178
  const db = new DatabaseSync(tempDbPath, { readOnly: true });
172
179
  try {
173
180
  const rows = db
@@ -277,7 +284,7 @@ function stripHostKeyDigest(buffer: Buffer, hostKey: string | null): Buffer {
277
284
  return buffer.subarray(0, 32).equals(digest) ? buffer.subarray(32) : buffer;
278
285
  }
279
286
 
280
- function readBrowserPassword(config: ChromiumBrowserConfig): string | null {
287
+ async function readBrowserPassword(config: ChromiumBrowserConfig): Promise<string | null> {
281
288
  const cacheKey = `${config.name}:${config.baseDir}`;
282
289
  if (passwordCache.has(cacheKey)) {
283
290
  return passwordCache.get(cacheKey) ?? null;
@@ -286,9 +293,13 @@ function readBrowserPassword(config: ChromiumBrowserConfig): string | null {
286
293
  let password: string | null = null;
287
294
  if (process.platform === "darwin") {
288
295
  if (!config.keychainAccount || !config.keychainService) return null;
289
- password = readMacKeychainPassword(config.name, config.keychainAccount, config.keychainService);
296
+ password = await readMacKeychainPassword(
297
+ config.name,
298
+ config.keychainAccount,
299
+ config.keychainService,
300
+ );
290
301
  } else if (process.platform === "linux") {
291
- password = readLinuxPassword(config.secretToolApp);
302
+ password = await readLinuxPassword(config.secretToolApp);
292
303
  }
293
304
 
294
305
  if (password) {
@@ -297,33 +308,33 @@ function readBrowserPassword(config: ChromiumBrowserConfig): string | null {
297
308
  return password;
298
309
  }
299
310
 
300
- function readMacKeychainPassword(
311
+ async function readMacKeychainPassword(
301
312
  browserName: string,
302
313
  account: string,
303
314
  service: string,
304
- ): string | null {
315
+ ): Promise<string | null> {
305
316
  try {
306
- return (
307
- execFileSync("security", ["find-generic-password", "-w", "-a", account, "-s", service], {
308
- encoding: "utf8",
309
- stdio: ["ignore", "pipe", "ignore"],
310
- }).trim() || null
317
+ const { stdout } = await execFileAsync(
318
+ "security",
319
+ ["find-generic-password", "-w", "-a", account, "-s", service],
320
+ { encoding: "utf8" },
311
321
  );
322
+ return stdout.trim() || null;
312
323
  } catch {
313
324
  throw new Error(`Could not read ${browserName} cookie decryption password from Keychain.`);
314
325
  }
315
326
  }
316
327
 
317
- function readLinuxPassword(secretToolApp?: string): string | null {
328
+ async function readLinuxPassword(secretToolApp?: string): Promise<string | null> {
318
329
  if (!secretToolApp) return "peanuts";
319
330
 
320
331
  try {
321
- return (
322
- execFileSync("secret-tool", ["lookup", "application", secretToolApp], {
323
- encoding: "utf8",
324
- stdio: ["ignore", "pipe", "ignore"],
325
- }).trim() || "peanuts"
332
+ const { stdout } = await execFileAsync(
333
+ "secret-tool",
334
+ ["lookup", "application", secretToolApp],
335
+ { encoding: "utf8" },
326
336
  );
337
+ return stdout.trim() || "peanuts";
327
338
  } catch {
328
339
  return "peanuts";
329
340
  }
@@ -11,32 +11,34 @@ import type {
11
11
  const COOKIE_CACHE_TTL_MS = 30_000;
12
12
  const cookieCache = new Map<string, { cookies: BrowserCookie[]; expiresAt: number }>();
13
13
 
14
- export function discoverProfiles(
14
+ export async function discoverProfiles(
15
15
  browserFamilies: WebsearchBrowserFamily[],
16
16
  preferredProfiles: WebsearchConfig["profiles"],
17
- ): BrowserProfile[] {
17
+ ): Promise<BrowserProfile[]> {
18
18
  const profiles: BrowserProfile[] = [];
19
19
 
20
20
  for (const family of browserFamilies) {
21
21
  if (family === "firefox") {
22
- profiles.push(...discoverFirefoxProfiles(preferredProfiles.firefox));
22
+ profiles.push(...(await discoverFirefoxProfiles(preferredProfiles.firefox)));
23
23
  continue;
24
24
  }
25
25
 
26
- profiles.push(...discoverChromiumProfiles(preferredProfiles.chromium));
26
+ profiles.push(...(await discoverChromiumProfiles(preferredProfiles.chromium)));
27
27
  }
28
28
 
29
29
  return profiles;
30
30
  }
31
31
 
32
- export function loadCookies(profile: BrowserProfile): BrowserCookie[] {
32
+ export async function loadCookies(profile: BrowserProfile): Promise<BrowserCookie[]> {
33
33
  const cacheKey = `${profile.family}:${profile.profilePath}`;
34
34
  const cached = cookieCache.get(cacheKey);
35
35
  if (cached && cached.expiresAt > Date.now()) return cached.cookies;
36
36
  if (cached) cookieCache.delete(cacheKey);
37
37
 
38
38
  const cookies =
39
- profile.family === "firefox" ? loadFirefoxCookies(profile) : loadChromiumCookies(profile);
39
+ profile.family === "firefox"
40
+ ? await loadFirefoxCookies(profile)
41
+ : await loadChromiumCookies(profile);
40
42
 
41
43
  if (cookies.length > 0) {
42
44
  cookieCache.set(cacheKey, {
@@ -48,11 +50,11 @@ export function loadCookies(profile: BrowserProfile): BrowserCookie[] {
48
50
  return cookies;
49
51
  }
50
52
 
51
- export function createBrowserSession(
53
+ export async function createBrowserSession(
52
54
  profile: BrowserProfile,
53
55
  domains: string[],
54
- ): BrowserSession | null {
55
- const cookies = loadCookies(profile).filter((cookie) => {
56
+ ): Promise<BrowserSession | null> {
57
+ const cookies = (await loadCookies(profile)).filter((cookie) => {
56
58
  return domains.some((domain) => matchesBrowserDomain(domain, cookie.domain));
57
59
  });
58
60
  if (cookies.length === 0) return null;
@@ -1,9 +1,10 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { readFile } from "node:fs/promises";
2
2
  import { DatabaseSync } from "node:sqlite";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
 
6
6
  import type { BrowserCookie, BrowserProfile } from "../types.js";
7
+ import { pathExists } from "./fs.js";
7
8
  import { withSqliteSnapshot } from "./sqlite.js";
8
9
 
9
10
  function firefoxBaseDir(): string | null {
@@ -45,7 +46,9 @@ function parseIni(text: string): Array<Record<string, string>> {
45
46
  return sections;
46
47
  }
47
48
 
48
- export function discoverFirefoxProfiles(preferredProfileName?: string): BrowserProfile[] {
49
+ export async function discoverFirefoxProfiles(
50
+ preferredProfileName?: string,
51
+ ): Promise<BrowserProfile[]> {
49
52
  const baseDir = firefoxBaseDir();
50
53
  if (!baseDir) return [];
51
54
 
@@ -53,7 +56,7 @@ export function discoverFirefoxProfiles(preferredProfileName?: string): BrowserP
53
56
 
54
57
  let sections: Array<Record<string, string>> = [];
55
58
  try {
56
- sections = parseIni(readFileSync(profilesIniPath, "utf8"));
59
+ sections = parseIni(await readFile(profilesIniPath, "utf8"));
57
60
  } catch {
58
61
  return [];
59
62
  }
@@ -67,7 +70,7 @@ export function discoverFirefoxProfiles(preferredProfileName?: string): BrowserP
67
70
  const profilePath =
68
71
  section.IsRelative === "1" ? path.join(baseDir, profilePathValue) : profilePathValue;
69
72
  const cookiesPath = path.join(profilePath, "cookies.sqlite");
70
- if (!existsSync(cookiesPath)) continue;
73
+ if (!(await pathExists(cookiesPath))) continue;
71
74
 
72
75
  profiles.push({
73
76
  family: "firefox",
@@ -100,12 +103,12 @@ function isPreferredFirefoxProfile(
100
103
  );
101
104
  }
102
105
 
103
- export function loadFirefoxCookies(profile: BrowserProfile): BrowserCookie[] {
106
+ export async function loadFirefoxCookies(profile: BrowserProfile): Promise<BrowserCookie[]> {
104
107
  const cookiesPath = path.join(profile.profilePath, "cookies.sqlite");
105
- if (!existsSync(cookiesPath)) return [];
108
+ if (!(await pathExists(cookiesPath))) return [];
106
109
 
107
110
  try {
108
- return withSqliteSnapshot(cookiesPath, "websearch-firefox", (tempDbPath) => {
111
+ return await withSqliteSnapshot(cookiesPath, "websearch-firefox", (tempDbPath) => {
109
112
  const db = new DatabaseSync(tempDbPath, { readOnly: true });
110
113
  try {
111
114
  const rows = db
@@ -0,0 +1,10 @@
1
+ import { access } from "node:fs/promises";
2
+
3
+ export async function pathExists(filePath: string): Promise<boolean> {
4
+ try {
5
+ await access(filePath);
6
+ return true;
7
+ } catch {
8
+ return false;
9
+ }
10
+ }
@@ -1,33 +1,40 @@
1
- import { copyFileSync, existsSync, mkdirSync, rmSync } from "node:fs";
1
+ import { copyFile, mkdtemp, rm } from "node:fs/promises";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
 
5
- export function withSqliteSnapshot<T>(
5
+ import { pathExists } from "./fs.js";
6
+
7
+ export async function withSqliteSnapshot<T>(
6
8
  sourceDbPath: string,
7
9
  tempPrefix: string,
8
- operation: (snapshotPath: string) => T,
9
- ): T {
10
- const tempDir = path.join(os.tmpdir(), `${tempPrefix}-${process.pid}-${Date.now()}`);
10
+ operation: (snapshotPath: string) => T | Promise<T>,
11
+ ): Promise<T> {
12
+ const tempDir = await mkdtemp(path.join(os.tmpdir(), `${tempPrefix}-${process.pid}-`));
11
13
  const snapshotPath = path.join(tempDir, path.basename(sourceDbPath));
12
14
 
13
15
  try {
14
- mkdirSync(tempDir, { recursive: true });
15
- copyFileSync(sourceDbPath, snapshotPath);
16
- copySidecar(sourceDbPath, snapshotPath, "-wal");
17
- copySidecar(sourceDbPath, snapshotPath, "-shm");
18
- copySidecar(sourceDbPath, snapshotPath, "-journal");
19
- return operation(snapshotPath);
16
+ await copyFile(sourceDbPath, snapshotPath);
17
+ await Promise.all([
18
+ copySidecar(sourceDbPath, snapshotPath, "-wal"),
19
+ copySidecar(sourceDbPath, snapshotPath, "-shm"),
20
+ copySidecar(sourceDbPath, snapshotPath, "-journal"),
21
+ ]);
22
+ return await operation(snapshotPath);
20
23
  } finally {
21
- rmSync(tempDir, { recursive: true, force: true });
24
+ await rm(tempDir, { recursive: true, force: true });
22
25
  }
23
26
  }
24
27
 
25
- export function copySidecar(sourceDbPath: string, targetDbPath: string, suffix: string): void {
28
+ export async function copySidecar(
29
+ sourceDbPath: string,
30
+ targetDbPath: string,
31
+ suffix: string,
32
+ ): Promise<void> {
26
33
  const source = `${sourceDbPath}${suffix}`;
27
- if (!existsSync(source)) return;
34
+ if (!(await pathExists(source))) return;
28
35
 
29
36
  try {
30
- copyFileSync(source, `${targetDbPath}${suffix}`);
37
+ await copyFile(source, `${targetDbPath}${suffix}`);
31
38
  } catch (error) {
32
39
  const code =
33
40
  error && typeof error === "object" && "code" in error
@@ -1,10 +1,10 @@
1
1
  import { readFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
2
  import path from "node:path";
3
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
4
4
 
5
5
  import type { WebsearchConfig, WebsearchRouteId } from "./types.js";
6
6
 
7
- const WEBSEARCH_CONFIG_PATH = path.join(homedir(), ".pi", "websearch.json");
7
+ const WEBSEARCH_CONFIG_PATH = path.join(getAgentDir(), "websearch.json");
8
8
 
9
9
  const DEFAULT_ROUTES: WebsearchRouteId[] = [
10
10
  "pi:openai-codex",
@@ -128,15 +128,15 @@ function throwIfAborted(signal?: AbortSignal): void {
128
128
  throw signal.reason ?? new Error("The operation was aborted.");
129
129
  }
130
130
 
131
- function getBrowserProfiles(
131
+ async function getBrowserProfiles(
132
132
  cache: Partial<Record<WebsearchBrowserFamily, BrowserProfile[]>>,
133
133
  family: WebsearchBrowserFamily,
134
134
  config: WebsearchConfig,
135
- ): BrowserProfile[] {
135
+ ): Promise<BrowserProfile[]> {
136
136
  const cached = cache[family];
137
137
  if (cached) return cached;
138
138
 
139
- const profiles = discoverProfiles([family], config.profiles);
139
+ const profiles = await discoverProfiles([family], config.profiles);
140
140
  cache[family] = profiles;
141
141
  return profiles;
142
142
  }
@@ -153,7 +153,7 @@ async function searchBrowserRoute(
153
153
  : "chromium";
154
154
  const browserProvider = route.endsWith(":gemini") ? browserGemini : browserOpenAICodex;
155
155
  const configuredProfileName = config.profiles[profileFamily];
156
- const profiles = getBrowserProfiles(profileCache, profileFamily, config);
156
+ const profiles = await getBrowserProfiles(profileCache, profileFamily, config);
157
157
  let lastError: string | null = null;
158
158
 
159
159
  if (configuredProfileName && profiles.length === 0) {
@@ -164,7 +164,7 @@ async function searchBrowserRoute(
164
164
  throwIfAborted(signal);
165
165
 
166
166
  try {
167
- const session = createBrowserSession(profile, browserProvider.domains);
167
+ const session = await createBrowserSession(profile, browserProvider.domains);
168
168
  if (!session) {
169
169
  if (configuredProfileName) {
170
170
  throw new Error(
@@ -265,7 +265,7 @@ export default function (pi: ExtensionAPI) {
265
265
  promptSnippet:
266
266
  "Search the web for current or external information unavailable in local files",
267
267
  promptGuidelines: [
268
- "Use this for recent facts, live service behavior, or external documentation that is not already present in the repo.",
268
+ "Use websearch for recent facts, live service behavior, or external documentation that is not already present in the repo.",
269
269
  "Do not use websearch when repository files or supplied context already answer the question.",
270
270
  ],
271
271
  parameters: Type.Object({
@@ -40,9 +40,9 @@ import * as fs from "node:fs";
40
40
  import * as path from "node:path";
41
41
 
42
42
  const STATUS_KEY = "0-worktree";
43
- const MANUAL_OPEN_MESSAGE_TYPE = "worktree-open-command";
44
- const RESTORE_STASH_MESSAGE_TYPE = "worktree-restore-command";
45
- const SCRIPT_RERUN_MESSAGE_TYPE = "worktree-script-rerun-command";
43
+ const MANUAL_OPEN_ENTRY_TYPE = "worktree-open-command";
44
+ const RESTORE_STASH_ENTRY_TYPE = "worktree-restore-command";
45
+ const SCRIPT_RERUN_ENTRY_TYPE = "worktree-script-rerun-command";
46
46
  const MANUAL_OPEN_INTRO =
47
47
  "Worktree ready. Open it in a separate terminal or tmux pane with this command";
48
48
 
@@ -224,15 +224,10 @@ function showCommandMessage(
224
224
  return;
225
225
  }
226
226
 
227
- pi.sendMessage({
228
- customType,
229
- content: intro,
230
- display: true,
231
- details: {
232
- intro,
233
- command,
234
- copiedToClipboard,
235
- } satisfies CommandMessageDetails,
227
+ pi.appendEntry<CommandMessageDetails>(customType, {
228
+ intro,
229
+ command,
230
+ copiedToClipboard,
236
231
  });
237
232
  }
238
233
 
@@ -241,7 +236,7 @@ function showManualOpenCommand(
241
236
  ctx: ExtensionCommandContext,
242
237
  command: string,
243
238
  ): void {
244
- showCommandMessage(pi, ctx, MANUAL_OPEN_MESSAGE_TYPE, MANUAL_OPEN_INTRO, command);
239
+ showCommandMessage(pi, ctx, MANUAL_OPEN_ENTRY_TYPE, MANUAL_OPEN_INTRO, command);
245
240
  }
246
241
 
247
242
  function showRestoreStashCommand(
@@ -253,7 +248,7 @@ function showRestoreStashCommand(
253
248
  showCommandMessage(
254
249
  pi,
255
250
  ctx,
256
- RESTORE_STASH_MESSAGE_TYPE,
251
+ RESTORE_STASH_ENTRY_TYPE,
257
252
  `Main-worktree changes were stashed as ${stashSpec}. Restore them in the new worktree with this command`,
258
253
  `cd ${shellQuoteCompact(targetPath)} && git stash apply ${shellQuote(stashSpec)}`,
259
254
  );
@@ -270,7 +265,7 @@ function showScriptRerunCommand(
270
265
  showCommandMessage(
271
266
  pi,
272
267
  ctx,
273
- SCRIPT_RERUN_MESSAGE_TYPE,
268
+ SCRIPT_RERUN_ENTRY_TYPE,
274
269
  `${label} failed (exit ${exitCode}). Re-run it manually with this command`,
275
270
  `cd ${shellQuoteCompact(worktreeRoot)} && ${command}`,
276
271
  );
@@ -2008,18 +2003,23 @@ async function handleList(pi: ExtensionAPI, ctx: ExtensionCommandContext): Promi
2008
2003
  return { repo, items };
2009
2004
  });
2010
2005
 
2011
- if (!ctx.hasUI) {
2012
- for (const item of items) {
2013
- const dirty = item.status === "dirty" ? " *" : "";
2014
- const meta: string[] = [];
2015
- if (item.tracked) meta.push("↑");
2016
- if (item.isMain) meta.push("[primary]");
2017
- if (item.pathState !== "ok") meta.push(item.pathState);
2018
- if (item.wt.locked)
2019
- meta.push(item.wt.lockedReason ? `locked:${item.wt.lockedReason}` : "locked");
2020
- const suffix = meta.length > 0 ? ` ${meta.join(" ")}` : "";
2021
- console.log(` ${item.branch}${dirty}${suffix} ${collapsePath(item.wt.path)}`);
2022
- }
2006
+ if (ctx.mode !== "tui") {
2007
+ const output = items
2008
+ .map((item) => {
2009
+ const dirty = item.status === "dirty" ? " *" : "";
2010
+ const meta: string[] = [];
2011
+ if (item.tracked) meta.push("↑");
2012
+ if (item.isMain) meta.push("[primary]");
2013
+ if (item.pathState !== "ok") meta.push(item.pathState);
2014
+ if (item.wt.locked)
2015
+ meta.push(item.wt.lockedReason ? `locked:${item.wt.lockedReason}` : "locked");
2016
+ const suffix = meta.length > 0 ? ` ${meta.join(" ")}` : "";
2017
+ return ` ${item.branch}${dirty}${suffix} ${collapsePath(item.wt.path)}`;
2018
+ })
2019
+ .join("\n");
2020
+
2021
+ if (ctx.mode === "print") console.log(output);
2022
+ else ctx.ui.notify(output, "info");
2023
2023
  return;
2024
2024
  }
2025
2025
 
@@ -2114,14 +2114,14 @@ function parseSubcommand(args: string): { subcommand: Subcommand | null; rest: s
2114
2114
 
2115
2115
  export default function worktreeExtension(pi: ExtensionAPI) {
2116
2116
  for (const customType of [
2117
- MANUAL_OPEN_MESSAGE_TYPE,
2118
- RESTORE_STASH_MESSAGE_TYPE,
2119
- SCRIPT_RERUN_MESSAGE_TYPE,
2117
+ MANUAL_OPEN_ENTRY_TYPE,
2118
+ RESTORE_STASH_ENTRY_TYPE,
2119
+ SCRIPT_RERUN_ENTRY_TYPE,
2120
2120
  ]) {
2121
- pi.registerMessageRenderer(customType, (message, _options, theme) => {
2122
- const details = message.details as CommandMessageDetails | undefined;
2121
+ pi.registerEntryRenderer<CommandMessageDetails>(customType, (entry, _options, theme) => {
2122
+ const details = entry.data;
2123
2123
  if (!details || typeof details.intro !== "string" || typeof details.command !== "string") {
2124
- return new Text(typeof message.content === "string" ? message.content : "", 0, 0);
2124
+ return undefined;
2125
2125
  }
2126
2126
 
2127
2127
  const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));