Skip to content

Handle delayed Claude tmux prompts#45

Merged
axeldelafosse merged 1 commit intomainfrom
fix/claude-tmux-delayed-prompt
Mar 27, 2026
Merged

Handle delayed Claude tmux prompts#45
axeldelafosse merged 1 commit intomainfrom
fix/claude-tmux-delayed-prompt

Conversation

@axeldelafosse
Copy link
Copy Markdown
Owner

Summary

  • harden Claude tmux startup prompt polling so delayed dev-channel/trust prompts are still caught
  • keep the existing prompt-handling flow but reset settle state after each handled prompt
  • add coverage for delayed dev-channel prompts and the bounded no-prompt wait path

Verification

  • bun test tests/loop/tmux.test.ts
  • bun run check

@axeldelafosse axeldelafosse marked this pull request as ready for review March 27, 2026 07:06
@axeldelafosse axeldelafosse merged commit fe304be into main Mar 27, 2026
2 checks passed
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request enhances the tmux pane unblocking mechanism by implementing snapshot-based change detection and increasing the polling limit to better handle delayed prompts. It also includes a new test case for delayed dev-channel prompts. The review feedback recommends refactoring the test file to use a helper function for shared setup, which would reduce boilerplate and improve maintainability across the runInTmux tests.

Comment on lines +1023 to +1101
test("runInTmux catches a delayed Claude dev-channel prompt", async () => {
const keyCalls: Array<{ keys: string[]; pane: string }> = [];
let sessionStarted = false;
let pollCount = 0;
const devChannelsPrompt = [
"WARNING: Loading development channels",
"",
"--dangerously-load-development-channels is for local channel development only.",
"",
"1. I am using this for local development",
].join("\n");
const manifest = createRunManifest({
cwd: "/repo",
mode: "paired",
pid: 1234,
repoId: "repo-123",
runId: "1",
status: "running",
});
const storage = {
manifestPath: "/repo/.loop/runs/1/manifest.json",
repoId: "repo-123",
runDir: "/repo/.loop/runs/1",
runId: "1",
storageRoot: "/repo/.loop/runs",
transcriptPath: "/repo/.loop/runs/1/transcript.jsonl",
};

await runInTmux(
["--tmux", "--proof", "verify with tests"],
{
capturePane: () => {
pollCount += 1;
return pollCount === 5 ? devChannelsPrompt : "";
},
cwd: "/repo",
env: {},
findBinary: () => true,
getCodexAppServerUrl: () => "ws://127.0.0.1:4500",
getLastCodexThreadId: () => "codex-thread-1",
isInteractive: () => false,
launchArgv: ["bun", "/repo/src/cli.ts"],
log: (): void => undefined,
makeClaudeSessionId: () => "claude-session-1",
preparePairedRun: (nextOpts) => {
nextOpts.codexMcpConfigArgs = [
"-c",
'mcp_servers.loop-bridge.command="loop"',
];
return { manifest, storage };
},
sendKeys: (pane: string, keys: string[]) => {
keyCalls.push({ keys, pane });
},
sendText: (): void => undefined,
sleep: () => Promise.resolve(),
startCodexProxy: () => Promise.resolve("ws://127.0.0.1:4600/"),
startPersistentAgentSession: () => Promise.resolve(undefined),
spawn: (args: string[]) => {
if (args[0] === "tmux" && args[1] === "has-session") {
return sessionStarted
? { exitCode: 0, stderr: "" }
: { exitCode: 1, stderr: "" };
}
if (args[0] === "tmux" && args[1] === "new-session") {
sessionStarted = true;
}
return { exitCode: 0, stderr: "" };
},
updateRunManifest: (_path, update) => update(manifest),
},
{ opts: makePairedOptions(), task: "Ship feature" }
);

expect(keyCalls).toContainEqual({
keys: ["Enter"],
pane: "repo-loop-1:0.0",
});
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

There's a lot of repeated setup code across the runInTmux tests. To improve maintainability and reduce boilerplate, consider creating a test helper function. This function could encapsulate the common setup for manifest, storage, and the default mocked dependencies for runInTmux.

Each test could then call this helper and override only the specific mocks it needs, like capturePane or sendKeys.

For example, you could have a helper like this:

const setupPairedRunTest = (
  overrides: Partial<Parameters<typeof runInTmux>[1]> = {}
) => {
  const keyCalls: Array<{ keys: string[]; pane: string }> = [];
  let sessionStarted = false;
  const manifest = createRunManifest({ ... });
  const storage = { ... };

  const defaultDeps = {
    capturePane: () => "",
    cwd: "/repo",
    env: {},
    findBinary: () => true,
    sendKeys: (pane: string, keys: string[]) => {
      keyCalls.push({ keys, pane });
    },
    spawn: (args: string[]) => {
    },
    ...overrides,
  };

  const run = (
    args: string[] = ["--tmux", "--proof", "verify with tests"],
    launch?: PairedTmuxLaunch
  ) => runInTmux(args, defaultDeps, launch ?? { opts: makePairedOptions(), task: "Ship feature" });

  return { run, keyCalls, manifest, storage };
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant