@lamemind/loom-deck 0.77.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 +2 -1
- package/dist/deliverables.js +139 -0
- package/dist/glyphs.js +16 -0
- package/dist/input.js +4 -0
- package/dist/overlays/dlv-mode.js +123 -0
- package/dist/overlays/sheet.js +39 -2
- package/dist/project-path.js +18 -11
- 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/package.json +1 -1
package/README.md
CHANGED
|
@@ -403,12 +403,13 @@ Le tab usano il profilo di Windows Terminal **`loom-deck`**, uno solo per tutti
|
|
|
403
403
|
"name": "loom-deck",
|
|
404
404
|
"guid": "{<un GUID nuovo>}",
|
|
405
405
|
"hidden": true,
|
|
406
|
+
"colorScheme": "<nome dello schema, es. One Half Light>",
|
|
406
407
|
"commandline": "%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe -NoLogo",
|
|
407
408
|
"closeOnExit": "graceful"
|
|
408
409
|
}
|
|
409
410
|
```
|
|
410
411
|
|
|
411
|
-
`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`).
|
|
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`).
|
|
412
413
|
|
|
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.
|
|
414
415
|
|
|
@@ -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/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/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/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')
|
|
@@ -72,12 +75,16 @@ export function matchingDirNames(entries, name, platform = process.platform) {
|
|
|
72
75
|
/**
|
|
73
76
|
* T176 — la cartella in cui nasce la tab che riprende o forka una conversazione.
|
|
74
77
|
*
|
|
75
|
-
* Su Linux la root del progetto, come prima. Su win32 la cwd che la
|
|
76
|
-
* conversazione ha registrato, in forma Windows
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* `
|
|
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.
|
|
81
88
|
*
|
|
82
89
|
* Una cwd in forma MSYS (`/c/…`) non dice la grafia del drive: la dice il nome
|
|
83
90
|
* della cartella del transcript, che Claude Code ha derivato dalla cwd vera
|
package/dist/ui/detail-screen.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
// Detail della task (T66): quarta schermata sostitutiva, col markdown reso in
|
|
3
3
|
// span tipizzati (T75) e la ricerca interna (T91).
|
|
4
4
|
import { Box, Text } from 'ink';
|
|
@@ -9,6 +9,7 @@ import { ChoiceRow, FieldText, LABEL_W } from './fields.js';
|
|
|
9
9
|
import { DETAIL_ACTIONS, MODELS } from '../spawn-catalog.js';
|
|
10
10
|
import { DROW, PRIORITY_CHOICES } from '../overlays/sheet.js';
|
|
11
11
|
import { CARET, CARET_OFF, WARN } from '../glyphs.js';
|
|
12
|
+
import { DlvArea } from './dlv-area.js';
|
|
12
13
|
/** Resa di ogni costrutto markdown (T75 · D4): un solo livello di enfasi per
|
|
13
14
|
* costrutto, senza un secondo alfabeto da imparare. Heading uguali a ogni
|
|
14
15
|
* livello — la gerarchia la porta già il testo. `code` e `fence` condividono
|
|
@@ -79,7 +80,7 @@ const ACTION_KEYS = DETAIL_ACTIONS.map((a) => a.label[0]).join(' ');
|
|
|
79
80
|
* rettangolo ha già coordinate e area cliccabile, quindi il layout sopravvive
|
|
80
81
|
* all'arrivo del mouse (T21 · SGR enable + hit-test) senza migrazione.
|
|
81
82
|
*/
|
|
82
|
-
export function DetailScreen({ id, title, missing, lines, spans, top, total, capacity, action, model, spawnNote, prompt, priority, cursor, columns, find, occ, occCur, }) {
|
|
83
|
+
export function DetailScreen({ id, title, missing, lines, spans, top, total, capacity, action, model, spawnNote, prompt, priority, cursor, columns, find, occ, occCur, dlv, dlvRows, }) {
|
|
83
84
|
const last = Math.min(total, top + capacity);
|
|
84
85
|
// Il taglio lo fa il chiamante (invariante ③ di width.ts): le righe bottoni
|
|
85
86
|
// sono ASCII, quindi `truncate-end` oggi darebbe il risultato giusto per caso
|
|
@@ -91,5 +92,9 @@ export function DetailScreen({ id, title, missing, lines, spans, top, total, cap
|
|
|
91
92
|
// campo vuoto): riservarle sempre toglierebbe testo visibile a un titolo lungo
|
|
92
93
|
// per un avviso assente.
|
|
93
94
|
const textW = (extra) => Math.max(10, width - LABEL_W - CARET_OFF.length - extra);
|
|
94
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "cyan", children: id }), " \u00B7 ", cut(title, Math.max(10, width - 34)), missing ? '' : ` · righe ${total === 0 ? 0 : top + 1}-${last} di ${total}`] }), find?.open ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "yellow", children: "\u2191\u2193" }), " occorrenza \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " caret \u00B7", ' ', _jsx(Text, { color: "yellow", children: "^U" }), " svuota \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " tieni \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "yellow", children: "PgUp/PgDn" }), " testo \u00B7", ' ', _jsx(Text, { color: "yellow", children: ACTION_KEYS }), " azione \u00B7", ' ', _jsx(Text, { color: "yellow", children: "^U" }), " svuota campo \u00B7 ", _jsx(Text, { color: "yellow", children: "^F" }), " cerca"] })), _jsx(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, marginTop: 1, children: missing ? (_jsxs(Text, { color: "yellow", wrap: "truncate-end", children: [WARN, " task file non trovato \u00B7 le azioni restano attive (deck-run risolve la task per id)"] })) : (lines.map((l, i) => (_jsx(DetailLine, { line: l, spans: spans, occ: occ, current: occCur }, top + i)))) }), find?.open ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { dimColor: true, children: "cerca " }), _jsx(FieldText, { value: find.q, caret: find.caret, focused: true, cols: Math.max(10, width - 28) }), find.q.length === 0 ? (_jsx(Text, { dimColor: true, children: " \u00B7 digita per cercare" })) : occ.length === 0 ? (_jsx(Text, { color: "yellow", children: " \u00B7 nessuna occorrenza" })) : (_jsxs(Text, { color: "cyan", children: [' ', "\u00B7 ", occCur + 1, "/", occ.length] }))] }) })) : null,
|
|
95
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "cyan", children: id }), " \u00B7 ", cut(title, Math.max(10, width - 34)), missing ? '' : ` · righe ${total === 0 ? 0 : top + 1}-${last} di ${total}`] }), find?.open ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "yellow", children: "\u2191\u2193" }), " occorrenza \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " caret \u00B7", ' ', _jsx(Text, { color: "yellow", children: "^U" }), " svuota \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " tieni \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : dlv.active ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [dlv.gate.kind === 'ready' ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "yellow", children: "\u2191\u2193" }), " DLV aperti \u00B7 ", _jsx(Text, { color: "yellow", children: "spazio" }), ' ', "seleziona \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " run \u00B7", ' '] })) : null, _jsx(Text, { color: "yellow", children: "tab" }), " campi \u00B7 ", _jsx(Text, { color: "yellow", children: "PgUp/PgDn" }), " testo \u00B7", ' ', _jsx(Text, { color: "yellow", children: "^F" }), " cerca"] })) : (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "yellow", children: "PgUp/PgDn" }), " testo \u00B7", ' ', _jsx(Text, { color: "yellow", children: ACTION_KEYS }), " azione \u00B7", ' ', _jsx(Text, { color: "yellow", children: "^U" }), " svuota campo \u00B7 ", _jsx(Text, { color: "yellow", children: "^F" }), " cerca \u00B7", ' ', _jsx(Text, { color: "yellow", children: "tab" }), " DLV"] })), _jsx(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, marginTop: 1, children: missing ? (_jsxs(Text, { color: "yellow", wrap: "truncate-end", children: [WARN, " task file non trovato \u00B7 le azioni restano attive (deck-run risolve la task per id)"] })) : (lines.map((l, i) => (_jsx(DetailLine, { line: l, spans: spans, occ: occ, current: occCur }, top + i)))) }), find?.open ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { dimColor: true, children: "cerca " }), _jsx(FieldText, { value: find.q, caret: find.caret, focused: true, cols: Math.max(10, width - 28) }), find.q.length === 0 ? (_jsx(Text, { dimColor: true, children: " \u00B7 digita per cercare" })) : occ.length === 0 ? (_jsx(Text, { color: "yellow", children: " \u00B7 nessuna occorrenza" })) : (_jsxs(Text, { color: "cyan", children: [' ', "\u00B7 ", occCur + 1, "/", occ.length] }))] }) })) : null, dlv.active ? (_jsx(DlvArea, { dlvs: dlv.dlvs, cursor: dlv.cursor, selected: dlv.selected, gate: dlv.gate, prompt: dlv.prompt, model: dlv.model, rows: dlvRows, width: width })) : (
|
|
96
|
+
/* Le quattro righe dell'area di compilazione, nell'ordine in cui si
|
|
97
|
+
decide: cosa lanciare, con quale prompt, con quale modello, sotto quale
|
|
98
|
+
titolo. */
|
|
99
|
+
_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(ChoiceRow, { label: "azione", values: DETAIL_ACTIONS.map((a) => a.label), index: action, focused: cursor.row === DROW.action, width: width }), _jsxs(Text, { wrap: "truncate-end", children: [mark(DROW.prompt), _jsx(Text, { dimColor: true, children: 'prompt'.padEnd(LABEL_W) }), _jsx(FieldText, { value: prompt, caret: cursor.caret, focused: cursor.row === DROW.prompt, cols: textW(prompt ? 0 : PROMPT_HINT.length) }), prompt ? null : _jsx(Text, { dimColor: true, children: PROMPT_HINT })] }), _jsx(ChoiceRow, { label: "modello", values: MODELS, index: Math.max(0, MODELS.indexOf(model)), focused: cursor.row === DROW.model, width: width }), _jsxs(Text, { wrap: "truncate-end", children: [mark(DROW.title), _jsx(Text, { dimColor: true, children: 'titolo'.padEnd(LABEL_W) }), _jsx(FieldText, { value: spawnNote, caret: cursor.caret, focused: cursor.row === DROW.title, cols: textW(spawnNote ? 0 : NOTE_HINT.length) }), spawnNote ? null : _jsx(Text, { dimColor: true, children: NOTE_HINT })] }), _jsx(ChoiceRow, { label: "priorit\u00E0", values: PRIORITY_CHOICES, index: priority ? 1 : 0, focused: cursor.row === DROW.priority, width: width })] }))] }));
|
|
95
100
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// T189 — la lista dei DLV nel detail della task, al posto dell'area di
|
|
3
|
+
// compilazione quando il modo DLV è attivo.
|
|
4
|
+
import { Box, Text } from 'ink';
|
|
5
|
+
import { cut, cutParts, pad } from '../width.js';
|
|
6
|
+
import { parseMarkdown } from '../markdown.js';
|
|
7
|
+
import { windowRange } from '../viewport.js';
|
|
8
|
+
import { CARET, CARET_OFF, DLV_MARK, DLV_MARK_W, WARN } from '../glyphs.js';
|
|
9
|
+
/** Il testo di una voce come lo rende il detail sopra: senza `**` e backtick.
|
|
10
|
+
* Il markdown resta grezzo nel task file, e una lista che mostrasse i marker
|
|
11
|
+
* direbbe la stessa voce in due forme sulla stessa schermata. */
|
|
12
|
+
function plain(text) {
|
|
13
|
+
return parseMarkdown(text).text;
|
|
14
|
+
}
|
|
15
|
+
/** La riga di stato: cosa parte su `⏎`, o perché non parte niente, più quante
|
|
16
|
+
* righe della lista stanno fuori dalla finestra. Quattro pezzi, uno per
|
|
17
|
+
* colore; il budget serve per primo il secondo, perché è quello che si legge
|
|
18
|
+
* prima di premere. */
|
|
19
|
+
function statusParts(gate, prompt, model, above, below) {
|
|
20
|
+
const overflow = above + below > 0 ? ` · ↑${above} ↓${below}` : '';
|
|
21
|
+
if (gate.kind === 'ready') {
|
|
22
|
+
return { parts: ['⏎ ', prompt, model ? ` · ${model}` : '', overflow], tone: 'ready' };
|
|
23
|
+
}
|
|
24
|
+
const lead = gate.kind === 'blocked' ? `${WARN} ` : '';
|
|
25
|
+
return { parts: [lead, gate.reason, ' · ⏎ inerte', overflow], tone: gate.kind };
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* La lista dei DLV numerata come la numera `run-task`, coi fatti in grigio e
|
|
29
|
+
* spuntati, più una riga di stato.
|
|
30
|
+
*
|
|
31
|
+
* Il cursore c'è solo quando il gate è `ready`: fuori da lì nessuna voce è
|
|
32
|
+
* selezionabile, e un caret su una riga inerte prometterebbe un gesto che non
|
|
33
|
+
* esiste. Sopra `rows` la lista si finestra sul cursore; senza cursore parte
|
|
34
|
+
* dall'inizio, e la riga di stato dice quante voci restano fuori.
|
|
35
|
+
*/
|
|
36
|
+
export function DlvArea({ dlvs, cursor, selected, gate, prompt, model, rows, width, }) {
|
|
37
|
+
const ready = gate.kind === 'ready';
|
|
38
|
+
const at = ready ? dlvs.findIndex((d) => d.n === cursor) : -1;
|
|
39
|
+
const { start, end } = windowRange(dlvs.length, at, rows);
|
|
40
|
+
const numW = String(dlvs.length).length;
|
|
41
|
+
// caret + marca + spazio + numero + spazio: tutto ciò che precede il testo.
|
|
42
|
+
const textW = Math.max(1, width - CARET_OFF.length - DLV_MARK_W - 1 - numW - 1);
|
|
43
|
+
const status = statusParts(gate, prompt, model, start, dlvs.length - end);
|
|
44
|
+
const [lead, body, tail, overflow] = cutParts(status.parts, width, 1);
|
|
45
|
+
return (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [dlvs.slice(start, end).map((d) => {
|
|
46
|
+
const focused = ready && d.n === cursor;
|
|
47
|
+
const picked = selected.has(d.n);
|
|
48
|
+
const mark = d.done ? DLV_MARK.done : picked ? DLV_MARK.picked : DLV_MARK.open;
|
|
49
|
+
return (_jsxs(Text, { wrap: "truncate-end", children: [focused ? CARET : CARET_OFF, _jsx(Text, { color: picked ? 'cyan' : undefined, dimColor: d.done, children: pad(mark, DLV_MARK_W) }), ' ', _jsx(Text, { dimColor: true, children: pad(String(d.n), numW, 'right') }), ' ', _jsx(Text, { dimColor: d.done, color: picked ? 'cyan' : undefined, bold: focused, children: cut(plain(d.text), textW) })] }, d.n));
|
|
50
|
+
}), _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: status.tone === 'off' ? undefined : 'yellow', dimColor: status.tone === 'off', children: lead }), _jsx(Text, { color: status.tone === 'ready' ? 'cyan' : status.tone === 'blocked' ? 'yellow' : undefined, dimColor: status.tone === 'off', children: body }), _jsx(Text, { dimColor: true, children: tail }), _jsx(Text, { dimColor: true, children: overflow })] })] }));
|
|
51
|
+
}
|
package/dist/ui/screens.js
CHANGED
|
@@ -153,7 +153,7 @@ export function screenFor(input) {
|
|
|
153
153
|
// posizione è lo scroll mosso a mano. Il clamp serve comunque — un resize
|
|
154
154
|
// può accorciare il testo sotto uno scroll già dato.
|
|
155
155
|
const start = Math.min(sheet.top, sheet.maxTop);
|
|
156
|
-
return (_jsx(DetailScreen, { id: sheet.sheet.id, title: sheet.sheet.title, missing: sheet.sheet.text === null, lines: sheet.lines.slice(start, start + sheet.capacity), spans: sheet.doc?.spans ?? [], top: start, total: sheet.lines.length, capacity: sheet.capacity, action: sheet.action, model: sheet.model, spawnNote: sheet.spawnNote, prompt: sheet.prompt, priority: sheet.priority, cursor: sheet.cursor, columns: columns, find: sheet.find, occ: sheet.findRes.occ, occCur: sheet.occCur }));
|
|
156
|
+
return (_jsx(DetailScreen, { id: sheet.sheet.id, title: sheet.sheet.title, missing: sheet.sheet.text === null, lines: sheet.lines.slice(start, start + sheet.capacity), spans: sheet.doc?.spans ?? [], top: start, total: sheet.lines.length, capacity: sheet.capacity, action: sheet.action, model: sheet.model, spawnNote: sheet.spawnNote, prompt: sheet.prompt, priority: sheet.priority, cursor: sheet.cursor, columns: columns, find: sheet.find, occ: sheet.findRes.occ, occCur: sheet.occCur, dlv: sheet.dlv, dlvRows: sheet.dlvRows }));
|
|
157
157
|
}
|
|
158
158
|
// ── T121 · viewer del project status ────────────────────────────────────
|
|
159
159
|
// Quinta schermata sostitutiva, stessa ragione delle altre quattro: un recap
|
package/dist/viewport.js
CHANGED
|
@@ -74,7 +74,7 @@ export const MODAL_HEIGHT = {
|
|
|
74
74
|
// Le quattro righe sono FISSE, non condizionali: la quarta ospita tanto gli
|
|
75
75
|
// scarti del bulk quanto la scelta keep/purge della singola task sporca, e due
|
|
76
76
|
// condizionali mutuamente esclusivi obbligherebbero il budget a sapere QUALE
|
|
77
|
-
// dei due è a schermo (stessa ragione della riga nota in
|
|
77
|
+
// dei due è a schermo (stessa ragione della riga nota in DETAIL_FIELD_ROWS, T111).
|
|
78
78
|
purge: 7,
|
|
79
79
|
// Conferma di eliminazione di una conversazione: marginTop + 2 bordi + 3
|
|
80
80
|
// righe (domanda con l'id, titolo, effetto sul disco + tasti). Nessuna riga
|
|
@@ -278,30 +278,38 @@ const READER_CHROME = 8;
|
|
|
278
278
|
export function readerCapacity(rows) {
|
|
279
279
|
return Math.max(0, (rows || 24) - SLACK - READER_CHROME);
|
|
280
280
|
}
|
|
281
|
-
// T66 — cornice del detail della task.
|
|
281
|
+
// T66 — cornice del detail della task, comune ai due modi (T189).
|
|
282
282
|
// 2 bordi del box esterno
|
|
283
283
|
// 1 titolo "loom-deck"
|
|
284
284
|
// 1 riga meta (id · titolo · posizione nel testo)
|
|
285
285
|
// 1 riga hint
|
|
286
286
|
// 1 marginTop del box testo
|
|
287
287
|
// 2 bordi del box testo
|
|
288
|
-
// 1 marginTop dell'area
|
|
288
|
+
// 1 marginTop dell'area sotto il testo
|
|
289
|
+
//
|
|
290
|
+
// Il detail non può riusare READER_CHROME perché sotto il testo porta un'AREA
|
|
291
|
+
// in più dentro l'overlay, e ogni riga di quell'area va scalata dalla capienza
|
|
292
|
+
// del contenuto o il frame sfonda `rows` (stessa invariante di
|
|
293
|
+
// TASKS_PANE_CHROME).
|
|
294
|
+
const DETAIL_BASE_CHROME = 9;
|
|
295
|
+
// L'area di compilazione: righe FISSE, una per campo.
|
|
289
296
|
// 1 riga azione (T66)
|
|
290
297
|
// 1 riga prompt (T117)
|
|
291
298
|
// 1 riga modello (T108)
|
|
292
299
|
// 1 riga titolo (T111)
|
|
293
300
|
// 1 riga priorità (T158)
|
|
294
301
|
//
|
|
295
|
-
//
|
|
296
|
-
// le cinque righe dell'area di compilazione sono righe FISSE in più dentro
|
|
297
|
-
// l'overlay, e ogni riga fissa aggiunta va scalata dalla capienza del contenuto o
|
|
298
|
-
// il frame sfonda `rows` (stessa invariante di TASKS_PANE_CHROME).
|
|
299
|
-
//
|
|
300
|
-
// Sono fisse e non `extra` condizionali di `detailCapacity` (T111 · D1): due
|
|
302
|
+
// Fisse e non `extra` condizionali dei singoli campi (T111 · D1): due
|
|
301
303
|
// condizionali mutuamente esclusivi obbligherebbero il parametro a dire QUALE è
|
|
302
304
|
// a schermo — un solo booleano per entrambi sottostima quando quello aperto è il
|
|
303
305
|
// più alto, e sommarli toglie righe al testo per un campo che non c'è.
|
|
304
|
-
const
|
|
306
|
+
export const DETAIL_FIELD_ROWS = 5;
|
|
307
|
+
// T189 — il modo DLV mette al posto dei campi la lista dei deliverable più una
|
|
308
|
+
// riga di stato (cosa parte su `⏎`, o perché non parte niente). È l'altezza che
|
|
309
|
+
// `detailCapacity` riceve come `area` quando il modo è attivo: il chiamante
|
|
310
|
+
// dice cosa c'è a schermo con un numero di righe, non con un flag, per la
|
|
311
|
+
// stessa ragione scritta sopra sui condizionali.
|
|
312
|
+
export const DLV_STATUS_ROWS = 1;
|
|
305
313
|
// T91 — la ricerca dentro il detail: marginTop + riga del campo.
|
|
306
314
|
//
|
|
307
315
|
// `MODAL_HEIGHT` dice che il detail costa 0 ai due pane (li sostituisce), e da
|
|
@@ -312,10 +320,25 @@ const DETAIL_CHROME = 14;
|
|
|
312
320
|
// frame nello scrollback a ogni tick del poll.
|
|
313
321
|
const DETAIL_SEARCH_CHROME = 2;
|
|
314
322
|
/** Righe di task file che entrano nel terminale; con la ricerca aperta il campo
|
|
315
|
-
* si paga qui, non altrove.
|
|
316
|
-
|
|
323
|
+
* si paga qui, non altrove. `area` = righe dell'area sotto il testo: i campi
|
|
324
|
+
* per default, `dlvListRows(…) + DLV_STATUS_ROWS` nel modo DLV. */
|
|
325
|
+
export function detailCapacity(rows, searching = false, area = DETAIL_FIELD_ROWS) {
|
|
326
|
+
const extra = searching ? DETAIL_SEARCH_CHROME : 0;
|
|
327
|
+
return Math.max(0, (rows || 24) - SLACK - DETAIL_BASE_CHROME - area - extra);
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* T189 — righe della lista DLV: tutte quelle che ci sono, ma mai più di METÀ
|
|
331
|
+
* dello spazio sotto la cornice. Il resto resta al testo della task, che è ciò
|
|
332
|
+
* per cui il detail si apre; una lista più lunga si finestra sul cursore
|
|
333
|
+
* (`windowRange`) e la riga di stato dice quante righe stanno fuori.
|
|
334
|
+
*
|
|
335
|
+
* Su un terminale basso può valere 0 anche con dei DLV: resta la sola riga di
|
|
336
|
+
* stato, e il frame non sfonda.
|
|
337
|
+
*/
|
|
338
|
+
export function dlvListRows(rows, searching, total) {
|
|
317
339
|
const extra = searching ? DETAIL_SEARCH_CHROME : 0;
|
|
318
|
-
|
|
340
|
+
const free = (rows || 24) - SLACK - DETAIL_BASE_CHROME - DLV_STATUS_ROWS - extra;
|
|
341
|
+
return Math.max(0, Math.min(total, Math.floor(free / 2)));
|
|
319
342
|
}
|
|
320
343
|
// T121 — cornice del viewer del project status. Gemella di READER_CHROME: il
|
|
321
344
|
// viewer scorre testo e basta, senza l'area di compilazione che rende caro il
|
|
@@ -383,7 +406,7 @@ const SPAWN_CHROME = 8;
|
|
|
383
406
|
// si modificano. Un'altezza che dipendesse dalla riga selezionata farebbe
|
|
384
407
|
// saltare la tabella di due righe a ogni movimento del caret, e il budget
|
|
385
408
|
// dovrebbe sapere quale riga è aperta per non sottostimare — lo stesso motivo
|
|
386
|
-
// per cui le cinque righe del detail sono
|
|
409
|
+
// per cui le cinque righe del detail sono fisse in `DETAIL_FIELD_ROWS` invece che
|
|
387
410
|
// condizionali (T111 · D1).
|
|
388
411
|
const SPAWN_FIELDS_CHROME = 5;
|
|
389
412
|
/**
|
package/package.json
CHANGED