@qwen-code/arena-agent 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/.env.example +52 -0
  2. package/README.md +436 -0
  3. package/dist/agent.d.ts +33 -0
  4. package/dist/agent.js +366 -0
  5. package/dist/agent.js.map +1 -0
  6. package/dist/attachments.d.ts +29 -0
  7. package/dist/attachments.js +222 -0
  8. package/dist/attachments.js.map +1 -0
  9. package/dist/cli.d.ts +2 -0
  10. package/dist/cli.js +256 -0
  11. package/dist/cli.js.map +1 -0
  12. package/dist/compaction.d.ts +15 -0
  13. package/dist/compaction.js +176 -0
  14. package/dist/compaction.js.map +1 -0
  15. package/dist/config.d.ts +23 -0
  16. package/dist/config.js +61 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/document-reader.d.ts +18 -0
  19. package/dist/document-reader.js +423 -0
  20. package/dist/document-reader.js.map +1 -0
  21. package/dist/fetch-page.d.ts +25 -0
  22. package/dist/fetch-page.js +428 -0
  23. package/dist/fetch-page.js.map +1 -0
  24. package/dist/history.d.ts +5 -0
  25. package/dist/history.js +31 -0
  26. package/dist/history.js.map +1 -0
  27. package/dist/mock.d.ts +4 -0
  28. package/dist/mock.js +151 -0
  29. package/dist/mock.js.map +1 -0
  30. package/dist/protocol.d.ts +23 -0
  31. package/dist/protocol.js +84 -0
  32. package/dist/protocol.js.map +1 -0
  33. package/dist/provider.d.ts +31 -0
  34. package/dist/provider.js +308 -0
  35. package/dist/provider.js.map +1 -0
  36. package/dist/sdk.d.ts +181 -0
  37. package/dist/sdk.js +674 -0
  38. package/dist/sdk.js.map +1 -0
  39. package/dist/server.d.ts +15 -0
  40. package/dist/server.js +1433 -0
  41. package/dist/server.js.map +1 -0
  42. package/dist/spec.d.ts +11 -0
  43. package/dist/spec.js +198 -0
  44. package/dist/spec.js.map +1 -0
  45. package/dist/tools.d.ts +38 -0
  46. package/dist/tools.js +992 -0
  47. package/dist/tools.js.map +1 -0
  48. package/dist/tui.d.ts +10 -0
  49. package/dist/tui.js +350 -0
  50. package/dist/tui.js.map +1 -0
  51. package/dist/types.d.ts +146 -0
  52. package/dist/types.js +2 -0
  53. package/dist/types.js.map +1 -0
  54. package/dist/web/app.d.ts +1 -0
  55. package/dist/web/app.js +1082 -0
  56. package/dist/web/app.js.map +1 -0
  57. package/docs/SDK.md +521 -0
  58. package/package.json +59 -0
  59. package/spec/message.json +150 -0
  60. package/spec/tools.json +418 -0
  61. package/web/arena-logo.svg +11 -0
  62. package/web/fonts.css +67 -0
  63. package/web/index.html +285 -0
  64. package/web/styles.css +1798 -0
@@ -0,0 +1,1082 @@
1
+ import { ArenaClient } from "../sdk.js";
2
+ const elements = {
3
+ addFiles: document.querySelector("#add-files"),
4
+ announcement: document.querySelector("#announcement"),
5
+ attachmentList: document.querySelector("#attachment-list"),
6
+ artifactCount: document.querySelector("#artifact-count"),
7
+ artifactClose: document.querySelector("#artifact-close"),
8
+ artifactEmpty: document.querySelector("#artifact-empty"),
9
+ artifactList: document.querySelector("#artifact-list"),
10
+ artifactOpen: document.querySelector("#artifact-open"),
11
+ artifactPanel: document.querySelector("#artifact-panel"),
12
+ artifactPreview: document.querySelector("#artifact-preview"),
13
+ artifactPreviewBody: document.querySelector("#artifact-preview-body"),
14
+ artifactPreviewPath: document.querySelector("#artifact-preview-path"),
15
+ artifactTitle: document.querySelector("#artifact-title"),
16
+ artifactToggle: document.querySelector("#artifact-toggle"),
17
+ composer: document.querySelector("#composer"),
18
+ connection: document.querySelector("#connection"),
19
+ connectionLabel: document.querySelector("#connection-label"),
20
+ conversation: document.querySelector("#conversation"),
21
+ dismissBanner: document.querySelector("#dismiss-banner"),
22
+ emptyState: document.querySelector("#empty-state"),
23
+ fileInput: document.querySelector("#file-input"),
24
+ hidePromo: document.querySelector(".hide-promo"),
25
+ messages: document.querySelector("#messages"),
26
+ mobileSidebarToggle: document.querySelector("#mobile-sidebar-toggle"),
27
+ modeLabel: document.querySelector("#mode-label"),
28
+ modeMenu: document.querySelector("#mode-menu"),
29
+ modeToggle: document.querySelector("#mode-toggle"),
30
+ modelName: document.querySelector("#model-name"),
31
+ newChat: document.querySelector("#new-chat"),
32
+ prompt: document.querySelector("#prompt"),
33
+ send: document.querySelector("#send"),
34
+ sidebar: document.querySelector("#sidebar"),
35
+ sidebarToggle: document.querySelector("#sidebar-toggle"),
36
+ stop: document.querySelector("#stop"),
37
+ taskId: document.querySelector("#task-id"),
38
+ template: document.querySelector("#message-template"),
39
+ };
40
+ const state = {
41
+ session: null,
42
+ sessionId: "",
43
+ stream: null,
44
+ activityTimer: null,
45
+ activityLabel: "",
46
+ activityStartedAt: 0,
47
+ connecting: false,
48
+ busy: false,
49
+ currentAssistant: null,
50
+ currentReasoning: null,
51
+ tools: new Map(),
52
+ inputRequests: new Map(),
53
+ artifacts: new Map(),
54
+ activeArtifact: "",
55
+ pendingFiles: [],
56
+ visitId: localStorage.getItem("arena-visit-id") || uuidv7(),
57
+ generation: 0,
58
+ };
59
+ const maxAttachmentBytes = 20 * 1024 * 1024;
60
+ const maxAttachmentCount = 10;
61
+ const arena = new ArenaClient({
62
+ onCredentials(credentials) {
63
+ localStorage.setItem(sessionTokenKey(credentials.sessionId), credentials.token);
64
+ },
65
+ });
66
+ localStorage.setItem("arena-visit-id", state.visitId);
67
+ function uuidv7() {
68
+ const bytes = crypto.getRandomValues(new Uint8Array(16));
69
+ let timestamp = BigInt(Date.now());
70
+ for (let index = 5; index >= 0; index -= 1) {
71
+ bytes[index] = Number(timestamp & 0xffn);
72
+ timestamp >>= 8n;
73
+ }
74
+ bytes[6] = 0x70 | (bytes[6] & 0x0f);
75
+ bytes[8] = 0x80 | (bytes[8] & 0x3f);
76
+ const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");
77
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
78
+ }
79
+ function sessionFromLocation() {
80
+ const match = location.pathname.match(/^\/agent\/([A-Za-z0-9-]{1,128})$/);
81
+ return match?.[1] || "";
82
+ }
83
+ function sessionTokenKey(sessionId) {
84
+ return `arena-session-token:${sessionId}`;
85
+ }
86
+ function rememberSession() {
87
+ if (!state.session)
88
+ return;
89
+ const credentials = state.session.credentials;
90
+ localStorage.setItem("arena-session-id", state.sessionId);
91
+ localStorage.setItem(sessionTokenKey(credentials.sessionId), credentials.token);
92
+ }
93
+ function forgetSession(sessionId) {
94
+ if (!sessionId)
95
+ return;
96
+ localStorage.removeItem(sessionTokenKey(sessionId));
97
+ }
98
+ function setConnection(label, status = "connecting") {
99
+ elements.connection.dataset.state = status;
100
+ elements.connectionLabel.textContent = label;
101
+ }
102
+ function startActivity(label) {
103
+ clearInterval(state.activityTimer);
104
+ state.activityLabel = label;
105
+ state.activityStartedAt = Date.now();
106
+ const render = () => {
107
+ const seconds = Math.floor((Date.now() - state.activityStartedAt) / 1000);
108
+ setConnection(seconds > 0 ? `${state.activityLabel} ${seconds}s` : state.activityLabel, "connecting");
109
+ };
110
+ render();
111
+ state.activityTimer = setInterval(render, 1000);
112
+ }
113
+ function stopActivity() {
114
+ clearInterval(state.activityTimer);
115
+ state.activityTimer = null;
116
+ }
117
+ function markStep(name, status) {
118
+ const step = document.querySelector(`[data-step="${name}"]`);
119
+ if (!step)
120
+ return;
121
+ step.classList.toggle("is-active", status === "active");
122
+ step.classList.toggle("is-complete", status === "complete");
123
+ }
124
+ function enterDraft() {
125
+ state.session = null;
126
+ state.sessionId = "";
127
+ localStorage.removeItem("arena-session-id");
128
+ elements.taskId.textContent = "New chat";
129
+ setConnection("Ready", "ready");
130
+ }
131
+ async function createSession(generation) {
132
+ markStep("session", "active");
133
+ const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
134
+ const providedLocation = globalThis.ARENA_CLIENT_CONTEXT?.location;
135
+ const session = await arena.sessions.create({
136
+ timezone,
137
+ ...(providedLocation ? { location: providedLocation } : {}),
138
+ });
139
+ if (generation !== state.generation)
140
+ return false;
141
+ state.session = session;
142
+ state.sessionId = session.id;
143
+ markStep("session", "complete");
144
+ elements.taskId.textContent = state.sessionId.slice(0, 18);
145
+ rememberSession();
146
+ history.replaceState(null, "", `/agent/${state.sessionId}`);
147
+ return true;
148
+ }
149
+ async function ensureSession(generation) {
150
+ if (generation !== state.generation)
151
+ return false;
152
+ if (state.session)
153
+ return true;
154
+ return createSession(generation);
155
+ }
156
+ function clearConversation() {
157
+ elements.messages.replaceChildren();
158
+ elements.emptyState.hidden = false;
159
+ document.body.classList.remove("has-messages");
160
+ state.currentAssistant = null;
161
+ state.currentReasoning = null;
162
+ state.tools.clear();
163
+ state.inputRequests.clear();
164
+ state.artifacts.clear();
165
+ clearPendingFiles();
166
+ resetArtifactPreview();
167
+ renderArtifacts();
168
+ }
169
+ function fileIdentity(file) {
170
+ return `${file.name}:${file.size}:${file.lastModified}`;
171
+ }
172
+ function renderPendingFiles(error = "") {
173
+ elements.attachmentList.replaceChildren();
174
+ for (const file of state.pendingFiles) {
175
+ const chip = document.createElement("div");
176
+ chip.className = "attachment-chip";
177
+ const name = document.createElement("span");
178
+ name.textContent = file.name;
179
+ name.title = `${file.name} · ${Math.max(1, Math.ceil(file.size / 1024))} KB`;
180
+ const remove = document.createElement("button");
181
+ remove.type = "button";
182
+ remove.setAttribute("aria-label", `Remove ${file.name}`);
183
+ remove.textContent = "×";
184
+ remove.disabled = state.busy;
185
+ remove.addEventListener("click", () => {
186
+ const identity = fileIdentity(file);
187
+ state.pendingFiles = state.pendingFiles.filter((candidate) => fileIdentity(candidate) !== identity);
188
+ renderPendingFiles();
189
+ syncSendState();
190
+ });
191
+ chip.append(name, remove);
192
+ elements.attachmentList.append(chip);
193
+ }
194
+ if (error) {
195
+ const message = document.createElement("span");
196
+ message.className = "attachment-error";
197
+ message.textContent = error;
198
+ elements.attachmentList.append(message);
199
+ }
200
+ elements.attachmentList.hidden =
201
+ state.pendingFiles.length === 0 && !error;
202
+ }
203
+ function addPendingFiles(files) {
204
+ const existing = new Set(state.pendingFiles.map(fileIdentity));
205
+ let error = "";
206
+ for (const file of files) {
207
+ if (state.pendingFiles.length >= maxAttachmentCount) {
208
+ error = `Up to ${maxAttachmentCount} files per message`;
209
+ break;
210
+ }
211
+ if (file.size > maxAttachmentBytes) {
212
+ error = `${file.name} exceeds 20 MB`;
213
+ continue;
214
+ }
215
+ const identity = fileIdentity(file);
216
+ if (!existing.has(identity)) {
217
+ state.pendingFiles.push(file);
218
+ existing.add(identity);
219
+ }
220
+ }
221
+ renderPendingFiles(error);
222
+ syncSendState();
223
+ }
224
+ function clearPendingFiles() {
225
+ state.pendingFiles = [];
226
+ if (elements.fileInput)
227
+ elements.fileInput.value = "";
228
+ if (elements.attachmentList)
229
+ renderPendingFiles();
230
+ syncSendState();
231
+ }
232
+ function addMessage(role, text = "", extraClass = "") {
233
+ elements.emptyState.hidden = true;
234
+ document.body.classList.add("has-messages");
235
+ const fragment = elements.template.content.cloneNode(true);
236
+ const article = fragment.querySelector(".message");
237
+ const avatar = fragment.querySelector(".avatar");
238
+ const label = fragment.querySelector(".message-label");
239
+ const content = fragment.querySelector(".message-content");
240
+ article.className = `message ${role} ${extraClass}`.trim();
241
+ avatar.textContent = role === "assistant" ? "A" : "U";
242
+ label.textContent = role === "assistant" ? "Arena" : "You";
243
+ content.textContent = text;
244
+ elements.messages.append(fragment);
245
+ elements.conversation.scrollTop = elements.conversation.scrollHeight;
246
+ return elements.messages.lastElementChild;
247
+ }
248
+ function pretty(value) {
249
+ if (typeof value === "string")
250
+ return value;
251
+ try {
252
+ return JSON.stringify(value, null, 2);
253
+ }
254
+ catch {
255
+ return String(value);
256
+ }
257
+ }
258
+ function addReasoning(text = "") {
259
+ if (!state.currentAssistant)
260
+ state.currentAssistant = addMessage("assistant");
261
+ const details = document.createElement("details");
262
+ details.className = "reasoning";
263
+ details.open = true;
264
+ const summary = document.createElement("summary");
265
+ summary.textContent = "Reasoning";
266
+ const content = document.createElement("pre");
267
+ content.textContent = text;
268
+ details.append(summary, content);
269
+ const body = state.currentAssistant.querySelector(".message-body");
270
+ body.insertBefore(details, body.querySelector(".message-content"));
271
+ return details;
272
+ }
273
+ function addTool(toolCallId, toolName) {
274
+ if (!state.currentAssistant)
275
+ state.currentAssistant = addMessage("assistant");
276
+ const card = document.createElement("div");
277
+ card.className = "tool-card";
278
+ card.dataset.toolCallId = toolCallId;
279
+ card.dataset.toolName = toolName;
280
+ const head = document.createElement("div");
281
+ head.className = "tool-head";
282
+ const symbol = document.createElement("span");
283
+ symbol.className = "tool-symbol";
284
+ symbol.textContent = "⌘";
285
+ const name = document.createElement("span");
286
+ name.className = "tool-name";
287
+ name.textContent = toolName;
288
+ const status = document.createElement("span");
289
+ status.className = "tool-status";
290
+ status.textContent = "Running";
291
+ head.append(symbol, name, status);
292
+ const content = document.createElement("pre");
293
+ card.append(head, content);
294
+ state.currentAssistant.querySelector(".message-body").append(card);
295
+ state.tools.set(toolCallId, card);
296
+ return card;
297
+ }
298
+ function markInputSubmitted(requestId) {
299
+ const card = state.inputRequests.get(requestId);
300
+ if (!card)
301
+ return;
302
+ card.classList.add("is-submitted");
303
+ card.querySelectorAll("input, button").forEach((control) => {
304
+ control.disabled = true;
305
+ });
306
+ card.querySelector(".user-input-status").textContent = "Submitted";
307
+ }
308
+ async function submitInputResponse(requestId, answers) {
309
+ if (!state.session)
310
+ throw new Error("The session is no longer active");
311
+ await state.session.respondToInput(requestId, answers);
312
+ }
313
+ function addInputRequest(request) {
314
+ if (state.inputRequests.has(request.requestId)) {
315
+ return state.inputRequests.get(request.requestId);
316
+ }
317
+ const toolCard = state.tools.get(request.toolCallId);
318
+ const assistant = toolCard?.closest(".message") ||
319
+ state.currentAssistant ||
320
+ addMessage("assistant");
321
+ const card = document.createElement("form");
322
+ card.className = "user-input-card";
323
+ card.dataset.requestId = request.requestId;
324
+ const heading = document.createElement("div");
325
+ heading.className = "user-input-heading";
326
+ const title = document.createElement("strong");
327
+ title.textContent = "Input needed";
328
+ const status = document.createElement("span");
329
+ status.className = "user-input-status";
330
+ status.textContent = "Waiting";
331
+ heading.append(title, status);
332
+ card.append(heading);
333
+ for (const [index, question] of (request.questions || []).entries()) {
334
+ const fieldset = document.createElement("fieldset");
335
+ fieldset.dataset.questionId = question.id;
336
+ const legend = document.createElement("legend");
337
+ legend.textContent = question.question;
338
+ fieldset.append(legend);
339
+ const groupName = `answer-${request.requestId}-${index}`;
340
+ for (const option of question.options || []) {
341
+ const label = document.createElement("label");
342
+ label.className = "user-input-option";
343
+ const input = document.createElement("input");
344
+ input.type = "radio";
345
+ input.name = groupName;
346
+ input.value = option.id;
347
+ input.required = true;
348
+ const copy = document.createElement("span");
349
+ const optionTitle = document.createElement("strong");
350
+ optionTitle.textContent = option.label;
351
+ copy.append(optionTitle);
352
+ if (option.description) {
353
+ const description = document.createElement("small");
354
+ description.textContent = option.description;
355
+ copy.append(description);
356
+ }
357
+ label.append(input, copy);
358
+ fieldset.append(label);
359
+ }
360
+ if (question.allowCustomResponse !== false) {
361
+ const custom = document.createElement("label");
362
+ custom.className = "user-input-option user-input-custom";
363
+ const input = document.createElement("input");
364
+ input.type = "radio";
365
+ input.name = groupName;
366
+ input.value = "__custom__";
367
+ input.required = true;
368
+ const text = document.createElement("input");
369
+ text.type = "text";
370
+ text.placeholder = "Other…";
371
+ text.addEventListener("focus", () => {
372
+ input.checked = true;
373
+ });
374
+ custom.append(input, text);
375
+ fieldset.append(custom);
376
+ }
377
+ card.append(fieldset);
378
+ }
379
+ const submit = document.createElement("button");
380
+ submit.className = "user-input-submit";
381
+ submit.type = "submit";
382
+ submit.textContent = "Continue";
383
+ card.append(submit);
384
+ card.addEventListener("submit", async (event) => {
385
+ event.preventDefault();
386
+ const answers = [];
387
+ for (const fieldset of card.querySelectorAll("fieldset")) {
388
+ const selected = fieldset.querySelector('input[type="radio"]:checked');
389
+ if (!selected) {
390
+ card.reportValidity();
391
+ return;
392
+ }
393
+ const answer = { questionId: fieldset.dataset.questionId };
394
+ if (selected.value === "__custom__") {
395
+ const customResponse = fieldset.querySelector('.user-input-custom input[type="text"]').value.trim();
396
+ if (!customResponse) {
397
+ status.textContent = "Enter a custom answer";
398
+ return;
399
+ }
400
+ answer.customResponse = customResponse;
401
+ }
402
+ else {
403
+ answer.optionId = selected.value;
404
+ }
405
+ answers.push(answer);
406
+ }
407
+ submit.disabled = true;
408
+ status.textContent = "Submitting…";
409
+ try {
410
+ await submitInputResponse(request.requestId, answers);
411
+ markInputSubmitted(request.requestId);
412
+ startActivity("Continuing…");
413
+ }
414
+ catch (error) {
415
+ submit.disabled = false;
416
+ status.textContent = error.message;
417
+ }
418
+ });
419
+ assistant.querySelector(".message-body").append(card);
420
+ state.inputRequests.set(request.requestId, card);
421
+ return card;
422
+ }
423
+ function fileExtension(filePath) {
424
+ const name = filePath.split("/").pop() || "";
425
+ return (name.includes(".") ? name.split(".").pop() : "FILE").slice(0, 4).toUpperCase();
426
+ }
427
+ function normalizeArtifact(artifact) {
428
+ if (!artifact || typeof artifact.path !== "string")
429
+ return null;
430
+ const normalized = artifact.path
431
+ .replace(/^\/home\/user\/?/, "")
432
+ .replace(/\\/g, "/")
433
+ .replace(/^\.\/+/, "");
434
+ if (!normalized ||
435
+ normalized.startsWith("/") ||
436
+ normalized.split("/").some((part) => !part || part === "." || part === ".." || part.startsWith("."))) {
437
+ return null;
438
+ }
439
+ return {
440
+ ...artifact,
441
+ path: normalized,
442
+ url: artifact.url ||
443
+ `/api/chat/sessions/${state.sessionId}/workspace/${normalized
444
+ .split("/")
445
+ .map(encodeURIComponent)
446
+ .join("/")}`,
447
+ };
448
+ }
449
+ function artifactPreviewKind(filePath) {
450
+ const extension = fileExtension(filePath).toLowerCase();
451
+ if (["bmp", "gif", "ico", "jpeg", "jpg", "png", "svg", "webp"].includes(extension)) {
452
+ return "image";
453
+ }
454
+ if (["aac", "m4a", "mp3", "ogg", "wav"].includes(extension))
455
+ return "audio";
456
+ if (["avi", "mov", "mp4", "ogv", "webm"].includes(extension))
457
+ return "video";
458
+ if (["docx", "pptx", "xlsx"].includes(extension))
459
+ return "download";
460
+ return "frame";
461
+ }
462
+ function resetArtifactPreview() {
463
+ state.activeArtifact = "";
464
+ elements.artifactPanel?.classList.remove("has-preview");
465
+ if (elements.artifactPreview)
466
+ elements.artifactPreview.hidden = true;
467
+ elements.artifactPreviewBody?.replaceChildren();
468
+ if (elements.artifactPreviewPath)
469
+ elements.artifactPreviewPath.textContent = "";
470
+ if (elements.artifactTitle)
471
+ elements.artifactTitle.textContent = "Artifacts";
472
+ elements.artifactOpen?.removeAttribute("href");
473
+ }
474
+ function renderArtifactPreview(artifact) {
475
+ const kind = artifactPreviewKind(artifact.path);
476
+ const name = artifact.path.split("/").pop();
477
+ const preview = elements.artifactPreviewBody;
478
+ preview.replaceChildren();
479
+ let content;
480
+ if (kind === "image") {
481
+ content = document.createElement("img");
482
+ content.alt = name;
483
+ content.src = artifact.url;
484
+ }
485
+ else if (kind === "audio") {
486
+ content = document.createElement("audio");
487
+ content.controls = true;
488
+ content.src = artifact.url;
489
+ }
490
+ else if (kind === "video") {
491
+ content = document.createElement("video");
492
+ content.controls = true;
493
+ content.src = artifact.url;
494
+ }
495
+ else if (kind === "frame") {
496
+ content = document.createElement("iframe");
497
+ content.title = `Preview of ${name}`;
498
+ content.src = artifact.url;
499
+ content.setAttribute("sandbox", "allow-scripts");
500
+ content.referrerPolicy = "no-referrer";
501
+ }
502
+ else {
503
+ content = document.createElement("div");
504
+ content.className = "artifact-preview-fallback";
505
+ const icon = document.createElement("span");
506
+ icon.textContent = fileExtension(artifact.path);
507
+ const title = document.createElement("strong");
508
+ title.textContent = "Preview unavailable";
509
+ const description = document.createElement("p");
510
+ description.textContent = "Open or download this file to view it in a compatible app.";
511
+ content.append(icon, title, description);
512
+ }
513
+ content.classList.add("artifact-preview-content");
514
+ preview.append(content);
515
+ elements.artifactPreviewPath.textContent = artifact.path;
516
+ elements.artifactOpen.href = artifact.url;
517
+ elements.artifactTitle.textContent = name;
518
+ elements.artifactPreview.hidden = false;
519
+ elements.artifactPanel.classList.add("has-preview");
520
+ }
521
+ function renderArtifacts() {
522
+ elements.artifactList.replaceChildren();
523
+ elements.artifactCount.textContent = String(state.artifacts.size);
524
+ elements.artifactEmpty.hidden = state.artifacts.size > 0;
525
+ for (const artifact of state.artifacts.values()) {
526
+ const item = document.createElement("button");
527
+ item.className = "artifact-item";
528
+ item.type = "button";
529
+ item.classList.toggle("is-active", artifact.path === state.activeArtifact);
530
+ item.setAttribute("aria-label", `Preview ${artifact.path}`);
531
+ const icon = document.createElement("span");
532
+ icon.className = "file-icon";
533
+ icon.textContent = fileExtension(artifact.path);
534
+ const details = document.createElement("span");
535
+ const name = document.createElement("strong");
536
+ name.textContent = artifact.path.split("/").pop();
537
+ const fullPath = document.createElement("small");
538
+ fullPath.textContent = artifact.path;
539
+ details.append(name, fullPath);
540
+ const arrow = document.createElement("span");
541
+ arrow.className = "artifact-arrow";
542
+ arrow.textContent = "›";
543
+ item.append(icon, details, arrow);
544
+ item.addEventListener("click", () => presentArtifact(artifact));
545
+ elements.artifactList.append(item);
546
+ }
547
+ }
548
+ function addArtifact(artifact) {
549
+ const normalized = normalizeArtifact(artifact);
550
+ if (!normalized)
551
+ return null;
552
+ state.artifacts.set(normalized.path, {
553
+ ...state.artifacts.get(normalized.path),
554
+ ...normalized,
555
+ });
556
+ renderArtifacts();
557
+ return state.artifacts.get(normalized.path);
558
+ }
559
+ function presentArtifact(artifact) {
560
+ const normalized = addArtifact(artifact);
561
+ if (!normalized)
562
+ return;
563
+ state.activeArtifact = normalized.path;
564
+ renderArtifactPreview(normalized);
565
+ renderArtifacts();
566
+ setArtifactPanelOpen(true);
567
+ }
568
+ function renderHistory(messages) {
569
+ if (!messages.length)
570
+ return;
571
+ elements.emptyState.hidden = true;
572
+ document.body.classList.add("has-messages");
573
+ for (const message of messages) {
574
+ if (message.role === "user") {
575
+ if (typeof message.content === "string") {
576
+ addMessage("user", message.content);
577
+ }
578
+ else if (Array.isArray(message.content)) {
579
+ const text = message.content
580
+ .filter((part) => typeof part?.text === "string" &&
581
+ !part.text.includes("<arena-system-message>"))
582
+ .map((part) => part.text.trim())
583
+ .filter(Boolean)
584
+ .join("\n");
585
+ const notice = message.content
586
+ .filter((part) => typeof part?.text === "string")
587
+ .map((part) => part.text.match(/The user attached the following files \(saved to \/home\/user\/uploads\/\):\n([\s\S]*?)\n<\/arena-system-message>/)?.[1])
588
+ .find(Boolean);
589
+ const display = [
590
+ text,
591
+ notice ? `Attached: ${notice.replace(/,\n/g, ", ")}` : "",
592
+ ]
593
+ .filter(Boolean)
594
+ .join("\n");
595
+ if (display)
596
+ addMessage("user", display);
597
+ }
598
+ }
599
+ else if (message.role === "assistant") {
600
+ if (message.reasoning_content) {
601
+ state.currentAssistant = addMessage("assistant", message.content || "");
602
+ state.currentReasoning = addReasoning(message.reasoning_content);
603
+ state.currentReasoning.open = false;
604
+ }
605
+ else if (message.content) {
606
+ state.currentAssistant = addMessage("assistant", message.content);
607
+ }
608
+ for (const call of message.tool_calls || []) {
609
+ const card = addTool(call.id, call.function?.name || "tool");
610
+ card.dataset.toolName = call.function?.name || "tool";
611
+ card.querySelector("pre").textContent = pretty(parseJson(call.function?.arguments || "{}"));
612
+ }
613
+ }
614
+ else if (message.role === "tool") {
615
+ const card = state.tools.get(message.tool_call_id);
616
+ if (card) {
617
+ const output = parseJson(message.content);
618
+ card.classList.add("is-complete");
619
+ card.querySelector(".tool-status").textContent = "Complete";
620
+ card.querySelector("pre").textContent += `\n\nOutput\n${pretty(output)}`;
621
+ const path = output?.path || output?.file_path;
622
+ if (path && output?.status !== "error") {
623
+ const artifact = {
624
+ path,
625
+ sourceTool: card.dataset.toolName,
626
+ };
627
+ if (card.dataset.toolName === "present_file") {
628
+ presentArtifact(artifact);
629
+ }
630
+ else {
631
+ addArtifact(artifact);
632
+ }
633
+ }
634
+ }
635
+ }
636
+ }
637
+ state.currentAssistant = null;
638
+ state.currentReasoning = null;
639
+ elements.conversation.scrollTop = elements.conversation.scrollHeight;
640
+ }
641
+ function parseJson(value) {
642
+ if (typeof value !== "string")
643
+ return value;
644
+ try {
645
+ return JSON.parse(value);
646
+ }
647
+ catch {
648
+ return value;
649
+ }
650
+ }
651
+ async function loadHistory() {
652
+ if (!state.session)
653
+ return;
654
+ const value = await state.session.getHistory();
655
+ renderHistory(value.messages || []);
656
+ if (value.activePrompt)
657
+ addMessage("user", value.activePrompt);
658
+ }
659
+ function setBusy(busy) {
660
+ state.busy = busy;
661
+ elements.send.disabled =
662
+ busy || (!elements.prompt.value.trim() && !state.pendingFiles.length);
663
+ elements.stop.hidden = !busy;
664
+ elements.prompt.disabled = busy;
665
+ elements.addFiles.disabled = busy;
666
+ elements.fileInput.disabled = busy;
667
+ renderPendingFiles();
668
+ }
669
+ function syncSendState() {
670
+ elements.send.disabled =
671
+ state.busy ||
672
+ (!elements.prompt.value.trim() && !state.pendingFiles.length);
673
+ }
674
+ function setSidebarExpanded(expanded) {
675
+ if (!elements.sidebar)
676
+ return;
677
+ elements.sidebar.classList.toggle("is-expanded", expanded);
678
+ elements.sidebarToggle?.setAttribute("aria-expanded", String(expanded));
679
+ elements.mobileSidebarToggle?.setAttribute("aria-expanded", String(expanded));
680
+ localStorage.setItem("arena-sidebar-expanded", String(expanded));
681
+ }
682
+ function closeModeMenu() {
683
+ if (!elements.modeMenu)
684
+ return;
685
+ elements.modeMenu.hidden = true;
686
+ elements.modeToggle?.setAttribute("aria-expanded", "false");
687
+ }
688
+ function toggleModeMenu() {
689
+ if (!elements.modeMenu)
690
+ return;
691
+ const willOpen = elements.modeMenu.hidden;
692
+ elements.modeMenu.hidden = !willOpen;
693
+ elements.modeToggle?.setAttribute("aria-expanded", String(willOpen));
694
+ }
695
+ function setArtifactPanelOpen(open) {
696
+ if (!elements.artifactPanel)
697
+ return;
698
+ elements.artifactPanel.hidden = !open;
699
+ elements.artifactToggle?.setAttribute("aria-expanded", String(open));
700
+ }
701
+ function handleStreamEvent(event) {
702
+ switch (event.type) {
703
+ case "start":
704
+ markStep("model", "active");
705
+ setBusy(true);
706
+ startActivity("Starting…");
707
+ state.currentAssistant = null;
708
+ state.currentReasoning = null;
709
+ break;
710
+ case "reasoning-start":
711
+ state.currentReasoning = addReasoning();
712
+ break;
713
+ case "reasoning-delta":
714
+ if (!state.currentReasoning)
715
+ state.currentReasoning = addReasoning();
716
+ state.currentReasoning.querySelector("pre").textContent += event.delta || "";
717
+ break;
718
+ case "reasoning-end":
719
+ if (state.currentReasoning)
720
+ state.currentReasoning.open = false;
721
+ state.currentReasoning = null;
722
+ break;
723
+ case "text-start":
724
+ if (!state.currentAssistant) {
725
+ state.currentAssistant = addMessage("assistant");
726
+ }
727
+ break;
728
+ case "text-delta":
729
+ if (!state.currentAssistant)
730
+ state.currentAssistant = addMessage("assistant");
731
+ state.currentAssistant.querySelector(".message-content").textContent += event.delta || "";
732
+ break;
733
+ case "tool-input-start":
734
+ addTool(event.toolCallId, event.toolName)
735
+ .querySelector(".tool-status").textContent = "Preparing";
736
+ break;
737
+ case "tool-input-delta": {
738
+ const card = state.tools.get(event.toolCallId) ||
739
+ addTool(event.toolCallId, event.toolName || "tool");
740
+ card.querySelector(".tool-name").textContent = event.toolName || "tool";
741
+ card.dataset.toolName = event.toolName || "tool";
742
+ card.querySelector("pre").textContent += event.inputTextDelta || "";
743
+ break;
744
+ }
745
+ case "tool-input-available": {
746
+ const card = state.tools.get(event.toolCallId) ||
747
+ addTool(event.toolCallId, event.toolName || "tool");
748
+ card.querySelector(".tool-name").textContent = event.toolName || "tool";
749
+ card.dataset.toolName = event.toolName || "tool";
750
+ card.querySelector("pre").textContent = pretty(event.input);
751
+ break;
752
+ }
753
+ case "tool-output-available": {
754
+ const card = state.tools.get(event.toolCallId) || addTool(event.toolCallId, "tool");
755
+ card.classList.add("is-complete");
756
+ card.querySelector(".tool-status").textContent = "Complete";
757
+ card.querySelector("pre").textContent += `\n\nOutput\n${pretty(event.output)}`;
758
+ break;
759
+ }
760
+ case "data-user-input-request": {
761
+ const request = event.data || {};
762
+ addInputRequest(request);
763
+ const tool = state.tools.get(request.toolCallId);
764
+ if (tool)
765
+ tool.querySelector(".tool-status").textContent = "Waiting";
766
+ startActivity("Waiting for your answer…");
767
+ break;
768
+ }
769
+ case "data-user-input-submitted":
770
+ markInputSubmitted(event.data?.requestId);
771
+ startActivity("Continuing…");
772
+ break;
773
+ case "data-agent-status": {
774
+ const activity = event.data || {};
775
+ if (activity.phase === "model") {
776
+ state.currentAssistant = null;
777
+ state.currentReasoning = null;
778
+ }
779
+ startActivity(activity.label || "Running…");
780
+ if (activity.toolCallId) {
781
+ const card = state.tools.get(activity.toolCallId);
782
+ if (card && activity.phase === "tool-running") {
783
+ card.querySelector(".tool-status").textContent = "Running";
784
+ }
785
+ if (card && activity.phase === "tool-complete") {
786
+ card.classList.add("is-complete");
787
+ card.querySelector(".tool-status").textContent = "Complete";
788
+ }
789
+ }
790
+ break;
791
+ }
792
+ case "data-workspace":
793
+ addArtifact(event.data);
794
+ break;
795
+ case "data-present-file":
796
+ presentArtifact(event.data);
797
+ break;
798
+ case "error":
799
+ stopActivity();
800
+ addMessage("assistant", event.errorText || "The agent stopped unexpectedly.", "error");
801
+ setConnection("Agent error", "error");
802
+ break;
803
+ case "trigger:turn-complete":
804
+ stopActivity();
805
+ setBusy(false);
806
+ state.currentAssistant = null;
807
+ state.currentReasoning = null;
808
+ markStep("model", "complete");
809
+ setConnection(event.status === "success" ? "Ready" : "Agent error", event.status === "success" ? "ready" : "error");
810
+ elements.prompt.disabled = false;
811
+ elements.prompt.focus();
812
+ break;
813
+ default:
814
+ break;
815
+ }
816
+ elements.conversation.scrollTop = elements.conversation.scrollHeight;
817
+ }
818
+ async function connectStream() {
819
+ if (!state.session || state.connecting || state.stream)
820
+ return;
821
+ const generation = state.generation;
822
+ state.connecting = true;
823
+ markStep("stream", "active");
824
+ setConnection("Connecting", "connecting");
825
+ const stream = state.session.connect({
826
+ reconnect: true,
827
+ onEvent: handleStreamEvent,
828
+ onStateChange(connectionState) {
829
+ if (generation !== state.generation || state.stream !== stream)
830
+ return;
831
+ if (connectionState === "connecting") {
832
+ setConnection("Connecting", "connecting");
833
+ }
834
+ else if (connectionState === "reconnecting") {
835
+ setConnection("Reconnecting", "connecting");
836
+ }
837
+ else if (connectionState === "connected") {
838
+ markStep("stream", "complete");
839
+ if (!state.busy)
840
+ setConnection("Ready", "ready");
841
+ }
842
+ },
843
+ onError(error) {
844
+ if (generation !== state.generation || state.stream !== stream)
845
+ return;
846
+ setConnection(error.message || "Reconnecting", "connecting");
847
+ },
848
+ });
849
+ state.stream = stream;
850
+ stream.closed.finally(() => {
851
+ if (state.stream === stream)
852
+ state.stream = null;
853
+ });
854
+ try {
855
+ await stream.ready;
856
+ if (generation !== state.generation || state.stream !== stream) {
857
+ stream.close();
858
+ return;
859
+ }
860
+ markStep("stream", "complete");
861
+ setConnection("Ready", "ready");
862
+ }
863
+ catch (error) {
864
+ if (generation === state.generation) {
865
+ setConnection(error.message || "Connection failed", "error");
866
+ }
867
+ throw error;
868
+ }
869
+ finally {
870
+ state.connecting = false;
871
+ }
872
+ }
873
+ async function submit(prompt) {
874
+ const value = prompt.trim();
875
+ const files = [...state.pendingFiles];
876
+ if ((!value && !files.length) || state.busy)
877
+ return;
878
+ const generation = state.generation;
879
+ addMessage("user", [
880
+ value,
881
+ files.length ? `Attached: ${files.map((file) => file.name).join(", ")}` : "",
882
+ ]
883
+ .filter(Boolean)
884
+ .join("\n"));
885
+ elements.prompt.value = "";
886
+ resizePrompt();
887
+ syncSendState();
888
+ setBusy(true);
889
+ startActivity("Starting…");
890
+ try {
891
+ if (!(await ensureSession(generation)))
892
+ return;
893
+ await connectStream();
894
+ if (generation !== state.generation)
895
+ return;
896
+ if (files.length)
897
+ startActivity("Uploading…");
898
+ markStep("append", "active");
899
+ const sent = await state.session.send({
900
+ input: value,
901
+ files,
902
+ messageId: uuidv7(),
903
+ metadata: {
904
+ arenaVisitId: state.visitId,
905
+ submissionSource: "chat_input",
906
+ },
907
+ });
908
+ if (generation !== state.generation)
909
+ return;
910
+ sent.attachments.forEach(addArtifact);
911
+ markStep("append", "complete");
912
+ clearPendingFiles();
913
+ }
914
+ catch (error) {
915
+ if (generation !== state.generation)
916
+ return;
917
+ stopActivity();
918
+ addMessage("assistant", error.message, "error");
919
+ setBusy(false);
920
+ setConnection("Request failed", "error");
921
+ }
922
+ }
923
+ async function cancelTurn() {
924
+ if (!state.session) {
925
+ stopActivity();
926
+ setBusy(false);
927
+ setConnection("Ready", "ready");
928
+ return;
929
+ }
930
+ try {
931
+ await state.session.cancel();
932
+ }
933
+ finally {
934
+ stopActivity();
935
+ setBusy(false);
936
+ setConnection("Ready", "ready");
937
+ }
938
+ }
939
+ function resizePrompt() {
940
+ elements.prompt.style.height = "auto";
941
+ elements.prompt.style.height = `${Math.min(elements.prompt.scrollHeight, 180)}px`;
942
+ }
943
+ function newChat() {
944
+ state.generation += 1;
945
+ stopActivity();
946
+ state.stream?.close();
947
+ state.stream = null;
948
+ state.connecting = false;
949
+ clearPendingFiles();
950
+ clearConversation();
951
+ setBusy(false);
952
+ enterDraft();
953
+ if (location.pathname !== "/") {
954
+ history.pushState(null, "", "/");
955
+ }
956
+ elements.prompt.focus();
957
+ }
958
+ async function initialize() {
959
+ state.sessionId = sessionFromLocation();
960
+ const token = state.sessionId
961
+ ? localStorage.getItem(sessionTokenKey(state.sessionId)) || ""
962
+ : "";
963
+ try {
964
+ const config = await arena.getConfig();
965
+ elements.modelName.textContent = config.model;
966
+ if (!state.sessionId) {
967
+ enterDraft();
968
+ return;
969
+ }
970
+ if (!token) {
971
+ forgetSession(state.sessionId);
972
+ history.replaceState(null, "", "/");
973
+ enterDraft();
974
+ return;
975
+ }
976
+ state.session = arena.sessions.resume({
977
+ sessionId: state.sessionId,
978
+ token,
979
+ });
980
+ elements.taskId.textContent = state.sessionId.slice(0, 18);
981
+ rememberSession();
982
+ await loadHistory();
983
+ connectStream();
984
+ }
985
+ catch (error) {
986
+ setConnection("Connection failed", "error");
987
+ addMessage("assistant", error.message, "error");
988
+ }
989
+ }
990
+ elements.composer.addEventListener("submit", (event) => {
991
+ event.preventDefault();
992
+ submit(elements.prompt.value);
993
+ });
994
+ elements.addFiles.addEventListener("click", () => {
995
+ if (!state.busy)
996
+ elements.fileInput.click();
997
+ });
998
+ elements.fileInput.addEventListener("change", () => {
999
+ addPendingFiles(Array.from(elements.fileInput.files || []));
1000
+ elements.fileInput.value = "";
1001
+ });
1002
+ elements.prompt.addEventListener("input", () => {
1003
+ resizePrompt();
1004
+ syncSendState();
1005
+ });
1006
+ elements.prompt.addEventListener("keydown", (event) => {
1007
+ if (event.key === "Enter" && !event.shiftKey) {
1008
+ event.preventDefault();
1009
+ elements.composer.requestSubmit();
1010
+ }
1011
+ });
1012
+ elements.stop.addEventListener("click", cancelTurn);
1013
+ elements.newChat.addEventListener("click", newChat);
1014
+ elements.sidebarToggle?.addEventListener("click", () => {
1015
+ setSidebarExpanded(!elements.sidebar?.classList.contains("is-expanded"));
1016
+ });
1017
+ elements.mobileSidebarToggle?.addEventListener("click", () => {
1018
+ setSidebarExpanded(!elements.sidebar?.classList.contains("is-expanded"));
1019
+ });
1020
+ elements.modeToggle?.addEventListener("click", (event) => {
1021
+ event.stopPropagation();
1022
+ toggleModeMenu();
1023
+ });
1024
+ elements.modeMenu?.addEventListener("click", (event) => {
1025
+ const option = event.target instanceof Element ? event.target.closest("[data-mode]") : null;
1026
+ if (!option)
1027
+ return;
1028
+ if (elements.modeLabel)
1029
+ elements.modeLabel.textContent = option.dataset.mode;
1030
+ elements.modeMenu.querySelectorAll("[data-mode]").forEach((candidate) => {
1031
+ candidate.classList.toggle("is-selected", candidate === option);
1032
+ });
1033
+ closeModeMenu();
1034
+ });
1035
+ elements.dismissBanner?.addEventListener("click", () => {
1036
+ if (elements.announcement)
1037
+ elements.announcement.hidden = true;
1038
+ });
1039
+ elements.hidePromo?.addEventListener("click", () => {
1040
+ const promo = elements.hidePromo.closest(".agent-promo");
1041
+ if (promo)
1042
+ promo.hidden = true;
1043
+ });
1044
+ elements.artifactToggle?.addEventListener("click", () => {
1045
+ setArtifactPanelOpen(elements.artifactPanel?.hidden ?? false);
1046
+ });
1047
+ elements.artifactClose?.addEventListener("click", () => {
1048
+ setArtifactPanelOpen(false);
1049
+ });
1050
+ document.querySelectorAll("[data-prompt]").forEach((button) => {
1051
+ button.addEventListener("click", () => submit(button.dataset.prompt));
1052
+ });
1053
+ document.addEventListener("keydown", (event) => {
1054
+ if (event.key === "Escape") {
1055
+ closeModeMenu();
1056
+ setArtifactPanelOpen(false);
1057
+ if (matchMedia("(max-width: 767px)").matches) {
1058
+ setSidebarExpanded(false);
1059
+ }
1060
+ }
1061
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") {
1062
+ event.preventDefault();
1063
+ newChat();
1064
+ }
1065
+ });
1066
+ document.addEventListener("click", (event) => {
1067
+ if (elements.modeMenu &&
1068
+ !elements.modeMenu.hidden &&
1069
+ !elements.modeMenu.contains(event.target) &&
1070
+ !elements.modeToggle?.contains(event.target)) {
1071
+ closeModeMenu();
1072
+ }
1073
+ });
1074
+ window.addEventListener("beforeunload", () => {
1075
+ state.stream?.close();
1076
+ });
1077
+ setSidebarExpanded(localStorage.getItem("arena-sidebar-expanded") === "true");
1078
+ closeModeMenu();
1079
+ setArtifactPanelOpen(false);
1080
+ syncSendState();
1081
+ initialize();
1082
+ //# sourceMappingURL=app.js.map