openvisio-agent 0.20.0 → 0.21.1

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/studio/app.mjs CHANGED
@@ -14,7 +14,7 @@ export function normalizeStatus(value, fallback = 'observed') {
14
14
  if (['active', 'running', 'started', 'ready', 'in_progress', 'working', 'online', 'preflight'].includes(status)) return 'active';
15
15
  if (['queued', 'pending', 'waiting'].includes(status)) return 'queued';
16
16
  if (['completed', 'complete', 'finished', 'done', 'success', 'succeeded', 'ok'].includes(status)) return 'completed';
17
- if (['failed', 'failure', 'error', 'rejected'].includes(status)) return 'failed';
17
+ if (['failed', 'failure', 'error', 'rejected', 'rate_limited'].includes(status)) return 'failed';
18
18
  if (['cancelled', 'canceled', 'aborted', 'interrupted'].includes(status)) return 'cancelled';
19
19
  if (['offline', 'stopped', 'disconnected', 'exited'].includes(status)) return 'offline';
20
20
  if (['skipped', 'recovering', 'idle', 'blocked', 'timeout'].includes(status)) return status;
@@ -164,6 +164,22 @@ export function summarizeModel(model, selectedAgent = 'all') {
164
164
  };
165
165
  }
166
166
 
167
+ // Shared task identity groups recorded work; it does not imply delegation.
168
+ export function taskActivityCycles(model, selectedCycle) {
169
+ const ticket = record(selectedCycle.data.ticket);
170
+ if (ticket.projectId == null || ticket.ticketId == null) return [selectedCycle];
171
+ const latest = new Map();
172
+ for (const cycle of model.cycles) {
173
+ const candidate = record(cycle.data.ticket);
174
+ if (String(candidate.projectId) !== String(ticket.projectId) || String(candidate.ticketId) !== String(ticket.ticketId)) continue;
175
+ const id = agentId(cycle.agent);
176
+ if (!latest.has(id) || timeValue(cycle.lastSeen) > timeValue(latest.get(id).lastSeen)) latest.set(id, cycle);
177
+ }
178
+ // Keep the selected historical cycle visible even when this agent ran again.
179
+ latest.set(agentId(selectedCycle.agent), selectedCycle);
180
+ return [...latest.values()].sort((a, b) => agentId(a.agent).localeCompare(agentId(b.agent)));
181
+ }
182
+
167
183
  export function eventTitle(event) {
168
184
  const data = record(event?.data);
169
185
  const titles = {
@@ -238,10 +254,13 @@ if (typeof document !== 'undefined') initializeStudio();
238
254
 
239
255
  function initializeStudio() {
240
256
  const $ = id => document.getElementById(id);
257
+ const launch = new URLSearchParams(location.hash.slice(1));
258
+ let launchSettings = launch.get('settings') === '1';
259
+ let editingAgent = null, editingSettings = null, settingsRequest = 0;
241
260
  const state = {
242
261
  snapshot: null, model: null, pending: null, paused: false, connected: false,
243
- selectedAgent: 'all', agentSearch: '', search: '', kind: 'all', status: 'all',
244
- view: 'activity', selectedCycle: null, selectedEvent: null, selectedProcess: null,
262
+ selectedAgent: launch.get('agent') || 'all', agentSearch: '', search: '', kind: 'all', status: 'all',
263
+ view: 'activity', detailView: 'plan', selectedCycle: null, selectedEvent: null, selectedProcess: null,
245
264
  limit: PAGE_SIZE, follow: true, pendingUpdates: 0, error: null,
246
265
  };
247
266
  let scheduled = false;
@@ -331,7 +350,7 @@ function initializeStudio() {
331
350
  button.type = 'button';
332
351
  button.setAttribute('aria-pressed', String(state.selectedAgent === id));
333
352
  const avatar = node('span', 'agent-avatar');
334
- if (id === 'all') avatar.append(icon('grid'));
353
+ if (id === 'all') avatar.append(icon('agents'));
335
354
  else { avatar.textContent = name.slice(0, 2).toUpperCase(); avatar.append(node('span', `avatar-status ${agent.status}`)); }
336
355
  const body = node('span', 'agent-text');
337
356
  body.append(node('span', 'agent-name', name), node('span', 'agent-caption', caption));
@@ -351,6 +370,64 @@ function initializeStudio() {
351
370
  $('metric-processes-caption').textContent = state.snapshot.demo ? 'Simulated processes' : summary.unreportedProcesses ? `${summary.unreportedProcesses} runtime PID not reported` : `${online} ${online === 1 ? 'watcher' : 'watchers'} connected`;
352
371
  $('metric-events-caption').textContent = 'In the available local history';
353
372
  }
373
+ function renderModelSettings() {
374
+ const agent = state.model.agents.find(agent => agentId(agent) === state.selectedAgent);
375
+ const section = $('model-settings-summary');
376
+ section.hidden = !agent || state.snapshot.demo;
377
+ if (!agent || state.snapshot.demo) return;
378
+ $('model-settings-agent').textContent = `${agentName(agent)} · Models`;
379
+ $('open-model-settings').disabled = !agent.settingsEditable;
380
+ const settings = agent.modelSettings || {};
381
+ const pending = settings.revision && agent.appliedModelRevision !== settings.revision;
382
+ $('model-settings-state').textContent = !agent.settingsEditable ? 'Model controls are available for agents configured on this computer.'
383
+ : pending ? agent.modelControlsSupported && agent.status === 'online' ? 'Saved · waiting for the watcher to apply your choices.' : 'Saved for the next watcher connection. Older watchers need an update to apply changes live.'
384
+ : `Replies: ${settings.chatModel || settings.model || 'Runtime default'} · Coding: ${settings.model || 'Runtime default'}`;
385
+ if (launchSettings && agent.settingsEditable) { launchSettings = false; void openModelSettings(agent); }
386
+ }
387
+ async function settingsFetch(path, options) {
388
+ const response = await fetch(path, { cache: 'no-store', ...options });
389
+ const body = await response.json();
390
+ if (!response.ok) throw new Error(body.error || 'Could not load model settings. Try again.');
391
+ return body;
392
+ }
393
+ async function loadModels(agent, request) {
394
+ $('model-catalog-status').textContent = 'Loading model choices…';
395
+ try {
396
+ const { models } = await settingsFetch(`/api/agents/${encodeURIComponent(agent.slug)}/models`);
397
+ if (request !== settingsRequest) return;
398
+ const choices = [...new Set([editingSettings?.model, editingSettings?.chatModel, ...models].filter(Boolean))];
399
+ $('runtime-models').replaceChildren(...choices.map(model => { const option = node('option'); option.value = model; return option; }));
400
+ $('model-catalog-status').textContent = choices.length ? `${choices.length} models · select a field to search, or enter a model ID.` : 'Enter a model ID supported by your connected runtime.';
401
+ } catch (error) {
402
+ if (request === settingsRequest) $('model-catalog-status').textContent = `${error.message} You can still enter a model ID.`;
403
+ }
404
+ }
405
+ async function openModelSettings(agent) {
406
+ const request = ++settingsRequest;
407
+ editingAgent = agent; editingSettings = null;
408
+ $('model-settings-title').textContent = `${agentName(agent)} · Model settings`;
409
+ $('model-settings-error').hidden = true;
410
+ $('reply-model').value = ''; $('coding-model').value = '';
411
+ $('runtime-models').replaceChildren();
412
+ $('save-model-settings').disabled = true;
413
+ $('reload-models').disabled = true;
414
+ $('model-catalog-status').textContent = 'Loading settings…';
415
+ if (!$('model-settings-dialog').open) $('model-settings-dialog').showModal();
416
+ try {
417
+ const settings = await settingsFetch(`/api/agents/${encodeURIComponent(agent.slug)}/settings`);
418
+ if (request !== settingsRequest) return;
419
+ editingSettings = settings;
420
+ $('reply-model').value = settings.chatModel || settings.model || '';
421
+ $('coding-model').value = settings.model || '';
422
+ $('save-model-settings').disabled = false;
423
+ $('reload-models').disabled = false;
424
+ void loadModels(agent, request);
425
+ } catch (error) {
426
+ if (request !== settingsRequest) return;
427
+ $('model-settings-error').hidden = false; $('model-settings-error').textContent = error.message;
428
+ $('model-catalog-status').textContent = 'Close this panel and try again when the agent is available.';
429
+ }
430
+ }
354
431
  function resetSelection() {
355
432
  state.limit = PAGE_SIZE;
356
433
  state.selectedEvent = null;
@@ -413,7 +490,12 @@ function initializeStudio() {
413
490
  return baseRow({ id: event.id, title: eventTitle(event), detail: description(event), agent: event.agent, status: eventStatus(event), timestamp: event.timestamp,
414
491
  iconName: kindIcons[event.type.split('.')[0]] || 'pulse', selected: state.selectedEvent === event.id,
415
492
  extra: text(ticket.slug || (ticket.ticketId != null ? `Ticket ${ticket.ticketId}` : '')),
416
- onClick: () => { state.selectedEvent = event.id; state.selectedCycle = cycleKey(event); state.selectedProcess = null; },
493
+ onClick: () => {
494
+ state.selectedEvent = event.id; state.selectedCycle = cycleKey(event); state.selectedProcess = null;
495
+ if (event.type.startsWith('tool.')) state.detailView = 'commands';
496
+ else if (event.type.startsWith('output.')) state.detailView = 'response';
497
+ else if (event.type === 'plan.updated') state.detailView = 'plan';
498
+ },
417
499
  });
418
500
  }
419
501
  function cycleRow(cycle) {
@@ -455,7 +537,7 @@ function initializeStudio() {
455
537
  return empty;
456
538
  }
457
539
  function detailSection(title, iconName) {
458
- const section = node('section', 'detail-section');
540
+ const section = node('section', 'detail-section oa-card');
459
541
  const heading = node('h3'); if (iconName) heading.append(icon(iconName)); heading.append(document.createTextNode(title));
460
542
  section.append(heading);
461
543
  return section;
@@ -479,6 +561,7 @@ function initializeStudio() {
479
561
  const inspector = $('inspector');
480
562
  // Preserve the user's expanded history and scroll across live snapshots.
481
563
  const historyOpen = inspector.querySelector('.history-details')?.open;
564
+ const runtimeOpen = inspector.querySelector('.runtime-details')?.open;
482
565
  const oldScroll = inspector.scrollTop;
483
566
  const process = state.view === 'processes' ? state.model.processes.find(item => item.id === state.selectedProcess) : null;
484
567
  const cycle = state.view !== 'processes' ? state.model.cycles.find(item => item.id === state.selectedCycle) : null;
@@ -489,15 +572,20 @@ function initializeStudio() {
489
572
  inspector.replaceChildren(empty); return;
490
573
  }
491
574
  if (process) renderProcessDetail(inspector, process);
492
- else if (cycle) renderCycleDetail(inspector, cycle, event);
575
+ else if (cycle) renderCycleDetail(inspector, cycle);
493
576
  else renderEventDetail(inspector, event);
494
577
  const history = inspector.querySelector('.history-details');
495
578
  if (history && historyOpen) history.open = true;
579
+ const runtime = inspector.querySelector('.runtime-details');
580
+ if (runtime && runtimeOpen) runtime.open = true;
496
581
  if (!state.follow) inspector.scrollTop = oldScroll;
497
582
  }
498
- function renderCycleDetail(inspector, cycle, selectedEvent) {
499
- inspector.replaceChildren(inspectorHeader('Cycle details', cycleTitle(cycle), `${agentName(cycle.agent)}${cycle.agent.provider ? ` · ${cycle.agent.provider}` : ''}`, cycle.status));
500
- const overview = detailSection('Runtime', 'terminal');
583
+ function renderCycleDetail(inspector, cycle) {
584
+ inspector.replaceChildren(taskActivityCard(cycle));
585
+ const overview = node('details', 'detail-section runtime-details');
586
+ const runtimeSummary = node('summary', '', 'Runtime details');
587
+ runtimeSummary.dataset.focusKey = `runtime:${cycle.id}`;
588
+ overview.append(runtimeSummary);
501
589
  const ticket = record(cycle.data.ticket); const thread = record(cycle.data.thread);
502
590
  overview.append(detailGrid([
503
591
  ['Model', cycle.data.model], ['Lane', cycle.data.lane || cycle.data.kind],
@@ -508,62 +596,137 @@ function initializeStudio() {
508
596
  ['Thread', thread.threadId != null ? `${thread.channelId != null ? `Channel ${thread.channelId} · ` : ''}${thread.threadId}` : null],
509
597
  ['Workspace', cycle.data.workdir, true],
510
598
  ]));
511
- inspector.append(overview);
512
599
  if (cycle.status === 'offline') {
513
600
  const note = detailSection('Activity unavailable', 'pulse');
514
601
  note.append(node('p', 'detail-note', 'This watcher stopped reporting or restarted. The recorded history has no final outcome for this cycle. Check the ticket and watcher log before deciding what to do next.'));
515
602
  inspector.append(note);
516
603
  }
517
604
  if (cycle.data.reason) { const reason = detailSection('Outcome', 'message'); reason.append(node('p', 'detail-note', cycle.data.reason)); inspector.append(reason); }
518
- if (selectedEvent && !selectedEvent.type.startsWith('cycle.')) {
519
- const selected = detailSection('Selected action', kindIcons[selectedEvent.type.split('.')[0]] || 'pulse');
520
- selected.append(node('p', 'tool-name', eventTitle(selectedEvent)), node('p', 'detail-note', fullTime(selectedEvent.timestamp)));
521
- const detail = description(selectedEvent); if (detail) selected.append(node('pre', 'tool-title', detail));
522
- inspector.append(selected);
605
+ const variants = node('div', 'detail-variants');
606
+ variants.setAttribute('role', 'group'); variants.setAttribute('aria-label', 'Cycle details view');
607
+ for (const [id, label, iconName] of [['plan', 'Plan', 'list'], ['commands', 'Commands', 'command'], ['response', 'Response', 'message']]) {
608
+ const button = node('button', `button variant${state.detailView === id ? ' is-selected' : ''}`);
609
+ button.type = 'button'; button.dataset.focusKey = `detail-view:${id}`;
610
+ button.setAttribute('aria-pressed', String(state.detailView === id));
611
+ button.append(icon(iconName), node('span', '', label));
612
+ button.addEventListener('click', () => { state.detailView = id; scheduleRender(); });
613
+ variants.append(button);
523
614
  }
524
- const plan = detailSection('Explicit plan', 'list');
525
- if (cycle.plan?.length) {
526
- const list = node('ol', 'plan-list');
527
- cycle.plan.forEach((step, index) => {
528
- const item = node('li', `plan-step ${step.status}`); const marker = node('span', 'plan-marker');
529
- if (step.status === 'completed') marker.append(icon('check')); else marker.textContent = String(index + 1);
530
- item.append(marker, node('span', 'plan-content', step.content), node('span', 'plan-status', statusLabels[step.status] || 'Pending'));
531
- list.append(item);
532
- });
533
- plan.append(list);
534
- } else plan.append(node('p', 'detail-note', 'No explicit plan recorded for this cycle.'));
535
- inspector.append(plan);
536
- if (cycle.tools.size) {
537
- const tools = detailSection('Tools & coding actions', 'tool'); tools.querySelector('h3').append(node('span', 'count', cycle.tools.size));
615
+ inspector.append(variants);
616
+ const detail = node('div', 'cycle-detail-content');
617
+ inspector.append(detail);
618
+ if (state.detailView === 'plan') {
619
+ const plan = detailSection('Plan', 'list');
620
+ if (cycle.plan?.length) {
621
+ plan.querySelector('h3').append(node('span', 'count', `${cycle.plan.filter(step => step.status === 'completed').length}/${cycle.plan.length}`));
622
+ const list = node('ol', 'plan-list');
623
+ cycle.plan.forEach((step, index) => {
624
+ const item = node('li', `plan-step ${step.status}`); const marker = node('span', 'plan-marker');
625
+ if (step.status === 'completed') marker.append(icon('check')); else marker.textContent = String(index + 1);
626
+ item.append(marker, node('span', 'plan-content', step.content), node('span', 'plan-status', statusLabels[step.status] || 'Pending'));
627
+ list.append(item);
628
+ });
629
+ plan.append(list);
630
+ } else plan.append(node('p', 'detail-note', 'No explicit plan recorded for this cycle.'));
631
+ detail.append(plan);
632
+ }
633
+ if (state.detailView === 'commands') {
634
+ const tools = detailSection('Commands & tools', 'command'); tools.querySelector('h3').append(node('span', 'count', cycle.tools.size));
538
635
  const list = node('div', 'tools-list');
539
636
  for (const tool of [...cycle.tools.values()].reverse()) {
540
637
  const item = node('div', 'tool-item'); const heading = node('div', 'tool-heading');
541
638
  heading.append(node('span', 'tool-name', tool.name || tool.kind || 'Tool'), badge(tool.status)); item.append(heading);
542
- if (tool.title) item.append(node('pre', 'tool-title', tool.title));
639
+ if (tool.title && tool.title !== tool.name) item.append(node('pre', 'tool-title', tool.title));
543
640
  for (const location of array(tool.locations)) {
544
641
  if (location?.path) item.append(node('span', 'file-location', `${location.path}${location.line != null ? `:${location.line}` : ''}`));
545
642
  }
546
643
  list.append(item);
547
644
  }
548
- tools.append(list); inspector.append(tools);
645
+ if (!cycle.tools.size) list.append(node('p', 'detail-note', 'Commands and tool calls will appear here as the agent works.'));
646
+ tools.append(list); detail.append(tools);
549
647
  }
550
- if (cycle.progress.size) {
551
- const progress = detailSection('Progress updates', 'message');
552
- for (const entry of cycle.progress.values()) progress.append(node('pre', 'recorded-output output-entry', entry.text));
553
- inspector.append(progress);
554
- }
555
- if (cycle.final !== null) {
556
- const final = detailSection('Final response', 'check'); final.append(node('pre', 'recorded-output', cycle.final || 'The recorded final response is empty.')); inspector.append(final);
557
- }
558
- if (!cycle.progress.size && cycle.final === null) {
559
- const output = detailSection('Recorded output', 'message'); output.append(node('p', 'detail-note', 'Visible progress and the final response will appear here when recorded.')); inspector.append(output);
648
+ if (state.detailView === 'response') {
649
+ if (cycle.progress.size) {
650
+ const progress = detailSection('Progress updates', 'message');
651
+ for (const entry of cycle.progress.values()) progress.append(node('pre', 'recorded-output output-entry', entry.text));
652
+ detail.append(progress);
653
+ }
654
+ if (cycle.final !== null) {
655
+ const final = detailSection('Final response', 'check'); final.append(node('pre', 'recorded-output', cycle.final || 'The recorded final response is empty.')); detail.append(final);
656
+ }
657
+ if (!cycle.progress.size && cycle.final === null) {
658
+ const output = detailSection('Recorded output', 'message'); output.append(node('p', 'detail-note', 'Visible progress and the final response will appear here when recorded.')); detail.append(output);
659
+ }
560
660
  }
661
+ inspector.append(overview);
561
662
  const history = node('details', 'detail-section history-details'); history.append(node('summary', '', `Cycle history · ${cycle.events.length} events`));
562
663
  history.querySelector('summary').dataset.focusKey = `history:${cycle.id}`;
563
664
  for (const event of cycle.events) { const row = node('div', 'mini-event'); row.append(node('time', '', shortTime(event.timestamp)), node('span', '', eventTitle(event))); history.append(row); }
564
665
  history.append(detailGrid([['Cycle ID', cycle.cycleId, true], ['Run ID', cycle.runId, true]]));
565
666
  inspector.append(history);
566
667
  }
668
+
669
+ function taskActivityCard(cycle) {
670
+ const related = taskActivityCycles(state.model, cycle);
671
+ const displayed = related.slice(0, 4);
672
+ if (!displayed.some(item => item.id === cycle.id)) displayed[displayed.length - 1] = cycle;
673
+ const card = node('section', 'task-activity-card oa-card');
674
+ card.setAttribute('aria-label', 'Task activity');
675
+ const header = node('div', 'fanout-header');
676
+ const title = node('h2', '', 'Task activity');
677
+ const count = node('span', 'badge fanout-count');
678
+ const dots = node('span', 'agent-dots'); dots.setAttribute('aria-hidden', 'true');
679
+ displayed.forEach((_, index) => dots.append(node('span', `dot agent-color-${index}`)));
680
+ count.append(dots, node('span', '', `${related.filter(item => item.status === 'completed').length}/${related.length} complete`));
681
+ header.append(title, count);
682
+ const surface = node('div', 'fanout-surface oa-inset');
683
+ const task = node('div', 'fanout-task');
684
+ task.append(node('span', 'eyebrow', 'Task'), node('span', '', cycleTitle(cycle)));
685
+ surface.append(task);
686
+ const graph = node('div', 'fanout-graph');
687
+ const root = node('span', `fanout-root ${cycle.status}`); root.append(icon('stack'));
688
+ graph.append(root);
689
+ const lines = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
690
+ lines.setAttribute('viewBox', '0 0 400 80'); lines.setAttribute('preserveAspectRatio', 'none');
691
+ lines.setAttribute('class', 'fanout-lines'); lines.setAttribute('aria-hidden', 'true');
692
+ for (let index = 0; index < displayed.length; index++) {
693
+ const x = (index + .5) * 400 / displayed.length;
694
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
695
+ path.setAttribute('d', `M200 0 C200 38 ${x} 38 ${x} 80`); lines.append(path);
696
+ }
697
+ graph.append(lines);
698
+ const branches = node('div', 'fanout-branches');
699
+ displayed.forEach((item, index) => {
700
+ const branch = node('button', `fanout-branch agent-color-${index}${item.id === cycle.id ? ' is-selected' : ''}`);
701
+ branch.type = 'button'; branch.dataset.focusKey = `branch:${item.id}`;
702
+ branch.setAttribute('aria-label', `${agentName(item.agent)} · ${statusLabels[item.status] || item.status} · View cycle`);
703
+ branch.setAttribute('aria-pressed', String(item.id === cycle.id));
704
+ const marker = node('span', `fanout-marker ${item.status}`);
705
+ marker.append(icon(item.status === 'completed' ? 'check' : ['failed', 'cancelled', 'blocked', 'timeout'].includes(item.status) ? 'close' : ['active', 'recovering'].includes(item.status) ? 'loader' : 'clock'));
706
+ branch.append(marker, node('span', 'fanout-name', agentName(item.agent)));
707
+ branch.addEventListener('click', () => {
708
+ state.selectedCycle = item.id; state.selectedEvent = null; state.selectedAgent = 'all';
709
+ state.view = 'cycles'; state.search = ''; state.kind = 'all'; state.status = 'all';
710
+ $('event-search').value = ''; $('kind-filter').value = 'all'; $('status-filter').value = 'all';
711
+ state.follow = false; $('follow-latest').checked = false; scheduleRender();
712
+ });
713
+ branches.append(branch);
714
+ });
715
+ graph.append(branches); surface.append(graph);
716
+ const rows = node('div', 'fanout-results');
717
+ for (const item of displayed) {
718
+ const row = node('div', 'fanout-result');
719
+ const elapsed = Number.isFinite(item.data.durationMs) ? duration(item.data.durationMs) : statusLabels[item.status] || item.status;
720
+ const summary = item.final || [...item.progress.values()].at(-1)?.text || `${item.tools.size} tool calls recorded`;
721
+ row.append(node('span', 'fanout-result-name', agentName(item.agent)), node('span', 'fanout-result-summary', summary), node('span', 'fanout-duration', elapsed));
722
+ rows.append(row);
723
+ }
724
+ surface.append(rows);
725
+ const footer = node('div', 'fanout-footer');
726
+ footer.append(node('span', '', related.length > displayed.length ? `${displayed.length} of ${related.length} agents shown` : `${cycle.events.length} events in this cycle`), badge(cycle.status));
727
+ card.append(header, surface, footer);
728
+ return card;
729
+ }
567
730
  function renderEventDetail(inspector, event) {
568
731
  inspector.replaceChildren(inspectorHeader('Event details', eventTitle(event), agentName(event.agent), eventStatus(event)));
569
732
  const section = detailSection('Recorded event', kindIcons[event.type.split('.')[0]] || 'pulse');
@@ -614,7 +777,7 @@ function initializeStudio() {
614
777
  for (const button of document.querySelectorAll('[data-view]')) {
615
778
  const selected = button.dataset.view === state.view; button.classList.toggle('is-selected', selected); button.setAttribute('aria-pressed', String(selected));
616
779
  }
617
- renderAgents(); renderMetrics(); renderActivity();
780
+ renderAgents(); renderMetrics(); renderActivity(); renderModelSettings();
618
781
  $('history-label').textContent = `Local history · ${state.model.events.length} of up to ${Number(state.snapshot.limits?.maxEvents) || MAX_EVENTS} recent events`;
619
782
  $('updated-at').textContent = `Snapshot at ${shortTime(state.snapshot.generatedAt)}`;
620
783
  $('updated-at').title = fullTime(state.snapshot.generatedAt);
@@ -622,6 +785,29 @@ function initializeStudio() {
622
785
  }
623
786
 
624
787
  $('server-address').textContent = location.host;
788
+ $('open-model-settings').addEventListener('click', () => {
789
+ const agent = state.model.agents.find(agent => agentId(agent) === state.selectedAgent);
790
+ if (agent?.settingsEditable) void openModelSettings(agent);
791
+ });
792
+ $('close-model-settings').addEventListener('click', () => $('model-settings-dialog').close());
793
+ $('model-settings-dialog').addEventListener('close', () => { settingsRequest++; editingAgent = null; editingSettings = null; });
794
+ $('reload-models').addEventListener('click', () => { if (editingAgent) void loadModels(editingAgent, settingsRequest); });
795
+ $('model-settings-form').addEventListener('submit', async event => {
796
+ event.preventDefault();
797
+ if (!editingAgent || !editingSettings) return;
798
+ const request = settingsRequest;
799
+ $('save-model-settings').disabled = true; $('save-model-settings').textContent = 'Saving…';
800
+ $('model-settings-error').hidden = true;
801
+ try {
802
+ await settingsFetch(`/api/agents/${encodeURIComponent(editingAgent.slug)}/settings`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ version: editingSettings.version, model: $('coding-model').value.trim(), chatModel: $('reply-model').value.trim() }) });
803
+ if (request !== settingsRequest) return;
804
+ $('model-settings-dialog').close();
805
+ toast('Models saved. New requests will use your choices once the watcher applies them.');
806
+ receive(await settingsFetch('/api/snapshot'));
807
+ } catch (error) {
808
+ if (request === settingsRequest) { $('model-settings-error').hidden = false; $('model-settings-error').textContent = error.message; }
809
+ } finally { $('save-model-settings').disabled = false; $('save-model-settings').textContent = 'Save models'; }
810
+ });
625
811
  $('pause-button').addEventListener('click', () => {
626
812
  state.paused = !state.paused;
627
813
  if (!state.paused && state.pending) { const pending = state.pending; state.pending = null; state.pendingUpdates = 0; receive(pending); }
package/studio/guide.html CHANGED
@@ -6,6 +6,7 @@
6
6
  <meta name="color-scheme" content="light">
7
7
  <meta name="description" content="Get started with Agent Studio, understand agent activity, and troubleshoot your local viewer.">
8
8
  <title>Agent Studio user guide · OpenVisio</title>
9
+ <link rel="icon" type="image/svg+xml" href="/openvisio.svg">
9
10
  <link rel="stylesheet" href="/style.css">
10
11
  </head>
11
12
  <body>
@@ -22,6 +23,7 @@
22
23
  <a href="#find-work">Find and inspect work</a>
23
24
  <a href="#statuses">Understand statuses</a>
24
25
  <a href="#controls">Pause and follow</a>
26
+ <a href="#models">Change models</a>
25
27
  <a href="#access">Keyboard and reading</a>
26
28
  <a href="#troubleshooting">Troubleshooting</a>
27
29
  <a href="#history">History and privacy</a>
@@ -30,13 +32,21 @@
30
32
  <section id="start">
31
33
  <h2>Open Studio</h2>
32
34
  <p>Agent Studio is a local viewer for bring-your-own (BYO) agents connected to an OpenVisio team. It shows recorded actions from Claude Code, Codex, and OpenCode watchers on this machine.</p>
33
- <p>With a Studio-capable version of <code>openvisio-agent</code> installed, run:</p>
35
+ <p>In the OpenVisio app, open <strong>Agents → Open Agent Studio</strong>. Agents can also include this button in chat replies. Updated backend watchers start Studio automatically on this computer.</p>
36
+ <p>You can also open Studio manually:</p>
34
37
  <pre><code>openvisio-agent studio</code></pre>
35
38
  <p>Your browser opens the local address printed in the terminal, usually <code>http://127.0.0.1:4317</code>. Keep that terminal running while you use Studio.</p>
36
39
  <p>To explore without connecting an agent:</p>
37
40
  <pre><code>openvisio-agent studio --demo</code></pre>
38
41
  <p>The demo uses clearly labeled sample activity. It makes no model calls. To return to your agents, press <kbd>Ctrl+C</kbd> in the Studio terminal and run the command again without <code>--demo</code>.</p>
39
42
  </section>
43
+ <section id="models">
44
+ <h2>Change models</h2>
45
+ <p>Select an agent in the sidebar, choose <strong>Model settings</strong>, select its reply and coding models, then choose <strong>Save models</strong>. Model fields let you search the runtime’s choices or enter a custom model ID.</p>
46
+ <p>Updated watchers apply saved choices to the next request without a restart. Work already running continues with its original model. Studio shows when a save is waiting for the watcher. Offline agents retain the selection for their next connection; older watchers need an update to apply changes live.</p>
47
+ <p>OpenCode choices come from its local model catalog. Listing a model does not guarantee credentials or available quota. If a model is rate-limited, choose one with available quota or retry after the provider’s limit resets.</p>
48
+ <p>Studio controls agents configured on this computer. An agent running on another teammate’s computer must be managed there.</p>
49
+ </section>
40
50
  <section id="watchers">
41
51
  <h2>Show your agents</h2>
42
52
  <p>A <strong>watcher</strong> is the local program that listens for assignments and messages, then starts your coding agent when work arrives. Studio displays what the watcher records.</p>
@@ -57,6 +67,7 @@
57
67
  <li>Select a row. Its details appear beside the list, or below it on a smaller screen.</li>
58
68
  </ol>
59
69
  <p>A <strong>cycle</strong> is one unit of agent work. It can include a plan, several tool calls, progress updates, and a final response. Expand <strong>Cycle history</strong> to review its recorded sequence.</p>
70
+ <p>Switch between <strong>Plan</strong>, <strong>Commands</strong>, and <strong>Response</strong> for steps, tool calls, and visible output. The <strong>Task activity</strong> card groups recorded work on the same ticket by agent; select an agent in the diagram to inspect its cycle. Expand <strong>Runtime details</strong> for model and timing information.</p>
60
71
  <p><strong>No explicit plan recorded</strong> means the provider did not send a plan in the available history. A missing process ID means the provider did not report that operating-system identifier. Neither message alone means the work failed.</p>
61
72
  <p>The process summary counts distinct reported process IDs associated with connected watchers. It is not a system-wide process monitor. The demo deliberately shows no real process count.</p>
62
73
  </section>
@@ -112,7 +123,7 @@ openvisio-agent watch --name ada</code></pre>
112
123
  <h2>History and privacy</h2>
113
124
  <p>Studio reads local journals under <code>~/.openvisio/observability</code>. The viewer serves up to 500 recent events from a bounded set of journal files. Logs rotate, large text can be shortened, and bursts can drop records. This is a recent activity view, not a complete audit archive.</p>
114
125
  <p>The journal records visible plans, tool metadata, and public output. Private reasoning and raw tool payloads are excluded. Known credentials and recognizable secret formats are redacted, but visible output can still include project text and file paths. Review it before sharing a screenshot or journal.</p>
115
- <p>Studio’s server accepts local connections only and has no controls that modify your team or start model calls. Your running watchers continue to use their configured team and provider connections.</p>
126
+ <p>Studio’s server accepts local connections only. Saving models changes only the selected agent’s local model settings; it does not start a model call or modify your team. Your running watchers continue to use their configured team and provider connections.</p>
116
127
  <p><a href="/">Return to Agent Studio</a></p>
117
128
  </section>
118
129
  </main>
package/studio/index.html CHANGED
@@ -6,11 +6,19 @@
6
6
  <meta name="color-scheme" content="light">
7
7
  <meta name="description" content="A local view of your BYO agents, their work, and recorded progress.">
8
8
  <title>Agent studio · OpenVisio</title>
9
+ <link rel="icon" type="image/svg+xml" href="/openvisio.svg">
10
+ <link rel="preload" href="/satoshi-400.woff2" as="font" type="font/woff2" crossorigin>
9
11
  <link rel="stylesheet" href="/style.css">
10
12
  <script type="module" src="/app.mjs"></script>
11
13
  </head>
12
14
  <body>
13
15
  <svg class="icon-definitions" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><defs>
16
+ <symbol id="i-agents" viewBox="0 0 24 24"><path d="M12 3v4m-7 5v-2h14v2"/><rect x="8" y="1" width="8" height="5" rx="2"/><rect x="1" y="13" width="8" height="8" rx="3"/><rect x="15" y="13" width="8" height="8" rx="3"/><path d="M4 17h2m12 0h2"/></symbol>
17
+ <symbol id="i-chevron" viewBox="0 0 24 24"><path d="m9 5 7 7-7 7"/></symbol>
18
+ <symbol id="i-book" viewBox="0 0 24 24"><path d="M12 5v16m0-16C9 3 5 3 2 4v15c4-1 7-1 10 2 3-3 6-3 10-2V4c-3-1-7-1-10 1Z"/></symbol>
19
+ <symbol id="i-command" viewBox="0 0 24 24"><path d="m5 7 5 5-5 5m9 0h6"/></symbol>
20
+ <symbol id="i-loader" viewBox="0 0 24 24"><path d="M12 3a9 9 0 1 1-9 9"/></symbol>
21
+
14
22
  <symbol id="i-grid" viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="2"/><rect x="14" y="3" width="7" height="7" rx="2"/><rect x="3" y="14" width="7" height="7" rx="2"/><rect x="14" y="14" width="7" height="7" rx="2"/></symbol>
15
23
  <symbol id="i-pulse" viewBox="0 0 24 24"><path d="M3 12h4l3-7 4 14 3-7h4"/></symbol>
16
24
  <symbol id="i-stack" viewBox="0 0 24 24"><path d="m3 7 9-4 9 4-9 4-9-4Zm0 5 9 4 9-4M3 17l9 4 9-4"/></symbol>
@@ -30,28 +38,56 @@
30
38
  <a class="skip-link" href="#workspace">Skip to activity</a>
31
39
  <div class="app-shell">
32
40
  <aside class="sidebar" aria-label="Agent navigation">
33
- <a class="brand" href="/" aria-label="OpenVisio Agent studio"><span class="brand-mark"><svg class="icon" aria-hidden="true"><use href="#i-grid"/></svg></span><span>OpenVisio<span class="brand-caption">Local workspace</span></span></a>
41
+ <a class="brand" href="/" aria-label="OpenVisio Agent studio"><span class="brand-mark"><img src="/openvisio.svg" alt="" width="68" height="68"></span><span>OpenVisio<span class="brand-caption">Local workspace</span></span></a>
34
42
  <div class="sidebar-title"><span>Agents</span><span class="count" id="agent-count">0</span></div>
35
43
  <label class="agent-search"><svg class="icon" aria-hidden="true"><use href="#i-search"/></svg><input id="agent-search" type="search" placeholder="Find an agent" aria-label="Filter agent list" autocomplete="off"></label>
36
- <nav id="agent-list" class="agent-list" aria-label="Select an agent"><button class="agent-item is-selected" type="button" aria-pressed="true"><span class="agent-avatar"><svg class="icon" aria-hidden="true"><use href="#i-grid"/></svg></span><span class="agent-text"><span class="agent-name">All agents</span><span class="agent-caption">Waiting for data</span></span></button></nav>
37
- <div class="sidebar-note"><span class="local-indicator"><span class="dot"></span>On this machine</span><p>Recorded actions from your running BYO agents.</p><span class="mono" id="server-address">localhost:4317</span></div>
44
+ <nav id="agent-list" class="agent-list" aria-label="Select an agent"><button class="agent-item is-selected" type="button" aria-pressed="true"><span class="agent-avatar"><svg class="icon" aria-hidden="true"><use href="#i-agents"/></svg></span><span class="agent-text"><span class="agent-name">All agents</span><span class="agent-caption">Waiting for data</span></span></button></nav>
45
+ <div class="sidebar-resources">
46
+ <section class="documentation-card oa-card" aria-labelledby="documentation-title">
47
+ <h2 id="documentation-title"><svg class="icon" aria-hidden="true"><use href="#i-book"/></svg>Documentation</h2>
48
+ <div class="documentation-inset oa-inset">
49
+ <p>Set up your agents and get to know Studio.</p>
50
+ <a class="button documentation-link" href="/guide">Read the guide<svg class="icon" aria-hidden="true"><use href="#i-arrow"/></svg></a>
51
+ </div>
52
+ </section>
53
+ <div class="sidebar-note"><span class="local-indicator"><span class="dot"></span>On this machine</span><p>Recorded actions from your running BYO agents.</p><span class="mono" id="server-address">localhost:4317</span></div>
54
+ </div>
38
55
  </aside>
39
56
  <main id="workspace" class="workspace" tabindex="-1">
40
57
  <header class="page-header">
41
58
  <div><div class="eyebrow">Runtime overview <span id="demo-badge" class="badge demo-badge" hidden>Demo data</span></div><h1>Agent studio</h1><p class="page-description">See what your agents are doing, as it happens.</p></div>
42
- <div class="header-actions"><a class="button" href="/guide">User guide</a><span id="connection-status" class="connection-status" role="status"><span class="dot"></span><span>Connecting</span></span><button id="pause-button" class="button" type="button" aria-pressed="false"><svg class="icon" aria-hidden="true"><use href="#i-pause"/></svg><span>Pause view</span></button></div>
59
+ <div class="header-actions"><a class="button header-documentation" href="/guide"><svg class="icon" aria-hidden="true"><use href="#i-book"/></svg>Documentation</a><span id="connection-status" class="connection-status" role="status"><span class="dot"></span><span>Connecting</span></span><button id="pause-button" class="button" type="button" aria-pressed="false"><svg class="icon" aria-hidden="true"><use href="#i-pause"/></svg><span>Pause view</span></button></div>
43
60
  </header>
44
61
  <div id="notice" class="notice" role="status" hidden></div>
62
+ <section id="model-settings-summary" class="model-settings-summary" aria-label="Agent model settings" hidden>
63
+ <div><h2 id="model-settings-agent">Model settings</h2><p id="model-settings-state" role="status"></p></div>
64
+ <button id="open-model-settings" class="button" type="button">Model settings</button>
65
+ </section>
66
+ <dialog id="model-settings-dialog" aria-labelledby="model-settings-title">
67
+ <form id="model-settings-form" class="model-settings-form">
68
+ <div class="section-heading"><h2 id="model-settings-title">Model settings</h2><button id="close-model-settings" class="button" type="button" aria-label="Close model settings">Close</button></div>
69
+ <p id="model-settings-description">Choose the models this agent uses. Changes apply to its next request; work already running continues.</p>
70
+ <label for="reply-model">Reply model<span>For chat, triage, and coordination</span></label>
71
+ <input id="reply-model" list="runtime-models" placeholder="Choose or search models" required maxlength="200" autocomplete="off">
72
+ <label for="coding-model">Coding model<span>For assigned coding work</span></label>
73
+ <input id="coding-model" list="runtime-models" placeholder="Choose or search models" required maxlength="200" autocomplete="off">
74
+ <datalist id="runtime-models"></datalist>
75
+ <p id="model-catalog-status" role="status">Loading model choices…</p>
76
+ <p class="detail-note">Model access and usage limits depend on the provider connected on this computer.</p>
77
+ <p id="model-settings-error" role="alert" hidden></p>
78
+ <div class="model-settings-actions"><button id="reload-models" class="button" type="button">Reload models</button><button id="save-model-settings" class="button primary" type="submit">Save models</button></div>
79
+ </form>
80
+ </dialog>
45
81
  <section class="metrics" aria-label="Workspace summary">
46
- <article class="metric"><div class="metric-label">Active cycles<svg class="icon" aria-hidden="true"><use href="#i-pulse"/></svg></div><div class="metric-value" id="metric-active">—</div><div class="metric-caption" id="metric-active-caption">Waiting for activity</div></article>
47
- <article class="metric"><div class="metric-label">In the queue<svg class="icon" aria-hidden="true"><use href="#i-stack"/></svg></div><div class="metric-value" id="metric-queued">—</div><div class="metric-caption" id="metric-queued-caption">Cycles waiting to start</div></article>
48
- <article class="metric"><div class="metric-label">Processes<svg class="icon" aria-hidden="true"><use href="#i-terminal"/></svg></div><div class="metric-value" id="metric-processes">—</div><div class="metric-caption" id="metric-processes-caption">Watchers and coding runtimes</div></article>
49
- <article class="metric"><div class="metric-label">Recorded events<svg class="icon" aria-hidden="true"><use href="#i-list"/></svg></div><div class="metric-value" id="metric-events">—</div><div class="metric-caption" id="metric-events-caption">Recent local history</div></article>
82
+ <article class="metric oa-card"><div class="metric-label">Active cycles<svg class="icon" aria-hidden="true"><use href="#i-pulse"/></svg></div><div class="metric-inset oa-inset"><div class="metric-value" id="metric-active">—</div><div class="metric-caption" id="metric-active-caption">Waiting for activity</div></div></article>
83
+ <article class="metric oa-card"><div class="metric-label">In the queue<svg class="icon" aria-hidden="true"><use href="#i-stack"/></svg></div><div class="metric-inset oa-inset"><div class="metric-value" id="metric-queued">—</div><div class="metric-caption" id="metric-queued-caption">Cycles waiting to start</div></div></article>
84
+ <article class="metric oa-card"><div class="metric-label">Processes<svg class="icon" aria-hidden="true"><use href="#i-terminal"/></svg></div><div class="metric-inset oa-inset"><div class="metric-value" id="metric-processes">—</div><div class="metric-caption" id="metric-processes-caption">Watchers and coding runtimes</div></div></article>
85
+ <article class="metric oa-card"><div class="metric-label">Recorded events<svg class="icon" aria-hidden="true"><use href="#i-list"/></svg></div><div class="metric-inset oa-inset"><div class="metric-value" id="metric-events">—</div><div class="metric-caption" id="metric-events-caption">Recent local history</div></div></article>
50
86
  </section>
51
87
  <section class="work-panel" aria-label="Agent activity">
52
88
  <div class="panel-toolbar"><div class="view-tabs" role="group" aria-label="Choose a view"><button class="tab is-selected" type="button" data-view="activity" aria-pressed="true"><svg class="icon" aria-hidden="true"><use href="#i-pulse"/></svg>Activity</button><button class="tab" type="button" data-view="cycles" aria-pressed="false"><svg class="icon" aria-hidden="true"><use href="#i-stack"/></svg>Cycles</button><button class="tab" type="button" data-view="processes" aria-pressed="false"><svg class="icon" aria-hidden="true"><use href="#i-terminal"/></svg>Processes</button></div><label class="follow-control"><input type="checkbox" id="follow-latest" checked>Follow latest</label></div>
53
89
  <div class="filters"><label class="search-field"><svg class="icon" aria-hidden="true"><use href="#i-search"/></svg><input id="event-search" type="search" placeholder="Search actions, tickets, files…" aria-label="Search recorded activity" autocomplete="off"></label><label class="select-field"><span class="sr-only">Event kind</span><select id="kind-filter"><option value="all">All kinds</option><option value="cycle">Cycles</option><option value="tool">Tools</option><option value="plan">Plans</option><option value="output">Output</option><option value="process">Processes</option><option value="runtime">Watchers</option></select></label><label class="select-field"><span class="sr-only">Status</span><select id="status-filter"><option value="all">All statuses</option><option value="active">Active</option><option value="queued">Queued</option><option value="completed">Completed</option><option value="failed">Failed</option><option value="cancelled">Cancelled</option><option value="blocked">Blocked</option><option value="skipped">Skipped</option><option value="recovering">Recovering</option><option value="idle">Idle</option><option value="connecting">Connecting</option><option value="timeout">Timed out</option><option value="offline">Offline</option></select></label></div>
54
- <div class="content-grid"><div class="activity-panel"><div class="section-heading"><h2 id="activity-heading">Latest activity</h2><span class="muted" id="result-count">Connecting…</span></div><div id="activity-list" class="activity-list" aria-label="Recorded activity"><div class="loading-state" role="status"><span class="skeleton skeleton-wide"></span><span class="skeleton"></span><span class="skeleton skeleton-wide"></span><span>Connecting to your local agents…</span></div></div><button id="load-more" class="button load-more" type="button" hidden>Show earlier activity</button></div><aside id="inspector" class="inspector" tabindex="-1" aria-label="Selected activity details"><div class="inspector-empty"><span class="empty-icon"><svg class="icon" aria-hidden="true"><use href="#i-terminal"/></svg></span><h2>The work, in view</h2><p>Select a cycle to see its tools, explicit plan, and recorded output.</p></div></aside></div>
90
+ <div class="content-grid"><div class="activity-panel oa-card"><div class="section-heading"><h2 id="activity-heading">Latest activity</h2><span class="muted" id="result-count">Connecting…</span></div><div id="activity-list" class="activity-list oa-inset" aria-label="Recorded activity"><div class="loading-state" role="status"><span class="skeleton skeleton-wide"></span><span class="skeleton"></span><span class="skeleton skeleton-wide"></span><span>Connecting to your local agents…</span></div></div><button id="load-more" class="button load-more" type="button" hidden>Show earlier activity</button></div><aside id="inspector" class="inspector" tabindex="-1" aria-label="Selected activity details"><div class="inspector-empty"><span class="empty-icon"><svg class="icon" aria-hidden="true"><use href="#i-terminal"/></svg></span><h2>The work, in view</h2><p>Select a cycle to see its tools, explicit plan, and recorded output.</p></div></aside></div>
55
91
  </section>
56
92
  <footer class="page-footer"><span id="history-label">Local history · no events yet</span><span id="updated-at">Waiting for the first snapshot</span></footer>
57
93
  </main>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240" width="240" height="240"><polygon points="90.6,111.0 120.0,128.0 90.6,145.0 61.1,128.0" fill="#A5B4FC"/><polygon points="61.1,128.0 90.6,145.0 90.6,179.0 61.1,162.0" fill="#6366F1"/><polygon points="120.0,128.0 120.0,162.0 90.6,179.0 90.6,145.0" fill="#4338CA"/><polygon points="149.4,111.0 178.9,128.0 149.4,145.0 120.0,128.0" fill="#A5B4FC"/><polygon points="120.0,128.0 149.4,145.0 149.4,179.0 120.0,162.0" fill="#6366F1"/><polygon points="178.9,128.0 178.9,162.0 149.4,179.0 149.4,145.0" fill="#4338CA"/><polygon points="120,60 149.4,77.0 120,94 90.6,77.0" fill="#FCD34D"/><polygon points="90.6,77.0 120,94 120,128 90.6,111.0" fill="#F59E0B"/><polygon points="149.4,77.0 149.4,111.0 120,128 120,94" fill="#B45309"/></svg>
Binary file
Binary file