@jmanuelcorral/openteam 0.1.41 → 0.1.42

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.
Files changed (35) hide show
  1. package/README.es.md +1 -1
  2. package/README.md +1 -1
  3. package/dist/cli.js +419 -36
  4. package/dist/commands/graph.d.ts +8 -0
  5. package/dist/commands/graph.d.ts.map +1 -1
  6. package/dist/config/schema.d.ts +18 -0
  7. package/dist/config/schema.d.ts.map +1 -1
  8. package/dist/console/assets.d.ts +9 -1
  9. package/dist/console/assets.d.ts.map +1 -1
  10. package/dist/console/opencodeClient.d.ts +6 -0
  11. package/dist/console/opencodeClient.d.ts.map +1 -1
  12. package/dist/console/protocol.d.ts +8 -0
  13. package/dist/console/protocol.d.ts.map +1 -1
  14. package/dist/console/render.d.ts +25 -0
  15. package/dist/console/render.d.ts.map +1 -1
  16. package/dist/index.js +8 -2
  17. package/dist/orchestrator/worktreeAdapter.d.ts +39 -0
  18. package/dist/orchestrator/worktreeAdapter.d.ts.map +1 -0
  19. package/dist/orchestrator/worktreeDispatch.d.ts +84 -0
  20. package/dist/orchestrator/worktreeDispatch.d.ts.map +1 -0
  21. package/dist/orchestrator/worktreeReconciler.d.ts +74 -0
  22. package/dist/orchestrator/worktreeReconciler.d.ts.map +1 -0
  23. package/dist/storage/graph/worktreeRegistry.d.ts +29 -0
  24. package/dist/storage/graph/worktreeRegistry.d.ts.map +1 -0
  25. package/dist/telemetry/graphView.d.ts +75 -0
  26. package/dist/telemetry/graphView.d.ts.map +1 -0
  27. package/dist/web/console.d.ts +6 -0
  28. package/dist/web/console.d.ts.map +1 -1
  29. package/dist/web/graphRoute.d.ts +36 -0
  30. package/dist/web/graphRoute.d.ts.map +1 -0
  31. package/dist/web/server.d.ts +21 -2
  32. package/dist/web/server.d.ts.map +1 -1
  33. package/dist/web/start.d.ts +11 -0
  34. package/dist/web/start.d.ts.map +1 -1
  35. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -33,6 +33,50 @@ function eventSessionID(event) {
33
33
  }
34
34
  return str(props.sessionID) ?? str(asRecord(props.part)?.sessionID) ?? str(asRecord(props.info)?.sessionID);
35
35
  }
36
+ function partToFrame(part) {
37
+ if (part.type === "text") {
38
+ const text = str(part.text);
39
+ return text === undefined ? undefined : { kind: "text", text };
40
+ }
41
+ if (part.type === "tool") {
42
+ const tool = str(part.tool) ?? "tool";
43
+ const status = str(asRecord(part.state)?.status) ?? "run";
44
+ return { kind: "tool", text: `⚙ ${tool} · ${status}` };
45
+ }
46
+ return;
47
+ }
48
+ function messagesToFrames(raw, sessionID) {
49
+ if (!Array.isArray(raw)) {
50
+ return [];
51
+ }
52
+ const frames = [];
53
+ for (const entry of raw) {
54
+ const record = asRecord(entry);
55
+ if (record === undefined) {
56
+ continue;
57
+ }
58
+ const info = asRecord(record.info);
59
+ const entrySession = str(info?.sessionID) ?? str(record.sessionID);
60
+ if (entrySession !== undefined && entrySession !== sessionID) {
61
+ continue;
62
+ }
63
+ const parts = record.parts;
64
+ if (!Array.isArray(parts)) {
65
+ continue;
66
+ }
67
+ for (const rawPart of parts) {
68
+ const part = asRecord(rawPart);
69
+ if (part === undefined) {
70
+ continue;
71
+ }
72
+ const frame = partToFrame(part);
73
+ if (frame !== undefined) {
74
+ frames.push(frame);
75
+ }
76
+ }
77
+ }
78
+ return frames;
79
+ }
36
80
  function eventToSessionFrame(event, sessionID) {
37
81
  if (eventSessionID(event) !== sessionID) {
38
82
  return;
@@ -41,19 +85,7 @@ function eventToSessionFrame(event, sessionID) {
41
85
  switch (event.type) {
42
86
  case "message.part.updated": {
43
87
  const part = asRecord(props.part);
44
- if (part === undefined) {
45
- return;
46
- }
47
- if (part.type === "text") {
48
- const text = str(part.text);
49
- return text === undefined ? undefined : { kind: "text", text };
50
- }
51
- if (part.type === "tool") {
52
- const tool = str(part.tool) ?? "tool";
53
- const status = str(asRecord(part.state)?.status) ?? "run";
54
- return { kind: "tool", text: `⚙ ${tool} · ${status}` };
55
- }
56
- return;
88
+ return part === undefined ? undefined : partToFrame(part);
57
89
  }
58
90
  case "session.idle":
59
91
  return { kind: "status", text: "● sesión lista" };
@@ -163,6 +195,16 @@ function createConsoleClient(deps) {
163
195
  async respondPermission(sessionID, permissionID, response) {
164
196
  await post(`/session/${encodeURIComponent(sessionID)}/permissions/${encodeURIComponent(permissionID)}`, { response });
165
197
  },
198
+ async listMessages(sessionID) {
199
+ const res = await doFetch(`${base}/session/${encodeURIComponent(sessionID)}/message`, { headers: { accept: "application/json", ...authHeaders } });
200
+ let raw;
201
+ try {
202
+ raw = await res.json();
203
+ } catch {
204
+ return [];
205
+ }
206
+ return messagesToFrames(raw, sessionID);
207
+ },
166
208
  async streamFrames(sessionID, onFrame, signal) {
167
209
  const res = await doFetch(`${base}/global/event`, {
168
210
  headers: { accept: "text/event-stream", ...authHeaders },
@@ -631,7 +673,10 @@ var BaselineModeSchema = z3.enum(["auto", "pinned"]);
631
673
  var GraphModeSchema = z3.enum(["off", "shadow", "active"]);
632
674
  var GraphConfigSchema = z3.object({
633
675
  mode: GraphModeSchema.default("off"),
634
- killSwitch: z3.boolean().default(false)
676
+ killSwitch: z3.boolean().default(false),
677
+ worktrees: z3.object({
678
+ enabled: z3.boolean().default(false)
679
+ }).default({ enabled: false })
635
680
  });
636
681
  var ConsoleHostSchema = z3.enum(["127.0.0.1", "localhost"]);
637
682
  var ConsoleConfigSchema = z3.object({
@@ -645,7 +690,10 @@ var ConsoleConfigSchema = z3.object({
645
690
  terminal: z3.object({
646
691
  enabled: z3.boolean().default(true),
647
692
  pty: z3.boolean().optional()
648
- }).default({ enabled: true })
693
+ }).default({ enabled: true }),
694
+ graphView: z3.object({
695
+ enabled: z3.boolean().default(false)
696
+ }).optional()
649
697
  }).default({
650
698
  host: "127.0.0.1",
651
699
  port: 4599,
@@ -1299,6 +1347,51 @@ code{font-family:ui-monospace,Consolas,monospace;background:#0b0d11;padding:1px
1299
1347
  .pty form.ptyinput button,.pty .pty-start{background:var(--frontier);color:#1a1200;border:none;border-radius:8px;padding:8px 16px;cursor:pointer;font:inherit;font-weight:600;margin-top:8px}
1300
1348
  .pty .pty-start:disabled{opacity:.5;cursor:default}
1301
1349
  `;
1350
+ var GRAPH_STYLE = `
1351
+ .graph-wrap{overflow:auto;background:#0b0d11;border:1px solid var(--line);border-radius:8px;padding:8px}
1352
+ svg.graph{display:block}
1353
+ .gedge{stroke:#3a4250;stroke-width:1.5;marker-end:url(#garrow)}
1354
+ .gnode rect{fill:var(--panel);stroke:var(--line);stroke-width:1.5;rx:8}
1355
+ .gnode .gname{fill:var(--fg);font:600 12px ui-monospace,Consolas,monospace}
1356
+ .gnode .grole{fill:var(--muted);font-size:11px}
1357
+ .gnode .gstate{font-size:10px;text-transform:uppercase;letter-spacing:.05em}
1358
+ .gnode.state-pending rect{stroke:#6e7681}
1359
+ .gnode.state-pending .gstate{fill:#6e7681}
1360
+ .gnode.state-ready rect{stroke:var(--local)}
1361
+ .gnode.state-ready .gstate{fill:var(--local)}
1362
+ .gnode.state-active rect{stroke:var(--frontier)}
1363
+ .gnode.state-active .gstate{fill:var(--frontier)}
1364
+ .gnode.state-succeeded rect{stroke:var(--good)}
1365
+ .gnode.state-succeeded .gstate{fill:var(--good)}
1366
+ .gnode.state-failed rect{stroke:#f85149}
1367
+ .gnode.state-failed .gstate{fill:#f85149}
1368
+ .gnode.state-cancelled rect{stroke:#484f58}
1369
+ .gnode.state-cancelled .gstate{fill:#484f58}
1370
+ .gbadge{font-size:10px;font-family:ui-monospace,Consolas,monospace}
1371
+ .gbadge.badge-retry{fill:var(--frontier)}
1372
+ .gbadge.badge-error{fill:#f85149}
1373
+ .gbadge.badge-cancelled{fill:#484f58}
1374
+ .gbadge.badge-verdict.verdict-approved{fill:var(--good)}
1375
+ .gbadge.badge-verdict.verdict-rejected{fill:#f85149}
1376
+ .gbadge.badge-verdict.verdict-inconclusive{fill:var(--frontier)}
1377
+ .gbadge.badge-lineage{fill:var(--local)}
1378
+ .gbadge.badge-artifact{fill:var(--muted)}
1379
+ .gnode[data-session]{cursor:pointer}
1380
+ .gnode[data-session]:hover rect{stroke-width:2.5}
1381
+ .graph-health{display:flex;flex-wrap:wrap;gap:8px;margin:0 0 12px}
1382
+ .gchip{font-size:11px;padding:2px 10px;border-radius:10px;border:1px solid var(--line);color:var(--muted)}
1383
+ .gchip.on{font-weight:600}
1384
+ .gchip.gh-recovery.on,.gchip.gh-unknown.on{color:var(--frontier);border-color:var(--frontier)}
1385
+ .gchip.gh-conflict.on,.gchip.gh-kill.on{color:#f85149;border-color:#f85149}
1386
+ .graph-drill{margin-top:14px;border-top:1px solid var(--line);padding-top:12px}
1387
+ .graph-drill h3{font-size:13px;margin:0 0 8px;color:var(--muted);text-transform:uppercase;letter-spacing:.04em}
1388
+ .graph-drill .drilllog{background:#0b0d11;border:1px solid var(--line);border-radius:8px;padding:10px;max-height:280px;overflow:auto;font-family:ui-monospace,Consolas,monospace;font-size:12px;white-space:pre-wrap}
1389
+ .graph-drill .drilllog .tl{padding:1px 0}
1390
+ .graph-drill .drilllog .tl.tool{color:var(--frontier)}
1391
+ .graph-drill .drilllog .tl.status{color:var(--muted)}
1392
+ .graph-drill .drilllog .tl.permission{color:var(--frontier);font-weight:600}
1393
+ .graph-drill .drilllog .tl.error{color:#f85149}
1394
+ `;
1302
1395
  var CONSOLE_CLIENT_JS = `
1303
1396
  (function(){
1304
1397
  var authTok='';
@@ -1424,6 +1517,47 @@ var CONSOLE_CLIENT_JS = `
1424
1517
  ');startBtn.disabled=false}
1425
1518
  }
1426
1519
  }
1520
+ function wireGraphDrill(){
1521
+ var root=document.documentElement;
1522
+ if(root.getAttribute('data-drilldown')!=='1'){return}
1523
+ var token=root.getAttribute('data-token');
1524
+ if(!token){return}
1525
+ var drill=document.querySelector('.graph-drill');
1526
+ if(!drill){return}
1527
+ var gview=drill.closest('.view');
1528
+ if(!gview){return}
1529
+ var log=drill.querySelector('.drilllog');
1530
+ var title=drill.querySelector('.drill-title');
1531
+ if(!log){return}
1532
+ var es=null;
1533
+ function append(cls,text){
1534
+ var line=document.createElement('div');
1535
+ line.className='tl '+cls;line.textContent=text;
1536
+ log.appendChild(line);log.scrollTop=log.scrollHeight;
1537
+ }
1538
+ function open(id){
1539
+ if(es){try{es.close()}catch(_){}}
1540
+ log.textContent='';drill.hidden=false;
1541
+ if(title){title.textContent='Conversación · '+id}
1542
+ var tk='token='+encodeURIComponent(token);
1543
+ var base='/console/session/'+encodeURIComponent(id);
1544
+ fetch(base+'/messages?'+tk).then(function(r){return r.ok?r.json():Promise.reject(r.status)})
1545
+ .then(function(d){(d&&d.frames||[]).forEach(function(f){append(f.kind,f.text)})})
1546
+ .catch(function(s){append('error','✖ no se pudo cargar la conversación ('+s+')')});
1547
+ if('EventSource' in window){
1548
+ try{
1549
+ es=new EventSource(base+'/stream?'+tk);
1550
+ es.addEventListener('frame',function(e){try{var f=JSON.parse(e.data);append(f.kind,f.text)}catch(_){}});
1551
+ es.onerror=function(){/* reconecta solo */};
1552
+ }catch(_){/* ignore */}
1553
+ }
1554
+ }
1555
+ gview.addEventListener('click',function(e){
1556
+ var t=e.target;
1557
+ while(t&&t!==gview&&!(t.getAttribute&&t.getAttribute('data-session'))){t=t.parentNode}
1558
+ if(t&&t.getAttribute&&t.getAttribute('data-session')){open(t.getAttribute('data-session'))}
1559
+ });
1560
+ }
1427
1561
  var bar=document.getElementById('tabbar');
1428
1562
  if(bar){
1429
1563
  bar.addEventListener('click',function(e){
@@ -1435,6 +1569,7 @@ var CONSOLE_CLIENT_JS = `
1435
1569
  var saved='all';
1436
1570
  try{saved=sessionStorage.getItem('openteam.tab')||'all'}catch(e){}
1437
1571
  if(!selectTab(saved)){selectTab('all')}
1572
+ wireGraphDrill();
1438
1573
  })();
1439
1574
  `;
1440
1575
 
@@ -1684,14 +1819,15 @@ var STATUS_LABELS = {
1684
1819
  function statusBadge(status) {
1685
1820
  return `<span class="badge status-${status}">${STATUS_LABELS[status]}</span>`;
1686
1821
  }
1687
- function tabBar(sessions) {
1822
+ function tabBar(sessions, graphEnabled) {
1688
1823
  const allTab = '<button type="button" class="tab" data-tab="all">Todas</button>';
1824
+ const graphTab = graphEnabled ? '<button type="button" class="tab" data-tab="graph">Grafo</button>' : "";
1689
1825
  const tabs = sessions.map((tab) => {
1690
1826
  const id = escapeHtml(tab.sessionID);
1691
1827
  const short = escapeHtml(shortSessionID(tab.sessionID));
1692
1828
  return `<button type="button" class="tab" data-tab="${id}"><span class="dot status-${tab.status}"></span>${short}</button>`;
1693
1829
  }).join("");
1694
- return `<nav class="tabs" id="tabbar">${allTab}${tabs}</nav>`;
1830
+ return `<nav class="tabs" id="tabbar">${allTab}${graphTab}${tabs}</nav>`;
1695
1831
  }
1696
1832
  function shortSessionID(sessionID) {
1697
1833
  return sessionID.length <= 12 ? sessionID : `${sessionID.slice(0, 12)}…`;
@@ -1753,10 +1889,182 @@ function sessionView(tab, terminalEnabled, ptyEnabled) {
1753
1889
  ].join("");
1754
1890
  return `<div class="view" data-view="${escapeHtml(tab.sessionID)}" hidden>${body}</div>`;
1755
1891
  }
1892
+ var GRAPH_COL_W = 220;
1893
+ var GRAPH_ROW_H = 96;
1894
+ var GRAPH_NODE_W = 180;
1895
+ var GRAPH_NODE_H = 64;
1896
+ var GRAPH_PAD = 20;
1897
+ function computeLayers(projection) {
1898
+ const layer = new Map;
1899
+ for (const node of projection.nodes) {
1900
+ layer.set(node.nodeID, 0);
1901
+ }
1902
+ for (let pass = 0;pass < projection.nodes.length; pass += 1) {
1903
+ let changed = false;
1904
+ for (const edge of projection.edges) {
1905
+ const from = layer.get(edge.from);
1906
+ const to = layer.get(edge.to);
1907
+ if (from === undefined || to === undefined) {
1908
+ continue;
1909
+ }
1910
+ if (from + 1 > to) {
1911
+ layer.set(edge.to, from + 1);
1912
+ changed = true;
1913
+ }
1914
+ }
1915
+ if (!changed) {
1916
+ break;
1917
+ }
1918
+ }
1919
+ return layer;
1920
+ }
1921
+ function layoutNodes(projection) {
1922
+ const layers = computeLayers(projection);
1923
+ const byLayer = [];
1924
+ for (const node of projection.nodes) {
1925
+ const l = layers.get(node.nodeID) ?? 0;
1926
+ let bucket = byLayer[l];
1927
+ if (bucket === undefined) {
1928
+ bucket = [];
1929
+ byLayer[l] = bucket;
1930
+ }
1931
+ bucket.push(node.nodeID);
1932
+ }
1933
+ const pos = new Map;
1934
+ let maxLayer = 0;
1935
+ let maxRows = 0;
1936
+ byLayer.forEach((ids, l) => {
1937
+ if (l > maxLayer) {
1938
+ maxLayer = l;
1939
+ }
1940
+ if (ids.length > maxRows) {
1941
+ maxRows = ids.length;
1942
+ }
1943
+ ids.forEach((id, row) => {
1944
+ pos.set(id, {
1945
+ x: GRAPH_PAD + l * GRAPH_COL_W,
1946
+ y: GRAPH_PAD + row * GRAPH_ROW_H
1947
+ });
1948
+ });
1949
+ });
1950
+ const width = GRAPH_PAD * 2 + maxLayer * GRAPH_COL_W + GRAPH_NODE_W;
1951
+ const height = GRAPH_PAD * 2 + Math.max(0, maxRows - 1) * GRAPH_ROW_H + GRAPH_NODE_H;
1952
+ return { pos, width, height };
1953
+ }
1954
+ function nodeBadges(node) {
1955
+ const badges = [];
1956
+ if (node.attempts > 1) {
1957
+ badges.push({ cls: "badge-retry", text: `⟳${node.attempts}` });
1958
+ }
1959
+ if (node.lastErrorClass !== undefined) {
1960
+ badges.push({
1961
+ cls: `badge-error err-${node.lastErrorClass}`,
1962
+ text: `⚠${node.lastErrorClass}`
1963
+ });
1964
+ }
1965
+ if (node.state === "cancelled") {
1966
+ badges.push({ cls: "badge-cancelled", text: "⊘" });
1967
+ }
1968
+ if (node.review !== undefined) {
1969
+ const symbol = node.review.verdict === "approved" ? "✓" : node.review.verdict === "rejected" ? "✗" : "?";
1970
+ badges.push({
1971
+ cls: `badge-verdict verdict-${node.review.verdict}`,
1972
+ text: node.review.reassigned ? `${symbol}↔` : symbol
1973
+ });
1974
+ }
1975
+ if (node.revisionOf !== undefined) {
1976
+ badges.push({
1977
+ cls: "badge-lineage",
1978
+ text: `↺${escapeHtml(node.revisionOf)}`
1979
+ });
1980
+ }
1981
+ if (node.artifact !== undefined) {
1982
+ badges.push({ cls: "badge-artifact", text: "⧉" });
1983
+ }
1984
+ return badges;
1985
+ }
1986
+ function nodeSvg(node, at) {
1987
+ const badges = nodeBadges(node).map((badge, index) => `<text class="gbadge ${badge.cls}" x="${10 + index * 34}" y="58">${badge.text}</text>`).join("");
1988
+ const sessionAttr = node.sessionRef === undefined ? "" : ` data-session="${escapeHtml(node.sessionRef)}"`;
1989
+ return [
1990
+ `<g class="gnode state-${node.state}" data-node="${escapeHtml(node.nodeID)}" data-state="${node.state}" data-attempts="${node.attempts}"${sessionAttr} transform="translate(${at.x},${at.y})">`,
1991
+ `<rect width="${GRAPH_NODE_W}" height="${GRAPH_NODE_H}"></rect>`,
1992
+ `<text class="gname" x="10" y="20">${escapeHtml(node.nodeID)}</text>`,
1993
+ `<text class="grole" x="10" y="36">${escapeHtml(node.role)}</text>`,
1994
+ `<text class="gstate" x="10" y="50">${node.state}</text>`,
1995
+ badges,
1996
+ "</g>"
1997
+ ].join("");
1998
+ }
1999
+ function edgeSvg(pos, from, to) {
2000
+ const a = pos.get(from);
2001
+ const b = pos.get(to);
2002
+ if (a === undefined || b === undefined) {
2003
+ return "";
2004
+ }
2005
+ const x1 = a.x + GRAPH_NODE_W;
2006
+ const y1 = a.y + GRAPH_NODE_H / 2;
2007
+ const x2 = b.x;
2008
+ const y2 = b.y + GRAPH_NODE_H / 2;
2009
+ return `<line class="gedge" x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" data-from="${escapeHtml(from)}" data-to="${escapeHtml(to)}"></line>`;
2010
+ }
2011
+ function renderGraphView(projection) {
2012
+ if (projection === null) {
2013
+ return '<p class="muted">Sin run de grafo activo.</p>';
2014
+ }
2015
+ if (projection.nodes.length === 0) {
2016
+ return '<p class="muted">El run no tiene nodos.</p>';
2017
+ }
2018
+ const { pos, width, height } = layoutNodes(projection);
2019
+ const edges = projection.edges.map((edge) => edgeSvg(pos, edge.from, edge.to)).join("");
2020
+ const nodes = projection.nodes.map((node) => {
2021
+ const at = pos.get(node.nodeID);
2022
+ return at === undefined ? "" : nodeSvg(node, at);
2023
+ }).join("");
2024
+ return [
2025
+ `<div class="graph-wrap"><svg class="graph" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}" role="img" aria-label="Grafo de agentes">`,
2026
+ '<defs><marker id="garrow" viewBox="0 0 8 8" refX="7" refY="4" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L8 4 L0 8 z" fill="#3a4250"></path></marker></defs>',
2027
+ edges,
2028
+ nodes,
2029
+ "</svg></div>"
2030
+ ].join("");
2031
+ }
2032
+ function healthBanner(health) {
2033
+ const chip = (cls, on, label) => `<span class="gchip ${cls} ${on ? "on" : "off"}">${label}</span>`;
2034
+ return [
2035
+ '<div class="graph-health" role="status">',
2036
+ chip("gh-recovery", health.recoveryRequired, health.recoveryRequired ? "recuperación requerida" : "sin recuperación"),
2037
+ chip("gh-unknown", health.unknownEffects > 0, `efectos desconocidos: ${health.unknownEffects}`),
2038
+ chip("gh-conflict", health.conflicts > 0, `conflictos lease/worktree: ${health.conflicts}`),
2039
+ chip("gh-kill", health.killSwitch, health.killSwitch ? "kill switch activo" : "kill switch inactivo"),
2040
+ "</div>"
2041
+ ].join("");
2042
+ }
2043
+ function drillPanel() {
2044
+ return [
2045
+ '<aside class="graph-drill" hidden>',
2046
+ '<h3 class="drill-title">Conversación</h3>',
2047
+ '<div class="drilllog" aria-live="polite"></div>',
2048
+ "</aside>"
2049
+ ].join("");
2050
+ }
2051
+ function graphViewPanel(projection, drilldownEnabled) {
2052
+ return [
2053
+ '<div class="view" data-view="graph" hidden>',
2054
+ '<section class="panel" id="panel-graph">',
2055
+ "<h2>Grafo de agentes</h2>",
2056
+ projection === null ? "" : healthBanner(projection.health),
2057
+ renderGraphView(projection),
2058
+ drilldownEnabled ? drillPanel() : "",
2059
+ "</section>",
2060
+ "</div>"
2061
+ ].join("");
2062
+ }
1756
2063
  function renderConsoleHtml(state, options = {}) {
1757
2064
  const refreshMs = options.refreshMs ?? 2000;
1758
2065
  const terminalEnabled = options.terminalEnabled === true;
1759
2066
  const ptyEnabled = options.ptyEnabled === true;
2067
+ const graphEnabled = options.graphView !== undefined;
1760
2068
  const allBody = [
1761
2069
  summaryPanel(state.cost),
1762
2070
  totalsPanel(state.totals),
@@ -1770,10 +2078,12 @@ function renderConsoleHtml(state, options = {}) {
1770
2078
  activityPanel(state.activity)
1771
2079
  ].join("");
1772
2080
  const allView = `<div class="view" data-view="all">${allBody}</div>`;
2081
+ const graphDrilldown = graphEnabled && options.drilldownEnabled === true;
2082
+ const graphView = graphEnabled ? graphViewPanel(options.graphView?.projection ?? null, graphDrilldown) : "";
1773
2083
  const sessionViews = state.sessionsDetail.map((tab) => sessionView(tab, terminalEnabled, ptyEnabled)).join("");
1774
- const body = allView + sessionViews;
2084
+ const body = allView + graphView + sessionViews;
1775
2085
  const session = state.session.id === undefined ? "" : ` · sesión ${escapeHtml(state.session.id)}`;
1776
- const tokenAttr = terminalEnabled && options.consoleToken !== undefined ? ` data-token="${escapeHtml(options.consoleToken)}" data-terminal="1"${ptyEnabled ? ' data-pty="1"' : ""}` : "";
2086
+ const tokenAttr = (terminalEnabled || graphDrilldown) && options.consoleToken !== undefined ? ` data-token="${escapeHtml(options.consoleToken)}"${terminalEnabled ? ' data-terminal="1"' : ""}${ptyEnabled ? ' data-pty="1"' : ""}${graphDrilldown ? ' data-drilldown="1"' : ""}` : "";
1777
2087
  return [
1778
2088
  "<!doctype html>",
1779
2089
  `<html lang="es" data-generated="${escapeHtml(state.generatedAt)}" data-refresh="${refreshMs}"${tokenAttr}>`,
@@ -1781,14 +2091,14 @@ function renderConsoleHtml(state, options = {}) {
1781
2091
  '<meta charset="utf-8">',
1782
2092
  '<meta name="viewport" content="width=device-width,initial-scale=1">',
1783
2093
  "<title>openteam console</title>",
1784
- `<style>${CONSOLE_STYLE}</style>`,
2094
+ `<style>${CONSOLE_STYLE}${graphEnabled ? GRAPH_STYLE : ""}</style>`,
1785
2095
  "</head>",
1786
2096
  "<body>",
1787
2097
  "<header>",
1788
2098
  "<h1>openteam console</h1>",
1789
2099
  `<span class="meta">actualizado ${escapeHtml(state.generatedAt)}${session}</span>`,
1790
2100
  "</header>",
1791
- tabBar(state.sessionsDetail),
2101
+ tabBar(state.sessionsDetail, graphEnabled),
1792
2102
  `<main>${body}</main>`,
1793
2103
  `<script>${CONSOLE_CLIENT_JS}</script>`,
1794
2104
  "</body>",
@@ -2120,7 +2430,7 @@ async function readJsonBody(request, limit) {
2120
2430
  });
2121
2431
  }
2122
2432
  function parseConsolePath(path) {
2123
- const match = /^\/console\/session\/([^/]+)\/(stream|input|permission|pty)$/.exec(path);
2433
+ const match = /^\/console\/session\/([^/]+)\/(stream|input|permission|pty|messages)$/.exec(path);
2124
2434
  if (match === null) {
2125
2435
  return;
2126
2436
  }
@@ -2150,6 +2460,15 @@ data: {}
2150
2460
  response.end();
2151
2461
  }
2152
2462
  }
2463
+ async function handleMessages(_request, response, sessionID, deps) {
2464
+ const endpoint = await deps.resolveEndpoint(sessionID);
2465
+ if (endpoint === undefined) {
2466
+ response.writeHead(404, JSON_HEADERS).end(JSON.stringify({ error: "no endpoint" }));
2467
+ return;
2468
+ }
2469
+ const frames = await deps.createClient(endpoint).listMessages(sessionID);
2470
+ response.writeHead(200, JSON_HEADERS).end(JSON.stringify({ frames }));
2471
+ }
2153
2472
  async function handleInput(request, response, sessionID, deps) {
2154
2473
  const endpoint = await deps.resolveEndpoint(sessionID);
2155
2474
  if (endpoint === undefined) {
@@ -2217,7 +2536,9 @@ async function handleConsoleRoute(request, response, url, deps) {
2217
2536
  response.writeHead(404, JSON_HEADERS).end(JSON.stringify({ error: "not found" }));
2218
2537
  return true;
2219
2538
  }
2220
- if (!deps.terminalEnabled) {
2539
+ const readOnlyAction = route.action === "stream" || route.action === "messages";
2540
+ const channelEnabled = deps.terminalEnabled || readOnlyAction && deps.drilldownEnabled === true;
2541
+ if (!channelEnabled) {
2221
2542
  response.writeHead(403, JSON_HEADERS).end(JSON.stringify({ error: "terminal disabled" }));
2222
2543
  return true;
2223
2544
  }
@@ -2229,6 +2550,10 @@ async function handleConsoleRoute(request, response, url, deps) {
2229
2550
  await handleStream(request, response, route.sessionID, deps);
2230
2551
  return true;
2231
2552
  }
2553
+ if (route.action === "messages" && request.method === "GET") {
2554
+ await handleMessages(request, response, route.sessionID, deps);
2555
+ return true;
2556
+ }
2232
2557
  if (route.action === "input" && request.method === "POST") {
2233
2558
  await handleInput(request, response, route.sessionID, deps);
2234
2559
  return true;
@@ -2245,10 +2570,15 @@ async function handleConsoleRoute(request, response, url, deps) {
2245
2570
  return true;
2246
2571
  }
2247
2572
 
2248
- // src/web/storageRoute.ts
2573
+ // src/web/graphRoute.ts
2249
2574
  var JSON_HEADERS2 = { "content-type": "application/json" };
2250
- var READ_OPS = new Set(["read", "exists", "list", "isDirectory", "stat"]);
2251
- var WRITE_OPS = new Set(["write", "append", "mkdir", "delete"]);
2575
+ var GRAPH_SSE_EVENT = "graph";
2576
+ function graphSseMessage(json) {
2577
+ return `event: ${GRAPH_SSE_EVENT}
2578
+ data: ${json}
2579
+
2580
+ `;
2581
+ }
2252
2582
  function bearerFromHeader2(request) {
2253
2583
  const header = request.headers.authorization;
2254
2584
  if (typeof header !== "string" || !header.startsWith("Bearer ")) {
@@ -2261,6 +2591,39 @@ function authorized2(request, query, token) {
2261
2591
  const provided = tokenFromQuery(query) ?? bearerFromHeader2(request);
2262
2592
  return provided !== undefined && safeEqualToken(provided, token);
2263
2593
  }
2594
+ async function handleGraphRoute(request, response, url, deps) {
2595
+ if (url.pathname !== "/api/graph") {
2596
+ return false;
2597
+ }
2598
+ if (request.method !== "GET") {
2599
+ response.writeHead(405, JSON_HEADERS2).end(JSON.stringify({ error: "method not allowed" }));
2600
+ return true;
2601
+ }
2602
+ if (deps.remoteToken !== undefined && !authorized2(request, url.searchParams, deps.remoteToken)) {
2603
+ response.writeHead(401, JSON_HEADERS2).end(JSON.stringify({ error: "unauthorized" }));
2604
+ return true;
2605
+ }
2606
+ const view = await deps.readView();
2607
+ response.writeHead(200, JSON_HEADERS2).end(JSON.stringify(view));
2608
+ return true;
2609
+ }
2610
+
2611
+ // src/web/storageRoute.ts
2612
+ var JSON_HEADERS3 = { "content-type": "application/json" };
2613
+ var READ_OPS = new Set(["read", "exists", "list", "isDirectory", "stat"]);
2614
+ var WRITE_OPS = new Set(["write", "append", "mkdir", "delete"]);
2615
+ function bearerFromHeader3(request) {
2616
+ const header = request.headers.authorization;
2617
+ if (typeof header !== "string" || !header.startsWith("Bearer ")) {
2618
+ return;
2619
+ }
2620
+ const value = header.slice("Bearer ".length).trim();
2621
+ return value.length === 0 ? undefined : value;
2622
+ }
2623
+ function authorized3(request, query, token) {
2624
+ const provided = tokenFromQuery(query) ?? bearerFromHeader3(request);
2625
+ return provided !== undefined && safeEqualToken(provided, token);
2626
+ }
2264
2627
  function pathAllowed(path, allowPrefixes) {
2265
2628
  if (allowPrefixes === undefined) {
2266
2629
  return true;
@@ -2300,7 +2663,7 @@ async function readJsonBody2(request, limit) {
2300
2663
  });
2301
2664
  }
2302
2665
  function sendJson(response, status, body) {
2303
- response.writeHead(status, JSON_HEADERS2).end(JSON.stringify(body));
2666
+ response.writeHead(status, JSON_HEADERS3).end(JSON.stringify(body));
2304
2667
  }
2305
2668
  async function handleRead(response, op, path, storage) {
2306
2669
  if (op === "read") {
@@ -2374,7 +2737,7 @@ async function handleStorageRoute(request, response, url, deps) {
2374
2737
  sendJson(response, 404, { error: "not found" });
2375
2738
  return true;
2376
2739
  }
2377
- if (!authorized2(request, url.searchParams, deps.token)) {
2740
+ if (!authorized3(request, url.searchParams, deps.token)) {
2378
2741
  sendJson(response, 401, { error: "unauthorized" });
2379
2742
  return true;
2380
2743
  }
@@ -2415,12 +2778,12 @@ var SSE_HEADERS2 = {
2415
2778
  "cache-control": "no-cache",
2416
2779
  connection: "keep-alive"
2417
2780
  };
2418
- var JSON_HEADERS3 = { "content-type": "application/json" };
2781
+ var JSON_HEADERS4 = { "content-type": "application/json" };
2419
2782
  var HTML_HEADERS = { "content-type": "text/html; charset=utf-8" };
2420
2783
  function isAddressInUse(error) {
2421
2784
  return typeof error === "object" && error !== null && "code" in error && error.code === "EADDRINUSE";
2422
2785
  }
2423
- function bearerFromHeader3(request) {
2786
+ function bearerFromHeader4(request) {
2424
2787
  const header = request.headers.authorization;
2425
2788
  if (typeof header !== "string" || !header.startsWith("Bearer ")) {
2426
2789
  return;
@@ -2429,16 +2792,19 @@ function bearerFromHeader3(request) {
2429
2792
  return value.length === 0 ? undefined : value;
2430
2793
  }
2431
2794
  function readAuthorized(request, url, token) {
2432
- const provided = tokenFromQuery(url.searchParams) ?? bearerFromHeader3(request);
2795
+ const provided = tokenFromQuery(url.searchParams) ?? bearerFromHeader4(request);
2433
2796
  return provided !== undefined && safeEqualToken(provided, token);
2434
2797
  }
2435
2798
  async function renderState(deps) {
2799
+ const graphView = deps.graphApi === undefined ? undefined : { projection: await deps.graphApi.readView() };
2436
2800
  return renderConsoleHtml(buildConsoleState(await deps.readSnapshot()), {
2437
2801
  refreshMs: deps.config.refreshMs,
2802
+ ...graphView !== undefined ? { graphView } : {},
2438
2803
  ...deps.console !== undefined ? {
2439
2804
  consoleToken: deps.console.token,
2440
2805
  terminalEnabled: deps.console.terminalEnabled,
2441
- ...deps.console.ptyEnabled !== undefined ? { ptyEnabled: deps.console.ptyEnabled } : {}
2806
+ ...deps.console.ptyEnabled !== undefined ? { ptyEnabled: deps.console.ptyEnabled } : {},
2807
+ ...deps.console.drilldownEnabled !== undefined ? { drilldownEnabled: deps.console.drilldownEnabled } : {}
2442
2808
  } : {}
2443
2809
  });
2444
2810
  }
@@ -2484,13 +2850,21 @@ async function createConsoleServer(deps) {
2484
2850
  resolveEndpoint,
2485
2851
  createClient: deps.console.createClient,
2486
2852
  ...deps.console.ptyEnabled !== undefined ? { ptyEnabled: deps.console.ptyEnabled } : {},
2853
+ ...deps.console.drilldownEnabled !== undefined ? { drilldownEnabled: deps.console.drilldownEnabled } : {},
2487
2854
  ...deps.console.createPtyClient !== undefined ? { createPtyClient: deps.console.createPtyClient } : {}
2488
2855
  };
2856
+ const graphDeps = deps.graphApi === undefined ? undefined : {
2857
+ readView: deps.graphApi.readView,
2858
+ ...deps.storageApi?.token !== undefined ? { remoteToken: deps.storageApi.token } : {}
2859
+ };
2489
2860
  const handle = async (request, response) => {
2490
2861
  const url2 = new URL(request.url ?? "/", "http://localhost");
2491
2862
  if (consoleDeps !== undefined && await handleConsoleRoute(request, response, url2, consoleDeps)) {
2492
2863
  return;
2493
2864
  }
2865
+ if (graphDeps !== undefined && await handleGraphRoute(request, response, url2, graphDeps)) {
2866
+ return;
2867
+ }
2494
2868
  if (deps.storageApi !== undefined && await handleStorageRoute(request, response, url2, deps.storageApi)) {
2495
2869
  return;
2496
2870
  }
@@ -2501,7 +2875,7 @@ async function createConsoleServer(deps) {
2501
2875
  const path = url2.pathname;
2502
2876
  const remoteToken = deps.storageApi?.token;
2503
2877
  if (remoteToken !== undefined && (path === "/" || path === "/index.html" || path === "/api/state" || path === "/events") && !readAuthorized(request, url2, remoteToken)) {
2504
- response.writeHead(401, JSON_HEADERS3).end(JSON.stringify({ error: "unauthorized" }));
2878
+ response.writeHead(401, JSON_HEADERS4).end(JSON.stringify({ error: "unauthorized" }));
2505
2879
  return;
2506
2880
  }
2507
2881
  if (path === "/" || path === "/index.html") {
@@ -2509,11 +2883,11 @@ async function createConsoleServer(deps) {
2509
2883
  return;
2510
2884
  }
2511
2885
  if (path === "/api/state") {
2512
- response.writeHead(200, JSON_HEADERS3).end(await stateJson(deps));
2886
+ response.writeHead(200, JSON_HEADERS4).end(await stateJson(deps));
2513
2887
  return;
2514
2888
  }
2515
2889
  if (path === "/healthz") {
2516
- response.writeHead(200, JSON_HEADERS3).end(JSON.stringify({ ok: true }));
2890
+ response.writeHead(200, JSON_HEADERS4).end(JSON.stringify({ ok: true }));
2517
2891
  return;
2518
2892
  }
2519
2893
  if (path === "/favicon.ico") {
@@ -2523,6 +2897,9 @@ async function createConsoleServer(deps) {
2523
2897
  if (path === "/events") {
2524
2898
  response.writeHead(200, SSE_HEADERS2);
2525
2899
  response.write(sseMessage(await stateJson(deps)));
2900
+ if (deps.graphApi !== undefined) {
2901
+ response.write(graphSseMessage(JSON.stringify(await deps.graphApi.readView())));
2902
+ }
2526
2903
  clients.add(response);
2527
2904
  request.on("close", () => {
2528
2905
  clients.delete(response);
@@ -2563,8 +2940,12 @@ async function createConsoleServer(deps) {
2563
2940
  return;
2564
2941
  }
2565
2942
  const message = sseMessage(await stateJson(deps));
2943
+ const graphMessage = deps.graphApi !== undefined ? graphSseMessage(JSON.stringify(await deps.graphApi.readView())) : undefined;
2566
2944
  for (const response of clients) {
2567
2945
  response.write(message);
2946
+ if (graphMessage !== undefined) {
2947
+ response.write(graphMessage);
2948
+ }
2568
2949
  }
2569
2950
  };
2570
2951
  const close = () => {
@@ -2895,12 +3276,14 @@ async function createConsoleRuntime(deps) {
2895
3276
  ...deps.session !== undefined ? { session: deps.session } : {}
2896
3277
  });
2897
3278
  const createServer = deps.serve ?? createConsoleServer;
3279
+ const graphApi = deps.config.graphView?.enabled === true && deps.graphView !== undefined ? { readView: deps.graphView.readView } : undefined;
2898
3280
  const server = await createServer({
2899
3281
  readSnapshot,
2900
3282
  config: deps.config,
2901
3283
  ...deps.log !== undefined ? { log: deps.log } : {},
2902
3284
  ...deps.console !== undefined ? { console: deps.console } : {},
2903
- ...deps.storageApi !== undefined ? { storageApi: deps.storageApi } : {}
3285
+ ...deps.storageApi !== undefined ? { storageApi: deps.storageApi } : {},
3286
+ ...graphApi !== undefined ? { graphApi } : {}
2904
3287
  });
2905
3288
  const watchFn = deps.watch ?? ((path, listener) => fsWatch(path, { persistent: false }, listener));
2906
3289
  const watcher = watchSources([deps.sessionsDir, decisionsPath, backlogPath, deps.agentDir], () => {