@colmbus72/yeehaw 0.1.0 → 0.3.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/app.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState, useCallback } from 'react';
2
+ import { useState, useCallback, useMemo, useEffect } from 'react';
3
3
  import { Box, Text, useApp, useInput, useStdout } from 'ink';
4
4
  import { homedir } from 'os';
5
5
  import { join } from 'path';
@@ -16,8 +16,9 @@ import { NightSkyView } from './views/NightSkyView.js';
16
16
  import { useConfig } from './hooks/useConfig.js';
17
17
  import { useSessions } from './hooks/useSessions.js';
18
18
  import { useRemoteYeehaw } from './hooks/useRemoteYeehaw.js';
19
- import { hasTmux, switchToWindow, updateStatusBar, createShellWindow, createSshWindow, detachFromSession, killYeehawSession, enterRemoteMode, } from './lib/tmux.js';
19
+ import { hasTmux, switchToWindow, updateStatusBar, createShellWindow, createSshWindow, detachFromSession, killYeehawSession, enterRemoteMode, ensureCorrectStatusBar, } from './lib/tmux.js';
20
20
  import { saveProject, deleteProject, saveBarn, deleteBarn, getLivestockForBarn, isLocalBarn, hasValidSshConfig } from './lib/config.js';
21
+ import { getVersionInfo } from './lib/update-check.js';
21
22
  function getHotkeyScope(view) {
22
23
  switch (view.type) {
23
24
  case 'global': return 'global-dashboard';
@@ -47,27 +48,27 @@ function getBottomBarItems(viewType) {
47
48
  return [...common, { key: 'c', label: 'claude' }, { key: 'q', label: 'detach' }, { key: 'Q', label: 'quit' }];
48
49
  }
49
50
  if (viewType === 'project') {
50
- return [...common, { key: 'c', label: 'claude' }, { key: 'w', label: 'wiki' }, { key: 'i', label: 'issues' }, { key: 'q', label: 'back' }];
51
+ return [...common, { key: 'c', label: 'claude' }, { key: 'w', label: 'wiki' }, { key: 'i', label: 'issues' }, { key: 'Esc', label: 'back' }];
51
52
  }
52
53
  if (viewType === 'barn') {
53
- return [...common, { key: 's', label: 'shell' }, { key: 'q', label: 'back' }];
54
+ return [...common, { key: 's', label: 'shell' }, { key: 'Esc', label: 'back' }];
54
55
  }
55
56
  if (viewType === 'wiki') {
56
- return [...common, { key: 'q', label: 'back' }];
57
+ return [...common, { key: 'Esc', label: 'back' }];
57
58
  }
58
59
  if (viewType === 'issues') {
59
- return [...common, { key: 'r', label: 'refresh' }, { key: 'o', label: 'open' }, { key: 'q', label: 'back' }];
60
+ return [...common, { key: 'r', label: 'refresh' }, { key: 'o', label: 'open' }, { key: 'Esc', label: 'back' }];
60
61
  }
61
62
  if (viewType === 'livestock') {
62
- return [...common, { key: 's', label: 'shell' }, { key: 'l', label: 'logs' }, { key: 'e', label: 'edit' }, { key: 'q', label: 'back' }];
63
+ return [...common, { key: 's', label: 'shell' }, { key: 'l', label: 'logs' }, { key: 'e', label: 'edit' }, { key: 'Esc', label: 'back' }];
63
64
  }
64
65
  if (viewType === 'logs') {
65
- return [...common, { key: 'r', label: 'refresh' }, { key: 'q', label: 'back' }];
66
+ return [...common, { key: 'r', label: 'refresh' }, { key: 'Esc', label: 'back' }];
66
67
  }
67
68
  if (viewType === 'night-sky') {
68
69
  return [{ key: 'c', label: 'cloud' }, { key: 'r', label: 'randomize' }, { key: 'Esc', label: 'exit' }];
69
70
  }
70
- return [...common, { key: 'q', label: 'back' }];
71
+ return [...common, { key: 'Esc', label: 'back' }];
71
72
  }
72
73
  export function App() {
73
74
  const { exit } = useApp();
@@ -84,6 +85,14 @@ export function App() {
84
85
  const terminalHeight = stdout?.rows || 24;
85
86
  // Check tmux availability
86
87
  const tmuxAvailable = hasTmux();
88
+ // Get version info (cached, so safe to call synchronously)
89
+ const versionInfo = useMemo(() => getVersionInfo(), []);
90
+ // Ensure status bar is hidden when on global dashboard
91
+ useEffect(() => {
92
+ if (view.type === 'global') {
93
+ ensureCorrectStatusBar();
94
+ }
95
+ }, [view.type]);
87
96
  const handleSelectProject = useCallback((project) => {
88
97
  setView({ type: 'project', project });
89
98
  updateStatusBar(project.name);
@@ -164,27 +173,33 @@ export function App() {
164
173
  setView({ type: 'project', project });
165
174
  updateStatusBar(project.name);
166
175
  }, []);
167
- const handleOpenLivestockDetail = useCallback((project, livestock) => {
168
- setView({ type: 'livestock', project, livestock });
176
+ const handleOpenLivestockDetail = useCallback((project, livestock, source, sourceBarn) => {
177
+ setView({ type: 'livestock', project, livestock, source, sourceBarn });
169
178
  updateStatusBar(`${project.name} / ${livestock.name}`);
170
179
  }, []);
171
- const handleOpenLogs = useCallback((project, livestock) => {
172
- setView({ type: 'logs', project, livestock });
180
+ const handleOpenLogs = useCallback((project, livestock, source, sourceBarn) => {
181
+ setView({ type: 'logs', project, livestock, source, sourceBarn });
173
182
  updateStatusBar(`${project.name} / ${livestock.name} Logs`);
174
183
  }, []);
175
- const handleBackFromLivestock = useCallback((project) => {
176
- setView({ type: 'project', project });
177
- updateStatusBar(project.name);
184
+ const handleBackFromLivestock = useCallback((source, project, sourceBarn) => {
185
+ if (source === 'barn' && sourceBarn) {
186
+ setView({ type: 'barn', barn: sourceBarn });
187
+ updateStatusBar(`Barn: ${sourceBarn.name}`);
188
+ }
189
+ else {
190
+ setView({ type: 'project', project });
191
+ updateStatusBar(project.name);
192
+ }
178
193
  }, []);
179
- const handleUpdateLivestock = useCallback((project, updatedLivestock) => {
194
+ const handleUpdateLivestock = useCallback((project, updatedLivestock, source, sourceBarn) => {
180
195
  const updatedProject = {
181
196
  ...project,
182
197
  livestock: (project.livestock || []).map((l) => l.name === updatedLivestock.name ? updatedLivestock : l),
183
198
  };
184
199
  saveProject(updatedProject);
185
200
  reload();
186
- // Update the view with the new livestock data
187
- setView({ type: 'livestock', project: updatedProject, livestock: updatedLivestock });
201
+ // Update the view with the new livestock data, preserving navigation context
202
+ setView({ type: 'livestock', project: updatedProject, livestock: updatedLivestock, source, sourceBarn });
188
203
  }, [reload]);
189
204
  const handleDeleteProject = useCallback((projectName) => {
190
205
  deleteProject(projectName);
@@ -288,46 +303,29 @@ export function App() {
288
303
  setShowHelp(false);
289
304
  return;
290
305
  }
291
- // Global shortcuts
292
- if (input === 'q') {
293
- if (view.type === 'wiki' || view.type === 'issues') {
294
- handleBackFromSubview(view.project);
295
- }
296
- else if (view.type === 'logs') {
297
- // Back to livestock detail
298
- setView({ type: 'livestock', project: view.project, livestock: view.livestock });
299
- updateStatusBar(`${view.project.name} / ${view.livestock.name}`);
300
- }
301
- else if (view.type === 'livestock') {
302
- handleBackFromLivestock(view.project);
303
- }
304
- else if (view.type === 'project' || view.type === 'barn') {
305
- handleBack();
306
- }
307
- else {
308
- // Detach from session (keeps yeehaw running in background)
309
- // Don't call exit() - let the TUI keep running so reattach works
310
- detachFromSession();
311
- }
306
+ // q: Detach from session (only on global dashboard)
307
+ if (input === 'q' && view.type === 'global') {
308
+ detachFromSession();
312
309
  return;
313
310
  }
314
- // Shift-Q: Kill everything
311
+ // Shift-Q: Kill everything (only on global dashboard)
315
312
  if (input === 'Q' && view.type === 'global') {
316
313
  killYeehawSession();
317
314
  exit();
318
315
  return;
319
316
  }
317
+ // ESC: Navigate back (handled by individual views for their sub-modes,
318
+ // but also handled here as a fallback for consistent navigation)
320
319
  if (key.escape) {
321
320
  if (view.type === 'wiki' || view.type === 'issues') {
322
321
  handleBackFromSubview(view.project);
323
322
  }
324
323
  else if (view.type === 'logs') {
325
- // Back to livestock detail
326
- setView({ type: 'livestock', project: view.project, livestock: view.livestock });
324
+ setView({ type: 'livestock', project: view.project, livestock: view.livestock, source: view.source, sourceBarn: view.sourceBarn });
327
325
  updateStatusBar(`${view.project.name} / ${view.livestock.name}`);
328
326
  }
329
327
  else if (view.type === 'livestock') {
330
- handleBackFromLivestock(view.project);
328
+ handleBackFromLivestock(view.source, view.project, view.sourceBarn);
331
329
  }
332
330
  else if (view.type === 'project' || view.type === 'barn') {
333
331
  handleBack();
@@ -364,12 +362,12 @@ export function App() {
364
362
  }
365
363
  switch (view.type) {
366
364
  case 'global':
367
- return (_jsx(GlobalDashboard, { projects: projects, barns: barns, windows: windows, onSelectProject: handleSelectProject, onSelectBarn: handleSelectBarn, onSelectWindow: handleSelectWindow, onNewClaude: handleNewClaude, onCreateProject: handleCreateProject, onCreateBarn: handleCreateBarn, onSshToBarn: handleSshToBarn }));
365
+ return (_jsx(GlobalDashboard, { projects: projects, barns: barns, windows: windows, versionInfo: versionInfo, onSelectProject: handleSelectProject, onSelectBarn: handleSelectBarn, onSelectWindow: handleSelectWindow, onNewClaude: handleNewClaude, onCreateProject: handleCreateProject, onCreateBarn: handleCreateBarn, onSshToBarn: handleSshToBarn }));
368
366
  case 'project':
369
- return (_jsx(ProjectContext, { project: view.project, barns: barns, windows: windows, onBack: handleBack, onNewClaude: handleNewClaude, onSelectWindow: handleSelectWindow, onSelectLivestock: (livestock, barn) => handleOpenLivestockDetail(view.project, livestock), onOpenLivestockSession: (livestock, barn) => handleOpenLivestockSession(livestock, barn, view.project.name), onUpdateProject: handleUpdateProject, onDeleteProject: handleDeleteProject, onOpenWiki: () => handleOpenWiki(view.project), onOpenIssues: () => handleOpenIssues(view.project) }));
367
+ return (_jsx(ProjectContext, { project: view.project, barns: barns, windows: windows, onBack: handleBack, onNewClaude: handleNewClaude, onSelectWindow: handleSelectWindow, onSelectLivestock: (livestock, barn) => handleOpenLivestockDetail(view.project, livestock, 'project'), onOpenLivestockSession: (livestock, barn) => handleOpenLivestockSession(livestock, barn, view.project.name), onUpdateProject: handleUpdateProject, onDeleteProject: handleDeleteProject, onOpenWiki: () => handleOpenWiki(view.project), onOpenIssues: () => handleOpenIssues(view.project) }));
370
368
  case 'barn':
371
369
  const barnLivestock = getLivestockForBarn(view.barn.name);
372
- return (_jsx(BarnContext, { barn: view.barn, livestock: barnLivestock, projects: projects, windows: windows, onBack: handleBack, onSshToBarn: () => handleSshToBarn(view.barn), onSelectLivestock: (project, livestock) => handleOpenLivestockDetail(project, livestock), onOpenLivestockSession: (project, livestock) => {
370
+ return (_jsx(BarnContext, { barn: view.barn, livestock: barnLivestock, projects: projects, windows: windows, onBack: handleBack, onSshToBarn: () => handleSshToBarn(view.barn), onSelectLivestock: (project, livestock) => handleOpenLivestockDetail(project, livestock, 'barn', view.barn), onOpenLivestockSession: (project, livestock) => {
373
371
  const barn = barns.find((b) => b.name === livestock.barn) || null;
374
372
  handleOpenLivestockSession(livestock, barn, project.name);
375
373
  }, onUpdateBarn: handleUpdateBarn, onDeleteBarn: handleDeleteBarn, onAddLivestock: (project, livestock) => {
@@ -397,13 +395,13 @@ export function App() {
397
395
  case 'issues':
398
396
  return (_jsx(IssuesView, { project: view.project, onBack: () => handleBackFromSubview(view.project) }));
399
397
  case 'livestock':
400
- return (_jsx(LivestockDetailView, { project: view.project, livestock: view.livestock, onBack: () => handleBackFromLivestock(view.project), onOpenLogs: () => handleOpenLogs(view.project, view.livestock), onOpenSession: () => {
398
+ return (_jsx(LivestockDetailView, { project: view.project, livestock: view.livestock, source: view.source, sourceBarn: view.sourceBarn, windows: windows, onBack: () => handleBackFromLivestock(view.source, view.project, view.sourceBarn), onOpenLogs: () => handleOpenLogs(view.project, view.livestock, view.source, view.sourceBarn), onOpenSession: () => {
401
399
  const barn = barns.find((b) => b.name === view.livestock.barn) || null;
402
400
  handleOpenLivestockSession(view.livestock, barn, view.project.name);
403
- }, onUpdateLivestock: (updatedLivestock) => handleUpdateLivestock(view.project, updatedLivestock) }));
401
+ }, onSelectWindow: handleSelectWindow, onUpdateLivestock: (updatedLivestock) => handleUpdateLivestock(view.project, updatedLivestock, view.source, view.sourceBarn) }));
404
402
  case 'logs':
405
403
  return (_jsx(LogsView, { project: view.project, livestock: view.livestock, onBack: () => {
406
- setView({ type: 'livestock', project: view.project, livestock: view.livestock });
404
+ setView({ type: 'livestock', project: view.project, livestock: view.livestock, source: view.source, sourceBarn: view.sourceBarn });
407
405
  updateStatusBar(`${view.project.name} / ${view.livestock.name}`);
408
406
  } }));
409
407
  case 'night-sky':
@@ -3,6 +3,10 @@ interface HeaderProps {
3
3
  subtitle?: string;
4
4
  summary?: string;
5
5
  color?: string;
6
+ versionInfo?: {
7
+ current: string;
8
+ latest: string | null;
9
+ };
6
10
  }
7
- export declare function Header({ text, subtitle, summary, color }: HeaderProps): import("react/jsx-runtime").JSX.Element;
11
+ export declare function Header({ text, subtitle, summary, color, versionInfo }: HeaderProps): import("react/jsx-runtime").JSX.Element;
8
12
  export {};
@@ -63,7 +63,7 @@ const TUMBLEWEED = [
63
63
  ];
64
64
  // Brownish tan color to complement yeehaw gold
65
65
  const TUMBLEWEED_COLOR = '#b8860b';
66
- export function Header({ text, subtitle, summary, color }) {
66
+ export function Header({ text, subtitle, summary, color, versionInfo }) {
67
67
  const [ascii, setAscii] = useState('');
68
68
  useEffect(() => {
69
69
  figlet.text(text.toUpperCase(), { font: 'ANSI Shadow' }, (err, result) => {
@@ -79,5 +79,5 @@ export function Header({ text, subtitle, summary, color }) {
79
79
  const showTumbleweed = text.toLowerCase() === 'yeehaw';
80
80
  // Vertically center tumbleweed next to ASCII art
81
81
  const tumbleweedTopPadding = Math.max(0, Math.floor((lines.length - TUMBLEWEED.length) / 2));
82
- return (_jsxs(Box, { flexDirection: "column", paddingTop: 1, paddingLeft: 2, children: [_jsxs(Box, { flexDirection: "row", children: [showTumbleweed && (_jsxs(Box, { flexDirection: "column", marginRight: 2, children: [Array(tumbleweedTopPadding).fill(null).map((_, i) => (_jsx(Text, { children: " " }, `pad-${i}`))), TUMBLEWEED.map((line, i) => (_jsx(Text, { color: TUMBLEWEED_COLOR, children: line }, `tumbleweed-${i}`)))] })), _jsx(Box, { flexDirection: "column", children: lines.map((line, i) => (_jsx(Text, { color: gradientColors[i], children: line }, i))) })] }), (subtitle || summary) && (_jsxs(Box, { gap: 2, children: [subtitle && _jsx(Text, { dimColor: true, children: subtitle }), summary && _jsxs(Text, { color: "gray", children: ["- ", summary] })] }))] }));
82
+ return (_jsxs(Box, { flexDirection: "column", paddingTop: 1, paddingLeft: 2, children: [_jsxs(Box, { flexDirection: "row", children: [showTumbleweed && (_jsxs(Box, { flexDirection: "column", marginRight: 2, children: [Array(tumbleweedTopPadding).fill(null).map((_, i) => (_jsx(Text, { children: " " }, `pad-${i}`))), TUMBLEWEED.map((line, i) => (_jsx(Text, { color: TUMBLEWEED_COLOR, children: line }, `tumbleweed-${i}`)))] })), _jsx(Box, { flexDirection: "column", children: lines.map((line, i) => (_jsx(Text, { color: gradientColors[i], children: line }, i))) }), versionInfo && showTumbleweed && (_jsx(Box, { marginLeft: 2, paddingRight: 1, alignItems: "flex-end", flexGrow: 1, justifyContent: "flex-end", children: versionInfo.latest && versionInfo.latest !== versionInfo.current ? (_jsxs(Text, { children: [_jsxs(Text, { dimColor: true, children: ["v", versionInfo.current] }), _jsx(Text, { dimColor: true, children: " \u2192 " }), _jsxs(Text, { color: "yellow", children: ["v", versionInfo.latest] })] })) : versionInfo.latest ? (_jsxs(Text, { children: [_jsxs(Text, { dimColor: true, children: ["v", versionInfo.current] }), _jsx(Text, { color: "green", children: " \u2713 latest" })] })) : (_jsxs(Text, { dimColor: true, children: ["v", versionInfo.current] })) }))] }), (subtitle || summary) && (_jsxs(Box, { gap: 2, children: [subtitle && _jsx(Text, { dimColor: true, children: subtitle }), summary && _jsxs(Text, { color: "gray", children: ["- ", summary] })] }))] }));
83
83
  }
@@ -0,0 +1,7 @@
1
+ import type { Project, Livestock } from '../types.js';
2
+ interface LivestockHeaderProps {
3
+ project: Project;
4
+ livestock: Livestock;
5
+ }
6
+ export declare function LivestockHeader({ project, livestock }: LivestockHeaderProps): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -0,0 +1,122 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ // Base cow ASCII art with @ symbols as pattern spots
4
+ // The @ symbols will be replaced with pattern characters for variation
5
+ // const COW_TEMPLATE = [
6
+ // " ___,,,___ _..............._",
7
+ // " '-/~'~\-'` @@@@@' .@@ \\",
8
+ // " |a a | '@@@' `@@ ||",
9
+ // " | /@@. @@@. .@@@. @@||",
10
+ // " (oo/@@' .@@@@..@@@@@@. /|",
11
+ // " `` '._ /@@@ @@@@' <`\/",
12
+ // " | /`--....-'`Y \))",
13
+ // " ||| //'. |((",
14
+ // " ||| // ||",
15
+ // " //( ` /(",
16
+ // ];
17
+ const COW_TEMPLATE = [
18
+ " /; ;\\",
19
+ " __ \\\\____//",
20
+ " /{_\\_/ `'\\____",
21
+ " \\___ (o) (o }",
22
+ " _______________________/ :--'",
23
+ " ,-,'`@@@@@@@@ @@@@ \\_ `__\\",
24
+ " ;:( @@@@@@@@@ @@ \\___(o'o)",
25
+ " :: ) @@@@ @@@ ,'@@( `===='",
26
+ " :: \\ @@@@@: @@@@) ( '@@@'",
27
+ " ;; /\\ /`, @@@@@\\ :@@@@@)",
28
+ " ::/ ) {_----------: :~`,~~;",
29
+ " ;;'`; : ) : / `; ;",
30
+ "`'`' / : : : : : :",
31
+ " )_ \\__; :_ ; \\_\\",
32
+ " :__\\ \\ \\ \\ : \\",
33
+ " `^' `^' `-^-'",
34
+ ];
35
+ // " /; ;\\",
36
+ // " __ \\\\____//",
37
+ // " /{_\\_/ `'\\____",
38
+ // " \\___ (o) (o }",
39
+ // " _____________________________/ :--'",
40
+ // ",-,'`@@@@@@@@@@@@@@@@@@@@@ \\_ `__\\",
41
+ // ";:( @@@@@@@@@@@@@@@@ @@@ @@@@@@@ \\___(o'o)",
42
+ // ":: ) @@@@@@@@@@@ @@@@@@@@@@@@@ ,'@@( `===='",
43
+ // ":: : @@@@@: @@@@@@@ @@@@ @@@@@@@ `@@@:",
44
+ // ":: \\ @@@@@: @@@@@@@) ( '@@@'",
45
+ // ";; /\\ /`, @@@@@@@@@\\ :@@@@@)",
46
+ // "::/ ) {_----------------: :~`,~~;",
47
+ // ";;'`; : ) : / `; ;",
48
+ // ";;;; : : ; : ; ; :",
49
+ // "`'`' / : : : : : :",
50
+ // " )_ \\__; :_ ; \\_\\ `,','",
51
+ // " :__\\ \\ \\ \\ : \\ * 8`;'* *",
52
+ // " `^' `^' `-^-' \\v/ : \\/",
53
+ //];
54
+ // Generate multiple hash values from a string for better distribution
55
+ function multiHash(str) {
56
+ const hashes = [];
57
+ // First hash - djb2
58
+ let hash1 = 5381;
59
+ for (let i = 0; i < str.length; i++) {
60
+ hash1 = ((hash1 << 5) + hash1) ^ str.charCodeAt(i);
61
+ }
62
+ hashes.push(Math.abs(hash1));
63
+ // Second hash - sdbm
64
+ let hash2 = 0;
65
+ for (let i = 0; i < str.length; i++) {
66
+ hash2 = str.charCodeAt(i) + (hash2 << 6) + (hash2 << 16) - hash2;
67
+ }
68
+ hashes.push(Math.abs(hash2));
69
+ // Third hash - fnv-1a inspired
70
+ let hash3 = 2166136261;
71
+ for (let i = 0; i < str.length; i++) {
72
+ hash3 ^= str.charCodeAt(i);
73
+ hash3 = (hash3 * 16777619) >>> 0;
74
+ }
75
+ hashes.push(hash3);
76
+ return hashes;
77
+ }
78
+ // Pattern characters - space and block characters only
79
+ // Space creates gaps in the spots, blocks create varying density
80
+ const PATTERN_CHARS = [' ', ' ', '░', '░', '▒', '▓', '█'];
81
+ // Generate pattern variation for the cow based on livestock/project data
82
+ function generateCowArt(livestock, project) {
83
+ // Use git branch (environment) + livestock name for unique seed
84
+ // Branch typically indicates environment (main, staging, production, etc.)
85
+ const seed = `${livestock.branch || 'default'}-${livestock.name}-${project.name}`;
86
+ const hashes = multiHash(seed);
87
+ // Replace @ symbols with pattern characters
88
+ // Use multiple hashes and position for maximum variety
89
+ let charIndex = 0;
90
+ const cowArt = COW_TEMPLATE.map((line, lineIndex) => {
91
+ let result = '';
92
+ for (const char of line) {
93
+ if (char === '@') {
94
+ // Mix multiple hashes with position data for variety
95
+ const h1 = hashes[0];
96
+ const h2 = hashes[1];
97
+ const h3 = hashes[2];
98
+ // Combine hashes with position in different ways
99
+ const mix = (h1 >> (charIndex % 17)) ^
100
+ (h2 >> ((charIndex + lineIndex) % 13)) ^
101
+ (h3 >> ((charIndex * 7 + lineIndex * 3) % 19));
102
+ const charChoice = Math.abs(mix) % PATTERN_CHARS.length;
103
+ result += PATTERN_CHARS[charChoice];
104
+ charIndex++;
105
+ }
106
+ else {
107
+ result += char;
108
+ }
109
+ }
110
+ return result;
111
+ });
112
+ return cowArt;
113
+ }
114
+ export function LivestockHeader({ project, livestock }) {
115
+ const cowArt = generateCowArt(livestock, project);
116
+ const color = project.color || '#b8860b'; // Use project color or default tan
117
+ // Calculate where to place the livestock name (in the body area)
118
+ // We'll show it to the right of the cow
119
+ const cowHeight = cowArt.length;
120
+ const infoStartLine = Math.floor(cowHeight / 2) - 1;
121
+ return (_jsx(Box, { flexDirection: "column", paddingTop: 1, paddingLeft: 1, children: _jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexDirection: "column", children: cowArt.map((line, i) => (_jsx(Text, { color: color, children: line }, i))) }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, justifyContent: "center", children: [_jsx(Text, { bold: true, color: color, children: livestock.name }), _jsxs(Text, { dimColor: true, children: ["project: ", project.name] }), _jsxs(Text, { dimColor: true, children: ["barn: ", livestock.barn || 'local'] }), livestock.branch && (_jsxs(Text, { dimColor: true, children: ["branch: ", livestock.branch] }))] })] }) }));
122
+ }
@@ -1,11 +1,16 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState, useEffect } from 'react';
2
+ import { useState, useEffect, useRef } from 'react';
3
3
  import { Box, Text, useInput } from 'ink';
4
4
  import { readdirSync, existsSync } from 'fs';
5
5
  import { join, dirname, basename } from 'path';
6
6
  import { homedir } from 'os';
7
7
  import { execaSync } from 'execa';
8
8
  import { hasValidSshConfig } from '../lib/config.js';
9
+ // Cache for remote directory listings: Map<"barnName:dirPath", string[]>
10
+ const remoteCompletionCache = new Map();
11
+ function getCacheKey(barnName, dirPath) {
12
+ return `${barnName}:${dirPath}`;
13
+ }
9
14
  function expandPath(path) {
10
15
  if (path.startsWith('~/')) {
11
16
  return join(homedir(), path.slice(2));
@@ -31,26 +36,22 @@ function getLocalCompletions(partialPath) {
31
36
  return [];
32
37
  }
33
38
  }
34
- function getRemoteCompletions(partialPath, barn) {
35
- if (!partialPath)
36
- return [];
37
- // Verify barn has valid SSH config
39
+ function fetchRemoteDirectoryListing(dir, barn) {
38
40
  if (!hasValidSshConfig(barn)) {
39
- return []; // Can't do remote completion without SSH config
41
+ return [];
40
42
  }
41
- // Handle ~ expansion for display
42
- const dir = partialPath.endsWith('/') ? partialPath : dirname(partialPath) || '~';
43
- const prefix = partialPath.endsWith('/') ? '' : basename(partialPath);
44
43
  try {
45
- // Use SSH to list directories on the remote server
44
+ // Fetch ALL directories in this directory (not filtered by prefix)
45
+ // This allows us to cache the full listing and filter client-side
46
46
  const result = execaSync('ssh', [
47
47
  '-p', String(barn.port),
48
48
  '-i', barn.identity_file,
49
49
  '-o', 'BatchMode=yes',
50
- '-o', 'ConnectTimeout=5',
50
+ '-o', 'ConnectTimeout=3',
51
+ '-o', 'StrictHostKeyChecking=accept-new',
51
52
  `${barn.user}@${barn.host}`,
52
- `ls -1d ${dir}/${prefix}*/ 2>/dev/null | xargs -n1 basename 2>/dev/null || true`
53
- ], { timeout: 10000 });
53
+ `ls -1F ${dir} 2>/dev/null | grep '/$' | sed 's|/$||' || true`
54
+ ], { timeout: 5000 });
54
55
  const output = result.stdout.trim();
55
56
  if (!output)
56
57
  return [];
@@ -60,21 +61,78 @@ function getRemoteCompletions(partialPath, barn) {
60
61
  return [];
61
62
  }
62
63
  }
64
+ function getRemoteCompletions(partialPath, barn) {
65
+ if (!partialPath)
66
+ return [];
67
+ // Verify barn has valid SSH config
68
+ if (!hasValidSshConfig(barn)) {
69
+ return [];
70
+ }
71
+ // Handle ~ expansion for display
72
+ const dir = partialPath.endsWith('/') ? partialPath.slice(0, -1) || '~' : dirname(partialPath) || '~';
73
+ const prefix = partialPath.endsWith('/') ? '' : basename(partialPath);
74
+ const cacheKey = getCacheKey(barn.name, dir);
75
+ // Check cache first
76
+ let allDirs = remoteCompletionCache.get(cacheKey);
77
+ if (!allDirs) {
78
+ // Fetch and cache
79
+ allDirs = fetchRemoteDirectoryListing(dir, barn);
80
+ remoteCompletionCache.set(cacheKey, allDirs);
81
+ }
82
+ // Filter by prefix
83
+ if (prefix) {
84
+ return allDirs.filter((d) => d.startsWith(prefix));
85
+ }
86
+ return allDirs;
87
+ }
88
+ // Pre-fetch a directory's contents in the background
89
+ function prefetchRemoteDirectory(dir, barn) {
90
+ if (!hasValidSshConfig(barn))
91
+ return;
92
+ const cacheKey = getCacheKey(barn.name, dir);
93
+ if (remoteCompletionCache.has(cacheKey))
94
+ return; // Already cached
95
+ // Fetch asynchronously (don't block)
96
+ setTimeout(() => {
97
+ const dirs = fetchRemoteDirectoryListing(dir, barn);
98
+ remoteCompletionCache.set(cacheKey, dirs);
99
+ }, 0);
100
+ }
63
101
  export function PathInput({ value, onChange, onSubmit, barn }) {
64
102
  const [cursorPos, setCursorPos] = useState(value.length);
65
103
  const [completions, setCompletions] = useState([]);
66
104
  const [loading, setLoading] = useState(false);
67
- // Update completions when value changes (debounced for remote)
105
+ const pendingFetch = useRef(null);
106
+ // Update completions when value changes
68
107
  useEffect(() => {
69
108
  if (barn) {
70
- // Remote completion - debounce
71
- setLoading(true);
72
- const timer = setTimeout(() => {
109
+ // Remote completion - check cache first
110
+ const dir = value.endsWith('/') ? value.slice(0, -1) || '~' : dirname(value) || '~';
111
+ const cacheKey = getCacheKey(barn.name, dir);
112
+ const cached = remoteCompletionCache.has(cacheKey);
113
+ if (cached) {
114
+ // Use cached data immediately
73
115
  const results = getRemoteCompletions(value, barn);
74
116
  setCompletions(results);
75
117
  setLoading(false);
76
- }, 300);
77
- return () => clearTimeout(timer);
118
+ }
119
+ else {
120
+ // Not cached - debounce the fetch
121
+ setLoading(true);
122
+ if (pendingFetch.current) {
123
+ clearTimeout(pendingFetch.current);
124
+ }
125
+ pendingFetch.current = setTimeout(() => {
126
+ const results = getRemoteCompletions(value, barn);
127
+ setCompletions(results);
128
+ setLoading(false);
129
+ }, 200);
130
+ }
131
+ return () => {
132
+ if (pendingFetch.current) {
133
+ clearTimeout(pendingFetch.current);
134
+ }
135
+ };
78
136
  }
79
137
  else {
80
138
  // Local completion - immediate
@@ -94,6 +152,10 @@ export function PathInput({ value, onChange, onSubmit, barn }) {
94
152
  const newPath = displayDir + completions[0] + '/';
95
153
  onChange(newPath);
96
154
  setCursorPos(newPath.length);
155
+ // Pre-fetch the subdirectory contents for faster subsequent completions
156
+ if (barn) {
157
+ prefetchRemoteDirectory(newPath.slice(0, -1), barn);
158
+ }
97
159
  }
98
160
  else if (completions.length > 1) {
99
161
  // Multiple matches - find common prefix
package/dist/index.js CHANGED
@@ -5,9 +5,21 @@ import { execaSync } from 'execa';
5
5
  import { App } from './app.js';
6
6
  import { isInsideYeehawSession, yeehawSessionExists, createYeehawSession, attachToYeehaw, hasTmux, } from './lib/tmux.js';
7
7
  import { ensureConfigDirs } from './lib/config.js';
8
+ import { checkForUpdates, formatUpdateMessage } from './lib/update-check.js';
8
9
  function main() {
9
10
  // Ensure config directories exist
10
11
  ensureConfigDirs();
12
+ // Check for updates (non-blocking, uses cache)
13
+ try {
14
+ const updateInfo = checkForUpdates();
15
+ if (updateInfo?.updateAvailable) {
16
+ console.log('\x1b[33m%s\x1b[0m', formatUpdateMessage(updateInfo));
17
+ console.log('');
18
+ }
19
+ }
20
+ catch {
21
+ // Ignore update check errors
22
+ }
11
23
  // Check if tmux is available
12
24
  if (!hasTmux()) {
13
25
  console.error('Error: tmux is required but not installed');
@@ -11,6 +11,17 @@ export function generateTmuxConfig() {
11
11
  set -g mouse on
12
12
  set -g history-limit 50000
13
13
 
14
+ # macOS clipboard support
15
+ # Enable copying to system clipboard when selecting with mouse
16
+ set -g set-clipboard on
17
+ bind-key -T copy-mode MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"
18
+ bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"
19
+ # Also support keyboard-based copy (Enter key in copy mode)
20
+ bind-key -T copy-mode Enter send-keys -X copy-pipe-and-cancel "pbcopy"
21
+ bind-key -T copy-mode-vi Enter send-keys -X copy-pipe-and-cancel "pbcopy"
22
+ # Use y to yank in vi mode
23
+ bind-key -T copy-mode-vi y send-keys -X copy-pipe-and-cancel "pbcopy"
24
+
14
25
  # Yeehaw keybindings
15
26
  bind-key -n C-y select-window -t :0 # Return to dashboard
16
27
  bind-key -n C-h previous-window # Go left one window
@@ -12,6 +12,11 @@ export declare function isInsideYeehawSession(): boolean;
12
12
  export declare function yeehawSessionExists(): boolean;
13
13
  export declare function createYeehawSession(): void;
14
14
  export declare function setupStatusBarHooks(): void;
15
+ /**
16
+ * Force check and correct the status bar visibility based on current window.
17
+ * Call this when you suspect the status bar might be in the wrong state.
18
+ */
19
+ export declare function ensureCorrectStatusBar(): void;
15
20
  export declare function attachToYeehaw(): void;
16
21
  export declare function createClaudeWindow(workingDir: string, windowName: string): number;
17
22
  export declare function createShellWindow(workingDir: string, windowName: string, shell?: string): number;
package/dist/lib/tmux.js CHANGED
@@ -61,6 +61,7 @@ export function createYeehawSession() {
61
61
  }
62
62
  export function setupStatusBarHooks() {
63
63
  // Hide status bar when in window 0, show in other windows
64
+ const statusCheck = 'if-shell -F "#{==:#{window_index},0}" "set status off" "set status on"';
64
65
  try {
65
66
  // Start with status off (we begin in window 0)
66
67
  execaSync('tmux', ['set', '-t', YEEHAW_SESSION, 'status', 'off']);
@@ -68,15 +69,56 @@ export function setupStatusBarHooks() {
68
69
  execaSync('tmux', [
69
70
  'set-hook', '-t', YEEHAW_SESSION,
70
71
  'after-select-window',
71
- 'if-shell -F "#{==:#{window_index},0}" "set status off" "set status on"'
72
+ statusCheck
73
+ ]);
74
+ // Hook for when a window is killed (we might land back on window 0)
75
+ execaSync('tmux', [
76
+ 'set-hook', '-t', YEEHAW_SESSION,
77
+ 'window-unlinked',
78
+ statusCheck
79
+ ]);
80
+ // Hook for when pane focus changes (covers edge cases)
81
+ execaSync('tmux', [
82
+ 'set-hook', '-t', YEEHAW_SESSION,
83
+ 'pane-focus-in',
84
+ statusCheck
85
+ ]);
86
+ // Hook for client attachment (ensure status is correct when reattaching)
87
+ execaSync('tmux', [
88
+ 'set-hook', '-t', YEEHAW_SESSION,
89
+ 'client-attached',
90
+ statusCheck
72
91
  ]);
73
92
  }
74
93
  catch {
75
94
  // Hooks might fail on older tmux versions, not critical
76
95
  }
77
96
  }
97
+ /**
98
+ * Force check and correct the status bar visibility based on current window.
99
+ * Call this when you suspect the status bar might be in the wrong state.
100
+ */
101
+ export function ensureCorrectStatusBar() {
102
+ try {
103
+ // Get current window index
104
+ const result = execaSync('tmux', ['display-message', '-p', '#{window_index}']);
105
+ const windowIndex = parseInt(result.stdout.trim(), 10);
106
+ // Set status based on window
107
+ if (windowIndex === 0) {
108
+ execaSync('tmux', ['set', '-t', YEEHAW_SESSION, 'status', 'off']);
109
+ }
110
+ else {
111
+ execaSync('tmux', ['set', '-t', YEEHAW_SESSION, 'status', 'on']);
112
+ }
113
+ }
114
+ catch {
115
+ // Ignore errors
116
+ }
117
+ }
78
118
  export function attachToYeehaw() {
79
- // This replaces the current process
119
+ // Attach to the existing yeehaw session
120
+ // Note: Multiple terminals attaching to the same session will share the same view
121
+ // (this is a tmux limitation - they'll see the same window selections)
80
122
  spawnSync('tmux', ['attach-session', '-t', YEEHAW_SESSION], {
81
123
  stdio: 'inherit',
82
124
  });
@@ -130,15 +172,18 @@ export function createClaudeWindow(workingDir, windowName) {
130
172
  ]);
131
173
  return parseInt(result.stdout.trim(), 10);
132
174
  }
133
- export function createShellWindow(workingDir, windowName, shell = '/bin/zsh') {
134
- // Create new window running shell (-a appends after current window)
175
+ export function createShellWindow(workingDir, windowName, shell) {
176
+ // Use the user's configured shell from $SHELL, fallback to /bin/bash
177
+ const userShell = shell || process.env.SHELL || '/bin/bash';
178
+ // Create new window running shell as a login shell (-l) so it loads .bashrc/.bash_profile/.zshrc etc.
179
+ // This ensures PS1 and other environment customizations are loaded
135
180
  execaSync('tmux', [
136
181
  'new-window',
137
182
  '-a',
138
183
  '-t', YEEHAW_SESSION,
139
184
  '-n', windowName,
140
185
  '-c', workingDir,
141
- shell,
186
+ `${userShell} -l`,
142
187
  ]);
143
188
  // Get the window index we just created (new window is now current)
144
189
  const result = execaSync('tmux', [
@@ -0,0 +1,22 @@
1
+ export interface UpdateInfo {
2
+ updateAvailable: boolean;
3
+ currentVersion: string;
4
+ latestVersion: string;
5
+ }
6
+ /**
7
+ * Get version information synchronously using cached data.
8
+ * Safe to call from React components.
9
+ */
10
+ export declare function getVersionInfo(): {
11
+ current: string;
12
+ latest: string | null;
13
+ };
14
+ /**
15
+ * Check for updates. Returns immediately with cached data if available,
16
+ * otherwise fetches from npm (with 5s timeout).
17
+ */
18
+ export declare function checkForUpdates(): UpdateInfo | null;
19
+ /**
20
+ * Format update notification message
21
+ */
22
+ export declare function formatUpdateMessage(info: UpdateInfo): string;
@@ -0,0 +1,128 @@
1
+ import { execFileSync } from 'child_process';
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { YEEHAW_DIR } from './paths.js';
5
+ const PACKAGE_NAME = '@colmbus72/yeehaw';
6
+ const CACHE_FILE = join(YEEHAW_DIR, '.update-check');
7
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
8
+ function getCurrentVersion() {
9
+ try {
10
+ const packagePath = new URL('../../package.json', import.meta.url);
11
+ const pkg = JSON.parse(readFileSync(packagePath, 'utf-8'));
12
+ return pkg.version;
13
+ }
14
+ catch {
15
+ return '0.0.0';
16
+ }
17
+ }
18
+ function readCache() {
19
+ try {
20
+ if (!existsSync(CACHE_FILE))
21
+ return null;
22
+ const data = JSON.parse(readFileSync(CACHE_FILE, 'utf-8'));
23
+ if (Date.now() - data.checkedAt < CACHE_TTL_MS) {
24
+ return data;
25
+ }
26
+ return null; // Cache expired
27
+ }
28
+ catch {
29
+ return null;
30
+ }
31
+ }
32
+ function writeCache(latestVersion) {
33
+ try {
34
+ if (!existsSync(YEEHAW_DIR)) {
35
+ mkdirSync(YEEHAW_DIR, { recursive: true });
36
+ }
37
+ const data = {
38
+ latestVersion,
39
+ checkedAt: Date.now(),
40
+ };
41
+ writeFileSync(CACHE_FILE, JSON.stringify(data), 'utf-8');
42
+ }
43
+ catch {
44
+ // Ignore cache write errors
45
+ }
46
+ }
47
+ function fetchLatestVersion() {
48
+ try {
49
+ // Use execFileSync for safety (no shell injection possible)
50
+ const result = execFileSync('npm', ['view', PACKAGE_NAME, 'version'], {
51
+ encoding: 'utf-8',
52
+ timeout: 5000,
53
+ stdio: ['pipe', 'pipe', 'pipe'],
54
+ });
55
+ return result.trim();
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ }
61
+ function compareVersions(current, latest) {
62
+ const parseVersion = (v) => v.split('.').map(n => parseInt(n, 10) || 0);
63
+ const [cMajor, cMinor, cPatch] = parseVersion(current);
64
+ const [lMajor, lMinor, lPatch] = parseVersion(latest);
65
+ if (lMajor > cMajor)
66
+ return 1;
67
+ if (lMajor < cMajor)
68
+ return -1;
69
+ if (lMinor > cMinor)
70
+ return 1;
71
+ if (lMinor < cMinor)
72
+ return -1;
73
+ if (lPatch > cPatch)
74
+ return 1;
75
+ if (lPatch < cPatch)
76
+ return -1;
77
+ return 0;
78
+ }
79
+ /**
80
+ * Get version information synchronously using cached data.
81
+ * Safe to call from React components.
82
+ */
83
+ export function getVersionInfo() {
84
+ const current = getCurrentVersion();
85
+ const cached = readCache();
86
+ return {
87
+ current,
88
+ latest: cached?.latestVersion || null,
89
+ };
90
+ }
91
+ /**
92
+ * Check for updates. Returns immediately with cached data if available,
93
+ * otherwise fetches from npm (with 5s timeout).
94
+ */
95
+ export function checkForUpdates() {
96
+ try {
97
+ const currentVersion = getCurrentVersion();
98
+ // Try cache first
99
+ const cached = readCache();
100
+ if (cached) {
101
+ return {
102
+ updateAvailable: compareVersions(currentVersion, cached.latestVersion) > 0,
103
+ currentVersion,
104
+ latestVersion: cached.latestVersion,
105
+ };
106
+ }
107
+ // Fetch from npm
108
+ const latestVersion = fetchLatestVersion();
109
+ if (!latestVersion)
110
+ return null;
111
+ // Update cache
112
+ writeCache(latestVersion);
113
+ return {
114
+ updateAvailable: compareVersions(currentVersion, latestVersion) > 0,
115
+ currentVersion,
116
+ latestVersion,
117
+ };
118
+ }
119
+ catch {
120
+ return null;
121
+ }
122
+ }
123
+ /**
124
+ * Format update notification message
125
+ */
126
+ export function formatUpdateMessage(info) {
127
+ return `Update available: ${info.currentVersion} → ${info.latestVersion}\nRun: npm install -g ${PACKAGE_NAME}`;
128
+ }
File without changes
package/dist/types.d.ts CHANGED
@@ -80,10 +80,14 @@ export type AppView = {
80
80
  type: 'livestock';
81
81
  project: Project;
82
82
  livestock: Livestock;
83
+ source: 'project' | 'barn';
84
+ sourceBarn?: Barn;
83
85
  } | {
84
86
  type: 'logs';
85
87
  project: Project;
86
88
  livestock: Livestock;
89
+ source: 'project' | 'barn';
90
+ sourceBarn?: Barn;
87
91
  } | {
88
92
  type: 'night-sky';
89
93
  };
@@ -82,10 +82,6 @@ export function BarnContext({ barn, livestock, projects, windows, onBack, onSshT
82
82
  }
83
83
  if (mode !== 'normal')
84
84
  return;
85
- if (input === 'q') {
86
- onBack();
87
- return;
88
- }
89
85
  if (key.tab) {
90
86
  setFocusedPanel((p) => (p === 'livestock' ? 'critters' : 'livestock'));
91
87
  return;
@@ -4,6 +4,10 @@ interface GlobalDashboardProps {
4
4
  projects: Project[];
5
5
  barns: Barn[];
6
6
  windows: TmuxWindow[];
7
+ versionInfo?: {
8
+ current: string;
9
+ latest: string | null;
10
+ };
7
11
  onSelectProject: (project: Project) => void;
8
12
  onSelectBarn: (barn: Barn) => void;
9
13
  onSelectWindow: (window: TmuxWindow) => void;
@@ -12,5 +16,5 @@ interface GlobalDashboardProps {
12
16
  onCreateBarn: (barn: Barn) => void;
13
17
  onSshToBarn: (barn: Barn) => void;
14
18
  }
15
- export declare function GlobalDashboard({ projects, barns, windows, onSelectProject, onSelectBarn, onSelectWindow, onNewClaude, onCreateProject, onCreateBarn, onSshToBarn, }: GlobalDashboardProps): import("react/jsx-runtime").JSX.Element;
19
+ export declare function GlobalDashboard({ projects, barns, windows, versionInfo, onSelectProject, onSelectBarn, onSelectWindow, onNewClaude, onCreateProject, onCreateBarn, onSshToBarn, }: GlobalDashboardProps): import("react/jsx-runtime").JSX.Element;
16
20
  export {};
@@ -8,10 +8,11 @@ import { List } from '../components/List.js';
8
8
  import { PathInput } from '../components/PathInput.js';
9
9
  import { parseSshConfig } from '../lib/ssh.js';
10
10
  import { getWindowStatus } from '../lib/tmux.js';
11
+ import { isLocalBarn } from '../lib/config.js';
11
12
  function countSessionsForProject(projectName, windows) {
12
13
  return windows.filter((w) => w.name.startsWith(projectName)).length;
13
14
  }
14
- export function GlobalDashboard({ projects, barns, windows, onSelectProject, onSelectBarn, onSelectWindow, onNewClaude, onCreateProject, onCreateBarn, onSshToBarn, }) {
15
+ export function GlobalDashboard({ projects, barns, windows, versionInfo, onSelectProject, onSelectBarn, onSelectWindow, onNewClaude, onCreateProject, onCreateBarn, onSshToBarn, }) {
15
16
  const [focusedPanel, setFocusedPanel] = useState('projects');
16
17
  const [mode, setMode] = useState('normal');
17
18
  // New project form state
@@ -170,25 +171,51 @@ export function GlobalDashboard({ projects, barns, windows, onSelectProject, onS
170
171
  meta: sessionCount > 0 ? `${sessionCount} session${sessionCount > 1 ? 's' : ''}` : undefined,
171
172
  };
172
173
  });
174
+ // Parse window name to show clearer labels
175
+ const formatSessionLabel = (name) => {
176
+ // Remote yeehaw connection
177
+ if (name.startsWith('remote:')) {
178
+ return { label: name.replace('remote:', ''), typeHint: 'remote' };
179
+ }
180
+ // Barn shell session
181
+ if (name.startsWith('barn-')) {
182
+ return { label: name.replace('barn-', ''), typeHint: 'barn' };
183
+ }
184
+ // Claude session
185
+ if (name.endsWith('-claude')) {
186
+ return { label: name.replace('-claude', ''), typeHint: 'claude' };
187
+ }
188
+ // Livestock session (project-livestock format)
189
+ const parts = name.split('-');
190
+ if (parts.length >= 2) {
191
+ const projectName = parts.slice(0, -1).join('-');
192
+ const livestockName = parts[parts.length - 1];
193
+ return { label: `${projectName} / ${livestockName}`, typeHint: 'shell' };
194
+ }
195
+ return { label: name, typeHint: '' };
196
+ };
173
197
  // Use display numbers (1-9) instead of window index
174
- const sessionItems = sessionWindows.map((w, i) => ({
175
- id: String(w.index),
176
- label: `[${i + 1}] ${w.name}`,
177
- status: w.active ? 'active' : 'inactive',
178
- meta: getWindowStatus(w),
179
- }));
198
+ const sessionItems = sessionWindows.map((w, i) => {
199
+ const { label, typeHint } = formatSessionLabel(w.name);
200
+ return {
201
+ id: String(w.index),
202
+ label: `[${i + 1}] ${label}`,
203
+ status: w.active ? 'active' : 'inactive',
204
+ meta: typeHint ? `${typeHint} · ${getWindowStatus(w)}` : getWindowStatus(w),
205
+ };
206
+ });
180
207
  const barnItems = barns.map((b) => ({
181
208
  id: b.name,
182
- label: b.name,
209
+ label: isLocalBarn(b) ? 'local' : b.name,
183
210
  status: 'active',
184
- meta: `${b.user}@${b.host}`,
211
+ meta: isLocalBarn(b) ? 'this machine' : `${b.user}@${b.host}`,
185
212
  }));
186
213
  // New project modals
187
214
  if (mode === 'new-project-name') {
188
- return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: "Create New Project" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Name: " }), _jsx(TextInput, { value: newProjectName, onChange: setNewProjectName, onSubmit: handleProjectNameSubmit })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Enter to continue, Esc to cancel" }) })] })] }));
215
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW", versionInfo: versionInfo }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: "Create New Project" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Name: " }), _jsx(TextInput, { value: newProjectName, onChange: setNewProjectName, onSubmit: handleProjectNameSubmit })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Enter to continue, Esc to cancel" }) })] })] }));
189
216
  }
190
217
  if (mode === 'new-project-path') {
191
- return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsxs(Text, { bold: true, color: "yellow", children: ["Create New Project: ", newProjectName] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Path: " }), _jsx(PathInput, { value: newProjectPath, onChange: setNewProjectPath, onSubmit: handleProjectPathSubmit })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Tab to autocomplete, Enter to create, Esc to cancel" }) })] })] }));
218
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW", versionInfo: versionInfo }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsxs(Text, { bold: true, color: "yellow", children: ["Create New Project: ", newProjectName] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Path: " }), _jsx(PathInput, { value: newProjectPath, onChange: setNewProjectPath, onSubmit: handleProjectPathSubmit })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Tab to autocomplete, Enter to create, Esc to cancel" }) })] })] }));
192
219
  }
193
220
  // SSH host selection for new barn
194
221
  if (mode === 'new-barn-select-ssh') {
@@ -201,7 +228,7 @@ export function GlobalDashboard({ projects, barns, windows, onSelectProject, onS
201
228
  meta: h.hostname ? `${h.user || 'root'}@${h.hostname}` : undefined,
202
229
  })),
203
230
  ];
204
- return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "green", children: "Add New Barn" }), _jsx(Text, { dimColor: true, children: "Select from SSH config or enter manually" }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(List, { items: sshHostItems, focused: true, onSelect: (item) => {
231
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW", versionInfo: versionInfo }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "green", children: "Add New Barn" }), _jsx(Text, { dimColor: true, children: "Select from SSH config or enter manually" }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsx(List, { items: sshHostItems, focused: true, onSelect: (item) => {
205
232
  if (item.id === '__manual__') {
206
233
  setMode('new-barn-name');
207
234
  }
@@ -214,25 +241,25 @@ export function GlobalDashboard({ projects, barns, windows, onSelectProject, onS
214
241
  }
215
242
  // New barn modals
216
243
  if (mode === 'new-barn-name') {
217
- return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "green", children: "Add New Barn" }), _jsx(Text, { dimColor: true, children: "A barn is a server you manage" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Name: " }), _jsx(TextInput, { value: newBarnName, onChange: setNewBarnName, onSubmit: handleBarnNameSubmit, placeholder: "my-server" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Esc: cancel" }) })] })] }));
244
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW", versionInfo: versionInfo }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "green", children: "Add New Barn" }), _jsx(Text, { dimColor: true, children: "A barn is a server you manage" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Name: " }), _jsx(TextInput, { value: newBarnName, onChange: setNewBarnName, onSubmit: handleBarnNameSubmit, placeholder: "my-server" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Esc: cancel" }) })] })] }));
218
245
  }
219
246
  if (mode === 'new-barn-host') {
220
- return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsxs(Text, { bold: true, color: "green", children: ["Add New Barn: ", newBarnName] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Host: " }), _jsx(TextInput, { value: newBarnHost, onChange: setNewBarnHost, onSubmit: handleBarnHostSubmit, placeholder: "192.168.1.100 or server.example.com" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Esc: cancel" }) })] })] }));
247
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW", versionInfo: versionInfo }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsxs(Text, { bold: true, color: "green", children: ["Add New Barn: ", newBarnName] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Host: " }), _jsx(TextInput, { value: newBarnHost, onChange: setNewBarnHost, onSubmit: handleBarnHostSubmit, placeholder: "192.168.1.100 or server.example.com" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Esc: cancel" }) })] })] }));
221
248
  }
222
249
  if (mode === 'new-barn-user') {
223
- return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsxs(Text, { bold: true, color: "green", children: ["Add New Barn: ", newBarnName] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "SSH User: " }), _jsx(TextInput, { value: newBarnUser, onChange: setNewBarnUser, onSubmit: handleBarnUserSubmit, placeholder: "root" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Esc: cancel" }) })] })] }));
250
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW", versionInfo: versionInfo }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsxs(Text, { bold: true, color: "green", children: ["Add New Barn: ", newBarnName] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "SSH User: " }), _jsx(TextInput, { value: newBarnUser, onChange: setNewBarnUser, onSubmit: handleBarnUserSubmit, placeholder: "root" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Esc: cancel" }) })] })] }));
224
251
  }
225
252
  if (mode === 'new-barn-port') {
226
- return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsxs(Text, { bold: true, color: "green", children: ["Add New Barn: ", newBarnName] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "SSH Port: " }), _jsx(TextInput, { value: newBarnPort, onChange: setNewBarnPort, onSubmit: handleBarnPortSubmit, placeholder: "22" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field (leave blank for 22), Esc: cancel" }) })] })] }));
253
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW", versionInfo: versionInfo }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsxs(Text, { bold: true, color: "green", children: ["Add New Barn: ", newBarnName] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "SSH Port: " }), _jsx(TextInput, { value: newBarnPort, onChange: setNewBarnPort, onSubmit: handleBarnPortSubmit, placeholder: "22" })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field (leave blank for 22), Esc: cancel" }) })] })] }));
227
254
  }
228
255
  if (mode === 'new-barn-key') {
229
- return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW" }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsxs(Text, { bold: true, color: "green", children: ["Add New Barn: ", newBarnName] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "SSH Key Path: " }), _jsx(PathInput, { value: newBarnKey, onChange: setNewBarnKey, onSubmit: handleBarnKeySubmit })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Tab: autocomplete, Enter: create barn, Esc: cancel" }) })] })] }));
256
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW", versionInfo: versionInfo }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsxs(Text, { bold: true, color: "green", children: ["Add New Barn: ", newBarnName] }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "SSH Key Path: " }), _jsx(PathInput, { value: newBarnKey, onChange: setNewBarnKey, onSubmit: handleBarnKeySubmit })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Tab: autocomplete, Enter: create barn, Esc: cancel" }) })] })] }));
230
257
  }
231
258
  // Panel-specific hints (page-level hotkeys like c are in BottomBar)
232
259
  const projectHints = '[n] new';
233
260
  const sessionHints = '1-9 switch';
234
261
  const barnHints = '[n] new [s] shell';
235
- return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW" }), _jsxs(Box, { flexGrow: 1, marginY: 1, paddingX: 1, gap: 2, children: [_jsx(Panel, { title: "Projects", focused: focusedPanel === 'projects', width: "40%", hints: projectHints, children: projectItems.length > 0 ? (_jsx(List, { items: projectItems, focused: focusedPanel === 'projects', onSelect: (item) => {
262
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: "YEEHAW", versionInfo: versionInfo }), _jsxs(Box, { flexGrow: 1, marginY: 1, paddingX: 1, gap: 2, children: [_jsx(Panel, { title: "Projects", focused: focusedPanel === 'projects', width: "40%", hints: projectHints, children: projectItems.length > 0 ? (_jsx(List, { items: projectItems, focused: focusedPanel === 'projects', onSelect: (item) => {
236
263
  const project = projects.find((p) => p.name === item.id);
237
264
  if (project)
238
265
  onSelectProject(project);
@@ -69,7 +69,7 @@ export function IssuesView({ project, onBack }) {
69
69
  const selectedIssue = issues[selectedIndex];
70
70
  // Handle input
71
71
  useInput((input, key) => {
72
- if (key.escape || input === 'q') {
72
+ if (key.escape) {
73
73
  onBack();
74
74
  return;
75
75
  }
@@ -1,11 +1,16 @@
1
- import type { Project, Livestock } from '../types.js';
1
+ import { type TmuxWindow } from '../lib/tmux.js';
2
+ import type { Project, Livestock, Barn } from '../types.js';
2
3
  interface LivestockDetailViewProps {
3
4
  project: Project;
4
5
  livestock: Livestock;
6
+ source: 'project' | 'barn';
7
+ sourceBarn?: Barn;
8
+ windows: TmuxWindow[];
5
9
  onBack: () => void;
6
10
  onOpenLogs: () => void;
7
11
  onOpenSession: () => void;
12
+ onSelectWindow: (window: TmuxWindow) => void;
8
13
  onUpdateLivestock: (livestock: Livestock) => void;
9
14
  }
10
- export declare function LivestockDetailView({ project, livestock, onBack, onOpenLogs, onOpenSession, onUpdateLivestock, }: LivestockDetailViewProps): import("react/jsx-runtime").JSX.Element;
15
+ export declare function LivestockDetailView({ project, livestock, source, sourceBarn, windows, onBack, onOpenLogs, onOpenSession, onSelectWindow, onUpdateLivestock, }: LivestockDetailViewProps): import("react/jsx-runtime").JSX.Element;
11
16
  export {};
@@ -1,12 +1,15 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from 'react';
2
+ import { useState, useEffect } from 'react';
3
3
  import { Box, Text, useInput } from 'ink';
4
4
  import TextInput from 'ink-text-input';
5
5
  import { Header } from '../components/Header.js';
6
+ import { LivestockHeader } from '../components/LivestockHeader.js';
7
+ import { List } from '../components/List.js';
6
8
  import { Panel } from '../components/Panel.js';
7
9
  import { PathInput } from '../components/PathInput.js';
8
10
  import { loadBarn } from '../lib/config.js';
9
- export function LivestockDetailView({ project, livestock, onBack, onOpenLogs, onOpenSession, onUpdateLivestock, }) {
11
+ import { getWindowStatus } from '../lib/tmux.js';
12
+ export function LivestockDetailView({ project, livestock, source, sourceBarn, windows, onBack, onOpenLogs, onOpenSession, onSelectWindow, onUpdateLivestock, }) {
10
13
  const [mode, setMode] = useState('normal');
11
14
  // Edit form state
12
15
  const [editName, setEditName] = useState(livestock.name);
@@ -17,6 +20,15 @@ export function LivestockDetailView({ project, livestock, onBack, onOpenLogs, on
17
20
  const [editEnvPath, setEditEnvPath] = useState(livestock.env_path || '');
18
21
  // Get barn info if remote
19
22
  const barn = livestock.barn ? loadBarn(livestock.barn) : null;
23
+ // Sync form state when livestock prop changes (e.g., after save)
24
+ useEffect(() => {
25
+ setEditName(livestock.name);
26
+ setEditPath(livestock.path);
27
+ setEditRepo(livestock.repo || '');
28
+ setEditBranch(livestock.branch || '');
29
+ setEditLogPath(livestock.log_path || '');
30
+ setEditEnvPath(livestock.env_path || '');
31
+ }, [livestock]);
20
32
  const resetForm = () => {
21
33
  setEditName(livestock.name);
22
34
  setEditPath(livestock.path);
@@ -65,10 +77,6 @@ export function LivestockDetailView({ project, livestock, onBack, onOpenLogs, on
65
77
  // Only process these in normal mode
66
78
  if (mode !== 'normal')
67
79
  return;
68
- if (input === 'q') {
69
- onBack();
70
- return;
71
- }
72
80
  if (input === 's') {
73
81
  onOpenSession();
74
82
  return;
@@ -134,7 +142,19 @@ export function LivestockDetailView({ project, livestock, onBack, onOpenLogs, on
134
142
  setMode('normal');
135
143
  } })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: save & finish, Esc: cancel" }) })] })] }));
136
144
  }
137
- // Normal view - show livestock info
138
- const hints = `[s] shell ${livestock.log_path ? '[l] logs ' : ''}[e] edit [q] back`;
139
- return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: livestock.name, color: project.color }), _jsx(Box, { flexGrow: 1, paddingX: 1, children: _jsx(Panel, { title: "Livestock Details", focused: true, hints: hints, children: _jsxs(Box, { flexDirection: "column", gap: 1, children: [_jsxs(Box, { children: [_jsx(Box, { width: 12, children: _jsx(Text, { bold: true, children: "Name:" }) }), _jsx(Text, { children: livestock.name })] }), _jsxs(Box, { children: [_jsx(Box, { width: 12, children: _jsx(Text, { bold: true, children: "Path:" }) }), _jsx(Text, { children: livestock.path })] }), _jsxs(Box, { children: [_jsx(Box, { width: 12, children: _jsx(Text, { bold: true, children: "Location:" }) }), barn ? (_jsxs(Text, { children: [barn.name, " ", _jsxs(Text, { dimColor: true, children: ["(", barn.host, ")"] })] })) : (_jsx(Text, { children: "Local" }))] }), livestock.repo && (_jsxs(Box, { children: [_jsx(Box, { width: 12, children: _jsx(Text, { bold: true, children: "Repo:" }) }), _jsx(Text, { children: livestock.repo })] })), livestock.branch && (_jsxs(Box, { children: [_jsx(Box, { width: 12, children: _jsx(Text, { bold: true, children: "Branch:" }) }), _jsx(Text, { children: livestock.branch })] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { bold: true, color: "yellow", children: "Operational Config" }) }), _jsxs(Box, { children: [_jsx(Box, { width: 12, children: _jsx(Text, { bold: true, children: "Log Path:" }) }), livestock.log_path ? (_jsx(Text, { children: livestock.log_path })) : (_jsx(Text, { dimColor: true, children: "Not configured" }))] }), _jsxs(Box, { children: [_jsx(Box, { width: 12, children: _jsx(Text, { bold: true, children: "Env Path:" }) }), livestock.env_path ? (_jsx(Text, { children: livestock.env_path })) : (_jsx(Text, { dimColor: true, children: "Not configured" }))] })] }) }) })] }));
145
+ // Filter windows to this livestock (match pattern: projectname-livestockname)
146
+ const livestockWindowName = `${project.name}-${livestock.name}`;
147
+ const livestockWindows = windows.filter(w => w.name === livestockWindowName || w.name.startsWith(`${livestockWindowName}-`));
148
+ const sessionItems = livestockWindows.map((w, i) => ({
149
+ id: String(w.index),
150
+ label: `[${i + 1}] shell`,
151
+ status: w.active ? 'active' : 'inactive',
152
+ meta: getWindowStatus(w),
153
+ }));
154
+ // Normal view - show livestock info inline with sessions
155
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(LivestockHeader, { project: project, livestock: livestock }), _jsxs(Box, { paddingX: 2, gap: 3, children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "path:" }), " ", livestock.path] }), barn && (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "barn:" }), " ", barn.name, " ", _jsxs(Text, { dimColor: true, children: ["(", barn.host, ")"] })] }))] }), _jsxs(Box, { paddingX: 2, gap: 3, marginBottom: 1, children: [livestock.repo && (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "repo:" }), " ", livestock.repo] })), livestock.log_path && (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "logs:" }), " ", livestock.log_path] })), livestock.env_path && (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "env:" }), " ", livestock.env_path] }))] }), _jsx(Box, { paddingX: 1, flexGrow: 1, children: _jsx(Panel, { title: "Sessions", focused: true, hints: `[s] shell ${livestock.log_path ? '[l] logs ' : ''}[e] edit`, children: sessionItems.length > 0 ? (_jsx(List, { items: sessionItems, focused: true, onSelect: (item) => {
156
+ const window = livestockWindows.find(w => String(w.index) === item.id);
157
+ if (window)
158
+ onSelectWindow(window);
159
+ } })) : (_jsx(Text, { dimColor: true, italic: true, children: "No active sessions. Press [s] to start a shell." })) }) })] }));
140
160
  }
@@ -33,7 +33,7 @@ export function LogsView({ project, livestock, onBack }) {
33
33
  }, [livestock]);
34
34
  const totalLines = logs.length;
35
35
  useInput((input, key) => {
36
- if (key.escape || input === 'q') {
36
+ if (key.escape) {
37
37
  onBack();
38
38
  return;
39
39
  }
@@ -9,6 +9,7 @@ import { PathInput } from '../components/PathInput.js';
9
9
  import { getWindowStatus } from '../lib/tmux.js';
10
10
  import { detectGitInfo, detectRemoteGitInfo } from '../lib/git.js';
11
11
  import { detectLivestockConfig } from '../lib/livestock.js';
12
+ import { isLocalBarn } from '../lib/config.js';
12
13
  export function ProjectContext({ project, barns, windows, onBack, onNewClaude, onSelectWindow, onSelectLivestock, onOpenLivestockSession, onUpdateProject, onDeleteProject, onOpenWiki, onOpenIssues, }) {
13
14
  const [focusedPanel, setFocusedPanel] = useState('livestock');
14
15
  const [mode, setMode] = useState('normal');
@@ -159,10 +160,6 @@ export function ProjectContext({ project, barns, windows, onBack, onNewClaude, o
159
160
  }
160
161
  if (mode !== 'normal')
161
162
  return;
162
- if (input === 'q') {
163
- onBack();
164
- return;
165
- }
166
163
  if (key.tab) {
167
164
  setFocusedPanel((p) => (p === 'livestock' ? 'sessions' : 'livestock'));
168
165
  return;
@@ -245,10 +242,11 @@ export function ProjectContext({ project, barns, windows, onBack, onNewClaude, o
245
242
  }, placeholder: "local, dev, production..." })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next, Esc: cancel" }) })] })] }));
246
243
  }
247
244
  if (mode === 'add-livestock-barn') {
248
- // Build barn options including "Local" option
245
+ // Build barn options including "Local" option (filter out local barn from array to avoid duplicates)
246
+ const remoteBarns = barns.filter((b) => !isLocalBarn(b));
249
247
  const barnOptions = [
250
248
  { id: '__local__', label: 'Local (this machine)', status: 'active' },
251
- ...barns.map((b) => ({
249
+ ...remoteBarns.map((b) => ({
252
250
  id: b.name,
253
251
  label: b.name,
254
252
  status: 'active',
@@ -65,10 +65,6 @@ export function WikiView({ project, onBack, onUpdateProject }) {
65
65
  // Only process these in normal mode
66
66
  if (mode !== 'normal')
67
67
  return;
68
- if (input === 'q') {
69
- onBack();
70
- return;
71
- }
72
68
  // Tab to switch focus between panels
73
69
  if (key.tab) {
74
70
  setFocusedPanel((prev) => (prev === 'sections' ? 'content' : 'sections'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colmbus72/yeehaw",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Terminal dashboard for managing projects, servers, and deployments",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",