floq 1.3.3 → 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.
package/dist/i18n/en.d.ts CHANGED
@@ -71,6 +71,27 @@ export declare const en: {
71
71
  notFound: string;
72
72
  noContexts: string;
73
73
  };
74
+ insights: {
75
+ title: string;
76
+ period: string;
77
+ weeklyCompletion: string;
78
+ weekLabel: string;
79
+ tasksCompleted: string;
80
+ dailyBreakdown: string;
81
+ currentStatus: string;
82
+ byContext: string;
83
+ byEffort: string;
84
+ noContext: string;
85
+ noEffort: string;
86
+ projectProgress: string;
87
+ activeProjects: string;
88
+ tasksRemaining: string;
89
+ averageCompletion: string;
90
+ daysAverage: string;
91
+ noData: string;
92
+ total: string;
93
+ andMore: string;
94
+ };
74
95
  };
75
96
  tui: {
76
97
  title: string;
@@ -121,6 +142,9 @@ export declare const en: {
121
142
  comment: string;
122
143
  back: string;
123
144
  delete: string;
145
+ focus: string;
146
+ focusFilter: string;
147
+ effort: string;
124
148
  pause: string;
125
149
  resume: string;
126
150
  skip: string;
@@ -155,6 +179,9 @@ export declare const en: {
155
179
  searchTasks: string;
156
180
  filterByContext: string;
157
181
  setContext: string;
182
+ toggleFocus: string;
183
+ toggleFocusFilter: string;
184
+ setEffort: string;
158
185
  pomodoro: string;
159
186
  startPomodoro: string;
160
187
  pauseResume: string;
@@ -167,6 +194,7 @@ export declare const en: {
167
194
  other: string;
168
195
  showHelp: string;
169
196
  showCalendar: string;
197
+ showInsights: string;
170
198
  quit: string;
171
199
  undo: string;
172
200
  redo: string;
@@ -194,6 +222,9 @@ export declare const en: {
194
222
  moveRight: string;
195
223
  moveLeft: string;
196
224
  searchTasks: string;
225
+ toggleFocus: string;
226
+ toggleFocusFilter: string;
227
+ setEffort: string;
197
228
  settings: string;
198
229
  changeTheme: string;
199
230
  changeViewMode: string;
@@ -246,6 +277,25 @@ export declare const en: {
246
277
  newContext: string;
247
278
  newContextPlaceholder: string;
248
279
  };
280
+ focus: {
281
+ label: string;
282
+ focused: string;
283
+ taskFocused: string;
284
+ taskUnfocused: string;
285
+ filterOn: string;
286
+ filterOff: string;
287
+ };
288
+ effort: {
289
+ label: string;
290
+ small: string;
291
+ medium: string;
292
+ large: string;
293
+ clear: string;
294
+ set: string;
295
+ setHelp: string;
296
+ effortSet: string;
297
+ effortCleared: string;
298
+ };
249
299
  pomodoro: {
250
300
  work: string;
251
301
  shortBreak: string;
@@ -385,6 +435,9 @@ export type HelpTranslations = {
385
435
  searchTasks: string;
386
436
  filterByContext: string;
387
437
  setContext: string;
438
+ toggleFocus?: string;
439
+ toggleFocusFilter?: string;
440
+ setEffort?: string;
388
441
  pomodoro: string;
389
442
  startPomodoro: string;
390
443
  pauseResume: string;
@@ -397,6 +450,7 @@ export type HelpTranslations = {
397
450
  other: string;
398
451
  showHelp: string;
399
452
  showCalendar?: string;
453
+ showInsights?: string;
400
454
  quit: string;
401
455
  undo: string;
402
456
  redo: string;
@@ -424,6 +478,9 @@ export type KanbanHelpTranslations = {
424
478
  moveRight: string;
425
479
  moveLeft: string;
426
480
  searchTasks: string;
481
+ toggleFocus?: string;
482
+ toggleFocusFilter?: string;
483
+ setEffort?: string;
427
484
  settings: string;
428
485
  changeTheme: string;
429
486
  changeViewMode: string;
@@ -451,6 +508,9 @@ export type KeyBarTranslations = {
451
508
  comment: string;
452
509
  back: string;
453
510
  delete: string;
511
+ focus?: string;
512
+ focusFilter?: string;
513
+ effort?: string;
454
514
  pause: string;
455
515
  resume: string;
456
516
  skip: string;
@@ -479,6 +539,25 @@ export type ContextTranslations = {
479
539
  newContext: string;
480
540
  newContextPlaceholder: string;
481
541
  };
542
+ export type FocusTranslations = {
543
+ label: string;
544
+ focused: string;
545
+ taskFocused: string;
546
+ taskUnfocused: string;
547
+ filterOn: string;
548
+ filterOff: string;
549
+ };
550
+ export type EffortTranslations = {
551
+ label: string;
552
+ small: string;
553
+ medium: string;
554
+ large: string;
555
+ clear: string;
556
+ set: string;
557
+ setHelp: string;
558
+ effortSet: string;
559
+ effortCleared: string;
560
+ };
482
561
  export type PomodoroTranslations = {
483
562
  work: string;
484
563
  shortBreak: string;
@@ -577,6 +656,8 @@ export type TuiTranslations = {
577
656
  kanbanHelp: KanbanHelpTranslations;
578
657
  search: SearchTranslations;
579
658
  context: ContextTranslations;
659
+ focus?: FocusTranslations;
660
+ effort?: EffortTranslations;
580
661
  info: InfoTranslations;
581
662
  pomodoro?: PomodoroTranslations;
582
663
  calendar?: CalendarTranslations;
@@ -667,6 +748,7 @@ export type Translations = {
667
748
  project: Record<string, string>;
668
749
  comment: Record<string, string>;
669
750
  context: Record<string, string>;
751
+ insights?: Record<string, string>;
670
752
  };
671
753
  tui: TuiTranslations;
672
754
  splash?: SplashTranslations;
package/dist/i18n/en.js CHANGED
@@ -75,6 +75,27 @@ export const en = {
75
75
  notFound: 'Context @{context} not found.',
76
76
  noContexts: 'No contexts configured.',
77
77
  },
78
+ insights: {
79
+ title: 'Task Insights',
80
+ period: 'Period',
81
+ weeklyCompletion: 'Weekly Completion',
82
+ weekLabel: 'Week of {date}',
83
+ tasksCompleted: '{count} tasks completed',
84
+ dailyBreakdown: 'Daily Breakdown',
85
+ currentStatus: 'Current Status',
86
+ byContext: 'By Context',
87
+ byEffort: 'By Effort',
88
+ noContext: 'No context',
89
+ noEffort: 'No effort set',
90
+ projectProgress: 'Project Progress',
91
+ activeProjects: 'Active Projects',
92
+ tasksRemaining: '{count} tasks remaining',
93
+ averageCompletion: 'Average Completion Time',
94
+ daysAverage: '{days} days',
95
+ noData: 'No completed tasks in this period',
96
+ total: 'Total',
97
+ andMore: ' ... and {count} more',
98
+ },
78
99
  },
79
100
  // TUI
80
101
  tui: {
@@ -90,17 +111,17 @@ export const en = {
90
111
  movedToWaiting: 'Moved "{title}" to Waiting (for {person})',
91
112
  waitingFor: 'Waiting for: ',
92
113
  refreshed: 'Refreshed',
93
- footer: 'a=add d=done D=delete n=next s=someday w=waiting i=inbox p=project P=link c=context',
114
+ footer: 'a=add d=done D=delete n=next s=someday w=waiting i=inbox p=project P=link c=context g=focus G=filter E=effort',
94
115
  // DQ/Mario style footers
95
116
  dqFooter: {
96
- tabs: 'j/k=select l/Enter=tasks 1-6=tab a=add @=filter /=search',
97
- tasks: 'j/k=select Enter=detail h/Esc=back d=done n=next s=someday w=wait i=inbox c=context p=project P=link D=delete u=undo /=search',
117
+ tabs: 'j/k=select l/Enter=tasks 1-6=tab a=add @=filter g=focus G=filter E=effort /=search',
118
+ tasks: 'j/k=select Enter=detail h/Esc=back d=done n=next s=someday w=wait i=inbox c=context g=focus E=effort p=project P=link D=delete u=undo /=search',
98
119
  projectDetail: 'j/k=select a=add d=done Esc/b=back /=search',
99
120
  taskDetail: 'j/k=select c/i=add comment P=link D=delete comment Esc/b=back',
100
121
  },
101
122
  kanbanFooter: {
102
- category: 'j/k=select l/Enter=tasks a=add @=filter /=search',
103
- tasks: 'j/k=select h/Esc=back d=done m=move a=add u=undo /=search',
123
+ category: 'j/k=select l/Enter=tasks a=add @=filter g=focus G=filter E=effort /=search',
124
+ tasks: 'j/k=select h/Esc=back d=done m=move a=add g=focus E=effort u=undo /=search',
104
125
  },
105
126
  noTasks: 'No tasks',
106
127
  // Tab labels
@@ -130,6 +151,9 @@ export const en = {
130
151
  comment: 'Comment',
131
152
  back: 'Back',
132
153
  delete: 'Delete',
154
+ focus: 'Focus',
155
+ focusFilter: 'Focus Filter',
156
+ effort: 'Effort',
133
157
  // Pomodoro keys
134
158
  pause: 'Pause',
135
159
  resume: 'Resume',
@@ -166,6 +190,9 @@ export const en = {
166
190
  searchTasks: 'Search tasks',
167
191
  filterByContext: 'Filter by context',
168
192
  setContext: 'Set context',
193
+ toggleFocus: 'Toggle focus',
194
+ toggleFocusFilter: 'Toggle focus filter',
195
+ setEffort: 'Set effort size',
169
196
  pomodoro: 'Pomodoro',
170
197
  startPomodoro: 'Start Pomodoro',
171
198
  pauseResume: 'Pause/Resume',
@@ -178,6 +205,7 @@ export const en = {
178
205
  other: 'Other',
179
206
  showHelp: 'Show this help',
180
207
  showCalendar: 'Show calendar',
208
+ showInsights: 'Show insights',
181
209
  quit: 'Quit',
182
210
  undo: 'Undo',
183
211
  redo: 'Redo',
@@ -207,6 +235,9 @@ export const en = {
207
235
  moveRight: 'Move task to next column',
208
236
  moveLeft: 'Move task to previous column',
209
237
  searchTasks: 'Search tasks',
238
+ toggleFocus: 'Toggle focus',
239
+ toggleFocusFilter: 'Toggle focus filter',
240
+ setEffort: 'Set effort size',
210
241
  settings: 'Settings',
211
242
  changeTheme: 'Change theme',
212
243
  changeViewMode: 'Change view mode',
@@ -262,6 +293,27 @@ export const en = {
262
293
  newContext: 'New context: ',
263
294
  newContextPlaceholder: 'Enter context name...',
264
295
  },
296
+ // Focus
297
+ focus: {
298
+ label: 'Focus',
299
+ focused: '★ Focused',
300
+ taskFocused: 'Focused: "{title}"',
301
+ taskUnfocused: 'Unfocused: "{title}"',
302
+ filterOn: 'Focus filter ON',
303
+ filterOff: 'Focus filter OFF',
304
+ },
305
+ // Effort
306
+ effort: {
307
+ label: 'Effort',
308
+ small: 'Small',
309
+ medium: 'Medium',
310
+ large: 'Large',
311
+ clear: 'Clear',
312
+ set: 'Set effort',
313
+ setHelp: 'j/k: select, Enter: confirm, Esc: cancel',
314
+ effortSet: 'Set effort {effort} for "{title}"',
315
+ effortCleared: 'Cleared effort for "{title}"',
316
+ },
265
317
  // Pomodoro
266
318
  pomodoro: {
267
319
  work: 'Work',
package/dist/i18n/ja.js CHANGED
@@ -75,6 +75,27 @@ export const ja = {
75
75
  notFound: 'コンテキスト @{context} が見つかりません。',
76
76
  noContexts: 'コンテキストが設定されていません。',
77
77
  },
78
+ insights: {
79
+ title: 'タスクインサイト',
80
+ period: '期間',
81
+ weeklyCompletion: '週別完了数',
82
+ weekLabel: '{date}の週',
83
+ tasksCompleted: '{count}件完了',
84
+ dailyBreakdown: '曜日別内訳',
85
+ currentStatus: '現在のステータス',
86
+ byContext: 'コンテキスト別',
87
+ byEffort: '作業量別',
88
+ noContext: 'コンテキストなし',
89
+ noEffort: '作業量未設定',
90
+ projectProgress: 'プロジェクト進捗',
91
+ activeProjects: 'アクティブ',
92
+ tasksRemaining: '残り{count}件',
93
+ averageCompletion: '平均完了時間',
94
+ daysAverage: '{days}日',
95
+ noData: 'この期間に完了したタスクはありません',
96
+ total: '合計',
97
+ andMore: ' ... 他{count}件',
98
+ },
78
99
  },
79
100
  // TUI
80
101
  tui: {
@@ -90,17 +111,17 @@ export const ja = {
90
111
  movedToWaiting: '「{title}」を連絡待ち({person})に移動しました',
91
112
  waitingFor: '待機相手: ',
92
113
  refreshed: '更新しました',
93
- footer: 'a=追加 d=完了 D=削除 n=次 s=いつか w=待ち i=Inbox p=プロジェクト化 P=紐づけ c=コンテキスト',
114
+ footer: 'a=追加 d=完了 D=削除 n=次 s=いつか w=待ち i=Inbox p=プロジェクト化 P=紐づけ c=コンテキスト g=集中 G=フィルター E=作業量',
94
115
  // DQ/Mario style footers
95
116
  dqFooter: {
96
- tabs: 'j/k=選択 l/Enter=タスク 1-6=タブ a=追加 @=フィルター /=検索',
97
- tasks: 'j/k=選択 Enter=詳細 h/Esc=戻る d=完了 n=次 s=いつか w=待ち i=inbox c=コンテキスト p=プロジェクト化 P=紐づけ D=削除 u=戻す /=検索',
117
+ tabs: 'j/k=選択 l/Enter=タスク 1-6=タブ a=追加 @=フィルター g=集中 G=フィルター E=作業量 /=検索',
118
+ tasks: 'j/k=選択 Enter=詳細 h/Esc=戻る d=完了 n=次 s=いつか w=待ち i=inbox c=コンテキスト g=集中 E=作業量 p=プロジェクト化 P=紐づけ D=削除 u=戻す /=検索',
98
119
  projectDetail: 'j/k=選択 a=追加 d=完了 Esc/b=戻る /=検索',
99
120
  taskDetail: 'j/k=選択 c/i=コメント追加 P=紐づけ D=コメント削除 Esc/b=戻る',
100
121
  },
101
122
  kanbanFooter: {
102
- category: 'j/k=選択 l/Enter=タスク a=追加 @=フィルター /=検索',
103
- tasks: 'j/k=選択 h/Esc=戻る d=完了 m=移動 a=追加 u=戻す /=検索',
123
+ category: 'j/k=選択 l/Enter=タスク a=追加 @=フィルター g=集中 G=フィルター E=作業量 /=検索',
124
+ tasks: 'j/k=選択 h/Esc=戻る d=完了 m=移動 a=追加 g=集中 E=作業量 u=戻す /=検索',
104
125
  },
105
126
  noTasks: 'タスクなし',
106
127
  // Tab labels
@@ -130,6 +151,9 @@ export const ja = {
130
151
  comment: 'コメント',
131
152
  back: '戻る',
132
153
  delete: '削除',
154
+ focus: '集中',
155
+ focusFilter: '集中フィルター',
156
+ effort: '作業量',
133
157
  // Pomodoro keys
134
158
  pause: '一時停止',
135
159
  resume: '再開',
@@ -166,6 +190,9 @@ export const ja = {
166
190
  searchTasks: 'タスク検索',
167
191
  filterByContext: 'コンテキストフィルター',
168
192
  setContext: 'コンテキスト設定',
193
+ toggleFocus: '集中モード切替',
194
+ toggleFocusFilter: '集中フィルター切替',
195
+ setEffort: '作業量設定',
169
196
  pomodoro: 'ポモドーロ',
170
197
  startPomodoro: 'ポモドーロ開始',
171
198
  pauseResume: '一時停止/再開',
@@ -178,6 +205,7 @@ export const ja = {
178
205
  other: 'その他',
179
206
  showHelp: 'このヘルプを表示',
180
207
  showCalendar: 'カレンダーを表示',
208
+ showInsights: 'インサイトを表示',
181
209
  quit: '終了',
182
210
  undo: '元に戻す',
183
211
  redo: 'やり直し',
@@ -207,6 +235,9 @@ export const ja = {
207
235
  moveRight: '次のカラムへ移動',
208
236
  moveLeft: '前のカラムへ戻す',
209
237
  searchTasks: 'タスク検索',
238
+ toggleFocus: '集中モード切替',
239
+ toggleFocusFilter: '集中フィルター切替',
240
+ setEffort: '作業量設定',
210
241
  settings: '設定',
211
242
  changeTheme: 'テーマ変更',
212
243
  changeViewMode: '表示モード変更',
@@ -262,6 +293,27 @@ export const ja = {
262
293
  newContext: '新規コンテキスト: ',
263
294
  newContextPlaceholder: 'コンテキスト名を入力...',
264
295
  },
296
+ // Focus
297
+ focus: {
298
+ label: '集中',
299
+ focused: '★ 集中',
300
+ taskFocused: '集中設定: 「{title}」',
301
+ taskUnfocused: '集中解除: 「{title}」',
302
+ filterOn: '集中フィルター ON',
303
+ filterOff: '集中フィルター OFF',
304
+ },
305
+ // Effort
306
+ effort: {
307
+ label: '作業量',
308
+ small: '小',
309
+ medium: '中',
310
+ large: '大',
311
+ clear: 'クリア',
312
+ set: '作業量設定',
313
+ setHelp: 'j/k: 選択, Enter: 確定, Esc: キャンセル',
314
+ effortSet: '「{title}」の作業量を{effort}に設定しました',
315
+ effortCleared: '「{title}」の作業量をクリアしました',
316
+ },
265
317
  // Pomodoro
266
318
  pomodoro: {
267
319
  work: '作業中',
package/dist/ui/App.js CHANGED
@@ -6,6 +6,7 @@ import { eq, and, gte } from 'drizzle-orm';
6
6
  import { v4 as uuidv4 } from 'uuid';
7
7
  import { TaskItem } from './components/TaskItem.js';
8
8
  import { HelpModal } from './components/HelpModal.js';
9
+ import { InsightsModal } from './components/InsightsModal.js';
9
10
  import { CalendarModal } from './components/CalendarModal.js';
10
11
  import { FunctionKeyBar } from './components/FunctionKeyBar.js';
11
12
  import { SearchBar } from './components/SearchBar.js';
@@ -22,14 +23,14 @@ import { LanguageSelector } from './LanguageSelector.js';
22
23
  import { getDb, schema } from '../db/index.js';
23
24
  import { t, fmt } from '../i18n/index.js';
24
25
  import { ThemeProvider, useTheme, getTheme } from './theme/index.js';
25
- import { getThemeName, getViewMode, setThemeName, setViewMode, setLocale, isTursoEnabled, getContexts, addContext, getSplashDuration, getContextFilter, setContextFilter as saveContextFilter, getPomodoroFocusMode, setPomodoroFocusMode } from '../config.js';
26
+ import { getThemeName, getViewMode, setThemeName, setViewMode, setLocale, isTursoEnabled, getContexts, addContext, getSplashDuration, getContextFilter, setContextFilter as saveContextFilter, getPomodoroFocusMode, setPomodoroFocusMode, getFocusFilter, setFocusFilter } from '../config.js';
26
27
  import { KanbanBoard } from './components/KanbanBoard.js';
27
28
  import { KanbanDQ } from './components/KanbanDQ.js';
28
29
  import { KanbanMario } from './components/KanbanMario.js';
29
30
  import { GtdDQ } from './components/GtdDQ.js';
30
31
  import { GtdMario } from './components/GtdMario.js';
31
32
  import { VERSION } from '../version.js';
32
- import { HistoryProvider, useHistory, CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, } from './history/index.js';
33
+ import { HistoryProvider, useHistory, CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, SetFocusCommand, SetEffortCommand, } from './history/index.js';
33
34
  const TABS = ['inbox', 'next', 'waiting', 'someday', 'projects', 'done'];
34
35
  export function App() {
35
36
  const [themeName, setThemeNameState] = useState(getThemeName);
@@ -123,7 +124,15 @@ function AppContent({ onOpenSettings }) {
123
124
  });
124
125
  }, []);
125
126
  const [availableContexts, setAvailableContexts] = useState([]);
127
+ const [focusFilterState, setFocusFilterState] = useState(getFocusFilter());
128
+ const [effortSelectIndex, setEffortSelectIndex] = useState(0);
126
129
  const i18n = t();
130
+ const EFFORT_OPTIONS = [
131
+ { value: 'small', label: i18n.tui.effort?.small || 'Small' },
132
+ { value: 'medium', label: i18n.tui.effort?.medium || 'Medium' },
133
+ { value: 'large', label: i18n.tui.effort?.large || 'Large' },
134
+ { value: null, label: i18n.tui.effort?.clear || 'Clear' },
135
+ ];
127
136
  // Pomodoro timer
128
137
  const handlePomodoroPhaseComplete = useCallback((type, completedCount) => {
129
138
  if (type === 'work') {
@@ -173,6 +182,9 @@ function AppContent({ onOpenSettings }) {
173
182
  allTasks = allTasks.filter(t => t.context === contextFilter);
174
183
  }
175
184
  }
185
+ if (focusFilterState) {
186
+ allTasks = allTasks.filter(t => t.isFocused);
187
+ }
176
188
  newTasks[status] = allTasks;
177
189
  }
178
190
  // Load projects (isProject = true, not done) - projects don't get context filtered
@@ -194,7 +206,7 @@ function AppContent({ onOpenSettings }) {
194
206
  setProjectProgress(progress);
195
207
  setTasks(newTasks);
196
208
  setAvailableContexts(getContexts());
197
- }, [contextFilter]);
209
+ }, [contextFilter, focusFilterState]);
198
210
  // Get parent project for a task
199
211
  const getParentProject = (parentId) => {
200
212
  if (!parentId)
@@ -222,6 +234,11 @@ function AppContent({ onOpenSettings }) {
222
234
  }, [loadTasks]);
223
235
  const currentTab = TABS[currentListIndex];
224
236
  const currentTasks = mode === 'project-detail' ? projectTasks : tasks[currentTab];
237
+ const getCurrentTask = () => {
238
+ if (currentTasks.length === 0)
239
+ return undefined;
240
+ return currentTasks[selectedTaskIndex];
241
+ };
225
242
  // Get all tasks for search (across all statuses)
226
243
  const getAllTasks = useCallback(() => {
227
244
  const allTasks = [];
@@ -446,6 +463,48 @@ function AppContent({ onOpenSettings }) {
446
463
  setMessage(description);
447
464
  await loadTasks();
448
465
  }, [i18n.tui.context, loadTasks, history]);
466
+ const toggleTaskFocus = async () => {
467
+ const task = getCurrentTask();
468
+ if (!task)
469
+ return;
470
+ const newFocused = !task.isFocused;
471
+ const description = newFocused
472
+ ? fmt(i18n.tui.focus?.taskFocused || 'Focused: "{title}"', { title: task.title })
473
+ : fmt(i18n.tui.focus?.taskUnfocused || 'Unfocused: "{title}"', { title: task.title });
474
+ const command = new SetFocusCommand({
475
+ taskId: task.id,
476
+ fromFocused: task.isFocused,
477
+ toFocused: newFocused,
478
+ description,
479
+ });
480
+ await history.execute(command);
481
+ setMessage(description);
482
+ await loadTasks();
483
+ };
484
+ const toggleFocusFilter = () => {
485
+ const newValue = !focusFilterState;
486
+ setFocusFilterState(newValue);
487
+ setFocusFilter(newValue);
488
+ setMessage(newValue ? (i18n.tui.focus?.filterOn || 'Focus filter ON') : (i18n.tui.focus?.filterOff || 'Focus filter OFF'));
489
+ };
490
+ const setTaskEffort = async (effort) => {
491
+ const task = getCurrentTask();
492
+ if (!task)
493
+ return;
494
+ const description = effort
495
+ ? fmt(i18n.tui.effort?.effortSet || 'Set effort {effort} for "{title}"', { effort: i18n.tui.effort?.[effort] || effort, title: task.title })
496
+ : fmt(i18n.tui.effort?.effortCleared || 'Cleared effort for "{title}"', { title: task.title });
497
+ const command = new SetEffortCommand({
498
+ taskId: task.id,
499
+ fromEffort: task.effort,
500
+ toEffort: effort,
501
+ description,
502
+ });
503
+ await history.execute(command);
504
+ setMessage(description);
505
+ setMode('normal');
506
+ await loadTasks();
507
+ };
449
508
  const deleteTask = useCallback(async (task) => {
450
509
  const command = new DeleteTaskCommand({
451
510
  task,
@@ -652,6 +711,26 @@ function AppContent({ onOpenSettings }) {
652
711
  }
653
712
  return;
654
713
  }
714
+ // Handle set-effort mode
715
+ if (mode === 'set-effort') {
716
+ if (key.escape) {
717
+ setMode('normal');
718
+ return;
719
+ }
720
+ if (input === 'j' || key.downArrow) {
721
+ setEffortSelectIndex(prev => Math.min(prev + 1, EFFORT_OPTIONS.length - 1));
722
+ return;
723
+ }
724
+ if (input === 'k' || key.upArrow) {
725
+ setEffortSelectIndex(prev => Math.max(prev - 1, 0));
726
+ return;
727
+ }
728
+ if (key.return) {
729
+ setTaskEffort(EFFORT_OPTIONS[effortSelectIndex].value);
730
+ return;
731
+ }
732
+ return;
733
+ }
655
734
  // Handle set-context mode
656
735
  if (mode === 'set-context') {
657
736
  if (key.escape) {
@@ -793,6 +872,11 @@ function AppContent({ onOpenSettings }) {
793
872
  setMode('help');
794
873
  return;
795
874
  }
875
+ // Show insights
876
+ if (input === 'I') {
877
+ setMode('insights');
878
+ return;
879
+ }
796
880
  // Show calendar
797
881
  if (input === 'C') {
798
882
  setMode('calendar');
@@ -818,6 +902,21 @@ function AppContent({ onOpenSettings }) {
818
902
  setMode('set-context');
819
903
  return;
820
904
  }
905
+ if (input === 'g') {
906
+ toggleTaskFocus();
907
+ return;
908
+ }
909
+ if (input === 'G') {
910
+ toggleFocusFilter();
911
+ return;
912
+ }
913
+ if (input === 'E') {
914
+ if (getCurrentTask()) {
915
+ setEffortSelectIndex(0);
916
+ setMode('set-effort');
917
+ }
918
+ return;
919
+ }
821
920
  // Settings: Theme selector
822
921
  if (input === 'T') {
823
922
  onOpenSettings('theme-select');
@@ -1055,11 +1154,15 @@ function AppContent({ onOpenSettings }) {
1055
1154
  });
1056
1155
  return;
1057
1156
  }
1058
- }, { isActive: mode !== 'help' && mode !== 'calendar' });
1157
+ }, { isActive: mode !== 'help' && mode !== 'insights' && mode !== 'calendar' });
1059
1158
  // Help modal overlay
1060
1159
  if (mode === 'help') {
1061
1160
  return (_jsx(Box, { flexDirection: "column", padding: 1, children: _jsx(HelpModal, { onClose: () => setMode('normal') }) }));
1062
1161
  }
1162
+ // Insights modal overlay
1163
+ if (mode === 'insights') {
1164
+ return (_jsx(Box, { flexDirection: "column", padding: 1, children: _jsx(InsightsModal, { onClose: () => setMode('normal') }) }));
1165
+ }
1063
1166
  // Calendar modal overlay
1064
1167
  if (mode === 'calendar') {
1065
1168
  return (_jsx(Box, { flexDirection: "column", padding: 1, children: _jsx(CalendarModal, { onClose: () => setMode('normal') }) }));
@@ -1080,12 +1183,12 @@ function AppContent({ onOpenSettings }) {
1080
1183
  const tursoEnabled = isTursoEnabled();
1081
1184
  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.primary, children: formatTitle(i18n.tui.title) }), _jsx(Text, { color: theme.colors.textMuted, children: theme.name === 'modern' ? ` v${VERSION}` : ` VER ${VERSION}` }), _jsx(Text, { color: tursoEnabled ? theme.colors.accent : theme.colors.textMuted, children: theme.name === 'modern'
1082
1185
  ? (tursoEnabled ? ' ☁️ turso' : ' 💾 local')
1083
- : (tursoEnabled ? ' [DB]TURSO' : ' [DB]local') }), contextFilter !== null && (_jsxs(Text, { color: theme.colors.accent, children: [' ', "@", contextFilter === '' ? (i18n.tui.context?.none || 'none') : contextFilter] }))] }), _jsxs(Box, { children: [_jsx(CalendarEvents, { compact: true, showLabel: true, withSeparator: true }), _jsx(Clock, {}), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.helpHint] })] })] }), pomodoro.isRunning && (_jsx(Box, { marginBottom: 1, children: _jsx(PomodoroTimer, { state: pomodoro.state, remainingSeconds: pomodoro.remainingSeconds, isPaused: pomodoro.isPaused }) })), _jsx(Box, { marginBottom: 1, children: TABS.map((tab, index) => {
1186
+ : (tursoEnabled ? ' [DB]TURSO' : ' [DB]local') }), contextFilter !== null && (_jsxs(Text, { color: theme.colors.accent, children: [' ', "@", contextFilter === '' ? (i18n.tui.context?.none || '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, {}), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.helpHint] })] })] }), pomodoro.isRunning && (_jsx(Box, { marginBottom: 1, children: _jsx(PomodoroTimer, { state: pomodoro.state, remainingSeconds: pomodoro.remainingSeconds, isPaused: pomodoro.isPaused }) })), _jsx(Box, { marginBottom: 1, children: TABS.map((tab, index) => {
1084
1187
  const isActive = index === currentListIndex && mode !== 'project-detail';
1085
1188
  const count = tasks[tab].length;
1086
1189
  const label = `${index + 1}:${getTabLabel(tab)}(${count})`;
1087
1190
  return (_jsx(Box, { marginRight: 1, children: _jsx(Text, { color: isActive ? theme.colors.textSelected : theme.colors.textMuted, bold: isActive, inverse: isActive && theme.style.tabActiveInverse, children: formatTabLabel(` ${label} `, isActive) }) }, tab));
1088
- }) }), mode === 'project-detail' && selectedProject && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: theme.colors.accent, bold: true, children: [theme.name === 'modern' ? '📁 ' : '>> ', selectedProject.title] }), _jsxs(Text, { color: theme.colors.textMuted, children: [" (Esc/b: ", i18n.tui.back || 'back', ")"] })] }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.projectTasks || 'Tasks', " (", projectTasks.length, ")"] }) })] })), (mode === 'task-detail' || mode === 'add-comment') && selectedTask && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: theme.colors.accent, bold: true, children: [theme.name === 'modern' ? '📋 ' : '>> ', i18n.tui.taskDetailTitle || 'Task Details'] }), _jsxs(Text, { color: theme.colors.textMuted, children: [" (Esc/b: ", i18n.tui.back || 'back', ", ", i18n.tui.commentHint || 'i: add comment', ")"] })] }), _jsxs(Box, { flexDirection: "column", borderStyle: theme.borders.list, borderColor: theme.colors.border, paddingX: 1, paddingY: 1, marginBottom: 1, 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, { marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailStatus, ": "] }), _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, { marginBottom: 1, children: _jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.comments || 'Comments', " (", taskComments.length, ")"] }) }), _jsx(Box, { flexDirection: "column", borderStyle: theme.borders.list, borderColor: theme.colors.border, paddingX: 1, paddingY: 1, minHeight: 5, children: taskComments.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noComments || 'No comments yet' })) : (taskComments.map((comment, index) => {
1191
+ }) }), mode === 'project-detail' && selectedProject && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: theme.colors.accent, bold: true, children: [theme.name === 'modern' ? '📁 ' : '>> ', selectedProject.title] }), _jsxs(Text, { color: theme.colors.textMuted, children: [" (Esc/b: ", i18n.tui.back || 'back', ")"] })] }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.projectTasks || 'Tasks', " (", projectTasks.length, ")"] }) })] })), (mode === 'task-detail' || mode === 'add-comment') && selectedTask && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: theme.colors.accent, bold: true, children: [theme.name === 'modern' ? '📋 ' : '>> ', i18n.tui.taskDetailTitle || 'Task Details'] }), _jsxs(Text, { color: theme.colors.textMuted, children: [" (Esc/b: ", i18n.tui.back || 'back', ", ", i18n.tui.commentHint || 'i: add comment', ")"] })] }), _jsxs(Box, { flexDirection: "column", borderStyle: theme.borders.list, borderColor: theme.colors.border, paddingX: 1, paddingY: 1, marginBottom: 1, 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, { marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailStatus, ": "] }), _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, { marginBottom: 1, children: _jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.comments || 'Comments', " (", taskComments.length, ")"] }) }), _jsx(Box, { flexDirection: "column", borderStyle: theme.borders.list, borderColor: theme.colors.border, paddingX: 1, paddingY: 1, minHeight: 5, children: taskComments.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noComments || 'No comments yet' })) : (taskComments.map((comment, index) => {
1089
1192
  const isSelected = index === selectedCommentIndex && mode === 'task-detail';
1090
1193
  return (_jsxs(Box, { flexDirection: "row", marginBottom: 1, children: [_jsx(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.textMuted, children: isSelected ? theme.style.selectedPrefix : theme.style.unselectedPrefix }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.textMuted, children: ["[", comment.createdAt.toLocaleString(), "]"] }), _jsx(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: comment.content })] })] }, comment.id));
1091
1194
  })) }), mode === 'add-comment' && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.addComment || 'New comment: ' }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: "" }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] }))] })), mode === 'search' && searchQuery && (_jsx(SearchResults, { results: searchResults, selectedIndex: searchResultIndex, query: searchQuery })), mode !== 'task-detail' && mode !== 'add-comment' && mode !== 'search' && (theme.uiStyle === 'titled-box' ? (_jsx(TitledBox, { title: mode === 'project-detail' && selectedProject ? selectedProject.title : getTabLabel(currentTab), borderColor: theme.colors.border, minHeight: 10, children: currentTasks.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noTasks })) : (currentTasks.map((task, index) => {
@@ -1118,7 +1221,7 @@ function AppContent({ onOpenSettings }) {
1118
1221
  const isActive = (ctx === 'clear' && !currentContext) ||
1119
1222
  (ctx !== 'clear' && ctx !== 'new' && currentContext === ctx);
1120
1223
  return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? theme.style.selectedPrefix : theme.style.unselectedPrefix, label, isActive && ' *'] }, ctx));
1121
- }) }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.context?.setContextHelp || 'j/k: select, Enter: confirm, Esc: cancel' })] })), mode === 'add-context' && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.context?.newContext || 'New context: ' }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: i18n.tui.context?.newContextPlaceholder || 'Enter context name...' }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] })), mode === 'search' && (_jsx(SearchBar, { value: searchQuery, onChange: handleSearchChange, onSubmit: handleInputSubmit })), mode === 'confirm-delete' && taskToDelete && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.accent, bold: true, children: fmt(i18n.tui.deleteConfirm || 'Delete "{title}"? (y/n)', { title: taskToDelete.title }) }) })), message && mode === 'normal' && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.textHighlight, children: message }) })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [(mode === 'task-detail' || mode === 'add-comment') ? (theme.style.showFunctionKeys ? (_jsx(FunctionKeyBar, { keys: [
1224
+ }) }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.context?.setContextHelp || 'j/k: select, Enter: confirm, Esc: cancel' })] })), mode === 'set-effort' && (_jsxs(Box, { flexDirection: "column", borderStyle: theme.borders.modal, borderColor: theme.colors.borderActive, paddingX: 2, paddingY: 1, children: [_jsx(Text, { bold: true, color: theme.colors.accent, children: i18n.tui.effort?.set || 'Set effort' }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.effort?.setHelp || 'j/k: select, Enter: confirm, Esc: cancel' }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: EFFORT_OPTIONS.map((option, index) => (_jsxs(Text, { color: index === effortSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === effortSelectIndex, children: [index === effortSelectIndex ? theme.style.selectedPrefix : theme.style.unselectedPrefix, option.label] }, option.label))) })] })), mode === 'add-context' && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.context?.newContext || 'New context: ' }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: i18n.tui.context?.newContextPlaceholder || 'Enter context name...' }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] })), mode === 'search' && (_jsx(SearchBar, { value: searchQuery, onChange: handleSearchChange, onSubmit: handleInputSubmit })), mode === 'confirm-delete' && taskToDelete && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.accent, bold: true, children: fmt(i18n.tui.deleteConfirm || 'Delete "{title}"? (y/n)', { title: taskToDelete.title }) }) })), message && mode === 'normal' && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.textHighlight, children: message }) })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [(mode === 'task-detail' || mode === 'add-comment') ? (theme.style.showFunctionKeys ? (_jsx(FunctionKeyBar, { keys: [
1122
1225
  { key: 'i', label: i18n.tui.keyBar.comment },
1123
1226
  { key: 'd', label: i18n.tui.keyBar.delete },
1124
1227
  { key: 'P', label: i18n.tui.keyBar.project },