open-managed-agents 0.1.0
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/LICENSE +93 -0
- package/README.md +248 -0
- package/bin/oma.mjs +499 -0
- package/dist/scripts/alpha-openai-compatible-fixture.mjs +134 -0
- package/dist/scripts/alpha-smoke-models.mjs +92 -0
- package/dist/scripts/alpha-smoke.mjs +581 -0
- package/dist/scripts/oma-doctor.js +230 -0
- package/dist/scripts/oma-models.js +299 -0
- package/dist/scripts/oma-workspaces.js +133 -0
- package/dist/src/control-plane/admin/auth.js +43 -0
- package/dist/src/control-plane/admin/routes.js +76 -0
- package/dist/src/control-plane/admin/service.js +102 -0
- package/dist/src/control-plane/admission.js +90 -0
- package/dist/src/control-plane/agents/routes.js +63 -0
- package/dist/src/control-plane/agents/service.js +687 -0
- package/dist/src/control-plane/agents/store.js +391 -0
- package/dist/src/control-plane/agents/types.js +1 -0
- package/dist/src/control-plane/api-constants.js +3 -0
- package/dist/src/control-plane/app.js +907 -0
- package/dist/src/control-plane/console/auth.js +68 -0
- package/dist/src/control-plane/console/static.js +112 -0
- package/dist/src/control-plane/deployment-runtime-config.js +328 -0
- package/dist/src/control-plane/deployment-runtime-event-coordinator.js +25 -0
- package/dist/src/control-plane/deployment-runtime-turn-guard.js +14 -0
- package/dist/src/control-plane/deployment-session-coordinator.js +25 -0
- package/dist/src/control-plane/deployment-session-output-coordinator.js +27 -0
- package/dist/src/control-plane/deployment-storage.js +354 -0
- package/dist/src/control-plane/egress/image.js +48 -0
- package/dist/src/control-plane/egress/policy.js +571 -0
- package/dist/src/control-plane/egress/presets.js +77 -0
- package/dist/src/control-plane/egress/proxy.js +72 -0
- package/dist/src/control-plane/egress/ssrf.js +106 -0
- package/dist/src/control-plane/egress/utils/debug.js +14 -0
- package/dist/src/control-plane/egress/vendor/http-proxy.js +335 -0
- package/dist/src/control-plane/egress/vendor/mitm-ca.js +226 -0
- package/dist/src/control-plane/egress/vendor/mitm-leaf.js +124 -0
- package/dist/src/control-plane/egress/vendor/parent-proxy.js +438 -0
- package/dist/src/control-plane/egress/vendor/request-filter.js +105 -0
- package/dist/src/control-plane/egress/vendor/sandbox-config.js +1 -0
- package/dist/src/control-plane/egress/vendor/tls-terminate-proxy.js +269 -0
- package/dist/src/control-plane/environments/routes.js +54 -0
- package/dist/src/control-plane/environments/service.js +112 -0
- package/dist/src/control-plane/environments/store.js +132 -0
- package/dist/src/control-plane/environments/types.js +1 -0
- package/dist/src/control-plane/errors.js +81 -0
- package/dist/src/control-plane/events/broadcaster.js +217 -0
- package/dist/src/control-plane/events/constants.js +2 -0
- package/dist/src/control-plane/events/persist.js +39 -0
- package/dist/src/control-plane/events/request.js +190 -0
- package/dist/src/control-plane/events/routes.js +159 -0
- package/dist/src/control-plane/events/runtime-helpers.js +134 -0
- package/dist/src/control-plane/events/service.js +1985 -0
- package/dist/src/control-plane/events/session-guards.js +29 -0
- package/dist/src/control-plane/events/sse.js +6 -0
- package/dist/src/control-plane/events/store.js +772 -0
- package/dist/src/control-plane/events/tool-persistence.js +102 -0
- package/dist/src/control-plane/events/types.js +21 -0
- package/dist/src/control-plane/files/routes.js +121 -0
- package/dist/src/control-plane/files/service.js +65 -0
- package/dist/src/control-plane/files/store-common.js +87 -0
- package/dist/src/control-plane/files/store-local.js +488 -0
- package/dist/src/control-plane/files/store-memory.js +270 -0
- package/dist/src/control-plane/files/store.js +2 -0
- package/dist/src/control-plane/files/types.js +7 -0
- package/dist/src/control-plane/http.js +25 -0
- package/dist/src/control-plane/ids.js +43 -0
- package/dist/src/control-plane/logging.js +263 -0
- package/dist/src/control-plane/models/auth-storage-backend.js +238 -0
- package/dist/src/control-plane/models/catalog.js +78 -0
- package/dist/src/control-plane/models/config-security.js +376 -0
- package/dist/src/control-plane/models/deployment-config.js +93 -0
- package/dist/src/control-plane/models/routes.js +32 -0
- package/dist/src/control-plane/models/service.js +138 -0
- package/dist/src/control-plane/observability/instruments.js +65 -0
- package/dist/src/control-plane/observability/metrics.js +125 -0
- package/dist/src/control-plane/observability/routes.js +95 -0
- package/dist/src/control-plane/openapi/document.js +282 -0
- package/dist/src/control-plane/openapi/routes.js +78 -0
- package/dist/src/control-plane/request-idempotency.js +85 -0
- package/dist/src/control-plane/secrets/envelope.js +132 -0
- package/dist/src/control-plane/secrets/master-key.js +44 -0
- package/dist/src/control-plane/secrets/routes.js +20 -0
- package/dist/src/control-plane/secrets/service.js +71 -0
- package/dist/src/control-plane/secrets/store.js +212 -0
- package/dist/src/control-plane/secrets/types.js +3 -0
- package/dist/src/control-plane/sessions/pi/custom-tools.js +146 -0
- package/dist/src/control-plane/sessions/pi/mcp/bridge.js +527 -0
- package/dist/src/control-plane/sessions/pi/mcp/client.js +157 -0
- package/dist/src/control-plane/sessions/pi/mcp/credential.js +28 -0
- package/dist/src/control-plane/sessions/pi/mcp/fetch.js +69 -0
- package/dist/src/control-plane/sessions/pi/mcp/probe.js +202 -0
- package/dist/src/control-plane/sessions/pi/mcp/runtime.js +13 -0
- package/dist/src/control-plane/sessions/pi/runner.js +1104 -0
- package/dist/src/control-plane/sessions/pi/sandbox/cma-glob.js +388 -0
- package/dist/src/control-plane/sessions/pi/sandbox/cma-grep.js +124 -0
- package/dist/src/control-plane/sessions/pi/sandbox/docker-egress.js +347 -0
- package/dist/src/control-plane/sessions/pi/sandbox/docker.js +1589 -0
- package/dist/src/control-plane/sessions/pi/sandbox/glob.js +48 -0
- package/dist/src/control-plane/sessions/pi/sandbox/image.js +7 -0
- package/dist/src/control-plane/sessions/pi/sandbox/microsandbox.js +1677 -0
- package/dist/src/control-plane/sessions/pi/sandbox/provider.js +561 -0
- package/dist/src/control-plane/sessions/pi/sandbox/selection.js +168 -0
- package/dist/src/control-plane/sessions/pi/span-normalizer.js +82 -0
- package/dist/src/control-plane/sessions/pi/tool-permissions.js +392 -0
- package/dist/src/control-plane/sessions/pi/translator.js +100 -0
- package/dist/src/control-plane/sessions/request.js +164 -0
- package/dist/src/control-plane/sessions/resources.js +63 -0
- package/dist/src/control-plane/sessions/routes.js +88 -0
- package/dist/src/control-plane/sessions/serialize.js +24 -0
- package/dist/src/control-plane/sessions/service.js +733 -0
- package/dist/src/control-plane/sessions/store.js +538 -0
- package/dist/src/control-plane/sessions/types.js +1 -0
- package/dist/src/control-plane/skills/archive.js +169 -0
- package/dist/src/control-plane/skills/routes.js +41 -0
- package/dist/src/control-plane/skills/service.js +29 -0
- package/dist/src/control-plane/skills/store.js +233 -0
- package/dist/src/control-plane/skills/types.js +5 -0
- package/dist/src/control-plane/sqlite-transaction.js +15 -0
- package/dist/src/control-plane/vaults/mcp-oauth-validate.js +165 -0
- package/dist/src/control-plane/vaults/oauth-refresh-ticker.js +43 -0
- package/dist/src/control-plane/vaults/oauth-refresh.js +448 -0
- package/dist/src/control-plane/vaults/routes.js +66 -0
- package/dist/src/control-plane/vaults/service.js +553 -0
- package/dist/src/control-plane/vaults/store.js +691 -0
- package/dist/src/control-plane/vaults/types.js +1 -0
- package/dist/src/control-plane/wake-loop.js +65 -0
- package/dist/src/control-plane/wiring.js +92 -0
- package/dist/src/control-plane/workspace.js +4 -0
- package/dist/src/control-plane/workspaces/store.js +158 -0
- package/dist/src/egress-proxy-main.js +103 -0
- package/dist/src/main.js +124 -0
- package/dist/src/types/agents.js +1 -0
- package/dist/src/types/common.js +1 -0
- package/dist/src/types/environments.js +1 -0
- package/dist/src/types/events.js +121 -0
- package/dist/src/types/files.js +1 -0
- package/dist/src/types/json.js +19 -0
- package/dist/src/types/sessions.js +1 -0
- package/dist/ui/managed-agents-console/README.md +52 -0
- package/dist/ui/managed-agents-console/docs/agents.md +23 -0
- package/dist/ui/managed-agents-console/docs/console.md +23 -0
- package/dist/ui/managed-agents-console/docs/dreams.md +5 -0
- package/dist/ui/managed-agents-console/docs/environments.md +27 -0
- package/dist/ui/managed-agents-console/docs/events.md +19 -0
- package/dist/ui/managed-agents-console/docs/files.md +17 -0
- package/dist/ui/managed-agents-console/docs/github.md +11 -0
- package/dist/ui/managed-agents-console/docs/integrations.md +19 -0
- package/dist/ui/managed-agents-console/docs/memory.md +11 -0
- package/dist/ui/managed-agents-console/docs/migration.md +17 -0
- package/dist/ui/managed-agents-console/docs/multiagent.md +11 -0
- package/dist/ui/managed-agents-console/docs/outcomes.md +11 -0
- package/dist/ui/managed-agents-console/docs/overview.md +41 -0
- package/dist/ui/managed-agents-console/docs/permissions.md +24 -0
- package/dist/ui/managed-agents-console/docs/quickstart.md +51 -0
- package/dist/ui/managed-agents-console/docs/reference.md +27 -0
- package/dist/ui/managed-agents-console/docs/sandbox-reference.md +19 -0
- package/dist/ui/managed-agents-console/docs/sandbox-security.md +15 -0
- package/dist/ui/managed-agents-console/docs/scheduled-deployments.md +5 -0
- package/dist/ui/managed-agents-console/docs/self-hosted-sandboxes.md +15 -0
- package/dist/ui/managed-agents-console/docs/session-operations.md +21 -0
- package/dist/ui/managed-agents-console/docs/sessions.md +21 -0
- package/dist/ui/managed-agents-console/docs/skills.md +15 -0
- package/dist/ui/managed-agents-console/docs/tools.md +23 -0
- package/dist/ui/managed-agents-console/docs/vaults.md +19 -0
- package/dist/ui/managed-agents-console/docs/webhooks.md +11 -0
- package/dist/ui/managed-agents-console/index.html +47 -0
- package/dist/ui/managed-agents-console/serve.mjs +116 -0
- package/dist/ui/managed-agents-console/src/agents-files.jsx +273 -0
- package/dist/ui/managed-agents-console/src/api.js +1043 -0
- package/dist/ui/managed-agents-console/src/app.jsx +633 -0
- package/dist/ui/managed-agents-console/src/auth.jsx +215 -0
- package/dist/ui/managed-agents-console/src/console.css +733 -0
- package/dist/ui/managed-agents-console/src/data.js +188 -0
- package/dist/ui/managed-agents-console/src/detail.jsx +719 -0
- package/dist/ui/managed-agents-console/src/docs.jsx +200 -0
- package/dist/ui/managed-agents-console/src/environments.jsx +365 -0
- package/dist/ui/managed-agents-console/src/forms.jsx +494 -0
- package/dist/ui/managed-agents-console/src/icons.jsx +48 -0
- package/dist/ui/managed-agents-console/src/skills.jsx +21 -0
- package/dist/ui/managed-agents-console/src/sse.js +185 -0
- package/dist/ui/managed-agents-console/src/states.jsx +90 -0
- package/dist/ui/managed-agents-console/src/tweaks-panel.jsx +541 -0
- package/dist/ui/managed-agents-console/src/ui.jsx +190 -0
- package/dist/ui/managed-agents-console/src/vaults-data.js +89 -0
- package/dist/ui/managed-agents-console/src/vaults.jsx +171 -0
- package/dist/ui/managed-agents-console/vendor/babel.min.js +4 -0
- package/dist/ui/managed-agents-console/vendor/react-dom.production.min.js +267 -0
- package/dist/ui/managed-agents-console/vendor/react.production.min.js +31 -0
- package/dist/ui/openapi-docs/VENDOR.md +27 -0
- package/dist/ui/openapi-docs/index.html +16 -0
- package/dist/ui/openapi-docs/swagger-initializer.js +14 -0
- package/dist/ui/openapi-docs/vendor/LICENSE +202 -0
- package/dist/ui/openapi-docs/vendor/NOTICE +2 -0
- package/dist/ui/openapi-docs/vendor/swagger-ui-bundle.js +2 -0
- package/dist/ui/openapi-docs/vendor/swagger-ui-standalone-preset.js +2 -0
- package/dist/ui/openapi-docs/vendor/swagger-ui.css +3 -0
- package/package.json +53 -0
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
// detail.jsx — Session detail (split-pane / three-column) → window.SessionDetail
|
|
2
|
+
const { useState: useStateD, useEffect: useEffectD, useRef: useRefD } = React;
|
|
3
|
+
|
|
4
|
+
function Role({ r }) {
|
|
5
|
+
const L = { user:'User', agent:'Agent', tool:'Tool', span:'Span', sys:'Sys' };
|
|
6
|
+
return <span className={'role ' + r}>{L[r]}</span>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function EvRow({ e, sel, onClick, mode }) {
|
|
10
|
+
const errored = e.ok === false;
|
|
11
|
+
return (
|
|
12
|
+
<div className={'ev' + (sel ? ' sel' : '')} onClick={onClick}>
|
|
13
|
+
<Role r={e.role} />
|
|
14
|
+
{mode === 'debug'
|
|
15
|
+
? <>
|
|
16
|
+
<span className="mono ev-type" style={{ width:186, flex:'0 0 auto', whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{e.type}</span>
|
|
17
|
+
<Pill>{e.tag}</Pill>
|
|
18
|
+
{e.ok && <span className="badge st-active"><i className="dot" />ok</span>}
|
|
19
|
+
{errored && <span className="badge st-error"><i className="dot" />error</span>}
|
|
20
|
+
{e.open && <span className="badge st-open"><i className="dot" />open</span>}
|
|
21
|
+
<span className="grow" />
|
|
22
|
+
</>
|
|
23
|
+
: <span className="ev-text">{e.text}</span>}
|
|
24
|
+
{e.tokens && <span className="ev-tok mono">{e.tokens}</span>}
|
|
25
|
+
<span className="ev-time mono">{e.time}</span>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function Inspector({ e, onClose }) {
|
|
31
|
+
if (!e) return (
|
|
32
|
+
<div className="panel inspector"><div className="empty">Select an event to inspect.</div></div>
|
|
33
|
+
);
|
|
34
|
+
return (
|
|
35
|
+
<div className="panel inspector fade-in" key={e.id}>
|
|
36
|
+
<div className="insp-head">
|
|
37
|
+
<Role r={e.role} />
|
|
38
|
+
<h3 className="mono" style={{ fontSize:14 }}>{e.type.split('.').pop()}</h3>
|
|
39
|
+
<span className="grow" />
|
|
40
|
+
{e.ok && <span className="badge st-active"><i className="dot" />ok</span>}
|
|
41
|
+
{e.ok === false && <span className="badge st-error"><i className="dot" />error</span>}
|
|
42
|
+
{e.open && <span className="badge st-open"><i className="dot" />open</span>}
|
|
43
|
+
<span className="kebab" onClick={onClose}><Icon name="x" size={16} /></span>
|
|
44
|
+
</div>
|
|
45
|
+
<div className="insp-body scroll" style={{ overflow:'auto' }}>
|
|
46
|
+
<div className="mono" style={{ fontSize:11.5, color:'var(--faint)' }}>{e.id} · {e.time}</div>
|
|
47
|
+
{e.usage && <>
|
|
48
|
+
<div className="divider" />
|
|
49
|
+
<div className="kv"><span className="k">duration</span><span className="v mono">{e.dur}</span></div>
|
|
50
|
+
<div className="kv"><span className="k">paired start</span><span className="v mono">{e.pairedStart}</span></div>
|
|
51
|
+
<div className="sec-label" style={{ marginTop:4 }}>model usage</div>
|
|
52
|
+
{Object.entries(e.usage).map(([k, v]) => (
|
|
53
|
+
<div className="kv" key={k}><span className="k">{k.replace(/([A-Z])/g, ' $1').toLowerCase()}</span><span className="v mono">{v}</span></div>
|
|
54
|
+
))}
|
|
55
|
+
</>}
|
|
56
|
+
{e.content && <>
|
|
57
|
+
<div className="divider" />
|
|
58
|
+
<div className="sec-label">content</div>
|
|
59
|
+
<div className="content-block" style={{ whiteSpace:'pre-wrap' }}>{e.content}</div>
|
|
60
|
+
</>}
|
|
61
|
+
<div className="divider" />
|
|
62
|
+
<div className="sec-label">raw event</div>
|
|
63
|
+
<div className="code scroll"><pre>{e.raw}</pre></div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function SpansView({ spans = SPANS, onPick }) {
|
|
70
|
+
return (
|
|
71
|
+
<div className="panel" style={{ padding:'18px 18px 16px' }}>
|
|
72
|
+
<div className="span-note">
|
|
73
|
+
<span>Ordered by <span className="mono">processed_at</span>.</span>
|
|
74
|
+
<span>Widths are approximate until per-span timing lands.</span>
|
|
75
|
+
</div>
|
|
76
|
+
<div className="axis">{['0s','7s','14s','20s','26s'].map((t) => <span key={t} className="mono">{t}</span>)}</div>
|
|
77
|
+
<div style={{ display:'flex', flexDirection:'column', gap:13 }}>
|
|
78
|
+
{spans.map((s, i) => (
|
|
79
|
+
<div className="span-row" key={i}>
|
|
80
|
+
<div className="span-label"><Role r={s.role} /><span className="mono" style={{ fontSize:11.5, color:'var(--soft)', whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{s.label}</span></div>
|
|
81
|
+
<div className="span-track">
|
|
82
|
+
<div className={'span-bar ' + s.kind} style={{ left:s.left + '%', width:s.width + '%' }}
|
|
83
|
+
onClick={() => onPick && onPick(s.eventId || (s.kind === 'tool' ? 'sevt_…a06' : 'sevt_…a03'))}>
|
|
84
|
+
<span className="mono">{s.info}</span>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
))}
|
|
89
|
+
</div>
|
|
90
|
+
<div className="legend">
|
|
91
|
+
<span><i style={{ background:'var(--r-agent-bg)', borderColor:'var(--r-agent)' }} />model request</span>
|
|
92
|
+
<span><i style={{ background:'var(--r-tool-bg)', borderColor:'var(--r-tool)' }} />tool</span>
|
|
93
|
+
<span><i style={{ background:'var(--accent-wash)', borderColor:'var(--accent-line)' }} />open / unpaired</span>
|
|
94
|
+
<span><i style={{ background:'var(--red-wash)', borderColor:'var(--red)' }} />error</span>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function FilesPanel({ files = FILES }) {
|
|
101
|
+
if (files.length === 0) {
|
|
102
|
+
return <EmptyState icon="folder" title="No output files" message="This session hasn’t produced any output files yet. Generated files appear here with download links." />;
|
|
103
|
+
}
|
|
104
|
+
return (
|
|
105
|
+
<div className="panel">
|
|
106
|
+
<div className="thead">
|
|
107
|
+
<span className="th" style={{ width:24 }} />
|
|
108
|
+
<span className="th grow">Filename</span>
|
|
109
|
+
<span className="th" style={{ width:80 }}>Type</span>
|
|
110
|
+
<span className="th" style={{ width:80 }}>Size</span>
|
|
111
|
+
<span className="th" style={{ width:70 }}>Created</span>
|
|
112
|
+
<span className="th" style={{ width:110 }}>Download</span>
|
|
113
|
+
</div>
|
|
114
|
+
{files.map((f, i) => (
|
|
115
|
+
<div className={'trow' + (f.dl ? '' : ' inert-row')} key={i}>
|
|
116
|
+
<span className="file-ico" style={{ marginRight:-4 }}>{f.ext}</span>
|
|
117
|
+
<span className="td grow cell-strong">{f.name}</span>
|
|
118
|
+
<span className="td mono" style={{ width:80, fontSize:11.5, color:'var(--faint)' }}>{f.type}</span>
|
|
119
|
+
<span className="td mono" style={{ width:80, color:'var(--soft)' }}>{f.size}</span>
|
|
120
|
+
<span className="td mono" style={{ width:70, color:'var(--faint)' }}>{f.created}</span>
|
|
121
|
+
<span className="td" style={{ width:110 }}>
|
|
122
|
+
{f.dl && f.href ? <a className="dl" href="#" onClick={(e) => { e.preventDefault(); OmaConsoleApi.downloadFile(f.href, f.name); }}><Icon name="download" size={14} />Download</a> : <span className="inert">{f.dl ? '— demo file' : '— mounted input'}</span>}
|
|
123
|
+
</span>
|
|
124
|
+
</div>
|
|
125
|
+
))}
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function SessionDetail({ session, layout, go, onArchive, onDelete, onSessionStateChange, onRefreshSession, dataState = 'loaded', apiMode = 'mock', readOnly = false, lifecycleReadOnly = readOnly, onAuthExpired }) {
|
|
131
|
+
const s = session;
|
|
132
|
+
const displayStatus = s.status === 'action' ? 'idle' : s.status;
|
|
133
|
+
const isLive = displayStatus === 'running';
|
|
134
|
+
const isConfirm = !!s.confirm || !!s.requiresAction;
|
|
135
|
+
const [view, setView] = useStateD('transcript'); // transcript | debug | spans | files
|
|
136
|
+
const [selId, setSel] = useStateD(isLive || isConfirm ? null : 'sevt_…a05');
|
|
137
|
+
const [filters, setFilters] = useStateD([]);
|
|
138
|
+
const [query, setQuery] = useStateD('');
|
|
139
|
+
const threecol = layout === 'threecol';
|
|
140
|
+
|
|
141
|
+
// ── live stream state ──
|
|
142
|
+
const baseEvents = s.events ?? (apiMode === 'api' ? [] : EVENTS);
|
|
143
|
+
const baseFiles = s.files ?? (apiMode === 'api' ? [] : FILES);
|
|
144
|
+
const baseSpans = s.spans ?? (apiMode === 'api' ? [] : SPANS);
|
|
145
|
+
const [shown, setShown] = useStateD(() => isLive && apiMode !== 'api' ? RUN_EVENTS.slice(0, 1) : isConfirm && apiMode !== 'api' ? CONFIRM_EVENTS : baseEvents);
|
|
146
|
+
const [status, setStatus] = useStateD(displayStatus);
|
|
147
|
+
const [working, setWorking] = useStateD(isLive);
|
|
148
|
+
const [confirmState, setConfirmState] = useStateD(isConfirm ? 'pending' : null);
|
|
149
|
+
const [menuOpen, setMenuOpen] = useStateD(false);
|
|
150
|
+
const [dialog, setDialog] = useStateD(null); // 'archive' | 'delete'
|
|
151
|
+
const [message, setMessage] = useStateD('');
|
|
152
|
+
const [actionBusy, setActionBusy] = useStateD(false);
|
|
153
|
+
const [actionError, setActionError] = useStateD(null);
|
|
154
|
+
const [lifecycleBusy, setLifecycleBusy] = useStateD(false);
|
|
155
|
+
const [lifecycleError, setLifecycleError] = useStateD(null);
|
|
156
|
+
const [streamState, setStreamState] = useStateD(apiMode === 'api' ? 'connecting' : 'closed');
|
|
157
|
+
const aliveRef = useRefD(true);
|
|
158
|
+
const idxRef = useRefD(1);
|
|
159
|
+
const timerRef = useRefD(null);
|
|
160
|
+
const streamRef = useRefD(null);
|
|
161
|
+
const messageIntentRef = useRefD(null);
|
|
162
|
+
const interruptIntentRef = useRefD(null);
|
|
163
|
+
const confirmationIntentRef = useRefD(null);
|
|
164
|
+
const archivedRef = useRefD(displayStatus === 'archived');
|
|
165
|
+
const running = status === 'running';
|
|
166
|
+
const archived = status === 'archived';
|
|
167
|
+
|
|
168
|
+
useEffectD(() => {
|
|
169
|
+
archivedRef.current = displayStatus === 'archived';
|
|
170
|
+
setStatus(displayStatus);
|
|
171
|
+
setWorking(isLive);
|
|
172
|
+
setConfirmState(isConfirm ? 'pending' : null);
|
|
173
|
+
setSel(isLive || isConfirm ? null : 'sevt_…a05');
|
|
174
|
+
setView('transcript');
|
|
175
|
+
setFilters([]);
|
|
176
|
+
setQuery('');
|
|
177
|
+
setMessage('');
|
|
178
|
+
setActionError(null);
|
|
179
|
+
setLifecycleError(null);
|
|
180
|
+
setStreamState(apiMode === 'api' ? 'connecting' : 'closed');
|
|
181
|
+
messageIntentRef.current = OmaConsoleApi.createIdempotencyIntent();
|
|
182
|
+
interruptIntentRef.current = OmaConsoleApi.createIdempotencyIntent();
|
|
183
|
+
confirmationIntentRef.current = OmaConsoleApi.createIdempotencyIntent();
|
|
184
|
+
}, [s.id]);
|
|
185
|
+
|
|
186
|
+
useEffectD(() => {
|
|
187
|
+
if (!isLive || apiMode === 'api') return;
|
|
188
|
+
aliveRef.current = true;
|
|
189
|
+
idxRef.current = 1;
|
|
190
|
+
const tick = () => {
|
|
191
|
+
if (!aliveRef.current) return;
|
|
192
|
+
if (idxRef.current >= RUN_EVENTS.length) { setStatus('idle'); setWorking(false); return; }
|
|
193
|
+
const ev = RUN_EVENTS[idxRef.current];
|
|
194
|
+
timerRef.current = setTimeout(() => {
|
|
195
|
+
if (!aliveRef.current) return;
|
|
196
|
+
setShown((prev) => [...prev, ev]);
|
|
197
|
+
if (ev.type === 'session.status_idle') { setStatus('idle'); setWorking(false); }
|
|
198
|
+
idxRef.current += 1;
|
|
199
|
+
tick();
|
|
200
|
+
}, ev.gap == null ? 1200 : ev.gap);
|
|
201
|
+
};
|
|
202
|
+
setWorking(true);
|
|
203
|
+
tick();
|
|
204
|
+
return () => { aliveRef.current = false; clearTimeout(timerRef.current); };
|
|
205
|
+
}, [isLive, apiMode]);
|
|
206
|
+
|
|
207
|
+
useEffectD(() => {
|
|
208
|
+
if (isLive && apiMode !== 'api') return;
|
|
209
|
+
if (isConfirm && apiMode !== 'api') return;
|
|
210
|
+
setShown(baseEvents);
|
|
211
|
+
}, [s.id, s.events, apiMode]);
|
|
212
|
+
|
|
213
|
+
useEffectD(() => {
|
|
214
|
+
if (apiMode !== 'api' || s.loadingEvents || s.eventError) return;
|
|
215
|
+
const controller = new AbortController();
|
|
216
|
+
const lastPersistedId = baseEvents.length > 0 ? baseEvents[baseEvents.length - 1].id : undefined;
|
|
217
|
+
setStreamState('connecting');
|
|
218
|
+
OmaConsoleApi.followSessionEvents(s.id, {
|
|
219
|
+
signal: controller.signal,
|
|
220
|
+
lastEventId: lastPersistedId,
|
|
221
|
+
onState: ({ status: next, error }) => {
|
|
222
|
+
if (archivedRef.current) return;
|
|
223
|
+
setStreamState(next);
|
|
224
|
+
if (next === 'failed') {
|
|
225
|
+
setActionError(error?.message || 'The live event stream failed. Persisted history remains available.');
|
|
226
|
+
if (error?.status === 401 && onAuthExpired) onAuthExpired();
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
onEvent: (event) => {
|
|
230
|
+
if (archivedRef.current) return;
|
|
231
|
+
const mapped = OmaConsoleApi.toUiSessionEvent(event);
|
|
232
|
+
setShown((current) => current.some((item) => item.id === mapped.id)
|
|
233
|
+
? current
|
|
234
|
+
: [...current, mapped]);
|
|
235
|
+
if (event.type === 'session.status_running') {
|
|
236
|
+
setStatus('running');
|
|
237
|
+
setWorking(true);
|
|
238
|
+
setConfirmState(null);
|
|
239
|
+
if (onSessionStateChange) onSessionStateChange(s.id, { status:'running', requiresAction:false });
|
|
240
|
+
} else if (event.type === 'session.status_idle') {
|
|
241
|
+
const requiresAction = event.stop_reason?.type === 'requires_action';
|
|
242
|
+
setStatus('idle');
|
|
243
|
+
setWorking(false);
|
|
244
|
+
setConfirmState(requiresAction ? 'pending' : null);
|
|
245
|
+
if (onSessionStateChange) onSessionStateChange(s.id, { status:'idle', requiresAction });
|
|
246
|
+
if (onRefreshSession) onRefreshSession({ ...s, status:'idle' });
|
|
247
|
+
} else if (event.type === 'session.status_terminated') {
|
|
248
|
+
setStatus('terminated');
|
|
249
|
+
setWorking(false);
|
|
250
|
+
setConfirmState(null);
|
|
251
|
+
if (onSessionStateChange) onSessionStateChange(s.id, { status:'terminated', requiresAction:false });
|
|
252
|
+
} else if (event.type === 'session.error') {
|
|
253
|
+
setWorking(false);
|
|
254
|
+
} else if (event.type === 'user.tool_confirmation') {
|
|
255
|
+
setConfirmState(event.result === 'allow' ? 'allowed' : 'denied');
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
}).catch((error) => {
|
|
259
|
+
if (controller.signal.aborted) return;
|
|
260
|
+
setActionError(error?.message || 'The live event stream failed. Persisted history remains available.');
|
|
261
|
+
});
|
|
262
|
+
return () => controller.abort();
|
|
263
|
+
}, [apiMode, s.id, s.loadingEvents, s.eventError]);
|
|
264
|
+
|
|
265
|
+
// auto-scroll the live stream as events arrive (no scrollIntoView)
|
|
266
|
+
useEffectD(() => {
|
|
267
|
+
if (!running) return;
|
|
268
|
+
const el = streamRef.current;
|
|
269
|
+
if (el) el.scrollTop = el.scrollHeight;
|
|
270
|
+
}, [shown, view, running]);
|
|
271
|
+
|
|
272
|
+
const interrupt = async () => {
|
|
273
|
+
if (archivedRef.current) return;
|
|
274
|
+
if (apiMode === 'api') {
|
|
275
|
+
if (actionBusy) return;
|
|
276
|
+
setActionBusy(true);
|
|
277
|
+
setActionError(null);
|
|
278
|
+
try {
|
|
279
|
+
interruptIntentRef.current ||= OmaConsoleApi.createIdempotencyIntent();
|
|
280
|
+
await OmaConsoleApi.sendSessionEvents(s.id, [{ type:'user.interrupt' }], { intent:interruptIntentRef.current });
|
|
281
|
+
interruptIntentRef.current = null;
|
|
282
|
+
} catch (error) {
|
|
283
|
+
setActionError(error.message || 'Interrupt request failed.');
|
|
284
|
+
if (error.status === 401 && onAuthExpired) onAuthExpired();
|
|
285
|
+
} finally {
|
|
286
|
+
setActionBusy(false);
|
|
287
|
+
}
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
aliveRef.current = false;
|
|
291
|
+
clearTimeout(timerRef.current);
|
|
292
|
+
const t = '0:00:06';
|
|
293
|
+
setShown((prev) => [
|
|
294
|
+
...prev,
|
|
295
|
+
{ id:'sevt_…int', role:'user', type:'user.interrupt', tag:'interrupt', transcript:true, time:t,
|
|
296
|
+
text:'user.interrupt · operator requested stop',
|
|
297
|
+
content:'Interrupt requested by operator. The agent will stop after the current step.',
|
|
298
|
+
raw:JSON.stringify({ type:'user.interrupt', id:'sevt_…int', reason:'operator' }, null, 2) },
|
|
299
|
+
{ id:'sevt_…idl', role:'sys', type:'session.status_idle', tag:'interrupted', time:t,
|
|
300
|
+
text:'session.status_idle · interrupted',
|
|
301
|
+
content:'Session went idle after interrupt. requires_action: false.',
|
|
302
|
+
raw:JSON.stringify({ type:'session.status_idle', id:'sevt_…idl', interrupted:true, requires_action:false }, null, 2) },
|
|
303
|
+
]);
|
|
304
|
+
setStatus('idle');
|
|
305
|
+
setWorking(false);
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const pool = apiMode === 'api' || isLive || isConfirm || s.events ? shown : EVENTS;
|
|
309
|
+
const usesSessionEvents = Array.isArray(s.events);
|
|
310
|
+
const sel = pool.find((e) => e.id === selId) ||
|
|
311
|
+
(!usesSessionEvents ? EVENTS.find((e) => e.id === selId) : null);
|
|
312
|
+
const queryText = query.trim().toLowerCase();
|
|
313
|
+
const eventMatchesQuery = (event) => {
|
|
314
|
+
if (!queryText) return true;
|
|
315
|
+
return [event.id, event.type, event.tag, event.text, event.content, event.raw]
|
|
316
|
+
.filter(Boolean)
|
|
317
|
+
.some((value) => String(value).toLowerCase().includes(queryText));
|
|
318
|
+
};
|
|
319
|
+
const eventMatchesType = (event) =>
|
|
320
|
+
filters.length === 0 || filters.some((filter) => event.type.includes(filter));
|
|
321
|
+
const filteredPool = pool.filter((event) =>
|
|
322
|
+
eventMatchesQuery(event) && (view === 'debug' ? eventMatchesType(event) : true));
|
|
323
|
+
const txEvents = filteredPool.filter((e) => e.transcript);
|
|
324
|
+
const dbgEvents = filteredPool;
|
|
325
|
+
const tabFor = (view === 'spans' || view === 'debug') ? 'debug' : view;
|
|
326
|
+
const fileCount = Array.isArray(baseFiles) ? baseFiles.length : 0;
|
|
327
|
+
const needsAction = Boolean(confirmState === 'pending' || confirmState === 'submitting');
|
|
328
|
+
const resultCount = view === 'debug' ? dbgEvents.length : txEvents.length;
|
|
329
|
+
const eventSearchVisible = view !== 'files' && view !== 'spans';
|
|
330
|
+
|
|
331
|
+
// resolve a pending tool confirmation → emit user.tool_confirmation + follow-up
|
|
332
|
+
const resolveConfirm = async (decision) => {
|
|
333
|
+
if (apiMode === 'api') {
|
|
334
|
+
if (!pendingTool || !OmaConsoleApi.canSubmitToolConfirmation({ status, readOnly, actionBusy })) return;
|
|
335
|
+
setActionBusy(true);
|
|
336
|
+
setActionError(null);
|
|
337
|
+
setConfirmState('submitting');
|
|
338
|
+
const event = {
|
|
339
|
+
type:'user.tool_confirmation',
|
|
340
|
+
tool_use_id:pendingTool.id,
|
|
341
|
+
result:decision,
|
|
342
|
+
...(decision === 'deny' ? { deny_message:'Denied in the OMA console.' } : {}),
|
|
343
|
+
};
|
|
344
|
+
try {
|
|
345
|
+
confirmationIntentRef.current ||= OmaConsoleApi.createIdempotencyIntent();
|
|
346
|
+
await OmaConsoleApi.sendSessionEvents(s.id, [event], { intent:confirmationIntentRef.current });
|
|
347
|
+
confirmationIntentRef.current = null;
|
|
348
|
+
} catch (error) {
|
|
349
|
+
setConfirmState('pending');
|
|
350
|
+
setActionError(error.message || 'Tool confirmation failed.');
|
|
351
|
+
if (error.status === 401 && onAuthExpired) onAuthExpired();
|
|
352
|
+
} finally {
|
|
353
|
+
setActionBusy(false);
|
|
354
|
+
}
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
const allow = decision === 'allow';
|
|
358
|
+
const t = '0:00:02';
|
|
359
|
+
const extra = allow
|
|
360
|
+
? [
|
|
361
|
+
{ id:'sevt_…c05', role:'user', type:'user.tool_confirmation', tag:'allow', transcript:true, time:t,
|
|
362
|
+
text:'user.tool_confirmation · allow · bash rm -rf build/',
|
|
363
|
+
content:'Operator allowed the tool call.',
|
|
364
|
+
raw:JSON.stringify({ type:'user.tool_confirmation', id:'sevt_…c05', tool_use_id:'sevt_…c03', decision:'allow' }, null, 2) },
|
|
365
|
+
{ id:'sevt_…c06', role:'tool', type:'agent.tool_result', tag:'exit 0', transcript:true, time:'0:00:03',
|
|
366
|
+
text:'agent.tool_result · exit 0 · build/ removed',
|
|
367
|
+
content:'exit 0 · removed build/ (42 files)', raw:JSON.stringify({ type:'agent.tool_result', id:'sevt_…c06', tool_use_id:'sevt_…c03', is_error:false }, null, 2) },
|
|
368
|
+
{ id:'sevt_…c07', role:'agent', type:'agent.message', tag:'message', transcript:true, time:'0:00:04', tokens:'5.4k / 120',
|
|
369
|
+
text:'Done — build/ removed and export.sh re-run. 3 artifacts written.',
|
|
370
|
+
content:'Done — build/ removed and export.sh re-run. 3 artifacts written.', raw:JSON.stringify({ type:'agent.message', id:'sevt_…c07' }, null, 2) },
|
|
371
|
+
]
|
|
372
|
+
: [
|
|
373
|
+
{ id:'sevt_…c05', role:'user', type:'user.tool_confirmation', tag:'deny', transcript:true, time:t,
|
|
374
|
+
text:'user.tool_confirmation · deny · bash rm -rf build/',
|
|
375
|
+
content:'Operator denied the tool call.',
|
|
376
|
+
raw:JSON.stringify({ type:'user.tool_confirmation', id:'sevt_…c05', tool_use_id:'sevt_…c03', decision:'deny' }, null, 2) },
|
|
377
|
+
{ id:'sevt_…c07', role:'agent', type:'agent.message', tag:'message', transcript:true, time:'0:00:03', tokens:'5.2k / 88',
|
|
378
|
+
text:'Understood — I\u2019ll leave build/ in place. Let me know how you\u2019d like to proceed.',
|
|
379
|
+
content:'Understood — I\u2019ll leave build/ in place. Let me know how you\u2019d like to proceed.', raw:JSON.stringify({ type:'agent.message', id:'sevt_…c07' }, null, 2) },
|
|
380
|
+
];
|
|
381
|
+
const idle = { id:'sevt_…c08', role:'sys', type:'session.status_idle', tag:'requires_action', time:'0:00:04',
|
|
382
|
+
text:'session.status_idle · requires_action: none', content:'Session idle. requires_action: false.',
|
|
383
|
+
raw:JSON.stringify({ type:'session.status_idle', id:'sevt_…c08', requires_action:false }, null, 2) };
|
|
384
|
+
setShown((prev) => [...prev, ...extra, idle]);
|
|
385
|
+
setConfirmState(allow ? 'allowed' : 'denied');
|
|
386
|
+
setStatus('idle');
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const confirmedToolIds = new Set(pool
|
|
390
|
+
.filter((event) => event.type === 'user.tool_confirmation')
|
|
391
|
+
.map((event) => event.source?.tool_use_id)
|
|
392
|
+
.filter(Boolean));
|
|
393
|
+
const latestRequiredIds = [...pool].reverse()
|
|
394
|
+
.find((event) => event.type === 'session.status_idle' && event.source?.stop_reason?.type === 'requires_action')
|
|
395
|
+
?.source?.stop_reason?.event_ids;
|
|
396
|
+
const pendingTool = apiMode === 'api'
|
|
397
|
+
? [...pool].reverse().find((event) =>
|
|
398
|
+
(event.type === 'agent.tool_use' || event.type === 'agent.mcp_tool_use') &&
|
|
399
|
+
(event.confirm || event.source?.evaluated_permission === 'ask') &&
|
|
400
|
+
!confirmedToolIds.has(event.id) &&
|
|
401
|
+
(!Array.isArray(latestRequiredIds) || latestRequiredIds.includes(event.id)))
|
|
402
|
+
: (isConfirm ? CONFIRM_EVENTS.find((e) => e.confirm) : null);
|
|
403
|
+
const renderConfirmCard = () => {
|
|
404
|
+
if ((confirmState !== 'pending' && confirmState !== 'submitting') || !pendingTool) return null;
|
|
405
|
+
const endpoint = 'POST /v1/sessions/:id/events';
|
|
406
|
+
const confirmationDisabled = !OmaConsoleApi.canSubmitToolConfirmation({ status, readOnly, actionBusy });
|
|
407
|
+
const confirmationTitle = archived
|
|
408
|
+
? 'Archived sessions are read-only.'
|
|
409
|
+
: readOnly ? `Read-only API mode · ${endpoint}` : undefined;
|
|
410
|
+
return (
|
|
411
|
+
<div className="confirm-card">
|
|
412
|
+
<div className="cc-head">
|
|
413
|
+
<div className="cc-ic"><Icon name="alert" size={16} /></div>
|
|
414
|
+
<div>
|
|
415
|
+
<div className="cc-label">Requires action · tool confirmation</div>
|
|
416
|
+
<div className="cc-title">The agent wants to run a tool that needs your approval.</div>
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
<div className="cc-body">
|
|
420
|
+
<div className="cc-tool"><span className="tn">{pendingTool.tool || 'tool'}</span><span style={{ color:'var(--faint)' }}>$</span>{pendingTool.cmd || JSON.stringify(pendingTool.source?.input || {})}</div>
|
|
421
|
+
</div>
|
|
422
|
+
<div className="cc-actions">
|
|
423
|
+
<button className="btn btn-accent" disabled={confirmationDisabled}
|
|
424
|
+
title={confirmationTitle}
|
|
425
|
+
onClick={() => !confirmationDisabled && resolveConfirm('allow')}>
|
|
426
|
+
<Icon name="checkCircle" size={14} />{actionBusy ? 'Submitting…' : 'Allow'}
|
|
427
|
+
</button>
|
|
428
|
+
<button className="btn btn-danger" disabled={confirmationDisabled}
|
|
429
|
+
title={confirmationTitle}
|
|
430
|
+
onClick={() => !confirmationDisabled && resolveConfirm('deny')}>
|
|
431
|
+
<Icon name="x" size={14} />Deny
|
|
432
|
+
</button>
|
|
433
|
+
<span style={{ flex:1 }} />
|
|
434
|
+
<span className="field-hint" style={{ alignSelf:'center' }}>
|
|
435
|
+
{archived ? <>Disabled · archived sessions are read-only</> : readOnly ? <>Disabled · <span className="mono">{endpoint}</span></> : <>Emits <span className="mono">user.tool_confirmation</span></>}
|
|
436
|
+
</span>
|
|
437
|
+
</div>
|
|
438
|
+
</div>
|
|
439
|
+
);
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
const sendMessage = async () => {
|
|
443
|
+
const text = message.trim();
|
|
444
|
+
if (!text || running || archived || readOnly || actionBusy) return;
|
|
445
|
+
setActionBusy(true);
|
|
446
|
+
setActionError(null);
|
|
447
|
+
const event = { type:'user.message', content:[{ type:'text', text }] };
|
|
448
|
+
try {
|
|
449
|
+
messageIntentRef.current ||= OmaConsoleApi.createIdempotencyIntent();
|
|
450
|
+
await OmaConsoleApi.sendSessionEvents(s.id, [event], { intent:messageIntentRef.current });
|
|
451
|
+
messageIntentRef.current = null;
|
|
452
|
+
setMessage('');
|
|
453
|
+
setStatus('running');
|
|
454
|
+
setWorking(true);
|
|
455
|
+
if (onSessionStateChange) onSessionStateChange(s.id, { status:'running', requiresAction:false });
|
|
456
|
+
} catch (error) {
|
|
457
|
+
setActionError(error.message || 'Message submission failed.');
|
|
458
|
+
if (error.status === 401 && onAuthExpired) onAuthExpired();
|
|
459
|
+
} finally {
|
|
460
|
+
setActionBusy(false);
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
const runLifecycle = async (kind) => {
|
|
465
|
+
if (lifecycleBusy) return;
|
|
466
|
+
const action = kind === 'archive' ? onArchive : onDelete;
|
|
467
|
+
if (!action) return;
|
|
468
|
+
setLifecycleBusy(true);
|
|
469
|
+
setLifecycleError(null);
|
|
470
|
+
try {
|
|
471
|
+
await action(s);
|
|
472
|
+
if (kind === 'archive') {
|
|
473
|
+
archivedRef.current = true;
|
|
474
|
+
setStatus('archived');
|
|
475
|
+
setWorking(false);
|
|
476
|
+
setConfirmState(null);
|
|
477
|
+
if (onSessionStateChange) onSessionStateChange(s.id, { status:'archived', requiresAction:false });
|
|
478
|
+
}
|
|
479
|
+
setDialog(null);
|
|
480
|
+
} catch (error) {
|
|
481
|
+
setLifecycleError(error);
|
|
482
|
+
} finally {
|
|
483
|
+
setLifecycleBusy(false);
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
const inspectorVisible = view !== 'files';
|
|
488
|
+
|
|
489
|
+
const renderStreamHeader = () => (
|
|
490
|
+
<>
|
|
491
|
+
{!threecol && (
|
|
492
|
+
<div className="toolbar" style={{ marginBottom:12 }}>
|
|
493
|
+
<div className="tabs">
|
|
494
|
+
{['transcript','debug','files'].map((t) => (
|
|
495
|
+
<div key={t} className={'tab' + (tabFor === t ? ' active' : '')}
|
|
496
|
+
onClick={() => setView(t)} style={{ textTransform:'capitalize' }}>
|
|
497
|
+
{t}{t === 'files' && fileCount > 0 ? ` (${fileCount})` : ''}
|
|
498
|
+
</div>
|
|
499
|
+
))}
|
|
500
|
+
</div>
|
|
501
|
+
{tabFor === 'debug' && (
|
|
502
|
+
<div className="tabs">
|
|
503
|
+
<div className={'tab' + (view === 'debug' ? ' active' : '')} onClick={() => setView('debug')}>Events</div>
|
|
504
|
+
<div className={'tab' + (view === 'spans' ? ' active' : '')} onClick={() => setView('spans')}>Spans</div>
|
|
505
|
+
</div>
|
|
506
|
+
)}
|
|
507
|
+
<span style={{ width:1, height:22, background:'var(--border)' }} />
|
|
508
|
+
{eventSearchVisible && <Field icon="search" placeholder="Search events" value={query} onChange={setQuery} style={{ width:260, maxWidth:260 }} />}
|
|
509
|
+
{eventSearchVisible && <span className="mono result-count">{resultCount} events</span>}
|
|
510
|
+
<span className="grow" />
|
|
511
|
+
<button className="btn btn-icon btn-ghost"><Icon name="copy" size={15} /></button>
|
|
512
|
+
<button className="btn btn-icon btn-ghost"><Icon name="download" size={15} /></button>
|
|
513
|
+
</div>
|
|
514
|
+
)}
|
|
515
|
+
</>
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
const WorkingRow = () => (
|
|
519
|
+
<div className="ev working-row">
|
|
520
|
+
<Role r="agent" />
|
|
521
|
+
<span className="working"><i /><i /><i /></span>
|
|
522
|
+
<span className="ev-text" style={{ color:'var(--faint)' }}>Agent is working…</span>
|
|
523
|
+
</div>
|
|
524
|
+
);
|
|
525
|
+
|
|
526
|
+
const renderStream = () => {
|
|
527
|
+
if (dataState === 'loading' || s.loadingEvents) return <SkeletonStream rows={6} />;
|
|
528
|
+
if (dataState === 'error' || s.eventError) return <ErrorState resource="events" onRetry={() => {}} />;
|
|
529
|
+
if (view === 'files') return <FilesPanel files={baseFiles} />;
|
|
530
|
+
if (view === 'spans') return <SpansView spans={baseSpans} onPick={setSel} />;
|
|
531
|
+
if (view === 'debug') return (
|
|
532
|
+
<div>
|
|
533
|
+
<div className="chips">
|
|
534
|
+
{EVENT_TYPES.map((t) => {
|
|
535
|
+
const on = filters.includes(t);
|
|
536
|
+
return <div key={t} className={'chip' + (on ? ' on' : '')}
|
|
537
|
+
onClick={() => setFilters(on ? filters.filter((x) => x !== t) : [...filters, t])}>
|
|
538
|
+
<span className="x" />{t}</div>;
|
|
539
|
+
})}
|
|
540
|
+
{filters.length > 0 && <div className="chip" onClick={() => setFilters([])} style={{ color:'var(--faint)' }}>clear</div>}
|
|
541
|
+
{query && <div className="chip" onClick={() => setQuery('')} style={{ color:'var(--faint)' }}>clear search</div>}
|
|
542
|
+
</div>
|
|
543
|
+
<div className="panel">
|
|
544
|
+
{dbgEvents.map((e) => <EvRow key={e.id} e={e} mode="debug" sel={e.id === selId} onClick={() => setSel(e.id)} />)}
|
|
545
|
+
{dbgEvents.length === 0 && <div className="empty">No events match the current filters.</div>}
|
|
546
|
+
{working && running && WorkingRow()}
|
|
547
|
+
</div>
|
|
548
|
+
{renderConfirmCard()}
|
|
549
|
+
</div>
|
|
550
|
+
);
|
|
551
|
+
// transcript
|
|
552
|
+
return (
|
|
553
|
+
<div>
|
|
554
|
+
<div className="scrubber"><div className="fill" style={{ width: running ? '100%' : '54%' }} /><div className="hd" style={{ left: running ? 'calc(100% - 3px)' : '54%' }} /></div>
|
|
555
|
+
<div className="panel">
|
|
556
|
+
{txEvents.map((e) => <EvRow key={e.id} e={e} mode="transcript" sel={e.id === selId} onClick={() => setSel(e.id)} />)}
|
|
557
|
+
{txEvents.length === 0 && <div className="empty">No transcript events match the current search.</div>}
|
|
558
|
+
{working && running && WorkingRow()}
|
|
559
|
+
</div>
|
|
560
|
+
{renderConfirmCard()}
|
|
561
|
+
</div>
|
|
562
|
+
);
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
const renderRail = () => {
|
|
566
|
+
const items = [
|
|
567
|
+
['transcript','Transcript','activity'],
|
|
568
|
+
['debug','Debug events','terminal'],
|
|
569
|
+
['spans','Spans · timing','zap'],
|
|
570
|
+
['files',`Files · output${fileCount > 0 ? ` (${fileCount})` : ''}`,'folder'],
|
|
571
|
+
];
|
|
572
|
+
return (
|
|
573
|
+
<div>
|
|
574
|
+
<div className="rail">
|
|
575
|
+
{items.map(([k, l, ic]) => (
|
|
576
|
+
<div key={k} className={'rail-item' + (view === k ? ' active' : '')} onClick={() => setView(k)}>
|
|
577
|
+
<Icon name={ic} size={15} />{l}
|
|
578
|
+
</div>
|
|
579
|
+
))}
|
|
580
|
+
</div>
|
|
581
|
+
<div className="divider" style={{ margin:'12px 6px' }} />
|
|
582
|
+
{view === 'debug' && <div className="chips" style={{ marginTop:0 }}>
|
|
583
|
+
{EVENT_TYPES.map((t) => {
|
|
584
|
+
const on = filters.includes(t);
|
|
585
|
+
return <div key={t} className={'chip' + (on ? ' on' : '')}
|
|
586
|
+
onClick={() => setFilters(on ? filters.filter((x) => x !== t) : [...filters, t])}>
|
|
587
|
+
<span className="x" />{t}</div>;
|
|
588
|
+
})}
|
|
589
|
+
</div>}
|
|
590
|
+
{eventSearchVisible && <Field icon="search" placeholder="Search events" value={query} onChange={setQuery} style={{ marginTop:8, maxWidth:'none' }} />}
|
|
591
|
+
</div>
|
|
592
|
+
);
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
return (
|
|
596
|
+
<div className="main-scroll scroll fade-in">
|
|
597
|
+
<Crumbs items={[{ label:'Sessions', onClick:() => go('sessions') }, { label:s.short }]} />
|
|
598
|
+
<div className="sess-head">
|
|
599
|
+
<div>
|
|
600
|
+
<div className="sess-title">
|
|
601
|
+
<h1>{s.title}</h1>
|
|
602
|
+
{running ? <span className="badge st-running live"><i className="dot" />Live</span> : <St k={status} />}
|
|
603
|
+
{needsAction && <NeedsAction />}
|
|
604
|
+
</div>
|
|
605
|
+
<div className="meta-row">
|
|
606
|
+
<Pill icon="bot">{s.agent}</Pill>
|
|
607
|
+
<span className="dotsep">·</span>
|
|
608
|
+
<Pill icon="database">{s.env}</Pill>
|
|
609
|
+
<span className="dotsep">·</span>
|
|
610
|
+
<Pill icon="folder">{fileCount} files</Pill>
|
|
611
|
+
<span className="dotsep">·</span>
|
|
612
|
+
<span className="m" title="Session-level duration is not reported by the API. Per-request timing appears on model span events."><Icon name="clock" />{running ? 'running…' : s.dur}</span>
|
|
613
|
+
<span className="m" title="Session-level usage is not reported by the API. Per-request tokens appear on span.model_request_end.model_usage."><Icon name="layers" />{s.tokens}</span>
|
|
614
|
+
<span className="m mono" style={{ color:'var(--faint)' }}>{s.id}</span>
|
|
615
|
+
</div>
|
|
616
|
+
</div>
|
|
617
|
+
<div style={{ display:'flex', gap:9 }}>
|
|
618
|
+
<div className="menu-wrap">
|
|
619
|
+
<button className="btn" onClick={() => setMenuOpen(!menuOpen)}>Actions<Icon name="chevDown" size={14} /></button>
|
|
620
|
+
{menuOpen && (
|
|
621
|
+
<>
|
|
622
|
+
<div style={{ position:'fixed', inset:0, zIndex:40 }} onClick={() => setMenuOpen(false)} />
|
|
623
|
+
<div className="menu">
|
|
624
|
+
<div className="menu-item" onClick={() => { setMenuOpen(false); }}><Icon name="copy" />Copy session ID</div>
|
|
625
|
+
<div className="menu-item" onClick={() => { setMenuOpen(false); }}><Icon name="download" />Export events (JSON)</div>
|
|
626
|
+
{!lifecycleReadOnly && <>
|
|
627
|
+
<div className="menu-sep" />
|
|
628
|
+
{status !== 'archived' && <div className="menu-item" onClick={() => { setMenuOpen(false); setLifecycleError(null); setDialog('archive'); }}><Icon name="archive" />Archive session</div>}
|
|
629
|
+
<div className="menu-item danger" onClick={() => { setMenuOpen(false); setLifecycleError(null); setDialog('delete'); }}><Icon name="x" />Delete session</div>
|
|
630
|
+
</>}
|
|
631
|
+
</div>
|
|
632
|
+
</>
|
|
633
|
+
)}
|
|
634
|
+
</div>
|
|
635
|
+
{running
|
|
636
|
+
? <button className="btn btn-danger" disabled={readOnly || actionBusy} title={readOnly ? 'Connect a live workspace to send session events.' : undefined}
|
|
637
|
+
onClick={() => !readOnly && interrupt()}><Icon name="stop" size={14} />Interrupt</button>
|
|
638
|
+
: <button className="btn btn-accent" disabled={archived || readOnly} title={archived ? 'Archived sessions are read-only.' : readOnly ? 'Connect a live workspace to send session events.' : undefined}
|
|
639
|
+
onClick={() => !archived && !readOnly && setView('transcript')}>
|
|
640
|
+
<Icon name="sparkles" size={15} />Ask Claude</button>}
|
|
641
|
+
</div>
|
|
642
|
+
</div>
|
|
643
|
+
|
|
644
|
+
{s.sessionError && (
|
|
645
|
+
<div className="banner">
|
|
646
|
+
<Icon name="alert" size={16} />
|
|
647
|
+
<div>
|
|
648
|
+
<div className="b-main">Session emitted <span className="mono">{s.sessionError.type}</span></div>
|
|
649
|
+
<div className="b-sub">{s.sessionError.message}</div>
|
|
650
|
+
</div>
|
|
651
|
+
</div>
|
|
652
|
+
)}
|
|
653
|
+
|
|
654
|
+
{Array.isArray(s.warnings) && s.warnings.length > 0 && (
|
|
655
|
+
<div className="inline-warn">
|
|
656
|
+
<Icon name="alert" size={14} />
|
|
657
|
+
<span>{s.warnings.join(' ')}</span>
|
|
658
|
+
</div>
|
|
659
|
+
)}
|
|
660
|
+
|
|
661
|
+
{apiMode === 'api' && streamState !== 'connected' && streamState !== 'closed' && (
|
|
662
|
+
<div className={streamState === 'failed' ? 'inline-warn' : 'inline-ok'} role="status">
|
|
663
|
+
<Icon name={streamState === 'failed' ? 'alert' : 'refresh'} size={14} />
|
|
664
|
+
<span>{streamState === 'reconnecting' ? 'Live events disconnected; reconnecting with replay…'
|
|
665
|
+
: streamState === 'failed' ? 'Live events are unavailable. Persisted history is still shown.'
|
|
666
|
+
: 'Connecting to live session events…'}</span>
|
|
667
|
+
</div>
|
|
668
|
+
)}
|
|
669
|
+
|
|
670
|
+
{actionError && (
|
|
671
|
+
<div className="inline-warn" role="alert">
|
|
672
|
+
<Icon name="alert" size={14} /><span>{actionError}</span>
|
|
673
|
+
</div>
|
|
674
|
+
)}
|
|
675
|
+
{s.refreshError && (
|
|
676
|
+
<div className="inline-warn" role="alert">
|
|
677
|
+
<Icon name="alert" size={14} /><span>Session finished, but files/history refresh failed: {s.refreshError.message}</span>
|
|
678
|
+
</div>
|
|
679
|
+
)}
|
|
680
|
+
|
|
681
|
+
{dialog === 'archive' &&
|
|
682
|
+
<ConfirmDialog icon="archive" title="Archive this session?"
|
|
683
|
+
message={<>Archiving <b>{s.title}</b> makes it read-only and hides it from the default list. Its events stay intact and remain available when archived sessions are included.</>}
|
|
684
|
+
confirmLabel="Archive session" endpoint="POST /v1/sessions/:id/archive"
|
|
685
|
+
onClose={() => setDialog(null)}
|
|
686
|
+
onConfirm={() => runLifecycle('archive')} busy={lifecycleBusy} error={lifecycleError} />}
|
|
687
|
+
{dialog === 'delete' &&
|
|
688
|
+
<ConfirmDialog icon="x" danger title="Delete this session?"
|
|
689
|
+
message={<>Deleting <b>{s.title}</b> permanently removes the session and its events. This cannot be undone.</>}
|
|
690
|
+
confirmLabel="Delete session" endpoint="DELETE /v1/sessions/:id"
|
|
691
|
+
onClose={() => setDialog(null)}
|
|
692
|
+
onConfirm={() => runLifecycle('delete')} busy={lifecycleBusy} error={lifecycleError} />}
|
|
693
|
+
|
|
694
|
+
{renderStreamHeader()}
|
|
695
|
+
|
|
696
|
+
<div className={'detail-grid' + (threecol ? ' threecol' : '')}>
|
|
697
|
+
{threecol && renderRail()}
|
|
698
|
+
<div className={'stream' + (running ? ' live scroll' : '')} ref={streamRef}>{renderStream()}</div>
|
|
699
|
+
{inspectorVisible && <Inspector e={sel} onClose={() => setSel(null)} />}
|
|
700
|
+
</div>
|
|
701
|
+
|
|
702
|
+
{view !== 'files' && (
|
|
703
|
+
<div className={'composer' + (readOnly || archived ? ' ro' : '')}>
|
|
704
|
+
<Icon name="terminal" size={16} style={{ color:'var(--faint)' }} />
|
|
705
|
+
<input id="session-message-composer" name="message" aria-label="Session message"
|
|
706
|
+
placeholder={archived ? 'Archived sessions are read-only.' : readOnly ? 'Connect a live workspace to send session events.' : running ? 'Streaming live — interrupt to send a message…' : 'Send a message to this session…'}
|
|
707
|
+
value={message} onChange={(event) => setMessage(event.target.value)}
|
|
708
|
+
onKeyDown={(event) => { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); sendMessage(); } }}
|
|
709
|
+
disabled={running || archived || readOnly || actionBusy} />
|
|
710
|
+
{running
|
|
711
|
+
? <button className="btn btn-sm btn-danger" disabled={readOnly || actionBusy} title={readOnly ? 'POST /v1/sessions/:id/events' : undefined} onClick={() => !readOnly && interrupt()}><Icon name="stop" size={13} />{actionBusy ? 'Stopping…' : 'Interrupt'}</button>
|
|
712
|
+
: <button className="btn btn-sm btn-primary" disabled={archived || readOnly || actionBusy || !message.trim()} title={archived ? 'Archived sessions are read-only.' : readOnly ? 'POST /v1/sessions/:id/events' : undefined} onClick={sendMessage}><Icon name="send" size={13} />{actionBusy ? 'Sending…' : 'Send'}</button>}
|
|
713
|
+
</div>
|
|
714
|
+
)}
|
|
715
|
+
</div>
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
window.SessionDetail = SessionDetail;
|