floq 0.3.1 → 0.4.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 CHANGED
@@ -10,6 +10,7 @@ MS-DOSスタイルのテーマを備えたターミナルベースのGTD(Getti
10
10
  - **GTDワークフロー**: Inbox、Next Actions、Waiting For、Someday/Maybe、Done
11
11
  - **カンバンモード**: 3カラムのカンバンボード表示(TODO、Doing、Done)
12
12
  - **プロジェクト**: タスクをプロジェクトに整理(進捗バー表示付き)
13
+ - **コンテキスト**: タスクにコンテキスト(@work、@homeなど)を設定してフィルタリング
13
14
  - **タスク検索**: `/` キーで全タスクを素早く検索
14
15
  - **コメント**: タスクにメモやコメントを追加
15
16
  - **クラウド同期**: [Turso](https://turso.tech/)のembedded replicasによるオプションの同期機能
@@ -57,6 +58,8 @@ floq
57
58
  | `w` | Waiting Forに移動(担当者入力) |
58
59
  | `p` | プロジェクトに変換 |
59
60
  | `P` | プロジェクトに紐付け |
61
+ | `c` | コンテキスト設定 |
62
+ | `@` | コンテキストでフィルター |
60
63
  | `Enter` | タスク詳細を開く / プロジェクトを開く |
61
64
  | `Esc/b` | 戻る |
62
65
  | `/` | タスク検索 |
@@ -106,6 +109,8 @@ floq
106
109
  | `d` | 完了にする |
107
110
  | `m` | タスクを右に移動(→) |
108
111
  | `Backspace` | タスクを左に移動(←) |
112
+ | `c` | コンテキスト設定 |
113
+ | `@` | コンテキストでフィルター |
109
114
  | `Enter` | タスク詳細を開く |
110
115
  | `/` | タスク検索 |
111
116
  | `r` | 更新 |
@@ -142,6 +147,7 @@ floq setup
142
147
  # タスク追加
143
148
  floq add "タスクのタイトル"
144
149
  floq add "タスクのタイトル" -p "プロジェクト名"
150
+ floq add "タスクのタイトル" -c work # コンテキスト付き
145
151
 
146
152
  # タスク一覧
147
153
  floq list # 未完了タスク全て
@@ -168,6 +174,11 @@ floq project complete <id>
168
174
  # コメント
169
175
  floq comment <id> "コメント内容" # コメント追加
170
176
  floq comment <id> # コメント一覧
177
+
178
+ # コンテキスト
179
+ floq context list # コンテキスト一覧
180
+ floq context add <name> # コンテキスト追加
181
+ floq context remove <name> # コンテキスト削除
171
182
  ```
172
183
 
173
184
  ## 設定
package/README.md CHANGED
@@ -10,6 +10,7 @@ A terminal-based GTD (Getting Things Done) task manager with MS-DOS style themes
10
10
  - **GTD Workflow**: Inbox, Next Actions, Waiting For, Someday/Maybe, Done
11
11
  - **Kanban Mode**: 3-column kanban board view (TODO, Doing, Done)
12
12
  - **Projects**: Organize tasks into projects with progress tracking
13
+ - **Contexts**: Tag tasks with contexts (@work, @home, etc.) and filter by context
13
14
  - **Task Search**: Quick search across all tasks with `/`
14
15
  - **Comments**: Add notes and comments to tasks
15
16
  - **Cloud Sync**: Optional sync with [Turso](https://turso.tech/) using embedded replicas
@@ -57,6 +58,8 @@ floq
57
58
  | `w` | Move to Waiting For (prompts for person) |
58
59
  | `p` | Convert to project |
59
60
  | `P` | Link to project |
61
+ | `c` | Set context |
62
+ | `@` | Filter by context |
60
63
  | `Enter` | Open task detail / Open project |
61
64
  | `Esc/b` | Back |
62
65
  | `/` | Search tasks |
@@ -106,6 +109,8 @@ floq
106
109
  | `d` | Mark as done |
107
110
  | `m` | Move task right (→) |
108
111
  | `Backspace` | Move task left (←) |
112
+ | `c` | Set context |
113
+ | `@` | Filter by context |
109
114
  | `Enter` | Open task detail |
110
115
  | `/` | Search tasks |
111
116
  | `r` | Refresh |
@@ -142,6 +147,7 @@ floq setup
142
147
  # Add task
143
148
  floq add "Task title"
144
149
  floq add "Task title" -p "Project name"
150
+ floq add "Task title" -c work # With context
145
151
 
146
152
  # List tasks
147
153
  floq list # All non-done tasks
@@ -168,6 +174,11 @@ floq project complete <id>
168
174
  # Comments
169
175
  floq comment <id> "Comment text" # Add comment
170
176
  floq comment <id> # List comments
177
+
178
+ # Contexts
179
+ floq context list # List available contexts
180
+ floq context add <name> # Add new context
181
+ floq context remove <name> # Remove context
171
182
  ```
172
183
 
173
184
  ## Configuration
package/dist/cli.js CHANGED
@@ -9,6 +9,7 @@ import { markDone } from './commands/done.js';
9
9
  import { addProject, listProjectsCommand, showProject, completeProject, } from './commands/project.js';
10
10
  import { showConfig, setLanguage, setDbPath, resetDbPath, setTheme, selectTheme, setViewModeCommand, selectMode, setTurso, disableTurso, syncCommand, resetDatabase } from './commands/config.js';
11
11
  import { addComment, listComments } from './commands/comment.js';
12
+ import { listContexts, addContextCommand, removeContextCommand } from './commands/context.js';
12
13
  import { runSetupWizard } from './commands/setup.js';
13
14
  import { VERSION } from './version.js';
14
15
  const program = new Command();
@@ -27,6 +28,7 @@ program
27
28
  .description('Add a new task to Inbox')
28
29
  .option('-p, --project <name>', 'Add to a specific project')
29
30
  .option('-d, --description <text>', 'Add a description')
31
+ .option('-c, --context <context>', 'Set context (e.g., work, home)')
30
32
  .action(async (title, options) => {
31
33
  await addTask(title, options);
32
34
  });
@@ -183,6 +185,28 @@ program
183
185
  await listComments(taskId);
184
186
  }
185
187
  });
188
+ // Context commands
189
+ const contextCmd = program
190
+ .command('context')
191
+ .description('Context management commands');
192
+ contextCmd
193
+ .command('list')
194
+ .description('List available contexts')
195
+ .action(async () => {
196
+ await listContexts();
197
+ });
198
+ contextCmd
199
+ .command('add <name>')
200
+ .description('Add a new context')
201
+ .action(async (name) => {
202
+ await addContextCommand(name);
203
+ });
204
+ contextCmd
205
+ .command('remove <name>')
206
+ .description('Remove a context')
207
+ .action(async (name) => {
208
+ await removeContextCommand(name);
209
+ });
186
210
  // Setup wizard command
187
211
  program
188
212
  .command('setup')
@@ -1,6 +1,7 @@
1
1
  interface AddOptions {
2
2
  project?: string;
3
3
  description?: string;
4
+ context?: string;
4
5
  }
5
6
  export declare function addTask(title: string, options: AddOptions): Promise<void>;
6
7
  export {};
@@ -18,12 +18,14 @@ export async function addTask(title, options) {
18
18
  }
19
19
  parentId = projects[0].id;
20
20
  }
21
+ const context = options.context?.toLowerCase().replace(/^@/, '');
21
22
  const task = {
22
23
  id: uuidv4(),
23
24
  title,
24
25
  description: options.description,
25
26
  status: 'inbox',
26
27
  parentId,
28
+ context: context || null,
27
29
  createdAt: now,
28
30
  updatedAt: now,
29
31
  };
@@ -0,0 +1,3 @@
1
+ export declare function listContexts(): Promise<void>;
2
+ export declare function addContextCommand(name: string): Promise<void>;
3
+ export declare function removeContextCommand(name: string): Promise<void>;
@@ -0,0 +1,36 @@
1
+ import { getContexts, addContext, removeContext } from '../config.js';
2
+ import { t, fmt } from '../i18n/index.js';
3
+ export async function listContexts() {
4
+ const i18n = t();
5
+ const contexts = getContexts();
6
+ if (contexts.length === 0) {
7
+ console.log(i18n.commands.context.noContexts);
8
+ return;
9
+ }
10
+ console.log(i18n.commands.context.list);
11
+ for (const context of contexts) {
12
+ console.log(` @${context}`);
13
+ }
14
+ }
15
+ export async function addContextCommand(name) {
16
+ const i18n = t();
17
+ const normalized = name.toLowerCase().replace(/^@/, '');
18
+ if (addContext(normalized)) {
19
+ console.log(fmt(i18n.commands.context.added, { context: normalized }));
20
+ }
21
+ else {
22
+ console.error(fmt(i18n.commands.context.alreadyExists, { context: normalized }));
23
+ process.exit(1);
24
+ }
25
+ }
26
+ export async function removeContextCommand(name) {
27
+ const i18n = t();
28
+ const normalized = name.toLowerCase().replace(/^@/, '');
29
+ if (removeContext(normalized)) {
30
+ console.log(fmt(i18n.commands.context.removed, { context: normalized }));
31
+ }
32
+ else {
33
+ console.error(fmt(i18n.commands.context.notFound, { context: normalized }));
34
+ process.exit(1);
35
+ }
36
+ }
package/dist/config.d.ts CHANGED
@@ -12,6 +12,7 @@ export interface Config {
12
12
  theme: ThemeName;
13
13
  viewMode: ViewMode;
14
14
  turso?: TursoConfig;
15
+ contexts?: string[];
15
16
  }
16
17
  export declare function loadConfig(): Config;
17
18
  export declare function saveConfig(updates: Partial<Config>): void;
@@ -26,3 +27,6 @@ export declare function setThemeName(theme: ThemeName): void;
26
27
  export declare function getViewMode(): ViewMode;
27
28
  export declare function setViewMode(viewMode: ViewMode): void;
28
29
  export declare function isFirstRun(): boolean;
30
+ export declare function getContexts(): string[];
31
+ export declare function addContext(context: string): boolean;
32
+ export declare function removeContext(context: string): boolean;
package/dist/config.js CHANGED
@@ -28,10 +28,12 @@ function migrateDbFiles() {
28
28
  }
29
29
  // Run DB file migration on module load
30
30
  migrateDbFiles();
31
+ const DEFAULT_CONTEXTS = ['work', 'home'];
31
32
  const DEFAULT_CONFIG = {
32
33
  locale: 'en',
33
34
  theme: 'modern',
34
35
  viewMode: 'gtd',
36
+ contexts: DEFAULT_CONTEXTS,
35
37
  };
36
38
  let configCache = null;
37
39
  export function loadConfig() {
@@ -113,3 +115,26 @@ export function setViewMode(viewMode) {
113
115
  export function isFirstRun() {
114
116
  return !existsSync(CONFIG_FILE);
115
117
  }
118
+ export function getContexts() {
119
+ return loadConfig().contexts || DEFAULT_CONTEXTS;
120
+ }
121
+ export function addContext(context) {
122
+ const contexts = getContexts();
123
+ const normalized = context.toLowerCase().replace(/^@/, '');
124
+ if (contexts.includes(normalized)) {
125
+ return false;
126
+ }
127
+ saveConfig({ contexts: [...contexts, normalized] });
128
+ return true;
129
+ }
130
+ export function removeContext(context) {
131
+ const contexts = getContexts();
132
+ const normalized = context.toLowerCase().replace(/^@/, '');
133
+ const index = contexts.indexOf(normalized);
134
+ if (index === -1) {
135
+ return false;
136
+ }
137
+ const newContexts = contexts.filter(c => c !== normalized);
138
+ saveConfig({ contexts: newContexts });
139
+ return true;
140
+ }
package/dist/db/index.js CHANGED
@@ -23,6 +23,7 @@ async function initializeRemoteSchema(tursoUrl, authToken) {
23
23
  const tableInfoResult = await remoteClient.execute("PRAGMA table_info(tasks)");
24
24
  const tableInfo = tableInfoResult.rows;
25
25
  const tableExists = tableInfo.length > 0;
26
+ const hasContext = tableInfo.some(col => col.name === 'context');
26
27
  if (!tableExists) {
27
28
  // Fresh install: create new schema on remote
28
29
  await remoteClient.execute(`
@@ -34,6 +35,7 @@ async function initializeRemoteSchema(tursoUrl, authToken) {
34
35
  is_project INTEGER NOT NULL DEFAULT 0,
35
36
  parent_id TEXT,
36
37
  waiting_for TEXT,
38
+ context TEXT,
37
39
  due_date INTEGER,
38
40
  created_at INTEGER NOT NULL,
39
41
  updated_at INTEGER NOT NULL
@@ -42,6 +44,12 @@ async function initializeRemoteSchema(tursoUrl, authToken) {
42
44
  await remoteClient.execute("CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status)");
43
45
  await remoteClient.execute("CREATE INDEX IF NOT EXISTS idx_tasks_parent_id ON tasks(parent_id)");
44
46
  await remoteClient.execute("CREATE INDEX IF NOT EXISTS idx_tasks_is_project ON tasks(is_project)");
47
+ await remoteClient.execute("CREATE INDEX IF NOT EXISTS idx_tasks_context ON tasks(context)");
48
+ }
49
+ else if (!hasContext) {
50
+ // Migration: add context column
51
+ await remoteClient.execute("ALTER TABLE tasks ADD COLUMN context TEXT");
52
+ await remoteClient.execute("CREATE INDEX IF NOT EXISTS idx_tasks_context ON tasks(context)");
45
53
  }
46
54
  // Create comments table
47
55
  await remoteClient.execute(`
@@ -66,6 +74,7 @@ async function initializeLocalSchema() {
66
74
  const tableInfo = tableInfoResult.rows;
67
75
  const hasProjectId = tableInfo.some(col => col.name === 'project_id');
68
76
  const hasIsProject = tableInfo.some(col => col.name === 'is_project');
77
+ const hasContext = tableInfo.some(col => col.name === 'context');
69
78
  const tableExists = tableInfo.length > 0;
70
79
  if (tableExists && hasProjectId && !hasIsProject) {
71
80
  // Migration: old schema -> new schema
@@ -96,6 +105,7 @@ async function initializeLocalSchema() {
96
105
  is_project INTEGER NOT NULL DEFAULT 0,
97
106
  parent_id TEXT,
98
107
  waiting_for TEXT,
108
+ context TEXT,
99
109
  due_date INTEGER,
100
110
  created_at INTEGER NOT NULL,
101
111
  updated_at INTEGER NOT NULL
@@ -104,6 +114,12 @@ async function initializeLocalSchema() {
104
114
  await client.execute("CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status)");
105
115
  await client.execute("CREATE INDEX IF NOT EXISTS idx_tasks_parent_id ON tasks(parent_id)");
106
116
  await client.execute("CREATE INDEX IF NOT EXISTS idx_tasks_is_project ON tasks(is_project)");
117
+ await client.execute("CREATE INDEX IF NOT EXISTS idx_tasks_context ON tasks(context)");
118
+ }
119
+ // Migration: add context column if missing
120
+ if (tableExists && !hasContext) {
121
+ await client.execute("ALTER TABLE tasks ADD COLUMN context TEXT");
122
+ await client.execute("CREATE INDEX IF NOT EXISTS idx_tasks_context ON tasks(context)");
107
123
  }
108
124
  // Create comments table
109
125
  await client.execute(`
@@ -133,6 +133,25 @@ export declare const tasks: import("drizzle-orm/sqlite-core").SQLiteTableWithCol
133
133
  }, {}, {
134
134
  length: number | undefined;
135
135
  }>;
136
+ context: import("drizzle-orm/sqlite-core").SQLiteColumn<{
137
+ name: "context";
138
+ tableName: "tasks";
139
+ dataType: "string";
140
+ columnType: "SQLiteText";
141
+ data: string;
142
+ driverParam: string;
143
+ notNull: false;
144
+ hasDefault: false;
145
+ isPrimaryKey: false;
146
+ isAutoincrement: false;
147
+ hasRuntimeDefault: false;
148
+ enumValues: [string, ...string[]];
149
+ baseColumn: never;
150
+ identity: undefined;
151
+ generated: undefined;
152
+ }, {}, {
153
+ length: number | undefined;
154
+ }>;
136
155
  dueDate: import("drizzle-orm/sqlite-core").SQLiteColumn<{
137
156
  name: "due_date";
138
157
  tableName: "tasks";
package/dist/db/schema.js CHANGED
@@ -7,6 +7,7 @@ export const tasks = sqliteTable('tasks', {
7
7
  isProject: integer('is_project', { mode: 'boolean' }).notNull().default(false),
8
8
  parentId: text('parent_id'),
9
9
  waitingFor: text('waiting_for'),
10
+ context: text('context'),
10
11
  dueDate: integer('due_date', { mode: 'timestamp' }),
11
12
  createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
12
13
  updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
package/dist/i18n/en.d.ts CHANGED
@@ -63,6 +63,14 @@ export declare const en: {
63
63
  noComments: string;
64
64
  listHeader: string;
65
65
  };
66
+ context: {
67
+ list: string;
68
+ added: string;
69
+ removed: string;
70
+ alreadyExists: string;
71
+ notFound: string;
72
+ noContexts: string;
73
+ };
66
74
  };
67
75
  tui: {
68
76
  title: string;
@@ -129,6 +137,8 @@ export declare const en: {
129
137
  taskDetail: string;
130
138
  addComment: string;
131
139
  searchTasks: string;
140
+ filterByContext: string;
141
+ setContext: string;
132
142
  settings: string;
133
143
  changeTheme: string;
134
144
  changeViewMode: string;
@@ -199,6 +209,21 @@ export declare const en: {
199
209
  resultsTitle: string;
200
210
  searchTasks: string;
201
211
  };
212
+ context: {
213
+ label: string;
214
+ filter: string;
215
+ filterHelp: string;
216
+ all: string;
217
+ none: string;
218
+ setContext: string;
219
+ setContextHelp: string;
220
+ contextSet: string;
221
+ contextCleared: string;
222
+ filterActive: string;
223
+ addNew: string;
224
+ newContext: string;
225
+ newContextPlaceholder: string;
226
+ };
202
227
  info: {
203
228
  settings: string;
204
229
  database: string;
@@ -290,6 +315,8 @@ export type HelpTranslations = {
290
315
  taskDetail: string;
291
316
  addComment: string;
292
317
  searchTasks: string;
318
+ filterByContext: string;
319
+ setContext: string;
293
320
  settings: string;
294
321
  changeTheme: string;
295
322
  changeViewMode: string;
@@ -359,6 +386,21 @@ export type SearchTranslations = {
359
386
  resultsTitle: string;
360
387
  searchTasks: string;
361
388
  };
389
+ export type ContextTranslations = {
390
+ label: string;
391
+ filter: string;
392
+ filterHelp: string;
393
+ all: string;
394
+ none: string;
395
+ setContext: string;
396
+ setContextHelp: string;
397
+ contextSet: string;
398
+ contextCleared: string;
399
+ filterActive: string;
400
+ addNew: string;
401
+ newContext: string;
402
+ newContextPlaceholder: string;
403
+ };
362
404
  export type InfoTranslations = {
363
405
  settings: string;
364
406
  database: string;
@@ -404,6 +446,7 @@ export type TuiTranslations = {
404
446
  whatsNew: WhatsNewTranslations;
405
447
  kanbanHelp: KanbanHelpTranslations;
406
448
  search: SearchTranslations;
449
+ context: ContextTranslations;
407
450
  info: InfoTranslations;
408
451
  addComment: string;
409
452
  noComments: string;
@@ -485,6 +528,7 @@ export type Translations = {
485
528
  done: Record<string, string>;
486
529
  project: Record<string, string>;
487
530
  comment: Record<string, string>;
531
+ context: Record<string, string>;
488
532
  };
489
533
  tui: TuiTranslations;
490
534
  setup: SetupTranslations;
package/dist/i18n/en.js CHANGED
@@ -67,6 +67,14 @@ export const en = {
67
67
  noComments: 'No comments',
68
68
  listHeader: 'Comments for "{title}":',
69
69
  },
70
+ context: {
71
+ list: 'Available contexts:',
72
+ added: 'Added context: @{context}',
73
+ removed: 'Removed context: @{context}',
74
+ alreadyExists: 'Context @{context} already exists.',
75
+ notFound: 'Context @{context} not found.',
76
+ noContexts: 'No contexts configured.',
77
+ },
70
78
  },
71
79
  // TUI
72
80
  tui: {
@@ -138,6 +146,8 @@ export const en = {
138
146
  taskDetail: 'View task details',
139
147
  addComment: 'Add comment',
140
148
  searchTasks: 'Search tasks',
149
+ filterByContext: 'Filter by context',
150
+ setContext: 'Set context',
141
151
  settings: 'Settings',
142
152
  changeTheme: 'Change theme',
143
153
  changeViewMode: 'Change view mode',
@@ -212,6 +222,22 @@ export const en = {
212
222
  resultsTitle: 'Search Results',
213
223
  searchTasks: 'Search tasks',
214
224
  },
225
+ // Context
226
+ context: {
227
+ label: 'Context',
228
+ filter: 'Filter by context',
229
+ filterHelp: 'j/k: select, Enter: confirm, Esc: cancel',
230
+ all: 'All',
231
+ none: 'No context',
232
+ setContext: 'Set context',
233
+ setContextHelp: 'j/k: select, Enter: confirm, Esc: cancel',
234
+ contextSet: 'Set context @{context} for "{title}"',
235
+ contextCleared: 'Cleared context for "{title}"',
236
+ filterActive: '@{context}',
237
+ addNew: '+ New context',
238
+ newContext: 'New context: ',
239
+ newContextPlaceholder: 'Enter context name...',
240
+ },
215
241
  // Info tab
216
242
  info: {
217
243
  settings: 'Settings',
package/dist/i18n/ja.js CHANGED
@@ -67,6 +67,14 @@ export const ja = {
67
67
  noComments: 'コメントはありません',
68
68
  listHeader: '「{title}」のコメント:',
69
69
  },
70
+ context: {
71
+ list: '利用可能なコンテキスト:',
72
+ added: 'コンテキストを追加しました: @{context}',
73
+ removed: 'コンテキストを削除しました: @{context}',
74
+ alreadyExists: 'コンテキスト @{context} は既に存在します。',
75
+ notFound: 'コンテキスト @{context} が見つかりません。',
76
+ noContexts: 'コンテキストが設定されていません。',
77
+ },
70
78
  },
71
79
  // TUI
72
80
  tui: {
@@ -138,6 +146,8 @@ export const ja = {
138
146
  taskDetail: 'タスク詳細を表示',
139
147
  addComment: 'コメント追加',
140
148
  searchTasks: 'タスク検索',
149
+ filterByContext: 'コンテキストフィルター',
150
+ setContext: 'コンテキスト設定',
141
151
  settings: '設定',
142
152
  changeTheme: 'テーマ変更',
143
153
  changeViewMode: '表示モード変更',
@@ -212,6 +222,22 @@ export const ja = {
212
222
  resultsTitle: '検索結果',
213
223
  searchTasks: 'タスク検索',
214
224
  },
225
+ // Context
226
+ context: {
227
+ label: 'コンテキスト',
228
+ filter: 'コンテキストでフィルター',
229
+ filterHelp: 'j/k: 選択, Enter: 確定, Esc: キャンセル',
230
+ all: 'すべて',
231
+ none: 'コンテキストなし',
232
+ setContext: 'コンテキスト設定',
233
+ setContextHelp: 'j/k: 選択, Enter: 確定, Esc: キャンセル',
234
+ contextSet: '「{title}」にコンテキスト @{context} を設定しました',
235
+ contextCleared: '「{title}」のコンテキストをクリアしました',
236
+ filterActive: '@{context}',
237
+ addNew: '+ 新規コンテキスト',
238
+ newContext: '新規コンテキスト: ',
239
+ newContextPlaceholder: 'コンテキスト名を入力...',
240
+ },
215
241
  // Info tab
216
242
  info: {
217
243
  settings: '設定',
package/dist/ui/App.js CHANGED
@@ -16,10 +16,10 @@ import { LanguageSelector } from './LanguageSelector.js';
16
16
  import { getDb, schema } from '../db/index.js';
17
17
  import { t, fmt } from '../i18n/index.js';
18
18
  import { ThemeProvider, useTheme } from './theme/index.js';
19
- import { getThemeName, getViewMode, setThemeName, setViewMode, setLocale, isTursoEnabled } from '../config.js';
19
+ import { getThemeName, getViewMode, setThemeName, setViewMode, setLocale, isTursoEnabled, getContexts, addContext } from '../config.js';
20
20
  import { KanbanBoard } from './components/KanbanBoard.js';
21
21
  import { VERSION } from '../version.js';
22
- import { HistoryProvider, useHistory, CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, } from './history/index.js';
22
+ import { HistoryProvider, useHistory, CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, } from './history/index.js';
23
23
  const TABS = ['inbox', 'next', 'waiting', 'someday', 'projects', 'done'];
24
24
  export function App() {
25
25
  const [themeName, setThemeNameState] = useState(getThemeName);
@@ -87,6 +87,10 @@ function AppContent({ onOpenSettings }) {
87
87
  const [searchQuery, setSearchQuery] = useState('');
88
88
  const [searchResults, setSearchResults] = useState([]);
89
89
  const [searchResultIndex, setSearchResultIndex] = useState(0);
90
+ // Context filter state
91
+ const [contextFilter, setContextFilter] = useState(null); // null = all, '' = no context, string = specific context
92
+ const [contextSelectIndex, setContextSelectIndex] = useState(0);
93
+ const [availableContexts, setAvailableContexts] = useState([]);
90
94
  const i18n = t();
91
95
  const loadTasks = useCallback(async () => {
92
96
  const db = getDb();
@@ -101,12 +105,24 @@ function AppContent({ onOpenSettings }) {
101
105
  // Load all tasks (including project children) by status
102
106
  const statusList = ['inbox', 'next', 'waiting', 'someday', 'done'];
103
107
  for (const status of statusList) {
104
- newTasks[status] = await db
108
+ let allTasks = await db
105
109
  .select()
106
110
  .from(schema.tasks)
107
111
  .where(and(eq(schema.tasks.status, status), eq(schema.tasks.isProject, false)));
112
+ // Apply context filter
113
+ if (contextFilter !== null) {
114
+ if (contextFilter === '') {
115
+ // Filter to tasks with no context
116
+ allTasks = allTasks.filter(t => !t.context);
117
+ }
118
+ else {
119
+ // Filter to specific context
120
+ allTasks = allTasks.filter(t => t.context === contextFilter);
121
+ }
122
+ }
123
+ newTasks[status] = allTasks;
108
124
  }
109
- // Load projects (isProject = true, not done)
125
+ // Load projects (isProject = true, not done) - projects don't get context filtered
110
126
  newTasks.projects = await db
111
127
  .select()
112
128
  .from(schema.tasks)
@@ -124,7 +140,8 @@ function AppContent({ onOpenSettings }) {
124
140
  }
125
141
  setProjectProgress(progress);
126
142
  setTasks(newTasks);
127
- }, []);
143
+ setAvailableContexts(getContexts());
144
+ }, [contextFilter]);
128
145
  // Get parent project for a task
129
146
  const getParentProject = (parentId) => {
130
147
  if (!parentId)
@@ -258,6 +275,23 @@ function AppContent({ onOpenSettings }) {
258
275
  setSearchResultIndex(0);
259
276
  return;
260
277
  }
278
+ // Handle add-context mode submit
279
+ if (mode === 'add-context') {
280
+ if (value.trim()) {
281
+ const newContext = value.trim().toLowerCase().replace(/^@/, '');
282
+ addContext(newContext);
283
+ setAvailableContexts(getContexts());
284
+ // Set the new context on the current task
285
+ if (currentTasks.length > 0) {
286
+ const task = currentTasks[selectedTaskIndex];
287
+ await setTaskContext(task, newContext);
288
+ }
289
+ }
290
+ setInputValue('');
291
+ setContextSelectIndex(0);
292
+ setMode('normal');
293
+ return;
294
+ }
261
295
  if (value.trim()) {
262
296
  if (mode === 'add-comment' && selectedTask) {
263
297
  await addCommentToTask(selectedTask, value);
@@ -343,6 +377,20 @@ function AppContent({ onOpenSettings }) {
343
377
  setMessage(fmt(i18n.tui.madeProject || 'Made project: {title}', { title: task.title }));
344
378
  await loadTasks();
345
379
  }, [i18n.tui.madeProject, loadTasks, history]);
380
+ const setTaskContext = useCallback(async (task, context) => {
381
+ const description = context
382
+ ? fmt(i18n.tui.context?.contextSet || 'Set context @{context} for "{title}"', { context, title: task.title })
383
+ : fmt(i18n.tui.context?.contextCleared || 'Cleared context for "{title}"', { title: task.title });
384
+ const command = new SetContextCommand({
385
+ taskId: task.id,
386
+ fromContext: task.context,
387
+ toContext: context,
388
+ description,
389
+ });
390
+ await history.execute(command);
391
+ setMessage(description);
392
+ await loadTasks();
393
+ }, [i18n.tui.context, loadTasks, history]);
346
394
  const deleteTask = useCallback(async (task) => {
347
395
  const command = new DeleteTaskCommand({
348
396
  task,
@@ -519,6 +567,87 @@ function AppContent({ onOpenSettings }) {
519
567
  }
520
568
  return;
521
569
  }
570
+ // Handle context-filter mode
571
+ if (mode === 'context-filter') {
572
+ if (key.escape) {
573
+ setContextSelectIndex(0);
574
+ setMode('normal');
575
+ return;
576
+ }
577
+ // Navigate context options (All, No context, then each context)
578
+ const contextOptions = ['all', 'none', ...availableContexts];
579
+ if (key.upArrow || input === 'k') {
580
+ setContextSelectIndex((prev) => (prev > 0 ? prev - 1 : contextOptions.length - 1));
581
+ return;
582
+ }
583
+ if (key.downArrow || input === 'j') {
584
+ setContextSelectIndex((prev) => (prev < contextOptions.length - 1 ? prev + 1 : 0));
585
+ return;
586
+ }
587
+ // Select context with Enter
588
+ if (key.return) {
589
+ const selected = contextOptions[contextSelectIndex];
590
+ if (selected === 'all') {
591
+ setContextFilter(null);
592
+ }
593
+ else if (selected === 'none') {
594
+ setContextFilter('');
595
+ }
596
+ else {
597
+ setContextFilter(selected);
598
+ }
599
+ setContextSelectIndex(0);
600
+ setMode('normal');
601
+ return;
602
+ }
603
+ return;
604
+ }
605
+ // Handle set-context mode
606
+ if (mode === 'set-context') {
607
+ if (key.escape) {
608
+ setContextSelectIndex(0);
609
+ setMode('normal');
610
+ return;
611
+ }
612
+ // Navigate context options (Clear, each context, then + New)
613
+ const contextOptions = ['clear', ...availableContexts, 'new'];
614
+ if (key.upArrow || input === 'k') {
615
+ setContextSelectIndex((prev) => (prev > 0 ? prev - 1 : contextOptions.length - 1));
616
+ return;
617
+ }
618
+ if (key.downArrow || input === 'j') {
619
+ setContextSelectIndex((prev) => (prev < contextOptions.length - 1 ? prev + 1 : 0));
620
+ return;
621
+ }
622
+ // Select context with Enter
623
+ if (key.return && currentTasks.length > 0) {
624
+ const selected = contextOptions[contextSelectIndex];
625
+ if (selected === 'new') {
626
+ setMode('add-context');
627
+ setInputValue('');
628
+ return;
629
+ }
630
+ const task = currentTasks[selectedTaskIndex];
631
+ if (selected === 'clear') {
632
+ setTaskContext(task, null);
633
+ }
634
+ else {
635
+ setTaskContext(task, selected);
636
+ }
637
+ setContextSelectIndex(0);
638
+ setMode('normal');
639
+ return;
640
+ }
641
+ return;
642
+ }
643
+ // Handle add-context mode
644
+ if (mode === 'add-context') {
645
+ if (key.escape) {
646
+ setInputValue('');
647
+ setMode('set-context');
648
+ }
649
+ return;
650
+ }
522
651
  // Handle project-detail mode
523
652
  if (mode === 'project-detail') {
524
653
  if (key.escape || key.backspace || input === 'b') {
@@ -622,6 +751,18 @@ function AppContent({ onOpenSettings }) {
622
751
  setSearchResultIndex(0);
623
752
  return;
624
753
  }
754
+ // Context filter mode
755
+ if (input === '@') {
756
+ setContextSelectIndex(0);
757
+ setMode('context-filter');
758
+ return;
759
+ }
760
+ // Set context mode (c key)
761
+ if (input === 'c' && currentTasks.length > 0 && currentTab !== 'projects') {
762
+ setContextSelectIndex(0);
763
+ setMode('set-context');
764
+ return;
765
+ }
625
766
  // Settings: Theme selector
626
767
  if (input === 'T') {
627
768
  onOpenSettings('theme-select');
@@ -841,12 +982,12 @@ function AppContent({ onOpenSettings }) {
841
982
  const tursoEnabled = isTursoEnabled();
842
983
  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}` }), _jsx(Text, { color: tursoEnabled ? theme.colors.accent : theme.colors.textMuted, children: theme.name === 'modern'
843
984
  ? (tursoEnabled ? ' ☁️ turso' : ' 💾 local')
844
- : (tursoEnabled ? ' [DB]TURSO' : ' [DB]local') })] }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.helpHint })] }), _jsx(Box, { marginBottom: 1, children: TABS.map((tab, index) => {
985
+ : (tursoEnabled ? ' [DB]TURSO' : ' [DB]local') }), contextFilter !== null && (_jsxs(Text, { color: theme.colors.accent, children: [' ', "@", contextFilter === '' ? (i18n.tui.context?.none || 'none') : contextFilter] }))] }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.helpHint })] }), _jsx(Box, { marginBottom: 1, children: TABS.map((tab, index) => {
845
986
  const isActive = index === currentListIndex && mode !== 'project-detail';
846
987
  const count = tasks[tab].length;
847
988
  const label = `${index + 1}:${getTabLabel(tab)}(${count})`;
848
989
  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));
849
- }) }), 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})`] })] })] }), _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) => {
990
+ }) }), 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') })] })] }), _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) => {
850
991
  const isSelected = index === selectedCommentIndex && mode === 'task-detail';
851
992
  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));
852
993
  })) }), 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) => {
@@ -855,7 +996,27 @@ function AppContent({ onOpenSettings }) {
855
996
  return (_jsx(TaskItem, { task: task, isSelected: index === selectedTaskIndex, projectName: parentProject?.title, progress: progress }, task.id));
856
997
  })) })), (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
857
998
  ? `${i18n.tui.newTask}[${selectedProject.title}] `
858
- : 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 })), mode === 'confirm-delete' && taskToDelete && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.accent, bold: true, children: fmt(i18n.tui.deleteConfirm || 'Delete "{title}"? (y/n)', { title: taskToDelete.title }) }) })), 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: [
999
+ : 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) => {
1000
+ const label = ctx === 'all'
1001
+ ? (i18n.tui.context?.all || 'All')
1002
+ : ctx === 'none'
1003
+ ? (i18n.tui.context?.none || 'No context')
1004
+ : `@${ctx}`;
1005
+ const isActive = (ctx === 'all' && contextFilter === null) ||
1006
+ (ctx === 'none' && contextFilter === '') ||
1007
+ (ctx !== 'all' && ctx !== 'none' && contextFilter === ctx);
1008
+ return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? theme.style.selectedPrefix : theme.style.unselectedPrefix, label, isActive && ' *'] }, ctx));
1009
+ }) }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.context?.filterHelp || 'j/k: select, Enter: confirm, Esc: cancel' })] })), mode === 'set-context' && currentTasks.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.setContext || 'Set context', ": ", currentTasks[selectedTaskIndex]?.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: theme.borders.list, borderColor: theme.colors.borderActive, paddingX: 1, children: ['clear', ...availableContexts, 'new'].map((ctx, index) => {
1010
+ const label = ctx === 'clear'
1011
+ ? (i18n.tui.context?.none || 'No context')
1012
+ : ctx === 'new'
1013
+ ? (i18n.tui.context?.addNew || '+ New context')
1014
+ : `@${ctx}`;
1015
+ const currentContext = currentTasks[selectedTaskIndex]?.context;
1016
+ const isActive = (ctx === 'clear' && !currentContext) ||
1017
+ (ctx !== 'clear' && ctx !== 'new' && currentContext === ctx);
1018
+ return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? theme.style.selectedPrefix : theme.style.unselectedPrefix, label, isActive && ' *'] }, ctx));
1019
+ }) }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.context?.setContextHelp || 'j/k: select, Enter: confirm, Esc: cancel' })] })), mode === 'add-context' && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.context?.newContext || 'New context: ' }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: i18n.tui.context?.newContextPlaceholder || 'Enter context name...' }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] })), mode === 'search' && (_jsx(SearchBar, { value: searchQuery, onChange: handleSearchChange, onSubmit: handleInputSubmit })), mode === 'confirm-delete' && taskToDelete && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.accent, bold: true, children: fmt(i18n.tui.deleteConfirm || 'Delete "{title}"? (y/n)', { title: taskToDelete.title }) }) })), 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: [
859
1020
  { key: 'i', label: i18n.tui.keyBar.comment },
860
1021
  { key: 'd', label: i18n.tui.keyBar.delete },
861
1022
  { key: 'P', label: i18n.tui.keyBar.project },
@@ -12,9 +12,9 @@ import { SearchResults } from './SearchResults.js';
12
12
  import { getDb, schema } from '../../db/index.js';
13
13
  import { t, fmt } from '../../i18n/index.js';
14
14
  import { useTheme } from '../theme/index.js';
15
- import { isTursoEnabled } from '../../config.js';
15
+ import { isTursoEnabled, getContexts, addContext } from '../../config.js';
16
16
  import { VERSION } from '../../version.js';
17
- import { useHistory, CreateTaskCommand, MoveTaskCommand, LinkTaskCommand, CreateCommentCommand, DeleteCommentCommand, } from '../history/index.js';
17
+ import { useHistory, CreateTaskCommand, MoveTaskCommand, LinkTaskCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, } from '../history/index.js';
18
18
  const COLUMNS = ['todo', 'doing', 'done'];
19
19
  export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
20
20
  const theme = useTheme();
@@ -43,6 +43,10 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
43
43
  const [searchQuery, setSearchQuery] = useState('');
44
44
  const [searchResults, setSearchResults] = useState([]);
45
45
  const [searchResultIndex, setSearchResultIndex] = useState(0);
46
+ // Context filter state
47
+ const [contextFilter, setContextFilter] = useState(null);
48
+ const [contextSelectIndex, setContextSelectIndex] = useState(0);
49
+ const [availableContexts, setAvailableContexts] = useState([]);
46
50
  const i18n = t();
47
51
  // Status mapping:
48
52
  // TODO = inbox + someday
@@ -50,18 +54,26 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
50
54
  // Done = done
51
55
  const loadTasks = useCallback(async () => {
52
56
  const db = getDb();
57
+ // Apply context filter helper
58
+ const filterByContext = (taskList) => {
59
+ if (contextFilter === null)
60
+ return taskList;
61
+ if (contextFilter === '')
62
+ return taskList.filter(t => !t.context);
63
+ return taskList.filter(t => t.context === contextFilter);
64
+ };
53
65
  // TODO: inbox + someday (non-project tasks)
54
- const todoTasks = await db
66
+ let todoTasks = await db
55
67
  .select()
56
68
  .from(schema.tasks)
57
69
  .where(and(inArray(schema.tasks.status, ['inbox', 'someday']), eq(schema.tasks.isProject, false)));
58
70
  // Doing: next + waiting (non-project tasks)
59
- const doingTasks = await db
71
+ let doingTasks = await db
60
72
  .select()
61
73
  .from(schema.tasks)
62
74
  .where(and(inArray(schema.tasks.status, ['next', 'waiting']), eq(schema.tasks.isProject, false)));
63
75
  // Done: done (non-project tasks)
64
- const doneTasks = await db
76
+ let doneTasks = await db
65
77
  .select()
66
78
  .from(schema.tasks)
67
79
  .where(and(eq(schema.tasks.status, 'done'), eq(schema.tasks.isProject, false)));
@@ -71,12 +83,13 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
71
83
  .from(schema.tasks)
72
84
  .where(and(eq(schema.tasks.isProject, true), eq(schema.tasks.status, 'next')));
73
85
  setTasks({
74
- todo: todoTasks,
75
- doing: doingTasks,
76
- done: doneTasks,
86
+ todo: filterByContext(todoTasks),
87
+ doing: filterByContext(doingTasks),
88
+ done: filterByContext(doneTasks),
77
89
  });
78
90
  setProjects(projectTasks);
79
- }, []);
91
+ setAvailableContexts(getContexts());
92
+ }, [contextFilter]);
80
93
  useEffect(() => {
81
94
  loadTasks();
82
95
  }, [loadTasks]);
@@ -218,6 +231,23 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
218
231
  setSearchResultIndex(0);
219
232
  return;
220
233
  }
234
+ // Handle add-context mode submit
235
+ if (mode === 'add-context') {
236
+ if (value.trim()) {
237
+ const newContext = value.trim().toLowerCase().replace(/^@/, '');
238
+ addContext(newContext);
239
+ setAvailableContexts(getContexts());
240
+ // Set the new context on the current task
241
+ if (currentTasks.length > 0) {
242
+ const task = currentTasks[selectedTaskIndex];
243
+ await setTaskContext(task, newContext);
244
+ }
245
+ }
246
+ setInputValue('');
247
+ setContextSelectIndex(0);
248
+ setMode('normal');
249
+ return;
250
+ }
221
251
  if (value.trim()) {
222
252
  await addTask(value);
223
253
  }
@@ -291,6 +321,20 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
291
321
  setMessage(fmt(i18n.tui.completed, { title: task.title }));
292
322
  await loadTasks();
293
323
  }, [i18n.tui.completed, loadTasks, history]);
324
+ const setTaskContext = useCallback(async (task, context) => {
325
+ const description = context
326
+ ? fmt(i18n.tui.context?.contextSet || 'Set context @{context} for "{title}"', { context, title: task.title })
327
+ : fmt(i18n.tui.context?.contextCleared || 'Cleared context for "{title}"', { title: task.title });
328
+ const command = new SetContextCommand({
329
+ taskId: task.id,
330
+ fromContext: task.context,
331
+ toContext: context,
332
+ description,
333
+ });
334
+ await history.execute(command);
335
+ setMessage(description);
336
+ await loadTasks();
337
+ }, [i18n.tui.context, loadTasks, history]);
294
338
  const getColumnLabel = (column) => {
295
339
  return i18n.kanban[column];
296
340
  };
@@ -401,6 +445,83 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
401
445
  }
402
446
  return;
403
447
  }
448
+ // Handle context-filter mode
449
+ if (mode === 'context-filter') {
450
+ if (key.escape) {
451
+ setContextSelectIndex(0);
452
+ setMode('normal');
453
+ return;
454
+ }
455
+ const contextOptions = ['all', 'none', ...availableContexts];
456
+ if (key.upArrow || input === 'k') {
457
+ setContextSelectIndex((prev) => (prev > 0 ? prev - 1 : contextOptions.length - 1));
458
+ return;
459
+ }
460
+ if (key.downArrow || input === 'j') {
461
+ setContextSelectIndex((prev) => (prev < contextOptions.length - 1 ? prev + 1 : 0));
462
+ return;
463
+ }
464
+ if (key.return) {
465
+ const selected = contextOptions[contextSelectIndex];
466
+ if (selected === 'all') {
467
+ setContextFilter(null);
468
+ }
469
+ else if (selected === 'none') {
470
+ setContextFilter('');
471
+ }
472
+ else {
473
+ setContextFilter(selected);
474
+ }
475
+ setContextSelectIndex(0);
476
+ setMode('normal');
477
+ return;
478
+ }
479
+ return;
480
+ }
481
+ // Handle set-context mode
482
+ if (mode === 'set-context') {
483
+ if (key.escape) {
484
+ setContextSelectIndex(0);
485
+ setMode('normal');
486
+ return;
487
+ }
488
+ const contextOptions = ['clear', ...availableContexts, 'new'];
489
+ if (key.upArrow || input === 'k') {
490
+ setContextSelectIndex((prev) => (prev > 0 ? prev - 1 : contextOptions.length - 1));
491
+ return;
492
+ }
493
+ if (key.downArrow || input === 'j') {
494
+ setContextSelectIndex((prev) => (prev < contextOptions.length - 1 ? prev + 1 : 0));
495
+ return;
496
+ }
497
+ if (key.return && currentTasks.length > 0) {
498
+ const selected = contextOptions[contextSelectIndex];
499
+ if (selected === 'new') {
500
+ setMode('add-context');
501
+ setInputValue('');
502
+ return;
503
+ }
504
+ const task = currentTasks[selectedTaskIndex];
505
+ if (selected === 'clear') {
506
+ setTaskContext(task, null);
507
+ }
508
+ else {
509
+ setTaskContext(task, selected);
510
+ }
511
+ setContextSelectIndex(0);
512
+ setMode('normal');
513
+ return;
514
+ }
515
+ return;
516
+ }
517
+ // Handle add-context mode
518
+ if (mode === 'add-context') {
519
+ if (key.escape) {
520
+ setInputValue('');
521
+ setMode('set-context');
522
+ }
523
+ return;
524
+ }
404
525
  // Clear message on any input
405
526
  if (message) {
406
527
  setMessage(null);
@@ -418,6 +539,18 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
418
539
  setSearchResultIndex(0);
419
540
  return;
420
541
  }
542
+ // Context filter mode
543
+ if (input === '@') {
544
+ setContextSelectIndex(0);
545
+ setMode('context-filter');
546
+ return;
547
+ }
548
+ // Set context mode (c key)
549
+ if (input === 'c' && currentTasks.length > 0) {
550
+ setContextSelectIndex(0);
551
+ setMode('set-context');
552
+ return;
553
+ }
421
554
  // Settings: Theme selector
422
555
  if (input === 'T' && onOpenSettings) {
423
556
  onOpenSettings('theme-select');
@@ -575,10 +708,30 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
575
708
  const tursoEnabled = isTursoEnabled();
576
709
  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.accent, children: " [KANBAN]" }), _jsx(Text, { color: theme.colors.textMuted, children: theme.name === 'modern' ? ` v${VERSION}` : ` VER ${VERSION}` }), _jsx(Text, { color: tursoEnabled ? theme.colors.accent : theme.colors.textMuted, children: theme.name === 'modern'
577
710
  ? (tursoEnabled ? ' ☁️ turso' : ' 💾 local')
578
- : (tursoEnabled ? ' [DB]TURSO' : ' [DB]local') })] }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.helpHint })] }), (mode === 'task-detail' || mode === 'add-comment' || mode === 'select-project') && 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}`, selectedTask.dueDate && ` (${selectedTask.dueDate.toLocaleDateString()})`] })] }), _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) => {
711
+ : (tursoEnabled ? ' [DB]TURSO' : ' [DB]local') }), contextFilter !== null && (_jsxs(Text, { color: theme.colors.accent, children: [' ', "@", contextFilter === '' ? (i18n.tui.context?.none || 'none') : contextFilter] }))] }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.helpHint })] }), (mode === 'task-detail' || mode === 'add-comment' || mode === 'select-project') && 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') })] }), selectedTask.dueDate && (_jsxs(Text, { color: theme.colors.textMuted, children: ["Due: ", selectedTask.dueDate.toLocaleDateString()] }))] }), _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) => {
579
712
  const isSelected = index === selectedCommentIndex && mode === 'task-detail';
580
713
  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));
581
- })) }), 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 === 'select-project' && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project for', ": ", selectedTask.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: theme.borders.list, borderColor: theme.colors.borderActive, paddingX: 1, children: 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(_Fragment, { children: searchQuery && (_jsx(SearchResults, { results: searchResults, selectedIndex: searchResultIndex, query: searchQuery })) })) : (_jsxs(_Fragment, { children: [_jsx(Box, { marginBottom: 1, children: COLUMNS.map((column, index) => (_jsx(Box, { flexGrow: 1, flexBasis: 0, marginRight: index < 2 ? 1 : 0, children: _jsxs(Text, { color: currentColumnIndex === index ? theme.colors.textHighlight : theme.colors.textMuted, children: [index + 1, ":"] }) }, column))) }), _jsx(Box, { flexDirection: "row", children: COLUMNS.map((column, index) => (_jsx(KanbanColumn, { title: getColumnLabel(column), tasks: tasks[column], isActive: index === currentColumnIndex, selectedTaskIndex: selectedTaskIndices[column], columnIndex: index }, column))) })] })), mode === 'add' && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: 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 === '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 === 'select-project' ? (_jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.selectProjectHelp || 'j/k: select, Enter: confirm, Esc: cancel' })) : (mode === 'task-detail' || mode === 'add-comment') ? (theme.style.showFunctionKeys ? (_jsx(FunctionKeyBar, { keys: [
714
+ })) }), 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 === 'select-project' && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project for', ": ", selectedTask.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: theme.borders.list, borderColor: theme.colors.borderActive, paddingX: 1, children: 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(_Fragment, { children: searchQuery && (_jsx(SearchResults, { results: searchResults, selectedIndex: searchResultIndex, query: searchQuery })) })) : 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) => {
715
+ const label = ctx === 'all'
716
+ ? (i18n.tui.context?.all || 'All')
717
+ : ctx === 'none'
718
+ ? (i18n.tui.context?.none || 'No context')
719
+ : `@${ctx}`;
720
+ const isActive = (ctx === 'all' && contextFilter === null) ||
721
+ (ctx === 'none' && contextFilter === '') ||
722
+ (ctx !== 'all' && ctx !== 'none' && contextFilter === ctx);
723
+ return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? theme.style.selectedPrefix : theme.style.unselectedPrefix, label, isActive && ' *'] }, ctx));
724
+ }) }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.context?.filterHelp || 'j/k: select, Enter: confirm, Esc: cancel' })] })) : mode === 'set-context' && currentTasks.length > 0 ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.setContext || 'Set context', ": ", currentTasks[selectedTaskIndex]?.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: theme.borders.list, borderColor: theme.colors.borderActive, paddingX: 1, children: ['clear', ...availableContexts, 'new'].map((ctx, index) => {
725
+ const label = ctx === 'clear'
726
+ ? (i18n.tui.context?.none || 'No context')
727
+ : ctx === 'new'
728
+ ? (i18n.tui.context?.addNew || '+ New context')
729
+ : `@${ctx}`;
730
+ const currentContext = currentTasks[selectedTaskIndex]?.context;
731
+ const isActive = (ctx === 'clear' && !currentContext) ||
732
+ (ctx !== 'clear' && ctx !== 'new' && currentContext === ctx);
733
+ return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? theme.style.selectedPrefix : theme.style.unselectedPrefix, label, isActive && ' *'] }, ctx));
734
+ }) }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.context?.setContextHelp || 'j/k: select, Enter: confirm, Esc: cancel' })] })) : mode === 'add-context' ? (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.context?.newContext || 'New context: ' }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: i18n.tui.context?.newContextPlaceholder || 'Enter context name...' }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] })) : (_jsxs(_Fragment, { children: [_jsx(Box, { marginBottom: 1, children: COLUMNS.map((column, index) => (_jsx(Box, { flexGrow: 1, flexBasis: 0, marginRight: index < 2 ? 1 : 0, children: _jsxs(Text, { color: currentColumnIndex === index ? theme.colors.textHighlight : theme.colors.textMuted, children: [index + 1, ":"] }) }, column))) }), _jsx(Box, { flexDirection: "row", children: COLUMNS.map((column, index) => (_jsx(KanbanColumn, { title: getColumnLabel(column), tasks: tasks[column], isActive: index === currentColumnIndex, selectedTaskIndex: selectedTaskIndices[column], columnIndex: index }, column))) })] })), mode === 'add' && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: 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 === '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 === 'select-project' ? (_jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.selectProjectHelp || 'j/k: select, Enter: confirm, Esc: cancel' })) : (mode === 'task-detail' || mode === 'add-comment') ? (theme.style.showFunctionKeys ? (_jsx(FunctionKeyBar, { keys: [
582
735
  { key: 'i', label: i18n.tui.keyBar.comment },
583
736
  { key: 'd', label: i18n.tui.keyBar.delete },
584
737
  { key: 'P', label: i18n.tui.keyBar.project },
@@ -7,5 +7,5 @@ export function TaskItem({ task, isSelected, projectName, progress }) {
7
7
  const shortId = task.id.slice(0, 8);
8
8
  const i18n = t();
9
9
  const theme = useTheme();
10
- return (_jsx(Box, { children: _jsxs(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [isSelected ? theme.style.selectedPrefix : theme.style.unselectedPrefix, "[", shortId, "] ", task.title, 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(), ")"] }))] }) }));
10
+ return (_jsx(Box, { children: _jsxs(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [isSelected ? theme.style.selectedPrefix : theme.style.unselectedPrefix, "[", shortId, "] ", 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(), ")"] }))] }) }));
11
11
  }
@@ -0,0 +1,20 @@
1
+ import type { UndoableCommand } from '../types.js';
2
+ interface SetContextParams {
3
+ taskId: string;
4
+ fromContext: string | null;
5
+ toContext: string | null;
6
+ description: string;
7
+ }
8
+ /**
9
+ * Command to set/change a task's context
10
+ */
11
+ export declare class SetContextCommand implements UndoableCommand {
12
+ readonly description: string;
13
+ private readonly taskId;
14
+ private readonly fromContext;
15
+ private readonly toContext;
16
+ constructor(params: SetContextParams);
17
+ execute(): Promise<void>;
18
+ undo(): Promise<void>;
19
+ }
20
+ export {};
@@ -0,0 +1,37 @@
1
+ import { eq } from 'drizzle-orm';
2
+ import { getDb, schema } from '../../../db/index.js';
3
+ /**
4
+ * Command to set/change a task's context
5
+ */
6
+ export class SetContextCommand {
7
+ description;
8
+ taskId;
9
+ fromContext;
10
+ toContext;
11
+ constructor(params) {
12
+ this.taskId = params.taskId;
13
+ this.fromContext = params.fromContext;
14
+ this.toContext = params.toContext;
15
+ this.description = params.description;
16
+ }
17
+ async execute() {
18
+ const db = getDb();
19
+ await db
20
+ .update(schema.tasks)
21
+ .set({
22
+ context: this.toContext,
23
+ updatedAt: new Date(),
24
+ })
25
+ .where(eq(schema.tasks.id, this.taskId));
26
+ }
27
+ async undo() {
28
+ const db = getDb();
29
+ await db
30
+ .update(schema.tasks)
31
+ .set({
32
+ context: this.fromContext,
33
+ updatedAt: new Date(),
34
+ })
35
+ .where(eq(schema.tasks.id, this.taskId));
36
+ }
37
+ }
@@ -5,3 +5,4 @@ export { LinkTaskCommand } from './LinkTaskCommand.js';
5
5
  export { ConvertToProjectCommand } from './ConvertToProjectCommand.js';
6
6
  export { CreateCommentCommand } from './CreateCommentCommand.js';
7
7
  export { DeleteCommentCommand } from './DeleteCommentCommand.js';
8
+ export { SetContextCommand } from './SetContextCommand.js';
@@ -5,3 +5,4 @@ export { LinkTaskCommand } from './LinkTaskCommand.js';
5
5
  export { ConvertToProjectCommand } from './ConvertToProjectCommand.js';
6
6
  export { CreateCommentCommand } from './CreateCommentCommand.js';
7
7
  export { DeleteCommentCommand } from './DeleteCommentCommand.js';
8
+ export { SetContextCommand } from './SetContextCommand.js';
@@ -3,4 +3,4 @@ export { MAX_HISTORY_SIZE } from './types.js';
3
3
  export { HistoryManager, getHistoryManager } from './HistoryManager.js';
4
4
  export { HistoryProvider, useHistoryContext } from './HistoryContext.js';
5
5
  export { useHistory } from './useHistory.js';
6
- export { CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, } from './commands/index.js';
6
+ export { CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, } from './commands/index.js';
@@ -5,4 +5,4 @@ export { HistoryManager, getHistoryManager } from './HistoryManager.js';
5
5
  export { HistoryProvider, useHistoryContext } from './HistoryContext.js';
6
6
  export { useHistory } from './useHistory.js';
7
7
  // Commands
8
- export { CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, } from './commands/index.js';
8
+ export { CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, } from './commands/index.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "floq",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Floq - Getting Things Done Task Manager with MS-DOS style themes",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",