@lamemind/loom-deck 0.76.0 → 0.78.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 -1
- package/dist/actions.js +32 -5
- package/dist/config.js +1 -1
- package/dist/deck-run.js +7 -0
- package/dist/deliverables.js +139 -0
- package/dist/doc-tree.js +1 -1
- package/dist/glyphs.js +16 -0
- package/dist/inbox.js +1 -1
- package/dist/input.js +4 -0
- package/dist/overlays/dlv-mode.js +123 -0
- package/dist/overlays/sheet.js +39 -2
- package/dist/platform.js +1 -2
- package/dist/project-path.js +43 -5
- package/dist/sane-note.js +18 -24
- package/dist/spawn-catalog.js +4 -4
- package/dist/spawn-config.js +4 -4
- package/dist/spawn.js +51 -7
- 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 +1 -1
- package/dist/ui/detail-screen.js +8 -3
- package/dist/ui/dlv-area.js +51 -0
- package/dist/ui/screens.js +1 -1
- package/dist/viewport.js +37 -14
- package/dist/windows-terminal.js +153 -0
- package/dist/wrap-scan.js +1 -1
- package/package.json +2 -1
- package/scripts/deck-run +11 -678
- package/scripts/prompt-catalog +6 -11
package/README.md
CHANGED
|
@@ -387,11 +387,34 @@ npx @lamemind/loom-deck
|
|
|
387
387
|
## Requisiti runtime
|
|
388
388
|
|
|
389
389
|
- **Node.js ≥ 18** (dichiarato in `engines`).
|
|
390
|
-
- **[Ptyxis](https://gitlab.gnome.org/chergert/ptyxis)** (terminale GNOME) — **dipendenza di runtime, non risolvibile da npm**. Lo spawn di una tab (`scripts/deck-run`) invoca il binario `ptyxis`: l'**install riesce anche senza**, ma al momento dello spawn il comando
|
|
390
|
+
- **[Ptyxis](https://gitlab.gnome.org/chergert/ptyxis)** (terminale GNOME), su Linux — **dipendenza di runtime, non risolvibile da npm**. Lo spawn di una tab (`scripts/deck-run`) invoca il binario `ptyxis`: l'**install riesce anche senza**, ma al momento dello spawn il comando
|
|
391
391
|
fallisce (gestito: handler `error` → la TUI resta viva, mostra la nota). Senza una GUI
|
|
392
392
|
GNOME con Ptyxis installato, il deck naviga i task ma **non apre sessioni**.
|
|
393
393
|
- **[Claude Code](https://claude.com/claude-code)** nel `PATH` — la tab spawnata avvia `claude`.
|
|
394
394
|
|
|
395
|
+
### Su Windows
|
|
396
|
+
|
|
397
|
+
Le tab si aprono in **Windows Terminal** (`wt.exe`, l'alias d'esecuzione che l'installazione dello Store mette nel `PATH`), nella finestra usata per ultima: `wt.exe -w 0 new-tab -p loom-deck -d <root> --title <titolo> --suppressApplicationTitle -- <node> <dist/tab-wrapper.js>`. L'argv di `claude` — nome, flag, prompt — non sta su quella riga: viaggia come JSON nella variabile `LOOM_DECK_TAB_ARGV`, che la tab eredita dal processo che lancia `wt.exe`, e il wrapper lo passa a `claude` senza shell. Il prompt non passa quindi né dal parser di `wt`, che legge `;` come separatore di sottocomandi in qualunque argomento, né da cmd o PowerShell.
|
|
398
|
+
|
|
399
|
+
Le tab usano il profilo di Windows Terminal **`loom-deck`**, uno solo per tutti i progetti. Il deck non lo crea e non scrive `settings.json`: lo si aggiunge a mano in `%LOCALAPPDATA%\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json`, sotto `profiles.list`, nascosto dal menu. Porta solo ciò che i flag di `wt` non sanno dire — titolo, cartella e comando li passa il deck, e battono quelli del profilo:
|
|
400
|
+
|
|
401
|
+
```jsonc
|
|
402
|
+
{
|
|
403
|
+
"name": "loom-deck",
|
|
404
|
+
"guid": "{<un GUID nuovo>}",
|
|
405
|
+
"hidden": true,
|
|
406
|
+
"colorScheme": "<nome dello schema, es. One Half Light>",
|
|
407
|
+
"commandline": "%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe -NoLogo",
|
|
408
|
+
"closeOnExit": "graceful"
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
`colorScheme` fissa i colori di tutte le tab del deck: è il nome di uno schema predefinito di Windows Terminal o di uno dichiarato sotto `schemes` nello stesso `settings.json`. `commandline` è la shell del tasto `t`; nelle conversazioni il comando del deck prende il suo posto. `closeOnExit` decide cosa resta della tab quando `claude` esce (il wrapper esce col suo codice). Senza il profilo la tab si apre comunque, col profilo di default. Il nome si cambia con `LOOM_DECK_WT_PROFILE` (vuota = nessun `-p`).
|
|
413
|
+
|
|
414
|
+
La tab eredita tutto l'env del deck, tranne i marker che una sessione Claude Code inietta nei figli (`CLAUDECODE`, `CLAUDE_CODE_CHILD_SESSION`, …): con quelli una conversazione aperta da un deck lanciato dentro Claude Code non salverebbe il transcript. Se manca, il deck aggiunge `CLAUDE_CODE_GIT_BASH_PATH`: col `PATH` di Git Bash `claude` eseguirebbe gli hook del plugin in PowerShell, e la task non arriverebbe in contesto.
|
|
415
|
+
|
|
416
|
+
Su Windows il resume di una conversazione e il restore (`^T`) restano spenti con un avviso finché il deck non sa quali conversazioni sono vive: senza quella informazione un resume può riaprire una conversazione già aperta in un'altra tab. Il fork resta acceso, perché apre un transcript nuovo.
|
|
417
|
+
|
|
395
418
|
## Uso (spike ①)
|
|
396
419
|
|
|
397
420
|
```bash
|
package/dist/actions.js
CHANGED
|
@@ -17,13 +17,15 @@
|
|
|
17
17
|
// migliorano di passaggio.
|
|
18
18
|
import { useRef } from 'react';
|
|
19
19
|
import { randomUUID } from 'node:crypto';
|
|
20
|
+
import { existsSync } from 'node:fs';
|
|
21
|
+
import { conversationCwd } from './project-path.js';
|
|
20
22
|
import { taskSlug } from './tasks.js';
|
|
21
23
|
import { appendNote, appendPin, appendPriority, appendSessionRecord, appendTaskBinding, } from './task-index.js';
|
|
22
24
|
import { neighborId } from './session-list.js';
|
|
23
25
|
import { cut, cutMiddle } from './width.js';
|
|
24
26
|
import { dropSessionFiles, sessionArtifacts } from './session-drop.js';
|
|
25
27
|
import { saveView, viewFilePath } from './view-store.js';
|
|
26
|
-
import { onInTabCommand, runLaunch, spawnClaudeEmpty, spawnBare, spawnDeck, spawnDeckFork, spawnDeckResume, spawnTerminal, DECK_RUN, } from './spawn.js';
|
|
28
|
+
import { onInTabCommand, runLaunch, spawnClaudeEmpty, spawnBare, spawnDeck, spawnDeckFork, spawnDeckResume, spawnTerminal, terminalName, DECK_RUN, } from './spawn.js';
|
|
27
29
|
import { MODEL_DEFAULT } from './spawn-catalog.js';
|
|
28
30
|
import { modelAlias } from './glyphs.js';
|
|
29
31
|
import { resolveSpawnId } from './spawn-config.js';
|
|
@@ -103,7 +105,7 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, overr
|
|
|
103
105
|
}
|
|
104
106
|
/** I buchi di un'azione su task. Lo slug viene dal NOME del task file, non
|
|
105
107
|
* dalla descrizione di `tasks.md`: il nome è già dentro l'alfabeto di
|
|
106
|
-
* `
|
|
108
|
+
* `saneNote` per costruzione — minuscolo, separato da trattini, senza
|
|
107
109
|
* punteggiatura — mentre la descrizione porta apostrofi e `/` che la riduzione
|
|
108
110
|
* toglie senza sostituto, saldando le parole. Task file assente → slug vuoto,
|
|
109
111
|
* e il titolo si spegne invece di ridursi al solo prefisso. */
|
|
@@ -220,9 +222,22 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, overr
|
|
|
220
222
|
// ricerca e il restore il modello ereditato dalla sessione senza superficie
|
|
221
223
|
// di scelta (D1 preflight). I chiamanti restano la stessa azione perché è
|
|
222
224
|
// lo spawn a essere identico, non la sorgente del valore.
|
|
225
|
+
/**
|
|
226
|
+
* T176 — la cartella della tab di una ripresa o di un fork: la root su Linux,
|
|
227
|
+
* la cwd registrata dalla conversazione su Windows (`conversationCwd`, P7
|
|
228
|
+
* preflight). Una cartella che non esiste più ricade sulla root: lo spawn
|
|
229
|
+
* fallirebbe prima ancora di aprire la tab.
|
|
230
|
+
*/
|
|
231
|
+
function tabCwdOf(sessionId) {
|
|
232
|
+
const s = model.sessions.find((x) => x.sessionId === sessionId);
|
|
233
|
+
if (!s)
|
|
234
|
+
return cwd;
|
|
235
|
+
const dir = conversationCwd(s.cwd, s.path, cwd);
|
|
236
|
+
return dir === cwd || existsSync(dir) ? dir : cwd;
|
|
237
|
+
}
|
|
223
238
|
function resumeSession(sessionId, modelKind) {
|
|
224
239
|
const bound = model.bindings.get(sessionId) ?? null;
|
|
225
|
-
const spawned = spawnDeckResume(bound,
|
|
240
|
+
const spawned = spawnDeckResume(bound, tabCwdOf(sessionId), sessionId, modelKind, model.sessionNotes.get(sessionId));
|
|
226
241
|
spawned.child.on('error', () => setNote(`⚠ resume fallito (${DECK_RUN})`));
|
|
227
242
|
noteSpawn(spawned);
|
|
228
243
|
return spawned;
|
|
@@ -242,6 +257,12 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, overr
|
|
|
242
257
|
function openSession(sessionId, modelKind) {
|
|
243
258
|
if (refuse('spawn', '⏎', setNote, 'resume'))
|
|
244
259
|
return;
|
|
260
|
+
// T176 — il resume vuole sapere se la conversazione è viva, non solo poter
|
|
261
|
+
// aprire una tab (D4 preflight): senza liveness un `⏎` su una conversazione
|
|
262
|
+
// aperta in un'altra tab avvierebbe un secondo `claude` sullo stesso
|
|
263
|
+
// transcript. Spento finché la piattaforma non ha le sessioni vive.
|
|
264
|
+
if (refuse('liveSessions', '⏎', setNote, 'resume'))
|
|
265
|
+
return;
|
|
245
266
|
enterSession(sessionId, {
|
|
246
267
|
isLive: (id) => model.live.has(id),
|
|
247
268
|
nameOf: (id) => model.live.get(id)?.name || id.slice(0, 8),
|
|
@@ -275,6 +296,12 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, overr
|
|
|
275
296
|
* chiudendo le tab.
|
|
276
297
|
*/
|
|
277
298
|
function restoreSessions() {
|
|
299
|
+
// T176 — la guardia delle sessioni vive PRIMA di quella dello spawn (P8
|
|
300
|
+
// preflight): l'insieme da riaprire è l'ultimo insieme vivo, e senza
|
|
301
|
+
// liveness sarebbe vuoto — «nessuna conversazione da riaprire» direbbe uno
|
|
302
|
+
// stato dove manca una funzione.
|
|
303
|
+
if (refuse('liveSessions', '^T', setNote, 'restore delle conversazioni'))
|
|
304
|
+
return;
|
|
278
305
|
if (refuse('spawn', '^T', setNote, 'restore delle conversazioni'))
|
|
279
306
|
return;
|
|
280
307
|
const list = model.restorable;
|
|
@@ -406,7 +433,7 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, overr
|
|
|
406
433
|
// T148 — il fork è l'altra continuità che condivide la stessa riga
|
|
407
434
|
// selezionata: porta via lo stesso `model.resumeModel` del resume, non un
|
|
408
435
|
// default fisso (P2 preflight).
|
|
409
|
-
const spawned = spawnDeckFork(bound,
|
|
436
|
+
const spawned = spawnDeckFork(bound, tabCwdOf(s.sessionId), s.sessionId, newId, model.resumeModel);
|
|
410
437
|
spawned.child.on('error', () => setNote(`⚠ fork fallito (${DECK_RUN})`));
|
|
411
438
|
noteSpawn(spawned);
|
|
412
439
|
}
|
|
@@ -533,7 +560,7 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, overr
|
|
|
533
560
|
return;
|
|
534
561
|
const title = model.identity ? `🖥️ ${model.identity.name} [term]` : null;
|
|
535
562
|
const child = spawnTerminal(cwd, title);
|
|
536
|
-
child.on('error', () => setNote(
|
|
563
|
+
child.on('error', () => setNote(`⚠ t → ${terminalName()} non lanciabile`));
|
|
537
564
|
setNote(`t → terminale su ${model.projectName}`);
|
|
538
565
|
}
|
|
539
566
|
/**
|
package/dist/config.js
CHANGED
|
@@ -115,7 +115,7 @@ export function launchLegend(entries, columns, reserved = 0) {
|
|
|
115
115
|
// `identity` sopra sono strutture. La catena da percorrere si ferma qui: nessun
|
|
116
116
|
// passaggio da `lib-config.sh`/`reg_pull`/dconf, perché il solo consumer è il
|
|
117
117
|
// deck e il file ce l'ha sotto mano. Il precedente esatto è `permissionMode`,
|
|
118
|
-
// che vive nel file, lo legge `deck-run`
|
|
118
|
+
// che vive nel file, lo legge anche `deck-run` e non è propagato al registry.
|
|
119
119
|
/** Interi positivi soltanto: uno 0 spegnerebbe la soglia (tutte le Done
|
|
120
120
|
* archiviabili), un negativo o un decimale sono un typo. Valore fuori dominio
|
|
121
121
|
* → default, mai passaggio cieco di un numero senza senso al conteggio. */
|
package/dist/deck-run.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// T176 — il punto d'ingresso di `deck-run`: `scripts/deck-run` (lo shim bash
|
|
2
|
+
// che compass e i test lanciano) e il deck su Windows eseguono questo file col
|
|
3
|
+
// proprio node. La logica sta in `tab-launch.ts`, che si testa senza aprire tab.
|
|
4
|
+
import { realIo, runDeckRun } from './tab-launch.js';
|
|
5
|
+
runDeckRun(process.argv.slice(2), realIo()).then((code) => {
|
|
6
|
+
process.exitCode = code;
|
|
7
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// T189 — i DELIVERABLE di un task file, e le due regole del plugin che il deck
|
|
2
|
+
// replica per offrirli nel detail.
|
|
3
|
+
//
|
|
4
|
+
// Il deck non esegue niente qui: legge i DLV per mostrarli e per comporre il
|
|
5
|
+
// perimetro di `run-task`. Ma il perimetro è un INDIRIZZO — `run-task T189 2,4`
|
|
6
|
+
// esegue il secondo e il quarto DLV come li conta il plugin, non come li conta
|
|
7
|
+
// chi ha composto il prompt. Se i due conteggi divergono l'umano seleziona DLV3
|
|
8
|
+
// e `run-task` esegue un altro DLV, senza errore. Per questo le due regole sono
|
|
9
|
+
// copie esatte, e la prima ha un test di parità contro lo script vero.
|
|
10
|
+
//
|
|
11
|
+
// ① NUMERAZIONE — `loom-works-plugin/scripts/task/task-deliverables.sh`:
|
|
12
|
+
// posizionale 1-based sulle sole checkbox a COLONNA ZERO dentro
|
|
13
|
+
// `## Deliverables Checklist`; le righe indentate sono prosa della voce.
|
|
14
|
+
// ② GATE DI ESEGUIBILITÀ — `run-task` SKILL.md §3: Size L vuole un
|
|
15
|
+
// `### Preflight` in `## Decisions`, S e M no, Size assente = M; un ultimo
|
|
16
|
+
// `### Preflight` con `— premessa decaduta` blocca a ogni Size. Questa regola
|
|
17
|
+
// vive nella prosa di una skill e non ha un presidio eseguibile: se cambia
|
|
18
|
+
// lì, va cambiata qui a mano.
|
|
19
|
+
//
|
|
20
|
+
// D6 (preflight T189) — parser TypeScript e non lo script del plugin lanciato
|
|
21
|
+
// dal deck: il deck gira anche su Windows, dove un `.sh` del plugin oggi non
|
|
22
|
+
// parte (T178), e il detail si apre a ogni `⏎` — uno spawn di bash lì dentro
|
|
23
|
+
// sarebbe un costo per tasto. La parità la garantisce il test, non la fonte
|
|
24
|
+
// comune.
|
|
25
|
+
//
|
|
26
|
+
// Logica pura, zero React/Ink.
|
|
27
|
+
import { parseTaskDetail } from './tasks.js';
|
|
28
|
+
/** Un heading di livello 2 apre una sezione nuova; `###` e oltre no. */
|
|
29
|
+
const SECTION_RE = /^## /;
|
|
30
|
+
const DLV_SECTION_RE = /^## Deliverables Checklist/;
|
|
31
|
+
/** Checkbox a colonna zero: `- [ ]`, `- [x]`, `- [X]`, e nient'altro. */
|
|
32
|
+
const CHECKBOX_RE = /^- \[[ xX]\]/;
|
|
33
|
+
/**
|
|
34
|
+
* I DLV del task file, nell'ordine del file.
|
|
35
|
+
*
|
|
36
|
+
* Copia riga per riga dell'`awk` di `task-deliverables.sh`, comprese le sue
|
|
37
|
+
* tolleranze: la sezione si riconosce per prefisso (`## Deliverables Checklist
|
|
38
|
+
* (…)` vale), un fence non sospende niente, e il testo è ciò che segue i sei
|
|
39
|
+
* caratteri della checkbox, spazi in testa tolti. Una «correzione» qui — saltare
|
|
40
|
+
* le checkbox dentro un fence, per dire — darebbe numeri diversi da quelli dello
|
|
41
|
+
* script sul primo task file che ne ha una.
|
|
42
|
+
*
|
|
43
|
+
* Testo assente (task file non trovato) → nessun DLV.
|
|
44
|
+
*/
|
|
45
|
+
export function parseDeliverables(text) {
|
|
46
|
+
if (!text)
|
|
47
|
+
return [];
|
|
48
|
+
const out = [];
|
|
49
|
+
let inSection = false;
|
|
50
|
+
for (const line of text.split('\n')) {
|
|
51
|
+
if (SECTION_RE.test(line)) {
|
|
52
|
+
inSection = DLV_SECTION_RE.test(line);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (!inSection || !CHECKBOX_RE.test(line))
|
|
56
|
+
continue;
|
|
57
|
+
out.push({
|
|
58
|
+
n: out.length + 1,
|
|
59
|
+
done: line[3] !== ' ',
|
|
60
|
+
text: line.slice(6).replace(/^[ \t]+/, ''),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
const DECISIONS_RE = /^## Decisions\b/;
|
|
66
|
+
const PREFLIGHT_RE = /^### Preflight\b/;
|
|
67
|
+
/** La dicitura che `preflight-task` mette nel heading quando si ferma davanti a
|
|
68
|
+
* una premessa smentita (§0b della skill). */
|
|
69
|
+
const DECAYED_RE = /premessa decaduta/i;
|
|
70
|
+
/**
|
|
71
|
+
* L'ULTIMO blocco `### Preflight` di `## Decisions` decide: `preflight-task`
|
|
72
|
+
* appende in fondo alla sezione, quindi l'ultimo nel file è il più recente.
|
|
73
|
+
* Una premessa decaduta seguita da un preflight riuscito non blocca più; il
|
|
74
|
+
* contrario sì.
|
|
75
|
+
*/
|
|
76
|
+
export function preflightState(text) {
|
|
77
|
+
if (!text)
|
|
78
|
+
return 'none';
|
|
79
|
+
let inDecisions = false;
|
|
80
|
+
let last = 'none';
|
|
81
|
+
for (const line of text.split('\n')) {
|
|
82
|
+
if (SECTION_RE.test(line)) {
|
|
83
|
+
inDecisions = DECISIONS_RE.test(line);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (inDecisions && PREFLIGHT_RE.test(line))
|
|
87
|
+
last = DECAYED_RE.test(line) ? 'decayed' : 'done';
|
|
88
|
+
}
|
|
89
|
+
return last;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* La Size della task, ridotta alla sua prima parola maiuscola (`L`, `M`, `S`,
|
|
93
|
+
* `EPIC`). Assente o vuota → `M`, come in `run-task` §4: la mancanza del campo
|
|
94
|
+
* non deve far scattare il ramo più severo.
|
|
95
|
+
*/
|
|
96
|
+
export function taskSize(id, text) {
|
|
97
|
+
if (!text)
|
|
98
|
+
return 'M';
|
|
99
|
+
const raw = parseTaskDetail(id, text).fields['Size'] ?? '';
|
|
100
|
+
return /^\s*([A-Za-z]+)/.exec(raw)?.[1]?.toUpperCase() ?? 'M';
|
|
101
|
+
}
|
|
102
|
+
export const DLV_REASON = {
|
|
103
|
+
closed: 'task chiusa',
|
|
104
|
+
empty: 'nessun deliverable in ## Deliverables Checklist',
|
|
105
|
+
allDone: 'tutti i deliverable sono chiusi',
|
|
106
|
+
decayed: 'premessa decaduta — ricostruisci le premesse, poi preflight-task',
|
|
107
|
+
sizeL: 'preflight obbligatorio — Size L senza ### Preflight in ## Decisions',
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Il gate di `run-task` §3 visto dal deck, più i due casi in cui non c'è niente
|
|
111
|
+
* da lanciare.
|
|
112
|
+
*
|
|
113
|
+
* L'ordine dei controlli è quello della domanda «cosa partirebbe?»: prima se c'è
|
|
114
|
+
* qualcosa da eseguire, poi se la skill lo eseguirebbe. Su una task chiusa con
|
|
115
|
+
* una premessa decaduta il motivo utile è «chiusa», non il preflight.
|
|
116
|
+
*
|
|
117
|
+
* `done` viene da `tasks.md` (la Prog ✔️ della lista, `isDone`) e non dal campo
|
|
118
|
+
* `Progress` del task file: è la stessa fonte che usa il resto del deck, e il
|
|
119
|
+
* testo che il detail ha in mano è già sanificato — il glifo di Done lì non è
|
|
120
|
+
* più quello scritto su disco.
|
|
121
|
+
*
|
|
122
|
+
* `Size: Epic` non esclude (D2): un'epica con DLV propri resta lanciabile, e se
|
|
123
|
+
* `run-task` si rifiuta il rifiuto è della skill, non del deck.
|
|
124
|
+
*/
|
|
125
|
+
export function dlvGate(id, text, dlvs, done) {
|
|
126
|
+
if (done)
|
|
127
|
+
return { kind: 'off', reason: DLV_REASON.closed };
|
|
128
|
+
if (dlvs.length === 0)
|
|
129
|
+
return { kind: 'off', reason: DLV_REASON.empty };
|
|
130
|
+
if (!dlvs.some((d) => !d.done))
|
|
131
|
+
return { kind: 'off', reason: DLV_REASON.allDone };
|
|
132
|
+
const preflight = preflightState(text);
|
|
133
|
+
if (preflight === 'decayed')
|
|
134
|
+
return { kind: 'blocked', reason: DLV_REASON.decayed };
|
|
135
|
+
if (taskSize(id, text) === 'L' && preflight === 'none') {
|
|
136
|
+
return { kind: 'blocked', reason: DLV_REASON.sizeL };
|
|
137
|
+
}
|
|
138
|
+
return { kind: 'ready' };
|
|
139
|
+
}
|
package/dist/doc-tree.js
CHANGED
|
@@ -376,7 +376,7 @@ export function docTargetKind(row, docsRoot) {
|
|
|
376
376
|
* T160 — il buco `{words}` del titolo di una sessione di rebalance: il path reso
|
|
377
377
|
* a parole.
|
|
378
378
|
*
|
|
379
|
-
* Gemello di `wrapWords`, e per la stessa ragione: l'alfabeto di `
|
|
379
|
+
* Gemello di `wrapWords`, e per la stessa ragione: l'alfabeto di `saneNote`
|
|
380
380
|
* non ha né `/` né `.`, quindi un path lasciato cadere lì dentro esce con le
|
|
381
381
|
* parole saldate (`runtimereferencedoc-systemmd`) mentre la lista del deck, che
|
|
382
382
|
* legge la nota grezza dal sidecar, mostrerebbe il path intero — la stessa
|
package/dist/glyphs.js
CHANGED
|
@@ -123,6 +123,22 @@ export const INBOX_MARK = {
|
|
|
123
123
|
* sessioni: la cella si riempie di spazi anche quando è vuota, o le righe
|
|
124
124
|
* `held` sposterebbero a sinistra tutto ciò che segue. */
|
|
125
125
|
export const INBOX_MARK_W = 2;
|
|
126
|
+
/**
|
|
127
|
+
* T189 — colonna di stato di un DLV nella lista del detail, larga 3 in tutte e
|
|
128
|
+
* tre le forme: il numero e il testo che seguono restano incolonnati.
|
|
129
|
+
*
|
|
130
|
+
* La selezione è `[•]` e non `[x]`: nel task file `[x]` vuol dire FATTO, e sulla
|
|
131
|
+
* stessa schermata il testo reso sopra la lista lo mostra proprio con quel
|
|
132
|
+
* significato. Un `[x]` che sotto vuol dire «scelto» sarebbe un falso amico. Il
|
|
133
|
+
* fatto porta la spunta del Done della lista task, lo stesso glifo per la
|
|
134
|
+
* stessa idea.
|
|
135
|
+
*/
|
|
136
|
+
export const DLV_MARK = {
|
|
137
|
+
done: sanitize('✔'),
|
|
138
|
+
open: '[ ]',
|
|
139
|
+
picked: '[•]',
|
|
140
|
+
};
|
|
141
|
+
export const DLV_MARK_W = 3;
|
|
126
142
|
// Marker Done per il DISPLAY. `task.prog` resta il `✔️` letto da tasks.md —
|
|
127
143
|
// `isDone()` e le lookup di `view.ts` ci confrontano sopra, e `task-edit` lo
|
|
128
144
|
// riscrive sul file: è una chiave semantica, non testo. Qui `sanitize` lo
|
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.
|
package/dist/input.js
CHANGED
|
@@ -26,6 +26,7 @@ import { captures, inertNote, scrolls, taskModeKey, } from './input-modes.js';
|
|
|
26
26
|
import { LEFT_PANE, META_ROWS, QUIT_WINDOW_MS, RIGHT_PANE } from './model.js';
|
|
27
27
|
import { TASK_VIEWS, taskView } from './pane-views.js';
|
|
28
28
|
import { loadTaskFileText } from './tasks.js';
|
|
29
|
+
import { isDone } from './layout.js';
|
|
29
30
|
import { inboxPrompt, loadInboxText } from './inbox.js';
|
|
30
31
|
import { dirListing, loadDocText } from './doc-tree.js';
|
|
31
32
|
import { refuse } from './platform.js';
|
|
@@ -524,6 +525,9 @@ export function useDeckInput({ cwd, tasksDir, mode, setMode, setNote, model, act
|
|
|
524
525
|
// restare senza intestazione solo perché il file manca.
|
|
525
526
|
title: model.detail?.title || task.desc,
|
|
526
527
|
text: loadTaskFileText(tasksDir, task.id),
|
|
528
|
+
// T189 — la Prog della lista, non il campo del task file: è la
|
|
529
|
+
// fonte che il resto del deck legge come «chiusa».
|
|
530
|
+
done: isDone(task.prog),
|
|
527
531
|
});
|
|
528
532
|
}
|
|
529
533
|
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// T189 — il MODO DLV del detail della task: la lista dei deliverable,
|
|
2
|
+
// selezionabile, al posto dell'area di compilazione.
|
|
3
|
+
//
|
|
4
|
+
// È una variante dell'azione `run` del detail, non un'azione nuova (D3): stesso
|
|
5
|
+
// spawn, stesso modello, stesso titolo, override di progetto compreso. Cambia
|
|
6
|
+
// solo il prompt — il perimetro scelto in coda (`/loom-works:run-task T189 2,4`),
|
|
7
|
+
// oppure `cosa fare ora [R0]` senza selezione (D4).
|
|
8
|
+
//
|
|
9
|
+
// Vive fuori da `sheet.ts` (D5) e non lo importa: il detail possiede il modo,
|
|
10
|
+
// questo modulo ne possiede lo stato. Le regole del plugin che decide di
|
|
11
|
+
// rispettare — numerazione e gate di `run-task` — stanno in `deliverables.ts`.
|
|
12
|
+
import { useState } from 'react';
|
|
13
|
+
import { dlvGate, parseDeliverables } from '../deliverables.js';
|
|
14
|
+
/** D4 — il prompt di `⏎` senza nessun DLV selezionato. */
|
|
15
|
+
export const DLV_IDLE_PROMPT = 'cosa fare ora [R0]';
|
|
16
|
+
/**
|
|
17
|
+
* D3/D4 — il prompt che parte da `⏎` nel modo DLV.
|
|
18
|
+
*
|
|
19
|
+
* Con una selezione: il prompt dell'azione `run` GIÀ RISOLTO (default del deck ←
|
|
20
|
+
* override di progetto, buchi riempiti) con il perimetro in coda, in ordine
|
|
21
|
+
* crescente — `run-task` normalizza da sé, ma il prompt è anche ciò che l'umano
|
|
22
|
+
* legge nella riga di stato prima di premere. Lista semplice e non range: la
|
|
23
|
+
* grammatica `1,3-5` di `run-task` la accetta, e compattare non toglie ambiguità.
|
|
24
|
+
*
|
|
25
|
+
* Senza selezione: `cosa fare ora [R0]`, nella stessa sessione bound di `run`.
|
|
26
|
+
*/
|
|
27
|
+
export function dlvPrompt(runPrompt, selected) {
|
|
28
|
+
const scope = [...selected].sort((a, b) => a - b);
|
|
29
|
+
if (scope.length === 0)
|
|
30
|
+
return DLV_IDLE_PROMPT;
|
|
31
|
+
return `${(runPrompt ?? '').trim()} ${scope.join(',')}`.trim();
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Il DLV aperto dopo (o prima di) `from`, in giro come le righe dell'area di
|
|
35
|
+
* compilazione: `↑↓` si fermano solo sui DLV da fare. `from` è un NUMERO di DLV
|
|
36
|
+
* e non un indice — resta valido comunque la lista venga finestrata a schermo.
|
|
37
|
+
* `from` fuori dagli aperti → il primo aperto; nessun DLV aperto → 0.
|
|
38
|
+
*/
|
|
39
|
+
export function stepOpen(dlvs, from, delta) {
|
|
40
|
+
const open = dlvs.filter((d) => !d.done).map((d) => d.n);
|
|
41
|
+
if (open.length === 0)
|
|
42
|
+
return 0;
|
|
43
|
+
const at = open.indexOf(from);
|
|
44
|
+
if (at < 0)
|
|
45
|
+
return open[0];
|
|
46
|
+
return open[(((at + delta) % open.length) + open.length) % open.length];
|
|
47
|
+
}
|
|
48
|
+
/** `spazio` sul DLV `n`: lo mette nella selezione o lo toglie. */
|
|
49
|
+
export function toggleIn(selected, n) {
|
|
50
|
+
const next = new Set(selected);
|
|
51
|
+
if (next.has(n))
|
|
52
|
+
next.delete(n);
|
|
53
|
+
else
|
|
54
|
+
next.add(n);
|
|
55
|
+
return next;
|
|
56
|
+
}
|
|
57
|
+
const NONE = new Set();
|
|
58
|
+
const CLOSED = { kind: 'off', reason: '' };
|
|
59
|
+
export function useDlvMode() {
|
|
60
|
+
/** Il detail mostra la lista DLV (`true`) o l'area di compilazione. */
|
|
61
|
+
const [active, setActive] = useState(false);
|
|
62
|
+
const [dlvs, setDlvs] = useState([]);
|
|
63
|
+
const [gate, setGate] = useState(CLOSED);
|
|
64
|
+
const [cursor, setCursor] = useState(0);
|
|
65
|
+
const [selected, setSelected] = useState(NONE);
|
|
66
|
+
const [run, setRun] = useState(null);
|
|
67
|
+
/**
|
|
68
|
+
* Azzera tutto a ogni apertura del detail, come gli altri campi: una selezione
|
|
69
|
+
* che sopravvive alla chiusura farebbe partire un perimetro che a schermo non
|
|
70
|
+
* si vede più.
|
|
71
|
+
*
|
|
72
|
+
* Il modo di apertura è il gate: si apre sui DLV solo se `⏎` lì lancerebbe
|
|
73
|
+
* davvero qualcosa — su una task bloccata o chiusa l'area di compilazione resta
|
|
74
|
+
* quella di sempre, e la lista si raggiunge con `tab`.
|
|
75
|
+
*/
|
|
76
|
+
function open(input) {
|
|
77
|
+
const list = parseDeliverables(input.text);
|
|
78
|
+
const g = dlvGate(input.id, input.text, list, input.done);
|
|
79
|
+
setDlvs(list);
|
|
80
|
+
setGate(g);
|
|
81
|
+
setRun(input.run);
|
|
82
|
+
setCursor(g.kind === 'ready' ? stepOpen(list, 0, 1) : 0);
|
|
83
|
+
setSelected(NONE);
|
|
84
|
+
setActive(g.kind === 'ready');
|
|
85
|
+
}
|
|
86
|
+
function toggle() {
|
|
87
|
+
setActive((a) => !a);
|
|
88
|
+
}
|
|
89
|
+
const prompt = dlvPrompt(run?.prompt ?? null, selected);
|
|
90
|
+
/** Lo spawn di `⏎`, o `null` se il gate non lo permette: la ragione è già a
|
|
91
|
+
* schermo nella riga di stato della lista, quindi il tasto resta inerte. */
|
|
92
|
+
function launch() {
|
|
93
|
+
if (gate.kind !== 'ready' || !run)
|
|
94
|
+
return null;
|
|
95
|
+
return { model: run.model, title: (run.title ?? '').trim(), prompt };
|
|
96
|
+
}
|
|
97
|
+
/** I tasti della lista. Il chiamante gestisce prima i suoi (`tab`, `⏎`, `esc`,
|
|
98
|
+
* `^F`, `PgUp`/`PgDn`); qui resta ciò che muove cursore e selezione. Fuori
|
|
99
|
+
* da `ready` nessun DLV è selezionabile, quindi ogni tasto è inerte. */
|
|
100
|
+
function onKey(input, key) {
|
|
101
|
+
if (gate.kind !== 'ready')
|
|
102
|
+
return;
|
|
103
|
+
if (key.upArrow || key.downArrow) {
|
|
104
|
+
setCursor((c) => stepOpen(dlvs, c, key.upArrow ? -1 : 1));
|
|
105
|
+
}
|
|
106
|
+
else if (input === ' ' && !key.ctrl && !key.meta && cursor > 0) {
|
|
107
|
+
setSelected((s) => toggleIn(s, cursor));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
active,
|
|
112
|
+
dlvs,
|
|
113
|
+
gate,
|
|
114
|
+
cursor,
|
|
115
|
+
selected,
|
|
116
|
+
prompt,
|
|
117
|
+
model: run?.model ?? null,
|
|
118
|
+
open,
|
|
119
|
+
toggle,
|
|
120
|
+
launch,
|
|
121
|
+
onKey,
|
|
122
|
+
};
|
|
123
|
+
}
|
package/dist/overlays/sheet.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// Lo spawn della sessione dalla barra azioni NON sta qui: arriva come callback
|
|
13
13
|
// `onAction`, per lo stesso confine di `useSearchOverlay`.
|
|
14
14
|
import { useEffect, useMemo, useState } from 'react';
|
|
15
|
-
import { detailCapacity, pageStep } from '../viewport.js';
|
|
15
|
+
import { DETAIL_FIELD_ROWS, DLV_STATUS_ROWS, detailCapacity, dlvListRows, pageStep, } from '../viewport.js';
|
|
16
16
|
import { wrapWithOffsets } from '../width.js';
|
|
17
17
|
import { scanText, topForOffset } from '../text-search.js';
|
|
18
18
|
import { parseMarkdown } from '../markdown.js';
|
|
@@ -21,6 +21,7 @@ import { sanitizeTyped } from '../glyphs.js';
|
|
|
21
21
|
import { ACTION_HOTKEYS, DETAIL_ACTIONS, MODELS, MODEL_DEFAULT, specializeRecap, } from '../spawn-catalog.js';
|
|
22
22
|
import { taskIsEpic } from '../tasks.js';
|
|
23
23
|
import { fieldsKey } from '../fields.js';
|
|
24
|
+
import { useDlvMode } from './dlv-mode.js';
|
|
24
25
|
// T117 — le righe dell'area di compilazione del detail, nell'ordine in cui si
|
|
25
26
|
// leggono dall'alto. Gli indici sono nominati perché li usano insieme l'handler
|
|
26
27
|
// dei tasti e la resa, e un `2` nudo in due file diversi è la coordinata che
|
|
@@ -67,6 +68,9 @@ export function useSheetOverlay(deps) {
|
|
|
67
68
|
* riparserebbe l'intero task file per un campo dell'header. */
|
|
68
69
|
const [epic, setEpic] = useState(false);
|
|
69
70
|
const [cursor, setCursor] = useState({ row: DROW.action, caret: 0 });
|
|
71
|
+
// T189 — il secondo modo del detail: la lista dei DLV al posto dell'area di
|
|
72
|
+
// compilazione, alternata da `tab`.
|
|
73
|
+
const dlv = useDlvMode();
|
|
70
74
|
// T91 — ricerca dentro il detail. `open` distingue i due modi in cui la si
|
|
71
75
|
// lascia: `esc` butta via ciò che il modale ha prodotto (`find` a null,
|
|
72
76
|
// evidenziazione via), `⏎` lo congela e restituisce il controllo allo strato
|
|
@@ -91,7 +95,11 @@ export function useSheetOverlay(deps) {
|
|
|
91
95
|
// RESO, ed è l'unica coordinata coerente che resti — il sorgente non è più
|
|
92
96
|
// ciò che sta a schermo.
|
|
93
97
|
const lines = useMemo(() => (doc ? wrapWithOffsets(doc.text, width) : []), [doc, width]);
|
|
94
|
-
|
|
98
|
+
// T189 — l'area sotto il testo cambia altezza col modo: cinque righe di campi,
|
|
99
|
+
// oppure la lista DLV più la sua riga di stato. Il testo prende il resto.
|
|
100
|
+
const searching = find?.open === true;
|
|
101
|
+
const dlvRows = dlvListRows(rows, searching, dlv.dlvs.length);
|
|
102
|
+
const capacity = detailCapacity(rows, searching, dlv.active ? dlvRows + DLV_STATUS_ROWS : DETAIL_FIELD_ROWS);
|
|
95
103
|
const maxTop = Math.max(0, lines.length - capacity);
|
|
96
104
|
// Lo scan gira sulla STESSA stringa che si renderizza: cercare su un testo
|
|
97
105
|
// diverso da quello a schermo darebbe offset che indicizzano un altro
|
|
@@ -137,6 +145,9 @@ export function useSheetOverlay(deps) {
|
|
|
137
145
|
setCursor({ row: DROW.action, caret: 0 });
|
|
138
146
|
setFind(null);
|
|
139
147
|
setOccIdx(0);
|
|
148
|
+
// T189/D3 — il modo DLV è una variante di `run`: la terna gli arriva già
|
|
149
|
+
// risolta, override di progetto compreso, come all'azione che replica.
|
|
150
|
+
dlv.open({ id: next.id, text: next.text, done: next.done === true, run: defaults('run', next.id) });
|
|
140
151
|
setNote('');
|
|
141
152
|
setMode('detail');
|
|
142
153
|
}
|
|
@@ -264,6 +275,13 @@ export function useSheetOverlay(deps) {
|
|
|
264
275
|
setFind((f) => (f ? { ...f, open: true } : { q: '', caret: 0, open: true }));
|
|
265
276
|
return;
|
|
266
277
|
}
|
|
278
|
+
if (key.tab) {
|
|
279
|
+
// T189 — `tab` alterna area di compilazione e lista DLV. Nel detail era
|
|
280
|
+
// inerte (`fieldsKey` non lo consuma), e il `tab` che cicla le viste vive
|
|
281
|
+
// solo nella lista: nessun significato tolto.
|
|
282
|
+
dlv.toggle();
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
267
285
|
if (key.escape) {
|
|
268
286
|
// Uno strato alla volta: se resta un'evidenziazione congelata, `esc`
|
|
269
287
|
// smonta quella; il detail lo chiude il secondo.
|
|
@@ -275,6 +293,19 @@ export function useSheetOverlay(deps) {
|
|
|
275
293
|
close();
|
|
276
294
|
return;
|
|
277
295
|
}
|
|
296
|
+
if (key.return && dlv.active) {
|
|
297
|
+
// T189 — nel modo DLV `⏎` lancia `run` col perimetro scelto. Gate non
|
|
298
|
+
// pronto → inerte: il motivo è già scritto sotto la lista, e chiudere il
|
|
299
|
+
// detail lo porterebbe via.
|
|
300
|
+
const launch = dlv.launch();
|
|
301
|
+
if (!launch)
|
|
302
|
+
return;
|
|
303
|
+
const id = sheet?.id;
|
|
304
|
+
close();
|
|
305
|
+
if (id)
|
|
306
|
+
onAction(id, launch.model, launch.title, launch.prompt, false);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
278
309
|
if (key.return) {
|
|
279
310
|
// `⏎` esegue SEMPRE l'azione selezionata, da qualunque riga: i campi sono
|
|
280
311
|
// di una riga sola, quindi nessuno di loro ha da farci un a-capo.
|
|
@@ -297,6 +328,10 @@ export function useSheetOverlay(deps) {
|
|
|
297
328
|
scroll(pageStep(capacity));
|
|
298
329
|
return;
|
|
299
330
|
}
|
|
331
|
+
if (dlv.active) {
|
|
332
|
+
dlv.onKey(input, key);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
300
335
|
// Tutto il resto è dell'area di compilazione. Ciò che non consuma resta
|
|
301
336
|
// inerte — dentro un modo capturing è la scelta giusta: gli acceleratori
|
|
302
337
|
// globali non devono riattivarsi, e su una riga a scelta una lettera che
|
|
@@ -319,6 +354,8 @@ export function useSheetOverlay(deps) {
|
|
|
319
354
|
maxTop,
|
|
320
355
|
findRes,
|
|
321
356
|
occCur,
|
|
357
|
+
dlv,
|
|
358
|
+
dlvRows,
|
|
322
359
|
open,
|
|
323
360
|
onKey,
|
|
324
361
|
scroll,
|
package/dist/platform.js
CHANGED
|
@@ -17,8 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
export const UNAVAILABLE = {
|
|
19
19
|
win32: [
|
|
20
|
-
'
|
|
21
|
-
'liveSessions', // T177 — liveness senza /proc
|
|
20
|
+
'liveSessions', // T177 — liveness senza /proc; spegne anche resume e restore
|
|
22
21
|
'pluginScripts', // T178 — .sh del plugin via Git Bash
|
|
23
22
|
'hardWrap', // T178 — python del wrap-scan
|
|
24
23
|
'launch', // T179 — comando per piattaforma nelle voci launch[]
|
package/dist/project-path.js
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
// stringa È il path; su Windows no, per tre ragioni misurate (verdetto 1 di T174):
|
|
8
8
|
//
|
|
9
9
|
// - NTFS non distingue le maiuscole, e Claude Code non normalizza la grafia
|
|
10
|
-
// della cwd: VS Code passa `c:\…`, Windows Terminal `C
|
|
11
|
-
// progetto
|
|
10
|
+
// della cwd: VS Code passa `c:\…`, Windows Terminal `C:\…`. La cartella di
|
|
11
|
+
// transcript del progetto è una sola, ma il suo nome porta la grafia di chi
|
|
12
|
+
// l'ha creata (`c--…` o `C--…`);
|
|
12
13
|
// - `\` e `/` sono entrambi separatori;
|
|
13
14
|
// - nei transcript `cwd` compare anche in forma MSYS (`/c/…`), quella di Git
|
|
14
15
|
// Bash.
|
|
@@ -59,9 +60,11 @@ export function projectBaseName(cwd, platform = process.platform) {
|
|
|
59
60
|
* Le cartelle di transcript del progetto, fra i nomi di `~/.claude/projects/`.
|
|
60
61
|
*
|
|
61
62
|
* Su Linux una sola, il nome esatto. Su win32 ogni nome che coincide ignorando
|
|
62
|
-
* le maiuscole:
|
|
63
|
-
*
|
|
64
|
-
*
|
|
63
|
+
* le maiuscole: per NTFS `c--…` e `C--…` sono la stessa cartella, ma `readdir`
|
|
64
|
+
* ne restituisce il nome con la grafia di chi l'ha creata — `c--…` se la prima
|
|
65
|
+
* conversazione è nata da VS Code (`c:\…`) — mentre il nome cercato il deck lo
|
|
66
|
+
* ricava dalla propria root, che può avere l'altra grafia. Un confronto esatto
|
|
67
|
+
* non troverebbe la cartella.
|
|
65
68
|
*/
|
|
66
69
|
export function matchingDirNames(entries, name, platform = process.platform) {
|
|
67
70
|
if (platform !== 'win32')
|
|
@@ -69,3 +72,38 @@ export function matchingDirNames(entries, name, platform = process.platform) {
|
|
|
69
72
|
const want = name.toLowerCase();
|
|
70
73
|
return entries.filter((e) => e.toLowerCase() === want);
|
|
71
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* T176 — la cartella in cui nasce la tab che riprende o forka una conversazione.
|
|
77
|
+
*
|
|
78
|
+
* Su Linux la root del progetto, come prima. Su win32 la cwd di avvio che la
|
|
79
|
+
* conversazione ha registrato, in forma Windows e con la grafia del drive
|
|
80
|
+
* d'origine (P7 preflight). La ragione non è trovare il transcript: per NTFS
|
|
81
|
+
* `c--…` e `C--…` sono la stessa cartella, e `claude --resume` lo trova da tutte
|
|
82
|
+
* e due le grafie (misurato in T176 con Claude Code 2.1.310, anche con
|
|
83
|
+
* `--fork-session`). La ragione è la fiducia della cartella: Claude Code la
|
|
84
|
+
* tiene in `~/.claude.json` per grafia esatta del path (`c:/…` e `C:/…` sono due
|
|
85
|
+
* voci), e una sessione interattiva in una grafia mai approvata si ferma sulla
|
|
86
|
+
* domanda di fiducia prima di caricare la conversazione. Una conversazione nata
|
|
87
|
+
* interattiva ha già la fiducia nella grafia in cui è nata.
|
|
88
|
+
*
|
|
89
|
+
* Una cwd in forma MSYS (`/c/…`) non dice la grafia del drive: la dice il nome
|
|
90
|
+
* della cartella del transcript, che Claude Code ha derivato dalla cwd vera
|
|
91
|
+
* (`c--…` o `C--…`). Una cwd che non è un path assoluto Windows ricade sulla
|
|
92
|
+
* root.
|
|
93
|
+
*/
|
|
94
|
+
export function conversationCwd(sessionCwd, transcriptPath, root, platform = process.platform) {
|
|
95
|
+
if (platform !== 'win32')
|
|
96
|
+
return root;
|
|
97
|
+
if (/^[a-zA-Z]:[\\/]/.test(sessionCwd))
|
|
98
|
+
return sessionCwd.replace(/\//g, '\\');
|
|
99
|
+
if (sessionCwd.startsWith('\\\\'))
|
|
100
|
+
return sessionCwd;
|
|
101
|
+
const msys = /^\/([a-zA-Z])(?:\/(.*))?$/.exec(sessionCwd);
|
|
102
|
+
if (!msys)
|
|
103
|
+
return root;
|
|
104
|
+
const folder = win32.basename(win32.dirname(transcriptPath));
|
|
105
|
+
const letter = folder.length > 2 && folder.slice(1, 3) === '--' && folder[0].toLowerCase() === msys[1].toLowerCase()
|
|
106
|
+
? folder[0]
|
|
107
|
+
: msys[1].toUpperCase();
|
|
108
|
+
return `${letter}:\\${(msys[2] ?? '').replace(/\//g, '\\')}`;
|
|
109
|
+
}
|