@lamemind/loom-deck 0.68.0 → 0.70.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/deck-model.js +18 -15
- package/dist/hooks.js +40 -2
- package/dist/session-meta.js +126 -0
- package/dist/sessions.js +7 -1
- package/dist/task-index.js +51 -2
- package/package.json +1 -1
- package/scripts/deck-run +32 -3
package/dist/deck-model.js
CHANGED
|
@@ -107,11 +107,28 @@ export function childSessionsOf(sessions, bindings, selectedTaskId, isAll) {
|
|
|
107
107
|
}
|
|
108
108
|
export function useDeckModel({ cwd, tasksPath, tasksDir, bareModel: bareModelDefault, setNote, }) {
|
|
109
109
|
const { tasks, loadError } = useTasks(tasksPath);
|
|
110
|
+
// Voci launch del progetto (T32): lette una volta, raggiunte per indice 1..9.
|
|
111
|
+
const launch = useMemo(() => loadLaunch(cwd), [cwd]);
|
|
112
|
+
// Identità (T37): titolo delle tab terminale spawnate col tasto `t`.
|
|
113
|
+
const identity = useMemo(() => loadIdentity(cwd), [cwd]);
|
|
114
|
+
// T53 — il core che ogni titolo di tab porta, quindi la colonna costante da
|
|
115
|
+
// togliere quando serve spazio. Hoistato perché lo consumano DUE schermate
|
|
116
|
+
// (lista e ricerca): calcolarlo su ogni call site è il modo in cui le due
|
|
117
|
+
// smettono di togliere la stessa cosa.
|
|
118
|
+
// T58 — il core è il solo `name`, non `<emoji> <name>` come la chiave di match
|
|
119
|
+
// di compass: qui non serve entropia (è un taglio cosmetico, non un matcher) e
|
|
120
|
+
// il nome nudo ripulisce anche i titoli storici, scritti quando la formula
|
|
121
|
+
// includeva l'owner.
|
|
122
|
+
// T162 — sta SOPRA `useSessions` perché ora è anche un suo ingresso: il deck
|
|
123
|
+
// scrive nel sidecar il residuo del titolo, e quel residuo si ottiene
|
|
124
|
+
// togliendo questo stesso core (§Riempimento dei buchi in `hooks.ts`).
|
|
125
|
+
const projectCore = identity ? identity.name : null;
|
|
126
|
+
const projectName = cwd.split('/').pop() || cwd;
|
|
110
127
|
// `notes` esce dall'indice come `sessionNotes`: nel deck `note` è già la riga
|
|
111
128
|
// di STATO in fondo al frame (il feedback di un'azione). Due concetti diversi
|
|
112
129
|
// a una lettera di distanza sarebbero una trappola di lettura — e di
|
|
113
130
|
// scrittura, visto che `setNote` compare in quasi ogni ramo.
|
|
114
|
-
const { sessions, bindings, forkOf, pinned, notes: sessionNotes, priority: sessionPriority, live, reload: reloadSessions, } = useSessions(cwd);
|
|
131
|
+
const { sessions, bindings, forkOf, pinned, notes: sessionNotes, priority: sessionPriority, live, reload: reloadSessions, } = useSessions(cwd, projectCore);
|
|
115
132
|
const [focus, setFocus] = useState('tasks');
|
|
116
133
|
// T39 — selezione KEYED SU ID, non su indice. Con una vista trasformata
|
|
117
134
|
// (filtro/sort) l'indice non identifica più la stessa task: leggere l'array
|
|
@@ -158,20 +175,6 @@ export function useDeckModel({ cwd, tasksPath, tasksDir, bareModel: bareModelDef
|
|
|
158
175
|
// sbagliata in silenzio (stessa trappola di T39 sulle task e T50 sulle
|
|
159
176
|
// conversazioni).
|
|
160
177
|
const [selInboxPath, setSelInboxPath] = useState(null);
|
|
161
|
-
// Voci launch del progetto (T32): lette una volta, raggiunte per indice 1..9.
|
|
162
|
-
const launch = useMemo(() => loadLaunch(cwd), [cwd]);
|
|
163
|
-
// Identità (T37): titolo delle tab terminale spawnate col tasto `t`.
|
|
164
|
-
const identity = useMemo(() => loadIdentity(cwd), [cwd]);
|
|
165
|
-
// T53 — il core che ogni titolo di tab porta, quindi la colonna costante da
|
|
166
|
-
// togliere quando serve spazio. Hoistato perché lo consumano DUE schermate
|
|
167
|
-
// (lista e ricerca): calcolarlo su ogni call site è il modo in cui le due
|
|
168
|
-
// smettono di togliere la stessa cosa.
|
|
169
|
-
// T58 — il core è il solo `name`, non `<emoji> <name>` come la chiave di match
|
|
170
|
-
// di compass: qui non serve entropia (è un taglio cosmetico, non un matcher) e
|
|
171
|
-
// il nome nudo ripulisce anche i titoli storici, scritti quando la formula
|
|
172
|
-
// includeva l'owner.
|
|
173
|
-
const projectCore = identity ? identity.name : null;
|
|
174
|
-
const projectName = cwd.split('/').pop() || cwd;
|
|
175
178
|
// T136 — data dell'ultimo commit di ogni task file, per la chiave `commit`
|
|
176
179
|
// della chain di sort.
|
|
177
180
|
const commitAt = useCommitTimes(tasksDir, cwd);
|
package/dist/hooks.js
CHANGED
|
@@ -9,6 +9,7 @@ import { loadTasks, loadTaskDetail } from './tasks.js';
|
|
|
9
9
|
import { discoverProjectSessions } from './sessions.js';
|
|
10
10
|
import { discoverLiveSessions, liveSig } from './live-sessions.js';
|
|
11
11
|
import { loadSessionIndex } from './task-index.js';
|
|
12
|
+
import { fillSidecarGaps } from './session-meta.js';
|
|
12
13
|
import { archivableIds, SCAN_INTERVAL_MS } from './archivable.js';
|
|
13
14
|
import { commitTimes } from './commit-times.js';
|
|
14
15
|
import { scanEpicHierarchy, EMPTY_EPIC_HIERARCHY } from './epic-hierarchy.js';
|
|
@@ -137,7 +138,15 @@ export function useEpicHierarchy(tasksDir) {
|
|
|
137
138
|
// ha cache mtime-keyed interna → il poll è economico; qui si evita comunque il
|
|
138
139
|
// re-render inutile con una signature (sessionId:ts + binding entries): setState
|
|
139
140
|
// solo quando cambia davvero qualcosa.
|
|
140
|
-
|
|
141
|
+
//
|
|
142
|
+
// T162 — è anche il posto dove il deck RIEMPIE I BUCHI dei campi derivati del
|
|
143
|
+
// sidecar (`title`, `model`): qui le due fonti sono già in mano nello stesso
|
|
144
|
+
// giro, la corrente (le `Session`) e quella su disco (l'indice). Il `core` del
|
|
145
|
+
// progetto arriva per argomento e non si rilegge qui: la stessa derivazione
|
|
146
|
+
// vive già nel modello (`projectCore`), e una seconda copia toglierebbe dal
|
|
147
|
+
// titolo scritto nel sidecar una stringa diversa da quella che la lista toglie
|
|
148
|
+
// dalle proprie righe.
|
|
149
|
+
export function useSessions(projectRoot, core) {
|
|
141
150
|
const [state, setState] = useState({
|
|
142
151
|
sessions: [],
|
|
143
152
|
bindings: new Map(),
|
|
@@ -168,6 +177,8 @@ export function useSessions(projectRoot) {
|
|
|
168
177
|
pinned: new Map(),
|
|
169
178
|
notes: new Map(),
|
|
170
179
|
priority: new Set(),
|
|
180
|
+
titles: new Map(),
|
|
181
|
+
models: new Map(),
|
|
171
182
|
};
|
|
172
183
|
}
|
|
173
184
|
// T62 — le vive stanno sullo STESSO tick delle altre fonti, non su una
|
|
@@ -183,6 +194,33 @@ export function useSessions(projectRoot) {
|
|
|
183
194
|
live = new Map();
|
|
184
195
|
}
|
|
185
196
|
const { bindings, forkOf, pinned, notes, priority } = index;
|
|
197
|
+
// T162 — i campi derivati delle PINNATE, allineati prima della signature.
|
|
198
|
+
//
|
|
199
|
+
// Gira a ogni giro e non solo al cambio di stato, perché il buco può
|
|
200
|
+
// nascere senza che nulla nel deck si muova: a pinnare può essere compass,
|
|
201
|
+
// dalla modale sulla conversazione in focus, e compass il titolo non lo
|
|
202
|
+
// conosce. Il deck è l'unico che ce l'ha, quindi è l'unico che può
|
|
203
|
+
// chiudere quel buco — e lo fa passando, non al momento del gesto.
|
|
204
|
+
//
|
|
205
|
+
// Costa solo quando c'è da scrivere: senza pinnate `sidecarGaps` esce
|
|
206
|
+
// subito, e con un sidecar già allineato ritorna una lista vuota (è
|
|
207
|
+
// l'invariante di convergenza del modulo — vedi `session-meta.ts`).
|
|
208
|
+
//
|
|
209
|
+
// Fuori dal gate della signature di proposito: un sidecar temporaneamente
|
|
210
|
+
// non scrivibile deve poter essere ritentato al giro dopo, e con lo stato
|
|
211
|
+
// invariato la signature non cambierebbe mai più.
|
|
212
|
+
//
|
|
213
|
+
// Gli append NON entrano nella signature e non producono un re-render: il
|
|
214
|
+
// deck il titolo lo ha dalla `Session`, che è la fonte. Questi campi
|
|
215
|
+
// esistono per un lettore che non può aprire un transcript, cioè compass.
|
|
216
|
+
fillSidecarGaps(projectRoot, {
|
|
217
|
+
pinned,
|
|
218
|
+
sessions,
|
|
219
|
+
bindings,
|
|
220
|
+
titles: index.titles,
|
|
221
|
+
models: index.models,
|
|
222
|
+
core,
|
|
223
|
+
});
|
|
186
224
|
// La signature copre anche fork, pin, note e marca di priorità: un record
|
|
187
225
|
// di lineage, un toggle di pin, una nota appena scritta o un 🚨 acceso
|
|
188
226
|
// cambiano la lista renderizzata, quindi devono forzare il re-render come
|
|
@@ -209,7 +247,7 @@ export function useSessions(projectRoot) {
|
|
|
209
247
|
reload();
|
|
210
248
|
const id = setInterval(reload, POLL_MS);
|
|
211
249
|
return () => clearInterval(id);
|
|
212
|
-
}, [projectRoot]);
|
|
250
|
+
}, [projectRoot, core]);
|
|
213
251
|
return { ...state, reload: () => reloadRef.current() };
|
|
214
252
|
}
|
|
215
253
|
// T61 — conteggio delle Done oltre soglia, su una scala di refresh TUTTA SUA.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// T162 — i campi DERIVATI del sidecar: cosa scrivere in `title`/`model` e
|
|
2
|
+
// quando scriverlo.
|
|
3
|
+
//
|
|
4
|
+
// Esiste perché compass ha bisogno del titolo di una conversazione e non può
|
|
5
|
+
// leggerne il transcript: l'estensione gira dentro il processo di gnome-shell,
|
|
6
|
+
// e una lettura sincrona di qualche megabyte blocca il compositore. Il dato
|
|
7
|
+
// deve quindi arrivargli da un file piccolo che già apre — il sidecar — messo
|
|
8
|
+
// lì da chi ce l'ha in mano, cioè il deck, che a ogni tick materializza le
|
|
9
|
+
// `Session` complete di titolo e modello.
|
|
10
|
+
//
|
|
11
|
+
// Il giudizio è separato dalla scrittura: `sidecarGaps` decide e ritorna i
|
|
12
|
+
// record, `fillSidecarGaps` li scrive. La separazione è ciò che rende testabile
|
|
13
|
+
// la parte non ovvia — la convergenza: un derivato che non combacia mai col
|
|
14
|
+
// valore su disco produrrebbe un append a ogni tick del poll, per sempre, senza
|
|
15
|
+
// nessun errore a dirlo.
|
|
16
|
+
//
|
|
17
|
+
// Nessun import da ink/react: il chiamante è un hook, questo modulo no.
|
|
18
|
+
import { modelAlias } from './glyphs.js';
|
|
19
|
+
import { stripProjectCore, stripTaskId } from './session-list.js';
|
|
20
|
+
import { NO_TITLE } from './sessions.js';
|
|
21
|
+
import { NO_SPAWN } from './spawn.js';
|
|
22
|
+
import { appendSessionMeta } from './task-index.js';
|
|
23
|
+
/**
|
|
24
|
+
* Cap del titolo scritto nel sidecar, in CODE POINT.
|
|
25
|
+
*
|
|
26
|
+
* È il `LABEL_MAX` del lettore (compass): oltre, Pango ellissizza comunque, e
|
|
27
|
+
* il file non deve portare una stringa che nessuno mostrerà per intero. Misura
|
|
28
|
+
* del preflight: col primo prompt come titolo il massimo osservato era 1.367
|
|
29
|
+
* caratteri, 36 conversazioni su 604 sopra 120, p90 a 61 — il cap morde su una
|
|
30
|
+
* minoranza e non tocca il caso normale.
|
|
31
|
+
*
|
|
32
|
+
* In code point e non in code unit perché il taglio va fatto senza spezzare una
|
|
33
|
+
* coppia surrogata: un'emoji tagliata a metà lascia nel file un surrogato
|
|
34
|
+
* spaiato, cioè un glifo rotto nel menu.
|
|
35
|
+
*/
|
|
36
|
+
export const SIDECAR_TITLE_MAX = 120;
|
|
37
|
+
function capTitle(s) {
|
|
38
|
+
const cps = [...s];
|
|
39
|
+
return cps.length <= SIDECAR_TITLE_MAX ? s : cps.slice(0, SIDECAR_TITLE_MAX).join('');
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Il valore da scrivere in `title`: il RESIDUO del titolo, non il titolo
|
|
43
|
+
* grezzo. `''` = non c'è niente da scrivere.
|
|
44
|
+
*
|
|
45
|
+
* I due casi non si trattano allo stesso modo, ed è il punto di questa
|
|
46
|
+
* funzione:
|
|
47
|
+
*
|
|
48
|
+
* - **con titolo custom** → il titolo di una conversazione nata dal deck È la
|
|
49
|
+
* label della tab (`🧵 loom-works · T32 revisione`), composta di tre pezzi
|
|
50
|
+
* che il lettore ha già altrove: il progetto (la riga sotto cui la
|
|
51
|
+
* conversazione compare), il task id (il campo `taskId` dello stesso
|
|
52
|
+
* record) e la nota (il campo `note`). Copiare il grezzo obbligherebbe
|
|
53
|
+
* compass a una seconda copia di `stripTaskId`, e a schermo ripeterebbe tre
|
|
54
|
+
* volte quello che già si legge.
|
|
55
|
+
* - **senza titolo custom** → `title` È il primo prompt, e vale intatto:
|
|
56
|
+
* quella è l'unica cosa che dice di cosa si parlava.
|
|
57
|
+
*
|
|
58
|
+
* Non è `sessionTitle`: quella, con residuo vuoto, cade sul primo prompt —
|
|
59
|
+
* dentro il deck è giusto, perché una riga muta non serve a nessuno, ma qui il
|
|
60
|
+
* primo prompt di una sessione nata dal deck è l'invocazione di una skill
|
|
61
|
+
* (`/loom-works:recap-status-task T32`), che nel menu non aggiunge nulla. Qui
|
|
62
|
+
* il residuo vuoto è ASSENZA: non si scrive, e chi legge cade sulla nota.
|
|
63
|
+
*/
|
|
64
|
+
export function sidecarTitle(s, core, taskId) {
|
|
65
|
+
if (!s.customTitle)
|
|
66
|
+
return s.title === NO_TITLE ? '' : capTitle(s.title);
|
|
67
|
+
return capTitle(stripTaskId(stripProjectCore(s.title, core), taskId));
|
|
68
|
+
}
|
|
69
|
+
export function sidecarGaps(input) {
|
|
70
|
+
const { pinned, sessions, bindings, titles, models, core } = input;
|
|
71
|
+
if (pinned.size === 0)
|
|
72
|
+
return [];
|
|
73
|
+
const byId = new Map(sessions.map((s) => [s.sessionId, s]));
|
|
74
|
+
const out = [];
|
|
75
|
+
for (const sessionId of pinned.keys()) {
|
|
76
|
+
const session = byId.get(sessionId);
|
|
77
|
+
if (!session)
|
|
78
|
+
continue; // pinnata stale: niente da cui derivare
|
|
79
|
+
const gap = { sessionId };
|
|
80
|
+
const title = sidecarTitle(session, core, bindings.get(sessionId) ?? null);
|
|
81
|
+
if (title && titles.get(sessionId) !== title)
|
|
82
|
+
gap.title = title;
|
|
83
|
+
const model = modelAlias(session.model);
|
|
84
|
+
if (model && models.get(sessionId) !== model)
|
|
85
|
+
gap.model = model;
|
|
86
|
+
if (gap.title !== undefined || gap.model !== undefined)
|
|
87
|
+
out.push(gap);
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Scrive i buchi di questo giro. Ritorna quanti record ha appeso — zero è
|
|
93
|
+
* l'esito normale a regime, non un fallimento.
|
|
94
|
+
*
|
|
95
|
+
* `NO_SPAWN` frena anche questa scrittura, ed è lo stesso freno delle tab
|
|
96
|
+
* Ptyxis per la stessa ragione: il gate su pseudo-terminale avvia il DECK VERO
|
|
97
|
+
* con cwd la project root del cappello, quindi senza freno ogni run della suite
|
|
98
|
+
* appende record nel sidecar reale di chi la lancia. Il valore scritto sarebbe
|
|
99
|
+
* pure corretto — è quello che il deck scriverebbe comunque — ma è una
|
|
100
|
+
* scrittura fuori dalla sandbox del test, che è precisamente ciò che il freno
|
|
101
|
+
* esiste per impedire.
|
|
102
|
+
*
|
|
103
|
+
* Il freno sta QUI e non nel chiamante perché questa è l'unica sede della
|
|
104
|
+
* scrittura: nel chiamante sarebbe una guardia che il prossimo scrittore non
|
|
105
|
+
* vede.
|
|
106
|
+
*
|
|
107
|
+
* Un sidecar non scrivibile non è un errore da mostrare: il giro dopo riprova,
|
|
108
|
+
* e nel frattempo il deck funziona identico — questi campi li legge un altro
|
|
109
|
+
* processo, non lui.
|
|
110
|
+
*/
|
|
111
|
+
export function fillSidecarGaps(projectRoot, input) {
|
|
112
|
+
if (NO_SPAWN)
|
|
113
|
+
return 0;
|
|
114
|
+
let scritti = 0;
|
|
115
|
+
try {
|
|
116
|
+
for (const gap of sidecarGaps(input)) {
|
|
117
|
+
appendSessionMeta(projectRoot, gap.sessionId, gap);
|
|
118
|
+
scritti++;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// sidecar non scrivibile: si riprova al giro dopo, niente a schermo — è
|
|
123
|
+
// manutenzione di un campo di servizio, non un'azione che l'utente ha chiesto.
|
|
124
|
+
}
|
|
125
|
+
return scritti;
|
|
126
|
+
}
|
package/dist/sessions.js
CHANGED
|
@@ -54,6 +54,12 @@ const INTERRUPT_MARKER = '[Request interrupted by user';
|
|
|
54
54
|
function isInterrupt(text) {
|
|
55
55
|
return text.trimStart().startsWith(INTERRUPT_MARKER);
|
|
56
56
|
}
|
|
57
|
+
/** Titolo di una conversazione che non ne ha nessuno — né custom né un primo
|
|
58
|
+
* prompt da cui derivarlo. È un SEGNAPOSTO di resa, non un titolo: chi copia
|
|
59
|
+
* il titolo altrove (T162, il sidecar per compass) deve poterlo riconoscere e
|
|
60
|
+
* trattarlo come assenza, invece di propagare la stringa. Esportato per quello
|
|
61
|
+
* — una seconda copia del letterale divergerebbe al primo ritocco del testo. */
|
|
62
|
+
export const NO_TITLE = '(senza titolo)';
|
|
57
63
|
// T110 — valore che il CLI scrive nel campo modello dei record che fabbrica da
|
|
58
64
|
// sé (osservato su `{"type":"text","text":"No response requested."}`). Restano
|
|
59
65
|
// `type:assistant` a tutti gli effetti: chi si fida del campo etichetta la
|
|
@@ -248,7 +254,7 @@ export function parseTranscript(content, path, mtime, sizeBytes) {
|
|
|
248
254
|
cwd,
|
|
249
255
|
gitBranch,
|
|
250
256
|
parentUuid,
|
|
251
|
-
title: sanitize(customTitle || firstUserText ||
|
|
257
|
+
title: sanitize(customTitle || firstUserText || NO_TITLE),
|
|
252
258
|
ts: mtime,
|
|
253
259
|
path,
|
|
254
260
|
sizeBytes,
|
package/dist/task-index.js
CHANGED
|
@@ -37,6 +37,31 @@ export function appendNote(projectRoot, sessionId, note) {
|
|
|
37
37
|
export function appendPriority(projectRoot, sessionId, priority) {
|
|
38
38
|
appendSessionRecord(projectRoot, { sessionId, priority });
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* T162 — scrive i campi DERIVATI (`title`, `model`) di una conversazione.
|
|
42
|
+
*
|
|
43
|
+
* I due vanno in UN record e non in due, anche se il lettore risolve last-wins
|
|
44
|
+
* per campo e due append darebbero lo stesso esito finale: due record lasciano
|
|
45
|
+
* un istante in cui su disco lo stato è a metà, e in quel mezzo ci sta il
|
|
46
|
+
* lettore di compass — che mostrerebbe il titolo nuovo e riprenderebbe col
|
|
47
|
+
* modello vecchio. È la stessa regola di `writeSessionMarks` lato compass.
|
|
48
|
+
*
|
|
49
|
+
* Il record porta SOLO i campi passati: uno che portasse anche l'altro col
|
|
50
|
+
* valore che aveva prima lo riscriverebbe, e il last-wins trasformerebbe quella
|
|
51
|
+
* riscrittura in una sovrascrittura di ciò che un altro scrittore ha messo nel
|
|
52
|
+
* frattempo. Un insieme vuoto non scrive niente, invece di appendere un record
|
|
53
|
+
* col solo `sessionId` che nessun lettore userebbe.
|
|
54
|
+
*/
|
|
55
|
+
export function appendSessionMeta(projectRoot, sessionId, fields) {
|
|
56
|
+
const rec = { sessionId };
|
|
57
|
+
if (fields.title !== undefined)
|
|
58
|
+
rec.title = fields.title;
|
|
59
|
+
if (fields.model !== undefined)
|
|
60
|
+
rec.model = fields.model;
|
|
61
|
+
if (rec.title === undefined && rec.model === undefined)
|
|
62
|
+
return;
|
|
63
|
+
appendSessionRecord(projectRoot, rec);
|
|
64
|
+
}
|
|
40
65
|
// Una sola lettura del JSONL per entrambe le mappe: il deck poll-a l'indice a
|
|
41
66
|
// ogni tick, leggere il file due volte raddoppierebbe l'I/O per nulla.
|
|
42
67
|
// Last-wins per campo (un re-pin dello stesso sessionId sovrascrive), e i due
|
|
@@ -48,12 +73,14 @@ export function loadSessionIndex(projectRoot) {
|
|
|
48
73
|
const pinned = new Map();
|
|
49
74
|
const notes = new Map();
|
|
50
75
|
const priority = new Set();
|
|
76
|
+
const titles = new Map();
|
|
77
|
+
const models = new Map();
|
|
51
78
|
let content;
|
|
52
79
|
try {
|
|
53
80
|
content = readFileSync(taskIndexPath(projectRoot), 'utf8');
|
|
54
81
|
}
|
|
55
82
|
catch {
|
|
56
|
-
return { bindings, forkOf, pinned, notes, priority };
|
|
83
|
+
return { bindings, forkOf, pinned, notes, priority, titles, models };
|
|
57
84
|
}
|
|
58
85
|
let order = 0; // posizione crescente dei record pinned → rango di pin (D2)
|
|
59
86
|
for (const line of content.split('\n')) {
|
|
@@ -108,10 +135,32 @@ export function loadSessionIndex(projectRoot) {
|
|
|
108
135
|
else
|
|
109
136
|
priority.delete(d.sessionId);
|
|
110
137
|
}
|
|
138
|
+
// T162 — stesso last-wins per campo di `note`, con una differenza che
|
|
139
|
+
// conta: NESSUNA sanificazione in lettura. `note` la paga perché il deck
|
|
140
|
+
// la mette nel frame e il file è editabile a mano; qui il valore non
|
|
141
|
+
// entra nel frame (il deck il titolo lo ha dalla `Session`) e chi lo
|
|
142
|
+
// rende — compass, sotto Pango — non fa contabilità di colonne.
|
|
143
|
+
//
|
|
144
|
+
// Sanificare qui avrebbe un costo suo: il deck decide se riempire il
|
|
145
|
+
// buco CONFRONTANDO il valore letto con quello derivato, e un valore
|
|
146
|
+
// riscritto in lettura non combacerebbe mai col derivato — un append a
|
|
147
|
+
// ogni tick del poll, per sempre, senza nessun errore a dirlo.
|
|
148
|
+
if (typeof d.title === 'string') {
|
|
149
|
+
if (d.title)
|
|
150
|
+
titles.set(d.sessionId, d.title);
|
|
151
|
+
else
|
|
152
|
+
titles.delete(d.sessionId);
|
|
153
|
+
}
|
|
154
|
+
if (typeof d.model === 'string') {
|
|
155
|
+
if (d.model)
|
|
156
|
+
models.set(d.sessionId, d.model);
|
|
157
|
+
else
|
|
158
|
+
models.delete(d.sessionId);
|
|
159
|
+
}
|
|
111
160
|
}
|
|
112
161
|
catch {
|
|
113
162
|
// riga corrotta → skip
|
|
114
163
|
}
|
|
115
164
|
}
|
|
116
|
-
return { bindings, forkOf, pinned, notes, priority };
|
|
165
|
+
return { bindings, forkOf, pinned, notes, priority, titles, models };
|
|
117
166
|
}
|
package/package.json
CHANGED
package/scripts/deck-run
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
# continuità nuova ........ --resume [--fork]
|
|
14
14
|
# prompt --prompt-kind none|recap|preflight|run|checkpoint | --prompt <testo>
|
|
15
15
|
# modello --model fable|opus|sonnet|haiku
|
|
16
|
+
# Fuori dai quattro, e volutamente non un asse: --new-window, che cambia SOLO il
|
|
17
|
+
# verbo dell'exec finale (tab nella finestra attiva → finestra nuova).
|
|
16
18
|
# Il terzo asse (T56) prima non esisteva: il prompt era una CONSEGUENZA degli
|
|
17
19
|
# altri due (bound ⇒ recap, --no-task ⇒ niente, --resume ⇒ niente), quindi
|
|
18
20
|
# "task-bound SENZA prompt" era inesprimibile e l'unico modo di non avere un
|
|
@@ -78,6 +80,17 @@ SESSION_ID=""
|
|
|
78
80
|
RESUME_ID=""
|
|
79
81
|
NO_TASK=0
|
|
80
82
|
FORK=0
|
|
83
|
+
# T162 — finestra NUOVA invece di una tab nella finestra attiva.
|
|
84
|
+
#
|
|
85
|
+
# `ptyxis --tab` va sempre nella finestra col focus, e va bene per il deck, che
|
|
86
|
+
# è già una tab della finestra del progetto. Un chiamante che vive FUORI da
|
|
87
|
+
# quella finestra — compass, che riapre una conversazione pinnata di un
|
|
88
|
+
# progetto chiuso — non ha una finestra attiva giusta: senza un verbo diverso
|
|
89
|
+
# la tab finisce nella finestra di un altro progetto, o in una a caso.
|
|
90
|
+
#
|
|
91
|
+
# Non è un quinto asse: non cambia cosa gira nella tab, solo dove la tab nasce.
|
|
92
|
+
# Vale identico nei due rami di spawn (inline e profile).
|
|
93
|
+
NEW_WINDOW=0
|
|
81
94
|
TITLE_NOTE=""
|
|
82
95
|
# Vuoto = flag non passato (≠ un default già risolto): serve a distinguere
|
|
83
96
|
# "kind implicito" da "kind chiesto", perché con --no-task il primo è legittimo
|
|
@@ -157,6 +170,8 @@ while [[ $# -gt 0 ]]; do
|
|
|
157
170
|
FORK=1; shift ;;
|
|
158
171
|
--no-task)
|
|
159
172
|
NO_TASK=1; shift ;;
|
|
173
|
+
--new-window)
|
|
174
|
+
NEW_WINDOW=1; shift ;;
|
|
160
175
|
*)
|
|
161
176
|
if [[ -z "$TASK" ]]; then TASK="$1"; shift
|
|
162
177
|
else echo "argomento inatteso: '$1'" >&2; exit 2; fi ;;
|
|
@@ -190,7 +205,10 @@ USAGE="uso: deck-run <TaskID> [--model <alias>] [--prompt-kind <kind>] [--sessio
|
|
|
190
205
|
valore ignoto → fallback sul default, con avviso su stderr
|
|
191
206
|
|
|
192
207
|
--title-note nota della conversazione, appesa al titolo tab
|
|
193
|
-
(ridotta a lettere/cifre/spazi/-/_, cap 60 char)
|
|
208
|
+
(ridotta a lettere/cifre/spazi/-/_, cap 60 char)
|
|
209
|
+
|
|
210
|
+
--new-window apre una FINESTRA nuova invece di una tab nella finestra
|
|
211
|
+
attiva. Per chi chiama da fuori dalla finestra del progetto"
|
|
194
212
|
|
|
195
213
|
if [[ $NO_TASK -eq 1 && -n "$TASK" ]]; then
|
|
196
214
|
echo "--no-task e <TaskID> sono mutuamente esclusivi" >&2
|
|
@@ -669,19 +687,30 @@ IN_TAB_CMD="${LOOM_DECK_INTAB_CMD:-$_default_intab}"
|
|
|
669
687
|
# vedono comunque quello che vedevano prima.
|
|
670
688
|
printf 'LOOM_DECK_INTAB %s\n' "$IN_TAB_CMD"
|
|
671
689
|
|
|
690
|
+
# Verbo di apertura (T162): tab nella finestra attiva, o finestra nuova. Una
|
|
691
|
+
# variabile e non due rami duplicati per ogni modalità di spawn — `--new-window`
|
|
692
|
+
# è l'unica differenza fra i due casi.
|
|
693
|
+
if [[ $NEW_WINDOW -eq 1 ]]; then WIN_FLAG="--new-window"; else WIN_FLAG="--tab"; fi
|
|
694
|
+
|
|
672
695
|
case "$MODE" in
|
|
673
696
|
inline)
|
|
674
697
|
# --tab: tab nella window attiva. -- <cmd>: comando inline nella tab.
|
|
675
698
|
# LOOM_TASK viaggia dentro il comando → nessuna scrittura dconf.
|
|
676
|
-
exec ptyxis
|
|
699
|
+
exec ptyxis "$WIN_FLAG" -d "$WORKDIR" -T "${TITLE}" -- bash -lc "$IN_TAB_CMD"
|
|
677
700
|
;;
|
|
678
701
|
profile)
|
|
679
702
|
# --tab-with-profile: riusa il profilo (custom-command FISSO) → per iniettare
|
|
680
703
|
# LOOM_TASK+task va riscritto il custom-command al volo via dconf (stato condiviso).
|
|
704
|
+
# Con --new-window i due flag convivono: la finestra nasce e il profilo
|
|
705
|
+
# decide cosa gira nella sua prima tab.
|
|
681
706
|
dpath="/org/gnome/Ptyxis/Profiles/${PROFILE_UUID}/"
|
|
682
707
|
dconf write "${dpath}use-custom-command" true
|
|
683
708
|
dconf write "${dpath}custom-command" "'bash -lc \"${IN_TAB_CMD//\"/\\\"}\"'"
|
|
684
|
-
|
|
709
|
+
if [[ $NEW_WINDOW -eq 1 ]]; then
|
|
710
|
+
exec ptyxis --new-window --tab-with-profile="$PROFILE_UUID" -d "$WORKDIR" -T "${TITLE}"
|
|
711
|
+
else
|
|
712
|
+
exec ptyxis --tab-with-profile="$PROFILE_UUID" -d "$WORKDIR" -T "${TITLE}"
|
|
713
|
+
fi
|
|
685
714
|
;;
|
|
686
715
|
*)
|
|
687
716
|
echo "LOOM_DECK_SPAWN_MODE ignoto: '$MODE' (usa 'inline' o 'profile')" >&2
|