floq 0.3.1 → 0.5.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/dist/ui/App.js CHANGED
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { useState, useEffect, useCallback } from 'react';
3
3
  import { Box, Text, useInput, useApp } from 'ink';
4
4
  import TextInput from 'ink-text-input';
5
- import { eq, and } from 'drizzle-orm';
5
+ import { eq, and, gte } from 'drizzle-orm';
6
6
  import { v4 as uuidv4 } from 'uuid';
7
7
  import { TaskItem } from './components/TaskItem.js';
8
8
  import { HelpModal } from './components/HelpModal.js';
@@ -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,35 @@ 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
+ // For done tasks, only show those completed in the last week
109
+ const oneWeekAgo = new Date();
110
+ oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
111
+ const conditions = [
112
+ eq(schema.tasks.status, status),
113
+ eq(schema.tasks.isProject, false),
114
+ ];
115
+ // Filter done tasks to last week only
116
+ if (status === 'done') {
117
+ conditions.push(gte(schema.tasks.updatedAt, oneWeekAgo));
118
+ }
119
+ let allTasks = await db
105
120
  .select()
106
121
  .from(schema.tasks)
107
- .where(and(eq(schema.tasks.status, status), eq(schema.tasks.isProject, false)));
122
+ .where(and(...conditions));
123
+ // Apply context filter
124
+ if (contextFilter !== null) {
125
+ if (contextFilter === '') {
126
+ // Filter to tasks with no context
127
+ allTasks = allTasks.filter(t => !t.context);
128
+ }
129
+ else {
130
+ // Filter to specific context
131
+ allTasks = allTasks.filter(t => t.context === contextFilter);
132
+ }
133
+ }
134
+ newTasks[status] = allTasks;
108
135
  }
109
- // Load projects (isProject = true, not done)
136
+ // Load projects (isProject = true, not done) - projects don't get context filtered
110
137
  newTasks.projects = await db
111
138
  .select()
112
139
  .from(schema.tasks)
@@ -124,7 +151,8 @@ function AppContent({ onOpenSettings }) {
124
151
  }
125
152
  setProjectProgress(progress);
126
153
  setTasks(newTasks);
127
- }, []);
154
+ setAvailableContexts(getContexts());
155
+ }, [contextFilter]);
128
156
  // Get parent project for a task
129
157
  const getParentProject = (parentId) => {
130
158
  if (!parentId)
@@ -188,7 +216,7 @@ function AppContent({ onOpenSettings }) {
188
216
  setMode('normal');
189
217
  }
190
218
  }, [tasks]);
191
- const addTask = useCallback(async (title, parentId) => {
219
+ const addTask = useCallback(async (title, parentId, context) => {
192
220
  if (!title.trim())
193
221
  return;
194
222
  const now = new Date();
@@ -199,6 +227,7 @@ function AppContent({ onOpenSettings }) {
199
227
  title: title.trim(),
200
228
  status: parentId ? 'next' : 'inbox',
201
229
  parentId: parentId || null,
230
+ context: context || null,
202
231
  createdAt: now,
203
232
  updatedAt: now,
204
233
  },
@@ -258,18 +287,36 @@ function AppContent({ onOpenSettings }) {
258
287
  setSearchResultIndex(0);
259
288
  return;
260
289
  }
290
+ // Handle add-context mode submit
291
+ if (mode === 'add-context') {
292
+ if (value.trim()) {
293
+ const newContext = value.trim().toLowerCase().replace(/^@/, '');
294
+ addContext(newContext);
295
+ setAvailableContexts(getContexts());
296
+ // Set the new context on the current task
297
+ if (currentTasks.length > 0) {
298
+ const task = currentTasks[selectedTaskIndex];
299
+ await setTaskContext(task, newContext);
300
+ }
301
+ }
302
+ setInputValue('');
303
+ setContextSelectIndex(0);
304
+ setMode('normal');
305
+ return;
306
+ }
261
307
  if (value.trim()) {
262
308
  if (mode === 'add-comment' && selectedTask) {
263
309
  await addCommentToTask(selectedTask, value);
264
310
  setMode('task-detail');
265
311
  }
266
312
  else if (mode === 'add-to-project' && selectedProject) {
267
- await addTask(value, selectedProject.id);
313
+ await addTask(value, selectedProject.id, contextFilter && contextFilter !== '' ? contextFilter : null);
268
314
  await loadProjectTasks(selectedProject.id);
269
315
  setMode('project-detail');
270
316
  }
271
317
  else {
272
- await addTask(value);
318
+ // Pass contextFilter when adding a task, so it inherits the current filter context
319
+ await addTask(value, undefined, contextFilter && contextFilter !== '' ? contextFilter : null);
273
320
  setMode('normal');
274
321
  }
275
322
  }
@@ -343,6 +390,20 @@ function AppContent({ onOpenSettings }) {
343
390
  setMessage(fmt(i18n.tui.madeProject || 'Made project: {title}', { title: task.title }));
344
391
  await loadTasks();
345
392
  }, [i18n.tui.madeProject, loadTasks, history]);
393
+ const setTaskContext = useCallback(async (task, context) => {
394
+ const description = context
395
+ ? fmt(i18n.tui.context?.contextSet || 'Set context @{context} for "{title}"', { context, title: task.title })
396
+ : fmt(i18n.tui.context?.contextCleared || 'Cleared context for "{title}"', { title: task.title });
397
+ const command = new SetContextCommand({
398
+ taskId: task.id,
399
+ fromContext: task.context,
400
+ toContext: context,
401
+ description,
402
+ });
403
+ await history.execute(command);
404
+ setMessage(description);
405
+ await loadTasks();
406
+ }, [i18n.tui.context, loadTasks, history]);
346
407
  const deleteTask = useCallback(async (task) => {
347
408
  const command = new DeleteTaskCommand({
348
409
  task,
@@ -519,6 +580,87 @@ function AppContent({ onOpenSettings }) {
519
580
  }
520
581
  return;
521
582
  }
583
+ // Handle context-filter mode
584
+ if (mode === 'context-filter') {
585
+ if (key.escape) {
586
+ setContextSelectIndex(0);
587
+ setMode('normal');
588
+ return;
589
+ }
590
+ // Navigate context options (All, No context, then each context)
591
+ const contextOptions = ['all', 'none', ...availableContexts];
592
+ if (key.upArrow || input === 'k') {
593
+ setContextSelectIndex((prev) => (prev > 0 ? prev - 1 : contextOptions.length - 1));
594
+ return;
595
+ }
596
+ if (key.downArrow || input === 'j') {
597
+ setContextSelectIndex((prev) => (prev < contextOptions.length - 1 ? prev + 1 : 0));
598
+ return;
599
+ }
600
+ // Select context with Enter
601
+ if (key.return) {
602
+ const selected = contextOptions[contextSelectIndex];
603
+ if (selected === 'all') {
604
+ setContextFilter(null);
605
+ }
606
+ else if (selected === 'none') {
607
+ setContextFilter('');
608
+ }
609
+ else {
610
+ setContextFilter(selected);
611
+ }
612
+ setContextSelectIndex(0);
613
+ setMode('normal');
614
+ return;
615
+ }
616
+ return;
617
+ }
618
+ // Handle set-context mode
619
+ if (mode === 'set-context') {
620
+ if (key.escape) {
621
+ setContextSelectIndex(0);
622
+ setMode('normal');
623
+ return;
624
+ }
625
+ // Navigate context options (Clear, each context, then + New)
626
+ const contextOptions = ['clear', ...availableContexts, 'new'];
627
+ if (key.upArrow || input === 'k') {
628
+ setContextSelectIndex((prev) => (prev > 0 ? prev - 1 : contextOptions.length - 1));
629
+ return;
630
+ }
631
+ if (key.downArrow || input === 'j') {
632
+ setContextSelectIndex((prev) => (prev < contextOptions.length - 1 ? prev + 1 : 0));
633
+ return;
634
+ }
635
+ // Select context with Enter
636
+ if (key.return && currentTasks.length > 0) {
637
+ const selected = contextOptions[contextSelectIndex];
638
+ if (selected === 'new') {
639
+ setMode('add-context');
640
+ setInputValue('');
641
+ return;
642
+ }
643
+ const task = currentTasks[selectedTaskIndex];
644
+ if (selected === 'clear') {
645
+ setTaskContext(task, null);
646
+ }
647
+ else {
648
+ setTaskContext(task, selected);
649
+ }
650
+ setContextSelectIndex(0);
651
+ setMode('normal');
652
+ return;
653
+ }
654
+ return;
655
+ }
656
+ // Handle add-context mode
657
+ if (mode === 'add-context') {
658
+ if (key.escape) {
659
+ setInputValue('');
660
+ setMode('set-context');
661
+ }
662
+ return;
663
+ }
522
664
  // Handle project-detail mode
523
665
  if (mode === 'project-detail') {
524
666
  if (key.escape || key.backspace || input === 'b') {
@@ -622,6 +764,18 @@ function AppContent({ onOpenSettings }) {
622
764
  setSearchResultIndex(0);
623
765
  return;
624
766
  }
767
+ // Context filter mode
768
+ if (input === '@') {
769
+ setContextSelectIndex(0);
770
+ setMode('context-filter');
771
+ return;
772
+ }
773
+ // Set context mode (c key)
774
+ if (input === 'c' && currentTasks.length > 0 && currentTab !== 'projects') {
775
+ setContextSelectIndex(0);
776
+ setMode('set-context');
777
+ return;
778
+ }
625
779
  // Settings: Theme selector
626
780
  if (input === 'T') {
627
781
  onOpenSettings('theme-select');
@@ -841,12 +995,12 @@ function AppContent({ onOpenSettings }) {
841
995
  const tursoEnabled = isTursoEnabled();
842
996
  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
997
  ? (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) => {
998
+ : (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
999
  const isActive = index === currentListIndex && mode !== 'project-detail';
846
1000
  const count = tasks[tab].length;
847
1001
  const label = `${index + 1}:${getTabLabel(tab)}(${count})`;
848
1002
  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) => {
1003
+ }) }), 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
1004
  const isSelected = index === selectedCommentIndex && mode === 'task-detail';
851
1005
  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
1006
  })) }), 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 +1009,27 @@ function AppContent({ onOpenSettings }) {
855
1009
  return (_jsx(TaskItem, { task: task, isSelected: index === selectedTaskIndex, projectName: parentProject?.title, progress: progress }, task.id));
856
1010
  })) })), (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
1011
  ? `${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: [
1012
+ : 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) => {
1013
+ const label = ctx === 'all'
1014
+ ? (i18n.tui.context?.all || 'All')
1015
+ : ctx === 'none'
1016
+ ? (i18n.tui.context?.none || 'No context')
1017
+ : `@${ctx}`;
1018
+ const isActive = (ctx === 'all' && contextFilter === null) ||
1019
+ (ctx === 'none' && contextFilter === '') ||
1020
+ (ctx !== 'all' && ctx !== 'none' && contextFilter === ctx);
1021
+ 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));
1022
+ }) }), _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) => {
1023
+ const label = ctx === 'clear'
1024
+ ? (i18n.tui.context?.none || 'No context')
1025
+ : ctx === 'new'
1026
+ ? (i18n.tui.context?.addNew || '+ New context')
1027
+ : `@${ctx}`;
1028
+ const currentContext = currentTasks[selectedTaskIndex]?.context;
1029
+ const isActive = (ctx === 'clear' && !currentContext) ||
1030
+ (ctx !== 'clear' && ctx !== 'new' && currentContext === ctx);
1031
+ 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));
1032
+ }) }), _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
1033
  { key: 'i', label: i18n.tui.keyBar.comment },
860
1034
  { key: 'd', label: i18n.tui.keyBar.delete },
861
1035
  { key: 'P', label: i18n.tui.keyBar.project },
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
2
2
  import { useState, useEffect, useCallback } from 'react';
3
3
  import { Box, Text, useInput, useApp } from 'ink';
4
4
  import TextInput from 'ink-text-input';
5
- import { eq, and, inArray } from 'drizzle-orm';
5
+ import { eq, and, inArray, gte } from 'drizzle-orm';
6
6
  import { v4 as uuidv4 } from 'uuid';
7
7
  import { KanbanColumn } from './KanbanColumn.js';
8
8
  import { HelpModal } from './HelpModal.js';
@@ -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,33 +54,44 @@ 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
- // Done: done (non-project tasks)
64
- const doneTasks = await db
75
+ // Done: done (non-project tasks) - only show last week
76
+ const oneWeekAgo = new Date();
77
+ oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
78
+ let doneTasks = await db
65
79
  .select()
66
80
  .from(schema.tasks)
67
- .where(and(eq(schema.tasks.status, 'done'), eq(schema.tasks.isProject, false)));
81
+ .where(and(eq(schema.tasks.status, 'done'), eq(schema.tasks.isProject, false), gte(schema.tasks.updatedAt, oneWeekAgo)));
68
82
  // Load projects for linking
69
83
  const projectTasks = await db
70
84
  .select()
71
85
  .from(schema.tasks)
72
86
  .where(and(eq(schema.tasks.isProject, true), eq(schema.tasks.status, 'next')));
73
87
  setTasks({
74
- todo: todoTasks,
75
- doing: doingTasks,
76
- done: doneTasks,
88
+ todo: filterByContext(todoTasks),
89
+ doing: filterByContext(doingTasks),
90
+ done: filterByContext(doneTasks),
77
91
  });
78
92
  setProjects(projectTasks);
79
- }, []);
93
+ setAvailableContexts(getContexts());
94
+ }, [contextFilter]);
80
95
  useEffect(() => {
81
96
  loadTasks();
82
97
  }, [loadTasks]);
@@ -176,7 +191,7 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
176
191
  setMode('normal');
177
192
  }
178
193
  }, [tasks]);
179
- const addTask = useCallback(async (title) => {
194
+ const addTask = useCallback(async (title, context) => {
180
195
  if (!title.trim())
181
196
  return;
182
197
  const now = new Date();
@@ -186,6 +201,7 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
186
201
  id: taskId,
187
202
  title: title.trim(),
188
203
  status: 'inbox', // New tasks go to inbox (which maps to TODO)
204
+ context: context || null,
189
205
  createdAt: now,
190
206
  updatedAt: now,
191
207
  },
@@ -218,8 +234,26 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
218
234
  setSearchResultIndex(0);
219
235
  return;
220
236
  }
237
+ // Handle add-context mode submit
238
+ if (mode === 'add-context') {
239
+ if (value.trim()) {
240
+ const newContext = value.trim().toLowerCase().replace(/^@/, '');
241
+ addContext(newContext);
242
+ setAvailableContexts(getContexts());
243
+ // Set the new context on the current task
244
+ if (currentTasks.length > 0) {
245
+ const task = currentTasks[selectedTaskIndex];
246
+ await setTaskContext(task, newContext);
247
+ }
248
+ }
249
+ setInputValue('');
250
+ setContextSelectIndex(0);
251
+ setMode('normal');
252
+ return;
253
+ }
221
254
  if (value.trim()) {
222
- await addTask(value);
255
+ // Pass contextFilter when adding a task, so it inherits the current filter context
256
+ await addTask(value, contextFilter && contextFilter !== '' ? contextFilter : null);
223
257
  }
224
258
  setInputValue('');
225
259
  setMode('normal');
@@ -291,6 +325,20 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
291
325
  setMessage(fmt(i18n.tui.completed, { title: task.title }));
292
326
  await loadTasks();
293
327
  }, [i18n.tui.completed, loadTasks, history]);
328
+ const setTaskContext = useCallback(async (task, context) => {
329
+ const description = context
330
+ ? fmt(i18n.tui.context?.contextSet || 'Set context @{context} for "{title}"', { context, title: task.title })
331
+ : fmt(i18n.tui.context?.contextCleared || 'Cleared context for "{title}"', { title: task.title });
332
+ const command = new SetContextCommand({
333
+ taskId: task.id,
334
+ fromContext: task.context,
335
+ toContext: context,
336
+ description,
337
+ });
338
+ await history.execute(command);
339
+ setMessage(description);
340
+ await loadTasks();
341
+ }, [i18n.tui.context, loadTasks, history]);
294
342
  const getColumnLabel = (column) => {
295
343
  return i18n.kanban[column];
296
344
  };
@@ -401,6 +449,83 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
401
449
  }
402
450
  return;
403
451
  }
452
+ // Handle context-filter mode
453
+ if (mode === 'context-filter') {
454
+ if (key.escape) {
455
+ setContextSelectIndex(0);
456
+ setMode('normal');
457
+ return;
458
+ }
459
+ const contextOptions = ['all', 'none', ...availableContexts];
460
+ if (key.upArrow || input === 'k') {
461
+ setContextSelectIndex((prev) => (prev > 0 ? prev - 1 : contextOptions.length - 1));
462
+ return;
463
+ }
464
+ if (key.downArrow || input === 'j') {
465
+ setContextSelectIndex((prev) => (prev < contextOptions.length - 1 ? prev + 1 : 0));
466
+ return;
467
+ }
468
+ if (key.return) {
469
+ const selected = contextOptions[contextSelectIndex];
470
+ if (selected === 'all') {
471
+ setContextFilter(null);
472
+ }
473
+ else if (selected === 'none') {
474
+ setContextFilter('');
475
+ }
476
+ else {
477
+ setContextFilter(selected);
478
+ }
479
+ setContextSelectIndex(0);
480
+ setMode('normal');
481
+ return;
482
+ }
483
+ return;
484
+ }
485
+ // Handle set-context mode
486
+ if (mode === 'set-context') {
487
+ if (key.escape) {
488
+ setContextSelectIndex(0);
489
+ setMode('normal');
490
+ return;
491
+ }
492
+ const contextOptions = ['clear', ...availableContexts, 'new'];
493
+ if (key.upArrow || input === 'k') {
494
+ setContextSelectIndex((prev) => (prev > 0 ? prev - 1 : contextOptions.length - 1));
495
+ return;
496
+ }
497
+ if (key.downArrow || input === 'j') {
498
+ setContextSelectIndex((prev) => (prev < contextOptions.length - 1 ? prev + 1 : 0));
499
+ return;
500
+ }
501
+ if (key.return && currentTasks.length > 0) {
502
+ const selected = contextOptions[contextSelectIndex];
503
+ if (selected === 'new') {
504
+ setMode('add-context');
505
+ setInputValue('');
506
+ return;
507
+ }
508
+ const task = currentTasks[selectedTaskIndex];
509
+ if (selected === 'clear') {
510
+ setTaskContext(task, null);
511
+ }
512
+ else {
513
+ setTaskContext(task, selected);
514
+ }
515
+ setContextSelectIndex(0);
516
+ setMode('normal');
517
+ return;
518
+ }
519
+ return;
520
+ }
521
+ // Handle add-context mode
522
+ if (mode === 'add-context') {
523
+ if (key.escape) {
524
+ setInputValue('');
525
+ setMode('set-context');
526
+ }
527
+ return;
528
+ }
404
529
  // Clear message on any input
405
530
  if (message) {
406
531
  setMessage(null);
@@ -418,6 +543,18 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
418
543
  setSearchResultIndex(0);
419
544
  return;
420
545
  }
546
+ // Context filter mode
547
+ if (input === '@') {
548
+ setContextSelectIndex(0);
549
+ setMode('context-filter');
550
+ return;
551
+ }
552
+ // Set context mode (c key)
553
+ if (input === 'c' && currentTasks.length > 0) {
554
+ setContextSelectIndex(0);
555
+ setMode('set-context');
556
+ return;
557
+ }
421
558
  // Settings: Theme selector
422
559
  if (input === 'T' && onOpenSettings) {
423
560
  onOpenSettings('theme-select');
@@ -575,10 +712,30 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
575
712
  const tursoEnabled = isTursoEnabled();
576
713
  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
714
  ? (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) => {
715
+ : (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
716
  const isSelected = index === selectedCommentIndex && mode === 'task-detail';
580
717
  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: [
718
+ })) }), 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, viewMode: "kanban" })) })) : 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) => {
719
+ const label = ctx === 'all'
720
+ ? (i18n.tui.context?.all || 'All')
721
+ : ctx === 'none'
722
+ ? (i18n.tui.context?.none || 'No context')
723
+ : `@${ctx}`;
724
+ const isActive = (ctx === 'all' && contextFilter === null) ||
725
+ (ctx === 'none' && contextFilter === '') ||
726
+ (ctx !== 'all' && ctx !== 'none' && contextFilter === ctx);
727
+ 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));
728
+ }) }), _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) => {
729
+ const label = ctx === 'clear'
730
+ ? (i18n.tui.context?.none || 'No context')
731
+ : ctx === 'new'
732
+ ? (i18n.tui.context?.addNew || '+ New context')
733
+ : `@${ctx}`;
734
+ const currentContext = currentTasks[selectedTaskIndex]?.context;
735
+ const isActive = (ctx === 'clear' && !currentContext) ||
736
+ (ctx !== 'clear' && ctx !== 'new' && currentContext === ctx);
737
+ 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));
738
+ }) }), _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
739
  { key: 'i', label: i18n.tui.keyBar.comment },
583
740
  { key: 'd', label: i18n.tui.keyBar.delete },
584
741
  { key: 'P', label: i18n.tui.keyBar.project },