floq 1.3.2 → 1.4.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.
@@ -7,11 +7,12 @@ import { v4 as uuidv4 } from 'uuid';
7
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
- import { isTursoEnabled, getContexts, addContext, getLocale, getContextFilter, setContextFilter as saveContextFilter, getPomodoroFocusMode, setPomodoroFocusMode } from '../../config.js';
11
- import { useHistory, CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, } from '../history/index.js';
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';
12
12
  import { SearchBar } from './SearchBar.js';
13
13
  import { SearchResults } from './SearchResults.js';
14
14
  import { HelpModal } from './HelpModal.js';
15
+ import { InsightsModal } from './InsightsModal.js';
15
16
  import { CalendarModal } from './CalendarModal.js';
16
17
  import { Clock } from './Clock.js';
17
18
  import { CalendarEvents } from './CalendarEvents.js';
@@ -155,6 +156,9 @@ export function GtdDQ({ onOpenSettings }) {
155
156
  const [searchQuery, setSearchQuery] = useState('');
156
157
  const [searchResults, setSearchResults] = useState([]);
157
158
  const [searchResultIndex, setSearchResultIndex] = useState(0);
159
+ // Focus filter state
160
+ const [focusFilterState, setFocusFilterState] = useState(getFocusFilter());
161
+ const [effortSelectIndex, setEffortSelectIndex] = useState(0);
158
162
  // Pomodoro focus mode state
159
163
  const [focusMode, setFocusModeState] = useState(() => getPomodoroFocusMode());
160
164
  const toggleFocusMode = useCallback(() => {
@@ -181,6 +185,17 @@ export function GtdDQ({ onOpenSettings }) {
181
185
  const rightPaneWidth = terminalWidth - leftPaneWidth - 6;
182
186
  const currentTab = TABS[currentTabIndex];
183
187
  const currentTasks = mode === 'project-detail' ? projectTasks : tasks[currentTab];
188
+ const EFFORT_OPTIONS = [
189
+ { value: 'small', label: i18n.tui.effort?.small || 'Small' },
190
+ { value: 'medium', label: i18n.tui.effort?.medium || 'Medium' },
191
+ { value: 'large', label: i18n.tui.effort?.large || 'Large' },
192
+ { value: null, label: i18n.tui.effort?.clear || 'Clear' },
193
+ ];
194
+ const getCurrentTask = () => {
195
+ if (currentTasks.length === 0)
196
+ return undefined;
197
+ return currentTasks[selectedTaskIndex];
198
+ };
184
199
  const loadTasks = useCallback(async () => {
185
200
  const db = getDb();
186
201
  const newTasks = {
@@ -214,6 +229,9 @@ export function GtdDQ({ onOpenSettings }) {
214
229
  allTasks = allTasks.filter(t => t.context === contextFilter);
215
230
  }
216
231
  }
232
+ if (focusFilterState) {
233
+ allTasks = allTasks.filter(t => t.isFocused);
234
+ }
217
235
  newTasks[status] = allTasks;
218
236
  }
219
237
  newTasks.projects = await db
@@ -233,7 +251,7 @@ export function GtdDQ({ onOpenSettings }) {
233
251
  setProjectProgress(progress);
234
252
  setTasks(newTasks);
235
253
  setAvailableContexts(getContexts());
236
- }, [contextFilter]);
254
+ }, [contextFilter, focusFilterState]);
237
255
  const loadProjectTasks = useCallback(async (projectId) => {
238
256
  const db = getDb();
239
257
  const children = await db
@@ -430,6 +448,48 @@ export function GtdDQ({ onOpenSettings }) {
430
448
  setMessage(description);
431
449
  await loadTasks();
432
450
  }, [i18n.tui.context, loadTasks, history]);
451
+ const toggleTaskFocus = async () => {
452
+ const task = getCurrentTask();
453
+ if (!task)
454
+ return;
455
+ const newFocused = !task.isFocused;
456
+ const description = newFocused
457
+ ? fmt(i18n.tui.focus?.taskFocused || 'Focused: "{title}"', { title: task.title })
458
+ : fmt(i18n.tui.focus?.taskUnfocused || 'Unfocused: "{title}"', { title: task.title });
459
+ const command = new SetFocusCommand({
460
+ taskId: task.id,
461
+ fromFocused: task.isFocused,
462
+ toFocused: newFocused,
463
+ description,
464
+ });
465
+ await history.execute(command);
466
+ setMessage(description);
467
+ await loadTasks();
468
+ };
469
+ const toggleFocusFilter = () => {
470
+ const newValue = !focusFilterState;
471
+ setFocusFilterState(newValue);
472
+ setFocusFilter(newValue);
473
+ setMessage(newValue ? (i18n.tui.focus?.filterOn || 'Focus filter ON') : (i18n.tui.focus?.filterOff || 'Focus filter OFF'));
474
+ };
475
+ const setTaskEffort = async (effort) => {
476
+ const task = getCurrentTask();
477
+ if (!task)
478
+ return;
479
+ const description = effort
480
+ ? fmt(i18n.tui.effort?.effortSet || 'Set effort {effort} for "{title}"', { effort: i18n.tui.effort?.[effort] || effort, title: task.title })
481
+ : fmt(i18n.tui.effort?.effortCleared || 'Cleared effort for "{title}"', { title: task.title });
482
+ const command = new SetEffortCommand({
483
+ taskId: task.id,
484
+ fromEffort: task.effort,
485
+ toEffort: effort,
486
+ description,
487
+ });
488
+ await history.execute(command);
489
+ setMessage(description);
490
+ setMode('normal');
491
+ await loadTasks();
492
+ };
433
493
  const handleInputSubmit = async (value) => {
434
494
  // Handle search mode submit
435
495
  if (mode === 'search') {
@@ -494,8 +554,8 @@ export function GtdDQ({ onOpenSettings }) {
494
554
  }
495
555
  };
496
556
  useInput((input, key) => {
497
- // Handle help mode - let HelpModal handle its own input
498
- if (mode === 'help') {
557
+ // Handle help/insights mode - let modals handle their own input
558
+ if (mode === 'help' || mode === 'insights') {
499
559
  return;
500
560
  }
501
561
  // Battle command navigation (j/k) - when timer is running and in focus mode
@@ -730,6 +790,26 @@ export function GtdDQ({ onOpenSettings }) {
730
790
  }
731
791
  return;
732
792
  }
793
+ // Handle set-effort mode
794
+ if (mode === 'set-effort') {
795
+ if (key.escape) {
796
+ setMode('normal');
797
+ return;
798
+ }
799
+ if (input === 'j' || key.downArrow) {
800
+ setEffortSelectIndex(prev => Math.min(prev + 1, EFFORT_OPTIONS.length - 1));
801
+ return;
802
+ }
803
+ if (input === 'k' || key.upArrow) {
804
+ setEffortSelectIndex(prev => Math.max(prev - 1, 0));
805
+ return;
806
+ }
807
+ if (key.return) {
808
+ setTaskEffort(EFFORT_OPTIONS[effortSelectIndex].value);
809
+ return;
810
+ }
811
+ return;
812
+ }
733
813
  // Handle select-project mode
734
814
  if (mode === 'select-project') {
735
815
  if (key.escape) {
@@ -801,6 +881,10 @@ export function GtdDQ({ onOpenSettings }) {
801
881
  setMode('help');
802
882
  return;
803
883
  }
884
+ if (input === 'I') {
885
+ setMode('insights');
886
+ return;
887
+ }
804
888
  if (input === 'C') {
805
889
  setMode('calendar');
806
890
  return;
@@ -958,6 +1042,24 @@ export function GtdDQ({ onOpenSettings }) {
958
1042
  setMode('set-context');
959
1043
  return;
960
1044
  }
1045
+ // Toggle focus on task
1046
+ if (input === 'g') {
1047
+ toggleTaskFocus();
1048
+ return;
1049
+ }
1050
+ // Toggle focus filter
1051
+ if (input === 'G') {
1052
+ toggleFocusFilter();
1053
+ return;
1054
+ }
1055
+ // Set effort
1056
+ if (input === 'E') {
1057
+ if (getCurrentTask()) {
1058
+ setEffortSelectIndex(0);
1059
+ setMode('set-effort');
1060
+ }
1061
+ return;
1062
+ }
961
1063
  }
962
1064
  // Undo
963
1065
  if (input === 'u') {
@@ -1035,6 +1137,10 @@ export function GtdDQ({ onOpenSettings }) {
1035
1137
  if (mode === 'help') {
1036
1138
  return (_jsx(Box, { flexDirection: "column", padding: 1, children: _jsx(HelpModal, { onClose: () => setMode('normal') }) }));
1037
1139
  }
1140
+ // Insights modal overlay
1141
+ if (mode === 'insights') {
1142
+ return (_jsx(Box, { flexDirection: "column", padding: 1, children: _jsx(InsightsModal, { onClose: () => setMode('normal') }) }));
1143
+ }
1038
1144
  // Calendar modal overlay
1039
1145
  if (mode === 'calendar') {
1040
1146
  return (_jsx(Box, { flexDirection: "column", padding: 1, children: _jsx(CalendarModal, { onClose: () => setMode('normal') }) }));
@@ -1045,13 +1151,13 @@ export function GtdDQ({ onOpenSettings }) {
1045
1151
  const battleWidth = terminalWidth - 4;
1046
1152
  return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(PomodoroBattleUI, { state: pomodoro.state, remainingSeconds: pomodoro.remainingSeconds, isPaused: pomodoro.isPaused, jobClass: jobClass, level: playerLevel, totalCompleted: tasks.done.length, width: battleWidth, selectedCommand: selectedCommand }), message && (_jsx(BattleMessage, { message: message, isNew: true })), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsxs(Box, { children: [_jsx(Text, { color: theme.colors.accent, children: "\u2328\uFE0F " }), _jsxs(Text, { color: theme.colors.textMuted, children: ["a=", i18n.tui.keyBar.add, " f=focus off"] })] }) })] }));
1047
1153
  }
1048
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.colors.accent, children: jobClass }), _jsx(Text, { color: theme.colors.text, children: " Lv." }), _jsx(Text, { bold: true, color: theme.colors.secondary, children: Math.floor(tasks.done.length / 5) + 1 }), _jsx(Text, { color: theme.colors.text, children: " HP " }), _jsx(Text, { bold: true, color: theme.colors.statusNext, children: tasks.inbox.length + tasks.next.length }), _jsxs(Text, { color: theme.colors.textMuted, children: ["/", tasks.inbox.length + tasks.next.length + tasks.waiting.length + tasks.someday.length] }), _jsx(Text, { color: theme.colors.text, children: " MP " }), _jsx(Text, { bold: true, color: theme.colors.statusWaiting, children: tasks.projects.length }), _jsx(Text, { color: tursoEnabled ? theme.colors.accent : theme.colors.textMuted, children: tursoEnabled ? ' [TURSO]' : '' }), contextFilter !== null && (_jsxs(Text, { color: theme.colors.accent, children: [' ', "@", contextFilter === '' ? 'none' : contextFilter] }))] }), _jsxs(Box, { children: [_jsx(CalendarEvents, { compact: true, showLabel: true, withSeparator: true }), _jsx(Clock, {}), _jsx(Text, { color: theme.colors.textMuted, children: " ?=help q=quit" })] })] }), pomodoro.isRunning && pomodoro.state && (_jsx(Box, { marginBottom: 1, children: _jsx(PomodoroBattleUI, { state: pomodoro.state, remainingSeconds: pomodoro.remainingSeconds, isPaused: pomodoro.isPaused, jobClass: jobClass, level: Math.floor(tasks.done.length / 5) + 1, totalCompleted: tasks.done.length, width: terminalWidth - 4, compact: true }) })), mode === 'context-filter' ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: "Filter by context" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: ['all', 'none', ...availableContexts].map((ctx, index) => {
1154
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.colors.accent, children: jobClass }), _jsx(Text, { color: theme.colors.text, children: " Lv." }), _jsx(Text, { bold: true, color: theme.colors.secondary, children: Math.floor(tasks.done.length / 5) + 1 }), _jsx(Text, { color: theme.colors.text, children: " HP " }), _jsx(Text, { bold: true, color: theme.colors.statusNext, children: tasks.inbox.length + tasks.next.length }), _jsxs(Text, { color: theme.colors.textMuted, children: ["/", tasks.inbox.length + tasks.next.length + tasks.waiting.length + tasks.someday.length] }), _jsx(Text, { color: theme.colors.text, children: " MP " }), _jsx(Text, { bold: true, color: theme.colors.statusWaiting, children: tasks.projects.length }), _jsx(Text, { color: tursoEnabled ? theme.colors.accent : theme.colors.textMuted, children: tursoEnabled ? ' [TURSO]' : '' }), contextFilter !== null && (_jsxs(Text, { color: theme.colors.accent, children: [' ', "@", contextFilter === '' ? 'none' : contextFilter] })), focusFilterState && (_jsxs(Text, { color: theme.colors.accent, children: [" ", i18n.tui.focus?.focused || '★ Focused'] }))] }), _jsxs(Box, { children: [_jsx(CalendarEvents, { compact: true, showLabel: true, withSeparator: true }), _jsx(Clock, {}), _jsx(Text, { color: theme.colors.textMuted, children: " ?=help q=quit" })] })] }), pomodoro.isRunning && pomodoro.state && (_jsx(Box, { marginBottom: 1, children: _jsx(PomodoroBattleUI, { state: pomodoro.state, remainingSeconds: pomodoro.remainingSeconds, isPaused: pomodoro.isPaused, jobClass: jobClass, level: Math.floor(tasks.done.length / 5) + 1, totalCompleted: tasks.done.length, width: terminalWidth - 4, compact: true }) })), mode === 'context-filter' ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: "Filter by context" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: ['all', 'none', ...availableContexts].map((ctx, index) => {
1049
1155
  const label = ctx === 'all' ? 'All' : ctx === 'none' ? 'No context' : `@${ctx}`;
1050
1156
  return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? '▶ ' : ' ', label] }, ctx));
1051
1157
  }) })] })) : 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) => {
1052
1158
  const label = ctx === 'clear' ? (i18n.tui.context?.none || 'Clear context') : ctx === 'new' ? (i18n.tui.context?.addNew || 'New context...') : `@${ctx}`;
1053
1159
  return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? '▶ ' : ' ', label] }, ctx));
1054
- }) })] })) : 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') })] })] }), _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) => {
1160
+ }) })] })) : 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 ? '★' : '-' })] })] }), _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) => {
1055
1161
  const isSelected = index === selectedCommentIndex && mode === 'task-detail';
1056
1162
  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));
1057
1163
  })) }) })] })) : (_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) => {
@@ -1062,6 +1168,8 @@ export function GtdDQ({ onOpenSettings }) {
1062
1168
  const isSelected = (paneFocus === 'tasks' || mode === 'project-detail') && index === selectedTaskIndex;
1063
1169
  const parentProject = getParentProject(task.parentId);
1064
1170
  const progress = currentTab === 'projects' ? projectProgress[task.id] : undefined;
1171
+ const focusPrefix = task.isFocused ? '★ ' : '';
1172
+ const effortBadge = task.effort ? `[${{ 'small': 'S', 'medium': 'M', 'large': 'L' }[task.effort]}] ` : '';
1065
1173
  // Calculate available width for title
1066
1174
  const prefix = isSelected ? '▶ ' : ' ';
1067
1175
  const suffix = [
@@ -1070,9 +1178,9 @@ export function GtdDQ({ onOpenSettings }) {
1070
1178
  parentProject ? ` [${parentProject.title}]` : '',
1071
1179
  progress ? ` [${progress.completed}/${progress.total}]` : '',
1072
1180
  ].join('');
1073
- const availableWidth = rightPaneWidth - 6 - getDisplayWidth(prefix) - getDisplayWidth(suffix);
1181
+ const availableWidth = rightPaneWidth - 6 - getDisplayWidth(prefix) - getDisplayWidth(focusPrefix) - getDisplayWidth(effortBadge) - getDisplayWidth(suffix);
1074
1182
  const displayTitle = truncateString(task.title, availableWidth);
1075
- return (_jsxs(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [prefix, displayTitle, task.waitingFor && _jsxs(Text, { color: theme.colors.muted, children: [" (", task.waitingFor, ")"] }), task.context && _jsxs(Text, { color: theme.colors.muted, children: [" @", task.context] }), parentProject && _jsxs(Text, { color: theme.colors.muted, children: [" [", parentProject.title, "]"] }), progress && _jsxs(Text, { color: theme.colors.muted, children: [" [", progress.completed, "/", progress.total, "]"] })] }, task.id));
1183
+ return (_jsxs(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [prefix, focusPrefix, effortBadge, displayTitle, task.waitingFor && _jsxs(Text, { color: theme.colors.muted, children: [" (", task.waitingFor, ")"] }), task.context && _jsxs(Text, { color: theme.colors.muted, children: [" @", task.context] }), parentProject && _jsxs(Text, { color: theme.colors.muted, children: [" [", parentProject.title, "]"] }), progress && _jsxs(Text, { color: theme.colors.muted, children: [" [", progress.completed, "/", progress.total, "]"] })] }, task.id));
1076
1184
  })) }) })] })), (mode === 'add' || mode === 'add-to-project') && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: mode === 'add-to-project' && selectedProject
1077
1185
  ? `${i18n.tui.newTask}[${selectedProject.title}] `
1078
1186
  : i18n.tui.newTask }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: i18n.tui.placeholder })] })), mode === 'move-to-waiting' && taskToWaiting && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.waitingFor }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: "" })] })), mode === 'add-context' && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: "New context: " }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: "Enter context name..." })] })), mode === 'add-comment' && selectedTask && (_jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.addComment || 'Add comment', ": "] }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: "Enter comment..." })] })), mode === 'search' && (_jsx(SearchBar, { value: searchQuery, onChange: handleSearchChange, onSubmit: handleInputSubmit })), mode === 'search' && searchQuery && (_jsx(SearchResults, { results: searchResults, selectedIndex: searchResultIndex, query: searchQuery })), message && mode === 'normal' && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.textHighlight, children: message }) })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.colors.accent, children: "\u2328\uFE0F " }), _jsx(Text, { color: theme.colors.textMuted, children: mode === 'task-detail'
@@ -7,12 +7,13 @@ import { v4 as uuidv4 } from 'uuid';
7
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
- import { isTursoEnabled, getContexts, addContext, getContextFilter, setContextFilter as saveContextFilter, getPomodoroFocusMode, setPomodoroFocusMode } from '../../config.js';
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, } from '../history/index.js';
12
+ import { useHistory, CreateTaskCommand, DeleteTaskCommand, 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';
16
+ import { InsightsModal } from './InsightsModal.js';
16
17
  import { CalendarModal } from './CalendarModal.js';
17
18
  import { MarioBoxInline } from './MarioBox.js';
18
19
  import { Clock } from './Clock.js';
@@ -97,6 +98,8 @@ export function GtdMario({ onOpenSettings }) {
97
98
  const [searchQuery, setSearchQuery] = useState('');
98
99
  const [searchResults, setSearchResults] = useState([]);
99
100
  const [searchResultIndex, setSearchResultIndex] = useState(0);
101
+ const [focusFilter, setFocusFilterState] = useState(getFocusFilter());
102
+ const [effortSelectIndex, setEffortSelectIndex] = useState(0);
100
103
  // Focus mode state (can be toggled during pomodoro)
101
104
  const [focusMode, setFocusModeState] = useState(() => getPomodoroFocusMode());
102
105
  const toggleFocusMode = useCallback(() => {
@@ -132,6 +135,17 @@ export function GtdMario({ onOpenSettings }) {
132
135
  const rightPaneWidth = terminalWidth - leftPaneWidth - 6;
133
136
  const currentTab = TABS[currentTabIndex];
134
137
  const currentTasks = mode === 'project-detail' ? projectTasks : tasks[currentTab];
138
+ const getCurrentTask = () => {
139
+ if (currentTasks.length === 0)
140
+ return undefined;
141
+ return currentTasks[selectedTaskIndex];
142
+ };
143
+ const EFFORT_OPTIONS = [
144
+ { value: 'small', label: i18n.tui.effort?.small || 'Small' },
145
+ { value: 'medium', label: i18n.tui.effort?.medium || 'Medium' },
146
+ { value: 'large', label: i18n.tui.effort?.large || 'Large' },
147
+ { value: null, label: i18n.tui.effort?.clear || 'Clear' },
148
+ ];
135
149
  const loadTasks = useCallback(async () => {
136
150
  const db = getDb();
137
151
  const newTasks = {
@@ -165,6 +179,9 @@ export function GtdMario({ onOpenSettings }) {
165
179
  allTasks = allTasks.filter(t => t.context === contextFilter);
166
180
  }
167
181
  }
182
+ if (focusFilter) {
183
+ allTasks = allTasks.filter(t => t.isFocused);
184
+ }
168
185
  newTasks[status] = allTasks;
169
186
  }
170
187
  newTasks.projects = await db
@@ -184,7 +201,7 @@ export function GtdMario({ onOpenSettings }) {
184
201
  setProjectProgress(progress);
185
202
  setTasks(newTasks);
186
203
  setAvailableContexts(getContexts());
187
- }, [contextFilter]);
204
+ }, [contextFilter, focusFilter]);
188
205
  const loadProjectTasks = useCallback(async (projectId) => {
189
206
  const db = getDb();
190
207
  const children = await db
@@ -377,6 +394,48 @@ export function GtdMario({ onOpenSettings }) {
377
394
  setMessage(description);
378
395
  await loadTasks();
379
396
  }, [i18n.tui.context, loadTasks, history]);
397
+ const toggleTaskFocus = async () => {
398
+ const task = getCurrentTask();
399
+ if (!task)
400
+ return;
401
+ const newFocused = !task.isFocused;
402
+ const description = newFocused
403
+ ? fmt(i18n.tui.focus?.taskFocused || 'Focused: "{title}"', { title: task.title })
404
+ : fmt(i18n.tui.focus?.taskUnfocused || 'Unfocused: "{title}"', { title: task.title });
405
+ const command = new SetFocusCommand({
406
+ taskId: task.id,
407
+ fromFocused: task.isFocused,
408
+ toFocused: newFocused,
409
+ description,
410
+ });
411
+ await history.execute(command);
412
+ setMessage(description);
413
+ await loadTasks();
414
+ };
415
+ const toggleFocusFilter = () => {
416
+ const newValue = !focusFilter;
417
+ setFocusFilterState(newValue);
418
+ setFocusFilter(newValue);
419
+ setMessage(newValue ? (i18n.tui.focus?.filterOn || 'Focus filter ON') : (i18n.tui.focus?.filterOff || 'Focus filter OFF'));
420
+ };
421
+ const setTaskEffort = async (effort) => {
422
+ const task = getCurrentTask();
423
+ if (!task)
424
+ return;
425
+ const description = effort
426
+ ? fmt(i18n.tui.effort?.effortSet || 'Set effort {effort} for "{title}"', { effort: i18n.tui.effort?.[effort] || effort, title: task.title })
427
+ : fmt(i18n.tui.effort?.effortCleared || 'Cleared effort for "{title}"', { title: task.title });
428
+ const command = new SetEffortCommand({
429
+ taskId: task.id,
430
+ fromEffort: task.effort,
431
+ toEffort: effort,
432
+ description,
433
+ });
434
+ await history.execute(command);
435
+ setMessage(description);
436
+ setMode('normal');
437
+ await loadTasks();
438
+ };
380
439
  const handleInputSubmit = async (value) => {
381
440
  if (mode === 'search') {
382
441
  if (searchResults.length > 0) {
@@ -439,7 +498,7 @@ export function GtdMario({ onOpenSettings }) {
439
498
  }
440
499
  };
441
500
  useInput((input, key) => {
442
- if (mode === 'help') {
501
+ if (mode === 'help' || mode === 'insights') {
443
502
  return;
444
503
  }
445
504
  // Focus mode key handling - simplified controls
@@ -644,6 +703,25 @@ export function GtdMario({ onOpenSettings }) {
644
703
  }
645
704
  return;
646
705
  }
706
+ if (mode === 'set-effort') {
707
+ if (key.escape) {
708
+ setMode('normal');
709
+ return;
710
+ }
711
+ if (input === 'j' || key.downArrow) {
712
+ setEffortSelectIndex(prev => Math.min(prev + 1, EFFORT_OPTIONS.length - 1));
713
+ return;
714
+ }
715
+ if (input === 'k' || key.upArrow) {
716
+ setEffortSelectIndex(prev => Math.max(prev - 1, 0));
717
+ return;
718
+ }
719
+ if (key.return) {
720
+ setTaskEffort(EFFORT_OPTIONS[effortSelectIndex].value);
721
+ return;
722
+ }
723
+ return;
724
+ }
647
725
  if (mode === 'select-project') {
648
726
  if (key.escape) {
649
727
  const wasFromTaskDetail = selectedTask !== null;
@@ -712,6 +790,10 @@ export function GtdMario({ onOpenSettings }) {
712
790
  setMode('help');
713
791
  return;
714
792
  }
793
+ if (input === 'I') {
794
+ setMode('insights');
795
+ return;
796
+ }
715
797
  if (input === 'C') {
716
798
  setMode('calendar');
717
799
  return;
@@ -857,6 +939,24 @@ export function GtdMario({ onOpenSettings }) {
857
939
  setMode('set-context');
858
940
  return;
859
941
  }
942
+ // Toggle focus on task
943
+ if (input === 'g') {
944
+ toggleTaskFocus();
945
+ return;
946
+ }
947
+ // Toggle focus filter
948
+ if (input === 'G') {
949
+ toggleFocusFilter();
950
+ return;
951
+ }
952
+ // Set effort
953
+ if (input === 'E') {
954
+ if (getCurrentTask()) {
955
+ setEffortSelectIndex(0);
956
+ setMode('set-effort');
957
+ }
958
+ return;
959
+ }
860
960
  }
861
961
  if (input === 'u') {
862
962
  history.undo().then((didUndo) => {
@@ -928,6 +1028,9 @@ export function GtdMario({ onOpenSettings }) {
928
1028
  if (mode === 'help') {
929
1029
  return (_jsx(Box, { flexDirection: "column", padding: 1, children: _jsx(HelpModal, { onClose: () => setMode('normal') }) }));
930
1030
  }
1031
+ if (mode === 'insights') {
1032
+ return (_jsx(Box, { flexDirection: "column", padding: 1, children: _jsx(InsightsModal, { onClose: () => setMode('normal') }) }));
1033
+ }
931
1034
  if (mode === 'calendar') {
932
1035
  return (_jsx(Box, { flexDirection: "column", padding: 1, children: _jsx(CalendarModal, { onClose: () => setMode('normal') }) }));
933
1036
  }
@@ -942,13 +1045,13 @@ export function GtdMario({ onOpenSettings }) {
942
1045
  const marioWidth = terminalWidth - 4;
943
1046
  return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsx(PomodoroMarioUI, { state: pomodoro.state, remainingSeconds: pomodoro.remainingSeconds, isPaused: pomodoro.isPaused, score: score, coins: coins, lives: lives, world: world, width: marioWidth, animationFrame: animationFrame }), message && (_jsx(MarioMessage, { message: message, isNew: true })), _jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsxs(Box, { children: [_jsx(Text, { color: theme.colors.accent, children: "\u2328\uFE0F " }), _jsxs(Text, { color: theme.colors.textMuted, children: ["a=", i18n.tui.keyBar.add, " Space=", pomodoro.isPaused ? 'resume' : 'pause', " S=skip X=stop f=focus off"] })] }) })] }));
944
1047
  }
945
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: "WORLD " }), _jsxs(Text, { color: theme.colors.primary, bold: true, children: [(new Date().getMonth() + 1), "-", new Date().getDate()] }), _jsx(Text, { color: theme.colors.text, children: " " }), _jsx(Text, { color: theme.colors.secondary, children: "x" }), _jsx(Text, { color: theme.colors.text, children: (tasks.inbox.length + tasks.next.length + tasks.waiting.length + tasks.someday.length + tasks.projects.length).toString().padStart(2, '0') }), _jsxs(Text, { color: theme.colors.textMuted, children: [" FLOQ v", VERSION] }), _jsx(Text, { color: tursoEnabled ? theme.colors.accent : theme.colors.textMuted, children: tursoEnabled ? ' [TURSO]' : ' [LOCAL]' }), contextFilter !== null && (_jsxs(Text, { color: theme.colors.accent, children: [' ', "@", contextFilter === '' ? 'none' : contextFilter] }))] }), _jsxs(Box, { children: [_jsx(CalendarEvents, { compact: true, showLabel: true, withSeparator: true }), _jsx(Clock, {})] })] }), pomodoro.isRunning && pomodoro.state && (_jsx(Box, { marginBottom: 1, children: _jsx(PomodoroMarioUI, { state: pomodoro.state, remainingSeconds: pomodoro.remainingSeconds, isPaused: pomodoro.isPaused, score: tasks.done.length * 100, coins: pomodoro.state.completedCount, lives: Math.floor(tasks.done.length / 10) + 3, world: `${new Date().getMonth() + 1}-${new Date().getDate()}`, width: terminalWidth - 4, compact: true }) })), mode === 'context-filter' ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: "Filter by context" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: ['all', 'none', ...availableContexts].map((ctx, index) => {
1048
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: "WORLD " }), _jsxs(Text, { color: theme.colors.primary, bold: true, children: [(new Date().getMonth() + 1), "-", new Date().getDate()] }), _jsx(Text, { color: theme.colors.text, children: " " }), _jsx(Text, { color: theme.colors.secondary, children: "x" }), _jsx(Text, { color: theme.colors.text, children: (tasks.inbox.length + tasks.next.length + tasks.waiting.length + tasks.someday.length + tasks.projects.length).toString().padStart(2, '0') }), _jsxs(Text, { color: theme.colors.textMuted, children: [" FLOQ v", VERSION] }), _jsx(Text, { color: tursoEnabled ? theme.colors.accent : theme.colors.textMuted, children: tursoEnabled ? ' [TURSO]' : ' [LOCAL]' }), contextFilter !== null && (_jsxs(Text, { color: theme.colors.accent, children: [' ', "@", contextFilter === '' ? 'none' : contextFilter] })), focusFilter && (_jsxs(Text, { color: theme.colors.accent, children: [" ", i18n.tui.focus?.focused || '★ Focused'] }))] }), _jsxs(Box, { children: [_jsx(CalendarEvents, { compact: true, showLabel: true, withSeparator: true }), _jsx(Clock, {})] })] }), pomodoro.isRunning && pomodoro.state && (_jsx(Box, { marginBottom: 1, children: _jsx(PomodoroMarioUI, { state: pomodoro.state, remainingSeconds: pomodoro.remainingSeconds, isPaused: pomodoro.isPaused, score: tasks.done.length * 100, coins: pomodoro.state.completedCount, lives: Math.floor(tasks.done.length / 10) + 3, world: `${new Date().getMonth() + 1}-${new Date().getDate()}`, width: terminalWidth - 4, compact: true }) })), mode === 'context-filter' ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: "Filter by context" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: ['all', 'none', ...availableContexts].map((ctx, index) => {
946
1049
  const label = ctx === 'all' ? 'All' : ctx === 'none' ? 'No context' : `@${ctx}`;
947
1050
  return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? '🍄 ' : ' ', label] }, ctx));
948
1051
  }) })] })) : 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) => {
949
1052
  const label = ctx === 'clear' ? (i18n.tui.context?.none || 'Clear context') : ctx === 'new' ? (i18n.tui.context?.addNew || 'New context...') : `@${ctx}`;
950
1053
  return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? '🍄 ' : ' ', label] }, ctx));
951
- }) })] })) : 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') })] })] }), _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) => {
1054
+ }) })] })) : 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 ? '★' : '-' })] })] }), _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) => {
952
1055
  const isSelected = index === selectedCommentIndex && mode === 'task-detail';
953
1056
  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));
954
1057
  })) }) })] })) : (_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) => {
@@ -959,6 +1062,8 @@ export function GtdMario({ onOpenSettings }) {
959
1062
  const isSelected = (paneFocus === 'tasks' || mode === 'project-detail') && index === selectedTaskIndex;
960
1063
  const parentProject = getParentProject(task.parentId);
961
1064
  const progress = currentTab === 'projects' ? projectProgress[task.id] : undefined;
1065
+ const focusPrefix = task.isFocused ? '★ ' : '';
1066
+ const effortBadge = task.effort ? `[${{ 'small': 'S', 'medium': 'M', 'large': 'L' }[task.effort]}] ` : '';
962
1067
  const prefix = isSelected ? '🍄 ' : ' ';
963
1068
  const suffix = [
964
1069
  task.waitingFor ? ` (${task.waitingFor})` : '',
@@ -966,9 +1071,9 @@ export function GtdMario({ onOpenSettings }) {
966
1071
  parentProject ? ` [${parentProject.title}]` : '',
967
1072
  progress ? ` [${progress.completed}/${progress.total}]` : '',
968
1073
  ].join('');
969
- const availableWidth = rightPaneWidth - 6 - getDisplayWidth(prefix) - getDisplayWidth(suffix);
1074
+ const availableWidth = rightPaneWidth - 6 - getDisplayWidth(prefix) - getDisplayWidth(`${focusPrefix}${effortBadge}`) - getDisplayWidth(suffix);
970
1075
  const displayTitle = truncateString(task.title, availableWidth);
971
- return (_jsxs(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [prefix, displayTitle, task.waitingFor && _jsxs(Text, { color: theme.colors.muted, children: [" (", task.waitingFor, ")"] }), task.context && _jsxs(Text, { color: theme.colors.muted, children: [" @", task.context] }), parentProject && _jsxs(Text, { color: theme.colors.muted, children: [" [", parentProject.title, "]"] }), progress && _jsxs(Text, { color: theme.colors.muted, children: [" [", progress.completed, "/", progress.total, "]"] })] }, task.id));
1076
+ return (_jsxs(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [prefix, focusPrefix, effortBadge, displayTitle, task.waitingFor && _jsxs(Text, { color: theme.colors.muted, children: [" (", task.waitingFor, ")"] }), task.context && _jsxs(Text, { color: theme.colors.muted, children: [" @", task.context] }), parentProject && _jsxs(Text, { color: theme.colors.muted, children: [" [", parentProject.title, "]"] }), progress && _jsxs(Text, { color: theme.colors.muted, children: [" [", progress.completed, "/", progress.total, "]"] })] }, task.id));
972
1077
  })) }) })] })), (mode === 'add' || mode === 'add-to-project') && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: mode === 'add-to-project' && selectedProject
973
1078
  ? `${i18n.tui.newTask}[${selectedProject.title}] `
974
1079
  : i18n.tui.newTask }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: i18n.tui.placeholder })] })), mode === 'move-to-waiting' && taskToWaiting && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.waitingFor }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: "" })] })), mode === 'add-context' && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: "New context: " }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: "Enter context name..." })] })), mode === 'add-comment' && selectedTask && (_jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.addComment || 'Add comment', ": "] }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: "Enter comment..." })] })), mode === 'search' && (_jsx(SearchBar, { value: searchQuery, onChange: handleSearchChange, onSubmit: handleInputSubmit })), mode === 'search' && searchQuery && (_jsx(SearchResults, { results: searchResults, selectedIndex: searchResultIndex, query: searchQuery })), message && mode === 'normal' && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.textHighlight, children: message }) })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: theme.colors.accent, children: "\u2328\uFE0F " }), _jsx(Text, { color: theme.colors.textMuted, children: mode === 'task-detail'
@@ -28,6 +28,9 @@ export function HelpModal({ onClose, isKanban = false }) {
28
28
  { type: 'key', key: 'd', value: kHelp.completeTask },
29
29
  { type: 'key', key: 'm', value: kHelp.moveRight },
30
30
  { type: 'key', key: 'BS', value: kHelp.moveLeft },
31
+ { type: 'key', key: 'g', value: kHelp.toggleFocus || 'Toggle focus' },
32
+ { type: 'key', key: 'G', value: kHelp.toggleFocusFilter || 'Toggle focus filter' },
33
+ { type: 'key', key: 'E', value: kHelp.setEffort || 'Set effort size' },
31
34
  { type: 'key', key: 'u', value: 'Undo' },
32
35
  { type: 'key', key: 'Ctrl+r', value: 'Redo' },
33
36
  { type: 'header', value: kHelp.settings },
@@ -36,6 +39,7 @@ export function HelpModal({ onClose, isKanban = false }) {
36
39
  { type: 'key', key: 'L', value: kHelp.changeLanguage },
37
40
  { type: 'header', value: kHelp.other },
38
41
  { type: 'key', key: '/', value: kHelp.searchTasks },
42
+ { type: 'key', key: 'I', value: help.showInsights || 'Show insights' },
39
43
  { type: 'key', key: '?', value: kHelp.showHelp },
40
44
  { type: 'key', key: 'q', value: kHelp.quit },
41
45
  ];
@@ -53,6 +57,9 @@ export function HelpModal({ onClose, isKanban = false }) {
53
57
  { type: 'key', key: 'w', value: help.moveToWaiting },
54
58
  { type: 'key', key: 'i', value: help.moveToInbox },
55
59
  { type: 'key', key: 'r', value: help.refresh },
60
+ { type: 'key', key: 'g', value: help.toggleFocus || 'Toggle focus' },
61
+ { type: 'key', key: 'G', value: help.toggleFocusFilter || 'Toggle focus filter' },
62
+ { type: 'key', key: 'E', value: help.setEffort || 'Set effort size' },
56
63
  { type: 'key', key: 'u', value: help.undo },
57
64
  { type: 'key', key: 'Ctrl+r', value: help.redo },
58
65
  { type: 'header', value: help.projects },
@@ -72,6 +79,7 @@ export function HelpModal({ onClose, isKanban = false }) {
72
79
  { type: 'header', value: help.other },
73
80
  { type: 'key', key: '/', value: help.searchTasks },
74
81
  { type: 'key', key: 'C', value: help.showCalendar || 'Show calendar' },
82
+ { type: 'key', key: 'I', value: help.showInsights || 'Show insights' },
75
83
  { type: 'key', key: '?', value: help.showHelp },
76
84
  { type: 'key', key: 'q', value: help.quit },
77
85
  ];
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ interface InsightsModalProps {
3
+ onClose: () => void;
4
+ }
5
+ export declare function InsightsModal({ onClose }: InsightsModalProps): React.ReactElement;
6
+ export {};