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.
- package/README.ja.md +4 -0
- package/README.md +4 -0
- package/dist/cli.js +32 -2
- package/dist/commands/config.js +2 -1
- package/dist/commands/done.js +1 -0
- package/dist/commands/insights.js +13 -10
- package/dist/commands/move.js +2 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.js +11 -0
- package/dist/db/index.js +36 -0
- package/dist/db/schema.d.ts +116 -0
- package/dist/db/schema.js +8 -0
- package/dist/i18n/en.d.ts +2 -0
- package/dist/i18n/en.js +1 -0
- package/dist/i18n/ja.js +1 -0
- package/dist/index.js +2 -1
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +186 -0
- package/dist/ui/App.js +7 -4
- package/dist/ui/components/GtdDQ.js +4 -1
- package/dist/ui/components/GtdMario.js +4 -1
- package/dist/ui/components/InsightsModal.js +15 -8
- package/dist/ui/components/KanbanBoard.js +3 -0
- package/dist/ui/components/KanbanDQ.js +2 -0
- package/dist/ui/components/KanbanMario.js +2 -0
- package/dist/ui/components/TaskItem.d.ts +2 -1
- package/dist/ui/components/TaskItem.js +4 -3
- package/dist/ui/history/HistoryContext.js +8 -0
- package/dist/ui/history/HistoryManager.d.ts +9 -1
- package/dist/ui/history/HistoryManager.js +140 -16
- package/dist/ui/history/commands/ConvertToProjectCommand.d.ts +5 -1
- package/dist/ui/history/commands/ConvertToProjectCommand.js +13 -0
- package/dist/ui/history/commands/CreateCommentCommand.d.ts +5 -1
- package/dist/ui/history/commands/CreateCommentCommand.js +22 -0
- package/dist/ui/history/commands/CreateTaskCommand.d.ts +5 -1
- package/dist/ui/history/commands/CreateTaskCommand.js +26 -1
- package/dist/ui/history/commands/DeleteCommentCommand.d.ts +5 -1
- package/dist/ui/history/commands/DeleteCommentCommand.js +22 -0
- package/dist/ui/history/commands/DeleteTaskCommand.d.ts +7 -2
- package/dist/ui/history/commands/DeleteTaskCommand.js +41 -0
- package/dist/ui/history/commands/LinkTaskCommand.d.ts +5 -1
- package/dist/ui/history/commands/LinkTaskCommand.js +14 -0
- package/dist/ui/history/commands/MoveTaskCommand.d.ts +7 -1
- package/dist/ui/history/commands/MoveTaskCommand.js +25 -0
- package/dist/ui/history/commands/SetContextCommand.d.ts +5 -1
- package/dist/ui/history/commands/SetContextCommand.js +14 -0
- package/dist/ui/history/commands/SetEffortCommand.d.ts +5 -1
- package/dist/ui/history/commands/SetEffortCommand.js +14 -0
- package/dist/ui/history/commands/SetFocusCommand.d.ts +5 -1
- package/dist/ui/history/commands/SetFocusCommand.js +14 -0
- package/dist/ui/history/commands/index.d.ts +1 -0
- package/dist/ui/history/commands/index.js +1 -0
- package/dist/ui/history/commands/registry.d.ts +2 -0
- package/dist/ui/history/commands/registry.js +28 -0
- package/dist/ui/history/index.d.ts +2 -2
- package/dist/ui/history/index.js +1 -1
- package/dist/ui/history/types.d.ts +9 -0
- 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'
|
|
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
|
|
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 =
|
|
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'),
|
|
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 =>
|
|
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
|
/**
|