@dungle-scrubs/tallow 0.8.26 → 0.8.28

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 (114) hide show
  1. package/README.md +42 -1
  2. package/dist/cli.js +7 -1
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config.d.ts +1 -1
  5. package/dist/config.js +1 -1
  6. package/dist/interactive-mode-patch.d.ts +1 -0
  7. package/dist/interactive-mode-patch.d.ts.map +1 -1
  8. package/dist/interactive-mode-patch.js +40 -1
  9. package/dist/interactive-mode-patch.js.map +1 -1
  10. package/dist/model-metadata-overrides.d.ts +2 -5
  11. package/dist/model-metadata-overrides.d.ts.map +1 -1
  12. package/dist/model-metadata-overrides.js +23 -12
  13. package/dist/model-metadata-overrides.js.map +1 -1
  14. package/dist/pid-manager.d.ts +2 -9
  15. package/dist/pid-manager.d.ts.map +1 -1
  16. package/dist/pid-manager.js +1 -58
  17. package/dist/pid-manager.js.map +1 -1
  18. package/dist/pid-schema.d.ts +51 -0
  19. package/dist/pid-schema.d.ts.map +1 -0
  20. package/dist/pid-schema.js +70 -0
  21. package/dist/pid-schema.js.map +1 -0
  22. package/dist/sdk.d.ts.map +1 -1
  23. package/dist/sdk.js +24 -17
  24. package/dist/sdk.js.map +1 -1
  25. package/dist/workspace-transition-interactive.d.ts.map +1 -1
  26. package/dist/workspace-transition-interactive.js +53 -3
  27. package/dist/workspace-transition-interactive.js.map +1 -1
  28. package/dist/workspace-transition.d.ts +2 -1
  29. package/dist/workspace-transition.d.ts.map +1 -1
  30. package/dist/workspace-transition.js +16 -4
  31. package/dist/workspace-transition.js.map +1 -1
  32. package/extensions/__integration__/audit-findings.test.ts +309 -0
  33. package/extensions/__integration__/cd-tool-guidelines.test.ts +46 -0
  34. package/extensions/__integration__/tasks-runtime.test.ts +63 -12
  35. package/extensions/__integration__/welcome-screen.test.ts +240 -0
  36. package/extensions/_shared/lazy-init.ts +88 -3
  37. package/extensions/_shared/pid-registry.ts +8 -82
  38. package/extensions/background-task-tool/index.ts +1 -1
  39. package/extensions/cd-tool/index.ts +4 -1
  40. package/extensions/cheatsheet/__tests__/cheatsheet.test.ts +47 -0
  41. package/extensions/clear/__tests__/clear.test.ts +38 -0
  42. package/extensions/edit-tool-enhanced/index.ts +3 -1
  43. package/extensions/git-status/__tests__/git-status.test.ts +32 -0
  44. package/extensions/health/__tests__/diagnostics.test.ts +25 -0
  45. package/extensions/health/index.ts +61 -0
  46. package/extensions/loop/__tests__/loop.test.ts +365 -1
  47. package/extensions/loop/index.ts +213 -3
  48. package/extensions/mcp-adapter-tool/index.ts +1 -1
  49. package/extensions/minimal-skill-display/__tests__/minimal-skill-display.test.ts +20 -0
  50. package/extensions/permissions/__tests__/permissions.test.ts +213 -0
  51. package/extensions/progress-indicator/__tests__/progress-indicator.test.ts +104 -0
  52. package/extensions/prompt-suggestions/__tests__/autocomplete.test.ts +111 -3
  53. package/extensions/prompt-suggestions/autocomplete.ts +23 -5
  54. package/extensions/prompt-suggestions/index.ts +62 -3
  55. package/extensions/random-spinner/__tests__/random-spinner.test.ts +35 -0
  56. package/extensions/read-tool-enhanced/index.ts +5 -1
  57. package/extensions/session-memory/index.ts +1 -1
  58. package/extensions/session-namer/index.ts +1 -1
  59. package/extensions/show-system-prompt/__tests__/show-system-prompt.test.ts +51 -0
  60. package/extensions/subagent-tool/__tests__/presentation-rendering.test.ts +9 -8
  61. package/extensions/subagent-tool/__tests__/process-liveness.test.ts +51 -0
  62. package/extensions/subagent-tool/__tests__/subprocess-args.test.ts +120 -0
  63. package/extensions/subagent-tool/formatting.ts +2 -0
  64. package/extensions/subagent-tool/index.ts +160 -97
  65. package/extensions/subagent-tool/process.ts +152 -40
  66. package/extensions/tasks/commands/register-tasks-extension.ts +64 -20
  67. package/extensions/tasks/extension.json +1 -0
  68. package/extensions/tasks/index.ts +2 -12
  69. package/extensions/tasks/state/index.ts +26 -0
  70. package/extensions/teams-tool/dashboard.ts +13 -1
  71. package/extensions/teams-tool/sessions/spawn.ts +2 -2
  72. package/extensions/teams-tool/tools/register-extension.ts +10 -2
  73. package/extensions/upstream-check/__tests__/upstream-check.test.ts +49 -0
  74. package/extensions/welcome-screen/__tests__/welcome-screen.test.ts +35 -0
  75. package/extensions/welcome-screen/extension.json +20 -0
  76. package/extensions/welcome-screen/index.ts +189 -0
  77. package/extensions/wezterm-notify/__tests__/index.test.ts +49 -11
  78. package/extensions/wezterm-notify/index.ts +5 -3
  79. package/extensions/write-tool-enhanced/__tests__/write-tool-enhanced.test.ts +296 -0
  80. package/node_modules/@mariozechner/pi-tui/dist/index.d.ts +2 -2
  81. package/node_modules/@mariozechner/pi-tui/dist/index.d.ts.map +1 -1
  82. package/node_modules/@mariozechner/pi-tui/dist/index.js +2 -2
  83. package/node_modules/@mariozechner/pi-tui/dist/index.js.map +1 -1
  84. package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts +309 -25
  85. package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts.map +1 -1
  86. package/node_modules/@mariozechner/pi-tui/dist/keybindings.js +392 -72
  87. package/node_modules/@mariozechner/pi-tui/dist/keybindings.js.map +1 -1
  88. package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts +30 -0
  89. package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts.map +1 -1
  90. package/node_modules/@mariozechner/pi-tui/dist/keys.js +50 -6
  91. package/node_modules/@mariozechner/pi-tui/dist/keys.js.map +1 -1
  92. package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts +27 -0
  93. package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts.map +1 -1
  94. package/node_modules/@mariozechner/pi-tui/dist/terminal.js +59 -4
  95. package/node_modules/@mariozechner/pi-tui/dist/terminal.js.map +1 -1
  96. package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts +9 -0
  97. package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts.map +1 -1
  98. package/node_modules/@mariozechner/pi-tui/dist/tui.js +50 -1
  99. package/node_modules/@mariozechner/pi-tui/dist/tui.js.map +1 -1
  100. package/node_modules/@mariozechner/pi-tui/package.json +1 -1
  101. package/node_modules/@mariozechner/pi-tui/src/__tests__/mouse-events.test.ts +134 -0
  102. package/node_modules/@mariozechner/pi-tui/src/__tests__/tmux-compat.test.ts +204 -0
  103. package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-diff-regression.test.ts +49 -0
  104. package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-render-scheduling.test.ts +2 -0
  105. package/node_modules/@mariozechner/pi-tui/src/index.ts +11 -0
  106. package/node_modules/@mariozechner/pi-tui/src/keybindings.ts +478 -140
  107. package/node_modules/@mariozechner/pi-tui/src/keys.ts +84 -6
  108. package/node_modules/@mariozechner/pi-tui/src/terminal.ts +69 -4
  109. package/node_modules/@mariozechner/pi-tui/src/tui.ts +64 -1
  110. package/package.json +11 -10
  111. package/runtime/config.ts +7 -0
  112. package/runtime/model-metadata-overrides.ts +7 -0
  113. package/runtime/pid-schema.ts +13 -0
  114. package/skills/tallow-expert/SKILL.md +7 -5
@@ -819,7 +819,10 @@ export function matchesKey(data: string, keyId: KeyId): boolean {
819
819
  if (modifier === 0) {
820
820
  return data === "\t" || matchesKittySequence(data, CODEPOINTS.tab, 0);
821
821
  }
822
- return matchesKittySequence(data, CODEPOINTS.tab, modifier);
822
+ return (
823
+ matchesKittySequence(data, CODEPOINTS.tab, modifier) ||
824
+ matchesModifyOtherKeys(data, CODEPOINTS.tab, modifier)
825
+ );
823
826
 
824
827
  case "enter":
825
828
  case "return":
@@ -873,7 +876,8 @@ export function matchesKey(data: string, keyId: KeyId): boolean {
873
876
  }
874
877
  return (
875
878
  matchesKittySequence(data, CODEPOINTS.enter, modifier) ||
876
- matchesKittySequence(data, CODEPOINTS.kpEnter, modifier)
879
+ matchesKittySequence(data, CODEPOINTS.kpEnter, modifier) ||
880
+ matchesModifyOtherKeys(data, CODEPOINTS.enter, modifier)
877
881
  );
878
882
 
879
883
  case "backspace":
@@ -1108,21 +1112,33 @@ export function matchesKey(data: string, keyId: KeyId): boolean {
1108
1112
  if (ctrl && !shift && !alt) {
1109
1113
  // Legacy: ctrl+key sends the control character
1110
1114
  if (rawCtrl && data === rawCtrl) return true;
1111
- return matchesKittySequence(data, codepoint, MODIFIERS.ctrl);
1115
+ return (
1116
+ matchesKittySequence(data, codepoint, MODIFIERS.ctrl) ||
1117
+ matchesModifyOtherKeys(data, codepoint, MODIFIERS.ctrl)
1118
+ );
1112
1119
  }
1113
1120
 
1114
1121
  if (ctrl && shift && !alt) {
1115
- return matchesKittySequence(data, codepoint, MODIFIERS.shift + MODIFIERS.ctrl);
1122
+ return (
1123
+ matchesKittySequence(data, codepoint, MODIFIERS.shift + MODIFIERS.ctrl) ||
1124
+ matchesModifyOtherKeys(data, codepoint, MODIFIERS.shift + MODIFIERS.ctrl)
1125
+ );
1116
1126
  }
1117
1127
 
1118
1128
  if (shift && !ctrl && !alt) {
1119
1129
  // Legacy: shift+letter produces uppercase
1120
1130
  if (data === key.toUpperCase()) return true;
1121
- return matchesKittySequence(data, codepoint, MODIFIERS.shift);
1131
+ return (
1132
+ matchesKittySequence(data, codepoint, MODIFIERS.shift) ||
1133
+ matchesModifyOtherKeys(data, codepoint, MODIFIERS.shift)
1134
+ );
1122
1135
  }
1123
1136
 
1124
1137
  if (modifier !== 0) {
1125
- return matchesKittySequence(data, codepoint, modifier);
1138
+ return (
1139
+ matchesKittySequence(data, codepoint, modifier) ||
1140
+ matchesModifyOtherKeys(data, codepoint, modifier)
1141
+ );
1126
1142
  }
1127
1143
 
1128
1144
  // Check both raw char and Kitty sequence (needed for release events)
@@ -1251,3 +1267,65 @@ export function parseKey(data: string): string | undefined {
1251
1267
 
1252
1268
  return undefined;
1253
1269
  }
1270
+
1271
+ // =============================================================================
1272
+ // Mouse Event Parsing
1273
+ // =============================================================================
1274
+
1275
+ /**
1276
+ * Parsed mouse event from SGR extended format.
1277
+ * SGR format: \x1b[<button;column;row[Mm]
1278
+ * M = press, m = release
1279
+ */
1280
+ export interface MouseEvent {
1281
+ /** Event type */
1282
+ type: "scroll-up" | "scroll-down" | "press" | "release" | "drag";
1283
+ /** 0=left, 1=middle, 2=right */
1284
+ button: number;
1285
+ /** 1-indexed column */
1286
+ x: number;
1287
+ /** 1-indexed row */
1288
+ y: number;
1289
+ }
1290
+
1291
+ /** SGR extended mouse format: \x1b[<button;x;y[Mm] */
1292
+ const SGR_MOUSE_RE = /^\x1b\[<(\d+);(\d+);(\d+)([Mm])$/;
1293
+
1294
+ /**
1295
+ * Parse an SGR mouse event from raw terminal input.
1296
+ *
1297
+ * @param data - Raw terminal input
1298
+ * @returns Parsed mouse event, or null if not a mouse sequence
1299
+ */
1300
+ export function parseMouseEvent(data: string): MouseEvent | null {
1301
+ const match = data.match(SGR_MOUSE_RE);
1302
+ if (!match) return null;
1303
+
1304
+ const code = parseInt(match[1]!, 10);
1305
+ const x = parseInt(match[2]!, 10);
1306
+ const y = parseInt(match[3]!, 10);
1307
+ const isRelease = match[4] === "m";
1308
+
1309
+ // Scroll wheel: codes 64 (up) and 65 (down)
1310
+ if (code === 64) return { type: "scroll-up", button: 0, x, y };
1311
+ if (code === 65) return { type: "scroll-down", button: 0, x, y };
1312
+
1313
+ // Button number is in the low 2 bits
1314
+ const button = code & 0x03;
1315
+
1316
+ // Bit 5 (32) = motion/drag
1317
+ if (code & 32) return { type: "drag", button, x, y };
1318
+
1319
+ return { type: isRelease ? "release" : "press", button, x, y };
1320
+ }
1321
+
1322
+ /**
1323
+ * Fast check: is this data an SGR mouse event?
1324
+ * Avoids regex for non-mouse input.
1325
+ *
1326
+ * @param data - Raw terminal input
1327
+ * @returns true if the input is an SGR mouse sequence
1328
+ */
1329
+ export function isMouseEvent(data: string): boolean {
1330
+ return data.length >= 9 && data.startsWith("\x1b[<");
1331
+ }
@@ -30,6 +30,9 @@ export interface Terminal {
30
30
  // Whether Kitty keyboard protocol is active
31
31
  get kittyProtocolActive(): boolean;
32
32
 
33
+ // Whether running inside tmux (using modifyOtherKeys instead of Kitty protocol)
34
+ get isTmux(): boolean;
35
+
33
36
  // Cursor positioning (relative to current position)
34
37
  moveBy(lines: number): void; // Move cursor up (negative) or down (positive) by N lines
35
38
 
@@ -42,6 +45,10 @@ export interface Terminal {
42
45
  clearFromCursor(): void; // Clear from cursor to end of screen
43
46
  clearScreen(): void; // Clear entire screen and move cursor to (0,0)
44
47
 
48
+ // Mouse reporting
49
+ enableMouse(): void; // Enable SGR mouse tracking (scroll, click)
50
+ disableMouse(): void; // Disable mouse tracking
51
+
45
52
  // Screen buffer mode
46
53
  enterAlternateScreen(): void; // Switch to alternate screen buffer (no scrollback)
47
54
  leaveAlternateScreen(): void; // Restore normal screen buffer and scrollback
@@ -67,11 +74,16 @@ export class ProcessTerminal implements Terminal {
67
74
  private stdinDataHandler?: (data: string) => void;
68
75
  private writeLogPath = process.env.PI_TUI_WRITE_LOG || "";
69
76
  private alternateScreenActive = false;
77
+ private mouseActive = false;
70
78
 
71
79
  get kittyProtocolActive(): boolean {
72
80
  return this._kittyProtocolActive;
73
81
  }
74
82
 
83
+ get isTmux(): boolean {
84
+ return !!process.env.TMUX;
85
+ }
86
+
75
87
  start(onInput: (data: string) => void, onResize: () => void): void {
76
88
  this.inputHandler = onInput;
77
89
  this.resizeHandler = onResize;
@@ -97,10 +109,24 @@ export class ProcessTerminal implements Terminal {
97
109
  process.kill(process.pid, "SIGWINCH");
98
110
  }
99
111
 
100
- // Query and enable Kitty keyboard protocol
101
- // The query handler intercepts input temporarily, then installs the user's handler
102
- // See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
103
- this.queryAndEnableKittyProtocol();
112
+ // Mouse tracking (enableMouse/disableMouse) is available but NOT
113
+ // enabled anywhere yet. The TUI has no scroll viewport content
114
+ // off-screen is only reachable via terminal scrollback (or tmux
115
+ // copy-mode). Enabling mouse tracking steals scroll events without
116
+ // providing scroll handling, making things strictly worse. Enable
117
+ // only after implementing a scroll viewport in the TUI.
118
+
119
+ // Enable keyboard protocol for modified key detection.
120
+ // tmux doesn't support the Kitty keyboard protocol but does support xterm's
121
+ // modifyOtherKeys. Detect tmux and use the appropriate protocol.
122
+ if (process.env.TMUX) {
123
+ this.setupTmuxInput();
124
+ } else {
125
+ // Query and enable Kitty keyboard protocol
126
+ // The query handler intercepts input temporarily, then installs the user's handler
127
+ // See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
128
+ this.queryAndEnableKittyProtocol();
129
+ }
104
130
  }
105
131
 
106
132
  /**
@@ -154,6 +180,22 @@ export class ProcessTerminal implements Terminal {
154
180
  };
155
181
  }
156
182
 
183
+ /**
184
+ * Set up stdin handling for tmux without Kitty keyboard protocol.
185
+ *
186
+ * tmux doesn't support the Kitty keyboard protocol. Escape and Ctrl+C
187
+ * arrive as raw bytes (\x1b and \x03) which the key matching handles natively.
188
+ *
189
+ * For Shift+Enter, tmux needs `extended-keys on` and `extended-keys-format csi-u`
190
+ * in tmux.conf. With that config, we request modifyOtherKeys mode 1 so tmux
191
+ * encodes modified keys (Shift+Enter → CSI 13;2 u) while leaving standard
192
+ * keys (Escape, Ctrl+C, regular typing) as raw bytes.
193
+ */
194
+ private setupTmuxInput(): void {
195
+ this.setupStdinBuffer();
196
+ process.stdin.on("data", this.stdinDataHandler!);
197
+ }
198
+
157
199
  /**
158
200
  * Query terminal for Kitty keyboard protocol support and enable if available.
159
201
  *
@@ -209,6 +251,9 @@ export class ProcessTerminal implements Terminal {
209
251
  this.leaveAlternateScreen();
210
252
  }
211
253
 
254
+ // Disable mouse tracking
255
+ this.disableMouse();
256
+
212
257
  // Disable bracketed paste mode
213
258
  process.stdout.write("\x1b[?2004l");
214
259
 
@@ -297,6 +342,26 @@ export class ProcessTerminal implements Terminal {
297
342
  process.stdout.write("\x1b[2J\x1b[H"); // Clear screen and move to home (1,1)
298
343
  }
299
344
 
345
+ /**
346
+ * Enable SGR mouse tracking.
347
+ * Mode 1000 = button press/release (includes scroll wheel).
348
+ * Mode 1006 = SGR extended format (avoids 223-column limit).
349
+ */
350
+ enableMouse(): void {
351
+ if (this.mouseActive) return;
352
+ process.stdout.write("\x1b[?1000h\x1b[?1006h");
353
+ this.mouseActive = true;
354
+ }
355
+
356
+ /**
357
+ * Disable mouse tracking and restore default terminal mouse handling.
358
+ */
359
+ disableMouse(): void {
360
+ if (!this.mouseActive) return;
361
+ process.stdout.write("\x1b[?1006l\x1b[?1000l");
362
+ this.mouseActive = false;
363
+ }
364
+
300
365
  /**
301
366
  * Switch to alternate screen buffer and clear it.
302
367
  * @returns void
@@ -5,7 +5,13 @@
5
5
  import * as fs from "node:fs";
6
6
  import * as os from "node:os";
7
7
  import * as path from "node:path";
8
- import { isKeyRelease, matchesKey } from "./keys.js";
8
+ import {
9
+ isKeyRelease,
10
+ isMouseEvent,
11
+ type MouseEvent,
12
+ matchesKey,
13
+ parseMouseEvent,
14
+ } from "./keys.js";
9
15
  import type { Terminal } from "./terminal.js";
10
16
  import { getCapabilities, isImageLine, setCellDimensions } from "./terminal-image.js";
11
17
  import {
@@ -209,6 +215,13 @@ export class TUI extends Container {
209
215
 
210
216
  /** Global callback for debug key (Shift+Ctrl+D). Called before input is forwarded to focused component. */
211
217
  public onDebug?: () => void;
218
+ /**
219
+ * Callback for mouse events. Called when a mouse event is received.
220
+ * Scroll events are the primary use case (scroll-up, scroll-down).
221
+ * Return value is ignored — mouse events are always consumed and never
222
+ * forwarded to focused components.
223
+ */
224
+ public onMouse?: (event: MouseEvent) => void;
212
225
  private renderRequested = false;
213
226
  private pendingRenderHandle?: ReturnType<typeof setTimeout>;
214
227
  private cursorRow = 0; // Logical cursor row (end of rendered content)
@@ -220,6 +233,7 @@ export class TUI extends Container {
220
233
  private maxLinesRendered = 0; // Track terminal's working area (max lines ever rendered)
221
234
  private previousViewportTop = 0; // Track previous viewport top for resize-aware cursor moves
222
235
  private fullRedrawCount = 0;
236
+ private rollingShrinkPeak = 0; // Recent peak line count for gradual shrink detection
223
237
  private stopped = false;
224
238
  private pendingScrollbackClear = false; // Clear scrollback on next full render (session breaks)
225
239
 
@@ -405,6 +419,12 @@ export class TUI extends Container {
405
419
  if (!getCapabilities().images) {
406
420
  return;
407
421
  }
422
+ // Skip cell size query inside tmux — tmux doesn't forward CSI 16 t responses,
423
+ // so cellSizeQueryPending would stay true and parseCellSizeResponse would eat
424
+ // bare \x1b (Escape key) as a "partial response", breaking Escape handling.
425
+ if (process.env.TMUX) {
426
+ return;
427
+ }
408
428
  // Query terminal for cell size in pixels: CSI 16 t
409
429
  // Response format: CSI 6 ; height ; width t
410
430
  this.cellSizeQueryPending = true;
@@ -442,6 +462,7 @@ export class TUI extends Container {
442
462
  this.hardwareCursorRow = 0;
443
463
  this.maxLinesRendered = 0;
444
464
  this.previousViewportTop = 0;
465
+ this.rollingShrinkPeak = 0;
445
466
  }
446
467
  if (this.renderRequested) return;
447
468
  this.scheduleRender();
@@ -511,6 +532,16 @@ export class TUI extends Container {
511
532
  data = filtered;
512
533
  }
513
534
 
535
+ // Mouse events — intercept before any key handling.
536
+ // Always consumed: mouse sequences must never reach components as text.
537
+ if (isMouseEvent(data)) {
538
+ const event = parseMouseEvent(data);
539
+ if (event && this.onMouse) {
540
+ this.onMouse(event);
541
+ }
542
+ return;
543
+ }
544
+
514
545
  // Global debug key handler (Shift+Ctrl+D)
515
546
  if (matchesKey(data, "shift+ctrl+d") && this.onDebug) {
516
547
  this.onDebug();
@@ -556,6 +587,19 @@ export class TUI extends Container {
556
587
  if (isKeyRelease(data) && !this.focusedComponent.wantsKeyRelease) {
557
588
  return;
558
589
  }
590
+ if (process.env.TALLOW_KEY_DEBUG && (data === "\x1b" || data === "\x03")) {
591
+ const escMatch = matchesKey(data, "escape");
592
+ const ctrlcMatch = matchesKey(data, "ctrl+c");
593
+ const comp = this.focusedComponent as unknown as {
594
+ onEscape?: () => void;
595
+ actionHandlers?: Map<string, unknown>;
596
+ };
597
+ const hasOnEscape = typeof comp.onEscape === "function";
598
+ const actionCount = comp.actionHandlers instanceof Map ? comp.actionHandlers.size : -1;
599
+ process.stderr.write(
600
+ `[key] ${data === "\x1b" ? "ESC" : "C-C"} matchEsc=${escMatch} matchCC=${ctrlcMatch} hasOnEscape=${hasOnEscape} actions=${actionCount}\n`
601
+ );
602
+ }
559
603
  this.focusedComponent.handleInput(data);
560
604
  this.requestRender();
561
605
  }
@@ -985,6 +1029,7 @@ export class TUI extends Container {
985
1029
  this.maxLinesRendered = Math.max(this.maxLinesRendered, newLines.length);
986
1030
  }
987
1031
  this.previousViewportTop = Math.max(0, this.maxLinesRendered - height);
1032
+ this.rollingShrinkPeak = newLines.length;
988
1033
  this.positionHardwareCursor(cursorPos, newLines.length);
989
1034
  this.previousLines = newLines;
990
1035
  this.previousWidth = width;
@@ -1036,6 +1081,21 @@ export class TUI extends Container {
1036
1081
  return;
1037
1082
  }
1038
1083
 
1084
+ // Rolling shrink detection: catches gradual shrinks where each individual
1085
+ // frame-to-frame delta is ≤5 lines (below the large-shrink threshold) but the
1086
+ // accumulated shrink from a recent peak exceeds it. This happens when
1087
+ // pollStates.clear() collapses tool-result anchors across multiple render
1088
+ // cycles while animations (loader, widget spinners) keep triggering renders.
1089
+ if (newLines.length >= this.rollingShrinkPeak) {
1090
+ this.rollingShrinkPeak = newLines.length;
1091
+ } else if (this.overlayStack.length === 0 && this.rollingShrinkPeak - newLines.length > 5) {
1092
+ logRedraw(
1093
+ `rolling shrink (peak=${this.rollingShrinkPeak}, now=${newLines.length}, delta=${this.rollingShrinkPeak - newLines.length})`
1094
+ );
1095
+ fullRender(true);
1096
+ return;
1097
+ }
1098
+
1039
1099
  const previousContentViewportTop = Math.max(0, this.previousLines.length - height);
1040
1100
  // Detect viewport basis drift: maxLinesRendered exceeds actual content,
1041
1101
  // causing viewportTop to be computed from a stale high-water mark.
@@ -1306,6 +1366,9 @@ export class TUI extends Container {
1306
1366
  this.maxLinesRendered = Math.max(this.maxLinesRendered, newLines.length);
1307
1367
  }
1308
1368
  this.previousViewportTop = Math.max(0, this.maxLinesRendered - height);
1369
+ // Update rolling peak for gradual shrink detection (partial path only —
1370
+ // fullRender paths reset it inside the fullRender closure).
1371
+ this.rollingShrinkPeak = Math.max(this.rollingShrinkPeak, newLines.length);
1309
1372
 
1310
1373
  // Position hardware cursor for IME
1311
1374
  this.positionHardwareCursor(cursorPos, newLines.length);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dungle-scrubs/tallow",
3
- "version": "0.8.26",
3
+ "version": "0.8.28",
4
4
  "description": "An opinionated coding agent. Built on pi.",
5
5
  "piConfig": {
6
6
  "name": "tallow",
@@ -53,7 +53,8 @@
53
53
  "format:check": "biome format .",
54
54
  "bench:startup": "bun scripts/benchmark-startup-fast-path.ts",
55
55
  "pre-pr": "bash scripts/pre-pr.sh",
56
- "prepare": "test -d .git && husky || true"
56
+ "prepare": "test -d .git && husky || true",
57
+ "postinstall": "node scripts/patch-upstream-debug.mjs"
57
58
  },
58
59
  "lint-staged": {
59
60
  "*.{ts,tsx,js,jsx,json,jsonc,css}": [
@@ -73,25 +74,25 @@
73
74
  },
74
75
  "dependencies": {
75
76
  "@clack/prompts": "^1.1.0",
76
- "@dungle-scrubs/synapse": "0.1.6",
77
- "@mariozechner/pi-coding-agent": "^0.60.0",
78
- "@mariozechner/pi-tui": "^0.60.0",
77
+ "@dungle-scrubs/synapse": "0.1.8",
78
+ "@mariozechner/pi-coding-agent": "^0.64.0",
79
+ "@mariozechner/pi-tui": "^0.62.0",
79
80
  "@opentelemetry/api": "^1.9.0",
80
81
  "@sinclair/typebox": "0.34.48",
81
82
  "ai": "^6.0.116",
82
83
  "commander": "^14.0.3",
83
84
  "unpdf": "^1.4.0",
84
- "vscode-jsonrpc": "8.2.1"
85
+ "vscode-jsonrpc": "8.2.1",
86
+ "vscode-languageserver-protocol": "3.17.5"
85
87
  },
86
88
  "devDependencies": {
87
89
  "@biomejs/biome": "2.4.2",
88
- "@mariozechner/pi-agent-core": "^0.60.0",
89
- "@mariozechner/pi-ai": "^0.60.0",
90
+ "@mariozechner/pi-agent-core": "^0.64.0",
91
+ "@mariozechner/pi-ai": "^0.64.0",
90
92
  "@types/node": "25.2.3",
91
93
  "husky": "^9.1.7",
92
94
  "lint-staged": "^16.4.0",
93
- "typescript": "^5.9.3",
94
- "vscode-languageserver-protocol": "3.17.5"
95
+ "typescript": "^5.9.3"
95
96
  },
96
97
  "engines": {
97
98
  "node": ">=22"
@@ -0,0 +1,7 @@
1
+ import { resolveRuntimeModuleUrl } from "./resolve-module.js";
2
+
3
+ const mod = (await import(
4
+ resolveRuntimeModuleUrl("config.js")
5
+ )) as typeof import("../src/config.js");
6
+
7
+ export const TALLOW_VERSION = mod.TALLOW_VERSION;
@@ -0,0 +1,7 @@
1
+ import { resolveRuntimeModuleUrl } from "./resolve-module.js";
2
+
3
+ const mod = (await import(
4
+ resolveRuntimeModuleUrl("model-metadata-overrides.js")
5
+ )) as typeof import("../src/model-metadata-overrides.js");
6
+
7
+ export const applyKnownModelMetadataOverrides = mod.applyKnownModelMetadataOverrides;
@@ -0,0 +1,13 @@
1
+ import { resolveRuntimeModuleUrl } from "./resolve-module.js";
2
+
3
+ const pidSchemaModule = (await import(
4
+ resolveRuntimeModuleUrl("pid-schema.js")
5
+ )) as typeof import("../src/pid-schema.js");
6
+
7
+ export type PidEntry = import("../src/pid-schema.js").PidEntry;
8
+ export type SessionOwner = import("../src/pid-schema.js").SessionOwner;
9
+ export type SessionPidFile = import("../src/pid-schema.js").SessionPidFile;
10
+
11
+ export const isPidEntry = pidSchemaModule.isPidEntry;
12
+ export const isSessionOwner = pidSchemaModule.isSessionOwner;
13
+ export const toOwnerKey = pidSchemaModule.toOwnerKey;
@@ -33,8 +33,8 @@ Relay that answer to the user.
33
33
 
34
34
  | Component | Location |
35
35
  |-----------|----------|
36
- | Core source | `src/` (agent-runner.ts, atomic-write.ts, auth-hardening.ts, cli-auto-rebuild.ts, cli.ts, compaction-cancel-patch.ts, config.ts, extensions-global.d.ts, fatal-errors.ts, index.ts, install.ts, interactive-mode-patch.ts, model-metadata-overrides.ts, otel.ts, pid-manager.ts, plugins.ts, process-cleanup.ts, project-trust-banner.ts, project-trust-interop.ts, project-trust.ts, runtime-path-provider.ts, runtime-provenance.ts, sdk.ts, session-migration.ts, session-utils.ts, startup-profile.ts, startup-timing.ts, streaming-yield-patch.ts, workspace-transition-interactive.ts, workspace-transition-relay.ts, workspace-transition.ts, yield-to-io.ts) |
37
- | Extensions | `extensions/` — extension.json + index.ts each (52 bundled) |
36
+ | Core source | `src/` (agent-runner.ts, atomic-write.ts, auth-hardening.ts, cli-auto-rebuild.ts, cli.ts, compaction-cancel-patch.ts, config.ts, extensions-global.d.ts, fatal-errors.ts, index.ts, install.ts, interactive-mode-patch.ts, model-metadata-overrides.ts, otel.ts, pid-manager.ts, pid-schema.ts, plugins.ts, process-cleanup.ts, project-trust-banner.ts, project-trust-interop.ts, project-trust.ts, runtime-path-provider.ts, runtime-provenance.ts, sdk.ts, session-migration.ts, session-utils.ts, startup-profile.ts, startup-timing.ts, streaming-yield-patch.ts, workspace-transition-interactive.ts, workspace-transition-relay.ts, workspace-transition.ts, yield-to-io.ts) |
37
+ | Extensions | `extensions/` — extension.json + index.ts each (53 bundled) |
38
38
  | Skills | `skills/` — subdirs with SKILL.md |
39
39
  | Agents | `agents/` — markdown with YAML frontmatter |
40
40
  | Themes | `themes/` — JSON files (34 dark-only themes) |
@@ -59,8 +59,8 @@ Extensions export a default function receiving `ExtensionAPI` (conventionally na
59
59
 
60
60
  #### Registration
61
61
 
62
- - `registerTool(tool: ToolDefinition<TParams, TDetails>)` — Register a tool that the LLM can call.
63
- - `registerCommand(name: string, options: Omit<RegisteredCommand, "name">)` — Register a custom command.
62
+ - `registerTool(tool: ToolDefinition<TParams, TDetails, TState>)` — Register a tool that the LLM can call.
63
+ - `registerCommand(name: string, options: Omit<RegisteredCommand, "name" | "sourceInfo">)` — Register a custom command.
64
64
  - `registerFlag(name: string, options: object)` — Register a CLI flag.
65
65
  - `registerMessageRenderer(customType: string, renderer: MessageRenderer<T>)` — Register a custom renderer for CustomMessageEntry.
66
66
  - `registerProvider(name: string, config: ProviderConfig)` — Register or override a model provider.
@@ -81,7 +81,7 @@ Extensions export a default function receiving `ExtensionAPI` (conventionally na
81
81
  - `getFlag(name: string)` — Get the value of a registered CLI flag.
82
82
  - `exec(command: string, args: string[], options?: ExecOptions)` — Execute a shell command.
83
83
  - `getActiveTools()` — Get the list of currently active tool names.
84
- - `getAllTools()` — Get all configured tools with name and description.
84
+ - `getAllTools()` — Get all configured tools with parameter schema and source metadata.
85
85
  - `setActiveTools(toolNames: string[])` — Set the active tools by name.
86
86
  - `getCommands()` — Get available slash commands in the current session.
87
87
  - `setModel(model: Model<any>)` — Set the current model.
@@ -154,6 +154,7 @@ Extensions export a default function receiving `ExtensionAPI` (conventionally na
154
154
  - `modelRegistry` — Model registry for API key resolution
155
155
  - `model` — Current model (may be undefined)
156
156
  - `isIdle()` — Whether the agent is idle (not streaming)
157
+ - `signal` — The current abort signal, or undefined when the agent is not streaming.
157
158
  - `abort()` — Abort the current agent operation
158
159
  - `hasPendingMessages()` — Whether there are queued messages waiting
159
160
  - `shutdown()` — Gracefully shutdown pi and exit.
@@ -178,6 +179,7 @@ Extensions export a default function receiving `ExtensionAPI` (conventionally na
178
179
  - `onTerminalInput(handler: TerminalInputHandler)` — Listen to raw terminal input (interactive mode only).
179
180
  - `setStatus(key: string, text: string)` — Set status text in the footer/status bar.
180
181
  - `setWorkingMessage(message?: string)` — Set the working/loading message shown during streaming.
182
+ - `setHiddenThinkingLabel(label?: string)` — Set the label shown for hidden thinking blocks.
181
183
  - `setWidget(key: string, content: string[], options?: ExtensionWidgetOptions)` — Set a widget to display above or below the editor.
182
184
  - `setTitle(title: string)` — Set the terminal window/tab title.
183
185
  - `pasteToEditor(text: string)` — Paste text into the editor, triggering paste handling (collapse for large content).