@lamemind/loom-deck 0.70.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.
@@ -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.70.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}"