@esso0428/pi-subagents 0.15.6 → 0.15.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 (217) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/agent-history-list.d.ts +1 -0
  3. package/dist/agent-history-list.d.ts.map +1 -0
  4. package/dist/agent-history-list.js +1 -0
  5. package/dist/agent-history-list.js.map +1 -0
  6. package/dist/agent-history.d.ts +1 -0
  7. package/dist/agent-history.d.ts.map +1 -0
  8. package/dist/agent-history.js +1 -0
  9. package/dist/agent-history.js.map +1 -0
  10. package/dist/agent-manager.d.ts +2 -0
  11. package/dist/agent-manager.d.ts.map +1 -0
  12. package/dist/agent-manager.js +71 -0
  13. package/dist/agent-manager.js.map +1 -0
  14. package/dist/agent-runner.d.ts +1 -0
  15. package/dist/agent-runner.d.ts.map +1 -0
  16. package/dist/agent-runner.js +8 -0
  17. package/dist/agent-runner.js.map +1 -0
  18. package/dist/agent-types.d.ts +1 -0
  19. package/dist/agent-types.d.ts.map +1 -0
  20. package/dist/agent-types.js +1 -0
  21. package/dist/agent-types.js.map +1 -0
  22. package/dist/context.d.ts +1 -0
  23. package/dist/context.d.ts.map +1 -0
  24. package/dist/context.js +1 -0
  25. package/dist/context.js.map +1 -0
  26. package/dist/cross-extension-rpc.d.ts +1 -0
  27. package/dist/cross-extension-rpc.d.ts.map +1 -0
  28. package/dist/cross-extension-rpc.js +1 -0
  29. package/dist/cross-extension-rpc.js.map +1 -0
  30. package/dist/custom-agents.d.ts +1 -0
  31. package/dist/custom-agents.d.ts.map +1 -0
  32. package/dist/custom-agents.js +1 -0
  33. package/dist/custom-agents.js.map +1 -0
  34. package/dist/default-agents.d.ts +1 -0
  35. package/dist/default-agents.d.ts.map +1 -0
  36. package/dist/default-agents.js +1 -0
  37. package/dist/default-agents.js.map +1 -0
  38. package/dist/enabled-models.d.ts +1 -0
  39. package/dist/enabled-models.d.ts.map +1 -0
  40. package/dist/enabled-models.js +1 -0
  41. package/dist/enabled-models.js.map +1 -0
  42. package/dist/env.d.ts +1 -0
  43. package/dist/env.d.ts.map +1 -0
  44. package/dist/env.js +1 -0
  45. package/dist/env.js.map +1 -0
  46. package/dist/group-join.d.ts +1 -0
  47. package/dist/group-join.d.ts.map +1 -0
  48. package/dist/group-join.js +1 -0
  49. package/dist/group-join.js.map +1 -0
  50. package/dist/index.d.ts +1 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +37 -10
  53. package/dist/index.js.map +1 -0
  54. package/dist/invocation-config.d.ts +1 -0
  55. package/dist/invocation-config.d.ts.map +1 -0
  56. package/dist/invocation-config.js +1 -0
  57. package/dist/invocation-config.js.map +1 -0
  58. package/dist/memory.d.ts +1 -0
  59. package/dist/memory.d.ts.map +1 -0
  60. package/dist/memory.js +1 -0
  61. package/dist/memory.js.map +1 -0
  62. package/dist/model-resolver.d.ts +1 -0
  63. package/dist/model-resolver.d.ts.map +1 -0
  64. package/dist/model-resolver.js +1 -0
  65. package/dist/model-resolver.js.map +1 -0
  66. package/dist/nico-overrides.d.ts +1 -0
  67. package/dist/nico-overrides.d.ts.map +1 -0
  68. package/dist/nico-overrides.js +1 -0
  69. package/dist/nico-overrides.js.map +1 -0
  70. package/dist/output-file.d.ts +1 -0
  71. package/dist/output-file.d.ts.map +1 -0
  72. package/dist/output-file.js +1 -0
  73. package/dist/output-file.js.map +1 -0
  74. package/dist/prompts.d.ts +1 -0
  75. package/dist/prompts.d.ts.map +1 -0
  76. package/dist/prompts.js +1 -0
  77. package/dist/prompts.js.map +1 -0
  78. package/dist/schedule-store.d.ts +1 -0
  79. package/dist/schedule-store.d.ts.map +1 -0
  80. package/dist/schedule-store.js +1 -0
  81. package/dist/schedule-store.js.map +1 -0
  82. package/dist/schedule.d.ts +1 -0
  83. package/dist/schedule.d.ts.map +1 -0
  84. package/dist/schedule.js +1 -0
  85. package/dist/schedule.js.map +1 -0
  86. package/dist/settings.d.ts +1 -0
  87. package/dist/settings.d.ts.map +1 -0
  88. package/dist/settings.js +1 -0
  89. package/dist/settings.js.map +1 -0
  90. package/dist/skill-loader.d.ts +1 -0
  91. package/dist/skill-loader.d.ts.map +1 -0
  92. package/dist/skill-loader.js +1 -0
  93. package/dist/skill-loader.js.map +1 -0
  94. package/dist/status-note.d.ts +1 -0
  95. package/dist/status-note.d.ts.map +1 -0
  96. package/dist/status-note.js +1 -0
  97. package/dist/status-note.js.map +1 -0
  98. package/dist/types.d.ts +5 -0
  99. package/dist/types.d.ts.map +1 -0
  100. package/dist/types.js +1 -0
  101. package/dist/types.js.map +1 -0
  102. package/dist/ui/agent-widget.d.ts +1 -0
  103. package/dist/ui/agent-widget.d.ts.map +1 -0
  104. package/dist/ui/agent-widget.js +4 -2
  105. package/dist/ui/agent-widget.js.map +1 -0
  106. package/dist/ui/ccstyle/diff/ansi-utils.d.ts +11 -0
  107. package/dist/ui/ccstyle/diff/ansi-utils.d.ts.map +1 -0
  108. package/dist/ui/ccstyle/diff/ansi-utils.js +145 -0
  109. package/dist/ui/ccstyle/diff/ansi-utils.js.map +1 -0
  110. package/dist/ui/ccstyle/diff/diff-presentation.d.ts +12 -0
  111. package/dist/ui/ccstyle/diff/diff-presentation.d.ts.map +1 -0
  112. package/dist/ui/ccstyle/diff/diff-presentation.js +49 -0
  113. package/dist/ui/ccstyle/diff/diff-presentation.js.map +1 -0
  114. package/dist/ui/ccstyle/diff/diff-renderer.d.ts +46 -0
  115. package/dist/ui/ccstyle/diff/diff-renderer.d.ts.map +1 -0
  116. package/dist/ui/ccstyle/diff/diff-renderer.js +2044 -0
  117. package/dist/ui/ccstyle/diff/diff-renderer.js.map +1 -0
  118. package/dist/ui/ccstyle/diff/line-width-safety.d.ts +12 -0
  119. package/dist/ui/ccstyle/diff/line-width-safety.d.ts.map +1 -0
  120. package/dist/ui/ccstyle/diff/line-width-safety.js +59 -0
  121. package/dist/ui/ccstyle/diff/line-width-safety.js.map +1 -0
  122. package/dist/ui/ccstyle/diff/render-utils.d.ts +6 -0
  123. package/dist/ui/ccstyle/diff/render-utils.d.ts.map +1 -0
  124. package/dist/ui/ccstyle/diff/render-utils.js +25 -0
  125. package/dist/ui/ccstyle/diff/render-utils.js.map +1 -0
  126. package/dist/ui/ccstyle/diff/types.d.ts +22 -0
  127. package/dist/ui/ccstyle/diff/types.d.ts.map +1 -0
  128. package/dist/ui/ccstyle/diff/types.js +10 -0
  129. package/dist/ui/ccstyle/diff/types.js.map +1 -0
  130. package/dist/ui/ccstyle/diff/write-display-utils.d.ts +2 -0
  131. package/dist/ui/ccstyle/diff/write-display-utils.d.ts.map +1 -0
  132. package/dist/ui/ccstyle/diff/write-display-utils.js +12 -0
  133. package/dist/ui/ccstyle/diff/write-display-utils.js.map +1 -0
  134. package/dist/ui/ccstyle/tool-renderer.d.ts +17 -0
  135. package/dist/ui/ccstyle/tool-renderer.d.ts.map +1 -0
  136. package/dist/ui/ccstyle/tool-renderer.js +58 -0
  137. package/dist/ui/ccstyle/tool-renderer.js.map +1 -0
  138. package/dist/ui/ccstyle/tool-result.d.ts +24 -0
  139. package/dist/ui/ccstyle/tool-result.d.ts.map +1 -0
  140. package/dist/ui/ccstyle/tool-result.js +193 -0
  141. package/dist/ui/ccstyle/tool-result.js.map +1 -0
  142. package/dist/ui/conversation-blocks.d.ts +28 -0
  143. package/dist/ui/conversation-blocks.d.ts.map +1 -0
  144. package/dist/ui/conversation-blocks.js +119 -27
  145. package/dist/ui/conversation-blocks.js.map +1 -0
  146. package/dist/ui/conversation-nvim.d.ts +1 -0
  147. package/dist/ui/conversation-nvim.d.ts.map +1 -0
  148. package/dist/ui/conversation-nvim.js +1 -0
  149. package/dist/ui/conversation-nvim.js.map +1 -0
  150. package/dist/ui/conversation-role.d.ts +13 -0
  151. package/dist/ui/conversation-role.d.ts.map +1 -0
  152. package/dist/ui/conversation-role.js +39 -0
  153. package/dist/ui/conversation-role.js.map +1 -0
  154. package/dist/ui/conversation-search.d.ts +1 -0
  155. package/dist/ui/conversation-search.d.ts.map +1 -0
  156. package/dist/ui/conversation-search.js +1 -0
  157. package/dist/ui/conversation-search.js.map +1 -0
  158. package/dist/ui/conversation-timeline.d.ts +62 -0
  159. package/dist/ui/conversation-timeline.d.ts.map +1 -0
  160. package/dist/ui/conversation-timeline.js +266 -0
  161. package/dist/ui/conversation-timeline.js.map +1 -0
  162. package/dist/ui/conversation-viewer.d.ts +7 -1
  163. package/dist/ui/conversation-viewer.d.ts.map +1 -0
  164. package/dist/ui/conversation-viewer.js +96 -54
  165. package/dist/ui/conversation-viewer.js.map +1 -0
  166. package/dist/ui/fleet-list.d.ts +1 -0
  167. package/dist/ui/fleet-list.d.ts.map +1 -0
  168. package/dist/ui/fleet-list.js +1 -0
  169. package/dist/ui/fleet-list.js.map +1 -0
  170. package/dist/ui/schedule-menu.d.ts +1 -0
  171. package/dist/ui/schedule-menu.d.ts.map +1 -0
  172. package/dist/ui/schedule-menu.js +1 -0
  173. package/dist/ui/schedule-menu.js.map +1 -0
  174. package/dist/ui/viewer-keys.d.ts +1 -0
  175. package/dist/ui/viewer-keys.d.ts.map +1 -0
  176. package/dist/ui/viewer-keys.js +1 -0
  177. package/dist/ui/viewer-keys.js.map +1 -0
  178. package/dist/usage.d.ts +1 -0
  179. package/dist/usage.d.ts.map +1 -0
  180. package/dist/usage.js +1 -0
  181. package/dist/usage.js.map +1 -0
  182. package/dist/worktree.d.ts +1 -0
  183. package/dist/worktree.d.ts.map +1 -0
  184. package/dist/worktree.js +1 -0
  185. package/dist/worktree.js.map +1 -0
  186. package/dist/write-execution.d.ts +42 -0
  187. package/dist/write-execution.d.ts.map +1 -0
  188. package/dist/write-execution.js +138 -0
  189. package/dist/write-execution.js.map +1 -0
  190. package/package.json +1 -1
  191. package/src/agent-manager.ts +61 -0
  192. package/src/agent-runner.ts +7 -0
  193. package/src/index.ts +35 -11
  194. package/src/types.ts +4 -0
  195. package/src/ui/agent-widget.ts +2 -1
  196. package/src/ui/ccstyle/ATTRIBUTION.md +37 -0
  197. package/src/ui/ccstyle/diff/ansi-utils.ts +168 -0
  198. package/src/ui/ccstyle/diff/diff-presentation.ts +70 -0
  199. package/src/ui/ccstyle/diff/diff-renderer.ts +3148 -0
  200. package/src/ui/ccstyle/diff/line-width-safety.ts +92 -0
  201. package/src/ui/ccstyle/diff/render-utils.ts +32 -0
  202. package/src/ui/ccstyle/diff/types.ts +33 -0
  203. package/src/ui/ccstyle/diff/write-display-utils.ts +12 -0
  204. package/src/ui/ccstyle/tool-renderer.ts +71 -0
  205. package/src/ui/ccstyle/tool-result.ts +238 -0
  206. package/src/ui/conversation-blocks.ts +160 -26
  207. package/src/ui/conversation-role.ts +45 -0
  208. package/src/ui/conversation-timeline.ts +318 -0
  209. package/src/ui/conversation-viewer.ts +91 -54
  210. package/src/write-execution.ts +180 -0
  211. package/test/output-file-history.test.ts +50 -0
  212. package/test/ui/conversation-blocks.test.ts +31 -0
  213. package/test/ui/conversation-diff.test.ts +91 -0
  214. package/test/ui/conversation-viewer.test.ts +70 -0
  215. package/test/write-execution.test.ts +83 -0
  216. package/tsconfig.json +16 -0
  217. package/vitest.temp.config.mts +6 -0
package/src/index.ts CHANGED
@@ -451,6 +451,9 @@ export default function (pi: ExtensionAPI) {
451
451
  id: record.id, type: record.type, description: record.description,
452
452
  status: record.status, result: record.result, error: record.error,
453
453
  startedAt: record.startedAt, completedAt: record.completedAt,
454
+ toolUses: record.toolUses,
455
+ lifetimeUsage: record.lifetimeUsage,
456
+ invocation: record.invocation,
454
457
  transcriptPath: record.transcriptPath,
455
458
  });
456
459
 
@@ -1168,7 +1171,9 @@ Terse command-style prompts produce shallow, generic work.
1168
1171
  const effectiveMaxTurns = normalizeMaxTurns(resolvedConfig.maxTurns ?? getDefaultMaxTurns());
1169
1172
  const agentInvocation: AgentInvocation = {
1170
1173
  modelName,
1174
+ effectiveModelName: model?.name ?? model?.id ?? ctx.model?.name ?? ctx.model?.id,
1171
1175
  thinking,
1176
+ effectiveThinking: thinking,
1172
1177
  // Explicit value only — the default fallback would just add noise.
1173
1178
  // Normalize so `0` (unlimited) doesn't surface as a misleading "max turns: 0".
1174
1179
  maxTurns: normalizeMaxTurns(resolvedConfig.maxTurns),
@@ -1758,6 +1763,26 @@ Terse command-style prompts produce shallow, generic work.
1758
1763
  }
1759
1764
  }
1760
1765
 
1766
+ function makeUniqueAgentOptionLabels(pairs: Array<{ record: AgentRecord; label: string }>): string[] {
1767
+ const counts = new Map<string, number>();
1768
+ for (const pair of pairs) counts.set(pair.label, (counts.get(pair.label) ?? 0) + 1);
1769
+ const used = new Set<string>();
1770
+ return pairs.map((pair) => {
1771
+ const { record, label } = pair;
1772
+ if ((counts.get(label) ?? 0) === 1) {
1773
+ used.add(label);
1774
+ return label;
1775
+ }
1776
+ const suffix = ` · #${record.id.slice(-8)}`;
1777
+ let candidate = `${label}${suffix}`;
1778
+ let n = 2;
1779
+ while (used.has(candidate)) candidate = `${label}${suffix}-${n++}`;
1780
+ used.add(candidate);
1781
+ pair.label = candidate;
1782
+ return candidate;
1783
+ });
1784
+ }
1785
+
1761
1786
  async function showRunningAgents(ctx: ExtensionCommandContext) {
1762
1787
  const { active: agents } = splitAgentRecords(manager.listAgents(), ctx.cwd);
1763
1788
  if (agents.length === 0) {
@@ -1765,19 +1790,17 @@ Terse command-style prompts produce shallow, generic work.
1765
1790
  return;
1766
1791
  }
1767
1792
 
1768
- const options = agents.map(a => {
1769
- const dn = getDisplayName(a.type);
1770
- const dur = formatDuration(a.startedAt, a.completedAt);
1771
- return `${dn} (${a.description}) · ${a.toolUses} tools · ${a.status} · ${dur}`;
1793
+ const pairs = agents.map((record) => {
1794
+ const dn = getDisplayName(record.type);
1795
+ const dur = formatDuration(record.startedAt, record.completedAt);
1796
+ return { record, label: `${dn} (${record.description}) · ${record.toolUses} tools · ${record.status} · ${dur}` };
1772
1797
  });
1798
+ const options = makeUniqueAgentOptionLabels(pairs);
1773
1799
 
1774
1800
  const choice = await ctx.ui.select("Running agents", options);
1775
1801
  if (!choice) return;
1776
-
1777
- // Find the selected agent by matching the option index
1778
- const idx = options.indexOf(choice);
1779
- if (idx < 0) return;
1780
- const record = agents[idx];
1802
+ const record = pairs.find((pair) => pair.label === choice)?.record;
1803
+ if (!record) return;
1781
1804
 
1782
1805
  await viewAgentConversation(ctx, record, "live");
1783
1806
  // Back-navigation: re-show the list
@@ -1791,10 +1814,11 @@ Terse command-style prompts produce shallow, generic work.
1791
1814
  return;
1792
1815
  }
1793
1816
 
1794
- const options = history.map(record => formatAgentHistoryOption(record, Date.now()));
1817
+ const pairs = history.map((record) => ({ record, label: formatAgentHistoryOption(record, Date.now()) }));
1818
+ const options = makeUniqueAgentOptionLabels(pairs);
1795
1819
  const choice = await ctx.ui.select("Agent history", options);
1796
1820
  if (!choice) return;
1797
- const record = history[options.indexOf(choice)];
1821
+ const record = pairs.find((pair) => pair.label === choice)?.record;
1798
1822
  if (!record) return;
1799
1823
 
1800
1824
  await viewAgentConversation(ctx, record, "history");
package/src/types.ts CHANGED
@@ -137,6 +137,10 @@ export interface AgentRecord {
137
137
  export interface AgentInvocation {
138
138
  /** Short display name, e.g. "haiku" — only set when different from parent. */
139
139
  modelName?: string;
140
+ /** Effective model captured from the child session, including inherited models. */
141
+ effectiveModelName?: string;
142
+ /** Requested/ effective thinking level. */
143
+ effectiveThinking?: AgentSession["thinkingLevel"];
140
144
  thinking?: ThinkingLevel;
141
145
  maxTurns?: number;
142
146
  isolated?: boolean;
@@ -166,7 +166,8 @@ export function buildInvocationTags(
166
166
  ): { modelName?: string; tags: string[] } {
167
167
  const tags: string[] = [];
168
168
  if (!invocation) return { tags };
169
- if (invocation.thinking) tags.push(`thinking: ${invocation.thinking}`);
169
+ const thinking = invocation.effectiveThinking ?? invocation.thinking;
170
+ if (thinking) tags.push(`thinking: ${thinking}`);
170
171
  if (invocation.isolated) tags.push("isolated");
171
172
  if (invocation.isolation === "worktree") tags.push("worktree");
172
173
  if (invocation.inheritContext) tags.push("inherit context");
@@ -0,0 +1,37 @@
1
+ # Attribution
2
+
3
+ The viewer-local ccstyle tool-card and diff renderer are adapted from
4
+ `@herbertgao/pi-cc-extensions` version `0.9.0`, specifically its
5
+ `extensions/renderer/tool/result.ts` and
6
+ `extensions/renderer/tool/diff/` modules.
7
+
8
+ The adaptation is local to this package and changes the configuration boundary,
9
+ write payload handling, width guards, and syntax highlighting fallback. It does
10
+ not install `@shikijs/cli`, patch Pi prototypes, or register global terminal
11
+ handlers. The local copy uses Pi's public `highlightCode` API and plain text
12
+ when highlighting is unavailable.
13
+
14
+ The source package is distributed under the MIT License. The relevant license
15
+ text is reproduced below:
16
+
17
+ > MIT License
18
+ >
19
+ > Copyright (c) 2026 Herbert Gao and pi-cc-extensions contributors
20
+ >
21
+ > Permission is hereby granted, free of charge, to any person obtaining a copy
22
+ > of this software and associated documentation files (the "Software"), to deal
23
+ > in the Software without restriction, including without limitation the rights
24
+ > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
25
+ > copies of the Software, and to permit persons to whom the Software is
26
+ > furnished to do so, subject to the following conditions:
27
+ >
28
+ > The above copyright notice and this permission notice shall be included in all
29
+ > copies or substantial portions of the Software.
30
+ >
31
+ > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
32
+ > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
33
+ > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
34
+ > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
35
+ > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
36
+ > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
37
+ > SOFTWARE.
@@ -0,0 +1,168 @@
1
+ const ANSI_SGR_PATTERN = /\x1b\[([0-9;]*)m/g;
2
+ const ANSI_SGR_WITH_COLON_PATTERN = /\x1b\[([0-9;:]*)m/g;
3
+ const STYLE_RESET_PARAMS = [39, 22, 23, 24, 25, 27, 28, 29, 59] as const;
4
+
5
+ export { ANSI_SGR_PATTERN, STYLE_RESET_PARAMS };
6
+
7
+ export function expandSgrReset(param: number): readonly number[] | undefined {
8
+ return param === 0 ? STYLE_RESET_PARAMS : undefined;
9
+ }
10
+
11
+ export function toSgrParams(rawParams: string): number[] {
12
+ if (!rawParams.trim()) {
13
+ return [0];
14
+ }
15
+
16
+ const parsed = rawParams
17
+ .split(";")
18
+ .map((token) => Number.parseInt(token, 10))
19
+ .filter((value) => Number.isFinite(value));
20
+
21
+ return parsed.length > 0 ? parsed : [];
22
+ }
23
+
24
+ export function isFiniteSgrParam(value: number | undefined): value is number {
25
+ return typeof value === "number" && Number.isFinite(value);
26
+ }
27
+
28
+ export function readSgrColorSequence(params: number[], index: number): number[] | undefined {
29
+ const param = params[index];
30
+ if (param !== 38 && param !== 48) {
31
+ return undefined;
32
+ }
33
+
34
+ const colorMode = params[index + 1];
35
+ if (colorMode === 5) {
36
+ const colorValue = params[index + 2];
37
+ return isFiniteSgrParam(colorValue) ? [param, colorMode, colorValue] : undefined;
38
+ }
39
+
40
+ if (colorMode === 2) {
41
+ const red = params[index + 2];
42
+ const green = params[index + 3];
43
+ const blue = params[index + 4];
44
+ return isFiniteSgrParam(red) && isFiniteSgrParam(green) && isFiniteSgrParam(blue)
45
+ ? [param, colorMode, red, green, blue]
46
+ : undefined;
47
+ }
48
+
49
+ return undefined;
50
+ }
51
+
52
+ export function stripBackgroundSgrParams(params: readonly number[]): number[] {
53
+ const sanitized: number[] = [];
54
+
55
+ for (let index = 0; index < params.length; index++) {
56
+ const param = params[index] ?? 0;
57
+
58
+ if (param === 0) {
59
+ sanitized.push(...expandSgrReset(param)!);
60
+ continue;
61
+ }
62
+
63
+ if (param === 49 || (param >= 40 && param <= 47) || (param >= 100 && param <= 107)) {
64
+ continue;
65
+ }
66
+
67
+ if (param === 38 || param === 48) {
68
+ const sequence = readSgrColorSequence(params as number[], index);
69
+ if (sequence) {
70
+ if (param === 38) {
71
+ sanitized.push(...sequence);
72
+ }
73
+ index += sequence.length - 1;
74
+ continue;
75
+ }
76
+ const advance = params[index + 1] === 5 ? 2 : params[index + 1] === 2 ? 4 : 0;
77
+ if (advance > 0) {
78
+ index += advance;
79
+ if (param === 38) {
80
+ sanitized.push(param);
81
+ }
82
+ continue;
83
+ }
84
+ }
85
+
86
+ sanitized.push(param);
87
+ }
88
+
89
+ return sanitized;
90
+ }
91
+
92
+ export function filterSgrSequences(text: string, filter: (params: number[]) => number[]): string {
93
+ if (!text || !text.includes("\x1b[")) {
94
+ return text;
95
+ }
96
+
97
+ return text.replace(ANSI_SGR_PATTERN, (_sequence, rawParams: string) => {
98
+ const parsed = toSgrParams(rawParams);
99
+ if (parsed.length === 0) {
100
+ return "";
101
+ }
102
+
103
+ const sanitized = filter(parsed);
104
+ if (sanitized.length === 0) {
105
+ return "";
106
+ }
107
+
108
+ return `\x1b[${sanitized.join(";")}m`;
109
+ });
110
+ }
111
+
112
+ function isValidColonForeground(segment: string): boolean {
113
+ const params = segment.split(":");
114
+ const isByte = (value: string | undefined) =>
115
+ value !== undefined && /^\d+$/.test(value) && Number(value) <= 255;
116
+ if (params[0] !== "38") return false;
117
+ if (params[1] === "5") return params.length === 3 && isByte(params[2]);
118
+ return (
119
+ params[1] === "2" &&
120
+ params.length === 6 &&
121
+ (params[2] === "" || /^\d+$/.test(params[2] ?? "")) &&
122
+ params.slice(3).every(isByte)
123
+ );
124
+ }
125
+
126
+ function sanitizeColonSgr(rawParams: string): string {
127
+ const segments = rawParams.split(";");
128
+ const sanitized: string[] = [];
129
+ for (let index = 0; index < segments.length; index++) {
130
+ const segment = segments[index] ?? "";
131
+ if (segment.includes(":")) {
132
+ const code = Number.parseInt(segment, 10);
133
+ if (code !== 48 && (code !== 38 || isValidColonForeground(segment))) {
134
+ sanitized.push(segment);
135
+ }
136
+ continue;
137
+ }
138
+ const param = Number.parseInt(segment, 10);
139
+ if (!Number.isFinite(param)) continue;
140
+ if (param === 0) {
141
+ sanitized.push(...STYLE_RESET_PARAMS.map(String));
142
+ continue;
143
+ }
144
+ if (param === 49 || (param >= 40 && param <= 47) || (param >= 100 && param <= 107)) {
145
+ continue;
146
+ }
147
+ if (param === 38 || param === 48) {
148
+ const mode = Number.parseInt(segments[index + 1] ?? "", 10);
149
+ const advance = mode === 5 ? 2 : mode === 2 ? 4 : 0;
150
+ if (advance > 0) {
151
+ if (param === 38) sanitized.push(...segments.slice(index, index + advance + 1));
152
+ index += advance;
153
+ continue;
154
+ }
155
+ }
156
+ sanitized.push(segment);
157
+ }
158
+ return sanitized.length > 0 ? `\x1b[${sanitized.join(";")}m` : "";
159
+ }
160
+
161
+ export function sanitizeAnsiForThemedOutput(text: string): string {
162
+ if (!text || !text.includes("\x1b[")) return text;
163
+ return text.replace(ANSI_SGR_WITH_COLON_PATTERN, (sequence, rawParams: string) =>
164
+ rawParams.includes(":")
165
+ ? sanitizeColonSgr(rawParams)
166
+ : filterSgrSequences(sequence, stripBackgroundSgrParams),
167
+ );
168
+ }
@@ -0,0 +1,70 @@
1
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
2
+ import { pluralize } from "./render-utils.js";
3
+ import type { ToolDisplayConfig } from "./types.js";
4
+
5
+ export interface DiffSummaryStats {
6
+ added: number;
7
+ removed: number;
8
+ hunks: number;
9
+ files: number;
10
+ }
11
+
12
+ export type DiffPresentationMode = "split" | "unified" | "compact" | "summary";
13
+
14
+ const MIN_COMPACT_DIFF_WIDTH = 8;
15
+ const MIN_UNIFIED_DIFF_WIDTH = 18;
16
+
17
+ export function normalizeDiffRenderWidth(width: number): number {
18
+ if (!Number.isFinite(width)) {
19
+ return 0;
20
+ }
21
+ return Math.max(0, Math.floor(width));
22
+ }
23
+
24
+ export function resolveDiffPresentationMode(
25
+ config: Pick<ToolDisplayConfig, "diffViewMode" | "diffSplitMinWidth">,
26
+ width: number,
27
+ canRenderSplitLayout: boolean,
28
+ ): DiffPresentationMode {
29
+ const safeWidth = normalizeDiffRenderWidth(width);
30
+ if (safeWidth < MIN_COMPACT_DIFF_WIDTH) {
31
+ return "summary";
32
+ }
33
+ if (safeWidth < MIN_UNIFIED_DIFF_WIDTH) {
34
+ return "compact";
35
+ }
36
+
37
+ switch (config.diffViewMode) {
38
+ case "split":
39
+ return canRenderSplitLayout ? "split" : "unified";
40
+ case "unified":
41
+ return "unified";
42
+ case "auto":
43
+ default:
44
+ return safeWidth >= (config.diffSplitMinWidth ?? 120) && canRenderSplitLayout ? "split" : "unified";
45
+ }
46
+ }
47
+
48
+ export function buildDiffSummaryText(stats: DiffSummaryStats, width: number): string {
49
+ const safeWidth = normalizeDiffRenderWidth(width);
50
+ if (safeWidth === 0) {
51
+ return "";
52
+ }
53
+
54
+ const summaryCandidates = [
55
+ `↳ diff +${stats.added} -${stats.removed} • ${stats.hunks} ${pluralize(stats.hunks, "hunk")} • ${stats.files} ${pluralize(stats.files, "file")}`,
56
+ `↳ diff +${stats.added} -${stats.removed} • ${stats.hunks}h • ${stats.files}f`,
57
+ `↳ diff +${stats.added} -${stats.removed}`,
58
+ `+${stats.added} -${stats.removed}`,
59
+ "diff",
60
+ "…",
61
+ ];
62
+
63
+ for (const candidate of summaryCandidates) {
64
+ if (visibleWidth(candidate) <= safeWidth) {
65
+ return candidate;
66
+ }
67
+ }
68
+
69
+ return truncateToWidth(summaryCandidates[summaryCandidates.length - 1] ?? "", safeWidth, "");
70
+ }