From 2e9640aed27c85b0724fd5264f530757ee0ed2da Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Thu, 30 Apr 2026 13:44:10 +0200 Subject: [PATCH] Open panel-launched agents in Kitty 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 --- .../proposal.md | 15 ++ .../specs/agents-multi-launcher/spec.md | 18 +++ .../tasks.md | 26 ++++ src/agents/start.js | 18 ++- test/agents-start-kitty-panel.test.js | 131 ++++++++++++++++++ 5 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 openspec/changes/agent-codex-kitty-gx-window-terminal-2026-04-30-13-36/proposal.md create mode 100644 openspec/changes/agent-codex-kitty-gx-window-terminal-2026-04-30-13-36/specs/agents-multi-launcher/spec.md create mode 100644 openspec/changes/agent-codex-kitty-gx-window-terminal-2026-04-30-13-36/tasks.md create mode 100644 test/agents-start-kitty-panel.test.js diff --git a/openspec/changes/agent-codex-kitty-gx-window-terminal-2026-04-30-13-36/proposal.md b/openspec/changes/agent-codex-kitty-gx-window-terminal-2026-04-30-13-36/proposal.md new file mode 100644 index 0000000..074a002 --- /dev/null +++ b/openspec/changes/agent-codex-kitty-gx-window-terminal-2026-04-30-13-36/proposal.md @@ -0,0 +1,15 @@ +# Kitty gx window terminal follow-up + +## Why + +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. + +## What Changes + +- Open a generated Kitty session after a successful single-lane panel launch. +- Preserve direct non-panel single starts so automation does not unexpectedly open a terminal. +- Keep `--terminal none` behavior routed through the existing Kitty launcher skip path. + +## Impact + +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. diff --git a/openspec/changes/agent-codex-kitty-gx-window-terminal-2026-04-30-13-36/specs/agents-multi-launcher/spec.md b/openspec/changes/agent-codex-kitty-gx-window-terminal-2026-04-30-13-36/specs/agents-multi-launcher/spec.md new file mode 100644 index 0000000..91cb08a --- /dev/null +++ b/openspec/changes/agent-codex-kitty-gx-window-terminal-2026-04-30-13-36/specs/agents-multi-launcher/spec.md @@ -0,0 +1,18 @@ +## ADDED Requirements + +### Requirement: Panel launch uses Kitty terminal surface + +`gx agents start --panel` SHALL keep the GitGuardex launcher shell behavior and open launched agent lanes in Kitty when terminal launch is enabled. + +#### Scenario: Single panel launch opens Kitty + +- **WHEN** an operator launches one selected agent from `gx agents start --panel` +- **THEN** Guardex SHALL create the `agent/*` lane and session metadata first +- **AND** SHALL write a Kitty session file for the created lane +- **AND** SHALL launch Kitty from that session file. + +#### Scenario: Non-panel single launch remains non-terminal + +- **WHEN** an operator runs a direct single-agent `gx agents start "fix auth"` without `--panel` +- **THEN** Guardex SHALL keep the existing branch/worktree/session behavior +- **AND** SHALL NOT open Kitty automatically. diff --git a/openspec/changes/agent-codex-kitty-gx-window-terminal-2026-04-30-13-36/tasks.md b/openspec/changes/agent-codex-kitty-gx-window-terminal-2026-04-30-13-36/tasks.md new file mode 100644 index 0000000..c0da06a --- /dev/null +++ b/openspec/changes/agent-codex-kitty-gx-window-terminal-2026-04-30-13-36/tasks.md @@ -0,0 +1,26 @@ +## 1. Spec + +- [x] Capture panel-launched Kitty terminal behavior. + +## 2. Tests + +- [x] Cover single-agent panel launch opening Kitty. +- [x] Cover direct single-agent start staying non-terminal. + +## 3. Implementation + +- [x] Route successful single-lane panel starts through the existing Kitty session launcher. +- [x] Preserve existing multi-agent Kitty behavior and non-panel single-agent behavior. + +## 4. Verification + +- [x] Run focused Node tests for start/panel terminal behavior. + - 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. +- [x] Run OpenSpec validation. + - Evidence: `openspec validate agent-codex-kitty-gx-window-terminal-2026-04-30-13-36 --strict` passed. + - Evidence: `openspec validate --specs` passed with no spec items found. + +## 5. Cleanup + +- [ ] Commit changes. +- [ ] Finish via PR, wait for merge, cleanup, and record `MERGED` evidence. diff --git a/src/agents/start.js b/src/agents/start.js index c5c4e99..6fe0bef 100644 --- a/src/agents/start.js +++ b/src/agents/start.js @@ -511,7 +511,23 @@ function startSingleAgentLane(repoRoot, options, deps = {}) { function startAgentLane(repoRoot, options, deps = {}) { const launchOptions = buildLaunchOptions(options); if (launchOptions.length === 1) { - return startSingleAgentLane(repoRoot, launchOptions[0], deps); + const result = startSingleAgentLane(repoRoot, launchOptions[0], deps); + if (result.status !== 0 || !result.session || !options.panel) { + return result; + } + + const terminalResult = launchAgentTerminal(repoRoot, [result.session], { + terminal: options.terminal, + runner: deps.terminalRunner, + kittyBin: deps.kittyBin, + }); + + return { + ...result, + stdout: `${String(result.stdout || '')}${String(terminalResult.stdout || '')}`, + stderr: `${String(result.stderr || '')}${String(terminalResult.stderr || '')}`, + terminal: terminalResult, + }; } let stdout = renderAgentSelectionPanel({ diff --git a/test/agents-start-kitty-panel.test.js b/test/agents-start-kitty-panel.test.js new file mode 100644 index 0000000..b2d3509 --- /dev/null +++ b/test/agents-start-kitty-panel.test.js @@ -0,0 +1,131 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const Module = require('node:module'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); + +function loadStartWithMocks({ + runPackageAsset, + createAgentSession, + updateAgentSession, + currentBranchName, + listAgentSessions = () => [], +}) { + const startPath = require.resolve('../src/agents/start'); + const runtimePath = require.resolve('../src/core/runtime'); + const sessionsPath = require.resolve('../src/agents/sessions'); + const terminalPath = require.resolve('../src/agents/terminal'); + const gitPath = require.resolve('../src/git'); + const originalLoad = Module._load; + + delete require.cache[startPath]; + delete require.cache[terminalPath]; + Module._load = function mockLoad(request, parent, isMain) { + const resolved = Module._resolveFilename(request, parent, isMain); + if (resolved === runtimePath) { + return { runPackageAsset }; + } + if (resolved === sessionsPath) { + return { createAgentSession, updateAgentSession, listAgentSessions }; + } + if (resolved === gitPath) { + return { currentBranchName }; + } + return originalLoad.apply(this, arguments); + }; + + try { + return require(startPath); + } finally { + Module._load = originalLoad; + delete require.cache[startPath]; + delete require.cache[terminalPath]; + } +} + +function branchStartOutput(branch, worktreePath) { + return [ + `[agent-branch-start] Created branch: ${branch}`, + `[agent-branch-start] Worktree: ${worktreePath}`, + '', + ].join('\n'); +} + +test('panel-launched single agent opens a Kitty terminal session', () => { + const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-kitty-panel-')); + const worktreePath = path.join(repoRoot, '.omx/agent-worktrees/repo__codex__fix-auth'); + const branch = 'agent/codex/fix-auth'; + const runCalls = []; + const terminalCalls = []; + const start = loadStartWithMocks({ + runPackageAsset(assetKey, args, options) { + runCalls.push({ assetKey, args, options }); + return { status: 0, stdout: branchStartOutput(branch, worktreePath), stderr: '' }; + }, + createAgentSession(repoRootArg, payload) { + return payload; + }, + updateAgentSession() { + throw new Error('unexpected update'); + }, + currentBranchName: () => 'main', + }); + + const result = start.startAgentLane(repoRoot, { + task: 'fix auth', + agent: 'codex', + base: 'main', + claims: [], + panel: true, + }, { + terminalRunner(cmd, args, options) { + terminalCalls.push({ cmd, args, options }); + return { status: 0, stdout: args[0] === '--version' ? 'kitty 0.36\n' : '', stderr: '' }; + }, + }); + + assert.equal(result.status, 0); + assert.equal(runCalls.length, 1); + assert.match(result.stdout, /Agent session id: agent__codex__fix-auth/); + assert.match(result.stdout, /Kitty agent terminal:/); + assert.deepEqual(terminalCalls.map((call) => call.args[0]), ['--version', '--detach']); + const sessionFile = terminalCalls[1].args[2]; + assert.match(sessionFile, /\.guardex\/agents\/terminals\/agent__codex__fix-auth-1\.kitty-session$/); + const sessionBody = fs.readFileSync(sessionFile, 'utf8'); + assert.match(sessionBody, /new_tab '1: codex fix-auth'/); + assert.match(sessionBody, /cd '.*repo__codex__fix-auth'/); + assert.match(sessionBody, /launch --title '1: codex fix-auth' sh -lc 'cd/); +}); + +test('non-panel single agent start keeps terminal launch opt-in unchanged', () => { + const terminalCalls = []; + const start = loadStartWithMocks({ + runPackageAsset() { + return { status: 0, stdout: branchStartOutput('agent/codex/fix-auth', '/repo/.omx/agent-worktrees/repo__codex__fix-auth'), stderr: '' }; + }, + createAgentSession(repoRootArg, payload) { + return payload; + }, + updateAgentSession() { + throw new Error('unexpected update'); + }, + currentBranchName: () => 'main', + }); + + const result = start.startAgentLane('/repo', { + task: 'fix auth', + agent: 'codex', + base: 'main', + claims: [], + }, { + terminalRunner(cmd, args, options) { + terminalCalls.push({ cmd, args, options }); + return { status: 0, stdout: '', stderr: '' }; + }, + }); + + assert.equal(result.status, 0); + assert.equal(terminalCalls.length, 0); + assert.doesNotMatch(result.stdout, /Kitty agent terminal:/); +});