@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.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;
274
+ this.notify();
275
+ }
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;
202
286
  this.notify();
203
287
  }
204
- /** 部分更新当前产出物(id 不可改);未 open 时告警并忽略 */
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
  };
@@ -2680,6 +3106,7 @@ function ActivityTrace({ message }) {
2680
3106
  );
2681
3107
  const answerPhase = mode === "responding" || !streaming;
2682
3108
  const { open, toggle } = usePinnedCollapse(!answerPhase);
3109
+ const collapsible = entries.length > 0 || thinkingParts.length > 0;
2683
3110
  const mountTimeRef = (0, import_react12.useRef)(0);
2684
3111
  if (mountTimeRef.current === 0) {
2685
3112
  mountTimeRef.current = Date.now();
@@ -2701,10 +3128,10 @@ function ActivityTrace({ message }) {
2701
3128
  elapsed,
2702
3129
  "s"
2703
3130
  ] }),
2704
- entries.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "pactor-chat-trace__chevron", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_components8.Icon, { name: "chevron-right", size: 12 }) })
3131
+ collapsible && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "pactor-chat-trace__chevron", children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(import_components8.Icon, { name: "chevron-right", size: 12 }) })
2705
3132
  ] });
2706
3133
  return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)("div", { className: "pactor-chat-trace", "data-open": open, children: [
2707
- entries.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
3134
+ collapsible ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2708
3135
  "button",
2709
3136
  {
2710
3137
  type: "button",
@@ -2714,15 +3141,17 @@ function ActivityTrace({ message }) {
2714
3141
  children: header
2715
3142
  }
2716
3143
  ) : /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "pactor-chat-trace__header", children: header }),
2717
- thinkingParts.map((part, index) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2718
- ThinkingCard,
2719
- {
2720
- part,
2721
- answerPhase
2722
- },
2723
- part.id ?? index
2724
- )),
2725
- open && entries.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "pactor-chat-trace__entries", children: entries.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(TraceRow, { entry }, entry.callIndex)) })
3144
+ open && /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(import_jsx_runtime12.Fragment, { children: [
3145
+ entries.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("div", { className: "pactor-chat-trace__entries", children: entries.map((entry) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(TraceRow, { entry }, entry.callIndex)) }),
3146
+ thinkingParts.map((part, index) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
3147
+ ThinkingCard,
3148
+ {
3149
+ part,
3150
+ answerPhase
3151
+ },
3152
+ part.id ?? index
3153
+ ))
3154
+ ] })
2726
3155
  ] });
2727
3156
  }
2728
3157
 
@@ -3671,6 +4100,8 @@ function ChatComposition({
3671
4100
  onSend,
3672
4101
  artifact,
3673
4102
  panelOpen,
4103
+ artifacts: artifactsProp,
4104
+ onSelectArtifact,
3674
4105
  onPanelClose,
3675
4106
  inspector,
3676
4107
  onArtifact,
@@ -3691,8 +4122,28 @@ function ChatComposition({
3691
4122
  (listener) => artifactStore.subscribe(listener),
3692
4123
  () => artifactStore.get()
3693
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
+ );
3694
4138
  const effectiveArtifact = panelControlled ? artifact ?? null : storeArtifact;
3695
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
+ };
3696
4147
  const handlePanelClose = () => {
3697
4148
  onPanelClose?.();
3698
4149
  if (!panelControlled) {
@@ -3834,10 +4285,14 @@ function ChatComposition({
3834
4285
  ArtifactPanel,
3835
4286
  {
3836
4287
  artifact: effectiveArtifact,
4288
+ artifacts: effectiveArtifacts,
4289
+ activeArtifactId: effectiveArtifact?.id,
4290
+ onSelectArtifact: handleSelectArtifact,
3837
4291
  inspector,
3838
4292
  onClose: handlePanelClose,
3839
4293
  safeRegistry: services.safeRegistry,
3840
- user: services.user
4294
+ user: services.user,
4295
+ artifactViewers: services.artifactViewers
3841
4296
  }
3842
4297
  )
3843
4298
  ] }) : null
@@ -3880,6 +4335,7 @@ function createChatWorkspace(services) {
3880
4335
  inspector,
3881
4336
  onArtifact,
3882
4337
  onOpenArtifact,
4338
+ onSelectArtifact,
3883
4339
  headerActions,
3884
4340
  editableTitle,
3885
4341
  onExport,
@@ -3920,6 +4376,7 @@ function createChatWorkspace(services) {
3920
4376
  inspector: inspector ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(DslNodeSlot, { node: inspector }) : void 0,
3921
4377
  onArtifact,
3922
4378
  onOpenArtifact,
4379
+ onSelectArtifact,
3923
4380
  headerActions,
3924
4381
  editableTitle,
3925
4382
  onExport,
@@ -4129,6 +4586,7 @@ function Chat({
4129
4586
  inspector,
4130
4587
  onArtifact,
4131
4588
  onOpenArtifact,
4589
+ onSelectArtifact,
4132
4590
  className,
4133
4591
  onSend,
4134
4592
  onMenuSelect
@@ -4193,7 +4651,8 @@ function Chat({
4193
4651
  onPanelClose,
4194
4652
  inspector: resolveSlot(inspector),
4195
4653
  onArtifact,
4196
- onOpenArtifact
4654
+ onOpenArtifact,
4655
+ onSelectArtifact
4197
4656
  }
4198
4657
  );
4199
4658
  }
@@ -4204,6 +4663,7 @@ function ChatDslAdapter({
4204
4663
  onPanelClose,
4205
4664
  onArtifact,
4206
4665
  onOpenArtifact,
4666
+ onSelectArtifact,
4207
4667
  onModelChange,
4208
4668
  onContextsChange,
4209
4669
  onMessageAction,
@@ -4222,6 +4682,7 @@ function ChatDslAdapter({
4222
4682
  onPanelClose: onPanelClose ? () => onPanelClose({}) : void 0,
4223
4683
  onArtifact: onArtifact ? (artifact) => onArtifact({ artifact }) : void 0,
4224
4684
  onOpenArtifact: onOpenArtifact ? (artifact) => onOpenArtifact({ artifact }) : void 0,
4685
+ onSelectArtifact: onSelectArtifact ? (artifact) => onSelectArtifact({ artifact }) : void 0,
4225
4686
  onModelChange: onModelChange ? (model) => onModelChange({ model }) : void 0,
4226
4687
  onContextsChange: onContextsChange ? (contexts) => onContextsChange({ contexts }) : void 0,
4227
4688
  onMessageAction: onMessageAction ? (action, message) => onMessageAction({ action, message }) : void 0,
@@ -4260,6 +4721,7 @@ function registerChatLayout(layoutRegistry) {
4260
4721
  }
4261
4722
  // Annotate the CommonJS export names for ESM import in node:
4262
4723
  0 && (module.exports = {
4724
+ ARTIFACT_IMAGE_EXTENSIONS,
4263
4725
  ActivityTrace,
4264
4726
  AppShell,
4265
4727
  ArtifactPanel,
@@ -4283,7 +4745,10 @@ function registerChatLayout(layoutRegistry) {
4283
4745
  MessageBubble,
4284
4746
  PartRenderer,
4285
4747
  applyChatEvent,
4748
+ buildArtifactBlob,
4749
+ buildArtifactTree,
4286
4750
  buildChatScope,
4751
+ collectArtifacts,
4287
4752
  collectMessageText,
4288
4753
  createChatComponentRegistry,
4289
4754
  createChatId,
@@ -4292,14 +4757,20 @@ function registerChatLayout(layoutRegistry) {
4292
4757
  createManualDriver,
4293
4758
  createSafeRegistry,
4294
4759
  detectStreamingMode,
4760
+ downloadArtifact,
4295
4761
  guardChatDsl,
4762
+ isImageArtifact,
4763
+ mergeArtifacts,
4296
4764
  pairToolTraces,
4297
4765
  registerCapabilityAction,
4298
4766
  registerChatComponent,
4299
4767
  registerChatLayout,
4300
4768
  registerChatShellComponent,
4769
+ resolveArtifactFileType,
4770
+ resolveArtifactFilename,
4301
4771
  runChatTurn,
4302
4772
  runRegenerateTurn,
4773
+ saveBlob,
4303
4774
  useChatAutoScroll,
4304
4775
  useChatServices,
4305
4776
  useSidebarDrawer,