@lamemind/loom-deck 0.43.0 → 0.45.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 +20 -10
- package/dist/cli.js +59 -90
- package/dist/fields.js +108 -0
- package/dist/glyphs.js +22 -0
- package/dist/layout.js +0 -9
- package/dist/model.js +14 -0
- package/dist/overlays/sheet.js +84 -72
- package/dist/prompt-catalog.js +55 -0
- package/dist/spawn.js +26 -4
- package/dist/ui/detail-screen.js +63 -78
- package/dist/ui/fields.js +23 -0
- package/dist/ui/modals.js +9 -16
- package/dist/ui/panes.js +23 -9
- package/dist/view.js +31 -0
- package/dist/viewport.js +14 -15
- package/package.json +1 -1
- package/scripts/deck-run +94 -22
- package/scripts/prompt-catalog +21 -0
package/README.md
CHANGED
|
@@ -92,7 +92,7 @@ Assegnazioni correnti:
|
|
|
92
92
|
| modale | `S` | sort chain |
|
|
93
93
|
| modale | `F` | filtri |
|
|
94
94
|
| modale | `^F` | **ricerca full-text** nelle conversazioni del progetto |
|
|
95
|
-
| modale | `⏎` | **detail** della task selezionata: task file scrollabile +
|
|
95
|
+
| modale | `⏎` | **detail** della task selezionata: task file scrollabile + area di compilazione a quattro righe |
|
|
96
96
|
| modale | `CANC` | **elimina** — la task selezionata; su una riga meta della vista `archiviabili`, l'intero insieme |
|
|
97
97
|
| immediata | `^K` | spawna la task selezionata col prompt di **recap** |
|
|
98
98
|
| immediata | `^P` | spawna la task selezionata sul **preflight** |
|
|
@@ -161,19 +161,26 @@ Col focus sul pane Sessions né `⏎` né i `CTRL` spawnano: l'oggetto dell'azio
|
|
|
161
161
|
|
|
162
162
|
### `⏎` — il detail della task
|
|
163
163
|
|
|
164
|
-
Overlay fullscreen a due zone: il **task file per intero**, scrollabile
|
|
165
|
-
(`↑↓` riga, `PgUp`/`PgDn` pagina, `g`/`G` estremi), e una riga di **bottoni**
|
|
166
|
-
navigabile con `←→`, con `open` a fuoco all'apertura. `⏎` esegue l'azione
|
|
167
|
-
selezionata e chiude; `esc` chiude lasciando la lista con la stessa selezione.
|
|
164
|
+
Overlay fullscreen a due zone: il **task file per intero**, scrollabile con `PgUp`/`PgDn`, e sotto un'**area di compilazione** a quattro righe — i quattro parametri con cui la sessione sta per nascere.
|
|
168
165
|
|
|
169
|
-
|
|
166
|
+
| Riga | Cosa | Come si cambia |
|
|
167
|
+
|---|---|---|
|
|
168
|
+
| `azione` | `open` `preflight` `run` `status` `checkpoint` | `←→`, oppure l'iniziale: `o` `p` `r` `s` `c` |
|
|
169
|
+
| `prompt` | il testo che riceverà la sessione | si scrive; parte pre-riempito dall'azione scelta |
|
|
170
|
+
| `modello` | `fable` `opus` `sonnet` `haiku` | `←→` |
|
|
171
|
+
| `titolo` | il nome della conversazione, in lista e nella tab | si scrive |
|
|
172
|
+
|
|
173
|
+
`↑↓` spostano il fuoco fra le righe, `⏎` spawna coi quattro valori correnti, `esc` chiude lasciando la lista con la stessa selezione. Le righe di testo ricevono i caratteri **solo quando sono in fuoco**: su `azione` una lettera che non è un'iniziale resta inerte invece di finire in un campo.
|
|
174
|
+
|
|
175
|
+
`↑↓` sono quindi una risorsa contesa, e vanno al fuoco: la lettura del task file resta su `PgUp`/`PgDn`, cioè una granularità sola invece di due. È il prezzo di avere `↑↓` nel loro significato di sempre su una schermata che ospita insieme un documento e dei campi.
|
|
170
176
|
|
|
171
|
-
|
|
177
|
+
**Il campo `prompt` è quello che parte davvero**, non un'anteprima: modificarlo cambia il messaggio che la sessione riceve. Cambiare azione lo riscrive col default della nuova — col fuoco per riga il cambio accidentale non esiste (`←→` toccano l'azione solo dalla sua riga), quindi non c'è nessuna modifica da proteggere.
|
|
172
178
|
|
|
173
|
-
|
|
174
|
-
ramo `useInput` della modalità chiude prima di arrivare a quello di default. Ink non ha focus-trap: la cattura **è** l'ordine dei rami, e cambiare cosa fa un tasto fuori dal detail non tocca cosa fa dentro.
|
|
179
|
+
La grammatica delle righe è la **stessa** del modale edit (`SHIFT+E`) e sta in `src/fields.ts`: descrittore delle righe, fuoco, caret, `^A`/`^E`/`^D`/`^U`. Finché era scritta due volte andava tenuta allineata a mano a ogni tasto aggiunto. `Home`/`End` non ci sono e non possono esserci: `useInput` non le espone — verificato su otto sequenze diverse, arrivano tutte come input vuoto senza flag — da cui `^A`/`^E`.
|
|
175
180
|
|
|
176
|
-
|
|
181
|
+
Le azioni non sono un catalogo nuovo: chiamano lo **stesso** percorso di spawn dei tasti diretti. Una superficie in più, zero percorsi di spawn in più. Sono bottoni affiancati e non voci di un menu verticale perché è la forma che sopravvive all'arrivo del mouse: un rettangolo ha già coordinate e area cliccabile, una lista di voci andrebbe rifatta.
|
|
182
|
+
|
|
183
|
+
Il **testo** dei prompt vive in `scripts/prompt-catalog`, un file dati sibling di `deck-run` che entrambi leggono a runtime: il deck per pre-riempire il campo, `deck-run` per comporre il comando in-tab. Come codice posseduto da un lato solo il deck avrebbe dovuto riscriverlo per mostrarlo, e le due copie divergerebbero alla prima voce aggiunta da una parte sola. Un prompt modificato a mano viaggia poi letterale (`--prompt`), perché a quel punto nessun `--prompt-kind` lo descrive più.
|
|
177
184
|
|
|
178
185
|
### `f` — forkare una conversazione
|
|
179
186
|
|
|
@@ -301,10 +308,13 @@ npx @lamemind/loom-deck
|
|
|
301
308
|
# dalla project dir con un tasks.md
|
|
302
309
|
scripts/deck-run T18
|
|
303
310
|
scripts/deck-run T18 --prompt-kind none|recap|preflight|run|checkpoint
|
|
311
|
+
scripts/deck-run T18 --prompt 'testo letterale del prompt'
|
|
304
312
|
```
|
|
305
313
|
|
|
306
314
|
Apre una tab Ptyxis nella window attiva con `LOOM_TASK=T18 claude 'recap stato task T18'`. Il prompt iniziale è il terzo asse dello spawn, ortogonale al binding task (`<TaskID>` vs `--no-task`) e alla continuità (nuova vs `--resume`/`--fork`): senza flag resta `recap`, `none` apre la sessione bound **senza** alcun prompt. `LOOM_DECK_ENTER_PROMPT` (placeholder `{TASK}`) resta un override che vince sul kind — tranne su `none`, che è una richiesta esplicita di non averne.
|
|
307
315
|
|
|
316
|
+
I testi del catalogo stanno in `scripts/prompt-catalog` (formato `kind<TAB>template`, `{TASK}` interpolato), non dentro lo script: li legge anche il deck, per mostrare nel detail il prompt prima dello spawn. `--prompt` passa invece un testo **letterale** ed è esclusivo con `--prompt-kind` — sono due modi di dire la stessa cosa, e accettarli insieme costringerebbe a stabilire quale vince. Il testo viene quotato per la shell in-tab, apici singoli compresi: è l'unico ingresso di testo libero in una riga che una shell parsa.
|
|
317
|
+
|
|
308
318
|
La tab porta anche `PTYXIS_PROFILE` forzata al profilo bindato al progetto nel registry (`bindings/claude`), letto da dconf: è la chiave con cui loom-compass associa a un progetto lo stato annunciato dagli hook (running/ask/done). Una tab `ptyxis --tab` nuda erediterebbe il profilo di default, e l'annuncio finirebbe keyed su un UUID che nessun progetto dichiara — pallino fermo su idle, senza alcun errore visibile. Override o disattivazione via `LOOM_DECK_STATE_PROFILE`
|
|
309
319
|
(settata a vuoto → nessun annuncio); progetto non registrato → nessun prefisso.
|
|
310
320
|
|
package/dist/cli.js
CHANGED
|
@@ -11,12 +11,13 @@ import { cellWidth, launchLegend, loadArchivableDays, loadIdentity, loadLaunch,
|
|
|
11
11
|
import { cycleSessionView, cycleTaskView, selectSessionRows, selectTasks, sessionView, taskView, TASK_VIEWS, } from './pane-views.js';
|
|
12
12
|
import { isCompact, layoutBudget, searchPreviewCapacity, windowRange, } from './viewport.js';
|
|
13
13
|
import { cut, cutMiddle, sanitize, termWidth } from './width.js';
|
|
14
|
-
import { applyView, cycleSort, describeSort, priName, progName, toggleHidden, PRI_ENTRIES, PROG_ENTRIES, } from './view.js';
|
|
14
|
+
import { applyView, cycleSort, describeSort, idColumnWidth, priName, progName, toggleHidden, PRI_ENTRIES, PROG_ENTRIES, } from './view.js';
|
|
15
15
|
import { initialDetail, writeTaskEdit, PRI_GLYPH, PRI_LABEL } from './task-edit.js';
|
|
16
|
-
import { ALL, EDIT_PRI, EDIT_PROG, MAX_SESSIONS, MAX_SESSIONS_ALL, META_ROWS, QUIT_WINDOW_MS, ROW_ALL, ROW_SPOT, SORT_TASTI, SPOT, } from './model.js';
|
|
16
|
+
import { ALL, EDIT_FIELDS, EDIT_PRI, EDIT_PROG, editTextField, MAX_SESSIONS, MAX_SESSIONS_ALL, META_ROWS, QUIT_WINDOW_MS, ROW_ALL, ROW_SPOT, SORT_TASTI, SPOT, } from './model.js';
|
|
17
17
|
import { purgeTargets, splitTargets } from './purge.js';
|
|
18
|
-
import { conversationLabel,
|
|
19
|
-
import {
|
|
18
|
+
import { conversationLabel, isDone } from './layout.js';
|
|
19
|
+
import { fieldsKey } from './fields.js';
|
|
20
|
+
import { TASK_EMPTY, relTime, sanitizeTyped, taskTail } from './glyphs.js';
|
|
20
21
|
import { commitTaskEdit, runLaunch, spawnClaudeEmpty, spawnCleanTasks, spawnCreateTask, spawnDeck, spawnDeckFork, spawnDeckResume, spawnTerminal, onInTabCommand, CLAUDE_CMD, DECK_RUN, MODEL_DEFAULT, } from './spawn.js';
|
|
21
22
|
import { EditModal, FilterModal, PurgeModal, SortModal, idList } from './ui/modals.js';
|
|
22
23
|
import { ReaderScreen, SearchScreen } from './ui/search-screen.js';
|
|
@@ -79,9 +80,10 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
79
80
|
const [taskViewId, setTaskViewId] = useState('tasks');
|
|
80
81
|
const [sessionViewId, setSessionViewId] = useState('context');
|
|
81
82
|
const [filterCursor, setFilterCursor] = useState({ row: 0, col: 0 });
|
|
82
|
-
// T41 — bozza dell'edit (null fuori dal modale) e
|
|
83
|
+
// T41 — bozza dell'edit (null fuori dal modale) e posizione nell'area di
|
|
84
|
+
// compilazione: riga in fuoco + caret dentro la riga di testo attiva.
|
|
83
85
|
const [edit, setEdit] = useState(null);
|
|
84
|
-
const [
|
|
86
|
+
const [editCursor, setEditCursor] = useState({ row: 0, caret: 0 });
|
|
85
87
|
// T112 — bozza della conferma di eliminazione (null fuori dal modale).
|
|
86
88
|
const [purge, setPurge] = useState(null);
|
|
87
89
|
// T116 — uscita a doppio `^C`. Il timer È lo stato dell'armamento: finché il
|
|
@@ -169,6 +171,19 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
169
171
|
else
|
|
170
172
|
spotCount++;
|
|
171
173
|
}
|
|
174
|
+
// T118 — colonne fisse della lista task, misurate su `paneTasks` (la vista
|
|
175
|
+
// attiva INTERA) e non sulla finestra visibile: gemelle di `sessionCols` e
|
|
176
|
+
// per la stessa ragione, che una larghezza derivata dallo schermo si muove a
|
|
177
|
+
// ogni scroll. La coda porta dentro il proprio gutter, così l'allineamento a
|
|
178
|
+
// destra lo produce `pad` da sé; nessuna riga con qualcosa da scrivere →
|
|
179
|
+
// colonna spenta a `0`, e le descrizioni si riprendono lo spazio.
|
|
180
|
+
const taskCols = (() => {
|
|
181
|
+
let tail = 0;
|
|
182
|
+
for (const t of paneTasks) {
|
|
183
|
+
tail = Math.max(tail, termWidth(taskTail(childCount.get(t.id) ?? 0, dirtyFolders.has(t.id))));
|
|
184
|
+
}
|
|
185
|
+
return { id: idColumnWidth(paneTasks), tail: tail > 0 ? tail + 1 : 0 };
|
|
186
|
+
})();
|
|
172
187
|
// Figli della selezione: tutte le conversazioni del progetto (`≡ tutte`), le
|
|
173
188
|
// sessioni bound alla task selezionata, oppure (spot) quelle senza binding.
|
|
174
189
|
// sessions è già ts desc → l'ordine si eredita in tutti e tre i rami.
|
|
@@ -374,12 +389,16 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
374
389
|
// il tempo di un tick la riga in lista comparirebbe nuda. Due record separati
|
|
375
390
|
// sullo stesso `sessionId` sono la forma normale di un file append-only
|
|
376
391
|
// last-wins, non una scrittura da fondere.
|
|
377
|
-
|
|
392
|
+
// T117 — `prompt` è il testo LETTERALE quando lo spawn arriva dal detail, dove
|
|
393
|
+
// il campo è editabile: quello che l'utente legge è quello che parte. Assente
|
|
394
|
+
// per gli acceleratori della lista, che non hanno un campo da cui prenderlo e
|
|
395
|
+
// viaggiano col simbolo.
|
|
396
|
+
function spawnForTask(id, kind, model, spawnNote = '', prompt) {
|
|
378
397
|
const sid = randomUUID();
|
|
379
398
|
appendTaskBinding(cwd, sid, id);
|
|
380
399
|
if (spawnNote)
|
|
381
400
|
appendNote(cwd, sid, spawnNote);
|
|
382
|
-
const spawned = spawnDeck(id, cwd, sid, kind, model, spawnNote);
|
|
401
|
+
const spawned = spawnDeck(id, cwd, sid, kind, model, spawnNote, prompt);
|
|
383
402
|
spawned.child.on('error', () => setNote(`⚠ spawn ${id} fallito (${DECK_RUN})`));
|
|
384
403
|
// Il modello resta SEMPRE visibile anche quando è il default, perché è un
|
|
385
404
|
// argomento esplicito del comando (T108): gli acceleratori della lista non
|
|
@@ -422,8 +441,8 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
422
441
|
appendNote(cwd, sid, text);
|
|
423
442
|
reloadSessions();
|
|
424
443
|
setNote(text
|
|
425
|
-
? `✎
|
|
426
|
-
: `✎
|
|
444
|
+
? `✎ titolo su ${sid.slice(0, 8)}: "${cut(text, 40)}"`
|
|
445
|
+
: `✎ titolo rimosso da ${sid.slice(0, 8)}`);
|
|
427
446
|
}
|
|
428
447
|
// T41 — apertura dell'edit: la bozza parte dai valori ATTUALI della task, non
|
|
429
448
|
// da default. La priorità arriva dal glifo di tasks.md (già in `selTask`), lo
|
|
@@ -444,11 +463,10 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
444
463
|
prog,
|
|
445
464
|
detail: initialDetail(detail?.fields['Progress'] ?? '', prog),
|
|
446
465
|
title: selTask.rawDesc,
|
|
447
|
-
// Si apre sulla riga 0 (priorità), che non è un campo di testo: il caret
|
|
448
|
-
// prende la sua posizione entrando in una riga di testo con ↑↓.
|
|
449
|
-
caret: 0,
|
|
450
466
|
});
|
|
451
|
-
|
|
467
|
+
// Si apre sulla riga 0 (priorità), che non è un campo di testo: il caret
|
|
468
|
+
// prende la sua posizione entrando in una riga di testo con ↑↓.
|
|
469
|
+
setEditCursor({ row: 0, caret: 0 });
|
|
452
470
|
setNote('');
|
|
453
471
|
setMode('edit');
|
|
454
472
|
}
|
|
@@ -713,7 +731,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
713
731
|
columns,
|
|
714
732
|
setMode,
|
|
715
733
|
setNote,
|
|
716
|
-
onAction: (id, kind, model, spawnNote) => spawnForTask(id, kind, model, spawnNote),
|
|
734
|
+
onAction: (id, kind, model, spawnNote, prompt) => spawnForTask(id, kind, model, spawnNote, prompt),
|
|
717
735
|
});
|
|
718
736
|
const search = useSearchOverlay({
|
|
719
737
|
sessions,
|
|
@@ -744,7 +762,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
744
762
|
if (key.escape) {
|
|
745
763
|
setMode('normal');
|
|
746
764
|
setNoteDraft('');
|
|
747
|
-
setNote('N →
|
|
765
|
+
setNote('N → titolo annullato');
|
|
748
766
|
}
|
|
749
767
|
else if (key.return) {
|
|
750
768
|
submitNote();
|
|
@@ -822,84 +840,31 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
822
840
|
: { ...v, hiddenProg: toggleHidden(v.hiddenProg, PROG_ENTRIES[col].name) });
|
|
823
841
|
}
|
|
824
842
|
}
|
|
843
|
+
// Il ponte fra le quattro righe del modale e la bozza. I valori restano
|
|
844
|
+
// tipizzati (`PriName`, `ProgName`) invece di finire in un record generico:
|
|
845
|
+
// `fields.ts` governa la grammatica dei tasti, non il modello dati.
|
|
846
|
+
const editIO = {
|
|
847
|
+
text: (row) => edit?.[editTextField(row)] ?? '',
|
|
848
|
+
setText: (row, next) => setEdit((e) => (e ? { ...e, [editTextField(row)]: next } : e)),
|
|
849
|
+
choice: (row) => row === 0
|
|
850
|
+
? Math.max(0, EDIT_PRI.indexOf(edit?.pri ?? 'med'))
|
|
851
|
+
: Math.max(0, EDIT_PROG.indexOf(edit?.prog ?? 'todo')),
|
|
852
|
+
setChoice: (row, index) => setEdit((e) => e ? (row === 0 ? { ...e, pri: EDIT_PRI[index] } : { ...e, prog: EDIT_PROG[index] }) : e),
|
|
853
|
+
};
|
|
825
854
|
function onEditKey(input, key) {
|
|
826
855
|
if (key.escape) {
|
|
827
856
|
setMode('normal');
|
|
828
857
|
setEdit(null);
|
|
829
858
|
setNote('E → edit annullato');
|
|
859
|
+
return;
|
|
830
860
|
}
|
|
831
|
-
|
|
861
|
+
if (key.return) {
|
|
832
862
|
submitEdit();
|
|
863
|
+
return;
|
|
833
864
|
}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
// Il caret segue la riga attiva e atterra in CODA al nuovo campo: è la
|
|
838
|
-
// posizione da cui si continua a scrivere, ed è anche l'unica che non
|
|
839
|
-
// dipende da dove stava il cursore nel campo precedente.
|
|
840
|
-
setEdit((e) => (e && isTextRow(next) ? { ...e, caret: cpLen(e[editField(next)]) } : e));
|
|
841
|
-
}
|
|
842
|
-
else if ((key.leftArrow || key.rightArrow) && !isTextRow(editRow)) {
|
|
843
|
-
const d = key.leftArrow ? -1 : 1;
|
|
844
|
-
// Scorrimento CICLICO (wrap) e non clampato: le liste sono di 3-4 voci,
|
|
845
|
-
// arrivare in fondo e ripartire costa meno di invertire direzione.
|
|
846
|
-
if (editRow === 0) {
|
|
847
|
-
setEdit((e) => e ? { ...e, pri: EDIT_PRI[(EDIT_PRI.indexOf(e.pri) + d + EDIT_PRI.length) % EDIT_PRI.length] } : e);
|
|
848
|
-
}
|
|
849
|
-
else if (editRow === 1) {
|
|
850
|
-
setEdit((e) => e
|
|
851
|
-
? { ...e, prog: EDIT_PROG[(EDIT_PROG.indexOf(e.prog) + d + EDIT_PROG.length) % EDIT_PROG.length] }
|
|
852
|
-
: e);
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
else if (isTextRow(editRow)) {
|
|
856
|
-
// Un solo ramo per i due campi di testo, la riga sceglie la chiave:
|
|
857
|
-
// duplicarlo significherebbe tenere allineate a mano due copie della
|
|
858
|
-
// stessa grammatica di input a ogni tasto aggiunto.
|
|
859
|
-
const field = editField(editRow);
|
|
860
|
-
if (key.ctrl) {
|
|
861
|
-
// T54 — ramo CTRL ANTEPOSTO a quelli su carattere, come in modalità
|
|
862
|
-
// normale: `^A` e `a` arrivano con lo stesso `input`, quindi senza
|
|
863
|
-
// questa precedenza il `^A` finirebbe dentro il testo.
|
|
864
|
-
//
|
|
865
|
-
// `^A`/`^E` (convenzione readline) perché `Home`/`End` NON sono
|
|
866
|
-
// esposte da `useInput`: arrivano come input vuoto, indistinguibili
|
|
867
|
-
// da qualunque altro tasto senza nome.
|
|
868
|
-
//
|
|
869
|
-
// `^D` è il delete-forward, e anche qui il motivo è un limite di Ink:
|
|
870
|
-
// il tasto Backspace fisico manda `\x7f` e il tasto Canc manda
|
|
871
|
-
// `\x1b[3~`, ma `parseKeypress` li battezza ENTRAMBI `delete` e
|
|
872
|
-
// svuota `input` — a valle sono lo stesso evento. `key.delete` va
|
|
873
|
-
// quindi al backspace (il tasto che si usa davvero) e la
|
|
874
|
-
// cancellazione in avanti prende il suo tasto readline.
|
|
875
|
-
if (input === 'a')
|
|
876
|
-
setEdit((e) => (e ? { ...e, caret: 0 } : e));
|
|
877
|
-
else if (input === 'e')
|
|
878
|
-
setEdit((e) => (e ? { ...e, caret: cpLen(e[field]) } : e));
|
|
879
|
-
else if (input === 'd')
|
|
880
|
-
setEdit((e) => (e ? { ...e, [field]: removeAt(e[field], e.caret) } : e));
|
|
881
|
-
}
|
|
882
|
-
else if (key.leftArrow || key.rightArrow) {
|
|
883
|
-
// CLAMP agli estremi, non wrap: a inizio campo `←` non deve saltare in
|
|
884
|
-
// fondo. Le liste di valori (righe 0/1) ciclano perché sono 3-4 voci;
|
|
885
|
-
// un testo no — il salto sarebbe indistinguibile da uno sfarfallio.
|
|
886
|
-
const d = key.leftArrow ? -1 : 1;
|
|
887
|
-
setEdit((e) => e ? { ...e, caret: Math.max(0, Math.min(cpLen(e[field]), e.caret + d)) } : e);
|
|
888
|
-
}
|
|
889
|
-
else if (key.backspace || key.delete) {
|
|
890
|
-
setEdit((e) => e ? { ...e, [field]: removeAt(e[field], e.caret - 1), caret: Math.max(0, e.caret - 1) } : e);
|
|
891
|
-
}
|
|
892
|
-
else if (input && !key.meta) {
|
|
893
|
-
// `sanitizeTyped`: `useInput` consegna il CHUNK di stdin, quindi un
|
|
894
|
-
// incollaggio porta dentro newline e byte di controllo — invisibili
|
|
895
|
-
// nel campo ma contati da Ink nella larghezza della riga, e destinati
|
|
896
|
-
// a finire tali e quali dentro tasks.md. Ed è per lo stesso motivo che
|
|
897
|
-
// il caret avanza della LUNGHEZZA del chunk, non di uno: un incollaggio
|
|
898
|
-
// entra tutto insieme.
|
|
899
|
-
const ins = sanitizeTyped(input);
|
|
900
|
-
setEdit((e) => e ? { ...e, [field]: insertAt(e[field], e.caret, ins), caret: e.caret + cpLen(ins) } : e);
|
|
901
|
-
}
|
|
902
|
-
}
|
|
865
|
+
// Tutto il resto è dell'area di compilazione, che è la STESSA del detail
|
|
866
|
+
// (T117): una grammatica sola, non due copie da tenere allineate a mano.
|
|
867
|
+
fieldsKey(input, key, EDIT_FIELDS, editCursor, setEditCursor, editIO);
|
|
903
868
|
}
|
|
904
869
|
/**
|
|
905
870
|
* T116 — `^C`: la prima pressione ARMA, la seconda entro la finestra chiude.
|
|
@@ -1174,7 +1139,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1174
1139
|
// su una pinnata STALE, perché annotare «questa non c'è più, era X» è
|
|
1175
1140
|
// proprio il caso in cui una nota serve.
|
|
1176
1141
|
if (focus !== 'sessions') {
|
|
1177
|
-
setNote('N →
|
|
1142
|
+
setNote('N → titolo: seleziona una sessione (→ per il pane)');
|
|
1178
1143
|
}
|
|
1179
1144
|
else if (!selSessionId) {
|
|
1180
1145
|
setNote('N → nessuna sessione da annotare');
|
|
@@ -1281,7 +1246,11 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1281
1246
|
? [purgeBulk ? 'CANC elimina tutte' : 'CANC elimina']
|
|
1282
1247
|
: []),
|
|
1283
1248
|
...(canResume ? ['f fork'] : []),
|
|
1284
|
-
|
|
1249
|
+
// T117 · D3 — «titolo» anche qui: è lo stesso valore che il detail chiede
|
|
1250
|
+
// allo spawn, e due nomi per una cosa sola li paga chi legge le due
|
|
1251
|
+
// schermate. Rename di sola ETICHETTA: il dato resta `note` nel sidecar e
|
|
1252
|
+
// `--title-note` in deck-run, dove rinominarlo sarebbe un breaking.
|
|
1253
|
+
...(canPin ? ['p pin', 'N titolo', 'A assegna'] : []),
|
|
1285
1254
|
'^F cerca',
|
|
1286
1255
|
'C nuova',
|
|
1287
1256
|
'E edit',
|
|
@@ -1321,7 +1290,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1321
1290
|
// posizione è lo scroll mosso a mano. Il clamp serve comunque — un resize
|
|
1322
1291
|
// può accorciare il testo sotto uno scroll già dato.
|
|
1323
1292
|
const start = Math.min(sheet.top, sheet.maxTop);
|
|
1324
|
-
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, columns: columns, find: sheet.find, occ: sheet.findRes.occ, occCur: sheet.occCur }));
|
|
1293
|
+
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, cursor: sheet.cursor, columns: columns, find: sheet.find, occ: sheet.findRes.occ, occCur: sheet.occCur }));
|
|
1325
1294
|
}
|
|
1326
1295
|
// ── T52 · schermate sostitutive ─────────────────────────────────────────
|
|
1327
1296
|
// Ricerca e reader sono gli unici modali che NON stanno in flusso sopra i
|
|
@@ -1412,7 +1381,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1412
1381
|
if (budget.compact) {
|
|
1413
1382
|
return (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), _jsxs(Text, { dimColor: true, children: [' ', "v", VERSION, " \u00B7 ", viewTasks.length, " task \u00B7 sel ", selectedTaskId ?? parentLabel, " \u00B7 terminale ", rows, "\u00D7", columns, ": troppo basso, allarga"] })] }));
|
|
1414
1383
|
}
|
|
1415
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), _jsxs(Text, { dimColor: true, children: ["v", VERSION] })] }), mode === 'create' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["nuova task \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " crea \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'sort' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["sort \u00B7 ", _jsx(Text, { color: "yellow", children: "p" }), " pri ", _jsx(Text, { color: "yellow", children: "s" }), " stato", ' ', _jsx(Text, { color: "yellow", children: "i" }), " id (asc\u2192desc\u2192off) \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " ok \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'filter' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["filtri \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2191\u2193\u2190\u2192" }), " naviga \u00B7 ", _jsx(Text, { color: "yellow", children: "spazio" }), ' ', "mostra/nascondi \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " ok \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'note' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["
|
|
1384
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), _jsxs(Text, { dimColor: true, children: ["v", VERSION] })] }), mode === 'create' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["nuova task \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " crea \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'sort' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["sort \u00B7 ", _jsx(Text, { color: "yellow", children: "p" }), " pri ", _jsx(Text, { color: "yellow", children: "s" }), " stato", ' ', _jsx(Text, { color: "yellow", children: "i" }), " id (asc\u2192desc\u2192off) \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " ok \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'filter' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["filtri \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2191\u2193\u2190\u2192" }), " naviga \u00B7 ", _jsx(Text, { color: "yellow", children: "spazio" }), ' ', "mostra/nascondi \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " ok \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'note' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["titolo conversazione \u00B7 ", _jsx(Text, { color: "yellow", children: "^U" }), " svuota \u00B7", ' ', _jsx(Text, { color: "yellow", children: "\u23CE" }), " salva (vuoto = rimuove) \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'purge' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["elimina task \u00B7", ' ', purge?.ignored ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " keep/purge dei file non tracciati \u00B7", ' '] })) : null, _jsx(Text, { color: "yellow", children: "\u23CE" }), " conferma \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'edit' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["edit \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2191\u2193" }), " campo \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " valore, o cursore sul testo \u00B7 ", _jsx(Text, { color: "yellow", children: "^A/^E" }), " inizio/fine \u00B7", ' ', _jsx(Text, { color: "yellow", children: "^D" }), " canc \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " salva+commit \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : (_jsx(Text, { dimColor: true, wrap: "truncate-end", children: keyLegend })), mode === 'normal' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [surfaceLegend, legend.shown ? ` · ${legend.shown}` : '', legend.overflow > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 +", legend.overflow, " fuori riga"] })) : null, legend.unreachable > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 ", legend.unreachable, " oltre la 9\u00AA (non raggiungibili)"] })) : null] })) : null, mode === 'create' ? (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: "yellow", children: "C \u203A " }), _jsx(Text, { children: draft }), _jsx(Text, { inverse: true, children: " " })] })) : null, mode === 'note' ? (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: "yellow", children: "\u270E \u203A " }), _jsx(Text, { children: noteDraft }), _jsx(Text, { inverse: true, children: " " })] })) : null, mode === 'sort' ? _jsx(SortModal, { sort: view.sort }) : null, mode === 'filter' ? _jsx(FilterModal, { view: view, cursor: filterCursor }) : null, mode === 'edit' && edit && selTask ? (_jsx(EditModal, { id: selTask.id, draft: edit, row: editCursor.row, caret: editCursor.caret, columns: columns })) : null, mode === 'purge' && purge ? _jsx(PurgeModal, { draft: purge, columns: columns }) : null, _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(TasksPane, { tasks: windowTasks, counts: taskCounts, activeView: taskViewId, paneCount: paneTasks.length, view: view, selected: selIndex, spotCount: spotCount, allCount: sessions.length, childCount: childCount, idW: taskCols.id, tailW: taskCols.tail, focused: focus === 'tasks', loadError: loadError, windowStart: taskWin.start, above: taskWin.start, below: paneTasks.length - taskWin.end, columns: columns, dirty: dirtyFolders }), _jsx(SessionsPane, { parentLabel: parentLabel, isSpot: isSpot, isAll: isAll, bindings: bindings, taskW: sessionCols.task, ageW: sessionCols.age, rows: windowRows, counts: sessionCounts, activeView: sessionViewId, paneCount: sessionRows.length, selectedId: selSessionId ?? undefined, focused: focus === 'sessions', above: sessionWin.start, below: sessionRows.length - sessionWin.end, columns: columns, forkOf: forkOf, sessionNotes: sessionNotes, projectCore: projectCore, live: live })] }), budget.preview && previewKind === 'task' && detail ? (_jsx(PreviewPane, { kind: "task", detail: detail, maxLines: budget.detailLines, columns: columns })) : budget.preview && previewKind === 'session' && selSessionObj ? (_jsx(PreviewPane, { kind: "session", s: selSessionObj, firstLines: budget.sessionFirstLines, lastLines: budget.sessionLastLines, columns: columns, origin: forkOf.get(selSessionObj.sessionId) ?? null, note: sessionNotes.get(selSessionObj.sessionId) ?? '', live: live.get(selSessionObj.sessionId) ?? null })) : null, note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: sanitize(note) }) : null] }));
|
|
1416
1385
|
}
|
|
1417
1386
|
const cwd = process.cwd();
|
|
1418
1387
|
// T116 — `exitOnCtrlC: false` toglie a Ink l'uscita immediata su `^C`: senza,
|
package/dist/fields.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { cpLen, insertAt, removeAt } from './layout.js';
|
|
2
|
+
import { sanitizeTyped } from './glyphs.js';
|
|
3
|
+
/** La riga `row` è un campo di testo (e quindi mangia le lettere). */
|
|
4
|
+
export function isTextField(specs, row) {
|
|
5
|
+
return specs[row]?.kind === 'text';
|
|
6
|
+
}
|
|
7
|
+
/** Caret in CODA al campo della riga `row`: la posizione da cui si continua a
|
|
8
|
+
* scrivere, e l'unica che non dipende da dove stava il cursore prima. Su una
|
|
9
|
+
* riga a scelta il caret non ha significato e vale 0. */
|
|
10
|
+
export function caretAtEnd(specs, row, io) {
|
|
11
|
+
return isTextField(specs, row) ? cpLen(io.text(row)) : 0;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Un tasto dell'area di compilazione. Ritorna `true` se lo ha consumato.
|
|
15
|
+
*
|
|
16
|
+
* Il chiamante gestisce PRIMA i tasti che sono suoi e non dell'area (`esc`,
|
|
17
|
+
* `⏎`, `^F`, `PgUp`/`PgDn`), poi delega qui; ciò che torna `false` è inerte, e
|
|
18
|
+
* lasciarlo cadere è la scelta giusta dentro un modo capturing.
|
|
19
|
+
*
|
|
20
|
+
* Il ramo CTRL è ANTEPOSTO a quelli su carattere, come in modalità normale: `^A`
|
|
21
|
+
* e `a` arrivano con lo stesso `input`, e `key.ctrl` è l'unico discriminante —
|
|
22
|
+
* senza la precedenza il `^A` finirebbe dentro il testo.
|
|
23
|
+
*/
|
|
24
|
+
export function fieldsKey(input, key, specs, cursor, setCursor, io) {
|
|
25
|
+
const spec = specs[cursor.row];
|
|
26
|
+
if (!spec)
|
|
27
|
+
return false;
|
|
28
|
+
if (key.upArrow || key.downArrow) {
|
|
29
|
+
// Ciclico: le aree sono di 4 righe, arrivare in fondo e ripartire costa meno
|
|
30
|
+
// che invertire direzione.
|
|
31
|
+
const d = key.upArrow ? -1 : 1;
|
|
32
|
+
const row = (cursor.row + specs.length + d) % specs.length;
|
|
33
|
+
setCursor({ row, caret: caretAtEnd(specs, row, io) });
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
if (spec.kind === 'choice') {
|
|
37
|
+
if (key.leftArrow || key.rightArrow) {
|
|
38
|
+
const d = key.leftArrow ? -1 : 1;
|
|
39
|
+
io.setChoice(cursor.row, (io.choice(cursor.row) + spec.count + d) % spec.count);
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
// Le lettere di selezione diretta arrivano nude: un `key.ctrl` con lo stesso
|
|
43
|
+
// carattere è un acceleratore, non una scelta.
|
|
44
|
+
if (input && !key.ctrl && !key.meta && spec.hotkeys) {
|
|
45
|
+
const at = spec.hotkeys[input];
|
|
46
|
+
if (at !== undefined && at < spec.count) {
|
|
47
|
+
io.setChoice(cursor.row, at);
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
const value = io.text(cursor.row);
|
|
54
|
+
if (key.ctrl) {
|
|
55
|
+
// `^A`/`^E` (convenzione readline) perché `Home`/`End` NON sono esposte da
|
|
56
|
+
// `useInput`: misurate su otto sequenze diverse (`\x1b[H`, `\x1bOH`,
|
|
57
|
+
// `\x1b[1~`, `\x1b[7~` e i gemelli End), arrivano tutte come `input` vuoto
|
|
58
|
+
// senza nessun flag alzato — indistinguibili fra loro e da qualunque altro
|
|
59
|
+
// tasto senza nome.
|
|
60
|
+
//
|
|
61
|
+
// `^D` è il delete-forward, e anche lì il motivo è un limite di Ink: il
|
|
62
|
+
// Backspace fisico manda `\x7f` e il Canc manda `\x1b[3~`, ma
|
|
63
|
+
// `parseKeypress` li battezza ENTRAMBI `delete` — a valle sono lo stesso
|
|
64
|
+
// evento. `key.delete` va quindi al backspace (il tasto che si usa davvero)
|
|
65
|
+
// e la cancellazione in avanti prende il suo tasto readline.
|
|
66
|
+
//
|
|
67
|
+
// `^U` svuota il campo in fuoco. Non è comodità: il backspace tenuto premuto
|
|
68
|
+
// cancella UN carattere per CHUNK letto da stdin, non per pressione, quindi
|
|
69
|
+
// svuotare un campo lungo a colpi di backspace è di fatto non eseguibile.
|
|
70
|
+
if (input === 'a')
|
|
71
|
+
setCursor({ ...cursor, caret: 0 });
|
|
72
|
+
else if (input === 'e')
|
|
73
|
+
setCursor({ ...cursor, caret: cpLen(value) });
|
|
74
|
+
else if (input === 'd')
|
|
75
|
+
io.setText(cursor.row, removeAt(value, cursor.caret));
|
|
76
|
+
else if (input === 'u') {
|
|
77
|
+
io.setText(cursor.row, '');
|
|
78
|
+
setCursor({ ...cursor, caret: 0 });
|
|
79
|
+
}
|
|
80
|
+
else
|
|
81
|
+
return false;
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
if (key.leftArrow || key.rightArrow) {
|
|
85
|
+
// CLAMP agli estremi, non wrap: a inizio campo `←` non deve saltare in
|
|
86
|
+
// fondo. Le righe a scelta ciclano perché sono 3-5 voci; un testo no — il
|
|
87
|
+
// salto sarebbe indistinguibile da uno sfarfallio.
|
|
88
|
+
const d = key.leftArrow ? -1 : 1;
|
|
89
|
+
setCursor({ ...cursor, caret: Math.max(0, Math.min(cpLen(value), cursor.caret + d)) });
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
if (key.backspace || key.delete) {
|
|
93
|
+
io.setText(cursor.row, removeAt(value, cursor.caret - 1));
|
|
94
|
+
setCursor({ ...cursor, caret: Math.max(0, cursor.caret - 1) });
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
if (input && !key.meta) {
|
|
98
|
+
// `sanitizeTyped`: `useInput` consegna il CHUNK di stdin, quindi un
|
|
99
|
+
// incollaggio porta dentro newline e byte di controllo — invisibili nel
|
|
100
|
+
// campo ma contati da Ink nella larghezza della riga. Ed è per lo stesso
|
|
101
|
+
// motivo che il caret avanza della LUNGHEZZA del chunk, non di uno.
|
|
102
|
+
const ins = sanitizeTyped(input);
|
|
103
|
+
io.setText(cursor.row, insertAt(value, cursor.caret, ins));
|
|
104
|
+
setCursor({ ...cursor, caret: cursor.caret + cpLen(ins) });
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
package/dist/glyphs.js
CHANGED
|
@@ -74,6 +74,28 @@ export const LIVE_NONE = ' ';
|
|
|
74
74
|
export function displayProg(prog) {
|
|
75
75
|
return sanitize(prog);
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* T118 — coda della riga task: marker dirty e contatore di conversazioni, nella
|
|
79
|
+
* colonna ancorata al bordo destro del pane.
|
|
80
|
+
*
|
|
81
|
+
* Il contatore va per ULTIMO perché è lui ad allinearsi al bordo — la colonna è
|
|
82
|
+
* resa `pad(…, 'right')`, quindi ciò che sta in fondo alla stringa cade sempre
|
|
83
|
+
* nella stessa colonna. Il marker gli sta a sinistra, dentro la stessa cella
|
|
84
|
+
* invece che in una colonna propria: è raro, e una colonna dedicata costerebbe
|
|
85
|
+
* 3 spazi vuoti su ogni riga di un pane largo la metà del terminale.
|
|
86
|
+
*
|
|
87
|
+
* Vive qui e non nel sito di render perché la STESSA stringa serve due volte —
|
|
88
|
+
* a misurare la larghezza della colonna (sulla vista completa, in `cli.tsx`) e
|
|
89
|
+
* a disegnarla (sulla finestra, in `panes.tsx`). Due formattazioni gemelle
|
|
90
|
+
* divergerebbero alla prima modifica, e la colonna risulterebbe larga quanto
|
|
91
|
+
* una stringa che nessuno scrive.
|
|
92
|
+
*/
|
|
93
|
+
export function taskTail(childCount, dirty) {
|
|
94
|
+
const count = childCount > 0 ? `(${childCount})` : '';
|
|
95
|
+
if (!dirty)
|
|
96
|
+
return count;
|
|
97
|
+
return count ? `${WARN} ${count}` : WARN;
|
|
98
|
+
}
|
|
77
99
|
// T49 — size umana compatta per il detail pane sessione.
|
|
78
100
|
export function fmtSize(bytes) {
|
|
79
101
|
if (bytes < 1024)
|
package/dist/layout.js
CHANGED
|
@@ -3,15 +3,6 @@
|
|
|
3
3
|
// da scalare va tolta in un posto solo, o due chiamanti tolgono cose diverse
|
|
4
4
|
// dalla stessa larghezza e il bordo viene mangiato.
|
|
5
5
|
import { stripProjectCore } from './session-list.js';
|
|
6
|
-
export const EDIT_ROWS = 4;
|
|
7
|
-
/** Le righe del modale edit che sono campi di TESTO (il resto è scelta ←→). */
|
|
8
|
-
export function isTextRow(r) {
|
|
9
|
-
return r === 2 || r === 3;
|
|
10
|
-
}
|
|
11
|
-
/** Chiave della bozza scritta dalla riga di testo `r`. */
|
|
12
|
-
export function editField(r) {
|
|
13
|
-
return r === 2 ? 'detail' : 'title';
|
|
14
|
-
}
|
|
15
6
|
/**
|
|
16
7
|
* Lunghezza in CODE POINT. Il caret ci indicizza sopra: `.length` conterebbe le
|
|
17
8
|
* code unit UTF-16 e un'emoji nel titolo varrebbe 2 posizioni, cioè un cursore
|
package/dist/model.js
CHANGED
|
@@ -60,3 +60,17 @@ export const KIND_LABEL = { ai: 'IA', tool: 'tools', human: 'human' };
|
|
|
60
60
|
// l'ordine naturale di lettura.
|
|
61
61
|
export const EDIT_PRI = ['high', 'med', 'low'];
|
|
62
62
|
export const EDIT_PROG = ['todo', 'wip', 'done', 'locked'];
|
|
63
|
+
// T117 — le righe del modale edit come DATO, nella forma che `fields.ts`
|
|
64
|
+
// consuma: 0 priorità · 1 stato · 2 progresso libero · 3 titolo. Il modale non
|
|
65
|
+
// dà lettere di selezione alle due righe a scelta, dove i valori sono glifi e
|
|
66
|
+
// non hanno un'iniziale da premere.
|
|
67
|
+
export const EDIT_FIELDS = [
|
|
68
|
+
{ kind: 'choice', count: EDIT_PRI.length },
|
|
69
|
+
{ kind: 'choice', count: EDIT_PROG.length },
|
|
70
|
+
{ kind: 'text' },
|
|
71
|
+
{ kind: 'text' },
|
|
72
|
+
];
|
|
73
|
+
/** Chiave della bozza scritta dalla riga di testo `r` (2 = progresso, 3 = titolo). */
|
|
74
|
+
export function editTextField(r) {
|
|
75
|
+
return r === 2 ? 'detail' : 'title';
|
|
76
|
+
}
|