floq 1.7.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,7 +8,7 @@ import { getDb, schema } from '../../db/index.js';
8
8
  import { t, fmt } from '../../i18n/index.js';
9
9
  import { useTheme } from '../theme/index.js';
10
10
  import { isTursoEnabled, getContexts, addContext, getLocale, getContextFilter, setContextFilter as saveContextFilter, getPomodoroFocusMode, setPomodoroFocusMode, getFocusFilter, setFocusFilter } from '../../config.js';
11
- import { useHistory, CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, SetFocusCommand, SetEffortCommand, } from '../history/index.js';
11
+ import { useHistory, CreateTaskCommand, DeleteTaskCommand, DeleteProjectCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, SetFocusCommand, SetEffortCommand, } from '../history/index.js';
12
12
  import { SearchBar } from './SearchBar.js';
13
13
  import { SearchResults } from './SearchResults.js';
14
14
  import { HelpModal } from './HelpModal.js';
@@ -145,6 +145,8 @@ export function GtdDQ({ onOpenSettings }) {
145
145
  const [selectedCommentIndex, setSelectedCommentIndex] = useState(0);
146
146
  const [taskToWaiting, setTaskToWaiting] = useState(null);
147
147
  const [taskToDelete, setTaskToDelete] = useState(null);
148
+ const [projectToDelete, setProjectToDelete] = useState(null);
149
+ const [projectChildCount, setProjectChildCount] = useState(0);
148
150
  const [projectProgress, setProjectProgress] = useState({});
149
151
  // Context filter state - load from config for persistence across sessions/terminals
150
152
  const [contextFilter, setContextFilterState] = useState(() => getContextFilter());
@@ -401,6 +403,19 @@ export function GtdDQ({ onOpenSettings }) {
401
403
  setMessage(fmt(i18n.tui.deleted || 'Deleted: "{title}"', { title: task.title }));
402
404
  await loadTasks();
403
405
  }, [i18n.tui.deleted, loadTasks, history]);
406
+ const deleteProjectAction = useCallback(async (project, mode, childCount) => {
407
+ const message = mode === 'cascade'
408
+ ? (childCount > 0
409
+ ? fmt(i18n.tui.deletedProjectWithTasks || 'Deleted project "{title}" and {count} task(s)', { title: project.title, count: childCount })
410
+ : fmt(i18n.tui.deletedProject || 'Deleted project: "{title}"', { title: project.title }))
411
+ : (childCount > 0
412
+ ? fmt(i18n.tui.projectTasksToInbox || 'Deleted project "{title}", moved {count} task(s) to Inbox', { title: project.title, count: childCount })
413
+ : fmt(i18n.tui.deletedProject || 'Deleted project: "{title}"', { title: project.title }));
414
+ const command = new DeleteProjectCommand({ project, mode, description: message });
415
+ await history.execute(command);
416
+ setMessage(message);
417
+ await loadTasks();
418
+ }, [i18n.tui.deletedProject, i18n.tui.deletedProjectWithTasks, i18n.tui.projectTasksToInbox, loadTasks, history]);
404
419
  const linkTaskToProject = useCallback(async (task, project) => {
405
420
  const command = new LinkTaskCommand({
406
421
  taskId: task.id,
@@ -738,6 +753,50 @@ export function GtdDQ({ onOpenSettings }) {
738
753
  }
739
754
  return;
740
755
  }
756
+ // Handle confirm-delete-project
757
+ if (mode === 'confirm-delete-project' && projectToDelete) {
758
+ const cancel = () => {
759
+ setMessage(i18n.tui.deleteCancelled || 'Delete cancelled');
760
+ setProjectToDelete(null);
761
+ setMode('normal');
762
+ };
763
+ const runDelete = (deleteMode) => {
764
+ const project = projectToDelete;
765
+ const count = projectChildCount;
766
+ deleteProjectAction(project, deleteMode, count).then(() => {
767
+ if (selectedTaskIndex >= currentTasks.length - 1) {
768
+ setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
769
+ }
770
+ });
771
+ setProjectToDelete(null);
772
+ setMode('normal');
773
+ };
774
+ if (projectChildCount > 0) {
775
+ if (input === 't' || input === 'T') {
776
+ runDelete('cascade');
777
+ return;
778
+ }
779
+ if (input === 'i' || input === 'I') {
780
+ runDelete('keep');
781
+ return;
782
+ }
783
+ if (input === 'n' || input === 'N' || key.escape) {
784
+ cancel();
785
+ return;
786
+ }
787
+ }
788
+ else {
789
+ if (input === 'y' || input === 'Y') {
790
+ runDelete('cascade');
791
+ return;
792
+ }
793
+ if (input === 'n' || input === 'N' || key.escape) {
794
+ cancel();
795
+ return;
796
+ }
797
+ }
798
+ return;
799
+ }
741
800
  // Handle context-filter mode
742
801
  if (mode === 'context-filter') {
743
802
  if (key.escape) {
@@ -1052,6 +1111,20 @@ export function GtdDQ({ onOpenSettings }) {
1052
1111
  setMode('select-project');
1053
1112
  return;
1054
1113
  }
1114
+ // Delete project (on projects tab)
1115
+ if (input === 'D' && currentTab === 'projects') {
1116
+ const project = task;
1117
+ const db = getDb();
1118
+ db.select()
1119
+ .from(schema.tasks)
1120
+ .where(eq(schema.tasks.parentId, project.id))
1121
+ .then((children) => {
1122
+ setProjectChildCount(children.length);
1123
+ setProjectToDelete(project);
1124
+ setMode('confirm-delete-project');
1125
+ });
1126
+ return;
1127
+ }
1055
1128
  // Delete
1056
1129
  if (input === 'D' && currentTab !== 'projects') {
1057
1130
  setTaskToDelete(task);
@@ -1179,7 +1252,9 @@ export function GtdDQ({ onOpenSettings }) {
1179
1252
  }) })] })) : mode === 'set-context' && currentTasks.length > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.setContext || 'Set context for', ": ", currentTasks[selectedTaskIndex]?.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: ['clear', ...availableContexts, 'new'].map((ctx, index) => {
1180
1253
  const label = ctx === 'clear' ? (i18n.tui.context?.none || 'Clear context') : ctx === 'new' ? (i18n.tui.context?.addNew || 'New context...') : `@${ctx}`;
1181
1254
  return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? '▶ ' : ' ', label] }, ctx));
1182
- }) })] })) : mode === 'set-effort' && getCurrentTask() ? (_jsx(Box, { flexDirection: "column", children: _jsx(TitledBoxInline, { title: i18n.tui.effort?.set || 'Set Effort', width: Math.min(40, terminalWidth - 4), minHeight: EFFORT_OPTIONS.length, isActive: true, children: EFFORT_OPTIONS.map((option, index) => (_jsxs(Text, { color: index === effortSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === effortSelectIndex, children: [index === effortSelectIndex ? '▶ ' : ' ', option.label] }, option.label))) }) })) : mode === 'select-project' && taskToLink ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project', ": ", taskToLink.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: tasks.projects.map((project, index) => (_jsxs(Text, { color: index === projectSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === projectSelectIndex, children: [index === projectSelectIndex ? '▶ ' : ' ', project.title] }, project.id))) })] })) : mode === 'confirm-delete' && taskToDelete ? (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: theme.colors.accent, bold: true, children: fmt(i18n.tui.deleteConfirm || 'Delete "{title}"? (y/n)', { title: taskToDelete.title }) }) })) : (mode === 'task-detail' || mode === 'add-comment') && selectedTask ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(TitledBoxInline, { title: i18n.tui.taskDetailTitle || 'Task Details', width: terminalWidth - 4, minHeight: 4, isActive: true, children: [_jsx(Text, { color: theme.colors.text, bold: true, children: selectedTask.title }), selectedTask.description && (_jsx(Text, { color: theme.colors.textMuted, children: selectedTask.description })), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailStatus || 'Status', ": "] }), _jsxs(Text, { color: theme.colors.accent, children: [i18n.status[selectedTask.status], selectedTask.waitingFor && ` (${selectedTask.waitingFor})`] })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.label || 'Context', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.context ? `@${selectedTask.context}` : (i18n.tui.context?.none || 'No context') })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.effort?.label || 'Effort', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.effort ? (i18n.tui.effort?.[selectedTask.effort] || selectedTask.effort) : '-' })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.focus?.label || 'Focus', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.isFocused ? '★' : '-' })] }), selectedTask.status === 'done' && (_jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailCompletedAt || 'Completed', ": "] }), _jsx(Text, { color: theme.colors.accent, children: (selectedTask.completedAt ?? selectedTask.updatedAt).toLocaleString() })] }))] }), _jsx(Box, { marginTop: 1, children: _jsx(TitledBoxInline, { title: `${i18n.tui.comments || 'Comments'} (${taskComments.length})`, width: terminalWidth - 4, minHeight: 5, isActive: mode === 'task-detail', children: taskComments.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noComments || 'No comments yet' })) : (taskComments.map((comment, index) => {
1255
+ }) })] })) : mode === 'set-effort' && getCurrentTask() ? (_jsx(Box, { flexDirection: "column", children: _jsx(TitledBoxInline, { title: i18n.tui.effort?.set || 'Set Effort', width: Math.min(40, terminalWidth - 4), minHeight: EFFORT_OPTIONS.length, isActive: true, children: EFFORT_OPTIONS.map((option, index) => (_jsxs(Text, { color: index === effortSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === effortSelectIndex, children: [index === effortSelectIndex ? '▶ ' : ' ', option.label] }, option.label))) }) })) : mode === 'select-project' && taskToLink ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project', ": ", taskToLink.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: tasks.projects.map((project, index) => (_jsxs(Text, { color: index === projectSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === projectSelectIndex, children: [index === projectSelectIndex ? '▶ ' : ' ', project.title] }, project.id))) })] })) : mode === 'confirm-delete' && taskToDelete ? (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: theme.colors.accent, bold: true, children: fmt(i18n.tui.deleteConfirm || 'Delete "{title}"? (y/n)', { title: taskToDelete.title }) }) })) : mode === 'confirm-delete-project' && projectToDelete ? (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: theme.colors.accent, bold: true, children: projectChildCount > 0
1256
+ ? fmt(i18n.tui.deleteProjectChildren || 'Project "{title}" has {count} task(s). t=delete all i=keep tasks (→Inbox) n=cancel', { title: projectToDelete.title, count: projectChildCount })
1257
+ : fmt(i18n.tui.deleteProjectConfirm || 'Delete project "{title}"? (y/n)', { title: projectToDelete.title }) }) })) : (mode === 'task-detail' || mode === 'add-comment') && selectedTask ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(TitledBoxInline, { title: i18n.tui.taskDetailTitle || 'Task Details', width: terminalWidth - 4, minHeight: 4, isActive: true, children: [_jsx(Text, { color: theme.colors.text, bold: true, children: selectedTask.title }), selectedTask.description && (_jsx(Text, { color: theme.colors.textMuted, children: selectedTask.description })), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailStatus || 'Status', ": "] }), _jsxs(Text, { color: theme.colors.accent, children: [i18n.status[selectedTask.status], selectedTask.waitingFor && ` (${selectedTask.waitingFor})`] })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.label || 'Context', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.context ? `@${selectedTask.context}` : (i18n.tui.context?.none || 'No context') })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.effort?.label || 'Effort', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.effort ? (i18n.tui.effort?.[selectedTask.effort] || selectedTask.effort) : '-' })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.focus?.label || 'Focus', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.isFocused ? '★' : '-' })] }), selectedTask.status === 'done' && (_jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailCompletedAt || 'Completed', ": "] }), _jsx(Text, { color: theme.colors.accent, children: (selectedTask.completedAt ?? selectedTask.updatedAt).toLocaleString() })] }))] }), _jsx(Box, { marginTop: 1, children: _jsx(TitledBoxInline, { title: `${i18n.tui.comments || 'Comments'} (${taskComments.length})`, width: terminalWidth - 4, minHeight: 5, isActive: mode === 'task-detail', children: taskComments.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noComments || 'No comments yet' })) : (taskComments.map((comment, index) => {
1183
1258
  const isSelected = index === selectedCommentIndex && mode === 'task-detail';
1184
1259
  return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: theme.colors.textMuted, children: [isSelected ? '▶ ' : ' ', "[", comment.createdAt.toLocaleString(), "]"] }), _jsxs(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [' ', comment.content] })] }, comment.id));
1185
1260
  })) }) })] })) : (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { marginRight: 2, children: _jsx(TitledBoxInline, { title: mode === 'project-detail' && selectedProject ? selectedProject.title : 'GTD', width: leftPaneWidth, minHeight: 8, isActive: paneFocus === 'tabs' && mode !== 'project-detail', children: mode === 'project-detail' ? (_jsx(Text, { color: theme.colors.textMuted, children: "\u2190 Esc/b: back" })) : (TABS.map((tab, index) => {
@@ -9,7 +9,7 @@ import { t, fmt } from '../../i18n/index.js';
9
9
  import { useTheme } from '../theme/index.js';
10
10
  import { isTursoEnabled, getContexts, addContext, getContextFilter, setContextFilter as saveContextFilter, getPomodoroFocusMode, setPomodoroFocusMode, getFocusFilter, setFocusFilter } from '../../config.js';
11
11
  import { VERSION } from '../../version.js';
12
- import { useHistory, CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, SetFocusCommand, SetEffortCommand, } from '../history/index.js';
12
+ import { useHistory, CreateTaskCommand, DeleteTaskCommand, DeleteProjectCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, SetFocusCommand, SetEffortCommand, } from '../history/index.js';
13
13
  import { SearchBar } from './SearchBar.js';
14
14
  import { SearchResults } from './SearchResults.js';
15
15
  import { HelpModal } from './HelpModal.js';
@@ -87,6 +87,8 @@ export function GtdMario({ onOpenSettings }) {
87
87
  const [selectedCommentIndex, setSelectedCommentIndex] = useState(0);
88
88
  const [taskToWaiting, setTaskToWaiting] = useState(null);
89
89
  const [taskToDelete, setTaskToDelete] = useState(null);
90
+ const [projectToDelete, setProjectToDelete] = useState(null);
91
+ const [projectChildCount, setProjectChildCount] = useState(0);
90
92
  const [projectProgress, setProjectProgress] = useState({});
91
93
  // Context filter state - load from config for persistence across sessions/terminals
92
94
  const [contextFilter, setContextFilterState] = useState(() => getContextFilter());
@@ -347,6 +349,19 @@ export function GtdMario({ onOpenSettings }) {
347
349
  setMessage(fmt(i18n.tui.deleted || 'Deleted: "{title}"', { title: task.title }));
348
350
  await loadTasks();
349
351
  }, [i18n.tui.deleted, loadTasks, history]);
352
+ const deleteProjectAction = useCallback(async (project, mode, childCount) => {
353
+ const message = mode === 'cascade'
354
+ ? (childCount > 0
355
+ ? fmt(i18n.tui.deletedProjectWithTasks || 'Deleted project "{title}" and {count} task(s)', { title: project.title, count: childCount })
356
+ : fmt(i18n.tui.deletedProject || 'Deleted project: "{title}"', { title: project.title }))
357
+ : (childCount > 0
358
+ ? fmt(i18n.tui.projectTasksToInbox || 'Deleted project "{title}", moved {count} task(s) to Inbox', { title: project.title, count: childCount })
359
+ : fmt(i18n.tui.deletedProject || 'Deleted project: "{title}"', { title: project.title }));
360
+ const command = new DeleteProjectCommand({ project, mode, description: message });
361
+ await history.execute(command);
362
+ setMessage(message);
363
+ await loadTasks();
364
+ }, [i18n.tui.deletedProject, i18n.tui.deletedProjectWithTasks, i18n.tui.projectTasksToInbox, loadTasks, history]);
350
365
  const linkTaskToProject = useCallback(async (task, project) => {
351
366
  const command = new LinkTaskCommand({
352
367
  taskId: task.id,
@@ -653,6 +668,49 @@ export function GtdMario({ onOpenSettings }) {
653
668
  }
654
669
  return;
655
670
  }
671
+ if (mode === 'confirm-delete-project' && projectToDelete) {
672
+ const cancel = () => {
673
+ setMessage(i18n.tui.deleteCancelled || 'Delete cancelled');
674
+ setProjectToDelete(null);
675
+ setMode('normal');
676
+ };
677
+ const runDelete = (deleteMode) => {
678
+ const project = projectToDelete;
679
+ const count = projectChildCount;
680
+ deleteProjectAction(project, deleteMode, count).then(() => {
681
+ if (selectedTaskIndex >= currentTasks.length - 1) {
682
+ setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
683
+ }
684
+ });
685
+ setProjectToDelete(null);
686
+ setMode('normal');
687
+ };
688
+ if (projectChildCount > 0) {
689
+ if (input === 't' || input === 'T') {
690
+ runDelete('cascade');
691
+ return;
692
+ }
693
+ if (input === 'i' || input === 'I') {
694
+ runDelete('keep');
695
+ return;
696
+ }
697
+ if (input === 'n' || input === 'N' || key.escape) {
698
+ cancel();
699
+ return;
700
+ }
701
+ }
702
+ else {
703
+ if (input === 'y' || input === 'Y') {
704
+ runDelete('cascade');
705
+ return;
706
+ }
707
+ if (input === 'n' || input === 'N' || key.escape) {
708
+ cancel();
709
+ return;
710
+ }
711
+ }
712
+ return;
713
+ }
656
714
  if (mode === 'context-filter') {
657
715
  if (key.escape) {
658
716
  setContextSelectIndex(0);
@@ -950,6 +1008,20 @@ export function GtdMario({ onOpenSettings }) {
950
1008
  setMode('select-project');
951
1009
  return;
952
1010
  }
1011
+ // Delete project (on projects tab)
1012
+ if (input === 'D' && currentTab === 'projects') {
1013
+ const project = task;
1014
+ const db = getDb();
1015
+ db.select()
1016
+ .from(schema.tasks)
1017
+ .where(eq(schema.tasks.parentId, project.id))
1018
+ .then((children) => {
1019
+ setProjectChildCount(children.length);
1020
+ setProjectToDelete(project);
1021
+ setMode('confirm-delete-project');
1022
+ });
1023
+ return;
1024
+ }
953
1025
  if (input === 'D' && currentTab !== 'projects') {
954
1026
  setTaskToDelete(task);
955
1027
  setMode('confirm-delete');
@@ -1073,7 +1145,9 @@ export function GtdMario({ onOpenSettings }) {
1073
1145
  }) })] })) : mode === 'set-context' && currentTasks.length > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.setContext || 'Set context for', ": ", currentTasks[selectedTaskIndex]?.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: ['clear', ...availableContexts, 'new'].map((ctx, index) => {
1074
1146
  const label = ctx === 'clear' ? (i18n.tui.context?.none || 'Clear context') : ctx === 'new' ? (i18n.tui.context?.addNew || 'New context...') : `@${ctx}`;
1075
1147
  return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? '🍄 ' : ' ', label] }, ctx));
1076
- }) })] })) : mode === 'set-effort' && currentTasks.length > 0 ? (_jsx(Box, { flexDirection: "column", children: _jsxs(MarioBoxInline, { title: i18n.tui.effort?.set || 'Set Effort', width: terminalWidth - 4, minHeight: 4, isActive: true, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.effort?.set || 'Set effort for', ": ", currentTasks[selectedTaskIndex]?.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: EFFORT_OPTIONS.map((opt, index) => (_jsxs(Text, { color: index === effortSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === effortSelectIndex, children: [index === effortSelectIndex ? '🍄 ' : ' ', opt.label] }, opt.value || 'clear'))) })] }) })) : mode === 'select-project' && taskToLink ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project', ": ", taskToLink.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: tasks.projects.map((project, index) => (_jsxs(Text, { color: index === projectSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === projectSelectIndex, children: [index === projectSelectIndex ? '🍄 ' : ' ', project.title] }, project.id))) })] })) : mode === 'confirm-delete' && taskToDelete ? (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: theme.colors.accent, bold: true, children: fmt(i18n.tui.deleteConfirm || 'Delete "{title}"? (y/n)', { title: taskToDelete.title }) }) })) : (mode === 'task-detail' || mode === 'add-comment') && selectedTask ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(MarioBoxInline, { title: i18n.tui.taskDetailTitle || 'Task Details', width: terminalWidth - 4, minHeight: 4, isActive: true, children: [_jsx(Text, { color: theme.colors.text, bold: true, children: selectedTask.title }), selectedTask.description && (_jsx(Text, { color: theme.colors.textMuted, children: selectedTask.description })), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailStatus || 'Status', ": "] }), _jsxs(Text, { color: theme.colors.accent, children: [i18n.status[selectedTask.status], selectedTask.waitingFor && ` (${selectedTask.waitingFor})`] })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.label || 'Context', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.context ? `@${selectedTask.context}` : (i18n.tui.context?.none || 'No context') })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.effort?.label || 'Effort', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.effort ? (i18n.tui.effort?.[selectedTask.effort] || selectedTask.effort) : '-' })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.focus?.label || 'Focus', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.isFocused ? '★' : '-' })] }), selectedTask.status === 'done' && (_jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailCompletedAt || 'Completed', ": "] }), _jsx(Text, { color: theme.colors.accent, children: (selectedTask.completedAt ?? selectedTask.updatedAt).toLocaleString() })] }))] }), _jsx(Box, { marginTop: 1, children: _jsx(MarioBoxInline, { title: `${i18n.tui.comments || 'Comments'} (${taskComments.length})`, width: terminalWidth - 4, minHeight: 5, isActive: mode === 'task-detail', children: taskComments.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noComments || 'No comments yet' })) : (taskComments.map((comment, index) => {
1148
+ }) })] })) : mode === 'set-effort' && currentTasks.length > 0 ? (_jsx(Box, { flexDirection: "column", children: _jsxs(MarioBoxInline, { title: i18n.tui.effort?.set || 'Set Effort', width: terminalWidth - 4, minHeight: 4, isActive: true, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.effort?.set || 'Set effort for', ": ", currentTasks[selectedTaskIndex]?.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: EFFORT_OPTIONS.map((opt, index) => (_jsxs(Text, { color: index === effortSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === effortSelectIndex, children: [index === effortSelectIndex ? '🍄 ' : ' ', opt.label] }, opt.value || 'clear'))) })] }) })) : mode === 'select-project' && taskToLink ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project', ": ", taskToLink.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: tasks.projects.map((project, index) => (_jsxs(Text, { color: index === projectSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === projectSelectIndex, children: [index === projectSelectIndex ? '🍄 ' : ' ', project.title] }, project.id))) })] })) : mode === 'confirm-delete' && taskToDelete ? (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: theme.colors.accent, bold: true, children: fmt(i18n.tui.deleteConfirm || 'Delete "{title}"? (y/n)', { title: taskToDelete.title }) }) })) : mode === 'confirm-delete-project' && projectToDelete ? (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: theme.colors.accent, bold: true, children: projectChildCount > 0
1149
+ ? fmt(i18n.tui.deleteProjectChildren || 'Project "{title}" has {count} task(s). t=delete all i=keep tasks (→Inbox) n=cancel', { title: projectToDelete.title, count: projectChildCount })
1150
+ : fmt(i18n.tui.deleteProjectConfirm || 'Delete project "{title}"? (y/n)', { title: projectToDelete.title }) }) })) : (mode === 'task-detail' || mode === 'add-comment') && selectedTask ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(MarioBoxInline, { title: i18n.tui.taskDetailTitle || 'Task Details', width: terminalWidth - 4, minHeight: 4, isActive: true, children: [_jsx(Text, { color: theme.colors.text, bold: true, children: selectedTask.title }), selectedTask.description && (_jsx(Text, { color: theme.colors.textMuted, children: selectedTask.description })), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailStatus || 'Status', ": "] }), _jsxs(Text, { color: theme.colors.accent, children: [i18n.status[selectedTask.status], selectedTask.waitingFor && ` (${selectedTask.waitingFor})`] })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.label || 'Context', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.context ? `@${selectedTask.context}` : (i18n.tui.context?.none || 'No context') })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.effort?.label || 'Effort', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.effort ? (i18n.tui.effort?.[selectedTask.effort] || selectedTask.effort) : '-' })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.focus?.label || 'Focus', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.isFocused ? '★' : '-' })] }), selectedTask.status === 'done' && (_jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailCompletedAt || 'Completed', ": "] }), _jsx(Text, { color: theme.colors.accent, children: (selectedTask.completedAt ?? selectedTask.updatedAt).toLocaleString() })] }))] }), _jsx(Box, { marginTop: 1, children: _jsx(MarioBoxInline, { title: `${i18n.tui.comments || 'Comments'} (${taskComments.length})`, width: terminalWidth - 4, minHeight: 5, isActive: mode === 'task-detail', children: taskComments.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noComments || 'No comments yet' })) : (taskComments.map((comment, index) => {
1077
1151
  const isSelected = index === selectedCommentIndex && mode === 'task-detail';
1078
1152
  return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: theme.colors.textMuted, children: [isSelected ? '🍄 ' : ' ', "[", comment.createdAt.toLocaleString(), "]"] }), _jsxs(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [' ', comment.content] })] }, comment.id));
1079
1153
  })) }) })] })) : (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { marginRight: 2, children: _jsx(MarioBoxInline, { title: mode === 'project-detail' && selectedProject ? selectedProject.title : 'STAGE', width: leftPaneWidth, minHeight: 8, isActive: paneFocus === 'tabs' && mode !== 'project-detail', children: mode === 'project-detail' ? (_jsx(Text, { color: theme.colors.textMuted, children: "\u2190 Esc/b: back" })) : (TABS.map((tab, index) => {
@@ -0,0 +1,31 @@
1
+ import { type ProjectDeleteMode } from '../../../db/projectDelete.js';
2
+ import type { UndoableCommand, SerializedCommand } from '../types.js';
3
+ import type { Task, Comment } from '../../../db/schema.js';
4
+ interface DeleteProjectParams {
5
+ project: Task;
6
+ mode: ProjectDeleteMode;
7
+ description: string;
8
+ deletedTasks?: Task[];
9
+ movedTasks?: Task[];
10
+ deletedComments?: Comment[];
11
+ }
12
+ /**
13
+ * Command to delete a project, handling its child tasks via `mode`
14
+ * (cascade = delete children too, keep = move children back to Inbox).
15
+ */
16
+ export declare class DeleteProjectCommand implements UndoableCommand {
17
+ readonly description: string;
18
+ private readonly project;
19
+ private readonly mode;
20
+ private deletedTasks;
21
+ private movedTasks;
22
+ private deletedComments;
23
+ constructor(params: DeleteProjectParams);
24
+ execute(): Promise<void>;
25
+ undo(): Promise<void>;
26
+ toJSON(): SerializedCommand;
27
+ static fromJSON(json: {
28
+ data: Record<string, unknown>;
29
+ }): DeleteProjectCommand;
30
+ }
31
+ export {};
@@ -0,0 +1,133 @@
1
+ import { eq } from 'drizzle-orm';
2
+ import { getDb, schema } from '../../../db/index.js';
3
+ import { deleteProject } from '../../../db/projectDelete.js';
4
+ function taskToValues(task) {
5
+ return {
6
+ id: task.id,
7
+ title: task.title,
8
+ description: task.description,
9
+ status: task.status,
10
+ isProject: task.isProject,
11
+ parentId: task.parentId,
12
+ waitingFor: task.waitingFor,
13
+ context: task.context,
14
+ isFocused: task.isFocused,
15
+ effort: task.effort,
16
+ dueDate: task.dueDate,
17
+ completedAt: task.completedAt,
18
+ createdAt: task.createdAt,
19
+ updatedAt: task.updatedAt,
20
+ };
21
+ }
22
+ function serializeTask(task) {
23
+ return {
24
+ ...task,
25
+ dueDate: task.dueDate ? task.dueDate.toISOString() : null,
26
+ completedAt: task.completedAt ? task.completedAt.toISOString() : null,
27
+ createdAt: task.createdAt.toISOString(),
28
+ updatedAt: task.updatedAt.toISOString(),
29
+ };
30
+ }
31
+ function deserializeTask(data) {
32
+ return {
33
+ ...data,
34
+ dueDate: data.dueDate ? new Date(data.dueDate) : null,
35
+ completedAt: data.completedAt ? new Date(data.completedAt) : null,
36
+ createdAt: new Date(data.createdAt),
37
+ updatedAt: new Date(data.updatedAt),
38
+ };
39
+ }
40
+ /**
41
+ * Command to delete a project, handling its child tasks via `mode`
42
+ * (cascade = delete children too, keep = move children back to Inbox).
43
+ */
44
+ export class DeleteProjectCommand {
45
+ description;
46
+ project;
47
+ mode;
48
+ deletedTasks = [];
49
+ movedTasks = [];
50
+ deletedComments = [];
51
+ constructor(params) {
52
+ this.project = params.project;
53
+ this.mode = params.mode;
54
+ this.description = params.description;
55
+ if (params.deletedTasks)
56
+ this.deletedTasks = params.deletedTasks;
57
+ if (params.movedTasks)
58
+ this.movedTasks = params.movedTasks;
59
+ if (params.deletedComments)
60
+ this.deletedComments = params.deletedComments;
61
+ }
62
+ async execute() {
63
+ const result = await deleteProject(this.project, this.mode);
64
+ this.deletedTasks = result.deletedTasks;
65
+ this.movedTasks = result.movedTasks;
66
+ this.deletedComments = result.deletedComments;
67
+ }
68
+ async undo() {
69
+ const db = getDb();
70
+ // Restore the project.
71
+ await db.insert(schema.tasks).values(taskToValues(this.project));
72
+ if (this.mode === 'cascade') {
73
+ // Re-create deleted child tasks.
74
+ for (const task of this.deletedTasks) {
75
+ await db.insert(schema.tasks).values(taskToValues(task));
76
+ }
77
+ }
78
+ else {
79
+ // Re-link moved children and restore their original status.
80
+ for (const task of this.movedTasks) {
81
+ await db
82
+ .update(schema.tasks)
83
+ .set({
84
+ parentId: task.parentId,
85
+ status: task.status,
86
+ updatedAt: task.updatedAt,
87
+ })
88
+ .where(eq(schema.tasks.id, task.id));
89
+ }
90
+ }
91
+ // Restore deleted comments.
92
+ for (const comment of this.deletedComments) {
93
+ await db.insert(schema.comments).values({
94
+ id: comment.id,
95
+ taskId: comment.taskId,
96
+ content: comment.content,
97
+ createdAt: comment.createdAt,
98
+ });
99
+ }
100
+ }
101
+ toJSON() {
102
+ return {
103
+ type: 'delete_project',
104
+ data: {
105
+ project: serializeTask(this.project),
106
+ mode: this.mode,
107
+ deletedTasks: this.deletedTasks.map(serializeTask),
108
+ movedTasks: this.movedTasks.map(serializeTask),
109
+ deletedComments: this.deletedComments.map((c) => ({
110
+ ...c,
111
+ createdAt: c.createdAt.toISOString(),
112
+ })),
113
+ description: this.description,
114
+ },
115
+ };
116
+ }
117
+ static fromJSON(json) {
118
+ const deletedTasksData = json.data.deletedTasks || [];
119
+ const movedTasksData = json.data.movedTasks || [];
120
+ const commentsData = json.data.deletedComments || [];
121
+ return new DeleteProjectCommand({
122
+ project: deserializeTask(json.data.project),
123
+ mode: json.data.mode,
124
+ deletedTasks: deletedTasksData.map(deserializeTask),
125
+ movedTasks: movedTasksData.map(deserializeTask),
126
+ deletedComments: commentsData.map((c) => ({
127
+ ...c,
128
+ createdAt: new Date(c.createdAt),
129
+ })),
130
+ description: json.data.description,
131
+ });
132
+ }
133
+ }
@@ -1,5 +1,6 @@
1
1
  export { CreateTaskCommand } from './CreateTaskCommand.js';
2
2
  export { DeleteTaskCommand } from './DeleteTaskCommand.js';
3
+ export { DeleteProjectCommand } from './DeleteProjectCommand.js';
3
4
  export { MoveTaskCommand } from './MoveTaskCommand.js';
4
5
  export { LinkTaskCommand } from './LinkTaskCommand.js';
5
6
  export { ConvertToProjectCommand } from './ConvertToProjectCommand.js';
@@ -1,5 +1,6 @@
1
1
  export { CreateTaskCommand } from './CreateTaskCommand.js';
2
2
  export { DeleteTaskCommand } from './DeleteTaskCommand.js';
3
+ export { DeleteProjectCommand } from './DeleteProjectCommand.js';
3
4
  export { MoveTaskCommand } from './MoveTaskCommand.js';
4
5
  export { LinkTaskCommand } from './LinkTaskCommand.js';
5
6
  export { ConvertToProjectCommand } from './ConvertToProjectCommand.js';
@@ -1,5 +1,6 @@
1
1
  import { CreateTaskCommand } from './CreateTaskCommand.js';
2
2
  import { DeleteTaskCommand } from './DeleteTaskCommand.js';
3
+ import { DeleteProjectCommand } from './DeleteProjectCommand.js';
3
4
  import { MoveTaskCommand } from './MoveTaskCommand.js';
4
5
  import { LinkTaskCommand } from './LinkTaskCommand.js';
5
6
  import { ConvertToProjectCommand } from './ConvertToProjectCommand.js';
@@ -11,6 +12,7 @@ import { SetEffortCommand } from './SetEffortCommand.js';
11
12
  const registry = {
12
13
  'create_task': (data) => CreateTaskCommand.fromJSON({ data }),
13
14
  'delete_task': (data) => DeleteTaskCommand.fromJSON({ data }),
15
+ 'delete_project': (data) => DeleteProjectCommand.fromJSON({ data }),
14
16
  'move_task': (data) => MoveTaskCommand.fromJSON({ data }),
15
17
  'link_task': (data) => LinkTaskCommand.fromJSON({ data }),
16
18
  'convert_to_project': (data) => ConvertToProjectCommand.fromJSON({ data }),
@@ -3,4 +3,4 @@ export { MAX_HISTORY_SIZE } from './types.js';
3
3
  export { HistoryManager, getHistoryManager } from './HistoryManager.js';
4
4
  export { HistoryProvider, useHistoryContext } from './HistoryContext.js';
5
5
  export { useHistory } from './useHistory.js';
6
- export { CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, SetFocusCommand, SetEffortCommand, deserializeCommand, } from './commands/index.js';
6
+ export { CreateTaskCommand, DeleteTaskCommand, DeleteProjectCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, SetFocusCommand, SetEffortCommand, deserializeCommand, } from './commands/index.js';
@@ -5,4 +5,4 @@ export { HistoryManager, getHistoryManager } from './HistoryManager.js';
5
5
  export { HistoryProvider, useHistoryContext } from './HistoryContext.js';
6
6
  export { useHistory } from './useHistory.js';
7
7
  // Commands
8
- export { CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, SetFocusCommand, SetEffortCommand, deserializeCommand, } from './commands/index.js';
8
+ export { CreateTaskCommand, DeleteTaskCommand, DeleteProjectCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, SetFocusCommand, SetEffortCommand, deserializeCommand, } from './commands/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "floq",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "description": "Floq - Getting Things Done Task Manager with MS-DOS style themes",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -39,18 +39,17 @@
39
39
  "dependencies": {
40
40
  "@inkjs/ui": "^2.0.0",
41
41
  "@libsql/client": "^0.17.0",
42
- "@modelcontextprotocol/sdk": "^1.27.0",
42
+ "@modelcontextprotocol/sdk": "^1.29.0",
43
43
  "better-sqlite3": "^11.7.0",
44
44
  "commander": "^13.1.0",
45
- "drizzle-orm": "^0.39.1",
46
- "googleapis": "^144.0.0",
45
+ "drizzle-orm": "^0.45.2",
47
46
  "ical.js": "^2.1.0",
48
47
  "ink": "^5.1.0",
49
48
  "ink-text-input": "^6.0.0",
50
49
  "open": "^10.1.0",
51
50
  "qrcode": "^1.5.4",
52
51
  "react": "^18.3.1",
53
- "uuid": "^11.0.5"
52
+ "uuid": "^14.0.0"
54
53
  },
55
54
  "devDependencies": {
56
55
  "@types/better-sqlite3": "^7.6.12",
@@ -58,7 +57,7 @@
58
57
  "@types/qrcode": "^1.5.6",
59
58
  "@types/react": "^18.3.18",
60
59
  "@types/uuid": "^10.0.0",
61
- "drizzle-kit": "^0.31.8",
60
+ "drizzle-kit": "^0.31.10",
62
61
  "typescript": "^5.7.3"
63
62
  },
64
63
  "overrides": {