@pactor-app/chat 1.15.5 → 1.16.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.
package/dist/index.js CHANGED
@@ -118,35 +118,113 @@ var ConversationStore = class {
118
118
  };
119
119
 
120
120
  // src/artifact.ts
121
+ var ARTIFACT_IMAGE_EXTENSIONS = [
122
+ "png",
123
+ "jpg",
124
+ "jpeg",
125
+ "gif",
126
+ "svg",
127
+ "webp",
128
+ "avif",
129
+ "bmp",
130
+ "ico"
131
+ ];
132
+ function resolveArtifactFileType(artifact) {
133
+ const declared = artifact.fileType?.trim().toLowerCase().replace(/^\./, "");
134
+ if (declared) {
135
+ return declared;
136
+ }
137
+ const match = /\.([A-Za-z0-9]+)$/.exec(artifact.title.trim());
138
+ return match ? match[1].toLowerCase() : null;
139
+ }
140
+ function isImageArtifact(artifact) {
141
+ const ext = resolveArtifactFileType(artifact);
142
+ return ext !== null && ARTIFACT_IMAGE_EXTENSIONS.includes(ext);
143
+ }
144
+ function collectArtifacts(messages) {
145
+ const order = [];
146
+ const byId = /* @__PURE__ */ new Map();
147
+ for (const message of messages) {
148
+ for (const part of message.parts) {
149
+ if (part.type !== "artifact" || !part.artifact) {
150
+ continue;
151
+ }
152
+ if (part.status !== void 0 && part.status !== "ready") {
153
+ continue;
154
+ }
155
+ if (!byId.has(part.artifact.id)) {
156
+ order.push(part.artifact.id);
157
+ }
158
+ byId.set(part.artifact.id, part.artifact);
159
+ }
160
+ }
161
+ return order.map((id) => byId.get(id));
162
+ }
163
+ function mergeArtifacts(stream, opened) {
164
+ const openedById = new Map(opened.map((item) => [item.id, item]));
165
+ const merged = stream.map((item) => openedById.get(item.id) ?? item);
166
+ for (const item of opened) {
167
+ if (!stream.some((s) => s.id === item.id)) {
168
+ merged.push(item);
169
+ }
170
+ }
171
+ return merged;
172
+ }
121
173
  var ArtifactStore = class {
122
174
  constructor() {
123
- this.artifact = null;
175
+ this.artifacts = [];
176
+ this.activeId = null;
124
177
  this.listeners = /* @__PURE__ */ new Set();
125
178
  }
126
- /** 当前产出物(未打开时为 null;引用在不变时保持稳定) */
179
+ /** 当前激活产出物(未打开时为 null;引用在不变时保持稳定) */
127
180
  get() {
128
- return this.artifact;
181
+ if (this.activeId === null) {
182
+ return null;
183
+ }
184
+ return this.artifacts.find((item) => item.id === this.activeId) ?? null;
129
185
  }
130
- /** 打开(或替换)当前产出物 */
186
+ /** 全部产出物(插入序;数组不可变,引用稳定) */
187
+ list() {
188
+ return this.artifacts;
189
+ }
190
+ /** 打开(或按 id 替换)产出物并激活 */
131
191
  open(artifact) {
132
- this.artifact = artifact;
192
+ const index = this.artifacts.findIndex((item) => item.id === artifact.id);
193
+ this.artifacts = index === -1 ? [...this.artifacts, artifact] : this.artifacts.map((item, i) => i === index ? artifact : item);
194
+ this.activeId = artifact.id;
195
+ this.notify();
196
+ }
197
+ /** 切换激活项(未知 id 告警并忽略,不抛错) */
198
+ activate(id) {
199
+ if (this.activeId === id) {
200
+ return;
201
+ }
202
+ if (!this.artifacts.some((item) => item.id === id)) {
203
+ console.warn("[@pactor-app/chat] unknown artifact id, activate ignored");
204
+ return;
205
+ }
206
+ this.activeId = id;
133
207
  this.notify();
134
208
  }
135
- /** 部分更新当前产出物(id 不可改);未 open 时告警并忽略 */
209
+ /** 部分更新当前激活产出物(id 不可改);未 open 时告警并忽略 */
136
210
  update(patch) {
137
- if (!this.artifact) {
211
+ const current = this.get();
212
+ if (!current) {
138
213
  console.warn("[@pactor-app/chat] artifact not open, update ignored");
139
214
  return;
140
215
  }
141
- this.artifact = { ...this.artifact, ...patch, id: this.artifact.id };
216
+ const next = { ...current, ...patch, id: current.id };
217
+ this.artifacts = this.artifacts.map(
218
+ (item) => item.id === current.id ? next : item
219
+ );
142
220
  this.notify();
143
221
  }
144
- /** 关闭面板(未打开时幂等,不重复通知) */
222
+ /** 关闭面板:清空激活项,列表保留(未打开时幂等,不重复通知) */
145
223
  close() {
146
- if (this.artifact === null) {
224
+ if (this.activeId === null) {
147
225
  return;
148
226
  }
149
- this.artifact = null;
227
+ this.activeId = null;
150
228
  this.notify();
151
229
  }
152
230
  /** 订阅变更;返回退订函数 */
@@ -163,6 +241,94 @@ var ArtifactStore = class {
163
241
  }
164
242
  };
165
243
 
244
+ // src/artifact-download.ts
245
+ var KIND_EXTENSIONS = {
246
+ markdown: "md",
247
+ text: "txt",
248
+ code: "txt",
249
+ dsl: "json"
250
+ };
251
+ function resolveArtifactFilename(artifact) {
252
+ const title = artifact.title.trim() || "artifact";
253
+ if (/\.[A-Za-z0-9]+$/.test(title)) {
254
+ return title;
255
+ }
256
+ const ext = resolveArtifactFileType(artifact) ?? KIND_EXTENSIONS[artifact.kind];
257
+ return `${title}.${ext}`;
258
+ }
259
+ function dataUrlToBlob(dataUrl) {
260
+ const comma = dataUrl.indexOf(",");
261
+ const meta = dataUrl.slice(0, comma);
262
+ const payload = dataUrl.slice(comma + 1);
263
+ const mime = /^data:([^;,]+)/.exec(meta)?.[1] ?? "application/octet-stream";
264
+ if (meta.includes(";base64")) {
265
+ const binary = atob(payload);
266
+ const bytes = new Uint8Array(binary.length);
267
+ for (let i = 0; i < binary.length; i++) {
268
+ bytes[i] = binary.charCodeAt(i);
269
+ }
270
+ return new Blob([bytes], { type: mime });
271
+ }
272
+ return new Blob([decodeURIComponent(payload)], { type: mime });
273
+ }
274
+ function isRemoteUrl(raw) {
275
+ return /^https?:\/\//.test(raw.trim());
276
+ }
277
+ function buildArtifactBlob(artifact) {
278
+ if (artifact.kind === "dsl" && typeof artifact.content === "object") {
279
+ return new Blob([JSON.stringify(artifact.content, null, 2)], {
280
+ type: "application/json"
281
+ });
282
+ }
283
+ const raw = String(artifact.content);
284
+ if (isImageArtifact(artifact)) {
285
+ const trimmed = raw.trim();
286
+ if (trimmed.startsWith("data:")) {
287
+ return dataUrlToBlob(trimmed);
288
+ }
289
+ if (trimmed.startsWith("<svg") || trimmed.startsWith("<?xml")) {
290
+ return new Blob([raw], { type: "image/svg+xml" });
291
+ }
292
+ if (isRemoteUrl(trimmed)) {
293
+ return null;
294
+ }
295
+ }
296
+ const ext = resolveArtifactFileType(artifact);
297
+ const mime = ext === "md" || artifact.kind === "markdown" ? "text/markdown" : "text/plain";
298
+ return new Blob([raw], { type: `${mime};charset=utf-8` });
299
+ }
300
+ function saveBlob(blob, filename) {
301
+ const url = URL.createObjectURL(blob);
302
+ const anchor = document.createElement("a");
303
+ anchor.href = url;
304
+ anchor.download = filename;
305
+ document.body.appendChild(anchor);
306
+ anchor.click();
307
+ anchor.remove();
308
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
309
+ }
310
+ async function downloadArtifact(artifact) {
311
+ const filename = resolveArtifactFilename(artifact);
312
+ const raw = String(artifact.content ?? "").trim();
313
+ if (isImageArtifact(artifact) && isRemoteUrl(raw)) {
314
+ try {
315
+ const response = await fetch(raw);
316
+ if (!response.ok) {
317
+ throw new Error(`HTTP ${response.status}`);
318
+ }
319
+ saveBlob(await response.blob(), filename);
320
+ return;
321
+ } catch {
322
+ window.open(raw, "_blank", "noopener");
323
+ return;
324
+ }
325
+ }
326
+ const blob = buildArtifactBlob(artifact);
327
+ if (blob) {
328
+ saveBlob(blob, filename);
329
+ }
330
+ }
331
+
166
332
  // src/events.ts
167
333
  function applyChatEvent(store, event) {
168
334
  switch (event.type) {
@@ -806,6 +972,7 @@ import { useRenderer as useRenderer4 } from "@pactor-app/runtime";
806
972
  import {
807
973
  Component as Component3,
808
974
  isValidElement,
975
+ useMemo as useMemo4,
809
976
  useRef as useRef13,
810
977
  useState as useState15,
811
978
  useSyncExternalStore as useSyncExternalStore2
@@ -1054,6 +1221,12 @@ function ensureChatChromeActions(actions) {
1054
1221
  function invokeChatPanelClose() {
1055
1222
  handlers.panelClose?.();
1056
1223
  }
1224
+ function invokeChatPanelToggleList() {
1225
+ handlers.panelToggleList?.();
1226
+ }
1227
+ function invokeChatPanelDownload() {
1228
+ handlers.panelDownload?.();
1229
+ }
1057
1230
 
1058
1231
  // src/view/chrome/ChromeSlot.tsx
1059
1232
  import { Fragment, jsx as jsx2 } from "react/jsx-runtime";
@@ -1264,10 +1437,47 @@ function ChatPanelCloseButton() {
1264
1437
  }
1265
1438
  );
1266
1439
  }
1267
- var PANEL_HEADER_INTERNAL_COMPONENTS = { ChatPanelCloseButton };
1440
+ var PANEL_HEADER_INTERNAL_COMPONENTS = {
1441
+ ChatPanelCloseButton,
1442
+ ChatPanelDownloadButton,
1443
+ ChatPanelListButton
1444
+ };
1445
+ function ChatPanelListButton() {
1446
+ return /* @__PURE__ */ jsx4(
1447
+ "button",
1448
+ {
1449
+ type: "button",
1450
+ className: "pactor-chat-icon-button pactor-chat-inspector__list-toggle",
1451
+ title: "\u4EA7\u7269\u76EE\u5F55",
1452
+ "aria-label": "\u4EA7\u7269\u76EE\u5F55",
1453
+ onClick: () => invokeChatPanelToggleList(),
1454
+ children: /* @__PURE__ */ jsx4(Icon2, { name: "arrow-left", size: 16 })
1455
+ }
1456
+ );
1457
+ }
1458
+ function ChatPanelDownloadButton() {
1459
+ return /* @__PURE__ */ jsx4(
1460
+ "button",
1461
+ {
1462
+ type: "button",
1463
+ className: "pactor-chat-icon-button pactor-chat-inspector__download",
1464
+ title: "\u4E0B\u8F7D\u4EA7\u7269",
1465
+ "aria-label": "\u4E0B\u8F7D\u4EA7\u7269",
1466
+ onClick: () => invokeChatPanelDownload(),
1467
+ children: /* @__PURE__ */ jsx4(Icon2, { name: "download", size: 16 })
1468
+ }
1469
+ );
1470
+ }
1268
1471
  function buildPanelHeaderFragment(props) {
1269
1472
  return {
1270
- requires: ["Flex", "Text", "Badge", "ChatPanelCloseButton"],
1473
+ requires: [
1474
+ "Flex",
1475
+ "Text",
1476
+ "Badge",
1477
+ "ChatPanelCloseButton",
1478
+ "ChatPanelDownloadButton",
1479
+ "ChatPanelListButton"
1480
+ ],
1271
1481
  node: {
1272
1482
  type: "Flex",
1273
1483
  props: {
@@ -1281,6 +1491,7 @@ function buildPanelHeaderFragment(props) {
1281
1491
  type: "Flex",
1282
1492
  props: { align: "center", gap: "small" },
1283
1493
  children: [
1494
+ ...props.showListButton ? [{ type: "ChatPanelListButton" }] : [],
1284
1495
  {
1285
1496
  type: "Text",
1286
1497
  props: {
@@ -1299,7 +1510,14 @@ function buildPanelHeaderFragment(props) {
1299
1510
  }
1300
1511
  ]
1301
1512
  },
1302
- { type: "ChatPanelCloseButton" }
1513
+ {
1514
+ type: "Flex",
1515
+ props: { align: "center", gap: "small" },
1516
+ children: [
1517
+ ...props.showDownload ? [{ type: "ChatPanelDownloadButton" }] : [],
1518
+ { type: "ChatPanelCloseButton" }
1519
+ ]
1520
+ }
1303
1521
  ]
1304
1522
  }
1305
1523
  };
@@ -1350,10 +1568,126 @@ function ArtifactDslContent({
1350
1568
  const guarded = guardChatDsl(node, runtime.context.components);
1351
1569
  return /* @__PURE__ */ jsx5(DslRenderer2, { context: runtime.context, engine: runtime.engine, children: /* @__PURE__ */ jsx5(ArtifactDslGuard, { children: /* @__PURE__ */ jsx5(ArtifactDslNode, { node: guarded }) }) });
1352
1570
  }
1571
+ function ArtifactKindIcon({ kind }) {
1572
+ const name = kind === "code" ? "code" : kind === "dsl" ? "layout-grid" : "file-text";
1573
+ return /* @__PURE__ */ jsx5(Icon3, { name, size: 13 });
1574
+ }
1575
+ function buildArtifactTree(artifacts) {
1576
+ const root = { name: "", path: "", children: [] };
1577
+ for (const artifact of artifacts) {
1578
+ const segments = artifact.id.split("/").filter(Boolean);
1579
+ let node = root;
1580
+ for (let i = 0; i < segments.length - 1; i++) {
1581
+ const path = segments.slice(0, i + 1).join("/");
1582
+ let child = node.children.find(
1583
+ (item) => item.path === path && item.artifact === void 0
1584
+ );
1585
+ if (!child) {
1586
+ child = { name: segments[i], path, children: [] };
1587
+ node.children.push(child);
1588
+ }
1589
+ node = child;
1590
+ }
1591
+ node.children.push({
1592
+ name: artifact.title,
1593
+ path: artifact.id,
1594
+ artifact,
1595
+ children: []
1596
+ });
1597
+ }
1598
+ const sortLevel = (nodes) => {
1599
+ nodes.sort((a, b) => {
1600
+ const dirDelta = Number(b.artifact === void 0) - Number(a.artifact === void 0);
1601
+ return dirDelta !== 0 ? dirDelta : a.name.localeCompare(b.name);
1602
+ });
1603
+ for (const item of nodes) {
1604
+ sortLevel(item.children);
1605
+ }
1606
+ };
1607
+ sortLevel(root.children);
1608
+ return root.children;
1609
+ }
1610
+ function ArtifactListView({
1611
+ nodes,
1612
+ activeId,
1613
+ onSelect
1614
+ }) {
1615
+ return /* @__PURE__ */ jsx5("ul", { className: "pactor-chat-inspector__list", children: nodes.map((node) => /* @__PURE__ */ jsx5(
1616
+ ArtifactTreeRow,
1617
+ {
1618
+ node,
1619
+ activeId,
1620
+ onSelect
1621
+ },
1622
+ node.path
1623
+ )) });
1624
+ }
1625
+ function ArtifactTreeRow({
1626
+ node,
1627
+ activeId,
1628
+ onSelect
1629
+ }) {
1630
+ const [open, setOpen] = useState5(true);
1631
+ if (node.artifact) {
1632
+ return /* @__PURE__ */ jsx5("li", { children: /* @__PURE__ */ jsxs2(
1633
+ "button",
1634
+ {
1635
+ type: "button",
1636
+ className: "pactor-chat-inspector__list-item",
1637
+ "data-active": node.artifact.id === activeId,
1638
+ onClick: () => onSelect?.(node.artifact),
1639
+ children: [
1640
+ /* @__PURE__ */ jsx5(ArtifactKindIcon, { kind: node.artifact.kind }),
1641
+ /* @__PURE__ */ jsx5("span", { className: "pactor-chat-inspector__list-item-title", children: node.artifact.title }),
1642
+ /* @__PURE__ */ jsx5("span", { className: "pactor-chat-inspector__kind", children: node.artifact.kind })
1643
+ ]
1644
+ }
1645
+ ) });
1646
+ }
1647
+ return /* @__PURE__ */ jsxs2("li", { children: [
1648
+ /* @__PURE__ */ jsxs2(
1649
+ "button",
1650
+ {
1651
+ type: "button",
1652
+ className: "pactor-chat-inspector__list-folder",
1653
+ "aria-expanded": open,
1654
+ "data-open": open,
1655
+ onClick: () => setOpen((value) => !value),
1656
+ children: [
1657
+ /* @__PURE__ */ jsx5("span", { className: "pactor-chat-trace__chevron", children: /* @__PURE__ */ jsx5(Icon3, { name: "chevron-right", size: 12 }) }),
1658
+ /* @__PURE__ */ jsx5(Icon3, { name: "folder", size: 13 }),
1659
+ /* @__PURE__ */ jsx5("span", { children: node.name })
1660
+ ]
1661
+ }
1662
+ ),
1663
+ open && /* @__PURE__ */ jsx5("div", { className: "pactor-chat-inspector__list-children", children: /* @__PURE__ */ jsx5(
1664
+ ArtifactListView,
1665
+ {
1666
+ nodes: node.children,
1667
+ activeId,
1668
+ onSelect
1669
+ }
1670
+ ) })
1671
+ ] });
1672
+ }
1673
+ function ArtifactImageContent({ artifact }) {
1674
+ const raw = String(artifact.content).trim();
1675
+ const src = raw.startsWith("<svg") || raw.startsWith("<?xml") ? `data:image/svg+xml;utf8,${encodeURIComponent(raw)}` : raw;
1676
+ return /* @__PURE__ */ jsx5("div", { className: "pactor-chat-inspector__image", children: /* @__PURE__ */ jsx5("img", { src, alt: artifact.title }) });
1677
+ }
1353
1678
  function ArtifactContent({
1354
1679
  artifact,
1355
- runtime
1680
+ runtime,
1681
+ viewers
1356
1682
  }) {
1683
+ const ext = resolveArtifactFileType(artifact);
1684
+ const custom = (ext ? viewers?.[ext] : void 0) ?? viewers?.[artifact.kind];
1685
+ if (custom) {
1686
+ return /* @__PURE__ */ jsx5(Fragment3, { children: custom(artifact) });
1687
+ }
1688
+ if (isImageArtifact(artifact)) {
1689
+ return /* @__PURE__ */ jsx5(ArtifactImageContent, { artifact });
1690
+ }
1357
1691
  switch (artifact.kind) {
1358
1692
  case "markdown":
1359
1693
  return /* @__PURE__ */ jsx5(MarkdownRenderer2, { content: String(artifact.content) });
@@ -1372,17 +1706,41 @@ function ArtifactContent({
1372
1706
  }
1373
1707
  function ArtifactPanel({
1374
1708
  artifact,
1709
+ artifacts,
1710
+ activeArtifactId,
1711
+ onSelectArtifact,
1375
1712
  inspector,
1376
1713
  onClose,
1377
1714
  safeRegistry,
1378
1715
  user,
1716
+ artifactViewers,
1379
1717
  className
1380
1718
  }) {
1381
1719
  const runtime = useArtifactRuntime(safeRegistry, user);
1720
+ const [view, setView] = useState5("content");
1721
+ const activeId = activeArtifactId ?? artifact?.id;
1722
+ useEffect3(() => {
1723
+ setView("content");
1724
+ }, [activeId]);
1725
+ const showListButton = artifacts !== void 0 && artifacts.length > 0;
1726
+ const toggleListRef = useRef4(() => {
1727
+ });
1728
+ toggleListRef.current = () => setView((value) => value === "list" ? "content" : "list");
1382
1729
  const closeRef = useRef4(onClose);
1383
1730
  closeRef.current = onClose;
1731
+ const artifactRef = useRef4(artifact);
1732
+ artifactRef.current = artifact;
1384
1733
  useEffect3(() => {
1385
- setChatChromeHandlers({ panelClose: () => closeRef.current?.() });
1734
+ setChatChromeHandlers({
1735
+ panelClose: () => closeRef.current?.(),
1736
+ panelToggleList: () => toggleListRef.current(),
1737
+ panelDownload: () => {
1738
+ const current = artifactRef.current;
1739
+ if (current) {
1740
+ void downloadArtifact(current);
1741
+ }
1742
+ }
1743
+ });
1386
1744
  }, []);
1387
1745
  const panelRef = useRef4(null);
1388
1746
  const [width, setWidth] = useState5(null);
@@ -1401,7 +1759,7 @@ function ArtifactPanel({
1401
1759
  document.body.style.userSelect = "none";
1402
1760
  const onMove = (moveEvent) => {
1403
1761
  const next = startWidth + (startX - moveEvent.clientX);
1404
- setWidth(Math.round(Math.min(Math.max(next, 240), maxWidth)));
1762
+ setWidth(Math.round(Math.min(Math.max(next, 320), maxWidth)));
1405
1763
  };
1406
1764
  const onUp = () => {
1407
1765
  document.body.style.userSelect = prevUserSelect;
@@ -1434,12 +1792,36 @@ function ArtifactPanel({
1434
1792
  {
1435
1793
  fragment: buildPanelHeaderFragment({
1436
1794
  title: artifact?.title ?? "\u4EA7\u51FA\u7269",
1437
- kind: artifact?.kind ?? "text"
1795
+ kind: artifact?.kind ?? "text",
1796
+ showListButton,
1797
+ showDownload: artifact != null
1438
1798
  }),
1439
1799
  components: PANEL_HEADER_INTERNAL_COMPONENTS,
1440
1800
  fallback: /* @__PURE__ */ jsxs2(Fragment3, { children: [
1801
+ showListButton ? /* @__PURE__ */ jsx5(
1802
+ "button",
1803
+ {
1804
+ type: "button",
1805
+ className: "pactor-chat-icon-button pactor-chat-inspector__list-toggle",
1806
+ title: "\u4EA7\u7269\u76EE\u5F55",
1807
+ "aria-label": "\u4EA7\u7269\u76EE\u5F55",
1808
+ onClick: () => toggleListRef.current(),
1809
+ children: /* @__PURE__ */ jsx5(Icon3, { name: "arrow-left", size: 16, strokeWidth: 1.7 })
1810
+ }
1811
+ ) : null,
1441
1812
  /* @__PURE__ */ jsx5("span", { className: "pactor-chat-inspector__title", children: artifact?.title ?? "\u4EA7\u51FA\u7269" }),
1442
1813
  artifact ? /* @__PURE__ */ jsx5("span", { className: "pactor-chat-inspector__kind", children: artifact.kind }) : null,
1814
+ artifact ? /* @__PURE__ */ jsx5(
1815
+ "button",
1816
+ {
1817
+ type: "button",
1818
+ className: "pactor-chat-icon-button pactor-chat-inspector__download",
1819
+ title: "\u4E0B\u8F7D\u4EA7\u7269",
1820
+ "aria-label": "\u4E0B\u8F7D\u4EA7\u7269",
1821
+ onClick: () => void downloadArtifact(artifact),
1822
+ children: /* @__PURE__ */ jsx5(Icon3, { name: "download", size: 16, strokeWidth: 1.7 })
1823
+ }
1824
+ ) : null,
1443
1825
  /* @__PURE__ */ jsx5(
1444
1826
  "button",
1445
1827
  {
@@ -1453,7 +1835,38 @@ function ArtifactPanel({
1453
1835
  ] })
1454
1836
  }
1455
1837
  ) }),
1456
- /* @__PURE__ */ jsx5("div", { className: "pactor-chat-inspector__body", children: inspector !== void 0 && inspector !== null ? inspector : artifact ? /* @__PURE__ */ jsx5(ArtifactContent, { artifact, runtime }) : null })
1838
+ artifacts !== void 0 && artifacts.length > 1 ? /* @__PURE__ */ jsx5("div", { className: "pactor-chat-inspector__tabs", role: "tablist", children: artifacts.map((item) => /* @__PURE__ */ jsxs2(
1839
+ "button",
1840
+ {
1841
+ type: "button",
1842
+ role: "tab",
1843
+ "aria-selected": item.id === activeId,
1844
+ "data-active": item.id === activeId,
1845
+ className: "pactor-chat-inspector__tab",
1846
+ title: item.title,
1847
+ onClick: () => onSelectArtifact?.(item),
1848
+ children: [
1849
+ /* @__PURE__ */ jsx5(ArtifactKindIcon, { kind: item.kind }),
1850
+ /* @__PURE__ */ jsx5("span", { className: "pactor-chat-inspector__tab-title", children: item.title })
1851
+ ]
1852
+ },
1853
+ item.id
1854
+ )) }) : null,
1855
+ /* @__PURE__ */ jsx5("div", { className: "pactor-chat-inspector__body", children: view === "list" && artifacts !== void 0 ? /* @__PURE__ */ jsx5(
1856
+ ArtifactListView,
1857
+ {
1858
+ nodes: buildArtifactTree(artifacts),
1859
+ activeId,
1860
+ onSelect: onSelectArtifact
1861
+ }
1862
+ ) : inspector !== void 0 && inspector !== null ? inspector : artifact ? /* @__PURE__ */ jsx5(
1863
+ ArtifactContent,
1864
+ {
1865
+ artifact,
1866
+ runtime,
1867
+ viewers: artifactViewers
1868
+ }
1869
+ ) : null })
1457
1870
  ]
1458
1871
  }
1459
1872
  );
@@ -2484,7 +2897,11 @@ import { useEffect as useEffect7, useRef as useRef8, useState as useState10 } fr
2484
2897
  import { Fragment as Fragment8, jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
2485
2898
  function usePinnedCollapse(autoOpen) {
2486
2899
  const [state, setState] = useState10("auto");
2487
- const open = state === "auto" ? autoOpen : state === "pinned-open";
2900
+ const autoCollapsedRef = useRef8(false);
2901
+ if (state === "auto" && !autoOpen) {
2902
+ autoCollapsedRef.current = true;
2903
+ }
2904
+ const open = state === "auto" ? autoOpen && !autoCollapsedRef.current : state === "pinned-open";
2488
2905
  const toggle = () => {
2489
2906
  setState(open ? "pinned-closed" : "pinned-open");
2490
2907
  };
@@ -2635,6 +3052,7 @@ function ActivityTrace({ message }) {
2635
3052
  );
2636
3053
  const answerPhase = mode === "responding" || !streaming;
2637
3054
  const { open, toggle } = usePinnedCollapse(!answerPhase);
3055
+ const collapsible = entries.length > 0 || thinkingParts.length > 0;
2638
3056
  const mountTimeRef = useRef8(0);
2639
3057
  if (mountTimeRef.current === 0) {
2640
3058
  mountTimeRef.current = Date.now();
@@ -2656,10 +3074,10 @@ function ActivityTrace({ message }) {
2656
3074
  elapsed,
2657
3075
  "s"
2658
3076
  ] }),
2659
- entries.length > 0 && /* @__PURE__ */ jsx12("span", { className: "pactor-chat-trace__chevron", children: /* @__PURE__ */ jsx12(Icon7, { name: "chevron-right", size: 12 }) })
3077
+ collapsible && /* @__PURE__ */ jsx12("span", { className: "pactor-chat-trace__chevron", children: /* @__PURE__ */ jsx12(Icon7, { name: "chevron-right", size: 12 }) })
2660
3078
  ] });
2661
3079
  return /* @__PURE__ */ jsxs8("div", { className: "pactor-chat-trace", "data-open": open, children: [
2662
- entries.length > 0 ? /* @__PURE__ */ jsx12(
3080
+ collapsible ? /* @__PURE__ */ jsx12(
2663
3081
  "button",
2664
3082
  {
2665
3083
  type: "button",
@@ -2669,15 +3087,17 @@ function ActivityTrace({ message }) {
2669
3087
  children: header
2670
3088
  }
2671
3089
  ) : /* @__PURE__ */ jsx12("div", { className: "pactor-chat-trace__header", children: header }),
2672
- thinkingParts.map((part, index) => /* @__PURE__ */ jsx12(
2673
- ThinkingCard,
2674
- {
2675
- part,
2676
- answerPhase
2677
- },
2678
- part.id ?? index
2679
- )),
2680
- open && entries.length > 0 && /* @__PURE__ */ jsx12("div", { className: "pactor-chat-trace__entries", children: entries.map((entry) => /* @__PURE__ */ jsx12(TraceRow, { entry }, entry.callIndex)) })
3090
+ open && /* @__PURE__ */ jsxs8(Fragment8, { children: [
3091
+ entries.length > 0 && /* @__PURE__ */ jsx12("div", { className: "pactor-chat-trace__entries", children: entries.map((entry) => /* @__PURE__ */ jsx12(TraceRow, { entry }, entry.callIndex)) }),
3092
+ thinkingParts.map((part, index) => /* @__PURE__ */ jsx12(
3093
+ ThinkingCard,
3094
+ {
3095
+ part,
3096
+ answerPhase
3097
+ },
3098
+ part.id ?? index
3099
+ ))
3100
+ ] })
2681
3101
  ] });
2682
3102
  }
2683
3103
 
@@ -3626,6 +4046,8 @@ function ChatComposition({
3626
4046
  onSend,
3627
4047
  artifact,
3628
4048
  panelOpen,
4049
+ artifacts: artifactsProp,
4050
+ onSelectArtifact,
3629
4051
  onPanelClose,
3630
4052
  inspector,
3631
4053
  onArtifact,
@@ -3646,8 +4068,28 @@ function ChatComposition({
3646
4068
  (listener) => artifactStore.subscribe(listener),
3647
4069
  () => artifactStore.get()
3648
4070
  );
4071
+ const storeArtifacts = useSyncExternalStore2(
4072
+ (listener) => artifactStore.subscribe(listener),
4073
+ () => artifactStore.list()
4074
+ );
4075
+ const messages = useSyncExternalStore2(
4076
+ (listener) => store.subscribe(listener),
4077
+ () => store.getMessages()
4078
+ );
4079
+ const streamArtifacts = useMemo4(() => collectArtifacts(messages), [messages]);
4080
+ const mergedArtifacts = useMemo4(
4081
+ () => mergeArtifacts(streamArtifacts, storeArtifacts),
4082
+ [streamArtifacts, storeArtifacts]
4083
+ );
3649
4084
  const effectiveArtifact = panelControlled ? artifact ?? null : storeArtifact;
3650
4085
  const effectivePanelOpen = panelControlled ? panelOpen ?? artifact != null : storeArtifact !== null;
4086
+ const effectiveArtifacts = artifactsProp ?? mergedArtifacts;
4087
+ const handleSelectArtifact = (next) => {
4088
+ onSelectArtifact?.(next);
4089
+ if (!panelControlled) {
4090
+ artifactStore.open(next);
4091
+ }
4092
+ };
3651
4093
  const handlePanelClose = () => {
3652
4094
  onPanelClose?.();
3653
4095
  if (!panelControlled) {
@@ -3789,10 +4231,14 @@ function ChatComposition({
3789
4231
  ArtifactPanel,
3790
4232
  {
3791
4233
  artifact: effectiveArtifact,
4234
+ artifacts: effectiveArtifacts,
4235
+ activeArtifactId: effectiveArtifact?.id,
4236
+ onSelectArtifact: handleSelectArtifact,
3792
4237
  inspector,
3793
4238
  onClose: handlePanelClose,
3794
4239
  safeRegistry: services.safeRegistry,
3795
- user: services.user
4240
+ user: services.user,
4241
+ artifactViewers: services.artifactViewers
3796
4242
  }
3797
4243
  )
3798
4244
  ] }) : null
@@ -3835,6 +4281,7 @@ function createChatWorkspace(services) {
3835
4281
  inspector,
3836
4282
  onArtifact,
3837
4283
  onOpenArtifact,
4284
+ onSelectArtifact,
3838
4285
  headerActions,
3839
4286
  editableTitle,
3840
4287
  onExport,
@@ -3875,6 +4322,7 @@ function createChatWorkspace(services) {
3875
4322
  inspector: inspector ? /* @__PURE__ */ jsx19(DslNodeSlot, { node: inspector }) : void 0,
3876
4323
  onArtifact,
3877
4324
  onOpenArtifact,
4325
+ onSelectArtifact,
3878
4326
  headerActions,
3879
4327
  editableTitle,
3880
4328
  onExport,
@@ -4084,6 +4532,7 @@ function Chat({
4084
4532
  inspector,
4085
4533
  onArtifact,
4086
4534
  onOpenArtifact,
4535
+ onSelectArtifact,
4087
4536
  className,
4088
4537
  onSend,
4089
4538
  onMenuSelect
@@ -4148,7 +4597,8 @@ function Chat({
4148
4597
  onPanelClose,
4149
4598
  inspector: resolveSlot(inspector),
4150
4599
  onArtifact,
4151
- onOpenArtifact
4600
+ onOpenArtifact,
4601
+ onSelectArtifact
4152
4602
  }
4153
4603
  );
4154
4604
  }
@@ -4159,6 +4609,7 @@ function ChatDslAdapter({
4159
4609
  onPanelClose,
4160
4610
  onArtifact,
4161
4611
  onOpenArtifact,
4612
+ onSelectArtifact,
4162
4613
  onModelChange,
4163
4614
  onContextsChange,
4164
4615
  onMessageAction,
@@ -4177,6 +4628,7 @@ function ChatDslAdapter({
4177
4628
  onPanelClose: onPanelClose ? () => onPanelClose({}) : void 0,
4178
4629
  onArtifact: onArtifact ? (artifact) => onArtifact({ artifact }) : void 0,
4179
4630
  onOpenArtifact: onOpenArtifact ? (artifact) => onOpenArtifact({ artifact }) : void 0,
4631
+ onSelectArtifact: onSelectArtifact ? (artifact) => onSelectArtifact({ artifact }) : void 0,
4180
4632
  onModelChange: onModelChange ? (model) => onModelChange({ model }) : void 0,
4181
4633
  onContextsChange: onContextsChange ? (contexts) => onContextsChange({ contexts }) : void 0,
4182
4634
  onMessageAction: onMessageAction ? (action, message) => onMessageAction({ action, message }) : void 0,
@@ -4214,6 +4666,7 @@ function registerChatLayout(layoutRegistry) {
4214
4666
  }
4215
4667
  }
4216
4668
  export {
4669
+ ARTIFACT_IMAGE_EXTENSIONS,
4217
4670
  ActivityTrace,
4218
4671
  AppShell,
4219
4672
  ArtifactPanel,
@@ -4237,7 +4690,10 @@ export {
4237
4690
  MessageBubble,
4238
4691
  PartRenderer,
4239
4692
  applyChatEvent,
4693
+ buildArtifactBlob,
4694
+ buildArtifactTree,
4240
4695
  buildChatScope,
4696
+ collectArtifacts,
4241
4697
  collectMessageText,
4242
4698
  createChatComponentRegistry,
4243
4699
  createChatId,
@@ -4246,14 +4702,20 @@ export {
4246
4702
  createManualDriver,
4247
4703
  createSafeRegistry,
4248
4704
  detectStreamingMode,
4705
+ downloadArtifact,
4249
4706
  guardChatDsl,
4707
+ isImageArtifact,
4708
+ mergeArtifacts,
4250
4709
  pairToolTraces,
4251
4710
  registerCapabilityAction,
4252
4711
  registerChatComponent,
4253
4712
  registerChatLayout,
4254
4713
  registerChatShellComponent,
4714
+ resolveArtifactFileType,
4715
+ resolveArtifactFilename,
4255
4716
  runChatTurn,
4256
4717
  runRegenerateTurn,
4718
+ saveBlob,
4257
4719
  useChatAutoScroll,
4258
4720
  useChatServices,
4259
4721
  useSidebarDrawer,