@oh-my-pi/pi-coding-agent 8.0.16 → 8.1.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.
Files changed (166) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/package.json +14 -11
  3. package/scripts/generate-wasm-b64.ts +24 -0
  4. package/src/capability/context-file.ts +1 -1
  5. package/src/capability/extension-module.ts +1 -1
  6. package/src/capability/extension.ts +1 -1
  7. package/src/capability/hook.ts +1 -1
  8. package/src/capability/instruction.ts +1 -1
  9. package/src/capability/mcp.ts +1 -1
  10. package/src/capability/prompt.ts +1 -1
  11. package/src/capability/rule.ts +1 -1
  12. package/src/capability/settings.ts +1 -1
  13. package/src/capability/skill.ts +1 -1
  14. package/src/capability/slash-command.ts +1 -1
  15. package/src/capability/ssh.ts +1 -1
  16. package/src/capability/system-prompt.ts +1 -1
  17. package/src/capability/tool.ts +1 -1
  18. package/src/cli/args.ts +1 -1
  19. package/src/cli/plugin-cli.ts +1 -5
  20. package/src/commit/agentic/agent.ts +309 -0
  21. package/src/commit/agentic/fallback.ts +96 -0
  22. package/src/commit/agentic/index.ts +359 -0
  23. package/src/commit/agentic/prompts/analyze-file.md +22 -0
  24. package/src/commit/agentic/prompts/session-user.md +26 -0
  25. package/src/commit/agentic/prompts/split-confirm.md +1 -0
  26. package/src/commit/agentic/prompts/system.md +40 -0
  27. package/src/commit/agentic/state.ts +74 -0
  28. package/src/commit/agentic/tools/analyze-file.ts +131 -0
  29. package/src/commit/agentic/tools/git-file-diff.ts +194 -0
  30. package/src/commit/agentic/tools/git-hunk.ts +50 -0
  31. package/src/commit/agentic/tools/git-overview.ts +84 -0
  32. package/src/commit/agentic/tools/index.ts +56 -0
  33. package/src/commit/agentic/tools/propose-changelog.ts +128 -0
  34. package/src/commit/agentic/tools/propose-commit.ts +154 -0
  35. package/src/commit/agentic/tools/recent-commits.ts +81 -0
  36. package/src/commit/agentic/tools/split-commit.ts +284 -0
  37. package/src/commit/agentic/topo-sort.ts +44 -0
  38. package/src/commit/agentic/trivial.ts +51 -0
  39. package/src/commit/agentic/validation.ts +200 -0
  40. package/src/commit/analysis/conventional.ts +169 -0
  41. package/src/commit/analysis/index.ts +4 -0
  42. package/src/commit/analysis/scope.ts +242 -0
  43. package/src/commit/analysis/summary.ts +114 -0
  44. package/src/commit/analysis/validation.ts +66 -0
  45. package/src/commit/changelog/detect.ts +36 -0
  46. package/src/commit/changelog/generate.ts +112 -0
  47. package/src/commit/changelog/index.ts +233 -0
  48. package/src/commit/changelog/parse.ts +44 -0
  49. package/src/commit/cli.ts +93 -0
  50. package/src/commit/git/diff.ts +148 -0
  51. package/src/commit/git/errors.ts +11 -0
  52. package/src/commit/git/index.ts +217 -0
  53. package/src/commit/git/operations.ts +53 -0
  54. package/src/commit/index.ts +5 -0
  55. package/src/commit/map-reduce/.map-phase.ts.kate-swp +0 -0
  56. package/src/commit/map-reduce/index.ts +63 -0
  57. package/src/commit/map-reduce/map-phase.ts +193 -0
  58. package/src/commit/map-reduce/reduce-phase.ts +147 -0
  59. package/src/commit/map-reduce/utils.ts +9 -0
  60. package/src/commit/message.ts +11 -0
  61. package/src/commit/model-selection.ts +84 -0
  62. package/src/commit/pipeline.ts +242 -0
  63. package/src/commit/prompts/analysis-system.md +155 -0
  64. package/src/commit/prompts/analysis-user.md +41 -0
  65. package/src/commit/prompts/changelog-system.md +56 -0
  66. package/src/commit/prompts/changelog-user.md +19 -0
  67. package/src/commit/prompts/file-observer-system.md +26 -0
  68. package/src/commit/prompts/file-observer-user.md +9 -0
  69. package/src/commit/prompts/reduce-system.md +60 -0
  70. package/src/commit/prompts/reduce-user.md +17 -0
  71. package/src/commit/prompts/summary-retry.md +4 -0
  72. package/src/commit/prompts/summary-system.md +52 -0
  73. package/src/commit/prompts/summary-user.md +13 -0
  74. package/src/commit/prompts/types-description.md +2 -0
  75. package/src/commit/types.ts +109 -0
  76. package/src/commit/utils/exclusions.ts +42 -0
  77. package/src/config/file-lock.ts +111 -0
  78. package/src/config/model-registry.ts +16 -7
  79. package/src/config/settings-manager.ts +115 -40
  80. package/src/config.ts +5 -5
  81. package/src/discovery/agents-md.ts +1 -1
  82. package/src/discovery/builtin.ts +1 -1
  83. package/src/discovery/claude.ts +1 -1
  84. package/src/discovery/cline.ts +1 -1
  85. package/src/discovery/codex.ts +1 -1
  86. package/src/discovery/cursor.ts +1 -1
  87. package/src/discovery/gemini.ts +1 -1
  88. package/src/discovery/github.ts +1 -1
  89. package/src/discovery/index.ts +11 -11
  90. package/src/discovery/mcp-json.ts +1 -1
  91. package/src/discovery/ssh.ts +1 -1
  92. package/src/discovery/vscode.ts +1 -1
  93. package/src/discovery/windsurf.ts +1 -1
  94. package/src/extensibility/custom-commands/loader.ts +1 -1
  95. package/src/extensibility/custom-commands/types.ts +1 -1
  96. package/src/extensibility/custom-tools/loader.ts +1 -1
  97. package/src/extensibility/custom-tools/types.ts +1 -1
  98. package/src/extensibility/extensions/loader.ts +1 -1
  99. package/src/extensibility/extensions/types.ts +1 -1
  100. package/src/extensibility/hooks/loader.ts +1 -1
  101. package/src/extensibility/hooks/types.ts +3 -3
  102. package/src/index.ts +10 -10
  103. package/src/ipy/executor.ts +97 -1
  104. package/src/lsp/index.ts +1 -1
  105. package/src/lsp/render.ts +90 -46
  106. package/src/main.ts +16 -3
  107. package/src/mcp/loader.ts +3 -3
  108. package/src/migrations.ts +3 -3
  109. package/src/modes/components/assistant-message.ts +29 -1
  110. package/src/modes/components/tool-execution.ts +5 -3
  111. package/src/modes/components/tree-selector.ts +1 -1
  112. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  113. package/src/modes/controllers/selector-controller.ts +1 -1
  114. package/src/modes/interactive-mode.ts +5 -3
  115. package/src/modes/rpc/rpc-client.ts +1 -1
  116. package/src/modes/rpc/rpc-mode.ts +1 -4
  117. package/src/modes/rpc/rpc-types.ts +1 -1
  118. package/src/modes/theme/mermaid-cache.ts +89 -0
  119. package/src/modes/theme/theme.ts +2 -0
  120. package/src/modes/types.ts +2 -2
  121. package/src/patch/index.ts +3 -9
  122. package/src/patch/shared.ts +33 -5
  123. package/src/prompts/tools/task.md +2 -0
  124. package/src/sdk.ts +60 -22
  125. package/src/session/agent-session.ts +3 -3
  126. package/src/session/agent-storage.ts +32 -28
  127. package/src/session/artifacts.ts +24 -1
  128. package/src/session/auth-storage.ts +25 -10
  129. package/src/session/storage-migration.ts +12 -53
  130. package/src/system-prompt.ts +2 -2
  131. package/src/task/.executor.ts.kate-swp +0 -0
  132. package/src/task/executor.ts +1 -1
  133. package/src/task/index.ts +10 -1
  134. package/src/task/output-manager.ts +94 -0
  135. package/src/task/render.ts +7 -12
  136. package/src/task/worker.ts +1 -1
  137. package/src/tools/ask.ts +35 -13
  138. package/src/tools/bash.ts +80 -87
  139. package/src/tools/calculator.ts +42 -40
  140. package/src/tools/complete.ts +1 -1
  141. package/src/tools/fetch.ts +67 -104
  142. package/src/tools/find.ts +83 -86
  143. package/src/tools/grep.ts +80 -96
  144. package/src/tools/index.ts +10 -7
  145. package/src/tools/ls.ts +39 -65
  146. package/src/tools/notebook.ts +48 -64
  147. package/src/tools/output-utils.ts +1 -1
  148. package/src/tools/python.ts +71 -183
  149. package/src/tools/read.ts +74 -15
  150. package/src/tools/render-utils.ts +1 -15
  151. package/src/tools/ssh.ts +43 -24
  152. package/src/tools/todo-write.ts +27 -15
  153. package/src/tools/write.ts +93 -64
  154. package/src/tui/code-cell.ts +115 -0
  155. package/src/tui/file-list.ts +48 -0
  156. package/src/tui/index.ts +11 -0
  157. package/src/tui/output-block.ts +73 -0
  158. package/src/tui/status-line.ts +40 -0
  159. package/src/tui/tree-list.ts +56 -0
  160. package/src/tui/types.ts +17 -0
  161. package/src/tui/utils.ts +49 -0
  162. package/src/vendor/photon/photon_rs_bg.wasm.b64.js +1 -0
  163. package/src/web/search/auth.ts +1 -1
  164. package/src/web/search/index.ts +1 -1
  165. package/src/web/search/render.ts +119 -163
  166. package/tsconfig.json +0 -42
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Render a code cell with optional output section.
3
+ */
4
+
5
+ import { highlightCode, type Theme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
6
+ import {
7
+ formatDuration,
8
+ formatExpandHint,
9
+ formatMoreItems,
10
+ replaceTabs,
11
+ } from "@oh-my-pi/pi-coding-agent/tools/render-utils";
12
+ import { renderOutputBlock } from "./output-block";
13
+ import type { State } from "./types";
14
+ import { getStateIcon } from "./utils";
15
+
16
+ export interface CodeCellOptions {
17
+ code: string;
18
+ language?: string;
19
+ index?: number;
20
+ total?: number;
21
+ title?: string;
22
+ status?: "pending" | "running" | "complete" | "error";
23
+ spinnerFrame?: number;
24
+ duration?: number;
25
+ output?: string;
26
+ outputMaxLines?: number;
27
+ codeMaxLines?: number;
28
+ expanded?: boolean;
29
+ width: number;
30
+ }
31
+
32
+ function getState(status?: CodeCellOptions["status"]): State | undefined {
33
+ if (!status) return undefined;
34
+ if (status === "complete") return "success";
35
+ if (status === "error") return "error";
36
+ if (status === "running") return "running";
37
+ return "pending";
38
+ }
39
+
40
+ function formatHeader(options: CodeCellOptions, theme: Theme): { title: string; meta?: string } {
41
+ const { index, total, title, status, spinnerFrame, duration } = options;
42
+ const parts: string[] = [];
43
+ if (index !== undefined && total !== undefined) {
44
+ parts.push(theme.fg("accent", `[${index + 1}/${total}]`));
45
+ }
46
+ if (title) {
47
+ parts.push(theme.fg("toolTitle", title));
48
+ }
49
+ const headerTitle = parts.length > 0 ? parts.join(" ") : theme.fg("toolTitle", "Code");
50
+
51
+ const metaParts: string[] = [];
52
+ if (duration !== undefined) {
53
+ metaParts.push(theme.fg("dim", `(${formatDuration(duration)})`));
54
+ }
55
+ if (status) {
56
+ const icon = getStateIcon(
57
+ status === "complete"
58
+ ? "success"
59
+ : status === "error"
60
+ ? "error"
61
+ : status === "running"
62
+ ? "running"
63
+ : "pending",
64
+ theme,
65
+ spinnerFrame,
66
+ );
67
+ if (status === "pending" || status === "running") {
68
+ metaParts.push(`${icon} ${theme.fg("muted", status)}`);
69
+ } else {
70
+ metaParts.push(icon);
71
+ }
72
+ }
73
+
74
+ if (metaParts.length === 0) return { title: headerTitle };
75
+ return { title: headerTitle, meta: metaParts.join(theme.fg("dim", theme.sep.dot)) };
76
+ }
77
+
78
+ export function renderCodeCell(options: CodeCellOptions, theme: Theme): string[] {
79
+ const { code, language, output, expanded = false, outputMaxLines = 6, codeMaxLines = 12, width } = options;
80
+ const { title, meta } = formatHeader(options, theme);
81
+ const state = getState(options.status);
82
+
83
+ const rawCodeLines = highlightCode(replaceTabs(code ?? ""), language);
84
+ const maxCodeLines = expanded ? rawCodeLines.length : Math.min(rawCodeLines.length, codeMaxLines);
85
+ const codeLines = rawCodeLines.slice(0, maxCodeLines);
86
+ const hiddenCodeLines = rawCodeLines.length - codeLines.length;
87
+ if (hiddenCodeLines > 0) {
88
+ const hint = formatExpandHint(theme, expanded, hiddenCodeLines > 0);
89
+ const moreLine = `${formatMoreItems(hiddenCodeLines, "line", theme)}${hint ? ` ${hint}` : ""}`;
90
+ codeLines.push(theme.fg("dim", moreLine));
91
+ }
92
+
93
+ const outputLines: string[] = [];
94
+ if (output?.trim()) {
95
+ const rawLines = output.split("\n");
96
+ const maxLines = expanded ? rawLines.length : Math.min(rawLines.length, outputMaxLines);
97
+ const displayLines = rawLines
98
+ .slice(0, maxLines)
99
+ .map((line) => (line.includes("\x1b[") ? line : theme.fg("toolOutput", line)));
100
+ outputLines.push(...displayLines);
101
+ const remaining = rawLines.length - maxLines;
102
+ if (remaining > 0) {
103
+ const hint = formatExpandHint(theme, expanded, remaining > 0);
104
+ const moreLine = `${formatMoreItems(remaining, "line", theme)}${hint ? ` ${hint}` : ""}`;
105
+ outputLines.push(theme.fg("dim", moreLine));
106
+ }
107
+ }
108
+
109
+ const sections: Array<{ label?: string; lines: string[] }> = [{ lines: codeLines }];
110
+ if (outputLines.length > 0) {
111
+ sections.push({ label: theme.fg("toolTitle", "Output"), lines: outputLines });
112
+ }
113
+
114
+ return renderOutputBlock({ header: title, headerMeta: meta, state, sections, width }, theme);
115
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Render file listings with optional icons and metadata.
3
+ */
4
+
5
+ import type { Theme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
6
+ import { getLanguageFromPath } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
7
+ import { renderTreeList } from "./tree-list";
8
+
9
+ export interface FileEntry {
10
+ path: string;
11
+ isDirectory?: boolean;
12
+ meta?: string;
13
+ }
14
+
15
+ export interface FileListOptions {
16
+ files: FileEntry[];
17
+ expanded?: boolean;
18
+ maxCollapsed?: number;
19
+ showIcons?: boolean;
20
+ }
21
+
22
+ export function renderFileList(options: FileListOptions, theme: Theme): string[] {
23
+ const { files, expanded = false, maxCollapsed = 8, showIcons = true } = options;
24
+
25
+ return renderTreeList(
26
+ {
27
+ items: files,
28
+ expanded,
29
+ maxCollapsed,
30
+ itemType: "file",
31
+ renderItem: (entry) => {
32
+ const isDirectory = entry.isDirectory ?? entry.path.endsWith("/");
33
+ const displayPath = isDirectory && entry.path.endsWith("/") ? entry.path : entry.path;
34
+ const lang = isDirectory ? undefined : getLanguageFromPath(displayPath);
35
+ const icon = !showIcons
36
+ ? ""
37
+ : isDirectory
38
+ ? theme.fg("accent", theme.icon.folder)
39
+ : theme.fg("muted", theme.getLangIcon(lang));
40
+ const labelColor = isDirectory ? "accent" : "toolOutput";
41
+ const meta = entry.meta ? ` ${theme.fg("dim", entry.meta)}` : "";
42
+ const iconPrefix = icon ? `${icon} ` : "";
43
+ return `${iconPrefix}${theme.fg(labelColor, displayPath)}${meta}`;
44
+ },
45
+ },
46
+ theme,
47
+ );
48
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Public exports for shared TUI components.
3
+ */
4
+
5
+ export * from "./code-cell";
6
+ export * from "./file-list";
7
+ export * from "./output-block";
8
+ export * from "./status-line";
9
+ export * from "./tree-list";
10
+ export * from "./types";
11
+ export * from "./utils";
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Bordered output container with optional header and sections.
3
+ */
4
+
5
+ import type { Theme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
6
+ import { visibleWidth } from "@oh-my-pi/pi-tui";
7
+ import type { State } from "./types";
8
+ import { getStateBgColor, padToWidth, truncateToWidth } from "./utils";
9
+
10
+ export interface OutputBlockOptions {
11
+ header?: string;
12
+ headerMeta?: string;
13
+ state?: State;
14
+ sections?: Array<{ label?: string; lines: string[] }>;
15
+ width: number;
16
+ }
17
+
18
+ export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): string[] {
19
+ const { header, headerMeta, state, sections = [], width } = options;
20
+ const h = theme.boxSharp.horizontal;
21
+ const v = theme.boxSharp.vertical;
22
+ const cap = h.repeat(3);
23
+ const lineWidth = Math.max(0, width);
24
+ // Border colors: running/pending use accent, success uses dim (gray), error/warning keep their colors
25
+ const borderColor: "error" | "warning" | "accent" | "dim" =
26
+ state === "error"
27
+ ? "error"
28
+ : state === "warning"
29
+ ? "warning"
30
+ : state === "running" || state === "pending"
31
+ ? "accent"
32
+ : "dim";
33
+ const border = (text: string) => theme.fg(borderColor, text);
34
+ const bgFn = state ? (text: string) => theme.bg(getStateBgColor(state), text) : undefined;
35
+
36
+ const buildBarLine = (leftChar: string, label?: string, meta?: string): string => {
37
+ const left = border(`${leftChar}${cap}`);
38
+ if (lineWidth <= 0) return left;
39
+ const labelText = [label, meta].filter(Boolean).join(theme.sep.dot);
40
+ const rawLabel = labelText ? ` ${labelText} ` : " ";
41
+ const maxLabelWidth = Math.max(0, lineWidth - visibleWidth(left));
42
+ const trimmedLabel = truncateToWidth(rawLabel, maxLabelWidth, theme.format.ellipsis);
43
+ const fillCount = Math.max(0, lineWidth - visibleWidth(left + trimmedLabel));
44
+ return `${left}${trimmedLabel}${border(h.repeat(fillCount))}`;
45
+ };
46
+
47
+ const contentPrefix = border(`${v} `);
48
+ const contentWidth = Math.max(0, lineWidth - visibleWidth(contentPrefix));
49
+ const lines: string[] = [];
50
+
51
+ lines.push(padToWidth(buildBarLine(theme.boxSharp.topLeft, header, headerMeta), lineWidth, bgFn));
52
+
53
+ const hasSections = sections.length > 0;
54
+ const normalizedSections = hasSections ? sections : [{ lines: [] }];
55
+
56
+ for (let i = 0; i < normalizedSections.length; i++) {
57
+ const section = normalizedSections[i];
58
+ if (section.label) {
59
+ lines.push(padToWidth(buildBarLine(theme.boxSharp.teeRight, section.label), lineWidth, bgFn));
60
+ }
61
+ for (const line of section.lines) {
62
+ const text = truncateToWidth(line, contentWidth, theme.format.ellipsis);
63
+ lines.push(padToWidth(`${contentPrefix}${text}`, lineWidth, bgFn));
64
+ }
65
+ }
66
+
67
+ const bottomLeft = border(`${theme.boxSharp.bottomLeft}${cap}`);
68
+ const bottomFillCount = Math.max(0, lineWidth - visibleWidth(bottomLeft));
69
+ const bottomLine = `${bottomLeft}${border(h.repeat(bottomFillCount))}`;
70
+ lines.push(padToWidth(bottomLine, lineWidth, bgFn));
71
+
72
+ return lines;
73
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Standardized status header rendering for tool output.
3
+ */
4
+
5
+ import type { Theme, ThemeColor } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
6
+ import type { IconType } from "./types";
7
+ import { getStateIcon } from "./utils";
8
+
9
+ export interface StatusLineOptions {
10
+ icon?: IconType;
11
+ spinnerFrame?: number;
12
+ title: string;
13
+ titleColor?: ThemeColor;
14
+ description?: string;
15
+ badge?: { label: string; color: ThemeColor };
16
+ meta?: string[];
17
+ }
18
+
19
+ export function renderStatusLine(options: StatusLineOptions, theme: Theme): string {
20
+ const icon = options.icon ? getStateIcon(options.icon, theme, options.spinnerFrame) : "";
21
+ const titleColor = options.titleColor ?? "accent";
22
+ const title = theme.fg(titleColor, options.title);
23
+ let line = icon ? `${icon} ${title}` : title;
24
+
25
+ if (options.description) {
26
+ line += `: ${theme.fg("muted", options.description)}`;
27
+ }
28
+
29
+ if (options.badge) {
30
+ const { label, color } = options.badge;
31
+ line += ` ${theme.fg(color, `${theme.format.bracketLeft}${label}${theme.format.bracketRight}`)}`;
32
+ }
33
+
34
+ const meta = options.meta?.filter((value) => value.trim().length > 0) ?? [];
35
+ if (meta.length > 0) {
36
+ line += ` ${theme.fg("dim", meta.join(theme.sep.dot))}`;
37
+ }
38
+
39
+ return line;
40
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Hierarchical tree list rendering helper.
3
+ */
4
+
5
+ import type { Theme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
6
+ import { formatMoreItems } from "@oh-my-pi/pi-coding-agent/tools/render-utils";
7
+ import type { TreeContext } from "./types";
8
+ import { getTreeBranch, getTreeContinuePrefix } from "./utils";
9
+
10
+ export interface TreeListOptions<T> {
11
+ items: T[];
12
+ expanded?: boolean;
13
+ maxCollapsed?: number;
14
+ itemType?: string;
15
+ renderItem: (item: T, context: TreeContext) => string | string[];
16
+ }
17
+
18
+ export function renderTreeList<T>(options: TreeListOptions<T>, theme: Theme): string[] {
19
+ const { items, expanded = false, maxCollapsed = 8, itemType = "item", renderItem } = options;
20
+ const lines: string[] = [];
21
+ const maxItems = expanded ? items.length : Math.min(items.length, maxCollapsed);
22
+
23
+ for (let i = 0; i < maxItems; i++) {
24
+ const isLast = i === maxItems - 1 && (expanded || items.length <= maxCollapsed);
25
+ const branch = getTreeBranch(isLast, theme);
26
+ const prefix = `${theme.fg("dim", branch)} `;
27
+ const continuePrefix = `${theme.fg("dim", getTreeContinuePrefix(isLast, theme))}`;
28
+ const context: TreeContext = {
29
+ index: i,
30
+ isLast,
31
+ depth: 0,
32
+ theme,
33
+ prefix,
34
+ continuePrefix,
35
+ };
36
+ const rendered = renderItem(items[i], context);
37
+ if (Array.isArray(rendered)) {
38
+ if (rendered.length === 0) continue;
39
+ lines.push(`${prefix}${rendered[0]}`);
40
+ for (let j = 1; j < rendered.length; j++) {
41
+ lines.push(`${continuePrefix}${rendered[j]}`);
42
+ }
43
+ } else {
44
+ lines.push(`${prefix}${rendered}`);
45
+ }
46
+ }
47
+
48
+ if (!expanded && items.length > maxItems) {
49
+ const remaining = items.length - maxItems;
50
+ lines.push(
51
+ `${theme.fg("dim", theme.tree.last)} ${theme.fg("muted", formatMoreItems(remaining, itemType, theme))}`,
52
+ );
53
+ }
54
+
55
+ return lines;
56
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Shared types for TUI rendering components.
3
+ */
4
+
5
+ import type { Theme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
6
+
7
+ export type State = "pending" | "running" | "success" | "error" | "warning";
8
+ export type IconType = "success" | "error" | "running" | "pending" | "warning" | "info";
9
+
10
+ export interface TreeContext {
11
+ index: number;
12
+ isLast: boolean;
13
+ depth: number;
14
+ theme: Theme;
15
+ prefix: string;
16
+ continuePrefix: string;
17
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Shared helpers for tool-rendered UI components.
3
+ */
4
+
5
+ import type { Theme, ThemeBg } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
6
+ import { truncateToWidth as truncateToWidthBase, visibleWidth } from "@oh-my-pi/pi-tui";
7
+ import type { IconType, State } from "./types";
8
+
9
+ export function buildTreePrefix(ancestors: boolean[], theme: Theme): string {
10
+ return ancestors.map((hasNext) => (hasNext ? `${theme.tree.vertical} ` : " ")).join("");
11
+ }
12
+
13
+ export function getTreeBranch(isLast: boolean, theme: Theme): string {
14
+ return isLast ? theme.tree.last : theme.tree.branch;
15
+ }
16
+
17
+ export function getTreeContinuePrefix(isLast: boolean, theme: Theme): string {
18
+ return isLast ? " " : `${theme.tree.vertical} `;
19
+ }
20
+
21
+ export function truncateToWidth(text: string, width: number, ellipsis: string): string {
22
+ return truncateToWidthBase(text, width, ellipsis);
23
+ }
24
+
25
+ export function padToWidth(text: string, width: number, bgFn?: (s: string) => string): string {
26
+ if (width <= 0) return bgFn ? bgFn(text) : text;
27
+ const paddingNeeded = Math.max(0, width - visibleWidth(text));
28
+ const padded = paddingNeeded > 0 ? text + " ".repeat(paddingNeeded) : text;
29
+ return bgFn ? bgFn(padded) : padded;
30
+ }
31
+
32
+ export function getStateBgColor(state: State): ThemeBg {
33
+ if (state === "success") return "toolSuccessBg";
34
+ if (state === "error") return "toolErrorBg";
35
+ return "toolPendingBg";
36
+ }
37
+
38
+ export function getStateIcon(icon: IconType, theme: Theme, spinnerFrame?: number): string {
39
+ if (icon === "success") return theme.styledSymbol("status.success", "success");
40
+ if (icon === "error") return theme.styledSymbol("status.error", "error");
41
+ if (icon === "warning") return theme.styledSymbol("status.warning", "warning");
42
+ if (icon === "info") return theme.styledSymbol("status.info", "accent");
43
+ if (icon === "pending") return theme.styledSymbol("status.pending", "accent");
44
+ if (spinnerFrame !== undefined) {
45
+ const frames = theme.spinnerFrames;
46
+ return frames[spinnerFrame % frames.length];
47
+ }
48
+ return theme.styledSymbol("status.running", "accent");
49
+ }