@lamemind/loom-deck 0.8.0 → 0.9.1
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 +14 -2
- package/dist/cli.js +120 -13
- package/dist/config.js +51 -0
- package/dist/viewport.js +153 -0
- package/package.json +1 -1
- package/scripts/deck-run +66 -10
package/README.md
CHANGED
|
@@ -59,13 +59,25 @@ Assegnazioni correnti:
|
|
|
59
59
|
| | Tasto | Cosa fa |
|
|
60
60
|
|---|---|---|
|
|
61
61
|
| modale | `C` | nuova task (create-task inline) |
|
|
62
|
+
| modale | `E` | edit priorità/stato della task selezionata (salva + commit) |
|
|
62
63
|
| modale | `S` | sort chain |
|
|
63
64
|
| modale | `F` | filtri |
|
|
65
|
+
| immediata | `t` | terminale @project-root (surface standard launch) |
|
|
66
|
+
| immediata | `c` | sessione Claude **nuda**: nessuna task, nessun prompt iniziale |
|
|
64
67
|
| immediata | `w` | salva la vista corrente su disco |
|
|
65
68
|
| launch | `1`…`9` | esegue il `command` della voce, con `cwd` = project root |
|
|
66
69
|
|
|
67
|
-
Le
|
|
68
|
-
|
|
70
|
+
Le voci `launch` sono elencate in una **riga di legenda** sotto il footer
|
|
71
|
+
(`launch 1 📝 codium · 2 ☕ idea`): l'indice da solo è opaco, perché le voci sono
|
|
72
|
+
custom per-progetto e non hanno una lettera fissa per app. Se non entrano in
|
|
73
|
+
larghezza, la legenda si ferma a voci intere e mostra il contatore di quelle
|
|
74
|
+
fuori riga — mai un troncamento silenzioso. Il cap a `9` è imposto dai tasti-cifra,
|
|
75
|
+
non dallo schema: un progetto può dichiarare più di 9 voci, quelle oltre la nona
|
|
76
|
+
sono configurate ma non raggiungibili (e la legenda lo dice).
|
|
77
|
+
|
|
78
|
+
`t` e `c` sono gemelle: entrambe aprono una surface del cappello nella stessa
|
|
79
|
+
finestra Ptyxis, senza passare da un modale. `c` (minuscola, azione) e `C`
|
|
80
|
+
(maiuscola, modale create-task) restano distinte per la regola sopra.
|
|
69
81
|
|
|
70
82
|
> **Nota di migrazione (0.6.0)**: `c` → **`C`** per creare una task, e le voci
|
|
71
83
|
> `codium`/`idea` non hanno più una lettera dedicata (erano `C`/`I` hardcoded):
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { render, Box, Text, useApp, useInput } from 'ink';
|
|
3
|
+
import { render, Box, Text, useApp, useInput, useStdout } from 'ink';
|
|
4
4
|
import { useState, useEffect, useMemo } from 'react';
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
6
|
import { statSync } from 'node:fs';
|
|
@@ -10,7 +10,8 @@ import { dirname, join } from 'node:path';
|
|
|
10
10
|
import { resolveTasksPath, resolveTasksDir, loadTasks, loadTaskDetail, } from './tasks.js';
|
|
11
11
|
import { discoverProjectSessions } from './sessions.js';
|
|
12
12
|
import { appendTaskBinding, loadTaskBindings } from './task-index.js';
|
|
13
|
-
import { loadIdentity, loadLaunch } from './config.js';
|
|
13
|
+
import { launchLegend, loadIdentity, loadLaunch } from './config.js';
|
|
14
|
+
import { layoutBudget, windowRange, wrapLines, } from './viewport.js';
|
|
14
15
|
import { applyView, cycleSort, describeSort, priName, progName, toggleHidden, PRI_ENTRIES, PROG_ENTRIES, } from './view.js';
|
|
15
16
|
import { initialDetail, progressText, writeTaskEdit, PRI_GLYPH, PRI_LABEL, PROG_GLYPH, } from './task-edit.js';
|
|
16
17
|
import { loadView, saveView, viewFilePath } from './view-store.js';
|
|
@@ -50,6 +51,21 @@ function spawnDeck(id, cwd, sessionId) {
|
|
|
50
51
|
child.unref();
|
|
51
52
|
return child;
|
|
52
53
|
}
|
|
54
|
+
// T42 — sessione Claude NUDA: nessuna task, nessun prompt iniziale, nessun
|
|
55
|
+
// sessionId pinnato (quindi nessuna entry nel sidecar session-tasks.jsonl: senza
|
|
56
|
+
// task non c'è nulla da legare). Funzione separata e non un parametro opzionale
|
|
57
|
+
// di spawnDeck: i tre argomenti mancano tutti insieme, un `if` per ciascuno
|
|
58
|
+
// sporcherebbe il percorso bound. Il titolo tab resta la label loom — lo mette
|
|
59
|
+
// deck-run, perché il match compass è window-level e non sa nulla di task.
|
|
60
|
+
function spawnClaudeEmpty(cwd) {
|
|
61
|
+
const child = spawn(DECK_RUN, ['--no-task'], {
|
|
62
|
+
cwd,
|
|
63
|
+
detached: true,
|
|
64
|
+
stdio: 'ignore',
|
|
65
|
+
});
|
|
66
|
+
child.unref();
|
|
67
|
+
return child;
|
|
68
|
+
}
|
|
53
69
|
// T39/T32: voce `launch` custom del file config, eseguita con cwd = project root.
|
|
54
70
|
// Spawn detached come spawnDeck: il deck lancia ma non possiede il processo.
|
|
55
71
|
// Shell login+interattiva (bash -lic) perché i comandi tipici sono alias o
|
|
@@ -151,6 +167,29 @@ function commitTaskEdit(cwd, paths, message, onResult) {
|
|
|
151
167
|
child.on('close', (code) => onResult(code === 0, err.trim().split('\n')[0] ?? ''));
|
|
152
168
|
return child;
|
|
153
169
|
}
|
|
170
|
+
// Dimensioni del terminale, live sul resize.
|
|
171
|
+
//
|
|
172
|
+
// Non è una comodità di layout: senza `rows` il frame non ha tetto, e un frame
|
|
173
|
+
// più alto del terminale fa cadere Ink nel ramo `clearTerminal` (ink.js:121)
|
|
174
|
+
// che su VTE/Ptyxis riversa ogni redraw nello scrollback.
|
|
175
|
+
//
|
|
176
|
+
// Il valore iniziale conta quanto il resize: una tab Ptyxis appena aperta parte
|
|
177
|
+
// spesso a 24 righe e riceve il SIGWINCH subito dopo. Nella finestra fra i due
|
|
178
|
+
// il deck disegnava già a piena altezza — motivo per cui lo scrollback risultava
|
|
179
|
+
// sporco fin dall'avvio, prima ancora di toccare un tasto.
|
|
180
|
+
function useTerminalSize() {
|
|
181
|
+
const { stdout } = useStdout();
|
|
182
|
+
const [size, setSize] = useState({ rows: stdout.rows || 24, columns: stdout.columns || 80 });
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
const onResize = () => setSize({ rows: stdout.rows || 24, columns: stdout.columns || 80 });
|
|
185
|
+
stdout.on('resize', onResize);
|
|
186
|
+
onResize(); // allinea se il resize è arrivato prima del mount
|
|
187
|
+
return () => {
|
|
188
|
+
stdout.off('resize', onResize);
|
|
189
|
+
};
|
|
190
|
+
}, [stdout]);
|
|
191
|
+
return size;
|
|
192
|
+
}
|
|
154
193
|
// Carica tasks.md e lo ri-legge quando cambia sotto (poll su mtime). Poll
|
|
155
194
|
// (non fs.watch) perché i writer di tasks.md — checkpoint-task/create-task —
|
|
156
195
|
// riscrivono il file (probabile replace atomico), che rompe il watch sull'inode
|
|
@@ -300,6 +339,8 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
300
339
|
// T41 — bozza dell'edit (null fuori dal modale) e riga attiva della griglia.
|
|
301
340
|
const [edit, setEdit] = useState(null);
|
|
302
341
|
const [editRow, setEditRow] = useState(0);
|
|
342
|
+
// Dimensioni vive del terminale: sono l'input del budget d'altezza sotto.
|
|
343
|
+
const { rows, columns } = useTerminalSize();
|
|
303
344
|
// Voci launch del progetto (T32): lette una volta, raggiunte per indice 1..9.
|
|
304
345
|
const launch = useMemo(() => loadLaunch(cwd), [cwd]);
|
|
305
346
|
// Identità (T37): titolo delle tab terminale spawnate col tasto `t`.
|
|
@@ -628,6 +669,14 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
628
669
|
child.on('error', () => setNote('⚠ t → ptyxis non lanciabile'));
|
|
629
670
|
setNote(`t → terminale su ${projectName}`);
|
|
630
671
|
}
|
|
672
|
+
else if (input === 'c') {
|
|
673
|
+
// Minuscola = azione immediata (convenzione T39), gemella di `t`: entrambe
|
|
674
|
+
// aprono una surface del cappello senza passare da un modale. `C` (create
|
|
675
|
+
// task) resta distinta — stessa lettera, ma la maiuscola è per i modali.
|
|
676
|
+
const child = spawnClaudeEmpty(cwd);
|
|
677
|
+
child.on('error', () => setNote(`⚠ c → spawn claude fallito (${DECK_RUN})`));
|
|
678
|
+
setNote(`c → claude nuda su ${projectName} (nessuna task)`);
|
|
679
|
+
}
|
|
631
680
|
else if (input === 'w') {
|
|
632
681
|
// Salvataggio ESPLICITO: comporre una vista non tocca il disco, così
|
|
633
682
|
// sperimentare non sporca lo stato persistito.
|
|
@@ -657,8 +706,39 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
657
706
|
const selSessionId = visibleSessions[selSession]?.sessionId;
|
|
658
707
|
const parentLabel = isSpot ? 'spot' : selectedTaskId ?? '—';
|
|
659
708
|
const canSpawn = focus === 'tasks' && !isSpot;
|
|
660
|
-
|
|
661
|
-
|
|
709
|
+
// Larghezza dal medesimo hook che dà l'altezza: dopo un resize la legenda si
|
|
710
|
+
// ricalcola con lo stesso re-render che ridimensiona i pane.
|
|
711
|
+
const legend = launchLegend(launch, columns);
|
|
712
|
+
// ── Budget d'altezza ────────────────────────────────────────────────────
|
|
713
|
+
// Il frame deve restare sotto `rows`, sempre: oltre quella soglia Ink smette
|
|
714
|
+
// di aggiornare per differenza e pulisce lo schermo a ogni redraw, che su
|
|
715
|
+
// Ptyxis significa un frame intero versato nello scrollback per ogni tick del
|
|
716
|
+
// poll. Tutto ciò che varia in altezza (le due liste e la descrizione del
|
|
717
|
+
// dettaglio) riceve qui la propria capienza.
|
|
718
|
+
const launchLine = mode === 'normal' && launch.length > 0;
|
|
719
|
+
const detailParts = detail ? detailMetaOf(detail) : null;
|
|
720
|
+
const budget = layoutBudget({
|
|
721
|
+
rows,
|
|
722
|
+
mode,
|
|
723
|
+
launchLine,
|
|
724
|
+
noteLine: Boolean(note),
|
|
725
|
+
hasDetail: Boolean(detail),
|
|
726
|
+
detailMetaLines: detailParts?.metaLines ?? 0,
|
|
727
|
+
});
|
|
728
|
+
// Finestre di rendering. Le liste "logiche" (viewTasks, visibleSessions)
|
|
729
|
+
// restano intere: navigazione, selezione e spawn continuano a ragionare su
|
|
730
|
+
// quelle, la finestra è solo ciò che finisce a schermo.
|
|
731
|
+
const taskWin = windowRange(viewTasks.length, selIndex - 1, budget.taskRows);
|
|
732
|
+
const windowTasks = viewTasks.slice(taskWin.start, taskWin.end);
|
|
733
|
+
const sessionWin = windowRange(visibleSessions.length, selSession, budget.sessionRows);
|
|
734
|
+
const windowSessions = visibleSessions.slice(sessionWin.start, sessionWin.end);
|
|
735
|
+
// Sotto la soglia minima il layout a box non entra a nessun costo: si scende
|
|
736
|
+
// a una riga sola. Perdere il deck per un terminale basso è meglio che
|
|
737
|
+
// sporcare la cronologia del terminale a ogni poll.
|
|
738
|
+
if (budget.compact) {
|
|
739
|
+
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
|
+
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] }));
|
|
662
742
|
}
|
|
663
743
|
const SORT_UI = { pri: 'pri', prog: 'stato', id: 'id' };
|
|
664
744
|
// Modali resi IN FLUSSO (come l'input box di create), non in overlay assoluto:
|
|
@@ -688,31 +768,58 @@ function EditModal({ id, draft, row }) {
|
|
|
688
768
|
const mark = (r) => (row === r ? '▶ ' : ' ');
|
|
689
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)] })] }));
|
|
690
770
|
}
|
|
691
|
-
function TasksPane({ tasks, total, hidden, view, selected, spotCount, childCount, focused, loadError, detail, }) {
|
|
771
|
+
function TasksPane({ tasks, filtered, total, hidden, view, selected, spotCount, childCount, focused, loadError, detail, windowStart, above, below, detailLines, columns, }) {
|
|
692
772
|
const spotSelected = selected === 0;
|
|
693
|
-
return (_jsxs(Box, { flexDirection: "column", width: "50%", marginRight: 1, borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', paddingX: 1, children: [_jsxs(Text, { bold: true, color: focused ? 'cyan' : undefined, children: ["Tasks (", hidden > 0 ? `${
|
|
773
|
+
return (_jsxs(Box, { flexDirection: "column", width: "50%", marginRight: 1, borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', paddingX: 1, children: [_jsxs(Text, { bold: true, color: focused ? 'cyan' : undefined, wrap: "truncate-end", children: ["Tasks (", hidden > 0 ? `${filtered}/${total}` : filtered, ")", hidden > 0 ? _jsxs(Text, { color: "yellow", children: [" \u00B7 ", hidden, " nascoste"] }) : null, above > 0 ? _jsxs(Text, { dimColor: true, children: [" \u00B7 \u2191", above] }) : null, below > 0 ? _jsxs(Text, { dimColor: true, children: [" \u00B7 \u2193", below] }) : null] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["sort: ", describeSort(view.sort), view.hiddenPri.length + view.hiddenProg.length > 0 ? (_jsxs(Text, { children: [' ', "\u00B7 filtri:", ' ', [
|
|
694
774
|
...PRI_ENTRIES.filter((e) => view.hiddenPri.includes(e.name)),
|
|
695
775
|
...PROG_ENTRIES.filter((e) => view.hiddenProg.includes(e.name)),
|
|
696
776
|
]
|
|
697
777
|
.map((e) => `−${e.glyph}`)
|
|
698
778
|
.join(' ')] })) : null] }), _jsxs(Text, { inverse: spotSelected && focused, bold: spotSelected && !focused, wrap: "truncate-end", children: [spotSelected ? '▶ ' : ' ', "\u25CB spot sessioni libere", spotCount > 0 ? ` (${spotCount})` : ''] }), loadError ? (_jsx(Text, { color: "red", wrap: "truncate-end", children: loadError })) : (tasks.map((task, i) => {
|
|
699
|
-
|
|
779
|
+
// windowStart riporta l'indice di finestra a quello della lista
|
|
780
|
+
// completa, su cui è keyata la selezione. +1: lo 0 è spot.
|
|
781
|
+
const sel = windowStart + i + 1 === selected;
|
|
700
782
|
const n = childCount.get(task.id) ?? 0;
|
|
701
783
|
return (_jsxs(Text, { inverse: sel && focused, bold: sel && !focused, dimColor: !sel && isDone(task.prog), wrap: "truncate-end", children: [sel ? '▶ ' : ' ', task.id, " ", forceEmojiWidth(task.pri), " ", displayProg(task.prog), " ", task.desc, n > 0 ? ` (${n})` : ''] }, task.id));
|
|
702
|
-
})), detail ? _jsx(DetailPane, { detail: detail }) : null] }));
|
|
784
|
+
})), detail && detailLines > 0 ? (_jsx(DetailPane, { detail: detail, maxLines: detailLines, columns: columns })) : null] }));
|
|
703
785
|
}
|
|
704
|
-
function SessionsPane({ parentLabel, isSpot, sessions, total, hidden, selectedId, focused, }) {
|
|
705
|
-
return (_jsxs(Box, { flexDirection: "column", width: "50%", borderStyle: "single", borderColor: focused ? 'cyan' : 'gray', paddingX: 1, children: [_jsxs(Text, { bold: true, color: focused ? 'cyan' : undefined, children: ["Sessions \u00B7 ", parentLabel, " (", total, ")", hidden > 0 ? _jsxs(Text, { dimColor: true, children: [" \u00B7 +", hidden, " pi\u00F9 vecchie"] }) : null] }), total === 0 ? (_jsx(Text, { color: "yellow", wrap: "truncate-end", children: isSpot ? 'nessuna sessione libera' : 'nessuna sessione legata a questa task' })) : (sessions.map((s) => {
|
|
786
|
+
function SessionsPane({ parentLabel, isSpot, sessions, total, hidden, selectedId, focused, above, below, }) {
|
|
787
|
+
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) => {
|
|
706
788
|
const sel = s.sessionId === selectedId;
|
|
707
789
|
return (_jsxs(Text, { inverse: sel && focused, bold: sel && !focused, wrap: "truncate-end", children: [sel ? '▶ ' : ' ', 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));
|
|
708
790
|
}))] }));
|
|
709
791
|
}
|
|
710
|
-
|
|
792
|
+
/**
|
|
793
|
+
* Righe non-wrappabili del dettaglio (titolo + meta + commit) e loro conteggio.
|
|
794
|
+
* Estratto dal componente perché il budget deve saperlo PRIMA di renderizzare:
|
|
795
|
+
* sono righe fisse che tolgono spazio alla descrizione.
|
|
796
|
+
*/
|
|
797
|
+
function detailMetaOf(detail) {
|
|
711
798
|
const meta = META_KEYS.map((k) => detail.fields[k])
|
|
712
799
|
.filter(Boolean)
|
|
713
800
|
.join(' · ');
|
|
714
|
-
const commit = detail.fields['Last tracked commit'];
|
|
715
|
-
return
|
|
801
|
+
const commit = detail.fields['Last tracked commit'] ?? '';
|
|
802
|
+
return { meta, commit, metaLines: 1 + (meta ? 1 : 0) + (commit ? 1 : 0) };
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* Larghezza utile del testo di descrizione, ricavata dalle colonne del
|
|
806
|
+
* terminale: box esterno (2 bordi + 2 padding) → pane al 50% → box dettaglio
|
|
807
|
+
* (2 bordi + 2 padding).
|
|
808
|
+
*
|
|
809
|
+
* Volutamente prudente: sottostimare tronca qualche carattere in più,
|
|
810
|
+
* sovrastimare farebbe andare a capo una riga e sforare il tetto d'altezza.
|
|
811
|
+
*/
|
|
812
|
+
function detailTextWidth(columns) {
|
|
813
|
+
return Math.max(10, Math.floor(((columns || 80) - 4) / 2) - 9);
|
|
814
|
+
}
|
|
815
|
+
function DetailPane({ detail, maxLines, columns, }) {
|
|
816
|
+
const { meta, commit } = detailMetaOf(detail);
|
|
817
|
+
// Wrap calcolato qui, non delegato a `<Text wrap="wrap">`: il budget ha
|
|
818
|
+
// riservato ESATTAMENTE `maxLines` righe, e un wrap deciso da Ink a runtime
|
|
819
|
+
// ne produrrebbe un numero che il budget non conosce — cioè il frame torna a
|
|
820
|
+
// sforare e il bug si riapre da questa singola casella di testo.
|
|
821
|
+
const lines = wrapLines(detail.description ?? '', detailTextWidth(columns), maxLines);
|
|
822
|
+
return (_jsxs(Box, { flexDirection: "column", marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, 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] }));
|
|
716
823
|
}
|
|
717
824
|
const cwd = process.cwd();
|
|
718
825
|
render(_jsx(Deck, { cwd: cwd, tasksPath: resolveTasksPath(cwd), tasksDir: resolveTasksDir(cwd) }));
|
package/dist/config.js
CHANGED
|
@@ -39,6 +39,57 @@ export function loadLaunch(projectRoot) {
|
|
|
39
39
|
return [];
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
+
// T43 — le voci launch si raggiungono con le CIFRE `1`..`9`: il cap non viene
|
|
43
|
+
// dallo schema config (che ne ammette quante se ne vogliono) ma dai tasti
|
|
44
|
+
// disponibili. Le voci oltre la nona restano configurate e non raggiungibili.
|
|
45
|
+
export const LAUNCH_MAX = 9;
|
|
46
|
+
// Larghezza in celle terminale, approssimata: emoji astrali (U+1F000+) e simboli
|
|
47
|
+
// BMP portati a presentazione emoji occupano 2 colonne, il VS16 è un modificatore
|
|
48
|
+
// a larghezza 0, tutto il resto 1. Serve solo a decidere quante voci stanno in
|
|
49
|
+
// riga — non deve essere esatta, deve non SOTTOstimare (sottostimare manderebbe
|
|
50
|
+
// la riga a capo, che è il difetto da evitare).
|
|
51
|
+
export function cellWidth(s) {
|
|
52
|
+
let w = 0;
|
|
53
|
+
for (const ch of s) {
|
|
54
|
+
const cp = ch.codePointAt(0);
|
|
55
|
+
if (cp === 0xfe0f)
|
|
56
|
+
continue;
|
|
57
|
+
w += cp >= 0x1f000 || (cp >= 0x2190 && cp <= 0x2bff) ? 2 : 1;
|
|
58
|
+
}
|
|
59
|
+
return w;
|
|
60
|
+
}
|
|
61
|
+
// T43 — legenda `indice → voce`. L'indice da solo è opaco (le launch sono voci
|
|
62
|
+
// custom per-progetto, non hanno una lettera fissa per app), quindi la resa deve
|
|
63
|
+
// esporre la mappa, non il conteggio. Degradazione mai silenziosa: ciò che non
|
|
64
|
+
// entra in larghezza finisce in un contatore esplicito, non troncato a metà.
|
|
65
|
+
export function launchLegend(entries, columns) {
|
|
66
|
+
const reachable = entries.slice(0, LAUNCH_MAX);
|
|
67
|
+
const unreachable = entries.length - reachable.length;
|
|
68
|
+
// bordo + padding del box + prefisso "launch " ≈ 12 celle; pavimento a 24 per
|
|
69
|
+
// non degenerare a legenda vuota su terminali strettissimi.
|
|
70
|
+
const budget = Math.max(24, columns - 12);
|
|
71
|
+
// I fallback di parseLaunch (`▸` per emoji, command per label) sono già
|
|
72
|
+
// applicati a monte: qui non si re-implementano né si assumono campi popolati.
|
|
73
|
+
const parts = reachable.map((e, i) => `${i + 1} ${e.emoji} ${e.label}`);
|
|
74
|
+
const fit = (reserve) => {
|
|
75
|
+
const taken = [];
|
|
76
|
+
let used = 0;
|
|
77
|
+
for (const p of parts) {
|
|
78
|
+
const cost = cellWidth(p) + (taken.length > 0 ? 3 : 0); // ' · '
|
|
79
|
+
if (used + cost > budget - reserve)
|
|
80
|
+
break;
|
|
81
|
+
taken.push(p);
|
|
82
|
+
used += cost;
|
|
83
|
+
}
|
|
84
|
+
return taken;
|
|
85
|
+
};
|
|
86
|
+
// Primo tentativo senza riserva: se entra tutto, nessuno spazio sprecato per un
|
|
87
|
+
// contatore che non servirebbe. Altrimenti si ripete riservando la coda.
|
|
88
|
+
let taken = fit(0);
|
|
89
|
+
if (taken.length < parts.length)
|
|
90
|
+
taken = fit(10);
|
|
91
|
+
return { shown: taken.join(' · '), overflow: parts.length - taken.length, unreachable };
|
|
92
|
+
}
|
|
42
93
|
export function parseIdentity(raw) {
|
|
43
94
|
if (!raw || typeof raw !== 'object')
|
|
44
95
|
return null;
|
package/dist/viewport.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// Budget d'altezza del frame — il deck non deve MAI renderizzare più righe di
|
|
2
|
+
// quante ne ha il terminale.
|
|
3
|
+
//
|
|
4
|
+
// Perché: quando `outputHeight >= stdout.rows`, Ink (ink/build/ink.js:121)
|
|
5
|
+
// abbandona il diff incrementale e passa a `clearTerminal + write` a ogni
|
|
6
|
+
// frame. Su VTE/Ptyxis `clearTerminal` non distrugge il contenuto: lo spinge
|
|
7
|
+
// nello scrollback. Col poll del deck che ridisegna ogni POLL_MS, ogni tick
|
|
8
|
+
// deposita un frame intero nella cronologia — da lì le righe-fantasma di
|
|
9
|
+
// bordi vuoti sopra l'header.
|
|
10
|
+
//
|
|
11
|
+
// Il fix non è cosmetico: è tenere il frame sotto `rows`, sempre. Tutto ciò
|
|
12
|
+
// che è a lunghezza variabile (lista task, lista sessioni, descrizione del
|
|
13
|
+
// dettaglio) passa da qui e riceve una capienza in righe.
|
|
14
|
+
//
|
|
15
|
+
// Logica pura, zero React/Ink: testabile senza pseudo-terminale.
|
|
16
|
+
/** Righe lasciate libere sotto il frame. La condizione di Ink è `>=`, quindi
|
|
17
|
+
* basterebbe 1; ne teniamo 1 come margine per l'a-capo del cursore. */
|
|
18
|
+
export const SLACK = 1;
|
|
19
|
+
/** Sotto questa soglia la lista task non è più utilizzabile: meglio sacrificare
|
|
20
|
+
* il pannello di dettaglio che ridurre la lista a due righe. */
|
|
21
|
+
const MIN_TASK_ROWS = 3;
|
|
22
|
+
/** Il dettaglio è secondario: non si prende mai più di così, anche con spazio. */
|
|
23
|
+
const MAX_DETAIL_LINES = 4;
|
|
24
|
+
/** Righe di "cornice" fisse dei tre contenitori a lunghezza variabile. */
|
|
25
|
+
const TASKS_PANE_CHROME = 5; // 2 bordi + header "Tasks (n)" + riga sort + riga spot
|
|
26
|
+
const SESSIONS_PANE_CHROME = 3; // 2 bordi + header "Sessions · …"
|
|
27
|
+
const DETAIL_CHROME = 3; // marginTop + 2 bordi
|
|
28
|
+
/** Altezza di ciascuna modale, marginTop incluso. In flusso, non in overlay:
|
|
29
|
+
* spingono giù i pane, quindi il loro costo va scalato dal budget. */
|
|
30
|
+
export const MODAL_HEIGHT = {
|
|
31
|
+
normal: 0,
|
|
32
|
+
create: 4, // marginTop + 2 bordi + 1 riga input
|
|
33
|
+
sort: 5, // marginTop + 2 bordi + titolo + 1 riga catena
|
|
34
|
+
filter: 6, // marginTop + 2 bordi + titolo + 2 righe (pri, stato)
|
|
35
|
+
edit: 8, // marginTop + 2 bordi + titolo + 3 campi + riga anteprima
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Distribuisce le righe disponibili fra lista task, lista sessioni e dettaglio.
|
|
39
|
+
*
|
|
40
|
+
* I due pane stanno affiancati (flexDirection row) → l'altezza del blocco è il
|
|
41
|
+
* MAX delle due colonne, non la somma: ognuna riceve lo stesso tetto.
|
|
42
|
+
*
|
|
43
|
+
* Ordine di sacrificio quando lo spazio stringe:
|
|
44
|
+
* 1. righe di descrizione del dettaglio (fino a sparire col pannello),
|
|
45
|
+
* 2. righe della lista task, mai sotto MIN_TASK_ROWS finché il dettaglio c'è.
|
|
46
|
+
*/
|
|
47
|
+
export function layoutBudget(input) {
|
|
48
|
+
const outerChrome = 2 + // bordi del box esterno
|
|
49
|
+
1 + // titolo "loom-deck"
|
|
50
|
+
1 + // riga navigazione
|
|
51
|
+
(input.launchLine ? 1 : 0) +
|
|
52
|
+
MODAL_HEIGHT[input.mode] +
|
|
53
|
+
1 + // marginTop del blocco pane
|
|
54
|
+
(input.noteLine ? 1 : 0);
|
|
55
|
+
// Tetto per colonna: righe che restano ai due pane affiancati.
|
|
56
|
+
const avail = (input.rows || 24) - SLACK - outerChrome;
|
|
57
|
+
// La cornice del pane task (bordi + 3 header) più ALMENO una riga di lista.
|
|
58
|
+
// Il `+1` non è cosmetico: senza, un terminale bassissimo produce un pane
|
|
59
|
+
// regolamentare con zero task dentro — occupa 5 righe per non mostrare nulla,
|
|
60
|
+
// mentre la riga compatta dice le stesse cose in una.
|
|
61
|
+
if (avail < TASKS_PANE_CHROME + 1) {
|
|
62
|
+
return { taskRows: 0, sessionRows: 0, detailLines: 0, compact: true };
|
|
63
|
+
}
|
|
64
|
+
const sessionRows = Math.max(0, avail - SESSIONS_PANE_CHROME);
|
|
65
|
+
let detailLines = 0;
|
|
66
|
+
let detailChrome = 0;
|
|
67
|
+
if (input.hasDetail) {
|
|
68
|
+
const fixed = DETAIL_CHROME + input.detailMetaLines;
|
|
69
|
+
// Righe che avanzano dopo aver garantito la lista minima e la cornice del
|
|
70
|
+
// dettaglio. Serve almeno 1 riga di descrizione per giustificare il box:
|
|
71
|
+
// un pannello con la sola cornice ruberebbe 3+ righe per mostrare nulla.
|
|
72
|
+
const spare = avail - TASKS_PANE_CHROME - MIN_TASK_ROWS - fixed;
|
|
73
|
+
if (spare >= 1) {
|
|
74
|
+
detailChrome = fixed;
|
|
75
|
+
detailLines = Math.min(MAX_DETAIL_LINES, spare);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const taskRows = Math.max(0, avail - TASKS_PANE_CHROME - detailChrome - detailLines);
|
|
79
|
+
return { taskRows, sessionRows, detailLines, compact: false };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Finestra scorrevole su una lista più lunga della capienza.
|
|
83
|
+
*
|
|
84
|
+
* Centra la selezione, poi clampa ai bordi — così in cima e in fondo alla lista
|
|
85
|
+
* la finestra non spreca righe fuori dai dati.
|
|
86
|
+
*
|
|
87
|
+
* `selected` è l'indice nella lista completa; -1 (o fuori range) = nessuna
|
|
88
|
+
* selezione, la finestra parte da capo.
|
|
89
|
+
*/
|
|
90
|
+
export function windowRange(total, selected, capacity) {
|
|
91
|
+
if (capacity <= 0 || total <= 0)
|
|
92
|
+
return { start: 0, end: 0 };
|
|
93
|
+
if (total <= capacity)
|
|
94
|
+
return { start: 0, end: total };
|
|
95
|
+
const sel = selected >= 0 && selected < total ? selected : 0;
|
|
96
|
+
const start = Math.max(0, Math.min(sel - Math.floor(capacity / 2), total - capacity));
|
|
97
|
+
return { start, end: start + capacity };
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Hard-wrap a larghezza fissa, con tetto di righe.
|
|
101
|
+
*
|
|
102
|
+
* Serve un conteggio righe DETERMINISTICO: `<Text wrap="wrap">` di Ink wrappa a
|
|
103
|
+
* runtime su una larghezza che il budget non conosce, quindi il pannello
|
|
104
|
+
* dettaglio potrebbe sforare il tetto e riaprire il bug. Qui il testo viene
|
|
105
|
+
* spezzato prima, e ogni riga è renderizzata con `wrap="truncate-end"`.
|
|
106
|
+
*
|
|
107
|
+
* Sottostimare `width` è sicuro (tronca prima), sovrastimarlo no (la riga
|
|
108
|
+
* andrebbe a capo aggiungendo altezza non contabilizzata).
|
|
109
|
+
*/
|
|
110
|
+
export function wrapLines(text, width, maxLines) {
|
|
111
|
+
if (maxLines <= 0 || width <= 0)
|
|
112
|
+
return [];
|
|
113
|
+
const flat = text.replace(/\s+/g, ' ').trim();
|
|
114
|
+
if (!flat)
|
|
115
|
+
return [];
|
|
116
|
+
const lines = [];
|
|
117
|
+
let line = '';
|
|
118
|
+
for (const word of flat.split(' ')) {
|
|
119
|
+
if (!line) {
|
|
120
|
+
line = word;
|
|
121
|
+
}
|
|
122
|
+
else if (line.length + 1 + word.length <= width) {
|
|
123
|
+
line += ' ' + word;
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
lines.push(line);
|
|
127
|
+
line = word;
|
|
128
|
+
if (lines.length === maxLines)
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
// Parola più lunga della riga: spezzala a forza, altrimenti l'a-capo lo
|
|
132
|
+
// farebbe il terminale — fuori dal nostro conteggio.
|
|
133
|
+
while (line.length > width) {
|
|
134
|
+
lines.push(line.slice(0, width));
|
|
135
|
+
line = line.slice(width);
|
|
136
|
+
if (lines.length === maxLines)
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
if (lines.length === maxLines)
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
if (line && lines.length < maxLines)
|
|
143
|
+
lines.push(line);
|
|
144
|
+
const kept = lines.slice(0, maxLines);
|
|
145
|
+
// Troncamento MAI silenzioso, come le liste: l'ellissi segnala che il testo
|
|
146
|
+
// continua oltre il pannello.
|
|
147
|
+
const consumed = kept.join(' ').length;
|
|
148
|
+
if (consumed < flat.length && kept.length > 0) {
|
|
149
|
+
const last = kept[kept.length - 1];
|
|
150
|
+
kept[kept.length - 1] = last.length >= width ? last.slice(0, Math.max(0, width - 1)) + '…' : last + '…';
|
|
151
|
+
}
|
|
152
|
+
return kept;
|
|
153
|
+
}
|
package/package.json
CHANGED
package/scripts/deck-run
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
#
|
|
3
|
-
# deck-run <TaskID>
|
|
3
|
+
# deck-run <TaskID> | deck-run --no-task — spawn primitive (UI-agnostico) di loom-deck.
|
|
4
4
|
#
|
|
5
5
|
# Apre una tab Ptyxis nella window ATTIVA (quella col focus = il deck) e vi
|
|
6
6
|
# avvia una sessione Claude Code già bound alla task via LOOM_TASK, dritta sul
|
|
7
7
|
# prompt iniziale (default: prompt diretto "recap stato task <TaskID>" — no skill,
|
|
8
8
|
# solo recap → più controllo). Riusabile identico da TUI (Ink) e da web.
|
|
9
9
|
#
|
|
10
|
+
# Con --no-task la sessione è NUDA: niente LOOM_TASK, niente prompt iniziale,
|
|
11
|
+
# niente --session-id. Serve al lavoro spot che non appartiene a nessuna task.
|
|
12
|
+
#
|
|
10
13
|
# Modalità spawn (LOOM_DECK_SPAWN_MODE):
|
|
11
14
|
# inline → ptyxis --tab -- <cmd> (default) comando inline, LOOM_TASK diretta, zero dconf
|
|
12
15
|
# profile → ptyxis --tab-with-profile=<UUID> riusa il profilo, riscrive il custom-command via dconf
|
|
@@ -21,27 +24,42 @@
|
|
|
21
24
|
# LOOM_DECK_WORKDIR dir di lavoro della tab (default: $PWD)
|
|
22
25
|
# LOOM_DECK_ENTER_PROMPT prompt iniziale sessione, {TASK}=TaskID (default: "recap stato task <TaskID>")
|
|
23
26
|
# LOOM_DECK_INTAB_CMD override comando in-tab (default: la sessione CC; usato per i test)
|
|
27
|
+
# LOOM_DECK_PERMISSION_MODE override del permissionMode del file config (default: campo file, poi 'manual')
|
|
24
28
|
#
|
|
25
29
|
set -euo pipefail
|
|
26
30
|
|
|
27
31
|
TASK=""
|
|
28
32
|
SESSION_ID=""
|
|
33
|
+
NO_TASK=0
|
|
29
34
|
# Positional <TaskID> + flag opzionale --session-id <uuid> (T27): il deck genera
|
|
30
35
|
# l'UUID e lo pinna così il binding sidecar sessionId↔taskId è deterministico.
|
|
36
|
+
# --no-task (T42): modalità NUDA, senza TaskID — nessuna LOOM_TASK, nessun prompt
|
|
37
|
+
# iniziale, nessun sessionId pinnato. Flag esplicito e non "positional opzionale"
|
|
38
|
+
# perché un `deck-run` a mani vuote resta un errore d'uso, non una sessione spot.
|
|
31
39
|
while [[ $# -gt 0 ]]; do
|
|
32
40
|
case "$1" in
|
|
33
41
|
--session-id)
|
|
34
42
|
SESSION_ID="${2:-}"; shift 2 ;;
|
|
35
43
|
--session-id=*)
|
|
36
44
|
SESSION_ID="${1#*=}"; shift ;;
|
|
45
|
+
--no-task)
|
|
46
|
+
NO_TASK=1; shift ;;
|
|
37
47
|
*)
|
|
38
48
|
if [[ -z "$TASK" ]]; then TASK="$1"; shift
|
|
39
49
|
else echo "argomento inatteso: '$1'" >&2; exit 2; fi ;;
|
|
40
50
|
esac
|
|
41
51
|
done
|
|
42
52
|
|
|
43
|
-
|
|
44
|
-
|
|
53
|
+
USAGE="uso: deck-run <TaskID> [--session-id <uuid>] (es. deck-run T18)
|
|
54
|
+
| deck-run --no-task (sessione nuda, senza task)"
|
|
55
|
+
|
|
56
|
+
if [[ $NO_TASK -eq 1 && -n "$TASK" ]]; then
|
|
57
|
+
echo "--no-task e <TaskID> sono mutuamente esclusivi" >&2
|
|
58
|
+
echo "$USAGE" >&2
|
|
59
|
+
exit 2
|
|
60
|
+
fi
|
|
61
|
+
if [[ $NO_TASK -eq 0 && -z "$TASK" ]]; then
|
|
62
|
+
echo "$USAGE" >&2
|
|
45
63
|
exit 2
|
|
46
64
|
fi
|
|
47
65
|
|
|
@@ -68,17 +86,47 @@ _find_project_root() { # <start-dir> → dir con .claude/loom-works.json, o vuo
|
|
|
68
86
|
return 1
|
|
69
87
|
}
|
|
70
88
|
|
|
71
|
-
|
|
89
|
+
# Project root risolta UNA volta: la usano sia la label sia permissionMode (T45).
|
|
90
|
+
_proot=""
|
|
72
91
|
if command -v jq >/dev/null 2>&1; then
|
|
73
92
|
_proot="$(_find_project_root "$WORKDIR" || true)"
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
93
|
+
fi
|
|
94
|
+
_cfg=""
|
|
95
|
+
[[ -n "$_proot" ]] && _cfg="${_proot}/.claude/loom-works.json"
|
|
96
|
+
|
|
97
|
+
# La label vale per OGNI surface claude, task o no (T42): il match compass è
|
|
98
|
+
# window-level e non sa nulla di task → legare la titolazione alla presenza di
|
|
99
|
+
# una task sarebbe un errore di layer. Cambia solo il suffisso: `· <task>` quando
|
|
100
|
+
# la sessione è bound, nessun suffisso quando è nuda.
|
|
101
|
+
if [[ $NO_TASK -eq 1 ]]; then TITLE="cc"; else TITLE="cc ${TASK}"; fi
|
|
102
|
+
if [[ -n "$_cfg" ]]; then
|
|
103
|
+
_label="$(jq -r 'if (.emoji and .owner and .name)
|
|
104
|
+
then "\(.emoji) \(.owner) \(.name)" else empty end' \
|
|
105
|
+
"$_cfg" 2>/dev/null || true)"
|
|
106
|
+
if [[ -n "${_label:-}" ]]; then
|
|
107
|
+
if [[ $NO_TASK -eq 1 ]]; then TITLE="${_label}"; else TITLE="${_label} · ${TASK}"; fi
|
|
79
108
|
fi
|
|
80
109
|
fi
|
|
81
110
|
|
|
111
|
+
# ── permissionMode (T45) ─────────────────────────────────────────────────────
|
|
112
|
+
# Precedenza: env LOOM_DECK_PERMISSION_MODE > campo del file config > 'manual'.
|
|
113
|
+
# Il flag è passato SEMPRE, anche per 'manual': lo spawn resta deterministico e
|
|
114
|
+
# leggibile nel process tree, invece di dipendere dal default del CLI (che può
|
|
115
|
+
# cambiare fra versioni). Un valore fuori enum NON viene passato al CLI (che
|
|
116
|
+
# rifiuterebbe, lasciando una tab con un comando rotto): fallback su 'manual'
|
|
117
|
+
# con segnalazione su stderr.
|
|
118
|
+
PERM_MODE="${LOOM_DECK_PERMISSION_MODE:-}"
|
|
119
|
+
if [[ -z "$PERM_MODE" && -n "$_cfg" ]]; then
|
|
120
|
+
PERM_MODE="$(jq -r '.permissionMode // empty' "$_cfg" 2>/dev/null || true)"
|
|
121
|
+
fi
|
|
122
|
+
case "${PERM_MODE:-}" in
|
|
123
|
+
acceptEdits|auto|bypassPermissions|manual|dontAsk|plan) ;;
|
|
124
|
+
'') PERM_MODE="manual" ;;
|
|
125
|
+
*) echo "permissionMode ignoto: '${PERM_MODE}' → fallback 'manual'" >&2
|
|
126
|
+
PERM_MODE="manual" ;;
|
|
127
|
+
esac
|
|
128
|
+
MODE_FLAG="--permission-mode ${PERM_MODE} "
|
|
129
|
+
|
|
82
130
|
# --session-id per la tab: pinna il sessionId della sessione CC (vuoto → CC ne
|
|
83
131
|
# genera uno). Lo spazio finale tiene la concatenazione pulita quando è assente.
|
|
84
132
|
SID_FLAG=""
|
|
@@ -102,7 +150,15 @@ fi
|
|
|
102
150
|
# `--name '<TITLE>'` = canale AUTORITATIVO del terminal-title (Ptyxis -T è solo il
|
|
103
151
|
# titolo iniziale, prima che CC parta): CC lo setta e nomina anche la sessione nel
|
|
104
152
|
# picker. Override-abile via LOOM_DECK_INTAB_CMD per i test empirici (probe non-CC).
|
|
105
|
-
|
|
153
|
+
# Ramo --no-task (T42): saltano i TRE elementi che legano la sessione alla task —
|
|
154
|
+
# iniezione LOOM_TASK, prompt iniziale, --session-id pinnato. Resta claude nudo,
|
|
155
|
+
# titolato e con il permission mode del progetto.
|
|
156
|
+
if [[ $NO_TASK -eq 1 ]]; then
|
|
157
|
+
_default_intab="claude --name '${TITLE}' ${MODE_FLAG}"
|
|
158
|
+
else
|
|
159
|
+
_default_intab="LOOM_TASK=${TASK} claude --name '${TITLE}' ${MODE_FLAG}${SID_FLAG}'${PROMPT}'"
|
|
160
|
+
fi
|
|
161
|
+
IN_TAB_CMD="${LOOM_DECK_INTAB_CMD:-$_default_intab}"
|
|
106
162
|
|
|
107
163
|
case "$MODE" in
|
|
108
164
|
inline)
|