@dungle-scrubs/tallow 0.9.4 → 0.9.7

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 (212) hide show
  1. package/dist/cli.js +8 -5
  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 -12
  6. package/dist/interactive-mode-patch.d.ts.map +1 -1
  7. package/dist/interactive-mode-patch.js +229 -146
  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/process-cleanup.js +1 -1
  26. package/dist/process-cleanup.js.map +1 -1
  27. package/dist/reset-diagnostics.d.ts +69 -0
  28. package/dist/reset-diagnostics.d.ts.map +1 -0
  29. package/dist/reset-diagnostics.js +41 -0
  30. package/dist/reset-diagnostics.js.map +1 -0
  31. package/dist/sdk.d.ts +7 -23
  32. package/dist/sdk.d.ts.map +1 -1
  33. package/dist/sdk.js +211 -174
  34. package/dist/sdk.js.map +1 -1
  35. package/dist/workspace-transition-interactive.d.ts +1 -0
  36. package/dist/workspace-transition-interactive.d.ts.map +1 -1
  37. package/dist/workspace-transition-interactive.js +8 -18
  38. package/dist/workspace-transition-interactive.js.map +1 -1
  39. package/extensions/__integration__/audit-findings.test.ts +4 -5
  40. package/extensions/_icons/index.ts +2 -4
  41. package/extensions/_shared/__tests__/image-metadata.test.ts +33 -0
  42. package/extensions/_shared/__tests__/shell-policy.test.ts +19 -0
  43. package/extensions/_shared/__tests__/terminal-links.test.ts +18 -0
  44. package/extensions/_shared/image-metadata.ts +99 -0
  45. package/extensions/_shared/inline-preview.ts +1 -1
  46. package/extensions/_shared/shell-policy.ts +121 -1
  47. package/extensions/_shared/terminal-links.ts +22 -0
  48. package/extensions/ask-user-question-tool/index.ts +0 -3
  49. package/extensions/clear/__tests__/clear.test.ts +269 -2
  50. package/extensions/command-expansion/index.ts +9 -3
  51. package/extensions/context-files/index.ts +5 -1
  52. package/extensions/context-fork/__tests__/context-fork.test.ts +94 -1
  53. package/extensions/context-fork/extension.json +1 -1
  54. package/extensions/context-fork/frontmatter-index.ts +6 -1
  55. package/extensions/context-fork/index.ts +32 -0
  56. package/extensions/edit-tool-enhanced/index.ts +2 -1
  57. package/extensions/git-status/__tests__/git-status.test.ts +65 -2
  58. package/extensions/git-status/index.ts +268 -98
  59. package/extensions/hooks/index.ts +33 -11
  60. package/extensions/loop/index.ts +14 -1
  61. package/extensions/lsp/index.ts +64 -13
  62. package/extensions/lsp/package.json +2 -2
  63. package/extensions/minimal-skill-display/index.ts +7 -1
  64. package/extensions/random-spinner/index.ts +7 -642
  65. package/extensions/read-tool-enhanced/index.ts +13 -10
  66. package/extensions/render-stabilizer/__tests__/render-stabilizer.test.ts +2 -3
  67. package/extensions/render-stabilizer/index.ts +6 -6
  68. package/extensions/rewind/__tests__/session-files.test.ts +115 -0
  69. package/extensions/rewind/__tests__/snapshots.test.ts +23 -0
  70. package/extensions/rewind/index.ts +5 -0
  71. package/extensions/rewind/session-files.ts +138 -0
  72. package/extensions/rewind/snapshots.ts +104 -5
  73. package/extensions/skill-commands/index.ts +6 -1
  74. package/extensions/slash-command-bridge/__tests__/slash-command-bridge.test.ts +26 -0
  75. package/extensions/slash-command-bridge/index.ts +14 -2
  76. package/extensions/subagent-tool/model-resolver.ts +274 -7
  77. package/extensions/subagent-tool/schema.ts +1 -2
  78. package/extensions/tasks/commands/register-tasks-extension.ts +9 -9
  79. package/extensions/teams-tool/tools/register-extension.ts +1 -3
  80. package/extensions/teams-tool/tools/teammate-tools.ts +1 -2
  81. package/extensions/web-search-tool/index.ts +2 -1
  82. package/extensions/wezterm-pane-control/index.ts +1 -2
  83. package/extensions/write-tool-enhanced/index.ts +2 -1
  84. package/node_modules/@mariozechner/pi-tui/README.md +56 -34
  85. package/node_modules/@mariozechner/pi-tui/dist/autocomplete.d.ts +18 -13
  86. package/node_modules/@mariozechner/pi-tui/dist/autocomplete.d.ts.map +1 -1
  87. package/node_modules/@mariozechner/pi-tui/dist/autocomplete.js +182 -113
  88. package/node_modules/@mariozechner/pi-tui/dist/autocomplete.js.map +1 -1
  89. package/node_modules/@mariozechner/pi-tui/dist/components/cancellable-loader.js +3 -3
  90. package/node_modules/@mariozechner/pi-tui/dist/components/cancellable-loader.js.map +1 -1
  91. package/node_modules/@mariozechner/pi-tui/dist/components/editor.d.ts +45 -36
  92. package/node_modules/@mariozechner/pi-tui/dist/components/editor.d.ts.map +1 -1
  93. package/node_modules/@mariozechner/pi-tui/dist/components/editor.js +489 -325
  94. package/node_modules/@mariozechner/pi-tui/dist/components/editor.js.map +1 -1
  95. package/node_modules/@mariozechner/pi-tui/dist/components/image.d.ts +1 -99
  96. package/node_modules/@mariozechner/pi-tui/dist/components/image.d.ts.map +1 -1
  97. package/node_modules/@mariozechner/pi-tui/dist/components/image.js +17 -192
  98. package/node_modules/@mariozechner/pi-tui/dist/components/image.js.map +1 -1
  99. package/node_modules/@mariozechner/pi-tui/dist/components/input.d.ts.map +1 -1
  100. package/node_modules/@mariozechner/pi-tui/dist/components/input.js +57 -60
  101. package/node_modules/@mariozechner/pi-tui/dist/components/input.js.map +1 -1
  102. package/node_modules/@mariozechner/pi-tui/dist/components/loader.d.ts +2 -69
  103. package/node_modules/@mariozechner/pi-tui/dist/components/loader.d.ts.map +1 -1
  104. package/node_modules/@mariozechner/pi-tui/dist/components/loader.js +5 -102
  105. package/node_modules/@mariozechner/pi-tui/dist/components/loader.js.map +1 -1
  106. package/node_modules/@mariozechner/pi-tui/dist/components/markdown.d.ts.map +1 -1
  107. package/node_modules/@mariozechner/pi-tui/dist/components/markdown.js +111 -53
  108. package/node_modules/@mariozechner/pi-tui/dist/components/markdown.js.map +1 -1
  109. package/node_modules/@mariozechner/pi-tui/dist/components/select-list.d.ts +19 -1
  110. package/node_modules/@mariozechner/pi-tui/dist/components/select-list.d.ts.map +1 -1
  111. package/node_modules/@mariozechner/pi-tui/dist/components/select-list.js +78 -67
  112. package/node_modules/@mariozechner/pi-tui/dist/components/select-list.js.map +1 -1
  113. package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.d.ts +1 -25
  114. package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.d.ts.map +1 -1
  115. package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.js +13 -50
  116. package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.js.map +1 -1
  117. package/node_modules/@mariozechner/pi-tui/dist/index.d.ts +8 -10
  118. package/node_modules/@mariozechner/pi-tui/dist/index.d.ts.map +1 -1
  119. package/node_modules/@mariozechner/pi-tui/dist/index.js +6 -9
  120. package/node_modules/@mariozechner/pi-tui/dist/index.js.map +1 -1
  121. package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts +108 -238
  122. package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts.map +1 -1
  123. package/node_modules/@mariozechner/pi-tui/dist/keybindings.js +108 -365
  124. package/node_modules/@mariozechner/pi-tui/dist/keybindings.js.map +1 -1
  125. package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts +33 -48
  126. package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts.map +1 -1
  127. package/node_modules/@mariozechner/pi-tui/dist/keys.js +239 -155
  128. package/node_modules/@mariozechner/pi-tui/dist/keys.js.map +1 -1
  129. package/node_modules/@mariozechner/pi-tui/dist/terminal-image.d.ts +14 -94
  130. package/node_modules/@mariozechner/pi-tui/dist/terminal-image.d.ts.map +1 -1
  131. package/node_modules/@mariozechner/pi-tui/dist/terminal-image.js +44 -186
  132. package/node_modules/@mariozechner/pi-tui/dist/terminal-image.js.map +1 -1
  133. package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts +13 -58
  134. package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts.map +1 -1
  135. package/node_modules/@mariozechner/pi-tui/dist/terminal.js +78 -111
  136. package/node_modules/@mariozechner/pi-tui/dist/terminal.js.map +1 -1
  137. package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts +24 -110
  138. package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts.map +1 -1
  139. package/node_modules/@mariozechner/pi-tui/dist/tui.js +188 -435
  140. package/node_modules/@mariozechner/pi-tui/dist/tui.js.map +1 -1
  141. package/node_modules/@mariozechner/pi-tui/dist/utils.d.ts +0 -18
  142. package/node_modules/@mariozechner/pi-tui/dist/utils.d.ts.map +1 -1
  143. package/node_modules/@mariozechner/pi-tui/dist/utils.js +251 -119
  144. package/node_modules/@mariozechner/pi-tui/dist/utils.js.map +1 -1
  145. package/node_modules/@mariozechner/pi-tui/package.json +6 -6
  146. package/node_modules/@mariozechner/pi-tui/src/__tests__/__snapshots__/render.test.ts.snap +3 -40
  147. package/node_modules/@mariozechner/pi-tui/src/__tests__/image-component.test.ts +71 -81
  148. package/node_modules/@mariozechner/pi-tui/src/__tests__/render.test.ts +0 -33
  149. package/node_modules/@mariozechner/pi-tui/src/__tests__/terminal-image.test.ts +93 -334
  150. package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-render-scheduling.test.ts +1 -1
  151. package/node_modules/@mariozechner/pi-tui/src/__tests__/utils.test.ts +11 -196
  152. package/node_modules/@mariozechner/pi-tui/src/autocomplete.ts +228 -142
  153. package/node_modules/@mariozechner/pi-tui/src/components/cancellable-loader.ts +3 -3
  154. package/node_modules/@mariozechner/pi-tui/src/components/editor.ts +624 -390
  155. package/node_modules/@mariozechner/pi-tui/src/components/image.ts +17 -227
  156. package/node_modules/@mariozechner/pi-tui/src/components/input.ts +71 -63
  157. package/node_modules/@mariozechner/pi-tui/src/components/loader.ts +5 -137
  158. package/node_modules/@mariozechner/pi-tui/src/components/markdown.ts +143 -52
  159. package/node_modules/@mariozechner/pi-tui/src/components/select-list.ts +136 -70
  160. package/node_modules/@mariozechner/pi-tui/src/components/settings-list.ts +12 -51
  161. package/node_modules/@mariozechner/pi-tui/src/index.ts +17 -36
  162. package/node_modules/@mariozechner/pi-tui/src/keybindings.ts +148 -421
  163. package/node_modules/@mariozechner/pi-tui/src/keys.ts +253 -181
  164. package/node_modules/@mariozechner/pi-tui/src/terminal-image.ts +51 -252
  165. package/node_modules/@mariozechner/pi-tui/src/terminal.ts +78 -133
  166. package/node_modules/@mariozechner/pi-tui/src/tui.ts +202 -478
  167. package/node_modules/@mariozechner/pi-tui/src/utils.ts +289 -125
  168. package/node_modules/@mariozechner/pi-tui/tsconfig.build.json +1 -0
  169. package/package.json +13 -13
  170. package/packages/tallow-tui/node_modules/@types/mime-types/README.md +8 -2
  171. package/packages/tallow-tui/node_modules/@types/mime-types/index.d.ts +6 -0
  172. package/packages/tallow-tui/node_modules/@types/mime-types/package.json +9 -3
  173. package/packages/tallow-tui/node_modules/get-east-asian-width/lookup-data.js +18 -0
  174. package/packages/tallow-tui/node_modules/get-east-asian-width/lookup.js +116 -384
  175. package/packages/tallow-tui/node_modules/get-east-asian-width/package.json +5 -4
  176. package/packages/tallow-tui/node_modules/get-east-asian-width/utilities.js +24 -0
  177. package/packages/tallow-tui/node_modules/marked/README.md +5 -4
  178. package/packages/tallow-tui/node_modules/marked/bin/main.js +10 -8
  179. package/packages/tallow-tui/node_modules/marked/bin/marked.js +2 -1
  180. package/packages/tallow-tui/node_modules/marked/lib/marked.d.ts +156 -125
  181. package/packages/tallow-tui/node_modules/marked/lib/marked.esm.js +67 -2179
  182. package/packages/tallow-tui/node_modules/marked/lib/marked.esm.js.map +3 -3
  183. package/packages/tallow-tui/node_modules/marked/lib/marked.umd.js +67 -2201
  184. package/packages/tallow-tui/node_modules/marked/lib/marked.umd.js.map +3 -3
  185. package/packages/tallow-tui/node_modules/marked/man/marked.1 +4 -2
  186. package/packages/tallow-tui/node_modules/marked/man/marked.1.md +2 -1
  187. package/packages/tallow-tui/node_modules/marked/package.json +26 -34
  188. package/skills/tallow-expert/SKILL.md +3 -5
  189. package/node_modules/@mariozechner/pi-tui/dist/border-styles.d.ts +0 -32
  190. package/node_modules/@mariozechner/pi-tui/dist/border-styles.d.ts.map +0 -1
  191. package/node_modules/@mariozechner/pi-tui/dist/border-styles.js +0 -46
  192. package/node_modules/@mariozechner/pi-tui/dist/border-styles.js.map +0 -1
  193. package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.d.ts +0 -52
  194. package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.d.ts.map +0 -1
  195. package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.js +0 -89
  196. package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.js.map +0 -1
  197. package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.d.ts +0 -14
  198. package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.d.ts.map +0 -1
  199. package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.js +0 -55
  200. package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.js.map +0 -1
  201. package/node_modules/@mariozechner/pi-tui/src/__tests__/editor-change-listener.test.ts +0 -121
  202. package/node_modules/@mariozechner/pi-tui/src/__tests__/editor-ghost-text.test.ts +0 -112
  203. package/node_modules/@mariozechner/pi-tui/src/__tests__/mouse-events.test.ts +0 -134
  204. package/node_modules/@mariozechner/pi-tui/src/__tests__/settings-list.test.ts +0 -81
  205. package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-diff-regression.test.ts +0 -555
  206. package/node_modules/@mariozechner/pi-tui/src/border-styles.ts +0 -60
  207. package/node_modules/@mariozechner/pi-tui/src/components/bordered-box.ts +0 -113
  208. package/node_modules/@mariozechner/pi-tui/src/test-utils/capability-env.ts +0 -56
  209. package/packages/tallow-tui/node_modules/marked/lib/marked.cjs +0 -2211
  210. package/packages/tallow-tui/node_modules/marked/lib/marked.cjs.map +0 -7
  211. package/packages/tallow-tui/node_modules/marked/lib/marked.d.cts +0 -728
  212. package/packages/tallow-tui/node_modules/marked/marked.min.js +0 -69
@@ -2,7 +2,7 @@ import { describe, expect, it } from "bun:test";
2
2
  import renderStabilizerExtension from "../index.js";
3
3
 
4
4
  describe("render-stabilizer extension", () => {
5
- it("registers session_start and session_before_switch handlers", () => {
5
+ it("registers only the legacy session_before_switch hook", () => {
6
6
  const handlers = new Map<string, unknown[]>();
7
7
 
8
8
  const mockPi = {
@@ -14,8 +14,7 @@ describe("render-stabilizer extension", () => {
14
14
 
15
15
  renderStabilizerExtension(mockPi as never);
16
16
 
17
- expect(handlers.has("session_start")).toBe(true);
18
- expect(handlers.get("session_start")?.length).toBe(1);
17
+ expect(handlers.has("session_start")).toBe(false);
19
18
  expect(handlers.has("session_before_switch")).toBe(true);
20
19
  expect(handlers.get("session_before_switch")?.length).toBe(1);
21
20
  });
@@ -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) => {
@@ -0,0 +1,115 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { listLiveSessionIdsForCwd } from "../session-files.js";
6
+
7
+ /**
8
+ * Encode a cwd into the session directory name used by tallow.
9
+ *
10
+ * @param cwd - Absolute working directory path
11
+ * @returns Encoded directory name
12
+ */
13
+ function encodeSessionDirName(cwd: string): string {
14
+ const withoutLeadingSlash = cwd.startsWith("/") || cwd.startsWith("\\") ? cwd.slice(1) : cwd;
15
+ const safeName = withoutLeadingSlash
16
+ .replaceAll("/", "-")
17
+ .replaceAll("\\", "-")
18
+ .replaceAll(":", "-");
19
+ return `--${safeName}--`;
20
+ }
21
+
22
+ /**
23
+ * Create a minimal JSONL session file for a test home/cwd pair.
24
+ *
25
+ * @param homeDir - Tallow home directory
26
+ * @param cwd - Session working directory
27
+ * @param sessionId - Session id to persist in the header
28
+ * @param fileName - Session filename to create
29
+ * @returns Absolute path to the created session file
30
+ */
31
+ function createSessionFile(
32
+ homeDir: string,
33
+ cwd: string,
34
+ sessionId: string,
35
+ fileName: string
36
+ ): string {
37
+ const sessionDir = join(homeDir, "sessions", encodeSessionDirName(cwd));
38
+ mkdirSync(sessionDir, { recursive: true });
39
+ const filePath = join(sessionDir, fileName);
40
+ writeFileSync(
41
+ filePath,
42
+ `${JSON.stringify({ cwd, id: sessionId, timestamp: new Date().toISOString(), type: "session", version: 3 })}\n`
43
+ );
44
+ return filePath;
45
+ }
46
+
47
+ describe("listLiveSessionIdsForCwd", () => {
48
+ const originalHome = process.env.HOME;
49
+ const originalTallowHome = process.env.TALLOW_CODING_AGENT_DIR;
50
+ const originalPiHome = process.env.PI_CODING_AGENT_DIR;
51
+ let tmpRoot: string;
52
+
53
+ beforeEach(() => {
54
+ tmpRoot = mkdtempSync(join(tmpdir(), "rewind-session-files-"));
55
+ process.env.HOME = tmpRoot;
56
+ delete process.env.PI_CODING_AGENT_DIR;
57
+ delete process.env.TALLOW_CODING_AGENT_DIR;
58
+ });
59
+
60
+ afterEach(() => {
61
+ if (originalHome === undefined) {
62
+ delete process.env.HOME;
63
+ } else {
64
+ process.env.HOME = originalHome;
65
+ }
66
+
67
+ if (originalTallowHome === undefined) {
68
+ delete process.env.TALLOW_CODING_AGENT_DIR;
69
+ } else {
70
+ process.env.TALLOW_CODING_AGENT_DIR = originalTallowHome;
71
+ }
72
+
73
+ if (originalPiHome === undefined) {
74
+ delete process.env.PI_CODING_AGENT_DIR;
75
+ } else {
76
+ process.env.PI_CODING_AGENT_DIR = originalPiHome;
77
+ }
78
+
79
+ rmSync(tmpRoot, { force: true, recursive: true });
80
+ });
81
+
82
+ it("finds live session ids across the default and active tallow homes", () => {
83
+ const cwd = "/Users/kevin/dev/tallow";
84
+ const defaultHome = join(tmpRoot, ".tallow");
85
+ const activeHome = join(tmpRoot, ".tallow-project");
86
+ process.env.TALLOW_CODING_AGENT_DIR = activeHome;
87
+
88
+ createSessionFile(defaultHome, cwd, "default-session", "2026-04-20_default-session.jsonl");
89
+ createSessionFile(activeHome, cwd, "active-session", "2026-04-20_active-session.jsonl");
90
+ createSessionFile(
91
+ activeHome,
92
+ "/Users/kevin/dev/other",
93
+ "other-project",
94
+ "2026-04-20_other.jsonl"
95
+ );
96
+
97
+ const ids = listLiveSessionIdsForCwd(cwd, [defaultHome, activeHome]);
98
+ expect([...ids].sort((a, b) => a.localeCompare(b))).toEqual([
99
+ "active-session",
100
+ "default-session",
101
+ ]);
102
+ });
103
+
104
+ it("ignores unreadable or corrupt session files", () => {
105
+ const cwd = "/Users/kevin/dev/tallow";
106
+ const defaultHome = join(tmpRoot, ".tallow");
107
+ const sessionDir = join(defaultHome, "sessions", encodeSessionDirName(cwd));
108
+ mkdirSync(sessionDir, { recursive: true });
109
+ writeFileSync(join(sessionDir, "corrupt.jsonl"), "not-json\n");
110
+ createSessionFile(defaultHome, cwd, "valid-session", "2026-04-20_valid-session.jsonl");
111
+
112
+ const ids = listLiveSessionIdsForCwd(cwd, [defaultHome]);
113
+ expect([...ids]).toEqual(["valid-session"]);
114
+ });
115
+ });
@@ -247,6 +247,29 @@ describe("SnapshotManager", () => {
247
247
  otherMgr.cleanup();
248
248
  });
249
249
 
250
+ it("should prune stale session refs while preserving live sessions", () => {
251
+ const otherMgr = new SnapshotManager(tmpDir, "other-session");
252
+ const staleMgr = new SnapshotManager(tmpDir, "stale-session");
253
+
254
+ writeFileSync(join(tmpDir, "a.txt"), "live-1");
255
+ mgr.createSnapshot(1);
256
+ writeFileSync(join(tmpDir, "a.txt"), "live-2");
257
+ otherMgr.createSnapshot(1);
258
+ writeFileSync(join(tmpDir, "a.txt"), "stale");
259
+ staleMgr.createSnapshot(1);
260
+
261
+ const deletedRefs = mgr.cleanupStaleSessions(new Set(["other-session", "test-session"]));
262
+ expect(deletedRefs).toBe(1);
263
+
264
+ const staleRefs = git(["for-each-ref", "refs/tallow/rewind/stale-session/"], tmpDir);
265
+ expect(staleRefs.trim()).toBe("");
266
+
267
+ const liveRefs = git(["for-each-ref", "refs/tallow/rewind/test-session/"], tmpDir);
268
+ expect(liveRefs.trim()).not.toBe("");
269
+ const otherRefs = git(["for-each-ref", "refs/tallow/rewind/other-session/"], tmpDir);
270
+ expect(otherRefs.trim()).not.toBe("");
271
+ });
272
+
250
273
  it("should return empty list when no snapshots exist", () => {
251
274
  expect(mgr.listSnapshots()).toHaveLength(0);
252
275
  });
@@ -14,6 +14,7 @@
14
14
  */
15
15
 
16
16
  import type { CustomEntry, ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
17
+ import { listLiveSessionIdsForCwd } from "./session-files.js";
17
18
  import { SnapshotManager } from "./snapshots.js";
18
19
  import type { RewindSnapshotEntry } from "./tracker.js";
19
20
  import { FileTracker } from "./tracker.js";
@@ -50,6 +51,10 @@ export default function rewind(pi: ExtensionAPI): void {
50
51
  snapshots = mgr;
51
52
  tracker.reset();
52
53
 
54
+ const liveSessionIds = listLiveSessionIdsForCwd(context.cwd);
55
+ liveSessionIds.add(sessionId);
56
+ mgr.cleanupStaleSessions(liveSessionIds);
57
+
53
58
  // Restore tracker state from persisted session entries
54
59
  const entries = context.sessionManager.getEntries();
55
60
  const snapshotEntries = entries
@@ -0,0 +1,138 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import type { SessionHeader } from "@mariozechner/pi-coding-agent";
5
+ import { getDefaultTallowHomeDir, getTallowHomeDir } from "../_shared/tallow-paths.js";
6
+
7
+ /**
8
+ * Encode a cwd into the per-project session directory name used by tallow.
9
+ *
10
+ * @param cwd - Absolute working directory path
11
+ * @returns Encoded directory name (for example `--Users-kevin-dev-tallow--`)
12
+ */
13
+ function encodeSessionDirName(cwd: string): string {
14
+ const withoutLeadingSlash = cwd.startsWith("/") || cwd.startsWith("\\") ? cwd.slice(1) : cwd;
15
+ const safeName = withoutLeadingSlash
16
+ .replaceAll("/", "-")
17
+ .replaceAll("\\", "-")
18
+ .replaceAll(":", "-");
19
+ return `--${safeName}--`;
20
+ }
21
+
22
+ /**
23
+ * Read additional tallow home directories from the maintainer's work-dir config.
24
+ *
25
+ * @returns Extra configured tallow home directories
26
+ */
27
+ function readConfiguredHomeDirs(): string[] {
28
+ const workDirsPath = join(homedir(), ".config", "tallow-work-dirs");
29
+
30
+ try {
31
+ const content = readFileSync(workDirsPath, "utf-8");
32
+ return content
33
+ .split("\n")
34
+ .map((line) => line.trim())
35
+ .filter((line) => line.length > 0 && !line.startsWith("#"))
36
+ .map((line) => {
37
+ const colonIndex = line.indexOf(":");
38
+ return colonIndex === -1 ? "" : line.slice(colonIndex + 1).trim();
39
+ })
40
+ .filter((configDir) => configDir.length > 0);
41
+ } catch {
42
+ // Missing or unreadable work-dir config means there are no extra homes to scan.
43
+ return [];
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Build the set of tallow homes that should be searched for session files.
49
+ *
50
+ * @param homeDirs - Optional explicit home directories (used by tests to avoid scanning real homes)
51
+ * @returns Unique tallow home directories to inspect
52
+ */
53
+ function resolveHomeDirs(homeDirs?: readonly string[]): Set<string> {
54
+ if (homeDirs) {
55
+ return new Set(homeDirs);
56
+ }
57
+
58
+ return new Set<string>([
59
+ getDefaultTallowHomeDir(),
60
+ getTallowHomeDir(),
61
+ ...readConfiguredHomeDirs(),
62
+ ]);
63
+ }
64
+
65
+ /**
66
+ * Discover every session directory that can contain sessions for a specific cwd.
67
+ *
68
+ * Tallow can store sessions under the default home, the active runtime home,
69
+ * and any per-project homes listed in `~/.config/tallow-work-dirs`.
70
+ *
71
+ * @param cwd - Working directory whose session subdirectory should be resolved
72
+ * @param homeDirs - Optional explicit home directories (used by tests to avoid scanning real homes)
73
+ * @returns Existing session directory paths across all known tallow homes
74
+ */
75
+ function discoverSessionDirsForCwd(cwd: string, homeDirs?: readonly string[]): string[] {
76
+ const dirName = encodeSessionDirName(cwd);
77
+ const dirs = new Set<string>();
78
+
79
+ for (const home of resolveHomeDirs(homeDirs)) {
80
+ const sessionsDir = join(home, "sessions", dirName);
81
+ if (existsSync(sessionsDir)) {
82
+ dirs.add(sessionsDir);
83
+ }
84
+ }
85
+
86
+ return [...dirs];
87
+ }
88
+
89
+ /**
90
+ * Read the session id from a JSONL session file.
91
+ *
92
+ * The conventional filename already contains the id, but parsing the header is
93
+ * more robust for renamed or migrated session files.
94
+ *
95
+ * @param filePath - Absolute path to the session JSONL file
96
+ * @returns Session id when readable and valid, otherwise null
97
+ */
98
+ function readSessionId(filePath: string): string | null {
99
+ try {
100
+ const content = readFileSync(filePath, "utf-8");
101
+ const firstNewline = content.indexOf("\n");
102
+ const headerLine = firstNewline === -1 ? content : content.slice(0, firstNewline);
103
+ const header = JSON.parse(headerLine) as SessionHeader;
104
+ if (header.type !== "session") return null;
105
+ return typeof header.id === "string" && header.id.length > 0 ? header.id : null;
106
+ } catch {
107
+ return null;
108
+ }
109
+ }
110
+
111
+ /**
112
+ * List every live session id for the current cwd across all known tallow homes.
113
+ *
114
+ * @param cwd - Working directory whose sessions should be considered live
115
+ * @param homeDirs - Optional explicit home directories to scan instead of runtime discovery
116
+ * @returns Set of live session ids
117
+ */
118
+ export function listLiveSessionIdsForCwd(cwd: string, homeDirs?: readonly string[]): Set<string> {
119
+ const ids = new Set<string>();
120
+
121
+ for (const sessionsDir of discoverSessionDirsForCwd(cwd, homeDirs)) {
122
+ let files: string[];
123
+ try {
124
+ files = readdirSync(sessionsDir).filter((file) => file.endsWith(".jsonl"));
125
+ } catch {
126
+ continue;
127
+ }
128
+
129
+ for (const file of files) {
130
+ const sessionId = readSessionId(join(sessionsDir, file));
131
+ if (sessionId) {
132
+ ids.add(sessionId);
133
+ }
134
+ }
135
+ }
136
+
137
+ return ids;
138
+ }
@@ -277,13 +277,112 @@ export class SnapshotManager {
277
277
  }
278
278
 
279
279
  /**
280
- * Removes all refs for the current session.
280
+ * Remove all refs for the current session.
281
+ *
282
+ * @returns Number of refs deleted for this session
283
+ */
284
+ cleanup(): number {
285
+ return this.cleanupSession(this.getSessionIdFromPrefix(this.refPrefix));
286
+ }
287
+
288
+ /**
289
+ * Remove rewind refs whose session ids no longer exist on disk.
290
+ *
291
+ * @param liveSessionIds - Session ids that still have backing session files
292
+ * @returns Count of deleted refs across all stale sessions
293
+ */
294
+ cleanupStaleSessions(liveSessionIds: ReadonlySet<string>): number {
295
+ let deletedRefs = 0;
296
+
297
+ for (const sessionId of this.listSessionIds()) {
298
+ if (liveSessionIds.has(sessionId)) continue;
299
+ deletedRefs += this.cleanupSession(sessionId);
300
+ }
301
+
302
+ return deletedRefs;
303
+ }
304
+
305
+ /**
306
+ * List every session id that currently has rewind refs in this repository.
307
+ *
308
+ * @returns Ordered unique session ids extracted from `refs/tallow/rewind/*`
309
+ */
310
+ private listSessionIds(): string[] {
311
+ const raw = this.git(["for-each-ref", "--format=%(refname)", "refs/tallow/rewind/"]);
312
+ if (!raw) return [];
313
+
314
+ const ids = new Set<string>();
315
+ for (const line of raw.split("\n")) {
316
+ const ref = this.normalizeRefName(line);
317
+ if (!ref) continue;
318
+ const sessionId = this.parseSessionIdFromRef(ref);
319
+ if (sessionId) {
320
+ ids.add(sessionId);
321
+ }
322
+ }
323
+
324
+ return [...ids].sort((a, b) => a.localeCompare(b));
325
+ }
326
+
327
+ /**
328
+ * Remove every rewind ref for a single session id.
329
+ *
330
+ * @param sessionId - Session id whose namespaced rewind refs should be deleted
331
+ * @returns Count of deleted refs
281
332
  */
282
- cleanup(): void {
283
- const snapshots = this.listSnapshots();
284
- for (const snap of snapshots) {
285
- this.git(["update-ref", "-d", snap.ref]);
333
+ private cleanupSession(sessionId: string): number {
334
+ const raw = this.git([
335
+ "for-each-ref",
336
+ "--format=%(refname)",
337
+ `refs/tallow/rewind/${sessionId}/`,
338
+ ]);
339
+ if (!raw) return 0;
340
+
341
+ let deletedRefs = 0;
342
+ for (const line of raw.split("\n")) {
343
+ const ref = this.normalizeRefName(line);
344
+ if (!ref) continue;
345
+ if (this.git(["update-ref", "-d", ref]) !== null) {
346
+ deletedRefs++;
347
+ }
286
348
  }
349
+
350
+ return deletedRefs;
351
+ }
352
+
353
+ /**
354
+ * Parse a session id from a full rewind ref name.
355
+ *
356
+ * @param ref - Full rewind ref name
357
+ * @returns Session id when the ref matches the rewind namespace, otherwise null
358
+ */
359
+ private parseSessionIdFromRef(ref: string): string | null {
360
+ const match = /^refs\/tallow\/rewind\/([^/]+)\/turn-\d+$/.exec(ref);
361
+ return match?.[1] ?? null;
362
+ }
363
+
364
+ /**
365
+ * Normalize a raw ref name line from git output.
366
+ *
367
+ * @param line - Raw line returned by `git for-each-ref`
368
+ * @returns Trimmed ref name without surrounding quotes
369
+ */
370
+ private normalizeRefName(line: string): string {
371
+ const trimmed = line.trim();
372
+ if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
373
+ return trimmed.slice(1, -1);
374
+ }
375
+ return trimmed;
376
+ }
377
+
378
+ /**
379
+ * Extract the current manager's session id from its ref prefix.
380
+ *
381
+ * @param refPrefix - Session-scoped rewind ref prefix
382
+ * @returns Session id portion of the prefix
383
+ */
384
+ private getSessionIdFromPrefix(refPrefix: string): string {
385
+ return refPrefix.replace(/^refs\/tallow\/rewind\//, "");
287
386
  }
288
387
 
289
388
  /**
@@ -199,7 +199,12 @@ export default function (pi: ExtensionAPI) {
199
199
  // Load skills synchronously during extension init for autocomplete to work.
200
200
  // includeDefaults: true picks up ~/.tallow/skills/ and ./skills/ (project).
201
201
  // extraSkillPaths adds shared dirs + Claude bridge paths.
202
- const { skills } = loadSkills({ agentDir, skillPaths: extraSkillPaths });
202
+ const { skills } = loadSkills({
203
+ cwd: process.cwd(),
204
+ agentDir,
205
+ skillPaths: extraSkillPaths,
206
+ includeDefaults: true,
207
+ });
203
208
 
204
209
  for (const skill of skills) {
205
210
  // Validate name before registration — invalid names produce broken commands
@@ -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);