@imdeadpool/guardex 7.0.39 → 7.0.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +85 -16
  2. package/package.json +2 -1
  3. package/skills/gitguardex/SKILL.md +13 -0
  4. package/skills/guardex-merge-skills-to-dev/SKILL.md +59 -0
  5. package/src/agents/cleanup-sessions.js +126 -0
  6. package/src/agents/detect.js +160 -0
  7. package/src/agents/finish.js +172 -0
  8. package/src/agents/inspect.js +189 -0
  9. package/src/agents/launch.js +240 -0
  10. package/src/agents/registry.js +133 -0
  11. package/src/agents/selection-panel.js +571 -0
  12. package/src/agents/sessions.js +151 -0
  13. package/src/agents/start.js +591 -0
  14. package/src/agents/status.js +143 -0
  15. package/src/agents/terminal.js +152 -0
  16. package/src/budget/index.js +343 -0
  17. package/src/ci-init/index.js +265 -0
  18. package/src/cli/args.js +305 -1
  19. package/src/cli/main.js +262 -132
  20. package/src/cockpit/action-runner.js +3 -0
  21. package/src/cockpit/actions.js +80 -0
  22. package/src/cockpit/control.js +1121 -0
  23. package/src/cockpit/index.js +426 -0
  24. package/src/cockpit/keybindings.js +224 -0
  25. package/src/cockpit/kitty-layout.js +549 -0
  26. package/src/cockpit/kitty-tree.js +144 -0
  27. package/src/cockpit/layout.js +224 -0
  28. package/src/cockpit/logs-reader.js +182 -0
  29. package/src/cockpit/menu.js +204 -0
  30. package/src/cockpit/pane-actions.js +597 -0
  31. package/src/cockpit/pane-menu.js +387 -0
  32. package/src/cockpit/projects-finder.js +178 -0
  33. package/src/cockpit/render.js +215 -0
  34. package/src/cockpit/settings-render.js +128 -0
  35. package/src/cockpit/settings.js +124 -0
  36. package/src/cockpit/shortcuts.js +24 -0
  37. package/src/cockpit/sidebar.js +311 -0
  38. package/src/cockpit/state.js +72 -0
  39. package/src/cockpit/theme.js +128 -0
  40. package/src/cockpit/welcome.js +266 -0
  41. package/src/context.js +78 -35
  42. package/src/doctor/index.js +4 -3
  43. package/src/finish/index.js +39 -2
  44. package/src/git/index.js +65 -0
  45. package/src/kitty/command.js +101 -0
  46. package/src/kitty/runtime.js +250 -0
  47. package/src/output/index.js +1 -1
  48. package/src/pr-review.js +241 -0
  49. package/src/scaffold/index.js +19 -0
  50. package/src/submodule/index.js +288 -0
  51. package/src/terminal/index.js +120 -0
  52. package/src/terminal/kitty.js +622 -0
  53. package/src/terminal/tmux.js +126 -0
  54. package/src/tmux/command.js +27 -0
  55. package/src/tmux/session.js +89 -0
  56. package/templates/AGENTS.multiagent-safety.md +421 -37
  57. package/templates/codex/skills/gitguardex/SKILL.md +2 -0
  58. package/templates/githooks/pre-commit +22 -1
  59. package/templates/github/workflows/README.md +87 -0
  60. package/templates/github/workflows/ci-full.yml +55 -0
  61. package/templates/github/workflows/ci.yml +56 -0
  62. package/templates/github/workflows/cr.yml +20 -1
  63. package/templates/scripts/agent-branch-finish.sh +545 -27
  64. package/templates/scripts/agent-branch-start.sh +193 -21
  65. package/templates/scripts/agent-preflight.sh +89 -0
  66. package/templates/scripts/agent-worktree-prune.sh +96 -5
  67. package/templates/scripts/codex-agent.sh +41 -6
  68. package/templates/scripts/openspec/init-plan-workspace.sh +43 -0
  69. package/templates/scripts/review-bot-watch.sh +31 -2
  70. package/templates/scripts/agent-session-state.js +0 -171
  71. package/templates/scripts/install-vscode-active-agents-extension.js +0 -135
  72. package/templates/vscode/guardex-active-agents/README.md +0 -34
  73. package/templates/vscode/guardex-active-agents/extension.js +0 -3782
  74. package/templates/vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json +0 -54
  75. package/templates/vscode/guardex-active-agents/fileicons/icons/agent.svg +0 -5
  76. package/templates/vscode/guardex-active-agents/fileicons/icons/branch.svg +0 -7
  77. package/templates/vscode/guardex-active-agents/fileicons/icons/config.svg +0 -4
  78. package/templates/vscode/guardex-active-agents/fileicons/icons/hook.svg +0 -4
  79. package/templates/vscode/guardex-active-agents/fileicons/icons/openspec.svg +0 -5
  80. package/templates/vscode/guardex-active-agents/fileicons/icons/plan.svg +0 -4
  81. package/templates/vscode/guardex-active-agents/fileicons/icons/spec.svg +0 -5
  82. package/templates/vscode/guardex-active-agents/icon.png +0 -0
  83. package/templates/vscode/guardex-active-agents/media/active-agents-hivemind.svg +0 -14
  84. package/templates/vscode/guardex-active-agents/package.json +0 -169
  85. package/templates/vscode/guardex-active-agents/session-schema.js +0 -1348
@@ -0,0 +1,224 @@
1
+ const DEFAULT_SESSION_NAME = 'guardex';
2
+ const DEFAULT_SIDEBAR_WIDTH = 34;
3
+ const DEFAULT_TERMINAL_COLUMNS = 120;
4
+ const DEFAULT_TERMINAL_ROWS = 40;
5
+ const MIN_CONTENT_COLUMNS = 20;
6
+
7
+ function text(value, fallback = '') {
8
+ if (typeof value === 'string') return value.trim() || fallback;
9
+ if (value === null || value === undefined) return fallback;
10
+ return String(value).trim() || fallback;
11
+ }
12
+
13
+ function positiveInteger(value, fallback) {
14
+ const parsed = Number.parseInt(String(value), 10);
15
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
16
+ }
17
+
18
+ function sidebarWidthFor(value, terminalColumns) {
19
+ const requested = positiveInteger(value, DEFAULT_SIDEBAR_WIDTH);
20
+ if (terminalColumns <= MIN_CONTENT_COLUMNS) {
21
+ return Math.max(1, terminalColumns - 1);
22
+ }
23
+ return Math.min(requested, terminalColumns - MIN_CONTENT_COLUMNS);
24
+ }
25
+
26
+ function shellQuote(value) {
27
+ return `'${String(value).replace(/'/g, "'\\''")}'`;
28
+ }
29
+
30
+ function normalizeSessions(sessions) {
31
+ if (!Array.isArray(sessions)) return [];
32
+ return sessions.map((session, index) => ({
33
+ sessionId: text(session.sessionId || session.id, `session-${index + 1}`),
34
+ branch: text(session.branch),
35
+ worktreePath: text(session.worktreePath),
36
+ command: text(session.command),
37
+ }));
38
+ }
39
+
40
+ function paneTarget(sessionName, paneIndex) {
41
+ return `${sessionName}:0.${paneIndex}`;
42
+ }
43
+
44
+ function windowTarget(sessionName) {
45
+ return `${sessionName}:0`;
46
+ }
47
+
48
+ function agentGrid(count) {
49
+ if (count <= 1) return { columns: 1, rows: 1 };
50
+ const columns = Math.ceil(Math.sqrt(count));
51
+ return {
52
+ columns,
53
+ rows: Math.ceil(count / columns),
54
+ };
55
+ }
56
+
57
+ function dimension(total, parts, index) {
58
+ const base = Math.floor(total / parts);
59
+ const remainder = total % parts;
60
+ return base + (index < remainder ? 1 : 0);
61
+ }
62
+
63
+ function offset(total, parts, index) {
64
+ let value = 0;
65
+ for (let part = 0; part < index; part += 1) {
66
+ value += dimension(total, parts, part);
67
+ }
68
+ return value;
69
+ }
70
+
71
+ function agentGeometry(index, count, sidebarWidth, terminalColumns, terminalRows) {
72
+ const mainColumns = Math.max(1, terminalColumns - sidebarWidth);
73
+ const grid = agentGrid(count);
74
+ const column = index % grid.columns;
75
+ const row = Math.floor(index / grid.columns);
76
+ return {
77
+ x: sidebarWidth + offset(mainColumns, grid.columns, column),
78
+ y: offset(terminalRows, grid.rows, row),
79
+ width: dimension(mainColumns, grid.columns, column),
80
+ height: dimension(terminalRows, grid.rows, row),
81
+ };
82
+ }
83
+
84
+ function sidebarPane(sessionName, sidebarWidth, terminalRows, command) {
85
+ return {
86
+ role: 'sidebar',
87
+ target: paneTarget(sessionName, 0),
88
+ width: sidebarWidth,
89
+ height: terminalRows,
90
+ command,
91
+ };
92
+ }
93
+
94
+ function agentCommand(session) {
95
+ if (session.command) return session.command;
96
+ if (!session.worktreePath) return 'gx agents status';
97
+ return `cd ${shellQuote(session.worktreePath)} && exec ${'${SHELL:-bash}'}`;
98
+ }
99
+
100
+ function agentPane(sessionName, session, index, count, selectedSessionId, sidebarWidth, terminalColumns, terminalRows) {
101
+ return {
102
+ role: 'agent',
103
+ target: paneTarget(sessionName, index + 1),
104
+ sessionId: session.sessionId,
105
+ branch: session.branch,
106
+ worktreePath: session.worktreePath,
107
+ selected: session.sessionId === selectedSessionId,
108
+ command: agentCommand(session),
109
+ ...agentGeometry(index, count, sidebarWidth, terminalColumns, terminalRows),
110
+ };
111
+ }
112
+
113
+ function detailsPane(sessionName, paneIndex, sidebarWidth, terminalColumns, terminalRows, command) {
114
+ return {
115
+ role: 'details',
116
+ target: paneTarget(sessionName, paneIndex),
117
+ x: sidebarWidth,
118
+ y: 0,
119
+ width: Math.max(1, terminalColumns - sidebarWidth),
120
+ height: terminalRows,
121
+ command,
122
+ };
123
+ }
124
+
125
+ function sendKeysCommand(role, target, command) {
126
+ return {
127
+ role,
128
+ args: ['send-keys', '-t', target, command, 'C-m'],
129
+ };
130
+ }
131
+
132
+ function buildTmuxCommands(plan) {
133
+ const commands = [
134
+ {
135
+ role: 'session',
136
+ args: ['new-session', '-d', '-s', plan.sessionName],
137
+ },
138
+ sendKeysCommand('sidebar', plan.panes[0].target, plan.panes[0].command),
139
+ ];
140
+
141
+ const contentPanes = plan.panes.filter((pane) => pane.role !== 'sidebar');
142
+ if (contentPanes.length > 0) {
143
+ commands.push({
144
+ role: 'content',
145
+ args: ['split-window', '-h', '-t', plan.panes[0].target],
146
+ });
147
+ commands.push({
148
+ role: 'sidebar',
149
+ args: ['resize-pane', '-t', plan.panes[0].target, '-x', String(plan.sidebarWidth)],
150
+ });
151
+ }
152
+
153
+ for (let index = 1; index < contentPanes.length; index += 1) {
154
+ commands.push({
155
+ role: contentPanes[index].role,
156
+ args: ['split-window', index % 2 === 1 ? '-h' : '-v', '-t', contentPanes[index - 1].target],
157
+ });
158
+ }
159
+
160
+ if (contentPanes.filter((pane) => pane.role === 'agent').length > 1) {
161
+ commands.push({
162
+ role: 'layout',
163
+ args: ['select-layout', '-t', windowTarget(plan.sessionName), 'tiled'],
164
+ });
165
+ commands.push({
166
+ role: 'sidebar',
167
+ args: ['resize-pane', '-t', plan.panes[0].target, '-x', String(plan.sidebarWidth)],
168
+ });
169
+ }
170
+
171
+ for (const pane of contentPanes) {
172
+ commands.push(sendKeysCommand(pane.role, pane.target, pane.command));
173
+ }
174
+
175
+ return commands;
176
+ }
177
+
178
+ function planCockpitLayout(options = {}) {
179
+ const sessions = normalizeSessions(options.sessions);
180
+ const terminalColumns = positiveInteger(options.terminalColumns, DEFAULT_TERMINAL_COLUMNS);
181
+ const terminalRows = positiveInteger(options.terminalRows, DEFAULT_TERMINAL_ROWS);
182
+ const sessionName = text(options.sessionName, DEFAULT_SESSION_NAME);
183
+ const sidebarWidth = sidebarWidthFor(options.sidebarWidth, terminalColumns);
184
+ const sidebarCommand = text(options.sidebarCommand, 'gx agents status');
185
+ const selectedSessionId = text(options.selectedSessionId);
186
+ const panes = [
187
+ sidebarPane(sessionName, sidebarWidth, terminalRows, sidebarCommand),
188
+ ];
189
+
190
+ if (sessions.length === 0) {
191
+ panes.push(detailsPane(sessionName, 1, sidebarWidth, terminalColumns, terminalRows, 'gx agents status'));
192
+ } else {
193
+ sessions.forEach((session, index) => {
194
+ panes.push(agentPane(
195
+ sessionName,
196
+ session,
197
+ index,
198
+ sessions.length,
199
+ selectedSessionId,
200
+ sidebarWidth,
201
+ terminalColumns,
202
+ terminalRows,
203
+ ));
204
+ });
205
+ }
206
+
207
+ const plan = {
208
+ sessionName,
209
+ terminalColumns,
210
+ terminalRows,
211
+ sidebarWidth,
212
+ panes,
213
+ };
214
+ return {
215
+ ...plan,
216
+ tmuxCommands: buildTmuxCommands(plan),
217
+ };
218
+ }
219
+
220
+ module.exports = {
221
+ DEFAULT_SESSION_NAME,
222
+ DEFAULT_SIDEBAR_WIDTH,
223
+ planCockpitLayout,
224
+ };
@@ -0,0 +1,182 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const DEFAULT_TAIL_BYTES = 32 * 1024;
7
+ const DEFAULT_LIMIT = 200;
8
+ const DEFAULT_LOG_GLOBS = ['apps/logs', '.omc/logs', '.omx/logs'];
9
+
10
+ const LEVELS = ['info', 'warning', 'error', 'debug'];
11
+ const LEVEL_PATTERNS = [
12
+ { level: 'error', regex: /\b(error|err|exception|fail(?:ed|ure)?|fatal|panic|traceback)\b/i },
13
+ { level: 'warning', regex: /\b(warn|warning|deprecated|caution)\b/i },
14
+ { level: 'debug', regex: /\b(debug|trace|verbose)\b/i },
15
+ ];
16
+
17
+ function text(value, fallback = '') {
18
+ if (typeof value === 'string') return value || fallback;
19
+ if (value === null || value === undefined) return fallback;
20
+ return String(value) || fallback;
21
+ }
22
+
23
+ function classifyLevel(line) {
24
+ for (const { level, regex } of LEVEL_PATTERNS) {
25
+ if (regex.test(line)) return level;
26
+ }
27
+ return 'info';
28
+ }
29
+
30
+ function pickFs(options = {}) {
31
+ return options.fs || fs;
32
+ }
33
+
34
+ function listLogPaths(root, options = {}) {
35
+ const fsImpl = pickFs(options);
36
+ const globs = Array.isArray(options.globs) && options.globs.length > 0 ? options.globs : DEFAULT_LOG_GLOBS;
37
+ const seen = new Set();
38
+ const paths = [];
39
+
40
+ for (const glob of globs) {
41
+ const dir = path.isAbsolute(glob) ? glob : path.join(root, glob);
42
+ pushLogsFromDir(dir, fsImpl, seen, paths);
43
+ }
44
+
45
+ return paths;
46
+ }
47
+
48
+ function pushLogsFromDir(dir, fsImpl, seen, paths) {
49
+ let entries;
50
+ try {
51
+ entries = fsImpl.readdirSync(dir, { withFileTypes: true });
52
+ } catch (_error) {
53
+ return;
54
+ }
55
+ for (const entry of entries) {
56
+ const full = path.join(dir, entry.name);
57
+ if (entry.isDirectory()) {
58
+ pushLogsFromDir(full, fsImpl, seen, paths);
59
+ continue;
60
+ }
61
+ if (!entry.isFile()) continue;
62
+ if (!entry.name.endsWith('.log')) continue;
63
+ if (seen.has(full)) continue;
64
+ seen.add(full);
65
+ paths.push(full);
66
+ }
67
+ }
68
+
69
+ function tailFile(file, options = {}) {
70
+ const fsImpl = pickFs(options);
71
+ const tailBytes = Number.isFinite(options.tailBytes) && options.tailBytes > 0
72
+ ? options.tailBytes
73
+ : DEFAULT_TAIL_BYTES;
74
+ let stat;
75
+ try {
76
+ stat = fsImpl.statSync(file, { throwIfNoEntry: false });
77
+ } catch (_error) {
78
+ return [];
79
+ }
80
+ if (!stat) return [];
81
+
82
+ const size = Number.isFinite(stat.size) ? stat.size : 0;
83
+ const start = size > tailBytes ? size - tailBytes : 0;
84
+
85
+ let content = '';
86
+ try {
87
+ if (typeof fsImpl.openSync === 'function' && typeof fsImpl.readSync === 'function' && typeof fsImpl.closeSync === 'function' && start > 0) {
88
+ const fd = fsImpl.openSync(file, 'r');
89
+ try {
90
+ const buffer = Buffer.alloc(size - start);
91
+ fsImpl.readSync(fd, buffer, 0, buffer.length, start);
92
+ content = buffer.toString('utf8');
93
+ } finally {
94
+ fsImpl.closeSync(fd);
95
+ }
96
+ } else if (typeof fsImpl.readFileSync === 'function') {
97
+ content = fsImpl.readFileSync(file, 'utf8');
98
+ if (content.length > tailBytes) content = content.slice(content.length - tailBytes);
99
+ }
100
+ } catch (_error) {
101
+ return [];
102
+ }
103
+
104
+ const lines = content.split('\n');
105
+ return lines.map((line, index) => ({ line, partial: index === 0 && start > 0 }));
106
+ }
107
+
108
+ function readLogs(options = {}) {
109
+ const root = text(options.repoRoot || options.root || process.cwd(), process.cwd());
110
+ const limit = Number.isFinite(options.limit) && options.limit > 0 ? options.limit : DEFAULT_LIMIT;
111
+ const sources = options.sources || listLogPaths(root, options);
112
+ const entries = [];
113
+
114
+ for (const source of sources) {
115
+ const tail = tailFile(source, options);
116
+ const sourceName = path.relative(root, source) || source;
117
+ for (let i = 0; i < tail.length; i += 1) {
118
+ const { line, partial } = tail[i];
119
+ if (partial && i === 0) continue;
120
+ const trimmed = line.replace(/\r$/, '');
121
+ if (!trimmed) continue;
122
+ entries.push({
123
+ source: sourceName,
124
+ line: trimmed,
125
+ level: classifyLevel(trimmed),
126
+ });
127
+ }
128
+ }
129
+
130
+ if (entries.length > limit) {
131
+ entries.splice(0, entries.length - limit);
132
+ }
133
+
134
+ return {
135
+ entries,
136
+ sources: sources.map((source) => path.relative(root, source) || source),
137
+ counts: tallyLevels(entries),
138
+ };
139
+ }
140
+
141
+ function tallyLevels(entries) {
142
+ const counts = { all: entries.length };
143
+ for (const level of LEVELS) counts[level] = 0;
144
+ for (const entry of entries) {
145
+ counts[entry.level] = (counts[entry.level] || 0) + 1;
146
+ }
147
+ return counts;
148
+ }
149
+
150
+ function filterEntries(entries, filter) {
151
+ const value = String(filter || 'all').toLowerCase();
152
+ if (value === 'all' || value === '') return entries.slice();
153
+ if (value === 'by-pane' || value === 'by-source') {
154
+ const groups = new Map();
155
+ for (const entry of entries) {
156
+ const key = entry.source || 'unknown';
157
+ const arr = groups.get(key) || [];
158
+ arr.push(entry);
159
+ groups.set(key, arr);
160
+ }
161
+ const ordered = [];
162
+ for (const [, arr] of groups) ordered.push(...arr);
163
+ return ordered;
164
+ }
165
+ if (LEVELS.includes(value)) {
166
+ return entries.filter((entry) => entry.level === value);
167
+ }
168
+ return entries.slice();
169
+ }
170
+
171
+ module.exports = {
172
+ DEFAULT_LIMIT,
173
+ DEFAULT_LOG_GLOBS,
174
+ DEFAULT_TAIL_BYTES,
175
+ LEVELS,
176
+ classifyLevel,
177
+ filterEntries,
178
+ listLogPaths,
179
+ readLogs,
180
+ tailFile,
181
+ tallyLevels,
182
+ };
@@ -0,0 +1,204 @@
1
+ 'use strict';
2
+
3
+ const paneMenu = require('./pane-menu');
4
+ const { colorize, getCockpitTheme, stripAnsi } = require('./theme');
5
+
6
+ const {
7
+ PANE_MENU_ACTIONS,
8
+ PANE_MENU_ACTION_IDS,
9
+ PANE_MENU_FOOTER,
10
+ normalizePaneMenuKey,
11
+ } = paneMenu;
12
+
13
+ const PANE_MENU_ITEMS = Object.freeze([
14
+ { id: PANE_MENU_ACTION_IDS.VIEW, label: 'View', hotkey: 'v', needsSession: true },
15
+ { id: PANE_MENU_ACTION_IDS.HIDE_PANE, label: 'Hide Pane', hotkey: 'h', needsSession: true },
16
+ { id: PANE_MENU_ACTION_IDS.CLOSE, label: 'Close', hotkey: 'x', danger: true, needsSession: true },
17
+ { id: PANE_MENU_ACTION_IDS.MERGE, label: 'Merge / Finish', hotkey: 'm', needsSession: true, needsWorktree: true, needsBranch: true },
18
+ { id: PANE_MENU_ACTION_IDS.CREATE_PR, label: 'Create GitHub PR', hotkey: 'p', needsSession: true, needsWorktree: true, needsBranch: true },
19
+ { id: PANE_MENU_ACTION_IDS.RENAME, label: 'Rename', hotkey: 'r', needsSession: true },
20
+ { id: PANE_MENU_ACTION_IDS.COPY_PATH, label: 'Copy Path', hotkey: 'c', needsSession: true, needsWorktree: true },
21
+ { id: PANE_MENU_ACTION_IDS.OPEN_EDITOR, label: 'Open in Editor', hotkey: 'o', needsSession: true, needsWorktree: true },
22
+ { id: PANE_MENU_ACTION_IDS.TOGGLE_AUTOPILOT, label: 'Toggle Autopilot', hotkey: 'a', needsSession: true, needsWorktree: true, needsBranch: true },
23
+ { id: PANE_MENU_ACTION_IDS.CREATE_CHILD_WORKTREE, label: 'Create Child Worktree', hotkey: 'b', needsSession: true, needsWorktree: true, needsBranch: true },
24
+ { id: PANE_MENU_ACTION_IDS.BROWSE_FILES, label: 'Browse Files', hotkey: 'f', needsSession: true, needsWorktree: true },
25
+ { id: PANE_MENU_ACTION_IDS.ADD_TERMINAL, label: 'Add Terminal to Worktree', hotkey: 'T', needsSession: true, needsWorktree: true },
26
+ { id: PANE_MENU_ACTION_IDS.ADD_AGENT, label: 'Add Agent to Worktree', hotkey: 'A', needsSession: true, needsWorktree: true, needsBranch: true },
27
+ ]);
28
+
29
+ function firstString(...values) {
30
+ for (const value of values) {
31
+ if (typeof value === 'string' && value.trim().length > 0) {
32
+ return value.trim();
33
+ }
34
+ }
35
+ return '';
36
+ }
37
+
38
+ function fileName(value) {
39
+ const text = String(value || '').replace(/[/\\]+$/, '');
40
+ const parts = text.split(/[/\\]+/).filter(Boolean);
41
+ return parts[parts.length - 1] || '';
42
+ }
43
+
44
+ function selectedPaneName(session = {}, context = {}) {
45
+ return firstString(
46
+ context.name,
47
+ session.displayName,
48
+ session.paneName,
49
+ session.name,
50
+ session.agentName,
51
+ session.agent,
52
+ fileName(session.worktreePath),
53
+ fileName(session.path),
54
+ session.branch,
55
+ session.id,
56
+ 'selected pane',
57
+ );
58
+ }
59
+
60
+ function paneMenuTitle(name) {
61
+ const text = String(name || '').trim() || 'selected pane';
62
+ return text.startsWith('Menu:') ? text : `Menu: ${text}`;
63
+ }
64
+
65
+ function selectedSession(context = {}) {
66
+ return context.session || context.selectedSession || context.pane || context.lane || null;
67
+ }
68
+
69
+ function resolveBranch(session = {}, context = {}) {
70
+ return firstString(
71
+ context.branch,
72
+ session.branch,
73
+ session.lane && session.lane.branch,
74
+ );
75
+ }
76
+
77
+ function resolveWorktreePath(session = {}, context = {}) {
78
+ return firstString(
79
+ context.worktreePath,
80
+ context.path,
81
+ session.worktreePath,
82
+ session.worktree && session.worktree.path,
83
+ session.path,
84
+ );
85
+ }
86
+
87
+ function resolveWorktreeExists(session = {}, context = {}, worktreePath = '') {
88
+ if (typeof context.worktreeExists === 'boolean') return context.worktreeExists;
89
+ if (typeof session.worktreeExists === 'boolean') return session.worktreeExists;
90
+ return worktreePath.length > 0;
91
+ }
92
+
93
+ function disabledReason(item, context) {
94
+ if (item.needsSession && !context.selected) return 'No pane selected';
95
+
96
+ const reasons = [];
97
+ if (item.needsWorktree && !context.worktreeExists) reasons.push('Worktree missing');
98
+ if (item.needsBranch && !context.branch) reasons.push('Branch missing');
99
+ return reasons.join('; ');
100
+ }
101
+
102
+ function createPaneMenuItems(context) {
103
+ return PANE_MENU_ITEMS.map((item) => {
104
+ const reason = disabledReason(item, context);
105
+ return {
106
+ id: item.id,
107
+ label: item.label,
108
+ hotkey: item.hotkey,
109
+ shortcut: item.hotkey,
110
+ enabled: reason.length === 0,
111
+ danger: Boolean(item.danger),
112
+ reason,
113
+ };
114
+ });
115
+ }
116
+
117
+ function createPaneMenuState(options = {}) {
118
+ const session = selectedSession(options);
119
+ const selected = Boolean(session) && options.selected !== false;
120
+ const source = session || {};
121
+ const branch = selected ? resolveBranch(source, options) : '';
122
+ const worktreePath = selected ? resolveWorktreePath(source, options) : '';
123
+ const context = {
124
+ selected,
125
+ branch,
126
+ worktreePath,
127
+ worktreeExists: selected && resolveWorktreeExists(source, options, worktreePath),
128
+ };
129
+ const items = Array.isArray(options.items) && options.items.length > 0
130
+ ? options.items.map((item) => ({ ...item }))
131
+ : createPaneMenuItems(context);
132
+
133
+ return paneMenu.createPaneMenuState({
134
+ ...options,
135
+ session,
136
+ title: paneMenuTitle(firstString(options.title, selectedPaneName(source, options))),
137
+ items,
138
+ });
139
+ }
140
+
141
+ function applyPaneMenuKey(state = {}, rawKey) {
142
+ return paneMenu.applyPaneMenuKey(createPaneMenuState(state), rawKey);
143
+ }
144
+
145
+ function themeMenuLine(line, state, theme) {
146
+ const plain = stripAnsi(line);
147
+ if (/^[┌├└+]/.test(plain)) {
148
+ return colorize(line, 'border', theme);
149
+ }
150
+ if (plain.includes('Menu:')) {
151
+ return colorize(line, 'title', theme);
152
+ }
153
+ if (plain.includes('status:')) {
154
+ return colorize(line, 'warning', theme);
155
+ }
156
+ if (plain.includes('Close')) {
157
+ return colorize(line, plain.includes('>') ? 'selected' : 'danger', theme);
158
+ }
159
+ if (plain.includes('>')) {
160
+ return colorize(line, 'selected', theme);
161
+ }
162
+ if (plain.includes(PANE_MENU_FOOTER)) {
163
+ return colorize(line, 'secondary', theme);
164
+ }
165
+ return line;
166
+ }
167
+
168
+ function applyMenuTheme(output, state, options) {
169
+ const theme = getCockpitTheme(options.theme || state.theme || (state.settings && state.settings.theme), options);
170
+ if (!theme.color) {
171
+ return output;
172
+ }
173
+ return `${String(output).replace(/\n$/, '').split('\n').map((line) => themeMenuLine(line, state, theme)).join('\n')}\n`;
174
+ }
175
+
176
+ function renderPaneMenu(state = {}, options = {}) {
177
+ const selectedIndex = Number.isInteger(options.selectedIndex)
178
+ ? options.selectedIndex
179
+ : state.selectedIndex;
180
+ const current = createPaneMenuState({ ...state, selectedIndex });
181
+ const output = paneMenu.renderPaneMenu(current, options).replace(/\u25b6/g, '>');
182
+ return applyMenuTheme(output, current, options);
183
+ }
184
+
185
+ function buildLaneMenu(session, context = {}) {
186
+ return createPaneMenuState({ ...context, session });
187
+ }
188
+
189
+ function renderLaneMenu(menu, options = {}) {
190
+ return renderPaneMenu(menu, options);
191
+ }
192
+
193
+ module.exports = {
194
+ PANE_MENU_ACTIONS,
195
+ PANE_MENU_ACTION_IDS,
196
+ PANE_MENU_FOOTER,
197
+ PANE_MENU_ITEMS,
198
+ applyPaneMenuKey,
199
+ buildLaneMenu,
200
+ createPaneMenuState,
201
+ normalizePaneMenuKey,
202
+ renderLaneMenu,
203
+ renderPaneMenu,
204
+ };