@dungle-scrubs/tallow 0.9.3 → 0.9.6

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 (207) hide show
  1. package/dist/cli.js +7 -4
  2. package/dist/cli.js.map +1 -1
  3. package/dist/config.d.ts +1 -1
  4. package/dist/config.js +1 -1
  5. package/dist/interactive-mode-patch.d.ts +24 -10
  6. package/dist/interactive-mode-patch.d.ts.map +1 -1
  7. package/dist/interactive-mode-patch.js +285 -148
  8. package/dist/interactive-mode-patch.js.map +1 -1
  9. package/dist/interactive-reset.d.ts +49 -0
  10. package/dist/interactive-reset.d.ts.map +1 -0
  11. package/dist/interactive-reset.js +40 -0
  12. package/dist/interactive-reset.js.map +1 -0
  13. package/dist/pi-tui-editor-patch.d.ts +10 -0
  14. package/dist/pi-tui-editor-patch.d.ts.map +1 -0
  15. package/dist/pi-tui-editor-patch.js +159 -0
  16. package/dist/pi-tui-editor-patch.js.map +1 -0
  17. package/dist/pi-tui-patch.d.ts +2 -0
  18. package/dist/pi-tui-patch.d.ts.map +1 -0
  19. package/dist/pi-tui-patch.js +563 -0
  20. package/dist/pi-tui-patch.js.map +1 -0
  21. package/dist/pi-tui-settings-list-patch.d.ts +11 -0
  22. package/dist/pi-tui-settings-list-patch.d.ts.map +1 -0
  23. package/dist/pi-tui-settings-list-patch.js +38 -0
  24. package/dist/pi-tui-settings-list-patch.js.map +1 -0
  25. package/dist/reset-diagnostics.d.ts +69 -0
  26. package/dist/reset-diagnostics.d.ts.map +1 -0
  27. package/dist/reset-diagnostics.js +41 -0
  28. package/dist/reset-diagnostics.js.map +1 -0
  29. package/dist/sdk.d.ts +5 -21
  30. package/dist/sdk.d.ts.map +1 -1
  31. package/dist/sdk.js +180 -149
  32. package/dist/sdk.js.map +1 -1
  33. package/dist/workspace-transition-interactive.d.ts +1 -0
  34. package/dist/workspace-transition-interactive.d.ts.map +1 -1
  35. package/dist/workspace-transition-interactive.js +7 -17
  36. package/dist/workspace-transition-interactive.js.map +1 -1
  37. package/extensions/__integration__/audit-findings.test.ts +6 -16
  38. package/extensions/__integration__/teams-runtime.test.ts +4 -1
  39. package/extensions/_icons/index.ts +2 -4
  40. package/extensions/_shared/__tests__/image-metadata.test.ts +33 -0
  41. package/extensions/_shared/__tests__/terminal-links.test.ts +18 -0
  42. package/extensions/_shared/image-metadata.ts +99 -0
  43. package/extensions/_shared/inline-preview.ts +1 -1
  44. package/extensions/_shared/pid-registry.ts +0 -1
  45. package/extensions/_shared/terminal-links.ts +22 -0
  46. package/extensions/ask-user-question-tool/index.ts +0 -3
  47. package/extensions/clear/__tests__/clear.test.ts +270 -3
  48. package/extensions/command-expansion/index.ts +1 -1
  49. package/extensions/context-files/index.ts +5 -1
  50. package/extensions/context-fork/__tests__/context-fork.test.ts +94 -1
  51. package/extensions/context-fork/extension.json +1 -1
  52. package/extensions/context-fork/index.ts +32 -0
  53. package/extensions/edit-tool-enhanced/index.ts +2 -1
  54. package/extensions/hooks/index.ts +33 -11
  55. package/extensions/loop/index.ts +14 -1
  56. package/extensions/lsp/index.ts +64 -13
  57. package/extensions/lsp/package.json +2 -2
  58. package/extensions/permissions/__tests__/permissions.test.ts +4 -4
  59. package/extensions/random-spinner/index.ts +7 -642
  60. package/extensions/read-tool-enhanced/index.ts +6 -8
  61. package/extensions/render-stabilizer/__tests__/render-stabilizer.test.ts +4 -5
  62. package/extensions/render-stabilizer/index.ts +6 -6
  63. package/extensions/show-system-prompt/__tests__/show-system-prompt.test.ts +1 -1
  64. package/extensions/slash-command-bridge/__tests__/slash-command-bridge.test.ts +26 -0
  65. package/extensions/slash-command-bridge/index.ts +14 -2
  66. package/extensions/subagent-tool/index.ts +1 -1
  67. package/extensions/subagent-tool/model-resolver.ts +274 -7
  68. package/extensions/tasks/__tests__/state-ui.test.ts +3 -3
  69. package/extensions/tasks/__tests__/widget-subagents.test.ts +2 -2
  70. package/extensions/tasks/commands/register-tasks-extension.ts +10 -10
  71. package/extensions/tasks/state/index.ts +1 -1
  72. package/extensions/tasks/ui/index.ts +2 -7
  73. package/extensions/teams-tool/tools/register-extension.ts +1 -3
  74. package/extensions/web-search-tool/index.ts +2 -1
  75. package/extensions/write-tool-enhanced/__tests__/write-tool-enhanced.test.ts +21 -6
  76. package/extensions/write-tool-enhanced/index.ts +2 -1
  77. package/node_modules/@mariozechner/pi-tui/README.md +56 -34
  78. package/node_modules/@mariozechner/pi-tui/dist/autocomplete.d.ts +18 -13
  79. package/node_modules/@mariozechner/pi-tui/dist/autocomplete.d.ts.map +1 -1
  80. package/node_modules/@mariozechner/pi-tui/dist/autocomplete.js +182 -113
  81. package/node_modules/@mariozechner/pi-tui/dist/autocomplete.js.map +1 -1
  82. package/node_modules/@mariozechner/pi-tui/dist/components/cancellable-loader.js +3 -3
  83. package/node_modules/@mariozechner/pi-tui/dist/components/cancellable-loader.js.map +1 -1
  84. package/node_modules/@mariozechner/pi-tui/dist/components/editor.d.ts +45 -36
  85. package/node_modules/@mariozechner/pi-tui/dist/components/editor.d.ts.map +1 -1
  86. package/node_modules/@mariozechner/pi-tui/dist/components/editor.js +489 -325
  87. package/node_modules/@mariozechner/pi-tui/dist/components/editor.js.map +1 -1
  88. package/node_modules/@mariozechner/pi-tui/dist/components/image.d.ts +1 -99
  89. package/node_modules/@mariozechner/pi-tui/dist/components/image.d.ts.map +1 -1
  90. package/node_modules/@mariozechner/pi-tui/dist/components/image.js +17 -192
  91. package/node_modules/@mariozechner/pi-tui/dist/components/image.js.map +1 -1
  92. package/node_modules/@mariozechner/pi-tui/dist/components/input.d.ts.map +1 -1
  93. package/node_modules/@mariozechner/pi-tui/dist/components/input.js +57 -60
  94. package/node_modules/@mariozechner/pi-tui/dist/components/input.js.map +1 -1
  95. package/node_modules/@mariozechner/pi-tui/dist/components/loader.d.ts +2 -69
  96. package/node_modules/@mariozechner/pi-tui/dist/components/loader.d.ts.map +1 -1
  97. package/node_modules/@mariozechner/pi-tui/dist/components/loader.js +5 -102
  98. package/node_modules/@mariozechner/pi-tui/dist/components/loader.js.map +1 -1
  99. package/node_modules/@mariozechner/pi-tui/dist/components/markdown.d.ts.map +1 -1
  100. package/node_modules/@mariozechner/pi-tui/dist/components/markdown.js +111 -53
  101. package/node_modules/@mariozechner/pi-tui/dist/components/markdown.js.map +1 -1
  102. package/node_modules/@mariozechner/pi-tui/dist/components/select-list.d.ts +19 -1
  103. package/node_modules/@mariozechner/pi-tui/dist/components/select-list.d.ts.map +1 -1
  104. package/node_modules/@mariozechner/pi-tui/dist/components/select-list.js +78 -67
  105. package/node_modules/@mariozechner/pi-tui/dist/components/select-list.js.map +1 -1
  106. package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.d.ts +0 -2
  107. package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.d.ts.map +1 -1
  108. package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.js +12 -23
  109. package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.js.map +1 -1
  110. package/node_modules/@mariozechner/pi-tui/dist/index.d.ts +8 -10
  111. package/node_modules/@mariozechner/pi-tui/dist/index.d.ts.map +1 -1
  112. package/node_modules/@mariozechner/pi-tui/dist/index.js +6 -9
  113. package/node_modules/@mariozechner/pi-tui/dist/index.js.map +1 -1
  114. package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts +108 -238
  115. package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts.map +1 -1
  116. package/node_modules/@mariozechner/pi-tui/dist/keybindings.js +108 -365
  117. package/node_modules/@mariozechner/pi-tui/dist/keybindings.js.map +1 -1
  118. package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts +33 -48
  119. package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts.map +1 -1
  120. package/node_modules/@mariozechner/pi-tui/dist/keys.js +239 -155
  121. package/node_modules/@mariozechner/pi-tui/dist/keys.js.map +1 -1
  122. package/node_modules/@mariozechner/pi-tui/dist/terminal-image.d.ts +14 -94
  123. package/node_modules/@mariozechner/pi-tui/dist/terminal-image.d.ts.map +1 -1
  124. package/node_modules/@mariozechner/pi-tui/dist/terminal-image.js +44 -186
  125. package/node_modules/@mariozechner/pi-tui/dist/terminal-image.js.map +1 -1
  126. package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts +13 -58
  127. package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts.map +1 -1
  128. package/node_modules/@mariozechner/pi-tui/dist/terminal.js +78 -111
  129. package/node_modules/@mariozechner/pi-tui/dist/terminal.js.map +1 -1
  130. package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts +24 -110
  131. package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts.map +1 -1
  132. package/node_modules/@mariozechner/pi-tui/dist/tui.js +188 -435
  133. package/node_modules/@mariozechner/pi-tui/dist/tui.js.map +1 -1
  134. package/node_modules/@mariozechner/pi-tui/dist/utils.d.ts +0 -18
  135. package/node_modules/@mariozechner/pi-tui/dist/utils.d.ts.map +1 -1
  136. package/node_modules/@mariozechner/pi-tui/dist/utils.js +251 -119
  137. package/node_modules/@mariozechner/pi-tui/dist/utils.js.map +1 -1
  138. package/node_modules/@mariozechner/pi-tui/package.json +6 -6
  139. package/node_modules/@mariozechner/pi-tui/src/__tests__/__snapshots__/render.test.ts.snap +3 -40
  140. package/node_modules/@mariozechner/pi-tui/src/__tests__/image-component.test.ts +71 -81
  141. package/node_modules/@mariozechner/pi-tui/src/__tests__/render.test.ts +0 -33
  142. package/node_modules/@mariozechner/pi-tui/src/__tests__/terminal-image.test.ts +93 -334
  143. package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-render-scheduling.test.ts +1 -1
  144. package/node_modules/@mariozechner/pi-tui/src/__tests__/utils.test.ts +11 -196
  145. package/node_modules/@mariozechner/pi-tui/src/autocomplete.ts +228 -142
  146. package/node_modules/@mariozechner/pi-tui/src/components/cancellable-loader.ts +3 -3
  147. package/node_modules/@mariozechner/pi-tui/src/components/editor.ts +624 -390
  148. package/node_modules/@mariozechner/pi-tui/src/components/image.ts +17 -227
  149. package/node_modules/@mariozechner/pi-tui/src/components/input.ts +71 -63
  150. package/node_modules/@mariozechner/pi-tui/src/components/loader.ts +5 -137
  151. package/node_modules/@mariozechner/pi-tui/src/components/markdown.ts +143 -52
  152. package/node_modules/@mariozechner/pi-tui/src/components/select-list.ts +136 -70
  153. package/node_modules/@mariozechner/pi-tui/src/components/settings-list.ts +11 -23
  154. package/node_modules/@mariozechner/pi-tui/src/index.ts +17 -36
  155. package/node_modules/@mariozechner/pi-tui/src/keybindings.ts +148 -421
  156. package/node_modules/@mariozechner/pi-tui/src/keys.ts +253 -181
  157. package/node_modules/@mariozechner/pi-tui/src/terminal-image.ts +51 -252
  158. package/node_modules/@mariozechner/pi-tui/src/terminal.ts +78 -133
  159. package/node_modules/@mariozechner/pi-tui/src/tui.ts +202 -478
  160. package/node_modules/@mariozechner/pi-tui/src/utils.ts +289 -125
  161. package/node_modules/@mariozechner/pi-tui/tsconfig.build.json +1 -0
  162. package/package.json +13 -13
  163. package/packages/tallow-tui/node_modules/@types/mime-types/README.md +8 -2
  164. package/packages/tallow-tui/node_modules/@types/mime-types/index.d.ts +6 -0
  165. package/packages/tallow-tui/node_modules/@types/mime-types/package.json +9 -3
  166. package/packages/tallow-tui/node_modules/get-east-asian-width/lookup-data.js +18 -0
  167. package/packages/tallow-tui/node_modules/get-east-asian-width/lookup.js +116 -384
  168. package/packages/tallow-tui/node_modules/get-east-asian-width/package.json +5 -4
  169. package/packages/tallow-tui/node_modules/get-east-asian-width/utilities.js +24 -0
  170. package/packages/tallow-tui/node_modules/marked/README.md +5 -4
  171. package/packages/tallow-tui/node_modules/marked/bin/main.js +10 -8
  172. package/packages/tallow-tui/node_modules/marked/bin/marked.js +2 -1
  173. package/packages/tallow-tui/node_modules/marked/lib/marked.d.ts +156 -125
  174. package/packages/tallow-tui/node_modules/marked/lib/marked.esm.js +67 -2179
  175. package/packages/tallow-tui/node_modules/marked/lib/marked.esm.js.map +3 -3
  176. package/packages/tallow-tui/node_modules/marked/lib/marked.umd.js +67 -2201
  177. package/packages/tallow-tui/node_modules/marked/lib/marked.umd.js.map +3 -3
  178. package/packages/tallow-tui/node_modules/marked/man/marked.1 +4 -2
  179. package/packages/tallow-tui/node_modules/marked/man/marked.1.md +2 -1
  180. package/packages/tallow-tui/node_modules/marked/package.json +26 -34
  181. package/runtime/model-metadata-overrides.ts +10 -1
  182. package/runtime/pid-schema.ts +26 -6
  183. package/skills/tallow-expert/SKILL.md +1 -3
  184. package/node_modules/@mariozechner/pi-tui/dist/border-styles.d.ts +0 -32
  185. package/node_modules/@mariozechner/pi-tui/dist/border-styles.d.ts.map +0 -1
  186. package/node_modules/@mariozechner/pi-tui/dist/border-styles.js +0 -46
  187. package/node_modules/@mariozechner/pi-tui/dist/border-styles.js.map +0 -1
  188. package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.d.ts +0 -52
  189. package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.d.ts.map +0 -1
  190. package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.js +0 -89
  191. package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.js.map +0 -1
  192. package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.d.ts +0 -14
  193. package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.d.ts.map +0 -1
  194. package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.js +0 -55
  195. package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.js.map +0 -1
  196. package/node_modules/@mariozechner/pi-tui/src/__tests__/editor-change-listener.test.ts +0 -121
  197. package/node_modules/@mariozechner/pi-tui/src/__tests__/editor-ghost-text.test.ts +0 -112
  198. package/node_modules/@mariozechner/pi-tui/src/__tests__/mouse-events.test.ts +0 -134
  199. package/node_modules/@mariozechner/pi-tui/src/__tests__/settings-list.test.ts +0 -49
  200. package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-diff-regression.test.ts +0 -555
  201. package/node_modules/@mariozechner/pi-tui/src/border-styles.ts +0 -60
  202. package/node_modules/@mariozechner/pi-tui/src/components/bordered-box.ts +0 -113
  203. package/node_modules/@mariozechner/pi-tui/src/test-utils/capability-env.ts +0 -56
  204. package/packages/tallow-tui/node_modules/marked/lib/marked.cjs +0 -2211
  205. package/packages/tallow-tui/node_modules/marked/lib/marked.cjs.map +0 -7
  206. package/packages/tallow-tui/node_modules/marked/lib/marked.d.cts +0 -728
  207. package/packages/tallow-tui/node_modules/marked/marked.min.js +0 -69
@@ -43,15 +43,15 @@ function captureTuiRef(ui: {
43
43
  /**
44
44
  * Register render stabilization hooks.
45
45
  *
46
+ * The shared reset helper now owns reset-grace handling for the main
47
+ * interactive reset paths. This extension remains as a compatibility shim
48
+ * for any remaining session-switch surfaces that still depend on an
49
+ * extension-side hook.
50
+ *
46
51
  * @param pi - Extension API
47
52
  */
48
53
  export default function renderStabilizerExtension(pi: ExtensionAPI): void {
49
- // Capture the TUI reference on first session_start
50
- pi.on("session_start", async (_event, ctx) => {
51
- captureTuiRef(ctx.ui);
52
- });
53
-
54
- // Reset the render grace period before a session switch so the
54
+ // Reset the render grace period before a legacy session switch so the
55
55
  // chatContainer.clear() → renderInitialMessages() transition uses
56
56
  // gentle line-by-line redraws instead of screen-clearing redraws.
57
57
  pi.on("session_before_switch", async (_event, ctx) => {
@@ -40,7 +40,7 @@ describe("show-system-prompt extension", () => {
40
40
  logged.push(args.join(" "));
41
41
  };
42
42
  try {
43
- await handler!("", ctx);
43
+ await handler?.("", ctx);
44
44
  } finally {
45
45
  console.log = origLog;
46
46
  }
@@ -13,6 +13,10 @@ import type {
13
13
  ExtensionUIContext,
14
14
  TurnEndEvent,
15
15
  } from "@mariozechner/pi-coding-agent";
16
+ import {
17
+ getResetDiagnosticsForTests,
18
+ resetResetDiagnosticsForTests,
19
+ } from "../../../src/reset-diagnostics.js";
16
20
  import { ExtensionHarness } from "../../../test-utils/extension-harness.js";
17
21
  import { ManualTimerScheduler } from "../../../test-utils/manual-timer-scheduler.js";
18
22
  import slashCommandBridge, {
@@ -40,6 +44,7 @@ beforeEach(async () => {
40
44
  });
41
45
 
42
46
  afterEach(() => {
47
+ resetResetDiagnosticsForTests();
43
48
  resetSlashCommandBridgeStateForTests();
44
49
  });
45
50
 
@@ -314,6 +319,11 @@ describe("compact", () => {
314
319
  const continuation = harness.sentMessages.find(
315
320
  (message) => message.customType === "compact-continue"
316
321
  );
322
+ expect(
323
+ getResetDiagnosticsForTests().some(
324
+ (event) => event.kind === "deferred_registered" && event.source === "slash-command-bridge"
325
+ )
326
+ ).toBe(true);
317
327
  expect(continuation?.display).toBe(false);
318
328
  expect(continuation?.options?.triggerTurn).toBe(true);
319
329
  expect(continuation?.content).toContain("compaction is complete");
@@ -344,6 +354,14 @@ describe("compact", () => {
344
354
  await harness.fireEvent("turn_start", { type: "turn_start", turnIndex: 0, timestamp: 0 }, ctx);
345
355
  scheduler.advanceBy(200);
346
356
 
357
+ expect(
358
+ getResetDiagnosticsForTests().some(
359
+ (event) =>
360
+ event.kind === "deferred_cancelled" &&
361
+ event.source === "slash-command-bridge" &&
362
+ event.reason === "turn_start"
363
+ )
364
+ ).toBe(true);
347
365
  expect(harness.sentMessages).toHaveLength(0);
348
366
  expect(widgetUpdates.at(-1)).toEqual({ key: "compact-progress", content: undefined });
349
367
  });
@@ -374,6 +392,14 @@ describe("compact", () => {
374
392
  compactOptions?.onComplete?.();
375
393
  scheduler.advanceBy(200);
376
394
 
395
+ expect(
396
+ getResetDiagnosticsForTests().some(
397
+ (event) =>
398
+ event.kind === "deferred_dropped" &&
399
+ event.source === "slash-command-bridge" &&
400
+ event.reason === "session_not_idle"
401
+ )
402
+ ).toBe(true);
377
403
  expect(harness.sentMessages).toHaveLength(0);
378
404
  expect(widgetUpdates.at(-1)).toEqual({ key: "compact-progress", content: undefined });
379
405
  expect(workingMessages.at(-1)).toBeUndefined();
@@ -17,6 +17,7 @@ import type {
17
17
  } from "@mariozechner/pi-coding-agent";
18
18
  import { Text } from "@mariozechner/pi-tui";
19
19
  import { Type } from "@sinclair/typebox";
20
+ import { recordResetDiagnostic } from "../../src/reset-diagnostics.js";
20
21
 
21
22
  /**
22
23
  * Deferred compact request — set by the tool handler, consumed on the first
@@ -154,13 +155,18 @@ function stopCompactProgress(ctx?: ExtensionContext): void {
154
155
  *
155
156
  * @returns Nothing
156
157
  */
157
- function clearContinuationTimer(): void {
158
+ function clearContinuationTimer(reason?: string): void {
158
159
  if (!continuationTimer) {
159
160
  return;
160
161
  }
161
162
 
162
163
  timerScheduler.clearTimeout(continuationTimer);
163
164
  continuationTimer = null;
165
+ recordResetDiagnostic({
166
+ kind: "deferred_cancelled",
167
+ reason: reason ?? "clear_continuation_timer",
168
+ source: "slash-command-bridge",
169
+ });
164
170
  }
165
171
 
166
172
  /**
@@ -338,6 +344,7 @@ function startDeferredCompact(
338
344
  // 200ms gives session.prompt()'s async setup (API key resolution,
339
345
  // compaction check) time to settle. The turn_start listener cancels
340
346
  // this timer if a turn starts before it fires (defense-in-depth).
347
+ recordResetDiagnostic({ kind: "deferred_registered", source: "slash-command-bridge" });
341
348
  continuationTimer = timerScheduler.setTimeout(() => {
342
349
  continuationTimer = null;
343
350
  if (ctx.isIdle()) {
@@ -354,6 +361,11 @@ function startDeferredCompact(
354
361
  } else {
355
362
  // User sent a message during compaction — their turn is
356
363
  // handling things, clean up our indicators.
364
+ recordResetDiagnostic({
365
+ kind: "deferred_dropped",
366
+ reason: "session_not_idle",
367
+ source: "slash-command-bridge",
368
+ });
357
369
  resumingAfterCompact = false;
358
370
  ctx.ui?.setWidget?.("compact-progress", undefined);
359
371
  ctx.ui?.setWorkingMessage?.();
@@ -601,7 +613,7 @@ WHEN NOT TO USE:
601
613
  * now active and showing the pending working message ("Resuming task…").
602
614
  */
603
615
  pi.on("turn_start", (_event, ctx) => {
604
- clearContinuationTimer();
616
+ clearContinuationTimer("turn_start");
605
617
  if (!resumingAfterCompact) return;
606
618
  resumingAfterCompact = false;
607
619
  ctx.ui?.setWidget?.("compact-progress", undefined);
@@ -1412,7 +1412,7 @@ function renderSubagentCall(args: Record<string, unknown>, theme: Theme) {
1412
1412
  return new Text(lines.join("\n"), 0, 0);
1413
1413
  }
1414
1414
 
1415
- const agentName = (args.agent as string) || "...";
1415
+ const _agentName = (args.agent as string) || "...";
1416
1416
  const task = typeof args.task === "string" ? args.task : "...";
1417
1417
  // Single mode: skip the redundant "subagent single" header — the result
1418
1418
  // renderer already shows "subagent running <duration> <agent>" with a spinner.
@@ -1,12 +1,279 @@
1
1
  /**
2
- * Fuzzy model name resolution re-exports from synapse.
2
+ * Deterministic fuzzy model resolution for subagents.
3
+ *
4
+ * We keep a local implementation instead of delegating straight to synapse
5
+ * because the direct dependency path was flaky under Linux CI in this repo.
6
+ * The behavior mirrors synapse's public resolver cascade closely enough for
7
+ * runtime callers and keeps tests deterministic across platforms.
3
8
  *
4
9
  * @module
5
10
  */
6
11
 
7
- export type { ModelSource, ResolvedModel } from "@dungle-scrubs/synapse";
8
- export {
9
- listAvailableModels,
10
- resolveModelCandidates,
11
- resolveModelFuzzy,
12
- } from "@dungle-scrubs/synapse";
12
+ import type { CandidateModel, ModelSource, ResolvedModel } from "@dungle-scrubs/synapse";
13
+ import { getModels, getProviders } from "@mariozechner/pi-ai";
14
+
15
+ export type { ModelSource, ResolvedModel };
16
+
17
+ /**
18
+ * Collect all models from the registered pi-ai providers.
19
+ *
20
+ * @returns Flat list of candidate models
21
+ */
22
+ function getAllModels(): CandidateModel[] {
23
+ const result: CandidateModel[] = [];
24
+ for (const provider of getProviders()) {
25
+ for (const model of getModels(provider)) {
26
+ result.push({ provider: model.provider, id: model.id, name: model.name });
27
+ }
28
+ }
29
+ return result;
30
+ }
31
+
32
+ /**
33
+ * Convert a candidate model into the resolved public shape.
34
+ *
35
+ * @param model - Candidate model
36
+ * @returns Resolved model descriptor
37
+ */
38
+ function toResolved(model: CandidateModel): ResolvedModel {
39
+ return {
40
+ displayName: `${model.provider}/${model.id}`,
41
+ id: model.id,
42
+ provider: model.provider,
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Tokenize a query or model identifier for overlap matching.
48
+ *
49
+ * @param value - Raw query or identifier
50
+ * @returns Lowercase tokens split on separators and digit boundaries
51
+ */
52
+ function tokenize(value: string): string[] {
53
+ return value
54
+ .toLowerCase()
55
+ .replace(/([a-z])(\d)/g, "$1 $2")
56
+ .replace(/(\d)([a-z])/g, "$1 $2")
57
+ .split(/[\s\-_.]+/)
58
+ .filter((token) => token.length > 0);
59
+ }
60
+
61
+ /**
62
+ * Normalize a query or identifier by removing separators.
63
+ *
64
+ * @param value - Raw query or identifier
65
+ * @returns Lowercase normalized string
66
+ */
67
+ function normalize(value: string): string {
68
+ return value.toLowerCase().replace(/[\s\-_.]+/g, "");
69
+ }
70
+
71
+ /**
72
+ * Compare model IDs using numeric-aware sorting.
73
+ *
74
+ * @param a - First model ID
75
+ * @param b - Second model ID
76
+ * @returns Positive when a is newer/higher than b
77
+ */
78
+ function compareModelIds(a: string, b: string): number {
79
+ return a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" });
80
+ }
81
+
82
+ /**
83
+ * Build a provider-priority lookup map.
84
+ *
85
+ * @param preferredProviders - Ordered provider preference list
86
+ * @returns Provider -> priority map (lower is better)
87
+ */
88
+ function buildProviderPreferenceMap(
89
+ preferredProviders: readonly string[] | undefined
90
+ ): ReadonlyMap<string, number> {
91
+ const priorities = new Map<string, number>();
92
+ for (const [index, provider] of (preferredProviders ?? []).entries()) {
93
+ priorities.set(provider.toLowerCase(), index);
94
+ }
95
+ return priorities;
96
+ }
97
+
98
+ /**
99
+ * Resolve one provider's priority, defaulting unknown providers last.
100
+ *
101
+ * @param preferenceMap - Provider priority map
102
+ * @param provider - Provider to score
103
+ * @returns Numeric priority (lower is better)
104
+ */
105
+ function getProviderPriority(preferenceMap: ReadonlyMap<string, number>, provider: string): number {
106
+ return preferenceMap.get(provider.toLowerCase()) ?? Number.MAX_SAFE_INTEGER;
107
+ }
108
+
109
+ /**
110
+ * Pick the best candidate by ID quality only.
111
+ *
112
+ * Tiebreak order:
113
+ * 1. Higher numeric-aware model ID
114
+ * 2. Shorter ID when numeric ordering ties
115
+ * 3. Lexicographically last as deterministic fallback
116
+ *
117
+ * @param models - Candidate models
118
+ * @returns Best candidate
119
+ */
120
+ function pickBestModel(models: readonly CandidateModel[]): CandidateModel {
121
+ return models.reduce((best, current) => {
122
+ const versionDiff = compareModelIds(best.id, current.id);
123
+ if (versionDiff !== 0) {
124
+ return versionDiff > 0 ? best : current;
125
+ }
126
+ if (best.id.length !== current.id.length) {
127
+ return best.id.length < current.id.length ? best : current;
128
+ }
129
+ return best.id >= current.id ? best : current;
130
+ });
131
+ }
132
+
133
+ /**
134
+ * Pick the best final candidate, optionally preferring specific providers.
135
+ *
136
+ * @param models - Candidate models
137
+ * @param preferredProviders - Ordered provider preference list
138
+ * @returns Best candidate
139
+ */
140
+ function pickBest(
141
+ models: readonly CandidateModel[],
142
+ preferredProviders?: readonly string[]
143
+ ): CandidateModel {
144
+ const bestModel = pickBestModel(models);
145
+ if (!preferredProviders || preferredProviders.length === 0) {
146
+ return bestModel;
147
+ }
148
+
149
+ const sameModel = models.filter((model) => model.id === bestModel.id);
150
+ if (sameModel.length <= 1) {
151
+ return bestModel;
152
+ }
153
+
154
+ const preferenceMap = buildProviderPreferenceMap(preferredProviders);
155
+ return sameModel.reduce((best, current) => {
156
+ return getProviderPriority(preferenceMap, best.provider) <=
157
+ getProviderPriority(preferenceMap, current.provider)
158
+ ? best
159
+ : current;
160
+ });
161
+ }
162
+
163
+ /**
164
+ * Find all candidates that tie at the best score for the first matching tier.
165
+ *
166
+ * Resolution cascade:
167
+ * 1. Exact ID match
168
+ * 2. Case-insensitive ID match
169
+ * 3. Normalized ID match (strips separators)
170
+ * 4. Provider/ID exact match
171
+ * 5. Token overlap scoring
172
+ * 6. Raw substring match
173
+ * 7. Normalized substring match
174
+ *
175
+ * @param query - Human-friendly query
176
+ * @param modelSource - Optional injected model source
177
+ * @returns Tied candidates from the first matching tier
178
+ */
179
+ function findCandidates(query: string, modelSource?: ModelSource): CandidateModel[] {
180
+ const models = modelSource ? modelSource() : getAllModels();
181
+ if (models.length === 0) return [];
182
+
183
+ const trimmed = query.trim();
184
+ if (trimmed.length === 0) return [];
185
+
186
+ const lower = trimmed.toLowerCase();
187
+ const normalized = normalize(trimmed);
188
+
189
+ const exact = models.filter((model) => model.id === trimmed);
190
+ if (exact.length > 0) return exact;
191
+
192
+ const caseInsensitive = models.filter((model) => model.id.toLowerCase() === lower);
193
+ if (caseInsensitive.length > 0) return caseInsensitive;
194
+
195
+ const normalizedId = models.filter((model) => normalize(model.id) === normalized);
196
+ if (normalizedId.length > 0) return normalizedId;
197
+
198
+ if (trimmed.includes("/")) {
199
+ const slashIndex = trimmed.indexOf("/");
200
+ const provider = trimmed.slice(0, slashIndex).toLowerCase();
201
+ const id = trimmed.slice(slashIndex + 1).toLowerCase();
202
+ const providerMatch = models.filter(
203
+ (model) => model.provider.toLowerCase() === provider && model.id.toLowerCase() === id
204
+ );
205
+ if (providerMatch.length > 0) return providerMatch;
206
+ }
207
+
208
+ const queryTokens = tokenize(trimmed);
209
+ if (queryTokens.length > 0) {
210
+ let bestScore = 0;
211
+ let bestMatches: CandidateModel[] = [];
212
+ for (const model of models) {
213
+ const idName = `${model.id} ${model.name}`.toLowerCase();
214
+ const providerText = model.provider.toLowerCase();
215
+ let score = 0;
216
+ for (const token of queryTokens) {
217
+ if (idName.includes(token)) score += 2;
218
+ else if (providerText.includes(token)) score += 1;
219
+ }
220
+ if (score > bestScore) {
221
+ bestScore = score;
222
+ bestMatches = [model];
223
+ } else if (score === bestScore && score > 0) {
224
+ bestMatches.push(model);
225
+ }
226
+ }
227
+ if (bestMatches.length > 0) return bestMatches;
228
+ }
229
+
230
+ const substring = models.filter(
231
+ (model) => model.id.toLowerCase().includes(lower) || model.name.toLowerCase().includes(lower)
232
+ );
233
+ if (substring.length > 0) return substring;
234
+
235
+ return models.filter(
236
+ (model) =>
237
+ normalize(model.id).includes(normalized) || normalize(model.name).includes(normalized)
238
+ );
239
+ }
240
+
241
+ /**
242
+ * Resolve a fuzzy model query to one best match.
243
+ *
244
+ * @param query - Human-friendly model query
245
+ * @param modelSource - Optional injected model source for deterministic tests
246
+ * @param preferredProviders - Optional provider preference ordering
247
+ * @returns Best matching model or undefined
248
+ */
249
+ export function resolveModelFuzzy(
250
+ query: string,
251
+ modelSource?: ModelSource,
252
+ preferredProviders?: string[]
253
+ ): ResolvedModel | undefined {
254
+ const candidates = findCandidates(query, modelSource);
255
+ if (candidates.length === 0) return undefined;
256
+ return toResolved(pickBest(candidates, preferredProviders));
257
+ }
258
+
259
+ /**
260
+ * Resolve a fuzzy model query to all tied candidates.
261
+ *
262
+ * @param query - Human-friendly model query
263
+ * @param modelSource - Optional injected model source for deterministic tests
264
+ * @returns Candidate matches
265
+ */
266
+ export function resolveModelCandidates(query: string, modelSource?: ModelSource): ResolvedModel[] {
267
+ return findCandidates(query, modelSource).map(toResolved);
268
+ }
269
+
270
+ /**
271
+ * List available provider/model identifiers.
272
+ *
273
+ * @param modelSource - Optional injected model source for deterministic tests
274
+ * @returns Provider/model identifier strings
275
+ */
276
+ export function listAvailableModels(modelSource?: ModelSource): string[] {
277
+ const models = modelSource ? modelSource() : getAllModels();
278
+ return models.map((model) => `${model.provider}/${model.id}`);
279
+ }
@@ -114,10 +114,10 @@ describe("tasks ui helpers", () => {
114
114
  expect(visibleWidth(truncated)).toBe(3);
115
115
  });
116
116
 
117
- it("mergeSideBySide bottom-aligns right column and keeps width bounds", () => {
117
+ it("mergeSideBySide top-aligns right column and keeps width bounds", () => {
118
118
  const merged = mergeSideBySide(["left1", "left2", "left3"], ["right"], 6, " | ", 16);
119
119
  expect(merged).toHaveLength(3);
120
- expect(merged[0]).toBe("left1 | ");
121
- expect(merged[2]).toContain("right");
120
+ expect(merged[0]).toContain("right");
121
+ expect(merged[2]).toBe("left3 | ");
122
122
  });
123
123
  });
@@ -406,7 +406,7 @@ describe("tasks widget subagent sections", () => {
406
406
  expectLinesWithinWidth(stackedNarrow, 55);
407
407
  expectLinesWithinWidth(stackedWide, 95);
408
408
 
409
- const sideBySideNarrow = renderWidget(fixture.captured, 124);
409
+ const sideBySideNarrow = renderWidget(fixture.captured, 144);
410
410
  const sideBySideWide = renderWidget(fixture.captured, 180);
411
411
  const sideBySideNarrowPreview = extractPreview(sideBySideNarrow, "seg01");
412
412
  const sideBySideWidePreview = extractPreview(sideBySideWide, "seg01");
@@ -417,7 +417,7 @@ describe("tasks widget subagent sections", () => {
417
417
  expect(visibleWidth(sideBySideWidePreview)).toBeGreaterThan(
418
418
  visibleWidth(sideBySideNarrowPreview)
419
419
  );
420
- expectLinesWithinWidth(sideBySideNarrow, 124);
420
+ expectLinesWithinWidth(sideBySideNarrow, 144);
421
421
  expectLinesWithinWidth(sideBySideWide, 180);
422
422
  } finally {
423
423
  await shutdownFixture(fixture);
@@ -685,7 +685,7 @@ export function registerTasksExtension(
685
685
  const useSideBySide = width >= MIN_SIDE_BY_SIDE_WIDTH && hasTasks && hasRightColumn;
686
686
 
687
687
  if (useSideBySide) {
688
- // Side-by-side: tasks on left, subagents + bg tasks on right (bottom-aligned)
688
+ // Side-by-side: tasks on left, subagents + bg tasks on right
689
689
  const separator = "\x1b[38;2;60;60;70m │ \x1b[0m"; // Dark gray
690
690
  const separatorWidth = 5; // " │ " is 5 visible chars
691
691
  const leftColumnWidth = Math.floor((width - separatorWidth) / 2);
@@ -2168,15 +2168,15 @@ Before calling manage_tasks complete/update, call manage_tasks list first so ind
2168
2168
  updateAgentBar(ctx);
2169
2169
  }
2170
2170
 
2171
- pi.on("session_start", async (_event, ctx) => {
2172
- restoreSessionState(ctx);
2173
- });
2174
-
2175
- pi.on("session_switch", async (_event, ctx) => {
2176
- restoreSessionState(ctx);
2177
- });
2178
-
2179
- // Cleanup on session end
2171
+ pi.on("session_start", async (_event, ctx) => restoreSessionState(ctx));
2172
+ (
2173
+ pi as unknown as {
2174
+ on: (
2175
+ event: string,
2176
+ handler: (event: unknown, ctx: ExtensionContext) => Promise<void>
2177
+ ) => void;
2178
+ }
2179
+ ).on("session_switch", async (_event, ctx) => restoreSessionState(ctx));
2180
2180
  pi.on("session_shutdown", async () => {
2181
2181
  emitInteropEvent(pi.events, INTEROP_EVENT_NAMES.backgroundTasksPresenterState, {
2182
2182
  active: false,
@@ -38,7 +38,7 @@ export const LEGACY_TEAMS_DIR = getTallowPath("teams");
38
38
  export const TEAM_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
39
39
 
40
40
  /** Minimum width for side-by-side layout (tasks left, subagents right). */
41
- export const MIN_SIDE_BY_SIDE_WIDTH = 120;
41
+ export const MIN_SIDE_BY_SIDE_WIDTH = 140;
42
42
 
43
43
  /** Prefix used for session-scoped shared task-group directories. */
44
44
  const SESSION_TASK_GROUP_PREFIX = "task-group";
@@ -45,7 +45,7 @@ export function padToWidth(line: string, targetWidth: number): string {
45
45
  }
46
46
 
47
47
  /**
48
- * Merge two column arrays into side-by-side lines, with right column bottom-aligned.
48
+ * Merge two column arrays into side-by-side lines with top-aligned rows.
49
49
  *
50
50
  * Both columns are truncated to their allotted widths to prevent overflow.
51
51
  *
@@ -68,14 +68,9 @@ export function mergeSideBySide(
68
68
  const maxRows = Math.max(leftLines.length, rightLines.length);
69
69
  const result: string[] = [];
70
70
 
71
- // Bottom-align: pad right column at the top
72
- const rightPadding = maxRows - rightLines.length;
73
-
74
71
  for (let i = 0; i < maxRows; i++) {
75
72
  const left = leftLines[i] ?? "";
76
- const rightIndex = i - rightPadding;
77
- const rawRight = rightIndex >= 0 ? (rightLines[rightIndex] ?? "") : "";
78
- // Truncate right column to prevent overflow
73
+ const rawRight = rightLines[i] ?? "";
79
74
  const right =
80
75
  rightWidth > 0 && visibleWidth(rawRight) > rightWidth
81
76
  ? truncateToWidth(rawRight, rightWidth, "")
@@ -20,7 +20,7 @@
20
20
  */
21
21
 
22
22
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
23
- import { Key, Loader, Text, type TUI } from "@mariozechner/pi-tui";
23
+ import { Key, Text, type TUI } from "@mariozechner/pi-tui";
24
24
  import { Type } from "@sinclair/typebox";
25
25
  import { INTEROP_EVENT_NAMES, onInteropEvent } from "../../_shared/interop-events.js";
26
26
  import {
@@ -248,7 +248,6 @@ export function registerTeamsToolExtension(pi: ExtensionAPI): void {
248
248
  dashboardEnabled = true;
249
249
  setDashboardFlag(true);
250
250
  startDashboardTicker();
251
- ctx.ui.setWorkingMessage(Loader.HIDE);
252
251
  ctx.ui.setStatus("team-dashboard", "Team dashboard active");
253
252
  ctx.ui.setEditorComponent((tui, editorTheme, keybindings) => {
254
253
  enterDashboardViewport(tui);
@@ -322,7 +321,6 @@ export function registerTeamsToolExtension(pi: ExtensionAPI): void {
322
321
 
323
322
  pi.on("turn_start", async (_event, ctx) => {
324
323
  if (!dashboardEnabled || !ctx.hasUI) return;
325
- ctx.ui.setWorkingMessage(Loader.HIDE);
326
324
  });
327
325
 
328
326
  // Archive all teams on session shutdown (preserves tasks for future recovery)
@@ -12,9 +12,10 @@
12
12
  */
13
13
 
14
14
  import type { ExtensionAPI, ThemeColor } from "@mariozechner/pi-coding-agent";
15
- import { hyperlink, Text } from "@mariozechner/pi-tui";
15
+ import { Text } from "@mariozechner/pi-tui";
16
16
  import { Type } from "@sinclair/typebox";
17
17
  import { getIcon } from "../_icons/index.js";
18
+ import { hyperlink } from "../_shared/terminal-links.js";
18
19
  import { formatToolVerb, renderLines } from "../tool-display/index.js";
19
20
  import { BraveSearchProvider } from "./providers/brave.js";
20
21
  import { SearchError, type SearchProvider, type SearchResult } from "./providers/interface.js";
@@ -68,6 +68,21 @@ function stubContext(): ExtensionContext {
68
68
  } as unknown as ExtensionContext;
69
69
  }
70
70
 
71
+ /**
72
+ * Resolve the registered write tool from a loaded harness.
73
+ *
74
+ * @param harness - Extension harness with write-tool-enhanced loaded
75
+ * @returns Registered write tool definition
76
+ * @throws {Error} When the write tool is unavailable
77
+ */
78
+ function getWriteTool(harness: ExtensionHarness) {
79
+ const tool = harness.tools.get("write");
80
+ if (!tool) {
81
+ throw new Error("write tool not registered");
82
+ }
83
+ return tool;
84
+ }
85
+
71
86
  // ── Tests ─────────────────────────────────────────────────────────────────────
72
87
 
73
88
  describe("write-tool-enhanced", () => {
@@ -109,7 +124,7 @@ describe("write-tool-enhanced", () => {
109
124
  const harness = ExtensionHarness.create();
110
125
  await harness.loadExtension(writePreview);
111
126
 
112
- const tool = harness.tools.get("write")!;
127
+ const tool = getWriteTool(harness);
113
128
  const content = "const x = 1;\nconst y = 2;";
114
129
 
115
130
  const result = await tool.execute(
@@ -128,7 +143,7 @@ describe("write-tool-enhanced", () => {
128
143
  const harness = ExtensionHarness.create();
129
144
  await harness.loadExtension(writePreview);
130
145
 
131
- const tool = harness.tools.get("write")!;
146
+ const tool = getWriteTool(harness);
132
147
  const content = "line1\nline2\nline3"; // 3 lines
133
148
 
134
149
  const result = await tool.execute(
@@ -148,7 +163,7 @@ describe("write-tool-enhanced", () => {
148
163
  const harness = ExtensionHarness.create();
149
164
  await harness.loadExtension(writePreview);
150
165
 
151
- const tool = harness.tools.get("write")!;
166
+ const tool = getWriteTool(harness);
152
167
  // 1200 chars → 1200/1024 ≈ 1.171875 → toFixed(1) = "1.2"
153
168
  const content = "a".repeat(1200);
154
169
  const expectedKb = (content.length / 1024).toFixed(1);
@@ -170,7 +185,7 @@ describe("write-tool-enhanced", () => {
170
185
  const harness = ExtensionHarness.create();
171
186
  await harness.loadExtension(writePreview);
172
187
 
173
- const tool = harness.tools.get("write")!;
188
+ const tool = getWriteTool(harness);
174
189
  const content = "hello\nworld"; // 2 lines, 11 chars
175
190
 
176
191
  const result = await tool.execute(
@@ -190,7 +205,7 @@ describe("write-tool-enhanced", () => {
190
205
  const harness = ExtensionHarness.create();
191
206
  await harness.loadExtension(writePreview);
192
207
 
193
- const tool = harness.tools.get("write")!;
208
+ const tool = getWriteTool(harness);
194
209
 
195
210
  const result = await tool.execute(
196
211
  "test-id",
@@ -211,7 +226,7 @@ describe("write-tool-enhanced", () => {
211
226
  const harness = ExtensionHarness.create();
212
227
  await harness.loadExtension(writePreview);
213
228
 
214
- const tool = harness.tools.get("write")!;
229
+ const tool = getWriteTool(harness);
215
230
 
216
231
  await expect(
217
232
  tool.execute(
@@ -2,8 +2,9 @@
2
2
  * Enhanced write tool — shows full written content with summary footer.
3
3
  */
4
4
  import { createWriteTool, type ExtensionAPI } from "@mariozechner/pi-coding-agent";
5
- import { fileLink, Text } from "@mariozechner/pi-tui";
5
+ import { Text } from "@mariozechner/pi-tui";
6
6
  import { getIcon } from "../_icons/index.js";
7
+ import { fileLink } from "../_shared/terminal-links.js";
7
8
  import {
8
9
  appendSection,
9
10
  dimProcessOutputLine,