@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 +26 -10
- package/dist/actions.js +79 -33
- package/dist/cli.js +51 -6
- package/dist/deck-model.js +21 -18
- package/dist/frame.js +4 -0
- package/dist/glyphs.js +5 -5
- package/dist/hooks.js +86 -3
- 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/session-meta.js +108 -0
- package/dist/sessions.js +7 -1
- package/dist/spawn-catalog.js +338 -0
- package/dist/spawn-config.js +280 -0
- package/dist/spawn.js +46 -157
- package/dist/task-index.js +51 -2
- 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 +58 -13
- package/scripts/prompt-catalog +17 -3
package/dist/hooks.js
CHANGED
|
@@ -8,13 +8,15 @@ import { statSync } from 'node:fs';
|
|
|
8
8
|
import { loadTasks, loadTaskDetail } from './tasks.js';
|
|
9
9
|
import { discoverProjectSessions } from './sessions.js';
|
|
10
10
|
import { discoverLiveSessions, liveSig } from './live-sessions.js';
|
|
11
|
-
import { loadSessionIndex } from './task-index.js';
|
|
11
|
+
import { appendSessionMeta, loadSessionIndex } from './task-index.js';
|
|
12
|
+
import { sidecarGaps } from './session-meta.js';
|
|
12
13
|
import { archivableIds, SCAN_INTERVAL_MS } from './archivable.js';
|
|
13
14
|
import { commitTimes } from './commit-times.js';
|
|
14
15
|
import { scanEpicHierarchy, EMPTY_EPIC_HIERARCHY } from './epic-hierarchy.js';
|
|
15
16
|
import { scanInbox, INBOX_SCAN_INTERVAL_MS } from './inbox.js';
|
|
16
17
|
import { mixedCount, readWrapCache, runWrapScan, wrapCacheFile, wrapCount, } from './wrap-scan.js';
|
|
17
18
|
import { purgeTargets } from './purge.js';
|
|
19
|
+
import { loadSpawnOverrides, saveSpawnOverrides, } from './spawn-config.js';
|
|
18
20
|
import { POLL_MS } from './model.js';
|
|
19
21
|
// Dimensioni del terminale, live sul resize.
|
|
20
22
|
//
|
|
@@ -136,7 +138,15 @@ export function useEpicHierarchy(tasksDir) {
|
|
|
136
138
|
// ha cache mtime-keyed interna → il poll è economico; qui si evita comunque il
|
|
137
139
|
// re-render inutile con una signature (sessionId:ts + binding entries): setState
|
|
138
140
|
// solo quando cambia davvero qualcosa.
|
|
139
|
-
|
|
141
|
+
//
|
|
142
|
+
// T162 — è anche il posto dove il deck RIEMPIE I BUCHI dei campi derivati del
|
|
143
|
+
// sidecar (`title`, `model`): qui le due fonti sono già in mano nello stesso
|
|
144
|
+
// giro, la corrente (le `Session`) e quella su disco (l'indice). Il `core` del
|
|
145
|
+
// progetto arriva per argomento e non si rilegge qui: la stessa derivazione
|
|
146
|
+
// vive già nel modello (`projectCore`), e una seconda copia toglierebbe dal
|
|
147
|
+
// titolo scritto nel sidecar una stringa diversa da quella che la lista toglie
|
|
148
|
+
// dalle proprie righe.
|
|
149
|
+
export function useSessions(projectRoot, core) {
|
|
140
150
|
const [state, setState] = useState({
|
|
141
151
|
sessions: [],
|
|
142
152
|
bindings: new Map(),
|
|
@@ -167,6 +177,8 @@ export function useSessions(projectRoot) {
|
|
|
167
177
|
pinned: new Map(),
|
|
168
178
|
notes: new Map(),
|
|
169
179
|
priority: new Set(),
|
|
180
|
+
titles: new Map(),
|
|
181
|
+
models: new Map(),
|
|
170
182
|
};
|
|
171
183
|
}
|
|
172
184
|
// T62 — le vive stanno sullo STESSO tick delle altre fonti, non su una
|
|
@@ -182,6 +194,42 @@ export function useSessions(projectRoot) {
|
|
|
182
194
|
live = new Map();
|
|
183
195
|
}
|
|
184
196
|
const { bindings, forkOf, pinned, notes, priority } = index;
|
|
197
|
+
// T162 — i campi derivati delle PINNATE, allineati prima della signature.
|
|
198
|
+
//
|
|
199
|
+
// Gira a ogni giro e non solo al cambio di stato, perché il buco può
|
|
200
|
+
// nascere senza che nulla nel deck si muova: a pinnare può essere compass,
|
|
201
|
+
// dalla modale sulla conversazione in focus, e compass il titolo non lo
|
|
202
|
+
// conosce. Il deck è l'unico che ce l'ha, quindi è l'unico che può
|
|
203
|
+
// chiudere quel buco — e lo fa passando, non al momento del gesto.
|
|
204
|
+
//
|
|
205
|
+
// Costa solo quando c'è da scrivere: senza pinnate `sidecarGaps` esce
|
|
206
|
+
// subito, e con un sidecar già allineato ritorna una lista vuota (è
|
|
207
|
+
// l'invariante di convergenza del modulo — vedi `session-meta.ts`).
|
|
208
|
+
//
|
|
209
|
+
// Fuori dal gate della signature di proposito: un sidecar temporaneamente
|
|
210
|
+
// non scrivibile deve poter essere ritentato al giro dopo, e con lo stato
|
|
211
|
+
// invariato la signature non cambierebbe mai più.
|
|
212
|
+
//
|
|
213
|
+
// Gli append NON entrano nella signature e non producono un re-render: il
|
|
214
|
+
// deck il titolo lo ha dalla `Session`, che è la fonte. Questi campi
|
|
215
|
+
// esistono per un lettore che non può aprire un transcript, cioè compass.
|
|
216
|
+
try {
|
|
217
|
+
for (const gap of sidecarGaps({
|
|
218
|
+
pinned,
|
|
219
|
+
sessions,
|
|
220
|
+
bindings,
|
|
221
|
+
titles: index.titles,
|
|
222
|
+
models: index.models,
|
|
223
|
+
core,
|
|
224
|
+
})) {
|
|
225
|
+
appendSessionMeta(projectRoot, gap.sessionId, gap);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// sidecar non scrivibile: il poll continua, il buco si riprova al giro
|
|
230
|
+
// dopo. Niente a schermo — è manutenzione di un campo di servizio, non
|
|
231
|
+
// un'azione che l'utente ha chiesto.
|
|
232
|
+
}
|
|
185
233
|
// La signature copre anche fork, pin, note e marca di priorità: un record
|
|
186
234
|
// di lineage, un toggle di pin, una nota appena scritta o un 🚨 acceso
|
|
187
235
|
// cambiano la lista renderizzata, quindi devono forzare il re-render come
|
|
@@ -208,7 +256,7 @@ export function useSessions(projectRoot) {
|
|
|
208
256
|
reload();
|
|
209
257
|
const id = setInterval(reload, POLL_MS);
|
|
210
258
|
return () => clearInterval(id);
|
|
211
|
-
}, [projectRoot]);
|
|
259
|
+
}, [projectRoot, core]);
|
|
212
260
|
return { ...state, reload: () => reloadRef.current() };
|
|
213
261
|
}
|
|
214
262
|
// T61 — conteggio delle Done oltre soglia, su una scala di refresh TUTTA SUA.
|
|
@@ -398,3 +446,38 @@ export function useTaskDetail(tasksDir, id) {
|
|
|
398
446
|
}, [tasksDir, id]);
|
|
399
447
|
return detail;
|
|
400
448
|
}
|
|
449
|
+
/**
|
|
450
|
+
* T161 — gli OVERRIDE di spawn del progetto, tenuti vivi per la durata del deck.
|
|
451
|
+
*
|
|
452
|
+
* Lettura una volta al mount, e da lì in poi lo stato in memoria è la fonte: la
|
|
453
|
+
* pagina delle azioni è l'unico scrittore, e rileggere il file a ogni tick del
|
|
454
|
+
* poll significherebbe pagare una `readFileSync` ogni 1,5 secondi per un dato
|
|
455
|
+
* che cambia quando lo si cambia. Chi lo modifica fuori dal deck (a mano, o un
|
|
456
|
+
* altro deck sullo stesso progetto) si riallinea alla riapertura, che è lo stesso
|
|
457
|
+
* regime di `launch` e `archivableDays`.
|
|
458
|
+
*
|
|
459
|
+
* Gli AVVISI del parse si mostrano una volta, all'avvio, nella riga di stato: una
|
|
460
|
+
* voce scartata in silenzio si presenta come un override che non fa niente, cioè
|
|
461
|
+
* come un guasto del deck. La riga di stato è l'unica superficie sempre presente
|
|
462
|
+
* — la pagina potrebbe non venire mai aperta.
|
|
463
|
+
*
|
|
464
|
+
* Il salvataggio NON si fida: ricarica dal disco quello che ha appena scritto,
|
|
465
|
+
* così quello che la pagina mostra dopo `w` è quello che il file dice davvero,
|
|
466
|
+
* riduzione del titolo e scarti compresi.
|
|
467
|
+
*/
|
|
468
|
+
export function useSpawnConfig({ cwd, setNote, }) {
|
|
469
|
+
const [parsed, setParsed] = useState(() => loadSpawnOverrides(cwd));
|
|
470
|
+
const warned = useRef(false);
|
|
471
|
+
useEffect(() => {
|
|
472
|
+
if (warned.current || parsed.warnings.length === 0)
|
|
473
|
+
return;
|
|
474
|
+
warned.current = true;
|
|
475
|
+
const [first, ...rest] = parsed.warnings;
|
|
476
|
+
setNote(`⚠ ${first}${rest.length > 0 ? ` · +${rest.length}` : ''}`);
|
|
477
|
+
}, [parsed, setNote]);
|
|
478
|
+
const save = useCallback((entries) => {
|
|
479
|
+
saveSpawnOverrides(cwd, entries);
|
|
480
|
+
setParsed(loadSpawnOverrides(cwd));
|
|
481
|
+
}, [cwd]);
|
|
482
|
+
return { overrides: parsed.entries, warnings: parsed.warnings, save };
|
|
483
|
+
}
|
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
|
package/dist/overlays/sheet.js
CHANGED
|
@@ -18,10 +18,9 @@ import { scanText, topForOffset } from '../text-search.js';
|
|
|
18
18
|
import { parseMarkdown } from '../markdown.js';
|
|
19
19
|
import { cpLen, insertAt, removeAt } from '../layout.js';
|
|
20
20
|
import { sanitizeTyped } from '../glyphs.js';
|
|
21
|
-
import { ACTION_HOTKEYS, DETAIL_ACTIONS, MODELS, MODEL_DEFAULT, specializeRecap, } from '../spawn.js';
|
|
21
|
+
import { ACTION_HOTKEYS, DETAIL_ACTIONS, MODELS, MODEL_DEFAULT, specializeRecap, } from '../spawn-catalog.js';
|
|
22
22
|
import { taskIsEpic } from '../tasks.js';
|
|
23
23
|
import { fieldsKey } from '../fields.js';
|
|
24
|
-
import { loadPromptCatalog, modelFor, promptFor } from '../prompt-catalog.js';
|
|
25
24
|
// T117 — le righe dell'area di compilazione del detail, nell'ordine in cui si
|
|
26
25
|
// leggono dall'alto. Gli indici sono nominati perché li usano insieme l'handler
|
|
27
26
|
// dei tasti e la resa, e un `2` nudo in due file diversi è la coordinata che
|
|
@@ -41,7 +40,7 @@ export const DETAIL_FIELDS = [
|
|
|
41
40
|
{ kind: 'choice', count: PRIORITY_CHOICES.length },
|
|
42
41
|
];
|
|
43
42
|
export function useSheetOverlay(deps) {
|
|
44
|
-
const { rows, columns, setMode, setNote, onAction } = deps;
|
|
43
|
+
const { rows, columns, setMode, setNote, defaults, onAction } = deps;
|
|
45
44
|
const [sheet, setSheet] = useState(null);
|
|
46
45
|
const [top, setTop] = useState(0);
|
|
47
46
|
const [action, setAction] = useState(0);
|
|
@@ -68,9 +67,6 @@ export function useSheetOverlay(deps) {
|
|
|
68
67
|
* riparserebbe l'intero task file per un campo dell'header. */
|
|
69
68
|
const [epic, setEpic] = useState(false);
|
|
70
69
|
const [cursor, setCursor] = useState({ row: DROW.action, caret: 0 });
|
|
71
|
-
// Il catalogo si legge una volta per vita del deck: è un file di quattro righe
|
|
72
|
-
// accanto al codice, non un dato che cambia sotto i piedi.
|
|
73
|
-
const catalog = useMemo(() => loadPromptCatalog(), []);
|
|
74
70
|
// T91 — ricerca dentro il detail. `open` distingue i due modi in cui la si
|
|
75
71
|
// lascia: `esc` butta via ciò che il modale ha prodotto (`find` a null,
|
|
76
72
|
// evidenziazione via), `⏎` lo congela e restituisce il controllo allo strato
|
|
@@ -130,13 +126,13 @@ export function useSheetOverlay(deps) {
|
|
|
130
126
|
setTop(0);
|
|
131
127
|
setAction(0);
|
|
132
128
|
setEpic(isEpic);
|
|
133
|
-
// T152 —
|
|
134
|
-
//
|
|
135
|
-
// può portare
|
|
136
|
-
|
|
137
|
-
setModel(
|
|
138
|
-
setSpawnNote('');
|
|
139
|
-
setPrompt(
|
|
129
|
+
// T152/T161 — i tre campi partono dalla terna dell'azione, risolta sul
|
|
130
|
+
// catalogo e sugli override di progetto: su una task Epic la prima azione
|
|
131
|
+
// specializza a `recap-epic`, che può portare valori diversi da `recap`.
|
|
132
|
+
const d = defaults(kind, next.id);
|
|
133
|
+
setModel(d.model);
|
|
134
|
+
setSpawnNote(d.title ?? '');
|
|
135
|
+
setPrompt(d.prompt ?? '');
|
|
140
136
|
setPriority(false);
|
|
141
137
|
setCursor({ row: DROW.action, caret: 0 });
|
|
142
138
|
setFind(null);
|
|
@@ -144,21 +140,27 @@ export function useSheetOverlay(deps) {
|
|
|
144
140
|
setNote('');
|
|
145
141
|
setMode('detail');
|
|
146
142
|
}
|
|
147
|
-
/** Cambia l'azione e RISCRIVE
|
|
143
|
+
/** Cambia l'azione e RISCRIVE i tre campi col default del nuovo kind (D2).
|
|
148
144
|
*
|
|
149
|
-
* Nessuna preservazione
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
145
|
+
* Nessuna preservazione delle scelte fatte a mano, per la stessa ragione già
|
|
146
|
+
* scritta sul prompt: col fuoco per riga un cambio di azione ACCIDENTALE non
|
|
147
|
+
* esiste — `←→` cambiano azione solo dalla riga azione — quindi la
|
|
148
|
+
* preservazione difenderebbe da nulla e costerebbe campi che non tornano più
|
|
149
|
+
* al default della nuova azione (T152).
|
|
150
|
+
*
|
|
151
|
+
* T161 — da qui ci passa anche il TITOLO, che prima restava vuoto e veniva
|
|
152
|
+
* riempito allo spawn da `fallbackTitle`: ora il campo mostra quello che
|
|
153
|
+
* partirà, come già il prompt e il modello. */
|
|
154
154
|
function selectAction(index) {
|
|
155
155
|
setAction(index);
|
|
156
156
|
const id = sheet?.id;
|
|
157
157
|
if (!id)
|
|
158
158
|
return;
|
|
159
159
|
const kind = specializeRecap(DETAIL_ACTIONS[index].kind, epic);
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
const d = defaults(kind, id);
|
|
161
|
+
setPrompt(d.prompt ?? '');
|
|
162
|
+
setModel(d.model);
|
|
163
|
+
setSpawnNote(d.title ?? '');
|
|
162
164
|
}
|
|
163
165
|
// Il ponte fra le quattro righe e i quattro stati. Le righe restano
|
|
164
166
|
// TIPIZZATE dove vivono (`ModelKind`, indice dell'azione) invece di finire in
|
|
@@ -279,13 +281,12 @@ export function useSheetOverlay(deps) {
|
|
|
279
281
|
// Il kind viaggia specializzato quanto il prompt che lo accompagna: se il
|
|
280
282
|
// campo è stato svuotato a mano il testo non parte e resta lui a dire cosa
|
|
281
283
|
// ricevera' la sessione, quindi i due non possono divergere.
|
|
282
|
-
const act = DETAIL_ACTIONS[action];
|
|
283
284
|
const id = sheet?.id;
|
|
284
285
|
const note = spawnNote.trim();
|
|
285
286
|
const text = prompt.trim();
|
|
286
287
|
close();
|
|
287
288
|
if (id)
|
|
288
|
-
onAction(id,
|
|
289
|
+
onAction(id, model, note, text, priority);
|
|
289
290
|
return;
|
|
290
291
|
}
|
|
291
292
|
if (key.pageUp) {
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// T161 — la PAGINA DELLE AZIONI DI SPAWN: ottava schermata sostitutiva.
|
|
2
|
+
//
|
|
3
|
+
// Una tabella di undici righe — una per azione che il deck sa aprire — con le
|
|
4
|
+
// quattro colonne funzione, titolo, modello, prompt. Le ultime tre si modificano
|
|
5
|
+
// e il salvataggio scrive il blocco `spawn` di `.claude/loom-works.json`.
|
|
6
|
+
//
|
|
7
|
+
// Due LIVELLI, non un edit dentro la cella (P14 preflight). Al primo si scorre
|
|
8
|
+
// la tabella; `⏎` apre sotto di essa l'area di compilazione della riga
|
|
9
|
+
// selezionata, che è la stessa forma già in esercizio nel detail della task —
|
|
10
|
+
// righe con un fuoco solo, `↑↓` fra le righe, `←→` sul valore o sul caret
|
|
11
|
+
// secondo il tipo (`fields.ts`). Un editor dentro la cella avrebbe voluto un
|
|
12
|
+
// caret che scorre dentro una colonna larga venti colonne, cioè una terza
|
|
13
|
+
// grammatica di input su una schermata che ne ha già due.
|
|
14
|
+
//
|
|
15
|
+
// La bozza vive IN MEMORIA fino a `w`. Comporre una configurazione non tocca il
|
|
16
|
+
// disco — stesso regime del salvataggio esplicito della vista (`w` sul deck) —
|
|
17
|
+
// e il file di progetto è committato: scriverci a ogni freccia produrrebbe un
|
|
18
|
+
// working tree sporco per una prova.
|
|
19
|
+
import { useState } from 'react';
|
|
20
|
+
import { pageStep, spawnListCapacity } from '../viewport.js';
|
|
21
|
+
import { fieldsKey } from '../fields.js';
|
|
22
|
+
import { cpLen } from '../layout.js';
|
|
23
|
+
import { saneTemplate } from '../sane-note.js';
|
|
24
|
+
import { configFilePath } from '../config.js';
|
|
25
|
+
import { cut } from '../width.js';
|
|
26
|
+
import { EXAMPLE_HOLES, MODELS, SPAWN_ACTIONS, editableFields, } from '../spawn-catalog.js';
|
|
27
|
+
import { rawSpawn, } from '../spawn-config.js';
|
|
28
|
+
/** Le tre righe dell'area di compilazione, nell'ordine in cui si leggono. Sono
|
|
29
|
+
* posizioni di SCHERMO e non indici del cursore: le righe non editabili si
|
|
30
|
+
* disegnano comunque (dicono perché non lo sono) ma il fuoco non ci passa. */
|
|
31
|
+
export const SROW = { title: 0, model: 1, prompt: 2 };
|
|
32
|
+
export function useSpawnPage(deps) {
|
|
33
|
+
const { cwd, rows, hasNote, overrides, save, catalog, setMode, setNote } = deps;
|
|
34
|
+
/** La bozza, `null` a pagina chiusa. Copia degli override all'apertura: una
|
|
35
|
+
* bozza che sopravvive alla chiusura sarebbe uno stato invisibile che `w`
|
|
36
|
+
* scriverebbe la volta dopo. */
|
|
37
|
+
const [draft, setDraft] = useState(null);
|
|
38
|
+
const [sel, setSel] = useState(0);
|
|
39
|
+
const [top, setTop] = useState(0);
|
|
40
|
+
const [dirty, setDirty] = useState(false);
|
|
41
|
+
/** L'area di compilazione è aperta sulla riga selezionata. */
|
|
42
|
+
const [editing, setEditing] = useState(false);
|
|
43
|
+
const [fieldTitle, setFieldTitle] = useState('');
|
|
44
|
+
const [fieldModel, setFieldModel] = useState(MODELS[0]);
|
|
45
|
+
const [fieldPrompt, setFieldPrompt] = useState('');
|
|
46
|
+
const [cursor, setCursor] = useState({ row: 0, caret: 0 });
|
|
47
|
+
const capacity = spawnListCapacity(rows, editing, hasNote);
|
|
48
|
+
const maxTop = Math.max(0, SPAWN_ACTIONS.length - capacity);
|
|
49
|
+
const open = draft !== null;
|
|
50
|
+
const action = SPAWN_ACTIONS[sel];
|
|
51
|
+
const editable = editableFields(action);
|
|
52
|
+
/** Le specifiche che `fields.ts` consuma: SOLO le celle editabili di questa
|
|
53
|
+
* riga, nell'ordine di schermo. Il cursore indicizza qui, non le tre righe
|
|
54
|
+
* disegnate — su `bare`, che ha il solo modello, `↑↓` non hanno dove andare e
|
|
55
|
+
* non devono finire su una riga che non si modifica. */
|
|
56
|
+
const specs = editable.map((f) => f === 'model' ? { kind: 'choice', count: MODELS.length } : { kind: 'text' });
|
|
57
|
+
const table = SPAWN_ACTIONS.map((a) => ({
|
|
58
|
+
action: a,
|
|
59
|
+
raw: rawSpawn(a, draft ?? overrides, catalog),
|
|
60
|
+
touched: (draft ?? overrides).has(a.id),
|
|
61
|
+
}));
|
|
62
|
+
function openPage() {
|
|
63
|
+
setDraft(new Map(overrides));
|
|
64
|
+
setSel(0);
|
|
65
|
+
setTop(0);
|
|
66
|
+
setDirty(false);
|
|
67
|
+
setEditing(false);
|
|
68
|
+
setNote('');
|
|
69
|
+
setMode('spawn');
|
|
70
|
+
}
|
|
71
|
+
function close() {
|
|
72
|
+
setMode('normal');
|
|
73
|
+
setDraft(null);
|
|
74
|
+
setEditing(false);
|
|
75
|
+
}
|
|
76
|
+
function scroll(delta) {
|
|
77
|
+
const next = Math.max(0, Math.min(SPAWN_ACTIONS.length - 1, sel + delta));
|
|
78
|
+
setSel(next);
|
|
79
|
+
// La finestra INSEGUE la selezione invece di centrarla: la tabella è di
|
|
80
|
+
// undici righe e su un terminale normale ci sta intera, quindi centrare
|
|
81
|
+
// farebbe saltare la lista sotto il caret per niente.
|
|
82
|
+
setTop(Math.max(0, Math.min(next, Math.max(Math.min(top, maxTop), next - capacity + 1))));
|
|
83
|
+
}
|
|
84
|
+
/** Apre l'area sulla riga selezionata, coi valori GREZZI correnti. */
|
|
85
|
+
function openFields() {
|
|
86
|
+
const raw = rawSpawn(action, draft ?? overrides, catalog);
|
|
87
|
+
const title = raw.title ?? '';
|
|
88
|
+
const prompt = raw.prompt ?? '';
|
|
89
|
+
setFieldTitle(title);
|
|
90
|
+
setFieldModel(raw.model);
|
|
91
|
+
setFieldPrompt(prompt);
|
|
92
|
+
// Caret in coda al primo campo editabile, come `caretAtEnd`: è la posizione
|
|
93
|
+
// da cui si continua a scrivere, e l'unica che non dipende da dove stava il
|
|
94
|
+
// cursore l'ultima volta che l'area era aperta.
|
|
95
|
+
const first = editable[0];
|
|
96
|
+
setCursor({ row: 0, caret: first === 'model' ? 0 : cpLen(first === 'title' ? title : prompt) });
|
|
97
|
+
setEditing(true);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Congela l'area nella bozza, tenendo solo ciò che DIFFERISCE dal default.
|
|
101
|
+
*
|
|
102
|
+
* È ciò che rende reversibile una modifica senza un gesto apposta: un valore
|
|
103
|
+
* riportato a mano a quello di partenza smette di essere un override, invece
|
|
104
|
+
* di restare nel file come una riga che non cambia niente (AC7). Il gesto
|
|
105
|
+
* apposta — `CANC` — resta per togliere tutte e tre le celle in un colpo.
|
|
106
|
+
*/
|
|
107
|
+
function commitFields() {
|
|
108
|
+
const base = rawSpawn(action, new Map(), catalog);
|
|
109
|
+
const next = {};
|
|
110
|
+
for (const f of editable) {
|
|
111
|
+
if (f === 'model') {
|
|
112
|
+
if (fieldModel !== base.model)
|
|
113
|
+
next.model = fieldModel;
|
|
114
|
+
}
|
|
115
|
+
else if (f === 'title') {
|
|
116
|
+
// P10 — la riduzione al confine: quello che entra nella bozza è quello
|
|
117
|
+
// che comparirà nella tab, coi buchi preservati. Farla solo al
|
|
118
|
+
// salvataggio mostrerebbe per tutta la sessione un titolo che la tab non
|
|
119
|
+
// porterà mai.
|
|
120
|
+
const title = saneTemplate(fieldTitle);
|
|
121
|
+
if (title !== (base.title ?? ''))
|
|
122
|
+
next.title = title;
|
|
123
|
+
}
|
|
124
|
+
else if (fieldPrompt !== (base.prompt ?? '')) {
|
|
125
|
+
next.prompt = fieldPrompt;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
setDraft((d) => {
|
|
129
|
+
const m = new Map(d ?? overrides);
|
|
130
|
+
if (Object.keys(next).length > 0)
|
|
131
|
+
m.set(action.id, next);
|
|
132
|
+
else
|
|
133
|
+
m.delete(action.id);
|
|
134
|
+
return m;
|
|
135
|
+
});
|
|
136
|
+
setDirty(true);
|
|
137
|
+
setEditing(false);
|
|
138
|
+
}
|
|
139
|
+
/** `CANC` sulla riga: via l'override, la riga torna al default. */
|
|
140
|
+
function clearRow() {
|
|
141
|
+
if (!(draft ?? overrides).has(action.id)) {
|
|
142
|
+
setNote(`CANC → ${action.id}: nessun override da togliere`);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
setDraft((d) => {
|
|
146
|
+
const m = new Map(d ?? overrides);
|
|
147
|
+
m.delete(action.id);
|
|
148
|
+
return m;
|
|
149
|
+
});
|
|
150
|
+
setDirty(true);
|
|
151
|
+
setNote(`CANC → ${action.id}: override rimosso, torna al default`);
|
|
152
|
+
}
|
|
153
|
+
function write() {
|
|
154
|
+
const entries = draft ?? overrides;
|
|
155
|
+
try {
|
|
156
|
+
save(entries);
|
|
157
|
+
}
|
|
158
|
+
catch (e) {
|
|
159
|
+
// Il file non è del deck: se non si parsa il saver rifiuta invece di
|
|
160
|
+
// rigenerarlo, e qui si dice perché. La bozza resta viva — chi ripara il
|
|
161
|
+
// file a mano può ripremere `w` senza rifare le modifiche.
|
|
162
|
+
setNote(`⚠ ${cut(e.message, 70)}`);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
setDirty(false);
|
|
166
|
+
const n = entries.size;
|
|
167
|
+
setNote(`w → ${n === 0 ? 'nessun override' : `${n} azione${n > 1 ? 'i' : ''} configurate`} · ${configFilePath(cwd)}`);
|
|
168
|
+
}
|
|
169
|
+
const fieldsIO = {
|
|
170
|
+
text: (row) => (editable[row] === 'title' ? fieldTitle : fieldPrompt),
|
|
171
|
+
setText: (row, next) => editable[row] === 'title' ? setFieldTitle(next) : setFieldPrompt(next),
|
|
172
|
+
choice: () => Math.max(0, MODELS.indexOf(fieldModel)),
|
|
173
|
+
setChoice: (_row, index) => setFieldModel(MODELS[index]),
|
|
174
|
+
};
|
|
175
|
+
function onFieldsKey(input, key) {
|
|
176
|
+
if (key.escape) {
|
|
177
|
+
setEditing(false);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (key.return) {
|
|
181
|
+
commitFields();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
fieldsKey(input, key, specs, cursor, setCursor, fieldsIO);
|
|
185
|
+
}
|
|
186
|
+
function onKey(input, key) {
|
|
187
|
+
if (!open)
|
|
188
|
+
return;
|
|
189
|
+
if (editing) {
|
|
190
|
+
onFieldsKey(input, key);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (key.escape) {
|
|
194
|
+
// Una bozza non salvata si perde: dirlo è l'unico modo di non perderla per
|
|
195
|
+
// sbaglio, e un modale di conferma su una pagina di configurazione
|
|
196
|
+
// costerebbe una domanda a ogni uscita.
|
|
197
|
+
if (dirty)
|
|
198
|
+
setNote('modifiche non salvate perse · w salva prima di uscire');
|
|
199
|
+
close();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (key.return) {
|
|
203
|
+
openFields();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (key.delete) {
|
|
207
|
+
clearRow();
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (key.upArrow)
|
|
211
|
+
scroll(-1);
|
|
212
|
+
else if (key.downArrow)
|
|
213
|
+
scroll(1);
|
|
214
|
+
else if (key.pageUp)
|
|
215
|
+
scroll(-pageStep(capacity));
|
|
216
|
+
else if (key.pageDown)
|
|
217
|
+
scroll(pageStep(capacity));
|
|
218
|
+
else if (input === 'w' && !key.ctrl)
|
|
219
|
+
write();
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
open,
|
|
223
|
+
table,
|
|
224
|
+
sel,
|
|
225
|
+
top,
|
|
226
|
+
capacity,
|
|
227
|
+
maxTop,
|
|
228
|
+
dirty,
|
|
229
|
+
editing,
|
|
230
|
+
editable,
|
|
231
|
+
fieldTitle,
|
|
232
|
+
fieldModel,
|
|
233
|
+
fieldPrompt,
|
|
234
|
+
cursor,
|
|
235
|
+
/** L'anteprima resa della riga aperta: i buchi riempiti coi valori finti. */
|
|
236
|
+
example: EXAMPLE_HOLES,
|
|
237
|
+
openPage,
|
|
238
|
+
onKey,
|
|
239
|
+
};
|
|
240
|
+
}
|
package/dist/overlays/status.js
CHANGED
|
@@ -17,7 +17,7 @@ import { parseMarkdown } from '../markdown.js';
|
|
|
17
17
|
import { CLAUDE_CMD, notifyDone, spawnProjectStatus } from '../spawn.js';
|
|
18
18
|
import { readStatusCache, statusFilePath, statusLabel, writeStatusCache, } from '../project-status.js';
|
|
19
19
|
export function useProjectStatus(deps) {
|
|
20
|
-
const { cwd, name, rows, columns, setMode, setNote } = deps;
|
|
20
|
+
const { cwd, name, rows, columns, model, prompt, setMode, setNote } = deps;
|
|
21
21
|
const path = useMemo(() => statusFilePath(cwd), [cwd]);
|
|
22
22
|
// Letta al mount e basta: è ciò che fa ripartire un deck riaperto dall'ora di
|
|
23
23
|
// prima invece che da `missing`.
|
|
@@ -78,7 +78,7 @@ export function useProjectStatus(deps) {
|
|
|
78
78
|
setFailed(false);
|
|
79
79
|
setStartedAt(Date.now());
|
|
80
80
|
setNote(`⏳ project status di ${name}… (sid ${sid.slice(0, 8)})`);
|
|
81
|
-
const child = spawnProjectStatus(cwd, sid, finish);
|
|
81
|
+
const child = spawnProjectStatus(cwd, sid, finish, model, prompt);
|
|
82
82
|
child.on('error', () => {
|
|
83
83
|
setStartedAt(null);
|
|
84
84
|
setFailed(true);
|
package/dist/overlays/wrap.js
CHANGED
|
@@ -14,7 +14,7 @@ import { useWrapScan } from '../hooks.js';
|
|
|
14
14
|
import { pageStep, wrapListCapacity } from '../viewport.js';
|
|
15
15
|
import { cpLen, insertAt, removeAt } from '../layout.js';
|
|
16
16
|
import { sanitizeTyped } from '../glyphs.js';
|
|
17
|
-
import { WRAP_DEFAULT_PATH
|
|
17
|
+
import { WRAP_DEFAULT_PATH } from '../wrap-scan.js';
|
|
18
18
|
export function useWrapOverlay(deps) {
|
|
19
19
|
const { cwd, rows, setMode, setNote, onApply } = deps;
|
|
20
20
|
// Lo stato dello scan vive QUI e non nel modello, sul precedente di
|
|
@@ -69,7 +69,7 @@ export function useWrapOverlay(deps) {
|
|
|
69
69
|
if (key.return) {
|
|
70
70
|
const target = path.trim() || WRAP_DEFAULT_PATH;
|
|
71
71
|
close();
|
|
72
|
-
onApply(target
|
|
72
|
+
onApply(target);
|
|
73
73
|
return;
|
|
74
74
|
}
|
|
75
75
|
if (key.upArrow) {
|
package/dist/prompt-catalog.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import { readFileSync } from 'node:fs';
|
|
17
17
|
import { fileURLToPath } from 'node:url';
|
|
18
18
|
import { dirname, join } from 'node:path';
|
|
19
|
-
import { MODELS } from './spawn.js';
|
|
19
|
+
import { MODELS } from './spawn-catalog.js';
|
|
20
20
|
/** Stessa risalita di `DECK_RUN`: src/ (dev) e dist/ (build) sono entrambi
|
|
21
21
|
* figli della package root, quindi il sibling è un livello sopra. */
|
|
22
22
|
export const PROMPT_CATALOG = join(dirname(fileURLToPath(import.meta.url)), '..', 'scripts', 'prompt-catalog');
|