@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 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 + barra azioni |
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
- 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ù.
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
- 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.
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
- Dentro il detail `←→` cicla i bottoni invece di cambiare pane, e `tab` non cambia la vistail
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
- Il catalogo dei prompt vive in `scripts/deck-run`, non nella TUI: è il primitive UI-agnostico, il deck gli passa un **simbolo** (`--prompt-kind`) e non una stringa. Così il testo sta in un posto solo, e con lui il suo vincolo di quoting il prompt viaggia dentro apici singoli in `bash -lc`, quindi non può contenerne.
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, cpLen, editField, insertAt, isDone, isTextRow, removeAt, EDIT_ROWS, } from './layout.js';
19
- import { TASK_EMPTY, relTime, sanitizeTyped } from './glyphs.js';
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 riga attiva della griglia.
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 [editRow, setEditRow] = useState(0);
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
- function spawnForTask(id, kind, model, spawnNote = '') {
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
- ? `✎ nota su ${sid.slice(0, 8)}: "${cut(text, 40)}"`
426
- : `✎ nota rimossa da ${sid.slice(0, 8)}`);
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
- setEditRow(0);
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 → nota annullata');
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
- else if (key.return) {
861
+ if (key.return) {
832
862
  submitEdit();
863
+ return;
833
864
  }
834
- else if (key.upArrow || key.downArrow) {
835
- const next = ((editRow + EDIT_ROWS + (key.upArrow ? -1 : 1)) % EDIT_ROWS);
836
- setEditRow(next);
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 → nota: seleziona una sessione (→ per il pane)');
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
- ...(canPin ? ['p pin', 'N nota', 'A assegna'] : []),
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: ["nota 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: editRow, 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, 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] }));
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
+ }