@lamemind/loom-deck 0.40.0 → 0.42.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 -0
- package/dist/cli.js +69 -24
- package/dist/overlays/sheet.js +1 -1
- package/dist/spawn.js +89 -28
- package/dist/width.js +50 -0
- package/package.json +1 -1
- package/scripts/deck-run +24 -0
package/README.md
CHANGED
|
@@ -26,6 +26,30 @@ Entrambi i prefissi del contratto loom entrano in lista: **`T`** (code task) e *
|
|
|
26
26
|
Il deck è **UN processo Node**: spawna ma **non contiene** le sessioni CC — le possiede Ptyxis. Chiudere il deck non uccide le sessioni. La tab nasce nella window *attiva*
|
|
27
27
|
(quella col focus = il deck) → desktop isolation "gratis".
|
|
28
28
|
|
|
29
|
+
### La riga di stato dice il comando, non una parafrasi
|
|
30
|
+
|
|
31
|
+
Ogni spawn di una sessione Claude — task (`⏎`/`^K`/`^P`/`^R`), resume, fork, sessione nuda (`c`) — scrive nella riga di stato in fondo al frame il **comando esatto** che gira dentro la tab, cioè l'invocazione `claude` vera con le env che la legano a task e progetto:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
$ LOOM_TASK=T115 claude --name '🧵 loom-works · T115 «parser»' --permission-mode auto --model opus --session-id 9f3a… '/loom-works:run-task T115'
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Task, `sessionId`, modello, titolo e prompt iniziale sono già lì dentro: una nota che li elencasse a parole li direbbe una seconda volta, in una grafia che non si può ricopiare in un terminale. Quello che si perde è il **tasto premuto**, che è ciò che hai appena fatto tu e non ciò che il deck ha fatto per te.
|
|
38
|
+
|
|
39
|
+
**Il comando non lo compone il deck**, e non potrebbe senza riscrivere le stesse regole due volte: catalogo dei prompt, quoting, permission mode, profilo di stato e titolo vivono in `deck-run`. Il primitive lo **annuncia** su stdout prima di `exec`, con una riga
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
LOOM_DECK_INTAB <comando>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
e il deck la legge da lì. L'annuncio è utile anche a chi lancia `deck-run` a mano: dice cosa sta per partire, mentre le diagnostiche restano su stderr.
|
|
46
|
+
|
|
47
|
+
Ne discende che il comando in-tab arriva **asincrono**, qualche millisecondo dopo lo spawn. Nel frattempo la riga mostra il comando `deck-run` — che resta l'unica cosa visibile quando l'annuncio non arriva affatto (argomenti rifiutati, `deck-run` morto prima dell'exec), ed è anche il comando da ripetere a mano per vedere l'errore.
|
|
48
|
+
|
|
49
|
+
Su un terminale stretto il taglio è **al mezzo** (`…`), non dalla coda: in un comando di spawn è la coda a distinguere un'invocazione dall'altra, e tagliare da lì la butterebbe via per intero.
|
|
50
|
+
|
|
51
|
+
Restano fuori gli spawn che non aprono una sessione Claude interattiva: terminale (`t`), voci `launch` (`1`-`9`) e le skill headless (`C` create-task, `CANC` clean-tasks), dove la riga di stato serve a riportare l'**esito** di un'operazione asincrona.
|
|
52
|
+
|
|
29
53
|
## Stato
|
|
30
54
|
|
|
31
55
|
Bootstrap + spike ① + **TUI ③** funzionante (legge `tasks.md`, `⏎` apre il detail). Roadmap:
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { render, Box, Text, useInput } from 'ink';
|
|
4
|
-
import { useState, useEffect, useMemo } from 'react';
|
|
4
|
+
import { useState, useEffect, useMemo, useRef } from 'react';
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
6
6
|
import { resolveTasksPath, resolveTasksDir, loadTasks, loadTaskFileText, } from './tasks.js';
|
|
7
7
|
import { rowIndexOfKey, selectedRow, } from './search.js';
|
|
@@ -10,14 +10,14 @@ import { assembleSessionList, firstSelectableId, moveSelection, neighborId, rowI
|
|
|
10
10
|
import { cellWidth, launchLegend, loadArchivableDays, loadIdentity, loadLaunch, } from './config.js';
|
|
11
11
|
import { cycleSessionView, cycleTaskView, selectSessionRows, selectTasks, sessionView, taskView, TASK_VIEWS, } from './pane-views.js';
|
|
12
12
|
import { isCompact, layoutBudget, searchPreviewCapacity, windowRange, } from './viewport.js';
|
|
13
|
-
import { cut, sanitize, termWidth } from './width.js';
|
|
13
|
+
import { cut, cutMiddle, sanitize, termWidth } from './width.js';
|
|
14
14
|
import { applyView, cycleSort, describeSort, priName, progName, toggleHidden, PRI_ENTRIES, PROG_ENTRIES, } from './view.js';
|
|
15
15
|
import { initialDetail, writeTaskEdit, PRI_GLYPH, PRI_LABEL } from './task-edit.js';
|
|
16
16
|
import { ALL, EDIT_PRI, EDIT_PROG, MAX_SESSIONS, MAX_SESSIONS_ALL, META_ROWS, ROW_ALL, ROW_SPOT, SORT_TASTI, SPOT, } from './model.js';
|
|
17
17
|
import { purgeTargets, splitTargets } from './purge.js';
|
|
18
18
|
import { conversationLabel, cpLen, editField, insertAt, isDone, isTextRow, removeAt, EDIT_ROWS, } from './layout.js';
|
|
19
19
|
import { TASK_EMPTY, relTime, sanitizeTyped } from './glyphs.js';
|
|
20
|
-
import { commitTaskEdit, runLaunch, spawnClaudeEmpty, spawnCleanTasks, spawnCreateTask, spawnDeck, spawnDeckFork, spawnDeckResume, spawnTerminal, CLAUDE_CMD, DECK_RUN, MODEL_DEFAULT, } from './spawn.js';
|
|
20
|
+
import { commitTaskEdit, runLaunch, spawnClaudeEmpty, spawnCleanTasks, spawnCreateTask, spawnDeck, spawnDeckFork, spawnDeckResume, spawnTerminal, onInTabCommand, CLAUDE_CMD, DECK_RUN, MODEL_DEFAULT, } from './spawn.js';
|
|
21
21
|
import { EditModal, FilterModal, PurgeModal, SortModal, idList } from './ui/modals.js';
|
|
22
22
|
import { ReaderScreen, SearchScreen } from './ui/search-screen.js';
|
|
23
23
|
import { DetailScreen } from './ui/detail-screen.js';
|
|
@@ -273,6 +273,52 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
273
273
|
});
|
|
274
274
|
child.on('error', () => setNote(`⚠ create-task: '${CLAUDE_CMD}' non lanciabile`));
|
|
275
275
|
}
|
|
276
|
+
// La riga di stato di OGNI spawn di sessione Claude: il comando esatto, come
|
|
277
|
+
// lo si scriverebbe in bash, invece di una parafrasi.
|
|
278
|
+
//
|
|
279
|
+
// Non è un di più sulla nota descrittiva, la sostituisce: task, sessionId,
|
|
280
|
+
// prompt-kind, modello e nota del titolo sono già tutti argomenti del comando,
|
|
281
|
+
// quindi elencarli a parole li direbbe una seconda volta in una grafia che non
|
|
282
|
+
// si può ricopiare in un terminale. Ciò che va perso è il tasto premuto
|
|
283
|
+
// (`^K`, `⏎`, `f`), che è ciò che l'utente ha appena fatto e non ciò che il
|
|
284
|
+
// deck ha fatto per lui.
|
|
285
|
+
//
|
|
286
|
+
// Il taglio è al MEZZO (`cutMiddle`, non `cut`): in un comando di spawn è la
|
|
287
|
+
// coda a distinguere un'invocazione dall'altra, e un taglio dalla coda la
|
|
288
|
+
// butterebbe via per intero. Si taglia QUI, alla composizione, e non al
|
|
289
|
+
// render: la riga di stato porta anche messaggi normali, dove è la testa a
|
|
290
|
+
// contare. Ne discende che un resize successivo non ricalcola l'elisione — la
|
|
291
|
+
// nota è transitoria e `wrap="truncate-end"` resta come rete.
|
|
292
|
+
function noteCommand(cmd) {
|
|
293
|
+
// 4 = bordo + padding della cornice esterna, 2 = il prompt `$ `. Sbagliare
|
|
294
|
+
// il budget non produce un errore visibile: la `truncate-end` di Ink taglia
|
|
295
|
+
// il resto dalla CODA, e il comando esce col mezzo eliso E la fine persa —
|
|
296
|
+
// cioè con entrambi i pezzi che l'elisione al mezzo voleva salvare.
|
|
297
|
+
setNote(`$ ${cutMiddle(cmd, Math.max(8, columns - 6))}`);
|
|
298
|
+
}
|
|
299
|
+
// Le due note di uno spawn, in quest'ordine: il comando di `deck-run` subito,
|
|
300
|
+
// e appena arriva l'annuncio quello della sessione `claude` che gira DENTRO
|
|
301
|
+
// la tab — che è ciò che si vuole vedere davvero (`deck-run` è l'involucro,
|
|
302
|
+
// l'invocazione vera la compone lui).
|
|
303
|
+
//
|
|
304
|
+
// La prima non è un ripiego di stile: è l'unica che esiste quando l'annuncio
|
|
305
|
+
// non arriva (spawn inerte nei test, argomenti rifiutati, `deck-run` morto
|
|
306
|
+
// prima dell'exec), ed è anche il comando da ripetere a mano per vedere
|
|
307
|
+
// l'errore. Il salto fra le due dura i millisecondi che `deck-run` impiega a
|
|
308
|
+
// comporre.
|
|
309
|
+
const spawnSeq = useRef(0);
|
|
310
|
+
function noteSpawn(spawned) {
|
|
311
|
+
noteCommand(spawned.cmd);
|
|
312
|
+
const mine = ++spawnSeq.current;
|
|
313
|
+
onInTabCommand(spawned.child, (inTab) => {
|
|
314
|
+
// Due spawn ravvicinati: gli annunci sono asincroni e possono tornare
|
|
315
|
+
// fuori ordine, e senza guardia il più vecchio scriverebbe sopra il più
|
|
316
|
+
// recente — la riga di stato mostrerebbe un comando che non è l'ultimo
|
|
317
|
+
// partito.
|
|
318
|
+
if (spawnSeq.current === mine)
|
|
319
|
+
noteCommand(inTab);
|
|
320
|
+
});
|
|
321
|
+
}
|
|
276
322
|
// T56 — apre una sessione bound alla task selezionata. Punto UNICO dei quattro
|
|
277
323
|
// tasti (⏎/^K/^P/^R): fra loro cambia solo il prompt iniziale, tutto il resto
|
|
278
324
|
// è identico — uuid pinnato, binding scritto PRIMA dello spawn (la sessione
|
|
@@ -315,25 +361,24 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
315
361
|
// il tempo di un tick la riga in lista comparirebbe nuda. Due record separati
|
|
316
362
|
// sullo stesso `sessionId` sono la forma normale di un file append-only
|
|
317
363
|
// last-wins, non una scrittura da fondere.
|
|
318
|
-
function spawnForTask(id, kind, model,
|
|
364
|
+
function spawnForTask(id, kind, model, spawnNote = '') {
|
|
319
365
|
const sid = randomUUID();
|
|
320
366
|
appendTaskBinding(cwd, sid, id);
|
|
321
367
|
if (spawnNote)
|
|
322
368
|
appendNote(cwd, sid, spawnNote);
|
|
323
|
-
const
|
|
324
|
-
child.on('error', () => setNote(`⚠ spawn ${id} fallito (${DECK_RUN})`));
|
|
325
|
-
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
setNote(`${keyLabel} spawn ${id}${what} · ${model}${named} → tab CC (sid ${sid.slice(0, 8)})`);
|
|
369
|
+
const spawned = spawnDeck(id, cwd, sid, kind, model, spawnNote);
|
|
370
|
+
spawned.child.on('error', () => setNote(`⚠ spawn ${id} fallito (${DECK_RUN})`));
|
|
371
|
+
// Il modello resta SEMPRE visibile anche quando è il default, perché è un
|
|
372
|
+
// argomento esplicito del comando (T108): gli acceleratori della lista non
|
|
373
|
+
// passano dal selettore del detail e usano il default fisso, quindi senza
|
|
374
|
+
// vederlo l'utente crederebbe di aver ereditato la scelta dell'ultimo
|
|
375
|
+
// detail aperto.
|
|
376
|
+
noteSpawn(spawned);
|
|
332
377
|
}
|
|
333
378
|
function spawnTaskSession(kind, keyLabel) {
|
|
334
379
|
const task = selectedTaskOr(keyLabel, 'spawnare');
|
|
335
380
|
if (task)
|
|
336
|
-
spawnForTask(task.id, kind, MODEL_DEFAULT
|
|
381
|
+
spawnForTask(task.id, kind, MODEL_DEFAULT);
|
|
337
382
|
}
|
|
338
383
|
// T53 — apertura del modale nota sulla conversazione selezionata. Come
|
|
339
384
|
// `openEdit`, la bozza parte dal valore ATTUALE: annotare una seconda volta è
|
|
@@ -615,9 +660,9 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
615
660
|
// devono restare la stessa azione.
|
|
616
661
|
function resumeSession(sessionId) {
|
|
617
662
|
const bound = bindings.get(sessionId) ?? null;
|
|
618
|
-
const
|
|
619
|
-
child.on('error', () => setNote(`⚠ resume fallito (${DECK_RUN})`));
|
|
620
|
-
|
|
663
|
+
const spawned = spawnDeckResume(bound, cwd, sessionId, sessionNotes.get(sessionId));
|
|
664
|
+
spawned.child.on('error', () => setNote(`⚠ resume fallito (${DECK_RUN})`));
|
|
665
|
+
noteSpawn(spawned);
|
|
621
666
|
}
|
|
622
667
|
// T57 — ⏎ nel modale: riscrive il binding nel sidecar e ricarica subito.
|
|
623
668
|
//
|
|
@@ -655,7 +700,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
655
700
|
columns,
|
|
656
701
|
setMode,
|
|
657
702
|
setNote,
|
|
658
|
-
onAction: (id, kind, model, spawnNote
|
|
703
|
+
onAction: (id, kind, model, spawnNote) => spawnForTask(id, kind, model, spawnNote),
|
|
659
704
|
});
|
|
660
705
|
const search = useSearchOverlay({
|
|
661
706
|
sessions,
|
|
@@ -1021,9 +1066,9 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1021
1066
|
...(bound ? { taskId: bound } : {}),
|
|
1022
1067
|
forkOf: s.sessionId,
|
|
1023
1068
|
});
|
|
1024
|
-
const
|
|
1025
|
-
child.on('error', () => setNote(`⚠ fork fallito (${DECK_RUN})`));
|
|
1026
|
-
|
|
1069
|
+
const spawned = spawnDeckFork(bound, cwd, s.sessionId, newId);
|
|
1070
|
+
spawned.child.on('error', () => setNote(`⚠ fork fallito (${DECK_RUN})`));
|
|
1071
|
+
noteSpawn(spawned);
|
|
1027
1072
|
}
|
|
1028
1073
|
}
|
|
1029
1074
|
}
|
|
@@ -1100,9 +1145,9 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1100
1145
|
// Minuscola = azione immediata (convenzione T39), gemella di `t`: entrambe
|
|
1101
1146
|
// aprono una surface del cappello senza passare da un modale. `C` (create
|
|
1102
1147
|
// task) resta distinta — stessa lettera, ma la maiuscola è per i modali.
|
|
1103
|
-
const
|
|
1104
|
-
child.on('error', () => setNote(`⚠ c → spawn claude fallito (${DECK_RUN})`));
|
|
1105
|
-
|
|
1148
|
+
const spawned = spawnClaudeEmpty(cwd);
|
|
1149
|
+
spawned.child.on('error', () => setNote(`⚠ c → spawn claude fallito (${DECK_RUN})`));
|
|
1150
|
+
noteSpawn(spawned);
|
|
1106
1151
|
}
|
|
1107
1152
|
else if (input === 'w') {
|
|
1108
1153
|
// Salvataggio ESPLICITO: comporre una vista non tocca il disco, così
|
package/dist/overlays/sheet.js
CHANGED
|
@@ -205,7 +205,7 @@ export function useSheetOverlay(deps) {
|
|
|
205
205
|
const note = spawnNote.trim();
|
|
206
206
|
close();
|
|
207
207
|
if (id)
|
|
208
|
-
onAction(id, act.kind, model, note
|
|
208
|
+
onAction(id, act.kind, model, note);
|
|
209
209
|
}
|
|
210
210
|
else if (key.tab) {
|
|
211
211
|
// T108 — `tab` scorre il catalogo dei modelli, e da T111 è il SOLO canale:
|
package/dist/spawn.js
CHANGED
|
@@ -32,6 +32,91 @@ export function spawnOut(cmd, args, opts) {
|
|
|
32
32
|
fake.unref = () => fake;
|
|
33
33
|
return fake;
|
|
34
34
|
}
|
|
35
|
+
// Caratteri che una shell POSIX passa attraverso senza interpretarli: tutto il
|
|
36
|
+
// resto (spazi, apici, `$`, glob, non-ASCII) va quotato o il comando ricopiato
|
|
37
|
+
// dalla riga di stato non farebbe la stessa cosa di quello eseguito.
|
|
38
|
+
const SHELL_SAFE = /^[A-Za-z0-9_@%+=:,./-]+$/;
|
|
39
|
+
/**
|
|
40
|
+
* Un argomento come lo si scriverebbe in bash.
|
|
41
|
+
*
|
|
42
|
+
* Apici SINGOLI, non doppi: dentro i singoli nessun carattere resta speciale,
|
|
43
|
+
* quindi non c'è da enumerare cosa sfuggire. L'unico caso da chiudere è l'apice
|
|
44
|
+
* stesso, che si esce dalla stringa (`'\''`) invece di essere escapato dentro.
|
|
45
|
+
*
|
|
46
|
+
* Il deck non passa MAI da una shell — `spawn` senza `shell:true` consegna
|
|
47
|
+
* l'argv a execve così com'è. Questo quoting esiste per la sola RESA: rende la
|
|
48
|
+
* riga ricopiabile in un terminale senza cambiare di una virgola ciò che il
|
|
49
|
+
* deck ha davvero eseguito.
|
|
50
|
+
*/
|
|
51
|
+
export function shellQuote(arg) {
|
|
52
|
+
if (arg === '')
|
|
53
|
+
return "''";
|
|
54
|
+
if (SHELL_SAFE.test(arg))
|
|
55
|
+
return arg;
|
|
56
|
+
return `'${arg.replace(/'/g, `'\\''`)}'`;
|
|
57
|
+
}
|
|
58
|
+
export function shellCommand(cmd, args) {
|
|
59
|
+
return [cmd, ...args].map(shellQuote).join(' ');
|
|
60
|
+
}
|
|
61
|
+
// I quattro percorsi di spawn di una sessione Claude interattiva differiscono
|
|
62
|
+
// SOLO per l'argv: stesso eseguibile, stesso detached, stesso `unref`. Il corpo
|
|
63
|
+
// sta qui una volta sola, così anche il comando mostrato nasce in un punto solo.
|
|
64
|
+
//
|
|
65
|
+
// stdout in PIPE e non `ignore`: è il canale su cui `deck-run` annuncia il
|
|
66
|
+
// comando in-tab (vedi `onInTabCommand`). Il figlio resta detached — la pipe
|
|
67
|
+
// serve a leggere una riga, non a possedere il processo.
|
|
68
|
+
function launchDeckRun(args, cwd) {
|
|
69
|
+
const child = spawnOut(DECK_RUN, args, {
|
|
70
|
+
cwd,
|
|
71
|
+
detached: true,
|
|
72
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
73
|
+
});
|
|
74
|
+
child.unref();
|
|
75
|
+
return { child, cmd: shellCommand(DECK_RUN, args) };
|
|
76
|
+
}
|
|
77
|
+
/** Prefisso della riga con cui `deck-run` annuncia il comando in-tab. */
|
|
78
|
+
export const INTAB_MARKER = 'LOOM_DECK_INTAB ';
|
|
79
|
+
/**
|
|
80
|
+
* Il comando che gira DENTRO la tab: la sessione `claude` vera, con le env che
|
|
81
|
+
* la legano a task e progetto (`LOOM_TASK`, `PTYXIS_PROFILE`), il titolo, il
|
|
82
|
+
* permission mode e il prompt iniziale già quotato.
|
|
83
|
+
*
|
|
84
|
+
* Non si compone qui e non si può: lo compone `deck-run`, che è il primitive
|
|
85
|
+
* dove vivono catalogo dei prompt, quoting, permission mode, profilo di stato e
|
|
86
|
+
* titolo. Ricostruirlo nel deck sarebbe una seconda scrittura delle stesse
|
|
87
|
+
* regole, che diverge al primo flag aggiunto da una parte sola — quindi
|
|
88
|
+
* `deck-run` lo annuncia su stdout prima di `exec` e qui lo si legge.
|
|
89
|
+
*
|
|
90
|
+
* Arriva ASINCRONO, millisecondi dopo lo spawn: chi mostra il comando deve
|
|
91
|
+
* avere già scritto qualcosa (il comando di `deck-run`), perché l'annuncio può
|
|
92
|
+
* non arrivare affatto — spawn inerte (`LOOM_DECK_NO_SPAWN`, che non ha nemmeno
|
|
93
|
+
* uno stdout), validazione degli argomenti fallita, `deck-run` morto prima
|
|
94
|
+
* dell'exec.
|
|
95
|
+
*
|
|
96
|
+
* Dopo la riga attesa lo stdout si drena e basta: da lì in poi appartiene a
|
|
97
|
+
* `ptyxis`, che ha preso il posto del processo.
|
|
98
|
+
*/
|
|
99
|
+
export function onInTabCommand(child, cb) {
|
|
100
|
+
if (!child.stdout)
|
|
101
|
+
return;
|
|
102
|
+
let buf = '';
|
|
103
|
+
let seen = false;
|
|
104
|
+
child.stdout.on('data', (chunk) => {
|
|
105
|
+
if (seen)
|
|
106
|
+
return;
|
|
107
|
+
buf += chunk.toString();
|
|
108
|
+
let nl;
|
|
109
|
+
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
110
|
+
const line = buf.slice(0, nl);
|
|
111
|
+
buf = buf.slice(nl + 1);
|
|
112
|
+
if (line.startsWith(INTAB_MARKER)) {
|
|
113
|
+
seen = true;
|
|
114
|
+
cb(line.slice(INTAB_MARKER.length));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
35
120
|
// L'ordine È il giro di `tab` nel detail, non una preferenza di lettura:
|
|
36
121
|
// cambiarlo sposta le voci sotto le dita di chi le ha imparate. Fino a T111 era
|
|
37
122
|
// anche il binding delle cifre `1`-`4`, passate poi al campo nota.
|
|
@@ -79,13 +164,7 @@ export function deckArgs(id, sessionId, kind, model = MODEL_DEFAULT, spawnNote)
|
|
|
79
164
|
return args;
|
|
80
165
|
}
|
|
81
166
|
export function spawnDeck(id, cwd, sessionId, kind, model = MODEL_DEFAULT, spawnNote) {
|
|
82
|
-
|
|
83
|
-
cwd,
|
|
84
|
-
detached: true,
|
|
85
|
-
stdio: 'ignore',
|
|
86
|
-
});
|
|
87
|
-
child.unref();
|
|
88
|
-
return child;
|
|
167
|
+
return launchDeckRun(deckArgs(id, sessionId, kind, model, spawnNote), cwd);
|
|
89
168
|
}
|
|
90
169
|
// T49 — resume di una sessione esistente come nuova tab Ptyxis. Scoped (taskId
|
|
91
170
|
// presente) → `deck-run <task> --resume <sid>`: la ripresa eredita LOOM_TASK +
|
|
@@ -109,13 +188,7 @@ export function resumeArgs(taskId, sessionId, note) {
|
|
|
109
188
|
return args;
|
|
110
189
|
}
|
|
111
190
|
export function spawnDeckResume(taskId, cwd, sessionId, note) {
|
|
112
|
-
|
|
113
|
-
cwd,
|
|
114
|
-
detached: true,
|
|
115
|
-
stdio: 'ignore',
|
|
116
|
-
});
|
|
117
|
-
child.unref();
|
|
118
|
-
return child;
|
|
191
|
+
return launchDeckRun(resumeArgs(taskId, sessionId, note), cwd);
|
|
119
192
|
}
|
|
120
193
|
// T28 — FORK: `deck-run <task|--no-task> --resume <origine> --fork --session-id
|
|
121
194
|
// <nuovo>`. Variante del resume, non una terza forma: cambia solo che CC apre un
|
|
@@ -140,13 +213,7 @@ export function forkArgs(taskId, originId, newId) {
|
|
|
140
213
|
];
|
|
141
214
|
}
|
|
142
215
|
export function spawnDeckFork(taskId, cwd, originId, newId) {
|
|
143
|
-
|
|
144
|
-
cwd,
|
|
145
|
-
detached: true,
|
|
146
|
-
stdio: 'ignore',
|
|
147
|
-
});
|
|
148
|
-
child.unref();
|
|
149
|
-
return child;
|
|
216
|
+
return launchDeckRun(forkArgs(taskId, originId, newId), cwd);
|
|
150
217
|
}
|
|
151
218
|
// T42 — sessione Claude NUDA: nessuna task, nessun prompt iniziale, nessun
|
|
152
219
|
// sessionId pinnato (quindi nessuna entry nel sidecar session-tasks.jsonl: senza
|
|
@@ -155,13 +222,7 @@ export function spawnDeckFork(taskId, cwd, originId, newId) {
|
|
|
155
222
|
// sporcherebbe il percorso bound. Il titolo tab resta la label loom — lo mette
|
|
156
223
|
// deck-run, perché il match compass è window-level e non sa nulla di task.
|
|
157
224
|
export function spawnClaudeEmpty(cwd) {
|
|
158
|
-
|
|
159
|
-
cwd,
|
|
160
|
-
detached: true,
|
|
161
|
-
stdio: 'ignore',
|
|
162
|
-
});
|
|
163
|
-
child.unref();
|
|
164
|
-
return child;
|
|
225
|
+
return launchDeckRun(['--no-task'], cwd);
|
|
165
226
|
}
|
|
166
227
|
// T39/T32: voce `launch` custom del file config, eseguita con cwd = project root.
|
|
167
228
|
// Spawn detached come spawnDeck: il deck lancia ma non possiede il processo.
|
package/dist/width.js
CHANGED
|
@@ -328,6 +328,56 @@ export function pad(s, cols, align = 'left') {
|
|
|
328
328
|
const fill = ' '.repeat(Math.max(0, cols - termWidth(t)));
|
|
329
329
|
return align === 'right' ? fill + t : t + fill;
|
|
330
330
|
}
|
|
331
|
+
/**
|
|
332
|
+
* Taglio a un budget di COLONNE che sacrifica il MEZZO invece della coda.
|
|
333
|
+
*
|
|
334
|
+
* Serve dove la testa e la coda portano informazione diversa e la TESTA è quella
|
|
335
|
+
* costante: il comando di uno spawn è sempre lo stesso path assoluto di
|
|
336
|
+
* `deck-run`, e quel che cambia da un'invocazione all'altra (task, sessionId,
|
|
337
|
+
* prompt-kind, modello, nota) sta tutto in fondo. Un `cut()` su quella riga
|
|
338
|
+
* mostrerebbe l'unica parte che non cambia mai.
|
|
339
|
+
*
|
|
340
|
+
* La coda prende due terzi del budget e la testa il resto: basta a riconoscere
|
|
341
|
+
* l'eseguibile senza mangiare gli argomenti. Testa e coda non possono
|
|
342
|
+
* sovrapporsi — insieme occupano al più `cols - 1` colonne, cioè meno della
|
|
343
|
+
* stringa intera (che qui è già più larga del budget).
|
|
344
|
+
*
|
|
345
|
+
* Niente collasso del whitespace (a differenza di `cut`): la stringa in ingresso
|
|
346
|
+
* è un comando, e due spazi dentro un argomento quotato sono suoi. Restano
|
|
347
|
+
* appiattiti i soli a-capo, che sfonderebbero la riga singola.
|
|
348
|
+
*/
|
|
349
|
+
export function cutMiddle(s, cols) {
|
|
350
|
+
const flat = s.replace(/[\r\n\t]+/g, ' ');
|
|
351
|
+
if (cols <= 0)
|
|
352
|
+
return '';
|
|
353
|
+
if (termWidth(flat) <= cols)
|
|
354
|
+
return flat;
|
|
355
|
+
if (cols === 1)
|
|
356
|
+
return '…';
|
|
357
|
+
const budget = cols - 1; // -1 = la colonna dell'ellissi, che qui c'è di sicuro
|
|
358
|
+
const headCols = budget - Math.ceil((budget * 2) / 3);
|
|
359
|
+
let head = '';
|
|
360
|
+
let hw = 0;
|
|
361
|
+
for (const ch of flat) {
|
|
362
|
+
const cw = termWidth(ch);
|
|
363
|
+
if (hw + cw > headCols)
|
|
364
|
+
break;
|
|
365
|
+
head += ch;
|
|
366
|
+
hw += cw;
|
|
367
|
+
}
|
|
368
|
+
const chars = [...flat];
|
|
369
|
+
let tail = '';
|
|
370
|
+
let tw = 0;
|
|
371
|
+
for (let i = chars.length - 1; i >= 0; i--) {
|
|
372
|
+
const ch = chars[i];
|
|
373
|
+
const cw = termWidth(ch);
|
|
374
|
+
if (hw + tw + cw > budget)
|
|
375
|
+
break;
|
|
376
|
+
tail = ch + tail;
|
|
377
|
+
tw += cw;
|
|
378
|
+
}
|
|
379
|
+
return head + '…' + tail;
|
|
380
|
+
}
|
|
331
381
|
/**
|
|
332
382
|
* Larghezza in colonne di UN carattere **dopo** `sanitize`.
|
|
333
383
|
*
|
package/package.json
CHANGED
package/scripts/deck-run
CHANGED
|
@@ -29,6 +29,12 @@
|
|
|
29
29
|
# riprende RAMANDOLA (`--fork-session`): id nuovo, transcript copiato, l'origine
|
|
30
30
|
# resta intatta e scrivibile.
|
|
31
31
|
#
|
|
32
|
+
# Su stdout, prima di aprire la tab, stampa una riga
|
|
33
|
+
# LOOM_DECK_INTAB <comando>
|
|
34
|
+
# col comando che girerà DENTRO la tab (la sessione `claude` con le sue env).
|
|
35
|
+
# Serve a chi spawna deck-run e vuole mostrare l'invocazione vera senza
|
|
36
|
+
# ricomporla per conto proprio; le diagnostiche restano tutte su stderr.
|
|
37
|
+
#
|
|
32
38
|
# Modalità spawn (LOOM_DECK_SPAWN_MODE):
|
|
33
39
|
# inline → ptyxis --tab -- <cmd> (default) comando inline, LOOM_TASK diretta, zero dconf
|
|
34
40
|
# profile → ptyxis --tab-with-profile=<UUID> riusa il profilo, riscrive il custom-command via dconf
|
|
@@ -440,6 +446,24 @@ else
|
|
|
440
446
|
fi
|
|
441
447
|
IN_TAB_CMD="${LOOM_DECK_INTAB_CMD:-$_default_intab}"
|
|
442
448
|
|
|
449
|
+
# ANNUNCIO del comando in-tab, su stdout, prima dell'exec.
|
|
450
|
+
#
|
|
451
|
+
# È l'unico modo che ha un chiamante di sapere cosa gira davvero dentro la tab:
|
|
452
|
+
# il comando si compone QUI — catalogo dei prompt, quoting, permission mode,
|
|
453
|
+
# profilo di stato, titolo — e ricomporlo dall'altro lato sarebbe una seconda
|
|
454
|
+
# scrittura delle stesse regole, divergente al primo flag aggiunto. Il deck lo
|
|
455
|
+
# legge da questa riga e la mostra nella propria riga di stato.
|
|
456
|
+
#
|
|
457
|
+
# Deve stare DOPO l'ultima assegnazione di IN_TAB_CMD e PRIMA dell'exec (che
|
|
458
|
+
# sostituisce il processo): annunciare un valore ancora modificabile
|
|
459
|
+
# annuncerebbe qualcosa di diverso da quello eseguito.
|
|
460
|
+
#
|
|
461
|
+
# stdout e non stderr: qui non c'è nessuna anomalia da segnalare, è l'esito
|
|
462
|
+
# normale del lavoro dello script. Le diagnostiche restano su stderr, e i
|
|
463
|
+
# chiamanti che leggono l'ULTIMA riga di stdout (lo shim `ptyxis` dei test)
|
|
464
|
+
# vedono comunque quello che vedevano prima.
|
|
465
|
+
printf 'LOOM_DECK_INTAB %s\n' "$IN_TAB_CMD"
|
|
466
|
+
|
|
443
467
|
case "$MODE" in
|
|
444
468
|
inline)
|
|
445
469
|
# --tab: tab nella window attiva. -- <cmd>: comando inline nella tab.
|