@qwen-code/arena-agent 0.0.8-dev.5 → 0.0.9-dev.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 (60) hide show
  1. package/.env.example +41 -15
  2. package/README.md +128 -29
  3. package/dist/agent.d.ts +8 -17
  4. package/dist/agent.js +60 -75
  5. package/dist/agent.js.map +1 -1
  6. package/dist/cli.js +12 -4
  7. package/dist/cli.js.map +1 -1
  8. package/dist/compaction.js +2 -1
  9. package/dist/compaction.js.map +1 -1
  10. package/dist/config.d.ts +11 -0
  11. package/dist/config.js +73 -9
  12. package/dist/config.js.map +1 -1
  13. package/dist/conversation-metrics.d.ts +61 -0
  14. package/dist/conversation-metrics.js +433 -0
  15. package/dist/conversation-metrics.js.map +1 -0
  16. package/dist/document-reader.js +9 -0
  17. package/dist/document-reader.js.map +1 -1
  18. package/dist/history.d.ts +31 -2
  19. package/dist/history.js +53 -3
  20. package/dist/history.js.map +1 -1
  21. package/dist/metrics.d.ts +61 -0
  22. package/dist/metrics.js +578 -0
  23. package/dist/metrics.js.map +1 -0
  24. package/dist/mock.js +153 -0
  25. package/dist/mock.js.map +1 -1
  26. package/dist/protocol.d.ts +2 -3
  27. package/dist/protocol.js +9 -5
  28. package/dist/protocol.js.map +1 -1
  29. package/dist/provider.js +22 -8
  30. package/dist/provider.js.map +1 -1
  31. package/dist/runtime-assets.d.ts +3 -0
  32. package/dist/runtime-assets.js +16 -0
  33. package/dist/runtime-assets.js.map +1 -0
  34. package/dist/sdk.d.ts +28 -1
  35. package/dist/sdk.js +38 -0
  36. package/dist/sdk.js.map +1 -1
  37. package/dist/server.js +486 -36
  38. package/dist/server.js.map +1 -1
  39. package/dist/side-query.d.ts +31 -0
  40. package/dist/side-query.js +138 -0
  41. package/dist/side-query.js.map +1 -0
  42. package/dist/spec.d.ts +1 -1
  43. package/dist/spec.js +6 -4
  44. package/dist/spec.js.map +1 -1
  45. package/dist/tools.d.ts +3 -8
  46. package/dist/tools.js +149 -200
  47. package/dist/tools.js.map +1 -1
  48. package/dist/tui.js +4 -3
  49. package/dist/tui.js.map +1 -1
  50. package/dist/types.d.ts +155 -5
  51. package/dist/web/app.d.ts +1 -0
  52. package/dist/web/app.js +333 -10
  53. package/dist/web/app.js.map +1 -1
  54. package/docs/SDK.md +42 -0
  55. package/package.json +10 -20
  56. package/web/index.html +25 -0
  57. package/web/styles.css +109 -3
  58. package/dist/index.d.ts +0 -9
  59. package/dist/index.js +0 -9
  60. package/dist/index.js.map +0 -1
package/dist/web/app.js CHANGED
@@ -14,6 +14,8 @@ const elements = {
14
14
  artifactPreviewPath: document.querySelector("#artifact-preview-path"),
15
15
  artifactTitle: document.querySelector("#artifact-title"),
16
16
  artifactToggle: document.querySelector("#artifact-toggle"),
17
+ bashMetric: document.querySelector("#bash-metric"),
18
+ bashMetricValue: document.querySelector("#bash-metric-value"),
17
19
  composer: document.querySelector("#composer"),
18
20
  connection: document.querySelector("#connection"),
19
21
  connectionLabel: document.querySelector("#connection-label"),
@@ -21,6 +23,8 @@ const elements = {
21
23
  dismissBanner: document.querySelector("#dismiss-banner"),
22
24
  emptyState: document.querySelector("#empty-state"),
23
25
  fileInput: document.querySelector("#file-input"),
26
+ feedbackMetric: document.querySelector("#feedback-metric"),
27
+ feedbackMetricValue: document.querySelector("#feedback-metric-value"),
24
28
  hidePromo: document.querySelector(".hide-promo"),
25
29
  messages: document.querySelector("#messages"),
26
30
  mobileSidebarToggle: document.querySelector("#mobile-sidebar-toggle"),
@@ -30,12 +34,18 @@ const elements = {
30
34
  modelName: document.querySelector("#model-name"),
31
35
  newChat: document.querySelector("#new-chat"),
32
36
  prompt: document.querySelector("#prompt"),
37
+ retry: document.querySelector("#retry"),
33
38
  send: document.querySelector("#send"),
34
39
  sidebar: document.querySelector("#sidebar"),
35
40
  sidebarToggle: document.querySelector("#sidebar-toggle"),
36
41
  stop: document.querySelector("#stop"),
42
+ steerMetric: document.querySelector("#steer-metric"),
43
+ steerMetricValue: document.querySelector("#steer-metric-value"),
37
44
  taskId: document.querySelector("#task-id"),
38
45
  template: document.querySelector("#message-template"),
46
+ undo: document.querySelector("#undo"),
47
+ toolMetric: document.querySelector("#tool-metric"),
48
+ toolMetricValue: document.querySelector("#tool-metric-value"),
39
49
  };
40
50
  const state = {
41
51
  session: null,
@@ -53,6 +63,11 @@ const state = {
53
63
  artifacts: new Map(),
54
64
  activeArtifact: "",
55
65
  pendingFiles: [],
66
+ pendingAttachments: [],
67
+ canUndo: false,
68
+ undoing: false,
69
+ retryMessageId: null,
70
+ metrics: null,
56
71
  visitId: localStorage.getItem("arena-visit-id") || uuidv7(),
57
72
  generation: 0,
58
73
  };
@@ -126,8 +141,17 @@ function enterDraft() {
126
141
  state.sessionId = "";
127
142
  localStorage.removeItem("arena-session-id");
128
143
  elements.taskId.textContent = "New chat";
144
+ setRetryable(null);
129
145
  setConnection("Ready", "ready");
130
146
  }
147
+ function setRetryable(messageId) {
148
+ state.retryMessageId = messageId || null;
149
+ if (!elements.retry)
150
+ return;
151
+ elements.retry.hidden = !state.retryMessageId;
152
+ elements.retry.disabled =
153
+ state.busy || state.undoing || !state.retryMessageId;
154
+ }
131
155
  async function createSession(generation) {
132
156
  markStep("session", "active");
133
157
  const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
@@ -162,6 +186,8 @@ function clearConversation() {
162
186
  state.tools.clear();
163
187
  state.inputRequests.clear();
164
188
  state.artifacts.clear();
189
+ renderSessionMetrics(null);
190
+ setRetryable(null);
165
191
  clearPendingFiles();
166
192
  resetArtifactPreview();
167
193
  renderArtifacts();
@@ -181,7 +207,7 @@ function renderPendingFiles(error = "") {
181
207
  remove.type = "button";
182
208
  remove.setAttribute("aria-label", `Remove ${file.name}`);
183
209
  remove.textContent = "×";
184
- remove.disabled = state.busy;
210
+ remove.disabled = state.busy || state.undoing;
185
211
  remove.addEventListener("click", () => {
186
212
  const identity = fileIdentity(file);
187
213
  state.pendingFiles = state.pendingFiles.filter((candidate) => fileIdentity(candidate) !== identity);
@@ -191,6 +217,25 @@ function renderPendingFiles(error = "") {
191
217
  chip.append(name, remove);
192
218
  elements.attachmentList.append(chip);
193
219
  }
220
+ for (const attachment of state.pendingAttachments) {
221
+ const chip = document.createElement("div");
222
+ chip.className = "attachment-chip";
223
+ const name = document.createElement("span");
224
+ name.textContent = attachment.name;
225
+ name.title = `${attachment.name} · ${Math.max(1, Math.ceil(attachment.size / 1024))} KB`;
226
+ const remove = document.createElement("button");
227
+ remove.type = "button";
228
+ remove.setAttribute("aria-label", `Remove ${attachment.name}`);
229
+ remove.textContent = "×";
230
+ remove.disabled = state.busy || state.undoing;
231
+ remove.addEventListener("click", () => {
232
+ state.pendingAttachments = state.pendingAttachments.filter((candidate) => candidate.path !== attachment.path);
233
+ renderPendingFiles();
234
+ syncSendState();
235
+ });
236
+ chip.append(name, remove);
237
+ elements.attachmentList.append(chip);
238
+ }
194
239
  if (error) {
195
240
  const message = document.createElement("span");
196
241
  message.className = "attachment-error";
@@ -198,13 +243,16 @@ function renderPendingFiles(error = "") {
198
243
  elements.attachmentList.append(message);
199
244
  }
200
245
  elements.attachmentList.hidden =
201
- state.pendingFiles.length === 0 && !error;
246
+ state.pendingFiles.length === 0 &&
247
+ state.pendingAttachments.length === 0 &&
248
+ !error;
202
249
  }
203
250
  function addPendingFiles(files) {
204
251
  const existing = new Set(state.pendingFiles.map(fileIdentity));
205
252
  let error = "";
206
253
  for (const file of files) {
207
- if (state.pendingFiles.length >= maxAttachmentCount) {
254
+ if (state.pendingFiles.length + state.pendingAttachments.length >=
255
+ maxAttachmentCount) {
208
256
  error = `Up to ${maxAttachmentCount} files per message`;
209
257
  break;
210
258
  }
@@ -223,6 +271,7 @@ function addPendingFiles(files) {
223
271
  }
224
272
  function clearPendingFiles() {
225
273
  state.pendingFiles = [];
274
+ state.pendingAttachments = [];
226
275
  if (elements.fileInput)
227
276
  elements.fileInput.value = "";
228
277
  if (elements.attachmentList)
@@ -638,6 +687,32 @@ function renderHistory(messages) {
638
687
  state.currentReasoning = null;
639
688
  elements.conversation.scrollTop = elements.conversation.scrollHeight;
640
689
  }
690
+ function applyUndo(result) {
691
+ if (!result?.undone && !Array.isArray(result?.messages))
692
+ return;
693
+ clearConversation();
694
+ renderHistory(result.messages || []);
695
+ renderSessionMetrics(result.metrics || null);
696
+ state.pendingAttachments = [...(result.attachments || [])];
697
+ state.canUndo = result.canUndo === true;
698
+ elements.prompt.value = result.input || "";
699
+ resizePrompt();
700
+ renderPendingFiles();
701
+ syncSendState();
702
+ syncUndoState();
703
+ setConnection("Ready", "ready");
704
+ elements.prompt.focus();
705
+ }
706
+ function replaceRenderedHistory(messages) {
707
+ elements.messages.replaceChildren();
708
+ elements.emptyState.hidden = false;
709
+ document.body.classList.remove("has-messages");
710
+ state.currentAssistant = null;
711
+ state.currentReasoning = null;
712
+ state.tools.clear();
713
+ state.inputRequests.clear();
714
+ renderHistory(messages);
715
+ }
641
716
  function parseJson(value) {
642
717
  if (typeof value !== "string")
643
718
  return value;
@@ -653,23 +728,195 @@ async function loadHistory() {
653
728
  return;
654
729
  const value = await state.session.getHistory();
655
730
  renderHistory(value.messages || []);
731
+ state.canUndo = value.canUndo === true;
732
+ syncUndoState();
733
+ renderSessionMetrics(value.metrics || null);
734
+ setRetryable(value.retryable ? value.retryMessageId : null);
656
735
  if (value.activePrompt)
657
736
  addMessage("user", value.activePrompt);
658
737
  }
738
+ function metricNumber(value) {
739
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
740
+ }
741
+ function renderSessionMetrics(metrics) {
742
+ if (!metrics) {
743
+ state.metrics = null;
744
+ elements.bashMetric.dataset.state = "neutral";
745
+ elements.toolMetric.dataset.state = "neutral";
746
+ elements.feedbackMetric.dataset.state = "neutral";
747
+ elements.steerMetric.dataset.state = "neutral";
748
+ elements.bashMetricValue.textContent = "—";
749
+ elements.toolMetricValue.textContent = "—";
750
+ elements.feedbackMetricValue.textContent = "—";
751
+ elements.steerMetricValue.textContent = "—";
752
+ elements.bashMetricValue.dataset.mobileValue = "B—";
753
+ elements.toolMetricValue.dataset.mobileValue = "T—";
754
+ elements.feedbackMetricValue.dataset.mobileValue = "P—";
755
+ elements.steerMetricValue.dataset.mobileValue = "S—";
756
+ return;
757
+ }
758
+ if (state.metrics &&
759
+ metricNumber(metrics.revision) < metricNumber(state.metrics.revision)) {
760
+ return;
761
+ }
762
+ state.metrics = metrics;
763
+ const bash = metrics.bashRecovery?.summary || {};
764
+ const tools = metrics.toolHallucination?.summary || {};
765
+ const feedback = metrics.praiseComplaint?.summary || {};
766
+ const steer = metrics.steerability?.summary || {};
767
+ const feedbackObservations = metrics.praiseComplaint?.observations || [];
768
+ const corrections = metrics.steerability?.corrections || [];
769
+ const bashCalls = metrics.bashRecovery?.calls || [];
770
+ const bashEpisodes = metrics.bashRecovery?.episodes || [];
771
+ const praise = metricNumber(feedback.praise);
772
+ const complaints = metricNumber(feedback.complaint);
773
+ const feedbackAmbiguous = feedbackObservations.filter((item) => item.praise === null || item.complaint === null).length;
774
+ const feedbackOutcome = feedback.result || "no_signal";
775
+ elements.feedbackMetric.dataset.state =
776
+ feedbackOutcome === "success"
777
+ ? "good"
778
+ : feedbackOutcome === "failure"
779
+ ? "bad"
780
+ : feedbackOutcome === "tie" || feedbackAmbiguous
781
+ ? "warning"
782
+ : "neutral";
783
+ elements.feedbackMetricValue.textContent =
784
+ feedbackOutcome === "no_signal"
785
+ ? "No feedback"
786
+ : `${praise} praise · ${complaints} complaint`;
787
+ elements.feedbackMetricValue.dataset.mobileValue =
788
+ feedbackOutcome === "no_signal" ? "P—" : `P${praise}:${complaints}`;
789
+ elements.feedbackMetric.title = [
790
+ `${praise} praise messages`,
791
+ `${complaints} complaint messages`,
792
+ `${feedbackAmbiguous} unclassified`,
793
+ `outcome ${feedbackOutcome}`,
794
+ ].join(" · ");
795
+ const steerSuccess = metricNumber(steer.successful);
796
+ const steerFailure = metricNumber(steer.unsuccessful);
797
+ const steerAwaiting = metricNumber(steer.pending);
798
+ const steerExcluded = metricNumber(steer.excluded);
799
+ const steerAmbiguous = corrections.filter((item) => item.state === "ambiguous").length;
800
+ const steerResolved = steerSuccess + steerFailure;
801
+ elements.steerMetric.dataset.state = steerFailure
802
+ ? "bad"
803
+ : steerAwaiting || steerAmbiguous
804
+ ? "warning"
805
+ : steerSuccess
806
+ ? "good"
807
+ : "neutral";
808
+ elements.steerMetricValue.textContent = steerFailure
809
+ ? `${steerSuccess}/${steerResolved} success`
810
+ : steerAwaiting
811
+ ? `${steerAwaiting} awaiting user`
812
+ : steerSuccess
813
+ ? `${steerSuccess}/${steerResolved} success`
814
+ : "No corrections";
815
+ elements.steerMetricValue.dataset.mobileValue = steerResolved
816
+ ? `S${steerSuccess}:${steerFailure}`
817
+ : steerAwaiting
818
+ ? `S…${steerAwaiting}`
819
+ : "S—";
820
+ elements.steerMetric.title = [
821
+ `${steerSuccess + steerFailure + steerAwaiting + steerExcluded} corrections`,
822
+ `${steerSuccess} successful`,
823
+ `${steerFailure} unsuccessful`,
824
+ `${steerAwaiting} awaiting explicit user verdict`,
825
+ `${steerExcluded} excluded`,
826
+ ].join(" · ");
827
+ const modelErrors = metricNumber(bash.modelErrors);
828
+ const recovering = bashEpisodes.filter((episode) => episode.state === "recovering").length;
829
+ const gaveUp = metricNumber(bash.gaveUp);
830
+ const recovered = metricNumber(bash.recovered);
831
+ const ambiguous = bashCalls.filter((call) => call.origin === "ambiguous").length;
832
+ const average = bash.averageCallsToRecover;
833
+ if (gaveUp) {
834
+ elements.bashMetric.dataset.state = "bad";
835
+ elements.bashMetricValue.textContent = `${gaveUp} gave up`;
836
+ }
837
+ else if (recovering || ambiguous) {
838
+ elements.bashMetric.dataset.state = "warning";
839
+ elements.bashMetricValue.textContent = recovering
840
+ ? `${recovering} recovering`
841
+ : `${ambiguous} unclassified`;
842
+ }
843
+ else if (recovered) {
844
+ elements.bashMetric.dataset.state = "good";
845
+ elements.bashMetricValue.textContent = `${Number(average).toFixed(1)} calls avg`;
846
+ }
847
+ else {
848
+ elements.bashMetric.dataset.state = "good";
849
+ elements.bashMetricValue.textContent = modelErrors ? "Resolved" : "No model errors";
850
+ }
851
+ elements.bashMetricValue.dataset.mobileValue = gaveUp
852
+ ? `B!${gaveUp}`
853
+ : recovering
854
+ ? `B…${recovering}`
855
+ : ambiguous
856
+ ? `B?${ambiguous}`
857
+ : recovered
858
+ ? `B${Number(average).toFixed(1)}`
859
+ : "B✓";
860
+ elements.bashMetric.title = [
861
+ `${bashCalls.length} Bash calls`,
862
+ `${modelErrors} model errors`,
863
+ `${recovered} recovered`,
864
+ `${ambiguous} ambiguous`,
865
+ ].join(" · ");
866
+ const hallucinated = metricNumber(tools.hallucinated);
867
+ const totalTools = metricNumber(tools.total);
868
+ elements.toolMetric.dataset.state = hallucinated ? "bad" : "good";
869
+ elements.toolMetricValue.textContent = hallucinated
870
+ ? `${hallucinated} invalid`
871
+ : "Clean";
872
+ elements.toolMetricValue.dataset.mobileValue = hallucinated
873
+ ? `T!${hallucinated}`
874
+ : "T✓";
875
+ elements.toolMetric.title = `${hallucinated} hallucinated of ${totalTools} structured tool calls`;
876
+ }
659
877
  function setBusy(busy) {
660
878
  state.busy = busy;
661
879
  elements.send.disabled =
662
- busy || (!elements.prompt.value.trim() && !state.pendingFiles.length);
880
+ busy ||
881
+ state.undoing ||
882
+ (!elements.prompt.value.trim() &&
883
+ !state.pendingFiles.length &&
884
+ !state.pendingAttachments.length);
663
885
  elements.stop.hidden = !busy;
664
- elements.prompt.disabled = busy;
665
- elements.addFiles.disabled = busy;
666
- elements.fileInput.disabled = busy;
886
+ elements.prompt.disabled = busy || state.undoing;
887
+ elements.addFiles.disabled = busy || state.undoing;
888
+ elements.fileInput.disabled = busy || state.undoing;
889
+ if (elements.retry) {
890
+ elements.retry.disabled =
891
+ busy || state.undoing || !state.retryMessageId;
892
+ }
667
893
  renderPendingFiles();
894
+ syncUndoState();
668
895
  }
669
896
  function syncSendState() {
670
897
  elements.send.disabled =
671
898
  state.busy ||
672
- (!elements.prompt.value.trim() && !state.pendingFiles.length);
899
+ state.undoing ||
900
+ (!elements.prompt.value.trim() &&
901
+ !state.pendingFiles.length &&
902
+ !state.pendingAttachments.length);
903
+ }
904
+ function syncUndoState() {
905
+ if (!elements.undo)
906
+ return;
907
+ elements.undo.hidden =
908
+ state.busy || !state.session || !state.canUndo;
909
+ elements.undo.disabled = state.undoing;
910
+ }
911
+ function setUndoing(undoing) {
912
+ state.undoing = undoing;
913
+ elements.prompt.disabled = undoing || state.busy;
914
+ elements.addFiles.disabled = undoing || state.busy;
915
+ elements.fileInput.disabled = undoing || state.busy;
916
+ renderPendingFiles();
917
+ syncSendState();
918
+ syncUndoState();
919
+ setRetryable(state.retryMessageId);
673
920
  }
674
921
  function setSidebarExpanded(expanded) {
675
922
  if (!elements.sidebar)
@@ -701,6 +948,7 @@ function setArtifactPanelOpen(open) {
701
948
  function handleStreamEvent(event) {
702
949
  switch (event.type) {
703
950
  case "start":
951
+ setRetryable(null);
704
952
  markStep("model", "active");
705
953
  setBusy(true);
706
954
  startActivity("Starting…");
@@ -795,6 +1043,15 @@ function handleStreamEvent(event) {
795
1043
  case "data-present-file":
796
1044
  presentArtifact(event.data);
797
1045
  break;
1046
+ case "data-turn-undone":
1047
+ applyUndo({ undone: true, canUndo: false, ...(event.data || {}) });
1048
+ break;
1049
+ case "data-messages":
1050
+ replaceRenderedHistory(event.data?.messages || []);
1051
+ break;
1052
+ case "data-session-metrics":
1053
+ renderSessionMetrics(event.data?.metrics || null);
1054
+ break;
798
1055
  case "error":
799
1056
  stopActivity();
800
1057
  addMessage("assistant", event.errorText || "The agent stopped unexpectedly.", "error");
@@ -807,6 +1064,9 @@ function handleStreamEvent(event) {
807
1064
  state.currentReasoning = null;
808
1065
  markStep("model", "complete");
809
1066
  setConnection(event.status === "success" ? "Ready" : "Agent error", event.status === "success" ? "ready" : "error");
1067
+ state.canUndo = true;
1068
+ setRetryable(event.status === "error" && event.retryable ? event.messageId : null);
1069
+ syncUndoState();
810
1070
  elements.prompt.disabled = false;
811
1071
  elements.prompt.focus();
812
1072
  break;
@@ -873,12 +1133,19 @@ async function connectStream() {
873
1133
  async function submit(prompt) {
874
1134
  const value = prompt.trim();
875
1135
  const files = [...state.pendingFiles];
876
- if ((!value && !files.length) || state.busy)
1136
+ const attachments = [...state.pendingAttachments];
1137
+ if ((!value && !files.length && !attachments.length) ||
1138
+ state.busy ||
1139
+ state.undoing)
877
1140
  return;
878
1141
  const generation = state.generation;
879
1142
  addMessage("user", [
880
1143
  value,
881
- files.length ? `Attached: ${files.map((file) => file.name).join(", ")}` : "",
1144
+ files.length || attachments.length
1145
+ ? `Attached: ${[...files, ...attachments]
1146
+ .map((file) => file.name)
1147
+ .join(", ")}`
1148
+ : "",
882
1149
  ]
883
1150
  .filter(Boolean)
884
1151
  .join("\n"));
@@ -899,6 +1166,7 @@ async function submit(prompt) {
899
1166
  const sent = await state.session.send({
900
1167
  input: value,
901
1168
  files,
1169
+ attachments,
902
1170
  messageId: uuidv7(),
903
1171
  metadata: {
904
1172
  arenaVisitId: state.visitId,
@@ -920,6 +1188,35 @@ async function submit(prompt) {
920
1188
  setConnection("Request failed", "error");
921
1189
  }
922
1190
  }
1191
+ async function retryFailedTurn() {
1192
+ if (!state.session ||
1193
+ !state.retryMessageId ||
1194
+ state.busy ||
1195
+ state.undoing)
1196
+ return;
1197
+ const generation = state.generation;
1198
+ const failedMessageId = state.retryMessageId;
1199
+ setBusy(true);
1200
+ startActivity("Retrying…");
1201
+ try {
1202
+ await connectStream();
1203
+ if (generation !== state.generation)
1204
+ return;
1205
+ await state.session.retry({ messageId: failedMessageId });
1206
+ if (generation !== state.generation)
1207
+ return;
1208
+ setRetryable(null);
1209
+ }
1210
+ catch (error) {
1211
+ if (generation !== state.generation)
1212
+ return;
1213
+ stopActivity();
1214
+ addMessage("assistant", error.message, "error");
1215
+ setBusy(false);
1216
+ setRetryable(failedMessageId);
1217
+ setConnection("Retry failed", "error");
1218
+ }
1219
+ }
923
1220
  async function cancelTurn() {
924
1221
  if (!state.session) {
925
1222
  stopActivity();
@@ -936,6 +1233,28 @@ async function cancelTurn() {
936
1233
  setConnection("Ready", "ready");
937
1234
  }
938
1235
  }
1236
+ async function undoTurn() {
1237
+ if (!state.session || state.busy || state.undoing || !state.canUndo)
1238
+ return;
1239
+ setUndoing(true);
1240
+ setConnection("Undoing…", "connecting");
1241
+ try {
1242
+ const result = await state.session.undo();
1243
+ if (result.undone)
1244
+ applyUndo(result);
1245
+ else {
1246
+ state.canUndo = false;
1247
+ syncUndoState();
1248
+ setConnection("Nothing to undo", "ready");
1249
+ }
1250
+ }
1251
+ catch (error) {
1252
+ setConnection(error.message || "Undo failed", "error");
1253
+ }
1254
+ finally {
1255
+ setUndoing(false);
1256
+ }
1257
+ }
939
1258
  function resizePrompt() {
940
1259
  elements.prompt.style.height = "auto";
941
1260
  elements.prompt.style.height = `${Math.min(elements.prompt.scrollHeight, 180)}px`;
@@ -948,6 +1267,8 @@ function newChat() {
948
1267
  state.connecting = false;
949
1268
  clearPendingFiles();
950
1269
  clearConversation();
1270
+ state.canUndo = false;
1271
+ state.undoing = false;
951
1272
  setBusy(false);
952
1273
  enterDraft();
953
1274
  if (location.pathname !== "/") {
@@ -1010,6 +1331,8 @@ elements.prompt.addEventListener("keydown", (event) => {
1010
1331
  }
1011
1332
  });
1012
1333
  elements.stop.addEventListener("click", cancelTurn);
1334
+ elements.undo?.addEventListener("click", undoTurn);
1335
+ elements.retry?.addEventListener("click", retryFailedTurn);
1013
1336
  elements.newChat.addEventListener("click", newChat);
1014
1337
  elements.sidebarToggle?.addEventListener("click", () => {
1015
1338
  setSidebarExpanded(!elements.sidebar?.classList.contains("is-expanded"));