@lamemind/loom-deck 0.66.1 → 0.68.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/spawn.js CHANGED
@@ -3,8 +3,11 @@
3
3
  import { spawn } from 'node:child_process';
4
4
  import { EventEmitter } from 'node:events';
5
5
  import { fileURLToPath } from 'node:url';
6
- import { basename, dirname, join } from 'node:path';
7
- import { findTaskFile } from './tasks.js';
6
+ import { dirname, join } from 'node:path';
7
+ // T161 — i cataloghi (kind, modelli, azioni) stanno in `spawn-catalog.ts`, che
8
+ // è DATO PURO e non importa questo file: la dipendenza va in un verso solo.
9
+ // Qui restano i soli effetti verso l'esterno — comporre un argv e lanciarlo.
10
+ import { MODEL_DEFAULT, PROJECT_STATUS_MODEL } from './spawn-catalog.js';
8
11
  // scripts/deck-run è un sibling della dir del bundle: src/ (dev, tsx) e dist/
9
12
  // (build, node) stanno entrambi sotto la package root → risalita di un livello.
10
13
  export const DECK_RUN = join(dirname(fileURLToPath(import.meta.url)), '..', 'scripts', 'deck-run');
@@ -118,169 +121,50 @@ export function onInTabCommand(child, cb) {
118
121
  }
119
122
  });
120
123
  }
121
- /**
122
- * `recap` → la sotto-skill giusta, quando chi spawna SA se la task è un cappello.
123
- *
124
- * `recap` resta il kind onesto per chi non lo sa: punta al dispatcher, che
125
- * risolve la task e classifica da sé. È il caso degli acceleratori della lista,
126
- * dove il deck ha in mano solo `tasks.md` — e lì il `Size` non c'è. Il DETAIL
127
- * invece il task file l'ha già letto, quindi può saltare il giro e pagare un
128
- * turno di modello in meno.
129
- *
130
- * Non è una classificazione duplicata: il criterio (`Size: Epic`) resta uno solo
131
- * e sta in `taskIsEpic`, che legge lo stesso campo che leggerebbe il dispatcher.
132
- * Quello che si evita è il RITARDO, non il giudizio.
133
- *
134
- * Ogni kind diverso da `recap` passa intatto: la specializzazione è un caso, non
135
- * una trasformazione da applicare a tutti.
136
- */
137
- export function specializeRecap(kind, epic) {
138
- if (kind !== 'recap')
139
- return kind;
140
- return epic ? 'recap-epic' : 'recap-task';
141
- }
142
- // L'ordine È il giro di `tab` nel detail, non una preferenza di lettura:
143
- // cambiarlo sposta le voci sotto le dita di chi le ha imparate. Fino a T111 era
144
- // anche il binding delle cifre `1`-`4`, passate poi al campo nota.
145
- export const MODELS = ['fable', 'opus', 'sonnet', 'haiku'];
146
- // Default del selettore e FALLBACK per ogni kind fuori catalogo (kind `none`,
147
- // o un catalogo mutilo). Duplicato del default di deck-run e non letto da lì:
148
- // il deck deve poter MOSTRARE la selezione iniziale prima di spawnare
149
- // alcunché, e un valore che si conosce solo a spawn avvenuto non è mostrabile.
150
- //
151
- // T152 — non è più IL default di `^K`/`^P`/`^R` dalla lista: quei percorsi
152
- // leggono il catalogo come il detail (`modelFor`), e cadono qui solo quando il
153
- // kind non ha una riga. Resta l'unico default che resume e fork usano per
154
- // intero, perché lì il modello non è mai una funzione del kind — è quello
155
- // della conversazione d'origine (T148).
156
- export const MODEL_DEFAULT = 'fable';
157
- // T152 — `recap-status-project` non è una chiave del catalogo (§Doc Impact,
158
- // "le chiavi del catalogo sono i valori di --prompt-kind"): lo spawn headless
159
- // non passa mai --prompt-kind, quindi una riga lì sarebbe irraggiungibile da
160
- // ogni chiamante. Il default vive qui, accanto a MODEL_DEFAULT, e non nel
161
- // catalogo. Resta `opus` mentre i tre recap del catalogo stanno su fable: è
162
- // l'unico recap che gira in headless (`-p`), senza nessuno che ne legga
163
- // l'esito mentre si forma e possa rilanciarlo — il testo prodotto è il
164
- // deliverable, e viene riletto da disco anche giorni dopo.
165
- export const PROJECT_STATUS_MODEL = 'opus';
166
- // T66 — le azioni del detail. Non sono un catalogo nuovo: ognuna è un
167
- // `--prompt-kind` già esistente più `checkpoint`, e tutte passano dallo stesso
168
- // `spawnForTask` dei CTRL della lista — una superficie in più, zero percorsi di
169
- // spawn in più.
170
- //
171
- // L'etichetta è distinta dal kind dove il kind è il nome del MECCANISMO e
172
- // l'etichetta quello dell'INTENZIONE: `none` è "aprire la task a mani nude",
173
- // `recap` è "vedere a che punto sta".
174
- export const DETAIL_ACTIONS = [
175
- { kind: 'none', label: 'open' },
176
- { kind: 'preflight', label: 'preflight' },
177
- { kind: 'run', label: 'run' },
178
- { kind: 'recap', label: 'status' },
179
- { kind: 'checkpoint', label: 'checkpoint' },
180
- ];
181
- // T117 — selezione DIRETTA di un'azione con la sua iniziale (`o p r s c`),
182
- // accanto allo scorrimento `←→`. Derivate dalle label e non cablate: una voce
183
- // aggiunta al catalogo porta con sé la propria lettera, invece di lasciare
184
- // indietro una seconda lista.
185
- // Il vincolo che la derivazione impone al catalogo: le iniziali devono restare
186
- // DISTINTE fra loro. Due label con la stessa lettera renderebbero la seconda
187
- // irraggiungibile, e in silenzio — chi aggiunge una voce lo controlla qui.
188
- export const ACTION_HOTKEYS = Object.fromEntries(DETAIL_ACTIONS.map((a, i) => [a.label[0], i]));
189
- // T150 — titolo di fallback quando il campo nota è vuoto, derivato dall'AZIONE
190
- // e dal task. Mappa sul KIND, non sulla label (P2 preflight): alla sede del
191
- // fallback (`spawnForTask` in actions.ts) arriva il kind — già specializzato
192
- // quando la chiamata viene dal detail (`recap-task`/`recap-epic`) — mai la
193
- // label del catalogo.
194
- // D2/P7 — `none` (label "open") non ha un prefisso: è "nessuna azione", e
195
- // inventargliene uno contraddirebbe l'intenzione dell'utente.
196
- // P3 — le tre varianti di recap condividono il prefisso: la specializzazione
197
- // sceglie quale skill parte, non cosa l'utente sta chiedendo.
198
- //
199
- // T156 — emoji invece della parola in maiuscolo (`PREFL`, `RUN`, …): il titolo
200
- // si legge in lista deck e nella tab Ptyxis, dove l'azione va riconosciuta a
201
- // colpo d'occhio e non letta.
202
- // Le quattro sono ASTRALI (code point ≥ U+10000) per necessità, non per gusto:
203
- // `sanitize` (`src/width.ts`) sostituisce con `·` ogni glifo su cui le due
204
- // contabilità di larghezza divergono, e le emoji del BMP a presentazione testo
205
- // (⚙️ ⏱️ 🛠️ …) divergono sempre — string-width le conta 2, VTE ne disegna 1, e
206
- // il VS16 non sposta nessuno dei due verdetti. Un'emoji aggiunta qui va prima
207
- // passata in `sanitize` per vedere se sopravvive.
208
- // Le stesse quattro sono anche l'unica classe di emoji che `_sane_note` di
209
- // `deck-run` lascia passare nel titolo della tab: la whitelist è una lista
210
- // chiusa che nomina questi code point (P2 preflight), quindi cambiare una voce
211
- // qui e non là riapre la divergenza fra titolo in lista e titolo in tab.
212
- // Le quadre che delimitavano il prefisso sono cadute: l'emoji da sola si stacca
213
- // già dallo slug, e con lei sono uscite dalla whitelist di `_sane_note`.
214
- const ACTION_EMOJI = {
215
- preflight: '📐',
216
- run: '🚀',
217
- recap: '📊',
218
- 'recap-task': '📊',
219
- 'recap-epic': '📊',
220
- checkpoint: '🏁',
221
- };
222
- /**
223
- * Titolo di fallback per una conversazione con nota vuota: `{emoji} {slug}`,
224
- * o il solo slug per `none` (D1/D2 preflight). Lo slug viene dal NOME del
225
- * task file (`findTaskFile`), non dalla descrizione di `tasks.md`: il nome è
226
- * già dentro l'alfabeto di `_sane_note` per costruzione — minuscolo, separato
227
- * da trattini, senza punteggiatura — mentre la descrizione porta apostrofi e
228
- * `/` che la riduzione toglie senza sostituto, saldando le parole (D1
229
- * razionale).
230
- *
231
- * Il PREFISSO invece nell'alfabeto non ci sta per costruzione: l'emoji ci entra
232
- * solo perché la whitelist di `_sane_note` le nomina una per una (T156). È
233
- * l'unica parte del titolo di fallback che dipende da una modifica fatta
234
- * dall'altro lato.
235
- *
236
- * `null` quando il task file non si trova: il chiamante decide se ripiegare
237
- * su nota vuota o su un altro fallback.
238
- */
239
- export function fallbackTitle(tasksDir, id, kind) {
240
- const path = findTaskFile(tasksDir, id);
241
- if (!path)
242
- return null;
243
- const slug = basename(path, '.md').slice(id.length + 1).replace(/-/g, ' ');
244
- const emoji = ACTION_EMOJI[kind];
245
- return emoji ? `${emoji} ${slug}` : slug;
246
- }
247
124
  // Spawn detached: il deck spawna ma NON contiene la sessione (la possiede
248
125
  // ptyxis-agent). unref + stdio ignore → ritorna subito, la TUI resta viva.
249
126
  // sessionId pinnato (T27) → il binding sidecar è deterministico allo spawn.
250
- // Il kind è OBBLIGATORIO e non ha default qui: il default vive in deck-run (per
251
- // le invocazioni a mano), mentre dal deck ogni tasto dichiara il proprio intento
252
- // — un default silenzioso renderebbe indistinguibili `⏎` e `^K`.
253
- // Il modello, al contrario del kind, ha un default (T108): i percorsi che non
254
- // passano dal selettore del detail non devono nominarlo per forza, e l'unico
255
- // valore sensato per loro è quello che il selettore stesso mostra all'apertura.
127
+ // Il modello ha un default (T108): i percorsi che non passano dal selettore del
128
+ // detail non devono nominarlo per forza, e l'unico valore sensato per loro è
129
+ // quello che il selettore stesso mostra all'apertura.
256
130
  // T111 — `spawnNote` è la nota data alla NASCITA della conversazione, dal campo
257
- // sempre attivo del detail. Stesso flag `--title-note` del resume (T64): il
131
+ // sempre attivo del detail — che da T161 nasce PRE-RIEMPITO col template di
132
+ // titolo dell'azione, reso sulla task aperta. Ne discende che un campo svuotato
133
+ // a mano significa «nessuna nota» e non più «usa il titolo di fallback»: il
134
+ // fallback non esiste più, perché il default è già a schermo.
135
+ // Stesso flag `--title-note` del resume (T64): il
258
136
  // suffisso `«nota»` viene appeso a `TITLE` dentro deck-run PRIMA che i rami si
259
137
  // separino, quindi il flag non appartiene alla ripresa — vale su ogni spawn.
260
138
  // Assente quando la nota è vuota, e non passato vuoto: `--title-note ''`
261
139
  // produrrebbe un `«»` a vuoto nel titolo.
262
- // T117 — `prompt` è il TESTO letterale, e quando c'è sostituisce il kind:
263
- // `--prompt` e `--prompt-kind` sono mutuamente esclusivi in deck-run. Serve al
264
- // detail, dove il prompt è un campo editabile e quello che l'utente legge è
265
- // quello che parte — dopo una modifica nessun kind lo descrive più.
266
- // `undefined` = questo percorso non ha un campo prompt (gli acceleratori della
267
- // lista) e viaggia col simbolo, come prima. Stringa VUOTA ≠ undefined: è la
268
- // richiesta esplicita di nessun prompt (azione `open`, o un campo svuotato a
269
- // mano) e si esprime col kind `none`, perché senza flag deck-run cadrebbe sul
270
- // proprio default `recap`.
271
- export function deckArgs(id, sessionId, kind, model = MODEL_DEFAULT, spawnNote, prompt) {
272
- const promptArgs = prompt === undefined
273
- ? ['--prompt-kind', kind]
274
- : prompt
275
- ? ['--prompt', prompt]
276
- : ['--prompt-kind', 'none'];
140
+ // T117 — `prompt` è il TESTO letterale, e sostituisce il kind: `--prompt` e
141
+ // `--prompt-kind` sono mutuamente esclusivi in deck-run.
142
+ //
143
+ // T161 — dal deck viaggia SEMPRE il testo, mai più il simbolo. Prima gli
144
+ // acceleratori della lista passavano `--prompt-kind` e lasciavano risolvere il
145
+ // testo a `deck-run`, che consultava il proprio catalogo; il detail passava già
146
+ // il letterale. Con un override di progetto sui prompt la risoluzione deve
147
+ // stare da una parte sola, e quella parte è il deck — che è l'unico dei due a
148
+ // leggere `.claude/loom-works.json` per questo (`jq` è dipendenza opzionale di
149
+ // `deck-run`). Un percorso solo, quindi, e ciò che parte è verificabile
150
+ // sull'argv.
151
+ //
152
+ // Due conseguenze dichiarate: `--prompt-kind` resta in `deck-run` per chi lo
153
+ // invoca a mano, e `LOOM_DECK_ENTER_PROMPT` non ha più effetto sugli spawn dal
154
+ // deck (non ha mai vinto su `--prompt`).
155
+ //
156
+ // Stringa VUOTA = richiesta esplicita di nessun prompt (azione `open`, o un
157
+ // campo svuotato a mano): si esprime col kind `none`, perché senza nessun flag
158
+ // `deck-run` cadrebbe sul proprio default `recap`.
159
+ export function deckArgs(id, sessionId, model = MODEL_DEFAULT, spawnNote, prompt = '') {
160
+ const promptArgs = prompt ? ['--prompt', prompt] : ['--prompt-kind', 'none'];
277
161
  const args = [id, '--session-id', sessionId, ...promptArgs, '--model', model];
278
162
  if (spawnNote)
279
163
  args.push('--title-note', spawnNote);
280
164
  return args;
281
165
  }
282
- export function spawnDeck(id, cwd, sessionId, kind, model = MODEL_DEFAULT, spawnNote, prompt) {
283
- return launchDeckRun(deckArgs(id, sessionId, kind, model, spawnNote, prompt), cwd);
166
+ export function spawnDeck(id, cwd, sessionId, model = MODEL_DEFAULT, spawnNote, prompt = '') {
167
+ return launchDeckRun(deckArgs(id, sessionId, model, spawnNote, prompt), cwd);
284
168
  }
285
169
  // T49 — resume di una sessione esistente come nuova tab Ptyxis. Scoped (taskId
286
170
  // presente) → `deck-run <task> --resume <sid>`: la ripresa eredita LOOM_TASK +
@@ -399,9 +283,30 @@ export function spawnClaudeEmpty(cwd, model) {
399
283
  * Il modello è passato SEMPRE, anche sul default, per la stessa ragione di
400
284
  * `permissionMode`: lo spawn resta deterministico e leggibile nel process tree
401
285
  * invece di dipendere da un default che può cambiare fra versioni.
286
+ *
287
+ * `sessionId` e `title` viaggiano INSIEME o non viaggiano: il titolo lo mostrano
288
+ * due superfici — la tab Ptyxis (via `--title-note`) e la riga della lista
289
+ * sessioni (via la nota nel sidecar, che è keyed sul sessionId) — e senza un id
290
+ * pinnato la seconda non ha una chiave su cui scrivere. Pinnarlo è quindi ciò
291
+ * che rende il titolo visibile in entrambe, non un binding task: una sessione
292
+ * `--no-task` non ne ha uno, ma un NOME ce l'ha.
293
+ *
294
+ * Il titolo sta in TESTA all'argv, prima del prompt: la riga di stato del deck
295
+ * taglia il comando al mezzo, e il prompt (specialmente quello cablato dello
296
+ * srotolamento) è l'argomento più lungo — in coda al titolo lo spingerebbe
297
+ * fuori vista. Stessa ragione dell'ordine di `resumeArgs`.
402
298
  */
403
- export function spawnBare(cwd, prompt, model) {
404
- return launchDeckRun(['--no-task', '--prompt', prompt, '--model', model], cwd);
299
+ export function bareArgs(prompt, model, sessionId, title) {
300
+ const args = ['--no-task'];
301
+ if (title)
302
+ args.push('--title-note', title);
303
+ if (sessionId)
304
+ args.push('--session-id', sessionId);
305
+ args.push('--prompt', prompt, '--model', model);
306
+ return args;
307
+ }
308
+ export function spawnBare(cwd, prompt, model, sessionId, title) {
309
+ return launchDeckRun(bareArgs(prompt, model, sessionId, title), cwd);
405
310
  }
406
311
  // T39/T32: voce `launch` custom del file config, eseguita con cwd = project root.
407
312
  // Spawn detached come spawnDeck: il deck lancia ma non possiede il processo.
@@ -555,12 +460,19 @@ export function spawnCleanTasks(ids, cwd, sessionId, ignored, onResult) {
555
460
  // permissivo toglie le richieste di permesso che il CLI pone su un tool, e
556
461
  // quelle in `-p` non hanno nessuno che risponda — il processo resterebbe appeso
557
462
  // senza emettere niente, cioè con l'indicatore che conta all'infinito.
558
- export function spawnProjectStatus(cwd, sessionId, onResult) {
559
- return spawnSkill('/loom-works:recap-status-project', cwd, sessionId, onResult, [
463
+ //
464
+ // T161 — modello e prompt arrivano dal CHIAMANTE, risolti sul catalogo delle
465
+ // azioni: erano gli ultimi due valori che questa funzione sceglieva da sé
466
+ // (`PROJECT_STATUS_MODEL` e la stringa della skill), e con l'override di
467
+ // progetto il default deve stare in un posto solo insieme a quello delle altre
468
+ // dieci azioni. Il `--permission-mode auto` invece resta qui: non è una
469
+ // preferenza ma la condizione perché un `-p` non resti appeso.
470
+ export function spawnProjectStatus(cwd, sessionId, onResult, model = PROJECT_STATUS_MODEL, prompt = '/loom-works:recap-status-project') {
471
+ return spawnSkill(prompt, cwd, sessionId, onResult, [
560
472
  '--permission-mode',
561
473
  'auto',
562
474
  '--model',
563
- PROJECT_STATUS_MODEL,
475
+ model,
564
476
  ]);
565
477
  }
566
478
  // Comando della notifica desktop. L'override esiste per il collaudo del ramo
package/dist/tasks.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { readFileSync, readdirSync } from 'node:fs';
2
- import { dirname, join } from 'node:path';
2
+ import { basename, dirname, join } from 'node:path';
3
3
  import { sanitize } from './width.js';
4
4
  /**
5
5
  * Forma di un ID task: `T` + numero. FONTE UNICA del gate — `parseTasks` lo usa
@@ -108,6 +108,26 @@ export function loadTasks(path) {
108
108
  // ID → path del task file: `<id>-<slug>.md` nella tasks dir. Il dash dopo l'ID
109
109
  // disambigua i prefissi (`T20-` non matcha `T2-…`). Se più file matchano, primo
110
110
  // in ordine. `null` se la dir non è leggibile o nessun file matcha.
111
+ /**
112
+ * T161 — lo SLUG di una task: il nome del suo file senza l'id e coi trattini
113
+ * sciolti in spazi, cioè il buco `{slug}` dei template di titolo.
114
+ *
115
+ * Viene dal NOME del file e non dalla descrizione in `tasks.md` per una ragione
116
+ * di alfabeto: il nome è già dentro quello di `_sane_note` per costruzione —
117
+ * minuscolo, separato da trattini, senza punteggiatura — mentre la descrizione
118
+ * porta apostrofi e `/`, che la riduzione toglie senza sostituto saldando le
119
+ * parole (`l'epica/doc` → `lepicadoc`).
120
+ *
121
+ * Stringa VUOTA quando il task file non si trova: chi interpola tratta un buco
122
+ * senza valore come «nessun titolo», invece di comporre un prefisso uguale per
123
+ * ogni task.
124
+ */
125
+ export function taskSlug(tasksDir, id) {
126
+ const path = findTaskFile(tasksDir, id);
127
+ if (!path)
128
+ return '';
129
+ return basename(path, '.md').slice(id.length + 1).replace(/-/g, ' ');
130
+ }
111
131
  export function findTaskFile(dir, id) {
112
132
  let entries;
113
133
  try {
@@ -6,7 +6,7 @@ import { cut } from '../width.js';
6
6
  import { sliceLine } from '../text-search.js';
7
7
  import { sliceSpans } from '../markdown.js';
8
8
  import { ChoiceRow, FieldText, LABEL_W } from './fields.js';
9
- import { DETAIL_ACTIONS, MODELS } from '../spawn.js';
9
+ import { DETAIL_ACTIONS, MODELS } from '../spawn-catalog.js';
10
10
  import { DROW, PRIORITY_CHOICES } from '../overlays/sheet.js';
11
11
  import { CARET, CARET_OFF, WARN } from '../glyphs.js';
12
12
  /** Resa di ogni costrutto markdown (T75 · D4): un solo livello di enfasi per
@@ -7,7 +7,7 @@ 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
9
  import { ChoiceRow } from './fields.js';
10
- import { MODELS } from '../spawn.js';
10
+ import { MODELS } from '../spawn-catalog.js';
11
11
  /** Colonne del prefisso delle due anteprime della preview sessione (`» `, `« `,
12
12
  * e i due spazi delle righe di continuazione). Costante perché entra nel
13
13
  * budget di wrap, che è il posto in cui dimenticarla non produce un errore ma
@@ -36,6 +36,7 @@ import { DetailScreen } from './detail-screen.js';
36
36
  import { StatusScreen } from './status-screen.js';
37
37
  import { InboxScreen } from './inbox-screen.js';
38
38
  import { WrapScreen } from './wrap-screen.js';
39
+ import { SpawnScreen } from './spawn-screen.js';
39
40
  import { ReaderScreen, SearchScreen } from './search-screen.js';
40
41
  /**
41
42
  * Il ripiego per un terminale troppo basso: una riga sola al posto della
@@ -107,7 +108,7 @@ export function HintBar({ mode, purge, keyLegend, indicators, columns, }) {
107
108
  */
108
109
  export function screenFor(input) {
109
110
  const { mode, rows, columns, note, overlays } = input;
110
- const { assign, sheet, search, status, inbox, wrap } = overlays;
111
+ const { assign, sheet, search, status, inbox, wrap, spawn } = overlays;
111
112
  // ── T57 · schermata di assegnazione ─────────────────────────────────────
112
113
  // Sostitutiva come ricerca e reader (D3): la lista task non entra in un box
113
114
  // sopra i due pane, e prendendo l'intero frame non costa nulla al loro budget.
@@ -185,6 +186,16 @@ export function screenFor(input) {
185
186
  const start = Math.min(wrap.top, wrap.maxTop);
186
187
  return (_jsx(WrapScreen, { files: wrap.files.slice(start, start + wrap.capacity), count: wrap.count, mixed: wrap.mixed, mtime: wrap.mtime, top: start, capacity: wrap.capacity, path: wrap.path, caret: wrap.caret, columns: columns }));
187
188
  }
189
+ // ── T161 · pagina delle azioni di spawn ─────────────────────────────────
190
+ // Ottava schermata sostitutiva: undici righe per quattro colonne, più l'area
191
+ // di compilazione. In un box sopra i due pane al prompt resterebbero due
192
+ // colonne.
193
+ if (mode === 'spawn' && spawn.open) {
194
+ if (isCompact(spawn.capacity)) {
195
+ return _jsx(CompactNotice, { what: "azioni di spawn", esc: "chiude", rows: rows, columns: columns });
196
+ }
197
+ return (_jsx(SpawnScreen, { rows: spawn.table, sel: spawn.sel, top: Math.min(spawn.top, spawn.maxTop), capacity: spawn.capacity, configured: spawn.table.filter((r) => r.touched).length, dirty: spawn.dirty, editing: spawn.editing, editable: spawn.editable, fieldTitle: spawn.fieldTitle, fieldModel: spawn.fieldModel, fieldPrompt: spawn.fieldPrompt, cursor: spawn.cursor, example: spawn.example, columns: columns, note: note }));
198
+ }
188
199
  // ── T52 · ricerca e reader ──────────────────────────────────────────────
189
200
  // Gli unici modali che NON stanno in flusso sopra i pane: una lista di
190
201
  // occorrenze non entra in un box da 4 righe. Prendono l'intero frame, quindi
@@ -0,0 +1,99 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // T161 — la pagina delle azioni di spawn: la tabella e la sua area di
3
+ // compilazione.
4
+ import { Box, Text } from 'ink';
5
+ import { cut, pad, sanitize, termWidth } from '../width.js';
6
+ import { ChoiceRow, FieldText, LABEL_W } from './fields.js';
7
+ import { CARET, CARET_OFF } from '../glyphs.js';
8
+ import { interpolate, saneNote } from '../sane-note.js';
9
+ import { MODELS, SPAWN_FIELDS, spawnCell, } from '../spawn-catalog.js';
10
+ /**
11
+ * Il glifo della riga configurata.
12
+ *
13
+ * Non è riusato da nessun'altra superficie del deck: `●`/`◍` dicono la liveness
14
+ * di un processo, `📌`/`🔗`/`○` l'appartenenza di una conversazione, `✎` il
15
+ * titolo — un falso amico costa più di un simbolo nuovo. Concorde alle due
16
+ * contabilità di larghezza (`agrees('◆')`), quindi `sanitize` lo lascia passare
17
+ * invece di renderlo `·`.
18
+ */
19
+ const TOUCHED = '◆';
20
+ /** Cella del glifo: si riempie di spazi anche quando è vuota, o le righe non
21
+ * configurate sposterebbero a sinistra tutto ciò che segue. */
22
+ const MARK_W = 2;
23
+ /** Colonna modello: il dominio è chiuso, e a decidere la larghezza non è la voce
24
+ * più lunga (`sonnet`, 6) ma l'INTESTAZIONE — `modello` ne vuole 7, e una
25
+ * testata di colonna troncata con l'ellissi si legge come un difetto. */
26
+ const MODEL_W = 7;
27
+ /** Tetto della colonna funzione: la label più lunga del catalogo (`drain di un
28
+ * file inbox`) sta esattamente dentro. Un tetto più alto le celle le
29
+ * toglierebbe al prompt, che è l'unico dei quattro valori senza una forma
30
+ * breve; più basso troncherebbe due label su undici già a piena larghezza. */
31
+ const FUNC_MAX = 22;
32
+ /** Le etichette delle righe dell'area, in italiano come ogni altra etichetta a
33
+ * schermo: la chiave del campo è un nome del codice e finisce nel file di
34
+ * progetto, non sotto gli occhi di chi compila. */
35
+ const FIELD_LABEL = {
36
+ title: 'titolo',
37
+ model: 'modello',
38
+ prompt: 'prompt',
39
+ };
40
+ /**
41
+ * Le quattro colonne, per sottrazione come ogni lista del deck: le fisse sono
42
+ * note, e ciò che resta si divide fra titolo e prompt.
43
+ *
44
+ * Il prompt prende la fetta maggiore perché è l'unico dei quattro valori che non
45
+ * ha una forma breve: un titolo tagliato resta riconoscibile dal prefisso, un
46
+ * prompt tagliato a dieci colonne dice solo `/loom-works…`.
47
+ *
48
+ * Pavimento 0 su ogni cella: è un tetto, non una preferenza, e alzarlo sopra lo
49
+ * spazio reale fa uscire la riga dal box mangiandone il bordo (invariante ③ di
50
+ * `width.ts`).
51
+ */
52
+ export function spawnColumns(width, rows) {
53
+ // `- 4` = bordo + padding del box tabella, che sta DENTRO quello esterno.
54
+ const avail = Math.max(0, width - 4);
55
+ const func = Math.min(FUNC_MAX, Math.max(0, ...rows.map((r) => termWidth(r.action.label))));
56
+ // I tre separatori fra le quattro colonne vanno contati insieme alle celle:
57
+ // sommare solo le celle è l'off-by-one che tronca la coda della riga.
58
+ const rest = Math.max(0, avail - CARET_OFF.length - MARK_W - func - MODEL_W - 3);
59
+ // Il titolo ha un TETTO oltre il quale non cresce: i template sono corti
60
+ // (`📊 {slug}`), quindi su un terminale largo ogni colonna in più gli
61
+ // resterebbe vuota mentre al prompt serve.
62
+ const title = Math.min(28, Math.floor(rest * 0.4));
63
+ return { func, title, model: MODEL_W, prompt: Math.max(0, rest - title) };
64
+ }
65
+ /** Una cella della tabella, col suo colore: il valore in chiaro, il segnaposto
66
+ * smorzato, l'override in ciano — l'origine si legge senza una legenda. */
67
+ function Cell({ row, field, value, width, }) {
68
+ const cell = spawnCell(row.action, field, value, row.raw.overridden.has(field));
69
+ const text = cut(cell.text, width);
70
+ return (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: cell.placeholder, color: cell.origin === 'override' && !cell.placeholder ? 'cyan' : undefined, children: text }), ' '.repeat(Math.max(0, width - termWidth(text)))] }));
71
+ }
72
+ export function SpawnScreen({ rows, sel, top, capacity, configured, dirty, editing, editable, fieldTitle, fieldModel, fieldPrompt, cursor, example, columns, note, }) {
73
+ const width = Math.max(20, (columns || 80) - 4);
74
+ const col = spawnColumns(width, rows);
75
+ const window = rows.slice(top, top + capacity);
76
+ const current = rows[sel];
77
+ /** La riga disegnata `field` è quella in fuoco? Il cursore indicizza le sole
78
+ * celle editabili, la resa le disegna tutte e tre. */
79
+ const focused = (field) => editing && editable[cursor.row] === field;
80
+ const textW = Math.max(10, width - LABEL_W - CARET_OFF.length);
81
+ // L'anteprima si fa sul titolo GIÀ ridotto, come lo farà `deck-run`: mostrare
82
+ // il template interpolato ma non ridotto prometterebbe un titolo che la tab
83
+ // non porta.
84
+ const rendered = saneNote(interpolate(fieldTitle, example) ?? '');
85
+ 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: "azioni di spawn" }), ' ', _jsxs(Text, { dimColor: true, children: ["\u00B7 ", rows.length, " azioni \u00B7 ", configured, " configurate"] }), dirty ? _jsx(Text, { color: "yellow", children: " \u00B7 modifiche non salvate" }) : null] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "yellow", children: "\u2191\u2193" }), " riga \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), ' ', editing ? 'conferma' : 'modifica', " \u00B7 ", _jsx(Text, { color: "yellow", children: "CANC" }), " torna al default \u00B7", ' ', _jsx(Text, { color: "yellow", children: "w" }), " salva \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), ' ', editing ? 'annulla' : 'chiude'] }), _jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, marginTop: 1, children: [_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [CARET_OFF, ' '.repeat(MARK_W), pad('funzione', col.func), " ", pad('titolo', col.title), " ", pad('modello', col.model), ' ', pad('prompt', col.prompt)] }), window.map((r, i) => {
86
+ const at = top + i;
87
+ return (_jsxs(Text, { wrap: "truncate-end", children: [at === sel ? CARET : CARET_OFF, _jsx(Text, { color: "cyan", children: pad(r.touched ? TOUCHED : '', MARK_W) }), _jsx(Text, { bold: at === sel, children: pad(cut(r.action.label, col.func), col.func) }), ' ', _jsx(Cell, { row: r, field: "title", value: r.raw.title, width: col.title }), ' ', _jsx(Cell, { row: r, field: "model", value: r.raw.model, width: col.model }), ' ', _jsx(Cell, { row: r, field: "prompt", value: r.raw.prompt, width: col.prompt })] }, r.action.id));
88
+ })] }), editing ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [SPAWN_FIELDS.map((field) => {
89
+ const fixed = current.action.fixed[field];
90
+ if (fixed !== undefined) {
91
+ return (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [CARET_OFF, FIELD_LABEL[field].padEnd(LABEL_W), "(", fixed, ")"] }, field));
92
+ }
93
+ if (field === 'model') {
94
+ return (_jsx(ChoiceRow, { label: "modello", values: MODELS, index: Math.max(0, MODELS.indexOf(fieldModel)), focused: focused('model'), width: width }, field));
95
+ }
96
+ const value = field === 'title' ? fieldTitle : fieldPrompt;
97
+ return (_jsxs(Text, { wrap: "truncate-end", children: [focused(field) ? CARET : CARET_OFF, _jsx(Text, { dimColor: true, children: FIELD_LABEL[field].padEnd(LABEL_W) }), _jsx(FieldText, { value: value, caret: cursor.caret, focused: focused(field), cols: textW })] }, field));
98
+ }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [CARET_OFF, 'esempio'.padEnd(LABEL_W), current.action.title === null ? ('nessun titolo per questa azione') : (_jsx(Text, { color: rendered ? 'green' : undefined, children: rendered ? cut(rendered, textW) : 'titolo a vuoto: nessuna nota' }))] })] })) : null, note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: sanitize(note) }) : null] }));
99
+ }
package/dist/viewport.js CHANGED
@@ -95,6 +95,10 @@ export const MODAL_HEIGHT = {
95
95
  // 77 `WRAP` e 147 `misto`, cioè una lista che non entra in nessun box sopra i
96
96
  // pane. Altezza da `wrapListCapacity`.
97
97
  wrap: 0,
98
+ // T161 — la pagina delle azioni di spawn: ottava sostitutiva. Undici righe di
99
+ // tabella più l'area di compilazione: in un box sopra i pane resterebbero due
100
+ // colonne per il prompt. Altezza da `spawnListCapacity`.
101
+ spawn: 0,
98
102
  };
99
103
  /**
100
104
  * Distribuisce le righe disponibili fra lista task, lista sessioni e preview.
@@ -335,6 +339,37 @@ const WRAP_CHROME = 12;
335
339
  export function wrapListCapacity(rows) {
336
340
  return Math.max(0, (rows || 24) - SLACK - WRAP_CHROME);
337
341
  }
342
+ // T161 — cornice della pagina delle azioni di spawn.
343
+ // 2 bordi del box esterno
344
+ // 1 riga di testata (nome della pagina · quante righe · quante configurate)
345
+ // 1 riga hint
346
+ // 1 marginTop del box tabella
347
+ // 2 bordi del box tabella
348
+ // 1 riga di intestazione delle colonne
349
+ const SPAWN_CHROME = 8;
350
+ // L'area di compilazione: marginTop + le TRE righe (titolo, modello, prompt) +
351
+ // la riga dell'anteprima resa.
352
+ //
353
+ // Fisse e non proporzionali alle celle editabili della riga aperta: su `bare`,
354
+ // che ha il solo modello, le altre due si disegnano comunque per dire perché non
355
+ // si modificano. Un'altezza che dipendesse dalla riga selezionata farebbe
356
+ // saltare la tabella di due righe a ogni movimento del caret, e il budget
357
+ // dovrebbe sapere quale riga è aperta per non sottostimare — lo stesso motivo
358
+ // per cui le cinque righe del detail sono dentro `DETAIL_CHROME` invece che
359
+ // condizionali (T111 · D1).
360
+ const SPAWN_FIELDS_CHROME = 5;
361
+ /**
362
+ * Righe di tabella che entrano nel terminale.
363
+ *
364
+ * Le righe dell'area aperta e la riga di stato si pagano QUI, non altrove: la
365
+ * pagina è una schermata sostitutiva, quindi la nota che le sue azioni scrivono
366
+ * la disegna lei — a differenza della lista, dove la riga di stato sta fuori dai
367
+ * pane e il suo costo è già in `layoutBudget`.
368
+ */
369
+ export function spawnListCapacity(rows, editing = false, noteLine = false) {
370
+ const extra = (editing ? SPAWN_FIELDS_CHROME : 0) + (noteLine ? 1 : 0);
371
+ return Math.max(0, (rows || 24) - SLACK - SPAWN_CHROME - extra);
372
+ }
338
373
  /**
339
374
  * Righe scorse da una pressione di PagGiù/PagSu: METÀ della capienza visibile.
340
375
  *
package/dist/wrap-scan.js CHANGED
@@ -84,23 +84,29 @@ export function mixedCount(files) {
84
84
  /** Radice di default dell'area di srotolamento: la project root intera (D4). */
85
85
  export const WRAP_DEFAULT_PATH = '.';
86
86
  /**
87
- * Il prompt della sessione che srotola un path.
87
+ * T161 — il buco `{path}` del titolo e del prompt di una sessione di
88
+ * srotolamento: il path reso a parole.
88
89
  *
89
- * CABLATO per decisione, non per fretta (D9): parametrizzarlo prima di aver
90
- * visto come si comporta significherebbe scegliere i parametri a occhio. Le tre
91
- * cose che dice sono le tre che rendono l'operazione reversibile e verificabile
92
- * — backup fuori dal repo, rapporto, commit solo se il diff è chiaramente
93
- * innocuo — e nessuna delle tre è una preferenza da esporre.
90
+ * Le barre diventano spazi e l'estensione cade QUI, prima di `_sane_note`, il
91
+ * cui alfabeto non ha né `/` né `.`: lasciandoceli cadere le parole si
92
+ * salderebbero (`runtimereferencedoc-systemmd`), e il titolo della tab
93
+ * divergerebbe da quello che la lista del deck legge dal sidecar.
94
94
  *
95
- * Il `<path>` è l'unico buco. Arriva dal campo della lista e finisce dentro
96
- * `--prompt` come argv singolo, quindi non passa da nessuna shell prima di
97
- * `deck-run`, che lo quota ad apici singoli: non c'è quoting da fare qui.
95
+ * Il path di default (`.`, la project root intera) non ha parole da mostrare e
96
+ * diventa una frase: interpolato in `📏 {path}` darebbe il solo prefisso, cioè
97
+ * un titolo uguale per ogni srotolamento.
98
+ *
99
+ * L'emoji del prefisso non sta più qui: è dentro il template della riga
100
+ * `unwrap` del catalogo delle azioni, dove il progetto può riscriverla.
98
101
  */
99
- export function wrapPrompt(path) {
102
+ export function wrapWords(path) {
100
103
  const target = path.trim() || WRAP_DEFAULT_PATH;
101
- return (`lancia md-wrap modo apply su ${target} e backup in cartella tmp dedicata. ` +
102
- 'verifica risultato e fai rapporto. se le modifiche sono tutte chiaramente safe, ' +
103
- 'puoi committare direttamente');
104
+ if (target === WRAP_DEFAULT_PATH)
105
+ return 'tutto il progetto';
106
+ return target
107
+ .replace(/\.md$/i, '')
108
+ .replace(/[./]+/g, ' ')
109
+ .trim();
104
110
  }
105
111
  /**
106
112
  * Cartella della cache, una per utente e con `mode 0700` — stessa ragione del
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lamemind/loom-deck",
3
- "version": "0.66.1",
3
+ "version": "0.68.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": {