floq 1.4.1 → 1.6.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 (58) hide show
  1. package/README.ja.md +4 -0
  2. package/README.md +4 -0
  3. package/dist/cli.js +32 -2
  4. package/dist/commands/config.js +2 -1
  5. package/dist/commands/done.js +1 -0
  6. package/dist/commands/insights.js +13 -10
  7. package/dist/commands/move.js +2 -1
  8. package/dist/config.d.ts +3 -0
  9. package/dist/config.js +11 -0
  10. package/dist/db/index.js +36 -0
  11. package/dist/db/schema.d.ts +116 -0
  12. package/dist/db/schema.js +8 -0
  13. package/dist/i18n/en.d.ts +2 -0
  14. package/dist/i18n/en.js +1 -0
  15. package/dist/i18n/ja.js +1 -0
  16. package/dist/index.js +2 -1
  17. package/dist/mcp/server.d.ts +1 -0
  18. package/dist/mcp/server.js +186 -0
  19. package/dist/ui/App.js +7 -4
  20. package/dist/ui/components/GtdDQ.js +4 -1
  21. package/dist/ui/components/GtdMario.js +4 -1
  22. package/dist/ui/components/InsightsModal.js +15 -8
  23. package/dist/ui/components/KanbanBoard.js +3 -0
  24. package/dist/ui/components/KanbanDQ.js +2 -0
  25. package/dist/ui/components/KanbanMario.js +2 -0
  26. package/dist/ui/components/TaskItem.d.ts +2 -1
  27. package/dist/ui/components/TaskItem.js +4 -3
  28. package/dist/ui/history/HistoryContext.js +8 -0
  29. package/dist/ui/history/HistoryManager.d.ts +9 -1
  30. package/dist/ui/history/HistoryManager.js +140 -16
  31. package/dist/ui/history/commands/ConvertToProjectCommand.d.ts +5 -1
  32. package/dist/ui/history/commands/ConvertToProjectCommand.js +13 -0
  33. package/dist/ui/history/commands/CreateCommentCommand.d.ts +5 -1
  34. package/dist/ui/history/commands/CreateCommentCommand.js +22 -0
  35. package/dist/ui/history/commands/CreateTaskCommand.d.ts +5 -1
  36. package/dist/ui/history/commands/CreateTaskCommand.js +26 -1
  37. package/dist/ui/history/commands/DeleteCommentCommand.d.ts +5 -1
  38. package/dist/ui/history/commands/DeleteCommentCommand.js +22 -0
  39. package/dist/ui/history/commands/DeleteTaskCommand.d.ts +7 -2
  40. package/dist/ui/history/commands/DeleteTaskCommand.js +41 -0
  41. package/dist/ui/history/commands/LinkTaskCommand.d.ts +5 -1
  42. package/dist/ui/history/commands/LinkTaskCommand.js +14 -0
  43. package/dist/ui/history/commands/MoveTaskCommand.d.ts +7 -1
  44. package/dist/ui/history/commands/MoveTaskCommand.js +25 -0
  45. package/dist/ui/history/commands/SetContextCommand.d.ts +5 -1
  46. package/dist/ui/history/commands/SetContextCommand.js +14 -0
  47. package/dist/ui/history/commands/SetEffortCommand.d.ts +5 -1
  48. package/dist/ui/history/commands/SetEffortCommand.js +14 -0
  49. package/dist/ui/history/commands/SetFocusCommand.d.ts +5 -1
  50. package/dist/ui/history/commands/SetFocusCommand.js +14 -0
  51. package/dist/ui/history/commands/index.d.ts +1 -0
  52. package/dist/ui/history/commands/index.js +1 -0
  53. package/dist/ui/history/commands/registry.d.ts +2 -0
  54. package/dist/ui/history/commands/registry.js +28 -0
  55. package/dist/ui/history/index.d.ts +2 -2
  56. package/dist/ui/history/index.js +1 -1
  57. package/dist/ui/history/types.d.ts +9 -0
  58. package/package.json +6 -5
@@ -0,0 +1,186 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { z } from 'zod';
4
+ import { v4 as uuidv4 } from 'uuid';
5
+ import { eq, like, ne, and } from 'drizzle-orm';
6
+ import { getDb, schema } from '../db/index.js';
7
+ import { VERSION } from '../version.js';
8
+ export async function startMcpServer() {
9
+ const server = new McpServer({
10
+ name: 'floq',
11
+ version: VERSION,
12
+ });
13
+ // floq_add_task
14
+ server.tool('floq_add_task', 'Add a new task to Floq. By default, tasks are added to the inbox.', {
15
+ title: z.string().describe('Task title'),
16
+ description: z.string().optional().describe('Task description'),
17
+ status: z.enum(['inbox', 'next', 'waiting', 'someday']).optional().describe('Initial status (default: inbox)'),
18
+ context: z.string().optional().describe('Task context (e.g., work, home)'),
19
+ }, async ({ title, description, status, context }) => {
20
+ const db = getDb();
21
+ const now = new Date();
22
+ const id = uuidv4();
23
+ await db.insert(schema.tasks).values({
24
+ id,
25
+ title,
26
+ description: description ?? null,
27
+ status: status ?? 'inbox',
28
+ context: context?.toLowerCase().replace(/^@/, '') ?? null,
29
+ createdAt: now,
30
+ updatedAt: now,
31
+ });
32
+ return {
33
+ content: [{
34
+ type: 'text',
35
+ text: JSON.stringify({ id, title, status: status ?? 'inbox' }),
36
+ }],
37
+ };
38
+ });
39
+ // floq_list_tasks
40
+ server.tool('floq_list_tasks', 'List tasks from Floq. Use status filter to narrow results. "all" returns all non-done tasks.', {
41
+ status: z.enum(['inbox', 'next', 'waiting', 'someday', 'done', 'all']).optional().describe('Filter by status (default: all, which excludes done)'),
42
+ }, async ({ status }) => {
43
+ const db = getDb();
44
+ const filter = status ?? 'all';
45
+ let tasks;
46
+ if (filter === 'all') {
47
+ tasks = await db
48
+ .select()
49
+ .from(schema.tasks)
50
+ .where(and(ne(schema.tasks.status, 'done'), eq(schema.tasks.isProject, false)));
51
+ }
52
+ else {
53
+ tasks = await db
54
+ .select()
55
+ .from(schema.tasks)
56
+ .where(and(eq(schema.tasks.status, filter), eq(schema.tasks.isProject, false)));
57
+ }
58
+ const result = tasks.map(task => ({
59
+ id: task.id,
60
+ shortId: task.id.slice(0, 8),
61
+ title: task.title,
62
+ description: task.description,
63
+ status: task.status,
64
+ context: task.context,
65
+ waitingFor: task.waitingFor,
66
+ dueDate: task.dueDate?.toISOString() ?? null,
67
+ createdAt: task.createdAt.toISOString(),
68
+ }));
69
+ return {
70
+ content: [{
71
+ type: 'text',
72
+ text: JSON.stringify(result),
73
+ }],
74
+ };
75
+ });
76
+ // floq_complete_task
77
+ server.tool('floq_complete_task', 'Mark a task as done. Accepts full ID or ID prefix (first 8 chars).', {
78
+ taskId: z.string().describe('Task ID or ID prefix'),
79
+ }, async ({ taskId }) => {
80
+ const db = getDb();
81
+ const tasks = await db
82
+ .select()
83
+ .from(schema.tasks)
84
+ .where(like(schema.tasks.id, `${taskId}%`));
85
+ if (tasks.length === 0) {
86
+ return {
87
+ content: [{
88
+ type: 'text',
89
+ text: JSON.stringify({ error: `No task found with ID prefix: ${taskId}` }),
90
+ }],
91
+ isError: true,
92
+ };
93
+ }
94
+ if (tasks.length > 1) {
95
+ const matches = tasks.map(t => ({ id: t.id.slice(0, 8), title: t.title }));
96
+ return {
97
+ content: [{
98
+ type: 'text',
99
+ text: JSON.stringify({ error: 'Multiple tasks match this prefix', matches }),
100
+ }],
101
+ isError: true,
102
+ };
103
+ }
104
+ const task = tasks[0];
105
+ if (task.status === 'done') {
106
+ return {
107
+ content: [{
108
+ type: 'text',
109
+ text: JSON.stringify({ id: task.id, title: task.title, status: 'done', message: 'Task is already done' }),
110
+ }],
111
+ };
112
+ }
113
+ const previousStatus = task.status;
114
+ await db.update(schema.tasks)
115
+ .set({
116
+ status: 'done',
117
+ completedAt: new Date(),
118
+ updatedAt: new Date(),
119
+ })
120
+ .where(eq(schema.tasks.id, task.id));
121
+ return {
122
+ content: [{
123
+ type: 'text',
124
+ text: JSON.stringify({ id: task.id, title: task.title, previousStatus, status: 'done' }),
125
+ }],
126
+ };
127
+ });
128
+ // floq_move_task
129
+ server.tool('floq_move_task', 'Move a task to a different status. Accepts full ID or ID prefix.', {
130
+ taskId: z.string().describe('Task ID or ID prefix'),
131
+ status: z.enum(['inbox', 'next', 'waiting', 'someday', 'done']).describe('Target status'),
132
+ waitingFor: z.string().optional().describe('Who/what the task is waiting for (required when status is "waiting")'),
133
+ }, async ({ taskId, status, waitingFor }) => {
134
+ const db = getDb();
135
+ if (status === 'waiting' && !waitingFor) {
136
+ return {
137
+ content: [{
138
+ type: 'text',
139
+ text: JSON.stringify({ error: 'waitingFor is required when status is "waiting"' }),
140
+ }],
141
+ isError: true,
142
+ };
143
+ }
144
+ const tasks = await db
145
+ .select()
146
+ .from(schema.tasks)
147
+ .where(like(schema.tasks.id, `${taskId}%`));
148
+ if (tasks.length === 0) {
149
+ return {
150
+ content: [{
151
+ type: 'text',
152
+ text: JSON.stringify({ error: `No task found with ID prefix: ${taskId}` }),
153
+ }],
154
+ isError: true,
155
+ };
156
+ }
157
+ if (tasks.length > 1) {
158
+ const matches = tasks.map(t => ({ id: t.id.slice(0, 8), title: t.title }));
159
+ return {
160
+ content: [{
161
+ type: 'text',
162
+ text: JSON.stringify({ error: 'Multiple tasks match this prefix', matches }),
163
+ }],
164
+ isError: true,
165
+ };
166
+ }
167
+ const task = tasks[0];
168
+ const previousStatus = task.status;
169
+ await db.update(schema.tasks)
170
+ .set({
171
+ status,
172
+ waitingFor: status === 'waiting' ? (waitingFor ?? null) : null,
173
+ completedAt: status === 'done' ? new Date() : null,
174
+ updatedAt: new Date(),
175
+ })
176
+ .where(eq(schema.tasks.id, task.id));
177
+ return {
178
+ content: [{
179
+ type: 'text',
180
+ text: JSON.stringify({ id: task.id, title: task.title, previousStatus, status }),
181
+ }],
182
+ };
183
+ });
184
+ const transport = new StdioServerTransport();
185
+ await server.connect(transport);
186
+ }
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,9 +1,10 @@
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
+ import { getInsightsWeeks } from '../../config.js';
7
8
  import { useTheme } from '../theme/index.js';
8
9
  const VISIBLE_LINES = 16;
9
10
  function stringWidth(str) {
@@ -75,16 +76,19 @@ export function InsightsModal({ onClose }) {
75
76
  useEffect(() => {
76
77
  const loadInsights = async () => {
77
78
  const db = getDb();
78
- const weeks = 2;
79
+ const weeks = getInsightsWeeks();
79
80
  const lines = [];
80
81
  const now = new Date();
81
82
  const startDate = getWeekStart(now);
82
83
  startDate.setDate(startDate.getDate() - (weeks - 1) * 7);
83
- // Query completed tasks
84
- const completedTasks = await db
84
+ // Query completed tasks (use completedAt with updatedAt fallback)
85
+ const completedTasks = (await db
85
86
  .select()
86
87
  .from(schema.tasks)
87
- .where(and(eq(schema.tasks.status, 'done'), gte(schema.tasks.updatedAt, startDate), eq(schema.tasks.isProject, false)));
88
+ .where(and(eq(schema.tasks.status, 'done'), eq(schema.tasks.isProject, false)))).filter(task => {
89
+ const completionDate = task.completedAt ?? task.updatedAt;
90
+ return completionDate >= startDate;
91
+ });
88
92
  // Period header
89
93
  lines.push({ type: 'text', value: `${l.period}: ${startDate.toLocaleDateString()} ~ ${now.toLocaleDateString()}` });
90
94
  lines.push({ type: 'spacer', value: '' });
@@ -101,7 +105,10 @@ export function InsightsModal({ onClose }) {
101
105
  ws.setDate(ws.getDate() - i * 7);
102
106
  const we = new Date(ws);
103
107
  we.setDate(we.getDate() + 7);
104
- const weekTasks = completedTasks.filter(t => t.updatedAt >= ws && t.updatedAt < we);
108
+ const weekTasks = completedTasks.filter(t => {
109
+ const d = t.completedAt ?? t.updatedAt;
110
+ return d >= ws && d < we;
111
+ });
105
112
  const weekLabel = fmt(l.weekLabel, { date: ws.toLocaleDateString() });
106
113
  const countLabel = fmt(l.tasksCompleted, { count: weekTasks.length });
107
114
  lines.push({ type: 'text', value: `${weekLabel}: ${countLabel}` });
@@ -119,7 +126,7 @@ export function InsightsModal({ onClose }) {
119
126
  lines.push({ type: 'header', value: l.dailyBreakdown });
120
127
  const dayCounts = new Array(7).fill(0);
121
128
  for (const task of completedTasks) {
122
- dayCounts[task.updatedAt.getDay()]++;
129
+ dayCounts[(task.completedAt ?? task.updatedAt).getDay()]++;
123
130
  }
124
131
  const maxDaily = Math.max(...dayCounts);
125
132
  const dayNames = isJa
@@ -211,7 +218,7 @@ export function InsightsModal({ onClose }) {
211
218
  let totalMs = 0;
212
219
  let validCount = 0;
213
220
  for (const task of completedTasks) {
214
- const diff = task.updatedAt.getTime() - task.createdAt.getTime();
221
+ const diff = (task.completedAt ?? task.updatedAt).getTime() - task.createdAt.getTime();
215
222
  if (diff > 0) {
216
223
  totalMs += diff;
217
224
  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);
@@ -244,6 +244,7 @@ export function KanbanMario({ onOpenSettings }) {
244
244
  toStatus: newStatus,
245
245
  fromWaitingFor: task.waitingFor,
246
246
  toWaitingFor: null,
247
+ fromCompletedAt: task.completedAt,
247
248
  description: fmt(i18n.tui.movedTo, { title: task.title, status: i18n.status[newStatus] }),
248
249
  });
249
250
  await history.execute(command);
@@ -257,6 +258,7 @@ export function KanbanMario({ onOpenSettings }) {
257
258
  toStatus: 'done',
258
259
  fromWaitingFor: task.waitingFor,
259
260
  toWaitingFor: null,
261
+ fromCompletedAt: task.completedAt,
260
262
  description: fmt(i18n.tui.completed, { title: task.title }),
261
263
  });
262
264
  await history.execute(command);
@@ -9,6 +9,7 @@ interface TaskItemProps {
9
9
  isSelected: boolean;
10
10
  projectName?: string;
11
11
  progress?: ProjectProgress;
12
+ showStatus?: boolean;
12
13
  }
13
- export declare function TaskItem({ task, isSelected, projectName, progress }: TaskItemProps): React.ReactElement;
14
+ export declare function TaskItem({ task, isSelected, projectName, progress, showStatus }: TaskItemProps): React.ReactElement;
14
15
  export {};
@@ -1,16 +1,17 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
3
  import { t } from '../../i18n/index.js';
4
4
  import { useTheme } from '../theme/index.js';
5
5
  import { ProgressBar } from './ProgressBar.js';
6
+ import { StatusBadge } from './StatusBadge.js';
6
7
  const EFFORT_LABELS = {
7
8
  small: 'S',
8
9
  medium: 'M',
9
10
  large: 'L',
10
11
  };
11
- export function TaskItem({ task, isSelected, projectName, progress }) {
12
+ export function TaskItem({ task, isSelected, projectName, progress, showStatus }) {
12
13
  const shortId = task.id.slice(0, 8);
13
14
  const i18n = t();
14
15
  const theme = useTheme();
15
- return (_jsx(Box, { children: _jsxs(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [isSelected ? theme.style.selectedPrefix : theme.style.unselectedPrefix, task.isFocused && _jsx(Text, { color: theme.colors.accent, children: "\u2605 " }), "[", shortId, "] ", task.effort && _jsxs(Text, { color: theme.colors.secondary, children: ["[", EFFORT_LABELS[task.effort], "] "] }), task.title, task.context && (_jsxs(Text, { color: theme.colors.accent, children: [" @", task.context] })), projectName && (_jsxs(Text, { color: theme.colors.statusSomeday, children: [" [", projectName, "]"] })), progress && progress.total > 0 && (_jsx(ProgressBar, { completed: progress.completed, total: progress.total })), task.waitingFor && (_jsxs(Text, { color: theme.colors.statusWaiting, children: [" (", i18n.status.waiting.toLowerCase(), ": ", task.waitingFor, ")"] })), task.dueDate && (_jsxs(Text, { color: theme.colors.accent, children: [" (due: ", task.dueDate.toLocaleDateString(), ")"] }))] }) }));
16
+ return (_jsx(Box, { children: _jsxs(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [isSelected ? theme.style.selectedPrefix : theme.style.unselectedPrefix, task.isFocused && _jsx(Text, { color: theme.colors.accent, children: "\u2605 " }), "[", shortId, "] ", showStatus && _jsxs(_Fragment, { children: [_jsx(StatusBadge, { status: task.status }), _jsx(Text, { children: " " })] }), task.effort && _jsxs(Text, { color: theme.colors.secondary, children: ["[", EFFORT_LABELS[task.effort], "] "] }), task.title, task.context && (_jsxs(Text, { color: theme.colors.accent, children: [" @", task.context] })), projectName && (_jsxs(Text, { color: theme.colors.statusSomeday, children: [" [", projectName, "]"] })), progress && progress.total > 0 && (_jsx(ProgressBar, { completed: progress.completed, total: progress.total })), task.waitingFor && (_jsxs(Text, { color: theme.colors.statusWaiting, children: [" (", i18n.status.waiting.toLowerCase(), ": ", task.waitingFor, ")"] })), task.dueDate && (_jsxs(Text, { color: theme.colors.accent, children: [" (due: ", task.dueDate.toLocaleDateString(), ")"] }))] }) }));
16
17
  }
@@ -5,6 +5,14 @@ const HistoryContext = createContext(null);
5
5
  export function HistoryProvider({ children }) {
6
6
  const manager = useMemo(() => getHistoryManager(), []);
7
7
  const [state, setState] = useState(manager.getState());
8
+ useEffect(() => {
9
+ // Load persisted history from DB on mount
10
+ manager.init().then(() => {
11
+ setState(manager.getState());
12
+ }).catch(() => {
13
+ // Ignore init errors - in-memory still works
14
+ });
15
+ }, [manager]);
8
16
  useEffect(() => {
9
17
  const unsubscribe = manager.subscribe(() => {
10
18
  setState(manager.getState());
@@ -1,11 +1,17 @@
1
1
  import type { UndoableCommand, HistoryState } from './types.js';
2
2
  /**
3
3
  * Manages undo/redo history using the Command Pattern
4
+ * with DB persistence for crash-safe undo
4
5
  */
5
6
  export declare class HistoryManager {
6
7
  private undoStack;
7
8
  private redoStack;
8
9
  private listeners;
10
+ private initialized;
11
+ /**
12
+ * Initialize by loading history from DB
13
+ */
14
+ init(): Promise<void>;
9
15
  /**
10
16
  * Execute a command and add it to the undo stack
11
17
  */
@@ -43,11 +49,13 @@ export declare class HistoryManager {
43
49
  /**
44
50
  * Clear all history
45
51
  */
46
- clear(): void;
52
+ clear(): Promise<void>;
47
53
  /**
48
54
  * Subscribe to history changes
49
55
  */
50
56
  subscribe(listener: () => void): () => void;
57
+ private loadFromDb;
58
+ private clearRedoFromDb;
51
59
  private notifyListeners;
52
60
  }
53
61
  /**