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
31 changes: 30 additions & 1 deletion src/cockpit/control.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const { stripAnsi } = require('./theme');
const { renderWelcomePage } = require('./welcome');
const { runCockpitAction } = require('./action-runner');
const { findProjects } = require('./projects-finder');
const { readKittyTree } = require('./kitty-tree');
const { readLogs, filterEntries, LEVELS: LOG_LEVELS } = require('./logs-reader');
const {
PANE_MENU_ITEMS,
Expand Down Expand Up @@ -963,12 +964,38 @@ function readControlSnapshot(options = {}, previousState) {
const cockpitState = stateReader(repoPath);
const settings = readCockpitSettings(repoPath, options);
const at = typeof options.now === 'function' ? options.now() : new Date().toISOString();
return applyCockpitAction(previousState || { repoPath }, {
const next = applyCockpitAction(previousState || { repoPath }, {
type: 'refresh',
cockpitState,
settings,
at,
});
return attachKittyTree(next, options);
}

function attachKittyTree(state, options = {}) {
if (!state || typeof state !== 'object') return state;
const env = options.env || process.env;
if (!env || !env.KITTY_LISTEN_ON) {
if (state.kittyTree) {
return { ...state, kittyTree: null };
}
return state;
}
const reader = typeof options.readKittyTree === 'function' ? options.readKittyTree : readKittyTree;
let tree;
try {
tree = reader({
env,
repoRoot: state.repoPath,
runner: options.kittyTreeRunner,
timeoutMs: options.kittyTreeTimeoutMs,
});
} catch (_error) {
return state;
}
if (!tree || tree.error) return state;
return { ...state, kittyTree: tree };
}

function refreshMsFrom(options, state) {
Expand Down Expand Up @@ -1079,10 +1106,12 @@ module.exports = {
SETTINGS_FIELDS,
applyCockpitAction,
applyCockpitKey: applyKey,
attachKittyTree,
buildCockpitActionContext,
normalizeControlState,
normalizeKey,
readCockpitSettings,
readControlSnapshot,
renderControlFrame,
resolveSelectedSession,
runCockpitAction,
Expand Down
70 changes: 70 additions & 0 deletions test/cockpit-kitty-tree-wire.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
'use strict';

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

const { readControlSnapshot } = require('../src/cockpit/control');

test('readControlSnapshot attaches state.kittyTree when KITTY_LISTEN_ON is set', () => {
const calls = [];
const fakeTree = {
user: 'deadpool',
sessionLabel: 'gitguardex',
osWindowId: 7,
windows: [{ id: 11, title: 'gx cockpit', kind: 'control', isFocused: true }],
error: '',
};
const state = readControlSnapshot({
repoPath: '/repo/gitguardex',
env: { KITTY_LISTEN_ON: 'unix:/tmp/test.sock' },
readState: () => ({ repoPath: '/repo/gitguardex', sessions: [] }),
readSettings: () => ({}),
readKittyTree: (opts) => {
calls.push(opts);
return fakeTree;
},
});

assert.equal(calls.length, 1);
assert.equal(calls[0].repoRoot, '/repo/gitguardex');
assert.equal(state.kittyTree && state.kittyTree.user, 'deadpool');
assert.equal(state.kittyTree.windows.length, 1);
});

test('readControlSnapshot leaves kittyTree null when KITTY_LISTEN_ON is unset', () => {
const state = readControlSnapshot({
repoPath: '/repo/gitguardex',
env: {},
readState: () => ({ repoPath: '/repo/gitguardex', sessions: [] }),
readSettings: () => ({}),
readKittyTree: () => {
throw new Error('readKittyTree should not be called when KITTY_LISTEN_ON is unset');
},
});
assert.equal(state.kittyTree || null, null);
});

test('readControlSnapshot drops kittyTree when the reader returns an error', () => {
const state = readControlSnapshot({
repoPath: '/repo/gitguardex',
env: { KITTY_LISTEN_ON: 'unix:/tmp/test.sock' },
readState: () => ({ repoPath: '/repo/gitguardex', sessions: [] }),
readSettings: () => ({}),
readKittyTree: () => ({
user: 'deadpool', sessionLabel: 'gitguardex', osWindowId: null, windows: [], error: 'kitty @ ls failed',
}),
});
assert.equal(state.kittyTree || null, null);
});

test('readControlSnapshot is resilient when readKittyTree throws', () => {
const state = readControlSnapshot({
repoPath: '/repo/gitguardex',
env: { KITTY_LISTEN_ON: 'unix:/tmp/test.sock' },
readState: () => ({ repoPath: '/repo/gitguardex', sessions: [] }),
readSettings: () => ({}),
readKittyTree: () => { throw new Error('boom'); },
});
// Throw is caught; state still produced, kittyTree absent.
assert.equal(state.kittyTree || null, null);
});
Loading