@lamemind/loom-deck 0.23.1 → 0.25.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 +162 -66
- package/dist/session-list.js +56 -16
- package/dist/viewport.js +4 -1
- package/dist/width.js +20 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -12,10 +12,10 @@ import { resolveTasksPath, resolveTasksDir, loadTasks, loadTaskDetail, } from '.
|
|
|
12
12
|
import { discoverProjectSessions } from './sessions.js';
|
|
13
13
|
import { buildRows, firstRowKey, moveRowSelection, rowIndexOfKey, searchSessions, selectedRow, DEFAULT_OPTIONS, MIN_QUERY, } from './search.js';
|
|
14
14
|
import { appendNote, appendPin, appendSessionRecord, appendTaskBinding, loadSessionIndex, } from './task-index.js';
|
|
15
|
-
import { assembleSessionList, firstSelectableId, moveSelection, neighborId, rowIndexOf, rowLabel, selectedSession, stripProjectCore, } from './session-list.js';
|
|
15
|
+
import { assembleSessionList, firstSelectableId, moveSelection, neighborId, rowIndexOf, rowLabel, selectedSession, sessionTitle, stripProjectCore, } from './session-list.js';
|
|
16
16
|
import { cellWidth, launchLegend, loadIdentity, loadLaunch } from './config.js';
|
|
17
17
|
import { assignListCapacity, isCompact, layoutBudget, readerCapacity, searchListCapacity, searchPreviewCapacity, windowRange, } from './viewport.js';
|
|
18
|
-
import { caretWindow, cut, sanitize, termWidth, wrapLines, wrapWithOffsets, } from './width.js';
|
|
18
|
+
import { caretWindow, cut, pad, sanitize, termWidth, wrapLines, wrapWithOffsets, } from './width.js';
|
|
19
19
|
import { applyView, cycleSort, describeSort, priName, progName, toggleHidden, PRI_ENTRIES, PROG_ENTRIES, } from './view.js';
|
|
20
20
|
import { initialDetail, progressText, writeTaskEdit, PRI_GLYPH, PRI_LABEL, PROG_GLYPH, } from './task-edit.js';
|
|
21
21
|
import { loadView, saveView, viewFilePath } from './view-store.js';
|
|
@@ -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) {
|
|
@@ -412,10 +430,12 @@ const WARN = sanitize('⚠');
|
|
|
412
430
|
const SESSION_SEP = '─'.repeat(16);
|
|
413
431
|
// Prefisso del sessionId mostrato in lista: stesso dato e stessa lunghezza del
|
|
414
432
|
// widget `⛓ <8 char>` della statusline, così le due superfici si confrontano a
|
|
415
|
-
// occhio.
|
|
416
|
-
// label deve scalare.
|
|
433
|
+
// occhio.
|
|
417
434
|
const SID_CHARS = 8;
|
|
418
|
-
|
|
435
|
+
/** T60 — segnaposto della colonna task su una riga senza binding. Una cella
|
|
436
|
+
* vuota di soli spazi lascerebbe un buco che si legge come "colonna finita",
|
|
437
|
+
* e la riga tornerebbe a sembrare disallineata pur non essendolo. */
|
|
438
|
+
const TASK_EMPTY = '·';
|
|
419
439
|
// Marker Done per il DISPLAY. `task.prog` resta il `✔️` letto da tasks.md —
|
|
420
440
|
// `isDone()` e le lookup di `view.ts` ci confrontano sopra, e `task-edit` lo
|
|
421
441
|
// riscrive sul file: è una chiave semantica, non testo. Qui `sanitize` lo
|
|
@@ -476,9 +496,12 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
476
496
|
const [focus, setFocus] = useState('tasks');
|
|
477
497
|
// T39 — selezione KEYED SU ID, non su indice. Con una vista trasformata
|
|
478
498
|
// (filtro/sort) l'indice non identifica più la stessa task: leggere l'array
|
|
479
|
-
// grezzo per posizione spawnerebbe la task sbagliata, in silenzio.
|
|
480
|
-
//
|
|
481
|
-
|
|
499
|
+
// grezzo per posizione spawnerebbe la task sbagliata, in silenzio.
|
|
500
|
+
// T59 — e le righe meta sono sentinelle, non `null`: gli stati sono tre.
|
|
501
|
+
// D4 — si apre su `≡ tutte`: la vista più ampia in cima, poi si scende verso
|
|
502
|
+
// i sottoinsiemi. La selezione non è persistita (a differenza di `view`, T39),
|
|
503
|
+
// quindi questo atterraggio vale a ogni avvio.
|
|
504
|
+
const [sel, setSel] = useState(ALL);
|
|
482
505
|
// T50 — selezione del pane sessioni KEYED SU sessionId (non indice): la lista
|
|
483
506
|
// a due gruppi + separatore è una vista trasformata, un indice grezzo punterebbe
|
|
484
507
|
// alla riga sbagliata dopo un pin o un cambio di contesto (stesso trap T39).
|
|
@@ -546,13 +569,14 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
546
569
|
// La vista è una trasformazione DERIVATA, applicata a valle del load: il
|
|
547
570
|
// polling di tasks.md continua a funzionare senza saperne nulla.
|
|
548
571
|
const { visible: viewTasks, hidden: hiddenTasks } = useMemo(() => applyView(tasks, view), [tasks, view]);
|
|
549
|
-
const isSpot =
|
|
572
|
+
const isSpot = sel === SPOT;
|
|
573
|
+
const isAll = sel === ALL;
|
|
550
574
|
const projectName = cwd.split('/').pop() || cwd;
|
|
551
575
|
// Unica fonte della selezione: si legge SEMPRE dalla vista, mai dall'array
|
|
552
576
|
// grezzo — è l'invariante che tiene allineati dettaglio mostrato e spawn.
|
|
553
|
-
const selTask =
|
|
577
|
+
const selTask = typeof sel === 'string' ? viewTasks.find((t) => t.id === sel) ?? null : null;
|
|
554
578
|
const selectedTaskId = selTask?.id ?? null;
|
|
555
|
-
const selIndex = selTask ? viewTasks.indexOf(selTask) +
|
|
579
|
+
const selIndex = selTask ? viewTasks.indexOf(selTask) + META_ROWS : isAll ? ROW_ALL : ROW_SPOT;
|
|
556
580
|
const detail = useTaskDetail(tasksDir, selectedTaskId ?? undefined);
|
|
557
581
|
// Conteggio figli per task + spot (badge nel Tasks pane).
|
|
558
582
|
const childCount = new Map();
|
|
@@ -564,20 +588,46 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
564
588
|
else
|
|
565
589
|
spotCount++;
|
|
566
590
|
}
|
|
567
|
-
// Figli della selezione:
|
|
568
|
-
//
|
|
591
|
+
// Figli della selezione: tutte le conversazioni del progetto (`≡ tutte`), le
|
|
592
|
+
// sessioni bound alla task selezionata, oppure (spot) quelle senza binding.
|
|
593
|
+
// sessions è già ts desc → l'ordine si eredita in tutti e tre i rami.
|
|
569
594
|
// Memoizzato così `sessionRows` resta stabile fra render che non cambiano gli
|
|
570
595
|
// input: l'effect di validità della selezione non rigira a vuoto.
|
|
571
|
-
const childSessions = useMemo(() =>
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
596
|
+
const childSessions = useMemo(() => isAll
|
|
597
|
+
? sessions
|
|
598
|
+
: sessions.filter((s) => {
|
|
599
|
+
const bound = bindings.get(s.sessionId);
|
|
600
|
+
return selectedTaskId ? bound === selectedTaskId : !bound;
|
|
601
|
+
}), [sessions, bindings, selectedTaskId, isAll]);
|
|
575
602
|
// T50 — lista a due gruppi: pinnate (sempre, in cima) + separatore +
|
|
576
603
|
// contestuali. Dedup, cap solo sulle contestuali, righe stale per le pinnate
|
|
577
604
|
// orfane. Core PURO in session-list.ts (testabile senza Ink).
|
|
578
|
-
const assembled = useMemo(() => assembleSessionList(childSessions, sessions, pinned, MAX_SESSIONS), [childSessions, sessions, pinned]);
|
|
605
|
+
const assembled = useMemo(() => assembleSessionList(childSessions, sessions, pinned, isAll ? MAX_SESSIONS_ALL : MAX_SESSIONS), [childSessions, sessions, pinned, isAll]);
|
|
579
606
|
const sessionRows = assembled.rows;
|
|
580
607
|
const selSessionObj = selectedSession(sessionRows, selSessionId);
|
|
608
|
+
// T60 — larghezze delle colonne fisse della lista sessioni, misurate sulla
|
|
609
|
+
// lista INTERA e non sulla finestra visibile: derivarle dalle sole righe a
|
|
610
|
+
// schermo le farebbe cambiare a ogni scroll, cioè l'opposto di una tabella.
|
|
611
|
+
// La colonna task esiste solo nella vista "tutte" (altrove il binding è lo
|
|
612
|
+
// stesso su ogni riga e sta già nell'header del pane), e `0` la spegne.
|
|
613
|
+
const sessionCols = useMemo(() => {
|
|
614
|
+
let task = 0;
|
|
615
|
+
let age = 2;
|
|
616
|
+
for (const r of sessionRows) {
|
|
617
|
+
if (r.kind === 'separator')
|
|
618
|
+
continue;
|
|
619
|
+
if (isAll) {
|
|
620
|
+
const b = bindings.get(r.sessionId);
|
|
621
|
+
if (b)
|
|
622
|
+
task = Math.max(task, termWidth(b));
|
|
623
|
+
}
|
|
624
|
+
if (r.session)
|
|
625
|
+
age = Math.max(age, termWidth(relTime(r.session.ts)));
|
|
626
|
+
}
|
|
627
|
+
// La cella vuota deve poter entrare nella colonna, o le righe spot
|
|
628
|
+
// perderebbero il segnaposto e con lui l'allineamento.
|
|
629
|
+
return { task: task > 0 ? Math.max(task, termWidth(TASK_EMPTY)) : 0, age };
|
|
630
|
+
}, [sessionRows, bindings, isAll]);
|
|
581
631
|
// T52 — ricerca EAGER: rigira a ogni carattere digitato, non su ⏎. È
|
|
582
632
|
// sostenibile perché i corpi sono già in RAM dentro la cache mtime-keyed
|
|
583
633
|
// dell'adapter (D5): misurato su questo progetto, 0,8 ms sui soli corpi IA e
|
|
@@ -627,10 +677,10 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
627
677
|
// dalla vista (filtro appena attivato, oppure sparita da tasks.md), si cade
|
|
628
678
|
// sulla prima visibile — fallback deterministico, mai una posizione a caso.
|
|
629
679
|
useEffect(() => {
|
|
630
|
-
if (
|
|
631
|
-
|
|
680
|
+
if (typeof sel === 'string' && !viewTasks.some((t) => t.id === sel)) {
|
|
681
|
+
setSel(viewTasks[0]?.id ?? ALL);
|
|
632
682
|
}
|
|
633
|
-
}, [viewTasks,
|
|
683
|
+
}, [viewTasks, sel]);
|
|
634
684
|
// T50 — la selezione (id) resta valida sotto la vista a due gruppi: se l'id
|
|
635
685
|
// non è più una riga selezionabile (cambio parent, lista mutata, pin rimosso,
|
|
636
686
|
// sessione sparita) cade sulla prima riga — fallback deterministico, mai una
|
|
@@ -712,8 +762,13 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
712
762
|
setNote(`${keyLabel} → spawn: seleziona una task (←→ per il pane)`);
|
|
713
763
|
return;
|
|
714
764
|
}
|
|
715
|
-
|
|
716
|
-
|
|
765
|
+
// T59 — la guardia è "non è una task", non "è spot": le righe meta sono due
|
|
766
|
+
// e nessuna delle due ha una task da aprire. Il messaggio dice quale delle
|
|
767
|
+
// due, perché il motivo è diverso (vista di sola lettura vs sessioni libere).
|
|
768
|
+
if (isAll || isSpot) {
|
|
769
|
+
setNote(isAll
|
|
770
|
+
? 'tutte: vista di sola lettura, nessuna task da spawnare'
|
|
771
|
+
: 'spot: sessioni libere, nessuna task da spawnare');
|
|
717
772
|
return;
|
|
718
773
|
}
|
|
719
774
|
if (!selTask)
|
|
@@ -889,12 +944,17 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
889
944
|
setViewBackup(null);
|
|
890
945
|
setMode('normal');
|
|
891
946
|
}
|
|
892
|
-
// Sposta la selezione di `delta` righe nella VISTA (0 =
|
|
893
|
-
// visibili) e la riconverte subito in id: l'indice
|
|
894
|
-
// cambio di filtro, l'id sì.
|
|
947
|
+
// Sposta la selezione di `delta` righe nella VISTA (0 = tutte, 1 = spot,
|
|
948
|
+
// 2..N+1 = task visibili) e la riconverte subito in sentinella o id: l'indice
|
|
949
|
+
// non sopravvive a un cambio di filtro, l'id sì.
|
|
895
950
|
function moveTaskSel(delta) {
|
|
896
|
-
const next = Math.max(0, Math.min(viewTasks.length, selIndex + delta));
|
|
897
|
-
|
|
951
|
+
const next = Math.max(0, Math.min(viewTasks.length + META_ROWS - 1, selIndex + delta));
|
|
952
|
+
if (next === ROW_ALL)
|
|
953
|
+
setSel(ALL);
|
|
954
|
+
else if (next === ROW_SPOT)
|
|
955
|
+
setSel(SPOT);
|
|
956
|
+
else
|
|
957
|
+
setSel(viewTasks[next - META_ROWS]?.id ?? SPOT);
|
|
898
958
|
}
|
|
899
959
|
// T52 — `⏎` contestuale al TIPO di riga: la lista ne mescola due e l'azione
|
|
900
960
|
// giusta dipende da quale è selezionata. Riga sessione → resume, identico al
|
|
@@ -1368,8 +1428,8 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1368
1428
|
setMode('create');
|
|
1369
1429
|
}
|
|
1370
1430
|
else if (input === 'E') {
|
|
1371
|
-
// L'edit ha senso solo su una task reale:
|
|
1372
|
-
if (
|
|
1431
|
+
// L'edit ha senso solo su una task reale: le righe meta non ne sono.
|
|
1432
|
+
if (!selTask)
|
|
1373
1433
|
setNote('E → nessuna task selezionata');
|
|
1374
1434
|
else
|
|
1375
1435
|
openEdit();
|
|
@@ -1509,8 +1569,8 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1509
1569
|
exit();
|
|
1510
1570
|
}
|
|
1511
1571
|
});
|
|
1512
|
-
const parentLabel = isSpot ? 'spot' : selectedTaskId ?? '—';
|
|
1513
|
-
const canSpawn = focus === 'tasks' &&
|
|
1572
|
+
const parentLabel = isAll ? 'tutte' : isSpot ? 'spot' : selectedTaskId ?? '—';
|
|
1573
|
+
const canSpawn = focus === 'tasks' && selTask !== null;
|
|
1514
1574
|
const canResume = focus === 'sessions' && selSessionObj !== null;
|
|
1515
1575
|
// T50 — il pin agisce su qualunque riga selezionata (anche stale, per
|
|
1516
1576
|
// spinnarla); basta il focus sul pane e una selezione.
|
|
@@ -1640,7 +1700,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1640
1700
|
// Finestre di rendering. Le liste "logiche" (viewTasks, sessionRows)
|
|
1641
1701
|
// restano intere: navigazione, selezione e spawn continuano a ragionare su
|
|
1642
1702
|
// quelle, la finestra è solo ciò che finisce a schermo.
|
|
1643
|
-
const taskWin = windowRange(viewTasks.length, selIndex -
|
|
1703
|
+
const taskWin = windowRange(viewTasks.length, selIndex - META_ROWS, budget.taskRows);
|
|
1644
1704
|
const windowTasks = viewTasks.slice(taskWin.start, taskWin.end);
|
|
1645
1705
|
const selRowIndex = rowIndexOf(sessionRows, selSessionId);
|
|
1646
1706
|
const sessionWin = windowRange(sessionRows.length, selRowIndex, budget.sessionRows);
|
|
@@ -1649,9 +1709,9 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1649
1709
|
// a una riga sola. Perdere il deck per un terminale basso è meglio che
|
|
1650
1710
|
// sporcare la cronologia del terminale a ogni poll.
|
|
1651
1711
|
if (budget.compact) {
|
|
1652
|
-
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 ??
|
|
1712
|
+
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"] })] }));
|
|
1653
1713
|
}
|
|
1654
|
-
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] }));
|
|
1714
|
+
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, taskW: sessionCols.task, ageW: sessionCols.age, 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] }));
|
|
1655
1715
|
}
|
|
1656
1716
|
const SORT_UI = { pri: 'pri', prog: 'stato', id: 'id' };
|
|
1657
1717
|
// Modali resi IN FLUSSO (come l'input box di create), non in overlay assoluto:
|
|
@@ -1889,17 +1949,19 @@ function AssignScreen({ sessionId, label, current, filter, rows, selected, match
|
|
|
1889
1949
|
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));
|
|
1890
1950
|
})] }), note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: sanitize(note) }) : null] }));
|
|
1891
1951
|
}
|
|
1892
|
-
function TasksPane({ tasks, filtered, total, hidden, view, selected, spotCount, childCount, focused, loadError, detail, windowStart, above, below, detailLines, columns, }) {
|
|
1893
|
-
const
|
|
1952
|
+
function TasksPane({ tasks, filtered, total, hidden, view, selected, spotCount, allCount, childCount, focused, loadError, detail, windowStart, above, below, detailLines, columns, }) {
|
|
1953
|
+
const allSelected = selected === ROW_ALL;
|
|
1954
|
+
const spotSelected = selected === ROW_SPOT;
|
|
1894
1955
|
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:", ' ', [
|
|
1895
1956
|
...PRI_ENTRIES.filter((e) => view.hiddenPri.includes(e.name)),
|
|
1896
1957
|
...PROG_ENTRIES.filter((e) => view.hiddenProg.includes(e.name)),
|
|
1897
1958
|
]
|
|
1898
1959
|
.map((e) => `−${sanitize(e.glyph)}`)
|
|
1899
|
-
.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) => {
|
|
1960
|
+
.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) => {
|
|
1900
1961
|
// windowStart riporta l'indice di finestra a quello della lista
|
|
1901
|
-
// completa, su cui è keyata la selezione. +
|
|
1902
|
-
|
|
1962
|
+
// completa, su cui è keyata la selezione. +META_ROWS: le prime due
|
|
1963
|
+
// righe sono le meta.
|
|
1964
|
+
const sel = windowStart + i + META_ROWS === selected;
|
|
1903
1965
|
const n = childCount.get(task.id) ?? 0;
|
|
1904
1966
|
// Invariante ③: la descrizione è l'unico pezzo a lunghezza libera, e
|
|
1905
1967
|
// si taglia QUI sul budget che resta dopo le colonne fisse. Lasciarlo
|
|
@@ -1913,8 +1975,12 @@ function TasksPane({ tasks, filtered, total, hidden, view, selected, spotCount,
|
|
|
1913
1975
|
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));
|
|
1914
1976
|
})), detail && detailLines > 0 ? (_jsx(DetailPane, { detail: detail, maxLines: detailLines, columns: columns })) : null] }));
|
|
1915
1977
|
}
|
|
1916
|
-
function SessionsPane({ parentLabel, isSpot, rows, total, pinnedCount, hidden, selectedId, focused, above, below, detail, firstLines, lastLines, columns, forkOf, sessionNotes, projectCore, }) {
|
|
1917
|
-
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:
|
|
1978
|
+
function SessionsPane({ parentLabel, isSpot, isAll, bindings, taskW, ageW, rows, total, pinnedCount, hidden, selectedId, focused, above, below, detail, firstLines, lastLines, columns, forkOf, sessionNotes, projectCore, }) {
|
|
1979
|
+
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
|
|
1980
|
+
? 'nessuna conversazione nel progetto'
|
|
1981
|
+
: isSpot
|
|
1982
|
+
? 'nessuna sessione libera'
|
|
1983
|
+
: 'nessuna sessione legata a questa task' })) : (rows.map((row, i) => {
|
|
1918
1984
|
// T50 — separatore leggero fra pinnate e contestuali: riga dim, non un
|
|
1919
1985
|
// box pesante (coerente con lo styling delle Done dimmate).
|
|
1920
1986
|
if (row.kind === 'separator') {
|
|
@@ -1924,32 +1990,62 @@ function SessionsPane({ parentLabel, isSpot, rows, total, pinnedCount, hidden, s
|
|
|
1924
1990
|
// T50 — pin stale: transcript sparito, nessuna Session da mostrare.
|
|
1925
1991
|
// Riga navigabile e spinnabile (`p`), marcata, mai un crash.
|
|
1926
1992
|
if (row.kind === 'pinned' && row.stale) {
|
|
1927
|
-
|
|
1993
|
+
// T60 — anche qui la nota si taglia sul budget DERIVATO, non su un
|
|
1994
|
+
// 30 inchiodato: su un pane stretto quel valore fisso mandava la
|
|
1995
|
+
// riga oltre il bordo, e a ripararla arrivava `cli-truncate` (che
|
|
1996
|
+
// sfora di una colonna per emoji e mangia il bordo stesso).
|
|
1997
|
+
const staleNote = sessionNotes.get(row.sessionId);
|
|
1998
|
+
const staleW = Math.max(0, paneTextWidth(columns) - (2 /* caret */ + termWidth(`${WARN} pin stale `) + SID_CHARS + 3 /* spazio + caporali */));
|
|
1999
|
+
return (_jsxs(Text, { inverse: sel && focused, bold: sel && !focused, dimColor: true, wrap: "truncate-end", children: [sel ? CARET : CARET_OFF, _jsx(Text, { color: "yellow", children: WARN }), " pin stale", ' ', _jsx(Text, { dimColor: true, children: row.sessionId.slice(0, SID_CHARS) }), staleNote ? _jsxs(Text, { color: "yellow", children: [" \u00AB", cut(staleNote, staleW), "\u00BB"] }) : null] }, row.sessionId));
|
|
1928
2000
|
}
|
|
1929
2001
|
const s = row.session; // non-stale → session presente
|
|
1930
2002
|
const isPinnedRow = row.kind === 'pinned';
|
|
1931
2003
|
// T28 — un ramo eredita il titolo dell'origine: senza marcatore le due
|
|
1932
|
-
// righe sarebbero identiche a occhio.
|
|
1933
|
-
// la troncatura non arriva mai.
|
|
2004
|
+
// righe sarebbero identiche a occhio.
|
|
1934
2005
|
const forked = forkOf.has(s.sessionId);
|
|
1935
|
-
//
|
|
1936
|
-
//
|
|
1937
|
-
//
|
|
1938
|
-
//
|
|
1939
|
-
//
|
|
1940
|
-
//
|
|
1941
|
-
//
|
|
1942
|
-
const
|
|
1943
|
-
const
|
|
2006
|
+
// T59 D2 — nella vista "tutte" il marker è PER-SESSIONE (binding letto
|
|
2007
|
+
// dal sidecar) e non deciso dal parent: la lista mescola scoped e spot,
|
|
2008
|
+
// quindi un marker uniforme mentirebbe su metà delle righe. E il solo
|
|
2009
|
+
// glifo direbbe *che* la conversazione è legata senza dire *a cosa* —
|
|
2010
|
+
// informazione monca proprio qui, l'unica vista dove l'appartenenza
|
|
2011
|
+
// non è scritta da nessun'altra parte dello schermo: da qui la colonna
|
|
2012
|
+
// task accanto, che esiste solo in questa vista.
|
|
2013
|
+
const bound = bindings.get(s.sessionId) ?? null;
|
|
2014
|
+
const linked = isAll ? Boolean(bound) : !isSpot;
|
|
2015
|
+
// T60 — colonne VERE: ogni cella fissa è larga esattamente quanto
|
|
2016
|
+
// dichiara, riempita di spazi con `pad` (che misura in colonne, non in
|
|
2017
|
+
// caratteri). Il marker va portato a 2 anche quando è `○`, largo 1:
|
|
2018
|
+
// era lui a far slittare a sinistra di una colonna tutta la riga di
|
|
2019
|
+
// ogni sessione spot.
|
|
2020
|
+
const age = relTime(s.ts);
|
|
2021
|
+
// Il taglio del titolo è ciò che RESTA, calcolato per sottrazione: le
|
|
2022
|
+
// colonne fisse sono note, quindi l'unica cella elastica prende il
|
|
2023
|
+
// resto. Pavimento `0` e non un minimo di cortesia — è un tetto, non
|
|
2024
|
+
// una preferenza: alzarlo sopra lo spazio reale fa uscire la riga dal
|
|
2025
|
+
// pane e le mangia il bordo (invariante ③).
|
|
2026
|
+
const titleW = Math.max(0, paneTextWidth(columns) -
|
|
1944
2027
|
(2 /* caret */ +
|
|
1945
|
-
2 /*
|
|
1946
|
-
1 /*
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
2028
|
+
2 /* marker */ +
|
|
2029
|
+
1 /* gutter */ +
|
|
2030
|
+
SID_CHARS +
|
|
2031
|
+
1 /* gutter */ +
|
|
2032
|
+
(taskW > 0 ? taskW + 1 : 0) +
|
|
2033
|
+
1 /* gutter prima della data */ +
|
|
2034
|
+
ageW));
|
|
2035
|
+
// T28 — `⑂` sta DENTRO la cella titolo, non in una colonna sua: una
|
|
2036
|
+
// colonna dedicata costerebbe 2 spazi vuoti su ogni riga non-fork, e
|
|
2037
|
+
// metterlo fuori cella sposterebbe il bordo del titolo solo sui rami —
|
|
2038
|
+
// cioè rimetterebbe lo slittamento che le colonne tolgono.
|
|
2039
|
+
const forkMark = forked ? '⑂ ' : '';
|
|
2040
|
+
const inner = Math.max(0, titleW - termWidth(forkMark));
|
|
2041
|
+
// T60 — il testo arriva già ripulito di ciò che le colonne accanto
|
|
2042
|
+
// dicono già (progetto e task id): senza, la cella conterrebbe
|
|
2043
|
+
// `🧵 loom-works · T59` accanto a una colonna che dice `T59`.
|
|
2044
|
+
const label = rowLabel(sessionTitle(s, projectCore, bound), sessionNotes.get(s.sessionId), inner);
|
|
2045
|
+
const used = (label.note ? termWidth(label.note) + 2 : 0) +
|
|
2046
|
+
(label.note && label.rest ? 1 : 0) +
|
|
2047
|
+
termWidth(label.rest);
|
|
2048
|
+
return (_jsxs(Text, { inverse: sel && focused, bold: sel && !focused, wrap: "truncate-end", children: [sel ? CARET : CARET_OFF, isPinnedRow ? (_jsx(Text, { color: "yellow", children: pad('📌', 2) })) : linked ? (_jsx(Text, { color: "green", children: pad('🔗', 2) })) : (_jsx(Text, { dimColor: true, children: pad('○', 2) })), ' ', _jsx(Text, { color: "cyan", children: s.sessionId.slice(0, SID_CHARS) }), ' ', taskW > 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: bound ? 'green' : undefined, dimColor: !bound, children: pad(bound ?? TASK_EMPTY, taskW) }), ' '] })) : null, forkMark ? _jsx(Text, { color: "magenta", children: forkMark }) : 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, ' '.repeat(Math.max(0, inner - used)), ' ', _jsx(Text, { dimColor: true, children: pad(age, ageW, 'right') })] }, s.sessionId));
|
|
1953
2049
|
})), detail ? (_jsx(SessionDetailPane, { s: detail, firstLines: firstLines, lastLines: lastLines, columns: columns, origin: forkOf.get(detail.sessionId) ?? null, note: sessionNotes.get(detail.sessionId) ?? '' })) : null] }));
|
|
1954
2050
|
}
|
|
1955
2051
|
// T49 — detail pane della sessione selezionata (hover), gemello del DetailPane
|
|
@@ -1964,7 +2060,7 @@ function SessionDetailPane({ s, firstLines, lastLines, columns, origin, note, })
|
|
|
1964
2060
|
const width = detailTextWidth(columns);
|
|
1965
2061
|
const first = s.customTitle && firstLines > 0 ? wrapLines(s.firstPrompt, width, firstLines) : [];
|
|
1966
2062
|
const last = s.lastReply && lastLines > 0 ? wrapLines(s.lastReply, width, lastLines) : [];
|
|
1967
|
-
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: [note ? _jsxs(Text, { color: "yellow", children: ["\u00AB", note, "\u00BB "] }) : null, _jsx(Text, { dimColor: Boolean(note), children: s.title })] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [fmtSize(s.sizeBytes), " \u00B7 ", s.turns, " turni \u00B7 ", fmtDateTime(s.ts), origin ? ` · ⑂ da ${origin.slice(0, 8)}` : ''] }), first.map((line, i) => (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [i === 0 ? '» ' : ' ', line] }, `f${i}`))), last.map((line, i) => (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [i === 0 ? '« ' : ' ', line] }, `l${i}`)))] }));
|
|
2063
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: [note ? _jsxs(Text, { color: "yellow", children: ["\u00AB", note, "\u00BB "] }) : null, _jsx(Text, { dimColor: Boolean(note), children: s.title })] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [fmtSize(s.sizeBytes), " \u00B7 ", s.turns, " turni \u00B7 ", fmtDateTime(s.ts), " \u00B7 ", s.gitBranch || '-', origin ? ` · ⑂ da ${origin.slice(0, 8)}` : ''] }), first.map((line, i) => (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [i === 0 ? '» ' : ' ', line] }, `f${i}`))), last.map((line, i) => (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [i === 0 ? '« ' : ' ', line] }, `l${i}`)))] }));
|
|
1968
2064
|
}
|
|
1969
2065
|
/**
|
|
1970
2066
|
* Righe non-wrappabili del dettaglio (titolo + meta + commit) e loro conteggio.
|
package/dist/session-list.js
CHANGED
|
@@ -38,6 +38,48 @@ export function stripProjectCore(title, core) {
|
|
|
38
38
|
}
|
|
39
39
|
return t.replace(/^[\s·]+/, '').trim();
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Toglie il token del task id dal titolo — quello e solo quello, confrontato
|
|
43
|
+
* per token intero e non per substring (`T5` non deve mordere dentro `T59`).
|
|
44
|
+
*
|
|
45
|
+
* Va in coppia con `stripProjectCore`: un titolo di tab è `<emoji> <progetto> ·
|
|
46
|
+
* <task>`, quindi tolto il progetto resta quasi sempre il solo task id.
|
|
47
|
+
*/
|
|
48
|
+
export function stripTaskId(title, taskId) {
|
|
49
|
+
if (!taskId)
|
|
50
|
+
return title;
|
|
51
|
+
return title
|
|
52
|
+
.split(/\s+/)
|
|
53
|
+
.filter((tok) => tok !== taskId)
|
|
54
|
+
.join(' ')
|
|
55
|
+
.replace(/^[\s·]+|[\s·]+$/g, '')
|
|
56
|
+
.trim();
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Il testo che DISTINGUE una conversazione dalle altre — tolto tutto ciò che è
|
|
60
|
+
* già scritto altrove nella stessa riga.
|
|
61
|
+
*
|
|
62
|
+
* Il titolo di una sessione è la label della tab Ptyxis quando esiste
|
|
63
|
+
* (`🧵 loom-works · T59`), e il primo prompt dell'utente quando non esiste. I
|
|
64
|
+
* due casi non vanno trattati allo stesso modo, ed è il punto di questa
|
|
65
|
+
* funzione:
|
|
66
|
+
*
|
|
67
|
+
* - **con titolo custom** → è composto di pezzi che la riga mostra già in
|
|
68
|
+
* colonna: il progetto (costante su ogni riga di un deck per-progetto) e il
|
|
69
|
+
* task id (colonna sua). Tolti quelli non resta quasi mai nulla → si cade sul
|
|
70
|
+
* primo prompt, che è l'unica cosa lì dentro a dire di cosa si parlava.
|
|
71
|
+
* - **senza titolo custom** → `title` È già il primo prompt: intatto.
|
|
72
|
+
*
|
|
73
|
+
* `title` (non `customTitle`) è la fonte anche nel primo ramo: è la stessa
|
|
74
|
+
* stringa già sanificata al confine dell'adapter, e usare il campo grezzo
|
|
75
|
+
* rimetterebbe in circolo glifi che il frame non sa disegnare.
|
|
76
|
+
*/
|
|
77
|
+
export function sessionTitle(s, core, taskId) {
|
|
78
|
+
if (!s.customTitle)
|
|
79
|
+
return s.title;
|
|
80
|
+
const rest = stripTaskId(stripProjectCore(s.title, core), taskId);
|
|
81
|
+
return rest || s.firstPrompt || '';
|
|
82
|
+
}
|
|
41
83
|
/** Colonne minime perché un residuo dopo la nota valga la riga: sotto questa
|
|
42
84
|
* soglia si vedrebbero due lettere e un'ellissi, cioè rumore. */
|
|
43
85
|
const MIN_REST = 6;
|
|
@@ -45,27 +87,25 @@ const MIN_REST = 6;
|
|
|
45
87
|
* è la parte scelta da un umano, non è lei a cedere il posto per prima. */
|
|
46
88
|
const MIN_NOTE = 14;
|
|
47
89
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* Due regimi, ed è una decisione deliberata che NON siano lo stesso:
|
|
90
|
+
* Come dividere `budget` colonne fra la nota umana e il testo della
|
|
91
|
+
* conversazione, quando ci sono entrambi.
|
|
51
92
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
93
|
+
* `text` arriva GIÀ ripulito da `sessionTitle`: qui non si decide più *cosa*
|
|
94
|
+
* mostrare, solo *quanto*. Prima lo strip del prefisso di progetto avveniva
|
|
95
|
+
* dentro questa funzione e solo sul ramo con nota — asimmetria nata quando il
|
|
96
|
+
* task id non aveva una colonna sua e togliere il core avrebbe lasciato il
|
|
97
|
+
* nulla. Con la riga incolonnata quella premessa è caduta, e tenere due regimi
|
|
98
|
+
* diversi avrebbe reso la colonna titolo larga in un caso e stretta nell'altro.
|
|
58
99
|
*
|
|
59
|
-
* Il budget
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* ridursi a un moncone.
|
|
100
|
+
* Il budget dà la precedenza alla nota, ma senza affamare il resto: con
|
|
101
|
+
* entrambi presenti la nota cede fino a `MIN_NOTE`, e il resto sparisce del
|
|
102
|
+
* tutto sotto `MIN_REST` invece di ridursi a un moncone.
|
|
63
103
|
*/
|
|
64
|
-
export function rowLabel(
|
|
104
|
+
export function rowLabel(text, note, budget) {
|
|
65
105
|
if (!note)
|
|
66
|
-
return { note: '', rest: cut(
|
|
106
|
+
return { note: '', rest: cut(text, Math.max(0, budget)) };
|
|
67
107
|
// 2 colonne per i caporali « », 1 per lo spazio prima del residuo.
|
|
68
|
-
const rest =
|
|
108
|
+
const rest = text;
|
|
69
109
|
const noteBudget = Math.max(0, budget - 2);
|
|
70
110
|
if (!rest)
|
|
71
111
|
return { note: cut(note, noteBudget), rest: '' };
|
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/dist/width.js
CHANGED
|
@@ -202,6 +202,26 @@ export function cut(s, cols) {
|
|
|
202
202
|
// informazione, tolgono una colonna di testo.
|
|
203
203
|
return out.trimEnd().replace(/…$/, '') + '…';
|
|
204
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Cella di larghezza ESATTA `cols`: taglia se eccede, riempie di spazi se manca.
|
|
207
|
+
*
|
|
208
|
+
* È il gemello di `cut` e serve a una cosa sola: fare colonne vere. Una lista
|
|
209
|
+
* incolonnata a mano allinea solo finché ogni cella misura davvero quanto
|
|
210
|
+
* dichiara — e la misura è in COLONNE del terminale (`termWidth`), non in
|
|
211
|
+
* caratteri: `'○'.padEnd(2)` e `'🔗'.padEnd(2)` danno due stringhe che
|
|
212
|
+
* `String.length` giura uguali e che il terminale disegna larghe 2 e 3. È
|
|
213
|
+
* esattamente lo slittamento di una colonna che rende ragged una lista.
|
|
214
|
+
*
|
|
215
|
+
* `align: 'right'` mette il riempimento davanti: serve ai campi ancorati al
|
|
216
|
+
* margine destro (la data), dove ad allinearsi sono le unità, non l'inizio.
|
|
217
|
+
*/
|
|
218
|
+
export function pad(s, cols, align = 'left') {
|
|
219
|
+
if (cols <= 0)
|
|
220
|
+
return '';
|
|
221
|
+
const t = termWidth(s) > cols ? cut(s, cols) : s;
|
|
222
|
+
const fill = ' '.repeat(Math.max(0, cols - termWidth(t)));
|
|
223
|
+
return align === 'right' ? fill + t : t + fill;
|
|
224
|
+
}
|
|
205
225
|
/**
|
|
206
226
|
* Larghezza in colonne di UN carattere **dopo** `sanitize`.
|
|
207
227
|
*
|
package/package.json
CHANGED