@colmbus72/yeehaw 0.2.0 → 0.4.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.
@@ -0,0 +1,5 @@
1
+ interface SplashScreenProps {
2
+ onComplete: () => void;
3
+ }
4
+ export declare function SplashScreen({ onComplete }: SplashScreenProps): import("react/jsx-runtime").JSX.Element;
5
+ export {};
@@ -0,0 +1,178 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect, useMemo } from 'react';
3
+ import { Box, Text, useStdout } from 'ink';
4
+ import figlet from 'figlet';
5
+ // Tumbleweed ASCII art - same as in Header.tsx
6
+ const TUMBLEWEED = [
7
+ ' ░ ░▒░ ░▒░',
8
+ '░▒ · ‿ · ▒░',
9
+ '▒░ ▒░▒░ ░▒',
10
+ ' ░▒░ ░▒░ ░',
11
+ ];
12
+ const TUMBLEWEED_COLOR = '#b8860b';
13
+ const BRAND_COLOR = '#f0c040';
14
+ // Match Header.tsx positioning exactly
15
+ const HEADER_PADDING_TOP = 1;
16
+ const TUMBLEWEED_TOP_PADDING = 1;
17
+ const HEADER_PADDING_LEFT = 2;
18
+ const TUMBLEWEED_WIDTH = 11;
19
+ const TITLE_OFFSET_LEFT = HEADER_PADDING_LEFT + TUMBLEWEED_WIDTH + 2;
20
+ // Gradient color helpers (matching Header.tsx)
21
+ function hexToRgb(hex) {
22
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
23
+ return result
24
+ ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) }
25
+ : null;
26
+ }
27
+ function interpolateColor(c1, c2, factor) {
28
+ const r = Math.round(c1.r + (c2.r - c1.r) * factor);
29
+ const g = Math.round(c1.g + (c2.g - c1.g) * factor);
30
+ const b = Math.round(c1.b + (c2.b - c1.b) * factor);
31
+ return `rgb(${r},${g},${b})`;
32
+ }
33
+ function getGradientColor(row, totalRows) {
34
+ const rgb = hexToRgb(BRAND_COLOR);
35
+ if (!rgb)
36
+ return BRAND_COLOR;
37
+ const startRgb = rgb;
38
+ const endRgb = { r: Math.round(rgb.r * 0.3), g: Math.round(rgb.g * 0.3), b: Math.round(rgb.b * 0.3) };
39
+ const factor = row / Math.max(totalRows - 1, 1);
40
+ return interpolateColor(startRgb, endRgb, factor);
41
+ }
42
+ export function SplashScreen({ onComplete }) {
43
+ const { stdout } = useStdout();
44
+ const terminalHeight = stdout?.rows || 24;
45
+ const terminalWidth = stdout?.columns || 80;
46
+ const [phase, setPhase] = useState('build');
47
+ const [visibleCount, setVisibleCount] = useState(0);
48
+ const [waveDistance, setWaveDistance] = useState(0);
49
+ const waveOriginRow = HEADER_PADDING_TOP + TUMBLEWEED_TOP_PADDING + 2;
50
+ const waveOriginCol = HEADER_PADDING_LEFT + 5;
51
+ // Generate title dots from figlet
52
+ const allDots = useMemo(() => {
53
+ const dots = [];
54
+ try {
55
+ const ascii = figlet.textSync('YEEHAW', { font: 'ANSI Shadow' });
56
+ const lines = ascii.split('\n').filter(line => line.trim() !== '');
57
+ const totalRows = lines.length;
58
+ lines.forEach((line, row) => {
59
+ for (let col = 0; col < line.length; col++) {
60
+ const char = line[col];
61
+ if (char !== ' ') {
62
+ const screenRow = HEADER_PADDING_TOP + row;
63
+ const screenCol = TITLE_OFFSET_LEFT + col;
64
+ const distance = Math.sqrt(Math.pow(screenRow - waveOriginRow, 2) +
65
+ Math.pow((screenCol - waveOriginCol) * 0.5, 2));
66
+ dots.push({ row: screenRow, col: screenCol, distance, gradientRow: row, totalRows });
67
+ }
68
+ }
69
+ });
70
+ }
71
+ catch {
72
+ // Fallback if figlet fails
73
+ }
74
+ return dots;
75
+ }, [waveOriginRow, waveOriginCol]);
76
+ const maxDistance = useMemo(() => {
77
+ if (allDots.length === 0)
78
+ return 100;
79
+ return Math.max(...allDots.map((d) => d.distance)) + 5;
80
+ }, [allDots]);
81
+ // Flatten tumbleweed into array of { char, row, col } for animation
82
+ const allChars = useMemo(() => {
83
+ const chars = [];
84
+ TUMBLEWEED.forEach((line, row) => {
85
+ for (let col = 0; col < line.length; col++) {
86
+ const char = line[col];
87
+ if (char !== ' ') {
88
+ chars.push({ char, row, col });
89
+ }
90
+ }
91
+ });
92
+ return chars;
93
+ }, []);
94
+ // Shuffle the characters for random build order
95
+ const shuffledChars = useMemo(() => [...allChars].sort(() => Math.random() - 0.5), [allChars]);
96
+ useEffect(() => {
97
+ if (phase === 'build') {
98
+ if (visibleCount < shuffledChars.length) {
99
+ const timer = setTimeout(() => {
100
+ setVisibleCount((c) => Math.min(c + 2, shuffledChars.length));
101
+ }, 30);
102
+ return () => clearTimeout(timer);
103
+ }
104
+ else {
105
+ // Tumbleweed complete, start pulse
106
+ const timer = setTimeout(() => setPhase('pulse'), 100);
107
+ return () => clearTimeout(timer);
108
+ }
109
+ }
110
+ else if (phase === 'pulse') {
111
+ if (waveDistance < maxDistance) {
112
+ // Advance the wave
113
+ const timer = setTimeout(() => {
114
+ setWaveDistance((d) => d + 3);
115
+ }, 20);
116
+ return () => clearTimeout(timer);
117
+ }
118
+ else {
119
+ // Wave complete
120
+ const timer = setTimeout(onComplete, 200);
121
+ return () => clearTimeout(timer);
122
+ }
123
+ }
124
+ }, [phase, visibleCount, waveDistance, shuffledChars.length, maxDistance, onComplete]);
125
+ // Build the current visible state of the tumbleweed
126
+ const visibleSet = new Set(shuffledChars.slice(0, visibleCount).map((c) => `${c.row},${c.col}`));
127
+ const renderedTumbleweed = TUMBLEWEED.map((line, row) => {
128
+ let result = '';
129
+ for (let col = 0; col < line.length; col++) {
130
+ const char = line[col];
131
+ if (char === ' ' || visibleSet.has(`${row},${col}`)) {
132
+ result += char;
133
+ }
134
+ else {
135
+ result += ' ';
136
+ }
137
+ }
138
+ return result;
139
+ });
140
+ const topPadding = HEADER_PADDING_TOP + TUMBLEWEED_TOP_PADDING;
141
+ // Render the wave revealing dots
142
+ const renderWave = () => {
143
+ const lines = [];
144
+ const waveWidth = 8;
145
+ // Group dots by row
146
+ const dotsByRow = new Map();
147
+ allDots.forEach((dot) => {
148
+ if (!dotsByRow.has(dot.row)) {
149
+ dotsByRow.set(dot.row, []);
150
+ }
151
+ dotsByRow.get(dot.row).push(dot);
152
+ });
153
+ for (let row = 0; row < terminalHeight; row++) {
154
+ const rowDots = dotsByRow.get(row) || [];
155
+ if (rowDots.length === 0) {
156
+ lines.push(_jsx(Text, { children: ' '.repeat(terminalWidth) }, row));
157
+ continue;
158
+ }
159
+ rowDots.sort((a, b) => a.col - b.col);
160
+ const segments = [];
161
+ let lastCol = 0;
162
+ for (const dot of rowDots) {
163
+ if (dot.distance > waveDistance)
164
+ continue;
165
+ if (dot.col > lastCol) {
166
+ segments.push(_jsx(Text, { children: ' '.repeat(dot.col - lastCol) }, `space-${lastCol}`));
167
+ }
168
+ const atWaveFront = dot.distance >= waveDistance - waveWidth;
169
+ const color = getGradientColor(dot.gradientRow, dot.totalRows);
170
+ segments.push(_jsx(Text, { color: color, bold: atWaveFront, children: "\u00B7" }, `dot-${dot.col}`));
171
+ lastCol = dot.col + 1;
172
+ }
173
+ lines.push(_jsx(Box, { children: segments }, row));
174
+ }
175
+ return lines;
176
+ };
177
+ return (_jsxs(Box, { flexDirection: "column", height: terminalHeight, children: [phase === 'pulse' && (_jsx(Box, { position: "absolute", flexDirection: "column", children: renderWave() })), _jsx(Box, { flexDirection: "column", paddingTop: topPadding, paddingLeft: HEADER_PADDING_LEFT, children: renderedTumbleweed.map((line, i) => (_jsx(Text, { color: TUMBLEWEED_COLOR, bold: true, children: line }, i))) })] }));
178
+ }
package/dist/index.js CHANGED
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { render } from 'ink';
4
- import { execaSync } from 'execa';
5
4
  import { App } from './app.js';
6
5
  import { isInsideYeehawSession, yeehawSessionExists, createYeehawSession, attachToYeehaw, hasTmux, } from './lib/tmux.js';
7
6
  import { ensureConfigDirs } from './lib/config.js';
@@ -33,11 +32,9 @@ function main() {
33
32
  }
34
33
  // We're not inside yeehaw session - need to create/attach
35
34
  if (!yeehawSessionExists()) {
36
- // Create new session with yeehaw running in window 0
35
+ // Create new session with yeehaw running directly in window 0
36
+ // (no shell intermediary - cleaner startup)
37
37
  createYeehawSession();
38
- // Now we need to run yeehaw inside window 0
39
- // Send the command to the window
40
- execaSync('tmux', ['send-keys', '-t', 'yeehaw:0', 'yeehaw', 'Enter']);
41
38
  }
42
39
  // Attach to the yeehaw session
43
40
  // This will exec into tmux and not return
@@ -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
@@ -8,6 +8,8 @@ import { shellEscape } from './shell.js';
8
8
  const __filename = fileURLToPath(import.meta.url);
9
9
  const __dirname = dirname(__filename);
10
10
  const MCP_SERVER_PATH = join(__dirname, '..', 'mcp-server.js');
11
+ // Get the path to the bundled Claude plugin (at package root, sibling to dist/)
12
+ const CLAUDE_PLUGIN_PATH = join(__dirname, '..', '..', 'claude-plugin');
11
13
  export const YEEHAW_SESSION = 'yeehaw';
12
14
  // Remote mode state tracking
13
15
  let remoteWindowIndex = null;
@@ -47,12 +49,14 @@ export function yeehawSessionExists() {
47
49
  export function createYeehawSession() {
48
50
  // Write the tmux config
49
51
  writeTmuxConfig();
50
- // Create the session with window 0 named "yeehaw"
52
+ // Create the session with window 0 named "yeehaw", running yeehaw directly
53
+ // This avoids the visible shell spawn - yeehaw runs immediately in the session
51
54
  execaSync('tmux', [
52
55
  'new-session',
53
56
  '-d',
54
57
  '-s', YEEHAW_SESSION,
55
58
  '-n', 'yeehaw',
59
+ 'yeehaw', // Run yeehaw directly instead of spawning a shell first
56
60
  ]);
57
61
  // Source the config
58
62
  execaSync('tmux', ['source-file', TMUX_CONFIG_PATH]);
@@ -61,6 +65,7 @@ export function createYeehawSession() {
61
65
  }
62
66
  export function setupStatusBarHooks() {
63
67
  // Hide status bar when in window 0, show in other windows
68
+ const statusCheck = 'if-shell -F "#{==:#{window_index},0}" "set status off" "set status on"';
64
69
  try {
65
70
  // Start with status off (we begin in window 0)
66
71
  execaSync('tmux', ['set', '-t', YEEHAW_SESSION, 'status', 'off']);
@@ -68,15 +73,56 @@ export function setupStatusBarHooks() {
68
73
  execaSync('tmux', [
69
74
  'set-hook', '-t', YEEHAW_SESSION,
70
75
  'after-select-window',
71
- 'if-shell -F "#{==:#{window_index},0}" "set status off" "set status on"'
76
+ statusCheck
77
+ ]);
78
+ // Hook for when a window is killed (we might land back on window 0)
79
+ execaSync('tmux', [
80
+ 'set-hook', '-t', YEEHAW_SESSION,
81
+ 'window-unlinked',
82
+ statusCheck
83
+ ]);
84
+ // Hook for when pane focus changes (covers edge cases)
85
+ execaSync('tmux', [
86
+ 'set-hook', '-t', YEEHAW_SESSION,
87
+ 'pane-focus-in',
88
+ statusCheck
89
+ ]);
90
+ // Hook for client attachment (ensure status is correct when reattaching)
91
+ execaSync('tmux', [
92
+ 'set-hook', '-t', YEEHAW_SESSION,
93
+ 'client-attached',
94
+ statusCheck
72
95
  ]);
73
96
  }
74
97
  catch {
75
98
  // Hooks might fail on older tmux versions, not critical
76
99
  }
77
100
  }
101
+ /**
102
+ * Force check and correct the status bar visibility based on current window.
103
+ * Call this when you suspect the status bar might be in the wrong state.
104
+ */
105
+ export function ensureCorrectStatusBar() {
106
+ try {
107
+ // Get current window index
108
+ const result = execaSync('tmux', ['display-message', '-p', '#{window_index}']);
109
+ const windowIndex = parseInt(result.stdout.trim(), 10);
110
+ // Set status based on window
111
+ if (windowIndex === 0) {
112
+ execaSync('tmux', ['set', '-t', YEEHAW_SESSION, 'status', 'off']);
113
+ }
114
+ else {
115
+ execaSync('tmux', ['set', '-t', YEEHAW_SESSION, 'status', 'on']);
116
+ }
117
+ }
118
+ catch {
119
+ // Ignore errors
120
+ }
121
+ }
78
122
  export function attachToYeehaw() {
79
- // This replaces the current process
123
+ // Attach to the existing yeehaw session
124
+ // Note: Multiple terminals attaching to the same session will share the same view
125
+ // (this is a tmux limitation - they'll see the same window selections)
80
126
  spawnSync('tmux', ['attach-session', '-t', YEEHAW_SESSION], {
81
127
  stdio: 'inherit',
82
128
  });
@@ -115,7 +161,8 @@ export function createClaudeWindow(workingDir, windowName) {
115
161
  const allowedTools = YEEHAW_MCP_TOOLS.join(',');
116
162
  // Create new window running claude with yeehaw MCP server (-a appends after current window)
117
163
  // Use shell escaping to safely handle special characters in JSON
118
- const claudeCmd = `claude --mcp-config ${shellEscape(mcpConfig)} --allowedTools ${shellEscape(allowedTools)}`;
164
+ // Include the bundled plugin directory for Yeehaw-specific skills
165
+ const claudeCmd = `claude --mcp-config ${shellEscape(mcpConfig)} --allowedTools ${shellEscape(allowedTools)} --plugin-dir ${shellEscape(CLAUDE_PLUGIN_PATH)}`;
119
166
  execaSync('tmux', [
120
167
  'new-window',
121
168
  '-a',
@@ -130,15 +177,18 @@ export function createClaudeWindow(workingDir, windowName) {
130
177
  ]);
131
178
  return parseInt(result.stdout.trim(), 10);
132
179
  }
133
- export function createShellWindow(workingDir, windowName, shell = '/bin/zsh') {
134
- // Create new window running shell (-a appends after current window)
180
+ export function createShellWindow(workingDir, windowName, shell) {
181
+ // Use the user's configured shell from $SHELL, fallback to /bin/bash
182
+ const userShell = shell || process.env.SHELL || '/bin/bash';
183
+ // Create new window running shell as a login shell (-l) so it loads .bashrc/.bash_profile/.zshrc etc.
184
+ // This ensures PS1 and other environment customizations are loaded
135
185
  execaSync('tmux', [
136
186
  'new-window',
137
187
  '-a',
138
188
  '-t', YEEHAW_SESSION,
139
189
  '-n', windowName,
140
190
  '-c', workingDir,
141
- shell,
191
+ `${userShell} -l`,
142
192
  ]);
143
193
  // Get the window index we just created (new window is now current)
144
194
  const result = execaSync('tmux', [
@@ -3,6 +3,14 @@ export interface UpdateInfo {
3
3
  currentVersion: string;
4
4
  latestVersion: string;
5
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
+ };
6
14
  /**
7
15
  * Check for updates. Returns immediately with cached data if available,
8
16
  * otherwise fetches from npm (with 5s timeout).
@@ -76,6 +76,18 @@ function compareVersions(current, latest) {
76
76
  return -1;
77
77
  return 0;
78
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
+ }
79
91
  /**
80
92
  * Check for updates. Returns immediately with cached data if available,
81
93
  * otherwise fetches from npm (with 5s timeout).
File without changes
package/dist/types.d.ts CHANGED
@@ -20,6 +20,8 @@ export interface Project {
20
20
  path: string;
21
21
  summary?: string;
22
22
  color?: string;
23
+ gradientSpread?: number;
24
+ gradientInverted?: boolean;
23
25
  livestock?: Livestock[];
24
26
  wiki?: WikiSection[];
25
27
  }
@@ -80,10 +82,14 @@ export type AppView = {
80
82
  type: 'livestock';
81
83
  project: Project;
82
84
  livestock: Livestock;
85
+ source: 'project' | 'barn';
86
+ sourceBarn?: Barn;
83
87
  } | {
84
88
  type: 'logs';
85
89
  project: Project;
86
90
  livestock: Livestock;
91
+ source: 'project' | 'barn';
92
+ sourceBarn?: Barn;
87
93
  } | {
88
94
  type: 'night-sky';
89
95
  };
@@ -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,40 +241,40 @@ 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) => {
236
- const project = projects.find((p) => p.name === item.id);
237
- if (project)
238
- onSelectProject(project);
239
- } })) : (_jsx(Text, { dimColor: true, children: "No projects yet" })) }), _jsx(Panel, { title: "Sessions", focused: focusedPanel === 'sessions', width: "60%", hints: sessionHints, children: sessionItems.length > 0 ? (_jsx(List, { items: sessionItems, focused: focusedPanel === 'sessions', 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: [_jsxs(Box, { flexDirection: "column", width: "40%", gap: 1, children: [_jsx(Panel, { title: "Projects", focused: focusedPanel === 'projects', hints: projectHints, children: projectItems.length > 0 ? (_jsx(List, { items: projectItems, focused: focusedPanel === 'projects', onSelect: (item) => {
263
+ const project = projects.find((p) => p.name === item.id);
264
+ if (project)
265
+ onSelectProject(project);
266
+ } })) : (_jsx(Text, { dimColor: true, children: "No projects yet" })) }), _jsx(Box, { flexGrow: 1, width: "100%", children: _jsx(Panel, { title: "Barns", focused: focusedPanel === 'barns', width: "100%", hints: barnHints, children: barnItems.length > 0 ? (_jsx(Box, { flexDirection: "column", children: _jsx(List, { items: barnItems, focused: focusedPanel === 'barns', onSelect: (item) => {
267
+ const barn = barns.find((b) => b.name === item.id);
268
+ if (barn)
269
+ onSelectBarn(barn);
270
+ }, onAction: (item) => {
271
+ // 's' key to SSH directly
272
+ const barn = barns.find((b) => b.name === item.id);
273
+ if (barn)
274
+ onSshToBarn(barn);
275
+ } }) })) : (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "No barns configured" }), _jsx(Text, { dimColor: true, italic: true, children: "Barns are servers you manage" })] })) }) })] }), _jsx(Panel, { title: "Sessions", focused: focusedPanel === 'sessions', width: "60%", hints: sessionHints, children: sessionItems.length > 0 ? (_jsx(List, { items: sessionItems, focused: focusedPanel === 'sessions', onSelect: (item) => {
240
276
  const window = sessionWindows.find((w) => String(w.index) === item.id);
241
277
  if (window)
242
278
  onSelectWindow(window);
243
- } })) : (_jsx(Text, { dimColor: true, children: "No active sessions" })) })] }), _jsx(Box, { paddingX: 1, marginBottom: 1, children: _jsx(Panel, { title: "Barns", focused: focusedPanel === 'barns', hints: barnHints, children: barnItems.length > 0 ? (_jsx(Box, { flexDirection: "column", children: _jsx(List, { items: barnItems, focused: focusedPanel === 'barns', onSelect: (item) => {
244
- const barn = barns.find((b) => b.name === item.id);
245
- if (barn)
246
- onSelectBarn(barn);
247
- }, onAction: (item) => {
248
- // 's' key to SSH directly
249
- const barn = barns.find((b) => b.name === item.id);
250
- if (barn)
251
- onSshToBarn(barn);
252
- } }) })) : (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "No barns configured" }), _jsx(Text, { dimColor: true, italic: true, children: "Barns are servers you manage" })] })) }) })] }));
279
+ } })) : (_jsx(Text, { dimColor: true, children: "No active sessions" })) })] })] }));
253
280
  }
@@ -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;
8
- onUpdateLivestock: (livestock: Livestock) => void;
12
+ onSelectWindow: (window: TmuxWindow) => void;
13
+ onUpdateLivestock: (originalLivestock: Livestock, updatedLivestock: 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 {};