@oh-my-pi/pi-coding-agent 10.0.0 → 10.2.0

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 CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [10.1.0] - 2026-02-01
6
+ ### Added
7
+
8
+ - Added work scheduling profiler to debug menu for analyzing CPU scheduling patterns over the last 30 seconds
9
+ - Added support for work profile data in report bundles including folded stacks, summary, and flamegraph visualization
10
+
5
11
  ## [10.0.0] - 2026-02-01
6
12
  ### Added
7
13
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "10.0.0",
3
+ "version": "10.2.0",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "ompConfig": {
@@ -79,12 +79,12 @@
79
79
  "test": "bun test"
80
80
  },
81
81
  "dependencies": {
82
- "@oh-my-pi/omp-stats": "10.0.0",
83
- "@oh-my-pi/pi-agent-core": "10.0.0",
84
- "@oh-my-pi/pi-ai": "10.0.0",
85
- "@oh-my-pi/pi-natives": "10.0.0",
86
- "@oh-my-pi/pi-tui": "10.0.0",
87
- "@oh-my-pi/pi-utils": "10.0.0",
82
+ "@oh-my-pi/omp-stats": "10.2.0",
83
+ "@oh-my-pi/pi-agent-core": "10.2.0",
84
+ "@oh-my-pi/pi-ai": "10.2.0",
85
+ "@oh-my-pi/pi-natives": "10.2.0",
86
+ "@oh-my-pi/pi-tui": "10.2.0",
87
+ "@oh-my-pi/pi-utils": "10.2.0",
88
88
  "@openai/agents": "^0.4.4",
89
89
  "@sinclair/typebox": "^0.34.48",
90
90
  "ajv": "^8.17.1",
@@ -4,6 +4,7 @@
4
4
  * Provides tools for debugging, bug report generation, and system diagnostics.
5
5
  */
6
6
  import * as fs from "node:fs/promises";
7
+ import { getWorkProfile } from "@oh-my-pi/pi-natives/work";
7
8
  import { Container, Loader, type SelectItem, SelectList, Spacer, Text } from "@oh-my-pi/pi-tui";
8
9
  import { getSessionsDir } from "../config";
9
10
  import { DynamicBorder } from "../modes/components/dynamic-border";
@@ -17,6 +18,7 @@ import { collectSystemInfo, formatSystemInfo } from "./system-info";
17
18
  const DEBUG_MENU_ITEMS: SelectItem[] = [
18
19
  { value: "open-artifacts", label: "Open: artifact folder", description: "Open session artifacts in file manager" },
19
20
  { value: "performance", label: "Report: performance issue", description: "Profile CPU, reproduce, then bundle" },
21
+ { value: "work", label: "Profile: work scheduling", description: "Open flamegraph of last 30s" },
20
22
  { value: "dump", label: "Report: dump session", description: "Create report bundle immediately" },
21
23
  { value: "memory", label: "Report: memory issue", description: "Heap snapshot + bundle" },
22
24
  { value: "logs", label: "View: recent logs", description: "Show last 50 log entries" },
@@ -69,6 +71,9 @@ export class DebugSelectorComponent extends Container {
69
71
  case "performance":
70
72
  await this.handlePerformanceReport();
71
73
  break;
74
+ case "work":
75
+ await this.handleWorkReport();
76
+ break;
72
77
  case "dump":
73
78
  await this.handleDumpReport();
74
79
  break;
@@ -138,10 +143,12 @@ export class DebugSelectorComponent extends Container {
138
143
 
139
144
  try {
140
145
  const cpuProfile = await session.stop();
146
+ const workProfile = getWorkProfile(30);
141
147
  const result = await createReportBundle({
142
148
  sessionFile: this.ctx.sessionManager.getSessionFile(),
143
149
  settings: this.getResolvedSettings(),
144
150
  cpuProfile,
151
+ workProfile,
145
152
  });
146
153
 
147
154
  loader.stop();
@@ -162,6 +169,39 @@ export class DebugSelectorComponent extends Container {
162
169
  this.ctx.ui.requestRender();
163
170
  }
164
171
 
172
+ private async handleWorkReport(): Promise<void> {
173
+ try {
174
+ const workProfile = getWorkProfile(30);
175
+
176
+ if (!workProfile.svg) {
177
+ this.ctx.showWarning(`No work profile data (${workProfile.sampleCount} samples)`);
178
+ return;
179
+ }
180
+
181
+ // Write SVG to temp file and open in browser
182
+ const tmpPath = `/tmp/work-profile-${Date.now()}.svg`;
183
+ await Bun.write(tmpPath, workProfile.svg);
184
+
185
+ const openCmd =
186
+ process.platform === "darwin"
187
+ ? ["open", tmpPath]
188
+ : process.platform === "win32"
189
+ ? ["cmd", "/c", "start", "", tmpPath]
190
+ : ["xdg-open", tmpPath];
191
+
192
+ Bun.spawn(openCmd, { stdout: "ignore", stderr: "ignore" }).unref();
193
+
194
+ this.ctx.chatContainer.addChild(new Spacer(1));
195
+ this.ctx.chatContainer.addChild(
196
+ new Text(theme.fg("dim", `Opened flamegraph (${workProfile.sampleCount} samples)`), 1, 0),
197
+ );
198
+ } catch (err) {
199
+ this.ctx.showError(`Failed to open profile: ${err instanceof Error ? err.message : String(err)}`);
200
+ }
201
+
202
+ this.ctx.ui.requestRender();
203
+ }
204
+
165
205
  private async handleDumpReport(): Promise<void> {
166
206
  const loader = new Loader(
167
207
  this.ctx.ui,
@@ -6,6 +6,7 @@
6
6
  import * as fs from "node:fs/promises";
7
7
  import * as os from "node:os";
8
8
  import * as path from "node:path";
9
+ import type { WorkProfile } from "@oh-my-pi/pi-natives/work";
9
10
  import { isEnoent } from "@oh-my-pi/pi-utils";
10
11
  import type { CpuProfile, HeapSnapshot } from "./profiler";
11
12
  import { collectSystemInfo, sanitizeEnv } from "./system-info";
@@ -42,6 +43,8 @@ export interface ReportBundleOptions {
42
43
  cpuProfile?: CpuProfile;
43
44
  /** Heap snapshot (for memory reports) */
44
45
  heapSnapshot?: HeapSnapshot;
46
+ /** Work profile (for work scheduling reports) */
47
+ workProfile?: WorkProfile;
45
48
  }
46
49
 
47
50
  export interface ReportBundleResult {
@@ -63,6 +66,9 @@ export interface ReportBundleResult {
63
66
  * - profile.cpuprofile: CPU profile (performance report only)
64
67
  * - profile.md: Markdown CPU profile (performance report only)
65
68
  * - heap.heapsnapshot: Heap snapshot (memory report only)
69
+ * - work.folded: Work profile folded stacks (work report only)
70
+ * - work.md: Work profile summary (work report only)
71
+ * - work.svg: Work profile flamegraph (work report only)
66
72
  */
67
73
  export async function createReportBundle(options: ReportBundleOptions): Promise<ReportBundleResult> {
68
74
  const reportsDir = getReportsDir();
@@ -131,6 +137,18 @@ export async function createReportBundle(options: ReportBundleOptions): Promise<
131
137
  files.push("heap.heapsnapshot");
132
138
  }
133
139
 
140
+ // Work profile
141
+ if (options.workProfile) {
142
+ data["work.folded"] = options.workProfile.folded;
143
+ files.push("work.folded");
144
+ data["work.md"] = options.workProfile.summary;
145
+ files.push("work.md");
146
+ if (options.workProfile.svg) {
147
+ data["work.svg"] = options.workProfile.svg;
148
+ files.push("work.svg");
149
+ }
150
+ }
151
+
134
152
  // Write archive
135
153
  await Bun.Archive.write(outputPath, data, { compress: "gzip" });
136
154
 
@@ -65,8 +65,6 @@ export async function executeBash(command: string, options?: BashExecutorOptions
65
65
  };
66
66
  }
67
67
 
68
- let abortListener: (() => void) | undefined;
69
-
70
68
  try {
71
69
  const sessionKey = buildSessionKey(shell, prefix, snapshotPath, shellEnv, options?.sessionKey);
72
70
  let shellSession = shellSessions.get(sessionKey);
@@ -75,19 +73,13 @@ export async function executeBash(command: string, options?: BashExecutorOptions
75
73
  shellSessions.set(sessionKey, shellSession);
76
74
  }
77
75
 
78
- if (options?.signal) {
79
- abortListener = () => {
80
- shellSession?.abort();
81
- };
82
- options.signal.addEventListener("abort", abortListener, { once: true });
83
- }
84
-
85
76
  const result = await shellSession.run(
86
77
  {
87
78
  command: finalCommand,
88
79
  cwd: options?.cwd,
89
80
  env: options?.env,
90
81
  timeoutMs: options?.timeout,
82
+ signal: options?.signal,
91
83
  },
92
84
  (err, chunk) => {
93
85
  if (!err) {
@@ -127,9 +119,6 @@ export async function executeBash(command: string, options?: BashExecutorOptions
127
119
  };
128
120
  } finally {
129
121
  await pendingChunks;
130
- if (options?.signal && abortListener) {
131
- options.signal.removeEventListener("abort", abortListener);
132
- }
133
122
  }
134
123
  }
135
124
 
@@ -4,7 +4,7 @@
4
4
  import * as fs from "node:fs/promises";
5
5
  import * as os from "node:os";
6
6
  import * as path from "node:path";
7
- import { getSystemInfo as getNativeSystemInfo, type SystemInfo, find as wasmFind } from "@oh-my-pi/pi-natives";
7
+ import { FileType, getSystemInfo as getNativeSystemInfo, glob, type SystemInfo } from "@oh-my-pi/pi-natives";
8
8
  import { untilAborted } from "@oh-my-pi/pi-utils";
9
9
  import { $ } from "bun";
10
10
  import chalk from "chalk";
@@ -184,10 +184,10 @@ async function scanProjectTreeWithGlob(root: string): Promise<ProjectTreeScan |
184
184
  const timeoutSignal = AbortSignal.timeout(GLOB_TIMEOUT_MS);
185
185
  try {
186
186
  const result = await untilAborted(timeoutSignal, () =>
187
- wasmFind({
187
+ glob({
188
188
  pattern: "**/*",
189
189
  path: root,
190
- fileType: "file",
190
+ fileType: FileType.File,
191
191
  }),
192
192
  );
193
193
  entries = result.matches.map(match => match.path).filter(entry => entry.length > 0);
package/src/tools/find.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
4
- import { type FindMatch, find as wasmFind } from "@oh-my-pi/pi-natives";
4
+ import { FileType, type GlobMatch, glob } from "@oh-my-pi/pi-natives";
5
5
  import type { Component } from "@oh-my-pi/pi-tui";
6
6
  import { Text } from "@oh-my-pi/pi-tui";
7
7
  import { isEnoent, untilAborted } from "@oh-my-pi/pi-utils";
@@ -252,7 +252,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
252
252
  throw new ToolError(`Path is not a directory: ${searchPath}`);
253
253
  }
254
254
 
255
- let matches: Awaited<ReturnType<typeof wasmFind>>["matches"];
255
+ let matches: Awaited<ReturnType<typeof glob>>["matches"];
256
256
  const onUpdateMatches: string[] = [];
257
257
  const updateIntervalMs = 200;
258
258
  let lastUpdate = 0;
@@ -273,11 +273,11 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
273
273
  });
274
274
  };
275
275
  const onMatch = onUpdate
276
- ? (match: FindMatch | null) => {
276
+ ? (match: GlobMatch | null) => {
277
277
  if (signal?.aborted || !match) return;
278
278
  let relativePath = match.path;
279
279
  if (!relativePath) return;
280
- if (match.fileType === "dir" && !relativePath.endsWith("/")) {
280
+ if (match.fileType === FileType.Dir && !relativePath.endsWith("/")) {
281
281
  relativePath += "/";
282
282
  }
283
283
  onUpdateMatches.push(relativePath);
@@ -288,11 +288,11 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
288
288
  const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
289
289
  try {
290
290
  const result = await untilAborted(combinedSignal, () =>
291
- wasmFind(
291
+ glob(
292
292
  {
293
293
  pattern: globPattern,
294
294
  path: searchPath,
295
- fileType: "file",
295
+ fileType: FileType.File,
296
296
  hidden: includeHidden,
297
297
  maxResults: effectiveLimit,
298
298
  sortByMtime: true,
@@ -328,7 +328,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
328
328
  const hadTrailingSlash = line.endsWith("/") || line.endsWith("\\");
329
329
  let relativePath = line;
330
330
 
331
- const isDirectory = match.fileType === "dir";
331
+ const isDirectory = match.fileType === FileType.Dir;
332
332
 
333
333
  if ((isDirectory || hadTrailingSlash) && !relativePath.endsWith("/")) {
334
334
  relativePath += "/";
package/src/tools/read.ts CHANGED
@@ -3,7 +3,7 @@ import * as os from "node:os";
3
3
  import path from "node:path";
4
4
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
5
5
  import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
6
- import { find as wasmFind } from "@oh-my-pi/pi-natives";
6
+ import { FileType, glob } from "@oh-my-pi/pi-natives";
7
7
  import type { Component } from "@oh-my-pi/pi-tui";
8
8
  import { Text } from "@oh-my-pi/pi-tui";
9
9
  import { ptree, untilAborted } from "@oh-my-pi/pi-utils";
@@ -361,10 +361,10 @@ async function listCandidateFiles(
361
361
  const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
362
362
  try {
363
363
  const result = await untilAborted(combinedSignal, () =>
364
- wasmFind({
364
+ glob({
365
365
  pattern: "**/*",
366
366
  path: searchRoot,
367
- fileType: "file",
367
+ fileType: FileType.File,
368
368
  hidden: true,
369
369
  }),
370
370
  );