@colmbus72/yeehaw 0.3.0 → 0.4.1

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.
@@ -3,10 +3,13 @@ interface HeaderProps {
3
3
  subtitle?: string;
4
4
  summary?: string;
5
5
  color?: string;
6
+ gradientSpread?: number;
7
+ gradientInverted?: boolean;
8
+ theme?: 'dark' | 'light';
6
9
  versionInfo?: {
7
10
  current: string;
8
11
  latest: string | null;
9
12
  };
10
13
  }
11
- export declare function Header({ text, subtitle, summary, color, versionInfo }: HeaderProps): import("react/jsx-runtime").JSX.Element;
14
+ export declare function Header({ text, subtitle, summary, color, gradientSpread, gradientInverted, theme, versionInfo }: HeaderProps): import("react/jsx-runtime").JSX.Element;
12
15
  export {};
@@ -1,7 +1,22 @@
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
+ // Compare semver versions - returns true if latest > current
6
+ function isNewerVersion(latest, current) {
7
+ const parseVersion = (v) => v.split('.').map(n => parseInt(n, 10) || 0);
8
+ const [lMajor, lMinor, lPatch] = parseVersion(latest);
9
+ const [cMajor, cMinor, cPatch] = parseVersion(current);
10
+ if (lMajor > cMajor)
11
+ return true;
12
+ if (lMajor < cMajor)
13
+ return false;
14
+ if (lMinor > cMinor)
15
+ return true;
16
+ if (lMinor < cMinor)
17
+ return false;
18
+ return lPatch > cPatch;
19
+ }
5
20
  // Convert hex to RGB
6
21
  function hexToRgb(hex) {
7
22
  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
@@ -21,34 +36,43 @@ function interpolateColor(color1, color2, factor) {
21
36
  return `rgb(${r},${g},${b})`;
22
37
  }
23
38
  // Generate gradient colors for each line
24
- function generateGradient(lines, baseColor) {
39
+ function generateGradient(lines, baseColor, spread = 5, inverted = false, theme = 'dark') {
25
40
  const rgb = hexToRgb(baseColor);
26
41
  if (!rgb)
27
42
  return lines.map(() => baseColor);
43
+ // Spread controls how much the gradient changes (0 = no change, 10 = max change)
44
+ // Convert 0-10 scale to a multiplier (0 = 1.0, 10 = 0.1 for darkening factor)
45
+ const spreadFactor = 1 - (spread / 10) * 0.9; // 0->1.0, 5->0.55, 10->0.1
28
46
  // Calculate luminance to detect dark colors
29
47
  const luminance = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255;
30
48
  let startRgb;
31
49
  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;
50
+ // Determine gradient direction based on color luminance and theme
51
+ const isDarkColor = luminance < 0.3;
52
+ const shouldLighten = isDarkColor || theme === 'light';
53
+ if (shouldLighten) {
54
+ // Go from a lighter tint down to the base color (or adjusted end)
55
+ const liftFactor = 1 + (spread / 10) * 2; // More spread = more lift
36
56
  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)),
57
+ r: Math.min(255, Math.round(rgb.r * liftFactor + spread * 8)),
58
+ g: Math.min(255, Math.round(rgb.g * liftFactor + spread * 8)),
59
+ b: Math.min(255, Math.round(rgb.b * liftFactor + spread * 8)),
40
60
  };
41
61
  endRgb = rgb;
42
62
  }
43
63
  else {
44
- // Light/medium color: go from base to a darker version
64
+ // Go from base to a darker version
45
65
  startRgb = rgb;
46
66
  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),
67
+ r: Math.round(rgb.r * spreadFactor),
68
+ g: Math.round(rgb.g * spreadFactor),
69
+ b: Math.round(rgb.b * spreadFactor),
50
70
  };
51
71
  }
72
+ // Invert if requested
73
+ if (inverted) {
74
+ [startRgb, endRgb] = [endRgb, startRgb];
75
+ }
52
76
  return lines.map((_, i) => {
53
77
  const factor = i / Math.max(lines.length - 1, 1);
54
78
  return interpolateColor(startRgb, endRgb, factor);
@@ -63,21 +87,22 @@ const TUMBLEWEED = [
63
87
  ];
64
88
  // Brownish tan color to complement yeehaw gold
65
89
  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
- });
90
+ export function Header({ text, subtitle, summary, color, gradientSpread, gradientInverted, theme, versionInfo }) {
91
+ // Use sync figlet to avoid flash on initial render
92
+ const ascii = useMemo(() => {
93
+ try {
94
+ return figlet.textSync(text.toUpperCase(), { font: 'ANSI Shadow' });
95
+ }
96
+ catch {
97
+ return text.toUpperCase();
98
+ }
74
99
  }, [text]);
75
100
  const lines = ascii.split('\n').filter(line => line.trim() !== '');
76
101
  const baseColor = color || '#f0c040'; // Default yeehaw gold
77
- const gradientColors = generateGradient(lines, baseColor);
102
+ const gradientColors = generateGradient(lines, baseColor, gradientSpread ?? 5, gradientInverted ?? false, theme ?? 'dark');
78
103
  // Show tumbleweed only for the main "yeehaw" title
79
104
  const showTumbleweed = text.toLowerCase() === 'yeehaw';
80
105
  // Vertically center tumbleweed next to ASCII art
81
106
  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] })] }))] }));
107
+ 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 && isNewerVersion(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] })] })) : (_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
108
  }
@@ -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',
@@ -321,7 +321,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
321
321
  // Wiki tools
322
322
  {
323
323
  name: 'get_wiki',
324
- description: 'Get all wiki sections for a project',
324
+ description: 'Get all wiki section titles for a project (use get_wiki_section to fetch content)',
325
325
  inputSchema: {
326
326
  type: 'object',
327
327
  properties: {
@@ -330,6 +330,18 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
330
330
  required: ['project'],
331
331
  },
332
332
  },
333
+ {
334
+ name: 'get_wiki_section',
335
+ description: 'Get the content of a specific wiki section',
336
+ inputSchema: {
337
+ type: 'object',
338
+ properties: {
339
+ project: { type: 'string', description: 'Project name' },
340
+ title: { type: 'string', description: 'Section title' },
341
+ },
342
+ required: ['project', 'title'],
343
+ },
344
+ },
333
345
  {
334
346
  name: 'add_wiki_section',
335
347
  description: 'Add a new wiki section to a project',
@@ -379,11 +391,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
379
391
  // Project operations
380
392
  case 'list_projects': {
381
393
  const projects = loadProjects();
394
+ // Return simplified project data to reduce context usage
395
+ const simplified = projects.map((p) => ({
396
+ name: p.name,
397
+ path: p.path,
398
+ summary: p.summary,
399
+ color: p.color,
400
+ livestock: (p.livestock || []).map((l) => ({
401
+ name: l.name,
402
+ path: l.path,
403
+ barn: l.barn,
404
+ })),
405
+ }));
382
406
  return {
383
407
  content: [
384
408
  {
385
409
  type: 'text',
386
- text: JSON.stringify(projects, null, 2),
410
+ text: JSON.stringify(simplified, null, 2),
387
411
  },
388
412
  ],
389
413
  };
@@ -397,8 +421,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
397
421
  isError: true,
398
422
  };
399
423
  }
424
+ // Return project with wiki section titles only (not full content)
425
+ // Use get_wiki_section to fetch individual section content
426
+ const projectWithWikiTitles = {
427
+ ...project,
428
+ wiki: (project.wiki || []).map((s) => ({ title: s.title })),
429
+ };
400
430
  return {
401
- content: [{ type: 'text', text: JSON.stringify(project, null, 2) }],
431
+ content: [{ type: 'text', text: JSON.stringify(projectWithWikiTitles, null, 2) }],
402
432
  };
403
433
  }
404
434
  case 'create_project': {
@@ -712,8 +742,31 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
712
742
  isError: true,
713
743
  };
714
744
  }
745
+ // Return only section titles to reduce context usage
746
+ const titles = (project.wiki || []).map((s) => ({ title: s.title }));
747
+ return {
748
+ content: [{ type: 'text', text: JSON.stringify(titles, null, 2) }],
749
+ };
750
+ }
751
+ case 'get_wiki_section': {
752
+ const projectName = requireString(args, 'project');
753
+ const title = requireString(args, 'title');
754
+ const project = loadProject(projectName);
755
+ if (!project) {
756
+ return {
757
+ content: [{ type: 'text', text: `Project not found: ${projectName}` }],
758
+ isError: true,
759
+ };
760
+ }
761
+ const section = (project.wiki || []).find((s) => s.title === title);
762
+ if (!section) {
763
+ return {
764
+ content: [{ type: 'text', text: `Wiki section not found: ${title}` }],
765
+ isError: true,
766
+ };
767
+ }
715
768
  return {
716
- content: [{ type: 'text', text: JSON.stringify(project.wiki || [], null, 2) }],
769
+ content: [{ type: 'text', text: JSON.stringify(section, null, 2) }],
717
770
  };
718
771
  }
719
772
  case 'add_wiki_section': {
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
  }
@@ -15,6 +15,7 @@ interface GlobalDashboardProps {
15
15
  onCreateProject: (name: string, path: string) => void;
16
16
  onCreateBarn: (barn: Barn) => void;
17
17
  onSshToBarn: (barn: Barn) => void;
18
+ onInputModeChange?: (isInputMode: boolean) => void;
18
19
  }
19
- export declare function GlobalDashboard({ projects, barns, windows, versionInfo, onSelectProject, onSelectBarn, onSelectWindow, onNewClaude, onCreateProject, onCreateBarn, onSshToBarn, }: GlobalDashboardProps): import("react/jsx-runtime").JSX.Element;
20
+ export declare function GlobalDashboard({ projects, barns, windows, versionInfo, onSelectProject, onSelectBarn, onSelectWindow, onNewClaude, onCreateProject, onCreateBarn, onSshToBarn, onInputModeChange, }: GlobalDashboardProps): import("react/jsx-runtime").JSX.Element;
20
21
  export {};
@@ -12,9 +12,14 @@ import { isLocalBarn } from '../lib/config.js';
12
12
  function countSessionsForProject(projectName, windows) {
13
13
  return windows.filter((w) => w.name.startsWith(projectName)).length;
14
14
  }
15
- export function GlobalDashboard({ projects, barns, windows, versionInfo, onSelectProject, onSelectBarn, onSelectWindow, onNewClaude, onCreateProject, onCreateBarn, onSshToBarn, }) {
15
+ export function GlobalDashboard({ projects, barns, windows, versionInfo, onSelectProject, onSelectBarn, onSelectWindow, onNewClaude, onCreateProject, onCreateBarn, onSshToBarn, onInputModeChange, }) {
16
16
  const [focusedPanel, setFocusedPanel] = useState('projects');
17
- const [mode, setMode] = useState('normal');
17
+ const [mode, setModeInternal] = useState('normal');
18
+ // Wrapper to notify parent when input mode changes
19
+ const setMode = (newMode) => {
20
+ setModeInternal(newMode);
21
+ onInputModeChange?.(newMode !== 'normal');
22
+ };
18
23
  // New project form state
19
24
  const [newProjectName, setNewProjectName] = useState('');
20
25
  const [newProjectPath, setNewProjectPath] = useState('');
@@ -190,7 +195,7 @@ export function GlobalDashboard({ projects, barns, windows, versionInfo, onSelec
190
195
  if (parts.length >= 2) {
191
196
  const projectName = parts.slice(0, -1).join('-');
192
197
  const livestockName = parts[parts.length - 1];
193
- return { label: `${projectName} / ${livestockName}`, typeHint: 'shell' };
198
+ return { label: `${projectName} · ${livestockName}`, typeHint: 'shell' };
194
199
  }
195
200
  return { label: name, typeHint: '' };
196
201
  };
@@ -259,22 +264,22 @@ export function GlobalDashboard({ projects, barns, windows, versionInfo, onSelec
259
264
  const projectHints = '[n] new';
260
265
  const sessionHints = '1-9 switch';
261
266
  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) => {
267
+ 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) => {
268
+ const project = projects.find((p) => p.name === item.id);
269
+ if (project)
270
+ onSelectProject(project);
271
+ } })) : (_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) => {
272
+ const barn = barns.find((b) => b.name === item.id);
273
+ if (barn)
274
+ onSelectBarn(barn);
275
+ }, onAction: (item) => {
276
+ // 's' key to SSH directly
277
+ const barn = barns.find((b) => b.name === item.id);
278
+ if (barn)
279
+ onSshToBarn(barn);
280
+ } }) })) : (_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
281
  const window = sessionWindows.find((w) => String(w.index) === item.id);
268
282
  if (window)
269
283
  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" })] })) }) })] }));
284
+ } })) : (_jsx(Text, { dimColor: true, children: "No active sessions" })) })] })] }));
280
285
  }
@@ -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 {};