@lamemind/loom-deck 0.64.0 → 0.65.1
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/layout.js +17 -0
- package/dist/spawn.js +47 -15
- package/dist/ui/preview.js +9 -4
- package/package.json +1 -1
- package/scripts/deck-run +30 -4
package/dist/layout.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
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
|
+
import { SID_CHARS } from './glyphs.js';
|
|
6
7
|
/**
|
|
7
8
|
* Lunghezza in CODE POINT. Il caret ci indicizza sopra: `.length` conterebbe le
|
|
8
9
|
* code unit UTF-16 e un'emoji nel titolo varrebbe 2 posizioni, cioè un cursore
|
|
@@ -96,6 +97,22 @@ export function assignTextWidth(columns) {
|
|
|
96
97
|
export function previewTextWidth(columns) {
|
|
97
98
|
return Math.max(10, (columns || 80) - 8);
|
|
98
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* I tre pezzi della riga d'apertura del blocco preview di una conversazione —
|
|
102
|
+
* sid, nota, titolo — ognuno col proprio separatore in coda, pronti per
|
|
103
|
+
* `cutParts` sul budget di `previewTextWidth`.
|
|
104
|
+
*
|
|
105
|
+
* Esistono come funzione, e il taglio passa da `cutParts`, per l'invariante ③
|
|
106
|
+
* (§`assignTextWidth` sopra): lasciare la riga a `wrap="truncate-end"` la fa
|
|
107
|
+
* tagliare da `cli-truncate`, che sfora di una colonna per emoji e mangia il
|
|
108
|
+
* bordo destro del box. Due dei tre pezzi portano testo libero — la nota la
|
|
109
|
+
* scrive l'utente, e un titolo di conversazione auto-generato apre col prefisso
|
|
110
|
+
* `[ <emoji> ]` — quindi le emoji su questa riga sono il caso normale, non il
|
|
111
|
+
* limite.
|
|
112
|
+
*/
|
|
113
|
+
export function previewTitleParts(sessionId, note, title) {
|
|
114
|
+
return [`${sessionId.slice(0, SID_CHARS)} `, note ? `${note} ` : '', title];
|
|
115
|
+
}
|
|
99
116
|
/**
|
|
100
117
|
* Larghezza del TESTO dentro un pane al 50%: box esterno (2 bordi + 2 padding)
|
|
101
118
|
* → metà → bordo + padding del pane.
|
package/dist/spawn.js
CHANGED
|
@@ -191,20 +191,34 @@ export const ACTION_HOTKEYS = Object.fromEntries(DETAIL_ACTIONS.map((a, i) => [a
|
|
|
191
191
|
// fallback (`spawnForTask` in actions.ts) arriva il kind — già specializzato
|
|
192
192
|
// quando la chiamata viene dal detail (`recap-task`/`recap-epic`) — mai la
|
|
193
193
|
// label del catalogo.
|
|
194
|
-
// D2/P7 — `none` (label "open") non ha
|
|
195
|
-
// inventargliene
|
|
196
|
-
// P3 — le tre varianti di recap condividono
|
|
194
|
+
// D2/P7 — `none` (label "open") non ha un prefisso: è "nessuna azione", e
|
|
195
|
+
// inventargliene uno contraddirebbe l'intenzione dell'utente.
|
|
196
|
+
// P3 — le tre varianti di recap condividono il prefisso: la specializzazione
|
|
197
197
|
// sceglie quale skill parte, non cosa l'utente sta chiedendo.
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
198
|
+
//
|
|
199
|
+
// T156 — emoji invece della parola in maiuscolo (`PREFL`, `RUN`, …): il titolo
|
|
200
|
+
// si legge in lista deck e nella tab Ptyxis, dove l'azione va riconosciuta a
|
|
201
|
+
// colpo d'occhio e non letta.
|
|
202
|
+
// Le quattro sono ASTRALI (code point ≥ U+10000) per necessità, non per gusto:
|
|
203
|
+
// `sanitize` (`src/width.ts`) sostituisce con `·` ogni glifo su cui le due
|
|
204
|
+
// contabilità di larghezza divergono, e le emoji del BMP a presentazione testo
|
|
205
|
+
// (⚙️ ⏱️ 🛠️ …) divergono sempre — string-width le conta 2, VTE ne disegna 1, e
|
|
206
|
+
// il VS16 non sposta nessuno dei due verdetti. Un'emoji aggiunta qui va prima
|
|
207
|
+
// passata in `sanitize` per vedere se sopravvive.
|
|
208
|
+
// Le stesse quattro sono anche l'unica classe di emoji che `_sane_note` di
|
|
209
|
+
// `deck-run` lascia passare nel titolo della tab: la whitelist è una lista
|
|
210
|
+
// chiusa che nomina questi code point (P2 preflight), quindi cambiare una voce
|
|
211
|
+
// qui e non là riapre la divergenza fra titolo in lista e titolo in tab.
|
|
212
|
+
const ACTION_EMOJI = {
|
|
213
|
+
preflight: '📐',
|
|
214
|
+
run: '🚀',
|
|
215
|
+
recap: '📊',
|
|
216
|
+
'recap-task': '📊',
|
|
217
|
+
'recap-epic': '📊',
|
|
218
|
+
checkpoint: '🏁',
|
|
205
219
|
};
|
|
206
220
|
/**
|
|
207
|
-
* Titolo di fallback per una conversazione con nota vuota: `{
|
|
221
|
+
* Titolo di fallback per una conversazione con nota vuota: `[ {emoji} ] {slug}`,
|
|
208
222
|
* o il solo slug per `none` (D1/D2 preflight). Lo slug viene dal NOME del
|
|
209
223
|
* task file (`findTaskFile`), non dalla descrizione di `tasks.md`: il nome è
|
|
210
224
|
* già dentro l'alfabeto di `_sane_note` per costruzione — minuscolo, separato
|
|
@@ -212,6 +226,11 @@ const ACTION_WORD = {
|
|
|
212
226
|
* `/` che la riduzione toglie senza sostituto, saldando le parole (D1
|
|
213
227
|
* razionale).
|
|
214
228
|
*
|
|
229
|
+
* Il PREFISSO invece nell'alfabeto non ci sta per costruzione: emoji e quadre
|
|
230
|
+
* ci entrano solo perché la whitelist di `_sane_note` le nomina una per una
|
|
231
|
+
* (T156). È l'unica parte del titolo di fallback che dipende da una modifica
|
|
232
|
+
* fatta dall'altro lato.
|
|
233
|
+
*
|
|
215
234
|
* `null` quando il task file non si trova: il chiamante decide se ripiegare
|
|
216
235
|
* su nota vuota o su un altro fallback.
|
|
217
236
|
*/
|
|
@@ -220,8 +239,8 @@ export function fallbackTitle(tasksDir, id, kind) {
|
|
|
220
239
|
if (!path)
|
|
221
240
|
return null;
|
|
222
241
|
const slug = basename(path, '.md').slice(id.length + 1).replace(/-/g, ' ');
|
|
223
|
-
const
|
|
224
|
-
return
|
|
242
|
+
const emoji = ACTION_EMOJI[kind];
|
|
243
|
+
return emoji ? `[ ${emoji} ] ${slug}` : slug;
|
|
225
244
|
}
|
|
226
245
|
// Spawn detached: il deck spawna ma NON contiene la sessione (la possiede
|
|
227
246
|
// ptyxis-agent). unref + stdio ignore → ritorna subito, la TUI resta viva.
|
|
@@ -283,11 +302,24 @@ export function spawnDeck(id, cwd, sessionId, kind, model = MODEL_DEFAULT, spawn
|
|
|
283
302
|
// default (P2 preflight). Qui però non c'è un default da cablare: chi
|
|
284
303
|
// chiama ha già risolto il valore — il modello della conversazione d'origine,
|
|
285
304
|
// o quello scelto al suo posto — e lo passa come argomento obbligatorio.
|
|
305
|
+
// La NOTA sta PRIMA di `--resume`, non in coda, ed è l'unico argomento di questa
|
|
306
|
+
// forma che non segue l'ordine di lettura. La ragione è la riga di stato: il
|
|
307
|
+
// comando ci arriva tagliato al mezzo (`noteCommand`, che tiene un terzo di
|
|
308
|
+
// testa e due terzi di coda), e `--title-note` è l'unico argomento a lunghezza
|
|
309
|
+
// LIBERA — porta il titolo della conversazione, che l'utente scrive come vuole.
|
|
310
|
+
// In coda si mangia i due terzi riservati, e a spingere fuori è proprio
|
|
311
|
+
// `--resume <uuid>`, cioè l'unica cosa che distingue una ripresa da un'altra: su
|
|
312
|
+
// una conversazione dal titolo lungo la riga di stato finiva per dire quale
|
|
313
|
+
// modello e quale titolo, mai quale conversazione. Spostata qui, la coda porta
|
|
314
|
+
// sempre `--resume <uuid> --model <alias>` — sessanta colonne su un budget di
|
|
315
|
+
// settantasei, indipendenti dal titolo. `deck-run` legge i flag in qualunque
|
|
316
|
+
// ordine (un solo posizionale, il TaskID), quindi lo spostamento non cambia
|
|
317
|
+
// niente per chi esegue.
|
|
286
318
|
export function resumeArgs(taskId, sessionId, model, note) {
|
|
287
|
-
const args = taskId ? [taskId
|
|
288
|
-
args.push('--model', model);
|
|
319
|
+
const args = taskId ? [taskId] : ['--no-task'];
|
|
289
320
|
if (note)
|
|
290
321
|
args.push('--title-note', note);
|
|
322
|
+
args.push('--resume', sessionId, '--model', model);
|
|
291
323
|
return args;
|
|
292
324
|
}
|
|
293
325
|
export function spawnDeckResume(taskId, cwd, sessionId, model, note) {
|
package/dist/ui/preview.js
CHANGED
|
@@ -2,9 +2,9 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
// Blocco preview a piena larghezza sotto i due pane: mostra la task o la
|
|
3
3
|
// conversazione selezionata a seconda del pane a fuoco.
|
|
4
4
|
import { Box, Text } from 'ink';
|
|
5
|
-
import { cut, wrapLines } from '../width.js';
|
|
6
|
-
import { previewTextWidth } from '../layout.js';
|
|
7
|
-
import { LIVE_BUSY, LIVE_IDLE, META_KEYS,
|
|
5
|
+
import { cut, cutParts, wrapLines } from '../width.js';
|
|
6
|
+
import { previewTextWidth, previewTitleParts } from '../layout.js';
|
|
7
|
+
import { LIVE_BUSY, LIVE_IDLE, META_KEYS, fmtDateTime, fmtSize, modelAlias, } from '../glyphs.js';
|
|
8
8
|
import { NATURA_SHORT, inboxMark } from '../inbox.js';
|
|
9
9
|
import { ChoiceRow } from './fields.js';
|
|
10
10
|
import { MODELS } from '../spawn.js';
|
|
@@ -64,7 +64,12 @@ export function SessionPreview({ s, firstLines, lastLines, columns, origin, note
|
|
|
64
64
|
// non c'è una provenienza vera da dichiarare.
|
|
65
65
|
const originAlias = modelAlias(s.model);
|
|
66
66
|
const originIndex = originAlias ? MODELS.indexOf(originAlias) : undefined;
|
|
67
|
-
|
|
67
|
+
// La riga del titolo si taglia QUI, con la contabilità del deck, invece di
|
|
68
|
+
// lasciarla a `wrap="truncate-end"`: Ink taglia con `cli-truncate`, che sfora
|
|
69
|
+
// di una colonna per emoji (invariante ③ di width.ts) e mangia il bordo del
|
|
70
|
+
// box. Pezzi e ragione stanno in `previewTitleParts`.
|
|
71
|
+
const [sidPart, notePart, titlePart] = cutParts(previewTitleParts(s.sessionId, note, s.title), previewTextWidth(columns));
|
|
72
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: [_jsx(Text, { color: "cyan", children: sidPart }), notePart ? _jsx(Text, { color: "yellow", children: notePart }) : null, _jsx(Text, { dimColor: Boolean(note), children: titlePart })] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [fmtSize(s.sizeBytes), " \u00B7 ", s.turns, " turni \u00B7 ", fmtDateTime(s.ts), " \u00B7 ", s.gitBranch || '-', s.model ? ` · ${s.model}` : '', origin ? ` · ⑂ da ${origin.slice(0, 8)}` : '', live ? (_jsx(Text, { color: live.status === 'busy' ? 'yellow' : 'green', children: ` · ${live.status === 'busy' ? LIVE_BUSY : LIVE_IDLE} viva pid ${live.pid} (${live.status})` })) : null] }), _jsx(ChoiceRow, { label: "modello", values: MODELS, index: Math.max(0, MODELS.indexOf(resumeModel)), focused: false, width: previewTextWidth(columns), originIndex: originIndex }), first.map((line, i) => (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [i === 0 ? '» ' : ' ', line] }, `f${i}`))), last.map((line, i) => (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [i === 0 ? '« ' : ' ', line] }, `l${i}`)))] }));
|
|
68
73
|
}
|
|
69
74
|
/**
|
|
70
75
|
* Righe non-wrappabili del dettaglio (titolo + meta + commit) e loro conteggio.
|
package/package.json
CHANGED
package/scripts/deck-run
CHANGED
|
@@ -116,7 +116,9 @@ MODEL=""
|
|
|
116
116
|
# tab. È l'UNICO ingresso di testo libero nel titolo — gli altri componenti
|
|
117
117
|
# (emoji/name dal file committato, TaskID) sono controllati — quindi il testo non
|
|
118
118
|
# si quota, si RIDUCE a un alfabeto sicuro (vedi _sane_note): tutto ciò che non
|
|
119
|
-
# ci rientra sparisce, apici inclusi.
|
|
119
|
+
# ci rientra sparisce, apici inclusi. L'alfabeto ammette le quadre e quattro
|
|
120
|
+
# emoji (T156) perché il deck ci fa passare il prefisso del titolo di fallback
|
|
121
|
+
# (`[ 🚀 ] slug`); ogni altra emoji cade ancora.
|
|
120
122
|
# --model <fable|opus|sonnet|haiku> (T108): modello della sessione. Enum e non
|
|
121
123
|
# stringa libera perché il valore finisce dentro `bash -lc` e un refuso
|
|
122
124
|
# produrrebbe una tab con un comando che il CLI rifiuta. Le voci sono ALIAS e
|
|
@@ -267,8 +269,23 @@ WORKDIR="${LOOM_DECK_WORKDIR:-$PWD}"
|
|
|
267
269
|
# titolo finisce dentro apici singoli in `bash -lc "claude --name '…'"`, e la
|
|
268
270
|
# nota è testo digitato a mano — enumerare i caratteri pericolosi significa
|
|
269
271
|
# sbagliarne uno, enumerare quelli ammessi no. Restano lettere, cifre, accentate
|
|
270
|
-
# italiane, spazio, trattino, underscore
|
|
271
|
-
# `$`, backtick, `;`, emoji…) cade.
|
|
272
|
+
# italiane, spazio, trattino, underscore, le quadre e quattro emoji (sotto);
|
|
273
|
+
# tutto il resto (apici, backslash, `$`, backtick, `;`, ogni altra emoji…) cade.
|
|
274
|
+
#
|
|
275
|
+
# T156 — le quadre e le quattro emoji entrano per il PREFISSO del titolo di
|
|
276
|
+
# fallback che il deck genera quando il campo nota è vuoto (`[ 🚀 ] slug`,
|
|
277
|
+
# `ACTION_EMOJI` in `src/spawn.ts`). Senza di loro il prefisso spariva qui e
|
|
278
|
+
# restava nella lista del deck, che riceve la nota grezza dal sidecar
|
|
279
|
+
# `session-tasks.jsonl`: la stessa conversazione portava due titoli diversi,
|
|
280
|
+
# senza errore.
|
|
281
|
+
# La whitelist le nomina una per una e NON apre un intervallo di code point. Due
|
|
282
|
+
# ragioni: un'emoji arriva nella tab solo se il deck la mostra identica in lista,
|
|
283
|
+
# cioè se `sanitize` (`src/width.ts`) la lascia passare invece di renderla `·`, e
|
|
284
|
+
# quel verdetto si verifica un glifo alla volta (🗺️ è astrale e finisce comunque
|
|
285
|
+
# in `·`); e l'ordine di un intervallo dentro un bracket expression segue la
|
|
286
|
+
# collazione della locale, non i code point.
|
|
287
|
+
# `]` sta per PRIMO nel set negato, dove chiude la classe invece di aprirla.
|
|
288
|
+
#
|
|
272
289
|
# Il cap a 60 CHARACTER è largo di proposito: una tab che sfora tronca a destra
|
|
273
290
|
# da sola, quindi il limite serve solo a non spingere fuori vista la parte del
|
|
274
291
|
# titolo che compass matcha, non a far stare la nota nella tab.
|
|
@@ -278,8 +295,17 @@ _sane_note() { # <raw> → nota ridotta, spazi collassati, cap 60 char
|
|
|
278
295
|
# titolo). L'assegnazione rifà setlocale, l'uscita dalla funzione lo ripristina.
|
|
279
296
|
local LC_ALL=C.UTF-8
|
|
280
297
|
local s
|
|
298
|
+
# La locale va rimessa SUL `sed`, non ereditata da qui: `local LC_ALL` non
|
|
299
|
+
# porta l'attributo export quando `LC_ALL` non era già esportata nell'ambiente
|
|
300
|
+
# (misurato: il figlio la vede vuota), quindi `sed` ricade su `LANG` — sotto
|
|
301
|
+
# una locale `C` lavora a byte e un carattere multibyte fuori whitelist lascia
|
|
302
|
+
# nel titolo i byte che coincidono con quelli ammessi. Le emoji astrali, che
|
|
303
|
+
# condividono i primi due byte `f0 9f`, ne lasciano sempre due: `🔥 fuoco`
|
|
304
|
+
# diventa `\xf0\x9f fuoco`, cioè UTF-8 rotto dentro il titolo della tab.
|
|
305
|
+
# `tr` non ne ha bisogno: squeeza uno spazio ASCII, e 0x20 non compare mai
|
|
306
|
+
# come byte di continuazione di una sequenza UTF-8.
|
|
281
307
|
s="$(printf '%s' "$1" \
|
|
282
|
-
| sed 's/[^A-Za-z0-9 _
|
|
308
|
+
| LC_ALL=C.UTF-8 sed 's/[^]A-Za-z0-9 _àèéìòùÀÈÉÌÒÙ[📐🚀📊🏁-]//g' \
|
|
283
309
|
| tr -s ' ')"
|
|
284
310
|
s="${s# }"; s="${s% }"
|
|
285
311
|
printf '%s' "${s:0:60}"
|