fix(panes): /tui works in panes with workspace instructions
Claude refused `/tui` ("Cannot switch renderers in this session — it has
restrictions a restart can't carry over") in every pane started with
--append-system-prompt-file, i.e. any workspace with instructions (member
panes usually have the owner's). The instructions are now written as the only
CLAUDE.md of a folder of their own (<data dir>/workspace-prompts/<id>/, a
member's prompt-<id>/ in the handoff dir via write-as), passed as one more
--add-dir with CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1. Verified with
Claude Code 2.1.301: the file is listed in /memory and /tui switches. Codex
is unchanged (its pointer line names the same file).
This commit is contained in:
@@ -181,12 +181,16 @@ describe("lane-sources instructions and launch", () => {
|
||||
assert.equal(lanesLib.getLane(lane.id).instructions, "- always answer in Vietnamese");
|
||||
});
|
||||
|
||||
it("starts a claude pane with --add-dir first and the instructions from a file", () => {
|
||||
it("starts a claude pane with --add-dir first and the instructions as a CLAUDE.md there", () => {
|
||||
let argv = null;
|
||||
let newSession = null;
|
||||
tmux.__setExecImpl((args) => {
|
||||
if (args[0] === "has-session") throw Object.assign(new Error("gone"), { status: 1 });
|
||||
// The CLI rides behind `sh -c <cd; wait for a client> sh <cwd>` (pty-run.js).
|
||||
if (args[0] === "new-session") argv = args.slice(args.indexOf("--") + 6);
|
||||
if (args[0] === "new-session") {
|
||||
newSession = args;
|
||||
argv = args.slice(args.indexOf("--") + 6);
|
||||
}
|
||||
return "";
|
||||
});
|
||||
pty.spawnRun({ laneId: lane.id, cwd: ws, initialPrompt: "-c fix it" });
|
||||
@@ -194,11 +198,15 @@ describe("lane-sources instructions and launch", () => {
|
||||
assert.equal(argv[0], "claude");
|
||||
assert.equal(argv[1], "--add-dir");
|
||||
assert.deepEqual(argv.slice(2, 4), [extra, src.contextDir(lane.id)]);
|
||||
assert.equal(argv[4], "--permission-mode");
|
||||
const flag = argv.find((a) => a.startsWith("--append-system-prompt-file="));
|
||||
const file = flag.slice("--append-system-prompt-file=".length);
|
||||
assert.ok(file.startsWith(process.env.DASHBOARD_DATA_DIR));
|
||||
const text = fs.readFileSync(file, "utf8");
|
||||
// The instructions' own folder, read as memory: a custom system prompt
|
||||
// (`--append-system-prompt-file`) made Claude refuse `/tui` in the pane.
|
||||
const dir = argv[4];
|
||||
assert.ok(dir.startsWith(process.env.DASHBOARD_DATA_DIR));
|
||||
assert.equal(argv[5], "--permission-mode");
|
||||
assert.ok(!argv.some((a) => a.startsWith("--append-system-prompt")));
|
||||
assert.ok(newSession.includes("CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1"));
|
||||
assert.deepEqual(fs.readdirSync(dir), ["CLAUDE.md"]);
|
||||
const text = fs.readFileSync(path.join(dir, "CLAUDE.md"), "utf8");
|
||||
assert.match(text, /^- always answer in Vietnamese/);
|
||||
assert.match(text, /pass wd\.md/);
|
||||
// A prompt that looks like flags stays one positional, never `claude -c`.
|
||||
|
||||
@@ -114,7 +114,7 @@ describe("a member pane's files live in the member's handoff dir", () => {
|
||||
assert.deepEqual(x.addDirs, [ctx]);
|
||||
assert.equal(fs.readFileSync(path.join(ctx, "spec.md"), "utf8"), "# spec\n");
|
||||
assert.equal(mode(path.join(ctx, "spec.md")), 0o644);
|
||||
assert.equal(x.systemPromptFile, path.join(HANDOFF, `prompt-${laneId}.md`));
|
||||
assert.equal(x.systemPromptFile, path.join(HANDOFF, `prompt-${laneId}`, "CLAUDE.md"));
|
||||
assert.equal(mode(x.systemPromptFile), 0o644);
|
||||
const text = fs.readFileSync(x.systemPromptFile, "utf8");
|
||||
assert.match(text, /Be brief\./);
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* workspace's own folder and not be or contain the OS home; an uploaded file
|
||||
* is stored by AgentHub under `<data dir>/workspace-context/<laneId>/` with a
|
||||
* name it chooses, so a request never names a path on disk. Only Claude
|
||||
* (`--add-dir`, `--append-system-prompt-file=`) and Codex (`--add-dir`,
|
||||
* (`--add-dir`, the instructions as a CLAUDE.md in a folder of their own) and Codex (`--add-dir`,
|
||||
* `-c developer_instructions=`) can take them; every other tool is reported
|
||||
* unsupported and its panes get nothing. The workspace OWNER's own agent
|
||||
* instructions (`user_prefs.instructions`, Settings → My agent instructions)
|
||||
@@ -184,7 +184,9 @@ function removeSource(laneId, sourceId) {
|
||||
function removeLaneContext(laneId) {
|
||||
const dir = contextDir(laneId);
|
||||
if (ownsFile(dir)) fs.rmSync(dir, { recursive: true, force: true });
|
||||
fs.rmSync(path.join(getDataDir(), "workspace-prompts", `${Number(laneId)}.md`), { force: true });
|
||||
const prompts = path.join(getDataDir(), "workspace-prompts");
|
||||
fs.rmSync(path.join(prompts, String(Number(laneId))), { recursive: true, force: true });
|
||||
fs.rmSync(path.join(prompts, `${Number(laneId)}.md`), { force: true }); // before v2.8.9
|
||||
}
|
||||
|
||||
/** Instructions text as stored: a string of at most MAX_INSTRUCTIONS, blank ⇒ null. */
|
||||
@@ -286,18 +288,23 @@ function launchExtras(laneId, tool) {
|
||||
// A data dir that cannot be written costs the pane its instructions (all of
|
||||
// Claude's, Codex's past the inline budget), never its start — same as a
|
||||
// vanished source.
|
||||
// The file is a `CLAUDE.md` alone in a folder of its own: Claude reads it as
|
||||
// memory from that folder (`--add-dir` + CLAUDE_CODE_ADDITIONAL_DIRECTORIES_
|
||||
// CLAUDE_MD, pty-run.js), not as `--append-system-prompt-file` — a custom
|
||||
// system prompt is a launch flag Claude cannot carry over a restart, so it
|
||||
// refused `/tui` (switching renderer restarts the session) in every pane
|
||||
// that had instructions. Codex names the same file in its pointer line.
|
||||
try {
|
||||
const sub = `prompt-${Number(laneId)}`;
|
||||
if (member) {
|
||||
const file = require("./run-as").handoffWrite(
|
||||
member,
|
||||
`prompt-${Number(laneId)}.md`,
|
||||
systemPrompt
|
||||
);
|
||||
const runAs = require("./run-as");
|
||||
runAs.mkdirAs(member, path.join(runAs.handoffDir(member), sub));
|
||||
const file = runAs.handoffWrite(member, path.join(sub, "CLAUDE.md"), systemPrompt);
|
||||
return { addDirs, systemPrompt, systemPromptFile: file };
|
||||
}
|
||||
const dir = path.join(getDataDir(), "workspace-prompts");
|
||||
const dir = path.join(getDataDir(), "workspace-prompts", String(Number(laneId)));
|
||||
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
||||
const systemPromptFile = path.join(dir, `${Number(laneId)}.md`);
|
||||
const systemPromptFile = path.join(dir, "CLAUDE.md");
|
||||
fs.writeFileSync(systemPromptFile, systemPrompt, { mode: 0o600 });
|
||||
return { addDirs, systemPrompt, systemPromptFile };
|
||||
} catch (err) {
|
||||
|
||||
@@ -202,7 +202,10 @@ function buildClaudeArgv({
|
||||
// `--add-dir` is VARIADIC (`<directories...>`): it swallows every following
|
||||
// argument up to the next option, so it goes first, where `--permission-mode`
|
||||
// always ends it — after the prompt it would eat the prompt as a directory.
|
||||
if (addDirs.length) argv.push("--add-dir", ...addDirs);
|
||||
// The instructions' own folder rides along: its CLAUDE.md is read as memory
|
||||
// (see lane-sources.js launchExtras), so `/tui` can still restart the pane.
|
||||
const dirs = systemPromptFile ? [...addDirs, path.dirname(systemPromptFile)] : addDirs;
|
||||
if (dirs.length) argv.push("--add-dir", ...dirs);
|
||||
argv.push("--permission-mode", permissionMode || "auto");
|
||||
// Inside a pane Claude Code sees $TMUX and opens agent-team teammates with
|
||||
// `split-window -P -F '#{pane_id}'`. psmux ignores -F, so the id it reads
|
||||
@@ -220,9 +223,6 @@ function buildClaudeArgv({
|
||||
// `=` form: `-w [name]` takes an OPTIONAL value and would otherwise swallow
|
||||
// the prompt as the worktree's name.
|
||||
if (worktreeName) argv.push(`--worktree=${worktreeName}`);
|
||||
// From a file: the instructions can be long, and the whole argv has to fit
|
||||
// in one tmux message. `=` form, so the path is never read as an option.
|
||||
if (systemPromptFile) argv.push(`--append-system-prompt-file=${systemPromptFile}`);
|
||||
// Puts "bypass permissions" INTO the pane's shift+tab cycle without turning
|
||||
// it on: `claude --help` — "Enable bypassing all permission checks as an
|
||||
// option, without it being enabled by default." Without it the cycle is only
|
||||
@@ -626,6 +626,11 @@ function spawnRun(args) {
|
||||
// journals and the proxy layer `-p <name>` loads live there — and the
|
||||
// tmux server's own env may predate, or never have had, CODEX_HOME.
|
||||
...(tool === "codex" ? { CODEX_HOME: getCodexHome() } : {}),
|
||||
// The workspace's instructions are a CLAUDE.md in an `--add-dir` folder,
|
||||
// which Claude reads only with this set (lane-sources.js launchExtras).
|
||||
...(entry.launcher === "claude" && systemPromptFile
|
||||
? { CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD: "1" }
|
||||
: {}),
|
||||
// No CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN: an inline claude duplicated its
|
||||
// output whenever the pane narrowed (tmux re-wraps the full-width rules and
|
||||
// pushes lines into history its redraw cannot clear). The user's own
|
||||
|
||||
Reference in New Issue
Block a user