@lamemind/loom-deck 0.75.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 +24 -2
- package/dist/actions.js +67 -5
- package/dist/archivable.js +1 -1
- package/dist/claude-cmd.js +101 -0
- package/dist/cli.js +18 -7
- package/dist/commit-times.js +5 -2
- package/dist/config.js +51 -6
- package/dist/deck-model.js +14 -9
- package/dist/deck-run.js +7 -0
- package/dist/doc-tree.js +2 -2
- package/dist/frame.js +7 -0
- package/dist/git-bash.js +78 -0
- package/dist/gitlink.js +1 -0
- package/dist/hooks.js +21 -7
- package/dist/inbox.js +2 -2
- package/dist/input.js +7 -0
- package/dist/live-sessions.js +4 -1
- package/dist/overlays/gitlink.js +12 -2
- package/dist/overlays/wrap.js +10 -1
- package/dist/pane-views.js +12 -2
- package/dist/platform.js +93 -0
- package/dist/project-path.js +102 -0
- package/dist/purge.js +1 -0
- package/dist/sane-note.js +18 -24
- package/dist/sessions.js +63 -29
- package/dist/spawn-catalog.js +4 -4
- package/dist/spawn-config.js +4 -4
- package/dist/spawn.js +77 -11
- 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 +9 -22
- package/dist/ui/doc-pane.js +6 -2
- package/dist/ui/inbox-pane.js +7 -2
- package/dist/windows-terminal.js +153 -0
- package/dist/wrap-scan.js +2 -2
- package/package.json +3 -2
- package/scripts/deck-run +11 -678
- package/scripts/prompt-catalog +6 -11
package/dist/frame.js
CHANGED
|
@@ -26,6 +26,7 @@ import { META_ROWS } from './model.js';
|
|
|
26
26
|
import { TASK_MODE_LEGEND } from './input-modes.js';
|
|
27
27
|
import { rowIndexOf } from './session-list.js';
|
|
28
28
|
import { VERSION } from './version.js';
|
|
29
|
+
import { OFF_MARK } from './platform.js';
|
|
29
30
|
// La surface built-in del cappello, in testa alla riga launch. Non sta fra i
|
|
30
31
|
// tasti perché ha la stessa natura delle voci `launch` — fire-once, cwd =
|
|
31
32
|
// project root, nessuno stato. Per il terminale compass usa 🖥️, che nel frame
|
|
@@ -230,6 +231,8 @@ export const INDICATOR_SEP = ' ';
|
|
|
230
231
|
* dell'ultimo riuscito, che resta vero e ancora apribile.
|
|
231
232
|
*/
|
|
232
233
|
export function inboxButton(state) {
|
|
234
|
+
if (state.available === false)
|
|
235
|
+
return sanitize(`[ Doc 📄 ${OFF_MARK} ]`);
|
|
233
236
|
const numbers = state.scanned
|
|
234
237
|
? `${state.counts.nozioni}/${state.counts.derivazione}/${state.counts.sweep}`
|
|
235
238
|
: STATUS_MISSING;
|
|
@@ -261,6 +264,8 @@ export function inboxButton(state) {
|
|
|
261
264
|
* l'esito dell'ultimo riuscito resta vero e ancora apribile.
|
|
262
265
|
*/
|
|
263
266
|
export function wrapIndicator(state) {
|
|
267
|
+
if (state.available === false)
|
|
268
|
+
return `wrap ${OFF_MARK}`;
|
|
264
269
|
const body = state.scanning
|
|
265
270
|
? 'scan…'
|
|
266
271
|
: state.mtime === null
|
|
@@ -289,6 +294,8 @@ export function wrapIndicator(state) {
|
|
|
289
294
|
* nomina i membri.
|
|
290
295
|
*/
|
|
291
296
|
export function gitlinkIndicator(state) {
|
|
297
|
+
if (state.available === false)
|
|
298
|
+
return `gitlink ${OFF_MARK}`;
|
|
292
299
|
const body = !state.scanned
|
|
293
300
|
? STATUS_MISSING
|
|
294
301
|
: !state.ok
|
package/dist/git-bash.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// T175 — il bash di Git for Windows: dove sta, e come lo riceve un `claude`
|
|
2
|
+
// lanciato dal deck.
|
|
3
|
+
//
|
|
4
|
+
// Serve già qui e non solo a T178 (gli script `.sh` del plugin), per il verdetto
|
|
5
|
+
// 9 di T174: un `claude` che eredita il `PATH` di Git Bash esegue gli hook del
|
|
6
|
+
// plugin in PowerShell — `cat` diventa `Get-Content`, lo script `.sh` una
|
|
7
|
+
// stringa stampata — e la task non arriva in contesto. Con
|
|
8
|
+
// `CLAUDE_CODE_GIT_BASH_PATH` nell'env, la variabile con cui Claude Code sceglie
|
|
9
|
+
// il proprio bash, gli hook tornano in bash. Un deck avviato da Git Bash non la
|
|
10
|
+
// porta: la risolve lui.
|
|
11
|
+
//
|
|
12
|
+
// Modulo PURO nel cuore (`resolveGitBash` riceve il filesystem come funzione),
|
|
13
|
+
// con un involucro che legge quello vero. Su Linux non fa niente.
|
|
14
|
+
import { existsSync } from 'node:fs';
|
|
15
|
+
import { win32 } from 'node:path';
|
|
16
|
+
export const GIT_BASH_ENV = 'CLAUDE_CODE_GIT_BASH_PATH';
|
|
17
|
+
/**
|
|
18
|
+
* Quanti livelli sopra la cartella di `git.exe` si cerca `bin\bash.exe`. Tre
|
|
19
|
+
* coprono le tre posizioni di `git.exe` in Git for Windows: `Git\cmd\` (quella
|
|
20
|
+
* del `PATH` di Windows), `Git\bin\` e `Git\mingw64\bin\` (la prima del `PATH`
|
|
21
|
+
* di Git Bash, dove accanto non c'è nessun bash).
|
|
22
|
+
*/
|
|
23
|
+
const MAX_UP = 3;
|
|
24
|
+
/**
|
|
25
|
+
* Il bash di Git, o `null`.
|
|
26
|
+
*
|
|
27
|
+
* Cascata (P6 preflight): la variabile, se impostata — è la scelta esplicita di
|
|
28
|
+
* chi ha configurato Claude Code; poi ogni `git.exe` del `PATH`, nell'ordine,
|
|
29
|
+
* risalendo da lì fino a trovare `bin\bash.exe`. Mai `bash` dal `PATH`: Git
|
|
30
|
+
* aggiunge solo `Git\cmd`, e `C:\Windows\System32\bash.exe` è il lanciatore di
|
|
31
|
+
* WSL. Mai un path cablato: Git può stare fuori da `C:\Program Files\Git`
|
|
32
|
+
* (sulla macchina di T174 sta in `C:\Dev\Tools\Git`).
|
|
33
|
+
*/
|
|
34
|
+
export function resolveGitBash(env, exists) {
|
|
35
|
+
const explicit = env[GIT_BASH_ENV];
|
|
36
|
+
if (explicit)
|
|
37
|
+
return explicit;
|
|
38
|
+
const path = env.PATH ?? env.Path ?? '';
|
|
39
|
+
for (const dir of path.split(win32.delimiter)) {
|
|
40
|
+
if (!dir)
|
|
41
|
+
continue;
|
|
42
|
+
const git = win32.join(dir, 'git.exe');
|
|
43
|
+
if (!exists(git))
|
|
44
|
+
continue;
|
|
45
|
+
let at = win32.dirname(git);
|
|
46
|
+
for (let i = 0; i < MAX_UP; i++) {
|
|
47
|
+
const bash = win32.join(at, 'bin', 'bash.exe');
|
|
48
|
+
if (exists(bash))
|
|
49
|
+
return bash;
|
|
50
|
+
const up = win32.dirname(at);
|
|
51
|
+
if (up === at)
|
|
52
|
+
break;
|
|
53
|
+
at = up;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
let cached;
|
|
59
|
+
/** Il bash di Git di questa macchina, risolto una volta per vita del deck:
|
|
60
|
+
* un'installazione di Git non si sposta mentre il deck è aperto. */
|
|
61
|
+
export function gitBash() {
|
|
62
|
+
if (cached === undefined)
|
|
63
|
+
cached = resolveGitBash(process.env, existsSync);
|
|
64
|
+
return cached;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* L'env di un `claude` lanciato dal deck.
|
|
68
|
+
*
|
|
69
|
+
* Su win32, quello del deck più `CLAUDE_CODE_GIT_BASH_PATH` quando non c'è già e
|
|
70
|
+
* il bash si trova; senza bash il figlio parte con l'env del deck, come prima.
|
|
71
|
+
* Altrove `undefined`, cioè l'env ereditato di `spawn`: nessun cambio su Linux.
|
|
72
|
+
*/
|
|
73
|
+
export function claudeEnv(platform = process.platform, env = process.env, bash = gitBash) {
|
|
74
|
+
if (platform !== 'win32' || env[GIT_BASH_ENV])
|
|
75
|
+
return undefined;
|
|
76
|
+
const found = bash();
|
|
77
|
+
return found ? { ...env, [GIT_BASH_ENV]: found } : undefined;
|
|
78
|
+
}
|
package/dist/gitlink.js
CHANGED
package/dist/hooks.js
CHANGED
|
@@ -20,6 +20,7 @@ import { mixedCount, readWrapCache, runWrapScan, wrapCacheFile, wrapCount, } fro
|
|
|
20
20
|
import { purgeTargets } from './purge.js';
|
|
21
21
|
import { loadSpawnOverrides, saveSpawnOverrides, } from './spawn-config.js';
|
|
22
22
|
import { POLL_MS } from './model.js';
|
|
23
|
+
import { isAvailable } from './platform.js';
|
|
23
24
|
// Dimensioni del terminale, live sul resize.
|
|
24
25
|
//
|
|
25
26
|
// Non è una comodità di layout: senza `rows` il frame non ha tetto, e un frame
|
|
@@ -203,9 +204,12 @@ export function useSessions(projectRoot, core) {
|
|
|
203
204
|
// un refresh più lento mostrerebbe `idle` su una sessione che lavora.
|
|
204
205
|
// Il try è separato perché il registry è una fonte indipendente: se manca
|
|
205
206
|
// (versione del CLI che non lo scrive) la lista deve restare, senza vive.
|
|
207
|
+
// T175 — dove la liveness manca non si interroga nemmeno il registry: la
|
|
208
|
+
// mappa vuota è il dato onesto, e chi ne dipende per una guardia (`CANC`)
|
|
209
|
+
// legge la mappa delle funzioni, non questa.
|
|
206
210
|
let live;
|
|
207
211
|
try {
|
|
208
|
-
live = discoverLiveSessions(projectRoot);
|
|
212
|
+
live = isAvailable('liveSessions') ? discoverLiveSessions(projectRoot) : new Map();
|
|
209
213
|
}
|
|
210
214
|
catch {
|
|
211
215
|
live = new Map();
|
|
@@ -400,7 +404,9 @@ export function useDirtyFolders(idsSig, tasksDir, projectRoot) {
|
|
|
400
404
|
}, [idsSig, tasksDir, projectRoot]);
|
|
401
405
|
return dirty;
|
|
402
406
|
}
|
|
403
|
-
|
|
407
|
+
/** T175 — `enabled` falso = la funzione è spenta su questa piattaforma: nessuno
|
|
408
|
+
* spawn, né all'avvio né a richiesta, e lo stato resta quello di partenza. */
|
|
409
|
+
export function useInboxScan(projectRoot, docsRoot, enabled = true) {
|
|
404
410
|
const [state, setState] = useState({
|
|
405
411
|
files: [],
|
|
406
412
|
ok: true,
|
|
@@ -437,10 +443,12 @@ export function useInboxScan(projectRoot, docsRoot) {
|
|
|
437
443
|
});
|
|
438
444
|
}, [projectRoot, docsRoot]);
|
|
439
445
|
useEffect(() => {
|
|
446
|
+
if (!enabled)
|
|
447
|
+
return;
|
|
440
448
|
run();
|
|
441
449
|
const id = setInterval(run, INBOX_SCAN_INTERVAL_MS);
|
|
442
450
|
return () => clearInterval(id);
|
|
443
|
-
}, [run, epoch]);
|
|
451
|
+
}, [run, epoch, enabled]);
|
|
444
452
|
// Lo scan a richiesta RESETTA il timer, come quello del wrap: senza,
|
|
445
453
|
// l'intervallo continuerebbe a scorrere dall'ultimo tick automatico e potrebbe
|
|
446
454
|
// rimisurare pochi istanti dopo un dato appena misurato.
|
|
@@ -493,7 +501,11 @@ export function useDocScan(projectRoot, docsRoot, enabled) {
|
|
|
493
501
|
}, []);
|
|
494
502
|
return { ...state, scan };
|
|
495
503
|
}
|
|
496
|
-
|
|
504
|
+
/** T175 — `enabled` falso = scan spento su questa piattaforma: niente periodico
|
|
505
|
+
* e niente scan a richiesta. Il periodico sarebbe il peggiore dei due — uno
|
|
506
|
+
* spawn di `python3` ogni sei ore che nessuno ha chiesto, e su Windows quel
|
|
507
|
+
* nome risolve nell'alias dello Store. */
|
|
508
|
+
export function useWrapScan(projectRoot, enabled = true) {
|
|
497
509
|
const path = useMemo(() => wrapCacheFile(projectRoot), [projectRoot]);
|
|
498
510
|
const [cache, setCache] = useState(() => readWrapCache(path));
|
|
499
511
|
const [ok, setOk] = useState(true);
|
|
@@ -505,7 +517,7 @@ export function useWrapScan(projectRoot) {
|
|
|
505
517
|
// secondo lancio.
|
|
506
518
|
const busy = useRef(false);
|
|
507
519
|
const run = useCallback(() => {
|
|
508
|
-
if (busy.current)
|
|
520
|
+
if (busy.current || !enabled)
|
|
509
521
|
return;
|
|
510
522
|
busy.current = true;
|
|
511
523
|
setScanning(true);
|
|
@@ -518,11 +530,13 @@ export function useWrapScan(projectRoot) {
|
|
|
518
530
|
if (good)
|
|
519
531
|
setCache(readWrapCache(path));
|
|
520
532
|
});
|
|
521
|
-
}, [projectRoot, path]);
|
|
533
|
+
}, [projectRoot, path, enabled]);
|
|
522
534
|
useEffect(() => {
|
|
535
|
+
if (!enabled)
|
|
536
|
+
return;
|
|
523
537
|
const id = setInterval(run, SCAN_INTERVAL_MS);
|
|
524
538
|
return () => clearInterval(id);
|
|
525
|
-
}, [run, epoch]);
|
|
539
|
+
}, [run, epoch, enabled]);
|
|
526
540
|
const scan = useCallback(() => {
|
|
527
541
|
setEpoch((e) => e + 1);
|
|
528
542
|
run();
|
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.
|
|
@@ -248,7 +248,7 @@ export async function scanInbox(projectRoot, docsRoot) {
|
|
|
248
248
|
if (!script)
|
|
249
249
|
return { files: [], ok: false };
|
|
250
250
|
try {
|
|
251
|
-
const { stdout } = await execFileAsync(script, ['--docs-root', docsRoot, '--inbox', '--format', 'tsv'], { cwd: projectRoot, maxBuffer: 4 * 1024 * 1024 });
|
|
251
|
+
const { stdout } = await execFileAsync(script, ['--docs-root', docsRoot, '--inbox', '--format', 'tsv'], { cwd: projectRoot, maxBuffer: 4 * 1024 * 1024, windowsHide: true });
|
|
252
252
|
return { files: parseInboxTsv(stdout), ok: true };
|
|
253
253
|
}
|
|
254
254
|
catch {
|
package/dist/input.js
CHANGED
|
@@ -28,6 +28,7 @@ import { TASK_VIEWS, taskView } from './pane-views.js';
|
|
|
28
28
|
import { loadTaskFileText } from './tasks.js';
|
|
29
29
|
import { inboxPrompt, loadInboxText } from './inbox.js';
|
|
30
30
|
import { dirListing, loadDocText } from './doc-tree.js';
|
|
31
|
+
import { refuse } from './platform.js';
|
|
31
32
|
// T116 — l'avviso della prima pressione di `^C`. La durata è INTERPOLATA dalla
|
|
32
33
|
// finestra, non ricopiata: un numero scritto a mano in un testo che promette un
|
|
33
34
|
// comportamento diventa falso il giorno che la costante cambia, e nessuno
|
|
@@ -461,6 +462,12 @@ export function useDeckInput({ cwd, tasksDir, mode, setMode, setNote, model, act
|
|
|
461
462
|
model.setSelSessionId((id) => moveSelection(model.sessionRows, id, 1));
|
|
462
463
|
}
|
|
463
464
|
else if (key.return) {
|
|
465
|
+
// T175 — i due pane del modo doc li riempie uno script del plugin: dove non
|
|
466
|
+
// gira, `⏎` lo dice invece di rispondere «nessuna riga selezionata».
|
|
467
|
+
if ((model.focus === 'doctree' || model.focus === 'inbox') &&
|
|
468
|
+
refuse('pluginScripts', '⏎', setNote, model.focus === 'inbox' ? 'coda inbox' : 'albero doc')) {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
464
471
|
if (model.focus === 'doctree') {
|
|
465
472
|
// T160 — `⏎` apre il DETAIL del bersaglio, non la sessione: la sessione
|
|
466
473
|
// la apre il `⏎` di dentro, come sul pane inbox. Il gesto è lo stesso su
|
package/dist/live-sessions.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync, readdirSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { basename, join } from 'node:path';
|
|
4
|
+
import { isWithin } from './project-path.js';
|
|
4
5
|
function liveSessionsRoot() {
|
|
5
6
|
return join(homedir(), '.claude', 'sessions');
|
|
6
7
|
}
|
|
@@ -132,7 +133,9 @@ export function discoverLiveSessions(projectRoot) {
|
|
|
132
133
|
if (!parsed)
|
|
133
134
|
continue;
|
|
134
135
|
const { entry, procStart } = parsed;
|
|
135
|
-
|
|
136
|
+
// T175 — `isWithin` regge le grafie di Windows: il registry scrive `C:\…`,
|
|
137
|
+
// e la root del deck può arrivare con la lettera del drive in un altro case.
|
|
138
|
+
if (!isWithin(entry.cwd, projectRoot))
|
|
136
139
|
continue;
|
|
137
140
|
if (!isAlive(pid, procStart))
|
|
138
141
|
continue;
|
package/dist/overlays/gitlink.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// preflight, D1/P12 — nessuno split dentro questa task).
|
|
8
8
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
9
9
|
import { attentionCount, bumpableCount, bumpGitlink, disalignedCount, gitlinkNote, GITLINK_SCAN_INTERVAL_MS, hasGitmodules, scanGitlink, } from '../gitlink.js';
|
|
10
|
+
import { isAvailable, refuse } from '../platform.js';
|
|
10
11
|
export function useGitlink(deps) {
|
|
11
12
|
const { cwd, setNote } = deps;
|
|
12
13
|
/**
|
|
@@ -21,6 +22,12 @@ export function useGitlink(deps) {
|
|
|
21
22
|
* qui invece che scoperto dopo.
|
|
22
23
|
*/
|
|
23
24
|
const enabled = useMemo(() => hasGitmodules(cwd), [cwd]);
|
|
25
|
+
/** T175 — il segmento esiste (c'è `.gitmodules`) ma lo script che lo misura
|
|
26
|
+
* non gira su questa piattaforma: l'indicatore dice `n/d` al posto del
|
|
27
|
+
* valore, nessun timer parte, `^U` lo spiega. Due gate e non uno, perché sono
|
|
28
|
+
* due fatti diversi: un repo senza submodule e una piattaforma senza bash. */
|
|
29
|
+
const available = isAvailable('pluginScripts');
|
|
30
|
+
const active = enabled && available;
|
|
24
31
|
const [members, setMembers] = useState([]);
|
|
25
32
|
const [ok, setOk] = useState(true);
|
|
26
33
|
/** Almeno un tentativo è stato fatto: distingue «non ho ancora misurato» da
|
|
@@ -65,12 +72,12 @@ export function useGitlink(deps) {
|
|
|
65
72
|
});
|
|
66
73
|
}, [cwd]);
|
|
67
74
|
useEffect(() => {
|
|
68
|
-
if (!
|
|
75
|
+
if (!active)
|
|
69
76
|
return;
|
|
70
77
|
run();
|
|
71
78
|
const id = setInterval(run, GITLINK_SCAN_INTERVAL_MS);
|
|
72
79
|
return () => clearInterval(id);
|
|
73
|
-
}, [
|
|
80
|
+
}, [active, run, epoch]);
|
|
74
81
|
/**
|
|
75
82
|
* Rimisura adesso e riparte col timer da zero.
|
|
76
83
|
*
|
|
@@ -100,6 +107,8 @@ export function useGitlink(deps) {
|
|
|
100
107
|
setNote('^U → questo progetto non ha submodule: nessun gitlink da bumpare');
|
|
101
108
|
return;
|
|
102
109
|
}
|
|
110
|
+
if (refuse('pluginScripts', '^U', setNote, 'bump del gitlink'))
|
|
111
|
+
return;
|
|
103
112
|
if (busy.current || bumping) {
|
|
104
113
|
setNote('^U → bump del gitlink già in corso');
|
|
105
114
|
return;
|
|
@@ -140,6 +149,7 @@ export function useGitlink(deps) {
|
|
|
140
149
|
}
|
|
141
150
|
return {
|
|
142
151
|
enabled,
|
|
152
|
+
available,
|
|
143
153
|
members,
|
|
144
154
|
ok,
|
|
145
155
|
scanned,
|
package/dist/overlays/wrap.js
CHANGED
|
@@ -15,6 +15,7 @@ import { pageStep, wrapListCapacity } from '../viewport.js';
|
|
|
15
15
|
import { cpLen, insertAt, removeAt } from '../layout.js';
|
|
16
16
|
import { sanitizeTyped } from '../glyphs.js';
|
|
17
17
|
import { WRAP_DEFAULT_PATH } from '../wrap-scan.js';
|
|
18
|
+
import { isAvailable, refuse } from '../platform.js';
|
|
18
19
|
export function useWrapOverlay(deps) {
|
|
19
20
|
const { cwd, rows, setMode, setNote, onApply } = deps;
|
|
20
21
|
// Lo stato dello scan vive QUI e non nel modello, sul precedente di
|
|
@@ -22,7 +23,10 @@ export function useWrapOverlay(deps) {
|
|
|
22
23
|
// numeri si leggono in testata a schermata chiusa, e la lista è una delle sue
|
|
23
24
|
// superfici invece che il suo contenuto. Tenerli in due posti significherebbe
|
|
24
25
|
// che l'indicatore e la lista possono dire cose diverse dello stesso scan.
|
|
25
|
-
|
|
26
|
+
// T175 — su una piattaforma senza lo scan, nessun periodico e nessuno spawn:
|
|
27
|
+
// l'indicatore dice `n/d` e i due tasti lo spiegano.
|
|
28
|
+
const available = isAvailable('hardWrap');
|
|
29
|
+
const measure = useWrapScan(cwd, available);
|
|
26
30
|
/** Le righe FOTOGRAFATE all'apertura: uno scan che finisse mentre la lista è
|
|
27
31
|
* a schermo la riscriverebbe sotto gli occhi, e la posizione di scroll non
|
|
28
32
|
* avrebbe più un riferimento. Stessa regola del viewer del project status. */
|
|
@@ -41,6 +45,8 @@ export function useWrapOverlay(deps) {
|
|
|
41
45
|
* uno solo. È la stessa forma di `^O` senza cache.
|
|
42
46
|
*/
|
|
43
47
|
function open() {
|
|
48
|
+
if (refuse('hardWrap', '^W', setNote, 'lista hard-wrap'))
|
|
49
|
+
return;
|
|
44
50
|
if (measure.files.length === 0) {
|
|
45
51
|
setNote(measure.mtime === null
|
|
46
52
|
? '^W → hard-wrap non ancora misurato · ^E per lo scan'
|
|
@@ -117,6 +123,8 @@ export function useWrapOverlay(deps) {
|
|
|
117
123
|
* l'albero del progetto intero, quindi dura, e un tasto che non desse segno
|
|
118
124
|
* si leggerebbe come inerte. */
|
|
119
125
|
function scan() {
|
|
126
|
+
if (refuse('hardWrap', '^E', setNote))
|
|
127
|
+
return;
|
|
120
128
|
if (measure.scanning) {
|
|
121
129
|
setNote('^E → scan hard-wrap già in corso');
|
|
122
130
|
return;
|
|
@@ -136,6 +144,7 @@ export function useWrapOverlay(deps) {
|
|
|
136
144
|
mtime: measure.mtime,
|
|
137
145
|
ok: measure.ok,
|
|
138
146
|
scanning: measure.scanning,
|
|
147
|
+
available,
|
|
139
148
|
open,
|
|
140
149
|
scan,
|
|
141
150
|
onKey,
|
package/dist/pane-views.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
// - `↑N`/`↓N` non stanno qui: contano elementi fuori finestra per altezza del
|
|
22
22
|
// terminale, cioè una POSIZIONE, non un insieme.
|
|
23
23
|
import { isVisible, groupHierarchy } from './view.js';
|
|
24
|
+
import { isAvailable, offText, OFF_MARK } from './platform.js';
|
|
24
25
|
export const TASK_VIEWS = [
|
|
25
26
|
{
|
|
26
27
|
id: 'tasks',
|
|
@@ -78,12 +79,21 @@ export const SESSION_VIEWS = [
|
|
|
78
79
|
empty: null,
|
|
79
80
|
},
|
|
80
81
|
{
|
|
82
|
+
// T175 — dove la liveness manca, la voce resta nel catalogo (D1 create: le
|
|
83
|
+
// voci non spariscono) ma il numero no: `●0 vive` direbbe «ho guardato e
|
|
84
|
+
// nessuna è aperta», cioè uno stato misurato che non esiste. Etichetta e
|
|
85
|
+
// nota vuota si leggono alla chiamata, non all'import, così la piattaforma
|
|
86
|
+
// dei test si cambia fra un caso e l'altro.
|
|
81
87
|
id: 'live',
|
|
82
|
-
label: (c) => `${LIVE_GLYPH}${c.live} vive`,
|
|
88
|
+
label: (c) => isAvailable('liveSessions') ? `${LIVE_GLYPH}${c.live} vive` : `${LIVE_GLYPH} vive ${OFF_MARK}`,
|
|
83
89
|
count: (c) => c.live,
|
|
84
90
|
rows: (ctx) => ctx.assembled.rows.filter((r) => ctx.isLive(r.sessionId)),
|
|
85
91
|
color: 'green',
|
|
86
|
-
empty
|
|
92
|
+
get empty() {
|
|
93
|
+
return isAvailable('liveSessions')
|
|
94
|
+
? 'nessuna conversazione viva in questa lista'
|
|
95
|
+
: offText('liveSessions');
|
|
96
|
+
},
|
|
87
97
|
},
|
|
88
98
|
{
|
|
89
99
|
// T133 D7 — sola vista che NON filtra la lista: il suo insieme è più largo
|
package/dist/platform.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// T175 — COSA il deck sa fare sulla piattaforma su cui gira.
|
|
2
|
+
//
|
|
3
|
+
// Un modulo solo e non un `process.platform` sparso nei file: la domanda «questa
|
|
4
|
+
// funzione c'è qui?» ha una risposta sola, e la mappa di cosa è spento e perché
|
|
5
|
+
// si legge in un posto invece che cercando dieci `if`. Le figlie di porting
|
|
6
|
+
// dell'epica T173 riaccendono una funzione TOGLIENDO UNA VOCE da `UNAVAILABLE`:
|
|
7
|
+
// i tasti, i pane e gli scan leggono la mappa, non la piattaforma.
|
|
8
|
+
//
|
|
9
|
+
// Modulo PURO: nessun React, nessun I/O. L'unico ingresso esterno è la
|
|
10
|
+
// piattaforma stessa, letta a ogni chiamata (`platform()`), così un test può
|
|
11
|
+
// cambiarla fra un caso e l'altro senza re-importare il modulo.
|
|
12
|
+
/**
|
|
13
|
+
* LA MAPPA: per piattaforma, le funzioni spente. Una piattaforma assente le ha
|
|
14
|
+
* tutte — è il comportamento di prima di T175 su Linux e su macOS.
|
|
15
|
+
*
|
|
16
|
+
* Il commento accanto a ogni voce nomina la figlia di T173 che la riaccende.
|
|
17
|
+
*/
|
|
18
|
+
export const UNAVAILABLE = {
|
|
19
|
+
win32: [
|
|
20
|
+
'liveSessions', // T177 — liveness senza /proc; spegne anche resume e restore
|
|
21
|
+
'pluginScripts', // T178 — .sh del plugin via Git Bash
|
|
22
|
+
'hardWrap', // T178 — python del wrap-scan
|
|
23
|
+
'launch', // T179 — comando per piattaforma nelle voci launch[]
|
|
24
|
+
'notify', // T180 — toast al posto di notify-send
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
/** Come una funzione si nomina in un avviso, quando il chiamante non ne dà un
|
|
28
|
+
* nome più preciso (il tasto che l'ha chiesta, per esempio). */
|
|
29
|
+
const FEATURE_LABEL = {
|
|
30
|
+
spawn: 'apertura di tab e conversazioni',
|
|
31
|
+
liveSessions: 'sessioni vive',
|
|
32
|
+
pluginScripts: 'script del plugin',
|
|
33
|
+
hardWrap: 'scan hard-wrap',
|
|
34
|
+
launch: 'voci launch',
|
|
35
|
+
notify: 'notifiche',
|
|
36
|
+
};
|
|
37
|
+
const PLATFORM_NAME = {
|
|
38
|
+
win32: 'Windows',
|
|
39
|
+
linux: 'Linux',
|
|
40
|
+
darwin: 'macOS',
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Il marcatore di una funzione spenta, al posto del valore: in un indicatore
|
|
44
|
+
* della testata (`gitlink n/d`), in un'etichetta di vista (`● vive n/d`).
|
|
45
|
+
*
|
|
46
|
+
* Il marcatore SOSTITUISCE il valore e non lo nasconde: un indicatore assente è
|
|
47
|
+
* già uno stato (il gitlink non disegnato vuol dire «repo senza `.gitmodules`»),
|
|
48
|
+
* e un `0` direbbe «ho misurato e non c'è niente».
|
|
49
|
+
*/
|
|
50
|
+
export const OFF_MARK = 'n/d';
|
|
51
|
+
/**
|
|
52
|
+
* La piattaforma delle FUNZIONI. `LOOM_DECK_PLATFORM` la sostituisce, ed esiste
|
|
53
|
+
* per i test: rende verificabile su Linux il deck di Windows, avvisi compresi.
|
|
54
|
+
*
|
|
55
|
+
* Vale solo per la mappa delle funzioni. La semantica dei path (`project-path.ts`)
|
|
56
|
+
* legge la piattaforma vera, perché descrive il filesystem e non una scelta.
|
|
57
|
+
*/
|
|
58
|
+
export function platform() {
|
|
59
|
+
return process.env.LOOM_DECK_PLATFORM || process.platform;
|
|
60
|
+
}
|
|
61
|
+
export function platformName(p = platform()) {
|
|
62
|
+
return PLATFORM_NAME[p] ?? p;
|
|
63
|
+
}
|
|
64
|
+
/** La funzione c'è su questa piattaforma. */
|
|
65
|
+
export function isAvailable(feature, p = platform()) {
|
|
66
|
+
return !(UNAVAILABLE[p] ?? []).includes(feature);
|
|
67
|
+
}
|
|
68
|
+
/** Il testo di una funzione spenta, per un pane o una vista: nomina la funzione
|
|
69
|
+
* e la piattaforma. `n/d` e non «non disponibile»: resta invariato davanti a un
|
|
70
|
+
* nome singolare o plurale. */
|
|
71
|
+
export function offText(feature, what, p = platform()) {
|
|
72
|
+
return `${what ?? FEATURE_LABEL[feature]} ${OFF_MARK} su ${platformName(p)}`;
|
|
73
|
+
}
|
|
74
|
+
/** La riga di stato di un tasto spento: il tasto, la funzione, la piattaforma. */
|
|
75
|
+
export function offNote(key, feature, what, p = platform()) {
|
|
76
|
+
return `⚠ ${key} → ${offText(feature, what, p)}`;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* La GUARDIA di un tasto: `true` se la funzione è spenta, e in quel caso
|
|
80
|
+
* l'avviso è già nella riga di stato. Il chiamante esce e basta:
|
|
81
|
+
*
|
|
82
|
+
* if (refuse('spawn', '^K', setNote)) return;
|
|
83
|
+
*
|
|
84
|
+
* Un tasto spento che non facesse niente in silenzio sarebbe indistinguibile da
|
|
85
|
+
* un binding mancante, e uno che arrivasse fino al processo esterno fallirebbe
|
|
86
|
+
* con un messaggio che non nomina la piattaforma («ptyxis non lanciabile»).
|
|
87
|
+
*/
|
|
88
|
+
export function refuse(feature, key, setNote, what) {
|
|
89
|
+
if (isAvailable(feature))
|
|
90
|
+
return false;
|
|
91
|
+
setNote(offNote(key, feature, what));
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// T175 — i confronti di path che il deck fa sulla root del progetto, per
|
|
2
|
+
// piattaforma.
|
|
3
|
+
//
|
|
4
|
+
// Tre punti confrontavano path come stringhe POSIX: il filtro delle
|
|
5
|
+
// conversazioni del progetto (`sessions.ts`), quello delle sessioni vive
|
|
6
|
+
// (`live-sessions.ts`) e il nome del progetto (`deck-model.ts`). Su Linux la
|
|
7
|
+
// stringa È il path; su Windows no, per tre ragioni misurate (verdetto 1 di T174):
|
|
8
|
+
//
|
|
9
|
+
// - NTFS non distingue le maiuscole, e Claude Code non normalizza la grafia
|
|
10
|
+
// della cwd: VS Code passa `c:\…`, Windows Terminal `C:\…`, e lo stesso
|
|
11
|
+
// progetto ha due cartelle di transcript (`c--…` e `C--…`);
|
|
12
|
+
// - `\` e `/` sono entrambi separatori;
|
|
13
|
+
// - nei transcript `cwd` compare anche in forma MSYS (`/c/…`), quella di Git
|
|
14
|
+
// Bash.
|
|
15
|
+
//
|
|
16
|
+
// Modulo PURO. La piattaforma è un parametro con default quella VERA, e non
|
|
17
|
+
// quella di `LOOM_DECK_PLATFORM` (`platform.ts`): qui si descrive il filesystem
|
|
18
|
+
// su cui il deck gira, non una funzione che si può spegnere.
|
|
19
|
+
import { posix, win32 } from 'node:path';
|
|
20
|
+
/**
|
|
21
|
+
* La forma di un path su cui si confronta, mai quella che si mostra.
|
|
22
|
+
*
|
|
23
|
+
* Su win32: separatori portati a `\`, forma MSYS `/c/…` riportata a `C:\…`,
|
|
24
|
+
* tutto minuscolo, nessun separatore in coda (tranne la radice del drive, dove
|
|
25
|
+
* `C:` da solo vorrebbe dire «la cartella corrente di C:»). Altrove il path così
|
|
26
|
+
* com'è, senza il separatore in coda: il confronto resta case-sensitive come il
|
|
27
|
+
* filesystem.
|
|
28
|
+
*/
|
|
29
|
+
export function comparablePath(p, platform = process.platform) {
|
|
30
|
+
if (platform !== 'win32')
|
|
31
|
+
return p.length > 1 ? p.replace(/\/+$/, '') : p;
|
|
32
|
+
let s = p.replace(/\//g, '\\');
|
|
33
|
+
// `\c\Users\…` (la forma MSYS dopo la sostituzione dei separatori) →
|
|
34
|
+
// `c:\Users\…`. Un UNC (`\\server\…`) non matcha: il secondo carattere è `\`.
|
|
35
|
+
const msys = /^\\([a-zA-Z])(?:\\|$)/.exec(s);
|
|
36
|
+
if (msys)
|
|
37
|
+
s = `${msys[1]}:\\${s.slice(msys[0].length)}`;
|
|
38
|
+
s = s.replace(/\\+$/, '');
|
|
39
|
+
if (/^[a-zA-Z]:$/.test(s))
|
|
40
|
+
s += '\\';
|
|
41
|
+
return s.toLowerCase();
|
|
42
|
+
}
|
|
43
|
+
/** `path` è la root stessa o sta sotto di lei. Mai un prefisso di nome: la root
|
|
44
|
+
* `C:\a\loom` non contiene `C:\a\loom-works`. */
|
|
45
|
+
export function isWithin(path, root, platform = process.platform) {
|
|
46
|
+
const p = comparablePath(path, platform);
|
|
47
|
+
const r = comparablePath(root, platform);
|
|
48
|
+
if (p === r)
|
|
49
|
+
return true;
|
|
50
|
+
const sep = platform === 'win32' ? '\\' : '/';
|
|
51
|
+
return p.startsWith(r.endsWith(sep) ? r : r + sep);
|
|
52
|
+
}
|
|
53
|
+
/** Il nome della cartella del progetto: l'ultimo segmento della root, con i
|
|
54
|
+
* separatori della piattaforma. Root senza segmenti (`/`, `C:\`) → la root. */
|
|
55
|
+
export function projectBaseName(cwd, platform = process.platform) {
|
|
56
|
+
return (platform === 'win32' ? win32 : posix).basename(cwd) || cwd;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Le cartelle di transcript del progetto, fra i nomi di `~/.claude/projects/`.
|
|
60
|
+
*
|
|
61
|
+
* Su Linux una sola, il nome esatto. Su win32 ogni nome che coincide ignorando
|
|
62
|
+
* le maiuscole: la stessa root avviata da VS Code (`c:\…`) e da Windows
|
|
63
|
+
* Terminal (`C:\…`) scrive in `c--…` e in `C--…`, e leggerne una sola perde le
|
|
64
|
+
* conversazioni dell'altra.
|
|
65
|
+
*/
|
|
66
|
+
export function matchingDirNames(entries, name, platform = process.platform) {
|
|
67
|
+
if (platform !== 'win32')
|
|
68
|
+
return entries.filter((e) => e === name);
|
|
69
|
+
const want = name.toLowerCase();
|
|
70
|
+
return entries.filter((e) => e.toLowerCase() === want);
|
|
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/purge.js
CHANGED
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
|