floq 1.4.1 → 1.5.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.
Files changed (49) hide show
  1. package/dist/commands/done.js +1 -0
  2. package/dist/commands/insights.js +13 -10
  3. package/dist/commands/move.js +2 -1
  4. package/dist/db/index.js +36 -0
  5. package/dist/db/schema.d.ts +116 -0
  6. package/dist/db/schema.js +8 -0
  7. package/dist/i18n/en.d.ts +2 -0
  8. package/dist/i18n/en.js +1 -0
  9. package/dist/i18n/ja.js +1 -0
  10. package/dist/ui/App.js +7 -4
  11. package/dist/ui/components/GtdDQ.js +4 -1
  12. package/dist/ui/components/GtdMario.js +4 -1
  13. package/dist/ui/components/InsightsModal.js +13 -7
  14. package/dist/ui/components/KanbanBoard.js +3 -0
  15. package/dist/ui/components/KanbanDQ.js +2 -0
  16. package/dist/ui/components/KanbanMario.js +2 -0
  17. package/dist/ui/components/TaskItem.d.ts +2 -1
  18. package/dist/ui/components/TaskItem.js +4 -3
  19. package/dist/ui/history/HistoryContext.js +8 -0
  20. package/dist/ui/history/HistoryManager.d.ts +9 -1
  21. package/dist/ui/history/HistoryManager.js +140 -16
  22. package/dist/ui/history/commands/ConvertToProjectCommand.d.ts +5 -1
  23. package/dist/ui/history/commands/ConvertToProjectCommand.js +13 -0
  24. package/dist/ui/history/commands/CreateCommentCommand.d.ts +5 -1
  25. package/dist/ui/history/commands/CreateCommentCommand.js +22 -0
  26. package/dist/ui/history/commands/CreateTaskCommand.d.ts +5 -1
  27. package/dist/ui/history/commands/CreateTaskCommand.js +26 -1
  28. package/dist/ui/history/commands/DeleteCommentCommand.d.ts +5 -1
  29. package/dist/ui/history/commands/DeleteCommentCommand.js +22 -0
  30. package/dist/ui/history/commands/DeleteTaskCommand.d.ts +7 -2
  31. package/dist/ui/history/commands/DeleteTaskCommand.js +41 -0
  32. package/dist/ui/history/commands/LinkTaskCommand.d.ts +5 -1
  33. package/dist/ui/history/commands/LinkTaskCommand.js +14 -0
  34. package/dist/ui/history/commands/MoveTaskCommand.d.ts +7 -1
  35. package/dist/ui/history/commands/MoveTaskCommand.js +25 -0
  36. package/dist/ui/history/commands/SetContextCommand.d.ts +5 -1
  37. package/dist/ui/history/commands/SetContextCommand.js +14 -0
  38. package/dist/ui/history/commands/SetEffortCommand.d.ts +5 -1
  39. package/dist/ui/history/commands/SetEffortCommand.js +14 -0
  40. package/dist/ui/history/commands/SetFocusCommand.d.ts +5 -1
  41. package/dist/ui/history/commands/SetFocusCommand.js +14 -0
  42. package/dist/ui/history/commands/index.d.ts +1 -0
  43. package/dist/ui/history/commands/index.js +1 -0
  44. package/dist/ui/history/commands/registry.d.ts +2 -0
  45. package/dist/ui/history/commands/registry.js +28 -0
  46. package/dist/ui/history/index.d.ts +2 -2
  47. package/dist/ui/history/index.js +1 -1
  48. package/dist/ui/history/types.d.ts +9 -0
  49. package/package.json +1 -1
@@ -28,6 +28,7 @@ export async function markDone(taskId) {
28
28
  await db.update(schema.tasks)
29
29
  .set({
30
30
  status: 'done',
31
+ completedAt: new Date(),
31
32
  updatedAt: new Date(),
32
33
  })
33
34
  .where(eq(schema.tasks.id, task.id));
@@ -1,4 +1,4 @@
1
- import { eq, and, gte } from 'drizzle-orm';
1
+ import { eq, and } from 'drizzle-orm';
2
2
  import { getDb, schema } from '../db/index.js';
3
3
  import { t, fmt } from '../i18n/index.js';
4
4
  function getWeekStart(date) {
@@ -53,8 +53,8 @@ function groupByWeek(tasks, weeks) {
53
53
  const weekEnd = new Date(weekStart);
54
54
  weekEnd.setDate(weekEnd.getDate() + 7);
55
55
  const weekTasks = tasks.filter(task => {
56
- const updated = task.updatedAt;
57
- return updated >= weekStart && updated < weekEnd;
56
+ const completionDate = task.completedAt ?? task.updatedAt;
57
+ return completionDate >= weekStart && completionDate < weekEnd;
58
58
  });
59
59
  result.push({
60
60
  weekStart,
@@ -69,7 +69,7 @@ function groupByDayOfWeek(tasks) {
69
69
  for (let i = 0; i < 7; i++)
70
70
  days.set(i, 0);
71
71
  for (const task of tasks) {
72
- const day = task.updatedAt.getDay();
72
+ const day = (task.completedAt ?? task.updatedAt).getDay();
73
73
  days.set(day, (days.get(day) || 0) + 1);
74
74
  }
75
75
  return days;
@@ -95,9 +95,9 @@ function calculateAverageCompletionDays(tasks) {
95
95
  let validCount = 0;
96
96
  for (const task of tasks) {
97
97
  const created = task.createdAt.getTime();
98
- const updated = task.updatedAt.getTime();
99
- if (updated > created) {
100
- totalMs += updated - created;
98
+ const completed = (task.completedAt ?? task.updatedAt).getTime();
99
+ if (completed > created) {
100
+ totalMs += completed - created;
101
101
  validCount++;
102
102
  }
103
103
  }
@@ -138,11 +138,14 @@ export async function showInsights(weeks) {
138
138
  startDate.setDate(startDate.getDate() - (weeks - 1) * 7);
139
139
  const endDate = new Date(now);
140
140
  endDate.setHours(23, 59, 59, 999);
141
- // Query all completed tasks in the period
142
- const completedTasks = await db
141
+ // Query completed tasks (use completedAt with updatedAt fallback)
142
+ const completedTasks = (await db
143
143
  .select()
144
144
  .from(schema.tasks)
145
- .where(and(eq(schema.tasks.status, 'done'), gte(schema.tasks.updatedAt, startDate), eq(schema.tasks.isProject, false)));
145
+ .where(and(eq(schema.tasks.status, 'done'), eq(schema.tasks.isProject, false)))).filter(task => {
146
+ const completionDate = task.completedAt ?? task.updatedAt;
147
+ return completionDate >= startDate;
148
+ });
146
149
  // Header
147
150
  console.log();
148
151
  console.log(l.title);
@@ -4,7 +4,7 @@ import { t, fmt } from '../i18n/index.js';
4
4
  export async function moveTask(taskId, targetStatus, waitingFor) {
5
5
  const db = getDb();
6
6
  const i18n = t();
7
- const validStatuses = ['inbox', 'next', 'waiting', 'someday'];
7
+ const validStatuses = ['inbox', 'next', 'waiting', 'someday', 'done'];
8
8
  if (!validStatuses.includes(targetStatus)) {
9
9
  console.error(fmt(i18n.commands.move.invalidStatus, { status: targetStatus }));
10
10
  console.error(fmt(i18n.commands.move.validStatuses, { statuses: validStatuses.join(', ') }));
@@ -35,6 +35,7 @@ export async function moveTask(taskId, targetStatus, waitingFor) {
35
35
  .set({
36
36
  status: targetStatus,
37
37
  waitingFor: targetStatus === 'waiting' ? waitingFor : null,
38
+ completedAt: targetStatus === 'done' ? new Date() : null,
38
39
  updatedAt: new Date(),
39
40
  })
40
41
  .where(eq(schema.tasks.id, task.id));
package/dist/db/index.js CHANGED
@@ -26,6 +26,7 @@ async function initializeRemoteSchema(tursoUrl, authToken) {
26
26
  const hasContext = tableInfo.some(col => col.name === 'context');
27
27
  const hasIsFocused = tableInfo.some(col => col.name === 'is_focused');
28
28
  const hasEffort = tableInfo.some(col => col.name === 'effort');
29
+ const hasCompletedAt = tableInfo.some(col => col.name === 'completed_at');
29
30
  if (!tableExists) {
30
31
  // Fresh install: create new schema on remote
31
32
  await remoteClient.execute(`
@@ -41,6 +42,7 @@ async function initializeRemoteSchema(tursoUrl, authToken) {
41
42
  is_focused INTEGER NOT NULL DEFAULT 0,
42
43
  effort TEXT,
43
44
  due_date INTEGER,
45
+ completed_at INTEGER,
44
46
  created_at INTEGER NOT NULL,
45
47
  updated_at INTEGER NOT NULL
46
48
  )
@@ -63,6 +65,11 @@ async function initializeRemoteSchema(tursoUrl, authToken) {
63
65
  if (tableExists && !hasEffort) {
64
66
  await remoteClient.execute("ALTER TABLE tasks ADD COLUMN effort TEXT");
65
67
  }
68
+ // Migration: add completed_at column if missing
69
+ if (tableExists && !hasCompletedAt) {
70
+ await remoteClient.execute("ALTER TABLE tasks ADD COLUMN completed_at INTEGER");
71
+ await remoteClient.execute("UPDATE tasks SET completed_at = updated_at WHERE status = 'done'");
72
+ }
66
73
  // Create comments table
67
74
  await remoteClient.execute(`
68
75
  CREATE TABLE IF NOT EXISTS comments (
@@ -86,6 +93,17 @@ async function initializeRemoteSchema(tursoUrl, authToken) {
86
93
  updated_at INTEGER NOT NULL
87
94
  )
88
95
  `);
96
+ // Create operation_history table for crash-safe undo
97
+ await remoteClient.execute(`
98
+ CREATE TABLE IF NOT EXISTS operation_history (
99
+ id TEXT PRIMARY KEY,
100
+ command_type TEXT NOT NULL,
101
+ command_data TEXT NOT NULL,
102
+ executed_at INTEGER NOT NULL,
103
+ is_undone INTEGER NOT NULL DEFAULT 0
104
+ )
105
+ `);
106
+ await remoteClient.execute("CREATE INDEX IF NOT EXISTS idx_operation_history_executed_at ON operation_history(executed_at)");
89
107
  }
90
108
  finally {
91
109
  remoteClient.close();
@@ -102,6 +120,7 @@ async function initializeLocalSchema() {
102
120
  const hasContext = tableInfo.some(col => col.name === 'context');
103
121
  const hasIsFocused = tableInfo.some(col => col.name === 'is_focused');
104
122
  const hasEffort = tableInfo.some(col => col.name === 'effort');
123
+ const hasCompletedAt = tableInfo.some(col => col.name === 'completed_at');
105
124
  const tableExists = tableInfo.length > 0;
106
125
  if (tableExists && hasProjectId && !hasIsProject) {
107
126
  // Migration: old schema -> new schema
@@ -136,6 +155,7 @@ async function initializeLocalSchema() {
136
155
  is_focused INTEGER NOT NULL DEFAULT 0,
137
156
  effort TEXT,
138
157
  due_date INTEGER,
158
+ completed_at INTEGER,
139
159
  created_at INTEGER NOT NULL,
140
160
  updated_at INTEGER NOT NULL
141
161
  )
@@ -158,6 +178,11 @@ async function initializeLocalSchema() {
158
178
  if (tableExists && !hasEffort) {
159
179
  await client.execute("ALTER TABLE tasks ADD COLUMN effort TEXT");
160
180
  }
181
+ // Migration: add completed_at column if missing
182
+ if (tableExists && !hasCompletedAt) {
183
+ await client.execute("ALTER TABLE tasks ADD COLUMN completed_at INTEGER");
184
+ await client.execute("UPDATE tasks SET completed_at = updated_at WHERE status = 'done'");
185
+ }
161
186
  // Create comments table
162
187
  await client.execute(`
163
188
  CREATE TABLE IF NOT EXISTS comments (
@@ -181,6 +206,17 @@ async function initializeLocalSchema() {
181
206
  updated_at INTEGER NOT NULL
182
207
  )
183
208
  `);
209
+ // Create operation_history table for crash-safe undo
210
+ await client.execute(`
211
+ CREATE TABLE IF NOT EXISTS operation_history (
212
+ id TEXT PRIMARY KEY,
213
+ command_type TEXT NOT NULL,
214
+ command_data TEXT NOT NULL,
215
+ executed_at INTEGER NOT NULL,
216
+ is_undone INTEGER NOT NULL DEFAULT 0
217
+ )
218
+ `);
219
+ await client.execute("CREATE INDEX IF NOT EXISTS idx_operation_history_executed_at ON operation_history(executed_at)");
184
220
  }
185
221
  // DB 初期化
186
222
  export async function initDb() {
@@ -205,6 +205,23 @@ export declare const tasks: import("drizzle-orm/sqlite-core").SQLiteTableWithCol
205
205
  identity: undefined;
206
206
  generated: undefined;
207
207
  }, {}, {}>;
208
+ completedAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
209
+ name: "completed_at";
210
+ tableName: "tasks";
211
+ dataType: "date";
212
+ columnType: "SQLiteTimestamp";
213
+ data: Date;
214
+ driverParam: number;
215
+ notNull: false;
216
+ hasDefault: false;
217
+ isPrimaryKey: false;
218
+ isAutoincrement: false;
219
+ hasRuntimeDefault: false;
220
+ enumValues: undefined;
221
+ baseColumn: never;
222
+ identity: undefined;
223
+ generated: undefined;
224
+ }, {}, {}>;
208
225
  createdAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
209
226
  name: "created_at";
210
227
  tableName: "tasks";
@@ -482,3 +499,102 @@ export declare const pomodoroSessions: import("drizzle-orm/sqlite-core").SQLiteT
482
499
  }>;
483
500
  export type PomodoroSession = typeof pomodoroSessions.$inferSelect;
484
501
  export type NewPomodoroSession = typeof pomodoroSessions.$inferInsert;
502
+ export declare const operationHistory: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
503
+ name: "operation_history";
504
+ schema: undefined;
505
+ columns: {
506
+ id: import("drizzle-orm/sqlite-core").SQLiteColumn<{
507
+ name: "id";
508
+ tableName: "operation_history";
509
+ dataType: "string";
510
+ columnType: "SQLiteText";
511
+ data: string;
512
+ driverParam: string;
513
+ notNull: true;
514
+ hasDefault: false;
515
+ isPrimaryKey: true;
516
+ isAutoincrement: false;
517
+ hasRuntimeDefault: false;
518
+ enumValues: [string, ...string[]];
519
+ baseColumn: never;
520
+ identity: undefined;
521
+ generated: undefined;
522
+ }, {}, {
523
+ length: number | undefined;
524
+ }>;
525
+ commandType: import("drizzle-orm/sqlite-core").SQLiteColumn<{
526
+ name: "command_type";
527
+ tableName: "operation_history";
528
+ dataType: "string";
529
+ columnType: "SQLiteText";
530
+ data: string;
531
+ driverParam: string;
532
+ notNull: true;
533
+ hasDefault: false;
534
+ isPrimaryKey: false;
535
+ isAutoincrement: false;
536
+ hasRuntimeDefault: false;
537
+ enumValues: [string, ...string[]];
538
+ baseColumn: never;
539
+ identity: undefined;
540
+ generated: undefined;
541
+ }, {}, {
542
+ length: number | undefined;
543
+ }>;
544
+ commandData: import("drizzle-orm/sqlite-core").SQLiteColumn<{
545
+ name: "command_data";
546
+ tableName: "operation_history";
547
+ dataType: "string";
548
+ columnType: "SQLiteText";
549
+ data: string;
550
+ driverParam: string;
551
+ notNull: true;
552
+ hasDefault: false;
553
+ isPrimaryKey: false;
554
+ isAutoincrement: false;
555
+ hasRuntimeDefault: false;
556
+ enumValues: [string, ...string[]];
557
+ baseColumn: never;
558
+ identity: undefined;
559
+ generated: undefined;
560
+ }, {}, {
561
+ length: number | undefined;
562
+ }>;
563
+ executedAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
564
+ name: "executed_at";
565
+ tableName: "operation_history";
566
+ dataType: "date";
567
+ columnType: "SQLiteTimestamp";
568
+ data: Date;
569
+ driverParam: number;
570
+ notNull: true;
571
+ hasDefault: false;
572
+ isPrimaryKey: false;
573
+ isAutoincrement: false;
574
+ hasRuntimeDefault: false;
575
+ enumValues: undefined;
576
+ baseColumn: never;
577
+ identity: undefined;
578
+ generated: undefined;
579
+ }, {}, {}>;
580
+ isUndone: import("drizzle-orm/sqlite-core").SQLiteColumn<{
581
+ name: "is_undone";
582
+ tableName: "operation_history";
583
+ dataType: "boolean";
584
+ columnType: "SQLiteBoolean";
585
+ data: boolean;
586
+ driverParam: number;
587
+ notNull: true;
588
+ hasDefault: true;
589
+ isPrimaryKey: false;
590
+ isAutoincrement: false;
591
+ hasRuntimeDefault: false;
592
+ enumValues: undefined;
593
+ baseColumn: never;
594
+ identity: undefined;
595
+ generated: undefined;
596
+ }, {}, {}>;
597
+ };
598
+ dialect: "sqlite";
599
+ }>;
600
+ export type OperationHistory = typeof operationHistory.$inferSelect;
package/dist/db/schema.js CHANGED
@@ -11,6 +11,7 @@ export const tasks = sqliteTable('tasks', {
11
11
  isFocused: integer('is_focused', { mode: 'boolean' }).notNull().default(false),
12
12
  effort: text('effort'),
13
13
  dueDate: integer('due_date', { mode: 'timestamp' }),
14
+ completedAt: integer('completed_at', { mode: 'timestamp' }),
14
15
  createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
15
16
  updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
16
17
  });
@@ -32,3 +33,10 @@ export const pomodoroSessions = sqliteTable('pomodoro_sessions', {
32
33
  completedCount: integer('completed_count').notNull().default(0),
33
34
  updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
34
35
  });
36
+ export const operationHistory = sqliteTable('operation_history', {
37
+ id: text('id').primaryKey(),
38
+ commandType: text('command_type').notNull(),
39
+ commandData: text('command_data').notNull(), // JSON serialized
40
+ executedAt: integer('executed_at', { mode: 'timestamp' }).notNull(),
41
+ isUndone: integer('is_undone', { mode: 'boolean' }).notNull().default(false),
42
+ });
package/dist/i18n/en.d.ts CHANGED
@@ -243,6 +243,7 @@ export declare const en: {
243
243
  taskDetailTitle: string;
244
244
  taskDetailFooter: string;
245
245
  taskDetailStatus: string;
246
+ taskDetailCompletedAt: string;
246
247
  deleteConfirm: string;
247
248
  deleted: string;
248
249
  deleteCancelled: string;
@@ -670,6 +671,7 @@ export type TuiTranslations = {
670
671
  taskDetailTitle: string;
671
672
  taskDetailFooter: string;
672
673
  taskDetailStatus: string;
674
+ taskDetailCompletedAt?: string;
673
675
  deleteConfirm: string;
674
676
  deleted: string;
675
677
  deleteCancelled: string;
package/dist/i18n/en.js CHANGED
@@ -256,6 +256,7 @@ export const en = {
256
256
  taskDetailTitle: 'Task Details',
257
257
  taskDetailFooter: 'j/k=select i=comment d=delete P=link b/Esc=back',
258
258
  taskDetailStatus: 'Status',
259
+ taskDetailCompletedAt: 'Completed',
259
260
  deleteConfirm: 'Delete "{title}"? (y/n)',
260
261
  deleted: 'Deleted: "{title}"',
261
262
  deleteCancelled: 'Delete cancelled',
package/dist/i18n/ja.js CHANGED
@@ -256,6 +256,7 @@ export const ja = {
256
256
  taskDetailTitle: 'タスク詳細',
257
257
  taskDetailFooter: 'j/k=選択 i=コメント d=削除 P=紐づけ b/Esc=戻る',
258
258
  taskDetailStatus: 'ステータス',
259
+ taskDetailCompletedAt: '完了日',
259
260
  deleteConfirm: '「{title}」を削除しますか? (y/n)',
260
261
  deleted: '削除しました: 「{title}」',
261
262
  deleteCancelled: '削除をキャンセルしました',
package/dist/ui/App.js CHANGED
@@ -407,6 +407,7 @@ function AppContent({ onOpenSettings }) {
407
407
  toStatus: 'done',
408
408
  fromWaitingFor: task.waitingFor,
409
409
  toWaitingFor: null,
410
+ fromCompletedAt: task.completedAt,
410
411
  description: fmt(i18n.tui.completed, { title: task.title }),
411
412
  });
412
413
  await history.execute(command);
@@ -420,6 +421,7 @@ function AppContent({ onOpenSettings }) {
420
421
  toStatus: status,
421
422
  fromWaitingFor: task.waitingFor,
422
423
  toWaitingFor: null,
424
+ fromCompletedAt: task.completedAt,
423
425
  description: fmt(i18n.tui.movedTo, { title: task.title, status: i18n.status[status] }),
424
426
  });
425
427
  await history.execute(command);
@@ -433,6 +435,7 @@ function AppContent({ onOpenSettings }) {
433
435
  toStatus: 'waiting',
434
436
  fromWaitingFor: task.waitingFor,
435
437
  toWaitingFor: waitingFor.trim(),
438
+ fromCompletedAt: task.completedAt,
436
439
  description: fmt(i18n.tui.movedToWaiting, { title: task.title, person: waitingFor.trim() }),
437
440
  });
438
441
  await history.execute(command);
@@ -1109,7 +1112,7 @@ function AppContent({ onOpenSettings }) {
1109
1112
  return;
1110
1113
  }
1111
1114
  // Move to inbox
1112
- if (input === 'i' && currentTasks.length > 0 && currentTab !== 'inbox' && currentTab !== 'projects' && currentTab !== 'done') {
1115
+ if (input === 'i' && currentTasks.length > 0 && currentTab !== 'inbox' && currentTab !== 'projects') {
1113
1116
  const task = currentTasks[selectedTaskIndex];
1114
1117
  moveTaskToStatus(task, 'inbox').then(() => {
1115
1118
  if (selectedTaskIndex >= currentTasks.length - 1) {
@@ -1188,17 +1191,17 @@ function AppContent({ onOpenSettings }) {
1188
1191
  const count = tasks[tab].length;
1189
1192
  const label = `${index + 1}:${getTabLabel(tab)}(${count})`;
1190
1193
  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));
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) => {
1194
+ }) }), 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 ? '★' : '-' })] }), 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, { 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) => {
1192
1195
  const isSelected = index === selectedCommentIndex && mode === 'task-detail';
1193
1196
  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));
1194
1197
  })) }), 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) => {
1195
1198
  const parentProject = getParentProject(task.parentId);
1196
1199
  const progress = currentTab === 'projects' ? projectProgress[task.id] : undefined;
1197
- return (_jsx(TaskItem, { task: task, isSelected: index === selectedTaskIndex, projectName: parentProject?.title, progress: progress }, task.id));
1200
+ return (_jsx(TaskItem, { task: task, isSelected: index === selectedTaskIndex, projectName: parentProject?.title, progress: progress, showStatus: mode === 'project-detail' }, task.id));
1198
1201
  })) })) : (_jsx(Box, { flexDirection: "column", borderStyle: theme.borders.list, borderColor: theme.colors.border, paddingX: 1, paddingY: 1, minHeight: 10, children: currentTasks.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noTasks })) : (currentTasks.map((task, index) => {
1199
1202
  const parentProject = getParentProject(task.parentId);
1200
1203
  const progress = currentTab === 'projects' ? projectProgress[task.id] : undefined;
1201
- return (_jsx(TaskItem, { task: task, isSelected: index === selectedTaskIndex, projectName: parentProject?.title, progress: progress }, task.id));
1204
+ return (_jsx(TaskItem, { task: task, isSelected: index === selectedTaskIndex, projectName: parentProject?.title, progress: progress, showStatus: mode === 'project-detail' }, task.id));
1202
1205
  })) }))), (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
1203
1206
  ? `${i18n.tui.newTask}[${selectedProject.title}] `
1204
1207
  : i18n.tui.newTask }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: i18n.tui.placeholder }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] })), 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: "" }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] })), mode === 'select-project' && taskToLink && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project for', ": ", taskToLink.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: theme.borders.list, borderColor: theme.colors.borderActive, paddingX: 1, children: tasks.projects.map((project, index) => (_jsxs(Text, { color: index === projectSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === projectSelectIndex, children: [index === projectSelectIndex ? theme.style.selectedPrefix : theme.style.unselectedPrefix, project.title] }, project.id))) }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.selectProjectHelp || 'j/k: select, Enter: confirm, Esc: cancel' })] })), mode === 'context-filter' && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.context?.filter || 'Filter by context' }), _jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: theme.borders.list, borderColor: theme.colors.borderActive, paddingX: 1, children: ['all', 'none', ...availableContexts].map((ctx, index) => {
@@ -347,6 +347,7 @@ export function GtdDQ({ onOpenSettings }) {
347
347
  toStatus: 'done',
348
348
  fromWaitingFor: task.waitingFor,
349
349
  toWaitingFor: null,
350
+ fromCompletedAt: task.completedAt,
350
351
  description: fmt(i18n.tui.completed, { title: task.title }),
351
352
  });
352
353
  await history.execute(command);
@@ -360,6 +361,7 @@ export function GtdDQ({ onOpenSettings }) {
360
361
  toStatus: status,
361
362
  fromWaitingFor: task.waitingFor,
362
363
  toWaitingFor: null,
364
+ fromCompletedAt: task.completedAt,
363
365
  description: fmt(i18n.tui.movedTo, { title: task.title, status: i18n.status[status] }),
364
366
  });
365
367
  await history.execute(command);
@@ -373,6 +375,7 @@ export function GtdDQ({ onOpenSettings }) {
373
375
  toStatus: 'waiting',
374
376
  fromWaitingFor: task.waitingFor,
375
377
  toWaitingFor: waitingFor.trim(),
378
+ fromCompletedAt: task.completedAt,
376
379
  description: fmt(i18n.tui.movedToWaiting, { title: task.title, person: waitingFor.trim() }),
377
380
  });
378
381
  await history.execute(command);
@@ -1176,7 +1179,7 @@ export function GtdDQ({ onOpenSettings }) {
1176
1179
  }) })] })) : 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) => {
1177
1180
  const label = ctx === 'clear' ? (i18n.tui.context?.none || 'Clear context') : ctx === 'new' ? (i18n.tui.context?.addNew || 'New context...') : `@${ctx}`;
1178
1181
  return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? '▶ ' : ' ', label] }, ctx));
1179
- }) })] })) : 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) => {
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) => {
1180
1183
  const isSelected = index === selectedCommentIndex && mode === 'task-detail';
1181
1184
  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));
1182
1185
  })) }) })] })) : (_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) => {
@@ -293,6 +293,7 @@ export function GtdMario({ onOpenSettings }) {
293
293
  toStatus: 'done',
294
294
  fromWaitingFor: task.waitingFor,
295
295
  toWaitingFor: null,
296
+ fromCompletedAt: task.completedAt,
296
297
  description: fmt(i18n.tui.completed, { title: task.title }),
297
298
  });
298
299
  await history.execute(command);
@@ -306,6 +307,7 @@ export function GtdMario({ onOpenSettings }) {
306
307
  toStatus: status,
307
308
  fromWaitingFor: task.waitingFor,
308
309
  toWaitingFor: null,
310
+ fromCompletedAt: task.completedAt,
309
311
  description: fmt(i18n.tui.movedTo, { title: task.title, status: i18n.status[status] }),
310
312
  });
311
313
  await history.execute(command);
@@ -319,6 +321,7 @@ export function GtdMario({ onOpenSettings }) {
319
321
  toStatus: 'waiting',
320
322
  fromWaitingFor: task.waitingFor,
321
323
  toWaitingFor: waitingFor.trim(),
324
+ fromCompletedAt: task.completedAt,
322
325
  description: fmt(i18n.tui.movedToWaiting, { title: task.title, person: waitingFor.trim() }),
323
326
  });
324
327
  await history.execute(command);
@@ -1070,7 +1073,7 @@ export function GtdMario({ onOpenSettings }) {
1070
1073
  }) })] })) : 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) => {
1071
1074
  const label = ctx === 'clear' ? (i18n.tui.context?.none || 'Clear context') : ctx === 'new' ? (i18n.tui.context?.addNew || 'New context...') : `@${ctx}`;
1072
1075
  return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? '🍄 ' : ' ', label] }, ctx));
1073
- }) })] })) : 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) => {
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) => {
1074
1077
  const isSelected = index === selectedCommentIndex && mode === 'task-detail';
1075
1078
  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));
1076
1079
  })) }) })] })) : (_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) => {
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useState, useEffect, useMemo } from 'react';
3
3
  import { Box, Text, useInput } from 'ink';
4
- import { eq, and, gte } from 'drizzle-orm';
4
+ import { eq, and } from 'drizzle-orm';
5
5
  import { getDb, schema } from '../../db/index.js';
6
6
  import { t, fmt } from '../../i18n/index.js';
7
7
  import { useTheme } from '../theme/index.js';
@@ -80,11 +80,14 @@ export function InsightsModal({ onClose }) {
80
80
  const now = new Date();
81
81
  const startDate = getWeekStart(now);
82
82
  startDate.setDate(startDate.getDate() - (weeks - 1) * 7);
83
- // Query completed tasks
84
- const completedTasks = await db
83
+ // Query completed tasks (use completedAt with updatedAt fallback)
84
+ const completedTasks = (await db
85
85
  .select()
86
86
  .from(schema.tasks)
87
- .where(and(eq(schema.tasks.status, 'done'), gte(schema.tasks.updatedAt, startDate), eq(schema.tasks.isProject, false)));
87
+ .where(and(eq(schema.tasks.status, 'done'), eq(schema.tasks.isProject, false)))).filter(task => {
88
+ const completionDate = task.completedAt ?? task.updatedAt;
89
+ return completionDate >= startDate;
90
+ });
88
91
  // Period header
89
92
  lines.push({ type: 'text', value: `${l.period}: ${startDate.toLocaleDateString()} ~ ${now.toLocaleDateString()}` });
90
93
  lines.push({ type: 'spacer', value: '' });
@@ -101,7 +104,10 @@ export function InsightsModal({ onClose }) {
101
104
  ws.setDate(ws.getDate() - i * 7);
102
105
  const we = new Date(ws);
103
106
  we.setDate(we.getDate() + 7);
104
- const weekTasks = completedTasks.filter(t => t.updatedAt >= ws && t.updatedAt < we);
107
+ const weekTasks = completedTasks.filter(t => {
108
+ const d = t.completedAt ?? t.updatedAt;
109
+ return d >= ws && d < we;
110
+ });
105
111
  const weekLabel = fmt(l.weekLabel, { date: ws.toLocaleDateString() });
106
112
  const countLabel = fmt(l.tasksCompleted, { count: weekTasks.length });
107
113
  lines.push({ type: 'text', value: `${weekLabel}: ${countLabel}` });
@@ -119,7 +125,7 @@ export function InsightsModal({ onClose }) {
119
125
  lines.push({ type: 'header', value: l.dailyBreakdown });
120
126
  const dayCounts = new Array(7).fill(0);
121
127
  for (const task of completedTasks) {
122
- dayCounts[task.updatedAt.getDay()]++;
128
+ dayCounts[(task.completedAt ?? task.updatedAt).getDay()]++;
123
129
  }
124
130
  const maxDaily = Math.max(...dayCounts);
125
131
  const dayNames = isJa
@@ -211,7 +217,7 @@ export function InsightsModal({ onClose }) {
211
217
  let totalMs = 0;
212
218
  let validCount = 0;
213
219
  for (const task of completedTasks) {
214
- const diff = task.updatedAt.getTime() - task.createdAt.getTime();
220
+ const diff = (task.completedAt ?? task.updatedAt).getTime() - task.createdAt.getTime();
215
221
  if (diff > 0) {
216
222
  totalMs += diff;
217
223
  validCount++;
@@ -306,6 +306,7 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
306
306
  toStatus: newStatus,
307
307
  fromWaitingFor: task.waitingFor,
308
308
  toWaitingFor: null,
309
+ fromCompletedAt: task.completedAt,
309
310
  description: fmt(i18n.tui.movedTo, { title: task.title, status: i18n.status[newStatus] }),
310
311
  });
311
312
  await history.execute(command);
@@ -333,6 +334,7 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
333
334
  toStatus: newStatus,
334
335
  fromWaitingFor: task.waitingFor,
335
336
  toWaitingFor: null,
337
+ fromCompletedAt: task.completedAt,
336
338
  description: fmt(i18n.tui.movedTo, { title: task.title, status: i18n.status[newStatus] }),
337
339
  });
338
340
  await history.execute(command);
@@ -346,6 +348,7 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
346
348
  toStatus: 'done',
347
349
  fromWaitingFor: task.waitingFor,
348
350
  toWaitingFor: null,
351
+ fromCompletedAt: task.completedAt,
349
352
  description: fmt(i18n.tui.completed, { title: task.title }),
350
353
  });
351
354
  await history.execute(command);
@@ -276,6 +276,7 @@ export function KanbanDQ({ onOpenSettings }) {
276
276
  toStatus: newStatus,
277
277
  fromWaitingFor: task.waitingFor,
278
278
  toWaitingFor: null,
279
+ fromCompletedAt: task.completedAt,
279
280
  description: fmt(i18n.tui.movedTo, { title: task.title, status: i18n.status[newStatus] }),
280
281
  });
281
282
  await history.execute(command);
@@ -289,6 +290,7 @@ export function KanbanDQ({ onOpenSettings }) {
289
290
  toStatus: 'done',
290
291
  fromWaitingFor: task.waitingFor,
291
292
  toWaitingFor: null,
293
+ fromCompletedAt: task.completedAt,
292
294
  description: fmt(i18n.tui.completed, { title: task.title }),
293
295
  });
294
296
  await history.execute(command);