Skip to content

Commit 0cc513a

Browse files
NagyViktNagyViktclaude
authored
Populate the cockpit kittyTree state on every control snapshot (#533)
Wires the phase-7 kitty-tree reader into readControlSnapshot so the cockpit sidebar's user > session > pane tree fills in automatically when running inside a Kitty session with KITTY_LISTEN_ON exported. The reader runs once per refresh tick (default ~2s); errors and non-Kitty environments leave state.kittyTree null so the sidebar falls back to its pre-phase-7 layout. The reader is injectable via options.readKittyTree for tests. Co-authored-by: NagyVikt <nagy.viktordp@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d7e2fe8 commit 0cc513a

2 files changed

Lines changed: 100 additions & 1 deletion

File tree

src/cockpit/control.js

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const { stripAnsi } = require('./theme');
99
const { renderWelcomePage } = require('./welcome');
1010
const { runCockpitAction } = require('./action-runner');
1111
const { findProjects } = require('./projects-finder');
12+
const { readKittyTree } = require('./kitty-tree');
1213
const { readLogs, filterEntries, LEVELS: LOG_LEVELS } = require('./logs-reader');
1314
const {
1415
PANE_MENU_ITEMS,
@@ -963,12 +964,38 @@ function readControlSnapshot(options = {}, previousState) {
963964
const cockpitState = stateReader(repoPath);
964965
const settings = readCockpitSettings(repoPath, options);
965966
const at = typeof options.now === 'function' ? options.now() : new Date().toISOString();
966-
return applyCockpitAction(previousState || { repoPath }, {
967+
const next = applyCockpitAction(previousState || { repoPath }, {
967968
type: 'refresh',
968969
cockpitState,
969970
settings,
970971
at,
971972
});
973+
return attachKittyTree(next, options);
974+
}
975+
976+
function attachKittyTree(state, options = {}) {
977+
if (!state || typeof state !== 'object') return state;
978+
const env = options.env || process.env;
979+
if (!env || !env.KITTY_LISTEN_ON) {
980+
if (state.kittyTree) {
981+
return { ...state, kittyTree: null };
982+
}
983+
return state;
984+
}
985+
const reader = typeof options.readKittyTree === 'function' ? options.readKittyTree : readKittyTree;
986+
let tree;
987+
try {
988+
tree = reader({
989+
env,
990+
repoRoot: state.repoPath,
991+
runner: options.kittyTreeRunner,
992+
timeoutMs: options.kittyTreeTimeoutMs,
993+
});
994+
} catch (_error) {
995+
return state;
996+
}
997+
if (!tree || tree.error) return state;
998+
return { ...state, kittyTree: tree };
972999
}
9731000

9741001
function refreshMsFrom(options, state) {
@@ -1079,10 +1106,12 @@ module.exports = {
10791106
SETTINGS_FIELDS,
10801107
applyCockpitAction,
10811108
applyCockpitKey: applyKey,
1109+
attachKittyTree,
10821110
buildCockpitActionContext,
10831111
normalizeControlState,
10841112
normalizeKey,
10851113
readCockpitSettings,
1114+
readControlSnapshot,
10861115
renderControlFrame,
10871116
resolveSelectedSession,
10881117
runCockpitAction,
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
'use strict';
2+
3+
const assert = require('node:assert/strict');
4+
const test = require('node:test');
5+
6+
const { readControlSnapshot } = require('../src/cockpit/control');
7+
8+
test('readControlSnapshot attaches state.kittyTree when KITTY_LISTEN_ON is set', () => {
9+
const calls = [];
10+
const fakeTree = {
11+
user: 'deadpool',
12+
sessionLabel: 'gitguardex',
13+
osWindowId: 7,
14+
windows: [{ id: 11, title: 'gx cockpit', kind: 'control', isFocused: true }],
15+
error: '',
16+
};
17+
const state = readControlSnapshot({
18+
repoPath: '/repo/gitguardex',
19+
env: { KITTY_LISTEN_ON: 'unix:/tmp/test.sock' },
20+
readState: () => ({ repoPath: '/repo/gitguardex', sessions: [] }),
21+
readSettings: () => ({}),
22+
readKittyTree: (opts) => {
23+
calls.push(opts);
24+
return fakeTree;
25+
},
26+
});
27+
28+
assert.equal(calls.length, 1);
29+
assert.equal(calls[0].repoRoot, '/repo/gitguardex');
30+
assert.equal(state.kittyTree && state.kittyTree.user, 'deadpool');
31+
assert.equal(state.kittyTree.windows.length, 1);
32+
});
33+
34+
test('readControlSnapshot leaves kittyTree null when KITTY_LISTEN_ON is unset', () => {
35+
const state = readControlSnapshot({
36+
repoPath: '/repo/gitguardex',
37+
env: {},
38+
readState: () => ({ repoPath: '/repo/gitguardex', sessions: [] }),
39+
readSettings: () => ({}),
40+
readKittyTree: () => {
41+
throw new Error('readKittyTree should not be called when KITTY_LISTEN_ON is unset');
42+
},
43+
});
44+
assert.equal(state.kittyTree || null, null);
45+
});
46+
47+
test('readControlSnapshot drops kittyTree when the reader returns an error', () => {
48+
const state = readControlSnapshot({
49+
repoPath: '/repo/gitguardex',
50+
env: { KITTY_LISTEN_ON: 'unix:/tmp/test.sock' },
51+
readState: () => ({ repoPath: '/repo/gitguardex', sessions: [] }),
52+
readSettings: () => ({}),
53+
readKittyTree: () => ({
54+
user: 'deadpool', sessionLabel: 'gitguardex', osWindowId: null, windows: [], error: 'kitty @ ls failed',
55+
}),
56+
});
57+
assert.equal(state.kittyTree || null, null);
58+
});
59+
60+
test('readControlSnapshot is resilient when readKittyTree throws', () => {
61+
const state = readControlSnapshot({
62+
repoPath: '/repo/gitguardex',
63+
env: { KITTY_LISTEN_ON: 'unix:/tmp/test.sock' },
64+
readState: () => ({ repoPath: '/repo/gitguardex', sessions: [] }),
65+
readSettings: () => ({}),
66+
readKittyTree: () => { throw new Error('boom'); },
67+
});
68+
// Throw is caught; state still produced, kittyTree absent.
69+
assert.equal(state.kittyTree || null, null);
70+
});

0 commit comments

Comments
 (0)