@lamemind/loom-deck 0.27.0 → 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 +81 -9
- 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';
|
|
@@ -680,7 +680,11 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
680
680
|
for (const r of sessionRows) {
|
|
681
681
|
if (r.kind === 'separator')
|
|
682
682
|
continue;
|
|
683
|
-
|
|
683
|
+
// La colonna serve dove l'appartenenza NON è già scritta altrove sullo
|
|
684
|
+
// schermo: nella vista "tutte" (ogni riga ha un binding proprio) e su
|
|
685
|
+
// OGNI riga pinnata, in qualunque vista — una pinnata è sganciata dal
|
|
686
|
+
// parent selezionato, quindi l'header del pane non parla per lei.
|
|
687
|
+
if (isAll || r.kind === 'pinned') {
|
|
684
688
|
const b = bindings.get(r.sessionId);
|
|
685
689
|
if (b)
|
|
686
690
|
task = Math.max(task, termWidth(b));
|
|
@@ -2013,15 +2017,43 @@ function AssignScreen({ sessionId, label, current, filter, rows, selected, match
|
|
|
2013
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));
|
|
2014
2018
|
})] }), note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: sanitize(note) }) : null] }));
|
|
2015
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
|
+
}
|
|
2016
2044
|
function TasksPane({ tasks, filtered, total, hidden, view, selected, spotCount, allCount, childCount, focused, loadError, detail, windowStart, above, below, detailLines, columns, archivable, }) {
|
|
2017
2045
|
const allSelected = selected === ROW_ALL;
|
|
2018
2046
|
const spotSelected = selected === ROW_SPOT;
|
|
2019
|
-
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
|
+
[
|
|
2020
2051
|
...PRI_ENTRIES.filter((e) => view.hiddenPri.includes(e.name)),
|
|
2021
2052
|
...PROG_ENTRIES.filter((e) => view.hiddenProg.includes(e.name)),
|
|
2022
2053
|
]
|
|
2023
|
-
.map((e) => `−${
|
|
2024
|
-
.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) => {
|
|
2025
2057
|
// windowStart riporta l'indice di finestra a quello della lista
|
|
2026
2058
|
// completa, su cui è keyata la selezione. +META_ROWS: le prime due
|
|
2027
2059
|
// righe sono le meta.
|
|
@@ -2039,8 +2071,32 @@ function TasksPane({ tasks, filtered, total, hidden, view, selected, spotCount,
|
|
|
2039
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));
|
|
2040
2072
|
})), detail && detailLines > 0 ? (_jsx(DetailPane, { detail: detail, maxLines: detailLines, columns: columns })) : null] }));
|
|
2041
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
|
+
}
|
|
2042
2098
|
function SessionsPane({ parentLabel, isSpot, isAll, bindings, taskW, ageW, rows, total, pinnedCount, hidden, selectedId, focused, above, below, detail, firstLines, lastLines, columns, forkOf, sessionNotes, projectCore, }) {
|
|
2043
|
-
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
|
|
2044
2100
|
? 'nessuna conversazione nel progetto'
|
|
2045
2101
|
: isSpot
|
|
2046
2102
|
? 'nessuna sessione libera'
|
|
@@ -2059,8 +2115,17 @@ function SessionsPane({ parentLabel, isSpot, isAll, bindings, taskW, ageW, rows,
|
|
|
2059
2115
|
// riga oltre il bordo, e a ripararla arrivava `cli-truncate` (che
|
|
2060
2116
|
// sfora di una colonna per emoji e mangia il bordo stesso).
|
|
2061
2117
|
const staleNote = sessionNotes.get(row.sessionId);
|
|
2062
|
-
|
|
2063
|
-
|
|
2118
|
+
// La riga stale è libera (niente colonne: non ha né titolo né
|
|
2119
|
+
// data), ma il binding va detto lo stesso — è una pinnata, quindi
|
|
2120
|
+
// l'header del pane non ne dice l'appartenenza.
|
|
2121
|
+
const staleTask = bindings.get(row.sessionId) ?? null;
|
|
2122
|
+
const staleW = Math.max(0, paneTextWidth(columns) -
|
|
2123
|
+
(2 /* caret */ +
|
|
2124
|
+
termWidth(`${WARN} pin stale `) +
|
|
2125
|
+
SID_CHARS +
|
|
2126
|
+
(staleTask ? termWidth(staleTask) + 1 : 0) +
|
|
2127
|
+
3 /* spazio + caporali */));
|
|
2128
|
+
return (_jsxs(Text, { inverse: sel && focused, bold: sel && !focused, dimColor: true, wrap: "truncate-end", children: [sel ? CARET : CARET_OFF, _jsx(Text, { color: "yellow", children: WARN }), " pin stale", ' ', _jsx(Text, { dimColor: true, children: row.sessionId.slice(0, SID_CHARS) }), staleTask ? _jsxs(Text, { color: "green", children: [" ", staleTask] }) : null, staleNote ? _jsxs(Text, { color: "yellow", children: [" \u00AB", cut(staleNote, staleW), "\u00BB"] }) : null] }, row.sessionId));
|
|
2064
2129
|
}
|
|
2065
2130
|
const s = row.session; // non-stale → session presente
|
|
2066
2131
|
const isPinnedRow = row.kind === 'pinned';
|
|
@@ -2076,6 +2141,13 @@ function SessionsPane({ parentLabel, isSpot, isAll, bindings, taskW, ageW, rows,
|
|
|
2076
2141
|
// task accanto, che esiste solo in questa vista.
|
|
2077
2142
|
const bound = bindings.get(s.sessionId) ?? null;
|
|
2078
2143
|
const linked = isAll ? Boolean(bound) : !isSpot;
|
|
2144
|
+
// Stesso motivo per cui la colonna esiste: una pinnata resta in lista
|
|
2145
|
+
// qualunque sia il parent selezionato, quindi l'header non ne dice
|
|
2146
|
+
// l'appartenenza e la cella va riempita anche fuori dalla vista
|
|
2147
|
+
// "tutte". Sulle contestuali, dove l'header parla già, resta vuota —
|
|
2148
|
+
// ma la cella è comunque larga `taskW`, o le colonne a destra
|
|
2149
|
+
// slitterebbero riga per riga.
|
|
2150
|
+
const taskCell = isAll || isPinnedRow ? (bound ?? TASK_EMPTY) : '';
|
|
2079
2151
|
// T60 — colonne VERE: ogni cella fissa è larga esattamente quanto
|
|
2080
2152
|
// dichiara, riempita di spazi con `pad` (che misura in colonne, non in
|
|
2081
2153
|
// caratteri). Il marker va portato a 2 anche quando è `○`, largo 1:
|
|
@@ -2109,7 +2181,7 @@ function SessionsPane({ parentLabel, isSpot, isAll, bindings, taskW, ageW, rows,
|
|
|
2109
2181
|
const used = (label.note ? termWidth(label.note) + 2 : 0) +
|
|
2110
2182
|
(label.note && label.rest ? 1 : 0) +
|
|
2111
2183
|
termWidth(label.rest);
|
|
2112
|
-
return (_jsxs(Text, { inverse: sel && focused, bold: sel && !focused, wrap: "truncate-end", children: [sel ? CARET : CARET_OFF, isPinnedRow ? (_jsx(Text, { color: "yellow", children: pad('📌', 2) })) : linked ? (_jsx(Text, { color: "green", children: pad('🔗', 2) })) : (_jsx(Text, { dimColor: true, children: pad('○', 2) })), ' ', _jsx(Text, { color: "cyan", children: s.sessionId.slice(0, SID_CHARS) }), ' ', taskW > 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: bound ? 'green' : undefined, dimColor: !bound, children: pad(
|
|
2184
|
+
return (_jsxs(Text, { inverse: sel && focused, bold: sel && !focused, wrap: "truncate-end", children: [sel ? CARET : CARET_OFF, isPinnedRow ? (_jsx(Text, { color: "yellow", children: pad('📌', 2) })) : linked ? (_jsx(Text, { color: "green", children: pad('🔗', 2) })) : (_jsx(Text, { dimColor: true, children: pad('○', 2) })), ' ', _jsx(Text, { color: "cyan", children: s.sessionId.slice(0, SID_CHARS) }), ' ', taskW > 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: bound && taskCell ? 'green' : undefined, dimColor: !bound, children: pad(taskCell, taskW) }), ' '] })) : null, forkMark ? _jsx(Text, { color: "magenta", children: forkMark }) : null, label.note ? (_jsxs(Text, { color: "yellow", bold: true, children: ["\u00AB", label.note, "\u00BB"] })) : null, label.note && label.rest ? ' ' : null, label.rest ? _jsx(Text, { dimColor: Boolean(label.note), children: label.rest }) : null, ' '.repeat(Math.max(0, inner - used)), ' ', _jsx(Text, { dimColor: true, children: pad(age, ageW, 'right') })] }, s.sessionId));
|
|
2113
2185
|
})), detail ? (_jsx(SessionDetailPane, { s: detail, firstLines: firstLines, lastLines: lastLines, columns: columns, origin: forkOf.get(detail.sessionId) ?? null, note: sessionNotes.get(detail.sessionId) ?? '' })) : null] }));
|
|
2114
2186
|
}
|
|
2115
2187
|
// T49 — detail pane della sessione selezionata (hover), gemello del DetailPane
|
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
|
|