@kaban-board/tui 0.3.0 → 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 -326
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1,21 +1,5 @@
1
1
  #!/usr/bin/env bun
2
2
  // @bun
3
- var __create = Object.create;
4
- var __getProtoOf = Object.getPrototypeOf;
5
- var __defProp = Object.defineProperty;
6
- var __getOwnPropNames = Object.getOwnPropertyNames;
7
- var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __toESM = (mod, isNodeMode, target) => {
9
- target = mod != null ? __create(__getProtoOf(mod)) : {};
10
- const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
11
- for (let key of __getOwnPropNames(mod))
12
- if (!__hasOwnProp.call(to, key))
13
- __defProp(to, key, {
14
- get: () => mod[key],
15
- enumerable: true
16
- });
17
- return to;
18
- };
19
3
  var __require = import.meta.require;
20
4
 
21
5
  // src/index.ts
@@ -140,9 +124,10 @@ async function refreshBoard(state) {
140
124
  alignItems: "center"
141
125
  });
142
126
  const modeIndicator = state.archiveViewMode ? " [ARCHIVE]" : "";
127
+ const projectPath = ` (${state.projectRoot})`;
143
128
  const headerText = new TextRenderable2(renderer, {
144
129
  id: "header-text",
145
- 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)}`
146
131
  });
147
132
  header.add(headerText);
148
133
  mainContainer.add(header);
@@ -323,6 +308,44 @@ function createButtonRow(renderer, id, buttons) {
323
308
  return state;
324
309
  }
325
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
+
326
349
  // src/components/overlay.ts
327
350
  import { BoxRenderable as BoxRenderable4 } from "@opentui/core";
328
351
  function createModalOverlay(renderer, options) {
@@ -401,8 +424,8 @@ function showAddTaskModal(state, onTaskCreated) {
401
424
  return;
402
425
  const { overlay, dialog } = createModalOverlay(renderer, {
403
426
  id: "add-task-dialog",
404
- width: 52,
405
- height: 11
427
+ width: MODAL_WIDTHS.medium,
428
+ height: MODAL_HEIGHTS.medium
406
429
  });
407
430
  const titleRow = new BoxRenderable5(renderer, {
408
431
  id: "title-row",
@@ -441,11 +464,15 @@ function showAddTaskModal(state, onTaskCreated) {
441
464
  const spacer2 = new BoxRenderable5(renderer, { id: "dialog-spacer2", width: "100%", height: 1 });
442
465
  const doCreate = async () => {
443
466
  const taskTitle = input.value.trim();
444
- if (taskTitle) {
445
- await state.taskService.addTask({ title: taskTitle, columnId: column.id });
467
+ if (!taskTitle) {
468
+ closeModal(state);
469
+ return;
446
470
  }
471
+ const result = await withErrorHandling(state, () => state.taskService.addTask({ title: taskTitle, columnId: column.id }), "Failed to create task");
447
472
  closeModal(state);
448
- await onTaskCreated();
473
+ if (result) {
474
+ await onTaskCreated();
475
+ }
449
476
  };
450
477
  const doCancel = () => {
451
478
  closeModal(state);
@@ -471,9 +498,6 @@ function showAddTaskModal(state, onTaskCreated) {
471
498
  state.activeModal = "addTask";
472
499
  input.on(InputRenderableEvents.ENTER, doCreate);
473
500
  }
474
- // src/components/modals/archive-task.ts
475
- import { BoxRenderable as BoxRenderable6, TextRenderable as TextRenderable5 } from "@opentui/core";
476
-
477
501
  // src/lib/types.ts
478
502
  function getSelectedTaskId(state) {
479
503
  const column = state.columns[state.currentColumnIndex];
@@ -487,9 +511,97 @@ function getSelectedTaskId(state) {
487
511
  return typeof value === "string" ? value : null;
488
512
  }
489
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
+
490
603
  // src/components/modals/archive-task.ts
491
604
  async function showArchiveTaskModal(state, onArchived) {
492
- const { renderer } = state;
493
605
  const taskId = getSelectedTaskId(state);
494
606
  if (!taskId) {
495
607
  return;
@@ -502,81 +614,160 @@ async function showArchiveTaskModal(state, onArchived) {
502
614
  return;
503
615
  }
504
616
  state.selectedTask = task;
505
- const { overlay, dialog } = createModalOverlay(renderer, {
617
+ showConfirmationModal(state, {
506
618
  id: "archive-task-dialog",
507
- width: 45,
508
- height: 10,
509
- 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
+ }
510
632
  });
511
- const titleRow = new BoxRenderable6(renderer, {
512
- 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",
513
687
  width: "100%",
514
688
  height: 1,
515
- justifyContent: "center"
689
+ flexDirection: "row"
516
690
  });
517
- const title = new TextRenderable5(renderer, {
518
- id: "archive-title",
519
- content: "Archive Task?",
520
- 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
521
695
  });
522
- titleRow.add(title);
523
- const spacer1 = new BoxRenderable6(renderer, { id: "archive-spacer1", width: "100%", height: 1 });
524
- const taskRow = new BoxRenderable6(renderer, {
525
- id: "archive-task-row",
696
+ titleRow.add(taskTitle);
697
+ const spacer = new BoxRenderable7(renderer, {
698
+ id: "history-spacer",
526
699
  width: "100%",
527
700
  height: 1
528
701
  });
529
- const taskText = new TextRenderable5(renderer, {
530
- id: "archive-task-text",
531
- content: task.title.slice(0, 40),
532
- fg: COLORS.text
533
- });
534
- taskRow.add(taskText);
535
- const infoRow = new BoxRenderable6(renderer, {
536
- 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",
537
744
  width: "100%",
538
745
  height: 1
539
746
  });
540
- const info = new TextRenderable5(renderer, {
541
- id: "archive-info",
542
- content: "Task will be moved to archive.",
543
- fg: COLORS.textMuted
544
- });
545
- infoRow.add(info);
546
- const spacer2 = new BoxRenderable6(renderer, { id: "archive-spacer2", width: "100%", height: 2 });
547
- const hintRow = new BoxRenderable6(renderer, {
548
- id: "archive-hint-row",
747
+ const hintRow = new BoxRenderable7(renderer, {
748
+ id: "history-hint-row",
549
749
  width: "100%",
550
750
  height: 1,
551
751
  justifyContent: "center"
552
752
  });
553
- const hint = new TextRenderable5(renderer, {
554
- id: "archive-hint",
555
- content: "[y] Archive [n/Esc] Cancel",
556
- fg: COLORS.textMuted
753
+ const hintText = new TextRenderable6(renderer, {
754
+ id: "history-hint-text",
755
+ content: "[Esc] close",
756
+ fg: COLORS.textDim
557
757
  });
558
- hintRow.add(hint);
559
- dialog.add(titleRow);
560
- dialog.add(spacer1);
561
- dialog.add(taskRow);
562
- dialog.add(infoRow);
563
- dialog.add(spacer2);
758
+ hintRow.add(hintText);
759
+ dialog.add(footerSpacer);
564
760
  dialog.add(hintRow);
565
761
  renderer.root.add(overlay);
566
762
  state.modalOverlay = overlay;
567
- state.activeModal = "archiveTask";
568
- state.onModalConfirm = async () => {
569
- await state.taskService.archiveTasks({ taskIds: [taskId] });
570
- closeModal(state);
571
- await onArchived();
572
- };
763
+ state.activeModal = "taskHistory";
573
764
  }
574
765
  // src/components/modals/assign-task.ts
575
766
  import {
576
- BoxRenderable as BoxRenderable7,
767
+ BoxRenderable as BoxRenderable8,
577
768
  InputRenderable as InputRenderable2,
578
769
  InputRenderableEvents as InputRenderableEvents2,
579
- TextRenderable as TextRenderable6
770
+ TextRenderable as TextRenderable7
580
771
  } from "@opentui/core";
581
772
  async function showAssignTaskModal(state, onAssigned) {
582
773
  const { renderer } = state;
@@ -590,39 +781,39 @@ async function showAssignTaskModal(state, onAssigned) {
590
781
  }
591
782
  const { overlay, dialog } = createModalOverlay(renderer, {
592
783
  id: "assign-task-dialog",
593
- width: 45,
784
+ width: MODAL_WIDTHS.confirmation,
594
785
  height: 12
595
786
  });
596
- const titleRow = new BoxRenderable7(renderer, {
787
+ const titleRow = new BoxRenderable8(renderer, {
597
788
  id: "assign-title-row",
598
789
  width: "100%",
599
790
  height: 1
600
791
  });
601
- const title = new TextRenderable6(renderer, {
792
+ const title = new TextRenderable7(renderer, {
602
793
  id: "assign-title",
603
794
  content: "Assign Task",
604
795
  fg: COLORS.accent
605
796
  });
606
797
  titleRow.add(title);
607
- const taskRow = new BoxRenderable7(renderer, {
798
+ const taskRow = new BoxRenderable8(renderer, {
608
799
  id: "assign-task-row",
609
800
  width: "100%",
610
801
  height: 1
611
802
  });
612
- const taskText = new TextRenderable6(renderer, {
803
+ const taskText = new TextRenderable7(renderer, {
613
804
  id: "assign-task-text",
614
- content: task.title.slice(0, 40),
805
+ content: task.title.slice(0, TRUNCATION.taskTitle),
615
806
  fg: COLORS.textMuted
616
807
  });
617
808
  taskRow.add(taskText);
618
- const spacer1 = new BoxRenderable7(renderer, { id: "assign-spacer1", width: "100%", height: 1 });
619
- const labelRow = new BoxRenderable7(renderer, {
809
+ const spacer1 = new BoxRenderable8(renderer, { id: "assign-spacer1", width: "100%", height: 1 });
810
+ const labelRow = new BoxRenderable8(renderer, {
620
811
  id: "assign-label-row",
621
812
  width: "100%",
622
813
  height: 1
623
814
  });
624
815
  const currentAssignee = task.assignedTo ?? "(unassigned)";
625
- const label = new TextRenderable6(renderer, {
816
+ const label = new TextRenderable7(renderer, {
626
817
  id: "assign-label",
627
818
  content: `Current: ${currentAssignee}`,
628
819
  fg: COLORS.text
@@ -639,14 +830,14 @@ async function showAssignTaskModal(state, onAssigned) {
639
830
  focusedBackgroundColor: COLORS.inputBg,
640
831
  cursorColor: COLORS.cursor
641
832
  });
642
- 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 });
643
834
  const doAssign = async () => {
644
835
  const assignee = input.value.trim();
645
- await state.taskService.updateTask(taskId, {
646
- assignedTo: assignee || null
647
- });
836
+ const result = await withErrorHandling(state, () => state.taskService.updateTask(taskId, { assignedTo: assignee || null }), "Failed to assign task");
648
837
  closeModal(state);
649
- await onAssigned();
838
+ if (result) {
839
+ await onAssigned();
840
+ }
650
841
  };
651
842
  const doCancel = () => {
652
843
  closeModal(state);
@@ -674,9 +865,7 @@ async function showAssignTaskModal(state, onAssigned) {
674
865
  input.on(InputRenderableEvents2.ENTER, doAssign);
675
866
  }
676
867
  // src/components/modals/delete-task.ts
677
- import { BoxRenderable as BoxRenderable8, TextRenderable as TextRenderable7 } from "@opentui/core";
678
868
  async function showDeleteTaskModal(state, onDeleted) {
679
- const { renderer } = state;
680
869
  const taskId = getSelectedTaskId(state);
681
870
  if (!taskId) {
682
871
  return;
@@ -686,168 +875,166 @@ async function showDeleteTaskModal(state, onDeleted) {
686
875
  return;
687
876
  }
688
877
  state.selectedTask = task;
689
- const { overlay, dialog } = createModalOverlay(renderer, {
878
+ showConfirmationModal(state, {
690
879
  id: "delete-task-dialog",
691
- width: 45,
692
- height: 10,
693
- borderColor: COLORS.danger
694
- });
695
- const titleRow = new BoxRenderable8(renderer, {
696
- id: "delete-title-row",
697
- width: "100%",
698
- height: 1,
699
- justifyContent: "center"
700
- });
701
- const title = new TextRenderable7(renderer, {
702
- id: "delete-title",
703
- content: "Delete Task?",
704
- fg: COLORS.danger
705
- });
706
- titleRow.add(title);
707
- const spacer1 = new BoxRenderable8(renderer, { id: "delete-spacer1", width: "100%", height: 1 });
708
- const taskRow = new BoxRenderable8(renderer, {
709
- id: "delete-task-row",
710
- width: "100%",
711
- height: 1
712
- });
713
- const taskText = new TextRenderable7(renderer, {
714
- id: "delete-task-text",
715
- content: task.title.slice(0, 40),
716
- fg: COLORS.text
717
- });
718
- taskRow.add(taskText);
719
- const warningRow = new BoxRenderable8(renderer, {
720
- id: "delete-warning-row",
721
- width: "100%",
722
- height: 1
723
- });
724
- const warning = new TextRenderable7(renderer, {
725
- id: "delete-warning",
726
- content: "This action cannot be undone.",
727
- fg: COLORS.warning
728
- });
729
- warningRow.add(warning);
730
- const spacer2 = new BoxRenderable8(renderer, { id: "delete-spacer2", width: "100%", height: 2 });
731
- const hintRow = new BoxRenderable8(renderer, {
732
- id: "delete-hint-row",
733
- width: "100%",
734
- height: 1,
735
- 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
+ }
736
892
  });
737
- const hint = new TextRenderable7(renderer, {
738
- id: "delete-hint",
739
- content: "[y] Delete [n/Esc] Cancel",
740
- 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
+ }
741
918
  });
742
- hintRow.add(hint);
743
- dialog.add(titleRow);
744
- dialog.add(spacer1);
745
- dialog.add(taskRow);
746
- dialog.add(warningRow);
747
- dialog.add(spacer2);
748
- dialog.add(hintRow);
749
- renderer.root.add(overlay);
750
- state.modalOverlay = overlay;
751
- state.activeModal = "deleteTask";
752
- state.onModalConfirm = async () => {
753
- await state.taskService.deleteTask(taskId);
754
- closeModal(state);
755
- await onDeleted();
756
- };
757
919
  }
758
920
  // src/components/modals/restore-task.ts
759
- import { BoxRenderable as BoxRenderable9, TextRenderable as TextRenderable8 } from "@opentui/core";
760
921
  async function showRestoreTaskModal(state, onRestored) {
761
- const { renderer } = state;
762
922
  const taskId = getSelectedTaskId(state);
763
923
  if (!taskId) {
764
924
  return;
765
925
  }
766
926
  const task = await state.taskService.getTask(taskId);
767
- if (!task) {
768
- return;
769
- }
770
- if (!task.archived) {
927
+ if (!task || !task.archived) {
771
928
  return;
772
929
  }
773
930
  state.selectedTask = task;
774
- const { overlay, dialog } = createModalOverlay(renderer, {
931
+ showConfirmationModal(state, {
775
932
  id: "restore-task-dialog",
776
- width: 45,
777
- height: 10,
778
- 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
779
961
  });
780
962
  const titleRow = new BoxRenderable9(renderer, {
781
- id: "restore-title-row",
963
+ id: "search-title-row",
782
964
  width: "100%",
783
- height: 1,
784
- justifyContent: "center"
965
+ height: 1
785
966
  });
786
967
  const title = new TextRenderable8(renderer, {
787
- id: "restore-title",
788
- content: "Restore Task?",
789
- fg: COLORS.success
968
+ id: "search-title",
969
+ content: " Search Archive ",
970
+ fg: COLORS.accent
790
971
  });
791
972
  titleRow.add(title);
792
- const spacer1 = new BoxRenderable9(renderer, { id: "restore-spacer1", width: "100%", height: 1 });
793
- const taskRow = new BoxRenderable9(renderer, {
794
- 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",
795
976
  width: "100%",
796
977
  height: 1
797
978
  });
798
- const taskText = new TextRenderable8(renderer, {
799
- id: "restore-task-text",
800
- content: task.title.slice(0, 40),
979
+ const label = new TextRenderable8(renderer, {
980
+ id: "search-label",
981
+ content: "Search query:",
801
982
  fg: COLORS.text
802
983
  });
803
- taskRow.add(taskText);
804
- const infoRow = new BoxRenderable9(renderer, {
805
- id: "restore-info-row",
806
- width: "100%",
807
- height: 1
808
- });
809
- const info = new TextRenderable8(renderer, {
810
- id: "restore-info",
811
- content: "Task will be restored to its original column.",
812
- 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
813
995
  });
814
- infoRow.add(info);
815
- 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 });
816
997
  const hintRow = new BoxRenderable9(renderer, {
817
- id: "restore-hint-row",
998
+ id: "search-hint-row",
818
999
  width: "100%",
819
- height: 1,
820
- justifyContent: "center"
1000
+ height: 1
821
1001
  });
822
1002
  const hint = new TextRenderable8(renderer, {
823
- id: "restore-hint",
824
- content: "[y] Restore [n/Esc] Cancel",
1003
+ id: "search-hint",
1004
+ content: "[Enter] Search [Esc] Cancel",
825
1005
  fg: COLORS.textMuted
826
1006
  });
827
1007
  hintRow.add(hint);
828
1008
  dialog.add(titleRow);
829
1009
  dialog.add(spacer1);
830
- dialog.add(taskRow);
831
- dialog.add(infoRow);
1010
+ dialog.add(labelRow);
1011
+ dialog.add(input);
832
1012
  dialog.add(spacer2);
833
1013
  dialog.add(hintRow);
834
1014
  renderer.root.add(overlay);
835
1015
  state.modalOverlay = overlay;
836
- state.activeModal = "restoreTask";
837
- state.onModalConfirm = async () => {
838
- 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");
839
1024
  closeModal(state);
840
- await onRestored();
841
- };
1025
+ if (result) {
1026
+ await onResults(result.tasks);
1027
+ }
1028
+ });
842
1029
  }
843
1030
  // src/components/modals/edit-task.ts
844
1031
  import {
845
1032
  BoxRenderable as BoxRenderable10,
846
- InputRenderable as InputRenderable3,
847
- InputRenderableEvents as InputRenderableEvents3,
1033
+ InputRenderable as InputRenderable4,
1034
+ InputRenderableEvents as InputRenderableEvents4,
848
1035
  TextRenderable as TextRenderable9
849
1036
  } from "@opentui/core";
850
- var DIALOG_WIDTH = 60;
1037
+ var DIALOG_WIDTH2 = MODAL_WIDTHS.large;
851
1038
  var DESC_INPUT_HEIGHT = 6;
852
1039
  async function showEditTaskModal(state, callbacks) {
853
1040
  const { renderer } = state;
@@ -863,7 +1050,7 @@ async function showEditTaskModal(state, callbacks) {
863
1050
  const dialogHeight = 18;
864
1051
  const { overlay, dialog } = createModalOverlay(renderer, {
865
1052
  id: "edit-task-dialog",
866
- width: DIALOG_WIDTH,
1053
+ width: DIALOG_WIDTH2,
867
1054
  height: dialogHeight
868
1055
  });
869
1056
  const headerRow = new BoxRenderable10(renderer, {
@@ -873,7 +1060,7 @@ async function showEditTaskModal(state, callbacks) {
873
1060
  });
874
1061
  const headerText = new TextRenderable9(renderer, {
875
1062
  id: "edit-header-text",
876
- content: `Edit Task: ${truncate(task.title, DIALOG_WIDTH - 20)}`,
1063
+ content: `Edit Task: ${truncate(task.title, DIALOG_WIDTH2 - 20)}`,
877
1064
  fg: COLORS.accent
878
1065
  });
879
1066
  headerRow.add(headerText);
@@ -893,9 +1080,9 @@ async function showEditTaskModal(state, callbacks) {
893
1080
  fg: COLORS.textMuted
894
1081
  });
895
1082
  titleLabelRow.add(titleLabel);
896
- const titleInput = new InputRenderable3(renderer, {
1083
+ const titleInput = new InputRenderable4(renderer, {
897
1084
  id: "edit-title-input",
898
- width: DIALOG_WIDTH - 6,
1085
+ width: DIALOG_WIDTH2 - 6,
899
1086
  height: 1,
900
1087
  placeholder: "Task title...",
901
1088
  textColor: COLORS.text,
@@ -921,9 +1108,9 @@ async function showEditTaskModal(state, callbacks) {
921
1108
  fg: COLORS.textMuted
922
1109
  });
923
1110
  descLabelRow.add(descLabel);
924
- const descInput = new InputRenderable3(renderer, {
1111
+ const descInput = new InputRenderable4(renderer, {
925
1112
  id: "edit-desc-input",
926
- width: DIALOG_WIDTH - 6,
1113
+ width: DIALOG_WIDTH2 - 6,
927
1114
  height: DESC_INPUT_HEIGHT,
928
1115
  placeholder: "Task description (optional)...",
929
1116
  textColor: COLORS.text,
@@ -946,10 +1133,13 @@ async function showEditTaskModal(state, callbacks) {
946
1133
  }
947
1134
  const hasChanges = newTitle !== task.title || newDescription !== (task.description ?? "");
948
1135
  if (hasChanges) {
949
- await state.taskService.updateTask(task.id, {
1136
+ const result = await withErrorHandling(state, () => state.taskService.updateTask(task.id, {
950
1137
  title: newTitle,
951
1138
  description: newDescription || undefined
952
- });
1139
+ }), "Failed to save task");
1140
+ if (!result) {
1141
+ return;
1142
+ }
953
1143
  }
954
1144
  closeModal(state);
955
1145
  state.editTaskState = null;
@@ -995,7 +1185,7 @@ async function showEditTaskModal(state, callbacks) {
995
1185
  state.taskInput = titleInput;
996
1186
  state.buttonRow = buttonRow;
997
1187
  state.activeModal = "editTask";
998
- titleInput.on(InputRenderableEvents3.ENTER, doSave);
1188
+ titleInput.on(InputRenderableEvents4.ENTER, doSave);
999
1189
  state.editTaskRuntime = {
1000
1190
  titleInput,
1001
1191
  descInput,
@@ -1028,6 +1218,30 @@ function focusNextEditField(state) {
1028
1218
  break;
1029
1219
  }
1030
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
+ }
1031
1245
  function cancelEditTask(state) {
1032
1246
  if (!state.editTaskRuntime)
1033
1247
  return;
@@ -1043,8 +1257,12 @@ var SHORTCUTS = [
1043
1257
  ["m", "Move task (change status)"],
1044
1258
  ["u", "Assign user to task"],
1045
1259
  ["d", "Delete task"],
1260
+ ["C", "Complete task (move to done)"],
1261
+ ["H", "View task history"],
1046
1262
  ["x", "Archive task"],
1047
1263
  ["r", "Restore task (archive view)"],
1264
+ ["/", "Search archive (archive view)"],
1265
+ ["P", "Purge archive (archive view)"],
1048
1266
  ["Tab", "Toggle archive view"],
1049
1267
  ["?", "Show/hide help"],
1050
1268
  ["q", "Quit"]
@@ -1053,8 +1271,8 @@ function showHelpModal(state) {
1053
1271
  const { renderer } = state;
1054
1272
  const { overlay, dialog } = createModalOverlay(renderer, {
1055
1273
  id: "help-dialog",
1056
- width: 45,
1057
- height: 20,
1274
+ width: MODAL_WIDTHS.confirmation,
1275
+ height: MODAL_HEIGHTS.large,
1058
1276
  padding: 2
1059
1277
  });
1060
1278
  const titleRow = new BoxRenderable11(renderer, {
@@ -1160,7 +1378,7 @@ async function showMoveTaskModal(state, onMoved) {
1160
1378
  });
1161
1379
  const taskText = new TextRenderable11(renderer, {
1162
1380
  id: "move-task-text",
1163
- content: task.title.slice(0, 35),
1381
+ content: task.title.slice(0, TRUNCATION.taskTitleShort),
1164
1382
  fg: COLORS.textMuted
1165
1383
  });
1166
1384
  taskRow.add(taskText);
@@ -1216,20 +1434,54 @@ async function showMoveTaskModal(state, onMoved) {
1216
1434
  state.activeModal = "moveTask";
1217
1435
  columnSelect.on(SelectRenderableEvents.ITEM_SELECTED, async () => {
1218
1436
  const selected = columnSelect.getSelectedOption();
1219
- if (selected?.value) {
1220
- 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);
1221
1446
  }
1222
- closeModal(state);
1223
- await onMoved();
1224
1447
  });
1225
1448
  }
1226
1449
  // src/components/modals/onboarding.ts
1227
1450
  import {
1228
1451
  BoxRenderable as BoxRenderable13,
1229
- InputRenderable as InputRenderable4,
1230
- InputRenderableEvents as InputRenderableEvents4,
1452
+ InputRenderable as InputRenderable5,
1453
+ InputRenderableEvents as InputRenderableEvents5,
1231
1454
  TextRenderable as TextRenderable12
1232
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
1233
1485
  async function showOnboarding(renderer) {
1234
1486
  return new Promise((resolvePromise) => {
1235
1487
  const container = new BoxRenderable13(renderer, {
@@ -1289,7 +1541,7 @@ async function showOnboarding(renderer) {
1289
1541
  fg: COLORS.text
1290
1542
  });
1291
1543
  labelRow.add(label);
1292
- const input = new InputRenderable4(renderer, {
1544
+ const input = new InputRenderable5(renderer, {
1293
1545
  id: "board-name-input",
1294
1546
  width: 44,
1295
1547
  height: 1,
@@ -1301,7 +1553,7 @@ async function showOnboarding(renderer) {
1301
1553
  cursorColor: COLORS.cursor
1302
1554
  });
1303
1555
  const spacer2 = new BoxRenderable13(renderer, { id: "spacer2", width: "100%", height: 1 });
1304
- const keyEmitter = renderer.keyInput;
1556
+ const keyEmitter = getKeyInput(renderer);
1305
1557
  const doCreate = () => {
1306
1558
  keyEmitter.off("keypress", keyHandler);
1307
1559
  const boardName = input.value.trim() || "Kaban Board";
@@ -1327,7 +1579,7 @@ async function showOnboarding(renderer) {
1327
1579
  container.add(card);
1328
1580
  renderer.root.add(container);
1329
1581
  input.focus();
1330
- input.on(InputRenderableEvents4.ENTER, doCreate);
1582
+ input.on(InputRenderableEvents5.ENTER, doCreate);
1331
1583
  const keyBindings = {
1332
1584
  tab: () => {
1333
1585
  input.blur();
@@ -1356,50 +1608,22 @@ async function showOnboarding(renderer) {
1356
1608
  });
1357
1609
  }
1358
1610
  // src/components/modals/quit.ts
1359
- import { BoxRenderable as BoxRenderable14, TextRenderable as TextRenderable13 } from "@opentui/core";
1360
1611
  function showQuitModal(state) {
1361
- const { renderer } = state;
1362
- const { overlay, dialog } = createModalOverlay(renderer, {
1612
+ showConfirmationModal(state, {
1363
1613
  id: "quit-dialog",
1364
- width: 32,
1365
- height: 8,
1366
- borderColor: COLORS.danger
1367
- });
1368
- const titleRow = new BoxRenderable14(renderer, {
1369
- id: "quit-title-row",
1370
- width: "100%",
1371
- height: 1,
1372
- justifyContent: "center"
1373
- });
1374
- const title = new TextRenderable13(renderer, {
1375
- id: "quit-title",
1376
- content: "Quit Kaban?",
1377
- fg: COLORS.danger
1378
- });
1379
- titleRow.add(title);
1380
- const spacer = new BoxRenderable14(renderer, { id: "quit-spacer", width: "100%", height: 2 });
1381
- const hintRow = new BoxRenderable14(renderer, {
1382
- id: "quit-hint-row",
1383
- width: "100%",
1384
- height: 1,
1385
- justifyContent: "center"
1386
- });
1387
- const hint = new TextRenderable13(renderer, {
1388
- id: "quit-hint",
1389
- content: "[y] Yes [n/Esc] No",
1390
- fg: COLORS.textMuted
1391
- });
1392
- hintRow.add(hint);
1393
- dialog.add(titleRow);
1394
- dialog.add(spacer);
1395
- dialog.add(hintRow);
1396
- renderer.root.add(overlay);
1397
- state.modalOverlay = overlay;
1398
- 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 () => {});
1399
1623
  }
1400
1624
  // src/components/modals/view-task.ts
1401
- import { BoxRenderable as BoxRenderable15, TextRenderable as TextRenderable14 } from "@opentui/core";
1402
- var DIALOG_WIDTH2 = 60;
1625
+ import { BoxRenderable as BoxRenderable14, TextRenderable as TextRenderable13 } from "@opentui/core";
1626
+ var DIALOG_WIDTH3 = MODAL_WIDTHS.large;
1403
1627
  var DESC_VISIBLE_LINES = 4;
1404
1628
  var LABEL_WIDTH = 12;
1405
1629
  function formatDate(date) {
@@ -1441,51 +1665,51 @@ async function showViewTaskModal(state, actions, taskIdOverride) {
1441
1665
  const dialogHeight = 24;
1442
1666
  const { overlay, dialog } = createModalOverlay(renderer, {
1443
1667
  id: "view-task-dialog",
1444
- width: DIALOG_WIDTH2,
1668
+ width: DIALOG_WIDTH3,
1445
1669
  height: dialogHeight
1446
1670
  });
1447
1671
  const headerDivider = createSectionDivider(renderer, {
1448
1672
  label: "Task Details",
1449
- width: DIALOG_WIDTH2 - 4,
1673
+ width: DIALOG_WIDTH3 - 4,
1450
1674
  id: "view-header"
1451
1675
  });
1452
- const spacerHeader = new BoxRenderable15(renderer, {
1676
+ const spacerHeader = new BoxRenderable14(renderer, {
1453
1677
  id: "view-spacer-header",
1454
1678
  width: "100%",
1455
1679
  height: 1
1456
1680
  });
1457
- const titleRow = new BoxRenderable15(renderer, {
1681
+ const titleRow = new BoxRenderable14(renderer, {
1458
1682
  id: "view-title-row",
1459
1683
  width: "100%",
1460
1684
  height: 1,
1461
1685
  flexDirection: "row",
1462
1686
  justifyContent: "space-between"
1463
1687
  });
1464
- const taskTitle = new TextRenderable14(renderer, {
1688
+ const taskTitle = new TextRenderable13(renderer, {
1465
1689
  id: "view-task-title",
1466
- content: truncate(task.title, DIALOG_WIDTH2 - 14),
1690
+ content: truncate(task.title, DIALOG_WIDTH3 - 14),
1467
1691
  fg: COLORS.text
1468
1692
  });
1469
- const editHint = new TextRenderable14(renderer, {
1693
+ const editHint = new TextRenderable13(renderer, {
1470
1694
  id: "view-edit-hint",
1471
1695
  content: "[e]dit",
1472
1696
  fg: COLORS.textDim
1473
1697
  });
1474
1698
  titleRow.add(taskTitle);
1475
1699
  titleRow.add(editHint);
1476
- const idRow = new BoxRenderable15(renderer, {
1700
+ const idRow = new BoxRenderable14(renderer, {
1477
1701
  id: "view-id-row",
1478
1702
  width: "100%",
1479
1703
  height: 1,
1480
1704
  flexDirection: "row",
1481
1705
  justifyContent: "space-between"
1482
1706
  });
1483
- const idValue = new TextRenderable14(renderer, {
1707
+ const idValue = new TextRenderable13(renderer, {
1484
1708
  id: "view-id-value",
1485
- content: truncateMiddle(task.id, DIALOG_WIDTH2 - 14),
1709
+ content: truncateMiddle(task.id, DIALOG_WIDTH3 - 14),
1486
1710
  fg: COLORS.textDim
1487
1711
  });
1488
- const copyHint = new TextRenderable14(renderer, {
1712
+ const copyHint = new TextRenderable13(renderer, {
1489
1713
  id: "view-copy-hint",
1490
1714
  content: "[c]opy",
1491
1715
  fg: COLORS.textDim
@@ -1494,26 +1718,26 @@ async function showViewTaskModal(state, actions, taskIdOverride) {
1494
1718
  idRow.add(copyHint);
1495
1719
  const statusDivider = createSectionDivider(renderer, {
1496
1720
  label: "Status",
1497
- width: DIALOG_WIDTH2 - 4,
1721
+ width: DIALOG_WIDTH3 - 4,
1498
1722
  id: "view-status"
1499
1723
  });
1500
- const columnRow = new BoxRenderable15(renderer, {
1724
+ const columnRow = new BoxRenderable14(renderer, {
1501
1725
  id: "view-column-row",
1502
1726
  width: "100%",
1503
1727
  height: 1,
1504
1728
  flexDirection: "row"
1505
1729
  });
1506
- const columnLabel = new TextRenderable14(renderer, {
1730
+ const columnLabel = new TextRenderable13(renderer, {
1507
1731
  id: "view-column-label",
1508
1732
  content: padLabel("Column"),
1509
1733
  fg: COLORS.textMuted
1510
1734
  });
1511
- const columnBullet = new TextRenderable14(renderer, {
1735
+ const columnBullet = new TextRenderable13(renderer, {
1512
1736
  id: "view-column-bullet",
1513
1737
  content: "\u25CF ",
1514
1738
  fg: statusColor
1515
1739
  });
1516
- const columnValue = new TextRenderable14(renderer, {
1740
+ const columnValue = new TextRenderable13(renderer, {
1517
1741
  id: "view-column-value",
1518
1742
  content: columnName,
1519
1743
  fg: COLORS.text
@@ -1521,54 +1745,54 @@ async function showViewTaskModal(state, actions, taskIdOverride) {
1521
1745
  columnRow.add(columnLabel);
1522
1746
  columnRow.add(columnBullet);
1523
1747
  columnRow.add(columnValue);
1524
- const assigneeRow = new BoxRenderable15(renderer, {
1748
+ const assigneeRow = new BoxRenderable14(renderer, {
1525
1749
  id: "view-assignee-row",
1526
1750
  width: "100%",
1527
1751
  height: 1,
1528
1752
  flexDirection: "row"
1529
1753
  });
1530
- const assigneeLabel = new TextRenderable14(renderer, {
1754
+ const assigneeLabel = new TextRenderable13(renderer, {
1531
1755
  id: "view-assignee-label",
1532
1756
  content: padLabel("Assignee"),
1533
1757
  fg: COLORS.textMuted
1534
1758
  });
1535
- const assigneeValue = new TextRenderable14(renderer, {
1759
+ const assigneeValue = new TextRenderable13(renderer, {
1536
1760
  id: "view-assignee-value",
1537
1761
  content: task.assignedTo ?? "\u2014 unassigned",
1538
1762
  fg: task.assignedTo ? COLORS.success : COLORS.textDim
1539
1763
  });
1540
1764
  assigneeRow.add(assigneeLabel);
1541
1765
  assigneeRow.add(assigneeValue);
1542
- const creatorRow = new BoxRenderable15(renderer, {
1766
+ const creatorRow = new BoxRenderable14(renderer, {
1543
1767
  id: "view-creator-row",
1544
1768
  width: "100%",
1545
1769
  height: 1,
1546
1770
  flexDirection: "row"
1547
1771
  });
1548
- const creatorLabel = new TextRenderable14(renderer, {
1772
+ const creatorLabel = new TextRenderable13(renderer, {
1549
1773
  id: "view-creator-label",
1550
1774
  content: padLabel("Creator"),
1551
1775
  fg: COLORS.textMuted
1552
1776
  });
1553
- const creatorValue = new TextRenderable14(renderer, {
1777
+ const creatorValue = new TextRenderable13(renderer, {
1554
1778
  id: "view-creator-value",
1555
1779
  content: task.createdBy,
1556
1780
  fg: COLORS.text
1557
1781
  });
1558
1782
  creatorRow.add(creatorLabel);
1559
1783
  creatorRow.add(creatorValue);
1560
- const labelsRow = new BoxRenderable15(renderer, {
1784
+ const labelsRow = new BoxRenderable14(renderer, {
1561
1785
  id: "view-labels-row",
1562
1786
  width: "100%",
1563
1787
  height: 1,
1564
1788
  flexDirection: "row"
1565
1789
  });
1566
- const labelsLabel = new TextRenderable14(renderer, {
1790
+ const labelsLabel = new TextRenderable13(renderer, {
1567
1791
  id: "view-labels-label",
1568
1792
  content: padLabel("Labels"),
1569
1793
  fg: COLORS.textMuted
1570
1794
  });
1571
- const labelsValue = new TextRenderable14(renderer, {
1795
+ const labelsValue = new TextRenderable13(renderer, {
1572
1796
  id: "view-labels-value",
1573
1797
  content: "\u2014 none",
1574
1798
  fg: COLORS.textDim
@@ -1577,52 +1801,52 @@ async function showViewTaskModal(state, actions, taskIdOverride) {
1577
1801
  labelsRow.add(labelsValue);
1578
1802
  const timelineDivider = createSectionDivider(renderer, {
1579
1803
  label: "Timeline",
1580
- width: DIALOG_WIDTH2 - 4,
1804
+ width: DIALOG_WIDTH3 - 4,
1581
1805
  id: "view-timeline"
1582
1806
  });
1583
- const createdRow = new BoxRenderable15(renderer, {
1807
+ const createdRow = new BoxRenderable14(renderer, {
1584
1808
  id: "view-created-row",
1585
1809
  width: "100%",
1586
1810
  height: 1,
1587
1811
  flexDirection: "row"
1588
1812
  });
1589
- const createdLabel = new TextRenderable14(renderer, {
1813
+ const createdLabel = new TextRenderable13(renderer, {
1590
1814
  id: "view-created-label",
1591
1815
  content: padLabel("Created"),
1592
1816
  fg: COLORS.textMuted
1593
1817
  });
1594
- const createdValue = new TextRenderable14(renderer, {
1818
+ const createdValue = new TextRenderable13(renderer, {
1595
1819
  id: "view-created-value",
1596
1820
  content: formatDate(task.createdAt),
1597
1821
  fg: COLORS.textDim
1598
1822
  });
1599
1823
  createdRow.add(createdLabel);
1600
1824
  createdRow.add(createdValue);
1601
- const updatedRow = new BoxRenderable15(renderer, {
1825
+ const updatedRow = new BoxRenderable14(renderer, {
1602
1826
  id: "view-updated-row",
1603
1827
  width: "100%",
1604
1828
  height: 1,
1605
1829
  flexDirection: "row",
1606
1830
  justifyContent: "space-between"
1607
1831
  });
1608
- const updatedLeft = new BoxRenderable15(renderer, {
1832
+ const updatedLeft = new BoxRenderable14(renderer, {
1609
1833
  id: "view-updated-left",
1610
1834
  height: 1,
1611
1835
  flexDirection: "row"
1612
1836
  });
1613
- const updatedLabel = new TextRenderable14(renderer, {
1837
+ const updatedLabel = new TextRenderable13(renderer, {
1614
1838
  id: "view-updated-label",
1615
1839
  content: padLabel("Updated"),
1616
1840
  fg: COLORS.textMuted
1617
1841
  });
1618
- const updatedValue = new TextRenderable14(renderer, {
1842
+ const updatedValue = new TextRenderable13(renderer, {
1619
1843
  id: "view-updated-value",
1620
1844
  content: formatDate(task.updatedAt),
1621
1845
  fg: COLORS.textDim
1622
1846
  });
1623
1847
  updatedLeft.add(updatedLabel);
1624
1848
  updatedLeft.add(updatedValue);
1625
- const relativeTime = new TextRenderable14(renderer, {
1849
+ const relativeTime = new TextRenderable13(renderer, {
1626
1850
  id: "view-relative-time",
1627
1851
  content: `(${formatRelativeTime(task.updatedAt)})`,
1628
1852
  fg: COLORS.textDim
@@ -1631,10 +1855,10 @@ async function showViewTaskModal(state, actions, taskIdOverride) {
1631
1855
  updatedRow.add(relativeTime);
1632
1856
  const descDivider = createSectionDivider(renderer, {
1633
1857
  label: "Description",
1634
- width: DIALOG_WIDTH2 - 4,
1858
+ width: DIALOG_WIDTH3 - 4,
1635
1859
  id: "view-desc"
1636
1860
  });
1637
- const descContainer = new BoxRenderable15(renderer, {
1861
+ const descContainer = new BoxRenderable14(renderer, {
1638
1862
  id: "view-desc-container",
1639
1863
  width: "100%",
1640
1864
  height: DESC_VISIBLE_LINES,
@@ -1642,7 +1866,7 @@ async function showViewTaskModal(state, actions, taskIdOverride) {
1642
1866
  });
1643
1867
  const descLineRenderables = [];
1644
1868
  for (let i = 0;i < DESC_VISIBLE_LINES; i++) {
1645
- const line = new TextRenderable14(renderer, {
1869
+ const line = new TextRenderable13(renderer, {
1646
1870
  id: `view-desc-line-${i}`,
1647
1871
  content: " ",
1648
1872
  fg: COLORS.text
@@ -1665,64 +1889,64 @@ async function showViewTaskModal(state, actions, taskIdOverride) {
1665
1889
  for (let i = 0;i < DESC_VISIBLE_LINES; i++) {
1666
1890
  const lineContent = visibleLines[i] ?? "";
1667
1891
  const isLastLine = i === DESC_VISIBLE_LINES - 1;
1668
- let displayContent = truncate(lineContent, DIALOG_WIDTH2 - 12);
1892
+ let displayContent = truncate(lineContent, DIALOG_WIDTH3 - 12);
1669
1893
  if (isLastLine && hasMore) {
1670
1894
  const remaining = totalDescLines - scrollOffset - DESC_VISIBLE_LINES;
1671
- displayContent = `${truncate(lineContent, DIALOG_WIDTH2 - 18)} \u25BC ${remaining}+`;
1895
+ displayContent = `${truncate(lineContent, DIALOG_WIDTH3 - 18)} \u25BC ${remaining}+`;
1672
1896
  }
1673
1897
  if (i === 0 && hasLess) {
1674
- displayContent = `\u25B2 ${scrollOffset}+ ${truncate(lineContent, DIALOG_WIDTH2 - 18)}`;
1898
+ displayContent = `\u25B2 ${scrollOffset}+ ${truncate(lineContent, DIALOG_WIDTH3 - 18)}`;
1675
1899
  }
1676
1900
  descLineRenderables[i].content = displayContent || " ";
1677
1901
  descLineRenderables[i].fg = i === 0 && hasLess || isLastLine && hasMore ? COLORS.textDim : COLORS.text;
1678
1902
  }
1679
1903
  }
1680
1904
  updateDescriptionContent(0);
1681
- const footerDivider = new BoxRenderable15(renderer, {
1905
+ const footerDivider = new BoxRenderable14(renderer, {
1682
1906
  id: "view-footer-divider",
1683
1907
  width: "100%",
1684
1908
  height: 1
1685
1909
  });
1686
- const footerLine = new TextRenderable14(renderer, {
1910
+ const footerLine = new TextRenderable13(renderer, {
1687
1911
  id: "view-footer-line",
1688
- content: "\u2500".repeat(DIALOG_WIDTH2 - 4),
1912
+ content: "\u2500".repeat(DIALOG_WIDTH3 - 4),
1689
1913
  fg: COLORS.border
1690
1914
  });
1691
1915
  footerDivider.add(footerLine);
1692
- const actionsRow = new BoxRenderable15(renderer, {
1916
+ const actionsRow = new BoxRenderable14(renderer, {
1693
1917
  id: "view-actions-row",
1694
1918
  width: "100%",
1695
1919
  height: 1,
1696
1920
  flexDirection: "row",
1697
1921
  justifyContent: "space-between"
1698
1922
  });
1699
- const actionsLeft = new BoxRenderable15(renderer, {
1923
+ const actionsLeft = new BoxRenderable14(renderer, {
1700
1924
  id: "view-actions-left",
1701
1925
  height: 1,
1702
1926
  flexDirection: "row",
1703
1927
  gap: 2
1704
1928
  });
1705
- const moveAction = new TextRenderable14(renderer, {
1929
+ const moveAction = new TextRenderable13(renderer, {
1706
1930
  id: "view-action-move",
1707
1931
  content: "[m] Move",
1708
1932
  fg: COLORS.textMuted
1709
1933
  });
1710
- const assignAction = new TextRenderable14(renderer, {
1934
+ const assignAction = new TextRenderable13(renderer, {
1711
1935
  id: "view-action-assign",
1712
1936
  content: "[u] Assign",
1713
1937
  fg: COLORS.textMuted
1714
1938
  });
1715
- const editAction = new TextRenderable14(renderer, {
1939
+ const editAction = new TextRenderable13(renderer, {
1716
1940
  id: "view-action-edit",
1717
1941
  content: "[e] Edit",
1718
1942
  fg: COLORS.textMuted
1719
1943
  });
1720
- const deleteAction = new TextRenderable14(renderer, {
1944
+ const deleteAction = new TextRenderable13(renderer, {
1721
1945
  id: "view-action-delete",
1722
1946
  content: "[d] Delete",
1723
1947
  fg: COLORS.danger
1724
1948
  });
1725
- const archiveRestoreAction = new TextRenderable14(renderer, {
1949
+ const archiveRestoreAction = new TextRenderable13(renderer, {
1726
1950
  id: "view-action-archive-restore",
1727
1951
  content: task.archived ? "[r] Restore" : "[x] Archive",
1728
1952
  fg: task.archived ? COLORS.success : COLORS.warning
@@ -1732,7 +1956,7 @@ async function showViewTaskModal(state, actions, taskIdOverride) {
1732
1956
  actionsLeft.add(editAction);
1733
1957
  actionsLeft.add(archiveRestoreAction);
1734
1958
  actionsLeft.add(deleteAction);
1735
- const escAction = new TextRenderable14(renderer, {
1959
+ const escAction = new TextRenderable13(renderer, {
1736
1960
  id: "view-action-esc",
1737
1961
  content: "[Esc]",
1738
1962
  fg: COLORS.textDim
@@ -1819,7 +2043,7 @@ async function copyTaskId(state) {
1819
2043
  copyHint.fg = COLORS.danger;
1820
2044
  state.viewTaskRuntime.copyTimeoutId = setTimeout(() => {
1821
2045
  if (state.viewTaskRuntime) {
1822
- idValue.content = truncateMiddle(taskId, DIALOG_WIDTH2 - 14);
2046
+ idValue.content = truncateMiddle(taskId, DIALOG_WIDTH3 - 14);
1823
2047
  idValue.fg = COLORS.textDim;
1824
2048
  copyHint.content = "[c]opy";
1825
2049
  copyHint.fg = COLORS.textDim;
@@ -1997,9 +2221,43 @@ var modalBindings = {
1997
2221
  return openArchiveModal(state);
1998
2222
  }
1999
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
+ },
2000
2252
  tab: toggleArchiveView,
2001
2253
  return: openViewModal,
2002
- "?": showHelpModal
2254
+ "?": showHelpModal,
2255
+ H: (state) => {
2256
+ const taskId = getSelectedTaskId(state);
2257
+ if (taskId) {
2258
+ return showTaskHistoryModal(state);
2259
+ }
2260
+ }
2003
2261
  },
2004
2262
  addTask: {
2005
2263
  escape: closeModal,
@@ -2077,6 +2335,7 @@ var modalBindings = {
2077
2335
  editTask: {
2078
2336
  escape: cancelEditTask,
2079
2337
  tab: focusNextEditField,
2338
+ "shift+tab": focusPrevEditField,
2080
2339
  left: buttonSelectPrev,
2081
2340
  right: buttonSelectNext,
2082
2341
  return: editTaskSave
@@ -2088,11 +2347,24 @@ var modalBindings = {
2088
2347
  y: quit,
2089
2348
  n: closeModal,
2090
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
2091
2362
  }
2092
2363
  };
2093
2364
  function handleKeypress(state, key) {
2094
2365
  const bindings = modalBindings[state.activeModal];
2095
- 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];
2096
2368
  return handler?.(state);
2097
2369
  }
2098
2370
 
@@ -2169,9 +2441,11 @@ async function main() {
2169
2441
  }
2170
2442
  const state = {
2171
2443
  renderer,
2444
+ db,
2172
2445
  taskService,
2173
2446
  boardService,
2174
2447
  boardName: board.name,
2448
+ projectRoot,
2175
2449
  columns: [],
2176
2450
  columnPanels: [],
2177
2451
  taskSelects: new Map,
@@ -2191,13 +2465,12 @@ async function main() {
2191
2465
  };
2192
2466
  await refreshBoard(state);
2193
2467
  let lastDataVersion = null;
2194
- const client = db.$client;
2468
+ const client = getDbClient(db);
2195
2469
  const checkForChanges = async () => {
2196
2470
  if (state.activeModal !== "none")
2197
2471
  return;
2198
2472
  try {
2199
- const result = await client.execute("PRAGMA data_version");
2200
- const currentVersion = result.rows[0]?.[0];
2473
+ const currentVersion = await getDataVersion(client);
2201
2474
  if (lastDataVersion !== null && currentVersion !== lastDataVersion) {
2202
2475
  await refreshBoard(state);
2203
2476
  }
@@ -2209,8 +2482,7 @@ async function main() {
2209
2482
  process.on("exit", cleanup);
2210
2483
  process.on("SIGINT", cleanup);
2211
2484
  process.on("SIGTERM", cleanup);
2212
- const keyEmitter = renderer.keyInput;
2213
- keyEmitter.on("keypress", (key) => {
2485
+ getKeyInput(renderer).on("keypress", (key) => {
2214
2486
  handleKeypress(state, key);
2215
2487
  });
2216
2488
  }