@lamemind/loom-deck 0.78.0 → 0.79.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/dist/live-sessions.js +99 -20
- package/dist/tab-compose.js +8 -1
- package/dist/tab-launch.js +4 -0
- package/dist/twin-title.js +63 -0
- package/package.json +1 -1
package/dist/live-sessions.js
CHANGED
|
@@ -2,8 +2,10 @@ import { readFileSync, readdirSync } from 'node:fs';
|
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { basename, join } from 'node:path';
|
|
4
4
|
import { isWithin } from './project-path.js';
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/** La cartella del registry. `LOOM_DECK_LIVE_SESSIONS_ROOT` la sposta, per i
|
|
6
|
+
* test che costruiscono un registro finto invece di leggere quello vero. */
|
|
7
|
+
export function liveSessionsRoot(env = process.env) {
|
|
8
|
+
return env.LOOM_DECK_LIVE_SESSIONS_ROOT || join(homedir(), '.claude', 'sessions');
|
|
7
9
|
}
|
|
8
10
|
/**
|
|
9
11
|
* Campo 22 (`starttime`) di `/proc/<pid>/stat`, o null se la riga non è
|
|
@@ -96,24 +98,16 @@ export function parseLiveEntry(content, pid) {
|
|
|
96
98
|
};
|
|
97
99
|
}
|
|
98
100
|
/**
|
|
99
|
-
* Le
|
|
100
|
-
* la stessa chiave del sidecar `session-tasks.jsonl` e di `deck-run --resume`,
|
|
101
|
-
* quindi il join con la riga di lista è diretto.
|
|
102
|
-
*
|
|
103
|
-
* Il filtro per `cwd` è d'EFFICIENZA, non di correttezza: risparmia lo `stat`
|
|
104
|
-
* di `/proc` sui processi degli altri progetti (il registry è globale, non
|
|
105
|
-
* per-progetto). Il match resta sul `sessionId`.
|
|
101
|
+
* Le entry VIVE del registry, nell'ordine di `readdir`.
|
|
106
102
|
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* mostrerebbe `idle` su una sessione che sta lavorando, cioè il difetto che la
|
|
110
|
-
* colonna esiste per non avere.
|
|
103
|
+
* `keep` scarta un'entry PRIMA del controllo di liveness: chi filtra per
|
|
104
|
+
* progetto risparmia così lo `stat` di `/proc` sui processi degli altri.
|
|
111
105
|
*/
|
|
112
|
-
|
|
113
|
-
const out =
|
|
106
|
+
function liveEntries(root, keep = () => true) {
|
|
107
|
+
const out = [];
|
|
114
108
|
let files;
|
|
115
109
|
try {
|
|
116
|
-
files = readdirSync(
|
|
110
|
+
files = readdirSync(root).filter((f) => f.endsWith('.json'));
|
|
117
111
|
}
|
|
118
112
|
catch {
|
|
119
113
|
return out; // registry assente (CLI mai avviato, o versione che non lo scrive)
|
|
@@ -124,7 +118,7 @@ export function discoverLiveSessions(projectRoot) {
|
|
|
124
118
|
continue;
|
|
125
119
|
let content;
|
|
126
120
|
try {
|
|
127
|
-
content = readFileSync(join(
|
|
121
|
+
content = readFileSync(join(root, f), 'utf8');
|
|
128
122
|
}
|
|
129
123
|
catch {
|
|
130
124
|
continue; // il processo può finire fra readdir e read
|
|
@@ -133,16 +127,101 @@ export function discoverLiveSessions(projectRoot) {
|
|
|
133
127
|
if (!parsed)
|
|
134
128
|
continue;
|
|
135
129
|
const { entry, procStart } = parsed;
|
|
136
|
-
|
|
137
|
-
// e la root del deck può arrivare con la lettera del drive in un altro case.
|
|
138
|
-
if (!isWithin(entry.cwd, projectRoot))
|
|
130
|
+
if (!keep(entry))
|
|
139
131
|
continue;
|
|
140
132
|
if (!isAlive(pid, procStart))
|
|
141
133
|
continue;
|
|
134
|
+
out.push(entry);
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Le sessioni VIVE del solo progetto corrente, indicizzate per `sessionId` —
|
|
140
|
+
* la stessa chiave del sidecar `session-tasks.jsonl` e di `deck-run --resume`,
|
|
141
|
+
* quindi il join con la riga di lista è diretto.
|
|
142
|
+
*
|
|
143
|
+
* Il filtro per `cwd` è d'EFFICIENZA, non di correttezza: risparmia lo `stat`
|
|
144
|
+
* di `/proc` sui processi degli altri progetti (il registry è globale, non
|
|
145
|
+
* per-progetto). Il match resta sul `sessionId`.
|
|
146
|
+
*
|
|
147
|
+
* Nessuna cache mtime-keyed come in `sessions.ts`: le entry sono poche e di
|
|
148
|
+
* poche centinaia di byte, mentre `status` cambia a ogni turno — una cache qui
|
|
149
|
+
* mostrerebbe `idle` su una sessione che sta lavorando, cioè il difetto che la
|
|
150
|
+
* colonna esiste per non avere.
|
|
151
|
+
*/
|
|
152
|
+
export function discoverLiveSessions(projectRoot) {
|
|
153
|
+
const out = new Map();
|
|
154
|
+
// T175 — `isWithin` regge le grafie di Windows: il registry scrive `C:\…`,
|
|
155
|
+
// e la root del deck può arrivare con la lettera del drive in un altro case.
|
|
156
|
+
for (const entry of liveEntries(liveSessionsRoot(), (e) => isWithin(e.cwd, projectRoot))) {
|
|
142
157
|
out.set(entry.sessionId, entry);
|
|
143
158
|
}
|
|
144
159
|
return out;
|
|
145
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Il nome CHIESTO in un argv di `claude` — il valore di `--name` o del suo alias
|
|
163
|
+
* `-n`, staccato o con `=` — oppure `null` se l'argv non ne chiede uno.
|
|
164
|
+
*
|
|
165
|
+
* Non è il campo `name` del registry, ed è la ragione per cui serve: il registry
|
|
166
|
+
* porta il nome EFFETTIVO, e quando il nome chiesto è già preso il CLI rinomina
|
|
167
|
+
* la sessione (`🧵 loom-works` → `🧵 loom-works-cryptic-hopper`, con
|
|
168
|
+
* `nameSource: "collision"`), mentre il titolo della tab resta il nome chiesto.
|
|
169
|
+
*
|
|
170
|
+
* GEMELLA di `procRequestedName` in `loom-compass/compass@lamemind/model.js`:
|
|
171
|
+
* le due letture devono riconoscere le stesse quattro forme.
|
|
172
|
+
*/
|
|
173
|
+
export function requestedNameIn(argv) {
|
|
174
|
+
for (let i = 0; i < argv.length; i++) {
|
|
175
|
+
const a = argv[i];
|
|
176
|
+
if (a === '--name' || a === '-n')
|
|
177
|
+
return argv[i + 1] ?? null;
|
|
178
|
+
if (a.startsWith('--name='))
|
|
179
|
+
return a.slice('--name='.length);
|
|
180
|
+
if (a.startsWith('-n='))
|
|
181
|
+
return a.slice('-n='.length);
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
/** Gli argomenti del processo: `/proc/<pid>/cmdline` li separa con NUL e ne
|
|
186
|
+
* mette uno anche in coda. Ogni argomento arriva intero, spazi compresi. */
|
|
187
|
+
function procArgv(pid) {
|
|
188
|
+
let raw;
|
|
189
|
+
try {
|
|
190
|
+
raw = readFileSync(`/proc/${pid}/cmdline`, 'utf8');
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return null; // processo finito fra la liveness e la lettura
|
|
194
|
+
}
|
|
195
|
+
const argv = raw.split('\0');
|
|
196
|
+
if (argv.length && argv[argv.length - 1] === '')
|
|
197
|
+
argv.pop();
|
|
198
|
+
return argv;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* T182 — i nomi chiesti con `--name` dalle conversazioni vive di TUTTI i
|
|
202
|
+
* progetti: l'ingresso di `twinTitle` (`src/twin-title.ts`).
|
|
203
|
+
*
|
|
204
|
+
* Nessun filtro per `cwd`, al contrario di `discoverLiveSessions`: chi cerca la
|
|
205
|
+
* tab per titolo la cerca in tutte le finestre Ptyxis, quindi una conversazione
|
|
206
|
+
* dello stesso progetto aperta da un worktree di lane (altro `cwd`, stesso
|
|
207
|
+
* titolo) è un gemello a tutti gli effetti. Il titolo porta già la label del
|
|
208
|
+
* progetto, e il confronto per uguaglianza intera non scambia un progetto con
|
|
209
|
+
* un altro.
|
|
210
|
+
*
|
|
211
|
+
* Una viva senza `--name` non entra: il suo titolo di tab non è un nome
|
|
212
|
+
* chiesto. Dove `/proc` non esiste nessuna entry risulta viva, e la lista
|
|
213
|
+
* esce vuota: il lancio usa il nome base.
|
|
214
|
+
*/
|
|
215
|
+
export function liveRequestedNames(root = liveSessionsRoot()) {
|
|
216
|
+
const names = [];
|
|
217
|
+
for (const entry of liveEntries(root)) {
|
|
218
|
+
const argv = procArgv(entry.pid);
|
|
219
|
+
const name = argv ? requestedNameIn(argv) : null;
|
|
220
|
+
if (name !== null)
|
|
221
|
+
names.push(name);
|
|
222
|
+
}
|
|
223
|
+
return names;
|
|
224
|
+
}
|
|
146
225
|
/** Signature del poll: cambia quando cambia l'insieme delle vive O lo stato di
|
|
147
226
|
* una di esse. Il `pid` ci sta dentro perché un `/clear` sposta lo stesso
|
|
148
227
|
* `sessionId` su un processo diverso senza toccare né insieme né stato. */
|
package/dist/tab-compose.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { saneNote } from './sane-note.js';
|
|
16
16
|
import { MODEL_DEFAULT, MODELS } from './spawn-catalog.js';
|
|
17
17
|
import { shellQuote } from './spawn.js';
|
|
18
|
+
import { twinTitle } from './twin-title.js';
|
|
18
19
|
export const PERMISSION_MODES = ['acceptEdits', 'auto', 'bypassPermissions', 'manual', 'dontAsk', 'plan'];
|
|
19
20
|
/** L'alfabeto della chiave di stato: il valore entra in `bash -lc "…"` e il
|
|
20
21
|
* file di progetto è editabile a mano, quindi si scarta invece di quotare. */
|
|
@@ -173,6 +174,12 @@ export function posixQuote(text) {
|
|
|
173
174
|
/**
|
|
174
175
|
* La composizione intera.
|
|
175
176
|
*
|
|
177
|
+
* Il titolo è `tabTitle` più, solo se una conversazione viva ha già chiesto lo
|
|
178
|
+
* stesso nome, il carattere del gemello (`twinTitle`, T182). Il carattere entra
|
|
179
|
+
* nel titolo intero — label, task, nota e fork compresi — perché due
|
|
180
|
+
* conversazioni della stessa task con note diverse hanno già titoli distinti e
|
|
181
|
+
* non sono gemelle. Lo stesso `title` alimenta `-T` di Ptyxis e `--name`.
|
|
182
|
+
*
|
|
176
183
|
* Ordine degli argomenti di `claude`: nome, permission mode e modello prima dei
|
|
177
184
|
* flag di continuità, il prompt sempre ultimo. Il modello sta accanto al
|
|
178
185
|
* permission mode perché sono la stessa cosa — la scelta di chi apre la
|
|
@@ -186,7 +193,7 @@ export function posixQuote(text) {
|
|
|
186
193
|
*/
|
|
187
194
|
export function composeTab(req, ctx) {
|
|
188
195
|
const warnings = [];
|
|
189
|
-
const title = tabTitle(req, projectLabel(ctx.config));
|
|
196
|
+
const title = twinTitle(tabTitle(req, projectLabel(ctx.config)), ctx.liveRequestedNames ?? []);
|
|
190
197
|
const mode = permissionMode(ctx, warnings);
|
|
191
198
|
const model = modelOf(req, ctx, warnings);
|
|
192
199
|
const key = stateKey(ctx, warnings);
|
package/dist/tab-launch.js
CHANGED
|
@@ -21,6 +21,7 @@ import { isUsageError, parseTabArgs } from './tab-args.js';
|
|
|
21
21
|
import { composeTab, ptyxisArgv } from './tab-compose.js';
|
|
22
22
|
import { loadPromptCatalog, PROMPT_CATALOG } from './prompt-catalog.js';
|
|
23
23
|
import { configFilePath } from './config.js';
|
|
24
|
+
import { liveRequestedNames, liveSessionsRoot } from './live-sessions.js';
|
|
24
25
|
import { resolveGitBash } from './git-bash.js';
|
|
25
26
|
import { semicolonArg, TAB_ARGV_ENV, windowsTabEnv, WT_CMD, wtNewTabArgs, wtProfile, } from './windows-terminal.js';
|
|
26
27
|
/** Il wrapper che gira dentro una tab di Windows Terminal, nel build: lo esegue
|
|
@@ -96,6 +97,9 @@ export async function runDeckRun(argv, io) {
|
|
|
96
97
|
catalog: loadPromptCatalog(catalogPath),
|
|
97
98
|
catalogPath,
|
|
98
99
|
platform: io.platform,
|
|
100
|
+
// Su Windows niente `/proc`, quindi nessuna liveness (T177): il titolo
|
|
101
|
+
// resta quello base, cioè il comportamento di prima del gemello.
|
|
102
|
+
liveRequestedNames: io.platform === 'win32' ? [] : liveRequestedNames(liveSessionsRoot(io.env)),
|
|
99
103
|
});
|
|
100
104
|
for (const w of plan.warnings)
|
|
101
105
|
io.stderr(`${w}\n`);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// T182 — il titolo della conversazione GEMELLA: un carattere in coda al
|
|
2
|
+
// `--name` quando una conversazione viva ha già chiesto lo stesso nome.
|
|
3
|
+
//
|
|
4
|
+
// Chi porta davanti la tab di una conversazione la cerca per titolo (coda via
|
|
5
|
+
// AT-SPI, `sessionsByName` di compass), e il titolo della tab è il `--name`
|
|
6
|
+
// passato a `claude`. Due conversazioni vive con lo stesso nome — tipico: il
|
|
7
|
+
// preflight e il run della stessa task — hanno due tab con lo stesso titolo, e
|
|
8
|
+
// chi cerca non può scegliere. Il lanciatore rende unico il secondo titolo; il
|
|
9
|
+
// primo lancio resta senza carattere, quindi nel caso normale il titolo è quello
|
|
10
|
+
// di sempre.
|
|
11
|
+
//
|
|
12
|
+
// Modulo PURO: i nomi chiesti dalle conversazioni vive arrivano già letti
|
|
13
|
+
// (`liveRequestedNames` in `live-sessions.ts`).
|
|
14
|
+
/**
|
|
15
|
+
* I caratteri del gemello, nell'ordine in cui si assegnano: il primo gemello
|
|
16
|
+
* prende `#`.
|
|
17
|
+
*
|
|
18
|
+
* GEMELLA di `TWIN_MARKS` in `loom-compass/compass@lamemind/model.js`, il
|
|
19
|
+
* secondo lanciatore di conversazioni: nessun codice confronta le due copie, e
|
|
20
|
+
* cambiarne una sola fa scegliere caratteri diversi ai due lanciatori senza
|
|
21
|
+
* errore. Si toccano insieme.
|
|
22
|
+
*
|
|
23
|
+
* Ogni carattere sta fuori da cinque classi, e ognuna rompe un consumer in
|
|
24
|
+
* silenzio:
|
|
25
|
+
* - fuori dall'ASCII a una cella: un simbolo di larghezza ambigua (`§`, `†`)
|
|
26
|
+
* occupa una o due celle a seconda della locale e sposta il taglio del
|
|
27
|
+
* titolo nella tab;
|
|
28
|
+
* - dentro l'alfabeto di `saneNote`: un carattere ammesso lì può arrivare da
|
|
29
|
+
* una nota digitata, e il gemello non si distingue più dal testo;
|
|
30
|
+
* - dentro `[A-Za-z0-9_-]`: il matcher di compass `titleKeyRe` riconosce una
|
|
31
|
+
* finestra con `^<emoji> <name>(?![\w-])`;
|
|
32
|
+
* - l'apice singolo: il titolo sta dentro `claude --name '…'` nella riga che
|
|
33
|
+
* `bash -lc` analizza;
|
|
34
|
+
* - i metacaratteri di shell (`; & $ \` | < >`): inerti fra apici, ma il test
|
|
35
|
+
* di `saneNote` asserisce che nessun `[;&`$]` sopravviva in un titolo, e un
|
|
36
|
+
* gemello della stessa classe renderebbe cieco quel test. Fuori anche la
|
|
37
|
+
* punteggiatura che si legge come testo (`. , : /`) e le parentesi.
|
|
38
|
+
*/
|
|
39
|
+
export const TWIN_MARKS = ['#', '+', '=', '~', '^', '%', '@', '*', '?', '!'];
|
|
40
|
+
/**
|
|
41
|
+
* Il titolo da usare: `base` se nessuna conversazione viva l'ha chiesto,
|
|
42
|
+
* altrimenti `base` + spazio + il primo carattere di `TWIN_MARKS` che nessuna
|
|
43
|
+
* conversazione viva porta già su quel nome.
|
|
44
|
+
*
|
|
45
|
+
* Lo spazio separa il carattere come ogni altro suffisso del titolo (` · T182`,
|
|
46
|
+
* ` <nota>`, ` · fork`): incollato all'ultima parola si leggerebbe come un
|
|
47
|
+
* refuso della nota. Il confronto è per uguaglianza intera, la stessa che fanno
|
|
48
|
+
* coda e `sessionsByName`.
|
|
49
|
+
*
|
|
50
|
+
* Insieme esaurito → `base`: il lancio parte comunque, e chi cerca la tab per
|
|
51
|
+
* titolo torna all'esito ambiguo che ha anche senza lanciatore.
|
|
52
|
+
*/
|
|
53
|
+
export function twinTitle(base, liveRequestedNames) {
|
|
54
|
+
const taken = new Set(liveRequestedNames);
|
|
55
|
+
if (!taken.has(base))
|
|
56
|
+
return base;
|
|
57
|
+
for (const mark of TWIN_MARKS) {
|
|
58
|
+
const candidate = `${base} ${mark}`;
|
|
59
|
+
if (!taken.has(candidate))
|
|
60
|
+
return candidate;
|
|
61
|
+
}
|
|
62
|
+
return base;
|
|
63
|
+
}
|
package/package.json
CHANGED