Compare commits

...
Author SHA1 Message Date
weichen 89bfa83cae Wire preview capabilities into the app shell and dual-dock placement
Why: the adjudicated plan (D1-D4) calls for office/link previews with
loaders injected from the host, right-rail light preview alongside the
overlay viewer, and artifact deletion kept as an API capability with no
UI entry.

Evidence: previewsetup.ts injects docx (mammoth), xlsx (SheetJS, sheet
tabs, 200-row cap), pptx (pptx-preview full-fidelity; pptx2html was
dropped for a broken module entry — tXml case mismatch breaks Linux
builds — per the D2 fallback clause), and the /api/url-preview fetcher,
all via dynamic import: the main entry grows ~12KB while xlsx (430KB),
pptx (1.2MB) and jszip (98KB) load on first preview. DocViewer becomes a
FilePreview container (keeps text-family editing); Workbench gains a
preview tab with rail-preview state (store.railPreview); artifact chips
and panel rows gain a rail-preview action; the panel delete button is
removed while api.deleteArtifact and DELETE /api/artifacts/{id} remain.
tsc clean; vitest 33 passed; production build succeeds.

Impact: previews work end to end for the whole format matrix; deletion
stays plugin-reachable only.

Refs: task feature/file-preview-caps; ui fb76239; core 4d29301; backend
6d4ccfd.
2026-09-30 17:17:29 +00:00
weichen 6752b788b9 Merge pull request 'Render submit_card card parts as CardAnchor with inline full-text expansion (CF1)' (#20) from feature/cardflow-r1 into main 2026-09-30 17:12:36 +00:00
weichen 411d09b3a8 Merge remote-tracking branch 'origin/main' into feature/cardflow-r1 2026-09-30 16:55:44 +00:00
weichen af0385451a Merge pull request 'Bump vitest to 4.1.11 past the mocker path traversal advisory' (#19) from fix/dep-vuln-bumps into main 2026-09-30 16:48:48 +00:00
weichen b329de3bf5 Add cardflow deltas on fresh main: CardAnchor routing, D32 locate, card contract field
Why: main(含任务1 PR #18 的 parts 模型与增强版 CF0)之上补齐卡流增量:cardId→CardAnchor 锚点路由、卡→消息定位(locateMessage + data-msg-id)、Part.card.cardId 契约镜像。
Evidence: tsc 0 错误;vitest 33/33(main 的 messageparts 等测试全过)。
Impact: Conversation/Workbench/store/contracts 四文件;CF0 与过程折叠采用 main 增强版不回退。
Refs: Dev 任务卡 feature/cardflow-r1。
2026-09-30 16:37:29 +00:00
12 changed files with 603 additions and 70 deletions
+390 -1
View File
@@ -8,9 +8,13 @@
"name": "agent-system-frontend", "name": "agent-system-frontend",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"jszip": "^3.10.2",
"mammoth": "^1.13.0",
"marked": "^15.0.7", "marked": "^15.0.7",
"pptx-preview": "^1.0.7",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0" "react-dom": "^19.2.0",
"xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^19.2.0", "@types/react": "^19.2.0",
@@ -1442,6 +1446,33 @@
"url": "https://opencollective.com/vitest" "url": "https://opencollective.com/vitest"
} }
}, },
"node_modules/@xmldom/xmldom": {
"version": "0.8.15",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.15.tgz",
"integrity": "sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/adler-32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"license": "MIT",
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/assertion-error": { "node_modules/assertion-error": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
@@ -1452,6 +1483,26 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/baseline-browser-mapping": { "node_modules/baseline-browser-mapping": {
"version": "2.11.26", "version": "2.11.26",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.26.tgz", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.26.tgz",
@@ -1520,6 +1571,19 @@
], ],
"license": "CC-BY-4.0" "license": "CC-BY-4.0"
}, },
"node_modules/cfb": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"crc-32": "~1.2.0"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/chai": { "node_modules/chai": {
"version": "6.2.2", "version": "6.2.2",
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
@@ -1530,6 +1594,15 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/codepage": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/convert-source-map": { "node_modules/convert-source-map": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -1537,6 +1610,24 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
"license": "Apache-2.0",
"bin": {
"crc32": "bin/crc32.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/csstype": { "node_modules/csstype": {
"version": "3.2.3", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@@ -1562,6 +1653,37 @@
} }
} }
}, },
"node_modules/dingbat-to-unicode": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.2.tgz",
"integrity": "sha512-R0bRerYzy/EZP6QzD3Hkl6JjUDA1Mnn+pd928w0nWjPzihnbAR6dcjGJGiRyznj5E4S1duEKzWrTqw02DPcdzg==",
"license": "BSD-2-Clause"
},
"node_modules/duck": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz",
"integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==",
"license": "BSD",
"dependencies": {
"underscore": "^1.13.1"
}
},
"node_modules/echarts": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
"integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "2.3.0",
"zrender": "5.6.1"
}
},
"node_modules/echarts/node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"license": "0BSD"
},
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.440", "version": "1.5.440",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.440.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.440.tgz",
@@ -1666,6 +1788,15 @@
} }
} }
}, },
"node_modules/frac": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/fsevents": { "node_modules/fsevents": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -1691,6 +1822,24 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -1724,6 +1873,44 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/jszip": {
"version": "3.10.2",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.2.tgz",
"integrity": "sha512-3l+rb15IOWtUhU0H5MFqES/T6Kh7abYwjosBey/vD6hDt8zoEffkSC5Ws5SGtgVw3gBx2NEbhTeSW1+kWkpyTQ==",
"license": "(MIT OR GPL-3.0-or-later)",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"setimmediate": "^1.0.5"
}
},
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"license": "MIT",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/lodash": {
"version": "4.18.1",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT"
},
"node_modules/lop": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/lop/-/lop-0.4.2.tgz",
"integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==",
"license": "BSD-2-Clause",
"dependencies": {
"duck": "^0.1.12",
"option": "~0.2.1",
"underscore": "^1.13.1"
}
},
"node_modules/lru-cache": { "node_modules/lru-cache": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -1744,6 +1931,28 @@
"@jridgewell/sourcemap-codec": "^1.5.5" "@jridgewell/sourcemap-codec": "^1.5.5"
} }
}, },
"node_modules/mammoth": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.13.0.tgz",
"integrity": "sha512-2cawcYFcP7ISfXbBcL9lmVgTNWo88v/52ND1gnywHtVT/pX0//puPnf0iMPV9ZgmuvNOE+zkIhDgZ9M2c2dDDg==",
"license": "BSD-2-Clause",
"dependencies": {
"@xmldom/xmldom": "^0.8.6",
"argparse": "~1.0.3",
"base64-js": "^1.5.1",
"dingbat-to-unicode": "^1.0.1",
"jszip": "^3.7.1",
"lop": "^0.4.2",
"underscore": "^1.13.1",
"xmlbuilder": "^10.0.0"
},
"bin": {
"mammoth": "bin/mammoth"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/marked": { "node_modules/marked": {
"version": "15.0.12", "version": "15.0.12",
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
@@ -1806,6 +2015,18 @@
"node": ">=12.20.0" "node": ">=12.20.0"
} }
}, },
"node_modules/option": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz",
"integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==",
"license": "BSD-2-Clause"
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/pathe": { "node_modules/pathe": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
@@ -1862,6 +2083,25 @@
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
} }
}, },
"node_modules/pptx-preview": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/pptx-preview/-/pptx-preview-1.0.7.tgz",
"integrity": "sha512-YByocJuyxAR4YB4Q3+VAxdLfEvA5LojG1gAJsx2Mw0QU5FJPps/2fkJOupJ6oBbA+KdWRpuAk6G6T34rKCHVxw==",
"license": "ISC",
"dependencies": {
"echarts": "^5.5.1",
"jszip": "^3.10.1",
"lodash": "^4.17.21",
"tslib": "^2.7.0",
"uuid": "^10.0.0"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/react": { "node_modules/react": {
"version": "19.3.0", "version": "19.3.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz",
@@ -1893,6 +2133,21 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.63.5", "version": "4.63.5",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.5.tgz", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.5.tgz",
@@ -1939,6 +2194,12 @@
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/scheduler": { "node_modules/scheduler": {
"version": "0.28.0", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz",
@@ -1955,6 +2216,12 @@
"semver": "bin/semver.js" "semver": "bin/semver.js"
} }
}, },
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"license": "MIT"
},
"node_modules/siginfo": { "node_modules/siginfo": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
@@ -1972,6 +2239,24 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
"license": "BSD-3-Clause"
},
"node_modules/ssf": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
"license": "Apache-2.0",
"dependencies": {
"frac": "~1.1.2"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/stackback": { "node_modules/stackback": {
"version": "0.0.2", "version": "0.0.2",
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
@@ -1986,6 +2271,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/tinybench": { "node_modules/tinybench": {
"version": "2.9.0", "version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
@@ -2030,6 +2324,12 @@
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.9.3", "version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@@ -2044,6 +2344,12 @@
"node": ">=14.17" "node": ">=14.17"
} }
}, },
"node_modules/underscore": {
"version": "1.13.8",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz",
"integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==",
"license": "MIT"
},
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz",
@@ -2075,6 +2381,26 @@
"browserslist": ">= 4.21.0" "browserslist": ">= 4.21.0"
} }
}, },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
"deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/vite": { "node_modules/vite": {
"version": "6.4.3", "version": "6.4.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
@@ -2257,12 +2583,75 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/wmf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/xlsx": {
"version": "0.18.5",
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"cfb": "~1.2.1",
"codepage": "~1.15.0",
"crc-32": "~1.2.1",
"ssf": "~0.11.2",
"wmf": "~1.0.1",
"word": "~0.3.0"
},
"bin": {
"xlsx": "bin/xlsx.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/xmlbuilder": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz",
"integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==",
"license": "MIT",
"engines": {
"node": ">=4.0"
}
},
"node_modules/yallist": { "node_modules/yallist": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
},
"node_modules/zrender": {
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
"integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
"license": "BSD-3-Clause",
"dependencies": {
"tslib": "2.3.0"
}
},
"node_modules/zrender/node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"license": "0BSD"
} }
} }
} }
+5 -1
View File
@@ -10,9 +10,13 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"jszip": "^3.10.2",
"mammoth": "^1.13.0",
"marked": "^15.0.7", "marked": "^15.0.7",
"pptx-preview": "^1.0.7",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0" "react-dom": "^19.2.0",
"xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^19.2.0", "@types/react": "^19.2.0",
+23 -6
View File
@@ -5,8 +5,8 @@ import { marked } from 'marked'
import { api } from '../api' import { api } from '../api'
import { store, useStore, activeRun, type LiveState, type ContextObs } from '../store' import { store, useStore, activeRun, type LiveState, type ContextObs } from '../store'
import { liveToMessage, showProcessInline } from '../live' import { liveToMessage, showProcessInline } from '../live'
import type { Message, Part, HITLCard } from '../contracts' import type { Message, Part, HITLCard, ActivityCard } from '../contracts'
import { UIRenderer, InlineConfirmCard, hasOpenConfirm } from '@ui' import { UIRenderer, InlineConfirmCard, hasOpenConfirm, CardAnchor } from '@ui'
import { docBridge } from './Workbench' import { docBridge } from './Workbench'
import { groupParts, artifactIcon } from '../messageparts' import { groupParts, artifactIcon } from '../messageparts'
@@ -45,6 +45,8 @@ function PartView({ part, live }: { part: Part; live?: boolean | null }) {
<button className="ac-main" title={`打开 ${name}`} onClick={() => { if (art) store.viewArtifact(art) }}> <button className="ac-main" title={`打开 ${name}`} onClick={() => { if (art) store.viewArtifact(art) }}>
<Icon name={icon} size={14} /> {name} <Icon name={icon} size={14} /> {name}
</button> </button>
<button className="ac-act" title={`右栏预览 ${name}(边聊边看)`}
onClick={() => { if (art) store.railPreview(art) }}><Icon name="panel-left" size={13} /></button>
<a className="ac-act" title={`下载 ${name}`} href={api.artifactDownloadUrl(a?.artifactId ?? '')} <a className="ac-act" title={`下载 ${name}`} href={api.artifactDownloadUrl(a?.artifactId ?? '')}
download={name} onClick={e => e.stopPropagation()}><Icon name="download" size={13} /></a> download={name} onClick={e => e.stopPropagation()}><Icon name="download" size={13} /></a>
</span> </span>
@@ -55,8 +57,16 @@ function PartView({ part, live }: { part: Part; live?: boolean | null }) {
case 'plan': case 'plan':
return null // 计划在右栏 Plan 标签呈现 return null // 计划在右栏 Plan 标签呈现
case 'card': { case 'card': {
// cardlink 互定位(D32):点击 ↗ 定位收件箱对应卡(从 data 中解析 card:<id>)。 // 卡流锚点(D39):cardId 非空 = submit_card 引用部件——中栏自足(可展开全文)+ 跳收件箱。
const m = /card:([A-Za-z0-9_]+)/.exec(JSON.stringify(part.card?.data ?? '')) if (part.card?.cardId) {
const ref = part.card.cardId
const card = findCard(ref)
const bodyChars = card?.body?.reduce((n, node) => n + (node.markdown?.length ?? 0), 0)
return <CardAnchor title={card?.title ?? part.card.kind} chars={bodyChars} card={card}
onLocate={() => store.openInbox(ref)} docBridge={docBridge} />
}
// 插件内嵌卡片部件(cardlink 互定位,D32):点击 ↗ 定位收件箱对应卡(从 data 解析 card:<id>)。
const m = JSON.stringify(part.card?.data ?? '').match(/card:([A-Za-z0-9_]+)/)
return ( return (
<div className="ui-section card-link" title={m ? '点击在收件箱中定位' : undefined} <div className="ui-section card-link" title={m ? '点击在收件箱中定位' : undefined}
onClick={() => m && store.openInbox(m[1])}> onClick={() => m && store.openInbox(m[1])}>
@@ -77,6 +87,12 @@ function findArtifact(id: string) {
return artifactCache[id] return artifactCache[id]
} }
// 收件箱卡片查询缓存(store.cards 的派生索引;卡流锚点取卡数据用)。
let cardCache: Record<string, ActivityCard> = {}
function findCard(id: string) {
return cardCache[id]
}
// 过程折叠:run 终态后把连续 process 部件折叠为摘要行(可展开回看)。 // 过程折叠:run 终态后把连续 process 部件折叠为摘要行(可展开回看)。
function ProcessCollapse({ parts }: { parts: Part[] }) { function ProcessCollapse({ parts }: { parts: Part[] }) {
const toolCount = parts.filter(p => p.type === 'tool_call').length const toolCount = parts.filter(p => p.type === 'tool_call').length
@@ -131,7 +147,7 @@ function AssistantMessage({ msg, live }: { msg: Message; live?: LiveState | null
return -1 return -1
})() })()
return ( return (
<div className="msg assistant"> <div className="msg assistant" data-msg-id={msg.id}>
<div className="msg-role"> <div className="msg-role">
<span className="role-mark role-assistant"><BrandMark size={16} /></span> <span className="role-mark role-assistant"><BrandMark size={16} /></span>
<span>助手</span> <span>助手</span>
@@ -261,6 +277,7 @@ export function Conversation() {
const bodyRef = useRef<HTMLDivElement>(null) const bodyRef = useRef<HTMLDivElement>(null)
artifactCache = Object.fromEntries(artifacts.map(a => [a.id, a])) artifactCache = Object.fromEntries(artifacts.map(a => [a.id, a]))
cardCache = Object.fromEntries(cards.map(c => [c.id, c]))
const active = activeRun(runs) const active = activeRun(runs)
@@ -326,7 +343,7 @@ export function Conversation() {
// 内联确认卡(T3):锚定本消息的危险命令确认,双现于对话流与收件箱。 // 内联确认卡(T3):锚定本消息的危险命令确认,双现于对话流与收件箱。
const anchored = cards.filter(c => c.anchorMessageId === m.id && c.kind === 'confirm') const anchored = cards.filter(c => c.anchorMessageId === m.id && c.kind === 'confirm')
return ( return (
<div className="msg user" key={m.id}> <div className="msg user" key={m.id} data-msg-id={m.id}>
<div className="msg-role"> <div className="msg-role">
<span className="role-mark"><Avatar name={myName} size="sm" /></span>你 <span className="role-mark"><Avatar name={myName} size="sm" /></span>你
</div> </div>
+19 -30
View File
@@ -1,26 +1,33 @@
// 文档查看器(4.4 → 工作区文件管理升级):单层 OverlayHost——markdown 渲染 / // 文档查看器(4.4 → 预览能力收敛):格式渲染统一交给 @ui FilePreview
// 文本编辑保存 / 图片与 pdf 预览 / csv·json 等宽 / 二进制信息卡 + 下载。 // (md/图片/pdf/代码/数据/docx/xlsx/pptx/URL 一套分派),这里只保留 OverlayHost
// 容器、工具栏与文本族的在线编辑保存。
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { marked } from 'marked'
import { api } from '../api' import { api } from '../api'
import { store } from '../store' import { store } from '../store'
import { OverlayHost, Icon, Btn, EmptyState } from '@ui' import { OverlayHost, Icon, Btn, FilePreview } from '@ui'
import type { Artifact } from '../contracts' import type { Artifact } from '../contracts'
import { artifactIcon, humanSize } from '../messageparts' import { humanSize } from '../messageparts'
export function DocViewer({ artifact }: { artifact: Artifact }) { export function DocViewer({ artifact }: { artifact: Artifact }) {
const [content, setContent] = useState<string | null>(null) const [content, setContent] = useState<string | null>(null)
const [editing, setEditing] = useState(false) const [editing, setEditing] = useState(false)
const [dirty, setDirty] = useState(false) const [dirty, setDirty] = useState(false)
const textKind = isTextKind(artifact)
useEffect(() => { useEffect(() => {
if (artifact.kind === 'image' || isBinaryKind(artifact)) return if (!textKind) return
setContent(null)
fetch(api.artifactContentUrl(artifact.id)).then(r => r.text()).then(setContent).catch(() => setContent('(读取失败)')) fetch(api.artifactContentUrl(artifact.id)).then(r => r.text()).then(setContent).catch(() => setContent('(读取失败)'))
}, [artifact.id]) }, [artifact.id])
const isPdf = /\.pdf$/i.test(artifact.name) const source = {
const isCsv = /\.csv$/i.test(artifact.name) kind: 'artifact' as const,
const isJson = /\.json$/i.test(artifact.name) id: artifact.id,
name: artifact.name,
mime: artifact.mime,
contentUrl: api.artifactContentUrl(artifact.id),
downloadUrl: api.artifactDownloadUrl(artifact.id),
}
return ( return (
<OverlayHost title={artifact.name} icon="📄" onClose={() => store.viewArtifact(null)}> <OverlayHost title={artifact.name} icon="📄" onClose={() => store.viewArtifact(null)}>
@@ -29,7 +36,7 @@ export function DocViewer({ artifact }: { artifact: Artifact }) {
<span className="docv-meta">{artifact.kind}{humanSize(artifact.size) ? ` · ${humanSize(artifact.size)}` : ''}</span> <span className="docv-meta">{artifact.kind}{humanSize(artifact.size) ? ` · ${humanSize(artifact.size)}` : ''}</span>
<span className="spacer" /> <span className="spacer" />
{dirty && <span className="docv-dirty">未保存</span>} {dirty && <span className="docv-dirty">未保存</span>}
{isTextKind(artifact) && ( {textKind && (
<> <>
<Btn variant="ghost" icon={editing ? 'eye' : 'list'} onClick={() => setEditing(e => !e)}>{editing ? '预览' : '编辑'}</Btn> <Btn variant="ghost" icon={editing ? 'eye' : 'list'} onClick={() => setEditing(e => !e)}>{editing ? '预览' : '编辑'}</Btn>
{editing && <Btn variant="primary" icon="check" disabled={!dirty} onClick={async () => { {editing && <Btn variant="primary" icon="check" disabled={!dirty} onClick={async () => {
@@ -44,25 +51,11 @@ export function DocViewer({ artifact }: { artifact: Artifact }) {
</a> </a>
</div> </div>
<div className="docv-body"> <div className="docv-body">
{artifact.kind === 'image' ? ( {editing && textKind ? (
<img src={api.artifactContentUrl(artifact.id)} alt={artifact.name} style={{ maxWidth: '100%', borderRadius: 8 }} />
) : isPdf ? (
<iframe src={api.artifactContentUrl(artifact.id)} title={artifact.name} className="docv-pdf" />
) : isBinaryKind(artifact) ? (
<EmptyState icon={artifactIcon(artifact.name, artifact.kind)}>
<b>{artifact.name}</b>
<div>{humanSize(artifact.size) || '二进制文件'} · 浏览器不支持此格式的在线预览</div>
<a className="btn v-primary" href={api.artifactDownloadUrl(artifact.id)} download={artifact.name}>
<Icon name="download" size={14} /> 下载文件
</a>
</EmptyState>
) : editing ? (
<textarea className="search-input docv-editor" value={content ?? ''} <textarea className="search-input docv-editor" value={content ?? ''}
onChange={e => { setContent(e.target.value); setDirty(true) }} /> onChange={e => { setContent(e.target.value); setDirty(true) }} />
) : isCsv || isJson ? (
<pre className="docv-pre">{content ?? '(加载中…'}</pre>
) : ( ) : (
<div className="doc-viewer" dangerouslySetInnerHTML={{ __html: marked.parse(content ?? '(加载中…') as string }} /> <FilePreview source={source} />
)} )}
</div> </div>
</div> </div>
@@ -73,7 +66,3 @@ export function DocViewer({ artifact }: { artifact: Artifact }) {
function isTextKind(a: Artifact): boolean { function isTextKind(a: Artifact): boolean {
return a.kind === 'document' && /\.(md|txt|csv|log|json)$/i.test(a.name) return a.kind === 'document' && /\.(md|txt|csv|log|json)$/i.test(a.name)
} }
function isBinaryKind(a: Artifact): boolean {
return /\.(docx|pdf|xlsx|pptx|doc|xls|ppt|zip|png|jpg|jpeg|gif|webp)$/i.test(a.name)
}
+46 -13
View File
@@ -1,9 +1,10 @@
// 右栏终态(提案 T4/D25):纯收件箱 + 「当前任务」常驻卡(计划/待办并入;工件/代理/ // 右栏终态(提案 T4/D25 → 预览双落位):收件箱/预览两页签 + 「当前任务」常驻卡。
// 插件面板迁为工作区弹层,见 Workspaces.tsx)。 // 预览页签(裁定 D4)承载右栏轻预览:FilePreview compact 模式,边聊边看。
import { store, useStore } from '../store' import { store, useStore } from '../store'
import type { Run } from '../contracts' import type { Run } from '../contracts'
import { InboxList, pendingCount, type ActivityCard, type DocBridge } from '@ui' import { InboxList, pendingCount, FilePreview, type ActivityCard, type DocBridge } from '@ui'
import { Icon } from '@ui' import { Icon } from '@ui'
import { api } from '../api'
// 工件解析/打开注入:渲染器纯展示,doc-ref 词汇经此桥接本仓状态(D29)。 // 工件解析/打开注入:渲染器纯展示,doc-ref 词汇经此桥接本仓状态(D29)。
export const docBridge: DocBridge = { export const docBridge: DocBridge = {
@@ -19,23 +20,55 @@ export function Workbench() {
const plan = useStore(s => s.plan) const plan = useStore(s => s.plan)
const todos = useStore(s => s.todos) const todos = useStore(s => s.todos)
const pending = pendingCount(cards) const pending = pendingCount(cards)
const rightTab = useStore(s => s.ui.rightTab)
const railId = useStore(s => s.ui.railPreviewId)
const railArtifact = useStore(s => (railId ? s.artifacts.find(a => a.id === railId) : undefined))
const showPreview = rightTab === 'preview'
return ( return (
<> <>
<div className="tabs"> <div className="tabs">
<span className="tab active">收件箱{pending > 0 ? ` ${pending}` : ''}</span> <button className={`tab ${!showPreview ? 'active' : ''}`} onClick={() => store.setUI({ rightTab: 'inbox' })}
title="收件箱">收件箱{pending > 0 ? ` ${pending}` : ''}</button>
<button className={`tab ${showPreview ? 'active' : ''}`} onClick={() => store.railPreview(railArtifact ?? null)}
title="预览(右栏轻预览)"><Icon name="eye" size={13} /> 预览</button>
<span className="spacer" style={{ flex: 1 }} />
<button className="tab tab-action" title="收纳已完成卡(done→关闭;已确认卡留痕不动)" onClick={() => store.tidyCards()}><Icon name="check" size={13} /> 收纳</button> <button className="tab tab-action" title="收纳已完成卡(done→关闭;已确认卡留痕不动)" onClick={() => store.tidyCards()}><Icon name="check" size={13} /> 收纳</button>
<button className="tab" title="收起收件箱" onClick={store.toggleRight}><Icon name="chevron-right" size={14} /></button> <button className="tab" title="收起收件箱" onClick={store.toggleRight}><Icon name="chevron-right" size={14} /></button>
</div> </div>
<div className="tab-body"> {showPreview ? (
<CurrentTaskCard plan={plan} todos={todos} /> <div className="tab-body" data-rail-preview="">
<InboxList cards={cards} docBridge={docBridge} callbacks={{ {railArtifact ? (
onAction: (c: ActivityCard, id: string) => store.cardAction(c, id), <>
onPin: (c: ActivityCard, p: boolean) => store.cardPin(c, p), <div className="rail-preview-head">
onDismiss: (c: ActivityCard) => store.cardDismiss(c), <span className="rp-name" title={railArtifact.name}>{railArtifact.name}</span>
onLocate: (c: ActivityCard) => store.openInbox(c.id), <span className="spacer" style={{ flex: 1 }} />
}} /> <button className="icon-btn" title="深预览(弹窗)" onClick={() => store.viewArtifact(railArtifact)}><Icon name="panel-left" size={14} /></button>
</div> <a className="icon-btn" title="下载" href={api.artifactDownloadUrl(railArtifact.id)} download={railArtifact.name}><Icon name="download" size={14} /></a>
<button className="icon-btn" title="关闭预览" onClick={() => store.railPreview(null)}><Icon name="x" size={14} /></button>
</div>
<FilePreview compact source={{
kind: 'artifact',
id: railArtifact.id,
name: railArtifact.name,
mime: railArtifact.mime,
contentUrl: api.artifactContentUrl(railArtifact.id),
downloadUrl: api.artifactDownloadUrl(railArtifact.id),
}} />
</>
) : <div className="empty-note" style={{ padding: 14 }}>在消息工件卡片或工件面板点「右栏预览」——文件会在这里边聊边看(md/图片/pdf/Office/代码均可)</div>}
</div>
) : (
<div className="tab-body">
<CurrentTaskCard plan={plan} todos={todos} />
<InboxList cards={cards} docBridge={docBridge} callbacks={{
onAction: (c: ActivityCard, id: string) => store.cardAction(c, id),
onPin: (c: ActivityCard, p: boolean) => store.cardPin(c, p),
onDismiss: (c: ActivityCard) => store.cardDismiss(c),
onLocate: (c: ActivityCard) => store.locateMessage(c.anchorMessageId),
}} />
</div>
)}
</> </>
) )
} }
+7 -17
View File
@@ -2,7 +2,7 @@
// ⌘1–9 快捷键与左栏快捷区共享同一清单(workspaceEntries)。 // ⌘1–9 快捷键与左栏快捷区共享同一清单(workspaceEntries)。
import { useState } from 'react' import { useState } from 'react'
import { store, useStore } from '../store' import { store, useStore } from '../store'
import type { Run, Artifact } from '../contracts' import type { Run } from '../contracts'
import { api } from '../api' import { api } from '../api'
import { OverlayHost, Icon, type DocBridge } from '@ui' import { OverlayHost, Icon, type DocBridge } from '@ui'
import { docBridge } from './Workbench' import { docBridge } from './Workbench'
@@ -55,26 +55,18 @@ export function Workspaces() {
return null return null
} }
/** 工件面板(工作区文件管理):搜索 + kind 过滤 + 上传;行内预览/下载/删除(带确认)。 */ /** 工件面板(工作区文件管理):搜索 + kind 过滤 + 上传;行内 深预览/右栏预览/下载。
* 删除是保留能力:DELETE /api/artifacts/{id} 与 api.deleteArtifact 均在(插件可用),
* 系统默认不提供删除入口(提案 2026-09-30-file-preview-and-reserved-capabilities §2.2)。 */
export function ArtifactsPanel() { export function ArtifactsPanel() {
const artifacts = useStore(s => s.artifacts) const artifacts = useStore(s => s.artifacts)
const [q, setQ] = useState('') const [q, setQ] = useState('')
const [kind, setKind] = useState<'all' | 'document' | 'data' | 'image'>('all') const [kind, setKind] = useState<'all' | 'document' | 'data' | 'image'>('all')
const [confirmDel, setConfirmDel] = useState<string | null>(null)
const kinds = ['all', 'document', 'data', 'image'] as const const kinds = ['all', 'document', 'data', 'image'] as const
const visible = artifacts.filter(a => const visible = artifacts.filter(a =>
(kind === 'all' || a.kind === kind) && a.name.toLowerCase().includes(q.toLowerCase())) (kind === 'all' || a.kind === kind) && a.name.toLowerCase().includes(q.toLowerCase()))
const del = async (a: Artifact) => {
try {
await api.deleteArtifact(a.id)
if (store.getState().ui.viewingArtifact?.id === a.id) store.viewArtifact(null)
store.toast(`已删除 ${a.name}`)
} catch (e) { store.toast(`删除失败:${e}`) }
if (store.getState().currentProjectId) store.refreshArtifacts(store.getState().currentProjectId!)
}
return ( return (
<div className="artifacts-panel"> <div className="artifacts-panel">
<div className="ap-toolbar"> <div className="ap-toolbar">
@@ -93,7 +85,7 @@ export function ArtifactsPanel() {
</label> </label>
</div> </div>
{artifacts.length === 0 {artifacts.length === 0
? <div className="empty-note">暂无文件——上传附件或让模型产出文档(mock:#doc);这里可浏览、预览、下载与删除</div> ? <div className="empty-note">暂无文件——上传附件或让模型产出文档(mock:#doc);这里可浏览、预览、下载</div>
: visible.length === 0 : visible.length === 0
? <div className="empty-note">没有匹配「{q}」的文件</div> ? <div className="empty-note">没有匹配「{q}」的文件</div>
: visible.map(a => ( : visible.map(a => (
@@ -104,12 +96,10 @@ export function ArtifactsPanel() {
<span className="meta">{a.kind} · {humanSize(a.size) || '—'} · {shortTime(a.updatedAt)}</span> <span className="meta">{a.kind} · {humanSize(a.size) || '—'} · {shortTime(a.updatedAt)}</span>
</span> </span>
<span className="ar-acts"> <span className="ar-acts">
<button className="icon-btn" title="预览" onClick={() => store.viewArtifact(a)}><Icon name="eye" size={14} /></button> <button className="icon-btn" title="预览(弹窗)" onClick={() => store.viewArtifact(a)}><Icon name="eye" size={14} /></button>
<button className="icon-btn" title="右栏预览(边聊边看)" onClick={() => store.railPreview(a)}><Icon name="panel-left" size={14} /></button>
<a className="icon-btn" title="下载" href={api.artifactDownloadUrl(a.id)} download={a.name} <a className="icon-btn" title="下载" href={api.artifactDownloadUrl(a.id)} download={a.name}
onClick={e => e.stopPropagation()}><Icon name="download" size={14} /></a> onClick={e => e.stopPropagation()}><Icon name="download" size={14} /></a>
{confirmDel === a.id
? <button className="icon-btn danger" title="确认删除" onClick={() => { setConfirmDel(null); del(a) }}><Icon name="check" size={14} /></button>
: <button className="icon-btn" title="删除" onClick={() => { setConfirmDel(a.id); setTimeout(() => setConfirmDel(c => c === a.id ? null : c), 2500) }}><Icon name="trash" size={14} /></button>}
</span> </span>
</div> </div>
))} ))}
+1 -1
View File
@@ -33,7 +33,7 @@ export interface Part {
artifact?: { artifactId: ID; name: string; kind?: string }; artifact?: { artifactId: ID; name: string; kind?: string };
hitl?: { cardId: ID; question: string; resolved: boolean; answer?: string }; hitl?: { cardId: ID; question: string; resolved: boolean; answer?: string };
plan?: PlanSnapshot; plan?: PlanSnapshot;
card?: { kind: string; data: UINode[] }; card?: { kind: string; data: UINode[]; cardId?: ID }; // cardId 非空 = 收件箱卡片引用部件(submit_card)
error?: { code: string; message: string }; error?: { code: string; message: string };
process?: boolean; // run 终态后折叠为摘要行 process?: boolean; // run 终态后折叠为摘要行
} }
+4
View File
@@ -1,10 +1,14 @@
import { StrictMode } from 'react' import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import { App } from './App' import { App } from './App'
import { wirePreviewCapabilities } from './previewsetup'
import '@ui/tokens.css' import '@ui/tokens.css'
import '@ui/ui.css' import '@ui/ui.css'
import './styles.css' import './styles.css'
// 预览能力装配(D1–D4):Office 解析器与链接预览 fetcher 注入 @ui(动态加载,不进主包)。
wirePreviewCapabilities()
createRoot(document.getElementById('root')!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>
<App /> <App />
+85
View File
@@ -0,0 +1,85 @@
// 预览能力装配(裁定 D1–D4):把重解析器经 configureFilePreviewLoaders 注入 @ui。
// 全部 dynamic import——对应格式首次预览时才下载 chunk,主 bundle 不含解析器。
// 链接预览 fetcher 走服务端 /api/url-preview(SSRF 防护在服务端执行端)。
import { configureFilePreviewLoaders, configureLinkPreviewFetcher } from '@ui'
import { auth } from './auth'
import type { LinkPreviewData } from '@ui'
const fetchBuf = async (url: string | undefined): Promise<ArrayBuffer> => {
const r = await fetch(url!)
if (!r.ok) throw new Error(`HTTP ${r.status}`)
return r.arrayBuffer()
}
const docxLoader = async (el: HTMLElement, source: { contentUrl?: string }) => {
const mammoth = await import('mammoth')
const { value } = await mammoth.convertToHtml({ arrayBuffer: await fetchBuf(source.contentUrl) })
el.innerHTML = value
}
const xlsxLoader = async (el: HTMLElement, source: { contentUrl?: string }) => {
const XLSX = await import('xlsx')
const wb = XLSX.read(await fetchBuf(source.contentUrl), { type: 'array' })
el.innerHTML = ''
const tabs = document.createElement('div')
tabs.className = 'ui-fp-sheet-tabs'
const wrap = document.createElement('div')
wrap.className = 'ui-fp-sheet-wrap'
el.append(tabs, wrap)
wb.SheetNames.forEach((name, i) => {
const tab = document.createElement('button')
tab.className = `ui-fp-sheet-tab ${i === 0 ? 'on' : ''}`
tab.textContent = name
tab.onclick = () => {
tabs.querySelectorAll('.ui-fp-sheet-tab').forEach(t => t.classList.remove('on'))
tab.classList.add('on')
renderSheet(wrap, XLSX.utils.sheet_to_json<unknown[]>(wb.Sheets[name], { header: 1, defval: '' }))
}
tabs.append(tab)
if (i === 0) renderSheet(wrap, XLSX.utils.sheet_to_json<unknown[]>(wb.Sheets[name], { header: 1, defval: '' }))
})
}
function renderSheet(wrap: HTMLElement, rows: unknown[][]) {
wrap.innerHTML = ''
if (rows.length > 200) {
const note = document.createElement('div')
note.className = 'ui-fp-note'
note.textContent = `仅显示前 200 行(共 ${rows.length} 行)——完整内容请下载`
wrap.append(note)
}
const table = document.createElement('table')
table.className = 'ui-fp-sheet'
for (const row of rows.slice(0, 200)) {
const tr = document.createElement('tr')
for (const cell of row) {
const td = document.createElement('td')
td.textContent = cell == null ? '' : String(cell)
td.title = td.textContent
tr.append(td)
}
table.append(tr)
}
wrap.append(table)
}
const pptxLoader = async (el: HTMLElement, source: { contentUrl?: string }) => {
// pptx2html@0.3.4 的 module 入口引用了大小写错配的 ./txml(Linux 打包必炸),
// 按 D2 备选条款改用 pptx-preview(init(dom).preview(arrayBuffer) 全保真渲染)。
const { init } = await import('pptx-preview')
el.innerHTML = ''
const stage = document.createElement('div')
stage.className = 'ui-fp-slide-stage'
el.append(stage)
const previewer = init(stage, { width: stage.clientWidth || 900 })
await previewer.preview(await fetchBuf(source.contentUrl))
}
export function wirePreviewCapabilities() {
configureFilePreviewLoaders({ docx: docxLoader, xlsx: xlsxLoader, pptx: pptxLoader })
configureLinkPreviewFetcher(async href => {
const r = await fetch(auth.withKey(`/api/url-preview?url=${encodeURIComponent(href)}`))
if (!r.ok) return { url: href, error: `HTTP ${r.status}` }
return (await r.json()) as LinkPreviewData
})
}
+15
View File
@@ -35,6 +35,8 @@ export interface UIState {
settingsOpen: boolean settingsOpen: boolean
trackerRunId: string | null trackerRunId: string | null
viewingArtifact: Artifact | null viewingArtifact: Artifact | null
/** 右栏轻预览的工件(rail 预览页签内容;与弹窗深预览 viewingArtifact 并存)。 */
railPreviewId: string | null
activeWorkspace: string | null activeWorkspace: string | null
taskOrg: 'project' | 'timeline' taskOrg: 'project' | 'timeline'
showArchived: boolean showArchived: boolean
@@ -83,6 +85,7 @@ const DEFAULT_UI: UIState = {
settingsOpen: false, settingsOpen: false,
trackerRunId: null, trackerRunId: null,
viewingArtifact: null, viewingArtifact: null,
railPreviewId: null,
taskOrg: 'project', taskOrg: 'project',
showArchived: false, showArchived: false,
lastProjectId: null, lastProjectId: null,
@@ -503,8 +506,20 @@ export const store = {
document.querySelector(`[data-card-id="${cardId}"]`)?.scrollIntoView({ behavior: 'smooth', block: 'center' }) document.querySelector(`[data-card-id="${cardId}"]`)?.scrollIntoView({ behavior: 'smooth', block: 'center' })
}, 60) }, 60)
}, },
// 卡→消息互定位另一半(D32):从收件箱卡滚回产生它的对话消息。
locateMessage(messageId?: string) {
if (!messageId) return
setTimeout(() => {
document.querySelector(`[data-msg-id="${messageId}"]`)?.scrollIntoView({ behavior: 'smooth', block: 'center' })
}, 60)
},
openTracker(runId: string) { setUI({ trackerRunId: runId }) }, openTracker(runId: string) { setUI({ trackerRunId: runId }) },
viewArtifact(a: Artifact | null) { setUI({ viewingArtifact: a }) }, viewArtifact(a: Artifact | null) { setUI({ viewingArtifact: a }) },
/** 右栏轻预览:打开指定工件并切到 rail 预览页签(双落位中的"轻"侧)。 */
railPreview(a: Artifact | null) {
if (!a) { setUI({ railPreviewId: null, rightTab: 'inbox' }); return }
setUI({ railPreviewId: a.id, rightTab: 'preview', rightCollapsed: false })
},
openSettings(open: boolean) { setUI({ settingsOpen: open }) }, openSettings(open: boolean) { setUI({ settingsOpen: open }) },
openProjectSettings(id: string | null) { setUI({ projectSettingsId: id }) }, openProjectSettings(id: string | null) { setUI({ projectSettingsId: id }) },
toast(msg: string) { toast(msg: string) {
+7
View File
@@ -397,3 +397,10 @@ button { font-family: inherit; font-size: inherit; color: inherit; }
.role-mark.role-assistant { background: none; border: none; padding: 0; } .role-mark.role-assistant { background: none; border: none; padding: 0; }
.msg.user .msg-role { align-items: center; } .msg.user .msg-role { align-items: center; }
.ad-user-cell { display: flex; align-items: center; gap: 8px; } .ad-user-cell { display: flex; align-items: center; gap: 8px; }
/* ---- 右栏轻预览(裁定 D4:右栏 + 弹窗双落位)---- */
.rail-preview-head { display: flex; align-items: center; gap: 6px; padding: 0 2px 8px; border-bottom: 1px solid var(--border-soft); margin-bottom: 8px; }
.rail-preview-head .rp-name { font-size: 13px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 55%; }
.tab-body[data-rail-preview] { display: flex; flex-direction: column; }
.tab-body[data-rail-preview] .ui-fp { flex: 1; }
.artifact-chip .ac-act { border: none; background: none; }
+1 -1
View File
@@ -1 +1 @@
{"root":["./src/App.tsx","./src/api.ts","./src/auth.ts","./src/contracts.ts","./src/live.ts","./src/main.tsx","./src/messageparts.ts","./src/passwords.ts","./src/store.ts","./src/tasklist.ts","./src/components/AdminUsersModal.tsx","./src/components/AgentTracker.tsx","./src/components/Conversation.tsx","./src/components/DocViewer.tsx","./src/components/LoginPage.tsx","./src/components/ProjectSettingsModal.tsx","./src/components/SettingsModal.tsx","./src/components/TaskSidebar.tsx","./src/components/Workbench.tsx","./src/components/Workspaces.tsx","./test/auth.test.ts","./test/contracts.test.ts","./test/live.test.ts","./test/messageparts.test.ts","./test/tasklist.test.ts"],"version":"5.9.3"} {"root":["./src/App.tsx","./src/api.ts","./src/auth.ts","./src/contracts.ts","./src/live.ts","./src/main.tsx","./src/messageparts.ts","./src/passwords.ts","./src/previewsetup.ts","./src/store.ts","./src/tasklist.ts","./src/components/AdminUsersModal.tsx","./src/components/AgentTracker.tsx","./src/components/Conversation.tsx","./src/components/DocViewer.tsx","./src/components/LoginPage.tsx","./src/components/ProjectSettingsModal.tsx","./src/components/SettingsModal.tsx","./src/components/TaskSidebar.tsx","./src/components/Workbench.tsx","./src/components/Workspaces.tsx","./test/auth.test.ts","./test/contracts.test.ts","./test/live.test.ts","./test/messageparts.test.ts","./test/tasklist.test.ts"],"version":"5.9.3"}