@sascha384/tic 5.16.0 → 5.17.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 (106) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/dist/backends/ado/index.js +1 -1
  3. package/dist/backends/ado/index.js.map +1 -1
  4. package/dist/backends/ado/mappers.d.ts +2 -2
  5. package/dist/backends/ado/mappers.js +7 -2
  6. package/dist/backends/ado/mappers.js.map +1 -1
  7. package/dist/backends/files/index.d.ts +6 -0
  8. package/dist/backends/files/index.js +36 -0
  9. package/dist/backends/files/index.js.map +1 -1
  10. package/dist/backends/files/sync.d.ts +2 -2
  11. package/dist/backends/files/sync.js +9 -7
  12. package/dist/backends/files/sync.js.map +1 -1
  13. package/dist/backends/github/index.js +3 -3
  14. package/dist/backends/github/index.js.map +1 -1
  15. package/dist/backends/github/mappers.js +2 -1
  16. package/dist/backends/github/mappers.js.map +1 -1
  17. package/dist/backends/github/pr-mappers.d.ts +1 -1
  18. package/dist/backends/github/pr-mappers.js +1 -1
  19. package/dist/backends/github/pr-mappers.js.map +1 -1
  20. package/dist/backends/gitlab/index.js +13 -6
  21. package/dist/backends/gitlab/index.js.map +1 -1
  22. package/dist/backends/gitlab/mappers.js +2 -1
  23. package/dist/backends/gitlab/mappers.js.map +1 -1
  24. package/dist/backends/jira/index.js +2 -2
  25. package/dist/backends/jira/index.js.map +1 -1
  26. package/dist/backends/jira/mappers.d.ts +1 -1
  27. package/dist/backends/jira/mappers.js +5 -4
  28. package/dist/backends/jira/mappers.js.map +1 -1
  29. package/dist/backends/local/items.js +9 -2
  30. package/dist/backends/local/items.js.map +1 -1
  31. package/dist/backends/types.d.ts +2 -3
  32. package/dist/backends/types.js +8 -2
  33. package/dist/backends/types.js.map +1 -1
  34. package/dist/cli/commands/config.js +0 -1
  35. package/dist/cli/commands/config.js.map +1 -1
  36. package/dist/cli/commands/item.js +45 -17
  37. package/dist/cli/commands/item.js.map +1 -1
  38. package/dist/cli/commands/mcp.js +24 -9
  39. package/dist/cli/commands/mcp.js.map +1 -1
  40. package/dist/cli/commands/pr.js +12 -6
  41. package/dist/cli/commands/pr.js.map +1 -1
  42. package/dist/cli/index.js +13 -6
  43. package/dist/cli/index.js.map +1 -1
  44. package/dist/components/BranchList.js +4 -2
  45. package/dist/components/BranchList.js.map +1 -1
  46. package/dist/components/CommandBar.js +8 -7
  47. package/dist/components/CommandBar.js.map +1 -1
  48. package/dist/components/DetailPanel.js +2 -2
  49. package/dist/components/DetailPanel.js.map +1 -1
  50. package/dist/components/Settings.js +1 -1
  51. package/dist/components/Settings.js.map +1 -1
  52. package/dist/components/StatusScreen.js +2 -2
  53. package/dist/components/StatusScreen.js.map +1 -1
  54. package/dist/components/WorkItemForm.js +107 -73
  55. package/dist/components/WorkItemForm.js.map +1 -1
  56. package/dist/components/WorkItemList.d.ts +5 -3
  57. package/dist/components/WorkItemList.js +175 -124
  58. package/dist/components/WorkItemList.js.map +1 -1
  59. package/dist/components/buildTree.js +15 -13
  60. package/dist/components/buildTree.js.map +1 -1
  61. package/dist/components/fuzzyMatch.js +1 -1
  62. package/dist/components/fuzzyMatch.js.map +1 -1
  63. package/dist/components/getMarkedDistribution.d.ts +2 -2
  64. package/dist/components/getMarkedDistribution.js +1 -1
  65. package/dist/components/getMarkedDistribution.js.map +1 -1
  66. package/dist/git.d.ts +2 -1
  67. package/dist/hooks/useFormValidation.js +31 -21
  68. package/dist/hooks/useFormValidation.js.map +1 -1
  69. package/dist/implement.js +3 -3
  70. package/dist/implement.js.map +1 -1
  71. package/dist/index.js +3 -1
  72. package/dist/index.js.map +1 -1
  73. package/dist/storage/config.d.ts +0 -1
  74. package/dist/storage/config.js +0 -6
  75. package/dist/storage/config.js.map +1 -1
  76. package/dist/storage/index.d.ts +25 -6
  77. package/dist/storage/index.js +304 -183
  78. package/dist/storage/index.js.map +1 -1
  79. package/dist/storage/mappers.d.ts +1 -1
  80. package/dist/storage/mappers.js +5 -3
  81. package/dist/storage/mappers.js.map +1 -1
  82. package/dist/storage/schema.d.ts +83 -380
  83. package/dist/storage/schema.js +27 -55
  84. package/dist/storage/schema.js.map +1 -1
  85. package/dist/storage/syncQueue.d.ts +2 -3
  86. package/dist/storage/syncQueue.js +10 -17
  87. package/dist/storage/syncQueue.js.map +1 -1
  88. package/dist/storage/undo.js +7 -6
  89. package/dist/storage/undo.js.map +1 -1
  90. package/dist/stores/backendDataStore.js +3 -1
  91. package/dist/stores/backendDataStore.js.map +1 -1
  92. package/dist/stores/formStackStore.d.ts +1 -1
  93. package/dist/stores/listViewStore.d.ts +6 -6
  94. package/dist/stores/navigationStore.d.ts +7 -7
  95. package/dist/stores/undoStore.d.ts +2 -2
  96. package/dist/sync/SyncManager.d.ts +6 -1
  97. package/dist/sync/SyncManager.js +80 -76
  98. package/dist/sync/SyncManager.js.map +1 -1
  99. package/dist/sync/types.d.ts +6 -7
  100. package/dist/test-helpers.d.ts +1 -1
  101. package/dist/test-helpers.js +11 -8
  102. package/dist/test-helpers.js.map +1 -1
  103. package/dist/types.d.ts +6 -5
  104. package/drizzle/0006_dual_id.sql +173 -0
  105. package/drizzle/meta/_journal.json +7 -0
  106. package/package.json +1 -1
@@ -37,7 +37,7 @@ function buildWorkItemColumns(capabilities, collapsedIds, selectionFg) {
37
37
  width: 4, // overridden dynamically via useMemo
38
38
  required: true,
39
39
  sortable: true,
40
- render: (ti, selected) => (_jsx(Text, { color: selected ? selectionFg : undefined, bold: selected, dimColor: ti.isCrossType && !selected, children: ti.item.id })),
40
+ render: (ti, selected) => (_jsx(Text, { color: selected ? selectionFg : undefined, bold: selected, dimColor: ti.isCrossType && !selected, children: ti.item.id ?? '\u00B7' })),
41
41
  });
42
42
  // Title column (flex)
43
43
  columns.push({
@@ -49,7 +49,7 @@ function buildWorkItemColumns(capabilities, collapsedIds, selectionFg) {
49
49
  render: (ti, selected) => {
50
50
  const { item, prefix, isCrossType, hasChildren } = ti;
51
51
  const collapseIndicator = hasChildren
52
- ? collapsedIds.has(item.id)
52
+ ? collapsedIds.has(item.rowId)
53
53
  ? '\u25B6 '
54
54
  : '\u25BC '
55
55
  : ' ';
@@ -124,11 +124,26 @@ function buildWorkItemColumns(capabilities, collapsedIds, selectionFg) {
124
124
  }
125
125
  return columns;
126
126
  }
127
- export function getTargetIds(markedIds, cursorItem) {
127
+ /** Get display IDs (strings) for target items. Used for backend API calls. */
128
+ export function getTargetIds(markedIds, cursorItem, allItems) {
129
+ if (markedIds.size > 0) {
130
+ // Look up display IDs from rowIds
131
+ const ids = [];
132
+ for (const rowId of markedIds) {
133
+ const item = allItems.find((i) => i.rowId === rowId);
134
+ if (item?.id)
135
+ ids.push(item.id);
136
+ }
137
+ return ids;
138
+ }
139
+ return cursorItem?.id ? [cursorItem.id] : [];
140
+ }
141
+ /** Get rowIds for target items. Used for undo entries and store operations. */
142
+ export function getTargetRowIds(markedIds, cursorItem) {
128
143
  if (markedIds.size > 0) {
129
144
  return [...markedIds];
130
145
  }
131
- return cursorItem ? [cursorItem.id] : [];
146
+ return cursorItem ? [cursorItem.rowId] : [];
132
147
  }
133
148
  export function WorkItemList() {
134
149
  const { accent, success, error: errorColor, warning: warningColor, marked, mutedDim, selectionBg, } = useThemeStore((s) => s.colors);
@@ -250,11 +265,16 @@ export function WorkItemList() {
250
265
  }
251
266
  }, [savedViews, defaultView]);
252
267
  const queue = useBackendDataStore((s) => s.queue);
253
- const queueWrite = async (action, itemId) => {
268
+ /** Look up the rowId for a display ID. Returns -1 if not found. */
269
+ const rowIdOf = (displayId) => {
270
+ const item = allItems.find((i) => i.id === displayId);
271
+ return item?.rowId ?? -1;
272
+ };
273
+ const queueWrite = async (action, itemRowId) => {
254
274
  if (queue) {
255
275
  await queue.append({
256
276
  action,
257
- itemId,
277
+ itemRowId,
258
278
  timestamp: new Date().toISOString(),
259
279
  });
260
280
  syncManager?.pushPending().catch((err) => {
@@ -272,7 +292,7 @@ export function WorkItemList() {
272
292
  type: 'update',
273
293
  label,
274
294
  itemSnapshots: snapshots,
275
- syncItemIds: [...targetIds],
295
+ syncItemRowIds: snapshots.map((s) => s.rowId),
276
296
  syncAction: 'update',
277
297
  });
278
298
  };
@@ -307,14 +327,16 @@ export function WorkItemList() {
307
327
  capabilities.relationships,
308
328
  sortStack,
309
329
  ]);
310
- const parentSuggestions = useMemo(() => allItems.map((item) => `${item.id} - ${item.title}`), [allItems]);
330
+ const parentSuggestions = useMemo(() => allItems
331
+ .filter((item) => item.id !== null)
332
+ .map((item) => `${item.id} - ${item.title}`), [allItems]);
311
333
  // Collapse state: set of item IDs that are collapsed (collapsed by default)
312
334
  // Track explicitly expanded items (inverse of collapsed).
313
335
  // All parents are collapsed by default; expanding removes from this set.
314
336
  // expandedIds comes from listViewStore (imported above)
315
337
  // Derive collapsedIds: all parents minus explicitly expanded ones
316
338
  const collapsedIds = useMemo(() => {
317
- const parentIds = new Set(fullTree.filter((t) => t.hasChildren).map((t) => t.item.id));
339
+ const parentIds = new Set(fullTree.filter((t) => t.hasChildren).map((t) => t.item.rowId));
318
340
  const collapsed = new Set();
319
341
  for (const id of parentIds) {
320
342
  if (!expandedIds.has(id)) {
@@ -332,7 +354,7 @@ export function WorkItemList() {
332
354
  continue;
333
355
  skipDepth = null;
334
356
  result.push(t);
335
- if (collapsedIds.has(t.item.id)) {
357
+ if (collapsedIds.has(t.item.rowId)) {
336
358
  skipDepth = t.depth;
337
359
  }
338
360
  }
@@ -388,14 +410,15 @@ export function WorkItemList() {
388
410
  }
389
411
  if (key.return) {
390
412
  const item = treeItems[cursor]?.item;
391
- if (item) {
413
+ if (item && item.id) {
414
+ const displayId = item.id;
392
415
  editorStore.getState().init(item.description ?? '', {
393
416
  returnScreen: 'list',
394
417
  onSave: (content) => {
395
418
  const state = backendDataStore.getState();
396
419
  if (state.backend) {
397
420
  void state.backend
398
- .cachedUpdateWorkItem(item.id, { description: content })
421
+ .cachedUpdateWorkItem(displayId, { description: content })
399
422
  .then(() => backendDataStore.getState().refresh());
400
423
  }
401
424
  },
@@ -451,7 +474,7 @@ export function WorkItemList() {
451
474
  setCursor(newCursor);
452
475
  const start = Math.min(anchor, newCursor);
453
476
  const end = Math.max(anchor, newCursor);
454
- setMarkedIds(new Set(treeItems.slice(start, end + 1).map((t) => t.item.id)));
477
+ setMarkedIds(new Set(treeItems.slice(start, end + 1).map((t) => t.item.rowId)));
455
478
  clearWarning();
456
479
  }
457
480
  if (matchesCommand('list-navigate', input, key)) {
@@ -487,18 +510,18 @@ export function WorkItemList() {
487
510
  const current = treeItems[cursor];
488
511
  if (current &&
489
512
  current.hasChildren &&
490
- collapsedIds.has(current.item.id)) {
491
- toggleExpanded(current.item.id);
513
+ collapsedIds.has(current.item.rowId)) {
514
+ toggleExpanded(current.item.rowId);
492
515
  }
493
516
  }
494
517
  if (matchesCommand('list-collapse', input, key) && treeItems.length > 0) {
495
518
  const current = treeItems[cursor];
496
519
  if (current) {
497
- if (current.hasChildren && !collapsedIds.has(current.item.id)) {
498
- toggleExpanded(current.item.id);
520
+ if (current.hasChildren && !collapsedIds.has(current.item.rowId)) {
521
+ toggleExpanded(current.item.rowId);
499
522
  }
500
523
  else if (current.depth > 0 && current.item.parent) {
501
- const parentIdx = treeItems.findIndex((t) => t.item.id === current.item.parent);
524
+ const parentIdx = treeItems.findIndex((t) => t.item.rowId === current.item.parent);
502
525
  if (parentIdx >= 0)
503
526
  setCursor(parentIdx);
504
527
  }
@@ -506,7 +529,7 @@ export function WorkItemList() {
506
529
  }
507
530
  if (matchesCommand('edit', input, key) && treeItems.length > 0) {
508
531
  setFormMode('item');
509
- selectWorkItem(treeItems[cursor].item.id);
532
+ selectWorkItem(treeItems[cursor].item.rowId);
510
533
  navigate('form');
511
534
  }
512
535
  if (matchesCommand('quit', input, key))
@@ -514,7 +537,7 @@ export function WorkItemList() {
514
537
  if (matchesCommand('set-iteration', input, key) &&
515
538
  capabilities.iterations &&
516
539
  treeItems.length > 0) {
517
- const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item);
540
+ const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item, allItems);
518
541
  if (targetIds.length > 0) {
519
542
  openOverlay({ type: 'iteration-picker', targetIds });
520
543
  }
@@ -554,14 +577,14 @@ export function WorkItemList() {
554
577
  formStackStore.getState().clear();
555
578
  navigationStore
556
579
  .getState()
557
- .setCreateChildParentId(treeItems[cursor].item.id);
580
+ .setCreateChildParentId(treeItems[cursor].item.rowId);
558
581
  setFormMode('item');
559
582
  setActiveTemplate(null);
560
583
  selectWorkItem(null);
561
584
  navigate('form');
562
585
  }
563
586
  if (matchesCommand('delete', input, key) && treeItems.length > 0) {
564
- const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item);
587
+ const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item, allItems);
565
588
  if (targetIds.length > 0) {
566
589
  openOverlay({ type: 'delete-confirm', targetIds });
567
590
  }
@@ -574,38 +597,42 @@ export function WorkItemList() {
574
597
  if (entry.type === 'delete') {
575
598
  if (isSoftDeleteBackend(backend)) {
576
599
  for (const snap of entry.itemSnapshots) {
577
- await backend.restoreWorkItem(snap.id);
600
+ if (snap.id)
601
+ await backend.restoreWorkItem(snap.id);
578
602
  }
579
603
  }
580
604
  if (queue) {
581
- await queue.removeByIds(entry.syncItemIds, 'delete');
605
+ await queue.removeByRowIds(entry.syncItemRowIds, 'delete');
582
606
  }
583
607
  refreshData();
584
608
  setToast(entry.itemSnapshots.length === 1
585
- ? `Restored #${entry.itemSnapshots[0].id}`
609
+ ? `Restored #${entry.itemSnapshots[0].id ?? entry.itemSnapshots[0].rowId}`
586
610
  : `Restored ${entry.itemSnapshots.length} items`);
587
611
  }
588
612
  else if (entry.type === 'create') {
589
- for (const id of entry.createdIds ?? []) {
590
- await backend.cachedDeleteWorkItem(id);
613
+ for (const rowId of entry.createdRowIds ?? []) {
614
+ const item = allItems.find((i) => i.rowId === rowId);
615
+ if (item?.id)
616
+ await backend.cachedDeleteWorkItem(item.id);
591
617
  }
592
618
  if (queue) {
593
- await queue.removeByIds(entry.syncItemIds, 'create');
619
+ await queue.removeByRowIds(entry.syncItemRowIds, 'create');
594
620
  }
595
621
  refreshData();
596
- setToast((entry.createdIds?.length ?? 0) === 1
597
- ? `Undid create #${entry.createdIds?.[0]}`
598
- : `Undid create of ${entry.createdIds?.length} items`);
622
+ setToast((entry.createdRowIds?.length ?? 0) === 1
623
+ ? `Undid create #${entry.createdRowIds?.[0]}`
624
+ : `Undid create of ${entry.createdRowIds?.length} items`);
599
625
  }
600
626
  else if (entry.type === 'update') {
601
627
  for (const snap of entry.itemSnapshots) {
602
- await backend.cachedUpdateWorkItem(snap.id, snap);
628
+ if (snap.id)
629
+ await backend.cachedUpdateWorkItem(snap.id, snap);
603
630
  }
604
631
  if (queue) {
605
- await queue.removeByIds(entry.syncItemIds, 'update');
632
+ await queue.removeByRowIds(entry.syncItemRowIds, 'update');
606
633
  }
607
634
  for (const snap of entry.itemSnapshots) {
608
- await queueWrite('update', snap.id);
635
+ await queueWrite('update', snap.rowId);
609
636
  }
610
637
  refreshData();
611
638
  setToast(`Undid ${entry.label}`);
@@ -618,39 +645,44 @@ export function WorkItemList() {
618
645
  }
619
646
  if (matchesCommand('open', input, key) && treeItems.length > 0) {
620
647
  setFormMode('item');
621
- selectWorkItem(treeItems[cursor].item.id);
648
+ selectWorkItem(treeItems[cursor].item.rowId);
622
649
  navigate('form');
623
650
  }
624
651
  if (matchesCommand('branch', input, key) &&
625
652
  gitAvailable &&
626
653
  treeItems.length > 0) {
627
654
  const item = treeItems[cursor].item;
628
- const comments = item.comments;
629
- try {
630
- const itemUrl = backend?.getItemUrl(item.id) || '';
631
- // Suspend terminal for interactive child process
632
- process.stdin.setRawMode?.(false);
633
- const result = beginImplementation(item, comments, { branchMode, branchCommand, copyToClipboard }, process.cwd(), { itemUrl });
634
- // Restore terminal after interactive shell
635
- process.stdin.setRawMode?.(true);
636
- console.clear();
637
- let msg = result.resumed
638
- ? `Resumed work on #${item.id}`
639
- : `Started work on #${item.id}`;
640
- if (result.commandFailed) {
641
- msg += ' (branch command failed, fell back to shell)';
642
- }
643
- setWarning(msg);
655
+ if (!item.id) {
656
+ setWarning('Cannot create branch for item without display ID');
644
657
  }
645
- catch (e) {
646
- process.stdin.setRawMode?.(true);
647
- console.clear();
648
- setWarning(e instanceof Error ? e.message : 'Failed to start implementation');
658
+ else {
659
+ const comments = item.comments;
660
+ try {
661
+ const itemUrl = backend?.getItemUrl(item.id) || '';
662
+ // Suspend terminal for interactive child process
663
+ process.stdin.setRawMode?.(false);
664
+ const result = beginImplementation(item, comments, { branchMode, branchCommand, copyToClipboard }, process.cwd(), { itemUrl });
665
+ // Restore terminal after interactive shell
666
+ process.stdin.setRawMode?.(true);
667
+ console.clear();
668
+ let msg = result.resumed
669
+ ? `Resumed work on #${item.id}`
670
+ : `Started work on #${item.id}`;
671
+ if (result.commandFailed) {
672
+ msg += ' (branch command failed, fell back to shell)';
673
+ }
674
+ setWarning(msg);
675
+ }
676
+ catch (e) {
677
+ process.stdin.setRawMode?.(true);
678
+ console.clear();
679
+ setWarning(e instanceof Error ? e.message : 'Failed to start implementation');
680
+ }
681
+ void backendDataStore
682
+ .getState()
683
+ .reloadItem(item.id)
684
+ .catch(() => { });
649
685
  }
650
- void backendDataStore
651
- .getState()
652
- .reloadItem(item.id)
653
- .catch(() => { });
654
686
  }
655
687
  if (matchesCommand('status', input, key)) {
656
688
  navigate('status');
@@ -669,7 +701,7 @@ export function WorkItemList() {
669
701
  setToast('Filters cleared');
670
702
  }
671
703
  if (matchesCommand('list-status', input, key) && treeItems.length > 0) {
672
- const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item);
704
+ const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item, allItems);
673
705
  if (targetIds.length > 0) {
674
706
  openOverlay({ type: 'status-picker', targetIds });
675
707
  }
@@ -690,7 +722,7 @@ export function WorkItemList() {
690
722
  prCapabilities.create &&
691
723
  treeItems.length > 0) {
692
724
  const item = treeItems[cursor]?.item;
693
- if (item) {
725
+ if (item && item.id) {
694
726
  const cwd = process.cwd();
695
727
  const currentBranch = getCurrentBranch(cwd);
696
728
  const expectedBranch = `tic/${slugify(item.id, item.title)}`;
@@ -700,7 +732,7 @@ export function WorkItemList() {
700
732
  .createPullRequest({
701
733
  title: item.title,
702
734
  sourceBranch,
703
- linkedItems: [item.id],
735
+ linkedItems: [item.rowId],
704
736
  })
705
737
  .then((pr) => {
706
738
  setToast(`PR #${String(pr.number)} created`);
@@ -731,12 +763,12 @@ export function WorkItemList() {
731
763
  }
732
764
  if (matchesCommand('mark', input, key) && treeItems.length > 0) {
733
765
  setRangeAnchor(null);
734
- const itemId = treeItems[cursor].item.id;
735
- toggleMarked(itemId);
766
+ const itemRowId = treeItems[cursor].item.rowId;
767
+ toggleMarked(itemRowId);
736
768
  }
737
769
  if (matchesCommand('clear-marks', input, key) && treeItems.length > 0) {
738
770
  setRangeAnchor(null);
739
- const visibleIds = treeItems.map((t) => t.item.id);
771
+ const visibleIds = treeItems.map((t) => t.item.rowId);
740
772
  const allMarked = visibleIds.every((id) => markedIds.has(id));
741
773
  if (allMarked) {
742
774
  clearMarked();
@@ -751,7 +783,7 @@ export function WorkItemList() {
751
783
  if (matchesCommand('set-priority', input, key) &&
752
784
  capabilities.fields.priority &&
753
785
  treeItems.length > 0) {
754
- const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item);
786
+ const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item, allItems);
755
787
  if (targetIds.length > 0) {
756
788
  openOverlay({ type: 'priority-picker', targetIds });
757
789
  }
@@ -759,7 +791,7 @@ export function WorkItemList() {
759
791
  if (matchesCommand('list-parent', input, key) &&
760
792
  capabilities.fields.parent &&
761
793
  treeItems.length > 0) {
762
- const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item);
794
+ const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item, allItems);
763
795
  if (targetIds.length > 0) {
764
796
  openOverlay({ type: 'parent-input', targetIds });
765
797
  }
@@ -767,7 +799,7 @@ export function WorkItemList() {
767
799
  if (matchesCommand('set-assignee', input, key) &&
768
800
  capabilities.fields.assignee &&
769
801
  treeItems.length > 0) {
770
- const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item);
802
+ const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item, allItems);
771
803
  if (targetIds.length > 0) {
772
804
  openOverlay({ type: 'assignee-input', targetIds });
773
805
  }
@@ -775,7 +807,7 @@ export function WorkItemList() {
775
807
  if (matchesCommand('set-labels', input, key) &&
776
808
  capabilities.fields.labels &&
777
809
  treeItems.length > 0) {
778
- const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item);
810
+ const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item, allItems);
779
811
  if (targetIds.length > 0) {
780
812
  openOverlay({ type: 'labels-input', targetIds });
781
813
  }
@@ -783,7 +815,7 @@ export function WorkItemList() {
783
815
  if (matchesCommand('set-type', input, key) &&
784
816
  capabilities.customTypes &&
785
817
  treeItems.length > 0) {
786
- const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item);
818
+ const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item, allItems);
787
819
  if (targetIds.length > 0) {
788
820
  openOverlay({ type: 'type-picker', targetIds });
789
821
  }
@@ -962,20 +994,20 @@ export function WorkItemList() {
962
994
  if (treeItems[cursor]) {
963
995
  navigationStore
964
996
  .getState()
965
- .setCreateChildParentId(treeItems[cursor].item.id);
997
+ .setCreateChildParentId(treeItems[cursor].item.rowId);
966
998
  selectWorkItem(null);
967
999
  navigate('form');
968
1000
  }
969
1001
  break;
970
1002
  case 'edit':
971
1003
  if (treeItems[cursor]) {
972
- selectWorkItem(treeItems[cursor].item.id);
1004
+ selectWorkItem(treeItems[cursor].item.rowId);
973
1005
  navigate('form');
974
1006
  }
975
1007
  break;
976
1008
  case 'delete':
977
1009
  if (treeItems.length > 0) {
978
- const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item);
1010
+ const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item, allItems);
979
1011
  if (targetIds.length > 0) {
980
1012
  openOverlay({ type: 'delete-confirm', targetIds });
981
1013
  }
@@ -984,37 +1016,44 @@ export function WorkItemList() {
984
1016
  case 'open':
985
1017
  if (treeItems[cursor]) {
986
1018
  setFormMode('item');
987
- selectWorkItem(treeItems[cursor].item.id);
1019
+ selectWorkItem(treeItems[cursor].item.rowId);
988
1020
  navigate('form');
989
1021
  }
990
1022
  break;
991
1023
  case 'branch':
992
1024
  if (treeItems[cursor]) {
993
1025
  const item = treeItems[cursor].item;
994
- const comments = item.comments;
995
- try {
996
- const itemUrl = backend?.getItemUrl(item.id) || '';
997
- process.stdin.setRawMode?.(false);
998
- const result = beginImplementation(item, comments, { branchMode, branchCommand, copyToClipboard }, process.cwd(), { itemUrl });
999
- process.stdin.setRawMode?.(true);
1000
- console.clear();
1001
- let msg = result.resumed
1002
- ? `Resumed work on #${item.id}`
1003
- : `Started work on #${item.id}`;
1004
- if (result.commandFailed) {
1005
- msg += ' (branch command failed, fell back to shell)';
1006
- }
1007
- setWarning(msg);
1026
+ if (!item.id) {
1027
+ setWarning('Cannot create branch for item without display ID');
1008
1028
  }
1009
- catch (e) {
1010
- process.stdin.setRawMode?.(true);
1011
- console.clear();
1012
- setWarning(e instanceof Error ? e.message : 'Failed to start implementation');
1029
+ else {
1030
+ const comments = item.comments;
1031
+ try {
1032
+ const itemUrl = backend?.getItemUrl(item.id) || '';
1033
+ process.stdin.setRawMode?.(false);
1034
+ const result = beginImplementation(item, comments, { branchMode, branchCommand, copyToClipboard }, process.cwd(), { itemUrl });
1035
+ process.stdin.setRawMode?.(true);
1036
+ console.clear();
1037
+ let msg = result.resumed
1038
+ ? `Resumed work on #${item.id}`
1039
+ : `Started work on #${item.id}`;
1040
+ if (result.commandFailed) {
1041
+ msg += ' (branch command failed, fell back to shell)';
1042
+ }
1043
+ setWarning(msg);
1044
+ }
1045
+ catch (e) {
1046
+ process.stdin.setRawMode?.(true);
1047
+ console.clear();
1048
+ setWarning(e instanceof Error
1049
+ ? e.message
1050
+ : 'Failed to start implementation');
1051
+ }
1052
+ void backendDataStore
1053
+ .getState()
1054
+ .reloadItem(item.id)
1055
+ .catch(() => { });
1013
1056
  }
1014
- void backendDataStore
1015
- .getState()
1016
- .reloadItem(item.id)
1017
- .catch(() => { });
1018
1057
  }
1019
1058
  break;
1020
1059
  case 'sync':
@@ -1029,7 +1068,7 @@ export function WorkItemList() {
1029
1068
  break;
1030
1069
  case 'set-iteration':
1031
1070
  if (treeItems.length > 0) {
1032
- const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item);
1071
+ const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item, allItems);
1033
1072
  if (targetIds.length > 0) {
1034
1073
  openOverlay({ type: 'iteration-picker', targetIds });
1035
1074
  }
@@ -1049,8 +1088,8 @@ export function WorkItemList() {
1049
1088
  break;
1050
1089
  case 'mark':
1051
1090
  if (treeItems[cursor]) {
1052
- const itemId = treeItems[cursor].item.id;
1053
- toggleMarked(itemId);
1091
+ const itemRowId = treeItems[cursor].item.rowId;
1092
+ toggleMarked(itemRowId);
1054
1093
  }
1055
1094
  break;
1056
1095
  case 'clear-marks':
@@ -1058,7 +1097,7 @@ export function WorkItemList() {
1058
1097
  break;
1059
1098
  case 'set-priority':
1060
1099
  {
1061
- const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item);
1100
+ const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item, allItems);
1062
1101
  if (targetIds.length > 0) {
1063
1102
  openOverlay({ type: 'priority-picker', targetIds });
1064
1103
  }
@@ -1066,7 +1105,7 @@ export function WorkItemList() {
1066
1105
  break;
1067
1106
  case 'set-assignee':
1068
1107
  {
1069
- const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item);
1108
+ const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item, allItems);
1070
1109
  if (targetIds.length > 0) {
1071
1110
  openOverlay({ type: 'assignee-input', targetIds });
1072
1111
  }
@@ -1074,7 +1113,7 @@ export function WorkItemList() {
1074
1113
  break;
1075
1114
  case 'set-labels':
1076
1115
  {
1077
- const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item);
1116
+ const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item, allItems);
1078
1117
  if (targetIds.length > 0) {
1079
1118
  openOverlay({ type: 'labels-input', targetIds });
1080
1119
  }
@@ -1082,7 +1121,7 @@ export function WorkItemList() {
1082
1121
  break;
1083
1122
  case 'set-type':
1084
1123
  {
1085
- const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item);
1124
+ const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item, allItems);
1086
1125
  if (targetIds.length > 0) {
1087
1126
  openOverlay({ type: 'type-picker', targetIds });
1088
1127
  }
@@ -1125,7 +1164,7 @@ export function WorkItemList() {
1125
1164
  }
1126
1165
  };
1127
1166
  const handleBulkAction = (action) => {
1128
- const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item);
1167
+ const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item, allItems);
1129
1168
  if (targetIds.length === 0)
1130
1169
  return;
1131
1170
  switch (action) {
@@ -1160,7 +1199,7 @@ export function WorkItemList() {
1160
1199
  : '';
1161
1200
  const visibleTreeItems = useMemo(() => treeItems.slice(viewport.start, viewport.end), [treeItems, viewport.start, viewport.end]);
1162
1201
  const workItemColumns = useMemo(() => {
1163
- const maxIdLen = visibleTreeItems.reduce((max, { item }) => Math.max(max, item.id.length), 2);
1202
+ const maxIdLen = visibleTreeItems.reduce((max, { item }) => Math.max(max, (item.id ?? '\u00B7').length), 2);
1164
1203
  const cols = buildWorkItemColumns(capabilities, collapsedIds, autoFg(selectionBg));
1165
1204
  cols[0].width = maxIdLen + 2;
1166
1205
  return cols;
@@ -1174,7 +1213,7 @@ export function WorkItemList() {
1174
1213
  ? formatIterationDates(it.startDate, it.endDate)
1175
1214
  : null;
1176
1215
  return dates ? ` ${dates}` : '';
1177
- })()] }), _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' ? ((() => {
1216
+ })()] }), _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.rowId}-${ti.item.type}`, isMarked: (ti) => markedIds.has(ti.item.rowId), 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' ? ((() => {
1178
1217
  const bulkItems = [];
1179
1218
  bulkItems.push({
1180
1219
  id: 'status',
@@ -1252,7 +1291,7 @@ export function WorkItemList() {
1252
1291
  await backend.cachedUpdateWorkItem(id, {
1253
1292
  status: item.value,
1254
1293
  });
1255
- await queueWrite('update', id);
1294
+ await queueWrite('update', rowIdOf(id));
1256
1295
  }
1257
1296
  for (const id of targetIds) {
1258
1297
  await backendDataStore.getState().reloadItem(id);
@@ -1280,7 +1319,7 @@ export function WorkItemList() {
1280
1319
  await backend.cachedUpdateWorkItem(id, {
1281
1320
  type: item.value,
1282
1321
  });
1283
- await queueWrite('update', id);
1322
+ await queueWrite('update', rowIdOf(id));
1284
1323
  }
1285
1324
  for (const id of targetIds) {
1286
1325
  await backendDataStore.getState().reloadItem(id);
@@ -1308,7 +1347,7 @@ export function WorkItemList() {
1308
1347
  pushUpdateUndo(targetIds, 'priority change');
1309
1348
  for (const id of targetIds) {
1310
1349
  await backend.cachedUpdateWorkItem(id, { priority });
1311
- await queueWrite('update', id);
1350
+ await queueWrite('update', rowIdOf(id));
1312
1351
  }
1313
1352
  for (const id of targetIds) {
1314
1353
  await backendDataStore.getState().reloadItem(id);
@@ -1350,16 +1389,21 @@ export function WorkItemList() {
1350
1389
  return;
1351
1390
  void (async () => {
1352
1391
  const raw = item.value.trim();
1353
- const newParent = raw.includes(' - ')
1392
+ const parentDisplayId = raw.includes(' - ')
1354
1393
  ? raw.split(' - ')[0].trim()
1355
1394
  : raw;
1395
+ const newParent = rowIdOf(parentDisplayId);
1396
+ if (newParent === -1) {
1397
+ setWarning(`Parent "${parentDisplayId}" not found`);
1398
+ return;
1399
+ }
1356
1400
  try {
1357
1401
  pushUpdateUndo(targetIds, 'parent change');
1358
1402
  for (const id of targetIds) {
1359
1403
  await backend.cachedUpdateWorkItem(id, {
1360
1404
  parent: newParent,
1361
1405
  });
1362
- await queueWrite('update', id);
1406
+ await queueWrite('update', rowIdOf(id));
1363
1407
  }
1364
1408
  clearWarning();
1365
1409
  }
@@ -1380,18 +1424,24 @@ export function WorkItemList() {
1380
1424
  return;
1381
1425
  void (async () => {
1382
1426
  const raw = text.trim();
1383
- const newParent = raw === ''
1384
- ? null
1385
- : raw.includes(' - ')
1427
+ let newParent = null;
1428
+ if (raw !== '') {
1429
+ const parentDisplayId = raw.includes(' - ')
1386
1430
  ? raw.split(' - ')[0].trim()
1387
1431
  : raw;
1432
+ newParent = rowIdOf(parentDisplayId);
1433
+ if (newParent === -1) {
1434
+ setWarning(`Parent "${parentDisplayId}" not found`);
1435
+ return;
1436
+ }
1437
+ }
1388
1438
  try {
1389
1439
  pushUpdateUndo(targetIds, 'parent change');
1390
1440
  for (const id of targetIds) {
1391
1441
  await backend.cachedUpdateWorkItem(id, {
1392
1442
  parent: newParent,
1393
1443
  });
1394
- await queueWrite('update', id);
1444
+ await queueWrite('update', rowIdOf(id));
1395
1445
  }
1396
1446
  clearWarning();
1397
1447
  }
@@ -1417,7 +1467,7 @@ export function WorkItemList() {
1417
1467
  await backend.cachedUpdateWorkItem(id, {
1418
1468
  assignee: item.value.trim(),
1419
1469
  });
1420
- await queueWrite('update', id);
1470
+ await queueWrite('update', rowIdOf(id));
1421
1471
  }
1422
1472
  for (const id of targetIds) {
1423
1473
  await backendDataStore.getState().reloadItem(id);
@@ -1441,7 +1491,7 @@ export function WorkItemList() {
1441
1491
  await backend.cachedUpdateWorkItem(id, {
1442
1492
  assignee: text.trim(),
1443
1493
  });
1444
- await queueWrite('update', id);
1494
+ await queueWrite('update', rowIdOf(id));
1445
1495
  }
1446
1496
  for (const id of targetIds) {
1447
1497
  await backendDataStore.getState().reloadItem(id);
@@ -1468,7 +1518,7 @@ export function WorkItemList() {
1468
1518
  const labels = selected.map((i) => i.value);
1469
1519
  for (const id of targetIds) {
1470
1520
  await backend.cachedUpdateWorkItem(id, { labels });
1471
- await queueWrite('update', id);
1521
+ await queueWrite('update', rowIdOf(id));
1472
1522
  }
1473
1523
  for (const id of targetIds) {
1474
1524
  await backendDataStore.getState().reloadItem(id);
@@ -1494,7 +1544,7 @@ export function WorkItemList() {
1494
1544
  .filter(Boolean);
1495
1545
  for (const id of targetIds) {
1496
1546
  await backend.cachedUpdateWorkItem(id, { labels });
1497
- await queueWrite('update', id);
1547
+ await queueWrite('update', rowIdOf(id));
1498
1548
  }
1499
1549
  for (const id of targetIds) {
1500
1550
  await backendDataStore.getState().reloadItem(id);
@@ -1664,7 +1714,7 @@ export function WorkItemList() {
1664
1714
  await backend.cachedUpdateWorkItem(id, {
1665
1715
  iteration: item.value,
1666
1716
  });
1667
- await queueWrite('update', id);
1717
+ await queueWrite('update', rowIdOf(id));
1668
1718
  }
1669
1719
  for (const id of targetIds) {
1670
1720
  await backendDataStore.getState().reloadItem(id);
@@ -1697,7 +1747,7 @@ export function WorkItemList() {
1697
1747
  else {
1698
1748
  await backend.cachedDeleteWorkItem(id);
1699
1749
  }
1700
- await queueWrite('delete', id);
1750
+ await queueWrite('delete', rowIdOf(id));
1701
1751
  }
1702
1752
  if (softDelete) {
1703
1753
  const evicted = undoStore.getState().pushUndo({
@@ -1706,18 +1756,19 @@ export function WorkItemList() {
1706
1756
  ? `deleted #${targetIds[0]}`
1707
1757
  : `deleted ${targetIds.length} items`,
1708
1758
  itemSnapshots: snapshots,
1709
- syncItemIds: [...targetIds],
1759
+ syncItemRowIds: snapshots.map((s) => s.rowId),
1710
1760
  syncAction: 'delete',
1711
1761
  });
1712
1762
  if (evicted?.type === 'delete') {
1713
1763
  for (const snap of evicted.itemSnapshots) {
1714
- await backend.permanentlyDeleteWorkItem(snap.id);
1764
+ if (snap.id)
1765
+ await backend.permanentlyDeleteWorkItem(snap.id);
1715
1766
  }
1716
1767
  }
1717
1768
  }
1718
1769
  closeOverlay();
1719
1770
  for (const id of targetIds) {
1720
- removeDeletedItem(id);
1771
+ removeDeletedItem(rowIdOf(id));
1721
1772
  }
1722
1773
  setCursor(Math.max(0, cursor - 1));
1723
1774
  for (const id of targetIds) {