@sascha384/tic 4.8.0 → 5.0.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 (51) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/dist/auth/gitlab.js +12 -5
  3. package/dist/auth/gitlab.js.map +1 -1
  4. package/dist/commands.d.ts +5 -0
  5. package/dist/commands.js +90 -1
  6. package/dist/commands.js.map +1 -1
  7. package/dist/components/BranchList.js +417 -281
  8. package/dist/components/BranchList.js.map +1 -1
  9. package/dist/components/ColorPill.js +3 -3
  10. package/dist/components/ColorPill.js.map +1 -1
  11. package/dist/components/CommandBar.d.ts +8 -0
  12. package/dist/components/CommandBar.js +142 -0
  13. package/dist/components/CommandBar.js.map +1 -0
  14. package/dist/components/DetailPanel.js +2 -2
  15. package/dist/components/DetailPanel.js.map +1 -1
  16. package/dist/components/ErrorBoundary.d.ts +1 -1
  17. package/dist/components/Header.js +6 -2
  18. package/dist/components/Header.js.map +1 -1
  19. package/dist/components/HelpScreen.js +3 -2
  20. package/dist/components/HelpScreen.js.map +1 -1
  21. package/dist/components/OverlayPanel.d.ts +1 -1
  22. package/dist/components/PullRequestList.js +118 -18
  23. package/dist/components/PullRequestList.js.map +1 -1
  24. package/dist/components/Settings.js +5 -0
  25. package/dist/components/Settings.js.map +1 -1
  26. package/dist/components/StatusScreen.js +16 -8
  27. package/dist/components/StatusScreen.js.map +1 -1
  28. package/dist/components/TableLayout.d.ts +26 -10
  29. package/dist/components/TableLayout.js +67 -146
  30. package/dist/components/TableLayout.js.map +1 -1
  31. package/dist/components/WorkItemForm.js +5 -0
  32. package/dist/components/WorkItemForm.js.map +1 -1
  33. package/dist/components/WorkItemList.js +119 -93
  34. package/dist/components/WorkItemList.js.map +1 -1
  35. package/dist/git.d.ts +8 -0
  36. package/dist/git.js.map +1 -1
  37. package/dist/implement.js +7 -3
  38. package/dist/implement.js.map +1 -1
  39. package/dist/stores/backendDataStore.d.ts +6 -0
  40. package/dist/stores/backendDataStore.js +81 -1
  41. package/dist/stores/backendDataStore.js.map +1 -1
  42. package/dist/stores/navigationStore.d.ts +4 -0
  43. package/dist/stores/navigationStore.js +10 -0
  44. package/dist/stores/navigationStore.js.map +1 -1
  45. package/dist/stores/uiStore.d.ts +12 -0
  46. package/dist/stores/uiStore.js.map +1 -1
  47. package/dist/test-helpers.d.ts +7 -0
  48. package/dist/test-helpers.js +208 -0
  49. package/dist/test-helpers.js.map +1 -0
  50. package/drizzle/meta/_journal.json +1 -1
  51. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useState, useMemo, useEffect, useCallback, useRef } from 'react';
3
3
  import { Box, Text, useInput, useApp } from 'ink';
4
4
  import { navigationStore, useNavigationStore, } from '../stores/navigationStore.js';
@@ -9,6 +9,7 @@ import { configStore, useConfigStore } from '../stores/configStore.js';
9
9
  import { uiStore, useUIStore, getOverlayTargetIds } from '../stores/uiStore.js';
10
10
  import { getMarkedDistribution } from './getMarkedDistribution.js';
11
11
  import { TableLayout } from './TableLayout.js';
12
+ import { ColorPill } from './ColorPill.js';
12
13
  import { useTerminalSize } from '../hooks/useTerminalSize.js';
13
14
  import { useScrollViewport } from '../hooks/useScrollViewport.js';
14
15
  import { useBackendDataStore, backendDataStore, } from '../stores/backendDataStore.js';
@@ -18,12 +19,108 @@ import { getVisibleCommands, } from '../commands.js';
18
19
  import { OverlayPanel } from './OverlayPanel.js';
19
20
  import { DetailPanel } from './DetailPanel.js';
20
21
  import { undoStore } from '../stores/undoStore.js';
21
- import { recentCommandsStore, useRecentCommandsStore, } from '../stores/recentCommandsStore.js';
22
+ import { CommandBar } from './CommandBar.js';
22
23
  import { isSoftDeleteBackend } from '../backends/types.js';
23
24
  import { filterStore, useFilterStore } from '../stores/filterStore.js';
24
25
  import { useThemeStore } from '../stores/themeStore.js';
25
26
  import { applyFilters, countActiveFilters, summarizeFilters, } from '../filters.js';
26
27
  const EMPTY_VIEWS = [];
28
+ function buildWorkItemColumns(capabilities, collapsedIds, accent) {
29
+ const columns = [];
30
+ // ID column
31
+ columns.push({
32
+ key: 'id',
33
+ header: 'ID',
34
+ width: 4, // overridden dynamically via useMemo
35
+ required: true,
36
+ sortable: true,
37
+ render: (ti, selected) => (_jsx(Text, { color: selected ? accent : undefined, bold: selected, dimColor: ti.isCrossType && !selected, children: ti.item.id })),
38
+ });
39
+ // Title column (flex)
40
+ columns.push({
41
+ key: 'title',
42
+ header: 'Title',
43
+ width: -1,
44
+ required: true,
45
+ sortable: true,
46
+ render: (ti, selected) => {
47
+ const { item, prefix, isCrossType, hasChildren } = ti;
48
+ const collapseIndicator = hasChildren
49
+ ? collapsedIds.has(item.id)
50
+ ? '\u25B6 '
51
+ : '\u25BC '
52
+ : ' ';
53
+ const typeLabel = isCrossType ? ` (${item.type})` : '';
54
+ return (_jsxs(Text, { color: selected ? accent : undefined, bold: selected, dimColor: isCrossType && !selected, wrap: "truncate", children: [capabilities.relationships ? prefix : '', collapseIndicator, item.title, typeLabel] }));
55
+ },
56
+ });
57
+ // Status column
58
+ columns.push({
59
+ key: 'status',
60
+ header: 'Status',
61
+ width: 14,
62
+ hidePriority: 3,
63
+ sortable: true,
64
+ render: (ti, selected) => {
65
+ const hasUnresolvedDeps = ti.item.dependsOn.length > 0;
66
+ return (_jsxs(_Fragment, { children: [capabilities.fields.dependsOn && hasUnresolvedDeps && (_jsxs(Text, { dimColor: ti.isCrossType && !selected, children: ['\u29D7', " "] })), _jsx(ColorPill, { field: "status", value: ti.item.status })] }));
67
+ },
68
+ });
69
+ // Assignee column (conditional)
70
+ if (capabilities.fields.assignee) {
71
+ columns.push({
72
+ key: 'assignee',
73
+ header: 'Assignee',
74
+ width: 20,
75
+ hidePriority: 4,
76
+ sortable: true,
77
+ hasData: (items) => items.some(({ item }) => !!item.assignee),
78
+ render: (ti, selected) => (_jsx(Text, { color: selected ? accent : undefined, bold: selected, dimColor: ti.isCrossType && !selected, wrap: "truncate", children: ti.item.assignee })),
79
+ });
80
+ }
81
+ // Labels column (conditional)
82
+ if (capabilities.fields.labels) {
83
+ columns.push({
84
+ key: 'labels',
85
+ header: 'Labels',
86
+ width: 20,
87
+ hidePriority: 2,
88
+ hasData: (items) => items.some(({ item }) => item.labels.length > 0),
89
+ render: (ti) => {
90
+ const maxWidth = 20;
91
+ const rendered = [];
92
+ let usedWidth = 0;
93
+ for (const label of ti.item.labels) {
94
+ const pillWidth = label.length + 2;
95
+ const needed = usedWidth === 0 ? pillWidth : pillWidth + 1;
96
+ if (usedWidth + needed > maxWidth) {
97
+ const remaining = ti.item.labels.length - rendered.length;
98
+ if (remaining > 0) {
99
+ return (_jsxs(Box, { gap: 1, children: [rendered.map((l) => (_jsx(ColorPill, { field: "label", value: l }, l))), _jsxs(Text, { dimColor: true, children: ["+", remaining] })] }));
100
+ }
101
+ break;
102
+ }
103
+ rendered.push(label);
104
+ usedWidth += needed;
105
+ }
106
+ return (_jsx(Box, { gap: 1, children: rendered.map((l) => (_jsx(ColorPill, { field: "label", value: l }, l))) }));
107
+ },
108
+ });
109
+ }
110
+ // Priority column (conditional)
111
+ if (capabilities.fields.priority) {
112
+ columns.push({
113
+ key: 'priority',
114
+ header: 'Priority',
115
+ width: 12,
116
+ hidePriority: 1,
117
+ sortable: true,
118
+ hasData: (items) => items.some(({ item }) => !!item.priority),
119
+ render: (ti) => ti.item.priority ? (_jsx(ColorPill, { field: "priority", value: ti.item.priority })) : (_jsx(Text, { children: " " })),
120
+ });
121
+ }
122
+ return columns;
123
+ }
27
124
  export function getTargetIds(markedIds, cursorItem) {
28
125
  if (markedIds.size > 0) {
29
126
  return [...markedIds];
@@ -102,11 +199,9 @@ export function WorkItemList() {
102
199
  const filterCount = useMemo(() => countActiveFilters(activeFilters), [activeFilters]);
103
200
  const { setCursor, toggleExpanded, toggleMarked, clearMarked, setMarkedIds, setRangeAnchor, clampCursor, removeDeletedItem, toggleSortColumn, clearSort, } = listViewStore.getState();
104
201
  // Local state for inputs and templates
105
- const [allSearchItems, setAllSearchItems] = useState([]);
106
202
  const [templates, setTemplates] = useState([]);
107
203
  const [showFullDescription, setShowFullDescription] = useState(false);
108
204
  const [descriptionScrollOffset, setDescriptionScrollOffset] = useState(0);
109
- const [commandBarQuery, setCommandBarQuery] = useState('');
110
205
  // UI overlay state from store
111
206
  const { activeOverlay, warning, toast } = useUIStore(useShallow((s) => ({
112
207
  activeOverlay: s.activeOverlay,
@@ -268,25 +363,6 @@ export function WorkItemList() {
268
363
  setShowFullDescription(false);
269
364
  setDescriptionScrollOffset(0);
270
365
  }, [cursor]);
271
- useEffect(() => {
272
- if (activeOverlay?.type !== 'command-bar' || !backend)
273
- return;
274
- let cancelled = false;
275
- void backend
276
- .listWorkItems()
277
- .then((items) => {
278
- if (!cancelled)
279
- setAllSearchItems(items);
280
- })
281
- .catch((err) => {
282
- uiStore
283
- .getState()
284
- .setToast(err instanceof Error ? err.message : 'Failed to load items');
285
- });
286
- return () => {
287
- cancelled = true;
288
- };
289
- }, [activeOverlay?.type, backend]);
290
366
  // Description viewport calculation
291
367
  const currentItem = treeItems[cursor]?.item;
292
368
  const descriptionLines = currentItem?.description?.split('\n') ?? [];
@@ -520,9 +596,12 @@ export function WorkItemList() {
520
596
  const comments = item.comments;
521
597
  try {
522
598
  const itemUrl = backend?.getItemUrl(item.id) || '';
599
+ // Suspend terminal for interactive child process
600
+ process.stdin.setRawMode?.(false);
523
601
  const result = beginImplementation(item, comments, { branchMode, branchCommand, copyToClipboard }, process.cwd(), { itemUrl });
524
- // Restore raw mode after interactive shell changed terminal settings
602
+ // Restore terminal after interactive shell
525
603
  process.stdin.setRawMode?.(true);
604
+ console.clear();
526
605
  let msg = result.resumed
527
606
  ? `Resumed work on #${item.id}`
528
607
  : `Started work on #${item.id}`;
@@ -533,6 +612,7 @@ export function WorkItemList() {
533
612
  }
534
613
  catch (e) {
535
614
  process.stdin.setRawMode?.(true);
615
+ console.clear();
536
616
  setWarning(e instanceof Error ? e.message : 'Failed to start implementation');
537
617
  }
538
618
  void backendDataStore
@@ -665,12 +745,6 @@ export function WorkItemList() {
665
745
  }
666
746
  }
667
747
  }, { isActive: activeOverlay === null && !showFullDescription });
668
- const handleSearchSelect = (item) => {
669
- setCommandBarQuery('');
670
- closeOverlay();
671
- selectWorkItem(item.id);
672
- navigate('form');
673
- };
674
748
  const commandContext = {
675
749
  screen: 'list',
676
750
  markedCount: markedIds.size,
@@ -682,6 +756,11 @@ export function WorkItemList() {
682
756
  gitAvailable,
683
757
  hasActiveFilters: filterCount > 0,
684
758
  hasSavedViews: savedViews.length > 0,
759
+ hasSelectedBranch: false,
760
+ isCurrentBranch: false,
761
+ hasWorktree: false,
762
+ hasPrCreateCapability: false,
763
+ hasSelectedPr: false,
685
764
  };
686
765
  const paletteCommands = useMemo(() => getVisibleCommands(commandContext), [
687
766
  commandContext.markedCount,
@@ -692,52 +771,6 @@ export function WorkItemList() {
692
771
  syncManager,
693
772
  gitAvailable,
694
773
  ]);
695
- const recentIds = useRecentCommandsStore((s) => s.recentIds);
696
- const commandBarItems = useMemo(() => {
697
- const query = commandBarQuery.toLowerCase();
698
- // Build command items (recent + categorized)
699
- const commandMap = new Map(paletteCommands.map((c) => [c.id, c]));
700
- const recentItems = [];
701
- for (const id of recentIds) {
702
- const cmd = commandMap.get(id);
703
- if (cmd) {
704
- recentItems.push({
705
- id: `recent-${cmd.id}`,
706
- label: cmd.label,
707
- value: cmd.id,
708
- category: 'Recent',
709
- kind: 'command',
710
- });
711
- }
712
- }
713
- const commandItems = paletteCommands.map((cmd) => ({
714
- id: cmd.id,
715
- label: cmd.label,
716
- value: cmd.id,
717
- category: cmd.category,
718
- kind: 'command',
719
- }));
720
- let allItems = [...recentItems, ...commandItems];
721
- // Filter commands by query
722
- if (query) {
723
- allItems = allItems.filter((item) => item.label.toLowerCase().includes(query));
724
- // Add up to 5 matching issues
725
- const matchingIssues = allSearchItems
726
- .filter((item) => item.title.toLowerCase().includes(query) ||
727
- item.id.toLowerCase().includes(query))
728
- .slice(0, 5)
729
- .map((item) => ({
730
- id: `issue-${item.id}`,
731
- label: `#${item.id} ${item.title}`,
732
- value: item.id,
733
- hint: item.type,
734
- category: 'Issues',
735
- kind: 'issue',
736
- }));
737
- allItems = [...allItems, ...matchingIssues];
738
- }
739
- return allItems;
740
- }, [paletteCommands, recentIds, commandBarQuery, allSearchItems]);
741
774
  const sortPickerItems = useMemo(() => {
742
775
  const columns = [
743
776
  { column: 'id', label: 'ID' },
@@ -874,9 +907,7 @@ export function WorkItemList() {
874
907
  return items;
875
908
  }, [savedViews, defaultView, activeViewName, lastViewName, filterCount]);
876
909
  const handleCommandSelect = (command) => {
877
- setCommandBarQuery('');
878
910
  closeOverlay();
879
- recentCommandsStore.getState().addRecent(command.id);
880
911
  switch (command.id) {
881
912
  case 'create':
882
913
  selectWorkItem(null);
@@ -914,8 +945,10 @@ export function WorkItemList() {
914
945
  const comments = item.comments;
915
946
  try {
916
947
  const itemUrl = backend?.getItemUrl(item.id) || '';
948
+ process.stdin.setRawMode?.(false);
917
949
  const result = beginImplementation(item, comments, { branchMode, branchCommand, copyToClipboard }, process.cwd(), { itemUrl });
918
950
  process.stdin.setRawMode?.(true);
951
+ console.clear();
919
952
  let msg = result.resumed
920
953
  ? `Resumed work on #${item.id}`
921
954
  : `Started work on #${item.id}`;
@@ -926,6 +959,7 @@ export function WorkItemList() {
926
959
  }
927
960
  catch (e) {
928
961
  process.stdin.setRawMode?.(true);
962
+ console.clear();
929
963
  setWarning(e instanceof Error ? e.message : 'Failed to start implementation');
930
964
  }
931
965
  void backendDataStore
@@ -1068,24 +1102,16 @@ export function WorkItemList() {
1068
1102
  ? activeType.charAt(0).toUpperCase() + activeType.slice(1) + 's'
1069
1103
  : '';
1070
1104
  const visibleTreeItems = useMemo(() => treeItems.slice(viewport.start, viewport.end), [treeItems, viewport.start, viewport.end]);
1105
+ const workItemColumns = useMemo(() => {
1106
+ const maxIdLen = visibleTreeItems.reduce((max, { item }) => Math.max(max, item.id.length), 2);
1107
+ const cols = buildWorkItemColumns(capabilities, collapsedIds, accent);
1108
+ cols[0].width = maxIdLen + 2;
1109
+ return cols;
1110
+ }, [visibleTreeItems, capabilities, collapsedIds, accent]);
1071
1111
  const positionText = treeItems.length > viewport.maxVisible
1072
1112
  ? `${cursor + 1}/${treeItems.length}`
1073
1113
  : '';
1074
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { wrap: "truncate", children: [_jsxs(Text, { bold: true, color: accent, children: [typeLabel, " \u2014 ", iteration] }), _jsx(Text, { dimColor: mutedDim, children: ` (${filterCount > 0 ? `${items.length}/${unfilteredCount}` : items.length} item${unfilteredCount === 1 ? '' : 's'})` }), markedCount > 0 && (_jsxs(Text, { color: marked, children: [` ● ${markedCount}`, markedDistribution.above > 0 && ` ↑${markedDistribution.above}`, markedDistribution.below > 0 && ` ↓${markedDistribution.below}`] })), filterCount > 0 && (_jsx(Text, { color: warningColor, children: ` [${filterCount} filter${filterCount === 1 ? '' : 's'}${activeViewName ? `: ${activeViewName}` : ''}]` }))] }) }), _jsx(TableLayout, { treeItems: visibleTreeItems, cursor: viewport.visibleCursor, capabilities: capabilities, collapsedIds: collapsedIds, markedIds: markedIds, terminalWidth: terminalWidth, sortStack: sortStack }), treeItems.length === 0 && !loading && initError && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: errorColor, children: "Failed to connect to backend:" }), _jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: errorColor, children: initError }) }), _jsx(Text, { dimColor: mutedDim, children: "Press , for settings or q to quit." })] })), treeItems.length === 0 && !loading && !initError && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: mutedDim, children: ["No ", activeType ?? 'item', "s in this iteration. Press c to create, / to search all."] }) })), loading && treeItems.length === 0 && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: mutedDim, children: "Loading..." }) })), showDetailPanel && treeItems.length > 0 && treeItems[cursor] && (_jsx(Box, { marginTop: 1, children: _jsx(DetailPanel, { item: treeItems[cursor].item, terminalWidth: terminalWidth, showFullDescription: showFullDescription, descriptionScrollOffset: descriptionScrollOffset, maxDescriptionHeight: maxDescriptionHeight }) })), _jsx(Box, { marginTop: 1, children: showFullDescription ? (_jsxs(Box, { children: [_jsx(Text, { dimColor: mutedDim, children: "\u2191\u2193 scroll space/esc close" }), positionText && _jsxs(Text, { dimColor: mutedDim, children: [" ", positionText] })] })) : activeOverlay?.type === 'command-bar' ? (_jsx(OverlayPanel, { title: "Commands", items: commandBarItems, placeholder: "Type to search...", externalFilter: true, onQueryChange: setCommandBarQuery, onSelect: (item) => {
1075
- if (item.kind === 'issue') {
1076
- const workItem = allSearchItems.find((i) => i.id === item.value);
1077
- if (workItem)
1078
- handleSearchSelect(workItem);
1079
- }
1080
- else {
1081
- const cmd = paletteCommands.find((c) => c.id === item.value);
1082
- if (cmd)
1083
- handleCommandSelect(cmd);
1084
- }
1085
- }, onCancel: () => {
1086
- setCommandBarQuery('');
1087
- closeOverlay();
1088
- } })) : activeOverlay?.type === 'bulk-menu' ? ((() => {
1114
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { wrap: "truncate", children: [_jsxs(Text, { bold: true, color: accent, children: [typeLabel, " \u2014 ", iteration] }), _jsx(Text, { dimColor: mutedDim, children: ` (${filterCount > 0 ? `${items.length}/${unfilteredCount}` : items.length} item${unfilteredCount === 1 ? '' : 's'})` }), markedCount > 0 && (_jsxs(Text, { color: marked, children: [` ● ${markedCount}`, markedDistribution.above > 0 && ` ↑${markedDistribution.above}`, markedDistribution.below > 0 && ` ↓${markedDistribution.below}`] })), filterCount > 0 && (_jsx(Text, { color: warningColor, children: ` [${filterCount} filter${filterCount === 1 ? '' : 's'}${activeViewName ? `: ${activeViewName}` : ''}]` }))] }) }), _jsx(TableLayout, { items: visibleTreeItems, columns: workItemColumns, cursor: viewport.visibleCursor, terminalWidth: terminalWidth, getKey: (ti) => `${ti.item.id}-${ti.item.type}`, isMarked: (ti) => markedIds.has(ti.item.id), sortStack: sortStack }), treeItems.length === 0 && !loading && initError && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: errorColor, children: "Failed to connect to backend:" }), _jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: errorColor, children: initError }) }), _jsx(Text, { dimColor: mutedDim, children: "Press , for settings or q to quit." })] })), treeItems.length === 0 && !loading && !initError && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: mutedDim, children: ["No ", activeType ?? 'item', "s in this iteration. Press c to create, / to search all."] }) })), loading && treeItems.length === 0 && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: mutedDim, children: "Loading..." }) })), showDetailPanel && treeItems.length > 0 && treeItems[cursor] && (_jsx(Box, { marginTop: 1, children: _jsx(DetailPanel, { item: treeItems[cursor].item, terminalWidth: terminalWidth, showFullDescription: showFullDescription, descriptionScrollOffset: descriptionScrollOffset, maxDescriptionHeight: maxDescriptionHeight }) })), _jsx(Box, { marginTop: 1, children: showFullDescription ? (_jsxs(Box, { children: [_jsx(Text, { dimColor: mutedDim, children: "\u2191\u2193 scroll space/esc close" }), positionText && _jsxs(Text, { dimColor: mutedDim, children: [" ", positionText] })] })) : activeOverlay?.type === 'command-bar' ? (_jsx(CommandBar, { commands: paletteCommands, onCommand: handleCommandSelect, onCancel: closeOverlay })) : activeOverlay?.type === 'bulk-menu' ? ((() => {
1089
1115
  const bulkItems = [];
1090
1116
  bulkItems.push({
1091
1117
  id: 'status',