@sascha384/tic 5.15.1 → 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 (121) 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/shared/api-client.js +9 -1
  32. package/dist/backends/shared/api-client.js.map +1 -1
  33. package/dist/backends/types.d.ts +2 -3
  34. package/dist/backends/types.js +8 -2
  35. package/dist/backends/types.js.map +1 -1
  36. package/dist/cli/commands/config.js +0 -1
  37. package/dist/cli/commands/config.js.map +1 -1
  38. package/dist/cli/commands/item.js +45 -17
  39. package/dist/cli/commands/item.js.map +1 -1
  40. package/dist/cli/commands/mcp.js +24 -9
  41. package/dist/cli/commands/mcp.js.map +1 -1
  42. package/dist/cli/commands/pr.js +12 -6
  43. package/dist/cli/commands/pr.js.map +1 -1
  44. package/dist/cli/index.js +13 -6
  45. package/dist/cli/index.js.map +1 -1
  46. package/dist/components/AuthPrompt.js +1 -1
  47. package/dist/components/AutocompleteInput.js +1 -1
  48. package/dist/components/BranchList.js +4 -2
  49. package/dist/components/BranchList.js.map +1 -1
  50. package/dist/components/CommandBar.js +8 -7
  51. package/dist/components/CommandBar.js.map +1 -1
  52. package/dist/components/DetailPanel.js +2 -2
  53. package/dist/components/DetailPanel.js.map +1 -1
  54. package/dist/components/MarkdownEditor.js +9 -5
  55. package/dist/components/MarkdownEditor.js.map +1 -1
  56. package/dist/components/MultiAutocompleteInput.js +1 -1
  57. package/dist/components/MultiSelectInput.js +1 -1
  58. package/dist/components/OverlayPanel.js +1 -1
  59. package/dist/components/Settings.js +2 -2
  60. package/dist/components/Settings.js.map +1 -1
  61. package/dist/components/StatusScreen.js +2 -2
  62. package/dist/components/StatusScreen.js.map +1 -1
  63. package/dist/components/TextInput.d.ts +12 -0
  64. package/dist/components/TextInput.js +207 -0
  65. package/dist/components/TextInput.js.map +1 -0
  66. package/dist/components/WorkItemForm.js +115 -98
  67. package/dist/components/WorkItemForm.js.map +1 -1
  68. package/dist/components/WorkItemList.d.ts +5 -3
  69. package/dist/components/WorkItemList.js +187 -123
  70. package/dist/components/WorkItemList.js.map +1 -1
  71. package/dist/components/buildTree.js +15 -13
  72. package/dist/components/buildTree.js.map +1 -1
  73. package/dist/components/fuzzyMatch.js +1 -1
  74. package/dist/components/fuzzyMatch.js.map +1 -1
  75. package/dist/components/getMarkedDistribution.d.ts +2 -2
  76. package/dist/components/getMarkedDistribution.js +1 -1
  77. package/dist/components/getMarkedDistribution.js.map +1 -1
  78. package/dist/git.d.ts +2 -1
  79. package/dist/hooks/useFormValidation.js +31 -21
  80. package/dist/hooks/useFormValidation.js.map +1 -1
  81. package/dist/hooks/useForwardDelete.d.ts +10 -0
  82. package/dist/hooks/useForwardDelete.js +34 -0
  83. package/dist/hooks/useForwardDelete.js.map +1 -0
  84. package/dist/implement.js +3 -3
  85. package/dist/implement.js.map +1 -1
  86. package/dist/index.js +3 -1
  87. package/dist/index.js.map +1 -1
  88. package/dist/storage/config.d.ts +0 -1
  89. package/dist/storage/config.js +0 -6
  90. package/dist/storage/config.js.map +1 -1
  91. package/dist/storage/index.d.ts +25 -6
  92. package/dist/storage/index.js +304 -183
  93. package/dist/storage/index.js.map +1 -1
  94. package/dist/storage/mappers.d.ts +1 -1
  95. package/dist/storage/mappers.js +5 -3
  96. package/dist/storage/mappers.js.map +1 -1
  97. package/dist/storage/schema.d.ts +83 -380
  98. package/dist/storage/schema.js +27 -55
  99. package/dist/storage/schema.js.map +1 -1
  100. package/dist/storage/syncQueue.d.ts +2 -3
  101. package/dist/storage/syncQueue.js +10 -17
  102. package/dist/storage/syncQueue.js.map +1 -1
  103. package/dist/storage/undo.js +7 -6
  104. package/dist/storage/undo.js.map +1 -1
  105. package/dist/stores/backendDataStore.js +3 -1
  106. package/dist/stores/backendDataStore.js.map +1 -1
  107. package/dist/stores/formStackStore.d.ts +1 -1
  108. package/dist/stores/listViewStore.d.ts +6 -6
  109. package/dist/stores/navigationStore.d.ts +7 -7
  110. package/dist/stores/undoStore.d.ts +2 -2
  111. package/dist/sync/SyncManager.d.ts +6 -1
  112. package/dist/sync/SyncManager.js +80 -76
  113. package/dist/sync/SyncManager.js.map +1 -1
  114. package/dist/sync/types.d.ts +6 -7
  115. package/dist/test-helpers.d.ts +1 -1
  116. package/dist/test-helpers.js +11 -8
  117. package/dist/test-helpers.js.map +1 -1
  118. package/dist/types.d.ts +6 -5
  119. package/drizzle/0006_dual_id.sql +173 -0
  120. package/drizzle/meta/_journal.json +7 -0
  121. package/package.json +1 -1
@@ -20,6 +20,7 @@ import { OverlayPanel } from './OverlayPanel.js';
20
20
  import { DetailPanel, countWrappedLines } from './DetailPanel.js';
21
21
  import { undoStore } from '../stores/undoStore.js';
22
22
  import { editorStore } from '../stores/editorStore.js';
23
+ import { formStackStore } from '../stores/formStackStore.js';
23
24
  import { CommandBar } from './CommandBar.js';
24
25
  import { isSoftDeleteBackend } from '../backends/types.js';
25
26
  import { filterStore, useFilterStore } from '../stores/filterStore.js';
@@ -36,7 +37,7 @@ function buildWorkItemColumns(capabilities, collapsedIds, selectionFg) {
36
37
  width: 4, // overridden dynamically via useMemo
37
38
  required: true,
38
39
  sortable: true,
39
- 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' })),
40
41
  });
41
42
  // Title column (flex)
42
43
  columns.push({
@@ -48,7 +49,7 @@ function buildWorkItemColumns(capabilities, collapsedIds, selectionFg) {
48
49
  render: (ti, selected) => {
49
50
  const { item, prefix, isCrossType, hasChildren } = ti;
50
51
  const collapseIndicator = hasChildren
51
- ? collapsedIds.has(item.id)
52
+ ? collapsedIds.has(item.rowId)
52
53
  ? '\u25B6 '
53
54
  : '\u25BC '
54
55
  : ' ';
@@ -123,11 +124,26 @@ function buildWorkItemColumns(capabilities, collapsedIds, selectionFg) {
123
124
  }
124
125
  return columns;
125
126
  }
126
- 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) {
127
143
  if (markedIds.size > 0) {
128
144
  return [...markedIds];
129
145
  }
130
- return cursorItem ? [cursorItem.id] : [];
146
+ return cursorItem ? [cursorItem.rowId] : [];
131
147
  }
132
148
  export function WorkItemList() {
133
149
  const { accent, success, error: errorColor, warning: warningColor, marked, mutedDim, selectionBg, } = useThemeStore((s) => s.colors);
@@ -249,11 +265,16 @@ export function WorkItemList() {
249
265
  }
250
266
  }, [savedViews, defaultView]);
251
267
  const queue = useBackendDataStore((s) => s.queue);
252
- 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) => {
253
274
  if (queue) {
254
275
  await queue.append({
255
276
  action,
256
- itemId,
277
+ itemRowId,
257
278
  timestamp: new Date().toISOString(),
258
279
  });
259
280
  syncManager?.pushPending().catch((err) => {
@@ -271,7 +292,7 @@ export function WorkItemList() {
271
292
  type: 'update',
272
293
  label,
273
294
  itemSnapshots: snapshots,
274
- syncItemIds: [...targetIds],
295
+ syncItemRowIds: snapshots.map((s) => s.rowId),
275
296
  syncAction: 'update',
276
297
  });
277
298
  };
@@ -306,14 +327,16 @@ export function WorkItemList() {
306
327
  capabilities.relationships,
307
328
  sortStack,
308
329
  ]);
309
- 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]);
310
333
  // Collapse state: set of item IDs that are collapsed (collapsed by default)
311
334
  // Track explicitly expanded items (inverse of collapsed).
312
335
  // All parents are collapsed by default; expanding removes from this set.
313
336
  // expandedIds comes from listViewStore (imported above)
314
337
  // Derive collapsedIds: all parents minus explicitly expanded ones
315
338
  const collapsedIds = useMemo(() => {
316
- 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));
317
340
  const collapsed = new Set();
318
341
  for (const id of parentIds) {
319
342
  if (!expandedIds.has(id)) {
@@ -331,7 +354,7 @@ export function WorkItemList() {
331
354
  continue;
332
355
  skipDepth = null;
333
356
  result.push(t);
334
- if (collapsedIds.has(t.item.id)) {
357
+ if (collapsedIds.has(t.item.rowId)) {
335
358
  skipDepth = t.depth;
336
359
  }
337
360
  }
@@ -387,14 +410,15 @@ export function WorkItemList() {
387
410
  }
388
411
  if (key.return) {
389
412
  const item = treeItems[cursor]?.item;
390
- if (item) {
413
+ if (item && item.id) {
414
+ const displayId = item.id;
391
415
  editorStore.getState().init(item.description ?? '', {
392
416
  returnScreen: 'list',
393
417
  onSave: (content) => {
394
418
  const state = backendDataStore.getState();
395
419
  if (state.backend) {
396
420
  void state.backend
397
- .cachedUpdateWorkItem(item.id, { description: content })
421
+ .cachedUpdateWorkItem(displayId, { description: content })
398
422
  .then(() => backendDataStore.getState().refresh());
399
423
  }
400
424
  },
@@ -450,7 +474,7 @@ export function WorkItemList() {
450
474
  setCursor(newCursor);
451
475
  const start = Math.min(anchor, newCursor);
452
476
  const end = Math.max(anchor, newCursor);
453
- 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)));
454
478
  clearWarning();
455
479
  }
456
480
  if (matchesCommand('list-navigate', input, key)) {
@@ -486,18 +510,18 @@ export function WorkItemList() {
486
510
  const current = treeItems[cursor];
487
511
  if (current &&
488
512
  current.hasChildren &&
489
- collapsedIds.has(current.item.id)) {
490
- toggleExpanded(current.item.id);
513
+ collapsedIds.has(current.item.rowId)) {
514
+ toggleExpanded(current.item.rowId);
491
515
  }
492
516
  }
493
517
  if (matchesCommand('list-collapse', input, key) && treeItems.length > 0) {
494
518
  const current = treeItems[cursor];
495
519
  if (current) {
496
- if (current.hasChildren && !collapsedIds.has(current.item.id)) {
497
- toggleExpanded(current.item.id);
520
+ if (current.hasChildren && !collapsedIds.has(current.item.rowId)) {
521
+ toggleExpanded(current.item.rowId);
498
522
  }
499
523
  else if (current.depth > 0 && current.item.parent) {
500
- const parentIdx = treeItems.findIndex((t) => t.item.id === current.item.parent);
524
+ const parentIdx = treeItems.findIndex((t) => t.item.rowId === current.item.parent);
501
525
  if (parentIdx >= 0)
502
526
  setCursor(parentIdx);
503
527
  }
@@ -505,7 +529,7 @@ export function WorkItemList() {
505
529
  }
506
530
  if (matchesCommand('edit', input, key) && treeItems.length > 0) {
507
531
  setFormMode('item');
508
- selectWorkItem(treeItems[cursor].item.id);
532
+ selectWorkItem(treeItems[cursor].item.rowId);
509
533
  navigate('form');
510
534
  }
511
535
  if (matchesCommand('quit', input, key))
@@ -513,7 +537,7 @@ export function WorkItemList() {
513
537
  if (matchesCommand('set-iteration', input, key) &&
514
538
  capabilities.iterations &&
515
539
  treeItems.length > 0) {
516
- const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item);
540
+ const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item, allItems);
517
541
  if (targetIds.length > 0) {
518
542
  openOverlay({ type: 'iteration-picker', targetIds });
519
543
  }
@@ -547,8 +571,20 @@ export function WorkItemList() {
547
571
  navigate('form');
548
572
  }
549
573
  }
574
+ if (matchesCommand('create-child', input, key) &&
575
+ treeItems[cursor] &&
576
+ capabilities.fields.parent) {
577
+ formStackStore.getState().clear();
578
+ navigationStore
579
+ .getState()
580
+ .setCreateChildParentId(treeItems[cursor].item.rowId);
581
+ setFormMode('item');
582
+ setActiveTemplate(null);
583
+ selectWorkItem(null);
584
+ navigate('form');
585
+ }
550
586
  if (matchesCommand('delete', input, key) && treeItems.length > 0) {
551
- const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item);
587
+ const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item, allItems);
552
588
  if (targetIds.length > 0) {
553
589
  openOverlay({ type: 'delete-confirm', targetIds });
554
590
  }
@@ -561,38 +597,42 @@ export function WorkItemList() {
561
597
  if (entry.type === 'delete') {
562
598
  if (isSoftDeleteBackend(backend)) {
563
599
  for (const snap of entry.itemSnapshots) {
564
- await backend.restoreWorkItem(snap.id);
600
+ if (snap.id)
601
+ await backend.restoreWorkItem(snap.id);
565
602
  }
566
603
  }
567
604
  if (queue) {
568
- await queue.removeByIds(entry.syncItemIds, 'delete');
605
+ await queue.removeByRowIds(entry.syncItemRowIds, 'delete');
569
606
  }
570
607
  refreshData();
571
608
  setToast(entry.itemSnapshots.length === 1
572
- ? `Restored #${entry.itemSnapshots[0].id}`
609
+ ? `Restored #${entry.itemSnapshots[0].id ?? entry.itemSnapshots[0].rowId}`
573
610
  : `Restored ${entry.itemSnapshots.length} items`);
574
611
  }
575
612
  else if (entry.type === 'create') {
576
- for (const id of entry.createdIds ?? []) {
577
- 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);
578
617
  }
579
618
  if (queue) {
580
- await queue.removeByIds(entry.syncItemIds, 'create');
619
+ await queue.removeByRowIds(entry.syncItemRowIds, 'create');
581
620
  }
582
621
  refreshData();
583
- setToast((entry.createdIds?.length ?? 0) === 1
584
- ? `Undid create #${entry.createdIds?.[0]}`
585
- : `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`);
586
625
  }
587
626
  else if (entry.type === 'update') {
588
627
  for (const snap of entry.itemSnapshots) {
589
- await backend.cachedUpdateWorkItem(snap.id, snap);
628
+ if (snap.id)
629
+ await backend.cachedUpdateWorkItem(snap.id, snap);
590
630
  }
591
631
  if (queue) {
592
- await queue.removeByIds(entry.syncItemIds, 'update');
632
+ await queue.removeByRowIds(entry.syncItemRowIds, 'update');
593
633
  }
594
634
  for (const snap of entry.itemSnapshots) {
595
- await queueWrite('update', snap.id);
635
+ await queueWrite('update', snap.rowId);
596
636
  }
597
637
  refreshData();
598
638
  setToast(`Undid ${entry.label}`);
@@ -605,39 +645,44 @@ export function WorkItemList() {
605
645
  }
606
646
  if (matchesCommand('open', input, key) && treeItems.length > 0) {
607
647
  setFormMode('item');
608
- selectWorkItem(treeItems[cursor].item.id);
648
+ selectWorkItem(treeItems[cursor].item.rowId);
609
649
  navigate('form');
610
650
  }
611
651
  if (matchesCommand('branch', input, key) &&
612
652
  gitAvailable &&
613
653
  treeItems.length > 0) {
614
654
  const item = treeItems[cursor].item;
615
- const comments = item.comments;
616
- try {
617
- const itemUrl = backend?.getItemUrl(item.id) || '';
618
- // Suspend terminal for interactive child process
619
- process.stdin.setRawMode?.(false);
620
- const result = beginImplementation(item, comments, { branchMode, branchCommand, copyToClipboard }, process.cwd(), { itemUrl });
621
- // Restore terminal after interactive shell
622
- process.stdin.setRawMode?.(true);
623
- console.clear();
624
- let msg = result.resumed
625
- ? `Resumed work on #${item.id}`
626
- : `Started work on #${item.id}`;
627
- if (result.commandFailed) {
628
- msg += ' (branch command failed, fell back to shell)';
629
- }
630
- setWarning(msg);
655
+ if (!item.id) {
656
+ setWarning('Cannot create branch for item without display ID');
631
657
  }
632
- catch (e) {
633
- process.stdin.setRawMode?.(true);
634
- console.clear();
635
- 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(() => { });
636
685
  }
637
- void backendDataStore
638
- .getState()
639
- .reloadItem(item.id)
640
- .catch(() => { });
641
686
  }
642
687
  if (matchesCommand('status', input, key)) {
643
688
  navigate('status');
@@ -656,7 +701,7 @@ export function WorkItemList() {
656
701
  setToast('Filters cleared');
657
702
  }
658
703
  if (matchesCommand('list-status', input, key) && treeItems.length > 0) {
659
- const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item);
704
+ const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item, allItems);
660
705
  if (targetIds.length > 0) {
661
706
  openOverlay({ type: 'status-picker', targetIds });
662
707
  }
@@ -677,7 +722,7 @@ export function WorkItemList() {
677
722
  prCapabilities.create &&
678
723
  treeItems.length > 0) {
679
724
  const item = treeItems[cursor]?.item;
680
- if (item) {
725
+ if (item && item.id) {
681
726
  const cwd = process.cwd();
682
727
  const currentBranch = getCurrentBranch(cwd);
683
728
  const expectedBranch = `tic/${slugify(item.id, item.title)}`;
@@ -687,7 +732,7 @@ export function WorkItemList() {
687
732
  .createPullRequest({
688
733
  title: item.title,
689
734
  sourceBranch,
690
- linkedItems: [item.id],
735
+ linkedItems: [item.rowId],
691
736
  })
692
737
  .then((pr) => {
693
738
  setToast(`PR #${String(pr.number)} created`);
@@ -718,12 +763,12 @@ export function WorkItemList() {
718
763
  }
719
764
  if (matchesCommand('mark', input, key) && treeItems.length > 0) {
720
765
  setRangeAnchor(null);
721
- const itemId = treeItems[cursor].item.id;
722
- toggleMarked(itemId);
766
+ const itemRowId = treeItems[cursor].item.rowId;
767
+ toggleMarked(itemRowId);
723
768
  }
724
769
  if (matchesCommand('clear-marks', input, key) && treeItems.length > 0) {
725
770
  setRangeAnchor(null);
726
- const visibleIds = treeItems.map((t) => t.item.id);
771
+ const visibleIds = treeItems.map((t) => t.item.rowId);
727
772
  const allMarked = visibleIds.every((id) => markedIds.has(id));
728
773
  if (allMarked) {
729
774
  clearMarked();
@@ -738,7 +783,7 @@ export function WorkItemList() {
738
783
  if (matchesCommand('set-priority', input, key) &&
739
784
  capabilities.fields.priority &&
740
785
  treeItems.length > 0) {
741
- const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item);
786
+ const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item, allItems);
742
787
  if (targetIds.length > 0) {
743
788
  openOverlay({ type: 'priority-picker', targetIds });
744
789
  }
@@ -746,7 +791,7 @@ export function WorkItemList() {
746
791
  if (matchesCommand('list-parent', input, key) &&
747
792
  capabilities.fields.parent &&
748
793
  treeItems.length > 0) {
749
- const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item);
794
+ const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item, allItems);
750
795
  if (targetIds.length > 0) {
751
796
  openOverlay({ type: 'parent-input', targetIds });
752
797
  }
@@ -754,7 +799,7 @@ export function WorkItemList() {
754
799
  if (matchesCommand('set-assignee', input, key) &&
755
800
  capabilities.fields.assignee &&
756
801
  treeItems.length > 0) {
757
- const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item);
802
+ const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item, allItems);
758
803
  if (targetIds.length > 0) {
759
804
  openOverlay({ type: 'assignee-input', targetIds });
760
805
  }
@@ -762,7 +807,7 @@ export function WorkItemList() {
762
807
  if (matchesCommand('set-labels', input, key) &&
763
808
  capabilities.fields.labels &&
764
809
  treeItems.length > 0) {
765
- const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item);
810
+ const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item, allItems);
766
811
  if (targetIds.length > 0) {
767
812
  openOverlay({ type: 'labels-input', targetIds });
768
813
  }
@@ -770,7 +815,7 @@ export function WorkItemList() {
770
815
  if (matchesCommand('set-type', input, key) &&
771
816
  capabilities.customTypes &&
772
817
  treeItems.length > 0) {
773
- const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item);
818
+ const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item, allItems);
774
819
  if (targetIds.length > 0) {
775
820
  openOverlay({ type: 'type-picker', targetIds });
776
821
  }
@@ -949,20 +994,20 @@ export function WorkItemList() {
949
994
  if (treeItems[cursor]) {
950
995
  navigationStore
951
996
  .getState()
952
- .setCreateChildParentId(treeItems[cursor].item.id);
997
+ .setCreateChildParentId(treeItems[cursor].item.rowId);
953
998
  selectWorkItem(null);
954
999
  navigate('form');
955
1000
  }
956
1001
  break;
957
1002
  case 'edit':
958
1003
  if (treeItems[cursor]) {
959
- selectWorkItem(treeItems[cursor].item.id);
1004
+ selectWorkItem(treeItems[cursor].item.rowId);
960
1005
  navigate('form');
961
1006
  }
962
1007
  break;
963
1008
  case 'delete':
964
1009
  if (treeItems.length > 0) {
965
- const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item);
1010
+ const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item, allItems);
966
1011
  if (targetIds.length > 0) {
967
1012
  openOverlay({ type: 'delete-confirm', targetIds });
968
1013
  }
@@ -971,37 +1016,44 @@ export function WorkItemList() {
971
1016
  case 'open':
972
1017
  if (treeItems[cursor]) {
973
1018
  setFormMode('item');
974
- selectWorkItem(treeItems[cursor].item.id);
1019
+ selectWorkItem(treeItems[cursor].item.rowId);
975
1020
  navigate('form');
976
1021
  }
977
1022
  break;
978
1023
  case 'branch':
979
1024
  if (treeItems[cursor]) {
980
1025
  const item = treeItems[cursor].item;
981
- const comments = item.comments;
982
- try {
983
- const itemUrl = backend?.getItemUrl(item.id) || '';
984
- process.stdin.setRawMode?.(false);
985
- const result = beginImplementation(item, comments, { branchMode, branchCommand, copyToClipboard }, process.cwd(), { itemUrl });
986
- process.stdin.setRawMode?.(true);
987
- console.clear();
988
- let msg = result.resumed
989
- ? `Resumed work on #${item.id}`
990
- : `Started work on #${item.id}`;
991
- if (result.commandFailed) {
992
- msg += ' (branch command failed, fell back to shell)';
993
- }
994
- setWarning(msg);
1026
+ if (!item.id) {
1027
+ setWarning('Cannot create branch for item without display ID');
995
1028
  }
996
- catch (e) {
997
- process.stdin.setRawMode?.(true);
998
- console.clear();
999
- 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(() => { });
1000
1056
  }
1001
- void backendDataStore
1002
- .getState()
1003
- .reloadItem(item.id)
1004
- .catch(() => { });
1005
1057
  }
1006
1058
  break;
1007
1059
  case 'sync':
@@ -1016,7 +1068,7 @@ export function WorkItemList() {
1016
1068
  break;
1017
1069
  case 'set-iteration':
1018
1070
  if (treeItems.length > 0) {
1019
- const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item);
1071
+ const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item, allItems);
1020
1072
  if (targetIds.length > 0) {
1021
1073
  openOverlay({ type: 'iteration-picker', targetIds });
1022
1074
  }
@@ -1036,8 +1088,8 @@ export function WorkItemList() {
1036
1088
  break;
1037
1089
  case 'mark':
1038
1090
  if (treeItems[cursor]) {
1039
- const itemId = treeItems[cursor].item.id;
1040
- toggleMarked(itemId);
1091
+ const itemRowId = treeItems[cursor].item.rowId;
1092
+ toggleMarked(itemRowId);
1041
1093
  }
1042
1094
  break;
1043
1095
  case 'clear-marks':
@@ -1045,7 +1097,7 @@ export function WorkItemList() {
1045
1097
  break;
1046
1098
  case 'set-priority':
1047
1099
  {
1048
- const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item);
1100
+ const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item, allItems);
1049
1101
  if (targetIds.length > 0) {
1050
1102
  openOverlay({ type: 'priority-picker', targetIds });
1051
1103
  }
@@ -1053,7 +1105,7 @@ export function WorkItemList() {
1053
1105
  break;
1054
1106
  case 'set-assignee':
1055
1107
  {
1056
- const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item);
1108
+ const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item, allItems);
1057
1109
  if (targetIds.length > 0) {
1058
1110
  openOverlay({ type: 'assignee-input', targetIds });
1059
1111
  }
@@ -1061,7 +1113,7 @@ export function WorkItemList() {
1061
1113
  break;
1062
1114
  case 'set-labels':
1063
1115
  {
1064
- const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item);
1116
+ const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item, allItems);
1065
1117
  if (targetIds.length > 0) {
1066
1118
  openOverlay({ type: 'labels-input', targetIds });
1067
1119
  }
@@ -1069,7 +1121,7 @@ export function WorkItemList() {
1069
1121
  break;
1070
1122
  case 'set-type':
1071
1123
  {
1072
- const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item);
1124
+ const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item, allItems);
1073
1125
  if (targetIds.length > 0) {
1074
1126
  openOverlay({ type: 'type-picker', targetIds });
1075
1127
  }
@@ -1112,7 +1164,7 @@ export function WorkItemList() {
1112
1164
  }
1113
1165
  };
1114
1166
  const handleBulkAction = (action) => {
1115
- const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item);
1167
+ const targetIds = getTargetIds(markedIds, treeItems[cursor]?.item, allItems);
1116
1168
  if (targetIds.length === 0)
1117
1169
  return;
1118
1170
  switch (action) {
@@ -1147,7 +1199,7 @@ export function WorkItemList() {
1147
1199
  : '';
1148
1200
  const visibleTreeItems = useMemo(() => treeItems.slice(viewport.start, viewport.end), [treeItems, viewport.start, viewport.end]);
1149
1201
  const workItemColumns = useMemo(() => {
1150
- 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);
1151
1203
  const cols = buildWorkItemColumns(capabilities, collapsedIds, autoFg(selectionBg));
1152
1204
  cols[0].width = maxIdLen + 2;
1153
1205
  return cols;
@@ -1161,7 +1213,7 @@ export function WorkItemList() {
1161
1213
  ? formatIterationDates(it.startDate, it.endDate)
1162
1214
  : null;
1163
1215
  return dates ? ` ${dates}` : '';
1164
- })()] }), _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' ? ((() => {
1165
1217
  const bulkItems = [];
1166
1218
  bulkItems.push({
1167
1219
  id: 'status',
@@ -1239,7 +1291,7 @@ export function WorkItemList() {
1239
1291
  await backend.cachedUpdateWorkItem(id, {
1240
1292
  status: item.value,
1241
1293
  });
1242
- await queueWrite('update', id);
1294
+ await queueWrite('update', rowIdOf(id));
1243
1295
  }
1244
1296
  for (const id of targetIds) {
1245
1297
  await backendDataStore.getState().reloadItem(id);
@@ -1267,7 +1319,7 @@ export function WorkItemList() {
1267
1319
  await backend.cachedUpdateWorkItem(id, {
1268
1320
  type: item.value,
1269
1321
  });
1270
- await queueWrite('update', id);
1322
+ await queueWrite('update', rowIdOf(id));
1271
1323
  }
1272
1324
  for (const id of targetIds) {
1273
1325
  await backendDataStore.getState().reloadItem(id);
@@ -1295,7 +1347,7 @@ export function WorkItemList() {
1295
1347
  pushUpdateUndo(targetIds, 'priority change');
1296
1348
  for (const id of targetIds) {
1297
1349
  await backend.cachedUpdateWorkItem(id, { priority });
1298
- await queueWrite('update', id);
1350
+ await queueWrite('update', rowIdOf(id));
1299
1351
  }
1300
1352
  for (const id of targetIds) {
1301
1353
  await backendDataStore.getState().reloadItem(id);
@@ -1337,16 +1389,21 @@ export function WorkItemList() {
1337
1389
  return;
1338
1390
  void (async () => {
1339
1391
  const raw = item.value.trim();
1340
- const newParent = raw.includes(' - ')
1392
+ const parentDisplayId = raw.includes(' - ')
1341
1393
  ? raw.split(' - ')[0].trim()
1342
1394
  : raw;
1395
+ const newParent = rowIdOf(parentDisplayId);
1396
+ if (newParent === -1) {
1397
+ setWarning(`Parent "${parentDisplayId}" not found`);
1398
+ return;
1399
+ }
1343
1400
  try {
1344
1401
  pushUpdateUndo(targetIds, 'parent change');
1345
1402
  for (const id of targetIds) {
1346
1403
  await backend.cachedUpdateWorkItem(id, {
1347
1404
  parent: newParent,
1348
1405
  });
1349
- await queueWrite('update', id);
1406
+ await queueWrite('update', rowIdOf(id));
1350
1407
  }
1351
1408
  clearWarning();
1352
1409
  }
@@ -1367,18 +1424,24 @@ export function WorkItemList() {
1367
1424
  return;
1368
1425
  void (async () => {
1369
1426
  const raw = text.trim();
1370
- const newParent = raw === ''
1371
- ? null
1372
- : raw.includes(' - ')
1427
+ let newParent = null;
1428
+ if (raw !== '') {
1429
+ const parentDisplayId = raw.includes(' - ')
1373
1430
  ? raw.split(' - ')[0].trim()
1374
1431
  : raw;
1432
+ newParent = rowIdOf(parentDisplayId);
1433
+ if (newParent === -1) {
1434
+ setWarning(`Parent "${parentDisplayId}" not found`);
1435
+ return;
1436
+ }
1437
+ }
1375
1438
  try {
1376
1439
  pushUpdateUndo(targetIds, 'parent change');
1377
1440
  for (const id of targetIds) {
1378
1441
  await backend.cachedUpdateWorkItem(id, {
1379
1442
  parent: newParent,
1380
1443
  });
1381
- await queueWrite('update', id);
1444
+ await queueWrite('update', rowIdOf(id));
1382
1445
  }
1383
1446
  clearWarning();
1384
1447
  }
@@ -1404,7 +1467,7 @@ export function WorkItemList() {
1404
1467
  await backend.cachedUpdateWorkItem(id, {
1405
1468
  assignee: item.value.trim(),
1406
1469
  });
1407
- await queueWrite('update', id);
1470
+ await queueWrite('update', rowIdOf(id));
1408
1471
  }
1409
1472
  for (const id of targetIds) {
1410
1473
  await backendDataStore.getState().reloadItem(id);
@@ -1428,7 +1491,7 @@ export function WorkItemList() {
1428
1491
  await backend.cachedUpdateWorkItem(id, {
1429
1492
  assignee: text.trim(),
1430
1493
  });
1431
- await queueWrite('update', id);
1494
+ await queueWrite('update', rowIdOf(id));
1432
1495
  }
1433
1496
  for (const id of targetIds) {
1434
1497
  await backendDataStore.getState().reloadItem(id);
@@ -1455,7 +1518,7 @@ export function WorkItemList() {
1455
1518
  const labels = selected.map((i) => i.value);
1456
1519
  for (const id of targetIds) {
1457
1520
  await backend.cachedUpdateWorkItem(id, { labels });
1458
- await queueWrite('update', id);
1521
+ await queueWrite('update', rowIdOf(id));
1459
1522
  }
1460
1523
  for (const id of targetIds) {
1461
1524
  await backendDataStore.getState().reloadItem(id);
@@ -1481,7 +1544,7 @@ export function WorkItemList() {
1481
1544
  .filter(Boolean);
1482
1545
  for (const id of targetIds) {
1483
1546
  await backend.cachedUpdateWorkItem(id, { labels });
1484
- await queueWrite('update', id);
1547
+ await queueWrite('update', rowIdOf(id));
1485
1548
  }
1486
1549
  for (const id of targetIds) {
1487
1550
  await backendDataStore.getState().reloadItem(id);
@@ -1651,7 +1714,7 @@ export function WorkItemList() {
1651
1714
  await backend.cachedUpdateWorkItem(id, {
1652
1715
  iteration: item.value,
1653
1716
  });
1654
- await queueWrite('update', id);
1717
+ await queueWrite('update', rowIdOf(id));
1655
1718
  }
1656
1719
  for (const id of targetIds) {
1657
1720
  await backendDataStore.getState().reloadItem(id);
@@ -1684,7 +1747,7 @@ export function WorkItemList() {
1684
1747
  else {
1685
1748
  await backend.cachedDeleteWorkItem(id);
1686
1749
  }
1687
- await queueWrite('delete', id);
1750
+ await queueWrite('delete', rowIdOf(id));
1688
1751
  }
1689
1752
  if (softDelete) {
1690
1753
  const evicted = undoStore.getState().pushUndo({
@@ -1693,18 +1756,19 @@ export function WorkItemList() {
1693
1756
  ? `deleted #${targetIds[0]}`
1694
1757
  : `deleted ${targetIds.length} items`,
1695
1758
  itemSnapshots: snapshots,
1696
- syncItemIds: [...targetIds],
1759
+ syncItemRowIds: snapshots.map((s) => s.rowId),
1697
1760
  syncAction: 'delete',
1698
1761
  });
1699
1762
  if (evicted?.type === 'delete') {
1700
1763
  for (const snap of evicted.itemSnapshots) {
1701
- await backend.permanentlyDeleteWorkItem(snap.id);
1764
+ if (snap.id)
1765
+ await backend.permanentlyDeleteWorkItem(snap.id);
1702
1766
  }
1703
1767
  }
1704
1768
  }
1705
1769
  closeOverlay();
1706
1770
  for (const id of targetIds) {
1707
- removeDeletedItem(id);
1771
+ removeDeletedItem(rowIdOf(id));
1708
1772
  }
1709
1773
  setCursor(Math.max(0, cursor - 1));
1710
1774
  for (const id of targetIds) {