floq 0.0.1 → 0.2.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 (55) hide show
  1. package/README.ja.md +157 -29
  2. package/README.md +157 -29
  3. package/dist/changelog.d.ts +13 -0
  4. package/dist/changelog.js +95 -0
  5. package/dist/cli.js +70 -1
  6. package/dist/commands/add.js +5 -6
  7. package/dist/commands/comment.d.ts +2 -0
  8. package/dist/commands/comment.js +67 -0
  9. package/dist/commands/config.d.ts +7 -0
  10. package/dist/commands/config.js +163 -14
  11. package/dist/commands/done.js +4 -6
  12. package/dist/commands/list.js +9 -13
  13. package/dist/commands/move.js +4 -6
  14. package/dist/commands/project.js +18 -26
  15. package/dist/commands/setup.d.ts +1 -0
  16. package/dist/commands/setup.js +13 -0
  17. package/dist/config.d.ts +15 -1
  18. package/dist/config.js +53 -2
  19. package/dist/db/index.d.ts +5 -4
  20. package/dist/db/index.js +127 -32
  21. package/dist/db/schema.d.ts +83 -0
  22. package/dist/db/schema.js +6 -0
  23. package/dist/i18n/en.d.ts +258 -0
  24. package/dist/i18n/en.js +138 -3
  25. package/dist/i18n/ja.js +138 -3
  26. package/dist/index.js +33 -1
  27. package/dist/paths.d.ts +4 -0
  28. package/dist/paths.js +63 -5
  29. package/dist/ui/App.js +384 -136
  30. package/dist/ui/ModeSelector.d.ts +7 -0
  31. package/dist/ui/ModeSelector.js +37 -0
  32. package/dist/ui/SetupWizard.d.ts +6 -0
  33. package/dist/ui/SetupWizard.js +321 -0
  34. package/dist/ui/ThemeSelector.d.ts +1 -1
  35. package/dist/ui/ThemeSelector.js +23 -10
  36. package/dist/ui/components/FunctionKeyBar.d.ts +5 -4
  37. package/dist/ui/components/FunctionKeyBar.js +19 -15
  38. package/dist/ui/components/HelpModal.d.ts +2 -1
  39. package/dist/ui/components/HelpModal.js +118 -4
  40. package/dist/ui/components/KanbanBoard.d.ts +6 -0
  41. package/dist/ui/components/KanbanBoard.js +508 -0
  42. package/dist/ui/components/KanbanColumn.d.ts +12 -0
  43. package/dist/ui/components/KanbanColumn.js +11 -0
  44. package/dist/ui/components/ProgressBar.d.ts +7 -0
  45. package/dist/ui/components/ProgressBar.js +13 -0
  46. package/dist/ui/components/SearchBar.d.ts +8 -0
  47. package/dist/ui/components/SearchBar.js +11 -0
  48. package/dist/ui/components/SearchResults.d.ts +9 -0
  49. package/dist/ui/components/SearchResults.js +18 -0
  50. package/dist/ui/components/TaskItem.d.ts +6 -1
  51. package/dist/ui/components/TaskItem.js +3 -2
  52. package/dist/ui/theme/themes.d.ts +12 -0
  53. package/dist/ui/theme/themes.js +495 -3
  54. package/dist/ui/theme/types.d.ts +1 -1
  55. package/package.json +2 -1
package/dist/ui/App.js CHANGED
@@ -1,5 +1,5 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState, useEffect } from 'react';
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useState, useEffect, useCallback } from 'react';
3
3
  import { Box, Text, useInput, useApp } from 'ink';
4
4
  import TextInput from 'ink-text-input';
5
5
  import { eq, and } from 'drizzle-orm';
@@ -7,15 +7,20 @@ import { v4 as uuidv4 } from 'uuid';
7
7
  import { TaskItem } from './components/TaskItem.js';
8
8
  import { HelpModal } from './components/HelpModal.js';
9
9
  import { FunctionKeyBar } from './components/FunctionKeyBar.js';
10
+ import { SearchBar } from './components/SearchBar.js';
11
+ import { SearchResults } from './components/SearchResults.js';
10
12
  import { SplashScreen } from './SplashScreen.js';
11
13
  import { getDb, schema } from '../db/index.js';
12
14
  import { t, fmt } from '../i18n/index.js';
13
15
  import { ThemeProvider, useTheme } from './theme/index.js';
14
- import { getThemeName } from '../config.js';
15
- const TABS = ['inbox', 'next', 'waiting', 'someday', 'projects'];
16
+ import { getThemeName, getViewMode, isTursoEnabled, getTursoConfig } from '../config.js';
17
+ import { KanbanBoard } from './components/KanbanBoard.js';
18
+ import { VERSION } from '../version.js';
19
+ const TABS = ['inbox', 'next', 'waiting', 'someday', 'projects', 'done'];
16
20
  export function App() {
17
21
  const themeName = getThemeName();
18
- return (_jsx(ThemeProvider, { themeName: themeName, children: _jsx(AppContent, {}) }));
22
+ const viewMode = getViewMode();
23
+ return (_jsx(ThemeProvider, { themeName: themeName, children: viewMode === 'kanban' ? _jsx(KanbanBoard, {}) : _jsx(AppContent, {}) }));
19
24
  }
20
25
  function AppContent() {
21
26
  const theme = useTheme();
@@ -30,14 +35,24 @@ function AppContent() {
30
35
  waiting: [],
31
36
  someday: [],
32
37
  projects: [],
38
+ done: [],
33
39
  });
34
40
  const [message, setMessage] = useState(null);
35
41
  const [selectedProject, setSelectedProject] = useState(null);
36
42
  const [projectTasks, setProjectTasks] = useState([]);
37
43
  const [taskToLink, setTaskToLink] = useState(null);
38
44
  const [projectSelectIndex, setProjectSelectIndex] = useState(0);
45
+ const [selectedTask, setSelectedTask] = useState(null);
46
+ const [taskComments, setTaskComments] = useState([]);
47
+ const [selectedCommentIndex, setSelectedCommentIndex] = useState(0);
48
+ const [taskToWaiting, setTaskToWaiting] = useState(null);
49
+ const [projectProgress, setProjectProgress] = useState({});
50
+ // Search state
51
+ const [searchQuery, setSearchQuery] = useState('');
52
+ const [searchResults, setSearchResults] = useState([]);
53
+ const [searchResultIndex, setSearchResultIndex] = useState(0);
39
54
  const i18n = t();
40
- const loadTasks = () => {
55
+ const loadTasks = useCallback(async () => {
41
56
  const db = getDb();
42
57
  const newTasks = {
43
58
  inbox: [],
@@ -45,50 +60,92 @@ function AppContent() {
45
60
  waiting: [],
46
61
  someday: [],
47
62
  projects: [],
63
+ done: [],
48
64
  };
49
65
  // Load all tasks (including project children) by status
50
- const statusList = ['inbox', 'next', 'waiting', 'someday'];
66
+ const statusList = ['inbox', 'next', 'waiting', 'someday', 'done'];
51
67
  for (const status of statusList) {
52
- newTasks[status] = db
68
+ newTasks[status] = await db
53
69
  .select()
54
70
  .from(schema.tasks)
55
- .where(and(eq(schema.tasks.status, status), eq(schema.tasks.isProject, false)))
56
- .all();
71
+ .where(and(eq(schema.tasks.status, status), eq(schema.tasks.isProject, false)));
57
72
  }
58
73
  // Load projects (isProject = true, not done)
59
- newTasks.projects = db
74
+ newTasks.projects = await db
60
75
  .select()
61
76
  .from(schema.tasks)
62
- .where(and(eq(schema.tasks.isProject, true), eq(schema.tasks.status, 'next')))
63
- .all();
77
+ .where(and(eq(schema.tasks.isProject, true), eq(schema.tasks.status, 'next')));
78
+ // Calculate progress for each project
79
+ const progress = {};
80
+ for (const project of newTasks.projects) {
81
+ const children = await db
82
+ .select()
83
+ .from(schema.tasks)
84
+ .where(eq(schema.tasks.parentId, project.id));
85
+ const total = children.length;
86
+ const completed = children.filter(t => t.status === 'done').length;
87
+ progress[project.id] = { completed, total };
88
+ }
89
+ setProjectProgress(progress);
64
90
  setTasks(newTasks);
65
- };
91
+ }, []);
66
92
  // Get parent project for a task
67
93
  const getParentProject = (parentId) => {
68
94
  if (!parentId)
69
95
  return undefined;
70
96
  return tasks.projects.find(p => p.id === parentId);
71
97
  };
72
- const loadProjectTasks = (projectId) => {
98
+ const loadProjectTasks = useCallback(async (projectId) => {
73
99
  const db = getDb();
74
- const children = db
100
+ const children = await db
75
101
  .select()
76
102
  .from(schema.tasks)
77
- .where(eq(schema.tasks.parentId, projectId))
78
- .all();
103
+ .where(eq(schema.tasks.parentId, projectId));
79
104
  setProjectTasks(children);
80
- };
105
+ }, []);
106
+ const loadTaskComments = useCallback(async (taskId) => {
107
+ const db = getDb();
108
+ const comments = await db
109
+ .select()
110
+ .from(schema.comments)
111
+ .where(eq(schema.comments.taskId, taskId));
112
+ setTaskComments(comments);
113
+ }, []);
81
114
  useEffect(() => {
82
115
  loadTasks();
83
- }, []);
116
+ }, [loadTasks]);
84
117
  const currentTab = TABS[currentListIndex];
85
118
  const currentTasks = mode === 'project-detail' ? projectTasks : tasks[currentTab];
86
- const addTask = (title, parentId) => {
119
+ // Get all tasks for search (across all statuses)
120
+ const getAllTasks = useCallback(() => {
121
+ const allTasks = [];
122
+ for (const tab of TABS) {
123
+ allTasks.push(...tasks[tab]);
124
+ }
125
+ return allTasks;
126
+ }, [tasks]);
127
+ // Search tasks by query
128
+ const searchTasks = useCallback((query) => {
129
+ if (!query.trim())
130
+ return [];
131
+ const lowerQuery = query.toLowerCase();
132
+ const allTasks = getAllTasks();
133
+ return allTasks.filter(task => task.title.toLowerCase().includes(lowerQuery) ||
134
+ (task.description && task.description.toLowerCase().includes(lowerQuery)));
135
+ }, [getAllTasks]);
136
+ // Handle search query change
137
+ const handleSearchChange = useCallback((value) => {
138
+ setSearchQuery(value);
139
+ const results = searchTasks(value);
140
+ setSearchResults(results);
141
+ setSearchResultIndex(0);
142
+ }, [searchTasks]);
143
+ const addTask = useCallback(async (title, parentId) => {
87
144
  if (!title.trim())
88
145
  return;
89
146
  const db = getDb();
90
147
  const now = new Date();
91
- db.insert(schema.tasks)
148
+ await db.insert(schema.tasks)
92
149
  .values({
93
150
  id: uuidv4(),
94
151
  title: title.trim(),
@@ -96,37 +153,120 @@ function AppContent() {
96
153
  parentId: parentId || null,
97
154
  createdAt: now,
98
155
  updatedAt: now,
99
- })
100
- .run();
156
+ });
101
157
  setMessage(fmt(i18n.tui.added, { title: title.trim() }));
102
- loadTasks();
103
- };
104
- const handleInputSubmit = (value) => {
158
+ await loadTasks();
159
+ }, [i18n.tui.added, loadTasks]);
160
+ const addCommentToTask = useCallback(async (task, content) => {
161
+ const db = getDb();
162
+ await db.insert(schema.comments).values({
163
+ id: uuidv4(),
164
+ taskId: task.id,
165
+ content: content.trim(),
166
+ createdAt: new Date(),
167
+ });
168
+ setMessage(i18n.tui.commentAdded || 'Comment added');
169
+ await loadTaskComments(task.id);
170
+ }, [i18n.tui.commentAdded, loadTaskComments]);
171
+ const deleteComment = useCallback(async (comment) => {
172
+ const db = getDb();
173
+ await db.delete(schema.comments).where(eq(schema.comments.id, comment.id));
174
+ setMessage(i18n.tui.commentDeleted || 'Comment deleted');
175
+ if (selectedTask) {
176
+ await loadTaskComments(selectedTask.id);
177
+ }
178
+ }, [i18n.tui.commentDeleted, loadTaskComments, selectedTask]);
179
+ const handleInputSubmit = async (value) => {
180
+ if (mode === 'move-to-waiting' && taskToWaiting) {
181
+ if (value.trim()) {
182
+ await moveTaskToWaiting(taskToWaiting, value);
183
+ }
184
+ setTaskToWaiting(null);
185
+ setMode('normal');
186
+ setInputValue('');
187
+ return;
188
+ }
189
+ // Handle search mode submit
190
+ if (mode === 'search') {
191
+ if (searchResults.length > 0) {
192
+ const task = searchResults[searchResultIndex];
193
+ setSelectedTask(task);
194
+ loadTaskComments(task.id);
195
+ setMode('task-detail');
196
+ }
197
+ else {
198
+ setMode('normal');
199
+ }
200
+ setSearchQuery('');
201
+ setSearchResults([]);
202
+ setSearchResultIndex(0);
203
+ return;
204
+ }
105
205
  if (value.trim()) {
106
- if (mode === 'add-to-project' && selectedProject) {
107
- addTask(value, selectedProject.id);
108
- loadProjectTasks(selectedProject.id);
206
+ if (mode === 'add-comment' && selectedTask) {
207
+ await addCommentToTask(selectedTask, value);
208
+ setMode('task-detail');
209
+ }
210
+ else if (mode === 'add-to-project' && selectedProject) {
211
+ await addTask(value, selectedProject.id);
212
+ await loadProjectTasks(selectedProject.id);
109
213
  setMode('project-detail');
110
214
  }
111
215
  else {
112
- addTask(value);
216
+ await addTask(value);
113
217
  setMode('normal');
114
218
  }
115
219
  }
116
220
  else {
117
- setMode(mode === 'add-to-project' ? 'project-detail' : 'normal');
221
+ if (mode === 'add-comment') {
222
+ setMode('task-detail');
223
+ }
224
+ else {
225
+ setMode(mode === 'add-to-project' ? 'project-detail' : 'normal');
226
+ }
118
227
  }
119
228
  setInputValue('');
120
229
  };
121
- const linkTaskToProject = (task, project) => {
230
+ const linkTaskToProject = useCallback(async (task, project) => {
122
231
  const db = getDb();
123
- db.update(schema.tasks)
232
+ await db.update(schema.tasks)
124
233
  .set({ parentId: project.id, updatedAt: new Date() })
125
- .where(eq(schema.tasks.id, task.id))
126
- .run();
234
+ .where(eq(schema.tasks.id, task.id));
127
235
  setMessage(fmt(i18n.tui.linkedToProject || 'Linked "{title}" to {project}', { title: task.title, project: project.title }));
128
- loadTasks();
129
- };
236
+ await loadTasks();
237
+ }, [i18n.tui.linkedToProject, loadTasks]);
238
+ const markTaskDone = useCallback(async (task) => {
239
+ const db = getDb();
240
+ await db.update(schema.tasks)
241
+ .set({ status: 'done', updatedAt: new Date() })
242
+ .where(eq(schema.tasks.id, task.id));
243
+ setMessage(fmt(i18n.tui.completed, { title: task.title }));
244
+ await loadTasks();
245
+ }, [i18n.tui.completed, loadTasks]);
246
+ const moveTaskToStatus = useCallback(async (task, status) => {
247
+ const db = getDb();
248
+ await db.update(schema.tasks)
249
+ .set({ status, waitingFor: null, updatedAt: new Date() })
250
+ .where(eq(schema.tasks.id, task.id));
251
+ setMessage(fmt(i18n.tui.movedTo, { title: task.title, status: i18n.status[status] }));
252
+ await loadTasks();
253
+ }, [i18n.tui.movedTo, i18n.status, loadTasks]);
254
+ const moveTaskToWaiting = useCallback(async (task, waitingFor) => {
255
+ const db = getDb();
256
+ await db.update(schema.tasks)
257
+ .set({ status: 'waiting', waitingFor: waitingFor.trim(), updatedAt: new Date() })
258
+ .where(eq(schema.tasks.id, task.id));
259
+ setMessage(fmt(i18n.tui.movedToWaiting, { title: task.title, person: waitingFor.trim() }));
260
+ await loadTasks();
261
+ }, [i18n.tui.movedToWaiting, loadTasks]);
262
+ const makeTaskProject = useCallback(async (task) => {
263
+ const db = getDb();
264
+ await db.update(schema.tasks)
265
+ .set({ isProject: true, status: 'next', updatedAt: new Date() })
266
+ .where(eq(schema.tasks.id, task.id));
267
+ setMessage(fmt(i18n.tui.madeProject || 'Made project: {title}', { title: task.title }));
268
+ await loadTasks();
269
+ }, [i18n.tui.madeProject, loadTasks]);
130
270
  const getTabLabel = (tab) => {
131
271
  switch (tab) {
132
272
  case 'inbox':
@@ -139,6 +279,8 @@ function AppContent() {
139
279
  return i18n.tui.tabSomeday;
140
280
  case 'projects':
141
281
  return i18n.tui.tabProjects || 'Projects';
282
+ case 'done':
283
+ return i18n.tui.tabDone || 'Done';
142
284
  }
143
285
  };
144
286
  useInput((input, key) => {
@@ -147,25 +289,111 @@ function AppContent() {
147
289
  setMode('normal');
148
290
  return;
149
291
  }
150
- // Handle help mode - any key closes
292
+ // Handle help mode - let HelpModal handle input
151
293
  if (mode === 'help') {
152
- setMode('normal');
294
+ return;
295
+ }
296
+ // Handle search mode
297
+ if (mode === 'search') {
298
+ if (key.escape) {
299
+ setSearchQuery('');
300
+ setSearchResults([]);
301
+ setSearchResultIndex(0);
302
+ setMode('normal');
303
+ return;
304
+ }
305
+ // Navigate search results with Ctrl+j/k or Ctrl+n/p
306
+ if (key.ctrl && (input === 'j' || input === 'n')) {
307
+ setSearchResultIndex((prev) => prev < searchResults.length - 1 ? prev + 1 : 0);
308
+ return;
309
+ }
310
+ if (key.ctrl && (input === 'k' || input === 'p')) {
311
+ setSearchResultIndex((prev) => prev > 0 ? prev - 1 : Math.max(0, searchResults.length - 1));
312
+ return;
313
+ }
314
+ // Let TextInput handle other keys
153
315
  return;
154
316
  }
155
317
  // Handle add mode
156
- if (mode === 'add' || mode === 'add-to-project') {
318
+ if (mode === 'add' || mode === 'add-to-project' || mode === 'add-comment' || mode === 'move-to-waiting') {
157
319
  if (key.escape) {
158
320
  setInputValue('');
159
- setMode(mode === 'add-to-project' ? 'project-detail' : 'normal');
321
+ if (mode === 'move-to-waiting') {
322
+ setTaskToWaiting(null);
323
+ setMode('normal');
324
+ }
325
+ else if (mode === 'add-comment') {
326
+ setMode('task-detail');
327
+ }
328
+ else if (mode === 'add-to-project') {
329
+ setMode('project-detail');
330
+ }
331
+ else {
332
+ setMode('normal');
333
+ }
334
+ }
335
+ return;
336
+ }
337
+ // Handle task-detail mode
338
+ if (mode === 'task-detail') {
339
+ if (key.escape || key.backspace || input === 'b') {
340
+ // If came from project-detail, go back to project-detail
341
+ if (selectedProject) {
342
+ setMode('project-detail');
343
+ // Find the task index in project tasks
344
+ const taskIndex = projectTasks.findIndex(t => t.id === selectedTask?.id);
345
+ if (taskIndex >= 0) {
346
+ setSelectedTaskIndex(taskIndex);
347
+ }
348
+ }
349
+ else {
350
+ setMode('normal');
351
+ }
352
+ setSelectedTask(null);
353
+ setTaskComments([]);
354
+ setSelectedCommentIndex(0);
355
+ return;
356
+ }
357
+ // Navigate comments
358
+ if (key.upArrow || input === 'k') {
359
+ setSelectedCommentIndex((prev) => (prev > 0 ? prev - 1 : Math.max(0, taskComments.length - 1)));
360
+ return;
361
+ }
362
+ if (key.downArrow || input === 'j') {
363
+ setSelectedCommentIndex((prev) => (prev < taskComments.length - 1 ? prev + 1 : 0));
364
+ return;
365
+ }
366
+ // Delete comment
367
+ if (input === 'd' && taskComments.length > 0) {
368
+ const comment = taskComments[selectedCommentIndex];
369
+ deleteComment(comment).then(() => {
370
+ if (selectedCommentIndex >= taskComments.length - 1) {
371
+ setSelectedCommentIndex(Math.max(0, selectedCommentIndex - 1));
372
+ }
373
+ });
374
+ return;
375
+ }
376
+ // Add comment
377
+ if (input === 'i') {
378
+ setMode('add-comment');
379
+ return;
380
+ }
381
+ // Link to project (P key)
382
+ if (input === 'P' && tasks.projects.length > 0) {
383
+ setTaskToLink(selectedTask);
384
+ setProjectSelectIndex(0);
385
+ setMode('select-project');
386
+ return;
160
387
  }
161
388
  return;
162
389
  }
163
390
  // Handle select-project mode
164
391
  if (mode === 'select-project') {
165
392
  if (key.escape) {
393
+ const wasFromTaskDetail = selectedTask !== null;
166
394
  setTaskToLink(null);
167
395
  setProjectSelectIndex(0);
168
- setMode('normal');
396
+ setMode(wasFromTaskDetail ? 'task-detail' : 'normal');
169
397
  return;
170
398
  }
171
399
  // Navigate projects
@@ -180,10 +408,11 @@ function AppContent() {
180
408
  // Select project with Enter
181
409
  if (key.return && taskToLink && tasks.projects.length > 0) {
182
410
  const project = tasks.projects[projectSelectIndex];
411
+ const wasFromTaskDetail = selectedTask !== null;
183
412
  linkTaskToProject(taskToLink, project);
184
413
  setTaskToLink(null);
185
414
  setProjectSelectIndex(0);
186
- setMode('normal');
415
+ setMode(wasFromTaskDetail ? 'task-detail' : 'normal');
187
416
  return;
188
417
  }
189
418
  return;
@@ -214,16 +443,19 @@ function AppContent() {
214
443
  // Mark child task as done
215
444
  if (input === 'd' && projectTasks.length > 0) {
216
445
  const task = projectTasks[selectedTaskIndex];
217
- const db = getDb();
218
- db.update(schema.tasks)
219
- .set({ status: 'done', updatedAt: new Date() })
220
- .where(eq(schema.tasks.id, task.id))
221
- .run();
222
- setMessage(fmt(i18n.tui.completed, { title: task.title }));
223
- if (selectedProject) {
224
- loadProjectTasks(selectedProject.id);
225
- }
226
- loadTasks();
446
+ markTaskDone(task).then(() => {
447
+ if (selectedProject) {
448
+ loadProjectTasks(selectedProject.id);
449
+ }
450
+ });
451
+ return;
452
+ }
453
+ // Enter to view task details
454
+ if (key.return && projectTasks.length > 0) {
455
+ const task = projectTasks[selectedTaskIndex];
456
+ setSelectedTask(task);
457
+ loadTaskComments(task.id);
458
+ setMode('task-detail');
227
459
  return;
228
460
  }
229
461
  return;
@@ -247,31 +479,21 @@ function AppContent() {
247
479
  if ((input === '\x1bOR' || input === '\x1b[13~') && currentTasks.length > 0) {
248
480
  // F3 - Done
249
481
  const task = currentTasks[selectedTaskIndex];
250
- const db = getDb();
251
- db.update(schema.tasks)
252
- .set({ status: 'done', updatedAt: new Date() })
253
- .where(eq(schema.tasks.id, task.id))
254
- .run();
255
- setMessage(fmt(i18n.tui.completed, { title: task.title }));
256
- loadTasks();
257
- if (selectedTaskIndex >= currentTasks.length - 1) {
258
- setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
259
- }
482
+ markTaskDone(task).then(() => {
483
+ if (selectedTaskIndex >= currentTasks.length - 1) {
484
+ setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
485
+ }
486
+ });
260
487
  return;
261
488
  }
262
489
  if ((input === '\x1bOS' || input === '\x1b[14~') && currentTasks.length > 0 && currentTab !== 'next' && currentTab !== 'projects') {
263
490
  // F4 - Move to Next
264
491
  const task = currentTasks[selectedTaskIndex];
265
- const db = getDb();
266
- db.update(schema.tasks)
267
- .set({ status: 'next', waitingFor: null, updatedAt: new Date() })
268
- .where(eq(schema.tasks.id, task.id))
269
- .run();
270
- setMessage(fmt(i18n.tui.movedTo, { title: task.title, status: i18n.status.next }));
271
- loadTasks();
272
- if (selectedTaskIndex >= currentTasks.length - 1) {
273
- setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
274
- }
492
+ moveTaskToStatus(task, 'next').then(() => {
493
+ if (selectedTaskIndex >= currentTasks.length - 1) {
494
+ setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
495
+ }
496
+ });
275
497
  return;
276
498
  }
277
499
  if (input === '\x1b[15~') {
@@ -290,6 +512,14 @@ function AppContent() {
290
512
  setMode('help');
291
513
  return;
292
514
  }
515
+ // Search mode
516
+ if (input === '/') {
517
+ setMode('search');
518
+ setSearchQuery('');
519
+ setSearchResults([]);
520
+ setSearchResultIndex(0);
521
+ return;
522
+ }
293
523
  // Quit
294
524
  if (input === 'q' || (key.ctrl && input === 'c')) {
295
525
  exit();
@@ -326,6 +556,11 @@ function AppContent() {
326
556
  setSelectedTaskIndex(0);
327
557
  return;
328
558
  }
559
+ if (input === '6') {
560
+ setCurrentListIndex(5);
561
+ setSelectedTaskIndex(0);
562
+ return;
563
+ }
329
564
  // Navigate between lists
330
565
  if (key.leftArrow || input === 'h') {
331
566
  setCurrentListIndex((prev) => (prev > 0 ? prev - 1 : TABS.length - 1));
@@ -355,87 +590,77 @@ function AppContent() {
355
590
  setSelectedTaskIndex(0);
356
591
  return;
357
592
  }
593
+ // Enter to view task details (on non-projects tabs)
594
+ if (key.return && currentTab !== 'projects' && currentTasks.length > 0) {
595
+ const task = currentTasks[selectedTaskIndex];
596
+ setSelectedTask(task);
597
+ loadTaskComments(task.id);
598
+ setMode('task-detail');
599
+ return;
600
+ }
358
601
  // Mark as project (p key)
359
- if (input === 'p' && currentTasks.length > 0 && currentTab !== 'projects') {
602
+ if (input === 'p' && currentTasks.length > 0 && currentTab !== 'projects' && currentTab !== 'done') {
360
603
  const task = currentTasks[selectedTaskIndex];
361
- const db = getDb();
362
- db.update(schema.tasks)
363
- .set({ isProject: true, status: 'next', updatedAt: new Date() })
364
- .where(eq(schema.tasks.id, task.id))
365
- .run();
366
- setMessage(fmt(i18n.tui.madeProject || 'Made project: {title}', { title: task.title }));
367
- loadTasks();
368
- if (selectedTaskIndex >= currentTasks.length - 1) {
369
- setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
370
- }
604
+ makeTaskProject(task).then(() => {
605
+ if (selectedTaskIndex >= currentTasks.length - 1) {
606
+ setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
607
+ }
608
+ });
371
609
  return;
372
610
  }
373
611
  // Link to project (P key - shift+p)
374
- if (input === 'P' && currentTasks.length > 0 && currentTab !== 'projects' && tasks.projects.length > 0) {
612
+ if (input === 'P' && currentTasks.length > 0 && currentTab !== 'projects' && currentTab !== 'done' && tasks.projects.length > 0) {
375
613
  const task = currentTasks[selectedTaskIndex];
376
614
  setTaskToLink(task);
377
615
  setProjectSelectIndex(0);
378
616
  setMode('select-project');
379
617
  return;
380
618
  }
381
- // Mark as done
382
- if (input === 'd' && currentTasks.length > 0) {
619
+ // Mark as done (not available on done tab)
620
+ if (input === 'd' && currentTasks.length > 0 && currentTab !== 'done') {
383
621
  const task = currentTasks[selectedTaskIndex];
384
- const db = getDb();
385
- db.update(schema.tasks)
386
- .set({ status: 'done', updatedAt: new Date() })
387
- .where(eq(schema.tasks.id, task.id))
388
- .run();
389
- setMessage(fmt(i18n.tui.completed, { title: task.title }));
390
- loadTasks();
391
- if (selectedTaskIndex >= currentTasks.length - 1) {
392
- setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
393
- }
622
+ markTaskDone(task).then(() => {
623
+ if (selectedTaskIndex >= currentTasks.length - 1) {
624
+ setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
625
+ }
626
+ });
394
627
  return;
395
628
  }
396
629
  // Move to next actions
397
- if (input === 'n' && currentTasks.length > 0 && currentTab !== 'next' && currentTab !== 'projects') {
630
+ if (input === 'n' && currentTasks.length > 0 && currentTab !== 'next' && currentTab !== 'projects' && currentTab !== 'done') {
398
631
  const task = currentTasks[selectedTaskIndex];
399
- const db = getDb();
400
- db.update(schema.tasks)
401
- .set({ status: 'next', waitingFor: null, updatedAt: new Date() })
402
- .where(eq(schema.tasks.id, task.id))
403
- .run();
404
- setMessage(fmt(i18n.tui.movedTo, { title: task.title, status: i18n.status.next }));
405
- loadTasks();
406
- if (selectedTaskIndex >= currentTasks.length - 1) {
407
- setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
408
- }
632
+ moveTaskToStatus(task, 'next').then(() => {
633
+ if (selectedTaskIndex >= currentTasks.length - 1) {
634
+ setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
635
+ }
636
+ });
409
637
  return;
410
638
  }
411
639
  // Move to someday
412
- if (input === 's' && currentTasks.length > 0 && currentTab !== 'someday' && currentTab !== 'projects') {
640
+ if (input === 's' && currentTasks.length > 0 && currentTab !== 'someday' && currentTab !== 'projects' && currentTab !== 'done') {
413
641
  const task = currentTasks[selectedTaskIndex];
414
- const db = getDb();
415
- db.update(schema.tasks)
416
- .set({ status: 'someday', waitingFor: null, updatedAt: new Date() })
417
- .where(eq(schema.tasks.id, task.id))
418
- .run();
419
- setMessage(fmt(i18n.tui.movedTo, { title: task.title, status: i18n.status.someday }));
420
- loadTasks();
421
- if (selectedTaskIndex >= currentTasks.length - 1) {
422
- setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
423
- }
642
+ moveTaskToStatus(task, 'someday').then(() => {
643
+ if (selectedTaskIndex >= currentTasks.length - 1) {
644
+ setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
645
+ }
646
+ });
647
+ return;
648
+ }
649
+ // Move to waiting
650
+ if (input === 'w' && currentTasks.length > 0 && currentTab !== 'waiting' && currentTab !== 'projects' && currentTab !== 'done') {
651
+ const task = currentTasks[selectedTaskIndex];
652
+ setTaskToWaiting(task);
653
+ setMode('move-to-waiting');
424
654
  return;
425
655
  }
426
656
  // Move to inbox
427
- if (input === 'i' && currentTasks.length > 0 && currentTab !== 'inbox' && currentTab !== 'projects') {
657
+ if (input === 'i' && currentTasks.length > 0 && currentTab !== 'inbox' && currentTab !== 'projects' && currentTab !== 'done') {
428
658
  const task = currentTasks[selectedTaskIndex];
429
- const db = getDb();
430
- db.update(schema.tasks)
431
- .set({ status: 'inbox', waitingFor: null, updatedAt: new Date() })
432
- .where(eq(schema.tasks.id, task.id))
433
- .run();
434
- setMessage(fmt(i18n.tui.movedTo, { title: task.title, status: i18n.status.inbox }));
435
- loadTasks();
436
- if (selectedTaskIndex >= currentTasks.length - 1) {
437
- setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
438
- }
659
+ moveTaskToStatus(task, 'inbox').then(() => {
660
+ if (selectedTaskIndex >= currentTasks.length - 1) {
661
+ setSelectedTaskIndex(Math.max(0, selectedTaskIndex - 1));
662
+ }
663
+ });
439
664
  return;
440
665
  }
441
666
  // Refresh
@@ -458,15 +683,38 @@ function AppContent() {
458
683
  const [open, close] = theme.style.tabBrackets;
459
684
  return `${open}${label}${close}`;
460
685
  };
461
- return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, justifyContent: "space-between", children: [_jsx(Text, { bold: true, color: theme.colors.primary, children: formatTitle(i18n.tui.title) }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.helpHint })] }), _jsx(Box, { marginBottom: 1, children: TABS.map((tab, index) => {
686
+ // Turso 接続情報を取得
687
+ const tursoEnabled = isTursoEnabled();
688
+ const tursoHost = tursoEnabled ? (() => {
689
+ const config = getTursoConfig();
690
+ if (config) {
691
+ try {
692
+ return new URL(config.url).host;
693
+ }
694
+ catch {
695
+ return config.url;
696
+ }
697
+ }
698
+ return '';
699
+ })() : '';
700
+ return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.colors.primary, children: formatTitle(i18n.tui.title) }), _jsx(Text, { color: theme.colors.textMuted, children: theme.name === 'modern' ? ` v${VERSION}` : ` VER ${VERSION}` }), tursoEnabled && (_jsxs(Text, { color: theme.colors.accent, children: [theme.name === 'modern' ? ' ☁️ ' : ' [SYNC] ', tursoHost] })), !tursoEnabled && (_jsx(Text, { color: theme.colors.textMuted, children: theme.name === 'modern' ? ' 💾 local' : ' [LOCAL]' }))] }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.helpHint })] }), _jsx(Box, { marginBottom: 1, children: TABS.map((tab, index) => {
462
701
  const isActive = index === currentListIndex && mode !== 'project-detail';
463
702
  const count = tasks[tab].length;
464
703
  const label = `${index + 1}:${getTabLabel(tab)}(${count})`;
465
704
  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));
466
- }) }), mode === 'project-detail' && selectedProject && (_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, { 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) => {
705
+ }) }), 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(Text, { color: theme.colors.textMuted, children: [i18n.status[selectedTask.status], selectedTask.waitingFor && ` - ${selectedTask.waitingFor}`] })] }), _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) => {
706
+ const isSelected = index === selectedCommentIndex && mode === 'task-detail';
707
+ 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));
708
+ })) }), 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' && (_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) => {
467
709
  const parentProject = getParentProject(task.parentId);
468
- return (_jsx(TaskItem, { task: task, isSelected: index === selectedTaskIndex, projectName: parentProject?.title }, task.id));
469
- })) }), (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
710
+ const progress = currentTab === 'projects' ? projectProgress[task.id] : undefined;
711
+ return (_jsx(TaskItem, { task: task, isSelected: index === selectedTaskIndex, projectName: parentProject?.title, progress: progress }, task.id));
712
+ })) })), (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
470
713
  ? `${i18n.tui.newTask}[${selectedProject.title}] `
471
- : 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 === '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' })] })), message && mode === 'normal' && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.textHighlight, children: message }) })), _jsx(Box, { marginTop: 1, children: theme.style.showFunctionKeys ? (_jsx(FunctionKeyBar, {})) : (_jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.footer })) })] }));
714
+ : 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 === 'search' && (_jsx(SearchBar, { value: searchQuery, onChange: handleSearchChange, onSubmit: handleInputSubmit })), message && mode === 'normal' && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.textHighlight, children: message }) })), _jsx(Box, { marginTop: 1, children: (mode === 'task-detail' || mode === 'add-comment') ? (theme.style.showFunctionKeys ? (_jsx(FunctionKeyBar, { keys: [
715
+ { key: 'i', label: i18n.tui.keyBar.comment },
716
+ { key: 'd', label: i18n.tui.keyBar.delete },
717
+ { key: 'P', label: i18n.tui.keyBar.project },
718
+ { key: 'b', label: i18n.tui.keyBar.back },
719
+ ] })) : (_jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.taskDetailFooter }))) : theme.style.showFunctionKeys ? (_jsx(FunctionKeyBar, {})) : (_jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.footer })) })] }));
472
720
  }