floq 1.4.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.ja.md +4 -0
  2. package/README.md +4 -0
  3. package/dist/cli.js +32 -2
  4. package/dist/commands/config.js +2 -1
  5. package/dist/commands/done.js +1 -0
  6. package/dist/commands/insights.js +13 -10
  7. package/dist/commands/move.js +2 -1
  8. package/dist/config.d.ts +3 -0
  9. package/dist/config.js +11 -0
  10. package/dist/db/index.js +36 -0
  11. package/dist/db/schema.d.ts +116 -0
  12. package/dist/db/schema.js +8 -0
  13. package/dist/i18n/en.d.ts +2 -0
  14. package/dist/i18n/en.js +1 -0
  15. package/dist/i18n/ja.js +1 -0
  16. package/dist/index.js +2 -1
  17. package/dist/mcp/server.d.ts +1 -0
  18. package/dist/mcp/server.js +186 -0
  19. package/dist/ui/App.js +7 -4
  20. package/dist/ui/components/GtdDQ.js +4 -1
  21. package/dist/ui/components/GtdMario.js +4 -1
  22. package/dist/ui/components/InsightsModal.js +15 -8
  23. package/dist/ui/components/KanbanBoard.js +3 -0
  24. package/dist/ui/components/KanbanDQ.js +2 -0
  25. package/dist/ui/components/KanbanMario.js +2 -0
  26. package/dist/ui/components/TaskItem.d.ts +2 -1
  27. package/dist/ui/components/TaskItem.js +4 -3
  28. package/dist/ui/history/HistoryContext.js +8 -0
  29. package/dist/ui/history/HistoryManager.d.ts +9 -1
  30. package/dist/ui/history/HistoryManager.js +140 -16
  31. package/dist/ui/history/commands/ConvertToProjectCommand.d.ts +5 -1
  32. package/dist/ui/history/commands/ConvertToProjectCommand.js +13 -0
  33. package/dist/ui/history/commands/CreateCommentCommand.d.ts +5 -1
  34. package/dist/ui/history/commands/CreateCommentCommand.js +22 -0
  35. package/dist/ui/history/commands/CreateTaskCommand.d.ts +5 -1
  36. package/dist/ui/history/commands/CreateTaskCommand.js +26 -1
  37. package/dist/ui/history/commands/DeleteCommentCommand.d.ts +5 -1
  38. package/dist/ui/history/commands/DeleteCommentCommand.js +22 -0
  39. package/dist/ui/history/commands/DeleteTaskCommand.d.ts +7 -2
  40. package/dist/ui/history/commands/DeleteTaskCommand.js +41 -0
  41. package/dist/ui/history/commands/LinkTaskCommand.d.ts +5 -1
  42. package/dist/ui/history/commands/LinkTaskCommand.js +14 -0
  43. package/dist/ui/history/commands/MoveTaskCommand.d.ts +7 -1
  44. package/dist/ui/history/commands/MoveTaskCommand.js +25 -0
  45. package/dist/ui/history/commands/SetContextCommand.d.ts +5 -1
  46. package/dist/ui/history/commands/SetContextCommand.js +14 -0
  47. package/dist/ui/history/commands/SetEffortCommand.d.ts +5 -1
  48. package/dist/ui/history/commands/SetEffortCommand.js +14 -0
  49. package/dist/ui/history/commands/SetFocusCommand.d.ts +5 -1
  50. package/dist/ui/history/commands/SetFocusCommand.js +14 -0
  51. package/dist/ui/history/commands/index.d.ts +1 -0
  52. package/dist/ui/history/commands/index.js +1 -0
  53. package/dist/ui/history/commands/registry.d.ts +2 -0
  54. package/dist/ui/history/commands/registry.js +28 -0
  55. package/dist/ui/history/index.d.ts +2 -2
  56. package/dist/ui/history/index.js +1 -1
  57. package/dist/ui/history/types.d.ts +9 -0
  58. package/package.json +6 -5
@@ -1,23 +1,70 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+ import { eq, desc, asc } from 'drizzle-orm';
3
+ import { getDb, schema } from '../../db/index.js';
4
+ import { deserializeCommand } from './commands/registry.js';
1
5
  import { MAX_HISTORY_SIZE } from './types.js';
2
6
  /**
3
7
  * Manages undo/redo history using the Command Pattern
8
+ * with DB persistence for crash-safe undo
4
9
  */
5
10
  export class HistoryManager {
6
11
  undoStack = [];
7
12
  redoStack = [];
8
13
  listeners = new Set();
14
+ initialized = false;
15
+ /**
16
+ * Initialize by loading history from DB
17
+ */
18
+ async init() {
19
+ if (this.initialized)
20
+ return;
21
+ try {
22
+ await this.loadFromDb();
23
+ }
24
+ catch {
25
+ // DB may not have the table yet - that's OK, just use in-memory
26
+ }
27
+ this.initialized = true;
28
+ }
9
29
  /**
10
30
  * Execute a command and add it to the undo stack
11
31
  */
12
32
  async execute(command) {
13
33
  await command.execute();
34
+ const dbId = uuidv4();
35
+ // Persist to DB
36
+ try {
37
+ const serialized = command.toJSON();
38
+ const db = getDb();
39
+ await db.insert(schema.operationHistory).values({
40
+ id: dbId,
41
+ commandType: serialized.type,
42
+ commandData: JSON.stringify(serialized.data),
43
+ executedAt: new Date(),
44
+ isUndone: false,
45
+ });
46
+ }
47
+ catch {
48
+ // If DB write fails, in-memory undo still works
49
+ }
14
50
  // Add to undo stack
15
- this.undoStack.push(command);
51
+ this.undoStack.push({ dbId, command });
16
52
  // Clear redo stack (new action invalidates redo history)
53
+ // Also clean up DB records for redo items
54
+ await this.clearRedoFromDb();
17
55
  this.redoStack = [];
18
56
  // Enforce max history size
19
57
  if (this.undoStack.length > MAX_HISTORY_SIZE) {
20
- this.undoStack.shift();
58
+ const removed = this.undoStack.shift();
59
+ if (removed) {
60
+ try {
61
+ const db = getDb();
62
+ await db.delete(schema.operationHistory).where(eq(schema.operationHistory.id, removed.dbId));
63
+ }
64
+ catch {
65
+ // ignore
66
+ }
67
+ }
21
68
  }
22
69
  this.notifyListeners();
23
70
  }
@@ -26,19 +73,29 @@ export class HistoryManager {
26
73
  * @returns true if undo was performed, false if nothing to undo
27
74
  */
28
75
  async undo() {
29
- const command = this.undoStack.pop();
30
- if (!command) {
76
+ const tracked = this.undoStack.pop();
77
+ if (!tracked) {
31
78
  return false;
32
79
  }
33
80
  try {
34
- await command.undo();
35
- this.redoStack.push(command);
81
+ await tracked.command.undo();
82
+ // Mark as undone in DB
83
+ try {
84
+ const db = getDb();
85
+ await db.update(schema.operationHistory)
86
+ .set({ isUndone: true })
87
+ .where(eq(schema.operationHistory.id, tracked.dbId));
88
+ }
89
+ catch {
90
+ // ignore DB errors
91
+ }
92
+ this.redoStack.push(tracked);
36
93
  this.notifyListeners();
37
94
  return true;
38
95
  }
39
96
  catch (error) {
40
97
  // Re-add command to undo stack if undo fails
41
- this.undoStack.push(command);
98
+ this.undoStack.push(tracked);
42
99
  throw error;
43
100
  }
44
101
  }
@@ -47,19 +104,29 @@ export class HistoryManager {
47
104
  * @returns true if redo was performed, false if nothing to redo
48
105
  */
49
106
  async redo() {
50
- const command = this.redoStack.pop();
51
- if (!command) {
107
+ const tracked = this.redoStack.pop();
108
+ if (!tracked) {
52
109
  return false;
53
110
  }
54
111
  try {
55
- await command.execute();
56
- this.undoStack.push(command);
112
+ await tracked.command.execute();
113
+ // Mark as not undone in DB
114
+ try {
115
+ const db = getDb();
116
+ await db.update(schema.operationHistory)
117
+ .set({ isUndone: false })
118
+ .where(eq(schema.operationHistory.id, tracked.dbId));
119
+ }
120
+ catch {
121
+ // ignore DB errors
122
+ }
123
+ this.undoStack.push(tracked);
57
124
  this.notifyListeners();
58
125
  return true;
59
126
  }
60
127
  catch (error) {
61
128
  // Re-add command to redo stack if redo fails
62
- this.redoStack.push(command);
129
+ this.redoStack.push(tracked);
63
130
  throw error;
64
131
  }
65
132
  }
@@ -71,7 +138,7 @@ export class HistoryManager {
71
138
  undoCount: this.undoStack.length,
72
139
  redoCount: this.redoStack.length,
73
140
  lastCommandDescription: this.undoStack.length > 0
74
- ? this.undoStack[this.undoStack.length - 1].description
141
+ ? this.undoStack[this.undoStack.length - 1].command.description
75
142
  : null,
76
143
  };
77
144
  }
@@ -92,7 +159,7 @@ export class HistoryManager {
92
159
  */
93
160
  getUndoDescription() {
94
161
  return this.undoStack.length > 0
95
- ? this.undoStack[this.undoStack.length - 1].description
162
+ ? this.undoStack[this.undoStack.length - 1].command.description
96
163
  : null;
97
164
  }
98
165
  /**
@@ -100,15 +167,22 @@ export class HistoryManager {
100
167
  */
101
168
  getRedoDescription() {
102
169
  return this.redoStack.length > 0
103
- ? this.redoStack[this.redoStack.length - 1].description
170
+ ? this.redoStack[this.redoStack.length - 1].command.description
104
171
  : null;
105
172
  }
106
173
  /**
107
174
  * Clear all history
108
175
  */
109
- clear() {
176
+ async clear() {
110
177
  this.undoStack = [];
111
178
  this.redoStack = [];
179
+ try {
180
+ const db = getDb();
181
+ await db.delete(schema.operationHistory);
182
+ }
183
+ catch {
184
+ // ignore DB errors
185
+ }
112
186
  this.notifyListeners();
113
187
  }
114
188
  /**
@@ -118,6 +192,56 @@ export class HistoryManager {
118
192
  this.listeners.add(listener);
119
193
  return () => this.listeners.delete(listener);
120
194
  }
195
+ async loadFromDb() {
196
+ const db = getDb();
197
+ // Load undo stack: non-undone operations, oldest first
198
+ const undoRows = await db
199
+ .select()
200
+ .from(schema.operationHistory)
201
+ .where(eq(schema.operationHistory.isUndone, false))
202
+ .orderBy(asc(schema.operationHistory.executedAt))
203
+ .limit(MAX_HISTORY_SIZE);
204
+ for (const row of undoRows) {
205
+ try {
206
+ const data = JSON.parse(row.commandData);
207
+ const command = deserializeCommand(row.commandType, data);
208
+ this.undoStack.push({ dbId: row.id, command });
209
+ }
210
+ catch {
211
+ // Skip commands that can't be deserialized
212
+ }
213
+ }
214
+ // Load redo stack: undone operations, most recent first (so most recent undo is on top)
215
+ const redoRows = await db
216
+ .select()
217
+ .from(schema.operationHistory)
218
+ .where(eq(schema.operationHistory.isUndone, true))
219
+ .orderBy(desc(schema.operationHistory.executedAt))
220
+ .limit(MAX_HISTORY_SIZE);
221
+ for (const row of redoRows) {
222
+ try {
223
+ const data = JSON.parse(row.commandData);
224
+ const command = deserializeCommand(row.commandType, data);
225
+ this.redoStack.push({ dbId: row.id, command });
226
+ }
227
+ catch {
228
+ // Skip commands that can't be deserialized
229
+ }
230
+ }
231
+ }
232
+ async clearRedoFromDb() {
233
+ if (this.redoStack.length === 0)
234
+ return;
235
+ try {
236
+ const db = getDb();
237
+ for (const tracked of this.redoStack) {
238
+ await db.delete(schema.operationHistory).where(eq(schema.operationHistory.id, tracked.dbId));
239
+ }
240
+ }
241
+ catch {
242
+ // ignore
243
+ }
244
+ }
121
245
  notifyListeners() {
122
246
  for (const listener of this.listeners) {
123
247
  listener();
@@ -1,4 +1,4 @@
1
- import type { UndoableCommand } from '../types.js';
1
+ import type { UndoableCommand, SerializedCommand } from '../types.js';
2
2
  import type { TaskStatus } from '../../../db/schema.js';
3
3
  interface ConvertToProjectParams {
4
4
  taskId: string;
@@ -15,5 +15,9 @@ export declare class ConvertToProjectCommand implements UndoableCommand {
15
15
  constructor(params: ConvertToProjectParams);
16
16
  execute(): Promise<void>;
17
17
  undo(): Promise<void>;
18
+ toJSON(): SerializedCommand;
19
+ static fromJSON(json: {
20
+ data: Record<string, unknown>;
21
+ }): ConvertToProjectCommand;
18
22
  }
19
23
  export {};
@@ -34,4 +34,17 @@ export class ConvertToProjectCommand {
34
34
  })
35
35
  .where(eq(schema.tasks.id, this.taskId));
36
36
  }
37
+ toJSON() {
38
+ return {
39
+ type: 'convert_to_project',
40
+ data: {
41
+ taskId: this.taskId,
42
+ originalStatus: this.originalStatus,
43
+ description: this.description,
44
+ },
45
+ };
46
+ }
47
+ static fromJSON(json) {
48
+ return new ConvertToProjectCommand(json.data);
49
+ }
37
50
  }
@@ -1,4 +1,4 @@
1
- import type { UndoableCommand } from '../types.js';
1
+ import type { UndoableCommand, SerializedCommand } from '../types.js';
2
2
  import type { NewComment } from '../../../db/schema.js';
3
3
  interface CreateCommentParams {
4
4
  comment: NewComment;
@@ -14,5 +14,9 @@ export declare class CreateCommentCommand implements UndoableCommand {
14
14
  constructor(params: CreateCommentParams);
15
15
  execute(): Promise<void>;
16
16
  undo(): Promise<void>;
17
+ toJSON(): SerializedCommand;
18
+ static fromJSON(json: {
19
+ data: Record<string, unknown>;
20
+ }): CreateCommentCommand;
17
21
  }
18
22
  export {};
@@ -20,4 +20,26 @@ export class CreateCommentCommand {
20
20
  const db = getDb();
21
21
  await db.delete(schema.comments).where(eq(schema.comments.id, this.createdCommentId));
22
22
  }
23
+ toJSON() {
24
+ return {
25
+ type: 'create_comment',
26
+ data: {
27
+ comment: {
28
+ ...this.comment,
29
+ createdAt: this.comment.createdAt instanceof Date ? this.comment.createdAt.toISOString() : this.comment.createdAt,
30
+ },
31
+ description: this.description,
32
+ },
33
+ };
34
+ }
35
+ static fromJSON(json) {
36
+ const commentData = json.data.comment;
37
+ return new CreateCommentCommand({
38
+ comment: {
39
+ ...commentData,
40
+ createdAt: new Date(commentData.createdAt),
41
+ },
42
+ description: json.data.description,
43
+ });
44
+ }
23
45
  }
@@ -1,4 +1,4 @@
1
- import type { UndoableCommand } from '../types.js';
1
+ import type { UndoableCommand, SerializedCommand } from '../types.js';
2
2
  import type { NewTask } from '../../../db/schema.js';
3
3
  interface CreateTaskParams {
4
4
  task: NewTask;
@@ -14,5 +14,9 @@ export declare class CreateTaskCommand implements UndoableCommand {
14
14
  constructor(params: CreateTaskParams);
15
15
  execute(): Promise<void>;
16
16
  undo(): Promise<void>;
17
+ toJSON(): SerializedCommand;
18
+ static fromJSON(json: {
19
+ data: Record<string, unknown>;
20
+ }): CreateTaskCommand;
17
21
  }
18
22
  export {};
@@ -18,7 +18,32 @@ export class CreateTaskCommand {
18
18
  }
19
19
  async undo() {
20
20
  const db = getDb();
21
- // Delete the created task
22
21
  await db.delete(schema.tasks).where(eq(schema.tasks.id, this.createdTaskId));
23
22
  }
23
+ toJSON() {
24
+ return {
25
+ type: 'create_task',
26
+ data: {
27
+ task: {
28
+ ...this.task,
29
+ dueDate: this.task.dueDate instanceof Date ? this.task.dueDate.toISOString() : this.task.dueDate ?? null,
30
+ createdAt: this.task.createdAt instanceof Date ? this.task.createdAt.toISOString() : this.task.createdAt,
31
+ updatedAt: this.task.updatedAt instanceof Date ? this.task.updatedAt.toISOString() : this.task.updatedAt,
32
+ },
33
+ description: this.description,
34
+ },
35
+ };
36
+ }
37
+ static fromJSON(json) {
38
+ const taskData = json.data.task;
39
+ return new CreateTaskCommand({
40
+ task: {
41
+ ...taskData,
42
+ dueDate: taskData.dueDate ? new Date(taskData.dueDate) : null,
43
+ createdAt: new Date(taskData.createdAt),
44
+ updatedAt: new Date(taskData.updatedAt),
45
+ },
46
+ description: json.data.description,
47
+ });
48
+ }
24
49
  }
@@ -1,4 +1,4 @@
1
- import type { UndoableCommand } from '../types.js';
1
+ import type { UndoableCommand, SerializedCommand } from '../types.js';
2
2
  import type { Comment } from '../../../db/schema.js';
3
3
  interface DeleteCommentParams {
4
4
  comment: Comment;
@@ -13,5 +13,9 @@ export declare class DeleteCommentCommand implements UndoableCommand {
13
13
  constructor(params: DeleteCommentParams);
14
14
  execute(): Promise<void>;
15
15
  undo(): Promise<void>;
16
+ toJSON(): SerializedCommand;
17
+ static fromJSON(json: {
18
+ data: Record<string, unknown>;
19
+ }): DeleteCommentCommand;
16
20
  }
17
21
  export {};
@@ -23,4 +23,26 @@ export class DeleteCommentCommand {
23
23
  createdAt: this.comment.createdAt,
24
24
  });
25
25
  }
26
+ toJSON() {
27
+ return {
28
+ type: 'delete_comment',
29
+ data: {
30
+ comment: {
31
+ ...this.comment,
32
+ createdAt: this.comment.createdAt.toISOString(),
33
+ },
34
+ description: this.description,
35
+ },
36
+ };
37
+ }
38
+ static fromJSON(json) {
39
+ const commentData = json.data.comment;
40
+ return new DeleteCommentCommand({
41
+ comment: {
42
+ ...commentData,
43
+ createdAt: new Date(commentData.createdAt),
44
+ },
45
+ description: json.data.description,
46
+ });
47
+ }
26
48
  }
@@ -1,8 +1,9 @@
1
- import type { UndoableCommand } from '../types.js';
2
- import type { Task } from '../../../db/schema.js';
1
+ import type { UndoableCommand, SerializedCommand } from '../types.js';
2
+ import type { Task, Comment } from '../../../db/schema.js';
3
3
  interface DeleteTaskParams {
4
4
  task: Task;
5
5
  description: string;
6
+ savedComments?: Comment[];
6
7
  }
7
8
  /**
8
9
  * Command to delete a task (and its comments)
@@ -14,5 +15,9 @@ export declare class DeleteTaskCommand implements UndoableCommand {
14
15
  constructor(params: DeleteTaskParams);
15
16
  execute(): Promise<void>;
16
17
  undo(): Promise<void>;
18
+ toJSON(): SerializedCommand;
19
+ static fromJSON(json: {
20
+ data: Record<string, unknown>;
21
+ }): DeleteTaskCommand;
17
22
  }
18
23
  export {};
@@ -10,6 +10,9 @@ export class DeleteTaskCommand {
10
10
  constructor(params) {
11
11
  this.task = params.task;
12
12
  this.description = params.description;
13
+ if (params.savedComments) {
14
+ this.savedComments = params.savedComments;
15
+ }
13
16
  }
14
17
  async execute() {
15
18
  const db = getDb();
@@ -35,6 +38,7 @@ export class DeleteTaskCommand {
35
38
  parentId: this.task.parentId,
36
39
  waitingFor: this.task.waitingFor,
37
40
  dueDate: this.task.dueDate,
41
+ completedAt: this.task.completedAt,
38
42
  createdAt: this.task.createdAt,
39
43
  updatedAt: this.task.updatedAt,
40
44
  });
@@ -48,4 +52,41 @@ export class DeleteTaskCommand {
48
52
  });
49
53
  }
50
54
  }
55
+ toJSON() {
56
+ return {
57
+ type: 'delete_task',
58
+ data: {
59
+ task: {
60
+ ...this.task,
61
+ dueDate: this.task.dueDate ? this.task.dueDate.toISOString() : null,
62
+ completedAt: this.task.completedAt ? this.task.completedAt.toISOString() : null,
63
+ createdAt: this.task.createdAt.toISOString(),
64
+ updatedAt: this.task.updatedAt.toISOString(),
65
+ },
66
+ savedComments: this.savedComments.map(c => ({
67
+ ...c,
68
+ createdAt: c.createdAt.toISOString(),
69
+ })),
70
+ description: this.description,
71
+ },
72
+ };
73
+ }
74
+ static fromJSON(json) {
75
+ const taskData = json.data.task;
76
+ const commentsData = json.data.savedComments || [];
77
+ return new DeleteTaskCommand({
78
+ task: {
79
+ ...taskData,
80
+ dueDate: taskData.dueDate ? new Date(taskData.dueDate) : null,
81
+ completedAt: taskData.completedAt ? new Date(taskData.completedAt) : null,
82
+ createdAt: new Date(taskData.createdAt),
83
+ updatedAt: new Date(taskData.updatedAt),
84
+ },
85
+ savedComments: commentsData.map(c => ({
86
+ ...c,
87
+ createdAt: new Date(c.createdAt),
88
+ })),
89
+ description: json.data.description,
90
+ });
91
+ }
51
92
  }
@@ -1,4 +1,4 @@
1
- import type { UndoableCommand } from '../types.js';
1
+ import type { UndoableCommand, SerializedCommand } from '../types.js';
2
2
  interface LinkTaskParams {
3
3
  taskId: string;
4
4
  fromParentId: string | null;
@@ -16,5 +16,9 @@ export declare class LinkTaskCommand implements UndoableCommand {
16
16
  constructor(params: LinkTaskParams);
17
17
  execute(): Promise<void>;
18
18
  undo(): Promise<void>;
19
+ toJSON(): SerializedCommand;
20
+ static fromJSON(json: {
21
+ data: Record<string, unknown>;
22
+ }): LinkTaskCommand;
19
23
  }
20
24
  export {};
@@ -34,4 +34,18 @@ export class LinkTaskCommand {
34
34
  })
35
35
  .where(eq(schema.tasks.id, this.taskId));
36
36
  }
37
+ toJSON() {
38
+ return {
39
+ type: 'link_task',
40
+ data: {
41
+ taskId: this.taskId,
42
+ fromParentId: this.fromParentId,
43
+ toParentId: this.toParentId,
44
+ description: this.description,
45
+ },
46
+ };
47
+ }
48
+ static fromJSON(json) {
49
+ return new LinkTaskCommand(json.data);
50
+ }
37
51
  }
@@ -1,4 +1,4 @@
1
- import type { UndoableCommand } from '../types.js';
1
+ import type { UndoableCommand, SerializedCommand } from '../types.js';
2
2
  import type { TaskStatus } from '../../../db/schema.js';
3
3
  interface MoveTaskParams {
4
4
  taskId: string;
@@ -6,6 +6,7 @@ interface MoveTaskParams {
6
6
  toStatus: TaskStatus;
7
7
  fromWaitingFor: string | null;
8
8
  toWaitingFor: string | null;
9
+ fromCompletedAt: Date | null;
9
10
  description: string;
10
11
  }
11
12
  /**
@@ -18,8 +19,13 @@ export declare class MoveTaskCommand implements UndoableCommand {
18
19
  private readonly toStatus;
19
20
  private readonly fromWaitingFor;
20
21
  private readonly toWaitingFor;
22
+ private readonly fromCompletedAt;
21
23
  constructor(params: MoveTaskParams);
22
24
  execute(): Promise<void>;
23
25
  undo(): Promise<void>;
26
+ toJSON(): SerializedCommand;
27
+ static fromJSON(json: {
28
+ data: Record<string, unknown>;
29
+ }): MoveTaskCommand;
24
30
  }
25
31
  export {};
@@ -10,12 +10,14 @@ export class MoveTaskCommand {
10
10
  toStatus;
11
11
  fromWaitingFor;
12
12
  toWaitingFor;
13
+ fromCompletedAt;
13
14
  constructor(params) {
14
15
  this.taskId = params.taskId;
15
16
  this.fromStatus = params.fromStatus;
16
17
  this.toStatus = params.toStatus;
17
18
  this.fromWaitingFor = params.fromWaitingFor;
18
19
  this.toWaitingFor = params.toWaitingFor;
20
+ this.fromCompletedAt = params.fromCompletedAt;
19
21
  this.description = params.description;
20
22
  }
21
23
  async execute() {
@@ -25,6 +27,7 @@ export class MoveTaskCommand {
25
27
  .set({
26
28
  status: this.toStatus,
27
29
  waitingFor: this.toWaitingFor,
30
+ completedAt: this.toStatus === 'done' ? new Date() : null,
28
31
  updatedAt: new Date(),
29
32
  })
30
33
  .where(eq(schema.tasks.id, this.taskId));
@@ -36,8 +39,30 @@ export class MoveTaskCommand {
36
39
  .set({
37
40
  status: this.fromStatus,
38
41
  waitingFor: this.fromWaitingFor,
42
+ completedAt: this.fromCompletedAt,
39
43
  updatedAt: new Date(),
40
44
  })
41
45
  .where(eq(schema.tasks.id, this.taskId));
42
46
  }
47
+ toJSON() {
48
+ return {
49
+ type: 'move_task',
50
+ data: {
51
+ taskId: this.taskId,
52
+ fromStatus: this.fromStatus,
53
+ toStatus: this.toStatus,
54
+ fromWaitingFor: this.fromWaitingFor,
55
+ toWaitingFor: this.toWaitingFor,
56
+ fromCompletedAt: this.fromCompletedAt ? this.fromCompletedAt.toISOString() : null,
57
+ description: this.description,
58
+ },
59
+ };
60
+ }
61
+ static fromJSON(json) {
62
+ const data = json.data;
63
+ return new MoveTaskCommand({
64
+ ...data,
65
+ fromCompletedAt: data.fromCompletedAt ? new Date(data.fromCompletedAt) : null,
66
+ });
67
+ }
43
68
  }
@@ -1,4 +1,4 @@
1
- import type { UndoableCommand } from '../types.js';
1
+ import type { UndoableCommand, SerializedCommand } from '../types.js';
2
2
  interface SetContextParams {
3
3
  taskId: string;
4
4
  fromContext: string | null;
@@ -16,5 +16,9 @@ export declare class SetContextCommand implements UndoableCommand {
16
16
  constructor(params: SetContextParams);
17
17
  execute(): Promise<void>;
18
18
  undo(): Promise<void>;
19
+ toJSON(): SerializedCommand;
20
+ static fromJSON(json: {
21
+ data: Record<string, unknown>;
22
+ }): SetContextCommand;
19
23
  }
20
24
  export {};
@@ -34,4 +34,18 @@ export class SetContextCommand {
34
34
  })
35
35
  .where(eq(schema.tasks.id, this.taskId));
36
36
  }
37
+ toJSON() {
38
+ return {
39
+ type: 'set_context',
40
+ data: {
41
+ taskId: this.taskId,
42
+ fromContext: this.fromContext,
43
+ toContext: this.toContext,
44
+ description: this.description,
45
+ },
46
+ };
47
+ }
48
+ static fromJSON(json) {
49
+ return new SetContextCommand(json.data);
50
+ }
37
51
  }
@@ -1,4 +1,4 @@
1
- import type { UndoableCommand } from '../types.js';
1
+ import type { UndoableCommand, SerializedCommand } from '../types.js';
2
2
  interface SetEffortParams {
3
3
  taskId: string;
4
4
  fromEffort: string | null;
@@ -16,5 +16,9 @@ export declare class SetEffortCommand implements UndoableCommand {
16
16
  constructor(params: SetEffortParams);
17
17
  execute(): Promise<void>;
18
18
  undo(): Promise<void>;
19
+ toJSON(): SerializedCommand;
20
+ static fromJSON(json: {
21
+ data: Record<string, unknown>;
22
+ }): SetEffortCommand;
19
23
  }
20
24
  export {};
@@ -34,4 +34,18 @@ export class SetEffortCommand {
34
34
  })
35
35
  .where(eq(schema.tasks.id, this.taskId));
36
36
  }
37
+ toJSON() {
38
+ return {
39
+ type: 'set_effort',
40
+ data: {
41
+ taskId: this.taskId,
42
+ fromEffort: this.fromEffort,
43
+ toEffort: this.toEffort,
44
+ description: this.description,
45
+ },
46
+ };
47
+ }
48
+ static fromJSON(json) {
49
+ return new SetEffortCommand(json.data);
50
+ }
37
51
  }