@lamemind/loom-deck 0.27.3 → 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/dist/cli.js +40 -31
- package/dist/viewport.js +61 -49
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -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:
|
|
@@ -2041,7 +2046,7 @@ function TasksHeader({ filtered, total, hidden, above, below, archivable, focuse
|
|
|
2041
2046
|
const shown = cutParts(segments.map((s) => s.text), paneTextWidth(columns));
|
|
2042
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) }));
|
|
2043
2048
|
}
|
|
2044
|
-
function TasksPane({ tasks, filtered, total, hidden, view, selected, spotCount, allCount, childCount, focused, loadError,
|
|
2049
|
+
function TasksPane({ tasks, filtered, total, hidden, view, selected, spotCount, allCount, childCount, focused, loadError, windowStart, above, below, columns, archivable, }) {
|
|
2045
2050
|
const allSelected = selected === ROW_ALL;
|
|
2046
2051
|
const spotSelected = selected === ROW_SPOT;
|
|
2047
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)}` +
|
|
@@ -2069,7 +2074,7 @@ function TasksPane({ tasks, filtered, total, hidden, view, selected, spotCount,
|
|
|
2069
2074
|
const tail = n > 0 ? ` (${n})` : '';
|
|
2070
2075
|
const desc = cut(task.desc, Math.max(4, paneTextWidth(columns) - termWidth(head) - termWidth(tail)));
|
|
2071
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));
|
|
2072
|
-
}))
|
|
2077
|
+
}))] }));
|
|
2073
2078
|
}
|
|
2074
2079
|
/**
|
|
2075
2080
|
* Header del pane sessioni, tagliato QUI e non da Ink.
|
|
@@ -2095,7 +2100,7 @@ function SessionsHeader({ parentLabel, total, pinnedCount, hidden, above, below,
|
|
|
2095
2100
|
const shown = cutParts(segments.map((s) => s.text), paneTextWidth(columns));
|
|
2096
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) }));
|
|
2097
2102
|
}
|
|
2098
|
-
function SessionsPane({ parentLabel, isSpot, isAll, bindings, taskW, ageW, rows, total, pinnedCount, hidden, selectedId, focused, above, below,
|
|
2103
|
+
function SessionsPane({ parentLabel, isSpot, isAll, bindings, taskW, ageW, rows, total, pinnedCount, hidden, selectedId, focused, above, below, columns, forkOf, sessionNotes, projectCore, }) {
|
|
2099
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
|
|
2100
2105
|
? 'nessuna conversazione nel progetto'
|
|
2101
2106
|
: isSpot
|
|
@@ -2182,21 +2187,23 @@ function SessionsPane({ parentLabel, isSpot, isAll, bindings, taskW, ageW, rows,
|
|
|
2182
2187
|
(label.note && label.rest ? 1 : 0) +
|
|
2183
2188
|
termWidth(label.rest);
|
|
2184
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));
|
|
2185
|
-
}))
|
|
2190
|
+
}))] }));
|
|
2186
2191
|
}
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
//
|
|
2191
|
-
//
|
|
2192
|
-
//
|
|
2193
|
-
//
|
|
2194
|
-
//
|
|
2195
|
-
|
|
2196
|
-
|
|
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 })) }));
|
|
2194
|
+
}
|
|
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);
|
|
2197
2204
|
const first = s.customTitle && firstLines > 0 ? wrapLines(s.firstPrompt, width, firstLines) : [];
|
|
2198
2205
|
const last = s.lastReply && lastLines > 0 ? wrapLines(s.lastReply, width, lastLines) : [];
|
|
2199
|
-
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}`)))] }));
|
|
2200
2207
|
}
|
|
2201
2208
|
/**
|
|
2202
2209
|
* Righe non-wrappabili del dettaglio (titolo + meta + commit) e loro conteggio.
|
|
@@ -2211,15 +2218,16 @@ function detailMetaOf(detail) {
|
|
|
2211
2218
|
return { meta, commit, metaLines: 1 + (meta ? 1 : 0) + (commit ? 1 : 0) };
|
|
2212
2219
|
}
|
|
2213
2220
|
/**
|
|
2214
|
-
*
|
|
2215
|
-
*
|
|
2216
|
-
*
|
|
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.
|
|
2217
2225
|
*
|
|
2218
2226
|
* Volutamente prudente: sottostimare tronca qualche carattere in più,
|
|
2219
2227
|
* sovrastimare farebbe andare a capo una riga e sforare il tetto d'altezza.
|
|
2220
2228
|
*/
|
|
2221
|
-
function
|
|
2222
|
-
return Math.max(10,
|
|
2229
|
+
function previewTextWidth(columns) {
|
|
2230
|
+
return Math.max(10, (columns || 80) - 8);
|
|
2223
2231
|
}
|
|
2224
2232
|
/**
|
|
2225
2233
|
* Larghezza del TESTO dentro un pane al 50%: box esterno (2 bordi + 2 padding)
|
|
@@ -2235,14 +2243,15 @@ function detailTextWidth(columns) {
|
|
|
2235
2243
|
function paneTextWidth(columns) {
|
|
2236
2244
|
return Math.max(20, Math.floor(((columns || 80) - 4) / 2) - 4);
|
|
2237
2245
|
}
|
|
2238
|
-
|
|
2246
|
+
/** Corpo della preview task: titolo, meta, descrizione wrappata, commit. */
|
|
2247
|
+
function TaskPreview({ detail, maxLines, columns, }) {
|
|
2239
2248
|
const { meta, commit } = detailMetaOf(detail);
|
|
2240
2249
|
// Wrap calcolato qui, non delegato a `<Text wrap="wrap">`: il budget ha
|
|
2241
2250
|
// riservato ESATTAMENTE `maxLines` righe, e un wrap deciso da Ink a runtime
|
|
2242
2251
|
// ne produrrebbe un numero che il budget non conosce — cioè il frame torna a
|
|
2243
2252
|
// sforare e il bug si riapre da questa singola casella di testo.
|
|
2244
|
-
const lines = wrapLines(detail.description ?? '',
|
|
2245
|
-
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] }));
|
|
2246
2255
|
}
|
|
2247
2256
|
const cwd = process.cwd();
|
|
2248
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/package.json
CHANGED