@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.
package/dist/ui/panes.js CHANGED
@@ -1,18 +1,26 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- // I due pane della vista principale — task e sessioni — con i rispettivi
2
+ // I due pane del modo TASK — lista task e conversazioni — con i rispettivi
3
3
  // header. Sono presentazionali puri: ricevono la vista già selezionata e le
4
4
  // larghezze già calcolate, non li derivano.
5
+ //
6
+ // T160 · D1 — i pane dell'altro modo sono usciti di qui: la coda inbox in
7
+ // `ui/inbox-pane.tsx`, l'albero doc in `ui/doc-pane.tsx`, e ciò che tutti e
8
+ // quattro condividono (`HeaderLine`) in `ui/header-line.tsx`. L'asse dello split
9
+ // è per PANE e non per livello: ogni pane porta il proprio header, la propria
10
+ // riga e la propria nota di vuoto, e sono le tre cose che si toccano insieme.
11
+ // Nessuno dei tre moduli estratti importa questo file — la dipendenza va in un
12
+ // verso solo, ed è verificabile a grep (`coding-standards` §Come si splitta un
13
+ // file ricresciuto).
5
14
  import { Box, Text } from 'ink';
6
15
  import { cut, pad, sanitize, termWidth } from '../width.js';
7
16
  import { paneTextWidth } from '../layout.js';
8
- import { CARET, CARET_OFF, INBOX_MARK, INBOX_MARK_W, LIVE_BUSY, LIVE_IDLE, LIVE_NONE, MODEL_W, SID_CHARS, TASK_EMPTY, WARN, metaCount, modelShort, relTime, } from '../glyphs.js';
9
- import { NATURA_SHORT, NATURA_W, inboxMark } from '../inbox.js';
10
- import { inboxView, } from '../inbox-views.js';
17
+ import { CARET, CARET_OFF, LIVE_BUSY, LIVE_IDLE, LIVE_NONE, MODEL_W, SID_CHARS, TASK_EMPTY, WARN, metaCount, modelShort, relTime, } from '../glyphs.js';
11
18
  import { TaskRow } from './task-row.js';
19
+ import { HeaderLine } from './header-line.js';
12
20
  import { META_ROWS, ROW_ALL, ROW_SPOT } from '../model.js';
13
21
  import { rowLabel, sessionTitle } from '../session-list.js';
14
22
  import { sessionView, taskView, } from '../pane-views.js';
15
- import { inboxHeaderParts, sessionHeaderParts, taskHeaderParts, } from '../pane-header.js';
23
+ import { sessionHeaderParts, taskHeaderParts } from '../pane-header.js';
16
24
  import { describeSort, PRI_ENTRIES, PROG_ENTRIES, } from '../view.js';
17
25
  /**
18
26
  * Header del pane task, tagliato QUI e non da Ink — stesso motivo del gemello
@@ -39,12 +47,6 @@ export function TasksHeader({ counts, active, above, below, focused, columns, })
39
47
  const { parts, shown } = taskHeaderParts(counts, active, above, below, columns);
40
48
  return _jsx(HeaderLine, { parts: parts, shown: shown, focused: focused });
41
49
  }
42
- /** Resa di un header già tagliato: ogni parte col suo stile, l'attiva in
43
- * video inverso (T100/D5 — costa 0 colonne e non entra in gara con la
44
- * semantica di colore già occupata). */
45
- function HeaderLine({ parts, shown, focused, }) {
46
- 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) }));
47
- }
48
50
  export function TasksPane({ tasks, counts, activeView, paneCount, view, selected, spotCount, allCount, idW, tailW, focused, loadError, windowStart, above, below, columns, data, blockMark, }) {
49
51
  const allSelected = selected === ROW_ALL;
50
52
  const spotSelected = selected === ROW_SPOT;
@@ -85,69 +87,6 @@ export function SessionsHeader({ parentLabel, counts, active, above, below, focu
85
87
  const { parts, shown } = sessionHeaderParts(parentLabel, counts, active, above, below, columns);
86
88
  return _jsx(HeaderLine, { parts: parts, shown: shown, focused: focused });
87
89
  }
88
- /**
89
- * T134 — header del pane inbox, terzo gemello dei due sopra: le parti e il loro
90
- * taglio vengono da `pane-header.ts`, lo stesso modulo da cui `frame.ts` ricava
91
- * le colonne cliccabili.
92
- */
93
- export function InboxHeader({ counts, active, above, below, focused, columns, }) {
94
- const { parts, shown } = inboxHeaderParts(counts, active, above, below, columns);
95
- return _jsx(HeaderLine, { parts: parts, shown: shown, focused: focused });
96
- }
97
- /**
98
- * T134 — il pane inbox, alternativo a quello delle sessioni nello slot destro
99
- * (D6). Uno dei due è montato e l'altro non esiste: `^B` li scambia.
100
- *
101
- * Mostra SEMPRE TUTTO, senza filtro sulla task selezionata a sinistra (D7
102
- * preflight, che supersede la clausola di D6 congelate): niente righe meta,
103
- * nessun trattamento speciale per il cappello vuoto o per un cappello che
104
- * nomina una task non più in `tasks.md`. Ne discende che la colonna `CAPPELLO`
105
- * di `doc-metrics` non serve al filtro — resta nel blocco preview, dove dice
106
- * qualcosa senza costare una colonna su ogni riga.
107
- *
108
- * La riga porta marcatori, branch e nome (D8). Non le cifre (nozioni, aperte,
109
- * char): stanno nella preview, e su un pane largo la metà del terminale ogni
110
- * colonna fissa la paga la cella elastica del nome — che è l'unica cosa con cui
111
- * si riconosce un file.
112
- */
113
- export function InboxPane({ files, counts, activeView, paneCount, selectedPath, focused, above, below, columns, ok, scanned, }) {
114
- const width = paneTextWidth(columns);
115
- // La cella elastica si calcola per SOTTRAZIONE dalle colonne fisse, come il
116
- // titolo della lista sessioni: pavimento 0 e non un minimo di cortesia — è un
117
- // tetto, non una preferenza, e alzarlo sopra lo spazio reale fa uscire la riga
118
- // dal pane mangiandone il bordo (invariante ③ di width.ts).
119
- const ageW = 3;
120
- const nameW = Math.max(0, width -
121
- (2 /* caret */ +
122
- NATURA_W +
123
- 1 /* gutter */ +
124
- INBOX_MARK_W +
125
- 1 /* gutter */ +
126
- 1 /* gutter prima della data */ +
127
- ageW));
128
- 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 ? (
129
- // Tre vuoti diversi, e dirlo è il punto: «non ho ancora misurato» non è
130
- // «ho misurato e non c'è niente», e «la misura si è rotta» non è nessuno
131
- // dei due (D2 preflight). Una lista vuota che non dice quale dei tre è
132
- // si legge come un pane rotto.
133
- _jsx(Text, { color: "yellow", wrap: "truncate-end", children: cut(!scanned
134
- ? 'coda non ancora misurata'
135
- : !ok
136
- ? 'misura della coda fallita: plugin assente o script in errore'
137
- : inboxView(activeView).empty, width) })) : (files.map((f) => {
138
- const sel = f.path === selectedPath;
139
- const mark = inboxMark(f);
140
- // Il branch sta DENTRO la cella del nome, non in una colonna propria:
141
- // è raro, e una colonna dedicata costerebbe spazio vuoto su ogni riga
142
- // non-branchata di un pane largo la metà del terminale. Stessa scelta
143
- // del marcatore di fork nella lista sessioni.
144
- const branchMark = f.branch ? `⟨${f.branch}⟩ ` : '';
145
- const inner = Math.max(0, nameW - termWidth(sanitize(branchMark)));
146
- const name = cut(f.basename.replace(/\.md$/, ''), inner);
147
- const age = relTime(f.created * 1000);
148
- 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));
149
- }))] }));
150
- }
151
90
  export function SessionsPane({ parentLabel, isSpot, isAll, bindings, taskW, ageW, rows, counts, activeView, paneCount, selectedId, focused, above, below, columns, forkOf, sessionNotes, sessionPriority, projectCore, live, }) {
152
91
  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 ? (
153
92
  // T100 — la nota della vista di default resta quella storica, che nomina
@@ -6,6 +6,7 @@ import { cut, cutParts, wrapLines } from '../width.js';
6
6
  import { previewTextWidth, previewTitleParts } from '../layout.js';
7
7
  import { LIVE_BUSY, LIVE_IDLE, META_KEYS, fmtDateTime, fmtSize, modelAlias, } from '../glyphs.js';
8
8
  import { NATURA_SHORT, inboxMark } from '../inbox.js';
9
+ import { flagNames } from '../doc-tree.js';
9
10
  import { ChoiceRow } from './fields.js';
10
11
  import { MODELS } from '../spawn-catalog.js';
11
12
  /** Colonne del prefisso delle due anteprime della preview sessione (`» `, `« `,
@@ -14,7 +15,7 @@ import { MODELS } from '../spawn-catalog.js';
14
15
  * un bordo mangiato. */
15
16
  const PREVIEW_PREFIX_W = 2;
16
17
  export function PreviewPane(p) {
17
- return (_jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: p.kind === 'task' ? (_jsx(TaskPreview, { detail: p.detail, maxLines: p.maxLines, columns: p.columns })) : p.kind === 'inbox' ? (_jsx(InboxPreview, { file: p.file, columns: p.columns })) : (_jsx(SessionPreview, { s: p.s, firstLines: p.firstLines, lastLines: p.lastLines, columns: p.columns, origin: p.origin, note: p.note, live: p.live, resumeModel: p.resumeModel })) }));
18
+ return (_jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: p.kind === 'task' ? (_jsx(TaskPreview, { detail: p.detail, maxLines: p.maxLines, columns: p.columns })) : p.kind === 'inbox' ? (_jsx(InboxPreview, { file: p.file, columns: p.columns })) : p.kind === 'doc' ? (_jsx(DocPreview, { row: p.row, columns: p.columns })) : (_jsx(SessionPreview, { s: p.s, firstLines: p.firstLines, lastLines: p.lastLines, columns: p.columns, origin: p.origin, note: p.note, live: p.live, resumeModel: p.resumeModel })) }));
18
19
  }
19
20
  /**
20
21
  * T134 — corpo della preview inbox: due righe FISSE, contate da
@@ -40,6 +41,29 @@ export function InboxPreview({ file, columns }) {
40
41
  : 'in coda: una skill può prenderlo da sola';
41
42
  return (_jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: [_jsx(Text, { color: "cyan", children: NATURA_SHORT[file.natura] }), ' ', cut(file.basename, Math.max(10, width - 8))] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [file.nozioni, " nozioni \u00B7 ", file.aperte, " aperte \u00B7 ", fmtSize(file.chars), " \u00B7", ' ', fmtDateTime(file.created * 1000), file.cappello ? ` · ${file.cappello}` : '', file.indexed ? ' · indexed' : '', " \u00B7 ", state] })] }));
42
43
  }
44
+ /**
45
+ * T160 — corpo della preview doc: due righe FISSE, contate da
46
+ * `DOC_DETAIL_FIXED`. Ogni riga aggiunta va scalata lì, o il frame sfonda `rows`
47
+ * e Ink passa a `clearTerminal`.
48
+ *
49
+ * Porta le due cose che la riga di lista non può (P6): il PATH intero — in lista
50
+ * l'albero mostra l'ultimo segmento e le viste piatte troncano — e i NOMI INTERI
51
+ * dei flag accanto alle cifre che li spiegano. Le short di riga dicono quale
52
+ * flag; qui si legge perché ce l'ha, che è il dato su cui si decide se lanciare
53
+ * l'operazione.
54
+ *
55
+ * La riga meta cambia di natura fra file e cartella e non è un ramo di
56
+ * cortesia: `char` di un file è la sua lunghezza, `char` di una cartella è la
57
+ * somma dei suoi — due misure che si confrontano con due soglie diverse, e
58
+ * mostrarle con la stessa etichetta le farebbe leggere come la stessa cosa.
59
+ */
60
+ export function DocPreview({ row, columns }) {
61
+ const width = previewTextWidth(columns);
62
+ const names = flagNames(row.flags);
63
+ return (_jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: [_jsx(Text, { color: "cyan", children: row.kind === 'dir' ? 'dir ' : 'file' }), ' ', cut(row.path, Math.max(10, width - 6))] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [row.kind === 'dir'
64
+ ? `${row.files} file · ${fmtSize(row.chars)} in cartella`
65
+ : `${fmtSize(row.chars)} · TLDR ${row.tldr || 'assente'}`, ' · ', names.length > 0 ? names.join(' · ') : 'nessun flag: in equilibrio'] })] }));
66
+ }
43
67
  // T49 — corpo della preview sessione. Tutti i campi vengono dal parse già
44
68
  // cached dell'adapter (mtime-keyed): non costa I/O al movimento di selezione.
45
69
  // Mostra "da dove parte, dove è arrivata": il primo prompt utente (`» `) e
@@ -34,7 +34,7 @@ import { taskColumns } from '../view.js';
34
34
  import { AssignScreen } from './assign-screen.js';
35
35
  import { DetailScreen } from './detail-screen.js';
36
36
  import { StatusScreen } from './status-screen.js';
37
- import { InboxScreen } from './inbox-screen.js';
37
+ import { FileSheetScreen, docSheetHead, inboxSheetHead } from './file-sheet-screen.js';
38
38
  import { WrapScreen } from './wrap-screen.js';
39
39
  import { SpawnScreen } from './spawn-screen.js';
40
40
  import { ReaderScreen, SearchScreen } from './search-screen.js';
@@ -108,7 +108,7 @@ export function HintBar({ mode, purge, keyLegend, indicators, columns, }) {
108
108
  */
109
109
  export function screenFor(input) {
110
110
  const { mode, rows, columns, note, overlays } = input;
111
- const { assign, sheet, search, status, inbox, wrap, spawn } = overlays;
111
+ const { assign, sheet, search, status, inbox, doc, wrap, spawn } = overlays;
112
112
  // ── T57 · schermata di assegnazione ─────────────────────────────────────
113
113
  // Sostitutiva come ricerca e reader (D3): la lista task non entra in un box
114
114
  // sopra i due pane, e prendendo l'intero frame non costa nulla al loro budget.
@@ -169,12 +169,24 @@ export function screenFor(input) {
169
169
  // nozioni supera i 28KB, cioè è più lungo di ogni task file del progetto.
170
170
  if (mode === 'inbox' && inbox.sheet) {
171
171
  if (isCompact(inbox.capacity)) {
172
- return (_jsx(CompactNotice, { what: inbox.sheet.file.basename, esc: "chiude", rows: rows, columns: columns }));
172
+ return (_jsx(CompactNotice, { what: inbox.sheet.item.basename, esc: "chiude", rows: rows, columns: columns }));
173
173
  }
174
174
  // Il clamp serve anche qui: un resize può accorciare il testo sotto uno
175
175
  // scroll già dato.
176
176
  const start = Math.min(inbox.top, inbox.maxTop);
177
- return (_jsx(InboxScreen, { file: inbox.sheet.file, missing: inbox.sheet.text === null, lines: inbox.lines.slice(start, start + inbox.capacity), spans: inbox.doc?.spans ?? [], top: start, total: inbox.lines.length, capacity: inbox.capacity, prompt: inbox.prompt, columns: columns }));
177
+ return (_jsx(FileSheetScreen, { head: inboxSheetHead(inbox.sheet.item), missing: inbox.sheet.text === null, lines: inbox.lines.slice(start, start + inbox.capacity), spans: inbox.doc?.spans ?? [], top: start, total: inbox.lines.length, capacity: inbox.capacity, prompt: inbox.prompt, hint: inbox.hint, columns: columns }));
178
+ }
179
+ // ── T160 · detail di un bersaglio doc ───────────────────────────────────
180
+ // Nona schermata sostitutiva: la stessa dell'inbox (`useFileSheet`, due
181
+ // istanze) su un altro oggetto. Su una cartella il corpo porta l'elenco dei
182
+ // suoi file al posto di un testo (P10): il gesto è lo stesso, cambia cosa si
183
+ // legge.
184
+ if (mode === 'doc' && doc.sheet) {
185
+ if (isCompact(doc.capacity)) {
186
+ return _jsx(CompactNotice, { what: doc.sheet.item.path, esc: "chiude", rows: rows, columns: columns });
187
+ }
188
+ const start = Math.min(doc.top, doc.maxTop);
189
+ return (_jsx(FileSheetScreen, { head: docSheetHead(doc.sheet.item), missing: doc.sheet.text === null, lines: doc.lines.slice(start, start + doc.capacity), spans: doc.doc?.spans ?? [], top: start, total: doc.lines.length, capacity: doc.capacity, prompt: doc.prompt, hint: doc.hint, columns: columns }));
178
190
  }
179
191
  // ── T134 · lista hard-wrap ──────────────────────────────────────────────
180
192
  // Settima schermata sostitutiva: sul cappello lo scan trova 77 `WRAP` e 147
package/dist/viewport.js CHANGED
@@ -29,12 +29,17 @@ const MAX_DETAIL_LINES = 6;
29
29
  // a un pane va scalata qui, o il frame sfonda `rows` e Ink passa a
30
30
  // clearTerminal (frame-fantasma nello scrollback di VTE).
31
31
  const TASKS_PANE_CHROME = 6; // 2 bordi + header "Tasks (n)" + riga sort + 2 righe meta
32
- // T134 — vale per ENTRAMBI i pane dello slot destro: il pane inbox ha la stessa
33
- // cornice (2 bordi + header) e uno solo dei due è montato alla volta, quindi
34
- // `Budget.sessionRows` è la capienza del pane destro qualunque esso sia. Se un
35
- // giorno l'inbox guadagnasse una riga fissa, questa costante deve sdoppiarsi e
36
- // il budget guadagnare un ramo: una riga fissa non contata è ciò che fa sfondare
37
- // `rows` e manda Ink nel ramo `clearTerminal`.
32
+ // T134/T160 — vale per TRE pane su quattro: conversazioni, coda inbox e albero
33
+ // doc hanno la stessa cornice (2 bordi + header) e ne sono montati al massimo
34
+ // due alla volta, quindi `Budget.sessionRows` è la capienza di ognuno di loro.
35
+ // Solo il pane task ha una cornice propria (`TASKS_PANE_CHROME`), perché porta
36
+ // la riga sort e le due righe meta.
37
+ //
38
+ // Il pane doc sta nello slot SINISTRO e paga comunque questa cornice, non quella
39
+ // del pane task: il costo lo decide ciò che il pane disegna, non il lato in cui
40
+ // sta. Se un giorno uno dei tre guadagnasse una riga fissa, questa costante deve
41
+ // sdoppiarsi e il budget guadagnare un ramo — una riga fissa non contata è ciò
42
+ // che fa sfondare `rows` e manda Ink nel ramo `clearTerminal`.
38
43
  const SESSIONS_PANE_CHROME = 3; // 2 bordi + header "Sessions · …"
39
44
  const PREVIEW_CHROME = 3; // marginTop + 2 bordi
40
45
  /** Preview sessione: righe fisse = titolo + riga meta (size · turni · ultima
@@ -50,6 +55,11 @@ const MAX_SESSION_PREVIEW = 3;
50
55
  * lettura di file a ogni movimento di selezione per un'anteprima che il detail
51
56
  * dà per intero. */
52
57
  const INBOX_DETAIL_FIXED = 2;
58
+ /** T160 — preview doc: nome intero del bersaglio + riga meta (cifre e nomi
59
+ * interi dei flag). Righe TUTTE fisse come per l'inbox, e per la stessa
60
+ * ragione: il corpo del file sta nel detail fullscreen, e mostrarne qui le
61
+ * prime righe costerebbe una lettura di file a ogni movimento di selezione. */
62
+ const DOC_DETAIL_FIXED = 2;
53
63
  /** Altezza di ciascuna modale, marginTop incluso. In flusso, non in overlay:
54
64
  * spingono giù i pane, quindi il loro costo va scalato dal budget. */
55
65
  export const MODAL_HEIGHT = {
@@ -89,8 +99,13 @@ export const MODAL_HEIGHT = {
89
99
  status: 0,
90
100
  // T134 — il detail di un file inbox: sesta sostitutiva. Un file di nozioni
91
101
  // arriva a 28KB, cioè più lungo di ogni task file del progetto: la stessa
92
- // ragione delle altre cinque. Altezza da `inboxDetailCapacity`.
102
+ // ragione delle altre cinque. Altezza da `fileSheetCapacity`.
93
103
  inbox: 0,
104
+ // T160 — il detail di un bersaglio doc, nona sostitutiva: stessa schermata
105
+ // dell'inbox (`useFileSheet`, due istanze) su un altro oggetto. Un modo
106
+ // proprio e non uno condiviso perché `esc` deve riportare al pane da cui si è
107
+ // entrati, e un modo solo obbligherebbe a ricordarselo altrove.
108
+ doc: 0,
94
109
  // T134 — la lista hard-wrap: settima sostitutiva. Sul cappello lo scan trova
95
110
  // 77 `WRAP` e 147 `misto`, cioè una lista che non entra in nessun box sopra i
96
111
  // pane. Altezza da `wrapListCapacity`.
@@ -184,11 +199,13 @@ export function layoutBudget(input) {
184
199
  previewCost = fixed + sessionFirstLines + sessionLastLines;
185
200
  }
186
201
  }
187
- else if (input.preview === 'inbox') {
202
+ else if (input.preview === 'inbox' || input.preview === 'doc') {
188
203
  // Tutte righe fisse: o il blocco entra intero o non esiste. Non c'è una
189
204
  // parte variabile da sacrificare per farlo stare, quindi non c'è nemmeno la
190
- // domanda di quanto concedergli.
191
- const fixed = PREVIEW_CHROME + INBOX_DETAIL_FIXED;
205
+ // domanda di quanto concedergli. I due blocchi costano uguale e non sono
206
+ // mai a schermo insieme (stanno nei due pane dello stesso modo, e il blocco
207
+ // preview è uno solo, scelto dal focus).
208
+ const fixed = PREVIEW_CHROME + (input.preview === 'inbox' ? INBOX_DETAIL_FIXED : DOC_DETAIL_FIXED);
192
209
  if (avail - floor - fixed >= 0) {
193
210
  preview = true;
194
211
  previewCost = fixed;
@@ -309,21 +326,27 @@ const STATUS_CHROME = 8;
309
326
  export function statusCapacity(rows) {
310
327
  return Math.max(0, (rows || 24) - SLACK - STATUS_CHROME);
311
328
  }
312
- // T134 — cornice del detail di un file inbox. Gemella di STATUS_CHROME più una
313
- // riga: il detail porta in fondo la riga dell'azione (quale skill partirà su
314
- // `⏎`), che è l'unica cosa che l'utente deve leggere prima di premere.
329
+ // T134/T160 — cornice del detail di un FILE, condivisa dalle due istanze dello
330
+ // sheet (coda inbox e bersaglio doc). Gemella di STATUS_CHROME più una riga: il
331
+ // detail porta in fondo la riga dell'azione (quale comando partirà su `⏎`), che
332
+ // è l'unica cosa che l'utente deve leggere prima di premere.
315
333
  // 2 bordi del box esterno
316
- // 1 riga di testata (natura :: nome del file)
334
+ // 1 riga di testata (natura o tipo :: nome del bersaglio)
317
335
  // 1 riga meta (marcatori · cifre · posizione nel testo)
318
336
  // 1 riga hint
319
337
  // 1 marginTop del box corpo
320
338
  // 2 bordi del box corpo
321
339
  // 1 marginTop della riga azione
322
340
  // 1 riga azione
323
- const INBOX_CHROME = 10;
324
- /** Righe di file inbox che entrano nel terminale. */
325
- export function inboxDetailCapacity(rows) {
326
- return Math.max(0, (rows || 24) - SLACK - INBOX_CHROME);
341
+ //
342
+ // Una costante sola per le due schermate perché è la stessa cornice, disegnata
343
+ // dallo stesso schema: due numeri uguali con due nomi diversi divergerebbero al
344
+ // primo ritocco di una delle due, e quella che resta indietro sfonda `rows`
345
+ // senza che niente lo dica.
346
+ const FILE_SHEET_CHROME = 10;
347
+ /** Righe di testo che entrano nel terminale dentro uno sheet di file. */
348
+ export function fileSheetCapacity(rows) {
349
+ return Math.max(0, (rows || 24) - SLACK - FILE_SHEET_CHROME);
327
350
  }
328
351
  // T134 — cornice della lista hard-wrap.
329
352
  // 2 bordi del box esterno
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamemind/loom-deck",
3
- "version": "0.69.0",
3
+ "version": "0.71.0",
4
4
  "description": "Deck TUI Ink per-progetto della famiglia loom: legge tasks.md e spawna sessioni Claude Code bound via LOOM_TASK",
5
5
  "type": "module",
6
6
  "bin": {
package/scripts/deck-run CHANGED
@@ -299,10 +299,11 @@ WORKDIR="${LOOM_DECK_WORKDIR:-$PWD}"
299
299
  # conversazione portava due titoli diversi, senza errore. Le quadre che
300
300
  # delimitavano il prefisso sono cadute col prefisso stesso: l'emoji da sola si
301
301
  # stacca già dallo slug.
302
- # T161 — le sei stanno tutte nei TEMPLATE di titolo del catalogo delle azioni
303
- # (`SPAWN_ACTIONS` in `src/spawn-catalog.ts`), che il progetto può riscrivere:
304
- # quattro nominano un'azione su una task (📐🚀📊🏁), 🧹 il file inbox che si
305
- # drena, 📏 il path che si srotola.
302
+ # T161/T160 — le sette stanno tutte nei TEMPLATE di titolo del catalogo delle
303
+ # azioni (`SPAWN_ACTIONS` in `src/spawn-catalog.ts`), che il progetto può
304
+ # riscrivere: quattro nominano un'azione su una task (📐🚀📊🏁), 🧹 il file inbox
305
+ # che si drena, 📏 il path che si srotola, 🧭 il bersaglio doc che si
306
+ # riorganizza.
306
307
  # Questa whitelist ha un GEMELLO TypeScript (`saneNote` in `src/sane-note.ts`),
307
308
  # che riduce il template al salvataggio perché quello che sta nel file di
308
309
  # progetto sia quello che la tab mostrerà. I due lati non si leggono a vicenda e
@@ -335,7 +336,7 @@ _sane_note() { # <raw> → nota ridotta, spazi collassati, cap 60 char
335
336
  # `tr` non ne ha bisogno: squeeza uno spazio ASCII, e 0x20 non compare mai
336
337
  # come byte di continuazione di una sequenza UTF-8.
337
338
  s="$(printf '%s' "$1" \
338
- | LC_ALL=C.UTF-8 sed 's/[^A-Za-z0-9 _àèéìòùÀÈÉÌÒÙ📐🚀📊🏁🧹📏-]//g' \
339
+ | LC_ALL=C.UTF-8 sed 's/[^A-Za-z0-9 _àèéìòùÀÈÉÌÒÙ📐🚀📊🏁🧹📏🧭-]//g' \
339
340
  | tr -s ' ')"
340
341
  s="${s# }"; s="${s% }"
341
342
  printf '%s' "${s:0:60}"