@lamemind/loom-deck 0.74.0 → 0.76.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 +1 -1
- package/dist/actions.js +76 -8
- package/dist/archivable.js +1 -1
- package/dist/claude-cmd.js +101 -0
- package/dist/cli.js +21 -8
- package/dist/commit-times.js +5 -2
- package/dist/config.js +51 -6
- package/dist/deck-model.js +14 -9
- package/dist/doc-tree.js +1 -1
- package/dist/focus-session.js +139 -0
- 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 +1 -1
- package/dist/input.js +11 -1
- 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 +94 -0
- package/dist/project-path.js +71 -0
- package/dist/purge.js +1 -0
- package/dist/sessions.js +63 -29
- package/dist/spawn.js +27 -5
- package/dist/tasks.js +8 -21
- package/dist/ui/doc-pane.js +6 -2
- package/dist/ui/inbox-pane.js +7 -2
- package/dist/wrap-scan.js +1 -1
- package/package.json +2 -2
- package/dist/overlays/inbox.js +0 -90
- package/dist/ui/inbox-screen.js +0 -21
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
}
|
package/dist/purge.js
CHANGED
package/dist/sessions.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { basename, join } from 'node:path';
|
|
4
4
|
import { sanitize } from './width.js';
|
|
5
|
+
import { isWithin, matchingDirNames } from './project-path.js';
|
|
5
6
|
// CC codifica la project dir sostituendo ogni char non-alfanumerico con '-'
|
|
6
7
|
// (verificato: `/home/lamemind/cc-host` → `-home-lamemind-cc-host`). È LOSSY
|
|
7
8
|
// (un '-' nel path e un '/' collassano nello stesso char) → il forward è
|
|
@@ -308,51 +309,84 @@ export function compareSessions(a, b) {
|
|
|
308
309
|
return b.lastReplyTs - a.lastReplyTs;
|
|
309
310
|
return b.ts - a.ts;
|
|
310
311
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
312
|
+
/**
|
|
313
|
+
* T175 — le cartelle di transcript del progetto.
|
|
314
|
+
*
|
|
315
|
+
* Su Linux una sola, il nome esatto, e senza leggere la cartella madre: il poll
|
|
316
|
+
* gira ogni 1,5 s, e un `readdir` di `~/.claude/projects/` costerebbe a ogni
|
|
317
|
+
* tick per trovare un nome già noto. Su win32 tutte quelle che coincidono a
|
|
318
|
+
* meno delle maiuscole (`matchingDirNames`): Claude Code non normalizza la
|
|
319
|
+
* lettera del drive, e lo stesso progetto ne ha due.
|
|
320
|
+
*/
|
|
321
|
+
function transcriptDirs(projectRoot) {
|
|
322
|
+
const root = claudeProjectsRoot();
|
|
323
|
+
const name = projectDirName(projectRoot);
|
|
324
|
+
if (process.platform !== 'win32')
|
|
325
|
+
return [join(root, name)];
|
|
326
|
+
let entries;
|
|
317
327
|
try {
|
|
318
|
-
|
|
328
|
+
entries = readdirSync(root);
|
|
319
329
|
}
|
|
320
330
|
catch {
|
|
321
331
|
return [];
|
|
322
332
|
}
|
|
323
|
-
|
|
333
|
+
return matchingDirNames(entries, name).map((e) => join(root, e));
|
|
334
|
+
}
|
|
335
|
+
// Discovery read-only delle sessioni del SOLO progetto corrente (D2 preflight
|
|
336
|
+
// T27): legge le project dir calcolate dal forward-transform, filtra per cwd
|
|
337
|
+
// (difesa contro le collisioni lossy del naming), ordina con `compareSessions`.
|
|
338
|
+
//
|
|
339
|
+
// D1 preflight T175 — restano fuori le conversazioni AVVIATE in una
|
|
340
|
+
// sottocartella, su ogni piattaforma: Claude Code ne scrive il transcript nella
|
|
341
|
+
// cartella che codifica la cwd di avvio, cioè un'altra. Il filtro accetta una
|
|
342
|
+
// cwd sotto la root perché non scarta niente di legittimo, non per raccoglierle.
|
|
343
|
+
export function discoverProjectSessions(projectRoot) {
|
|
344
|
+
// Keyed sul sessionId: su win32 due cartelle dello stesso progetto potrebbero
|
|
345
|
+
// portare lo stesso transcript, e due righe con un id solo romperebbero la
|
|
346
|
+
// selezione. Vince il più recente.
|
|
347
|
+
const out = new Map();
|
|
324
348
|
const seen = new Set();
|
|
325
|
-
for (const
|
|
326
|
-
|
|
327
|
-
seen.add(path);
|
|
328
|
-
let mtime;
|
|
329
|
-
let sizeBytes;
|
|
349
|
+
for (const dir of transcriptDirs(projectRoot)) {
|
|
350
|
+
let files;
|
|
330
351
|
try {
|
|
331
|
-
|
|
332
|
-
mtime = st.mtimeMs;
|
|
333
|
-
sizeBytes = st.size;
|
|
352
|
+
files = readdirSync(dir).filter((f) => f.endsWith('.jsonl'));
|
|
334
353
|
}
|
|
335
354
|
catch {
|
|
336
355
|
continue;
|
|
337
356
|
}
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
357
|
+
for (const f of files) {
|
|
358
|
+
const path = join(dir, f);
|
|
359
|
+
seen.add(path);
|
|
360
|
+
let mtime;
|
|
361
|
+
let sizeBytes;
|
|
362
|
+
try {
|
|
363
|
+
const st = statSync(path);
|
|
364
|
+
mtime = st.mtimeMs;
|
|
365
|
+
sizeBytes = st.size;
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
const cached = cache.get(path);
|
|
371
|
+
let session;
|
|
372
|
+
if (cached && cached.mtime === mtime) {
|
|
373
|
+
session = cached.session;
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
session = parseSessionFile(path, mtime, sizeBytes);
|
|
377
|
+
cache.set(path, { mtime, session });
|
|
378
|
+
}
|
|
379
|
+
if (!session || !isWithin(session.cwd, projectRoot))
|
|
380
|
+
continue;
|
|
381
|
+
const prev = out.get(session.sessionId);
|
|
382
|
+
if (!prev || session.ts > prev.ts)
|
|
383
|
+
out.set(session.sessionId, session);
|
|
349
384
|
}
|
|
350
385
|
}
|
|
351
386
|
for (const key of cache.keys())
|
|
352
387
|
if (!seen.has(key))
|
|
353
388
|
cache.delete(key);
|
|
354
|
-
out.sort(compareSessions);
|
|
355
|
-
return out;
|
|
389
|
+
return [...out.values()].sort(compareSessions);
|
|
356
390
|
}
|
|
357
391
|
// Raggruppa per gitBranch (D2: group-by-branch nel progetto corrente). Ordine
|
|
358
392
|
// dei gruppi = la prima sessione di ciascuno, con `compareSessions`; dentro il
|
package/dist/spawn.js
CHANGED
|
@@ -8,6 +8,9 @@ import { dirname, join } from 'node:path';
|
|
|
8
8
|
// è DATO PURO e non importa questo file: la dipendenza va in un verso solo.
|
|
9
9
|
// Qui restano i soli effetti verso l'esterno — comporre un argv e lanciarlo.
|
|
10
10
|
import { MODEL_DEFAULT, PROJECT_STATUS_MODEL } from './spawn-catalog.js';
|
|
11
|
+
import { isAvailable } from './platform.js';
|
|
12
|
+
import { claudeLauncher } from './claude-cmd.js';
|
|
13
|
+
import { claudeEnv } from './git-bash.js';
|
|
11
14
|
// scripts/deck-run è un sibling della dir del bundle: src/ (dev, tsx) e dist/
|
|
12
15
|
// (build, node) stanno entrambi sotto la package root → risalita di un livello.
|
|
13
16
|
export const DECK_RUN = join(dirname(fileURLToPath(import.meta.url)), '..', 'scripts', 'deck-run');
|
|
@@ -27,9 +30,14 @@ export const DECK_RUN = join(dirname(fileURLToPath(import.meta.url)), '..', 'scr
|
|
|
27
30
|
* test misura resta identico.
|
|
28
31
|
*/
|
|
29
32
|
export const NO_SPAWN = process.env.LOOM_DECK_NO_SPAWN === '1';
|
|
33
|
+
// T175 — `windowsHide` su OGNI figlio, qui e non a ogni chiamante: su Windows
|
|
34
|
+
// un processo console lanciato senza apre una finestra (un `claude -p`, un
|
|
35
|
+
// `git`), e ogni sede di spawn che la dimenticasse ne farebbe lampeggiare una a
|
|
36
|
+
// ogni giro. Su Linux l'opzione non ha effetto. Un chiamante può ancora
|
|
37
|
+
// sovrascriverla, perché `opts` viene dopo.
|
|
30
38
|
export function spawnOut(cmd, args, opts) {
|
|
31
39
|
if (!NO_SPAWN)
|
|
32
|
-
return spawn(cmd, args, opts);
|
|
40
|
+
return spawn(cmd, args, { windowsHide: true, ...opts });
|
|
33
41
|
// Figlio inerte: emette nulla, quindi i `.on('error'|'close')` dei chiamanti
|
|
34
42
|
// restano appesi senza mai scattare — che è esattamente "non è successo niente".
|
|
35
43
|
const fake = new EventEmitter();
|
|
@@ -347,8 +355,11 @@ export function spawnTerminal(cwd, title) {
|
|
|
347
355
|
child.unref();
|
|
348
356
|
return child;
|
|
349
357
|
}
|
|
350
|
-
//
|
|
351
|
-
|
|
358
|
+
// Il NOME di claude nelle note della riga di stato: l'override se c'è,
|
|
359
|
+
// `claude` altrimenti. Non è ciò che si esegue — quello lo risolve
|
|
360
|
+
// `claudeLauncher` (`claude-cmd.ts`), che su Windows può arrivare a
|
|
361
|
+
// `node …cli.js` da uno shim npm: nelle note resta il nome che l'utente conosce.
|
|
362
|
+
export const CLAUDE_CMD = process.env.LOOM_DECK_CLAUDE_CMD || 'claude';
|
|
352
363
|
// T30: invocazione di una skill del plugin da CC HEADLESS (`-p`), con
|
|
353
364
|
// `--session-id` pinnato. È la TERZA strada per eseguire uno script del plugin
|
|
354
365
|
// da un consumer che non ha `CLAUDE_PLUGIN_ROOT`: il path version-pinned della
|
|
@@ -365,7 +376,12 @@ export const CLAUDE_CMD = process.env.LOOM_DECK_CLAUDE_CMD ?? 'claude';
|
|
|
365
376
|
// ha funzionato" — che su un'operazione distruttiva non basta a sapere se
|
|
366
377
|
// qualcosa è stato rimosso.
|
|
367
378
|
function spawnSkill(prompt, cwd, sessionId, onResult, extraArgs = []) {
|
|
368
|
-
|
|
379
|
+
// T175 — il lanciatore risolto (shim npm compreso, mai una shell) e, su
|
|
380
|
+
// Windows, l'env col bash di Git: senza, un deck avviato da Git Bash passa al
|
|
381
|
+
// figlio il `PATH` che manda gli hook del plugin in PowerShell.
|
|
382
|
+
const claude = claudeLauncher();
|
|
383
|
+
const child = spawnOut(claude.cmd, [
|
|
384
|
+
...claude.prefix,
|
|
369
385
|
'-p',
|
|
370
386
|
'--output-format',
|
|
371
387
|
'stream-json',
|
|
@@ -374,7 +390,7 @@ function spawnSkill(prompt, cwd, sessionId, onResult, extraArgs = []) {
|
|
|
374
390
|
sessionId,
|
|
375
391
|
...extraArgs,
|
|
376
392
|
prompt,
|
|
377
|
-
], { cwd, detached: true, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
393
|
+
], { cwd, detached: true, stdio: ['ignore', 'pipe', 'pipe'], env: claudeEnv() });
|
|
378
394
|
let buf = '';
|
|
379
395
|
let isError = null;
|
|
380
396
|
let detail = '';
|
|
@@ -491,7 +507,13 @@ export const NOTIFY_CMD = process.env.LOOM_DECK_NOTIFY_CMD ?? 'notify-send';
|
|
|
491
507
|
// Agganciato, la degradazione è MUTA (D6): un binario assente è una proprietà
|
|
492
508
|
// stabile della macchina, e dirlo a ogni generazione sarebbe lo stesso messaggio
|
|
493
509
|
// a ogni giro su una funzione che non è il deliverable.
|
|
510
|
+
//
|
|
511
|
+
// T175 — dove le notifiche non ci sono, nessuno spawn e nessun avviso: non
|
|
512
|
+
// hanno un tasto né un pane, e l'esito del project status è già nella riga di
|
|
513
|
+
// stato. Il `null` dice al chiamante che niente è partito.
|
|
494
514
|
export function notifyDone(title, body) {
|
|
515
|
+
if (!isAvailable('notify'))
|
|
516
|
+
return null;
|
|
495
517
|
const child = spawnOut(NOTIFY_CMD, ['--app-name=loom-deck', title, body], {
|
|
496
518
|
detached: true,
|
|
497
519
|
stdio: 'ignore',
|
package/dist/tasks.js
CHANGED
|
@@ -8,29 +8,16 @@ import { sanitize } from './width.js';
|
|
|
8
8
|
* divergano (uno accetterebbe righe che l'altro rifiuta).
|
|
9
9
|
*/
|
|
10
10
|
export const TASK_ID_RE = /^T\d+$/;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
* mano — nasce da compass, che legge `docsRoot` dal registry dconf e lo
|
|
17
|
-
* antepone al comando, o dal custom-command del profilo Ptyxis, che ce l'ha
|
|
18
|
-
* cablato — quindi l'env c'è sempre nei percorsi di avvio reali.
|
|
19
|
-
*
|
|
20
|
-
* T134 — funzione e non più letterale ripetuto: la legge anche lo scan della
|
|
21
|
-
* coda inbox, e due copie della stessa cascata divergerebbero appena una delle
|
|
22
|
-
* due imparasse a leggere il file config.
|
|
23
|
-
*/
|
|
24
|
-
export function docsRootName() {
|
|
25
|
-
return process.env.LOOM_DECK_DOCS_ROOT || 'docs';
|
|
26
|
-
}
|
|
27
|
-
export function resolveTasksPath(cwd = process.cwd()) {
|
|
28
|
-
return join(cwd, docsRootName(), 'tasks.md');
|
|
11
|
+
// La docs-root arriva dal chiamante, già risolta: la cascata che la sceglie
|
|
12
|
+
// (`docsRootName` in `config.ts`) gira una volta per vita del deck, e i due path
|
|
13
|
+
// qui sotto devono nascere dallo stesso valore che leggono gli scan del modo doc.
|
|
14
|
+
export function resolveTasksPath(cwd, docsRoot) {
|
|
15
|
+
return join(cwd, docsRoot, 'tasks.md');
|
|
29
16
|
}
|
|
30
17
|
// I task file vivono in `<docsRoot>/tasks/` — sibling di tasks.md. Derivo la
|
|
31
|
-
// dir dallo stesso path
|
|
32
|
-
export function resolveTasksDir(cwd
|
|
33
|
-
return join(dirname(resolveTasksPath(cwd)), 'tasks');
|
|
18
|
+
// dir dallo stesso path, così le due non possono puntare a docs-root diverse.
|
|
19
|
+
export function resolveTasksDir(cwd, docsRoot) {
|
|
20
|
+
return join(dirname(resolveTasksPath(cwd, docsRoot)), 'tasks');
|
|
34
21
|
}
|
|
35
22
|
/**
|
|
36
23
|
* Indici di colonna della Tasks Overview letti dall'HEADER, non cablati.
|
package/dist/ui/doc-pane.js
CHANGED
|
@@ -20,6 +20,7 @@ import { DOC_FLAG_W, flagCell } from '../doc-tree.js';
|
|
|
20
20
|
import { docView } from '../doc-views.js';
|
|
21
21
|
import { docHeaderParts } from '../pane-header.js';
|
|
22
22
|
import { HeaderLine } from './header-line.js';
|
|
23
|
+
import { offText } from '../platform.js';
|
|
23
24
|
/**
|
|
24
25
|
* Rientro di un livello dell'albero: UNA colonna, non due.
|
|
25
26
|
*
|
|
@@ -53,14 +54,17 @@ export function DocHeader({ counts, active, above, below, focused, columns, hasD
|
|
|
53
54
|
const { parts, shown } = docHeaderParts(counts, active, above, below, columns, hasDirs);
|
|
54
55
|
return _jsx(HeaderLine, { parts: parts, shown: shown, focused: focused });
|
|
55
56
|
}
|
|
56
|
-
export function DocPane({ rows, counts, activeView, paneCount, selectedPath, focused, above, below, columns, ok, scanned, scanning, hasDirs, }) {
|
|
57
|
+
export function DocPane({ rows, counts, activeView, paneCount, selectedPath, focused, above, below, columns, ok, scanned, scanning, hasDirs, available = true, }) {
|
|
57
58
|
const width = paneTextWidth(columns);
|
|
58
59
|
// Cella elastica per sottrazione dalle colonne fisse, come nel pane inbox e
|
|
59
60
|
// nella lista sessioni. Pavimento 0 e non un minimo di cortesia: è un tetto,
|
|
60
61
|
// non una preferenza, e alzarlo sopra lo spazio reale fa uscire la riga dal
|
|
61
62
|
// pane mangiandone il bordo (invariante ③ di width.ts).
|
|
62
63
|
const nameW = Math.max(0, width - (2 /* caret */ + DOC_FLAG_W + 1 /* gutter */));
|
|
63
|
-
return (_jsxs(Box, { flexDirection: "column", width: "50%", marginRight: 1, borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', paddingX: 1, children: [_jsx(DocHeader, { counts: counts, active: activeView, above: above, below: below, focused: focused, columns: columns, hasDirs: hasDirs }),
|
|
64
|
+
return (_jsxs(Box, { flexDirection: "column", width: "50%", marginRight: 1, borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', paddingX: 1, children: [_jsx(DocHeader, { counts: counts, active: activeView, above: above, below: below, focused: focused, columns: columns, hasDirs: hasDirs }), !available ? (
|
|
65
|
+
// T175 — gemello del pane inbox: la misura qui non esiste, e va detto
|
|
66
|
+
// per nome invece di restare su «albero non ancora misurato».
|
|
67
|
+
_jsx(Text, { color: "yellow", wrap: "truncate-end", children: cut(offText('pluginScripts', 'albero doc'), width) })) : paneCount === 0 ? (
|
|
64
68
|
// Quattro vuoti diversi, e dirlo è il punto. Ai tre del pane inbox se ne
|
|
65
69
|
// aggiunge uno che solo questo scan ha: «sto misurando». Quattro secondi
|
|
66
70
|
// di wall clock sono abbastanza da vedersi, e un pane vuoto per quattro
|
package/dist/ui/inbox-pane.js
CHANGED
|
@@ -19,6 +19,7 @@ import { NATURA_SHORT, NATURA_W, inboxMark } from '../inbox.js';
|
|
|
19
19
|
import { inboxView } from '../inbox-views.js';
|
|
20
20
|
import { inboxHeaderParts } from '../pane-header.js';
|
|
21
21
|
import { HeaderLine } from './header-line.js';
|
|
22
|
+
import { offText } from '../platform.js';
|
|
22
23
|
/**
|
|
23
24
|
* Header del pane inbox: le parti e il loro taglio vengono da `pane-header.ts`,
|
|
24
25
|
* lo stesso modulo da cui `frame.ts` ricava le colonne cliccabili — una fonte
|
|
@@ -43,7 +44,7 @@ export function InboxHeader({ counts, active, above, below, focused, columns, })
|
|
|
43
44
|
* colonna fissa la paga la cella elastica del nome — che è l'unica cosa con cui
|
|
44
45
|
* si riconosce un file.
|
|
45
46
|
*/
|
|
46
|
-
export function InboxPane({ files, counts, activeView, paneCount, selectedPath, focused, above, below, columns, ok, scanned, }) {
|
|
47
|
+
export function InboxPane({ files, counts, activeView, paneCount, selectedPath, focused, above, below, columns, ok, scanned, available = true, }) {
|
|
47
48
|
const width = paneTextWidth(columns);
|
|
48
49
|
// La cella elastica si calcola per SOTTRAZIONE dalle colonne fisse, come il
|
|
49
50
|
// titolo della lista sessioni: pavimento 0 e non un minimo di cortesia — è un
|
|
@@ -58,7 +59,11 @@ export function InboxPane({ files, counts, activeView, paneCount, selectedPath,
|
|
|
58
59
|
1 /* gutter */ +
|
|
59
60
|
1 /* gutter prima della data */ +
|
|
60
61
|
ageW));
|
|
61
|
-
return (_jsxs(Box, { flexDirection: "column", width: "50%", borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', paddingX: 1, children: [_jsx(InboxHeader, { counts: counts, active: activeView, above: above, below: below, focused: focused, columns: columns }),
|
|
62
|
+
return (_jsxs(Box, { flexDirection: "column", width: "50%", borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', paddingX: 1, children: [_jsx(InboxHeader, { counts: counts, active: activeView, above: above, below: below, focused: focused, columns: columns }), !available ? (
|
|
63
|
+
// T175 — un quarto vuoto, il solo che non è uno stato della coda: la
|
|
64
|
+
// misura qui non esiste. Va detto per nome, o si leggerebbe come «coda
|
|
65
|
+
// non ancora misurata» per sempre.
|
|
66
|
+
_jsx(Text, { color: "yellow", wrap: "truncate-end", children: cut(offText('pluginScripts', 'coda inbox'), width) })) : paneCount === 0 ? (
|
|
62
67
|
// Tre vuoti diversi, e dirlo è il punto: «non ho ancora misurato» non è
|
|
63
68
|
// «ho misurato e non c'è niente», e «la misura si è rotta» non è nessuno
|
|
64
69
|
// dei due (D2 preflight). Una lista vuota che non dice quale dei tre è
|
package/dist/wrap-scan.js
CHANGED
|
@@ -145,7 +145,7 @@ export async function runWrapScan(projectRoot) {
|
|
|
145
145
|
if (!script)
|
|
146
146
|
return false;
|
|
147
147
|
try {
|
|
148
|
-
const { stdout } = await execFileAsync('python3', [script, '--root', projectRoot, '--scan'], { cwd: projectRoot, maxBuffer: 16 * 1024 * 1024 });
|
|
148
|
+
const { stdout } = await execFileAsync('python3', [script, '--root', projectRoot, '--scan'], { cwd: projectRoot, maxBuffer: 16 * 1024 * 1024, windowsHide: true });
|
|
149
149
|
const path = wrapCacheFile(projectRoot);
|
|
150
150
|
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
151
151
|
writeFileSync(path, stdout, { mode: 0o600 });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lamemind/loom-deck",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.76.0",
|
|
4
4
|
"description": "Deck TUI Ink per-progetto della famiglia loom: legge tasks.md e spawna sessioni Claude Code bound via LOOM_TASK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"scripts": {
|
|
14
14
|
"dev": "tsx src/cli.tsx",
|
|
15
15
|
"build": "tsc",
|
|
16
|
-
"test": "
|
|
16
|
+
"test": "node test/run.mjs",
|
|
17
17
|
"start": "node dist/cli.js",
|
|
18
18
|
"prepare": "tsc"
|
|
19
19
|
},
|
package/dist/overlays/inbox.js
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
// T134 — il DETAIL di un file inbox: sesta schermata sostitutiva.
|
|
2
|
-
//
|
|
3
|
-
// Stessa forma dello `sheet` della task e per la stessa ragione: il file è
|
|
4
|
-
// FOTOGRAFATO all'apertura — nome, natura, testo — e non è una vista sulla
|
|
5
|
-
// selezione corrente. L'overlay copre la lista, quindi l'oggetto dell'azione
|
|
6
|
-
// deve restare quello che si è scelto anche se un tick dello scan spostasse la
|
|
7
|
-
// selezione sotto.
|
|
8
|
-
//
|
|
9
|
-
// Molto più magro dello sheet: nessuna area di compilazione. Lo sheet ha
|
|
10
|
-
// quattro righe perché quattro parametri dello spawn sono una scelta (azione,
|
|
11
|
-
// prompt, modello, titolo); qui non ce n'è nessuna — la skill la decide la
|
|
12
|
-
// natura (D8), il modello è `MODEL_DEFAULT` per tutte e tre (D11 preflight), la sessione
|
|
13
|
-
// nasce nuda (D10 preflight). Restano la lettura e un tasto.
|
|
14
|
-
//
|
|
15
|
-
// Lo spawn NON sta qui: arriva come callback `onDrain`, per lo stesso confine
|
|
16
|
-
// di `useSheetOverlay` e `useSearchOverlay` — un hook di overlay non esce dal
|
|
17
|
-
// deck.
|
|
18
|
-
import { useMemo, useState } from 'react';
|
|
19
|
-
import { inboxDetailCapacity, pageStep } from '../viewport.js';
|
|
20
|
-
import { wrapWithOffsets } from '../width.js';
|
|
21
|
-
import { parseMarkdown } from '../markdown.js';
|
|
22
|
-
import { inboxPrompt } from '../inbox.js';
|
|
23
|
-
export function useInboxOverlay(deps) {
|
|
24
|
-
const { rows, columns, setMode, setNote, onDrain } = deps;
|
|
25
|
-
const [sheet, setSheet] = useState(null);
|
|
26
|
-
const [top, setTop] = useState(0);
|
|
27
|
-
// Cornici da scalare: box esterno (2 bordi + 2 padding) + box testo (2 bordi
|
|
28
|
-
// + 2 padding) = 8, come il detail della task.
|
|
29
|
-
const width = Math.max(20, (columns || 80) - 8);
|
|
30
|
-
// Il markdown si rende PRIMA del wrap: `**foo**` occupa 3 colonne rese e 7
|
|
31
|
-
// grezze, quindi wrappare sui marker manderebbe a capo su un conteggio che il
|
|
32
|
-
// terminale non disegna. Memo separato dal wrap perché il parse dipende solo
|
|
33
|
-
// dal testo: un resize ri-wrappa, non ri-parsa.
|
|
34
|
-
const doc = useMemo(() => (sheet?.text ? parseMarkdown(sheet.text) : null), [sheet]);
|
|
35
|
-
const lines = useMemo(() => (doc ? wrapWithOffsets(doc.text, width) : []), [doc, width]);
|
|
36
|
-
const capacity = inboxDetailCapacity(rows);
|
|
37
|
-
const maxTop = Math.max(0, lines.length - capacity);
|
|
38
|
-
/** Il prompt che partirà: derivato una volta e mostrato in fondo alla
|
|
39
|
-
* schermata, così `⏎` non è mai un salto nel buio. */
|
|
40
|
-
const prompt = sheet ? inboxPrompt(sheet.file) : '';
|
|
41
|
-
function open(next) {
|
|
42
|
-
setSheet(next);
|
|
43
|
-
setTop(0);
|
|
44
|
-
setNote('');
|
|
45
|
-
setMode('inbox');
|
|
46
|
-
}
|
|
47
|
-
function close() {
|
|
48
|
-
setMode('normal');
|
|
49
|
-
setSheet(null);
|
|
50
|
-
}
|
|
51
|
-
function scroll(delta) {
|
|
52
|
-
setTop((t) => Math.max(0, Math.min(maxTop, t + delta)));
|
|
53
|
-
}
|
|
54
|
-
function onKey(input, key) {
|
|
55
|
-
if (key.escape) {
|
|
56
|
-
close();
|
|
57
|
-
}
|
|
58
|
-
else if (key.return) {
|
|
59
|
-
// Si chiude PRIMA di spawnare, come lo sheet della task: la schermata ha
|
|
60
|
-
// finito il suo lavoro, e lasciarla aperta sopra una tab appena nata
|
|
61
|
-
// farebbe credere che ci sia dell'altro da decidere.
|
|
62
|
-
const s = sheet;
|
|
63
|
-
close();
|
|
64
|
-
if (s)
|
|
65
|
-
onDrain(s.file, inboxPrompt(s.file));
|
|
66
|
-
}
|
|
67
|
-
else if (key.upArrow) {
|
|
68
|
-
scroll(-1);
|
|
69
|
-
}
|
|
70
|
-
else if (key.downArrow) {
|
|
71
|
-
scroll(1);
|
|
72
|
-
}
|
|
73
|
-
else if (key.pageUp) {
|
|
74
|
-
scroll(-pageStep(capacity));
|
|
75
|
-
}
|
|
76
|
-
else if (key.pageDown) {
|
|
77
|
-
scroll(pageStep(capacity));
|
|
78
|
-
}
|
|
79
|
-
else if (input === 'g') {
|
|
80
|
-
// `g`/`G` sugli estremi, come reader, detail e project status: anche
|
|
81
|
-
// questa schermata scorre testo, quindi eredita lo stesso alfabeto invece
|
|
82
|
-
// di inventarne un secondo.
|
|
83
|
-
setTop(0);
|
|
84
|
-
}
|
|
85
|
-
else if (input === 'G') {
|
|
86
|
-
setTop(maxTop);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return { sheet, doc, lines, top, capacity, maxTop, prompt, open, onKey, scroll };
|
|
90
|
-
}
|
package/dist/ui/inbox-screen.js
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
// T134 — il detail di un file inbox: testo scorrevole più la riga che dichiara
|
|
3
|
-
// cosa partirà su `⏎`.
|
|
4
|
-
//
|
|
5
|
-
// Riusa `DetailLine` del detail della task invece di ri-renderizzare il
|
|
6
|
-
// markdown per conto proprio: sono lo stesso costrutto — un documento reso in
|
|
7
|
-
// span tipizzati dentro un box — e due rese dello stesso markdown
|
|
8
|
-
// divergerebbero al primo stile aggiunto. L'evidenziazione della ricerca non
|
|
9
|
-
// serve qui (nessuna ricerca dentro l'inbox), quindi le occorrenze arrivano
|
|
10
|
-
// vuote e il secondo taglio di `DetailLine` non gira nemmeno.
|
|
11
|
-
import { Box, Text } from 'ink';
|
|
12
|
-
import { cut } from '../width.js';
|
|
13
|
-
import { DetailLine } from './detail-screen.js';
|
|
14
|
-
import { INBOX_MARK, WARN, fmtDateTime, fmtSize } from '../glyphs.js';
|
|
15
|
-
import { NATURA_SHORT, inboxMark } from '../inbox.js';
|
|
16
|
-
export function InboxScreen({ file, missing, lines, spans, top, total, capacity, prompt, columns, }) {
|
|
17
|
-
const last = Math.min(total, top + capacity);
|
|
18
|
-
const width = Math.max(20, (columns || 80) - 4);
|
|
19
|
-
const mark = inboxMark(file);
|
|
20
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: [_jsx(Text, { color: "cyan", children: NATURA_SHORT[file.natura] }), ' ', cut(file.basename, Math.max(10, width - 8))] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [INBOX_MARK[mark] ? `${INBOX_MARK[mark]} ` : '', mark, file.branch ? ` ⟨${file.branch}⟩` : '', file.cappello ? ` · ${file.cappello}` : '', " \u00B7 ", file.nozioni, " nozioni \u00B7 ", file.aperte, ' ', "aperte \u00B7 ", fmtSize(file.chars), " \u00B7 ", fmtDateTime(file.created * 1000), missing ? '' : ` · righe ${total === 0 ? 0 : top + 1}-${last} di ${total}`] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "yellow", children: "\u2191\u2193/PgUp/PgDn" }), " testo \u00B7 ", _jsx(Text, { color: "yellow", children: "g/G" }), " estremi \u00B7", ' ', _jsx(Text, { color: "yellow", children: "\u23CE" }), " apre la sessione \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " chiude"] }), _jsx(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, marginTop: 1, children: missing ? (_jsxs(Text, { color: "yellow", wrap: "truncate-end", children: [WARN, " file non leggibile \u00B7 l'azione resta attiva (la skill risolve il file per nome)"] })) : (lines.map((l, i) => (_jsx(DetailLine, { line: l, spans: spans, occ: [], current: -1 }, top + i)))) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { dimColor: true, children: "\u23CE \u203A " }), _jsx(Text, { color: "green", children: cut(prompt, Math.max(10, width - 4)) })] }) })] }));
|
|
21
|
-
}
|