@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
package/README.md
CHANGED
|
@@ -419,7 +419,7 @@ npm test # node:test sul core vista (src/view.ts), senza Ink né termina
|
|
|
419
419
|
|
|
420
420
|
Il core di filtri e ordinamenti (`src/view.ts`) è **puro**: nessun import da Ink o React, nessun I/O. È il motivo per cui è testabile con `node:test` su array fixture, mentre la TUI resta un guscio sottile che lo consuma.
|
|
421
421
|
|
|
422
|
-
Il deck cerca `tasks.md` in `$PWD
|
|
422
|
+
Il deck cerca `tasks.md` in `$PWD/<docs-root>/tasks.md`. La docs-root si risolve in quest'ordine: la variabile `LOOM_DECK_DOCS_ROOT`, se impostata; il campo `docsRoot` di `$PWD/.claude/loom-works.json`; `docs`. Un progetto con docs-root non-standard la dichiara nel file, e il deck la trova anche avviato a mano.
|
|
423
423
|
|
|
424
424
|
La lista si **auto-aggiorna** quando `tasks.md` cambia sotto (poll su `mtime`, ~1.5s):
|
|
425
425
|
crei/checkpoint una task da un'altra sessione → il deck la riflette senza riavvio.
|
package/dist/actions.js
CHANGED
|
@@ -31,6 +31,8 @@ import { useTaskOps } from './task-ops.js';
|
|
|
31
31
|
import { inboxPrompt, inboxWords } from './inbox.js';
|
|
32
32
|
import { docTargetKind, docWords } from './doc-tree.js';
|
|
33
33
|
import { wrapWords } from './wrap-scan.js';
|
|
34
|
+
import { enterSession, focusSession } from './focus-session.js';
|
|
35
|
+
import { refuse } from './platform.js';
|
|
34
36
|
/**
|
|
35
37
|
* T181 — l'attesa massima di un passo del restore prima di passare al
|
|
36
38
|
* successivo. Un `deck-run` esce appena `ptyxis --tab` ha consegnato la tab
|
|
@@ -155,6 +157,10 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, overr
|
|
|
155
157
|
// della lista (^K/^P/^R), che passano di qui con `spawnNote` sul proprio
|
|
156
158
|
// default '' (P1 preflight) — nascono quindi titolati anche loro.
|
|
157
159
|
function spawnForTask(id, modelKind, spawnNote = '', prompt = '', priority = false) {
|
|
160
|
+
// T175 — prima del sidecar: un binding scritto per una tab che non si apre
|
|
161
|
+
// sarebbe una conversazione figlia della task che non esiste.
|
|
162
|
+
if (refuse('spawn', '⏎', setNote))
|
|
163
|
+
return;
|
|
158
164
|
const sid = randomUUID();
|
|
159
165
|
const note = spawnNote;
|
|
160
166
|
appendTaskBinding(cwd, sid, id);
|
|
@@ -184,6 +190,8 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, overr
|
|
|
184
190
|
// async: senza handler diventa uncaughtException e ucciderebbe il deck, che
|
|
185
191
|
// invece deve restare vivo.
|
|
186
192
|
function spawnTaskSession(kind, keyLabel) {
|
|
193
|
+
if (refuse('spawn', keyLabel, setNote))
|
|
194
|
+
return;
|
|
187
195
|
const task = selectedTaskOr(keyLabel, 'spawnare');
|
|
188
196
|
if (!task)
|
|
189
197
|
return;
|
|
@@ -196,13 +204,21 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, overr
|
|
|
196
204
|
const d = spawnDefaults(kind, taskHoles(task.id));
|
|
197
205
|
spawnForTask(task.id, d.model, d.title ?? '', d.prompt ?? '');
|
|
198
206
|
}
|
|
199
|
-
// T49 — resume di una conversazione in una nuova tab. Unico punto
|
|
200
|
-
// il `⏎` della lista sessioni e quello
|
|
201
|
-
//
|
|
207
|
+
// T49 — resume di una conversazione in una nuova tab. Unico punto dello spawn,
|
|
208
|
+
// con due chiamanti: `openSession` (il `⏎` della lista sessioni e quello
|
|
209
|
+
// sulla riga-sessione della ricerca) quando la conversazione NON è viva, e il
|
|
210
|
+
// restore di T181 (`restoreSessions`).
|
|
211
|
+
// T184 — riprende SEMPRE, e non chiede se la conversazione è viva: quella
|
|
212
|
+
// domanda è del chiamante, e i due chiamanti la pongono in modo diverso. Il
|
|
213
|
+
// `⏎` la pone a ogni pressione (`openSession`); il restore l'ha già posta
|
|
214
|
+
// per l'insieme intero, che `restoreSet` svuota appena una conversazione è
|
|
215
|
+
// viva. Il ramo di focus resta fuori da qui anche per il contratto di
|
|
216
|
+
// ritorno: il restore scandisce la serie sull'uscita del `deck-run` che
|
|
217
|
+
// questa funzione restituisce, e un focus non ha nessun figlio da aspettare.
|
|
202
218
|
// T148 — il modello lo decide il CHIAMANTE, non questa funzione: la lista
|
|
203
219
|
// principale passa il selettore della preview (`model.resumeModel`), la
|
|
204
|
-
// ricerca
|
|
205
|
-
// scelta (D1 preflight). I
|
|
220
|
+
// ricerca e il restore il modello ereditato dalla sessione senza superficie
|
|
221
|
+
// di scelta (D1 preflight). I chiamanti restano la stessa azione perché è
|
|
206
222
|
// lo spawn a essere identico, non la sorgente del valore.
|
|
207
223
|
function resumeSession(sessionId, modelKind) {
|
|
208
224
|
const bound = model.bindings.get(sessionId) ?? null;
|
|
@@ -211,14 +227,39 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, overr
|
|
|
211
227
|
noteSpawn(spawned);
|
|
212
228
|
return spawned;
|
|
213
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* T184 — `⏎` su una conversazione, dalla lista sessioni e dalla riga-sessione
|
|
232
|
+
* della ricerca: la stessa azione dai due posti, come chiede T49.
|
|
233
|
+
*
|
|
234
|
+
* Viva → compass porta davanti la sua tab, e nessun `deck-run` parte: un
|
|
235
|
+
* secondo resume aprirebbe un secondo processo `claude` sullo stesso
|
|
236
|
+
* transcript. Non viva → `resumeSession`, come prima di T184. La scelta e il
|
|
237
|
+
* canale vivono in `focus-session.ts`; qui si cablano coi valori del deck.
|
|
238
|
+
*
|
|
239
|
+
* Il nome della nota viene dal registro (`LiveSession.name`, il titolo della
|
|
240
|
+
* tab); senza nome, il prefisso del `sessionId` come nelle altre note.
|
|
241
|
+
*/
|
|
242
|
+
function openSession(sessionId, modelKind) {
|
|
243
|
+
if (refuse('spawn', '⏎', setNote, 'resume'))
|
|
244
|
+
return;
|
|
245
|
+
enterSession(sessionId, {
|
|
246
|
+
isLive: (id) => model.live.has(id),
|
|
247
|
+
nameOf: (id) => model.live.get(id)?.name || id.slice(0, 8),
|
|
248
|
+
focus: focusSession,
|
|
249
|
+
resume: (id) => resumeSession(id, modelKind),
|
|
250
|
+
note: setNote,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
214
253
|
/**
|
|
215
254
|
* T181 — `^T` e il bottone in testata: riapre le conversazioni dell'ultimo
|
|
216
255
|
* insieme vivo, una tab per conversazione, ognuna col proprio binding di
|
|
217
256
|
* task, la propria nota e il modello con cui girava.
|
|
218
257
|
*
|
|
219
|
-
* Passa da `resumeSession
|
|
220
|
-
*
|
|
221
|
-
*
|
|
258
|
+
* Passa da `resumeSession` diretto, non da `openSession` come il `⏎` (T184):
|
|
259
|
+
* l'insieme è vuoto appena una conversazione è viva, quindi qui non c'è
|
|
260
|
+
* nessuna viva da portare davanti. Come la ricerca non ha una superficie di
|
|
261
|
+
* scelta del modello: lo eredita dalla conversazione
|
|
262
|
+
* (`modelAlias(session.model) ?? MODEL_DEFAULT`, T148/D1).
|
|
222
263
|
*
|
|
223
264
|
* IN SERIE, e non tutte insieme (P7 preflight): la tab successiva parte
|
|
224
265
|
* quando il `deck-run` della precedente è uscito, così le tab si aprono
|
|
@@ -234,6 +275,8 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, overr
|
|
|
234
275
|
* chiudendo le tab.
|
|
235
276
|
*/
|
|
236
277
|
function restoreSessions() {
|
|
278
|
+
if (refuse('spawn', '^T', setNote, 'restore delle conversazioni'))
|
|
279
|
+
return;
|
|
237
280
|
const list = model.restorable;
|
|
238
281
|
if (list.length === 0) {
|
|
239
282
|
setNote(model.restoring ? '^T → restore già in corso' : '^T → nessuna conversazione da riaprire');
|
|
@@ -336,6 +379,8 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, overr
|
|
|
336
379
|
// ha per oggetto una conversazione, e senza focus lì non ce n'è una
|
|
337
380
|
// selezionata su cui agire.
|
|
338
381
|
function forkSession() {
|
|
382
|
+
if (refuse('spawn', 'f', setNote, 'fork'))
|
|
383
|
+
return;
|
|
339
384
|
if (model.focus !== 'sessions') {
|
|
340
385
|
setNote('f → fork: seleziona una sessione (→ per il pane)');
|
|
341
386
|
return;
|
|
@@ -431,6 +476,12 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, overr
|
|
|
431
476
|
setNote('CANC → eliminare: seleziona una conversazione (→ per il pane)');
|
|
432
477
|
return null;
|
|
433
478
|
}
|
|
479
|
+
// T175 — il rifiuto della VIVA qui sotto legge `model.live`, e dove la
|
|
480
|
+
// liveness manca la mappa è sempre vuota: il rifiuto non scatterebbe mai, e
|
|
481
|
+
// `CANC` toglierebbe il transcript a una conversazione aperta. Spento finché
|
|
482
|
+
// la liveness non c'è, non degradato a «nessuna è viva».
|
|
483
|
+
if (refuse('liveSessions', 'CANC', setNote, 'eliminazione delle conversazioni'))
|
|
484
|
+
return null;
|
|
434
485
|
const sid = model.selSessionId;
|
|
435
486
|
if (!sid) {
|
|
436
487
|
setNote('CANC → nessuna conversazione da eliminare');
|
|
@@ -456,6 +507,10 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, overr
|
|
|
456
507
|
* appena eliminata apposta.
|
|
457
508
|
*/
|
|
458
509
|
function dropSession(draft) {
|
|
510
|
+
// La stessa guardia della bozza: chi arrivasse qui senza passare da lei non
|
|
511
|
+
// deve trovare un `rmSync` sguarnito.
|
|
512
|
+
if (refuse('liveSessions', 'CANC', setNote, 'eliminazione delle conversazioni'))
|
|
513
|
+
return;
|
|
459
514
|
const short = draft.sessionId.slice(0, 8);
|
|
460
515
|
const landing = neighborId(model.sessionRows, draft.sessionId);
|
|
461
516
|
try {
|
|
@@ -474,6 +529,8 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, overr
|
|
|
474
529
|
}
|
|
475
530
|
/** `t` — terminale a project root, con un titolo che il matcher di compass riconosce. */
|
|
476
531
|
function openTerminal() {
|
|
532
|
+
if (refuse('spawn', 't', setNote, 'terminale'))
|
|
533
|
+
return;
|
|
477
534
|
const title = model.identity ? `🖥️ ${model.identity.name} [term]` : null;
|
|
478
535
|
const child = spawnTerminal(cwd, title);
|
|
479
536
|
child.on('error', () => setNote('⚠ t → ptyxis non lanciabile'));
|
|
@@ -500,6 +557,8 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, overr
|
|
|
500
557
|
* due superfici mostrano nasce da un valore solo.
|
|
501
558
|
*/
|
|
502
559
|
function drainInbox(file, prompt) {
|
|
560
|
+
if (refuse('spawn', '⏎', setNote))
|
|
561
|
+
return;
|
|
503
562
|
const sid = randomUUID();
|
|
504
563
|
const d = spawnDefaults('drain', { file: inboxWords(file) });
|
|
505
564
|
const title = d.title ?? '';
|
|
@@ -576,6 +635,8 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, overr
|
|
|
576
635
|
function rebalanceDoc(row, prompt) {
|
|
577
636
|
if (!prompt)
|
|
578
637
|
return;
|
|
638
|
+
if (refuse('spawn', '⏎', setNote))
|
|
639
|
+
return;
|
|
579
640
|
// Il bersaglio inbox prende la terna della riga `drain`, non quella di
|
|
580
641
|
// `rebalance`: il prompt da solo non basterebbe — modello e titolo della tab
|
|
581
642
|
// verrebbero comunque dalla riga sbagliata, e la conversazione si
|
|
@@ -610,6 +671,8 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, overr
|
|
|
610
671
|
* non progettare niente.
|
|
611
672
|
*/
|
|
612
673
|
function unwrapPath(path) {
|
|
674
|
+
if (refuse('spawn', '⏎', setNote))
|
|
675
|
+
return;
|
|
613
676
|
const sid = randomUUID();
|
|
614
677
|
const d = spawnDefaults('unwrap', { path: wrapWords(path) });
|
|
615
678
|
const title = d.title ?? '';
|
|
@@ -623,6 +686,8 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, overr
|
|
|
623
686
|
* modello è quello del selettore proprio della nuda (`model.bareModel`),
|
|
624
687
|
* passato SEMPRE nell'argv anche sul default. */
|
|
625
688
|
function openClaude() {
|
|
689
|
+
if (refuse('spawn', 'c', setNote, 'sessione claude'))
|
|
690
|
+
return;
|
|
626
691
|
const spawned = spawnClaudeEmpty(cwd, model.bareModel);
|
|
627
692
|
spawned.child.on('error', () => setNote(`⚠ c → spawn claude fallito (${DECK_RUN})`));
|
|
628
693
|
noteSpawn(spawned);
|
|
@@ -640,6 +705,8 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, overr
|
|
|
640
705
|
}
|
|
641
706
|
/** `1`-`9` — le voci launch del progetto, per indice base-1. */
|
|
642
707
|
function runLaunchAt(input) {
|
|
708
|
+
if (refuse('launch', input, setNote))
|
|
709
|
+
return;
|
|
643
710
|
const entry = model.launch[Number(input) - 1];
|
|
644
711
|
if (!entry) {
|
|
645
712
|
setNote(`${input} → nessuna voce launch (${model.launch.length} configurate)`);
|
|
@@ -663,6 +730,7 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, overr
|
|
|
663
730
|
spawnForTask,
|
|
664
731
|
spawnTaskSession,
|
|
665
732
|
resumeSession,
|
|
733
|
+
openSession,
|
|
666
734
|
restoreSessions,
|
|
667
735
|
cycleResumeModel,
|
|
668
736
|
cycleBareModel,
|
package/dist/archivable.js
CHANGED
|
@@ -102,7 +102,7 @@ async function resolveDoneDate(id, content, taskFile, projectRoot) {
|
|
|
102
102
|
* un throw: un contatore informativo non può rompere il deck. */
|
|
103
103
|
async function gitOut(args, cwd) {
|
|
104
104
|
try {
|
|
105
|
-
const { stdout } = await execFileAsync('git', args, { cwd });
|
|
105
|
+
const { stdout } = await execFileAsync('git', args, { cwd, windowsHide: true });
|
|
106
106
|
return stdout.trim() || null;
|
|
107
107
|
}
|
|
108
108
|
catch {
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// T175 — COME il deck lancia `claude`, senza shell, su ogni installazione.
|
|
2
|
+
//
|
|
3
|
+
// Su Linux `claude` è un eseguibile nel `PATH` e basta il nome. Su Windows le
|
|
4
|
+
// installazioni sono due e si comportano in modo opposto davanti a `spawn`:
|
|
5
|
+
//
|
|
6
|
+
// - l'installer nativo dà `claude.exe`, e `spawn('claude')` lo trova — libuv
|
|
7
|
+
// prova da sé `.com` ed `.exe`;
|
|
8
|
+
// - l'installazione npm dà uno shim `claude.cmd`, e `spawn` non lo trova
|
|
9
|
+
// (`ENOENT` col nome nudo) né lo lancia (`EINVAL` col nome intero: Node ≥
|
|
10
|
+
// 18.20.2 rifiuta un `.cmd` senza shell).
|
|
11
|
+
//
|
|
12
|
+
// La shell non è un rimedio: con `shell: true` il prompt passerebbe da cmd.exe,
|
|
13
|
+
// che interpreta `& | % ^`, e un prompt è testo arbitrario. Lo shim invece è un
|
|
14
|
+
// file generato da cmd-shim con una forma fissa, e dice in chiaro cosa lancia:
|
|
15
|
+
// il deck lo legge e lancia il bersaglio da sé (P5 preflight).
|
|
16
|
+
//
|
|
17
|
+
// Modulo PURO nel cuore (`resolveClaude` riceve il filesystem come funzioni),
|
|
18
|
+
// con un involucro che legge quello vero.
|
|
19
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
20
|
+
import { win32 } from 'node:path';
|
|
21
|
+
/**
|
|
22
|
+
* Il bersaglio di uno shim di cmd-shim, o `null` se il testo non ne ha uno che
|
|
23
|
+
* il deck sappia lanciare.
|
|
24
|
+
*
|
|
25
|
+
* Il bersaglio è l'ultimo `"%dp0%\<path>"` seguito da `%*` (le due forme dello
|
|
26
|
+
* shim: `%dp0%` dalla v4 di cmd-shim, `%~dp0` prima). Un `.js` parte col node
|
|
27
|
+
* del deck — è il node che lo shim userebbe in assenza di un `node.exe`
|
|
28
|
+
* accanto, cioè quello del `PATH` —, un `.exe` parte da sé. Ogni altro bersaglio
|
|
29
|
+
* (uno script sh, per esempio) vorrebbe il suo interprete, e il deck non lo
|
|
30
|
+
* indovina.
|
|
31
|
+
*/
|
|
32
|
+
export function parseCmdShim(text, shimPath, nodePath) {
|
|
33
|
+
const re = /"%~?dp0%?\\?([^"]+)"\s+%\*/g;
|
|
34
|
+
let rel = null;
|
|
35
|
+
for (let m = re.exec(text); m; m = re.exec(text))
|
|
36
|
+
rel = m[1];
|
|
37
|
+
if (!rel)
|
|
38
|
+
return null;
|
|
39
|
+
const target = win32.join(win32.dirname(shimPath), rel);
|
|
40
|
+
if (/\.(c|m)?js$/i.test(target))
|
|
41
|
+
return { cmd: nodePath, prefix: [target] };
|
|
42
|
+
if (/\.(exe|com)$/i.test(target))
|
|
43
|
+
return { cmd: target, prefix: [] };
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* La cascata (P5 preflight): `LOOM_DECK_CLAUDE_CMD` → `claude` così com'è →
|
|
48
|
+
* shim `claude.cmd` nel `PATH`.
|
|
49
|
+
*
|
|
50
|
+
* Su win32 «così com'è» vuol dire che un `claude.exe` o `claude.com` sta nel
|
|
51
|
+
* `PATH`: è lo stesso giro che farebbe libuv, fatto prima per sapere se serve
|
|
52
|
+
* il gradino dopo. Un override che nomina un `.cmd` passa dallo stesso parser,
|
|
53
|
+
* o cadrebbe nell'`EINVAL`. Niente trovato → `claude` nudo: lo spawn fallirà,
|
|
54
|
+
* e l'errore dirà «`claude` non lanciabile», che è il fatto.
|
|
55
|
+
*/
|
|
56
|
+
export function resolveClaude(env, platform, nodePath, fs) {
|
|
57
|
+
const fromShim = (shim) => {
|
|
58
|
+
try {
|
|
59
|
+
return parseCmdShim(fs.read(shim), shim, nodePath);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const override = env.LOOM_DECK_CLAUDE_CMD;
|
|
66
|
+
if (override) {
|
|
67
|
+
if (platform === 'win32' && /\.(cmd|bat)$/i.test(override)) {
|
|
68
|
+
return fromShim(override) ?? { cmd: override, prefix: [] };
|
|
69
|
+
}
|
|
70
|
+
return { cmd: override, prefix: [] };
|
|
71
|
+
}
|
|
72
|
+
const bare = { cmd: 'claude', prefix: [] };
|
|
73
|
+
if (platform !== 'win32')
|
|
74
|
+
return bare;
|
|
75
|
+
const dirs = (env.PATH ?? env.Path ?? '').split(win32.delimiter).filter(Boolean);
|
|
76
|
+
for (const dir of dirs) {
|
|
77
|
+
if (fs.exists(win32.join(dir, 'claude.com')) || fs.exists(win32.join(dir, 'claude.exe')))
|
|
78
|
+
return bare;
|
|
79
|
+
}
|
|
80
|
+
for (const dir of dirs) {
|
|
81
|
+
const shim = win32.join(dir, 'claude.cmd');
|
|
82
|
+
if (!fs.exists(shim))
|
|
83
|
+
continue;
|
|
84
|
+
const launcher = fromShim(shim);
|
|
85
|
+
if (launcher)
|
|
86
|
+
return launcher;
|
|
87
|
+
}
|
|
88
|
+
return bare;
|
|
89
|
+
}
|
|
90
|
+
let cached;
|
|
91
|
+
/** Il `claude` di questa macchina, risolto alla prima richiesta e tenuto per la
|
|
92
|
+
* vita del deck. */
|
|
93
|
+
export function claudeLauncher() {
|
|
94
|
+
if (!cached) {
|
|
95
|
+
cached = resolveClaude(process.env, process.platform, process.execPath, {
|
|
96
|
+
exists: existsSync,
|
|
97
|
+
read: (p) => readFileSync(p, 'utf8'),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return cached;
|
|
101
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -16,7 +16,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
16
16
|
import { render, Box, Text } from 'ink';
|
|
17
17
|
import { useMemo, useState } from 'react';
|
|
18
18
|
import { resolveTasksPath, resolveTasksDir } from './tasks.js';
|
|
19
|
-
import { LAUNCH_SEP } from './config.js';
|
|
19
|
+
import { LAUNCH_SEP, docsRootName } from './config.js';
|
|
20
20
|
import { anchorFrame, enableMouse } from './mouse.js';
|
|
21
21
|
import { sanitize } from './width.js';
|
|
22
22
|
import { MODEL_SHORT_LIST, modelAlias } from './glyphs.js';
|
|
@@ -48,7 +48,7 @@ import { DocPane } from './ui/doc-pane.js';
|
|
|
48
48
|
import { PreviewPane, detailMetaOf } from './ui/preview.js';
|
|
49
49
|
import { CompactNotice, HintBar, TextBox, screenFor } from './ui/screens.js';
|
|
50
50
|
import { VERSION } from './version.js';
|
|
51
|
-
function Deck({ cwd, tasksPath, tasksDir }) {
|
|
51
|
+
function Deck({ cwd, docsRoot, tasksPath, tasksDir, }) {
|
|
52
52
|
// La riga di STATO in fondo al frame: il feedback dell'ultima azione. Non è
|
|
53
53
|
// la nota di una conversazione, che nel modello si chiama `sessionNotes`.
|
|
54
54
|
const [note, setNote] = useState('');
|
|
@@ -68,6 +68,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
68
68
|
const bareDefault = resolveSpawnId('bare', spawnConfig.overrides, catalog).model;
|
|
69
69
|
const model = useDeckModel({
|
|
70
70
|
cwd,
|
|
71
|
+
docsRoot,
|
|
71
72
|
tasksPath,
|
|
72
73
|
tasksDir,
|
|
73
74
|
bareModel: bareDefault,
|
|
@@ -111,7 +112,9 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
111
112
|
setNote,
|
|
112
113
|
// T148/D1 — nessun selettore nella ricerca: il modello è quello ereditato
|
|
113
114
|
// dalla conversazione, senza superficie di scelta.
|
|
114
|
-
|
|
115
|
+
// T184 — `openSession` come il `⏎` della lista: una viva va davanti, solo
|
|
116
|
+
// una non viva si riprende.
|
|
117
|
+
onResume: (row) => actions.openSession(row.session.sessionId, modelAlias(row.session.model) ?? MODEL_DEFAULT),
|
|
115
118
|
});
|
|
116
119
|
// T121 — il project status. Non è solo un overlay come i tre sopra: il suo
|
|
117
120
|
// stato si legge in TESTATA a schermata chiusa, e il viewer è una delle sue
|
|
@@ -265,6 +268,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
265
268
|
scanned: gitlink.scanned,
|
|
266
269
|
ok: gitlink.ok,
|
|
267
270
|
failed: gitlink.failed,
|
|
271
|
+
available: gitlink.available,
|
|
268
272
|
}
|
|
269
273
|
: null,
|
|
270
274
|
inbox: {
|
|
@@ -272,8 +276,15 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
272
276
|
stale: model.inboxStale,
|
|
273
277
|
scanned: model.inboxScanned,
|
|
274
278
|
ok: model.inboxOk,
|
|
279
|
+
available: model.pluginScripts,
|
|
280
|
+
},
|
|
281
|
+
wrap: {
|
|
282
|
+
count: wrap.count,
|
|
283
|
+
mtime: wrap.mtime,
|
|
284
|
+
ok: wrap.ok,
|
|
285
|
+
scanning: wrap.scanning,
|
|
286
|
+
available: wrap.available,
|
|
275
287
|
},
|
|
276
|
-
wrap: { count: wrap.count, mtime: wrap.mtime, ok: wrap.ok, scanning: wrap.scanning },
|
|
277
288
|
}, columns);
|
|
278
289
|
const frame = frameGeometry({
|
|
279
290
|
rows,
|
|
@@ -355,9 +366,13 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
355
366
|
hasDoc: model.selDoc !== null,
|
|
356
367
|
gitlink: gitlink.enabled,
|
|
357
368
|
restore: frame.headerButton !== null,
|
|
358
|
-
}), indicators: indicators, columns: columns }), frame.launchLine ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [launch.segments.map((s) => s.text).join(LAUNCH_SEP), launch.overflow > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 +", launch.overflow, " fuori riga"] })) : null, launch.unreachable > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 ", launch.unreachable, " oltre la 9\u00AA (non raggiungibili)"] })) : null, launch.filler, BARE_LABEL.text, ' ', bareButtons.nodes] })) : null, mode === 'create' ? _jsx(TextBox, { glyph: "C", value: textModals.draft }) : null, mode === 'note' ? _jsx(TextBox, { glyph: "\u270E", value: textModals.noteDraft }) : null, mode === 'sort' ? _jsx(SortModal, { sort: model.view.sort }) : null, mode === 'filter' ? (_jsx(FilterModal, { view: model.view, cursor: viewModals.filterCursor })) : null, mode === 'edit' && textModals.edit && textModals.editTask ? (_jsx(EditModal, { id: textModals.editTask.id, draft: textModals.edit, row: textModals.editCursor.row, caret: textModals.editCursor.caret, columns: columns })) : null, mode === 'purge' && purge.draft ? (_jsx(PurgeModal, { draft: purge.draft, columns: columns })) : null, mode === 'drop' && drop.draft ? _jsx(DropModal, { draft: drop.draft, columns: columns }) : null, _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [model.deckMode === 'doc' ? (_jsx(DocPane, { rows: frame.windowDoc, counts: model.docCounts, activeView: model.docViewId, paneCount: model.docRows.length, selectedPath: model.selDocPath, focused: model.focus === 'doctree', above: frame.docWin.start, below: model.docRows.length - frame.docWin.end, columns: columns, ok: model.docOk, scanned: model.docScanned, scanning: model.docScanning, hasDirs: model.docHasDirs })) : (_jsx(TasksPane, { tasks: frame.windowTasks, counts: model.taskCounts, activeView: model.taskViewId, paneCount: model.paneTasks.length, view: model.view, selected: model.selIndex, spotCount: model.spotCount, allCount: model.sessions.length, idW: model.taskCols.id, tailW: model.taskCols.tail, focused: model.focus === 'tasks', loadError: model.loadError, windowStart: frame.taskWin.start, above: frame.taskWin.start, below: model.paneTasks.length - frame.taskWin.end, columns: columns, data: model.taskRowData, blockMark: model.blockMark })), model.deckMode === 'doc' ? (_jsx(InboxPane, { files: frame.windowInbox, counts: model.inboxCounts, activeView: model.inboxViewId, paneCount: model.inboxFiles.length, selectedPath: model.selInboxPath, focused: model.focus === 'inbox', above: frame.inboxWin.start, below: model.inboxFiles.length - frame.inboxWin.end, columns: columns, ok: model.inboxOk, scanned: model.inboxScanned })) : (_jsx(SessionsPane, { parentLabel: model.parentLabel, isSpot: model.isSpot, isAll: model.isAll, bindings: model.bindings, taskW: model.sessionCols.task, ageW: model.sessionCols.age, rows: frame.windowRows, counts: model.sessionCounts, activeView: model.sessionViewId, paneCount: model.sessionRows.length, selectedId: model.selSessionId ?? undefined, focused: model.focus === 'sessions', above: frame.sessionWin.start, below: model.sessionRows.length - frame.sessionWin.end, columns: columns, forkOf: model.forkOf, sessionNotes: model.sessionNotes, sessionPriority: model.sessionPriority, projectCore: model.projectCore, live: model.live }))] }), frame.budget.preview && previewKind === 'task' && model.detail ? (_jsx(PreviewPane, { kind: "task", detail: model.detail, maxLines: frame.budget.detailLines, columns: columns })) : frame.budget.preview && previewKind === 'inbox' && model.selInbox ? (_jsx(PreviewPane, { kind: "inbox", file: model.selInbox, columns: columns })) : frame.budget.preview && previewKind === 'doc' && model.selDoc ? (_jsx(PreviewPane, { kind: "doc", row: model.selDoc, columns: columns })) : frame.budget.preview && previewKind === 'session' && model.selSessionObj ? (_jsx(PreviewPane, { kind: "session", s: model.selSessionObj, firstLines: frame.budget.sessionFirstLines, lastLines: frame.budget.sessionLastLines, columns: columns, origin: model.forkOf.get(model.selSessionObj.sessionId) ?? null, note: model.sessionNotes.get(model.selSessionObj.sessionId) ?? '', live: model.live.get(model.selSessionObj.sessionId) ?? null, resumeModel: model.resumeModel })) : null, note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: sanitize(note) }) : null] }));
|
|
369
|
+
}), indicators: indicators, columns: columns }), frame.launchLine ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [launch.segments.map((s) => s.text).join(LAUNCH_SEP), launch.overflow > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 +", launch.overflow, " fuori riga"] })) : null, launch.unreachable > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 ", launch.unreachable, " oltre la 9\u00AA (non raggiungibili)"] })) : null, launch.filler, BARE_LABEL.text, ' ', bareButtons.nodes] })) : null, mode === 'create' ? _jsx(TextBox, { glyph: "C", value: textModals.draft }) : null, mode === 'note' ? _jsx(TextBox, { glyph: "\u270E", value: textModals.noteDraft }) : null, mode === 'sort' ? _jsx(SortModal, { sort: model.view.sort }) : null, mode === 'filter' ? (_jsx(FilterModal, { view: model.view, cursor: viewModals.filterCursor })) : null, mode === 'edit' && textModals.edit && textModals.editTask ? (_jsx(EditModal, { id: textModals.editTask.id, draft: textModals.edit, row: textModals.editCursor.row, caret: textModals.editCursor.caret, columns: columns })) : null, mode === 'purge' && purge.draft ? (_jsx(PurgeModal, { draft: purge.draft, columns: columns })) : null, mode === 'drop' && drop.draft ? _jsx(DropModal, { draft: drop.draft, columns: columns }) : null, _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [model.deckMode === 'doc' ? (_jsx(DocPane, { rows: frame.windowDoc, counts: model.docCounts, activeView: model.docViewId, paneCount: model.docRows.length, selectedPath: model.selDocPath, focused: model.focus === 'doctree', above: frame.docWin.start, below: model.docRows.length - frame.docWin.end, columns: columns, ok: model.docOk, scanned: model.docScanned, scanning: model.docScanning, hasDirs: model.docHasDirs, available: model.pluginScripts })) : (_jsx(TasksPane, { tasks: frame.windowTasks, counts: model.taskCounts, activeView: model.taskViewId, paneCount: model.paneTasks.length, view: model.view, selected: model.selIndex, spotCount: model.spotCount, allCount: model.sessions.length, idW: model.taskCols.id, tailW: model.taskCols.tail, focused: model.focus === 'tasks', loadError: model.loadError, windowStart: frame.taskWin.start, above: frame.taskWin.start, below: model.paneTasks.length - frame.taskWin.end, columns: columns, data: model.taskRowData, blockMark: model.blockMark })), model.deckMode === 'doc' ? (_jsx(InboxPane, { files: frame.windowInbox, counts: model.inboxCounts, activeView: model.inboxViewId, paneCount: model.inboxFiles.length, selectedPath: model.selInboxPath, focused: model.focus === 'inbox', above: frame.inboxWin.start, below: model.inboxFiles.length - frame.inboxWin.end, columns: columns, ok: model.inboxOk, scanned: model.inboxScanned, available: model.pluginScripts })) : (_jsx(SessionsPane, { parentLabel: model.parentLabel, isSpot: model.isSpot, isAll: model.isAll, bindings: model.bindings, taskW: model.sessionCols.task, ageW: model.sessionCols.age, rows: frame.windowRows, counts: model.sessionCounts, activeView: model.sessionViewId, paneCount: model.sessionRows.length, selectedId: model.selSessionId ?? undefined, focused: model.focus === 'sessions', above: frame.sessionWin.start, below: model.sessionRows.length - frame.sessionWin.end, columns: columns, forkOf: model.forkOf, sessionNotes: model.sessionNotes, sessionPriority: model.sessionPriority, projectCore: model.projectCore, live: model.live }))] }), frame.budget.preview && previewKind === 'task' && model.detail ? (_jsx(PreviewPane, { kind: "task", detail: model.detail, maxLines: frame.budget.detailLines, columns: columns })) : frame.budget.preview && previewKind === 'inbox' && model.selInbox ? (_jsx(PreviewPane, { kind: "inbox", file: model.selInbox, columns: columns })) : frame.budget.preview && previewKind === 'doc' && model.selDoc ? (_jsx(PreviewPane, { kind: "doc", row: model.selDoc, columns: columns })) : frame.budget.preview && previewKind === 'session' && model.selSessionObj ? (_jsx(PreviewPane, { kind: "session", s: model.selSessionObj, firstLines: frame.budget.sessionFirstLines, lastLines: frame.budget.sessionLastLines, columns: columns, origin: model.forkOf.get(model.selSessionObj.sessionId) ?? null, note: model.sessionNotes.get(model.selSessionObj.sessionId) ?? '', live: model.live.get(model.selSessionObj.sessionId) ?? null, resumeModel: model.resumeModel })) : null, note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: sanitize(note) }) : null] }));
|
|
359
370
|
}
|
|
360
371
|
const cwd = process.cwd();
|
|
372
|
+
// T175 — la docs-root si risolve QUI, una volta, e scende a tutti: i path di
|
|
373
|
+
// `tasks.md` e dei task file e i due scan del modo doc devono vedere la stessa
|
|
374
|
+
// cartella, e due risoluzioni potrebbero non vederla.
|
|
375
|
+
const docsRoot = docsRootName(cwd);
|
|
361
376
|
// T116 — `exitOnCtrlC: false` toglie a Ink l'uscita immediata su `^C`: senza,
|
|
362
377
|
// il tasto non arriverebbe mai a `useInput` e il doppio colpo non sarebbe
|
|
363
378
|
// osservabile. In raw mode `^C` non è un SIGINT — è il byte `\x03` nello
|
|
@@ -370,6 +385,4 @@ const cwd = process.cwd();
|
|
|
370
385
|
// esista qualcosa da cui uscire.
|
|
371
386
|
anchorFrame();
|
|
372
387
|
enableMouse();
|
|
373
|
-
render(_jsx(Deck, { cwd: cwd, tasksPath: resolveTasksPath(cwd), tasksDir: resolveTasksDir(cwd) }), {
|
|
374
|
-
exitOnCtrlC: false,
|
|
375
|
-
});
|
|
388
|
+
render(_jsx(Deck, { cwd: cwd, docsRoot: docsRoot, tasksPath: resolveTasksPath(cwd, docsRoot), tasksDir: resolveTasksDir(cwd, docsRoot) }), { exitOnCtrlC: false });
|
package/dist/commit-times.js
CHANGED
|
@@ -37,7 +37,10 @@ export function commitLogSpawnCount() {
|
|
|
37
37
|
}
|
|
38
38
|
async function headSha(projectRoot) {
|
|
39
39
|
try {
|
|
40
|
-
const { stdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], {
|
|
40
|
+
const { stdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], {
|
|
41
|
+
cwd: projectRoot,
|
|
42
|
+
windowsHide: true,
|
|
43
|
+
});
|
|
41
44
|
return stdout.trim();
|
|
42
45
|
}
|
|
43
46
|
catch {
|
|
@@ -98,7 +101,7 @@ export async function commitTimes(tasksDir, projectRoot) {
|
|
|
98
101
|
// Il contatore sale PRIMA dell'await: conta gli spawn partiti, e uno
|
|
99
102
|
// spawn fallito costa comunque la fork del processo.
|
|
100
103
|
logSpawns++;
|
|
101
|
-
const { stdout } = await execFileAsync('git', ['log', '--format=#%ct', '--name-only', '--', tasksDir], { cwd: projectRoot });
|
|
104
|
+
const { stdout } = await execFileAsync('git', ['log', '--format=#%ct', '--name-only', '--', tasksDir], { cwd: projectRoot, windowsHide: true });
|
|
102
105
|
map = parseCommitLog(stdout);
|
|
103
106
|
}
|
|
104
107
|
catch {
|
package/dist/config.js
CHANGED
|
@@ -112,12 +112,10 @@ export function launchLegend(entries, columns, reserved = 0) {
|
|
|
112
112
|
// T61 — soglia d'età del contatore archiviabili, campo `archivableDays`.
|
|
113
113
|
//
|
|
114
114
|
// È il PRIMO scalare che il lato TypeScript legge dal file config: `launch` e
|
|
115
|
-
// `identity` sopra sono strutture
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
// `permissionMode`, che vive nel file, lo legge `deck-run` via jq e non è
|
|
120
|
-
// propagato al registry.
|
|
115
|
+
// `identity` sopra sono strutture. La catena da percorrere si ferma qui: nessun
|
|
116
|
+
// passaggio da `lib-config.sh`/`reg_pull`/dconf, perché il solo consumer è il
|
|
117
|
+
// deck e il file ce l'ha sotto mano. Il precedente esatto è `permissionMode`,
|
|
118
|
+
// che vive nel file, lo legge `deck-run` via jq e non è propagato al registry.
|
|
121
119
|
/** Interi positivi soltanto: uno 0 spegnerebbe la soglia (tutte le Done
|
|
122
120
|
* archiviabili), un negativo o un decimale sono un typo. Valore fuori dominio
|
|
123
121
|
* → default, mai passaggio cieco di un numero senza senso al conteggio. */
|
|
@@ -149,6 +147,53 @@ export function loadArchivableDays(projectRoot) {
|
|
|
149
147
|
}
|
|
150
148
|
return DEFAULT_ARCHIVABLE_DAYS;
|
|
151
149
|
}
|
|
150
|
+
/** La docs-root di un progetto che non la dichiara: la stessa di `lw_docs_root`
|
|
151
|
+
* nel plugin. */
|
|
152
|
+
export const DEFAULT_DOCS_ROOT = 'docs';
|
|
153
|
+
/** Stringa non vuota, presa così com'è: è il valore che il plugin legge con
|
|
154
|
+
* `jq -r '.docsRoot'`, e due letture che normalizzassero in modo diverso
|
|
155
|
+
* manderebbero deck e skill in due cartelle. */
|
|
156
|
+
export function parseDocsRoot(raw) {
|
|
157
|
+
if (!raw || typeof raw !== 'object')
|
|
158
|
+
return null;
|
|
159
|
+
const v = raw.docsRoot;
|
|
160
|
+
return typeof v === 'string' && v.trim() ? v : null;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Nome della cartella doc del progetto (`docs`, `runtime`, …): `tasks.md`,
|
|
164
|
+
* `tasks/`, `inbox/` stanno lì sotto.
|
|
165
|
+
*
|
|
166
|
+
* T175 — precedenza `env → file → default`. Fino a T175 il deck leggeva la sola
|
|
167
|
+
* env `LOOM_DECK_DOCS_ROOT`, contando su chi lo avvia: compass la antepone al
|
|
168
|
+
* comando, il profilo Ptyxis la porta cablata. Un deck avviato a mano da una
|
|
169
|
+
* shell — su Linux, e su Windows sempre, perché lì nessuno dei due canali
|
|
170
|
+
* esiste — cercava `docs/` e mostrava una lista vuota invece di un errore. Il
|
|
171
|
+
* campo `docsRoot` del file è la fonte unica della famiglia (lo legge anche il
|
|
172
|
+
* plugin): il deck lo legge da sé.
|
|
173
|
+
*
|
|
174
|
+
* L'env resta IN TESTA, e non solo per i test: è il canale con cui compass
|
|
175
|
+
* passa il valore del registry, e una env impostata è una scelta esplicita di
|
|
176
|
+
* chi avvia, che batte il file come `LOOM_DECK_ARCHIVABLE_DAYS` batte
|
|
177
|
+
* `archivableDays`.
|
|
178
|
+
*
|
|
179
|
+
* Si risolve UNA volta per vita del deck, in `cli.tsx`, e il valore scende a
|
|
180
|
+
* chi lo usa: i path di `tasks.md` e dei task file e i due scan del modo doc
|
|
181
|
+
* devono vedere la stessa cartella.
|
|
182
|
+
*/
|
|
183
|
+
export function docsRootName(projectRoot) {
|
|
184
|
+
const env = process.env.LOOM_DECK_DOCS_ROOT;
|
|
185
|
+
if (env)
|
|
186
|
+
return env;
|
|
187
|
+
try {
|
|
188
|
+
const fromFile = parseDocsRoot(JSON.parse(readFileSync(configFilePath(projectRoot), 'utf8')));
|
|
189
|
+
if (fromFile !== null)
|
|
190
|
+
return fromFile;
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
// file assente o malformato → default, come loadLaunch/loadIdentity
|
|
194
|
+
}
|
|
195
|
+
return DEFAULT_DOCS_ROOT;
|
|
196
|
+
}
|
|
152
197
|
export function parseIdentity(raw) {
|
|
153
198
|
if (!raw || typeof raw !== 'object')
|
|
154
199
|
return null;
|
package/dist/deck-model.js
CHANGED
|
@@ -19,7 +19,6 @@ import { DEFAULT_INBOX_STALE_HOURS, staleCount } from './inbox.js';
|
|
|
19
19
|
import { aliveIds, restoreSet } from './live-marks.js';
|
|
20
20
|
import { cycleDocView, docCounts as deriveDocCounts, docView, selectDocRows, } from './doc-views.js';
|
|
21
21
|
import { cycleInboxView, inboxCounts as deriveInboxCounts, inboxView, selectInboxRows, } from './inbox-views.js';
|
|
22
|
-
import { docsRootName } from './tasks.js';
|
|
23
22
|
import { isDone } from './layout.js';
|
|
24
23
|
import { TASK_EMPTY, modelAlias, relTime } from './glyphs.js';
|
|
25
24
|
import { assembleSessionList, firstSelectableId, rowIndexOf, selectedSession, } from './session-list.js';
|
|
@@ -29,6 +28,8 @@ import { applyView, epicRollup, taskColumns } from './view.js';
|
|
|
29
28
|
import { loadView } from './view-store.js';
|
|
30
29
|
import { termWidth } from './width.js';
|
|
31
30
|
import { MODELS, MODEL_DEFAULT } from './spawn-catalog.js';
|
|
31
|
+
import { isAvailable } from './platform.js';
|
|
32
|
+
import { projectBaseName } from './project-path.js';
|
|
32
33
|
/**
|
|
33
34
|
* Conteggio dei figli per task, più il rollup delle VIVE e le conversazioni
|
|
34
35
|
* senza binding.
|
|
@@ -107,7 +108,7 @@ export function childSessionsOf(sessions, bindings, selectedTaskId, isAll) {
|
|
|
107
108
|
return selectedTaskId ? bound === selectedTaskId : !bound;
|
|
108
109
|
});
|
|
109
110
|
}
|
|
110
|
-
export function useDeckModel({ cwd, tasksPath, tasksDir, bareModel: bareModelDefault, setNote, }) {
|
|
111
|
+
export function useDeckModel({ cwd, docsRoot, tasksPath, tasksDir, bareModel: bareModelDefault, setNote, }) {
|
|
111
112
|
const { tasks, loadError } = useTasks(tasksPath);
|
|
112
113
|
// Voci launch del progetto (T32): lette una volta, raggiunte per indice 1..9.
|
|
113
114
|
const launch = useMemo(() => loadLaunch(cwd), [cwd]);
|
|
@@ -125,7 +126,9 @@ export function useDeckModel({ cwd, tasksPath, tasksDir, bareModel: bareModelDef
|
|
|
125
126
|
// scrive nel sidecar il residuo del titolo, e quel residuo si ottiene
|
|
126
127
|
// togliendo questo stesso core (§Riempimento dei buchi in `hooks.ts`).
|
|
127
128
|
const projectCore = identity ? identity.name : null;
|
|
128
|
-
|
|
129
|
+
// T175 — `projectBaseName` e non uno split su `/`: su Windows restituiva il
|
|
130
|
+
// path intero.
|
|
131
|
+
const projectName = projectBaseName(cwd);
|
|
129
132
|
// `notes` esce dall'indice come `sessionNotes`: nel deck `note` è già la riga
|
|
130
133
|
// di STATO in fondo al frame (il feedback di un'azione). Due concetti diversi
|
|
131
134
|
// a una lettera di distanza sarebbero una trappola di lettura — e di
|
|
@@ -214,11 +217,12 @@ export function useDeckModel({ cwd, tasksPath, tasksDir, bareModel: bareModelDef
|
|
|
214
217
|
const archivableSig = useMemo(() => [...archivable].sort().join(','), [archivable]);
|
|
215
218
|
const dirtyFolders = useDirtyFolders(archivableSig, tasksDir, cwd);
|
|
216
219
|
// T134 — terzo scan della famiglia (D1): coda inbox per natura, età del più
|
|
217
|
-
// vecchio.
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
const
|
|
220
|
+
// vecchio. T175 — la docs-root è quella di `tasksPath`, risolta una volta in
|
|
221
|
+
// `cli.tsx`: lista task e coda inbox guardano la stessa cartella.
|
|
222
|
+
// T175 — coda inbox e albero doc li misura lo stesso script del plugin: dove
|
|
223
|
+
// non gira, nessuno dei due scan parte e i due pane dicono perché.
|
|
224
|
+
const pluginScripts = isAvailable('pluginScripts');
|
|
225
|
+
const inbox = useInboxScan(cwd, docsRoot, pluginScripts);
|
|
222
226
|
const inboxCounts = useMemo(() => deriveInboxCounts(inbox.files), [inbox.files]);
|
|
223
227
|
// Il numero accanto alla sirena: solo i file in coda oltre soglia. `Date.now()`
|
|
224
228
|
// letto a ogni render e non memoizzato — è una sottrazione, e congelarlo
|
|
@@ -232,7 +236,7 @@ export function useDeckModel({ cwd, tasksPath, tasksDir, bareModel: bareModelDef
|
|
|
232
236
|
// vero, quindi il montaggio rimisura da sé — `toggleDeckMode` sotto non deve
|
|
233
237
|
// chiamare `doc.scan()` come fa il gemello inbox, e chiamarlo darebbe due
|
|
234
238
|
// spawn da quattro secondi per un gesto solo.
|
|
235
|
-
const doc = useDocScan(cwd, docsRoot, deckMode === 'doc');
|
|
239
|
+
const doc = useDocScan(cwd, docsRoot, deckMode === 'doc' && pluginScripts);
|
|
236
240
|
// La misura GREZZA, memoizzata a parte: la consumano tre derivazioni (i
|
|
237
241
|
// contatori, le righe della vista, l'elenco dei file di una cartella nello
|
|
238
242
|
// sheet) e ricomporre l'oggetto a ogni render darebbe una identità nuova a
|
|
@@ -599,6 +603,7 @@ export function useDeckModel({ cwd, tasksPath, tasksDir, bareModel: bareModelDef
|
|
|
599
603
|
deckMode,
|
|
600
604
|
// derivazioni del pane inbox
|
|
601
605
|
docsRoot,
|
|
606
|
+
pluginScripts,
|
|
602
607
|
inboxScanned: inbox.scanned,
|
|
603
608
|
inboxOk: inbox.ok,
|
|
604
609
|
inboxViewId,
|
package/dist/doc-tree.js
CHANGED
|
@@ -468,7 +468,7 @@ export async function scanDocTree(projectRoot, docsRoot) {
|
|
|
468
468
|
if (!script)
|
|
469
469
|
return { files: [], dirs: [], hasDirs: false, ok: false };
|
|
470
470
|
try {
|
|
471
|
-
const { stdout } = await execFileAsync(script, ['--docs-root', docsRoot, '--format', 'tsv'], { cwd: projectRoot, maxBuffer: 8 * 1024 * 1024 });
|
|
471
|
+
const { stdout } = await execFileAsync(script, ['--docs-root', docsRoot, '--format', 'tsv'], { cwd: projectRoot, maxBuffer: 8 * 1024 * 1024, windowsHide: true });
|
|
472
472
|
return { ...parseDocTsv(stdout), ok: true };
|
|
473
473
|
}
|
|
474
474
|
catch {
|