@lamemind/loom-deck 0.67.0 → 0.69.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/README.md CHANGED
@@ -74,7 +74,7 @@ Regola unica, senza eccezioni — pensata per reggere l'aggiunta di nuove azioni
74
74
  | `⏎` | azione primaria del pane | Tasks → apre il **detail** della task selezionata · Sessions → riprende (`claude --resume`) la sessione selezionata |
75
75
  | **MAIUSCOLA** | **apre un modale** | cattura tutti i tasti; `esc` annulla, non esce |
76
76
  | minuscola | azione immediata, one-shot | |
77
- | `CTRL`+lettera | idiomi universali, toggle dentro i modali di testo, varianti di un'azione | `^F` = find, come ovunque · `^K`/`^P`/`^R` = spawn con un altro prompt · `^G`/`^O` = genera/apri il project status · `^U` = update del gitlink |
77
+ | `CTRL`+lettera | idiomi universali, toggle dentro i modali di testo, varianti di un'azione | `^F` = find, come ovunque · `^K`/`^P`/`^R` = spawn con un altro prompt · `^G`/`^O` = genera/apri il project status · `^U` = update del gitlink · `^S` = pagina delle azioni di spawn |
78
78
  | `1`…`9` | voce `launch` n-esima del progetto | da `.claude/loom-works.json` |
79
79
  | `esc` | chiude l'overlay aperto | in modalità normale è inerte: **nessuna lettera chiude il deck** |
80
80
  | `^C` `^C` | chiude il deck | la prima pressione avverte in riga di stato, la seconda entro **5 secondi** esce; passata la finestra si riparte dall'avviso. Da dentro un modale il primo `^C` riporta alla lista — l'avviso vive nella riga di stato, che le schermate a pieno frame non disegnano |
@@ -95,6 +95,7 @@ Assegnazioni correnti:
95
95
  | modale | `⏎` | **detail** della task selezionata: task file scrollabile + area di compilazione a quattro righe |
96
96
  | modale | `CANC` | **elimina** — la task selezionata; su una riga meta della vista `archiviabili`, l'intero insieme |
97
97
  | modale | `^O` | apre il **project status** in cache (viewer fullscreen) |
98
+ | modale | `^S` | **azioni di spawn**: la tabella di tutto ciò che il deck apre, con titolo/modello/prompt configurabili per progetto |
98
99
  | immediata | `^G` | **genera** il project status (skill headless, dura minuti) |
99
100
  | immediata | `^U` | **bumpa il gitlink** dei submodule idonei (solo su un progetto con `.gitmodules`) |
100
101
  | immediata | `^K` | spawna la task selezionata col prompt di **recap** |
@@ -109,7 +110,7 @@ Assegnazioni correnti:
109
110
  Il footer è **due righe con due nature diverse**:
110
111
 
111
112
  ```
112
- ⏎ detail · ^K/^P/^R spawn · ^G genera status · ^O apri status · ^U bumpa gitlink · ^F cerca · C nuova · E edit · S sort · F filtri · w salva
113
+ ⏎ detail · ^K/^P/^R spawn · ^G genera status · ^O apri status · ^U bumpa gitlink · ^S azioni · ^F cerca · C nuova · E edit · S sort · F filtri · w salva
113
114
  t 💻 · c 🤖 · 1 📝 codium · 2 ☕ idea
114
115
  ```
115
116
 
@@ -161,42 +162,57 @@ Da qui la forma della conferma: nomina il bersaglio (quante e quali ID, troncand
161
162
 
162
163
  Sulla task selezionata (focus sul pane Tasks) si apre una sessione **bound**: `LOOM_TASK` iniettata, `sessionId` pinnato, binding scritto nel sidecar. Fra i modi cambia **solo il prompt iniziale**.
163
164
 
164
- | Azione (detail) | Tasto diretto | Prompt |
165
+ | Azione (detail) | Tasto diretto | Prompt di default |
165
166
  |---|---|---|
166
167
  | `open` | — | nessuno — il contesto lo carica l'hook `SessionStart`, il primo messaggio lo scrivi tu |
167
- | `status` | `^K` | `recap stato task <id>` — prompt diretto, nessuna skill |
168
+ | `status` | `^K` | `/loom-works:recap-status <id>`, che dal detail si specializza in `-task` o `-epic` |
168
169
  | `preflight` | `^P` | `/loom-works:preflight-task <id>` |
169
- | `run` | `^R` | `/loom-works:run-task <id>`, oppure `/loom-works:run-doc <id>` se l'id è una `D` |
170
+ | `run` | `^R` | `/loom-works:run-task <id>` |
170
171
  | `checkpoint` | — | `/loom-works:checkpoint-task <id>` |
171
172
 
172
173
  Il prompt iniziale è la scelta di **chi apre**, non una proprietà della task: lo stesso task file si apre per leggerne lo stato, per congelarne le decisioni, per eseguirlo o per entrarci a mani nude — e l'azione scelta *è* quell'intento.
173
174
 
175
+ Sono **default**, non costanti: titolo, modello e prompt di ognuna di queste azioni si riscrivono dalla pagina `^S` e si salvano nel progetto (§`^S` — le azioni di spawn sotto).
176
+
174
177
  Le due superfici sono complementari, non alternative. `⏎` apre il **detail** e lì si legge la Description *mentre* si decide: è il caso normale, perché la domanda «quale azione?» quasi sempre si risponde leggendo la task. I tre `CTRL` saltano il passaggio per chi lo sa già. Dentro il detail sono inerti — i bottoni sono lì.
175
178
 
176
179
  Col focus sul pane Sessions né `⏎` né i `CTRL` spawnano: l'oggetto dell'azione è la task selezionata, e senza quel pane a fuoco non ce n'è una (il deck lo dice nella riga di nota, invece di aprire qualcosa a caso).
177
180
 
178
181
  ### `⏎` — il detail della task
179
182
 
180
- Overlay fullscreen a due zone: il **task file per intero**, scrollabile con `PgUp`/`PgDn`, e sotto un'**area di compilazione** a quattro righe — i quattro parametri con cui la sessione sta per nascere.
183
+ Overlay fullscreen a due zone: il **task file per intero**, scrollabile con `PgUp`/`PgDn`, e sotto un'**area di compilazione** a cinque righe — i parametri con cui la sessione sta per nascere.
181
184
 
182
185
  | Riga | Cosa | Come si cambia |
183
186
  |---|---|---|
184
187
  | `azione` | `open` `preflight` `run` `status` `checkpoint` | `←→`, oppure l'iniziale: `o` `p` `r` `s` `c` |
185
188
  | `prompt` | il testo che riceverà la sessione | si scrive; parte pre-riempito dall'azione scelta |
186
189
  | `modello` | `fable` `opus` `sonnet` `haiku` | `←→` |
187
- | `titolo` | il nome della conversazione, in lista e nella tab | si scrive |
190
+ | `titolo` | il nome della conversazione, in lista e nella tab | si scrive; parte pre-riempito col template dell'azione, reso su questa task |
191
+ | `priorità` | la conversazione nasce già marcata | `←→` |
188
192
 
189
- `↑↓` spostano il fuoco fra le righe, `⏎` spawna coi quattro valori correnti, `esc` chiude lasciando la lista con la stessa selezione. Le righe di testo ricevono i caratteri **solo quando sono in fuoco**: su `azione` una lettera che non è un'iniziale resta inerte invece di finire in un campo.
193
+ `↑↓` spostano il fuoco fra le righe, `⏎` spawna coi valori correnti, `esc` chiude lasciando la lista con la stessa selezione. Le righe di testo ricevono i caratteri **solo quando sono in fuoco**: su `azione` una lettera che non è un'iniziale resta inerte invece di finire in un campo.
190
194
 
191
195
  `↑↓` sono quindi una risorsa contesa, e vanno al fuoco: la lettura del task file resta su `PgUp`/`PgDn`, cioè una granularità sola invece di due. È il prezzo di avere `↑↓` nel loro significato di sempre su una schermata che ospita insieme un documento e dei campi.
192
196
 
193
- **Il campo `prompt` è quello che parte davvero**, non un'anteprima: modificarlo cambia il messaggio che la sessione riceve. Cambiare azione lo riscrive col default della nuova — col fuoco per riga il cambio accidentale non esiste (`←→` toccano l'azione solo dalla sua riga), quindi non c'è nessuna modifica da proteggere.
197
+ **I tre campi mostrano quello che parte davvero**, non un'anteprima: modificarli cambia il comando. Cambiare azione li riscrive coi default della nuova — col fuoco per riga il cambio accidentale non esiste (`←→` toccano l'azione solo dalla sua riga), quindi non c'è nessuna modifica da proteggere. Un campo titolo svuotato a mano significa **nessuna nota**: il default è già a schermo, quindi non c'è nessun fallback invisibile da far scattare.
194
198
 
195
199
  La grammatica delle righe è la **stessa** del modale edit (`SHIFT+E`) e sta in `src/fields.ts`: descrittore delle righe, fuoco, caret, `^A`/`^E`/`^D`/`^U`. Finché era scritta due volte andava tenuta allineata a mano a ogni tasto aggiunto. `Home`/`End` non ci sono e non possono esserci: `useInput` non le espone — verificato su otto sequenze diverse, arrivano tutte come input vuoto senza flag — da cui `^A`/`^E`.
196
200
 
197
201
  Le azioni non sono un catalogo nuovo: chiamano lo **stesso** percorso di spawn dei tasti diretti. Una superficie in più, zero percorsi di spawn in più. Sono bottoni affiancati e non voci di un menu verticale perché è la forma che sopravvive all'arrivo del mouse: un rettangolo ha già coordinate e area cliccabile, una lista di voci andrebbe rifatta.
198
202
 
199
- Il **testo** dei prompt vive in `scripts/prompt-catalog`, un file dati sibling di `deck-run` che entrambi leggono a runtime: il deck per pre-riempire il campo, `deck-run` per comporre il comando in-tab. Come codice posseduto da un lato solo il deck avrebbe dovuto riscriverlo per mostrarlo, e le due copie divergerebbero alla prima voce aggiunta da una parte sola. Un prompt modificato a mano viaggia poi letterale (`--prompt`), perché a quel punto nessun `--prompt-kind` lo descrive più.
203
+ Il **testo** dei prompt vive in `scripts/prompt-catalog`, un file dati sibling di `deck-run` che entrambi leggono a runtime: il deck per pre-riempire il campo, `deck-run` per comporre il comando in-tab quando lo si invoca a mano. Come codice posseduto da un lato solo il deck avrebbe dovuto riscriverlo per mostrarlo, e le due copie divergerebbero alla prima voce aggiunta da una parte sola. Dal deck il prompt viaggia comunque **letterale** (`--prompt`), su ogni percorso: la risoluzione — default cablato, riga del catalogo dati, override di progetto — sta tutta da questo lato, e un simbolo non saprebbe più descrivere il testo che ne esce.
204
+
205
+ ### `^S` — le azioni di spawn
206
+
207
+ Una pagina fullscreen con **una riga per ogni cosa che il deck sa aprire** e quattro colonne: funzione, titolo, modello, prompt. Le ultime tre si modificano, e `w` scrive il blocco `spawn` di `.claude/loom-works.json` — committato, quindi la configurazione viaggia col repo.
208
+
209
+ Due livelli: `↑↓` scorrono la tabella, `⏎` apre sotto di essa l'area di compilazione della riga selezionata (la stessa grammatica del detail), `⏎` conferma in memoria, `esc` annulla. `CANC` toglie l'override di una riga e la riporta al default; comporre non tocca il disco finché non si preme `w`.
210
+
211
+ Le celle portano il **template grezzo**, coi buchi ancora dentro — `📐 {slug}`, `📏 {path}` — e l'area mostra accanto l'esempio reso: un valore già interpolato si modificherebbe a partire da un esempio, e ogni modifica cancellerebbe il buco senza dirlo. Una riga con almeno un override si marca con `◆`.
212
+
213
+ Non tutte le celle esistono per tutte le righe, e quelle che non esistono **dicono perché** invece di restare vuote: la sessione nuda non pinna un `sessionId` e quindi non ha un titolo, il project status gira headless e non apre nessuna tab, il prompt del drain è una funzione della natura del file inbox. Resume e fork non hanno una riga affatto — il loro modello è ereditato dalla conversazione d'origine, non una preferenza.
214
+
215
+ Un file assente, illeggibile o con una voce sbagliata non rompe niente: il deck parte sui default e lo dice in riga di stato, una voce alla volta.
200
216
 
201
217
  ### `f` — forkare una conversazione
202
218
 
package/dist/actions.js CHANGED
@@ -15,18 +15,19 @@
15
15
  // `test/modes-smoke.test.ts` (`seleziona una task`, `terminale su`, `deck-run`,
16
16
  // `nessun push`, `eliminare N task?`, `scartate`): si copiano verbatim, non si
17
17
  // migliorano di passaggio.
18
- import { useMemo, useRef } from 'react';
18
+ import { useRef } from 'react';
19
19
  import { randomUUID } from 'node:crypto';
20
- import { loadPromptCatalog, modelFor } from './prompt-catalog.js';
20
+ import { taskSlug } from './tasks.js';
21
21
  import { appendNote, appendPin, appendPriority, appendSessionRecord, appendTaskBinding, } from './task-index.js';
22
22
  import { neighborId } from './session-list.js';
23
23
  import { cut, cutMiddle } from './width.js';
24
24
  import { saveView, viewFilePath } from './view-store.js';
25
- import { onInTabCommand, runLaunch, spawnClaudeEmpty, spawnBare, spawnDeck, spawnDeckFork, spawnDeckResume, spawnTerminal, fallbackTitle, DECK_RUN, MODEL_DEFAULT, } from './spawn.js';
25
+ import { onInTabCommand, runLaunch, spawnClaudeEmpty, spawnBare, spawnDeck, spawnDeckFork, spawnDeckResume, spawnTerminal, DECK_RUN, } from './spawn.js';
26
+ import { resolveSpawnId } from './spawn-config.js';
26
27
  import { useTaskOps } from './task-ops.js';
27
- import { inboxTitle } from './inbox.js';
28
- import { wrapTitle } from './wrap-scan.js';
29
- export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, setNote, }) {
28
+ import { inboxWords } from './inbox.js';
29
+ import { wrapWords } from './wrap-scan.js';
30
+ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, overrides, catalog, setNote, }) {
30
31
  // La riga di stato di OGNI spawn di sessione Claude: il comando esatto, come
31
32
  // lo si scriverebbe in bash, invece di una parafrasi.
32
33
  //
@@ -60,9 +61,6 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, setNo
60
61
  // prima dell'exec), ed è anche il comando da ripetere a mano per vedere
61
62
  // l'errore. Il salto fra le due dura i millisecondi che `deck-run` impiega a
62
63
  // comporre.
63
- // T152 — stessa fonte del detail (sheet.ts): un file di sei righe accanto al
64
- // codice, letto una volta per vita del deck.
65
- const catalog = useMemo(() => loadPromptCatalog(), []);
66
64
  const spawnSeq = useRef(0);
67
65
  function noteSpawn(spawned) {
68
66
  noteCommand(spawned.cmd);
@@ -76,6 +74,35 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, setNo
76
74
  noteCommand(inTab);
77
75
  });
78
76
  }
77
+ /**
78
+ * T161 — la TERNA di un'azione: default del deck ← override di progetto, coi
79
+ * buchi già riempiti.
80
+ *
81
+ * Un punto solo. Prima di questa task ogni sede di chiamata sceglieva da sé —
82
+ * `MODEL_DEFAULT` dentro `drainInbox`, `'sonnet'` dentro `unwrapPath`,
83
+ * `modelFor(catalog, kind)` dentro `spawnTaskSession`, `PROJECT_STATUS_MODEL`
84
+ * dentro `spawnProjectStatus` — e nessuna delle quattro poteva essere
85
+ * indirizzata da una configurazione, perché non esisteva un nome con cui
86
+ * nominarla.
87
+ */
88
+ function spawnDefaults(id, holes = {}) {
89
+ return resolveSpawnId(id, overrides, catalog, holes);
90
+ }
91
+ /** I buchi di un'azione su task. Lo slug viene dal NOME del task file, non
92
+ * dalla descrizione di `tasks.md`: il nome è già dentro l'alfabeto di
93
+ * `_sane_note` per costruzione — minuscolo, separato da trattini, senza
94
+ * punteggiatura — mentre la descrizione porta apostrofi e `/` che la riduzione
95
+ * toglie senza sostituto, saldando le parole. Task file assente → slug vuoto,
96
+ * e il titolo si spegne invece di ridursi al solo prefisso. */
97
+ function taskHoles(id) {
98
+ return { TASK: id, slug: taskSlug(tasksDir, id) };
99
+ }
100
+ /** La terna di un'azione SU UNA TASK, per chi ha in mano l'id e non i buchi:
101
+ * il detail. Derivare i buchi dal chiamante sarebbe la seconda copia della
102
+ * regola, e diverge il giorno che un template ne guadagna uno terzo. */
103
+ function spawnTaskDefaults(id, taskId) {
104
+ return spawnDefaults(id, taskHoles(taskId));
105
+ }
79
106
  // T66 — la guardia dello spawn bound, con due chiamanti: gli acceleratori
80
107
  // della lista e l'apertura del detail. Le tre uscite sono le stesse (pane
81
108
  // sbagliato, riga meta, task sparita), e duplicarle vorrebbe dire tenerne
@@ -116,9 +143,9 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, setNo
116
143
  // nomi diversi per la stessa conversazione. Vale anche sugli acceleratori
117
144
  // della lista (^K/^P/^R), che passano di qui con `spawnNote` sul proprio
118
145
  // default '' (P1 preflight) — nascono quindi titolati anche loro.
119
- function spawnForTask(id, kind, modelKind, spawnNote = '', prompt, priority = false) {
146
+ function spawnForTask(id, modelKind, spawnNote = '', prompt = '', priority = false) {
120
147
  const sid = randomUUID();
121
- const note = spawnNote || fallbackTitle(tasksDir, id, kind) || '';
148
+ const note = spawnNote;
122
149
  appendTaskBinding(cwd, sid, id);
123
150
  if (note)
124
151
  appendNote(cwd, sid, note);
@@ -131,7 +158,7 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, setNo
131
158
  // riempirebbe il sidecar di smarcature di conversazioni mai marcate.
132
159
  if (priority)
133
160
  appendPriority(cwd, sid, true);
134
- const spawned = spawnDeck(id, cwd, sid, kind, modelKind, note, prompt);
161
+ const spawned = spawnDeck(id, cwd, sid, modelKind, note, prompt);
135
162
  spawned.child.on('error', () => setNote(`⚠ spawn ${id} fallito (${DECK_RUN})`));
136
163
  // Il modello resta SEMPRE visibile anche quando è il default, perché è un
137
164
  // argomento esplicito del comando (T108): gli acceleratori della lista non
@@ -147,11 +174,16 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, setNo
147
174
  // invece deve restare vivo.
148
175
  function spawnTaskSession(kind, keyLabel) {
149
176
  const task = selectedTaskOr(keyLabel, 'spawnare');
150
- // T152 — stesso catalogo del detail: senza, `^R` partirebbe sul default
151
- // fisso mentre l'azione `run` del detail parte sul modello del kind, e le
152
- // due superfici divergerebbero sullo stesso comando.
153
- if (task)
154
- spawnForTask(task.id, kind, modelFor(catalog, kind) ?? MODEL_DEFAULT);
177
+ if (!task)
178
+ return;
179
+ // T161 — stessa risoluzione del detail, sullo stesso id di catalogo: senza,
180
+ // `^R` e l'azione `run` del detail partirebbero su due terne diverse per lo
181
+ // stesso comando. Il kind NON si specializza qui: gli acceleratori leggono
182
+ // solo `tasks.md`, dove il `Size` non c'è, quindi `recap` resta il
183
+ // dispatcher — che è la riga di catalogo giusta per chi non sa se la task è
184
+ // un cappello.
185
+ const d = spawnDefaults(kind, taskHoles(task.id));
186
+ spawnForTask(task.id, d.model, d.title ?? '', d.prompt ?? '');
155
187
  }
156
188
  // T49 — resume di una conversazione in una nuova tab. Unico punto: lo chiamano
157
189
  // il `⏎` della lista sessioni e quello sulla riga-sessione della ricerca, che
@@ -332,11 +364,13 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, setNo
332
364
  * darebbe due tabelle capaci di divergere, e la seconda si scoprirebbe solo
333
365
  * il giorno in cui una natura nuova apre la skill sbagliata.
334
366
  *
335
- * Sessione NUDA e modello esplicito: il drain lavora sulla doc, non sulla
336
- * task (D10 preflight), e `MODEL_DEFAULT` passa nell'argv anche essendo il
337
- * default, come `permissionMode`.
367
+ * Sessione NUDA: il drain lavora sulla doc, non sulla task (D10 preflight).
368
+ * Modello e titolo vengono dalla riga `drain` del catalogo delle azioni
369
+ * (T161), non più da un letterale scritto qui; il PROMPT resta del chiamante,
370
+ * perché la skill è una funzione della natura del file e quella mappa vive in
371
+ * un posto solo (`inboxPrompt`, `src/inbox.ts`).
338
372
  *
339
- * Il TITOLO lo deriva `inboxTitle` dal file, e serve un `sessionId` pinnato
373
+ * Il titolo interpola il basename del file, e serve un `sessionId` pinnato
340
374
  * per scriverlo nel sidecar — senza, la nota non avrebbe una chiave e la riga
341
375
  * in lista resterebbe nuda mentre la tab porta un nome. Le due scritture
342
376
  * stanno qui accanto per la stessa ragione di `spawnForTask`: il nome che le
@@ -344,26 +378,36 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, setNo
344
378
  */
345
379
  function drainInbox(file, prompt) {
346
380
  const sid = randomUUID();
347
- const title = inboxTitle(file);
348
- appendNote(cwd, sid, title);
349
- const spawned = spawnBare(cwd, prompt, MODEL_DEFAULT, sid, title);
381
+ const d = spawnDefaults('drain', { file: inboxWords(file) });
382
+ const title = d.title ?? '';
383
+ if (title)
384
+ appendNote(cwd, sid, title);
385
+ const spawned = spawnBare(cwd, prompt, d.model, sid, title);
350
386
  spawned.child.on('error', () => setNote(`⚠ drain ${file.basename} fallito (${DECK_RUN})`));
351
387
  noteSpawn(spawned);
352
388
  }
353
389
  /**
354
390
  * T134 — apre la sessione che SROTOLA l'hard-wrap di un path.
355
391
  *
356
- * `sonnet` e non il default (D9): lo srotolamento è una passata meccanica con
357
- * un verificatore deterministico dietro (`md-wrap --apply` confronta le due
358
- * versioni normalizzate `\s+ → spazio` e rimette indietro il file se
359
- * differiscono), quindi il giudizio richiesto al modello è leggere un diff e
360
- * decidere se committarlo — non progettare niente.
392
+ * Terna intera dalla riga `unwrap` del catalogo (T161): il modello, il titolo
393
+ * e anche il PROMPT, che fino a T134 era cablato in `wrap-scan.ts`. A
394
+ * differenza del drain qui un template unico basta — la skill è sempre la
395
+ * stessa e l'unico buco è il `{path}`.
396
+ *
397
+ * Il default resta `sonnet` e non il modello di default del deck (D9): lo
398
+ * srotolamento è una passata meccanica con un verificatore deterministico
399
+ * dietro (`md-wrap --apply` confronta le due versioni normalizzate
400
+ * `\s+ → spazio` e rimette indietro il file se differiscono), quindi il
401
+ * giudizio richiesto al modello è leggere un diff e decidere se committarlo —
402
+ * non progettare niente.
361
403
  */
362
- function unwrapPath(path, prompt) {
404
+ function unwrapPath(path) {
363
405
  const sid = randomUUID();
364
- const title = wrapTitle(path);
365
- appendNote(cwd, sid, title);
366
- const spawned = spawnBare(cwd, prompt, 'sonnet', sid, title);
406
+ const d = spawnDefaults('unwrap', { path: wrapWords(path) });
407
+ const title = d.title ?? '';
408
+ if (title)
409
+ appendNote(cwd, sid, title);
410
+ const spawned = spawnBare(cwd, d.prompt ?? '', d.model, sid, title);
367
411
  spawned.child.on('error', () => setNote(`⚠ srotolamento ${path} fallito (${DECK_RUN})`));
368
412
  noteSpawn(spawned);
369
413
  }
@@ -406,6 +450,8 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, setNo
406
450
  return {
407
451
  ...taskOps,
408
452
  selectedTaskOr,
453
+ spawnDefaults,
454
+ spawnTaskDefaults,
409
455
  spawnForTask,
410
456
  spawnTaskSession,
411
457
  resumeSession,
package/dist/cli.js CHANGED
@@ -14,13 +14,15 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
14
14
  // come valore, e prima di questo split la leggeva da una closure risolta solo
15
15
  // all'arrivo dell'evento, cioè dopo la fine del render.
16
16
  import { render, Box, Text } from 'ink';
17
- import { useState } from 'react';
17
+ import { useMemo, useState } from 'react';
18
18
  import { resolveTasksPath, resolveTasksDir } from './tasks.js';
19
19
  import { LAUNCH_SEP } from './config.js';
20
20
  import { anchorFrame, enableMouse } from './mouse.js';
21
21
  import { sanitize } from './width.js';
22
22
  import { MODEL_SHORT_LIST, modelAlias } from './glyphs.js';
23
- import { MODEL_DEFAULT, MODELS } from './spawn.js';
23
+ import { MODEL_DEFAULT, MODELS } from './spawn-catalog.js';
24
+ import { loadPromptCatalog } from './prompt-catalog.js';
25
+ import { resolveSpawnId } from './spawn-config.js';
24
26
  import { BARE_BUTTONS_WIDTH, BARE_LABEL, deckLegend, frameGeometry, headlineWidth, indicatorRow, launchRow, } from './frame.js';
25
27
  import { choiceButtons } from './ui/fields.js';
26
28
  import { useDeckModel } from './deck-model.js';
@@ -32,10 +34,11 @@ import { useAssignOverlay } from './overlays/assign.js';
32
34
  import { useProjectStatus } from './overlays/status.js';
33
35
  import { useInboxOverlay } from './overlays/inbox.js';
34
36
  import { useWrapOverlay } from './overlays/wrap.js';
37
+ import { useSpawnPage } from './overlays/spawn.js';
35
38
  import { useGitlink } from './overlays/gitlink.js';
36
39
  import { usePurgeOverlay } from './overlays/purge.js';
37
40
  import { useTextModals, useViewModals } from './overlays/modals.js';
38
- import { useTerminalSize } from './hooks.js';
41
+ import { useTerminalSize, useSpawnConfig } from './hooks.js';
39
42
  import { EditModal, FilterModal, PurgeModal, SortModal } from './ui/modals.js';
40
43
  import { StatusHeadline } from './ui/status-screen.js';
41
44
  import { InboxPane, SessionsPane, TasksPane } from './ui/panes.js';
@@ -50,8 +53,29 @@ function Deck({ cwd, tasksPath, tasksDir }) {
50
53
  const [mode, setMode] = useState('normal');
51
54
  // Dimensioni vive del terminale: sono l'input del budget d'altezza.
52
55
  const { rows, columns } = useTerminalSize();
53
- const model = useDeckModel({ cwd, tasksPath, tasksDir, setNote });
54
- const actions = useDeckActions({ cwd, tasksPath, tasksDir, columns, model, setNote });
56
+ // T161 — il catalogo dati dei prompt e gli override di progetto: i due
57
+ // gradini del default di ogni azione di spawn. Si leggono una volta per vita
58
+ // del deck e scendono a chi risolve la terna (l'attuatore, il detail) e a chi
59
+ // la configura (la pagina), invece di essere riletti a ogni sede di chiamata.
60
+ const catalog = useMemo(() => loadPromptCatalog(), []);
61
+ const spawnConfig = useSpawnConfig({ cwd, setNote });
62
+ const model = useDeckModel({
63
+ cwd,
64
+ tasksPath,
65
+ tasksDir,
66
+ bareModel: resolveSpawnId('bare', spawnConfig.overrides, catalog).model,
67
+ setNote,
68
+ });
69
+ const actions = useDeckActions({
70
+ cwd,
71
+ tasksPath,
72
+ tasksDir,
73
+ columns,
74
+ model,
75
+ overrides: spawnConfig.overrides,
76
+ catalog,
77
+ setNote,
78
+ });
55
79
  const assign = useAssignOverlay({
56
80
  viewTasks: model.viewTasks,
57
81
  rows,
@@ -65,6 +89,10 @@ function Deck({ cwd, tasksPath, tasksDir }) {
65
89
  columns,
66
90
  setMode,
67
91
  setNote,
92
+ // T161 — il detail chiede la terna per ID DI CATALOGO e task aperta; chi la
93
+ // risolve è l'attuatore, che ha già in mano override, catalogo dati e i
94
+ // fornitori dei buchi.
95
+ defaults: actions.spawnTaskDefaults,
68
96
  onAction: actions.spawnForTask,
69
97
  });
70
98
  const search = useSearchOverlay({
@@ -84,11 +112,14 @@ function Deck({ cwd, tasksPath, tasksDir }) {
84
112
  // Il nome è quello del file config quando c'è, la cartella altrimenti: la
85
113
  // testata deve dire di quale progetto parla anche su un checkout non ancora
86
114
  // registrato.
115
+ const statusDefaults = actions.spawnDefaults('project-status');
87
116
  const status = useProjectStatus({
88
117
  cwd,
89
118
  name: model.projectCore ?? model.projectName,
90
119
  rows,
91
120
  columns,
121
+ model: statusDefaults.model,
122
+ prompt: statusDefaults.prompt ?? '',
92
123
  setMode,
93
124
  setNote,
94
125
  });
@@ -116,6 +147,19 @@ function Deck({ cwd, tasksPath, tasksDir }) {
116
147
  // schermata da aprire — l'unica superficie è l'indicatore, e l'unica azione
117
148
  // committa.
118
149
  const gitlink = useGitlink({ cwd, setNote });
150
+ // T161 — la pagina delle azioni di spawn. Come il detail e la lista hard-wrap
151
+ // non esce dal deck da sé: qui riceve il saver degli override, e lo spawn che
152
+ // quegli override governano parte da tutt'altra parte.
153
+ const spawn = useSpawnPage({
154
+ cwd,
155
+ rows,
156
+ hasNote: Boolean(note),
157
+ overrides: spawnConfig.overrides,
158
+ save: spawnConfig.save,
159
+ catalog,
160
+ setMode,
161
+ setNote,
162
+ });
119
163
  const purge = usePurgeOverlay({
120
164
  setMode,
121
165
  setNote,
@@ -145,6 +189,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
145
189
  status,
146
190
  inbox,
147
191
  wrap,
192
+ spawn,
148
193
  gitlink,
149
194
  purge,
150
195
  view: viewModals,
@@ -251,7 +296,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
251
296
  projectName: model.projectName,
252
297
  taskRowData: model.taskRowData,
253
298
  hiddenTasks: model.hiddenTasks,
254
- overlays: { assign, sheet, search, status, inbox, wrap },
299
+ overlays: { assign, sheet, search, status, inbox, wrap, spawn },
255
300
  });
256
301
  if (screen)
257
302
  return screen;
@@ -26,7 +26,7 @@ import { ALL, MAX_SESSIONS, MAX_SESSIONS_ALL, META_ROWS, ROW_ALL, ROW_SPOT, SPOT
26
26
  import { applyView, epicRollup, taskColumns } from './view.js';
27
27
  import { loadView } from './view-store.js';
28
28
  import { termWidth } from './width.js';
29
- import { MODELS, MODEL_DEFAULT } from './spawn.js';
29
+ import { MODELS, MODEL_DEFAULT } from './spawn-catalog.js';
30
30
  /**
31
31
  * Conteggio dei figli per task, più il rollup delle VIVE e le conversazioni
32
32
  * senza binding.
@@ -105,13 +105,30 @@ export function childSessionsOf(sessions, bindings, selectedTaskId, isAll) {
105
105
  return selectedTaskId ? bound === selectedTaskId : !bound;
106
106
  });
107
107
  }
108
- export function useDeckModel({ cwd, tasksPath, tasksDir, setNote, }) {
108
+ export function useDeckModel({ cwd, tasksPath, tasksDir, bareModel: bareModelDefault, setNote, }) {
109
109
  const { tasks, loadError } = useTasks(tasksPath);
110
+ // Voci launch del progetto (T32): lette una volta, raggiunte per indice 1..9.
111
+ const launch = useMemo(() => loadLaunch(cwd), [cwd]);
112
+ // Identità (T37): titolo delle tab terminale spawnate col tasto `t`.
113
+ const identity = useMemo(() => loadIdentity(cwd), [cwd]);
114
+ // T53 — il core che ogni titolo di tab porta, quindi la colonna costante da
115
+ // togliere quando serve spazio. Hoistato perché lo consumano DUE schermate
116
+ // (lista e ricerca): calcolarlo su ogni call site è il modo in cui le due
117
+ // smettono di togliere la stessa cosa.
118
+ // T58 — il core è il solo `name`, non `<emoji> <name>` come la chiave di match
119
+ // di compass: qui non serve entropia (è un taglio cosmetico, non un matcher) e
120
+ // il nome nudo ripulisce anche i titoli storici, scritti quando la formula
121
+ // includeva l'owner.
122
+ // T162 — sta SOPRA `useSessions` perché ora è anche un suo ingresso: il deck
123
+ // scrive nel sidecar il residuo del titolo, e quel residuo si ottiene
124
+ // togliendo questo stesso core (§Riempimento dei buchi in `hooks.ts`).
125
+ const projectCore = identity ? identity.name : null;
126
+ const projectName = cwd.split('/').pop() || cwd;
110
127
  // `notes` esce dall'indice come `sessionNotes`: nel deck `note` è già la riga
111
128
  // di STATO in fondo al frame (il feedback di un'azione). Due concetti diversi
112
129
  // a una lettera di distanza sarebbero una trappola di lettura — e di
113
130
  // scrittura, visto che `setNote` compare in quasi ogni ramo.
114
- const { sessions, bindings, forkOf, pinned, notes: sessionNotes, priority: sessionPriority, live, reload: reloadSessions, } = useSessions(cwd);
131
+ const { sessions, bindings, forkOf, pinned, notes: sessionNotes, priority: sessionPriority, live, reload: reloadSessions, } = useSessions(cwd, projectCore);
115
132
  const [focus, setFocus] = useState('tasks');
116
133
  // T39 — selezione KEYED SU ID, non su indice. Con una vista trasformata
117
134
  // (filtro/sort) l'indice non identifica più la stessa task: leggere l'array
@@ -136,7 +153,7 @@ export function useDeckModel({ cwd, tasksPath, tasksDir, setNote, }) {
136
153
  // ha una riga, è un'azione sulla surface del cappello) e quindi non si
137
154
  // azzera mai — resta quello scelto finché non si sceglie altro, per tutta la
138
155
  // vita del deck.
139
- const [bareModel, setBareModel] = useState(MODEL_DEFAULT);
156
+ const [bareModel, setBareModel] = useState(bareModelDefault);
140
157
  // T39 — vista corrente (filtri + sort). Vive nel modello e non nell'hook dei
141
158
  // modali che la editano: è ciò che `applyView` consuma per produrre la lista,
142
159
  // è persistita su disco e la rilegge il tasto `w`. È stato del modello che due
@@ -158,20 +175,6 @@ export function useDeckModel({ cwd, tasksPath, tasksDir, setNote, }) {
158
175
  // sbagliata in silenzio (stessa trappola di T39 sulle task e T50 sulle
159
176
  // conversazioni).
160
177
  const [selInboxPath, setSelInboxPath] = useState(null);
161
- // Voci launch del progetto (T32): lette una volta, raggiunte per indice 1..9.
162
- const launch = useMemo(() => loadLaunch(cwd), [cwd]);
163
- // Identità (T37): titolo delle tab terminale spawnate col tasto `t`.
164
- const identity = useMemo(() => loadIdentity(cwd), [cwd]);
165
- // T53 — il core che ogni titolo di tab porta, quindi la colonna costante da
166
- // togliere quando serve spazio. Hoistato perché lo consumano DUE schermate
167
- // (lista e ricerca): calcolarlo su ogni call site è il modo in cui le due
168
- // smettono di togliere la stessa cosa.
169
- // T58 — il core è il solo `name`, non `<emoji> <name>` come la chiave di match
170
- // di compass: qui non serve entropia (è un taglio cosmetico, non un matcher) e
171
- // il nome nudo ripulisce anche i titoli storici, scritti quando la formula
172
- // includeva l'owner.
173
- const projectCore = identity ? identity.name : null;
174
- const projectName = cwd.split('/').pop() || cwd;
175
178
  // T136 — data dell'ultimo commit di ogni task file, per la chiave `commit`
176
179
  // della chain di sort.
177
180
  const commitAt = useCommitTimes(tasksDir, cwd);
package/dist/frame.js CHANGED
@@ -117,6 +117,10 @@ export function deckLegend(state) {
117
117
  // direbbe il contrario di ciò che il tasto fa. Stessa regola di `CANC
118
118
  // elimina tutte`, che nomina il bersaglio e non il tasto.
119
119
  state.inboxPane ? '^B sessioni' : '^B inbox',
120
+ // T161 — la voce nomina l'OGGETTO e non il gesto, al contrario di `^G`/
121
+ // `^O`/`^U`: lì il tasto fa qualcosa (genera, bumpa) e il nome dell'oggetto
122
+ // da solo sarebbe ambiguo, qui apre una pagina e basta.
123
+ '^S azioni',
120
124
  '^F cerca',
121
125
  'C nuova',
122
126
  'E edit',
package/dist/glyphs.js CHANGED
@@ -1,11 +1,11 @@
1
1
  // Glifi letterali del frame e formatter di display. Fase VISTA, ma senza JSX:
2
2
  // tutto qui è puro e testabile, e nessun sito di render scrive un glifo nudo.
3
3
  import { sanitize } from './width.js';
4
- // T148/P3 — costo accettato: la fase vista importa dalla fase effetti esterni.
5
- // Il TIPO si cancella alla compilazione (nessuna dipendenza runtime); T154
6
- // aggiunge MODELS come VALORE — una dipendenza runtime vera, ma senza ciclo:
7
- // `spawn.ts` non importa questo file, quindi il verso resta uno solo.
8
- import { MODELS } from './spawn.js';
4
+ // T161 — il catalogo dei modelli è DATO PURO (`spawn-catalog.ts`) e non importa
5
+ // nessuno: la fase vista può leggerlo senza ciclo. Prima di T161 la stessa riga
6
+ // prendeva `MODELS` da `spawn.ts`, cioè dalla fase «effetti esterni» — costo
7
+ // accettato allora (T148/P3) e caduto adesso.
8
+ import { MODELS } from './spawn-catalog.js';
9
9
  // Glifi LETTERALI del JSX. I dati passano dai loader, che sanificano al
10
10
  // confine; questi no — quindi passano da `sanitize` una volta qui, così nessun
11
11
  // sito di render scrive un glifo nudo. `↳ ○ ▸ ⏎ · − ↑ ↓` sono già concordi e