floq 0.6.0 → 0.8.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/changelog.js CHANGED
@@ -1,27 +1,44 @@
1
1
  import { readFileSync } from 'fs';
2
2
  import { fileURLToPath } from 'url';
3
3
  import { dirname, join } from 'path';
4
+ import { getLocale } from './config.js';
4
5
  function findChangelogPath() {
5
6
  // Get the directory of the current module
6
7
  const currentFile = fileURLToPath(import.meta.url);
7
8
  const currentDir = dirname(currentFile);
8
- // Try to find CHANGELOG.md relative to the package root
9
+ const locale = getLocale();
10
+ const filename = locale === 'ja' ? 'CHANGELOG.ja.md' : 'CHANGELOG.md';
11
+ const fallbackFilename = 'CHANGELOG.md';
12
+ // Try to find localized CHANGELOG relative to the package root
9
13
  // From dist/ or src/, go up one level
10
- const possiblePaths = [
11
- join(currentDir, '..', 'CHANGELOG.md'),
12
- join(currentDir, '..', '..', 'CHANGELOG.md'),
13
- join(process.cwd(), 'CHANGELOG.md'),
14
+ const basePaths = [
15
+ join(currentDir, '..'),
16
+ join(currentDir, '..', '..'),
17
+ process.cwd(),
14
18
  ];
15
- for (const path of possiblePaths) {
19
+ // First, try to find the localized version
20
+ for (const basePath of basePaths) {
21
+ const localizedPath = join(basePath, filename);
16
22
  try {
17
- readFileSync(path, 'utf-8');
18
- return path;
23
+ readFileSync(localizedPath, 'utf-8');
24
+ return localizedPath;
19
25
  }
20
26
  catch {
21
27
  // continue to next path
22
28
  }
23
29
  }
24
- return possiblePaths[0]; // Return first path as fallback
30
+ // Fall back to default CHANGELOG.md
31
+ for (const basePath of basePaths) {
32
+ const fallbackPath = join(basePath, fallbackFilename);
33
+ try {
34
+ readFileSync(fallbackPath, 'utf-8');
35
+ return fallbackPath;
36
+ }
37
+ catch {
38
+ // continue to next path
39
+ }
40
+ }
41
+ return join(basePaths[0], fallbackFilename); // Return first path as fallback
25
42
  }
26
43
  export function parseChangelog() {
27
44
  const changelogPath = findChangelogPath();
package/dist/cli.js CHANGED
@@ -20,7 +20,14 @@ program
20
20
  // Default command - launch TUI
21
21
  program
22
22
  .action(() => {
23
- render(React.createElement(App));
23
+ // Enter alternate screen buffer (like btop/vim)
24
+ process.stdout.write('\x1b[?1049h');
25
+ process.stdout.write('\x1b[H'); // Move cursor to top-left
26
+ const { waitUntilExit } = render(React.createElement(App));
27
+ waitUntilExit().then(() => {
28
+ // Leave alternate screen buffer
29
+ process.stdout.write('\x1b[?1049l');
30
+ });
24
31
  });
25
32
  // Add task
26
33
  program
package/dist/config.d.ts CHANGED
@@ -14,6 +14,7 @@ export interface Config {
14
14
  turso?: TursoConfig;
15
15
  contexts?: string[];
16
16
  splashDuration?: number;
17
+ contextFilter?: string | null;
17
18
  }
18
19
  export declare function loadConfig(): Config;
19
20
  export declare function saveConfig(updates: Partial<Config>): void;
@@ -33,3 +34,5 @@ export declare function addContext(context: string): boolean;
33
34
  export declare function removeContext(context: string): boolean;
34
35
  export declare function getSplashDuration(): number;
35
36
  export declare function setSplashDuration(duration: number): void;
37
+ export declare function getContextFilter(): string | null;
38
+ export declare function setContextFilter(contextFilter: string | null): void;
package/dist/config.js CHANGED
@@ -151,3 +151,11 @@ export function setSplashDuration(duration) {
151
151
  // Allow -1 (wait for key), 0 (disabled), or positive values
152
152
  saveConfig({ splashDuration: duration >= 0 ? duration : -1 });
153
153
  }
154
+ export function getContextFilter() {
155
+ const config = loadConfig();
156
+ // undefined means not set (default to null = all)
157
+ return config.contextFilter === undefined ? null : config.contextFilter;
158
+ }
159
+ export function setContextFilter(contextFilter) {
160
+ saveConfig({ contextFilter });
161
+ }
package/dist/i18n/en.d.ts CHANGED
@@ -85,6 +85,16 @@ export declare const en: {
85
85
  waitingFor: string;
86
86
  refreshed: string;
87
87
  footer: string;
88
+ dqFooter: {
89
+ tabs: string;
90
+ tasks: string;
91
+ projectDetail: string;
92
+ taskDetail: string;
93
+ };
94
+ kanbanFooter: {
95
+ category: string;
96
+ tasks: string;
97
+ };
88
98
  noTasks: string;
89
99
  tabInbox: string;
90
100
  tabNext: string;
@@ -435,6 +445,16 @@ export type TuiTranslations = {
435
445
  waitingFor: string;
436
446
  refreshed: string;
437
447
  footer: string;
448
+ dqFooter: {
449
+ tabs: string;
450
+ tasks: string;
451
+ projectDetail: string;
452
+ taskDetail: string;
453
+ };
454
+ kanbanFooter: {
455
+ category: string;
456
+ tasks: string;
457
+ };
438
458
  noTasks: string;
439
459
  tabInbox: string;
440
460
  tabNext: string;
package/dist/i18n/en.js CHANGED
@@ -89,7 +89,18 @@ export const en = {
89
89
  movedToWaiting: 'Moved "{title}" to Waiting (for {person})',
90
90
  waitingFor: 'Waiting for: ',
91
91
  refreshed: 'Refreshed',
92
- footer: 'a=add d=done D=delete n=next s=someday w=waiting i=inbox p=project P=link',
92
+ footer: 'a=add d=done D=delete n=next s=someday w=waiting i=inbox p=project P=link c=context',
93
+ // DQ/Mario style footers
94
+ dqFooter: {
95
+ tabs: 'j/k=select l/Enter=tasks 1-6=tab a=add @=filter /=search',
96
+ tasks: 'j/k=select Enter=detail h/Esc=back d=done n=next s=someday w=wait i=inbox c=context p=project P=link D=delete u=undo /=search',
97
+ projectDetail: 'j/k=select a=add d=done Esc/b=back /=search',
98
+ taskDetail: 'j/k=select c/i=add comment P=link D=delete comment Esc/b=back',
99
+ },
100
+ kanbanFooter: {
101
+ category: 'j/k=select l/Enter=tasks a=add @=filter /=search',
102
+ tasks: 'j/k=select h/Esc=back d=done m=move a=add u=undo /=search',
103
+ },
93
104
  noTasks: 'No tasks',
94
105
  // Tab labels
95
106
  tabInbox: 'Inbox',
package/dist/i18n/ja.js CHANGED
@@ -89,7 +89,18 @@ export const ja = {
89
89
  movedToWaiting: '「{title}」を連絡待ち({person})に移動しました',
90
90
  waitingFor: '待機相手: ',
91
91
  refreshed: '更新しました',
92
- footer: 'a=追加 d=完了 D=削除 n=次 s=いつか w=待ち i=Inbox p=プロジェクト化 P=紐づけ',
92
+ footer: 'a=追加 d=完了 D=削除 n=次 s=いつか w=待ち i=Inbox p=プロジェクト化 P=紐づけ c=コンテキスト',
93
+ // DQ/Mario style footers
94
+ dqFooter: {
95
+ tabs: 'j/k=選択 l/Enter=タスク 1-6=タブ a=追加 @=フィルター /=検索',
96
+ tasks: 'j/k=選択 Enter=詳細 h/Esc=戻る d=完了 n=次 s=いつか w=待ち i=inbox c=コンテキスト p=プロジェクト化 P=紐づけ D=削除 u=戻す /=検索',
97
+ projectDetail: 'j/k=選択 a=追加 d=完了 Esc/b=戻る /=検索',
98
+ taskDetail: 'j/k=選択 c/i=コメント追加 P=紐づけ D=コメント削除 Esc/b=戻る',
99
+ },
100
+ kanbanFooter: {
101
+ category: 'j/k=選択 l/Enter=タスク a=追加 @=フィルター /=検索',
102
+ tasks: 'j/k=選択 h/Esc=戻る d=完了 m=移動 a=追加 u=戻す /=検索',
103
+ },
93
104
  noTasks: 'タスクなし',
94
105
  // Tab labels
95
106
  tabInbox: 'Inbox',
package/dist/index.js CHANGED
File without changes
package/dist/ui/App.js CHANGED
@@ -17,10 +17,12 @@ import { LanguageSelector } from './LanguageSelector.js';
17
17
  import { getDb, schema } from '../db/index.js';
18
18
  import { t, fmt } from '../i18n/index.js';
19
19
  import { ThemeProvider, useTheme, getTheme } from './theme/index.js';
20
- import { getThemeName, getViewMode, setThemeName, setViewMode, setLocale, isTursoEnabled, getContexts, addContext, getSplashDuration } from '../config.js';
20
+ import { getThemeName, getViewMode, setThemeName, setViewMode, setLocale, isTursoEnabled, getContexts, addContext, getSplashDuration, getContextFilter, setContextFilter as saveContextFilter } from '../config.js';
21
21
  import { KanbanBoard } from './components/KanbanBoard.js';
22
22
  import { KanbanDQ } from './components/KanbanDQ.js';
23
+ import { KanbanMario } from './components/KanbanMario.js';
23
24
  import { GtdDQ } from './components/GtdDQ.js';
25
+ import { GtdMario } from './components/GtdMario.js';
24
26
  import { VERSION } from '../version.js';
25
27
  import { HistoryProvider, useHistory, CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, } from './history/index.js';
26
28
  const TABS = ['inbox', 'next', 'waiting', 'someday', 'projects', 'done'];
@@ -51,6 +53,7 @@ export function App() {
51
53
  };
52
54
  const currentTheme = getTheme(themeName);
53
55
  const useDQStyle = currentTheme.uiStyle === 'titled-box';
56
+ const useMarioStyle = currentTheme.uiStyle === 'mario-block';
54
57
  // Show splash screen (all themes, configurable duration)
55
58
  if (showSplash) {
56
59
  return (_jsx(ThemeProvider, { themeName: themeName, children: _jsx(SplashScreen, { onComplete: () => setShowSplash(false), duration: splashDuration, viewMode: viewMode }) }));
@@ -65,7 +68,7 @@ export function App() {
65
68
  if (settingsMode === 'lang-select') {
66
69
  return (_jsx(ThemeProvider, { themeName: themeName, children: _jsx(LanguageSelector, { onSelect: handleLocaleSelect, onCancel: handleSettingsCancel }) }));
67
70
  }
68
- return (_jsx(ThemeProvider, { themeName: themeName, children: _jsx(HistoryProvider, { children: viewMode === 'kanban' ? (useDQStyle ? (_jsx(KanbanDQ, { onOpenSettings: setSettingsMode })) : (_jsx(KanbanBoard, { onOpenSettings: setSettingsMode }))) : (useDQStyle ? (_jsx(GtdDQ, { onOpenSettings: setSettingsMode })) : (_jsx(AppContent, { onOpenSettings: setSettingsMode }))) }) }));
71
+ return (_jsx(ThemeProvider, { themeName: themeName, children: _jsx(HistoryProvider, { children: viewMode === 'kanban' ? (useMarioStyle ? (_jsx(KanbanMario, { onOpenSettings: setSettingsMode })) : useDQStyle ? (_jsx(KanbanDQ, { onOpenSettings: setSettingsMode })) : (_jsx(KanbanBoard, { onOpenSettings: setSettingsMode }))) : (useMarioStyle ? (_jsx(GtdMario, { onOpenSettings: setSettingsMode })) : useDQStyle ? (_jsx(GtdDQ, { onOpenSettings: setSettingsMode })) : (_jsx(AppContent, { onOpenSettings: setSettingsMode }))) }) }));
69
72
  }
70
73
  function AppContent({ onOpenSettings }) {
71
74
  const theme = useTheme();
@@ -98,8 +101,12 @@ function AppContent({ onOpenSettings }) {
98
101
  const [searchQuery, setSearchQuery] = useState('');
99
102
  const [searchResults, setSearchResults] = useState([]);
100
103
  const [searchResultIndex, setSearchResultIndex] = useState(0);
101
- // Context filter state
102
- const [contextFilter, setContextFilter] = useState(null); // null = all, '' = no context, string = specific context
104
+ // Context filter state - load from config for persistence across sessions/terminals
105
+ const [contextFilter, setContextFilterState] = useState(() => getContextFilter());
106
+ const setContextFilter = useCallback((value) => {
107
+ setContextFilterState(value);
108
+ saveContextFilter(value);
109
+ }, []);
103
110
  const [contextSelectIndex, setContextSelectIndex] = useState(0);
104
111
  const [availableContexts, setAvailableContexts] = useState([]);
105
112
  const i18n = t();
@@ -52,6 +52,40 @@ const DQ_BORDER = {
52
52
  vertical: '│',
53
53
  };
54
54
  const TAGLINE = 'Flow your tasks, clear your mind';
55
+ // Mario/Nintendo style splash - SFC boot screen inspired
56
+ const MARIO_LOGO = [
57
+ ' ★ ★ ★ ★ ★ ★ ★ ★ ★',
58
+ '',
59
+ ' ████████ ██ ██████ ██████',
60
+ ' ██ ██ ██ ██ ██ ██',
61
+ ' ██████ ██ ██ ██ ██ ██',
62
+ ' ██ ██ ██ ██ ██ ▄▄ ██',
63
+ ' ██ ███████ ██████ ██████',
64
+ '',
65
+ ' ★ ★ ★ ★ ★ ★ ★ ★ ★',
66
+ ];
67
+ const MARIO_QUOTES_JA = [
68
+ 'イッツァミー!タスクマネージャー!',
69
+ 'マンマミーア!タスクがいっぱいだ!',
70
+ 'レッツァゴー!',
71
+ 'ヤッフー!',
72
+ 'スーパータスククリア!',
73
+ 'コインをゲット!',
74
+ '1UPキノコ!',
75
+ 'ファイアフラワー!',
76
+ 'スターパワー!',
77
+ ];
78
+ const MARIO_QUOTES_EN = [
79
+ "It's-a me! Task Manager!",
80
+ 'Mamma mia! So many tasks!',
81
+ "Let's-a go!",
82
+ 'Yahoo!',
83
+ 'Super Task Clear!',
84
+ 'Got a coin!',
85
+ '1-UP Mushroom!',
86
+ 'Fire Flower!',
87
+ 'Star Power!',
88
+ ];
55
89
  // Dragon Quest famous quotes for splash screen
56
90
  const DQ_QUOTES_JA = [
57
91
  'へんじがない。ただのしかばねのようだ。',
@@ -84,12 +118,17 @@ export function SplashScreen({ onComplete, duration = 1500, viewMode = 'gtd' })
84
118
  const theme = useTheme();
85
119
  const i18n = t();
86
120
  const isDqStyle = theme.uiStyle === 'titled-box';
87
- const isDosStyle = theme.name !== 'modern';
121
+ const isMarioStyle = theme.uiStyle === 'mario-block';
122
+ const isDosStyle = theme.name !== 'modern' && !isMarioStyle;
88
123
  const logo = isDosStyle ? LOGO_DOS : LOGO_MODERN;
89
124
  const [filled, empty] = theme.style.loadingChars;
90
125
  // Pick a random quote (stable across re-renders)
91
126
  const [randomQuote] = useState(() => {
92
127
  const isJapanese = i18n.splash?.welcome === 'ようこそ!';
128
+ if (isMarioStyle) {
129
+ const quotes = isJapanese ? MARIO_QUOTES_JA : MARIO_QUOTES_EN;
130
+ return quotes[Math.floor(Math.random() * quotes.length)];
131
+ }
93
132
  const quotes = isJapanese ? DQ_QUOTES_JA : DQ_QUOTES_EN;
94
133
  return quotes[Math.floor(Math.random() * quotes.length)];
95
134
  });
@@ -118,9 +157,9 @@ export function SplashScreen({ onComplete, duration = 1500, viewMode = 'gtd' })
118
157
  const taglineTimer = setTimeout(() => {
119
158
  setShowTagline(true);
120
159
  }, 600);
121
- // Blink effect for DQ style or wait-for-key mode
160
+ // Blink effect for DQ style, Mario style, or wait-for-key mode
122
161
  let blinkInterval = null;
123
- if (isDqStyle || waitForKeyPress) {
162
+ if (isDqStyle || isMarioStyle || waitForKeyPress) {
124
163
  blinkInterval = setInterval(() => {
125
164
  setBlinkVisible((prev) => !prev);
126
165
  }, 500);
@@ -140,7 +179,11 @@ export function SplashScreen({ onComplete, duration = 1500, viewMode = 'gtd' })
140
179
  if (blinkInterval)
141
180
  clearInterval(blinkInterval);
142
181
  };
143
- }, [onComplete, duration, isDqStyle, waitForKeyPress]);
182
+ }, [onComplete, duration, isDqStyle, isMarioStyle, waitForKeyPress]);
183
+ // Mario / Nintendo style splash (SFC boot screen inspired)
184
+ if (isMarioStyle) {
185
+ return (_jsxs(Box, { flexDirection: "column", alignItems: "center", justifyContent: "center", padding: 2, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: theme.colors.secondary, children: "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501" }) }), _jsx(Box, { flexDirection: "column", alignItems: "center", marginBottom: 1, children: MARIO_LOGO.map((line, index) => (_jsx(Text, { color: index === 0 || index === 8 ? theme.colors.secondary : theme.colors.primary, bold: true, children: line }, index))) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: theme.colors.text, bold: true, children: "\uFF5E SUPER TASK MANAGER \uFF5E" }) }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: theme.colors.accent, children: ["\uD83C\uDF44 ", randomQuote] }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { color: theme.colors.secondary, children: "\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501" }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.textMuted, children: blinkVisible ? '- PRESS START -' : ' ' }) }), _jsxs(Box, { marginTop: 2, flexDirection: "column", alignItems: "center", children: [_jsxs(Text, { color: theme.colors.textMuted, children: ["VER ", VERSION] }), _jsx(Text, { color: theme.colors.textMuted, children: "\u00A9 2026 polidog/PartyHard Inc." })] })] }));
186
+ }
144
187
  // Dragon Quest style splash
145
188
  if (isDqStyle) {
146
189
  const boxWidth = 40;
@@ -7,13 +7,41 @@ import { v4 as uuidv4 } from 'uuid';
7
7
  import { getDb, schema } from '../../db/index.js';
8
8
  import { t, fmt } from '../../i18n/index.js';
9
9
  import { useTheme } from '../theme/index.js';
10
- import { isTursoEnabled, getContexts, addContext } from '../../config.js';
11
- import { VERSION } from '../../version.js';
12
- import { useHistory, CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, } from '../history/index.js';
10
+ import { isTursoEnabled, getContexts, addContext, getLocale, getContextFilter, setContextFilter as saveContextFilter } from '../../config.js';
11
+ import { useHistory, CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, } from '../history/index.js';
13
12
  import { SearchBar } from './SearchBar.js';
14
13
  import { SearchResults } from './SearchResults.js';
15
14
  import { HelpModal } from './HelpModal.js';
16
15
  const TABS = ['inbox', 'next', 'waiting', 'someday', 'projects', 'done'];
16
+ // Dragon Quest job classes
17
+ const DQ_JOBS_JA = [
18
+ 'ゆうしゃ',
19
+ 'せんし',
20
+ 'まほうつかい',
21
+ 'そうりょ',
22
+ 'ぶとうか',
23
+ 'とうぞく',
24
+ 'あそびにん',
25
+ 'けんじゃ',
26
+ 'バトルマスター',
27
+ 'パラディン',
28
+ 'レンジャー',
29
+ 'スーパースター',
30
+ ];
31
+ const DQ_JOBS_EN = [
32
+ 'Hero',
33
+ 'Warrior',
34
+ 'Mage',
35
+ 'Priest',
36
+ 'Martial Artist',
37
+ 'Thief',
38
+ 'Gadabout',
39
+ 'Sage',
40
+ 'Battle Master',
41
+ 'Paladin',
42
+ 'Ranger',
43
+ 'Superstar',
44
+ ];
17
45
  // Round border characters
18
46
  const BORDER = {
19
47
  topLeft: '╭',
@@ -81,6 +109,12 @@ export function GtdDQ({ onOpenSettings }) {
81
109
  const { stdout } = useStdout();
82
110
  const history = useHistory();
83
111
  const i18n = t();
112
+ // Random job class (stable across re-renders)
113
+ const [jobClass] = useState(() => {
114
+ const isJapanese = getLocale() === 'ja';
115
+ const jobs = isJapanese ? DQ_JOBS_JA : DQ_JOBS_EN;
116
+ return jobs[Math.floor(Math.random() * jobs.length)];
117
+ });
84
118
  const [mode, setMode] = useState('normal');
85
119
  const [paneFocus, setPaneFocus] = useState('tabs');
86
120
  const [currentTabIndex, setCurrentTabIndex] = useState(0);
@@ -105,7 +139,12 @@ export function GtdDQ({ onOpenSettings }) {
105
139
  const [taskToWaiting, setTaskToWaiting] = useState(null);
106
140
  const [taskToDelete, setTaskToDelete] = useState(null);
107
141
  const [projectProgress, setProjectProgress] = useState({});
108
- const [contextFilter, setContextFilter] = useState(null);
142
+ // Context filter state - load from config for persistence across sessions/terminals
143
+ const [contextFilter, setContextFilterState] = useState(() => getContextFilter());
144
+ const setContextFilter = useCallback((value) => {
145
+ setContextFilterState(value);
146
+ saveContextFilter(value);
147
+ }, []);
109
148
  const [contextSelectIndex, setContextSelectIndex] = useState(0);
110
149
  const [availableContexts, setAvailableContexts] = useState([]);
111
150
  const [searchQuery, setSearchQuery] = useState('');
@@ -325,6 +364,46 @@ export function GtdDQ({ onOpenSettings }) {
325
364
  setMessage(fmt(i18n.tui.linkedToProject || 'Linked "{title}" to {project}', { title: task.title, project: project.title }));
326
365
  await loadTasks();
327
366
  }, [i18n.tui.linkedToProject, loadTasks, history]);
367
+ const addCommentToTask = useCallback(async (task, content) => {
368
+ const commentId = uuidv4();
369
+ const command = new CreateCommentCommand({
370
+ comment: {
371
+ id: commentId,
372
+ taskId: task.id,
373
+ content: content.trim(),
374
+ createdAt: new Date(),
375
+ },
376
+ description: i18n.tui.commentAdded || 'Comment added',
377
+ });
378
+ await history.execute(command);
379
+ setMessage(i18n.tui.commentAdded || 'Comment added');
380
+ await loadTaskComments(task.id);
381
+ }, [history, i18n.tui.commentAdded, loadTaskComments]);
382
+ const deleteComment = useCallback(async (comment) => {
383
+ const command = new DeleteCommentCommand({
384
+ comment,
385
+ description: i18n.tui.commentDeleted || 'Comment deleted',
386
+ });
387
+ await history.execute(command);
388
+ setMessage(i18n.tui.commentDeleted || 'Comment deleted');
389
+ if (selectedTask) {
390
+ await loadTaskComments(selectedTask.id);
391
+ }
392
+ }, [i18n.tui.commentDeleted, loadTaskComments, selectedTask, 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]);
328
407
  const handleInputSubmit = async (value) => {
329
408
  // Handle search mode submit
330
409
  if (mode === 'search') {
@@ -340,6 +419,15 @@ export function GtdDQ({ onOpenSettings }) {
340
419
  setSearchResultIndex(0);
341
420
  return;
342
421
  }
422
+ // Handle add-comment mode
423
+ if (mode === 'add-comment' && selectedTask) {
424
+ if (value.trim()) {
425
+ await addCommentToTask(selectedTask, value);
426
+ }
427
+ setInputValue('');
428
+ setMode('task-detail');
429
+ return;
430
+ }
343
431
  if (mode === 'move-to-waiting' && taskToWaiting) {
344
432
  if (value.trim()) {
345
433
  await moveTaskToWaiting(taskToWaiting, value);
@@ -394,12 +482,57 @@ export function GtdDQ({ onOpenSettings }) {
394
482
  else if (mode === 'add-context') {
395
483
  setMode('set-context');
396
484
  }
485
+ else if (mode === 'add-comment') {
486
+ setMode('task-detail');
487
+ }
397
488
  else {
398
489
  setMode('normal');
399
490
  }
400
491
  }
401
492
  return;
402
493
  }
494
+ // Handle task-detail mode
495
+ if (mode === 'task-detail') {
496
+ if (key.escape || input === 'b' || input === 'h' || key.leftArrow) {
497
+ setSelectedTask(null);
498
+ setTaskComments([]);
499
+ setSelectedCommentIndex(0);
500
+ setMode('normal');
501
+ return;
502
+ }
503
+ // Navigate comments
504
+ if (key.upArrow || input === 'k') {
505
+ setSelectedCommentIndex((prev) => (prev > 0 ? prev - 1 : Math.max(0, taskComments.length - 1)));
506
+ return;
507
+ }
508
+ if (key.downArrow || input === 'j') {
509
+ setSelectedCommentIndex((prev) => (prev < taskComments.length - 1 ? prev + 1 : 0));
510
+ return;
511
+ }
512
+ // Add comment
513
+ if (input === 'c' || input === 'i') {
514
+ setMode('add-comment');
515
+ return;
516
+ }
517
+ // Delete comment
518
+ if (input === 'D' && taskComments.length > 0) {
519
+ const comment = taskComments[selectedCommentIndex];
520
+ deleteComment(comment).then(() => {
521
+ if (selectedCommentIndex >= taskComments.length - 1) {
522
+ setSelectedCommentIndex(Math.max(0, selectedCommentIndex - 1));
523
+ }
524
+ });
525
+ return;
526
+ }
527
+ // Link to project (P key)
528
+ if (input === 'P' && tasks.projects.length > 0) {
529
+ setTaskToLink(selectedTask);
530
+ setProjectSelectIndex(0);
531
+ setMode('select-project');
532
+ return;
533
+ }
534
+ return;
535
+ }
403
536
  // Handle search mode
404
537
  if (mode === 'search') {
405
538
  if (key.escape) {
@@ -493,7 +626,13 @@ export function GtdDQ({ onOpenSettings }) {
493
626
  setInputValue('');
494
627
  return;
495
628
  }
496
- // TODO: implement setTaskContext
629
+ const task = currentTasks[selectedTaskIndex];
630
+ if (selected === 'clear') {
631
+ setTaskContext(task, null);
632
+ }
633
+ else {
634
+ setTaskContext(task, selected);
635
+ }
497
636
  setContextSelectIndex(0);
498
637
  setMode('normal');
499
638
  return;
@@ -503,9 +642,10 @@ export function GtdDQ({ onOpenSettings }) {
503
642
  // Handle select-project mode
504
643
  if (mode === 'select-project') {
505
644
  if (key.escape) {
645
+ const wasFromTaskDetail = selectedTask !== null;
506
646
  setTaskToLink(null);
507
647
  setProjectSelectIndex(0);
508
- setMode('normal');
648
+ setMode(wasFromTaskDetail ? 'task-detail' : 'normal');
509
649
  return;
510
650
  }
511
651
  if (key.upArrow || input === 'k') {
@@ -518,10 +658,11 @@ export function GtdDQ({ onOpenSettings }) {
518
658
  }
519
659
  if (key.return && taskToLink && tasks.projects.length > 0) {
520
660
  const project = tasks.projects[projectSelectIndex];
661
+ const wasFromTaskDetail = selectedTask !== null;
521
662
  linkTaskToProject(taskToLink, project);
522
663
  setTaskToLink(null);
523
664
  setProjectSelectIndex(0);
524
- setMode('normal');
665
+ setMode(wasFromTaskDetail ? 'task-detail' : 'normal');
525
666
  return;
526
667
  }
527
668
  return;
@@ -641,6 +782,14 @@ export function GtdDQ({ onOpenSettings }) {
641
782
  setSelectedTaskIndex((prev) => (prev < currentTasks.length - 1 ? prev + 1 : 0));
642
783
  return;
643
784
  }
785
+ // Enter to view task details (on non-projects tabs)
786
+ if (key.return && currentTab !== 'projects' && currentTasks.length > 0) {
787
+ const task = currentTasks[selectedTaskIndex];
788
+ setSelectedTask(task);
789
+ loadTaskComments(task.id);
790
+ setMode('task-detail');
791
+ return;
792
+ }
644
793
  // Task actions
645
794
  if (currentTasks.length > 0) {
646
795
  const task = currentTasks[selectedTaskIndex];
@@ -708,6 +857,12 @@ export function GtdDQ({ onOpenSettings }) {
708
857
  setMode('confirm-delete');
709
858
  return;
710
859
  }
860
+ // Set context
861
+ if (input === 'c' && currentTab !== 'projects') {
862
+ setContextSelectIndex(0);
863
+ setMode('set-context');
864
+ return;
865
+ }
711
866
  }
712
867
  // Undo
713
868
  if (input === 'u') {
@@ -741,10 +896,16 @@ export function GtdDQ({ onOpenSettings }) {
741
896
  if (mode === 'help') {
742
897
  return (_jsx(Box, { flexDirection: "column", padding: 1, children: _jsx(HelpModal, { onClose: () => setMode('normal') }) }));
743
898
  }
744
- 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: "FLOQ" }), _jsxs(Text, { color: theme.colors.textMuted, children: [" v", VERSION] }), _jsx(Text, { color: tursoEnabled ? theme.colors.accent : theme.colors.textMuted, children: tursoEnabled ? ' [TURSO]' : ' [LOCAL]' }), contextFilter !== null && (_jsxs(Text, { color: theme.colors.accent, children: [' ', "@", contextFilter === '' ? 'none' : contextFilter] }))] }), _jsx(Text, { color: theme.colors.textMuted, children: "?=help q=quit" })] }), mode === 'context-filter' ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: "Filter by context" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: ['all', 'none', ...availableContexts].map((ctx, index) => {
899
+ 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.accent, children: jobClass }), _jsx(Text, { color: theme.colors.text, children: " Lv." }), _jsx(Text, { bold: true, color: theme.colors.secondary, children: Math.floor(tasks.done.length / 5) + 1 }), _jsx(Text, { color: theme.colors.text, children: " HP " }), _jsx(Text, { bold: true, color: theme.colors.statusNext, children: tasks.inbox.length + tasks.next.length }), _jsxs(Text, { color: theme.colors.textMuted, children: ["/", tasks.inbox.length + tasks.next.length + tasks.waiting.length + tasks.someday.length] }), _jsx(Text, { color: theme.colors.text, children: " MP " }), _jsx(Text, { bold: true, color: theme.colors.statusWaiting, children: tasks.projects.length }), _jsx(Text, { color: tursoEnabled ? theme.colors.accent : theme.colors.textMuted, children: tursoEnabled ? ' [TURSO]' : '' }), contextFilter !== null && (_jsxs(Text, { color: theme.colors.accent, children: [' ', "@", contextFilter === '' ? 'none' : contextFilter] }))] }), _jsx(Text, { color: theme.colors.textMuted, children: "?=help q=quit" })] }), mode === 'context-filter' ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: "Filter by context" }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: ['all', 'none', ...availableContexts].map((ctx, index) => {
745
900
  const label = ctx === 'all' ? 'All' : ctx === 'none' ? 'No context' : `@${ctx}`;
746
901
  return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? '▶ ' : ' ', label] }, ctx));
747
- }) })] })) : mode === 'select-project' && taskToLink ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project', ": ", taskToLink.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: tasks.projects.map((project, index) => (_jsxs(Text, { color: index === projectSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === projectSelectIndex, children: [index === projectSelectIndex ? '' : ' ', project.title] }, project.id))) })] })) : mode === 'confirm-delete' && taskToDelete ? (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: theme.colors.accent, bold: true, children: fmt(i18n.tui.deleteConfirm || 'Delete "{title}"? (y/n)', { title: taskToDelete.title }) }) })) : (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { marginRight: 2, children: _jsx(TitledBoxInline, { title: mode === 'project-detail' && selectedProject ? selectedProject.title : 'GTD', width: leftPaneWidth, minHeight: 8, isActive: paneFocus === 'tabs' && mode !== 'project-detail', children: mode === 'project-detail' ? (_jsx(Text, { color: theme.colors.textMuted, children: "\u2190 Esc/b: back" })) : (TABS.map((tab, index) => {
902
+ }) })] })) : mode === 'set-context' && currentTasks.length > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.setContext || 'Set context for', ": ", currentTasks[selectedTaskIndex]?.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: ['clear', ...availableContexts, 'new'].map((ctx, index) => {
903
+ const label = ctx === 'clear' ? (i18n.tui.context?.none || 'Clear context') : ctx === 'new' ? (i18n.tui.context?.addNew || 'New context...') : `@${ctx}`;
904
+ return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? '▶ ' : ' ', label] }, ctx));
905
+ }) })] })) : mode === 'select-project' && taskToLink ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project', ": ", taskToLink.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, children: tasks.projects.map((project, index) => (_jsxs(Text, { color: index === projectSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === projectSelectIndex, children: [index === projectSelectIndex ? '▶ ' : ' ', project.title] }, project.id))) })] })) : mode === 'confirm-delete' && taskToDelete ? (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { color: theme.colors.accent, bold: true, children: fmt(i18n.tui.deleteConfirm || 'Delete "{title}"? (y/n)', { title: taskToDelete.title }) }) })) : (mode === 'task-detail' || mode === 'add-comment') && selectedTask ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(TitledBoxInline, { title: i18n.tui.taskDetailTitle || 'Task Details', width: terminalWidth - 4, minHeight: 4, isActive: true, children: [_jsx(Text, { color: theme.colors.text, bold: true, children: selectedTask.title }), selectedTask.description && (_jsx(Text, { color: theme.colors.textMuted, children: selectedTask.description })), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailStatus || 'Status', ": "] }), _jsxs(Text, { color: theme.colors.accent, children: [i18n.status[selectedTask.status], selectedTask.waitingFor && ` (${selectedTask.waitingFor})`] })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.label || 'Context', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.context ? `@${selectedTask.context}` : (i18n.tui.context?.none || 'No context') })] })] }), _jsx(Box, { marginTop: 1, children: _jsx(TitledBoxInline, { title: `${i18n.tui.comments || 'Comments'} (${taskComments.length})`, width: terminalWidth - 4, minHeight: 5, isActive: mode === 'task-detail', children: taskComments.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noComments || 'No comments yet' })) : (taskComments.map((comment, index) => {
906
+ const isSelected = index === selectedCommentIndex && mode === 'task-detail';
907
+ return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: theme.colors.textMuted, children: [isSelected ? '▶ ' : ' ', "[", comment.createdAt.toLocaleString(), "]"] }), _jsxs(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [' ', comment.content] })] }, comment.id));
908
+ })) }) })] })) : (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { marginRight: 2, children: _jsx(TitledBoxInline, { title: mode === 'project-detail' && selectedProject ? selectedProject.title : 'GTD', width: leftPaneWidth, minHeight: 8, isActive: paneFocus === 'tabs' && mode !== 'project-detail', children: mode === 'project-detail' ? (_jsx(Text, { color: theme.colors.textMuted, children: "\u2190 Esc/b: back" })) : (TABS.map((tab, index) => {
748
909
  const isSelected = index === currentTabIndex;
749
910
  const count = tasks[tab].length;
750
911
  return (_jsxs(Text, { color: isSelected && paneFocus === 'tabs' ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [isSelected ? '▶ ' : ' ', index + 1, ":", getTabLabel(tab), " (", count, ")"] }, tab));
@@ -765,9 +926,11 @@ export function GtdDQ({ onOpenSettings }) {
765
926
  return (_jsxs(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [prefix, displayTitle, task.waitingFor && _jsxs(Text, { color: theme.colors.muted, children: [" (", task.waitingFor, ")"] }), task.context && _jsxs(Text, { color: theme.colors.muted, children: [" @", task.context] }), parentProject && _jsxs(Text, { color: theme.colors.muted, children: [" [", parentProject.title, "]"] }), progress && _jsxs(Text, { color: theme.colors.muted, children: [" [", progress.completed, "/", progress.total, "]"] })] }, task.id));
766
927
  })) }) })] })), (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
767
928
  ? `${i18n.tui.newTask}[${selectedProject.title}] `
768
- : i18n.tui.newTask }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: i18n.tui.placeholder })] })), 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: "" })] })), mode === 'add-context' && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: "New context: " }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: "Enter context name..." })] })), mode === 'search' && (_jsx(SearchBar, { value: searchQuery, onChange: handleSearchChange, onSubmit: handleInputSubmit })), mode === 'search' && searchQuery && (_jsx(SearchResults, { results: searchResults, selectedIndex: searchResultIndex, query: searchQuery })), message && mode === 'normal' && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.textHighlight, children: message }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.textMuted, children: mode === 'project-detail'
769
- ? 'j/k=select a=add d=done Esc/b=back /=search'
770
- : paneFocus === 'tabs'
771
- ? 'j/k=select l/Enter=tasks 1-6=tab a=add @=filter /=search'
772
- : 'j/k=select h/Esc=back d=done n=next s=someday w=wait i=inbox p=project P=link D=delete u=undo /=search' }) })] }));
929
+ : i18n.tui.newTask }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: i18n.tui.placeholder })] })), 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: "" })] })), mode === 'add-context' && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: "New context: " }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: "Enter context name..." })] })), mode === 'add-comment' && selectedTask && (_jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.addComment || 'Add comment', ": "] }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: "Enter comment..." })] })), mode === 'search' && (_jsx(SearchBar, { value: searchQuery, onChange: handleSearchChange, onSubmit: handleInputSubmit })), mode === 'search' && searchQuery && (_jsx(SearchResults, { results: searchResults, selectedIndex: searchResultIndex, query: searchQuery })), message && mode === 'normal' && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.textHighlight, children: message }) })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.textMuted, children: mode === 'task-detail'
930
+ ? i18n.tui.dqFooter.taskDetail
931
+ : mode === 'project-detail'
932
+ ? i18n.tui.dqFooter.projectDetail
933
+ : paneFocus === 'tabs'
934
+ ? i18n.tui.dqFooter.tabs
935
+ : i18n.tui.dqFooter.tasks }) })] }));
773
936
  }
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ type SettingsMode = 'none' | 'theme-select' | 'mode-select' | 'lang-select';
3
+ interface GtdMarioProps {
4
+ onOpenSettings?: (mode: SettingsMode) => void;
5
+ }
6
+ export declare function GtdMario({ onOpenSettings }: GtdMarioProps): React.ReactElement;
7
+ export {};