@lamemind/loom-deck 0.31.0 → 0.33.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/README.md +77 -160
- package/dist/archivable.js +13 -7
- package/dist/cli.js +244 -74
- package/dist/markdown.js +260 -0
- package/dist/pane-views.js +135 -0
- package/dist/session-list.js +5 -2
- package/dist/width.js +55 -4
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -15,10 +15,12 @@ import { buildRows, firstRowKey, moveRowSelection, rowIndexOfKey, searchSessions
|
|
|
15
15
|
import { appendNote, appendPin, appendSessionRecord, appendTaskBinding, loadSessionIndex, } from './task-index.js';
|
|
16
16
|
import { assembleSessionList, firstSelectableId, moveSelection, neighborId, rowIndexOf, rowLabel, selectedSession, sessionTitle, stripProjectCore, unpinLandingId, } from './session-list.js';
|
|
17
17
|
import { cellWidth, launchLegend, loadArchivableDays, loadIdentity, loadLaunch, } from './config.js';
|
|
18
|
-
import {
|
|
18
|
+
import { archivableIds, SCAN_INTERVAL_MS } from './archivable.js';
|
|
19
|
+
import { cycleSessionView, cycleTaskView, selectSessionRows, selectTasks, sessionView, taskView, SESSION_VIEWS, TASK_VIEWS, } from './pane-views.js';
|
|
19
20
|
import { assignListCapacity, detailCapacity, isCompact, layoutBudget, readerCapacity, searchListCapacity, searchPreviewCapacity, windowRange, } from './viewport.js';
|
|
20
21
|
import { caretWindow, cut, cutParts, pad, sanitize, termWidth, wrapLines, wrapWithOffsets, } from './width.js';
|
|
21
22
|
import { scanText, sliceLine, topForOffset, } from './text-search.js';
|
|
23
|
+
import { parseMarkdown, sliceSpans, } from './markdown.js';
|
|
22
24
|
import { applyView, cycleSort, describeSort, priName, progName, toggleHidden, PRI_ENTRIES, PROG_ENTRIES, } from './view.js';
|
|
23
25
|
import { initialDetail, progressText, writeTaskEdit, PRI_GLYPH, PRI_LABEL, PROG_GLYPH, } from './task-edit.js';
|
|
24
26
|
import { loadView, saveView, viewFilePath } from './view-store.js';
|
|
@@ -469,22 +471,26 @@ function useSessions(projectRoot) {
|
|
|
469
471
|
// `doneSig` è una stringa, non l'array: `tasks` cambia identità a ogni re-read
|
|
470
472
|
// di tasks.md, e usarlo come dipendenza rimetterebbe lo scan sul tick da 1,5s
|
|
471
473
|
// per la via di dietro.
|
|
474
|
+
//
|
|
475
|
+
// T100 — tiene gli ID e non più il conteggio: `archiviabili` è una vista del
|
|
476
|
+
// pane, quindi servono le righe. Il contatore dell'header è `.size` dello stesso
|
|
477
|
+
// insieme che disegna la lista — un numero e una lista che non possono divergere.
|
|
472
478
|
function useArchivable(doneSig, tasksDir, projectRoot, days) {
|
|
473
|
-
const [
|
|
479
|
+
const [ids, setIds] = useState(() => new Set());
|
|
474
480
|
useEffect(() => {
|
|
475
481
|
let alive = true;
|
|
476
|
-
const
|
|
482
|
+
const done = doneSig ? doneSig.split(',') : [];
|
|
477
483
|
const scan = () => {
|
|
478
|
-
|
|
479
|
-
.then((
|
|
484
|
+
archivableIds(done, { tasksDir, projectRoot, days })
|
|
485
|
+
.then((found) => {
|
|
480
486
|
if (alive)
|
|
481
|
-
|
|
487
|
+
setIds(new Set(found));
|
|
482
488
|
})
|
|
483
|
-
// Scan fallito (task file illeggibili, git muto) →
|
|
484
|
-
//
|
|
489
|
+
// Scan fallito (task file illeggibili, git muto) → insieme vuoto, cioè
|
|
490
|
+
// voce a 0. Un contatore informativo non merita un errore a schermo.
|
|
485
491
|
.catch(() => {
|
|
486
492
|
if (alive)
|
|
487
|
-
|
|
493
|
+
setIds(new Set());
|
|
488
494
|
});
|
|
489
495
|
};
|
|
490
496
|
scan();
|
|
@@ -494,7 +500,7 @@ function useArchivable(doneSig, tasksDir, projectRoot, days) {
|
|
|
494
500
|
clearInterval(id);
|
|
495
501
|
};
|
|
496
502
|
}, [doneSig, tasksDir, projectRoot, days]);
|
|
497
|
-
return
|
|
503
|
+
return ids;
|
|
498
504
|
}
|
|
499
505
|
// Legge il task file della task selezionata (Q1+B T20). On-id-change: navigare
|
|
500
506
|
// con ↑↓ ricarica il dettaglio; leggere un singolo file 4-9KB è I/O triviale,
|
|
@@ -622,6 +628,13 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
622
628
|
// modale: la lista si aggiorna dal vivo, quindi `esc` deve poter ripristinare.
|
|
623
629
|
const [view, setView] = useState(() => loadView(cwd));
|
|
624
630
|
const [viewBackup, setViewBackup] = useState(null);
|
|
631
|
+
// T100 — vista attiva di ciascun pane, navigata con ←/→. VOLATILE per
|
|
632
|
+
// decisione (D3 create): non entra in `deck-view.json`, il deck riapre sempre
|
|
633
|
+
// su `Tasks` e su `{parent}`. Il criterio è il rischio di leggere una lista
|
|
634
|
+
// parziale credendola completa — un filtro salvato lo si è scelto, una vista
|
|
635
|
+
// riaperta a freddo si legge come la lista intera.
|
|
636
|
+
const [taskViewId, setTaskViewId] = useState('tasks');
|
|
637
|
+
const [sessionViewId, setSessionViewId] = useState('context');
|
|
625
638
|
const [filterCursor, setFilterCursor] = useState({ row: 0, col: 0 });
|
|
626
639
|
// T41 — bozza dell'edit (null fuori dal modale) e riga attiva della griglia.
|
|
627
640
|
const [edit, setEdit] = useState(null);
|
|
@@ -697,15 +710,31 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
697
710
|
const doneSig = useMemo(() => tasks.filter((t) => isDone(t.prog)).map((t) => t.id).join(','), [tasks]);
|
|
698
711
|
const archivableDays = useMemo(() => loadArchivableDays(cwd), [cwd]);
|
|
699
712
|
const archivable = useArchivable(doneSig, tasksDir, cwd, archivableDays);
|
|
713
|
+
// T100 — le task effettivamente a schermo: la vista principale coincide con
|
|
714
|
+
// `viewTasks` (nessun ricalcolo sul cammino di default), le altre due passano
|
|
715
|
+
// dal predicato del catalogo. I CONTATORI restano misurati sulla vista di
|
|
716
|
+
// default, o navigare cambierebbe i numeri che si sta navigando.
|
|
717
|
+
const taskCounts = {
|
|
718
|
+
filtered: viewTasks.length,
|
|
719
|
+
total: tasks.length,
|
|
720
|
+
hidden: hiddenTasks,
|
|
721
|
+
archivable: archivable.size,
|
|
722
|
+
};
|
|
723
|
+
const paneTasks = useMemo(() => taskViewId === 'tasks'
|
|
724
|
+
? viewTasks
|
|
725
|
+
: selectTasks(tasks, taskViewId, { view, archivable }), [taskViewId, viewTasks, tasks, view, archivable]);
|
|
700
726
|
const isSpot = sel === SPOT;
|
|
701
727
|
const isAll = sel === ALL;
|
|
702
728
|
const projectName = cwd.split('/').pop() || cwd;
|
|
703
729
|
// Unica fonte della selezione: si legge SEMPRE dalla vista, mai dall'array
|
|
704
730
|
// grezzo — è l'invariante che tiene allineati dettaglio mostrato e spawn.
|
|
705
|
-
const selTask = typeof sel === 'string' ?
|
|
731
|
+
const selTask = typeof sel === 'string' ? paneTasks.find((t) => t.id === sel) ?? null : null;
|
|
706
732
|
const selectedTaskId = selTask?.id ?? null;
|
|
707
|
-
const selIndex = selTask ?
|
|
733
|
+
const selIndex = selTask ? paneTasks.indexOf(selTask) + META_ROWS : isAll ? ROW_ALL : ROW_SPOT;
|
|
708
734
|
const detail = useTaskDetail(tasksDir, selectedTaskId ?? undefined);
|
|
735
|
+
// Il parent delle conversazioni: l'asse che sceglie il pane task, ortogonale
|
|
736
|
+
// alla vista che sceglie l'header (D2 create).
|
|
737
|
+
const parentLabel = isAll ? 'tutte' : isSpot ? 'spot' : selectedTaskId ?? '—';
|
|
709
738
|
// Conteggio figli per task + spot (badge nel Tasks pane).
|
|
710
739
|
const childCount = new Map();
|
|
711
740
|
let spotCount = 0;
|
|
@@ -731,12 +760,24 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
731
760
|
// contestuali. Dedup, cap solo sulle contestuali, righe stale per le pinnate
|
|
732
761
|
// orfane. Core PURO in session-list.ts (testabile senza Ink).
|
|
733
762
|
const assembled = useMemo(() => assembleSessionList(childSessions, sessions, pinned, isAll ? MAX_SESSIONS_ALL : MAX_SESSIONS), [childSessions, sessions, pinned, isAll]);
|
|
734
|
-
const sessionRows = assembled.rows;
|
|
735
|
-
const selSessionObj = selectedSession(sessionRows, selSessionId);
|
|
736
763
|
// T62 — contato sulla lista INTERA (stessa ragione delle larghezze di colonna
|
|
737
764
|
// qui sotto): derivarlo dalla finestra visibile lo farebbe cambiare a ogni
|
|
738
765
|
// scroll, cioè un contatore che conta lo schermo invece della lista.
|
|
739
|
-
|
|
766
|
+
// T100 — «intera» ora vuol dire la lista della vista di DEFAULT (`assembled`),
|
|
767
|
+
// non quella a schermo: il contatore di una voce del catalogo non può
|
|
768
|
+
// dipendere da quale voce è selezionata, o navigare muoverebbe i numeri.
|
|
769
|
+
const liveCount = useMemo(() => assembled.rows.filter((r) => r.kind !== 'separator' && live.has(r.sessionId)).length, [assembled, live]);
|
|
770
|
+
const sessionCounts = {
|
|
771
|
+
total: assembled.pinnedCount + assembled.contextTotal,
|
|
772
|
+
live: liveCount,
|
|
773
|
+
pinned: assembled.pinnedCount,
|
|
774
|
+
older: assembled.contextHidden,
|
|
775
|
+
};
|
|
776
|
+
// T100 — le righe a schermo sono quelle della vista attiva. Fuori dalla vista
|
|
777
|
+
// di default il separatore non c'è: segna il confine fra pinnate e
|
|
778
|
+
// contestuali, e in un sottoinsieme quel confine non esiste più.
|
|
779
|
+
const sessionRows = useMemo(() => selectSessionRows(sessionViewId, { assembled, isLive: (id) => live.has(id) }), [sessionViewId, assembled, live]);
|
|
780
|
+
const selSessionObj = selectedSession(sessionRows, selSessionId);
|
|
740
781
|
// T60 — larghezze delle colonne fisse della lista sessioni, misurate sulla
|
|
741
782
|
// lista INTERA e non sulla finestra visibile: derivarle dalle sole righe a
|
|
742
783
|
// schermo le farebbe cambiare a ogni scroll, cioè l'opposto di una tabella.
|
|
@@ -800,18 +841,32 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
800
841
|
// 2 padding) = 8. Sottostimare tronca un carattere, sovrastimare manda a capo
|
|
801
842
|
// una riga che il budget d'altezza non ha contato.
|
|
802
843
|
const sheetWidth = Math.max(20, (columns || 80) - 8);
|
|
844
|
+
// T75 — il markdown si rende PRIMA del wrap, e il resto della catena lavora
|
|
845
|
+
// sul testo reso: `**foo**` occupa 3 colonne rese e 7 grezze, quindi
|
|
846
|
+
// wrappare sui marker manderebbe a capo su un conteggio che il terminale non
|
|
847
|
+
// disegna. Memo separato dal wrap perché il parse dipende solo dal testo: un
|
|
848
|
+
// resize ri-wrappa 9KB, non li ri-parsa.
|
|
849
|
+
const sheetDoc = useMemo(() => (sheet?.text ? parseMarkdown(sheet.text) : null), [sheet]);
|
|
803
850
|
// Le righe conservano i propri offset invece di essere appiattite a stringa
|
|
804
851
|
// (T66 le buttava con `.map((l) => l.text)`): è ciò che rende
|
|
805
852
|
// l'evidenziazione un'intersezione di intervalli invece di un caso speciale
|
|
806
|
-
// per il match spezzato dall'a-capo.
|
|
807
|
-
|
|
853
|
+
// per il match spezzato dall'a-capo. Dopo T75 gli offset indicizzano il testo
|
|
854
|
+
// RESO, ed è l'unica coordinata coerente che resti — il sorgente non è più
|
|
855
|
+
// ciò che sta a schermo.
|
|
856
|
+
const sheetLines = useMemo(() => (sheetDoc ? wrapWithOffsets(sheetDoc.text, sheetWidth) : []), [sheetDoc, sheetWidth]);
|
|
808
857
|
const sheetCap = detailCapacity(rows, find?.open === true);
|
|
809
858
|
const sheetMaxTop = Math.max(0, sheetLines.length - sheetCap);
|
|
810
|
-
// Lo scan gira sulla STESSA stringa che si renderizza
|
|
811
|
-
//
|
|
812
|
-
//
|
|
813
|
-
//
|
|
814
|
-
|
|
859
|
+
// Lo scan gira sulla STESSA stringa che si renderizza: cercare su un testo
|
|
860
|
+
// diverso da quello a schermo darebbe offset che indicizzano un altro
|
|
861
|
+
// documento, cioè un'evidenziazione spostata di N caratteri e nessun errore.
|
|
862
|
+
//
|
|
863
|
+
// Dopo T75 quella stringa è il testo RESO, non più il sorgente: si cerca ciò
|
|
864
|
+
// che si vede. Ne discende che `**` non è più cercabile — è la conseguenza
|
|
865
|
+
// voluta, perché a schermo non c'è; e `Priority`, che prima era `**Priority**`
|
|
866
|
+
// e si trovava lo stesso, continua a trovarsi.
|
|
867
|
+
const findRes = useMemo(() => find && sheetDoc
|
|
868
|
+
? scanText(sheetDoc.text, find.q)
|
|
869
|
+
: { occ: [], error: '' }, [find?.q, sheetDoc]);
|
|
815
870
|
// L'indice si clampa qui invece di essere corretto a ogni `setOccIdx`: la
|
|
816
871
|
// lista si accorcia da sola mentre si digita, e un indice fuori range vivrebbe
|
|
817
872
|
// per il tempo di un render.
|
|
@@ -849,10 +904,10 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
849
904
|
// dalla vista (filtro appena attivato, oppure sparita da tasks.md), si cade
|
|
850
905
|
// sulla prima visibile — fallback deterministico, mai una posizione a caso.
|
|
851
906
|
useEffect(() => {
|
|
852
|
-
if (typeof sel === 'string' && !
|
|
853
|
-
setSel(
|
|
907
|
+
if (typeof sel === 'string' && !paneTasks.some((t) => t.id === sel)) {
|
|
908
|
+
setSel(paneTasks[0]?.id ?? ALL);
|
|
854
909
|
}
|
|
855
|
-
}, [
|
|
910
|
+
}, [paneTasks, sel]);
|
|
856
911
|
// T50 — la selezione (id) resta valida sotto la vista a due gruppi: se l'id
|
|
857
912
|
// non è più una riga selezionabile (cambio parent, lista mutata, pin rimosso,
|
|
858
913
|
// sessione sparita) cade sulla prima riga — fallback deterministico, mai una
|
|
@@ -935,7 +990,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
935
990
|
// messaggi a mano. `verb` è l'unica cosa che cambia fra i due usi.
|
|
936
991
|
function selectedTaskOr(keyLabel, verb) {
|
|
937
992
|
if (focus !== 'tasks') {
|
|
938
|
-
setNote(`${keyLabel} → ${verb}: seleziona una task (
|
|
993
|
+
setNote(`${keyLabel} → ${verb}: seleziona una task (tab per il pane)`);
|
|
939
994
|
return null;
|
|
940
995
|
}
|
|
941
996
|
// T59 — la guardia è "non è una task", non "è spot": le righe meta sono due
|
|
@@ -1178,13 +1233,33 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1178
1233
|
// 2..N+1 = task visibili) e la riconverte subito in sentinella o id: l'indice
|
|
1179
1234
|
// non sopravvive a un cambio di filtro, l'id sì.
|
|
1180
1235
|
function moveTaskSel(delta) {
|
|
1181
|
-
const next = Math.max(0, Math.min(
|
|
1236
|
+
const next = Math.max(0, Math.min(paneTasks.length + META_ROWS - 1, selIndex + delta));
|
|
1182
1237
|
if (next === ROW_ALL)
|
|
1183
1238
|
setSel(ALL);
|
|
1184
1239
|
else if (next === ROW_SPOT)
|
|
1185
1240
|
setSel(SPOT);
|
|
1186
1241
|
else
|
|
1187
|
-
setSel(
|
|
1242
|
+
setSel(paneTasks[next - META_ROWS]?.id ?? SPOT);
|
|
1243
|
+
}
|
|
1244
|
+
// T100 — ←/→ navigano il catalogo viste del pane in focus. Il reset della
|
|
1245
|
+
// selezione è la regola letterale «prima riga in alto», senza eccezioni: sul
|
|
1246
|
+
// pane task è `ROW_ALL` (D2 preflight — le righe meta non si saltano, e il
|
|
1247
|
+
// parent delle sessioni che torna a `tutte` è un effetto accettato); sul pane
|
|
1248
|
+
// sessioni basta invalidare l'id, e l'effect di validità atterra sulla prima
|
|
1249
|
+
// riga selezionabile della vista nuova.
|
|
1250
|
+
function cycleView(delta) {
|
|
1251
|
+
if (focus === 'tasks') {
|
|
1252
|
+
const next = cycleTaskView(taskViewId, delta);
|
|
1253
|
+
setTaskViewId(next);
|
|
1254
|
+
setSel(ALL);
|
|
1255
|
+
setNote(`vista task: ${taskView(next).label(taskCounts)}`);
|
|
1256
|
+
}
|
|
1257
|
+
else {
|
|
1258
|
+
const next = cycleSessionView(sessionViewId, delta);
|
|
1259
|
+
setSessionViewId(next);
|
|
1260
|
+
setSelSessionId(null);
|
|
1261
|
+
setNote(`vista sessioni: ${sessionView(next).label(sessionCounts, parentLabel)}`);
|
|
1262
|
+
}
|
|
1188
1263
|
}
|
|
1189
1264
|
// T52 — `⏎` contestuale al TIPO di riga: la lista ne mescola due e l'azione
|
|
1190
1265
|
// giusta dipende da quale è selezionata. Riga sessione → resume, identico al
|
|
@@ -1718,9 +1793,18 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1718
1793
|
}
|
|
1719
1794
|
return;
|
|
1720
1795
|
}
|
|
1721
|
-
if (key.
|
|
1796
|
+
if (key.tab) {
|
|
1797
|
+
// T100 — `tab` resta l'unico tasto che sposta il focus fra i due pane. Le
|
|
1798
|
+
// frecce orizzontali facevano lo stesso lavoro (due tasti per un'azione)
|
|
1799
|
+
// mentre la navigazione DENTRO un pane non ne aveva nessuno: ora sono il
|
|
1800
|
+
// selettore di vista dell'header. Il ramo `mode === 'detail'`, anteposto e
|
|
1801
|
+
// chiuso da `return`, non è più l'eccezione a «cambia pane» ma a «cambia
|
|
1802
|
+
// vista» — l'ordine dei rami non cambia, cambia cosa cattura.
|
|
1722
1803
|
setFocus((f) => (f === 'tasks' ? 'sessions' : 'tasks'));
|
|
1723
1804
|
}
|
|
1805
|
+
else if (key.leftArrow || key.rightArrow) {
|
|
1806
|
+
cycleView(key.leftArrow ? -1 : 1);
|
|
1807
|
+
}
|
|
1724
1808
|
else if (key.upArrow) {
|
|
1725
1809
|
if (focus === 'tasks')
|
|
1726
1810
|
moveTaskSel(-1);
|
|
@@ -1775,9 +1859,20 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1775
1859
|
setMode('sort');
|
|
1776
1860
|
}
|
|
1777
1861
|
else if (input === 'F') {
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1862
|
+
// T100/D3 — i filtri valgono SOLO sulla vista principale: su `nascoste`
|
|
1863
|
+
// riapplicarli non ha senso (quella vista È il loro complemento), su
|
|
1864
|
+
// `archiviabili` non li si vuole (è cieca ai filtri per decisione). Stessa
|
|
1865
|
+
// forma dell'inerzia di ^K/^P/^R dentro il detail, ma keyed sulla vista
|
|
1866
|
+
// invece che sul modo — e come là, l'inerzia lo DICE invece di non fare
|
|
1867
|
+
// niente in silenzio.
|
|
1868
|
+
if (taskViewId !== 'tasks') {
|
|
1869
|
+
setNote(`F → filtri: solo sulla vista ${TASK_VIEWS[0].label(taskCounts)} (ora: ${taskView(taskViewId).label(taskCounts)})`);
|
|
1870
|
+
}
|
|
1871
|
+
else {
|
|
1872
|
+
setViewBackup(view);
|
|
1873
|
+
setNote('');
|
|
1874
|
+
setMode('filter');
|
|
1875
|
+
}
|
|
1781
1876
|
}
|
|
1782
1877
|
else if (input === 'f') {
|
|
1783
1878
|
// T28 — fork della sessione selezionata. Minuscola come `t`/`c` (T39):
|
|
@@ -1785,7 +1880,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1785
1880
|
// Vive solo sul pane sessioni: il fork ha per oggetto una conversazione,
|
|
1786
1881
|
// e senza focus lì non ce n'è una selezionata su cui agire.
|
|
1787
1882
|
if (focus !== 'sessions') {
|
|
1788
|
-
setNote('f → fork: seleziona una sessione (
|
|
1883
|
+
setNote('f → fork: seleziona una sessione (tab per il pane)');
|
|
1789
1884
|
}
|
|
1790
1885
|
else {
|
|
1791
1886
|
const s = selSessionObj;
|
|
@@ -1817,7 +1912,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1817
1912
|
// pinnata STALE (l'unico modo di spinnarla). Scrive il sidecar e ricarica
|
|
1818
1913
|
// subito, senza attendere il tick del poll.
|
|
1819
1914
|
if (focus !== 'sessions') {
|
|
1820
|
-
setNote('p → pin: seleziona una sessione (
|
|
1915
|
+
setNote('p → pin: seleziona una sessione (tab per il pane)');
|
|
1821
1916
|
}
|
|
1822
1917
|
else if (!selSessionId) {
|
|
1823
1918
|
setNote('p → nessuna sessione da pinnare');
|
|
@@ -1845,7 +1940,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1845
1940
|
// su una pinnata STALE, perché annotare «questa non c'è più, era X» è
|
|
1846
1941
|
// proprio il caso in cui una nota serve.
|
|
1847
1942
|
if (focus !== 'sessions') {
|
|
1848
|
-
setNote('N → nota: seleziona una sessione (
|
|
1943
|
+
setNote('N → nota: seleziona una sessione (tab per il pane)');
|
|
1849
1944
|
}
|
|
1850
1945
|
else if (!selSessionId) {
|
|
1851
1946
|
setNote('N → nessuna sessione da annotare');
|
|
@@ -1863,7 +1958,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1863
1958
|
// il binding è nostro, il transcript è di CC — riassegnare una
|
|
1864
1959
|
// conversazione il cui transcript non c'è più resta legittimo.
|
|
1865
1960
|
if (focus !== 'sessions') {
|
|
1866
|
-
setNote('A → assegna: seleziona una sessione (
|
|
1961
|
+
setNote('A → assegna: seleziona una sessione (tab per il pane)');
|
|
1867
1962
|
}
|
|
1868
1963
|
else if (!selSessionId) {
|
|
1869
1964
|
setNote('A → nessuna sessione da assegnare');
|
|
@@ -1912,7 +2007,6 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1912
2007
|
exit();
|
|
1913
2008
|
}
|
|
1914
2009
|
});
|
|
1915
|
-
const parentLabel = isAll ? 'tutte' : isSpot ? 'spot' : selectedTaskId ?? '—';
|
|
1916
2010
|
const canSpawn = focus === 'tasks' && selTask !== null;
|
|
1917
2011
|
const canResume = focus === 'sessions' && selSessionObj !== null;
|
|
1918
2012
|
// T50 — il pin agisce su qualunque riga selezionata (anche stale, per
|
|
@@ -1982,7 +2076,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1982
2076
|
// posizione è lo scroll mosso a mano. Il clamp serve comunque — un resize
|
|
1983
2077
|
// può accorciare il testo sotto uno scroll già dato.
|
|
1984
2078
|
const start = Math.min(sheetTop, sheetMaxTop);
|
|
1985
|
-
return (_jsx(DetailScreen, { id: sheet.id, title: sheet.title, missing: sheet.text === null, lines: sheetLines.slice(start, start + sheetCap), top: start, total: sheetLines.length, capacity: sheetCap, action: sheetAction, columns: columns, find: find, occ: findRes.occ, occCur: occCur }));
|
|
2079
|
+
return (_jsx(DetailScreen, { id: sheet.id, title: sheet.title, missing: sheet.text === null, lines: sheetLines.slice(start, start + sheetCap), spans: sheetDoc?.spans ?? [], top: start, total: sheetLines.length, capacity: sheetCap, action: sheetAction, columns: columns, find: find, occ: findRes.occ, occCur: occCur }));
|
|
1986
2080
|
}
|
|
1987
2081
|
// ── T52 · schermate sostitutive ─────────────────────────────────────────
|
|
1988
2082
|
// Ricerca e reader sono gli unici modali che NON stanno in flusso sopra i
|
|
@@ -2062,8 +2156,8 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
2062
2156
|
// Finestre di rendering. Le liste "logiche" (viewTasks, sessionRows)
|
|
2063
2157
|
// restano intere: navigazione, selezione e spawn continuano a ragionare su
|
|
2064
2158
|
// quelle, la finestra è solo ciò che finisce a schermo.
|
|
2065
|
-
const taskWin = windowRange(
|
|
2066
|
-
const windowTasks =
|
|
2159
|
+
const taskWin = windowRange(paneTasks.length, selIndex - META_ROWS, budget.taskRows);
|
|
2160
|
+
const windowTasks = paneTasks.slice(taskWin.start, taskWin.end);
|
|
2067
2161
|
const selRowIndex = rowIndexOf(sessionRows, selSessionId);
|
|
2068
2162
|
const sessionWin = windowRange(sessionRows.length, selRowIndex, budget.sessionRows);
|
|
2069
2163
|
const windowRows = sessionRows.slice(sessionWin.start, sessionWin.end);
|
|
@@ -2073,7 +2167,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
2073
2167
|
if (budget.compact) {
|
|
2074
2168
|
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"] })] }));
|
|
2075
2169
|
}
|
|
2076
|
-
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,
|
|
2170
|
+
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, counts: taskCounts, activeView: taskViewId, paneCount: paneTasks.length, view: view, selected: selIndex, spotCount: spotCount, allCount: sessions.length, childCount: childCount, focused: focus === 'tasks', loadError: loadError, windowStart: taskWin.start, above: taskWin.start, below: paneTasks.length - taskWin.end, columns: columns }), _jsx(SessionsPane, { parentLabel: parentLabel, isSpot: isSpot, isAll: isAll, bindings: bindings, taskW: sessionCols.task, ageW: sessionCols.age, rows: windowRows, counts: sessionCounts, activeView: sessionViewId, paneCount: sessionRows.length, selectedId: selSessionId ?? undefined, focused: focus === 'sessions', above: sessionWin.start, below: sessionRows.length - sessionWin.end, columns: columns, forkOf: forkOf, sessionNotes: sessionNotes, projectCore: projectCore, live: live })] }), budget.preview && previewKind === 'task' && detail ? (_jsx(PreviewPane, { kind: "task", detail: detail, maxLines: budget.detailLines, columns: columns })) : budget.preview && previewKind === 'session' && selSessionObj ? (_jsx(PreviewPane, { kind: "session", s: selSessionObj, firstLines: budget.sessionFirstLines, lastLines: budget.sessionLastLines, columns: columns, origin: forkOf.get(selSessionObj.sessionId) ?? null, note: sessionNotes.get(selSessionObj.sessionId) ?? '', live: live.get(selSessionObj.sessionId) ?? null })) : null, note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: sanitize(note) }) : null] }));
|
|
2077
2171
|
}
|
|
2078
2172
|
const SORT_UI = { pri: 'pri', prog: 'stato', id: 'id' };
|
|
2079
2173
|
// Modali resi IN FLUSSO (come l'input box di create), non in overlay assoluto:
|
|
@@ -2272,6 +2366,50 @@ function ReaderLine({ line, occ, current, }) {
|
|
|
2272
2366
|
// mostrerebbe più dove sono le altre.
|
|
2273
2367
|
_jsx(Text, { backgroundColor: s.current ? 'cyan' : 'yellow', color: "black", children: s.text }, i)) : (_jsx(Text, { children: s.text }, i))) }));
|
|
2274
2368
|
}
|
|
2369
|
+
/** Resa di ogni costrutto markdown (T75 · D4): un solo livello di enfasi per
|
|
2370
|
+
* costrutto, senza un secondo alfabeto da imparare. Heading uguali a ogni
|
|
2371
|
+
* livello — la gerarchia la porta già il testo. `code` e `fence` condividono
|
|
2372
|
+
* il giallo perché sono lo stesso costrutto a due granularità: dargli due
|
|
2373
|
+
* colori direbbe che sono due cose. */
|
|
2374
|
+
const MD_STYLE = {
|
|
2375
|
+
heading: { bold: true, color: 'cyan' },
|
|
2376
|
+
bold: { bold: true },
|
|
2377
|
+
code: { color: 'yellow' },
|
|
2378
|
+
fence: { color: 'yellow' },
|
|
2379
|
+
};
|
|
2380
|
+
/**
|
|
2381
|
+
* Una riga del detail (T75): markdown reso, con sopra l'evidenziazione della
|
|
2382
|
+
* ricerca.
|
|
2383
|
+
*
|
|
2384
|
+
* Due segmentazioni sulla stessa riga, annidate e non fuse: prima si taglia sui
|
|
2385
|
+
* costrutti markdown, poi ogni pezzo si ritaglia sulle occorrenze. L'ordine non
|
|
2386
|
+
* è indifferente — così un match a cavallo di un `**grassetto**` resta
|
|
2387
|
+
* evidenziato per intero e insieme conserva il grassetto sulla metà che ce
|
|
2388
|
+
* l'ha, cosa che una segmentazione unica dovrebbe risolvere decidendo chi vince.
|
|
2389
|
+
*
|
|
2390
|
+
* Gli offset di `occ` e di `spans` indicizzano ENTRAMBI il testo reso: è ciò
|
|
2391
|
+
* che permette di comporli senza rimappature. Vedi `sheetDoc` per il perché la
|
|
2392
|
+
* ricerca del detail ha smesso di scandire il sorgente.
|
|
2393
|
+
*/
|
|
2394
|
+
function DetailLine({ line, spans, occ, current, }) {
|
|
2395
|
+
const styled = sliceSpans(line, spans);
|
|
2396
|
+
// Riga vuota → uno spazio: un `<Text>` senza contenuto Ink non lo disegna, e
|
|
2397
|
+
// il testo si compatterebbe perdendo la struttura del file.
|
|
2398
|
+
if (styled.length === 0)
|
|
2399
|
+
return _jsx(Text, { wrap: "truncate-end", children: " " });
|
|
2400
|
+
let off = line.start;
|
|
2401
|
+
return (_jsx(Text, { wrap: "truncate-end", children: styled.map((seg, i) => {
|
|
2402
|
+
const st = seg.kind ? MD_STYLE[seg.kind] : undefined;
|
|
2403
|
+
const at = off;
|
|
2404
|
+
off += seg.text.length;
|
|
2405
|
+
// Senza ricerca aperta il secondo taglio non ha niente da tagliare, e
|
|
2406
|
+
// saltarlo evita di allocare tre array per ogni riga a ogni freccia.
|
|
2407
|
+
if (occ.length === 0) {
|
|
2408
|
+
return (_jsx(Text, { bold: st?.bold, color: st?.color, children: seg.text }, i));
|
|
2409
|
+
}
|
|
2410
|
+
return (_jsx(Text, { bold: st?.bold, color: st?.color, children: sliceLine(seg.text, at, occ, current).map((p, j) => p.hit ? (_jsx(Text, { backgroundColor: p.current ? 'cyan' : 'yellow', color: "black", children: p.text }, j)) : (_jsx(Text, { children: p.text }, j))) }, i));
|
|
2411
|
+
}) }));
|
|
2412
|
+
}
|
|
2275
2413
|
/** Campo della ricerca nel detail: finestra ancorata al caret, cursore inverso
|
|
2276
2414
|
* sulla cella reale. Gemello di `EditTextField` senza la label, che qui sta
|
|
2277
2415
|
* fuori perché il campo vive in FLUSSO su una riga condivisa col contatore —
|
|
@@ -2293,7 +2431,7 @@ function DetailFindField({ value, caret, cols }) {
|
|
|
2293
2431
|
* all'arrivo del mouse (T21 · SGR enable + hit-test) senza migrazione. La
|
|
2294
2432
|
* navigazione da tastiera ci si sovrappone senza conflitti.
|
|
2295
2433
|
*/
|
|
2296
|
-
function DetailScreen({ id, title, missing, lines, top, total, capacity, action, columns, find, occ, occCur, }) {
|
|
2434
|
+
function DetailScreen({ id, title, missing, lines, spans, top, total, capacity, action, columns, find, occ, occCur, }) {
|
|
2297
2435
|
const last = Math.min(total, top + capacity);
|
|
2298
2436
|
// Il taglio lo fa il chiamante (invariante ③ di width.ts): la riga bottoni è
|
|
2299
2437
|
// ASCII, quindi `truncate-end` oggi darebbe il risultato giusto per caso — ma
|
|
@@ -2314,10 +2452,7 @@ function DetailScreen({ id, title, missing, lines, top, total, capacity, action,
|
|
|
2314
2452
|
if (dropped(shown) > 0)
|
|
2315
2453
|
shown = cutParts(parts, Math.max(0, width - 6));
|
|
2316
2454
|
const cutCount = dropped(shown);
|
|
2317
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "cyan", children: id }), " \u00B7 ", cut(title, Math.max(10, width - 34)), missing ? '' : ` · righe ${total === 0 ? 0 : top + 1}-${last} di ${total}`] }), find?.open ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "yellow", children: "\u2191\u2193" }), " occorrenza \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " caret \u00B7", ' ', _jsx(Text, { color: "yellow", children: "^U" }), " svuota \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " tieni \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "yellow", children: "\u2191\u2193" }), " riga \u00B7 ", _jsx(Text, { color: "yellow", children: "PgUp/PgDn" }), " pagina \u00B7", ' ', _jsx(Text, { color: "yellow", children: "g/G" }), " estremi \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " azione \u00B7", ' ', _jsx(Text, { color: "yellow", children: "^F" }), " cerca \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " esegui \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " chiudi"] })), _jsx(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, marginTop: 1, children: missing ? (_jsxs(Text, { color: "yellow", wrap: "truncate-end", children: [WARN, " task file non trovato \u00B7 le azioni restano attive (deck-run risolve la task per id)"] })) : (
|
|
2318
|
-
// Riga vuota → uno spazio: un `<Text>` senza contenuto Ink non lo
|
|
2319
|
-
// disegna, e il testo si compatterebbe perdendo la struttura del file.
|
|
2320
|
-
lines.map((l, i) => _jsx(ReaderLine, { line: l, occ: occ, current: occCur }, top + i))) }), find?.open ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { dimColor: true, children: "cerca " }), _jsx(DetailFindField, { value: find.q, caret: find.caret, cols: Math.max(10, width - 28) }), find.q.length === 0 ? (_jsx(Text, { dimColor: true, children: " \u00B7 digita per cercare" })) : occ.length === 0 ? (_jsx(Text, { color: "yellow", children: " \u00B7 nessuna occorrenza" })) : (_jsxs(Text, { color: "cyan", children: [' ', "\u00B7 ", occCur + 1, "/", occ.length] }))] }) })) : null, _jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "truncate-end", children: [shown.map((part, i) => i % 2 === 1 ? (_jsx(Text, { children: part }, i)) : (_jsx(Text, { inverse: i / 2 === action, color: i / 2 === action ? 'green' : 'gray', children: part }, i))), cutCount > 0 ? _jsxs(Text, { color: "yellow", children: [" \u00B7 +", cutCount] }) : null] }) })] }));
|
|
2455
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "cyan", children: id }), " \u00B7 ", cut(title, Math.max(10, width - 34)), missing ? '' : ` · righe ${total === 0 ? 0 : top + 1}-${last} di ${total}`] }), find?.open ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "yellow", children: "\u2191\u2193" }), " occorrenza \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " caret \u00B7", ' ', _jsx(Text, { color: "yellow", children: "^U" }), " svuota \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " tieni \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "yellow", children: "\u2191\u2193" }), " riga \u00B7 ", _jsx(Text, { color: "yellow", children: "PgUp/PgDn" }), " pagina \u00B7", ' ', _jsx(Text, { color: "yellow", children: "g/G" }), " estremi \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " azione \u00B7", ' ', _jsx(Text, { color: "yellow", children: "^F" }), " cerca \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " esegui \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " chiudi"] })), _jsx(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, marginTop: 1, children: missing ? (_jsxs(Text, { color: "yellow", wrap: "truncate-end", children: [WARN, " task file non trovato \u00B7 le azioni restano attive (deck-run risolve la task per id)"] })) : (lines.map((l, i) => (_jsx(DetailLine, { line: l, spans: spans, occ: occ, current: occCur }, top + i)))) }), find?.open ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { dimColor: true, children: "cerca " }), _jsx(DetailFindField, { value: find.q, caret: find.caret, cols: Math.max(10, width - 28) }), find.q.length === 0 ? (_jsx(Text, { dimColor: true, children: " \u00B7 digita per cercare" })) : occ.length === 0 ? (_jsx(Text, { color: "yellow", children: " \u00B7 nessuna occorrenza" })) : (_jsxs(Text, { color: "cyan", children: [' ', "\u00B7 ", occCur + 1, "/", occ.length] }))] }) })) : null, _jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "truncate-end", children: [shown.map((part, i) => i % 2 === 1 ? (_jsx(Text, { children: part }, i)) : (_jsx(Text, { inverse: i / 2 === action, color: i / 2 === action ? 'green' : 'gray', children: part }, i))), cutCount > 0 ? _jsxs(Text, { color: "yellow", children: [" \u00B7 +", cutCount] }) : null] }) })] }));
|
|
2321
2456
|
}
|
|
2322
2457
|
/**
|
|
2323
2458
|
* Larghezza del testo dentro la lista della schermata di assegnazione: box
|
|
@@ -2375,24 +2510,39 @@ function AssignScreen({ sessionId, label, current, filter, rows, selected, match
|
|
|
2375
2510
|
* scoprirlo sarebbe il bordo del pane a schermo.
|
|
2376
2511
|
*
|
|
2377
2512
|
* `truncate-end` taglia dalla coda, e `cutParts` conserva l'ordine: l'ultimo
|
|
2378
|
-
* segmento resta il primo a cedere il posto (
|
|
2379
|
-
*
|
|
2513
|
+
* segmento resta il primo a cedere il posto (`↑↓` in coda alle voci navigabili).
|
|
2514
|
+
*
|
|
2515
|
+
* T100 — la riga non è più informativa: le voci del catalogo sono SELEZIONABILI
|
|
2516
|
+
* con ←/→, e l'attiva si distingue in video inverso (D5 — costa 0 colonne e non
|
|
2517
|
+
* entra in gara con la semantica di colore già occupata). Le voci ci sono tutte
|
|
2518
|
+
* anche a 0 (D1): un catalogo che si accorcia sposta le voci sotto le dita.
|
|
2519
|
+
* L'ordine è vincolato — le navigabili PRIMA di `↑N`/`↓N`, che cadono per primi
|
|
2520
|
+
* su un terminale stretto — e la voce attiva ha la precedenza sul budget (D6).
|
|
2380
2521
|
*/
|
|
2381
|
-
function TasksHeader({
|
|
2522
|
+
function TasksHeader({ counts, active, above, below, focused, columns, }) {
|
|
2523
|
+
const views = TASK_VIEWS.map((v, i) => {
|
|
2524
|
+
const n = v.count(counts);
|
|
2525
|
+
return {
|
|
2526
|
+
// Il separatore sta nel segmento, non fra i segmenti: `cutParts` misura la
|
|
2527
|
+
// riga pezzo per pezzo e uno spazio fuori dai pezzi non verrebbe contato.
|
|
2528
|
+
text: `${i > 0 ? ' · ' : ''}${v.label(counts)}`,
|
|
2529
|
+
color: v.color,
|
|
2530
|
+
dim: v.dim || n === 0,
|
|
2531
|
+
active: v.id === active,
|
|
2532
|
+
};
|
|
2533
|
+
});
|
|
2382
2534
|
const segments = [
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
...(below > 0 ? [{ text: ` · ↓${below}`, dim: true }] : []),
|
|
2387
|
-
...(archivable > 0 ? [{ text: ` · ${archivable} archiviabili`, dim: true }] : []),
|
|
2535
|
+
...views,
|
|
2536
|
+
{ text: above > 0 ? ` · ↑${above}` : '', dim: true, active: false, color: undefined },
|
|
2537
|
+
{ text: below > 0 ? ` · ↓${below}` : '', dim: true, active: false, color: undefined },
|
|
2388
2538
|
];
|
|
2389
|
-
const shown = cutParts(segments.map((s) => s.text), paneTextWidth(columns));
|
|
2390
|
-
return (_jsx(Text, { bold: true, color: focused ? 'cyan' : undefined, wrap: "truncate-end", children: segments.map((seg, i) => shown[i] ? (_jsx(Text, { color: seg.color, dimColor: seg.dim, children: shown[i] }, i)) : null) }));
|
|
2539
|
+
const shown = cutParts(segments.map((s) => s.text), paneTextWidth(columns), segments.findIndex((s) => s.active));
|
|
2540
|
+
return (_jsx(Text, { bold: true, color: focused ? 'cyan' : undefined, wrap: "truncate-end", children: segments.map((seg, i) => shown[i] ? (_jsx(Text, { color: seg.color, dimColor: seg.dim, inverse: seg.active, children: shown[i] }, i)) : null) }));
|
|
2391
2541
|
}
|
|
2392
|
-
function TasksPane({ tasks,
|
|
2542
|
+
function TasksPane({ tasks, counts, activeView, paneCount, view, selected, spotCount, allCount, childCount, focused, loadError, windowStart, above, below, columns, }) {
|
|
2393
2543
|
const allSelected = selected === ROW_ALL;
|
|
2394
2544
|
const spotSelected = selected === ROW_SPOT;
|
|
2395
|
-
return (_jsxs(Box, { flexDirection: "column", width: "50%", marginRight: 1, borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', paddingX: 1, children: [_jsx(TasksHeader, {
|
|
2545
|
+
return (_jsxs(Box, { flexDirection: "column", width: "50%", marginRight: 1, borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', paddingX: 1, children: [_jsx(TasksHeader, { counts: counts, active: activeView, above: above, below: below, focused: focused, columns: columns }), _jsx(Text, { dimColor: true, wrap: "truncate-end", children: cut(sanitize(`sort: ${describeSort(view.sort)}` +
|
|
2396
2546
|
(view.hiddenPri.length + view.hiddenProg.length > 0
|
|
2397
2547
|
? ' · filtri: ' +
|
|
2398
2548
|
[
|
|
@@ -2401,7 +2551,12 @@ function TasksPane({ tasks, filtered, total, hidden, view, selected, spotCount,
|
|
|
2401
2551
|
]
|
|
2402
2552
|
.map((e) => `−${e.glyph}`)
|
|
2403
2553
|
.join(' ')
|
|
2404
|
-
: '')), paneTextWidth(columns)) }), _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 })) :
|
|
2554
|
+
: '')), paneTextWidth(columns)) }), _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 })) : paneCount === 0 ? (
|
|
2555
|
+
// T100/D1 — una voce a contatore 0 resta navigabile, e selezionarla dà
|
|
2556
|
+
// una lista vuota che DICE perché è vuota. Senza la nota il pane si
|
|
2557
|
+
// legge come rotto: le righe meta restano, le task no, e niente spiega
|
|
2558
|
+
// che è la vista scelta a non contenere nulla.
|
|
2559
|
+
_jsx(Text, { color: "yellow", wrap: "truncate-end", children: cut(taskView(activeView).empty, paneTextWidth(columns)) })) : (tasks.map((task, i) => {
|
|
2405
2560
|
// windowStart riporta l'indice di finestra a quello della lista
|
|
2406
2561
|
// completa, su cui è keyata la selezione. +META_ROWS: le prime due
|
|
2407
2562
|
// righe sono le meta.
|
|
@@ -2432,24 +2587,39 @@ function TasksPane({ tasks, filtered, total, hidden, view, selected, spotCount,
|
|
|
2432
2587
|
* condiviso, `cutParts`), la resa è del pezzo. Il giallo su `📌N` distingue le
|
|
2433
2588
|
* pinnate dal resto dell'header e non è decorazione.
|
|
2434
2589
|
*/
|
|
2435
|
-
function SessionsHeader({ parentLabel,
|
|
2590
|
+
function SessionsHeader({ parentLabel, counts, active, above, below, focused, columns, }) {
|
|
2591
|
+
const views = SESSION_VIEWS.map((v) => {
|
|
2592
|
+
const n = v.count(counts);
|
|
2593
|
+
return {
|
|
2594
|
+
text: ` · ${v.label(counts, parentLabel)}`,
|
|
2595
|
+
color: v.color,
|
|
2596
|
+
dim: v.dim || n === 0,
|
|
2597
|
+
active: v.id === active,
|
|
2598
|
+
};
|
|
2599
|
+
});
|
|
2436
2600
|
const segments = [
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
...
|
|
2441
|
-
|
|
2442
|
-
|
|
2601
|
+
// `Sessions` non è una voce del catalogo: nomina il pane, non un
|
|
2602
|
+
// sottoinsieme, quindi non è raggiungibile con le frecce.
|
|
2603
|
+
{ text: 'Sessions', color: undefined, dim: false, active: false },
|
|
2604
|
+
...views,
|
|
2605
|
+
{ text: above > 0 ? ` · ↑${above}` : '', dim: true, active: false, color: undefined },
|
|
2606
|
+
{ text: below > 0 ? ` · ↓${below}` : '', dim: true, active: false, color: undefined },
|
|
2443
2607
|
];
|
|
2444
|
-
const shown = cutParts(segments.map((s) => s.text), paneTextWidth(columns));
|
|
2445
|
-
return (_jsx(Text, { bold: true, color: focused ? 'cyan' : undefined, wrap: "truncate-end", children: segments.map((seg, i) => shown[i] ? (_jsx(Text, { color: seg.color, dimColor: seg.dim, children: shown[i] }, i)) : null) }));
|
|
2608
|
+
const shown = cutParts(segments.map((s) => s.text), paneTextWidth(columns), segments.findIndex((s) => s.active));
|
|
2609
|
+
return (_jsx(Text, { bold: true, color: focused ? 'cyan' : undefined, wrap: "truncate-end", children: segments.map((seg, i) => shown[i] ? (_jsx(Text, { color: seg.color, dimColor: seg.dim, inverse: seg.active, children: shown[i] }, i)) : null) }));
|
|
2446
2610
|
}
|
|
2447
|
-
function SessionsPane({ parentLabel, isSpot, isAll, bindings, taskW, ageW, rows,
|
|
2448
|
-
return (_jsxs(Box, { flexDirection: "column", width: "50%", borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', paddingX: 1, children: [_jsx(SessionsHeader, { parentLabel: parentLabel,
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2611
|
+
function SessionsPane({ parentLabel, isSpot, isAll, bindings, taskW, ageW, rows, counts, activeView, paneCount, selectedId, focused, above, below, columns, forkOf, sessionNotes, projectCore, live, }) {
|
|
2612
|
+
return (_jsxs(Box, { flexDirection: "column", width: "50%", borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', paddingX: 1, children: [_jsx(SessionsHeader, { parentLabel: parentLabel, counts: counts, active: activeView, above: above, below: below, focused: focused, columns: columns }), paneCount === 0 ? (
|
|
2613
|
+
// T100 — la nota della vista di default resta quella storica, che nomina
|
|
2614
|
+
// il PARENT (task, spot o tutte); le altre tre viste portano la propria,
|
|
2615
|
+
// che nomina il sottoinsieme. Sono due vuoti diversi: «questo parent non
|
|
2616
|
+
// ha conversazioni» e «questo sottoinsieme del parent è vuoto».
|
|
2617
|
+
_jsx(Text, { color: "yellow", wrap: "truncate-end", children: cut(sessionView(activeView).empty ??
|
|
2618
|
+
(isAll
|
|
2619
|
+
? 'nessuna conversazione nel progetto'
|
|
2620
|
+
: isSpot
|
|
2621
|
+
? 'nessuna sessione libera'
|
|
2622
|
+
: 'nessuna sessione legata a questa task'), paneTextWidth(columns)) })) : (rows.map((row, i) => {
|
|
2453
2623
|
// T50 — separatore leggero fra pinnate e contestuali: riga dim, non un
|
|
2454
2624
|
// box pesante (coerente con lo styling delle Done dimmate).
|
|
2455
2625
|
if (row.kind === 'separator') {
|