@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/README.md +26 -10
- package/dist/actions.js +86 -26
- package/dist/cli.js +51 -6
- package/dist/deck-model.js +3 -3
- package/dist/frame.js +4 -0
- package/dist/glyphs.js +5 -5
- package/dist/hooks.js +36 -0
- package/dist/inbox.js +19 -0
- package/dist/input-modes.js +1 -0
- package/dist/input.js +16 -0
- package/dist/overlays/sheet.js +24 -23
- package/dist/overlays/spawn.js +240 -0
- package/dist/overlays/status.js +2 -2
- package/dist/overlays/wrap.js +2 -2
- package/dist/prompt-catalog.js +1 -1
- package/dist/sane-note.js +115 -0
- package/dist/spawn-catalog.js +338 -0
- package/dist/spawn-config.js +280 -0
- package/dist/spawn.js +69 -157
- package/dist/tasks.js +21 -1
- package/dist/ui/detail-screen.js +1 -1
- package/dist/ui/preview.js +1 -1
- package/dist/ui/screens.js +12 -1
- package/dist/ui/spawn-screen.js +99 -0
- package/dist/viewport.js +35 -0
- package/dist/wrap-scan.js +19 -13
- package/package.json +1 -1
- package/scripts/deck-run +28 -8
- package/scripts/prompt-catalog +17 -3
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` |
|
|
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
|
|
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
|
|
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
|
|
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
|
-
**
|
|
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.
|
|
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,16 +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 {
|
|
18
|
+
import { useRef } from 'react';
|
|
19
19
|
import { randomUUID } from 'node:crypto';
|
|
20
|
-
import {
|
|
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,
|
|
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
|
-
|
|
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, }) {
|
|
28
31
|
// La riga di stato di OGNI spawn di sessione Claude: il comando esatto, come
|
|
29
32
|
// lo si scriverebbe in bash, invece di una parafrasi.
|
|
30
33
|
//
|
|
@@ -58,9 +61,6 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, setNo
|
|
|
58
61
|
// prima dell'exec), ed è anche il comando da ripetere a mano per vedere
|
|
59
62
|
// l'errore. Il salto fra le due dura i millisecondi che `deck-run` impiega a
|
|
60
63
|
// comporre.
|
|
61
|
-
// T152 — stessa fonte del detail (sheet.ts): un file di sei righe accanto al
|
|
62
|
-
// codice, letto una volta per vita del deck.
|
|
63
|
-
const catalog = useMemo(() => loadPromptCatalog(), []);
|
|
64
64
|
const spawnSeq = useRef(0);
|
|
65
65
|
function noteSpawn(spawned) {
|
|
66
66
|
noteCommand(spawned.cmd);
|
|
@@ -74,6 +74,35 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, setNo
|
|
|
74
74
|
noteCommand(inTab);
|
|
75
75
|
});
|
|
76
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
|
+
}
|
|
77
106
|
// T66 — la guardia dello spawn bound, con due chiamanti: gli acceleratori
|
|
78
107
|
// della lista e l'apertura del detail. Le tre uscite sono le stesse (pane
|
|
79
108
|
// sbagliato, riga meta, task sparita), e duplicarle vorrebbe dire tenerne
|
|
@@ -114,9 +143,9 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, setNo
|
|
|
114
143
|
// nomi diversi per la stessa conversazione. Vale anche sugli acceleratori
|
|
115
144
|
// della lista (^K/^P/^R), che passano di qui con `spawnNote` sul proprio
|
|
116
145
|
// default '' (P1 preflight) — nascono quindi titolati anche loro.
|
|
117
|
-
function spawnForTask(id,
|
|
146
|
+
function spawnForTask(id, modelKind, spawnNote = '', prompt = '', priority = false) {
|
|
118
147
|
const sid = randomUUID();
|
|
119
|
-
const note = spawnNote
|
|
148
|
+
const note = spawnNote;
|
|
120
149
|
appendTaskBinding(cwd, sid, id);
|
|
121
150
|
if (note)
|
|
122
151
|
appendNote(cwd, sid, note);
|
|
@@ -129,7 +158,7 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, setNo
|
|
|
129
158
|
// riempirebbe il sidecar di smarcature di conversazioni mai marcate.
|
|
130
159
|
if (priority)
|
|
131
160
|
appendPriority(cwd, sid, true);
|
|
132
|
-
const spawned = spawnDeck(id, cwd, sid,
|
|
161
|
+
const spawned = spawnDeck(id, cwd, sid, modelKind, note, prompt);
|
|
133
162
|
spawned.child.on('error', () => setNote(`⚠ spawn ${id} fallito (${DECK_RUN})`));
|
|
134
163
|
// Il modello resta SEMPRE visibile anche quando è il default, perché è un
|
|
135
164
|
// argomento esplicito del comando (T108): gli acceleratori della lista non
|
|
@@ -145,11 +174,16 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, setNo
|
|
|
145
174
|
// invece deve restare vivo.
|
|
146
175
|
function spawnTaskSession(kind, keyLabel) {
|
|
147
176
|
const task = selectedTaskOr(keyLabel, 'spawnare');
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
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 ?? '');
|
|
153
187
|
}
|
|
154
188
|
// T49 — resume di una conversazione in una nuova tab. Unico punto: lo chiamano
|
|
155
189
|
// il `⏎` della lista sessioni e quello sulla riga-sessione della ricerca, che
|
|
@@ -330,26 +364,50 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, setNo
|
|
|
330
364
|
* darebbe due tabelle capaci di divergere, e la seconda si scoprirebbe solo
|
|
331
365
|
* il giorno in cui una natura nuova apre la skill sbagliata.
|
|
332
366
|
*
|
|
333
|
-
* Sessione NUDA
|
|
334
|
-
*
|
|
335
|
-
*
|
|
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`).
|
|
372
|
+
*
|
|
373
|
+
* Il titolo interpola il basename del file, e serve un `sessionId` pinnato
|
|
374
|
+
* per scriverlo nel sidecar — senza, la nota non avrebbe una chiave e la riga
|
|
375
|
+
* in lista resterebbe nuda mentre la tab porta un nome. Le due scritture
|
|
376
|
+
* stanno qui accanto per la stessa ragione di `spawnForTask`: il nome che le
|
|
377
|
+
* due superfici mostrano nasce da un valore solo.
|
|
336
378
|
*/
|
|
337
379
|
function drainInbox(file, prompt) {
|
|
338
|
-
const
|
|
380
|
+
const sid = randomUUID();
|
|
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);
|
|
339
386
|
spawned.child.on('error', () => setNote(`⚠ drain ${file.basename} fallito (${DECK_RUN})`));
|
|
340
387
|
noteSpawn(spawned);
|
|
341
388
|
}
|
|
342
389
|
/**
|
|
343
390
|
* T134 — apre la sessione che SROTOLA l'hard-wrap di un path.
|
|
344
391
|
*
|
|
345
|
-
* `
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
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.
|
|
350
403
|
*/
|
|
351
|
-
function unwrapPath(path
|
|
352
|
-
const
|
|
404
|
+
function unwrapPath(path) {
|
|
405
|
+
const sid = randomUUID();
|
|
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);
|
|
353
411
|
spawned.child.on('error', () => setNote(`⚠ srotolamento ${path} fallito (${DECK_RUN})`));
|
|
354
412
|
noteSpawn(spawned);
|
|
355
413
|
}
|
|
@@ -392,6 +450,8 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, setNo
|
|
|
392
450
|
return {
|
|
393
451
|
...taskOps,
|
|
394
452
|
selectedTaskOr,
|
|
453
|
+
spawnDefaults,
|
|
454
|
+
spawnTaskDefaults,
|
|
395
455
|
spawnForTask,
|
|
396
456
|
spawnTaskSession,
|
|
397
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
|
-
|
|
54
|
-
|
|
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;
|
package/dist/deck-model.js
CHANGED
|
@@ -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,7 +105,7 @@ 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
110
|
// `notes` esce dall'indice come `sessionNotes`: nel deck `note` è già la riga
|
|
111
111
|
// di STATO in fondo al frame (il feedback di un'azione). Due concetti diversi
|
|
@@ -136,7 +136,7 @@ export function useDeckModel({ cwd, tasksPath, tasksDir, setNote, }) {
|
|
|
136
136
|
// ha una riga, è un'azione sulla surface del cappello) e quindi non si
|
|
137
137
|
// azzera mai — resta quello scelto finché non si sceglie altro, per tutta la
|
|
138
138
|
// vita del deck.
|
|
139
|
-
const [bareModel, setBareModel] = useState(
|
|
139
|
+
const [bareModel, setBareModel] = useState(bareModelDefault);
|
|
140
140
|
// T39 — vista corrente (filtri + sort). Vive nel modello e non nell'hook dei
|
|
141
141
|
// modali che la editano: è ciò che `applyView` consuma per produrre la lista,
|
|
142
142
|
// è persistita su disco e la rilegge il tasto `w`. È stato del modello che due
|
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
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
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
|
package/dist/hooks.js
CHANGED
|
@@ -15,6 +15,7 @@ import { scanEpicHierarchy, EMPTY_EPIC_HIERARCHY } from './epic-hierarchy.js';
|
|
|
15
15
|
import { scanInbox, INBOX_SCAN_INTERVAL_MS } from './inbox.js';
|
|
16
16
|
import { mixedCount, readWrapCache, runWrapScan, wrapCacheFile, wrapCount, } from './wrap-scan.js';
|
|
17
17
|
import { purgeTargets } from './purge.js';
|
|
18
|
+
import { loadSpawnOverrides, saveSpawnOverrides, } from './spawn-config.js';
|
|
18
19
|
import { POLL_MS } from './model.js';
|
|
19
20
|
// Dimensioni del terminale, live sul resize.
|
|
20
21
|
//
|
|
@@ -398,3 +399,38 @@ export function useTaskDetail(tasksDir, id) {
|
|
|
398
399
|
}, [tasksDir, id]);
|
|
399
400
|
return detail;
|
|
400
401
|
}
|
|
402
|
+
/**
|
|
403
|
+
* T161 — gli OVERRIDE di spawn del progetto, tenuti vivi per la durata del deck.
|
|
404
|
+
*
|
|
405
|
+
* Lettura una volta al mount, e da lì in poi lo stato in memoria è la fonte: la
|
|
406
|
+
* pagina delle azioni è l'unico scrittore, e rileggere il file a ogni tick del
|
|
407
|
+
* poll significherebbe pagare una `readFileSync` ogni 1,5 secondi per un dato
|
|
408
|
+
* che cambia quando lo si cambia. Chi lo modifica fuori dal deck (a mano, o un
|
|
409
|
+
* altro deck sullo stesso progetto) si riallinea alla riapertura, che è lo stesso
|
|
410
|
+
* regime di `launch` e `archivableDays`.
|
|
411
|
+
*
|
|
412
|
+
* Gli AVVISI del parse si mostrano una volta, all'avvio, nella riga di stato: una
|
|
413
|
+
* voce scartata in silenzio si presenta come un override che non fa niente, cioè
|
|
414
|
+
* come un guasto del deck. La riga di stato è l'unica superficie sempre presente
|
|
415
|
+
* — la pagina potrebbe non venire mai aperta.
|
|
416
|
+
*
|
|
417
|
+
* Il salvataggio NON si fida: ricarica dal disco quello che ha appena scritto,
|
|
418
|
+
* così quello che la pagina mostra dopo `w` è quello che il file dice davvero,
|
|
419
|
+
* riduzione del titolo e scarti compresi.
|
|
420
|
+
*/
|
|
421
|
+
export function useSpawnConfig({ cwd, setNote, }) {
|
|
422
|
+
const [parsed, setParsed] = useState(() => loadSpawnOverrides(cwd));
|
|
423
|
+
const warned = useRef(false);
|
|
424
|
+
useEffect(() => {
|
|
425
|
+
if (warned.current || parsed.warnings.length === 0)
|
|
426
|
+
return;
|
|
427
|
+
warned.current = true;
|
|
428
|
+
const [first, ...rest] = parsed.warnings;
|
|
429
|
+
setNote(`⚠ ${first}${rest.length > 0 ? ` · +${rest.length}` : ''}`);
|
|
430
|
+
}, [parsed, setNote]);
|
|
431
|
+
const save = useCallback((entries) => {
|
|
432
|
+
saveSpawnOverrides(cwd, entries);
|
|
433
|
+
setParsed(loadSpawnOverrides(cwd));
|
|
434
|
+
}, [cwd]);
|
|
435
|
+
return { overrides: parsed.entries, warnings: parsed.warnings, save };
|
|
436
|
+
}
|
package/dist/inbox.js
CHANGED
|
@@ -192,6 +192,25 @@ export function inboxPrompt(f) {
|
|
|
192
192
|
}
|
|
193
193
|
return `/loom-works:${DRAIN_SKILL[f.natura]} ${f.basename}`;
|
|
194
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* T161 — il buco `{file}` del titolo di una sessione di drain: il basename senza
|
|
197
|
+
* estensione e senza punti. L'emoji del prefisso non sta più qui — è dentro il
|
|
198
|
+
* template della riga `drain` del catalogo delle azioni, dove il progetto può
|
|
199
|
+
* riscriverla.
|
|
200
|
+
*
|
|
201
|
+
* Il `.md` si toglie QUI e non lo si lascia cadere a valle: l'alfabeto di
|
|
202
|
+
* `_sane_note` non ha il punto, quindi il titolo della tab uscirebbe con le due
|
|
203
|
+
* lettere saldate al nome (`…-prioritariamd`) mentre la lista del deck, che
|
|
204
|
+
* legge la nota grezza dal sidecar, mostrerebbe il nome intero — la stessa
|
|
205
|
+
* conversazione con due nomi, senza errore.
|
|
206
|
+
*
|
|
207
|
+
* Uno per ogni natura, `malformato` compreso: quella sessione ripara il marker
|
|
208
|
+
* invece di drenare, ma l'oggetto su cui lavora è lo stesso file, ed è il file
|
|
209
|
+
* che il titolo nomina. L'azione la dice il prompt.
|
|
210
|
+
*/
|
|
211
|
+
export function inboxWords(f) {
|
|
212
|
+
return f.basename.replace(/\.md$/i, '').replace(/\./g, '-');
|
|
213
|
+
}
|
|
195
214
|
/**
|
|
196
215
|
* Il testo del file inbox, per il detail fullscreen. `null` = illeggibile —
|
|
197
216
|
* il detail lo dice e tiene l'azione attiva, come fa quello della task con un
|
package/dist/input-modes.js
CHANGED
package/dist/input.js
CHANGED
|
@@ -79,6 +79,7 @@ export function useDeckInput({ cwd, tasksDir, mode, setMode, setNote, model, act
|
|
|
79
79
|
status: overlays.status.onKey,
|
|
80
80
|
inbox: overlays.inbox.onKey,
|
|
81
81
|
wrap: overlays.wrap.onKey,
|
|
82
|
+
spawn: overlays.spawn.onKey,
|
|
82
83
|
reader: overlays.search.onReaderKey,
|
|
83
84
|
search: overlays.search.onSearchKey,
|
|
84
85
|
assign: overlays.assign.onKey,
|
|
@@ -335,6 +336,21 @@ export function useDeckInput({ cwd, tasksDir, mode, setMode, setNote, model, act
|
|
|
335
336
|
// stesso doppio significato per contesto già in esercizio su `^F`.
|
|
336
337
|
overlays.gitlink.bump();
|
|
337
338
|
}
|
|
339
|
+
else if (input === 's') {
|
|
340
|
+
// T161/P13 preflight — `^S` apre la pagina delle azioni di spawn. Le
|
|
341
|
+
// schermate sostitutive si aprono tutte con `CTRL` più lettera, e `s`
|
|
342
|
+
// sta per «spawn»; `^A`, che sarebbe l'altra mnemonica, è già
|
|
343
|
+
// inizio-riga dentro ogni campo di testo del deck.
|
|
344
|
+
//
|
|
345
|
+
// Sul flow-control XON/XOFF il repo porta due commenti in
|
|
346
|
+
// contraddizione — `model.ts` dice che il raw mode di Ink lo disattiva e
|
|
347
|
+
// il tasto passa pulito, `cli.tsx` che il terminale lo intercetta. La
|
|
348
|
+
// contraddizione si chiude qui con una MISURA, non con un'opinione: il
|
|
349
|
+
// gate pty di `test/modes-smoke.test.ts` manda `^S` al deck vero dentro
|
|
350
|
+
// un pseudo-terminale e pretende che la pagina si apra, quindi se il
|
|
351
|
+
// byte non arrivasse il test fallirebbe invece di lasciare il dubbio.
|
|
352
|
+
overlays.spawn.openPage();
|
|
353
|
+
}
|
|
338
354
|
else if (input === 'b') {
|
|
339
355
|
// T134/D8 preflight — `^B` (box/bacheca) scambia i due pane dello slot
|
|
340
356
|
// destro. Non è un modale né una schermata: il pane resta uno dei due
|