@lamemind/loom-deck 0.23.0 → 0.24.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 +111 -43
- package/dist/viewport.js +4 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -26,10 +26,28 @@ const POLL_MS = 1500;
|
|
|
26
26
|
// Cap del pane sessioni: le più recenti (ts desc), le altre restano nell'indice
|
|
27
27
|
// ma fuori vista. Non-silenzioso → l'header mostra quante sono nascoste.
|
|
28
28
|
const MAX_SESSIONS = 30;
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
29
|
+
// T59 D3 — cap dedicato alla vista "tutte": a 30 su un progetto con ~170
|
|
30
|
+
// conversazioni la lista sarebbe esaustiva solo sull'ultimo 17%, cioè
|
|
31
|
+
// contraddirebbe lo scopo della riga. 100 copre la finestra temporale utile e
|
|
32
|
+
// tiene comunque un tetto alle righe da attraversare con ↑↓.
|
|
33
|
+
const MAX_SESSIONS_ALL = 100;
|
|
34
|
+
// Modello task-centrico: il Tasks pane ha, oltre alle task reali, DUE righe
|
|
35
|
+
// meta in testa — `≡ tutte` (ogni conversazione del progetto) e `○ spot` (le
|
|
36
|
+
// sole NON legate ad alcuna task). La selezione nel Tasks pane è il "padre"; il
|
|
37
|
+
// Sessions pane mostra i suoi figli.
|
|
38
|
+
//
|
|
39
|
+
// T59 D1 — le sentinelle sono Symbol, non `null` né stringhe riservate: la
|
|
40
|
+
// selezione è un ENUM a tre casi (task / spot / tutte), non un flag. Un Symbol
|
|
41
|
+
// non può collidere con un task id e si confronta secco (`sel === ALL`); una
|
|
42
|
+
// stringa sentinella farebbe invece circolare un id-fantasma dentro un tipo che
|
|
43
|
+
// altrove significa "task id".
|
|
32
44
|
const SPOT = Symbol('spot');
|
|
45
|
+
const ALL = Symbol('all');
|
|
46
|
+
// Le righe meta occupano le prime posizioni della lista: l'indice di una task
|
|
47
|
+
// nella VISTA è quindi il suo indice in `viewTasks` + META_ROWS.
|
|
48
|
+
const ROW_ALL = 0;
|
|
49
|
+
const ROW_SPOT = 1;
|
|
50
|
+
const META_ROWS = 2;
|
|
33
51
|
const EDIT_ROWS = 4;
|
|
34
52
|
/** Le righe del modale edit che sono campi di TESTO (il resto è scelta ←→). */
|
|
35
53
|
function isTextRow(r) {
|
|
@@ -410,6 +428,12 @@ const WARN = sanitize('⚠');
|
|
|
410
428
|
// largo 1 sia per string-width sia per il terminale. Corto +
|
|
411
429
|
// wrap="truncate-end" così non va mai a capo nel pane al 50%.
|
|
412
430
|
const SESSION_SEP = '─'.repeat(16);
|
|
431
|
+
// Prefisso del sessionId mostrato in lista: stesso dato e stessa lunghezza del
|
|
432
|
+
// widget `⛓ <8 char>` della statusline, così le due superfici si confrontano a
|
|
433
|
+
// occhio. `SID_W` include lo spazio che segue → è quello che il budget della
|
|
434
|
+
// label deve scalare.
|
|
435
|
+
const SID_CHARS = 8;
|
|
436
|
+
const SID_W = SID_CHARS + 1;
|
|
413
437
|
// Marker Done per il DISPLAY. `task.prog` resta il `✔️` letto da tasks.md —
|
|
414
438
|
// `isDone()` e le lookup di `view.ts` ci confrontano sopra, e `task-edit` lo
|
|
415
439
|
// riscrive sul file: è una chiave semantica, non testo. Qui `sanitize` lo
|
|
@@ -470,9 +494,12 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
470
494
|
const [focus, setFocus] = useState('tasks');
|
|
471
495
|
// T39 — selezione KEYED SU ID, non su indice. Con una vista trasformata
|
|
472
496
|
// (filtro/sort) l'indice non identifica più la stessa task: leggere l'array
|
|
473
|
-
// grezzo per posizione spawnerebbe la task sbagliata, in silenzio.
|
|
474
|
-
//
|
|
475
|
-
|
|
497
|
+
// grezzo per posizione spawnerebbe la task sbagliata, in silenzio.
|
|
498
|
+
// T59 — e le righe meta sono sentinelle, non `null`: gli stati sono tre.
|
|
499
|
+
// D4 — si apre su `≡ tutte`: la vista più ampia in cima, poi si scende verso
|
|
500
|
+
// i sottoinsiemi. La selezione non è persistita (a differenza di `view`, T39),
|
|
501
|
+
// quindi questo atterraggio vale a ogni avvio.
|
|
502
|
+
const [sel, setSel] = useState(ALL);
|
|
476
503
|
// T50 — selezione del pane sessioni KEYED SU sessionId (non indice): la lista
|
|
477
504
|
// a due gruppi + separatore è una vista trasformata, un indice grezzo punterebbe
|
|
478
505
|
// alla riga sbagliata dopo un pin o un cambio di contesto (stesso trap T39).
|
|
@@ -540,13 +567,14 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
540
567
|
// La vista è una trasformazione DERIVATA, applicata a valle del load: il
|
|
541
568
|
// polling di tasks.md continua a funzionare senza saperne nulla.
|
|
542
569
|
const { visible: viewTasks, hidden: hiddenTasks } = useMemo(() => applyView(tasks, view), [tasks, view]);
|
|
543
|
-
const isSpot =
|
|
570
|
+
const isSpot = sel === SPOT;
|
|
571
|
+
const isAll = sel === ALL;
|
|
544
572
|
const projectName = cwd.split('/').pop() || cwd;
|
|
545
573
|
// Unica fonte della selezione: si legge SEMPRE dalla vista, mai dall'array
|
|
546
574
|
// grezzo — è l'invariante che tiene allineati dettaglio mostrato e spawn.
|
|
547
|
-
const selTask =
|
|
575
|
+
const selTask = typeof sel === 'string' ? viewTasks.find((t) => t.id === sel) ?? null : null;
|
|
548
576
|
const selectedTaskId = selTask?.id ?? null;
|
|
549
|
-
const selIndex = selTask ? viewTasks.indexOf(selTask) +
|
|
577
|
+
const selIndex = selTask ? viewTasks.indexOf(selTask) + META_ROWS : isAll ? ROW_ALL : ROW_SPOT;
|
|
550
578
|
const detail = useTaskDetail(tasksDir, selectedTaskId ?? undefined);
|
|
551
579
|
// Conteggio figli per task + spot (badge nel Tasks pane).
|
|
552
580
|
const childCount = new Map();
|
|
@@ -558,18 +586,21 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
558
586
|
else
|
|
559
587
|
spotCount++;
|
|
560
588
|
}
|
|
561
|
-
// Figli della selezione:
|
|
562
|
-
//
|
|
589
|
+
// Figli della selezione: tutte le conversazioni del progetto (`≡ tutte`), le
|
|
590
|
+
// sessioni bound alla task selezionata, oppure (spot) quelle senza binding.
|
|
591
|
+
// sessions è già ts desc → l'ordine si eredita in tutti e tre i rami.
|
|
563
592
|
// Memoizzato così `sessionRows` resta stabile fra render che non cambiano gli
|
|
564
593
|
// input: l'effect di validità della selezione non rigira a vuoto.
|
|
565
|
-
const childSessions = useMemo(() =>
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
594
|
+
const childSessions = useMemo(() => isAll
|
|
595
|
+
? sessions
|
|
596
|
+
: sessions.filter((s) => {
|
|
597
|
+
const bound = bindings.get(s.sessionId);
|
|
598
|
+
return selectedTaskId ? bound === selectedTaskId : !bound;
|
|
599
|
+
}), [sessions, bindings, selectedTaskId, isAll]);
|
|
569
600
|
// T50 — lista a due gruppi: pinnate (sempre, in cima) + separatore +
|
|
570
601
|
// contestuali. Dedup, cap solo sulle contestuali, righe stale per le pinnate
|
|
571
602
|
// orfane. Core PURO in session-list.ts (testabile senza Ink).
|
|
572
|
-
const assembled = useMemo(() => assembleSessionList(childSessions, sessions, pinned, MAX_SESSIONS), [childSessions, sessions, pinned]);
|
|
603
|
+
const assembled = useMemo(() => assembleSessionList(childSessions, sessions, pinned, isAll ? MAX_SESSIONS_ALL : MAX_SESSIONS), [childSessions, sessions, pinned, isAll]);
|
|
573
604
|
const sessionRows = assembled.rows;
|
|
574
605
|
const selSessionObj = selectedSession(sessionRows, selSessionId);
|
|
575
606
|
// T52 — ricerca EAGER: rigira a ogni carattere digitato, non su ⏎. È
|
|
@@ -621,10 +652,10 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
621
652
|
// dalla vista (filtro appena attivato, oppure sparita da tasks.md), si cade
|
|
622
653
|
// sulla prima visibile — fallback deterministico, mai una posizione a caso.
|
|
623
654
|
useEffect(() => {
|
|
624
|
-
if (
|
|
625
|
-
|
|
655
|
+
if (typeof sel === 'string' && !viewTasks.some((t) => t.id === sel)) {
|
|
656
|
+
setSel(viewTasks[0]?.id ?? ALL);
|
|
626
657
|
}
|
|
627
|
-
}, [viewTasks,
|
|
658
|
+
}, [viewTasks, sel]);
|
|
628
659
|
// T50 — la selezione (id) resta valida sotto la vista a due gruppi: se l'id
|
|
629
660
|
// non è più una riga selezionabile (cambio parent, lista mutata, pin rimosso,
|
|
630
661
|
// sessione sparita) cade sulla prima riga — fallback deterministico, mai una
|
|
@@ -706,8 +737,13 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
706
737
|
setNote(`${keyLabel} → spawn: seleziona una task (←→ per il pane)`);
|
|
707
738
|
return;
|
|
708
739
|
}
|
|
709
|
-
|
|
710
|
-
|
|
740
|
+
// T59 — la guardia è "non è una task", non "è spot": le righe meta sono due
|
|
741
|
+
// e nessuna delle due ha una task da aprire. Il messaggio dice quale delle
|
|
742
|
+
// due, perché il motivo è diverso (vista di sola lettura vs sessioni libere).
|
|
743
|
+
if (isAll || isSpot) {
|
|
744
|
+
setNote(isAll
|
|
745
|
+
? 'tutte: vista di sola lettura, nessuna task da spawnare'
|
|
746
|
+
: 'spot: sessioni libere, nessuna task da spawnare');
|
|
711
747
|
return;
|
|
712
748
|
}
|
|
713
749
|
if (!selTask)
|
|
@@ -883,12 +919,17 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
883
919
|
setViewBackup(null);
|
|
884
920
|
setMode('normal');
|
|
885
921
|
}
|
|
886
|
-
// Sposta la selezione di `delta` righe nella VISTA (0 =
|
|
887
|
-
// visibili) e la riconverte subito in id: l'indice
|
|
888
|
-
// cambio di filtro, l'id sì.
|
|
922
|
+
// Sposta la selezione di `delta` righe nella VISTA (0 = tutte, 1 = spot,
|
|
923
|
+
// 2..N+1 = task visibili) e la riconverte subito in sentinella o id: l'indice
|
|
924
|
+
// non sopravvive a un cambio di filtro, l'id sì.
|
|
889
925
|
function moveTaskSel(delta) {
|
|
890
|
-
const next = Math.max(0, Math.min(viewTasks.length, selIndex + delta));
|
|
891
|
-
|
|
926
|
+
const next = Math.max(0, Math.min(viewTasks.length + META_ROWS - 1, selIndex + delta));
|
|
927
|
+
if (next === ROW_ALL)
|
|
928
|
+
setSel(ALL);
|
|
929
|
+
else if (next === ROW_SPOT)
|
|
930
|
+
setSel(SPOT);
|
|
931
|
+
else
|
|
932
|
+
setSel(viewTasks[next - META_ROWS]?.id ?? SPOT);
|
|
892
933
|
}
|
|
893
934
|
// T52 — `⏎` contestuale al TIPO di riga: la lista ne mescola due e l'azione
|
|
894
935
|
// giusta dipende da quale è selezionata. Riga sessione → resume, identico al
|
|
@@ -1362,8 +1403,8 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1362
1403
|
setMode('create');
|
|
1363
1404
|
}
|
|
1364
1405
|
else if (input === 'E') {
|
|
1365
|
-
// L'edit ha senso solo su una task reale:
|
|
1366
|
-
if (
|
|
1406
|
+
// L'edit ha senso solo su una task reale: le righe meta non ne sono.
|
|
1407
|
+
if (!selTask)
|
|
1367
1408
|
setNote('E → nessuna task selezionata');
|
|
1368
1409
|
else
|
|
1369
1410
|
openEdit();
|
|
@@ -1503,8 +1544,8 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1503
1544
|
exit();
|
|
1504
1545
|
}
|
|
1505
1546
|
});
|
|
1506
|
-
const parentLabel = isSpot ? 'spot' : selectedTaskId ?? '—';
|
|
1507
|
-
const canSpawn = focus === 'tasks' &&
|
|
1547
|
+
const parentLabel = isAll ? 'tutte' : isSpot ? 'spot' : selectedTaskId ?? '—';
|
|
1548
|
+
const canSpawn = focus === 'tasks' && selTask !== null;
|
|
1508
1549
|
const canResume = focus === 'sessions' && selSessionObj !== null;
|
|
1509
1550
|
// T50 — il pin agisce su qualunque riga selezionata (anche stale, per
|
|
1510
1551
|
// spinnarla); basta il focus sul pane e una selezione.
|
|
@@ -1634,7 +1675,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1634
1675
|
// Finestre di rendering. Le liste "logiche" (viewTasks, sessionRows)
|
|
1635
1676
|
// restano intere: navigazione, selezione e spawn continuano a ragionare su
|
|
1636
1677
|
// quelle, la finestra è solo ciò che finisce a schermo.
|
|
1637
|
-
const taskWin = windowRange(viewTasks.length, selIndex -
|
|
1678
|
+
const taskWin = windowRange(viewTasks.length, selIndex - META_ROWS, budget.taskRows);
|
|
1638
1679
|
const windowTasks = viewTasks.slice(taskWin.start, taskWin.end);
|
|
1639
1680
|
const selRowIndex = rowIndexOf(sessionRows, selSessionId);
|
|
1640
1681
|
const sessionWin = windowRange(sessionRows.length, selRowIndex, budget.sessionRows);
|
|
@@ -1643,9 +1684,9 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1643
1684
|
// a una riga sola. Perdere il deck per un terminale basso è meglio che
|
|
1644
1685
|
// sporcare la cronologia del terminale a ogni poll.
|
|
1645
1686
|
if (budget.compact) {
|
|
1646
|
-
return (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), _jsxs(Text, { dimColor: true, children: [' ', "\u00B7 ", viewTasks.length, " task \u00B7 sel ", selectedTaskId ??
|
|
1687
|
+
return (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), _jsxs(Text, { dimColor: true, children: [' ', "\u00B7 ", viewTasks.length, " task \u00B7 sel ", selectedTaskId ?? parentLabel, " \u00B7 terminale ", rows, "\u00D7", columns, ": troppo basso, allarga"] })] }));
|
|
1647
1688
|
}
|
|
1648
|
-
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, wrap: "truncate-end", 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, wrap: "truncate-end", 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, wrap: "truncate-end", 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 === 'note' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["nota conversazione \u00B7 ", _jsx(Text, { color: "yellow", children: "^U" }), " svuota \u00B7", ' ', _jsx(Text, { color: "yellow", children: "\u23CE" }), " salva (vuoto = rimuove) \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'edit' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["edit \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2191\u2193" }), " campo \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " valore, o cursore sul testo \u00B7 ", _jsx(Text, { color: "yellow", children: "^A/^E" }), " inizio/fine \u00B7", ' ', _jsx(Text, { color: "yellow", children: "^D" }), " canc \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " salva+commit \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : (_jsx(Text, { dimColor: true, wrap: "truncate-end", children: keyLegend })), mode === 'normal' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [surfaceLegend, legend.shown ? ` · ${legend.shown}` : '', legend.overflow > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 +", legend.overflow, " fuori riga"] })) : null, legend.unreachable > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 ", legend.unreachable, " oltre la 9\u00AA (non raggiungibili)"] })) : null] })) : null, 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 === 'note' ? (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: "yellow", children: "\u270E \u203A " }), _jsx(Text, { children: noteDraft }), _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, columns: columns })) : null, _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(TasksPane, { tasks: windowTasks, filtered: viewTasks.length, total: tasks.length, hidden: hiddenTasks, view: view, selected: selIndex, spotCount: spotCount, childCount: childCount, focused: focus === 'tasks', loadError: loadError, detail: detail, windowStart: taskWin.start, above: taskWin.start, below: viewTasks.length - taskWin.end, detailLines: budget.detailLines, columns: columns }), _jsx(SessionsPane, { parentLabel: parentLabel, isSpot: isSpot, rows: windowRows, total: assembled.pinnedCount + assembled.contextTotal, pinnedCount: assembled.pinnedCount, hidden: assembled.contextHidden, selectedId: selSessionId ?? undefined, focused: focus === 'sessions', above: sessionWin.start, below: sessionRows.length - sessionWin.end, detail: budget.sessionDetail ? selSessionObj : null, firstLines: budget.sessionFirstLines, lastLines: budget.sessionLastLines, columns: columns, forkOf: forkOf, sessionNotes: sessionNotes, projectCore: projectCore })] }), note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: sanitize(note) }) : null] }));
|
|
1689
|
+
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, wrap: "truncate-end", 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, wrap: "truncate-end", 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, wrap: "truncate-end", 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 === 'note' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["nota conversazione \u00B7 ", _jsx(Text, { color: "yellow", children: "^U" }), " svuota \u00B7", ' ', _jsx(Text, { color: "yellow", children: "\u23CE" }), " salva (vuoto = rimuove) \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'edit' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["edit \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2191\u2193" }), " campo \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " valore, o cursore sul testo \u00B7 ", _jsx(Text, { color: "yellow", children: "^A/^E" }), " inizio/fine \u00B7", ' ', _jsx(Text, { color: "yellow", children: "^D" }), " canc \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " salva+commit \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : (_jsx(Text, { dimColor: true, wrap: "truncate-end", children: keyLegend })), mode === 'normal' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [surfaceLegend, legend.shown ? ` · ${legend.shown}` : '', legend.overflow > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 +", legend.overflow, " fuori riga"] })) : null, legend.unreachable > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 ", legend.unreachable, " oltre la 9\u00AA (non raggiungibili)"] })) : null] })) : null, 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 === 'note' ? (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: "yellow", children: "\u270E \u203A " }), _jsx(Text, { children: noteDraft }), _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, columns: columns })) : null, _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(TasksPane, { tasks: windowTasks, filtered: viewTasks.length, total: tasks.length, hidden: hiddenTasks, view: view, selected: selIndex, spotCount: spotCount, allCount: sessions.length, childCount: childCount, focused: focus === 'tasks', loadError: loadError, detail: detail, windowStart: taskWin.start, above: taskWin.start, below: viewTasks.length - taskWin.end, detailLines: budget.detailLines, columns: columns }), _jsx(SessionsPane, { parentLabel: parentLabel, isSpot: isSpot, isAll: isAll, bindings: bindings, rows: windowRows, total: assembled.pinnedCount + assembled.contextTotal, pinnedCount: assembled.pinnedCount, hidden: assembled.contextHidden, selectedId: selSessionId ?? undefined, focused: focus === 'sessions', above: sessionWin.start, below: sessionRows.length - sessionWin.end, detail: budget.sessionDetail ? selSessionObj : null, firstLines: budget.sessionFirstLines, lastLines: budget.sessionLastLines, columns: columns, forkOf: forkOf, sessionNotes: sessionNotes, projectCore: projectCore })] }), note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: sanitize(note) }) : null] }));
|
|
1649
1690
|
}
|
|
1650
1691
|
const SORT_UI = { pri: 'pri', prog: 'stato', id: 'id' };
|
|
1651
1692
|
// Modali resi IN FLUSSO (come l'input box di create), non in overlay assoluto:
|
|
@@ -1883,17 +1924,19 @@ function AssignScreen({ sessionId, label, current, filter, rows, selected, match
|
|
|
1883
1924
|
return (_jsxs(Text, { inverse: sel, dimColor: !sel && isDone(task.prog), wrap: "truncate-end", children: [sel ? CARET : CARET_OFF, task.id, " ", sanitize(task.pri), " ", displayProg(task.prog), " ", desc, tail] }, task.id));
|
|
1884
1925
|
})] }), note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: sanitize(note) }) : null] }));
|
|
1885
1926
|
}
|
|
1886
|
-
function TasksPane({ tasks, filtered, total, hidden, view, selected, spotCount, childCount, focused, loadError, detail, windowStart, above, below, detailLines, columns, }) {
|
|
1887
|
-
const
|
|
1927
|
+
function TasksPane({ tasks, filtered, total, hidden, view, selected, spotCount, allCount, childCount, focused, loadError, detail, windowStart, above, below, detailLines, columns, }) {
|
|
1928
|
+
const allSelected = selected === ROW_ALL;
|
|
1929
|
+
const spotSelected = selected === ROW_SPOT;
|
|
1888
1930
|
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, wrap: "truncate-end", children: ["Tasks (", hidden > 0 ? `${filtered}/${total}` : filtered, ")", hidden > 0 ? _jsxs(Text, { color: "yellow", children: [" \u00B7 ", hidden, " nascoste"] }) : null, above > 0 ? _jsxs(Text, { dimColor: true, children: [" \u00B7 \u2191", above] }) : null, below > 0 ? _jsxs(Text, { dimColor: true, children: [" \u00B7 \u2193", below] }) : 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:", ' ', [
|
|
1889
1931
|
...PRI_ENTRIES.filter((e) => view.hiddenPri.includes(e.name)),
|
|
1890
1932
|
...PROG_ENTRIES.filter((e) => view.hiddenProg.includes(e.name)),
|
|
1891
1933
|
]
|
|
1892
1934
|
.map((e) => `−${sanitize(e.glyph)}`)
|
|
1893
|
-
.join(' ')] })) : null] }), _jsxs(Text, { inverse: spotSelected && focused, bold: spotSelected && !focused, wrap: "truncate-end", children: [spotSelected ? CARET : CARET_OFF, "\u25CB spot sessioni libere", spotCount > 0 ? ` (${spotCount})` : ''] }), loadError ? (_jsx(Text, { color: "red", wrap: "truncate-end", children: loadError })) : (tasks.map((task, i) => {
|
|
1935
|
+
.join(' ')] })) : null] }), _jsxs(Text, { inverse: allSelected && focused, bold: allSelected && !focused, wrap: "truncate-end", children: [allSelected ? CARET : CARET_OFF, "\u2261 tutte le sessioni", allCount > 0 ? ` (${allCount})` : ''] }), _jsxs(Text, { inverse: spotSelected && focused, bold: spotSelected && !focused, wrap: "truncate-end", children: [spotSelected ? CARET : CARET_OFF, "\u25CB spot sessioni libere", spotCount > 0 ? ` (${spotCount})` : ''] }), loadError ? (_jsx(Text, { color: "red", wrap: "truncate-end", children: loadError })) : (tasks.map((task, i) => {
|
|
1894
1936
|
// windowStart riporta l'indice di finestra a quello della lista
|
|
1895
|
-
// completa, su cui è keyata la selezione. +
|
|
1896
|
-
|
|
1937
|
+
// completa, su cui è keyata la selezione. +META_ROWS: le prime due
|
|
1938
|
+
// righe sono le meta.
|
|
1939
|
+
const sel = windowStart + i + META_ROWS === selected;
|
|
1897
1940
|
const n = childCount.get(task.id) ?? 0;
|
|
1898
1941
|
// Invariante ③: la descrizione è l'unico pezzo a lunghezza libera, e
|
|
1899
1942
|
// si taglia QUI sul budget che resta dopo le colonne fisse. Lasciarlo
|
|
@@ -1907,8 +1950,12 @@ function TasksPane({ tasks, filtered, total, hidden, view, selected, spotCount,
|
|
|
1907
1950
|
return (_jsxs(Text, { inverse: sel && focused, bold: sel && !focused, dimColor: !sel && isDone(task.prog), wrap: "truncate-end", children: [sel ? CARET : CARET_OFF, task.id, " ", sanitize(task.pri), " ", displayProg(task.prog), " ", desc, tail] }, task.id));
|
|
1908
1951
|
})), detail && detailLines > 0 ? (_jsx(DetailPane, { detail: detail, maxLines: detailLines, columns: columns })) : null] }));
|
|
1909
1952
|
}
|
|
1910
|
-
function SessionsPane({ parentLabel, isSpot, rows, total, pinnedCount, hidden, selectedId, focused, above, below, detail, firstLines, lastLines, columns, forkOf, sessionNotes, projectCore, }) {
|
|
1911
|
-
return (_jsxs(Box, { flexDirection: "column", width: "50%", borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', paddingX: 1, children: [_jsxs(Text, { bold: true, color: focused ? 'cyan' : undefined, wrap: "truncate-end", children: ["Sessions \u00B7 ", parentLabel, " (", total, ")", pinnedCount > 0 ? _jsxs(Text, { color: "yellow", children: [" \u00B7 \uD83D\uDCCC", pinnedCount] }) : null, hidden > 0 ? _jsxs(Text, { dimColor: true, children: [" \u00B7 +", hidden, " pi\u00F9 vecchie"] }) : null, above > 0 ? _jsxs(Text, { dimColor: true, children: [" \u00B7 \u2191", above] }) : null, below > 0 ? _jsxs(Text, { dimColor: true, children: [" \u00B7 \u2193", below] }) : null] }), total === 0 ? (_jsx(Text, { color: "yellow", wrap: "truncate-end", children:
|
|
1953
|
+
function SessionsPane({ parentLabel, isSpot, isAll, bindings, rows, total, pinnedCount, hidden, selectedId, focused, above, below, detail, firstLines, lastLines, columns, forkOf, sessionNotes, projectCore, }) {
|
|
1954
|
+
return (_jsxs(Box, { flexDirection: "column", width: "50%", borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', paddingX: 1, children: [_jsxs(Text, { bold: true, color: focused ? 'cyan' : undefined, wrap: "truncate-end", children: ["Sessions \u00B7 ", parentLabel, " (", total, ")", pinnedCount > 0 ? _jsxs(Text, { color: "yellow", children: [" \u00B7 \uD83D\uDCCC", pinnedCount] }) : null, hidden > 0 ? _jsxs(Text, { dimColor: true, children: [" \u00B7 +", hidden, " pi\u00F9 vecchie"] }) : null, above > 0 ? _jsxs(Text, { dimColor: true, children: [" \u00B7 \u2191", above] }) : null, below > 0 ? _jsxs(Text, { dimColor: true, children: [" \u00B7 \u2193", below] }) : null] }), total === 0 ? (_jsx(Text, { color: "yellow", wrap: "truncate-end", children: isAll
|
|
1955
|
+
? 'nessuna conversazione nel progetto'
|
|
1956
|
+
: isSpot
|
|
1957
|
+
? 'nessuna sessione libera'
|
|
1958
|
+
: 'nessuna sessione legata a questa task' })) : (rows.map((row, i) => {
|
|
1912
1959
|
// T50 — separatore leggero fra pinnate e contestuali: riga dim, non un
|
|
1913
1960
|
// box pesante (coerente con lo styling delle Done dimmate).
|
|
1914
1961
|
if (row.kind === 'separator') {
|
|
@@ -1933,13 +1980,34 @@ function SessionsPane({ parentLabel, isSpot, rows, total, pinnedCount, hidden, s
|
|
|
1933
1980
|
// Con un valore fisso (erano 44) su un terminale stretto la riga
|
|
1934
1981
|
// superava il pane, e a troncarla finiva `cli-truncate` — che sfora
|
|
1935
1982
|
// di una colonna per emoji e si mangia il bordo destro.
|
|
1983
|
+
// T59 D2 — nella vista "tutte" il marker è PER-SESSIONE (binding letto
|
|
1984
|
+
// dal sidecar) e non deciso dal parent: la lista mescola scoped e spot,
|
|
1985
|
+
// quindi un marker uniforme mentirebbe su metà delle righe. E il solo
|
|
1986
|
+
// glifo direbbe *che* la conversazione è legata senza dire *a cosa* —
|
|
1987
|
+
// informazione monca proprio qui, l'unica vista dove l'appartenenza
|
|
1988
|
+
// non è scritta da nessun'altra parte dello schermo. Da qui il task id
|
|
1989
|
+
// inline, che costa colonne al titolo solo in questa vista.
|
|
1990
|
+
const bound = bindings.get(s.sessionId) ?? null;
|
|
1991
|
+
const idTag = isAll && bound ? `${bound} ` : '';
|
|
1992
|
+
const linked = isAll ? Boolean(bound) : !isSpot;
|
|
1936
1993
|
const meta = ` · ${s.gitBranch || '-'} · ${relTime(s.ts)}`;
|
|
1937
|
-
|
|
1938
|
-
|
|
1994
|
+
// Il pavimento è `0`, non un minimo di cortesia: questo numero è un
|
|
1995
|
+
// TETTO (le colonne che restano), non una preferenza. Un `Math.max(12,
|
|
1996
|
+
// …)` lo alza sopra lo spazio reale appena il resto della riga cresce
|
|
1997
|
+
// — e la riga esce dal pane mangiandosi il bordo, cioè il difetto che
|
|
1998
|
+
// l'invariante ③ esiste per impedire. Con poco spazio è il titolo a
|
|
1999
|
+
// sparire: hash, task id, branch e data restano, e il frame regge.
|
|
2000
|
+
const labelBudget = Math.max(0, paneTextWidth(columns) -
|
|
2001
|
+
(2 /* caret */ +
|
|
2002
|
+
2 /* icona */ +
|
|
2003
|
+
1 /* spazio */ +
|
|
2004
|
+
SID_W /* hash + spazio */ +
|
|
2005
|
+
termWidth(idTag) +
|
|
2006
|
+
(forked ? 2 : 0)) -
|
|
1939
2007
|
termWidth(meta) -
|
|
1940
2008
|
1 /* spazio prima del meta */);
|
|
1941
2009
|
const label = rowLabel(s.title, sessionNotes.get(s.sessionId), projectCore, labelBudget);
|
|
1942
|
-
return (_jsxs(Text, { inverse: sel && focused, bold: sel && !focused, wrap: "truncate-end", children: [sel ? CARET : CARET_OFF, isPinnedRow ? (_jsx(Text, { color: "yellow", children: "\uD83D\uDCCC" })) :
|
|
2010
|
+
return (_jsxs(Text, { inverse: sel && focused, bold: sel && !focused, wrap: "truncate-end", children: [sel ? CARET : CARET_OFF, isPinnedRow ? (_jsx(Text, { color: "yellow", children: "\uD83D\uDCCC" })) : linked ? (_jsx(Text, { color: "green", children: "\uD83D\uDD17" })) : (_jsx(Text, { dimColor: true, children: "\u25CB" })), ' ', _jsx(Text, { color: "cyan", children: s.sessionId.slice(0, SID_CHARS) }), ' ', idTag ? _jsx(Text, { color: "green", children: idTag }) : null, forked ? _jsx(Text, { color: "magenta", children: "\u2442 " }) : null, label.note ? (_jsxs(Text, { color: "yellow", bold: true, children: ["\u00AB", label.note, "\u00BB"] })) : null, label.note && label.rest ? ' ' : null, label.rest ? _jsx(Text, { dimColor: Boolean(label.note), children: label.rest }) : null, ' ', _jsxs(Text, { dimColor: true, children: ["\u00B7 ", s.gitBranch || '-', " \u00B7 ", relTime(s.ts)] })] }, s.sessionId));
|
|
1943
2011
|
})), detail ? (_jsx(SessionDetailPane, { s: detail, firstLines: firstLines, lastLines: lastLines, columns: columns, origin: forkOf.get(detail.sessionId) ?? null, note: sessionNotes.get(detail.sessionId) ?? '' })) : null] }));
|
|
1944
2012
|
}
|
|
1945
2013
|
// T49 — detail pane della sessione selezionata (hover), gemello del DetailPane
|
package/dist/viewport.js
CHANGED
|
@@ -22,7 +22,10 @@ const MIN_TASK_ROWS = 3;
|
|
|
22
22
|
/** Il dettaglio è secondario: non si prende mai più di così, anche con spazio. */
|
|
23
23
|
const MAX_DETAIL_LINES = 4;
|
|
24
24
|
/** Righe di "cornice" fisse dei tre contenitori a lunghezza variabile. */
|
|
25
|
-
|
|
25
|
+
// T59 — le righe meta sono DUE (`≡ tutte`, `○ spot`): ogni riga fissa aggiunta
|
|
26
|
+
// a un pane va scalata qui, o il frame sfonda `rows` e Ink passa a
|
|
27
|
+
// clearTerminal (frame-fantasma nello scrollback di VTE).
|
|
28
|
+
const TASKS_PANE_CHROME = 6; // 2 bordi + header "Tasks (n)" + riga sort + 2 righe meta
|
|
26
29
|
const SESSIONS_PANE_CHROME = 3; // 2 bordi + header "Sessions · …"
|
|
27
30
|
const DETAIL_CHROME = 3; // marginTop + 2 bordi
|
|
28
31
|
/** Detail pane sessione (T49): righe fisse = titolo + riga meta (size · turni ·
|
package/package.json
CHANGED