@colmbus72/yeehaw 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude-plugin/.claude-plugin/plugin.json +2 -1
- package/claude-plugin/hooks/hooks.json +41 -0
- package/claude-plugin/hooks/session-status.sh +13 -0
- package/dist/app.js +80 -31
- package/dist/components/HelpOverlay.js +4 -2
- package/dist/components/List.d.ts +8 -1
- package/dist/components/List.js +14 -5
- package/dist/components/Panel.js +27 -1
- package/dist/components/SplashScreen.js +1 -1
- package/dist/hooks/useSessions.js +2 -2
- package/dist/index.js +41 -1
- package/dist/lib/config.d.ts +13 -1
- package/dist/lib/config.js +51 -0
- package/dist/lib/critters.d.ts +28 -0
- package/dist/lib/critters.js +201 -0
- package/dist/lib/hooks.d.ts +20 -0
- package/dist/lib/hooks.js +91 -0
- package/dist/lib/hotkeys.js +24 -20
- package/dist/lib/paths.d.ts +2 -0
- package/dist/lib/paths.js +2 -0
- package/dist/lib/signals.d.ts +30 -0
- package/dist/lib/signals.js +104 -0
- package/dist/lib/tmux.d.ts +8 -2
- package/dist/lib/tmux.js +69 -18
- package/dist/mcp-server.js +161 -1
- package/dist/types.d.ts +4 -2
- package/dist/views/BarnContext.d.ts +4 -2
- package/dist/views/BarnContext.js +79 -20
- package/dist/views/GlobalDashboard.d.ts +2 -2
- package/dist/views/GlobalDashboard.js +20 -18
- package/dist/views/LivestockDetailView.js +11 -7
- package/dist/views/ProjectContext.d.ts +2 -2
- package/dist/views/ProjectContext.js +33 -24
- package/package.json +5 -5
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Yeehaw session status tracking hooks",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"PreToolUse": [
|
|
5
|
+
{
|
|
6
|
+
"matcher": "*",
|
|
7
|
+
"hooks": [
|
|
8
|
+
{
|
|
9
|
+
"type": "command",
|
|
10
|
+
"command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/session-status.sh working",
|
|
11
|
+
"timeout": 5
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"Stop": [
|
|
17
|
+
{
|
|
18
|
+
"matcher": "*",
|
|
19
|
+
"hooks": [
|
|
20
|
+
{
|
|
21
|
+
"type": "command",
|
|
22
|
+
"command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/session-status.sh idle",
|
|
23
|
+
"timeout": 5
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"Notification": [
|
|
29
|
+
{
|
|
30
|
+
"matcher": "idle_prompt",
|
|
31
|
+
"hooks": [
|
|
32
|
+
{
|
|
33
|
+
"type": "command",
|
|
34
|
+
"command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/session-status.sh waiting",
|
|
35
|
+
"timeout": 5
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Yeehaw Session Status Hook
|
|
3
|
+
# Writes session status to signal file for the Yeehaw CLI to read
|
|
4
|
+
|
|
5
|
+
STATUS="$1"
|
|
6
|
+
PANE_ID="${TMUX_PANE:-unknown}"
|
|
7
|
+
SIGNAL_DIR="$HOME/.yeehaw/session-signals"
|
|
8
|
+
SIGNAL_FILE="$SIGNAL_DIR/${PANE_ID//[^a-zA-Z0-9]/_}.json"
|
|
9
|
+
|
|
10
|
+
mkdir -p "$SIGNAL_DIR"
|
|
11
|
+
cat > "$SIGNAL_FILE" << EOF
|
|
12
|
+
{"status":"$STATUS","updated":$(date +%s)}
|
|
13
|
+
EOF
|
package/dist/app.js
CHANGED
|
@@ -17,8 +17,8 @@ import { NightSkyView } from './views/NightSkyView.js';
|
|
|
17
17
|
import { useConfig } from './hooks/useConfig.js';
|
|
18
18
|
import { useSessions } from './hooks/useSessions.js';
|
|
19
19
|
import { useRemoteYeehaw } from './hooks/useRemoteYeehaw.js';
|
|
20
|
-
import { hasTmux, switchToWindow, updateStatusBar, createShellWindow, createSshWindow, detachFromSession, killYeehawSession, enterRemoteMode, ensureCorrectStatusBar, } from './lib/tmux.js';
|
|
21
|
-
import { saveProject, deleteProject, saveBarn, deleteBarn, getLivestockForBarn, isLocalBarn, hasValidSshConfig } from './lib/config.js';
|
|
20
|
+
import { hasTmux, switchToWindow, updateStatusBar, createShellWindow, createSshWindow, detachFromSession, killYeehawSession, restartYeehaw, enterRemoteMode, ensureCorrectStatusBar, } from './lib/tmux.js';
|
|
21
|
+
import { saveProject, deleteProject, saveBarn, deleteBarn, getLivestockForBarn, isLocalBarn, hasValidSshConfig, addCritterToBarn, removeCritterFromBarn } from './lib/config.js';
|
|
22
22
|
import { getVersionInfo } from './lib/update-check.js';
|
|
23
23
|
function getHotkeyScope(view) {
|
|
24
24
|
switch (view.type) {
|
|
@@ -39,37 +39,41 @@ function expandPath(path) {
|
|
|
39
39
|
}
|
|
40
40
|
return path;
|
|
41
41
|
}
|
|
42
|
-
// Global bottom bar items
|
|
42
|
+
// Global bottom bar items - minimal, consistent across all views
|
|
43
43
|
function getBottomBarItems(viewType) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (viewType === 'project') {
|
|
52
|
-
return [...common, { key: 'c', label: 'claude' }, { key: 'w', label: 'wiki' }, { key: 'i', label: 'issues' }, { key: 'Esc', label: 'back' }];
|
|
53
|
-
}
|
|
54
|
-
if (viewType === 'barn') {
|
|
55
|
-
return [...common, { key: 's', label: 'shell' }, { key: 'Esc', label: 'back' }];
|
|
56
|
-
}
|
|
57
|
-
if (viewType === 'wiki') {
|
|
58
|
-
return [...common, { key: 'Esc', label: 'back' }];
|
|
44
|
+
// Night sky has its own unique actions
|
|
45
|
+
if (viewType === 'night-sky') {
|
|
46
|
+
return [
|
|
47
|
+
{ key: 'c', label: 'cloud' },
|
|
48
|
+
{ key: 'r', label: 'randomize' },
|
|
49
|
+
{ key: 'Esc', label: 'exit' },
|
|
50
|
+
];
|
|
59
51
|
}
|
|
60
|
-
|
|
61
|
-
|
|
52
|
+
// Global dashboard: exit options
|
|
53
|
+
if (viewType === 'global') {
|
|
54
|
+
return [
|
|
55
|
+
{ key: 'q', label: 'detach' },
|
|
56
|
+
{ key: 'Q', label: 'quit' },
|
|
57
|
+
{ key: 'Tab', label: '' },
|
|
58
|
+
{ key: '?', label: 'help' },
|
|
59
|
+
];
|
|
62
60
|
}
|
|
61
|
+
// Livestock detail: page-level actions
|
|
63
62
|
if (viewType === 'livestock') {
|
|
64
|
-
return [
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
63
|
+
return [
|
|
64
|
+
{ key: 's', label: 'shell' },
|
|
65
|
+
{ key: 'l', label: 'logs' },
|
|
66
|
+
{ key: 'e', label: 'edit' },
|
|
67
|
+
{ key: 'Esc', label: 'back' },
|
|
68
|
+
{ key: '?', label: 'help' },
|
|
69
|
+
];
|
|
71
70
|
}
|
|
72
|
-
|
|
71
|
+
// All other views: back + help
|
|
72
|
+
return [
|
|
73
|
+
{ key: 'Esc', label: 'back' },
|
|
74
|
+
{ key: 'Tab', label: '' },
|
|
75
|
+
{ key: '?', label: 'help' },
|
|
76
|
+
];
|
|
73
77
|
}
|
|
74
78
|
export function App() {
|
|
75
79
|
const { exit } = useApp();
|
|
@@ -158,6 +162,38 @@ export function App() {
|
|
|
158
162
|
setError(`Failed to create Claude session: ${message}`);
|
|
159
163
|
}
|
|
160
164
|
}, [tmuxAvailable, view, createClaude]);
|
|
165
|
+
const handleNewClaudeForProject = useCallback((project) => {
|
|
166
|
+
if (!tmuxAvailable) {
|
|
167
|
+
setError('tmux is not installed');
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
const workingDir = expandPath(project.path);
|
|
172
|
+
const windowName = `${project.name}-claude`;
|
|
173
|
+
const windowIndex = createClaude(workingDir, windowName);
|
|
174
|
+
switchToWindow(windowIndex);
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
178
|
+
setError(`Failed to create Claude session: ${message}`);
|
|
179
|
+
}
|
|
180
|
+
}, [tmuxAvailable, createClaude]);
|
|
181
|
+
const handleNewClaudeForLivestock = useCallback((livestock, projectName) => {
|
|
182
|
+
if (!tmuxAvailable) {
|
|
183
|
+
setError('tmux is not installed');
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
const workingDir = expandPath(livestock.path);
|
|
188
|
+
const windowName = `${projectName}-${livestock.name}-claude`;
|
|
189
|
+
const windowIndex = createClaude(workingDir, windowName);
|
|
190
|
+
switchToWindow(windowIndex);
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
194
|
+
setError(`Failed to create Claude session: ${message}`);
|
|
195
|
+
}
|
|
196
|
+
}, [tmuxAvailable, createClaude]);
|
|
161
197
|
const handleOpenLivestockSession = useCallback((livestock, barn, projectName) => {
|
|
162
198
|
if (!tmuxAvailable) {
|
|
163
199
|
setError('tmux is not installed');
|
|
@@ -353,6 +389,11 @@ export function App() {
|
|
|
353
389
|
exit();
|
|
354
390
|
return;
|
|
355
391
|
}
|
|
392
|
+
// Ctrl-R: Restart Yeehaw (preserves other tmux windows)
|
|
393
|
+
if (key.ctrl && input === 'r') {
|
|
394
|
+
restartYeehaw();
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
356
397
|
// ESC: Navigate back (handled by individual views for their sub-modes,
|
|
357
398
|
// but also handled here as a fallback for consistent navigation)
|
|
358
399
|
if (key.escape) {
|
|
@@ -401,9 +442,9 @@ export function App() {
|
|
|
401
442
|
}
|
|
402
443
|
switch (view.type) {
|
|
403
444
|
case 'global':
|
|
404
|
-
return (_jsx(GlobalDashboard, { projects: projects, barns: barns, windows: windows, versionInfo: versionInfo, onSelectProject: handleSelectProject, onSelectBarn: handleSelectBarn, onSelectWindow: handleSelectWindow,
|
|
445
|
+
return (_jsx(GlobalDashboard, { projects: projects, barns: barns, windows: windows, versionInfo: versionInfo, onSelectProject: handleSelectProject, onSelectBarn: handleSelectBarn, onSelectWindow: handleSelectWindow, onNewClaudeForProject: handleNewClaudeForProject, onCreateProject: handleCreateProject, onCreateBarn: handleCreateBarn, onSshToBarn: handleSshToBarn, onInputModeChange: setIsChildInputMode }));
|
|
405
446
|
case 'project':
|
|
406
|
-
return (_jsx(ProjectContext, { project: view.project, barns: barns, windows: windows, onBack: handleBack,
|
|
447
|
+
return (_jsx(ProjectContext, { project: view.project, barns: barns, windows: windows, onBack: handleBack, onNewClaudeForLivestock: (livestock) => handleNewClaudeForLivestock(livestock, view.project.name), onSelectWindow: handleSelectWindow, onSelectLivestock: (livestock, barn) => handleOpenLivestockDetail(view.project, livestock, 'project'), onOpenLivestockSession: (livestock, barn) => handleOpenLivestockSession(livestock, barn, view.project.name), onUpdateProject: handleUpdateProject, onDeleteProject: handleDeleteProject, onOpenWiki: () => handleOpenWiki(view.project), onOpenIssues: () => handleOpenIssues(view.project) }));
|
|
407
448
|
case 'barn':
|
|
408
449
|
const barnLivestock = getLivestockForBarn(view.barn.name);
|
|
409
450
|
return (_jsx(BarnContext, { barn: view.barn, livestock: barnLivestock, projects: projects, windows: windows, onBack: handleBack, onSshToBarn: () => handleSshToBarn(view.barn), onSelectLivestock: (project, livestock) => handleOpenLivestockDetail(project, livestock, 'barn', view.barn), onOpenLivestockSession: (project, livestock) => {
|
|
@@ -428,6 +469,12 @@ export function App() {
|
|
|
428
469
|
const updatedProject = { ...project, livestock: updatedLivestock };
|
|
429
470
|
saveProject(updatedProject);
|
|
430
471
|
reload();
|
|
472
|
+
}, onAddCritter: (critter) => {
|
|
473
|
+
addCritterToBarn(view.barn.name, critter);
|
|
474
|
+
reload();
|
|
475
|
+
}, onRemoveCritter: (critterName) => {
|
|
476
|
+
removeCritterFromBarn(view.barn.name, critterName);
|
|
477
|
+
reload();
|
|
431
478
|
} }));
|
|
432
479
|
case 'wiki':
|
|
433
480
|
return (_jsx(WikiView, { project: view.project, onBack: () => handleBackFromSubview(view.project), onUpdateProject: handleUpdateProject }));
|
|
@@ -447,9 +494,11 @@ export function App() {
|
|
|
447
494
|
return (_jsx(NightSkyView, { onExit: handleExitNightSky }));
|
|
448
495
|
}
|
|
449
496
|
};
|
|
497
|
+
// Memoized callback for splash screen to prevent unnecessary re-renders
|
|
498
|
+
const handleSplashComplete = useCallback(() => setShowSplash(false), []);
|
|
450
499
|
// Show splash screen on first load
|
|
451
500
|
if (showSplash) {
|
|
452
|
-
return _jsx(SplashScreen, { onComplete:
|
|
501
|
+
return _jsx(SplashScreen, { onComplete: handleSplashComplete });
|
|
453
502
|
}
|
|
454
503
|
return (_jsxs(Box, { flexDirection: "column", height: terminalHeight, children: [error && (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: "red", children: ["Error: ", error] }) })), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: renderView() }), !showHelp && (_jsx(BottomBar, { items: getBottomBarItems(view.type), environments: environments, isDetecting: isDetecting }))] }));
|
|
455
504
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import { getHotkeysGrouped } from '../lib/hotkeys.js';
|
|
4
|
+
// Yeehaw brand gold (darker for light mode readability)
|
|
5
|
+
const BRAND_COLOR = '#d4a020';
|
|
4
6
|
function HotkeyRow({ hotkey }) {
|
|
5
|
-
return (_jsxs(Box, { gap: 2, children: [_jsx(Box, { width: 12, children: _jsx(Text, { color:
|
|
7
|
+
return (_jsxs(Box, { gap: 2, children: [_jsx(Box, { width: 12, children: _jsx(Text, { color: BRAND_COLOR, children: hotkey.key }) }), _jsx(Text, { children: hotkey.description })] }));
|
|
6
8
|
}
|
|
7
9
|
function HotkeySection({ title, hotkeys }) {
|
|
8
10
|
if (hotkeys.length === 0)
|
|
@@ -13,5 +15,5 @@ export function HelpOverlay({ scope, focusedPanel }) {
|
|
|
13
15
|
const grouped = getHotkeysGrouped(scope, focusedPanel);
|
|
14
16
|
// Also include list navigation if we're in a view with lists
|
|
15
17
|
const listHotkeys = scope !== 'global' ? getHotkeysGrouped('list').navigation : [];
|
|
16
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "double", borderColor:
|
|
18
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "double", borderColor: BRAND_COLOR, paddingX: 2, paddingY: 1, children: [_jsx(Text, { bold: true, color: BRAND_COLOR, children: "Keyboard Shortcuts" }), _jsx(HotkeySection, { title: "Navigation", hotkeys: [...grouped.navigation, ...listHotkeys] }), _jsx(HotkeySection, { title: "Actions", hotkeys: grouped.action }), _jsx(HotkeySection, { title: "System", hotkeys: grouped.system }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press ? to close" }) })] }));
|
|
17
19
|
}
|
|
@@ -1,15 +1,22 @@
|
|
|
1
|
+
import type { SessionStatus } from '../lib/signals.js';
|
|
2
|
+
export interface RowAction {
|
|
3
|
+
key: string;
|
|
4
|
+
label: string;
|
|
5
|
+
}
|
|
1
6
|
export interface ListItem {
|
|
2
7
|
id: string;
|
|
3
8
|
label: string;
|
|
4
9
|
status?: 'active' | 'inactive' | 'error';
|
|
5
10
|
meta?: string;
|
|
11
|
+
sessionStatus?: SessionStatus;
|
|
12
|
+
actions?: RowAction[];
|
|
6
13
|
}
|
|
7
14
|
interface ListProps {
|
|
8
15
|
items: ListItem[];
|
|
9
16
|
focused?: boolean;
|
|
10
17
|
selectedIndex?: number;
|
|
11
18
|
onSelect?: (item: ListItem) => void;
|
|
12
|
-
onAction?: (item: ListItem) => void;
|
|
19
|
+
onAction?: (item: ListItem, action: string) => void;
|
|
13
20
|
onHighlight?: (item: ListItem | null) => void;
|
|
14
21
|
onSelectionChange?: (index: number) => void;
|
|
15
22
|
}
|
package/dist/components/List.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useEffect } from 'react';
|
|
3
3
|
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
// Yeehaw brand gold (darker for light mode readability)
|
|
5
|
+
const BRAND_COLOR = '#d4a020';
|
|
4
6
|
export function List({ items, focused = false, selectedIndex: controlledIndex, onSelect, onAction, onHighlight, onSelectionChange }) {
|
|
5
7
|
const [internalIndex, setInternalIndex] = useState(0);
|
|
6
8
|
// Use controlled index if provided, otherwise internal
|
|
@@ -35,8 +37,13 @@ export function List({ items, focused = false, selectedIndex: controlledIndex, o
|
|
|
35
37
|
if (key.return && items[selectedIndex] && onSelect) {
|
|
36
38
|
onSelect(items[selectedIndex]);
|
|
37
39
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
// Handle row-level actions (replaces hardcoded 's' check)
|
|
41
|
+
const currentItem = items[selectedIndex];
|
|
42
|
+
if (currentItem?.actions && onAction) {
|
|
43
|
+
const action = currentItem.actions.find(a => a.key === input);
|
|
44
|
+
if (action) {
|
|
45
|
+
onAction(currentItem, action.key);
|
|
46
|
+
}
|
|
40
47
|
}
|
|
41
48
|
});
|
|
42
49
|
if (items.length === 0) {
|
|
@@ -44,10 +51,12 @@ export function List({ items, focused = false, selectedIndex: controlledIndex, o
|
|
|
44
51
|
}
|
|
45
52
|
return (_jsx(Box, { flexDirection: "column", children: items.map((item, index) => {
|
|
46
53
|
const isSelected = index === selectedIndex && focused;
|
|
54
|
+
// Session status takes priority for meta coloring
|
|
55
|
+
const sessionStatusColor = item.sessionStatus === 'waiting' ? 'yellow' :
|
|
56
|
+
item.sessionStatus === 'working' ? 'cyan' :
|
|
57
|
+
item.sessionStatus === 'error' ? 'red' : undefined;
|
|
47
58
|
const statusColor = item.status === 'active' ? 'green' :
|
|
48
59
|
item.status === 'error' ? 'red' : 'gray';
|
|
49
|
-
|
|
50
|
-
const selectionColor = '#f0c040';
|
|
51
|
-
return (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: isSelected ? selectionColor : undefined, children: isSelected ? '›' : ' ' }, "arrow"), _jsx(Text, { color: isSelected ? selectionColor : undefined, bold: isSelected, children: item.label }, "label"), item.status && (_jsx(Text, { color: statusColor, children: "\u25CF" }, "status")), item.meta && (_jsx(Text, { dimColor: true, children: item.meta }, "meta"))] }, item.id));
|
|
60
|
+
return (_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Box, { flexShrink: 0, width: 2, children: _jsx(Text, { color: isSelected ? BRAND_COLOR : undefined, children: isSelected ? '›' : ' ' }) }), _jsxs(Box, { gap: 1, flexShrink: 1, flexGrow: 1, overflow: "hidden", children: [_jsx(Text, { color: isSelected ? BRAND_COLOR : undefined, bold: isSelected, wrap: "truncate", children: item.label }), item.status && (_jsx(Text, { color: statusColor, children: "\u25CF" })), item.meta && (_jsx(Text, { color: sessionStatusColor, dimColor: !sessionStatusColor, wrap: "truncate", children: item.meta }))] }), isSelected && item.actions && item.actions.length > 0 && (_jsx(Box, { gap: 2, flexShrink: 0, marginLeft: 1, children: item.actions.map((action) => (_jsxs(Text, { children: [_jsxs(Text, { color: BRAND_COLOR, children: ["[", action.key, "]"] }), _jsxs(Text, { dimColor: true, children: [" ", action.label] })] }, action.key))) }))] }, item.id));
|
|
52
61
|
}) }));
|
|
53
62
|
}
|
package/dist/components/Panel.js
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
+
// Yeehaw brand gold (darker for light mode readability)
|
|
4
|
+
const BRAND_COLOR = '#d4a020';
|
|
5
|
+
// Render hints with gold keys and gray labels: "[n] new" -> gold "[n]" + gray " new"
|
|
6
|
+
function renderHints(hints) {
|
|
7
|
+
const parts = [];
|
|
8
|
+
// Match [key] patterns and split around them
|
|
9
|
+
const regex = /(\[[^\]]+\])/g;
|
|
10
|
+
let lastIndex = 0;
|
|
11
|
+
let match;
|
|
12
|
+
let keyIndex = 0;
|
|
13
|
+
while ((match = regex.exec(hints)) !== null) {
|
|
14
|
+
// Add text before the match (gray)
|
|
15
|
+
if (match.index > lastIndex) {
|
|
16
|
+
parts.push(_jsx(Text, { dimColor: true, children: hints.slice(lastIndex, match.index) }, `text-${keyIndex}`));
|
|
17
|
+
}
|
|
18
|
+
// Add the [key] part (gold)
|
|
19
|
+
parts.push(_jsx(Text, { color: BRAND_COLOR, children: match[1] }, `key-${keyIndex}`));
|
|
20
|
+
lastIndex = regex.lastIndex;
|
|
21
|
+
keyIndex++;
|
|
22
|
+
}
|
|
23
|
+
// Add remaining text after last match (gray)
|
|
24
|
+
if (lastIndex < hints.length) {
|
|
25
|
+
parts.push(_jsx(Text, { dimColor: true, children: hints.slice(lastIndex) }, `text-end`));
|
|
26
|
+
}
|
|
27
|
+
return parts;
|
|
28
|
+
}
|
|
3
29
|
export function Panel({ title, children, focused = false, width, hints }) {
|
|
4
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: focused ?
|
|
30
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: focused ? BRAND_COLOR : 'gray', width: width, children: [_jsx(Box, { paddingX: 1, marginBottom: 0, children: _jsx(Text, { bold: true, color: focused ? BRAND_COLOR : 'gray', children: title }) }), _jsx(Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: children }), _jsx(Box, { paddingX: 1, justifyContent: "flex-end", height: 1, children: focused && hints ? (_jsx(Text, { children: renderHints(hints) })) : (_jsx(Text, { children: " " })) })] }));
|
|
5
31
|
}
|
|
@@ -10,7 +10,7 @@ const TUMBLEWEED = [
|
|
|
10
10
|
' ░▒░ ░▒░ ░',
|
|
11
11
|
];
|
|
12
12
|
const TUMBLEWEED_COLOR = '#b8860b';
|
|
13
|
-
const BRAND_COLOR = '#
|
|
13
|
+
const BRAND_COLOR = '#d4a020'; // Darker for light mode readability
|
|
14
14
|
// Match Header.tsx positioning exactly
|
|
15
15
|
const HEADER_PADDING_TOP = 1;
|
|
16
16
|
const TUMBLEWEED_TOP_PADDING = 1;
|
|
@@ -18,8 +18,8 @@ export function useSessions() {
|
|
|
18
18
|
const result = listYeehawWindows();
|
|
19
19
|
setWindows(result);
|
|
20
20
|
setLoading(false);
|
|
21
|
-
// Poll
|
|
22
|
-
const interval = setInterval(refresh,
|
|
21
|
+
// Poll for window updates (without loading state changes)
|
|
22
|
+
const interval = setInterval(refresh, 800);
|
|
23
23
|
return () => clearInterval(interval);
|
|
24
24
|
}, [refresh]);
|
|
25
25
|
const createClaude = useCallback((workingDir, name) => {
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,43 @@ import { App } from './app.js';
|
|
|
5
5
|
import { isInsideYeehawSession, yeehawSessionExists, createYeehawSession, attachToYeehaw, hasTmux, } from './lib/tmux.js';
|
|
6
6
|
import { ensureConfigDirs } from './lib/config.js';
|
|
7
7
|
import { checkForUpdates, formatUpdateMessage } from './lib/update-check.js';
|
|
8
|
+
import { installHookScript, getClaudeHooksConfig, checkClaudeHooksInstalled } from './lib/hooks.js';
|
|
9
|
+
/**
|
|
10
|
+
* Handle CLI subcommands
|
|
11
|
+
*/
|
|
12
|
+
function handleSubcommands() {
|
|
13
|
+
const args = process.argv.slice(2);
|
|
14
|
+
if (args[0] === 'hooks' && args[1] === 'install') {
|
|
15
|
+
const scriptPath = installHookScript();
|
|
16
|
+
console.log(`\x1b[32m✓\x1b[0m Hook script installed: ${scriptPath}`);
|
|
17
|
+
console.log('');
|
|
18
|
+
console.log('\x1b[33mNote:\x1b[0m Claude sessions started from Yeehaw already have hooks enabled.');
|
|
19
|
+
console.log('This command is only needed for Claude sessions started outside Yeehaw.');
|
|
20
|
+
if (checkClaudeHooksInstalled()) {
|
|
21
|
+
console.log('\n\x1b[32m✓\x1b[0m Claude hooks already configured in ~/.claude/settings.json');
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
console.log('\nTo enable status tracking for external Claude sessions,');
|
|
25
|
+
console.log('add this to ~/.claude/settings.json:');
|
|
26
|
+
console.log(JSON.stringify(getClaudeHooksConfig(), null, 2));
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
if (args[0] === 'hooks') {
|
|
31
|
+
console.log('Usage: yeehaw hooks install');
|
|
32
|
+
console.log('');
|
|
33
|
+
console.log('Install Claude hooks for session status tracking.');
|
|
34
|
+
console.log('Note: Sessions started from Yeehaw already have hooks enabled automatically.');
|
|
35
|
+
console.log('This is only needed for Claude sessions started outside Yeehaw.');
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
8
40
|
function main() {
|
|
41
|
+
// Handle subcommands first (before tmux checks)
|
|
42
|
+
if (handleSubcommands()) {
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
9
45
|
// Ensure config directories exist
|
|
10
46
|
ensureConfigDirs();
|
|
11
47
|
// Check for updates (non-blocking, uses cache)
|
|
@@ -27,7 +63,11 @@ function main() {
|
|
|
27
63
|
}
|
|
28
64
|
// If we're already inside the yeehaw tmux session, just render the TUI
|
|
29
65
|
if (isInsideYeehawSession()) {
|
|
30
|
-
render(_jsx(App, {})
|
|
66
|
+
render(_jsx(App, {}), {
|
|
67
|
+
patchConsole: true,
|
|
68
|
+
incrementalRendering: true,
|
|
69
|
+
// maxFps: 60,
|
|
70
|
+
});
|
|
31
71
|
return;
|
|
32
72
|
}
|
|
33
73
|
// We're not inside yeehaw session - need to create/attach
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Config, Project, Barn, Livestock } from '../types.js';
|
|
1
|
+
import type { Config, Project, Barn, Livestock, Critter } from '../types.js';
|
|
2
2
|
export declare const LOCAL_BARN: Barn;
|
|
3
3
|
export declare function isLocalBarn(barn: Barn): boolean;
|
|
4
4
|
/**
|
|
@@ -25,3 +25,15 @@ export declare function getLivestockForBarn(barnName: string): Array<{
|
|
|
25
25
|
project: Project;
|
|
26
26
|
livestock: Livestock;
|
|
27
27
|
}>;
|
|
28
|
+
/**
|
|
29
|
+
* Add a critter to a barn
|
|
30
|
+
*/
|
|
31
|
+
export declare function addCritterToBarn(barnName: string, critter: Critter): void;
|
|
32
|
+
/**
|
|
33
|
+
* Remove a critter from a barn
|
|
34
|
+
*/
|
|
35
|
+
export declare function removeCritterFromBarn(barnName: string, critterName: string): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Get a specific critter from a barn
|
|
38
|
+
*/
|
|
39
|
+
export declare function getCritter(barnName: string, critterName: string): Critter | undefined;
|
package/dist/lib/config.js
CHANGED
|
@@ -148,3 +148,54 @@ export function getLivestockForBarn(barnName) {
|
|
|
148
148
|
}
|
|
149
149
|
return result;
|
|
150
150
|
}
|
|
151
|
+
// ============================================================================
|
|
152
|
+
// Critter operations
|
|
153
|
+
// ============================================================================
|
|
154
|
+
/**
|
|
155
|
+
* Add a critter to a barn
|
|
156
|
+
*/
|
|
157
|
+
export function addCritterToBarn(barnName, critter) {
|
|
158
|
+
const barn = loadBarn(barnName);
|
|
159
|
+
if (!barn) {
|
|
160
|
+
throw new Error(`Barn not found: ${barnName}`);
|
|
161
|
+
}
|
|
162
|
+
barn.critters = barn.critters || [];
|
|
163
|
+
// Check for duplicate
|
|
164
|
+
if (barn.critters.some(c => c.name === critter.name)) {
|
|
165
|
+
throw new Error(`Critter "${critter.name}" already exists on barn "${barnName}"`);
|
|
166
|
+
}
|
|
167
|
+
barn.critters.push(critter);
|
|
168
|
+
// Only save to file if it's not the local barn
|
|
169
|
+
if (barnName !== 'local') {
|
|
170
|
+
saveBarn(barn);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Remove a critter from a barn
|
|
175
|
+
*/
|
|
176
|
+
export function removeCritterFromBarn(barnName, critterName) {
|
|
177
|
+
const barn = loadBarn(barnName);
|
|
178
|
+
if (!barn) {
|
|
179
|
+
throw new Error(`Barn not found: ${barnName}`);
|
|
180
|
+
}
|
|
181
|
+
const originalLength = (barn.critters || []).length;
|
|
182
|
+
barn.critters = (barn.critters || []).filter(c => c.name !== critterName);
|
|
183
|
+
if (barn.critters.length === originalLength) {
|
|
184
|
+
return false; // Critter wasn't found
|
|
185
|
+
}
|
|
186
|
+
// Only save to file if it's not the local barn
|
|
187
|
+
if (barnName !== 'local') {
|
|
188
|
+
saveBarn(barn);
|
|
189
|
+
}
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Get a specific critter from a barn
|
|
194
|
+
*/
|
|
195
|
+
export function getCritter(barnName, critterName) {
|
|
196
|
+
const barn = loadBarn(barnName);
|
|
197
|
+
if (!barn) {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
return barn.critters?.find(c => c.name === critterName);
|
|
201
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Critter, Barn } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Discovered critter from scanning a barn
|
|
4
|
+
*/
|
|
5
|
+
export interface DiscoveredCritter {
|
|
6
|
+
service: string;
|
|
7
|
+
suggested_name: string;
|
|
8
|
+
binary?: string;
|
|
9
|
+
config_path?: string;
|
|
10
|
+
status: 'running' | 'stopped';
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Read logs from a critter (via journald or custom path)
|
|
14
|
+
*/
|
|
15
|
+
export declare function readCritterLogs(critter: Critter, barn: Barn, options?: {
|
|
16
|
+
lines?: number;
|
|
17
|
+
pattern?: string;
|
|
18
|
+
}): Promise<{
|
|
19
|
+
content: string;
|
|
20
|
+
error?: string;
|
|
21
|
+
}>;
|
|
22
|
+
/**
|
|
23
|
+
* Discover critters (running services) on a barn
|
|
24
|
+
*/
|
|
25
|
+
export declare function discoverCritters(barn: Barn): Promise<{
|
|
26
|
+
critters: DiscoveredCritter[];
|
|
27
|
+
error?: string;
|
|
28
|
+
}>;
|