@oh-my-pi/pi-coding-agent 6.8.1 → 6.8.3

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.
@@ -799,10 +799,11 @@ const reportFatal = async (message: string): Promise<void> => {
799
799
  } catch {
800
800
  // Ignore cleanup errors
801
801
  }
802
+ const error = new Error(message);
802
803
 
803
804
  const runState = activeRun;
804
805
  if (runState) {
805
- runState.abortController.abort();
806
+ runState.abortController.abort(error);
806
807
  if (runState.session) {
807
808
  void runState.session.abort();
808
809
  }
@@ -1,5 +1,4 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { mkdirSync } from "node:fs";
3
2
  import path from "node:path";
4
3
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
5
4
  import { StringEnum } from "@oh-my-pi/pi-ai";
@@ -13,7 +12,6 @@ import todoWriteDescription from "../../prompts/tools/todo-write.md" with { type
13
12
  import type { RenderResultOptions } from "../custom-tools/types";
14
13
  import { renderPromptTemplate } from "../prompt-templates";
15
14
  import type { ToolSession } from "../sdk";
16
- import { ensureArtifactsDir, getArtifactsDir } from "./task/artifacts";
17
15
 
18
16
  const todoWriteSchema = Type.Object({
19
17
  todos: Type.Array(
@@ -135,10 +133,6 @@ async function loadTodoFile(filePath: string): Promise<TodoFile | null> {
135
133
  }
136
134
  }
137
135
 
138
- async function saveTodoFile(filePath: string, data: TodoFile): Promise<void> {
139
- await Bun.write(filePath, JSON.stringify(data, null, 2));
140
- }
141
-
142
136
  function formatTodoSummary(todos: TodoItem[]): string {
143
137
  if (todos.length === 0) return "Todo list cleared.";
144
138
  const completed = todos.filter((t) => t.status === "completed").length;
@@ -200,24 +194,14 @@ export class TodoWriteTool implements AgentTool<typeof todoWriteSchema, TodoWrit
200
194
  };
201
195
  }
202
196
 
203
- const artifactsDir = getArtifactsDir(sessionFile);
204
- if (!artifactsDir) {
205
- return {
206
- content: [{ type: "text", text: formatTodoSummary(todos) }],
207
- details: { todos, updatedAt, storage: "memory" },
208
- };
209
- }
210
-
211
- ensureArtifactsDir(artifactsDir);
212
- const todoPath = path.join(artifactsDir, TODO_FILE_NAME);
197
+ const todoPath = path.join(sessionFile.slice(0, -6), TODO_FILE_NAME);
213
198
  const existing = await loadTodoFile(todoPath);
214
199
  const storedTodos = existing?.todos ?? [];
215
200
  const merged = todos.length > 0 ? todos : [];
216
201
  const fileData: TodoFile = { updatedAt, todos: merged };
217
202
 
218
203
  try {
219
- mkdirSync(artifactsDir, { recursive: true });
220
- await saveTodoFile(todoPath, fileData);
204
+ await Bun.write(todoPath, JSON.stringify(fileData, null, 2));
221
205
  } catch (error) {
222
206
  logger.error("Failed to write todo file", { path: todoPath, error: String(error) });
223
207
  return {
@@ -2,7 +2,6 @@ import { unlinkSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
3
  import path from "node:path";
4
4
  import { cspawn } from "@oh-my-pi/pi-utils";
5
- import type { FileSink } from "bun";
6
5
  import { nanoid } from "nanoid";
7
6
  import { ensureTool } from "../../../utils/tools-manager";
8
7
  import type { RenderResult, SpecialHandler } from "./types";
@@ -16,59 +15,17 @@ async function exec(
16
15
  args: string[],
17
16
  options?: { timeout?: number; input?: string | Buffer; signal?: AbortSignal },
18
17
  ): Promise<{ stdout: string; stderr: string; ok: boolean; exitCode: number | null }> {
19
- const controller = new AbortController();
20
- const onAbort = () => controller.abort(options?.signal?.reason ?? new Error("Aborted"));
21
- if (options?.signal) {
22
- if (options.signal.aborted) {
23
- onAbort();
24
- } else {
25
- options.signal.addEventListener("abort", onAbort, { once: true });
26
- }
27
- }
28
- const timeoutId =
29
- options?.timeout && options.timeout > 0
30
- ? setTimeout(() => controller.abort(new Error("Timeout")), options.timeout)
31
- : undefined;
32
18
  const proc = cspawn([cmd, ...args], {
33
- signal: controller.signal,
19
+ signal: options?.signal,
20
+ timeout: options?.timeout,
21
+ stdin: options?.input ? Buffer.from(options.input) : undefined,
34
22
  });
35
23
 
36
- if (options?.input && proc.stdin) {
37
- const stdin = proc.stdin as FileSink;
38
- const payload = typeof options.input === "string" ? new TextEncoder().encode(options.input) : options.input;
39
- stdin.write(payload);
40
- const flushed = stdin.flush();
41
- if (flushed instanceof Promise) {
42
- await flushed;
43
- }
44
- const ended = stdin.end();
45
- if (ended instanceof Promise) {
46
- await ended;
47
- }
48
- }
49
-
50
24
  const [stdout, stderr, exitResult] = await Promise.all([
51
- new Response(proc.stdout).text(),
52
- new Response(proc.stderr).text(),
53
- (async () => {
54
- try {
55
- await proc.exited;
56
- return proc.exitCode ?? 0;
57
- } catch (err) {
58
- if (err && typeof err === "object" && "exitCode" in err) {
59
- const exitValue = (err as { exitCode?: number }).exitCode;
60
- if (typeof exitValue === "number") {
61
- return exitValue;
62
- }
63
- }
64
- throw err instanceof Error ? err : new Error(String(err));
65
- }
66
- })(),
25
+ proc.stdout.text(),
26
+ proc.stderr.text(),
27
+ proc.exited.then(() => proc.exitCode ?? 0),
67
28
  ]);
68
- if (timeoutId) clearTimeout(timeoutId);
69
- if (options?.signal) {
70
- options.signal.removeEventListener("abort", onAbort);
71
- }
72
29
 
73
30
  return {
74
31
  stdout,
@@ -1,6 +1,6 @@
1
1
  import { nanoid } from "nanoid";
2
2
  import { WorktreeError, WorktreeErrorCode } from "./errors";
3
- import { git, gitWithStdin } from "./git";
3
+ import { git, gitWithInput } from "./git";
4
4
  import { find, remove, type Worktree } from "./operations";
5
5
 
6
6
  export type CollapseStrategy = "simple" | "merge-base" | "rebase";
@@ -121,10 +121,10 @@ async function collapseRebase(src: Worktree, dst: Worktree): Promise<string> {
121
121
  }
122
122
 
123
123
  async function applyDiff(diff: string, targetPath: string): Promise<void> {
124
- let result = await gitWithStdin(["apply"], diff, targetPath);
124
+ let result = await gitWithInput(["apply"], diff, targetPath);
125
125
  if (result.code === 0) return;
126
126
 
127
- result = await gitWithStdin(["apply", "--3way"], diff, targetPath);
127
+ result = await gitWithInput(["apply", "--3way"], diff, targetPath);
128
128
  if (result.code === 0) return;
129
129
 
130
130
  throw new WorktreeError(
@@ -1,5 +1,5 @@
1
1
  import * as path from "node:path";
2
- import type { Subprocess } from "bun";
2
+ import { ptree } from "@oh-my-pi/pi-utils";
3
3
  import { execCommand } from "../../core/exec";
4
4
  import { WorktreeError, WorktreeErrorCode } from "./errors";
5
5
 
@@ -9,32 +9,6 @@ export interface GitResult {
9
9
  stderr: string;
10
10
  }
11
11
 
12
- type WritableLike = {
13
- write: (chunk: string | Uint8Array) => unknown;
14
- flush?: () => unknown;
15
- end?: () => unknown;
16
- };
17
-
18
- const textEncoder = new TextEncoder();
19
-
20
- async function writeStdin(handle: unknown, stdin: string): Promise<void> {
21
- if (!handle || typeof handle === "number") return;
22
- if (typeof (handle as WritableStream<Uint8Array>).getWriter === "function") {
23
- const writer = (handle as WritableStream<Uint8Array>).getWriter();
24
- try {
25
- await writer.write(textEncoder.encode(stdin));
26
- } finally {
27
- await writer.close();
28
- }
29
- return;
30
- }
31
-
32
- const sink = handle as WritableLike;
33
- sink.write(stdin);
34
- if (sink.flush) sink.flush();
35
- if (sink.end) sink.end();
36
- }
37
-
38
12
  /**
39
13
  * Execute a git command.
40
14
  * @param args - Command arguments (excluding 'git')
@@ -50,23 +24,15 @@ export async function git(args: string[], cwd?: string): Promise<GitResult> {
50
24
  * Execute git command with stdin input.
51
25
  * Used for piping diffs to `git apply`.
52
26
  */
53
- export async function gitWithStdin(args: string[], stdin: string, cwd?: string): Promise<GitResult> {
54
- const proc: Subprocess = Bun.spawn(["git", ...args], {
27
+ export async function gitWithInput(args: string[], stdin: string, cwd?: string): Promise<GitResult> {
28
+ const proc = ptree.cspawn(["git", ...args], {
55
29
  cwd: cwd ?? process.cwd(),
56
- stdin: "pipe",
57
- stdout: "pipe",
58
- stderr: "pipe",
30
+ stdin: Buffer.from(stdin),
59
31
  });
60
32
 
61
- await writeStdin(proc.stdin, stdin);
62
-
63
- const [stdout, stderr, exitCode] = await Promise.all([
64
- (proc.stdout as ReadableStream<Uint8Array>).text(),
65
- (proc.stderr as ReadableStream<Uint8Array>).text(),
66
- proc.exited,
67
- ]);
33
+ const [stdout, stderr] = await Promise.all([proc.stdout.text(), proc.stderr.text()]);
68
34
 
69
- return { code: exitCode ?? 0, stdout, stderr };
35
+ return { code: proc.exitCode ?? 0, stdout, stderr };
70
36
  }
71
37
 
72
38
  /**
@@ -1,7 +1,7 @@
1
1
  export { type CollapseOptions, type CollapseResult, type CollapseStrategy, collapse } from "./collapse";
2
2
  export { WORKTREE_BASE } from "./constants";
3
3
  export { WorktreeError, WorktreeErrorCode } from "./errors";
4
- export { getRepoName, getRepoRoot, git, gitWithStdin } from "./git";
4
+ export { getRepoName, getRepoRoot, git, gitWithInput as gitWithStdin } from "./git";
5
5
  export { create, find, list, prune, remove, type Worktree, which } from "./operations";
6
6
  export {
7
7
  cleanupSessions,
@@ -1,7 +1,6 @@
1
1
  import * as path from "node:path";
2
2
  import { Text } from "@oh-my-pi/pi-tui";
3
3
  import { logger } from "@oh-my-pi/pi-utils";
4
- import { getArtifactsDir } from "../../../core/tools/task/artifacts";
5
4
  import { theme } from "../theme/theme";
6
5
  import type { TodoItem } from "../types";
7
6
 
@@ -40,13 +39,7 @@ export class TodoDisplayComponent {
40
39
  return;
41
40
  }
42
41
 
43
- const artifactsDir = getArtifactsDir(this.sessionFile);
44
- if (!artifactsDir) {
45
- this.todos = [];
46
- this.visible = false;
47
- return;
48
- }
49
-
42
+ const artifactsDir = this.sessionFile.slice(0, -6); // strip .jsonl extension
50
43
  const todoPath = path.join(artifactsDir, TODO_FILE_NAME);
51
44
  const data = await loadTodoFile(todoPath);
52
45
  this.todos = data?.todos ?? [];
@@ -707,7 +707,7 @@ class TreeList implements Component {
707
707
  this.searchQuery = this.searchQuery.slice(0, -1);
708
708
  this.applyFilter();
709
709
  }
710
- } else if (keyData === "l" && !this.searchQuery) {
710
+ } else if (matchesKey(keyData, "shift+l") && !this.searchQuery) {
711
711
  const selected = this.filteredNodes[this.selectedIndex];
712
712
  if (selected && this.onLabelEdit) {
713
713
  this.onLabelEdit(selected.node.entry.id, selected.node.label);
@@ -821,7 +821,7 @@ export class TreeSelectorComponent extends Container {
821
821
  new TruncatedText(
822
822
  theme.fg(
823
823
  "muted",
824
- " Up/Down: move. Left/Right: page. l: label. Ctrl+O/Shift+Ctrl+O: filter. Alt+D/T/U/L/A: filter. Type to search",
824
+ " Up/Down: move. Left/Right: page. Shift+L: label. Ctrl+O/Shift+Ctrl+O: filter. Alt+D/T/U/L/A: filter. Type to search",
825
825
  ),
826
826
  0,
827
827
  0,
@@ -28,7 +28,6 @@ import { getRecentSessions } from "../../core/session-manager";
28
28
  import type { SettingsManager } from "../../core/settings-manager";
29
29
  import { loadSlashCommands } from "../../core/slash-commands";
30
30
  import { setTerminalTitle } from "../../core/title-generator";
31
- import { getArtifactsDir } from "../../core/tools/task/artifacts";
32
31
  import { VoiceSupervisor } from "../../core/voice-supervisor";
33
32
  import type { AssistantMessageComponent } from "./components/assistant-message";
34
33
  import type { BashExecutionComponent } from "./components/bash-execution";
@@ -384,15 +383,6 @@ export class InteractiveMode implements InteractiveModeContext {
384
383
 
385
384
  // Initial top border update
386
385
  this.updateEditorTopBorder();
387
-
388
- if (!startupQuiet) {
389
- const templateNames = this.session.promptTemplates.map((template) => template.name).sort();
390
- if (templateNames.length > 0) {
391
- const preview = templateNames.slice(0, 3).join(", ");
392
- const suffix = templateNames.length > 3 ? ` +${templateNames.length - 3} more` : "";
393
- this.showStatus(`Loaded prompt templates: ${preview}${suffix}`);
394
- }
395
- }
396
386
  }
397
387
 
398
388
  async getUserInput(): Promise<{ text: string; images?: ImageContent[] }> {
@@ -481,11 +471,7 @@ export class InteractiveMode implements InteractiveModeContext {
481
471
  this.renderTodoList();
482
472
  return;
483
473
  }
484
- const artifactsDir = getArtifactsDir(sessionFile);
485
- if (!artifactsDir) {
486
- this.renderTodoList();
487
- return;
488
- }
474
+ const artifactsDir = sessionFile.slice(0, -6);
489
475
  const todoPath = path.join(artifactsDir, TODO_FILE_NAME);
490
476
  const file = Bun.file(todoPath);
491
477
  if (!(await file.exists())) {
@@ -32,35 +32,40 @@ function selectPreferredImageMimeType(mimeTypes: string[]): string | null {
32
32
  }
33
33
 
34
34
  export async function copyToClipboard(text: string): Promise<void> {
35
- const timeout = Bun.sleep(3000).then(() => Promise.reject(new Error("Clipboard operation timed out")));
35
+ const p = platform();
36
+ const timeout = 5000;
36
37
 
37
- let promise: Promise<void>;
38
38
  try {
39
- switch (platform()) {
40
- case "darwin":
41
- promise = $`pbcopy ${text}`.quiet().then(() => void 0);
42
- break;
43
- case "win32":
44
- promise = $`clip ${text}`.quiet().then(() => void 0);
45
- break;
46
- case "linux":
47
- if (isWaylandSession()) {
48
- $`wl-copy ${text}`.quiet(); // fire and forget
39
+ if (p === "darwin") {
40
+ await Bun.spawn(["pbcopy"], { stdin: Buffer.from(text), timeout }).exited;
41
+ } else if (p === "win32") {
42
+ await Bun.spawn(["clip"], { stdin: Buffer.from(text), timeout }).exited;
43
+ } else {
44
+ const wayland = isWaylandSession();
45
+ if (wayland) {
46
+ const wlCopyPath = Bun.which("wl-copy");
47
+ if (wlCopyPath) {
48
+ // Fire-and-forget: wl-copy may not exit promptly, so we unref to avoid blocking
49
+ void Bun.spawn([wlCopyPath], { stdin: Buffer.from(text), timeout }).unref();
49
50
  return;
50
- } else {
51
- promise = $`xclip -selection clipboard -t text/plain -i ${text}`.quiet().then(() => void 0);
52
51
  }
53
- break;
54
- default:
55
- throw new Error(`Unsupported platform: ${platform()}`);
52
+ }
53
+
54
+ // Linux - try xclip first, fall back to xsel
55
+ try {
56
+ await Bun.spawn(["xclip", "-selection", "clipboard"], { stdin: Buffer.from(text), timeout }).exited;
57
+ } catch {
58
+ await Bun.spawn(["xsel", "--clipboard", "--input"], { stdin: Buffer.from(text), timeout }).exited;
59
+ }
56
60
  }
57
61
  } catch (error) {
58
- if (error instanceof Error) {
59
- throw new Error(`Failed to copy to clipboard: ${error.message}`);
62
+ const msg = error instanceof Error ? error.message : String(error);
63
+ if (p === "linux") {
64
+ const tools = isWaylandSession() ? "wl-copy, xclip, or xsel" : "xclip or xsel";
65
+ throw new Error(`Failed to copy to clipboard. Install ${tools}: ${msg}`);
60
66
  }
61
- throw new Error(`Failed to copy to clipboard: ${String(error)}`);
67
+ throw new Error(`Failed to copy to clipboard: ${msg}`);
62
68
  }
63
- await Promise.race([promise, timeout]);
64
69
  }
65
70
 
66
71
  export interface ClipboardImage {
@@ -1,112 +0,0 @@
1
- /**
2
- * Session artifacts for subagent outputs.
3
- *
4
- * When a session exists, writes agent outputs to a sibling directory.
5
- * Otherwise uses temp files that are cleaned up after execution.
6
- */
7
-
8
- import * as fs from "node:fs";
9
- import * as os from "node:os";
10
- import * as path from "node:path";
11
- import { nanoid } from "nanoid";
12
-
13
- /**
14
- * Derive artifacts directory from session file path.
15
- *
16
- * /path/to/sessions/project/2026-01-01T14-28-11-636Z_uuid.jsonl
17
- * → /path/to/sessions/project/2026-01-01T14-28-11-636Z_uuid/
18
- */
19
- export function getArtifactsDir(sessionFile: string | null): string | null {
20
- if (!sessionFile) return null;
21
- // Strip .jsonl extension to get directory path
22
- if (sessionFile.endsWith(".jsonl")) {
23
- return sessionFile.slice(0, -6);
24
- }
25
- return sessionFile;
26
- }
27
-
28
- /**
29
- * Ensure artifacts directory exists.
30
- */
31
- export function ensureArtifactsDir(dir: string): void {
32
- if (!fs.existsSync(dir)) {
33
- fs.mkdirSync(dir, { recursive: true });
34
- }
35
- }
36
-
37
- /**
38
- * Generate artifact file paths for an agent run.
39
- */
40
- export function getArtifactPaths(
41
- dir: string,
42
- taskId: string,
43
- ): { inputPath: string; outputPath: string; jsonlPath: string } {
44
- return {
45
- inputPath: path.join(dir, `${taskId}.in.md`),
46
- outputPath: path.join(dir, `${taskId}.out.md`),
47
- jsonlPath: path.join(dir, `${taskId}.jsonl`),
48
- };
49
- }
50
-
51
- /**
52
- * Write artifacts for an agent run.
53
- */
54
- export async function writeArtifacts(
55
- dir: string,
56
- taskId: string,
57
- input: string,
58
- output: string,
59
- jsonlEvents?: string[],
60
- ): Promise<{ inputPath: string; outputPath: string; jsonlPath?: string }> {
61
- ensureArtifactsDir(dir);
62
-
63
- const paths = getArtifactPaths(dir, taskId);
64
-
65
- // Write input
66
- await Bun.write(paths.inputPath, input);
67
-
68
- // Write output
69
- await Bun.write(paths.outputPath, output);
70
-
71
- // Write JSONL if events provided
72
- if (jsonlEvents && jsonlEvents.length > 0) {
73
- await Bun.write(paths.jsonlPath, jsonlEvents.join("\n"));
74
- return paths;
75
- }
76
-
77
- return { inputPath: paths.inputPath, outputPath: paths.outputPath };
78
- }
79
-
80
- /**
81
- * Create a temporary artifacts directory.
82
- */
83
- export function createTempArtifactsDir(runId?: string): string {
84
- const id = runId || nanoid();
85
- const dir = path.join(os.tmpdir(), `omp-task-${id}`);
86
- ensureArtifactsDir(dir);
87
- return dir;
88
- }
89
-
90
- /**
91
- * Clean up temporary artifacts.
92
- */
93
- export async function cleanupTempArtifacts(paths: string[]): Promise<void> {
94
- for (const p of paths) {
95
- try {
96
- await fs.promises.unlink(p);
97
- } catch {
98
- // Ignore cleanup errors
99
- }
100
- }
101
- }
102
-
103
- /**
104
- * Clean up a temporary directory and its contents.
105
- */
106
- export async function cleanupTempDir(dir: string): Promise<void> {
107
- try {
108
- await fs.promises.rm(dir, { recursive: true, force: true });
109
- } catch {
110
- // Ignore cleanup errors
111
- }
112
- }