@lamemind/loom-deck 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/cli.js +160 -3
- package/dist/config.js +17 -0
- package/dist/task-edit.js +160 -0
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -10,8 +10,9 @@ import { dirname, join } from 'node:path';
|
|
|
10
10
|
import { resolveTasksPath, resolveTasksDir, loadTasks, loadTaskDetail, } from './tasks.js';
|
|
11
11
|
import { discoverProjectSessions } from './sessions.js';
|
|
12
12
|
import { appendTaskBinding, loadTaskBindings } from './task-index.js';
|
|
13
|
-
import { loadLaunch } from './config.js';
|
|
14
|
-
import { applyView, cycleSort, describeSort, toggleHidden, PRI_ENTRIES, PROG_ENTRIES, } from './view.js';
|
|
13
|
+
import { loadIdentity, loadLaunch } from './config.js';
|
|
14
|
+
import { applyView, cycleSort, describeSort, priName, progName, toggleHidden, PRI_ENTRIES, PROG_ENTRIES, } from './view.js';
|
|
15
|
+
import { initialDetail, progressText, writeTaskEdit, PRI_GLYPH, PRI_LABEL, PROG_GLYPH, } from './task-edit.js';
|
|
15
16
|
import { loadView, saveView, viewFilePath } from './view-store.js';
|
|
16
17
|
// scripts/deck-run è un sibling della dir del bundle: src/ (dev, tsx) e dist/
|
|
17
18
|
// (build, node) stanno entrambi sotto la package root → risalita di un livello.
|
|
@@ -27,6 +28,13 @@ const SPOT = Symbol('spot');
|
|
|
27
28
|
// Modale sort a grammatica libera: un tasto per chiave, pressioni successive
|
|
28
29
|
// ciclano asc → desc → fuori dalla chain.
|
|
29
30
|
const SORT_TASTI = { p: 'pri', s: 'prog', i: 'id' };
|
|
31
|
+
// T41 — ordine dei valori nel modale edit. Deliberatamente DIVERSO da
|
|
32
|
+
// PRI_ENTRIES/PROG_ENTRIES (che seguono il rango di sort): qui si sceglie un
|
|
33
|
+
// valore, non si ordina, quindi vince l'ordine del CICLO DI VITA — da fare →
|
|
34
|
+
// in corso → chiusa → bloccata. La priorità resta alta→bassa, che è già
|
|
35
|
+
// l'ordine naturale di lettura.
|
|
36
|
+
const EDIT_PRI = ['high', 'med', 'low'];
|
|
37
|
+
const EDIT_PROG = ['todo', 'wip', 'done', 'locked'];
|
|
30
38
|
function isDone(prog) {
|
|
31
39
|
return prog.includes('✔');
|
|
32
40
|
}
|
|
@@ -59,6 +67,22 @@ function runLaunch(entry, cwd) {
|
|
|
59
67
|
child.unref();
|
|
60
68
|
return child;
|
|
61
69
|
}
|
|
70
|
+
// T37 — surface STANDARD LAUNCH `terminal`: built-in e universale (nessuna
|
|
71
|
+
// dichiarazione in `launch[]`), ma di natura launch — fire-once, nessuno stato.
|
|
72
|
+
// Il deck gira già DENTRO una tab Ptyxis → `--tab` mette il terminale accanto a
|
|
73
|
+
// sé nella stessa finestra, invece di sparpagliare finestre.
|
|
74
|
+
// Nessun `-- CMD`: l'azione È aprire la shell (differenza dalle launch custom,
|
|
75
|
+
// che eseguono un comando dentro `bash -lic`).
|
|
76
|
+
// `-T <title>` col core `<owner> <name>` tiene la finestra matchabile da compass
|
|
77
|
+
// anche mentre la tab attiva è il terminale; senza identità nel file config si
|
|
78
|
+
// spawna senza titolo (la surface resta funzionante, il progetto risulta assente
|
|
79
|
+
// dal radar finché quella tab è in primo piano).
|
|
80
|
+
function spawnTerminal(cwd, title) {
|
|
81
|
+
const args = title ? ['--tab', '-T', title, '-d', cwd] : ['--tab', '-d', cwd];
|
|
82
|
+
const child = spawn('ptyxis', args, { cwd, detached: true, stdio: 'ignore' });
|
|
83
|
+
child.unref();
|
|
84
|
+
return child;
|
|
85
|
+
}
|
|
62
86
|
// Comando claude (override per ambienti dove non è su PATH; loom-deck → NPM).
|
|
63
87
|
const CLAUDE_CMD = process.env.LOOM_DECK_CLAUDE_CMD ?? 'claude';
|
|
64
88
|
// T30: create-task inline. Spawna CC HEADLESS (`-p`) con `--session-id` pinnato
|
|
@@ -109,6 +133,24 @@ function spawnCreateTask(text, cwd, sessionId, onResult) {
|
|
|
109
133
|
});
|
|
110
134
|
return child;
|
|
111
135
|
}
|
|
136
|
+
// T41 — Commit dell'edit. `git commit -- <paths>` committa lo stato working-tree
|
|
137
|
+
// SOLO di quei path, ignorando l'index: se l'utente ha altro in stage (o altri
|
|
138
|
+
// file sporchi) non finisce dentro per errore. NON detached: è un'operazione
|
|
139
|
+
// veloce e il suo esito va riportato nella nota. stderr raccolto per dire perché
|
|
140
|
+
// ha fallito (identità git assente, hook che rifiuta, …) invece di un generico ⚠.
|
|
141
|
+
function commitTaskEdit(cwd, paths, message, onResult) {
|
|
142
|
+
const child = spawn('git', ['commit', '-m', message, '--', ...paths], {
|
|
143
|
+
cwd,
|
|
144
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
145
|
+
});
|
|
146
|
+
let err = '';
|
|
147
|
+
child.stderr?.on('data', (c) => {
|
|
148
|
+
err += c.toString();
|
|
149
|
+
});
|
|
150
|
+
child.on('error', () => onResult(false, 'git non lanciabile'));
|
|
151
|
+
child.on('close', (code) => onResult(code === 0, err.trim().split('\n')[0] ?? ''));
|
|
152
|
+
return child;
|
|
153
|
+
}
|
|
112
154
|
// Carica tasks.md e lo ri-legge quando cambia sotto (poll su mtime). Poll
|
|
113
155
|
// (non fs.watch) perché i writer di tasks.md — checkpoint-task/create-task —
|
|
114
156
|
// riscrivono il file (probabile replace atomico), che rompe il watch sull'inode
|
|
@@ -255,8 +297,13 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
255
297
|
const [view, setView] = useState(() => loadView(cwd));
|
|
256
298
|
const [viewBackup, setViewBackup] = useState(null);
|
|
257
299
|
const [filterCursor, setFilterCursor] = useState({ row: 0, col: 0 });
|
|
300
|
+
// T41 — bozza dell'edit (null fuori dal modale) e riga attiva della griglia.
|
|
301
|
+
const [edit, setEdit] = useState(null);
|
|
302
|
+
const [editRow, setEditRow] = useState(0);
|
|
258
303
|
// Voci launch del progetto (T32): lette una volta, raggiunte per indice 1..9.
|
|
259
304
|
const launch = useMemo(() => loadLaunch(cwd), [cwd]);
|
|
305
|
+
// Identità (T37): titolo delle tab terminale spawnate col tasto `t`.
|
|
306
|
+
const identity = useMemo(() => loadIdentity(cwd), [cwd]);
|
|
260
307
|
// La vista è una trasformazione DERIVATA, applicata a valle del load: il
|
|
261
308
|
// polling di tasks.md continua a funzionare senza saperne nulla.
|
|
262
309
|
const { visible: viewTasks, hidden: hiddenTasks } = useMemo(() => applyView(tasks, view), [tasks, view]);
|
|
@@ -338,6 +385,53 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
338
385
|
});
|
|
339
386
|
child.on('error', () => setNote(`⚠ create-task: '${CLAUDE_CMD}' non lanciabile`));
|
|
340
387
|
}
|
|
388
|
+
// T41 — apertura dell'edit: la bozza parte dai valori ATTUALI della task, non
|
|
389
|
+
// da default. La priorità arriva dal glifo di tasks.md (già in `selTask`), lo
|
|
390
|
+
// stato dal suo glifo Prog; il progresso arbitrario dal campo `Progress` del
|
|
391
|
+
// task file — ma solo se è davvero custom (vedi `initialDetail`).
|
|
392
|
+
function openEdit() {
|
|
393
|
+
if (!selTask)
|
|
394
|
+
return;
|
|
395
|
+
const prog = progName(selTask.prog) ?? 'todo';
|
|
396
|
+
setEdit({
|
|
397
|
+
pri: priName(selTask.pri) ?? 'med',
|
|
398
|
+
prog,
|
|
399
|
+
detail: initialDetail(detail?.fields['Progress'] ?? '', prog),
|
|
400
|
+
});
|
|
401
|
+
setEditRow(0);
|
|
402
|
+
setNote('');
|
|
403
|
+
setMode('edit');
|
|
404
|
+
}
|
|
405
|
+
// T41 — ⏎ nell'edit: scrive tasks.md + task file, poi committa. Il commit è
|
|
406
|
+
// immediato e non confermato (scelta esplicita: l'edit è una micro-modifica,
|
|
407
|
+
// la storia granulare vale più di un batch). Se nessuno dei due lati è stato
|
|
408
|
+
// scritto non si committa nulla — `paths` vuoto renderebbe `git commit --`
|
|
409
|
+
// un commit di TUTTO il working tree, che è l'opposto di ciò che vogliamo.
|
|
410
|
+
function submitEdit() {
|
|
411
|
+
const task = selTask;
|
|
412
|
+
const draft = edit;
|
|
413
|
+
setMode('normal');
|
|
414
|
+
setEdit(null);
|
|
415
|
+
if (!task || !draft)
|
|
416
|
+
return;
|
|
417
|
+
let res;
|
|
418
|
+
try {
|
|
419
|
+
res = writeTaskEdit({ tasksPath, tasksDir, id: task.id, ...draft });
|
|
420
|
+
}
|
|
421
|
+
catch (e) {
|
|
422
|
+
setNote(`⚠ ${task.id}: scrittura fallita (${e.message})`);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (res.paths.length === 0) {
|
|
426
|
+
setNote(`⚠ ${task.id}: nessun campo aggiornabile (riga o task file assenti)`);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
const summary = `${PRI_GLYPH[draft.pri]} ${PRI_LABEL[draft.pri]} · ${res.progress}`;
|
|
430
|
+
setNote(`⏳ ${task.id} → ${summary} · commit…`);
|
|
431
|
+
commitTaskEdit(cwd, res.paths, `chore(${task.id}): pri ${PRI_LABEL[draft.pri]} · stato ${res.progress}`, (ok, err) => {
|
|
432
|
+
setNote(ok ? `✔ ${task.id} → ${summary} · committato` : `⚠ ${task.id} salvato, commit fallito: ${err}`);
|
|
433
|
+
});
|
|
434
|
+
}
|
|
341
435
|
// Chiusura di un modale di vista: `restore` rimette la fotografia scattata
|
|
342
436
|
// all'apertura (esc = annulla), altrimenti tiene ciò che si è composto (⏎).
|
|
343
437
|
function closeViewModal(restore) {
|
|
@@ -428,6 +522,48 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
428
522
|
}
|
|
429
523
|
return;
|
|
430
524
|
}
|
|
525
|
+
// T41 — modale edit: griglia a 3 righe. Righe 0/1 = scelta a valore singolo
|
|
526
|
+
// (←→ scorre), riga 2 = testo libero (ogni carattere stampabile entra nel
|
|
527
|
+
// progresso). Come gli altri modali cattura tutto: `esc` annulla senza
|
|
528
|
+
// scrivere né uscire dal deck.
|
|
529
|
+
if (mode === 'edit') {
|
|
530
|
+
if (key.escape) {
|
|
531
|
+
setMode('normal');
|
|
532
|
+
setEdit(null);
|
|
533
|
+
setNote('E → edit annullato');
|
|
534
|
+
}
|
|
535
|
+
else if (key.return) {
|
|
536
|
+
submitEdit();
|
|
537
|
+
}
|
|
538
|
+
else if (key.upArrow) {
|
|
539
|
+
setEditRow((r) => ((r + 2) % 3));
|
|
540
|
+
}
|
|
541
|
+
else if (key.downArrow) {
|
|
542
|
+
setEditRow((r) => ((r + 1) % 3));
|
|
543
|
+
}
|
|
544
|
+
else if (key.leftArrow || key.rightArrow) {
|
|
545
|
+
const d = key.leftArrow ? -1 : 1;
|
|
546
|
+
// Scorrimento CICLICO (wrap) e non clampato: le liste sono di 3-4 voci,
|
|
547
|
+
// arrivare in fondo e ripartire costa meno di invertire direzione.
|
|
548
|
+
if (editRow === 0) {
|
|
549
|
+
setEdit((e) => e ? { ...e, pri: EDIT_PRI[(EDIT_PRI.indexOf(e.pri) + d + EDIT_PRI.length) % EDIT_PRI.length] } : e);
|
|
550
|
+
}
|
|
551
|
+
else if (editRow === 1) {
|
|
552
|
+
setEdit((e) => e
|
|
553
|
+
? { ...e, prog: EDIT_PROG[(EDIT_PROG.indexOf(e.prog) + d + EDIT_PROG.length) % EDIT_PROG.length] }
|
|
554
|
+
: e);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
else if (editRow === 2) {
|
|
558
|
+
if (key.backspace || key.delete) {
|
|
559
|
+
setEdit((e) => (e ? { ...e, detail: e.detail.slice(0, -1) } : e));
|
|
560
|
+
}
|
|
561
|
+
else if (input && !key.ctrl && !key.meta) {
|
|
562
|
+
setEdit((e) => (e ? { ...e, detail: e.detail + input } : e));
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
431
567
|
if (key.leftArrow || key.rightArrow || key.tab) {
|
|
432
568
|
setFocus((f) => (f === 'tasks' ? 'sessions' : 'tasks'));
|
|
433
569
|
}
|
|
@@ -469,6 +605,13 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
469
605
|
setNote('');
|
|
470
606
|
setMode('create');
|
|
471
607
|
}
|
|
608
|
+
else if (input === 'E') {
|
|
609
|
+
// L'edit ha senso solo su una task reale: la riga meta "spot" non ne è una.
|
|
610
|
+
if (isSpot || !selTask)
|
|
611
|
+
setNote('E → nessuna task selezionata');
|
|
612
|
+
else
|
|
613
|
+
openEdit();
|
|
614
|
+
}
|
|
472
615
|
else if (input === 'S') {
|
|
473
616
|
setViewBackup(view);
|
|
474
617
|
setNote('');
|
|
@@ -479,6 +622,12 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
479
622
|
setNote('');
|
|
480
623
|
setMode('filter');
|
|
481
624
|
}
|
|
625
|
+
else if (input === 't') {
|
|
626
|
+
const title = identity ? `🖥️ ${identity.owner} ${identity.name} [term]` : null;
|
|
627
|
+
const child = spawnTerminal(cwd, title);
|
|
628
|
+
child.on('error', () => setNote('⚠ t → ptyxis non lanciabile'));
|
|
629
|
+
setNote(`t → terminale su ${projectName}`);
|
|
630
|
+
}
|
|
482
631
|
else if (input === 'w') {
|
|
483
632
|
// Salvataggio ESPLICITO: comporre una vista non tocca il disco, così
|
|
484
633
|
// sperimentare non sporca lo stato persistito.
|
|
@@ -509,7 +658,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
509
658
|
const parentLabel = isSpot ? 'spot' : selectedTaskId ?? '—';
|
|
510
659
|
const canSpawn = focus === 'tasks' && !isSpot;
|
|
511
660
|
const launchHint = launch.length > 0 ? `1-${Math.min(9, launch.length)} launch · ` : '';
|
|
512
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), mode === 'create' ? (_jsxs(Text, { dimColor: true, children: ["nuova task \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " crea \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'sort' ? (_jsxs(Text, { dimColor: true, children: ["sort \u00B7 ", _jsx(Text, { color: "yellow", children: "p" }), " pri ", _jsx(Text, { color: "yellow", children: "s" }), " stato", ' ', _jsx(Text, { color: "yellow", children: "i" }), " id (asc\u2192desc\u2192off) \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " ok \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'filter' ? (_jsxs(Text, { dimColor: true, children: ["filtri \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2191\u2193\u2190\u2192" }), " naviga \u00B7 ", _jsx(Text, { color: "yellow", children: "spazio" }), ' ', "mostra/nascondi \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " ok \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : (_jsxs(Text, { dimColor: true, children: ["\u2191\u2193 naviga \u00B7 \u2190\u2192 pane \u00B7 \u23CE ", canSpawn ? 'spawn' : '—', " \u00B7 C nuova \u00B7 S sort \u00B7 F filtri \u00B7 w salva \u00B7
|
|
661
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), mode === 'create' ? (_jsxs(Text, { dimColor: true, children: ["nuova task \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " crea \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'sort' ? (_jsxs(Text, { dimColor: true, children: ["sort \u00B7 ", _jsx(Text, { color: "yellow", children: "p" }), " pri ", _jsx(Text, { color: "yellow", children: "s" }), " stato", ' ', _jsx(Text, { color: "yellow", children: "i" }), " id (asc\u2192desc\u2192off) \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " ok \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'filter' ? (_jsxs(Text, { dimColor: true, children: ["filtri \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2191\u2193\u2190\u2192" }), " naviga \u00B7 ", _jsx(Text, { color: "yellow", children: "spazio" }), ' ', "mostra/nascondi \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " ok \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'edit' ? (_jsxs(Text, { dimColor: true, children: ["edit \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2191\u2193" }), " campo \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " valore \u00B7", ' ', _jsx(Text, { color: "yellow", children: "\u23CE" }), " salva+commit \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : (_jsxs(Text, { dimColor: true, children: ["\u2191\u2193 naviga \u00B7 \u2190\u2192 pane \u00B7 \u23CE ", canSpawn ? 'spawn' : '—', " \u00B7 C nuova \u00B7 E edit \u00B7 S sort \u00B7 F filtri \u00B7 w salva \u00B7 t term \u00B7 ", launchHint, "q esci \u00B7 focus: ", _jsx(Text, { color: "cyan", children: focus })] })), mode === 'create' ? (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: "yellow", children: "C \u203A " }), _jsx(Text, { children: draft }), _jsx(Text, { inverse: true, children: " " })] })) : null, mode === 'sort' ? _jsx(SortModal, { sort: view.sort }) : null, mode === 'filter' ? _jsx(FilterModal, { view: view, cursor: filterCursor }) : null, mode === 'edit' && edit && selTask ? (_jsx(EditModal, { id: selTask.id, draft: edit, row: editRow })) : null, _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(TasksPane, { tasks: viewTasks, total: tasks.length, hidden: hiddenTasks, view: view, selected: selIndex, spotCount: spotCount, childCount: childCount, focused: focus === 'tasks', loadError: loadError, detail: detail }), _jsx(SessionsPane, { parentLabel: parentLabel, isSpot: isSpot, sessions: visibleSessions, total: childSessions.length, hidden: hiddenSessions, selectedId: selSessionId, focused: focus === 'sessions' })] }), note ? _jsx(Text, { color: "green", children: note }) : null] }));
|
|
513
662
|
}
|
|
514
663
|
const SORT_UI = { pri: 'pri', prog: 'stato', id: 'id' };
|
|
515
664
|
// Modali resi IN FLUSSO (come l'input box di create), non in overlay assoluto:
|
|
@@ -531,6 +680,14 @@ function FilterModal({ view, cursor }) {
|
|
|
531
680
|
return (_jsxs(Text, { inverse: here, color: on ? 'green' : 'gray', dimColor: !on, children: [' ', "[", on ? 'x' : ' ', "] ", forceEmojiWidth(e.glyph)] }, e.name));
|
|
532
681
|
})] }, row.label)))] }));
|
|
533
682
|
}
|
|
683
|
+
// T41 — modale edit, in flusso come gli altri (spinge giù i pane invece di
|
|
684
|
+
// coprirli: la riga che stai modificando resta visibile sopra la lista).
|
|
685
|
+
// La riga di anteprima mostra il testo ESATTO che finirà nel campo `Progress`
|
|
686
|
+
// del task file — così il default (`✔️ Done at <oggi>`) non è una sorpresa.
|
|
687
|
+
function EditModal({ id, draft, row }) {
|
|
688
|
+
const mark = (r) => (row === r ? '▶ ' : ' ');
|
|
689
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, marginTop: 1, children: [_jsxs(Text, { color: "yellow", children: ["E \u203A ", id, " \u00B7 priorit\u00E0 e stato"] }), _jsxs(Text, { children: [mark(0), _jsx(Text, { dimColor: true, children: "pri " }), EDIT_PRI.map((p) => (_jsxs(Text, { inverse: draft.pri === p, color: draft.pri === p ? 'green' : 'gray', children: [' ', forceEmojiWidth(PRI_GLYPH[p]), " ", PRI_LABEL[p]] }, p)))] }), _jsxs(Text, { children: [mark(1), _jsx(Text, { dimColor: true, children: "stato" }), EDIT_PROG.map((p) => (_jsxs(Text, { inverse: draft.prog === p, color: draft.prog === p ? 'green' : 'gray', children: [' ', forceEmojiWidth(PROG_GLYPH[p]), " ", p] }, p)))] }), _jsxs(Text, { children: [mark(2), _jsx(Text, { dimColor: true, children: "prog " }), _jsxs(Text, { children: [' ', draft.detail] }), row === 2 ? _jsx(Text, { inverse: true, children: " " }) : null, !draft.detail && row !== 2 ? _jsx(Text, { dimColor: true, children: "(default)" }) : null] }), _jsxs(Text, { dimColor: true, children: ["\u21B3 ", progressText(draft.prog, draft.detail)] })] }));
|
|
690
|
+
}
|
|
534
691
|
function TasksPane({ tasks, total, hidden, view, selected, spotCount, childCount, focused, loadError, detail, }) {
|
|
535
692
|
const spotSelected = selected === 0;
|
|
536
693
|
return (_jsxs(Box, { flexDirection: "column", width: "50%", marginRight: 1, borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', paddingX: 1, children: [_jsxs(Text, { bold: true, color: focused ? 'cyan' : undefined, children: ["Tasks (", hidden > 0 ? `${tasks.length}/${total}` : tasks.length, ")", hidden > 0 ? _jsxs(Text, { color: "yellow", children: [" \u00B7 ", hidden, " nascoste"] }) : null] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["sort: ", describeSort(view.sort), view.hiddenPri.length + view.hiddenProg.length > 0 ? (_jsxs(Text, { children: [' ', "\u00B7 filtri:", ' ', [
|
package/dist/config.js
CHANGED
|
@@ -39,3 +39,20 @@ export function loadLaunch(projectRoot) {
|
|
|
39
39
|
return [];
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
+
export function parseIdentity(raw) {
|
|
43
|
+
if (!raw || typeof raw !== 'object')
|
|
44
|
+
return null;
|
|
45
|
+
const { owner, name } = raw;
|
|
46
|
+
if (typeof owner !== 'string' || !owner || typeof name !== 'string' || !name)
|
|
47
|
+
return null;
|
|
48
|
+
return { owner, name };
|
|
49
|
+
}
|
|
50
|
+
/** File assente o malformato → nessuna identità. Mai un throw. */
|
|
51
|
+
export function loadIdentity(projectRoot) {
|
|
52
|
+
try {
|
|
53
|
+
return parseIdentity(JSON.parse(readFileSync(configFilePath(projectRoot), 'utf8')));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// T41 — Edit inline di priorità/stato dal deck.
|
|
2
|
+
// Le funzioni di rewrite sono PURE (stringa → stringa): la scrittura su disco è
|
|
3
|
+
// isolata in `writeTaskEdit`, così la grammatica delle sostituzioni è testabile
|
|
4
|
+
// senza toccare il filesystem.
|
|
5
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { findTaskFile } from './tasks.js';
|
|
7
|
+
// Il task file scrive la priorità per NOME (`- **Priority**: Med`), tasks.md la
|
|
8
|
+
// scrive come GLIFO (colonna Pri). Due rappresentazioni dello stesso valore →
|
|
9
|
+
// due mappe, entrambe keyed sul nome canonico di view.ts (unica fonte del rango).
|
|
10
|
+
export const PRI_LABEL = { high: 'High', med: 'Med', low: 'Low' };
|
|
11
|
+
export const PRI_GLYPH = { high: '🔥', med: '⚡', low: '🔹' };
|
|
12
|
+
// `✔️` CON VS16: è la forma usata nelle righe Done già presenti in tasks.md e nei
|
|
13
|
+
// task file. view.ts normalizza via VS16 in lettura, quindi scrivere la forma
|
|
14
|
+
// lunga resta riconosciuto da priName/progName/isDone.
|
|
15
|
+
export const PROG_GLYPH = {
|
|
16
|
+
todo: '🔵',
|
|
17
|
+
wip: '🟡',
|
|
18
|
+
done: '✔️',
|
|
19
|
+
locked: '🔒',
|
|
20
|
+
};
|
|
21
|
+
// Testo di default per stato, usato quando l'utente non digita un progresso
|
|
22
|
+
// arbitrario. Il campo `Progress` del task file è testo libero (`🟡 85%`,
|
|
23
|
+
// `🔵 Todo`, `✔️ Done at 2026-07-20`) → il glifo è il prefisso, il resto è prosa.
|
|
24
|
+
const PROG_DEFAULT = {
|
|
25
|
+
todo: 'Todo',
|
|
26
|
+
wip: 'In Progress',
|
|
27
|
+
done: 'Done',
|
|
28
|
+
locked: 'Locked',
|
|
29
|
+
};
|
|
30
|
+
/** Data locale `YYYY-MM-DD` (non UTC: `toISOString` sposterebbe il giorno la sera). */
|
|
31
|
+
export function today(d = new Date()) {
|
|
32
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
33
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Riga `Progress` del task file. `detail` è il progresso ARBITRARIO digitato
|
|
37
|
+
* dall'utente: se c'è vince sempre (anche su done/locked — nessuna forma è
|
|
38
|
+
* preclusa), se manca si cade sul default dello stato. Solo `done` senza detail
|
|
39
|
+
* data-stampa, coerente con le righe già presenti (`✔️ Done at YYYY-MM-DD`).
|
|
40
|
+
*/
|
|
41
|
+
export function progressText(prog, detail, date = today()) {
|
|
42
|
+
const glyph = PROG_GLYPH[prog];
|
|
43
|
+
const d = detail.trim();
|
|
44
|
+
if (d)
|
|
45
|
+
return `${glyph} ${d}`;
|
|
46
|
+
if (prog === 'done')
|
|
47
|
+
return `${glyph} Done at ${date}`;
|
|
48
|
+
return `${glyph} ${PROG_DEFAULT[prog]}`;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Toglie il glifo di testa da un valore `Progress` (`🟡 85%` → `85%`).
|
|
52
|
+
* Il glifo lo ri-deriva `progressText` dallo stato selezionato.
|
|
53
|
+
*/
|
|
54
|
+
export function stripProgGlyph(s) {
|
|
55
|
+
return s.replace(/^[\p{Extended_Pictographic}✔️\s]+/u, '').trim();
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Valore iniziale del campo "progresso arbitrario" all'apertura del modale.
|
|
59
|
+
*
|
|
60
|
+
* NON è semplicemente il testo corrente spogliato del glifo: se quel testo è
|
|
61
|
+
* esattamente ciò che il default già produrrebbe (`🔵 Todo`, `🟡 In Progress`)
|
|
62
|
+
* non c'è nulla di custom da preservare, e pre-riempirlo lo renderebbe
|
|
63
|
+
* APPICCICOSO — cambiando stato la scritta vecchia resterebbe attaccata al
|
|
64
|
+
* glifo nuovo (`🔵 Todo` → wip → `🟡 Todo`). Si parte quindi da vuoto e il
|
|
65
|
+
* default segue lo stato scelto.
|
|
66
|
+
*
|
|
67
|
+
* Un testo che il default NON sa riprodurre è invece informazione dell'utente e
|
|
68
|
+
* va tenuta: `🟡 85%` → `85%`, e `✔️ Done at 2026-07-14` → `Done at 2026-07-14`
|
|
69
|
+
* (la data storica sopravvive, non viene ri-stampata a oggi).
|
|
70
|
+
*/
|
|
71
|
+
export function initialDetail(current, prog, date = today()) {
|
|
72
|
+
const s = current.trim();
|
|
73
|
+
if (!s || s === progressText(prog, '', date))
|
|
74
|
+
return '';
|
|
75
|
+
return stripProgGlyph(s);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Riscrive le celle Pri (col 2) e Prog (col 4) della riga `| Tnn | … |` in
|
|
79
|
+
* tasks.md. Solo la PRIMA riga con quell'id — l'overview è unica, un secondo
|
|
80
|
+
* match sarebbe un duplicato da non propagare. Le altre celle (K, descrizione)
|
|
81
|
+
* restano i token grezzi originali: nessun re-flow della tabella, il diff resta
|
|
82
|
+
* di una riga. `ok:false` = id assente → il chiamante non scrive nulla.
|
|
83
|
+
*
|
|
84
|
+
* L'id deve rispettare `^T\d+$`, stesso gate di `parseTasks`: senza, un id
|
|
85
|
+
* arbitrario matcherebbe la riga di HEADER (`| ID | Pri | K | Prog |`) o quella
|
|
86
|
+
* di separatore, riscrivendone le celle e sfondando la tabella.
|
|
87
|
+
*/
|
|
88
|
+
const TASK_ID_RE = /^T\d+$/;
|
|
89
|
+
export function updateTasksMdRow(content, id, priGlyph, progGlyph) {
|
|
90
|
+
if (!TASK_ID_RE.test(id))
|
|
91
|
+
return { content, ok: false };
|
|
92
|
+
let ok = false;
|
|
93
|
+
const lines = content.split('\n').map((line) => {
|
|
94
|
+
if (ok)
|
|
95
|
+
return line;
|
|
96
|
+
if (!line.trim().startsWith('|'))
|
|
97
|
+
return line;
|
|
98
|
+
const cells = line.split('|');
|
|
99
|
+
if ((cells[1] ?? '').trim() !== id)
|
|
100
|
+
return line;
|
|
101
|
+
if (cells.length < 6)
|
|
102
|
+
return line; // riga malformata → non toccarla
|
|
103
|
+
ok = true;
|
|
104
|
+
cells[2] = ` ${priGlyph} `;
|
|
105
|
+
cells[4] = ` ${progGlyph} `;
|
|
106
|
+
return cells.join('|');
|
|
107
|
+
});
|
|
108
|
+
return { content: ok ? lines.join('\n') : content, ok };
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Riscrive i bullet header `- **Priority**:` e `- **Progress**:` del task file.
|
|
112
|
+
* First-match-wins per chiave, stessa regola di `parseTaskDetail`: se un campo
|
|
113
|
+
* ricompare nel body (residuo template) vince quello dell'header — così ciò che
|
|
114
|
+
* il deck mostra e ciò che scrive restano la stessa riga.
|
|
115
|
+
*/
|
|
116
|
+
export function updateTaskFileFields(content, priLabel, progress) {
|
|
117
|
+
let priDone = false;
|
|
118
|
+
let progDone = false;
|
|
119
|
+
const lines = content.split('\n').map((line) => {
|
|
120
|
+
if (!priDone && /^-\s*\*\*Priority\*\*:/.test(line)) {
|
|
121
|
+
priDone = true;
|
|
122
|
+
return `- **Priority**: ${priLabel}`;
|
|
123
|
+
}
|
|
124
|
+
if (!progDone && /^-\s*\*\*Progress\*\*:/.test(line)) {
|
|
125
|
+
progDone = true;
|
|
126
|
+
return `- **Progress**: ${progress}`;
|
|
127
|
+
}
|
|
128
|
+
return line;
|
|
129
|
+
});
|
|
130
|
+
const ok = priDone || progDone;
|
|
131
|
+
return { content: ok ? lines.join('\n') : content, ok };
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Scrive i DUE lati della task: la riga di tasks.md (vista d'insieme) e i campi
|
|
135
|
+
* del task file (dettaglio). Sono due fonti che devono restare allineate — il
|
|
136
|
+
* deck legge la prima e mostra la seconda — quindi si toccano insieme.
|
|
137
|
+
* Un lato mancante (id fuori overview, o nessun task file) non blocca l'altro:
|
|
138
|
+
* si scrive ciò che esiste e il risultato dice cosa è stato toccato.
|
|
139
|
+
*/
|
|
140
|
+
export function writeTaskEdit(input) {
|
|
141
|
+
const { tasksPath, tasksDir, id, pri, prog, detail } = input;
|
|
142
|
+
const progress = progressText(prog, detail);
|
|
143
|
+
const paths = [];
|
|
144
|
+
const row = updateTasksMdRow(readFileSync(tasksPath, 'utf8'), id, PRI_GLYPH[pri], PROG_GLYPH[prog]);
|
|
145
|
+
if (row.ok) {
|
|
146
|
+
writeFileSync(tasksPath, row.content, 'utf8');
|
|
147
|
+
paths.push(tasksPath);
|
|
148
|
+
}
|
|
149
|
+
let fileUpdated = false;
|
|
150
|
+
const taskFile = findTaskFile(tasksDir, id);
|
|
151
|
+
if (taskFile) {
|
|
152
|
+
const upd = updateTaskFileFields(readFileSync(taskFile, 'utf8'), PRI_LABEL[pri], progress);
|
|
153
|
+
if (upd.ok) {
|
|
154
|
+
writeFileSync(taskFile, upd.content, 'utf8');
|
|
155
|
+
paths.push(taskFile);
|
|
156
|
+
fileUpdated = true;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return { paths, rowUpdated: row.ok, fileUpdated, progress };
|
|
160
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lamemind/loom-deck",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Deck TUI Ink per-progetto della famiglia loom: legge tasks.md e spawna sessioni Claude Code bound via LOOM_TASK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"loom-deck": "
|
|
7
|
+
"loom-deck": "dist/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|