@opengeni/react 6.0.0-canary.1 → 6.1.0-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +17 -0
  2. package/dist/capability-catalog-row.d.ts +20 -0
  3. package/dist/{chunk-VRBL3F5E.js → chunk-5GAL23FF.js} +10 -2
  4. package/dist/{chunk-VRBL3F5E.js.map → chunk-5GAL23FF.js.map} +1 -1
  5. package/dist/{chunk-IHQII5WO.js → chunk-G5JFM6ZE.js} +29 -4
  6. package/dist/chunk-G5JFM6ZE.js.map +1 -0
  7. package/dist/{chunk-UMECRJRV.js → chunk-HJ5VJ4JB.js} +21 -12
  8. package/dist/chunk-HJ5VJ4JB.js.map +1 -0
  9. package/dist/{chunk-R4KRSFGA.js → chunk-IP4BUBKH.js} +3 -3
  10. package/dist/{chunk-CC7AVRIJ.js → chunk-KZ4QDBGM.js} +87 -14
  11. package/dist/chunk-KZ4QDBGM.js.map +1 -0
  12. package/dist/{chunk-PZ27MCZP.js → chunk-L5VJYZIW.js} +34 -4
  13. package/dist/chunk-L5VJYZIW.js.map +1 -0
  14. package/dist/{chunk-45XEUVTK.js → chunk-UWVY3OXF.js} +12 -5
  15. package/dist/chunk-UWVY3OXF.js.map +1 -0
  16. package/dist/components/message-timeline.d.ts +2 -2
  17. package/dist/composer.js +4 -4
  18. package/dist/connect-panel.d.ts +2 -1
  19. package/dist/connect-setup.d.ts +3 -1
  20. package/dist/connect.d.ts +1 -0
  21. package/dist/connect.js +620 -424
  22. package/dist/connect.js.map +1 -1
  23. package/dist/connection-catalog.d.ts +6 -1
  24. package/dist/connection-installed.d.ts +1 -0
  25. package/dist/connection-logo.d.ts +4 -2
  26. package/dist/discovery-cache.d.ts +5 -0
  27. package/dist/index.js +7 -7
  28. package/dist/{model-policy-picker-menu-3R336DP2.js → model-policy-picker-menu-RVOQ4ZZG.js} +4 -4
  29. package/dist/model-policy.d.ts +6 -2
  30. package/dist/model-policy.js +1 -1
  31. package/dist/plugin-discovery.d.ts +4 -0
  32. package/dist/realtime.js +2 -2
  33. package/dist/session-ui.js +6 -6
  34. package/dist/session.js +1 -1
  35. package/dist/skill-discovery.d.ts +11 -0
  36. package/dist/timeline/projection.d.ts +3 -1
  37. package/dist/timeline/turn-summary.d.ts +2 -2
  38. package/package.json +3 -3
  39. package/src/capability-catalog-row.tsx +94 -0
  40. package/src/components/markdown.tsx +32 -3
  41. package/src/components/message-timeline.tsx +88 -4
  42. package/src/components/model-picker.tsx +11 -3
  43. package/src/components/model-policy-picker-menu.tsx +10 -3
  44. package/src/components/session-conversation.tsx +1 -3
  45. package/src/components/session-status.tsx +2 -2
  46. package/src/connect-panel.tsx +19 -8
  47. package/src/connect-setup.tsx +49 -14
  48. package/src/connect.ts +6 -0
  49. package/src/connection-catalog.tsx +61 -39
  50. package/src/connection-installed.tsx +2 -0
  51. package/src/connection-logo.tsx +14 -10
  52. package/src/discovery-cache.ts +42 -0
  53. package/src/hooks/use-session-events.ts +24 -2
  54. package/src/model-policy.ts +21 -0
  55. package/src/plugin-details.tsx +8 -5
  56. package/src/plugin-discovery.tsx +87 -71
  57. package/src/skill-discovery.tsx +101 -56
  58. package/src/timeline/projection.ts +31 -1
  59. package/src/timeline/turn-summary.tsx +3 -3
  60. package/styles/compiled.css +27 -0
  61. package/styles/connect.css +56 -15
  62. package/dist/chunk-45XEUVTK.js.map +0 -1
  63. package/dist/chunk-CC7AVRIJ.js.map +0 -1
  64. package/dist/chunk-IHQII5WO.js.map +0 -1
  65. package/dist/chunk-PZ27MCZP.js.map +0 -1
  66. package/dist/chunk-UMECRJRV.js.map +0 -1
  67. /package/dist/{chunk-R4KRSFGA.js.map → chunk-IP4BUBKH.js.map} +0 -0
  68. /package/dist/{model-policy-picker-menu-3R336DP2.js.map → model-policy-picker-menu-RVOQ4ZZG.js.map} +0 -0
@@ -1,5 +1,13 @@
1
+ import { createDiscoveryCache } from "./discovery-cache";
1
2
  import { useEffect, useRef, useState } from "react";
2
3
  import type { PluginDiscoveryItem, PluginDiscoveryPage } from "@opengeni/sdk";
4
+ import { BoxesIcon } from "lucide-react";
5
+ import { CapabilityCatalogRow } from "./capability-catalog-row";
6
+ import { ConnectionLogo } from "./connection-logo";
7
+
8
+ const discoveryCache = createDiscoveryCache<PluginDiscoveryPage>();
9
+ const EMPTY_INSTALLED_IDS: ReadonlySet<string> = new Set();
10
+
3
11
  export type PluginDiscoveryProps = {
4
12
  client: {
5
13
  discoverPlugins(
@@ -9,30 +17,39 @@ export type PluginDiscoveryProps = {
9
17
  };
10
18
  workspaceId: string;
11
19
  query: string;
20
+ /** Discovery identities already installed in the current workspace. */
21
+ installedIds?: ReadonlySet<string>;
22
+ resultLimit?: number;
23
+ onShowMore?: () => void;
12
24
  onOpen: (item: PluginDiscoveryItem) => void;
13
25
  };
14
26
  export function PluginDiscovery(props: PluginDiscoveryProps) {
15
27
  const [provider, setProvider] = useState("");
16
28
  return (
17
- <section className="og-plugin-discovery" aria-label="Discover plugins">
29
+ <section
30
+ className={`og-plugin-discovery ${props.resultLimit ? "og-catalog-overview" : ""}`}
31
+ aria-label="Discover plugins"
32
+ >
18
33
  <header>
19
- <h3>Browse plugins</h3>
20
- <div className="og-plugin-filters" role="group" aria-label="Plugin registry">
21
- {[
22
- { value: "", label: "All" },
23
- { value: "openai", label: "OpenAI plugin registry" },
24
- { value: "anthropic", label: "Anthropic plugin registry" },
25
- ].map((option) => (
26
- <button
27
- key={option.value}
28
- type="button"
29
- aria-pressed={provider === option.value}
30
- onClick={() => setProvider(option.value)}
31
- >
32
- {option.label}
33
- </button>
34
- ))}
35
- </div>
34
+ <h3>{props.resultLimit ? "Plugins" : "Browse plugins"}</h3>
35
+ {!props.resultLimit ? (
36
+ <div className="og-plugin-filters" role="group" aria-label="Plugin registry">
37
+ {[
38
+ { value: "", label: "All" },
39
+ { value: "openai", label: "OpenAI plugin registry" },
40
+ { value: "anthropic", label: "Anthropic plugin registry" },
41
+ ].map((option) => (
42
+ <button
43
+ key={option.value}
44
+ type="button"
45
+ aria-pressed={provider === option.value}
46
+ onClick={() => setProvider(option.value)}
47
+ >
48
+ {option.label}
49
+ </button>
50
+ ))}
51
+ </div>
52
+ ) : null}
36
53
  </header>
37
54
  <Results
38
55
  key={`${props.workspaceId}:${props.query}:${provider}`}
@@ -47,39 +64,49 @@ function Results({
47
64
  workspaceId,
48
65
  query,
49
66
  provider,
67
+ installedIds = EMPTY_INSTALLED_IDS,
50
68
  onOpen,
69
+ resultLimit,
70
+ onShowMore,
51
71
  }: PluginDiscoveryProps & { provider: string }) {
52
- const [items, setItems] = useState<PluginDiscoveryItem[]>([]);
72
+ const initial = discoveryCache.peek(client, JSON.stringify([workspaceId, query, provider, 0]));
73
+ const [items, setItems] = useState<PluginDiscoveryItem[]>(initial?.items ?? []);
53
74
  const [offset, setOffset] = useState(0);
54
- const [next, setNext] = useState<number | null>(null);
55
- const [total, setTotal] = useState(0);
56
- const [loading, setLoading] = useState(true);
75
+ const [next, setNext] = useState<number | null>(initial?.nextOffset ?? null);
76
+ const [total, setTotal] = useState(initial?.total ?? 0);
77
+ const [loading, setLoading] = useState(!initial);
57
78
  const [error, setError] = useState(false);
58
79
  const [retry, setRetry] = useState(0);
59
80
  const sentinel = useRef<HTMLDivElement>(null);
60
81
  useEffect(() => {
61
82
  let active = true;
62
- setLoading(true);
83
+ const cacheKey = JSON.stringify([workspaceId, query, provider, offset]);
84
+ const cached = discoveryCache.peek(client, cacheKey);
85
+ setLoading(!cached);
63
86
  setError(false);
64
87
  const timer = setTimeout(
65
88
  () => {
66
- void client.discoverPlugins(workspaceId, { query, provider, offset }).then(
67
- (page) => {
68
- if (!active) return;
69
- setItems((previous) => (offset ? [...previous, ...page.items] : page.items));
70
- setTotal(page.total);
71
- setNext(page.nextOffset);
72
- setLoading(false);
73
- },
74
- () => {
75
- if (active) {
76
- setError(true);
89
+ void discoveryCache
90
+ .read(client, cacheKey, () =>
91
+ client.discoverPlugins(workspaceId, { query, provider, offset }),
92
+ )
93
+ .then(
94
+ (page) => {
95
+ if (!active) return;
96
+ setItems((previous) => (offset ? [...previous, ...page.items] : page.items));
97
+ setTotal(page.total);
98
+ setNext(page.nextOffset);
77
99
  setLoading(false);
78
- }
79
- },
80
- );
100
+ },
101
+ () => {
102
+ if (active) {
103
+ setError(true);
104
+ setLoading(false);
105
+ }
106
+ },
107
+ );
81
108
  },
82
- offset ? 0 : 200,
109
+ cached || offset ? 0 : 200,
83
110
  );
84
111
  return () => {
85
112
  active = false;
@@ -87,7 +114,7 @@ function Results({
87
114
  };
88
115
  }, [client, workspaceId, query, provider, offset, retry]);
89
116
  useEffect(() => {
90
- if (loading || error || next === null || !sentinel.current) return;
117
+ if (resultLimit || loading || error || next === null || !sentinel.current) return;
91
118
  const observer = new IntersectionObserver(
92
119
  (entries) => {
93
120
  if (entries.some((entry) => entry.isIntersecting)) setOffset(next);
@@ -96,7 +123,7 @@ function Results({
96
123
  );
97
124
  observer.observe(sentinel.current);
98
125
  return () => observer.disconnect();
99
- }, [loading, error, next]);
126
+ }, [loading, error, next, resultLimit]);
100
127
  return (
101
128
  <>
102
129
  {!loading && !error ? (
@@ -105,39 +132,23 @@ function Results({
105
132
  </p>
106
133
  ) : null}
107
134
  <div className="og-plugin-discovery-grid">
108
- {items.map((item) => (
109
- <button
135
+ {items.slice(0, resultLimit).map((item) => (
136
+ <CapabilityCatalogRow
110
137
  key={item.id}
111
- className="og-plugin-discovery-row"
112
- type="button"
113
- onClick={() => onOpen(item)}
114
- >
115
- {item.logoUrl ? (
116
- <img
138
+ data-plugin-id={item.id}
139
+ name={item.displayName.replace(/-/g, " ")}
140
+ description={item.description}
141
+ status={installedIds.has(item.id) ? "added" : "available"}
142
+ statusLabel={installedIds.has(item.id) ? "Installed" : "Available to install"}
143
+ onOpen={() => onOpen(item)}
144
+ icon={
145
+ <ConnectionLogo
117
146
  src={item.logoUrl}
118
- alt=""
119
- loading="lazy"
120
- onError={(event) => {
121
- event.currentTarget.style.display = "none";
122
- }}
147
+ name={item.displayName}
148
+ fallback={<BoxesIcon aria-hidden="true" />}
123
149
  />
124
- ) : null}
125
- <span className="og-plugin-copy">
126
- <span className="og-plugin-title">
127
- <strong>{item.displayName.replace(/-/g, " ")}</strong>
128
- <small>
129
- {item.provider === "openai"
130
- ? "OpenAI plugin registry"
131
- : "Anthropic plugin registry"}
132
- </small>
133
- </span>
134
- <span className="og-plugin-description">{item.description}</span>
135
- {item.category ? <span className="og-plugin-category">{item.category}</span> : null}
136
- </span>
137
- <span className="og-plugin-open" aria-hidden="true">
138
-
139
- </span>
140
- </button>
150
+ }
151
+ />
141
152
  ))}
142
153
  </div>
143
154
  {loading ? <p role="status">Loading plugins…</p> : null}
@@ -149,8 +160,13 @@ function Results({
149
160
  </button>
150
161
  </p>
151
162
  ) : null}
163
+ {resultLimit && !loading && !error && total > resultLimit ? (
164
+ <button className="og-catalog-more" type="button" onClick={onShowMore}>
165
+ View all plugins
166
+ </button>
167
+ ) : null}
152
168
  <div ref={sentinel}>
153
- {next !== null && !loading && !error ? (
169
+ {!resultLimit && next !== null && !loading && !error ? (
154
170
  <button className="og-plugin-more" type="button" onClick={() => setOffset(next)}>
155
171
  Show more plugins
156
172
  </button>
@@ -1,4 +1,7 @@
1
1
  import { useEffect, useState } from "react";
2
+ import { createDiscoveryCache } from "./discovery-cache";
3
+ import { BookOpenIcon } from "lucide-react";
4
+ import { CapabilityCatalogRow, type CapabilityCatalogStatus } from "./capability-catalog-row";
2
5
 
3
6
  export type SkillDiscoveryItem = {
4
7
  id: string;
@@ -8,6 +11,8 @@ export type SkillDiscoveryItem = {
8
11
  url: string;
9
12
  };
10
13
  export type SkillDiscoveryPage = { items: SkillDiscoveryItem[]; nextCursor: null };
14
+ const discoveryCache = createDiscoveryCache<SkillDiscoveryPage>();
15
+
11
16
  export type SkillDiscoveryClient = {
12
17
  searchPublicSkills(workspaceId: string, query: string): Promise<SkillDiscoveryPage>;
13
18
  };
@@ -18,35 +23,53 @@ export type SkillDiscoveryProps = {
18
23
  query: string;
19
24
  onImport: (url: string) => void;
20
25
  canManage: boolean;
26
+ localSkills?: readonly {
27
+ id: string;
28
+ name: string;
29
+ description?: string;
30
+ status?: CapabilityCatalogStatus;
31
+ statusLabel?: string;
32
+ onOpen: () => void;
33
+ }[];
34
+ resultLimit?: number;
35
+ onShowMore?: () => void;
21
36
  onSearch?: (() => void) | undefined;
22
37
  };
23
38
 
24
39
  /** Host-owned query and import flow; browse presentation is reusable independently. */
25
40
  export function SkillDiscovery(props: SkillDiscoveryProps) {
26
41
  const query = props.query.trim();
42
+ const discoveryQuery = query || "agent";
27
43
  return (
28
44
  <section className="og-skill-discovery" aria-label="Discover skills">
29
45
  <header>
30
- <h3>Discover skills</h3>
31
- <a
32
- href="https://skills.sh/"
33
- target="_blank"
34
- rel="noopener noreferrer"
35
- className="og-skill-discovery-browse"
36
- >
37
- Browse skills.sh ↗
38
- </a>
46
+ <h3>{props.resultLimit ? "Skills" : query ? "Search skills" : "Browse skills"}</h3>
47
+ {!props.resultLimit ? (
48
+ <a
49
+ href="https://skills.sh/"
50
+ target="_blank"
51
+ rel="noopener noreferrer"
52
+ className="og-skill-discovery-browse"
53
+ >
54
+ Browse skills.sh ↗
55
+ </a>
56
+ ) : null}
39
57
  </header>
40
- {!query && props.onSearch ? (
41
- <button type="button" className="og-skill-discovery-search" onClick={props.onSearch}>
42
- Search skills
43
- </button>
58
+ {!query ? (
59
+ <p>Agent skills from skills.sh. Search above for another topic.</p>
60
+ ) : query.length < 2 ? (
61
+ <p>Enter at least two characters to search skills.sh.</p>
44
62
  ) : null}
45
- <DiscoveryResults key={`${props.workspaceId}:${query}`} {...props} query={query} />
63
+ <DiscoveryResults
64
+ key={`${props.workspaceId}:${discoveryQuery}`}
65
+ {...props}
66
+ query={discoveryQuery}
67
+ />
46
68
  </section>
47
69
  );
48
70
  }
49
71
 
72
+ const EMPTY_LOCAL_SKILLS: NonNullable<SkillDiscoveryProps["localSkills"]> = [];
50
73
  const EMPTY_INSTALLED_SKILLS: NonNullable<SkillDiscoveryProps["installedSkills"]> = [];
51
74
 
52
75
  function DiscoveryResults({
@@ -55,21 +78,29 @@ function DiscoveryResults({
55
78
  query,
56
79
  canManage,
57
80
  onImport,
81
+ resultLimit,
82
+ onShowMore,
83
+ localSkills = EMPTY_LOCAL_SKILLS,
58
84
  installedSkills = EMPTY_INSTALLED_SKILLS,
59
85
  }: SkillDiscoveryProps) {
60
- const [result, setResult] = useState<SkillDiscoveryPage | null>(null);
61
- const [loading, setLoading] = useState(query.length >= 2);
86
+ const cacheKey = JSON.stringify([workspaceId, query]);
87
+ const cached = discoveryCache.peek(client, cacheKey);
88
+ const [result, setResult] = useState<SkillDiscoveryPage | null>(cached ?? null);
89
+ const [loading, setLoading] = useState(query.length >= 2 && !cached);
62
90
  const [error, setError] = useState(false);
63
91
  const [retry, setRetry] = useState(0);
64
92
  useEffect(() => {
65
93
  let active = true;
66
94
  setError(false);
67
95
  if (query.length < 2) return;
68
- setLoading(true);
96
+ const existing = discoveryCache.peek(client, cacheKey);
97
+ setLoading(!existing);
69
98
  let timer: ReturnType<typeof setTimeout>;
70
99
  const search = async (attempt: number) => {
71
100
  try {
72
- const data = await client.searchPublicSkills(workspaceId, query);
101
+ const data = await discoveryCache.read(client, cacheKey, () =>
102
+ client.searchPublicSkills(workspaceId, query),
103
+ );
73
104
  if (!active) return;
74
105
  setResult(data);
75
106
  setLoading(false);
@@ -90,51 +121,65 @@ function DiscoveryResults({
90
121
  setLoading(false);
91
122
  }
92
123
  };
93
- timer = setTimeout(() => void search(0), 300);
124
+ timer = setTimeout(() => void search(0), existing ? 0 : 300);
94
125
  return () => {
95
126
  active = false;
96
127
  clearTimeout(timer);
97
128
  };
98
- }, [client, workspaceId, query, retry]);
129
+ }, [client, workspaceId, query, cacheKey, retry]);
130
+ const remoteSkills =
131
+ result?.items.filter(
132
+ (skill) =>
133
+ !localSkills.some((local) => local.name.toLowerCase() === skill.name.toLowerCase()),
134
+ ) ?? [];
99
135
  return (
100
136
  <div aria-busy={loading}>
101
137
  <div className="og-skill-discovery-grid">
102
- {result?.items.map((skill) => {
103
- const installed = installedSkills.some(
104
- (entry) =>
105
- entry.sourceUrl.replace(/\/$/, "").toLowerCase() === skill.url.toLowerCase() ||
106
- (entry.repositoryUrl.replace(/\/$/, "").toLowerCase() ===
107
- `https://github.com/${skill.source}`.toLowerCase() &&
108
- entry.name.toLowerCase() === skill.name.toLowerCase()),
109
- );
110
- return (
111
- <button
112
- key={skill.id}
113
- type="button"
114
- className="og-skill-discovery-item"
115
- disabled={!canManage}
116
- onClick={() => onImport(skill.url)}
117
- >
118
- <span className="og-skill-discovery-copy">
119
- <strong>{skill.name}</strong>
120
- <span>{skill.source}</span>
121
- <small>{skill.installs.toLocaleString()} installs</small>
122
- </span>
123
- <span className="og-skill-discovery-action">
124
- {installed ? (
125
- <>
126
- <span aria-hidden="true">✓ </span>Installed
127
- </>
128
- ) : !canManage ? (
129
- "Admin required"
130
- ) : (
131
- "Preview →"
132
- )}
133
- </span>
134
- </button>
135
- );
136
- })}
138
+ {localSkills.slice(0, resultLimit).map((skill) => (
139
+ <CapabilityCatalogRow
140
+ key={skill.id}
141
+ name={skill.name}
142
+ description={skill.description}
143
+ icon={<BookOpenIcon aria-hidden="true" />}
144
+ status={skill.status ?? "added"}
145
+ statusLabel={skill.statusLabel}
146
+ onOpen={skill.onOpen}
147
+ />
148
+ ))}
149
+ {remoteSkills
150
+ .slice(
151
+ 0,
152
+ resultLimit === undefined ? undefined : Math.max(0, resultLimit - localSkills.length),
153
+ )
154
+ .map((skill) => {
155
+ const installed = installedSkills.some(
156
+ (entry) =>
157
+ entry.sourceUrl.replace(/\/$/, "").toLowerCase() === skill.url.toLowerCase() ||
158
+ (entry.repositoryUrl.replace(/\/$/, "").toLowerCase() ===
159
+ `https://github.com/${skill.source}`.toLowerCase() &&
160
+ entry.name.toLowerCase() === skill.name.toLowerCase()),
161
+ );
162
+ return (
163
+ <CapabilityCatalogRow
164
+ key={skill.id}
165
+ disabled={!canManage}
166
+ name={skill.name}
167
+ description={skill.source}
168
+ icon={<BookOpenIcon aria-hidden="true" />}
169
+ status={!canManage ? "unavailable" : installed ? "added" : "available"}
170
+ statusLabel={
171
+ !canManage ? "Admin required" : installed ? "Installed" : "Available to add"
172
+ }
173
+ onOpen={() => onImport(skill.url)}
174
+ />
175
+ );
176
+ })}
137
177
  </div>
178
+ {resultLimit && remoteSkills.length + localSkills.length > resultLimit && !loading ? (
179
+ <button className="og-catalog-more" type="button" onClick={onShowMore}>
180
+ View all skills
181
+ </button>
182
+ ) : null}
138
183
  {loading ? <p role="status">Loading skills…</p> : null}
139
184
  {error ? (
140
185
  <div role="alert" className="og-skill-discovery-error">
@@ -144,7 +189,7 @@ function DiscoveryResults({
144
189
  </button>
145
190
  </div>
146
191
  ) : null}
147
- {result && !result.items.length && !loading && !error ? (
192
+ {result && !remoteSkills.length && !localSkills.length && !loading && !error ? (
148
193
  <p role="status">No skills found. Try a different search.</p>
149
194
  ) : null}
150
195
  </div>
@@ -129,10 +129,20 @@ function timelineAnnotationToolOutputText(output: unknown): string | null {
129
129
  return text.length > 0 ? text : null;
130
130
  }
131
131
 
132
- export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
132
+ export function buildTimeline(
133
+ events: SessionEvent[],
134
+ options: { partialStart?: boolean } = {},
135
+ ): TimelineItem[] {
133
136
  const items: TimelineItem[] = [];
134
137
  const prescan = prescanTurnAnchors(events);
135
138
  const ordered = orderTimelineEvents(events, prescan);
139
+ // A bounded replay can begin halfway through a message (including inside an
140
+ // interactive fence). Never parse that suffix as a complete Markdown source.
141
+ // Keep the raw events available for pagination; a completed receipt restores
142
+ // the authoritative message without requiring an unbounded history fetch.
143
+ let completeHistoryPrefix = options.partialStart !== true;
144
+ const knownMessagePrefixTurns = new Set<string | null>();
145
+ const incompleteMessageKeys = new Set<string>();
136
146
  const pendingWaitOutcomeByTurn = new Map<string | null, PendingWaitOutcome>();
137
147
  const latestAgentResponseByTurn = new Map<string | null, TrackedAgentResponse>();
138
148
  const identifiedMessages = new Map<string, AgentMessageItem>();
@@ -299,6 +309,18 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
299
309
  for (const event of ordered) {
300
310
  const payload = asRecord(event.payload);
301
311
  const turnId = event.turnId ?? null;
312
+ if (event.type === "session.created") {
313
+ completeHistoryPrefix = true;
314
+ } else if (
315
+ event.type === "turn.started" ||
316
+ (event.type === "user.message" &&
317
+ payload.delivery !== "steer" &&
318
+ payload.routing !== "accepted_for_steering")
319
+ ) {
320
+ // Accepted Steer is visible before its replacement turn starts. It says
321
+ // nothing about the prefix of a still-streaming message from the old turn.
322
+ knownMessagePrefixTurns.add(turnId);
323
+ }
302
324
  startupEvent = event;
303
325
  startupTurnId = turnId;
304
326
  startupAttemptId =
@@ -415,6 +437,13 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
415
437
  }
416
438
  const messageId = stringValue(payload.messageId);
417
439
  const messageKey = messageId ? JSON.stringify([turnId, messageId]) : null;
440
+ if (
441
+ (!completeHistoryPrefix && !knownMessagePrefixTurns.has(turnId)) ||
442
+ (messageKey && incompleteMessageKeys.has(messageKey))
443
+ ) {
444
+ if (messageKey) incompleteMessageKeys.add(messageKey);
445
+ break;
446
+ }
418
447
  const identified = messageKey ? identifiedMessages.get(messageKey) : undefined;
419
448
  if (identified) {
420
449
  if (identified.annotationSource?.eventType !== "agent.message.completed") {
@@ -470,6 +499,7 @@ export function buildTimeline(events: SessionEvent[]): TimelineItem[] {
470
499
  }
471
500
 
472
501
  case "agent.message.completed": {
502
+ knownMessagePrefixTurns.add(turnId);
473
503
  const text = stringValue(payload.text);
474
504
  const messageId = stringValue(payload.messageId);
475
505
  const messageKey = messageId ? JSON.stringify([turnId, messageId]) : null;
@@ -125,8 +125,8 @@ export type TurnSummaryProps = {
125
125
  items: ActivityItem[];
126
126
  /**
127
127
  * The settled verdict — or absent for a completed CLUSTER of a still-running
128
- * turn, which folds neutrally: no verdict glyph (the turn has none yet), a
129
- * quiet pulse dot in its place so alignment and the running feel both hold.
128
+ * turn, which folds neutrally. Only unfinished activity inside this summary
129
+ * animates; the parent turn may still be composing a response elsewhere.
130
130
  */
131
131
  outcome?: TurnOutcome | undefined;
132
132
  /** A short failure reason shown inline on a failed chip (never hidden). */
@@ -447,7 +447,7 @@ export function TurnSummary({
447
447
  />
448
448
  {/* Completion is the quiet default and needs no repeated glyph. Failed,
449
449
  cancelled, and still-running folds retain a visible state marker. */}
450
- {outcome === "complete" || (liveHeader && !outcome) ? null : (
450
+ {outcome === "complete" || (!outcome && (liveHeader || context.settled)) ? null : (
451
451
  <span
452
452
  className={cn(
453
453
  "inline-flex shrink-0 items-center justify-center",
@@ -1623,6 +1623,9 @@
1623
1623
  :where(.og-root).min-h-14,:where(.og-root) .min-h-14 {
1624
1624
  min-height: calc(var(--spacing) * 14);
1625
1625
  }
1626
+ :where(.og-root).min-h-32,:where(.og-root) .min-h-32 {
1627
+ min-height: calc(var(--spacing) * 32);
1628
+ }
1626
1629
  :where(.og-root).min-h-56,:where(.og-root) .min-h-56 {
1627
1630
  min-height: calc(var(--spacing) * 56);
1628
1631
  }
@@ -1785,6 +1788,9 @@
1785
1788
  :where(.og-root).max-w-md,:where(.og-root) .max-w-md {
1786
1789
  max-width: var(--container-md);
1787
1790
  }
1791
+ :where(.og-root).max-w-prose,:where(.og-root) .max-w-prose {
1792
+ max-width: 65ch;
1793
+ }
1788
1794
  :where(.og-root).max-w-sm,:where(.og-root) .max-w-sm {
1789
1795
  max-width: var(--container-sm);
1790
1796
  }
@@ -4173,6 +4179,11 @@
4173
4179
  padding-top: calc(var(--spacing) * 0);
4174
4180
  }
4175
4181
  }
4182
+ :where(.og-root).first\:pt-1,:where(.og-root) .first\:pt-1 {
4183
+ &:first-child {
4184
+ padding-top: calc(var(--spacing) * 1);
4185
+ }
4186
+ }
4176
4187
  :where(.og-root).first\:pl-0,:where(.og-root) .first\:pl-0 {
4177
4188
  &:first-child {
4178
4189
  padding-left: calc(var(--spacing) * 0);
@@ -5020,6 +5031,11 @@
5020
5031
  }
5021
5032
  }
5022
5033
  }
5034
+ :where(.og-root).motion-safe\:animate-spin,:where(.og-root) .motion-safe\:animate-spin {
5035
+ @media (prefers-reduced-motion: no-preference) {
5036
+ animation: og-spin 1s linear infinite;
5037
+ }
5038
+ }
5023
5039
  :where(.og-root).motion-reduce\:animate-none,:where(.og-root) .motion-reduce\:animate-none {
5024
5040
  @media (prefers-reduced-motion: reduce) {
5025
5041
  animation: none;
@@ -5544,6 +5560,17 @@
5544
5560
  color: var(--color-fg-muted);
5545
5561
  }
5546
5562
  }
5563
+ :where(.og-root).\[\&\+section\]\:border-t,:where(.og-root) .\[\&\+section\]\:border-t {
5564
+ &+section {
5565
+ border-top-style: var(--tw-border-style);
5566
+ border-top-width: 1px;
5567
+ }
5568
+ }
5569
+ :where(.og-root).\[\&\+section\]\:border-og-border,:where(.og-root) .\[\&\+section\]\:border-og-border {
5570
+ &+section {
5571
+ border-color: var(--og-color-border);
5572
+ }
5573
+ }
5547
5574
  :where(.og-root).\[\&\:\:-webkit-details-marker\]\:hidden,:where(.og-root) .\[\&\:\:-webkit-details-marker\]\:hidden {
5548
5575
  &::-webkit-details-marker {
5549
5576
  display: none;