@oh-my-pi/pi-coding-agent 15.5.15 → 15.7.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 (274) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/dist/types/capability/rule-buckets.d.ts +30 -0
  3. package/dist/types/capability/rule.d.ts +7 -0
  4. package/dist/types/cli/classify-install-target.d.ts +0 -10
  5. package/dist/types/cli/completion-gen.d.ts +80 -0
  6. package/dist/types/cli/initial-message.d.ts +1 -1
  7. package/dist/types/cli/tiny-models-cli.d.ts +9 -0
  8. package/dist/types/commands/complete.d.ts +6 -0
  9. package/dist/types/commands/completions.d.ts +13 -0
  10. package/dist/types/commands/setup.d.ts +10 -1
  11. package/dist/types/commands/tiny-models.d.ts +22 -0
  12. package/dist/types/commit/analysis/conventional.d.ts +1 -1
  13. package/dist/types/commit/analysis/summary.d.ts +1 -1
  14. package/dist/types/commit/changelog/generate.d.ts +1 -1
  15. package/dist/types/commit/changelog/index.d.ts +2 -2
  16. package/dist/types/commit/map-reduce/map-phase.d.ts +1 -1
  17. package/dist/types/commit/map-reduce/reduce-phase.d.ts +1 -1
  18. package/dist/types/config/model-id-affixes.d.ts +10 -0
  19. package/dist/types/config/settings-schema.d.ts +402 -17
  20. package/dist/types/discovery/builtin-defaults.d.ts +1 -0
  21. package/dist/types/discovery/builtin-rules/index.d.ts +7 -0
  22. package/dist/types/discovery/helpers.d.ts +1 -1
  23. package/dist/types/discovery/index.d.ts +1 -0
  24. package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
  25. package/dist/types/edit/hashline/block-resolver.d.ts +9 -0
  26. package/dist/types/edit/hashline/index.d.ts +1 -0
  27. package/dist/types/eval/js/shared/rewrite-imports.d.ts +16 -1
  28. package/dist/types/eval/py/kernel.d.ts +3 -0
  29. package/dist/types/eval/py/runtime.d.ts +11 -1
  30. package/dist/types/export/html/template.generated.d.ts +1 -1
  31. package/dist/types/internal-urls/agent-protocol.d.ts +2 -1
  32. package/dist/types/internal-urls/artifact-protocol.d.ts +2 -1
  33. package/dist/types/internal-urls/local-protocol.d.ts +2 -1
  34. package/dist/types/internal-urls/memory-protocol.d.ts +2 -1
  35. package/dist/types/internal-urls/omp-protocol.d.ts +2 -1
  36. package/dist/types/internal-urls/router.d.ts +8 -1
  37. package/dist/types/internal-urls/rule-protocol.d.ts +2 -1
  38. package/dist/types/internal-urls/skill-protocol.d.ts +2 -1
  39. package/dist/types/internal-urls/types.d.ts +26 -0
  40. package/dist/types/main.d.ts +1 -0
  41. package/dist/types/memory-backend/index.d.ts +1 -0
  42. package/dist/types/memory-backend/resolve.d.ts +2 -1
  43. package/dist/types/memory-backend/types.d.ts +7 -1
  44. package/dist/types/mnemosyne/backend.d.ts +4 -0
  45. package/dist/types/mnemosyne/config.d.ts +29 -0
  46. package/dist/types/mnemosyne/index.d.ts +3 -0
  47. package/dist/types/mnemosyne/state.d.ts +72 -0
  48. package/dist/types/modes/components/custom-editor.d.ts +2 -3
  49. package/dist/types/modes/components/hook-selector.d.ts +27 -0
  50. package/dist/types/modes/components/index.d.ts +2 -0
  51. package/dist/types/modes/components/segment-track.d.ts +22 -0
  52. package/dist/types/modes/components/status-line/context-thresholds.d.ts +6 -0
  53. package/dist/types/modes/components/tiny-title-download-progress.d.ts +11 -0
  54. package/dist/types/modes/components/welcome.d.ts +22 -0
  55. package/dist/types/modes/controllers/extension-ui-controller.d.ts +4 -1
  56. package/dist/types/modes/gradient-highlight.d.ts +23 -0
  57. package/dist/types/modes/interactive-mode.d.ts +7 -4
  58. package/dist/types/modes/internal-url-autocomplete.d.ts +43 -0
  59. package/dist/types/modes/orchestrate.d.ts +10 -0
  60. package/dist/types/modes/setup-wizard/index.d.ts +16 -0
  61. package/dist/types/modes/setup-wizard/scenes/glyph.d.ts +2 -0
  62. package/dist/types/modes/setup-wizard/scenes/outro.d.ts +2 -0
  63. package/dist/types/modes/setup-wizard/scenes/providers.d.ts +2 -0
  64. package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +19 -0
  65. package/dist/types/modes/setup-wizard/scenes/splash.d.ts +11 -0
  66. package/dist/types/modes/setup-wizard/scenes/theme.d.ts +2 -0
  67. package/dist/types/modes/setup-wizard/scenes/types.d.ts +43 -0
  68. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +19 -0
  69. package/dist/types/modes/setup-wizard/wizard-overlay.d.ts +14 -0
  70. package/dist/types/modes/theme/defaults/index.d.ts +8406 -8406
  71. package/dist/types/modes/theme/shimmer.d.ts +2 -0
  72. package/dist/types/modes/theme/theme.d.ts +11 -0
  73. package/dist/types/modes/types.d.ts +5 -1
  74. package/dist/types/modes/ultrathink.d.ts +3 -3
  75. package/dist/types/modes/utils/keybinding-matchers.d.ts +5 -0
  76. package/dist/types/sdk.d.ts +3 -0
  77. package/dist/types/session/agent-session.d.ts +33 -0
  78. package/dist/types/system-prompt.d.ts +2 -0
  79. package/dist/types/task/executor.d.ts +2 -0
  80. package/dist/types/task/render.d.ts +5 -1
  81. package/dist/types/tiny/device.d.ts +78 -0
  82. package/dist/types/tiny/dtype.d.ts +85 -0
  83. package/dist/types/tiny/models.d.ts +185 -0
  84. package/dist/types/tiny/text.d.ts +19 -0
  85. package/dist/types/tiny/title-client.d.ts +32 -0
  86. package/dist/types/tiny/title-protocol.d.ts +74 -0
  87. package/dist/types/tiny/worker.d.ts +2 -0
  88. package/dist/types/tools/bash.d.ts +3 -2
  89. package/dist/types/tools/eval.d.ts +1 -1
  90. package/dist/types/tools/index.d.ts +7 -4
  91. package/dist/types/tools/memory-edit.d.ts +40 -0
  92. package/dist/types/tools/{hindsight-recall.d.ts → memory-recall.d.ts} +6 -6
  93. package/dist/types/tools/{hindsight-reflect.d.ts → memory-reflect.d.ts} +6 -6
  94. package/dist/types/tools/memory-render.d.ts +60 -0
  95. package/dist/types/tools/{hindsight-retain.d.ts → memory-retain.d.ts} +6 -6
  96. package/dist/types/tools/todo-write.d.ts +8 -0
  97. package/dist/types/tools/tool-result.d.ts +2 -0
  98. package/dist/types/tui/code-cell.d.ts +2 -0
  99. package/dist/types/tui/output-block.d.ts +17 -0
  100. package/dist/types/utils/title-generator.d.ts +3 -0
  101. package/package.json +18 -14
  102. package/scripts/build-binary.ts +1 -0
  103. package/src/capability/rule-buckets.ts +64 -0
  104. package/src/capability/rule.ts +8 -0
  105. package/src/cli/completion-gen.ts +550 -0
  106. package/src/cli/setup-cli.ts +5 -3
  107. package/src/cli/tiny-models-cli.ts +127 -0
  108. package/src/cli-commands.ts +3 -0
  109. package/src/cli.ts +9 -15
  110. package/src/commands/complete.ts +66 -0
  111. package/src/commands/completions.ts +60 -0
  112. package/src/commands/setup.ts +29 -4
  113. package/src/commands/tiny-models.ts +36 -0
  114. package/src/config/model-equivalence.ts +43 -2
  115. package/src/config/model-id-affixes.ts +64 -0
  116. package/src/config/model-registry.ts +84 -10
  117. package/src/config/settings-schema.ts +275 -15
  118. package/src/discovery/builtin-defaults.ts +39 -0
  119. package/src/discovery/builtin-rules/index.ts +48 -0
  120. package/src/discovery/builtin-rules/rs-box-leak.md +48 -0
  121. package/src/discovery/builtin-rules/rs-future-prelude.md +23 -0
  122. package/src/discovery/builtin-rules/rs-lazylock.md +51 -0
  123. package/src/discovery/builtin-rules/rs-match-ergonomics.md +67 -0
  124. package/src/discovery/builtin-rules/rs-parking-lot.md +44 -0
  125. package/src/discovery/builtin-rules/rs-result-type.md +19 -0
  126. package/src/discovery/builtin-rules/ts-bare-catch.md +38 -0
  127. package/src/discovery/builtin-rules/ts-import-type.md +42 -0
  128. package/src/discovery/builtin-rules/ts-no-any.md +56 -0
  129. package/src/discovery/builtin-rules/ts-no-dynamic-import.md +39 -0
  130. package/src/discovery/builtin-rules/ts-no-return-type.md +45 -0
  131. package/src/discovery/builtin-rules/ts-no-tiny-functions.md +50 -0
  132. package/src/discovery/builtin-rules/ts-promise-with-resolvers.md +65 -0
  133. package/src/discovery/builtin-rules/ts-set-map.md +28 -0
  134. package/src/discovery/index.ts +1 -0
  135. package/src/edit/hashline/block-resolver.ts +14 -0
  136. package/src/edit/hashline/diff.ts +9 -8
  137. package/src/edit/hashline/execute.ts +2 -1
  138. package/src/edit/hashline/index.ts +1 -0
  139. package/src/eval/__tests__/shared-executors.test.ts +36 -0
  140. package/src/eval/js/shared/local-module-loader.ts +13 -1
  141. package/src/eval/js/shared/rewrite-imports.ts +31 -26
  142. package/src/eval/py/kernel.ts +37 -15
  143. package/src/eval/py/runtime.ts +57 -28
  144. package/src/export/html/template.generated.ts +1 -1
  145. package/src/export/html/template.js +0 -12
  146. package/src/export/ttsr.ts +2 -0
  147. package/src/internal-urls/agent-protocol.ts +18 -1
  148. package/src/internal-urls/artifact-protocol.ts +19 -1
  149. package/src/internal-urls/docs-index.generated.ts +8 -7
  150. package/src/internal-urls/local-protocol.ts +14 -1
  151. package/src/internal-urls/memory-protocol.ts +6 -1
  152. package/src/internal-urls/omp-protocol.ts +5 -1
  153. package/src/internal-urls/router.ts +20 -1
  154. package/src/internal-urls/rule-protocol.ts +8 -1
  155. package/src/internal-urls/skill-protocol.ts +8 -1
  156. package/src/internal-urls/types.ts +27 -0
  157. package/src/lsp/render.ts +1 -1
  158. package/src/main.ts +18 -1
  159. package/src/mcp/oauth-flow.ts +2 -2
  160. package/src/memory-backend/index.ts +1 -0
  161. package/src/memory-backend/resolve.ts +4 -1
  162. package/src/memory-backend/types.ts +8 -1
  163. package/src/mnemosyne/backend.ts +374 -0
  164. package/src/mnemosyne/config.ts +160 -0
  165. package/src/mnemosyne/index.ts +3 -0
  166. package/src/mnemosyne/state.ts +548 -0
  167. package/src/modes/acp/acp-agent.ts +11 -6
  168. package/src/modes/components/agent-dashboard.ts +4 -4
  169. package/src/modes/components/custom-editor.ts +3 -2
  170. package/src/modes/components/diff.ts +2 -2
  171. package/src/modes/components/extensions/extension-list.ts +3 -2
  172. package/src/modes/components/footer.ts +5 -6
  173. package/src/modes/components/history-search.ts +3 -3
  174. package/src/modes/components/hook-selector.ts +92 -8
  175. package/src/modes/components/index.ts +2 -0
  176. package/src/modes/components/mcp-add-wizard.ts +3 -3
  177. package/src/modes/components/model-selector.ts +5 -4
  178. package/src/modes/components/oauth-selector.ts +3 -3
  179. package/src/modes/components/segment-track.ts +52 -0
  180. package/src/modes/components/session-observer-overlay.ts +19 -13
  181. package/src/modes/components/session-selector.ts +3 -3
  182. package/src/modes/components/settings-defs.ts +7 -0
  183. package/src/modes/components/status-line/context-thresholds.ts +11 -0
  184. package/src/modes/components/status-line/segments.ts +2 -2
  185. package/src/modes/components/tiny-title-download-progress.ts +90 -0
  186. package/src/modes/components/tips.txt +13 -0
  187. package/src/modes/components/tool-execution.ts +72 -4
  188. package/src/modes/components/tree-selector.ts +3 -3
  189. package/src/modes/components/user-message-selector.ts +3 -3
  190. package/src/modes/components/welcome.ts +102 -43
  191. package/src/modes/controllers/command-controller.ts +16 -1
  192. package/src/modes/controllers/extension-ui-controller.ts +3 -1
  193. package/src/modes/controllers/input-controller.ts +69 -21
  194. package/src/modes/gradient-highlight.ts +70 -0
  195. package/src/modes/interactive-mode.ts +75 -114
  196. package/src/modes/internal-url-autocomplete.ts +143 -0
  197. package/src/modes/orchestrate.ts +36 -0
  198. package/src/modes/prompt-action-autocomplete.ts +12 -0
  199. package/src/modes/setup-wizard/index.ts +88 -0
  200. package/src/modes/setup-wizard/scenes/glyph.ts +96 -0
  201. package/src/modes/setup-wizard/scenes/outro.ts +35 -0
  202. package/src/modes/setup-wizard/scenes/providers.ts +69 -0
  203. package/src/modes/setup-wizard/scenes/sign-in.ts +193 -0
  204. package/src/modes/setup-wizard/scenes/splash.ts +201 -0
  205. package/src/modes/setup-wizard/scenes/theme.ts +299 -0
  206. package/src/modes/setup-wizard/scenes/types.ts +48 -0
  207. package/src/modes/setup-wizard/scenes/web-search.ts +128 -0
  208. package/src/modes/setup-wizard/wizard-overlay.ts +275 -0
  209. package/src/modes/theme/shimmer.ts +5 -0
  210. package/src/modes/theme/theme.ts +44 -20
  211. package/src/modes/types.ts +6 -1
  212. package/src/modes/ultrathink.ts +9 -53
  213. package/src/modes/utils/keybinding-matchers.ts +11 -0
  214. package/src/prompts/system/memory-consolidation-system.md +8 -0
  215. package/src/prompts/system/memory-extraction-system.md +26 -0
  216. package/src/prompts/{commands/orchestrate.md → system/orchestrate-notice.md} +6 -17
  217. package/src/prompts/system/system-prompt.md +2 -0
  218. package/src/prompts/system/tiny-title-system.md +8 -0
  219. package/src/prompts/tools/memory-edit.md +8 -0
  220. package/src/prompts/tools/read.md +4 -0
  221. package/src/prompts/tools/task.md +4 -7
  222. package/src/sdk.ts +13 -21
  223. package/src/session/agent-session.ts +128 -44
  224. package/src/slash-commands/builtin-registry.ts +18 -1
  225. package/src/system-prompt.ts +4 -0
  226. package/src/task/commands.ts +1 -5
  227. package/src/task/executor.ts +8 -0
  228. package/src/task/index.ts +2 -0
  229. package/src/task/render.ts +69 -26
  230. package/src/tiny/device.ts +117 -0
  231. package/src/tiny/dtype.ts +101 -0
  232. package/src/tiny/models.ts +218 -0
  233. package/src/tiny/text.ts +54 -0
  234. package/src/tiny/title-client.ts +395 -0
  235. package/src/tiny/title-protocol.ts +51 -0
  236. package/src/tiny/worker.ts +587 -0
  237. package/src/tools/bash.ts +74 -29
  238. package/src/tools/browser/tab-worker.ts +1 -1
  239. package/src/tools/eval.ts +9 -4
  240. package/src/tools/index.ts +17 -22
  241. package/src/tools/memory-edit.ts +59 -0
  242. package/src/tools/memory-recall.ts +100 -0
  243. package/src/tools/memory-reflect.ts +88 -0
  244. package/src/tools/memory-render.ts +185 -0
  245. package/src/tools/memory-retain.ts +91 -0
  246. package/src/tools/read.ts +1 -0
  247. package/src/tools/renderers.ts +4 -2
  248. package/src/tools/todo-write.ts +128 -29
  249. package/src/tools/tool-result.ts +8 -0
  250. package/src/tui/code-cell.ts +6 -1
  251. package/src/tui/output-block.ts +199 -38
  252. package/src/utils/title-generator.ts +115 -13
  253. package/dist/types/tools/recipe/index.d.ts +0 -46
  254. package/dist/types/tools/recipe/render.d.ts +0 -36
  255. package/dist/types/tools/recipe/runner.d.ts +0 -60
  256. package/dist/types/tools/recipe/runners/cargo.d.ts +0 -16
  257. package/dist/types/tools/recipe/runners/index.d.ts +0 -2
  258. package/dist/types/tools/recipe/runners/just.d.ts +0 -2
  259. package/dist/types/tools/recipe/runners/make.d.ts +0 -2
  260. package/dist/types/tools/recipe/runners/pkg.d.ts +0 -2
  261. package/dist/types/tools/recipe/runners/task.d.ts +0 -2
  262. package/src/prompts/tools/recipe.md +0 -16
  263. package/src/tools/hindsight-recall.ts +0 -69
  264. package/src/tools/hindsight-reflect.ts +0 -58
  265. package/src/tools/hindsight-retain.ts +0 -57
  266. package/src/tools/recipe/index.ts +0 -81
  267. package/src/tools/recipe/render.ts +0 -19
  268. package/src/tools/recipe/runner.ts +0 -219
  269. package/src/tools/recipe/runners/cargo.ts +0 -131
  270. package/src/tools/recipe/runners/index.ts +0 -8
  271. package/src/tools/recipe/runners/just.ts +0 -73
  272. package/src/tools/recipe/runners/make.ts +0 -101
  273. package/src/tools/recipe/runners/pkg.ts +0 -167
  274. package/src/tools/recipe/runners/task.ts +0 -72
@@ -15,8 +15,81 @@ export interface OutputBlockOptions {
15
15
  sections?: Array<{ label?: string; lines: string[] }>;
16
16
  width: number;
17
17
  applyBg?: boolean;
18
+ /** Animate the border with a sweeping dark segment (pending/running state). */
19
+ animate?: boolean;
18
20
  }
19
21
 
22
+ const BORDER_SHIMMER_TICK_MS = 50;
23
+ /** Duration of one full clockwise lap around the border, in ms. Fixed so a box
24
+ * growing (new output line) or resizing only nudges the segment proportionally
25
+ * instead of teleporting it. */
26
+ const BORDER_LAP_MS = 4000;
27
+ /** Length, in border cells, of the moving segment. */
28
+ const BORDER_SEGMENT_LEN = 8;
29
+
30
+ /**
31
+ * Monotonic frame counter for animated borders. Quantized coarse enough to
32
+ * coalesce multiple render passes inside one frame, fine enough to advance on
33
+ * every spinner interval so cached blocks re-render while the segment travels.
34
+ */
35
+ export function borderShimmerTick(): number {
36
+ return Math.floor(Date.now() / BORDER_SHIMMER_TICK_MS);
37
+ }
38
+
39
+ /** Ease-in-out so the segment decelerates into and accelerates out of corners. */
40
+ function easeInOutQuad(t: number): number {
41
+ return t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2;
42
+ }
43
+
44
+ /**
45
+ * Perimeter index of the moving segment's head for a box of inner width `W` and
46
+ * height `H` at time `now`. The lap is split across the four edges in proportion
47
+ * to their length (so the average speed is uniform) and each edge is eased, for a
48
+ * deliberate, non-linear glide that slows at every corner. Position is derived
49
+ * from the wall clock against a fixed lap duration, so a perimeter change (new
50
+ * row / resize) shifts the head by at most a cell or two — no reset.
51
+ */
52
+ export function borderSegmentHead(W: number, H: number, now: number): number {
53
+ const P = 2 * W + 2 * H - 4;
54
+ if (P <= 0) return 0;
55
+ // Edge cell counts, clockwise from top-left: top, right, bottom, left.
56
+ const edgeLens = [W, H - 1, W - 1, H - 2];
57
+ const t = (((now % BORDER_LAP_MS) + BORDER_LAP_MS) % BORDER_LAP_MS) / BORDER_LAP_MS;
58
+ let acc = 0;
59
+ let start = 0;
60
+ for (let i = 0; i < 4; i++) {
61
+ const len = edgeLens[i]!;
62
+ const frac = len / P;
63
+ if (len > 0 && t < acc + frac) {
64
+ const lf = (t - acc) / frac;
65
+ return (start + Math.floor(easeInOutQuad(lf) * len)) % P;
66
+ }
67
+ acc += frac;
68
+ start += len;
69
+ }
70
+ return P - 1;
71
+ }
72
+
73
+ /**
74
+ * Scale a truecolor foreground escape toward black by `factor`. Returns
75
+ * undefined for 256-color escapes (no RGB to scale) so callers fall back to a
76
+ * dimmer theme color.
77
+ */
78
+ function darkenFgAnsi(ansi: string, factor: number): string | undefined {
79
+ const m = /38;2;(\d+);(\d+);(\d+)/.exec(ansi);
80
+ if (!m) return undefined;
81
+ const r = Math.round(Number(m[1]) * factor);
82
+ const g = Math.round(Number(m[2]) * factor);
83
+ const b = Math.round(Number(m[3]) * factor);
84
+ return `\x1b[38;2;${r};${g};${b}m`;
85
+ }
86
+
87
+ type BlockRow =
88
+ | { kind: "bar"; leftChar: string; rightChar: string; label?: string; meta?: string }
89
+ | { kind: "bottom"; leftChar: string; rightChar: string }
90
+ | { kind: "content"; inner: string }
91
+ | { kind: "sixel"; raw: string };
92
+
20
93
  export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): string[] {
21
94
  const { header, headerMeta, state, sections = [], width, applyBg = true } = options;
22
95
  const h = theme.boxSharp.horizontal;
@@ -46,62 +119,148 @@ export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): st
46
119
  };
47
120
  })();
48
121
 
49
- const buildBarLine = (leftChar: string, rightChar: string, label?: string, meta?: string): string => {
50
- const left = border(`${leftChar}${cap}`);
51
- const right = border(rightChar);
52
- if (lineWidth <= 0) return left + right;
53
- const labelText = [label, meta].filter(Boolean).join(theme.sep.dot);
54
- const rawLabel = labelText ? ` ${labelText} ` : " ";
55
- const leftWidth = visibleWidth(left);
56
- const rightWidth = visibleWidth(right);
57
- const maxLabelWidth = Math.max(0, lineWidth - leftWidth - rightWidth);
58
- const trimmedLabel = truncateToWidth(rawLabel, maxLabelWidth);
59
- const labelWidth = visibleWidth(trimmedLabel);
60
- const fillCount = Math.max(0, lineWidth - leftWidth - labelWidth - rightWidth);
61
- return `${left}${trimmedLabel}${border(h.repeat(fillCount))}${right}`;
62
- };
63
-
64
- const contentPrefix = border(`${v} `);
65
- const contentSuffix = border(v);
66
- const contentWidth = Math.max(0, lineWidth - visibleWidth(contentPrefix) - visibleWidth(contentSuffix));
67
- const lines: string[] = [];
122
+ const contentWidth = Math.max(0, lineWidth - visibleWidth(`${v} `) - visibleWidth(v));
68
123
 
69
- lines.push(
70
- padToWidth(buildBarLine(theme.boxSharp.topLeft, theme.boxSharp.topRight, header, headerMeta), lineWidth, bgFn),
71
- );
124
+ // ── Layout pass: collect row descriptors so the border perimeter length is
125
+ // known before the moving segment is positioned. ──
126
+ const rows: BlockRow[] = [];
127
+ rows.push({
128
+ kind: "bar",
129
+ leftChar: theme.boxSharp.topLeft,
130
+ rightChar: theme.boxSharp.topRight,
131
+ label: header,
132
+ meta: headerMeta,
133
+ });
72
134
 
73
- const hasSections = sections.length > 0;
74
- const normalizedSections = hasSections ? sections : [{ lines: [] }];
75
-
76
- for (let i = 0; i < normalizedSections.length; i++) {
77
- const section = normalizedSections[i];
135
+ const normalizedSections = sections.length > 0 ? sections : [{ lines: [] as string[] }];
136
+ for (const section of normalizedSections) {
78
137
  if (section.label) {
79
- lines.push(
80
- padToWidth(buildBarLine(theme.boxSharp.teeRight, theme.boxSharp.teeLeft, section.label), lineWidth, bgFn),
81
- );
138
+ rows.push({
139
+ kind: "bar",
140
+ leftChar: theme.boxSharp.teeRight,
141
+ rightChar: theme.boxSharp.teeLeft,
142
+ label: section.label,
143
+ });
82
144
  }
83
145
  const allLines = section.lines.flatMap(l => l.split("\n"));
84
146
  const sixelLineMask = TERMINAL.imageProtocol === ImageProtocol.Sixel ? getSixelLineMask(allLines) : undefined;
85
147
  for (let lineIndex = 0; lineIndex < allLines.length; lineIndex++) {
86
148
  const line = allLines[lineIndex]!;
87
149
  if (sixelLineMask?.[lineIndex]) {
88
- lines.push(line);
150
+ rows.push({ kind: "sixel", raw: line });
89
151
  continue;
90
152
  }
91
153
  const wrappedLines = wrapTextWithAnsi(line.trimEnd(), contentWidth);
92
154
  for (const wrappedLine of wrappedLines) {
93
155
  const innerPadding = padding(Math.max(0, contentWidth - visibleWidth(wrappedLine)));
94
- const fullLine = `${contentPrefix}${wrappedLine}${innerPadding}${contentSuffix}`;
95
- lines.push(padToWidth(fullLine, lineWidth, bgFn));
156
+ rows.push({ kind: "content", inner: `${wrappedLine}${innerPadding}` });
96
157
  }
97
158
  }
98
159
  }
99
160
 
100
- const bottomLeft = border(`${theme.boxSharp.bottomLeft}${cap}`);
101
- const bottomRight = border(theme.boxSharp.bottomRight);
102
- const bottomFillCount = Math.max(0, lineWidth - visibleWidth(bottomLeft) - visibleWidth(bottomRight));
103
- const bottomLine = `${bottomLeft}${border(h.repeat(bottomFillCount))}${bottomRight}`;
104
- lines.push(padToWidth(bottomLine, lineWidth, bgFn));
161
+ rows.push({ kind: "bottom", leftChar: theme.boxSharp.bottomLeft, rightChar: theme.boxSharp.bottomRight });
162
+
163
+ const H = rows.length;
164
+ const W = lineWidth;
165
+ const animate = (options.animate ?? false) && (state === "running" || state === "pending") && W >= 2 && H >= 2;
166
+
167
+ // ── Segment geometry: one dark run travels the outer edge clockwise,
168
+ // top → right → bottom → left → top. ──
169
+ const P = animate ? 2 * W + 2 * H - 4 : 0;
170
+ const segLen = Math.min(BORDER_SEGMENT_LEN, P);
171
+ const head = animate ? borderSegmentHead(W, H, Date.now()) : 0;
172
+ const segAnsi = animate ? (darkenFgAnsi(theme.getFgAnsi(borderColor), 0.4) ?? theme.getFgAnsi("borderMuted")) : "";
173
+ const seg = (text: string) => `${segAnsi}${text}\x1b[39m`;
174
+
175
+ // Perimeter index of border cell (row r, col c), clockwise from top-left.
176
+ const perimIndex = (r: number, c: number): number => {
177
+ if (r === 0) return c;
178
+ if (c === W - 1) return W - 1 + r;
179
+ if (r === H - 1) return W - 1 + (H - 1) + (W - 1 - c);
180
+ return W - 1 + (H - 1) + (W - 1) + (H - 1 - r);
181
+ };
182
+ const isLit = (idx: number): boolean => (((idx - head) % P) + P) % P < segLen;
183
+ // Color a run of border glyphs starting at (row r, col startCol), grouping
184
+ // consecutive same-state cells so each run emits a single escape pair.
185
+ const colorEdge = (glyphs: string, r: number, startCol: number): string => {
186
+ let out = "";
187
+ let runLit: boolean | null = null;
188
+ let buf = "";
189
+ for (let i = 0; i < glyphs.length; i++) {
190
+ const lit = isLit(perimIndex(r, startCol + i));
191
+ if (lit !== runLit) {
192
+ if (runLit !== null) out += (runLit ? seg : border)(buf);
193
+ buf = "";
194
+ runLit = lit;
195
+ }
196
+ buf += glyphs[i];
197
+ }
198
+ if (runLit !== null) out += (runLit ? seg : border)(buf);
199
+ return out;
200
+ };
201
+
202
+ const renderBar = (
203
+ row: { leftChar: string; rightChar: string; label?: string; meta?: string },
204
+ r: number,
205
+ ): string => {
206
+ const leftGlyphs = `${row.leftChar}${cap}`;
207
+ const rightGlyph = row.rightChar;
208
+ if (lineWidth <= 0) return border(leftGlyphs) + border(rightGlyph);
209
+ const labelText = [row.label, row.meta].filter(Boolean).join(theme.sep.dot);
210
+ const rawLabel = labelText ? ` ${labelText} ` : " ";
211
+ const leftWidth = visibleWidth(leftGlyphs);
212
+ const rightWidth = visibleWidth(rightGlyph);
213
+ const maxLabelWidth = Math.max(0, lineWidth - leftWidth - rightWidth);
214
+ const trimmedLabel = truncateToWidth(rawLabel, maxLabelWidth);
215
+ const labelWidth = visibleWidth(trimmedLabel);
216
+ const fillCount = Math.max(0, lineWidth - leftWidth - labelWidth - rightWidth);
217
+ const fillGlyphs = h.repeat(fillCount);
218
+ if (!animate) {
219
+ return `${border(leftGlyphs)}${trimmedLabel}${border(fillGlyphs)}${border(rightGlyph)}`;
220
+ }
221
+ if (r === 0 || r === H - 1) {
222
+ // Top/bottom edge: the whole horizontal run lies on the perimeter.
223
+ const leftStr = colorEdge(leftGlyphs, r, 0);
224
+ const fillStr = colorEdge(fillGlyphs, r, leftWidth + labelWidth);
225
+ const rightStr = colorEdge(rightGlyph, r, lineWidth - rightWidth);
226
+ return `${leftStr}${trimmedLabel}${fillStr}${rightStr}`;
227
+ }
228
+ // Interior separator: only the first/last cell sit on the outer edge.
229
+ return `${colorEdge(row.leftChar, r, 0)}${border(cap)}${trimmedLabel}${border(fillGlyphs)}${colorEdge(rightGlyph, r, lineWidth - rightWidth)}`;
230
+ };
231
+
232
+ const renderBottom = (row: { leftChar: string; rightChar: string }, r: number): string => {
233
+ const leftGlyphs = `${row.leftChar}${cap}`;
234
+ const rightGlyph = row.rightChar;
235
+ const fillCount = Math.max(0, lineWidth - visibleWidth(leftGlyphs) - visibleWidth(rightGlyph));
236
+ const fillGlyphs = h.repeat(fillCount);
237
+ if (!animate) return `${border(leftGlyphs)}${border(fillGlyphs)}${border(rightGlyph)}`;
238
+ const leftStr = colorEdge(leftGlyphs, r, 0);
239
+ const fillStr = colorEdge(fillGlyphs, r, visibleWidth(leftGlyphs));
240
+ const rightStr = colorEdge(rightGlyph, r, lineWidth - visibleWidth(rightGlyph));
241
+ return `${leftStr}${fillStr}${rightStr}`;
242
+ };
243
+
244
+ const renderContent = (inner: string, r: number): string => {
245
+ if (!animate) return `${border(`${v} `)}${inner}${border(v)}`;
246
+ return `${colorEdge(v, r, 0)} ${inner}${colorEdge(v, r, lineWidth - 1)}`;
247
+ };
248
+
249
+ const lines: string[] = [];
250
+ for (let r = 0; r < H; r++) {
251
+ const row = rows[r]!;
252
+ if (row.kind === "sixel") {
253
+ lines.push(row.raw);
254
+ continue;
255
+ }
256
+ const line =
257
+ row.kind === "bar"
258
+ ? renderBar(row, r)
259
+ : row.kind === "bottom"
260
+ ? renderBottom(row, r)
261
+ : renderContent(row.inner, r);
262
+ lines.push(padToWidth(line, lineWidth, bgFn));
263
+ }
105
264
 
106
265
  return lines;
107
266
  }
@@ -137,6 +296,8 @@ export class CachedOutputBlock {
137
296
  h.optional(options.headerMeta);
138
297
  h.optional(options.state);
139
298
  h.bool(options.applyBg ?? true);
299
+ h.bool(options.animate ?? false);
300
+ if (options.animate) h.u32(borderShimmerTick());
140
301
  if (options.sections) {
141
302
  for (const s of options.sections) {
142
303
  h.optional(s.label);
@@ -9,13 +9,16 @@ import type { ModelRegistry } from "../config/model-registry";
9
9
  import { resolveRoleSelection } from "../config/model-resolver";
10
10
  import type { Settings } from "../config/settings";
11
11
  import titleSystemPrompt from "../prompts/system/title-system.md" with { type: "text" };
12
+ import { ONLINE_TINY_TITLE_MODEL_KEY } from "../tiny/models";
13
+ import { formatTitleUserMessage, normalizeGeneratedTitle } from "../tiny/text";
14
+ import { tinyTitleClient } from "../tiny/title-client";
12
15
 
13
16
  const TITLE_SYSTEM_PROMPT = prompt.render(titleSystemPrompt);
14
17
 
15
18
  const DEFAULT_TERMINAL_TITLE = "π";
16
19
  const TERMINAL_TITLE_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f]/g;
17
20
 
18
- const MAX_INPUT_CHARS = 2000;
21
+ export const TITLE_LOCAL_FALLBACK_DELAY_MS = 10_000;
19
22
  const TITLE_MAX_TOKENS = 30;
20
23
  const REASONING_SAFE_MAX_TOKENS = 1024;
21
24
  const SET_TITLE_TOOL_NAME = "set_title";
@@ -48,6 +51,78 @@ function getTitleModel(registry: ModelRegistry, settings: Settings, currentModel
48
51
  return undefined;
49
52
  }
50
53
 
54
+ export async function raceFirstNonNull<T>(
55
+ primary: Promise<T | null>,
56
+ startFallback: () => Promise<T | null>,
57
+ delayMs: number = TITLE_LOCAL_FALLBACK_DELAY_MS,
58
+ onPrimaryWinAfterFallback?: () => void,
59
+ ): Promise<T | null> {
60
+ const { promise, resolve } = Promise.withResolvers<T | null>();
61
+ let resolved = false;
62
+ let primarySettled = false;
63
+ let fallbackStarted = false;
64
+ let fallbackSettled = false;
65
+
66
+ const resolveOnce = (value: T | null): void => {
67
+ if (resolved) return;
68
+ resolved = true;
69
+ resolve(value);
70
+ };
71
+ const maybeResolveNull = (): void => {
72
+ if (primarySettled && fallbackStarted && fallbackSettled) resolveOnce(null);
73
+ };
74
+ const startFallbackOnce = (): void => {
75
+ if (fallbackStarted || resolved) return;
76
+ fallbackStarted = true;
77
+ let fallback: Promise<T | null>;
78
+ try {
79
+ fallback = startFallback();
80
+ } catch {
81
+ fallbackSettled = true;
82
+ maybeResolveNull();
83
+ return;
84
+ }
85
+ void fallback.then(
86
+ value => {
87
+ fallbackSettled = true;
88
+ if (value !== null) resolveOnce(value);
89
+ else maybeResolveNull();
90
+ },
91
+ () => {
92
+ fallbackSettled = true;
93
+ maybeResolveNull();
94
+ },
95
+ );
96
+ };
97
+
98
+ const timer = setTimeout(startFallbackOnce, delayMs);
99
+ void primary.then(
100
+ value => {
101
+ primarySettled = true;
102
+ clearTimeout(timer);
103
+ if (value !== null) {
104
+ if (fallbackStarted) onPrimaryWinAfterFallback?.();
105
+ resolveOnce(value);
106
+ return;
107
+ }
108
+ startFallbackOnce();
109
+ maybeResolveNull();
110
+ },
111
+ () => {
112
+ primarySettled = true;
113
+ clearTimeout(timer);
114
+ startFallbackOnce();
115
+ maybeResolveNull();
116
+ },
117
+ );
118
+
119
+ try {
120
+ return await promise;
121
+ } finally {
122
+ clearTimeout(timer);
123
+ }
124
+ }
125
+
51
126
  /**
52
127
  * Generate a title for a session based on the first user message.
53
128
  *
@@ -68,6 +143,41 @@ export async function generateSessionTitle(
68
143
  sessionId?: string,
69
144
  currentModel?: Model<Api>,
70
145
  metadataResolver?: (provider: string) => Record<string, unknown> | undefined,
146
+ ): Promise<string | null> {
147
+ const tinyModel = settings.get("providers.tinyModel");
148
+ if (tinyModel === ONLINE_TINY_TITLE_MODEL_KEY) {
149
+ return generateTitleOnline(firstMessage, registry, settings, sessionId, currentModel, metadataResolver);
150
+ }
151
+
152
+ const onlineAbortController = new AbortController();
153
+ const localTitle = tinyTitleClient.generate(tinyModel, firstMessage).then(
154
+ title => title || null,
155
+ () => null,
156
+ );
157
+ const startOnline = (): Promise<string | null> =>
158
+ generateTitleOnline(
159
+ firstMessage,
160
+ registry,
161
+ settings,
162
+ sessionId,
163
+ currentModel,
164
+ metadataResolver,
165
+ onlineAbortController.signal,
166
+ );
167
+
168
+ return raceFirstNonNull(localTitle, startOnline, TITLE_LOCAL_FALLBACK_DELAY_MS, () => {
169
+ onlineAbortController.abort();
170
+ });
171
+ }
172
+
173
+ export async function generateTitleOnline(
174
+ firstMessage: string,
175
+ registry: ModelRegistry,
176
+ settings: Settings,
177
+ sessionId?: string,
178
+ currentModel?: Model<Api>,
179
+ metadataResolver?: (provider: string) => Record<string, unknown> | undefined,
180
+ signal?: AbortSignal,
71
181
  ): Promise<string | null> {
72
182
  const model = getTitleModel(registry, settings, currentModel);
73
183
  if (!model) {
@@ -75,12 +185,7 @@ export async function generateSessionTitle(
75
185
  return null;
76
186
  }
77
187
 
78
- // Truncate message if too long
79
- const truncatedMessage =
80
- firstMessage.length > MAX_INPUT_CHARS ? `${firstMessage.slice(0, MAX_INPUT_CHARS)}…` : firstMessage;
81
- const userMessage = `<user-message>
82
- ${truncatedMessage}
83
- </user-message>`;
188
+ const userMessage = formatTitleUserMessage(firstMessage);
84
189
 
85
190
  const apiKey = await registry.getApiKey(model, sessionId);
86
191
  if (!apiKey) {
@@ -122,6 +227,7 @@ ${truncatedMessage}
122
227
  disableReasoning: true,
123
228
  toolChoice: { type: "tool", name: SET_TITLE_TOOL_NAME },
124
229
  metadata,
230
+ signal,
125
231
  },
126
232
  );
127
233
 
@@ -134,7 +240,7 @@ ${truncatedMessage}
134
240
  return null;
135
241
  }
136
242
 
137
- const title = extractGeneratedTitle(response.content);
243
+ const title = normalizeGeneratedTitle(extractGeneratedTitle(response.content));
138
244
 
139
245
  logger.debug("title-generator: response", {
140
246
  model: request.model,
@@ -143,11 +249,7 @@ ${truncatedMessage}
143
249
  stopReason: response.stopReason,
144
250
  });
145
251
 
146
- if (!title) {
147
- return null;
148
- }
149
-
150
- return title.replace(/^["']|["']$/g, "").replace(/[.!?]$/, "");
252
+ return title;
151
253
  } catch (err) {
152
254
  logger.debug("title-generator: error", {
153
255
  model: request.model,
@@ -1,46 +0,0 @@
1
- import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
2
- import type { Component } from "@oh-my-pi/pi-tui";
3
- import * as z from "zod/v4";
4
- import type { RenderResultOptions } from "../../extensibility/custom-tools/types";
5
- import type { Theme } from "../../modes/theme/theme";
6
- import type { ToolSession } from "..";
7
- import { type BashRenderContext, type BashToolDetails } from "../bash";
8
- import { type RecipeRenderArgs } from "./render";
9
- import { type DetectedRunner } from "./runner";
10
- declare const recipeSchema: z.ZodObject<{
11
- op: z.ZodString;
12
- }, z.core.$strict>;
13
- type RecipeParams = z.infer<typeof recipeSchema>;
14
- type RecipeRenderResult = {
15
- content: Array<{
16
- type: string;
17
- text?: string;
18
- }>;
19
- details?: BashToolDetails;
20
- isError?: boolean;
21
- };
22
- export declare class RecipeTool implements AgentTool<typeof recipeSchema, BashToolDetails, Theme> {
23
- #private;
24
- readonly name = "recipe";
25
- readonly label = "Run";
26
- readonly approval: "exec";
27
- readonly description: string;
28
- readonly parameters: z.ZodObject<{
29
- op: z.ZodString;
30
- }, z.core.$strict>;
31
- readonly strict = true;
32
- readonly concurrency = "exclusive";
33
- readonly loadMode = "discoverable";
34
- readonly summary = "Execute a saved bash recipe (multi-step shell command preset)";
35
- readonly mergeCallAndResult = true;
36
- readonly inline = true;
37
- readonly renderCall: (args: RecipeRenderArgs, options: RenderResultOptions, uiTheme: Theme) => Component;
38
- readonly renderResult: (result: RecipeRenderResult, options: RenderResultOptions & {
39
- renderContext?: BashRenderContext;
40
- }, uiTheme: Theme, args?: RecipeRenderArgs) => Component;
41
- constructor(session: ToolSession, runners: DetectedRunner[]);
42
- static createIf(session: ToolSession): Promise<RecipeTool | null>;
43
- execute(toolCallId: string, { op }: RecipeParams, signal?: AbortSignal, onUpdate?: AgentToolUpdateCallback<BashToolDetails>, ctx?: AgentToolContext): Promise<AgentToolResult<BashToolDetails>>;
44
- }
45
- export * from "./runner";
46
- export { tasksFromCargoMetadata } from "./runners/cargo";
@@ -1,36 +0,0 @@
1
- import type { DetectedRunner } from "./runner";
2
- export interface RecipeRenderArgs {
3
- op?: string;
4
- __partialJson?: string;
5
- [key: string]: unknown;
6
- }
7
- export declare function createRecipeToolRenderer(runners: DetectedRunner[]): {
8
- renderCall(args: RecipeRenderArgs, options: import("../..").RenderResultOptions, uiTheme: import("../..").Theme): import("@oh-my-pi/pi-tui").Component;
9
- renderResult(result: {
10
- content: {
11
- type: string;
12
- text?: string;
13
- }[];
14
- details?: import("..").BashToolDetails;
15
- isError?: boolean;
16
- }, options: import("../..").RenderResultOptions & {
17
- renderContext?: import("..").BashRenderContext;
18
- }, uiTheme: import("../..").Theme, args?: RecipeRenderArgs | undefined): import("@oh-my-pi/pi-tui").Component;
19
- mergeCallAndResult: boolean;
20
- inline: boolean;
21
- };
22
- export declare const recipeToolRenderer: {
23
- renderCall(args: RecipeRenderArgs, options: import("../..").RenderResultOptions, uiTheme: import("../..").Theme): import("@oh-my-pi/pi-tui").Component;
24
- renderResult(result: {
25
- content: {
26
- type: string;
27
- text?: string;
28
- }[];
29
- details?: import("..").BashToolDetails;
30
- isError?: boolean;
31
- }, options: import("../..").RenderResultOptions & {
32
- renderContext?: import("..").BashRenderContext;
33
- }, uiTheme: import("../..").Theme, args?: RecipeRenderArgs | undefined): import("@oh-my-pi/pi-tui").Component;
34
- mergeCallAndResult: boolean;
35
- inline: boolean;
36
- };
@@ -1,60 +0,0 @@
1
- export interface RunnerTask {
2
- name: string;
3
- doc?: string;
4
- /** Parameter names only; used for the `name foo bar` signature line in the description. */
5
- parameters: string[];
6
- /** Override for this specific task, e.g. `cargo run --package crate --bin`. */
7
- commandPrefix?: string;
8
- /** Token passed to the runner command; defaults to `name`. Used when display names are namespaced. */
9
- commandName?: string;
10
- /** Working directory for the task, relative to the session cwd; absent means the runner's root cwd. */
11
- cwd?: string;
12
- }
13
- export interface DetectedRunner {
14
- id: string;
15
- label: string;
16
- /** Resolved shell prefix, e.g. "just" or "bun run" or "make". */
17
- commandPrefix: string;
18
- tasks: RunnerTask[];
19
- }
20
- export interface TaskRunner {
21
- id: string;
22
- label: string;
23
- /**
24
- * Probe `cwd` for the manifest, the binary, and the task list.
25
- * Returns null when this runner does not apply.
26
- */
27
- detect(cwd: string): Promise<DetectedRunner | null>;
28
- }
29
- interface PromptTaskModel {
30
- name: string;
31
- paramSig?: string;
32
- command?: string;
33
- doc?: string;
34
- cwd?: string;
35
- }
36
- interface PromptRunnerModel {
37
- id: string;
38
- label: string;
39
- commandPrefix: string;
40
- tasks: PromptTaskModel[];
41
- hiddenTaskCount?: number;
42
- }
43
- export interface RecipePromptModel {
44
- [key: string]: unknown;
45
- hasMultipleRunners: boolean;
46
- ambiguityExampleRunner?: string;
47
- ambiguityExampleTask?: string;
48
- runners: PromptRunnerModel[];
49
- }
50
- export interface ResolvedTask {
51
- command: string;
52
- cwd?: string;
53
- }
54
- export declare function resolveCommand(op: string, runners: DetectedRunner[]): ResolvedTask;
55
- export declare function resolveTaskFromOp(op: string | undefined, runners: DetectedRunner[]): ResolvedTask | undefined;
56
- export declare function commandFromOp(op: string | undefined, runners: DetectedRunner[]): string | undefined;
57
- export declare function cwdFromOp(op: string | undefined, runners: DetectedRunner[]): string | undefined;
58
- export declare function titleFromOp(op: string | undefined, runners: DetectedRunner[]): string;
59
- export declare function buildPromptModel(runners: DetectedRunner[]): RecipePromptModel;
60
- export {};
@@ -1,16 +0,0 @@
1
- import type { RunnerTask, TaskRunner } from "../runner";
2
- export interface CargoMetadataTarget {
3
- kind?: string[];
4
- name?: string;
5
- }
6
- export interface CargoMetadataPackage {
7
- id?: string;
8
- name?: string;
9
- targets?: CargoMetadataTarget[];
10
- }
11
- export interface CargoMetadata {
12
- packages?: CargoMetadataPackage[];
13
- workspace_members?: string[];
14
- }
15
- export declare function tasksFromCargoMetadata(metadata: CargoMetadata): RunnerTask[];
16
- export declare const cargoRunner: TaskRunner;
@@ -1,2 +0,0 @@
1
- import type { TaskRunner } from "../runner";
2
- export declare const RUNNERS: TaskRunner[];
@@ -1,2 +0,0 @@
1
- import type { TaskRunner } from "../runner";
2
- export declare const justRunner: TaskRunner;
@@ -1,2 +0,0 @@
1
- import type { TaskRunner } from "../runner";
2
- export declare const makeRunner: TaskRunner;
@@ -1,2 +0,0 @@
1
- import type { TaskRunner } from "../runner";
2
- export declare const pkgRunner: TaskRunner;
@@ -1,2 +0,0 @@
1
- import type { TaskRunner } from "../runner";
2
- export declare const taskRunner: TaskRunner;
@@ -1,16 +0,0 @@
1
- Run a recipe / script / target from the project's task runners.
2
-
3
- <instruction>
4
- - `op` is a single string: task name plus any args, e.g. `{op: "test"}` or `{op: "build --release"}`.
5
- - In monorepos, package and Cargo target tasks are namespaced with `/`, e.g. `{op: "pkg-a/test"}` or `{op: "crate/bin/server"}`.
6
- {{#if hasMultipleRunners}}- When the same task name exists in more than one runner, prefix with the runner id, e.g. `{op: "{{ambiguityExampleRunner}}:{{ambiguityExampleTask}}"}`. The available runner ids are: {{#each runners}}`{{id}}`{{#unless @last}}, {{/unless}}{{/each}}.
7
- {{/if}}- Runs in the session's cwd. Output and exit code are returned in the same shape as `bash`.
8
- </instruction>
9
-
10
- {{#each runners}}
11
- <runner id="{{id}}" label="{{label}}" command="{{commandPrefix}}">
12
- {{#each tasks}}
13
- - `{{name}}{{#if paramSig}} {{paramSig}}{{/if}}`{{#if doc}} — {{doc}}{{/if}}{{#if command}} (`{{command}}`{{#if cwd}} in `{{cwd}}`{{/if}}){{/if}}
14
- {{/each}}
15
- </runner>
16
- {{/each}}