@agent-native/core 0.92.11 → 0.92.12

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 (38) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +6 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/production-agent.ts +33 -2
  5. package/corpus/core/src/agent/run-manager.ts +41 -10
  6. package/corpus/core/src/agent/run-store.ts +72 -27
  7. package/corpus/core/src/client/agent-chat-adapter.ts +56 -12
  8. package/corpus/templates/clips/app/components/player/transcript-panel.tsx +27 -5
  9. package/corpus/templates/clips/app/hooks/use-player-shortcuts.ts +1 -0
  10. package/corpus/templates/clips/changelog/2026-07-10-fixed-copying-agent-responses-and-transcript-text-from-recor.md +6 -0
  11. package/corpus/templates/content/actions/_builder-cms-read-client.ts +82 -7
  12. package/corpus/templates/content/actions/_database-source-utils.ts +149 -30
  13. package/corpus/templates/content/actions/add-content-database-source-field-property.ts +4 -7
  14. package/corpus/templates/content/actions/attach-content-database-source.ts +41 -17
  15. package/corpus/templates/content/actions/change-content-database-source-role.ts +46 -8
  16. package/corpus/templates/content/actions/suggest-source-join-key.ts +8 -1
  17. package/corpus/templates/content/changelog/2026-07-09-builder-source-refreshes-now-keep-mapped-fields-such-as-topi.md +6 -0
  18. package/corpus/templates/content/shared/api.ts +1 -0
  19. package/dist/agent/production-agent.d.ts +3 -1
  20. package/dist/agent/production-agent.d.ts.map +1 -1
  21. package/dist/agent/production-agent.js +19 -3
  22. package/dist/agent/production-agent.js.map +1 -1
  23. package/dist/agent/run-manager.d.ts.map +1 -1
  24. package/dist/agent/run-manager.js +34 -9
  25. package/dist/agent/run-manager.js.map +1 -1
  26. package/dist/agent/run-store.d.ts.map +1 -1
  27. package/dist/agent/run-store.js +68 -22
  28. package/dist/agent/run-store.js.map +1 -1
  29. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  30. package/dist/client/agent-chat-adapter.js +48 -12
  31. package/dist/client/agent-chat-adapter.js.map +1 -1
  32. package/dist/collab/awareness.d.ts +2 -2
  33. package/dist/collab/awareness.d.ts.map +1 -1
  34. package/dist/collab/struct-routes.d.ts +1 -1
  35. package/dist/observability/routes.d.ts +5 -5
  36. package/dist/progress/routes.d.ts +1 -1
  37. package/dist/resources/handlers.d.ts +3 -3
  38. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -28,4 +28,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
28
28
  ## Generated Counts
29
29
 
30
30
  - core files: 2155
31
- - template files: 5195
31
+ - template files: 5197
@@ -1,5 +1,11 @@
1
1
  # @agent-native/core
2
2
 
3
+ ## 0.92.12
4
+
5
+ ### Patch Changes
6
+
7
+ - 1ecce26: Surface missing LLM credentials and earlier stream errors as failed agent runs instead of successful completions.
8
+
3
9
  ## 0.92.11
4
10
 
5
11
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.92.11",
3
+ "version": "0.92.12",
4
4
  "description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -136,6 +136,7 @@ import {
136
136
  insertRun,
137
137
  updateRunHeartbeat,
138
138
  updateRunStatusIfRunning,
139
+ setRunError,
139
140
  setRunTerminalReason,
140
141
  claimBackgroundRun,
141
142
  readBackgroundRunClaim,
@@ -4881,18 +4882,47 @@ export function shouldChainBackgroundContinuation(opts: {
4881
4882
  export async function markBackgroundContinuationChunkTerminal(opts: {
4882
4883
  runId: string;
4883
4884
  continuationReason: AgentLoopContinuationReason;
4885
+ terminalEvent?: AgentChatEvent;
4884
4886
  deps?: {
4885
4887
  updateRunStatusIfRunning?: typeof updateRunStatusIfRunning;
4888
+ setRunError?: typeof setRunError;
4886
4889
  setRunTerminalReason?: typeof setRunTerminalReason;
4887
4890
  };
4888
4891
  }): Promise<boolean> {
4889
4892
  const updateStatus =
4890
4893
  opts.deps?.updateRunStatusIfRunning ?? updateRunStatusIfRunning;
4894
+ const persistError = opts.deps?.setRunError ?? setRunError;
4891
4895
  const setTerminalReason =
4892
4896
  opts.deps?.setRunTerminalReason ?? setRunTerminalReason;
4893
- const updated = await updateStatus(opts.runId, "completed");
4897
+ const terminalEvent = opts.terminalEvent;
4898
+ const isTerminalFailure =
4899
+ terminalEvent?.type === "error" ||
4900
+ terminalEvent?.type === "missing_api_key";
4901
+ const terminalReason =
4902
+ terminalEvent?.type === "error"
4903
+ ? `error:${terminalEvent.errorCode || "unknown"}`
4904
+ : terminalEvent?.type === "missing_api_key"
4905
+ ? "missing_api_key"
4906
+ : opts.continuationReason;
4907
+ const updated = await updateStatus(
4908
+ opts.runId,
4909
+ isTerminalFailure ? "errored" : "completed",
4910
+ );
4894
4911
  if (updated) {
4895
- await setTerminalReason(opts.runId, opts.continuationReason);
4912
+ await setTerminalReason(opts.runId, terminalReason);
4913
+ if (terminalEvent?.type === "error") {
4914
+ await persistError(
4915
+ opts.runId,
4916
+ terminalEvent.errorCode,
4917
+ terminalEvent.details || terminalEvent.error,
4918
+ );
4919
+ } else if (terminalEvent?.type === "missing_api_key") {
4920
+ await persistError(
4921
+ opts.runId,
4922
+ LLM_MISSING_CREDENTIALS_ERROR_CODE,
4923
+ LLM_MISSING_CREDENTIALS_MESSAGE,
4924
+ );
4925
+ }
4896
4926
  }
4897
4927
  return updated;
4898
4928
  }
@@ -5513,6 +5543,7 @@ export async function chainServerDrivenContinuation(opts: {
5513
5543
  .markBackgroundContinuationChunkTerminal({
5514
5544
  runId,
5515
5545
  continuationReason,
5546
+ terminalEvent: run.events.at(-1)?.event,
5516
5547
  })
5517
5548
  .catch(() => {});
5518
5549
  } catch (chainErr) {
@@ -1,5 +1,9 @@
1
1
  import { captureError } from "../server/capture-error.js";
2
- import { isLlmCredentialError } from "./engine/credential-errors.js";
2
+ import {
3
+ isLlmCredentialError,
4
+ LLM_MISSING_CREDENTIALS_ERROR_CODE,
5
+ LLM_MISSING_CREDENTIALS_MESSAGE,
6
+ } from "./engine/credential-errors.js";
3
7
  import { EngineError } from "./engine/types.js";
4
8
  import {
5
9
  insertRun,
@@ -377,6 +381,10 @@ function isTerminalRunEvent(event: AgentChatEvent): boolean {
377
381
  );
378
382
  }
379
383
 
384
+ function terminalEventForcesErroredStatus(event: AgentChatEvent | null) {
385
+ return event?.type === "error" || event?.type === "missing_api_key";
386
+ }
387
+
380
388
  function terminalReasonForRun(
381
389
  finalStatus: "completed" | "errored" | "aborted",
382
390
  terminalEvent: AgentChatEvent | null,
@@ -899,14 +907,27 @@ export function startRun(
899
907
  // /runs/active check while we wait for SQL writes to land.
900
908
  let completionError: unknown = null;
901
909
  let terminalPersistenceError: unknown = null;
910
+ const terminalEvent = pendingTerminalEvent?.event ?? null;
902
911
  if (
903
912
  onComplete &&
904
913
  !(run.status === "aborted" && run.abortReason === "no_progress")
905
914
  ) {
906
915
  try {
907
- const completionRun: ActiveRun = pendingTerminalEvent
908
- ? { ...run, events: [...run.events, pendingTerminalEvent] }
909
- : run;
916
+ const completionStatus =
917
+ run.status !== "aborted" &&
918
+ terminalEventForcesErroredStatus(terminalEvent)
919
+ ? "errored"
920
+ : run.status;
921
+ const completionRun: ActiveRun =
922
+ pendingTerminalEvent || completionStatus !== run.status
923
+ ? {
924
+ ...run,
925
+ status: completionStatus,
926
+ events: pendingTerminalEvent
927
+ ? [...run.events, pendingTerminalEvent]
928
+ : run.events,
929
+ }
930
+ : run;
910
931
  await onComplete(completionRun);
911
932
  } catch (err) {
912
933
  completionError = err;
@@ -924,12 +945,14 @@ export function startRun(
924
945
  const finalStatus =
925
946
  run.status === "aborted"
926
947
  ? "aborted"
927
- : run.status === "errored" || completionError
948
+ : run.status === "errored" ||
949
+ completionError ||
950
+ terminalEventForcesErroredStatus(terminalEvent)
928
951
  ? "errored"
929
952
  : "completed";
930
953
  const terminalReason = terminalReasonForRun(
931
954
  finalStatus,
932
- pendingTerminalEvent?.event ?? null,
955
+ terminalEvent,
933
956
  run.abortReason,
934
957
  completionError,
935
958
  );
@@ -953,7 +976,8 @@ export function startRun(
953
976
  const terminalEvent: AgentChatEvent =
954
977
  finalStatus === "completed"
955
978
  ? (pendingTerminalEvent?.event ?? { type: "done" })
956
- : pendingTerminalEvent?.event.type === "error"
979
+ : pendingTerminalEvent?.event.type === "error" ||
980
+ pendingTerminalEvent?.event.type === "missing_api_key"
957
981
  ? pendingTerminalEvent.event
958
982
  : pendingTerminalEvent?.event.type === "auto_continue"
959
983
  ? // The run was checkpointed at a soft-timeout/loop boundary and
@@ -1030,14 +1054,21 @@ export function startRun(
1030
1054
  if (finalStatus === "errored") {
1031
1055
  let errorCode: string | undefined;
1032
1056
  let errorDetail: string | undefined;
1033
- for (let i = run.events.length - 1; i >= 0; i--) {
1034
- const ev = run.events[i].event as {
1057
+ const diagnosticEvents = pendingTerminalEvent
1058
+ ? [...run.events, pendingTerminalEvent]
1059
+ : run.events;
1060
+ for (let i = diagnosticEvents.length - 1; i >= 0; i--) {
1061
+ const ev = diagnosticEvents[i].event as {
1035
1062
  type: string;
1036
1063
  error?: string;
1037
1064
  errorCode?: string;
1038
1065
  details?: string;
1039
1066
  };
1040
- if (ev.type === "error") {
1067
+ if (ev.type === "missing_api_key") {
1068
+ errorCode = LLM_MISSING_CREDENTIALS_ERROR_CODE;
1069
+ errorDetail = LLM_MISSING_CREDENTIALS_MESSAGE;
1070
+ break;
1071
+ } else if (ev.type === "error") {
1041
1072
  errorCode = ev.errorCode;
1042
1073
  errorDetail = ev.error ?? ev.details;
1043
1074
  break;
@@ -7,6 +7,10 @@ import { getDbExec, intType, isPostgres } from "../db/client.js";
7
7
  import { ensureColumnExists, ensureTableExists } from "../db/ddl-guard.js";
8
8
  import { widenIntColumnsToBigInt } from "../db/widen-columns.js";
9
9
  import { captureError } from "../server/capture-error.js";
10
+ import {
11
+ LLM_MISSING_CREDENTIALS_ERROR_CODE,
12
+ LLM_MISSING_CREDENTIALS_MESSAGE,
13
+ } from "./engine/credential-errors.js";
10
14
  import type { AgentChatEvent } from "./types.js";
11
15
 
12
16
  let _initPromise: Promise<void> | undefined;
@@ -877,9 +881,9 @@ function terminalStatusForEvent(
877
881
  event: AgentChatEvent,
878
882
  ): "completed" | "errored" | null {
879
883
  if (event.type === "error") return "errored";
884
+ if (event.type === "missing_api_key") return "errored";
880
885
  if (
881
886
  event.type === "done" ||
882
- event.type === "missing_api_key" ||
883
887
  event.type === "loop_limit" ||
884
888
  event.type === "auto_continue"
885
889
  ) {
@@ -897,32 +901,78 @@ function terminalReasonForEvent(event: AgentChatEvent): string | null {
897
901
  return null;
898
902
  }
899
903
 
900
- async function getLatestRunEvent(runId: string): Promise<{
904
+ function isRealFailureTerminalEvent(event: AgentChatEvent): boolean {
905
+ if (event.type === "missing_api_key") return true;
906
+ if (event.type !== "error") return false;
907
+ return event.errorCode !== STALE_RUN_ERROR_EVENT.errorCode;
908
+ }
909
+
910
+ const RUN_RECONCILIATION_TERMINAL_EVENT_LIMIT = 100;
911
+
912
+ async function getRunEventForReconciliation(runId: string): Promise<{
901
913
  event: AgentChatEvent;
902
914
  eventAt: number | null;
903
915
  } | null> {
904
916
  const client = getDbExec();
905
917
  const { rows } = await client.execute({
906
- sql: `SELECT seq, event_data, event_at FROM agent_run_events WHERE run_id = ? ORDER BY seq DESC LIMIT 1`,
907
- args: [runId],
918
+ sql: `SELECT seq, event_data, event_at
919
+ FROM agent_run_events
920
+ WHERE run_id = ?
921
+ AND (
922
+ event_data LIKE '{"type":"done"%'
923
+ OR event_data LIKE '{"type":"error"%'
924
+ OR event_data LIKE '{"type":"missing_api_key"%'
925
+ OR event_data LIKE '{"type":"loop_limit"%'
926
+ OR event_data LIKE '{"type":"auto_continue"%'
927
+ )
928
+ ORDER BY seq DESC
929
+ LIMIT ?`,
930
+ args: [runId, RUN_RECONCILIATION_TERMINAL_EVENT_LIMIT],
908
931
  });
909
- const row = rows[0] as
910
- | {
911
- event_at?: number | string | null;
912
- event_data?: string;
932
+ let latestTerminal: {
933
+ event: AgentChatEvent;
934
+ eventAt: number | null;
935
+ } | null = null;
936
+ for (const row of rows as Array<{
937
+ event_at?: number | string | null;
938
+ event_data?: string;
939
+ }>) {
940
+ const raw = row.event_data;
941
+ if (!raw) continue;
942
+ try {
943
+ const event = JSON.parse(raw) as AgentChatEvent;
944
+ if (!terminalStatusForEvent(event) || !terminalReasonForEvent(event)) {
945
+ continue;
913
946
  }
914
- | undefined;
915
- const raw = row?.event_data;
916
- if (!raw) return null;
917
- try {
918
- const eventAt = row.event_at == null ? NaN : Number(row.event_at);
919
- return {
920
- event: JSON.parse(raw) as AgentChatEvent,
921
- eventAt: Number.isFinite(eventAt) && eventAt > 0 ? eventAt : null,
922
- };
923
- } catch {
924
- return null;
947
+ const rawEventAt = row.event_at == null ? NaN : Number(row.event_at);
948
+ const parsed = {
949
+ event,
950
+ eventAt:
951
+ Number.isFinite(rawEventAt) && rawEventAt > 0 ? rawEventAt : null,
952
+ };
953
+ if (!latestTerminal) latestTerminal = parsed;
954
+ // A real stream failure must not be laundered into success by a later
955
+ // done/continuation boundary. Keep the synthetic stale-run error special:
956
+ // that repair marker may be superseded by a later durable done event.
957
+ if (isRealFailureTerminalEvent(event)) return parsed;
958
+ } catch {
959
+ continue;
960
+ }
925
961
  }
962
+ return latestTerminal;
963
+ }
964
+
965
+ function errorCodeForTerminalEvent(event: AgentChatEvent): string | null {
966
+ if (event.type === "missing_api_key")
967
+ return LLM_MISSING_CREDENTIALS_ERROR_CODE;
968
+ if (event.type === "error") return event.errorCode ?? null;
969
+ return null;
970
+ }
971
+
972
+ function errorDetailForTerminalEvent(event: AgentChatEvent): string | null {
973
+ if (event.type === "missing_api_key") return LLM_MISSING_CREDENTIALS_MESSAGE;
974
+ if (event.type !== "error") return null;
975
+ return (event.details || event.error || "").slice(0, 2000) || null;
926
976
  }
927
977
 
928
978
  /**
@@ -938,20 +988,15 @@ export async function reconcileTerminalRunFromEvents(
938
988
  runId: string,
939
989
  ): Promise<boolean> {
940
990
  await ensureRunTables();
941
- const latest = await getLatestRunEvent(runId);
991
+ const latest = await getRunEventForReconciliation(runId);
942
992
  if (!latest) return false;
943
993
  const status = terminalStatusForEvent(latest.event);
944
994
  const terminalReason = terminalReasonForEvent(latest.event);
945
995
  if (!status || !terminalReason) return false;
946
996
 
947
997
  const client = getDbExec();
948
- const errorCode =
949
- latest.event.type === "error" ? (latest.event.errorCode ?? null) : null;
950
- const errorDetail =
951
- latest.event.type === "error"
952
- ? (latest.event.details || latest.event.error || "").slice(0, 2000) ||
953
- null
954
- : null;
998
+ const errorCode = errorCodeForTerminalEvent(latest.event);
999
+ const errorDetail = errorDetailForTerminalEvent(latest.event);
955
1000
  const { rowsAffected } = await client.execute({
956
1001
  sql: `UPDATE agent_runs
957
1002
  SET status = ?,
@@ -1,6 +1,10 @@
1
1
  import type { ChatModelAdapter, ChatModelRunResult } from "@assistant-ui/react";
2
2
 
3
3
  import { actionPreparationContinuationNote } from "../agent/action-continuation-guidance.js";
4
+ import {
5
+ LLM_MISSING_CREDENTIALS_ERROR_CODE,
6
+ LLM_MISSING_CREDENTIALS_MESSAGE,
7
+ } from "../agent/engine/credential-errors.js";
4
8
  import type {
5
9
  AgentChatStructuredContentPart,
6
10
  AgentChatStructuredMessage,
@@ -157,7 +161,9 @@ const BACKGROUND_FOLLOW_IDLE_TIMEOUT_MS = 150_000;
157
161
  * (`agent_runs.terminal_reason`, surfaced by /runs/active). These runs died in
158
162
  * server-side handoff machinery where no richer error event may exist, so the
159
163
  * client owns the message. Keys are matched after stripping an optional
160
- * `error:` prefix (`terminalReasonForEvent` records `error:<code>`).
164
+ * `error:` prefix (`terminalReasonForEvent` records `error:<code>`). Keep this
165
+ * map scoped to terminal failures, including reasons that must override a
166
+ * mistakenly completed row.
161
167
  */
162
168
  const BACKGROUND_TERMINAL_REASON_MESSAGES: Record<string, string> = {
163
169
  background_worker_never_started:
@@ -166,6 +172,8 @@ const BACKGROUND_TERMINAL_REASON_MESSAGES: Record<string, string> = {
166
172
  "The agent's background worker could not hand off the next step of this run. Retry to continue from the preserved context.",
167
173
  dispatch_payload_missing:
168
174
  "The agent's background run lost its saved request data and could not continue. Retry to start a fresh run.",
175
+ missing_api_key: LLM_MISSING_CREDENTIALS_MESSAGE,
176
+ missing_credentials: LLM_MISSING_CREDENTIALS_MESSAGE,
169
177
  turn_continuation_budget_exhausted:
170
178
  "This request needed more automatic continuations than allowed and was stopped. Try breaking it into smaller steps.",
171
179
  };
@@ -1761,6 +1769,11 @@ export function createAgentChatAdapter(
1761
1769
  nextRunId: string,
1762
1770
  previousRunId: string | null,
1763
1771
  ) => {
1772
+ if (previousRunId !== nextRunId) {
1773
+ lastAutoContinueReason = null;
1774
+ lastRecoverableRunError = null;
1775
+ lastActivityTrail = [];
1776
+ }
1764
1777
  const rememberedSeq = seenRunSeqs.get(nextRunId);
1765
1778
  if (rememberedSeq !== undefined) {
1766
1779
  lastSeq = rememberedSeq;
@@ -2132,6 +2145,9 @@ export function createAgentChatAdapter(
2132
2145
  ...(runId ? { runId } : {}),
2133
2146
  };
2134
2147
  if (typeof window !== "undefined") {
2148
+ if (args.errorCode === LLM_MISSING_CREDENTIALS_ERROR_CODE) {
2149
+ dispatchMissingApiKey();
2150
+ }
2135
2151
  window.dispatchEvent(
2136
2152
  new CustomEvent("agent-chat:run-error", {
2137
2153
  detail: { ...runError, tabId },
@@ -2169,7 +2185,46 @@ export function createAgentChatAdapter(
2169
2185
  : "";
2170
2186
  // terminal_reason is either a bare reason ("dispatch_payload_missing")
2171
2187
  // or "error:<errorCode>" when derived from a terminal error event.
2188
+ const hasErrorTerminalReason = rawTerminalReason.startsWith("error:");
2172
2189
  const terminalReason = rawTerminalReason.replace(/^error:/, "");
2190
+ const mappedMessage = terminalReason
2191
+ ? BACKGROUND_TERMINAL_REASON_MESSAGES[terminalReason]
2192
+ : undefined;
2193
+ const mappedErrorCode =
2194
+ terminalReason === "missing_api_key" ||
2195
+ terminalReason === LLM_MISSING_CREDENTIALS_ERROR_CODE
2196
+ ? LLM_MISSING_CREDENTIALS_ERROR_CODE
2197
+ : terminalReason;
2198
+
2199
+ if (mappedMessage) {
2200
+ yield* emitBackgroundTerminalError({
2201
+ message: mappedMessage,
2202
+ errorCode: mappedErrorCode,
2203
+ details: `terminal_reason: ${rawTerminalReason}`,
2204
+ });
2205
+ return;
2206
+ }
2207
+
2208
+ if (hasErrorTerminalReason) {
2209
+ if (lastRecoverableRunError) {
2210
+ yield* emitBackgroundTerminalError({
2211
+ message: lastRecoverableRunError.message,
2212
+ errorCode:
2213
+ lastRecoverableRunError.errorCode ||
2214
+ mappedErrorCode ||
2215
+ "background_run_failed",
2216
+ details: lastRecoverableRunError.details,
2217
+ });
2218
+ return;
2219
+ }
2220
+ yield* emitBackgroundTerminalError({
2221
+ message:
2222
+ "The agent's background run failed before its final response could be recovered. You can retry from the preserved chat context.",
2223
+ errorCode: mappedErrorCode || "background_run_failed",
2224
+ details: `terminal_reason: ${rawTerminalReason}`,
2225
+ });
2226
+ return;
2227
+ }
2173
2228
 
2174
2229
  if (status === "completed") {
2175
2230
  // The turn finished server-side; the events we managed to fold are
@@ -2186,17 +2241,6 @@ export function createAgentChatAdapter(
2186
2241
  return;
2187
2242
  }
2188
2243
 
2189
- const mappedMessage = terminalReason
2190
- ? BACKGROUND_TERMINAL_REASON_MESSAGES[terminalReason]
2191
- : undefined;
2192
- if (mappedMessage) {
2193
- yield* emitBackgroundTerminalError({
2194
- message: mappedMessage,
2195
- errorCode: terminalReason,
2196
- details: `terminal_reason: ${rawTerminalReason}`,
2197
- });
2198
- return;
2199
- }
2200
2244
  if (lastRecoverableRunError) {
2201
2245
  // A replayed terminal error event already carried the real
2202
2246
  // message (recoverable errors replay as auto-continue signals
@@ -305,14 +305,24 @@ export function TranscriptPanel(props: TranscriptPanelProps) {
305
305
  const isActive = displaySegments[activeIndex] === seg;
306
306
  return (
307
307
  <li key={seg.startMs}>
308
- <button
309
- onClick={() => onSeek(seg.startMs)}
308
+ <div
309
+ role="button"
310
+ tabIndex={0}
311
+ onClick={(event) => {
312
+ if (hasSelectionWithin(event.currentTarget)) return;
313
+ onSeek(seg.startMs);
314
+ }}
315
+ onKeyDown={(event) => {
316
+ if (event.key !== "Enter" && event.key !== " ") return;
317
+ event.preventDefault();
318
+ onSeek(seg.startMs);
319
+ }}
310
320
  className={cn(
311
- "w-full text-left px-3 py-2 flex gap-3 items-start hover:bg-accent/50",
321
+ "flex w-full cursor-pointer items-start gap-3 px-3 py-2 text-left hover:bg-accent/50",
312
322
  isActive && "bg-accent",
313
323
  )}
314
324
  >
315
- <span className="text-[11px] text-muted-foreground font-mono tabular-nums pt-0.5 shrink-0">
325
+ <span className="shrink-0 pt-0.5 font-mono text-[11px] tabular-nums text-muted-foreground">
316
326
  {msToClock(seg.startMs)}
317
327
  </span>
318
328
  <span
@@ -324,7 +334,7 @@ export function TranscriptPanel(props: TranscriptPanelProps) {
324
334
  __html: highlight(seg.text, query),
325
335
  }}
326
336
  />
327
- </button>
337
+ </div>
328
338
  </li>
329
339
  );
330
340
  })}
@@ -335,6 +345,18 @@ export function TranscriptPanel(props: TranscriptPanelProps) {
335
345
  );
336
346
  }
337
347
 
348
+ function hasSelectionWithin(element: HTMLElement): boolean {
349
+ const selection = window.getSelection();
350
+ if (!selection || selection.isCollapsed || !selection.toString().trim()) {
351
+ return false;
352
+ }
353
+ const { anchorNode, focusNode } = selection;
354
+ return Boolean(
355
+ (anchorNode && element.contains(anchorNode)) ||
356
+ (focusNode && element.contains(focusNode)),
357
+ );
358
+ }
359
+
338
360
  function highlight(text: string, q: string): string {
339
361
  const escaped = text.replace(/[&<>]/g, (c) =>
340
362
  c === "&" ? "&amp;" : c === "<" ? "&lt;" : "&gt;",
@@ -40,6 +40,7 @@ export function usePlayerShortcuts(opts: UsePlayerShortcutsOpts) {
40
40
  if (!enabled) return;
41
41
 
42
42
  function onKey(e: KeyboardEvent) {
43
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
43
44
  if (shouldIgnore(e.target)) return;
44
45
  const player = playerRef.current;
45
46
  if (!player) return;
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-10
4
+ ---
5
+
6
+ Fixed copying agent responses and transcript text from recording pages.