@colmbus72/yeehaw 0.4.2 → 0.5.0

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.
package/dist/lib/tmux.js CHANGED
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'url';
4
4
  import { dirname, join } from 'path';
5
5
  import { writeTmuxConfig, TMUX_CONFIG_PATH } from './tmux-config.js';
6
6
  import { shellEscape } from './shell.js';
7
+ import { readSignal, getStatusIcon } from './signals.js';
7
8
  // Get the path to the MCP server (it's in dist/, not dist/lib/)
8
9
  const __filename = fileURLToPath(import.meta.url);
9
10
  const __dirname = dirname(__filename);
@@ -147,6 +148,15 @@ const YEEHAW_MCP_TOOLS = [
147
148
  'mcp__yeehaw__update_wiki_section',
148
149
  'mcp__yeehaw__delete_wiki_section',
149
150
  ];
151
+ /**
152
+ * Set the window type option for a window (used for reliable window type detection)
153
+ */
154
+ function setWindowType(windowIndex, type) {
155
+ execaSync('tmux', [
156
+ 'set-option', '-w', '-t', `${YEEHAW_SESSION}:${windowIndex}`,
157
+ '@yeehaw_type', type,
158
+ ]);
159
+ }
150
160
  export function createClaudeWindow(workingDir, windowName) {
151
161
  // Build MCP config for yeehaw server
152
162
  const mcpConfig = JSON.stringify({
@@ -175,7 +185,10 @@ export function createClaudeWindow(workingDir, windowName) {
175
185
  const result = execaSync('tmux', [
176
186
  'display-message', '-p', '#{window_index}'
177
187
  ]);
178
- return parseInt(result.stdout.trim(), 10);
188
+ const windowIndex = parseInt(result.stdout.trim(), 10);
189
+ // Mark this window as a Claude session
190
+ setWindowType(windowIndex, 'claude');
191
+ return windowIndex;
179
192
  }
180
193
  export function createShellWindow(workingDir, windowName, shell) {
181
194
  // Use the user's configured shell from $SHELL, fallback to /bin/bash
@@ -194,7 +207,10 @@ export function createShellWindow(workingDir, windowName, shell) {
194
207
  const result = execaSync('tmux', [
195
208
  'display-message', '-p', '#{window_index}'
196
209
  ]);
197
- return parseInt(result.stdout.trim(), 10);
210
+ const windowIndex = parseInt(result.stdout.trim(), 10);
211
+ // Mark this window as a shell session
212
+ setWindowType(windowIndex, 'shell');
213
+ return windowIndex;
198
214
  }
199
215
  export function createSshWindow(windowName, host, user, port, identityFile, remotePath) {
200
216
  // Two levels of escaping needed:
@@ -222,7 +238,10 @@ export function createSshWindow(windowName, host, user, port, identityFile, remo
222
238
  const result = execaSync('tmux', [
223
239
  'display-message', '-p', '#{window_index}'
224
240
  ]);
225
- return parseInt(result.stdout.trim(), 10);
241
+ const windowIndex = parseInt(result.stdout.trim(), 10);
242
+ // Mark this window as an SSH session
243
+ setWindowType(windowIndex, 'ssh');
244
+ return windowIndex;
226
245
  }
227
246
  export function detachFromSession() {
228
247
  execaSync('tmux', ['detach-client']);
@@ -235,29 +254,37 @@ export function killYeehawSession() {
235
254
  // Session might already be dead
236
255
  }
237
256
  }
257
+ export function restartYeehaw() {
258
+ // Respawn window 0 with a fresh yeehaw process
259
+ // This kills the current process but preserves all other windows
260
+ execaSync('tmux', ['respawn-window', '-k', '-t', `${YEEHAW_SESSION}:0`, 'yeehaw']);
261
+ }
238
262
  export function switchToWindow(windowIndex) {
239
263
  execaSync('tmux', ['select-window', '-t', `${YEEHAW_SESSION}:${windowIndex}`]);
240
264
  }
241
265
  export function listYeehawWindows() {
242
266
  try {
243
267
  // Use tab as delimiter since pane_title can contain colons
268
+ // Include @yeehaw_type window option for reliable window type detection
244
269
  const result = execaSync('tmux', [
245
270
  'list-windows',
246
271
  '-t', YEEHAW_SESSION,
247
- '-F', '#{window_index}\t#{window_name}\t#{window_active}\t#{pane_title}\t#{pane_current_command}\t#{window_activity}',
272
+ '-F', '#{window_index}\t#{window_name}\t#{window_active}\t#{pane_id}\t#{pane_title}\t#{pane_current_command}\t#{window_activity}\t#{@yeehaw_type}',
248
273
  ]);
249
274
  return result.stdout
250
275
  .split('\n')
251
276
  .filter(Boolean)
252
277
  .map((line) => {
253
- const [index, name, active, paneTitle, paneCurrentCommand, windowActivity] = line.split('\t');
278
+ const [index, name, active, paneId, paneTitle, paneCurrentCommand, windowActivity, type] = line.split('\t');
254
279
  return {
255
280
  index: parseInt(index, 10),
256
281
  name,
257
282
  active: active === '1',
283
+ paneId: paneId || '',
258
284
  paneTitle: paneTitle || '',
259
285
  paneCurrentCommand: paneCurrentCommand || '',
260
286
  windowActivity: parseInt(windowActivity, 10) || 0,
287
+ type: (type || ''),
261
288
  };
262
289
  });
263
290
  }
@@ -296,34 +323,58 @@ function isClaudeWorking(paneTitle) {
296
323
  return spinnerChars.some(char => paneTitle.startsWith(char));
297
324
  }
298
325
  /**
299
- * Get formatted status text for a tmux window
326
+ * Get formatted status info for a tmux window
300
327
  */
301
328
  export function getWindowStatus(window) {
302
- const isClaudeSession = window.name.includes('claude') && window.paneCurrentCommand === 'node';
329
+ const isClaudeSession = window.type === 'claude';
303
330
  const relativeTime = formatRelativeTime(window.windowActivity);
331
+ // Check for signal file first (written by Claude hooks)
332
+ if (isClaudeSession && window.paneId) {
333
+ const signal = readSignal(window.paneId);
334
+ if (signal) {
335
+ const icon = getStatusIcon(signal.status);
336
+ const text = signal.status === 'waiting'
337
+ ? 'Waiting for input'
338
+ : signal.status === 'working'
339
+ ? window.paneTitle || 'Working...'
340
+ : signal.status === 'error'
341
+ ? 'Error'
342
+ : `idle ${relativeTime}`;
343
+ return { text: `${icon} ${text}`, status: signal.status, icon };
344
+ }
345
+ }
346
+ // Fallback to tmux-native detection for Claude sessions
304
347
  if (isClaudeSession) {
305
- // For Claude sessions, use pane title (contains task context)
306
348
  if (window.paneTitle) {
307
349
  const working = isClaudeWorking(window.paneTitle);
308
350
  if (working) {
309
- return window.paneTitle; // Shows spinner + task description
351
+ return {
352
+ text: window.paneTitle,
353
+ status: 'working',
354
+ icon: getStatusIcon('working'),
355
+ };
310
356
  }
311
- // Not working - show title with idle time if stale
312
- if (relativeTime !== 'now' && relativeTime !== '1m') {
313
- return `${window.paneTitle} (${relativeTime})`;
314
- }
315
- return window.paneTitle;
357
+ // Not working - likely idle
358
+ const text = relativeTime !== 'now' && relativeTime !== '1m'
359
+ ? `${window.paneTitle} (${relativeTime})`
360
+ : window.paneTitle;
361
+ return { text, status: 'idle', icon: getStatusIcon('idle') };
316
362
  }
317
- return relativeTime === 'now' ? 'active' : `idle ${relativeTime}`;
363
+ const text = relativeTime === 'now' ? 'active' : `idle ${relativeTime}`;
364
+ return { text: `○ ${text}`, status: 'idle', icon: '○' };
365
+ }
366
+ // For shell sessions, check if pane is dead
367
+ if (window.paneCurrentCommand === '') {
368
+ return { text: '✖ disconnected', status: 'error', icon: '✖' };
318
369
  }
319
370
  // For shell sessions, show current command
320
371
  const cmd = window.paneCurrentCommand;
321
372
  if (cmd && cmd !== 'zsh' && cmd !== 'bash' && cmd !== 'sh' && cmd !== 'fish') {
322
- // Running a specific command
323
- return cmd;
373
+ return { text: cmd, status: 'working', icon: getStatusIcon('working') };
324
374
  }
325
375
  // At shell prompt - show idle time
326
- return relativeTime === 'now' ? 'ready' : `idle ${relativeTime}`;
376
+ const text = relativeTime === 'now' ? 'ready' : `idle ${relativeTime}`;
377
+ return { text: `○ ${text}`, status: 'idle', icon: '○' };
327
378
  }
328
379
  export function updateStatusBar(projectName) {
329
380
  const left = projectName
@@ -23,8 +23,9 @@
23
23
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
24
24
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
25
25
  import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
26
- import { loadProjects, loadProject, loadBarns, loadBarn, saveProject, saveBarn, deleteProject, deleteBarn, getLivestockForBarn, ensureConfigDirs, } from './lib/config.js';
26
+ import { loadProjects, loadProject, loadBarns, loadBarn, saveProject, saveBarn, deleteProject, deleteBarn, getLivestockForBarn, ensureConfigDirs, addCritterToBarn, removeCritterFromBarn, getCritter, } from './lib/config.js';
27
27
  import { readLivestockLogs, readLivestockEnv } from './lib/livestock.js';
28
+ import { readCritterLogs, discoverCritters } from './lib/critters.js';
28
29
  import { requireString, optionalString, optionalNumber, optionalBoolean, } from './lib/mcp-validation.js';
29
30
  const server = new Server({
30
31
  name: 'yeehaw',
@@ -381,6 +382,60 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
381
382
  required: ['project', 'title'],
382
383
  },
383
384
  },
385
+ // Critter tools
386
+ {
387
+ name: 'add_critter',
388
+ description: 'Add a critter (system service like mysql, redis, nginx) to a barn',
389
+ inputSchema: {
390
+ type: 'object',
391
+ properties: {
392
+ barn: { type: 'string', description: 'Barn name' },
393
+ name: { type: 'string', description: 'Critter name (user-friendly, e.g., "mysql", "redis-cache")' },
394
+ service: { type: 'string', description: 'systemd service name (e.g., "mysql.service")' },
395
+ config_path: { type: 'string', description: 'Path to config file (optional)' },
396
+ log_path: { type: 'string', description: 'Custom log path if not using journald (optional)' },
397
+ use_journald: { type: 'boolean', description: 'Use journalctl for logs (default: true)' },
398
+ },
399
+ required: ['barn', 'name', 'service'],
400
+ },
401
+ },
402
+ {
403
+ name: 'remove_critter',
404
+ description: 'Remove a critter from a barn',
405
+ inputSchema: {
406
+ type: 'object',
407
+ properties: {
408
+ barn: { type: 'string', description: 'Barn name' },
409
+ name: { type: 'string', description: 'Critter name to remove' },
410
+ },
411
+ required: ['barn', 'name'],
412
+ },
413
+ },
414
+ {
415
+ name: 'read_critter_logs',
416
+ description: 'Read logs from a critter (via journald or custom path)',
417
+ inputSchema: {
418
+ type: 'object',
419
+ properties: {
420
+ barn: { type: 'string', description: 'Barn name' },
421
+ critter: { type: 'string', description: 'Critter name' },
422
+ lines: { type: 'number', description: 'Last N lines (default: 100)' },
423
+ pattern: { type: 'string', description: 'Grep pattern to filter logs (case-insensitive)' },
424
+ },
425
+ required: ['barn', 'critter'],
426
+ },
427
+ },
428
+ {
429
+ name: 'discover_critters',
430
+ description: 'Scan a barn for running services and return suggestions for critters to add',
431
+ inputSchema: {
432
+ type: 'object',
433
+ properties: {
434
+ barn: { type: 'string', description: 'Barn name to scan' },
435
+ },
436
+ required: ['barn'],
437
+ },
438
+ },
384
439
  ],
385
440
  };
386
441
  });
@@ -851,6 +906,111 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
851
906
  content: [{ type: 'text', text: `Deleted wiki section '${title}' from project '${project.name}'` }],
852
907
  };
853
908
  }
909
+ // Critter operations
910
+ case 'add_critter': {
911
+ const barnName = requireString(args, 'barn');
912
+ const critterName = requireString(args, 'name');
913
+ const service = requireString(args, 'service');
914
+ const config_path = optionalString(args, 'config_path');
915
+ const log_path = optionalString(args, 'log_path');
916
+ const use_journald = optionalBoolean(args, 'use_journald', true);
917
+ const critter = {
918
+ name: critterName,
919
+ service,
920
+ config_path,
921
+ log_path,
922
+ use_journald,
923
+ };
924
+ try {
925
+ addCritterToBarn(barnName, critter);
926
+ return {
927
+ content: [{ type: 'text', text: `Added critter '${critterName}' (${service}) to barn '${barnName}'` }],
928
+ };
929
+ }
930
+ catch (err) {
931
+ return {
932
+ content: [{ type: 'text', text: err instanceof Error ? err.message : String(err) }],
933
+ isError: true,
934
+ };
935
+ }
936
+ }
937
+ case 'remove_critter': {
938
+ const barnName = requireString(args, 'barn');
939
+ const critterName = requireString(args, 'name');
940
+ try {
941
+ const removed = removeCritterFromBarn(barnName, critterName);
942
+ if (!removed) {
943
+ return {
944
+ content: [{ type: 'text', text: `Critter '${critterName}' not found on barn '${barnName}'` }],
945
+ isError: true,
946
+ };
947
+ }
948
+ return {
949
+ content: [{ type: 'text', text: `Removed critter '${critterName}' from barn '${barnName}'` }],
950
+ };
951
+ }
952
+ catch (err) {
953
+ return {
954
+ content: [{ type: 'text', text: err instanceof Error ? err.message : String(err) }],
955
+ isError: true,
956
+ };
957
+ }
958
+ }
959
+ case 'read_critter_logs': {
960
+ const barnName = requireString(args, 'barn');
961
+ const critterName = requireString(args, 'critter');
962
+ const lines = optionalNumber(args, 'lines');
963
+ const pattern = optionalString(args, 'pattern');
964
+ const barn = loadBarn(barnName);
965
+ if (!barn) {
966
+ return {
967
+ content: [{ type: 'text', text: `Barn not found: ${barnName}` }],
968
+ isError: true,
969
+ };
970
+ }
971
+ const critter = getCritter(barnName, critterName);
972
+ if (!critter) {
973
+ return {
974
+ content: [{ type: 'text', text: `Critter '${critterName}' not found on barn '${barnName}'` }],
975
+ isError: true,
976
+ };
977
+ }
978
+ const result = await readCritterLogs(critter, barn, { lines, pattern });
979
+ if (result.error) {
980
+ return {
981
+ content: [{ type: 'text', text: result.error }],
982
+ isError: true,
983
+ };
984
+ }
985
+ return {
986
+ content: [{ type: 'text', text: result.content }],
987
+ };
988
+ }
989
+ case 'discover_critters': {
990
+ const barnName = requireString(args, 'barn');
991
+ const barn = loadBarn(barnName);
992
+ if (!barn) {
993
+ return {
994
+ content: [{ type: 'text', text: `Barn not found: ${barnName}` }],
995
+ isError: true,
996
+ };
997
+ }
998
+ const result = await discoverCritters(barn);
999
+ if (result.error) {
1000
+ return {
1001
+ content: [{ type: 'text', text: result.error }],
1002
+ isError: true,
1003
+ };
1004
+ }
1005
+ if (result.critters.length === 0) {
1006
+ return {
1007
+ content: [{ type: 'text', text: 'No interesting services discovered on this barn' }],
1008
+ };
1009
+ }
1010
+ return {
1011
+ content: [{ type: 'text', text: JSON.stringify(result.critters, null, 2) }],
1012
+ };
1013
+ }
854
1014
  default:
855
1015
  return {
856
1016
  content: [{ type: 'text', text: `Unknown tool: ${name}` }],
package/dist/types.d.ts CHANGED
@@ -40,8 +40,10 @@ export interface Livestock {
40
40
  }
41
41
  export interface Critter {
42
42
  name: string;
43
- type: string;
44
- status?: 'running' | 'stopped' | 'unknown';
43
+ service: string;
44
+ config_path?: string;
45
+ log_path?: string;
46
+ use_journald?: boolean;
45
47
  }
46
48
  export interface Barn {
47
49
  name: string;
@@ -1,4 +1,4 @@
1
- import type { Barn, Project, Livestock } from '../types.js';
1
+ import type { Barn, Project, Livestock, Critter } from '../types.js';
2
2
  import type { TmuxWindow } from '../lib/tmux.js';
3
3
  interface LivestockWithProject {
4
4
  project: Project;
@@ -17,6 +17,8 @@ interface BarnContextProps {
17
17
  onDeleteBarn: (barnName: string) => void;
18
18
  onAddLivestock: (project: Project, livestock: Livestock) => void;
19
19
  onRemoveLivestock: (project: Project, livestockName: string) => void;
20
+ onAddCritter: (critter: Critter) => void;
21
+ onRemoveCritter: (critterName: string) => void;
20
22
  }
21
- export declare function BarnContext({ barn, livestock, projects, windows, onBack, onSshToBarn, onSelectLivestock, onOpenLivestockSession, onUpdateBarn, onDeleteBarn, onAddLivestock, onRemoveLivestock, }: BarnContextProps): import("react/jsx-runtime").JSX.Element;
23
+ export declare function BarnContext({ barn, livestock, projects, windows, onBack, onSshToBarn, onSelectLivestock, onOpenLivestockSession, onUpdateBarn, onDeleteBarn, onAddLivestock, onRemoveLivestock, onAddCritter, onRemoveCritter, }: BarnContextProps): import("react/jsx-runtime").JSX.Element;
22
24
  export {};
@@ -9,7 +9,7 @@ import { PathInput } from '../components/PathInput.js';
9
9
  import { detectRemoteGitInfo, detectGitInfo } from '../lib/git.js';
10
10
  import { isLocalBarn } from '../lib/config.js';
11
11
  import { parseGitHubUrl } from '../lib/github.js';
12
- export function BarnContext({ barn, livestock, projects, windows, onBack, onSshToBarn, onSelectLivestock, onOpenLivestockSession, onUpdateBarn, onDeleteBarn, onAddLivestock, onRemoveLivestock, }) {
12
+ export function BarnContext({ barn, livestock, projects, windows, onBack, onSshToBarn, onSelectLivestock, onOpenLivestockSession, onUpdateBarn, onDeleteBarn, onAddLivestock, onRemoveLivestock, onAddCritter, onRemoveCritter, }) {
13
13
  const [focusedPanel, setFocusedPanel] = useState('livestock');
14
14
  const [mode, setMode] = useState('normal');
15
15
  // Check if this is the local barn
@@ -28,6 +28,11 @@ export function BarnContext({ barn, livestock, projects, windows, onBack, onSshT
28
28
  // Delete livestock state
29
29
  const [selectedLivestockIndex, setSelectedLivestockIndex] = useState(0);
30
30
  const [deleteLivestockTarget, setDeleteLivestockTarget] = useState(null);
31
+ // Critter state
32
+ const [selectedCritterIndex, setSelectedCritterIndex] = useState(0);
33
+ const [newCritterName, setNewCritterName] = useState('');
34
+ const [newCritterService, setNewCritterService] = useState('');
35
+ const [deleteCritterTarget, setDeleteCritterTarget] = useState(null);
31
36
  // Filter windows that are barn sessions
32
37
  const barnWindows = windows.filter((w) => w.index > 0 && w.name.startsWith(`barn-${barn.name}`));
33
38
  const startEdit = () => {
@@ -80,24 +85,26 @@ export function BarnContext({ barn, livestock, projects, windows, onBack, onSshT
80
85
  }
81
86
  return;
82
87
  }
88
+ // Handle delete critter confirmation
89
+ if (mode === 'delete-critter-confirm' && deleteCritterTarget) {
90
+ if (input === 'y') {
91
+ onRemoveCritter(deleteCritterTarget.name);
92
+ setDeleteCritterTarget(null);
93
+ setSelectedCritterIndex(Math.max(0, selectedCritterIndex - 1));
94
+ setMode('normal');
95
+ }
96
+ else if (input === 'n') {
97
+ setDeleteCritterTarget(null);
98
+ setMode('normal');
99
+ }
100
+ return;
101
+ }
83
102
  if (mode !== 'normal')
84
103
  return;
85
104
  if (key.tab) {
86
105
  setFocusedPanel((p) => (p === 'livestock' ? 'critters' : 'livestock'));
87
106
  return;
88
107
  }
89
- if (input === 's') {
90
- // Context-aware shell: livestock session if focused on livestock, otherwise SSH to barn
91
- if (focusedPanel === 'livestock' && livestock.length > 0) {
92
- const target = livestock[selectedLivestockIndex];
93
- if (target) {
94
- onOpenLivestockSession(target.project, target.livestock);
95
- return;
96
- }
97
- }
98
- onSshToBarn();
99
- return;
100
- }
101
108
  if (input === 'e' && !isLocal) {
102
109
  startEdit();
103
110
  return;
@@ -125,6 +132,23 @@ export function BarnContext({ barn, livestock, projects, windows, onBack, onSshT
125
132
  }
126
133
  return;
127
134
  }
135
+ if (input === 'n' && focusedPanel === 'critters') {
136
+ // Start add critter flow
137
+ setNewCritterName('');
138
+ setNewCritterService('');
139
+ setMode('add-critter-name');
140
+ return;
141
+ }
142
+ if (input === 'd' && focusedPanel === 'critters' && (barn.critters || []).length > 0) {
143
+ // Delete selected critter
144
+ const critters = barn.critters || [];
145
+ const target = critters[selectedCritterIndex];
146
+ if (target) {
147
+ setDeleteCritterTarget(target);
148
+ setMode('delete-critter-confirm');
149
+ }
150
+ return;
151
+ }
128
152
  });
129
153
  // Edit mode screens
130
154
  if (mode === 'edit-host') {
@@ -220,29 +244,64 @@ export function BarnContext({ barn, livestock, projects, windows, onBack, onSshT
220
244
  if (mode === 'delete-livestock-confirm' && deleteLivestockTarget) {
221
245
  return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(BarnHeader, { name: barn.name, subtitle: "Remove livestock" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "red", children: "Remove Livestock" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: ["Remove \"", deleteLivestockTarget.livestock.name, "\" from ", deleteLivestockTarget.project.name, "?"] }) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Path: ", deleteLivestockTarget.livestock.path] }) }), _jsxs(Box, { marginTop: 1, gap: 2, children: [_jsx(Text, { color: "red", bold: true, children: "[y] Yes, remove" }), _jsx(Text, { dimColor: true, children: "[n/Esc] Cancel" })] })] })] }));
222
246
  }
247
+ // Add critter flow: name → service
248
+ if (mode === 'add-critter-name') {
249
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(BarnHeader, { name: barn.name, subtitle: "Adding critter" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsxs(Text, { bold: true, color: "green", children: ["Add Critter to ", barn.name] }), _jsx(Text, { dimColor: true, children: "A critter is a system service like mysql, redis, nginx, etc." }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Name: " }), _jsx(TextInput, { value: newCritterName, onChange: setNewCritterName, onSubmit: () => {
250
+ if (newCritterName.trim()) {
251
+ // Auto-suggest service name
252
+ setNewCritterService(`${newCritterName.trim()}.service`);
253
+ setMode('add-critter-service');
254
+ }
255
+ }, placeholder: "mysql, redis, nginx..." })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next, Esc: cancel" }) })] })] }));
256
+ }
257
+ if (mode === 'add-critter-service') {
258
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(BarnHeader, { name: barn.name, subtitle: "Adding critter" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsxs(Text, { bold: true, color: "green", children: ["Add Critter: ", newCritterName] }), _jsx(Text, { dimColor: true, children: "Enter the systemd service name" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Service: " }), _jsx(TextInput, { value: newCritterService, onChange: setNewCritterService, onSubmit: () => {
259
+ if (newCritterService.trim()) {
260
+ const critter = {
261
+ name: newCritterName.trim(),
262
+ service: newCritterService.trim(),
263
+ use_journald: true,
264
+ };
265
+ onAddCritter(critter);
266
+ setMode('normal');
267
+ }
268
+ }, placeholder: "mysql.service, redis-server.service..." })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: save, Esc: cancel" }) })] })] }));
269
+ }
270
+ // Delete critter confirmation
271
+ if (mode === 'delete-critter-confirm' && deleteCritterTarget) {
272
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(BarnHeader, { name: barn.name, subtitle: "Remove critter" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "red", children: "Remove Critter" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { children: ["Remove \"", deleteCritterTarget.name, "\" from ", barn.name, "?"] }) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Service: ", deleteCritterTarget.service] }) }), _jsxs(Box, { marginTop: 1, gap: 2, children: [_jsx(Text, { color: "red", bold: true, children: "[y] Yes, remove" }), _jsx(Text, { dimColor: true, children: "[n/Esc] Cancel" })] })] })] }));
273
+ }
223
274
  // Build livestock items
224
275
  const livestockItems = livestock.map((l) => ({
225
276
  id: `${l.project.name}/${l.livestock.name}`,
226
277
  label: `${l.project.name}/${l.livestock.name}`,
227
278
  status: 'active',
228
279
  meta: l.livestock.path,
280
+ actions: [{ key: 's', label: 'shell' }],
229
281
  }));
230
282
  // Build critter items
231
283
  const critterItems = (barn.critters || []).map((c) => ({
232
284
  id: c.name,
233
285
  label: c.name,
234
- status: c.status === 'running' ? 'active' : c.status === 'stopped' ? 'inactive' : 'inactive',
235
- meta: c.type,
286
+ status: 'active', // Critters are assumed active (discovery only finds running services)
287
+ meta: c.service,
236
288
  }));
237
289
  // Panel-specific hints (page-level hotkeys like s are in BottomBar)
238
- const livestockHints = '[s] shell [n] new [d] delete';
239
- const crittersHints = '';
290
+ const livestockHints = '[n] new [d] delete';
291
+ const crittersHints = '[n] new [d] delete';
240
292
  return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(BarnHeader, { name: barn.name, subtitle: isLocal ? 'Local machine' : `${barn.user}@${barn.host}:${barn.port}` }), !isLocal && (_jsx(Box, { paddingX: 2, marginY: 1, children: _jsxs(Box, { gap: 4, children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Host:" }), " ", barn.host] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "User:" }), " ", barn.user] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Port:" }), " ", barn.port] }), _jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Key:" }), " ", barn.identity_file] })] }) })), _jsxs(Box, { flexGrow: 1, marginY: 1, paddingX: 1, gap: 2, children: [_jsx(Panel, { title: "Livestock", focused: focusedPanel === 'livestock', width: "50%", hints: livestockHints, children: livestockItems.length > 0 ? (_jsx(List, { items: livestockItems, focused: focusedPanel === 'livestock', selectedIndex: selectedLivestockIndex, onSelectionChange: setSelectedLivestockIndex, onSelect: (item) => {
241
293
  const found = livestock.find((l) => `${l.project.name}/${l.livestock.name}` === item.id);
242
294
  if (found) {
243
295
  onSelectLivestock(found.project, found.livestock);
244
296
  }
245
- } })) : (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { dimColor: true, children: "No livestock deployed to this barn" }) })) }), _jsx(Panel, { title: "Critters", focused: focusedPanel === 'critters', width: "50%", hints: crittersHints, children: critterItems.length > 0 ? (_jsx(List, { items: critterItems, focused: focusedPanel === 'critters', onSelect: () => {
246
- // TODO: Could show critter details or manage service
247
- } })) : (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "No critters configured" }), _jsx(Text, { dimColor: true, italic: true, children: "Critters are system services (nginx, mysql, etc.)" })] })) })] })] }));
297
+ }, onAction: (item, actionKey) => {
298
+ if (actionKey === 's') {
299
+ const found = livestock.find((l) => `${l.project.name}/${l.livestock.name}` === item.id);
300
+ if (found) {
301
+ onOpenLivestockSession(found.project, found.livestock);
302
+ }
303
+ }
304
+ } })) : (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { dimColor: true, children: "No livestock deployed to this barn" }) })) }), _jsx(Panel, { title: "Critters", focused: focusedPanel === 'critters', width: "50%", hints: crittersHints, children: critterItems.length > 0 ? (_jsx(List, { items: critterItems, focused: focusedPanel === 'critters', selectedIndex: selectedCritterIndex, onSelectionChange: setSelectedCritterIndex, onSelect: () => {
305
+ // Could show critter details or logs in future
306
+ } })) : (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "No critters configured" }), _jsx(Text, { dimColor: true, italic: true, children: "Press [n] to add a critter" })] })) })] })] }));
248
307
  }
@@ -11,11 +11,11 @@ interface GlobalDashboardProps {
11
11
  onSelectProject: (project: Project) => void;
12
12
  onSelectBarn: (barn: Barn) => void;
13
13
  onSelectWindow: (window: TmuxWindow) => void;
14
- onNewClaude: () => void;
14
+ onNewClaudeForProject: (project: Project) => void;
15
15
  onCreateProject: (name: string, path: string) => void;
16
16
  onCreateBarn: (barn: Barn) => void;
17
17
  onSshToBarn: (barn: Barn) => void;
18
18
  onInputModeChange?: (isInputMode: boolean) => void;
19
19
  }
20
- export declare function GlobalDashboard({ projects, barns, windows, versionInfo, onSelectProject, onSelectBarn, onSelectWindow, onNewClaude, onCreateProject, onCreateBarn, onSshToBarn, onInputModeChange, }: GlobalDashboardProps): import("react/jsx-runtime").JSX.Element;
20
+ export declare function GlobalDashboard({ projects, barns, windows, versionInfo, onSelectProject, onSelectBarn, onSelectWindow, onNewClaudeForProject, onCreateProject, onCreateBarn, onSshToBarn, onInputModeChange, }: GlobalDashboardProps): import("react/jsx-runtime").JSX.Element;
21
21
  export {};