@lamemind/loom-deck 0.62.1 → 0.63.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/dist/actions.js +25 -16
- package/dist/cli.js +10 -4
- package/dist/deck-model.js +21 -1
- package/dist/frame.js +63 -15
- package/dist/glyphs.js +11 -0
- package/dist/input.js +10 -3
- package/dist/spawn.js +11 -2
- package/dist/ui/fields.js +24 -8
- package/package.json +1 -1
package/dist/actions.js
CHANGED
|
@@ -156,24 +156,30 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, setNo
|
|
|
156
156
|
spawned.child.on('error', () => setNote(`⚠ resume fallito (${DECK_RUN})`));
|
|
157
157
|
noteSpawn(spawned);
|
|
158
158
|
}
|
|
159
|
-
/** T148 — `m
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
159
|
+
/** T148/T154(P2,P3) — `m` col bersaglio RESUME: `input.ts` instrada qui solo
|
|
160
|
+
* quando il focus è sulle sessioni E una riga è selezionata (P2), quindi il
|
|
161
|
+
* solo caso residuo è il pin STALE — l'id c'è ma il transcript no, quindi
|
|
162
|
+
* non c'è un bottone da cambiare (P3): resta sul selettore di resume con la
|
|
163
|
+
* propria nota, invece di spostare di soppiatto il selettore della nuda
|
|
164
|
+
* mentre il caret è su una riga sessione.
|
|
165
|
+
*
|
|
166
|
+
* La nota sul SUCCESSO nomina bersaglio e valore (`m → resume: opus`, P2):
|
|
167
|
+
* rovescia la scelta di T148 di non scriverne nessuna, perché lì `m` aveva
|
|
168
|
+
* un bersaglio solo e il bottone che cambiava era l'unico a schermo — da
|
|
169
|
+
* T154 i due selettori possono stare a schermo insieme. */
|
|
165
170
|
function cycleResumeModel() {
|
|
166
|
-
if (model.focus !== 'sessions') {
|
|
167
|
-
setNote('m → modello: seleziona una sessione (→ per il pane)');
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
171
|
if (!model.selSessionObj) {
|
|
171
|
-
setNote(
|
|
172
|
-
? 'm → pin stale: nessun modello da cambiare'
|
|
173
|
-
: 'm → nessuna sessione selezionata');
|
|
172
|
+
setNote('m → pin stale: nessun modello da cambiare');
|
|
174
173
|
return;
|
|
175
174
|
}
|
|
176
|
-
model.cycleResumeModel();
|
|
175
|
+
const next = model.cycleResumeModel();
|
|
176
|
+
setNote(`m → resume: ${next}`);
|
|
177
|
+
}
|
|
178
|
+
/** T154 — `m` col bersaglio NUDA (`c`): il selettore che governa sta sempre
|
|
179
|
+
* a schermo sulla riga launch, quindi il tasto non è mai inerte qui. */
|
|
180
|
+
function cycleBareModel() {
|
|
181
|
+
const next = model.cycleBareModel();
|
|
182
|
+
setNote(`m → nuda: ${next}`);
|
|
177
183
|
}
|
|
178
184
|
// T53 — scrive il sidecar e ricarica subito, senza attendere il tick del poll
|
|
179
185
|
// (stesso feedback immediato del pin).
|
|
@@ -311,9 +317,11 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, setNo
|
|
|
311
317
|
spawned.child.on('error', () => setNote(`⚠ srotolamento ${path} fallito (${DECK_RUN})`));
|
|
312
318
|
noteSpawn(spawned);
|
|
313
319
|
}
|
|
314
|
-
/** `c` — sessione claude a mani nude, senza task e senza prompt.
|
|
320
|
+
/** `c` — sessione claude a mani nude, senza task e senza prompt. T154 — il
|
|
321
|
+
* modello è quello del selettore proprio della nuda (`model.bareModel`),
|
|
322
|
+
* passato SEMPRE nell'argv anche sul default. */
|
|
315
323
|
function openClaude() {
|
|
316
|
-
const spawned = spawnClaudeEmpty(cwd);
|
|
324
|
+
const spawned = spawnClaudeEmpty(cwd, model.bareModel);
|
|
317
325
|
spawned.child.on('error', () => setNote(`⚠ c → spawn claude fallito (${DECK_RUN})`));
|
|
318
326
|
noteSpawn(spawned);
|
|
319
327
|
}
|
|
@@ -352,6 +360,7 @@ export function useDeckActions({ cwd, tasksPath, tasksDir, columns, model, setNo
|
|
|
352
360
|
spawnTaskSession,
|
|
353
361
|
resumeSession,
|
|
354
362
|
cycleResumeModel,
|
|
363
|
+
cycleBareModel,
|
|
355
364
|
writeNote,
|
|
356
365
|
assignSession,
|
|
357
366
|
forkSession,
|
package/dist/cli.js
CHANGED
|
@@ -19,9 +19,10 @@ import { resolveTasksPath, resolveTasksDir } from './tasks.js';
|
|
|
19
19
|
import { LAUNCH_SEP } from './config.js';
|
|
20
20
|
import { anchorFrame, enableMouse } from './mouse.js';
|
|
21
21
|
import { sanitize } from './width.js';
|
|
22
|
-
import { modelAlias } from './glyphs.js';
|
|
23
|
-
import { MODEL_DEFAULT } from './spawn.js';
|
|
24
|
-
import { deckLegend, frameGeometry, headlineWidth, indicatorRow, launchRow } from './frame.js';
|
|
22
|
+
import { MODEL_SHORT_LIST, modelAlias } from './glyphs.js';
|
|
23
|
+
import { MODEL_DEFAULT, MODELS } from './spawn.js';
|
|
24
|
+
import { BARE_BUTTONS_WIDTH, BARE_LABEL, deckLegend, frameGeometry, headlineWidth, indicatorRow, launchRow, } from './frame.js';
|
|
25
|
+
import { choiceButtons } from './ui/fields.js';
|
|
25
26
|
import { useDeckModel } from './deck-model.js';
|
|
26
27
|
import { useDeckActions } from './actions.js';
|
|
27
28
|
import { useDeckInput } from './input.js';
|
|
@@ -162,6 +163,11 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
162
163
|
: 'none';
|
|
163
164
|
const detailParts = model.detail ? detailMetaOf(model.detail) : null;
|
|
164
165
|
const launch = launchRow(model.launch, columns);
|
|
166
|
+
// T154 — i bottoni del blocco selettore della nuda: stessa primitiva
|
|
167
|
+
// (`choiceButtons`) del selettore di resume/fork, sulle SHORT (D1) e con
|
|
168
|
+
// `avail` pari alla larghezza naturale del gruppo, così `cutParts` non
|
|
169
|
+
// taglia mai (il blocco è riservato, non conteso — vedi `launchRow`).
|
|
170
|
+
const bareButtons = choiceButtons(MODEL_SHORT_LIST, MODELS.indexOf(model.bareModel), BARE_BUTTONS_WIDTH, MODELS.indexOf(MODEL_DEFAULT));
|
|
165
171
|
// T134 — gli indicatori ancorati a destra della riga legenda. Si compongono
|
|
166
172
|
// PRIMA della legenda perché è la loro larghezza a decidere quanto ne resta:
|
|
167
173
|
// hanno la precedenza sul budget (D5 preflight).
|
|
@@ -245,7 +251,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
245
251
|
purgeBulk: model.purgeBulk,
|
|
246
252
|
inboxPane: model.rightPane === 'inbox',
|
|
247
253
|
hasInbox: model.selInbox !== null,
|
|
248
|
-
}), indicators: indicators, columns: columns }), frame.launchLine ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [launch.segments.map((s) => s.text).join(LAUNCH_SEP), launch.overflow > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 +", launch.overflow, " fuori riga"] })) : null, launch.unreachable > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 ", launch.unreachable, " oltre la 9\u00AA (non raggiungibili)"] })) : null] })) : null, mode === 'create' ? _jsx(TextBox, { glyph: "C", value: textModals.draft }) : null, mode === 'note' ? _jsx(TextBox, { glyph: "\u270E", value: textModals.noteDraft }) : null, mode === 'sort' ? _jsx(SortModal, { sort: model.view.sort }) : null, mode === 'filter' ? (_jsx(FilterModal, { view: model.view, cursor: viewModals.filterCursor })) : null, mode === 'edit' && textModals.edit && textModals.editTask ? (_jsx(EditModal, { id: textModals.editTask.id, draft: textModals.edit, row: textModals.editCursor.row, caret: textModals.editCursor.caret, columns: columns })) : null, mode === 'purge' && purge.draft ? (_jsx(PurgeModal, { draft: purge.draft, columns: columns })) : null, _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(TasksPane, { tasks: frame.windowTasks, counts: model.taskCounts, activeView: model.taskViewId, paneCount: model.paneTasks.length, view: model.view, selected: model.selIndex, spotCount: model.spotCount, allCount: model.sessions.length, idW: model.taskCols.id, tailW: model.taskCols.tail, focused: model.focus === 'tasks', loadError: model.loadError, windowStart: frame.taskWin.start, above: frame.taskWin.start, below: model.paneTasks.length - frame.taskWin.end, columns: columns, data: model.taskRowData, blockMark: model.blockMark }), model.rightPane === 'inbox' ? (_jsx(InboxPane, { files: frame.windowInbox, counts: model.inboxCounts, activeView: model.inboxViewId, paneCount: model.inboxFiles.length, selectedPath: model.selInboxPath, focused: model.focus === 'inbox', above: frame.inboxWin.start, below: model.inboxFiles.length - frame.inboxWin.end, columns: columns, ok: model.inboxOk, scanned: model.inboxScanned })) : (_jsx(SessionsPane, { parentLabel: model.parentLabel, isSpot: model.isSpot, isAll: model.isAll, bindings: model.bindings, taskW: model.sessionCols.task, ageW: model.sessionCols.age, rows: frame.windowRows, counts: model.sessionCounts, activeView: model.sessionViewId, paneCount: model.sessionRows.length, selectedId: model.selSessionId ?? undefined, focused: model.focus === 'sessions', above: frame.sessionWin.start, below: model.sessionRows.length - frame.sessionWin.end, columns: columns, forkOf: model.forkOf, sessionNotes: model.sessionNotes, projectCore: model.projectCore, live: model.live }))] }), frame.budget.preview && previewKind === 'task' && model.detail ? (_jsx(PreviewPane, { kind: "task", detail: model.detail, maxLines: frame.budget.detailLines, columns: columns })) : frame.budget.preview && previewKind === 'inbox' && model.selInbox ? (_jsx(PreviewPane, { kind: "inbox", file: model.selInbox, columns: columns })) : frame.budget.preview && previewKind === 'session' && model.selSessionObj ? (_jsx(PreviewPane, { kind: "session", s: model.selSessionObj, firstLines: frame.budget.sessionFirstLines, lastLines: frame.budget.sessionLastLines, columns: columns, origin: model.forkOf.get(model.selSessionObj.sessionId) ?? null, note: model.sessionNotes.get(model.selSessionObj.sessionId) ?? '', live: model.live.get(model.selSessionObj.sessionId) ?? null, resumeModel: model.resumeModel })) : null, note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: sanitize(note) }) : null] }));
|
|
254
|
+
}), indicators: indicators, columns: columns }), frame.launchLine ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [launch.segments.map((s) => s.text).join(LAUNCH_SEP), launch.overflow > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 +", launch.overflow, " fuori riga"] })) : null, launch.unreachable > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 ", launch.unreachable, " oltre la 9\u00AA (non raggiungibili)"] })) : null, launch.filler, BARE_LABEL.text, ' ', bareButtons.nodes] })) : null, mode === 'create' ? _jsx(TextBox, { glyph: "C", value: textModals.draft }) : null, mode === 'note' ? _jsx(TextBox, { glyph: "\u270E", value: textModals.noteDraft }) : null, mode === 'sort' ? _jsx(SortModal, { sort: model.view.sort }) : null, mode === 'filter' ? (_jsx(FilterModal, { view: model.view, cursor: viewModals.filterCursor })) : null, mode === 'edit' && textModals.edit && textModals.editTask ? (_jsx(EditModal, { id: textModals.editTask.id, draft: textModals.edit, row: textModals.editCursor.row, caret: textModals.editCursor.caret, columns: columns })) : null, mode === 'purge' && purge.draft ? (_jsx(PurgeModal, { draft: purge.draft, columns: columns })) : null, _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(TasksPane, { tasks: frame.windowTasks, counts: model.taskCounts, activeView: model.taskViewId, paneCount: model.paneTasks.length, view: model.view, selected: model.selIndex, spotCount: model.spotCount, allCount: model.sessions.length, idW: model.taskCols.id, tailW: model.taskCols.tail, focused: model.focus === 'tasks', loadError: model.loadError, windowStart: frame.taskWin.start, above: frame.taskWin.start, below: model.paneTasks.length - frame.taskWin.end, columns: columns, data: model.taskRowData, blockMark: model.blockMark }), model.rightPane === 'inbox' ? (_jsx(InboxPane, { files: frame.windowInbox, counts: model.inboxCounts, activeView: model.inboxViewId, paneCount: model.inboxFiles.length, selectedPath: model.selInboxPath, focused: model.focus === 'inbox', above: frame.inboxWin.start, below: model.inboxFiles.length - frame.inboxWin.end, columns: columns, ok: model.inboxOk, scanned: model.inboxScanned })) : (_jsx(SessionsPane, { parentLabel: model.parentLabel, isSpot: model.isSpot, isAll: model.isAll, bindings: model.bindings, taskW: model.sessionCols.task, ageW: model.sessionCols.age, rows: frame.windowRows, counts: model.sessionCounts, activeView: model.sessionViewId, paneCount: model.sessionRows.length, selectedId: model.selSessionId ?? undefined, focused: model.focus === 'sessions', above: frame.sessionWin.start, below: model.sessionRows.length - frame.sessionWin.end, columns: columns, forkOf: model.forkOf, sessionNotes: model.sessionNotes, projectCore: model.projectCore, live: model.live }))] }), frame.budget.preview && previewKind === 'task' && model.detail ? (_jsx(PreviewPane, { kind: "task", detail: model.detail, maxLines: frame.budget.detailLines, columns: columns })) : frame.budget.preview && previewKind === 'inbox' && model.selInbox ? (_jsx(PreviewPane, { kind: "inbox", file: model.selInbox, columns: columns })) : frame.budget.preview && previewKind === 'session' && model.selSessionObj ? (_jsx(PreviewPane, { kind: "session", s: model.selSessionObj, firstLines: frame.budget.sessionFirstLines, lastLines: frame.budget.sessionLastLines, columns: columns, origin: model.forkOf.get(model.selSessionObj.sessionId) ?? null, note: model.sessionNotes.get(model.selSessionObj.sessionId) ?? '', live: model.live.get(model.selSessionObj.sessionId) ?? null, resumeModel: model.resumeModel })) : null, note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: sanitize(note) }) : null] }));
|
|
249
255
|
}
|
|
250
256
|
const cwd = process.cwd();
|
|
251
257
|
// T116 — `exitOnCtrlC: false` toglie a Ink l'uscita immediata su `^C`: senza,
|
package/dist/deck-model.js
CHANGED
|
@@ -131,6 +131,12 @@ export function useDeckModel({ cwd, tasksPath, tasksDir, setNote, }) {
|
|
|
131
131
|
// tornare su una riga già visitata NON ricorda la scelta fatta lì — è lo
|
|
132
132
|
// stesso costo che T108 ha già accettato per il selettore del detail.
|
|
133
133
|
const [resumeModel, setResumeModel] = useState(MODEL_DEFAULT);
|
|
134
|
+
// T154 — il modello con cui la sessione NUDA (`c`) apre. Stato PROPRIO,
|
|
135
|
+
// indipendente da `resumeModel`: non segue nessuna riga selezionata (`c` non
|
|
136
|
+
// ha una riga, è un'azione sulla surface del cappello) e quindi non si
|
|
137
|
+
// azzera mai — resta quello scelto finché non si sceglie altro, per tutta la
|
|
138
|
+
// vita del deck.
|
|
139
|
+
const [bareModel, setBareModel] = useState(MODEL_DEFAULT);
|
|
134
140
|
// T39 — vista corrente (filtri + sort). Vive nel modello e non nell'hook dei
|
|
135
141
|
// modali che la editano: è ciò che `applyView` consuma per produrre la lista,
|
|
136
142
|
// è persistita su disco e la rilegge il tasto `w`. È stato del modello che due
|
|
@@ -407,10 +413,22 @@ export function useDeckModel({ cwd, tasksPath, tasksDir, setNote, }) {
|
|
|
407
413
|
* valore SUCCESSIVO del catalogo (ciclico), gemella di `cycleView` ma su un
|
|
408
414
|
* asse diverso: qui non c'è una vista da cambiare, solo un valore da far
|
|
409
415
|
* avanzare. Il guard (nessuna sessione, focus altrove) sta nell'attuatore
|
|
410
|
-
* `actions.ts`, come per `f`/`p`: qui c'è solo la meccanica dello stato.
|
|
416
|
+
* `actions.ts`, come per `f`/`p`: qui c'è solo la meccanica dello stato.
|
|
417
|
+
* T154/P2 — ritorna il valore NUOVO: il chiamante lo scrive nella nota di
|
|
418
|
+
* conferma prima che il render con lo stato aggiornato arrivi. */
|
|
411
419
|
function cycleResumeModel() {
|
|
412
420
|
const next = MODELS[(MODELS.indexOf(resumeModel) + 1) % MODELS.length];
|
|
413
421
|
setResumeModel(next);
|
|
422
|
+
return next;
|
|
423
|
+
}
|
|
424
|
+
/** T154 — gemella di `cycleResumeModel`, sullo stato della sessione NUDA:
|
|
425
|
+
* `m` ci finisce quando il focus non è su una riga sessione vera (P2, D4/D5
|
|
426
|
+
* degli AC). Nessun guard qui: il bersaglio non è mai inerte — il selettore
|
|
427
|
+
* che governa è sempre a schermo sulla riga launch. */
|
|
428
|
+
function cycleBareModel() {
|
|
429
|
+
const next = MODELS[(MODELS.indexOf(bareModel) + 1) % MODELS.length];
|
|
430
|
+
setBareModel(next);
|
|
431
|
+
return next;
|
|
414
432
|
}
|
|
415
433
|
/**
|
|
416
434
|
* T134 — `^B` scambia i due pane dello slot destro (D8 preflight).
|
|
@@ -493,6 +511,8 @@ export function useDeckModel({ cwd, tasksPath, tasksDir, setNote, }) {
|
|
|
493
511
|
sessionCols,
|
|
494
512
|
resumeModel,
|
|
495
513
|
cycleResumeModel,
|
|
514
|
+
bareModel,
|
|
515
|
+
cycleBareModel,
|
|
496
516
|
// derivazioni del pane inbox
|
|
497
517
|
rightPane,
|
|
498
518
|
inboxScanned: inbox.scanned,
|
package/dist/frame.js
CHANGED
|
@@ -20,17 +20,15 @@ import { FRAME_TEXT_COL, HINT_ROW, inlineRegions, LAUNCH_ROW, PANE_TEXT_PAD, pan
|
|
|
20
20
|
import { cellWidth, launchLegend, LAUNCH_SEP } from './config.js';
|
|
21
21
|
import { layoutBudget, windowRange } from './viewport.js';
|
|
22
22
|
import { sanitize, termWidth } from './width.js';
|
|
23
|
-
import { WARN } from './glyphs.js';
|
|
23
|
+
import { MODEL_SHORT_LIST, WARN } from './glyphs.js';
|
|
24
24
|
import { STATUS_MISSING } from './project-status.js';
|
|
25
25
|
import { META_ROWS } from './model.js';
|
|
26
26
|
import { rowIndexOf } from './session-list.js';
|
|
27
27
|
import { VERSION } from './version.js';
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
// compass: 🤖 = nuova sessione claude. Per il terminale compass usa 🖥️, che nel
|
|
33
|
-
// frame Ink NON passa — `sanitize` lo sostituisce (VTE lo disegna largo 1,
|
|
28
|
+
// La surface built-in del cappello, in testa alla riga launch. Non sta fra i
|
|
29
|
+
// tasti perché ha la stessa natura delle voci `launch` — fire-once, cwd =
|
|
30
|
+
// project root, nessuno stato. Per il terminale compass usa 🖥️, che nel frame
|
|
31
|
+
// Ink NON passa — `sanitize` lo sostituisce (VTE lo disegna largo 1,
|
|
34
32
|
// string-width dice 2: discordante, invariante ① di width.ts) e resterebbe un
|
|
35
33
|
// `·` muto. 💻 è il gemello concorde; il `sanitize` qui rende il vincolo
|
|
36
34
|
// automatico invece che da ricordare.
|
|
@@ -38,10 +36,35 @@ import { VERSION } from './version.js';
|
|
|
38
36
|
// T21 — `key` è il tasto che la superficie rappresenta: un click su di essa
|
|
39
37
|
// entra nell'handler di tastiera con quel tasto, invece di chiamare l'azione
|
|
40
38
|
// per conto proprio. È ciò che tiene click e tasto per costruzione allineati.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
//
|
|
40
|
+
// T154/P9 — `c` non vive più qui: la surface trasloca a destra come etichetta
|
|
41
|
+
// del blocco selettore della nuda (`BARE_LABEL` sotto), adiacente ai bottoni
|
|
42
|
+
// col modello con cui apre invece che sull'estremo opposto della riga.
|
|
43
|
+
export const SURFACE_SEGMENTS = [{ key: 't', text: sanitize('t 💻') }];
|
|
44
|
+
// T154/P9 — l'etichetta del blocco selettore della nuda: unico segmento
|
|
45
|
+
// cliccabile del blocco (nessun tasto nomina un modello, `m` cicla e non
|
|
46
|
+
// seleziona — i bottoni non lo sono).
|
|
47
|
+
export const BARE_LABEL = { key: 'c', text: sanitize('c 🤖') };
|
|
48
|
+
// T154/D1 — i bottoni usano le SHORT (`fab`/`ops`/`son`/`hak`), non i nomi
|
|
49
|
+
// interi del selettore di resume/fork: la riga launch compete per spazio con
|
|
50
|
+
// la legenda delle voci custom, e le short sono già il vocabolario compatto
|
|
51
|
+
// che la colonna modello della lista sessioni usa per lo stesso motivo.
|
|
52
|
+
const BARE_BUTTONS_TEXT = MODEL_SHORT_LIST.map((s) => `[ ${s} ]`).join(' ');
|
|
53
|
+
/** Larghezza dei soli bottoni, in celle: passata come `avail` a
|
|
54
|
+
* `choiceButtons` — esattamente la larghezza naturale, così `cutParts` non
|
|
55
|
+
* taglia mai (il blocco è RISERVATO, D1/P8: a cedere sono le voci `launch`,
|
|
56
|
+
* mai il selettore). */
|
|
57
|
+
export const BARE_BUTTONS_WIDTH = termWidth(BARE_BUTTONS_TEXT);
|
|
58
|
+
/** Spazio fra l'etichetta `c 🤖` e il primo bottone. */
|
|
59
|
+
const BARE_LABEL_GAP = ' ';
|
|
60
|
+
/** Larghezza FISSA del blocco intero (etichetta + spazio + bottoni): non
|
|
61
|
+
* dipende da quale modello sia selezionato né dal default — ogni short è
|
|
62
|
+
* larga uguale (3 lettere) — quindi la geometria non ha bisogno di saperli;
|
|
63
|
+
* la STILATURA sì, e la fa il chiamante con `choiceButtons` sugli stessi
|
|
64
|
+
* valori (`MODEL_SHORT_LIST`). 39 celle con l'attuale `MODELS` a 4 voci
|
|
65
|
+
* (D1 preflight).
|
|
66
|
+
*/
|
|
67
|
+
const BARE_BLOCK_WIDTH = termWidth(BARE_LABEL.text) + termWidth(BARE_LABEL_GAP) + BARE_BUTTONS_WIDTH;
|
|
45
68
|
/**
|
|
46
69
|
* La legenda della modalità normale. Elenca SOLO i tasti che fanno qualcosa qui
|
|
47
70
|
* e ora: le voci contestuali compaiono quando il pane a fuoco le rende possibili
|
|
@@ -102,21 +125,46 @@ export function deckLegend(state) {
|
|
|
102
125
|
* seconde ri-splittando il primo sarebbe un conto parallelo, che diverge alla
|
|
103
126
|
* prima label che contenga il separatore.
|
|
104
127
|
*
|
|
105
|
-
* Le celle
|
|
106
|
-
* sulla riga → vanno riservate, o le voci launch la sfonderebbero di quel
|
|
128
|
+
* Le celle della surface (più il ` · ` che la separa dalle voci) sono già spese
|
|
129
|
+
* sulla riga → vanno riservate, o le voci launch la sfonderebbero di quel
|
|
130
|
+
* tanto. T154/P8 — il blocco selettore della nuda entra nella STESSA riserva:
|
|
131
|
+
* su un progetto con molte voci `launch` a cedere per prima è una voce custom,
|
|
132
|
+
* mai il selettore (un bottone di modello troncato sarebbe indistinguibile da
|
|
133
|
+
* un modello che non esiste, una voce launch resta raggiungibile col suo
|
|
134
|
+
* tasto-cifra anche quando non è scritta in riga).
|
|
107
135
|
*/
|
|
108
136
|
export function launchRow(launch, columns) {
|
|
109
137
|
const surfaceLegend = SURFACE_SEGMENTS.map((s) => s.text).join(LAUNCH_SEP);
|
|
110
|
-
const
|
|
138
|
+
const reserved = cellWidth(surfaceLegend) + cellWidth(LAUNCH_SEP) + BARE_BLOCK_WIDTH;
|
|
139
|
+
const legend = launchLegend(launch, columns, reserved);
|
|
111
140
|
const segments = [
|
|
112
141
|
...SURFACE_SEGMENTS,
|
|
113
142
|
...legend.taken.map((text, i) => ({ key: String(i + 1), text })),
|
|
114
143
|
];
|
|
144
|
+
// Larghezza REALE della sinistra, testo di overflow/irraggiungibili incluso:
|
|
145
|
+
// sono loro a decidere quanto riempimento serve, non solo i segmenti.
|
|
146
|
+
const leftWidth = termWidth(segments.map((s) => s.text).join(LAUNCH_SEP)) +
|
|
147
|
+
(legend.overflow > 0 ? termWidth(` · +${legend.overflow} fuori riga`) : 0) +
|
|
148
|
+
(legend.unreachable > 0
|
|
149
|
+
? termWidth(` · ${legend.unreachable} oltre la 9ª (non raggiungibili)`)
|
|
150
|
+
: 0);
|
|
151
|
+
// T154/P5 — la formula dell'ancoraggio: colonne del bordo esterno, meno la
|
|
152
|
+
// sinistra già disegnata, meno il blocco che deve starci per intero. Un
|
|
153
|
+
// pavimento di 1 tiene sempre almeno uno spazio fra le due metà, invece di
|
|
154
|
+
// farle toccare quando lo schermo è troppo stretto per il resto.
|
|
155
|
+
const filler = ' '.repeat(Math.max(1, columns - 6 - leftWidth - BARE_BLOCK_WIDTH));
|
|
156
|
+
const bareCol = FRAME_TEXT_COL + leftWidth + termWidth(filler);
|
|
157
|
+
const bareRegion = {
|
|
158
|
+
key: BARE_LABEL.key,
|
|
159
|
+
start: bareCol,
|
|
160
|
+
end: bareCol + termWidth(BARE_LABEL.text) - 1,
|
|
161
|
+
};
|
|
115
162
|
return {
|
|
116
163
|
segments,
|
|
117
|
-
regions: rowRegions(segments, LAUNCH_SEP, FRAME_TEXT_COL),
|
|
164
|
+
regions: [...rowRegions(segments, LAUNCH_SEP, FRAME_TEXT_COL), bareRegion],
|
|
118
165
|
overflow: legend.overflow,
|
|
119
166
|
unreachable: legend.unreachable,
|
|
167
|
+
filler,
|
|
120
168
|
};
|
|
121
169
|
}
|
|
122
170
|
/**
|
package/dist/glyphs.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
// Glifi letterali del frame e formatter di display. Fase VISTA, ma senza JSX:
|
|
2
2
|
// tutto qui è puro e testabile, e nessun sito di render scrive un glifo nudo.
|
|
3
3
|
import { sanitize } from './width.js';
|
|
4
|
+
// T148/P3 — costo accettato: la fase vista importa dalla fase effetti esterni.
|
|
5
|
+
// Il TIPO si cancella alla compilazione (nessuna dipendenza runtime); T154
|
|
6
|
+
// aggiunge MODELS come VALORE — una dipendenza runtime vera, ma senza ciclo:
|
|
7
|
+
// `spawn.ts` non importa questo file, quindi il verso resta uno solo.
|
|
8
|
+
import { MODELS } from './spawn.js';
|
|
4
9
|
// Glifi LETTERALI del JSX. I dati passano dai loader, che sanificano al
|
|
5
10
|
// confine; questi no — quindi passano da `sanitize` una volta qui, così nessun
|
|
6
11
|
// sito di render scrive un glifo nudo. `↳ ○ ▸ ⏎ · − ↑ ↓` sono già concordi e
|
|
@@ -41,6 +46,12 @@ export const MODEL_EMPTY = TASK_EMPTY;
|
|
|
41
46
|
* legge come una riga normale e si scorre senza notarla; l'id per intero sta
|
|
42
47
|
* nel blocco preview. */
|
|
43
48
|
export const MODEL_UNKNOWN = '?';
|
|
49
|
+
/** T154 — le short di `MODEL_SHORT` nell'ordine POSIZIONALE di `MODELS`, non
|
|
50
|
+
* quello di inserimento dell'oggetto sopra: il blocco selettore della nuda
|
|
51
|
+
* sulla riga launch cicla su questo elenco, e `index`/`originIndex` restano
|
|
52
|
+
* indici in `MODELS` — derivarlo da lì invece di scriverlo a mano evita una
|
|
53
|
+
* seconda lista capace di disallinearsi al primo modello aggiunto. */
|
|
54
|
+
export const MODEL_SHORT_LIST = MODELS.map((m) => MODEL_SHORT[m]);
|
|
44
55
|
export function modelShort(id) {
|
|
45
56
|
if (!id)
|
|
46
57
|
return MODEL_EMPTY;
|
package/dist/input.js
CHANGED
|
@@ -467,9 +467,16 @@ export function useDeckInput({ cwd, tasksDir, mode, setMode, setNote, model, act
|
|
|
467
467
|
actions.togglePin();
|
|
468
468
|
}
|
|
469
469
|
else if (input === 'm') {
|
|
470
|
-
// T148 —
|
|
471
|
-
//
|
|
472
|
-
|
|
470
|
+
// T148/T154 — bersaglio deciso dal focus E dalla selezione (P2): con
|
|
471
|
+
// una riga sessioni selezionata (stale compresa, P3) scorre il modello
|
|
472
|
+
// di resume/fork di quella riga; altrove (focus task, focus inbox,
|
|
473
|
+
// pane sessioni senza selezione) scorre il modello della sessione nuda.
|
|
474
|
+
if (model.focus === 'sessions' && model.selSessionId) {
|
|
475
|
+
actions.cycleResumeModel();
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
actions.cycleBareModel();
|
|
479
|
+
}
|
|
473
480
|
}
|
|
474
481
|
else if (input === 'N') {
|
|
475
482
|
// T53 — nota sulla conversazione selezionata. MAIUSCOLA perché apre un
|
package/dist/spawn.js
CHANGED
|
@@ -330,8 +330,17 @@ export function spawnDeckFork(taskId, cwd, originId, newId, model) {
|
|
|
330
330
|
// di spawnDeck: i tre argomenti mancano tutti insieme, un `if` per ciascuno
|
|
331
331
|
// sporcherebbe il percorso bound. Il titolo tab resta la label loom — lo mette
|
|
332
332
|
// deck-run, perché il match compass è window-level e non sa nulla di task.
|
|
333
|
-
|
|
334
|
-
|
|
333
|
+
//
|
|
334
|
+
// T154 — il modello viaggia SEMPRE nell'argv (`emptyArgs`), anche sul default:
|
|
335
|
+
// prima di questa task il tasto `c` non lo passava affatto, e chi lo premeva
|
|
336
|
+
// non sceglieva né vedeva quale modello `deck-run` avrebbe risolto da sé.
|
|
337
|
+
// Estratta come i suoi fratelli (`deckArgs`/`resumeArgs`/`forkArgs`): tutti i
|
|
338
|
+
// costruttori di argv con un test proprio lo sono (P4 preflight).
|
|
339
|
+
export function emptyArgs(model) {
|
|
340
|
+
return ['--no-task', '--model', model];
|
|
341
|
+
}
|
|
342
|
+
export function spawnClaudeEmpty(cwd, model) {
|
|
343
|
+
return launchDeckRun(emptyArgs(model), cwd);
|
|
335
344
|
}
|
|
336
345
|
/**
|
|
337
346
|
* T134 — sessione NUDA con un prompt letterale: `deck-run --no-task --prompt
|
package/dist/ui/fields.js
CHANGED
|
@@ -28,8 +28,8 @@ export function FieldText({ value, caret, focused, cols, }) {
|
|
|
28
28
|
* preview sessione). */
|
|
29
29
|
export const LABEL_W = 8;
|
|
30
30
|
/**
|
|
31
|
-
* T117 — una riga a
|
|
32
|
-
* voce attiva in video inverso.
|
|
31
|
+
* T117 — i soli BOTTONI di una riga a scelta: valore per valore, già tagliati
|
|
32
|
+
* al budget e stilati (voce attiva in video inverso, origine sottolineata).
|
|
33
33
|
*
|
|
34
34
|
* Due passate di `cutParts`: la seconda serve SOLO quando qualcosa cade, e
|
|
35
35
|
* riserva le colonne del contatore. Riservarle sempre costerebbe 6 colonne su
|
|
@@ -39,11 +39,14 @@ export const LABEL_W = 8;
|
|
|
39
39
|
* cancellerebbe l'unica informazione che la riga esiste per dare — quale valore
|
|
40
40
|
* sta per essere usato.
|
|
41
41
|
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
42
|
+
* T154/P6 — estratta da `ChoiceRow`, che ne resta il primo chiamante: un
|
|
43
|
+
* secondo (il blocco selettore della nuda sulla riga launch) ha bisogno dei
|
|
44
|
+
* soli bottoni dentro una cornice propria (nessun caret, un'etichetta diversa
|
|
45
|
+
* da `label.padEnd`), e riscriverne la resa produrrebbe la seconda copia della
|
|
46
|
+
* regola di troncamento e sottolineatura — quella che diverge al primo modello
|
|
47
|
+
* aggiunto a `MODELS`.
|
|
45
48
|
*/
|
|
46
|
-
export function
|
|
49
|
+
export function choiceButtons(values, index, avail, originIndex) {
|
|
47
50
|
const segs = values.map((v) => `[ ${v} ]`);
|
|
48
51
|
const parts = [];
|
|
49
52
|
segs.forEach((s, i) => {
|
|
@@ -51,11 +54,24 @@ export function ChoiceRow({ label, values, index, focused, width, originIndex, }
|
|
|
51
54
|
parts.push(' ');
|
|
52
55
|
parts.push(s);
|
|
53
56
|
});
|
|
54
|
-
const avail = Math.max(0, width - LABEL_W - CARET_OFF.length);
|
|
55
57
|
let shown = cutParts(parts, avail, index * 2);
|
|
56
58
|
const dropped = (v) => segs.filter((s, i) => v[i * 2] !== s).length;
|
|
57
59
|
if (dropped(shown) > 0)
|
|
58
60
|
shown = cutParts(parts, Math.max(0, avail - 6), index * 2);
|
|
59
61
|
const cut = dropped(shown);
|
|
60
|
-
|
|
62
|
+
const nodes = shown.map((part, i) => i % 2 === 1 ? (_jsx(Text, { children: part }, i)) : (_jsx(Text, { inverse: i / 2 === index, underline: i / 2 === originIndex, color: i / 2 === index ? 'green' : 'gray', children: part }, i)));
|
|
63
|
+
return { nodes, cut };
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* T117 — una riga a SCELTA di un'area di compilazione: caret + etichetta +
|
|
67
|
+
* bottoni di `choiceButtons`.
|
|
68
|
+
*
|
|
69
|
+
* T148 — estratto dal detail della task perché un secondo chiamante (il
|
|
70
|
+
* selettore modello della preview sessione) ne riusa la stessa resa a quadre,
|
|
71
|
+
* non un componente gemello capace di divergere.
|
|
72
|
+
*/
|
|
73
|
+
export function ChoiceRow({ label, values, index, focused, width, originIndex, }) {
|
|
74
|
+
const avail = Math.max(0, width - LABEL_W - CARET_OFF.length);
|
|
75
|
+
const { nodes, cut } = choiceButtons(values, index, avail, originIndex);
|
|
76
|
+
return (_jsxs(Text, { wrap: "truncate-end", children: [focused ? CARET : CARET_OFF, _jsx(Text, { dimColor: true, children: label.padEnd(LABEL_W) }), nodes, cut > 0 ? _jsxs(Text, { color: "yellow", children: [" \u00B7 +", cut] }) : null] }));
|
|
61
77
|
}
|
package/package.json
CHANGED