@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/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');
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// T161 — il gemello TypeScript di `_sane_note`, la riduzione che `deck-run`
|
|
2
|
+
// applica a ogni nota del titolo.
|
|
3
|
+
//
|
|
4
|
+
// Perché serve un gemello. La nota del titolo è l'UNICO ingresso di testo libero
|
|
5
|
+
// nel titolo di una tab, quindi `deck-run` non la quota: la RIDUCE a una
|
|
6
|
+
// whitelist chiusa di code point, e tutto ciò che non ci rientra sparisce senza
|
|
7
|
+
// errore. Finché la nota nasceva da un campo scritto al volo, la divergenza fra
|
|
8
|
+
// quello che si legge nel deck e quello che compare nella tab durava il tempo di
|
|
9
|
+
// uno spawn. Da quando un TEMPLATE del titolo si scrive in una pagina e si salva
|
|
10
|
+
// su disco, quella divergenza diventa permanente: il file direbbe una cosa e la
|
|
11
|
+
// tab ne mostrerebbe un'altra, per sempre e senza un errore da nessuna parte.
|
|
12
|
+
//
|
|
13
|
+
// La riduzione si applica quindi QUI, al confine — al salvataggio e alla
|
|
14
|
+
// lettura del blocco `spawn` — così quello che sta nel file è quello che la tab
|
|
15
|
+
// mostrerà.
|
|
16
|
+
//
|
|
17
|
+
// L'ALLINEAMENTO fra i due lati non è assunto: `test/sane-note.test.ts` passa la
|
|
18
|
+
// stessa batteria di input a questa funzione e a `deck-run` vero (via lo shim
|
|
19
|
+
// `ptyxis` di `deck-run.test.ts`) e pretende l'uguaglianza. La whitelist non si
|
|
20
|
+
// allarga — e se un giorno lo facesse, sono tre le sedi da toccare insieme
|
|
21
|
+
// (`deck-run`, questo file, le emoji dei titoli in `spawn-catalog.ts`), col test
|
|
22
|
+
// che fallisce se una resta indietro.
|
|
23
|
+
/**
|
|
24
|
+
* L'alfabeto ammesso, negato: lettere, cifre, spazio, `_`, `-`, le accentate
|
|
25
|
+
* italiane e sei emoji.
|
|
26
|
+
*
|
|
27
|
+
* Whitelist e non blacklist, come in bash: il titolo finisce dentro apici
|
|
28
|
+
* singoli in `bash -lc "claude --name '…'"`, ed enumerare i caratteri pericolosi
|
|
29
|
+
* significa sbagliarne uno.
|
|
30
|
+
*
|
|
31
|
+
* Le sei emoji sono nominate una per una e NON come intervallo di code point,
|
|
32
|
+
* per due ragioni che valgono su entrambi i lati: un'emoji arriva nella tab solo
|
|
33
|
+
* se il deck la mostra identica in lista, cioè se `sanitize` (`src/width.ts`) la
|
|
34
|
+
* lascia passare invece di renderla `·`, e quel verdetto si verifica un glifo
|
|
35
|
+
* alla volta; e l'ordine di un intervallo dentro un bracket expression segue la
|
|
36
|
+
* collazione della locale, non i code point.
|
|
37
|
+
*
|
|
38
|
+
* Il flag `u` non è cosmetico: senza, le quattro emoji astrali entrerebbero
|
|
39
|
+
* nella classe come coppie di surrogati sciolti, e la riduzione lascerebbe
|
|
40
|
+
* passare mezzo carattere di qualunque altra emoji che ne condivida il primo.
|
|
41
|
+
*/
|
|
42
|
+
const OUT_OF_ALPHABET = /[^A-Za-z0-9 _àèéìòùÀÈÉÌÒÙ📐🚀📊🏁🧹📏-]/gu;
|
|
43
|
+
/** Cap in CARATTERI (code point), non in byte: a byte un taglio a metà di una
|
|
44
|
+
* `à` lascerebbe UTF-8 rotto nel titolo. Largo di proposito — una tab che sfora
|
|
45
|
+
* tronca a destra da sola, e il limite serve solo a non spingere fuori vista la
|
|
46
|
+
* parte del titolo che compass matcha. */
|
|
47
|
+
export const SANE_CAP = 60;
|
|
48
|
+
/** Un buco di template: `{TASK}`, `{slug}`, `{file}`, `{path}`. La cattura nel
|
|
49
|
+
* gruppo è ciò che fa comparire i buchi nei dispari di `split`. */
|
|
50
|
+
const HOLE = /(\{[A-Za-z]+\})/;
|
|
51
|
+
/** Spazi collassati, estremi potati, cap: i tre passi che in bash sono `tr -s`,
|
|
52
|
+
* i due `${s#/%}` e `${s:0:60}`. */
|
|
53
|
+
function tidy(s) {
|
|
54
|
+
const squeezed = s.replace(/ +/g, ' ');
|
|
55
|
+
const trimmed = squeezed.replace(/^ /, '').replace(/ $/, '');
|
|
56
|
+
return [...trimmed].slice(0, SANE_CAP).join('');
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* La nota ridotta, esattamente come la ridurrebbe `deck-run`.
|
|
60
|
+
*
|
|
61
|
+
* Si applica al titolo GIÀ interpolato, cioè al testo che finisce nel sidecar e
|
|
62
|
+
* in `--title-note`: le due superfici che mostrano quel nome — la riga di lista
|
|
63
|
+
* del deck, che legge la nota grezza dal sidecar, e la tab Ptyxis — devono
|
|
64
|
+
* vedere la stessa stringa.
|
|
65
|
+
*/
|
|
66
|
+
export function saneNote(raw) {
|
|
67
|
+
return tidy(raw.replace(OUT_OF_ALPHABET, ''));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* La stessa riduzione, coi BUCHI preservati.
|
|
71
|
+
*
|
|
72
|
+
* Le graffe non sono nell'alfabeto: passare un template da `saneNote` lo
|
|
73
|
+
* ridurrebbe a `📐 slug`, cioè a un titolo che nomina la parola «slug» invece di
|
|
74
|
+
* interpolare il nome della task. Il template si riduce comunque — è testo
|
|
75
|
+
* scritto a mano, e il resto della riga deve sopravvivere alla tab — ma i buchi
|
|
76
|
+
* attraversano intatti e vengono riempiti dopo.
|
|
77
|
+
*
|
|
78
|
+
* Fuori dai buchi il comportamento è identico a `saneNote`, compresi collasso
|
|
79
|
+
* degli spazi, potatura e cap: un template senza buchi passa dalle stesse regole
|
|
80
|
+
* di una nota qualunque.
|
|
81
|
+
*/
|
|
82
|
+
export function saneTemplate(raw) {
|
|
83
|
+
return tidy(raw
|
|
84
|
+
.split(HOLE)
|
|
85
|
+
.map((part, i) => (i % 2 === 1 ? part : part.replace(OUT_OF_ALPHABET, '')))
|
|
86
|
+
.join(''));
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Riempie i buchi di un template. `null` quando un buco nominato non ha un
|
|
90
|
+
* valore da mettere.
|
|
91
|
+
*
|
|
92
|
+
* Il `null` non è difensivo, è il comportamento: un titolo `📐 {slug}` su una
|
|
93
|
+
* task il cui file non si trova diventerebbe `📐` — un prefisso solo, uguale per
|
|
94
|
+
* ogni task, che è peggio di nessun titolo. Chi chiama riceve `null` e apre la
|
|
95
|
+
* conversazione senza nota, che è ciò che faceva il vecchio `fallbackTitle`
|
|
96
|
+
* quando il task file mancava.
|
|
97
|
+
*
|
|
98
|
+
* I buchi non dichiarati dall'azione restano LETTERALI e non fanno fallire
|
|
99
|
+
* l'interpolazione: non sono un valore mancante, sono testo che l'utente ha
|
|
100
|
+
* scritto — e il giro di riduzione dopo l'interpolazione toglierà le graffe.
|
|
101
|
+
*/
|
|
102
|
+
export function interpolate(template, values) {
|
|
103
|
+
let missing = false;
|
|
104
|
+
const filled = template.replace(/\{([A-Za-z]+)\}/g, (whole, name) => {
|
|
105
|
+
if (!(name in values))
|
|
106
|
+
return whole;
|
|
107
|
+
const v = values[name];
|
|
108
|
+
if (!v) {
|
|
109
|
+
missing = true;
|
|
110
|
+
return '';
|
|
111
|
+
}
|
|
112
|
+
return v;
|
|
113
|
+
});
|
|
114
|
+
return missing ? null : filled;
|
|
115
|
+
}
|