@lamemind/loom-deck 0.9.1 → 0.9.2
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 +25 -24
- package/dist/config.js +6 -2
- package/dist/sessions.js +2 -1
- package/dist/tasks.js +23 -2
- package/dist/viewport.js +37 -0
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -11,7 +11,7 @@ import { resolveTasksPath, resolveTasksDir, loadTasks, loadTaskDetail, } from '.
|
|
|
11
11
|
import { discoverProjectSessions } from './sessions.js';
|
|
12
12
|
import { appendTaskBinding, loadTaskBindings } from './task-index.js';
|
|
13
13
|
import { launchLegend, loadIdentity, loadLaunch } from './config.js';
|
|
14
|
-
import { layoutBudget, windowRange, wrapLines, } from './viewport.js';
|
|
14
|
+
import { layoutBudget, normalizeEmoji, windowRange, wrapLines, } from './viewport.js';
|
|
15
15
|
import { applyView, cycleSort, describeSort, priName, progName, toggleHidden, PRI_ENTRIES, PROG_ENTRIES, } from './view.js';
|
|
16
16
|
import { initialDetail, progressText, writeTaskEdit, PRI_GLYPH, PRI_LABEL, PROG_GLYPH, } from './task-edit.js';
|
|
17
17
|
import { loadView, saveView, viewFilePath } from './view-store.js';
|
|
@@ -273,22 +273,23 @@ function truncate(s, n) {
|
|
|
273
273
|
const flat = s.replace(/\s+/g, ' ').trim();
|
|
274
274
|
return flat.length > n ? flat.slice(0, n - 1).trimEnd() + '…' : flat;
|
|
275
275
|
}
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
276
|
+
// Normalizzazione larghezza glifi → `normalizeEmoji` in viewport.ts.
|
|
277
|
+
//
|
|
278
|
+
// Sostituisce il precedente `forceEmojiWidth`, che aveva l'intuizione giusta
|
|
279
|
+
// (timbrare il VS16) ma due limiti che lasciavano passare il difetto:
|
|
280
|
+
// - agiva solo su stringhe di UN codepoint, quindi non toccava mai un testo
|
|
281
|
+
// composto — la riga della legenda launch, una descrizione task con emoji
|
|
282
|
+
// dentro, il titolo di una sessione;
|
|
283
|
+
// - decideva per intervallo di codepoint invece che per larghezza misurata,
|
|
284
|
+
// quindi avrebbe timbrato anche `↓` `↑` `−` (larghi 1) se gli fossero
|
|
285
|
+
// arrivati da soli, accorciando la riga invece di allargarla.
|
|
286
|
+
const forceEmojiWidth = normalizeEmoji;
|
|
287
|
+
// Glifi LETTERALI del JSX che ricadono nella classe mal misurata (BMP largo 2
|
|
288
|
+
// senza VS16). I dati passano dai loader, questi no: normalizzati una volta
|
|
289
|
+
// qui, così nessun sito di render li scrive nudi. `↳ ○ ▸ ⏎ · − ↑ ↓` sono
|
|
290
|
+
// larghi 1 e restano intatti.
|
|
291
|
+
const CARET = normalizeEmoji('▶ ');
|
|
292
|
+
const CARET_OFF = ' ';
|
|
292
293
|
// Normalizza il marker Done per il display. `✔` (U+2714) è text-presentation-
|
|
293
294
|
// default: string-width — quindi Ink, sia per il layout sia per il troncamento —
|
|
294
295
|
// lo misura 2 (rispetta il VS16 di `✔️`), ma VTE/Ptyxis lo disegna largo 1
|
|
@@ -738,7 +739,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
738
739
|
if (budget.compact) {
|
|
739
740
|
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 ?? 'spot', " \u00B7 terminale ", rows, "\u00D7", columns, ": troppo basso, allarga"] })] }));
|
|
740
741
|
}
|
|
741
|
-
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 === '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 \u00B7", ' ', _jsx(Text, { color: "yellow", children: "\u23CE" }), " salva+commit \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["\u2191\u2193 naviga \u00B7 \u2190\u2192 pane \u00B7 \u23CE ", canSpawn ? 'spawn' : '—', " \u00B7 C nuova \u00B7 E edit \u00B7 S sort \u00B7 F filtri \u00B7 w salva \u00B7 t term \u00B7 c claude \u00B7 q esci \u00B7 focus: ", _jsx(Text, { color: "cyan", children: focus })] })), mode === 'normal' && launch.length > 0 ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["launch ", 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 === '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 })) : null, _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(TasksPane, { tasks: windowTasks, filtered: viewTasks.length, total: tasks.length, hidden: hiddenTasks, view: view, selected: selIndex, spotCount: spotCount, childCount: childCount, focused: focus === 'tasks', loadError: loadError, detail: detail, windowStart: taskWin.start, above: taskWin.start, below: viewTasks.length - taskWin.end, detailLines: budget.detailLines, columns: columns }), _jsx(SessionsPane, { parentLabel: parentLabel, isSpot: isSpot, sessions: windowSessions, total: childSessions.length, hidden: hiddenSessions, selectedId: selSessionId, focused: focus === 'sessions', above: sessionWin.start, below: visibleSessions.length - sessionWin.end })] }), note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: note }) : null] }));
|
|
742
|
+
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 === '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 \u00B7", ' ', _jsx(Text, { color: "yellow", children: "\u23CE" }), " salva+commit \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["\u2191\u2193 naviga \u00B7 \u2190\u2192 pane \u00B7 \u23CE ", canSpawn ? 'spawn' : '—', " \u00B7 C nuova \u00B7 E edit \u00B7 S sort \u00B7 F filtri \u00B7 w salva \u00B7 t term \u00B7 c claude \u00B7 q esci \u00B7 focus: ", _jsx(Text, { color: "cyan", children: focus })] })), mode === 'normal' && launch.length > 0 ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["launch ", 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 === '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 })) : null, _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(TasksPane, { tasks: windowTasks, filtered: viewTasks.length, total: tasks.length, hidden: hiddenTasks, view: view, selected: selIndex, spotCount: spotCount, childCount: childCount, focused: focus === 'tasks', loadError: loadError, detail: detail, windowStart: taskWin.start, above: taskWin.start, below: viewTasks.length - taskWin.end, detailLines: budget.detailLines, columns: columns }), _jsx(SessionsPane, { parentLabel: parentLabel, isSpot: isSpot, sessions: windowSessions, total: childSessions.length, hidden: hiddenSessions, selectedId: selSessionId, focused: focus === 'sessions', above: sessionWin.start, below: visibleSessions.length - sessionWin.end })] }), note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: normalizeEmoji(note) }) : null] }));
|
|
742
743
|
}
|
|
743
744
|
const SORT_UI = { pri: 'pri', prog: 'stato', id: 'id' };
|
|
744
745
|
// Modali resi IN FLUSSO (come l'input box di create), non in overlay assoluto:
|
|
@@ -765,8 +766,8 @@ function FilterModal({ view, cursor }) {
|
|
|
765
766
|
// La riga di anteprima mostra il testo ESATTO che finirà nel campo `Progress`
|
|
766
767
|
// del task file — così il default (`✔️ Done at <oggi>`) non è una sorpresa.
|
|
767
768
|
function EditModal({ id, draft, row }) {
|
|
768
|
-
const mark = (r) => (row === r ?
|
|
769
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, marginTop: 1, children: [_jsxs(Text, { color: "yellow", children: ["E \u203A ", id, " \u00B7 priorit\u00E0 e stato"] }), _jsxs(Text, { children: [mark(0), _jsx(Text, { dimColor: true, children: "pri " }), EDIT_PRI.map((p) => (_jsxs(Text, { inverse: draft.pri === p, color: draft.pri === p ? 'green' : 'gray', children: [' ', forceEmojiWidth(PRI_GLYPH[p]), " ", PRI_LABEL[p]] }, p)))] }), _jsxs(Text, { children: [mark(1), _jsx(Text, { dimColor: true, children: "stato" }), EDIT_PROG.map((p) => (_jsxs(Text, { inverse: draft.prog === p, color: draft.prog === p ? 'green' : 'gray', children: [' ', forceEmojiWidth(PROG_GLYPH[p]), " ", p] }, p)))] }), _jsxs(Text, { children: [mark(2), _jsx(Text, { dimColor: true, children: "prog " }), _jsxs(Text, { children: [' ', draft.detail] }), row === 2 ? _jsx(Text, { inverse: true, children: " " }) : null, !draft.detail && row !== 2 ? _jsx(Text, { dimColor: true, children: "(default)" }) : null] }), _jsxs(Text, { dimColor: true, children: ["\u21B3 ", progressText(draft.prog, draft.detail)] })] }));
|
|
769
|
+
const mark = (r) => (row === r ? CARET : CARET_OFF);
|
|
770
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, marginTop: 1, children: [_jsxs(Text, { color: "yellow", children: ["E \u203A ", id, " \u00B7 priorit\u00E0 e stato"] }), _jsxs(Text, { children: [mark(0), _jsx(Text, { dimColor: true, children: "pri " }), EDIT_PRI.map((p) => (_jsxs(Text, { inverse: draft.pri === p, color: draft.pri === p ? 'green' : 'gray', children: [' ', forceEmojiWidth(PRI_GLYPH[p]), " ", PRI_LABEL[p]] }, p)))] }), _jsxs(Text, { children: [mark(1), _jsx(Text, { dimColor: true, children: "stato" }), EDIT_PROG.map((p) => (_jsxs(Text, { inverse: draft.prog === p, color: draft.prog === p ? 'green' : 'gray', children: [' ', forceEmojiWidth(PROG_GLYPH[p]), " ", p] }, p)))] }), _jsxs(Text, { children: [mark(2), _jsx(Text, { dimColor: true, children: "prog " }), _jsxs(Text, { children: [' ', draft.detail] }), row === 2 ? _jsx(Text, { inverse: true, children: " " }) : null, !draft.detail && row !== 2 ? _jsx(Text, { dimColor: true, children: "(default)" }) : null] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["\u21B3 ", normalizeEmoji(progressText(draft.prog, draft.detail))] })] }));
|
|
770
771
|
}
|
|
771
772
|
function TasksPane({ tasks, filtered, total, hidden, view, selected, spotCount, childCount, focused, loadError, detail, windowStart, above, below, detailLines, columns, }) {
|
|
772
773
|
const spotSelected = selected === 0;
|
|
@@ -774,19 +775,19 @@ function TasksPane({ tasks, filtered, total, hidden, view, selected, spotCount,
|
|
|
774
775
|
...PRI_ENTRIES.filter((e) => view.hiddenPri.includes(e.name)),
|
|
775
776
|
...PROG_ENTRIES.filter((e) => view.hiddenProg.includes(e.name)),
|
|
776
777
|
]
|
|
777
|
-
.map((e) => `−${e.glyph}`)
|
|
778
|
-
.join(' ')] })) : null] }), _jsxs(Text, { inverse: spotSelected && focused, bold: spotSelected && !focused, wrap: "truncate-end", children: [spotSelected ?
|
|
778
|
+
.map((e) => `−${normalizeEmoji(e.glyph)}`)
|
|
779
|
+
.join(' ')] })) : null] }), _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) => {
|
|
779
780
|
// windowStart riporta l'indice di finestra a quello della lista
|
|
780
781
|
// completa, su cui è keyata la selezione. +1: lo 0 è spot.
|
|
781
782
|
const sel = windowStart + i + 1 === selected;
|
|
782
783
|
const n = childCount.get(task.id) ?? 0;
|
|
783
|
-
return (_jsxs(Text, { inverse: sel && focused, bold: sel && !focused, dimColor: !sel && isDone(task.prog), wrap: "truncate-end", children: [sel ?
|
|
784
|
+
return (_jsxs(Text, { inverse: sel && focused, bold: sel && !focused, dimColor: !sel && isDone(task.prog), wrap: "truncate-end", children: [sel ? CARET : CARET_OFF, task.id, " ", forceEmojiWidth(task.pri), " ", displayProg(task.prog), " ", task.desc, n > 0 ? ` (${n})` : ''] }, task.id));
|
|
784
785
|
})), detail && detailLines > 0 ? (_jsx(DetailPane, { detail: detail, maxLines: detailLines, columns: columns })) : null] }));
|
|
785
786
|
}
|
|
786
787
|
function SessionsPane({ parentLabel, isSpot, sessions, total, hidden, selectedId, focused, above, below, }) {
|
|
787
788
|
return (_jsxs(Box, { flexDirection: "column", width: "50%", borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', paddingX: 1, children: [_jsxs(Text, { bold: true, color: focused ? 'cyan' : undefined, wrap: "truncate-end", children: ["Sessions \u00B7 ", parentLabel, " (", total, ")", hidden > 0 ? _jsxs(Text, { dimColor: true, children: [" \u00B7 +", hidden, " pi\u00F9 vecchie"] }) : null, above > 0 ? _jsxs(Text, { dimColor: true, children: [" \u00B7 \u2191", above] }) : null, below > 0 ? _jsxs(Text, { dimColor: true, children: [" \u00B7 \u2193", below] }) : null] }), total === 0 ? (_jsx(Text, { color: "yellow", wrap: "truncate-end", children: isSpot ? 'nessuna sessione libera' : 'nessuna sessione legata a questa task' })) : (sessions.map((s) => {
|
|
788
789
|
const sel = s.sessionId === selectedId;
|
|
789
|
-
return (_jsxs(Text, { inverse: sel && focused, bold: sel && !focused, wrap: "truncate-end", children: [sel ?
|
|
790
|
+
return (_jsxs(Text, { inverse: sel && focused, bold: sel && !focused, wrap: "truncate-end", children: [sel ? CARET : CARET_OFF, isSpot ? _jsx(Text, { dimColor: true, children: "\u25CB" }) : _jsx(Text, { color: "green", children: "\uD83D\uDD17" }), ' ', truncate(s.title, 44), ' ', _jsxs(Text, { dimColor: true, children: ["\u00B7 ", s.gitBranch || '-', " \u00B7 ", relTime(s.ts)] })] }, s.sessionId));
|
|
790
791
|
}))] }));
|
|
791
792
|
}
|
|
792
793
|
/**
|
package/dist/config.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// indice 1..9. Il `command` gira con cwd = project root.
|
|
6
6
|
import { readFileSync } from 'node:fs';
|
|
7
7
|
import { join } from 'node:path';
|
|
8
|
+
import { normalizeEmoji } from './viewport.js';
|
|
8
9
|
export function configFilePath(projectRoot) {
|
|
9
10
|
return join(projectRoot, '.claude', 'loom-works.json');
|
|
10
11
|
}
|
|
@@ -22,9 +23,12 @@ export function parseLaunch(raw) {
|
|
|
22
23
|
if (typeof command !== 'string' || !command.trim())
|
|
23
24
|
continue;
|
|
24
25
|
out.push({
|
|
25
|
-
emoji
|
|
26
|
+
// L'emoji arriva dal file config: testo arbitrario, quindi va normalizzato
|
|
27
|
+
// come ogni altro dato esterno — `☕` senza VS16 allargherebbe di una cella
|
|
28
|
+
// la riga della legenda, mandandola a capo (vedi normalizeEmoji).
|
|
29
|
+
emoji: normalizeEmoji(typeof emoji === 'string' ? emoji : '▸'),
|
|
26
30
|
// `label` è opzionale per contratto → fallback sul comando stesso.
|
|
27
|
-
label: typeof label === 'string' && label ? label : command,
|
|
31
|
+
label: normalizeEmoji(typeof label === 'string' && label ? label : command),
|
|
28
32
|
command,
|
|
29
33
|
});
|
|
30
34
|
}
|
package/dist/sessions.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { basename, join } from 'node:path';
|
|
4
|
+
import { normalizeEmoji } from './viewport.js';
|
|
4
5
|
// CC codifica la project dir sostituendo ogni char non-alfanumerico con '-'
|
|
5
6
|
// (verificato: `/home/lamemind/cc-host` → `-home-lamemind-cc-host`). È LOSSY
|
|
6
7
|
// (un '-' nel path e un '/' collassano nello stesso char) → il forward è
|
|
@@ -91,7 +92,7 @@ function parseSessionFile(path, mtime) {
|
|
|
91
92
|
cwd,
|
|
92
93
|
gitBranch,
|
|
93
94
|
parentUuid,
|
|
94
|
-
title: customTitle || firstUserText || '(senza titolo)',
|
|
95
|
+
title: normalizeEmoji(customTitle || firstUserText || '(senza titolo)'),
|
|
95
96
|
ts: mtime,
|
|
96
97
|
path,
|
|
97
98
|
};
|
package/dist/tasks.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync, readdirSync } from 'node:fs';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
|
+
import { normalizeEmoji } from './viewport.js';
|
|
3
4
|
// D1 (preflight T20): default docs/tasks.md, override della docs-root via env
|
|
4
5
|
// LOOM_DECK_DOCS_ROOT (es. questo progetto usa `runtime`). No auto-detect.
|
|
5
6
|
export function resolveTasksPath(cwd = process.cwd()) {
|
|
@@ -32,7 +33,16 @@ export function parseTasks(content) {
|
|
|
32
33
|
const prog = cells[4] ?? '';
|
|
33
34
|
// desc = colonna finale; join per resistere a eventuali `|` nella descrizione.
|
|
34
35
|
const desc = cells.slice(5, -1).join('|').trim();
|
|
35
|
-
|
|
36
|
+
// Normalizzazione larghezza glifi AL CONFINE: tutto ciò che entra da
|
|
37
|
+
// tasks.md è testo arbitrario e può contenere emoji BMP che Ink misura una
|
|
38
|
+
// cella in meno del terminale (vedi normalizeEmoji). Farlo qui invece che a
|
|
39
|
+
// ogni sito di render è ciò che impedisce di dimenticarne uno.
|
|
40
|
+
tasks.push({
|
|
41
|
+
id,
|
|
42
|
+
pri: normalizeEmoji(pri),
|
|
43
|
+
prog: normalizeEmoji(prog),
|
|
44
|
+
desc: normalizeEmoji(desc),
|
|
45
|
+
});
|
|
36
46
|
}
|
|
37
47
|
return tasks;
|
|
38
48
|
}
|
|
@@ -85,7 +95,18 @@ export function parseTaskDetail(id, content) {
|
|
|
85
95
|
fields[key] = f[2].trim();
|
|
86
96
|
}
|
|
87
97
|
}
|
|
88
|
-
|
|
98
|
+
// Stessa normalizzazione dell'overview: il task file è testo libero, e la
|
|
99
|
+
// descrizione finisce nel pannello dettaglio dove un glifo mal misurato
|
|
100
|
+
// allarga la riga oltre il bordo del pane.
|
|
101
|
+
const normFields = {};
|
|
102
|
+
for (const [k, v] of Object.entries(fields))
|
|
103
|
+
normFields[k] = normalizeEmoji(v);
|
|
104
|
+
return {
|
|
105
|
+
id,
|
|
106
|
+
title: normalizeEmoji(title),
|
|
107
|
+
fields: normFields,
|
|
108
|
+
description: normalizeEmoji(descLines.join('\n').trim()),
|
|
109
|
+
};
|
|
89
110
|
}
|
|
90
111
|
export function loadTaskDetail(dir, id) {
|
|
91
112
|
const path = findTaskFile(dir, id);
|
package/dist/viewport.js
CHANGED
|
@@ -13,6 +13,43 @@
|
|
|
13
13
|
// dettaglio) passa da qui e riceve una capienza in righe.
|
|
14
14
|
//
|
|
15
15
|
// Logica pura, zero React/Ink: testabile senza pseudo-terminale.
|
|
16
|
+
import stringWidth from 'string-width';
|
|
17
|
+
/** Variation Selector-16: forza la presentazione emoji del carattere che precede. */
|
|
18
|
+
const VS16 = '️';
|
|
19
|
+
/**
|
|
20
|
+
* Riallinea la larghezza dei glifi fra Ink e il terminale.
|
|
21
|
+
*
|
|
22
|
+
* Secondo meccanismo di sporcamento dello scrollback, indipendente dal budget
|
|
23
|
+
* d'altezza. Ink assembla ogni riga su una griglia di CARATTERI: per un emoji
|
|
24
|
+
* BMP senza VS16 (`☕` U+2615, `✔` U+2714, `⚡` U+26A1, `✅` U+2705, `▶` U+25B6)
|
|
25
|
+
* riserva una sola posizione, mentre il terminale ne disegna due. La riga esce
|
|
26
|
+
* larga `columns + 1`, il terminale la manda a capo, e l'altezza reale supera
|
|
27
|
+
* quella che Ink crede — senza mai passare dal ramo `clearTerminal`. Con più
|
|
28
|
+
* glifi sulla stessa riga l'eccesso si somma.
|
|
29
|
+
*
|
|
30
|
+
* Il VS16 esplicito fa contare 2 anche a Ink, riallineando le due contabilità.
|
|
31
|
+
*
|
|
32
|
+
* Il predicato è la LARGHEZZA MISURATA, non l'intervallo di codepoint: nello
|
|
33
|
+
* stesso blocco U+2190–U+2BFF vivono anche `↓` `↑` `−`, larghi 1, e timbrarli
|
|
34
|
+
* col VS16 li porterebbe a 2 accorciando la riga — l'errore opposto, con lo
|
|
35
|
+
* stesso effetto di layout rotto. Gli astrali (U+1F000+) Ink li conta già bene.
|
|
36
|
+
*
|
|
37
|
+
* Idempotente: un glifo che ha già il VS16 non viene ri-timbrato.
|
|
38
|
+
*/
|
|
39
|
+
export function normalizeEmoji(s) {
|
|
40
|
+
if (!s)
|
|
41
|
+
return s;
|
|
42
|
+
const cps = [...s];
|
|
43
|
+
let out = '';
|
|
44
|
+
for (let i = 0; i < cps.length; i++) {
|
|
45
|
+
const ch = cps[i];
|
|
46
|
+
out += ch;
|
|
47
|
+
const cp = ch.codePointAt(0);
|
|
48
|
+
if (cp < 0x10000 && stringWidth(ch) === 2 && cps[i + 1] !== VS16)
|
|
49
|
+
out += VS16;
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
16
53
|
/** Righe lasciate libere sotto il frame. La condizione di Ink è `>=`, quindi
|
|
17
54
|
* basterebbe 1; ne teniamo 1 come margine per l'a-capo del cursore. */
|
|
18
55
|
export const SLACK = 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lamemind/loom-deck",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "Deck TUI Ink per-progetto della famiglia loom: legge tasks.md e spawna sessioni Claude Code bound via LOOM_TASK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -42,7 +42,8 @@
|
|
|
42
42
|
"license": "MIT",
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"ink": "^5.1.0",
|
|
45
|
-
"react": "^18.3.1"
|
|
45
|
+
"react": "^18.3.1",
|
|
46
|
+
"string-width": "^7.2.0"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
48
49
|
"@types/node": "^22.20.0",
|