neoctl 0.1.7 → 0.1.9
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 +29 -1
- package/dist/core/query-engine.d.ts +2 -0
- package/dist/core/query-engine.js +20 -0
- package/dist/core/query-engine.js.map +1 -1
- package/dist/model/context-window.js +1 -0
- package/dist/model/context-window.js.map +1 -1
- package/dist/repl/commands.d.ts +8 -0
- package/dist/repl/commands.js +45 -0
- package/dist/repl/commands.js.map +1 -1
- package/dist/repl/index.js +229 -73
- package/dist/repl/index.js.map +1 -1
- package/dist/web/html.js +183 -39
- package/dist/web/html.js.map +1 -1
- package/dist/web/index.js +327 -38
- package/dist/web/index.js.map +1 -1
- package/package.json +4 -1
- package/scripts/build-standalone.mjs +139 -0
package/dist/web/html.js
CHANGED
|
@@ -29,8 +29,10 @@ export const WEB_HTML = String.raw `<!doctype html>
|
|
|
29
29
|
body { background: radial-gradient(circle at top, #101522 0, var(--bg) 42rem); color: var(--text); font: 14px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; }
|
|
30
30
|
#app { height: 100%; display: flex; flex-direction: column; }
|
|
31
31
|
.topbar { height: 34px; display: flex; align-items: center; gap: 12px; padding: 0 var(--topbar-gutter); border-bottom: 1px solid var(--line); color: var(--muted); background: rgba(7, 8, 11, .75); backdrop-filter: blur(12px); }
|
|
32
|
-
.brand { color: var(--cyan); font-weight: 700; letter-spacing: .08em; }
|
|
33
|
-
.
|
|
32
|
+
.brand { color: var(--cyan); font-weight: 700; letter-spacing: .08em; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; border: 0; background: transparent; padding: 0; font: inherit; cursor: pointer; text-align: left; }
|
|
33
|
+
.brand:hover, .brand:focus-visible { color: #67e8f9; text-decoration: underline; text-underline-offset: 3px; outline: none; }
|
|
34
|
+
.brand.session-title { letter-spacing: 0; }
|
|
35
|
+
#connection { flex: 0 0 auto; color: var(--yellow); }
|
|
34
36
|
#transcriptWrap { position: relative; flex: 1; min-height: 0; }
|
|
35
37
|
#transcript { height: 100%; overflow: auto; padding: 22px var(--page-gutter) 10px; scroll-behavior: smooth; }
|
|
36
38
|
.scroll-bottom-zone { position: absolute; left: 0; right: 0; bottom: 0; height: 22px; padding: 0 var(--page-gutter); display: flex; align-items: flex-end; opacity: 0; pointer-events: none; transition: opacity .14s ease; z-index: 2; }
|
|
@@ -93,29 +95,68 @@ export const WEB_HTML = String.raw `<!doctype html>
|
|
|
93
95
|
.live .marker { animation: pulse 900ms ease-in-out infinite; }
|
|
94
96
|
@keyframes pulse { 50% { opacity: .35; } }
|
|
95
97
|
.ansi { color: #d1d5db; }
|
|
96
|
-
|
|
98
|
+
.diff { color: #d1d5db; }
|
|
99
|
+
.diff-line { display: block; }
|
|
100
|
+
.diff-add { color: var(--green); }
|
|
101
|
+
.diff-del { color: var(--red); }
|
|
102
|
+
.diff-hunk { color: var(--cyan); }
|
|
103
|
+
.diff-meta { color: var(--muted); }
|
|
104
|
+
#status { flex: 0 0 auto; min-height: 28px; padding: 4px var(--page-gutter); color: var(--muted); border-top: 1px solid var(--line); display: flex; flex-direction: column; align-items: stretch; gap: 2px; overflow: hidden; white-space: nowrap; }
|
|
105
|
+
.status-main, .status-bg-row { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
106
|
+
.status-bg-row { color: var(--yellow); font-size: 12px; }
|
|
97
107
|
.phase { font-weight: 700; color: var(--green); }
|
|
98
108
|
.phase.active { color: var(--cyan); text-shadow: 0 0 12px currentColor; animation: shimmer 1.35s linear infinite; }
|
|
99
109
|
.phase.thinking { color: var(--purple); }
|
|
100
110
|
.phase.tools { color: var(--gold); }
|
|
101
111
|
.phase.stopped { color: var(--yellow); }
|
|
102
112
|
.sep { color: var(--muted); padding: 0 7px; }
|
|
103
|
-
.ctx-stat { word-spacing: .35em; }
|
|
104
|
-
.ctx-stat span { word-spacing: normal; }
|
|
105
113
|
.token-hot { font-weight: 700; }
|
|
106
114
|
.token-input-hot { color: var(--green); }
|
|
107
115
|
.token-output-hot { color: var(--cyan); }
|
|
108
116
|
.token-error-hot { color: var(--red); }
|
|
109
117
|
@keyframes shimmer { 0%, 100% { filter: brightness(.9); } 45% { filter: brightness(1.9); } }
|
|
110
118
|
#queued { display: none; padding: 0 var(--page-gutter) 4px; color: var(--yellow); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
111
|
-
#panel { display: none; flex: 0 0 auto; padding:
|
|
119
|
+
#panel { display: none; flex: 0 0 auto; padding: 12px var(--page-gutter); border-top: 1px solid var(--line); background: rgba(7, 8, 11, .97); color: var(--muted); max-height: min(58vh, 560px); overflow: auto; }
|
|
120
|
+
#app.sessions-page #transcriptWrap, #app.sessions-page #status, #app.sessions-page #queued, #app.sessions-page #composerWrap { display: none; }
|
|
121
|
+
#app.sessions-page #panel { flex: 1 1 auto; max-height: none; border-top: 0; padding-top: 18px; }
|
|
122
|
+
#app.sessions-page .topbar { display: none; }
|
|
112
123
|
#panel.open { display: block; }
|
|
113
124
|
.panel-title { color: var(--cyan); font-weight: 700; margin-bottom: 6px; }
|
|
114
|
-
.panel-
|
|
115
|
-
.panel-
|
|
125
|
+
.panel-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; margin-bottom: 10px; }
|
|
126
|
+
.panel-subtitle { color: var(--muted); font-size: 12px; margin-top: 2px; }
|
|
127
|
+
.session-list { display: grid; gap: 8px; }
|
|
128
|
+
.session-card { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 10px; padding: 10px 12px; border: 1px solid #1f2937; border-radius: 12px; background: linear-gradient(180deg, rgba(15, 23, 42, .72), rgba(11, 13, 18, .82)); color: var(--text); cursor: pointer; }
|
|
129
|
+
.session-card.selected { border-color: rgba(34, 211, 238, .78); box-shadow: 0 0 0 1px rgba(34, 211, 238, .18), 0 0 22px rgba(34, 211, 238, .12); }
|
|
130
|
+
.session-card.running { border-color: rgba(34, 197, 94, .58); }
|
|
131
|
+
.session-card.current { background: linear-gradient(180deg, rgba(8, 47, 73, .56), rgba(15, 23, 42, .82)); }
|
|
132
|
+
.session-main { min-width: 0; }
|
|
133
|
+
.session-title-line { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
134
|
+
.session-name { font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
135
|
+
.session-badges { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 6px; }
|
|
136
|
+
.session-badge { border: 1px solid #263043; border-radius: 999px; padding: 1px 7px; color: var(--muted); font-size: 11px; line-height: 17px; background: rgba(15, 23, 42, .72); }
|
|
137
|
+
.session-badge.running { color: #bbf7d0; border-color: rgba(34, 197, 94, .45); background: rgba(22, 101, 52, .22); }
|
|
138
|
+
.session-badge.current { color: #a5f3fc; border-color: rgba(34, 211, 238, .45); background: rgba(8, 145, 178, .16); }
|
|
139
|
+
.session-meta { margin-top: 7px; color: var(--muted); font-size: 12px; overflow-wrap: anywhere; }
|
|
140
|
+
.session-actions { display: flex; align-items: center; gap: 6px; }
|
|
116
141
|
.panel-muted { color: var(--muted); }
|
|
117
|
-
.panel-
|
|
118
|
-
.panel-actions button
|
|
142
|
+
.panel-toolbar { display: flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
|
|
143
|
+
.panel-actions button, .login-actions button, .panel-close, .panel-primary { border: 1px solid #263043; border-radius: 999px; background: rgba(15, 23, 42, .92); color: var(--muted); font: inherit; font-size: 11px; cursor: pointer; min-height: 24px; padding: 2px 9px; }
|
|
144
|
+
.panel-actions button:disabled, .login-actions button:disabled, .panel-close:disabled, .panel-primary:disabled { opacity: .45; cursor: not-allowed; box-shadow: none; }
|
|
145
|
+
.panel-primary { width: 34px; height: 34px; min-height: 34px; padding: 0; border-radius: 50%; color: #020617; border-color: rgba(34, 211, 238, .72); background: linear-gradient(90deg, var(--cyan), #67e8f9); font-weight: 700; font-size: 22px; line-height: 30px; }
|
|
146
|
+
.panel-actions button:hover, .login-actions button:hover, .panel-close:hover, .panel-primary:hover { color: var(--cyan); border-color: #31556b; box-shadow: 0 0 14px rgba(34, 211, 238, .18); }
|
|
147
|
+
.panel-primary:hover { color: #020617; }
|
|
148
|
+
.panel-actions button.danger:hover { color: var(--red); border-color: rgba(239, 68, 68, .5); box-shadow: 0 0 14px rgba(239, 68, 68, .14); }
|
|
149
|
+
@media (max-width: 640px) {
|
|
150
|
+
:root { --page-gutter: 12px; --topbar-gutter: 12px; }
|
|
151
|
+
.topbar { height: 40px; }
|
|
152
|
+
#panel { max-height: 72vh; padding-top: 10px; }
|
|
153
|
+
.panel-header { align-items: center; }
|
|
154
|
+
.session-card { grid-template-columns: 1fr; padding: 12px; }
|
|
155
|
+
.session-actions { justify-content: stretch; }
|
|
156
|
+
.session-actions .panel-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; width: 100%; }
|
|
157
|
+
.session-actions button { margin: 0; min-height: 36px; }
|
|
158
|
+
.session-title-line { align-items: flex-start; }
|
|
159
|
+
}
|
|
119
160
|
.login-grid { display: grid; grid-template-columns: minmax(12ch, 22ch) 1fr; gap: 6px 10px; align-items: center; }
|
|
120
161
|
.login-grid label { color: var(--muted); }
|
|
121
162
|
.login-grid input, .login-grid select { min-width: 0; border: 1px solid #263043; border-radius: 6px; padding: 4px 6px; background: #0b1020; color: var(--text); font: inherit; }
|
|
@@ -139,7 +180,7 @@ export const WEB_HTML = String.raw `<!doctype html>
|
|
|
139
180
|
</head>
|
|
140
181
|
<body>
|
|
141
182
|
<div id="app">
|
|
142
|
-
<div class="topbar"><
|
|
183
|
+
<div class="topbar"><button id="brand" class="brand" type="button" title="Open sessions">neo web</button><span id="connection" hidden>connecting…</span></div>
|
|
143
184
|
<div id="transcriptWrap"><div id="transcript"></div><div id="scrollBottomZone" class="scroll-bottom-zone"><button id="scrollBottom" type="button" aria-label="Scroll to bottom">bottom</button></div></div>
|
|
144
185
|
<div id="status"></div>
|
|
145
186
|
<div id="queued"></div>
|
|
@@ -159,7 +200,7 @@ const ANIMATED_NUMBER_INTERVAL_MS = 50;
|
|
|
159
200
|
const ANIMATED_NUMBER_MIN_DURATION_MS = 180;
|
|
160
201
|
const ANIMATED_NUMBER_MAX_DURATION_MS = 700;
|
|
161
202
|
const ANIMATED_NUMBER_DURATION_SCALE_MS = 130;
|
|
162
|
-
const state = { lines: [], status: { phase: 'ready', streamedOutputTokens: 0 }, busy: false, queuedInput: undefined, backgroundTaskCount: 0, session: undefined, catalog: { commands: [], modelIds: [], reasoning: [] }, interactive: {}, tips: [], tipIndex: 0, history: [], historyIndex: undefined, completionIndex: 0, expandedToolLines: new Set(), panel: undefined, panelSelection: 0, attachments: [], attachmentCounter: 0 };
|
|
203
|
+
const state = { lines: [], status: { phase: 'ready', streamedOutputTokens: 0 }, busy: false, queuedInput: undefined, backgroundTaskCount: 0, backgroundTasks: [], backgroundSessionRunCount: 0, runningSessionIds: [], session: undefined, catalog: { commands: [], modelIds: [], reasoning: [] }, interactive: {}, tips: [], tipIndex: 0, history: [], historyIndex: undefined, completionIndex: 0, expandedToolLines: new Set(), panel: undefined, panelSelection: 0, attachments: [], attachmentCounter: 0, view: location.pathname === '/sessions' ? 'sessions' : 'chat' };
|
|
163
204
|
const animatedNumbers = { input: { target: undefined, display: undefined, timer: undefined }, output: { target: undefined, display: undefined, timer: undefined } };
|
|
164
205
|
const renderedLineKeys = new Map();
|
|
165
206
|
const statusNodes = {};
|
|
@@ -173,12 +214,20 @@ const queuedEl = document.getElementById('queued');
|
|
|
173
214
|
const panelEl = document.getElementById('panel');
|
|
174
215
|
const input = document.getElementById('input');
|
|
175
216
|
const completionsEl = document.getElementById('completions');
|
|
217
|
+
const brand = document.getElementById('brand');
|
|
176
218
|
const connection = document.getElementById('connection');
|
|
177
|
-
const pageTitle = document.getElementById('pageTitle');
|
|
178
219
|
|
|
220
|
+
const sessionsPage = () => state.view === 'sessions';
|
|
221
|
+
const openSessionsOnLoad = state.view === 'sessions';
|
|
179
222
|
const es = new EventSource('/events');
|
|
180
|
-
es.addEventListener('open', () =>
|
|
181
|
-
|
|
223
|
+
es.addEventListener('open', () => {
|
|
224
|
+
connection.hidden = true;
|
|
225
|
+
connection.textContent = '';
|
|
226
|
+
});
|
|
227
|
+
es.addEventListener('error', () => {
|
|
228
|
+
connection.hidden = false;
|
|
229
|
+
connection.textContent = 'reconnecting…';
|
|
230
|
+
});
|
|
182
231
|
es.addEventListener('sync', (event) => {
|
|
183
232
|
const payload = JSON.parse(event.data);
|
|
184
233
|
state.lines = payload.lines || [];
|
|
@@ -186,6 +235,9 @@ es.addEventListener('sync', (event) => {
|
|
|
186
235
|
state.busy = !!payload.busy;
|
|
187
236
|
state.queuedInput = payload.queuedInput;
|
|
188
237
|
state.backgroundTaskCount = payload.backgroundTaskCount || 0;
|
|
238
|
+
state.backgroundTasks = payload.backgroundTasks || [];
|
|
239
|
+
state.backgroundSessionRunCount = payload.backgroundSessionRunCount || 0;
|
|
240
|
+
state.runningSessionIds = payload.runningSessionIds || state.runningSessionIds || [];
|
|
189
241
|
state.session = payload.session;
|
|
190
242
|
if (payload.catalog) state.catalog = payload.catalog;
|
|
191
243
|
if (payload.interactive) state.interactive = payload.interactive;
|
|
@@ -263,7 +315,7 @@ function markerForLine(line, kind) {
|
|
|
263
315
|
return '●';
|
|
264
316
|
}
|
|
265
317
|
function shouldRenderMarkdown(line) {
|
|
266
|
-
if (line.format === 'ansi' || line.format === 'plain') return false;
|
|
318
|
+
if (line.format === 'ansi' || line.format === 'plain' || line.format === 'diff') return false;
|
|
267
319
|
return line.kind === 'assistant' || line.kind === 'thinking' || line.kind === 'system' || line.kind === 'tool';
|
|
268
320
|
}
|
|
269
321
|
function hasMoreThanLines(text, maxLines) {
|
|
@@ -276,9 +328,26 @@ function hasMoreThanLines(text, maxLines) {
|
|
|
276
328
|
}
|
|
277
329
|
function renderText(text, format, markdown) {
|
|
278
330
|
if (format === 'ansi') return '<span class="ansi">' + esc(stripAnsi(text)) + '</span>';
|
|
331
|
+
if (format === 'diff') return renderDiffText(text);
|
|
279
332
|
if (!markdown) return linkify(esc(text));
|
|
280
333
|
return sanitizeMarkdownHtml(marked.parse(text || ''));
|
|
281
334
|
}
|
|
335
|
+
function renderDiffText(text) {
|
|
336
|
+
const lines = String(text || '').split('\n');
|
|
337
|
+
return '<span class="diff">' + lines.map((line) => {
|
|
338
|
+
const cls = diffLineClass(line);
|
|
339
|
+
return '<span class="diff-line ' + cls + '">' + esc(line) + '</span>';
|
|
340
|
+
}).join('') + '</span>';
|
|
341
|
+
}
|
|
342
|
+
function diffLineClass(line) {
|
|
343
|
+
const pipeIndex = line.indexOf('│ ');
|
|
344
|
+
const diffMarker = pipeIndex >= 0 ? line.slice(pipeIndex + 2, pipeIndex + 3) : line.slice(0, 1);
|
|
345
|
+
if (line.startsWith('@@')) return 'diff-hunk';
|
|
346
|
+
if (line.startsWith('--- ') || line.startsWith('+++ ') || line.startsWith('create ') || line.startsWith('edit ') || line.startsWith('write ') || line.startsWith('failed ') || line === 'no changes') return 'diff-meta';
|
|
347
|
+
if (diffMarker === '+') return 'diff-add';
|
|
348
|
+
if (diffMarker === '-') return 'diff-del';
|
|
349
|
+
return 'diff-meta';
|
|
350
|
+
}
|
|
282
351
|
function renderStatus() {
|
|
283
352
|
ensureStatusNodes();
|
|
284
353
|
const s = state.status || {};
|
|
@@ -296,7 +365,6 @@ function renderStatus() {
|
|
|
296
365
|
setText(statusNodes.ctxPercent, ctx.percent);
|
|
297
366
|
const ctxColor = contextColor(s.metrics);
|
|
298
367
|
if (statusNodes.ctxPercent.style.color !== ctxColor) statusNodes.ctxPercent.style.color = ctxColor;
|
|
299
|
-
setText(statusNodes.ctxLimit, ctx.limit);
|
|
300
368
|
const now = Date.now();
|
|
301
369
|
const retryPending = retryCooldownActive(s, now);
|
|
302
370
|
const inputArrowClass = retryPending ? 'token-hot token-error-hot' : tokenArrowHotClass(s.inputTokenUpdatedAt, now, 'token-input-hot');
|
|
@@ -305,14 +373,29 @@ function renderStatus() {
|
|
|
305
373
|
const outputArrowClass = modelOutputPending(s, now) ? '' : tokenArrowHotClass(s.outputTokenUpdatedAt, now, 'token-output-hot');
|
|
306
374
|
if (statusNodes.outputArrow.className !== outputArrowClass) statusNodes.outputArrow.className = outputArrowClass;
|
|
307
375
|
setText(statusNodes.outputTokens, outputTokens);
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
376
|
+
renderBackgroundTasks();
|
|
377
|
+
}
|
|
378
|
+
function renderBackgroundTasks() {
|
|
379
|
+
const tasks = state.backgroundTasks || [];
|
|
380
|
+
const rows = statusNodes.backgroundRows;
|
|
381
|
+
if (!rows) return;
|
|
382
|
+
rows.innerHTML = '';
|
|
383
|
+
if (!tasks.length) { rows.style.display = 'none'; return; }
|
|
384
|
+
rows.style.display = '';
|
|
385
|
+
const summary = document.createElement('div');
|
|
386
|
+
summary.className = 'status-bg-row';
|
|
387
|
+
summary.textContent = '◇ background tools: ' + tasks.length + ' task' + (tasks.length === 1 ? '' : 's');
|
|
388
|
+
rows.appendChild(summary);
|
|
389
|
+
for (const task of tasks.slice(0, 2)) {
|
|
390
|
+
const row = document.createElement('div');
|
|
391
|
+
row.className = 'status-bg-row';
|
|
392
|
+
row.textContent = ' ' + task.type + ':' + truncateMiddle(task.description || task.agentId || task.taskId, Math.max(12, Math.floor(window.innerWidth / 18))) + ' · ' + task.status + ' · ' + formatElapsed(Date.now() - Date.parse(task.createdAt || new Date().toISOString()));
|
|
393
|
+
rows.appendChild(row);
|
|
394
|
+
}
|
|
312
395
|
}
|
|
313
396
|
function ensureStatusNodes() {
|
|
314
397
|
if (statusNodes.phase) return;
|
|
315
|
-
statusEl.innerHTML = '<span data-part="phase"></span><span class="sep">·</span><span data-part="model"></span><span class="sep">·</span><span
|
|
398
|
+
statusEl.innerHTML = '<div class="status-main"><span data-part="phase"></span><span class="sep">·</span><span data-part="model"></span><span class="sep">·</span><span data-part="ctxPercent"></span><span class="sep">·</span><span data-part="inputArrow">↑</span> <span data-part="inputTokens"></span><span class="sep">·</span><span data-part="outputArrow">↓</span> <span data-part="outputTokens"></span></div><div data-part="backgroundRows"></div>';
|
|
316
399
|
for (const node of statusEl.querySelectorAll('[data-part]')) statusNodes[node.getAttribute('data-part')] = node;
|
|
317
400
|
}
|
|
318
401
|
function minimumDisplayPhase(target) {
|
|
@@ -351,10 +434,13 @@ function renderTitle() {
|
|
|
351
434
|
const prefix = isActivePhase((state.status || {}).phase) || state.backgroundTaskCount > 0 ? '● ' : '✓ ';
|
|
352
435
|
if (title) {
|
|
353
436
|
const value = prefix + title;
|
|
354
|
-
setText(
|
|
437
|
+
setText(brand, value);
|
|
438
|
+
brand.classList.add('session-title');
|
|
355
439
|
if (document.title !== value) document.title = value;
|
|
356
440
|
} else {
|
|
357
|
-
setText(
|
|
441
|
+
setText(brand, 'neo web');
|
|
442
|
+
brand.classList.remove('session-title');
|
|
443
|
+
if (document.title !== 'neo web') document.title = 'neo web';
|
|
358
444
|
}
|
|
359
445
|
}
|
|
360
446
|
function sessionDisplayTitle(session) {
|
|
@@ -418,28 +504,65 @@ function renderQueued() {
|
|
|
418
504
|
setText(queuedEl, 'queued next: ' + state.queuedInput.replace(/\s+/g, ' ').trim() + ' (Esc/Ctrl+C to clear)');
|
|
419
505
|
}
|
|
420
506
|
function renderPanel() {
|
|
507
|
+
document.getElementById('app').classList.toggle('sessions-page', sessionsPage());
|
|
421
508
|
if (!state.panel) { panelEl.className = ''; panelEl.innerHTML = ''; return; }
|
|
422
509
|
panelEl.className = 'open';
|
|
423
510
|
if (state.panel === 'sessions') renderSessionsPanel();
|
|
424
511
|
else if (state.panel === 'login') renderLoginPanel();
|
|
425
512
|
}
|
|
426
513
|
async function openSessionsPanel() {
|
|
514
|
+
state.view = 'sessions';
|
|
427
515
|
state.panel = 'sessions';
|
|
428
516
|
state.panelSelection = 0;
|
|
517
|
+
history.replaceState(null, '', '/sessions');
|
|
518
|
+
renderPanel();
|
|
429
519
|
panelEl.className = 'open';
|
|
430
|
-
panelEl.innerHTML = '<div class="panel-title">
|
|
520
|
+
panelEl.innerHTML = '<div class="panel-title">Sessions</div><div class="panel-muted">loading…</div>';
|
|
431
521
|
const res = await fetch('/api/sessions');
|
|
432
522
|
const body = await res.json();
|
|
433
523
|
state.sessions = body.sessions || [];
|
|
524
|
+
state.runningSessionIds = body.runningSessionIds || [];
|
|
434
525
|
renderPanel();
|
|
435
526
|
}
|
|
436
527
|
function renderSessionsPanel() {
|
|
437
528
|
const sessions = state.sessions || [];
|
|
438
529
|
const selected = Math.max(0, Math.min(state.panelSelection, sessions.length - 1));
|
|
439
530
|
state.panelSelection = selected;
|
|
440
|
-
|
|
441
|
-
|
|
531
|
+
const currentSessionId = state.session && state.session.sessionId;
|
|
532
|
+
const header = '<div class="panel-header"><div><div class="panel-title">Sessions</div><div class="panel-subtitle">Manage saved sessions.</div></div><div class="panel-toolbar"><button class="panel-primary" data-action="new-session" title="New session" aria-label="New session">+</button></div></div>';
|
|
533
|
+
const body = sessions.length ? '<div class="session-list">' + sessions.map((s, i) => renderSessionCard(s, i, selected, currentSessionId)).join('') + '</div>' : '<div class="panel-muted">No saved sessions found. Tap + to start a new session.</div>';
|
|
534
|
+
panelEl.innerHTML = header + body + '<div class="panel-muted" style="margin-top:8px">↑/↓ select · Enter enter · Delete remove</div>';
|
|
535
|
+
}
|
|
536
|
+
function renderSessionCard(s, i, selected, currentSessionId) {
|
|
537
|
+
const isCurrent = s.sessionId === currentSessionId;
|
|
538
|
+
const isRunning = (state.runningSessionIds || []).includes(s.sessionId) || (isCurrent && (state.busy || isActivePhase((state.status || {}).phase)));
|
|
539
|
+
const badges = [
|
|
540
|
+
isRunning ? '<span class="session-badge running">● running</span>' : '',
|
|
541
|
+
isCurrent ? '<span class="session-badge current">current</span>' : '',
|
|
542
|
+
'<span class="session-badge">' + esc(s.messages) + ' messages</span>',
|
|
543
|
+
].filter(Boolean).join('');
|
|
544
|
+
const classes = ['session-card', i === selected ? 'selected' : '', isCurrent ? 'current' : '', isRunning ? 'running' : ''].filter(Boolean).join(' ');
|
|
545
|
+
return '<div class="' + classes + '" data-session-index="' + i + '"><div class="session-main"><div class="session-title-line"><span class="session-name">' + esc(s.title || '(untitled)') + '</span></div><div class="session-badges">' + badges + '</div><div class="session-meta">' + esc(truncateMiddle(s.sessionId, 28)) + ' · updated ' + esc(s.updatedAt || 'unknown') + '</div></div><div class="session-actions"><span class="panel-actions"><button data-action="enter" data-session-id="' + esc(s.sessionId) + '">enter</button><button class="danger" data-action="delete" data-session-id="' + esc(s.sessionId) + '">delete</button></span></div></div>';
|
|
546
|
+
}
|
|
547
|
+
function showChatView() {
|
|
548
|
+
state.view = 'chat';
|
|
549
|
+
state.panel = undefined;
|
|
550
|
+
history.replaceState(null, '', '/');
|
|
551
|
+
renderPanel();
|
|
552
|
+
input.focus();
|
|
553
|
+
}
|
|
554
|
+
async function enterSession(sessionId) {
|
|
555
|
+
const result = await postJson('/api/sessions/resume', { sessionId });
|
|
556
|
+
if (result.ok) showChatView();
|
|
557
|
+
}
|
|
558
|
+
async function createAndEnterSession() {
|
|
559
|
+
const result = await postJson('/api/sessions/new', {});
|
|
560
|
+
if (result.ok) showChatView();
|
|
561
|
+
}
|
|
442
562
|
async function openSessionsPanelAfterDelete(sessionId) {
|
|
563
|
+
const session = (state.sessions || []).find(s => s.sessionId === sessionId);
|
|
564
|
+
const label = session ? (session.title || session.sessionId) : sessionId;
|
|
565
|
+
if (!confirm('Delete session "' + label + '"? This cannot be undone.')) return;
|
|
443
566
|
await postJson('/api/sessions/delete', { sessionId });
|
|
444
567
|
await openSessionsPanel();
|
|
445
568
|
}
|
|
@@ -538,6 +661,15 @@ async function submit() {
|
|
|
538
661
|
const text = input.value;
|
|
539
662
|
if (text.trim() === '/sessions') { input.value = ''; autosize(); renderCompletions(); await openSessionsPanel(); return; }
|
|
540
663
|
if (text.trim() === '/login') { input.value = ''; autosize(); renderCompletions(); await openLoginPanel(); return; }
|
|
664
|
+
if (text.trim() === '/new') {
|
|
665
|
+
input.value = '';
|
|
666
|
+
state.attachments = [];
|
|
667
|
+
autosize();
|
|
668
|
+
renderCompletions();
|
|
669
|
+
const result = await postJson('/api/sessions/new', {});
|
|
670
|
+
if (!result.ok && result.error) alert(result.error);
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
541
673
|
const attachments = attachmentsForText(text);
|
|
542
674
|
if (!text.trim() && attachments.length === 0) return;
|
|
543
675
|
state.history = [text].concat(state.history.filter(x => x !== text)).slice(0, 100);
|
|
@@ -552,12 +684,14 @@ async function submit() {
|
|
|
552
684
|
}
|
|
553
685
|
transcript.addEventListener('scroll', updateScrollBottomAffordance, { passive: true });
|
|
554
686
|
scrollBottom.addEventListener('click', () => { transcript.scrollTo({ top: transcript.scrollHeight, behavior: 'smooth' }); updateScrollBottomAffordance(); });
|
|
687
|
+
brand.addEventListener('click', () => { void openSessionsPanel(); });
|
|
555
688
|
panelEl.addEventListener('click', async (e) => {
|
|
556
689
|
const button = e.target.closest('button');
|
|
557
690
|
if (!button) return;
|
|
558
691
|
const action = button.getAttribute('data-action');
|
|
559
692
|
if (action === 'panel-close') { state.panel = undefined; renderPanel(); input.focus(); return; }
|
|
560
|
-
if (action === '
|
|
693
|
+
if (action === 'new-session') { await createAndEnterSession(); return; }
|
|
694
|
+
if (action === 'enter') { await enterSession(button.getAttribute('data-session-id')); return; }
|
|
561
695
|
if (action === 'delete') { await openSessionsPanelAfterDelete(button.getAttribute('data-session-id')); return; }
|
|
562
696
|
if (action === 'login-save') { await saveLoginPanel(); return; }
|
|
563
697
|
});
|
|
@@ -582,16 +716,19 @@ transcript.addEventListener('click', (e) => {
|
|
|
582
716
|
renderedLineKeys.set(String(id), lineRenderKey(line));
|
|
583
717
|
}
|
|
584
718
|
});
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
719
|
+
function handleSessionsKey(e) {
|
|
720
|
+
if (state.panel !== 'sessions') return false;
|
|
721
|
+
const countSessions = (state.sessions || []).length;
|
|
722
|
+
if (e.key === 'Escape') { e.preventDefault(); showChatView(); return true; }
|
|
723
|
+
if (e.key === 'ArrowUp' && countSessions) { e.preventDefault(); state.panelSelection = (state.panelSelection + countSessions - 1) % countSessions; renderPanel(); return true; }
|
|
724
|
+
if (e.key === 'ArrowDown' && countSessions) { e.preventDefault(); state.panelSelection = (state.panelSelection + 1) % countSessions; renderPanel(); return true; }
|
|
725
|
+
if (e.key === 'Enter' && countSessions) { e.preventDefault(); const s = state.sessions[state.panelSelection]; if (s) void enterSession(s.sessionId); return true; }
|
|
726
|
+
if ((e.key === 'Delete' || e.key === 'Backspace') && countSessions) { e.preventDefault(); const s = state.sessions[state.panelSelection]; if (s) openSessionsPanelAfterDelete(s.sessionId); return true; }
|
|
727
|
+
return false;
|
|
728
|
+
}
|
|
729
|
+
input.addEventListener('keydown', (e) => {
|
|
730
|
+
const count = completions().length;
|
|
731
|
+
if (handleSessionsKey(e)) return;
|
|
595
732
|
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); const c = selectedCompletion(); if (c && c.kind === 'command' && c.arguments !== 'none') { completeSelection(); input.value += ' '; input.selectionStart = input.selectionEnd = input.value.length; return; } submit(); return; }
|
|
596
733
|
if (e.key === 'Tab') { if (completeSelection()) e.preventDefault(); else if (!input.value) { e.preventDefault(); advanceTip(); } return; }
|
|
597
734
|
if (e.key === 'ArrowUp' && count) { e.preventDefault(); state.completionIndex = (state.completionIndex + count - 1) % count; renderCompletions(); return; }
|
|
@@ -605,14 +742,19 @@ input.addEventListener('keydown', (e) => {
|
|
|
605
742
|
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'c') { if (input.value) { input.value = ''; autosize(); renderCompletions(); } else fetch('/api/interrupt', { method: 'POST' }); }
|
|
606
743
|
if (e.key === 'Escape') { state.completionIndex = 0; if (state.queuedInput) fetch('/api/interrupt', { method: 'POST' }); else renderCompletions(); }
|
|
607
744
|
});
|
|
745
|
+
document.addEventListener('keydown', (e) => {
|
|
746
|
+
if (e.target === input || e.target.closest('input, textarea, select')) return;
|
|
747
|
+
handleSessionsKey(e);
|
|
748
|
+
});
|
|
608
749
|
input.addEventListener('input', () => { state.completionIndex = 0; state.attachments = attachmentsForText(input.value); advanceTip(); autosize(); renderCompletions(); });
|
|
609
750
|
input.addEventListener('paste', (e) => { void handlePaste(e); });
|
|
610
751
|
function autosize() { input.style.height = 'auto'; input.style.height = Math.min(input.scrollHeight, window.innerHeight * .35) + 'px'; updateScrollBottomAffordance(); }
|
|
611
752
|
function phaseLabel(phase) { if (phase === 'calling_model') return 'model'; if (phase === 'thinking') return 'think'; if (phase === 'running_tools') return 'tools'; if (phase === 'injecting_context') return 'context'; return phase || 'ready'; }
|
|
612
753
|
function isActivePhase(phase) { return ['running', 'preparing', 'calling_model', 'thinking', 'running_tools', 'compacting', 'injecting_context'].includes(phase); }
|
|
613
|
-
function contextParts(metrics) { if (!metrics) return {
|
|
754
|
+
function contextParts(metrics) { if (!metrics) return { percent: '?' }; return { percent: metrics.contextUsageRatio === undefined ? '?' : (metrics.contextUsageRatio * 100).toFixed(1) + '%' }; }
|
|
614
755
|
function contextColor(metrics) { const r = metrics && metrics.contextUsageRatio; if (r === undefined) return 'var(--muted)'; if (r >= .9) return 'var(--red)'; if (r >= .75) return 'var(--yellow)'; return 'var(--muted)'; }
|
|
615
756
|
function compactNumber(value) { if (value === undefined || value === null) return '?'; const n = Math.max(0, Math.round(value)); if (n >= 1000000) return trimFixed(n / 1000000) + 'm'; if (n >= 10000) return Math.round(n / 1000) + 'k'; if (n >= 1000) return trimFixed(n / 1000) + 'k'; return String(n); }
|
|
757
|
+
function formatElapsed(ms) { const seconds = Math.max(0, Math.floor(ms / 1000)); if (seconds < 60) return seconds + 's'; const minutes = Math.floor(seconds / 60); const rem = String(seconds % 60).padStart(2, '0'); if (minutes < 60) return minutes + 'm' + rem + 's'; return Math.floor(minutes / 60) + 'h' + String(minutes % 60).padStart(2, '0') + 'm'; }
|
|
616
758
|
function trimFixed(v) { return v >= 10 ? v.toFixed(0) : v.toFixed(1).replace(/\.0$/, ''); }
|
|
617
759
|
function truncateMiddle(value, max) { value = String(value); if (value.length <= max) return value; if (max <= 3) return value.slice(0, max); const l = Math.ceil((max - 3) / 2), r = Math.floor((max - 3) / 2); return value.slice(0, l) + '...' + value.slice(value.length - r); }
|
|
618
760
|
function stripAnsi(value) { return String(value).replace(/\x1b\[[0-9;]*m/g, ''); }
|
|
@@ -689,8 +831,10 @@ function safeHref(value) {
|
|
|
689
831
|
}
|
|
690
832
|
function esc(value) { return String(value).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); }
|
|
691
833
|
function linkify(value) { return value.replace(/(https?:\/\/[^\s<]+)/g, '<a style="color:var(--cyan)" target="_blank" href="$1">$1</a>'); }
|
|
834
|
+
document.getElementById('app').classList.toggle('sessions-page', sessionsPage());
|
|
692
835
|
updateInputPlaceholder();
|
|
693
836
|
autosize();
|
|
837
|
+
if (openSessionsOnLoad) void openSessionsPanel();
|
|
694
838
|
</script>
|
|
695
839
|
</body>
|
|
696
840
|
</html>`;
|
package/dist/web/html.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"html.js","sourceRoot":"","sources":["../../src/web/html.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAA
|
|
1
|
+
{"version":3,"file":"html.js","sourceRoot":"","sources":["../../src/web/html.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QAu0B1B,CAAC"}
|