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
63 changes: 63 additions & 0 deletions src/cockpit/actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
'use strict';

function firstString(...values) {
for (const value of values) {
if (typeof value === 'string' && value.length > 0) return value;
}
return undefined;
}

function normalizeStartResult(result) {
const payload = result && typeof result === 'object' ? result : {};
const ok = Object.prototype.hasOwnProperty.call(payload, 'ok')
? Boolean(payload.ok)
: true;

return {
ok,
sessionId: firstString(payload.sessionId, payload.session?.id, payload.id),
branch: firstString(payload.branch, payload.lane?.branch),
worktreePath: firstString(payload.worktreePath, payload.worktree?.path, payload.path),
message: firstString(
payload.message,
ok ? 'Started agent lane.' : 'Failed to start agent lane.',
),
};
}

function resolveStartImplementation(deps = {}) {
if (typeof deps.startImplementation === 'function') return deps.startImplementation;
if (typeof deps.startAgentLane === 'function') return deps.startAgentLane;
if (typeof deps.startAgent === 'function') return deps.startAgent;

const startModule = require('../agents/start');
if (typeof startModule === 'function') return startModule;
if (typeof startModule.startAgentLane === 'function') return startModule.startAgentLane;
if (typeof startModule.startAgent === 'function') return startModule.startAgent;
if (typeof startModule.start === 'function') return startModule.start;

throw new Error('gx agents start implementation is unavailable');
}

function startAgentLane(options = {}, deps = {}) {
const request = {
task: options.task,
agent: options.agent,
base: options.base,
claims: options.claims,
};
const startImplementation = resolveStartImplementation(deps);
const result = startImplementation(request);

if (result && typeof result.then === 'function') {
return result.then(normalizeStartResult);
}

return normalizeStartResult(result);
}

module.exports = {
startAgentLane,
normalizeStartResult,
resolveStartImplementation,
};
3 changes: 3 additions & 0 deletions src/cockpit/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const { readCockpitState } = require('./state');
const { renderCockpit } = require('./render');
const actions = require('./actions');
const {
ensureTmuxAvailable,
sessionExists,
Expand Down Expand Up @@ -138,4 +139,6 @@ module.exports = {
openCockpit,
render,
startCockpit,
...actions,
actions,
};
91 changes: 91 additions & 0 deletions test/cockpit-actions.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
'use strict';

const assert = require('node:assert/strict');
const test = require('node:test');

const cockpit = require('../src/cockpit');
const {
startAgentLane,
normalizeStartResult,
} = require('../src/cockpit/actions');

test('startAgentLane delegates to the gx agents start implementation', () => {
const calls = [];

const result = startAgentLane(
{
task: 'fix flaky test',
agent: 'codex',
base: 'main',
claims: ['src/foo.js', 'test/foo.test.js'],
},
{
startImplementation(request) {
calls.push(request);
return {
ok: true,
sessionId: 'session-123',
branch: 'agent/codex/fix-flaky-test',
worktreePath: '/repo/.omx/agent-worktrees/fix-flaky-test',
message: 'Started agent lane.',
};
},
},
);

assert.deepEqual(calls, [
{
task: 'fix flaky test',
agent: 'codex',
base: 'main',
claims: ['src/foo.js', 'test/foo.test.js'],
},
]);
assert.deepEqual(result, {
ok: true,
sessionId: 'session-123',
branch: 'agent/codex/fix-flaky-test',
worktreePath: '/repo/.omx/agent-worktrees/fix-flaky-test',
message: 'Started agent lane.',
});
});

test('startAgentLane normalizes async implementation results', async () => {
const result = await startAgentLane(
{ task: 'ship feature', agent: 'claude', base: 'dev', claims: [] },
{
async startAgentLane() {
return {
session: { id: 'session-async' },
lane: { branch: 'agent/claude/ship-feature' },
worktree: { path: '/repo/.omx/agent-worktrees/ship-feature' },
};
},
},
);

assert.deepEqual(result, {
ok: true,
sessionId: 'session-async',
branch: 'agent/claude/ship-feature',
worktreePath: '/repo/.omx/agent-worktrees/ship-feature',
message: 'Started agent lane.',
});
});

test('cockpit index exports startAgentLane action without dropping render API', () => {
assert.equal(cockpit.startAgentLane, startAgentLane);
assert.equal(cockpit.actions.startAgentLane, startAgentLane);
assert.equal(typeof cockpit.render, 'function');
assert.equal(typeof cockpit.startCockpit, 'function');
});

test('normalizeStartResult preserves failure shape', () => {
assert.deepEqual(normalizeStartResult({ ok: false, message: 'missing agent' }), {
ok: false,
sessionId: undefined,
branch: undefined,
worktreePath: undefined,
message: 'missing agent',
});
});
Loading