@lamemind/loom-deck 0.27.1 → 0.28.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 +8 -0
- package/dist/cli.js +97 -36
- package/dist/viewport.js +61 -49
- 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';
|
|
@@ -1749,21 +1749,26 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1749
1749
|
// non dipende più da quante voci `launch` il progetto dichiara.
|
|
1750
1750
|
const launchLine = mode === 'normal';
|
|
1751
1751
|
const detailParts = detail ? detailMetaOf(detail) : null;
|
|
1752
|
+
// T70 — un solo blocco preview, sotto i due pane, e il FOCUS decide cosa
|
|
1753
|
+
// contiene: a sinistra la task selezionata, a destra la conversazione. È il
|
|
1754
|
+
// focus e non la selezione perché il blocco è uno — averne due significava
|
|
1755
|
+
// pagare due cornici per mostrare contemporaneamente il dettaglio di una cosa
|
|
1756
|
+
// che si sta guardando e di una che non si sta guardando.
|
|
1757
|
+
// `none` quando non c'è contenuto (riga meta selezionata, task file assente,
|
|
1758
|
+
// pin stale): il blocco sparisce del tutto, non resta una cornice vuota.
|
|
1759
|
+
const previewKind = focus === 'tasks' ? (detail ? 'task' : 'none') : selSessionObj ? 'session' : 'none';
|
|
1752
1760
|
const budget = layoutBudget({
|
|
1753
1761
|
rows,
|
|
1754
1762
|
mode,
|
|
1755
1763
|
launchLine,
|
|
1756
1764
|
noteLine: Boolean(note),
|
|
1757
|
-
|
|
1765
|
+
preview: previewKind,
|
|
1758
1766
|
detailMetaLines: detailParts?.metaLines ?? 0,
|
|
1759
|
-
//
|
|
1760
|
-
// l'hover, non uno stato persistente; navigando le task non ruba righe.
|
|
1761
|
-
hasSessionDetail: canResume,
|
|
1762
|
-
// Riservo righe di preview solo per i blocchi che davvero renderizzano: il
|
|
1767
|
+
// Riservo righe di anteprima solo per i blocchi che davvero renderizzano: il
|
|
1763
1768
|
// primo prompt aggiunge info solo con un titolo custom (senza, titolo ===
|
|
1764
1769
|
// primo prompt); l'ultima risposta solo se il modello ha già risposto.
|
|
1765
|
-
sessionHasFirstPreview:
|
|
1766
|
-
sessionHasLastPreview:
|
|
1770
|
+
sessionHasFirstPreview: previewKind === 'session' && Boolean(selSessionObj?.customTitle),
|
|
1771
|
+
sessionHasLastPreview: previewKind === 'session' && Boolean(selSessionObj?.lastReply),
|
|
1767
1772
|
});
|
|
1768
1773
|
// Finestre di rendering. Le liste "logiche" (viewTasks, sessionRows)
|
|
1769
1774
|
// restano intere: navigazione, selezione e spawn continuano a ragionare su
|
|
@@ -1779,7 +1784,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1779
1784
|
if (budget.compact) {
|
|
1780
1785
|
return (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), _jsxs(Text, { dimColor: true, children: [' ', "\u00B7 ", viewTasks.length, " task \u00B7 sel ", selectedTaskId ?? parentLabel, " \u00B7 terminale ", rows, "\u00D7", columns, ": troppo basso, allarga"] })] }));
|
|
1781
1786
|
}
|
|
1782
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), mode === 'create' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["nuova task \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " crea \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'sort' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["sort \u00B7 ", _jsx(Text, { color: "yellow", children: "p" }), " pri ", _jsx(Text, { color: "yellow", children: "s" }), " stato", ' ', _jsx(Text, { color: "yellow", children: "i" }), " id (asc\u2192desc\u2192off) \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " ok \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'filter' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["filtri \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2191\u2193\u2190\u2192" }), " naviga \u00B7 ", _jsx(Text, { color: "yellow", children: "spazio" }), ' ', "mostra/nascondi \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " ok \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'note' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["nota conversazione \u00B7 ", _jsx(Text, { color: "yellow", children: "^U" }), " svuota \u00B7", ' ', _jsx(Text, { color: "yellow", children: "\u23CE" }), " salva (vuoto = rimuove) \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'edit' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["edit \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2191\u2193" }), " campo \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " valore, o cursore sul testo \u00B7 ", _jsx(Text, { color: "yellow", children: "^A/^E" }), " inizio/fine \u00B7", ' ', _jsx(Text, { color: "yellow", children: "^D" }), " canc \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " salva+commit \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : (_jsx(Text, { dimColor: true, wrap: "truncate-end", children: keyLegend })), mode === 'normal' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [surfaceLegend, legend.shown ? ` · ${legend.shown}` : '', legend.overflow > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 +", legend.overflow, " fuori riga"] })) : null, legend.unreachable > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 ", legend.unreachable, " oltre la 9\u00AA (non raggiungibili)"] })) : null] })) : null, mode === 'create' ? (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: "yellow", children: "C \u203A " }), _jsx(Text, { children: draft }), _jsx(Text, { inverse: true, children: " " })] })) : null, mode === 'note' ? (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: "yellow", children: "\u270E \u203A " }), _jsx(Text, { children: noteDraft }), _jsx(Text, { inverse: true, children: " " })] })) : null, mode === 'sort' ? _jsx(SortModal, { sort: view.sort }) : null, mode === 'filter' ? _jsx(FilterModal, { view: view, cursor: filterCursor }) : null, mode === 'edit' && edit && selTask ? (_jsx(EditModal, { id: selTask.id, draft: edit, row: editRow, columns: columns })) : null, _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(TasksPane, { tasks: windowTasks, filtered: viewTasks.length, total: tasks.length, hidden: hiddenTasks, archivable: archivable, view: view, selected: selIndex, spotCount: spotCount, allCount: sessions.length, childCount: childCount, focused: focus === 'tasks', loadError: loadError,
|
|
1787
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), mode === 'create' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["nuova task \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " crea \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'sort' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["sort \u00B7 ", _jsx(Text, { color: "yellow", children: "p" }), " pri ", _jsx(Text, { color: "yellow", children: "s" }), " stato", ' ', _jsx(Text, { color: "yellow", children: "i" }), " id (asc\u2192desc\u2192off) \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " ok \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'filter' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["filtri \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2191\u2193\u2190\u2192" }), " naviga \u00B7 ", _jsx(Text, { color: "yellow", children: "spazio" }), ' ', "mostra/nascondi \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " ok \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'note' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["nota conversazione \u00B7 ", _jsx(Text, { color: "yellow", children: "^U" }), " svuota \u00B7", ' ', _jsx(Text, { color: "yellow", children: "\u23CE" }), " salva (vuoto = rimuove) \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'edit' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["edit \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2191\u2193" }), " campo \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " valore, o cursore sul testo \u00B7 ", _jsx(Text, { color: "yellow", children: "^A/^E" }), " inizio/fine \u00B7", ' ', _jsx(Text, { color: "yellow", children: "^D" }), " canc \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " salva+commit \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : (_jsx(Text, { dimColor: true, wrap: "truncate-end", children: keyLegend })), mode === 'normal' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [surfaceLegend, legend.shown ? ` · ${legend.shown}` : '', legend.overflow > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 +", legend.overflow, " fuori riga"] })) : null, legend.unreachable > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 ", legend.unreachable, " oltre la 9\u00AA (non raggiungibili)"] })) : null] })) : null, mode === 'create' ? (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: "yellow", children: "C \u203A " }), _jsx(Text, { children: draft }), _jsx(Text, { inverse: true, children: " " })] })) : null, mode === 'note' ? (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: "yellow", children: "\u270E \u203A " }), _jsx(Text, { children: noteDraft }), _jsx(Text, { inverse: true, children: " " })] })) : null, mode === 'sort' ? _jsx(SortModal, { sort: view.sort }) : null, mode === 'filter' ? _jsx(FilterModal, { view: view, cursor: filterCursor }) : null, mode === 'edit' && edit && selTask ? (_jsx(EditModal, { id: selTask.id, draft: edit, row: editRow, columns: columns })) : null, _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(TasksPane, { tasks: windowTasks, filtered: viewTasks.length, total: tasks.length, hidden: hiddenTasks, archivable: archivable, view: view, selected: selIndex, spotCount: spotCount, allCount: sessions.length, childCount: childCount, focused: focus === 'tasks', loadError: loadError, windowStart: taskWin.start, above: taskWin.start, below: viewTasks.length - taskWin.end, columns: columns }), _jsx(SessionsPane, { parentLabel: parentLabel, isSpot: isSpot, isAll: isAll, bindings: bindings, taskW: sessionCols.task, ageW: sessionCols.age, rows: windowRows, total: assembled.pinnedCount + assembled.contextTotal, pinnedCount: assembled.pinnedCount, hidden: assembled.contextHidden, selectedId: selSessionId ?? undefined, focused: focus === 'sessions', above: sessionWin.start, below: sessionRows.length - sessionWin.end, columns: columns, forkOf: forkOf, sessionNotes: sessionNotes, projectCore: projectCore })] }), budget.preview && previewKind === 'task' && detail ? (_jsx(PreviewPane, { kind: "task", detail: detail, maxLines: budget.detailLines, columns: columns })) : budget.preview && previewKind === 'session' && selSessionObj ? (_jsx(PreviewPane, { kind: "session", s: selSessionObj, firstLines: budget.sessionFirstLines, lastLines: budget.sessionLastLines, columns: columns, origin: forkOf.get(selSessionObj.sessionId) ?? null, note: sessionNotes.get(selSessionObj.sessionId) ?? '' })) : null, note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: sanitize(note) }) : null] }));
|
|
1783
1788
|
}
|
|
1784
1789
|
const SORT_UI = { pri: 'pri', prog: 'stato', id: 'id' };
|
|
1785
1790
|
// Modali resi IN FLUSSO (come l'input box di create), non in overlay assoluto:
|
|
@@ -2017,15 +2022,43 @@ function AssignScreen({ sessionId, label, current, filter, rows, selected, match
|
|
|
2017
2022
|
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
2023
|
})] }), note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: sanitize(note) }) : null] }));
|
|
2019
2024
|
}
|
|
2020
|
-
|
|
2025
|
+
/**
|
|
2026
|
+
* Header del pane task, tagliato QUI e non da Ink — stesso motivo del gemello
|
|
2027
|
+
* `SessionsHeader`, con una differenza di rischio: qui i segmenti sono tutti
|
|
2028
|
+
* ASCII più le frecce `↑↓` (larghe 1), quindi `cli-truncate` oggi darebbe la
|
|
2029
|
+
* riga giusta per caso. Il taglio resta del deck perché la correttezza non deve
|
|
2030
|
+
* dipendere dall'alfabeto che capita nella riga: il primo glifo largo 2 che
|
|
2031
|
+
* entrasse in un segmento nuovo riaprirebbe il difetto in silenzio, e a
|
|
2032
|
+
* scoprirlo sarebbe il bordo del pane a schermo.
|
|
2033
|
+
*
|
|
2034
|
+
* `truncate-end` taglia dalla coda, e `cutParts` conserva l'ordine: l'ultimo
|
|
2035
|
+
* segmento resta il primo a cedere il posto (vedi T61 sotto, `archiviabili` in
|
|
2036
|
+
* coda ai contatori della vista corrente).
|
|
2037
|
+
*/
|
|
2038
|
+
function TasksHeader({ filtered, total, hidden, above, below, archivable, focused, columns, }) {
|
|
2039
|
+
const segments = [
|
|
2040
|
+
{ text: `Tasks (${hidden > 0 ? `${filtered}/${total}` : filtered})` },
|
|
2041
|
+
...(hidden > 0 ? [{ text: ` · ${hidden} nascoste`, color: 'yellow' }] : []),
|
|
2042
|
+
...(above > 0 ? [{ text: ` · ↑${above}`, dim: true }] : []),
|
|
2043
|
+
...(below > 0 ? [{ text: ` · ↓${below}`, dim: true }] : []),
|
|
2044
|
+
...(archivable > 0 ? [{ text: ` · ${archivable} archiviabili`, dim: true }] : []),
|
|
2045
|
+
];
|
|
2046
|
+
const shown = cutParts(segments.map((s) => s.text), paneTextWidth(columns));
|
|
2047
|
+
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) }));
|
|
2048
|
+
}
|
|
2049
|
+
function TasksPane({ tasks, filtered, total, hidden, view, selected, spotCount, allCount, childCount, focused, loadError, windowStart, above, below, columns, archivable, }) {
|
|
2021
2050
|
const allSelected = selected === ROW_ALL;
|
|
2022
2051
|
const spotSelected = selected === ROW_SPOT;
|
|
2023
|
-
return (_jsxs(Box, { flexDirection: "column", width: "50%", marginRight: 1, borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', paddingX: 1, children: [
|
|
2052
|
+
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)}` +
|
|
2053
|
+
(view.hiddenPri.length + view.hiddenProg.length > 0
|
|
2054
|
+
? ' · filtri: ' +
|
|
2055
|
+
[
|
|
2024
2056
|
...PRI_ENTRIES.filter((e) => view.hiddenPri.includes(e.name)),
|
|
2025
2057
|
...PROG_ENTRIES.filter((e) => view.hiddenProg.includes(e.name)),
|
|
2026
2058
|
]
|
|
2027
|
-
.map((e) => `−${
|
|
2028
|
-
.join(' ')
|
|
2059
|
+
.map((e) => `−${e.glyph}`)
|
|
2060
|
+
.join(' ')
|
|
2061
|
+
: '')), 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
2062
|
// windowStart riporta l'indice di finestra a quello della lista
|
|
2030
2063
|
// completa, su cui è keyata la selezione. +META_ROWS: le prime due
|
|
2031
2064
|
// righe sono le meta.
|
|
@@ -2041,10 +2074,34 @@ function TasksPane({ tasks, filtered, total, hidden, view, selected, spotCount,
|
|
|
2041
2074
|
const tail = n > 0 ? ` (${n})` : '';
|
|
2042
2075
|
const desc = cut(task.desc, Math.max(4, paneTextWidth(columns) - termWidth(head) - termWidth(tail)));
|
|
2043
2076
|
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
|
-
}))
|
|
2077
|
+
}))] }));
|
|
2078
|
+
}
|
|
2079
|
+
/**
|
|
2080
|
+
* Header del pane sessioni, tagliato QUI e non da Ink.
|
|
2081
|
+
*
|
|
2082
|
+
* `📌{pinnedCount}` è un glifo largo 2 in mezzo alla riga: con
|
|
2083
|
+
* `wrap="truncate-end"` il taglio passava da `cli-truncate`, che indicizza per
|
|
2084
|
+
* code point con un budget in colonne e restituiva una riga larga 45 su un
|
|
2085
|
+
* budget di 44 — la colonna in più finiva sopra il bordo destro del pane, che
|
|
2086
|
+
* spariva dalla riga (invariante ③ di `width.ts`).
|
|
2087
|
+
*
|
|
2088
|
+
* I segmenti restano segmenti fino al render: il taglio è della RIGA (budget
|
|
2089
|
+
* condiviso, `cutParts`), la resa è del pezzo. Il giallo su `📌N` distingue le
|
|
2090
|
+
* pinnate dal resto dell'header e non è decorazione.
|
|
2091
|
+
*/
|
|
2092
|
+
function SessionsHeader({ parentLabel, total, pinnedCount, hidden, above, below, focused, columns, }) {
|
|
2093
|
+
const segments = [
|
|
2094
|
+
{ text: `Sessions · ${parentLabel} (${total})` },
|
|
2095
|
+
...(pinnedCount > 0 ? [{ text: ` · 📌${pinnedCount}`, color: 'yellow' }] : []),
|
|
2096
|
+
...(hidden > 0 ? [{ text: ` · +${hidden} più vecchie`, dim: true }] : []),
|
|
2097
|
+
...(above > 0 ? [{ text: ` · ↑${above}`, dim: true }] : []),
|
|
2098
|
+
...(below > 0 ? [{ text: ` · ↓${below}`, dim: true }] : []),
|
|
2099
|
+
];
|
|
2100
|
+
const shown = cutParts(segments.map((s) => s.text), paneTextWidth(columns));
|
|
2101
|
+
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) }));
|
|
2045
2102
|
}
|
|
2046
|
-
function SessionsPane({ parentLabel, isSpot, isAll, bindings, taskW, ageW, rows, total, pinnedCount, hidden, selectedId, focused, above, below,
|
|
2047
|
-
return (_jsxs(Box, { flexDirection: "column", width: "50%", borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', paddingX: 1, children: [
|
|
2103
|
+
function SessionsPane({ parentLabel, isSpot, isAll, bindings, taskW, ageW, rows, total, pinnedCount, hidden, selectedId, focused, above, below, columns, forkOf, sessionNotes, projectCore, }) {
|
|
2104
|
+
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
2105
|
? 'nessuna conversazione nel progetto'
|
|
2049
2106
|
: isSpot
|
|
2050
2107
|
? 'nessuna sessione libera'
|
|
@@ -2130,21 +2187,23 @@ function SessionsPane({ parentLabel, isSpot, isAll, bindings, taskW, ageW, rows,
|
|
|
2130
2187
|
(label.note && label.rest ? 1 : 0) +
|
|
2131
2188
|
termWidth(label.rest);
|
|
2132
2189
|
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));
|
|
2133
|
-
}))
|
|
2190
|
+
}))] }));
|
|
2191
|
+
}
|
|
2192
|
+
function PreviewPane(p) {
|
|
2193
|
+
return (_jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: p.kind === 'task' ? (_jsx(TaskPreview, { detail: p.detail, maxLines: p.maxLines, columns: p.columns })) : (_jsx(SessionPreview, { s: p.s, firstLines: p.firstLines, lastLines: p.lastLines, columns: p.columns, origin: p.origin, note: p.note })) }));
|
|
2134
2194
|
}
|
|
2135
|
-
// T49 —
|
|
2136
|
-
//
|
|
2137
|
-
//
|
|
2138
|
-
//
|
|
2139
|
-
//
|
|
2140
|
-
//
|
|
2141
|
-
//
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
const width = detailTextWidth(columns);
|
|
2195
|
+
// T49 — corpo della preview sessione. Tutti i campi vengono dal parse già
|
|
2196
|
+
// cached dell'adapter (mtime-keyed): non costa I/O al movimento di selezione.
|
|
2197
|
+
// Mostra "da dove parte, dove è arrivata": il primo prompt utente (`» `) e
|
|
2198
|
+
// l'ultima risposta del modello (`« `). L'anteprima del primo prompt compare
|
|
2199
|
+
// SOLO con un titolo custom — senza, il titolo È già il primo prompt e la riga
|
|
2200
|
+
// lo duplicherebbe (D4 preflight). Le righe rese non superano mai il riservato
|
|
2201
|
+
// dal budget (`firstLines`/`lastLines`); renderne meno è sicuro (frame più corto).
|
|
2202
|
+
function SessionPreview({ s, firstLines, lastLines, columns, origin, note, }) {
|
|
2203
|
+
const width = previewTextWidth(columns);
|
|
2145
2204
|
const first = s.customTitle && firstLines > 0 ? wrapLines(s.firstPrompt, width, firstLines) : [];
|
|
2146
2205
|
const last = s.lastReply && lastLines > 0 ? wrapLines(s.lastReply, width, lastLines) : [];
|
|
2147
|
-
return (_jsxs(
|
|
2206
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: [_jsx(Text, { color: "cyan", children: s.sessionId.slice(0, SID_CHARS) }), ' ', note ? _jsxs(Text, { color: "yellow", children: ["\u00AB", note, "\u00BB "] }) : null, _jsx(Text, { dimColor: Boolean(note), children: s.title })] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [fmtSize(s.sizeBytes), " \u00B7 ", s.turns, " turni \u00B7 ", fmtDateTime(s.ts), " \u00B7 ", s.gitBranch || '-', origin ? ` · ⑂ da ${origin.slice(0, 8)}` : ''] }), 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}`)))] }));
|
|
2148
2207
|
}
|
|
2149
2208
|
/**
|
|
2150
2209
|
* Righe non-wrappabili del dettaglio (titolo + meta + commit) e loro conteggio.
|
|
@@ -2159,15 +2218,16 @@ function detailMetaOf(detail) {
|
|
|
2159
2218
|
return { meta, commit, metaLines: 1 + (meta ? 1 : 0) + (commit ? 1 : 0) };
|
|
2160
2219
|
}
|
|
2161
2220
|
/**
|
|
2162
|
-
*
|
|
2163
|
-
*
|
|
2164
|
-
*
|
|
2221
|
+
* T70 — larghezza utile del testo dentro il blocco preview, che è a PIENA
|
|
2222
|
+
* larghezza: box esterno (2 bordi + 2 padding) → box preview (2 bordi + 2
|
|
2223
|
+
* padding). Niente più `/2`: il blocco non vive più dentro un pane al 50%,
|
|
2224
|
+
* quindi una riga di descrizione dispone del doppio delle colonne.
|
|
2165
2225
|
*
|
|
2166
2226
|
* Volutamente prudente: sottostimare tronca qualche carattere in più,
|
|
2167
2227
|
* sovrastimare farebbe andare a capo una riga e sforare il tetto d'altezza.
|
|
2168
2228
|
*/
|
|
2169
|
-
function
|
|
2170
|
-
return Math.max(10,
|
|
2229
|
+
function previewTextWidth(columns) {
|
|
2230
|
+
return Math.max(10, (columns || 80) - 8);
|
|
2171
2231
|
}
|
|
2172
2232
|
/**
|
|
2173
2233
|
* Larghezza del TESTO dentro un pane al 50%: box esterno (2 bordi + 2 padding)
|
|
@@ -2183,14 +2243,15 @@ function detailTextWidth(columns) {
|
|
|
2183
2243
|
function paneTextWidth(columns) {
|
|
2184
2244
|
return Math.max(20, Math.floor(((columns || 80) - 4) / 2) - 4);
|
|
2185
2245
|
}
|
|
2186
|
-
|
|
2246
|
+
/** Corpo della preview task: titolo, meta, descrizione wrappata, commit. */
|
|
2247
|
+
function TaskPreview({ detail, maxLines, columns, }) {
|
|
2187
2248
|
const { meta, commit } = detailMetaOf(detail);
|
|
2188
2249
|
// Wrap calcolato qui, non delegato a `<Text wrap="wrap">`: il budget ha
|
|
2189
2250
|
// riservato ESATTAMENTE `maxLines` righe, e un wrap deciso da Ink a runtime
|
|
2190
2251
|
// ne produrrebbe un numero che il budget non conosce — cioè il frame torna a
|
|
2191
2252
|
// sforare e il bug si riapre da questa singola casella di testo.
|
|
2192
|
-
const lines = wrapLines(detail.description ?? '',
|
|
2193
|
-
return (_jsxs(
|
|
2253
|
+
const lines = wrapLines(detail.description ?? '', previewTextWidth(columns), maxLines);
|
|
2254
|
+
return (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, wrap: "truncate-end", children: detail.title || detail.id }), meta ? _jsx(Text, { dimColor: true, wrap: "truncate-end", children: meta }) : null, lines.map((line, i) => (_jsx(Text, { wrap: "truncate-end", children: line }, i))), commit ? _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["\u21B3 ", commit] }) : null] }));
|
|
2194
2255
|
}
|
|
2195
2256
|
const cwd = process.cwd();
|
|
2196
2257
|
render(_jsx(Deck, { cwd: cwd, tasksPath: resolveTasksPath(cwd), tasksDir: resolveTasksDir(cwd) }));
|
package/dist/viewport.js
CHANGED
|
@@ -17,25 +17,25 @@
|
|
|
17
17
|
* basterebbe 1; ne teniamo 1 come margine per l'a-capo del cursore. */
|
|
18
18
|
export const SLACK = 1;
|
|
19
19
|
/** Sotto questa soglia la lista task non è più utilizzabile: meglio sacrificare
|
|
20
|
-
* il
|
|
20
|
+
* il blocco preview che ridurre la lista a due righe. */
|
|
21
21
|
const MIN_TASK_ROWS = 3;
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
/** La preview è secondaria: non si prende mai più di così, anche con spazio.
|
|
23
|
+
* A piena larghezza (T70) una riga contiene il doppio del testo di prima,
|
|
24
|
+
* quindi il tetto sale insieme allo spazio: il blocco è uno solo e lo paga
|
|
25
|
+
* una volta sola per entrambi i pane. */
|
|
26
|
+
const MAX_DETAIL_LINES = 6;
|
|
27
|
+
/** Righe di "cornice" fisse dei contenitori a lunghezza variabile. */
|
|
25
28
|
// T59 — le righe meta sono DUE (`≡ tutte`, `○ spot`): ogni riga fissa aggiunta
|
|
26
29
|
// a un pane va scalata qui, o il frame sfonda `rows` e Ink passa a
|
|
27
30
|
// clearTerminal (frame-fantasma nello scrollback di VTE).
|
|
28
31
|
const TASKS_PANE_CHROME = 6; // 2 bordi + header "Tasks (n)" + riga sort + 2 righe meta
|
|
29
32
|
const SESSIONS_PANE_CHROME = 3; // 2 bordi + header "Sessions · …"
|
|
30
|
-
const
|
|
31
|
-
/**
|
|
32
|
-
*
|
|
33
|
-
* ciascuna al più MAX_SESSION_PREVIEW righe. */
|
|
33
|
+
const PREVIEW_CHROME = 3; // marginTop + 2 bordi
|
|
34
|
+
/** Preview sessione: righe fisse = titolo + riga meta (size · turni · ultima
|
|
35
|
+
* attività · branch). Le due anteprime (primo prompt + ultima risposta) sono
|
|
36
|
+
* variabili, ciascuna al più MAX_SESSION_PREVIEW righe. */
|
|
34
37
|
const SESSION_DETAIL_FIXED = 2;
|
|
35
|
-
const MAX_SESSION_PREVIEW =
|
|
36
|
-
/** Gemello di MIN_TASK_ROWS: sotto questa soglia la lista sessioni non serve
|
|
37
|
-
* più — meglio sacrificare il detail pane. */
|
|
38
|
-
const MIN_SESSION_ROWS = 3;
|
|
38
|
+
const MAX_SESSION_PREVIEW = 3;
|
|
39
39
|
/** Altezza di ciascuna modale, marginTop incluso. In flusso, non in overlay:
|
|
40
40
|
* spingono giù i pane, quindi il loro costo va scalato dal budget. */
|
|
41
41
|
export const MODAL_HEIGHT = {
|
|
@@ -59,14 +59,21 @@ export const MODAL_HEIGHT = {
|
|
|
59
59
|
assign: 0,
|
|
60
60
|
};
|
|
61
61
|
/**
|
|
62
|
-
* Distribuisce le righe disponibili fra lista task, lista sessioni e
|
|
62
|
+
* Distribuisce le righe disponibili fra lista task, lista sessioni e preview.
|
|
63
63
|
*
|
|
64
64
|
* I due pane stanno affiancati (flexDirection row) → l'altezza del blocco è il
|
|
65
65
|
* MAX delle due colonne, non la somma: ognuna riceve lo stesso tetto.
|
|
66
66
|
*
|
|
67
|
+
* T70 — il blocco preview è UNO e sta SOTTO i due pane, non dentro uno di essi:
|
|
68
|
+
* il suo costo esce quindi da `avail` PRIMA della distribuzione, e ciò che
|
|
69
|
+
* resta va a entrambe le colonne. Con i due pannelli separati il costo gravava
|
|
70
|
+
* su una colonna sola, e siccome l'altezza del blocco è il max delle due, il
|
|
71
|
+
* pannello del pane meno alto era di fatto gratis: contabilità che qui non
|
|
72
|
+
* esiste più.
|
|
73
|
+
*
|
|
67
74
|
* Ordine di sacrificio quando lo spazio stringe:
|
|
68
|
-
* 1. righe
|
|
69
|
-
* 2. righe
|
|
75
|
+
* 1. righe variabili della preview (fino a sparire col blocco),
|
|
76
|
+
* 2. righe delle liste, mai sotto MIN_TASK_ROWS finché la preview c'è.
|
|
70
77
|
*/
|
|
71
78
|
export function layoutBudget(input) {
|
|
72
79
|
const outerChrome = 2 + // bordi del box esterno
|
|
@@ -86,58 +93,63 @@ export function layoutBudget(input) {
|
|
|
86
93
|
return {
|
|
87
94
|
taskRows: 0,
|
|
88
95
|
sessionRows: 0,
|
|
96
|
+
preview: false,
|
|
89
97
|
detailLines: 0,
|
|
90
|
-
sessionDetail: false,
|
|
91
98
|
sessionFirstLines: 0,
|
|
92
99
|
sessionLastLines: 0,
|
|
93
100
|
compact: true,
|
|
94
101
|
};
|
|
95
102
|
}
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
let sessionDetail = false;
|
|
105
|
-
let sessionDetailCost = 0;
|
|
103
|
+
// Pavimento sotto cui la preview non si concede: la lista minima del pane
|
|
104
|
+
// TASK, cioè il più caro in cornice dei due. Garantito lui, il pane sessioni
|
|
105
|
+
// (3 di cornice contro 6) sta comodo per costruzione — un secondo controllo
|
|
106
|
+
// sul suo minimo sarebbe una condizione che non può mai scattare.
|
|
107
|
+
const floor = TASKS_PANE_CHROME + MIN_TASK_ROWS;
|
|
108
|
+
let preview = false;
|
|
109
|
+
let previewCost = 0;
|
|
110
|
+
let detailLines = 0;
|
|
106
111
|
let sessionFirstLines = 0;
|
|
107
112
|
let sessionLastLines = 0;
|
|
108
|
-
if (input.
|
|
109
|
-
const fixed =
|
|
110
|
-
|
|
113
|
+
if (input.preview === 'task') {
|
|
114
|
+
const fixed = PREVIEW_CHROME + input.detailMetaLines;
|
|
115
|
+
// Serve almeno 1 riga di descrizione per giustificare il blocco: titolo e
|
|
116
|
+
// meta sono già nella riga di lista, quindi una cornice senza descrizione
|
|
117
|
+
// ruberebbe righe alle liste per non aggiungere niente.
|
|
118
|
+
const spare = avail - floor - fixed;
|
|
119
|
+
if (spare >= 1) {
|
|
120
|
+
preview = true;
|
|
121
|
+
detailLines = Math.min(MAX_DETAIL_LINES, spare);
|
|
122
|
+
previewCost = fixed + detailLines;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else if (input.preview === 'session') {
|
|
126
|
+
const fixed = PREVIEW_CHROME + SESSION_DETAIL_FIXED;
|
|
127
|
+
const spare = avail - floor - fixed;
|
|
128
|
+
// A differenza della task il blocco regge anche a zero righe variabili:
|
|
129
|
+
// titolo e riga meta (size, turni, data, branch) non stanno da nessun'altra
|
|
130
|
+
// parte a schermo, quindi sono già il valore. Priorità al primo prompt, poi
|
|
131
|
+
// l'ultima risposta prende ciò che resta: quando lo spazio stringe cade
|
|
132
|
+
// prima l'ultima risposta, non il primo prompt. Le righe si riservano solo
|
|
133
|
+
// per le anteprime che davvero renderizzeranno (i due `has…Preview`), o si
|
|
134
|
+
// toglierebbero righe alle liste per un blocco vuoto.
|
|
111
135
|
if (spare >= 0) {
|
|
112
|
-
|
|
136
|
+
preview = true;
|
|
113
137
|
if (input.sessionHasFirstPreview)
|
|
114
138
|
sessionFirstLines = Math.min(MAX_SESSION_PREVIEW, spare);
|
|
115
139
|
if (input.sessionHasLastPreview) {
|
|
116
140
|
sessionLastLines = Math.min(MAX_SESSION_PREVIEW, spare - sessionFirstLines);
|
|
117
141
|
}
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
const sessionRows = Math.max(0, avail - SESSIONS_PANE_CHROME - sessionDetailCost);
|
|
122
|
-
let detailLines = 0;
|
|
123
|
-
let detailChrome = 0;
|
|
124
|
-
if (input.hasDetail) {
|
|
125
|
-
const fixed = DETAIL_CHROME + input.detailMetaLines;
|
|
126
|
-
// Righe che avanzano dopo aver garantito la lista minima e la cornice del
|
|
127
|
-
// dettaglio. Serve almeno 1 riga di descrizione per giustificare il box:
|
|
128
|
-
// un pannello con la sola cornice ruberebbe 3+ righe per mostrare nulla.
|
|
129
|
-
const spare = avail - TASKS_PANE_CHROME - MIN_TASK_ROWS - fixed;
|
|
130
|
-
if (spare >= 1) {
|
|
131
|
-
detailChrome = fixed;
|
|
132
|
-
detailLines = Math.min(MAX_DETAIL_LINES, spare);
|
|
142
|
+
previewCost = fixed + sessionFirstLines + sessionLastLines;
|
|
133
143
|
}
|
|
134
144
|
}
|
|
135
|
-
|
|
145
|
+
// Ciò che resta va a ENTRAMBE le colonne: la preview sta sotto il blocco dei
|
|
146
|
+
// pane, quindi il suo costo lo pagano insieme una volta sola.
|
|
147
|
+
const listAvail = avail - previewCost;
|
|
136
148
|
return {
|
|
137
|
-
taskRows,
|
|
138
|
-
sessionRows,
|
|
149
|
+
taskRows: Math.max(0, listAvail - TASKS_PANE_CHROME),
|
|
150
|
+
sessionRows: Math.max(0, listAvail - SESSIONS_PANE_CHROME),
|
|
151
|
+
preview,
|
|
139
152
|
detailLines,
|
|
140
|
-
sessionDetail,
|
|
141
153
|
sessionFirstLines,
|
|
142
154
|
sessionLastLines,
|
|
143
155
|
compact: false,
|
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
|
|