@lamemind/loom-deck 0.67.0 → 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 +79 -33
- 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 +6 -12
- 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 +46 -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 +14 -33
- package/package.json +1 -1
- package/scripts/deck-run +26 -10
- 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,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 {
|
|
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
|
-
import {
|
|
28
|
-
import {
|
|
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,
|
|
146
|
+
function spawnForTask(id, modelKind, spawnNote = '', prompt = '', priority = false) {
|
|
120
147
|
const sid = randomUUID();
|
|
121
|
-
const note = spawnNote
|
|
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,
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
336
|
-
*
|
|
337
|
-
*
|
|
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
|
|
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
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
* `
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
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
|
|
404
|
+
function unwrapPath(path) {
|
|
363
405
|
const sid = randomUUID();
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
|
|
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,16 +192,11 @@ export function inboxPrompt(f) {
|
|
|
192
192
|
}
|
|
193
193
|
return `/loom-works:${DRAIN_SKILL[f.natura]} ${f.basename}`;
|
|
194
194
|
}
|
|
195
|
-
/** Prefisso del titolo di una sessione aperta su un file inbox. Astrale come le
|
|
196
|
-
* emoji delle azioni su task (`ACTION_EMOJI` in `src/spawn.ts`) e per la stessa
|
|
197
|
-
* ragione: `sanitize` di `src/width.ts` sostituisce con `·` ogni glifo su cui
|
|
198
|
-
* le due contabilità di larghezza divergono, e le emoji del BMP divergono
|
|
199
|
-
* sempre. Sta anche nella whitelist di `_sane_note` in `deck-run`, o arriverebbe
|
|
200
|
-
* nella tab ridotta a niente mentre la lista del deck la mostra. */
|
|
201
|
-
const INBOX_EMOJI = '🧹';
|
|
202
195
|
/**
|
|
203
|
-
*
|
|
204
|
-
* senza
|
|
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.
|
|
205
200
|
*
|
|
206
201
|
* Il `.md` si toglie QUI e non lo si lascia cadere a valle: l'alfabeto di
|
|
207
202
|
* `_sane_note` non ha il punto, quindi il titolo della tab uscirebbe con le due
|
|
@@ -213,9 +208,8 @@ const INBOX_EMOJI = '🧹';
|
|
|
213
208
|
* invece di drenare, ma l'oggetto su cui lavora è lo stesso file, ed è il file
|
|
214
209
|
* che il titolo nomina. L'azione la dice il prompt.
|
|
215
210
|
*/
|
|
216
|
-
export function
|
|
217
|
-
|
|
218
|
-
return `${INBOX_EMOJI} ${name}`;
|
|
211
|
+
export function inboxWords(f) {
|
|
212
|
+
return f.basename.replace(/\.md$/i, '').replace(/\./g, '-');
|
|
219
213
|
}
|
|
220
214
|
/**
|
|
221
215
|
* Il testo del file inbox, per il detail fullscreen. `null` = illeggibile —
|
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
|