@adecore/agents-react 0.44.3 → 0.45.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.
@@ -1570,133 +1570,166 @@ function _temp4(tool) {
1570
1570
  return tool.state === "error";
1571
1571
  }
1572
1572
  export function TurnFoldRow(t0) {
1573
- const $ = _c(31);
1573
+ const $ = _c(39);
1574
1574
  const {
1575
1575
  turn,
1576
1576
  label,
1577
1577
  work,
1578
1578
  expanded,
1579
+ expandable,
1579
1580
  onToggle
1580
1581
  } = t0;
1581
1582
  const {
1582
1583
  commandLooks
1583
1584
  } = useChatHost();
1585
+ let className;
1584
1586
  let t1;
1585
1587
  let t2;
1586
1588
  let t3;
1587
- let t4;
1588
- let t5;
1589
- let t6;
1590
- if ($[0] !== commandLooks || $[1] !== expanded || $[2] !== label || $[3] !== onToggle || $[4] !== turn.state || $[5] !== work) {
1589
+ if ($[0] !== commandLooks || $[1] !== expandable || $[2] !== expanded || $[3] !== label || $[4] !== turn.state || $[5] !== work) {
1591
1590
  const brands = commandLooks?.flatMap(_temp5) ?? [];
1592
- const t7 = turn.state === "error" && "text-status-error";
1593
- if ($[12] !== t7) {
1594
- t1 = clsx("group group/line -mx-1 mb-2 flex h-7 w-[calc(100%+8px)] min-w-0 items-center gap-2 rounded-md px-1 text-left text-xs text-text-muted hover:bg-surface-hover", t7);
1595
- $[12] = t7;
1596
- $[13] = t1;
1591
+ const t4 = expandable && "hover:bg-surface-hover";
1592
+ const t5 = turn.state === "error" && "text-status-error";
1593
+ let t6;
1594
+ if ($[10] !== t4 || $[11] !== t5) {
1595
+ t6 = clsx("group group/line -mx-1 mb-2 flex h-7 w-[calc(100%+8px)] min-w-0 items-center gap-2 rounded-md px-1 text-left text-xs text-text-muted", t4, t5);
1596
+ $[10] = t4;
1597
+ $[11] = t5;
1598
+ $[12] = t6;
1597
1599
  } else {
1598
- t1 = $[13];
1600
+ t6 = $[12];
1599
1601
  }
1600
- t2 = expanded;
1601
- t3 = onToggle;
1602
- const t8 = expanded && "rotate-90";
1603
- let t9;
1604
- if ($[14] !== t8) {
1605
- t9 = clsx("transition-transform", t8);
1606
- $[14] = t8;
1607
- $[15] = t9;
1602
+ className = t6;
1603
+ let t7;
1604
+ if ($[13] !== expandable || $[14] !== expanded) {
1605
+ t7 = expandable && /*#__PURE__*/_jsx(Icon, {
1606
+ icon: ChevronRight,
1607
+ size: 12,
1608
+ className: clsx("transition-transform", expanded && "rotate-90")
1609
+ });
1610
+ $[13] = expandable;
1611
+ $[14] = expanded;
1612
+ $[15] = t7;
1608
1613
  } else {
1609
- t9 = $[15];
1614
+ t7 = $[15];
1610
1615
  }
1611
- if ($[16] !== t9) {
1612
- t4 = /*#__PURE__*/_jsx("span", {
1616
+ if ($[16] !== t7) {
1617
+ t1 = /*#__PURE__*/_jsx("span", {
1613
1618
  className: ROW_GUTTER,
1614
- children: /*#__PURE__*/_jsx(Icon, {
1615
- icon: ChevronRight,
1616
- size: 12,
1617
- className: t9
1618
- })
1619
+ children: t7
1619
1620
  });
1620
- $[16] = t9;
1621
- $[17] = t4;
1621
+ $[16] = t7;
1622
+ $[17] = t1;
1622
1623
  } else {
1623
- t4 = $[17];
1624
+ t1 = $[17];
1624
1625
  }
1625
- if ($[18] !== label) {
1626
- t5 = /*#__PURE__*/_jsx("span", {
1627
- className: "shrink-0 group-hover:text-text",
1626
+ const t8 = expandable && "group-hover:text-text";
1627
+ let t9;
1628
+ if ($[18] !== t8) {
1629
+ t9 = clsx("shrink-0", t8);
1630
+ $[18] = t8;
1631
+ $[19] = t9;
1632
+ } else {
1633
+ t9 = $[19];
1634
+ }
1635
+ if ($[20] !== label || $[21] !== t9) {
1636
+ t2 = /*#__PURE__*/_jsx("span", {
1637
+ className: t9,
1628
1638
  children: label
1629
1639
  });
1630
- $[18] = label;
1631
- $[19] = t5;
1640
+ $[20] = label;
1641
+ $[21] = t9;
1642
+ $[22] = t2;
1632
1643
  } else {
1633
- t5 = $[19];
1644
+ t2 = $[22];
1634
1645
  }
1635
- t6 = work.length > 0 && /*#__PURE__*/_jsx("span", {
1646
+ t3 = work.length > 0 && /*#__PURE__*/_jsx("span", {
1636
1647
  className: "min-w-0 truncate text-text-faint",
1637
1648
  children: work.map((part, index) => /*#__PURE__*/_jsxs(Fragment, {
1638
1649
  children: [" \xB7 ", withBrand(part, brands)]
1639
1650
  }, index))
1640
1651
  });
1641
1652
  $[0] = commandLooks;
1642
- $[1] = expanded;
1643
- $[2] = label;
1644
- $[3] = onToggle;
1653
+ $[1] = expandable;
1654
+ $[2] = expanded;
1655
+ $[3] = label;
1645
1656
  $[4] = turn.state;
1646
1657
  $[5] = work;
1647
- $[6] = t1;
1648
- $[7] = t2;
1649
- $[8] = t3;
1650
- $[9] = t4;
1651
- $[10] = t5;
1652
- $[11] = t6;
1658
+ $[6] = className;
1659
+ $[7] = t1;
1660
+ $[8] = t2;
1661
+ $[9] = t3;
1653
1662
  } else {
1654
- t1 = $[6];
1655
- t2 = $[7];
1656
- t3 = $[8];
1657
- t4 = $[9];
1658
- t5 = $[10];
1659
- t6 = $[11];
1663
+ className = $[6];
1664
+ t1 = $[7];
1665
+ t2 = $[8];
1666
+ t3 = $[9];
1660
1667
  }
1661
- let t7;
1662
- if ($[20] === Symbol.for("react.memo_cache_sentinel")) {
1663
- t7 = /*#__PURE__*/_jsx("span", {
1668
+ let t4;
1669
+ if ($[23] === Symbol.for("react.memo_cache_sentinel")) {
1670
+ t4 = /*#__PURE__*/_jsx("span", {
1664
1671
  className: "grow"
1665
1672
  });
1666
- $[20] = t7;
1673
+ $[23] = t4;
1667
1674
  } else {
1668
- t7 = $[20];
1675
+ t4 = $[23];
1669
1676
  }
1670
- let t8;
1671
- if ($[21] !== turn.createdAt) {
1672
- t8 = /*#__PURE__*/_jsx(RowTime, {
1677
+ let t5;
1678
+ if ($[24] !== turn.createdAt) {
1679
+ t5 = /*#__PURE__*/_jsx(RowTime, {
1673
1680
  at: turn.createdAt
1674
1681
  });
1675
- $[21] = turn.createdAt;
1676
- $[22] = t8;
1682
+ $[24] = turn.createdAt;
1683
+ $[25] = t5;
1677
1684
  } else {
1678
- t8 = $[22];
1685
+ t5 = $[25];
1679
1686
  }
1680
- let t9;
1681
- if ($[23] !== t1 || $[24] !== t2 || $[25] !== t3 || $[26] !== t4 || $[27] !== t5 || $[28] !== t6 || $[29] !== t8) {
1682
- t9 = /*#__PURE__*/_jsxs("button", {
1683
- className: t1,
1684
- "aria-expanded": t2,
1685
- onClick: t3,
1686
- children: [t4, t5, t6, t7, t8]
1687
- });
1688
- $[23] = t1;
1689
- $[24] = t2;
1690
- $[25] = t3;
1691
- $[26] = t4;
1692
- $[27] = t5;
1693
- $[28] = t6;
1694
- $[29] = t8;
1695
- $[30] = t9;
1696
- } else {
1697
- t9 = $[30];
1698
- }
1699
- return t9;
1687
+ let t6;
1688
+ if ($[26] !== t1 || $[27] !== t2 || $[28] !== t3 || $[29] !== t5) {
1689
+ t6 = /*#__PURE__*/_jsxs(_Fragment, {
1690
+ children: [t1, t2, t3, t4, t5]
1691
+ });
1692
+ $[26] = t1;
1693
+ $[27] = t2;
1694
+ $[28] = t3;
1695
+ $[29] = t5;
1696
+ $[30] = t6;
1697
+ } else {
1698
+ t6 = $[30];
1699
+ }
1700
+ const content = t6;
1701
+ if (!expandable) {
1702
+ let t7;
1703
+ if ($[31] !== className || $[32] !== content) {
1704
+ t7 = /*#__PURE__*/_jsx("div", {
1705
+ className: className,
1706
+ children: content
1707
+ });
1708
+ $[31] = className;
1709
+ $[32] = content;
1710
+ $[33] = t7;
1711
+ } else {
1712
+ t7 = $[33];
1713
+ }
1714
+ return t7;
1715
+ }
1716
+ let t7;
1717
+ if ($[34] !== className || $[35] !== content || $[36] !== expanded || $[37] !== onToggle) {
1718
+ t7 = /*#__PURE__*/_jsx("button", {
1719
+ className: className,
1720
+ "aria-expanded": expanded,
1721
+ onClick: onToggle,
1722
+ children: content
1723
+ });
1724
+ $[34] = className;
1725
+ $[35] = content;
1726
+ $[36] = expanded;
1727
+ $[37] = onToggle;
1728
+ $[38] = t7;
1729
+ } else {
1730
+ t7 = $[38];
1731
+ }
1732
+ return t7;
1700
1733
  }
1701
1734
  function _temp5(t0) {
1702
1735
  const {
@@ -12,6 +12,7 @@ export declare function useHostTask(...args: Parameters<ChatHost['tasks']['useTa
12
12
  export declare function useHostExtraPrompts(...args: Parameters<ChatHost['prompts']['useExtra']>): ReturnType<ChatHost['prompts']['useExtra']>;
13
13
  export declare function useHostStreaming(): ReturnType<ChatHost['useStreaming']>;
14
14
  export declare function useHostSendDelivery(): ReturnType<ChatHost['useSendDelivery']>;
15
+ export declare function useHostHidesToolCalls(...args: Parameters<ChatHost['useHidesToolCalls']>): ReturnType<ChatHost['useHidesToolCalls']>;
15
16
  export declare function useHostTimelineFind(...args: Parameters<ChatHost['useTimelineFind']>): ReturnType<ChatHost['useTimelineFind']>;
16
17
  export declare function useHostReferableChats(): ReturnType<ChatHost['useReferableChats']>;
17
18
  export declare function useHostReferables(scopeId: string, chatId: string): Referables;
@@ -26,6 +26,9 @@ export function useHostStreaming() {
26
26
  export function useHostSendDelivery() {
27
27
  return useChatHost().useSendDelivery();
28
28
  }
29
+ export function useHostHidesToolCalls(...args) {
30
+ return useChatHost().useHidesToolCalls(...args);
31
+ }
29
32
  export function useHostTimelineFind(...args) {
30
33
  return useChatHost().useTimelineFind(...args);
31
34
  }
package/dist/host.d.ts CHANGED
@@ -111,6 +111,12 @@ export interface ComposerSlotProps {
111
111
  disabled: boolean;
112
112
  insert(text: string): void;
113
113
  }
114
+ export interface ComposerHeaderSlotProps {
115
+ scopeId: string;
116
+ chatId: string;
117
+ disabled: boolean;
118
+ focused: boolean;
119
+ }
114
120
  export interface ComposerSendDraft {
115
121
  scopeId: string;
116
122
  chatId: string;
@@ -252,6 +258,7 @@ export interface ChatHost {
252
258
  };
253
259
  useStreaming(): 'words' | 'blocks' | 'whole';
254
260
  useSendDelivery(): 'queue' | 'steer';
261
+ useHidesToolCalls(scopeId: string, chatId: string): boolean;
255
262
  fileLinks: {
256
263
  target(text: string, cwd: string | null, scopeId?: string | null): FileRef | null;
257
264
  open(cwd: string | null, ref: FileRef, scopeId?: string | null): void;
@@ -306,6 +313,7 @@ export interface ChatHost {
306
313
  useResumeAtReset(scopeId: string): boolean;
307
314
  useComposerPlaceholder(scopeId: string, chatId: string): string | null;
308
315
  ComposerSlot: ComponentType<ComposerSlotProps> | null;
316
+ ComposerHeaderSlot?: ComponentType<ComposerHeaderSlotProps> | null;
309
317
  useComposerSendActions?(scopeId: string, chatId: string): readonly ComposerSendAction[];
310
318
  SubagentSlot: ComponentType<SubagentSlotProps> | null;
311
319
  useThreadCards(scopeId: string, chatId: string): readonly ThreadCard[];
package/dist/host.js CHANGED
@@ -49,6 +49,7 @@ const DEFAULT_HOST = {
49
49
  },
50
50
  useStreaming: () => 'words',
51
51
  useSendDelivery: () => 'queue',
52
+ useHidesToolCalls: () => false,
52
53
  fileLinks: null,
53
54
  useTimelineFind: () => NO_FIND,
54
55
  dictation: {
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { type ChatActions, type FileRef, type ShellCodeBlockContext, type ChatPlace, type SubagentTask, type TimelineFindOptions, type TimelineFind, type ComposerDictationProps, type ComposerSlotProps, type ComposerSendAction, type ComposerSendDraft, type SubagentSlotProps, type ThreadCard, type VisualHost, type HostMenuAction, type ReplyAuthor, type ChatHost, type UiMapSource, type UiModelViewProps, type CommandLook, type CommandLookMatch, type CommandName, ChatHostProvider, useChatHost, type AccentChoice, type ChatToast, type TextareaProps, type ResourceUrl } from './host.js';
1
+ export { type ChatActions, type FileRef, type ShellCodeBlockContext, type ChatPlace, type SubagentTask, type TimelineFindOptions, type TimelineFind, type ComposerDictationProps, type ComposerHeaderSlotProps, type ComposerSlotProps, type ComposerSendAction, type ComposerSendDraft, type SubagentSlotProps, type ThreadCard, type VisualHost, type HostMenuAction, type ReplyAuthor, type ChatHost, type UiMapSource, type UiModelViewProps, type CommandLook, type CommandLookMatch, type CommandName, ChatHostProvider, useChatHost, type AccentChoice, type ChatToast, type TextareaProps, type ResourceUrl } from './host.js';
2
2
  export { type ChatScope, ChatScopeContext, useChatScope } from './scope.js';
3
3
  export { type ChatRequestMap, type ChatEventMap, type ChatTransportStatus, type ChatTransport, ChatTransportError, errorCode, isConnectionError } from './transport.js';
4
4
  export { portTransport, type PortTransport } from './port-transport.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/agents-react",
3
- "version": "0.44.3",
3
+ "version": "0.45.0",
4
4
  "description": "React agent chat threads, composers, approvals, provider accounts and usage views.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -72,11 +72,11 @@
72
72
  "test:visuals": "bun --config=integration.bunfig.toml --conditions=source test ./src/chat/visual-viewport.integration.test.ts"
73
73
  },
74
74
  "dependencies": {
75
- "@adecore/agent-contracts": "0.44.3",
76
- "@adecore/editor": "0.44.3",
77
- "@adecore/editor-react": "0.44.3",
78
- "@adecore/intelligent-ui": "0.44.3",
79
- "@adecore/merge": "0.44.3",
75
+ "@adecore/agent-contracts": "0.45.0",
76
+ "@adecore/editor": "0.45.0",
77
+ "@adecore/editor-react": "0.45.0",
78
+ "@adecore/intelligent-ui": "0.45.0",
79
+ "@adecore/merge": "0.45.0",
80
80
  "@codemirror/commands": "^6.11.1",
81
81
  "@codemirror/language": "^6.13.1",
82
82
  "@codemirror/state": "^6.7.6",
@@ -100,14 +100,14 @@
100
100
  "zustand": "^5.0.16"
101
101
  },
102
102
  "peerDependencies": {
103
- "@adecore/ui": "^0.44.3",
103
+ "@adecore/ui": "^0.45.0",
104
104
  "i18next": "^26.4.2",
105
105
  "react": "^19.3.0",
106
106
  "react-dom": "^19.3.0",
107
107
  "react-i18next": "^17.0.15"
108
108
  },
109
109
  "devDependencies": {
110
- "@adecore/ui": "0.44.3",
110
+ "@adecore/ui": "0.45.0",
111
111
  "@types/bun": "^1.4.4",
112
112
  "@types/react": "^19.3.0",
113
113
  "@types/react-dom": "^19.3.0",
@@ -67,7 +67,7 @@ export type TimelineRow =
67
67
  // Set when `diff` holds back the diffs past its first few, which the host answers whole.
68
68
  heldBack?: boolean;
69
69
  }
70
- | { kind: 'turn-fold'; id: string; turn: ChatTurnItem; label: string; work: string[]; hiddenCount: number; expanded: boolean }
70
+ | { kind: 'turn-fold'; id: string; turn: ChatTurnItem; label: string; work: string[]; hiddenCount: number; expanded: boolean; expandable: boolean }
71
71
  | { kind: 'forks'; id: string; turnId: string }
72
72
  | { kind: 'working'; id: string; startedAt: number }
73
73
  // A card of the app's own (`thread-cards.ts`), never derived from an item.
@@ -84,6 +84,15 @@ interface TimelineOptions {
84
84
  forkedTurns?: ReadonlySet<string>;
85
85
  /* The host's programs drawn as tools of their own, whose runs read as their own sentences. */
86
86
  commandLooks?: readonly CommandLook[];
87
+ /* Leaves out the tool lines and their groups, while a settled turn still says how long it worked. */
88
+ hideTools?: boolean;
89
+ }
90
+
91
+ const TOOL_KINDS = new Set<TimelineRow['kind']>(['work', 'work-group', 'work-live']);
92
+
93
+ // A generated image is drawn as a tool call but reads as the turn's result, so it stays.
94
+ function shownRows(rows: TimelineRow[], options: TimelineOptions): TimelineRow[] {
95
+ return options.hideTools === true ? rows.filter((row) => !TOOL_KINDS.has(row.kind) || isImageRow(row)) : rows;
87
96
  }
88
97
 
89
98
  /*
@@ -599,7 +608,7 @@ export function deriveTimelineRows(items: ChatItem[], options: TimelineOptions):
599
608
 
600
609
  for (const chunk of chunksByTurn(items)) {
601
610
  if (chunk.turnId === null) {
602
- rows.push(...rowsForItems(chunk.items, options, children));
611
+ rows.push(...shownRows(rowsForItems(chunk.items, options, children), options));
603
612
 
604
613
  continue;
605
614
  }
@@ -616,7 +625,8 @@ export function deriveTimelineRows(items: ChatItem[], options: TimelineOptions):
616
625
  const active = turnId === options.activeTurnId || turn?.state === 'running';
617
626
 
618
627
  if (!turn || active) {
619
- const live = rowsForItems(conversation, options, children);
628
+ // Without its call, a thought has nothing to stand over and stays where it was thought.
629
+ const live = shownRows(rowsForItems(conversation, options, children), options);
620
630
  rows.push(...(active ? withLiveThought(live, conversation) : live));
621
631
 
622
632
  if (active) {
@@ -628,7 +638,7 @@ export function deriveTimelineRows(items: ChatItem[], options: TimelineOptions):
628
638
 
629
639
  // Steering splits the conversation inside a turn. Earlier responses keep their place above it.
630
640
  const lastUser = conversation.findLastIndex((item) => item.kind === 'user');
631
- rows.push(...rowsForItems(conversation.slice(0, lastUser + 1), options, children));
641
+ rows.push(...shownRows(rowsForItems(conversation.slice(0, lastUser + 1), options, children), options));
632
642
  const rest = conversation.slice(lastUser + 1);
633
643
  const work = rowsForItems(rest, options, children);
634
644
  // The closing answer stays visible; the work after the last input folds behind the label.
@@ -642,7 +652,9 @@ export function deriveTimelineRows(items: ChatItem[], options: TimelineOptions):
642
652
  );
643
653
 
644
654
  const folded = before.length - standing.length;
645
- const expanded = options.expandedTurns.has(turnId);
655
+ // With the tool lines left out, a turn that only ran tools has nothing behind its fold to open.
656
+ const expandable = shownRows(before, options).some((row) => !standing.includes(row));
657
+ const expanded = expandable && options.expandedTurns.has(turnId);
646
658
 
647
659
  if (folded > 0) {
648
660
  const tools = rest.filter(
@@ -656,11 +668,13 @@ export function deriveTimelineRows(items: ChatItem[], options: TimelineOptions):
656
668
  label: turnLabel(turn, chunk.items),
657
669
  work: summarizeTurn(tools, options.commandLooks),
658
670
  hiddenCount: folded,
659
- expanded
671
+ expanded,
672
+ expandable
660
673
  });
661
674
  }
662
675
 
663
- rows.push(...(expanded ? before : standing));
676
+ // The fold counts the tool lines it would hide, so a turn of only tools still says how long it worked.
677
+ rows.push(...(expanded ? shownRows(before, options) : standing));
664
678
  const changed = changedFilesRow(turn, conversation);
665
679
 
666
680
  if (changed) {
@@ -247,6 +247,7 @@ export function Composer({ chatId, info, focused, answerPromptsElsewhere, disabl
247
247
  )}
248
248
  {...files.dropTarget}
249
249
  >
250
+ {host.ComposerHeaderSlot && <host.ComposerHeaderSlot scopeId={scope.id} chatId={chatId} disabled={!writable} focused={focused} />}
250
251
  <PromptComposer
251
252
  chatId={chatId}
252
253
  pending={pending}
@@ -18,6 +18,7 @@ import { useChatRow } from '../../state/chats';
18
18
  import { useChatPreferences } from '../preferences';
19
19
  import { readingWidthStyle } from '../reading-width';
20
20
  import { useChatHost } from '../../host';
21
+ import { useHostHidesToolCalls } from '../../host-hooks';
21
22
  import { useChatScope } from '../../scope';
22
23
  import { EmptyState, ContextMenu, Icon } from '@adecore/ui';
23
24
 
@@ -67,12 +68,15 @@ export function SubagentTimeline({ chatId, toolUseId }: { chatId: string; toolUs
67
68
  }
68
69
  }, [state.unsupported, scope]);
69
70
 
71
+ const hideTools = useHostHidesToolCalls(scope.id, chatId);
72
+
70
73
  const rows = deriveTimelineRows(state.items, {
71
74
  expandedGroups: groups.ids,
72
75
  expandedTurns: NO_TURNS,
73
76
  expandedSubagents: subagents.ids,
74
77
  activeTurnId: null,
75
- commandLooks
78
+ commandLooks,
79
+ hideTools
76
80
  });
77
81
 
78
82
  useLayoutEffect(() => {
@@ -80,7 +80,7 @@ import { FileLinkContext } from './file-links';
80
80
  import { AgentIcon } from '../../agents/AgentIcon';
81
81
  import { useModelName } from '../../agents/model-name';
82
82
  import { ContextMenu, EmptyState, ErrorBoundary, Icon, SectionLabel } from '@adecore/ui';
83
- import { useHostContextSources, useHostHidesFolder, useHostThreadCards, useHostTimelineFind, useHostWelcome } from '../../host-hooks';
83
+ import { useHostContextSources, useHostHidesFolder, useHostHidesToolCalls, useHostThreadCards, useHostTimelineFind, useHostWelcome } from '../../host-hooks';
84
84
 
85
85
  const ESTIMATED_ROW_PX = 56;
86
86
  // How long a jump waits for a thread that is still loading before it calls the item missing.
@@ -273,6 +273,8 @@ export function Timeline({
273
273
  });
274
274
  }, [scope, chatId, cursor]);
275
275
 
276
+ const hideTools = useHostHidesToolCalls(scopeId, chatId);
277
+
276
278
  const threadRows = useMemo(() => {
277
279
  if (!order || !items) {
278
280
  return [];
@@ -286,10 +288,11 @@ export function Timeline({
286
288
  expandedSubagents: subagents.ids,
287
289
  activeTurnId,
288
290
  forkedTurns,
289
- commandLooks: host.commandLooks
291
+ commandLooks: host.commandLooks,
292
+ hideTools
290
293
  }
291
294
  );
292
- }, [order, items, groups.ids, turns.ids, subagents.ids, activeTurnId, forkedTurns, host.commandLooks]);
295
+ }, [order, items, groups.ids, turns.ids, subagents.ids, activeTurnId, forkedTurns, host.commandLooks, hideTools]);
293
296
 
294
297
  const cards = useHostThreadCards(scopeId, chatId);
295
298
  const drawsVisuals = host.visuals !== null;
@@ -480,6 +483,34 @@ export function Timeline({
480
483
  scrollGeometry.current = { top: element.scrollTop, height: element.scrollHeight, viewport: element.clientHeight };
481
484
  }, [rows.length, totalSize, composerHeight, frame.height]);
482
485
 
486
+ /*
487
+ * A height that changes after the first paint (the composer taking its draft, a row measured again) is
488
+ * seen by an observer before the page paints, but the state it sets is drawn after that paint. The
489
+ * end is pinned here as well, so the page never paints the thread with its end out of view.
490
+ */
491
+ useLayoutEffect(() => {
492
+ const content = contentRef.current;
493
+
494
+ if (content === null || typeof ResizeObserver === 'undefined') {
495
+ return;
496
+ }
497
+
498
+ const observer = new ResizeObserver(() => {
499
+ const element = scrollRef.current;
500
+
501
+ if (!followRef.current || element === null) {
502
+ return;
503
+ }
504
+
505
+ element.scrollTop = element.scrollHeight;
506
+ scrollGeometry.current = { top: element.scrollTop, height: element.scrollHeight, viewport: element.clientHeight };
507
+ });
508
+
509
+ observer.observe(content);
510
+
511
+ return () => observer.disconnect();
512
+ }, []);
513
+
483
514
  // Prepending history moves rows; keep the reader on the same row at the same height.
484
515
  useLayoutEffect(() => {
485
516
  const element = scrollRef.current;
@@ -78,7 +78,16 @@ export function Row({ row, chatId, toggleGroup, toggleTurn, toggleSubagent, open
78
78
  case 'subagent':
79
79
  return <SubagentBranchRow chatId={chatId} branch={row} onToggle={toggleSubagent} onOpenConversation={openConversation} />;
80
80
  case 'turn-fold':
81
- return <TurnFoldRow turn={row.turn} label={row.label} work={row.work} expanded={row.expanded} onToggle={() => toggleTurn(row.turn.id)} />;
81
+ return (
82
+ <TurnFoldRow
83
+ turn={row.turn}
84
+ label={row.label}
85
+ work={row.work}
86
+ expanded={row.expanded}
87
+ expandable={row.expandable}
88
+ onToggle={() => toggleTurn(row.turn.id)}
89
+ />
90
+ );
82
91
  case 'forks':
83
92
  return <ForksRow chatId={chatId} turnId={row.turnId} />;
84
93
  case 'changed-files':
@@ -18,7 +18,7 @@ import { canOpenSubagent, crumbOf, type SubagentStep } from '../../subagent-view
18
18
  import { type SubagentTask } from '../../../host';
19
19
  import { useChatScope } from '../../../scope';
20
20
  import { useCurrentItem } from '../../../state/chats';
21
- import { useHostTask } from '../../../host-hooks';
21
+ import { useHostHidesToolCalls, useHostTask } from '../../../host-hooks';
22
22
 
23
23
  // The work of a long-running agent scrolls inside its row instead of pushing the thread away.
24
24
  const CHILDREN_MAX_PX = 320;
@@ -81,25 +81,34 @@ function StatusPill({ item, task }: { item: ChatSubagentItem; task: SubagentTask
81
81
  /* What the sub-agent did, live: its own tool calls and the text it wrote, as ordinary rows. */
82
82
  function SubagentWork({ chatId, item, work }: { chatId: string; item: ChatSubagentItem; work: ChatItem[] }) {
83
83
  const { t } = useTranslation('agent-chat');
84
+ const { id } = useChatScope();
85
+ const hideTools = useHostHidesToolCalls(id, chatId);
84
86
  const scroller = useRef<HTMLDivElement>(null);
85
87
  const running = item.status === 'running';
88
+ // A generated image reads as a result, not as a tool line, so it stays when the lines go.
89
+ const shown = hideTools ? work.filter((child) => child.kind !== 'tool' || isImageGeneration(child)) : work;
86
90
 
87
91
  useLayoutEffect(() => {
88
92
  // While it works, the newest line is the one worth seeing.
89
93
  if (running && scroller.current) {
90
94
  scroller.current.scrollTop = scroller.current.scrollHeight;
91
95
  }
92
- }, [work.length, running]);
96
+ }, [shown.length, running]);
93
97
 
94
98
  if (work.length === 0) {
95
99
  // A line inside a chat row, where the centered block of an EmptyState would outweigh the row itself.
96
100
  return <div className="ml-6 pb-1 text-xs text-text-faint">{t('rows.subagent.nothingYet')}</div>;
97
101
  }
98
102
 
103
+ // It did work, only none that is shown; "nothing yet" would say it did none.
104
+ if (shown.length === 0) {
105
+ return null;
106
+ }
107
+
99
108
  return (
100
109
  <div ref={scroller} className="ml-6 overflow-y-auto" style={{ maxHeight: CHILDREN_MAX_PX }}>
101
110
  {item.itemsTruncated && <div className="pb-1 text-xs text-text-faint">{t('rows.subagent.truncated')}</div>}
102
- {work.map((child) => (
111
+ {shown.map((child) => (
103
112
  <ChildRow key={child.id} chatId={chatId} item={child} />
104
113
  ))}
105
114
  </div>