@lamemind/loom-deck 0.76.0 → 0.77.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 +23 -1
- package/dist/actions.js +32 -5
- package/dist/config.js +1 -1
- package/dist/deck-run.js +7 -0
- package/dist/doc-tree.js +1 -1
- package/dist/inbox.js +1 -1
- package/dist/platform.js +1 -2
- package/dist/project-path.js +31 -0
- 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/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/README.md
CHANGED
|
@@ -387,11 +387,33 @@ npx @lamemind/loom-deck
|
|
|
387
387
|
## Requisiti runtime
|
|
388
388
|
|
|
389
389
|
- **Node.js ≥ 18** (dichiarato in `engines`).
|
|
390
|
-
- **[Ptyxis](https://gitlab.gnome.org/chergert/ptyxis)** (terminale GNOME) — **dipendenza di runtime, non risolvibile da npm**. Lo spawn di una tab (`scripts/deck-run`) invoca il binario `ptyxis`: l'**install riesce anche senza**, ma al momento dello spawn il comando
|
|
390
|
+
- **[Ptyxis](https://gitlab.gnome.org/chergert/ptyxis)** (terminale GNOME), su Linux — **dipendenza di runtime, non risolvibile da npm**. Lo spawn di una tab (`scripts/deck-run`) invoca il binario `ptyxis`: l'**install riesce anche senza**, ma al momento dello spawn il comando
|
|
391
391
|
fallisce (gestito: handler `error` → la TUI resta viva, mostra la nota). Senza una GUI
|
|
392
392
|
GNOME con Ptyxis installato, il deck naviga i task ma **non apre sessioni**.
|
|
393
393
|
- **[Claude Code](https://claude.com/claude-code)** nel `PATH` — la tab spawnata avvia `claude`.
|
|
394
394
|
|
|
395
|
+
### Su Windows
|
|
396
|
+
|
|
397
|
+
Le tab si aprono in **Windows Terminal** (`wt.exe`, l'alias d'esecuzione che l'installazione dello Store mette nel `PATH`), nella finestra usata per ultima: `wt.exe -w 0 new-tab -p loom-deck -d <root> --title <titolo> --suppressApplicationTitle -- <node> <dist/tab-wrapper.js>`. L'argv di `claude` — nome, flag, prompt — non sta su quella riga: viaggia come JSON nella variabile `LOOM_DECK_TAB_ARGV`, che la tab eredita dal processo che lancia `wt.exe`, e il wrapper lo passa a `claude` senza shell. Il prompt non passa quindi né dal parser di `wt`, che legge `;` come separatore di sottocomandi in qualunque argomento, né da cmd o PowerShell.
|
|
398
|
+
|
|
399
|
+
Le tab usano il profilo di Windows Terminal **`loom-deck`**, uno solo per tutti i progetti. Il deck non lo crea e non scrive `settings.json`: lo si aggiunge a mano in `%LOCALAPPDATA%\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json`, sotto `profiles.list`, nascosto dal menu. Porta solo ciò che i flag di `wt` non sanno dire — titolo, cartella e comando li passa il deck, e battono quelli del profilo:
|
|
400
|
+
|
|
401
|
+
```jsonc
|
|
402
|
+
{
|
|
403
|
+
"name": "loom-deck",
|
|
404
|
+
"guid": "{<un GUID nuovo>}",
|
|
405
|
+
"hidden": true,
|
|
406
|
+
"commandline": "%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe -NoLogo",
|
|
407
|
+
"closeOnExit": "graceful"
|
|
408
|
+
}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
`commandline` è la shell del tasto `t`; nelle conversazioni il comando del deck prende il suo posto. `closeOnExit` decide cosa resta della tab quando `claude` esce (il wrapper esce col suo codice). Senza il profilo la tab si apre comunque, col profilo di default. Il nome si cambia con `LOOM_DECK_WT_PROFILE` (vuota = nessun `-p`).
|
|
412
|
+
|
|
413
|
+
La tab eredita tutto l'env del deck, tranne i marker che una sessione Claude Code inietta nei figli (`CLAUDECODE`, `CLAUDE_CODE_CHILD_SESSION`, …): con quelli una conversazione aperta da un deck lanciato dentro Claude Code non salverebbe il transcript. Se manca, il deck aggiunge `CLAUDE_CODE_GIT_BASH_PATH`: col `PATH` di Git Bash `claude` eseguirebbe gli hook del plugin in PowerShell, e la task non arriverebbe in contesto.
|
|
414
|
+
|
|
415
|
+
Su Windows il resume di una conversazione e il restore (`^T`) restano spenti con un avviso finché il deck non sa quali conversazioni sono vive: senza quella informazione un resume può riaprire una conversazione già aperta in un'altra tab. Il fork resta acceso, perché apre un transcript nuovo.
|
|
416
|
+
|
|
395
417
|
## Uso (spike ①)
|
|
396
418
|
|
|
397
419
|
```bash
|
package/dist/actions.js
CHANGED
|
@@ -17,13 +17,15 @@
|
|
|
17
17
|
// migliorano di passaggio.
|
|
18
18
|
import { useRef } from 'react';
|
|
19
19
|
import { randomUUID } from 'node:crypto';
|
|
20
|
+
import { existsSync } from 'node:fs';
|
|
21
|
+
import { conversationCwd } from './project-path.js';
|
|
20
22
|
import { taskSlug } from './tasks.js';
|
|
21
23
|
import { appendNote, appendPin, appendPriority, appendSessionRecord, appendTaskBinding, } from './task-index.js';
|
|
22
24
|
import { neighborId } from './session-list.js';
|
|
23
25
|
import { cut, cutMiddle } from './width.js';
|
|
24
26
|
import { dropSessionFiles, sessionArtifacts } from './session-drop.js';
|
|
25
27
|
import { saveView, viewFilePath } from './view-store.js';
|
|
26
|
-
import { onInTabCommand, runLaunch, spawnClaudeEmpty, spawnBare, spawnDeck, spawnDeckFork, spawnDeckResume, spawnTerminal, DECK_RUN, } from './spawn.js';
|
|
28
|
+
import { onInTabCommand, runLaunch, spawnClaudeEmpty, spawnBare, spawnDeck, spawnDeckFork, spawnDeckResume, spawnTerminal, terminalName, DECK_RUN, } from './spawn.js';
|
|
27
29
|
import { MODEL_DEFAULT } from './spawn-catalog.js';
|
|
28
30
|
import { modelAlias } from './glyphs.js';
|
|
29
31
|
import { resolveSpawnId } from './spawn-config.js';
|
|
@@ -103,7 +105,7 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, overr
|
|
|
103
105
|
}
|
|
104
106
|
/** I buchi di un'azione su task. Lo slug viene dal NOME del task file, non
|
|
105
107
|
* dalla descrizione di `tasks.md`: il nome è già dentro l'alfabeto di
|
|
106
|
-
* `
|
|
108
|
+
* `saneNote` per costruzione — minuscolo, separato da trattini, senza
|
|
107
109
|
* punteggiatura — mentre la descrizione porta apostrofi e `/` che la riduzione
|
|
108
110
|
* toglie senza sostituto, saldando le parole. Task file assente → slug vuoto,
|
|
109
111
|
* e il titolo si spegne invece di ridursi al solo prefisso. */
|
|
@@ -220,9 +222,22 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, overr
|
|
|
220
222
|
// ricerca e il restore il modello ereditato dalla sessione senza superficie
|
|
221
223
|
// di scelta (D1 preflight). I chiamanti restano la stessa azione perché è
|
|
222
224
|
// lo spawn a essere identico, non la sorgente del valore.
|
|
225
|
+
/**
|
|
226
|
+
* T176 — la cartella della tab di una ripresa o di un fork: la root su Linux,
|
|
227
|
+
* la cwd registrata dalla conversazione su Windows (`conversationCwd`, P7
|
|
228
|
+
* preflight). Una cartella che non esiste più ricade sulla root: lo spawn
|
|
229
|
+
* fallirebbe prima ancora di aprire la tab.
|
|
230
|
+
*/
|
|
231
|
+
function tabCwdOf(sessionId) {
|
|
232
|
+
const s = model.sessions.find((x) => x.sessionId === sessionId);
|
|
233
|
+
if (!s)
|
|
234
|
+
return cwd;
|
|
235
|
+
const dir = conversationCwd(s.cwd, s.path, cwd);
|
|
236
|
+
return dir === cwd || existsSync(dir) ? dir : cwd;
|
|
237
|
+
}
|
|
223
238
|
function resumeSession(sessionId, modelKind) {
|
|
224
239
|
const bound = model.bindings.get(sessionId) ?? null;
|
|
225
|
-
const spawned = spawnDeckResume(bound,
|
|
240
|
+
const spawned = spawnDeckResume(bound, tabCwdOf(sessionId), sessionId, modelKind, model.sessionNotes.get(sessionId));
|
|
226
241
|
spawned.child.on('error', () => setNote(`⚠ resume fallito (${DECK_RUN})`));
|
|
227
242
|
noteSpawn(spawned);
|
|
228
243
|
return spawned;
|
|
@@ -242,6 +257,12 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, overr
|
|
|
242
257
|
function openSession(sessionId, modelKind) {
|
|
243
258
|
if (refuse('spawn', '⏎', setNote, 'resume'))
|
|
244
259
|
return;
|
|
260
|
+
// T176 — il resume vuole sapere se la conversazione è viva, non solo poter
|
|
261
|
+
// aprire una tab (D4 preflight): senza liveness un `⏎` su una conversazione
|
|
262
|
+
// aperta in un'altra tab avvierebbe un secondo `claude` sullo stesso
|
|
263
|
+
// transcript. Spento finché la piattaforma non ha le sessioni vive.
|
|
264
|
+
if (refuse('liveSessions', '⏎', setNote, 'resume'))
|
|
265
|
+
return;
|
|
245
266
|
enterSession(sessionId, {
|
|
246
267
|
isLive: (id) => model.live.has(id),
|
|
247
268
|
nameOf: (id) => model.live.get(id)?.name || id.slice(0, 8),
|
|
@@ -275,6 +296,12 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, overr
|
|
|
275
296
|
* chiudendo le tab.
|
|
276
297
|
*/
|
|
277
298
|
function restoreSessions() {
|
|
299
|
+
// T176 — la guardia delle sessioni vive PRIMA di quella dello spawn (P8
|
|
300
|
+
// preflight): l'insieme da riaprire è l'ultimo insieme vivo, e senza
|
|
301
|
+
// liveness sarebbe vuoto — «nessuna conversazione da riaprire» direbbe uno
|
|
302
|
+
// stato dove manca una funzione.
|
|
303
|
+
if (refuse('liveSessions', '^T', setNote, 'restore delle conversazioni'))
|
|
304
|
+
return;
|
|
278
305
|
if (refuse('spawn', '^T', setNote, 'restore delle conversazioni'))
|
|
279
306
|
return;
|
|
280
307
|
const list = model.restorable;
|
|
@@ -406,7 +433,7 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, overr
|
|
|
406
433
|
// T148 — il fork è l'altra continuità che condivide la stessa riga
|
|
407
434
|
// selezionata: porta via lo stesso `model.resumeModel` del resume, non un
|
|
408
435
|
// default fisso (P2 preflight).
|
|
409
|
-
const spawned = spawnDeckFork(bound,
|
|
436
|
+
const spawned = spawnDeckFork(bound, tabCwdOf(s.sessionId), s.sessionId, newId, model.resumeModel);
|
|
410
437
|
spawned.child.on('error', () => setNote(`⚠ fork fallito (${DECK_RUN})`));
|
|
411
438
|
noteSpawn(spawned);
|
|
412
439
|
}
|
|
@@ -533,7 +560,7 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, overr
|
|
|
533
560
|
return;
|
|
534
561
|
const title = model.identity ? `🖥️ ${model.identity.name} [term]` : null;
|
|
535
562
|
const child = spawnTerminal(cwd, title);
|
|
536
|
-
child.on('error', () => setNote(
|
|
563
|
+
child.on('error', () => setNote(`⚠ t → ${terminalName()} non lanciabile`));
|
|
537
564
|
setNote(`t → terminale su ${model.projectName}`);
|
|
538
565
|
}
|
|
539
566
|
/**
|
package/dist/config.js
CHANGED
|
@@ -115,7 +115,7 @@ export function launchLegend(entries, columns, reserved = 0) {
|
|
|
115
115
|
// `identity` sopra sono strutture. La catena da percorrere si ferma qui: nessun
|
|
116
116
|
// passaggio da `lib-config.sh`/`reg_pull`/dconf, perché il solo consumer è il
|
|
117
117
|
// deck e il file ce l'ha sotto mano. Il precedente esatto è `permissionMode`,
|
|
118
|
-
// che vive nel file, lo legge `deck-run`
|
|
118
|
+
// che vive nel file, lo legge anche `deck-run` e non è propagato al registry.
|
|
119
119
|
/** Interi positivi soltanto: uno 0 spegnerebbe la soglia (tutte le Done
|
|
120
120
|
* archiviabili), un negativo o un decimale sono un typo. Valore fuori dominio
|
|
121
121
|
* → default, mai passaggio cieco di un numero senza senso al conteggio. */
|
package/dist/deck-run.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// T176 — il punto d'ingresso di `deck-run`: `scripts/deck-run` (lo shim bash
|
|
2
|
+
// che compass e i test lanciano) e il deck su Windows eseguono questo file col
|
|
3
|
+
// proprio node. La logica sta in `tab-launch.ts`, che si testa senza aprire tab.
|
|
4
|
+
import { realIo, runDeckRun } from './tab-launch.js';
|
|
5
|
+
runDeckRun(process.argv.slice(2), realIo()).then((code) => {
|
|
6
|
+
process.exitCode = code;
|
|
7
|
+
});
|
package/dist/doc-tree.js
CHANGED
|
@@ -376,7 +376,7 @@ export function docTargetKind(row, docsRoot) {
|
|
|
376
376
|
* T160 — il buco `{words}` del titolo di una sessione di rebalance: il path reso
|
|
377
377
|
* a parole.
|
|
378
378
|
*
|
|
379
|
-
* Gemello di `wrapWords`, e per la stessa ragione: l'alfabeto di `
|
|
379
|
+
* Gemello di `wrapWords`, e per la stessa ragione: l'alfabeto di `saneNote`
|
|
380
380
|
* non ha né `/` né `.`, quindi un path lasciato cadere lì dentro esce con le
|
|
381
381
|
* parole saldate (`runtimereferencedoc-systemmd`) mentre la lista del deck, che
|
|
382
382
|
* legge la nota grezza dal sidecar, mostrerebbe il path intero — la stessa
|
package/dist/inbox.js
CHANGED
|
@@ -200,7 +200,7 @@ export function inboxPrompt(f) {
|
|
|
200
200
|
* riscriverla.
|
|
201
201
|
*
|
|
202
202
|
* Il `.md` si toglie QUI e non lo si lascia cadere a valle: l'alfabeto di
|
|
203
|
-
* `
|
|
203
|
+
* `saneNote` non ha il punto, quindi il titolo della tab uscirebbe con le due
|
|
204
204
|
* lettere saldate al nome (`…-prioritariamd`) mentre la lista del deck, che
|
|
205
205
|
* legge la nota grezza dal sidecar, mostrerebbe il nome intero — la stessa
|
|
206
206
|
* conversazione con due nomi, senza errore.
|
package/dist/platform.js
CHANGED
|
@@ -17,8 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
export const UNAVAILABLE = {
|
|
19
19
|
win32: [
|
|
20
|
-
'
|
|
21
|
-
'liveSessions', // T177 — liveness senza /proc
|
|
20
|
+
'liveSessions', // T177 — liveness senza /proc; spegne anche resume e restore
|
|
22
21
|
'pluginScripts', // T178 — .sh del plugin via Git Bash
|
|
23
22
|
'hardWrap', // T178 — python del wrap-scan
|
|
24
23
|
'launch', // T179 — comando per piattaforma nelle voci launch[]
|
package/dist/project-path.js
CHANGED
|
@@ -69,3 +69,34 @@ export function matchingDirNames(entries, name, platform = process.platform) {
|
|
|
69
69
|
const want = name.toLowerCase();
|
|
70
70
|
return entries.filter((e) => e.toLowerCase() === want);
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* T176 — la cartella in cui nasce la tab che riprende o forka una conversazione.
|
|
74
|
+
*
|
|
75
|
+
* Su Linux la root del progetto, come prima. Su win32 la cwd che la
|
|
76
|
+
* conversazione ha registrato, in forma Windows (P7 preflight): `claude
|
|
77
|
+
* --resume` cerca il transcript nella cartella che ricava dalla PROPRIA cwd, e
|
|
78
|
+
* Claude Code non normalizza la lettera del drive (T174 verdetto 1) — una
|
|
79
|
+
* conversazione nata da VS Code (`c:\…`) sta in `c--…`, e un `claude` partito in
|
|
80
|
+
* `C:\…` la cercherebbe in `C--…`, senza trovarla.
|
|
81
|
+
*
|
|
82
|
+
* Una cwd in forma MSYS (`/c/…`) non dice la grafia del drive: la dice il nome
|
|
83
|
+
* della cartella del transcript, che Claude Code ha derivato dalla cwd vera
|
|
84
|
+
* (`c--…` o `C--…`). Una cwd che non è un path assoluto Windows ricade sulla
|
|
85
|
+
* root.
|
|
86
|
+
*/
|
|
87
|
+
export function conversationCwd(sessionCwd, transcriptPath, root, platform = process.platform) {
|
|
88
|
+
if (platform !== 'win32')
|
|
89
|
+
return root;
|
|
90
|
+
if (/^[a-zA-Z]:[\\/]/.test(sessionCwd))
|
|
91
|
+
return sessionCwd.replace(/\//g, '\\');
|
|
92
|
+
if (sessionCwd.startsWith('\\\\'))
|
|
93
|
+
return sessionCwd;
|
|
94
|
+
const msys = /^\/([a-zA-Z])(?:\/(.*))?$/.exec(sessionCwd);
|
|
95
|
+
if (!msys)
|
|
96
|
+
return root;
|
|
97
|
+
const folder = win32.basename(win32.dirname(transcriptPath));
|
|
98
|
+
const letter = folder.length > 2 && folder.slice(1, 3) === '--' && folder[0].toLowerCase() === msys[1].toLowerCase()
|
|
99
|
+
? folder[0]
|
|
100
|
+
: msys[1].toUpperCase();
|
|
101
|
+
return `${letter}:\\${(msys[2] ?? '').replace(/\//g, '\\')}`;
|
|
102
|
+
}
|
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
|
+
}
|