@kaban-board/tui 0.3.1 → 0.3.2

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 (2) hide show
  1. package/dist/index.js +598 -310
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -124,9 +124,10 @@ async function refreshBoard(state) {
124
124
  alignItems: "center"
125
125
  });
126
126
  const modeIndicator = state.archiveViewMode ? " [ARCHIVE]" : "";
127
+ const projectPath = ` (${state.projectRoot})`;
127
128
  const headerText = new TextRenderable2(renderer, {
128
129
  id: "header-text",
129
- content: t`${fg(COLORS.warning)(LOGO)} ${fg(COLORS.accent)(state.boardName)}${fg(COLORS.textMuted)(modeIndicator)}`
130
+ content: t`${fg(COLORS.warning)(LOGO)} ${fg(COLORS.accent)(state.boardName)}${fg(COLORS.textMuted)(modeIndicator)}${fg(COLORS.textDim)(projectPath)}`
130
131
  });
131
132
  header.add(headerText);
132
133
  mainContainer.add(header);
@@ -307,6 +308,44 @@ function createButtonRow(renderer, id, buttons) {
307
308
  return state;
308
309
  }
309
310
 
311
+ // src/lib/constants.ts
312
+ var MODAL_WIDTHS = {
313
+ small: 32,
314
+ confirmation: 45,
315
+ medium: 52,
316
+ large: 60,
317
+ history: 65
318
+ };
319
+ var MODAL_HEIGHTS = {
320
+ small: 8,
321
+ confirmation: 10,
322
+ medium: 11,
323
+ large: 24
324
+ };
325
+ var TRUNCATION = {
326
+ taskTitle: 40,
327
+ taskTitleShort: 30,
328
+ taskId: 8
329
+ };
330
+
331
+ // src/lib/error.ts
332
+ async function withErrorHandling(state, operation, context) {
333
+ try {
334
+ return await operation();
335
+ } catch (error) {
336
+ try {
337
+ const message = error instanceof Error ? error.message : String(error);
338
+ showErrorToast(state, `${context}: ${message}`);
339
+ } catch {
340
+ console.error(`[CRITICAL] Error handler failed: ${context}`);
341
+ }
342
+ return null;
343
+ }
344
+ }
345
+ function showErrorToast(_state, message) {
346
+ console.error(`[TUI Error] ${message}`);
347
+ }
348
+
310
349
  // src/components/overlay.ts
311
350
  import { BoxRenderable as BoxRenderable4 } from "@opentui/core";
312
351
  function createModalOverlay(renderer, options) {
@@ -385,8 +424,8 @@ function showAddTaskModal(state, onTaskCreated) {
385
424
  return;
386
425
  const { overlay, dialog } = createModalOverlay(renderer, {
387
426
  id: "add-task-dialog",
388
- width: 52,
389
- height: 11
427
+ width: MODAL_WIDTHS.medium,
428
+ height: MODAL_HEIGHTS.medium
390
429
  });
391
430
  const titleRow = new BoxRenderable5(renderer, {
392
431
  id: "title-row",
@@ -425,11 +464,15 @@ function showAddTaskModal(state, onTaskCreated) {
425
464
  const spacer2 = new BoxRenderable5(renderer, { id: "dialog-spacer2", width: "100%", height: 1 });
426
465
  const doCreate = async () => {
427
466
  const taskTitle = input.value.trim();
428
- if (taskTitle) {
429
- await state.taskService.addTask({ title: taskTitle, columnId: column.id });
467
+ if (!taskTitle) {
468
+ closeModal(state);
469
+ return;
430
470
  }
471
+ const result = await withErrorHandling(state, () => state.taskService.addTask({ title: taskTitle, columnId: column.id }), "Failed to create task");
431
472
  closeModal(state);
432
- await onTaskCreated();
473
+ if (result) {
474
+ await onTaskCreated();
475
+ }
433
476
  };
434
477
  const doCancel = () => {
435
478
  closeModal(state);
@@ -455,9 +498,6 @@ function showAddTaskModal(state, onTaskCreated) {
455
498
  state.activeModal = "addTask";
456
499
  input.on(InputRenderableEvents.ENTER, doCreate);
457
500
  }
458
- // src/components/modals/archive-task.ts
459
- import { BoxRenderable as BoxRenderable6, TextRenderable as TextRenderable5 } from "@opentui/core";
460
-
461
501
  // src/lib/types.ts
462
502
  function getSelectedTaskId(state) {
463
503
  const column = state.columns[state.currentColumnIndex];
@@ -471,9 +511,97 @@ function getSelectedTaskId(state) {
471
511
  return typeof value === "string" ? value : null;
472
512
  }
473
513
 
514
+ // src/components/modals/factories/confirmation.ts
515
+ import { BoxRenderable as BoxRenderable6, TextRenderable as TextRenderable5 } from "@opentui/core";
516
+ function showConfirmationModal(state, options, onConfirm) {
517
+ const { renderer } = state;
518
+ const width = options.width ?? MODAL_WIDTHS.confirmation;
519
+ const height = options.height ?? MODAL_HEIGHTS.confirmation;
520
+ const messageColor = options.messageColor ?? COLORS.text;
521
+ const warningColor = options.warningColor ?? COLORS.warning;
522
+ const confirmHint = options.confirmHint ?? "[y] Yes [n/Esc] No";
523
+ const borderColor = options.borderColor ?? COLORS.accent;
524
+ const { overlay, dialog } = createModalOverlay(renderer, {
525
+ id: options.id,
526
+ width,
527
+ height,
528
+ borderColor
529
+ });
530
+ const titleRow = new BoxRenderable6(renderer, {
531
+ id: `${options.id}-title-row`,
532
+ width: "100%",
533
+ height: 1,
534
+ justifyContent: "center"
535
+ });
536
+ const title = new TextRenderable5(renderer, {
537
+ id: `${options.id}-title`,
538
+ content: options.title,
539
+ fg: options.titleColor
540
+ });
541
+ titleRow.add(title);
542
+ const spacer1 = new BoxRenderable6(renderer, {
543
+ id: `${options.id}-spacer1`,
544
+ width: "100%",
545
+ height: 1
546
+ });
547
+ const messageRow = new BoxRenderable6(renderer, {
548
+ id: `${options.id}-message-row`,
549
+ width: "100%",
550
+ height: 1
551
+ });
552
+ const message = new TextRenderable5(renderer, {
553
+ id: `${options.id}-message`,
554
+ content: options.message,
555
+ fg: messageColor
556
+ });
557
+ messageRow.add(message);
558
+ dialog.add(titleRow);
559
+ dialog.add(spacer1);
560
+ dialog.add(messageRow);
561
+ if (options.warning) {
562
+ const warningRow = new BoxRenderable6(renderer, {
563
+ id: `${options.id}-warning-row`,
564
+ width: "100%",
565
+ height: 1
566
+ });
567
+ const warning = new TextRenderable5(renderer, {
568
+ id: `${options.id}-warning`,
569
+ content: options.warning,
570
+ fg: warningColor
571
+ });
572
+ warningRow.add(warning);
573
+ dialog.add(warningRow);
574
+ }
575
+ const spacer2 = new BoxRenderable6(renderer, {
576
+ id: `${options.id}-spacer2`,
577
+ width: "100%",
578
+ height: options.warning ? 1 : 2
579
+ });
580
+ dialog.add(spacer2);
581
+ const hintRow = new BoxRenderable6(renderer, {
582
+ id: `${options.id}-hint-row`,
583
+ width: "100%",
584
+ height: 1,
585
+ justifyContent: "center"
586
+ });
587
+ const hint = new TextRenderable5(renderer, {
588
+ id: `${options.id}-hint`,
589
+ content: confirmHint,
590
+ fg: COLORS.textMuted
591
+ });
592
+ hintRow.add(hint);
593
+ dialog.add(hintRow);
594
+ renderer.root.add(overlay);
595
+ state.modalOverlay = overlay;
596
+ state.activeModal = options.modalType;
597
+ state.onModalConfirm = async () => {
598
+ await onConfirm();
599
+ closeModal(state);
600
+ };
601
+ }
602
+
474
603
  // src/components/modals/archive-task.ts
475
604
  async function showArchiveTaskModal(state, onArchived) {
476
- const { renderer } = state;
477
605
  const taskId = getSelectedTaskId(state);
478
606
  if (!taskId) {
479
607
  return;
@@ -486,81 +614,160 @@ async function showArchiveTaskModal(state, onArchived) {
486
614
  return;
487
615
  }
488
616
  state.selectedTask = task;
489
- const { overlay, dialog } = createModalOverlay(renderer, {
617
+ showConfirmationModal(state, {
490
618
  id: "archive-task-dialog",
491
- width: 45,
492
- height: 10,
493
- borderColor: COLORS.warning
619
+ modalType: "archiveTask",
620
+ title: "Archive Task?",
621
+ titleColor: COLORS.warning,
622
+ message: task.title.slice(0, 40),
623
+ warning: "Task will be moved to archive.",
624
+ warningColor: COLORS.textMuted,
625
+ borderColor: COLORS.warning,
626
+ confirmHint: "[y] Archive [n/Esc] Cancel"
627
+ }, async () => {
628
+ const result = await withErrorHandling(state, () => state.taskService.archiveTasks({ taskIds: [taskId] }), "Failed to archive task");
629
+ if (result) {
630
+ await onArchived();
631
+ }
494
632
  });
495
- const titleRow = new BoxRenderable6(renderer, {
496
- id: "archive-title-row",
633
+ }
634
+ // src/components/modals/history.ts
635
+ import { AuditService } from "@kaban-board/core/bun";
636
+ import { BoxRenderable as BoxRenderable7, TextRenderable as TextRenderable6 } from "@opentui/core";
637
+ var DIALOG_WIDTH = MODAL_WIDTHS.history;
638
+ var MAX_ENTRIES = 15;
639
+ function formatTimestamp(date) {
640
+ return date.toLocaleString("en-US", {
641
+ month: "short",
642
+ day: "numeric",
643
+ hour: "2-digit",
644
+ minute: "2-digit"
645
+ });
646
+ }
647
+ function formatEntry(entry) {
648
+ const time = formatTimestamp(entry.timestamp);
649
+ const actor = entry.actor ? `@${entry.actor}` : "";
650
+ if (entry.eventType === "CREATE") {
651
+ return `${time} CREATED ${actor}`;
652
+ }
653
+ if (entry.eventType === "DELETE") {
654
+ return `${time} DELETED ${actor}`;
655
+ }
656
+ const field = entry.fieldName ?? "?";
657
+ const oldVal = entry.oldValue === null ? "null" : truncate(entry.oldValue, 15);
658
+ const newVal = entry.newValue === null ? "null" : truncate(entry.newValue, 15);
659
+ return `${time} ${field}: ${oldVal} -> ${newVal} ${actor}`;
660
+ }
661
+ async function showTaskHistoryModal(state) {
662
+ const { renderer, db, taskService } = state;
663
+ const taskId = getSelectedTaskId(state);
664
+ if (!taskId)
665
+ return;
666
+ const task = await taskService.getTask(taskId);
667
+ if (!task)
668
+ return;
669
+ blurCurrentColumnSelect(state);
670
+ const auditService = new AuditService(db);
671
+ const entries = await withErrorHandling(state, () => auditService.getTaskHistory(task.id, MAX_ENTRIES), "Failed to load task history");
672
+ if (!entries)
673
+ return;
674
+ const dialogHeight = Math.min(entries.length + 8, 20);
675
+ const { overlay, dialog } = createModalOverlay(renderer, {
676
+ id: "history-dialog",
677
+ width: DIALOG_WIDTH,
678
+ height: dialogHeight
679
+ });
680
+ const headerDivider = createSectionDivider(renderer, {
681
+ label: "Task History",
682
+ width: DIALOG_WIDTH - 4,
683
+ id: "history-header"
684
+ });
685
+ const titleRow = new BoxRenderable7(renderer, {
686
+ id: "history-title-row",
497
687
  width: "100%",
498
688
  height: 1,
499
- justifyContent: "center"
689
+ flexDirection: "row"
500
690
  });
501
- const title = new TextRenderable5(renderer, {
502
- id: "archive-title",
503
- content: "Archive Task?",
504
- fg: COLORS.warning
691
+ const taskTitle = new TextRenderable6(renderer, {
692
+ id: "history-task-title",
693
+ content: `[${task.id.slice(0, TRUNCATION.taskId)}] "${truncate(task.title, TRUNCATION.taskTitle)}"`,
694
+ fg: COLORS.accent
505
695
  });
506
- titleRow.add(title);
507
- const spacer1 = new BoxRenderable6(renderer, { id: "archive-spacer1", width: "100%", height: 1 });
508
- const taskRow = new BoxRenderable6(renderer, {
509
- id: "archive-task-row",
696
+ titleRow.add(taskTitle);
697
+ const spacer = new BoxRenderable7(renderer, {
698
+ id: "history-spacer",
510
699
  width: "100%",
511
700
  height: 1
512
701
  });
513
- const taskText = new TextRenderable5(renderer, {
514
- id: "archive-task-text",
515
- content: task.title.slice(0, 40),
516
- fg: COLORS.text
517
- });
518
- taskRow.add(taskText);
519
- const infoRow = new BoxRenderable6(renderer, {
520
- id: "archive-info-row",
702
+ dialog.add(headerDivider);
703
+ dialog.add(titleRow);
704
+ dialog.add(spacer);
705
+ if (entries.length === 0) {
706
+ const emptyRow = new BoxRenderable7(renderer, {
707
+ id: "history-empty",
708
+ width: "100%",
709
+ height: 1
710
+ });
711
+ const emptyText = new TextRenderable6(renderer, {
712
+ id: "history-empty-text",
713
+ content: " No history found",
714
+ fg: COLORS.textMuted
715
+ });
716
+ emptyRow.add(emptyText);
717
+ dialog.add(emptyRow);
718
+ } else {
719
+ for (let i = 0;i < entries.length; i++) {
720
+ const entry = entries[i];
721
+ const row = new BoxRenderable7(renderer, {
722
+ id: `history-entry-${i}`,
723
+ width: "100%",
724
+ height: 1
725
+ });
726
+ let color = COLORS.textMuted;
727
+ if (entry.eventType === "CREATE")
728
+ color = COLORS.success;
729
+ else if (entry.eventType === "DELETE")
730
+ color = COLORS.danger;
731
+ else
732
+ color = COLORS.text;
733
+ const text = new TextRenderable6(renderer, {
734
+ id: `history-entry-text-${i}`,
735
+ content: ` ${formatEntry(entry)}`,
736
+ fg: color
737
+ });
738
+ row.add(text);
739
+ dialog.add(row);
740
+ }
741
+ }
742
+ const footerSpacer = new BoxRenderable7(renderer, {
743
+ id: "history-footer-spacer",
521
744
  width: "100%",
522
745
  height: 1
523
746
  });
524
- const info = new TextRenderable5(renderer, {
525
- id: "archive-info",
526
- content: "Task will be moved to archive.",
527
- fg: COLORS.textMuted
528
- });
529
- infoRow.add(info);
530
- const spacer2 = new BoxRenderable6(renderer, { id: "archive-spacer2", width: "100%", height: 2 });
531
- const hintRow = new BoxRenderable6(renderer, {
532
- id: "archive-hint-row",
747
+ const hintRow = new BoxRenderable7(renderer, {
748
+ id: "history-hint-row",
533
749
  width: "100%",
534
750
  height: 1,
535
751
  justifyContent: "center"
536
752
  });
537
- const hint = new TextRenderable5(renderer, {
538
- id: "archive-hint",
539
- content: "[y] Archive [n/Esc] Cancel",
540
- fg: COLORS.textMuted
753
+ const hintText = new TextRenderable6(renderer, {
754
+ id: "history-hint-text",
755
+ content: "[Esc] close",
756
+ fg: COLORS.textDim
541
757
  });
542
- hintRow.add(hint);
543
- dialog.add(titleRow);
544
- dialog.add(spacer1);
545
- dialog.add(taskRow);
546
- dialog.add(infoRow);
547
- dialog.add(spacer2);
758
+ hintRow.add(hintText);
759
+ dialog.add(footerSpacer);
548
760
  dialog.add(hintRow);
549
761
  renderer.root.add(overlay);
550
762
  state.modalOverlay = overlay;
551
- state.activeModal = "archiveTask";
552
- state.onModalConfirm = async () => {
553
- await state.taskService.archiveTasks({ taskIds: [taskId] });
554
- closeModal(state);
555
- await onArchived();
556
- };
763
+ state.activeModal = "taskHistory";
557
764
  }
558
765
  // src/components/modals/assign-task.ts
559
766
  import {
560
- BoxRenderable as BoxRenderable7,
767
+ BoxRenderable as BoxRenderable8,
561
768
  InputRenderable as InputRenderable2,
562
769
  InputRenderableEvents as InputRenderableEvents2,
563
- TextRenderable as TextRenderable6
770
+ TextRenderable as TextRenderable7
564
771
  } from "@opentui/core";
565
772
  async function showAssignTaskModal(state, onAssigned) {
566
773
  const { renderer } = state;
@@ -574,39 +781,39 @@ async function showAssignTaskModal(state, onAssigned) {
574
781
  }
575
782
  const { overlay, dialog } = createModalOverlay(renderer, {
576
783
  id: "assign-task-dialog",
577
- width: 45,
784
+ width: MODAL_WIDTHS.confirmation,
578
785
  height: 12
579
786
  });
580
- const titleRow = new BoxRenderable7(renderer, {
787
+ const titleRow = new BoxRenderable8(renderer, {
581
788
  id: "assign-title-row",
582
789
  width: "100%",
583
790
  height: 1
584
791
  });
585
- const title = new TextRenderable6(renderer, {
792
+ const title = new TextRenderable7(renderer, {
586
793
  id: "assign-title",
587
794
  content: "Assign Task",
588
795
  fg: COLORS.accent
589
796
  });
590
797
  titleRow.add(title);
591
- const taskRow = new BoxRenderable7(renderer, {
798
+ const taskRow = new BoxRenderable8(renderer, {
592
799
  id: "assign-task-row",
593
800
  width: "100%",
594
801
  height: 1
595
802
  });
596
- const taskText = new TextRenderable6(renderer, {
803
+ const taskText = new TextRenderable7(renderer, {
597
804
  id: "assign-task-text",
598
- content: task.title.slice(0, 40),
805
+ content: task.title.slice(0, TRUNCATION.taskTitle),
599
806
  fg: COLORS.textMuted
600
807
  });
601
808
  taskRow.add(taskText);
602
- const spacer1 = new BoxRenderable7(renderer, { id: "assign-spacer1", width: "100%", height: 1 });
603
- const labelRow = new BoxRenderable7(renderer, {
809
+ const spacer1 = new BoxRenderable8(renderer, { id: "assign-spacer1", width: "100%", height: 1 });
810
+ const labelRow = new BoxRenderable8(renderer, {
604
811
  id: "assign-label-row",
605
812
  width: "100%",
606
813
  height: 1
607
814
  });
608
815
  const currentAssignee = task.assignedTo ?? "(unassigned)";
609
- const label = new TextRenderable6(renderer, {
816
+ const label = new TextRenderable7(renderer, {
610
817
  id: "assign-label",
611
818
  content: `Current: ${currentAssignee}`,
612
819
  fg: COLORS.text
@@ -623,14 +830,14 @@ async function showAssignTaskModal(state, onAssigned) {
623
830
  focusedBackgroundColor: COLORS.inputBg,
624
831
  cursorColor: COLORS.cursor
625
832
  });
626
- const spacer2 = new BoxRenderable7(renderer, { id: "assign-spacer2", width: "100%", height: 1 });
833
+ const spacer2 = new BoxRenderable8(renderer, { id: "assign-spacer2", width: "100%", height: 1 });
627
834
  const doAssign = async () => {
628
835
  const assignee = input.value.trim();
629
- await state.taskService.updateTask(taskId, {
630
- assignedTo: assignee || null
631
- });
836
+ const result = await withErrorHandling(state, () => state.taskService.updateTask(taskId, { assignedTo: assignee || null }), "Failed to assign task");
632
837
  closeModal(state);
633
- await onAssigned();
838
+ if (result) {
839
+ await onAssigned();
840
+ }
634
841
  };
635
842
  const doCancel = () => {
636
843
  closeModal(state);
@@ -658,9 +865,7 @@ async function showAssignTaskModal(state, onAssigned) {
658
865
  input.on(InputRenderableEvents2.ENTER, doAssign);
659
866
  }
660
867
  // src/components/modals/delete-task.ts
661
- import { BoxRenderable as BoxRenderable8, TextRenderable as TextRenderable7 } from "@opentui/core";
662
868
  async function showDeleteTaskModal(state, onDeleted) {
663
- const { renderer } = state;
664
869
  const taskId = getSelectedTaskId(state);
665
870
  if (!taskId) {
666
871
  return;
@@ -670,168 +875,166 @@ async function showDeleteTaskModal(state, onDeleted) {
670
875
  return;
671
876
  }
672
877
  state.selectedTask = task;
673
- const { overlay, dialog } = createModalOverlay(renderer, {
878
+ showConfirmationModal(state, {
674
879
  id: "delete-task-dialog",
675
- width: 45,
676
- height: 10,
677
- borderColor: COLORS.danger
678
- });
679
- const titleRow = new BoxRenderable8(renderer, {
680
- id: "delete-title-row",
681
- width: "100%",
682
- height: 1,
683
- justifyContent: "center"
684
- });
685
- const title = new TextRenderable7(renderer, {
686
- id: "delete-title",
687
- content: "Delete Task?",
688
- fg: COLORS.danger
689
- });
690
- titleRow.add(title);
691
- const spacer1 = new BoxRenderable8(renderer, { id: "delete-spacer1", width: "100%", height: 1 });
692
- const taskRow = new BoxRenderable8(renderer, {
693
- id: "delete-task-row",
694
- width: "100%",
695
- height: 1
696
- });
697
- const taskText = new TextRenderable7(renderer, {
698
- id: "delete-task-text",
699
- content: task.title.slice(0, 40),
700
- fg: COLORS.text
701
- });
702
- taskRow.add(taskText);
703
- const warningRow = new BoxRenderable8(renderer, {
704
- id: "delete-warning-row",
705
- width: "100%",
706
- height: 1
707
- });
708
- const warning = new TextRenderable7(renderer, {
709
- id: "delete-warning",
710
- content: "This action cannot be undone.",
711
- fg: COLORS.warning
712
- });
713
- warningRow.add(warning);
714
- const spacer2 = new BoxRenderable8(renderer, { id: "delete-spacer2", width: "100%", height: 2 });
715
- const hintRow = new BoxRenderable8(renderer, {
716
- id: "delete-hint-row",
717
- width: "100%",
718
- height: 1,
719
- justifyContent: "center"
880
+ modalType: "deleteTask",
881
+ title: "Delete Task?",
882
+ titleColor: COLORS.danger,
883
+ message: task.title.slice(0, 40),
884
+ warning: "This action cannot be undone.",
885
+ borderColor: COLORS.danger,
886
+ confirmHint: "[y] Delete [n/Esc] Cancel"
887
+ }, async () => {
888
+ const result = await withErrorHandling(state, () => state.taskService.deleteTask(taskId), "Failed to delete task");
889
+ if (result !== null) {
890
+ await onDeleted();
891
+ }
720
892
  });
721
- const hint = new TextRenderable7(renderer, {
722
- id: "delete-hint",
723
- content: "[y] Delete [n/Esc] Cancel",
724
- fg: COLORS.textMuted
893
+ }
894
+ // src/components/modals/purge-archive.ts
895
+ async function showPurgeArchiveModal(state, onPurged) {
896
+ const stats = await state.taskService.getArchiveStats();
897
+ if (stats.totalArchived === 0) {
898
+ return;
899
+ }
900
+ const taskCount = stats.totalArchived;
901
+ const taskWord = taskCount === 1 ? "task" : "tasks";
902
+ showConfirmationModal(state, {
903
+ id: "purge-archive-dialog",
904
+ modalType: "purgeArchive",
905
+ title: "Purge Archive?",
906
+ titleColor: COLORS.danger,
907
+ message: `${taskCount} archived ${taskWord} will be deleted`,
908
+ warning: "This will permanently delete ALL archived tasks. Cannot be undone.",
909
+ borderColor: COLORS.danger,
910
+ width: 50,
911
+ height: 12,
912
+ confirmHint: "[y] Purge All [n/Esc] Cancel"
913
+ }, async () => {
914
+ const result = await withErrorHandling(state, () => state.taskService.purgeArchive(), "Failed to purge archive");
915
+ if (result) {
916
+ await onPurged();
917
+ }
725
918
  });
726
- hintRow.add(hint);
727
- dialog.add(titleRow);
728
- dialog.add(spacer1);
729
- dialog.add(taskRow);
730
- dialog.add(warningRow);
731
- dialog.add(spacer2);
732
- dialog.add(hintRow);
733
- renderer.root.add(overlay);
734
- state.modalOverlay = overlay;
735
- state.activeModal = "deleteTask";
736
- state.onModalConfirm = async () => {
737
- await state.taskService.deleteTask(taskId);
738
- closeModal(state);
739
- await onDeleted();
740
- };
741
919
  }
742
920
  // src/components/modals/restore-task.ts
743
- import { BoxRenderable as BoxRenderable9, TextRenderable as TextRenderable8 } from "@opentui/core";
744
921
  async function showRestoreTaskModal(state, onRestored) {
745
- const { renderer } = state;
746
922
  const taskId = getSelectedTaskId(state);
747
923
  if (!taskId) {
748
924
  return;
749
925
  }
750
926
  const task = await state.taskService.getTask(taskId);
751
- if (!task) {
752
- return;
753
- }
754
- if (!task.archived) {
927
+ if (!task || !task.archived) {
755
928
  return;
756
929
  }
757
930
  state.selectedTask = task;
758
- const { overlay, dialog } = createModalOverlay(renderer, {
931
+ showConfirmationModal(state, {
759
932
  id: "restore-task-dialog",
760
- width: 45,
761
- height: 10,
762
- borderColor: COLORS.success
933
+ modalType: "restoreTask",
934
+ title: "Restore Task?",
935
+ titleColor: COLORS.success,
936
+ message: task.title.slice(0, 40),
937
+ warning: "Task will be restored to its original column.",
938
+ warningColor: COLORS.textMuted,
939
+ borderColor: COLORS.success,
940
+ confirmHint: "[y] Restore [n/Esc] Cancel"
941
+ }, async () => {
942
+ const result = await withErrorHandling(state, () => state.taskService.restoreTask(taskId), "Failed to restore task");
943
+ if (result) {
944
+ await onRestored();
945
+ }
946
+ });
947
+ }
948
+ // src/components/modals/search-archive.ts
949
+ import {
950
+ BoxRenderable as BoxRenderable9,
951
+ InputRenderable as InputRenderable3,
952
+ InputRenderableEvents as InputRenderableEvents3,
953
+ TextRenderable as TextRenderable8
954
+ } from "@opentui/core";
955
+ function showSearchArchiveModal(state, onResults) {
956
+ const { renderer } = state;
957
+ const { overlay, dialog } = createModalOverlay(renderer, {
958
+ id: "search-archive-dialog",
959
+ width: MODAL_WIDTHS.medium,
960
+ height: 9
763
961
  });
764
962
  const titleRow = new BoxRenderable9(renderer, {
765
- id: "restore-title-row",
963
+ id: "search-title-row",
766
964
  width: "100%",
767
- height: 1,
768
- justifyContent: "center"
965
+ height: 1
769
966
  });
770
967
  const title = new TextRenderable8(renderer, {
771
- id: "restore-title",
772
- content: "Restore Task?",
773
- fg: COLORS.success
968
+ id: "search-title",
969
+ content: " Search Archive ",
970
+ fg: COLORS.accent
774
971
  });
775
972
  titleRow.add(title);
776
- const spacer1 = new BoxRenderable9(renderer, { id: "restore-spacer1", width: "100%", height: 1 });
777
- const taskRow = new BoxRenderable9(renderer, {
778
- id: "restore-task-row",
973
+ const spacer1 = new BoxRenderable9(renderer, { id: "search-spacer1", width: "100%", height: 1 });
974
+ const labelRow = new BoxRenderable9(renderer, {
975
+ id: "search-label-row",
779
976
  width: "100%",
780
977
  height: 1
781
978
  });
782
- const taskText = new TextRenderable8(renderer, {
783
- id: "restore-task-text",
784
- content: task.title.slice(0, 40),
979
+ const label = new TextRenderable8(renderer, {
980
+ id: "search-label",
981
+ content: "Search query:",
785
982
  fg: COLORS.text
786
983
  });
787
- taskRow.add(taskText);
788
- const infoRow = new BoxRenderable9(renderer, {
789
- id: "restore-info-row",
790
- width: "100%",
791
- height: 1
792
- });
793
- const info = new TextRenderable8(renderer, {
794
- id: "restore-info",
795
- content: "Task will be restored to its original column.",
796
- fg: COLORS.textMuted
984
+ labelRow.add(label);
985
+ const input = new InputRenderable3(renderer, {
986
+ id: "search-input",
987
+ width: 46,
988
+ height: 1,
989
+ placeholder: "Enter search query...",
990
+ textColor: COLORS.text,
991
+ placeholderColor: COLORS.textDim,
992
+ backgroundColor: COLORS.inputBg,
993
+ focusedBackgroundColor: COLORS.inputBg,
994
+ cursorColor: COLORS.cursor
797
995
  });
798
- infoRow.add(info);
799
- const spacer2 = new BoxRenderable9(renderer, { id: "restore-spacer2", width: "100%", height: 2 });
996
+ const spacer2 = new BoxRenderable9(renderer, { id: "search-spacer2", width: "100%", height: 2 });
800
997
  const hintRow = new BoxRenderable9(renderer, {
801
- id: "restore-hint-row",
998
+ id: "search-hint-row",
802
999
  width: "100%",
803
- height: 1,
804
- justifyContent: "center"
1000
+ height: 1
805
1001
  });
806
1002
  const hint = new TextRenderable8(renderer, {
807
- id: "restore-hint",
808
- content: "[y] Restore [n/Esc] Cancel",
1003
+ id: "search-hint",
1004
+ content: "[Enter] Search [Esc] Cancel",
809
1005
  fg: COLORS.textMuted
810
1006
  });
811
1007
  hintRow.add(hint);
812
1008
  dialog.add(titleRow);
813
1009
  dialog.add(spacer1);
814
- dialog.add(taskRow);
815
- dialog.add(infoRow);
1010
+ dialog.add(labelRow);
1011
+ dialog.add(input);
816
1012
  dialog.add(spacer2);
817
1013
  dialog.add(hintRow);
818
1014
  renderer.root.add(overlay);
819
1015
  state.modalOverlay = overlay;
820
- state.activeModal = "restoreTask";
821
- state.onModalConfirm = async () => {
822
- await state.taskService.restoreTask(taskId);
1016
+ state.activeModal = "searchArchive";
1017
+ state.taskInput = input;
1018
+ setImmediate(() => input.focus());
1019
+ input.on(InputRenderableEvents3.ENTER, async () => {
1020
+ const query = input.value.trim();
1021
+ if (!query)
1022
+ return;
1023
+ const result = await withErrorHandling(state, () => state.taskService.searchArchive(query, { limit: 50 }), "Failed to search archive");
823
1024
  closeModal(state);
824
- await onRestored();
825
- };
1025
+ if (result) {
1026
+ await onResults(result.tasks);
1027
+ }
1028
+ });
826
1029
  }
827
1030
  // src/components/modals/edit-task.ts
828
1031
  import {
829
1032
  BoxRenderable as BoxRenderable10,
830
- InputRenderable as InputRenderable3,
831
- InputRenderableEvents as InputRenderableEvents3,
1033
+ InputRenderable as InputRenderable4,
1034
+ InputRenderableEvents as InputRenderableEvents4,
832
1035
  TextRenderable as TextRenderable9
833
1036
  } from "@opentui/core";
834
- var DIALOG_WIDTH = 60;
1037
+ var DIALOG_WIDTH2 = MODAL_WIDTHS.large;
835
1038
  var DESC_INPUT_HEIGHT = 6;
836
1039
  async function showEditTaskModal(state, callbacks) {
837
1040
  const { renderer } = state;
@@ -847,7 +1050,7 @@ async function showEditTaskModal(state, callbacks) {
847
1050
  const dialogHeight = 18;
848
1051
  const { overlay, dialog } = createModalOverlay(renderer, {
849
1052
  id: "edit-task-dialog",
850
- width: DIALOG_WIDTH,
1053
+ width: DIALOG_WIDTH2,
851
1054
  height: dialogHeight
852
1055
  });
853
1056
  const headerRow = new BoxRenderable10(renderer, {
@@ -857,7 +1060,7 @@ async function showEditTaskModal(state, callbacks) {
857
1060
  });
858
1061
  const headerText = new TextRenderable9(renderer, {
859
1062
  id: "edit-header-text",
860
- content: `Edit Task: ${truncate(task.title, DIALOG_WIDTH - 20)}`,
1063
+ content: `Edit Task: ${truncate(task.title, DIALOG_WIDTH2 - 20)}`,
861
1064
  fg: COLORS.accent
862
1065
  });
863
1066
  headerRow.add(headerText);
@@ -877,9 +1080,9 @@ async function showEditTaskModal(state, callbacks) {
877
1080
  fg: COLORS.textMuted
878
1081
  });
879
1082
  titleLabelRow.add(titleLabel);
880
- const titleInput = new InputRenderable3(renderer, {
1083
+ const titleInput = new InputRenderable4(renderer, {
881
1084
  id: "edit-title-input",
882
- width: DIALOG_WIDTH - 6,
1085
+ width: DIALOG_WIDTH2 - 6,
883
1086
  height: 1,
884
1087
  placeholder: "Task title...",
885
1088
  textColor: COLORS.text,
@@ -905,9 +1108,9 @@ async function showEditTaskModal(state, callbacks) {
905
1108
  fg: COLORS.textMuted
906
1109
  });
907
1110
  descLabelRow.add(descLabel);
908
- const descInput = new InputRenderable3(renderer, {
1111
+ const descInput = new InputRenderable4(renderer, {
909
1112
  id: "edit-desc-input",
910
- width: DIALOG_WIDTH - 6,
1113
+ width: DIALOG_WIDTH2 - 6,
911
1114
  height: DESC_INPUT_HEIGHT,
912
1115
  placeholder: "Task description (optional)...",
913
1116
  textColor: COLORS.text,
@@ -930,10 +1133,13 @@ async function showEditTaskModal(state, callbacks) {
930
1133
  }
931
1134
  const hasChanges = newTitle !== task.title || newDescription !== (task.description ?? "");
932
1135
  if (hasChanges) {
933
- await state.taskService.updateTask(task.id, {
1136
+ const result = await withErrorHandling(state, () => state.taskService.updateTask(task.id, {
934
1137
  title: newTitle,
935
1138
  description: newDescription || undefined
936
- });
1139
+ }), "Failed to save task");
1140
+ if (!result) {
1141
+ return;
1142
+ }
937
1143
  }
938
1144
  closeModal(state);
939
1145
  state.editTaskState = null;
@@ -979,7 +1185,7 @@ async function showEditTaskModal(state, callbacks) {
979
1185
  state.taskInput = titleInput;
980
1186
  state.buttonRow = buttonRow;
981
1187
  state.activeModal = "editTask";
982
- titleInput.on(InputRenderableEvents3.ENTER, doSave);
1188
+ titleInput.on(InputRenderableEvents4.ENTER, doSave);
983
1189
  state.editTaskRuntime = {
984
1190
  titleInput,
985
1191
  descInput,
@@ -1012,6 +1218,30 @@ function focusNextEditField(state) {
1012
1218
  break;
1013
1219
  }
1014
1220
  }
1221
+ function focusPrevEditField(state) {
1222
+ if (!state.editTaskRuntime || !state.editTaskState)
1223
+ return;
1224
+ const { titleInput, descInput } = state.editTaskRuntime;
1225
+ const { buttonRow } = state;
1226
+ switch (state.editTaskState.focusedField) {
1227
+ case "title":
1228
+ titleInput.blur();
1229
+ buttonRow?.setFocused(true);
1230
+ state.editTaskState.focusedField = "buttons";
1231
+ break;
1232
+ case "description":
1233
+ descInput.blur();
1234
+ titleInput.focus();
1235
+ buttonRow?.setFocused(false);
1236
+ state.editTaskState.focusedField = "title";
1237
+ break;
1238
+ case "buttons":
1239
+ buttonRow?.setFocused(false);
1240
+ descInput.focus();
1241
+ state.editTaskState.focusedField = "description";
1242
+ break;
1243
+ }
1244
+ }
1015
1245
  function cancelEditTask(state) {
1016
1246
  if (!state.editTaskRuntime)
1017
1247
  return;
@@ -1027,8 +1257,12 @@ var SHORTCUTS = [
1027
1257
  ["m", "Move task (change status)"],
1028
1258
  ["u", "Assign user to task"],
1029
1259
  ["d", "Delete task"],
1260
+ ["C", "Complete task (move to done)"],
1261
+ ["H", "View task history"],
1030
1262
  ["x", "Archive task"],
1031
1263
  ["r", "Restore task (archive view)"],
1264
+ ["/", "Search archive (archive view)"],
1265
+ ["P", "Purge archive (archive view)"],
1032
1266
  ["Tab", "Toggle archive view"],
1033
1267
  ["?", "Show/hide help"],
1034
1268
  ["q", "Quit"]
@@ -1037,8 +1271,8 @@ function showHelpModal(state) {
1037
1271
  const { renderer } = state;
1038
1272
  const { overlay, dialog } = createModalOverlay(renderer, {
1039
1273
  id: "help-dialog",
1040
- width: 45,
1041
- height: 20,
1274
+ width: MODAL_WIDTHS.confirmation,
1275
+ height: MODAL_HEIGHTS.large,
1042
1276
  padding: 2
1043
1277
  });
1044
1278
  const titleRow = new BoxRenderable11(renderer, {
@@ -1144,7 +1378,7 @@ async function showMoveTaskModal(state, onMoved) {
1144
1378
  });
1145
1379
  const taskText = new TextRenderable11(renderer, {
1146
1380
  id: "move-task-text",
1147
- content: task.title.slice(0, 35),
1381
+ content: task.title.slice(0, TRUNCATION.taskTitleShort),
1148
1382
  fg: COLORS.textMuted
1149
1383
  });
1150
1384
  taskRow.add(taskText);
@@ -1200,20 +1434,54 @@ async function showMoveTaskModal(state, onMoved) {
1200
1434
  state.activeModal = "moveTask";
1201
1435
  columnSelect.on(SelectRenderableEvents.ITEM_SELECTED, async () => {
1202
1436
  const selected = columnSelect.getSelectedOption();
1203
- if (selected?.value) {
1204
- await state.taskService.moveTask(taskId, selected.value);
1437
+ if (selected?.value && typeof selected.value === "string") {
1438
+ const columnId = selected.value;
1439
+ const result = await withErrorHandling(state, () => state.taskService.moveTask(taskId, columnId), "Failed to move task");
1440
+ closeModal(state);
1441
+ if (result) {
1442
+ await onMoved();
1443
+ }
1444
+ } else {
1445
+ closeModal(state);
1205
1446
  }
1206
- closeModal(state);
1207
- await onMoved();
1208
1447
  });
1209
1448
  }
1210
1449
  // src/components/modals/onboarding.ts
1211
1450
  import {
1212
1451
  BoxRenderable as BoxRenderable13,
1213
- InputRenderable as InputRenderable4,
1214
- InputRenderableEvents as InputRenderableEvents4,
1452
+ InputRenderable as InputRenderable5,
1453
+ InputRenderableEvents as InputRenderableEvents5,
1215
1454
  TextRenderable as TextRenderable12
1216
1455
  } from "@opentui/core";
1456
+
1457
+ // src/lib/db-client.ts
1458
+ function getDbClient(db) {
1459
+ const internal = db;
1460
+ if (!internal.$client) {
1461
+ throw new Error("Unable to access database client");
1462
+ }
1463
+ const client = internal.$client;
1464
+ if (typeof client.query === "function" || typeof client.execute === "function") {
1465
+ return client;
1466
+ }
1467
+ throw new Error("Unknown database client type");
1468
+ }
1469
+ function isBunSqlite(client) {
1470
+ return typeof client.query === "function";
1471
+ }
1472
+ async function getDataVersion(client) {
1473
+ if (isBunSqlite(client)) {
1474
+ const row = client.query("PRAGMA data_version").get();
1475
+ return row?.data_version ?? 0;
1476
+ }
1477
+ const result = await client.execute("PRAGMA data_version");
1478
+ return result.rows[0]?.[0];
1479
+ }
1480
+ function getKeyInput(renderer) {
1481
+ return renderer.keyInput;
1482
+ }
1483
+
1484
+ // src/components/modals/onboarding.ts
1217
1485
  async function showOnboarding(renderer) {
1218
1486
  return new Promise((resolvePromise) => {
1219
1487
  const container = new BoxRenderable13(renderer, {
@@ -1273,7 +1541,7 @@ async function showOnboarding(renderer) {
1273
1541
  fg: COLORS.text
1274
1542
  });
1275
1543
  labelRow.add(label);
1276
- const input = new InputRenderable4(renderer, {
1544
+ const input = new InputRenderable5(renderer, {
1277
1545
  id: "board-name-input",
1278
1546
  width: 44,
1279
1547
  height: 1,
@@ -1285,7 +1553,7 @@ async function showOnboarding(renderer) {
1285
1553
  cursorColor: COLORS.cursor
1286
1554
  });
1287
1555
  const spacer2 = new BoxRenderable13(renderer, { id: "spacer2", width: "100%", height: 1 });
1288
- const keyEmitter = renderer.keyInput;
1556
+ const keyEmitter = getKeyInput(renderer);
1289
1557
  const doCreate = () => {
1290
1558
  keyEmitter.off("keypress", keyHandler);
1291
1559
  const boardName = input.value.trim() || "Kaban Board";
@@ -1311,7 +1579,7 @@ async function showOnboarding(renderer) {
1311
1579
  container.add(card);
1312
1580
  renderer.root.add(container);
1313
1581
  input.focus();
1314
- input.on(InputRenderableEvents4.ENTER, doCreate);
1582
+ input.on(InputRenderableEvents5.ENTER, doCreate);
1315
1583
  const keyBindings = {
1316
1584
  tab: () => {
1317
1585
  input.blur();
@@ -1340,50 +1608,22 @@ async function showOnboarding(renderer) {
1340
1608
  });
1341
1609
  }
1342
1610
  // src/components/modals/quit.ts
1343
- import { BoxRenderable as BoxRenderable14, TextRenderable as TextRenderable13 } from "@opentui/core";
1344
1611
  function showQuitModal(state) {
1345
- const { renderer } = state;
1346
- const { overlay, dialog } = createModalOverlay(renderer, {
1612
+ showConfirmationModal(state, {
1347
1613
  id: "quit-dialog",
1348
- width: 32,
1349
- height: 8,
1350
- borderColor: COLORS.danger
1351
- });
1352
- const titleRow = new BoxRenderable14(renderer, {
1353
- id: "quit-title-row",
1354
- width: "100%",
1355
- height: 1,
1356
- justifyContent: "center"
1357
- });
1358
- const title = new TextRenderable13(renderer, {
1359
- id: "quit-title",
1360
- content: "Quit Kaban?",
1361
- fg: COLORS.danger
1362
- });
1363
- titleRow.add(title);
1364
- const spacer = new BoxRenderable14(renderer, { id: "quit-spacer", width: "100%", height: 2 });
1365
- const hintRow = new BoxRenderable14(renderer, {
1366
- id: "quit-hint-row",
1367
- width: "100%",
1368
- height: 1,
1369
- justifyContent: "center"
1370
- });
1371
- const hint = new TextRenderable13(renderer, {
1372
- id: "quit-hint",
1373
- content: "[y] Yes [n/Esc] No",
1374
- fg: COLORS.textMuted
1375
- });
1376
- hintRow.add(hint);
1377
- dialog.add(titleRow);
1378
- dialog.add(spacer);
1379
- dialog.add(hintRow);
1380
- renderer.root.add(overlay);
1381
- state.modalOverlay = overlay;
1382
- state.activeModal = "quit";
1614
+ modalType: "quit",
1615
+ title: "Quit Kaban?",
1616
+ titleColor: COLORS.danger,
1617
+ message: "",
1618
+ borderColor: COLORS.danger,
1619
+ width: MODAL_WIDTHS.small,
1620
+ height: MODAL_HEIGHTS.small,
1621
+ confirmHint: "[y] Yes [n/Esc] No"
1622
+ }, async () => {});
1383
1623
  }
1384
1624
  // src/components/modals/view-task.ts
1385
- import { BoxRenderable as BoxRenderable15, TextRenderable as TextRenderable14 } from "@opentui/core";
1386
- var DIALOG_WIDTH2 = 60;
1625
+ import { BoxRenderable as BoxRenderable14, TextRenderable as TextRenderable13 } from "@opentui/core";
1626
+ var DIALOG_WIDTH3 = MODAL_WIDTHS.large;
1387
1627
  var DESC_VISIBLE_LINES = 4;
1388
1628
  var LABEL_WIDTH = 12;
1389
1629
  function formatDate(date) {
@@ -1425,51 +1665,51 @@ async function showViewTaskModal(state, actions, taskIdOverride) {
1425
1665
  const dialogHeight = 24;
1426
1666
  const { overlay, dialog } = createModalOverlay(renderer, {
1427
1667
  id: "view-task-dialog",
1428
- width: DIALOG_WIDTH2,
1668
+ width: DIALOG_WIDTH3,
1429
1669
  height: dialogHeight
1430
1670
  });
1431
1671
  const headerDivider = createSectionDivider(renderer, {
1432
1672
  label: "Task Details",
1433
- width: DIALOG_WIDTH2 - 4,
1673
+ width: DIALOG_WIDTH3 - 4,
1434
1674
  id: "view-header"
1435
1675
  });
1436
- const spacerHeader = new BoxRenderable15(renderer, {
1676
+ const spacerHeader = new BoxRenderable14(renderer, {
1437
1677
  id: "view-spacer-header",
1438
1678
  width: "100%",
1439
1679
  height: 1
1440
1680
  });
1441
- const titleRow = new BoxRenderable15(renderer, {
1681
+ const titleRow = new BoxRenderable14(renderer, {
1442
1682
  id: "view-title-row",
1443
1683
  width: "100%",
1444
1684
  height: 1,
1445
1685
  flexDirection: "row",
1446
1686
  justifyContent: "space-between"
1447
1687
  });
1448
- const taskTitle = new TextRenderable14(renderer, {
1688
+ const taskTitle = new TextRenderable13(renderer, {
1449
1689
  id: "view-task-title",
1450
- content: truncate(task.title, DIALOG_WIDTH2 - 14),
1690
+ content: truncate(task.title, DIALOG_WIDTH3 - 14),
1451
1691
  fg: COLORS.text
1452
1692
  });
1453
- const editHint = new TextRenderable14(renderer, {
1693
+ const editHint = new TextRenderable13(renderer, {
1454
1694
  id: "view-edit-hint",
1455
1695
  content: "[e]dit",
1456
1696
  fg: COLORS.textDim
1457
1697
  });
1458
1698
  titleRow.add(taskTitle);
1459
1699
  titleRow.add(editHint);
1460
- const idRow = new BoxRenderable15(renderer, {
1700
+ const idRow = new BoxRenderable14(renderer, {
1461
1701
  id: "view-id-row",
1462
1702
  width: "100%",
1463
1703
  height: 1,
1464
1704
  flexDirection: "row",
1465
1705
  justifyContent: "space-between"
1466
1706
  });
1467
- const idValue = new TextRenderable14(renderer, {
1707
+ const idValue = new TextRenderable13(renderer, {
1468
1708
  id: "view-id-value",
1469
- content: truncateMiddle(task.id, DIALOG_WIDTH2 - 14),
1709
+ content: truncateMiddle(task.id, DIALOG_WIDTH3 - 14),
1470
1710
  fg: COLORS.textDim
1471
1711
  });
1472
- const copyHint = new TextRenderable14(renderer, {
1712
+ const copyHint = new TextRenderable13(renderer, {
1473
1713
  id: "view-copy-hint",
1474
1714
  content: "[c]opy",
1475
1715
  fg: COLORS.textDim
@@ -1478,26 +1718,26 @@ async function showViewTaskModal(state, actions, taskIdOverride) {
1478
1718
  idRow.add(copyHint);
1479
1719
  const statusDivider = createSectionDivider(renderer, {
1480
1720
  label: "Status",
1481
- width: DIALOG_WIDTH2 - 4,
1721
+ width: DIALOG_WIDTH3 - 4,
1482
1722
  id: "view-status"
1483
1723
  });
1484
- const columnRow = new BoxRenderable15(renderer, {
1724
+ const columnRow = new BoxRenderable14(renderer, {
1485
1725
  id: "view-column-row",
1486
1726
  width: "100%",
1487
1727
  height: 1,
1488
1728
  flexDirection: "row"
1489
1729
  });
1490
- const columnLabel = new TextRenderable14(renderer, {
1730
+ const columnLabel = new TextRenderable13(renderer, {
1491
1731
  id: "view-column-label",
1492
1732
  content: padLabel("Column"),
1493
1733
  fg: COLORS.textMuted
1494
1734
  });
1495
- const columnBullet = new TextRenderable14(renderer, {
1735
+ const columnBullet = new TextRenderable13(renderer, {
1496
1736
  id: "view-column-bullet",
1497
1737
  content: "\u25CF ",
1498
1738
  fg: statusColor
1499
1739
  });
1500
- const columnValue = new TextRenderable14(renderer, {
1740
+ const columnValue = new TextRenderable13(renderer, {
1501
1741
  id: "view-column-value",
1502
1742
  content: columnName,
1503
1743
  fg: COLORS.text
@@ -1505,54 +1745,54 @@ async function showViewTaskModal(state, actions, taskIdOverride) {
1505
1745
  columnRow.add(columnLabel);
1506
1746
  columnRow.add(columnBullet);
1507
1747
  columnRow.add(columnValue);
1508
- const assigneeRow = new BoxRenderable15(renderer, {
1748
+ const assigneeRow = new BoxRenderable14(renderer, {
1509
1749
  id: "view-assignee-row",
1510
1750
  width: "100%",
1511
1751
  height: 1,
1512
1752
  flexDirection: "row"
1513
1753
  });
1514
- const assigneeLabel = new TextRenderable14(renderer, {
1754
+ const assigneeLabel = new TextRenderable13(renderer, {
1515
1755
  id: "view-assignee-label",
1516
1756
  content: padLabel("Assignee"),
1517
1757
  fg: COLORS.textMuted
1518
1758
  });
1519
- const assigneeValue = new TextRenderable14(renderer, {
1759
+ const assigneeValue = new TextRenderable13(renderer, {
1520
1760
  id: "view-assignee-value",
1521
1761
  content: task.assignedTo ?? "\u2014 unassigned",
1522
1762
  fg: task.assignedTo ? COLORS.success : COLORS.textDim
1523
1763
  });
1524
1764
  assigneeRow.add(assigneeLabel);
1525
1765
  assigneeRow.add(assigneeValue);
1526
- const creatorRow = new BoxRenderable15(renderer, {
1766
+ const creatorRow = new BoxRenderable14(renderer, {
1527
1767
  id: "view-creator-row",
1528
1768
  width: "100%",
1529
1769
  height: 1,
1530
1770
  flexDirection: "row"
1531
1771
  });
1532
- const creatorLabel = new TextRenderable14(renderer, {
1772
+ const creatorLabel = new TextRenderable13(renderer, {
1533
1773
  id: "view-creator-label",
1534
1774
  content: padLabel("Creator"),
1535
1775
  fg: COLORS.textMuted
1536
1776
  });
1537
- const creatorValue = new TextRenderable14(renderer, {
1777
+ const creatorValue = new TextRenderable13(renderer, {
1538
1778
  id: "view-creator-value",
1539
1779
  content: task.createdBy,
1540
1780
  fg: COLORS.text
1541
1781
  });
1542
1782
  creatorRow.add(creatorLabel);
1543
1783
  creatorRow.add(creatorValue);
1544
- const labelsRow = new BoxRenderable15(renderer, {
1784
+ const labelsRow = new BoxRenderable14(renderer, {
1545
1785
  id: "view-labels-row",
1546
1786
  width: "100%",
1547
1787
  height: 1,
1548
1788
  flexDirection: "row"
1549
1789
  });
1550
- const labelsLabel = new TextRenderable14(renderer, {
1790
+ const labelsLabel = new TextRenderable13(renderer, {
1551
1791
  id: "view-labels-label",
1552
1792
  content: padLabel("Labels"),
1553
1793
  fg: COLORS.textMuted
1554
1794
  });
1555
- const labelsValue = new TextRenderable14(renderer, {
1795
+ const labelsValue = new TextRenderable13(renderer, {
1556
1796
  id: "view-labels-value",
1557
1797
  content: "\u2014 none",
1558
1798
  fg: COLORS.textDim
@@ -1561,52 +1801,52 @@ async function showViewTaskModal(state, actions, taskIdOverride) {
1561
1801
  labelsRow.add(labelsValue);
1562
1802
  const timelineDivider = createSectionDivider(renderer, {
1563
1803
  label: "Timeline",
1564
- width: DIALOG_WIDTH2 - 4,
1804
+ width: DIALOG_WIDTH3 - 4,
1565
1805
  id: "view-timeline"
1566
1806
  });
1567
- const createdRow = new BoxRenderable15(renderer, {
1807
+ const createdRow = new BoxRenderable14(renderer, {
1568
1808
  id: "view-created-row",
1569
1809
  width: "100%",
1570
1810
  height: 1,
1571
1811
  flexDirection: "row"
1572
1812
  });
1573
- const createdLabel = new TextRenderable14(renderer, {
1813
+ const createdLabel = new TextRenderable13(renderer, {
1574
1814
  id: "view-created-label",
1575
1815
  content: padLabel("Created"),
1576
1816
  fg: COLORS.textMuted
1577
1817
  });
1578
- const createdValue = new TextRenderable14(renderer, {
1818
+ const createdValue = new TextRenderable13(renderer, {
1579
1819
  id: "view-created-value",
1580
1820
  content: formatDate(task.createdAt),
1581
1821
  fg: COLORS.textDim
1582
1822
  });
1583
1823
  createdRow.add(createdLabel);
1584
1824
  createdRow.add(createdValue);
1585
- const updatedRow = new BoxRenderable15(renderer, {
1825
+ const updatedRow = new BoxRenderable14(renderer, {
1586
1826
  id: "view-updated-row",
1587
1827
  width: "100%",
1588
1828
  height: 1,
1589
1829
  flexDirection: "row",
1590
1830
  justifyContent: "space-between"
1591
1831
  });
1592
- const updatedLeft = new BoxRenderable15(renderer, {
1832
+ const updatedLeft = new BoxRenderable14(renderer, {
1593
1833
  id: "view-updated-left",
1594
1834
  height: 1,
1595
1835
  flexDirection: "row"
1596
1836
  });
1597
- const updatedLabel = new TextRenderable14(renderer, {
1837
+ const updatedLabel = new TextRenderable13(renderer, {
1598
1838
  id: "view-updated-label",
1599
1839
  content: padLabel("Updated"),
1600
1840
  fg: COLORS.textMuted
1601
1841
  });
1602
- const updatedValue = new TextRenderable14(renderer, {
1842
+ const updatedValue = new TextRenderable13(renderer, {
1603
1843
  id: "view-updated-value",
1604
1844
  content: formatDate(task.updatedAt),
1605
1845
  fg: COLORS.textDim
1606
1846
  });
1607
1847
  updatedLeft.add(updatedLabel);
1608
1848
  updatedLeft.add(updatedValue);
1609
- const relativeTime = new TextRenderable14(renderer, {
1849
+ const relativeTime = new TextRenderable13(renderer, {
1610
1850
  id: "view-relative-time",
1611
1851
  content: `(${formatRelativeTime(task.updatedAt)})`,
1612
1852
  fg: COLORS.textDim
@@ -1615,10 +1855,10 @@ async function showViewTaskModal(state, actions, taskIdOverride) {
1615
1855
  updatedRow.add(relativeTime);
1616
1856
  const descDivider = createSectionDivider(renderer, {
1617
1857
  label: "Description",
1618
- width: DIALOG_WIDTH2 - 4,
1858
+ width: DIALOG_WIDTH3 - 4,
1619
1859
  id: "view-desc"
1620
1860
  });
1621
- const descContainer = new BoxRenderable15(renderer, {
1861
+ const descContainer = new BoxRenderable14(renderer, {
1622
1862
  id: "view-desc-container",
1623
1863
  width: "100%",
1624
1864
  height: DESC_VISIBLE_LINES,
@@ -1626,7 +1866,7 @@ async function showViewTaskModal(state, actions, taskIdOverride) {
1626
1866
  });
1627
1867
  const descLineRenderables = [];
1628
1868
  for (let i = 0;i < DESC_VISIBLE_LINES; i++) {
1629
- const line = new TextRenderable14(renderer, {
1869
+ const line = new TextRenderable13(renderer, {
1630
1870
  id: `view-desc-line-${i}`,
1631
1871
  content: " ",
1632
1872
  fg: COLORS.text
@@ -1649,64 +1889,64 @@ async function showViewTaskModal(state, actions, taskIdOverride) {
1649
1889
  for (let i = 0;i < DESC_VISIBLE_LINES; i++) {
1650
1890
  const lineContent = visibleLines[i] ?? "";
1651
1891
  const isLastLine = i === DESC_VISIBLE_LINES - 1;
1652
- let displayContent = truncate(lineContent, DIALOG_WIDTH2 - 12);
1892
+ let displayContent = truncate(lineContent, DIALOG_WIDTH3 - 12);
1653
1893
  if (isLastLine && hasMore) {
1654
1894
  const remaining = totalDescLines - scrollOffset - DESC_VISIBLE_LINES;
1655
- displayContent = `${truncate(lineContent, DIALOG_WIDTH2 - 18)} \u25BC ${remaining}+`;
1895
+ displayContent = `${truncate(lineContent, DIALOG_WIDTH3 - 18)} \u25BC ${remaining}+`;
1656
1896
  }
1657
1897
  if (i === 0 && hasLess) {
1658
- displayContent = `\u25B2 ${scrollOffset}+ ${truncate(lineContent, DIALOG_WIDTH2 - 18)}`;
1898
+ displayContent = `\u25B2 ${scrollOffset}+ ${truncate(lineContent, DIALOG_WIDTH3 - 18)}`;
1659
1899
  }
1660
1900
  descLineRenderables[i].content = displayContent || " ";
1661
1901
  descLineRenderables[i].fg = i === 0 && hasLess || isLastLine && hasMore ? COLORS.textDim : COLORS.text;
1662
1902
  }
1663
1903
  }
1664
1904
  updateDescriptionContent(0);
1665
- const footerDivider = new BoxRenderable15(renderer, {
1905
+ const footerDivider = new BoxRenderable14(renderer, {
1666
1906
  id: "view-footer-divider",
1667
1907
  width: "100%",
1668
1908
  height: 1
1669
1909
  });
1670
- const footerLine = new TextRenderable14(renderer, {
1910
+ const footerLine = new TextRenderable13(renderer, {
1671
1911
  id: "view-footer-line",
1672
- content: "\u2500".repeat(DIALOG_WIDTH2 - 4),
1912
+ content: "\u2500".repeat(DIALOG_WIDTH3 - 4),
1673
1913
  fg: COLORS.border
1674
1914
  });
1675
1915
  footerDivider.add(footerLine);
1676
- const actionsRow = new BoxRenderable15(renderer, {
1916
+ const actionsRow = new BoxRenderable14(renderer, {
1677
1917
  id: "view-actions-row",
1678
1918
  width: "100%",
1679
1919
  height: 1,
1680
1920
  flexDirection: "row",
1681
1921
  justifyContent: "space-between"
1682
1922
  });
1683
- const actionsLeft = new BoxRenderable15(renderer, {
1923
+ const actionsLeft = new BoxRenderable14(renderer, {
1684
1924
  id: "view-actions-left",
1685
1925
  height: 1,
1686
1926
  flexDirection: "row",
1687
1927
  gap: 2
1688
1928
  });
1689
- const moveAction = new TextRenderable14(renderer, {
1929
+ const moveAction = new TextRenderable13(renderer, {
1690
1930
  id: "view-action-move",
1691
1931
  content: "[m] Move",
1692
1932
  fg: COLORS.textMuted
1693
1933
  });
1694
- const assignAction = new TextRenderable14(renderer, {
1934
+ const assignAction = new TextRenderable13(renderer, {
1695
1935
  id: "view-action-assign",
1696
1936
  content: "[u] Assign",
1697
1937
  fg: COLORS.textMuted
1698
1938
  });
1699
- const editAction = new TextRenderable14(renderer, {
1939
+ const editAction = new TextRenderable13(renderer, {
1700
1940
  id: "view-action-edit",
1701
1941
  content: "[e] Edit",
1702
1942
  fg: COLORS.textMuted
1703
1943
  });
1704
- const deleteAction = new TextRenderable14(renderer, {
1944
+ const deleteAction = new TextRenderable13(renderer, {
1705
1945
  id: "view-action-delete",
1706
1946
  content: "[d] Delete",
1707
1947
  fg: COLORS.danger
1708
1948
  });
1709
- const archiveRestoreAction = new TextRenderable14(renderer, {
1949
+ const archiveRestoreAction = new TextRenderable13(renderer, {
1710
1950
  id: "view-action-archive-restore",
1711
1951
  content: task.archived ? "[r] Restore" : "[x] Archive",
1712
1952
  fg: task.archived ? COLORS.success : COLORS.warning
@@ -1716,7 +1956,7 @@ async function showViewTaskModal(state, actions, taskIdOverride) {
1716
1956
  actionsLeft.add(editAction);
1717
1957
  actionsLeft.add(archiveRestoreAction);
1718
1958
  actionsLeft.add(deleteAction);
1719
- const escAction = new TextRenderable14(renderer, {
1959
+ const escAction = new TextRenderable13(renderer, {
1720
1960
  id: "view-action-esc",
1721
1961
  content: "[Esc]",
1722
1962
  fg: COLORS.textDim
@@ -1803,7 +2043,7 @@ async function copyTaskId(state) {
1803
2043
  copyHint.fg = COLORS.danger;
1804
2044
  state.viewTaskRuntime.copyTimeoutId = setTimeout(() => {
1805
2045
  if (state.viewTaskRuntime) {
1806
- idValue.content = truncateMiddle(taskId, DIALOG_WIDTH2 - 14);
2046
+ idValue.content = truncateMiddle(taskId, DIALOG_WIDTH3 - 14);
1807
2047
  idValue.fg = COLORS.textDim;
1808
2048
  copyHint.content = "[c]opy";
1809
2049
  copyHint.fg = COLORS.textDim;
@@ -1981,9 +2221,43 @@ var modalBindings = {
1981
2221
  return openArchiveModal(state);
1982
2222
  }
1983
2223
  },
2224
+ C: async (state) => {
2225
+ if (state.archiveViewMode)
2226
+ return;
2227
+ const taskId = getSelectedTaskId(state);
2228
+ if (!taskId)
2229
+ return;
2230
+ const terminal = await state.boardService.getTerminalColumn();
2231
+ if (!terminal)
2232
+ return;
2233
+ const result = await withErrorHandling(state, () => state.taskService.moveTask(taskId, terminal.id), "Failed to complete task");
2234
+ if (result) {
2235
+ await refreshBoard(state);
2236
+ }
2237
+ },
2238
+ "/": (state) => {
2239
+ if (state.archiveViewMode) {
2240
+ return showSearchArchiveModal(state, async (_tasks) => {
2241
+ await refreshBoard(state);
2242
+ });
2243
+ }
2244
+ },
2245
+ P: (state) => {
2246
+ if (state.archiveViewMode) {
2247
+ return showPurgeArchiveModal(state, async () => {
2248
+ await refreshBoard(state);
2249
+ });
2250
+ }
2251
+ },
1984
2252
  tab: toggleArchiveView,
1985
2253
  return: openViewModal,
1986
- "?": showHelpModal
2254
+ "?": showHelpModal,
2255
+ H: (state) => {
2256
+ const taskId = getSelectedTaskId(state);
2257
+ if (taskId) {
2258
+ return showTaskHistoryModal(state);
2259
+ }
2260
+ }
1987
2261
  },
1988
2262
  addTask: {
1989
2263
  escape: closeModal,
@@ -2061,6 +2335,7 @@ var modalBindings = {
2061
2335
  editTask: {
2062
2336
  escape: cancelEditTask,
2063
2337
  tab: focusNextEditField,
2338
+ "shift+tab": focusPrevEditField,
2064
2339
  left: buttonSelectPrev,
2065
2340
  right: buttonSelectNext,
2066
2341
  return: editTaskSave
@@ -2072,11 +2347,24 @@ var modalBindings = {
2072
2347
  y: quit,
2073
2348
  n: closeModal,
2074
2349
  escape: closeModal
2350
+ },
2351
+ searchArchive: {
2352
+ escape: closeModal
2353
+ },
2354
+ purgeArchive: {
2355
+ y: confirmModal,
2356
+ n: closeModal,
2357
+ escape: closeModal
2358
+ },
2359
+ taskHistory: {
2360
+ escape: closeModal,
2361
+ [WILDCARD]: closeModal
2075
2362
  }
2076
2363
  };
2077
2364
  function handleKeypress(state, key) {
2078
2365
  const bindings = modalBindings[state.activeModal];
2079
- const handler = bindings[key.name] ?? bindings[WILDCARD];
2366
+ const shiftKey = key.shift ? `shift+${key.name}` : undefined;
2367
+ const handler = (shiftKey ? bindings[shiftKey] : undefined) ?? bindings[key.name] ?? bindings[WILDCARD];
2080
2368
  return handler?.(state);
2081
2369
  }
2082
2370
 
@@ -2153,9 +2441,11 @@ async function main() {
2153
2441
  }
2154
2442
  const state = {
2155
2443
  renderer,
2444
+ db,
2156
2445
  taskService,
2157
2446
  boardService,
2158
2447
  boardName: board.name,
2448
+ projectRoot,
2159
2449
  columns: [],
2160
2450
  columnPanels: [],
2161
2451
  taskSelects: new Map,
@@ -2175,13 +2465,12 @@ async function main() {
2175
2465
  };
2176
2466
  await refreshBoard(state);
2177
2467
  let lastDataVersion = null;
2178
- const client = db.$client;
2468
+ const client = getDbClient(db);
2179
2469
  const checkForChanges = async () => {
2180
2470
  if (state.activeModal !== "none")
2181
2471
  return;
2182
2472
  try {
2183
- const result = await client.execute("PRAGMA data_version");
2184
- const currentVersion = result.rows[0]?.[0];
2473
+ const currentVersion = await getDataVersion(client);
2185
2474
  if (lastDataVersion !== null && currentVersion !== lastDataVersion) {
2186
2475
  await refreshBoard(state);
2187
2476
  }
@@ -2193,8 +2482,7 @@ async function main() {
2193
2482
  process.on("exit", cleanup);
2194
2483
  process.on("SIGINT", cleanup);
2195
2484
  process.on("SIGTERM", cleanup);
2196
- const keyEmitter = renderer.keyInput;
2197
- keyEmitter.on("keypress", (key) => {
2485
+ getKeyInput(renderer).on("keypress", (key) => {
2198
2486
  handleKeypress(state, key);
2199
2487
  });
2200
2488
  }