@colmbus72/yeehaw 0.3.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.
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useState } from 'react';
2
+ import { useMemo } from 'react';
3
3
  import { Box, Text } from 'ink';
4
4
  import figlet from 'figlet';
5
5
  // Convert hex to RGB
@@ -21,34 +21,43 @@ function interpolateColor(color1, color2, factor) {
21
21
  return `rgb(${r},${g},${b})`;
22
22
  }
23
23
  // Generate gradient colors for each line
24
- function generateGradient(lines, baseColor) {
24
+ function generateGradient(lines, baseColor, spread = 5, inverted = false, theme = 'dark') {
25
25
  const rgb = hexToRgb(baseColor);
26
26
  if (!rgb)
27
27
  return lines.map(() => baseColor);
28
+ // Spread controls how much the gradient changes (0 = no change, 10 = max change)
29
+ // Convert 0-10 scale to a multiplier (0 = 1.0, 10 = 0.1 for darkening factor)
30
+ const spreadFactor = 1 - (spread / 10) * 0.9; // 0->1.0, 5->0.55, 10->0.1
28
31
  // Calculate luminance to detect dark colors
29
32
  const luminance = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255;
30
33
  let startRgb;
31
34
  let endRgb;
32
- if (luminance < 0.3) {
33
- // Dark color: go from a lighter tint down to the base color
34
- // Lift toward white while preserving hue
35
- const liftFactor = 2.5;
35
+ // Determine gradient direction based on color luminance and theme
36
+ const isDarkColor = luminance < 0.3;
37
+ const shouldLighten = isDarkColor || theme === 'light';
38
+ if (shouldLighten) {
39
+ // Go from a lighter tint down to the base color (or adjusted end)
40
+ const liftFactor = 1 + (spread / 10) * 2; // More spread = more lift
36
41
  startRgb = {
37
- r: Math.min(255, Math.round(rgb.r * liftFactor + 40)),
38
- g: Math.min(255, Math.round(rgb.g * liftFactor + 40)),
39
- b: Math.min(255, Math.round(rgb.b * liftFactor + 40)),
42
+ r: Math.min(255, Math.round(rgb.r * liftFactor + spread * 8)),
43
+ g: Math.min(255, Math.round(rgb.g * liftFactor + spread * 8)),
44
+ b: Math.min(255, Math.round(rgb.b * liftFactor + spread * 8)),
40
45
  };
41
46
  endRgb = rgb;
42
47
  }
43
48
  else {
44
- // Light/medium color: go from base to a darker version
49
+ // Go from base to a darker version
45
50
  startRgb = rgb;
46
51
  endRgb = {
47
- r: Math.round(rgb.r * 0.3),
48
- g: Math.round(rgb.g * 0.3),
49
- b: Math.round(rgb.b * 0.3),
52
+ r: Math.round(rgb.r * spreadFactor),
53
+ g: Math.round(rgb.g * spreadFactor),
54
+ b: Math.round(rgb.b * spreadFactor),
50
55
  };
51
56
  }
57
+ // Invert if requested
58
+ if (inverted) {
59
+ [startRgb, endRgb] = [endRgb, startRgb];
60
+ }
52
61
  return lines.map((_, i) => {
53
62
  const factor = i / Math.max(lines.length - 1, 1);
54
63
  return interpolateColor(startRgb, endRgb, factor);
@@ -63,21 +72,22 @@ const TUMBLEWEED = [
63
72
  ];
64
73
  // Brownish tan color to complement yeehaw gold
65
74
  const TUMBLEWEED_COLOR = '#b8860b';
66
- export function Header({ text, subtitle, summary, color, versionInfo }) {
67
- const [ascii, setAscii] = useState('');
68
- useEffect(() => {
69
- figlet.text(text.toUpperCase(), { font: 'ANSI Shadow' }, (err, result) => {
70
- if (!err && result) {
71
- setAscii(result);
72
- }
73
- });
75
+ export function Header({ text, subtitle, summary, color, gradientSpread, gradientInverted, theme, versionInfo }) {
76
+ // Use sync figlet to avoid flash on initial render
77
+ const ascii = useMemo(() => {
78
+ try {
79
+ return figlet.textSync(text.toUpperCase(), { font: 'ANSI Shadow' });
80
+ }
81
+ catch {
82
+ return text.toUpperCase();
83
+ }
74
84
  }, [text]);
75
85
  const lines = ascii.split('\n').filter(line => line.trim() !== '');
76
86
  const baseColor = color || '#f0c040'; // Default yeehaw gold
77
- const gradientColors = generateGradient(lines, baseColor);
87
+ const gradientColors = generateGradient(lines, baseColor, gradientSpread ?? 5, gradientInverted ?? false, theme ?? 'dark');
78
88
  // Show tumbleweed only for the main "yeehaw" title
79
89
  const showTumbleweed = text.toLowerCase() === 'yeehaw';
80
90
  // Vertically center tumbleweed next to ASCII art
81
91
  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))) }), 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] })] }))] }));
92
+ 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, bold: true, 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
93
  }
@@ -20,11 +20,11 @@ const COW_TEMPLATE = [
20
20
  " /{_\\_/ `'\\____",
21
21
  " \\___ (o) (o }",
22
22
  " _______________________/ :--'",
23
- " ,-,'`@@@@@@@@ @@@@ \\_ `__\\",
24
- " ;:( @@@@@@@@@ @@ \\___(o'o)",
25
- " :: ) @@@@ @@@ ,'@@( `===='",
26
- " :: \\ @@@@@: @@@@) ( '@@@'",
27
- " ;; /\\ /`, @@@@@\\ :@@@@@)",
23
+ " ,-,'`@@@@@@@@@@@@@@@@@@@@@@ \\_ `__\\",
24
+ " ;:( @@@@@@@@@@@@@@@@@@@@@@@@ \\___(o'o)",
25
+ " :: ) @@@@@@@@@@@@@@@@@@@@@@@,'@@( `===='",
26
+ " :: \\ @@@@@@: @@@@@@@) @@ ( '@@@'",
27
+ " ;; /\\ @@@ /`, @@@@@\\ :@@@@@)",
28
28
  " ::/ ) {_----------: :~`,~~;",
29
29
  " ;;'`; : ) : / `; ;",
30
30
  "`'`' / : : : : : :",
@@ -118,5 +118,5 @@ export function LivestockHeader({ project, livestock }) {
118
118
  // We'll show it to the right of the cow
119
119
  const cowHeight = cowArt.length;
120
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] }))] })] }) }));
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, bold: true, 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
122
  }
@@ -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
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]);
@@ -157,7 +161,8 @@ export function createClaudeWindow(workingDir, windowName) {
157
161
  const allowedTools = YEEHAW_MCP_TOOLS.join(',');
158
162
  // Create new window running claude with yeehaw MCP server (-a appends after current window)
159
163
  // Use shell escaping to safely handle special characters in JSON
160
- 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)}`;
161
166
  execaSync('tmux', [
162
167
  'new-window',
163
168
  '-a',
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
  }
@@ -190,7 +190,7 @@ export function GlobalDashboard({ projects, barns, windows, versionInfo, onSelec
190
190
  if (parts.length >= 2) {
191
191
  const projectName = parts.slice(0, -1).join('-');
192
192
  const livestockName = parts[parts.length - 1];
193
- return { label: `${projectName} / ${livestockName}`, typeHint: 'shell' };
193
+ return { label: `${projectName} · ${livestockName}`, typeHint: 'shell' };
194
194
  }
195
195
  return { label: name, typeHint: '' };
196
196
  };
@@ -259,22 +259,22 @@ export function GlobalDashboard({ projects, barns, windows, versionInfo, onSelec
259
259
  const projectHints = '[n] new';
260
260
  const sessionHints = '1-9 switch';
261
261
  const barnHints = '[n] new [s] shell';
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) => {
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(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) => {
267
276
  const window = sessionWindows.find((w) => String(w.index) === item.id);
268
277
  if (window)
269
278
  onSelectWindow(window);
270
- } })) : (_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) => {
271
- const barn = barns.find((b) => b.name === item.id);
272
- if (barn)
273
- onSelectBarn(barn);
274
- }, onAction: (item) => {
275
- // 's' key to SSH directly
276
- const barn = barns.find((b) => b.name === item.id);
277
- if (barn)
278
- onSshToBarn(barn);
279
- } }) })) : (_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" })) })] })] }));
280
280
  }
@@ -10,7 +10,7 @@ interface LivestockDetailViewProps {
10
10
  onOpenLogs: () => void;
11
11
  onOpenSession: () => void;
12
12
  onSelectWindow: (window: TmuxWindow) => void;
13
- onUpdateLivestock: (livestock: Livestock) => void;
13
+ onUpdateLivestock: (originalLivestock: Livestock, updatedLivestock: Livestock) => void;
14
14
  }
15
15
  export declare function LivestockDetailView({ project, livestock, source, sourceBarn, windows, onBack, onOpenLogs, onOpenSession, onSelectWindow, onUpdateLivestock, }: LivestockDetailViewProps): import("react/jsx-runtime").JSX.Element;
16
16
  export {};
@@ -37,29 +37,18 @@ export function LivestockDetailView({ project, livestock, source, sourceBarn, wi
37
37
  setEditLogPath(livestock.log_path || '');
38
38
  setEditEnvPath(livestock.env_path || '');
39
39
  };
40
- const saveEdit = (field, value) => {
41
- const updated = { ...livestock };
42
- switch (field) {
43
- case 'name':
44
- updated.name = value;
45
- break;
46
- case 'path':
47
- updated.path = value;
48
- break;
49
- case 'repo':
50
- updated.repo = value || undefined;
51
- break;
52
- case 'branch':
53
- updated.branch = value || undefined;
54
- break;
55
- case 'log_path':
56
- updated.log_path = value || undefined;
57
- break;
58
- case 'env_path':
59
- updated.env_path = value || undefined;
60
- break;
61
- }
62
- onUpdateLivestock(updated);
40
+ // Save all pending changes at once
41
+ const saveAllChanges = () => {
42
+ const updated = {
43
+ ...livestock,
44
+ name: editName.trim() || livestock.name,
45
+ path: editPath.trim() || livestock.path,
46
+ repo: editRepo.trim() || undefined,
47
+ branch: editBranch.trim() || undefined,
48
+ log_path: editLogPath.trim() || undefined,
49
+ env_path: editEnvPath.trim() || undefined,
50
+ };
51
+ onUpdateLivestock(livestock, updated);
63
52
  setMode('normal');
64
53
  };
65
54
  useInput((input, key) => {
@@ -74,6 +63,14 @@ export function LivestockDetailView({ project, livestock, source, sourceBarn, wi
74
63
  }
75
64
  return;
76
65
  }
66
+ // Handle Ctrl+S to save and exit from any edit mode
67
+ // Note: Ctrl+S sends ASCII 19 (\x13), not 's'
68
+ if ((key.ctrl && input === 's') || input === '\x13') {
69
+ if (mode !== 'normal') {
70
+ saveAllChanges();
71
+ return;
72
+ }
73
+ }
77
74
  // Only process these in normal mode
78
75
  if (mode !== 'normal')
79
76
  return;
@@ -99,48 +96,33 @@ export function LivestockDetailView({ project, livestock, source, sourceBarn, wi
99
96
  if (mode === 'edit-name') {
100
97
  return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: `Edit: ${livestock.name}`, color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: "Edit Livestock" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Name: " }), _jsx(TextInput, { value: editName, onChange: setEditName, onSubmit: () => {
101
98
  if (editName.trim()) {
102
- saveEdit('name', editName.trim());
103
99
  setMode('edit-path');
104
100
  }
105
- } })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Esc: cancel" }) })] })] }));
101
+ } })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Ctrl+S: save & exit, Esc: cancel" }) })] })] }));
106
102
  }
107
103
  // Edit path
108
104
  if (mode === 'edit-path') {
109
105
  return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: `Edit: ${livestock.name}`, color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: "Edit Livestock" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Path: " }), _jsx(PathInput, { value: editPath, onChange: setEditPath, onSubmit: () => {
110
106
  if (editPath.trim()) {
111
- saveEdit('path', editPath.trim());
112
107
  setMode('edit-repo');
113
108
  }
114
- } })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Tab: autocomplete, Esc: cancel" }) })] })] }));
109
+ } })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next, Tab: autocomplete, Ctrl+S: save & exit, Esc: cancel" }) })] })] }));
115
110
  }
116
111
  // Edit repo
117
112
  if (mode === 'edit-repo') {
118
- return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: `Edit: ${livestock.name}`, color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: "Edit Livestock" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Git Repo (optional): " }), _jsx(TextInput, { value: editRepo, onChange: setEditRepo, onSubmit: () => {
119
- saveEdit('repo', editRepo.trim());
120
- setMode('edit-branch');
121
- } })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Esc: cancel" }) })] })] }));
113
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: `Edit: ${livestock.name}`, color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: "Edit Livestock" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Git Repo (optional): " }), _jsx(TextInput, { value: editRepo, onChange: setEditRepo, onSubmit: () => setMode('edit-branch') })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Ctrl+S: save & exit, Esc: cancel" }) })] })] }));
122
114
  }
123
115
  // Edit branch
124
116
  if (mode === 'edit-branch') {
125
- return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: `Edit: ${livestock.name}`, color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: "Edit Livestock" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Git Branch (optional): " }), _jsx(TextInput, { value: editBranch, onChange: setEditBranch, onSubmit: () => {
126
- saveEdit('branch', editBranch.trim());
127
- setMode('edit-log-path');
128
- } })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Esc: cancel" }) })] })] }));
117
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: `Edit: ${livestock.name}`, color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: "Edit Livestock" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Git Branch (optional): " }), _jsx(TextInput, { value: editBranch, onChange: setEditBranch, onSubmit: () => setMode('edit-log-path') })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Ctrl+S: save & exit, Esc: cancel" }) })] })] }));
129
118
  }
130
119
  // Edit log path
131
120
  if (mode === 'edit-log-path') {
132
- return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: `Edit: ${livestock.name}`, color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: "Edit Livestock" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Log Path (optional, relative): " }), _jsx(TextInput, { value: editLogPath, onChange: setEditLogPath, onSubmit: () => {
133
- saveEdit('log_path', editLogPath.trim());
134
- setMode('edit-env-path');
135
- } })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Esc: cancel" }) })] })] }));
121
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: `Edit: ${livestock.name}`, color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: "Edit Livestock" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Log Path (optional, relative): " }), _jsx(TextInput, { value: editLogPath, onChange: setEditLogPath, onSubmit: () => setMode('edit-env-path') })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: next field, Ctrl+S: save & exit, Esc: cancel" }) })] })] }));
136
122
  }
137
- // Edit env path
123
+ // Edit env path (last field - saves all changes)
138
124
  if (mode === 'edit-env-path') {
139
- return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: `Edit: ${livestock.name}`, color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: "Edit Livestock" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Env Path (optional, relative): " }), _jsx(TextInput, { value: editEnvPath, onChange: setEditEnvPath, onSubmit: () => {
140
- saveEdit('env_path', editEnvPath.trim());
141
- // All done - return to normal mode
142
- setMode('normal');
143
- } })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: save & finish, Esc: cancel" }) })] })] }));
125
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: `Edit: ${livestock.name}`, color: project.color }), _jsxs(Box, { flexDirection: "column", padding: 2, children: [_jsx(Text, { bold: true, color: "yellow", children: "Edit Livestock" }), _jsxs(Box, { marginTop: 1, children: [_jsx(Text, { children: "Env Path (optional, relative): " }), _jsx(TextInput, { value: editEnvPath, onChange: setEditEnvPath, onSubmit: saveAllChanges })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: save & finish, Ctrl+S: save & exit, Esc: cancel" }) })] })] }));
144
126
  }
145
127
  // Filter windows to this livestock (match pattern: projectname-livestockname)
146
128
  const livestockWindowName = `${project.name}-${livestock.name}`;