@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.cjs CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ ARTIFACT_IMAGE_EXTENSIONS: () => ARTIFACT_IMAGE_EXTENSIONS,
23
24
  ActivityTrace: () => ActivityTrace,
24
25
  AppShell: () => AppShell,
25
26
  ArtifactPanel: () => ArtifactPanel,
@@ -43,7 +44,10 @@ __export(index_exports, {
43
44
  MessageBubble: () => MessageBubble,
44
45
  PartRenderer: () => PartRenderer,
45
46
  applyChatEvent: () => applyChatEvent,
47
+ buildArtifactBlob: () => buildArtifactBlob,
48
+ buildArtifactTree: () => buildArtifactTree,
46
49
  buildChatScope: () => buildChatScope,
50
+ collectArtifacts: () => collectArtifacts,
47
51
  collectMessageText: () => collectMessageText,
48
52
  createChatComponentRegistry: () => createChatComponentRegistry,
49
53
  createChatId: () => createChatId,
@@ -52,14 +56,20 @@ __export(index_exports, {
52
56
  createManualDriver: () => createManualDriver,
53
57
  createSafeRegistry: () => createSafeRegistry,
54
58
  detectStreamingMode: () => detectStreamingMode,
59
+ downloadArtifact: () => downloadArtifact,
55
60
  guardChatDsl: () => guardChatDsl,
61
+ isImageArtifact: () => isImageArtifact,
62
+ mergeArtifacts: () => mergeArtifacts,
56
63
  pairToolTraces: () => pairToolTraces,
57
64
  registerCapabilityAction: () => registerCapabilityAction,
58
65
  registerChatComponent: () => registerChatComponent,
59
66
  registerChatLayout: () => registerChatLayout,
60
67
  registerChatShellComponent: () => registerChatShellComponent,
68
+ resolveArtifactFileType: () => resolveArtifactFileType,
69
+ resolveArtifactFilename: () => resolveArtifactFilename,
61
70
  runChatTurn: () => runChatTurn,
62
71
  runRegenerateTurn: () => runRegenerateTurn,
72
+ saveBlob: () => saveBlob,
63
73
  useChatAutoScroll: () => useChatAutoScroll,
64
74
  useChatServices: () => useChatServices,
65
75
  useSidebarDrawer: () => useSidebarDrawer,
@@ -187,35 +197,113 @@ var ConversationStore = class {
187
197
  };
188
198
 
189
199
  // src/artifact.ts
200
+ var ARTIFACT_IMAGE_EXTENSIONS = [
201
+ "png",
202
+ "jpg",
203
+ "jpeg",
204
+ "gif",
205
+ "svg",
206
+ "webp",
207
+ "avif",
208
+ "bmp",
209
+ "ico"
210
+ ];
211
+ function resolveArtifactFileType(artifact) {
212
+ const declared = artifact.fileType?.trim().toLowerCase().replace(/^\./, "");
213
+ if (declared) {
214
+ return declared;
215
+ }
216
+ const match = /\.([A-Za-z0-9]+)$/.exec(artifact.title.trim());
217
+ return match ? match[1].toLowerCase() : null;
218
+ }
219
+ function isImageArtifact(artifact) {
220
+ const ext = resolveArtifactFileType(artifact);
221
+ return ext !== null && ARTIFACT_IMAGE_EXTENSIONS.includes(ext);
222
+ }
223
+ function collectArtifacts(messages) {
224
+ const order = [];
225
+ const byId = /* @__PURE__ */ new Map();
226
+ for (const message of messages) {
227
+ for (const part of message.parts) {
228
+ if (part.type !== "artifact" || !part.artifact) {
229
+ continue;
230
+ }
231
+ if (part.status !== void 0 && part.status !== "ready") {
232
+ continue;
233
+ }
234
+ if (!byId.has(part.artifact.id)) {
235
+ order.push(part.artifact.id);
236
+ }
237
+ byId.set(part.artifact.id, part.artifact);
238
+ }
239
+ }
240
+ return order.map((id) => byId.get(id));
241
+ }
242
+ function mergeArtifacts(stream, opened) {
243
+ const openedById = new Map(opened.map((item) => [item.id, item]));
244
+ const merged = stream.map((item) => openedById.get(item.id) ?? item);
245
+ for (const item of opened) {
246
+ if (!stream.some((s) => s.id === item.id)) {
247
+ merged.push(item);
248
+ }
249
+ }
250
+ return merged;
251
+ }
190
252
  var ArtifactStore = class {
191
253
  constructor() {
192
- this.artifact = null;
254
+ this.artifacts = [];
255
+ this.activeId = null;
193
256
  this.listeners = /* @__PURE__ */ new Set();
194
257
  }
195
- /** 当前产出物(未打开时为 null;引用在不变时保持稳定) */
258
+ /** 当前激活产出物(未打开时为 null;引用在不变时保持稳定) */
196
259
  get() {
197
- return this.artifact;
260
+ if (this.activeId === null) {
261
+ return null;
262
+ }
263
+ return this.artifacts.find((item) => item.id === this.activeId) ?? null;
198
264
  }
199
- /** 打开(或替换)当前产出物 */
265
+ /** 全部产出物(插入序;数组不可变,引用稳定) */
266
+ list() {
267
+ return this.artifacts;
268
+ }
269
+ /** 打开(或按 id 替换)产出物并激活 */
200
270
  open(artifact) {
201
- this.artifact = artifact;
271
+ const index = this.artifacts.findIndex((item) => item.id === artifact.id);
272
+ this.artifacts = index === -1 ? [...this.artifacts, artifact] : this.artifacts.map((item, i) => i === index ? artifact : item);
273
+ this.activeId = artifact.id;
202
274
  this.notify();
203
275
  }
204
- /** 部分更新当前产出物(id 不可改);未 open 时告警并忽略 */
276
+ /** 切换激活项(未知 id 告警并忽略,不抛错) */
277
+ activate(id) {
278
+ if (this.activeId === id) {
279
+ return;
280
+ }
281
+ if (!this.artifacts.some((item) => item.id === id)) {
282
+ console.warn("[@pactor-app/chat] unknown artifact id, activate ignored");
283
+ return;
284
+ }
285
+ this.activeId = id;
286
+ this.notify();
287
+ }
288
+ /** 部分更新当前激活产出物(id 不可改);未 open 时告警并忽略 */
205
289
  update(patch) {
206
- if (!this.artifact) {
290
+ const current = this.get();
291
+ if (!current) {
207
292
  console.warn("[@pactor-app/chat] artifact not open, update ignored");
208
293
  return;
209
294
  }
210
- this.artifact = { ...this.artifact, ...patch, id: this.artifact.id };
295
+ const next = { ...current, ...patch, id: current.id };
296
+ this.artifacts = this.artifacts.map(
297
+ (item) => item.id === current.id ? next : item
298
+ );
211
299
  this.notify();
212
300
  }
213
- /** 关闭面板(未打开时幂等,不重复通知) */
301
+ /** 关闭面板:清空激活项,列表保留(未打开时幂等,不重复通知) */
214
302
  close() {
215
- if (this.artifact === null) {
303
+ if (this.activeId === null) {
216
304
  return;
217
305
  }
218
- this.artifact = null;
306
+ this.activeId = null;
219
307
  this.notify();
220
308
  }
221
309
  /** 订阅变更;返回退订函数 */
@@ -232,6 +320,94 @@ var ArtifactStore = class {
232
320
  }
233
321
  };
234
322
 
323
+ // src/artifact-download.ts
324
+ var KIND_EXTENSIONS = {
325
+ markdown: "md",
326
+ text: "txt",
327
+ code: "txt",
328
+ dsl: "json"
329
+ };
330
+ function resolveArtifactFilename(artifact) {
331
+ const title = artifact.title.trim() || "artifact";
332
+ if (/\.[A-Za-z0-9]+$/.test(title)) {
333
+ return title;
334
+ }
335
+ const ext = resolveArtifactFileType(artifact) ?? KIND_EXTENSIONS[artifact.kind];
336
+ return `${title}.${ext}`;
337
+ }
338
+ function dataUrlToBlob(dataUrl) {
339
+ const comma = dataUrl.indexOf(",");
340
+ const meta = dataUrl.slice(0, comma);
341
+ const payload = dataUrl.slice(comma + 1);
342
+ const mime = /^data:([^;,]+)/.exec(meta)?.[1] ?? "application/octet-stream";
343
+ if (meta.includes(";base64")) {
344
+ const binary = atob(payload);
345
+ const bytes = new Uint8Array(binary.length);
346
+ for (let i = 0; i < binary.length; i++) {
347
+ bytes[i] = binary.charCodeAt(i);
348
+ }
349
+ return new Blob([bytes], { type: mime });
350
+ }
351
+ return new Blob([decodeURIComponent(payload)], { type: mime });
352
+ }
353
+ function isRemoteUrl(raw) {
354
+ return /^https?:\/\//.test(raw.trim());
355
+ }
356
+ function buildArtifactBlob(artifact) {
357
+ if (artifact.kind === "dsl" && typeof artifact.content === "object") {
358
+ return new Blob([JSON.stringify(artifact.content, null, 2)], {
359
+ type: "application/json"
360
+ });
361
+ }
362
+ const raw = String(artifact.content);
363
+ if (isImageArtifact(artifact)) {
364
+ const trimmed = raw.trim();
365
+ if (trimmed.startsWith("data:")) {
366
+ return dataUrlToBlob(trimmed);
367
+ }
368
+ if (trimmed.startsWith("<svg") || trimmed.startsWith("<?xml")) {
369
+ return new Blob([raw], { type: "image/svg+xml" });
370
+ }
371
+ if (isRemoteUrl(trimmed)) {
372
+ return null;
373
+ }
374
+ }
375
+ const ext = resolveArtifactFileType(artifact);
376
+ const mime = ext === "md" || artifact.kind === "markdown" ? "text/markdown" : "text/plain";
377
+ return new Blob([raw], { type: `${mime};charset=utf-8` });
378
+ }
379
+ function saveBlob(blob, filename) {
380
+ const url = URL.createObjectURL(blob);
381
+ const anchor = document.createElement("a");
382
+ anchor.href = url;
383
+ anchor.download = filename;
384
+ document.body.appendChild(anchor);
385
+ anchor.click();
386
+ anchor.remove();
387
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
388
+ }
389
+ async function downloadArtifact(artifact) {
390
+ const filename = resolveArtifactFilename(artifact);
391
+ const raw = String(artifact.content ?? "").trim();
392
+ if (isImageArtifact(artifact) && isRemoteUrl(raw)) {
393
+ try {
394
+ const response = await fetch(raw);
395
+ if (!response.ok) {
396
+ throw new Error(`HTTP ${response.status}`);
397
+ }
398
+ saveBlob(await response.blob(), filename);
399
+ return;
400
+ } catch {
401
+ window.open(raw, "_blank", "noopener");
402
+ return;
403
+ }
404
+ }
405
+ const blob = buildArtifactBlob(artifact);
406
+ if (blob) {
407
+ saveBlob(blob, filename);
408
+ }
409
+ }
410
+
235
411
  // src/events.ts
236
412
  function applyChatEvent(store, event) {
237
413
  switch (event.type) {
@@ -1104,6 +1280,12 @@ function ensureChatChromeActions(actions) {
1104
1280
  function invokeChatPanelClose() {
1105
1281
  handlers.panelClose?.();
1106
1282
  }
1283
+ function invokeChatPanelToggleList() {
1284
+ handlers.panelToggleList?.();
1285
+ }
1286
+ function invokeChatPanelDownload() {
1287
+ handlers.panelDownload?.();
1288
+ }
1107
1289
 
1108
1290
  // src/view/chrome/ChromeSlot.tsx
1109
1291
  var import_jsx_runtime2 = require("react/jsx-runtime");
@@ -1314,10 +1496,47 @@ function ChatPanelCloseButton() {
1314
1496
  }
1315
1497
  );
1316
1498
  }
1317
- var PANEL_HEADER_INTERNAL_COMPONENTS = { ChatPanelCloseButton };
1499
+ var PANEL_HEADER_INTERNAL_COMPONENTS = {
1500
+ ChatPanelCloseButton,
1501
+ ChatPanelDownloadButton,
1502
+ ChatPanelListButton
1503
+ };
1504
+ function ChatPanelListButton() {
1505
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1506
+ "button",
1507
+ {
1508
+ type: "button",
1509
+ className: "pactor-chat-icon-button pactor-chat-inspector__list-toggle",
1510
+ title: "\u4EA7\u7269\u76EE\u5F55",
1511
+ "aria-label": "\u4EA7\u7269\u76EE\u5F55",
1512
+ onClick: () => invokeChatPanelToggleList(),
1513
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_components3.Icon, { name: "arrow-left", size: 16 })
1514
+ }
1515
+ );
1516
+ }
1517
+ function ChatPanelDownloadButton() {
1518
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1519
+ "button",
1520
+ {
1521
+ type: "button",
1522
+ className: "pactor-chat-icon-button pactor-chat-inspector__download",
1523
+ title: "\u4E0B\u8F7D\u4EA7\u7269",
1524
+ "aria-label": "\u4E0B\u8F7D\u4EA7\u7269",
1525
+ onClick: () => invokeChatPanelDownload(),
1526
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_components3.Icon, { name: "download", size: 16 })
1527
+ }
1528
+ );
1529
+ }
1318
1530
  function buildPanelHeaderFragment(props) {
1319
1531
  return {
1320
- requires: ["Flex", "Text", "Badge", "ChatPanelCloseButton"],
1532
+ requires: [
1533
+ "Flex",
1534
+ "Text",
1535
+ "Badge",
1536
+ "ChatPanelCloseButton",
1537
+ "ChatPanelDownloadButton",
1538
+ "ChatPanelListButton"
1539
+ ],
1321
1540
  node: {
1322
1541
  type: "Flex",
1323
1542
  props: {
@@ -1331,6 +1550,7 @@ function buildPanelHeaderFragment(props) {
1331
1550
  type: "Flex",
1332
1551
  props: { align: "center", gap: "small" },
1333
1552
  children: [
1553
+ ...props.showListButton ? [{ type: "ChatPanelListButton" }] : [],
1334
1554
  {
1335
1555
  type: "Text",
1336
1556
  props: {
@@ -1349,7 +1569,14 @@ function buildPanelHeaderFragment(props) {
1349
1569
  }
1350
1570
  ]
1351
1571
  },
1352
- { type: "ChatPanelCloseButton" }
1572
+ {
1573
+ type: "Flex",
1574
+ props: { align: "center", gap: "small" },
1575
+ children: [
1576
+ ...props.showDownload ? [{ type: "ChatPanelDownloadButton" }] : [],
1577
+ { type: "ChatPanelCloseButton" }
1578
+ ]
1579
+ }
1353
1580
  ]
1354
1581
  }
1355
1582
  };
@@ -1400,10 +1627,126 @@ function ArtifactDslContent({
1400
1627
  const guarded = guardChatDsl(node, runtime.context.components);
1401
1628
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_runtime4.DslRenderer, { context: runtime.context, engine: runtime.engine, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ArtifactDslGuard, { children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ArtifactDslNode, { node: guarded }) }) });
1402
1629
  }
1630
+ function ArtifactKindIcon({ kind }) {
1631
+ const name = kind === "code" ? "code" : kind === "dsl" ? "layout-grid" : "file-text";
1632
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_components4.Icon, { name, size: 13 });
1633
+ }
1634
+ function buildArtifactTree(artifacts) {
1635
+ const root = { name: "", path: "", children: [] };
1636
+ for (const artifact of artifacts) {
1637
+ const segments = artifact.id.split("/").filter(Boolean);
1638
+ let node = root;
1639
+ for (let i = 0; i < segments.length - 1; i++) {
1640
+ const path = segments.slice(0, i + 1).join("/");
1641
+ let child = node.children.find(
1642
+ (item) => item.path === path && item.artifact === void 0
1643
+ );
1644
+ if (!child) {
1645
+ child = { name: segments[i], path, children: [] };
1646
+ node.children.push(child);
1647
+ }
1648
+ node = child;
1649
+ }
1650
+ node.children.push({
1651
+ name: artifact.title,
1652
+ path: artifact.id,
1653
+ artifact,
1654
+ children: []
1655
+ });
1656
+ }
1657
+ const sortLevel = (nodes) => {
1658
+ nodes.sort((a, b) => {
1659
+ const dirDelta = Number(b.artifact === void 0) - Number(a.artifact === void 0);
1660
+ return dirDelta !== 0 ? dirDelta : a.name.localeCompare(b.name);
1661
+ });
1662
+ for (const item of nodes) {
1663
+ sortLevel(item.children);
1664
+ }
1665
+ };
1666
+ sortLevel(root.children);
1667
+ return root.children;
1668
+ }
1669
+ function ArtifactListView({
1670
+ nodes,
1671
+ activeId,
1672
+ onSelect
1673
+ }) {
1674
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("ul", { className: "pactor-chat-inspector__list", children: nodes.map((node) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1675
+ ArtifactTreeRow,
1676
+ {
1677
+ node,
1678
+ activeId,
1679
+ onSelect
1680
+ },
1681
+ node.path
1682
+ )) });
1683
+ }
1684
+ function ArtifactTreeRow({
1685
+ node,
1686
+ activeId,
1687
+ onSelect
1688
+ }) {
1689
+ const [open, setOpen] = (0, import_react6.useState)(true);
1690
+ if (node.artifact) {
1691
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
1692
+ "button",
1693
+ {
1694
+ type: "button",
1695
+ className: "pactor-chat-inspector__list-item",
1696
+ "data-active": node.artifact.id === activeId,
1697
+ onClick: () => onSelect?.(node.artifact),
1698
+ children: [
1699
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ArtifactKindIcon, { kind: node.artifact.kind }),
1700
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "pactor-chat-inspector__list-item-title", children: node.artifact.title }),
1701
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "pactor-chat-inspector__kind", children: node.artifact.kind })
1702
+ ]
1703
+ }
1704
+ ) });
1705
+ }
1706
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("li", { children: [
1707
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
1708
+ "button",
1709
+ {
1710
+ type: "button",
1711
+ className: "pactor-chat-inspector__list-folder",
1712
+ "aria-expanded": open,
1713
+ "data-open": open,
1714
+ onClick: () => setOpen((value) => !value),
1715
+ children: [
1716
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "pactor-chat-trace__chevron", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_components4.Icon, { name: "chevron-right", size: 12 }) }),
1717
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_components4.Icon, { name: "folder", size: 13 }),
1718
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { children: node.name })
1719
+ ]
1720
+ }
1721
+ ),
1722
+ open && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "pactor-chat-inspector__list-children", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1723
+ ArtifactListView,
1724
+ {
1725
+ nodes: node.children,
1726
+ activeId,
1727
+ onSelect
1728
+ }
1729
+ ) })
1730
+ ] });
1731
+ }
1732
+ function ArtifactImageContent({ artifact }) {
1733
+ const raw = String(artifact.content).trim();
1734
+ const src = raw.startsWith("<svg") || raw.startsWith("<?xml") ? `data:image/svg+xml;utf8,${encodeURIComponent(raw)}` : raw;
1735
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "pactor-chat-inspector__image", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("img", { src, alt: artifact.title }) });
1736
+ }
1403
1737
  function ArtifactContent({
1404
1738
  artifact,
1405
- runtime
1739
+ runtime,
1740
+ viewers
1406
1741
  }) {
1742
+ const ext = resolveArtifactFileType(artifact);
1743
+ const custom = (ext ? viewers?.[ext] : void 0) ?? viewers?.[artifact.kind];
1744
+ if (custom) {
1745
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_jsx_runtime5.Fragment, { children: custom(artifact) });
1746
+ }
1747
+ if (isImageArtifact(artifact)) {
1748
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ArtifactImageContent, { artifact });
1749
+ }
1407
1750
  switch (artifact.kind) {
1408
1751
  case "markdown":
1409
1752
  return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_markdown2.MarkdownRenderer, { content: String(artifact.content) });
@@ -1422,17 +1765,41 @@ function ArtifactContent({
1422
1765
  }
1423
1766
  function ArtifactPanel({
1424
1767
  artifact,
1768
+ artifacts,
1769
+ activeArtifactId,
1770
+ onSelectArtifact,
1425
1771
  inspector,
1426
1772
  onClose,
1427
1773
  safeRegistry,
1428
1774
  user,
1775
+ artifactViewers,
1429
1776
  className
1430
1777
  }) {
1431
1778
  const runtime = useArtifactRuntime(safeRegistry, user);
1779
+ const [view, setView] = (0, import_react6.useState)("content");
1780
+ const activeId = activeArtifactId ?? artifact?.id;
1781
+ (0, import_react6.useEffect)(() => {
1782
+ setView("content");
1783
+ }, [activeId]);
1784
+ const showListButton = artifacts !== void 0 && artifacts.length > 0;
1785
+ const toggleListRef = (0, import_react6.useRef)(() => {
1786
+ });
1787
+ toggleListRef.current = () => setView((value) => value === "list" ? "content" : "list");
1432
1788
  const closeRef = (0, import_react6.useRef)(onClose);
1433
1789
  closeRef.current = onClose;
1790
+ const artifactRef = (0, import_react6.useRef)(artifact);
1791
+ artifactRef.current = artifact;
1434
1792
  (0, import_react6.useEffect)(() => {
1435
- setChatChromeHandlers({ panelClose: () => closeRef.current?.() });
1793
+ setChatChromeHandlers({
1794
+ panelClose: () => closeRef.current?.(),
1795
+ panelToggleList: () => toggleListRef.current(),
1796
+ panelDownload: () => {
1797
+ const current = artifactRef.current;
1798
+ if (current) {
1799
+ void downloadArtifact(current);
1800
+ }
1801
+ }
1802
+ });
1436
1803
  }, []);
1437
1804
  const panelRef = (0, import_react6.useRef)(null);
1438
1805
  const [width, setWidth] = (0, import_react6.useState)(null);
@@ -1451,7 +1818,7 @@ function ArtifactPanel({
1451
1818
  document.body.style.userSelect = "none";
1452
1819
  const onMove = (moveEvent) => {
1453
1820
  const next = startWidth + (startX - moveEvent.clientX);
1454
- setWidth(Math.round(Math.min(Math.max(next, 240), maxWidth)));
1821
+ setWidth(Math.round(Math.min(Math.max(next, 320), maxWidth)));
1455
1822
  };
1456
1823
  const onUp = () => {
1457
1824
  document.body.style.userSelect = prevUserSelect;
@@ -1484,12 +1851,36 @@ function ArtifactPanel({
1484
1851
  {
1485
1852
  fragment: buildPanelHeaderFragment({
1486
1853
  title: artifact?.title ?? "\u4EA7\u51FA\u7269",
1487
- kind: artifact?.kind ?? "text"
1854
+ kind: artifact?.kind ?? "text",
1855
+ showListButton,
1856
+ showDownload: artifact != null
1488
1857
  }),
1489
1858
  components: PANEL_HEADER_INTERNAL_COMPONENTS,
1490
1859
  fallback: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
1860
+ showListButton ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1861
+ "button",
1862
+ {
1863
+ type: "button",
1864
+ className: "pactor-chat-icon-button pactor-chat-inspector__list-toggle",
1865
+ title: "\u4EA7\u7269\u76EE\u5F55",
1866
+ "aria-label": "\u4EA7\u7269\u76EE\u5F55",
1867
+ onClick: () => toggleListRef.current(),
1868
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_components4.Icon, { name: "arrow-left", size: 16, strokeWidth: 1.7 })
1869
+ }
1870
+ ) : null,
1491
1871
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "pactor-chat-inspector__title", children: artifact?.title ?? "\u4EA7\u51FA\u7269" }),
1492
1872
  artifact ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "pactor-chat-inspector__kind", children: artifact.kind }) : null,
1873
+ artifact ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1874
+ "button",
1875
+ {
1876
+ type: "button",
1877
+ className: "pactor-chat-icon-button pactor-chat-inspector__download",
1878
+ title: "\u4E0B\u8F7D\u4EA7\u7269",
1879
+ "aria-label": "\u4E0B\u8F7D\u4EA7\u7269",
1880
+ onClick: () => void downloadArtifact(artifact),
1881
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_components4.Icon, { name: "download", size: 16, strokeWidth: 1.7 })
1882
+ }
1883
+ ) : null,
1493
1884
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1494
1885
  "button",
1495
1886
  {
@@ -1503,7 +1894,38 @@ function ArtifactPanel({
1503
1894
  ] })
1504
1895
  }
1505
1896
  ) }),
1506
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "pactor-chat-inspector__body", children: inspector !== void 0 && inspector !== null ? inspector : artifact ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ArtifactContent, { artifact, runtime }) : null })
1897
+ artifacts !== void 0 && artifacts.length > 1 ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "pactor-chat-inspector__tabs", role: "tablist", children: artifacts.map((item) => /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
1898
+ "button",
1899
+ {
1900
+ type: "button",
1901
+ role: "tab",
1902
+ "aria-selected": item.id === activeId,
1903
+ "data-active": item.id === activeId,
1904
+ className: "pactor-chat-inspector__tab",
1905
+ title: item.title,
1906
+ onClick: () => onSelectArtifact?.(item),
1907
+ children: [
1908
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(ArtifactKindIcon, { kind: item.kind }),
1909
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "pactor-chat-inspector__tab-title", children: item.title })
1910
+ ]
1911
+ },
1912
+ item.id
1913
+ )) }) : null,
1914
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "pactor-chat-inspector__body", children: view === "list" && artifacts !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1915
+ ArtifactListView,
1916
+ {
1917
+ nodes: buildArtifactTree(artifacts),
1918
+ activeId,
1919
+ onSelect: onSelectArtifact
1920
+ }
1921
+ ) : inspector !== void 0 && inspector !== null ? inspector : artifact ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1922
+ ArtifactContent,
1923
+ {
1924
+ artifact,
1925
+ runtime,
1926
+ viewers: artifactViewers
1927
+ }
1928
+ ) : null })
1507
1929
  ]
1508
1930
  }
1509
1931
  );
@@ -2529,7 +2951,11 @@ var import_react12 = require("react");
2529
2951
  var import_jsx_runtime12 = require("react/jsx-runtime");
2530
2952
  function usePinnedCollapse(autoOpen) {
2531
2953
  const [state, setState] = (0, import_react12.useState)("auto");
2532
- const open = state === "auto" ? autoOpen : state === "pinned-open";
2954
+ const autoCollapsedRef = (0, import_react12.useRef)(false);
2955
+ if (state === "auto" && !autoOpen) {
2956
+ autoCollapsedRef.current = true;
2957
+ }
2958
+ const open = state === "auto" ? autoOpen && !autoCollapsedRef.current : state === "pinned-open";
2533
2959
  const toggle = () => {
2534
2960
  setState(open ? "pinned-closed" : "pinned-open");
2535
2961
  };
@@ -3674,6 +4100,8 @@ function ChatComposition({
3674
4100
  onSend,
3675
4101
  artifact,
3676
4102
  panelOpen,
4103
+ artifacts: artifactsProp,
4104
+ onSelectArtifact,
3677
4105
  onPanelClose,
3678
4106
  inspector,
3679
4107
  onArtifact,
@@ -3694,8 +4122,28 @@ function ChatComposition({
3694
4122
  (listener) => artifactStore.subscribe(listener),
3695
4123
  () => artifactStore.get()
3696
4124
  );
4125
+ const storeArtifacts = (0, import_react17.useSyncExternalStore)(
4126
+ (listener) => artifactStore.subscribe(listener),
4127
+ () => artifactStore.list()
4128
+ );
4129
+ const messages = (0, import_react17.useSyncExternalStore)(
4130
+ (listener) => store.subscribe(listener),
4131
+ () => store.getMessages()
4132
+ );
4133
+ const streamArtifacts = (0, import_react17.useMemo)(() => collectArtifacts(messages), [messages]);
4134
+ const mergedArtifacts = (0, import_react17.useMemo)(
4135
+ () => mergeArtifacts(streamArtifacts, storeArtifacts),
4136
+ [streamArtifacts, storeArtifacts]
4137
+ );
3697
4138
  const effectiveArtifact = panelControlled ? artifact ?? null : storeArtifact;
3698
4139
  const effectivePanelOpen = panelControlled ? panelOpen ?? artifact != null : storeArtifact !== null;
4140
+ const effectiveArtifacts = artifactsProp ?? mergedArtifacts;
4141
+ const handleSelectArtifact = (next) => {
4142
+ onSelectArtifact?.(next);
4143
+ if (!panelControlled) {
4144
+ artifactStore.open(next);
4145
+ }
4146
+ };
3699
4147
  const handlePanelClose = () => {
3700
4148
  onPanelClose?.();
3701
4149
  if (!panelControlled) {
@@ -3837,10 +4285,14 @@ function ChatComposition({
3837
4285
  ArtifactPanel,
3838
4286
  {
3839
4287
  artifact: effectiveArtifact,
4288
+ artifacts: effectiveArtifacts,
4289
+ activeArtifactId: effectiveArtifact?.id,
4290
+ onSelectArtifact: handleSelectArtifact,
3840
4291
  inspector,
3841
4292
  onClose: handlePanelClose,
3842
4293
  safeRegistry: services.safeRegistry,
3843
- user: services.user
4294
+ user: services.user,
4295
+ artifactViewers: services.artifactViewers
3844
4296
  }
3845
4297
  )
3846
4298
  ] }) : null
@@ -3883,6 +4335,7 @@ function createChatWorkspace(services) {
3883
4335
  inspector,
3884
4336
  onArtifact,
3885
4337
  onOpenArtifact,
4338
+ onSelectArtifact,
3886
4339
  headerActions,
3887
4340
  editableTitle,
3888
4341
  onExport,
@@ -3923,6 +4376,7 @@ function createChatWorkspace(services) {
3923
4376
  inspector: inspector ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(DslNodeSlot, { node: inspector }) : void 0,
3924
4377
  onArtifact,
3925
4378
  onOpenArtifact,
4379
+ onSelectArtifact,
3926
4380
  headerActions,
3927
4381
  editableTitle,
3928
4382
  onExport,
@@ -4132,6 +4586,7 @@ function Chat({
4132
4586
  inspector,
4133
4587
  onArtifact,
4134
4588
  onOpenArtifact,
4589
+ onSelectArtifact,
4135
4590
  className,
4136
4591
  onSend,
4137
4592
  onMenuSelect
@@ -4196,7 +4651,8 @@ function Chat({
4196
4651
  onPanelClose,
4197
4652
  inspector: resolveSlot(inspector),
4198
4653
  onArtifact,
4199
- onOpenArtifact
4654
+ onOpenArtifact,
4655
+ onSelectArtifact
4200
4656
  }
4201
4657
  );
4202
4658
  }
@@ -4207,6 +4663,7 @@ function ChatDslAdapter({
4207
4663
  onPanelClose,
4208
4664
  onArtifact,
4209
4665
  onOpenArtifact,
4666
+ onSelectArtifact,
4210
4667
  onModelChange,
4211
4668
  onContextsChange,
4212
4669
  onMessageAction,
@@ -4225,6 +4682,7 @@ function ChatDslAdapter({
4225
4682
  onPanelClose: onPanelClose ? () => onPanelClose({}) : void 0,
4226
4683
  onArtifact: onArtifact ? (artifact) => onArtifact({ artifact }) : void 0,
4227
4684
  onOpenArtifact: onOpenArtifact ? (artifact) => onOpenArtifact({ artifact }) : void 0,
4685
+ onSelectArtifact: onSelectArtifact ? (artifact) => onSelectArtifact({ artifact }) : void 0,
4228
4686
  onModelChange: onModelChange ? (model) => onModelChange({ model }) : void 0,
4229
4687
  onContextsChange: onContextsChange ? (contexts) => onContextsChange({ contexts }) : void 0,
4230
4688
  onMessageAction: onMessageAction ? (action, message) => onMessageAction({ action, message }) : void 0,
@@ -4263,6 +4721,7 @@ function registerChatLayout(layoutRegistry) {
4263
4721
  }
4264
4722
  // Annotate the CommonJS export names for ESM import in node:
4265
4723
  0 && (module.exports = {
4724
+ ARTIFACT_IMAGE_EXTENSIONS,
4266
4725
  ActivityTrace,
4267
4726
  AppShell,
4268
4727
  ArtifactPanel,
@@ -4286,7 +4745,10 @@ function registerChatLayout(layoutRegistry) {
4286
4745
  MessageBubble,
4287
4746
  PartRenderer,
4288
4747
  applyChatEvent,
4748
+ buildArtifactBlob,
4749
+ buildArtifactTree,
4289
4750
  buildChatScope,
4751
+ collectArtifacts,
4290
4752
  collectMessageText,
4291
4753
  createChatComponentRegistry,
4292
4754
  createChatId,
@@ -4295,14 +4757,20 @@ function registerChatLayout(layoutRegistry) {
4295
4757
  createManualDriver,
4296
4758
  createSafeRegistry,
4297
4759
  detectStreamingMode,
4760
+ downloadArtifact,
4298
4761
  guardChatDsl,
4762
+ isImageArtifact,
4763
+ mergeArtifacts,
4299
4764
  pairToolTraces,
4300
4765
  registerCapabilityAction,
4301
4766
  registerChatComponent,
4302
4767
  registerChatLayout,
4303
4768
  registerChatShellComponent,
4769
+ resolveArtifactFileType,
4770
+ resolveArtifactFilename,
4304
4771
  runChatTurn,
4305
4772
  runRegenerateTurn,
4773
+ saveBlob,
4306
4774
  useChatAutoScroll,
4307
4775
  useChatServices,
4308
4776
  useSidebarDrawer,