@colmbus72/yeehaw 0.4.2 → 0.6.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/claude-plugin/skills/yeehaw-development/SKILL.md +70 -0
- package/dist/app.js +228 -28
- package/dist/components/CritterHeader.d.ts +7 -0
- package/dist/components/CritterHeader.js +81 -0
- package/dist/components/HelpOverlay.js +4 -2
- package/dist/components/List.d.ts +10 -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/auth/index.d.ts +2 -0
- package/dist/lib/auth/index.js +3 -0
- package/dist/lib/auth/linear.d.ts +20 -0
- package/dist/lib/auth/linear.js +79 -0
- package/dist/lib/auth/storage.d.ts +12 -0
- package/dist/lib/auth/storage.js +53 -0
- package/dist/lib/config.d.ts +13 -1
- package/dist/lib/config.js +51 -0
- package/dist/lib/context.d.ts +10 -0
- package/dist/lib/context.js +63 -0
- package/dist/lib/critters.d.ts +61 -0
- package/dist/lib/critters.js +365 -0
- package/dist/lib/hooks.d.ts +20 -0
- package/dist/lib/hooks.js +91 -0
- package/dist/lib/hotkeys.d.ts +1 -1
- package/dist/lib/hotkeys.js +28 -20
- package/dist/lib/issues/github.d.ts +11 -0
- package/dist/lib/issues/github.js +154 -0
- package/dist/lib/issues/index.d.ts +14 -0
- package/dist/lib/issues/index.js +27 -0
- package/dist/lib/issues/linear.d.ts +24 -0
- package/dist/lib/issues/linear.js +345 -0
- package/dist/lib/issues/types.d.ts +82 -0
- package/dist/lib/issues/types.js +2 -0
- package/dist/lib/paths.d.ts +3 -0
- package/dist/lib/paths.js +3 -0
- package/dist/lib/signals.d.ts +30 -0
- package/dist/lib/signals.js +104 -0
- package/dist/lib/tmux.d.ts +9 -2
- package/dist/lib/tmux.js +114 -18
- package/dist/mcp-server.js +161 -1
- package/dist/types.d.ts +23 -2
- package/dist/views/BarnContext.d.ts +5 -2
- package/dist/views/BarnContext.js +202 -21
- package/dist/views/CritterDetailView.d.ts +10 -0
- package/dist/views/CritterDetailView.js +117 -0
- package/dist/views/CritterLogsView.d.ts +8 -0
- package/dist/views/CritterLogsView.js +100 -0
- package/dist/views/GlobalDashboard.d.ts +2 -2
- package/dist/views/GlobalDashboard.js +20 -18
- package/dist/views/IssuesView.d.ts +2 -1
- package/dist/views/IssuesView.js +661 -98
- package/dist/views/LivestockDetailView.d.ts +2 -1
- package/dist/views/LivestockDetailView.js +19 -8
- package/dist/views/ProjectContext.d.ts +2 -2
- package/dist/views/ProjectContext.js +68 -25
- 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
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: yeehaw-development
|
|
3
|
+
description: Guide development using Yeehaw's wiki as context source and living documentation. Use when working on any project tracked in Yeehaw - consult wiki before architectural decisions and contribute back when patterns evolve.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Yeehaw Development
|
|
7
|
+
|
|
8
|
+
You're working on a project tracked in Yeehaw. The project wiki contains curated context to guide development - architecture patterns, conventions, gotchas, and common tasks documented by the project owner and evolved through development.
|
|
9
|
+
|
|
10
|
+
## Wiki-First Development
|
|
11
|
+
|
|
12
|
+
Before making architectural decisions or implementing significant features:
|
|
13
|
+
|
|
14
|
+
1. **Check available sections** with `mcp__yeehaw__get_wiki`
|
|
15
|
+
2. **Fetch relevant sections** with `mcp__yeehaw__get_wiki_section`
|
|
16
|
+
3. **Respect documented patterns** - they exist for a reason
|
|
17
|
+
|
|
18
|
+
The wiki is your primary source of project-specific context. It contains knowledge that isn't obvious from the code alone.
|
|
19
|
+
|
|
20
|
+
## When to Consult the Wiki
|
|
21
|
+
|
|
22
|
+
| Task | Check These Sections |
|
|
23
|
+
|------|---------------------|
|
|
24
|
+
| Adding features | Architecture, Conventions, Common Tasks |
|
|
25
|
+
| Debugging issues | Gotchas, Domain Context |
|
|
26
|
+
| Unsure about patterns | Conventions, Architecture |
|
|
27
|
+
| New integrations | Architecture, Gotchas |
|
|
28
|
+
| Understanding domain | Domain Context |
|
|
29
|
+
|
|
30
|
+
If a section exists that's relevant to your task, read it before proceeding.
|
|
31
|
+
|
|
32
|
+
## Proactive Wiki Updates
|
|
33
|
+
|
|
34
|
+
The wiki is a living document. After completing significant work, consider whether the wiki should be updated:
|
|
35
|
+
|
|
36
|
+
**When to update existing sections:**
|
|
37
|
+
- A documented pattern has evolved
|
|
38
|
+
- You discovered nuances worth capturing
|
|
39
|
+
- Steps in Common Tasks have changed
|
|
40
|
+
|
|
41
|
+
**When to add new sections:**
|
|
42
|
+
- Introduced a new architectural pattern
|
|
43
|
+
- Discovered gotchas during debugging
|
|
44
|
+
- Built a feature type that others might repeat
|
|
45
|
+
- Established new conventions
|
|
46
|
+
|
|
47
|
+
**How to update:**
|
|
48
|
+
- `mcp__yeehaw__update_wiki_section` for edits
|
|
49
|
+
- `mcp__yeehaw__add_wiki_section` for new content
|
|
50
|
+
|
|
51
|
+
**Guidelines:**
|
|
52
|
+
- Not every session needs to update the wiki
|
|
53
|
+
- Only update when something genuinely changes project direction or establishes new patterns
|
|
54
|
+
- Keep sections focused and scannable
|
|
55
|
+
- Be specific to THIS codebase, not generic advice
|
|
56
|
+
|
|
57
|
+
## Project Context
|
|
58
|
+
|
|
59
|
+
Use `mcp__yeehaw__get_project` to understand:
|
|
60
|
+
- Project structure and configured paths
|
|
61
|
+
- Deployment environments (livestock)
|
|
62
|
+
- Available wiki sections
|
|
63
|
+
|
|
64
|
+
When spawned from a specific livestock, you may be focused on deployment-specific work - check if there's relevant context in the wiki.
|
|
65
|
+
|
|
66
|
+
## Summary
|
|
67
|
+
|
|
68
|
+
1. **Before major decisions** → check the wiki
|
|
69
|
+
2. **After significant work** → consider updating the wiki
|
|
70
|
+
3. **When unsure** → the wiki likely has guidance
|
package/dist/app.js
CHANGED
|
@@ -13,12 +13,15 @@ import { WikiView } from './views/WikiView.js';
|
|
|
13
13
|
import { IssuesView } from './views/IssuesView.js';
|
|
14
14
|
import { LivestockDetailView } from './views/LivestockDetailView.js';
|
|
15
15
|
import { LogsView } from './views/LogsView.js';
|
|
16
|
+
import { CritterDetailView } from './views/CritterDetailView.js';
|
|
17
|
+
import { CritterLogsView } from './views/CritterLogsView.js';
|
|
16
18
|
import { NightSkyView } from './views/NightSkyView.js';
|
|
17
19
|
import { useConfig } from './hooks/useConfig.js';
|
|
18
20
|
import { useSessions } from './hooks/useSessions.js';
|
|
19
21
|
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';
|
|
22
|
+
import { hasTmux, switchToWindow, updateStatusBar, createShellWindow, createSshWindow, detachFromSession, killYeehawSession, restartYeehaw, enterRemoteMode, ensureCorrectStatusBar, createClaudeWindowWithPrompt, } from './lib/tmux.js';
|
|
23
|
+
import { saveProject, deleteProject, saveBarn, deleteBarn, getLivestockForBarn, isLocalBarn, hasValidSshConfig, addCritterToBarn, removeCritterFromBarn } from './lib/config.js';
|
|
24
|
+
import { buildProjectContext, buildLivestockContext } from './lib/context.js';
|
|
22
25
|
import { getVersionInfo } from './lib/update-check.js';
|
|
23
26
|
function getHotkeyScope(view) {
|
|
24
27
|
switch (view.type) {
|
|
@@ -29,6 +32,8 @@ function getHotkeyScope(view) {
|
|
|
29
32
|
case 'issues': return 'issues-view';
|
|
30
33
|
case 'livestock': return 'livestock-detail';
|
|
31
34
|
case 'logs': return 'logs-view';
|
|
35
|
+
case 'critter': return 'critter-detail';
|
|
36
|
+
case 'critter-logs': return 'critter-logs';
|
|
32
37
|
case 'night-sky': return 'night-sky';
|
|
33
38
|
default: return 'global-dashboard';
|
|
34
39
|
}
|
|
@@ -39,37 +44,105 @@ function expandPath(path) {
|
|
|
39
44
|
}
|
|
40
45
|
return path;
|
|
41
46
|
}
|
|
42
|
-
// Global bottom bar items
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
// Global bottom bar items - minimal, consistent across all views
|
|
48
|
+
// Every hotkey should be visible somewhere in our 3-tier system
|
|
49
|
+
function getBottomBarItems(viewType, options) {
|
|
50
|
+
// Night sky has its own unique actions
|
|
51
|
+
if (viewType === 'night-sky') {
|
|
52
|
+
return [
|
|
53
|
+
{ key: 'c', label: 'cloud' },
|
|
54
|
+
{ key: 'r', label: 'randomize' },
|
|
55
|
+
{ key: 'Esc', label: 'exit' },
|
|
56
|
+
];
|
|
57
|
+
}
|
|
58
|
+
// Global dashboard: exit options + visualizer
|
|
48
59
|
if (viewType === 'global') {
|
|
49
|
-
return [
|
|
60
|
+
return [
|
|
61
|
+
{ key: 'v', label: 'visualizer' },
|
|
62
|
+
{ key: 'q', label: 'detach' },
|
|
63
|
+
{ key: 'Q', label: 'quit' },
|
|
64
|
+
{ key: 'Tab', label: '' },
|
|
65
|
+
{ key: '?', label: 'help' },
|
|
66
|
+
];
|
|
50
67
|
}
|
|
68
|
+
// Project context: page-level actions
|
|
51
69
|
if (viewType === 'project') {
|
|
52
|
-
return [
|
|
70
|
+
return [
|
|
71
|
+
{ key: 'w', label: 'wiki' },
|
|
72
|
+
{ key: 'i', label: 'issues' },
|
|
73
|
+
{ key: 'e', label: 'edit' },
|
|
74
|
+
{ key: 'Esc', label: 'back' },
|
|
75
|
+
{ key: 'Tab', label: '' },
|
|
76
|
+
{ key: '?', label: 'help' },
|
|
77
|
+
];
|
|
53
78
|
}
|
|
79
|
+
// Barn context: page-level actions (edit only for remote barns)
|
|
54
80
|
if (viewType === 'barn') {
|
|
55
|
-
|
|
81
|
+
const items = [];
|
|
82
|
+
if (!options?.isLocalBarn) {
|
|
83
|
+
items.push({ key: 'e', label: 'edit' });
|
|
84
|
+
}
|
|
85
|
+
items.push({ key: 'Esc', label: 'back' }, { key: 'Tab', label: '' }, { key: '?', label: 'help' });
|
|
86
|
+
return items;
|
|
56
87
|
}
|
|
88
|
+
// Wiki view: panel hints handle n/e/d, bottom bar just needs navigation
|
|
57
89
|
if (viewType === 'wiki') {
|
|
58
|
-
return [
|
|
90
|
+
return [
|
|
91
|
+
{ key: 'Esc', label: 'back' },
|
|
92
|
+
{ key: 'Tab', label: '' },
|
|
93
|
+
{ key: '?', label: 'help' },
|
|
94
|
+
];
|
|
59
95
|
}
|
|
96
|
+
// Issues view: page-level actions (f/r are page-level, c/o are row-level shown on selected item)
|
|
60
97
|
if (viewType === 'issues') {
|
|
61
|
-
return [
|
|
98
|
+
return [
|
|
99
|
+
{ key: 'f', label: 'filter' },
|
|
100
|
+
{ key: 'r', label: 'refresh' },
|
|
101
|
+
{ key: 'Esc', label: 'back' },
|
|
102
|
+
{ key: 'Tab', label: '' },
|
|
103
|
+
{ key: '?', label: 'help' },
|
|
104
|
+
];
|
|
62
105
|
}
|
|
106
|
+
// Livestock detail: page-level actions
|
|
107
|
+
// Local livestock gets [c] claude, remote only gets [s] shell
|
|
63
108
|
if (viewType === 'livestock') {
|
|
64
|
-
|
|
109
|
+
const items = [];
|
|
110
|
+
if (options?.isLocalLivestock) {
|
|
111
|
+
items.push({ key: 'c', label: 'claude' });
|
|
112
|
+
}
|
|
113
|
+
items.push({ key: 's', label: 'shell' }, { key: 'l', label: 'logs' }, { key: 'e', label: 'edit' }, { key: 'Esc', label: 'back' }, { key: '?', label: 'help' });
|
|
114
|
+
return items;
|
|
65
115
|
}
|
|
116
|
+
// Logs view: page-level actions
|
|
66
117
|
if (viewType === 'logs') {
|
|
67
|
-
return [
|
|
118
|
+
return [
|
|
119
|
+
{ key: 'r', label: 'refresh' },
|
|
120
|
+
{ key: 'Esc', label: 'back' },
|
|
121
|
+
{ key: '?', label: 'help' },
|
|
122
|
+
];
|
|
68
123
|
}
|
|
69
|
-
|
|
70
|
-
|
|
124
|
+
// Critter detail: page-level actions
|
|
125
|
+
if (viewType === 'critter') {
|
|
126
|
+
return [
|
|
127
|
+
{ key: 'l', label: 'logs' },
|
|
128
|
+
{ key: 'e', label: 'edit' },
|
|
129
|
+
{ key: 'Esc', label: 'back' },
|
|
130
|
+
{ key: '?', label: 'help' },
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
// Critter logs view: page-level actions
|
|
134
|
+
if (viewType === 'critter-logs') {
|
|
135
|
+
return [
|
|
136
|
+
{ key: 'r', label: 'refresh' },
|
|
137
|
+
{ key: 'Esc', label: 'back' },
|
|
138
|
+
{ key: '?', label: 'help' },
|
|
139
|
+
];
|
|
71
140
|
}
|
|
72
|
-
|
|
141
|
+
// Fallback (shouldn't reach here)
|
|
142
|
+
return [
|
|
143
|
+
{ key: 'Esc', label: 'back' },
|
|
144
|
+
{ key: '?', label: 'help' },
|
|
145
|
+
];
|
|
73
146
|
}
|
|
74
147
|
export function App() {
|
|
75
148
|
const { exit } = useApp();
|
|
@@ -123,6 +196,16 @@ export function App() {
|
|
|
123
196
|
return { ...currentView, barn: freshBarn };
|
|
124
197
|
}
|
|
125
198
|
}
|
|
199
|
+
// Update critter detail view
|
|
200
|
+
if (currentView.type === 'critter' || currentView.type === 'critter-logs') {
|
|
201
|
+
const freshBarn = barns.find((b) => b.name === currentView.barn.name);
|
|
202
|
+
if (freshBarn && freshBarn !== currentView.barn) {
|
|
203
|
+
const freshCritter = (freshBarn.critters || []).find((c) => c.name === currentView.critter.name);
|
|
204
|
+
if (freshCritter) {
|
|
205
|
+
return { ...currentView, barn: freshBarn, critter: freshCritter };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
126
209
|
return currentView;
|
|
127
210
|
});
|
|
128
211
|
}, [projects, barns]);
|
|
@@ -150,7 +233,11 @@ export function App() {
|
|
|
150
233
|
const projectName = view.type === 'project' ? view.project.name : 'yeehaw';
|
|
151
234
|
const workingDir = view.type === 'project' ? expandPath(view.project.path) : process.cwd();
|
|
152
235
|
const windowName = `${projectName}-claude`;
|
|
153
|
-
|
|
236
|
+
// Inject context when in a project view
|
|
237
|
+
const context = view.type === 'project' ? buildProjectContext(view.project.name) : null;
|
|
238
|
+
const windowIndex = context
|
|
239
|
+
? createClaudeWindowWithPrompt(workingDir, windowName, context)
|
|
240
|
+
: createClaude(workingDir, windowName);
|
|
154
241
|
switchToWindow(windowIndex);
|
|
155
242
|
}
|
|
156
243
|
catch (err) {
|
|
@@ -158,6 +245,64 @@ export function App() {
|
|
|
158
245
|
setError(`Failed to create Claude session: ${message}`);
|
|
159
246
|
}
|
|
160
247
|
}, [tmuxAvailable, view, createClaude]);
|
|
248
|
+
const handleNewClaudeForProject = useCallback((project) => {
|
|
249
|
+
if (!tmuxAvailable) {
|
|
250
|
+
setError('tmux is not installed');
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
const workingDir = expandPath(project.path);
|
|
255
|
+
const windowName = `${project.name}-claude`;
|
|
256
|
+
const context = buildProjectContext(project.name);
|
|
257
|
+
// Use context injection if we have project context, otherwise fall back to basic
|
|
258
|
+
const windowIndex = context
|
|
259
|
+
? createClaudeWindowWithPrompt(workingDir, windowName, context)
|
|
260
|
+
: createClaude(workingDir, windowName);
|
|
261
|
+
switchToWindow(windowIndex);
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
265
|
+
setError(`Failed to create Claude session: ${message}`);
|
|
266
|
+
}
|
|
267
|
+
}, [tmuxAvailable, createClaude]);
|
|
268
|
+
const handleNewClaudeForLivestock = useCallback((livestock, projectName) => {
|
|
269
|
+
if (!tmuxAvailable) {
|
|
270
|
+
setError('tmux is not installed');
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
const workingDir = expandPath(livestock.path);
|
|
275
|
+
const windowName = `${projectName}-${livestock.name}-claude`;
|
|
276
|
+
const context = buildLivestockContext(projectName, livestock.name);
|
|
277
|
+
// Use context injection if we have project context, otherwise fall back to basic
|
|
278
|
+
const windowIndex = context
|
|
279
|
+
? createClaudeWindowWithPrompt(workingDir, windowName, context)
|
|
280
|
+
: createClaude(workingDir, windowName);
|
|
281
|
+
switchToWindow(windowIndex);
|
|
282
|
+
}
|
|
283
|
+
catch (err) {
|
|
284
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
285
|
+
setError(`Failed to create Claude session: ${message}`);
|
|
286
|
+
}
|
|
287
|
+
}, [tmuxAvailable, createClaude]);
|
|
288
|
+
const handleOpenClaudeWithContext = useCallback((workingDir, issueContext) => {
|
|
289
|
+
if (!tmuxAvailable) {
|
|
290
|
+
setError('tmux is not installed');
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
// Get the project name for the window name
|
|
295
|
+
const projectName = (view.type === 'issues' ? view.project.name : 'issue');
|
|
296
|
+
const windowName = `${projectName}-claude`;
|
|
297
|
+
// Create claude window with system prompt
|
|
298
|
+
const index = createClaudeWindowWithPrompt(workingDir, windowName, issueContext);
|
|
299
|
+
switchToWindow(index);
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
303
|
+
setError(`Failed to create Claude session: ${message}`);
|
|
304
|
+
}
|
|
305
|
+
}, [tmuxAvailable, view]);
|
|
161
306
|
const handleOpenLivestockSession = useCallback((livestock, barn, projectName) => {
|
|
162
307
|
if (!tmuxAvailable) {
|
|
163
308
|
setError('tmux is not installed');
|
|
@@ -240,6 +385,26 @@ export function App() {
|
|
|
240
385
|
// Update the view with the new livestock data, preserving navigation context
|
|
241
386
|
setView({ type: 'livestock', project: updatedProject, livestock: updatedLivestock, source, sourceBarn });
|
|
242
387
|
}, [reload]);
|
|
388
|
+
const handleOpenCritterDetail = useCallback((barn, critter) => {
|
|
389
|
+
setView({ type: 'critter', barn, critter });
|
|
390
|
+
updateStatusBar(`${barn.name} / ${critter.name}`);
|
|
391
|
+
}, []);
|
|
392
|
+
const handleOpenCritterLogs = useCallback((barn, critter) => {
|
|
393
|
+
setView({ type: 'critter-logs', barn, critter });
|
|
394
|
+
updateStatusBar(`${barn.name} / ${critter.name} Logs`);
|
|
395
|
+
}, []);
|
|
396
|
+
const handleBackFromCritter = useCallback((barn) => {
|
|
397
|
+
setView({ type: 'barn', barn });
|
|
398
|
+
updateStatusBar(`Barn: ${barn.name}`);
|
|
399
|
+
}, []);
|
|
400
|
+
const handleUpdateCritter = useCallback((barn, originalCritter, updatedCritter) => {
|
|
401
|
+
// Remove old critter and add updated one
|
|
402
|
+
removeCritterFromBarn(barn.name, originalCritter.name);
|
|
403
|
+
addCritterToBarn(barn.name, updatedCritter);
|
|
404
|
+
reload();
|
|
405
|
+
// Update view with new critter data
|
|
406
|
+
setView({ type: 'critter', barn, critter: updatedCritter });
|
|
407
|
+
}, [reload]);
|
|
243
408
|
const handleDeleteProject = useCallback((projectName) => {
|
|
244
409
|
deleteProject(projectName);
|
|
245
410
|
reload();
|
|
@@ -353,6 +518,11 @@ export function App() {
|
|
|
353
518
|
exit();
|
|
354
519
|
return;
|
|
355
520
|
}
|
|
521
|
+
// Ctrl-R: Restart Yeehaw (preserves other tmux windows)
|
|
522
|
+
if (key.ctrl && input === 'r') {
|
|
523
|
+
restartYeehaw();
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
356
526
|
// ESC: Navigate back (handled by individual views for their sub-modes,
|
|
357
527
|
// but also handled here as a fallback for consistent navigation)
|
|
358
528
|
if (key.escape) {
|
|
@@ -366,6 +536,13 @@ export function App() {
|
|
|
366
536
|
else if (view.type === 'livestock') {
|
|
367
537
|
handleBackFromLivestock(view.source, view.project, view.sourceBarn);
|
|
368
538
|
}
|
|
539
|
+
else if (view.type === 'critter-logs') {
|
|
540
|
+
setView({ type: 'critter', barn: view.barn, critter: view.critter });
|
|
541
|
+
updateStatusBar(`${view.barn.name} / ${view.critter.name}`);
|
|
542
|
+
}
|
|
543
|
+
else if (view.type === 'critter') {
|
|
544
|
+
handleBackFromCritter(view.barn);
|
|
545
|
+
}
|
|
369
546
|
else if (view.type === 'project' || view.type === 'barn') {
|
|
370
547
|
handleBack();
|
|
371
548
|
}
|
|
@@ -401,9 +578,9 @@ export function App() {
|
|
|
401
578
|
}
|
|
402
579
|
switch (view.type) {
|
|
403
580
|
case 'global':
|
|
404
|
-
return (_jsx(GlobalDashboard, { projects: projects, barns: barns, windows: windows, versionInfo: versionInfo, onSelectProject: handleSelectProject, onSelectBarn: handleSelectBarn, onSelectWindow: handleSelectWindow,
|
|
581
|
+
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
582
|
case 'project':
|
|
406
|
-
return (_jsx(ProjectContext, { project: view.project, barns: barns, windows: windows, onBack: handleBack,
|
|
583
|
+
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
584
|
case 'barn':
|
|
408
585
|
const barnLivestock = getLivestockForBarn(view.barn.name);
|
|
409
586
|
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,28 +605,51 @@ export function App() {
|
|
|
428
605
|
const updatedProject = { ...project, livestock: updatedLivestock };
|
|
429
606
|
saveProject(updatedProject);
|
|
430
607
|
reload();
|
|
431
|
-
}
|
|
608
|
+
}, onAddCritter: (critter) => {
|
|
609
|
+
addCritterToBarn(view.barn.name, critter);
|
|
610
|
+
reload();
|
|
611
|
+
}, onRemoveCritter: (critterName) => {
|
|
612
|
+
removeCritterFromBarn(view.barn.name, critterName);
|
|
613
|
+
reload();
|
|
614
|
+
}, onSelectCritter: (critter) => handleOpenCritterDetail(view.barn, critter) }));
|
|
432
615
|
case 'wiki':
|
|
433
616
|
return (_jsx(WikiView, { project: view.project, onBack: () => handleBackFromSubview(view.project), onUpdateProject: handleUpdateProject }));
|
|
434
617
|
case 'issues':
|
|
435
|
-
return (_jsx(IssuesView, { project: view.project, onBack: () => handleBackFromSubview(view.project) }));
|
|
618
|
+
return (_jsx(IssuesView, { project: view.project, onBack: () => handleBackFromSubview(view.project), onOpenClaude: handleOpenClaudeWithContext }));
|
|
436
619
|
case 'livestock':
|
|
620
|
+
const livestockBarn = barns.find((b) => b.name === view.livestock.barn) || null;
|
|
621
|
+
const isLocalLivestock = !livestockBarn || isLocalBarn(livestockBarn);
|
|
437
622
|
return (_jsx(LivestockDetailView, { project: view.project, livestock: view.livestock, source: view.source, sourceBarn: view.sourceBarn, windows: windows, onBack: () => handleBackFromLivestock(view.source, view.project, view.sourceBarn), onOpenLogs: () => handleOpenLogs(view.project, view.livestock, view.source, view.sourceBarn), onOpenSession: () => {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
}, onSelectWindow: handleSelectWindow, onUpdateLivestock: (originalLivestock, updatedLivestock) => handleUpdateLivestock(view.project, originalLivestock, updatedLivestock, view.source, view.sourceBarn) }));
|
|
623
|
+
handleOpenLivestockSession(view.livestock, livestockBarn, view.project.name);
|
|
624
|
+
}, onOpenClaude: isLocalLivestock ? () => handleNewClaudeForLivestock(view.livestock, view.project.name) : undefined, onSelectWindow: handleSelectWindow, onUpdateLivestock: (originalLivestock, updatedLivestock) => handleUpdateLivestock(view.project, originalLivestock, updatedLivestock, view.source, view.sourceBarn) }));
|
|
441
625
|
case 'logs':
|
|
442
626
|
return (_jsx(LogsView, { project: view.project, livestock: view.livestock, onBack: () => {
|
|
443
627
|
setView({ type: 'livestock', project: view.project, livestock: view.livestock, source: view.source, sourceBarn: view.sourceBarn });
|
|
444
628
|
updateStatusBar(`${view.project.name} / ${view.livestock.name}`);
|
|
445
629
|
} }));
|
|
630
|
+
case 'critter':
|
|
631
|
+
return (_jsx(CritterDetailView, { barn: view.barn, critter: view.critter, onBack: () => handleBackFromCritter(view.barn), onOpenLogs: () => handleOpenCritterLogs(view.barn, view.critter), onUpdateCritter: (original, updated) => handleUpdateCritter(view.barn, original, updated) }));
|
|
632
|
+
case 'critter-logs':
|
|
633
|
+
return (_jsx(CritterLogsView, { barn: view.barn, critter: view.critter, onBack: () => {
|
|
634
|
+
setView({ type: 'critter', barn: view.barn, critter: view.critter });
|
|
635
|
+
updateStatusBar(`${view.barn.name} / ${view.critter.name}`);
|
|
636
|
+
} }));
|
|
446
637
|
case 'night-sky':
|
|
447
638
|
return (_jsx(NightSkyView, { onExit: handleExitNightSky }));
|
|
448
639
|
}
|
|
449
640
|
};
|
|
641
|
+
// Memoized callback for splash screen to prevent unnecessary re-renders
|
|
642
|
+
const handleSplashComplete = useCallback(() => setShowSplash(false), []);
|
|
450
643
|
// Show splash screen on first load
|
|
451
644
|
if (showSplash) {
|
|
452
|
-
return _jsx(SplashScreen, { onComplete:
|
|
645
|
+
return _jsx(SplashScreen, { onComplete: handleSplashComplete });
|
|
453
646
|
}
|
|
454
|
-
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
|
|
647
|
+
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, {
|
|
648
|
+
isLocalLivestock: view.type === 'livestock'
|
|
649
|
+
? !view.livestock.barn || isLocalBarn(barns.find((b) => b.name === view.livestock.barn) || { name: 'local' })
|
|
650
|
+
: undefined,
|
|
651
|
+
isLocalBarn: view.type === 'barn'
|
|
652
|
+
? isLocalBarn(view.barn)
|
|
653
|
+
: undefined,
|
|
654
|
+
}), environments: environments, isDetecting: isDetecting }))] }));
|
|
455
655
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
// Rabbit ASCII art template - @ symbols are pattern spots
|
|
4
|
+
const RABBIT_TEMPLATE = [
|
|
5
|
+
" __",
|
|
6
|
+
" /\\ .-\" /",
|
|
7
|
+
" / ; .' .' ",
|
|
8
|
+
" : :/ .' ",
|
|
9
|
+
" \\ ;-.' ",
|
|
10
|
+
" .--\"\"\"\"--..__/ `. ",
|
|
11
|
+
" .'@@@@@@@@@@@.' o \\ ",
|
|
12
|
+
" /@@@@@@@@@@@@@@@@ ; ",
|
|
13
|
+
" :@@@@@@@@@@@@@@@@\\ : ",
|
|
14
|
+
" .-;@@@@@@@@-.@@@@@@@`.__.--' ",
|
|
15
|
+
": ;@@@@@@@@@@\\@@@@@, ; ",
|
|
16
|
+
"'._:@@@@@@@@@@@;@@@: ( ",
|
|
17
|
+
" \\/ .__ ; \\ `-. ",
|
|
18
|
+
" ; \"-,/_..--\"`-..__) ",
|
|
19
|
+
" '\"\"--.._: ",
|
|
20
|
+
];
|
|
21
|
+
// Generate multiple hash values from a string for better distribution
|
|
22
|
+
function multiHash(str) {
|
|
23
|
+
const hashes = [];
|
|
24
|
+
// First hash - djb2
|
|
25
|
+
let hash1 = 5381;
|
|
26
|
+
for (let i = 0; i < str.length; i++) {
|
|
27
|
+
hash1 = ((hash1 << 5) + hash1) ^ str.charCodeAt(i);
|
|
28
|
+
}
|
|
29
|
+
hashes.push(Math.abs(hash1));
|
|
30
|
+
// Second hash - sdbm
|
|
31
|
+
let hash2 = 0;
|
|
32
|
+
for (let i = 0; i < str.length; i++) {
|
|
33
|
+
hash2 = str.charCodeAt(i) + (hash2 << 6) + (hash2 << 16) - hash2;
|
|
34
|
+
}
|
|
35
|
+
hashes.push(Math.abs(hash2));
|
|
36
|
+
// Third hash - fnv-1a inspired
|
|
37
|
+
let hash3 = 2166136261;
|
|
38
|
+
for (let i = 0; i < str.length; i++) {
|
|
39
|
+
hash3 ^= str.charCodeAt(i);
|
|
40
|
+
hash3 = (hash3 * 16777619) >>> 0;
|
|
41
|
+
}
|
|
42
|
+
hashes.push(hash3);
|
|
43
|
+
return hashes;
|
|
44
|
+
}
|
|
45
|
+
// Pattern characters - space and block characters
|
|
46
|
+
const PATTERN_CHARS = [' ', ' ', '░', '░', '▒', '▓', '█'];
|
|
47
|
+
// Generate pattern variation for the rabbit based on critter/barn data
|
|
48
|
+
function generateRabbitArt(critter, barn) {
|
|
49
|
+
// Use barn name + critter name + service for unique seed
|
|
50
|
+
const seed = `${barn.name}-${critter.name}-${critter.service}`;
|
|
51
|
+
const hashes = multiHash(seed);
|
|
52
|
+
// Replace @ symbols with pattern characters
|
|
53
|
+
let charIndex = 0;
|
|
54
|
+
const rabbitArt = RABBIT_TEMPLATE.map((line, lineIndex) => {
|
|
55
|
+
let result = '';
|
|
56
|
+
for (const char of line) {
|
|
57
|
+
if (char === '@') {
|
|
58
|
+
const h1 = hashes[0];
|
|
59
|
+
const h2 = hashes[1];
|
|
60
|
+
const h3 = hashes[2];
|
|
61
|
+
const mix = (h1 >> (charIndex % 17)) ^
|
|
62
|
+
(h2 >> ((charIndex + lineIndex) % 13)) ^
|
|
63
|
+
(h3 >> ((charIndex * 7 + lineIndex * 3) % 19));
|
|
64
|
+
const charChoice = Math.abs(mix) % PATTERN_CHARS.length;
|
|
65
|
+
result += PATTERN_CHARS[charChoice];
|
|
66
|
+
charIndex++;
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
result += char;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
});
|
|
74
|
+
return rabbitArt;
|
|
75
|
+
}
|
|
76
|
+
export function CritterHeader({ barn, critter }) {
|
|
77
|
+
const rabbitArt = generateRabbitArt(critter, barn);
|
|
78
|
+
// Use a default color for critters (could be configurable later)
|
|
79
|
+
const color = '#8fbc8f'; // Dark sea green
|
|
80
|
+
return (_jsx(Box, { flexDirection: "column", paddingTop: 1, paddingLeft: 1, children: _jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexDirection: "column", children: rabbitArt.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: critter.name }), _jsxs(Text, { dimColor: true, children: ["barn: ", barn.name] }), _jsxs(Text, { dimColor: true, children: ["service: ", critter.service] })] })] }) }));
|
|
81
|
+
}
|
|
@@ -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,24 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { SessionStatus } from '../lib/signals.js';
|
|
3
|
+
export interface RowAction {
|
|
4
|
+
key: string;
|
|
5
|
+
label: string;
|
|
6
|
+
}
|
|
1
7
|
export interface ListItem {
|
|
2
8
|
id: string;
|
|
3
9
|
label: string;
|
|
4
10
|
status?: 'active' | 'inactive' | 'error';
|
|
5
11
|
meta?: string;
|
|
12
|
+
sessionStatus?: SessionStatus;
|
|
13
|
+
actions?: RowAction[];
|
|
14
|
+
prefix?: React.ReactNode;
|
|
6
15
|
}
|
|
7
16
|
interface ListProps {
|
|
8
17
|
items: ListItem[];
|
|
9
18
|
focused?: boolean;
|
|
10
19
|
selectedIndex?: number;
|
|
11
20
|
onSelect?: (item: ListItem) => void;
|
|
12
|
-
onAction?: (item: ListItem) => void;
|
|
21
|
+
onAction?: (item: ListItem, action: string) => void;
|
|
13
22
|
onHighlight?: (item: ListItem | null) => void;
|
|
14
23
|
onSelectionChange?: (index: number) => void;
|
|
15
24
|
}
|