@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.
Files changed (61) hide show
  1. package/claude-plugin/.claude-plugin/plugin.json +2 -1
  2. package/claude-plugin/hooks/hooks.json +41 -0
  3. package/claude-plugin/hooks/session-status.sh +13 -0
  4. package/claude-plugin/skills/yeehaw-development/SKILL.md +70 -0
  5. package/dist/app.js +228 -28
  6. package/dist/components/CritterHeader.d.ts +7 -0
  7. package/dist/components/CritterHeader.js +81 -0
  8. package/dist/components/HelpOverlay.js +4 -2
  9. package/dist/components/List.d.ts +10 -1
  10. package/dist/components/List.js +14 -5
  11. package/dist/components/Panel.js +27 -1
  12. package/dist/components/SplashScreen.js +1 -1
  13. package/dist/hooks/useSessions.js +2 -2
  14. package/dist/index.js +41 -1
  15. package/dist/lib/auth/index.d.ts +2 -0
  16. package/dist/lib/auth/index.js +3 -0
  17. package/dist/lib/auth/linear.d.ts +20 -0
  18. package/dist/lib/auth/linear.js +79 -0
  19. package/dist/lib/auth/storage.d.ts +12 -0
  20. package/dist/lib/auth/storage.js +53 -0
  21. package/dist/lib/config.d.ts +13 -1
  22. package/dist/lib/config.js +51 -0
  23. package/dist/lib/context.d.ts +10 -0
  24. package/dist/lib/context.js +63 -0
  25. package/dist/lib/critters.d.ts +61 -0
  26. package/dist/lib/critters.js +365 -0
  27. package/dist/lib/hooks.d.ts +20 -0
  28. package/dist/lib/hooks.js +91 -0
  29. package/dist/lib/hotkeys.d.ts +1 -1
  30. package/dist/lib/hotkeys.js +28 -20
  31. package/dist/lib/issues/github.d.ts +11 -0
  32. package/dist/lib/issues/github.js +154 -0
  33. package/dist/lib/issues/index.d.ts +14 -0
  34. package/dist/lib/issues/index.js +27 -0
  35. package/dist/lib/issues/linear.d.ts +24 -0
  36. package/dist/lib/issues/linear.js +345 -0
  37. package/dist/lib/issues/types.d.ts +82 -0
  38. package/dist/lib/issues/types.js +2 -0
  39. package/dist/lib/paths.d.ts +3 -0
  40. package/dist/lib/paths.js +3 -0
  41. package/dist/lib/signals.d.ts +30 -0
  42. package/dist/lib/signals.js +104 -0
  43. package/dist/lib/tmux.d.ts +9 -2
  44. package/dist/lib/tmux.js +114 -18
  45. package/dist/mcp-server.js +161 -1
  46. package/dist/types.d.ts +23 -2
  47. package/dist/views/BarnContext.d.ts +5 -2
  48. package/dist/views/BarnContext.js +202 -21
  49. package/dist/views/CritterDetailView.d.ts +10 -0
  50. package/dist/views/CritterDetailView.js +117 -0
  51. package/dist/views/CritterLogsView.d.ts +8 -0
  52. package/dist/views/CritterLogsView.js +100 -0
  53. package/dist/views/GlobalDashboard.d.ts +2 -2
  54. package/dist/views/GlobalDashboard.js +20 -18
  55. package/dist/views/IssuesView.d.ts +2 -1
  56. package/dist/views/IssuesView.js +661 -98
  57. package/dist/views/LivestockDetailView.d.ts +2 -1
  58. package/dist/views/LivestockDetailView.js +19 -8
  59. package/dist/views/ProjectContext.d.ts +2 -2
  60. package/dist/views/ProjectContext.js +68 -25
  61. package/package.json +5 -5
@@ -3,5 +3,6 @@
3
3
  "description": "Yeehaw CLI plugin with skills for project setup and configuration",
4
4
  "author": {
5
5
  "name": "Yeehaw"
6
- }
6
+ },
7
+ "hooks": "./hooks/hooks.json"
7
8
  }
@@ -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 by view type
43
- function getBottomBarItems(viewType) {
44
- const common = [
45
- { key: 'Tab', label: 'switch' },
46
- { key: '?', label: 'help' },
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 [...common, { key: 'c', label: 'claude' }, { key: 'q', label: 'detach' }, { key: 'Q', label: 'quit' }];
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 [...common, { key: 'c', label: 'claude' }, { key: 'w', label: 'wiki' }, { key: 'i', label: 'issues' }, { key: 'Esc', label: 'back' }];
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
- return [...common, { key: 's', label: 'shell' }, { key: 'Esc', label: 'back' }];
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 [...common, { key: 'Esc', label: 'back' }];
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 [...common, { key: 'r', label: 'refresh' }, { key: 'o', label: 'open' }, { key: 'Esc', label: 'back' }];
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
- return [...common, { key: 's', label: 'shell' }, { key: 'l', label: 'logs' }, { key: 'e', label: 'edit' }, { key: 'Esc', label: 'back' }];
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 [...common, { key: 'r', label: 'refresh' }, { key: 'Esc', label: 'back' }];
118
+ return [
119
+ { key: 'r', label: 'refresh' },
120
+ { key: 'Esc', label: 'back' },
121
+ { key: '?', label: 'help' },
122
+ ];
68
123
  }
69
- if (viewType === 'night-sky') {
70
- return [{ key: 'c', label: 'cloud' }, { key: 'r', label: 'randomize' }, { key: 'Esc', label: 'exit' }];
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
- return [...common, { key: 'Esc', label: 'back' }];
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
- const windowIndex = createClaude(workingDir, windowName);
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, onNewClaude: handleNewClaude, onCreateProject: handleCreateProject, onCreateBarn: handleCreateBarn, onSshToBarn: handleSshToBarn, onInputModeChange: setIsChildInputMode }));
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, onNewClaude: handleNewClaude, 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) }));
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
- const barn = barns.find((b) => b.name === view.livestock.barn) || null;
439
- handleOpenLivestockSession(view.livestock, barn, view.project.name);
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: () => setShowSplash(false) });
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), environments: environments, isDetecting: isDetecting }))] }));
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,7 @@
1
+ import type { Critter, Barn } from '../types.js';
2
+ interface CritterHeaderProps {
3
+ barn: Barn;
4
+ critter: Critter;
5
+ }
6
+ export declare function CritterHeader({ barn, critter }: CritterHeaderProps): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -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: "cyan", children: hotkey.key }) }), _jsx(Text, { children: hotkey.description })] }));
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: "yellow", paddingX: 2, paddingY: 1, children: [_jsx(Text, { bold: true, color: "yellow", 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" }) })] }));
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
  }