dsh-claude-slides 2.10.1 → 2.11.1

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/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- // dsh-claude-slides 2.10.1: built from baoyu-design.
1
+ // dsh-claude-slides 2.11.1: built from baoyu-design.
2
2
  var define_PLUGIN_default = { name: "claude-slides", provider: "dsh-claude-slides", skills: [{ name: "claude-slides", description: "Make and revise slide decks in the Slides artifact format (project/deck.json plus one HTML file per slide) as local files, then open them in a local slide editor where the user can present, retype, drag, and export PDF, HTML or PowerPoint. Use when the user asks for an editable slide deck or presentation in this format, names claude-slides or the Slides type, or wants to revise a folder that holds project/deck.json. Not for a one-off single-file HTML deck or prototype.", comments: true }] };
3
3
 
4
4
  import { readFile } from "node:fs/promises";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-claude-slides",
3
- "version": "2.10.1",
3
+ "version": "2.11.1",
4
4
  "description": "DeepSeek Harness plugin: the claude-slides skill from baoyu-design. The agent writes editable slide decks as files and opens them in a local slide editor.",
5
5
  "keywords": [
6
6
  "dsh",
@@ -21,6 +21,58 @@
21
21
  "type": "module",
22
22
  "main": "lib/index.js",
23
23
  "icon": "./icon.svg",
24
+ "baoyuDesign": {
25
+ "releaseNotes": [
26
+ {
27
+ "version": "2.11.1",
28
+ "date": "2026-11-05",
29
+ "en": "### Features\n- **DeepSeek Harness: the Artifacts page offers the plugins you haven't installed.** Beside the tiles for the skills you have, each other baoyu-design plugin gets a dimmed tile marked **Not installed**. Click it to see what the plugin makes and install it from there, at the same version as the ones you have, usually without a restart. Once it's in, **Start** opens a session with the skill. In an app that can't install from the page, the tile points you to the Plugins page. A plugin you installed and switched off still gets no tile.\n\n### Fixes\n- With `--agent-hook`, a comment could reach the agent twice: a second send could start the hook again after the first run had finished but before the comment was marked sent.",
30
+ "zh": "### 新功能\n- **DeepSeek Harness:「作品」页会列出你还没装的插件。** 已装技能的入口旁边,其他 baoyu-design 插件也各有一个入口,颜色淡一些,标着 **未安装**。点开会说明这个插件做什么,可以直接在这里安装,版本和你已装的一致,多数情况不用重启。装好后点 **新建** 就用这个技能开一个会话。应用不支持在页面里安装时,会引导你去插件页。装了但被你关掉的插件,仍然没有入口。\n\n### 修复\n- 用 `--agent-hook` 时,一条评论可能发给 agent 两次:第一次运行已经结束、评论还没标成已发送时,第二次发送会再启动一次 hook。"
31
+ },
32
+ {
33
+ "version": "2.11.0",
34
+ "date": "2026-11-05",
35
+ "en": "### Features\n- **DeepSeek Harness: a new version no longer pops up over the app.** The **Artifacts** row in the left sidebar gets an **Update** label instead (a dot on the icon, where a collapsed sidebar keeps its icons), and a panel at the top of the Artifacts page shows the versions, each plugin and what the release changes. **Update all** updates them together; **Update** on a plugin's line takes just that one. **Later** folds the panel to one line and removes the label until the next version.\n- While an update runs you can leave the Artifacts page: the label says **Updating**, **Failed** or **Restart**. **Try again** picks up where it stopped: the rest after **Update all**, that plugin after its own **Update**.\n- The **Update** button on a plugin's page now updates only that plugin, and a page that was open when its plugin was replaced comes back by itself.",
36
+ "zh": "### 新功能\n- **DeepSeek Harness:新版本不再弹出来打断你。** 左侧栏的 **Artifacts**(「作品」)这一行会多一个 **更新** 标签(侧栏收起后还留着图标的话,是图标上的小圆点),「作品」页顶部的面板写着版本、每个插件和这次改了什么。**全部更新** 一起更新;每个插件那一行的 **更新** 只更新它自己。**稍后** 把面板收成一行,标签也消失,等下一个版本再提示。\n- 更新时可以离开「作品」页:标签会显示 **更新中**、**更新失败** 或 **待重启**。**重试** 从停下的地方接着来:**全部更新** 失败就接着更新剩下的,单独更新失败就再试这一个。\n- 插件详情页的 **更新** 按钮现在只更新这一个插件;更新时开着的页面被替换后,会自己回来。"
37
+ },
38
+ {
39
+ "version": "2.10.1",
40
+ "date": "2026-11-05",
41
+ "en": "### Fixes\n- DeepSeek Harness 0.2.2 has its own plugin updates, so a plugin's page showed two update buttons and two tags, the app's and the plugin's. It now shows one: the app's for a plugin the card hasn't updated, the plugin's own for one it has.\n- The one-click update looks again at what there is to update before it starts, so a plugin the app's button just updated isn't tried again. When the reply to an update is lost, the card no longer says the plugin stayed on its version: it asks you to check on the Plugins page.\n- The [how-to](dsh-plugins/README.md#if-something-doesnt-work) says what to do when the app's own **Update to x** fails on the second of two plugins and switches it off, and that 2.9.2 and earlier can use that button on DeepSeek Harness 0.2.2 instead of updating by hand.",
42
+ "zh": "### 修复\n- DeepSeek Harness 0.2.2 自带了插件更新,插件详情页因此出现两个更新按钮和两个标签,应用的和插件的各一套。现在只有一个:没用卡片更新过的插件显示应用的按钮,更新过的显示插件自己的。\n- 一键更新开始前会重新确认要更新什么,刚用应用自带按钮更新过的插件不会再更新一次。更新的回复丢失时,卡片不再说插件还是原来的版本,而是请你到插件页确认。\n- [使用说明](dsh-plugins/README.zh-CN.md#遇到问题)写明了应用自带的 **更新到 x** 更新第二个插件失败、插件被关掉时怎么办;也说明了在 DeepSeek Harness 0.2.2 上,2.9.2 和更早的版本可以直接用这个按钮更新,不用手动。"
43
+ },
44
+ {
45
+ "version": "2.10.0",
46
+ "date": "2026-11-05",
47
+ "en": "### Features\n- **The DeepSeek Harness plugins update in one click.** When a new version is out, the app offers it: a card at the bottom right, a line at the top of the **Artifacts** page, and an **Update** button on each plugin's page. One click updates the plugins together, with nothing to uninstall, and most updates need no restart. If an update fails, the plugin stays on the version it was. A plugin added from a folder isn't offered one. This starts with 2.10.0, so 2.9.2 and earlier need [one last update by hand](dsh-plugins/README.md#update).\n- **DeepSeek Harness: a skill can be kept to one agent preset.** Turn off the plugin's own row and declare the plugin in the preset, and `/` lists the skill only in that preset's sessions. The **Artifacts** page and comments keep working, through a new package, `dsh-claude-artifacts`, that each plugin installs with it. See [how to set it up](dsh-plugins/README.md#keep-a-skill-to-one-preset).\n\n### Fixes\n- `archive` and the **Whole folder** export zipped `.git`, including the remote's address and any token in it, and that zip is what gets sent to other people. `.git` now stays out, at any depth.\n- An artifact folder's default `.gitignore` took `exports/` (a film's MP4 too) and two of the three temp file shapes into git, and its `history/` rule also matched a project's own folder of that name, which git then dropped. `history/` and `exports/` now match only at the top of the folder.\n- `share` could give out a TUN proxy's address, such as sing-box's `172.19.0.1`, first when the system lists it first. It now skips an address that answers at every port, and lists tunnels after the local network.\n- Design's Standalone export: a `#id` link did nothing when the file was opened directly, and over `http://` it also scrolled the page around the frame. A click now scrolls inside the frame only, as Play does.",
48
+ "zh": "### 新功能\n- **DeepSeek Harness 插件可以一键更新。** 出了新版本,应用会提示你:右下角弹出卡片,**Artifacts** 页(中文界面是「作品」)顶部和每个插件的详情页也有 **Update**(更新)按钮。点一下,这些插件一起更新,不用卸载,多数情况也不用重启。更新失败时插件还是原来的版本。从文件夹安装的插件不会收到提示。这个功能从 2.10.0 开始,所以 2.9.2 和更早的版本要[最后手动更新一次](dsh-plugins/README.zh-CN.md#更新)。\n- **DeepSeek Harness:技能可以只留给某个 Agent 预设。** 关掉插件自己那一行,再在预设里声明这个插件,`/` 就只在这个预设的会话里列出这个技能。作品页和评论照常工作,它们现在由新包 `dsh-claude-artifacts` 负责,每个插件都会带上它。[设置方法](dsh-plugins/README.zh-CN.md#只让某个预设用某个技能)。\n\n### 修复\n- `archive` 和导出的 **Whole folder**(整个文件夹)会把 `.git` 打进 zip,里面有远程仓库地址和可能带着的 token,而这个 zip 正是要发给别人的。现在任何层级的 `.git` 都不会打进去。\n- 作品文件夹默认的 `.gitignore` 会把 `exports/`(包括影片的 MP4)和三种临时文件里的两种提交进 git;它的 `history/` 规则还会匹配项目自己的同名文件夹,导致 git 把那个文件夹漏掉。现在 `history/` 和 `exports/` 只匹配文件夹顶层。\n- 系统把 TUN 代理的网卡排在前面时,`share` 可能先给出代理的地址,比如 sing-box 的 `172.19.0.1`。现在会跳过在所有端口都有应答的地址,并把隧道网卡排在局域网后面。\n- 设计稿的 Standalone 导出:直接打开文件时,`#id` 链接点了没反应;通过 `http://` 打开时,还会连带滚动框架外的页面。现在点击只在框架内滚动,和 Play 一样。"
49
+ },
50
+ {
51
+ "version": "2.9.2",
52
+ "date": "2026-11-04",
53
+ "en": "### Fixes\n- DeepSeek Harness on Windows: the command the skills told the agent to run was written for a POSIX shell, and PowerShell reads a quoted path at the start of a line as a string, not a command. On Windows they now give `& '<node>' '<script>' <command>`, and say that PowerShell has no heredoc.",
54
+ "zh": "### 修复\n- Windows 上的 DeepSeek Harness:技能告诉 agent 运行的命令是按 POSIX shell 写的,而 PowerShell 会把行首带引号的路径当成字符串,不当成命令。现在 Windows 上给的是 `& '<node>' '<script>' <command>`,并说明 PowerShell 没有 heredoc。"
55
+ },
56
+ {
57
+ "version": "2.9.1",
58
+ "date": "2026-11-04",
59
+ "en": "### Fixes\n- DeepSeek Harness on Windows: the plugins only recognized a shell tool named `bash`, and on Windows it is named `pwsh`, so they never noticed an `open`. The folder stayed off the **Artifacts** page, **Send to Agent** never appeared, and comments did not reach the agent. They now recognize both names (by @leil894-collab, [#31](https://github.com/JimLiu/baoyu-design/pull/31); reported by @EmotionTowel, [#30](https://github.com/JimLiu/baoyu-design/issues/30)).",
60
+ "zh": "### 修复\n- Windows 上的 DeepSeek Harness:插件只认名叫 `bash` 的 shell 工具,而它在 Windows 上叫 `pwsh`,所以一直注意不到 `open`。文件夹不会出现在作品页,**Send to Agent** 不出现,评论也到不了 Agent。现在两个名字都认(by @leil894-collab,[#31](https://github.com/JimLiu/baoyu-design/pull/31);@EmotionTowel 报告,[#30](https://github.com/JimLiu/baoyu-design/issues/30))。"
61
+ },
62
+ {
63
+ "version": "2.9.0",
64
+ "date": "2026-11-03",
65
+ "en": "### Features\n- **ChatGPT plugins.** The skills now also come as plugins for the ChatGPT desktop app and Codex: Baoyu Slides, Baoyu Canvas, Baoyu Design System, Baoyu Dashboard, Baoyu Motion (with `motion-director` and `dataviz`) and Baoyu Dataviz. This repository is their marketplace: run `codex plugin marketplace add JimLiu/baoyu-design --sparse .agents/plugins --sparse chatgpt-plugins`, then install the ones you want. [chatgpt-plugins/README.md](chatgpt-plugins/README.md) has the steps.\n- DeepSeek Harness: each deck, canvas, design system, dashboard and film on the **Artifacts** page now has a real thumbnail, taken from its own editor and retaken when its files change. This needs the desktop app; in `dsh web` a card keeps a drawn picture of its type.\n- The **Artifacts** page's tiles and **+** menu start a new session with the input filled in, such as `/claude-slides Make me a slide deck about: `, and sending it loads that skill. Only plugins that are on get a tile.\n- The **Artifacts** page has grid and list views, search, sorting (last opened, last edited, date created, name) and filters (type, date opened, sharing, pinned).\n- Each card on the **Artifacts** page has a menu to pin, rename, copy the link or delete. Rename changes the title in the artifact itself, so its editor shows the new name. Delete moves the folder to `baoyu-design/trash/` in the app's data folder, with an Undo; a folder that also holds other files stays where it is and only leaves the list.\n\n### Fixes\n- In a sandbox that blocks local network, such as Codex's default one, `open` said `PORT_IN_USE`, as if every port were taken. It now says `NETWORK_BLOCKED` and how to run the command, and the skills say which commands need local network.",
66
+ "zh": "### 新功能\n- **ChatGPT 插件。** 这些技能也做成了 ChatGPT 桌面版和 Codex 的插件:Baoyu Slides、Baoyu Canvas、Baoyu Design System、Baoyu Dashboard、Baoyu Motion(带 `motion-director` 和 `dataviz`)和 Baoyu Dataviz。本仓库就是它们的 marketplace:运行 `codex plugin marketplace add JimLiu/baoyu-design --sparse .agents/plugins --sparse chatgpt-plugins`,再装你要的插件。步骤见 [chatgpt-plugins/README.zh-CN.md](chatgpt-plugins/README.zh-CN.md)。\n- DeepSeek Harness:**Artifacts** 页(中文界面是「作品」)上的幻灯片、画布、设计系统、仪表盘和影片现在都有真实的缩略图,从各自的编辑器里截取,文件改过后会重新截。这要在桌面版里;`dsh web` 的卡片仍然是画出来的类型示意图。\n- 作品页顶部的入口和 **+** 菜单会新开一个会话,并填好输入框,比如 `/claude-slides 帮我做一份幻灯片,主题是:`,发送时会加载对应的技能。只有开着的插件才有入口。\n- 作品页可以切换网格和列表,可以搜索、排序(最近打开、最近修改、创建时间、名称)和筛选(类型、打开时间、共享、已固定)。\n- 作品页的每张卡片都有菜单,可以固定、重命名、复制链接和删除。重命名改的是作品本身的标题,编辑器里也会显示新名字。删除会把文件夹移到应用数据目录的 `baoyu-design/trash/`,可以撤销;文件夹里还有别的文件时,文件夹留在原处,只从列表移除。\n\n### 修复\n- 在不允许访问本机网络的沙箱里(比如 Codex 的默认沙箱),`open` 会报 `PORT_IN_USE`,好像所有端口都被占了。现在它报 `NETWORK_BLOCKED`,并说明该怎么运行;技能说明里也写了哪些命令要访问本机网络。"
67
+ },
68
+ {
69
+ "version": "2.8.2",
70
+ "date": "2026-11-02",
71
+ "en": "### Fixes\n- `claude-slides`: in a PowerPoint export, Chinese, Japanese or Korean text could show as boxes when its East Asian font was missing on the reader's computer or held none of those characters: Arial in the basic-fonts export, a Latin family, or an embedded subset that left characters out of a long deck. That text now names a font Windows and Office have instead (Microsoft YaHei, Microsoft JhengHei, Yu Gothic or Malgun Gothic). Text the embedded font holds, text without these characters and the Google Slides export are unchanged ([#29](https://github.com/JimLiu/baoyu-design/issues/29)).",
72
+ "zh": "### 修复\n- `claude-slides`:导出 PowerPoint 时,如果中日韩文字用的东亚字体在对方电脑上没有,或者这个字体本身不含这些字(basic 字体导出里的 Arial、拉丁字体、长 deck 里漏了字的内嵌子集),文字可能显示成方块。这类文字现在改用 Windows 和 Office 常见的字体(Microsoft YaHei、Microsoft JhengHei、Yu Gothic 或 Malgun Gothic)。内嵌字体装得下的文字、不含这些字的文字和 Google Slides 导出都不变([#29](https://github.com/JimLiu/baoyu-design/issues/29))。"
73
+ }
74
+ ]
75
+ },
24
76
  "exports": {
25
77
  ".": "./lib/index.js",
26
78
  "./cordis.patch.yml": "./cordis.patch.yml",
@@ -43,7 +95,7 @@
43
95
  "dsh": ">=0.2.0-0"
44
96
  },
45
97
  "dependencies": {
46
- "dsh-claude-artifacts": "^2.10.1"
98
+ "dsh-claude-artifacts": "^2.11.1"
47
99
  },
48
100
  "dsh": {
49
101
  "bundle": {
@@ -85,7 +85,7 @@ h2 { flex: 1; min-width: 0; margin: 0; font-size: 17px; font-weight: 600; letter
85
85
  .toast p { margin: 0; font-size: 13.5px; overflow-wrap: anywhere; }
86
86
  .toast .path { margin-top: 4px; font: 12.5px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--fg); user-select: text; -webkit-user-select: text; overflow-wrap: anywhere; }
87
87
  .toast.err p { color: var(--danger); }
88
- `;function V(){if(/\bElectron\/\d/.test(navigator.userAgent))return!0;if(window.top===window)return!1;try{return window.top.location.origin!==location.origin}catch{return!0}}function F(t){let e=/filename\*=UTF-8''([^;]+)/i.exec(t??"");if(e)try{return decodeURIComponent(e[1])}catch{}return/filename="([^"]+)"/i.exec(t??"")?.[1]??""}async function q(t){let e=await t.json().catch(()=>null);if(!t.ok||!e||typeof e.path!="string")throw new Error(e?.message||`HTTP ${t.status}`);return e}var Y=class{layer=o("div",{"data-baoyu-save-layer":""});wrap=o("div",{class:"wrap"});node=null;timer;constructor(){this.layer.attachShadow({mode:"open"}).append(o("style",{},E+Ce),this.wrap)}saved(e,n=N.hint){this.show([o("p",{},N.saved(e.name)),o("div",{class:"path"},e.shown),o("div",{class:"hint"},n)],!1)}failed(e){this.show([o("p",{},e)],!0)}show(e,n){this.layer.isConnected||document.body.append(this.layer),window.clearTimeout(this.timer),this.node?.remove();let i=o("div",{class:`toast${n?" err":""}`,role:n?"alert":"status","data-baoyu-save-toast":""},...e);this.wrap.append(i),this.node=i,this.timer=window.setTimeout(()=>{i.remove(),this.node===i&&(this.node=null)},n?1e4:12e3)}},He=null;function U(){return He??=new Y}async function Ne(t,e){return q(await fetch(`${Oe}?name=${encodeURIComponent(e)}`,{method:"POST",body:t,headers:{"Content-Type":t.type||"application/octet-stream"}}))}function _e(t){for(let e of t.composedPath())if(e instanceof HTMLAnchorElement&&e.hasAttribute("download")&&e.href)return e;return null}function ze(t){try{let e=new URL(t.href,location.href);return e.protocol==="blob:"||e.protocol==="data:"||e.origin===location.origin}catch{return!1}}function ce(t){if(!V())return!1;let e=U(),n=async(a,c,u={})=>{try{let f=await Ne(a,c);return u.quiet||e.saved(f),f}catch(f){throw u.quiet||e.failed(N.failed(c,f instanceof Error?f.message:String(f))),f}},i=a=>{let c=a.getAttribute("download")??"",u=a.href,f=fetch(u,{cache:"no-store"});(async()=>{let h=c||"download",g;try{let m=await f;if(!m.ok)throw new Error(`HTTP ${m.status}`);h=c||F(m.headers.get("Content-Disposition"))||"download",g=await m.blob()}catch(m){return e.failed(N.failed(h,m instanceof Error?m.message:String(m)))}await n(g,h).catch(()=>{})})()},r=a=>a!==null&&(!t||ze(a)),s=a=>{t?i(a):e.failed(N.guest)};window.addEventListener("click",a=>{let c=_e(a);a.defaultPrevented||!r(c)||(a.preventDefault(),s(c))},!0);let p=HTMLAnchorElement.prototype.click;return HTMLAnchorElement.prototype.click=function(){if(!this.isConnected&&this.hasAttribute("download")&&this.href&&r(this))return s(this);p.call(this)},t&&(window.__baoyuSave=n),!0}var J="/__share/archive",je={label:"Whole folder (.zip)",description:"Everything here, to back up or move"},R=y?{started:t=>`\u5F00\u59CB\u4E0B\u8F7D ${t}`,hint:"\u6574\u4E2A\u6587\u4EF6\u5939\u6253\u6210\u4E86\u4E00\u4E2A zip\uFF0C\u53EF\u4EE5\u5907\u4EFD\u3001\u6362\u7535\u8111\u6216\u53D1\u7ED9\u522B\u4EBA\u3002\u89E3\u538B\u540E\u7528\u540C\u4E00\u4E2A skill \u6253\u5F00\u5C31\u80FD\u63A5\u7740\u7528\uFF1B\u5206\u4EAB\u94FE\u63A5\u548C\u8FD9\u53F0\u7535\u8111\u4E0A\u7684\u670D\u52A1\u5668\u72B6\u6001\u4E0D\u5728\u91CC\u9762\u3002",beside:"\u8FD9\u4E2A\u6D4F\u89C8\u5668\u4E0D\u80FD\u4E0B\u8F7D\u6587\u4EF6\uFF0Czip \u5B58\u5728\u4E86\u6587\u4EF6\u5939\u65C1\u8FB9\u3002\u89E3\u538B\u540E\u7528\u540C\u4E00\u4E2A skill \u6253\u5F00\u5C31\u80FD\u63A5\u7740\u7528\u3002",tooLarge:"\u6587\u4EF6\u5939\u8D85\u8FC7\u4E86 zip \u7684\u4E0A\u9650\uFF084 GiB \u6216 65535 \u4E2A\u6587\u4EF6\uFF09\uFF0C\u8BF7\u76F4\u63A5\u590D\u5236\u6587\u4EF6\u5939\u3002",failed:"\u6CA1\u80FD\u6253\u5305\uFF1A"}:{started:t=>`Downloading ${t}`,hint:"The whole folder in one zip, to back up, move to another computer or send to someone. Unzip it and open it with the same skill to carry on; share links and this computer's server state are left out.",beside:"This browser doesn't download files, so the zip went beside the folder. Unzip it and open it with the same skill to carry on.",tooLarge:"The folder is over what a zip holds (4 GiB or 65,535 files): copy the folder itself instead.",failed:"Could not zip the folder: "},Be=`
88
+ `;function V(){if(window.__baoyuNativeDownloads===!0)return!1;if(/\bElectron\/\d/.test(navigator.userAgent))return!0;if(window.top===window)return!1;try{return window.top.location.origin!==location.origin}catch{return!0}}function F(t){let e=/filename\*=UTF-8''([^;]+)/i.exec(t??"");if(e)try{return decodeURIComponent(e[1])}catch{}return/filename="([^"]+)"/i.exec(t??"")?.[1]??""}async function q(t){let e=await t.json().catch(()=>null);if(!t.ok||!e||typeof e.path!="string")throw new Error(e?.message||`HTTP ${t.status}`);return e}var Y=class{layer=o("div",{"data-baoyu-save-layer":""});wrap=o("div",{class:"wrap"});node=null;timer;constructor(){this.layer.attachShadow({mode:"open"}).append(o("style",{},E+Ce),this.wrap)}saved(e,n=N.hint){this.show([o("p",{},N.saved(e.name)),o("div",{class:"path"},e.shown),o("div",{class:"hint"},n)],!1)}failed(e){this.show([o("p",{},e)],!0)}show(e,n){this.layer.isConnected||document.body.append(this.layer),window.clearTimeout(this.timer),this.node?.remove();let i=o("div",{class:`toast${n?" err":""}`,role:n?"alert":"status","data-baoyu-save-toast":""},...e);this.wrap.append(i),this.node=i,this.timer=window.setTimeout(()=>{i.remove(),this.node===i&&(this.node=null)},n?1e4:12e3)}},He=null;function U(){return He??=new Y}async function Ne(t,e){return q(await fetch(`${Oe}?name=${encodeURIComponent(e)}`,{method:"POST",body:t,headers:{"Content-Type":t.type||"application/octet-stream"}}))}function _e(t){for(let e of t.composedPath())if(e instanceof HTMLAnchorElement&&e.hasAttribute("download")&&e.href)return e;return null}function ze(t){try{let e=new URL(t.href,location.href);return e.protocol==="blob:"||e.protocol==="data:"||e.origin===location.origin}catch{return!1}}function ce(t){if(!V())return!1;let e=U(),n=async(a,c,u={})=>{try{let f=await Ne(a,c);return u.quiet||e.saved(f),f}catch(f){throw u.quiet||e.failed(N.failed(c,f instanceof Error?f.message:String(f))),f}},i=a=>{let c=a.getAttribute("download")??"",u=a.href,f=fetch(u,{cache:"no-store"});(async()=>{let h=c||"download",g;try{let m=await f;if(!m.ok)throw new Error(`HTTP ${m.status}`);h=c||F(m.headers.get("Content-Disposition"))||"download",g=await m.blob()}catch(m){return e.failed(N.failed(h,m instanceof Error?m.message:String(m)))}await n(g,h).catch(()=>{})})()},r=a=>a!==null&&(!t||ze(a)),s=a=>{t?i(a):e.failed(N.guest)};window.addEventListener("click",a=>{let c=_e(a);a.defaultPrevented||!r(c)||(a.preventDefault(),s(c))},!0);let p=HTMLAnchorElement.prototype.click;return HTMLAnchorElement.prototype.click=function(){if(!this.isConnected&&this.hasAttribute("download")&&this.href&&r(this))return s(this);p.call(this)},t&&(window.__baoyuSave=n),!0}var J="/__share/archive",je={label:"Whole folder (.zip)",description:"Everything here, to back up or move"},R=y?{started:t=>`\u5F00\u59CB\u4E0B\u8F7D ${t}`,hint:"\u6574\u4E2A\u6587\u4EF6\u5939\u6253\u6210\u4E86\u4E00\u4E2A zip\uFF0C\u53EF\u4EE5\u5907\u4EFD\u3001\u6362\u7535\u8111\u6216\u53D1\u7ED9\u522B\u4EBA\u3002\u89E3\u538B\u540E\u7528\u540C\u4E00\u4E2A skill \u6253\u5F00\u5C31\u80FD\u63A5\u7740\u7528\uFF1B\u5206\u4EAB\u94FE\u63A5\u548C\u8FD9\u53F0\u7535\u8111\u4E0A\u7684\u670D\u52A1\u5668\u72B6\u6001\u4E0D\u5728\u91CC\u9762\u3002",beside:"\u8FD9\u4E2A\u6D4F\u89C8\u5668\u4E0D\u80FD\u4E0B\u8F7D\u6587\u4EF6\uFF0Czip \u5B58\u5728\u4E86\u6587\u4EF6\u5939\u65C1\u8FB9\u3002\u89E3\u538B\u540E\u7528\u540C\u4E00\u4E2A skill \u6253\u5F00\u5C31\u80FD\u63A5\u7740\u7528\u3002",tooLarge:"\u6587\u4EF6\u5939\u8D85\u8FC7\u4E86 zip \u7684\u4E0A\u9650\uFF084 GiB \u6216 65535 \u4E2A\u6587\u4EF6\uFF09\uFF0C\u8BF7\u76F4\u63A5\u590D\u5236\u6587\u4EF6\u5939\u3002",failed:"\u6CA1\u80FD\u6253\u5305\uFF1A"}:{started:t=>`Downloading ${t}`,hint:"The whole folder in one zip, to back up, move to another computer or send to someone. Unzip it and open it with the same skill to carry on; share links and this computer's server state are left out.",beside:"This browser doesn't download files, so the zip went beside the folder. Unzip it and open it with the same skill to carry on.",tooLarge:"The folder is over what a zip holds (4 GiB or 65,535 files): copy the folder itself instead.",failed:"Could not zip the folder: "},Be=`
89
89
  .toast { position: fixed; right: 16px; bottom: 16px; z-index: 2147483003; width: min(380px, calc(100vw - 32px)); background: var(--bg); border: 1px solid var(--line); border-radius: 14px; box-shadow: var(--shadow); padding: 12px 14px; }
90
90
  .toast p { margin: 0; font-size: 13.5px; overflow-wrap: anywhere; }
91
91
  .toast.err p { color: var(--danger); }
@@ -451,6 +451,7 @@ data: ${JSON.stringify(snap)}
451
451
  if (agentHook) await runHook(agentHook);
452
452
  }
453
453
  async function runHook(command) {
454
+ if (typeof command !== "string") return runHandler(command);
454
455
  const doc = await readComments(root);
455
456
  for (const thread of pendingForAgent(doc)) {
456
457
  if (hookRunning.has(thread.id)) continue;
@@ -475,15 +476,15 @@ data: ${JSON.stringify(snap)}
475
476
  log(`comments: the agent hook failed to start: ${error.message}`);
476
477
  });
477
478
  child.on("exit", (code) => {
478
- hookRunning.delete(thread.id);
479
479
  if (code !== 0) {
480
+ hookRunning.delete(thread.id);
480
481
  log(`comments: the agent hook exited with ${code} for ${thread.id}; the comment stays waiting${stderr ? `: ${stderr.trim()}` : ""}`);
481
482
  return;
482
483
  }
483
484
  void updateComments(root, (d) => {
484
485
  const t = d.threads.find((x) => x.id === thread.id);
485
486
  if (t) markDelivered(t, now(), "hook");
486
- }).then(() => {
487
+ }).finally(() => hookRunning.delete(thread.id)).then(() => {
487
488
  log(`comments: sent to the agent (hook): ${thread.id}`);
488
489
  return broadcast();
489
490
  }).catch((error) => log(`comments: ${error.message}`));
@@ -524,6 +525,44 @@ data: ${JSON.stringify(snap)}
524
525
  });
525
526
  }
526
527
  }
528
+ async function runHandler(handler) {
529
+ for (const { id } of pendingForAgent(await readComments(root))) {
530
+ if (hookRunning.has(id)) continue;
531
+ hookRunning.add(id);
532
+ try {
533
+ const doc = await readComments(root);
534
+ const thread = pendingForAgent(doc).find((t) => t.id === id);
535
+ if (!thread) continue;
536
+ try {
537
+ await handler.thread(agentViews(doc, [thread])[0]);
538
+ } catch (error) {
539
+ log(`comments: the agent handler failed for ${id}; the comment stays waiting: ${error.message}`);
540
+ continue;
541
+ }
542
+ await updateComments(root, (d) => {
543
+ const t = d.threads.find((x) => x.id === id);
544
+ if (t) markDelivered(t, now(), "hook");
545
+ });
546
+ log(`comments: sent to the agent (handler): ${id}`);
547
+ await broadcast();
548
+ } finally {
549
+ hookRunning.delete(id);
550
+ }
551
+ }
552
+ const failed = [];
553
+ for (const request of requests.splice(0)) {
554
+ setRequestState(request.id, "working");
555
+ try {
556
+ await handler.request(request);
557
+ log(`comments: sent to the agent (handler): ${request.id}`);
558
+ } catch (error) {
559
+ failed.push(request);
560
+ setRequestState(request.id, "queued");
561
+ log(`comments: the agent handler failed for ${request.id}; the request stays waiting: ${error.message}`);
562
+ }
563
+ }
564
+ requests.unshift(...failed);
565
+ }
527
566
  let debounce;
528
567
  const changed = () => {
529
568
  clearTimeout(debounce);
@@ -3235,8 +3274,11 @@ var ShareHost = class {
3235
3274
  * their own artifact; a guest comes over a LAN address (this computer's own included: that is how the owner sees
3236
3275
  * what a role sees).
3237
3276
  */
3277
+ isLocal(request) {
3278
+ return this.options.local ? this.options.local(request) : isLoopback(request.socket.remoteAddress);
3279
+ }
3238
3280
  viewerOf(request) {
3239
- if (isLoopback(request.socket.remoteAddress)) return { role: "owner", user: this.options.owner };
3281
+ if (this.isLocal(request)) return { role: "owner", user: this.options.owner };
3240
3282
  const guest = this.on ? this.sharing.guest(this.cookie(request)) : null;
3241
3283
  if (!guest) return null;
3242
3284
  this.sharing.seen(guest);
@@ -3368,7 +3410,7 @@ var ShareHost = class {
3368
3410
  async join(request, response, method, token2) {
3369
3411
  const t = this.words(request);
3370
3412
  const role = this.sharing.linkRole(token2);
3371
- const local = isLoopback(request.socket.remoteAddress);
3413
+ const local = this.isLocal(request);
3372
3414
  const form = method === "POST" ? await readBody(request, 8 << 10).then((body) => body.toString("utf8"), () => "") : "";
3373
3415
  const title = this.options.title();
3374
3416
  if (!role || !local && !this.on) return this.sendPage(response, 404, t.linkGoneTitle, `<h1>${esc2(t.linkGoneTitle)}</h1><p>${esc2(t.linkGone)}</p>`);
@@ -4523,7 +4565,7 @@ function watchArtifact(store, onChange, onError) {
4523
4565
  };
4524
4566
  }
4525
4567
  var publishError = (response, status, code, message) => sendJson2(response, status, { code, message });
4526
- async function mountSite(root, { log: stamp2, network, agentHook }) {
4568
+ async function mountSite(root, { log: stamp2, network, agentHook, local }) {
4527
4569
  const project = path12.join(root, "project");
4528
4570
  const editorPath = pagePathOf(KIND, root);
4529
4571
  const projectDir = await fsp4.realpath(project);
@@ -4540,6 +4582,7 @@ async function mountSite(root, { log: stamp2, network, agentHook }) {
4540
4582
  title: () => artifactTitle(root, projectDir),
4541
4583
  log: stamp2,
4542
4584
  uiFile: path12.join(EDITOR_DIR, "share-ui.js"),
4585
+ ...local ? { local } : {},
4543
4586
  // The History panel's address and what this person may do there; on a version's page (?at=n), which version.
4544
4587
  globals: (viewer, query) => {
4545
4588
  const at = Number(query.get("at") ?? "");