@oh-my-pi/pi-coding-agent 4.4.5 → 4.4.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [4.4.8] - 2026-01-12
6
+ ### Changed
7
+
8
+ - Changed review finding priority format from numeric (0-3) to string labels (P0-P3) for clearer severity indication
9
+ - Replaced Type.Union with Type.Literal patterns with StringEnum helper across tool schemas for cleaner enum definitions
10
+
11
+ ## [4.4.6] - 2026-01-11
12
+
5
13
  ## [4.4.5] - 2026-01-11
6
14
 
7
15
  ### Changed
@@ -16,6 +16,9 @@ export default function (pi: ExtensionAPI) {
16
16
  pi.logger.debug("API demo extension loaded");
17
17
 
18
18
  // 3. Register a tool that uses all three API features
19
+ // Import StringEnum from typebox helpers
20
+ const { StringEnum } = pi.pi;
21
+
19
22
  pi.registerTool({
20
23
  name: "api_demo",
21
24
  label: "API Demo",
@@ -23,7 +26,7 @@ export default function (pi: ExtensionAPI) {
23
26
  parameters: Type.Object({
24
27
  message: Type.String({ description: "Test message" }),
25
28
  logLevel: Type.Optional(
26
- Type.Union([Type.Literal("error"), Type.Literal("warn"), Type.Literal("debug")], {
29
+ StringEnum(["error", "warn", "debug"], {
27
30
  description: "Log level to use",
28
31
  default: "debug",
29
32
  }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-coding-agent",
3
- "version": "4.4.5",
3
+ "version": "4.4.8",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "ompConfig": {
@@ -39,10 +39,10 @@
39
39
  "prepublishOnly": "bun run generate-template && bun run clean && bun run build"
40
40
  },
41
41
  "dependencies": {
42
- "@oh-my-pi/pi-ai": "4.4.5",
43
- "@oh-my-pi/pi-agent-core": "4.4.5",
44
- "@oh-my-pi/pi-git-tool": "4.4.5",
45
- "@oh-my-pi/pi-tui": "4.4.5",
42
+ "@oh-my-pi/pi-ai": "4.4.8",
43
+ "@oh-my-pi/pi-agent-core": "4.4.8",
44
+ "@oh-my-pi/pi-git-tool": "4.4.8",
45
+ "@oh-my-pi/pi-tui": "4.4.8",
46
46
  "@openai/agents": "^0.3.7",
47
47
  "@sinclair/typebox": "^0.34.46",
48
48
  "ajv": "^8.17.1",
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
8
+ import { StringEnum } from "@oh-my-pi/pi-ai";
8
9
  import { Type } from "@sinclair/typebox";
9
10
  import Ajv, { type ErrorObject, type ValidateFunction } from "ajv";
10
11
  import type { ToolSession } from "./index";
@@ -81,7 +82,7 @@ export function createCompleteTool(session: ToolSession) {
81
82
  const completeParams = Type.Object({
82
83
  data: Type.Optional(dataSchema),
83
84
  status: Type.Optional(
84
- Type.Union([Type.Literal("success"), Type.Literal("aborted")], {
85
+ StringEnum(["success", "aborted"], {
85
86
  default: "success",
86
87
  description: "Use 'aborted' if the task cannot be completed",
87
88
  }),
@@ -4,6 +4,7 @@
4
4
  * Basic neural/keyword search, deep research, code search, and URL crawling.
5
5
  */
6
6
 
7
+ import { StringEnum } from "@oh-my-pi/pi-ai";
7
8
  import { Type } from "@sinclair/typebox";
8
9
  import type { CustomTool } from "../../custom-tools/types";
9
10
  import type { ExaRenderDetails } from "./types";
@@ -31,7 +32,7 @@ Parameters:
31
32
  parameters: Type.Object({
32
33
  query: Type.String({ description: "Search query" }),
33
34
  type: Type.Optional(
34
- Type.Union([Type.Literal("keyword"), Type.Literal("neural"), Type.Literal("auto")], {
35
+ StringEnum(["keyword", "neural", "auto"], {
35
36
  description: "Search type - neural (semantic), keyword (exact), or auto",
36
37
  }),
37
38
  ),
@@ -128,7 +129,7 @@ Similar parameters to exa_search, optimized for research depth.`,
128
129
  parameters: Type.Object({
129
130
  query: Type.String({ description: "Research query" }),
130
131
  type: Type.Optional(
131
- Type.Union([Type.Literal("keyword"), Type.Literal("neural"), Type.Literal("auto")], {
132
+ StringEnum(["keyword", "neural", "auto"], {
132
133
  description: "Search type - neural (semantic), keyword (exact), or auto",
133
134
  }),
134
135
  ),
@@ -1,5 +1,6 @@
1
1
  import path from "node:path";
2
2
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
3
+ import { StringEnum } from "@oh-my-pi/pi-ai";
3
4
  import type { Component } from "@oh-my-pi/pi-tui";
4
5
  import { Text } from "@oh-my-pi/pi-tui";
5
6
  import { Type } from "@sinclair/typebox";
@@ -25,7 +26,7 @@ const findSchema = Type.Object({
25
26
  Type.Boolean({ description: "Sort results by modification time, most recent first (default: false)" }),
26
27
  ),
27
28
  type: Type.Optional(
28
- Type.Union([Type.Literal("file"), Type.Literal("dir"), Type.Literal("all")], {
29
+ StringEnum(["file", "dir", "all"], {
29
30
  description:
30
31
  "Filter by type: 'file' for files only, 'dir' for directories only, 'all' for both (default: 'all')",
31
32
  }),
@@ -1,5 +1,6 @@
1
1
  import { tmpdir } from "node:os";
2
2
  import { join } from "node:path";
3
+ import { StringEnum } from "@oh-my-pi/pi-ai";
3
4
  import { type Static, Type } from "@sinclair/typebox";
4
5
  import { nanoid } from "nanoid";
5
6
  import geminiImageDescription from "../../prompts/tools/gemini-image.md" with { type: "text" };
@@ -21,12 +22,11 @@ interface ImageApiKey {
21
22
  apiKey: string;
22
23
  }
23
24
 
24
- const responseModalitySchema = Type.Union([Type.Literal("Image"), Type.Literal("Text")]);
25
- const aspectRatioSchema = Type.Union(
26
- [Type.Literal("1:1"), Type.Literal("3:4"), Type.Literal("4:3"), Type.Literal("9:16"), Type.Literal("16:9")],
27
- { description: "Aspect ratio (1:1, 3:4, 4:3, 9:16, 16:9)." },
28
- );
29
- const imageSizeSchema = Type.Union([Type.Literal("1024x1024"), Type.Literal("1536x1024"), Type.Literal("1024x1536")], {
25
+ const responseModalitySchema = StringEnum(["Image", "Text"]);
26
+ const aspectRatioSchema = StringEnum(["1:1", "3:4", "4:3", "9:16", "16:9"], {
27
+ description: "Aspect ratio (1:1, 3:4, 4:3, 9:16, 16:9).",
28
+ });
29
+ const imageSizeSchema = StringEnum(["1024x1024", "1536x1024", "1024x1536"], {
30
30
  description: "Image size, mainly for gemini-3-pro-image-preview.",
31
31
  });
32
32
 
@@ -1,4 +1,5 @@
1
1
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
2
+ import { StringEnum } from "@oh-my-pi/pi-ai";
2
3
  import { type GitParams, gitTool as gitToolCore, type ToolResponse } from "@oh-my-pi/pi-git-tool";
3
4
  import { type Static, Type } from "@sinclair/typebox";
4
5
  import gitDescription from "../../prompts/tools/git.md" with { type: "text" };
@@ -6,50 +7,38 @@ import { renderPromptTemplate } from "../prompt-templates";
6
7
  import type { ToolSession } from "./index";
7
8
 
8
9
  const gitSchema = Type.Object({
9
- operation: Type.Union([
10
- Type.Literal("status"),
11
- Type.Literal("diff"),
12
- Type.Literal("log"),
13
- Type.Literal("show"),
14
- Type.Literal("blame"),
15
- Type.Literal("branch"),
16
- Type.Literal("add"),
17
- Type.Literal("restore"),
18
- Type.Literal("commit"),
19
- Type.Literal("checkout"),
20
- Type.Literal("merge"),
21
- Type.Literal("rebase"),
22
- Type.Literal("stash"),
23
- Type.Literal("cherry-pick"),
24
- Type.Literal("fetch"),
25
- Type.Literal("pull"),
26
- Type.Literal("push"),
27
- Type.Literal("tag"),
28
- Type.Literal("pr"),
29
- Type.Literal("issue"),
30
- Type.Literal("ci"),
31
- Type.Literal("release"),
10
+ operation: StringEnum([
11
+ "status",
12
+ "diff",
13
+ "log",
14
+ "show",
15
+ "blame",
16
+ "branch",
17
+ "add",
18
+ "restore",
19
+ "commit",
20
+ "checkout",
21
+ "merge",
22
+ "rebase",
23
+ "stash",
24
+ "cherry-pick",
25
+ "fetch",
26
+ "pull",
27
+ "push",
28
+ "tag",
29
+ "pr",
30
+ "issue",
31
+ "ci",
32
+ "release",
32
33
  ]),
33
34
 
34
35
  // Status
35
- only: Type.Optional(
36
- Type.Union([
37
- Type.Literal("branch"),
38
- Type.Literal("modified"),
39
- Type.Literal("staged"),
40
- Type.Literal("untracked"),
41
- Type.Literal("conflicts"),
42
- Type.Literal("sync"),
43
- ]),
44
- ),
36
+ only: Type.Optional(StringEnum(["branch", "modified", "staged", "untracked", "conflicts", "sync"])),
45
37
  ignored: Type.Optional(Type.Boolean()),
46
38
 
47
39
  // Diff
48
40
  target: Type.Optional(
49
41
  Type.Union([
50
- Type.Literal("unstaged"),
51
- Type.Literal("staged"),
52
- Type.Literal("head"),
53
42
  Type.Object({
54
43
  from: Type.String(),
55
44
  to: Type.Optional(Type.String()),
@@ -71,7 +60,7 @@ const gitSchema = Type.Object({
71
60
  since: Type.Optional(Type.String()),
72
61
  until: Type.Optional(Type.String()),
73
62
  grep: Type.Optional(Type.String()),
74
- format: Type.Optional(Type.Union([Type.Literal("oneline"), Type.Literal("short"), Type.Literal("full")])),
63
+ format: Type.Optional(StringEnum(["oneline", "short", "full"])),
75
64
  stat: Type.Optional(Type.Boolean()),
76
65
  merges: Type.Optional(Type.Boolean()),
77
66
  first_parent: Type.Optional(Type.Boolean()),
@@ -90,15 +79,7 @@ const gitSchema = Type.Object({
90
79
  root: Type.Optional(Type.Boolean()),
91
80
 
92
81
  // Branch
93
- action: Type.Optional(
94
- Type.Union([
95
- Type.Literal("list"),
96
- Type.Literal("create"),
97
- Type.Literal("delete"),
98
- Type.Literal("rename"),
99
- Type.Literal("current"),
100
- ]),
101
- ),
82
+ action: Type.Optional(StringEnum(["list", "create", "delete", "rename", "current"])),
102
83
  name: Type.Optional(Type.String()),
103
84
  newName: Type.Optional(Type.String()),
104
85
  startPoint: Type.Optional(Type.String()),
@@ -165,13 +146,9 @@ const gitSchema = Type.Object({
165
146
  base: Type.Optional(Type.String()),
166
147
  head: Type.Optional(Type.String()),
167
148
  draft: Type.Optional(Type.Boolean()),
168
- state: Type.Optional(
169
- Type.Union([Type.Literal("open"), Type.Literal("closed"), Type.Literal("merged"), Type.Literal("all")]),
170
- ),
171
- merge_method: Type.Optional(Type.Union([Type.Literal("merge"), Type.Literal("squash"), Type.Literal("rebase")])),
172
- review_action: Type.Optional(
173
- Type.Union([Type.Literal("approve"), Type.Literal("request-changes"), Type.Literal("comment")]),
174
- ),
149
+ state: Type.Optional(StringEnum(["open", "closed", "merged", "all"])),
150
+ merge_method: Type.Optional(StringEnum(["merge", "squash", "rebase"])),
151
+ review_action: Type.Optional(StringEnum(["approve", "request-changes", "comment"])),
175
152
  review_body: Type.Optional(Type.String()),
176
153
 
177
154
  // Issue
@@ -1,5 +1,6 @@
1
1
  import nodePath from "node:path";
2
2
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
3
+ import { StringEnum } from "@oh-my-pi/pi-ai";
3
4
  import type { Component } from "@oh-my-pi/pi-tui";
4
5
  import { Text } from "@oh-my-pi/pi-tui";
5
6
  import { Type } from "@sinclair/typebox";
@@ -44,7 +45,7 @@ const grepSchema = Type.Object({
44
45
  ),
45
46
  limit: Type.Optional(Type.Number({ description: "Maximum number of matches to return (default: 100)" })),
46
47
  outputMode: Type.Optional(
47
- Type.Union([Type.Literal("content"), Type.Literal("files_with_matches"), Type.Literal("count")], {
48
+ StringEnum(["content", "files_with_matches", "count"], {
48
49
  description:
49
50
  "Output mode: 'content' shows matching lines, 'files_with_matches' shows only file paths, 'count' shows match counts per file (default: 'content')",
50
51
  }),
@@ -1,3 +1,4 @@
1
+ import { StringEnum } from "@oh-my-pi/pi-ai";
1
2
  import { type Static, Type } from "@sinclair/typebox";
2
3
  import type { Subprocess } from "bun";
3
4
 
@@ -6,28 +7,28 @@ import type { Subprocess } from "bun";
6
7
  // =============================================================================
7
8
 
8
9
  export const lspSchema = Type.Object({
9
- action: Type.Union(
10
+ action: StringEnum(
10
11
  [
11
12
  // Standard LSP operations
12
- Type.Literal("diagnostics"),
13
- Type.Literal("workspace_diagnostics"),
14
- Type.Literal("references"),
15
- Type.Literal("definition"),
16
- Type.Literal("hover"),
17
- Type.Literal("symbols"),
18
- Type.Literal("workspace_symbols"),
19
- Type.Literal("rename"),
20
- Type.Literal("actions"),
21
- Type.Literal("incoming_calls"),
22
- Type.Literal("outgoing_calls"),
23
- Type.Literal("status"),
13
+ "diagnostics",
14
+ "workspace_diagnostics",
15
+ "references",
16
+ "definition",
17
+ "hover",
18
+ "symbols",
19
+ "workspace_symbols",
20
+ "rename",
21
+ "actions",
22
+ "incoming_calls",
23
+ "outgoing_calls",
24
+ "status",
24
25
  // Rust-analyzer specific operations
25
- Type.Literal("flycheck"),
26
- Type.Literal("expand_macro"),
27
- Type.Literal("ssr"),
28
- Type.Literal("runnables"),
29
- Type.Literal("related_tests"),
30
- Type.Literal("reload_workspace"),
26
+ "flycheck",
27
+ "expand_macro",
28
+ "ssr",
29
+ "runnables",
30
+ "related_tests",
31
+ "reload_workspace",
31
32
  ],
32
33
  { description: "LSP action to perform" },
33
34
  ),
@@ -1,4 +1,5 @@
1
1
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
2
+ import { StringEnum } from "@oh-my-pi/pi-ai";
2
3
  import type { Component } from "@oh-my-pi/pi-tui";
3
4
  import { Text } from "@oh-my-pi/pi-tui";
4
5
  import { Type } from "@sinclair/typebox";
@@ -17,14 +18,14 @@ import {
17
18
  } from "./render-utils";
18
19
 
19
20
  const notebookSchema = Type.Object({
20
- action: Type.Union([Type.Literal("edit"), Type.Literal("insert"), Type.Literal("delete")], {
21
+ action: StringEnum(["edit", "insert", "delete"], {
21
22
  description: "Action to perform on the notebook cell",
22
23
  }),
23
24
  notebook_path: Type.String({ description: "Path to the .ipynb file (relative or absolute)" }),
24
25
  cell_index: Type.Number({ description: "0-based index of the cell to operate on" }),
25
26
  content: Type.Optional(Type.String({ description: "New cell content (required for edit/insert)" })),
26
27
  cell_type: Type.Optional(
27
- Type.Union([Type.Literal("code"), Type.Literal("markdown")], {
28
+ StringEnum(["code", "markdown"], {
28
29
  description: "Cell type for insert (default: code)",
29
30
  }),
30
31
  ),
@@ -7,7 +7,7 @@
7
7
  import * as fs from "node:fs";
8
8
  import * as path from "node:path";
9
9
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
10
- import type { TextContent } from "@oh-my-pi/pi-ai";
10
+ import { StringEnum, type TextContent } from "@oh-my-pi/pi-ai";
11
11
  import type { Component } from "@oh-my-pi/pi-tui";
12
12
  import { Text } from "@oh-my-pi/pi-tui";
13
13
  import { Type } from "@sinclair/typebox";
@@ -33,7 +33,7 @@ const outputSchema = Type.Object({
33
33
  minItems: 1,
34
34
  }),
35
35
  format: Type.Optional(
36
- Type.Union([Type.Literal("raw"), Type.Literal("json"), Type.Literal("stripped")], {
36
+ StringEnum(["raw", "json", "stripped"], {
37
37
  description: "Output format: raw (default), json (structured), stripped (no ANSI)",
38
38
  }),
39
39
  ),
@@ -12,24 +12,33 @@ import { Container, Text } from "@oh-my-pi/pi-tui";
12
12
  import { Type } from "@sinclair/typebox";
13
13
  import type { Theme, ThemeColor } from "../../modes/interactive/theme/theme";
14
14
 
15
- const PRIORITY_LABELS: Record<number, string> = {
16
- 0: "P0",
17
- 1: "P1",
18
- 2: "P2",
19
- 3: "P3",
15
+ export type FindingPriority = "P0" | "P1" | "P2" | "P3";
16
+
17
+ export interface FindingPriorityInfo {
18
+ ord: 0 | 1 | 2 | 3;
19
+ symbol: "status.error" | "status.warning" | "status.info";
20
+ color: ThemeColor;
21
+ }
22
+
23
+ const PRIORITY_INFO: Record<FindingPriority, FindingPriorityInfo> = {
24
+ P0: { ord: 0, symbol: "status.error", color: "error" },
25
+ P1: { ord: 1, symbol: "status.warning", color: "warning" },
26
+ P2: { ord: 2, symbol: "status.warning", color: "muted" },
27
+ P3: { ord: 3, symbol: "status.info", color: "accent" },
20
28
  };
21
29
 
22
- const PRIORITY_META: Record<number, { symbol: "status.error" | "status.warning" | "status.info"; color: ThemeColor }> =
23
- {
24
- 0: { symbol: "status.error", color: "error" },
25
- 1: { symbol: "status.warning", color: "warning" },
26
- 2: { symbol: "status.warning", color: "muted" },
27
- 3: { symbol: "status.info", color: "accent" },
28
- };
30
+ export const PRIORITY_LABELS: FindingPriority[] = ["P0", "P1", "P2", "P3"];
31
+
32
+ export function getPriorityInfo(priority: FindingPriority): FindingPriorityInfo {
33
+ return PRIORITY_INFO[priority] ?? { ord: 3, symbol: "status.info", color: "muted" };
34
+ }
29
35
 
30
- function getPriorityDisplay(priority: number, theme: Theme): { label: string; icon: string; color: ThemeColor } {
31
- const label = PRIORITY_LABELS[priority] ?? "P?";
32
- const meta = PRIORITY_META[priority] ?? { symbol: "status.info", color: "muted" as const };
36
+ function getPriorityDisplay(
37
+ priority: FindingPriority,
38
+ theme: Theme,
39
+ ): { label: string; icon: string; color: ThemeColor } {
40
+ const label = priority;
41
+ const meta = PRIORITY_INFO[priority] ?? { symbol: "status.info", color: "muted" as const };
33
42
  return {
34
43
  label,
35
44
  icon: theme.styledSymbol(meta.symbol, meta.color),
@@ -45,7 +54,7 @@ const ReportFindingParams = Type.Object({
45
54
  body: Type.String({
46
55
  description: "Markdown explaining why this is a problem. One paragraph max.",
47
56
  }),
48
- priority: Type.Union([Type.Literal(0), Type.Literal(1), Type.Literal(2), Type.Literal(3)], {
57
+ priority: StringEnum(["P0", "P1", "P2", "P3"], {
49
58
  description: "0=P0 (critical), 1=P1 (urgent), 2=P2 (normal), 3=P3 (low)",
50
59
  }),
51
60
  confidence: Type.Number({
@@ -61,7 +70,7 @@ const ReportFindingParams = Type.Object({
61
70
  interface ReportFindingDetails {
62
71
  title: string;
63
72
  body: string;
64
- priority: number;
73
+ priority: FindingPriority;
65
74
  confidence: number;
66
75
  file_path: string;
67
76
  line_start: number;
@@ -81,7 +90,7 @@ export const reportFindingTool: AgentTool<typeof ReportFindingParams, ReportFind
81
90
  content: [
82
91
  {
83
92
  type: "text",
84
- text: `Finding recorded: ${PRIORITY_LABELS[priority]} ${title}\nLocation: ${location}\nConfidence: ${(
93
+ text: `Finding recorded: ${priority} ${title}\nLocation: ${location}\nConfidence: ${(
85
94
  confidence * 100
86
95
  ).toFixed(0)}%`,
87
96
  },
@@ -91,7 +100,7 @@ export const reportFindingTool: AgentTool<typeof ReportFindingParams, ReportFind
91
100
  },
92
101
 
93
102
  renderCall(args, theme): Component {
94
- const { label, icon, color } = getPriorityDisplay(args.priority as number, theme);
103
+ const { label, icon, color } = getPriorityDisplay(args.priority, theme);
95
104
  const titleText = String(args.title).replace(/^\[P\d\]\s*/, "");
96
105
  return new Text(
97
106
  `${theme.fg("toolTitle", theme.bold("report_finding "))}${icon} ${theme.fg(color, `[${label}]`)} ${theme.fg(
@@ -141,6 +150,7 @@ export type { ReportFindingDetails };
141
150
  // ─────────────────────────────────────────────────────────────────────────────
142
151
 
143
152
  import path from "node:path";
153
+ import { StringEnum } from "@oh-my-pi/pi-ai";
144
154
  import { subprocessToolRegistry } from "./task/subprocess-tool-registry";
145
155
 
146
156
  // Register report_finding handler
@@ -130,7 +130,7 @@ describe("sanitizeSchemaForGoogle", () => {
130
130
  expect(sanitized).toEqual({ type: "string", enum: ["active", "inactive"] });
131
131
  });
132
132
 
133
- it("handles nested const in anyOf", () => {
133
+ it("collapses anyOf with const values into enum", () => {
134
134
  const schema = {
135
135
  anyOf: [
136
136
  { type: "string", const: "file" },
@@ -138,11 +138,10 @@ describe("sanitizeSchemaForGoogle", () => {
138
138
  ],
139
139
  };
140
140
  const sanitized = sanitizeSchemaForGoogle(schema);
141
+ // anyOf with all const values should collapse into a single enum
141
142
  expect(sanitized).toEqual({
142
- anyOf: [
143
- { type: "string", enum: ["file"] },
144
- { type: "string", enum: ["dir"] },
145
- ],
143
+ type: "string",
144
+ enum: ["file", "dir"],
146
145
  });
147
146
  });
148
147
 
@@ -18,18 +18,16 @@ import {
18
18
  formatTokens,
19
19
  truncate,
20
20
  } from "../render-utils";
21
- import type { ReportFindingDetails, SubmitReviewDetails } from "../review";
21
+ import {
22
+ type FindingPriority,
23
+ getPriorityInfo,
24
+ PRIORITY_LABELS,
25
+ type ReportFindingDetails,
26
+ type SubmitReviewDetails,
27
+ } from "../review";
22
28
  import { subprocessToolRegistry } from "./subprocess-tool-registry";
23
29
  import type { AgentProgress, SingleResult, TaskParams, TaskToolDetails } from "./types";
24
30
 
25
- /** Priority labels for review findings */
26
- const PRIORITY_LABELS: Record<number, string> = {
27
- 0: "P0",
28
- 1: "P1",
29
- 2: "P2",
30
- 3: "P3",
31
- };
32
-
33
31
  /**
34
32
  * Get status icon for agent state.
35
33
  * For running status, uses animated spinner if spinnerFrame is provided.
@@ -53,25 +51,17 @@ function getStatusIcon(status: AgentProgress["status"], theme: Theme, spinnerFra
53
51
  function formatFindingSummary(findings: ReportFindingDetails[], theme: Theme): string {
54
52
  if (findings.length === 0) return theme.fg("dim", "Findings: none");
55
53
 
56
- const counts = new Map<number, number>();
54
+ const counts: { [P in FindingPriority]?: number } = {};
57
55
  for (const finding of findings) {
58
- counts.set(finding.priority, (counts.get(finding.priority) ?? 0) + 1);
56
+ counts[finding.priority] = (counts[finding.priority] ?? 0) + 1;
59
57
  }
60
58
 
61
- const priorityMeta: Record<number, { icon: string; color: "error" | "warning" | "muted" | "accent" }> = {
62
- 0: { icon: theme.styledSymbol("status.error", "error"), color: "error" },
63
- 1: { icon: theme.styledSymbol("status.warning", "warning"), color: "warning" },
64
- 2: { icon: theme.styledSymbol("status.warning", "muted"), color: "muted" },
65
- 3: { icon: theme.styledSymbol("status.info", "accent"), color: "accent" },
66
- };
67
-
68
59
  const parts: string[] = [];
69
- for (const priority of [0, 1, 2, 3]) {
70
- const label = PRIORITY_LABELS[priority] ?? "P?";
71
- const meta = priorityMeta[priority] ?? { icon: "", color: "muted" as const };
72
- const count = counts.get(priority) ?? 0;
73
- const text = theme.fg(meta.color, `${label}:${count}`);
74
- parts.push(meta.icon ? `${meta.icon} ${text}` : text);
60
+ for (const label of PRIORITY_LABELS) {
61
+ const { symbol, color } = getPriorityInfo(label);
62
+ const count = counts[label] ?? 0;
63
+ const text = theme.fg(color, `${label}:${count}`);
64
+ parts.push(theme.styledSymbol(symbol, color) ? `${theme.styledSymbol(symbol, color)} ${text}` : text);
75
65
  }
76
66
 
77
67
  return `${theme.fg("dim", "Findings:")} ${parts.join(theme.sep.dot)}`;
@@ -467,7 +457,9 @@ function renderFindings(
467
457
  const lines: string[] = [];
468
458
 
469
459
  // Sort by priority (lower = more severe) when collapsed to show most important first
470
- const sortedFindings = expanded ? findings : [...findings].sort((a, b) => a.priority - b.priority);
460
+ const sortedFindings = expanded
461
+ ? findings
462
+ : [...findings].sort((a, b) => getPriorityInfo(a.priority).ord - getPriorityInfo(b.priority).ord);
471
463
  const displayCount = expanded ? sortedFindings.length : Math.min(3, sortedFindings.length);
472
464
 
473
465
  for (let i = 0; i < displayCount; i++) {
@@ -476,13 +468,12 @@ function renderFindings(
476
468
  const findingPrefix = isLastFinding ? theme.tree.last : theme.tree.branch;
477
469
  const findingContinue = isLastFinding ? " " : `${theme.tree.vertical} `;
478
470
 
479
- const priority = PRIORITY_LABELS[finding.priority] ?? "P?";
480
- const color = finding.priority === 0 ? "error" : finding.priority === 1 ? "warning" : "muted";
471
+ const { color } = getPriorityInfo(finding.priority);
481
472
  const titleText = finding.title.replace(/^\[P\d\]\s*/, "");
482
473
  const loc = `${path.basename(finding.file_path)}:${finding.line_start}`;
483
474
 
484
475
  lines.push(
485
- `${continuePrefix}${findingPrefix} ${theme.fg(color, `[${priority}]`)} ${titleText} ${theme.fg("dim", loc)}`,
476
+ `${continuePrefix}${findingPrefix} ${theme.fg(color, `[${finding.priority}]`)} ${titleText} ${theme.fg("dim", loc)}`,
486
477
  );
487
478
 
488
479
  // Show body when expanded
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { mkdirSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
5
+ import { StringEnum } from "@oh-my-pi/pi-ai";
5
6
  import type { Component } from "@oh-my-pi/pi-tui";
6
7
  import { Text } from "@oh-my-pi/pi-tui";
7
8
  import { Type } from "@sinclair/typebox";
@@ -20,7 +21,7 @@ const todoWriteSchema = Type.Object({
20
21
  id: Type.Optional(Type.String({ description: "Stable todo id" })),
21
22
  content: Type.String({ minLength: 1, description: "Imperative task description (e.g., 'Run tests')" }),
22
23
  activeForm: Type.String({ minLength: 1, description: "Present continuous form (e.g., 'Running tests')" }),
23
- status: Type.Union([Type.Literal("pending"), Type.Literal("in_progress"), Type.Literal("completed")]),
24
+ status: StringEnum(["pending", "in_progress", "completed"]),
24
25
  }),
25
26
  { description: "The updated todo list" },
26
27
  ),
@@ -13,6 +13,7 @@
13
13
  */
14
14
 
15
15
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
16
+ import { StringEnum } from "@oh-my-pi/pi-ai";
16
17
  import { Type } from "@sinclair/typebox";
17
18
  import type { Theme } from "../../../modes/interactive/theme/theme";
18
19
  import webSearchDescription from "../../../prompts/tools/web-search.md" with { type: "text" };
@@ -36,7 +37,7 @@ export const webSearchSchema = Type.Object({
36
37
  // Common
37
38
  query: Type.String({ description: "Search query" }),
38
39
  provider: Type.Optional(
39
- Type.Union([Type.Literal("auto"), Type.Literal("exa"), Type.Literal("anthropic"), Type.Literal("perplexity")], {
40
+ StringEnum(["auto", "exa", "anthropic", "perplexity"], {
40
41
  description: "Search provider (auto-detected if omitted or set to auto)",
41
42
  }),
42
43
  ),
@@ -58,12 +59,12 @@ export const webSearchSchema = Type.Object({
58
59
 
59
60
  // Perplexity-specific
60
61
  model: Type.Optional(
61
- Type.Union([Type.Literal("sonar"), Type.Literal("sonar-pro")], {
62
+ StringEnum(["sonar", "sonar-pro"], {
62
63
  description: "Perplexity model - sonar (fast) or sonar-pro (comprehensive research)",
63
64
  }),
64
65
  ),
65
66
  search_recency_filter: Type.Optional(
66
- Type.Union([Type.Literal("day"), Type.Literal("week"), Type.Literal("month"), Type.Literal("year")], {
67
+ StringEnum(["day", "week", "month", "year"], {
67
68
  description: "Filter results by recency (Perplexity only)",
68
69
  }),
69
70
  ),
@@ -73,7 +74,7 @@ export const webSearchSchema = Type.Object({
73
74
  }),
74
75
  ),
75
76
  search_context_size: Type.Optional(
76
- Type.Union([Type.Literal("low"), Type.Literal("medium"), Type.Literal("high")], {
77
+ StringEnum(["low", "medium", "high"], {
77
78
  description: "Context size for cost control (Perplexity only)",
78
79
  }),
79
80
  ),
@@ -379,7 +380,7 @@ export function createWebSearchTool(_session: ToolSession): AgentTool<typeof web
379
380
  const webSearchDeepSchema = Type.Object({
380
381
  query: Type.String({ description: "Research query" }),
381
382
  type: Type.Optional(
382
- Type.Union([Type.Literal("keyword"), Type.Literal("neural"), Type.Literal("auto")], {
383
+ StringEnum(["keyword", "neural", "auto"], {
383
384
  description: "Search type - neural (semantic), keyword (exact), or auto",
384
385
  }),
385
386
  ),
package/src/index.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  // Core session management
2
2
 
3
+ // TypeBox helper for string enums (convenience for custom tools)
4
+ // Re-export from pi-ai which uses the correct enum-based schema format
5
+ export { StringEnum } from "@oh-my-pi/pi-ai";
3
6
  // Re-export TUI components for custom tool rendering
4
7
  export { Container, Markdown, Spacer, Text } from "@oh-my-pi/pi-tui";
5
8
  export {
@@ -264,15 +267,3 @@ export {
264
267
  Theme,
265
268
  type ThemeColor,
266
269
  } from "./modes/interactive/theme/theme";
267
-
268
- // TypeBox helper for string enums (convenience for custom tools)
269
- import { type TSchema, Type } from "@sinclair/typebox";
270
- export function StringEnum<T extends readonly string[]>(
271
- values: T,
272
- options?: { description?: string; default?: T[number] },
273
- ): TSchema {
274
- return Type.Union(
275
- values.map((v) => Type.Literal(v)),
276
- options,
277
- );
278
- }
@@ -824,6 +824,7 @@ const ColorValueSchema = Type.Union([
824
824
 
825
825
  type ColorValue = Static<typeof ColorValueSchema>;
826
826
 
827
+ // Use Type.Union here (not StringEnum) because TypeCompiler doesn't support Type.Unsafe
827
828
  const SymbolPresetSchema = Type.Union([Type.Literal("unicode"), Type.Literal("nerd"), Type.Literal("ascii")]);
828
829
 
829
830
  const SymbolsSchema = Type.Optional(