@sascha384/tic 1.34.0 → 2.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 (107) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/dist/backends/availability.js +4 -2
  3. package/dist/backends/availability.js.map +1 -1
  4. package/dist/backends/factory.d.ts +5 -3
  5. package/dist/backends/factory.js +28 -43
  6. package/dist/backends/factory.js.map +1 -1
  7. package/dist/backends/files/hash.d.ts +1 -0
  8. package/dist/backends/files/hash.js +5 -0
  9. package/dist/backends/files/hash.js.map +1 -0
  10. package/dist/backends/files/index.d.ts +48 -0
  11. package/dist/backends/files/index.js +174 -0
  12. package/dist/backends/files/index.js.map +1 -0
  13. package/dist/backends/files/sync.d.ts +13 -0
  14. package/dist/backends/files/sync.js +69 -0
  15. package/dist/backends/files/sync.js.map +1 -0
  16. package/dist/backends/jira/config.d.ts +1 -1
  17. package/dist/backends/jira/config.js +6 -9
  18. package/dist/backends/jira/config.js.map +1 -1
  19. package/dist/backends/types.d.ts +12 -0
  20. package/dist/backends/types.js +5 -1
  21. package/dist/backends/types.js.map +1 -1
  22. package/dist/cli/commands/config.js +27 -14
  23. package/dist/cli/commands/config.js.map +1 -1
  24. package/dist/cli/commands/init.js +10 -3
  25. package/dist/cli/commands/init.js.map +1 -1
  26. package/dist/cli/commands/mcp.d.ts +4 -4
  27. package/dist/cli/commands/mcp.js +16 -25
  28. package/dist/cli/commands/mcp.js.map +1 -1
  29. package/dist/cli/index.js +16 -19
  30. package/dist/cli/index.js.map +1 -1
  31. package/dist/commands.d.ts +2 -0
  32. package/dist/commands.js +33 -0
  33. package/dist/commands.js.map +1 -1
  34. package/dist/components/Header.js +9 -2
  35. package/dist/components/Header.js.map +1 -1
  36. package/dist/components/HelpScreen.js +3 -0
  37. package/dist/components/HelpScreen.js.map +1 -1
  38. package/dist/components/OverlayPanel.d.ts +2 -1
  39. package/dist/components/OverlayPanel.js +14 -1
  40. package/dist/components/OverlayPanel.js.map +1 -1
  41. package/dist/components/Settings.js +6 -11
  42. package/dist/components/Settings.js.map +1 -1
  43. package/dist/components/StatusScreen.js +29 -4
  44. package/dist/components/StatusScreen.js.map +1 -1
  45. package/dist/components/WorkItemForm.js +6 -9
  46. package/dist/components/WorkItemForm.js.map +1 -1
  47. package/dist/components/WorkItemList.js +353 -36
  48. package/dist/components/WorkItemList.js.map +1 -1
  49. package/dist/filters.d.ts +28 -0
  50. package/dist/filters.js +47 -0
  51. package/dist/filters.js.map +1 -0
  52. package/dist/implement.js +4 -56
  53. package/dist/implement.js.map +1 -1
  54. package/dist/index.js +20 -8
  55. package/dist/index.js.map +1 -1
  56. package/dist/storage/config.d.ts +61 -0
  57. package/dist/storage/config.js +309 -0
  58. package/dist/storage/config.js.map +1 -0
  59. package/dist/storage/db.d.ts +11 -0
  60. package/dist/storage/db.js +34 -0
  61. package/dist/storage/db.js.map +1 -0
  62. package/dist/storage/index.d.ts +73 -0
  63. package/dist/storage/index.js +966 -0
  64. package/dist/storage/index.js.map +1 -0
  65. package/dist/storage/mappers.d.ts +35 -0
  66. package/dist/storage/mappers.js +70 -0
  67. package/dist/storage/mappers.js.map +1 -0
  68. package/dist/storage/schema.d.ts +1844 -0
  69. package/dist/storage/schema.js +197 -0
  70. package/dist/storage/schema.js.map +1 -0
  71. package/dist/storage/syncQueue.d.ts +13 -0
  72. package/dist/storage/syncQueue.js +98 -0
  73. package/dist/storage/syncQueue.js.map +1 -0
  74. package/dist/storage/undo.d.ts +22 -0
  75. package/dist/storage/undo.js +129 -0
  76. package/dist/storage/undo.js.map +1 -0
  77. package/dist/stores/backendDataStore.d.ts +4 -1
  78. package/dist/stores/backendDataStore.js +61 -40
  79. package/dist/stores/backendDataStore.js.map +1 -1
  80. package/dist/stores/configStore.d.ts +3 -1
  81. package/dist/stores/configStore.js +25 -65
  82. package/dist/stores/configStore.js.map +1 -1
  83. package/dist/stores/filterStore.d.ts +13 -0
  84. package/dist/stores/filterStore.js +38 -0
  85. package/dist/stores/filterStore.js.map +1 -0
  86. package/dist/stores/listViewStore.d.ts +1 -0
  87. package/dist/stores/listViewStore.js +1 -0
  88. package/dist/stores/listViewStore.js.map +1 -1
  89. package/dist/stores/uiStore.d.ts +8 -0
  90. package/dist/stores/uiStore.js.map +1 -1
  91. package/dist/stores/undoStore.d.ts +4 -0
  92. package/dist/stores/undoStore.js +32 -0
  93. package/dist/stores/undoStore.js.map +1 -1
  94. package/dist/sync/SyncManager.d.ts +5 -4
  95. package/dist/sync/SyncManager.js +129 -36
  96. package/dist/sync/SyncManager.js.map +1 -1
  97. package/dist/sync/types.d.ts +25 -1
  98. package/package.json +5 -1
  99. package/dist/backends/local/config.d.ts +0 -23
  100. package/dist/backends/local/config.js +0 -42
  101. package/dist/backends/local/config.js.map +0 -1
  102. package/dist/backends/local/index.d.ts +0 -45
  103. package/dist/backends/local/index.js +0 -291
  104. package/dist/backends/local/index.js.map +0 -1
  105. package/dist/sync/queue.d.ts +0 -12
  106. package/dist/sync/queue.js +0 -56
  107. package/dist/sync/queue.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
- import { useState, useMemo, useEffect, useCallback } from 'react';
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';
5
5
  import { listViewStore, useListViewStore } from '../stores/listViewStore.js';
@@ -12,7 +12,6 @@ import { useTerminalSize } from '../hooks/useTerminalSize.js';
12
12
  import { useScrollViewport } from '../hooks/useScrollViewport.js';
13
13
  import { useBackendDataStore, backendDataStore, } from '../stores/backendDataStore.js';
14
14
  import { useShallow } from 'zustand/shallow';
15
- import { SyncQueueStore } from '../sync/queue.js';
16
15
  import { buildTree, sortTree } from './buildTree.js';
17
16
  import { getVisibleCommands, } from '../commands.js';
18
17
  import { OverlayPanel } from './OverlayPanel.js';
@@ -20,6 +19,9 @@ import { DetailPanel } from './DetailPanel.js';
20
19
  import { undoStore } from '../stores/undoStore.js';
21
20
  import { recentCommandsStore, useRecentCommandsStore, } from '../stores/recentCommandsStore.js';
22
21
  import { isSoftDeleteBackend } from '../backends/types.js';
22
+ import { filterStore, useFilterStore } from '../stores/filterStore.js';
23
+ import { applyFilters, countActiveFilters, summarizeFilters, } from '../filters.js';
24
+ const EMPTY_VIEWS = [];
23
25
  export function getTargetIds(markedIds, cursorItem) {
24
26
  if (markedIds.size > 0) {
25
27
  return [...markedIds];
@@ -77,6 +79,8 @@ export function WorkItemList() {
77
79
  const branchCommand = useConfigStore((s) => s.config.branchCommand);
78
80
  const copyToClipboard = useConfigStore((s) => s.config.copyToClipboard);
79
81
  const showDetailPanel = useConfigStore((s) => s.config.showDetailPanel ?? true);
82
+ const savedViews = useConfigStore((s) => s.config.views ?? EMPTY_VIEWS);
83
+ const defaultView = useConfigStore((s) => s.config.defaultView);
80
84
  const { exit } = useApp();
81
85
  // Store selectors for persistent list view state
82
86
  const { cursor, markedIds, expandedIds, rangeAnchor, sortStack } = useListViewStore(useShallow((s) => ({
@@ -86,6 +90,12 @@ export function WorkItemList() {
86
90
  rangeAnchor: s.rangeAnchor,
87
91
  sortStack: s.sortStack,
88
92
  })));
93
+ const { activeFilters, activeViewName, lastViewName } = useFilterStore(useShallow((s) => ({
94
+ activeFilters: s.activeFilters,
95
+ activeViewName: s.activeViewName,
96
+ lastViewName: s.lastViewName,
97
+ })));
98
+ const filterCount = useMemo(() => countActiveFilters(activeFilters), [activeFilters]);
89
99
  const { setCursor, toggleExpanded, toggleMarked, clearMarked, setMarkedIds, setRangeAnchor, clampCursor, removeDeletedItem, toggleSortColumn, clearSort, } = listViewStore.getState();
90
100
  // Local state for inputs and templates
91
101
  const [allSearchItems, setAllSearchItems] = useState([]);
@@ -121,14 +131,26 @@ export function WorkItemList() {
121
131
  void backend.listTemplates().then(setTemplates);
122
132
  }
123
133
  }, [backend, capabilities.templates]);
124
- const queueStore = useMemo(() => {
125
- if (!syncManager)
126
- return null;
127
- return new SyncQueueStore(process.cwd());
128
- }, [syncManager]);
134
+ // Load default view on startup
135
+ const defaultViewLoadedRef = useRef(false);
136
+ useEffect(() => {
137
+ if (defaultViewLoadedRef.current)
138
+ return;
139
+ if (!defaultView)
140
+ return;
141
+ const view = savedViews.find((v) => v.name === defaultView);
142
+ if (view) {
143
+ defaultViewLoadedRef.current = true;
144
+ filterStore.getState().loadView(view);
145
+ if (view.sort) {
146
+ listViewStore.getState().setSortStack(view.sort);
147
+ }
148
+ }
149
+ }, [savedViews, defaultView]);
150
+ const queue = useBackendDataStore((s) => s.queue);
129
151
  const queueWrite = async (action, itemId) => {
130
- if (queueStore) {
131
- await queueStore.append({
152
+ if (queue) {
153
+ await queue.append({
132
154
  action,
133
155
  itemId,
134
156
  timestamp: new Date().toISOString(),
@@ -151,17 +173,34 @@ export function WorkItemList() {
151
173
  const { width: terminalWidth, height: terminalHeight } = useTerminalSize();
152
174
  const gitAvailable = useMemo(() => isGitRepo(process.cwd()), []);
153
175
  useEffect(() => {
154
- if (activeType === null && types.length > 0) {
176
+ if (types.length > 0 &&
177
+ (activeType === null || !types.includes(activeType))) {
155
178
  setActiveType(defaultType && types.includes(defaultType) ? defaultType : types[0]);
156
179
  }
157
180
  }, [activeType, types, setActiveType, defaultType]);
158
- const items = useMemo(() => allItems.filter((item) => item.type === activeType), [allItems, activeType]);
181
+ // Apply view filters to all items (used for children in tree view)
182
+ const viewFilteredItems = useMemo(() => applyFilters(allItems, activeFilters), [allItems, activeFilters]);
183
+ const unfilteredCount = useMemo(() => allItems.filter((item) => item.type === activeType).length, [allItems, activeType]);
184
+ const items = useMemo(() => {
185
+ const hasTypeFilter = (activeFilters.types?.length ?? 0) > 0;
186
+ let filtered = hasTypeFilter
187
+ ? allItems
188
+ : allItems.filter((item) => item.type === activeType);
189
+ filtered = applyFilters(filtered, activeFilters);
190
+ return filtered;
191
+ }, [allItems, activeType, activeFilters]);
159
192
  const fullTree = useMemo(() => {
160
193
  const tree = capabilities.relationships
161
- ? buildTree(items, allItems, activeType ?? '')
194
+ ? buildTree(items, viewFilteredItems, activeType ?? '')
162
195
  : buildTree(items, items, activeType ?? '');
163
196
  return sortTree(tree, sortStack);
164
- }, [items, allItems, activeType, capabilities.relationships, sortStack]);
197
+ }, [
198
+ items,
199
+ viewFilteredItems,
200
+ activeType,
201
+ capabilities.relationships,
202
+ sortStack,
203
+ ]);
165
204
  const parentSuggestions = useMemo(() => allItems.map((item) => `${item.id} - ${item.title}`), [allItems]);
166
205
  // Collapse state: set of item IDs that are collapsed (collapsed by default)
167
206
  // Track explicitly expanded items (inverse of collapsed).
@@ -387,8 +426,8 @@ export function WorkItemList() {
387
426
  await backend.restoreWorkItem(snap.id);
388
427
  }
389
428
  }
390
- if (queueStore) {
391
- await queueStore.removeByIds(entry.syncItemIds, 'delete');
429
+ if (queue) {
430
+ await queue.removeByIds(entry.syncItemIds, 'delete');
392
431
  }
393
432
  refreshData();
394
433
  setToast(entry.itemSnapshots.length === 1
@@ -399,8 +438,8 @@ export function WorkItemList() {
399
438
  for (const id of entry.createdIds ?? []) {
400
439
  await backend.cachedDeleteWorkItem(id);
401
440
  }
402
- if (queueStore) {
403
- await queueStore.removeByIds(entry.syncItemIds, 'create');
441
+ if (queue) {
442
+ await queue.removeByIds(entry.syncItemIds, 'create');
404
443
  }
405
444
  refreshData();
406
445
  setToast((entry.createdIds?.length ?? 0) === 1
@@ -411,8 +450,8 @@ export function WorkItemList() {
411
450
  for (const snap of entry.itemSnapshots) {
412
451
  await backend.cachedUpdateWorkItem(snap.id, snap);
413
452
  }
414
- if (queueStore) {
415
- await queueStore.removeByIds(entry.syncItemIds, 'update');
453
+ if (queue) {
454
+ await queue.removeByIds(entry.syncItemIds, 'update');
416
455
  }
417
456
  for (const snap of entry.itemSnapshots) {
418
457
  await queueWrite('update', snap.id);
@@ -424,8 +463,9 @@ export function WorkItemList() {
424
463
  }
425
464
  if (input === 'o' && treeItems.length > 0 && backend) {
426
465
  void (async () => {
427
- await backend.openItem(treeItems[cursor].item.id);
428
- refreshData();
466
+ const itemId = treeItems[cursor].item.id;
467
+ await backend.openItem(itemId);
468
+ void backendDataStore.getState().reloadItem(itemId);
429
469
  })();
430
470
  }
431
471
  if (input === 'b' && gitAvailable && treeItems.length > 0) {
@@ -434,6 +474,8 @@ export function WorkItemList() {
434
474
  try {
435
475
  const itemUrl = backend?.getItemUrl(item.id) || '';
436
476
  const result = beginImplementation(item, comments, { branchMode, branchCommand, copyToClipboard }, process.cwd(), { itemUrl });
477
+ // Restore raw mode after interactive shell changed terminal settings
478
+ process.stdin.setRawMode?.(true);
437
479
  let msg = result.resumed
438
480
  ? `Resumed work on #${item.id}`
439
481
  : `Started work on #${item.id}`;
@@ -443,9 +485,10 @@ export function WorkItemList() {
443
485
  setWarning(msg);
444
486
  }
445
487
  catch (e) {
488
+ process.stdin.setRawMode?.(true);
446
489
  setWarning(e instanceof Error ? e.message : 'Failed to start implementation');
447
490
  }
448
- refreshData();
491
+ void backendDataStore.getState().reloadItem(item.id);
449
492
  }
450
493
  if (input === 'S') {
451
494
  navigate('status');
@@ -453,6 +496,16 @@ export function WorkItemList() {
453
496
  if (input === 'O') {
454
497
  openOverlay({ type: 'sort-picker' });
455
498
  }
499
+ if (input === 'F') {
500
+ openOverlay({ type: 'filter-picker' });
501
+ }
502
+ if (input === 'V') {
503
+ openOverlay({ type: 'view-picker' });
504
+ }
505
+ if (input === 'X' && filterCount > 0) {
506
+ filterStore.getState().clearFilters();
507
+ setToast('Filters cleared');
508
+ }
456
509
  if (input === 's' && treeItems.length > 0) {
457
510
  const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item);
458
511
  if (targetIds.length > 0) {
@@ -551,6 +604,8 @@ export function WorkItemList() {
551
604
  activeType,
552
605
  hasSyncManager: syncManager !== null,
553
606
  gitAvailable,
607
+ hasActiveFilters: filterCount > 0,
608
+ hasSavedViews: savedViews.length > 0,
554
609
  };
555
610
  const paletteCommands = useMemo(() => getVisibleCommands(commandContext), [
556
611
  commandContext.markedCount,
@@ -618,6 +673,110 @@ export function WorkItemList() {
618
673
  }
619
674
  return items;
620
675
  }, [sortStack, capabilities.fields.priority, capabilities.fields.assignee]);
676
+ const filterPickerItems = useMemo(() => {
677
+ const items = [];
678
+ if (filterCount > 0) {
679
+ items.push({
680
+ id: '__clear__',
681
+ label: 'Clear all filters',
682
+ value: '__clear__',
683
+ });
684
+ }
685
+ for (const s of statuses) {
686
+ items.push({
687
+ id: `status-${s}`,
688
+ label: s,
689
+ value: s,
690
+ category: 'Status',
691
+ selected: activeFilters.statuses?.includes(s),
692
+ });
693
+ }
694
+ for (const p of ['critical', 'high', 'medium', 'low']) {
695
+ if (!capabilities.fields.priority)
696
+ continue;
697
+ items.push({
698
+ id: `priority-${p}`,
699
+ label: p.charAt(0).toUpperCase() + p.slice(1),
700
+ value: p,
701
+ category: 'Priority',
702
+ selected: activeFilters.priorities?.includes(p),
703
+ });
704
+ }
705
+ for (const t of types) {
706
+ items.push({
707
+ id: `type-${t}`,
708
+ label: t.charAt(0).toUpperCase() + t.slice(1),
709
+ value: t,
710
+ category: 'Type',
711
+ selected: activeFilters.types?.includes(t),
712
+ });
713
+ }
714
+ for (const a of assignees) {
715
+ if (!capabilities.fields.assignee)
716
+ continue;
717
+ items.push({
718
+ id: `assignee-${a}`,
719
+ label: a,
720
+ value: a,
721
+ category: 'Assignee',
722
+ selected: activeFilters.assignees?.includes(a),
723
+ });
724
+ }
725
+ for (const l of labelSuggestions) {
726
+ if (!capabilities.fields.labels)
727
+ continue;
728
+ items.push({
729
+ id: `label-${l}`,
730
+ label: l,
731
+ value: l,
732
+ category: 'Labels',
733
+ selected: activeFilters.labels?.includes(l),
734
+ });
735
+ }
736
+ return items;
737
+ }, [
738
+ statuses,
739
+ types,
740
+ assignees,
741
+ labelSuggestions,
742
+ capabilities,
743
+ activeFilters,
744
+ filterCount,
745
+ ]);
746
+ const viewPickerItems = useMemo(() => {
747
+ const noFilterLabel = !defaultView ? 'No filters (default)' : 'No filters';
748
+ const items = [
749
+ {
750
+ id: '__no-filters__',
751
+ label: noFilterLabel,
752
+ value: '__no-filters__',
753
+ hint: !activeViewName && filterCount === 0 ? '●' : '',
754
+ },
755
+ ...savedViews.map((v) => ({
756
+ id: v.name,
757
+ label: v.name + (v.name === defaultView ? ' (default)' : ''),
758
+ value: v.name,
759
+ hint: summarizeFilters(v.filters) + (v.name === activeViewName ? ' ●' : ''),
760
+ })),
761
+ ];
762
+ if (filterCount > 0) {
763
+ if (lastViewName) {
764
+ items.push({
765
+ id: '__save__',
766
+ label: `Save to "${lastViewName}"`,
767
+ value: '__save__',
768
+ category: 'Actions',
769
+ });
770
+ }
771
+ items.push({
772
+ id: '__new__',
773
+ label: 'New view...',
774
+ value: '__new__',
775
+ category: 'Actions',
776
+ });
777
+ }
778
+ return items;
779
+ }, [savedViews, defaultView, activeViewName, lastViewName, filterCount]);
621
780
  const handleCommandSelect = (command) => {
622
781
  closeOverlay();
623
782
  recentCommandsStore.getState().addRecent(command.id);
@@ -643,8 +802,9 @@ export function WorkItemList() {
643
802
  case 'open':
644
803
  if (treeItems[cursor] && backend) {
645
804
  void (async () => {
646
- await backend.openItem(treeItems[cursor].item.id);
647
- refreshData();
805
+ const itemId = treeItems[cursor].item.id;
806
+ await backend.openItem(itemId);
807
+ void backendDataStore.getState().reloadItem(itemId);
648
808
  })();
649
809
  }
650
810
  break;
@@ -655,6 +815,7 @@ export function WorkItemList() {
655
815
  try {
656
816
  const itemUrl = backend?.getItemUrl(item.id) || '';
657
817
  const result = beginImplementation(item, comments, { branchMode, branchCommand, copyToClipboard }, process.cwd(), { itemUrl });
818
+ process.stdin.setRawMode?.(true);
658
819
  let msg = result.resumed
659
820
  ? `Resumed work on #${item.id}`
660
821
  : `Started work on #${item.id}`;
@@ -664,9 +825,10 @@ export function WorkItemList() {
664
825
  setWarning(msg);
665
826
  }
666
827
  catch (e) {
828
+ process.stdin.setRawMode?.(true);
667
829
  setWarning(e instanceof Error ? e.message : 'Failed to start implementation');
668
830
  }
669
- refreshData();
831
+ void backendDataStore.getState().reloadItem(item.id);
670
832
  }
671
833
  break;
672
834
  case 'sync':
@@ -733,6 +895,22 @@ export function WorkItemList() {
733
895
  case 'sort':
734
896
  openOverlay({ type: 'sort-picker' });
735
897
  break;
898
+ case 'filter':
899
+ openOverlay({ type: 'filter-picker' });
900
+ break;
901
+ case 'clear-filters':
902
+ filterStore.getState().clearFilters();
903
+ setToast('Filters cleared');
904
+ break;
905
+ case 'load-view':
906
+ openOverlay({ type: 'view-picker' });
907
+ break;
908
+ case 'save-view':
909
+ openOverlay({ type: 'save-view-input' });
910
+ break;
911
+ case 'delete-view':
912
+ openOverlay({ type: 'delete-view-picker' });
913
+ break;
736
914
  case 'quit':
737
915
  exit();
738
916
  break;
@@ -785,7 +963,7 @@ export function WorkItemList() {
785
963
  const positionText = treeItems.length > viewport.maxVisible
786
964
  ? `${cursor + 1}/${treeItems.length}`
787
965
  : '';
788
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { wrap: "truncate", children: [_jsxs(Text, { bold: true, color: "cyan", children: [typeLabel, " \u2014 ", iteration] }), _jsx(Text, { dimColor: true, children: ` (${items.length} items)` }), markedCount > 0 && (_jsx(Text, { color: "magenta", children: ` ● ${markedCount} marked` }))] }) }), _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: "red", children: "Failed to connect to backend:" }), _jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: "red", children: initError }) }), _jsx(Text, { dimColor: true, children: "Press , for settings or q to quit." })] })), treeItems.length === 0 && !loading && !initError && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, 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: true, children: "Loading..." }) })), showDetailPanel && treeItems.length > 0 && treeItems[cursor] && (_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: true, children: "\u2191\u2193 scroll space/esc close" }), positionText && _jsxs(Text, { dimColor: true, children: [" ", positionText] })] })) : activeOverlay?.type === 'search' ? ((() => {
966
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { wrap: "truncate", children: [_jsxs(Text, { bold: true, color: "cyan", children: [typeLabel, " \u2014 ", iteration] }), _jsx(Text, { dimColor: true, children: ` (${filterCount > 0 ? `${items.length}/${unfilteredCount}` : items.length} item${unfilteredCount === 1 ? '' : 's'})` }), markedCount > 0 && (_jsx(Text, { color: "magenta", children: ` ● ${markedCount} marked` })), filterCount > 0 && (_jsx(Text, { color: "yellow", 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: "red", children: "Failed to connect to backend:" }), _jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: "red", children: initError }) }), _jsx(Text, { dimColor: true, children: "Press , for settings or q to quit." })] })), treeItems.length === 0 && !loading && !initError && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, 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: true, 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: true, children: "\u2191\u2193 scroll space/esc close" }), positionText && _jsxs(Text, { dimColor: true, children: [" ", positionText] })] })) : activeOverlay?.type === 'search' ? ((() => {
789
967
  const searchItems = allSearchItems.map((item) => ({
790
968
  id: item.id,
791
969
  label: `#${item.id} ${item.title}`,
@@ -884,7 +1062,9 @@ export function WorkItemList() {
884
1062
  });
885
1063
  await queueWrite('update', id);
886
1064
  }
887
- refreshData();
1065
+ for (const id of targetIds) {
1066
+ await backendDataStore.getState().reloadItem(id);
1067
+ }
888
1068
  setToast(targetIds.length === 1
889
1069
  ? 'Status updated — press u to undo'
890
1070
  : `${targetIds.length} items updated — press u to undo`);
@@ -906,7 +1086,9 @@ export function WorkItemList() {
906
1086
  });
907
1087
  await queueWrite('update', id);
908
1088
  }
909
- refreshData();
1089
+ for (const id of targetIds) {
1090
+ await backendDataStore.getState().reloadItem(id);
1091
+ }
910
1092
  setToast(targetIds.length === 1
911
1093
  ? 'Type updated — press u to undo'
912
1094
  : `${targetIds.length} items updated — press u to undo`);
@@ -928,7 +1110,9 @@ export function WorkItemList() {
928
1110
  await backend.cachedUpdateWorkItem(id, { priority });
929
1111
  await queueWrite('update', id);
930
1112
  }
931
- refreshData();
1113
+ for (const id of targetIds) {
1114
+ await backendDataStore.getState().reloadItem(id);
1115
+ }
932
1116
  setToast(targetIds.length === 1
933
1117
  ? 'Priority updated — press u to undo'
934
1118
  : `${targetIds.length} items updated — press u to undo`);
@@ -979,7 +1163,9 @@ export function WorkItemList() {
979
1163
  setWarning(e instanceof Error ? e.message : 'Invalid parent');
980
1164
  }
981
1165
  closeOverlay();
982
- refreshData();
1166
+ for (const id of targetIds) {
1167
+ await backendDataStore.getState().reloadItem(id);
1168
+ }
983
1169
  setToast(targetIds.length === 1
984
1170
  ? 'Parent updated — press u to undo'
985
1171
  : `${targetIds.length} items updated — press u to undo`);
@@ -1009,7 +1195,9 @@ export function WorkItemList() {
1009
1195
  setWarning(e instanceof Error ? e.message : 'Invalid parent');
1010
1196
  }
1011
1197
  closeOverlay();
1012
- refreshData();
1198
+ for (const id of targetIds) {
1199
+ await backendDataStore.getState().reloadItem(id);
1200
+ }
1013
1201
  setToast(targetIds.length === 1
1014
1202
  ? 'Parent updated — press u to undo'
1015
1203
  : `${targetIds.length} items updated — press u to undo`);
@@ -1027,7 +1215,9 @@ export function WorkItemList() {
1027
1215
  });
1028
1216
  await queueWrite('update', id);
1029
1217
  }
1030
- refreshData();
1218
+ for (const id of targetIds) {
1219
+ await backendDataStore.getState().reloadItem(id);
1220
+ }
1031
1221
  setToast(targetIds.length === 1
1032
1222
  ? 'Assignee updated — press u to undo'
1033
1223
  : `${targetIds.length} items updated — press u to undo`);
@@ -1045,7 +1235,9 @@ export function WorkItemList() {
1045
1235
  });
1046
1236
  await queueWrite('update', id);
1047
1237
  }
1048
- refreshData();
1238
+ for (const id of targetIds) {
1239
+ await backendDataStore.getState().reloadItem(id);
1240
+ }
1049
1241
  setToast(targetIds.length === 1
1050
1242
  ? 'Assignee updated — press u to undo'
1051
1243
  : `${targetIds.length} items updated — press u to undo`);
@@ -1066,7 +1258,9 @@ export function WorkItemList() {
1066
1258
  await backend.cachedUpdateWorkItem(id, { labels });
1067
1259
  await queueWrite('update', id);
1068
1260
  }
1069
- refreshData();
1261
+ for (const id of targetIds) {
1262
+ await backendDataStore.getState().reloadItem(id);
1263
+ }
1070
1264
  setToast(targetIds.length === 1
1071
1265
  ? 'Labels updated — press u to undo'
1072
1266
  : `${targetIds.length} items updated — press u to undo`);
@@ -1086,7 +1280,9 @@ export function WorkItemList() {
1086
1280
  await backend.cachedUpdateWorkItem(id, { labels });
1087
1281
  await queueWrite('update', id);
1088
1282
  }
1089
- refreshData();
1283
+ for (const id of targetIds) {
1284
+ await backendDataStore.getState().reloadItem(id);
1285
+ }
1090
1286
  setToast(targetIds.length === 1
1091
1287
  ? 'Labels updated — press u to undo'
1092
1288
  : `${targetIds.length} items updated — press u to undo`);
@@ -1099,6 +1295,125 @@ export function WorkItemList() {
1099
1295
  else {
1100
1296
  toggleSortColumn(item.value);
1101
1297
  }
1298
+ }, onCancel: () => closeOverlay() })) : activeOverlay?.type === 'filter-picker' ? ((() => {
1299
+ const handleFilterConfirm = (selected) => {
1300
+ const newFilters = {};
1301
+ for (const item of selected) {
1302
+ const cat = item.category;
1303
+ if (cat === 'Status') {
1304
+ (newFilters.statuses ??= []).push(item.value);
1305
+ }
1306
+ else if (cat === 'Priority') {
1307
+ (newFilters.priorities ??= []).push(item.value);
1308
+ }
1309
+ else if (cat === 'Type') {
1310
+ (newFilters.types ??= []).push(item.value);
1311
+ }
1312
+ else if (cat === 'Assignee') {
1313
+ (newFilters.assignees ??= []).push(item.value);
1314
+ }
1315
+ else if (cat === 'Labels') {
1316
+ (newFilters.labels ??= []).push(item.value);
1317
+ }
1318
+ }
1319
+ filterStore.getState().setFilters(newFilters);
1320
+ closeOverlay();
1321
+ const count = countActiveFilters(newFilters);
1322
+ if (count > 0) {
1323
+ setToast(`${count} filter${count === 1 ? '' : 's'} applied`);
1324
+ }
1325
+ else {
1326
+ setToast('Filters cleared');
1327
+ }
1328
+ };
1329
+ const handleFilterSelect = (item) => {
1330
+ if (item.value === '__clear__') {
1331
+ filterStore.getState().clearFilters();
1332
+ closeOverlay();
1333
+ setToast('Filters cleared');
1334
+ }
1335
+ };
1336
+ return (_jsx(OverlayPanel, { title: filterCount > 0 ? `Filter [${filterCount} active]` : 'Filter', items: filterPickerItems, multiSelect: true, onSelect: handleFilterSelect, onConfirm: handleFilterConfirm, onCancel: () => closeOverlay(), placeholder: "Type to filter...", footer: "space toggle enter confirm esc cancel" }));
1337
+ })()) : activeOverlay?.type === 'view-picker' ? (_jsx(OverlayPanel, { title: "Load View", items: viewPickerItems, onSelect: (item) => {
1338
+ if (item.value === '__no-filters__') {
1339
+ filterStore.getState().clearFilters();
1340
+ listViewStore.getState().setSortStack([]);
1341
+ closeOverlay();
1342
+ setToast('Filters cleared');
1343
+ return;
1344
+ }
1345
+ if (item.value === '__save__' && lastViewName) {
1346
+ const newView = {
1347
+ name: lastViewName,
1348
+ filters: { ...activeFilters },
1349
+ ...(sortStack.length > 0 ? { sort: [...sortStack] } : {}),
1350
+ };
1351
+ const existing = savedViews.filter((v) => v.name !== lastViewName);
1352
+ void configStore.getState().update({
1353
+ views: [...existing, newView],
1354
+ });
1355
+ filterStore.setState({
1356
+ activeViewName: lastViewName,
1357
+ });
1358
+ closeOverlay();
1359
+ setToast(`View "${lastViewName}" saved`);
1360
+ return;
1361
+ }
1362
+ if (item.value === '__new__') {
1363
+ openOverlay({ type: 'save-view-input' });
1364
+ return;
1365
+ }
1366
+ const view = savedViews.find((v) => v.name === item.value);
1367
+ if (view) {
1368
+ filterStore.getState().loadView(view);
1369
+ if (view.sort) {
1370
+ listViewStore
1371
+ .getState()
1372
+ .setSortStack(view.sort);
1373
+ }
1374
+ closeOverlay();
1375
+ setToast(`View "${view.name}" loaded`);
1376
+ }
1377
+ }, onAction: (item) => {
1378
+ if (item.value === '__no-filters__' ||
1379
+ item.value === defaultView) {
1380
+ void configStore.getState().update({ defaultView: undefined });
1381
+ setToast('Default view cleared');
1382
+ }
1383
+ else {
1384
+ void configStore.getState().update({ defaultView: item.value });
1385
+ setToast(`View "${item.value}" set as default`);
1386
+ }
1387
+ }, onCancel: () => closeOverlay(), footer: "\u2191\u2193 navigate enter load tab set default esc cancel" })) : activeOverlay?.type === 'save-view-input' ? (_jsx(OverlayPanel, { title: "Save View", items: [], allowFreeform: true, onSelect: () => { }, onSubmitFreeform: (name) => {
1388
+ if (!name.trim()) {
1389
+ closeOverlay();
1390
+ return;
1391
+ }
1392
+ const newView = {
1393
+ name: name.trim(),
1394
+ filters: { ...activeFilters },
1395
+ ...(sortStack.length > 0 ? { sort: [...sortStack] } : {}),
1396
+ };
1397
+ const existing = savedViews.filter((v) => v.name !== name.trim());
1398
+ void configStore.getState().update({
1399
+ views: [...existing, newView],
1400
+ });
1401
+ filterStore.setState({ activeViewName: name.trim() });
1402
+ closeOverlay();
1403
+ setToast(`View "${name.trim()}" saved`);
1404
+ }, onCancel: () => closeOverlay(), placeholder: "Enter view name...", emptyMessage: "Type a name and press enter" })) : activeOverlay?.type === 'delete-view-picker' ? (_jsx(OverlayPanel, { title: "Delete View", items: viewPickerItems.filter((i) => i.id !== '__no-filters__'), onSelect: (item) => {
1405
+ const remaining = savedViews.filter((v) => v.name !== item.value);
1406
+ void configStore.getState().update({
1407
+ views: remaining,
1408
+ ...(defaultView === item.value
1409
+ ? { defaultView: undefined }
1410
+ : {}),
1411
+ });
1412
+ if (activeViewName === item.value) {
1413
+ filterStore.setState({ activeViewName: null });
1414
+ }
1415
+ closeOverlay();
1416
+ setToast(`View "${item.value}" deleted`);
1102
1417
  }, onCancel: () => closeOverlay() })) : activeOverlay?.type === 'delete-confirm' ? (_jsx(OverlayPanel, { title: `Delete ${activeOverlay.targetIds.length} item${activeOverlay.targetIds.length > 1 ? 's' : ''}?`, items: [
1103
1418
  { id: 'yes', label: 'Yes, delete', value: 'yes' },
1104
1419
  { id: 'no', label: 'Cancel', value: 'no' },
@@ -1142,7 +1457,9 @@ export function WorkItemList() {
1142
1457
  removeDeletedItem(id);
1143
1458
  }
1144
1459
  setCursor(Math.max(0, cursor - 1));
1145
- refreshData();
1460
+ for (const id of targetIds) {
1461
+ backendDataStore.getState().removeItem(id);
1462
+ }
1146
1463
  setToast(targetIds.length === 1
1147
1464
  ? `Item #${targetIds[0]} deleted${softDelete ? ' — press u to undo' : ''}`
1148
1465
  : `${targetIds.length} items deleted${softDelete ? ' — press u to undo' : ''}`);