@atolis-hq/wake 0.2.19 → 0.2.20
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.
|
@@ -60,10 +60,27 @@ export const indexHtml = `<!DOCTYPE html>
|
|
|
60
60
|
th, td { text-align: left; padding: 0.35rem 0.5rem; border-bottom: 1px solid #2c313a; }
|
|
61
61
|
th { color: #9aa2ad; font-weight: 600; }
|
|
62
62
|
pre { background: #1a1d23; padding: 0.75rem; border-radius: 6px; overflow: auto; font-size: 0.75rem; }
|
|
63
|
-
.
|
|
64
|
-
.
|
|
65
|
-
.
|
|
66
|
-
.
|
|
63
|
+
.modal-overlay { position: fixed; inset: 0; display: none; align-items: center; justify-content: center; background: rgba(0, 0, 0, 0.58); padding: 1.25rem; z-index: 10; }
|
|
64
|
+
.modal-overlay.open { display: flex; }
|
|
65
|
+
.modal { width: calc(100vw - 3rem); height: calc(100vh - 3rem); background: #191c22; border: 1px solid #2c313a; border-radius: 8px; display: flex; flex-direction: column; box-shadow: 0 18px 60px rgba(0, 0, 0, 0.38); }
|
|
66
|
+
.modal-header { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 0.85rem 1rem 0.55rem; border-bottom: 1px solid #2c313a; }
|
|
67
|
+
.modal-header h2 { margin: 0; font-size: 1rem; }
|
|
68
|
+
.modal .close { cursor: pointer; color: #9aa2ad; background: none; border: 0; font-size: 0.9rem; padding: 0.2rem 0; }
|
|
69
|
+
.modal .close:hover { color: var(--accent-light); }
|
|
70
|
+
.modal-tabs { display: flex; gap: 0.25rem; padding: 0 1rem; border-bottom: 1px solid #2c313a; }
|
|
71
|
+
.modal-tabs .nav-button { background: none; border: none; border-bottom: 2px solid transparent; color: rgba(255, 255, 255, 0.65); padding: 0.45rem 0.7rem 0.5rem; margin-bottom: -1px; cursor: pointer; font-size: 0.85rem; }
|
|
72
|
+
.modal-tabs .nav-button:hover { color: #fff; }
|
|
73
|
+
.modal-tabs .nav-button.active { color: var(--accent-light); border-bottom-color: var(--accent); }
|
|
74
|
+
.modal-body { flex: 1; min-height: 0; overflow-y: auto; padding: 1rem; }
|
|
75
|
+
.modal-body h3:first-child { margin-top: 0; }
|
|
76
|
+
.transcript-session { margin: 0.75rem 0; color: #9aa2ad; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 0.75rem; text-align: center; }
|
|
77
|
+
.transcript-entry { background: #101216; border: 1px solid #2c313a; border-radius: 6px; margin-bottom: 0.75rem; overflow: hidden; }
|
|
78
|
+
.transcript-head { color: #9aa2ad; background: #171a20; border-bottom: 1px solid #2c313a; padding: 0.45rem 0.6rem; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 0.72rem; }
|
|
79
|
+
.transcript-text { white-space: pre-wrap; margin: 0; background: transparent; border-radius: 0; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 0.76rem; }
|
|
80
|
+
@media (max-width: 767px) {
|
|
81
|
+
.modal-overlay { padding: 0; }
|
|
82
|
+
.modal { width: 100%; height: 100%; max-height: none; border-radius: 0; border-left: 0; border-right: 0; }
|
|
83
|
+
}
|
|
67
84
|
.tiles { display: flex; gap: 0.6rem; flex-wrap: wrap; margin-bottom: 1rem; }
|
|
68
85
|
.tile { background: #1a1d23; border-radius: 10px; padding: 0.6rem 0.9rem; min-width: 120px; }
|
|
69
86
|
.tile .n { font-size: 1.3rem; font-weight: 700; }
|
|
@@ -101,7 +118,16 @@ export const indexHtml = `<!DOCTYPE html>
|
|
|
101
118
|
<button data-view="health">Health</button>
|
|
102
119
|
</nav>
|
|
103
120
|
<main id="main"></main>
|
|
104
|
-
<div id="
|
|
121
|
+
<div id="modal-overlay" class="modal-overlay">
|
|
122
|
+
<section class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-title">
|
|
123
|
+
<div class="modal-header">
|
|
124
|
+
<h2 id="modal-title"></h2>
|
|
125
|
+
<button type="button" class="close" id="modal-close">close ✕</button>
|
|
126
|
+
</div>
|
|
127
|
+
<div id="modal-tabs" class="modal-tabs"></div>
|
|
128
|
+
<div id="modal-body" class="modal-body"></div>
|
|
129
|
+
</section>
|
|
130
|
+
</div>
|
|
105
131
|
<script>
|
|
106
132
|
const API = '/api/v1';
|
|
107
133
|
const CONDITIONS = ['needs-human', 'active', 'ready', 'waiting', 'stalled', 'finished'];
|
|
@@ -185,7 +211,7 @@ async function renderBoard() {
|
|
|
185
211
|
const items = board.filter((c) => c.condition === cond);
|
|
186
212
|
const cards = items.map((item) => el('div', {
|
|
187
213
|
class: 'card',
|
|
188
|
-
onclick: () =>
|
|
214
|
+
onclick: () => openItemModal(item.repo, item.number),
|
|
189
215
|
}, [
|
|
190
216
|
el('div', { class: 'title', text: item.repo + '#' + item.number + ' ' + item.title }),
|
|
191
217
|
el('div', { class: 'meta' }, [
|
|
@@ -217,18 +243,72 @@ function resourceUriToUrl(resourceUri) {
|
|
|
217
243
|
return null;
|
|
218
244
|
}
|
|
219
245
|
|
|
220
|
-
async function
|
|
221
|
-
const
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
246
|
+
async function openItemModal(repo, number) {
|
|
247
|
+
const overlay = document.getElementById('modal-overlay');
|
|
248
|
+
const title = document.getElementById('modal-title');
|
|
249
|
+
const tabs = document.getElementById('modal-tabs');
|
|
250
|
+
const body = document.getElementById('modal-body');
|
|
251
|
+
const loaded = new Map();
|
|
252
|
+
body.textContent = 'Loading...';
|
|
253
|
+
tabs.innerHTML = '';
|
|
254
|
+
title.textContent = repo + '#' + number;
|
|
255
|
+
overlay.classList.add('open');
|
|
256
|
+
|
|
257
|
+
for (const tab of ['details', 'events', 'transcripts']) {
|
|
258
|
+
tabs.appendChild(el('button', {
|
|
259
|
+
type: 'button',
|
|
260
|
+
class: 'nav-button',
|
|
261
|
+
text: tab[0].toUpperCase() + tab.slice(1),
|
|
262
|
+
onclick: () => switchItemTab(tab),
|
|
263
|
+
}));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function ensureDetail() {
|
|
267
|
+
if (!loaded.has('detail')) {
|
|
268
|
+
loaded.set('detail', await getJson('/items/' + encodeURIComponent(repo) + '/' + number));
|
|
269
|
+
}
|
|
270
|
+
return loaded.get('detail');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function switchItemTab(tab) {
|
|
274
|
+
for (const button of tabs.querySelectorAll('button')) {
|
|
275
|
+
button.classList.toggle('active', button.textContent.toLowerCase() === tab);
|
|
276
|
+
}
|
|
277
|
+
body.scrollTop = 0;
|
|
278
|
+
if (!loaded.has(tab)) {
|
|
279
|
+
body.textContent = 'Loading...';
|
|
280
|
+
loaded.set(tab, await renderItemTab(tab));
|
|
281
|
+
}
|
|
282
|
+
body.replaceChildren(loaded.get(tab));
|
|
283
|
+
if (tab === 'transcripts') {
|
|
284
|
+
body.scrollTop = body.scrollHeight;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function renderItemTab(tab) {
|
|
289
|
+
const detail = await ensureDetail();
|
|
290
|
+
if (!detail) return el('p', { text: 'Not found' });
|
|
291
|
+
if (tab === 'events') {
|
|
292
|
+
return el('div', {}, [el('pre', { text: JSON.stringify(detail.events, null, 2) })]);
|
|
293
|
+
}
|
|
294
|
+
if (tab === 'transcripts') {
|
|
295
|
+
const transcripts = await getJson('/work-items/' + encodeURIComponent(detail.item.workItemKey) + '/transcripts');
|
|
296
|
+
return renderTranscripts(transcripts);
|
|
297
|
+
}
|
|
298
|
+
return renderItemDetails(detail);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
await switchItemTab('details');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function renderItemDetails(detail) {
|
|
305
|
+
const body = el('div');
|
|
306
|
+
const repo = detail.item.issue.repo;
|
|
307
|
+
const number = detail.item.issue.number;
|
|
228
308
|
const headLink = el('a', { href: detail.item.issue.url, target: '_blank', rel: 'noopener noreferrer', text: repo + '#' + number });
|
|
229
|
-
body.appendChild(el('
|
|
309
|
+
body.appendChild(el('h3', {}, [headLink]));
|
|
230
310
|
body.appendChild(el('p', { text: detail.item.issue.title }));
|
|
231
|
-
body.appendChild(el('p', { class: 'meta', text: 'stage: ' + detail.item.wake.stage + (detail.item.wake.sessionId ? '
|
|
311
|
+
body.appendChild(el('p', { class: 'meta', text: 'stage: ' + detail.item.wake.stage + (detail.item.wake.sessionId ? ' | session: ' + detail.item.wake.sessionId : '') }));
|
|
232
312
|
if (detail.item.wake.workspacePath) {
|
|
233
313
|
body.appendChild(el('p', { class: 'meta', text: 'workspace: ' + detail.item.wake.workspacePath }));
|
|
234
314
|
}
|
|
@@ -237,7 +317,7 @@ async function openItem(repo, number) {
|
|
|
237
317
|
const retryBtn = el('button', { type: 'button', class: 'btn', text: 'Retry' });
|
|
238
318
|
retryBtn.addEventListener('click', async () => {
|
|
239
319
|
retryBtn.disabled = true;
|
|
240
|
-
retryBtn.textContent = 'Queuing retry
|
|
320
|
+
retryBtn.textContent = 'Queuing retry...';
|
|
241
321
|
try {
|
|
242
322
|
await postJson('/work-items/' + encodeURIComponent(detail.item.workItemKey) + '/retry');
|
|
243
323
|
retryBtn.textContent = 'Retry queued';
|
|
@@ -264,18 +344,48 @@ async function openItem(repo, number) {
|
|
|
264
344
|
body.appendChild(resourceList);
|
|
265
345
|
}
|
|
266
346
|
body.appendChild(el('h3', { text: 'Runs' }));
|
|
267
|
-
|
|
347
|
+
body.appendChild(el('table', {}, [
|
|
268
348
|
el('tr', {}, ['action', 'status', 'sentinel', 'started', 'runId'].map((h) => el('th', { text: h }))),
|
|
269
349
|
...detail.runs.map((r) => el('tr', {}, [
|
|
270
350
|
el('td', { text: r.action }), el('td', { text: r.status }), el('td', { text: r.sentinel || '' }),
|
|
271
351
|
el('td', { text: r.startedAt }), el('td', { text: r.runId }),
|
|
272
352
|
])),
|
|
273
|
-
]);
|
|
274
|
-
body.appendChild(runsTable);
|
|
353
|
+
]));
|
|
275
354
|
body.appendChild(el('h3', { text: 'Context' }));
|
|
276
355
|
body.appendChild(el('pre', { text: JSON.stringify(detail.item.context, null, 2) }));
|
|
277
|
-
body
|
|
278
|
-
|
|
356
|
+
return body;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function renderTranscripts(transcripts) {
|
|
360
|
+
if (!transcripts.enabled) {
|
|
361
|
+
return el('p', { class: 'meta', text: 'Transcripts are not enabled \\u2014 set transcripts.enabled: true in config to turn them on.' });
|
|
362
|
+
}
|
|
363
|
+
if (!transcripts.sessions || transcripts.sessions.length === 0) {
|
|
364
|
+
return el('p', { class: 'meta', text: 'No transcripts available for this item.' });
|
|
365
|
+
}
|
|
366
|
+
const root = el('div');
|
|
367
|
+
for (const session of transcripts.sessions) {
|
|
368
|
+
const firstStartedAt = session.entries[0] ? session.entries[0].startedAt : '';
|
|
369
|
+
const sessionLabel = session.sessionId || session.sessionKey;
|
|
370
|
+
root.appendChild(
|
|
371
|
+
el('div', {
|
|
372
|
+
class: 'transcript-session',
|
|
373
|
+
text:
|
|
374
|
+
'\\u2014 new session \\u00b7 ' +
|
|
375
|
+
sessionLabel +
|
|
376
|
+
' \\u00b7 ' +
|
|
377
|
+
firstStartedAt +
|
|
378
|
+
' \\u2014',
|
|
379
|
+
}),
|
|
380
|
+
);
|
|
381
|
+
for (const entry of session.entries) {
|
|
382
|
+
root.appendChild(el('article', { class: 'transcript-entry' }, [
|
|
383
|
+
el('div', { class: 'transcript-head', text: entry.role + ' \\u00b7 ' + entry.action + ' \\u00b7 ' + entry.cli + ' \\u00b7 ' + entry.startedAt }),
|
|
384
|
+
el('pre', { class: 'transcript-text', text: entry.text }),
|
|
385
|
+
]));
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return root;
|
|
279
389
|
}
|
|
280
390
|
|
|
281
391
|
async function renderActivity() {
|
|
@@ -415,8 +525,13 @@ function switchView(view) {
|
|
|
415
525
|
for (const btn of document.querySelectorAll('nav button')) {
|
|
416
526
|
btn.addEventListener('click', () => switchView(btn.dataset.view));
|
|
417
527
|
}
|
|
418
|
-
document.getElementById('
|
|
419
|
-
document.getElementById('
|
|
528
|
+
document.getElementById('modal-close').addEventListener('click', () => {
|
|
529
|
+
document.getElementById('modal-overlay').classList.remove('open');
|
|
530
|
+
});
|
|
531
|
+
document.getElementById('modal-overlay').addEventListener('click', (event) => {
|
|
532
|
+
if (event.target.id === 'modal-overlay') {
|
|
533
|
+
event.currentTarget.classList.remove('open');
|
|
534
|
+
}
|
|
420
535
|
});
|
|
421
536
|
document.getElementById('force-tick').addEventListener('click', forceTickNow);
|
|
422
537
|
|
|
@@ -252,6 +252,101 @@ export async function buildItemDetail(input) {
|
|
|
252
252
|
const events = await input.stateStore.listEventEnvelopesForWorkItem(item.workItemKey, 50);
|
|
253
253
|
return { item, runs, events };
|
|
254
254
|
}
|
|
255
|
+
function parseTranscriptFileName(file) {
|
|
256
|
+
if (!file.endsWith('.txt')) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
const parts = file.slice(0, -'.txt'.length).split('.');
|
|
260
|
+
if (parts.length < 4) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
const kind = parts.at(-1);
|
|
264
|
+
if (kind !== 'prompt' && kind !== 'response') {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
const action = parts.at(-2);
|
|
268
|
+
const cli = parts.at(-3);
|
|
269
|
+
const runId = parts.slice(0, -3).join('.');
|
|
270
|
+
if (action === undefined || cli === undefined || runId.length === 0) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
runId,
|
|
275
|
+
cli,
|
|
276
|
+
action,
|
|
277
|
+
kind,
|
|
278
|
+
role: kind === 'prompt' ? 'user' : 'agent',
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
export async function buildItemTranscripts(input) {
|
|
282
|
+
if (!input.config.transcripts.enabled) {
|
|
283
|
+
return { enabled: false, sessions: [] };
|
|
284
|
+
}
|
|
285
|
+
const runs = (await input.stateStore.listRunRecords()).filter((run) => run.workItemKey === input.workItemKey);
|
|
286
|
+
const runsById = new Map(runs.map((run) => [run.runId, run]));
|
|
287
|
+
const workDir = input.stateStore.paths.transcriptWorkDir(input.workItemKey);
|
|
288
|
+
const sessionDirs = (await readdir(workDir, { withFileTypes: true }).catch(() => []))
|
|
289
|
+
.filter((entry) => entry.isDirectory())
|
|
290
|
+
.map((entry) => entry.name);
|
|
291
|
+
const sessionsByKey = new Map();
|
|
292
|
+
for (const sessionKey of sessionDirs) {
|
|
293
|
+
const sessionDir = join(workDir, sessionKey);
|
|
294
|
+
const files = (await readdir(sessionDir, { withFileTypes: true }).catch(() => []))
|
|
295
|
+
.filter((entry) => entry.isFile())
|
|
296
|
+
.map((entry) => entry.name);
|
|
297
|
+
for (const file of files) {
|
|
298
|
+
const parsed = parseTranscriptFileName(file);
|
|
299
|
+
if (parsed === null) {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
const run = runsById.get(parsed.runId);
|
|
303
|
+
if (run === undefined) {
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
const transcriptEntry = {
|
|
307
|
+
runId: parsed.runId,
|
|
308
|
+
action: parsed.action,
|
|
309
|
+
cli: parsed.cli,
|
|
310
|
+
role: parsed.role,
|
|
311
|
+
startedAt: run.startedAt,
|
|
312
|
+
text: await readFile(join(sessionDir, file), 'utf8'),
|
|
313
|
+
};
|
|
314
|
+
const canonicalSessionKey = run.sessionId ?? sessionKey;
|
|
315
|
+
const existing = sessionsByKey.get(canonicalSessionKey);
|
|
316
|
+
if (existing === undefined) {
|
|
317
|
+
sessionsByKey.set(canonicalSessionKey, {
|
|
318
|
+
sessionKey: canonicalSessionKey,
|
|
319
|
+
...(run.sessionId === undefined ? {} : { sessionId: run.sessionId }),
|
|
320
|
+
entries: [transcriptEntry],
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
existing.entries.push(transcriptEntry);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const sessions = [...sessionsByKey.values()];
|
|
329
|
+
for (const session of sessions) {
|
|
330
|
+
session.entries.sort((left, right) => {
|
|
331
|
+
const timeOrder = left.startedAt.localeCompare(right.startedAt);
|
|
332
|
+
if (timeOrder !== 0) {
|
|
333
|
+
return timeOrder;
|
|
334
|
+
}
|
|
335
|
+
if (left.runId !== right.runId) {
|
|
336
|
+
return left.runId.localeCompare(right.runId);
|
|
337
|
+
}
|
|
338
|
+
const roleRank = { user: 0, agent: 1 };
|
|
339
|
+
return roleRank[left.role] - roleRank[right.role];
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
sessions.sort((left, right) => {
|
|
343
|
+
const leftStartedAt = left.entries[0]?.startedAt ?? '';
|
|
344
|
+
const rightStartedAt = right.entries[0]?.startedAt ?? '';
|
|
345
|
+
const timeOrder = leftStartedAt.localeCompare(rightStartedAt);
|
|
346
|
+
return timeOrder === 0 ? left.sessionKey.localeCompare(right.sessionKey) : timeOrder;
|
|
347
|
+
});
|
|
348
|
+
return { enabled: true, sessions };
|
|
349
|
+
}
|
|
255
350
|
export async function buildEventsFeed(input) {
|
|
256
351
|
const limit = input.limit ?? 200;
|
|
257
352
|
return input.stateStore.listRecentEventEnvelopes({
|
|
@@ -5,7 +5,7 @@ import { configuredTicketSource } from '../../domain/sources.js';
|
|
|
5
5
|
import { createEventEnvelope } from '../../lib/event-log.js';
|
|
6
6
|
import { writeJsonFile } from '../../lib/json-file.js';
|
|
7
7
|
import { indexHtml } from './ui-assets.js';
|
|
8
|
-
import { buildBoard, buildConfigView, buildEventsFeed, buildHealth, buildItemDetail, buildRuns, buildStatus, buildWorkspaces, } from './ui-data.js';
|
|
8
|
+
import { buildBoard, buildConfigView, buildEventsFeed, buildHealth, buildItemDetail, buildItemTranscripts, buildRuns, buildStatus, buildWorkspaces, } from './ui-data.js';
|
|
9
9
|
function sendJson(res, status, body) {
|
|
10
10
|
const payload = JSON.stringify(body, null, 2);
|
|
11
11
|
res.writeHead(status, {
|
|
@@ -134,6 +134,17 @@ async function handleRequest(req, res, options, now, projectionUpdater) {
|
|
|
134
134
|
sendJson(res, 202, { workItemKey, retryEventId: retryId });
|
|
135
135
|
return;
|
|
136
136
|
}
|
|
137
|
+
if (req.method === 'GET' &&
|
|
138
|
+
resource === 'work-items' &&
|
|
139
|
+
segments.length === 3 &&
|
|
140
|
+
segments[2] === 'transcripts') {
|
|
141
|
+
sendJson(res, 200, await buildItemTranscripts({
|
|
142
|
+
stateStore,
|
|
143
|
+
config,
|
|
144
|
+
workItemKey: segments[1] ?? '',
|
|
145
|
+
}));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
137
148
|
if (req.method === 'POST' && resource === 'tick' && segments.length === 1) {
|
|
138
149
|
const request = {
|
|
139
150
|
requestId: randomUUID(),
|
package/dist/src/version.js
CHANGED