@colmbus72/yeehaw 0.1.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 (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +124 -0
  3. package/dist/app.d.ts +1 -0
  4. package/dist/app.js +414 -0
  5. package/dist/components/BarnHeader.d.ts +6 -0
  6. package/dist/components/BarnHeader.js +21 -0
  7. package/dist/components/BottomBar.d.ts +16 -0
  8. package/dist/components/BottomBar.js +7 -0
  9. package/dist/components/Header.d.ts +8 -0
  10. package/dist/components/Header.js +83 -0
  11. package/dist/components/HelpOverlay.d.ts +7 -0
  12. package/dist/components/HelpOverlay.js +17 -0
  13. package/dist/components/List.d.ts +17 -0
  14. package/dist/components/List.js +53 -0
  15. package/dist/components/Markdown.d.ts +8 -0
  16. package/dist/components/Markdown.js +23 -0
  17. package/dist/components/Panel.d.ts +10 -0
  18. package/dist/components/Panel.js +5 -0
  19. package/dist/components/PathInput.d.ts +9 -0
  20. package/dist/components/PathInput.js +141 -0
  21. package/dist/components/ScrollableMarkdown.d.ts +11 -0
  22. package/dist/components/ScrollableMarkdown.js +56 -0
  23. package/dist/components/StatusBar.d.ts +5 -0
  24. package/dist/components/StatusBar.js +20 -0
  25. package/dist/components/TextArea.d.ts +17 -0
  26. package/dist/components/TextArea.js +140 -0
  27. package/dist/components/index.d.ts +5 -0
  28. package/dist/components/index.js +5 -0
  29. package/dist/hooks/index.d.ts +3 -0
  30. package/dist/hooks/index.js +3 -0
  31. package/dist/hooks/useConfig.d.ts +11 -0
  32. package/dist/hooks/useConfig.js +36 -0
  33. package/dist/hooks/useRemoteYeehaw.d.ts +13 -0
  34. package/dist/hooks/useRemoteYeehaw.js +49 -0
  35. package/dist/hooks/useSessions.d.ts +11 -0
  36. package/dist/hooks/useSessions.js +46 -0
  37. package/dist/index.d.ts +2 -0
  38. package/dist/index.js +34 -0
  39. package/dist/lib/config.d.ts +27 -0
  40. package/dist/lib/config.js +150 -0
  41. package/dist/lib/detection.d.ts +16 -0
  42. package/dist/lib/detection.js +41 -0
  43. package/dist/lib/editor.d.ts +5 -0
  44. package/dist/lib/editor.js +35 -0
  45. package/dist/lib/errors.d.ts +28 -0
  46. package/dist/lib/errors.js +48 -0
  47. package/dist/lib/git.d.ts +11 -0
  48. package/dist/lib/git.js +73 -0
  49. package/dist/lib/github.d.ts +43 -0
  50. package/dist/lib/github.js +111 -0
  51. package/dist/lib/hotkeys.d.ts +27 -0
  52. package/dist/lib/hotkeys.js +92 -0
  53. package/dist/lib/index.d.ts +10 -0
  54. package/dist/lib/index.js +10 -0
  55. package/dist/lib/livestock.d.ts +51 -0
  56. package/dist/lib/livestock.js +233 -0
  57. package/dist/lib/mcp-validation.d.ts +33 -0
  58. package/dist/lib/mcp-validation.js +62 -0
  59. package/dist/lib/paths.d.ts +8 -0
  60. package/dist/lib/paths.js +28 -0
  61. package/dist/lib/shell.d.ts +34 -0
  62. package/dist/lib/shell.js +61 -0
  63. package/dist/lib/ssh.d.ts +15 -0
  64. package/dist/lib/ssh.js +77 -0
  65. package/dist/lib/tmux-config.d.ts +3 -0
  66. package/dist/lib/tmux-config.js +42 -0
  67. package/dist/lib/tmux.d.ts +32 -0
  68. package/dist/lib/tmux.js +397 -0
  69. package/dist/mcp-server.d.ts +23 -0
  70. package/dist/mcp-server.js +825 -0
  71. package/dist/types.d.ts +89 -0
  72. package/dist/types.js +2 -0
  73. package/dist/views/BarnContext.d.ts +22 -0
  74. package/dist/views/BarnContext.js +252 -0
  75. package/dist/views/GlobalDashboard.d.ts +16 -0
  76. package/dist/views/GlobalDashboard.js +253 -0
  77. package/dist/views/Home.d.ts +11 -0
  78. package/dist/views/Home.js +27 -0
  79. package/dist/views/IssuesView.d.ts +7 -0
  80. package/dist/views/IssuesView.js +157 -0
  81. package/dist/views/LivestockDetailView.d.ts +11 -0
  82. package/dist/views/LivestockDetailView.js +140 -0
  83. package/dist/views/LogsView.d.ts +8 -0
  84. package/dist/views/LogsView.js +84 -0
  85. package/dist/views/NightSkyView.d.ts +5 -0
  86. package/dist/views/NightSkyView.js +441 -0
  87. package/dist/views/ProjectContext.d.ts +18 -0
  88. package/dist/views/ProjectContext.js +333 -0
  89. package/dist/views/Projects.d.ts +8 -0
  90. package/dist/views/Projects.js +20 -0
  91. package/dist/views/WikiView.d.ts +8 -0
  92. package/dist/views/WikiView.js +138 -0
  93. package/dist/views/index.d.ts +2 -0
  94. package/dist/views/index.js +2 -0
  95. package/package.json +65 -0
@@ -0,0 +1,83 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ import { Box, Text } from 'ink';
4
+ import figlet from 'figlet';
5
+ // Convert hex to RGB
6
+ function hexToRgb(hex) {
7
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
8
+ return result
9
+ ? {
10
+ r: parseInt(result[1], 16),
11
+ g: parseInt(result[2], 16),
12
+ b: parseInt(result[3], 16),
13
+ }
14
+ : null;
15
+ }
16
+ // Interpolate between two colors
17
+ function interpolateColor(color1, color2, factor) {
18
+ const r = Math.round(color1.r + (color2.r - color1.r) * factor);
19
+ const g = Math.round(color1.g + (color2.g - color1.g) * factor);
20
+ const b = Math.round(color1.b + (color2.b - color1.b) * factor);
21
+ return `rgb(${r},${g},${b})`;
22
+ }
23
+ // Generate gradient colors for each line
24
+ function generateGradient(lines, baseColor) {
25
+ const rgb = hexToRgb(baseColor);
26
+ if (!rgb)
27
+ return lines.map(() => baseColor);
28
+ // Calculate luminance to detect dark colors
29
+ const luminance = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255;
30
+ let startRgb;
31
+ let endRgb;
32
+ if (luminance < 0.3) {
33
+ // Dark color: go from a lighter tint down to the base color
34
+ // Lift toward white while preserving hue
35
+ const liftFactor = 2.5;
36
+ startRgb = {
37
+ r: Math.min(255, Math.round(rgb.r * liftFactor + 40)),
38
+ g: Math.min(255, Math.round(rgb.g * liftFactor + 40)),
39
+ b: Math.min(255, Math.round(rgb.b * liftFactor + 40)),
40
+ };
41
+ endRgb = rgb;
42
+ }
43
+ else {
44
+ // Light/medium color: go from base to a darker version
45
+ startRgb = rgb;
46
+ endRgb = {
47
+ r: Math.round(rgb.r * 0.3),
48
+ g: Math.round(rgb.g * 0.3),
49
+ b: Math.round(rgb.b * 0.3),
50
+ };
51
+ }
52
+ return lines.map((_, i) => {
53
+ const factor = i / Math.max(lines.length - 1, 1);
54
+ return interpolateColor(startRgb, endRgb, factor);
55
+ });
56
+ }
57
+ // Tumbleweed mascot art
58
+ const TUMBLEWEED = [
59
+ ' ░ ░▒░ ░▒░',
60
+ '░▒ · ‿ · ▒░',
61
+ '▒░ ▒░▒░ ░▒',
62
+ ' ░▒░ ░▒░ ░',
63
+ ];
64
+ // Brownish tan color to complement yeehaw gold
65
+ const TUMBLEWEED_COLOR = '#b8860b';
66
+ export function Header({ text, subtitle, summary, color }) {
67
+ const [ascii, setAscii] = useState('');
68
+ useEffect(() => {
69
+ figlet.text(text.toUpperCase(), { font: 'ANSI Shadow' }, (err, result) => {
70
+ if (!err && result) {
71
+ setAscii(result);
72
+ }
73
+ });
74
+ }, [text]);
75
+ const lines = ascii.split('\n').filter(line => line.trim() !== '');
76
+ const baseColor = color || '#f0c040'; // Default yeehaw gold
77
+ const gradientColors = generateGradient(lines, baseColor);
78
+ // Show tumbleweed only for the main "yeehaw" title
79
+ const showTumbleweed = text.toLowerCase() === 'yeehaw';
80
+ // Vertically center tumbleweed next to ASCII art
81
+ const tumbleweedTopPadding = Math.max(0, Math.floor((lines.length - TUMBLEWEED.length) / 2));
82
+ return (_jsxs(Box, { flexDirection: "column", paddingTop: 1, paddingLeft: 2, children: [_jsxs(Box, { flexDirection: "row", children: [showTumbleweed && (_jsxs(Box, { flexDirection: "column", marginRight: 2, children: [Array(tumbleweedTopPadding).fill(null).map((_, i) => (_jsx(Text, { children: " " }, `pad-${i}`))), TUMBLEWEED.map((line, i) => (_jsx(Text, { color: TUMBLEWEED_COLOR, children: line }, `tumbleweed-${i}`)))] })), _jsx(Box, { flexDirection: "column", children: lines.map((line, i) => (_jsx(Text, { color: gradientColors[i], children: line }, i))) })] }), (subtitle || summary) && (_jsxs(Box, { gap: 2, children: [subtitle && _jsx(Text, { dimColor: true, children: subtitle }), summary && _jsxs(Text, { color: "gray", children: ["- ", summary] })] }))] }));
83
+ }
@@ -0,0 +1,7 @@
1
+ import { type HotkeyScope } from '../lib/hotkeys.js';
2
+ interface HelpOverlayProps {
3
+ scope: HotkeyScope;
4
+ focusedPanel?: string;
5
+ }
6
+ export declare function HelpOverlay({ scope, focusedPanel }: HelpOverlayProps): import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { getHotkeysGrouped } from '../lib/hotkeys.js';
4
+ 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 })] }));
6
+ }
7
+ function HotkeySection({ title, hotkeys }) {
8
+ if (hotkeys.length === 0)
9
+ return null;
10
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, dimColor: true, children: title }), hotkeys.map((h, i) => (_jsx(HotkeyRow, { hotkey: h }, `${h.key}-${i}`)))] }));
11
+ }
12
+ export function HelpOverlay({ scope, focusedPanel }) {
13
+ const grouped = getHotkeysGrouped(scope, focusedPanel);
14
+ // Also include list navigation if we're in a view with lists
15
+ 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" }) })] }));
17
+ }
@@ -0,0 +1,17 @@
1
+ export interface ListItem {
2
+ id: string;
3
+ label: string;
4
+ status?: 'active' | 'inactive' | 'error';
5
+ meta?: string;
6
+ }
7
+ interface ListProps {
8
+ items: ListItem[];
9
+ focused?: boolean;
10
+ selectedIndex?: number;
11
+ onSelect?: (item: ListItem) => void;
12
+ onAction?: (item: ListItem) => void;
13
+ onHighlight?: (item: ListItem | null) => void;
14
+ onSelectionChange?: (index: number) => void;
15
+ }
16
+ export declare function List({ items, focused, selectedIndex: controlledIndex, onSelect, onAction, onHighlight, onSelectionChange }: ListProps): import("react/jsx-runtime").JSX.Element;
17
+ export {};
@@ -0,0 +1,53 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ export function List({ items, focused = false, selectedIndex: controlledIndex, onSelect, onAction, onHighlight, onSelectionChange }) {
5
+ const [internalIndex, setInternalIndex] = useState(0);
6
+ // Use controlled index if provided, otherwise internal
7
+ const selectedIndex = controlledIndex ?? internalIndex;
8
+ const setSelectedIndex = (indexOrFn) => {
9
+ const newIndex = typeof indexOrFn === 'function' ? indexOrFn(selectedIndex) : indexOrFn;
10
+ if (controlledIndex === undefined) {
11
+ setInternalIndex(newIndex);
12
+ }
13
+ onSelectionChange?.(newIndex);
14
+ };
15
+ useEffect(() => {
16
+ if (items.length > 0 && onHighlight) {
17
+ onHighlight(items[selectedIndex] ?? null);
18
+ }
19
+ }, [selectedIndex, items, onHighlight]);
20
+ useInput((input, key) => {
21
+ if (!focused)
22
+ return;
23
+ if (input === 'j' || key.downArrow) {
24
+ setSelectedIndex((i) => Math.min(i + 1, items.length - 1));
25
+ }
26
+ if (input === 'k' || key.upArrow) {
27
+ setSelectedIndex((i) => Math.max(i - 1, 0));
28
+ }
29
+ if (input === 'g') {
30
+ setSelectedIndex(0);
31
+ }
32
+ if (input === 'G') {
33
+ setSelectedIndex(items.length - 1);
34
+ }
35
+ if (key.return && items[selectedIndex] && onSelect) {
36
+ onSelect(items[selectedIndex]);
37
+ }
38
+ if (input === 's' && items[selectedIndex] && onAction) {
39
+ onAction(items[selectedIndex]);
40
+ }
41
+ });
42
+ if (items.length === 0) {
43
+ return _jsx(Text, { dimColor: true, children: "No items" });
44
+ }
45
+ return (_jsx(Box, { flexDirection: "column", children: items.map((item, index) => {
46
+ const isSelected = index === selectedIndex && focused;
47
+ const statusColor = item.status === 'active' ? 'green' :
48
+ item.status === 'error' ? 'red' : 'gray';
49
+ // Yeehaw brand gold for selection
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));
52
+ }) }));
53
+ }
@@ -0,0 +1,8 @@
1
+ interface MarkdownProps {
2
+ children: string;
3
+ }
4
+ /**
5
+ * Render markdown content in the terminal with formatting.
6
+ */
7
+ export declare function Markdown({ children }: MarkdownProps): import("react/jsx-runtime").JSX.Element;
8
+ export {};
@@ -0,0 +1,23 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Text } from 'ink';
3
+ import { marked } from 'marked';
4
+ // @ts-ignore - no types available
5
+ import TerminalRenderer from 'marked-terminal';
6
+ // Configure marked to use terminal renderer
7
+ marked.setOptions({
8
+ renderer: new TerminalRenderer({
9
+ // Customize terminal rendering options
10
+ showSectionPrefix: false,
11
+ reflowText: true,
12
+ width: 80,
13
+ }),
14
+ });
15
+ /**
16
+ * Render markdown content in the terminal with formatting.
17
+ */
18
+ export function Markdown({ children }) {
19
+ const rendered = marked.parse(children);
20
+ // marked-terminal returns a string with ANSI codes
21
+ // Ink's Text component will render these correctly
22
+ return _jsx(Text, { children: String(rendered).trim() });
23
+ }
@@ -0,0 +1,10 @@
1
+ import { ReactNode } from 'react';
2
+ interface PanelProps {
3
+ title: string;
4
+ children: ReactNode;
5
+ focused?: boolean;
6
+ width?: number | string;
7
+ hints?: string;
8
+ }
9
+ export declare function Panel({ title, children, focused, width, hints }: PanelProps): import("react/jsx-runtime").JSX.Element;
10
+ export {};
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ export function Panel({ title, children, focused = false, width, hints }) {
4
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', width: width, children: [_jsxs(Box, { paddingX: 1, marginBottom: 0, justifyContent: "space-between", children: [_jsx(Text, { bold: true, color: focused ? 'cyan' : 'white', children: title }), focused && hints && (_jsx(Text, { color: "cyan", children: hints }))] }), _jsx(Box, { flexDirection: "column", paddingX: 1, children: children })] }));
5
+ }
@@ -0,0 +1,9 @@
1
+ import type { Barn } from '../types.js';
2
+ interface PathInputProps {
3
+ value: string;
4
+ onChange: (value: string) => void;
5
+ onSubmit: (value: string) => void;
6
+ barn?: Barn;
7
+ }
8
+ export declare function PathInput({ value, onChange, onSubmit, barn }: PathInputProps): import("react/jsx-runtime").JSX.Element;
9
+ export {};
@@ -0,0 +1,141 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import { readdirSync, existsSync } from 'fs';
5
+ import { join, dirname, basename } from 'path';
6
+ import { homedir } from 'os';
7
+ import { execaSync } from 'execa';
8
+ import { hasValidSshConfig } from '../lib/config.js';
9
+ function expandPath(path) {
10
+ if (path.startsWith('~/')) {
11
+ return join(homedir(), path.slice(2));
12
+ }
13
+ return path;
14
+ }
15
+ function getLocalCompletions(partialPath) {
16
+ if (!partialPath)
17
+ return [];
18
+ const expanded = expandPath(partialPath);
19
+ const dir = partialPath.endsWith('/') ? expanded : dirname(expanded);
20
+ const prefix = partialPath.endsWith('/') ? '' : basename(expanded);
21
+ try {
22
+ if (!existsSync(dir))
23
+ return [];
24
+ const entries = readdirSync(dir, { withFileTypes: true });
25
+ const matches = entries
26
+ .filter((e) => e.name.startsWith(prefix) && e.isDirectory())
27
+ .map((e) => e.name);
28
+ return matches;
29
+ }
30
+ catch {
31
+ return [];
32
+ }
33
+ }
34
+ function getRemoteCompletions(partialPath, barn) {
35
+ if (!partialPath)
36
+ return [];
37
+ // Verify barn has valid SSH config
38
+ if (!hasValidSshConfig(barn)) {
39
+ return []; // Can't do remote completion without SSH config
40
+ }
41
+ // Handle ~ expansion for display
42
+ const dir = partialPath.endsWith('/') ? partialPath : dirname(partialPath) || '~';
43
+ const prefix = partialPath.endsWith('/') ? '' : basename(partialPath);
44
+ try {
45
+ // Use SSH to list directories on the remote server
46
+ const result = execaSync('ssh', [
47
+ '-p', String(barn.port),
48
+ '-i', barn.identity_file,
49
+ '-o', 'BatchMode=yes',
50
+ '-o', 'ConnectTimeout=5',
51
+ `${barn.user}@${barn.host}`,
52
+ `ls -1d ${dir}/${prefix}*/ 2>/dev/null | xargs -n1 basename 2>/dev/null || true`
53
+ ], { timeout: 10000 });
54
+ const output = result.stdout.trim();
55
+ if (!output)
56
+ return [];
57
+ return output.split('\n').filter(Boolean);
58
+ }
59
+ catch {
60
+ return [];
61
+ }
62
+ }
63
+ export function PathInput({ value, onChange, onSubmit, barn }) {
64
+ const [cursorPos, setCursorPos] = useState(value.length);
65
+ const [completions, setCompletions] = useState([]);
66
+ const [loading, setLoading] = useState(false);
67
+ // Update completions when value changes (debounced for remote)
68
+ useEffect(() => {
69
+ if (barn) {
70
+ // Remote completion - debounce
71
+ setLoading(true);
72
+ const timer = setTimeout(() => {
73
+ const results = getRemoteCompletions(value, barn);
74
+ setCompletions(results);
75
+ setLoading(false);
76
+ }, 300);
77
+ return () => clearTimeout(timer);
78
+ }
79
+ else {
80
+ // Local completion - immediate
81
+ setCompletions(getLocalCompletions(value));
82
+ }
83
+ }, [value, barn]);
84
+ useInput((input, key) => {
85
+ if (key.return) {
86
+ onSubmit(value);
87
+ return;
88
+ }
89
+ if (key.tab) {
90
+ // Tab completion
91
+ if (completions.length === 1) {
92
+ // Single match - complete it
93
+ const displayDir = value.endsWith('/') ? value : value.slice(0, value.lastIndexOf('/') + 1) || (barn ? '~/' : '');
94
+ const newPath = displayDir + completions[0] + '/';
95
+ onChange(newPath);
96
+ setCursorPos(newPath.length);
97
+ }
98
+ else if (completions.length > 1) {
99
+ // Multiple matches - find common prefix
100
+ const commonPrefix = completions.reduce((acc, curr) => {
101
+ let i = 0;
102
+ while (i < acc.length && i < curr.length && acc[i] === curr[i])
103
+ i++;
104
+ return acc.slice(0, i);
105
+ });
106
+ if (commonPrefix) {
107
+ const displayDir = value.endsWith('/') ? value : value.slice(0, value.lastIndexOf('/') + 1) || (barn ? '~/' : '');
108
+ const newPath = displayDir + commonPrefix;
109
+ onChange(newPath);
110
+ setCursorPos(newPath.length);
111
+ }
112
+ }
113
+ return;
114
+ }
115
+ if (key.backspace || key.delete) {
116
+ if (value.length > 0) {
117
+ const newValue = value.slice(0, -1);
118
+ onChange(newValue);
119
+ setCursorPos(Math.max(0, cursorPos - 1));
120
+ }
121
+ return;
122
+ }
123
+ if (key.leftArrow) {
124
+ setCursorPos(Math.max(0, cursorPos - 1));
125
+ return;
126
+ }
127
+ if (key.rightArrow) {
128
+ setCursorPos(Math.min(value.length, cursorPos + 1));
129
+ return;
130
+ }
131
+ // Regular character input
132
+ if (input && !key.ctrl && !key.meta) {
133
+ const newValue = value + input;
134
+ onChange(newValue);
135
+ setCursorPos(newValue.length);
136
+ }
137
+ });
138
+ // Show completions hint
139
+ const showHint = completions.length > 1 && completions.length <= 5;
140
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { children: value }), _jsx(Text, { backgroundColor: "white", color: "black", children: " " }), loading && _jsx(Text, { dimColor: true, children: " (loading...)" })] }), showHint && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Tab: ", completions.join(' ')] }) }))] }));
141
+ }
@@ -0,0 +1,11 @@
1
+ interface ScrollableMarkdownProps {
2
+ children: string;
3
+ focused?: boolean;
4
+ height?: number;
5
+ }
6
+ /**
7
+ * Scrollable markdown content panel.
8
+ * When focused, j/k or arrow keys scroll the content.
9
+ */
10
+ export declare function ScrollableMarkdown({ children, focused, height, }: ScrollableMarkdownProps): import("react/jsx-runtime").JSX.Element;
11
+ export {};
@@ -0,0 +1,56 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ import { marked } from 'marked';
5
+ // @ts-ignore - no types available
6
+ import TerminalRenderer from 'marked-terminal';
7
+ // Configure marked to use terminal renderer
8
+ marked.setOptions({
9
+ renderer: new TerminalRenderer({
10
+ showSectionPrefix: false,
11
+ reflowText: true,
12
+ width: 80,
13
+ }),
14
+ });
15
+ /**
16
+ * Scrollable markdown content panel.
17
+ * When focused, j/k or arrow keys scroll the content.
18
+ */
19
+ export function ScrollableMarkdown({ children, focused = false, height = 20, }) {
20
+ const [scrollOffset, setScrollOffset] = useState(0);
21
+ // Reset scroll when content changes
22
+ useEffect(() => {
23
+ setScrollOffset(0);
24
+ }, [children]);
25
+ // Render markdown to string with ANSI codes
26
+ const rendered = String(marked.parse(children)).trim();
27
+ const lines = rendered.split('\n');
28
+ const totalLines = lines.length;
29
+ const visibleLines = height - 1; // Leave room for scroll indicator
30
+ useInput((input, key) => {
31
+ if (!focused)
32
+ return;
33
+ if (input === 'j' || key.downArrow) {
34
+ setScrollOffset((prev) => Math.min(prev + 1, Math.max(0, totalLines - visibleLines)));
35
+ }
36
+ if (input === 'k' || key.upArrow) {
37
+ setScrollOffset((prev) => Math.max(prev - 1, 0));
38
+ }
39
+ if (input === 'g') {
40
+ setScrollOffset(0);
41
+ }
42
+ if (input === 'G') {
43
+ setScrollOffset(Math.max(0, totalLines - visibleLines));
44
+ }
45
+ if (key.pageDown) {
46
+ setScrollOffset((prev) => Math.min(prev + visibleLines, Math.max(0, totalLines - visibleLines)));
47
+ }
48
+ if (key.pageUp) {
49
+ setScrollOffset((prev) => Math.max(prev - visibleLines, 0));
50
+ }
51
+ });
52
+ // Get visible slice of lines
53
+ const displayLines = lines.slice(scrollOffset, scrollOffset + visibleLines);
54
+ const showScrollIndicator = totalLines > visibleLines;
55
+ return (_jsxs(Box, { flexDirection: "column", height: height, children: [_jsx(Box, { flexDirection: "column", flexGrow: 1, children: displayLines.map((line, idx) => (_jsx(Text, { children: line || ' ' }, scrollOffset + idx))) }), showScrollIndicator && (_jsx(Box, { justifyContent: "flex-end", children: _jsxs(Text, { dimColor: true, children: ["[", scrollOffset + 1, "-", Math.min(scrollOffset + visibleLines, totalLines), "/", totalLines, "]", focused ? ' (j/k to scroll)' : ''] }) }))] }));
56
+ }
@@ -0,0 +1,5 @@
1
+ interface StatusBarProps {
2
+ view: 'global' | 'project';
3
+ }
4
+ export declare function StatusBar({ view }: StatusBarProps): import("react/jsx-runtime").JSX.Element;
5
+ export {};
@@ -0,0 +1,20 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ const globalShortcuts = [
4
+ { key: 'Enter', label: 'select' },
5
+ { key: 'Tab', label: 'switch panel' },
6
+ { key: 'n', label: 'new project' },
7
+ { key: 'c', label: 'claude' },
8
+ { key: '?', label: 'help' },
9
+ ];
10
+ const projectShortcuts = [
11
+ { key: 'Enter', label: 'shell' },
12
+ { key: 'c', label: 'claude' },
13
+ { key: 'Tab', label: 'switch panel' },
14
+ { key: '?', label: 'help' },
15
+ { key: 'Esc', label: 'back' },
16
+ ];
17
+ export function StatusBar({ view }) {
18
+ const shortcuts = view === 'global' ? globalShortcuts : projectShortcuts;
19
+ return (_jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, justifyContent: "space-between", children: [_jsx(Box, { gap: 2, children: shortcuts.map((shortcut) => (_jsxs(Text, { children: [_jsxs(Text, { color: "cyan", children: ["[", shortcut.key, "]"] }), _jsx(Text, { dimColor: true, children: shortcut.label })] }, shortcut.key))) }), view === 'global' ? (_jsxs(Box, { gap: 2, children: [_jsxs(Text, { children: [_jsx(Text, { color: "yellow", children: "q" }), _jsx(Text, { dimColor: true, children: ":detach" })] }), _jsxs(Text, { children: [_jsx(Text, { color: "red", children: "Q" }), _jsx(Text, { dimColor: true, children: ":quit all" })] })] })) : (_jsxs(Text, { children: [_jsx(Text, { color: "cyan", children: "q" }), _jsx(Text, { dimColor: true, children: ":back" })] }))] }));
20
+ }
@@ -0,0 +1,17 @@
1
+ interface TextAreaProps {
2
+ value: string;
3
+ onChange: (value: string) => void;
4
+ onSubmit?: (value: string) => void;
5
+ placeholder?: string;
6
+ height?: number;
7
+ }
8
+ /**
9
+ * Multiline text input component.
10
+ * - Enter: add newline
11
+ * - Ctrl+S: save/submit
12
+ * - Ctrl+D: also save/submit (alternative)
13
+ * - Backspace: delete character
14
+ * - Arrow keys: navigate
15
+ */
16
+ export declare function TextArea({ value, onChange, onSubmit, placeholder, height, }: TextAreaProps): import("react/jsx-runtime").JSX.Element;
17
+ export {};
@@ -0,0 +1,140 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect } from 'react';
3
+ import { Box, Text, useInput } from 'ink';
4
+ /**
5
+ * Multiline text input component.
6
+ * - Enter: add newline
7
+ * - Ctrl+S: save/submit
8
+ * - Ctrl+D: also save/submit (alternative)
9
+ * - Backspace: delete character
10
+ * - Arrow keys: navigate
11
+ */
12
+ export function TextArea({ value, onChange, onSubmit, placeholder, height = 8, }) {
13
+ const [cursorPos, setCursorPos] = useState(value.length);
14
+ const [scrollOffset, setScrollOffset] = useState(0);
15
+ // Keep cursor in bounds when value changes
16
+ useEffect(() => {
17
+ if (cursorPos > value.length) {
18
+ setCursorPos(value.length);
19
+ }
20
+ }, [value, cursorPos]);
21
+ // Calculate cursor line for auto-scroll
22
+ const getCursorLine = () => {
23
+ const beforeCursor = value.slice(0, cursorPos);
24
+ return (beforeCursor.match(/\n/g) || []).length;
25
+ };
26
+ // Auto-scroll to keep cursor visible
27
+ useEffect(() => {
28
+ const cursorLine = getCursorLine();
29
+ const visibleLines = height - 2;
30
+ if (cursorLine < scrollOffset) {
31
+ setScrollOffset(cursorLine);
32
+ }
33
+ else if (cursorLine >= scrollOffset + visibleLines) {
34
+ setScrollOffset(cursorLine - visibleLines + 1);
35
+ }
36
+ }, [cursorPos, value, height]);
37
+ useInput((input, key) => {
38
+ // Ctrl+S or Ctrl+D to submit
39
+ if ((key.ctrl && input === 's') || (key.ctrl && input === 'd')) {
40
+ onSubmit?.(value);
41
+ return;
42
+ }
43
+ // Enter adds newline
44
+ if (key.return) {
45
+ const newValue = value.slice(0, cursorPos) + '\n' + value.slice(cursorPos);
46
+ onChange(newValue);
47
+ setCursorPos(cursorPos + 1);
48
+ return;
49
+ }
50
+ // Backspace
51
+ if (key.backspace || key.delete) {
52
+ if (cursorPos > 0) {
53
+ const newValue = value.slice(0, cursorPos - 1) + value.slice(cursorPos);
54
+ onChange(newValue);
55
+ setCursorPos(cursorPos - 1);
56
+ }
57
+ return;
58
+ }
59
+ // Arrow keys for cursor movement
60
+ if (key.leftArrow) {
61
+ setCursorPos(Math.max(0, cursorPos - 1));
62
+ return;
63
+ }
64
+ if (key.rightArrow) {
65
+ setCursorPos(Math.min(value.length, cursorPos + 1));
66
+ return;
67
+ }
68
+ if (key.upArrow) {
69
+ const beforeCursor = value.slice(0, cursorPos);
70
+ const currentLineStart = beforeCursor.lastIndexOf('\n') + 1;
71
+ if (currentLineStart > 0) {
72
+ const posInLine = cursorPos - currentLineStart;
73
+ const prevLineEnd = currentLineStart - 1;
74
+ const prevLineStart = value.lastIndexOf('\n', prevLineEnd - 1) + 1;
75
+ const prevLineLength = prevLineEnd - prevLineStart;
76
+ const newPos = prevLineStart + Math.min(posInLine, prevLineLength);
77
+ setCursorPos(newPos);
78
+ }
79
+ return;
80
+ }
81
+ if (key.downArrow) {
82
+ const nextLineStart = value.indexOf('\n', cursorPos);
83
+ if (nextLineStart !== -1) {
84
+ const currentLineStart = value.lastIndexOf('\n', cursorPos - 1) + 1;
85
+ const posInLine = cursorPos - currentLineStart;
86
+ const nextLineEnd = value.indexOf('\n', nextLineStart + 1);
87
+ const nextLineLength = (nextLineEnd === -1 ? value.length : nextLineEnd) - nextLineStart - 1;
88
+ const newPos = nextLineStart + 1 + Math.min(posInLine, nextLineLength);
89
+ setCursorPos(newPos);
90
+ }
91
+ return;
92
+ }
93
+ // Page up/down for scrolling
94
+ if (key.pageUp) {
95
+ setScrollOffset(Math.max(0, scrollOffset - (height - 2)));
96
+ return;
97
+ }
98
+ if (key.pageDown) {
99
+ const totalLines = (value.match(/\n/g) || []).length + 1;
100
+ setScrollOffset(Math.min(totalLines - 1, scrollOffset + (height - 2)));
101
+ return;
102
+ }
103
+ // Regular character input
104
+ if (input && !key.ctrl && !key.meta && input.charCodeAt(0) >= 32) {
105
+ const newValue = value.slice(0, cursorPos) + input + value.slice(cursorPos);
106
+ onChange(newValue);
107
+ setCursorPos(cursorPos + input.length);
108
+ }
109
+ });
110
+ // Render
111
+ const displayValue = value || '';
112
+ const showPlaceholder = !value && placeholder;
113
+ const lines = (showPlaceholder ? placeholder : displayValue).split('\n');
114
+ const totalLines = lines.length;
115
+ const visibleLines = height - 2;
116
+ // Calculate cursor position
117
+ let cursorLine = 0;
118
+ let cursorCol = 0;
119
+ if (!showPlaceholder && value) {
120
+ const beforeCursor = value.slice(0, cursorPos);
121
+ cursorLine = (beforeCursor.match(/\n/g) || []).length;
122
+ const lastNewline = beforeCursor.lastIndexOf('\n');
123
+ cursorCol = lastNewline === -1 ? cursorPos : cursorPos - lastNewline - 1;
124
+ }
125
+ // Get visible lines with scroll
126
+ const visibleStart = scrollOffset;
127
+ const visibleEnd = Math.min(scrollOffset + visibleLines, totalLines);
128
+ const displayLines = lines.slice(visibleStart, visibleEnd);
129
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", padding: 1, height: height, children: [displayLines.map((line, displayIdx) => {
130
+ const actualLineIdx = visibleStart + displayIdx;
131
+ const isCursorLine = actualLineIdx === cursorLine && !showPlaceholder;
132
+ if (isCursorLine) {
133
+ const before = line.slice(0, cursorCol);
134
+ const cursorChar = line[cursorCol] || ' ';
135
+ const after = line.slice(cursorCol + 1);
136
+ return (_jsx(Box, { children: _jsxs(Text, { children: [before, _jsx(Text, { inverse: true, children: cursorChar }), after] }) }, actualLineIdx));
137
+ }
138
+ return (_jsx(Box, { children: _jsx(Text, { dimColor: showPlaceholder ? true : undefined, children: line || ' ' }) }, actualLineIdx));
139
+ }), totalLines > visibleLines && (_jsx(Box, { justifyContent: "flex-end", children: _jsxs(Text, { dimColor: true, children: ["[", visibleStart + 1, "-", visibleEnd, "/", totalLines, "]"] }) }))] }));
140
+ }
@@ -0,0 +1,5 @@
1
+ export { Header } from './Header.js';
2
+ export { StatusBar } from './StatusBar.js';
3
+ export { Panel } from './Panel.js';
4
+ export { List, type ListItem } from './List.js';
5
+ export { HelpOverlay } from './HelpOverlay.js';
@@ -0,0 +1,5 @@
1
+ export { Header } from './Header.js';
2
+ export { StatusBar } from './StatusBar.js';
3
+ export { Panel } from './Panel.js';
4
+ export { List } from './List.js';
5
+ export { HelpOverlay } from './HelpOverlay.js';