@oh-my-pi/pi-coding-agent 14.9.5 → 14.9.8

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 (54) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/package.json +7 -7
  3. package/scripts/generate-template.ts +4 -3
  4. package/src/cli/setup-cli.ts +14 -161
  5. package/src/cli/stats-cli.ts +56 -2
  6. package/src/cli.ts +0 -1
  7. package/src/config/settings-schema.ts +0 -10
  8. package/src/eval/eval.lark +30 -10
  9. package/src/eval/js/context-manager.ts +334 -564
  10. package/src/eval/js/shared/helpers.ts +237 -0
  11. package/src/eval/js/shared/indirect-eval.ts +30 -0
  12. package/src/eval/js/shared/rewrite-imports.ts +211 -0
  13. package/src/eval/js/shared/runtime.ts +168 -0
  14. package/src/eval/js/shared/types.ts +18 -0
  15. package/src/eval/js/tool-bridge.ts +2 -4
  16. package/src/eval/js/worker-core.ts +146 -0
  17. package/src/eval/js/worker-entry.ts +24 -0
  18. package/src/eval/js/worker-protocol.ts +41 -0
  19. package/src/eval/parse.ts +218 -49
  20. package/src/eval/py/display.ts +71 -0
  21. package/src/eval/py/executor.ts +74 -89
  22. package/src/eval/py/index.ts +1 -2
  23. package/src/eval/py/kernel.ts +472 -900
  24. package/src/eval/py/prelude.py +95 -7
  25. package/src/eval/py/runner.py +879 -0
  26. package/src/eval/py/runtime.ts +3 -16
  27. package/src/eval/py/tool-bridge.ts +137 -0
  28. package/src/export/html/index.ts +5 -2
  29. package/src/export/html/template.generated.ts +1 -1
  30. package/src/export/html/template.js +93 -5
  31. package/src/export/html/template.macro.ts +4 -3
  32. package/src/internal-urls/docs-index.generated.ts +3 -3
  33. package/src/modes/components/read-tool-group.ts +9 -0
  34. package/src/modes/controllers/command-controller.ts +0 -23
  35. package/src/prompts/tools/eval.md +14 -27
  36. package/src/prompts/tools/read.md +1 -0
  37. package/src/session/agent-session.ts +0 -1
  38. package/src/session/history-storage.ts +77 -19
  39. package/src/tools/browser/tab-protocol.ts +4 -0
  40. package/src/tools/browser/tab-supervisor.ts +86 -5
  41. package/src/tools/browser/tab-worker.ts +104 -58
  42. package/src/tools/conflict-detect.ts +661 -0
  43. package/src/tools/eval.ts +1 -1
  44. package/src/tools/index.ts +6 -0
  45. package/src/tools/path-utils.ts +1 -1
  46. package/src/tools/read.ts +130 -0
  47. package/src/tools/write.ts +204 -0
  48. package/src/web/search/index.ts +6 -4
  49. package/src/cli/jupyter-cli.ts +0 -106
  50. package/src/commands/jupyter.ts +0 -32
  51. package/src/eval/py/cancellation.ts +0 -28
  52. package/src/eval/py/gateway-coordinator.ts +0 -424
  53. /package/src/eval/js/{prelude.ts → shared/prelude.ts} +0 -0
  54. /package/src/eval/js/{prelude.txt → shared/prelude.txt} +0 -0
package/src/tools/read.ts CHANGED
@@ -33,6 +33,17 @@ import { ImageInputTooLargeError, loadImageInput, MAX_IMAGE_INPUT_BYTES } from "
33
33
  import { convertFileWithMarkit } from "../utils/markit";
34
34
  import { buildDirectoryTree, type DirectoryTree } from "../workspace-tree";
35
35
  import { type ArchiveReader, openArchive, parseArchivePathCandidates } from "./archive-reader";
36
+ import {
37
+ type ConflictEntry,
38
+ type ConflictScope,
39
+ formatConflictSummary,
40
+ formatConflictWarning,
41
+ getConflictHistory,
42
+ parseConflictUri,
43
+ renderConflictRegion,
44
+ scanConflictLines,
45
+ scanFileForConflicts,
46
+ } from "./conflict-detect";
36
47
  import {
37
48
  executeReadUrl,
38
49
  isReadableUrlPath,
@@ -455,6 +466,8 @@ export interface ReadToolDetails {
455
466
  * so the TUI can render the file content with its own gutter without re-parsing the formatted text. */
456
467
  displayContent?: { text: string; startLine: number };
457
468
  summary?: { lines: number; elidedSpans: number };
469
+ /** Number of unresolved git conflicts surfaced by this read (TUI uses for inline `⚠ N` badge). */
470
+ conflictCount?: number;
458
471
  }
459
472
 
460
473
  type ReadParams = ReadToolInput;
@@ -463,6 +476,7 @@ type ReadParams = ReadToolInput;
463
476
  type ParsedSelector =
464
477
  | { kind: "none" }
465
478
  | { kind: "raw" }
479
+ | { kind: "conflicts" }
466
480
  | { kind: "lines"; startLine: number; endLine: number | undefined; raw?: boolean };
467
481
 
468
482
  const LINE_RANGE_RE = /^L?(\d+)(?:([-+])L?(\d+))?$/i;
@@ -521,6 +535,7 @@ function parseSel(sel: string | undefined): ParsedSelector {
521
535
  }
522
536
 
523
537
  if (sel.toLowerCase() === "raw") return { kind: "raw" };
538
+ if (sel.toLowerCase() === "conflicts") return { kind: "conflicts" };
524
539
  const range = parseLineRangeChunk(sel);
525
540
  if (range) {
526
541
  return { kind: "lines", startLine: range.startLine, endLine: range.endLine };
@@ -1153,6 +1168,16 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1153
1168
  if (readPath.startsWith("file://")) {
1154
1169
  readPath = expandPath(readPath);
1155
1170
  }
1171
+
1172
+ const conflictUri = parseConflictUri(readPath);
1173
+ if (conflictUri) {
1174
+ if (conflictUri.id === "*") {
1175
+ throw new ToolError(
1176
+ "`read conflict://*` is not supported — wildcards are write-only. Use `read <path>:conflicts` for the full list of conflicts in a file, or `read conflict://<N>` to inspect a single block.",
1177
+ );
1178
+ }
1179
+ return this.#readConflictRegion(conflictUri.id, conflictUri.scope);
1180
+ }
1156
1181
  const displayMode = resolveFileDisplayMode(this.session);
1157
1182
 
1158
1183
  const parsedUrlTarget = parseReadUrlTarget(readPath);
@@ -1257,6 +1282,10 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1257
1282
  return dirResult;
1258
1283
  }
1259
1284
 
1285
+ if (parsed.kind === "conflicts") {
1286
+ return this.#readFileConflicts(absolutePath, suffixResolution, signal);
1287
+ }
1288
+
1260
1289
  const imageMetadata = await readImageMetadata(absolutePath);
1261
1290
  const mimeType = imageMetadata?.mimeType;
1262
1291
  const ext = path.extname(absolutePath).toLowerCase();
@@ -1517,6 +1546,38 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1517
1546
  details.displayContent = capturedDisplayContent;
1518
1547
  }
1519
1548
 
1549
+ if (!firstLineExceedsLimit && collectedLines.length > 0) {
1550
+ const blocks = scanConflictLines(collectedLines, startLineDisplay);
1551
+ if (blocks.length > 0) {
1552
+ const history = getConflictHistory(this.session);
1553
+ const displayPathForWarning = formatPathRelativeToCwd(absolutePath, this.session.cwd);
1554
+ const entries = blocks.map(block =>
1555
+ history.register({
1556
+ absolutePath,
1557
+ displayPath: displayPathForWarning,
1558
+ ...block,
1559
+ }),
1560
+ );
1561
+ // Cheap full-file scan only when the window already showed
1562
+ // at least one conflict — otherwise pay nothing on clean files.
1563
+ let totalInFile = entries.length;
1564
+ let scanTruncated = false;
1565
+ try {
1566
+ const fileScan = await scanFileForConflicts(absolutePath);
1567
+ totalInFile = Math.max(entries.length, fileScan.blocks.length);
1568
+ scanTruncated = fileScan.scanTruncated;
1569
+ } catch {
1570
+ // Best-effort enrichment; fall back to window-only count.
1571
+ }
1572
+ outputText += formatConflictWarning(entries, {
1573
+ totalInFile,
1574
+ displayPath: displayPathForWarning,
1575
+ scanTruncated,
1576
+ });
1577
+ details.conflictCount = entries.length;
1578
+ }
1579
+ }
1580
+
1520
1581
  content = [{ type: "text", text: outputText }];
1521
1582
  }
1522
1583
  }
@@ -1542,6 +1603,71 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
1542
1603
  return resultBuilder.done();
1543
1604
  }
1544
1605
 
1606
+ /**
1607
+ * Render a `conflict://<N>` (or `conflict://<N>/<scope>`) region as
1608
+ * regular file content. The lines are emitted with their original
1609
+ * file line numbers so hashline anchors line up with the source
1610
+ * file, and no truncation footer is appended.
1611
+ */
1612
+ async #readConflictRegion(id: number, scope: ConflictScope | undefined): Promise<AgentToolResult<ReadToolDetails>> {
1613
+ const entry: ConflictEntry | undefined = getConflictHistory(this.session).get(id);
1614
+ if (!entry) {
1615
+ throw new ToolError(
1616
+ `Conflict #${id} not found. Conflict ids are registered when \`read\` surfaces a marker block; re-read the file to get a current id.`,
1617
+ );
1618
+ }
1619
+
1620
+ const region = renderConflictRegion(entry, scope);
1621
+ const displayMode = resolveFileDisplayMode(this.session);
1622
+ const shouldAddHashLines = displayMode.hashLines;
1623
+ const shouldAddLineNumbers = shouldAddHashLines ? false : displayMode.lineNumbers;
1624
+
1625
+ const rawText = region.lines.join("\n");
1626
+ const formattedText = formatTextWithMode(rawText, region.startLine, shouldAddHashLines, shouldAddLineNumbers);
1627
+
1628
+ const details: ReadToolDetails = {
1629
+ resolvedPath: entry.absolutePath,
1630
+ displayContent: { text: rawText, startLine: region.startLine },
1631
+ };
1632
+ return toolResult<ReadToolDetails>(details).text(formattedText).sourcePath(entry.absolutePath).done();
1633
+ }
1634
+
1635
+ /**
1636
+ * Implement `read <path>:conflicts`: scan the whole file once, register
1637
+ * every block in the session's conflict history, and return a compact
1638
+ * `#N L_a-L_b` index instead of file content. Designed for heavily
1639
+ * conflicted files where dumping every body would be wasteful.
1640
+ */
1641
+ async #readFileConflicts(
1642
+ absolutePath: string,
1643
+ suffixResolution: { from: string; to: string } | undefined,
1644
+ signal: AbortSignal | undefined,
1645
+ ): Promise<AgentToolResult<ReadToolDetails>> {
1646
+ throwIfAborted(signal);
1647
+ const scan = await scanFileForConflicts(absolutePath);
1648
+ const displayPath = formatPathRelativeToCwd(absolutePath, this.session.cwd);
1649
+ const history = getConflictHistory(this.session);
1650
+ const entries = scan.blocks.map(block =>
1651
+ history.register({
1652
+ absolutePath,
1653
+ displayPath,
1654
+ ...block,
1655
+ }),
1656
+ );
1657
+
1658
+ const summary =
1659
+ entries.length === 0
1660
+ ? `No unresolved git merge conflicts in ${displayPath}.`
1661
+ : formatConflictSummary(entries, { displayPath, scanTruncated: scan.scanTruncated });
1662
+
1663
+ const details: ReadToolDetails = {
1664
+ resolvedPath: absolutePath,
1665
+ suffixResolution,
1666
+ conflictCount: entries.length,
1667
+ };
1668
+ return toolResult<ReadToolDetails>(details).text(summary).sourcePath(absolutePath).done();
1669
+ }
1670
+
1545
1671
  /**
1546
1672
  * Handle internal URLs (agent://, artifact://, memory://, skill://, rule://, local://, mcp://).
1547
1673
  * Supports pagination via offset/limit but rejects them when query extraction is used.
@@ -1763,6 +1889,10 @@ export const readToolRenderer = {
1763
1889
  if (details?.summary) {
1764
1890
  title += ` (summary: ${details.summary.elidedSpans} elided span${details.summary.elidedSpans === 1 ? "" : "s"})`;
1765
1891
  }
1892
+ if (details?.conflictCount && details.conflictCount > 0) {
1893
+ const n = details.conflictCount;
1894
+ title += ` ${uiTheme.fg("warning", `(⚠ ${n} conflict${n === 1 ? "" : "s"})`)}`;
1895
+ }
1766
1896
  let cachedWidth: number | undefined;
1767
1897
  let cachedLines: string[] | undefined;
1768
1898
  return {
@@ -16,6 +16,13 @@ import { Ellipsis, Hasher, type RenderCache, renderStatusLine, truncateToWidth }
16
16
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
17
17
  import { parseArchivePathCandidates } from "./archive-reader";
18
18
  import { assertEditableFile } from "./auto-generated-guard";
19
+ import {
20
+ type ConflictEntry,
21
+ expandContentTokens,
22
+ getConflictHistory,
23
+ parseConflictUri,
24
+ spliceConflict,
25
+ } from "./conflict-detect";
19
26
  import { invalidateFsScanAfterWrite } from "./fs-cache-invalidation";
20
27
  import { type OutputMeta, outputMeta } from "./output-meta";
21
28
  import { formatPathRelativeToCwd } from "./path-utils";
@@ -423,6 +430,185 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
423
430
  }
424
431
  }
425
432
 
433
+ /**
434
+ * Resolve a single `conflict://<N>` write by splicing the recorded
435
+ * marker region in the registered file with `replacementContent`,
436
+ * then routing the new file content through the normal writethrough
437
+ * pipeline so LSP format/diagnostics still run.
438
+ *
439
+ * Entry ids are session-stable: they keep working even after later
440
+ * writes resolve other blocks in the same file. The recorded range
441
+ * is re-validated on disk before splicing so an out-of-band edit
442
+ * surfaces as a clear error instead of corrupting the file.
443
+ */
444
+ async #resolveConflict(
445
+ entry: ConflictEntry,
446
+ replacementContent: string,
447
+ stripped: boolean,
448
+ signal: AbortSignal | undefined,
449
+ context: AgentToolContext | undefined,
450
+ ): Promise<AgentToolResult<WriteToolDetails>> {
451
+ const absolutePath = entry.absolutePath;
452
+ if (!(await fs.exists(absolutePath))) {
453
+ throw new ToolError(`Conflict #${entry.id} target '${entry.displayPath}' no longer exists.`);
454
+ }
455
+
456
+ const expanded = expandContentTokens(replacementContent, entry);
457
+ const originalText = await Bun.file(absolutePath).text();
458
+ const newContent = spliceConflict(originalText, entry, expanded);
459
+
460
+ const batchRequest = getLspBatchRequest(context?.toolCall);
461
+ const diagnostics = await this.#writethrough(absolutePath, newContent, signal, undefined, batchRequest);
462
+ invalidateFsScanAfterWrite(absolutePath);
463
+ this.session.fileReadCache?.invalidate(absolutePath);
464
+ this.session.conflictHistory?.invalidate(entry.id);
465
+
466
+ const range =
467
+ entry.startLine === entry.endLine
468
+ ? `line ${entry.startLine}`
469
+ : `lines ${entry.startLine}\u2013${entry.endLine}`;
470
+ let resultText = `Resolved conflict #${entry.id} at ${range} in ${entry.displayPath}.`;
471
+ if (stripped) {
472
+ resultText += `\nNote: auto-stripped hashline display prefixes from content before writing.`;
473
+ }
474
+
475
+ if (!diagnostics) {
476
+ return {
477
+ content: [{ type: "text", text: resultText }],
478
+ details: {},
479
+ };
480
+ }
481
+ return {
482
+ content: [{ type: "text", text: resultText }],
483
+ details: {
484
+ diagnostics,
485
+ meta: outputMeta()
486
+ .diagnostics(diagnostics.summary, diagnostics.messages ?? [])
487
+ .get(),
488
+ },
489
+ };
490
+ }
491
+
492
+ /**
493
+ * Bulk-resolve every registered conflict via `conflict://*`.
494
+ *
495
+ * Entries are grouped by file and applied bottom-up by recorded start
496
+ * line so each splice keeps later anchors valid. `content` tokens are
497
+ * expanded *per entry*, so `content: "@ours"` keeps each block's own
498
+ * ours side rather than collapsing every conflict to the first
499
+ * block's ours.
500
+ *
501
+ * All-or-nothing semantics within a file: if any splice for a file
502
+ * fails (stale anchors, missing base for `@base`, etc.), that file is
503
+ * left untouched and the error is surfaced. Files that succeed are
504
+ * still written. The result text reports per-file counts so the agent
505
+ * can re-read the failed files and retry.
506
+ */
507
+ async #resolveAllConflicts(
508
+ replacementContent: string,
509
+ stripped: boolean,
510
+ signal: AbortSignal | undefined,
511
+ context: AgentToolContext | undefined,
512
+ ): Promise<AgentToolResult<WriteToolDetails>> {
513
+ const history = getConflictHistory(this.session);
514
+ const allEntries = history.entries();
515
+ if (allEntries.length === 0) {
516
+ throw new ToolError(
517
+ "`conflict://*` has nothing to resolve — no conflicts are currently registered. Re-read the file(s) with conflicts first.",
518
+ );
519
+ }
520
+
521
+ const byFile = new Map<string, ConflictEntry[]>();
522
+ for (const entry of allEntries) {
523
+ const bucket = byFile.get(entry.absolutePath) ?? [];
524
+ bucket.push(entry);
525
+ byFile.set(entry.absolutePath, bucket);
526
+ }
527
+
528
+ const batchRequest = getLspBatchRequest(context?.toolCall);
529
+ const allDiagnostics: FileDiagnosticsResult[] = [];
530
+ const succeededFiles: { displayPath: string; count: number }[] = [];
531
+ const failedFiles: { displayPath: string; count: number; error: string }[] = [];
532
+ let totalResolvedIds = 0;
533
+
534
+ for (const [absolutePath, fileEntries] of byFile) {
535
+ const sample = fileEntries[0]!;
536
+ if (!(await fs.exists(absolutePath))) {
537
+ failedFiles.push({
538
+ displayPath: sample.displayPath,
539
+ count: fileEntries.length,
540
+ error: "file no longer exists",
541
+ });
542
+ continue;
543
+ }
544
+
545
+ fileEntries.sort((a, b) => b.startLine - a.startLine);
546
+
547
+ let text: string;
548
+ try {
549
+ text = await Bun.file(absolutePath).text();
550
+ for (const entry of fileEntries) {
551
+ const expanded = expandContentTokens(replacementContent, entry);
552
+ text = spliceConflict(text, entry, expanded);
553
+ }
554
+ } catch (error) {
555
+ failedFiles.push({
556
+ displayPath: sample.displayPath,
557
+ count: fileEntries.length,
558
+ error: error instanceof Error ? error.message : String(error),
559
+ });
560
+ continue;
561
+ }
562
+
563
+ const diagnostics = await this.#writethrough(absolutePath, text, signal, undefined, batchRequest);
564
+ invalidateFsScanAfterWrite(absolutePath);
565
+ this.session.fileReadCache?.invalidate(absolutePath);
566
+ for (const entry of fileEntries) history.invalidate(entry.id);
567
+ succeededFiles.push({ displayPath: sample.displayPath, count: fileEntries.length });
568
+ totalResolvedIds += fileEntries.length;
569
+ if (diagnostics) allDiagnostics.push(diagnostics);
570
+ }
571
+
572
+ const summaryLines: string[] = [];
573
+ const fileWord = (n: number) => (n === 1 ? "file" : "files");
574
+ const conflictWord = (n: number) => (n === 1 ? "conflict" : "conflicts");
575
+ if (succeededFiles.length > 0) {
576
+ summaryLines.push(
577
+ `Resolved ${totalResolvedIds} ${conflictWord(totalResolvedIds)} across ${succeededFiles.length} ${fileWord(succeededFiles.length)}:`,
578
+ );
579
+ for (const file of succeededFiles) {
580
+ summaryLines.push(` ${file.displayPath}: ${file.count} ${conflictWord(file.count)}`);
581
+ }
582
+ }
583
+ if (failedFiles.length > 0) {
584
+ summaryLines.push(
585
+ `Failed to resolve ${failedFiles.length} ${fileWord(failedFiles.length)} — registered entries left intact for retry:`,
586
+ );
587
+ for (const file of failedFiles) {
588
+ summaryLines.push(` ${file.displayPath}: ${file.count} ${conflictWord(file.count)} (${file.error})`);
589
+ }
590
+ }
591
+ if (stripped) {
592
+ summaryLines.push("Note: auto-stripped hashline display prefixes from content before writing.");
593
+ }
594
+ const resultText = summaryLines.join("\n");
595
+
596
+ if (allDiagnostics.length === 0) {
597
+ if (failedFiles.length > 0 && succeededFiles.length === 0) {
598
+ throw new ToolError(resultText);
599
+ }
600
+ return { content: [{ type: "text", text: resultText }], details: {} };
601
+ }
602
+ const mergedSummary = allDiagnostics.map(d => d.summary).join("\n");
603
+ const mergedMessages = allDiagnostics.flatMap(d => d.messages ?? []);
604
+ return {
605
+ content: [{ type: "text", text: resultText }],
606
+ details: {
607
+ meta: outputMeta().diagnostics(mergedSummary, mergedMessages).get(),
608
+ },
609
+ };
610
+ }
611
+
426
612
  async execute(
427
613
  _toolCallId: string,
428
614
  { path, content }: WriteParams,
@@ -433,6 +619,24 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
433
619
  return untilAborted(signal, async () => {
434
620
  // Strip hashline display prefixes (LINE+ID|) if the model copied them from read output
435
621
  const { text: cleanContent, stripped } = stripWriteContent(this.session, content);
622
+ const conflictUri = parseConflictUri(path);
623
+ if (conflictUri) {
624
+ if (conflictUri.scope) {
625
+ throw new ToolError(
626
+ `Conflict URI scope '/${conflictUri.scope}' is read-only — use \`read conflict://${conflictUri.id}/${conflictUri.scope}\` to inspect that side. To write, drop the scope (\`conflict://${conflictUri.id}\`) and put the chosen content (or shorthand like \`@${conflictUri.scope}\`) in \`content\`.`,
627
+ );
628
+ }
629
+ if (conflictUri.id === "*") {
630
+ return this.#resolveAllConflicts(cleanContent, stripped, signal, context);
631
+ }
632
+ const entry = getConflictHistory(this.session).get(conflictUri.id);
633
+ if (!entry) {
634
+ throw new ToolError(
635
+ `Conflict #${conflictUri.id} not found. Conflict ids are registered when \`read\` surfaces a marker block; re-read the file to get a current id.`,
636
+ );
637
+ }
638
+ return this.#resolveConflict(entry, cleanContent, stripped, signal, context);
639
+ }
436
640
  const resolvedArchivePath = await this.#resolveArchiveWritePath(path);
437
641
  if (resolvedArchivePath) {
438
642
  enforcePlanModeWrite(this.session, resolvedArchivePath.archivePath, {
@@ -136,6 +136,7 @@ function formatForLLM(response: SearchResponse): string {
136
136
  async function executeSearch(
137
137
  _toolCallId: string,
138
138
  params: SearchQueryParams,
139
+ signal?: AbortSignal,
139
140
  ): Promise<{ content: Array<{ type: "text"; text: string }>; details: SearchRenderDetails }> {
140
141
  const providers =
141
142
  params.provider && params.provider !== "auto"
@@ -165,6 +166,7 @@ async function executeSearch(
165
166
  maxOutputTokens: params.max_tokens,
166
167
  numSearchResults: params.num_search_results,
167
168
  temperature: params.temperature,
169
+ signal,
168
170
  });
169
171
 
170
172
  const text = formatForLLM(response);
@@ -221,11 +223,11 @@ export class WebSearchTool implements AgentTool<typeof webSearchSchema, SearchRe
221
223
  async execute(
222
224
  _toolCallId: string,
223
225
  params: SearchToolParams,
224
- _signal?: AbortSignal,
226
+ signal?: AbortSignal,
225
227
  _onUpdate?: AgentToolUpdateCallback<SearchRenderDetails>,
226
228
  _context?: AgentToolContext,
227
229
  ): Promise<AgentToolResult<SearchRenderDetails>> {
228
- return executeSearch(_toolCallId, params);
230
+ return executeSearch(_toolCallId, params, signal);
229
231
  }
230
232
  }
231
233
 
@@ -241,9 +243,9 @@ export const webSearchCustomTool: CustomTool<typeof webSearchSchema, SearchRende
241
243
  params: SearchToolParams,
242
244
  _onUpdate,
243
245
  _ctx: CustomToolContext,
244
- _signal?: AbortSignal,
246
+ signal?: AbortSignal,
245
247
  ) {
246
- return executeSearch(toolCallId, params);
248
+ return executeSearch(toolCallId, params, signal);
247
249
  },
248
250
 
249
251
  renderCall(args: SearchToolParams, options: RenderResultOptions, theme: Theme) {
@@ -1,106 +0,0 @@
1
- /**
2
- * Jupyter CLI command handlers.
3
- *
4
- * Handles `omp jupyter` subcommand for managing the shared Python gateway.
5
- */
6
-
7
- import { APP_NAME } from "@oh-my-pi/pi-utils";
8
- import chalk from "chalk";
9
- import { getGatewayStatus, shutdownSharedGateway } from "../eval/py/gateway-coordinator";
10
-
11
- export type JupyterAction = "kill" | "status";
12
-
13
- export interface JupyterCommandArgs {
14
- action: JupyterAction;
15
- }
16
-
17
- export function parseJupyterArgs(args: string[]): JupyterCommandArgs | undefined {
18
- if (args.length === 0 || args[0] !== "jupyter") {
19
- return undefined;
20
- }
21
-
22
- const action = args[1] as JupyterAction | undefined;
23
- if (!action || !["kill", "status"].includes(action)) {
24
- return { action: "status" };
25
- }
26
-
27
- return { action };
28
- }
29
-
30
- export async function runJupyterCommand(cmd: JupyterCommandArgs): Promise<void> {
31
- switch (cmd.action) {
32
- case "kill":
33
- await runKill();
34
- break;
35
- case "status":
36
- await runStatus();
37
- break;
38
- }
39
- }
40
-
41
- async function runKill(): Promise<void> {
42
- const status = await getGatewayStatus();
43
-
44
- if (!status.active) {
45
- console.log(chalk.dim("No Jupyter gateway is running"));
46
- return;
47
- }
48
-
49
- console.log(`Killing Jupyter gateway (PID ${status.pid})...`);
50
- await shutdownSharedGateway();
51
- console.log(chalk.green("Jupyter gateway stopped"));
52
- }
53
-
54
- async function runStatus(): Promise<void> {
55
- const status = await getGatewayStatus();
56
-
57
- if (!status.active) {
58
- console.log(chalk.dim("No Jupyter gateway is running"));
59
- return;
60
- }
61
-
62
- console.log(chalk.bold("Jupyter Gateway Status\n"));
63
- console.log(` ${chalk.green("●")} Running`);
64
- console.log(` PID: ${status.pid}`);
65
- console.log(` URL: ${status.url}`);
66
- if (status.uptime !== null) {
67
- console.log(` Uptime: ${formatUptime(status.uptime)}`);
68
- }
69
- if (status.pythonPath) {
70
- console.log(` Python: ${status.pythonPath}`);
71
- }
72
- if (status.venvPath) {
73
- console.log(` Venv: ${status.venvPath}`);
74
- }
75
- }
76
-
77
- function formatUptime(ms: number): string {
78
- const seconds = Math.floor(ms / 1000);
79
- const minutes = Math.floor(seconds / 60);
80
- const hours = Math.floor(minutes / 60);
81
-
82
- if (hours > 0) {
83
- return `${hours}h ${minutes % 60}m`;
84
- }
85
- if (minutes > 0) {
86
- return `${minutes}m ${seconds % 60}s`;
87
- }
88
- return `${seconds}s`;
89
- }
90
-
91
- export function printJupyterHelp(): void {
92
- console.log(`${chalk.bold(`${APP_NAME} jupyter`)} - Manage the shared Jupyter gateway
93
-
94
- ${chalk.bold("Usage:")}
95
- ${APP_NAME} jupyter <command>
96
-
97
- ${chalk.bold("Commands:")}
98
- status Show gateway status (default)
99
- kill Stop the running gateway
100
-
101
- ${chalk.bold("Examples:")}
102
- ${APP_NAME} jupyter # Show status
103
- ${APP_NAME} jupyter status # Show status
104
- ${APP_NAME} jupyter kill # Stop the gateway
105
- `);
106
- }
@@ -1,32 +0,0 @@
1
- /**
2
- * Manage the shared Jupyter gateway.
3
- */
4
- import { Args, Command } from "@oh-my-pi/pi-utils/cli";
5
- import { type JupyterAction, type JupyterCommandArgs, runJupyterCommand } from "../cli/jupyter-cli";
6
- import { initTheme } from "../modes/theme/theme";
7
-
8
- const ACTIONS: JupyterAction[] = ["kill", "status"];
9
-
10
- export default class Jupyter extends Command {
11
- static description = "Manage the shared Jupyter gateway";
12
-
13
- static args = {
14
- action: Args.string({
15
- description: "Jupyter action",
16
- required: false,
17
- options: ACTIONS,
18
- }),
19
- };
20
-
21
- async run(): Promise<void> {
22
- const { args } = await this.parse(Jupyter);
23
- const action = (args.action ?? "status") as JupyterAction;
24
-
25
- const cmd: JupyterCommandArgs = {
26
- action,
27
- };
28
-
29
- await initTheme();
30
- await runJupyterCommand(cmd);
31
- }
32
- }
@@ -1,28 +0,0 @@
1
- export function getAbortReason(signal: AbortSignal | undefined, fallbackReason: string): Error {
2
- if (signal?.reason instanceof Error) return signal.reason;
3
- if (typeof signal?.reason === "string" && signal.reason.length > 0) {
4
- return new Error(signal.reason);
5
- }
6
-
7
- return new Error(fallbackReason);
8
- }
9
-
10
- export function createCancellationError(name: "AbortError" | "TimeoutError", message: string): Error {
11
- const error = new Error(message);
12
- error.name = name;
13
- return error;
14
- }
15
-
16
- export function getExecutionCancellationError(
17
- result: { timedOut?: boolean },
18
- signal: AbortSignal | undefined,
19
- fallbackReason: string,
20
- ): Error {
21
- if (signal?.aborted) {
22
- return getAbortReason(signal, fallbackReason);
23
- }
24
- if (result.timedOut) {
25
- return createCancellationError("TimeoutError", fallbackReason);
26
- }
27
- return createCancellationError("AbortError", fallbackReason);
28
- }