instar 1.3.1584 → 1.3.1586
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/dashboard/console/console.css +17 -0
- package/dashboard/console/sessions-model.js +54 -6
- package/dashboard/console/sessions.js +137 -13
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +20 -2
- package/dist/commands/server.js.map +1 -1
- package/dist/core/AutoUpdater.d.ts.map +1 -1
- package/dist/core/AutoUpdater.js +7 -3
- package/dist/core/AutoUpdater.js.map +1 -1
- package/dist/core/ForegroundRestartWatcher.d.ts +2 -0
- package/dist/core/ForegroundRestartWatcher.d.ts.map +1 -1
- package/dist/core/ForegroundRestartWatcher.js +13 -0
- package/dist/core/ForegroundRestartWatcher.js.map +1 -1
- package/dist/core/PlannedRestartVerifyGuard.d.ts +26 -0
- package/dist/core/PlannedRestartVerifyGuard.d.ts.map +1 -0
- package/dist/core/PlannedRestartVerifyGuard.js +125 -0
- package/dist/core/PlannedRestartVerifyGuard.js.map +1 -0
- package/dist/core/QueueDrainLoop.d.ts +10 -2
- package/dist/core/QueueDrainLoop.d.ts.map +1 -1
- package/dist/core/QueueDrainLoop.js +28 -6
- package/dist/core/QueueDrainLoop.js.map +1 -1
- package/dist/core/SessionManager.d.ts +3 -0
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +1 -0
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/SessionRouter.d.ts +21 -3
- package/dist/core/SessionRouter.d.ts.map +1 -1
- package/dist/core/SessionRouter.js +58 -6
- package/dist/core/SessionRouter.js.map +1 -1
- package/dist/core/SessionTranscriptReader.d.ts +25 -0
- package/dist/core/SessionTranscriptReader.d.ts.map +1 -0
- package/dist/core/SessionTranscriptReader.js +143 -0
- package/dist/core/SessionTranscriptReader.js.map +1 -0
- package/dist/core/WriteDomainRegistry.d.ts.map +1 -1
- package/dist/core/WriteDomainRegistry.js +8 -0
- package/dist/core/WriteDomainRegistry.js.map +1 -1
- package/dist/data/standards-guard-index.json +1 -1
- package/dist/data/standards-guard-index.meta.json +2 -2
- package/dist/data/standards-registry.meta.json +1 -1
- package/dist/lifeline/ServerSupervisor.d.ts +1 -0
- package/dist/lifeline/ServerSupervisor.d.ts.map +1 -1
- package/dist/lifeline/ServerSupervisor.js +19 -2
- package/dist/lifeline/ServerSupervisor.js.map +1 -1
- package/dist/localVerify/LocalVerifyService.d.ts +1 -1
- package/dist/localVerify/LocalVerifyService.d.ts.map +1 -1
- package/dist/localVerify/LocalVerifyService.js +8 -3
- package/dist/localVerify/LocalVerifyService.js.map +1 -1
- package/dist/monitoring/guardManifest.d.ts.map +1 -1
- package/dist/monitoring/guardManifest.js +1 -0
- package/dist/monitoring/guardManifest.js.map +1 -1
- package/dist/native/instar-desktop-host +0 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +146 -1
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +48 -48
- package/src/data/standards-guard-index.json +1 -1
- package/src/data/standards-guard-index.meta.json +2 -2
- package/src/data/standards-registry.meta.json +1 -1
- package/upgrades/1.3.1585.md +29 -0
- package/upgrades/1.3.1586.md +28 -0
- package/upgrades/side-effects/console-session-followups.eli16.md +18 -0
- package/upgrades/side-effects/console-session-followups.md +44 -0
- package/upgrades/side-effects/console-session-history.eli16.md +14 -0
- package/upgrades/side-effects/console-session-history.md +32 -0
- package/upgrades/side-effects/planned-restart-local-verify-hold.md +81 -0
- package/upgrades/side-effects/spawn-handoff-ack.md +117 -0
- package/upgrades/spawn-handoff-ack.eli16.md +43 -0
|
@@ -985,11 +985,28 @@ button.run-mark:focus-visible { outline: 2px solid var(--accent); outline-offset
|
|
|
985
985
|
.sess-term-wrap { position: relative; flex: 1; min-height: 320px; display: flex; }
|
|
986
986
|
.sess-term { flex: 1; min-width: 0; min-height: 320px; height: 52dvh; background: #0a0e17; border: 1px solid var(--hairline); border-radius: var(--r-md); padding: 6px; overflow: hidden; }
|
|
987
987
|
.sess-term.plain { overflow: auto; }
|
|
988
|
+
.sess-tabs { display: flex; gap: var(--s-2); }
|
|
989
|
+
.sess-tab[aria-pressed="true"] { border-color: var(--accent); color: var(--accent); background: var(--accent-wash); }
|
|
990
|
+
.sess-history { flex: 1; min-width: 0; min-height: 320px; height: 52dvh; overflow-y: auto; padding: var(--s-3); background: var(--surface); border: 1px solid var(--hairline); border-radius: var(--r-md); display: flex; flex-direction: column; gap: var(--s-3); }
|
|
991
|
+
.sess-history[hidden], .sess-term[hidden] { display: none; }
|
|
992
|
+
.sess-history-note { margin: 0; text-align: center; font-size: 13px; color: var(--text-muted); }
|
|
993
|
+
.sess-h { max-width: 92%; }
|
|
994
|
+
.sess-h-user { align-self: flex-end; background: var(--accent-wash); border: 1px solid var(--hairline); border-radius: var(--r-md); padding: var(--s-2) var(--s-3); }
|
|
995
|
+
.sess-h-assistant { align-self: flex-start; }
|
|
996
|
+
.sess-h-who { font-size: 12px; color: var(--text-faint); margin-bottom: 2px; }
|
|
997
|
+
.sess-h-text { white-space: pre-wrap; overflow-wrap: anywhere; font-size: 14.5px; line-height: 1.5; color: var(--text); }
|
|
998
|
+
.sess-h-tool { margin: 0; font-family: var(--font-mono); font-size: 12px; color: var(--text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; }
|
|
988
999
|
.sess-pre { margin: 0; font-family: var(--font-mono); font-size: 12px; line-height: 1.4; color: #d6dbe4; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
989
1000
|
.sess-resume { position: absolute; bottom: 12px; left: 50%; transform: translateX(-50%); z-index: 2; }
|
|
990
1001
|
.sess-composer { display: flex; gap: var(--s-2); align-items: flex-end; position: sticky; bottom: 0; padding: var(--s-2) 0; background: var(--bg); }
|
|
991
1002
|
.sess-input { flex: 1; min-height: 44px; font-size: 16px; resize: none; }
|
|
992
1003
|
.sess-composer-btns { display: flex; gap: var(--s-2); }
|
|
1004
|
+
.sess-attach { min-width: 44px; min-height: 44px; padding: 0 10px; font-size: 18px; }
|
|
1005
|
+
.sess-attach:disabled { opacity: .45; cursor: not-allowed; }
|
|
1006
|
+
.sess-attachments { display: flex; flex-wrap: wrap; gap: var(--s-2); }
|
|
1007
|
+
.sess-file { gap: 8px; max-width: 100%; overflow: hidden; }
|
|
1008
|
+
.sess-file span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 260px; }
|
|
1009
|
+
.sess-composer.dropping { outline: 2px dashed var(--accent); outline-offset: 4px; border-radius: var(--r-md); }
|
|
993
1010
|
.keys-tray { display: flex; flex-direction: column; gap: var(--s-3); padding: var(--s-3); border: 1px solid var(--hairline); border-radius: var(--r-md); }
|
|
994
1011
|
.keys-row { display: flex; flex-wrap: wrap; gap: var(--s-2); }
|
|
995
1012
|
.key-chip { min-width: 44px; min-height: 44px; justify-content: center; font-family: var(--font-mono); }
|
|
@@ -102,7 +102,52 @@ function nicknameOf(pool, machineId) {
|
|
|
102
102
|
* The whole list. sessionsRes is the GET /sessions?scope=pool read; ws is the live
|
|
103
103
|
* socket's list for this machine (null when the socket has not sent one).
|
|
104
104
|
*/
|
|
105
|
-
|
|
105
|
+
/** The message a session receives for attached files: the operator's text, then one
|
|
106
|
+
* line per file naming where it was saved, so the session can open it. */
|
|
107
|
+
export function attachmentMessage(text, paths) {
|
|
108
|
+
const lines = paths.map((p) => `[Attached file from the dashboard: ${p} (read it to see what I sent)]`);
|
|
109
|
+
return [String(text || '').trim(), ...lines].filter(Boolean).join('\n');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** `triggeredBy` on a session the operator created from the dashboard's New session box. */
|
|
113
|
+
export const OPERATOR_CREATED = 'dashboard-operator';
|
|
114
|
+
|
|
115
|
+
/** Internal = machinery the operator did not start: scheduled jobs, and helper sessions
|
|
116
|
+
* the agent spawned for itself (no chat attached, not created from the dashboard). A
|
|
117
|
+
* conversation (Telegram, Slack, ...) is never internal. Hidden by default, one toggle away. */
|
|
118
|
+
export function isInternalSession(s) {
|
|
119
|
+
if (!s) return false;
|
|
120
|
+
if (s.jobSlug || s.type === 'job') return true;
|
|
121
|
+
return (s.platform || 'headless') === 'headless' && s.triggeredBy !== OPERATOR_CREATED;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The History read for a session: its conversation record, a page at a time. A session
|
|
125
|
+
* on another machine is read through this machine, which asks the owner. */
|
|
126
|
+
export const HISTORY_PAGE = 150;
|
|
127
|
+
export function historyUrl(s, before = null) {
|
|
128
|
+
const q = new URLSearchParams({ limit: String(HISTORY_PAGE) });
|
|
129
|
+
if (before != null) q.set('before', String(before));
|
|
130
|
+
if (s.remote && s.machineId) q.set('machineId', s.machineId);
|
|
131
|
+
return `/sessions/${encodeURIComponent(s.tmuxSession)}/transcript?${q}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Words for a failed History read: never "no history" when the read failed. */
|
|
135
|
+
export function historyErrorWords(res) {
|
|
136
|
+
const d = (res && res.data) || {};
|
|
137
|
+
if (d.reason === 'no-transcript') return 'This session has no conversation record yet.';
|
|
138
|
+
if (d.reason === 'peer-unreachable' || d.reason === 'peer-unknown') return `Can’t tell: ${d.error || 'the session’s machine did not answer'}.`;
|
|
139
|
+
const why = d.error || (res && res.reason) || (res && res.status ? `the server answered ${res.status}` : 'the server could not be reached');
|
|
140
|
+
return `Can’t tell: ${String(why).replace(/\.$/, '')}.`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** A user line from a chat bridge carries a routing prefix; show only what was said. */
|
|
144
|
+
export function historyText(e) {
|
|
145
|
+
let t = String((e && e.text) || '');
|
|
146
|
+
if (e && e.role === 'user') t = t.replace(/^\[(telegram|slack|whatsapp):[^\]]*\]\s*/, '');
|
|
147
|
+
return t;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function buildSessions(sessionsRes, { ws = null, now = Date.now(), topicNames = {}, showInternal = false } = {}) {
|
|
106
151
|
if (!sessionsRes || !sessionsRes.ok) return { ok: false, reason: `Couldn’t list sessions: ${(sessionsRes && sessionsRes.reason) || 'no answer'}.` };
|
|
107
152
|
const data = sessionsRes.data || {};
|
|
108
153
|
if (!Array.isArray(data.sessions)) return { ok: false, reason: 'The session list answered without sessions.' };
|
|
@@ -114,7 +159,7 @@ export function buildSessions(sessionsRes, { ws = null, now = Date.now(), topicN
|
|
|
114
159
|
const duplicateConversations = new Set((Array.isArray(pool.duplicateTopics) ? pool.duplicateTopics : [])
|
|
115
160
|
.filter((d) => typeof d.platform === 'string' && d.platformId != null)
|
|
116
161
|
.map((d) => `${d.platform}:${String(d.platformId)}`));
|
|
117
|
-
const
|
|
162
|
+
const allRows = [...local.map((s) => ({ ...s, machineNickname: s.machineNickname || selfName })), ...remote].map((s) => {
|
|
118
163
|
const a = activity(s, now);
|
|
119
164
|
const duplicate = s.duplicateTopic === true || (typeof s.platform === 'string' && s.platformId != null
|
|
120
165
|
&& duplicateConversations.has(`${s.platform}:${String(s.platformId)}`));
|
|
@@ -123,15 +168,18 @@ export function buildSessions(sessionsRes, { ws = null, now = Date.now(), topicN
|
|
|
123
168
|
const running = Number.isFinite(started) ? spanWords(now - started) : null;
|
|
124
169
|
const sub = [machine, running && running !== 'now' ? running : running ? 'just started' : null, duplicate ? 'running on two machines at once' : a.state === 'no-reading' ? null : a.words].filter(Boolean).join(' · ');
|
|
125
170
|
return {
|
|
126
|
-
key: sessionKey(s), title: sessionTitle(s, topicNames), sub, end: a.end, activity: a.state, duplicate,
|
|
171
|
+
key: sessionKey(s), title: sessionTitle(s, topicNames), sub, end: a.end, activity: a.state, duplicate, internal: isInternalSession(s),
|
|
127
172
|
tone: duplicate ? 'bad' : a.state === 'quiet' ? 'attn' : a.state === 'no-reading' ? 'unknown' : null,
|
|
128
173
|
machine, remote: s.remote === true, model: modelWords(s.model),
|
|
129
|
-
session: { tmuxSession: s.tmuxSession, id: s.id, name: s.name, machineId: s.machineId || null, machineNickname: machine, remote: s.remote === true, protected: s.protected, platform: s.platform || 'headless', platformId: s.platformId ?? null, model: s.model || null, type: s.type || (s.jobSlug ? 'job' : 'interactive'), telemetry: s.telemetry || null, startedAt: s.startedAt || null, status: s.status },
|
|
174
|
+
session: { tmuxSession: s.tmuxSession, id: s.id, name: s.name, machineId: s.machineId || null, machineNickname: machine, remote: s.remote === true, protected: s.protected, platform: s.platform || 'headless', platformId: s.platformId ?? null, model: s.model || null, type: s.type || (s.jobSlug ? 'job' : 'interactive'), internal: isInternalSession(s), telemetry: s.telemetry || null, startedAt: s.startedAt || null, status: s.status },
|
|
130
175
|
};
|
|
131
176
|
});
|
|
132
177
|
// Most recently active first inside each group.
|
|
133
178
|
const lastOf = (r) => Date.parse(r.session.telemetry && r.session.telemetry.lastActivity) || Date.parse(r.session.startedAt) || 0;
|
|
134
|
-
|
|
179
|
+
allRows.sort((x, y) => lastOf(y) - lastOf(x));
|
|
180
|
+
const hiddenInternal = showInternal ? 0 : allRows.filter((r) => r.internal).length;
|
|
181
|
+
const internalCount = allRows.filter((r) => r.internal).length;
|
|
182
|
+
const rows = showInternal ? allRows : allRows.filter((r) => !r.internal);
|
|
135
183
|
const needsLook = rows.filter((r) => r.duplicate || r.activity === 'quiet');
|
|
136
184
|
const working = rows.filter((r) => !needsLook.includes(r));
|
|
137
185
|
const failedIds = new Set((Array.isArray(pool.failed) ? pool.failed : []).map((f) => f.machineId));
|
|
@@ -148,7 +196,7 @@ export function buildSessions(sessionsRes, { ws = null, now = Date.now(), topicN
|
|
|
148
196
|
})) : [];
|
|
149
197
|
const machineCount = multi ? machines.length : 1;
|
|
150
198
|
return {
|
|
151
|
-
ok: true, rows, needsLook, working, idleMachines, unanswered, multi, selfName,
|
|
199
|
+
ok: true, rows, allRows, hiddenInternal, internalCount, showInternal, needsLook, working, idleMachines, unanswered, multi, selfName,
|
|
152
200
|
selfMachineId: pool.selfMachineId || null,
|
|
153
201
|
counts: { running: rows.length, quiet: rows.filter((r) => r.activity === 'quiet').length, duplicate: rows.filter((r) => r.duplicate).length, machines: machineCount, answering: machineCount - unanswered.length },
|
|
154
202
|
};
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
buildSessions, sessionsHeadline, sessionsFacts, partialWords, titleLine, closeRequest, closeConfirmWords,
|
|
12
12
|
closeResultWords, doorChoices, modelChoices, doorModelWords, regimeWords, machineChoices, PLATFORMS,
|
|
13
13
|
platformAllowsDoor, createBody, createResultWords, defaultWords, profileSourceWords, switchLanded,
|
|
14
|
-
canSwitchDoor, KEYS, STOP_KEY, frame, nextHistoryBatch, streamErrorWords, composerRows, modelWords, HISTORY_FIRST,
|
|
14
|
+
canSwitchDoor, attachmentMessage, historyUrl, historyErrorWords, historyText, KEYS, STOP_KEY, frame, nextHistoryBatch, streamErrorWords, composerRows, modelWords, HISTORY_FIRST,
|
|
15
15
|
} from './sessions-model.js';
|
|
16
16
|
import { el, setKids, pageHead, sheet, closeSheet, story, details, note, cantTell } from './ui.js';
|
|
17
17
|
import { legacyHref } from './nav-model.js';
|
|
@@ -22,6 +22,20 @@ const XTERM_CSS = 'https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min
|
|
|
22
22
|
const FIT_JS = 'https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js';
|
|
23
23
|
const POOL_TIMEOUT_MS = 8000;
|
|
24
24
|
|
|
25
|
+
const ATTACH_MAX_BYTES = 8 * 1024 * 1024;
|
|
26
|
+
function toBase64(file) {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const r = new FileReader();
|
|
29
|
+
r.onload = () => resolve(String(r.result).replace(/^data:[^,]*,/, ''));
|
|
30
|
+
r.onerror = () => reject(r.error);
|
|
31
|
+
r.readAsDataURL(file);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const SHOW_INTERNAL_KEY = 'instar_console_show_internal';
|
|
36
|
+
function showInternal() { try { return localStorage.getItem(SHOW_INTERNAL_KEY) === '1'; } catch { return false; } }
|
|
37
|
+
function setShowInternal(on) { try { localStorage.setItem(SHOW_INTERNAL_KEY, on ? '1' : '0'); } catch { /* per-viewer convenience only */ } }
|
|
38
|
+
|
|
25
39
|
function bearer() { try { return localStorage.getItem('instar_token') || ''; } catch { return ''; } }
|
|
26
40
|
|
|
27
41
|
/** A write that keeps the status and the server's body. */
|
|
@@ -69,7 +83,7 @@ const onPage = () => Boolean(st.main && st.main.isConnected && st.main.querySele
|
|
|
69
83
|
function currentModel() {
|
|
70
84
|
const src = st.poolData || (st.lastFailReason ? { ok: false, reason: st.lastFailReason } : null);
|
|
71
85
|
if (!src) return null;
|
|
72
|
-
return buildSessions(src, { ws: st.wsUp ? st.wsList : null, now: Date.now(), topicNames: st.topicNames || {} });
|
|
86
|
+
return buildSessions(src, { ws: st.wsUp ? st.wsList : null, now: Date.now(), topicNames: st.topicNames || {}, showInternal: showInternal() });
|
|
73
87
|
}
|
|
74
88
|
|
|
75
89
|
function paint() {
|
|
@@ -83,7 +97,7 @@ function paint() {
|
|
|
83
97
|
paintList(page.querySelector('.sess-list'), m, key);
|
|
84
98
|
const v = page.querySelector('.sess-view');
|
|
85
99
|
if (!key) { if (st.view) leaveView(); setKids(v, el('p', { class: 'empty-line sess-pick', text: 'Pick a session to watch it and type to it.' })); return; }
|
|
86
|
-
const row = m.ok ? m.
|
|
100
|
+
const row = m.ok ? m.allRows.find((r) => r.key === key) : null;
|
|
87
101
|
if (st.view && st.view.key === key) { if (row) updateViewTitle(row); else markEnded(); return; }
|
|
88
102
|
if (st.view) leaveView();
|
|
89
103
|
if (!row) {
|
|
@@ -118,7 +132,13 @@ function paintList(box, m, openKey) {
|
|
|
118
132
|
m.idleMachines.map((i) => el('p', { class: `machine-line${i.ok ? '' : ' dashed'}`, text: `${i.name}: ${i.words}` }))));
|
|
119
133
|
}
|
|
120
134
|
}
|
|
121
|
-
|
|
135
|
+
if (m.ok && m.internalCount) {
|
|
136
|
+
const on = m.showInternal;
|
|
137
|
+
parts.push(el('button', { class: 'link-btn sess-internal-toggle', type: 'button', 'aria-pressed': on ? 'true' : 'false',
|
|
138
|
+
text: on ? `Hide ${m.internalCount} internal session${m.internalCount === 1 ? '' : 's'} (jobs and helpers)` : `Show ${m.internalCount} internal session${m.internalCount === 1 ? '' : 's'} (jobs and helpers)`,
|
|
139
|
+
onclick: () => { setShowInternal(!on); paint(); } }));
|
|
140
|
+
}
|
|
141
|
+
parts.push(el('p', { class: 'foot-note' }, el('a', { href: legacyHref('sessions'), text: 'Open sessions in the legacy dashboard' })));
|
|
122
142
|
setKids(box, parts);
|
|
123
143
|
if (focused) {
|
|
124
144
|
const esc = typeof CSS !== 'undefined' && CSS.escape ? CSS.escape(focused) : focused.replace(/["\\]/g, '\\$&');
|
|
@@ -222,8 +242,37 @@ function openView(box, row) {
|
|
|
222
242
|
const input = el('textarea', { class: 'reason-input sess-input', rows: '1', placeholder: 'Type a message…', 'aria-label': 'Message to the session' });
|
|
223
243
|
const sendBtn = el('button', { class: 'btn-primary btn-inline', type: 'button', text: 'Send', 'data-send': '1' });
|
|
224
244
|
const keysBtn = el('button', { class: 'btn-quiet', type: 'button', text: 'Keys', 'aria-expanded': 'false', 'data-keys': '1' });
|
|
245
|
+
// Attach: a screenshot or file, picked, pasted or dropped. Saved on this machine, so a
|
|
246
|
+
// session on another machine says so instead of offering it.
|
|
247
|
+
const filePick = el('input', { type: 'file', multiple: true, hidden: true, 'data-file': '1' });
|
|
248
|
+
const attachBtn = el('button', { class: 'btn-quiet sess-attach', type: 'button', 'aria-label': 'Attach a screenshot or file', text: '📎', 'data-attach': '1',
|
|
249
|
+
title: s.remote ? `Attaching works for sessions on this machine; open ${s.machineNickname}’s own console to attach there.` : 'Attach a screenshot or file' });
|
|
250
|
+
if (s.remote) attachBtn.disabled = true;
|
|
251
|
+
const chips = el('div', { class: 'sess-attachments', hidden: true });
|
|
252
|
+
const files = [];
|
|
253
|
+
const paintChips = () => {
|
|
254
|
+
chips.hidden = !files.length;
|
|
255
|
+
setKids(chips, files.map((f, i) => el('span', { class: 'chip sess-file' }, el('span', { text: f.name }),
|
|
256
|
+
el('button', { class: 'link-btn', type: 'button', 'aria-label': `Remove ${f.name}`, text: '✕', onclick: () => { files.splice(i, 1); paintChips(); } }))));
|
|
257
|
+
};
|
|
258
|
+
const addFiles = (list) => {
|
|
259
|
+
if (s.remote) { setStatus(`Attaching works for sessions on this machine; open ${s.machineNickname}’s own console to attach there.`); return; }
|
|
260
|
+
for (const f of list) {
|
|
261
|
+
if (f.size > ATTACH_MAX_BYTES) { setStatus(`${f.name} is too big to attach (up to 8 MB).`); continue; }
|
|
262
|
+
files.push({ name: f.name || `pasted-${Date.now()}.png`, file: f });
|
|
263
|
+
}
|
|
264
|
+
paintChips();
|
|
265
|
+
};
|
|
266
|
+
attachBtn.addEventListener('click', () => filePick.click());
|
|
267
|
+
filePick.addEventListener('change', () => { addFiles([...filePick.files]); filePick.value = ''; });
|
|
225
268
|
const tray = el('div', { class: 'keys-tray', hidden: true });
|
|
226
|
-
|
|
269
|
+
// Live (the terminal) or History (the conversation record, which goes back to the start:
|
|
270
|
+
// a full-screen session keeps no terminal scrollback).
|
|
271
|
+
const historyBox = el('div', { class: 'sess-history', hidden: true, 'aria-label': 'Conversation history' });
|
|
272
|
+
const liveTab = el('button', { class: 'chip sess-tab', type: 'button', text: 'Live', 'aria-pressed': 'true', 'data-tab': 'live' });
|
|
273
|
+
const historyTab = el('button', { class: 'chip sess-tab', type: 'button', text: 'History', 'aria-pressed': 'false', 'data-tab': 'history' });
|
|
274
|
+
const tabs = el('div', { class: 'sess-tabs', role: 'group', 'aria-label': 'View' }, liveTab, historyTab);
|
|
275
|
+
const view = { key: row.key, s, titleSub, statusLine, termBox, historyBox, history: null, resume, following: true, pending: null, loaded: HISTORY_FIRST, exhausted: false, loading: false, term: null, fit: null, pre: null };
|
|
227
276
|
st.view = view;
|
|
228
277
|
|
|
229
278
|
// Keys: the ordinary ones, then Stop separated, red, and asked once.
|
|
@@ -239,26 +288,60 @@ function openView(box, row) {
|
|
|
239
288
|
tray.append(keyRow, stopBox);
|
|
240
289
|
keysBtn.addEventListener('click', () => { tray.hidden = !tray.hidden; keysBtn.setAttribute('aria-expanded', tray.hidden ? 'false' : 'true'); });
|
|
241
290
|
|
|
242
|
-
const sendText = () => {
|
|
243
|
-
|
|
244
|
-
if (!text.trim()) return;
|
|
291
|
+
const sendText = async () => {
|
|
292
|
+
let text = input.value;
|
|
293
|
+
if (!text.trim() && !files.length) return;
|
|
294
|
+
if (!st.wsUp) { setStatus('Not sent: live updates are paused. Your text is still here.'); return; }
|
|
295
|
+
if (files.length) {
|
|
296
|
+
sendBtn.disabled = true; setStatus(`Attaching ${files.length === 1 ? files[0].name : `${files.length} files`}…`);
|
|
297
|
+
const paths = [];
|
|
298
|
+
for (const f of files) {
|
|
299
|
+
const r = await send('POST', `/sessions/${encodeURIComponent(view.s.tmuxSession)}/attachments`, { filename: f.name, dataBase64: await toBase64(f.file) });
|
|
300
|
+
if (!r.ok || !r.data.path) { sendBtn.disabled = false; setStatus(`Not sent: ${f.name} could not be attached (${r.data.error || `status ${r.status}`}). Your message is still here.`); return; }
|
|
301
|
+
paths.push(r.data.path);
|
|
302
|
+
}
|
|
303
|
+
sendBtn.disabled = false; setStatus('');
|
|
304
|
+
text = attachmentMessage(text, paths);
|
|
305
|
+
}
|
|
245
306
|
if (!wsSend(frame('input', view.s, { text }))) { setStatus('Not sent: live updates are paused. Your text is still here.'); return; }
|
|
307
|
+
files.length = 0; paintChips();
|
|
246
308
|
input.value = ''; input.rows = 1; input.focus();
|
|
247
309
|
};
|
|
310
|
+
input.addEventListener('paste', (e) => {
|
|
311
|
+
const pasted = [...((e.clipboardData && e.clipboardData.files) || [])];
|
|
312
|
+
if (pasted.length) { e.preventDefault(); addFiles(pasted); }
|
|
313
|
+
});
|
|
314
|
+
const composer = el('div', { class: 'sess-composer' }, attachBtn, filePick, input, el('div', { class: 'sess-composer-btns' }, keysBtn, sendBtn));
|
|
315
|
+
for (const t of [composer, termBox]) {
|
|
316
|
+
t.addEventListener('dragover', (e) => { if (e.dataTransfer && [...e.dataTransfer.types].includes('Files')) { e.preventDefault(); composer.classList.add('dropping'); } });
|
|
317
|
+
t.addEventListener('dragleave', () => composer.classList.remove('dropping'));
|
|
318
|
+
t.addEventListener('drop', (e) => { if (!e.dataTransfer || !e.dataTransfer.files.length) return; e.preventDefault(); composer.classList.remove('dropping'); addFiles([...e.dataTransfer.files]); });
|
|
319
|
+
}
|
|
248
320
|
sendBtn.addEventListener('click', sendText);
|
|
249
321
|
input.addEventListener('input', () => { input.rows = composerRows(input.value); });
|
|
250
322
|
input.addEventListener('keydown', (e) => { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); sendText(); } });
|
|
251
323
|
more.addEventListener('click', () => moreSheet(view));
|
|
252
324
|
resume.addEventListener('click', () => backToLive());
|
|
325
|
+
const showTab = (which) => {
|
|
326
|
+
const hist = which === 'history';
|
|
327
|
+
liveTab.setAttribute('aria-pressed', hist ? 'false' : 'true'); historyTab.setAttribute('aria-pressed', hist ? 'true' : 'false');
|
|
328
|
+
termBox.hidden = hist; historyBox.hidden = !hist; if (hist) resume.hidden = true;
|
|
329
|
+
if (hist && !view.history) openHistory(view);
|
|
330
|
+
if (!hist && view.fit) requestAnimationFrame(() => { try { view.fit.fit(); } catch { /* not visible yet */ } });
|
|
331
|
+
};
|
|
332
|
+
liveTab.addEventListener('click', () => showTab('live'));
|
|
333
|
+
historyTab.addEventListener('click', () => showTab('history'));
|
|
253
334
|
|
|
254
335
|
setKids(box, el('div', { class: 'sess-open', 'data-open': row.key },
|
|
255
336
|
el('div', { class: 'sess-bar' },
|
|
256
337
|
el('a', { class: 'chip icon-btn sess-back', href: listHref, 'aria-label': 'All sessions', text: '‹' }),
|
|
257
338
|
el('div', { class: 'sess-titles' }, el('h1', { class: 'sess-title', text: row.title }), titleSub),
|
|
258
339
|
more),
|
|
340
|
+
tabs,
|
|
259
341
|
statusLine,
|
|
260
|
-
el('div', { class: 'sess-term-wrap' }, termBox, resume),
|
|
261
|
-
|
|
342
|
+
el('div', { class: 'sess-term-wrap' }, termBox, historyBox, resume),
|
|
343
|
+
chips,
|
|
344
|
+
composer,
|
|
262
345
|
tray));
|
|
263
346
|
|
|
264
347
|
if (s.remote) setStatus(`Streaming from ${s.machineNickname}…`);
|
|
@@ -349,6 +432,50 @@ function sendKey(key) {
|
|
|
349
432
|
if (!wsSend(frame('key', st.view.s, { key }))) setStatus('Not sent: live updates are paused.');
|
|
350
433
|
}
|
|
351
434
|
|
|
435
|
+
// ── History: the conversation record, newest at the bottom, older on scroll-up ──
|
|
436
|
+
async function openHistory(view) {
|
|
437
|
+
view.history = { before: null, done: false, loading: false, count: 0 };
|
|
438
|
+
view.historyBox.addEventListener('scroll', () => { if (view.historyBox.scrollTop < 200) loadHistory(view); });
|
|
439
|
+
await loadHistory(view);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async function loadHistory(view) {
|
|
443
|
+
const h = view.history;
|
|
444
|
+
if (!h || h.loading || h.done || st.view !== view) return;
|
|
445
|
+
h.loading = true;
|
|
446
|
+
const box = view.historyBox;
|
|
447
|
+
const marker = el('p', { class: 'sess-history-note', text: h.count ? 'Loading earlier messages…' : 'Loading the conversation…' });
|
|
448
|
+
box.prepend(marker);
|
|
449
|
+
const res = await send('GET', historyUrl(view.s, h.before));
|
|
450
|
+
marker.remove();
|
|
451
|
+
h.loading = false;
|
|
452
|
+
if (st.view !== view) return;
|
|
453
|
+
if (!res.ok || !res.data || !Array.isArray(res.data.entries)) {
|
|
454
|
+
h.done = true;
|
|
455
|
+
box.prepend(el('p', { class: 'sess-history-note', text: historyErrorWords(res) }));
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const first = !h.count;
|
|
459
|
+
const prevHeight = box.scrollHeight, prevTop = box.scrollTop;
|
|
460
|
+
const frag = document.createDocumentFragment();
|
|
461
|
+
for (const e of res.data.entries) {
|
|
462
|
+
const text = historyText(e);
|
|
463
|
+
if (!text) continue;
|
|
464
|
+
const when = e.at ? new Date(e.at).toLocaleString([], { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }) : null;
|
|
465
|
+
frag.append(e.role === 'tool'
|
|
466
|
+
? el('p', { class: 'sess-h sess-h-tool', text: `→ ${text}` })
|
|
467
|
+
: el('div', { class: `sess-h sess-h-${e.role}` }, el('div', { class: 'sess-h-who', text: [e.role === 'user' ? 'Message in' : 'Session', when].filter(Boolean).join(' · ') }), el('div', { class: 'sess-h-text', text })));
|
|
468
|
+
}
|
|
469
|
+
h.count += res.data.entries.length;
|
|
470
|
+
h.before = res.data.nextBefore;
|
|
471
|
+
if (h.before == null) { h.done = true; frag.prepend(el('p', { class: 'sess-history-note', text: h.count ? 'The start of this conversation.' : 'Nothing has been said in this session yet.' })); }
|
|
472
|
+
box.prepend(frag);
|
|
473
|
+
if (first) box.scrollTop = box.scrollHeight;
|
|
474
|
+
else box.scrollTop = prevTop + (box.scrollHeight - prevHeight);
|
|
475
|
+
// A short first page may not fill the box: keep loading until it scrolls.
|
|
476
|
+
if (!h.done && box.scrollHeight <= box.clientHeight + 200) loadHistory(view);
|
|
477
|
+
}
|
|
478
|
+
|
|
352
479
|
function leaveView() {
|
|
353
480
|
const v = st.view;
|
|
354
481
|
if (!v) return;
|
|
@@ -369,9 +496,6 @@ function moreSheet(view) {
|
|
|
369
496
|
])];
|
|
370
497
|
const actions = el('div', { class: 'sheet-actions sess-actions' });
|
|
371
498
|
if (canSwitchDoor(s)) actions.append(el('button', { class: 'btn-primary btn-inline', type: 'button', text: 'Switch door and model', 'data-door': '1', onclick: () => doorSheet(view) }));
|
|
372
|
-
actions.append(s.remote
|
|
373
|
-
? note(`Long text can only be stored for sessions on this machine; type it into the composer instead, or open ${s.machineNickname}’s own console.`)
|
|
374
|
-
: el('a', { class: 'btn-quiet', href: legacyHref('dropzone'), text: 'Paste long text' }));
|
|
375
499
|
blocks.push(actions, closeBlock(view));
|
|
376
500
|
const t = s.telemetry || {};
|
|
377
501
|
blocks.push(details({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"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"}
|
package/dist/commands/server.js
CHANGED
|
@@ -26536,11 +26536,29 @@ export async function startServer(options) {
|
|
|
26536
26536
|
}
|
|
26537
26537
|
throw new Error(`deliverMessage rejected: ${res.status} ${res.reason ?? ''}`);
|
|
26538
26538
|
},
|
|
26539
|
-
|
|
26539
|
+
// The placement hop is a deliverMessage at the epoch the CAS just
|
|
26540
|
+
// landed, read exactly like a forward. It used to send epoch 0 and
|
|
26541
|
+
// discard the reply: a receiver holding an older record for the
|
|
26542
|
+
// topic NACKed it stale and the router still logged 'spawned'.
|
|
26543
|
+
spawnOnMachine: async (machineId, msg, ownershipEpoch) => {
|
|
26540
26544
|
const url = peerUrl(machineId);
|
|
26541
26545
|
if (!url)
|
|
26542
26546
|
throw new Error(`no peer url for ${machineId}`);
|
|
26543
|
-
await meshClient.send({ machineId, url }, { type: 'deliverMessage', session: msg.sessionKey, messageId: msg.messageId, payload: msg.payload, ownershipEpoch:
|
|
26547
|
+
const res = await meshClient.send({ machineId, url }, { type: 'deliverMessage', session: msg.sessionKey, messageId: msg.messageId, payload: msg.payload, ownershipEpoch, ...(msg.senderEnvelope ? { senderEnvelope: msg.senderEnvelope } : {}) }, ownershipEpoch);
|
|
26548
|
+
if (res.ok && res.result && typeof res.result === 'object' && 'accepted' in res.result) {
|
|
26549
|
+
return res.result;
|
|
26550
|
+
}
|
|
26551
|
+
throw new Error(`spawn deliverMessage rejected: ${res.status} ${res.reason ?? ''}`);
|
|
26552
|
+
},
|
|
26553
|
+
// Take back a placement whose spawn never landed (see SessionRouter):
|
|
26554
|
+
// only the still-placing record this router put on `placedOn`.
|
|
26555
|
+
reclaimPlacement: (sk, placedOn) => {
|
|
26556
|
+
const cur = ownReg.read(sk);
|
|
26557
|
+
if (!cur || cur.status !== 'placing' || cur.ownerMachineId !== placedOn)
|
|
26558
|
+
return false;
|
|
26559
|
+
const r = ownReg.cas({ type: 'force-claim', machineId: meshSelfId }, { sessionKey: sk, sender: meshSelfId, nonce: `${meshSelfId}:rc:${++routerNonce}` });
|
|
26560
|
+
emitPlacement(sk, r, 'failover', placedOn);
|
|
26561
|
+
return r.ok;
|
|
26544
26562
|
},
|
|
26545
26563
|
handleLocally: async () => { },
|
|
26546
26564
|
// Durable Inbound Message Queue §2.2: tri-state custody taking.
|