@lamemind/loom-deck 0.54.0 → 0.55.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 +28 -27
- package/dist/glyphs.js +0 -4
- package/dist/pane-views.js +9 -14
- package/dist/session-list.js +58 -92
- package/dist/ui/panes.js +17 -20
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
6
6
|
import { resolveTasksPath, resolveTasksDir, loadTasks, loadTaskFileText, } from './tasks.js';
|
|
7
7
|
import { rowIndexOfKey, selectedRow, } from './search.js';
|
|
8
8
|
import { appendNote, appendPin, appendSessionRecord, appendTaskBinding, } from './task-index.js';
|
|
9
|
-
import { assembleSessionList, firstSelectableId, moveSelection, neighborId, rowIndexOf, selectedSession,
|
|
9
|
+
import { assembleSessionList, firstSelectableId, moveSelection, neighborId, rowIndexOf, selectedSession, } from './session-list.js';
|
|
10
10
|
import { cellWidth, launchLegend, LAUNCH_SEP, loadArchivableDays, loadIdentity, loadLaunch, } from './config.js';
|
|
11
11
|
import { anchorFrame, enableMouse, FRAME_TEXT_COL, hitRegion, inlineRegions, isWheel, LAUNCH_ROW, listHit, PANE_TEXT_PAD, paneSpans, rowRegions, takeMouse, WHEEL_LINES, wheelDir, } from './mouse.js';
|
|
12
12
|
import { headerItems, sessionHeaderParts, taskHeaderParts } from './pane-header.js';
|
|
@@ -251,9 +251,9 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
251
251
|
const bound = bindings.get(s.sessionId);
|
|
252
252
|
return selectedTaskId ? bound === selectedTaskId : !bound;
|
|
253
253
|
}), [sessions, bindings, selectedTaskId, isAll]);
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
//
|
|
254
|
+
// T133 — lista unica: le figlie del parent, `ts desc`, cap su tutte. Le
|
|
255
|
+
// pinnate del progetto escono a parte (`pinnedRows`), per la sola vista `📌`.
|
|
256
|
+
// Core PURO in session-list.ts (testabile senza Ink).
|
|
257
257
|
const assembled = useMemo(() => assembleSessionList(childSessions, sessions, pinned, isAll ? MAX_SESSIONS_ALL : MAX_SESSIONS), [childSessions, sessions, pinned, isAll]);
|
|
258
258
|
// T62 — contato sulla lista INTERA (stessa ragione delle larghezze di colonna
|
|
259
259
|
// qui sotto): derivarlo dalla finestra visibile lo farebbe cambiare a ogni
|
|
@@ -261,16 +261,14 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
261
261
|
// T100 — «intera» ora vuol dire la lista della vista di DEFAULT (`assembled`),
|
|
262
262
|
// non quella a schermo: il contatore di una voce del catalogo non può
|
|
263
263
|
// dipendere da quale voce è selezionata, o navigare muoverebbe i numeri.
|
|
264
|
-
const liveCount = useMemo(() => assembled.rows.filter((r) =>
|
|
264
|
+
const liveCount = useMemo(() => assembled.rows.filter((r) => live.has(r.sessionId)).length, [assembled, live]);
|
|
265
265
|
const sessionCounts = {
|
|
266
|
-
total: assembled.
|
|
266
|
+
total: assembled.contextTotal,
|
|
267
267
|
live: liveCount,
|
|
268
|
-
pinned: assembled.
|
|
269
|
-
older: assembled.
|
|
268
|
+
pinned: assembled.pinnedRows.length,
|
|
269
|
+
older: assembled.overflowRows.length,
|
|
270
270
|
};
|
|
271
|
-
// T100 — le righe a schermo sono quelle della vista attiva.
|
|
272
|
-
// di default il separatore non c'è: segna il confine fra pinnate e
|
|
273
|
-
// contestuali, e in un sottoinsieme quel confine non esiste più.
|
|
271
|
+
// T100 — le righe a schermo sono quelle della vista attiva.
|
|
274
272
|
const sessionRows = useMemo(() => selectSessionRows(sessionViewId, { assembled, isLive: (id) => live.has(id) }), [sessionViewId, assembled, live]);
|
|
275
273
|
const selSessionObj = selectedSession(sessionRows, selSessionId);
|
|
276
274
|
// T60 — larghezze delle colonne fisse della lista sessioni, misurate sulla
|
|
@@ -282,18 +280,15 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
282
280
|
let task = 0;
|
|
283
281
|
let age = 2;
|
|
284
282
|
for (const r of sessionRows) {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
//
|
|
288
|
-
|
|
289
|
-
// OGNI riga pinnata, in qualunque vista — una pinnata è sganciata dal
|
|
290
|
-
// parent selezionato, quindi l'header del pane non parla per lei.
|
|
291
|
-
if (isAll || r.kind === 'pinned') {
|
|
283
|
+
// T133 D10 — la colonna serve dove l'appartenenza NON è già scritta
|
|
284
|
+
// altrove sullo schermo, cioè nella sola vista "tutte": ogni altra lista
|
|
285
|
+
// contiene le figlie del parent selezionato, che l'header nomina.
|
|
286
|
+
if (isAll) {
|
|
292
287
|
const b = bindings.get(r.sessionId);
|
|
293
288
|
if (b)
|
|
294
289
|
task = Math.max(task, termWidth(b));
|
|
295
290
|
}
|
|
296
|
-
if (r.session)
|
|
291
|
+
if (r.kind === 'session')
|
|
297
292
|
age = Math.max(age, termWidth(relTime(r.session.ts)));
|
|
298
293
|
}
|
|
299
294
|
// La cella vuota deve poter entrare nella colonna, o le righe spot
|
|
@@ -1122,7 +1117,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1122
1117
|
}
|
|
1123
1118
|
else {
|
|
1124
1119
|
const row = windowRows[hit.index];
|
|
1125
|
-
if (!row
|
|
1120
|
+
if (!row)
|
|
1126
1121
|
return;
|
|
1127
1122
|
if (focus === 'sessions' && row.sessionId === selSessionId) {
|
|
1128
1123
|
onKey('', { ...NO_MODIFIERS, return: true });
|
|
@@ -1337,12 +1332,12 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1337
1332
|
}
|
|
1338
1333
|
else {
|
|
1339
1334
|
const isPinned = pinned.has(selSessionId);
|
|
1340
|
-
//
|
|
1341
|
-
//
|
|
1342
|
-
//
|
|
1343
|
-
//
|
|
1344
|
-
//
|
|
1345
|
-
const landing = isPinned ?
|
|
1335
|
+
// T133 D12 — il caret si sposta solo quando la riga ESCE dalla lista, e
|
|
1336
|
+
// succede nella sola vista `📌` (spinnare toglie la riga dall'insieme che
|
|
1337
|
+
// la vista mostra). Altrove pin e unpin non riordinano niente: la riga è
|
|
1338
|
+
// in lista perché è figlia del parent, e ci resta al suo posto. Il vicino
|
|
1339
|
+
// va calcolato PRIMA di riscrivere il sidecar, quando la riga c'è ancora.
|
|
1340
|
+
const landing = isPinned && sessionViewId === 'pinned' ? neighborId(sessionRows, selSessionId) : null;
|
|
1346
1341
|
appendPin(cwd, selSessionId, !isPinned);
|
|
1347
1342
|
reloadSessions();
|
|
1348
1343
|
if (landing)
|
|
@@ -1638,7 +1633,13 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1638
1633
|
if (budget.compact) {
|
|
1639
1634
|
return (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), _jsxs(Text, { dimColor: true, children: [' ', "v", VERSION, " \u00B7 ", viewTasks.length, " task \u00B7 sel ", selectedTaskId ?? parentLabel, " \u00B7 terminale ", rows, "\u00D7", columns, ": troppo basso, allarga"] })] }));
|
|
1640
1635
|
}
|
|
1641
|
-
|
|
1636
|
+
// Segmento destro della testata: dimensione del terminale in CELLE
|
|
1637
|
+
// (colonne×righe, mai pixel — un processo dentro un terminale vede solo la
|
|
1638
|
+
// griglia di caratteri) e versione. La parte sinistra riceve un budget
|
|
1639
|
+
// derivato da questa stringa e non da una lunghezza fissa: la risoluzione
|
|
1640
|
+
// cambia a ogni resize, quindi anche la larghezza che occupa.
|
|
1641
|
+
const headerRight = `${columns}×${rows} · v${VERSION}`;
|
|
1642
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(StatusHeadline, { name: projectCore ?? projectName, label: status.label, building: status.building, failed: status.failed, cols: Math.max(4, columns - 4 - termWidth(headerRight) - 1) }), _jsx(Text, { dimColor: true, children: headerRight })] }), 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: ["titolo 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 === 'purge' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["elimina task \u00B7", ' ', purge?.ignored ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " keep/purge dei file non tracciati \u00B7", ' '] })) : null, _jsx(Text, { color: "yellow", children: "\u23CE" }), " conferma \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: [launchSegments.map((s) => s.text).join(LAUNCH_SEP), 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: editCursor.row, caret: editCursor.caret, columns: columns })) : null, mode === 'purge' && purge ? _jsx(PurgeModal, { draft: purge, columns: columns }) : null, _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(TasksPane, { tasks: windowTasks, counts: taskCounts, activeView: taskViewId, paneCount: paneTasks.length, view: view, selected: selIndex, spotCount: spotCount, allCount: sessions.length, idW: taskCols.id, tailW: taskCols.tail, focused: focus === 'tasks', loadError: loadError, windowStart: taskWin.start, above: taskWin.start, below: paneTasks.length - taskWin.end, columns: columns, data: taskRowData }), _jsx(SessionsPane, { parentLabel: parentLabel, isSpot: isSpot, isAll: isAll, bindings: bindings, taskW: sessionCols.task, ageW: sessionCols.age, rows: windowRows, counts: sessionCounts, activeView: sessionViewId, paneCount: sessionRows.length, selectedId: selSessionId ?? undefined, focused: focus === 'sessions', above: sessionWin.start, below: sessionRows.length - sessionWin.end, columns: columns, forkOf: forkOf, sessionNotes: sessionNotes, projectCore: projectCore, live: live })] }), 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) ?? '', live: live.get(selSessionObj.sessionId) ?? null })) : null, note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: sanitize(note) }) : null] }));
|
|
1642
1643
|
}
|
|
1643
1644
|
const cwd = process.cwd();
|
|
1644
1645
|
// T116 — `exitOnCtrlC: false` toglie a Ink l'uscita immediata su `^C`: senza,
|
package/dist/glyphs.js
CHANGED
|
@@ -8,10 +8,6 @@ import { sanitize } from './width.js';
|
|
|
8
8
|
export const CARET = sanitize('▶ ');
|
|
9
9
|
export const CARET_OFF = ' ';
|
|
10
10
|
export const WARN = sanitize('⚠');
|
|
11
|
-
// T50 — separatore leggero fra blocco pinnate e contestuali. `─` (box-drawing) è
|
|
12
|
-
// largo 1 sia per string-width sia per il terminale. Corto +
|
|
13
|
-
// wrap="truncate-end" così non va mai a capo nel pane al 50%.
|
|
14
|
-
export const SESSION_SEP = '─'.repeat(16);
|
|
15
11
|
// Prefisso del sessionId mostrato in lista: stesso dato e stessa lunghezza del
|
|
16
12
|
// widget `⛓ <8 char>` della statusline, così le due superfici si confrontano a
|
|
17
13
|
// occhio.
|
package/dist/pane-views.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// vista raggiungibile con `tab`.
|
|
5
5
|
//
|
|
6
6
|
// Modulo PURO: nessun import da ink/react, nessun I/O. Il calcolo resta dove
|
|
7
|
-
// stava — `view.ts` per i filtri task, `session-list.ts` per l'assemblaggio
|
|
8
|
-
//
|
|
7
|
+
// stava — `view.ts` per i filtri task, `session-list.ts` per l'assemblaggio
|
|
8
|
+
// della lista — qui c'è solo la mappa di quali sottoinsiemi esistono, come si
|
|
9
9
|
// chiamano e con che numero.
|
|
10
10
|
//
|
|
11
11
|
// Invarianti (decisioni congelate al create-task e al preflight):
|
|
@@ -65,11 +65,6 @@ export function selectTasks(tasks, id, ctx) {
|
|
|
65
65
|
* concorde (largo 1 per entrambe le contabilità), quindi non passa da
|
|
66
66
|
* `sanitize`. */
|
|
67
67
|
const LIVE_GLYPH = '●';
|
|
68
|
-
/** Righe selezionabili della lista assemblata (il separatore non è una riga di
|
|
69
|
-
* contenuto: fuori dalla vista di default i due gruppi non ci sono più). */
|
|
70
|
-
function selectable(rows) {
|
|
71
|
-
return rows.filter((r) => r.kind !== 'separator');
|
|
72
|
-
}
|
|
73
68
|
export const SESSION_VIEWS = [
|
|
74
69
|
{
|
|
75
70
|
id: 'context',
|
|
@@ -82,15 +77,19 @@ export const SESSION_VIEWS = [
|
|
|
82
77
|
id: 'live',
|
|
83
78
|
label: (c) => `${LIVE_GLYPH}${c.live} vive`,
|
|
84
79
|
count: (c) => c.live,
|
|
85
|
-
rows: (ctx) =>
|
|
80
|
+
rows: (ctx) => ctx.assembled.rows.filter((r) => ctx.isLive(r.sessionId)),
|
|
86
81
|
color: 'green',
|
|
87
82
|
empty: 'nessuna conversazione viva in questa lista',
|
|
88
83
|
},
|
|
89
84
|
{
|
|
85
|
+
// T133 D7 — sola vista che NON filtra la lista: il suo insieme è più largo
|
|
86
|
+
// (le pinnate di ogni task, più le stale), quindi il core lo consegna a
|
|
87
|
+
// parte. Un filtro su `rows` mostrerebbe le sole pinnate del parent
|
|
88
|
+
// selezionato, cioè la risposta sbagliata alla domanda «quali ho pinnato».
|
|
90
89
|
id: 'pinned',
|
|
91
90
|
label: (c) => `📌${c.pinned}`,
|
|
92
91
|
count: (c) => c.pinned,
|
|
93
|
-
rows: (ctx) => ctx.assembled.
|
|
92
|
+
rows: (ctx) => ctx.assembled.pinnedRows,
|
|
94
93
|
color: 'yellow',
|
|
95
94
|
empty: 'nessuna conversazione pinnata',
|
|
96
95
|
},
|
|
@@ -100,11 +99,7 @@ export const SESSION_VIEWS = [
|
|
|
100
99
|
id: 'older',
|
|
101
100
|
label: (c) => `+${c.older} più vecchie`,
|
|
102
101
|
count: (c) => c.older,
|
|
103
|
-
rows: (ctx) => ctx.assembled.
|
|
104
|
-
kind: 'context',
|
|
105
|
-
sessionId: s.sessionId,
|
|
106
|
-
session: s,
|
|
107
|
-
})),
|
|
102
|
+
rows: (ctx) => ctx.assembled.overflowRows,
|
|
108
103
|
dim: true,
|
|
109
104
|
empty: 'nessuna conversazione oltre il cap',
|
|
110
105
|
},
|
package/dist/session-list.js
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// T133 — Assemblaggio della lista sessioni: UNA lista sola, le figlie del parent
|
|
2
|
+
// selezionato in ordine `ts desc`. Il pin è un ATTRIBUTO della riga (glifo, vista
|
|
3
|
+
// dedicata), non una posizione privilegiata.
|
|
3
4
|
// Modulo PURO: nessun import da ink/react, nessun I/O → testabile senza terminale
|
|
4
5
|
// (il pacchetto non ha infrastruttura TUI di test, solo unit sui core puri).
|
|
5
6
|
//
|
|
6
|
-
// Invarianti (decisioni congelate al preflight):
|
|
7
|
-
// -
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// -
|
|
11
|
-
//
|
|
12
|
-
// -
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
7
|
+
// Invarianti (decisioni congelate al preflight T133):
|
|
8
|
+
// - D1/D3: una forma sola di lista qualunque sia il parent, vista «tutte»
|
|
9
|
+
// compresa. Nessun criterio speciale per le righe pinnate: nessun blocco in
|
|
10
|
+
// cima, nessun ordine proprio, nessuna esenzione dal cap (D6).
|
|
11
|
+
// - D4: una pinnata compare in lista se e solo se è figlia del parent
|
|
12
|
+
// selezionato. Le pinnate fuori contesto vivono nella sola vista `📌`.
|
|
13
|
+
// - D7: le righe pinnate escono in un CAMPO A PARTE (`pinnedRows`), non dentro
|
|
14
|
+
// `rows`: il loro insieme è più largo della lista (tutte le sessioni del
|
|
15
|
+
// progetto più le stale), quindi non è esprimibile come filtro di `rows`.
|
|
16
|
+
// - D8: `pinnedRows` conserva l'ordine per RANGO di pin desc (ultima pinnata in
|
|
17
|
+
// cima), dato dal sidecar `session-tasks.jsonl`. È l'unico lettore rimasto del
|
|
18
|
+
// rango.
|
|
19
|
+
// - D5: una pinnata il cui transcript non esiste più è una riga STALE, presente
|
|
20
|
+
// nelle sole `pinnedRows` — navigabile e spinnabile dalla vista `📌`.
|
|
21
|
+
// - Trap T39: la selezione è KEYED SU sessionId, non su indice posizionale. Le
|
|
22
|
+
// righe a schermo sono quelle della vista attiva, e un indice grezzo
|
|
23
|
+
// identificherebbe la riga sbagliata al primo cambio di vista o di parent.
|
|
17
24
|
import { cut, termWidth } from './width.js';
|
|
18
25
|
// ── T53 · etichetta di riga ────────────────────────────────────────────────
|
|
19
26
|
/**
|
|
@@ -121,66 +128,50 @@ export function rowLabel(text, note, budget) {
|
|
|
121
128
|
return { note: shownNote, rest: restBudget >= MIN_REST ? cut(rest, restBudget) : '' };
|
|
122
129
|
}
|
|
123
130
|
export function assembleSessionList(childSessions, allSessions, pinned, maxContext) {
|
|
124
|
-
// Le pinnate si risolvono contro TUTTE le sessioni del progetto, non solo le
|
|
125
|
-
// figlie del parent: una pinnata può appartenere a un'altra task. Assente
|
|
126
|
-
// dallo store → stale.
|
|
127
|
-
const byId = new Map(allSessions.map((s) => [s.sessionId, s]));
|
|
128
|
-
const pinnedIds = [...pinned.entries()]
|
|
129
|
-
.sort((a, b) => b[1] - a[1]) // rango desc → ultima pinnata in cima (D2)
|
|
130
|
-
.map(([id]) => id);
|
|
131
|
-
const pinnedRows = pinnedIds.map((id) => {
|
|
132
|
-
const session = byId.get(id) ?? null;
|
|
133
|
-
return { kind: 'pinned', sessionId: id, session, stale: session === null };
|
|
134
|
-
});
|
|
135
|
-
const pinnedSet = new Set(pinnedIds);
|
|
136
|
-
const dedupedContext = childSessions.filter((s) => !pinnedSet.has(s.sessionId));
|
|
137
131
|
const cap = Math.max(0, maxContext);
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
const contextRows = shownContext.map((s) => ({
|
|
141
|
-
kind: 'context',
|
|
132
|
+
const toRow = (s) => ({
|
|
133
|
+
kind: 'session',
|
|
142
134
|
sessionId: s.sessionId,
|
|
143
135
|
session: s,
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
136
|
+
pinned: pinned.has(s.sessionId),
|
|
137
|
+
});
|
|
138
|
+
const rows = childSessions.slice(0, cap).map(toRow);
|
|
139
|
+
const overflowRows = childSessions.slice(cap).map(toRow);
|
|
140
|
+
// Le pinnate si risolvono contro TUTTE le sessioni del progetto, non solo le
|
|
141
|
+
// figlie del parent: una pinnata può appartenere a un'altra task, e la vista
|
|
142
|
+
// dedicata è l'unico posto dove resta raggiungibile. Assente dallo store →
|
|
143
|
+
// stale.
|
|
144
|
+
const byId = new Map(allSessions.map((s) => [s.sessionId, s]));
|
|
145
|
+
const pinnedRows = [...pinned.entries()]
|
|
146
|
+
.sort((a, b) => b[1] - a[1]) // rango desc → ultima pinnata in cima (D8)
|
|
147
|
+
.map(([id]) => {
|
|
148
|
+
const session = byId.get(id);
|
|
149
|
+
return session
|
|
150
|
+
? { kind: 'session', sessionId: id, session, pinned: true }
|
|
151
|
+
: { kind: 'stale', sessionId: id };
|
|
152
|
+
});
|
|
153
|
+
return { rows, pinnedRows, contextTotal: childSessions.length, overflowRows };
|
|
161
154
|
}
|
|
162
|
-
/** Primo id
|
|
155
|
+
/** Primo id della lista; null = lista senza righe. */
|
|
163
156
|
export function firstSelectableId(rows) {
|
|
164
|
-
return rows
|
|
157
|
+
return rows[0]?.sessionId ?? null;
|
|
165
158
|
}
|
|
166
159
|
/** Indice della riga con quel sessionId nell'ARRAY COMPLETO (per il windowing);
|
|
167
|
-
* -1 se assente o id null.
|
|
160
|
+
* -1 se assente o id null. */
|
|
168
161
|
export function rowIndexOf(rows, sessionId) {
|
|
169
162
|
if (sessionId === null)
|
|
170
163
|
return -1;
|
|
171
|
-
return rows.findIndex((r) =>
|
|
164
|
+
return rows.findIndex((r) => r.sessionId === sessionId);
|
|
172
165
|
}
|
|
173
|
-
/** Sposta la selezione di `delta
|
|
174
|
-
* separatore senza fermarcisi). Id perso → prima riga; lista vuota → null. */
|
|
166
|
+
/** Sposta la selezione di `delta`. Id perso → prima riga; lista vuota → null. */
|
|
175
167
|
export function moveSelection(rows, currentId, delta) {
|
|
176
|
-
|
|
177
|
-
if (selectable.length === 0)
|
|
168
|
+
if (rows.length === 0)
|
|
178
169
|
return null;
|
|
179
|
-
const cur =
|
|
170
|
+
const cur = rows.findIndex((r) => r.sessionId === currentId);
|
|
180
171
|
if (cur < 0)
|
|
181
|
-
return
|
|
182
|
-
const next = Math.max(0, Math.min(
|
|
183
|
-
return
|
|
172
|
+
return rows[0].sessionId;
|
|
173
|
+
const next = Math.max(0, Math.min(rows.length - 1, cur + delta));
|
|
174
|
+
return rows[next].sessionId;
|
|
184
175
|
}
|
|
185
176
|
/**
|
|
186
177
|
* T57 — id su cui atterrare quando la riga `sessionId` sta per USCIRE dalla
|
|
@@ -192,44 +183,19 @@ export function moveSelection(rows, currentId, delta) {
|
|
|
192
183
|
* assegnazione rimanderebbe la selezione in cima. L'id va catturato PRIMA di
|
|
193
184
|
* riscrivere il sidecar — dopo, la riga non c'è più e il vicino non è
|
|
194
185
|
* calcolabile.
|
|
195
|
-
*/
|
|
196
|
-
export function neighborId(rows, sessionId) {
|
|
197
|
-
const selectable = rows.filter(isSelectable);
|
|
198
|
-
const at = selectable.findIndex((r) => r.sessionId === sessionId);
|
|
199
|
-
if (at < 0)
|
|
200
|
-
return null;
|
|
201
|
-
return selectable[at + 1]?.sessionId ?? selectable[at - 1]?.sessionId ?? null;
|
|
202
|
-
}
|
|
203
|
-
/**
|
|
204
|
-
* Id su cui atterrare quando la riga pinnata `sessionId` viene SPINNATA: la
|
|
205
|
-
* pinnata successiva, la precedente se era l'ultima, la prima contestuale se il
|
|
206
|
-
* gruppo pinnate si svuota. `null` se `sessionId` non è pinnata o non resta
|
|
207
|
-
* nessuna riga.
|
|
208
186
|
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
* fra i due gruppi è la regola, non un ostacolo.
|
|
213
|
-
*
|
|
214
|
-
* L'id va calcolato PRIMA di riscrivere il sidecar: dopo, la riga pinnata non
|
|
215
|
-
* c'è più e il vicino nel gruppo non è calcolabile. Serve perché una spinnata
|
|
216
|
-
* non sparisce necessariamente dalla lista — se appartiene al parent
|
|
217
|
-
* selezionato ricompare fra le contestuali, e la selezione keyed sull'id la
|
|
218
|
-
* seguirebbe fin laggiù, trascinando il caret.
|
|
187
|
+
* T133 D12 — stesso servizio per l'unpin dentro la vista `📌`, dove spinnare fa
|
|
188
|
+
* uscire la riga. Nella lista principale non serve: lì una pinnata è in lista
|
|
189
|
+
* perché è figlia del parent, e spinnarla non la fa uscire.
|
|
219
190
|
*/
|
|
220
|
-
export function
|
|
221
|
-
const
|
|
222
|
-
const at = pinnedRows.findIndex((r) => r.sessionId === sessionId);
|
|
191
|
+
export function neighborId(rows, sessionId) {
|
|
192
|
+
const at = rows.findIndex((r) => r.sessionId === sessionId);
|
|
223
193
|
if (at < 0)
|
|
224
194
|
return null;
|
|
225
|
-
|
|
226
|
-
if (inGroup)
|
|
227
|
-
return inGroup;
|
|
228
|
-
const firstContext = rows.find((r) => r.kind === 'context');
|
|
229
|
-
return firstContext && isSelectable(firstContext) ? firstContext.sessionId : null;
|
|
195
|
+
return rows[at + 1]?.sessionId ?? rows[at - 1]?.sessionId ?? null;
|
|
230
196
|
}
|
|
231
197
|
/** Session della riga selezionata; null se stale o nessuna selezione. */
|
|
232
198
|
export function selectedSession(rows, sessionId) {
|
|
233
|
-
const r = rows.find((row) =>
|
|
234
|
-
return r &&
|
|
199
|
+
const r = rows.find((row) => row.sessionId === sessionId);
|
|
200
|
+
return r && r.kind === 'session' ? r.session : null;
|
|
235
201
|
}
|
package/dist/ui/panes.js
CHANGED
|
@@ -5,7 +5,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
5
5
|
import { Box, Text } from 'ink';
|
|
6
6
|
import { cut, pad, sanitize, termWidth } from '../width.js';
|
|
7
7
|
import { paneTextWidth } from '../layout.js';
|
|
8
|
-
import { CARET, CARET_OFF, LIVE_BUSY, LIVE_IDLE, LIVE_NONE, MODEL_W,
|
|
8
|
+
import { CARET, CARET_OFF, LIVE_BUSY, LIVE_IDLE, LIVE_NONE, MODEL_W, SID_CHARS, TASK_EMPTY, WARN, metaCount, modelShort, relTime, } from '../glyphs.js';
|
|
9
9
|
import { TaskRow } from './task-row.js';
|
|
10
10
|
import { META_ROWS, ROW_ALL, ROW_SPOT } from '../model.js';
|
|
11
11
|
import { rowLabel, sessionTitle } from '../session-list.js';
|
|
@@ -94,24 +94,22 @@ export function SessionsPane({ parentLabel, isSpot, isAll, bindings, taskW, ageW
|
|
|
94
94
|
? 'nessuna conversazione nel progetto'
|
|
95
95
|
: isSpot
|
|
96
96
|
? 'nessuna sessione libera'
|
|
97
|
-
: 'nessuna sessione legata a questa task'), paneTextWidth(columns)) })) : (rows.map((row
|
|
98
|
-
// T50 — separatore leggero fra pinnate e contestuali: riga dim, non un
|
|
99
|
-
// box pesante (coerente con lo styling delle Done dimmate).
|
|
100
|
-
if (row.kind === 'separator') {
|
|
101
|
-
return (_jsx(Text, { dimColor: true, wrap: "truncate-end", children: SESSION_SEP }, `sep${i}`));
|
|
102
|
-
}
|
|
97
|
+
: 'nessuna sessione legata a questa task'), paneTextWidth(columns)) })) : (rows.map((row) => {
|
|
103
98
|
const sel = row.sessionId === selectedId;
|
|
104
99
|
// T50 — pin stale: transcript sparito, nessuna Session da mostrare.
|
|
105
|
-
// Riga navigabile e spinnabile (`p`), marcata, mai un crash.
|
|
106
|
-
|
|
100
|
+
// Riga navigabile e spinnabile (`p`), marcata, mai un crash. T133 D5 —
|
|
101
|
+
// compare nella sola vista `📌`, dove non ha accanto nessuna griglia da
|
|
102
|
+
// rispettare: resta testo libero.
|
|
103
|
+
if (row.kind === 'stale') {
|
|
107
104
|
// T60 — anche qui la nota si taglia sul budget DERIVATO, non su un
|
|
108
105
|
// 30 inchiodato: su un pane stretto quel valore fisso mandava la
|
|
109
106
|
// riga oltre il bordo, e a ripararla arrivava `cli-truncate` (che
|
|
110
107
|
// sfora di una colonna per emoji e mangia il bordo stesso).
|
|
111
108
|
const staleNote = sessionNotes.get(row.sessionId);
|
|
112
109
|
// La riga stale è libera (niente colonne: non ha né titolo né
|
|
113
|
-
// data), ma il binding va detto lo stesso —
|
|
114
|
-
//
|
|
110
|
+
// data), ma il binding va detto lo stesso — vive nella vista `📌`,
|
|
111
|
+
// che raccoglie le pinnate di ogni task e quindi non ne dice
|
|
112
|
+
// l'appartenenza dall'header.
|
|
115
113
|
const staleTask = bindings.get(row.sessionId) ?? null;
|
|
116
114
|
const staleW = Math.max(0, paneTextWidth(columns) -
|
|
117
115
|
(2 /* caret */ +
|
|
@@ -121,8 +119,8 @@ export function SessionsPane({ parentLabel, isSpot, isAll, bindings, taskW, ageW
|
|
|
121
119
|
3 /* spazio + caporali */));
|
|
122
120
|
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));
|
|
123
121
|
}
|
|
124
|
-
const s = row.session;
|
|
125
|
-
const isPinnedRow = row.
|
|
122
|
+
const s = row.session;
|
|
123
|
+
const isPinnedRow = row.pinned;
|
|
126
124
|
// T28 — un ramo eredita il titolo dell'origine: senza marcatore le due
|
|
127
125
|
// righe sarebbero identiche a occhio.
|
|
128
126
|
const forked = forkOf.has(s.sessionId);
|
|
@@ -139,13 +137,12 @@ export function SessionsPane({ parentLabel, isSpot, isAll, bindings, taskW, ageW
|
|
|
139
137
|
// appartiene la conversazione (pin/task/spot), questa dice se è aperta
|
|
140
138
|
// adesso. Farle condividere una cella perderebbe una delle due.
|
|
141
139
|
const liveEntry = live.get(s.sessionId);
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
const taskCell = isAll || isPinnedRow ? (bound ?? TASK_EMPTY) : '';
|
|
140
|
+
// T133 D10 — la colonna vive nella sola vista "tutte". Il ramo sulle
|
|
141
|
+
// pinnate esisteva perché una pinnata stava in lista anche sotto un
|
|
142
|
+
// parent che non era il suo; ora ogni riga è figlia del parent
|
|
143
|
+
// selezionato, quindi l'header parla già per tutte e la cella
|
|
144
|
+
// ripeterebbe N volte ciò che è scritto una riga sopra.
|
|
145
|
+
const taskCell = isAll ? (bound ?? TASK_EMPTY) : '';
|
|
149
146
|
// T60 — colonne VERE: ogni cella fissa è larga esattamente quanto
|
|
150
147
|
// dichiara, riempita di spazi con `pad` (che misura in colonne, non in
|
|
151
148
|
// caratteri). Il marker va portato a 2 anche quando è `○`, largo 1:
|
package/package.json
CHANGED