@lamemind/loom-deck 0.27.1 → 0.27.3
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 +8 -0
- package/dist/cli.js +57 -5
- package/dist/width.js +55 -0
- package/package.json +1 -1
- package/scripts/deck-run +49 -2
package/README.md
CHANGED
|
@@ -308,6 +308,14 @@ senza flag resta `recap`, `none` apre la sessione bound **senza** alcun prompt.
|
|
|
308
308
|
`LOOM_DECK_ENTER_PROMPT` (placeholder `{TASK}`) resta un override che vince sul
|
|
309
309
|
kind — tranne su `none`, che è una richiesta esplicita di non averne.
|
|
310
310
|
|
|
311
|
+
La tab porta anche `PTYXIS_PROFILE` forzata al profilo bindato al progetto nel
|
|
312
|
+
registry (`bindings/claude`), letto da dconf: è la chiave con cui loom-compass
|
|
313
|
+
associa a un progetto lo stato annunciato dagli hook (running/ask/done). Una tab
|
|
314
|
+
`ptyxis --tab` nuda erediterebbe il profilo di default, e l'annuncio finirebbe
|
|
315
|
+
keyed su un UUID che nessun progetto dichiara — pallino fermo su idle, senza
|
|
316
|
+
alcun errore visibile. Override o disattivazione via `LOOM_DECK_STATE_PROFILE`
|
|
317
|
+
(settata a vuoto → nessun annuncio); progetto non registrato → nessun prefisso.
|
|
318
|
+
|
|
311
319
|
## Sviluppo (TUI Ink)
|
|
312
320
|
|
|
313
321
|
```bash
|
package/dist/cli.js
CHANGED
|
@@ -16,7 +16,7 @@ import { assembleSessionList, firstSelectableId, moveSelection, neighborId, rowI
|
|
|
16
16
|
import { cellWidth, launchLegend, loadArchivableDays, loadIdentity, loadLaunch, } from './config.js';
|
|
17
17
|
import { countArchivable, SCAN_INTERVAL_MS } from './archivable.js';
|
|
18
18
|
import { assignListCapacity, isCompact, layoutBudget, readerCapacity, searchListCapacity, searchPreviewCapacity, windowRange, } from './viewport.js';
|
|
19
|
-
import { caretWindow, cut, pad, sanitize, termWidth, wrapLines, wrapWithOffsets, } from './width.js';
|
|
19
|
+
import { caretWindow, cut, cutParts, pad, sanitize, termWidth, wrapLines, wrapWithOffsets, } from './width.js';
|
|
20
20
|
import { applyView, cycleSort, describeSort, priName, progName, toggleHidden, PRI_ENTRIES, PROG_ENTRIES, } from './view.js';
|
|
21
21
|
import { initialDetail, progressText, writeTaskEdit, PRI_GLYPH, PRI_LABEL, PROG_GLYPH, } from './task-edit.js';
|
|
22
22
|
import { loadView, saveView, viewFilePath } from './view-store.js';
|
|
@@ -2017,15 +2017,43 @@ function AssignScreen({ sessionId, label, current, filter, rows, selected, match
|
|
|
2017
2017
|
return (_jsxs(Text, { inverse: sel, dimColor: !sel && isDone(task.prog), wrap: "truncate-end", children: [sel ? CARET : CARET_OFF, task.id, " ", sanitize(task.pri), " ", displayProg(task.prog), " ", desc, tail] }, task.id));
|
|
2018
2018
|
})] }), note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: sanitize(note) }) : null] }));
|
|
2019
2019
|
}
|
|
2020
|
+
/**
|
|
2021
|
+
* Header del pane task, tagliato QUI e non da Ink — stesso motivo del gemello
|
|
2022
|
+
* `SessionsHeader`, con una differenza di rischio: qui i segmenti sono tutti
|
|
2023
|
+
* ASCII più le frecce `↑↓` (larghe 1), quindi `cli-truncate` oggi darebbe la
|
|
2024
|
+
* riga giusta per caso. Il taglio resta del deck perché la correttezza non deve
|
|
2025
|
+
* dipendere dall'alfabeto che capita nella riga: il primo glifo largo 2 che
|
|
2026
|
+
* entrasse in un segmento nuovo riaprirebbe il difetto in silenzio, e a
|
|
2027
|
+
* scoprirlo sarebbe il bordo del pane a schermo.
|
|
2028
|
+
*
|
|
2029
|
+
* `truncate-end` taglia dalla coda, e `cutParts` conserva l'ordine: l'ultimo
|
|
2030
|
+
* segmento resta il primo a cedere il posto (vedi T61 sotto, `archiviabili` in
|
|
2031
|
+
* coda ai contatori della vista corrente).
|
|
2032
|
+
*/
|
|
2033
|
+
function TasksHeader({ filtered, total, hidden, above, below, archivable, focused, columns, }) {
|
|
2034
|
+
const segments = [
|
|
2035
|
+
{ text: `Tasks (${hidden > 0 ? `${filtered}/${total}` : filtered})` },
|
|
2036
|
+
...(hidden > 0 ? [{ text: ` · ${hidden} nascoste`, color: 'yellow' }] : []),
|
|
2037
|
+
...(above > 0 ? [{ text: ` · ↑${above}`, dim: true }] : []),
|
|
2038
|
+
...(below > 0 ? [{ text: ` · ↓${below}`, dim: true }] : []),
|
|
2039
|
+
...(archivable > 0 ? [{ text: ` · ${archivable} archiviabili`, dim: true }] : []),
|
|
2040
|
+
];
|
|
2041
|
+
const shown = cutParts(segments.map((s) => s.text), paneTextWidth(columns));
|
|
2042
|
+
return (_jsx(Text, { bold: true, color: focused ? 'cyan' : undefined, wrap: "truncate-end", children: segments.map((seg, i) => shown[i] ? (_jsx(Text, { color: seg.color, dimColor: seg.dim, children: shown[i] }, i)) : null) }));
|
|
2043
|
+
}
|
|
2020
2044
|
function TasksPane({ tasks, filtered, total, hidden, view, selected, spotCount, allCount, childCount, focused, loadError, detail, windowStart, above, below, detailLines, columns, archivable, }) {
|
|
2021
2045
|
const allSelected = selected === ROW_ALL;
|
|
2022
2046
|
const spotSelected = selected === ROW_SPOT;
|
|
2023
|
-
return (_jsxs(Box, { flexDirection: "column", width: "50%", marginRight: 1, borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', paddingX: 1, children: [
|
|
2047
|
+
return (_jsxs(Box, { flexDirection: "column", width: "50%", marginRight: 1, borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', paddingX: 1, children: [_jsx(TasksHeader, { filtered: filtered, total: total, hidden: hidden, above: above, below: below, archivable: archivable, focused: focused, columns: columns }), _jsx(Text, { dimColor: true, wrap: "truncate-end", children: cut(sanitize(`sort: ${describeSort(view.sort)}` +
|
|
2048
|
+
(view.hiddenPri.length + view.hiddenProg.length > 0
|
|
2049
|
+
? ' · filtri: ' +
|
|
2050
|
+
[
|
|
2024
2051
|
...PRI_ENTRIES.filter((e) => view.hiddenPri.includes(e.name)),
|
|
2025
2052
|
...PROG_ENTRIES.filter((e) => view.hiddenProg.includes(e.name)),
|
|
2026
2053
|
]
|
|
2027
|
-
.map((e) => `−${
|
|
2028
|
-
.join(' ')
|
|
2054
|
+
.map((e) => `−${e.glyph}`)
|
|
2055
|
+
.join(' ')
|
|
2056
|
+
: '')), paneTextWidth(columns)) }), _jsxs(Text, { inverse: allSelected && focused, bold: allSelected && !focused, wrap: "truncate-end", children: [allSelected ? CARET : CARET_OFF, "\u2261 tutte le sessioni", allCount > 0 ? ` (${allCount})` : ''] }), _jsxs(Text, { inverse: spotSelected && focused, bold: spotSelected && !focused, wrap: "truncate-end", children: [spotSelected ? CARET : CARET_OFF, "\u25CB spot sessioni libere", spotCount > 0 ? ` (${spotCount})` : ''] }), loadError ? (_jsx(Text, { color: "red", wrap: "truncate-end", children: loadError })) : (tasks.map((task, i) => {
|
|
2029
2057
|
// windowStart riporta l'indice di finestra a quello della lista
|
|
2030
2058
|
// completa, su cui è keyata la selezione. +META_ROWS: le prime due
|
|
2031
2059
|
// righe sono le meta.
|
|
@@ -2043,8 +2071,32 @@ function TasksPane({ tasks, filtered, total, hidden, view, selected, spotCount,
|
|
|
2043
2071
|
return (_jsxs(Text, { inverse: sel && focused, bold: sel && !focused, dimColor: !sel && isDone(task.prog), wrap: "truncate-end", children: [sel ? CARET : CARET_OFF, task.id, " ", sanitize(task.pri), " ", displayProg(task.prog), " ", desc, tail] }, task.id));
|
|
2044
2072
|
})), detail && detailLines > 0 ? (_jsx(DetailPane, { detail: detail, maxLines: detailLines, columns: columns })) : null] }));
|
|
2045
2073
|
}
|
|
2074
|
+
/**
|
|
2075
|
+
* Header del pane sessioni, tagliato QUI e non da Ink.
|
|
2076
|
+
*
|
|
2077
|
+
* `📌{pinnedCount}` è un glifo largo 2 in mezzo alla riga: con
|
|
2078
|
+
* `wrap="truncate-end"` il taglio passava da `cli-truncate`, che indicizza per
|
|
2079
|
+
* code point con un budget in colonne e restituiva una riga larga 45 su un
|
|
2080
|
+
* budget di 44 — la colonna in più finiva sopra il bordo destro del pane, che
|
|
2081
|
+
* spariva dalla riga (invariante ③ di `width.ts`).
|
|
2082
|
+
*
|
|
2083
|
+
* I segmenti restano segmenti fino al render: il taglio è della RIGA (budget
|
|
2084
|
+
* condiviso, `cutParts`), la resa è del pezzo. Il giallo su `📌N` distingue le
|
|
2085
|
+
* pinnate dal resto dell'header e non è decorazione.
|
|
2086
|
+
*/
|
|
2087
|
+
function SessionsHeader({ parentLabel, total, pinnedCount, hidden, above, below, focused, columns, }) {
|
|
2088
|
+
const segments = [
|
|
2089
|
+
{ text: `Sessions · ${parentLabel} (${total})` },
|
|
2090
|
+
...(pinnedCount > 0 ? [{ text: ` · 📌${pinnedCount}`, color: 'yellow' }] : []),
|
|
2091
|
+
...(hidden > 0 ? [{ text: ` · +${hidden} più vecchie`, dim: true }] : []),
|
|
2092
|
+
...(above > 0 ? [{ text: ` · ↑${above}`, dim: true }] : []),
|
|
2093
|
+
...(below > 0 ? [{ text: ` · ↓${below}`, dim: true }] : []),
|
|
2094
|
+
];
|
|
2095
|
+
const shown = cutParts(segments.map((s) => s.text), paneTextWidth(columns));
|
|
2096
|
+
return (_jsx(Text, { bold: true, color: focused ? 'cyan' : undefined, wrap: "truncate-end", children: segments.map((seg, i) => shown[i] ? (_jsx(Text, { color: seg.color, dimColor: seg.dim, children: shown[i] }, i)) : null) }));
|
|
2097
|
+
}
|
|
2046
2098
|
function SessionsPane({ parentLabel, isSpot, isAll, bindings, taskW, ageW, rows, total, pinnedCount, hidden, selectedId, focused, above, below, detail, firstLines, lastLines, columns, forkOf, sessionNotes, projectCore, }) {
|
|
2047
|
-
return (_jsxs(Box, { flexDirection: "column", width: "50%", borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', paddingX: 1, children: [
|
|
2099
|
+
return (_jsxs(Box, { flexDirection: "column", width: "50%", borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', paddingX: 1, children: [_jsx(SessionsHeader, { parentLabel: parentLabel, total: total, pinnedCount: pinnedCount, hidden: hidden, above: above, below: below, focused: focused, columns: columns }), total === 0 ? (_jsx(Text, { color: "yellow", wrap: "truncate-end", children: isAll
|
|
2048
2100
|
? 'nessuna conversazione nel progetto'
|
|
2049
2101
|
: isSpot
|
|
2050
2102
|
? 'nessuna sessione libera'
|
package/dist/width.js
CHANGED
|
@@ -202,6 +202,61 @@ export function cut(s, cols) {
|
|
|
202
202
|
// informazione, tolgono una colonna di testo.
|
|
203
203
|
return out.trimEnd().replace(/…$/, '') + '…';
|
|
204
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Taglio di una riga COMPOSTA da più pezzi, ognuno con la propria resa (colore,
|
|
207
|
+
* dim, bold). Restituisce un array della stessa lunghezza: i pezzi interi finché
|
|
208
|
+
* il budget regge, quello a cavallo del taglio accorciato, i successivi vuoti.
|
|
209
|
+
*
|
|
210
|
+
* Perché non basta `cut` chiamato pezzo per pezzo: il budget è della RIGA, non
|
|
211
|
+
* del singolo pezzo — un pezzo va tagliato in base a quanto hanno già consumato
|
|
212
|
+
* quelli prima di lui. E `cut` collassa il whitespace, quindi mangerebbe lo
|
|
213
|
+
* spazio che separa i segmenti (` · 📌3` diventerebbe `· 📌3`), disallineando la
|
|
214
|
+
* riga di una colonna per ogni giunzione.
|
|
215
|
+
*
|
|
216
|
+
* Perché non basta concatenare e tagliare una volta sola: il risultato sarebbe
|
|
217
|
+
* una stringa unica, e i pezzi tornerebbero renderizzabili solo tutti dello
|
|
218
|
+
* stesso colore. Un header che perde il giallo su `📌N` perde il segnale, non
|
|
219
|
+
* solo la vernice.
|
|
220
|
+
*
|
|
221
|
+
* Invariante ③ (§ in testa a questo file): serve a togliere l'ultima riga
|
|
222
|
+
* composita ancora tagliata da Ink. Gli header dei due pane sono l'ultimo posto
|
|
223
|
+
* dove `wrap="truncate-end"` era rimasto il meccanismo e non la rete: entrambi
|
|
224
|
+
* portano glifi larghi 2 (`📌` nelle sessioni, i glifi di priorità nei filtri
|
|
225
|
+
* delle task), cioè la condizione esatta in cui `cli-truncate` restituisce una
|
|
226
|
+
* riga più larga del budget e il bordo del pane sparisce.
|
|
227
|
+
*/
|
|
228
|
+
export function cutParts(parts, cols) {
|
|
229
|
+
const empty = parts.map(() => '');
|
|
230
|
+
if (cols <= 0)
|
|
231
|
+
return empty;
|
|
232
|
+
if (parts.reduce((w, p) => w + termWidth(p), 0) <= cols)
|
|
233
|
+
return [...parts];
|
|
234
|
+
const out = [...empty];
|
|
235
|
+
const budget = cols - 1; // -1 = la colonna dell'ellissi, che qui c'è di sicuro
|
|
236
|
+
let w = 0;
|
|
237
|
+
let full = false;
|
|
238
|
+
for (let i = 0; i < parts.length && !full; i++) {
|
|
239
|
+
let acc = '';
|
|
240
|
+
for (const ch of parts[i]) {
|
|
241
|
+
const cw = termWidth(ch);
|
|
242
|
+
if (w + cw > budget) {
|
|
243
|
+
full = true;
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
acc += ch;
|
|
247
|
+
w += cw;
|
|
248
|
+
}
|
|
249
|
+
out[i] = acc;
|
|
250
|
+
}
|
|
251
|
+
// L'ellissi va sull'ultimo pezzo che ha davvero del testo: appiccicarla a un
|
|
252
|
+
// pezzo vuoto la staccherebbe dal punto di taglio, e il lettore la leggerebbe
|
|
253
|
+
// come un segmento a sé.
|
|
254
|
+
let last = out.length - 1;
|
|
255
|
+
while (last > 0 && out[last] === '')
|
|
256
|
+
last--;
|
|
257
|
+
out[last] = out[last].trimEnd().replace(/…$/, '') + '…';
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
205
260
|
/**
|
|
206
261
|
* Cella di larghezza ESATTA `cols`: taglia se eccede, riempie di spazi se manca.
|
|
207
262
|
*
|
package/package.json
CHANGED
package/scripts/deck-run
CHANGED
|
@@ -40,6 +40,8 @@
|
|
|
40
40
|
# LOOM_DECK_WORKDIR dir di lavoro della tab (default: $PWD)
|
|
41
41
|
# LOOM_DECK_ENTER_PROMPT override del prompt iniziale, {TASK}=TaskID (vince sul kind, tranne `none`)
|
|
42
42
|
# LOOM_DECK_INTAB_CMD override comando in-tab (default: la sessione CC; usato per i test)
|
|
43
|
+
# LOOM_DECK_STATE_PROFILE PTYXIS_PROFILE annunciata a compass (default: bindings/claude
|
|
44
|
+
# del progetto, letto da dconf; settata a vuoto = nessun annuncio)
|
|
43
45
|
# LOOM_DECK_PERMISSION_MODE override del permissionMode del file config (default: campo file, poi 'manual')
|
|
44
46
|
#
|
|
45
47
|
set -euo pipefail
|
|
@@ -271,6 +273,48 @@ case "${PERM_MODE:-}" in
|
|
|
271
273
|
esac
|
|
272
274
|
MODE_FLAG="--permission-mode ${PERM_MODE} "
|
|
273
275
|
|
|
276
|
+
# ── Profilo di stato per compass ─────────────────────────────────────────────
|
|
277
|
+
# Lo stato di una sessione (running/ask/done) viaggia verso compass via D-Bus
|
|
278
|
+
# keyed su $PTYXIS_PROFILE (hook Claude → `compass <stato>`), e compass lo mappa
|
|
279
|
+
# a un progetto SOLO se quell'UUID compare in `bindings/<surface>/profile` del
|
|
280
|
+
# registry. Ma nel modo `inline` la tab nasce da `ptyxis --tab` nudo, che eredita
|
|
281
|
+
# il profilo DEFAULT di Ptyxis: l'annuncio arriva keyed su un UUID che nessun
|
|
282
|
+
# progetto dichiara → stato scartato, pallino fermo su idle per sempre. Il match
|
|
283
|
+
# della FINESTRA non se ne accorge (passa dal titolo, non dal profilo), quindi il
|
|
284
|
+
# progetto risulta presente ma senza stato — un guasto che non si manifesta come
|
|
285
|
+
# errore, solo come pallino che non cambia mai.
|
|
286
|
+
# Rimedio: forzare in-tab la PTYXIS_PROFILE del binding claude del progetto, così
|
|
287
|
+
# l'annuncio è keyed sull'UUID che compass cerca davvero. Il profilo bindato NON
|
|
288
|
+
# è usabile come profilo vero della tab (`--tab-with-profile`): ha
|
|
289
|
+
# use-custom-command=true, che fa scartare il comando inline — vedi il ramo
|
|
290
|
+
# `profile` sotto, che per questo deve riscrivere dconf.
|
|
291
|
+
#
|
|
292
|
+
# `${VAR+set}`, non `${VAR:-}`: env settata anche a VUOTO = "salta il lookup"
|
|
293
|
+
# (deterministico per i test, che altrimenti leggerebbero il dconf della macchina
|
|
294
|
+
# su cui gira la suite). Non settata = risolvi dal registry.
|
|
295
|
+
# Assente/non risolvibile → nessun prefisso: si degrada al comportamento di prima
|
|
296
|
+
# (stato orfano) invece di rompere lo spawn.
|
|
297
|
+
if [[ -n "${LOOM_DECK_STATE_PROFILE+set}" ]]; then
|
|
298
|
+
STATE_PROFILE="${LOOM_DECK_STATE_PROFILE}"
|
|
299
|
+
else
|
|
300
|
+
STATE_PROFILE=""
|
|
301
|
+
if [[ -n "$_cfg" ]] && command -v dconf >/dev/null 2>&1; then
|
|
302
|
+
_pid="$(jq -r '.id // empty' "$_cfg" 2>/dev/null || true)"
|
|
303
|
+
if [[ -n "$_pid" ]]; then
|
|
304
|
+
STATE_PROFILE="$(dconf read "/org/lamemind/loom/projects/${_pid}/bindings/claude/profile" 2>/dev/null | tr -d "'" || true)"
|
|
305
|
+
fi
|
|
306
|
+
fi
|
|
307
|
+
fi
|
|
308
|
+
# Whitelist come per la nota del titolo: il valore entra in `bash -lc "…"`, e un
|
|
309
|
+
# registry è pur sempre un file editabile a mano. Un UUID Ptyxis è esadecimale;
|
|
310
|
+
# accetto l'alfabeto id-safe e scarto il resto invece di quotare.
|
|
311
|
+
if [[ -n "$STATE_PROFILE" && ! "$STATE_PROFILE" =~ ^[A-Za-z0-9_-]+$ ]]; then
|
|
312
|
+
echo "profilo di stato ignorato (caratteri non ammessi): '${STATE_PROFILE}'" >&2
|
|
313
|
+
STATE_PROFILE=""
|
|
314
|
+
fi
|
|
315
|
+
PROFILE_ENV=""
|
|
316
|
+
[[ -n "$STATE_PROFILE" ]] && PROFILE_ENV="PTYXIS_PROFILE=${STATE_PROFILE} "
|
|
317
|
+
|
|
274
318
|
# --session-id per la tab: pinna il sessionId della sessione CC (vuoto → CC ne
|
|
275
319
|
# genera uno). Lo spazio finale tiene la concatenazione pulita quando è assente.
|
|
276
320
|
SID_FLAG=""
|
|
@@ -344,10 +388,13 @@ PROMPT_ARG=""
|
|
|
344
388
|
# agli altri elementi task-bound: nel fork di una sessione SPOT il sessionId
|
|
345
389
|
# pinnato non serve a legare una task (non c'è) ma a rendere noto in anticipo
|
|
346
390
|
# l'id del ramo, senza il quale il lineage non sarebbe registrabile.
|
|
391
|
+
# PROFILE_ENV in testa a entrambi i rami: lo stato è una proprietà della SESSIONE
|
|
392
|
+
# (esiste anche senza task), esattamente come la label del titolo — legarlo al
|
|
393
|
+
# ramo task-bound ripeterebbe l'errore di layer già evitato per la titolazione.
|
|
347
394
|
if [[ $NO_TASK -eq 1 ]]; then
|
|
348
|
-
_default_intab="claude --name '${TITLE}' ${MODE_FLAG}${SID_FLAG}${RES_FLAG}${FORK_FLAG}"
|
|
395
|
+
_default_intab="${PROFILE_ENV}claude --name '${TITLE}' ${MODE_FLAG}${SID_FLAG}${RES_FLAG}${FORK_FLAG}"
|
|
349
396
|
else
|
|
350
|
-
_default_intab="LOOM_TASK=${TASK} claude --name '${TITLE}' ${MODE_FLAG}${SID_FLAG}${RES_FLAG}${FORK_FLAG}${PROMPT_ARG}"
|
|
397
|
+
_default_intab="${PROFILE_ENV}LOOM_TASK=${TASK} claude --name '${TITLE}' ${MODE_FLAG}${SID_FLAG}${RES_FLAG}${FORK_FLAG}${PROMPT_ARG}"
|
|
351
398
|
fi
|
|
352
399
|
IN_TAB_CMD="${LOOM_DECK_INTAB_CMD:-$_default_intab}"
|
|
353
400
|
|