Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/agents-cockpit.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,25 @@ inside Kitty" mode.
set). It does not require `allow_remote_control` to be enabled in
`kitty.conf`, because the spawned host is configured inline via `-o`.

### Auto-host default

When you run `gx cockpit` from an interactive terminal that is **not**
already a Kitty session (e.g. from gnome-terminal, alacritty, or any
shell where `KITTY_LISTEN_ON` is unset) and the kitty backend is
selected, `gx cockpit` auto-bootstraps a Kitty host as if you had
passed `--host`.

Auto-host is disabled when any of the following hold:
- The cockpit is invoked with `--no-host`.
- `KITTY_LISTEN_ON` is already exported (you are inside a Kitty
session — remote control is reachable through the parent socket).
- `stdout` is not a TTY (CI, piped output, scripted tests).
- `GUARDEX_AUTO_HOST=0` (or `false`/`no`/`off`) is exported.

This makes the dmux-style "type one command, get a guarded multi-agent
window" UX work out of the box while keeping the legacy "I am already
inside Kitty with RC enabled" path untouched.

## Start agent lanes

Start Codex:
Expand Down
15 changes: 14 additions & 1 deletion src/cockpit/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -193,11 +193,22 @@ function writeOpenedCockpitMessage({ backend, action, options, repoRoot, control
stdout.write(`[${toolName}] Control pane: ${controlCommand}\n`);
}

function shouldAutoHost(options = {}, context = {}) {
if (options.host === true || options.host === false) return false;
const env = context.env || process.env;
const optOut = String(env.GUARDEX_AUTO_HOST || '').trim().toLowerCase();
if (optOut === '0' || optOut === 'false' || optOut === 'no' || optOut === 'off') return false;
if (env.KITTY_LISTEN_ON) return false;
const stdout = context.stdout || process.stdout;
return Boolean(stdout && stdout.isTTY);
}

function openWithBackend(backend, options, repoRoot, controlCommand, deps = {}) {
const stdout = deps.stdout || process.stdout;
const toolName = deps.toolName || 'gitguardex';
const env = deps.env || process.env;
if (backend.name === 'kitty') {
const autoHost = shouldAutoHost(options, { env, stdout });
const result = openKittyCockpit({
repoRoot,
sessionName: options.sessionName,
Expand All @@ -213,7 +224,9 @@ function openWithBackend(backend, options, repoRoot, controlCommand, deps = {})
kittyBin: deps.kittyBin || env.GUARDEX_KITTY_BIN,
env,
backend,
bootstrap: options.host === true ? true : options.host === false ? false : undefined,
bootstrap: options.host === true || (options.host === undefined && autoHost)
? true
: options.host === false ? false : undefined,
bootstrapWhenHostless: false,
socket: options.socket,
hostRunner: deps.kittyHostRunner,
Expand Down
82 changes: 82 additions & 0 deletions test/cockpit-kitty-bootstrap.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,88 @@ test('openKittyCockpit without bootstrap leaves args untouched', () => {
}
});

test('auto-host bootstraps when stdout is a TTY and KITTY_LISTEN_ON is unset', () => {
const result = cockpit.openCockpit(['--target', '/repo/gitguardex'], {
resolveRepoRoot: (target) => target,
toolName: 'gx',
stdout: { isTTY: true, write() {} },
env: {},
dryRun: true,
readState: () => fakeState([fakeSession('alpha')]),
readSettings: () => ({}),
terminalBackends: { kitty: fakeBackendStub({ socket: '/tmp/auto-host.sock' }) },
});

assert.equal(result.backend, 'kitty');
assert.ok(result.plan.host, 'plan.host should be populated');
assert.equal(result.plan.host.socket, '/tmp/auto-host.sock');
for (const cmd of result.plan.commands) {
assert.equal(cmd.args[1], '--to=/tmp/auto-host.sock');
}
});

test('auto-host stays off when stdout is not a TTY', () => {
const result = cockpit.openCockpit(['--target', '/repo/gitguardex'], {
resolveRepoRoot: (target) => target,
toolName: 'gx',
stdout: { isTTY: false, write() {} },
env: {},
dryRun: true,
readState: () => fakeState([fakeSession('alpha')]),
readSettings: () => ({}),
terminalBackends: { kitty: fakeBackendStub() },
});

assert.equal(result.backend, 'kitty');
assert.equal(result.plan.host || null, null);
});

test('auto-host stays off when KITTY_LISTEN_ON is set', () => {
const result = cockpit.openCockpit(['--target', '/repo/gitguardex'], {
resolveRepoRoot: (target) => target,
toolName: 'gx',
stdout: { isTTY: true, write() {} },
env: { KITTY_LISTEN_ON: 'unix:/tmp/parent.sock' },
dryRun: true,
readState: () => fakeState([fakeSession('alpha')]),
readSettings: () => ({}),
terminalBackends: { kitty: fakeBackendStub() },
});

assert.equal(result.backend, 'kitty');
assert.equal(result.plan.host || null, null);
});

test('--no-host overrides the auto-host default', () => {
const result = cockpit.openCockpit(['--no-host', '--target', '/repo/gitguardex'], {
resolveRepoRoot: (target) => target,
toolName: 'gx',
stdout: { isTTY: true, write() {} },
env: {},
dryRun: true,
readState: () => fakeState([fakeSession('alpha')]),
readSettings: () => ({}),
terminalBackends: { kitty: fakeBackendStub() },
});

assert.equal(result.plan.host || null, null);
});

test('GUARDEX_AUTO_HOST=0 disables the auto-host default', () => {
const result = cockpit.openCockpit(['--target', '/repo/gitguardex'], {
resolveRepoRoot: (target) => target,
toolName: 'gx',
stdout: { isTTY: true, write() {} },
env: { GUARDEX_AUTO_HOST: '0' },
dryRun: true,
readState: () => fakeState([fakeSession('alpha')]),
readSettings: () => ({}),
terminalBackends: { kitty: fakeBackendStub() },
});

assert.equal(result.plan.host || null, null);
});

test('parseCockpitArgs accepts --host and --socket', () => {
const opts1 = cockpit.parseCockpitArgs(['--host']);
assert.equal(opts1.host, true);
Expand Down
Loading