@lamemind/loom-deck 0.35.1 → 0.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -285,17 +285,28 @@ function Deck({ cwd, tasksPath, tasksDir }) {
285
285
  // Lo spawn vero, su una task GIÀ risolta. Separato dalla guardia perché il
286
286
  // detail passa l'id fotografato all'apertura e non la selezione corrente: la
287
287
  // lista lì sotto non è più a schermo, quindi non è più la fonte dell'oggetto.
288
- function spawnForTask(id, kind, model, keyLabel) {
288
+ // T111 `spawnNote` arriva dal campo sempre attivo del detail ed è vuota per
289
+ // ogni altro percorso (`^K`/`^P`/`^R` non passano di lì, quindi non hanno da
290
+ // dove prenderla). Si scrive nel sidecar PRIMA dello spawn, accanto al
291
+ // binding e per la stessa ragione: la conversazione deve risultare figlia
292
+ // della task e portare la propria maniglia appena il suo JSONL compare, o per
293
+ // il tempo di un tick la riga in lista comparirebbe nuda. Due record separati
294
+ // sullo stesso `sessionId` sono la forma normale di un file append-only
295
+ // last-wins, non una scrittura da fondere.
296
+ function spawnForTask(id, kind, model, keyLabel, spawnNote = '') {
289
297
  const sid = randomUUID();
290
298
  appendTaskBinding(cwd, sid, id);
291
- const child = spawnDeck(id, cwd, sid, kind, model);
299
+ if (spawnNote)
300
+ appendNote(cwd, sid, spawnNote);
301
+ const child = spawnDeck(id, cwd, sid, kind, model, spawnNote);
292
302
  child.on('error', () => setNote(`⚠ spawn ${id} fallito (${DECK_RUN})`));
293
303
  const what = kind === 'none' ? '' : ` · ${kind}`;
294
304
  // Il modello è SEMPRE nominato, anche quando è il default: gli acceleratori
295
305
  // della lista non passano dal selettore del detail e usano il default fisso
296
306
  // (T108 · via a), quindi senza dirlo l'utente crederebbe di aver ereditato
297
307
  // la scelta fatta nell'ultimo detail aperto.
298
- setNote(`${keyLabel} spawn ${id}${what} · ${model} tab CC (sid ${sid.slice(0, 8)})`);
308
+ const named = spawnNote ? ` · «${cut(spawnNote, 24)}»` : '';
309
+ setNote(`${keyLabel} spawn ${id}${what} · ${model}${named} → tab CC (sid ${sid.slice(0, 8)})`);
299
310
  }
300
311
  function spawnTaskSession(kind, keyLabel) {
301
312
  const task = selectedTaskOr(keyLabel, 'spawnare');
@@ -490,7 +501,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
490
501
  columns,
491
502
  setMode,
492
503
  setNote,
493
- onAction: (id, kind, model, label) => spawnForTask(id, kind, model, label),
504
+ onAction: (id, kind, model, spawnNote, label) => spawnForTask(id, kind, model, label, spawnNote),
494
505
  });
495
506
  const search = useSearchOverlay({
496
507
  sessions,
@@ -1023,7 +1034,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
1023
1034
  // posizione è lo scroll mosso a mano. Il clamp serve comunque — un resize
1024
1035
  // può accorciare il testo sotto uno scroll già dato.
1025
1036
  const start = Math.min(sheet.top, sheet.maxTop);
1026
- return (_jsx(DetailScreen, { id: sheet.sheet.id, title: sheet.sheet.title, missing: sheet.sheet.text === null, lines: sheet.lines.slice(start, start + sheet.capacity), spans: sheet.doc?.spans ?? [], top: start, total: sheet.lines.length, capacity: sheet.capacity, action: sheet.action, model: sheet.model, columns: columns, find: sheet.find, occ: sheet.findRes.occ, occCur: sheet.occCur }));
1037
+ return (_jsx(DetailScreen, { id: sheet.sheet.id, title: sheet.sheet.title, missing: sheet.sheet.text === null, lines: sheet.lines.slice(start, start + sheet.capacity), spans: sheet.doc?.spans ?? [], top: start, total: sheet.lines.length, capacity: sheet.capacity, action: sheet.action, model: sheet.model, spawnNote: sheet.spawnNote, columns: columns, find: sheet.find, occ: sheet.findRes.occ, occCur: sheet.occCur }));
1027
1038
  }
1028
1039
  // ── T52 · schermate sostitutive ─────────────────────────────────────────
1029
1040
  // Ricerca e reader sono gli unici modali che NON stanno in flusso sopra i
@@ -28,6 +28,13 @@ export function useSheetOverlay(deps) {
28
28
  // deck: si azzera a ogni apertura come scroll e azione, quindi non esiste una
29
29
  // selezione invisibile che cambi il comportamento dei tasti della lista.
30
30
  const [model, setModel] = useState(MODEL_DEFAULT);
31
+ // T111 — la nota con cui nascerà la conversazione. Campo SEMPRE ATTIVO: riceve
32
+ // la scrittura appena il detail si apre, senza nessun tasto che lo apra, e per
33
+ // questo si prende l'alfabeto nudo del modo (le cifre del selettore modello,
34
+ // `g`/`G` degli estremi del testo). Si azzera a ogni apertura come scroll,
35
+ // azione e modello: una nota armata che sopravvive alla chiusura sarebbe uno
36
+ // stato invisibile che cambia il titolo dello spawn successivo.
37
+ const [spawnNote, setSpawnNote] = useState('');
31
38
  // T91 — ricerca dentro il detail. `open` distingue i due modi in cui la si
32
39
  // lascia: `esc` butta via ciò che il modale ha prodotto (`find` a null,
33
40
  // evidenziazione via), `⏎` lo congela e restituisce il controllo allo strato
@@ -77,12 +84,13 @@ export function useSheetOverlay(deps) {
77
84
  return;
78
85
  setTop(topForOffset(lines, o.start, capacity));
79
86
  }, [findRes, occCur, lines, capacity]);
80
- /** Apre il detail su una task, azzerando scroll, azione, modello e ricerca. */
87
+ /** Apre il detail su una task, azzerando scroll, azione, modello, nota e ricerca. */
81
88
  function open(next) {
82
89
  setSheet(next);
83
90
  setTop(0);
84
91
  setAction(0);
85
92
  setModel(MODEL_DEFAULT);
93
+ setSpawnNote('');
86
94
  setFind(null);
87
95
  setOccIdx(0);
88
96
  setNote('');
@@ -150,16 +158,32 @@ export function useSheetOverlay(deps) {
150
158
  // selezione per ognuna. Il modo cattura TUTTO, acceleratori `^K`/`^P`/`^R`
151
159
  // compresi: chi è già nel detail ha i bottoni. L'unica deroga è `^F`, che qui
152
160
  // apre la ricerca nel testo invece di quella sulle conversazioni.
161
+ //
162
+ // T111 — con il campo nota sempre attivo l'ultimo ramo è la SCRITTURA, quindi
163
+ // ogni funzione che resta viva deve stare su un tasto che un campo di testo
164
+ // non contende: `tab`, frecce, `PgUp`/`PgDn`, CTRL, `⏎`, `esc`.
153
165
  function onKey(input, key) {
154
166
  if (find?.open) {
155
167
  onFindKey(input, key);
156
168
  return;
157
169
  }
158
- if (key.ctrl && input === 'f') {
159
- // Fuori dal detail `^F` è la ricerca conversazioni; qui è la ricerca nel
160
- // testo. La query sopravvive a una chiusura con `⏎`, quindi riaprire
161
- // riprende da dov'era invece di ricominciare.
162
- setFind((f) => (f ? { ...f, open: true } : { q: '', caret: 0, open: true }));
170
+ if (key.ctrl) {
171
+ // Ramo CTRL chiuso in testa, e non più il solo `if` su `^F`: sotto c'è la
172
+ // scrittura nel campo nota, e senza questo ramo `^K` ci finirebbe come
173
+ // lettera `k` la combo non arriva come tasto proprio, `key.ctrl` è
174
+ // l'unico discriminante fra `^X` e `x`.
175
+ //
176
+ // `^F` è la deroga dichiarata (`CTRL_DEROGATIONS.detail`): fuori dal
177
+ // detail è la ricerca conversazioni, qui è la ricerca nel testo. La query
178
+ // sopravvive a una chiusura con `⏎`, quindi riaprire riprende da dov'era.
179
+ // `^U` svuota la nota — non è una deroga ma una combo che il modo gestisce
180
+ // da sé, come il filtro del modale assegnazione. Ogni altro CTRL è inerte.
181
+ if (input === 'f') {
182
+ setFind((f) => (f ? { ...f, open: true } : { q: '', caret: 0, open: true }));
183
+ }
184
+ else if (input === 'u') {
185
+ setSpawnNote('');
186
+ }
163
187
  return;
164
188
  }
165
189
  if (key.escape) {
@@ -178,24 +202,18 @@ export function useSheetOverlay(deps) {
178
202
  // competizione sul tasto.
179
203
  const act = DETAIL_ACTIONS[action];
180
204
  const id = sheet?.id;
205
+ const note = spawnNote.trim();
181
206
  close();
182
207
  if (id)
183
- onAction(id, act.kind, model, `⏎ ${act.label}`);
208
+ onAction(id, act.kind, model, note, `⏎ ${act.label}`);
184
209
  }
185
210
  else if (key.tab) {
186
- // T108 — `tab` scorre il catalogo dei modelli. Libero solo QUI: in vista
187
- // normale cicla la vista del pane a fuoco, e le cifre `1`-`9` sono della
188
- // legenda launch. Il detail cattura l'input per intero, quindi è dove un
189
- // alfabeto già speso torna disponibile.
211
+ // T108 — `tab` scorre il catalogo dei modelli, e da T111 è il SOLO canale:
212
+ // le cifre `1`-`4` sono passate al campo nota. Libero solo QUI: in vista
213
+ // normale cicla la vista del pane a fuoco. Il detail cattura l'input per
214
+ // intero, quindi è dove un alfabeto già speso torna disponibile.
190
215
  setModel((m) => MODELS[(MODELS.indexOf(m) + 1) % MODELS.length]);
191
216
  }
192
- else if (input.length === 1 && input >= '1' && input <= String(MODELS.length)) {
193
- // Scelta diretta: chi sa già quale vuole non paga lo scorrimento. Il test
194
- // sulla lunghezza non è pleonastico: `useInput` consegna il CHUNK letto da
195
- // stdin, quindi un incollato come `12` passerebbe il confronto fra
196
- // stringhe e indicizzerebbe il catalogo fuori range.
197
- setModel(MODELS[Number(input) - 1]);
198
- }
199
217
  else if (key.leftArrow || key.rightArrow) {
200
218
  // Scorrimento CICLICO come le righe di scelta del modale edit: cinque
201
219
  // voci, arrivare in fondo e ripartire costa meno che invertire direzione.
@@ -214,14 +232,19 @@ export function useSheetOverlay(deps) {
214
232
  else if (key.pageDown) {
215
233
  scroll(capacity);
216
234
  }
217
- else if (input === 'g') {
218
- // Estremi su lettera per lo stesso motivo del reader: Ink riconosce
219
- // `Home`/`End` ma non le espone, e qui non c'è input di testo che
220
- // contenda le lettere.
221
- setTop(0);
235
+ else if (key.backspace || key.delete) {
236
+ // Nessun movimento di caret nel campo (D3): `←→` sono già delle azioni e
237
+ // riprenderle costerebbe la barra. Si cancella quindi in coda e
238
+ // `key.delete` è ANCHE Backspace in Ink, come nel campo di ricerca.
239
+ setSpawnNote((s) => removeAt(s, cpLen(s) - 1));
222
240
  }
223
- else if (input === 'G') {
224
- setTop(maxTop);
241
+ else if (input && !key.meta) {
242
+ // Ultimo ramo: tutto ciò che nessun tasto vivo ha reclamato è testo. Ci
243
+ // cadono le cifre `1`-`4` (prima il selettore modello) e `g`/`G` (prima
244
+ // gli estremi del testo, che nel detail si raggiungono con
245
+ // `PgUp`/`PgDn`); nel reader fullscreen restano, perché lì nessun campo
246
+ // contende le lettere.
247
+ setSpawnNote((s) => s + sanitizeTyped(input));
225
248
  }
226
249
  }
227
250
  return {
@@ -230,6 +253,7 @@ export function useSheetOverlay(deps) {
230
253
  top,
231
254
  action,
232
255
  model,
256
+ spawnNote,
233
257
  find,
234
258
  lines,
235
259
  capacity,
package/dist/spawn.js CHANGED
@@ -32,8 +32,9 @@ export function spawnOut(cmd, args, opts) {
32
32
  fake.unref = () => fake;
33
33
  return fake;
34
34
  }
35
- // L'ordine È il binding dei tasti `1`-`4` nel detail, non una preferenza di
36
- // lettura: cambiarlo sposta i tasti sotto le dita di chi li ha imparati.
35
+ // L'ordine È il giro di `tab` nel detail, non una preferenza di lettura:
36
+ // cambiarlo sposta le voci sotto le dita di chi le ha imparate. Fino a T111 era
37
+ // anche il binding delle cifre `1`-`4`, passate poi al campo nota.
37
38
  export const MODELS = ['fable', 'opus', 'sonnet', 'haiku'];
38
39
  // Default del selettore e di ogni percorso di spawn che non passa da lui
39
40
  // (`^K`/`^P`/`^R` dalla lista, resume, fork). Duplicato del default di deck-run
@@ -65,11 +66,20 @@ export const DETAIL_ACTIONS = [
65
66
  // Il modello, al contrario del kind, ha un default (T108): i percorsi che non
66
67
  // passano dal selettore del detail non devono nominarlo per forza, e l'unico
67
68
  // valore sensato per loro è quello che il selettore stesso mostra all'apertura.
68
- export function deckArgs(id, sessionId, kind, model = MODEL_DEFAULT) {
69
- return [id, '--session-id', sessionId, '--prompt-kind', kind, '--model', model];
69
+ // T111 `spawnNote` è la nota data alla NASCITA della conversazione, dal campo
70
+ // sempre attivo del detail. Stesso flag `--title-note` del resume (T64): il
71
+ // suffisso `«nota»` viene appeso a `TITLE` dentro deck-run PRIMA che i rami si
72
+ // separino, quindi il flag non appartiene alla ripresa — vale su ogni spawn.
73
+ // Assente quando la nota è vuota, e non passato vuoto: `--title-note ''`
74
+ // produrrebbe un `«»` a vuoto nel titolo.
75
+ export function deckArgs(id, sessionId, kind, model = MODEL_DEFAULT, spawnNote) {
76
+ const args = [id, '--session-id', sessionId, '--prompt-kind', kind, '--model', model];
77
+ if (spawnNote)
78
+ args.push('--title-note', spawnNote);
79
+ return args;
70
80
  }
71
- export function spawnDeck(id, cwd, sessionId, kind, model = MODEL_DEFAULT) {
72
- const child = spawnOut(DECK_RUN, deckArgs(id, sessionId, kind, model), {
81
+ export function spawnDeck(id, cwd, sessionId, kind, model = MODEL_DEFAULT, spawnNote) {
82
+ const child = spawnOut(DECK_RUN, deckArgs(id, sessionId, kind, model, spawnNote), {
73
83
  cwd,
74
84
  detached: true,
75
85
  stdio: 'ignore',
@@ -5,6 +5,7 @@ import { Box, Text } from 'ink';
5
5
  import { caretWindow, cut, cutParts, sanitize } from '../width.js';
6
6
  import { sliceLine } from '../text-search.js';
7
7
  import { sliceSpans } from '../markdown.js';
8
+ import { cpLen } from '../layout.js';
8
9
  import { DETAIL_ACTIONS, MODELS } from '../spawn.js';
9
10
  import { WARN } from '../glyphs.js';
10
11
  /** Resa di ogni costrutto markdown (T75 · D4): un solo livello di enfasi per
@@ -51,14 +52,19 @@ export function DetailLine({ line, spans, occ, current, }) {
51
52
  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));
52
53
  }) }));
53
54
  }
54
- /** Campo della ricerca nel detail: finestra ancorata al caret, cursore inverso
55
- * sulla cella reale. Gemello di `EditTextField` senza la label, che qui sta
56
- * fuori perché il campo vive in FLUSSO su una riga condivisa col contatore
57
- * non su una riga propria. */
58
- export function DetailFindField({ value, caret, cols }) {
55
+ /** Campo di testo del detail: finestra ancorata al caret, cursore inverso sulla
56
+ * cella reale. Gemello di `EditTextField` senza la label, che qui sta fuori
57
+ * perché il campo vive in FLUSSO su una riga condivisa con altro (il contatore
58
+ * di occorrenze per la ricerca, il segnaposto per la nota) — non su una riga
59
+ * propria. Lo usano i due campi del detail: la ricerca `^F`, dove il caret si
60
+ * muove, e la nota (T111), dove il caret sta sempre in coda. */
61
+ export function DetailTextField({ value, caret, cols }) {
59
62
  const win = caretWindow(value, caret, cols);
60
63
  return (_jsxs(_Fragment, { children: [_jsx(Text, { children: sanitize(win.head) }), _jsx(Text, { inverse: true, children: sanitize(win.at) }), _jsx(Text, { children: sanitize(win.tail) })] }));
61
64
  }
65
+ /** Segnaposto della riga nota quando il campo è vuoto. Costante e non letterale
66
+ * inline perché la sua larghezza entra nel budget del campo accanto. */
67
+ const NOTE_HINT = ' · nome della conversazione';
62
68
  /**
63
69
  * Detail della task (T66): il task file scrollabile più la barra azioni.
64
70
  *
@@ -72,7 +78,7 @@ export function DetailFindField({ value, caret, cols }) {
72
78
  * all'arrivo del mouse (T21 · SGR enable + hit-test) senza migrazione. La
73
79
  * navigazione da tastiera ci si sovrappone senza conflitti.
74
80
  */
75
- export function DetailScreen({ id, title, missing, lines, spans, top, total, capacity, action, model, columns, find, occ, occCur, }) {
81
+ export function DetailScreen({ id, title, missing, lines, spans, top, total, capacity, action, model, spawnNote, columns, find, occ, occCur, }) {
76
82
  const last = Math.min(total, top + capacity);
77
83
  // Il taglio lo fa il chiamante (invariante ③ di width.ts): la riga bottoni è
78
84
  // ASCII, quindi `truncate-end` oggi darebbe il risultato giusto per caso — ma
@@ -98,14 +104,16 @@ export function DetailScreen({ id, title, missing, lines, spans, top, total, cap
98
104
  // enfasi diverse su due righe adiacenti (là `inverse`, qui un grassetto
99
105
  // colorato) obbligano a guardare da vicino per capire quale voce è scelta —
100
106
  // la stessa resa si legge di colpo su entrambe.
101
- // La cifra sta DENTRO la quadra: è il tasto che seleziona quella voce, e
102
- // sull'unica riga del deck dove le cifre valgono qualcosa toglierla
103
- // costringerebbe a contare le posizioni.
107
+ // T111 — la cifra è USCITA dalla quadra: `1`-`4` sono passate al campo nota e
108
+ // il modello si scorre col solo `tab`. Un'etichetta che nomina un tasto è
109
+ // accoppiata al binding, e tenerla dopo che il binding è morto non è
110
+ // un'informazione parziale — è una resa che dichiara un tasto che non fa più
111
+ // quella cosa, cioè peggio di nessuna indicazione.
104
112
  // `priority` sulla voce SELEZIONATA e non sulla prima: qui il troncamento
105
113
  // cancellerebbe l'unica informazione che la riga esiste per dare — quale
106
114
  // modello sta per essere usato — mentre nella barra azioni la voce attiva è
107
115
  // comunque nota dal tasto appena premuto.
108
- const mSegs = MODELS.map((m, i) => `[ ${i + 1} ${m} ]`);
116
+ const mSegs = MODELS.map((m) => `[ ${m} ]`);
109
117
  const mParts = [];
110
118
  mSegs.forEach((s, i) => {
111
119
  if (i > 0)
@@ -120,5 +128,14 @@ export function DetailScreen({ id, title, missing, lines, spans, top, total, cap
120
128
  if (dropped(mShown, mSegs) > 0)
121
129
  mShown = cutParts(mParts, Math.max(0, mWidth - 6), mIdx * 2);
122
130
  const mCut = dropped(mShown, mSegs);
123
- 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: "1-4/tab" }), " modello \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, _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { dimColor: true, children: "modello " }), mShown.map((part, i) => i % 2 === 1 ? (_jsx(Text, { children: part }, i)) : (_jsx(Text, { inverse: i / 2 === mIdx, color: i / 2 === mIdx ? 'green' : 'gray', children: part }, i))), mCut > 0 ? _jsxs(Text, { color: "yellow", children: [" \u00B7 +", mCut] }) : null] }), _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] })] })] }));
131
+ // T111 riga della nota. FISSA (D1): il valore armato si vede sempre, e il
132
+ // suo costo sta in `DETAIL_CHROME` invece che in un secondo `extra`
133
+ // condizionale di `detailCapacity`. Prefisso largo 8 come `modello `, così i
134
+ // due parametri dello spawn si leggono incolonnati.
135
+ // Il campo non ha nessun tasto che lo apra: il cursore è l'unica cosa che lo
136
+ // dichiara attivo, e il segnaposto dice cosa ci si scrive. Le colonne del
137
+ // segnaposto si riservano SOLO quando c'è (cioè a nota vuota): riservarle
138
+ // sempre toglierebbe testo visibile a una nota lunga per un avviso assente.
139
+ const nWidth = Math.max(10, width - 8 - (spawnNote ? 0 : NOTE_HINT.length));
140
+ 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/PgUp/PgDn" }), " testo \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " azione \u00B7", ' ', _jsx(Text, { color: "yellow", children: "tab" }), " modello \u00B7", ' ', _jsx(Text, { color: "yellow", children: "scrivi" }), " nota \u00B7 ", _jsx(Text, { color: "yellow", children: "^U" }), " svuota \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(DetailTextField, { 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, _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { dimColor: true, children: "modello " }), mShown.map((part, i) => i % 2 === 1 ? (_jsx(Text, { children: part }, i)) : (_jsx(Text, { inverse: i / 2 === mIdx, color: i / 2 === mIdx ? 'green' : 'gray', children: part }, i))), mCut > 0 ? _jsxs(Text, { color: "yellow", children: [" \u00B7 +", mCut] }) : null] }), _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { dimColor: true, children: "nota " }), _jsx(DetailTextField, { value: spawnNote, caret: cpLen(spawnNote), cols: nWidth }), spawnNote ? null : _jsx(Text, { dimColor: true, children: NOTE_HINT })] }), _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] })] })] }));
124
141
  }
package/dist/viewport.js CHANGED
@@ -218,13 +218,21 @@ export function readerCapacity(rows) {
218
218
  // 2 bordi del box testo
219
219
  // 1 marginTop della riga azioni
220
220
  // 1 riga modello (T108)
221
+ // 1 riga nota (T111)
221
222
  // 1 riga azioni
222
223
  //
223
- // Le ultime tre sono il motivo per cui il detail non può riusare READER_CHROME:
224
- // barra bottoni e selettore modello sono righe FISSE in più dentro l'overlay, e
225
- // ogni riga fissa aggiunta va scalata dalla capienza del contenuto o il frame
226
- // sfonda `rows` (stessa invariante di TASKS_PANE_CHROME).
227
- const DETAIL_CHROME = 11;
224
+ // Le ultime quattro sono il motivo per cui il detail non può riusare
225
+ // READER_CHROME: barra bottoni, selettore modello e campo nota sono righe FISSE
226
+ // in più dentro l'overlay, e ogni riga fissa aggiunta va scalata dalla capienza
227
+ // del contenuto o il frame sfonda `rows` (stessa invariante di
228
+ // TASKS_PANE_CHROME).
229
+ //
230
+ // La nota è una riga fissa e non un secondo `extra` di `detailCapacity` (T111 ·
231
+ // D1): due condizionali mutuamente esclusivi obbligherebbero il parametro a dire
232
+ // QUALE è aperto — un solo booleano per entrambi sottostima quando quello aperto
233
+ // è il più alto, e sommarli toglie righe al testo per un campo che non è a
234
+ // schermo.
235
+ const DETAIL_CHROME = 12;
228
236
  // T91 — la ricerca dentro il detail: marginTop + riga del campo.
229
237
  //
230
238
  // `MODAL_HEIGHT` dice che il detail costa 0 ai due pane (li sostituisce), e da
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamemind/loom-deck",
3
- "version": "0.35.1",
3
+ "version": "0.36.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": {