@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/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,169 +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 sono anche l'unica classe di emoji che `_sane_note` di
|
|
209
|
-
// `deck-run` lascia passare nel titolo della tab: la whitelist è una lista
|
|
210
|
-
// chiusa che nomina questi code point (P2 preflight), quindi cambiare una voce
|
|
211
|
-
// qui e non là riapre la divergenza fra titolo in lista e titolo in tab.
|
|
212
|
-
// Le quadre che delimitavano il prefisso sono cadute: l'emoji da sola si stacca
|
|
213
|
-
// già dallo slug, e con lei sono uscite dalla whitelist di `_sane_note`.
|
|
214
|
-
const ACTION_EMOJI = {
|
|
215
|
-
preflight: '📐',
|
|
216
|
-
run: '🚀',
|
|
217
|
-
recap: '📊',
|
|
218
|
-
'recap-task': '📊',
|
|
219
|
-
'recap-epic': '📊',
|
|
220
|
-
checkpoint: '🏁',
|
|
221
|
-
};
|
|
222
|
-
/**
|
|
223
|
-
* Titolo di fallback per una conversazione con nota vuota: `{emoji} {slug}`,
|
|
224
|
-
* o il solo slug per `none` (D1/D2 preflight). Lo slug viene dal NOME del
|
|
225
|
-
* task file (`findTaskFile`), non dalla descrizione di `tasks.md`: il nome è
|
|
226
|
-
* già dentro l'alfabeto di `_sane_note` per costruzione — minuscolo, separato
|
|
227
|
-
* da trattini, senza punteggiatura — mentre la descrizione porta apostrofi e
|
|
228
|
-
* `/` che la riduzione toglie senza sostituto, saldando le parole (D1
|
|
229
|
-
* razionale).
|
|
230
|
-
*
|
|
231
|
-
* Il PREFISSO invece nell'alfabeto non ci sta per costruzione: l'emoji ci entra
|
|
232
|
-
* solo perché la whitelist di `_sane_note` le nomina una per una (T156). È
|
|
233
|
-
* l'unica parte del titolo di fallback che dipende da una modifica fatta
|
|
234
|
-
* dall'altro lato.
|
|
235
|
-
*
|
|
236
|
-
* `null` quando il task file non si trova: il chiamante decide se ripiegare
|
|
237
|
-
* su nota vuota o su un altro fallback.
|
|
238
|
-
*/
|
|
239
|
-
export function fallbackTitle(tasksDir, id, kind) {
|
|
240
|
-
const path = findTaskFile(tasksDir, id);
|
|
241
|
-
if (!path)
|
|
242
|
-
return null;
|
|
243
|
-
const slug = basename(path, '.md').slice(id.length + 1).replace(/-/g, ' ');
|
|
244
|
-
const emoji = ACTION_EMOJI[kind];
|
|
245
|
-
return emoji ? `${emoji} ${slug}` : slug;
|
|
246
|
-
}
|
|
247
124
|
// Spawn detached: il deck spawna ma NON contiene la sessione (la possiede
|
|
248
125
|
// ptyxis-agent). unref + stdio ignore → ritorna subito, la TUI resta viva.
|
|
249
126
|
// sessionId pinnato (T27) → il binding sidecar è deterministico allo spawn.
|
|
250
|
-
// Il
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
// Il modello, al contrario del kind, ha un default (T108): i percorsi che non
|
|
254
|
-
// passano dal selettore del detail non devono nominarlo per forza, e l'unico
|
|
255
|
-
// 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.
|
|
256
130
|
// T111 — `spawnNote` è la nota data alla NASCITA della conversazione, dal campo
|
|
257
|
-
// 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
|
|
258
136
|
// suffisso `«nota»` viene appeso a `TITLE` dentro deck-run PRIMA che i rami si
|
|
259
137
|
// separino, quindi il flag non appartiene alla ripresa — vale su ogni spawn.
|
|
260
138
|
// Assente quando la nota è vuota, e non passato vuoto: `--title-note ''`
|
|
261
139
|
// produrrebbe un `«»` a vuoto nel titolo.
|
|
262
|
-
// T117 — `prompt` è il TESTO letterale, e
|
|
263
|
-
// `--prompt
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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'];
|
|
277
161
|
const args = [id, '--session-id', sessionId, ...promptArgs, '--model', model];
|
|
278
162
|
if (spawnNote)
|
|
279
163
|
args.push('--title-note', spawnNote);
|
|
280
164
|
return args;
|
|
281
165
|
}
|
|
282
|
-
export function spawnDeck(id, cwd, sessionId,
|
|
283
|
-
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);
|
|
284
168
|
}
|
|
285
169
|
// T49 — resume di una sessione esistente come nuova tab Ptyxis. Scoped (taskId
|
|
286
170
|
// presente) → `deck-run <task> --resume <sid>`: la ripresa eredita LOOM_TASK +
|
|
@@ -399,9 +283,30 @@ export function spawnClaudeEmpty(cwd, model) {
|
|
|
399
283
|
* Il modello è passato SEMPRE, anche sul default, per la stessa ragione di
|
|
400
284
|
* `permissionMode`: lo spawn resta deterministico e leggibile nel process tree
|
|
401
285
|
* invece di dipendere da un default che può cambiare fra versioni.
|
|
286
|
+
*
|
|
287
|
+
* `sessionId` e `title` viaggiano INSIEME o non viaggiano: il titolo lo mostrano
|
|
288
|
+
* due superfici — la tab Ptyxis (via `--title-note`) e la riga della lista
|
|
289
|
+
* sessioni (via la nota nel sidecar, che è keyed sul sessionId) — e senza un id
|
|
290
|
+
* pinnato la seconda non ha una chiave su cui scrivere. Pinnarlo è quindi ciò
|
|
291
|
+
* che rende il titolo visibile in entrambe, non un binding task: una sessione
|
|
292
|
+
* `--no-task` non ne ha uno, ma un NOME ce l'ha.
|
|
293
|
+
*
|
|
294
|
+
* Il titolo sta in TESTA all'argv, prima del prompt: la riga di stato del deck
|
|
295
|
+
* taglia il comando al mezzo, e il prompt (specialmente quello cablato dello
|
|
296
|
+
* srotolamento) è l'argomento più lungo — in coda al titolo lo spingerebbe
|
|
297
|
+
* fuori vista. Stessa ragione dell'ordine di `resumeArgs`.
|
|
402
298
|
*/
|
|
403
|
-
export function
|
|
404
|
-
|
|
299
|
+
export function bareArgs(prompt, model, sessionId, title) {
|
|
300
|
+
const args = ['--no-task'];
|
|
301
|
+
if (title)
|
|
302
|
+
args.push('--title-note', title);
|
|
303
|
+
if (sessionId)
|
|
304
|
+
args.push('--session-id', sessionId);
|
|
305
|
+
args.push('--prompt', prompt, '--model', model);
|
|
306
|
+
return args;
|
|
307
|
+
}
|
|
308
|
+
export function spawnBare(cwd, prompt, model, sessionId, title) {
|
|
309
|
+
return launchDeckRun(bareArgs(prompt, model, sessionId, title), cwd);
|
|
405
310
|
}
|
|
406
311
|
// T39/T32: voce `launch` custom del file config, eseguita con cwd = project root.
|
|
407
312
|
// Spawn detached come spawnDeck: il deck lancia ma non possiede il processo.
|
|
@@ -555,12 +460,19 @@ export function spawnCleanTasks(ids, cwd, sessionId, ignored, onResult) {
|
|
|
555
460
|
// permissivo toglie le richieste di permesso che il CLI pone su un tool, e
|
|
556
461
|
// quelle in `-p` non hanno nessuno che risponda — il processo resterebbe appeso
|
|
557
462
|
// senza emettere niente, cioè con l'indicatore che conta all'infinito.
|
|
558
|
-
|
|
559
|
-
|
|
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, [
|
|
560
472
|
'--permission-mode',
|
|
561
473
|
'auto',
|
|
562
474
|
'--model',
|
|
563
|
-
|
|
475
|
+
model,
|
|
564
476
|
]);
|
|
565
477
|
}
|
|
566
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,23 +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.
|
|
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.
|
|
94
94
|
*
|
|
95
|
-
* Il
|
|
96
|
-
*
|
|
97
|
-
*
|
|
95
|
+
* Il path di default (`.`, la project root intera) non ha parole da mostrare e
|
|
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.
|
|
98
101
|
*/
|
|
99
|
-
export function
|
|
102
|
+
export function wrapWords(path) {
|
|
100
103
|
const target = path.trim() || WRAP_DEFAULT_PATH;
|
|
101
|
-
|
|
102
|
-
'
|
|
103
|
-
|
|
104
|
+
if (target === WRAP_DEFAULT_PATH)
|
|
105
|
+
return 'tutto il progetto';
|
|
106
|
+
return target
|
|
107
|
+
.replace(/\.md$/i, '')
|
|
108
|
+
.replace(/[./]+/g, ' ')
|
|
109
|
+
.trim();
|
|
104
110
|
}
|
|
105
111
|
/**
|
|
106
112
|
* Cartella della cache, una per utente e con `mode 0700` — stessa ragione del
|
package/package.json
CHANGED