Skip to content

Commit 1fb0073

Browse files
NagyViktNagyViktclaude
authored
Auto-bootstrap Kitty host when gx cockpit runs from a non-Kitty TTY (#523)
gx cockpit on the kitty backend now defaults to --host when stdout is a TTY and KITTY_LISTEN_ON is unset, so plain `gx cockpit` opens a fresh Kitty window with tiled agent lanes. Disabled by --no-host, by being inside a Kitty session, when stdout is not a TTY (CI/scripted tests), or when GUARDEX_AUTO_HOST=0. Co-authored-by: NagyVikt <nagy.viktordp@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 075e39a commit 1fb0073

3 files changed

Lines changed: 115 additions & 1 deletion

File tree

docs/agents-cockpit.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,25 @@ inside Kitty" mode.
8282
set). It does not require `allow_remote_control` to be enabled in
8383
`kitty.conf`, because the spawned host is configured inline via `-o`.
8484

85+
### Auto-host default
86+
87+
When you run `gx cockpit` from an interactive terminal that is **not**
88+
already a Kitty session (e.g. from gnome-terminal, alacritty, or any
89+
shell where `KITTY_LISTEN_ON` is unset) and the kitty backend is
90+
selected, `gx cockpit` auto-bootstraps a Kitty host as if you had
91+
passed `--host`.
92+
93+
Auto-host is disabled when any of the following hold:
94+
- The cockpit is invoked with `--no-host`.
95+
- `KITTY_LISTEN_ON` is already exported (you are inside a Kitty
96+
session — remote control is reachable through the parent socket).
97+
- `stdout` is not a TTY (CI, piped output, scripted tests).
98+
- `GUARDEX_AUTO_HOST=0` (or `false`/`no`/`off`) is exported.
99+
100+
This makes the dmux-style "type one command, get a guarded multi-agent
101+
window" UX work out of the box while keeping the legacy "I am already
102+
inside Kitty with RC enabled" path untouched.
103+
85104
## Start agent lanes
86105

87106
Start Codex:

src/cockpit/index.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,11 +193,22 @@ function writeOpenedCockpitMessage({ backend, action, options, repoRoot, control
193193
stdout.write(`[${toolName}] Control pane: ${controlCommand}\n`);
194194
}
195195

196+
function shouldAutoHost(options = {}, context = {}) {
197+
if (options.host === true || options.host === false) return false;
198+
const env = context.env || process.env;
199+
const optOut = String(env.GUARDEX_AUTO_HOST || '').trim().toLowerCase();
200+
if (optOut === '0' || optOut === 'false' || optOut === 'no' || optOut === 'off') return false;
201+
if (env.KITTY_LISTEN_ON) return false;
202+
const stdout = context.stdout || process.stdout;
203+
return Boolean(stdout && stdout.isTTY);
204+
}
205+
196206
function openWithBackend(backend, options, repoRoot, controlCommand, deps = {}) {
197207
const stdout = deps.stdout || process.stdout;
198208
const toolName = deps.toolName || 'gitguardex';
199209
const env = deps.env || process.env;
200210
if (backend.name === 'kitty') {
211+
const autoHost = shouldAutoHost(options, { env, stdout });
201212
const result = openKittyCockpit({
202213
repoRoot,
203214
sessionName: options.sessionName,
@@ -213,7 +224,9 @@ function openWithBackend(backend, options, repoRoot, controlCommand, deps = {})
213224
kittyBin: deps.kittyBin || env.GUARDEX_KITTY_BIN,
214225
env,
215226
backend,
216-
bootstrap: options.host === true ? true : options.host === false ? false : undefined,
227+
bootstrap: options.host === true || (options.host === undefined && autoHost)
228+
? true
229+
: options.host === false ? false : undefined,
217230
bootstrapWhenHostless: false,
218231
socket: options.socket,
219232
hostRunner: deps.kittyHostRunner,

test/cockpit-kitty-bootstrap.test.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,88 @@ test('openKittyCockpit without bootstrap leaves args untouched', () => {
139139
}
140140
});
141141

142+
test('auto-host bootstraps when stdout is a TTY and KITTY_LISTEN_ON is unset', () => {
143+
const result = cockpit.openCockpit(['--target', '/repo/gitguardex'], {
144+
resolveRepoRoot: (target) => target,
145+
toolName: 'gx',
146+
stdout: { isTTY: true, write() {} },
147+
env: {},
148+
dryRun: true,
149+
readState: () => fakeState([fakeSession('alpha')]),
150+
readSettings: () => ({}),
151+
terminalBackends: { kitty: fakeBackendStub({ socket: '/tmp/auto-host.sock' }) },
152+
});
153+
154+
assert.equal(result.backend, 'kitty');
155+
assert.ok(result.plan.host, 'plan.host should be populated');
156+
assert.equal(result.plan.host.socket, '/tmp/auto-host.sock');
157+
for (const cmd of result.plan.commands) {
158+
assert.equal(cmd.args[1], '--to=/tmp/auto-host.sock');
159+
}
160+
});
161+
162+
test('auto-host stays off when stdout is not a TTY', () => {
163+
const result = cockpit.openCockpit(['--target', '/repo/gitguardex'], {
164+
resolveRepoRoot: (target) => target,
165+
toolName: 'gx',
166+
stdout: { isTTY: false, write() {} },
167+
env: {},
168+
dryRun: true,
169+
readState: () => fakeState([fakeSession('alpha')]),
170+
readSettings: () => ({}),
171+
terminalBackends: { kitty: fakeBackendStub() },
172+
});
173+
174+
assert.equal(result.backend, 'kitty');
175+
assert.equal(result.plan.host || null, null);
176+
});
177+
178+
test('auto-host stays off when KITTY_LISTEN_ON is set', () => {
179+
const result = cockpit.openCockpit(['--target', '/repo/gitguardex'], {
180+
resolveRepoRoot: (target) => target,
181+
toolName: 'gx',
182+
stdout: { isTTY: true, write() {} },
183+
env: { KITTY_LISTEN_ON: 'unix:/tmp/parent.sock' },
184+
dryRun: true,
185+
readState: () => fakeState([fakeSession('alpha')]),
186+
readSettings: () => ({}),
187+
terminalBackends: { kitty: fakeBackendStub() },
188+
});
189+
190+
assert.equal(result.backend, 'kitty');
191+
assert.equal(result.plan.host || null, null);
192+
});
193+
194+
test('--no-host overrides the auto-host default', () => {
195+
const result = cockpit.openCockpit(['--no-host', '--target', '/repo/gitguardex'], {
196+
resolveRepoRoot: (target) => target,
197+
toolName: 'gx',
198+
stdout: { isTTY: true, write() {} },
199+
env: {},
200+
dryRun: true,
201+
readState: () => fakeState([fakeSession('alpha')]),
202+
readSettings: () => ({}),
203+
terminalBackends: { kitty: fakeBackendStub() },
204+
});
205+
206+
assert.equal(result.plan.host || null, null);
207+
});
208+
209+
test('GUARDEX_AUTO_HOST=0 disables the auto-host default', () => {
210+
const result = cockpit.openCockpit(['--target', '/repo/gitguardex'], {
211+
resolveRepoRoot: (target) => target,
212+
toolName: 'gx',
213+
stdout: { isTTY: true, write() {} },
214+
env: { GUARDEX_AUTO_HOST: '0' },
215+
dryRun: true,
216+
readState: () => fakeState([fakeSession('alpha')]),
217+
readSettings: () => ({}),
218+
terminalBackends: { kitty: fakeBackendStub() },
219+
});
220+
221+
assert.equal(result.plan.host || null, null);
222+
});
223+
142224
test('parseCockpitArgs accepts --host and --socket', () => {
143225
const opts1 = cockpit.parseCockpitArgs(['--host']);
144226
assert.equal(opts1.host, true);

0 commit comments

Comments
 (0)