@lamemind/loom-deck 0.69.0 → 0.71.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.
@@ -0,0 +1,101 @@
1
+ // Il DETAIL di un file: testo scorrevole più la riga che dichiara cosa partirà
2
+ // su `⏎`.
3
+ //
4
+ // T134 nasce come `useInboxOverlay`, un hook per la sola coda inbox. T160/P11 lo
5
+ // rende PARAMETRICO su cosa sta guardando, e lo instanzia due volte: una per il
6
+ // file inbox, una per il bersaglio doc (file o cartella). Le due istanze restano
7
+ // due voci distinte nei tre cataloghi dei modi (`CAPTURING_MODES`,
8
+ // `SCROLLING_MODES`, `MODAL_HEIGHT`), perché sono due schermate diverse anche se
9
+ // condividono la meccanica: `esc` da una torna al pane inbox, dall'altra al pane
10
+ // doc, e un modo solo per entrambe obbligherebbe a ricordare da dove si era
11
+ // entrati.
12
+ //
13
+ // Cosa è parametrico e cosa no. Parametrico: l'OGGETTO fotografato (`T`), il
14
+ // testo, il prompt e la callback. Non parametrico: lo scorrimento, la capienza,
15
+ // la chiusura prima dello spawn — cioè tutto ciò che è la meccanica dello sheet,
16
+ // ed è la parte che non aveva senso duplicare.
17
+ //
18
+ // L'oggetto è FOTOGRAFATO all'apertura e non è una vista sulla selezione
19
+ // corrente: l'overlay copre la lista, quindi il bersaglio dell'azione deve
20
+ // restare quello che si è scelto anche se un tick dello scan spostasse la
21
+ // selezione sotto.
22
+ //
23
+ // Lo spawn NON sta qui: arriva come callback, per lo stesso confine di
24
+ // `useSheetOverlay` e `useSearchOverlay` — un hook di overlay non esce dal deck.
25
+ import { useMemo, useState } from 'react';
26
+ import { fileSheetCapacity, pageStep } from '../viewport.js';
27
+ import { wrapWithOffsets } from '../width.js';
28
+ import { parseMarkdown } from '../markdown.js';
29
+ export function useFileSheet(deps) {
30
+ const { mode, rows, columns, setMode, setNote, onEnter } = deps;
31
+ const [sheet, setSheet] = useState(null);
32
+ const [top, setTop] = useState(0);
33
+ // Cornici da scalare: box esterno (2 bordi + 2 padding) + box testo (2 bordi
34
+ // + 2 padding) = 8, come il detail della task.
35
+ const width = Math.max(20, (columns || 80) - 8);
36
+ // Il markdown si rende PRIMA del wrap: `**foo**` occupa 3 colonne rese e 7
37
+ // grezze, quindi wrappare sui marker manderebbe a capo su un conteggio che il
38
+ // terminale non disegna. Memo separato dal wrap perché il parse dipende solo
39
+ // dal testo: un resize ri-wrappa, non ri-parsa.
40
+ const doc = useMemo(() => (sheet?.text ? parseMarkdown(sheet.text) : null), [sheet]);
41
+ const lines = useMemo(() => (doc ? wrapWithOffsets(doc.text, width) : []), [doc, width]);
42
+ const capacity = fileSheetCapacity(rows);
43
+ const maxTop = Math.max(0, lines.length - capacity);
44
+ const prompt = sheet?.prompt ?? '';
45
+ const hint = sheet?.hint ?? '';
46
+ function open(next) {
47
+ setSheet(next);
48
+ setTop(0);
49
+ setNote('');
50
+ setMode(mode);
51
+ }
52
+ function close() {
53
+ setMode('normal');
54
+ setSheet(null);
55
+ }
56
+ function scroll(delta) {
57
+ setTop((t) => Math.max(0, Math.min(maxTop, t + delta)));
58
+ }
59
+ function onKey(input, key) {
60
+ if (key.escape) {
61
+ close();
62
+ }
63
+ else if (key.return) {
64
+ // Si chiude PRIMA di spawnare, come lo sheet della task: la schermata ha
65
+ // finito il suo lavoro, e lasciarla aperta sopra una tab appena nata
66
+ // farebbe credere che ci sia dell'altro da decidere.
67
+ const s = sheet;
68
+ if (s && !s.prompt) {
69
+ // Bersaglio senza azione: la schermata resta aperta e lo dice. Chiuderla
70
+ // senza fare niente sarebbe indistinguibile da uno spawn riuscito.
71
+ setNote(s.hint || '⏎ → nessuna azione su questo bersaglio');
72
+ return;
73
+ }
74
+ close();
75
+ if (s)
76
+ onEnter(s.item, s.prompt);
77
+ }
78
+ else if (key.upArrow) {
79
+ scroll(-1);
80
+ }
81
+ else if (key.downArrow) {
82
+ scroll(1);
83
+ }
84
+ else if (key.pageUp) {
85
+ scroll(-pageStep(capacity));
86
+ }
87
+ else if (key.pageDown) {
88
+ scroll(pageStep(capacity));
89
+ }
90
+ else if (input === 'g') {
91
+ // `g`/`G` sugli estremi, come reader, detail e project status: anche
92
+ // questa schermata scorre testo, quindi eredita lo stesso alfabeto invece
93
+ // di inventarne un secondo.
94
+ setTop(0);
95
+ }
96
+ else if (input === 'G') {
97
+ setTop(maxTop);
98
+ }
99
+ }
100
+ return { sheet, doc, lines, top, capacity, maxTop, prompt, hint, open, onKey, scroll };
101
+ }
@@ -13,6 +13,8 @@ import { cutParts } from './width.js';
13
13
  import { paneTextWidth } from './layout.js';
14
14
  import { SESSION_VIEWS, TASK_VIEWS, } from './pane-views.js';
15
15
  import { INBOX_VIEWS } from './inbox-views.js';
16
+ import { DOC_VIEWS } from './doc-views.js';
17
+ import { WARN } from './glyphs.js';
16
18
  function finish(parts, columns) {
17
19
  const shown = cutParts(parts.map((p) => p.text), paneTextWidth(columns), parts.findIndex((p) => p.active));
18
20
  return { parts, shown };
@@ -80,6 +82,46 @@ export function inboxHeaderParts(counts, active, above, below, columns) {
80
82
  { key: null, text: below > 0 ? ` · ↓${below}` : '', dim: true, active: false },
81
83
  ], columns);
82
84
  }
85
+ /**
86
+ * T160 — header del pane doc, quarto gemello: `Doc` nomina il pane e non si
87
+ * seleziona, poi le cinque viste del catalogo, poi l'avviso di misura parziale,
88
+ * poi `↑N`/`↓N`.
89
+ *
90
+ * L'attenuazione la decide il catalogo (`v.dim(counts)`) come per l'inbox: la
91
+ * voce `Tutti` conta ogni riga dell'albero ma si grigia su quante ne sono
92
+ * flaggate, perché è quello a dire se c'è qualcosa da lanciare.
93
+ *
94
+ * L'avviso `senza cartelle` copre il caso della cascata (P2): il deck esegue lo
95
+ * script dalla cache installata del plugin e può incontrare una versione che
96
+ * emette la sola tabella dei file. L'albero si costruisce lo stesso — le
97
+ * cartelle si deducono dai path — ma i flag di cartella non ci sono, e senza
98
+ * l'avviso un `REGROUP (0)` direbbe «nessuna cartella da riorganizzare» invece
99
+ * di «non l'ho potuto misurare». Sta PRIMA di `↑N`/`↓N` perché è
100
+ * un'informazione sulla misura, e i contatori di scroll sono i primi a cedere il
101
+ * posto su un terminale stretto.
102
+ */
103
+ export function docHeaderParts(counts, active, above, below, columns, hasDirs) {
104
+ const views = DOC_VIEWS.map((v) => ({
105
+ key: v.id,
106
+ text: ` · ${v.label(counts)}`,
107
+ color: v.color,
108
+ dim: v.dim(counts),
109
+ active: v.id === active,
110
+ }));
111
+ return finish([
112
+ { key: null, text: 'Doc', dim: false, active: false },
113
+ ...views,
114
+ {
115
+ key: null,
116
+ text: hasDirs ? '' : ` · ${WARN} senza cartelle`,
117
+ color: 'yellow',
118
+ dim: false,
119
+ active: false,
120
+ },
121
+ { key: null, text: above > 0 ? ` · ↑${above}` : '', dim: true, active: false },
122
+ { key: null, text: below > 0 ? ` · ↓${below}` : '', dim: true, active: false },
123
+ ], columns);
124
+ }
83
125
  /** Le parti COME SONO A SCHERMO — testo tagliato, chiave intatta — nella forma
84
126
  * che `inlineRegions` (`mouse.ts`) misura per l'hit-test. */
85
127
  export function headerItems(h) {
package/dist/sane-note.js CHANGED
@@ -22,24 +22,24 @@
22
22
  // che fallisce se una resta indietro.
23
23
  /**
24
24
  * L'alfabeto ammesso, negato: lettere, cifre, spazio, `_`, `-`, le accentate
25
- * italiane e sei emoji.
25
+ * italiane e sette emoji.
26
26
  *
27
27
  * Whitelist e non blacklist, come in bash: il titolo finisce dentro apici
28
28
  * singoli in `bash -lc "claude --name '…'"`, ed enumerare i caratteri pericolosi
29
29
  * significa sbagliarne uno.
30
30
  *
31
- * Le sei emoji sono nominate una per una e NON come intervallo di code point,
31
+ * Le sette emoji sono nominate una per una e NON come intervallo di code point,
32
32
  * per due ragioni che valgono su entrambi i lati: un'emoji arriva nella tab solo
33
33
  * se il deck la mostra identica in lista, cioè se `sanitize` (`src/width.ts`) la
34
34
  * lascia passare invece di renderla `·`, e quel verdetto si verifica un glifo
35
35
  * alla volta; e l'ordine di un intervallo dentro un bracket expression segue la
36
36
  * collazione della locale, non i code point.
37
37
  *
38
- * Il flag `u` non è cosmetico: senza, le quattro emoji astrali entrerebbero
38
+ * Il flag `u` non è cosmetico: senza, le emoji astrali entrerebbero
39
39
  * nella classe come coppie di surrogati sciolti, e la riduzione lascerebbe
40
40
  * passare mezzo carattere di qualunque altra emoji che ne condivida il primo.
41
41
  */
42
- const OUT_OF_ALPHABET = /[^A-Za-z0-9 _àèéìòùÀÈÉÌÒÙ📐🚀📊🏁🧹📏-]/gu;
42
+ const OUT_OF_ALPHABET = /[^A-Za-z0-9 _àèéìòùÀÈÉÌÒÙ📐🚀📊🏁🧹📏🧭-]/gu;
43
43
  /** Cap in CARATTERI (code point), non in byte: a byte un taglio a metà di una
44
44
  * `à` lascerebbe UTF-8 rotto nel titolo. Largo di proposito — una tab che sfora
45
45
  * tronca a destra da sola, e il limite serve solo a non spingere fuori vista la
@@ -8,14 +8,18 @@
8
8
  // lì da chi ce l'ha in mano, cioè il deck, che a ogni tick materializza le
9
9
  // `Session` complete di titolo e modello.
10
10
  //
11
- // Modulo PURO come `session-list.ts`: nessun I/O, nessun import da ink/react.
12
- // Decide COSA appendere e ritorna i record; il chiamante li scrive. La
13
- // separazione è ciò che rende testabile la parte non ovvia — la convergenza:
14
- // un derivato che non combacia mai col valore su disco produrrebbe un append a
15
- // ogni tick del poll, per sempre, senza nessun errore a dirlo.
11
+ // Il giudizio è separato dalla scrittura: `sidecarGaps` decide e ritorna i
12
+ // record, `fillSidecarGaps` li scrive. La separazione è ciò che rende testabile
13
+ // la parte non ovvia — la convergenza: un derivato che non combacia mai col
14
+ // valore su disco produrrebbe un append a ogni tick del poll, per sempre, senza
15
+ // nessun errore a dirlo.
16
+ //
17
+ // Nessun import da ink/react: il chiamante è un hook, questo modulo no.
16
18
  import { modelAlias } from './glyphs.js';
17
19
  import { stripProjectCore, stripTaskId } from './session-list.js';
18
20
  import { NO_TITLE } from './sessions.js';
21
+ import { NO_SPAWN } from './spawn.js';
22
+ import { appendSessionMeta } from './task-index.js';
19
23
  /**
20
24
  * Cap del titolo scritto nel sidecar, in CODE POINT.
21
25
  *
@@ -62,28 +66,6 @@ export function sidecarTitle(s, core, taskId) {
62
66
  return s.title === NO_TITLE ? '' : capTitle(s.title);
63
67
  return capTitle(stripTaskId(stripProjectCore(s.title, core), taskId));
64
68
  }
65
- /**
66
- * I buchi da riempire in questo giro: per ogni conversazione PINNATA, i campi
67
- * derivati che nel sidecar mancano o non corrispondono più al valore corrente.
68
- *
69
- * Solo le pinnate, e non tutte le conversazioni del progetto, perché sono le
70
- * sole che un lettore esterno mostra: il resto sarebbe churn su un file
71
- * append-only, un record per conversazione per ogni deck che si apre.
72
- *
73
- * Tre cose che non producono un append, e nessuna delle tre è un errore:
74
- * - una pinnata STALE (il transcript non c'è più) — nessuna `Session`, quindi
75
- * nessun valore da cui derivare;
76
- * - un titolo derivato VUOTO — è l'assenza di P1, e appenderlo scriverebbe la
77
- * cancellazione di un campo che nessuno ha chiesto di cancellare;
78
- * - un modello non riconducibile a un alias (`''` o fuori famiglia) — chi
79
- * riprende cadrà sulla propria cascata, che è meglio di un alias inventato.
80
- *
81
- * La CONVERGENZA è l'invariante da tenere: il confronto avviene fra il valore
82
- * derivato e il valore letto dal sidecar senza riscritture in mezzo (nessuna
83
- * sanificazione in lettura, cap applicato da questo lato una volta sola),
84
- * quindi un buco riempito resta riempito e il giro successivo non trova niente
85
- * da fare.
86
- */
87
69
  export function sidecarGaps(input) {
88
70
  const { pinned, sessions, bindings, titles, models, core } = input;
89
71
  if (pinned.size === 0)
@@ -106,3 +88,39 @@ export function sidecarGaps(input) {
106
88
  }
107
89
  return out;
108
90
  }
91
+ /**
92
+ * Scrive i buchi di questo giro. Ritorna quanti record ha appeso — zero è
93
+ * l'esito normale a regime, non un fallimento.
94
+ *
95
+ * `NO_SPAWN` frena anche questa scrittura, ed è lo stesso freno delle tab
96
+ * Ptyxis per la stessa ragione: il gate su pseudo-terminale avvia il DECK VERO
97
+ * con cwd la project root del cappello, quindi senza freno ogni run della suite
98
+ * appende record nel sidecar reale di chi la lancia. Il valore scritto sarebbe
99
+ * pure corretto — è quello che il deck scriverebbe comunque — ma è una
100
+ * scrittura fuori dalla sandbox del test, che è precisamente ciò che il freno
101
+ * esiste per impedire.
102
+ *
103
+ * Il freno sta QUI e non nel chiamante perché questa è l'unica sede della
104
+ * scrittura: nel chiamante sarebbe una guardia che il prossimo scrittore non
105
+ * vede.
106
+ *
107
+ * Un sidecar non scrivibile non è un errore da mostrare: il giro dopo riprova,
108
+ * e nel frattempo il deck funziona identico — questi campi li legge un altro
109
+ * processo, non lui.
110
+ */
111
+ export function fillSidecarGaps(projectRoot, input) {
112
+ if (NO_SPAWN)
113
+ return 0;
114
+ let scritti = 0;
115
+ try {
116
+ for (const gap of sidecarGaps(input)) {
117
+ appendSessionMeta(projectRoot, gap.sessionId, gap);
118
+ scritti++;
119
+ }
120
+ }
121
+ catch {
122
+ // sidecar non scrivibile: si riprova al giro dopo, niente a schermo — è
123
+ // manutenzione di un campo di servizio, non un'azione che l'utente ha chiesto.
124
+ }
125
+ return scritti;
126
+ }
@@ -110,7 +110,7 @@ const UNWRAP_PROMPT = 'lancia md-wrap modo apply su {path} e backup in cartella
110
110
  'verifica risultato e fai rapporto. se le modifiche sono tutte chiaramente safe, ' +
111
111
  'puoi committare direttamente';
112
112
  /**
113
- * Le UNDICI azioni configurabili, nell'ordine in cui la pagina le elenca.
113
+ * Le DODICI azioni configurabili, nell'ordine in cui la pagina le elenca.
114
114
  *
115
115
  * Il perimetro è quello di D1: entra una riga se ha almeno una cella editabile.
116
116
  * Ne restano fuori resume e fork — lì il modello non è una preferenza ma
@@ -123,9 +123,10 @@ const UNWRAP_PROMPT = 'lancia md-wrap modo apply su {path} e backup in cartella
123
123
  * T156 — le emoji dei titoli sono ASTRALI (code point ≥ U+10000) per necessità,
124
124
  * non per gusto: `sanitize` (`src/width.ts`) sostituisce con `·` ogni glifo su
125
125
  * cui le due contabilità di larghezza divergono, e le emoji del BMP a
126
- * presentazione testo divergono sempre. Le stesse sei stanno nella whitelist di
127
- * `_sane_note` in `deck-run` e nel suo gemello `src/sane-note.ts`: un'emoji
128
- * cambiata qui e non là sparisce fra la tabella e la tab, senza errore.
126
+ * presentazione testo divergono sempre. Le stesse SETTE (T160 aggiunge `🧭`)
127
+ * stanno nella whitelist di `_sane_note` in `deck-run` e nel suo gemello
128
+ * `src/sane-note.ts`: un'emoji cambiata qui e non là sparisce fra la tabella e
129
+ * la tab, senza errore.
129
130
  */
130
131
  export const SPAWN_ACTIONS = [
131
132
  {
@@ -258,6 +259,35 @@ export const SPAWN_ACTIONS = [
258
259
  holes: ['path'],
259
260
  fixed: {},
260
261
  },
262
+ {
263
+ // T160 — il rebalance della topologia doc su un bersaglio (file o cartella).
264
+ //
265
+ // La cella MODELLO è fissa, e la ragione mostrata al posto del valore non è
266
+ // una scorciatoia: `rebalance-doc` dichiara `model: sonnet` nel frontmatter,
267
+ // e quel campo ri-timbra il modello della sessione per la durata della
268
+ // skill, qualunque `--model` sia stato passato allo spawn. Una cella
269
+ // editabile qui accetterebbe un valore che non sopravvive all'invocazione —
270
+ // cioè mentirebbe senza produrre nessun errore. Lo stesso vale per le altre
271
+ // tre skill doc (`drain-notions`, `derive-notions`, `align-doc`), che la
272
+ // riga `drain` non ha ancora recepito.
273
+ //
274
+ // DUE buchi per lo stesso dato, e servono entrambi: `{target}` è il path
275
+ // VERO, che la skill risolve contro la colonna `PATH` della misura, e
276
+ // `{words}` lo stesso path reso a parole per il titolo. L'alfabeto di
277
+ // `_sane_note` non ha né `/` né `.`, quindi un `{target}` in un template di
278
+ // titolo uscirebbe con le parole saldate (`runtimereferencedoc-systemmd`) e
279
+ // la tab mostrerebbe un nome diverso da quello che il deck legge dal
280
+ // sidecar.
281
+ id: 'rebalance',
282
+ label: 'rebalance della doc',
283
+ surface: 'pane doc ⏎⏎',
284
+ kind: null,
285
+ title: '🧭 {words}',
286
+ model: 'sonnet',
287
+ prompt: '/loom-works:rebalance-doc {target}',
288
+ holes: ['target', 'words'],
289
+ fixed: { model: 'lo impone il frontmatter della skill' },
290
+ },
261
291
  {
262
292
  // T121 — il recap di progetto, headless (`claude -p`): nessuna tab, quindi
263
293
  // nessun titolo da configurare.
@@ -289,6 +319,12 @@ export const EXAMPLE_HOLES = {
289
319
  slug: 'esempio di task',
290
320
  file: 'T42-nozioni',
291
321
  path: 'runtime reference',
322
+ // T160 — i due buchi della riga `rebalance`, che portano lo stesso dato in due
323
+ // alfabeti: il path vero per il prompt della skill, le parole per il titolo
324
+ // della tab. L'esempio li mostra entrambi nella forma in cui arrivano davvero,
325
+ // o chi scrive un template li scambierebbe senza accorgersene.
326
+ target: 'runtime/reference/doc-system',
327
+ words: 'runtime reference doc-system',
292
328
  };
293
329
  /**
294
330
  * I valori sono quelli che i FORNITORI dei buchi producono davvero, non testo
@@ -0,0 +1,85 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // T160 — il pane dell'albero doc, che in modo doc occupa lo slot SINISTRO.
3
+ //
4
+ // Risponde a una domanda sola, ed è la ragione della sua forma: **vale la pena
5
+ // lanciare `rebalance-doc` adesso, e su cosa?** Da qui la colonna dei flag in
6
+ // testa, dove si scorre con l'occhio, e il nome come unica cella elastica.
7
+ //
8
+ // Le CIFRE non stanno in riga (P6): char, lunghezza del TLDR e conteggio file
9
+ // vivono nel blocco preview, insieme ai nomi interi dei flag. Su un pane largo la
10
+ // metà del terminale ogni colonna fissa la paga il nome, che è l'unica cosa con
11
+ // cui si riconosce un file — la stessa scelta già fatta per il pane inbox.
12
+ //
13
+ // Presentazionale puro: riceve la vista già selezionata e le larghezze già
14
+ // calcolate, non le deriva.
15
+ import { Box, Text } from 'ink';
16
+ import { cut } from '../width.js';
17
+ import { paneTextWidth } from '../layout.js';
18
+ import { CARET, CARET_OFF } from '../glyphs.js';
19
+ import { DOC_FLAG_W, flagCell } from '../doc-tree.js';
20
+ import { docView } from '../doc-views.js';
21
+ import { docHeaderParts } from '../pane-header.js';
22
+ import { HeaderLine } from './header-line.js';
23
+ /**
24
+ * Rientro di un livello dell'albero: UNA colonna, non due.
25
+ *
26
+ * L'albero della doc arriva a quattro livelli (`runtime/reference/loom-deck/
27
+ * sessions/`), e a due colonne per livello il rientro mangerebbe otto colonne
28
+ * della cella nome proprio sulle righe più profonde — che su un pane a 80
29
+ * colonne sono la metà di ciò che resta. Una colonna basta a vedere la
30
+ * gerarchia, perché le cartelle stanno comunque prima dei loro file.
31
+ */
32
+ const INDENT_W = 1;
33
+ /** I flag che chiamano un'operazione di `rebalance-doc`: la riga che ne porta
34
+ * almeno uno è quella per cui il pane esiste, e si legge accesa. */
35
+ const ACTIONABLE = new Set(['SPLIT', 'MERGE?', 'REGROUP']);
36
+ /** Quelli che chiamano `write-tldr`: un difetto vero, ma di un'altra skill. */
37
+ const TLDR_FLAGS = new Set(['TLDR>CAP', 'NOTLDR', 'TLDR-ORFANA']);
38
+ /** Il colore della cella flag, dal flag più urgente che la riga porta. Un colore
39
+ * solo per cella e non uno per short: la cella è un `Text` unico, e stilarne i
40
+ * pezzi costerebbe tre nodi annidati per riga su una lista lunga quanto la doc. */
41
+ function flagColor(row) {
42
+ if (row.flags.some((f) => ACTIONABLE.has(f)))
43
+ return 'yellow';
44
+ if (row.flags.some((f) => TLDR_FLAGS.has(f)))
45
+ return 'cyan';
46
+ return undefined;
47
+ }
48
+ /**
49
+ * Header del pane doc: le parti e il loro taglio vengono da `pane-header.ts`, lo
50
+ * stesso modulo da cui `frame.ts` ricava le colonne cliccabili.
51
+ */
52
+ export function DocHeader({ counts, active, above, below, focused, columns, hasDirs, }) {
53
+ const { parts, shown } = docHeaderParts(counts, active, above, below, columns, hasDirs);
54
+ return _jsx(HeaderLine, { parts: parts, shown: shown, focused: focused });
55
+ }
56
+ export function DocPane({ rows, counts, activeView, paneCount, selectedPath, focused, above, below, columns, ok, scanned, scanning, hasDirs, }) {
57
+ const width = paneTextWidth(columns);
58
+ // Cella elastica per sottrazione dalle colonne fisse, come nel pane inbox e
59
+ // nella lista sessioni. Pavimento 0 e non un minimo di cortesia: è un tetto,
60
+ // non una preferenza, e alzarlo sopra lo spazio reale fa uscire la riga dal
61
+ // pane mangiandone il bordo (invariante ③ di width.ts).
62
+ const nameW = Math.max(0, width - (2 /* caret */ + DOC_FLAG_W + 1 /* gutter */));
63
+ return (_jsxs(Box, { flexDirection: "column", width: "50%", marginRight: 1, borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', paddingX: 1, children: [_jsx(DocHeader, { counts: counts, active: activeView, above: above, below: below, focused: focused, columns: columns, hasDirs: hasDirs }), paneCount === 0 ? (
64
+ // Quattro vuoti diversi, e dirlo è il punto. Ai tre del pane inbox se ne
65
+ // aggiunge uno che solo questo scan ha: «sto misurando». Quattro secondi
66
+ // di wall clock sono abbastanza da vedersi, e un pane vuoto per quattro
67
+ // secondi si legge come una doc senza file.
68
+ _jsx(Text, { color: "yellow", wrap: "truncate-end", children: cut(scanning
69
+ ? 'misura in corso…'
70
+ : !scanned
71
+ ? 'albero non ancora misurato'
72
+ : !ok
73
+ ? 'misura della doc fallita: plugin assente o script in errore'
74
+ : docView(activeView).empty, width) })) : (rows.map((r) => {
75
+ const sel = r.path === selectedPath;
76
+ const indent = ' '.repeat(r.depth * INDENT_W);
77
+ // La barra finale distingue una cartella da un file senza spendere una
78
+ // colonna di marcatore: è la convenzione di `ls -F`, e su una riga che
79
+ // porta già una colonna di flag un glifo in più sarebbe la terza cosa
80
+ // da leggere prima del nome.
81
+ const label = r.kind === 'dir' ? `${r.name}/` : r.name;
82
+ const text = cut(indent + label, nameW);
83
+ return (_jsxs(Text, { inverse: sel && focused, bold: sel && !focused, wrap: "truncate-end", children: [sel ? CARET : CARET_OFF, _jsx(Text, { color: flagColor(r), dimColor: r.flags.length === 0, children: flagCell(r.flags) }), ' ', _jsx(Text, { bold: r.kind === 'dir', dimColor: r.kind === 'file' && r.flags.length === 0, children: text })] }, r.path));
84
+ }))] }));
85
+ }
@@ -0,0 +1,58 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // T134/T160 — il detail di un FILE: testo scorrevole più la riga che dichiara
3
+ // cosa partirà su `⏎`.
4
+ //
5
+ // Una schermata per le due istanze di `useFileSheet` (coda inbox e bersaglio
6
+ // doc). Quello che cambia fra loro è la TESTATA — tre celle di testo, che i due
7
+ // `…SheetHead` qui sotto compongono — e non la struttura: stesse righe fisse,
8
+ // stessa cornice, stesso `FILE_SHEET_CHROME` nel budget d'altezza. Due
9
+ // componenti avrebbero duplicato la cornice e il conteggio delle righe fisse,
10
+ // che è esattamente ciò che fa sfondare `rows` quando una delle due copie
11
+ // guadagna una riga e l'altra no.
12
+ //
13
+ // Riusa `DetailLine` del detail della task invece di ri-renderizzare il markdown
14
+ // per conto proprio: sono lo stesso costrutto — un documento reso in span
15
+ // tipizzati dentro un box — e due rese dello stesso markdown divergerebbero al
16
+ // primo stile aggiunto. L'evidenziazione della ricerca non serve qui, quindi le
17
+ // occorrenze arrivano vuote e il secondo taglio di `DetailLine` non gira nemmeno.
18
+ import { Box, Text } from 'ink';
19
+ import { cut } from '../width.js';
20
+ import { DetailLine } from './detail-screen.js';
21
+ import { INBOX_MARK, WARN, fmtDateTime, fmtSize } from '../glyphs.js';
22
+ import { NATURA_SHORT, inboxMark } from '../inbox.js';
23
+ import { flagNames } from '../doc-tree.js';
24
+ export function inboxSheetHead(file) {
25
+ const mark = inboxMark(file);
26
+ return {
27
+ tag: NATURA_SHORT[file.natura],
28
+ name: file.basename,
29
+ meta: `${INBOX_MARK[mark] ? `${INBOX_MARK[mark]} ` : ''}${mark}` +
30
+ `${file.branch ? ` ⟨${file.branch}⟩` : ''}` +
31
+ `${file.cappello ? ` · ${file.cappello}` : ''} · ${file.nozioni} nozioni · ` +
32
+ `${file.aperte} aperte · ${fmtSize(file.chars)} · ${fmtDateTime(file.created * 1000)}`,
33
+ };
34
+ }
35
+ /**
36
+ * T160 — la testata del bersaglio doc.
37
+ *
38
+ * La meta cambia di natura fra file e cartella e non è un ramo di cortesia: il
39
+ * `char` di un file è la sua lunghezza, quello di una cartella la somma dei
40
+ * suoi, e le due si confrontano con due soglie diverse. Mostrarle sotto la
41
+ * stessa etichetta le farebbe leggere come la stessa misura.
42
+ */
43
+ export function docSheetHead(row) {
44
+ const flags = flagNames(row.flags);
45
+ return {
46
+ tag: row.kind === 'dir' ? 'dir' : 'file',
47
+ name: row.path,
48
+ meta: (row.kind === 'dir'
49
+ ? `${row.files} file · ${fmtSize(row.chars)} in cartella`
50
+ : `${fmtSize(row.chars)} · TLDR ${row.tldr || 'assente'}`) +
51
+ ` · ${flags.length > 0 ? flags.join(' · ') : 'nessun flag'}`,
52
+ };
53
+ }
54
+ export function FileSheetScreen({ head, missing, lines, spans, top, total, capacity, prompt, hint, columns, }) {
55
+ const last = Math.min(total, top + capacity);
56
+ const width = Math.max(20, (columns || 80) - 4);
57
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: [_jsx(Text, { color: "cyan", children: head.tag }), " ", cut(head.name, Math.max(10, width - 8))] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [head.meta, missing ? '' : ` · righe ${total === 0 ? 0 : top + 1}-${last} di ${total}`] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "yellow", children: "\u2191\u2193/PgUp/PgDn" }), " testo \u00B7 ", _jsx(Text, { color: "yellow", children: "g/G" }), " estremi \u00B7", ' ', _jsx(Text, { color: "yellow", children: "\u23CE" }), " apre la sessione \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " chiude"] }), _jsx(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, marginTop: 1, children: missing ? (_jsxs(Text, { color: "yellow", wrap: "truncate-end", children: [WARN, " non leggibile \u00B7 l'azione resta attiva (la skill risolve il bersaglio per nome)"] })) : (lines.map((l, i) => (_jsx(DetailLine, { line: l, spans: spans, occ: [], current: -1 }, top + i)))) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { dimColor: true, children: "\u23CE \u203A " }), _jsx(Text, { color: prompt ? 'green' : 'yellow', children: cut(prompt || hint || 'nessuna azione su questo bersaglio', Math.max(10, width - 4)) })] }) })] }));
58
+ }
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // La resa di un header di pane già tagliato: ogni parte col suo stile, l'attiva
3
+ // in video inverso.
4
+ //
5
+ // T160 · D1 — esce da `ui/panes.tsx` insieme ai pane che ne uscivano, perché è
6
+ // ciò che tutti e quattro condividono. Il vincolo di `coding-standards`
7
+ // §Come si splitta un file ricresciuto è che nessun modulo estratto importi il
8
+ // file da cui è stato estratto: lasciando `HeaderLine` in `panes.tsx`, il pane
9
+ // inbox e il pane doc avrebbero dovuto importarlo da lì — cioè la dipendenza
10
+ // circolare che il vincolo esiste per vietare. Sta quindi in un file proprio, e
11
+ // la dipendenza va in un verso solo: i quattro pane importano questo, questo non
12
+ // importa nessuno di loro.
13
+ //
14
+ // Il video inverso e non un colore (T100/D5): costa 0 colonne e non entra in
15
+ // gara con la semantica di colore già occupata dalle voci del catalogo.
16
+ import { Text } from 'ink';
17
+ export function HeaderLine({ parts, shown, focused, }) {
18
+ return (_jsx(Text, { bold: true, color: focused ? 'cyan' : undefined, wrap: "truncate-end", children: parts.map((seg, i) => shown[i] ? (_jsx(Text, { color: seg.color, dimColor: seg.dim, inverse: seg.active, children: shown[i] }, i)) : null) }));
19
+ }
@@ -0,0 +1,83 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // T134 — il pane della coda inbox, che in modo doc occupa lo slot DESTRO.
3
+ //
4
+ // T160 · D1 — estratto da `ui/panes.tsx` insieme al pane doc, per la ragione
5
+ // scritta in `coding-standards` §Un criterio verificato una volta non è un
6
+ // custode: quel file era già stato splittato e stava ricrescendo. L'asse dello
7
+ // split è per PANE, non per livello — ogni pane porta il proprio header, la
8
+ // propria riga e la propria nota di vuoto, e sono le tre cose che si toccano
9
+ // insieme. Ciò che i quattro condividono (`HeaderLine`) sta in
10
+ // `ui/header-line.tsx`, e nessuno dei moduli estratti importa `ui/panes.tsx`.
11
+ //
12
+ // Presentazionale puro: riceve la vista già selezionata e le larghezze già
13
+ // calcolate, non le deriva.
14
+ import { Box, Text } from 'ink';
15
+ import { cut, pad, sanitize, termWidth } from '../width.js';
16
+ import { paneTextWidth } from '../layout.js';
17
+ import { CARET, CARET_OFF, INBOX_MARK, INBOX_MARK_W, relTime } from '../glyphs.js';
18
+ import { NATURA_SHORT, NATURA_W, inboxMark } from '../inbox.js';
19
+ import { inboxView } from '../inbox-views.js';
20
+ import { inboxHeaderParts } from '../pane-header.js';
21
+ import { HeaderLine } from './header-line.js';
22
+ /**
23
+ * Header del pane inbox: le parti e il loro taglio vengono da `pane-header.ts`,
24
+ * lo stesso modulo da cui `frame.ts` ricava le colonne cliccabili — una fonte
25
+ * sola, non due conti paralleli che divergono alla prima voce che cambia testo.
26
+ */
27
+ export function InboxHeader({ counts, active, above, below, focused, columns, }) {
28
+ const { parts, shown } = inboxHeaderParts(counts, active, above, below, columns);
29
+ return _jsx(HeaderLine, { parts: parts, shown: shown, focused: focused });
30
+ }
31
+ /**
32
+ * T134 — il pane inbox, montato nello slot destro dal modo doc (D6).
33
+ *
34
+ * Mostra SEMPRE TUTTO, senza filtro sulla task selezionata a sinistra (D7
35
+ * preflight, che supersede la clausola di D6 congelate): niente righe meta,
36
+ * nessun trattamento speciale per il cappello vuoto o per un cappello che nomina
37
+ * una task non più in `tasks.md`. Ne discende che la colonna `CAPPELLO` di
38
+ * `doc-metrics` non serve al filtro — resta nel blocco preview, dove dice
39
+ * qualcosa senza costare una colonna su ogni riga.
40
+ *
41
+ * La riga porta marcatori, branch e nome (D8). Non le cifre (nozioni, aperte,
42
+ * char): stanno nella preview, e su un pane largo la metà del terminale ogni
43
+ * colonna fissa la paga la cella elastica del nome — che è l'unica cosa con cui
44
+ * si riconosce un file.
45
+ */
46
+ export function InboxPane({ files, counts, activeView, paneCount, selectedPath, focused, above, below, columns, ok, scanned, }) {
47
+ const width = paneTextWidth(columns);
48
+ // La cella elastica si calcola per SOTTRAZIONE dalle colonne fisse, come il
49
+ // titolo della lista sessioni: pavimento 0 e non un minimo di cortesia — è un
50
+ // tetto, non una preferenza, e alzarlo sopra lo spazio reale fa uscire la riga
51
+ // dal pane mangiandone il bordo (invariante ③ di width.ts).
52
+ const ageW = 3;
53
+ const nameW = Math.max(0, width -
54
+ (2 /* caret */ +
55
+ NATURA_W +
56
+ 1 /* gutter */ +
57
+ INBOX_MARK_W +
58
+ 1 /* gutter */ +
59
+ 1 /* gutter prima della data */ +
60
+ ageW));
61
+ return (_jsxs(Box, { flexDirection: "column", width: "50%", borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', paddingX: 1, children: [_jsx(InboxHeader, { counts: counts, active: activeView, above: above, below: below, focused: focused, columns: columns }), paneCount === 0 ? (
62
+ // Tre vuoti diversi, e dirlo è il punto: «non ho ancora misurato» non è
63
+ // «ho misurato e non c'è niente», e «la misura si è rotta» non è nessuno
64
+ // dei due (D2 preflight). Una lista vuota che non dice quale dei tre è
65
+ // si legge come un pane rotto.
66
+ _jsx(Text, { color: "yellow", wrap: "truncate-end", children: cut(!scanned
67
+ ? 'coda non ancora misurata'
68
+ : !ok
69
+ ? 'misura della coda fallita: plugin assente o script in errore'
70
+ : inboxView(activeView).empty, width) })) : (files.map((f) => {
71
+ const sel = f.path === selectedPath;
72
+ const mark = inboxMark(f);
73
+ // Il branch sta DENTRO la cella del nome, non in una colonna propria:
74
+ // è raro, e una colonna dedicata costerebbe spazio vuoto su ogni riga
75
+ // non-branchata di un pane largo la metà del terminale. Stessa scelta
76
+ // del marcatore di fork nella lista sessioni.
77
+ const branchMark = f.branch ? `⟨${f.branch}⟩ ` : '';
78
+ const inner = Math.max(0, nameW - termWidth(sanitize(branchMark)));
79
+ const name = cut(f.basename.replace(/\.md$/, ''), inner);
80
+ const age = relTime(f.created * 1000);
81
+ return (_jsxs(Text, { inverse: sel && focused, bold: sel && !focused, wrap: "truncate-end", children: [sel ? CARET : CARET_OFF, _jsx(Text, { color: mark === 'broken' ? 'red' : mark === 'queued' ? 'green' : undefined, dimColor: mark === 'held' || mark === 'branched', children: pad(NATURA_SHORT[f.natura], NATURA_W) }), ' ', _jsx(Text, { color: mark === 'branched' ? 'yellow' : undefined, children: pad(INBOX_MARK[mark], INBOX_MARK_W) }), ' ', branchMark ? _jsx(Text, { color: "yellow", children: sanitize(branchMark) }) : null, _jsx(Text, { dimColor: mark !== 'queued', children: name }), ' '.repeat(Math.max(0, inner - termWidth(name))), ' ', _jsx(Text, { dimColor: true, children: pad(age, ageW, 'right') })] }, f.path));
82
+ }))] }));
83
+ }