@colmbus72/yeehaw 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,5 +27,5 @@ function renderHints(hints) {
27
27
  return parts;
28
28
  }
29
29
  export function Panel({ title, children, focused = false, width, hints }) {
30
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: focused ? BRAND_COLOR : 'gray', width: width, children: [_jsx(Box, { paddingX: 1, marginBottom: 0, children: _jsx(Text, { bold: true, color: focused ? BRAND_COLOR : 'gray', children: title }) }), _jsx(Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: children }), _jsx(Box, { paddingX: 1, justifyContent: "flex-end", height: 1, children: focused && hints ? (_jsx(Text, { children: renderHints(hints) })) : (_jsx(Text, { children: " " })) })] }));
30
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: focused ? BRAND_COLOR : 'gray', width: width, flexShrink: 1, overflow: "hidden", children: [_jsx(Box, { paddingX: 1, marginBottom: 0, flexShrink: 0, children: _jsx(Text, { bold: true, color: focused ? BRAND_COLOR : 'gray', children: title }) }), _jsx(Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, flexShrink: 1, overflow: "hidden", children: children }), _jsx(Box, { paddingX: 1, justifyContent: "flex-end", height: 1, flexShrink: 0, children: focused && hints ? (_jsx(Text, { children: renderHints(hints) })) : (_jsx(Text, { children: " " })) })] }));
31
31
  }
@@ -52,5 +52,5 @@ export function ScrollableMarkdown({ children, focused = false, height = 20, })
52
52
  // Get visible slice of lines
53
53
  const displayLines = lines.slice(scrollOffset, scrollOffset + visibleLines);
54
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)' : ''] }) }))] }));
55
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: [_jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, children: displayLines.map((line, idx) => (_jsx(Text, { wrap: "truncate", children: line || ' ' }, scrollOffset + idx))) }), showScrollIndicator && (_jsx(Box, { justifyContent: "flex-end", flexShrink: 0, children: _jsxs(Text, { dimColor: true, children: ["[", scrollOffset + 1, "-", Math.min(scrollOffset + visibleLines, totalLines), "/", totalLines, "]", focused ? ' (j/k to scroll)' : ''] }) }))] }));
56
56
  }
package/dist/lib/tmux.js CHANGED
@@ -170,6 +170,8 @@ function setWindowType(windowIndex, type) {
170
170
  ]);
171
171
  }
172
172
  export function createClaudeWindow(workingDir, windowName) {
173
+ // Ensure workingDir is valid, fallback to current working directory
174
+ const effectiveWorkingDir = workingDir || process.cwd();
173
175
  // Build MCP config for yeehaw server
174
176
  const mcpConfig = JSON.stringify({
175
177
  mcpServers: {
@@ -190,7 +192,7 @@ export function createClaudeWindow(workingDir, windowName) {
190
192
  '-a',
191
193
  '-t', YEEHAW_SESSION,
192
194
  '-n', windowName,
193
- '-c', workingDir,
195
+ '-c', effectiveWorkingDir,
194
196
  claudeCmd,
195
197
  ]);
196
198
  // Get the window index we just created (new window is now current)
@@ -203,6 +205,8 @@ export function createClaudeWindow(workingDir, windowName) {
203
205
  return windowIndex;
204
206
  }
205
207
  export function createClaudeWindowWithPrompt(workingDir, windowName, systemPrompt) {
208
+ // Ensure workingDir is valid, fallback to current working directory
209
+ const effectiveWorkingDir = workingDir || process.cwd();
206
210
  // Build MCP config for yeehaw server
207
211
  const mcpConfig = JSON.stringify({
208
212
  mcpServers: {
@@ -223,7 +227,7 @@ export function createClaudeWindowWithPrompt(workingDir, windowName, systemPromp
223
227
  '-a',
224
228
  '-t', YEEHAW_SESSION,
225
229
  '-n', windowName,
226
- '-c', workingDir,
230
+ '-c', effectiveWorkingDir,
227
231
  claudeCmd,
228
232
  ]);
229
233
  // Get the window index we just created
@@ -1,8 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // src/views/IssuesView.tsx
3
3
  import { useState, useEffect, useCallback } from 'react';
4
- import { Box, Text, useInput } from 'ink';
4
+ import { Box, Text, useInput, useStdout } from 'ink';
5
5
  import TextInput from 'ink-text-input';
6
+ import { join } from 'path';
7
+ import { homedir } from 'os';
6
8
  import { Header } from '../components/Header.js';
7
9
  import { Panel } from '../components/Panel.js';
8
10
  import { List } from '../components/List.js';
@@ -12,6 +14,13 @@ import { saveProject } from '../lib/config.js';
12
14
  import { buildProjectContext } from '../lib/context.js';
13
15
  import { openIssueInBrowser } from '../lib/github.js';
14
16
  import { saveLinearApiKey, validateLinearApiKey, LINEAR_API_KEY_URL, clearLinearToken } from '../lib/auth/linear.js';
17
+ // Expand ~ in paths to home directory
18
+ function expandPath(path) {
19
+ if (path.startsWith('~/')) {
20
+ return join(homedir(), path.slice(2));
21
+ }
22
+ return path;
23
+ }
15
24
  // Status indicator based on Linear state type
16
25
  function getStatusIndicator(stateType) {
17
26
  switch (stateType) {
@@ -51,15 +60,21 @@ function getInitials(name) {
51
60
  return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
52
61
  }
53
62
  export function IssuesView({ project, onBack, onOpenClaude }) {
63
+ const { stdout } = useStdout();
54
64
  const [focusedPanel, setFocusedPanel] = useState('list');
55
65
  const [selectedIndex, setSelectedIndex] = useState(0);
56
66
  const [viewState, setViewState] = useState({ type: 'loading' });
57
67
  const [linearProvider, setLinearProvider] = useState(null);
58
68
  const [apiKeyInput, setApiKeyInput] = useState('');
69
+ // Calculate dynamic height for ScrollableMarkdown based on terminal size
70
+ // Layout: Header(3) + FilterInfo(1) + PanelBorders(2) + PanelTitle(1) + PanelHints(1) + Padding(2) = ~10 lines overhead
71
+ const terminalHeight = stdout?.rows ?? 24;
72
+ const panelContentHeight = Math.max(8, terminalHeight - 10);
59
73
  // Filter state for Linear
60
74
  const [cycles, setCycles] = useState([]);
61
75
  const [assignees, setAssignees] = useState([]);
62
76
  const [currentFilter, setCurrentFilter] = useState({
77
+ stateType: ['backlog', 'unstarted', 'started'], // Default to open issues
63
78
  sortBy: 'priority',
64
79
  sortDirection: 'desc',
65
80
  });
@@ -71,6 +86,9 @@ export function IssuesView({ project, onBack, onOpenClaude }) {
71
86
  const [teamName, setTeamName] = useState();
72
87
  // Cached current user ID for filter
73
88
  const [currentUserId, setCurrentUserId] = useState(null);
89
+ // Track if user explicitly selected "Me" filter (separate from actual ID)
90
+ // Default to true since we want "assigned to me" as the default filter
91
+ const [filterByMe, setFilterByMe] = useState(true);
74
92
  const loadIssues = useCallback(async (filter) => {
75
93
  setViewState({ type: 'loading' });
76
94
  const provider = getProvider(project);
@@ -116,14 +134,23 @@ export function IssuesView({ project, onBack, onOpenClaude }) {
116
134
  setCycles(fetchedCycles);
117
135
  setAssignees(fetchedAssignees);
118
136
  setCurrentUserId(userId);
119
- // Set default filter: current cycle, assigned to me
137
+ // Set default filter: current cycle, assigned to me, open issues
120
138
  const activeCycleId = linearProv.getActiveCycleId();
121
139
  const defaultFilter = {
122
140
  cycleId: activeCycleId,
123
- assigneeId: userId ?? undefined,
141
+ stateType: ['backlog', 'unstarted', 'started'],
124
142
  sortBy: 'priority',
125
143
  sortDirection: 'desc',
126
144
  };
145
+ // Only add assigneeId if we have a valid user ID
146
+ if (userId) {
147
+ defaultFilter.assigneeId = userId;
148
+ setFilterByMe(true);
149
+ }
150
+ else {
151
+ // Couldn't get user ID, so can't filter by "me"
152
+ setFilterByMe(false);
153
+ }
127
154
  setCurrentFilter(defaultFilter);
128
155
  setFilterInitialized(true);
129
156
  filter = defaultFilter;
@@ -189,14 +216,23 @@ export function IssuesView({ project, onBack, onOpenClaude }) {
189
216
  setCycles(fetchedCycles);
190
217
  setAssignees(fetchedAssignees);
191
218
  setCurrentUserId(userId);
192
- // Set default filter
219
+ // Set default filter: current cycle, assigned to me, open issues
193
220
  const activeCycleId = linearProvider.getActiveCycleId();
194
221
  const defaultFilter = {
195
222
  cycleId: activeCycleId,
196
- assigneeId: userId ?? undefined,
223
+ stateType: ['backlog', 'unstarted', 'started'],
197
224
  sortBy: 'priority',
198
225
  sortDirection: 'desc',
199
226
  };
227
+ // Only add assigneeId if we have a valid user ID
228
+ if (userId) {
229
+ defaultFilter.assigneeId = userId;
230
+ setFilterByMe(true);
231
+ }
232
+ else {
233
+ // Couldn't get user ID, so can't filter by "me"
234
+ setFilterByMe(false);
235
+ }
200
236
  setCurrentFilter(defaultFilter);
201
237
  setFilterInitialized(true);
202
238
  // Fetch issues
@@ -400,16 +436,23 @@ export function IssuesView({ project, onBack, onOpenClaude }) {
400
436
  switch (field) {
401
437
  case 'assignee':
402
438
  if (option.id === '__me__') {
403
- newFilter.assigneeId = currentUserId ?? undefined;
439
+ // Only set if we have a valid user ID
440
+ if (currentUserId) {
441
+ newFilter.assigneeId = currentUserId;
442
+ }
443
+ setFilterByMe(true);
404
444
  }
405
445
  else if (option.id === '__any__') {
406
446
  delete newFilter.assigneeId;
447
+ setFilterByMe(false);
407
448
  }
408
449
  else if (option.id === '__none__') {
409
450
  newFilter.assigneeId = null;
451
+ setFilterByMe(false);
410
452
  }
411
453
  else {
412
454
  newFilter.assigneeId = option.id;
455
+ setFilterByMe(false);
413
456
  }
414
457
  break;
415
458
  case 'cycle':
@@ -459,17 +502,25 @@ export function IssuesView({ project, onBack, onOpenClaude }) {
459
502
  switch (field) {
460
503
  case 'assignee':
461
504
  if (option.id === '__me__') {
462
- const userId = await linearProvider?.getCurrentUserId();
463
- newFilter.assigneeId = userId ?? undefined;
505
+ // Use cached currentUserId or fetch it
506
+ const userId = currentUserId || await linearProvider?.getCurrentUserId();
507
+ if (userId) {
508
+ newFilter.assigneeId = userId;
509
+ setCurrentUserId(userId); // Cache it if we just fetched
510
+ }
511
+ setFilterByMe(true);
464
512
  }
465
513
  else if (option.id === '__any__') {
466
514
  delete newFilter.assigneeId;
515
+ setFilterByMe(false);
467
516
  }
468
517
  else if (option.id === '__none__') {
469
518
  newFilter.assigneeId = null;
519
+ setFilterByMe(false);
470
520
  }
471
521
  else {
472
522
  newFilter.assigneeId = option.id;
523
+ setFilterByMe(false);
473
524
  }
474
525
  break;
475
526
  case 'cycle':
@@ -519,6 +570,13 @@ export function IssuesView({ project, onBack, onOpenClaude }) {
519
570
  else {
520
571
  workingDir = project.path;
521
572
  }
573
+ // Expand ~ to home directory and fallback to cwd if empty
574
+ if (workingDir) {
575
+ workingDir = expandPath(workingDir);
576
+ }
577
+ else {
578
+ workingDir = process.cwd();
579
+ }
522
580
  const context = buildClaudeContext(issue);
523
581
  onOpenClaude(workingDir, context);
524
582
  };
@@ -692,21 +750,72 @@ export function IssuesView({ project, onBack, onOpenClaude }) {
692
750
  if (!isLinear)
693
751
  return '';
694
752
  const parts = [];
695
- if (currentFilter.assigneeId) {
753
+ // Assignee - check explicitly for the different states
754
+ // assigneeId can be: string (specific user), null (unassigned), or undefined (anyone)
755
+ if (currentFilter.assigneeId === null) {
756
+ parts.push('Unassigned');
757
+ }
758
+ else if (filterByMe) {
759
+ // User explicitly selected "Me" filter
760
+ parts.push('Me');
761
+ }
762
+ else if (currentFilter.assigneeId !== undefined && currentFilter.assigneeId !== '') {
763
+ // Has a specific assignee ID (not "me")
696
764
  const assignee = assignees.find((a) => a.id === currentFilter.assigneeId);
697
- parts.push(assignee ? assignee.name : 'Me');
765
+ parts.push(assignee?.name ?? 'Assigned');
698
766
  }
699
- else if (currentFilter.assigneeId === null) {
700
- parts.push('Unassigned');
767
+ else {
768
+ // undefined or empty string means anyone
769
+ parts.push('Anyone');
701
770
  }
771
+ // Cycle
702
772
  if (currentFilter.cycleId) {
703
773
  const cycle = cycles.find((c) => c.id === currentFilter.cycleId);
704
774
  parts.push(cycle?.name ?? 'Cycle');
705
775
  }
706
- return parts.length > 0 ? parts.join(' • ') : 'All issues';
776
+ else {
777
+ parts.push('Any cycle');
778
+ }
779
+ // Status
780
+ if (currentFilter.stateType) {
781
+ const stateType = currentFilter.stateType;
782
+ if (Array.isArray(stateType)) {
783
+ // Check if it's the "open" preset
784
+ const isOpenPreset = stateType.length === 3 &&
785
+ stateType.includes('backlog') &&
786
+ stateType.includes('unstarted') &&
787
+ stateType.includes('started');
788
+ parts.push(isOpenPreset ? 'Open' : stateType.join(', '));
789
+ }
790
+ else {
791
+ // Single status
792
+ const statusLabels = {
793
+ backlog: 'Backlog',
794
+ unstarted: 'Todo',
795
+ started: 'In Progress',
796
+ completed: 'Done',
797
+ canceled: 'Canceled',
798
+ };
799
+ parts.push(statusLabels[stateType] ?? stateType);
800
+ }
801
+ }
802
+ else {
803
+ parts.push('All statuses');
804
+ }
805
+ // Sort
806
+ if (currentFilter.sortBy === 'priority') {
807
+ parts.push(currentFilter.sortDirection === 'asc' ? '↑Priority' : '↓Priority');
808
+ }
809
+ else if (currentFilter.sortBy === 'createdAt') {
810
+ parts.push('Recent');
811
+ }
812
+ else if (currentFilter.sortBy === 'updatedAt') {
813
+ parts.push('Updated');
814
+ }
815
+ return parts.join(' • ');
707
816
  };
708
817
  const detailsHints = 'j/k scroll';
709
- return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: getSubtitle(), color: project.color }), isLinear && (_jsxs(Box, { paddingX: 2, children: [_jsx(Text, { dimColor: true, children: "Showing: " }), _jsx(Text, { color: "cyan", children: getFilterDescription() }), _jsxs(Text, { dimColor: true, children: [" (", viewState.issues.length, ")"] })] })), _jsxs(Box, { flexGrow: 1, paddingX: 1, gap: 2, children: [_jsx(Panel, { title: "Issues", focused: focusedPanel === 'list', width: "45%", children: issueItems.length > 0 ? (_jsx(List, { items: issueItems, focused: focusedPanel === 'list', selectedIndex: selectedIndex, onSelectionChange: setSelectedIndex, onAction: (item, actionKey) => {
818
+ return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [_jsx(Header, { text: project.name, subtitle: getSubtitle(), color: project.color }), isLinear && (_jsxs(Box, { paddingX: 2, children: [_jsx(Text, { dimColor: true, children: "Showing: " }), _jsx(Text, { color: "cyan", children: getFilterDescription() }), _jsxs(Text, { dimColor: true, children: [" (", viewState.issues.length, ")"] })] })), _jsxs(Box, { flexGrow: 1, flexShrink: 1, paddingX: 1, gap: 2, overflow: "hidden", children: [_jsx(Panel, { title: "Issues", focused: focusedPanel === 'list', width: "45%", children: issueItems.length > 0 ? (_jsx(List, { items: issueItems, focused: focusedPanel === 'list', selectedIndex: selectedIndex, onSelectionChange: setSelectedIndex, onAction: (item, actionKey) => {
710
819
  const issue = viewState.issues[parseInt(item.id, 10)];
711
820
  if (!issue)
712
821
  return;
@@ -716,5 +825,5 @@ export function IssuesView({ project, onBack, onOpenClaude }) {
716
825
  else if (actionKey === 'o') {
717
826
  openIssueInBrowser(issue.url);
718
827
  }
719
- } })) : (_jsx(Text, { dimColor: true, children: "No issues match the current filter" })) }), _jsx(Panel, { title: selectedIssue ? `${selectedIssue.identifier} ${truncate(selectedIssue.title, 30)}` : 'Details', focused: focusedPanel === 'details', width: "55%", hints: detailsHints, children: selectedIssue ? (_jsx(ScrollableMarkdown, { focused: focusedPanel === 'details', height: 20, children: buildIssueDetails(selectedIssue) })) : (_jsx(Text, { dimColor: true, children: "Select an issue to view details" })) })] })] }));
828
+ } })) : (_jsx(Text, { dimColor: true, children: "No issues match the current filter" })) }), _jsx(Panel, { title: selectedIssue ? `${selectedIssue.identifier} ${truncate(selectedIssue.title, 30)}` : 'Details', focused: focusedPanel === 'details', width: "55%", hints: detailsHints, children: selectedIssue ? (_jsx(ScrollableMarkdown, { focused: focusedPanel === 'details', height: panelContentHeight, children: buildIssueDetails(selectedIssue) })) : (_jsx(Text, { dimColor: true, children: "Select an issue to view details" })) })] })] }));
720
829
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colmbus72/yeehaw",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Terminal dashboard for managing projects, servers, and deployments",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",