@atolis-hq/wake 0.2.18 → 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.
@@ -229,6 +229,9 @@ export function formatWakeComment(payload, controlPlaneUrl) {
229
229
  if (kind === 'approval-request') {
230
230
  sections.push('_To approve this work, reply with `/approved`. To request changes, reply with `/changes` followed by your feedback. To ask a question without requesting changes, reply with `/ask` followed by your question._');
231
231
  }
232
+ if (kind === 'question') {
233
+ sections.push('_Reply on this thread to continue — any reply will resume Wake with your response. To request changes to the overall approach instead, reply with `/changes` followed by your feedback._');
234
+ }
232
235
  if (sessionId !== undefined) {
233
236
  const resumeCommandArgs = cli === undefined
234
237
  ? null
@@ -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
- .drawer { position: fixed; top: 0; right: 0; width: min(560px, 100%); height: 100%; background: #191c22; border-left: 1px solid #2c313a; overflow-y: auto; padding: 1rem; transform: translateX(100%); transition: transform 0.15s ease; }
64
- .drawer.open { transform: translateX(0); }
65
- .drawer .close { float: right; cursor: pointer; color: #9aa2ad; }
66
- .drawer .close:hover { color: var(--accent-light); }
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="drawer" class="drawer"><span class="close" id="drawer-close">close ✕</span><div id="drawer-body"></div></div>
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 &#x2715;</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: () => openItem(item.repo, item.number),
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 openItem(repo, number) {
221
- const drawer = document.getElementById('drawer');
222
- const body = document.getElementById('drawer-body');
223
- body.innerHTML = 'Loading…';
224
- drawer.classList.add('open');
225
- const detail = await getJson('/items/' + encodeURIComponent(repo) + '/' + number);
226
- if (!detail) { body.textContent = 'Not found'; return; }
227
- body.innerHTML = '';
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('h2', {}, [headLink]));
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 ? ' · session: ' + 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
- const runsTable = el('table', {}, [
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.appendChild(el('h3', { text: 'Recent events' }));
278
- body.appendChild(el('pre', { text: JSON.stringify(detail.events, null, 2) }));
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('drawer-close').addEventListener('click', () => {
419
- document.getElementById('drawer').classList.remove('open');
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(),
@@ -124,4 +124,4 @@ export function resolveWakeVersion(options = {}) {
124
124
  }
125
125
  return '0.1.0-dev';
126
126
  }
127
- export const wakeVersion = "g5c65f84";
127
+ export const wakeVersion = "ge9439a7";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atolis-hq/wake",
3
- "version": "0.2.18",
3
+ "version": "0.2.20",
4
4
  "description": "Local autonomous agent control plane for software development",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {