Skip to content

Commit eb5f08e

Browse files
NagyViktNagyVikt
andauthored
Open panel-launched agents in Kitty (#498)
The dmux-style gx panel already hands operators a launch surface, but single-agent panel launches still left the created lane unopened. Route successful panel launches through the existing Kitty session launcher after branch/worktree/session creation, while leaving direct non-panel single starts unchanged for automation. Constraint: Preserve branch creation, locks, and session metadata order before terminal launch Rejected: Open Kitty for every single-agent start | direct non-panel starts are used by automation and should not spawn desktop terminals Confidence: high Scope-risk: narrow Tested: node --test test/agents-start-kitty-panel.test.js test/agents-start.test.js test/agents-start-dry-run.test.js test/agents-selection-panel.test.js Tested: openspec validate agent-codex-kitty-gx-window-terminal-2026-04-30-13-36 --strict Tested: openspec validate --specs Co-authored-by: NagyVikt <nagy.viktordp@gmail.com>
1 parent 6086efc commit eb5f08e

5 files changed

Lines changed: 207 additions & 1 deletion

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Kitty gx window terminal follow-up
2+
3+
## Why
4+
5+
The dmux-style `gx agents start --panel` shell can launch a single selected agent, but single-panel launches still leave the operator in the original shell without opening the new agent lane in Kitty. The visual flow should keep the same GitGuardex panel style while using Kitty as the terminal surface for launched lanes.
6+
7+
## What Changes
8+
9+
- Open a generated Kitty session after a successful single-lane panel launch.
10+
- Preserve direct non-panel single starts so automation does not unexpectedly open a terminal.
11+
- Keep `--terminal none` behavior routed through the existing Kitty launcher skip path.
12+
13+
## Impact
14+
15+
The change is limited to panel-driven agent starts and focused tests. It does not change branch creation, locks, session metadata, multi-agent behavior, or finish cleanup.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
## ADDED Requirements
2+
3+
### Requirement: Panel launch uses Kitty terminal surface
4+
5+
`gx agents start --panel` SHALL keep the GitGuardex launcher shell behavior and open launched agent lanes in Kitty when terminal launch is enabled.
6+
7+
#### Scenario: Single panel launch opens Kitty
8+
9+
- **WHEN** an operator launches one selected agent from `gx agents start --panel`
10+
- **THEN** Guardex SHALL create the `agent/*` lane and session metadata first
11+
- **AND** SHALL write a Kitty session file for the created lane
12+
- **AND** SHALL launch Kitty from that session file.
13+
14+
#### Scenario: Non-panel single launch remains non-terminal
15+
16+
- **WHEN** an operator runs a direct single-agent `gx agents start "fix auth"` without `--panel`
17+
- **THEN** Guardex SHALL keep the existing branch/worktree/session behavior
18+
- **AND** SHALL NOT open Kitty automatically.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
## 1. Spec
2+
3+
- [x] Capture panel-launched Kitty terminal behavior.
4+
5+
## 2. Tests
6+
7+
- [x] Cover single-agent panel launch opening Kitty.
8+
- [x] Cover direct single-agent start staying non-terminal.
9+
10+
## 3. Implementation
11+
12+
- [x] Route successful single-lane panel starts through the existing Kitty session launcher.
13+
- [x] Preserve existing multi-agent Kitty behavior and non-panel single-agent behavior.
14+
15+
## 4. Verification
16+
17+
- [x] Run focused Node tests for start/panel terminal behavior.
18+
- Evidence: `node --test test/agents-start-kitty-panel.test.js test/agents-start.test.js test/agents-start-dry-run.test.js test/agents-selection-panel.test.js` passed 21/21.
19+
- [x] Run OpenSpec validation.
20+
- Evidence: `openspec validate agent-codex-kitty-gx-window-terminal-2026-04-30-13-36 --strict` passed.
21+
- Evidence: `openspec validate --specs` passed with no spec items found.
22+
23+
## 5. Cleanup
24+
25+
- [ ] Commit changes.
26+
- [ ] Finish via PR, wait for merge, cleanup, and record `MERGED` evidence.

src/agents/start.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -511,7 +511,23 @@ function startSingleAgentLane(repoRoot, options, deps = {}) {
511511
function startAgentLane(repoRoot, options, deps = {}) {
512512
const launchOptions = buildLaunchOptions(options);
513513
if (launchOptions.length === 1) {
514-
return startSingleAgentLane(repoRoot, launchOptions[0], deps);
514+
const result = startSingleAgentLane(repoRoot, launchOptions[0], deps);
515+
if (result.status !== 0 || !result.session || !options.panel) {
516+
return result;
517+
}
518+
519+
const terminalResult = launchAgentTerminal(repoRoot, [result.session], {
520+
terminal: options.terminal,
521+
runner: deps.terminalRunner,
522+
kittyBin: deps.kittyBin,
523+
});
524+
525+
return {
526+
...result,
527+
stdout: `${String(result.stdout || '')}${String(terminalResult.stdout || '')}`,
528+
stderr: `${String(result.stderr || '')}${String(terminalResult.stderr || '')}`,
529+
terminal: terminalResult,
530+
};
515531
}
516532

517533
let stdout = renderAgentSelectionPanel({
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
const test = require('node:test');
2+
const assert = require('node:assert/strict');
3+
const Module = require('node:module');
4+
const fs = require('node:fs');
5+
const os = require('node:os');
6+
const path = require('node:path');
7+
8+
function loadStartWithMocks({
9+
runPackageAsset,
10+
createAgentSession,
11+
updateAgentSession,
12+
currentBranchName,
13+
listAgentSessions = () => [],
14+
}) {
15+
const startPath = require.resolve('../src/agents/start');
16+
const runtimePath = require.resolve('../src/core/runtime');
17+
const sessionsPath = require.resolve('../src/agents/sessions');
18+
const terminalPath = require.resolve('../src/agents/terminal');
19+
const gitPath = require.resolve('../src/git');
20+
const originalLoad = Module._load;
21+
22+
delete require.cache[startPath];
23+
delete require.cache[terminalPath];
24+
Module._load = function mockLoad(request, parent, isMain) {
25+
const resolved = Module._resolveFilename(request, parent, isMain);
26+
if (resolved === runtimePath) {
27+
return { runPackageAsset };
28+
}
29+
if (resolved === sessionsPath) {
30+
return { createAgentSession, updateAgentSession, listAgentSessions };
31+
}
32+
if (resolved === gitPath) {
33+
return { currentBranchName };
34+
}
35+
return originalLoad.apply(this, arguments);
36+
};
37+
38+
try {
39+
return require(startPath);
40+
} finally {
41+
Module._load = originalLoad;
42+
delete require.cache[startPath];
43+
delete require.cache[terminalPath];
44+
}
45+
}
46+
47+
function branchStartOutput(branch, worktreePath) {
48+
return [
49+
`[agent-branch-start] Created branch: ${branch}`,
50+
`[agent-branch-start] Worktree: ${worktreePath}`,
51+
'',
52+
].join('\n');
53+
}
54+
55+
test('panel-launched single agent opens a Kitty terminal session', () => {
56+
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-kitty-panel-'));
57+
const worktreePath = path.join(repoRoot, '.omx/agent-worktrees/repo__codex__fix-auth');
58+
const branch = 'agent/codex/fix-auth';
59+
const runCalls = [];
60+
const terminalCalls = [];
61+
const start = loadStartWithMocks({
62+
runPackageAsset(assetKey, args, options) {
63+
runCalls.push({ assetKey, args, options });
64+
return { status: 0, stdout: branchStartOutput(branch, worktreePath), stderr: '' };
65+
},
66+
createAgentSession(repoRootArg, payload) {
67+
return payload;
68+
},
69+
updateAgentSession() {
70+
throw new Error('unexpected update');
71+
},
72+
currentBranchName: () => 'main',
73+
});
74+
75+
const result = start.startAgentLane(repoRoot, {
76+
task: 'fix auth',
77+
agent: 'codex',
78+
base: 'main',
79+
claims: [],
80+
panel: true,
81+
}, {
82+
terminalRunner(cmd, args, options) {
83+
terminalCalls.push({ cmd, args, options });
84+
return { status: 0, stdout: args[0] === '--version' ? 'kitty 0.36\n' : '', stderr: '' };
85+
},
86+
});
87+
88+
assert.equal(result.status, 0);
89+
assert.equal(runCalls.length, 1);
90+
assert.match(result.stdout, /Agent session id: agent__codex__fix-auth/);
91+
assert.match(result.stdout, /Kitty agent terminal:/);
92+
assert.deepEqual(terminalCalls.map((call) => call.args[0]), ['--version', '--detach']);
93+
const sessionFile = terminalCalls[1].args[2];
94+
assert.match(sessionFile, /\.guardex\/agents\/terminals\/agent__codex__fix-auth-1\.kitty-session$/);
95+
const sessionBody = fs.readFileSync(sessionFile, 'utf8');
96+
assert.match(sessionBody, /new_tab '1: codex fix-auth'/);
97+
assert.match(sessionBody, /cd '.*repo__codex__fix-auth'/);
98+
assert.match(sessionBody, /launch --title '1: codex fix-auth' sh -lc 'cd/);
99+
});
100+
101+
test('non-panel single agent start keeps terminal launch opt-in unchanged', () => {
102+
const terminalCalls = [];
103+
const start = loadStartWithMocks({
104+
runPackageAsset() {
105+
return { status: 0, stdout: branchStartOutput('agent/codex/fix-auth', '/repo/.omx/agent-worktrees/repo__codex__fix-auth'), stderr: '' };
106+
},
107+
createAgentSession(repoRootArg, payload) {
108+
return payload;
109+
},
110+
updateAgentSession() {
111+
throw new Error('unexpected update');
112+
},
113+
currentBranchName: () => 'main',
114+
});
115+
116+
const result = start.startAgentLane('/repo', {
117+
task: 'fix auth',
118+
agent: 'codex',
119+
base: 'main',
120+
claims: [],
121+
}, {
122+
terminalRunner(cmd, args, options) {
123+
terminalCalls.push({ cmd, args, options });
124+
return { status: 0, stdout: '', stderr: '' };
125+
},
126+
});
127+
128+
assert.equal(result.status, 0);
129+
assert.equal(terminalCalls.length, 0);
130+
assert.doesNotMatch(result.stdout, /Kitty agent terminal:/);
131+
});

0 commit comments

Comments
 (0)