@pactor-app/chat 1.15.6 → 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;
133
195
  this.notify();
134
196
  }
135
- /** 部分更新当前产出物(id 不可改);未 open 时告警并忽略 */
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;
207
+ this.notify();
208
+ }
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
  };
@@ -3629,6 +4046,8 @@ function ChatComposition({
3629
4046
  onSend,
3630
4047
  artifact,
3631
4048
  panelOpen,
4049
+ artifacts: artifactsProp,
4050
+ onSelectArtifact,
3632
4051
  onPanelClose,
3633
4052
  inspector,
3634
4053
  onArtifact,
@@ -3649,8 +4068,28 @@ function ChatComposition({
3649
4068
  (listener) => artifactStore.subscribe(listener),
3650
4069
  () => artifactStore.get()
3651
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
+ );
3652
4084
  const effectiveArtifact = panelControlled ? artifact ?? null : storeArtifact;
3653
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
+ };
3654
4093
  const handlePanelClose = () => {
3655
4094
  onPanelClose?.();
3656
4095
  if (!panelControlled) {
@@ -3792,10 +4231,14 @@ function ChatComposition({
3792
4231
  ArtifactPanel,
3793
4232
  {
3794
4233
  artifact: effectiveArtifact,
4234
+ artifacts: effectiveArtifacts,
4235
+ activeArtifactId: effectiveArtifact?.id,
4236
+ onSelectArtifact: handleSelectArtifact,
3795
4237
  inspector,
3796
4238
  onClose: handlePanelClose,
3797
4239
  safeRegistry: services.safeRegistry,
3798
- user: services.user
4240
+ user: services.user,
4241
+ artifactViewers: services.artifactViewers
3799
4242
  }
3800
4243
  )
3801
4244
  ] }) : null
@@ -3838,6 +4281,7 @@ function createChatWorkspace(services) {
3838
4281
  inspector,
3839
4282
  onArtifact,
3840
4283
  onOpenArtifact,
4284
+ onSelectArtifact,
3841
4285
  headerActions,
3842
4286
  editableTitle,
3843
4287
  onExport,
@@ -3878,6 +4322,7 @@ function createChatWorkspace(services) {
3878
4322
  inspector: inspector ? /* @__PURE__ */ jsx19(DslNodeSlot, { node: inspector }) : void 0,
3879
4323
  onArtifact,
3880
4324
  onOpenArtifact,
4325
+ onSelectArtifact,
3881
4326
  headerActions,
3882
4327
  editableTitle,
3883
4328
  onExport,
@@ -4087,6 +4532,7 @@ function Chat({
4087
4532
  inspector,
4088
4533
  onArtifact,
4089
4534
  onOpenArtifact,
4535
+ onSelectArtifact,
4090
4536
  className,
4091
4537
  onSend,
4092
4538
  onMenuSelect
@@ -4151,7 +4597,8 @@ function Chat({
4151
4597
  onPanelClose,
4152
4598
  inspector: resolveSlot(inspector),
4153
4599
  onArtifact,
4154
- onOpenArtifact
4600
+ onOpenArtifact,
4601
+ onSelectArtifact
4155
4602
  }
4156
4603
  );
4157
4604
  }
@@ -4162,6 +4609,7 @@ function ChatDslAdapter({
4162
4609
  onPanelClose,
4163
4610
  onArtifact,
4164
4611
  onOpenArtifact,
4612
+ onSelectArtifact,
4165
4613
  onModelChange,
4166
4614
  onContextsChange,
4167
4615
  onMessageAction,
@@ -4180,6 +4628,7 @@ function ChatDslAdapter({
4180
4628
  onPanelClose: onPanelClose ? () => onPanelClose({}) : void 0,
4181
4629
  onArtifact: onArtifact ? (artifact) => onArtifact({ artifact }) : void 0,
4182
4630
  onOpenArtifact: onOpenArtifact ? (artifact) => onOpenArtifact({ artifact }) : void 0,
4631
+ onSelectArtifact: onSelectArtifact ? (artifact) => onSelectArtifact({ artifact }) : void 0,
4183
4632
  onModelChange: onModelChange ? (model) => onModelChange({ model }) : void 0,
4184
4633
  onContextsChange: onContextsChange ? (contexts) => onContextsChange({ contexts }) : void 0,
4185
4634
  onMessageAction: onMessageAction ? (action, message) => onMessageAction({ action, message }) : void 0,
@@ -4217,6 +4666,7 @@ function registerChatLayout(layoutRegistry) {
4217
4666
  }
4218
4667
  }
4219
4668
  export {
4669
+ ARTIFACT_IMAGE_EXTENSIONS,
4220
4670
  ActivityTrace,
4221
4671
  AppShell,
4222
4672
  ArtifactPanel,
@@ -4240,7 +4690,10 @@ export {
4240
4690
  MessageBubble,
4241
4691
  PartRenderer,
4242
4692
  applyChatEvent,
4693
+ buildArtifactBlob,
4694
+ buildArtifactTree,
4243
4695
  buildChatScope,
4696
+ collectArtifacts,
4244
4697
  collectMessageText,
4245
4698
  createChatComponentRegistry,
4246
4699
  createChatId,
@@ -4249,14 +4702,20 @@ export {
4249
4702
  createManualDriver,
4250
4703
  createSafeRegistry,
4251
4704
  detectStreamingMode,
4705
+ downloadArtifact,
4252
4706
  guardChatDsl,
4707
+ isImageArtifact,
4708
+ mergeArtifacts,
4253
4709
  pairToolTraces,
4254
4710
  registerCapabilityAction,
4255
4711
  registerChatComponent,
4256
4712
  registerChatLayout,
4257
4713
  registerChatShellComponent,
4714
+ resolveArtifactFileType,
4715
+ resolveArtifactFilename,
4258
4716
  runChatTurn,
4259
4717
  runRegenerateTurn,
4718
+ saveBlob,
4260
4719
  useChatAutoScroll,
4261
4720
  useChatServices,
4262
4721
  useSidebarDrawer,