@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
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
// T161 — gli OVERRIDE di progetto delle azioni di spawn: lettura, validazione,
|
|
2
|
+
// risoluzione della terna, scrittura.
|
|
3
|
+
//
|
|
4
|
+
// ── La sede (P19 preflight) ─────────────────────────────────────────────────
|
|
5
|
+
// Blocco `spawn` dentro `.claude/loom-works.json`, il file di progetto che il
|
|
6
|
+
// deck già apre in tre loader (`launch`, `archivableDays`, `identity`) e che
|
|
7
|
+
// `deck-run` legge con `jq`. Nessun file nuovo: sotto `.claude/` ci sono già tre
|
|
8
|
+
// file di loom, e le impostazioni di spawn sono decisioni di progetto che devono
|
|
9
|
+
// viaggiare col repo — cioè committate, esattamente come `permissionMode` e
|
|
10
|
+
// `order`, e a differenza della vista (`.claude/loom/deck-view.json`, che è
|
|
11
|
+
// gitignored perché è una preferenza di questa macchina).
|
|
12
|
+
//
|
|
13
|
+
// Il costo della scelta, dichiarato: da qui in poi il deck SCRIVE in
|
|
14
|
+
// `loom-works.json`, cosa che prima non faceva mai. Il saver è un
|
|
15
|
+
// read-modify-write che conserva ogni altra chiave nel suo ordine e tocca il
|
|
16
|
+
// solo blocco `spawn`, e RIFIUTA di scrivere se il file non si parsa — quel file
|
|
17
|
+
// regge identità, label e registry di compass, e rigenerarlo da zero al posto di
|
|
18
|
+
// uno rotto significherebbe perderli.
|
|
19
|
+
//
|
|
20
|
+
// Lo schema del plugin (`templates/loom-works.schema.json`) dichiara
|
|
21
|
+
// `additionalProperties: false` e il blocco `spawn` non vi compare ancora: non è
|
|
22
|
+
// un errore a runtime, perché `cfg_validate` in `lib-config.sh` è jq scritto a
|
|
23
|
+
// mano sui soli campi noti e un campo sconosciuto passa. Lo schema è reference
|
|
24
|
+
// per chi scrive il file a mano, e la voce `"spawn": { "type": "object" }` lo
|
|
25
|
+
// raggiunge al prossimo publish del plugin (Prod Validation della task).
|
|
26
|
+
//
|
|
27
|
+
// ── Il formato ──────────────────────────────────────────────────────────────
|
|
28
|
+
// "spawn": { "<id azione>": { "title"?: string, "model"?: alias, "prompt"?: string } }
|
|
29
|
+
//
|
|
30
|
+
// SOLO override: una chiave assente significa «il default del deck», e il blocco
|
|
31
|
+
// sparisce dal file quando non resta nessun override. Vuoto ≠ assente (P15): un
|
|
32
|
+
// `"title": ""` è un override legittimo che dice «nessuna nota».
|
|
33
|
+
//
|
|
34
|
+
// ── Chi legge cosa ──────────────────────────────────────────────────────────
|
|
35
|
+
// Il blocco lo legge il SOLO deck, che risolve la terna e la passa a `deck-run`
|
|
36
|
+
// sempre esplicita nell'argv (`--title-note`, `--model`, `--prompt`). `deck-run`
|
|
37
|
+
// non lo legge e non deve: `jq` è dipendenza OPZIONALE di quello script, e
|
|
38
|
+
// dargli un blocco JSON da consultare la renderebbe obbligatoria su un percorso
|
|
39
|
+
// che oggi degrada senza. La cascata env → catalogo → `fable` di `deck-run`
|
|
40
|
+
// resta intatta per chi lo invoca a mano.
|
|
41
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
42
|
+
import { configFilePath } from './config.js';
|
|
43
|
+
import { modelFor } from './prompt-catalog.js';
|
|
44
|
+
import { interpolate, saneNote, saneTemplate } from './sane-note.js';
|
|
45
|
+
import { MODEL_DEFAULT, SPAWN_ACTIONS, SPAWN_FIELDS, editableFields, isModelKind, spawnAction, } from './spawn-catalog.js';
|
|
46
|
+
/** La chiave del blocco nel file di progetto. Costante e non letterale sparso:
|
|
47
|
+
* la nominano il parser, il saver e il messaggio d'avviso. */
|
|
48
|
+
export const SPAWN_BLOCK = 'spawn';
|
|
49
|
+
/**
|
|
50
|
+
* Validazione per VOCE, non per file (P7).
|
|
51
|
+
*
|
|
52
|
+
* La voce sbagliata cade con una nota che la nomina, il resto vive. Rifiutare
|
|
53
|
+
* l'intero blocco alla prima chiave sporca significherebbe che un refuso su una
|
|
54
|
+
* riga spegne in silenzio gli override di tutte le altre; e conservare per
|
|
55
|
+
* sempre le voci ignote significherebbe riscriverle a ogni salvataggio, cioè
|
|
56
|
+
* tenere in vita nel file una configurazione che nessuno legge più. È lo stesso
|
|
57
|
+
* regime di `sanitizeSort` (`view-store.ts`) e di `parseLaunch`.
|
|
58
|
+
*
|
|
59
|
+
* Cosa cade e cosa no:
|
|
60
|
+
* - id fuori catalogo → l'intera voce, con nota (un'azione che non esiste più);
|
|
61
|
+
* - modello fuori dai quattro alias → il solo campo, con nota (arriverebbe al
|
|
62
|
+
* CLI e produrrebbe una tab con un comando che fallisce all'avvio);
|
|
63
|
+
* - campo su una cella FISSA → il solo campo, con nota (il valore non avrebbe
|
|
64
|
+
* nessuna strada per arrivare allo spawn);
|
|
65
|
+
* - titolo → ridotto, mai scartato: quello che si legge nella pagina è quello
|
|
66
|
+
* che comparirà nella tab, anche quando il file è stato scritto a mano;
|
|
67
|
+
* - prompt → qualunque testo, che viaggia come argv singolo.
|
|
68
|
+
*/
|
|
69
|
+
export function parseSpawnOverrides(raw) {
|
|
70
|
+
const entries = new Map();
|
|
71
|
+
const warnings = [];
|
|
72
|
+
if (!raw || typeof raw !== 'object')
|
|
73
|
+
return { entries, warnings };
|
|
74
|
+
const block = raw[SPAWN_BLOCK];
|
|
75
|
+
if (block === undefined)
|
|
76
|
+
return { entries, warnings };
|
|
77
|
+
if (!block || typeof block !== 'object' || Array.isArray(block)) {
|
|
78
|
+
warnings.push(`blocco ${SPAWN_BLOCK} ignorato: non è un oggetto`);
|
|
79
|
+
return { entries, warnings };
|
|
80
|
+
}
|
|
81
|
+
for (const [id, value] of Object.entries(block)) {
|
|
82
|
+
const action = spawnAction(id);
|
|
83
|
+
if (!action) {
|
|
84
|
+
warnings.push(`${SPAWN_BLOCK}.${id}: azione fuori catalogo, voce scartata`);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
88
|
+
warnings.push(`${SPAWN_BLOCK}.${id}: non è un oggetto, voce scartata`);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const src = value;
|
|
92
|
+
const editable = new Set(editableFields(action));
|
|
93
|
+
const out = {};
|
|
94
|
+
for (const field of SPAWN_FIELDS) {
|
|
95
|
+
const v = src[field];
|
|
96
|
+
if (v === undefined)
|
|
97
|
+
continue;
|
|
98
|
+
if (!editable.has(field)) {
|
|
99
|
+
warnings.push(`${SPAWN_BLOCK}.${id}.${field}: ${action.fixed[field]}, campo scartato`);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (field === 'model') {
|
|
103
|
+
if (!isModelKind(v)) {
|
|
104
|
+
warnings.push(`${SPAWN_BLOCK}.${id}.model: '${String(v)}' non è un modello, campo scartato`);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
out.model = v;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (typeof v !== 'string') {
|
|
111
|
+
warnings.push(`${SPAWN_BLOCK}.${id}.${field}: non è un testo, campo scartato`);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (field === 'title')
|
|
115
|
+
out.title = saneTemplate(v);
|
|
116
|
+
else
|
|
117
|
+
out.prompt = v;
|
|
118
|
+
}
|
|
119
|
+
// Una voce rimasta senza nessun campo valido non entra: nella pagina
|
|
120
|
+
// marcherebbe la riga come «toccata» senza che niente sia stato cambiato.
|
|
121
|
+
if (Object.keys(out).length > 0)
|
|
122
|
+
entries.set(action.id, out);
|
|
123
|
+
}
|
|
124
|
+
return { entries, warnings };
|
|
125
|
+
}
|
|
126
|
+
/** File assente → nessun override e NESSUN avviso: è lo stato normale di un
|
|
127
|
+
* progetto che non ha mai aperto la pagina, come per `loadLaunch` e
|
|
128
|
+
* `loadArchivableDays`. Illeggibile o non-JSON → default con avviso: lì c'è un
|
|
129
|
+
* file, e sta dicendo qualcosa che non si riesce a leggere. */
|
|
130
|
+
export function loadSpawnOverrides(projectRoot) {
|
|
131
|
+
let text;
|
|
132
|
+
try {
|
|
133
|
+
text = readFileSync(configFilePath(projectRoot), 'utf8');
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return { entries: new Map(), warnings: [] };
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
return parseSpawnOverrides(JSON.parse(text));
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return {
|
|
143
|
+
entries: new Map(),
|
|
144
|
+
warnings: [`${configFilePath(projectRoot)} non si parsa: override di spawn ignorati`],
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Default del deck ← override di progetto, più l'interpolazione dei buchi.
|
|
150
|
+
*
|
|
151
|
+
* La precedenza è a due soli livelli, per tutta la task: i valori cablati nel
|
|
152
|
+
* sorgente sono i default, l'unico livello di override è il progetto. Nessuna
|
|
153
|
+
* configurazione d'utente — né sotto `~/.claude/`, né sotto `~/.config/`.
|
|
154
|
+
*
|
|
155
|
+
* Dentro il livello «default» ci sono però due sorgenti, e l'ordine conta: il
|
|
156
|
+
* modello e il prompt cablati nella riga di catalogo vincono su quelli del
|
|
157
|
+
* catalogo dati, che si consulta solo per le azioni che hanno un `kind`. È
|
|
158
|
+
* l'ordine giusto perché un'azione senza `kind` non ha una riga in
|
|
159
|
+
* `prompt-catalog` per costruzione, e una che ce l'ha non porta valori propri.
|
|
160
|
+
*/
|
|
161
|
+
export function resolveSpawn(action, overrides, catalog, holes = {}) {
|
|
162
|
+
const raw = rawSpawn(action, overrides, catalog);
|
|
163
|
+
let prompt = raw.prompt;
|
|
164
|
+
if (prompt)
|
|
165
|
+
prompt = interpolate(prompt, holes) ?? '';
|
|
166
|
+
// La riduzione si applica DOPO l'interpolazione, perché i buchi portano dentro
|
|
167
|
+
// testo che l'alfabeto non ammette: uno slug col punto, un path con le barre.
|
|
168
|
+
// Quello che esce di qui è la stessa stringa che `deck-run` comporrà.
|
|
169
|
+
const filled = raw.title === null ? null : interpolate(raw.title, holes);
|
|
170
|
+
const title = filled === null ? null : saneNote(filled) || null;
|
|
171
|
+
return { title, model: raw.model, prompt, overridden: raw.overridden };
|
|
172
|
+
}
|
|
173
|
+
/** La terna di un'azione per ID di catalogo. Due chiamanti — l'attuatore, che la
|
|
174
|
+
* espone a tutte le sedi di spawn, e il valore iniziale del selettore della
|
|
175
|
+
* sessione nuda, che si risolve prima che l'attuatore esista — e una sola
|
|
176
|
+
* implementazione. */
|
|
177
|
+
export function resolveSpawnId(id, overrides, catalog, holes = {}) {
|
|
178
|
+
return resolveSpawn(spawnAction(id), overrides, catalog, holes);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Gli stessi tre valori PRIMA dell'interpolazione: i template coi buchi ancora
|
|
182
|
+
* dentro.
|
|
183
|
+
*
|
|
184
|
+
* È la forma che la pagina mostra e fa modificare (P9 preflight): nella cella si
|
|
185
|
+
* legge `🚀 {slug}`, non il titolo di una task particolare — un valore già reso
|
|
186
|
+
* si modificherebbe a partire da un esempio, e ogni modifica cancellerebbe il
|
|
187
|
+
* buco senza che nulla lo dica. L'esempio reso sta accanto, nell'area di
|
|
188
|
+
* compilazione, dove serve a vedere l'effetto senza essere il campo.
|
|
189
|
+
*
|
|
190
|
+
* Serve anche al confronto col DEFAULT: chi conferma una modifica scrive la
|
|
191
|
+
* cella solo se differisce dal grezzo che uscirebbe senza override, ed è così
|
|
192
|
+
* che un valore riportato a mano al default smette di essere un override invece
|
|
193
|
+
* di restare nel file come una riga che non cambia niente.
|
|
194
|
+
*/
|
|
195
|
+
export function rawSpawn(action, overrides, catalog) {
|
|
196
|
+
const ov = overrides.get(action.id) ?? {};
|
|
197
|
+
const overridden = new Set(Object.keys(ov));
|
|
198
|
+
const model = ov.model ??
|
|
199
|
+
action.model ??
|
|
200
|
+
(action.kind ? modelFor(catalog, action.kind) : undefined) ??
|
|
201
|
+
MODEL_DEFAULT;
|
|
202
|
+
let prompt;
|
|
203
|
+
if (ov.prompt !== undefined)
|
|
204
|
+
prompt = ov.prompt;
|
|
205
|
+
else if (action.prompt !== null)
|
|
206
|
+
prompt = action.prompt;
|
|
207
|
+
// Catalogo dati assente o voce mancante → nessun prompt, non un testo
|
|
208
|
+
// inventato qui: un default scritto nel deck sarebbe la copia del catalogo che
|
|
209
|
+
// il file dati esiste per non avere.
|
|
210
|
+
else if (action.kind)
|
|
211
|
+
prompt = catalog.get(action.kind)?.template ?? '';
|
|
212
|
+
else
|
|
213
|
+
prompt = null;
|
|
214
|
+
return { title: ov.title ?? action.title, model, prompt, overridden };
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Il blocco `spawn` come lo si scrive nel file: le sole voci non vuote, coi soli
|
|
218
|
+
* campi presenti, ordinate come il catalogo.
|
|
219
|
+
*
|
|
220
|
+
* L'ordine è quello di `SPAWN_ACTIONS` e non quello di inserimento nella mappa:
|
|
221
|
+
* il file è committato e lo si legge in diff, dove una riga che si sposta da
|
|
222
|
+
* sola a ogni salvataggio è rumore su cui poi si fa merge.
|
|
223
|
+
*
|
|
224
|
+
* `undefined` quando non resta nessun override: il chiamante toglie la chiave
|
|
225
|
+
* invece di scrivere `"spawn": {}`, che sarebbe una configurazione vuota da
|
|
226
|
+
* spiegare a chi la legge.
|
|
227
|
+
*/
|
|
228
|
+
export function serializeSpawnBlock(entries) {
|
|
229
|
+
const out = {};
|
|
230
|
+
// L'ordine viene da `SPAWN_ACTIONS`, non da una seconda lista di id scritta
|
|
231
|
+
// qui: una copia dell'elenco diverge alla prima riga aggiunta al catalogo, e
|
|
232
|
+
// la voce nuova finirebbe fuori dal file senza che niente lo segnali.
|
|
233
|
+
for (const action of SPAWN_ACTIONS) {
|
|
234
|
+
const ov = entries.get(action.id);
|
|
235
|
+
if (!ov)
|
|
236
|
+
continue;
|
|
237
|
+
const clean = {};
|
|
238
|
+
if (ov.title !== undefined)
|
|
239
|
+
clean.title = ov.title;
|
|
240
|
+
if (ov.model !== undefined)
|
|
241
|
+
clean.model = ov.model;
|
|
242
|
+
if (ov.prompt !== undefined)
|
|
243
|
+
clean.prompt = ov.prompt;
|
|
244
|
+
if (Object.keys(clean).length > 0)
|
|
245
|
+
out[action.id] = clean;
|
|
246
|
+
}
|
|
247
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Scrive il blocco `spawn` nel file di progetto, conservando tutto il resto.
|
|
251
|
+
*
|
|
252
|
+
* Read-modify-write su un file che NON è nostro: `loom-works.json` regge
|
|
253
|
+
* identità, label, surface, registry di compass e `permissionMode`, e il deck
|
|
254
|
+
* finora non ci aveva mai scritto. Da qui le due cautele:
|
|
255
|
+
* - se il file non si parsa la funzione LANCIA invece di rigenerarlo: un
|
|
256
|
+
* `loom-works.json` rotto va riparato a mano, non sostituito con uno che ha
|
|
257
|
+
* perso l'identità del progetto;
|
|
258
|
+
* - ogni altra chiave sopravvive nel suo ordine (`JSON.parse` conserva l'ordine
|
|
259
|
+
* di inserimento delle chiavi stringa, `JSON.stringify` lo riscrive uguale),
|
|
260
|
+
* così il diff del commit mostra il solo blocco toccato.
|
|
261
|
+
*
|
|
262
|
+
* File ASSENTE: anche qui si lancia. Un progetto senza `loom-works.json` non è
|
|
263
|
+
* registrato, e creargliene uno col solo blocco `spawn` produrrebbe un file che
|
|
264
|
+
* fallisce ogni validazione a valle.
|
|
265
|
+
*/
|
|
266
|
+
export function saveSpawnOverrides(projectRoot, entries) {
|
|
267
|
+
const path = configFilePath(projectRoot);
|
|
268
|
+
const raw = readFileSync(path, 'utf8');
|
|
269
|
+
const parsed = JSON.parse(raw);
|
|
270
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
271
|
+
throw new Error(`${path}: non è un oggetto JSON`);
|
|
272
|
+
}
|
|
273
|
+
const doc = parsed;
|
|
274
|
+
const block = serializeSpawnBlock(entries);
|
|
275
|
+
if (block)
|
|
276
|
+
doc[SPAWN_BLOCK] = block;
|
|
277
|
+
else
|
|
278
|
+
delete doc[SPAWN_BLOCK];
|
|
279
|
+
writeFileSync(path, JSON.stringify(doc, null, 2) + '\n', 'utf8');
|
|
280
|
+
}
|
package/dist/spawn.js
CHANGED
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
4
|
import { EventEmitter } from 'node:events';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import {
|
|
7
|
-
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
// T161 — i cataloghi (kind, modelli, azioni) stanno in `spawn-catalog.ts`, che
|
|
8
|
+
// è DATO PURO e non importa questo file: la dipendenza va in un verso solo.
|
|
9
|
+
// Qui restano i soli effetti verso l'esterno — comporre un argv e lanciarlo.
|
|
10
|
+
import { MODEL_DEFAULT, PROJECT_STATUS_MODEL } from './spawn-catalog.js';
|
|
8
11
|
// scripts/deck-run è un sibling della dir del bundle: src/ (dev, tsx) e dist/
|
|
9
12
|
// (build, node) stanno entrambi sotto la package root → risalita di un livello.
|
|
10
13
|
export const DECK_RUN = join(dirname(fileURLToPath(import.meta.url)), '..', 'scripts', 'deck-run');
|
|
@@ -118,171 +121,50 @@ export function onInTabCommand(child, cb) {
|
|
|
118
121
|
}
|
|
119
122
|
});
|
|
120
123
|
}
|
|
121
|
-
/**
|
|
122
|
-
* `recap` → la sotto-skill giusta, quando chi spawna SA se la task è un cappello.
|
|
123
|
-
*
|
|
124
|
-
* `recap` resta il kind onesto per chi non lo sa: punta al dispatcher, che
|
|
125
|
-
* risolve la task e classifica da sé. È il caso degli acceleratori della lista,
|
|
126
|
-
* dove il deck ha in mano solo `tasks.md` — e lì il `Size` non c'è. Il DETAIL
|
|
127
|
-
* invece il task file l'ha già letto, quindi può saltare il giro e pagare un
|
|
128
|
-
* turno di modello in meno.
|
|
129
|
-
*
|
|
130
|
-
* Non è una classificazione duplicata: il criterio (`Size: Epic`) resta uno solo
|
|
131
|
-
* e sta in `taskIsEpic`, che legge lo stesso campo che leggerebbe il dispatcher.
|
|
132
|
-
* Quello che si evita è il RITARDO, non il giudizio.
|
|
133
|
-
*
|
|
134
|
-
* Ogni kind diverso da `recap` passa intatto: la specializzazione è un caso, non
|
|
135
|
-
* una trasformazione da applicare a tutti.
|
|
136
|
-
*/
|
|
137
|
-
export function specializeRecap(kind, epic) {
|
|
138
|
-
if (kind !== 'recap')
|
|
139
|
-
return kind;
|
|
140
|
-
return epic ? 'recap-epic' : 'recap-task';
|
|
141
|
-
}
|
|
142
|
-
// L'ordine È il giro di `tab` nel detail, non una preferenza di lettura:
|
|
143
|
-
// cambiarlo sposta le voci sotto le dita di chi le ha imparate. Fino a T111 era
|
|
144
|
-
// anche il binding delle cifre `1`-`4`, passate poi al campo nota.
|
|
145
|
-
export const MODELS = ['fable', 'opus', 'sonnet', 'haiku'];
|
|
146
|
-
// Default del selettore e FALLBACK per ogni kind fuori catalogo (kind `none`,
|
|
147
|
-
// o un catalogo mutilo). Duplicato del default di deck-run e non letto da lì:
|
|
148
|
-
// il deck deve poter MOSTRARE la selezione iniziale prima di spawnare
|
|
149
|
-
// alcunché, e un valore che si conosce solo a spawn avvenuto non è mostrabile.
|
|
150
|
-
//
|
|
151
|
-
// T152 — non è più IL default di `^K`/`^P`/`^R` dalla lista: quei percorsi
|
|
152
|
-
// leggono il catalogo come il detail (`modelFor`), e cadono qui solo quando il
|
|
153
|
-
// kind non ha una riga. Resta l'unico default che resume e fork usano per
|
|
154
|
-
// intero, perché lì il modello non è mai una funzione del kind — è quello
|
|
155
|
-
// della conversazione d'origine (T148).
|
|
156
|
-
export const MODEL_DEFAULT = 'fable';
|
|
157
|
-
// T152 — `recap-status-project` non è una chiave del catalogo (§Doc Impact,
|
|
158
|
-
// "le chiavi del catalogo sono i valori di --prompt-kind"): lo spawn headless
|
|
159
|
-
// non passa mai --prompt-kind, quindi una riga lì sarebbe irraggiungibile da
|
|
160
|
-
// ogni chiamante. Il default vive qui, accanto a MODEL_DEFAULT, e non nel
|
|
161
|
-
// catalogo. Resta `opus` mentre i tre recap del catalogo stanno su fable: è
|
|
162
|
-
// l'unico recap che gira in headless (`-p`), senza nessuno che ne legga
|
|
163
|
-
// l'esito mentre si forma e possa rilanciarlo — il testo prodotto è il
|
|
164
|
-
// deliverable, e viene riletto da disco anche giorni dopo.
|
|
165
|
-
export const PROJECT_STATUS_MODEL = 'opus';
|
|
166
|
-
// T66 — le azioni del detail. Non sono un catalogo nuovo: ognuna è un
|
|
167
|
-
// `--prompt-kind` già esistente più `checkpoint`, e tutte passano dallo stesso
|
|
168
|
-
// `spawnForTask` dei CTRL della lista — una superficie in più, zero percorsi di
|
|
169
|
-
// spawn in più.
|
|
170
|
-
//
|
|
171
|
-
// L'etichetta è distinta dal kind dove il kind è il nome del MECCANISMO e
|
|
172
|
-
// l'etichetta quello dell'INTENZIONE: `none` è "aprire la task a mani nude",
|
|
173
|
-
// `recap` è "vedere a che punto sta".
|
|
174
|
-
export const DETAIL_ACTIONS = [
|
|
175
|
-
{ kind: 'none', label: 'open' },
|
|
176
|
-
{ kind: 'preflight', label: 'preflight' },
|
|
177
|
-
{ kind: 'run', label: 'run' },
|
|
178
|
-
{ kind: 'recap', label: 'status' },
|
|
179
|
-
{ kind: 'checkpoint', label: 'checkpoint' },
|
|
180
|
-
];
|
|
181
|
-
// T117 — selezione DIRETTA di un'azione con la sua iniziale (`o p r s c`),
|
|
182
|
-
// accanto allo scorrimento `←→`. Derivate dalle label e non cablate: una voce
|
|
183
|
-
// aggiunta al catalogo porta con sé la propria lettera, invece di lasciare
|
|
184
|
-
// indietro una seconda lista.
|
|
185
|
-
// Il vincolo che la derivazione impone al catalogo: le iniziali devono restare
|
|
186
|
-
// DISTINTE fra loro. Due label con la stessa lettera renderebbero la seconda
|
|
187
|
-
// irraggiungibile, e in silenzio — chi aggiunge una voce lo controlla qui.
|
|
188
|
-
export const ACTION_HOTKEYS = Object.fromEntries(DETAIL_ACTIONS.map((a, i) => [a.label[0], i]));
|
|
189
|
-
// T150 — titolo di fallback quando il campo nota è vuoto, derivato dall'AZIONE
|
|
190
|
-
// e dal task. Mappa sul KIND, non sulla label (P2 preflight): alla sede del
|
|
191
|
-
// fallback (`spawnForTask` in actions.ts) arriva il kind — già specializzato
|
|
192
|
-
// quando la chiamata viene dal detail (`recap-task`/`recap-epic`) — mai la
|
|
193
|
-
// label del catalogo.
|
|
194
|
-
// D2/P7 — `none` (label "open") non ha un prefisso: è "nessuna azione", e
|
|
195
|
-
// inventargliene uno contraddirebbe l'intenzione dell'utente.
|
|
196
|
-
// P3 — le tre varianti di recap condividono il prefisso: la specializzazione
|
|
197
|
-
// sceglie quale skill parte, non cosa l'utente sta chiedendo.
|
|
198
|
-
//
|
|
199
|
-
// T156 — emoji invece della parola in maiuscolo (`PREFL`, `RUN`, …): il titolo
|
|
200
|
-
// si legge in lista deck e nella tab Ptyxis, dove l'azione va riconosciuta a
|
|
201
|
-
// colpo d'occhio e non letta.
|
|
202
|
-
// Le quattro sono ASTRALI (code point ≥ U+10000) per necessità, non per gusto:
|
|
203
|
-
// `sanitize` (`src/width.ts`) sostituisce con `·` ogni glifo su cui le due
|
|
204
|
-
// contabilità di larghezza divergono, e le emoji del BMP a presentazione testo
|
|
205
|
-
// (⚙️ ⏱️ 🛠️ …) divergono sempre — string-width le conta 2, VTE ne disegna 1, e
|
|
206
|
-
// il VS16 non sposta nessuno dei due verdetti. Un'emoji aggiunta qui va prima
|
|
207
|
-
// passata in `sanitize` per vedere se sopravvive.
|
|
208
|
-
// Le stesse quattro stanno anche nella whitelist di `_sane_note` in `deck-run`,
|
|
209
|
-
// che è una lista chiusa di code point (P2 preflight): cambiare una voce qui e
|
|
210
|
-
// non là riapre la divergenza fra titolo in lista e titolo in tab. La whitelist
|
|
211
|
-
// è più larga di questa tabella — porta anche le emoji dei titoli che non
|
|
212
|
-
// nascono da un'azione su task (🧹 drain di un file inbox, `src/inbox.ts`; 📏
|
|
213
|
-
// srotolamento di un path, `src/wrap-scan.ts`).
|
|
214
|
-
// Le quadre che delimitavano il prefisso sono cadute: l'emoji da sola si stacca
|
|
215
|
-
// già dallo slug, e con lei sono uscite dalla whitelist di `_sane_note`.
|
|
216
|
-
const ACTION_EMOJI = {
|
|
217
|
-
preflight: '📐',
|
|
218
|
-
run: '🚀',
|
|
219
|
-
recap: '📊',
|
|
220
|
-
'recap-task': '📊',
|
|
221
|
-
'recap-epic': '📊',
|
|
222
|
-
checkpoint: '🏁',
|
|
223
|
-
};
|
|
224
|
-
/**
|
|
225
|
-
* Titolo di fallback per una conversazione con nota vuota: `{emoji} {slug}`,
|
|
226
|
-
* o il solo slug per `none` (D1/D2 preflight). Lo slug viene dal NOME del
|
|
227
|
-
* task file (`findTaskFile`), non dalla descrizione di `tasks.md`: il nome è
|
|
228
|
-
* già dentro l'alfabeto di `_sane_note` per costruzione — minuscolo, separato
|
|
229
|
-
* da trattini, senza punteggiatura — mentre la descrizione porta apostrofi e
|
|
230
|
-
* `/` che la riduzione toglie senza sostituto, saldando le parole (D1
|
|
231
|
-
* razionale).
|
|
232
|
-
*
|
|
233
|
-
* Il PREFISSO invece nell'alfabeto non ci sta per costruzione: l'emoji ci entra
|
|
234
|
-
* solo perché la whitelist di `_sane_note` le nomina una per una (T156). È
|
|
235
|
-
* l'unica parte del titolo di fallback che dipende da una modifica fatta
|
|
236
|
-
* dall'altro lato.
|
|
237
|
-
*
|
|
238
|
-
* `null` quando il task file non si trova: il chiamante decide se ripiegare
|
|
239
|
-
* su nota vuota o su un altro fallback.
|
|
240
|
-
*/
|
|
241
|
-
export function fallbackTitle(tasksDir, id, kind) {
|
|
242
|
-
const path = findTaskFile(tasksDir, id);
|
|
243
|
-
if (!path)
|
|
244
|
-
return null;
|
|
245
|
-
const slug = basename(path, '.md').slice(id.length + 1).replace(/-/g, ' ');
|
|
246
|
-
const emoji = ACTION_EMOJI[kind];
|
|
247
|
-
return emoji ? `${emoji} ${slug}` : slug;
|
|
248
|
-
}
|
|
249
124
|
// Spawn detached: il deck spawna ma NON contiene la sessione (la possiede
|
|
250
125
|
// ptyxis-agent). unref + stdio ignore → ritorna subito, la TUI resta viva.
|
|
251
126
|
// sessionId pinnato (T27) → il binding sidecar è deterministico allo spawn.
|
|
252
|
-
// Il
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
// Il modello, al contrario del kind, ha un default (T108): i percorsi che non
|
|
256
|
-
// passano dal selettore del detail non devono nominarlo per forza, e l'unico
|
|
257
|
-
// valore sensato per loro è quello che il selettore stesso mostra all'apertura.
|
|
127
|
+
// Il modello ha un default (T108): i percorsi che non passano dal selettore del
|
|
128
|
+
// detail non devono nominarlo per forza, e l'unico valore sensato per loro è
|
|
129
|
+
// quello che il selettore stesso mostra all'apertura.
|
|
258
130
|
// T111 — `spawnNote` è la nota data alla NASCITA della conversazione, dal campo
|
|
259
|
-
// sempre attivo del detail
|
|
131
|
+
// sempre attivo del detail — che da T161 nasce PRE-RIEMPITO col template di
|
|
132
|
+
// titolo dell'azione, reso sulla task aperta. Ne discende che un campo svuotato
|
|
133
|
+
// a mano significa «nessuna nota» e non più «usa il titolo di fallback»: il
|
|
134
|
+
// fallback non esiste più, perché il default è già a schermo.
|
|
135
|
+
// Stesso flag `--title-note` del resume (T64): il
|
|
260
136
|
// suffisso `«nota»` viene appeso a `TITLE` dentro deck-run PRIMA che i rami si
|
|
261
137
|
// separino, quindi il flag non appartiene alla ripresa — vale su ogni spawn.
|
|
262
138
|
// Assente quando la nota è vuota, e non passato vuoto: `--title-note ''`
|
|
263
139
|
// produrrebbe un `«»` a vuoto nel titolo.
|
|
264
|
-
// T117 — `prompt` è il TESTO letterale, e
|
|
265
|
-
// `--prompt
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
140
|
+
// T117 — `prompt` è il TESTO letterale, e sostituisce il kind: `--prompt` e
|
|
141
|
+
// `--prompt-kind` sono mutuamente esclusivi in deck-run.
|
|
142
|
+
//
|
|
143
|
+
// T161 — dal deck viaggia SEMPRE il testo, mai più il simbolo. Prima gli
|
|
144
|
+
// acceleratori della lista passavano `--prompt-kind` e lasciavano risolvere il
|
|
145
|
+
// testo a `deck-run`, che consultava il proprio catalogo; il detail passava già
|
|
146
|
+
// il letterale. Con un override di progetto sui prompt la risoluzione deve
|
|
147
|
+
// stare da una parte sola, e quella parte è il deck — che è l'unico dei due a
|
|
148
|
+
// leggere `.claude/loom-works.json` per questo (`jq` è dipendenza opzionale di
|
|
149
|
+
// `deck-run`). Un percorso solo, quindi, e ciò che parte è verificabile
|
|
150
|
+
// sull'argv.
|
|
151
|
+
//
|
|
152
|
+
// Due conseguenze dichiarate: `--prompt-kind` resta in `deck-run` per chi lo
|
|
153
|
+
// invoca a mano, e `LOOM_DECK_ENTER_PROMPT` non ha più effetto sugli spawn dal
|
|
154
|
+
// deck (non ha mai vinto su `--prompt`).
|
|
155
|
+
//
|
|
156
|
+
// Stringa VUOTA = richiesta esplicita di nessun prompt (azione `open`, o un
|
|
157
|
+
// campo svuotato a mano): si esprime col kind `none`, perché senza nessun flag
|
|
158
|
+
// `deck-run` cadrebbe sul proprio default `recap`.
|
|
159
|
+
export function deckArgs(id, sessionId, model = MODEL_DEFAULT, spawnNote, prompt = '') {
|
|
160
|
+
const promptArgs = prompt ? ['--prompt', prompt] : ['--prompt-kind', 'none'];
|
|
279
161
|
const args = [id, '--session-id', sessionId, ...promptArgs, '--model', model];
|
|
280
162
|
if (spawnNote)
|
|
281
163
|
args.push('--title-note', spawnNote);
|
|
282
164
|
return args;
|
|
283
165
|
}
|
|
284
|
-
export function spawnDeck(id, cwd, sessionId,
|
|
285
|
-
return launchDeckRun(deckArgs(id, sessionId,
|
|
166
|
+
export function spawnDeck(id, cwd, sessionId, model = MODEL_DEFAULT, spawnNote, prompt = '') {
|
|
167
|
+
return launchDeckRun(deckArgs(id, sessionId, model, spawnNote, prompt), cwd);
|
|
286
168
|
}
|
|
287
169
|
// T49 — resume di una sessione esistente come nuova tab Ptyxis. Scoped (taskId
|
|
288
170
|
// presente) → `deck-run <task> --resume <sid>`: la ripresa eredita LOOM_TASK +
|
|
@@ -578,12 +460,19 @@ export function spawnCleanTasks(ids, cwd, sessionId, ignored, onResult) {
|
|
|
578
460
|
// permissivo toglie le richieste di permesso che il CLI pone su un tool, e
|
|
579
461
|
// quelle in `-p` non hanno nessuno che risponda — il processo resterebbe appeso
|
|
580
462
|
// senza emettere niente, cioè con l'indicatore che conta all'infinito.
|
|
581
|
-
|
|
582
|
-
|
|
463
|
+
//
|
|
464
|
+
// T161 — modello e prompt arrivano dal CHIAMANTE, risolti sul catalogo delle
|
|
465
|
+
// azioni: erano gli ultimi due valori che questa funzione sceglieva da sé
|
|
466
|
+
// (`PROJECT_STATUS_MODEL` e la stringa della skill), e con l'override di
|
|
467
|
+
// progetto il default deve stare in un posto solo insieme a quello delle altre
|
|
468
|
+
// dieci azioni. Il `--permission-mode auto` invece resta qui: non è una
|
|
469
|
+
// preferenza ma la condizione perché un `-p` non resti appeso.
|
|
470
|
+
export function spawnProjectStatus(cwd, sessionId, onResult, model = PROJECT_STATUS_MODEL, prompt = '/loom-works:recap-status-project') {
|
|
471
|
+
return spawnSkill(prompt, cwd, sessionId, onResult, [
|
|
583
472
|
'--permission-mode',
|
|
584
473
|
'auto',
|
|
585
474
|
'--model',
|
|
586
|
-
|
|
475
|
+
model,
|
|
587
476
|
]);
|
|
588
477
|
}
|
|
589
478
|
// Comando della notifica desktop. L'override esiste per il collaudo del ramo
|
package/dist/task-index.js
CHANGED
|
@@ -37,6 +37,31 @@ export function appendNote(projectRoot, sessionId, note) {
|
|
|
37
37
|
export function appendPriority(projectRoot, sessionId, priority) {
|
|
38
38
|
appendSessionRecord(projectRoot, { sessionId, priority });
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* T162 — scrive i campi DERIVATI (`title`, `model`) di una conversazione.
|
|
42
|
+
*
|
|
43
|
+
* I due vanno in UN record e non in due, anche se il lettore risolve last-wins
|
|
44
|
+
* per campo e due append darebbero lo stesso esito finale: due record lasciano
|
|
45
|
+
* un istante in cui su disco lo stato è a metà, e in quel mezzo ci sta il
|
|
46
|
+
* lettore di compass — che mostrerebbe il titolo nuovo e riprenderebbe col
|
|
47
|
+
* modello vecchio. È la stessa regola di `writeSessionMarks` lato compass.
|
|
48
|
+
*
|
|
49
|
+
* Il record porta SOLO i campi passati: uno che portasse anche l'altro col
|
|
50
|
+
* valore che aveva prima lo riscriverebbe, e il last-wins trasformerebbe quella
|
|
51
|
+
* riscrittura in una sovrascrittura di ciò che un altro scrittore ha messo nel
|
|
52
|
+
* frattempo. Un insieme vuoto non scrive niente, invece di appendere un record
|
|
53
|
+
* col solo `sessionId` che nessun lettore userebbe.
|
|
54
|
+
*/
|
|
55
|
+
export function appendSessionMeta(projectRoot, sessionId, fields) {
|
|
56
|
+
const rec = { sessionId };
|
|
57
|
+
if (fields.title !== undefined)
|
|
58
|
+
rec.title = fields.title;
|
|
59
|
+
if (fields.model !== undefined)
|
|
60
|
+
rec.model = fields.model;
|
|
61
|
+
if (rec.title === undefined && rec.model === undefined)
|
|
62
|
+
return;
|
|
63
|
+
appendSessionRecord(projectRoot, rec);
|
|
64
|
+
}
|
|
40
65
|
// Una sola lettura del JSONL per entrambe le mappe: il deck poll-a l'indice a
|
|
41
66
|
// ogni tick, leggere il file due volte raddoppierebbe l'I/O per nulla.
|
|
42
67
|
// Last-wins per campo (un re-pin dello stesso sessionId sovrascrive), e i due
|
|
@@ -48,12 +73,14 @@ export function loadSessionIndex(projectRoot) {
|
|
|
48
73
|
const pinned = new Map();
|
|
49
74
|
const notes = new Map();
|
|
50
75
|
const priority = new Set();
|
|
76
|
+
const titles = new Map();
|
|
77
|
+
const models = new Map();
|
|
51
78
|
let content;
|
|
52
79
|
try {
|
|
53
80
|
content = readFileSync(taskIndexPath(projectRoot), 'utf8');
|
|
54
81
|
}
|
|
55
82
|
catch {
|
|
56
|
-
return { bindings, forkOf, pinned, notes, priority };
|
|
83
|
+
return { bindings, forkOf, pinned, notes, priority, titles, models };
|
|
57
84
|
}
|
|
58
85
|
let order = 0; // posizione crescente dei record pinned → rango di pin (D2)
|
|
59
86
|
for (const line of content.split('\n')) {
|
|
@@ -108,10 +135,32 @@ export function loadSessionIndex(projectRoot) {
|
|
|
108
135
|
else
|
|
109
136
|
priority.delete(d.sessionId);
|
|
110
137
|
}
|
|
138
|
+
// T162 — stesso last-wins per campo di `note`, con una differenza che
|
|
139
|
+
// conta: NESSUNA sanificazione in lettura. `note` la paga perché il deck
|
|
140
|
+
// la mette nel frame e il file è editabile a mano; qui il valore non
|
|
141
|
+
// entra nel frame (il deck il titolo lo ha dalla `Session`) e chi lo
|
|
142
|
+
// rende — compass, sotto Pango — non fa contabilità di colonne.
|
|
143
|
+
//
|
|
144
|
+
// Sanificare qui avrebbe un costo suo: il deck decide se riempire il
|
|
145
|
+
// buco CONFRONTANDO il valore letto con quello derivato, e un valore
|
|
146
|
+
// riscritto in lettura non combacerebbe mai col derivato — un append a
|
|
147
|
+
// ogni tick del poll, per sempre, senza nessun errore a dirlo.
|
|
148
|
+
if (typeof d.title === 'string') {
|
|
149
|
+
if (d.title)
|
|
150
|
+
titles.set(d.sessionId, d.title);
|
|
151
|
+
else
|
|
152
|
+
titles.delete(d.sessionId);
|
|
153
|
+
}
|
|
154
|
+
if (typeof d.model === 'string') {
|
|
155
|
+
if (d.model)
|
|
156
|
+
models.set(d.sessionId, d.model);
|
|
157
|
+
else
|
|
158
|
+
models.delete(d.sessionId);
|
|
159
|
+
}
|
|
111
160
|
}
|
|
112
161
|
catch {
|
|
113
162
|
// riga corrotta → skip
|
|
114
163
|
}
|
|
115
164
|
}
|
|
116
|
-
return { bindings, forkOf, pinned, notes, priority };
|
|
165
|
+
return { bindings, forkOf, pinned, notes, priority, titles, models };
|
|
117
166
|
}
|
package/dist/tasks.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync, readdirSync } from 'node:fs';
|
|
2
|
-
import { dirname, join } from 'node:path';
|
|
2
|
+
import { basename, dirname, join } from 'node:path';
|
|
3
3
|
import { sanitize } from './width.js';
|
|
4
4
|
/**
|
|
5
5
|
* Forma di un ID task: `T` + numero. FONTE UNICA del gate — `parseTasks` lo usa
|
|
@@ -108,6 +108,26 @@ export function loadTasks(path) {
|
|
|
108
108
|
// ID → path del task file: `<id>-<slug>.md` nella tasks dir. Il dash dopo l'ID
|
|
109
109
|
// disambigua i prefissi (`T20-` non matcha `T2-…`). Se più file matchano, primo
|
|
110
110
|
// in ordine. `null` se la dir non è leggibile o nessun file matcha.
|
|
111
|
+
/**
|
|
112
|
+
* T161 — lo SLUG di una task: il nome del suo file senza l'id e coi trattini
|
|
113
|
+
* sciolti in spazi, cioè il buco `{slug}` dei template di titolo.
|
|
114
|
+
*
|
|
115
|
+
* Viene dal NOME del file e non dalla descrizione in `tasks.md` per una ragione
|
|
116
|
+
* di alfabeto: il nome è già dentro quello di `_sane_note` per costruzione —
|
|
117
|
+
* minuscolo, separato da trattini, senza punteggiatura — mentre la descrizione
|
|
118
|
+
* porta apostrofi e `/`, che la riduzione toglie senza sostituto saldando le
|
|
119
|
+
* parole (`l'epica/doc` → `lepicadoc`).
|
|
120
|
+
*
|
|
121
|
+
* Stringa VUOTA quando il task file non si trova: chi interpola tratta un buco
|
|
122
|
+
* senza valore come «nessun titolo», invece di comporre un prefisso uguale per
|
|
123
|
+
* ogni task.
|
|
124
|
+
*/
|
|
125
|
+
export function taskSlug(tasksDir, id) {
|
|
126
|
+
const path = findTaskFile(tasksDir, id);
|
|
127
|
+
if (!path)
|
|
128
|
+
return '';
|
|
129
|
+
return basename(path, '.md').slice(id.length + 1).replace(/-/g, ' ');
|
|
130
|
+
}
|
|
111
131
|
export function findTaskFile(dir, id) {
|
|
112
132
|
let entries;
|
|
113
133
|
try {
|
package/dist/ui/detail-screen.js
CHANGED
|
@@ -6,7 +6,7 @@ import { cut } from '../width.js';
|
|
|
6
6
|
import { sliceLine } from '../text-search.js';
|
|
7
7
|
import { sliceSpans } from '../markdown.js';
|
|
8
8
|
import { ChoiceRow, FieldText, LABEL_W } from './fields.js';
|
|
9
|
-
import { DETAIL_ACTIONS, MODELS } from '../spawn.js';
|
|
9
|
+
import { DETAIL_ACTIONS, MODELS } from '../spawn-catalog.js';
|
|
10
10
|
import { DROW, PRIORITY_CHOICES } from '../overlays/sheet.js';
|
|
11
11
|
import { CARET, CARET_OFF, WARN } from '../glyphs.js';
|
|
12
12
|
/** Resa di ogni costrutto markdown (T75 · D4): un solo livello di enfasi per
|
package/dist/ui/preview.js
CHANGED
|
@@ -7,7 +7,7 @@ import { previewTextWidth, previewTitleParts } from '../layout.js';
|
|
|
7
7
|
import { LIVE_BUSY, LIVE_IDLE, META_KEYS, fmtDateTime, fmtSize, modelAlias, } from '../glyphs.js';
|
|
8
8
|
import { NATURA_SHORT, inboxMark } from '../inbox.js';
|
|
9
9
|
import { ChoiceRow } from './fields.js';
|
|
10
|
-
import { MODELS } from '../spawn.js';
|
|
10
|
+
import { MODELS } from '../spawn-catalog.js';
|
|
11
11
|
/** Colonne del prefisso delle due anteprime della preview sessione (`» `, `« `,
|
|
12
12
|
* e i due spazi delle righe di continuazione). Costante perché entra nel
|
|
13
13
|
* budget di wrap, che è il posto in cui dimenticarla non produce un errore ma
|