@dungle-scrubs/tallow 0.8.27 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -1
- package/dist/cli.js +7 -1
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +2 -9
- package/dist/install.js.map +1 -1
- package/dist/interactive-mode-patch.d.ts.map +1 -1
- package/dist/interactive-mode-patch.js +20 -9
- package/dist/interactive-mode-patch.js.map +1 -1
- package/dist/model-metadata-overrides.d.ts +2 -5
- package/dist/model-metadata-overrides.d.ts.map +1 -1
- package/dist/model-metadata-overrides.js +23 -12
- package/dist/model-metadata-overrides.js.map +1 -1
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +20 -9
- package/dist/sdk.js.map +1 -1
- package/dist/workspace-transition-interactive.d.ts.map +1 -1
- package/dist/workspace-transition-interactive.js +53 -3
- package/dist/workspace-transition-interactive.js.map +1 -1
- package/dist/workspace-transition.d.ts +2 -1
- package/dist/workspace-transition.d.ts.map +1 -1
- package/dist/workspace-transition.js +16 -4
- package/dist/workspace-transition.js.map +1 -1
- package/extensions/__integration__/cd-tool-guidelines.test.ts +46 -0
- package/extensions/__integration__/welcome-screen.test.ts +240 -0
- package/extensions/_icons/__tests__/icons.test.ts +0 -1
- package/extensions/_icons/index.ts +0 -2
- package/extensions/_shared/pid-registry.ts +5 -5
- package/extensions/background-task-tool/index.ts +1 -1
- package/extensions/cd-tool/index.ts +4 -1
- package/extensions/context-fork/__tests__/context-fork.test.ts +9 -0
- package/extensions/edit-tool-enhanced/index.ts +3 -1
- package/extensions/health/__tests__/diagnostics.test.ts +25 -0
- package/extensions/health/index.ts +62 -1
- package/extensions/loop/__tests__/loop.test.ts +365 -1
- package/extensions/loop/index.ts +213 -3
- package/extensions/prompt-suggestions/__tests__/autocomplete.test.ts +111 -3
- package/extensions/prompt-suggestions/autocomplete.ts +23 -5
- package/extensions/prompt-suggestions/index.ts +62 -3
- package/extensions/read-tool-enhanced/index.ts +5 -1
- package/extensions/render-stabilizer/__tests__/render-stabilizer.test.ts +42 -0
- package/extensions/render-stabilizer/extension.json +5 -0
- package/extensions/render-stabilizer/index.ts +66 -0
- package/extensions/session-memory/index.ts +1 -1
- package/extensions/session-namer/index.ts +1 -1
- package/extensions/subagent-tool/__tests__/auto-cheap-model.test.ts +66 -6
- package/extensions/subagent-tool/__tests__/model-router-explicit-resolution.test.ts +79 -5
- package/extensions/subagent-tool/__tests__/presentation-rendering.test.ts +4 -4
- package/extensions/subagent-tool/index.ts +4 -2
- package/extensions/subagent-tool/process.ts +26 -8
- package/extensions/teams-tool/sessions/spawn.ts +2 -2
- package/extensions/welcome-screen/__tests__/welcome-screen.test.ts +35 -0
- package/extensions/welcome-screen/extension.json +20 -0
- package/extensions/welcome-screen/index.ts +189 -0
- package/node_modules/@mariozechner/pi-tui/dist/index.d.ts +2 -2
- package/node_modules/@mariozechner/pi-tui/dist/index.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/index.js +2 -2
- package/node_modules/@mariozechner/pi-tui/dist/index.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts +309 -25
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.js +392 -72
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts +30 -0
- package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keys.js +50 -6
- package/node_modules/@mariozechner/pi-tui/dist/keys.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts +27 -0
- package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/terminal.js +59 -4
- package/node_modules/@mariozechner/pi-tui/dist/terminal.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts +56 -0
- package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/tui.js +188 -5
- package/node_modules/@mariozechner/pi-tui/dist/tui.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/package.json +1 -1
- package/node_modules/@mariozechner/pi-tui/src/__tests__/mouse-events.test.ts +134 -0
- package/node_modules/@mariozechner/pi-tui/src/__tests__/tmux-compat.test.ts +204 -0
- package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-diff-regression.test.ts +49 -0
- package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-render-scheduling.test.ts +2 -0
- package/node_modules/@mariozechner/pi-tui/src/index.ts +11 -0
- package/node_modules/@mariozechner/pi-tui/src/keybindings.ts +478 -140
- package/node_modules/@mariozechner/pi-tui/src/keys.ts +84 -6
- package/node_modules/@mariozechner/pi-tui/src/terminal.ts +69 -4
- package/node_modules/@mariozechner/pi-tui/src/tui.ts +205 -5
- package/package.json +9 -9
- package/runtime/config.ts +7 -0
- package/runtime/model-metadata-overrides.ts +7 -0
- package/schemas/settings.schema.json +0 -5
- package/skills/tallow-expert/SKILL.md +6 -4
- package/extensions/plan-mode-tool/__tests__/e2e.mjs +0 -350
- package/extensions/plan-mode-tool/__tests__/index.test.ts +0 -213
- package/extensions/plan-mode-tool/__tests__/utils.test.ts +0 -381
- package/extensions/plan-mode-tool/extension.json +0 -22
- package/extensions/plan-mode-tool/index.ts +0 -583
- package/extensions/plan-mode-tool/utils.ts +0 -257
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration test: cd tool prompt guidelines.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that the cd tool's promptGuidelines are injected into the
|
|
5
|
+
* system prompt, preventing the model from combining cd with other tools.
|
|
6
|
+
*/
|
|
7
|
+
import { afterEach, describe, expect, it } from "bun:test";
|
|
8
|
+
import { createScriptedStreamFn } from "../../test-utils/mock-model.js";
|
|
9
|
+
import { createSessionRunner, type SessionRunner } from "../../test-utils/session-runner.js";
|
|
10
|
+
import cdToolExtension from "../cd-tool/index.js";
|
|
11
|
+
|
|
12
|
+
let runner: SessionRunner | undefined;
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
runner?.dispose();
|
|
16
|
+
runner = undefined;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("cd tool prompt guidelines", () => {
|
|
20
|
+
it("injects exclusive-call guideline into the system prompt", async () => {
|
|
21
|
+
runner = await createSessionRunner({
|
|
22
|
+
streamFn: createScriptedStreamFn([{ text: "ok" }]),
|
|
23
|
+
extensionFactories: [cdToolExtension],
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Run a prompt so the system prompt is built (includes tool guidelines)
|
|
27
|
+
await runner.run("hello");
|
|
28
|
+
|
|
29
|
+
const systemPrompt = runner.session.systemPrompt;
|
|
30
|
+
expect(systemPrompt).toContain("cd tool triggers an interactive workspace transition");
|
|
31
|
+
expect(systemPrompt).toContain("SOLE tool call");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("includes the cd tool in the available tools", async () => {
|
|
35
|
+
runner = await createSessionRunner({
|
|
36
|
+
streamFn: createScriptedStreamFn([{ text: "ok" }]),
|
|
37
|
+
extensionFactories: [cdToolExtension],
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
await runner.run("hello");
|
|
41
|
+
|
|
42
|
+
const systemPrompt = runner.session.systemPrompt;
|
|
43
|
+
// The tool description should appear (either in the tools section or as a snippet)
|
|
44
|
+
expect(systemPrompt).toContain("cd");
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E integration test for the welcome-screen extension.
|
|
3
|
+
*
|
|
4
|
+
* Boots a real tallow session with the bundled welcome-screen extension,
|
|
5
|
+
* binds extensions with a mock UI, and verifies:
|
|
6
|
+
* - setHeader IS called on fresh sessions
|
|
7
|
+
* - The rendered output contains the ASCII logo and version
|
|
8
|
+
* - setHeader is NOT called on resumed sessions with conversation history
|
|
9
|
+
*/
|
|
10
|
+
import { afterEach, describe, expect, it } from "bun:test";
|
|
11
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { visibleWidth } from "@mariozechner/pi-tui";
|
|
15
|
+
import { TALLOW_VERSION } from "../../src/config.js";
|
|
16
|
+
import { createTallowSession, type TallowSession } from "../../src/sdk.js";
|
|
17
|
+
import { withExclusiveTallowHome } from "../../test-utils/tallow-home-env.js";
|
|
18
|
+
import welcomeScreenExtension from "../welcome-screen/index.js";
|
|
19
|
+
|
|
20
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
/** Lines the ASCII logo must contain (stripped of ANSI). */
|
|
23
|
+
const LOGO_FRAGMENTS = ["▐████████████▌", "████", "▐█▌"];
|
|
24
|
+
|
|
25
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escape stripping requires matching \x1b
|
|
26
|
+
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
27
|
+
|
|
28
|
+
/** Strip ANSI escape sequences for content assertions. */
|
|
29
|
+
function stripAnsi(s: string): string {
|
|
30
|
+
return s.replace(ANSI_RE, "");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Create a session with only the welcome-screen extension loaded.
|
|
35
|
+
*
|
|
36
|
+
* @param cwd - Working directory for the session
|
|
37
|
+
* @returns TallowSession promise
|
|
38
|
+
*/
|
|
39
|
+
function createWelcomeSession(cwd: string): Promise<TallowSession> {
|
|
40
|
+
return withExclusiveTallowHome(cwd, () =>
|
|
41
|
+
createTallowSession({
|
|
42
|
+
cwd,
|
|
43
|
+
provider: "anthropic",
|
|
44
|
+
apiKey: "test-key",
|
|
45
|
+
session: { type: "memory" },
|
|
46
|
+
noBundledExtensions: true,
|
|
47
|
+
noBundledSkills: true,
|
|
48
|
+
extensionFactories: [welcomeScreenExtension],
|
|
49
|
+
})
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build a mock UI context that captures setHeader calls.
|
|
55
|
+
*
|
|
56
|
+
* @returns Object with the UI context and captured state
|
|
57
|
+
*/
|
|
58
|
+
function createCapturingUI(): {
|
|
59
|
+
uiContext: Record<string, unknown>;
|
|
60
|
+
capture: {
|
|
61
|
+
setHeaderCalled: boolean;
|
|
62
|
+
headerFactory: ((tui: unknown, theme: unknown) => { render(w: number): string[] }) | null;
|
|
63
|
+
};
|
|
64
|
+
} {
|
|
65
|
+
const capture = {
|
|
66
|
+
setHeaderCalled: false,
|
|
67
|
+
headerFactory: null as
|
|
68
|
+
| ((tui: unknown, theme: unknown) => { render(w: number): string[] })
|
|
69
|
+
| null,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const uiContext: Record<string, unknown> = {
|
|
73
|
+
notify: () => {},
|
|
74
|
+
confirm: async () => true,
|
|
75
|
+
input: async () => null,
|
|
76
|
+
select: async () => null,
|
|
77
|
+
custom: async () => null,
|
|
78
|
+
setWorkingMessage: () => {},
|
|
79
|
+
setHeader: (factory: typeof capture.headerFactory) => {
|
|
80
|
+
capture.setHeaderCalled = true;
|
|
81
|
+
capture.headerFactory = factory;
|
|
82
|
+
},
|
|
83
|
+
setFooter: () => {},
|
|
84
|
+
setToolsExpanded: () => {},
|
|
85
|
+
setEditorComponent: () => {},
|
|
86
|
+
addTerminalInputListener: () => () => {},
|
|
87
|
+
setStatus: () => {},
|
|
88
|
+
setWidget: () => {},
|
|
89
|
+
setTitle: () => {},
|
|
90
|
+
pasteToEditor: () => {},
|
|
91
|
+
setEditorText: () => {},
|
|
92
|
+
getEditorText: () => "",
|
|
93
|
+
editor: async () => undefined,
|
|
94
|
+
hasUI: true,
|
|
95
|
+
};
|
|
96
|
+
return { uiContext, capture };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let tmpDir: string | undefined;
|
|
100
|
+
let session: TallowSession | undefined;
|
|
101
|
+
|
|
102
|
+
afterEach(() => {
|
|
103
|
+
if (tmpDir) {
|
|
104
|
+
try {
|
|
105
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
106
|
+
} catch {
|
|
107
|
+
// best-effort
|
|
108
|
+
}
|
|
109
|
+
tmpDir = undefined;
|
|
110
|
+
}
|
|
111
|
+
session = undefined;
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// ── Tests ────────────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
describe("welcome-screen E2E", () => {
|
|
117
|
+
it("calls setHeader with the ASCII logo on a fresh session", async () => {
|
|
118
|
+
tmpDir = mkdtempSync(join(tmpdir(), "tallow-welcome-e2e-"));
|
|
119
|
+
session = await createWelcomeSession(tmpDir);
|
|
120
|
+
|
|
121
|
+
const { uiContext, capture } = createCapturingUI();
|
|
122
|
+
await session.session.bindExtensions({ uiContext });
|
|
123
|
+
|
|
124
|
+
// ── setHeader must have been invoked ──
|
|
125
|
+
expect(capture.setHeaderCalled).toBe(true);
|
|
126
|
+
expect(capture.headerFactory).not.toBeNull();
|
|
127
|
+
|
|
128
|
+
// ── Render the header and validate content ──
|
|
129
|
+
const component = capture.headerFactory?.(null, null);
|
|
130
|
+
expect(component).toBeDefined();
|
|
131
|
+
expect(typeof component?.render).toBe("function");
|
|
132
|
+
|
|
133
|
+
const lines = component?.render(80) ?? [];
|
|
134
|
+
const plainLines = lines.map(stripAnsi);
|
|
135
|
+
const joined = plainLines.join("\n");
|
|
136
|
+
|
|
137
|
+
// Logo fragments must be present
|
|
138
|
+
for (const frag of LOGO_FRAGMENTS) {
|
|
139
|
+
expect(joined).toContain(frag);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Version string must be present
|
|
143
|
+
expect(joined).toContain(`tallow v${TALLOW_VERSION}`);
|
|
144
|
+
}, 30_000);
|
|
145
|
+
|
|
146
|
+
it("renders lines centered within the given width", async () => {
|
|
147
|
+
tmpDir = mkdtempSync(join(tmpdir(), "tallow-welcome-e2e-"));
|
|
148
|
+
session = await createWelcomeSession(tmpDir);
|
|
149
|
+
|
|
150
|
+
const { uiContext, capture } = createCapturingUI();
|
|
151
|
+
await session.session.bindExtensions({ uiContext });
|
|
152
|
+
|
|
153
|
+
const component = capture.headerFactory?.(null, null);
|
|
154
|
+
expect(component).toBeDefined();
|
|
155
|
+
|
|
156
|
+
const width = 120;
|
|
157
|
+
const lines = component?.render(width) ?? [];
|
|
158
|
+
|
|
159
|
+
// Every line should fit within the width
|
|
160
|
+
for (const line of lines) {
|
|
161
|
+
expect(visibleWidth(line)).toBeLessThanOrEqual(width);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Logo lines should be approximately centered (left padding > 0)
|
|
165
|
+
const logoLine = lines[0]; // first line = logo top bar
|
|
166
|
+
const leading = logoLine.length - logoLine.trimStart().length;
|
|
167
|
+
expect(leading).toBeGreaterThan(0);
|
|
168
|
+
}, 30_000);
|
|
169
|
+
|
|
170
|
+
it("does NOT call setHeader on a resumed session with conversation entries", async () => {
|
|
171
|
+
tmpDir = mkdtempSync(join(tmpdir(), "tallow-welcome-e2e-"));
|
|
172
|
+
session = await createWelcomeSession(tmpDir);
|
|
173
|
+
|
|
174
|
+
// Inject conversation entries to simulate a resumed session
|
|
175
|
+
const sm = session.session.sessionManager;
|
|
176
|
+
sm.appendMessage({
|
|
177
|
+
role: "user",
|
|
178
|
+
content: [{ type: "text", text: "hello" }],
|
|
179
|
+
});
|
|
180
|
+
sm.appendMessage({
|
|
181
|
+
role: "assistant",
|
|
182
|
+
content: [{ type: "text", text: "hi" }],
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const { uiContext, capture } = createCapturingUI();
|
|
186
|
+
await session.session.bindExtensions({ uiContext });
|
|
187
|
+
|
|
188
|
+
// setHeader must NOT be called for resumed sessions
|
|
189
|
+
expect(capture.setHeaderCalled).toBe(false);
|
|
190
|
+
}, 30_000);
|
|
191
|
+
|
|
192
|
+
it("does NOT skip fresh sessions that only have metadata entries", async () => {
|
|
193
|
+
tmpDir = mkdtempSync(join(tmpdir(), "tallow-welcome-e2e-"));
|
|
194
|
+
session = await createWelcomeSession(tmpDir);
|
|
195
|
+
|
|
196
|
+
// Verify metadata entries exist but don't prevent the welcome screen
|
|
197
|
+
const entries = session.session.sessionManager.getEntries();
|
|
198
|
+
const metadataOnly = entries.every(
|
|
199
|
+
(e) => !("role" in e) || !["user", "assistant"].includes(String(e.role))
|
|
200
|
+
);
|
|
201
|
+
expect(metadataOnly).toBe(true);
|
|
202
|
+
expect(entries.length).toBeGreaterThan(0); // model_change, thinking_level_change
|
|
203
|
+
|
|
204
|
+
const { uiContext, capture } = createCapturingUI();
|
|
205
|
+
await session.session.bindExtensions({ uiContext });
|
|
206
|
+
|
|
207
|
+
// setHeader MUST be called even though metadata entries exist
|
|
208
|
+
expect(capture.setHeaderCalled).toBe(true);
|
|
209
|
+
}, 30_000);
|
|
210
|
+
|
|
211
|
+
it("defaults quietStartup to true so resource listing is suppressed", async () => {
|
|
212
|
+
tmpDir = mkdtempSync(join(tmpdir(), "tallow-welcome-e2e-"));
|
|
213
|
+
session = await createWelcomeSession(tmpDir);
|
|
214
|
+
|
|
215
|
+
// The settingsManager should have quietStartup=true by default,
|
|
216
|
+
// which suppresses the keybinding hints and [Context]/[Skills] listing.
|
|
217
|
+
const quiet = session.session.settingsManager.getQuietStartup();
|
|
218
|
+
expect(quiet).toBe(true);
|
|
219
|
+
}, 30_000);
|
|
220
|
+
|
|
221
|
+
it("respects explicit quietStartup=false override", async () => {
|
|
222
|
+
tmpDir = mkdtempSync(join(tmpdir(), "tallow-welcome-e2e-"));
|
|
223
|
+
session = await withExclusiveTallowHome(tmpDir, () =>
|
|
224
|
+
createTallowSession({
|
|
225
|
+
cwd: tmpDir ?? tmpdir(),
|
|
226
|
+
provider: "anthropic",
|
|
227
|
+
apiKey: "test-key",
|
|
228
|
+
session: { type: "memory" },
|
|
229
|
+
noBundledExtensions: true,
|
|
230
|
+
noBundledSkills: true,
|
|
231
|
+
extensionFactories: [welcomeScreenExtension],
|
|
232
|
+
settings: { quietStartup: false },
|
|
233
|
+
})
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
// User explicitly opted out — resource listing should be visible
|
|
237
|
+
const quiet = session.session.settingsManager.getQuietStartup();
|
|
238
|
+
expect(quiet).toBe(false);
|
|
239
|
+
}, 30_000);
|
|
240
|
+
});
|
|
@@ -13,7 +13,6 @@ describe("Icon Registry", () => {
|
|
|
13
13
|
expect(registry.getString("active")).toBe("⚡");
|
|
14
14
|
expect(registry.getString("blocked")).toBe("◇");
|
|
15
15
|
expect(registry.getString("unavailable")).toBe("⊘");
|
|
16
|
-
expect(registry.getString("plan_mode")).toBe("⏸");
|
|
17
16
|
expect(registry.getString("task_list")).toBe("📋");
|
|
18
17
|
expect(registry.getString("comment")).toBe("💬");
|
|
19
18
|
});
|
|
@@ -32,7 +32,6 @@ export type IconKey =
|
|
|
32
32
|
| "active"
|
|
33
33
|
| "blocked"
|
|
34
34
|
| "unavailable"
|
|
35
|
-
| "plan_mode"
|
|
36
35
|
| "task_list"
|
|
37
36
|
| "comment";
|
|
38
37
|
|
|
@@ -63,7 +62,6 @@ export const ICON_DEFAULTS: Record<IconKey, IconValue> = {
|
|
|
63
62
|
active: "⚡",
|
|
64
63
|
blocked: "◇",
|
|
65
64
|
unavailable: "⊘",
|
|
66
|
-
plan_mode: "⏸",
|
|
67
65
|
task_list: "📋",
|
|
68
66
|
comment: "💬",
|
|
69
67
|
};
|
|
@@ -12,10 +12,6 @@
|
|
|
12
12
|
import { spawnSync } from "node:child_process";
|
|
13
13
|
import { existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
14
14
|
import { dirname, join } from "node:path";
|
|
15
|
-
import {
|
|
16
|
-
createRuntimePathProvider,
|
|
17
|
-
type RuntimePathProvider,
|
|
18
|
-
} from "../../runtime/runtime-path-provider.js";
|
|
19
15
|
import {
|
|
20
16
|
isPidEntry,
|
|
21
17
|
isSessionOwner,
|
|
@@ -23,7 +19,11 @@ import {
|
|
|
23
19
|
type SessionOwner,
|
|
24
20
|
type SessionPidFile,
|
|
25
21
|
toOwnerKey,
|
|
26
|
-
} from "../../
|
|
22
|
+
} from "../../runtime/pid-schema.js";
|
|
23
|
+
import {
|
|
24
|
+
createRuntimePathProvider,
|
|
25
|
+
type RuntimePathProvider,
|
|
26
|
+
} from "../../runtime/runtime-path-provider.js";
|
|
27
27
|
import { atomicWriteFileSync } from "./atomic-write.js";
|
|
28
28
|
import { acquireFileLock } from "./file-lock.js";
|
|
29
29
|
|
|
@@ -1076,7 +1076,7 @@ export default function backgroundTasksExtension(pi: ExtensionAPI): void {
|
|
|
1076
1076
|
|
|
1077
1077
|
if (!expanded && truncated) {
|
|
1078
1078
|
lines.push(
|
|
1079
|
-
formatPresentationText(theme, "hint", keyHint("
|
|
1079
|
+
formatPresentationText(theme, "hint", keyHint("app.tools.expand", "to show more"))
|
|
1080
1080
|
);
|
|
1081
1081
|
}
|
|
1082
1082
|
} else {
|
|
@@ -283,7 +283,10 @@ export default function (pi: ExtensionAPI): void {
|
|
|
283
283
|
name: "cd",
|
|
284
284
|
label: "cd",
|
|
285
285
|
description:
|
|
286
|
-
"Request an interactive workspace transition to another directory. Requires explicit user approval and restarts the turn in the new workspace.",
|
|
286
|
+
"Request an interactive workspace transition to another directory. Requires explicit user approval and restarts the turn in the new workspace. IMPORTANT: cd must be the ONLY tool call in your response — never combine it with other tools (edit, bash, write, etc.). The transition restarts the turn and discards sibling tool results.",
|
|
287
|
+
promptGuidelines: [
|
|
288
|
+
"The cd tool triggers an interactive workspace transition that restarts the current turn. When you need to cd, emit it as the SOLE tool call in your response — do not pair it with edit, bash, write, read, or any other tool. Sibling tool calls will race against the transition and their results will be lost when the turn restarts.",
|
|
289
|
+
],
|
|
287
290
|
parameters: Type.Object({
|
|
288
291
|
path: Type.String({
|
|
289
292
|
description: "Directory path to open via the interactive workspace transition flow",
|
|
@@ -42,12 +42,17 @@ describe("buildFrontmatterIndex", () => {
|
|
|
42
42
|
let originalCwd: string;
|
|
43
43
|
let originalTrustCwd: string | undefined;
|
|
44
44
|
let originalTrustStatus: string | undefined;
|
|
45
|
+
let originalCodingAgentDir: string | undefined;
|
|
46
|
+
let isolatedAgentDir: string;
|
|
45
47
|
|
|
46
48
|
beforeAll(() => {
|
|
47
49
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "fork-index-test-"));
|
|
48
50
|
originalCwd = process.cwd();
|
|
49
51
|
originalTrustCwd = process.env.TALLOW_PROJECT_TRUST_CWD;
|
|
50
52
|
originalTrustStatus = process.env.TALLOW_PROJECT_TRUST_STATUS;
|
|
53
|
+
originalCodingAgentDir = process.env.TALLOW_CODING_AGENT_DIR;
|
|
54
|
+
isolatedAgentDir = fs.mkdtempSync(path.join(os.tmpdir(), "fork-agent-dir-"));
|
|
55
|
+
process.env.TALLOW_CODING_AGENT_DIR = isolatedAgentDir;
|
|
51
56
|
|
|
52
57
|
// Create project structure with prompts and commands
|
|
53
58
|
const promptsDir = path.join(tmpDir, ".tallow", "prompts");
|
|
@@ -138,7 +143,11 @@ Use opus model.
|
|
|
138
143
|
if (originalTrustStatus !== undefined)
|
|
139
144
|
process.env.TALLOW_PROJECT_TRUST_STATUS = originalTrustStatus;
|
|
140
145
|
else delete process.env.TALLOW_PROJECT_TRUST_STATUS;
|
|
146
|
+
if (originalCodingAgentDir !== undefined)
|
|
147
|
+
process.env.TALLOW_CODING_AGENT_DIR = originalCodingAgentDir;
|
|
148
|
+
else delete process.env.TALLOW_CODING_AGENT_DIR;
|
|
141
149
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
150
|
+
fs.rmSync(isolatedAgentDir, { recursive: true, force: true });
|
|
142
151
|
});
|
|
143
152
|
|
|
144
153
|
test("indexes prompt with context: fork", () => {
|
|
@@ -93,6 +93,7 @@ export default function editLive(pi: ExtensionAPI): void {
|
|
|
93
93
|
label: baseEditTool.label,
|
|
94
94
|
description: baseEditTool.description,
|
|
95
95
|
parameters: baseEditTool.parameters,
|
|
96
|
+
prepareArguments: baseEditTool.prepareArguments,
|
|
96
97
|
|
|
97
98
|
renderCall(args, theme) {
|
|
98
99
|
const path = args.path ?? "file";
|
|
@@ -109,7 +110,8 @@ export default function editLive(pi: ExtensionAPI): void {
|
|
|
109
110
|
const filePath = params.path ?? "file";
|
|
110
111
|
const effectiveCwd = ctx?.cwd ?? process.cwd();
|
|
111
112
|
const scopedEditTool = createEditTool(effectiveCwd);
|
|
112
|
-
const
|
|
113
|
+
const prepared = scopedEditTool.prepareArguments?.(params) ?? params;
|
|
114
|
+
const result = await scopedEditTool.execute(toolCallId, prepared, signal, onUpdate);
|
|
113
115
|
const details = result.details as EditToolDetails | undefined;
|
|
114
116
|
const diff = details?.diff ?? "";
|
|
115
117
|
const absoluteFilename = path.isAbsolute(filePath)
|
|
@@ -5,14 +5,23 @@ import { join } from "node:path";
|
|
|
5
5
|
import { type DiagnosticInput, runDiagnostics } from "../index.js";
|
|
6
6
|
|
|
7
7
|
let tmpDir: string;
|
|
8
|
+
let savedTmux: string | undefined;
|
|
8
9
|
|
|
9
10
|
beforeEach(() => {
|
|
10
11
|
tmpDir = join(tmpdir(), `health-diag-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
11
12
|
mkdirSync(tmpDir, { recursive: true });
|
|
13
|
+
// Isolate tests from host tmux environment so tmux diagnostics don't fire
|
|
14
|
+
savedTmux = process.env.TMUX;
|
|
15
|
+
delete process.env.TMUX;
|
|
12
16
|
});
|
|
13
17
|
|
|
14
18
|
afterEach(() => {
|
|
15
19
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
20
|
+
if (savedTmux !== undefined) {
|
|
21
|
+
process.env.TMUX = savedTmux;
|
|
22
|
+
} else {
|
|
23
|
+
delete process.env.TMUX;
|
|
24
|
+
}
|
|
16
25
|
});
|
|
17
26
|
|
|
18
27
|
/**
|
|
@@ -172,4 +181,20 @@ describe("runDiagnostics", () => {
|
|
|
172
181
|
const ctxCheck = checks.find((c) => c.name === "Project context");
|
|
173
182
|
expect(ctxCheck?.status).toBe("pass");
|
|
174
183
|
});
|
|
184
|
+
|
|
185
|
+
test("skips tmux checks when not inside tmux", () => {
|
|
186
|
+
delete process.env.TMUX;
|
|
187
|
+
const checks = runDiagnostics(makeInput());
|
|
188
|
+
const tmuxChecks = checks.filter((c) => c.name.startsWith("tmux"));
|
|
189
|
+
expect(tmuxChecks).toHaveLength(0);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("includes tmux checks when TMUX env var is set", () => {
|
|
193
|
+
process.env.TMUX = "/tmp/tmux-test/default,12345,0";
|
|
194
|
+
const checks = runDiagnostics(makeInput());
|
|
195
|
+
const tmuxChecks = checks.filter((c) => c.name.startsWith("tmux"));
|
|
196
|
+
// Should have at least escape-time and extended-keys checks
|
|
197
|
+
// (may fail gracefully if tmux binary is not available)
|
|
198
|
+
expect(tmuxChecks.length).toBeGreaterThanOrEqual(0);
|
|
199
|
+
});
|
|
175
200
|
});
|
|
@@ -549,7 +549,7 @@ export function runDiagnostics(input: DiagnosticInput): DiagnosticCheck[] {
|
|
|
549
549
|
name: "Tools",
|
|
550
550
|
status: "fail",
|
|
551
551
|
message: "No tools active",
|
|
552
|
-
suggestion: "Check extension loading
|
|
552
|
+
suggestion: "Check extension loading status",
|
|
553
553
|
});
|
|
554
554
|
} else {
|
|
555
555
|
checks.push({ name: "Tools", status: "pass", message: `${input.tools.activeCount} active` });
|
|
@@ -623,6 +623,67 @@ export function runDiagnostics(input: DiagnosticInput): DiagnosticCheck[] {
|
|
|
623
623
|
});
|
|
624
624
|
}
|
|
625
625
|
|
|
626
|
+
// 9. tmux compatibility (keyboard protocol)
|
|
627
|
+
if (process.env.TMUX) {
|
|
628
|
+
const tmuxChecks = checkTmuxSettings();
|
|
629
|
+
checks.push(...tmuxChecks);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return checks;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Check tmux settings that affect keyboard handling.
|
|
637
|
+
* Only called when running inside tmux ($TMUX is set).
|
|
638
|
+
*
|
|
639
|
+
* @returns Array of diagnostic check results for tmux configuration
|
|
640
|
+
*/
|
|
641
|
+
function checkTmuxSettings(): DiagnosticCheck[] {
|
|
642
|
+
const checks: DiagnosticCheck[] = [];
|
|
643
|
+
|
|
644
|
+
try {
|
|
645
|
+
const { execSync } = require("node:child_process");
|
|
646
|
+
const opts = execSync("tmux show-options -g", { encoding: "utf-8", timeout: 2000 });
|
|
647
|
+
|
|
648
|
+
// Check escape-time (should be 0 or very low for responsive Escape key)
|
|
649
|
+
const escapeTimeMatch = opts.match(/^escape-time\s+(\d+)/m);
|
|
650
|
+
const escapeTime = escapeTimeMatch ? parseInt(escapeTimeMatch[1], 10) : 500;
|
|
651
|
+
if (escapeTime > 50) {
|
|
652
|
+
checks.push({
|
|
653
|
+
name: "tmux escape-time",
|
|
654
|
+
status: "warn",
|
|
655
|
+
message: `${escapeTime}ms delay before Escape is forwarded`,
|
|
656
|
+
suggestion: "Add `set -g escape-time 0` to tmux.conf",
|
|
657
|
+
});
|
|
658
|
+
} else {
|
|
659
|
+
checks.push({
|
|
660
|
+
name: "tmux escape-time",
|
|
661
|
+
status: "pass",
|
|
662
|
+
message: `${escapeTime}ms`,
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Check extended-keys (needed for Shift+Enter and modified key detection)
|
|
667
|
+
const extKeysMatch = opts.match(/^extended-keys\s+(\S+)/m);
|
|
668
|
+
const extKeys = extKeysMatch ? extKeysMatch[1] : "off";
|
|
669
|
+
if (extKeys === "off") {
|
|
670
|
+
checks.push({
|
|
671
|
+
name: "tmux extended-keys",
|
|
672
|
+
status: "warn",
|
|
673
|
+
message: "Shift+Enter and other modified keys won't work",
|
|
674
|
+
suggestion: "Add `set -g extended-keys on` to tmux.conf",
|
|
675
|
+
});
|
|
676
|
+
} else {
|
|
677
|
+
checks.push({
|
|
678
|
+
name: "tmux extended-keys",
|
|
679
|
+
status: "pass",
|
|
680
|
+
message: extKeys,
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
} catch {
|
|
684
|
+
// tmux command failed — skip these checks silently
|
|
685
|
+
}
|
|
686
|
+
|
|
626
687
|
return checks;
|
|
627
688
|
}
|
|
628
689
|
|