@lamemind/loom-deck 0.77.0 → 0.79.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/live-sessions.js +99 -20
- package/dist/overlays/dlv-mode.js +123 -0
- package/dist/overlays/sheet.js +39 -2
- package/dist/project-path.js +18 -11
- package/dist/tab-compose.js +8 -1
- package/dist/tab-launch.js +4 -0
- package/dist/twin-title.js +63 -0
- 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
|
}
|
package/dist/live-sessions.js
CHANGED
|
@@ -2,8 +2,10 @@ import { readFileSync, readdirSync } from 'node:fs';
|
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { basename, join } from 'node:path';
|
|
4
4
|
import { isWithin } from './project-path.js';
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/** La cartella del registry. `LOOM_DECK_LIVE_SESSIONS_ROOT` la sposta, per i
|
|
6
|
+
* test che costruiscono un registro finto invece di leggere quello vero. */
|
|
7
|
+
export function liveSessionsRoot(env = process.env) {
|
|
8
|
+
return env.LOOM_DECK_LIVE_SESSIONS_ROOT || join(homedir(), '.claude', 'sessions');
|
|
7
9
|
}
|
|
8
10
|
/**
|
|
9
11
|
* Campo 22 (`starttime`) di `/proc/<pid>/stat`, o null se la riga non è
|
|
@@ -96,24 +98,16 @@ export function parseLiveEntry(content, pid) {
|
|
|
96
98
|
};
|
|
97
99
|
}
|
|
98
100
|
/**
|
|
99
|
-
* Le
|
|
100
|
-
* la stessa chiave del sidecar `session-tasks.jsonl` e di `deck-run --resume`,
|
|
101
|
-
* quindi il join con la riga di lista è diretto.
|
|
102
|
-
*
|
|
103
|
-
* Il filtro per `cwd` è d'EFFICIENZA, non di correttezza: risparmia lo `stat`
|
|
104
|
-
* di `/proc` sui processi degli altri progetti (il registry è globale, non
|
|
105
|
-
* per-progetto). Il match resta sul `sessionId`.
|
|
101
|
+
* Le entry VIVE del registry, nell'ordine di `readdir`.
|
|
106
102
|
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* mostrerebbe `idle` su una sessione che sta lavorando, cioè il difetto che la
|
|
110
|
-
* colonna esiste per non avere.
|
|
103
|
+
* `keep` scarta un'entry PRIMA del controllo di liveness: chi filtra per
|
|
104
|
+
* progetto risparmia così lo `stat` di `/proc` sui processi degli altri.
|
|
111
105
|
*/
|
|
112
|
-
|
|
113
|
-
const out =
|
|
106
|
+
function liveEntries(root, keep = () => true) {
|
|
107
|
+
const out = [];
|
|
114
108
|
let files;
|
|
115
109
|
try {
|
|
116
|
-
files = readdirSync(
|
|
110
|
+
files = readdirSync(root).filter((f) => f.endsWith('.json'));
|
|
117
111
|
}
|
|
118
112
|
catch {
|
|
119
113
|
return out; // registry assente (CLI mai avviato, o versione che non lo scrive)
|
|
@@ -124,7 +118,7 @@ export function discoverLiveSessions(projectRoot) {
|
|
|
124
118
|
continue;
|
|
125
119
|
let content;
|
|
126
120
|
try {
|
|
127
|
-
content = readFileSync(join(
|
|
121
|
+
content = readFileSync(join(root, f), 'utf8');
|
|
128
122
|
}
|
|
129
123
|
catch {
|
|
130
124
|
continue; // il processo può finire fra readdir e read
|
|
@@ -133,16 +127,101 @@ export function discoverLiveSessions(projectRoot) {
|
|
|
133
127
|
if (!parsed)
|
|
134
128
|
continue;
|
|
135
129
|
const { entry, procStart } = parsed;
|
|
136
|
-
|
|
137
|
-
// e la root del deck può arrivare con la lettera del drive in un altro case.
|
|
138
|
-
if (!isWithin(entry.cwd, projectRoot))
|
|
130
|
+
if (!keep(entry))
|
|
139
131
|
continue;
|
|
140
132
|
if (!isAlive(pid, procStart))
|
|
141
133
|
continue;
|
|
134
|
+
out.push(entry);
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Le sessioni VIVE del solo progetto corrente, indicizzate per `sessionId` —
|
|
140
|
+
* la stessa chiave del sidecar `session-tasks.jsonl` e di `deck-run --resume`,
|
|
141
|
+
* quindi il join con la riga di lista è diretto.
|
|
142
|
+
*
|
|
143
|
+
* Il filtro per `cwd` è d'EFFICIENZA, non di correttezza: risparmia lo `stat`
|
|
144
|
+
* di `/proc` sui processi degli altri progetti (il registry è globale, non
|
|
145
|
+
* per-progetto). Il match resta sul `sessionId`.
|
|
146
|
+
*
|
|
147
|
+
* Nessuna cache mtime-keyed come in `sessions.ts`: le entry sono poche e di
|
|
148
|
+
* poche centinaia di byte, mentre `status` cambia a ogni turno — una cache qui
|
|
149
|
+
* mostrerebbe `idle` su una sessione che sta lavorando, cioè il difetto che la
|
|
150
|
+
* colonna esiste per non avere.
|
|
151
|
+
*/
|
|
152
|
+
export function discoverLiveSessions(projectRoot) {
|
|
153
|
+
const out = new Map();
|
|
154
|
+
// T175 — `isWithin` regge le grafie di Windows: il registry scrive `C:\…`,
|
|
155
|
+
// e la root del deck può arrivare con la lettera del drive in un altro case.
|
|
156
|
+
for (const entry of liveEntries(liveSessionsRoot(), (e) => isWithin(e.cwd, projectRoot))) {
|
|
142
157
|
out.set(entry.sessionId, entry);
|
|
143
158
|
}
|
|
144
159
|
return out;
|
|
145
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Il nome CHIESTO in un argv di `claude` — il valore di `--name` o del suo alias
|
|
163
|
+
* `-n`, staccato o con `=` — oppure `null` se l'argv non ne chiede uno.
|
|
164
|
+
*
|
|
165
|
+
* Non è il campo `name` del registry, ed è la ragione per cui serve: il registry
|
|
166
|
+
* porta il nome EFFETTIVO, e quando il nome chiesto è già preso il CLI rinomina
|
|
167
|
+
* la sessione (`🧵 loom-works` → `🧵 loom-works-cryptic-hopper`, con
|
|
168
|
+
* `nameSource: "collision"`), mentre il titolo della tab resta il nome chiesto.
|
|
169
|
+
*
|
|
170
|
+
* GEMELLA di `procRequestedName` in `loom-compass/compass@lamemind/model.js`:
|
|
171
|
+
* le due letture devono riconoscere le stesse quattro forme.
|
|
172
|
+
*/
|
|
173
|
+
export function requestedNameIn(argv) {
|
|
174
|
+
for (let i = 0; i < argv.length; i++) {
|
|
175
|
+
const a = argv[i];
|
|
176
|
+
if (a === '--name' || a === '-n')
|
|
177
|
+
return argv[i + 1] ?? null;
|
|
178
|
+
if (a.startsWith('--name='))
|
|
179
|
+
return a.slice('--name='.length);
|
|
180
|
+
if (a.startsWith('-n='))
|
|
181
|
+
return a.slice('-n='.length);
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
/** Gli argomenti del processo: `/proc/<pid>/cmdline` li separa con NUL e ne
|
|
186
|
+
* mette uno anche in coda. Ogni argomento arriva intero, spazi compresi. */
|
|
187
|
+
function procArgv(pid) {
|
|
188
|
+
let raw;
|
|
189
|
+
try {
|
|
190
|
+
raw = readFileSync(`/proc/${pid}/cmdline`, 'utf8');
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return null; // processo finito fra la liveness e la lettura
|
|
194
|
+
}
|
|
195
|
+
const argv = raw.split('\0');
|
|
196
|
+
if (argv.length && argv[argv.length - 1] === '')
|
|
197
|
+
argv.pop();
|
|
198
|
+
return argv;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* T182 — i nomi chiesti con `--name` dalle conversazioni vive di TUTTI i
|
|
202
|
+
* progetti: l'ingresso di `twinTitle` (`src/twin-title.ts`).
|
|
203
|
+
*
|
|
204
|
+
* Nessun filtro per `cwd`, al contrario di `discoverLiveSessions`: chi cerca la
|
|
205
|
+
* tab per titolo la cerca in tutte le finestre Ptyxis, quindi una conversazione
|
|
206
|
+
* dello stesso progetto aperta da un worktree di lane (altro `cwd`, stesso
|
|
207
|
+
* titolo) è un gemello a tutti gli effetti. Il titolo porta già la label del
|
|
208
|
+
* progetto, e il confronto per uguaglianza intera non scambia un progetto con
|
|
209
|
+
* un altro.
|
|
210
|
+
*
|
|
211
|
+
* Una viva senza `--name` non entra: il suo titolo di tab non è un nome
|
|
212
|
+
* chiesto. Dove `/proc` non esiste nessuna entry risulta viva, e la lista
|
|
213
|
+
* esce vuota: il lancio usa il nome base.
|
|
214
|
+
*/
|
|
215
|
+
export function liveRequestedNames(root = liveSessionsRoot()) {
|
|
216
|
+
const names = [];
|
|
217
|
+
for (const entry of liveEntries(root)) {
|
|
218
|
+
const argv = procArgv(entry.pid);
|
|
219
|
+
const name = argv ? requestedNameIn(argv) : null;
|
|
220
|
+
if (name !== null)
|
|
221
|
+
names.push(name);
|
|
222
|
+
}
|
|
223
|
+
return names;
|
|
224
|
+
}
|
|
146
225
|
/** Signature del poll: cambia quando cambia l'insieme delle vive O lo stato di
|
|
147
226
|
* una di esse. Il `pid` ci sta dentro perché un `/clear` sposta lo stesso
|
|
148
227
|
* `sessionId` su un processo diverso senza toccare né insieme né stato. */
|
|
@@ -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/tab-compose.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { saneNote } from './sane-note.js';
|
|
16
16
|
import { MODEL_DEFAULT, MODELS } from './spawn-catalog.js';
|
|
17
17
|
import { shellQuote } from './spawn.js';
|
|
18
|
+
import { twinTitle } from './twin-title.js';
|
|
18
19
|
export const PERMISSION_MODES = ['acceptEdits', 'auto', 'bypassPermissions', 'manual', 'dontAsk', 'plan'];
|
|
19
20
|
/** L'alfabeto della chiave di stato: il valore entra in `bash -lc "…"` e il
|
|
20
21
|
* file di progetto è editabile a mano, quindi si scarta invece di quotare. */
|
|
@@ -173,6 +174,12 @@ export function posixQuote(text) {
|
|
|
173
174
|
/**
|
|
174
175
|
* La composizione intera.
|
|
175
176
|
*
|
|
177
|
+
* Il titolo è `tabTitle` più, solo se una conversazione viva ha già chiesto lo
|
|
178
|
+
* stesso nome, il carattere del gemello (`twinTitle`, T182). Il carattere entra
|
|
179
|
+
* nel titolo intero — label, task, nota e fork compresi — perché due
|
|
180
|
+
* conversazioni della stessa task con note diverse hanno già titoli distinti e
|
|
181
|
+
* non sono gemelle. Lo stesso `title` alimenta `-T` di Ptyxis e `--name`.
|
|
182
|
+
*
|
|
176
183
|
* Ordine degli argomenti di `claude`: nome, permission mode e modello prima dei
|
|
177
184
|
* flag di continuità, il prompt sempre ultimo. Il modello sta accanto al
|
|
178
185
|
* permission mode perché sono la stessa cosa — la scelta di chi apre la
|
|
@@ -186,7 +193,7 @@ export function posixQuote(text) {
|
|
|
186
193
|
*/
|
|
187
194
|
export function composeTab(req, ctx) {
|
|
188
195
|
const warnings = [];
|
|
189
|
-
const title = tabTitle(req, projectLabel(ctx.config));
|
|
196
|
+
const title = twinTitle(tabTitle(req, projectLabel(ctx.config)), ctx.liveRequestedNames ?? []);
|
|
190
197
|
const mode = permissionMode(ctx, warnings);
|
|
191
198
|
const model = modelOf(req, ctx, warnings);
|
|
192
199
|
const key = stateKey(ctx, warnings);
|
package/dist/tab-launch.js
CHANGED
|
@@ -21,6 +21,7 @@ import { isUsageError, parseTabArgs } from './tab-args.js';
|
|
|
21
21
|
import { composeTab, ptyxisArgv } from './tab-compose.js';
|
|
22
22
|
import { loadPromptCatalog, PROMPT_CATALOG } from './prompt-catalog.js';
|
|
23
23
|
import { configFilePath } from './config.js';
|
|
24
|
+
import { liveRequestedNames, liveSessionsRoot } from './live-sessions.js';
|
|
24
25
|
import { resolveGitBash } from './git-bash.js';
|
|
25
26
|
import { semicolonArg, TAB_ARGV_ENV, windowsTabEnv, WT_CMD, wtNewTabArgs, wtProfile, } from './windows-terminal.js';
|
|
26
27
|
/** Il wrapper che gira dentro una tab di Windows Terminal, nel build: lo esegue
|
|
@@ -96,6 +97,9 @@ export async function runDeckRun(argv, io) {
|
|
|
96
97
|
catalog: loadPromptCatalog(catalogPath),
|
|
97
98
|
catalogPath,
|
|
98
99
|
platform: io.platform,
|
|
100
|
+
// Su Windows niente `/proc`, quindi nessuna liveness (T177): il titolo
|
|
101
|
+
// resta quello base, cioè il comportamento di prima del gemello.
|
|
102
|
+
liveRequestedNames: io.platform === 'win32' ? [] : liveRequestedNames(liveSessionsRoot(io.env)),
|
|
99
103
|
});
|
|
100
104
|
for (const w of plan.warnings)
|
|
101
105
|
io.stderr(`${w}\n`);
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// T182 — il titolo della conversazione GEMELLA: un carattere in coda al
|
|
2
|
+
// `--name` quando una conversazione viva ha già chiesto lo stesso nome.
|
|
3
|
+
//
|
|
4
|
+
// Chi porta davanti la tab di una conversazione la cerca per titolo (coda via
|
|
5
|
+
// AT-SPI, `sessionsByName` di compass), e il titolo della tab è il `--name`
|
|
6
|
+
// passato a `claude`. Due conversazioni vive con lo stesso nome — tipico: il
|
|
7
|
+
// preflight e il run della stessa task — hanno due tab con lo stesso titolo, e
|
|
8
|
+
// chi cerca non può scegliere. Il lanciatore rende unico il secondo titolo; il
|
|
9
|
+
// primo lancio resta senza carattere, quindi nel caso normale il titolo è quello
|
|
10
|
+
// di sempre.
|
|
11
|
+
//
|
|
12
|
+
// Modulo PURO: i nomi chiesti dalle conversazioni vive arrivano già letti
|
|
13
|
+
// (`liveRequestedNames` in `live-sessions.ts`).
|
|
14
|
+
/**
|
|
15
|
+
* I caratteri del gemello, nell'ordine in cui si assegnano: il primo gemello
|
|
16
|
+
* prende `#`.
|
|
17
|
+
*
|
|
18
|
+
* GEMELLA di `TWIN_MARKS` in `loom-compass/compass@lamemind/model.js`, il
|
|
19
|
+
* secondo lanciatore di conversazioni: nessun codice confronta le due copie, e
|
|
20
|
+
* cambiarne una sola fa scegliere caratteri diversi ai due lanciatori senza
|
|
21
|
+
* errore. Si toccano insieme.
|
|
22
|
+
*
|
|
23
|
+
* Ogni carattere sta fuori da cinque classi, e ognuna rompe un consumer in
|
|
24
|
+
* silenzio:
|
|
25
|
+
* - fuori dall'ASCII a una cella: un simbolo di larghezza ambigua (`§`, `†`)
|
|
26
|
+
* occupa una o due celle a seconda della locale e sposta il taglio del
|
|
27
|
+
* titolo nella tab;
|
|
28
|
+
* - dentro l'alfabeto di `saneNote`: un carattere ammesso lì può arrivare da
|
|
29
|
+
* una nota digitata, e il gemello non si distingue più dal testo;
|
|
30
|
+
* - dentro `[A-Za-z0-9_-]`: il matcher di compass `titleKeyRe` riconosce una
|
|
31
|
+
* finestra con `^<emoji> <name>(?![\w-])`;
|
|
32
|
+
* - l'apice singolo: il titolo sta dentro `claude --name '…'` nella riga che
|
|
33
|
+
* `bash -lc` analizza;
|
|
34
|
+
* - i metacaratteri di shell (`; & $ \` | < >`): inerti fra apici, ma il test
|
|
35
|
+
* di `saneNote` asserisce che nessun `[;&`$]` sopravviva in un titolo, e un
|
|
36
|
+
* gemello della stessa classe renderebbe cieco quel test. Fuori anche la
|
|
37
|
+
* punteggiatura che si legge come testo (`. , : /`) e le parentesi.
|
|
38
|
+
*/
|
|
39
|
+
export const TWIN_MARKS = ['#', '+', '=', '~', '^', '%', '@', '*', '?', '!'];
|
|
40
|
+
/**
|
|
41
|
+
* Il titolo da usare: `base` se nessuna conversazione viva l'ha chiesto,
|
|
42
|
+
* altrimenti `base` + spazio + il primo carattere di `TWIN_MARKS` che nessuna
|
|
43
|
+
* conversazione viva porta già su quel nome.
|
|
44
|
+
*
|
|
45
|
+
* Lo spazio separa il carattere come ogni altro suffisso del titolo (` · T182`,
|
|
46
|
+
* ` <nota>`, ` · fork`): incollato all'ultima parola si leggerebbe come un
|
|
47
|
+
* refuso della nota. Il confronto è per uguaglianza intera, la stessa che fanno
|
|
48
|
+
* coda e `sessionsByName`.
|
|
49
|
+
*
|
|
50
|
+
* Insieme esaurito → `base`: il lancio parte comunque, e chi cerca la tab per
|
|
51
|
+
* titolo torna all'esito ambiguo che ha anche senza lanciatore.
|
|
52
|
+
*/
|
|
53
|
+
export function twinTitle(base, liveRequestedNames) {
|
|
54
|
+
const taken = new Set(liveRequestedNames);
|
|
55
|
+
if (!taken.has(base))
|
|
56
|
+
return base;
|
|
57
|
+
for (const mark of TWIN_MARKS) {
|
|
58
|
+
const candidate = `${base} ${mark}`;
|
|
59
|
+
if (!taken.has(candidate))
|
|
60
|
+
return candidate;
|
|
61
|
+
}
|
|
62
|
+
return base;
|
|
63
|
+
}
|
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