@agent-native/toolkit 0.226.0 → 0.226.1-nightly-20261107004051

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 (46) hide show
  1. package/dist/app/agentkit/react/components.d.ts +11 -9
  2. package/dist/app/agentkit/react/components.d.ts.map +1 -1
  3. package/dist/app/agentkit/react/components.js +115 -111
  4. package/dist/app/agentkit/react/components.js.map +1 -1
  5. package/dist/app/agentkit/react/stop-button.js +5 -5
  6. package/dist/app/agentkit/react/stop-button.js.map +1 -1
  7. package/dist/app/agentkit/react/styles.css +19 -48
  8. package/dist/app/auth/AuthPage.d.ts.map +1 -1
  9. package/dist/app/auth/AuthPage.js +7 -5
  10. package/dist/app/auth/AuthPage.js.map +1 -1
  11. package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
  12. package/dist/app/chat/AgentKitAssistantChat.js +7 -2
  13. package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
  14. package/dist/app/integrations/IntegrationDetailPage.d.ts.map +1 -1
  15. package/dist/app/integrations/IntegrationDetailPage.js +43 -1
  16. package/dist/app/integrations/IntegrationDetailPage.js.map +1 -1
  17. package/dist/app/integrations/IntegrationsPage.d.ts.map +1 -1
  18. package/dist/app/integrations/IntegrationsPage.js +16 -7
  19. package/dist/app/integrations/IntegrationsPage.js.map +1 -1
  20. package/dist/app/integrations/connected-server-id.d.ts +17 -0
  21. package/dist/app/integrations/connected-server-id.d.ts.map +1 -0
  22. package/dist/app/integrations/connected-server-id.js +26 -0
  23. package/dist/app/integrations/connected-server-id.js.map +1 -0
  24. package/dist/composer/TiptapComposer.d.ts +5 -3
  25. package/dist/composer/TiptapComposer.d.ts.map +1 -1
  26. package/dist/composer/TiptapComposer.js +1 -1
  27. package/dist/composer/TiptapComposer.js.map +1 -1
  28. package/package.json +4 -4
  29. package/src/app/agentkit/react/components.interaction.spec.tsx +112 -0
  30. package/src/app/agentkit/react/components.tsx +178 -192
  31. package/src/app/agentkit/react/lifecycle.spec.tsx +5 -16
  32. package/src/app/agentkit/react/stop-button.spec.tsx +1 -1
  33. package/src/app/agentkit/react/stop-button.tsx +5 -5
  34. package/src/app/agentkit/react/styles.css +19 -48
  35. package/src/app/agentkit/react/work-handoff.spec.tsx +202 -69
  36. package/src/app/auth/AuthPage.interaction.spec.tsx +17 -1
  37. package/src/app/auth/AuthPage.tsx +12 -4
  38. package/src/app/chat/AgentKitAssistantChat.tsx +6 -2
  39. package/src/app/integrations/IntegrationDetailPage.spec.tsx +30 -0
  40. package/src/app/integrations/IntegrationDetailPage.tsx +85 -1
  41. package/src/app/integrations/IntegrationsPage.spec.tsx +74 -0
  42. package/src/app/integrations/IntegrationsPage.tsx +47 -20
  43. package/src/app/integrations/connected-server-id.ts +34 -0
  44. package/src/composer/TiptapComposer.spec.ts +7 -7
  45. package/src/composer/TiptapComposer.tsx +12 -13
  46. package/src/composer/TiptapComposer.voice-toolbar.spec.tsx +24 -1
@@ -2268,18 +2268,21 @@ export const AgentActivityGroup = memo(function AgentActivityGroup({
2268
2268
  messageId,
2269
2269
  foldedReasoningMessageId,
2270
2270
  summarizesTurn = false,
2271
+ part,
2271
2272
  }: {
2272
- runId?: RunId;
2273
- afterSequence?: number;
2274
- throughSequence?: number;
2275
- excludeAgentActivities?: boolean;
2276
- isCurrentSegment?: boolean;
2273
+ runId?: RunId | undefined;
2274
+ afterSequence?: number | undefined;
2275
+ throughSequence?: number | undefined;
2276
+ excludeAgentActivities?: boolean | undefined;
2277
+ isCurrentSegment?: boolean | undefined;
2277
2278
  /** The reply this work segment precedes. */
2278
- messageId?: string;
2279
+ messageId?: string | undefined;
2279
2280
  /** A saved reply whose reasoning before its answer joins this group. */
2280
- foldedReasoningMessageId?: string;
2281
+ foldedReasoningMessageId?: string | undefined;
2281
2282
  /** This segment is its run's whole turn, so it shows a row with no work. */
2282
- summarizesTurn?: boolean;
2283
+ summarizesTurn?: boolean | undefined;
2284
+ /** Only the steps' result cards, or only the work row; both by default. */
2285
+ part?: "results" | "row" | undefined;
2283
2286
  }) {
2284
2287
  const {
2285
2288
  threadId,
@@ -2598,6 +2601,15 @@ export const AgentActivityGroup = memo(function AgentActivityGroup({
2598
2601
  minute: labels.durationMinuteShort,
2599
2602
  second: labels.durationSecondShort,
2600
2603
  });
2604
+ const resultCards =
2605
+ durableToolResults.length && part !== "row" ? (
2606
+ <div data-agentkit-tool-results="true">
2607
+ {durableToolResults.map(({ tool, Renderer }) => (
2608
+ <Renderer key={tool.id} value={tool} threadId={threadId} />
2609
+ ))}
2610
+ </div>
2611
+ ) : null;
2612
+ if (part === "results") return resultCards;
2601
2613
  if (
2602
2614
  activityItems.length === 0 &&
2603
2615
  durableToolResults.length === 0 &&
@@ -2732,13 +2744,7 @@ export const AgentActivityGroup = memo(function AgentActivityGroup({
2732
2744
  };
2733
2745
  return (
2734
2746
  <>
2735
- {durableToolResults.length ? (
2736
- <div data-agentkit-tool-results="true">
2737
- {durableToolResults.map(({ tool, Renderer }) => (
2738
- <Renderer key={tool.id} value={tool} threadId={threadId} />
2739
- ))}
2740
- </div>
2741
- ) : null}
2747
+ {resultCards}
2742
2748
  {hasExpandableActivity || activelyWorking ? (
2743
2749
  <AgentWorkDisclosure
2744
2750
  label={baseSummaryLabel}
@@ -3353,7 +3359,13 @@ function AgentWorkDisclosure({
3353
3359
  children: ReactNode;
3354
3360
  }) {
3355
3361
  const { slots, labels, workedForRowLayout } = useAgentKit();
3356
- const [open, setOpen] = useState(false);
3362
+ const row = useContext(AgentWorkRowOpenContext);
3363
+ const [open, setOpen] = useState(() => row?.opened.has(row.key) ?? false);
3364
+ useEffect(() => {
3365
+ if (!row) return;
3366
+ if (open) row.opened.add(row.key);
3367
+ else row.opened.delete(row.key);
3368
+ }, [open, row]);
3357
3369
  const contentId = useId();
3358
3370
  const Summary = slots.workedForSummary;
3359
3371
  const failed =
@@ -3368,9 +3380,7 @@ function AgentWorkDisclosure({
3368
3380
  ? failed.split("{{summary}}")
3369
3381
  : ["", ""];
3370
3382
  const fullLabel = failed ? failed.replace("{{summary}}", () => label) : label;
3371
- const footer = useAgentMessageWorkHost(running ? undefined : messageId);
3372
- const footerHost = footer?.element ?? null;
3373
- const handoff = useAgentWorkHandoff(running, footer, setOpen);
3383
+ const footerHost = useAgentMessageWorkHost(messageId);
3374
3384
  const summaryLabel = label ? (
3375
3385
  <span className="agentkit-activities-label">
3376
3386
  {failedBefore || failedAfter ? (
@@ -3403,12 +3413,10 @@ function AgentWorkDisclosure({
3403
3413
  // work with nothing to expand is text, not a disabled button.
3404
3414
  const disclosure = (
3405
3415
  <div
3406
- ref={handoff.inPlaceRef}
3407
3416
  className="agentkit-activities"
3408
3417
  data-running={running ? "true" : undefined}
3409
3418
  data-expandable={expandable ? "true" : "false"}
3410
3419
  data-open={open ? "true" : "false"}
3411
- data-handoff={handoff.leavingHeight ? "enter" : undefined}
3412
3420
  >
3413
3421
  <AgentKitDisclosureSummary
3414
3422
  as={expandable || running ? "button" : "static"}
@@ -3437,92 +3445,9 @@ function AgentWorkDisclosure({
3437
3445
  </AgentKitDisclosureContent>
3438
3446
  </div>
3439
3447
  );
3440
- // Finished work joins its reply's controls under the reply; live work stays
3441
- // above it, where it is happening. On a live finish the space it left above
3442
- // the reply closes over the same beat as the row fades in below, so the
3443
- // reply glides up instead of jumping.
3444
- return (
3445
- <>
3446
- {handoff.leavingHeight ? (
3447
- <div
3448
- className="agentkit-work-handoff"
3449
- data-agentkit-work-handoff="leave"
3450
- aria-hidden="true"
3451
- inert
3452
- style={
3453
- {
3454
- "--agentkit-work-handoff-size": `${handoff.leavingHeight}px`,
3455
- } as CSSProperties
3456
- }
3457
- >
3458
- <AgentKitDisclosureSummary
3459
- as="static"
3460
- className="agentkit-activities-summary"
3461
- rowClassName="agentkit-activities-row"
3462
- open={false}
3463
- onToggle={noop}
3464
- expandable={false}
3465
- style={workedForRowSummaryStyle(workedForRowLayout)}
3466
- icon={icon}
3467
- label={summaryLabel}
3468
- >
3469
- {summaryContent}
3470
- </AgentKitDisclosureSummary>
3471
- </div>
3472
- ) : null}
3473
- {footerHost ? createPortal(disclosure, footerHost) : disclosure}
3474
- </>
3475
- );
3476
- }
3477
-
3478
- const AGENT_WORK_HANDOFF_MS = 180;
3479
-
3480
- /**
3481
- * Tracks a work row's move from above its reply to the reply's footer. Only
3482
- * a finish seen live animates (the row ran, or its reply streamed): a saved
3483
- * thread renders the row in place for one unpainted pass before its host
3484
- * registers, and that must not read as a finish. A row opened to watch live
3485
- * work closes as it moves, so its steps never land below the reply.
3486
- */
3487
- function useAgentWorkHandoff(
3488
- running: boolean | undefined,
3489
- footer: AgentMessageWorkHost | null,
3490
- setOpen: (open: boolean) => void,
3491
- ) {
3492
- const prefersReducedMotion = usePrefersReducedMotion();
3493
- const footerHost = footer?.element ?? null;
3494
- const live = footer?.live ?? false;
3495
- const inPlaceRef = useRef<HTMLDivElement>(null);
3496
- const sawRunningRef = useRef(false);
3497
- const inPlaceHeightRef = useRef(0);
3498
- const previousHostRef = useRef<HTMLElement | null>(null);
3499
- const [leavingHeight, setLeavingHeight] = useState(0);
3500
- // Measured on every in-place commit, because once the row portals away
3501
- // its old box is gone. The placeholder keeps the row's margins from the
3502
- // stylesheet, so only the border box is carried over.
3503
- useLayoutEffect(() => {
3504
- if (running) sawRunningRef.current = true;
3505
- if (footerHost || !inPlaceRef.current) return;
3506
- inPlaceHeightRef.current = inPlaceRef.current.offsetHeight;
3507
- });
3508
- useLayoutEffect(() => {
3509
- const previousHost = previousHostRef.current;
3510
- previousHostRef.current = footerHost;
3511
- if (!footerHost || previousHost) return;
3512
- if (!live && !sawRunningRef.current) return;
3513
- setOpen(false);
3514
- if (prefersReducedMotion || inPlaceHeightRef.current <= 0) return;
3515
- setLeavingHeight(inPlaceHeightRef.current);
3516
- }, [footerHost, live, prefersReducedMotion, setOpen]);
3517
- useEffect(() => {
3518
- if (!leavingHeight) return;
3519
- const timer = setTimeout(
3520
- () => setLeavingHeight(0),
3521
- AGENT_WORK_HANDOFF_MS + 60,
3522
- );
3523
- return () => clearTimeout(timer);
3524
- }, [leavingHeight]);
3525
- return { inPlaceRef, leavingHeight: footerHost ? leavingHeight : 0 };
3448
+ // A reply's work sits on its footer line, live or finished, so the finish
3449
+ // never moves it.
3450
+ return footerHost ? createPortal(disclosure, footerHost) : disclosure;
3526
3451
  }
3527
3452
 
3528
3453
  function AgentReasoningPartDisclosure({
@@ -4903,38 +4828,29 @@ function selectedRunModel(
4903
4828
  /**
4904
4829
  * A finished reply's footer offers a host under its message id, and the
4905
4830
  * reply's work row portals into it so it shares the line with the controls.
4906
- * `live` says the footer saw its reply stream, so the row arriving is a
4907
- * finish the reader watched, not a saved reply loading.
4908
4831
  */
4909
- interface AgentMessageWorkHost {
4910
- element: HTMLElement;
4911
- live: boolean;
4912
- }
4913
-
4914
4832
  interface AgentMessageWorkHosts {
4915
- get: (messageId: string) => AgentMessageWorkHost | undefined;
4916
- set: (messageId: string, host: AgentMessageWorkHost) => void;
4833
+ get: (messageId: string) => HTMLElement | undefined;
4834
+ set: (messageId: string, element: HTMLElement) => void;
4917
4835
  release: (messageId: string, element: HTMLElement) => void;
4918
4836
  subscribe: (listener: () => void) => () => void;
4919
4837
  }
4920
4838
 
4921
4839
  function createAgentMessageWorkHosts(): AgentMessageWorkHosts {
4922
- const hosts = new Map<string, AgentMessageWorkHost>();
4840
+ const hosts = new Map<string, HTMLElement>();
4923
4841
  const listeners = new Set<() => void>();
4924
4842
  const notify = () => {
4925
4843
  for (const listener of listeners) listener();
4926
4844
  };
4927
4845
  return {
4928
4846
  get: (messageId) => hosts.get(messageId),
4929
- set(messageId, host) {
4930
- const current = hosts.get(messageId);
4931
- if (current?.element === host.element && current.live === host.live)
4932
- return;
4933
- hosts.set(messageId, host);
4847
+ set(messageId, element) {
4848
+ if (hosts.get(messageId) === element) return;
4849
+ hosts.set(messageId, element);
4934
4850
  notify();
4935
4851
  },
4936
4852
  release(messageId, element) {
4937
- if (hosts.get(messageId)?.element !== element) return;
4853
+ if (hosts.get(messageId) !== element) return;
4938
4854
  hosts.delete(messageId);
4939
4855
  notify();
4940
4856
  },
@@ -4945,6 +4861,16 @@ function createAgentMessageWorkHosts(): AgentMessageWorkHosts {
4945
4861
  };
4946
4862
  }
4947
4863
 
4864
+ /**
4865
+ * The work rows the reader opened, by run and segment. A segment's row
4866
+ * remounts when narration hides it and again when the run finishes, and an
4867
+ * opened row comes back open.
4868
+ */
4869
+ const AgentWorkRowOpenContext = createContext<{
4870
+ key: string;
4871
+ opened: Set<string>;
4872
+ } | null>(null);
4873
+
4948
4874
  const AgentMessageWorkHostsContext =
4949
4875
  createContext<AgentMessageWorkHosts | null>(null);
4950
4876
 
@@ -4954,7 +4880,7 @@ function noopSubscribe() {
4954
4880
 
4955
4881
  function useAgentMessageWorkHost(
4956
4882
  messageId: string | undefined,
4957
- ): AgentMessageWorkHost | null {
4883
+ ): HTMLElement | null {
4958
4884
  const hosts = useContext(AgentMessageWorkHostsContext);
4959
4885
  return useSyncExternalStore(
4960
4886
  hosts?.subscribe ?? noopSubscribe,
@@ -4963,23 +4889,17 @@ function useAgentMessageWorkHost(
4963
4889
  );
4964
4890
  }
4965
4891
 
4966
- /**
4967
- * A callback ref that offers its element as a reply's work row host once
4968
- * the reply stops streaming.
4969
- */
4892
+ /** A callback ref that offers its element as a reply's work row host. */
4970
4893
  function useAgentMessageWorkHostRef(
4971
4894
  messageId: string | undefined,
4972
- streaming: boolean,
4973
4895
  ): (element: HTMLElement | null) => void {
4974
4896
  const hosts = useContext(AgentMessageWorkHostsContext);
4975
4897
  const [element, setElement] = useState<HTMLElement | null>(null);
4976
- const sawStreamingRef = useRef(false);
4977
4898
  useLayoutEffect(() => {
4978
- if (streaming) sawStreamingRef.current = true;
4979
- if (!hosts || !messageId || !element || streaming) return;
4980
- hosts.set(messageId, { element, live: sawStreamingRef.current });
4899
+ if (!hosts || !messageId || !element) return;
4900
+ hosts.set(messageId, element);
4981
4901
  return () => hosts.release(messageId, element);
4982
- }, [element, hosts, messageId, streaming]);
4902
+ }, [element, hosts, messageId]);
4983
4903
  return setElement;
4984
4904
  }
4985
4905
 
@@ -5376,10 +5296,9 @@ export const AgentMessageActions = memo(function AgentMessageActions({
5376
5296
  const { messageSeq, requestId, runId } = position;
5377
5297
  const isAssistant = message.role === "assistant";
5378
5298
  const isLastAssistantMessage = isAssistant && position.isLast;
5379
- // A streaming reply keeps its work above it until the answer is complete.
5299
+ // The reply's work row sits on this footer line, live or finished.
5380
5300
  const workHostRef = useAgentMessageWorkHostRef(
5381
5301
  isAssistant ? message.id : undefined,
5382
- message.status === "streaming",
5383
5302
  );
5384
5303
  const [copied, setCopied] = useState(false);
5385
5304
  const [feedback, setFeedback] = useState<"positive" | "negative" | null>(
@@ -6166,7 +6085,7 @@ export function AgentRunFailure({
6166
6085
  </ActionButton>
6167
6086
  </div>
6168
6087
  ) : null}
6169
- {continueFailed ? (
6088
+ {continueFailed && !superseded ? (
6170
6089
  <span className="agentkit-command-error" role="alert">
6171
6090
  {labels.continueRunUnavailable}
6172
6091
  </span>
@@ -7801,6 +7720,7 @@ export function AgentKitChat({
7801
7720
  [editingMessage, messageEditEnabled, setEditingMessage],
7802
7721
  );
7803
7722
  const [messageWorkHosts] = useState(createAgentMessageWorkHosts);
7723
+ const [openedWorkRows] = useState(() => new Set<string>());
7804
7724
  const hasConversation =
7805
7725
  thread.messages.length > 0 ||
7806
7726
  pendingSubmissions.length > 0 ||
@@ -8475,6 +8395,12 @@ export function AgentKitChat({
8475
8395
  return messageId ? [messageId] : [];
8476
8396
  }),
8477
8397
  );
8398
+ // A saved reply's reasoning before its answer joins the work group above
8399
+ // it, so the turn shows one disclosure; the stream did not place it there.
8400
+ const foldsLeadingReasoning = (message: AgentMessage) =>
8401
+ message.status !== "streaming" &&
8402
+ !groupedReasoningMessageIds.has(message.id) &&
8403
+ leadingReasoningMessage(message) !== undefined;
8478
8404
  const hasRunWork = (
8479
8405
  runId: RunId,
8480
8406
  afterSequence?: number,
@@ -8487,7 +8413,7 @@ export function AgentKitChat({
8487
8413
  );
8488
8414
  // A message the user sent into a running turn ends the run's work so far,
8489
8415
  // even when no reply was written before it.
8490
- const { runSegmentEnds, steerSequences } = useMemo(() => {
8416
+ const { runSegmentEnds, runSegmentStarts, steerSequences } = useMemo(() => {
8491
8417
  const deliveredAt = new Map<string, number>();
8492
8418
  for (const event of thread.events) {
8493
8419
  if (
@@ -8499,15 +8425,19 @@ export function AgentKitChat({
8499
8425
  }
8500
8426
  }
8501
8427
  const ends = new Map<RunId, { id: string; sequence: number }>();
8428
+ const starts = new Map<RunId, { id: string; sequence: number }>();
8502
8429
  const steers = new Map<string, number>();
8430
+ const endSegment = (runId: RunId, id: string, sequence: number) => {
8431
+ const previous = ends.get(runId);
8432
+ if (previous) starts.set(runId, previous);
8433
+ ends.set(runId, { id, sequence });
8434
+ };
8503
8435
  for (const message of thread.messages) {
8504
8436
  const runId = messageRunIds.get(message.id);
8505
8437
  if (!runId) continue;
8506
8438
  if (message.role === "assistant") {
8507
8439
  const sequence = messageBoundarySequences.get(message.id);
8508
- if (sequence !== undefined) {
8509
- ends.set(runId, { id: message.id, sequence });
8510
- }
8440
+ if (sequence !== undefined) endSegment(runId, message.id, sequence);
8511
8441
  continue;
8512
8442
  }
8513
8443
  const sequence =
@@ -8524,9 +8454,13 @@ export function AgentKitChat({
8524
8454
  );
8525
8455
  if (!workBefore) continue;
8526
8456
  steers.set(message.id, sequence);
8527
- ends.set(runId, { id: message.id, sequence });
8457
+ endSegment(runId, message.id, sequence);
8528
8458
  }
8529
- return { runSegmentEnds: ends, steerSequences: steers };
8459
+ return {
8460
+ runSegmentEnds: ends,
8461
+ runSegmentStarts: starts,
8462
+ steerSequences: steers,
8463
+ };
8530
8464
  }, [
8531
8465
  messageBoundarySequences,
8532
8466
  messageRunIds,
@@ -8564,19 +8498,40 @@ export function AgentKitChat({
8564
8498
  ]),
8565
8499
  );
8566
8500
  const pendingRuns = new Set(pendingRunIds);
8567
- // A running turn's work after its reply's latest text joins the row above
8568
- // that reply. As its own row below, the work before reads as finished while
8569
- // the run is still working, and the turn shows two rows until it ends.
8570
- const liveReplyRunIds = new Set(
8571
- pendingRunIds.filter((runId) => {
8572
- const reply = lastAssistantMessagesByRun.get(runId);
8573
- return (
8574
- thread.runs[runId]?.status === "running" &&
8575
- reply !== undefined &&
8576
- runSegmentEnds.get(runId)?.id === reply.id
8577
- );
8578
- }),
8579
- );
8501
+ // While a run works on its reply, the reply's work is one row below its
8502
+ // latest text, shown while work runs there: above the reply, narration
8503
+ // pushes it out of view, and split at that text, the work before reads as
8504
+ // finished while the run works. Result cards stay above the reply, where
8505
+ // they sit once it finishes. A finished reply with nothing running after
8506
+ // it keeps its row in its footer.
8507
+ const liveReplies = new Map<
8508
+ RunId,
8509
+ {
8510
+ replyId: string;
8511
+ start: { id: string; sequence: number } | undefined;
8512
+ foldsReasoning: boolean;
8513
+ }
8514
+ >();
8515
+ for (const [runId, reply] of lastAssistantMessagesByRun) {
8516
+ if (
8517
+ thread.runs[runId]?.status !== "running" ||
8518
+ runSegmentEnds.get(runId)?.id !== reply.id
8519
+ ) {
8520
+ continue;
8521
+ }
8522
+ const start = runSegmentStarts.get(runId);
8523
+ const message = thread.messages.find(({ id }) => id === reply.id);
8524
+ if (
8525
+ (message?.status === "streaming" || pendingRuns.has(runId)) &&
8526
+ hasRunWork(runId, start?.sequence)
8527
+ ) {
8528
+ liveReplies.set(runId, {
8529
+ replyId: reply.id,
8530
+ start,
8531
+ foldsReasoning: message ? foldsLeadingReasoning(message) : false,
8532
+ });
8533
+ }
8534
+ }
8580
8535
  const runRevision = (runId: RunId) =>
8581
8536
  `${runId}:${eventIndex.byRun.get(runId)?.length ?? 0}`;
8582
8537
  const renderRunFailure = (runId: RunId) => {
@@ -8630,6 +8585,7 @@ export function AgentKitChat({
8630
8585
  messageId,
8631
8586
  foldedReasoningMessageId,
8632
8587
  summarizesTurn,
8588
+ part,
8633
8589
  }: {
8634
8590
  runId: RunId;
8635
8591
  anchor: string;
@@ -8638,8 +8594,10 @@ export function AgentKitChat({
8638
8594
  messageId?: string | undefined;
8639
8595
  foldedReasoningMessageId?: string | undefined;
8640
8596
  summarizesTurn?: boolean | undefined;
8597
+ part?: "results" | "row" | undefined;
8641
8598
  }) => {
8642
- const key = `run-work:${threadId}:${runId}:${anchor}`;
8599
+ const openKey = `${threadId}:${runId}:${anchor}`;
8600
+ const key = `run-work:${openKey}${part === "row" ? ":row" : ""}`;
8643
8601
  const resetKey = `${anchor}:${runRevision(runId)}`;
8644
8602
  return cachedRowElement(
8645
8603
  key,
@@ -8650,10 +8608,12 @@ export function AgentKitChat({
8650
8608
  messageId,
8651
8609
  foldedReasoningMessageId,
8652
8610
  summarizesTurn,
8611
+ part,
8653
8612
  ],
8654
8613
  () =>
8655
8614
  renderRunWorkElement({
8656
8615
  key,
8616
+ openKey,
8657
8617
  resetKey,
8658
8618
  runId,
8659
8619
  afterSequence,
@@ -8661,11 +8621,13 @@ export function AgentKitChat({
8661
8621
  messageId,
8662
8622
  foldedReasoningMessageId,
8663
8623
  summarizesTurn,
8624
+ part,
8664
8625
  }),
8665
8626
  );
8666
8627
  };
8667
8628
  const renderRunWorkElement = ({
8668
8629
  key,
8630
+ openKey,
8669
8631
  resetKey,
8670
8632
  runId,
8671
8633
  afterSequence,
@@ -8673,8 +8635,10 @@ export function AgentKitChat({
8673
8635
  messageId,
8674
8636
  foldedReasoningMessageId,
8675
8637
  summarizesTurn,
8638
+ part,
8676
8639
  }: {
8677
8640
  key: string;
8641
+ openKey: string;
8678
8642
  resetKey: string;
8679
8643
  runId: RunId;
8680
8644
  afterSequence?: number | undefined;
@@ -8682,33 +8646,61 @@ export function AgentKitChat({
8682
8646
  messageId?: string | undefined;
8683
8647
  foldedReasoningMessageId?: string | undefined;
8684
8648
  summarizesTurn?: boolean | undefined;
8649
+ part?: "results" | "row" | undefined;
8685
8650
  }) => (
8686
8651
  <AgentKitSurfaceBoundary key={key} surface="activity" resetKey={resetKey}>
8687
- <AgentCollaborationFeed
8688
- runId={runId}
8689
- afterSequence={afterSequence}
8690
- throughSequence={throughSequence}
8691
- />
8692
- <AgentTaskGroup
8693
- runId={runId}
8694
- afterSequence={afterSequence}
8695
- throughSequence={throughSequence}
8696
- />
8697
- <AgentActivityGroup
8698
- runId={runId}
8699
- afterSequence={afterSequence}
8700
- throughSequence={throughSequence}
8701
- excludeAgentActivities
8702
- isCurrentSegment={throughSequence === undefined}
8703
- messageId={messageId}
8704
- foldedReasoningMessageId={foldedReasoningMessageId}
8705
- summarizesTurn={summarizesTurn}
8706
- />
8652
+ <AgentWorkRowOpenContext.Provider
8653
+ value={{ key: openKey, opened: openedWorkRows }}
8654
+ >
8655
+ {part === "row" ? null : (
8656
+ <>
8657
+ <AgentCollaborationFeed
8658
+ runId={runId}
8659
+ afterSequence={afterSequence}
8660
+ throughSequence={throughSequence}
8661
+ />
8662
+ <AgentTaskGroup
8663
+ runId={runId}
8664
+ afterSequence={afterSequence}
8665
+ throughSequence={throughSequence}
8666
+ />
8667
+ </>
8668
+ )}
8669
+ <AgentActivityGroup
8670
+ runId={runId}
8671
+ afterSequence={afterSequence}
8672
+ throughSequence={throughSequence}
8673
+ excludeAgentActivities
8674
+ isCurrentSegment={throughSequence === undefined}
8675
+ messageId={messageId}
8676
+ foldedReasoningMessageId={foldedReasoningMessageId}
8677
+ summarizesTurn={summarizesTurn}
8678
+ part={part}
8679
+ />
8680
+ </AgentWorkRowOpenContext.Provider>
8707
8681
  </AgentKitSurfaceBoundary>
8708
8682
  );
8709
8683
  const renderRunTail = (runId: RunId) => {
8710
8684
  const items: ReactNode[] = [];
8711
- if (pendingRuns.has(runId)) {
8685
+ const liveReply = liveReplies.get(runId);
8686
+ if (liveReply) {
8687
+ // While the reply's text is the newest output the text is the focus,
8688
+ // so the row shows only while work runs after it.
8689
+ if (pendingRuns.has(runId)) {
8690
+ items.push(
8691
+ renderRunWork({
8692
+ runId,
8693
+ anchor: liveReply.start?.id ?? "start",
8694
+ afterSequence: liveReply.start?.sequence,
8695
+ messageId: liveReply.replyId,
8696
+ foldedReasoningMessageId: liveReply.foldsReasoning
8697
+ ? liveReply.replyId
8698
+ : undefined,
8699
+ part: "row",
8700
+ }),
8701
+ );
8702
+ }
8703
+ } else if (pendingRuns.has(runId)) {
8712
8704
  const boundary = runSegmentEnds.get(runId);
8713
8705
  items.push(
8714
8706
  renderRunWork({
@@ -8752,7 +8744,8 @@ export function AgentKitChat({
8752
8744
  }
8753
8745
  const tailRunIds = Array.from(
8754
8746
  new Set([
8755
- ...pendingRunIds.filter((runId) => !liveReplyRunIds.has(runId)),
8747
+ ...pendingRunIds,
8748
+ ...liveReplies.keys(),
8756
8749
  ...Array.from(failedRuns.keys()).filter(
8757
8750
  (runId) => !lastAssistantMessagesByRun.has(runId),
8758
8751
  ),
@@ -8886,10 +8879,8 @@ export function AgentKitChat({
8886
8879
  !hasRunWork(runId, sequence) &&
8887
8880
  !hasVisibleReasoning(message),
8888
8881
  );
8889
- const extendsLiveWork =
8890
- isAssistantBoundary &&
8891
- liveReplyRunIds.has(runId) &&
8892
- lastAssistantMessagesByRun.get(runId)?.id === message.id;
8882
+ const isLiveReply =
8883
+ isAssistantBoundary && liveReplies.get(runId)?.replyId === message.id;
8893
8884
  const rendersRunWork = Boolean(
8894
8885
  isAssistantBoundary &&
8895
8886
  runId &&
@@ -8897,26 +8888,21 @@ export function AgentKitChat({
8897
8888
  hasRunWork(
8898
8889
  runId,
8899
8890
  previousAssistant?.sequence,
8900
- extendsLiveWork ? undefined : sequence,
8891
+ isLiveReply ? undefined : sequence,
8901
8892
  )),
8902
8893
  );
8903
- // A saved reply's reasoning before its answer joins the work group above
8904
- // it, so the turn shows one disclosure; the stream did not place it there.
8905
- const foldsReasoning =
8906
- rendersRunWork &&
8907
- message.status !== "streaming" &&
8908
- !groupedReasoningMessageIds.has(message.id) &&
8909
- leadingReasoningMessage(message) !== undefined;
8894
+ const foldsReasoning = rendersRunWork && foldsLeadingReasoning(message);
8910
8895
  if (rendersRunWork && runId) {
8911
8896
  transcriptItems.push(
8912
8897
  renderRunWork({
8913
8898
  runId,
8914
8899
  anchor: previousAssistant?.id ?? "start",
8915
8900
  afterSequence: previousAssistant?.sequence,
8916
- throughSequence: extendsLiveWork ? undefined : sequence,
8901
+ throughSequence: isLiveReply ? undefined : sequence,
8917
8902
  messageId: message.id,
8918
8903
  foldedReasoningMessageId: foldsReasoning ? message.id : undefined,
8919
8904
  summarizesTurn,
8905
+ part: isLiveReply ? "results" : undefined,
8920
8906
  }),
8921
8907
  );
8922
8908
  }
@@ -1093,21 +1093,9 @@ describe("AgentChat lifecycle", () => {
1093
1093
  ?.querySelector(".agentkit-message-actions")
1094
1094
  ?.getAttribute("data-streaming"),
1095
1095
  ).toBe("true");
1096
- expect(runWorkAfter).not.toBeNull();
1097
- expect(
1098
- runWorkAfter?.querySelector(".agentkit-activities-label")?.textContent,
1099
- ).toBe("Worked for 4s");
1100
- expect(runWorkAfter?.textContent).not.toContain("Thinking");
1101
- expect(
1102
- runWorkAfter?.querySelector("[data-agentkit-current-activity]"),
1103
- ).toBeNull();
1104
- expect(runWorkAfter?.querySelector('[data-status="running"]')).toBeNull();
1105
- expect(
1106
- runWorkAfter && response
1107
- ? runWorkAfter.compareDocumentPosition(response) &
1108
- Node.DOCUMENT_POSITION_FOLLOWING
1109
- : 0,
1110
- ).not.toBe(0);
1096
+ // oracle-change: while the reply streams after the run's work, the text is the focus, so no work row shows until the run finishes.
1097
+ expect(runWorkAfter).toBeNull();
1098
+ expect(tree.container.textContent).not.toContain("Thinking");
1111
1099
 
1112
1100
  observable.update({
1113
1101
  connection: "connected",
@@ -1566,7 +1554,8 @@ describe("AgentChat lifecycle", () => {
1566
1554
  expect(
1567
1555
  tree.container.querySelector("[data-visible-empty-widget]")?.textContent,
1568
1556
  ).toBe("Rendered widget");
1569
- expect(tree.container.textContent).toContain("Reading components.tsx");
1557
+ // oracle-change: the streaming reply is the focus, so its finished step waits for the footer row instead of a live row.
1558
+ expect(tree.container.querySelector(".agentkit-activities")).toBeNull();
1570
1559
  expect(
1571
1560
  tree.container.querySelector("[data-agentkit-current-activity]"),
1572
1561
  ).toBeNull();
@@ -75,7 +75,7 @@ describe("useAgentKitStopButton", () => {
75
75
  );
76
76
  });
77
77
 
78
- it("steps down to secondary only while Send sits beside it", async () => {
78
+ it("steps down to secondary only while End voice chat sits beside it", async () => {
79
79
  mocks.activeRunIds = ["run-1"];
80
80
  container = document.createElement("div");
81
81
  document.body.appendChild(container);