@lamemind/loom-deck 0.67.0 → 0.68.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -10
- package/dist/actions.js +79 -33
- package/dist/cli.js +51 -6
- package/dist/deck-model.js +3 -3
- package/dist/frame.js +4 -0
- package/dist/glyphs.js +5 -5
- package/dist/hooks.js +36 -0
- package/dist/inbox.js +6 -12
- package/dist/input-modes.js +1 -0
- package/dist/input.js +16 -0
- package/dist/overlays/sheet.js +24 -23
- package/dist/overlays/spawn.js +240 -0
- package/dist/overlays/status.js +2 -2
- package/dist/overlays/wrap.js +2 -2
- package/dist/prompt-catalog.js +1 -1
- package/dist/sane-note.js +115 -0
- package/dist/spawn-catalog.js +338 -0
- package/dist/spawn-config.js +280 -0
- package/dist/spawn.js +46 -157
- package/dist/tasks.js +21 -1
- package/dist/ui/detail-screen.js +1 -1
- package/dist/ui/preview.js +1 -1
- package/dist/ui/screens.js +12 -1
- package/dist/ui/spawn-screen.js +99 -0
- package/dist/viewport.js +35 -0
- package/dist/wrap-scan.js +14 -33
- package/package.json +1 -1
- package/scripts/deck-run +26 -10
- package/scripts/prompt-catalog +17 -3
package/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/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
|
package/dist/ui/screens.js
CHANGED
|
@@ -36,6 +36,7 @@ import { DetailScreen } from './detail-screen.js';
|
|
|
36
36
|
import { StatusScreen } from './status-screen.js';
|
|
37
37
|
import { InboxScreen } from './inbox-screen.js';
|
|
38
38
|
import { WrapScreen } from './wrap-screen.js';
|
|
39
|
+
import { SpawnScreen } from './spawn-screen.js';
|
|
39
40
|
import { ReaderScreen, SearchScreen } from './search-screen.js';
|
|
40
41
|
/**
|
|
41
42
|
* Il ripiego per un terminale troppo basso: una riga sola al posto della
|
|
@@ -107,7 +108,7 @@ export function HintBar({ mode, purge, keyLegend, indicators, columns, }) {
|
|
|
107
108
|
*/
|
|
108
109
|
export function screenFor(input) {
|
|
109
110
|
const { mode, rows, columns, note, overlays } = input;
|
|
110
|
-
const { assign, sheet, search, status, inbox, wrap } = overlays;
|
|
111
|
+
const { assign, sheet, search, status, inbox, wrap, spawn } = overlays;
|
|
111
112
|
// ── T57 · schermata di assegnazione ─────────────────────────────────────
|
|
112
113
|
// Sostitutiva come ricerca e reader (D3): la lista task non entra in un box
|
|
113
114
|
// sopra i due pane, e prendendo l'intero frame non costa nulla al loro budget.
|
|
@@ -185,6 +186,16 @@ export function screenFor(input) {
|
|
|
185
186
|
const start = Math.min(wrap.top, wrap.maxTop);
|
|
186
187
|
return (_jsx(WrapScreen, { files: wrap.files.slice(start, start + wrap.capacity), count: wrap.count, mixed: wrap.mixed, mtime: wrap.mtime, top: start, capacity: wrap.capacity, path: wrap.path, caret: wrap.caret, columns: columns }));
|
|
187
188
|
}
|
|
189
|
+
// ── T161 · pagina delle azioni di spawn ─────────────────────────────────
|
|
190
|
+
// Ottava schermata sostitutiva: undici righe per quattro colonne, più l'area
|
|
191
|
+
// di compilazione. In un box sopra i due pane al prompt resterebbero due
|
|
192
|
+
// colonne.
|
|
193
|
+
if (mode === 'spawn' && spawn.open) {
|
|
194
|
+
if (isCompact(spawn.capacity)) {
|
|
195
|
+
return _jsx(CompactNotice, { what: "azioni di spawn", esc: "chiude", rows: rows, columns: columns });
|
|
196
|
+
}
|
|
197
|
+
return (_jsx(SpawnScreen, { rows: spawn.table, sel: spawn.sel, top: Math.min(spawn.top, spawn.maxTop), capacity: spawn.capacity, configured: spawn.table.filter((r) => r.touched).length, dirty: spawn.dirty, editing: spawn.editing, editable: spawn.editable, fieldTitle: spawn.fieldTitle, fieldModel: spawn.fieldModel, fieldPrompt: spawn.fieldPrompt, cursor: spawn.cursor, example: spawn.example, columns: columns, note: note }));
|
|
198
|
+
}
|
|
188
199
|
// ── T52 · ricerca e reader ──────────────────────────────────────────────
|
|
189
200
|
// Gli unici modali che NON stanno in flusso sopra i pane: una lista di
|
|
190
201
|
// occorrenze non entra in un box da 4 righe. Prendono l'intero frame, quindi
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// T161 — la pagina delle azioni di spawn: la tabella e la sua area di
|
|
3
|
+
// compilazione.
|
|
4
|
+
import { Box, Text } from 'ink';
|
|
5
|
+
import { cut, pad, sanitize, termWidth } from '../width.js';
|
|
6
|
+
import { ChoiceRow, FieldText, LABEL_W } from './fields.js';
|
|
7
|
+
import { CARET, CARET_OFF } from '../glyphs.js';
|
|
8
|
+
import { interpolate, saneNote } from '../sane-note.js';
|
|
9
|
+
import { MODELS, SPAWN_FIELDS, spawnCell, } from '../spawn-catalog.js';
|
|
10
|
+
/**
|
|
11
|
+
* Il glifo della riga configurata.
|
|
12
|
+
*
|
|
13
|
+
* Non è riusato da nessun'altra superficie del deck: `●`/`◍` dicono la liveness
|
|
14
|
+
* di un processo, `📌`/`🔗`/`○` l'appartenenza di una conversazione, `✎` il
|
|
15
|
+
* titolo — un falso amico costa più di un simbolo nuovo. Concorde alle due
|
|
16
|
+
* contabilità di larghezza (`agrees('◆')`), quindi `sanitize` lo lascia passare
|
|
17
|
+
* invece di renderlo `·`.
|
|
18
|
+
*/
|
|
19
|
+
const TOUCHED = '◆';
|
|
20
|
+
/** Cella del glifo: si riempie di spazi anche quando è vuota, o le righe non
|
|
21
|
+
* configurate sposterebbero a sinistra tutto ciò che segue. */
|
|
22
|
+
const MARK_W = 2;
|
|
23
|
+
/** Colonna modello: il dominio è chiuso, e a decidere la larghezza non è la voce
|
|
24
|
+
* più lunga (`sonnet`, 6) ma l'INTESTAZIONE — `modello` ne vuole 7, e una
|
|
25
|
+
* testata di colonna troncata con l'ellissi si legge come un difetto. */
|
|
26
|
+
const MODEL_W = 7;
|
|
27
|
+
/** Tetto della colonna funzione: la label più lunga del catalogo (`drain di un
|
|
28
|
+
* file inbox`) sta esattamente dentro. Un tetto più alto le celle le
|
|
29
|
+
* toglierebbe al prompt, che è l'unico dei quattro valori senza una forma
|
|
30
|
+
* breve; più basso troncherebbe due label su undici già a piena larghezza. */
|
|
31
|
+
const FUNC_MAX = 22;
|
|
32
|
+
/** Le etichette delle righe dell'area, in italiano come ogni altra etichetta a
|
|
33
|
+
* schermo: la chiave del campo è un nome del codice e finisce nel file di
|
|
34
|
+
* progetto, non sotto gli occhi di chi compila. */
|
|
35
|
+
const FIELD_LABEL = {
|
|
36
|
+
title: 'titolo',
|
|
37
|
+
model: 'modello',
|
|
38
|
+
prompt: 'prompt',
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Le quattro colonne, per sottrazione come ogni lista del deck: le fisse sono
|
|
42
|
+
* note, e ciò che resta si divide fra titolo e prompt.
|
|
43
|
+
*
|
|
44
|
+
* Il prompt prende la fetta maggiore perché è l'unico dei quattro valori che non
|
|
45
|
+
* ha una forma breve: un titolo tagliato resta riconoscibile dal prefisso, un
|
|
46
|
+
* prompt tagliato a dieci colonne dice solo `/loom-works…`.
|
|
47
|
+
*
|
|
48
|
+
* Pavimento 0 su ogni cella: è un tetto, non una preferenza, e alzarlo sopra lo
|
|
49
|
+
* spazio reale fa uscire la riga dal box mangiandone il bordo (invariante ③ di
|
|
50
|
+
* `width.ts`).
|
|
51
|
+
*/
|
|
52
|
+
export function spawnColumns(width, rows) {
|
|
53
|
+
// `- 4` = bordo + padding del box tabella, che sta DENTRO quello esterno.
|
|
54
|
+
const avail = Math.max(0, width - 4);
|
|
55
|
+
const func = Math.min(FUNC_MAX, Math.max(0, ...rows.map((r) => termWidth(r.action.label))));
|
|
56
|
+
// I tre separatori fra le quattro colonne vanno contati insieme alle celle:
|
|
57
|
+
// sommare solo le celle è l'off-by-one che tronca la coda della riga.
|
|
58
|
+
const rest = Math.max(0, avail - CARET_OFF.length - MARK_W - func - MODEL_W - 3);
|
|
59
|
+
// Il titolo ha un TETTO oltre il quale non cresce: i template sono corti
|
|
60
|
+
// (`📊 {slug}`), quindi su un terminale largo ogni colonna in più gli
|
|
61
|
+
// resterebbe vuota mentre al prompt serve.
|
|
62
|
+
const title = Math.min(28, Math.floor(rest * 0.4));
|
|
63
|
+
return { func, title, model: MODEL_W, prompt: Math.max(0, rest - title) };
|
|
64
|
+
}
|
|
65
|
+
/** Una cella della tabella, col suo colore: il valore in chiaro, il segnaposto
|
|
66
|
+
* smorzato, l'override in ciano — l'origine si legge senza una legenda. */
|
|
67
|
+
function Cell({ row, field, value, width, }) {
|
|
68
|
+
const cell = spawnCell(row.action, field, value, row.raw.overridden.has(field));
|
|
69
|
+
const text = cut(cell.text, width);
|
|
70
|
+
return (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: cell.placeholder, color: cell.origin === 'override' && !cell.placeholder ? 'cyan' : undefined, children: text }), ' '.repeat(Math.max(0, width - termWidth(text)))] }));
|
|
71
|
+
}
|
|
72
|
+
export function SpawnScreen({ rows, sel, top, capacity, configured, dirty, editing, editable, fieldTitle, fieldModel, fieldPrompt, cursor, example, columns, note, }) {
|
|
73
|
+
const width = Math.max(20, (columns || 80) - 4);
|
|
74
|
+
const col = spawnColumns(width, rows);
|
|
75
|
+
const window = rows.slice(top, top + capacity);
|
|
76
|
+
const current = rows[sel];
|
|
77
|
+
/** La riga disegnata `field` è quella in fuoco? Il cursore indicizza le sole
|
|
78
|
+
* celle editabili, la resa le disegna tutte e tre. */
|
|
79
|
+
const focused = (field) => editing && editable[cursor.row] === field;
|
|
80
|
+
const textW = Math.max(10, width - LABEL_W - CARET_OFF.length);
|
|
81
|
+
// L'anteprima si fa sul titolo GIÀ ridotto, come lo farà `deck-run`: mostrare
|
|
82
|
+
// il template interpolato ma non ridotto prometterebbe un titolo che la tab
|
|
83
|
+
// non porta.
|
|
84
|
+
const rendered = saneNote(interpolate(fieldTitle, example) ?? '');
|
|
85
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: [_jsx(Text, { color: "cyan", children: "azioni di spawn" }), ' ', _jsxs(Text, { dimColor: true, children: ["\u00B7 ", rows.length, " azioni \u00B7 ", configured, " configurate"] }), dirty ? _jsx(Text, { color: "yellow", children: " \u00B7 modifiche non salvate" }) : null] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "yellow", children: "\u2191\u2193" }), " riga \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), ' ', editing ? 'conferma' : 'modifica', " \u00B7 ", _jsx(Text, { color: "yellow", children: "CANC" }), " torna al default \u00B7", ' ', _jsx(Text, { color: "yellow", children: "w" }), " salva \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), ' ', editing ? 'annulla' : 'chiude'] }), _jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, marginTop: 1, children: [_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [CARET_OFF, ' '.repeat(MARK_W), pad('funzione', col.func), " ", pad('titolo', col.title), " ", pad('modello', col.model), ' ', pad('prompt', col.prompt)] }), window.map((r, i) => {
|
|
86
|
+
const at = top + i;
|
|
87
|
+
return (_jsxs(Text, { wrap: "truncate-end", children: [at === sel ? CARET : CARET_OFF, _jsx(Text, { color: "cyan", children: pad(r.touched ? TOUCHED : '', MARK_W) }), _jsx(Text, { bold: at === sel, children: pad(cut(r.action.label, col.func), col.func) }), ' ', _jsx(Cell, { row: r, field: "title", value: r.raw.title, width: col.title }), ' ', _jsx(Cell, { row: r, field: "model", value: r.raw.model, width: col.model }), ' ', _jsx(Cell, { row: r, field: "prompt", value: r.raw.prompt, width: col.prompt })] }, r.action.id));
|
|
88
|
+
})] }), editing ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [SPAWN_FIELDS.map((field) => {
|
|
89
|
+
const fixed = current.action.fixed[field];
|
|
90
|
+
if (fixed !== undefined) {
|
|
91
|
+
return (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [CARET_OFF, FIELD_LABEL[field].padEnd(LABEL_W), "(", fixed, ")"] }, field));
|
|
92
|
+
}
|
|
93
|
+
if (field === 'model') {
|
|
94
|
+
return (_jsx(ChoiceRow, { label: "modello", values: MODELS, index: Math.max(0, MODELS.indexOf(fieldModel)), focused: focused('model'), width: width }, field));
|
|
95
|
+
}
|
|
96
|
+
const value = field === 'title' ? fieldTitle : fieldPrompt;
|
|
97
|
+
return (_jsxs(Text, { wrap: "truncate-end", children: [focused(field) ? CARET : CARET_OFF, _jsx(Text, { dimColor: true, children: FIELD_LABEL[field].padEnd(LABEL_W) }), _jsx(FieldText, { value: value, caret: cursor.caret, focused: focused(field), cols: textW })] }, field));
|
|
98
|
+
}), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [CARET_OFF, 'esempio'.padEnd(LABEL_W), current.action.title === null ? ('nessun titolo per questa azione') : (_jsx(Text, { color: rendered ? 'green' : undefined, children: rendered ? cut(rendered, textW) : 'titolo a vuoto: nessuna nota' }))] })] })) : null, note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: sanitize(note) }) : null] }));
|
|
99
|
+
}
|
package/dist/viewport.js
CHANGED
|
@@ -95,6 +95,10 @@ export const MODAL_HEIGHT = {
|
|
|
95
95
|
// 77 `WRAP` e 147 `misto`, cioè una lista che non entra in nessun box sopra i
|
|
96
96
|
// pane. Altezza da `wrapListCapacity`.
|
|
97
97
|
wrap: 0,
|
|
98
|
+
// T161 — la pagina delle azioni di spawn: ottava sostitutiva. Undici righe di
|
|
99
|
+
// tabella più l'area di compilazione: in un box sopra i pane resterebbero due
|
|
100
|
+
// colonne per il prompt. Altezza da `spawnListCapacity`.
|
|
101
|
+
spawn: 0,
|
|
98
102
|
};
|
|
99
103
|
/**
|
|
100
104
|
* Distribuisce le righe disponibili fra lista task, lista sessioni e preview.
|
|
@@ -335,6 +339,37 @@ const WRAP_CHROME = 12;
|
|
|
335
339
|
export function wrapListCapacity(rows) {
|
|
336
340
|
return Math.max(0, (rows || 24) - SLACK - WRAP_CHROME);
|
|
337
341
|
}
|
|
342
|
+
// T161 — cornice della pagina delle azioni di spawn.
|
|
343
|
+
// 2 bordi del box esterno
|
|
344
|
+
// 1 riga di testata (nome della pagina · quante righe · quante configurate)
|
|
345
|
+
// 1 riga hint
|
|
346
|
+
// 1 marginTop del box tabella
|
|
347
|
+
// 2 bordi del box tabella
|
|
348
|
+
// 1 riga di intestazione delle colonne
|
|
349
|
+
const SPAWN_CHROME = 8;
|
|
350
|
+
// L'area di compilazione: marginTop + le TRE righe (titolo, modello, prompt) +
|
|
351
|
+
// la riga dell'anteprima resa.
|
|
352
|
+
//
|
|
353
|
+
// Fisse e non proporzionali alle celle editabili della riga aperta: su `bare`,
|
|
354
|
+
// che ha il solo modello, le altre due si disegnano comunque per dire perché non
|
|
355
|
+
// si modificano. Un'altezza che dipendesse dalla riga selezionata farebbe
|
|
356
|
+
// saltare la tabella di due righe a ogni movimento del caret, e il budget
|
|
357
|
+
// dovrebbe sapere quale riga è aperta per non sottostimare — lo stesso motivo
|
|
358
|
+
// per cui le cinque righe del detail sono dentro `DETAIL_CHROME` invece che
|
|
359
|
+
// condizionali (T111 · D1).
|
|
360
|
+
const SPAWN_FIELDS_CHROME = 5;
|
|
361
|
+
/**
|
|
362
|
+
* Righe di tabella che entrano nel terminale.
|
|
363
|
+
*
|
|
364
|
+
* Le righe dell'area aperta e la riga di stato si pagano QUI, non altrove: la
|
|
365
|
+
* pagina è una schermata sostitutiva, quindi la nota che le sue azioni scrivono
|
|
366
|
+
* la disegna lei — a differenza della lista, dove la riga di stato sta fuori dai
|
|
367
|
+
* pane e il suo costo è già in `layoutBudget`.
|
|
368
|
+
*/
|
|
369
|
+
export function spawnListCapacity(rows, editing = false, noteLine = false) {
|
|
370
|
+
const extra = (editing ? SPAWN_FIELDS_CHROME : 0) + (noteLine ? 1 : 0);
|
|
371
|
+
return Math.max(0, (rows || 24) - SLACK - SPAWN_CHROME - extra);
|
|
372
|
+
}
|
|
338
373
|
/**
|
|
339
374
|
* Righe scorse da una pressione di PagGiù/PagSu: METÀ della capienza visibile.
|
|
340
375
|
*
|
package/dist/wrap-scan.js
CHANGED
|
@@ -84,48 +84,29 @@ export function mixedCount(files) {
|
|
|
84
84
|
/** Radice di default dell'area di srotolamento: la project root intera (D4). */
|
|
85
85
|
export const WRAP_DEFAULT_PATH = '.';
|
|
86
86
|
/**
|
|
87
|
-
*
|
|
87
|
+
* T161 — il buco `{path}` del titolo e del prompt di una sessione di
|
|
88
|
+
* srotolamento: il path reso a parole.
|
|
88
89
|
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* innocuo — e nessuna delle tre è una preferenza da esporre.
|
|
94
|
-
*
|
|
95
|
-
* Il `<path>` è l'unico buco. Arriva dal campo della lista e finisce dentro
|
|
96
|
-
* `--prompt` come argv singolo, quindi non passa da nessuna shell prima di
|
|
97
|
-
* `deck-run`, che lo quota ad apici singoli: non c'è quoting da fare qui.
|
|
98
|
-
*/
|
|
99
|
-
export function wrapPrompt(path) {
|
|
100
|
-
const target = path.trim() || WRAP_DEFAULT_PATH;
|
|
101
|
-
return (`lancia md-wrap modo apply su ${target} e backup in cartella tmp dedicata. ` +
|
|
102
|
-
'verifica risultato e fai rapporto. se le modifiche sono tutte chiaramente safe, ' +
|
|
103
|
-
'puoi committare direttamente');
|
|
104
|
-
}
|
|
105
|
-
/** Prefisso del titolo di una sessione di srotolamento. Astrale, come le altre
|
|
106
|
-
* emoji che finiscono in un titolo: le BMP `sanitize` (`src/width.ts`) le rende
|
|
107
|
-
* `·`. Va tenuta allineata alla whitelist di `_sane_note` in `deck-run`. */
|
|
108
|
-
const WRAP_EMOJI = '📏';
|
|
109
|
-
/**
|
|
110
|
-
* Il titolo della sessione che srotola un path: `📏 <path a parole>`.
|
|
111
|
-
*
|
|
112
|
-
* Le barre diventano spazi e l'estensione cade PRIMA di `_sane_note`, il cui
|
|
113
|
-
* alfabeto non ha né `/` né `.`: lasciandoceli cadere le parole si salderebbero
|
|
114
|
-
* (`runtimereferencedoc-systemmd`), e il titolo della tab divergerebbe da quello
|
|
115
|
-
* che la lista del deck legge dal sidecar.
|
|
90
|
+
* Le barre diventano spazi e l'estensione cade QUI, prima di `_sane_note`, il
|
|
91
|
+
* cui alfabeto non ha né `/` né `.`: lasciandoceli cadere le parole si
|
|
92
|
+
* salderebbero (`runtimereferencedoc-systemmd`), e il titolo della tab
|
|
93
|
+
* divergerebbe da quello che la lista del deck legge dal sidecar.
|
|
116
94
|
*
|
|
117
95
|
* Il path di default (`.`, la project root intera) non ha parole da mostrare e
|
|
118
|
-
* diventa una frase:
|
|
96
|
+
* diventa una frase: interpolato in `📏 {path}` darebbe il solo prefisso, cioè
|
|
97
|
+
* un titolo uguale per ogni srotolamento.
|
|
98
|
+
*
|
|
99
|
+
* L'emoji del prefisso non sta più qui: è dentro il template della riga
|
|
100
|
+
* `unwrap` del catalogo delle azioni, dove il progetto può riscriverla.
|
|
119
101
|
*/
|
|
120
|
-
export function
|
|
102
|
+
export function wrapWords(path) {
|
|
121
103
|
const target = path.trim() || WRAP_DEFAULT_PATH;
|
|
122
104
|
if (target === WRAP_DEFAULT_PATH)
|
|
123
|
-
return
|
|
124
|
-
|
|
105
|
+
return 'tutto il progetto';
|
|
106
|
+
return target
|
|
125
107
|
.replace(/\.md$/i, '')
|
|
126
108
|
.replace(/[./]+/g, ' ')
|
|
127
109
|
.trim();
|
|
128
|
-
return `${WRAP_EMOJI} ${words}`;
|
|
129
110
|
}
|
|
130
111
|
/**
|
|
131
112
|
* Cartella della cache, una per utente e con `mode 0700` — stessa ragione del
|
package/package.json
CHANGED
package/scripts/deck-run
CHANGED
|
@@ -56,6 +56,9 @@
|
|
|
56
56
|
# LOOM_DECK_PROFILE_UUID UUID profilo Ptyxis (default: cc-host)
|
|
57
57
|
# LOOM_DECK_WORKDIR dir di lavoro della tab (default: $PWD)
|
|
58
58
|
# LOOM_DECK_ENTER_PROMPT override del prompt iniziale, {TASK}=TaskID (vince sul kind, tranne `none`)
|
|
59
|
+
# T161: inerte sugli spawn che arrivano dal deck TUI —
|
|
60
|
+
# quelli passano SEMPRE `--prompt`, su cui non ha mai
|
|
61
|
+
# vinto. Resta per le invocazioni a mano e per i test.
|
|
59
62
|
# LOOM_DECK_INTAB_CMD override comando in-tab (default: la sessione CC; usato per i test)
|
|
60
63
|
# LOOM_DECK_STATE_PROFILE PTYXIS_PROFILE annunciata a compass (default: bindings/claude
|
|
61
64
|
# del progetto, letto da dconf; settata a vuoto = nessun annuncio)
|
|
@@ -272,17 +275,22 @@ WORKDIR="${LOOM_DECK_WORKDIR:-$PWD}"
|
|
|
272
275
|
# italiane, spazio, trattino, underscore e sei emoji (sotto); tutto il resto
|
|
273
276
|
# (apici, backslash, `$`, backtick, `;`, quadre, ogni altra emoji…) cade.
|
|
274
277
|
#
|
|
275
|
-
# T156 — le emoji entrano per il PREFISSO del titolo
|
|
276
|
-
#
|
|
277
|
-
#
|
|
278
|
-
#
|
|
279
|
-
# stessa conversazione portava due titoli diversi, senza errore. Le quadre che
|
|
278
|
+
# T156 — le emoji entrano per il PREFISSO del titolo che il deck compone
|
|
279
|
+
# (`🚀 slug`). Senza di loro il prefisso spariva qui e restava nella lista del
|
|
280
|
+
# deck, che riceve la nota grezza dal sidecar `session-tasks.jsonl`: la stessa
|
|
281
|
+
# conversazione portava due titoli diversi, senza errore. Le quadre che
|
|
280
282
|
# delimitavano il prefisso sono cadute col prefisso stesso: l'emoji da sola si
|
|
281
283
|
# stacca già dallo slug.
|
|
282
|
-
#
|
|
283
|
-
#
|
|
284
|
-
#
|
|
285
|
-
#
|
|
284
|
+
# T161 — le sei stanno tutte nei TEMPLATE di titolo del catalogo delle azioni
|
|
285
|
+
# (`SPAWN_ACTIONS` in `src/spawn-catalog.ts`), che il progetto può riscrivere:
|
|
286
|
+
# quattro nominano un'azione su una task (📐🚀📊🏁), 🧹 il file inbox che si
|
|
287
|
+
# drena, 📏 il path che si srotola.
|
|
288
|
+
# Questa whitelist ha un GEMELLO TypeScript (`saneNote` in `src/sane-note.ts`),
|
|
289
|
+
# che riduce il template al salvataggio perché quello che sta nel file di
|
|
290
|
+
# progetto sia quello che la tab mostrerà. I due lati non si leggono a vicenda e
|
|
291
|
+
# vanno cambiati insieme: il custode è il gate di `test/deck-run.test.ts`, che
|
|
292
|
+
# passa la stessa batteria di input a questa funzione e al gemello e pretende
|
|
293
|
+
# l'uguaglianza.
|
|
286
294
|
# La whitelist le nomina una per una e NON apre un intervallo di code point. Due
|
|
287
295
|
# ragioni: un'emoji arriva nella tab solo se il deck la mostra identica in lista,
|
|
288
296
|
# cioè se `sanitize` (`src/width.ts`) la lascia passare invece di renderla `·`, e
|
|
@@ -386,7 +394,15 @@ MODE_FLAG="--permission-mode ${PERM_MODE} "
|
|
|
386
394
|
|
|
387
395
|
# ── modello (T108 + T152) ────────────────────────────────────────────────────
|
|
388
396
|
# Precedenza: --model > env LOOM_DECK_MODEL > modello del kind nel catalogo >
|
|
389
|
-
# 'fable'.
|
|
397
|
+
# 'fable'.
|
|
398
|
+
#
|
|
399
|
+
# T161 — questa cascata vale per chi invoca deck-run A MANO. Il deck TUI passa
|
|
400
|
+
# SEMPRE `--model` già risolto (default del deck ← override di progetto nel
|
|
401
|
+
# blocco `spawn` di `.claude/loom-works.json`), quindi dal deck si ferma al primo
|
|
402
|
+
# gradino e i tre sotto non intervengono mai. Il blocco di progetto qui non si
|
|
403
|
+
# legge di proposito: `jq` è dipendenza opzionale di questo script, e dargli un
|
|
404
|
+
# JSON da consultare la renderebbe obbligatoria su un percorso che oggi degrada
|
|
405
|
+
# senza. L'ultimo gradino è il gemello bash di `MODEL_DEFAULT` in spawn.ts e
|
|
390
406
|
# va tenuto allineato a mano: i due lati non si leggono a vicenda (il deck deve
|
|
391
407
|
# poter MOSTRARE la selezione iniziale prima che deck-run esista come processo),
|
|
392
408
|
# quindi un cambio qui senza l'altro fa partire dalla lista un modello diverso
|