@lamemind/loom-deck 0.76.0 → 0.78.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 +24 -1
- package/dist/actions.js +32 -5
- package/dist/config.js +1 -1
- package/dist/deck-run.js +7 -0
- package/dist/deliverables.js +139 -0
- package/dist/doc-tree.js +1 -1
- package/dist/glyphs.js +16 -0
- package/dist/inbox.js +1 -1
- package/dist/input.js +4 -0
- package/dist/overlays/dlv-mode.js +123 -0
- package/dist/overlays/sheet.js +39 -2
- package/dist/platform.js +1 -2
- package/dist/project-path.js +43 -5
- package/dist/sane-note.js +18 -24
- package/dist/spawn-catalog.js +4 -4
- package/dist/spawn-config.js +4 -4
- package/dist/spawn.js +51 -7
- package/dist/tab-args.js +141 -0
- package/dist/tab-compose.js +233 -0
- package/dist/tab-launch.js +169 -0
- package/dist/tab-wrapper.js +44 -0
- package/dist/tasks.js +1 -1
- package/dist/ui/detail-screen.js +8 -3
- package/dist/ui/dlv-area.js +51 -0
- package/dist/ui/screens.js +1 -1
- package/dist/viewport.js +37 -14
- package/dist/windows-terminal.js +153 -0
- package/dist/wrap-scan.js +1 -1
- package/package.json +2 -1
- package/scripts/deck-run +11 -678
- package/scripts/prompt-catalog +6 -11
package/dist/sane-note.js
CHANGED
|
@@ -1,35 +1,29 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// La riduzione della nota del titolo di una tab: l'unica, per il deck e per
|
|
2
|
+
// `deck-run`.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
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.
|
|
4
|
+
// La nota del titolo è l'UNICO ingresso di testo libero nel titolo di una tab,
|
|
5
|
+
// quindi non si quota: si RIDUCE a una whitelist chiusa di code point, e tutto
|
|
6
|
+
// ciò che non ci rientra sparisce senza errore. Da quando un TEMPLATE del titolo
|
|
7
|
+
// si scrive in una pagina e si salva su disco (T161), la riduzione si applica
|
|
8
|
+
// anche QUI, al confine — al salvataggio e alla lettura del blocco `spawn` —
|
|
9
|
+
// così quello che sta nel file è quello che la tab mostrerà.
|
|
12
10
|
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
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.
|
|
11
|
+
// T176 — fino a T176 questo modulo era il gemello di `_sane_note`, la stessa
|
|
12
|
+
// riduzione scritta in bash dentro `deck-run`, e un test teneva uguali i due
|
|
13
|
+
// lati. Il composer della tab ora è TypeScript (`src/tab-compose.ts`) e chiama
|
|
14
|
+
// questa funzione: la whitelist ha due sedi da toccare insieme, questo file e le
|
|
15
|
+
// emoji dei titoli in `spawn-catalog.ts`, e `test/deck-run.test.ts` fallisce se
|
|
16
|
+
// un'emoji di un titolo del catalogo non passa.
|
|
23
17
|
/**
|
|
24
18
|
* L'alfabeto ammesso, negato: lettere, cifre, spazio, `_`, `-`, le accentate
|
|
25
19
|
* italiane e sette emoji.
|
|
26
20
|
*
|
|
27
|
-
* Whitelist e non blacklist
|
|
28
|
-
*
|
|
29
|
-
* significa sbagliarne uno.
|
|
21
|
+
* Whitelist e non blacklist: il titolo finisce in una riga di shell su Linux
|
|
22
|
+
* (`bash -lc "claude --name '…'"`) e nella riga di `wt.exe` su Windows, ed
|
|
23
|
+
* enumerare i caratteri pericolosi significa sbagliarne uno.
|
|
30
24
|
*
|
|
31
25
|
* Le sette emoji sono nominate una per una e NON come intervallo di code point,
|
|
32
|
-
* per due ragioni
|
|
26
|
+
* per due ragioni: un'emoji arriva nella tab solo
|
|
33
27
|
* se il deck la mostra identica in lista, cioè se `sanitize` (`src/width.ts`) la
|
|
34
28
|
* lascia passare invece di renderla `·`, e quel verdetto si verifica un glifo
|
|
35
29
|
* alla volta; e l'ordine di un intervallo dentro un bracket expression segue la
|
package/dist/spawn-catalog.js
CHANGED
|
@@ -123,9 +123,9 @@ const UNWRAP_PROMPT = 'lancia md-wrap modo apply su {path} e backup in cartella
|
|
|
123
123
|
* non per gusto: `sanitize` (`src/width.ts`) sostituisce con `·` ogni glifo su
|
|
124
124
|
* cui le due contabilità di larghezza divergono, e le emoji del BMP a
|
|
125
125
|
* presentazione testo divergono sempre. Le stesse SETTE (T160 aggiunge `🧭`)
|
|
126
|
-
* stanno nella whitelist di `
|
|
127
|
-
*
|
|
128
|
-
*
|
|
126
|
+
* stanno nella whitelist di `saneNote` (`src/sane-note.ts`), la riduzione del
|
|
127
|
+
* titolo: un'emoji cambiata qui e non là sparisce fra la tabella e la tab,
|
|
128
|
+
* senza errore.
|
|
129
129
|
*/
|
|
130
130
|
export const SPAWN_ACTIONS = [
|
|
131
131
|
{
|
|
@@ -276,7 +276,7 @@ export const SPAWN_ACTIONS = [
|
|
|
276
276
|
// DUE buchi per lo stesso dato, e servono entrambi: `{target}` è il path
|
|
277
277
|
// VERO, che la skill risolve contro la colonna `PATH` della misura, e
|
|
278
278
|
// `{words}` lo stesso path reso a parole per il titolo. L'alfabeto di
|
|
279
|
-
// `
|
|
279
|
+
// `saneNote` non ha né `/` né `.`, quindi un `{target}` in un template di
|
|
280
280
|
// titolo uscirebbe con le parole saldate (`runtimereferencedoc-systemmd`) e
|
|
281
281
|
// la tab mostrerebbe un nome diverso da quello che il deck legge dal
|
|
282
282
|
// sidecar.
|
package/dist/spawn-config.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// ── La sede (P19 preflight) ─────────────────────────────────────────────────
|
|
5
5
|
// Blocco `spawn` dentro `.claude/loom-works.json`, il file di progetto che il
|
|
6
6
|
// deck già apre in tre loader (`launch`, `archivableDays`, `identity`) e che
|
|
7
|
-
// `deck-run` legge
|
|
7
|
+
// `deck-run` legge per label e permission mode. Nessun file nuovo: sotto `.claude/` ci sono già tre
|
|
8
8
|
// file di loom, e le impostazioni di spawn sono decisioni di progetto che devono
|
|
9
9
|
// viaggiare col repo — cioè committate, esattamente come `permissionMode` e
|
|
10
10
|
// `order`, e a differenza della vista (`.claude/loom/deck-view.json`, che è
|
|
@@ -34,9 +34,9 @@
|
|
|
34
34
|
// ── Chi legge cosa ──────────────────────────────────────────────────────────
|
|
35
35
|
// Il blocco lo legge il SOLO deck, che risolve la terna e la passa a `deck-run`
|
|
36
36
|
// sempre esplicita nell'argv (`--title-note`, `--model`, `--prompt`). `deck-run`
|
|
37
|
-
// non lo legge e non deve:
|
|
38
|
-
//
|
|
39
|
-
//
|
|
37
|
+
// non lo legge e non deve: la terna che parte è quella che il deck ha mostrato,
|
|
38
|
+
// e una seconda risoluzione dall'altra parte del processo sarebbe una seconda
|
|
39
|
+
// copia delle stesse regole. La cascata env → catalogo → `fable` di `deck-run`
|
|
40
40
|
// resta intatta per chi lo invoca a mano.
|
|
41
41
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
42
42
|
import { configFilePath } from './config.js';
|
package/dist/spawn.js
CHANGED
|
@@ -10,10 +10,24 @@ import { dirname, join } from 'node:path';
|
|
|
10
10
|
import { MODEL_DEFAULT, PROJECT_STATUS_MODEL } from './spawn-catalog.js';
|
|
11
11
|
import { isAvailable } from './platform.js';
|
|
12
12
|
import { claudeLauncher } from './claude-cmd.js';
|
|
13
|
-
import { claudeEnv } from './git-bash.js';
|
|
13
|
+
import { claudeEnv, gitBash } from './git-bash.js';
|
|
14
|
+
import { windowsTabEnv, WT_CMD, wtNewTabArgs, wtProfile } from './windows-terminal.js';
|
|
14
15
|
// scripts/deck-run è un sibling della dir del bundle: src/ (dev, tsx) e dist/
|
|
15
16
|
// (build, node) stanno entrambi sotto la package root → risalita di un livello.
|
|
16
17
|
export const DECK_RUN = join(dirname(fileURLToPath(import.meta.url)), '..', 'scripts', 'deck-run');
|
|
18
|
+
// T176 — il punto d'ingresso che lo shim `scripts/deck-run` esegue, nel build.
|
|
19
|
+
// Su Windows il deck lo lancia da sé: CreateProcess non esegue uno script bash
|
|
20
|
+
// senza estensione.
|
|
21
|
+
export const DECK_RUN_JS = join(dirname(fileURLToPath(import.meta.url)), '..', 'dist', 'deck-run.js');
|
|
22
|
+
/**
|
|
23
|
+
* Come il deck lancia `deck-run`: lo shim su Linux, il node del deck sul punto
|
|
24
|
+
* d'ingresso su Windows. Le due forme eseguono lo stesso programma — lo shim è
|
|
25
|
+
* una riga che fa `exec node dist/deck-run.js` —, e su Linux resta lo shim
|
|
26
|
+
* perché è il comando che la riga di stato mostra e che si ripete a mano.
|
|
27
|
+
*/
|
|
28
|
+
export function deckRunLauncher(platform = process.platform, nodePath = process.execPath) {
|
|
29
|
+
return platform === 'win32' ? { cmd: nodePath, prefix: [DECK_RUN_JS] } : { cmd: DECK_RUN, prefix: [] };
|
|
30
|
+
}
|
|
17
31
|
/**
|
|
18
32
|
* Freno agli effetti VERSO L'ESTERNO (tab Ptyxis, sessioni Claude, git commit).
|
|
19
33
|
*
|
|
@@ -78,13 +92,15 @@ export function shellCommand(cmd, args) {
|
|
|
78
92
|
// comando in-tab (vedi `onInTabCommand`). Il figlio resta detached — la pipe
|
|
79
93
|
// serve a leggere una riga, non a possedere il processo.
|
|
80
94
|
function launchDeckRun(args, cwd) {
|
|
81
|
-
const
|
|
95
|
+
const run = deckRunLauncher();
|
|
96
|
+
const argv = [...run.prefix, ...args];
|
|
97
|
+
const child = spawnOut(run.cmd, argv, {
|
|
82
98
|
cwd,
|
|
83
99
|
detached: true,
|
|
84
100
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
85
101
|
});
|
|
86
102
|
child.unref();
|
|
87
|
-
return { child, cmd: shellCommand(
|
|
103
|
+
return { child, cmd: shellCommand(run.cmd, argv) };
|
|
88
104
|
}
|
|
89
105
|
/** Prefisso della riga con cui `deck-run` annuncia il comando in-tab. */
|
|
90
106
|
export const INTAB_MARKER = 'LOOM_DECK_INTAB ';
|
|
@@ -152,10 +168,9 @@ export function onInTabCommand(child, cb) {
|
|
|
152
168
|
// acceleratori della lista passavano `--prompt-kind` e lasciavano risolvere il
|
|
153
169
|
// testo a `deck-run`, che consultava il proprio catalogo; il detail passava già
|
|
154
170
|
// il letterale. Con un override di progetto sui prompt la risoluzione deve
|
|
155
|
-
// stare da una parte sola, e quella parte è il deck —
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
// sull'argv.
|
|
171
|
+
// stare da una parte sola, e quella parte è il deck — l'unico dei due che legge
|
|
172
|
+
// il blocco `spawn` di `.claude/loom-works.json`. Un percorso solo, quindi, e
|
|
173
|
+
// ciò che parte è verificabile sull'argv.
|
|
159
174
|
//
|
|
160
175
|
// Due conseguenze dichiarate: `--prompt-kind` resta in `deck-run` per chi lo
|
|
161
176
|
// invoca a mano, e `LOOM_DECK_ENTER_PROMPT` non ha più effetto sugli spawn dal
|
|
@@ -346,7 +361,36 @@ export function runLaunch(entry, cwd) {
|
|
|
346
361
|
export function terminalArgs(cwd, title) {
|
|
347
362
|
return title ? ['--tab', '-T', title, '-d', cwd] : ['--tab', '-d', cwd];
|
|
348
363
|
}
|
|
364
|
+
/**
|
|
365
|
+
* T176 — il tasto `t` su Windows (P10 preflight): una tab di Windows Terminal
|
|
366
|
+
* nella finestra attiva, col profilo del deck e il titolo bloccato, SENZA
|
|
367
|
+
* comando — la shell è il `commandline` del profilo, come `ptyxis --tab` apre
|
|
368
|
+
* quella del profilo di Ptyxis.
|
|
369
|
+
*/
|
|
370
|
+
export function windowsTerminalArgs(cwd, title, env = process.env) {
|
|
371
|
+
return wtNewTabArgs({ newWindow: false, profile: wtProfile(env), cwd, title });
|
|
372
|
+
}
|
|
373
|
+
/** Il terminale del tasto `t`, per nome: è ciò che la nota dice quando non parte. */
|
|
374
|
+
export function terminalName(platform = process.platform) {
|
|
375
|
+
return platform === 'win32' ? WT_CMD : 'ptyxis';
|
|
376
|
+
}
|
|
349
377
|
export function spawnTerminal(cwd, title) {
|
|
378
|
+
if (process.platform === 'win32') {
|
|
379
|
+
// La tab eredita l'env del deck (T174 verdetto 5): senza i marker di
|
|
380
|
+
// sessione e senza `LOOM_TASK`, col bash di Git — come una conversazione
|
|
381
|
+
// `--no-task`, perché un `claude` lanciato da questa shell nascerebbe con
|
|
382
|
+
// l'env che riceve. `windowsHide: false`: `wt.exe` è un'app GUI, e con
|
|
383
|
+
// `true` una finestra nuova partirebbe nascosta.
|
|
384
|
+
const child = spawnOut(WT_CMD, windowsTerminalArgs(cwd, title), {
|
|
385
|
+
cwd,
|
|
386
|
+
detached: true,
|
|
387
|
+
stdio: 'ignore',
|
|
388
|
+
env: windowsTabEnv(process.env, null, gitBash()),
|
|
389
|
+
windowsHide: false,
|
|
390
|
+
});
|
|
391
|
+
child.unref();
|
|
392
|
+
return child;
|
|
393
|
+
}
|
|
350
394
|
const child = spawnOut('ptyxis', terminalArgs(cwd, title), {
|
|
351
395
|
cwd,
|
|
352
396
|
detached: true,
|
package/dist/tab-args.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/** I kind accettati da `--prompt-kind`. Enum e non stringa libera: un kind è un
|
|
2
|
+
* simbolo interno del deck, e uno fuori elenco è un errore del chiamante. */
|
|
3
|
+
export const PROMPT_KINDS = [
|
|
4
|
+
'none',
|
|
5
|
+
'recap',
|
|
6
|
+
'recap-task',
|
|
7
|
+
'recap-epic',
|
|
8
|
+
'preflight',
|
|
9
|
+
'run',
|
|
10
|
+
'checkpoint',
|
|
11
|
+
];
|
|
12
|
+
export const USAGE = `uso: deck-run <TaskID> [--model <alias>] [--prompt-kind <kind>] [--session-id <uuid>]
|
|
13
|
+
(es. deck-run T18)
|
|
14
|
+
| deck-run --no-task (sessione nuda, senza task)
|
|
15
|
+
| deck-run <TaskID> --resume <uuid> (riprende sessione scoped)
|
|
16
|
+
| deck-run --no-task --resume <uuid> (riprende sessione spot)
|
|
17
|
+
| deck-run <TaskID> --resume <uuid> --fork [--session-id <nuovo-uuid>]
|
|
18
|
+
(forka: ramo con id NUOVO)
|
|
19
|
+
|
|
20
|
+
--prompt-kind prompt iniziale della sessione bound (default: recap)
|
|
21
|
+
none nessun prompt — sessione aperta sulla task, a mani nude
|
|
22
|
+
recap /loom-works:recap-status <TaskID> (dispatcher)
|
|
23
|
+
recap-task /loom-works:recap-status-task <TaskID> (task non cappello)
|
|
24
|
+
recap-epic /loom-works:recap-status-epic <TaskID> (Size: Epic)
|
|
25
|
+
preflight /loom-works:preflight-task <TaskID>
|
|
26
|
+
run /loom-works:run-task <TaskID>
|
|
27
|
+
checkpoint /loom-works:checkpoint-task <TaskID>
|
|
28
|
+
|
|
29
|
+
--prompt prompt LETTERALE, per chi lo ha già composto (o fatto
|
|
30
|
+
modificare). Esclusivo con --prompt-kind; vuoto = nessun
|
|
31
|
+
prompt. Ammesso anche con --no-task: un testo già scritto non
|
|
32
|
+
ha una task da nominare, a differenza dei kind del catalogo
|
|
33
|
+
|
|
34
|
+
--model modello della sessione: fable|opus|sonnet|haiku
|
|
35
|
+
(default: il modello del kind in catalogo, o 'fable' fuori catalogo)
|
|
36
|
+
valore ignoto → fallback sul default, con avviso su stderr
|
|
37
|
+
|
|
38
|
+
--title-note nota della conversazione, appesa al titolo tab
|
|
39
|
+
(ridotta a lettere/cifre/spazi/-/_, cap 60 char)
|
|
40
|
+
|
|
41
|
+
--new-window apre una FINESTRA nuova invece di una tab nella finestra
|
|
42
|
+
attiva. Per chi chiama da fuori dalla finestra del progetto`;
|
|
43
|
+
/** I flag che portano un valore, nel campo della richiesta che lo riceve. */
|
|
44
|
+
const VALUED = {
|
|
45
|
+
'--model': 'model',
|
|
46
|
+
'--title-note': 'titleNote',
|
|
47
|
+
'--prompt-kind': 'promptKind',
|
|
48
|
+
'--prompt': 'promptText',
|
|
49
|
+
'--session-id': 'sessionId',
|
|
50
|
+
'--resume': 'resumeId',
|
|
51
|
+
};
|
|
52
|
+
function usageError(...lines) {
|
|
53
|
+
return { lines: [...lines, USAGE] };
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* L'argv di `deck-run` come richiesta validata, o l'errore d'uso.
|
|
57
|
+
*
|
|
58
|
+
* Le due forme di un flag con valore (`--model x` e `--model=x`) valgono uguale;
|
|
59
|
+
* un argomento che non è un flag noto è il TaskID se il TaskID non c'è ancora,
|
|
60
|
+
* altrimenti un errore — anche `--qualcosa`, come nello script.
|
|
61
|
+
*
|
|
62
|
+
* L'ordine delle validazioni è quello dello script, perché è l'ordine in cui un
|
|
63
|
+
* chiamante riceve i messaggi quando più regole sono violate insieme.
|
|
64
|
+
*/
|
|
65
|
+
export function parseTabArgs(argv) {
|
|
66
|
+
const req = {
|
|
67
|
+
task: '',
|
|
68
|
+
noTask: false,
|
|
69
|
+
sessionId: '',
|
|
70
|
+
resumeId: '',
|
|
71
|
+
fork: false,
|
|
72
|
+
newWindow: false,
|
|
73
|
+
titleNote: '',
|
|
74
|
+
promptKind: '',
|
|
75
|
+
promptText: '',
|
|
76
|
+
promptGiven: false,
|
|
77
|
+
model: '',
|
|
78
|
+
};
|
|
79
|
+
for (let i = 0; i < argv.length; i++) {
|
|
80
|
+
const arg = argv[i];
|
|
81
|
+
const eq = arg.indexOf('=');
|
|
82
|
+
const name = eq > 0 ? arg.slice(0, eq) : arg;
|
|
83
|
+
const field = VALUED[name];
|
|
84
|
+
if (field) {
|
|
85
|
+
let value;
|
|
86
|
+
if (eq > 0)
|
|
87
|
+
value = arg.slice(eq + 1);
|
|
88
|
+
else if (i + 1 < argv.length)
|
|
89
|
+
value = argv[++i];
|
|
90
|
+
else
|
|
91
|
+
return usageError(`${name} richiede un valore`);
|
|
92
|
+
req[field] = value;
|
|
93
|
+
if (field === 'promptText')
|
|
94
|
+
req.promptGiven = true;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (arg === '--fork')
|
|
98
|
+
req.fork = true;
|
|
99
|
+
else if (arg === '--no-task')
|
|
100
|
+
req.noTask = true;
|
|
101
|
+
else if (arg === '--new-window')
|
|
102
|
+
req.newWindow = true;
|
|
103
|
+
else if (!req.task)
|
|
104
|
+
req.task = arg;
|
|
105
|
+
else
|
|
106
|
+
return { lines: [`argomento inatteso: '${arg}'`] };
|
|
107
|
+
}
|
|
108
|
+
if (req.noTask && req.task) {
|
|
109
|
+
return usageError('--no-task e <TaskID> sono mutuamente esclusivi');
|
|
110
|
+
}
|
|
111
|
+
if (!req.noTask && !req.task)
|
|
112
|
+
return usageError();
|
|
113
|
+
if (req.fork && !req.resumeId) {
|
|
114
|
+
return usageError('--fork richiede --resume <uuid>: si forka una conversazione esistente, non il nulla');
|
|
115
|
+
}
|
|
116
|
+
// Una ripresa nuda riscrive il transcript dell'id ripreso, quindi un id
|
|
117
|
+
// pinnato non ha senso; il fork apre un transcript NUOVO, che si può pinnare.
|
|
118
|
+
if (req.resumeId && req.sessionId && !req.fork) {
|
|
119
|
+
return usageError('--resume e --session-id sono mutuamente esclusivi (la sessione ripresa ha già il suo id)', 'usa --fork se vuoi un ramo con id nuovo');
|
|
120
|
+
}
|
|
121
|
+
// Un kind fuori catalogo è un errore del CHIAMANTE (simbolo interno, non
|
|
122
|
+
// configurazione utente): uscita, non fallback silenzioso come il modello.
|
|
123
|
+
if (req.promptKind && !PROMPT_KINDS.includes(req.promptKind)) {
|
|
124
|
+
return usageError(`--prompt-kind ignoto: '${req.promptKind}' (usa ${PROMPT_KINDS.join('|')})`);
|
|
125
|
+
}
|
|
126
|
+
// Ogni prompt del catalogo nomina il TaskID: senza task produrrebbe un
|
|
127
|
+
// comando monco (`/loom-works:run-task ` con l'id vuoto). Il testo letterale
|
|
128
|
+
// invece è ammesso con `--no-task` (T134/D9): non ha una task da nominare.
|
|
129
|
+
if (req.noTask && req.promptKind) {
|
|
130
|
+
return usageError('--prompt-kind richiede una task: una sessione --no-task non ha prompt iniziale');
|
|
131
|
+
}
|
|
132
|
+
// Due modi di dire la stessa cosa: accettarli insieme obbligherebbe a
|
|
133
|
+
// stabilire quale vince.
|
|
134
|
+
if (req.promptGiven && req.promptKind) {
|
|
135
|
+
return usageError('--prompt e --prompt-kind sono mutuamente esclusivi (il testo o il simbolo, non entrambi)');
|
|
136
|
+
}
|
|
137
|
+
return req;
|
|
138
|
+
}
|
|
139
|
+
export function isUsageError(r) {
|
|
140
|
+
return 'lines' in r;
|
|
141
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// T176 — la COMPOSIZIONE di una tab di conversazione: dai quattro assi di
|
|
2
|
+
// `deck-run` (`tab-args.ts`) a titolo, flag di `claude`, env di sessione e
|
|
3
|
+
// comando in-tab.
|
|
4
|
+
//
|
|
5
|
+
// Un composer solo per Linux e Windows (D1 preflight). Cambia per piattaforma
|
|
6
|
+
// solo CHI riceve il risultato: su Linux una riga per `bash -lc` dentro una tab
|
|
7
|
+
// Ptyxis, su Windows l'argv di `claude` in una variabile d'ambiente letta dal
|
|
8
|
+
// wrapper della tab (`windows-terminal.ts`). Titolo, modello, permission mode e
|
|
9
|
+
// prompt si risolvono qui una volta, per entrambe.
|
|
10
|
+
//
|
|
11
|
+
// Modulo PURO: il file di progetto, il catalogo dei prompt e l'env arrivano già
|
|
12
|
+
// letti. Le regole e i messaggi sono quelli dello script bash che questo modulo
|
|
13
|
+
// ha sostituito, e il gate di parità su Linux (`test/deck-run.test.ts`) li
|
|
14
|
+
// misura sul comando in-tab.
|
|
15
|
+
import { saneNote } from './sane-note.js';
|
|
16
|
+
import { MODEL_DEFAULT, MODELS } from './spawn-catalog.js';
|
|
17
|
+
import { shellQuote } from './spawn.js';
|
|
18
|
+
export const PERMISSION_MODES = ['acceptEdits', 'auto', 'bypassPermissions', 'manual', 'dontAsk', 'plan'];
|
|
19
|
+
/** L'alfabeto della chiave di stato: il valore entra in `bash -lc "…"` e il
|
|
20
|
+
* file di progetto è editabile a mano, quindi si scarta invece di quotare. */
|
|
21
|
+
const STATE_KEY_RE = /^[A-Za-z0-9_-]+$/;
|
|
22
|
+
/**
|
|
23
|
+
* Un campo del file di progetto come lo leggeva `jq -r '.campo // empty'`:
|
|
24
|
+
* `null`, `false` o assente danno vuoto, una stringa passa com'è, ogni altro
|
|
25
|
+
* valore diventa il suo JSON (`jq -r` stampa così numeri e oggetti).
|
|
26
|
+
*/
|
|
27
|
+
function configField(config, key) {
|
|
28
|
+
if (!config || typeof config !== 'object' || Array.isArray(config))
|
|
29
|
+
return '';
|
|
30
|
+
const v = config[key];
|
|
31
|
+
if (v === undefined || v === null || v === false)
|
|
32
|
+
return '';
|
|
33
|
+
return typeof v === 'string' ? v : JSON.stringify(v);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* La label del progetto, `{emoji} {name}`: la chiave con cui compass riconosce
|
|
37
|
+
* la finestra cercando nel titolo della tab attiva. Vuota se il file manca o
|
|
38
|
+
* non porta entrambi i campi, e il titolo ricade su `cc`.
|
|
39
|
+
*/
|
|
40
|
+
export function projectLabel(config) {
|
|
41
|
+
const emoji = configField(config, 'emoji');
|
|
42
|
+
const name = configField(config, 'name');
|
|
43
|
+
return emoji && name ? `${emoji} ${name}` : '';
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Il titolo della tab: label + `· <task>` + nota + `· fork`.
|
|
47
|
+
*
|
|
48
|
+
* La label sta in TESTA perché il match di compass è un `.includes` sulla
|
|
49
|
+
* chiave, e tutto il resto va in coda per non spezzarla. Il suffisso task
|
|
50
|
+
* distingue le tab di più task, la nota (T64) quelle di più conversazioni sulla
|
|
51
|
+
* stessa task, `· fork` (T28) un ramo dalla sua origine. La nota è l'unico testo
|
|
52
|
+
* libero del titolo: non si quota, si RIDUCE (`saneNote`) — e ridotta a vuoto
|
|
53
|
+
* non lascia nessun suffisso.
|
|
54
|
+
*/
|
|
55
|
+
export function tabTitle(req, label) {
|
|
56
|
+
let title;
|
|
57
|
+
if (req.noTask)
|
|
58
|
+
title = label || 'cc';
|
|
59
|
+
else
|
|
60
|
+
title = label ? `${label} · ${req.task}` : `cc ${req.task}`;
|
|
61
|
+
if (req.titleNote) {
|
|
62
|
+
const note = saneNote(req.titleNote);
|
|
63
|
+
if (note)
|
|
64
|
+
title = `${title} ${note}`;
|
|
65
|
+
}
|
|
66
|
+
if (req.fork)
|
|
67
|
+
title = `${title} · fork`;
|
|
68
|
+
return title;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Il permission mode: env `LOOM_DECK_PERMISSION_MODE` > campo del file > `manual`.
|
|
72
|
+
*
|
|
73
|
+
* Passato SEMPRE al CLI, anche `manual`: lo spawn resta deterministico invece
|
|
74
|
+
* di dipendere da un default che cambia fra versioni. Un valore fuori elenco
|
|
75
|
+
* non arriva al CLI — produrrebbe una tab con un comando rotto — e degrada a
|
|
76
|
+
* `manual` con un avviso.
|
|
77
|
+
*/
|
|
78
|
+
function permissionMode(ctx, warnings) {
|
|
79
|
+
const raw = ctx.env.LOOM_DECK_PERMISSION_MODE || configField(ctx.config, 'permissionMode');
|
|
80
|
+
if (!raw)
|
|
81
|
+
return 'manual';
|
|
82
|
+
if (PERMISSION_MODES.includes(raw))
|
|
83
|
+
return raw;
|
|
84
|
+
warnings.push(`permissionMode ignoto: '${raw}' → fallback 'manual'`);
|
|
85
|
+
return 'manual';
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Il kind di catalogo effettivo: solo sul ramo bound senza prompt letterale.
|
|
89
|
+
* `--no-task` e `--prompt` non hanno una riga di catalogo da consultare, né per
|
|
90
|
+
* il prompt né per il modello.
|
|
91
|
+
*/
|
|
92
|
+
function catalogKind(req) {
|
|
93
|
+
return !req.promptGiven && !req.noTask ? req.promptKind || 'recap' : '';
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Il modello: `--model` > env `LOOM_DECK_MODEL` > modello del kind nel catalogo
|
|
97
|
+
* > `MODEL_DEFAULT`.
|
|
98
|
+
*
|
|
99
|
+
* Il deck TUI passa SEMPRE `--model` già risolto (T161), quindi dal deck la
|
|
100
|
+
* cascata si ferma al primo gradino: i tre sotto servono a chi invoca a mano.
|
|
101
|
+
* Le voci restano ALIAS fino dentro `claude --model`: un id versionato cablato
|
|
102
|
+
* qui diventerebbe un modello inesistente al primo cambio di generazione. Un
|
|
103
|
+
* valore ignoto degrada al default con un avviso — arriverebbe al CLI, che lo
|
|
104
|
+
* rifiuterebbe all'avvio.
|
|
105
|
+
*/
|
|
106
|
+
function modelOf(req, ctx, warnings) {
|
|
107
|
+
let model = req.model || ctx.env.LOOM_DECK_MODEL || '';
|
|
108
|
+
const kind = catalogKind(req);
|
|
109
|
+
if (!model && kind && kind !== 'none')
|
|
110
|
+
model = ctx.catalog.get(kind)?.model ?? '';
|
|
111
|
+
if (!model)
|
|
112
|
+
return MODEL_DEFAULT;
|
|
113
|
+
if (MODELS.includes(model))
|
|
114
|
+
return model;
|
|
115
|
+
warnings.push(`modello ignoto: '${model}' → fallback '${MODEL_DEFAULT}' (usa ${MODELS.join('|')})`);
|
|
116
|
+
return MODEL_DEFAULT;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Il prompt iniziale, vuoto = nessun prompt.
|
|
120
|
+
*
|
|
121
|
+
* `--prompt` vince su tutto ed è usato così com'è. Una sessione nuda senza
|
|
122
|
+
* `--prompt` non ha prompt: il default `recap` del catalogo non si applica,
|
|
123
|
+
* perché ogni template interpola `{TASK}` e con la task vuota sarebbe un
|
|
124
|
+
* comando monco. Sul ramo bound il kind sceglie il template; un catalogo che
|
|
125
|
+
* non ha la voce è un'installazione mutila, e la sessione parte senza prompt
|
|
126
|
+
* dicendolo. `LOOM_DECK_ENTER_PROMPT` vince sul kind, tranne su `none`.
|
|
127
|
+
*/
|
|
128
|
+
function promptOf(req, ctx, warnings) {
|
|
129
|
+
if (req.promptGiven)
|
|
130
|
+
return req.promptText;
|
|
131
|
+
if (req.noTask)
|
|
132
|
+
return '';
|
|
133
|
+
const kind = catalogKind(req);
|
|
134
|
+
if (kind === 'none')
|
|
135
|
+
return '';
|
|
136
|
+
const enter = ctx.env.LOOM_DECK_ENTER_PROMPT;
|
|
137
|
+
const template = ctx.catalog.get(kind)?.template;
|
|
138
|
+
if (template === undefined) {
|
|
139
|
+
warnings.push(`catalogo prompt non leggibile ('${ctx.catalogPath}'): sessione senza prompt iniziale`);
|
|
140
|
+
}
|
|
141
|
+
const text = enter || template || '';
|
|
142
|
+
return text.replaceAll('{TASK}', req.task);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* La chiave di stato per compass, solo su Linux: env `LOOM_DECK_STATE_PROFILE`
|
|
146
|
+
* (anche vuota = nessun annuncio) > `id` del file di progetto.
|
|
147
|
+
*
|
|
148
|
+
* Lo stato di una sessione viaggia verso compass via D-Bus keyed su
|
|
149
|
+
* `PTYXIS_PROFILE`, e compass lo associa a un progetto solo se la chiave è l'`id`
|
|
150
|
+
* di un progetto del registry. Senza la variabile forzata in-tab, Ptyxis ne
|
|
151
|
+
* esporterebbe una col valore del profilo di default, e il badge resterebbe
|
|
152
|
+
* fermo senza nessun errore. Il nome della variabile è storico: non contiene
|
|
153
|
+
* l'UUID di un profilo. Su Windows compass non esiste, e la chiave non serve.
|
|
154
|
+
*/
|
|
155
|
+
function stateKey(ctx, warnings) {
|
|
156
|
+
if (ctx.platform === 'win32')
|
|
157
|
+
return '';
|
|
158
|
+
const raw = ctx.env.LOOM_DECK_STATE_PROFILE !== undefined
|
|
159
|
+
? ctx.env.LOOM_DECK_STATE_PROFILE
|
|
160
|
+
: configField(ctx.config, 'id');
|
|
161
|
+
if (raw && !STATE_KEY_RE.test(raw)) {
|
|
162
|
+
warnings.push(`profilo di stato ignorato (caratteri non ammessi): '${raw}'`);
|
|
163
|
+
return '';
|
|
164
|
+
}
|
|
165
|
+
return raw;
|
|
166
|
+
}
|
|
167
|
+
/** Un testo come UN argomento di una shell POSIX: sempre fra apici singoli, con
|
|
168
|
+
* l'apice stesso chiuso, escapato e riaperto (`'\''`). Dentro gli apici nessun
|
|
169
|
+
* altro carattere resta speciale, newline compresi. */
|
|
170
|
+
export function posixQuote(text) {
|
|
171
|
+
return `'${text.replace(/'/g, `'\\''`)}'`;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* La composizione intera.
|
|
175
|
+
*
|
|
176
|
+
* Ordine degli argomenti di `claude`: nome, permission mode e modello prima dei
|
|
177
|
+
* flag di continuità, il prompt sempre ultimo. Il modello sta accanto al
|
|
178
|
+
* permission mode perché sono la stessa cosa — la scelta di chi apre la
|
|
179
|
+
* sessione — e non una proprietà della task né della ripresa.
|
|
180
|
+
*
|
|
181
|
+
* Su una ripresa il prompt salta anche sul ramo bound: riprendere significa
|
|
182
|
+
* continuare la conversazione, non iniettarle un messaggio nuovo; il fork è una
|
|
183
|
+
* ripresa con id nuovo e ricade nello stesso ramo. Il `--session-id` vale invece
|
|
184
|
+
* anche sul ramo nudo: nel fork di una sessione spot rende noto in anticipo
|
|
185
|
+
* l'id del ramo, senza il quale il lineage non sarebbe registrabile.
|
|
186
|
+
*/
|
|
187
|
+
export function composeTab(req, ctx) {
|
|
188
|
+
const warnings = [];
|
|
189
|
+
const title = tabTitle(req, projectLabel(ctx.config));
|
|
190
|
+
const mode = permissionMode(ctx, warnings);
|
|
191
|
+
const model = modelOf(req, ctx, warnings);
|
|
192
|
+
const key = stateKey(ctx, warnings);
|
|
193
|
+
const prompt = promptOf(req, ctx, warnings);
|
|
194
|
+
const withPrompt = prompt !== '' && !req.resumeId;
|
|
195
|
+
const flags = ['--permission-mode', mode, '--model', model];
|
|
196
|
+
if (req.sessionId)
|
|
197
|
+
flags.push('--session-id', req.sessionId);
|
|
198
|
+
if (req.resumeId)
|
|
199
|
+
flags.push('--resume', req.resumeId);
|
|
200
|
+
if (req.fork)
|
|
201
|
+
flags.push('--fork-session');
|
|
202
|
+
const sessionEnv = {};
|
|
203
|
+
if (key)
|
|
204
|
+
sessionEnv.PTYXIS_PROFILE = key;
|
|
205
|
+
if (!req.noTask)
|
|
206
|
+
sessionEnv.LOOM_TASK = req.task;
|
|
207
|
+
// La riga in-tab nella grafia storica dello script: ogni flag seguito da uno
|
|
208
|
+
// spazio, il prompt in coda fra apici. Il gate di parità su Linux confronta
|
|
209
|
+
// questa riga byte per byte, spazio finale compreso quando il prompt manca.
|
|
210
|
+
const envPrefix = Object.entries(sessionEnv)
|
|
211
|
+
.map(([k, v]) => `${k}=${shellQuote(v)} `)
|
|
212
|
+
.join('');
|
|
213
|
+
const flagText = flags.map((f) => `${f} `).join('');
|
|
214
|
+
const inTab = `${envPrefix}claude --name ${posixQuote(title)} ${flagText}` +
|
|
215
|
+
(withPrompt ? posixQuote(prompt) : '');
|
|
216
|
+
return {
|
|
217
|
+
title,
|
|
218
|
+
claudeArgs: ['--name', title, ...flags, ...(withPrompt ? [prompt] : [])],
|
|
219
|
+
sessionEnv,
|
|
220
|
+
inTab,
|
|
221
|
+
warnings,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* L'argv di Ptyxis: una tab nella finestra attiva (quella col focus, cioè il
|
|
226
|
+
* deck), o una finestra nuova con `--new-window` (T162) per chi chiama da fuori
|
|
227
|
+
* dalla finestra del progetto. Il comando gira dentro `bash -lc`, e le env di
|
|
228
|
+
* sessione viaggiano DENTRO il comando: `ptyxis --tab` consegna la tab
|
|
229
|
+
* all'istanza già aperta, che non eredita l'env di chi la chiede.
|
|
230
|
+
*/
|
|
231
|
+
export function ptyxisArgv(newWindow, workdir, title, inTab) {
|
|
232
|
+
return [newWindow ? '--new-window' : '--tab', '-d', workdir, '-T', title, '--', 'bash', '-lc', inTab];
|
|
233
|
+
}
|