@agentstep/agent-sdk 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.
Files changed (105) hide show
  1. package/package.json +45 -0
  2. package/src/auth/middleware.ts +38 -0
  3. package/src/backends/claude/args.ts +88 -0
  4. package/src/backends/claude/index.ts +193 -0
  5. package/src/backends/claude/permission-hook.ts +152 -0
  6. package/src/backends/claude/tool-bridge.ts +211 -0
  7. package/src/backends/claude/translator.ts +209 -0
  8. package/src/backends/claude/wrapper-script.ts +45 -0
  9. package/src/backends/codex/args.ts +69 -0
  10. package/src/backends/codex/auth.ts +35 -0
  11. package/src/backends/codex/index.ts +57 -0
  12. package/src/backends/codex/setup.ts +37 -0
  13. package/src/backends/codex/translator.ts +223 -0
  14. package/src/backends/codex/wrapper-script.ts +26 -0
  15. package/src/backends/factory/args.ts +45 -0
  16. package/src/backends/factory/auth.ts +30 -0
  17. package/src/backends/factory/index.ts +56 -0
  18. package/src/backends/factory/setup.ts +34 -0
  19. package/src/backends/factory/translator.ts +139 -0
  20. package/src/backends/factory/wrapper-script.ts +33 -0
  21. package/src/backends/gemini/args.ts +44 -0
  22. package/src/backends/gemini/auth.ts +30 -0
  23. package/src/backends/gemini/index.ts +53 -0
  24. package/src/backends/gemini/setup.ts +34 -0
  25. package/src/backends/gemini/translator.ts +139 -0
  26. package/src/backends/gemini/wrapper-script.ts +26 -0
  27. package/src/backends/opencode/args.ts +53 -0
  28. package/src/backends/opencode/auth.ts +53 -0
  29. package/src/backends/opencode/index.ts +70 -0
  30. package/src/backends/opencode/mcp.ts +67 -0
  31. package/src/backends/opencode/setup.ts +54 -0
  32. package/src/backends/opencode/translator.ts +168 -0
  33. package/src/backends/opencode/wrapper-script.ts +46 -0
  34. package/src/backends/registry.ts +38 -0
  35. package/src/backends/shared/ndjson.ts +29 -0
  36. package/src/backends/shared/translator-types.ts +69 -0
  37. package/src/backends/shared/wrap-prompt.ts +17 -0
  38. package/src/backends/types.ts +85 -0
  39. package/src/config/index.ts +95 -0
  40. package/src/db/agents.ts +185 -0
  41. package/src/db/api_keys.ts +78 -0
  42. package/src/db/batch.ts +142 -0
  43. package/src/db/client.ts +81 -0
  44. package/src/db/environments.ts +127 -0
  45. package/src/db/events.ts +208 -0
  46. package/src/db/memory.ts +143 -0
  47. package/src/db/migrations.ts +295 -0
  48. package/src/db/proxy.ts +37 -0
  49. package/src/db/sessions.ts +295 -0
  50. package/src/db/vaults.ts +110 -0
  51. package/src/errors.ts +53 -0
  52. package/src/handlers/agents.ts +194 -0
  53. package/src/handlers/batch.ts +41 -0
  54. package/src/handlers/docs.ts +87 -0
  55. package/src/handlers/environments.ts +154 -0
  56. package/src/handlers/events.ts +234 -0
  57. package/src/handlers/index.ts +12 -0
  58. package/src/handlers/memory.ts +141 -0
  59. package/src/handlers/openapi.ts +14 -0
  60. package/src/handlers/sessions.ts +223 -0
  61. package/src/handlers/stream.ts +76 -0
  62. package/src/handlers/threads.ts +26 -0
  63. package/src/handlers/ui/app.js +984 -0
  64. package/src/handlers/ui/index.html +112 -0
  65. package/src/handlers/ui/style.css +164 -0
  66. package/src/handlers/ui.ts +1281 -0
  67. package/src/handlers/vaults.ts +99 -0
  68. package/src/http.ts +35 -0
  69. package/src/index.ts +104 -0
  70. package/src/init.ts +227 -0
  71. package/src/openapi/registry.ts +8 -0
  72. package/src/openapi/schemas.ts +625 -0
  73. package/src/openapi/spec.ts +691 -0
  74. package/src/providers/apple.ts +220 -0
  75. package/src/providers/daytona.ts +217 -0
  76. package/src/providers/docker.ts +264 -0
  77. package/src/providers/e2b.ts +203 -0
  78. package/src/providers/fly.ts +276 -0
  79. package/src/providers/modal.ts +222 -0
  80. package/src/providers/podman.ts +206 -0
  81. package/src/providers/registry.ts +28 -0
  82. package/src/providers/shared.ts +11 -0
  83. package/src/providers/sprites.ts +55 -0
  84. package/src/providers/types.ts +73 -0
  85. package/src/providers/vercel.ts +208 -0
  86. package/src/proxy/forward.ts +111 -0
  87. package/src/queue/index.ts +111 -0
  88. package/src/sessions/actor.ts +53 -0
  89. package/src/sessions/bus.ts +155 -0
  90. package/src/sessions/driver.ts +818 -0
  91. package/src/sessions/grader.ts +120 -0
  92. package/src/sessions/interrupt.ts +14 -0
  93. package/src/sessions/sweeper.ts +136 -0
  94. package/src/sessions/threads.ts +126 -0
  95. package/src/sessions/tools.ts +50 -0
  96. package/src/shutdown.ts +78 -0
  97. package/src/sprite/client.ts +294 -0
  98. package/src/sprite/exec.ts +161 -0
  99. package/src/sprite/lifecycle.ts +339 -0
  100. package/src/sprite/pool.ts +65 -0
  101. package/src/sprite/setup.ts +159 -0
  102. package/src/state.ts +61 -0
  103. package/src/types.ts +339 -0
  104. package/src/util/clock.ts +7 -0
  105. package/src/util/ids.ts +11 -0
@@ -0,0 +1,1281 @@
1
+ // AUTO-GENERATED by scripts/build-ui.ts — do not edit directly.
2
+ // Edit packages/agent-sdk/src/handlers/ui/{index.html,style.css,app.js} instead.
3
+
4
+ const HTML_TEMPLATE = `<!doctype html>
5
+ <html lang="en">
6
+ <head>
7
+ <meta charset="utf-8" />
8
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
9
+ <meta http-equiv="cache-control" content="no-store, no-cache, must-revalidate" />
10
+ <meta http-equiv="pragma" content="no-cache" />
11
+ <meta http-equiv="expires" content="0" />
12
+ <title>AgentStep Gateway</title>
13
+ <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><defs><linearGradient id='g' x1='0' y1='0' x2='1' y2='1'><stop offset='0%25' stop-color='%23eafb6e'/><stop offset='100%25' stop-color='%23b8fc5e'/></linearGradient></defs><circle cx='50' cy='50' r='40' fill='url(%23g)'/></svg>" />
14
+ <script src="https://cdn.jsdelivr.net/npm/marked@15/marked.min.js"></script>
15
+ <script src="https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js"></script>
16
+ <link rel="preconnect" href="https://rsms.me/" />
17
+ <link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
18
+ <style>*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
19
+
20
+ :root {
21
+ --bg: #0a0a0a; --surface: #171717; --surface2: #1e1e1e;
22
+ --border: rgba(255,255,255,0.08); --border-strong: rgba(255,255,255,0.14);
23
+ --heading: #fafafa; --body: #d4d4d4; --muted: #a3a3a3; --dim: #636363;
24
+ --accent: rgb(163, 230, 53); --accent-hover: rgb(190, 242, 100);
25
+ --error: #f87171; --success: #4ade80;
26
+ --font: 'Inter', -apple-system, sans-serif;
27
+ --mono: 'Geist Mono', 'SF Mono', ui-monospace, monospace;
28
+ }
29
+
30
+ html, body { height: 100%; background: var(--bg); color: var(--body); font-family: var(--font); font-size: 14px; -webkit-font-smoothing: antialiased; }
31
+ a { color: var(--accent); text-decoration: none; }
32
+ input, select, textarea, button { font-family: inherit; font-size: inherit; }
33
+
34
+ /* Layout */
35
+ #app { display: flex; flex-direction: column; height: 100vh; }
36
+ .header { display: flex; align-items: center; height: 48px; border-bottom: 1px solid var(--border); padding: 0 16px; gap: 12px; flex-shrink: 0; }
37
+ .header h1 { font-size: 16px; font-weight: 600; color: var(--heading); }
38
+ .header .key-input { margin-left: auto; display: flex; align-items: center; gap: 8px; }
39
+ .header input[type="text"], .header input[type="password"] { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 4px 10px; color: var(--body); font-size: 12px; width: 200px; }
40
+
41
+ /* Tabs */
42
+ .tabs { display: flex; gap: 2px; padding: 0 16px; border-bottom: 1px solid var(--border); flex-shrink: 0; }
43
+ .tab { padding: 8px 16px; font-size: 13px; font-weight: 500; color: var(--muted); cursor: pointer; border: none; background: none; border-bottom: 2px solid transparent; transition: all 0.15s; }
44
+ .tab:hover { color: var(--heading); }
45
+ .tab.active { color: var(--accent); border-bottom-color: var(--accent); }
46
+
47
+ /* Content */
48
+ .content { flex: 1; overflow: hidden; display: flex; }
49
+ .panel { display: none; flex: 1; overflow: hidden; }
50
+ .panel.active { display: flex; }
51
+
52
+ /* Chat */
53
+ .chat-layout { display: flex; flex: 1; overflow: hidden; }
54
+ .chat-sidebar { width: 220px; border-right: 1px solid var(--border); display: flex; flex-direction: column; flex-shrink: 0; }
55
+ .chat-sidebar-header { display: flex; align-items: center; height: 40px; padding: 0 12px; border-bottom: 1px solid var(--border); gap: 8px; }
56
+ .chat-sidebar-header span { flex: 1; font-size: 12px; font-weight: 500; color: var(--muted); }
57
+ .session-list { flex: 1; overflow-y: auto; padding: 4px; }
58
+ .session-item { display: flex; flex-direction: column; gap: 2px; padding: 6px 8px; border-radius: 6px; cursor: pointer; font-size: 12px; }
59
+ .session-item:hover { background: var(--surface); }
60
+ .session-item.active { background: var(--surface2); }
61
+ .session-item .title { color: var(--body); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
62
+ .session-item .meta { font-size: 10px; color: var(--dim); display: flex; justify-content: space-between; }
63
+
64
+ .chat-main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
65
+ .messages { flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 12px; }
66
+ .message { max-width: 720px; width: 100%; margin: 0 auto; }
67
+ .message.user { }
68
+ .message.assistant { }
69
+ .message-content { padding: 10px 14px; border-radius: 12px; font-size: 13px; line-height: 1.6; word-break: break-word; }
70
+ .message.user .message-content { white-space: pre-wrap; }
71
+ .message.user .message-content { background: var(--surface2); color: var(--body); }
72
+ .message.assistant .message-content { color: var(--heading); }
73
+ .message.tool .message-content { background: var(--surface); color: var(--muted); font-family: var(--mono); font-size: 11px; border: 1px solid var(--border); }
74
+ .message.error .message-content { background: #dc2626; color: #fff; border-radius: 8px; }
75
+ .message.error .message-role { color: #dc2626; }
76
+ .message-role { font-size: 10px; font-weight: 500; color: var(--dim); margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.05em; }
77
+ .typing { display: flex; align-items: center; gap: 8px; padding: 10px 14px; color: var(--muted); font-size: 13px; max-width: 720px; margin: 0 auto; }
78
+ .typing-dots { display: flex; gap: 3px; }
79
+ .typing-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--muted); animation: bounce 1.2s infinite; }
80
+ .typing-dot:nth-child(2) { animation-delay: 0.15s; }
81
+ .typing-dot:nth-child(3) { animation-delay: 0.3s; }
82
+ @keyframes bounce { 0%, 80%, 100% { transform: translateY(0); } 40% { transform: translateY(-6px); } }
83
+
84
+ .chat-input-area { padding: 12px 16px; border-top: 1px solid var(--border); }
85
+ .chat-input-wrap { max-width: 720px; margin: 0 auto; display: flex; gap: 8px; }
86
+ .chat-input-wrap textarea { flex: 1; background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 10px 14px; color: var(--body); resize: none; min-height: 42px; max-height: 150px; }
87
+ .chat-input-wrap textarea:focus { outline: none; border-color: var(--accent); }
88
+ .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; flex: 1; gap: 12px; color: var(--dim); }
89
+ .empty-state svg { opacity: 0.3; }
90
+
91
+ /* Config */
92
+ .config-layout { flex: 1; display: flex; gap: 1px; background: var(--border); overflow: hidden; }
93
+ .config-col { flex: 1; display: flex; flex-direction: column; gap: 12px; overflow-y: auto; padding: 16px; background: var(--bg); }
94
+ .config-col h2 { font-size: 14px; font-weight: 600; color: var(--heading); display: flex; align-items: center; justify-content: space-between; }
95
+ .card { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 12px; }
96
+ .card-item { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-bottom: 1px solid var(--border); }
97
+ .card-item:last-child { border-bottom: none; }
98
+ .card-item .name { font-size: 13px; color: var(--heading); }
99
+ .card-item .detail { font-size: 11px; color: var(--dim); font-family: var(--mono); }
100
+
101
+ /* Events */
102
+ .events-layout { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
103
+ .events-toolbar { display: flex; align-items: center; gap: 8px; padding: 8px 16px; border-bottom: 1px solid var(--border); flex-shrink: 0; }
104
+ .events-list { flex: 1; overflow-y: auto; }
105
+ .event-row { display: flex; align-items: flex-start; gap: 8px; padding: 6px 16px; border-bottom: 1px solid var(--border); cursor: pointer; font-size: 12px; }
106
+ .event-row:hover { background: var(--surface); }
107
+ .event-row .seq { color: var(--dim); font-family: var(--mono); font-size: 10px; width: 28px; text-align: right; flex-shrink: 0; padding-top: 2px; }
108
+ .event-row .delta { color: var(--dim); font-family: var(--mono); font-size: 10px; flex-shrink: 0; padding-top: 2px; }
109
+ .event-row .tokens { color: #a78bfa; font-family: var(--mono); font-size: 10px; flex-shrink: 0; padding-top: 2px; }
110
+ .event-row .preview { flex: 1; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
111
+ .event-detail { padding: 4px 16px 8px 52px; }
112
+ .event-detail pre { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 8px; font-family: var(--mono); font-size: 10px; color: var(--body); overflow-x: auto; max-height: 200px; overflow-y: auto; white-space: pre-wrap; }
113
+
114
+ /* Badges */
115
+ .badge { display: inline-block; padding: 1px 6px; border-radius: 4px; font-size: 10px; font-weight: 500; font-family: var(--mono); }
116
+ .badge-user { background: #1e3a5f; color: #60a5fa; }
117
+ .badge-agent { background: #064e3b; color: #34d399; }
118
+ .badge-status { background: #422006; color: #fbbf24; }
119
+ .badge-error { background: #450a0a; color: #f87171; }
120
+ .badge-span { background: #2e1065; color: #a78bfa; }
121
+ .badge-idle { background: var(--surface2); color: var(--muted); }
122
+ .badge-running { background: #064e3b; color: #34d399; }
123
+
124
+ /* Markdown in messages */
125
+ .message-content strong { color: var(--heading); font-weight: 600; }
126
+ .message-content em { font-style: italic; }
127
+ .message-content code { background: var(--surface); padding: 1px 5px; border-radius: 4px; font-family: var(--mono); font-size: 0.9em; }
128
+ .message-content pre { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 10px 12px; margin: 8px 0; overflow-x: auto; font-family: var(--mono); font-size: 12px; line-height: 1.5; }
129
+ .message-content pre code { background: none; padding: 0; }
130
+ .message-content a { color: var(--accent); text-decoration: underline; }
131
+ /* Reset ALL margins inside message content — browser defaults cause spacing issues */
132
+ .message-content p, .message-content ul, .message-content ol,
133
+ .message-content li, .message-content h1, .message-content h2,
134
+ .message-content h3, .message-content h4, .message-content blockquote {
135
+ margin: 0 !important; padding: 0 !important;
136
+ }
137
+ .message-content ul, .message-content ol { padding-left: 1.5em !important; margin: 2px 0 !important; }
138
+ .message-content li { line-height: 1.5; }
139
+ .message-content p + p { margin-top: 8px !important; }
140
+ .message-content p + ul, .message-content p + ol { margin-top: 2px !important; }
141
+ .message-content ul + p, .message-content ol + p { margin-top: 8px !important; }
142
+ .message-content h1, .message-content h2, .message-content h3 { color: var(--heading); margin: 8px 0 2px !important; }
143
+ .message-content h1 { font-size: 1.3em; } .message-content h2 { font-size: 1.15em; } .message-content h3 { font-size: 1.05em; }
144
+ .badge-running { background: #064e3b; color: #34d399; }
145
+
146
+ /* Buttons */
147
+ .btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; padding: 10px 20px; border-radius: 10px; font-size: 13px; font-weight: 600; border: none; cursor: pointer; transition: all 0.15s; }
148
+ .btn-primary { background: linear-gradient(135deg, #e2f751, #a5fb3c); color: #0a0a0a; }
149
+ .btn-primary:hover { background: linear-gradient(135deg, #eafb6e, #b8fc5e); box-shadow: 0 0 16px 2px rgba(165, 251, 60, 0.25); }
150
+ .btn-secondary { background: var(--surface); color: var(--body); border: 1px solid var(--border); }
151
+ .btn-secondary:hover { background: var(--surface2); }
152
+ .btn-danger { background: transparent; color: var(--error); border: 1px solid var(--error); }
153
+ .btn-danger:hover { background: rgba(248,113,113,0.1); }
154
+ .btn-sm { padding: 4px 8px; font-size: 11px; }
155
+ .btn-icon { padding: 4px; border-radius: 6px; background: none; border: none; color: var(--muted); cursor: pointer; }
156
+ .btn-icon:hover { color: var(--heading); background: var(--surface); }
157
+
158
+ /* Forms */
159
+ .form-group { display: flex; flex-direction: column; gap: 4px; }
160
+ .form-label { font-size: 11px; font-weight: 500; color: var(--muted); }
161
+ .form-input, .form-select, .form-textarea { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 6px 10px; color: var(--body); font-size: 13px; }
162
+ .form-input:focus, .form-select:focus, .form-textarea:focus { outline: none; border-color: var(--accent); }
163
+ .form-select { appearance: none; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23a3a3a3' stroke-width='2'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E"); background-repeat: no-repeat; background-position: right 8px center; padding-right: 28px; }
164
+
165
+ /* Modal */
166
+ .modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 100; }
167
+ .modal { background: var(--bg); border: 1px solid var(--border-strong); border-radius: 12px; padding: 20px; width: 380px; max-width: 90vw; display: flex; flex-direction: column; gap: 16px; }
168
+ .modal h2 { font-size: 16px; font-weight: 600; color: var(--heading); }
169
+ .modal p { font-size: 12px; color: var(--muted); }
170
+ .modal-actions { display: flex; gap: 8px; justify-content: flex-end; }
171
+
172
+ /* Toast */
173
+ .toast { position: fixed; bottom: 16px; right: 16px; padding: 10px 16px; border-radius: 8px; font-size: 12px; z-index: 200; animation: fadeIn 0.2s; }
174
+ .toast-error { background: #450a0a; color: var(--error); border: 1px solid var(--error); }
175
+ .toast-success { background: #052e16; color: var(--success); border: 1px solid var(--success); }
176
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
177
+
178
+ /* Scrollbar */
179
+ ::-webkit-scrollbar { width: 6px; }
180
+ ::-webkit-scrollbar-track { background: transparent; }
181
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
182
+ </style>
183
+ </head>
184
+ <body>
185
+ <div id="app">
186
+ <!-- Header -->
187
+ <div class="header">
188
+ <h1>AgentStep Gateway</h1>
189
+ <div class="key-input">
190
+ <label style="font-size:11px;color:var(--dim);margin-right:4px">API Key</label>
191
+ <input type="password" id="apiKeyInput" placeholder="ck_..." />
192
+ <button class="btn-icon" onclick="toggleKeyVisibility()" id="keyEyeBtn" title="Show/hide key"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></svg></button>
193
+ </div>
194
+ </div>
195
+
196
+ <!-- Tabs -->
197
+ <div class="tabs">
198
+ <button class="tab active" data-tab="chat">Chat</button>
199
+ <button class="tab" data-tab="config">Config</button>
200
+ <button class="tab" data-tab="events">Events</button>
201
+ </div>
202
+
203
+ <!-- Content -->
204
+ <div class="content">
205
+ <!-- Chat Panel -->
206
+ <div class="panel active" id="panel-chat">
207
+ <div class="chat-layout">
208
+ <div class="chat-sidebar">
209
+ <div class="chat-sidebar-header">
210
+ <span>Sessions</span>
211
+ <button class="btn-icon" onclick="loadSessions()" title="Refresh"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/></svg></button>
212
+ <button class="btn-icon" onclick="showNewSessionModal()" title="New"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg></button>
213
+ </div>
214
+ <div class="session-list" id="sessionList"></div>
215
+ </div>
216
+ <div class="chat-main">
217
+ <div class="messages" id="messages">
218
+ <div class="empty-state" id="chatEmpty">
219
+ <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 100 100"><circle cx="50" cy="50" r="40" fill="#a3e635" opacity="0.3"/><circle cx="50" cy="50" r="20" fill="#a3e635" opacity="0.6"/></svg>
220
+ <p style="font-size:16px;font-weight:600;margin-bottom:4px">AgentStep Gateway</p>
221
+ <p style="font-size:12px">Select a session to continue, or click + to start a new one</p>
222
+ </div>
223
+ </div>
224
+ <div class="chat-input-area" id="chatInputArea" style="display:none">
225
+ <div class="chat-input-wrap">
226
+ <textarea id="chatInput" placeholder="Message..." rows="1" onkeydown="handleChatKey(event)"></textarea>
227
+ <button class="btn btn-primary" onclick="sendMessage()">Send</button>
228
+ </div>
229
+ </div>
230
+ </div>
231
+ </div>
232
+ </div>
233
+
234
+ <!-- Config Panel -->
235
+ <div class="panel" id="panel-config">
236
+ <div class="config-layout">
237
+ <div class="config-col">
238
+ <h2>Agents <button class="btn btn-sm btn-secondary" onclick="showCreateAgentModal()">+ New</button></h2>
239
+ <div class="card" id="agentsList"><p style="color:var(--dim);font-size:12px">Loading...</p></div>
240
+ </div>
241
+ <div class="config-col">
242
+ <h2>Environments <button class="btn btn-sm btn-secondary" onclick="showCreateEnvModal()">+ New</button></h2>
243
+ <div class="card" id="envsList"><p style="color:var(--dim);font-size:12px">Loading...</p></div>
244
+ </div>
245
+ <div class="config-col">
246
+ <h2>Secrets <button class="btn btn-sm btn-secondary" onclick="showCreateVaultModal()">+ New</button></h2>
247
+ <div class="card" id="vaultsList"><p style="color:var(--dim);font-size:12px">Loading...</p></div>
248
+ </div>
249
+ </div>
250
+ </div>
251
+
252
+ <!-- Events Panel -->
253
+ <div class="panel" id="panel-events">
254
+ <div class="events-layout">
255
+ <div class="events-toolbar">
256
+ <select class="form-select" id="eventsSessionSelect" onchange="loadEvents()" style="width:280px">
257
+ <option value="">Select session...</option>
258
+ </select>
259
+ <button class="btn btn-sm btn-secondary" onclick="copyEvents()">Copy JSON</button>
260
+ <span id="eventsStats" style="margin-left:auto;font-size:11px;color:var(--dim)"></span>
261
+ </div>
262
+ <div class="events-list" id="eventsList">
263
+ <div class="empty-state">
264
+ <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
265
+ <p>Select a session to inspect events</p>
266
+ </div>
267
+ </div>
268
+ </div>
269
+ </div>
270
+ </div>
271
+ </div>
272
+
273
+ <!-- Modals container -->
274
+ <div id="modals"></div>
275
+
276
+ __INJECT__
277
+ <script>// ── Icons (inline SVGs — Lucide-compatible) ──
278
+ const ICON = {
279
+ eye: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0"/><circle cx="12" cy="12" r="3"/></svg>',
280
+ eyeOff: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"/><path d="M14.084 14.158a3 3 0 0 1-4.242-4.242"/><path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"/><path d="m2 2 20 20"/></svg>',
281
+ x: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>',
282
+ refresh: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/></svg>',
283
+ plus: '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>',
284
+ pencil: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/></svg>',
285
+ trash: '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>',
286
+ };
287
+
288
+ // ── State ──
289
+ // Server-injected key ALWAYS wins over localStorage
290
+ let apiKey = "";
291
+ if (window.__MA_API_KEY__) {
292
+ apiKey = window.__MA_API_KEY__;
293
+ localStorage.setItem("ma-api-key", apiKey);
294
+ } else {
295
+ apiKey = localStorage.getItem("ma-api-key") || "";
296
+ }
297
+ let sessions = [];
298
+ let activeSessionId = null;
299
+ let sseReader = null;
300
+ let sseAbort = null;
301
+ let isRunning = false;
302
+ let agents = [];
303
+ let environments = [];
304
+ let allEvents = [];
305
+ let expandedEventIds = new Set();
306
+ let seenMessageTexts = new Set();
307
+ let lastAppendedRole = "";
308
+
309
+ let onboardingStep = 0;
310
+ let onboardVaultId = null;
311
+ let onboardBackend = "claude";
312
+
313
+ // ── Init ──
314
+ document.getElementById("apiKeyInput").value = apiKey;
315
+ document.getElementById("apiKeyInput").addEventListener("input", (e) => {
316
+ apiKey = e.target.value;
317
+ localStorage.setItem("ma-api-key", apiKey);
318
+ });
319
+ document.querySelectorAll(".tab").forEach((t) =>
320
+ t.addEventListener("click", () => switchTab(t.dataset.tab))
321
+ );
322
+ if (apiKey) { checkOnboarding(); }
323
+
324
+ // ── API wrapper ──
325
+ async function api(path, opts = {}) {
326
+ const headers = { "content-type": "application/json" };
327
+ if (apiKey) headers["x-api-key"] = apiKey;
328
+ const res = await fetch(path, { ...opts, headers: { ...headers, ...opts.headers } });
329
+ if (!res.ok) {
330
+ let msg = \`HTTP \${res.status}\`;
331
+ try { const e = await res.json(); if (e.error?.message) msg = e.error.message; } catch {}
332
+ if (res.status === 401) { showToast(msg, "error"); }
333
+ throw new Error(msg);
334
+ }
335
+ return res.json();
336
+ }
337
+
338
+ // ── Tabs ──
339
+ function switchTab(name) {
340
+ document.querySelectorAll(".tab").forEach((t) => t.classList.toggle("active", t.dataset.tab === name));
341
+ document.querySelectorAll(".panel").forEach((p) => p.classList.toggle("active", p.id === \`panel-\${name}\`));
342
+ if (name === "config") { loadResources(); loadVaults(); }
343
+ if (name === "events") loadSessionsForEvents();
344
+ }
345
+
346
+ // ── Toast ──
347
+ function showToast(msg, type = "success") {
348
+ const el = document.createElement("div");
349
+ el.className = \`toast toast-\${type}\`;
350
+ el.textContent = msg;
351
+ document.body.appendChild(el);
352
+ setTimeout(() => el.remove(), 3000);
353
+ }
354
+ function clearKey() { apiKey = ""; localStorage.removeItem("ma-api-key"); document.getElementById("apiKeyInput").value = ""; }
355
+ function toggleKeyVisibility() { const el = document.getElementById("apiKeyInput"); el.type = el.type === "password" ? "text" : "password"; }
356
+
357
+ // ═══════════════════════════════════════════════════════════════════
358
+ // ONBOARDING
359
+ // ═══════════════════════════════════════════════════════════════════
360
+
361
+ async function checkOnboarding() {
362
+ try {
363
+ const [a, e] = await Promise.all([api("/v1/agents?limit=1"), api("/v1/environments?limit=1")]);
364
+ if ((a.data || []).length === 0 || (e.data || []).filter(x => x.state === "ready").length === 0) {
365
+ agents = a.data || [];
366
+ environments = (e.data || []).filter(x => x.state === "ready");
367
+ onboardingStep = agents.length === 0 ? 0 : environments.length === 0 ? 1 : 2;
368
+ renderOnboarding();
369
+ } else {
370
+ loadSessions();
371
+ loadResources();
372
+ }
373
+ } catch (e) { loadSessions(); loadResources(); }
374
+ }
375
+
376
+ let onboardProvider = "docker";
377
+
378
+ function renderOnboarding() {
379
+ const el = document.getElementById("messages");
380
+ const steps = ["Agent", "Environment", "Secrets", "Chat"];
381
+ const stepIndicator = steps.map((s, i) =>
382
+ \`<span style="display:inline-flex;align-items:center;gap:6px">\` +
383
+ \`<span style="display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border-radius:50%;border:2px solid \${i < onboardingStep ? 'var(--success)' : i === onboardingStep ? 'var(--heading)' : 'var(--border-strong)'};font-size:11px;color:\${i < onboardingStep ? 'var(--success)' : i === onboardingStep ? 'var(--heading)' : 'var(--dim)'}">\${i < onboardingStep ? '✓' : i + 1}</span>\` +
384
+ \`<span style="font-size:12px;font-weight:500;color:\${i <= onboardingStep ? 'var(--heading)' : 'var(--dim)'}">\${s}</span>\` +
385
+ (i < steps.length - 1 ? \`<span style="color:var(--dim);margin:0 4px">→</span>\` : '') +
386
+ \`</span>\`
387
+ ).join("");
388
+
389
+ let content = "";
390
+
391
+ if (onboardingStep === 0) {
392
+ content = \`
393
+ <h2 style="font-size:18px;font-weight:600;color:var(--heading)">Create your first agent</h2>
394
+ <p style="font-size:13px;color:var(--muted);margin-top:4px">Pick a backend and model to get started.</p>
395
+ <div style="display:flex;flex-direction:column;gap:12px;margin-top:16px;width:100%">
396
+ <div class="form-group"><label class="form-label">Name</label><input class="form-input" id="obAgentName" value="Coder" style="width:100%" /></div>
397
+ <div class="form-group"><label class="form-label">Backend</label>
398
+ <select class="form-select" id="obBackend" onchange="updateObModels()" style="width:100%">
399
+ <option value="claude">Claude — Max subscription or API key</option>
400
+ <option value="opencode">OpenCode — Multi-provider</option>
401
+ <option value="codex">Codex — GPT-5.4 models</option>
402
+ <option value="gemini">Gemini — Google AI models</option>
403
+ <option value="factory">Factory — Multi-provider Droid</option>
404
+ </select>
405
+ </div>
406
+ <div class="form-group"><label class="form-label">Model</label><select class="form-select" id="obModel" style="width:100%"></select></div>
407
+ <button class="btn btn-primary" onclick="onboardCreateAgent()" style="width:100%">Create Agent</button>
408
+ </div>\`;
409
+ } else if (onboardingStep === 1) {
410
+ content = \`
411
+ <h2 style="font-size:18px;font-weight:600;color:var(--heading)">Set up an environment</h2>
412
+ <p style="font-size:13px;color:var(--muted);margin-top:4px">Where should your agent run?</p>
413
+ <div style="display:flex;flex-direction:column;gap:12px;margin-top:16px;width:100%">
414
+ <div class="form-group"><label class="form-label">Name</label><input class="form-input" id="obEnvName" value="dev" style="width:100%" /></div>
415
+ <div class="form-group"><label class="form-label">Provider</label>
416
+ <select class="form-select" id="obProvider" onchange="onboardProvider=this.value" style="width:100%">
417
+ <option value="docker">Docker — Local containers (~3s)</option>
418
+ <option value="sprites">sprites.dev — Cloud containers (~2s)</option>
419
+ <option value="apple">Apple Containers — macOS 26+ (~1s)</option>
420
+ </select>
421
+ </div>
422
+ <button class="btn btn-primary" onclick="onboardCreateEnv()" style="width:100%">Create Environment</button>
423
+ </div>\`;
424
+ } else if (onboardingStep === 2) {
425
+ // Build adaptive secret fields based on backend + provider
426
+ const fields = [];
427
+ const backendKeys = { claude: [{ key: "ANTHROPIC_API_KEY", label: "Anthropic API Key", placeholder: "sk-ant-...", alt: "or CLAUDE_CODE_OAUTH_TOKEN" }], opencode: [{ key: "OPENAI_API_KEY", label: "OpenAI API Key", placeholder: "sk-..." }], codex: [{ key: "OPENAI_API_KEY", label: "OpenAI API Key", placeholder: "sk-..." }], gemini: [{ key: "GEMINI_API_KEY", label: "Gemini API Key", placeholder: "AIza..." }], factory: [{ key: "FACTORY_API_KEY", label: "Factory API Key", placeholder: "fk-..." }] };
428
+ const providerKeys = { sprites: [{ key: "SPRITE_TOKEN", label: "Sprites.dev Token", placeholder: "user/org/.../token" }] };
429
+ (backendKeys[onboardBackend] || []).forEach(f => fields.push(f));
430
+ (providerKeys[onboardProvider] || []).forEach(f => fields.push(f));
431
+
432
+ const fieldHtml = fields.map((f, i) => \`
433
+ <div class="form-group">
434
+ <label class="form-label">\${f.label}\${f.alt ? \` <span style="color:var(--dim);font-weight:400">\${f.alt}</span>\` : ''}</label>
435
+ <input class="form-input" id="obSecret\${i}" type="password" placeholder="\${f.placeholder}" data-key="\${f.key}" style="width:100%" />
436
+ </div>\`).join("");
437
+
438
+ content = \`
439
+ <h2 style="font-size:18px;font-weight:600;color:var(--heading)">Add your secrets</h2>
440
+ <p style="font-size:13px;color:var(--muted);margin-top:4px">Keys are stored in a vault and injected into the container at runtime.</p>
441
+ <div style="display:flex;flex-direction:column;gap:12px;margin-top:16px;width:100%">
442
+ \${fieldHtml}
443
+ <button class="btn btn-primary" onclick="onboardSaveSecrets(\${fields.length})" style="width:100%">Save & Continue</button>
444
+ <button class="btn btn-secondary" onclick="onboardingStep=3;renderOnboarding()" style="width:100%">Skip (use server .env)</button>
445
+ </div>\`;
446
+ } else if (onboardingStep === 3) {
447
+ content = \`
448
+ <h2 style="font-size:18px;font-weight:600;color:var(--heading)">Ready to go!</h2>
449
+ <p style="font-size:13px;color:var(--muted);margin-top:4px">Your agent and environment are set up.</p>
450
+ <div style="background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:12px;margin-top:16px;width:100%;font-size:13px;display:flex;flex-direction:column;gap:6px">
451
+ <div style="display:flex;justify-content:space-between"><span style="color:var(--muted)">Agent</span><span style="color:var(--heading)">\${esc(agents[0]?.name || "")}</span></div>
452
+ <div style="display:flex;justify-content:space-between"><span style="color:var(--muted)">Backend</span><span style="font-family:var(--mono);font-size:11px">\${agents[0]?.backend || ""}</span></div>
453
+ <div style="display:flex;justify-content:space-between"><span style="color:var(--muted)">Model</span><span style="font-family:var(--mono);font-size:11px">\${agents[0]?.model || ""}</span></div>
454
+ <div style="display:flex;justify-content:space-between"><span style="color:var(--muted)">Environment</span><span style="color:var(--heading)">\${esc(environments[0]?.name || "")}</span></div>
455
+ \${onboardVaultId ? '<div style="display:flex;justify-content:space-between"><span style="color:var(--muted)">Secrets</span><span style="color:var(--success)">✓ Vault configured</span></div>' : '<div style="display:flex;justify-content:space-between"><span style="color:var(--muted)">Secrets</span><span style="color:var(--dim)">Using server .env</span></div>'}
456
+ </div>
457
+ <button class="btn btn-primary" onclick="onboardStart()" style="width:100%;margin-top:16px">Start Chatting</button>\`;
458
+ }
459
+
460
+ el.innerHTML = \`
461
+ <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;flex:1;gap:24px;padding:24px;max-width:440px;margin:0 auto">
462
+ <div style="display:flex;align-items:center;gap:4px">\${stepIndicator}</div>
463
+ \${content}
464
+ </div>\`;
465
+
466
+ if (onboardingStep === 0) setTimeout(updateObModels, 0);
467
+ }
468
+
469
+ function updateObModels() {
470
+ const backend = document.getElementById("obBackend")?.value || "claude";
471
+ const models = MODELS[backend] || MODELS.claude;
472
+ const el = document.getElementById("obModel");
473
+ if (el) el.innerHTML = models.map(m => \`<option value="\${m}">\${m}</option>\`).join("");
474
+ }
475
+
476
+ async function onboardCreateAgent() {
477
+ const name = document.getElementById("obAgentName")?.value?.trim();
478
+ const backend = document.getElementById("obBackend")?.value;
479
+ const model = document.getElementById("obModel")?.value;
480
+ if (!name) return;
481
+ try {
482
+ const body = { name, model, backend };
483
+ if (backend === "claude") body.tools = [{ type: "agent_toolset_20260401" }];
484
+ await api("/v1/agents", { method: "POST", body: JSON.stringify(body) });
485
+ const a = await api("/v1/agents?limit=50");
486
+ agents = a.data || [];
487
+ onboardBackend = backend;
488
+ onboardingStep = 1; // → environment step
489
+ renderOnboarding();
490
+ showToast("Agent created");
491
+ } catch (e) { showToast(e.message, "error"); }
492
+ }
493
+
494
+ async function onboardSaveSecrets(fieldCount) {
495
+ try {
496
+ // Collect all non-empty fields
497
+ const entries = [];
498
+ for (let i = 0; i < fieldCount; i++) {
499
+ const el = document.getElementById(\`obSecret\${i}\`);
500
+ if (el && el.value.trim()) {
501
+ let key = el.dataset.key;
502
+ const val = el.value.trim();
503
+ // Claude oauth token detection
504
+ if (key === "ANTHROPIC_API_KEY" && val.startsWith("sk-ant-oat")) {
505
+ key = "CLAUDE_CODE_OAUTH_TOKEN";
506
+ }
507
+ entries.push({ key, value: val });
508
+ }
509
+ }
510
+ if (entries.length === 0) { onboardingStep = 3; renderOnboarding(); return; }
511
+
512
+ // Create vault + set entries
513
+ const vault = await api("/v1/vaults", { method: "POST", body: JSON.stringify({ agent_id: agents[0].id, name: "secrets" }) });
514
+ onboardVaultId = vault.id;
515
+ for (const e of entries) {
516
+ await api(\`/v1/vaults/\${vault.id}/entries/\${e.key}\`, { method: "PUT", body: JSON.stringify({ value: e.value }) });
517
+ }
518
+ onboardingStep = 3;
519
+ renderOnboarding();
520
+ showToast(\`\${entries.length} secret(s) saved to vault\`);
521
+ } catch (e) { showToast(e.message, "error"); }
522
+ }
523
+
524
+ async function onboardCreateEnv() {
525
+ const name = document.getElementById("obEnvName")?.value?.trim();
526
+ const provider = document.getElementById("obProvider")?.value;
527
+ if (!name) return;
528
+ try {
529
+ await api("/v1/environments", { method: "POST", body: JSON.stringify({ name, config: { type: "cloud", provider } }) });
530
+ // Poll for ready state
531
+ showToast("Environment created — preparing...");
532
+ const check = async () => {
533
+ const e = await api("/v1/environments?limit=50");
534
+ environments = (e.data || []).filter(x => x.state === "ready");
535
+ if (environments.length > 0) {
536
+ onboardingStep = 2; // → secrets step
537
+ renderOnboarding();
538
+ } else {
539
+ setTimeout(check, 2000);
540
+ }
541
+ };
542
+ setTimeout(check, 2000);
543
+ } catch (e) { showToast(e.message, "error"); }
544
+ }
545
+
546
+ async function onboardStart() {
547
+ if (!agents[0] || !environments[0]) return;
548
+ try {
549
+ const sessionBody = { agent: agents[0].id, environment_id: environments[0].id };
550
+ if (onboardVaultId) sessionBody.vault_ids = [onboardVaultId];
551
+ const data = await api("/v1/sessions", { method: "POST", body: JSON.stringify(sessionBody) });
552
+
553
+ // Restore chat DOM (onboarding replaced #messages innerHTML)
554
+ const el = document.getElementById("messages");
555
+ el.innerHTML = '<div class="empty-state" id="chatEmpty" style="display:none"></div><div id="typing" style="display:none"></div><div id="messagesEnd"></div>';
556
+ document.getElementById("chatInputArea").style.display = "block";
557
+
558
+ await loadSessions();
559
+ await loadResources();
560
+ selectSession(data.id);
561
+ showToast("Session created — start chatting!");
562
+ } catch (e) { showToast(e.message, "error"); }
563
+ }
564
+
565
+ // ═══════════════════════════════════════════════════════════════════
566
+ // CHAT
567
+ // ═══════════════════════════════════════════════════════════════════
568
+
569
+ async function loadSessions() {
570
+ try {
571
+ const data = await api("/v1/sessions?limit=50&order=desc");
572
+ sessions = data.data || [];
573
+ renderSessionList();
574
+ } catch (e) { console.error(e); }
575
+ }
576
+
577
+ function renderSessionList() {
578
+ const el = document.getElementById("sessionList");
579
+ if (!sessions.length) { el.innerHTML = '<p style="padding:12px;text-align:center;font-size:11px;color:var(--dim)">No sessions</p>'; return; }
580
+ el.innerHTML = sessions.map((s) => \`
581
+ <div class="session-item \${s.id === activeSessionId ? 'active' : ''}" onclick="selectSession('\${esc(s.id)}')">
582
+ <div class="title">\${esc(s.title || s.id.slice(0, 16))}</div>
583
+ <div class="meta">
584
+ <span class="badge badge-\${s.status}">\${s.status}</span>
585
+ <span>\${new Date(s.created_at).toLocaleDateString()}</span>
586
+ </div>
587
+ </div>
588
+ \`).join("");
589
+ }
590
+
591
+ async function selectSession(id) {
592
+ activeSessionId = id;
593
+ seenMessageTexts.clear();
594
+ lastAppendedRole = "";
595
+ renderSessionList();
596
+ document.getElementById("chatEmpty").style.display = "none";
597
+ document.getElementById("chatInputArea").style.display = "block";
598
+ disconnectSSE();
599
+
600
+ // Load history
601
+ try {
602
+ const data = await api(\`/v1/sessions/\${id}/events?limit=200&order=asc\`);
603
+ const msgs = [];
604
+ for (const evt of (data.data || [])) {
605
+ const m = eventToMessage(evt);
606
+ if (m) msgs.push(m);
607
+ }
608
+ renderMessages(msgs);
609
+ const lastSeq = data.data?.length ? data.data[data.data.length - 1].seq : 0;
610
+ connectSSE(id, lastSeq);
611
+ } catch (e) { showToast(e.message, "error"); }
612
+ }
613
+
614
+ function eventToMessage(evt) {
615
+ if (evt.type === "user.message") {
616
+ const text = (evt.content || []).map((b) => b.text || "").join("");
617
+ return { role: "user", content: text, type: evt.type };
618
+ }
619
+ if (evt.type === "agent.message") {
620
+ const text = (evt.content || []).map((b) => b.text || "").join("");
621
+ if (text) return { role: "assistant", content: text, type: evt.type };
622
+ }
623
+ if (evt.type === "agent.tool_use" || evt.type === "agent.custom_tool_use") {
624
+ return { role: "tool", content: \`Using \${evt.name || "tool"}\`, type: evt.type };
625
+ }
626
+ if (evt.type === "session.status_running") { isRunning = true; renderTyping(); }
627
+ if (evt.type === "session.status_idle") { isRunning = false; renderTyping(); loadSessions(); }
628
+ if (evt.type === "session.error") {
629
+ isRunning = false; renderTyping(); loadSessions();
630
+ const errorMsg = evt.error?.message || evt.payload?.error?.message || "An unknown error occurred";
631
+ return { role: "error", content: errorMsg, type: evt.type };
632
+ }
633
+ return null;
634
+ }
635
+
636
+ function renderMessages(msgs) {
637
+ const el = document.getElementById("messages");
638
+ let html = "";
639
+ let prevRole = "";
640
+ for (const m of msgs) {
641
+ const rendered = m.role === "assistant" ? renderMarkdown(m.content) : esc(m.content);
642
+ const showRole = m.role !== prevRole && !(m.role === "tool" && prevRole === "assistant");
643
+ const roleHtml = showRole ? \`<div class="message-role">\${m.role === "tool" ? "agent" : m.role}</div>\` : "";
644
+ html += \`<div class="message \${m.role}">
645
+ \${roleHtml}
646
+ <div class="message-content">\${rendered}</div>
647
+ </div>\`;
648
+ prevRole = m.role === "tool" ? "assistant" : m.role; // tool doesn't break assistant grouping
649
+ }
650
+ html += '<div id="typing" style="display:none"><div class="typing"><div class="typing-dots"><div class="typing-dot"></div><div class="typing-dot"></div><div class="typing-dot"></div></div>Agent is thinking...</div></div>';
651
+ html += '<div id="messagesEnd"></div>';
652
+ el.innerHTML = html;
653
+ scrollToBottom();
654
+ }
655
+
656
+ function appendMessage(m) {
657
+ // Deduplicate: skip if we've already shown this exact role+content
658
+ const dedupKey = \`\${m.role}:\${m.content}\`;
659
+ if (seenMessageTexts.has(dedupKey)) return;
660
+ seenMessageTexts.add(dedupKey);
661
+
662
+ const typing = document.getElementById("typing");
663
+ const end = document.getElementById("messagesEnd");
664
+ if (!typing || !end) return;
665
+ const div = document.createElement("div");
666
+ div.className = \`message \${m.role}\`;
667
+ const rendered = m.role === "assistant" ? renderMarkdown(m.content) : esc(m.content);
668
+ const effectiveRole = m.role === "tool" ? "assistant" : m.role;
669
+ const showRole = effectiveRole !== lastAppendedRole;
670
+ const roleHtml = showRole ? \`<div class="message-role">\${m.role === "tool" ? "agent" : m.role}</div>\` : "";
671
+ div.innerHTML = \`\${roleHtml}<div class="message-content">\${rendered}</div>\`;
672
+ lastAppendedRole = effectiveRole;
673
+ typing.before(div);
674
+ scrollToBottom();
675
+ }
676
+
677
+ function renderTyping() {
678
+ const el = document.getElementById("typing");
679
+ if (el) el.style.display = isRunning ? "block" : "none";
680
+ if (isRunning) scrollToBottom();
681
+ }
682
+
683
+ function scrollToBottom() {
684
+ const el = document.getElementById("messagesEnd");
685
+ if (el) el.scrollIntoView({ behavior: "smooth" });
686
+ }
687
+
688
+ async function sendMessage() {
689
+ const input = document.getElementById("chatInput");
690
+ const text = input.value.trim();
691
+ if (!text || !activeSessionId) return;
692
+ input.value = "";
693
+ appendMessage({ role: "user", content: text });
694
+ try {
695
+ await api(\`/v1/sessions/\${activeSessionId}/events\`, {
696
+ method: "POST",
697
+ body: JSON.stringify({ events: [{ type: "user.message", content: [{ type: "text", text }] }] }),
698
+ });
699
+ } catch (e) { showToast(e.message, "error"); }
700
+ }
701
+
702
+ function handleChatKey(e) {
703
+ if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); }
704
+ }
705
+
706
+ // ── SSE ──
707
+ function connectSSE(sessionId, afterSeq) {
708
+ disconnectSSE();
709
+ const ctrl = new AbortController();
710
+ sseAbort = ctrl;
711
+
712
+ fetch(\`/v1/sessions/\${sessionId}/stream?after_seq=\${afterSeq}\`, {
713
+ headers: { "x-api-key": apiKey },
714
+ signal: ctrl.signal,
715
+ }).then((res) => {
716
+ const reader = res.body.getReader();
717
+ sseReader = reader;
718
+ const decoder = new TextDecoder();
719
+ let buffer = "";
720
+ function pump() {
721
+ reader.read().then(({ done, value }) => {
722
+ if (done) return;
723
+ buffer += decoder.decode(value, { stream: true });
724
+ const parts = buffer.split("\\n\\n");
725
+ buffer = parts.pop();
726
+ for (const part of parts) {
727
+ const evt = parseSSE(part);
728
+ if (evt && evt.type !== "ping") {
729
+ const m = eventToMessage(evt);
730
+ if (m) appendMessage(m);
731
+ }
732
+ }
733
+ pump();
734
+ }).catch(() => {});
735
+ }
736
+ pump();
737
+ }).catch(() => {});
738
+ }
739
+
740
+ function disconnectSSE() {
741
+ if (sseAbort) { sseAbort.abort(); sseAbort = null; }
742
+ sseReader = null;
743
+ }
744
+
745
+ function parseSSE(block) {
746
+ let data = null;
747
+ for (const line of block.split("\\n")) {
748
+ if (line.startsWith("data: ")) {
749
+ try { data = JSON.parse(line.slice(6)); } catch {}
750
+ }
751
+ }
752
+ return data;
753
+ }
754
+
755
+ // ── New Session Modal ──
756
+ async function showNewSessionModal() {
757
+ await loadResources();
758
+ const html = \`<div class="modal-overlay" onclick="if(event.target===this)closeModal()">
759
+ <div class="modal">
760
+ <h2>New Session</h2>
761
+ <div class="form-group">
762
+ <label class="form-label">Agent</label>
763
+ <select class="form-select" id="modalAgent">\${agents.map((a) => \`<option value="\${a.id}">\${esc(a.name)}</option>\`).join("")}</select>
764
+ </div>
765
+ <div class="form-group">
766
+ <label class="form-label">Environment</label>
767
+ <select class="form-select" id="modalEnv">\${environments.filter((e) => e.state === "ready").map((e) => \`<option value="\${e.id}">\${esc(e.name)}</option>\`).join("")}</select>
768
+ </div>
769
+ <div class="modal-actions">
770
+ <button class="btn btn-secondary" onclick="closeModal()">Cancel</button>
771
+ <button class="btn btn-primary" onclick="createSession()">Create</button>
772
+ </div>
773
+ </div>
774
+ </div>\`;
775
+ document.getElementById("modals").innerHTML = html;
776
+ }
777
+
778
+ async function createSession() {
779
+ const agentId = document.getElementById("modalAgent").value;
780
+ const envId = document.getElementById("modalEnv").value;
781
+ if (!agentId || !envId) return;
782
+ try {
783
+ // Auto-attach agent's vaults so secrets are available
784
+ const vaultsData = await api(\`/v1/vaults?agent_id=\${agentId}\`);
785
+ const vaultIds = (vaultsData.data || []).map(v => v.id);
786
+ const body = { agent: agentId, environment_id: envId };
787
+ if (vaultIds.length > 0) body.vault_ids = vaultIds;
788
+ const data = await api("/v1/sessions", { method: "POST", body: JSON.stringify(body) });
789
+ closeModal();
790
+ await loadSessions();
791
+ selectSession(data.id);
792
+ showToast("Session created");
793
+ } catch (e) { showToast(e.message, "error"); }
794
+ }
795
+
796
+ function closeModal() { document.getElementById("modals").innerHTML = ""; }
797
+
798
+ // ═══════════════════════════════════════════════════════════════════
799
+ // CONFIG
800
+ // ═══════════════════════════════════════════════════════════════════
801
+
802
+ async function loadResources() {
803
+ try {
804
+ const [a, e] = await Promise.all([api("/v1/agents?limit=50"), api("/v1/environments?limit=50")]);
805
+ agents = a.data || [];
806
+ environments = e.data || [];
807
+ renderAgents();
808
+ renderEnvs();
809
+ } catch (e) { console.error(e); }
810
+ }
811
+
812
+ function renderAgents() {
813
+ const el = document.getElementById("agentsList");
814
+ if (!agents.length) { el.innerHTML = '<p style="color:var(--dim);font-size:12px;padding:8px">No agents yet</p>'; return; }
815
+ el.innerHTML = agents.map((a) => \`
816
+ <div class="card-item">
817
+ <div><div class="name">\${esc(a.name)}</div><div class="detail">\${esc(a.model)} / \${a.backend}</div></div>
818
+ <div style="display:flex;gap:4px">
819
+ <button class="btn btn-sm btn-secondary" onclick="showAgentConfig('\${esc(a.id)}')">Config</button>
820
+ <button class="btn btn-sm btn-danger" onclick="deleteAgent('\${esc(a.id)}')">Delete</button>
821
+ </div>
822
+ </div>
823
+ \`).join("");
824
+ }
825
+
826
+ function toYaml(obj, indent = 0) {
827
+ const pad = " ".repeat(indent);
828
+ const lines = [];
829
+ for (const [k, v] of Object.entries(obj)) {
830
+ if (v === null || v === undefined) continue;
831
+ if (Array.isArray(v)) {
832
+ if (v.length === 0) continue;
833
+ lines.push(\`\${pad}\${k}:\`);
834
+ v.forEach(item => {
835
+ if (typeof item === "object") {
836
+ lines.push(\`\${pad} -\`);
837
+ lines.push(toYaml(item, indent + 2).replace(/^/, \`\${pad} \`).replace(/\\n/g, \`\\n\`));
838
+ } else {
839
+ lines.push(\`\${pad} - \${item}\`);
840
+ }
841
+ });
842
+ } else if (typeof v === "object") {
843
+ if (Object.keys(v).length === 0) continue;
844
+ lines.push(\`\${pad}\${k}:\`);
845
+ lines.push(toYaml(v, indent + 1));
846
+ } else {
847
+ lines.push(\`\${pad}\${k}: \${v}\`);
848
+ }
849
+ }
850
+ return lines.join("\\n");
851
+ }
852
+
853
+ let _configModalData = {};
854
+ function showAgentConfig(id) {
855
+ const agent = agents.find(a => a.id === id);
856
+ if (!agent) return;
857
+ _configModalData = { yaml: toYaml(agent), json: JSON.stringify(agent, null, 2) };
858
+ document.getElementById("modals").innerHTML = \`
859
+ <div class="modal-overlay" onclick="if(event.target===this)closeModal()">
860
+ <div class="modal" style="max-width:800px;width:90vw;max-height:80vh;display:flex;flex-direction:column">
861
+ <div class="modal-header">
862
+ <h3>\${esc(agent.name)} — Config</h3>
863
+ <button class="btn-icon" onclick="closeModal()">\${ICON.x}</button>
864
+ </div>
865
+ <pre style="flex:1;overflow:auto;margin:0;padding:16px;background:var(--surface);border-radius:6px;font-size:12px;font-family:var(--mono);color:var(--body);white-space:pre-wrap;border:1px solid var(--border)">\${esc(_configModalData.yaml)}</pre>
866
+ <div class="modal-actions" style="margin-top:12px">
867
+ <button class="btn btn-secondary" onclick="navigator.clipboard.writeText(_configModalData.yaml);showToast('Copied')">Copy YAML</button>
868
+ <button class="btn btn-secondary" onclick="navigator.clipboard.writeText(_configModalData.json);showToast('Copied')">Copy JSON</button>
869
+ <button class="btn btn-primary" onclick="closeModal()">Close</button>
870
+ </div>
871
+ </div>
872
+ </div>\`;
873
+ }
874
+
875
+ function renderEnvs() {
876
+ const el = document.getElementById("envsList");
877
+ if (!environments.length) { el.innerHTML = '<p style="color:var(--dim);font-size:12px;padding:8px">No environments yet</p>'; return; }
878
+ el.innerHTML = environments.map((e) => \`
879
+ <div class="card-item">
880
+ <div><div class="name">\${esc(e.name)}</div><div class="detail">\${e.state}\${e.config?.provider ? ' / ' + e.config.provider : ''}</div></div>
881
+ <button class="btn btn-sm btn-danger" onclick="deleteEnv('\${esc(e.id)}')">Delete</button>
882
+ </div>
883
+ \`).join("");
884
+ }
885
+
886
+ const MODELS = { claude: ["claude-sonnet-4-6","claude-opus-4-6","claude-haiku-4-5"], opencode: ["anthropic/claude-sonnet-4-6","openai/gpt-4o-mini"], codex: ["gpt-5.4-mini","gpt-5.4"], gemini: ["gemini-3.1-pro-preview","gemini-3","gemini-2.5-flash"], factory: ["claude-sonnet-4-6","gpt-5.4","gemini-3.1-pro-preview"] };
887
+
888
+ function showCreateAgentModal() {
889
+ document.getElementById("modals").innerHTML = \`<div class="modal-overlay" onclick="if(event.target===this)closeModal()">
890
+ <div class="modal">
891
+ <h2>Create Agent</h2>
892
+ <div class="form-group"><label class="form-label">Name</label><input class="form-input" id="agentName" value="Coder" /></div>
893
+ <div class="form-group"><label class="form-label">Backend</label>
894
+ <select class="form-select" id="agentBackend" onchange="updateModelOptions()">
895
+ <option value="claude">Claude</option><option value="opencode">OpenCode</option><option value="codex">Codex</option><option value="gemini">Gemini</option><option value="factory">Factory</option>
896
+ </select>
897
+ </div>
898
+ <div class="form-group"><label class="form-label">Model</label><select class="form-select" id="agentModel"></select></div>
899
+ <div class="modal-actions">
900
+ <button class="btn btn-secondary" onclick="closeModal()">Cancel</button>
901
+ <button class="btn btn-primary" onclick="createAgent()">Create</button>
902
+ </div>
903
+ </div>
904
+ </div>\`;
905
+ updateModelOptions();
906
+ }
907
+
908
+ function updateModelOptions() {
909
+ const backend = document.getElementById("agentBackend").value;
910
+ const models = MODELS[backend] || MODELS.claude;
911
+ document.getElementById("agentModel").innerHTML = models.map((m) => \`<option value="\${m}">\${m}</option>\`).join("");
912
+ }
913
+
914
+ async function createAgent() {
915
+ const name = document.getElementById("agentName").value.trim();
916
+ const backend = document.getElementById("agentBackend").value;
917
+ const model = document.getElementById("agentModel").value;
918
+ if (!name) return;
919
+ try {
920
+ const body = { name, model, backend };
921
+ if (backend === "claude") body.tools = [{ type: "agent_toolset_20260401" }];
922
+ await api("/v1/agents", { method: "POST", body: JSON.stringify(body) });
923
+ closeModal(); loadResources(); showToast("Agent created");
924
+ } catch (e) { showToast(e.message, "error"); }
925
+ }
926
+
927
+ async function deleteAgent(id) {
928
+ try { await api(\`/v1/agents/\${id}\`, { method: "DELETE" }); loadResources(); showToast("Agent deleted"); }
929
+ catch (e) { showToast(e.message, "error"); }
930
+ }
931
+
932
+ function showCreateEnvModal() {
933
+ document.getElementById("modals").innerHTML = \`<div class="modal-overlay" onclick="if(event.target===this)closeModal()">
934
+ <div class="modal">
935
+ <h2>Create Environment</h2>
936
+ <div class="form-group"><label class="form-label">Name</label><input class="form-input" id="envName" value="dev" style="width:100%" /></div>
937
+ <div class="form-group"><label class="form-label">Provider</label>
938
+ <select class="form-select" id="envProvider" onchange="toggleEnvToken()" style="width:100%">
939
+ <option value="docker">Docker — Local containers</option>
940
+ <option value="sprites">sprites.dev — Cloud containers</option>
941
+ <option value="apple">Apple Containers — macOS 26+</option>
942
+ <option value="e2b">E2B — AI sandboxes</option>
943
+ <option value="vercel">Vercel Sandbox — Firecracker VMs</option>
944
+ <option value="daytona">Daytona — Dev environments</option>
945
+ <option value="fly">Fly.io — Global VMs</option>
946
+ <option value="modal">Modal — Serverless containers</option>
947
+ </select>
948
+ </div>
949
+ <div class="form-group" id="envTokenGroup" style="display:none">
950
+ <label class="form-label" id="envTokenLabel">Token</label>
951
+ <input class="form-input" id="envToken" type="password" style="width:100%" />
952
+ <p style="font-size:10px;color:var(--dim);margin-top:2px">Saved to vault for this environment.</p>
953
+ </div>
954
+ <div class="modal-actions">
955
+ <button class="btn btn-secondary" onclick="closeModal()">Cancel</button>
956
+ <button class="btn btn-primary" onclick="createEnv()">Create</button>
957
+ </div>
958
+ </div>
959
+ </div>\`;
960
+ }
961
+
962
+ const PROVIDER_TOKENS = {
963
+ sprites: { key: "SPRITE_TOKEN", label: "Sprites.dev Token", placeholder: "user/org/.../token" },
964
+ e2b: { key: "E2B_API_KEY", label: "E2B API Key", placeholder: "e2b_..." },
965
+ vercel: { key: "VERCEL_TOKEN", label: "Vercel Token", placeholder: "..." },
966
+ daytona: { key: "DAYTONA_API_KEY", label: "Daytona API Key", placeholder: "..." },
967
+ fly: { key: "FLY_API_TOKEN", label: "Fly.io API Token", placeholder: "fo1_..." },
968
+ modal: { key: "MODAL_TOKEN_ID", label: "Modal Token ID", placeholder: "..." },
969
+ };
970
+
971
+ function toggleEnvToken() {
972
+ const provider = document.getElementById("envProvider")?.value;
973
+ const group = document.getElementById("envTokenGroup");
974
+ const label = document.getElementById("envTokenLabel");
975
+ const input = document.getElementById("envToken");
976
+ const info = PROVIDER_TOKENS[provider];
977
+ if (info) {
978
+ group.style.display = "block";
979
+ label.textContent = info.label;
980
+ input.placeholder = info.placeholder;
981
+ } else {
982
+ group.style.display = "none";
983
+ }
984
+ }
985
+
986
+ async function createEnv() {
987
+ const name = document.getElementById("envName").value.trim();
988
+ const provider = document.getElementById("envProvider").value;
989
+ const token = document.getElementById("envToken")?.value?.trim();
990
+ if (!name) return;
991
+ try {
992
+ const tokenInfo = PROVIDER_TOKENS[provider];
993
+ if (token && tokenInfo && agents.length > 0) {
994
+ let vaultId = null;
995
+ const existing = await api("/v1/vaults?limit=50");
996
+ const found = (existing.data || []).find(v => v.name === "secrets");
997
+ if (found) { vaultId = found.id; }
998
+ else { const vault = await api("/v1/vaults", { method: "POST", body: JSON.stringify({ agent_id: agents[0].id, name: "secrets" }) }); vaultId = vault.id; }
999
+ await api("/v1/vaults/" + vaultId + "/entries/" + tokenInfo.key, { method: "PUT", body: JSON.stringify({ value: token }) });
1000
+ }
1001
+ await api("/v1/environments", { method: "POST", body: JSON.stringify({ name, config: { type: "cloud", provider } }) });
1002
+ closeModal(); loadResources(); loadVaults(); showToast("Environment created");
1003
+ } catch (e) { showToast(e.message, "error"); }
1004
+ }
1005
+
1006
+ async function deleteEnv(id) {
1007
+ try {
1008
+ await api(\`/v1/environments/\${id}\`, { method: "DELETE" });
1009
+ loadResources(); showToast("Environment deleted");
1010
+ } catch (e) {
1011
+ if (e.message && e.message.includes("active sessions")) {
1012
+ if (confirm("This environment has active sessions. Archive them and delete?")) {
1013
+ try {
1014
+ const data = await api(\`/v1/sessions?environment_id=\${id}&limit=100\`);
1015
+ for (const s of (data.data || [])) {
1016
+ try { await api(\`/v1/sessions/\${s.id}/archive\`, { method: "POST" }); } catch {}
1017
+ }
1018
+ await api(\`/v1/environments/\${id}\`, { method: "DELETE" });
1019
+ loadResources(); loadSessions(); showToast("Environment and sessions cleaned up");
1020
+ } catch (e2) { showToast(e2.message, "error"); }
1021
+ }
1022
+ } else {
1023
+ showToast(e.message, "error");
1024
+ }
1025
+ }
1026
+ }
1027
+
1028
+ // ── Vaults/Secrets ──
1029
+ let vaults = [];
1030
+
1031
+ async function loadVaults() {
1032
+ try {
1033
+ const data = await api("/v1/vaults?limit=50");
1034
+ vaults = data.data || [];
1035
+ renderVaults();
1036
+ } catch (e) { console.error(e); }
1037
+ }
1038
+
1039
+ async function renderVaults() {
1040
+ const el = document.getElementById("vaultsList");
1041
+ if (!el) return;
1042
+ if (!vaults.length) { el.innerHTML = '<p style="color:var(--dim);font-size:12px;padding:8px">No vaults yet</p>'; return; }
1043
+
1044
+ let html = "";
1045
+ for (const v of vaults) {
1046
+ try {
1047
+ const entries = await api(\`/v1/vaults/\${v.id}/entries\`);
1048
+ const entryHtml = (entries.data || []).map(e =>
1049
+ \`<div style="display:flex;justify-content:space-between;align-items:center;padding:4px 0;border-bottom:1px solid var(--border)">
1050
+ <span style="font-family:var(--mono);font-size:11px;color:var(--accent)">\${esc(e.key)}</span>
1051
+ <div style="display:flex;gap:4px;align-items:center">
1052
+ <span style="font-size:10px;color:var(--dim)">\${e.value.slice(0,8)}...\${e.value.slice(-4)}</span>
1053
+ <button class="btn-icon" onclick="editVaultEntry('\${esc(v.id)}','\${esc(e.key)}')" title="Edit" style="font-size:10px">\${ICON.pencil}</button>
1054
+ <button class="btn-icon" onclick="deleteVaultEntry('\${esc(v.id)}','\${esc(e.key)}')" title="Delete" style="font-size:10px">\${ICON.trash}</button>
1055
+ </div>
1056
+ </div>\`
1057
+ ).join("");
1058
+
1059
+ html += \`<div style="margin-bottom:8px">
1060
+ <div class="card-item">
1061
+ <div><div class="name">\${esc(v.name)}</div><div class="detail">\${(entries.data || []).length} entries</div></div>
1062
+ <div style="display:flex;gap:4px">
1063
+ <button class="btn btn-sm btn-secondary" onclick="showAddEntryModal('\${esc(v.id)}')">+ Entry</button>
1064
+ <button class="btn btn-sm btn-danger" onclick="deleteVault('\${esc(v.id)}')">Delete</button>
1065
+ </div>
1066
+ </div>
1067
+ \${entryHtml}
1068
+ </div>\`;
1069
+ } catch (e) { console.error(e); }
1070
+ }
1071
+ el.innerHTML = html || '<p style="color:var(--dim);font-size:12px;padding:8px">No vaults</p>';
1072
+ }
1073
+
1074
+ function showCreateVaultModal() {
1075
+ document.getElementById("modals").innerHTML = \`<div class="modal-overlay" onclick="if(event.target===this)closeModal()">
1076
+ <div class="modal">
1077
+ <h2>Create Vault</h2>
1078
+ <div class="form-group"><label class="form-label">Agent</label>
1079
+ <select class="form-select" id="vaultAgent" style="width:100%">\${agents.map(a => \`<option value="\${a.id}">\${esc(a.name)}</option>\`).join("")}</select>
1080
+ </div>
1081
+ <div class="form-group"><label class="form-label">Vault Name</label><input class="form-input" id="vaultName" value="secrets" style="width:100%" /></div>
1082
+ <div class="modal-actions">
1083
+ <button class="btn btn-secondary" onclick="closeModal()">Cancel</button>
1084
+ <button class="btn btn-primary" onclick="createVault()">Create</button>
1085
+ </div>
1086
+ </div>
1087
+ </div>\`;
1088
+ }
1089
+
1090
+ async function createVault() {
1091
+ const agentId = document.getElementById("vaultAgent")?.value;
1092
+ const name = document.getElementById("vaultName")?.value?.trim();
1093
+ if (!agentId || !name) return;
1094
+ try {
1095
+ await api("/v1/vaults", { method: "POST", body: JSON.stringify({ agent_id: agentId, name }) });
1096
+ closeModal(); loadVaults(); showToast("Vault created");
1097
+ } catch (e) { showToast(e.message, "error"); }
1098
+ }
1099
+
1100
+ async function deleteVault(id) {
1101
+ try { await api(\`/v1/vaults/\${id}\`, { method: "DELETE" }); loadVaults(); showToast("Vault deleted"); }
1102
+ catch (e) { showToast(e.message, "error"); }
1103
+ }
1104
+
1105
+ function showAddEntryModal(vaultId) {
1106
+ document.getElementById("modals").innerHTML = \`<div class="modal-overlay" onclick="if(event.target===this)closeModal()">
1107
+ <div class="modal">
1108
+ <h2>Add Secret</h2>
1109
+ <div class="form-group"><label class="form-label">Key</label>
1110
+ <select class="form-select" id="entryKey" style="width:100%">
1111
+ <option value="ANTHROPIC_API_KEY">ANTHROPIC_API_KEY</option>
1112
+ <option value="CLAUDE_CODE_OAUTH_TOKEN">CLAUDE_CODE_OAUTH_TOKEN</option>
1113
+ <option value="OPENAI_API_KEY">OPENAI_API_KEY</option>
1114
+ <option value="SPRITE_TOKEN">SPRITE_TOKEN</option>
1115
+ <option value="custom">Custom key...</option>
1116
+ </select>
1117
+ </div>
1118
+ <div class="form-group" id="customKeyGroup" style="display:none"><label class="form-label">Custom Key Name</label><input class="form-input" id="customKeyName" style="width:100%" /></div>
1119
+ <div class="form-group"><label class="form-label">Value</label><input class="form-input" id="entryValue" type="password" style="width:100%" /></div>
1120
+ <div class="modal-actions">
1121
+ <button class="btn btn-secondary" onclick="closeModal()">Cancel</button>
1122
+ <button class="btn btn-primary" onclick="addEntry('\${esc(vaultId)}')">Save</button>
1123
+ </div>
1124
+ </div>
1125
+ </div>\`;
1126
+ document.getElementById("entryKey").addEventListener("change", (e) => {
1127
+ document.getElementById("customKeyGroup").style.display = e.target.value === "custom" ? "block" : "none";
1128
+ });
1129
+ }
1130
+
1131
+ async function addEntry(vaultId) {
1132
+ let key = document.getElementById("entryKey")?.value;
1133
+ if (key === "custom") key = document.getElementById("customKeyName")?.value?.trim();
1134
+ const value = document.getElementById("entryValue")?.value;
1135
+ if (!key || !value) { showToast("Key and value required", "error"); return; }
1136
+ try {
1137
+ await api(\`/v1/vaults/\${vaultId}/entries/\${key}\`, { method: "PUT", body: JSON.stringify({ value }) });
1138
+ closeModal(); loadVaults(); showToast("Secret saved");
1139
+ } catch (e) { showToast(e.message, "error"); }
1140
+ }
1141
+
1142
+ function editVaultEntry(vaultId, key) {
1143
+ document.getElementById("modals").innerHTML = \`<div class="modal-overlay" onclick="if(event.target===this)closeModal()">
1144
+ <div class="modal">
1145
+ <h2>Edit \${esc(key)}</h2>
1146
+ <div class="form-group"><label class="form-label">New Value</label><input class="form-input" id="editEntryValue" type="password" style="width:100%" /></div>
1147
+ <div class="modal-actions">
1148
+ <button class="btn btn-secondary" onclick="closeModal()">Cancel</button>
1149
+ <button class="btn btn-primary" onclick="updateEntry('\${esc(vaultId)}','\${esc(key)}')">Save</button>
1150
+ </div>
1151
+ </div>
1152
+ </div>\`;
1153
+ }
1154
+
1155
+ async function updateEntry(vaultId, key) {
1156
+ const value = document.getElementById("editEntryValue")?.value;
1157
+ if (!value) { showToast("Value required", "error"); return; }
1158
+ try {
1159
+ await api(\`/v1/vaults/\${vaultId}/entries/\${key}\`, { method: "PUT", body: JSON.stringify({ value }) });
1160
+ closeModal(); loadVaults(); showToast("Secret updated");
1161
+ } catch (e) { showToast(e.message, "error"); }
1162
+ }
1163
+
1164
+ async function deleteVaultEntry(vaultId, key) {
1165
+ try { await api(\`/v1/vaults/\${vaultId}/entries/\${key}\`, { method: "DELETE" }); loadVaults(); showToast("Secret deleted"); }
1166
+ catch (e) { showToast(e.message, "error"); }
1167
+ }
1168
+
1169
+ // ═══════════════════════════════════════════════════════════════════
1170
+ // EVENTS
1171
+ // ═══════════════════════════════════════════════════════════════════
1172
+
1173
+ async function loadSessionsForEvents() {
1174
+ try {
1175
+ const data = await api("/v1/sessions?limit=50&order=desc");
1176
+ const select = document.getElementById("eventsSessionSelect");
1177
+ select.innerHTML = '<option value="">Select session...</option>' +
1178
+ (data.data || []).map((s) => \`<option value="\${s.id}">\${esc(s.title || s.id.slice(0, 20))} (\${s.status})</option>\`).join("");
1179
+ } catch (e) { console.error(e); }
1180
+ }
1181
+
1182
+ async function loadEvents() {
1183
+ const sessionId = document.getElementById("eventsSessionSelect").value;
1184
+ if (!sessionId) return;
1185
+ try {
1186
+ const data = await api(\`/v1/sessions/\${sessionId}/events?limit=200&order=asc\`);
1187
+ allEvents = (data.data || []).map((evt, i, arr) => ({
1188
+ ...evt,
1189
+ deltaMs: i > 0 && evt.processed_at && arr[i-1].processed_at
1190
+ ? new Date(evt.processed_at) - new Date(arr[i-1].processed_at)
1191
+ : null,
1192
+ }));
1193
+ expandedEventIds.clear();
1194
+ renderEvents();
1195
+ } catch (e) { showToast(e.message, "error"); }
1196
+ }
1197
+
1198
+ function renderEvents() {
1199
+ const el = document.getElementById("eventsList");
1200
+ const stats = document.getElementById("eventsStats");
1201
+ if (!allEvents.length) { el.innerHTML = '<div class="empty-state"><p>No events</p></div>'; stats.textContent = ""; return; }
1202
+
1203
+ let totalIn = 0, totalOut = 0;
1204
+ allEvents.forEach((e) => {
1205
+ if (e.model_usage) { totalIn += e.model_usage.input_tokens || 0; totalOut += e.model_usage.output_tokens || 0; }
1206
+ });
1207
+ stats.textContent = \`\${allEvents.length} events\` + (totalIn ? \` · \${totalIn}↓ \${totalOut}↑ tokens\` : "");
1208
+
1209
+ el.innerHTML = allEvents.map((evt) => {
1210
+ const expanded = expandedEventIds.has(evt.id);
1211
+ const badge = badgeClass(evt.type);
1212
+ let preview = "";
1213
+ if (evt.type === "user.message" || evt.type === "agent.message") {
1214
+ const text = (evt.content || []).map((b) => b.text || "").join("");
1215
+ preview = text.length > 100 ? text.slice(0, 100) + "..." : text;
1216
+ } else if (evt.name) { preview = evt.name; }
1217
+
1218
+ let tokens = "";
1219
+ if (evt.model_usage) tokens = \`<span class="tokens">\${evt.model_usage.input_tokens || 0}↓ \${evt.model_usage.output_tokens || 0}↑</span>\`;
1220
+
1221
+ return \`<div class="event-row" onclick="toggleEvent('\${esc(evt.id)}')">
1222
+ <span class="seq">\${evt.seq}</span>
1223
+ <span class="badge \${badge}">\${evt.type}</span>
1224
+ <span class="preview">\${esc(preview)}</span>
1225
+ \${tokens}
1226
+ \${evt.deltaMs != null ? \`<span class="delta">+\${evt.deltaMs}ms</span>\` : ''}
1227
+ </div>
1228
+ \${expanded ? \`<div class="event-detail"><pre>\${esc(JSON.stringify(evt, null, 2))}</pre></div>\` : ''}\`;
1229
+ }).join("");
1230
+ }
1231
+
1232
+ function toggleEvent(id) {
1233
+ expandedEventIds.has(id) ? expandedEventIds.delete(id) : expandedEventIds.add(id);
1234
+ renderEvents();
1235
+ }
1236
+
1237
+ function badgeClass(type) {
1238
+ if (type.startsWith("user.")) return "badge-user";
1239
+ if (type.startsWith("agent.")) return "badge-agent";
1240
+ if (type.startsWith("session.error")) return "badge-error";
1241
+ if (type.startsWith("session.")) return "badge-status";
1242
+ if (type.startsWith("span.")) return "badge-span";
1243
+ return "badge-idle";
1244
+ }
1245
+
1246
+ function copyEvents() {
1247
+ navigator.clipboard.writeText(JSON.stringify(allEvents, null, 2));
1248
+ showToast("Copied JSON");
1249
+ }
1250
+
1251
+ // ── Util ──
1252
+ function esc(s) { if (!s) return ""; const d = document.createElement("div"); d.textContent = s; return d.innerHTML; }
1253
+
1254
+ function renderMarkdown(text) {
1255
+ if (typeof marked !== "undefined") {
1256
+ const html = marked.parse(text);
1257
+ return typeof DOMPurify !== "undefined" ? DOMPurify.sanitize(html) : html;
1258
+ }
1259
+ return '<p>' + esc(text).replace(/\\n\\n/g, '</p><p>').replace(/\\n/g, '<br>') + '</p>';
1260
+ }
1261
+ </script>
1262
+ </body>
1263
+ </html>
1264
+ `;
1265
+ const UI_VERSION = "77929c44";
1266
+
1267
+ export async function handleGetUI(opts?: { apiKey?: string }): Promise<Response> {
1268
+ const inject = opts?.apiKey
1269
+ ? `<script>window.__MA_API_KEY__ = "${opts.apiKey}";</script>`
1270
+ : "";
1271
+ const html = HTML_TEMPLATE.replace("__INJECT__", inject);
1272
+ return new Response(html, {
1273
+ headers: {
1274
+ "content-type": "text/html; charset=utf-8",
1275
+ "cache-control": "no-store, no-cache, must-revalidate, max-age=0",
1276
+ "pragma": "no-cache",
1277
+ "expires": "0",
1278
+ "etag": UI_VERSION,
1279
+ },
1280
+ });
1281
+ }