@brainfile/cli 0.9.2 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +30 -4
  3. package/dist/commands/tui.d.ts +1 -1
  4. package/dist/commands/tui.d.ts.map +1 -1
  5. package/dist/commands/tui.js +8 -2
  6. package/dist/commands/tui.js.map +1 -1
  7. package/dist/tui/BrainfileTUI.d.ts.map +1 -1
  8. package/dist/tui/BrainfileTUI.js +47 -14
  9. package/dist/tui/BrainfileTUI.js.map +1 -1
  10. package/dist/tui/actions.d.ts +32 -2
  11. package/dist/tui/actions.d.ts.map +1 -1
  12. package/dist/tui/actions.js +274 -2
  13. package/dist/tui/actions.js.map +1 -1
  14. package/dist/tui/components/ArchivePanel.d.ts +16 -0
  15. package/dist/tui/components/ArchivePanel.d.ts.map +1 -0
  16. package/dist/tui/components/ArchivePanel.js +172 -0
  17. package/dist/tui/components/ArchivePanel.js.map +1 -0
  18. package/dist/tui/components/HelpOverlay.d.ts.map +1 -1
  19. package/dist/tui/components/HelpOverlay.js +43 -27
  20. package/dist/tui/components/HelpOverlay.js.map +1 -1
  21. package/dist/tui/components/MainPanelTabs.d.ts +9 -0
  22. package/dist/tui/components/MainPanelTabs.d.ts.map +1 -0
  23. package/dist/tui/components/MainPanelTabs.js +37 -0
  24. package/dist/tui/components/MainPanelTabs.js.map +1 -0
  25. package/dist/tui/components/RulesPanel.d.ts +24 -0
  26. package/dist/tui/components/RulesPanel.d.ts.map +1 -0
  27. package/dist/tui/components/RulesPanel.js +125 -0
  28. package/dist/tui/components/RulesPanel.js.map +1 -0
  29. package/dist/tui/components/index.d.ts +6 -0
  30. package/dist/tui/components/index.d.ts.map +1 -1
  31. package/dist/tui/components/index.js +7 -1
  32. package/dist/tui/components/index.js.map +1 -1
  33. package/dist/tui/hooks/useKeyboardNavigation.d.ts.map +1 -1
  34. package/dist/tui/hooks/useKeyboardNavigation.js +516 -190
  35. package/dist/tui/hooks/useKeyboardNavigation.js.map +1 -1
  36. package/dist/tui/index.d.ts +1 -1
  37. package/dist/tui/index.d.ts.map +1 -1
  38. package/dist/tui/theme.d.ts +6 -0
  39. package/dist/tui/theme.d.ts.map +1 -1
  40. package/dist/tui/theme.js +6 -0
  41. package/dist/tui/theme.js.map +1 -1
  42. package/dist/tui/types.d.ts +16 -2
  43. package/dist/tui/types.d.ts.map +1 -1
  44. package/dist/tui/types.js.map +1 -1
  45. package/package.json +1 -1
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.useKeyboardNavigation = useKeyboardNavigation;
4
4
  const ink_1 = require("ink");
5
5
  const actions_js_1 = require("../actions.js");
6
+ const RULE_TYPES = ['always', 'never', 'prefer', 'context'];
6
7
  // Helper to show status message
7
8
  function showStatus(setState, text, type) {
8
9
  const timestamp = Date.now();
@@ -173,7 +174,7 @@ function useKeyboardNavigation({ state, setState, currentTasks, maxTaskIndex, fi
173
174
  }
174
175
  return;
175
176
  }
176
- // Search mode handling
177
+ // Search mode handling (Tasks panel)
177
178
  if (state.mode === 'search') {
178
179
  if (key.escape) {
179
180
  setState(prev => ({ ...prev, mode: 'browse', searchQuery: '' }));
@@ -193,7 +194,162 @@ function useKeyboardNavigation({ state, setState, currentTasks, maxTaskIndex, fi
193
194
  }
194
195
  return;
195
196
  }
196
- // Browse mode
197
+ // Rule add/edit mode
198
+ if (state.mode === 'rule-add' || state.mode === 'rule-edit') {
199
+ if (key.escape) {
200
+ setState(prev => ({ ...prev, mode: 'browse', ruleEditText: '', ruleEditId: null }));
201
+ return;
202
+ }
203
+ if (key.return) {
204
+ const text = state.ruleEditText.trim();
205
+ if (!text) {
206
+ showStatus(setState, 'Rule text required', 'error');
207
+ return;
208
+ }
209
+ if (state.mode === 'rule-add') {
210
+ const result = (0, actions_js_1.addRuleAction)(filePath, state.activeRuleType, text);
211
+ if (result.success) {
212
+ showStatus(setState, result.message || 'Rule added', 'success');
213
+ loadBrainfile(true);
214
+ }
215
+ else {
216
+ showStatus(setState, result.error || 'Failed to add rule', 'error');
217
+ }
218
+ }
219
+ else if (state.ruleEditId !== null) {
220
+ const result = (0, actions_js_1.updateRuleAction)(filePath, state.activeRuleType, state.ruleEditId, text);
221
+ if (result.success) {
222
+ showStatus(setState, result.message || 'Rule updated', 'success');
223
+ loadBrainfile(true);
224
+ }
225
+ else {
226
+ showStatus(setState, result.error || 'Failed to update rule', 'error');
227
+ }
228
+ }
229
+ setState(prev => ({ ...prev, mode: 'browse', ruleEditText: '', ruleEditId: null }));
230
+ return;
231
+ }
232
+ if (key.backspace || key.delete) {
233
+ setState(prev => ({ ...prev, ruleEditText: prev.ruleEditText.slice(0, -1) }));
234
+ return;
235
+ }
236
+ if (input && !key.ctrl && !key.meta) {
237
+ setState(prev => ({ ...prev, ruleEditText: prev.ruleEditText + input }));
238
+ return;
239
+ }
240
+ return;
241
+ }
242
+ // Rule delete confirmation
243
+ if (state.mode === 'rule-delete-confirm') {
244
+ const rules = state.board?.rules?.[state.activeRuleType] || [];
245
+ const rule = rules[state.selectedRuleIndex];
246
+ if (input === 'y' || input === 'Y') {
247
+ if (rule) {
248
+ const result = (0, actions_js_1.deleteRuleAction)(filePath, state.activeRuleType, rule.id);
249
+ if (result.success) {
250
+ showStatus(setState, result.message || 'Rule deleted', 'success');
251
+ loadBrainfile(true);
252
+ // Adjust selection if needed
253
+ setState(prev => ({
254
+ ...prev,
255
+ mode: 'browse',
256
+ selectedRuleIndex: Math.max(0, prev.selectedRuleIndex - 1),
257
+ }));
258
+ }
259
+ else {
260
+ showStatus(setState, result.error || 'Failed to delete rule', 'error');
261
+ setState(prev => ({ ...prev, mode: 'browse' }));
262
+ }
263
+ }
264
+ return;
265
+ }
266
+ if (input === 'n' || input === 'N' || key.escape) {
267
+ setState(prev => ({ ...prev, mode: 'browse' }));
268
+ showStatus(setState, 'Delete cancelled', 'info');
269
+ return;
270
+ }
271
+ return;
272
+ }
273
+ // Archive restore mode - column picker
274
+ if (state.mode === 'archive-restore') {
275
+ if (key.escape) {
276
+ setState(prev => ({ ...prev, mode: 'browse' }));
277
+ return;
278
+ }
279
+ if (key.downArrow || input === 'j') {
280
+ setState(prev => ({
281
+ ...prev,
282
+ archiveRestoreColumnIndex: Math.min(prev.archiveRestoreColumnIndex + 1, allColumns.length - 1),
283
+ }));
284
+ return;
285
+ }
286
+ if (key.upArrow || input === 'k') {
287
+ setState(prev => ({
288
+ ...prev,
289
+ archiveRestoreColumnIndex: Math.max(prev.archiveRestoreColumnIndex - 1, 0),
290
+ }));
291
+ return;
292
+ }
293
+ if (key.return) {
294
+ const task = state.archive[state.selectedArchiveIndex];
295
+ const column = allColumns[state.archiveRestoreColumnIndex];
296
+ if (task && column) {
297
+ const result = (0, actions_js_1.restoreTaskAction)(filePath, task.id, column.id);
298
+ if (result.success) {
299
+ showStatus(setState, result.message || 'Task restored', 'success');
300
+ loadBrainfile(true);
301
+ // Reload archive
302
+ const archiveResult = (0, actions_js_1.loadArchive)(filePath);
303
+ setState(prev => ({
304
+ ...prev,
305
+ mode: 'browse',
306
+ archive: archiveResult.archive,
307
+ selectedArchiveIndex: Math.max(0, prev.selectedArchiveIndex - 1),
308
+ }));
309
+ }
310
+ else {
311
+ showStatus(setState, result.error || 'Failed to restore task', 'error');
312
+ setState(prev => ({ ...prev, mode: 'browse' }));
313
+ }
314
+ }
315
+ return;
316
+ }
317
+ return;
318
+ }
319
+ // Archive delete confirmation
320
+ if (state.mode === 'archive-delete-confirm') {
321
+ const task = state.archive[state.selectedArchiveIndex];
322
+ if (input === 'y' || input === 'Y') {
323
+ if (task) {
324
+ const result = (0, actions_js_1.deleteArchivedTaskAction)(filePath, task.id);
325
+ if (result.success) {
326
+ showStatus(setState, result.message || 'Task permanently deleted', 'success');
327
+ // Reload archive
328
+ const archiveResult = (0, actions_js_1.loadArchive)(filePath);
329
+ setState(prev => ({
330
+ ...prev,
331
+ mode: 'browse',
332
+ archive: archiveResult.archive,
333
+ selectedArchiveIndex: Math.max(0, prev.selectedArchiveIndex - 1),
334
+ }));
335
+ }
336
+ else {
337
+ showStatus(setState, result.error || 'Failed to delete task', 'error');
338
+ setState(prev => ({ ...prev, mode: 'browse' }));
339
+ }
340
+ }
341
+ return;
342
+ }
343
+ if (input === 'n' || input === 'N' || key.escape) {
344
+ setState(prev => ({ ...prev, mode: 'browse' }));
345
+ showStatus(setState, 'Delete cancelled', 'info');
346
+ return;
347
+ }
348
+ return;
349
+ }
350
+ // ============================================================
351
+ // GLOBAL KEYS (work in browse mode across all panels)
352
+ // ============================================================
197
353
  if (input === 'q' || (key.ctrl && input === 'c')) {
198
354
  exit();
199
355
  return;
@@ -202,232 +358,402 @@ function useKeyboardNavigation({ state, setState, currentTasks, maxTaskIndex, fi
202
358
  setState(prev => ({ ...prev, mode: 'help' }));
203
359
  return;
204
360
  }
205
- if (input === '/') {
206
- setState(prev => ({ ...prev, mode: 'search', searchQuery: '' }));
361
+ // Panel switching: 1/2/3
362
+ if (input === '1') {
363
+ setState(prev => ({ ...prev, activePanel: 'tasks', mode: 'browse' }));
364
+ return;
365
+ }
366
+ if (input === '2') {
367
+ setState(prev => ({ ...prev, activePanel: 'rules', mode: 'browse' }));
368
+ return;
369
+ }
370
+ if (input === '3') {
371
+ setState(prev => ({ ...prev, activePanel: 'archive', mode: 'browse' }));
372
+ // Archive is loaded via useEffect in BrainfileTUI
207
373
  return;
208
374
  }
209
375
  // Refresh (force refresh bypasses hash check)
210
- if (input === 'r') {
376
+ if (input === 'r' && state.activePanel !== 'archive') {
211
377
  loadBrainfile(true);
212
378
  showStatus(setState, 'Refreshed', 'info');
213
379
  return;
214
380
  }
215
- // === Task Management Keys ===
216
- // 'e' - Edit task in $EDITOR (blocks until editor closes)
217
- if (input === 'e') {
218
- if (!currentTask) {
219
- showStatus(setState, 'No task selected', 'error');
381
+ // ============================================================
382
+ // TASKS PANEL - Browse mode
383
+ // ============================================================
384
+ if (state.activePanel === 'tasks') {
385
+ if (input === '/') {
386
+ setState(prev => ({ ...prev, mode: 'search', searchQuery: '' }));
220
387
  return;
221
388
  }
222
- // spawnSync blocks the entire process, giving terminal to editor
223
- const result = (0, actions_js_1.editTaskInEditor)(filePath, currentTask.id);
224
- if (result.success) {
225
- showStatus(setState, result.message || 'Task updated', 'success');
226
- loadBrainfile(true);
389
+ // 'e' - Edit task in $EDITOR
390
+ if (input === 'e') {
391
+ if (!currentTask) {
392
+ showStatus(setState, 'No task selected', 'error');
393
+ return;
394
+ }
395
+ const result = (0, actions_js_1.editTaskInEditor)(filePath, currentTask.id);
396
+ if (result.success) {
397
+ showStatus(setState, result.message || 'Task updated', 'success');
398
+ loadBrainfile(true);
399
+ }
400
+ else {
401
+ showStatus(setState, result.error || 'Edit failed', 'error');
402
+ }
403
+ return;
227
404
  }
228
- else {
229
- showStatus(setState, result.error || 'Edit failed', 'error');
405
+ // 'm' - Move task
406
+ if (input === 'm') {
407
+ if (!currentTask) {
408
+ showStatus(setState, 'No task selected', 'error');
409
+ return;
410
+ }
411
+ setState(prev => ({ ...prev, mode: 'move', moveTargetIndex: prev.activeColumnIndex }));
412
+ return;
230
413
  }
231
- return;
232
- }
233
- // 'm' - Move task (open column picker)
234
- if (input === 'm') {
235
- if (!currentTask) {
236
- showStatus(setState, 'No task selected', 'error');
414
+ // 'd' - Delete task
415
+ if (input === 'd') {
416
+ if (!currentTask) {
417
+ showStatus(setState, 'No task selected', 'error');
418
+ return;
419
+ }
420
+ setState(prev => ({ ...prev, mode: 'delete-confirm' }));
237
421
  return;
238
422
  }
239
- setState(prev => ({ ...prev, mode: 'move', moveTargetIndex: prev.activeColumnIndex }));
240
- return;
241
- }
242
- // 'd' - Delete task (with confirmation)
243
- if (input === 'd') {
244
- if (!currentTask) {
245
- showStatus(setState, 'No task selected', 'error');
423
+ // 'A' - Archive task
424
+ if (input === 'A') {
425
+ if (!currentTask) {
426
+ showStatus(setState, 'No task selected', 'error');
427
+ return;
428
+ }
429
+ const result = (0, actions_js_1.archiveTaskAction)(filePath, currentTask.id);
430
+ if (result.success) {
431
+ showStatus(setState, result.message || 'Task archived', 'success');
432
+ loadBrainfile(true);
433
+ }
434
+ else {
435
+ showStatus(setState, result.error || 'Archive failed', 'error');
436
+ }
246
437
  return;
247
438
  }
248
- setState(prev => ({ ...prev, mode: 'delete-confirm' }));
249
- return;
250
- }
251
- // 'a' - Archive task
252
- if (input === 'a') {
253
- if (!currentTask) {
254
- showStatus(setState, 'No task selected', 'error');
439
+ // 'p' - Cycle priority
440
+ if (input === 'p') {
441
+ if (!currentTask) {
442
+ showStatus(setState, 'No task selected', 'error');
443
+ return;
444
+ }
445
+ const result = (0, actions_js_1.cyclePriorityAction)(filePath, currentTask.id);
446
+ if (result.success) {
447
+ showStatus(setState, result.message || 'Priority updated', 'success');
448
+ loadBrainfile(true);
449
+ }
450
+ else {
451
+ showStatus(setState, result.error || 'Failed to update priority', 'error');
452
+ }
255
453
  return;
256
454
  }
257
- const result = (0, actions_js_1.archiveTaskAction)(filePath, currentTask.id);
258
- if (result.success) {
259
- showStatus(setState, result.message || 'Task archived', 'success');
260
- loadBrainfile(true);
455
+ // 't' - Toggle subtask
456
+ if (input === 't') {
457
+ if (!currentTask) {
458
+ showStatus(setState, 'No task selected', 'error');
459
+ return;
460
+ }
461
+ if (!currentTask.subtasks || currentTask.subtasks.length === 0) {
462
+ showStatus(setState, 'No subtasks', 'info');
463
+ return;
464
+ }
465
+ setState(prev => ({ ...prev, mode: 'subtask', selectedSubtaskIndex: 0 }));
466
+ return;
261
467
  }
262
- else {
263
- showStatus(setState, result.error || 'Archive failed', 'error');
468
+ // 'y' - Copy task ID
469
+ if (input === 'y') {
470
+ if (!currentTask) {
471
+ showStatus(setState, 'No task selected', 'error');
472
+ return;
473
+ }
474
+ const result = (0, actions_js_1.copyToClipboard)(currentTask.id);
475
+ if (result.success) {
476
+ showStatus(setState, `Copied ${currentTask.id}`, 'success');
477
+ }
478
+ else {
479
+ showStatus(setState, result.error || 'Copy failed', 'error');
480
+ }
481
+ return;
264
482
  }
265
- return;
266
- }
267
- // 'p' - Cycle priority
268
- if (input === 'p') {
269
- if (!currentTask) {
270
- showStatus(setState, 'No task selected', 'error');
483
+ // 'n' - New task (quick)
484
+ if (input === 'n') {
485
+ setState(prev => ({ ...prev, mode: 'new-task', newTaskTitle: '' }));
271
486
  return;
272
487
  }
273
- const result = (0, actions_js_1.cyclePriorityAction)(filePath, currentTask.id);
274
- if (result.success) {
275
- showStatus(setState, result.message || 'Priority updated', 'success');
276
- loadBrainfile(true);
488
+ // 'N' - New task (editor)
489
+ if (input === 'N') {
490
+ const currentCol = allColumns[state.activeColumnIndex];
491
+ if (currentCol) {
492
+ const result = (0, actions_js_1.newTaskInEditor)(filePath, currentCol.id);
493
+ if (result.success) {
494
+ showStatus(setState, result.message || 'Task created', 'success');
495
+ loadBrainfile(true);
496
+ }
497
+ else {
498
+ showStatus(setState, result.error || 'Failed to create task', 'error');
499
+ }
500
+ }
501
+ return;
277
502
  }
278
- else {
279
- showStatus(setState, result.error || 'Failed to update priority', 'error');
503
+ // Navigation: up/down
504
+ if (key.downArrow || input === 'j') {
505
+ setState(prev => ({
506
+ ...prev,
507
+ selectedTaskIndex: Math.min(prev.selectedTaskIndex + 1, maxTaskIndex),
508
+ }));
509
+ return;
280
510
  }
281
- return;
282
- }
283
- // 't' - Toggle subtask (open subtask picker if has subtasks)
284
- if (input === 't') {
285
- if (!currentTask) {
286
- showStatus(setState, 'No task selected', 'error');
511
+ if (key.upArrow || input === 'k') {
512
+ setState(prev => ({
513
+ ...prev,
514
+ selectedTaskIndex: Math.max(prev.selectedTaskIndex - 1, 0),
515
+ }));
287
516
  return;
288
517
  }
289
- if (!currentTask.subtasks || currentTask.subtasks.length === 0) {
290
- showStatus(setState, 'No subtasks', 'info');
518
+ // Page scrolling
519
+ if (key.ctrl && input === 'd') {
520
+ setState(prev => ({
521
+ ...prev,
522
+ selectedTaskIndex: Math.min(prev.selectedTaskIndex + Math.floor(viewportHeight / 2), maxTaskIndex),
523
+ }));
291
524
  return;
292
525
  }
293
- setState(prev => ({ ...prev, mode: 'subtask', selectedSubtaskIndex: 0 }));
294
- return;
295
- }
296
- // 'y' - Yank/copy task ID to clipboard
297
- if (input === 'y') {
298
- if (!currentTask) {
299
- showStatus(setState, 'No task selected', 'error');
526
+ if (key.ctrl && input === 'u') {
527
+ setState(prev => ({
528
+ ...prev,
529
+ selectedTaskIndex: Math.max(prev.selectedTaskIndex - Math.floor(viewportHeight / 2), 0),
530
+ }));
531
+ return;
532
+ }
533
+ // Column switching
534
+ if (key.tab || key.rightArrow || input === 'l') {
535
+ setState(prev => ({
536
+ ...prev,
537
+ activeColumnIndex: (prev.activeColumnIndex + 1) % filteredColumnsLength,
538
+ selectedTaskIndex: 0,
539
+ }));
300
540
  return;
301
541
  }
302
- const result = (0, actions_js_1.copyToClipboard)(currentTask.id);
303
- if (result.success) {
304
- showStatus(setState, `Copied ${currentTask.id}`, 'success');
542
+ if ((key.shift && key.tab) || key.leftArrow || input === 'h') {
543
+ setState(prev => ({
544
+ ...prev,
545
+ activeColumnIndex: prev.activeColumnIndex === 0
546
+ ? filteredColumnsLength - 1
547
+ : prev.activeColumnIndex - 1,
548
+ selectedTaskIndex: 0,
549
+ }));
550
+ return;
305
551
  }
306
- else {
307
- showStatus(setState, result.error || 'Copy failed', 'error');
552
+ // Home/End
553
+ if (input === 'g') {
554
+ setState(prev => ({ ...prev, selectedTaskIndex: 0 }));
555
+ return;
556
+ }
557
+ if (input === 'G') {
558
+ setState(prev => ({ ...prev, selectedTaskIndex: maxTaskIndex }));
559
+ return;
560
+ }
561
+ // Expand/collapse task
562
+ if (key.return) {
563
+ const task = currentTasks[state.selectedTaskIndex];
564
+ if (task) {
565
+ setState(prev => {
566
+ const newExpanded = new Set(prev.expandedTaskIds);
567
+ if (newExpanded.has(task.id)) {
568
+ newExpanded.delete(task.id);
569
+ }
570
+ else {
571
+ newExpanded.add(task.id);
572
+ }
573
+ return { ...prev, expandedTaskIds: newExpanded };
574
+ });
575
+ }
576
+ return;
577
+ }
578
+ // Escape: collapse all or clear search
579
+ if (key.escape) {
580
+ setState(prev => ({
581
+ ...prev,
582
+ expandedTaskIds: new Set(),
583
+ searchQuery: '',
584
+ }));
585
+ return;
308
586
  }
309
- return;
310
- }
311
- // 'n' - New task (quick add - title only)
312
- if (input === 'n') {
313
- setState(prev => ({ ...prev, mode: 'new-task', newTaskTitle: '' }));
314
- return;
315
587
  }
316
- // 'N' - New task (full editor)
317
- if (input === 'N') {
318
- const currentCol = allColumns[state.activeColumnIndex];
319
- if (currentCol) {
320
- const result = (0, actions_js_1.newTaskInEditor)(filePath, currentCol.id);
321
- if (result.success) {
322
- showStatus(setState, result.message || 'Task created', 'success');
323
- loadBrainfile(true);
588
+ // ============================================================
589
+ // RULES PANEL - Browse mode
590
+ // ============================================================
591
+ if (state.activePanel === 'rules') {
592
+ const rules = state.board?.rules?.[state.activeRuleType] || [];
593
+ const maxRuleIndex = Math.max(0, rules.length - 1);
594
+ // h/l or left/right to switch rule type
595
+ if (key.leftArrow || input === 'h') {
596
+ const currentIdx = RULE_TYPES.indexOf(state.activeRuleType);
597
+ const newIdx = currentIdx === 0 ? RULE_TYPES.length - 1 : currentIdx - 1;
598
+ setState(prev => ({ ...prev, activeRuleType: RULE_TYPES[newIdx], selectedRuleIndex: 0 }));
599
+ return;
600
+ }
601
+ if (key.rightArrow || input === 'l') {
602
+ const currentIdx = RULE_TYPES.indexOf(state.activeRuleType);
603
+ const newIdx = (currentIdx + 1) % RULE_TYPES.length;
604
+ setState(prev => ({ ...prev, activeRuleType: RULE_TYPES[newIdx], selectedRuleIndex: 0 }));
605
+ return;
606
+ }
607
+ // j/k to navigate rules
608
+ if (key.downArrow || input === 'j') {
609
+ setState(prev => ({
610
+ ...prev,
611
+ selectedRuleIndex: Math.min(prev.selectedRuleIndex + 1, maxRuleIndex),
612
+ }));
613
+ return;
614
+ }
615
+ if (key.upArrow || input === 'k') {
616
+ setState(prev => ({
617
+ ...prev,
618
+ selectedRuleIndex: Math.max(prev.selectedRuleIndex - 1, 0),
619
+ }));
620
+ return;
621
+ }
622
+ // n - New rule
623
+ if (input === 'n') {
624
+ setState(prev => ({ ...prev, mode: 'rule-add', ruleEditText: '', ruleEditId: null }));
625
+ return;
626
+ }
627
+ // e - Edit rule
628
+ if (input === 'e') {
629
+ const rule = rules[state.selectedRuleIndex];
630
+ if (rule) {
631
+ setState(prev => ({
632
+ ...prev,
633
+ mode: 'rule-edit',
634
+ ruleEditText: rule.rule,
635
+ ruleEditId: rule.id,
636
+ }));
324
637
  }
325
638
  else {
326
- showStatus(setState, result.error || 'Failed to create task', 'error');
639
+ showStatus(setState, 'No rule selected', 'error');
327
640
  }
641
+ return;
328
642
  }
329
- return;
330
- }
331
- // Navigation: up/down through tasks
332
- if (key.downArrow || input === 'j') {
333
- setState(prev => ({
334
- ...prev,
335
- selectedTaskIndex: Math.min(prev.selectedTaskIndex + 1, maxTaskIndex),
336
- }));
337
- return;
338
- }
339
- if (key.upArrow || input === 'k') {
340
- setState(prev => ({
341
- ...prev,
342
- selectedTaskIndex: Math.max(prev.selectedTaskIndex - 1, 0),
343
- }));
344
- return;
345
- }
346
- // Page scrolling
347
- if (key.ctrl && input === 'd') {
348
- setState(prev => ({
349
- ...prev,
350
- selectedTaskIndex: Math.min(prev.selectedTaskIndex + Math.floor(viewportHeight / 2), maxTaskIndex),
351
- }));
352
- return;
353
- }
354
- if (key.ctrl && input === 'u') {
355
- setState(prev => ({
356
- ...prev,
357
- selectedTaskIndex: Math.max(prev.selectedTaskIndex - Math.floor(viewportHeight / 2), 0),
358
- }));
359
- return;
360
- }
361
- // Column switching: TAB / left/right
362
- if (key.tab || key.rightArrow || input === 'l') {
363
- setState(prev => ({
364
- ...prev,
365
- activeColumnIndex: (prev.activeColumnIndex + 1) % filteredColumnsLength,
366
- selectedTaskIndex: 0,
367
- }));
368
- return;
369
- }
370
- if ((key.shift && key.tab) || key.leftArrow || input === 'h') {
371
- setState(prev => ({
372
- ...prev,
373
- activeColumnIndex: prev.activeColumnIndex === 0
374
- ? filteredColumnsLength - 1
375
- : prev.activeColumnIndex - 1,
376
- selectedTaskIndex: 0,
377
- }));
378
- return;
379
- }
380
- // Jump to column headers with { and }
381
- if (input === '{') {
382
- setState(prev => ({
383
- ...prev,
384
- activeColumnIndex: Math.max(0, prev.activeColumnIndex - 1),
385
- selectedTaskIndex: 0,
386
- }));
387
- return;
388
- }
389
- if (input === '}') {
390
- setState(prev => ({
391
- ...prev,
392
- activeColumnIndex: Math.min(filteredColumnsLength - 1, prev.activeColumnIndex + 1),
393
- selectedTaskIndex: 0,
394
- }));
395
- return;
396
- }
397
- // Home/End
398
- if (input === 'g') {
399
- setState(prev => ({ ...prev, selectedTaskIndex: 0 }));
400
- return;
401
- }
402
- if (input === 'G') {
403
- setState(prev => ({ ...prev, selectedTaskIndex: maxTaskIndex }));
404
- return;
405
- }
406
- // Expand/collapse task
407
- if (key.return) {
408
- const task = currentTasks[state.selectedTaskIndex];
409
- if (task) {
410
- setState(prev => {
411
- const newExpanded = new Set(prev.expandedTaskIds);
412
- if (newExpanded.has(task.id)) {
413
- newExpanded.delete(task.id);
414
- }
415
- else {
416
- newExpanded.add(task.id);
417
- }
418
- return { ...prev, expandedTaskIds: newExpanded };
419
- });
643
+ // d - Delete rule
644
+ if (input === 'd') {
645
+ const rule = rules[state.selectedRuleIndex];
646
+ if (rule) {
647
+ setState(prev => ({ ...prev, mode: 'rule-delete-confirm' }));
648
+ }
649
+ else {
650
+ showStatus(setState, 'No rule selected', 'error');
651
+ }
652
+ return;
653
+ }
654
+ // Home/End
655
+ if (input === 'g') {
656
+ setState(prev => ({ ...prev, selectedRuleIndex: 0 }));
657
+ return;
658
+ }
659
+ if (input === 'G') {
660
+ setState(prev => ({ ...prev, selectedRuleIndex: maxRuleIndex }));
661
+ return;
662
+ }
663
+ // Escape
664
+ if (key.escape) {
665
+ setState(prev => ({ ...prev, selectedRuleIndex: 0 }));
666
+ return;
420
667
  }
421
- return;
422
668
  }
423
- // Escape: collapse all or clear search
424
- if (key.escape) {
425
- setState(prev => ({
426
- ...prev,
427
- expandedTaskIds: new Set(),
428
- searchQuery: '',
429
- }));
430
- return;
669
+ // ============================================================
670
+ // ARCHIVE PANEL - Browse mode
671
+ // ============================================================
672
+ if (state.activePanel === 'archive') {
673
+ const maxArchiveIndex = Math.max(0, state.archive.length - 1);
674
+ // / - Search archive
675
+ if (input === '/') {
676
+ // Archive search is inline in the panel, just focus on it
677
+ // For simplicity, we'll clear and let user type
678
+ setState(prev => ({ ...prev, archiveSearchQuery: '' }));
679
+ showStatus(setState, 'Type to search archive', 'info');
680
+ return;
681
+ }
682
+ // r - Refresh archive
683
+ if (input === 'r') {
684
+ const result = (0, actions_js_1.loadArchive)(filePath);
685
+ setState(prev => ({ ...prev, archive: result.archive }));
686
+ showStatus(setState, 'Archive refreshed', 'info');
687
+ return;
688
+ }
689
+ // j/k navigation
690
+ if (key.downArrow || input === 'j') {
691
+ setState(prev => ({
692
+ ...prev,
693
+ selectedArchiveIndex: Math.min(prev.selectedArchiveIndex + 1, maxArchiveIndex),
694
+ }));
695
+ return;
696
+ }
697
+ if (key.upArrow || input === 'k') {
698
+ setState(prev => ({
699
+ ...prev,
700
+ selectedArchiveIndex: Math.max(prev.selectedArchiveIndex - 1, 0),
701
+ }));
702
+ return;
703
+ }
704
+ // Enter - Expand/collapse
705
+ if (key.return) {
706
+ const task = state.archive[state.selectedArchiveIndex];
707
+ if (task) {
708
+ setState(prev => {
709
+ const newExpanded = new Set(prev.expandedArchiveIds);
710
+ if (newExpanded.has(task.id)) {
711
+ newExpanded.delete(task.id);
712
+ }
713
+ else {
714
+ newExpanded.add(task.id);
715
+ }
716
+ return { ...prev, expandedArchiveIds: newExpanded };
717
+ });
718
+ }
719
+ return;
720
+ }
721
+ // r key for restore (use 'R' to avoid conflict with refresh)
722
+ if (input === 'R') {
723
+ if (state.archive.length === 0) {
724
+ showStatus(setState, 'No archived tasks', 'error');
725
+ return;
726
+ }
727
+ setState(prev => ({ ...prev, mode: 'archive-restore', archiveRestoreColumnIndex: 0 }));
728
+ return;
729
+ }
730
+ // d - Delete permanently
731
+ if (input === 'd') {
732
+ if (state.archive.length === 0) {
733
+ showStatus(setState, 'No archived tasks', 'error');
734
+ return;
735
+ }
736
+ setState(prev => ({ ...prev, mode: 'archive-delete-confirm' }));
737
+ return;
738
+ }
739
+ // Home/End
740
+ if (input === 'g') {
741
+ setState(prev => ({ ...prev, selectedArchiveIndex: 0 }));
742
+ return;
743
+ }
744
+ if (input === 'G') {
745
+ setState(prev => ({ ...prev, selectedArchiveIndex: maxArchiveIndex }));
746
+ return;
747
+ }
748
+ // Escape
749
+ if (key.escape) {
750
+ setState(prev => ({
751
+ ...prev,
752
+ expandedArchiveIds: new Set(),
753
+ archiveSearchQuery: '',
754
+ }));
755
+ return;
756
+ }
431
757
  }
432
758
  });
433
759
  }