@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,115 @@
|
|
|
1
|
+
// T161 — il gemello TypeScript di `_sane_note`, la riduzione che `deck-run`
|
|
2
|
+
// applica a ogni nota del titolo.
|
|
3
|
+
//
|
|
4
|
+
// Perché serve un gemello. La nota del titolo è l'UNICO ingresso di testo libero
|
|
5
|
+
// nel titolo di una tab, quindi `deck-run` non la quota: la RIDUCE a una
|
|
6
|
+
// whitelist chiusa di code point, e tutto ciò che non ci rientra sparisce senza
|
|
7
|
+
// errore. Finché la nota nasceva da un campo scritto al volo, la divergenza fra
|
|
8
|
+
// quello che si legge nel deck e quello che compare nella tab durava il tempo di
|
|
9
|
+
// uno spawn. Da quando un TEMPLATE del titolo si scrive in una pagina e si salva
|
|
10
|
+
// su disco, quella divergenza diventa permanente: il file direbbe una cosa e la
|
|
11
|
+
// tab ne mostrerebbe un'altra, per sempre e senza un errore da nessuna parte.
|
|
12
|
+
//
|
|
13
|
+
// La riduzione si applica quindi QUI, al confine — al salvataggio e alla
|
|
14
|
+
// lettura del blocco `spawn` — così quello che sta nel file è quello che la tab
|
|
15
|
+
// mostrerà.
|
|
16
|
+
//
|
|
17
|
+
// L'ALLINEAMENTO fra i due lati non è assunto: `test/sane-note.test.ts` passa la
|
|
18
|
+
// stessa batteria di input a questa funzione e a `deck-run` vero (via lo shim
|
|
19
|
+
// `ptyxis` di `deck-run.test.ts`) e pretende l'uguaglianza. La whitelist non si
|
|
20
|
+
// allarga — e se un giorno lo facesse, sono tre le sedi da toccare insieme
|
|
21
|
+
// (`deck-run`, questo file, le emoji dei titoli in `spawn-catalog.ts`), col test
|
|
22
|
+
// che fallisce se una resta indietro.
|
|
23
|
+
/**
|
|
24
|
+
* L'alfabeto ammesso, negato: lettere, cifre, spazio, `_`, `-`, le accentate
|
|
25
|
+
* italiane e sei emoji.
|
|
26
|
+
*
|
|
27
|
+
* Whitelist e non blacklist, come in bash: il titolo finisce dentro apici
|
|
28
|
+
* singoli in `bash -lc "claude --name '…'"`, ed enumerare i caratteri pericolosi
|
|
29
|
+
* significa sbagliarne uno.
|
|
30
|
+
*
|
|
31
|
+
* Le sei emoji sono nominate una per una e NON come intervallo di code point,
|
|
32
|
+
* per due ragioni che valgono su entrambi i lati: un'emoji arriva nella tab solo
|
|
33
|
+
* se il deck la mostra identica in lista, cioè se `sanitize` (`src/width.ts`) la
|
|
34
|
+
* lascia passare invece di renderla `·`, e quel verdetto si verifica un glifo
|
|
35
|
+
* alla volta; e l'ordine di un intervallo dentro un bracket expression segue la
|
|
36
|
+
* collazione della locale, non i code point.
|
|
37
|
+
*
|
|
38
|
+
* Il flag `u` non è cosmetico: senza, le quattro emoji astrali entrerebbero
|
|
39
|
+
* nella classe come coppie di surrogati sciolti, e la riduzione lascerebbe
|
|
40
|
+
* passare mezzo carattere di qualunque altra emoji che ne condivida il primo.
|
|
41
|
+
*/
|
|
42
|
+
const OUT_OF_ALPHABET = /[^A-Za-z0-9 _àèéìòùÀÈÉÌÒÙ📐🚀📊🏁🧹📏-]/gu;
|
|
43
|
+
/** Cap in CARATTERI (code point), non in byte: a byte un taglio a metà di una
|
|
44
|
+
* `à` lascerebbe UTF-8 rotto nel titolo. Largo di proposito — una tab che sfora
|
|
45
|
+
* tronca a destra da sola, e il limite serve solo a non spingere fuori vista la
|
|
46
|
+
* parte del titolo che compass matcha. */
|
|
47
|
+
export const SANE_CAP = 60;
|
|
48
|
+
/** Un buco di template: `{TASK}`, `{slug}`, `{file}`, `{path}`. La cattura nel
|
|
49
|
+
* gruppo è ciò che fa comparire i buchi nei dispari di `split`. */
|
|
50
|
+
const HOLE = /(\{[A-Za-z]+\})/;
|
|
51
|
+
/** Spazi collassati, estremi potati, cap: i tre passi che in bash sono `tr -s`,
|
|
52
|
+
* i due `${s#/%}` e `${s:0:60}`. */
|
|
53
|
+
function tidy(s) {
|
|
54
|
+
const squeezed = s.replace(/ +/g, ' ');
|
|
55
|
+
const trimmed = squeezed.replace(/^ /, '').replace(/ $/, '');
|
|
56
|
+
return [...trimmed].slice(0, SANE_CAP).join('');
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* La nota ridotta, esattamente come la ridurrebbe `deck-run`.
|
|
60
|
+
*
|
|
61
|
+
* Si applica al titolo GIÀ interpolato, cioè al testo che finisce nel sidecar e
|
|
62
|
+
* in `--title-note`: le due superfici che mostrano quel nome — la riga di lista
|
|
63
|
+
* del deck, che legge la nota grezza dal sidecar, e la tab Ptyxis — devono
|
|
64
|
+
* vedere la stessa stringa.
|
|
65
|
+
*/
|
|
66
|
+
export function saneNote(raw) {
|
|
67
|
+
return tidy(raw.replace(OUT_OF_ALPHABET, ''));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* La stessa riduzione, coi BUCHI preservati.
|
|
71
|
+
*
|
|
72
|
+
* Le graffe non sono nell'alfabeto: passare un template da `saneNote` lo
|
|
73
|
+
* ridurrebbe a `📐 slug`, cioè a un titolo che nomina la parola «slug» invece di
|
|
74
|
+
* interpolare il nome della task. Il template si riduce comunque — è testo
|
|
75
|
+
* scritto a mano, e il resto della riga deve sopravvivere alla tab — ma i buchi
|
|
76
|
+
* attraversano intatti e vengono riempiti dopo.
|
|
77
|
+
*
|
|
78
|
+
* Fuori dai buchi il comportamento è identico a `saneNote`, compresi collasso
|
|
79
|
+
* degli spazi, potatura e cap: un template senza buchi passa dalle stesse regole
|
|
80
|
+
* di una nota qualunque.
|
|
81
|
+
*/
|
|
82
|
+
export function saneTemplate(raw) {
|
|
83
|
+
return tidy(raw
|
|
84
|
+
.split(HOLE)
|
|
85
|
+
.map((part, i) => (i % 2 === 1 ? part : part.replace(OUT_OF_ALPHABET, '')))
|
|
86
|
+
.join(''));
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Riempie i buchi di un template. `null` quando un buco nominato non ha un
|
|
90
|
+
* valore da mettere.
|
|
91
|
+
*
|
|
92
|
+
* Il `null` non è difensivo, è il comportamento: un titolo `📐 {slug}` su una
|
|
93
|
+
* task il cui file non si trova diventerebbe `📐` — un prefisso solo, uguale per
|
|
94
|
+
* ogni task, che è peggio di nessun titolo. Chi chiama riceve `null` e apre la
|
|
95
|
+
* conversazione senza nota, che è ciò che faceva il vecchio `fallbackTitle`
|
|
96
|
+
* quando il task file mancava.
|
|
97
|
+
*
|
|
98
|
+
* I buchi non dichiarati dall'azione restano LETTERALI e non fanno fallire
|
|
99
|
+
* l'interpolazione: non sono un valore mancante, sono testo che l'utente ha
|
|
100
|
+
* scritto — e il giro di riduzione dopo l'interpolazione toglierà le graffe.
|
|
101
|
+
*/
|
|
102
|
+
export function interpolate(template, values) {
|
|
103
|
+
let missing = false;
|
|
104
|
+
const filled = template.replace(/\{([A-Za-z]+)\}/g, (whole, name) => {
|
|
105
|
+
if (!(name in values))
|
|
106
|
+
return whole;
|
|
107
|
+
const v = values[name];
|
|
108
|
+
if (!v) {
|
|
109
|
+
missing = true;
|
|
110
|
+
return '';
|
|
111
|
+
}
|
|
112
|
+
return v;
|
|
113
|
+
});
|
|
114
|
+
return missing ? null : filled;
|
|
115
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// T162 — i campi DERIVATI del sidecar: cosa scrivere in `title`/`model` e
|
|
2
|
+
// quando scriverlo.
|
|
3
|
+
//
|
|
4
|
+
// Esiste perché compass ha bisogno del titolo di una conversazione e non può
|
|
5
|
+
// leggerne il transcript: l'estensione gira dentro il processo di gnome-shell,
|
|
6
|
+
// e una lettura sincrona di qualche megabyte blocca il compositore. Il dato
|
|
7
|
+
// deve quindi arrivargli da un file piccolo che già apre — il sidecar — messo
|
|
8
|
+
// lì da chi ce l'ha in mano, cioè il deck, che a ogni tick materializza le
|
|
9
|
+
// `Session` complete di titolo e modello.
|
|
10
|
+
//
|
|
11
|
+
// Modulo PURO come `session-list.ts`: nessun I/O, nessun import da ink/react.
|
|
12
|
+
// Decide COSA appendere e ritorna i record; il chiamante li scrive. La
|
|
13
|
+
// separazione è ciò che rende testabile la parte non ovvia — la convergenza:
|
|
14
|
+
// un derivato che non combacia mai col valore su disco produrrebbe un append a
|
|
15
|
+
// ogni tick del poll, per sempre, senza nessun errore a dirlo.
|
|
16
|
+
import { modelAlias } from './glyphs.js';
|
|
17
|
+
import { stripProjectCore, stripTaskId } from './session-list.js';
|
|
18
|
+
import { NO_TITLE } from './sessions.js';
|
|
19
|
+
/**
|
|
20
|
+
* Cap del titolo scritto nel sidecar, in CODE POINT.
|
|
21
|
+
*
|
|
22
|
+
* È il `LABEL_MAX` del lettore (compass): oltre, Pango ellissizza comunque, e
|
|
23
|
+
* il file non deve portare una stringa che nessuno mostrerà per intero. Misura
|
|
24
|
+
* del preflight: col primo prompt come titolo il massimo osservato era 1.367
|
|
25
|
+
* caratteri, 36 conversazioni su 604 sopra 120, p90 a 61 — il cap morde su una
|
|
26
|
+
* minoranza e non tocca il caso normale.
|
|
27
|
+
*
|
|
28
|
+
* In code point e non in code unit perché il taglio va fatto senza spezzare una
|
|
29
|
+
* coppia surrogata: un'emoji tagliata a metà lascia nel file un surrogato
|
|
30
|
+
* spaiato, cioè un glifo rotto nel menu.
|
|
31
|
+
*/
|
|
32
|
+
export const SIDECAR_TITLE_MAX = 120;
|
|
33
|
+
function capTitle(s) {
|
|
34
|
+
const cps = [...s];
|
|
35
|
+
return cps.length <= SIDECAR_TITLE_MAX ? s : cps.slice(0, SIDECAR_TITLE_MAX).join('');
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Il valore da scrivere in `title`: il RESIDUO del titolo, non il titolo
|
|
39
|
+
* grezzo. `''` = non c'è niente da scrivere.
|
|
40
|
+
*
|
|
41
|
+
* I due casi non si trattano allo stesso modo, ed è il punto di questa
|
|
42
|
+
* funzione:
|
|
43
|
+
*
|
|
44
|
+
* - **con titolo custom** → il titolo di una conversazione nata dal deck È la
|
|
45
|
+
* label della tab (`🧵 loom-works · T32 revisione`), composta di tre pezzi
|
|
46
|
+
* che il lettore ha già altrove: il progetto (la riga sotto cui la
|
|
47
|
+
* conversazione compare), il task id (il campo `taskId` dello stesso
|
|
48
|
+
* record) e la nota (il campo `note`). Copiare il grezzo obbligherebbe
|
|
49
|
+
* compass a una seconda copia di `stripTaskId`, e a schermo ripeterebbe tre
|
|
50
|
+
* volte quello che già si legge.
|
|
51
|
+
* - **senza titolo custom** → `title` È il primo prompt, e vale intatto:
|
|
52
|
+
* quella è l'unica cosa che dice di cosa si parlava.
|
|
53
|
+
*
|
|
54
|
+
* Non è `sessionTitle`: quella, con residuo vuoto, cade sul primo prompt —
|
|
55
|
+
* dentro il deck è giusto, perché una riga muta non serve a nessuno, ma qui il
|
|
56
|
+
* primo prompt di una sessione nata dal deck è l'invocazione di una skill
|
|
57
|
+
* (`/loom-works:recap-status-task T32`), che nel menu non aggiunge nulla. Qui
|
|
58
|
+
* il residuo vuoto è ASSENZA: non si scrive, e chi legge cade sulla nota.
|
|
59
|
+
*/
|
|
60
|
+
export function sidecarTitle(s, core, taskId) {
|
|
61
|
+
if (!s.customTitle)
|
|
62
|
+
return s.title === NO_TITLE ? '' : capTitle(s.title);
|
|
63
|
+
return capTitle(stripTaskId(stripProjectCore(s.title, core), taskId));
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* I buchi da riempire in questo giro: per ogni conversazione PINNATA, i campi
|
|
67
|
+
* derivati che nel sidecar mancano o non corrispondono più al valore corrente.
|
|
68
|
+
*
|
|
69
|
+
* Solo le pinnate, e non tutte le conversazioni del progetto, perché sono le
|
|
70
|
+
* sole che un lettore esterno mostra: il resto sarebbe churn su un file
|
|
71
|
+
* append-only, un record per conversazione per ogni deck che si apre.
|
|
72
|
+
*
|
|
73
|
+
* Tre cose che non producono un append, e nessuna delle tre è un errore:
|
|
74
|
+
* - una pinnata STALE (il transcript non c'è più) — nessuna `Session`, quindi
|
|
75
|
+
* nessun valore da cui derivare;
|
|
76
|
+
* - un titolo derivato VUOTO — è l'assenza di P1, e appenderlo scriverebbe la
|
|
77
|
+
* cancellazione di un campo che nessuno ha chiesto di cancellare;
|
|
78
|
+
* - un modello non riconducibile a un alias (`''` o fuori famiglia) — chi
|
|
79
|
+
* riprende cadrà sulla propria cascata, che è meglio di un alias inventato.
|
|
80
|
+
*
|
|
81
|
+
* La CONVERGENZA è l'invariante da tenere: il confronto avviene fra il valore
|
|
82
|
+
* derivato e il valore letto dal sidecar senza riscritture in mezzo (nessuna
|
|
83
|
+
* sanificazione in lettura, cap applicato da questo lato una volta sola),
|
|
84
|
+
* quindi un buco riempito resta riempito e il giro successivo non trova niente
|
|
85
|
+
* da fare.
|
|
86
|
+
*/
|
|
87
|
+
export function sidecarGaps(input) {
|
|
88
|
+
const { pinned, sessions, bindings, titles, models, core } = input;
|
|
89
|
+
if (pinned.size === 0)
|
|
90
|
+
return [];
|
|
91
|
+
const byId = new Map(sessions.map((s) => [s.sessionId, s]));
|
|
92
|
+
const out = [];
|
|
93
|
+
for (const sessionId of pinned.keys()) {
|
|
94
|
+
const session = byId.get(sessionId);
|
|
95
|
+
if (!session)
|
|
96
|
+
continue; // pinnata stale: niente da cui derivare
|
|
97
|
+
const gap = { sessionId };
|
|
98
|
+
const title = sidecarTitle(session, core, bindings.get(sessionId) ?? null);
|
|
99
|
+
if (title && titles.get(sessionId) !== title)
|
|
100
|
+
gap.title = title;
|
|
101
|
+
const model = modelAlias(session.model);
|
|
102
|
+
if (model && models.get(sessionId) !== model)
|
|
103
|
+
gap.model = model;
|
|
104
|
+
if (gap.title !== undefined || gap.model !== undefined)
|
|
105
|
+
out.push(gap);
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
package/dist/sessions.js
CHANGED
|
@@ -54,6 +54,12 @@ const INTERRUPT_MARKER = '[Request interrupted by user';
|
|
|
54
54
|
function isInterrupt(text) {
|
|
55
55
|
return text.trimStart().startsWith(INTERRUPT_MARKER);
|
|
56
56
|
}
|
|
57
|
+
/** Titolo di una conversazione che non ne ha nessuno — né custom né un primo
|
|
58
|
+
* prompt da cui derivarlo. È un SEGNAPOSTO di resa, non un titolo: chi copia
|
|
59
|
+
* il titolo altrove (T162, il sidecar per compass) deve poterlo riconoscere e
|
|
60
|
+
* trattarlo come assenza, invece di propagare la stringa. Esportato per quello
|
|
61
|
+
* — una seconda copia del letterale divergerebbe al primo ritocco del testo. */
|
|
62
|
+
export const NO_TITLE = '(senza titolo)';
|
|
57
63
|
// T110 — valore che il CLI scrive nel campo modello dei record che fabbrica da
|
|
58
64
|
// sé (osservato su `{"type":"text","text":"No response requested."}`). Restano
|
|
59
65
|
// `type:assistant` a tutti gli effetti: chi si fida del campo etichetta la
|
|
@@ -248,7 +254,7 @@ export function parseTranscript(content, path, mtime, sizeBytes) {
|
|
|
248
254
|
cwd,
|
|
249
255
|
gitBranch,
|
|
250
256
|
parentUuid,
|
|
251
|
-
title: sanitize(customTitle || firstUserText ||
|
|
257
|
+
title: sanitize(customTitle || firstUserText || NO_TITLE),
|
|
252
258
|
ts: mtime,
|
|
253
259
|
path,
|
|
254
260
|
sizeBytes,
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
// Il CATALOGO delle azioni di spawn: per ogni cosa che il deck sa aprire, un id
|
|
2
|
+
// stabile e la terna titolo/modello/prompt con la sorgente di ciascun valore.
|
|
3
|
+
//
|
|
4
|
+
// Prima di questo modulo un'azione di spawn non era un'entità: era un tasto che
|
|
5
|
+
// chiamava una funzione, e ogni funzione decideva da sé — nel proprio punto di
|
|
6
|
+
// chiamata — quale modello e quale prompt passare. Il modello veniva da quattro
|
|
7
|
+
// sorgenti diverse (il catalogo `prompt-catalog` per kind, il transcript per
|
|
8
|
+
// resume e fork, un selettore vivo per la nuda, un letterale in `actions.ts` per
|
|
9
|
+
// drain e srotolamento) e non esisteva nessun posto in cui leggerne l'elenco.
|
|
10
|
+
// Senza quell'elenco non c'è nessuna tabella da mostrare, e nessuna riga che un
|
|
11
|
+
// override di progetto possa indirizzare.
|
|
12
|
+
//
|
|
13
|
+
// DATO PURO: nessuna lettura di file, nessun React, nessun import di
|
|
14
|
+
// `spawn.ts`. La dipendenza va in un verso solo (coding-standards: nessun modulo
|
|
15
|
+
// estratto importa il file da cui è stato estratto) — `spawn.ts`, `actions.ts`,
|
|
16
|
+
// `sheet.ts`, `glyphs.ts` e `prompt-catalog.ts` importano da qui.
|
|
17
|
+
//
|
|
18
|
+
// Chi COMPONE i default con gli override di progetto è `spawn-config.ts`: lì
|
|
19
|
+
// vivono la lettura del file, la validazione al confine e la risoluzione della
|
|
20
|
+
// terna. Qui ci sono solo i default del deck.
|
|
21
|
+
/**
|
|
22
|
+
* `recap` → la sotto-skill giusta, quando chi spawna SA se la task è un cappello.
|
|
23
|
+
*
|
|
24
|
+
* `recap` resta il kind onesto per chi non lo sa: punta al dispatcher, che
|
|
25
|
+
* risolve la task e classifica da sé. È il caso degli acceleratori della lista,
|
|
26
|
+
* dove il deck ha in mano solo `tasks.md` — e lì il `Size` non c'è. Il DETAIL
|
|
27
|
+
* invece il task file l'ha già letto, quindi può saltare il giro e pagare un
|
|
28
|
+
* turno di modello in meno.
|
|
29
|
+
*
|
|
30
|
+
* Non è una classificazione duplicata: il criterio (`Size: Epic`) resta uno solo
|
|
31
|
+
* e sta in `taskIsEpic`, che legge lo stesso campo che leggerebbe il dispatcher.
|
|
32
|
+
* Quello che si evita è il RITARDO, non il giudizio.
|
|
33
|
+
*
|
|
34
|
+
* T161 — la stessa funzione è anche il ponte dal detail al catalogo: gli id
|
|
35
|
+
* delle sette azioni su task SONO i sette kind, quindi specializzare un kind
|
|
36
|
+
* significa già scegliere la riga di catalogo (e l'override che la indirizza).
|
|
37
|
+
*
|
|
38
|
+
* Ogni kind diverso da `recap` passa intatto: la specializzazione è un caso, non
|
|
39
|
+
* una trasformazione da applicare a tutti.
|
|
40
|
+
*/
|
|
41
|
+
export function specializeRecap(kind, epic) {
|
|
42
|
+
if (kind !== 'recap')
|
|
43
|
+
return kind;
|
|
44
|
+
return epic ? 'recap-epic' : 'recap-task';
|
|
45
|
+
}
|
|
46
|
+
// L'ordine È il giro di `tab` nel detail, non una preferenza di lettura:
|
|
47
|
+
// cambiarlo sposta le voci sotto le dita di chi le ha imparate. Fino a T111 era
|
|
48
|
+
// anche il binding delle cifre `1`-`4`, passate poi al campo nota.
|
|
49
|
+
export const MODELS = ['fable', 'opus', 'sonnet', 'haiku'];
|
|
50
|
+
/** Il valore è uno dei quattro alias? Guardia del confine: la usano il parser
|
|
51
|
+
* del catalogo dati e quello del blocco `spawn` del file di progetto, che
|
|
52
|
+
* leggono entrambi testo scritto a mano. */
|
|
53
|
+
export function isModelKind(v) {
|
|
54
|
+
return typeof v === 'string' && MODELS.includes(v);
|
|
55
|
+
}
|
|
56
|
+
// Default del selettore e FALLBACK ultimo di ogni risoluzione. Duplicato del
|
|
57
|
+
// default di deck-run e non letto da lì: il deck deve poter MOSTRARE la
|
|
58
|
+
// selezione iniziale prima di spawnare alcunché, e un valore che si conosce solo
|
|
59
|
+
// a spawn avvenuto non è mostrabile.
|
|
60
|
+
//
|
|
61
|
+
// T161 — non è più il default di nessuna azione in particolare: ogni riga del
|
|
62
|
+
// catalogo porta il proprio (o lo prende dal catalogo dati), e qui si cade solo
|
|
63
|
+
// quando nessuna delle due sorgenti ha una voce.
|
|
64
|
+
export const MODEL_DEFAULT = 'fable';
|
|
65
|
+
// T152 — `recap-status-project` non è una chiave di `prompt-catalog` (le chiavi
|
|
66
|
+
// lì sono i valori di `--prompt-kind`, e lo spawn headless non ne passa mai
|
|
67
|
+
// uno): il suo default vive nella riga `project-status` di questo catalogo.
|
|
68
|
+
// Resta `opus` mentre i tre recap stanno su fable, ed è l'unico recap che gira
|
|
69
|
+
// in headless (`-p`), senza nessuno che ne legga l'esito mentre si forma e possa
|
|
70
|
+
// rilanciarlo — il testo prodotto è il deliverable, e viene riletto da disco
|
|
71
|
+
// anche giorni dopo.
|
|
72
|
+
export const PROJECT_STATUS_MODEL = 'opus';
|
|
73
|
+
// T66 — le azioni del detail. Non sono un catalogo a parte: ognuna nomina una
|
|
74
|
+
// riga di questo catalogo, per kind, e tutte passano dallo stesso `spawnForTask`
|
|
75
|
+
// dei CTRL della lista — una superficie in più, zero percorsi di spawn in più.
|
|
76
|
+
//
|
|
77
|
+
// L'etichetta è distinta dal kind dove il kind è il nome del MECCANISMO e
|
|
78
|
+
// l'etichetta quello dell'INTENZIONE: `none` è "aprire la task a mani nude",
|
|
79
|
+
// `recap` è "vedere a che punto sta".
|
|
80
|
+
export const DETAIL_ACTIONS = [
|
|
81
|
+
{ kind: 'none', label: 'open' },
|
|
82
|
+
{ kind: 'preflight', label: 'preflight' },
|
|
83
|
+
{ kind: 'run', label: 'run' },
|
|
84
|
+
{ kind: 'recap', label: 'status' },
|
|
85
|
+
{ kind: 'checkpoint', label: 'checkpoint' },
|
|
86
|
+
];
|
|
87
|
+
// T117 — selezione DIRETTA di un'azione con la sua iniziale (`o p r s c`),
|
|
88
|
+
// accanto allo scorrimento `←→`. Derivate dalle label e non cablate: una voce
|
|
89
|
+
// aggiunta al catalogo porta con sé la propria lettera, invece di lasciare
|
|
90
|
+
// indietro una seconda lista.
|
|
91
|
+
// Il vincolo che la derivazione impone al catalogo: le iniziali devono restare
|
|
92
|
+
// DISTINTE fra loro. Due label con la stessa lettera renderebbero la seconda
|
|
93
|
+
// irraggiungibile, e in silenzio — chi aggiunge una voce lo controlla qui.
|
|
94
|
+
export const ACTION_HOTKEYS = Object.fromEntries(DETAIL_ACTIONS.map((a, i) => [a.label[0], i]));
|
|
95
|
+
export const SPAWN_FIELDS = ['title', 'model', 'prompt'];
|
|
96
|
+
/**
|
|
97
|
+
* Il prompt cablato della sessione che srotola l'hard-wrap di un path.
|
|
98
|
+
*
|
|
99
|
+
* Le tre cose che dice sono le tre che rendono l'operazione reversibile e
|
|
100
|
+
* verificabile — backup fuori dal repo, rapporto, commit solo se il diff è
|
|
101
|
+
* chiaramente innocuo. Era cablato in `wrap-scan.ts` e da T161 è il DEFAULT di
|
|
102
|
+
* una riga del catalogo: resta la stessa frase, ma ora è sovrascrivibile dal
|
|
103
|
+
* progetto invece che dal solo sorgente.
|
|
104
|
+
*
|
|
105
|
+
* Nessun apice singolo, come i template di `prompt-catalog`: il testo finisce
|
|
106
|
+
* dentro `'…'` nel comando passato a `bash -lc`, e `deck-run` lo quota, ma un
|
|
107
|
+
* apice qui sarebbe comunque una scrittura da rileggere due volte.
|
|
108
|
+
*/
|
|
109
|
+
const UNWRAP_PROMPT = 'lancia md-wrap modo apply su {path} e backup in cartella tmp dedicata. ' +
|
|
110
|
+
'verifica risultato e fai rapporto. se le modifiche sono tutte chiaramente safe, ' +
|
|
111
|
+
'puoi committare direttamente';
|
|
112
|
+
/**
|
|
113
|
+
* Le UNDICI azioni configurabili, nell'ordine in cui la pagina le elenca.
|
|
114
|
+
*
|
|
115
|
+
* Il perimetro è quello di D1: entra una riga se ha almeno una cella editabile.
|
|
116
|
+
* Ne restano fuori resume e fork — lì il modello non è una preferenza ma
|
|
117
|
+
* un'EREDITÀ, letta dal transcript della conversazione d'origine, e un override
|
|
118
|
+
* sarebbe una cella che mente — insieme a `create-task` e `clean-tasks`, che non
|
|
119
|
+
* passano `--model` e girano sul default del CLI (dargliene una sarebbe un
|
|
120
|
+
* cambio di comportamento fatto per riempire una riga), al terminale e alle voci
|
|
121
|
+
* `launch`, che non sono sessioni Claude e non hanno né modello né prompt.
|
|
122
|
+
*
|
|
123
|
+
* T156 — le emoji dei titoli sono ASTRALI (code point ≥ U+10000) per necessità,
|
|
124
|
+
* non per gusto: `sanitize` (`src/width.ts`) sostituisce con `·` ogni glifo su
|
|
125
|
+
* cui le due contabilità di larghezza divergono, e le emoji del BMP a
|
|
126
|
+
* presentazione testo divergono sempre. Le stesse sei stanno nella whitelist di
|
|
127
|
+
* `_sane_note` in `deck-run` e nel suo gemello `src/sane-note.ts`: un'emoji
|
|
128
|
+
* cambiata qui e non là sparisce fra la tabella e la tab, senza errore.
|
|
129
|
+
*/
|
|
130
|
+
export const SPAWN_ACTIONS = [
|
|
131
|
+
{
|
|
132
|
+
id: 'none',
|
|
133
|
+
label: 'apri la task',
|
|
134
|
+
surface: 'detail o · ⏎⏎',
|
|
135
|
+
kind: 'none',
|
|
136
|
+
// D2 di T150 — nessun prefisso: `open` è "nessuna azione", e inventargliene
|
|
137
|
+
// uno contraddirebbe l'intenzione di chi la sceglie.
|
|
138
|
+
title: '{slug}',
|
|
139
|
+
model: null,
|
|
140
|
+
prompt: '',
|
|
141
|
+
holes: ['TASK', 'slug'],
|
|
142
|
+
fixed: {},
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: 'recap',
|
|
146
|
+
label: 'stato · dispatcher',
|
|
147
|
+
surface: 'lista ^K',
|
|
148
|
+
kind: 'recap',
|
|
149
|
+
// P3 di T150 — le tre varianti di recap condividono il prefisso: la
|
|
150
|
+
// specializzazione sceglie quale skill parte, non cosa si sta chiedendo.
|
|
151
|
+
title: '📊 {slug}',
|
|
152
|
+
model: null,
|
|
153
|
+
prompt: null,
|
|
154
|
+
holes: ['TASK', 'slug'],
|
|
155
|
+
fixed: {},
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: 'recap-task',
|
|
159
|
+
label: 'stato task',
|
|
160
|
+
surface: 'detail s',
|
|
161
|
+
kind: 'recap-task',
|
|
162
|
+
title: '📊 {slug}',
|
|
163
|
+
model: null,
|
|
164
|
+
prompt: null,
|
|
165
|
+
holes: ['TASK', 'slug'],
|
|
166
|
+
fixed: {},
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
id: 'recap-epic',
|
|
170
|
+
label: 'stato epica',
|
|
171
|
+
surface: 'detail s · cappello',
|
|
172
|
+
kind: 'recap-epic',
|
|
173
|
+
title: '📊 {slug}',
|
|
174
|
+
model: null,
|
|
175
|
+
prompt: null,
|
|
176
|
+
holes: ['TASK', 'slug'],
|
|
177
|
+
fixed: {},
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: 'preflight',
|
|
181
|
+
label: 'preflight',
|
|
182
|
+
surface: 'detail p · lista ^P',
|
|
183
|
+
kind: 'preflight',
|
|
184
|
+
title: '📐 {slug}',
|
|
185
|
+
model: null,
|
|
186
|
+
prompt: null,
|
|
187
|
+
holes: ['TASK', 'slug'],
|
|
188
|
+
fixed: {},
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
id: 'run',
|
|
192
|
+
label: 'run',
|
|
193
|
+
surface: 'detail r · lista ^R',
|
|
194
|
+
kind: 'run',
|
|
195
|
+
title: '🚀 {slug}',
|
|
196
|
+
model: null,
|
|
197
|
+
prompt: null,
|
|
198
|
+
holes: ['TASK', 'slug'],
|
|
199
|
+
fixed: {},
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
id: 'checkpoint',
|
|
203
|
+
label: 'checkpoint',
|
|
204
|
+
surface: 'detail c',
|
|
205
|
+
kind: 'checkpoint',
|
|
206
|
+
title: '🏁 {slug}',
|
|
207
|
+
model: null,
|
|
208
|
+
prompt: null,
|
|
209
|
+
holes: ['TASK', 'slug'],
|
|
210
|
+
fixed: {},
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
// T42/T154 — la sessione a mani nude. Configurabile sul solo modello (P12):
|
|
214
|
+
// non pinna un `sessionId` e non scrive nel sidecar, quindi un titolo
|
|
215
|
+
// comparirebbe nella tab e non in lista — la divergenza che `bareArgs`
|
|
216
|
+
// esiste per non avere — e un prompt la renderebbe non nuda.
|
|
217
|
+
id: 'bare',
|
|
218
|
+
label: 'sessione nuda',
|
|
219
|
+
surface: 'tasto c',
|
|
220
|
+
kind: null,
|
|
221
|
+
title: null,
|
|
222
|
+
model: MODEL_DEFAULT,
|
|
223
|
+
prompt: '',
|
|
224
|
+
holes: [],
|
|
225
|
+
fixed: {
|
|
226
|
+
title: 'nessun sessionId pinnato',
|
|
227
|
+
prompt: 'la renderebbe non nuda',
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
// T134 — il drain di un file inbox. Il prompt lo compone `inboxPrompt`
|
|
232
|
+
// (`src/inbox.ts`), che è l'unico posto in cui vive la mappa natura →
|
|
233
|
+
// skill: un template unico non saprebbe esprimerla, e spezzare la riga in
|
|
234
|
+
// una per natura contraddirebbe il perimetro di D1.
|
|
235
|
+
id: 'drain',
|
|
236
|
+
label: 'drain di un file inbox',
|
|
237
|
+
surface: 'pane inbox ⏎⏎',
|
|
238
|
+
kind: null,
|
|
239
|
+
title: '🧹 {file}',
|
|
240
|
+
model: MODEL_DEFAULT,
|
|
241
|
+
prompt: null,
|
|
242
|
+
holes: ['file'],
|
|
243
|
+
fixed: { prompt: 'natura del file' },
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
// T134 — lo srotolamento dell'hard-wrap. `sonnet` e non il default: è una
|
|
247
|
+
// passata meccanica con un verificatore deterministico dietro (`md-wrap
|
|
248
|
+
// --apply` confronta le due versioni normalizzate e rimette indietro il
|
|
249
|
+
// file se differiscono), quindi il giudizio chiesto al modello è leggere un
|
|
250
|
+
// diff e decidere se committarlo — non progettare niente.
|
|
251
|
+
id: 'unwrap',
|
|
252
|
+
label: 'srotolamento hard-wrap',
|
|
253
|
+
surface: 'lista ^W ⏎',
|
|
254
|
+
kind: null,
|
|
255
|
+
title: '📏 {path}',
|
|
256
|
+
model: 'sonnet',
|
|
257
|
+
prompt: UNWRAP_PROMPT,
|
|
258
|
+
holes: ['path'],
|
|
259
|
+
fixed: {},
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
// T121 — il recap di progetto, headless (`claude -p`): nessuna tab, quindi
|
|
263
|
+
// nessun titolo da configurare.
|
|
264
|
+
id: 'project-status',
|
|
265
|
+
label: 'project status',
|
|
266
|
+
surface: 'tasto ^G',
|
|
267
|
+
kind: null,
|
|
268
|
+
title: null,
|
|
269
|
+
model: PROJECT_STATUS_MODEL,
|
|
270
|
+
prompt: '/loom-works:recap-status-project',
|
|
271
|
+
holes: [],
|
|
272
|
+
fixed: { title: 'headless: nessuna tab' },
|
|
273
|
+
},
|
|
274
|
+
];
|
|
275
|
+
/**
|
|
276
|
+
* Valori d'esempio dei buchi, per l'ANTEPRIMA dell'area di compilazione.
|
|
277
|
+
*
|
|
278
|
+
* La cella della tabella mostra il template grezzo (`🚀 {slug}`), l'area mostra
|
|
279
|
+
* come verrebbe reso: senza l'esempio un template si scrive alla cieca, e
|
|
280
|
+
* l'effetto si scopre alla prima conversazione aperta.
|
|
281
|
+
*
|
|
282
|
+
* Sono valori FINTI e dichiarati tali dalla pagina, non la task selezionata: la
|
|
283
|
+
* pagina non ha una selezione — si apre da qualunque punto del deck — e legare
|
|
284
|
+
* l'anteprima a ciò che capita di avere sotto il caret la renderebbe diversa a
|
|
285
|
+
* ogni apertura, per la stessa configurazione.
|
|
286
|
+
*/
|
|
287
|
+
export const EXAMPLE_HOLES = {
|
|
288
|
+
TASK: 'T42',
|
|
289
|
+
slug: 'esempio di task',
|
|
290
|
+
file: 'T42-nozioni',
|
|
291
|
+
path: 'runtime reference',
|
|
292
|
+
};
|
|
293
|
+
/**
|
|
294
|
+
* I valori sono quelli che i FORNITORI dei buchi producono davvero, non testo
|
|
295
|
+
* grezzo: lo slug arriva dal nome del task file coi trattini già sciolti in
|
|
296
|
+
* spazi, il basename dell'inbox senza `.md`, il path già reso in parole. Un
|
|
297
|
+
* esempio con dentro un `/` o un `.` mostrerebbe l'effetto della riduzione — le
|
|
298
|
+
* parole saldate — su un caso che allo spawn non si presenta mai, e chi lo
|
|
299
|
+
* legge correggerebbe un titolo che non è rotto.
|
|
300
|
+
*/
|
|
301
|
+
const BY_ID = new Map(SPAWN_ACTIONS.map((a) => [a.id, a]));
|
|
302
|
+
/** L'azione di un id, o `undefined` per un id che il catalogo non conosce — il
|
|
303
|
+
* caso di un override scritto a mano per un'azione che non esiste più. */
|
|
304
|
+
export function spawnAction(id) {
|
|
305
|
+
return BY_ID.get(id);
|
|
306
|
+
}
|
|
307
|
+
/** L'id è nel catalogo? Guardia del parser degli override. */
|
|
308
|
+
export function isSpawnActionId(id) {
|
|
309
|
+
return typeof id === 'string' && BY_ID.has(id);
|
|
310
|
+
}
|
|
311
|
+
/** Le celle editabili di una riga: il complemento di `fixed`, derivato e mai
|
|
312
|
+
* scritto a mano. */
|
|
313
|
+
export function editableFields(action) {
|
|
314
|
+
return SPAWN_FIELDS.filter((f) => !(f in action.fixed));
|
|
315
|
+
}
|
|
316
|
+
/** Il segnaposto di una cella editabile ma a vuoto. Il vuoto è un valore
|
|
317
|
+
* legittimo e distinto dall'assenza (P15): un titolo svuotato significa
|
|
318
|
+
* «nessuna nota», un prompt svuotato «nessun prompt». */
|
|
319
|
+
const EMPTY_LABEL = {
|
|
320
|
+
title: '(nessun titolo)',
|
|
321
|
+
model: '(nessun modello)',
|
|
322
|
+
prompt: '(nessun prompt)',
|
|
323
|
+
};
|
|
324
|
+
export function spawnCell(action, field,
|
|
325
|
+
/** Il valore risolto: `null` quando la cella non ne ha uno. */
|
|
326
|
+
value,
|
|
327
|
+
/** Il valore viene da un override di progetto. */
|
|
328
|
+
overridden) {
|
|
329
|
+
const reason = action.fixed[field];
|
|
330
|
+
if (reason !== undefined) {
|
|
331
|
+
return { text: `(${reason})`, editable: false, origin: 'fisso', placeholder: true };
|
|
332
|
+
}
|
|
333
|
+
const origin = overridden ? 'override' : 'default';
|
|
334
|
+
if (value === null || value === '') {
|
|
335
|
+
return { text: EMPTY_LABEL[field], editable: true, origin, placeholder: true };
|
|
336
|
+
}
|
|
337
|
+
return { text: value, editable: true, origin, placeholder: false };
|
|
338
|
+
}
|