@lamemind/loom-deck 0.62.1 → 0.64.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 +35 -2
- package/dist/actions.js +25 -16
- package/dist/cli.js +29 -4
- package/dist/deck-model.js +21 -1
- package/dist/frame.js +105 -18
- package/dist/gitlink.js +210 -0
- package/dist/glyphs.js +11 -0
- package/dist/input.js +23 -3
- package/dist/overlays/gitlink.js +154 -0
- package/dist/spawn.js +11 -2
- package/dist/ui/fields.js +24 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -74,7 +74,7 @@ Regola unica, senza eccezioni — pensata per reggere l'aggiunta di nuove azioni
|
|
|
74
74
|
| `⏎` | azione primaria del pane | Tasks → apre il **detail** della task selezionata · Sessions → riprende (`claude --resume`) la sessione selezionata |
|
|
75
75
|
| **MAIUSCOLA** | **apre un modale** | cattura tutti i tasti; `esc` annulla, non esce |
|
|
76
76
|
| minuscola | azione immediata, one-shot | |
|
|
77
|
-
| `CTRL`+lettera | idiomi universali, toggle dentro i modali di testo, varianti di un'azione | `^F` = find, come ovunque · `^K`/`^P`/`^R` = spawn con un altro prompt · `^G`/`^O` = genera/apri il project status |
|
|
77
|
+
| `CTRL`+lettera | idiomi universali, toggle dentro i modali di testo, varianti di un'azione | `^F` = find, come ovunque · `^K`/`^P`/`^R` = spawn con un altro prompt · `^G`/`^O` = genera/apri il project status · `^U` = update del gitlink |
|
|
78
78
|
| `1`…`9` | voce `launch` n-esima del progetto | da `.claude/loom-works.json` |
|
|
79
79
|
| `esc` | chiude l'overlay aperto | in modalità normale è inerte: **nessuna lettera chiude il deck** |
|
|
80
80
|
| `^C` `^C` | chiude il deck | la prima pressione avverte in riga di stato, la seconda entro **5 secondi** esce; passata la finestra si riparte dall'avviso. Da dentro un modale il primo `^C` riporta alla lista — l'avviso vive nella riga di stato, che le schermate a pieno frame non disegnano |
|
|
@@ -96,6 +96,7 @@ Assegnazioni correnti:
|
|
|
96
96
|
| modale | `CANC` | **elimina** — la task selezionata; su una riga meta della vista `archiviabili`, l'intero insieme |
|
|
97
97
|
| modale | `^O` | apre il **project status** in cache (viewer fullscreen) |
|
|
98
98
|
| immediata | `^G` | **genera** il project status (skill headless, dura minuti) |
|
|
99
|
+
| immediata | `^U` | **bumpa il gitlink** dei submodule idonei (solo su un progetto con `.gitmodules`) |
|
|
99
100
|
| immediata | `^K` | spawna la task selezionata col prompt di **recap** |
|
|
100
101
|
| immediata | `^P` | spawna la task selezionata sul **preflight** |
|
|
101
102
|
| immediata | `^R` | spawna la task selezionata in **esecuzione** |
|
|
@@ -108,7 +109,7 @@ Assegnazioni correnti:
|
|
|
108
109
|
Il footer è **due righe con due nature diverse**:
|
|
109
110
|
|
|
110
111
|
```
|
|
111
|
-
⏎ detail · ^K/^P/^R spawn · ^G genera status · ^O apri status · ^F cerca · C nuova · E edit · S sort · F filtri · w salva
|
|
112
|
+
⏎ detail · ^K/^P/^R spawn · ^G genera status · ^O apri status · ^U bumpa gitlink · ^F cerca · C nuova · E edit · S sort · F filtri · w salva
|
|
112
113
|
t 💻 · c 🤖 · 1 📝 codium · 2 ☕ idea
|
|
113
114
|
```
|
|
114
115
|
|
|
@@ -274,6 +275,38 @@ Il recap vive in un file markdown sotto `/tmp/loom-deck-status-<uid>/`, col nome
|
|
|
274
275
|
|
|
275
276
|
A generazione finita arriva una notifica desktop (`notify-send`): la generazione dura minuti e nel frattempo si guarda altrove, e la notifica di GNOME persiste nel cassetto — cosa che un suono non farebbe. Senza `notify-send` installato non succede **niente**: nessuna riga, nessun messaggio. L'assenza di un binario è una proprietà stabile della macchina, non un evento, e segnalarla a ogni generazione sarebbe rumore ripetuto su una funzione accessoria mentre il deliverable vero è arrivato comunque.
|
|
276
277
|
|
|
278
|
+
### `^U` — il gitlink dei submodule
|
|
279
|
+
|
|
280
|
+
Su un progetto **cappello** (un repo che aggrega altri repo come submodule) ogni membro è pinnato a un commit fisso. Quando il membro avanza — un commit nel deck, nel plugin — quel puntatore resta indietro finché qualcuno non lo bumpa, e il gesto vive nella memoria di chi committa. Il deck lo mette sotto gli occhi e lo rende un tasto.
|
|
281
|
+
|
|
282
|
+
L'indicatore sta in coda alla riga della legenda, accanto agli altri due:
|
|
283
|
+
|
|
284
|
+
```
|
|
285
|
+
gitlink 2 wrap 77 [ Inbox 📄 3/1/2 ]
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
| Corpo | Significato |
|
|
289
|
+
|---|---|
|
|
290
|
+
| _(assente)_ | il progetto non ha `.gitmodules`: nessun timer, nessuno spawn, nessuna colonna spesa |
|
|
291
|
+
| `missing` | prima misura non ancora tornata |
|
|
292
|
+
| `?` | plugin loom-works non installato su questa macchina, o script in errore |
|
|
293
|
+
| `ok` | ogni membro coincide col proprio gitlink |
|
|
294
|
+
| `2` | due membri col checkout diverso dal gitlink |
|
|
295
|
+
| `⚠` in coda | l'ultimo bump è fallito, **oppure** un membro richiede un gesto che il bump non fa |
|
|
296
|
+
|
|
297
|
+
Nel numero entrano solo i membri che un bump può sistemare. Un submodule non inizializzato o in conflitto di merge resta fuori e si dice col glifo: sommarlo darebbe un contatore che il tasto non porta mai a zero, e un contatore così smette di essere letto.
|
|
298
|
+
|
|
299
|
+
**`^U` bumpa**, senza conferma: il commit è locale e si annulla con un `git reset` che non tocca nessun remote. Il deck non pusha il cappello — quel gesto resta al flusso normale, perché un gitlink pushato per sbaglio è già nella copia di chiunque abbia pullato.
|
|
300
|
+
|
|
301
|
+
Il deck **non classifica da sé**: spawna `bump-gitlink.sh` del plugin (in `--dry-run` per la misura, nudo per il bump) e mostra il TSV che ne esce. Nessuna sessione Claude in mezzo — è un `execFile` con esito sincrono, come il contatore inbox e lo scan hard-wrap. Lo stesso script si lancia a mano da shell, ed è lì che vivono le due guardie che rendono il bump sicuro:
|
|
302
|
+
|
|
303
|
+
- **il verso** — `git submodule status` marca con `+` un checkout diverso dal gitlink **in entrambi i versi**, senza distinguerli. Un cappello pullato senza `git submodule update` ha il checkout più vecchio del gitlink e porta lo stesso `+`: bumparlo pinnerebbe il cappello a un commit precedente. Quel membro si salta, e la riga di stato nomina `git submodule update`.
|
|
304
|
+
- **il remote** — il cappello può pinnare solo commit già raggiungibili dal remote del membro. Un gitlink verso un commit locale produce un cappello che si clona ma non si ricostruisce, e il difetto è invisibile a chi lo introduce: nella sua copia quel commit esiste. Quel membro si salta, e la riga di stato nomina `git -C <membro> push`.
|
|
305
|
+
|
|
306
|
+
La riga di stato dopo il tasto riporta **un esito per membro**, non un totale: nel caso misto — uno pushato, uno no — un riassunto direbbe «1 bumpato» e lascerebbe fuori proprio il comando che sblocca l'altro.
|
|
307
|
+
|
|
308
|
+
La misura gira ogni 30 secondi e subito dopo un bump. Il `--dry-run` non contatta la rete: il `fetch` che precede la guardia sul remote gira solo nel bump, quindi il contatore può dire «bumpabile» di un commit che il bump poi salta perché sparito dal remote. La guardia sta sull'azione, e un contatore non è un'autorizzazione.
|
|
309
|
+
|
|
277
310
|
## Vista: filtri e ordinamenti
|
|
278
311
|
|
|
279
312
|
La lista è una **vista** sulla `tasks.md`: si filtra e si ordina senza toccare il file.
|
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';
|
|
@@ -31,6 +32,7 @@ import { useAssignOverlay } from './overlays/assign.js';
|
|
|
31
32
|
import { useProjectStatus } from './overlays/status.js';
|
|
32
33
|
import { useInboxOverlay } from './overlays/inbox.js';
|
|
33
34
|
import { useWrapOverlay } from './overlays/wrap.js';
|
|
35
|
+
import { useGitlink } from './overlays/gitlink.js';
|
|
34
36
|
import { usePurgeOverlay } from './overlays/purge.js';
|
|
35
37
|
import { useTextModals, useViewModals } from './overlays/modals.js';
|
|
36
38
|
import { useTerminalSize } from './hooks.js';
|
|
@@ -109,6 +111,11 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
109
111
|
setNote,
|
|
110
112
|
onApply: actions.unwrapPath,
|
|
111
113
|
});
|
|
114
|
+
// T155 — il gitlink dei submodule. Terzo sensore con lo stato in vista
|
|
115
|
+
// normale, dopo il project status e l'hard-wrap: qui però non c'è nessuna
|
|
116
|
+
// schermata da aprire — l'unica superficie è l'indicatore, e l'unica azione
|
|
117
|
+
// committa.
|
|
118
|
+
const gitlink = useGitlink({ cwd, setNote });
|
|
112
119
|
const purge = usePurgeOverlay({
|
|
113
120
|
setMode,
|
|
114
121
|
setNote,
|
|
@@ -138,6 +145,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
138
145
|
status,
|
|
139
146
|
inbox,
|
|
140
147
|
wrap,
|
|
148
|
+
gitlink,
|
|
141
149
|
purge,
|
|
142
150
|
view: viewModals,
|
|
143
151
|
text: textModals,
|
|
@@ -162,10 +170,26 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
162
170
|
: 'none';
|
|
163
171
|
const detailParts = model.detail ? detailMetaOf(model.detail) : null;
|
|
164
172
|
const launch = launchRow(model.launch, columns);
|
|
173
|
+
// T154 — i bottoni del blocco selettore della nuda: stessa primitiva
|
|
174
|
+
// (`choiceButtons`) del selettore di resume/fork, sulle SHORT (D1) e con
|
|
175
|
+
// `avail` pari alla larghezza naturale del gruppo, così `cutParts` non
|
|
176
|
+
// taglia mai (il blocco è riservato, non conteso — vedi `launchRow`).
|
|
177
|
+
const bareButtons = choiceButtons(MODEL_SHORT_LIST, MODELS.indexOf(model.bareModel), BARE_BUTTONS_WIDTH, MODELS.indexOf(MODEL_DEFAULT));
|
|
165
178
|
// T134 — gli indicatori ancorati a destra della riga legenda. Si compongono
|
|
166
179
|
// PRIMA della legenda perché è la loro larghezza a decidere quanto ne resta:
|
|
167
180
|
// hanno la precedenza sul budget (D5 preflight).
|
|
168
181
|
const indicators = indicatorRow({
|
|
182
|
+
// T155 — `null` chiude il gate: su un progetto senza `.gitmodules` il
|
|
183
|
+
// segmento non esiste, e la riga legenda si riprende quelle colonne.
|
|
184
|
+
gitlink: gitlink.enabled
|
|
185
|
+
? {
|
|
186
|
+
count: gitlink.count,
|
|
187
|
+
attention: gitlink.attention,
|
|
188
|
+
scanned: gitlink.scanned,
|
|
189
|
+
ok: gitlink.ok,
|
|
190
|
+
failed: gitlink.failed,
|
|
191
|
+
}
|
|
192
|
+
: null,
|
|
169
193
|
inbox: {
|
|
170
194
|
counts: model.inboxCounts,
|
|
171
195
|
stale: model.inboxStale,
|
|
@@ -245,7 +269,8 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
245
269
|
purgeBulk: model.purgeBulk,
|
|
246
270
|
inboxPane: model.rightPane === 'inbox',
|
|
247
271
|
hasInbox: model.selInbox !== null,
|
|
248
|
-
|
|
272
|
+
gitlink: gitlink.enabled,
|
|
273
|
+
}), 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
274
|
}
|
|
250
275
|
const cwd = process.cwd();
|
|
251
276
|
// 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
|
|
@@ -83,6 +106,9 @@ export function deckLegend(state) {
|
|
|
83
106
|
// cosa e sta a due voci di distanza.
|
|
84
107
|
'^G genera status',
|
|
85
108
|
'^O apri status',
|
|
109
|
+
// T155 — la voce nomina il GESTO, come le due sopra: «gitlink» da solo
|
|
110
|
+
// direbbe l'oggetto e non che il tasto lo allinea con un commit.
|
|
111
|
+
...(state.gitlink ? ['^U bumpa gitlink'] : []),
|
|
86
112
|
// T134 — la voce nomina il pane che il tasto MONTA, non quello montato:
|
|
87
113
|
// `^B` scambia i due, e annunciare quello che si sta già guardando
|
|
88
114
|
// direbbe il contrario di ciò che il tasto fa. Stessa regola di `CANC
|
|
@@ -102,21 +128,46 @@ export function deckLegend(state) {
|
|
|
102
128
|
* seconde ri-splittando il primo sarebbe un conto parallelo, che diverge alla
|
|
103
129
|
* prima label che contenga il separatore.
|
|
104
130
|
*
|
|
105
|
-
* Le celle
|
|
106
|
-
* sulla riga → vanno riservate, o le voci launch la sfonderebbero di quel
|
|
131
|
+
* Le celle della surface (più il ` · ` che la separa dalle voci) sono già spese
|
|
132
|
+
* sulla riga → vanno riservate, o le voci launch la sfonderebbero di quel
|
|
133
|
+
* tanto. T154/P8 — il blocco selettore della nuda entra nella STESSA riserva:
|
|
134
|
+
* su un progetto con molte voci `launch` a cedere per prima è una voce custom,
|
|
135
|
+
* mai il selettore (un bottone di modello troncato sarebbe indistinguibile da
|
|
136
|
+
* un modello che non esiste, una voce launch resta raggiungibile col suo
|
|
137
|
+
* tasto-cifra anche quando non è scritta in riga).
|
|
107
138
|
*/
|
|
108
139
|
export function launchRow(launch, columns) {
|
|
109
140
|
const surfaceLegend = SURFACE_SEGMENTS.map((s) => s.text).join(LAUNCH_SEP);
|
|
110
|
-
const
|
|
141
|
+
const reserved = cellWidth(surfaceLegend) + cellWidth(LAUNCH_SEP) + BARE_BLOCK_WIDTH;
|
|
142
|
+
const legend = launchLegend(launch, columns, reserved);
|
|
111
143
|
const segments = [
|
|
112
144
|
...SURFACE_SEGMENTS,
|
|
113
145
|
...legend.taken.map((text, i) => ({ key: String(i + 1), text })),
|
|
114
146
|
];
|
|
147
|
+
// Larghezza REALE della sinistra, testo di overflow/irraggiungibili incluso:
|
|
148
|
+
// sono loro a decidere quanto riempimento serve, non solo i segmenti.
|
|
149
|
+
const leftWidth = termWidth(segments.map((s) => s.text).join(LAUNCH_SEP)) +
|
|
150
|
+
(legend.overflow > 0 ? termWidth(` · +${legend.overflow} fuori riga`) : 0) +
|
|
151
|
+
(legend.unreachable > 0
|
|
152
|
+
? termWidth(` · ${legend.unreachable} oltre la 9ª (non raggiungibili)`)
|
|
153
|
+
: 0);
|
|
154
|
+
// T154/P5 — la formula dell'ancoraggio: colonne del bordo esterno, meno la
|
|
155
|
+
// sinistra già disegnata, meno il blocco che deve starci per intero. Un
|
|
156
|
+
// pavimento di 1 tiene sempre almeno uno spazio fra le due metà, invece di
|
|
157
|
+
// farle toccare quando lo schermo è troppo stretto per il resto.
|
|
158
|
+
const filler = ' '.repeat(Math.max(1, columns - 6 - leftWidth - BARE_BLOCK_WIDTH));
|
|
159
|
+
const bareCol = FRAME_TEXT_COL + leftWidth + termWidth(filler);
|
|
160
|
+
const bareRegion = {
|
|
161
|
+
key: BARE_LABEL.key,
|
|
162
|
+
start: bareCol,
|
|
163
|
+
end: bareCol + termWidth(BARE_LABEL.text) - 1,
|
|
164
|
+
};
|
|
115
165
|
return {
|
|
116
166
|
segments,
|
|
117
|
-
regions: rowRegions(segments, LAUNCH_SEP, FRAME_TEXT_COL),
|
|
167
|
+
regions: [...rowRegions(segments, LAUNCH_SEP, FRAME_TEXT_COL), bareRegion],
|
|
118
168
|
overflow: legend.overflow,
|
|
119
169
|
unreachable: legend.unreachable,
|
|
170
|
+
filler,
|
|
120
171
|
};
|
|
121
172
|
}
|
|
122
173
|
/**
|
|
@@ -184,11 +235,47 @@ export function wrapIndicator(state) {
|
|
|
184
235
|
: String(state.count);
|
|
185
236
|
return sanitize(`wrap ${body}${state.ok ? '' : ` ${WARN}`}`);
|
|
186
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* T155 — l'indicatore del gitlink: `gitlink 2`.
|
|
240
|
+
*
|
|
241
|
+
* Conta i membri il cui checkout NON coincide col gitlink registrato, in
|
|
242
|
+
* entrambi i versi (`disalignedCount` in `gitlink.ts`). Fuori dal numero restano
|
|
243
|
+
* i membri che nessun bump sistema — non inizializzati, in conflitto — perché un
|
|
244
|
+
* contatore che il tasto non può portare a zero smette di essere letto: è la
|
|
245
|
+
* stessa regola per cui `misto` non entra nel contatore dell'hard-wrap.
|
|
246
|
+
*
|
|
247
|
+
* Quattro corpi distinti, come per gli altri due indicatori: `missing` finché la
|
|
248
|
+
* prima misura non è tornata, `?` quando lo script non è raggiungibile (plugin
|
|
249
|
+
* non installato su questa macchina, o uscito male), il numero quando c'è
|
|
250
|
+
* qualcosa da bumpare, `ok` quando tutti i membri coincidono.
|
|
251
|
+
*
|
|
252
|
+
* Il glifo di allerta si AGGIUNGE al corpo invece di sostituirlo, e copre i due
|
|
253
|
+
* fatti che il numero non dice: l'ultimo bump è fallito, oppure un membro
|
|
254
|
+
* richiede un gesto che il bump non fa. Entrambi restano veri accanto al
|
|
255
|
+
* conteggio, e chi lo vede preme il tasto per leggere la riga di stato, che
|
|
256
|
+
* nomina i membri.
|
|
257
|
+
*/
|
|
258
|
+
export function gitlinkIndicator(state) {
|
|
259
|
+
const body = !state.scanned
|
|
260
|
+
? STATUS_MISSING
|
|
261
|
+
: !state.ok
|
|
262
|
+
? '?'
|
|
263
|
+
: state.count > 0
|
|
264
|
+
? String(state.count)
|
|
265
|
+
: 'ok';
|
|
266
|
+
const warn = state.failed || state.attention > 0 ? ` ${WARN}` : '';
|
|
267
|
+
return sanitize(`gitlink ${body}${warn}`);
|
|
268
|
+
}
|
|
187
269
|
export function indicatorRow(state, columns) {
|
|
188
|
-
// Ordine di lettura:
|
|
189
|
-
// destro perché è l'unico dei
|
|
190
|
-
// normale — la lista del wrap è una schermata che si apre e si chiude.
|
|
270
|
+
// Ordine di lettura: gli indicatori, poi il bottone. Il bottone sta
|
|
271
|
+
// all'estremo destro perché è l'unico dei tre che MONTA qualcosa restando in
|
|
272
|
+
// vista normale — la lista del wrap è una schermata che si apre e si chiude.
|
|
273
|
+
//
|
|
274
|
+
// Il gitlink sta in TESTA al blocco, che è ancorato al bordo destro: è l'unico
|
|
275
|
+
// segmento che può mancare del tutto (il gate su `.gitmodules`), e da lì la
|
|
276
|
+
// sua assenza non sposta gli altri due.
|
|
191
277
|
const segments = [
|
|
278
|
+
...(state.gitlink ? [{ key: '^u', text: gitlinkIndicator(state.gitlink) }] : []),
|
|
192
279
|
{ key: '^w', text: wrapIndicator(state.wrap) },
|
|
193
280
|
{ key: '^b', text: inboxButton(state.inbox) },
|
|
194
281
|
];
|
package/dist/gitlink.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// T155 — il gitlink dei submodule come dato del deck.
|
|
2
|
+
//
|
|
3
|
+
// Il cappello pinna ogni membro a un commit fisso (gitlink `160000`). Quando un
|
|
4
|
+
// submodule avanza — un commit nel deck, nel plugin, in compass — il gitlink
|
|
5
|
+
// resta indietro finché qualcuno non lo bumpa a mano.
|
|
6
|
+
//
|
|
7
|
+
// LA CLASSIFICAZIONE NON STA QUI (P1 preflight). Il deck spawna
|
|
8
|
+
// `bump-gitlink.sh --dry-run` del plugin e mostra il TSV che ne esce: la stessa
|
|
9
|
+
// sequenza che poi committa è quella che ha misurato, quindi il sensore non può
|
|
10
|
+
// dire «bumpabile» in base a un criterio diverso da quello del bump. Replicarla
|
|
11
|
+
// in TypeScript darebbe due implementazioni delle due guardie — quella sul verso
|
|
12
|
+
// e quella sul remote — e una divergenza fra le due si vedrebbe solo come un
|
|
13
|
+
// cappello pinnato su un commit sbagliato, giorni dopo, nel clone di qualcun
|
|
14
|
+
// altro. Stesso confine di `inbox.ts` e `wrap-scan.ts`, e non quello di
|
|
15
|
+
// `purge.ts`, che passa da una sessione Claude: qui non c'è nessun prompt, solo
|
|
16
|
+
// un `execFile` con esito sincrono.
|
|
17
|
+
//
|
|
18
|
+
// Lettore puro + spawn: nessun React, nessuna resa. È l'unico modo di provarlo
|
|
19
|
+
// senza pseudo-terminale.
|
|
20
|
+
import { execFile } from 'node:child_process';
|
|
21
|
+
import { existsSync } from 'node:fs';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
import { promisify } from 'node:util';
|
|
24
|
+
import { pluginScript } from './plugin-cache.js';
|
|
25
|
+
const execFileAsync = promisify(execFile);
|
|
26
|
+
export const GITLINK_SCRIPT = 'scripts/utils/bump-gitlink.sh';
|
|
27
|
+
/**
|
|
28
|
+
* Ogni 30 secondi (P2 preflight).
|
|
29
|
+
*
|
|
30
|
+
* Il gate che regge gli altri poll stretti del deck qui NON funziona: il
|
|
31
|
+
* disallineamento nasce da un commit dentro il submodule, che lascia fermi sia
|
|
32
|
+
* `HEAD` del cappello (`commit-times.ts`) sia la mtime dei task file
|
|
33
|
+
* (`epic-hierarchy.ts`) — non c'è niente di economico da misurare prima. Resta
|
|
34
|
+
* il costo nudo, misurato sul cappello a tre submodule: 0,03s per giro, dentro
|
|
35
|
+
* il budget che T153 ha fissato per il loop.
|
|
36
|
+
*
|
|
37
|
+
* Il dry-run NON contatta la rete (P11): la guardia col `fetch` gira solo nel
|
|
38
|
+
* bump. Ne discende che il sensore può contare «bumpabile» un commit che il
|
|
39
|
+
* bump poi salta perché sparito dal remote — la guardia sta sull'AZIONE, e un
|
|
40
|
+
* contatore non è un'autorizzazione.
|
|
41
|
+
*/
|
|
42
|
+
export const GITLINK_SCAN_INTERVAL_MS = 30 * 1000;
|
|
43
|
+
/**
|
|
44
|
+
* Gli stati che lo script emette, più `sconosciuto` che è del solo lettore.
|
|
45
|
+
*
|
|
46
|
+
* I primi tre vengono dai prefissi di `git submodule status`; gli altri
|
|
47
|
+
* spacchettano il prefisso `+`, che da solo NON dice il verso — `man
|
|
48
|
+
* git-submodule`: «`+` if the currently checked out submodule commit does not
|
|
49
|
+
* match the SHA-1 found in the index». Un checkout più vecchio del gitlink
|
|
50
|
+
* (cappello pullato senza `git submodule update`) porta lo stesso `+` di un
|
|
51
|
+
* commit nuovo nel membro, e le due situazioni hanno rimedi opposti.
|
|
52
|
+
*
|
|
53
|
+
* `bumpato` esiste solo nell'output del bump: nel dry-run un membro idoneo resta
|
|
54
|
+
* `avanti`. Sono due fatti diversi — «lo bumperei» e «l'ho bumpato» — e un solo
|
|
55
|
+
* nome per entrambi renderebbe la riga di stato indistinguibile da un'anteprima.
|
|
56
|
+
*/
|
|
57
|
+
export const GITLINK_STATES = [
|
|
58
|
+
'allineato',
|
|
59
|
+
'non-inizializzato',
|
|
60
|
+
'conflitto',
|
|
61
|
+
'avanti',
|
|
62
|
+
'indietro',
|
|
63
|
+
'non-pushato',
|
|
64
|
+
'remote-irraggiungibile',
|
|
65
|
+
'bumpato',
|
|
66
|
+
];
|
|
67
|
+
const KNOWN = new Set(GITLINK_STATES);
|
|
68
|
+
/**
|
|
69
|
+
* Il TSV di `bump-gitlink.sh`: una riga per submodule, `path ⇥ stato ⇥ dettaglio`.
|
|
70
|
+
* Nessuna riga di intestazione — il primo campo è già un path.
|
|
71
|
+
*
|
|
72
|
+
* Uno stato che il deck non conosce diventa `sconosciuto` invece di sparire: la
|
|
73
|
+
* riga la si è comunque ricevuta, e scartarla farebbe calare il contatore ogni
|
|
74
|
+
* volta che lo script guadagna uno stato nuovo — cioè proprio quando il deck ha
|
|
75
|
+
* più bisogno di dirlo. Il dettaglio è l'ULTIMO campo e si ricompone: può
|
|
76
|
+
* contenere un comando, e un comando può contenere un tab.
|
|
77
|
+
*/
|
|
78
|
+
export function parseGitlinkTsv(stdout) {
|
|
79
|
+
const out = [];
|
|
80
|
+
for (const line of stdout.split('\n')) {
|
|
81
|
+
if (!line.trim())
|
|
82
|
+
continue;
|
|
83
|
+
const c = line.split('\t');
|
|
84
|
+
if (c.length < 2 || !c[0])
|
|
85
|
+
continue;
|
|
86
|
+
out.push({
|
|
87
|
+
path: c[0],
|
|
88
|
+
state: KNOWN.has(c[1]) ? c[1] : 'sconosciuto',
|
|
89
|
+
detail: c.slice(2).join('\t'),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Gli stati che il contatore somma: i membri il cui checkout non coincide col
|
|
96
|
+
* gitlink, in entrambi i versi.
|
|
97
|
+
*
|
|
98
|
+
* `non-inizializzato` e `conflitto` restano FUORI per decisione: nessun bump li
|
|
99
|
+
* sistema — il primo vuole un `git submodule update --init`, il secondo la
|
|
100
|
+
* risoluzione di un merge — e sommarli darebbe un numero che il tasto non può
|
|
101
|
+
* portare a zero. Un contatore che non arriva mai a zero smette di essere letto
|
|
102
|
+
* (stessa regola di `misto` nell'indicatore hard-wrap). Si dicono comunque, col
|
|
103
|
+
* glifo di allerta accanto al numero.
|
|
104
|
+
*/
|
|
105
|
+
const DISALIGNED = new Set([
|
|
106
|
+
'avanti',
|
|
107
|
+
'indietro',
|
|
108
|
+
'non-pushato',
|
|
109
|
+
'remote-irraggiungibile',
|
|
110
|
+
]);
|
|
111
|
+
/** Quanti membri hanno il checkout diverso dal gitlink: è il numero a schermo. */
|
|
112
|
+
export function disalignedCount(members) {
|
|
113
|
+
return members.filter((m) => DISALIGNED.has(m.state)).length;
|
|
114
|
+
}
|
|
115
|
+
/** Quanti sono bumpabili adesso: il solo `avanti` passa entrambe le guardie. */
|
|
116
|
+
export function bumpableCount(members) {
|
|
117
|
+
return members.filter((m) => m.state === 'avanti').length;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Qualcosa richiede attenzione ma nessun bump lo sistema: un membro non
|
|
121
|
+
* inizializzato, un conflitto di merge, uno stato che questo deck non conosce.
|
|
122
|
+
* Vale il glifo di allerta, mai una cifra nel contatore.
|
|
123
|
+
*/
|
|
124
|
+
export function attentionCount(members) {
|
|
125
|
+
return members.filter((m) => m.state === 'non-inizializzato' || m.state === 'conflitto' || m.state === 'sconosciuto').length;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Gli stati che il bump ha SALTATO: il membro è disallineato ma una delle due
|
|
129
|
+
* guardie l'ha fermato, e il rimedio non è un bump ma il gesto che il dettaglio
|
|
130
|
+
* nomina.
|
|
131
|
+
*/
|
|
132
|
+
const SKIPPED = new Set(['indietro', 'non-pushato', 'remote-irraggiungibile']);
|
|
133
|
+
/**
|
|
134
|
+
* La riga di stato dopo un bump: un esito per membro, non un totale.
|
|
135
|
+
*
|
|
136
|
+
* Il caso che rende necessario nominarli è il MISTO — un membro pushato e uno
|
|
137
|
+
* no. Lì un messaggio riassuntivo direbbe «1 bumpato» e lascerebbe fuori proprio
|
|
138
|
+
* l'informazione che serve ad agire: quale membro è rimasto indietro e con quale
|
|
139
|
+
* comando lo si sblocca. Il gesto arriva già scritto dallo script, che è dove la
|
|
140
|
+
* guardia lo ha deciso: comporlo qui darebbe due frasi da tenere allineate.
|
|
141
|
+
*
|
|
142
|
+
* I bumpati vengono per primi — è l'esito che il tasto prometteva — e i saltati
|
|
143
|
+
* dopo, ognuno col proprio glifo di allerta. I membri allineati non compaiono:
|
|
144
|
+
* la riga di stato dice cosa è successo, non cosa esiste.
|
|
145
|
+
*/
|
|
146
|
+
export function gitlinkNote(members) {
|
|
147
|
+
const done = members
|
|
148
|
+
.filter((m) => m.state === 'bumpato')
|
|
149
|
+
.map((m) => `✔ ${m.path} ${m.detail}`.trim());
|
|
150
|
+
const skipped = members
|
|
151
|
+
.filter((m) => SKIPPED.has(m.state))
|
|
152
|
+
.map((m) => `⚠ ${m.path}: ${m.detail}`);
|
|
153
|
+
return [...done, ...skipped].join(' · ');
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Il progetto ha submodule da misurare.
|
|
157
|
+
*
|
|
158
|
+
* È il gate di ATTIVAZIONE dell'intero sensore (DLV2): senza `.gitmodules` non
|
|
159
|
+
* parte nessun timer, non si spawna niente e l'indicatore non compare affatto —
|
|
160
|
+
* non «0», che occuperebbe colonne per dire che la funzione non si applica.
|
|
161
|
+
*
|
|
162
|
+
* Il file, non `git submodule status`: rispondere alla domanda «ci sono
|
|
163
|
+
* submodule?» con lo stesso spawn che si vuole evitare sarebbe la misura che il
|
|
164
|
+
* gate esiste per non pagare.
|
|
165
|
+
*/
|
|
166
|
+
export function hasGitmodules(projectRoot) {
|
|
167
|
+
return existsSync(join(projectRoot, '.gitmodules'));
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Invoca lo script e ne parsa l'output.
|
|
171
|
+
*
|
|
172
|
+
* I modi di non avere il dato — plugin non installato su questa macchina,
|
|
173
|
+
* script in errore, git muto — collassano in un solo `ok: false`, come per la
|
|
174
|
+
* coda inbox: a schermo non cambiano la prima mossa di chi li vede. Nessuno dei
|
|
175
|
+
* due è un throw, un indicatore informativo non può rompere il deck.
|
|
176
|
+
*
|
|
177
|
+
* Lo stdout si legge ANCHE quando l'exit è non-zero: lo script classifica tutti
|
|
178
|
+
* i membri prima di agire, quindi su un errore parziale (un `git` che fallisce
|
|
179
|
+
* su un membro) le righe già emesse restano vere e dicono più di una schermata
|
|
180
|
+
* vuota.
|
|
181
|
+
*/
|
|
182
|
+
async function run(projectRoot, args) {
|
|
183
|
+
const script = pluginScript(GITLINK_SCRIPT);
|
|
184
|
+
if (!script)
|
|
185
|
+
return { members: [], ok: false };
|
|
186
|
+
try {
|
|
187
|
+
const { stdout } = await execFileAsync(script, args, {
|
|
188
|
+
cwd: projectRoot,
|
|
189
|
+
maxBuffer: 1024 * 1024,
|
|
190
|
+
});
|
|
191
|
+
return { members: parseGitlinkTsv(stdout), ok: true };
|
|
192
|
+
}
|
|
193
|
+
catch (e) {
|
|
194
|
+
const stdout = e.stdout ?? '';
|
|
195
|
+
return { members: parseGitlinkTsv(stdout), ok: false };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/** La misura, senza toccare niente: nessun fetch, nessun commit. */
|
|
199
|
+
export function scanGitlink(projectRoot) {
|
|
200
|
+
return run(projectRoot, ['--dry-run']);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Il bump vero: fetch, due guardie, commit unico coi soli membri idonei. Il
|
|
204
|
+
* cappello NON viene pushato — quel gesto resta al flusso normale, perché un
|
|
205
|
+
* gitlink committato per sbaglio si annulla con un `git reset` che non tocca
|
|
206
|
+
* nessun remote, mentre uno pushato è già nella copia di chiunque abbia pullato.
|
|
207
|
+
*/
|
|
208
|
+
export function bumpGitlink(projectRoot) {
|
|
209
|
+
return run(projectRoot, []);
|
|
210
|
+
}
|
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
|
@@ -322,6 +322,19 @@ export function useDeckInput({ cwd, tasksDir, mode, setMode, setNote, model, act
|
|
|
322
322
|
else if (input === 'e') {
|
|
323
323
|
overlays.wrap.scan();
|
|
324
324
|
}
|
|
325
|
+
else if (input === 'u') {
|
|
326
|
+
// T155/P5 preflight — `^U` (update del gitlink) bumpa i submodule
|
|
327
|
+
// idonei. Non è la coppia genera/apri di `^G`/`^O` e `^W`/`^E`: qui non
|
|
328
|
+
// c'è nessuna schermata da aprire, la misura gira da sé ogni 30 secondi
|
|
329
|
+
// e l'unico gesto è l'azione. Nessuna conferma, perché il commit è
|
|
330
|
+
// locale e reversibile (P6).
|
|
331
|
+
//
|
|
332
|
+
// `^B` era la mnemonica migliore per «bump» ed è di T134 (scambia i due
|
|
333
|
+
// pane dello slot destro). `^U` svuota un campo dentro i modi capturing,
|
|
334
|
+
// ma quelli consumano l'input per intero e non vedono mai questo ramo —
|
|
335
|
+
// stesso doppio significato per contesto già in esercizio su `^F`.
|
|
336
|
+
overlays.gitlink.bump();
|
|
337
|
+
}
|
|
325
338
|
else if (input === 'b') {
|
|
326
339
|
// T134/D8 preflight — `^B` (box/bacheca) scambia i due pane dello slot
|
|
327
340
|
// destro. Non è un modale né una schermata: il pane resta uno dei due
|
|
@@ -467,9 +480,16 @@ export function useDeckInput({ cwd, tasksDir, mode, setMode, setNote, model, act
|
|
|
467
480
|
actions.togglePin();
|
|
468
481
|
}
|
|
469
482
|
else if (input === 'm') {
|
|
470
|
-
// T148 —
|
|
471
|
-
//
|
|
472
|
-
|
|
483
|
+
// T148/T154 — bersaglio deciso dal focus E dalla selezione (P2): con
|
|
484
|
+
// una riga sessioni selezionata (stale compresa, P3) scorre il modello
|
|
485
|
+
// di resume/fork di quella riga; altrove (focus task, focus inbox,
|
|
486
|
+
// pane sessioni senza selezione) scorre il modello della sessione nuda.
|
|
487
|
+
if (model.focus === 'sessions' && model.selSessionId) {
|
|
488
|
+
actions.cycleResumeModel();
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
actions.cycleBareModel();
|
|
492
|
+
}
|
|
473
493
|
}
|
|
474
494
|
else if (input === 'N') {
|
|
475
495
|
// T53 — nota sulla conversazione selezionata. MAIUSCOLA perché apre un
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// T155 — il sensore del gitlink: stato, gate di attivazione, cadenza.
|
|
2
|
+
//
|
|
3
|
+
// Non è un overlay come `sheet` o `search` — non apre nessuna schermata e non
|
|
4
|
+
// entra in `MODE_KEYS`. Sta qui per la stessa ragione di `useProjectStatus` e
|
|
5
|
+
// `useWrapOverlay`: tiene uno stato che si legge in vista normale e possiede
|
|
6
|
+
// l'azione che quello stato abilita, e `hooks.ts` è già a 487 righe (P3
|
|
7
|
+
// preflight, D1/P12 — nessuno split dentro questa task).
|
|
8
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
9
|
+
import { attentionCount, bumpableCount, bumpGitlink, disalignedCount, gitlinkNote, GITLINK_SCAN_INTERVAL_MS, hasGitmodules, scanGitlink, } from '../gitlink.js';
|
|
10
|
+
export function useGitlink(deps) {
|
|
11
|
+
const { cwd, setNote } = deps;
|
|
12
|
+
/**
|
|
13
|
+
* IL GATE. Senza `.gitmodules` il progetto non ha submodule, quindi non c'è
|
|
14
|
+
* niente da misurare: nessun timer parte, nessuno spawn viene fatto, e
|
|
15
|
+
* l'indicatore non compare affatto — non «0», che occuperebbe colonne della
|
|
16
|
+
* riga legenda per dire che la funzione non si applica.
|
|
17
|
+
*
|
|
18
|
+
* Misurato UNA VOLTA per project root e non a ogni giro: un repo non guadagna
|
|
19
|
+
* submodule mentre il deck è aperto, e se lo facesse basta riavviarlo. È lo
|
|
20
|
+
* stesso limite accettato dalla cache del plugin (`plugin-cache.ts`), scritto
|
|
21
|
+
* qui invece che scoperto dopo.
|
|
22
|
+
*/
|
|
23
|
+
const enabled = useMemo(() => hasGitmodules(cwd), [cwd]);
|
|
24
|
+
const [members, setMembers] = useState([]);
|
|
25
|
+
const [ok, setOk] = useState(true);
|
|
26
|
+
/** Almeno un tentativo è stato fatto: distingue «non ho ancora misurato» da
|
|
27
|
+
* «ho misurato e sono tutti allineati», che a schermo sono due cose diverse. */
|
|
28
|
+
const [scanned, setScanned] = useState(false);
|
|
29
|
+
const [epoch, setEpoch] = useState(0);
|
|
30
|
+
const [bumping, setBumping] = useState(false);
|
|
31
|
+
/** L'ultimo bump si è fermato prima di committare. STICKY: un giro di misura
|
|
32
|
+
* riuscito non lo azzera — dice com'è andata l'ultima AZIONE, che è un fatto
|
|
33
|
+
* diverso da com'è andata l'ultima misura, e i due restano entrambi veri.
|
|
34
|
+
* Stessa regola del `failed` del project status. */
|
|
35
|
+
const [failed, setFailed] = useState(false);
|
|
36
|
+
// Il flag di corsa sta in un ref e non in stato, come in `useInboxScan`: due
|
|
37
|
+
// richieste ravvicinate girano nello stesso tick di React, e un valore di
|
|
38
|
+
// stato arriverebbe al render dopo — cioè troppo tardi per impedire il
|
|
39
|
+
// secondo spawn.
|
|
40
|
+
const busy = useRef(false);
|
|
41
|
+
// `alive` vive fuori dall'effect perché la richiesta forzata sopravvive al
|
|
42
|
+
// giro che l'ha ospitata: la scatena un gesto, non l'effect.
|
|
43
|
+
const alive = useRef(true);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
alive.current = true;
|
|
46
|
+
return () => {
|
|
47
|
+
alive.current = false;
|
|
48
|
+
};
|
|
49
|
+
}, []);
|
|
50
|
+
const run = useCallback(() => {
|
|
51
|
+
if (busy.current)
|
|
52
|
+
return;
|
|
53
|
+
busy.current = true;
|
|
54
|
+
scanGitlink(cwd).then((res) => {
|
|
55
|
+
busy.current = false;
|
|
56
|
+
if (!alive.current)
|
|
57
|
+
return;
|
|
58
|
+
// Un tentativo fallito NON butta via l'esito dell'ultimo riuscito: le
|
|
59
|
+
// righe precedenti restano vere, e il guasto si dice col glifo di allerta
|
|
60
|
+
// accanto al numero. Stessa regola del project status e della coda inbox.
|
|
61
|
+
setOk(res.ok);
|
|
62
|
+
setScanned(true);
|
|
63
|
+
if (res.ok || res.members.length > 0)
|
|
64
|
+
setMembers(res.members);
|
|
65
|
+
});
|
|
66
|
+
}, [cwd]);
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!enabled)
|
|
69
|
+
return;
|
|
70
|
+
run();
|
|
71
|
+
const id = setInterval(run, GITLINK_SCAN_INTERVAL_MS);
|
|
72
|
+
return () => clearInterval(id);
|
|
73
|
+
}, [enabled, run, epoch]);
|
|
74
|
+
/**
|
|
75
|
+
* Rimisura adesso e riparte col timer da zero.
|
|
76
|
+
*
|
|
77
|
+
* Il reset non è cosmetico: senza, l'intervallo continuerebbe a scorrere
|
|
78
|
+
* dall'ultimo tick automatico e potrebbe rimisurare pochi istanti dopo un
|
|
79
|
+
* dato appena misurato — costo pieno, risultato identico. Stessa forma di
|
|
80
|
+
* `useWrapScan` e `useInboxScan`.
|
|
81
|
+
*/
|
|
82
|
+
const refresh = useCallback(() => {
|
|
83
|
+
setEpoch((e) => e + 1);
|
|
84
|
+
}, []);
|
|
85
|
+
const count = disalignedCount(members);
|
|
86
|
+
const bumpable = bumpableCount(members);
|
|
87
|
+
/**
|
|
88
|
+
* Il bump vero — l'unica azione del sensore.
|
|
89
|
+
*
|
|
90
|
+
* NESSUNA CONFERMA (P6 preflight): il commit è locale e si annulla con un
|
|
91
|
+
* `git reset` che non tocca nessun remote, e lo script non pusha il cappello.
|
|
92
|
+
* È la stessa forma di `^G`, che genera senza chiedere.
|
|
93
|
+
*
|
|
94
|
+
* I quattro rifiuti dicono SEMPRE perché (AC: il tasto su zero disallineati
|
|
95
|
+
* non spawna niente e lo dice). Un tasto che non facesse niente in silenzio
|
|
96
|
+
* sarebbe indistinguibile da un binding mancante.
|
|
97
|
+
*/
|
|
98
|
+
function bump() {
|
|
99
|
+
if (!enabled) {
|
|
100
|
+
setNote('^U → questo progetto non ha submodule: nessun gitlink da bumpare');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (busy.current || bumping) {
|
|
104
|
+
setNote('^U → bump del gitlink già in corso');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (!scanned) {
|
|
108
|
+
setNote('^U → gitlink non ancora misurato');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (bumpable === 0) {
|
|
112
|
+
const why = gitlinkNote(members);
|
|
113
|
+
setNote(count === 0
|
|
114
|
+
? '^U → gitlink già allineato: niente da bumpare'
|
|
115
|
+
: `^U → nessun membro bumpabile · ${why}`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
busy.current = true;
|
|
119
|
+
setBumping(true);
|
|
120
|
+
setNote(`⏳ bump del gitlink su ${bumpable} membro${bumpable > 1 ? 'i' : ''}…`);
|
|
121
|
+
bumpGitlink(cwd).then((res) => {
|
|
122
|
+
busy.current = false;
|
|
123
|
+
if (!alive.current)
|
|
124
|
+
return;
|
|
125
|
+
setBumping(false);
|
|
126
|
+
setOk(res.ok);
|
|
127
|
+
setScanned(true);
|
|
128
|
+
setFailed(!res.ok);
|
|
129
|
+
if (res.members.length > 0)
|
|
130
|
+
setMembers(res.members);
|
|
131
|
+
const note = gitlinkNote(res.members);
|
|
132
|
+
setNote(note || (res.ok ? '✔ gitlink: nessun membro bumpato' : '⚠ bump del gitlink fallito'));
|
|
133
|
+
// Rimisura: l'output del bump porta già gli stati nuovi, ma il giro
|
|
134
|
+
// successivo li conferma da `git submodule status` invece che dalla
|
|
135
|
+
// memoria di ciò che si è appena fatto — ed è ciò che fa scendere il
|
|
136
|
+
// contatore senza riavviare il deck anche quando il commit ha toccato più
|
|
137
|
+
// di quanto lo script credeva.
|
|
138
|
+
refresh();
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
enabled,
|
|
143
|
+
members,
|
|
144
|
+
ok,
|
|
145
|
+
scanned,
|
|
146
|
+
bumping,
|
|
147
|
+
failed,
|
|
148
|
+
count,
|
|
149
|
+
bumpable,
|
|
150
|
+
attention: attentionCount(members),
|
|
151
|
+
bump,
|
|
152
|
+
refresh,
|
|
153
|
+
};
|
|
154
|
+
}
|
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