cortena-ui 1.5.0 → 1.7.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.
@@ -1,7 +1,8 @@
1
1
  "use client";
2
2
  "use client";
3
3
  import { extractA2UIBlocks, foldA2UIBlocks } from "./a2ui-block.js";
4
- import { isProvisionalSessionKey, isSessionOfAgent, mintSessionKey, parseHistoryMessages, readStoredSessionKey, separateThinking, wouldShrinkHistory, writeStoredSessionKey } from "./session.js";
4
+ import { confirmationRequiredLabel, isGenericLabel, stepLabel, truncate } from "./step-label.js";
5
+ import { isProvisionalSessionKey, isSessionOfAgent, mintSessionKey, parseHistoryMessages, readStoredSessionKey, readStoredSteps, separateThinking, wouldShrinkHistory, writeStoredSessionKey, writeStoredSteps } from "./session.js";
5
6
  import * as React from "react";
6
7
  //#region src/agent-chat/store.ts
7
8
  /**
@@ -36,6 +37,8 @@ const EMPTY = {
36
37
  loadingHistory: false,
37
38
  toolOrder: [],
38
39
  tools: {},
40
+ steps: [],
41
+ retiredSteps: [],
39
42
  approvals: [],
40
43
  historyCount: 0,
41
44
  sessions: [],
@@ -63,6 +66,11 @@ function reduce(state, action) {
63
66
  ...state,
64
67
  approvals: state.approvals.filter((r) => r.id !== action.id)
65
68
  };
69
+ case "approval/denied": return {
70
+ ...state,
71
+ approvals: state.approvals.filter((r) => r.id !== action.id),
72
+ steps: endRunningSteps(state.steps, { kind: "denied" })
73
+ };
66
74
  case "approval/failed": return state.approvals.some((r) => r.id === action.request.id) ? state : {
67
75
  ...state,
68
76
  approvals: [...state.approvals, {
@@ -78,7 +86,10 @@ function reduce(state, action) {
78
86
  case "send": return {
79
87
  ...state,
80
88
  sessionKey: action.sessionKey,
81
- messages: action.message ? [...state.messages, action.message] : state.messages,
89
+ messages: action.message ? [...state.messages, action.message] : action.reanchor ? state.messages.map((m) => m.id === action.reanchor ? {
90
+ ...m,
91
+ runId: action.runId
92
+ } : m) : state.messages,
82
93
  streaming: true,
83
94
  streamText: "",
84
95
  streamThinkingText: "",
@@ -86,9 +97,15 @@ function reduce(state, action) {
86
97
  runId: action.runId,
87
98
  toolOrder: [],
88
99
  tools: {},
100
+ steps: [],
101
+ retiredSteps: [],
89
102
  approvals: [],
90
103
  error: null
91
104
  };
105
+ case "message/remove": return {
106
+ ...state,
107
+ messages: state.messages.filter((m) => m.id !== action.id)
108
+ };
92
109
  case "send/failed": return {
93
110
  ...state,
94
111
  streaming: false,
@@ -108,13 +125,18 @@ function reduce(state, action) {
108
125
  streamThinkingText: "",
109
126
  streamA2ui: [],
110
127
  runId: null,
111
- approvals: []
128
+ approvals: [],
129
+ steps: endRunningSteps(state.steps, { kind: "stopped" })
112
130
  };
113
131
  case "history/loading": return {
114
132
  ...state,
115
133
  loadingHistory: true
116
134
  };
117
135
  case "history/loaded":
136
+ if (action.sessionKey !== state.sessionKey) return {
137
+ ...state,
138
+ loadingHistory: false
139
+ };
118
140
  if (wouldShrinkHistory({
119
141
  currentSessionKey: state.sessionKey,
120
142
  loadedSessionKey: action.sessionKey,
@@ -139,13 +161,18 @@ function reduce(state, action) {
139
161
  streamThinkingText: "",
140
162
  streamA2ui: []
141
163
  };
142
- case "history/older": return {
143
- ...state,
144
- messages: [...action.messages, ...state.messages],
145
- hasMoreHistory: action.hasMore,
146
- loadingHistory: false,
147
- historyCount: state.historyCount + action.records
148
- };
164
+ case "history/older":
165
+ if (action.sessionKey !== state.sessionKey) return {
166
+ ...state,
167
+ loadingHistory: false
168
+ };
169
+ return {
170
+ ...state,
171
+ messages: [...action.messages, ...state.messages],
172
+ hasMoreHistory: action.hasMore,
173
+ loadingHistory: false,
174
+ historyCount: state.historyCount + action.records
175
+ };
149
176
  case "history/failed": return {
150
177
  ...state,
151
178
  loadingHistory: false
@@ -159,6 +186,10 @@ function reduce(state, action) {
159
186
  sessions: action.sessions,
160
187
  loadingSessions: false
161
188
  };
189
+ case "steps/restored": return state.steps.length > 0 ? state : {
190
+ ...state,
191
+ steps: action.steps.slice(-50)
192
+ };
162
193
  case "error": return {
163
194
  ...state,
164
195
  error: action.message
@@ -203,7 +234,8 @@ function reduceChat(state, event) {
203
234
  streaming: false,
204
235
  streamText: "",
205
236
  streamThinkingText: "",
206
- streamA2ui: []
237
+ streamA2ui: [],
238
+ steps: endRunningSteps(state.steps, event.interrupted ? { kind: "paused" } : { kind: "finished" })
207
239
  };
208
240
  if (!text && a2ui.length === 0) {
209
241
  const accumulated = state.streamThinkingText;
@@ -260,6 +292,10 @@ function reduceChat(state, event) {
260
292
  streamThinkingText: "",
261
293
  streamA2ui: [],
262
294
  runId: null,
295
+ steps: endRunningSteps(state.steps, {
296
+ kind: "failed",
297
+ message: truncate(event.errorMessage ?? "The run failed.", ERROR_LIMIT)
298
+ }),
263
299
  messages: [...state.messages, {
264
300
  id: generateId(),
265
301
  role: "assistant",
@@ -333,16 +369,275 @@ function reduceTool(state, event) {
333
369
  break;
334
370
  default: return state;
335
371
  }
372
+ const stepped = reduceStep(state, event, id);
336
373
  return {
337
374
  ...state,
338
375
  tools: {
339
376
  ...state.tools,
340
377
  [id]: entry
341
378
  },
342
- toolOrder: state.toolOrder.includes(id) ? state.toolOrder : [...state.toolOrder, id]
379
+ toolOrder: state.toolOrder.includes(id) ? state.toolOrder : [...state.toolOrder, id],
380
+ steps: stepped.steps,
381
+ retiredSteps: stepped.retiredSteps
382
+ };
383
+ }
384
+ /**
385
+ * `TOOL_CALL_RESULT.metadata`, read defensively.
386
+ *
387
+ * `null` when nothing usable arrived, which a transport that predates the
388
+ * contract will produce. A step with no metadata at all is treated as a
389
+ * success by `tool.complete` and as a failure by `tool.error`, which is the
390
+ * most either event can honestly claim on its own.
391
+ *
392
+ * Note what is NOT here: any reading of `content`. The result text is the
393
+ * model's copy of the reply, in whatever shape the tool returned, and the
394
+ * client's previous attempt to find `ok: false` in it matched nothing real —
395
+ * a Cortena tool result is `{ content: [...], details }`, so the envelope it
396
+ * was looking for was two levels down when it existed at all.
397
+ */
398
+ function readResultMetadata(raw) {
399
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
400
+ const record = raw;
401
+ const meta = readMeta(record.meta);
402
+ return {
403
+ ...typeof record.toolName === "string" ? { toolName: record.toolName } : {},
404
+ isError: record.isError === true,
405
+ ...meta ? { meta } : {}
406
+ };
407
+ }
408
+ function readMeta(raw) {
409
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
410
+ const record = raw;
411
+ const meta = {
412
+ ...typeof record.status === "number" ? { status: record.status } : {},
413
+ ...typeof record.code === "string" ? { code: record.code } : {},
414
+ ...typeof record.message === "string" ? { message: record.message } : {}
415
+ };
416
+ return Object.keys(meta).length > 0 ? meta : null;
417
+ }
418
+ /**
419
+ * Is this the broker asking for a yes rather than reporting a failure?
420
+ *
421
+ * The broker refuses a catalogued write with `409 confirmation_required` until
422
+ * the call carries `confirmed: true`. It is the safety rail working, and the
423
+ * step says so instead of showing the user a red cross for it.
424
+ *
425
+ * Keyed on the CODE, never on the status. `409` is also an ordinary conflict —
426
+ * a task already claimed, a version that moved under a write — and wording one
427
+ * of those as "Needs your confirmation" invites the user to retry something
428
+ * that will never work.
429
+ */
430
+ function isConfirmationRequired(meta) {
431
+ return meta?.code === "confirmation_required";
432
+ }
433
+ /** Longest failure message put on a step. */
434
+ const ERROR_LIMIT = 200;
435
+ /**
436
+ * The human part of a tool result, for a failure that carried no `meta`.
437
+ *
438
+ * A Cortena tool returns `{ content: [{ type: "text", text }], details }`,
439
+ * JSON-stringified by the mapper, so the text blocks are the message and the
440
+ * rest is machinery. Anything else — a plain string, a shape this does not
441
+ * recognise — is used as it stands. Bounded and sanitised either way.
442
+ */
443
+ function resultText(output) {
444
+ const direct = typeof output === "string" ? output : "";
445
+ let value = output;
446
+ if (direct) try {
447
+ value = JSON.parse(direct);
448
+ } catch {
449
+ return truncate(direct, ERROR_LIMIT);
450
+ }
451
+ if (typeof value === "object" && value !== null) {
452
+ const content = value.content;
453
+ if (Array.isArray(content)) {
454
+ const text = content.map((part) => typeof part === "object" && part !== null && typeof part.text === "string" ? part.text : "").filter(Boolean).join(" ");
455
+ if (text) return truncate(text, ERROR_LIMIT);
456
+ }
457
+ }
458
+ return truncate(direct, ERROR_LIMIT);
459
+ }
460
+ /** What a failed step says: the server's message, or the result text. */
461
+ function errorMessageOf(meta, output) {
462
+ return (meta?.message ? truncate(meta.message, ERROR_LIMIT) : "") || resultText(output) || "The call failed.";
463
+ }
464
+ /** The call's input as an object, or nothing when it never parsed as one. */
465
+ function readArgs(input) {
466
+ return typeof input === "object" && input !== null && !Array.isArray(input) ? input : void 0;
467
+ }
468
+ /** Is this step still open — spinning, or waiting on the user? */
469
+ function isOpenStep(step) {
470
+ return step.status === "running" || step.status === "paused";
471
+ }
472
+ function endRunningSteps(steps, outcome) {
473
+ const closes = (step) => outcome.kind === "paused" ? step.status === "running" : outcome.kind === "denied" ? step.status === "paused" : isOpenStep(step);
474
+ if (!steps.some(closes)) return steps;
475
+ const endedAt = Date.now();
476
+ return steps.map((step) => {
477
+ if (!closes(step)) return step;
478
+ if (outcome.kind === "paused") return {
479
+ ...step,
480
+ status: "paused"
481
+ };
482
+ if (outcome.kind === "failed") return {
483
+ ...step,
484
+ status: "error",
485
+ endedAt,
486
+ errorMessage: outcome.message
487
+ };
488
+ if (outcome.kind === "stopped" || outcome.kind === "denied") return {
489
+ ...step,
490
+ status: "stopped",
491
+ endedAt
492
+ };
493
+ return {
494
+ ...step,
495
+ status: "done",
496
+ endedAt
497
+ };
498
+ });
499
+ }
500
+ /**
501
+ * A step appended, with the list capped at `STEP_LIMIT`.
502
+ *
503
+ * A turn with more calls than this is a loop, not a plan, and an uncapped list
504
+ * grows without bound in memory and re-renders the whole strip on every event
505
+ * of it. The most recent are kept: they are what the user is waiting on, and
506
+ * the ids of the ones dropped are kept so a late result for one is recognised
507
+ * rather than mistaken for a call nobody announced.
508
+ */
509
+ function append(steps, retiredSteps, step) {
510
+ const next = [...steps, step];
511
+ if (next.length <= 50) return {
512
+ steps: next,
513
+ retiredSteps
514
+ };
515
+ const dropped = next.slice(0, next.length - 50).map((s) => s.id);
516
+ return {
517
+ steps: next.slice(-50),
518
+ retiredSteps: [...retiredSteps, ...dropped].slice(-RETIRED_LIMIT)
519
+ };
520
+ }
521
+ /** Most dropped ids remembered. Enough for stragglers, not a transcript. */
522
+ const RETIRED_LIMIT = 100;
523
+ function replaceStep(steps, index, next) {
524
+ const copy = [...steps];
525
+ copy[index] = next;
526
+ return copy;
527
+ }
528
+ /**
529
+ * One tool event, folded into the step list.
530
+ *
531
+ * Start and end are paired by the AG-UI `toolCallId`, not by position: calls
532
+ * overlap, and a run that issues two searches at once ended the wrong one when
533
+ * the list was treated as a stack.
534
+ *
535
+ * The label is recomputed when the arguments land, because they land in a
536
+ * SECOND event. `TOOL_CALL_START` carries the name only, so a label fixed at
537
+ * start time is "Working: engram_search" for the whole of the call — the one
538
+ * line the wording map exists to avoid.
539
+ */
540
+ function reduceStep(state, event, id) {
541
+ const { steps, retiredSteps } = state;
542
+ const keep = {
543
+ steps,
544
+ retiredSteps
545
+ };
546
+ const index = steps.findIndex((step) => step.id === id);
547
+ const existing = index === -1 ? void 0 : steps[index];
548
+ if (!existing && retiredSteps.includes(id)) return keep;
549
+ if (event.event === "tool.start") {
550
+ if (existing && !isOpenStep(existing)) return keep;
551
+ const toolName = event.data.name ?? existing?.toolName ?? "tool";
552
+ const step = {
553
+ id,
554
+ toolName,
555
+ ...existing?.args ? { args: existing.args } : {},
556
+ label: stepLabel(toolName, existing?.args, "running"),
557
+ startedAt: event.data.startedAt ?? existing?.startedAt ?? Date.now(),
558
+ status: "running"
559
+ };
560
+ return existing && index !== -1 ? {
561
+ steps: replaceStep(steps, index, step),
562
+ retiredSteps
563
+ } : append(steps, retiredSteps, step);
564
+ }
565
+ const base = existing ?? {
566
+ id,
567
+ toolName: event.data.name ?? "tool",
568
+ label: stepLabel(event.data.name ?? "tool", void 0, "running"),
569
+ startedAt: event.data.startedAt ?? Date.now(),
570
+ status: "running"
571
+ };
572
+ const at = index === -1 ? steps.length : index;
573
+ const put = (next) => index === -1 ? append(steps, retiredSteps, next) : {
574
+ steps: replaceStep(steps, at, next),
575
+ retiredSteps
576
+ };
577
+ switch (event.event) {
578
+ case "tool.args": {
579
+ const args = readArgs(event.data.input);
580
+ if (!args) return keep;
581
+ if (base.label === CONFIRMATION_FALLBACK) return put({
582
+ ...base,
583
+ args,
584
+ label: confirmationRequiredLabel(args)
585
+ });
586
+ const open = isOpenStep(base);
587
+ const relabel = open || isGenericLabel(base.toolName, base.label);
588
+ return put({
589
+ ...base,
590
+ args,
591
+ label: relabel ? stepLabel(base.toolName, args, open ? "running" : "done") : base.label
592
+ });
593
+ }
594
+ case "tool.progress": return keep;
595
+ case "tool.complete":
596
+ case "tool.error": return put(endStep(base, event));
597
+ default: return keep;
598
+ }
599
+ }
600
+ /** The confirmation label with no operation on it; see `tool.args` above. */
601
+ const CONFIRMATION_FALLBACK = confirmationRequiredLabel(void 0);
602
+ /**
603
+ * A start event's step, ended by its result.
604
+ *
605
+ * The whole outcome decision, in one place, against
606
+ * `TOOL_CALL_RESULT.metadata` and nothing else:
607
+ *
608
+ * - `isError` with `meta.code === "confirmation_required"` — not a failure.
609
+ * The broker is asking a question, and the step is relabelled to ask it.
610
+ * - `isError` otherwise — a failure, worded from `meta.message` when there is
611
+ * one and from the result text when there is not.
612
+ * - neither — done.
613
+ */
614
+ function endStep(base, event) {
615
+ const metadata = readResultMetadata(event.data.metadata);
616
+ const isError = metadata?.isError === true || event.event === "tool.error";
617
+ const meta = metadata?.meta;
618
+ const endedAt = event.data.completedAt ?? Date.now();
619
+ if (isError && isConfirmationRequired(meta)) return {
620
+ ...base,
621
+ status: "done",
622
+ endedAt,
623
+ label: confirmationRequiredLabel(base.args)
624
+ };
625
+ if (isError) return {
626
+ ...base,
627
+ status: "error",
628
+ endedAt,
629
+ errorMessage: errorMessageOf(meta, event.data.output)
630
+ };
631
+ return {
632
+ ...base,
633
+ status: "done",
634
+ endedAt,
635
+ label: stepLabel(base.toolName, base.args, "done")
343
636
  };
344
637
  }
345
- function useAgentChat({ client, storageScope, ephemeral, thinking }) {
638
+ /** How long a burst of step changes is collapsed for before it is written. */
639
+ const STEP_WRITE_DEBOUNCE_MS = 250;
640
+ function useAgentChat({ client, storageScope, ephemeral, thinking, mintSessionKey: mintKey, restoreSession = true, listSessions = true }) {
346
641
  const [state, dispatch] = React.useReducer(reduce, EMPTY);
347
642
  const stateRef = React.useRef(state);
348
643
  stateRef.current = state;
@@ -372,6 +667,7 @@ function useAgentChat({ client, storageScope, ephemeral, thinking }) {
372
667
  };
373
668
  }, [client]);
374
669
  const refreshSessions = React.useCallback(() => {
670
+ if (!listSessions) return;
375
671
  dispatch({ kind: "sessions/loading" });
376
672
  client.request("sessions.list", { includeDerivedTitles: true }).then((response) => {
377
673
  const sessions = (response?.sessions ?? []).filter((s) => isSessionOfAgent(s.key, client.agentId));
@@ -389,7 +685,7 @@ function useAgentChat({ client, storageScope, ephemeral, thinking }) {
389
685
  message: errorText(err)
390
686
  });
391
687
  });
392
- }, [client]);
688
+ }, [client, listSessions]);
393
689
  const loadHistory = React.useCallback((sessionKey) => {
394
690
  dispatch({ kind: "history/loading" });
395
691
  client.request("chat.history", {
@@ -410,13 +706,18 @@ function useAgentChat({ client, storageScope, ephemeral, thinking }) {
410
706
  React.useEffect(() => {
411
707
  if (restored.current) return;
412
708
  restored.current = true;
413
- const stored = ephemeral ? null : readStoredSessionKey(storageScope);
709
+ const stored = ephemeral || !restoreSession ? null : readStoredSessionKey(storageScope);
414
710
  const key = stored && isSessionOfAgent(stored, client.agentId) ? stored : null;
415
711
  if (key) {
416
712
  dispatch({
417
713
  kind: "session",
418
714
  key
419
715
  });
716
+ const steps = ephemeral ? [] : readStoredSteps(storageScope, key);
717
+ if (steps.length > 0) dispatch({
718
+ kind: "steps/restored",
719
+ steps
720
+ });
420
721
  loadHistory(key);
421
722
  }
422
723
  refreshSessions();
@@ -425,8 +726,50 @@ function useAgentChat({ client, storageScope, ephemeral, thinking }) {
425
726
  ephemeral,
426
727
  loadHistory,
427
728
  refreshSessions,
729
+ restoreSession,
428
730
  storageScope
429
731
  ]);
732
+ /**
733
+ * Keep the per-window store in step with the strip, on a debounce.
734
+ *
735
+ * Skipped while there is no session key, which is also the first commit —
736
+ * writing then would empty the slot the effect above is about to read.
737
+ *
738
+ * Debounced because a busy run changes `steps` several times per call —
739
+ * start, arguments, result — and each write serialises the whole list and
740
+ * hands it to a SYNCHRONOUS storage API on the main thread. A quarter of a
741
+ * second collapses a call's worth of edits into one write and is far below
742
+ * anything a reload could notice.
743
+ */
744
+ const pendingSteps = React.useRef(null);
745
+ React.useEffect(() => {
746
+ if (ephemeral || !state.sessionKey) return;
747
+ const sessionKey = state.sessionKey;
748
+ const steps = state.steps;
749
+ pendingSteps.current = {
750
+ sessionKey,
751
+ steps
752
+ };
753
+ const id = setTimeout(() => {
754
+ pendingSteps.current = null;
755
+ writeStoredSteps(storageScope, sessionKey, steps);
756
+ }, STEP_WRITE_DEBOUNCE_MS);
757
+ return () => clearTimeout(id);
758
+ }, [
759
+ ephemeral,
760
+ state.sessionKey,
761
+ state.steps,
762
+ storageScope
763
+ ]);
764
+ React.useEffect(() => {
765
+ return () => {
766
+ const pending = pendingSteps.current;
767
+ if (!ephemeral && pending) {
768
+ pendingSteps.current = null;
769
+ writeStoredSteps(storageScope, pending.sessionKey, pending.steps);
770
+ }
771
+ };
772
+ }, [ephemeral, storageScope]);
430
773
  const setSession = React.useCallback((key) => {
431
774
  dispatch({
432
775
  kind: "session",
@@ -434,24 +777,26 @@ function useAgentChat({ client, storageScope, ephemeral, thinking }) {
434
777
  });
435
778
  if (!ephemeral) writeStoredSessionKey(storageScope, key);
436
779
  }, [ephemeral, storageScope]);
437
- const run = React.useCallback((text, extra = {}) => {
780
+ const run = React.useCallback((text, extra = {}, reanchor) => {
438
781
  const current = stateRef.current;
439
782
  if (current.streaming) return;
440
- const sessionKey = current.sessionKey ?? mintSessionKey(client.agentId);
783
+ const sessionKey = current.sessionKey ?? (mintKey ? mintKey() : mintSessionKey(client.agentId));
441
784
  if (!current.sessionKey && !ephemeral) writeStoredSessionKey(storageScope, sessionKey);
442
785
  const runId = generateId();
443
- const message = text || !extra.a2uiAction && !extra.mcpAppAction ? {
786
+ const message = !reanchor && (text || !extra.a2uiAction && !extra.mcpAppAction) ? {
444
787
  id: runId,
445
788
  role: "user",
446
789
  text,
447
790
  timestamp: Date.now(),
448
- runId
791
+ runId,
792
+ ...extra.attachments?.length ? { attachmentItems: extra.attachments } : {}
449
793
  } : null;
450
794
  dispatch({
451
795
  kind: "send",
452
796
  message,
453
797
  runId,
454
- sessionKey
798
+ sessionKey,
799
+ ...reanchor ? { reanchor } : {}
455
800
  });
456
801
  client.send({
457
802
  sessionKey,
@@ -466,16 +811,44 @@ function useAgentChat({ client, storageScope, ephemeral, thinking }) {
466
811
  }, [
467
812
  client,
468
813
  ephemeral,
814
+ mintKey,
469
815
  refreshSessions,
470
816
  storageScope,
471
817
  thinking
472
818
  ]);
473
- const send = React.useCallback((text, attachments) => {
474
- run(text, attachments?.length ? { attachments } : {});
819
+ const send = React.useCallback((text, attachments, options) => {
820
+ run(text, {
821
+ ...attachments?.length ? { attachments } : {},
822
+ ...options?.model ? { model: options.model } : {},
823
+ ...options?.thinking ? { thinking: options.thinking } : {}
824
+ });
475
825
  }, [run]);
476
826
  const sendAction = React.useCallback((a2uiAction) => {
477
827
  run("", { a2uiAction });
478
828
  }, [run]);
829
+ const sendMcpAppAction = React.useCallback((mcpAppAction) => {
830
+ run("", { mcpAppAction });
831
+ }, [run]);
832
+ const regenerate = React.useCallback((messageId) => {
833
+ const { messages, streaming } = stateRef.current;
834
+ if (streaming) return;
835
+ const index = messages.findIndex((m) => m.id === messageId);
836
+ if (index < 1) return;
837
+ let question = null;
838
+ for (let i = index - 1; i >= 0; i -= 1) {
839
+ const candidate = messages[i];
840
+ if (candidate?.role === "user" && !candidate.isToolMessage) {
841
+ question = candidate;
842
+ break;
843
+ }
844
+ }
845
+ if (question === null) return;
846
+ dispatch({
847
+ kind: "message/remove",
848
+ id: messageId
849
+ });
850
+ run(question.text, {}, question.id);
851
+ }, [run]);
479
852
  const abort = React.useCallback(() => {
480
853
  const { runId, sessionKey } = stateRef.current;
481
854
  dispatch({ kind: "abort" });
@@ -487,7 +860,10 @@ function useAgentChat({ client, storageScope, ephemeral, thinking }) {
487
860
  const resolveApproval = React.useCallback((id, decision) => {
488
861
  const request = stateRef.current.approvals.find((r) => r.id === id);
489
862
  if (!request) return;
490
- dispatch({
863
+ dispatch(decision === "deny" ? {
864
+ kind: "approval/denied",
865
+ id
866
+ } : {
491
867
  kind: "approval/resolved",
492
868
  id
493
869
  });
@@ -503,38 +879,55 @@ function useAgentChat({ client, storageScope, ephemeral, thinking }) {
503
879
  });
504
880
  }, [client]);
505
881
  const newSession = React.useCallback(() => {
506
- setSession(mintSessionKey(client.agentId));
507
- }, [client, setSession]);
882
+ setSession(mintKey ? mintKey() : mintSessionKey(client.agentId));
883
+ }, [
884
+ client,
885
+ mintKey,
886
+ setSession
887
+ ]);
888
+ const clearSession = React.useCallback(() => {
889
+ dispatch({
890
+ kind: "session",
891
+ key: null
892
+ });
893
+ if (!ephemeral) writeStoredSessionKey(storageScope, null);
894
+ }, [ephemeral, storageScope]);
508
895
  const resumeSession = React.useCallback((key) => {
509
896
  setSession(key);
510
897
  loadHistory(key);
511
898
  }, [loadHistory, setSession]);
512
899
  const loadOlder = React.useCallback(() => {
513
900
  const current = stateRef.current;
514
- if (!current.sessionKey || current.loadingHistory || !current.hasMoreHistory) return;
901
+ if (!current.sessionKey || current.loadingHistory || !current.hasMoreHistory) return false;
902
+ const sessionKey = current.sessionKey;
515
903
  dispatch({ kind: "history/loading" });
516
904
  client.request("chat.history", {
517
- sessionKey: current.sessionKey,
905
+ sessionKey,
518
906
  limit: 100,
519
907
  offset: current.historyCount
520
908
  }).then((response) => {
521
909
  const records = response?.messages ?? [];
522
910
  dispatch({
523
911
  kind: "history/older",
912
+ sessionKey,
524
913
  messages: parseHistoryMessages(records),
525
914
  hasMore: response?.hasMore ?? false,
526
915
  records: records.length
527
916
  });
528
917
  }).catch(() => dispatch({ kind: "history/failed" }));
918
+ return true;
529
919
  }, [client]);
530
920
  return {
531
921
  ...state,
532
922
  send,
533
923
  sendAction,
924
+ sendMcpAppAction,
534
925
  abort,
535
926
  resolveApproval,
536
927
  newSession,
537
928
  resumeSession,
929
+ clearSession,
930
+ regenerate,
538
931
  loadOlder,
539
932
  refreshSessions
540
933
  };
@@ -543,6 +936,6 @@ function errorText(err) {
543
936
  return err instanceof Error ? err.message : "The agent could not be reached";
544
937
  }
545
938
  //#endregion
546
- export { useAgentChat };
939
+ export { readResultMetadata, useAgentChat };
547
940
 
548
941
  //# sourceMappingURL=store.js.map