dsh-tiddlywiki 0.13.2 → 0.14.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/README.md CHANGED
@@ -18,7 +18,7 @@
18
18
  | 能力 | 说明 |
19
19
  |---|---|
20
20
  | 🤖 **Agent 工具** | `tiddlywiki_search` / `get` / `put` / `batch_put` / `rename` / `delete` / `recent` / `list_tags` / `git_sync` / `git_resolve` 十个工具:检索、读写、批量、重命名、删除、git 同步与冲突解决 |
21
- | 📤 **一键发送给 Agent** | TW 笔记工具栏「**发送给 Agent**」按钮(首次启动自动写入 wiki):把当前笔记作为消息注入所选 dsh 会话(按工作区分组选择,可新建工作区/会话,**可选「工作模式」= Agent 预设**,如 默认/cordis/blade,已有会话显示其当前模式);**消息自动附加待办说明**——告知 Agent 这是用户提前编辑在 wiki 中的待办事项,不清楚应主动提问 |
21
+ | 📤 **一键发送给 Agent** | TW 笔记工具栏「**发送给 Agent**」按钮(首次启动自动写入 wiki;**独立加粗纸飞机图标**,遵循核心工具栏图标约定,工具栏设置列表也显示各按钮图标/说明):把当前笔记作为消息注入所选 dsh 会话(按工作区分组选择,可新建工作区/会话,**可选「工作模式」= Agent 预设**、**可选「权限」= 权限预设**(沙箱+审批),**可选附加说明**随消息一起发给 Agent,已有会话显示其当前模式);**消息自动附加待办说明**——告知 Agent 这是用户提前编辑在 wiki 中的待办事项,不清楚应主动提问 |
22
22
  | 🧭 **内嵌编辑器** | 侧边栏「TiddlyWiki」入口 → 中央列内嵌完整 TW 5 编辑器(**同源代理**,经 DSH origin 访问,Tailscale/内网/域名/HTTPS 均可用) |
23
23
  | 🌗 **跟随 DSH 主题** | 嵌入式 TW(中央面板 +「在 TW 中编辑」弹窗)**自适应 DSH 深浅主题**:暗色自动切深色 palette、浅色恢复原 palette;**纯内存切换,不写回 wiki、不进 git**;设置页可关可换深色 palette |
24
24
  | 📝 **快速笔记** | 右下角「知识库」悬浮按钮 → 快速笔记卡片:**CodeMirror 6** Markdown 编辑器(语法高亮 + 撤销/重做)、文件上传、多选/自动补全 tag、**草稿自动保存(刷新不丢)**、**「🕘 最近」一键载入旧笔记**,Ctrl+Enter 保存;可整体隐藏 |
@@ -33,6 +33,7 @@
33
33
 
34
34
  > 最近几个主要版本的一句话更新记录(完整变更见 git log / Releases)。
35
35
 
36
+ - **v0.14.0**(2026-09-05):TW「**发送给 Agent**」一键发送三处优化。① **工具栏按钮修复**——之前按钮在 控制台→外观→工具栏 里**不显示图标与说明**(还和「导出此条目」用同一个导出图标):现为按钮补上 `icon`/`caption`/`description` 字段,并在 bundle 里**自带独立的「发送」图标**(不再借用 core 的 export-button),工具栏设置与条目标题栏都显示独立图标;后续小修:图标改用**更饱满的实心纸飞机**并遵循核心图标约定(`\parameters (size:"22pt")` + `width/height` + `tc-image-button` class + `$:/tags/Image` 标签),修复宽度比相邻图标窄的问题;同时 bundle **shadow 覆盖核心 `$:/core/ui/ControlPanel/Toolbars/ItemTemplate`**(镜像 EditorItemTemplate),让 查看/页面/编辑工具栏 的设置列表**也显示每个按钮的图标**(原来只有编辑器工具栏列表显示);**再修「图标裂了」**——根因是 bundle 里 icon tiddler 被设了 `type: image/svg+xml`:`{{icon}}` 会走 imageparser 渲染成 `<img src="data:image/svg+xml,...">`,数据 URI 保留原始文本(开头的 `\parameters` 与未展开的 `width=<<size>>` 不是合法独立 SVG),Chrome/Edge 直接加载失败显示裂图;核心图标**不带 type 字段**(默认 wikitext),`{{icon}}` 被 wiki 化成语法展开后的**内联 `<svg width="22pt">`**。修复:icon tiddler 去掉 type 字段(保持 wikitext,与核心一致),已在真实浏览器验证图标渲染尺寸与核心图标完全一致(32.9×24.7)且无裂图;② **可选「附加说明」**——发送弹层新增文本输入框,填写的说明会以 `【附加说明】` 段随消息一起发给 Agent(不填则无此段);③ **可选「权限」**——弹层新增「权限(权限预设)」选择器(来自 DSH `permissionPresets`,如 工作区写入+询问 / 完全访问+免确认),**新建会话并发送**时 `/agent/create` 传 `permission` → 创建后对会话日志应用该预设(`permission/preset` + `sandbox/mode` + `approval/policy` 事件),覆盖部署默认;对旧宿主**向后兼容**:权限服务缺失时选择器降级为提示、不发送该字段。bundle 源码收进仓库 `scripts/bundle/send-to-agent/`(startup.js/button.tid/icon.svg/item-template.tid)+ 组装脚本 `scripts/build-send-to-agent-bundle.mjs`,改按钮后重跑组装 → gen-seed → build 即可。
36
37
  - **v0.13.1**(2026-09-04):**客户端 bundle 压缩(minify)**。CodeMirror 6 + Lezer markdown 让 `lib/client.js`(`./client` 导出)达到 1.06MB,超过插件目录注册表(如 dsh.pub 收录校验)经 GitHub Contents API 检查的 **1MB 上限**,导致 dsh.pub 收录 PR 首轮校验失败(`invalid_file`);开启 tsdown `minify` 后产物降到 **0.58MB**(gzip ~193KB),浏览器加载更快,dsh.pub 收录 PR #83 校验全绿并已合并。构建配置见 `tsdown.client.config.ts`。
37
38
  - **v0.13.0**(2026-09-04):**menubar 顶栏主题自适应**。tiddlywiki/menubar 顶栏背景原来一直停在默认色映射的蓝色(`$:/config/DefaultColourMappings/` → `#5778d8`),不随 DSH 深浅主题变化——根因是大部分浅色 palette(Vanilla/Blanca…)不定义 `menubar-background`,`<<colour menubar-background>>` 落回插件硬编码的蓝色。新增 seed `menubar-theme`:写入样式表覆盖 `$:/plugins/dsh-tiddlywiki/menubar-theme`(tag `$:/tags/Stylesheet`),把顶栏改为跟随**活动 palette** 的 `background`/`foreground`(`!important` 压过插件自身规则);嵌入式 TW 的 `$:/palette` 由主题同步随 DSH 翻转时,TW 会实时重渲染全部样式表,menubar 即自动换色。作为新 seed 进统一注册表(`menubar-theme`),首次启动自动写入(ONE-SHOT,不覆盖你的改动),设置页「初始化」可单独/全部**重新初始化**。
38
39
  - **v0.12.0**(2026-09-04):**新默认主页「主页」+ 「所有文章」两列分页页**。把首页拆成三页:**主页**(默认打开,`$:/DefaultTiddlers` 指向它)= 四象限待办 + 「所有标签」「所有文章」入口;**所有标签**瘦身为独立标签统计页(标签 + Agent 区块 + 回主页链接);新增 **所有文章**——全部条目分两列(🤖 Agent 撰写 / 👤 人工·人类,含 `human-edited` 的 Agent 笔记)**各自分页**展示(排除系统页、草稿与 `索引` 导航页),每页条数取配置 `ui.allArticles.pageSize`(默认 10,设置页可调、实时生效)。**所有文章**作为新 seed 进统一注册表(`all-articles`),设置页可单独/全部**重新初始化**。
@@ -111,10 +112,13 @@ dsh plugin --profile web add link:/path/to/your/dsh-tiddlywiki
111
112
  ### 🧑‍💻 给人:界面操作
112
113
 
113
114
  **📤 一键发送给 Agent** — 插件首次启动会把「发送给 Agent」按钮插件写入 wiki(`$:/plugins/dsh/send-to-agent`,ONE-SHOT)。在 TW 里打开任意笔记,工具栏点「**发送给 Agent**」:
115
+ - 弹层顶部有**「附加说明」(可选)**输入框(v0.14.0):填写的说明会以 `【附加说明】` 段随消息一起发给 Agent(不填则没有该段);
114
116
  - 弹层顶部有**「工作模式」(Agent 预设)**选择器(v0.11.0):列出 DSH 全部可用预设(`/agent/modes`,标记部署默认),**新建工作区/会话并发送**时按所选模式创建(`/agent/create` 传 `mode` → `sessionController.create(agentPreset)`);已有会话旁显示其当前模式徽标(🧭);
117
+ - 弹层顶部还有**「权限」(权限预设)**选择器(v0.14.0):列出 DSH 权限预设(沙箱 + 审批的捆绑,如 工作区写入+询问 / 完全访问+免确认,来自 `permissionPresets`),**新建会话并发送**时应用所选权限(`/agent/create` 传 `permission` → 创建后 `permissionPresets.set` 写入会话日志);权限服务不可用时选择器自动隐藏、按部署默认;
115
118
  - 弹层**按工作区(cwd)分组**列出可见会话,点选即把当前笔记作为消息注入(`sessionController.prompt`,与聊天输入同 API);
116
119
  - 也可以**新建工作区/会话**再发送(`/agent/create` 按 cwd 落入真实 Workspace,会话不落「未分组」);
117
- - 消息格式为 `《标题》` + 标签/类型 + **待办说明**(告知 Agent 这是用户提前编辑在 wiki 中的待办事项、不清楚应主动提问)+ 正文;
120
+ - 消息格式为 `《标题》` + 标签/类型 + **待办说明**(告知 Agent 这是用户提前编辑在 wiki 中的待办事项、不清楚应主动提问)+ **附加说明**(可选)+ 正文;
121
+ - 按钮在 控制台→外观→工具栏 里带**独立加粗图标与说明**(v0.14.0 起不再借用「导出此条目」的图标;bundle 还 shadow 覆盖核心工具栏行模板,让查看/页面/编辑工具栏的设置列表都显示每个按钮的图标);
118
122
  - 开关与 token 见设置页「常规配置」/配置项 `ui.sendToAgent`(默认开)。
119
123
 
120
124
  **🧭 中央列编辑器** — 侧边栏「TiddlyWiki」按钮开关中央编辑器面板(**同源代理**:iframe 指向 `<DSH origin>/dsh-tiddlywiki/tw/`,由 DSH 转发到回环上的 TW 服务),完整 TW 5 编辑器。
@@ -268,8 +272,11 @@ node scripts/verify-menubar-theme.mjs # 可选:真实浏览器验证 menubar
268
272
  node scripts/verify-seed-send-to-agent.mjs # 可选:全新 wiki 上 E2E 验证「发送给 Agent」按钮 seed(bundle 写入/幂等/marker)
269
273
  node scripts/verify-seeds-admin.mjs # 可选:全新 wiki + 真实 HTTP 验证 /admin/seeds 状态与 /admin/seeds/run(单跑/全跑/force/unknown id)
270
274
 
271
- # 改了 wiki 里的 $:/plugins/dsh/send-to-agent bundle 后,重新生成内置常量:
272
- node scripts/gen-seed-send-to-agent.mjs '<wiki>/tiddlers/$__plugins_dsh_send-to-agent.json' src/host/seed-send-to-agent.ts && npm run build
275
+ # 改了 scripts/bundle/send-to-agent/ 下的按钮源码(startup.js / button.tid / icon.svg)后,
276
+ # 重新组装 bundle → 重新生成内置常量:
277
+ node scripts/build-send-to-agent-bundle.mjs
278
+ node scripts/gen-seed-send-to-agent.mjs scripts/bundle/send-to-agent.bundle.json src/host/seed-send-to-agent.ts && npm run build
279
+ node scripts/verify-send-to-agent-bundle.mjs # 可选:校验组装出的 bundle 字段/内容
273
280
 
274
281
  # 改了 wiki 首页「主页 / 所有标签 / 标签笔记」后,重新生成内置 seed 常量:
275
282
  node scripts/gen-seed-home.mjs '<wiki>/tiddlers/主页.tid' '<wiki>/tiddlers/所有标签.tid' '<wiki>/tiddlers/标签笔记.tid' src/host/seed-home.ts && npm run build
@@ -295,9 +302,9 @@ node scripts/gen-seed-home.mjs '<wiki>/tiddlers/主页.tid' '<wiki>/tiddlers/所
295
302
  | `/dsh-tiddlywiki/upload` | POST | 文件上传到 `files/`(原始 body + `X-Filename`) |
296
303
  | `/dsh-tiddlywiki/restart` | POST | 重启 TW 子进程 |
297
304
  | `/dsh-tiddlywiki/agent/sessions` | GET | 可见会话列表(TW「发送给 Agent」选择器;含每会话 `agentPreset` 模式徽标) |
298
- | `/dsh-tiddlywiki/agent/modes` | GET | 可用「工作模式」(Agent 预设)清单 + 部署默认(v0.11.0) |
305
+ | `/dsh-tiddlywiki/agent/modes` | GET | 可用「工作模式」(Agent 预设)清单 + 部署默认;同时返回 `permissions`(权限预设 roster:`{defaultId, items:[{value,name,description}]}`,v0.14.0,来自 `permissionPresets`,缺失时 `null`) |
299
306
  | `/dsh-tiddlywiki/agent/send` | POST | 把笔记作为消息注入一个会话(`sessionController.prompt`) |
300
- | `/dsh-tiddlywiki/agent/create` | POST | 按 cwd 新建/复用 Workspace + 会话(工作区优先);可选 `mode`(Agent 预设) |
307
+ | `/dsh-tiddlywiki/agent/create` | POST | 按 cwd 新建/复用 Workspace + 会话(工作区优先);可选 `mode`(Agent 预设)、可选 `permission`(权限预设,创建后经 `permissionPresets.set` 应用到会话日志,v0.14.0;未知预设 400 拒绝) |
301
308
  | `/dsh-tiddlywiki/api/*` | any | 透传到 TW 服务(JSON) |
302
309
  | `/dsh-tiddlywiki/tw/*` | any | **同源 TW 代理**:整个 TW 前端(index + `/files/*` + TiddlyWeb API)→ 回环 TW(v0.6.0,远程访问核心) |
303
310
 
package/lib/index.js CHANGED
@@ -934,6 +934,16 @@ function registerRoutes(ctx, deps) {
934
934
  } catch {
935
935
  defaultId = void 0;
936
936
  }
937
+ let permissions = null;
938
+ const pp = deps.getPermissionPresets();
939
+ if (pp !== void 0) try {
940
+ permissions = {
941
+ defaultId: pp.defaultPreset ?? null,
942
+ items: pp.names.map((n) => pp.optionOf(n))
943
+ };
944
+ } catch {
945
+ permissions = null;
946
+ }
937
947
  json$1(res, {
938
948
  ok: true,
939
949
  defaultId: defaultId ?? null,
@@ -944,7 +954,8 @@ function registerRoutes(ctx, deps) {
944
954
  trust: p.trust ?? "user",
945
955
  broken: p.broken ?? null,
946
956
  isDefault: p.id === defaultId
947
- }))
957
+ })),
958
+ permissions
948
959
  });
949
960
  } catch (err) {
950
961
  json$1(res, {
@@ -1031,6 +1042,12 @@ function registerRoutes(ctx, deps) {
1031
1042
  * Optional `mode` names the "工作模式" (an Agent preset id, e.g. from
1032
1043
  * /agent/modes); it is forwarded to `sessionController.create(agentPreset)`
1033
1044
  * so the new session launches under that preset. Omitted → deployment default.
1045
+ *
1046
+ * Optional `permission` names a "权限" preset (e.g. from /agent/modes'
1047
+ * `permissions` roster). After the session is created it is applied to the
1048
+ * live session's log via `permissionPresets.set` (durable knob events:
1049
+ * `permission/preset`, `sandbox/mode`, `approval/policy`), overriding the
1050
+ * deployment default pinned at creation. Omitted → keep the default.
1034
1051
  */
1035
1052
  const handleAgentCreate = async (req, res) => {
1036
1053
  try {
@@ -1055,6 +1072,24 @@ function registerRoutes(ctx, deps) {
1055
1072
  const body = JSON.parse(await readBody$1(req));
1056
1073
  const cwd = typeof body.cwd === "string" ? body.cwd.trim() : "";
1057
1074
  const mode = typeof body.mode === "string" && body.mode.trim().length > 0 ? body.mode.trim() : void 0;
1075
+ const permission = typeof body.permission === "string" && body.permission.trim().length > 0 ? body.permission.trim() : void 0;
1076
+ const pp = deps.getPermissionPresets();
1077
+ if (permission !== void 0) {
1078
+ if (pp === void 0) {
1079
+ json$1(res, {
1080
+ ok: false,
1081
+ error: "permission selected but the permission-presets service is unavailable"
1082
+ }, 503);
1083
+ return;
1084
+ }
1085
+ if (!pp.names.includes(permission)) {
1086
+ json$1(res, {
1087
+ ok: false,
1088
+ error: `unknown permission preset "${permission}" (available: ${pp.names.join(", ")})`
1089
+ }, 400);
1090
+ return;
1091
+ }
1092
+ }
1058
1093
  const sc = deps.getSessionController();
1059
1094
  if (sc === void 0) {
1060
1095
  json$1(res, {
@@ -1066,30 +1101,36 @@ function registerRoutes(ctx, deps) {
1066
1101
  const ws = deps.getWorkspaceRegistry();
1067
1102
  if (cwd.length > 0) await mkdir(cwd, { recursive: true });
1068
1103
  let created;
1104
+ let workspaceId;
1069
1105
  if (cwd.length > 0 && ws !== void 0) {
1070
- const workspace = await ws.create(cwd);
1106
+ workspaceId = (await ws.create(cwd)).id;
1071
1107
  created = await sc.create({
1072
- workspaceId: workspace.id,
1108
+ workspaceId,
1073
1109
  agentPreset: mode
1074
1110
  });
1075
- json$1(res, {
1076
- ok: true,
1077
- sessionId: created.sessionId,
1078
- cwd,
1079
- workspaceId: workspace.id,
1080
- mode: mode ?? null
1081
- });
1082
- return;
1083
- }
1084
- created = await sc.create({
1111
+ } else created = await sc.create({
1085
1112
  cwd: cwd.length > 0 ? cwd : void 0,
1086
1113
  agentPreset: mode
1087
1114
  });
1115
+ let permissionApplied = false;
1116
+ if (permission !== void 0) {
1117
+ const sessionsSvc = deps.getSessions();
1118
+ if (sessionsSvc !== void 0) try {
1119
+ const session = sessionsSvc.get(created.sessionId);
1120
+ if (session !== void 0) {
1121
+ pp?.set(session, permission);
1122
+ permissionApplied = true;
1123
+ }
1124
+ } catch {}
1125
+ }
1088
1126
  json$1(res, {
1089
1127
  ok: true,
1090
1128
  sessionId: created.sessionId,
1091
1129
  cwd: cwd || null,
1092
- mode: mode ?? null
1130
+ workspaceId: workspaceId ?? null,
1131
+ mode: mode ?? null,
1132
+ permission: permission ?? null,
1133
+ permissionApplied
1093
1134
  });
1094
1135
  } catch (err) {
1095
1136
  json$1(res, {
@@ -2195,7 +2236,7 @@ const SEND_TO_AGENT_PLUGIN_TITLE = "$:/plugins/dsh/send-to-agent";
2195
2236
  /** One-time marker: presence means "the button was offered once — hands off". */
2196
2237
  const SEND_TO_AGENT_MARKER_TITLE = "$:/plugins/dsh-tiddlywiki/seed-send-to-agent";
2197
2238
  /** The bundle's JSON text (`{"tiddlers": {...}}`), exactly as TW stores it. */
2198
- const SEND_TO_AGENT_BUNDLE_TEXT = "{\n \"tiddlers\": {\n \"$:/plugins/dsh/send-to-agent/plugin.info\": {\n \"title\": \"$:/plugins/dsh/send-to-agent/plugin.info\",\n \"type\": \"application/json\",\n \"text\": \"{\\\"title\\\":\\\"$:/plugins/dsh/send-to-agent\\\",\\\"name\\\":\\\"Send to Agent\\\",\\\"description\\\":\\\"把当前笔记一键发送给 DSH Agent(TiddlyWiki → DSH 会话注入)\\\",\\\"author\\\":\\\"dsh-tiddlywiki\\\",\\\"version\\\":\\\"0.2.0\\\",\\\"plugin-type\\\":\\\"plugin\\\"}\"\n },\n \"$:/plugins/dsh/send-to-agent/startup.js\": {\n \"title\": \"$:/plugins/dsh/send-to-agent/startup.js\",\n \"type\": \"application/javascript\",\n \"module-type\": \"startup\",\n \"text\": \"/*\\\\\\ntitle: $:/plugins/dsh/send-to-agent/startup.js\\ntype: application/javascript\\nmodule-type: startup\\n\\n\\\\*/\\n(function(){\\n\\n/*jslint node: true, browser: true */\\n/*global $tw: false */\\n\\\"use strict\\\";\\n\\nexports.name = \\\"dsh-send-to-agent\\\";\\nexports.after = [\\\"story\\\"];\\nexports.platforms = [\\\"browser\\\"];\\n\\nfunction readConfig() {\\n\\tvar config = { enabled: true, endpoint: \\\"\\\", token: \\\"\\\" };\\n\\ttry {\\n\\t\\tvar t = $tw.wiki.getTiddler(\\\"$:/plugins/dsh-tiddlywiki/config\\\");\\n\\t\\tif (t && t.fields && typeof t.fields.text === \\\"string\\\") {\\n\\t\\t\\tvar parsed = JSON.parse(t.fields.text);\\n\\t\\t\\tvar s2a = parsed && parsed.ui && parsed.ui.sendToAgent;\\n\\t\\t\\tif (s2a) {\\n\\t\\t\\t\\tif (typeof s2a.enabled === \\\"boolean\\\") { config.enabled = s2a.enabled; }\\n\\t\\t\\t\\tif (typeof s2a.endpoint === \\\"string\\\" && s2a.endpoint.length > 0) { config.endpoint = s2a.endpoint; }\\n\\t\\t\\t\\tif (typeof s2a.token === \\\"string\\\" && s2a.token.length > 0) { config.token = s2a.token; }\\n\\t\\t\\t}\\n\\t\\t}\\n\\t} catch (e) {}\\n\\treturn config;\\n}\\n\\nfunction baseEndpoint() {\\n\\tvar config = readConfig();\\n\\tif (config.endpoint.length > 0) { return config.endpoint.replace(/\\\\/+$/, \\\"\\\"); }\\n\\tif (typeof location !== \\\"undefined\\\" && location.origin) { return location.origin + \\\"/dsh-tiddlywiki\\\"; }\\n\\treturn \\\"/dsh-tiddlywiki\\\";\\n}\\n\\nfunction notify(msg) {\\n\\tif ($tw.notifier && typeof $tw.notifier.display === \\\"function\\\") {\\n\\t\\t$tw.notifier.display(msg);\\n\\t} else if (typeof alert === \\\"function\\\") {\\n\\t\\talert(msg);\\n\\t}\\n}\\n\\nfunction doSend(payload, sessionId) {\\n\\tvar config = readConfig();\\n\\tvar text = \\\"《\\\" + payload.title + \\\"》\\\\n标签: \\\" + (payload.tags || []).join(\\\", \\\") + \\\"\\\\n类型: \\\" + (payload.type || \\\"无\\\") + \\\"\\\\n\\\\n【待办说明】以下内容是我(用户)提前编辑在 TiddlyWiki 知识库中的待办事项,通过「发送给 Agent」一键发送给你处理。请按内容执行;如有任何不清楚的地方,请主动向我提问,不要臆测或擅自发挥。\\\\n\\\\n\\\" + (payload.text || \\\"\\\");\\n\\tvar headers = { \\\"Content-Type\\\": \\\"application/json\\\" };\\n\\tif (config.token.length > 0) { headers[\\\"x-send-to-agent-token\\\"] = config.token; }\\n\\t$tw.utils.httpRequest({\\n\\t\\turl: baseEndpoint() + \\\"/agent/send\\\",\\n\\t\\ttype: \\\"POST\\\",\\n\\t\\theaders: headers,\\n\\t\\tdata: JSON.stringify({ sessionId: sessionId, text: text }),\\n\\t\\tcallback: function(err, data) {\\n\\t\\t\\tvar parsed = null;\\n\\t\\t\\ttry { parsed = JSON.parse(data || \\\"\\\"); } catch (e) {}\\n\\t\\t\\tif (err) { notify(\\\"发送失败:\\\" + err); return; }\\n\\t\\t\\tif (parsed && parsed.ok) {\\n\\t\\t\\t\\tnotify(\\\"已发送 ✓ 会话 \\\" + sessionId.slice(0, 8));\\n\\t\\t\\t} else {\\n\\t\\t\\t\\tnotify(\\\"发送失败:\\\" + ((parsed && parsed.error) || data || \\\"未知错误\\\"));\\n\\t\\t\\t}\\n\\t\\t}\\n\\t});\\n}\\n\\nfunction closeOverlay(overlay, escHandler) {\\n\\tif (escHandler) { document.removeEventListener(\\\"keydown\\\", escHandler); }\\n\\tif (overlay && overlay.parentNode) { overlay.parentNode.removeChild(overlay); }\\n}\\n\\nfunction showPicker(payload, items, modes, defaultId) {\\n\\tif (typeof document === \\\"undefined\\\") { return; }\\n\\t// remove any previously-open picker\\n\\tvar old = document.getElementById(\\\"dsh-send-picker\\\");\\n\\tif (old && old.parentNode) { old.parentNode.removeChild(old); }\\n\\n\\tvar state = { workspace: null, mode: \\\"\\\" };\\n\\n\\tvar overlay = document.createElement(\\\"div\\\");\\n\\toverlay.id = \\\"dsh-send-picker\\\";\\n\\toverlay.setAttribute(\\\"style\\\", \\\"position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.35);z-index:99999;display:flex;align-items:center;justify-content:center;\\\");\\n\\tvar box = document.createElement(\\\"div\\\");\\n\\tbox.setAttribute(\\\"style\\\", \\\"background:#fff;color:#333;border-radius:8px;padding:14px;min-width:340px;max-width:560px;max-height:78vh;display:flex;flex-direction:column;box-shadow:0 8px 30px rgba(0,0,0,.3);font-family:system-ui,-apple-system,sans-serif;\\\");\\n\\n\\t// header\\n\\tvar header = document.createElement(\\\"div\\\");\\n\\theader.setAttribute(\\\"style\\\", \\\"display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;\\\");\\n\\tvar h = document.createElement(\\\"h3\\\");\\n\\th.setAttribute(\\\"style\\\", \\\"margin:0;font-size:15px;\\\");\\n\\th.textContent = \\\"发送给 Agent · 选择目标\\\";\\n\\tvar closeX = document.createElement(\\\"button\\\");\\n\\tcloseX.type = \\\"button\\\";\\n\\tcloseX.textContent = \\\"✕\\\";\\n\\tcloseX.title = \\\"关闭\\\";\\n\\tcloseX.setAttribute(\\\"style\\\", \\\"border:none;background:transparent;font-size:15px;cursor:pointer;color:#888;padding:2px 8px;border-radius:4px;\\\");\\n\\tcloseX.addEventListener(\\\"click\\\", function() { closeOverlay(overlay, escHandler); });\\n\\theader.appendChild(h);\\n\\theader.appendChild(closeX);\\n\\tbox.appendChild(header);\\n\\n\\t// description\\n\\tvar desc = document.createElement(\\\"p\\\");\\n\\tdesc.setAttribute(\\\"style\\\", \\\"margin:0 0 8px;font-size:12px;color:#888;word-break:break-all;\\\");\\n\\tdesc.textContent = \\\"《\\\" + payload.title + \\\"》将作为消息注入所选会话\\\";\\n\\tbox.appendChild(desc);\\n\\n\\t// 工作模式(Agent 预设)selector — applies to newly created sessions; the\\n\\t// modes come from GET /agent/modes (id/name/description + deployment\\n\\t// default). When the modes endpoint is unreachable (e.g. an older host) the\\n\\t// row degrades to a hint and no mode is sent — DSH uses its default.\\n\\tvar modeRow = document.createElement(\\\"div\\\");\\n\\tmodeRow.setAttribute(\\\"style\\\", \\\"margin:0 0 8px;\\\");\\n\\tif (modes && modes.length > 0) {\\n\\t\\tvar modeLbl = document.createElement(\\\"label\\\");\\n\\t\\tmodeLbl.setAttribute(\\\"style\\\", \\\"display:block;font-size:12px;color:#666;margin-bottom:4px;\\\");\\n\\t\\tmodeLbl.textContent = \\\"工作模式(Agent 预设)— 用于新建会话\\\";\\n\\t\\tmodeRow.appendChild(modeLbl);\\n\\t\\tvar sel = document.createElement(\\\"select\\\");\\n\\t\\tsel.setAttribute(\\\"style\\\", \\\"width:100%;box-sizing:border-box;padding:6px 8px;border:1px solid #ccc;border-radius:6px;font-size:13px;background:#fff;\\\");\\n\\t\\tvar optDefault = document.createElement(\\\"option\\\");\\n\\t\\toptDefault.value = \\\"\\\";\\n\\t\\toptDefault.textContent = \\\"(默认模式)\\\";\\n\\t\\tsel.appendChild(optDefault);\\n\\t\\tmodes.forEach(function(m) {\\n\\t\\t\\tvar o = document.createElement(\\\"option\\\");\\n\\t\\t\\to.value = m.id || \\\"\\\";\\n\\t\\t\\to.textContent = (m.name || m.id) + (m.isDefault ? \\\"(默认)\\\" : \\\"\\\");\\n\\t\\t\\tsel.appendChild(o);\\n\\t\\t});\\n\\t\\t// preselect the deployment default when listed\\n\\t\\tif (defaultId) {\\n\\t\\t\\tfor (var i = 0; i < sel.options.length; i++) {\\n\\t\\t\\t\\tif (sel.options[i].value === defaultId) { sel.selectedIndex = i; break; }\\n\\t\\t\\t}\\n\\t\\t}\\n\\t\\tstate.mode = sel.options[sel.selectedIndex] ? sel.options[sel.selectedIndex].value : \\\"\\\";\\n\\t\\tsel.addEventListener(\\\"change\\\", function() { state.mode = sel.value; });\\n\\t\\tmodeRow.appendChild(sel);\\n\\t} else {\\n\\t\\tvar modeHint = document.createElement(\\\"p\\\");\\n\\t\\tmodeHint.setAttribute(\\\"style\\\", \\\"margin:0;font-size:12px;color:#999;\\\");\\n\\t\\tmodeHint.textContent = \\\"(未获取到可用工作模式,将使用 DSH 默认模式)\\\";\\n\\t\\tmodeRow.appendChild(modeHint);\\n\\t}\\n\\tbox.appendChild(modeRow);\\n\\n\\t// scrollable body\\n\\tvar body = document.createElement(\\\"div\\\");\\n\\tbody.setAttribute(\\\"style\\\", \\\"overflow:auto;flex:1;min-height:0;\\\");\\n\\tbox.appendChild(body);\\n\\n\\t// footer with cancel\\n\\tvar footer = document.createElement(\\\"div\\\");\\n\\tfooter.setAttribute(\\\"style\\\", \\\"display:flex;justify-content:flex-end;gap:8px;margin-top:10px;\\\");\\n\\tvar cancel = document.createElement(\\\"button\\\");\\n\\tcancel.type = \\\"button\\\";\\n\\tcancel.textContent = \\\"取消\\\";\\n\\tcancel.setAttribute(\\\"style\\\", \\\"padding:6px 14px;border:1px solid #ccc;border-radius:6px;background:#fff;cursor:pointer;font-size:13px;\\\");\\n\\tcancel.addEventListener(\\\"click\\\", function() { closeOverlay(overlay, escHandler); });\\n\\tfooter.appendChild(cancel);\\n\\tbox.appendChild(footer);\\n\\n\\toverlay.appendChild(box);\\n\\tdocument.body.appendChild(overlay);\\n\\n\\t// backdrop click + Esc close\\n\\toverlay.addEventListener(\\\"click\\\", function(e) {\\n\\t\\tif (e.target === overlay) { closeOverlay(overlay, escHandler); }\\n\\t});\\n\\tvar escHandler = function(e) { if (e.key === \\\"Escape\\\") { closeOverlay(overlay, escHandler); } };\\n\\tdocument.addEventListener(\\\"keydown\\\", escHandler);\\n\\n\\t// group sessions by workspace (cwd)\\n\\tvar groups = {};\\n\\tvar order = [];\\n\\t(items || []).forEach(function(s) {\\n\\t\\tvar key = (s.cwd && String(s.cwd).length > 0) ? s.cwd : \\\"__default__\\\";\\n\\t\\tif (!groups[key]) { groups[key] = { cwd: s.cwd, sessions: [], max: 0 }; order.push(key); }\\n\\t\\tgroups[key].sessions.push(s);\\n\\t\\tif ((s.updatedAt || 0) > groups[key].max) { groups[key].max = s.updatedAt || 0; }\\n\\t});\\n\\torder.sort(function(a, b) { return groups[b].max - groups[a].max; });\\n\\n\\tfunction btnStyle() {\\n\\t\\treturn \\\"display:block;width:100%;text-align:left;padding:8px 10px;margin:4px 0;border:1px solid #ddd;border-radius:6px;background:#f7f7f7;cursor:pointer;font-size:13px;\\\";\\n\\t}\\n\\tfunction primaryBtnStyle() {\\n\\t\\treturn \\\"display:block;width:100%;text-align:center;padding:7px 10px;margin:6px 0 0;border:1px solid #4a90d9;border-radius:6px;background:#4a90d9;color:#fff;cursor:pointer;font-size:13px;\\\";\\n\\t}\\n\\tfunction smallBtnStyle() {\\n\\t\\treturn \\\"margin:0 0 6px;padding:4px 10px;border:1px solid #ccc;border-radius:6px;background:#fff;cursor:pointer;font-size:12px;color:#555;\\\";\\n\\t}\\n\\n\\tfunction shortSessionLabel(s) {\\n\\t\\tvar id = s.sessionId || \\\"\\\";\\n\\t\\tvar parts = id.split(\\\"-\\\");\\n\\t\\tvar short = (parts.length > 1 ? parts[parts.length - 1] : id).slice(-10);\\n\\t\\tvar when = \\\"\\\";\\n\\t\\tif (s.updatedAt) {\\n\\t\\t\\tvar diff = Date.now() - s.updatedAt;\\n\\t\\t\\tif (diff < 60000) { when = \\\"刚刚\\\"; }\\n\\t\\t\\telse if (diff < 3600000) { when = Math.floor(diff / 60000) + \\\" 分钟前\\\"; }\\n\\t\\t\\telse if (diff < 86400000) { when = Math.floor(diff / 3600000) + \\\" 小时前\\\"; }\\n\\t\\t\\telse {\\n\\t\\t\\t\\tvar d = new Date(s.updatedAt);\\n\\t\\t\\t\\twhen = (d.getMonth() + 1) + \\\"-\\\" + d.getDate() + \\\" \\\" + (\\\"0\\\" + d.getHours()).slice(-2) + \\\":\\\" + (\\\"0\\\" + d.getMinutes()).slice(-2);\\n\\t\\t\\t}\\n\\t\\t}\\n\\t\\treturn \\\"#\\\" + short + (when ? \\\" · \\\" + when : \\\"\\\") + (s.running ? \\\" · ●运行中\\\" : \\\"\\\") + (s.blank ? \\\" · (空)\\\" : \\\"\\\");\\n\\t}\\n\\n\\tfunction modeName(id) {\\n\\t\\tif (!id) { return \\\"\\\"; }\\n\\t\\tfor (var i = 0; i < (modes || []).length; i++) {\\n\\t\\t\\tif (modes[i].id === id) { return modes[i].name || modes[i].id; }\\n\\t\\t}\\n\\t\\treturn id;\\n\\t}\\n\\n\\tfunction render() {\\n\\t\\tbody.innerHTML = \\\"\\\";\\n\\t\\tif (state.workspace === null) { renderWorkspaces(); } else { renderSessions(state.workspace); }\\n\\t}\\n\\n\\tfunction renderWorkspaces() {\\n\\t\\tif (order.length === 0) {\\n\\t\\t\\tvar none = document.createElement(\\\"p\\\");\\n\\t\\t\\tnone.textContent = \\\"还没有任何会话——在下方新建一个吧\\\";\\n\\t\\t\\tnone.setAttribute(\\\"style\\\", \\\"color:#999;font-size:13px;margin:0 0 8px;\\\");\\n\\t\\t\\tbody.appendChild(none);\\n\\t\\t} else {\\n\\t\\t\\torder.forEach(function(key) {\\n\\t\\t\\t\\tvar g = groups[key];\\n\\t\\t\\t\\tvar label = g.cwd || \\\"(默认工作区)\\\";\\n\\t\\t\\t\\tvar b = document.createElement(\\\"button\\\");\\n\\t\\t\\t\\tb.type = \\\"button\\\";\\n\\t\\t\\t\\tb.setAttribute(\\\"style\\\", btnStyle());\\n\\t\\t\\t\\tb.textContent = \\\"📁 \\\" + label + \\\" · \\\" + g.sessions.length + \\\" 个会话\\\";\\n\\t\\t\\t\\tb.title = g.cwd || \\\"默认工作区\\\";\\n\\t\\t\\t\\tb.addEventListener(\\\"click\\\", function() { state.workspace = key; render(); });\\n\\t\\t\\t\\tbody.appendChild(b);\\n\\t\\t\\t});\\n\\t\\t}\\n\\t\\t// new workspace / session\\n\\t\\tvar newRow = document.createElement(\\\"div\\\");\\n\\t\\tnewRow.setAttribute(\\\"style\\\", \\\"margin-top:10px;border-top:1px solid #eee;padding-top:8px;\\\");\\n\\t\\tvar lbl = document.createElement(\\\"p\\\");\\n\\t\\tlbl.textContent = \\\"新建工作区 / 会话\\\";\\n\\t\\tlbl.setAttribute(\\\"style\\\", \\\"margin:0 0 6px;font-size:12px;color:#666;\\\");\\n\\t\\tnewRow.appendChild(lbl);\\n\\t\\tvar input = document.createElement(\\\"input\\\");\\n\\t\\tinput.type = \\\"text\\\";\\n\\t\\tinput.placeholder = \\\"输入工作区路径,留空为默认工作区\\\";\\n\\t\\tinput.setAttribute(\\\"style\\\", \\\"width:100%;box-sizing:border-box;padding:6px 8px;border:1px solid #ccc;border-radius:6px;font-size:13px;\\\");\\n\\t\\tnewRow.appendChild(input);\\n\\t\\tvar go = document.createElement(\\\"button\\\");\\n\\t\\tgo.type = \\\"button\\\";\\n\\t\\tgo.textContent = \\\"创建并发送\\\";\\n\\t\\tgo.setAttribute(\\\"style\\\", primaryBtnStyle());\\n\\t\\tgo.addEventListener(\\\"click\\\", function() { createAndSend(payload, input.value.trim()); });\\n\\t\\tnewRow.appendChild(go);\\n\\t\\tinput.addEventListener(\\\"keydown\\\", function(e) { if (e.key === \\\"Enter\\\") { createAndSend(payload, input.value.trim()); } });\\n\\t\\tbody.appendChild(newRow);\\n\\t}\\n\\n\\tfunction renderSessions(key) {\\n\\t\\tvar g = groups[key];\\n\\t\\tvar back = document.createElement(\\\"button\\\");\\n\\t\\tback.type = \\\"button\\\";\\n\\t\\tback.textContent = \\\"← 返回工作区列表\\\";\\n\\t\\tback.setAttribute(\\\"style\\\", smallBtnStyle());\\n\\t\\tback.addEventListener(\\\"click\\\", function() { state.workspace = null; render(); });\\n\\t\\tbody.appendChild(back);\\n\\n\\t\\tvar wsName = document.createElement(\\\"p\\\");\\n\\t\\twsName.textContent = g.cwd ? \\\"📁 \\\" + g.cwd : \\\"(默认工作区)\\\";\\n\\t\\twsName.setAttribute(\\\"style\\\", \\\"margin:0 0 6px;font-size:13px;font-weight:600;word-break:break-all;\\\");\\n\\t\\tbody.appendChild(wsName);\\n\\n\\t\\t(g.sessions || []).forEach(function(s) {\\n\\t\\t\\tvar b = document.createElement(\\\"button\\\");\\n\\t\\t\\tb.type = \\\"button\\\";\\n\\t\\t\\tb.setAttribute(\\\"style\\\", btnStyle());\\n\\t\\t\\tvar label = shortSessionLabel(s);\\n\\t\\t\\tif (s.agentPreset) { label += \\\" · 🧭 \\\" + modeName(s.agentPreset); }\\n\\t\\t\\tb.textContent = label;\\n\\t\\t\\tb.title = \\\"会话 \\\" + s.sessionId + (s.agentPreset ? \\\" · 工作模式 \\\" + modeName(s.agentPreset) : \\\"\\\");\\n\\t\\t\\tb.addEventListener(\\\"click\\\", function() { closeOverlay(overlay, escHandler); doSend(payload, s.sessionId); });\\n\\t\\t\\tbody.appendChild(b);\\n\\t\\t});\\n\\n\\t\\tvar newBtn = document.createElement(\\\"button\\\");\\n\\t\\tnewBtn.type = \\\"button\\\";\\n\\t\\tnewBtn.textContent = \\\"➕ 在此工作区新建会话并发送\\\";\\n\\t\\tnewBtn.setAttribute(\\\"style\\\", primaryBtnStyle());\\n\\t\\tnewBtn.addEventListener(\\\"click\\\", function() { createAndSend(payload, g.cwd); });\\n\\t\\tbody.appendChild(newBtn);\\n\\t}\\n\\n\\tfunction createAndSend(payload2, cwd) {\\n\\t\\tvar config = readConfig();\\n\\t\\tvar headers = { \\\"Content-Type\\\": \\\"application/json\\\" };\\n\\t\\tif (config.token.length > 0) { headers[\\\"x-send-to-agent-token\\\"] = config.token; }\\n\\t\\tvar body = { cwd: cwd || \\\"\\\" };\\n\\t\\tif (state.mode && String(state.mode).length > 0) { body.mode = state.mode; }\\n\\t\\t$tw.utils.httpRequest({\\n\\t\\t\\turl: baseEndpoint() + \\\"/agent/create\\\",\\n\\t\\t\\ttype: \\\"POST\\\",\\n\\t\\t\\theaders: headers,\\n\\t\\t\\tdata: JSON.stringify(body),\\n\\t\\t\\tcallback: function(err, data) {\\n\\t\\t\\t\\tvar parsed = null;\\n\\t\\t\\t\\ttry { parsed = JSON.parse(data || \\\"\\\"); } catch (e) {}\\n\\t\\t\\t\\tif (err) { notify(\\\"新建会话失败:\\\" + err); return; }\\n\\t\\t\\t\\tif (parsed && parsed.ok && parsed.sessionId) {\\n\\t\\t\\t\\t\\tcloseOverlay(overlay, escHandler);\\n\\t\\t\\t\\t\\tvar modeNote = parsed.mode ? \\\"(模式 \\\" + modeName(parsed.mode) + \\\")\\\" : \\\"\\\";\\n\\t\\t\\t\\t\\tnotify(\\\"已创建会话并发送 ✓ \\\" + parsed.sessionId.slice(0, 8) + modeNote);\\n\\t\\t\\t\\t\\tdoSend(payload2, parsed.sessionId);\\n\\t\\t\\t\\t} else {\\n\\t\\t\\t\\t\\tnotify(\\\"新建会话失败:\\\" + ((parsed && parsed.error) || data || \\\"未知错误\\\"));\\n\\t\\t\\t\\t}\\n\\t\\t\\t}\\n\\t\\t});\\n\\t}\\n\\n\\trender();\\n}\\n\\nfunction handleSend(title) {\\n\\tif (!title) { notify(\\\"无法确定当前笔记标题\\\"); return; }\\n\\tvar tiddler = $tw.wiki.getTiddler(title);\\n\\tif (!tiddler) { notify(\\\"找不到笔记:\\\" + title); return; }\\n\\tvar config = readConfig();\\n\\tif (!config.enabled) { notify(\\\"「发送给 Agent」特性未启用(可在 TW 配置中打开)\\\"); return; }\\n\\tvar payload = {\\n\\t\\ttitle: title,\\n\\t\\ttext: tiddler.fields.text || \\\"\\\",\\n\\t\\ttags: tiddler.fields.tags || [],\\n\\t\\ttype: tiddler.fields.type || \\\"\\\"\\n\\t};\\n\\tvar headers = {};\\n\\tif (config.token.length > 0) { headers[\\\"x-send-to-agent-token\\\"] = config.token; }\\n\\t$tw.utils.httpRequest({\\n\\t\\turl: baseEndpoint() + \\\"/agent/sessions\\\",\\n\\t\\ttype: \\\"GET\\\",\\n\\t\\theaders: headers,\\n\\t\\tcallback: function(err, data) {\\n\\t\\t\\tvar parsed = null;\\n\\t\\t\\ttry { parsed = JSON.parse(data || \\\"\\\"); } catch (e) {}\\n\\t\\t\\tif (err) { notify(\\\"获取会话列表失败:\\\" + err); return; }\\n\\t\\t\\tif (!parsed || !parsed.ok || !parsed.items) { notify(\\\"获取会话列表失败:\\\" + ((parsed && parsed.error) || \\\"未知错误\\\")); return; }\\n\\t\\t\\t// fetch the available 工作模式 (Agent presets) alongside; degrade to\\n\\t\\t\\t// an empty roster (default mode) when the endpoint is missing.\\n\\t\\t\\t$tw.utils.httpRequest({\\n\\t\\t\\t\\turl: baseEndpoint() + \\\"/agent/modes\\\",\\n\\t\\t\\t\\ttype: \\\"GET\\\",\\n\\t\\t\\t\\theaders: headers,\\n\\t\\t\\t\\tcallback: function(err2, data2) {\\n\\t\\t\\t\\t\\tvar modes = [];\\n\\t\\t\\t\\t\\tvar defaultId = \\\"\\\";\\n\\t\\t\\t\\t\\tvar parsed2 = null;\\n\\t\\t\\t\\t\\ttry { parsed2 = JSON.parse(data2 || \\\"\\\"); } catch (e) {}\\n\\t\\t\\t\\t\\tif (!err2 && parsed2 && parsed2.ok && Array.isArray(parsed2.items)) {\\n\\t\\t\\t\\t\\t\\tmodes = parsed2.items;\\n\\t\\t\\t\\t\\t\\tdefaultId = parsed2.defaultId || \\\"\\\";\\n\\t\\t\\t\\t\\t}\\n\\t\\t\\t\\t\\tshowPicker(payload, parsed.items, modes, defaultId);\\n\\t\\t\\t\\t}\\n\\t\\t\\t});\\n\\t\\t}\\n\\t});\\n}\\n\\nexports.startup = function() {\\n\\tconsole.log(\\\"[dsh-send-to-agent] startup ran\\\");\\n\\tif (!$tw.rootWidget || typeof $tw.rootWidget.addEventListener !== \\\"function\\\") { return; }\\n\\t$tw.rootWidget.addEventListener(\\\"dsh-send-to-agent\\\", function(event) {\\n\\t\\thandleSend(event.param);\\n\\t});\\n};\\n\\n})();\\n\"\n },\n \"$:/plugins/dsh/send-to-agent/ui/ViewToolbar/SendToAgent\": {\n \"title\": \"$:/plugins/dsh/send-to-agent/ui/ViewToolbar/SendToAgent\",\n \"type\": \"text/vnd.tiddlywiki\",\n \"tags\": [\n \"$:/tags/ViewToolbar\"\n ],\n \"text\": \"\\\\whitespace trim\\n<$button message=\\\"dsh-send-to-agent\\\"\\n\\tparam=<<currentTiddler>>\\n\\ttooltip=\\\"把当前笔记一键发送给 DSH Agent\\\"\\n\\taria-label=\\\"把当前笔记一键发送给 DSH Agent\\\"\\n\\tclass=<<tv-config-toolbar-class>>\\n>\\n\\t<%if [<tv-config-toolbar-icons>match[yes]] %>\\n\\t\\t{{$:/core/images/export-button}}\\n\\t<%endif%>\\n\\t<%if [<tv-config-toolbar-text>match[yes]] %>\\n\\t\\t<span class=\\\"tc-btn-text\\\">\\n\\t\\t<$text text=\\\"发送给 Agent\\\"/>\\n\\t</span>\\n\\t<%endif%>\\n</$button>\\n\"\n }\n }\n}\n";
2239
+ const SEND_TO_AGENT_BUNDLE_TEXT = "{\n \"tiddlers\": {\n \"$:/plugins/dsh/send-to-agent/plugin.info\": {\n \"title\": \"$:/plugins/dsh/send-to-agent/plugin.info\",\n \"type\": \"application/json\",\n \"text\": \"{\\\"title\\\":\\\"$:/plugins/dsh/send-to-agent\\\",\\\"name\\\":\\\"Send to Agent\\\",\\\"description\\\":\\\"把当前笔记一键发送给 DSH Agent(TiddlyWiki → DSH 会话注入)\\\",\\\"author\\\":\\\"dsh-tiddlywiki\\\",\\\"version\\\":\\\"0.3.2\\\",\\\"plugin-type\\\":\\\"plugin\\\"}\"\n },\n \"$:/plugins/dsh/send-to-agent/startup.js\": {\n \"title\": \"$:/plugins/dsh/send-to-agent/startup.js\",\n \"type\": \"application/javascript\",\n \"module-type\": \"startup\",\n \"text\": \"/*\\\\\\ntitle: $:/plugins/dsh/send-to-agent/startup.js\\ntype: application/javascript\\nmodule-type: startup\\n\\n\\\\*/\\n(function(){\\n\\n/*jslint node: true, browser: true */\\n/*global $tw: false */\\n\\\"use strict\\\";\\n\\nexports.name = \\\"dsh-send-to-agent\\\";\\nexports.after = [\\\"story\\\"];\\nexports.platforms = [\\\"browser\\\"];\\n\\nfunction readConfig() {\\n\\tvar config = { enabled: true, endpoint: \\\"\\\", token: \\\"\\\" };\\n\\ttry {\\n\\t\\tvar t = $tw.wiki.getTiddler(\\\"$:/plugins/dsh-tiddlywiki/config\\\");\\n\\t\\tif (t && t.fields && typeof t.fields.text === \\\"string\\\") {\\n\\t\\t\\tvar parsed = JSON.parse(t.fields.text);\\n\\t\\t\\tvar s2a = parsed && parsed.ui && parsed.ui.sendToAgent;\\n\\t\\t\\tif (s2a) {\\n\\t\\t\\t\\tif (typeof s2a.enabled === \\\"boolean\\\") { config.enabled = s2a.enabled; }\\n\\t\\t\\t\\tif (typeof s2a.endpoint === \\\"string\\\" && s2a.endpoint.length > 0) { config.endpoint = s2a.endpoint; }\\n\\t\\t\\t\\tif (typeof s2a.token === \\\"string\\\" && s2a.token.length > 0) { config.token = s2a.token; }\\n\\t\\t\\t}\\n\\t\\t}\\n\\t} catch (e) {}\\n\\treturn config;\\n}\\n\\nfunction baseEndpoint() {\\n\\tvar config = readConfig();\\n\\tif (config.endpoint.length > 0) { return config.endpoint.replace(/\\\\/+$/, \\\"\\\"); }\\n\\tif (typeof location !== \\\"undefined\\\" && location.origin) { return location.origin + \\\"/dsh-tiddlywiki\\\"; }\\n\\treturn \\\"/dsh-tiddlywiki\\\";\\n}\\n\\nfunction notify(msg) {\\n\\tif ($tw.notifier && typeof $tw.notifier.display === \\\"function\\\") {\\n\\t\\t$tw.notifier.display(msg);\\n\\t} else if (typeof alert === \\\"function\\\") {\\n\\t\\talert(msg);\\n\\t}\\n}\\n\\nfunction doSend(payload, sessionId, note) {\\n\\tvar config = readConfig();\\n\\tvar lines = [];\\n\\tlines.push(\\\"《\\\" + payload.title + \\\"》\\\");\\n\\tlines.push(\\\"标签: \\\" + (payload.tags || []).join(\\\", \\\"));\\n\\tlines.push(\\\"类型: \\\" + (payload.type || \\\"无\\\"));\\n\\tlines.push(\\\"\\\");\\n\\tlines.push(\\\"【待办说明】以下内容是我(用户)提前编辑在 TiddlyWiki 知识库中的待办事项,通过「发送给 Agent」一键发送给你处理。请按内容执行;如有任何不清楚的地方,请主动向我提问,不要臆测或擅自发挥。\\\");\\n\\tif (note && String(note).trim().length > 0) {\\n\\t\\tlines.push(\\\"\\\");\\n\\t\\tlines.push(\\\"【附加说明】\\\" + String(note).trim());\\n\\t}\\n\\tlines.push(\\\"\\\");\\n\\tlines.push(payload.text || \\\"\\\");\\n\\tvar text = lines.join(\\\"\\\\n\\\");\\n\\tvar headers = { \\\"Content-Type\\\": \\\"application/json\\\" };\\n\\tif (config.token.length > 0) { headers[\\\"x-send-to-agent-token\\\"] = config.token; }\\n\\t$tw.utils.httpRequest({\\n\\t\\turl: baseEndpoint() + \\\"/agent/send\\\",\\n\\t\\ttype: \\\"POST\\\",\\n\\t\\theaders: headers,\\n\\t\\tdata: JSON.stringify({ sessionId: sessionId, text: text }),\\n\\t\\tcallback: function(err, data) {\\n\\t\\t\\tvar parsed = null;\\n\\t\\t\\ttry { parsed = JSON.parse(data || \\\"\\\"); } catch (e) {}\\n\\t\\t\\tif (err) { notify(\\\"发送失败:\\\" + err); return; }\\n\\t\\t\\tif (parsed && parsed.ok) {\\n\\t\\t\\t\\tnotify(\\\"已发送 ✓ 会话 \\\" + sessionId.slice(0, 8));\\n\\t\\t\\t} else {\\n\\t\\t\\t\\tnotify(\\\"发送失败:\\\" + ((parsed && parsed.error) || data || \\\"未知错误\\\"));\\n\\t\\t\\t}\\n\\t\\t}\\n\\t});\\n}\\n\\nfunction closeOverlay(overlay, escHandler) {\\n\\tif (escHandler) { document.removeEventListener(\\\"keydown\\\", escHandler); }\\n\\tif (overlay && overlay.parentNode) { overlay.parentNode.removeChild(overlay); }\\n}\\n\\nfunction showPicker(payload, items, modes, defaultId, permissions) {\\n\\tif (typeof document === \\\"undefined\\\") { return; }\\n\\t// remove any previously-open picker\\n\\tvar old = document.getElementById(\\\"dsh-send-picker\\\");\\n\\tif (old && old.parentNode) { old.parentNode.removeChild(old); }\\n\\n\\tvar state = { workspace: null, mode: \\\"\\\", note: \\\"\\\", permission: \\\"\\\" };\\n\\n\\tvar overlay = document.createElement(\\\"div\\\");\\n\\toverlay.id = \\\"dsh-send-picker\\\";\\n\\toverlay.setAttribute(\\\"style\\\", \\\"position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.35);z-index:99999;display:flex;align-items:center;justify-content:center;\\\");\\n\\tvar box = document.createElement(\\\"div\\\");\\n\\tbox.setAttribute(\\\"style\\\", \\\"background:#fff;color:#333;border-radius:8px;padding:14px;min-width:340px;max-width:560px;max-height:82vh;display:flex;flex-direction:column;box-shadow:0 8px 30px rgba(0,0,0,.3);font-family:system-ui,-apple-system,sans-serif;\\\");\\n\\n\\t// header\\n\\tvar header = document.createElement(\\\"div\\\");\\n\\theader.setAttribute(\\\"style\\\", \\\"display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;\\\");\\n\\tvar h = document.createElement(\\\"h3\\\");\\n\\th.setAttribute(\\\"style\\\", \\\"margin:0;font-size:15px;\\\");\\n\\th.textContent = \\\"发送给 Agent · 选择目标\\\";\\n\\tvar closeX = document.createElement(\\\"button\\\");\\n\\tcloseX.type = \\\"button\\\";\\n\\tcloseX.textContent = \\\"✕\\\";\\n\\tcloseX.title = \\\"关闭\\\";\\n\\tcloseX.setAttribute(\\\"style\\\", \\\"border:none;background:transparent;font-size:15px;cursor:pointer;color:#888;padding:2px 8px;border-radius:4px;\\\");\\n\\tcloseX.addEventListener(\\\"click\\\", function() { closeOverlay(overlay, escHandler); });\\n\\theader.appendChild(h);\\n\\theader.appendChild(closeX);\\n\\tbox.appendChild(header);\\n\\n\\t// description\\n\\tvar desc = document.createElement(\\\"p\\\");\\n\\tdesc.setAttribute(\\\"style\\\", \\\"margin:0 0 8px;font-size:12px;color:#888;word-break:break-all;\\\");\\n\\tdesc.textContent = \\\"《\\\" + payload.title + \\\"》将作为消息注入所选会话\\\";\\n\\tbox.appendChild(desc);\\n\\n\\t// 附加说明(可选)— any extra context the user wants to attach to the\\n\\t// message, e.g. what to focus on or how to handle it.\\n\\tvar noteRow = document.createElement(\\\"div\\\");\\n\\tnoteRow.setAttribute(\\\"style\\\", \\\"margin:0 0 8px;\\\");\\n\\tvar noteLbl = document.createElement(\\\"label\\\");\\n\\tnoteLbl.setAttribute(\\\"style\\\", \\\"display:block;font-size:12px;color:#666;margin-bottom:4px;\\\");\\n\\tnoteLbl.textContent = \\\"附加说明(可选,随笔记一起发给 Agent)\\\";\\n\\tnoteRow.appendChild(noteLbl);\\n\\tvar noteTa = document.createElement(\\\"textarea\\\");\\n\\tnoteTa.placeholder = \\\"例如:请重点看第 3 条;我希望你按 XX 方式处理…\\\";\\n\\tnoteTa.setAttribute(\\\"style\\\", \\\"width:100%;box-sizing:border-box;padding:6px 8px;border:1px solid #ccc;border-radius:6px;font-size:13px;min-height:44px;resize:vertical;font-family:inherit;\\\");\\n\\tnoteTa.addEventListener(\\\"input\\\", function() { state.note = noteTa.value; });\\n\\tnoteRow.appendChild(noteTa);\\n\\tbox.appendChild(noteRow);\\n\\n\\t// 工作模式(Agent 预设)selector — applies to newly created sessions; the\\n\\t// modes come from GET /agent/modes (id/name/description + deployment\\n\\t// default). When the modes endpoint is unreachable (e.g. an older host) the\\n\\t// row degrades to a hint and no mode is sent — DSH uses its default.\\n\\tvar modeRow = document.createElement(\\\"div\\\");\\n\\tmodeRow.setAttribute(\\\"style\\\", \\\"margin:0 0 8px;\\\");\\n\\tif (modes && modes.length > 0) {\\n\\t\\tvar modeLbl = document.createElement(\\\"label\\\");\\n\\t\\tmodeLbl.setAttribute(\\\"style\\\", \\\"display:block;font-size:12px;color:#666;margin-bottom:4px;\\\");\\n\\t\\tmodeLbl.textContent = \\\"工作模式(Agent 预设)— 用于新建会话\\\";\\n\\t\\tmodeRow.appendChild(modeLbl);\\n\\t\\tvar sel = document.createElement(\\\"select\\\");\\n\\t\\tsel.setAttribute(\\\"style\\\", \\\"width:100%;box-sizing:border-box;padding:6px 8px;border:1px solid #ccc;border-radius:6px;font-size:13px;background:#fff;\\\");\\n\\t\\tvar optDefault = document.createElement(\\\"option\\\");\\n\\t\\toptDefault.value = \\\"\\\";\\n\\t\\toptDefault.textContent = \\\"(默认模式)\\\";\\n\\t\\tsel.appendChild(optDefault);\\n\\t\\tmodes.forEach(function(m) {\\n\\t\\t\\tvar o = document.createElement(\\\"option\\\");\\n\\t\\t\\to.value = m.id || \\\"\\\";\\n\\t\\t\\to.textContent = (m.name || m.id) + (m.isDefault ? \\\"(默认)\\\" : \\\"\\\");\\n\\t\\t\\tsel.appendChild(o);\\n\\t\\t});\\n\\t\\t// preselect the deployment default when listed\\n\\t\\tif (defaultId) {\\n\\t\\t\\tfor (var i = 0; i < sel.options.length; i++) {\\n\\t\\t\\t\\tif (sel.options[i].value === defaultId) { sel.selectedIndex = i; break; }\\n\\t\\t\\t}\\n\\t\\t}\\n\\t\\tstate.mode = sel.options[sel.selectedIndex] ? sel.options[sel.selectedIndex].value : \\\"\\\";\\n\\t\\tsel.addEventListener(\\\"change\\\", function() { state.mode = sel.value; });\\n\\t\\tmodeRow.appendChild(sel);\\n\\t} else {\\n\\t\\tvar modeHint = document.createElement(\\\"p\\\");\\n\\t\\tmodeHint.setAttribute(\\\"style\\\", \\\"margin:0;font-size:12px;color:#999;\\\");\\n\\t\\tmodeHint.textContent = \\\"(未获取到可用工作模式,将使用 DSH 默认模式)\\\";\\n\\t\\tmodeRow.appendChild(modeHint);\\n\\t}\\n\\tbox.appendChild(modeRow);\\n\\n\\t// 权限(权限预设)selector — applies to newly created sessions only; the\\n\\t// options come from GET /agent/modes' `permissions` roster (each bundles a\\n\\t// sandbox mode + approval policy). Existing sessions keep their own\\n\\t// permission, so this only affects \\\"新建会话并发送\\\".\\n\\tvar permRow = document.createElement(\\\"div\\\");\\n\\tpermRow.setAttribute(\\\"style\\\", \\\"margin:0 0 8px;\\\");\\n\\tif (permissions && Array.isArray(permissions.items) && permissions.items.length > 0) {\\n\\t\\tvar permLbl = document.createElement(\\\"label\\\");\\n\\t\\tpermLbl.setAttribute(\\\"style\\\", \\\"display:block;font-size:12px;color:#666;margin-bottom:4px;\\\");\\n\\t\\tpermLbl.textContent = \\\"权限(权限预设)— 用于新建会话\\\";\\n\\t\\tpermRow.appendChild(permLbl);\\n\\t\\tvar psel = document.createElement(\\\"select\\\");\\n\\t\\tpsel.setAttribute(\\\"style\\\", \\\"width:100%;box-sizing:border-box;padding:6px 8px;border:1px solid #ccc;border-radius:6px;font-size:13px;background:#fff;\\\");\\n\\t\\tpermissions.items.forEach(function(p) {\\n\\t\\t\\tvar o = document.createElement(\\\"option\\\");\\n\\t\\t\\to.value = p.value || \\\"\\\";\\n\\t\\t\\to.textContent = p.name || p.value;\\n\\t\\t\\tif (p.description) { o.title = p.description; }\\n\\t\\t\\tpsel.appendChild(o);\\n\\t\\t});\\n\\t\\t// preselect the deployment default when listed\\n\\t\\tif (permissions.defaultId) {\\n\\t\\t\\tfor (var j = 0; j < psel.options.length; j++) {\\n\\t\\t\\t\\tif (psel.options[j].value === permissions.defaultId) { psel.selectedIndex = j; break; }\\n\\t\\t\\t}\\n\\t\\t}\\n\\t\\tstate.permission = psel.options[psel.selectedIndex] ? psel.options[psel.selectedIndex].value : \\\"\\\";\\n\\t\\tpsel.addEventListener(\\\"change\\\", function() { state.permission = psel.value; });\\n\\t\\tpermRow.appendChild(psel);\\n\\t} else {\\n\\t\\tvar permHint = document.createElement(\\\"p\\\");\\n\\t\\tpermHint.setAttribute(\\\"style\\\", \\\"margin:0;font-size:12px;color:#999;\\\");\\n\\t\\tpermHint.textContent = \\\"(未获取到权限预设,新建会话将使用 DSH 默认权限)\\\";\\n\\t\\tpermRow.appendChild(permHint);\\n\\t}\\n\\tbox.appendChild(permRow);\\n\\n\\t// scrollable body\\n\\tvar body = document.createElement(\\\"div\\\");\\n\\tbody.setAttribute(\\\"style\\\", \\\"overflow:auto;flex:1;min-height:0;\\\");\\n\\tbox.appendChild(body);\\n\\n\\t// footer with cancel\\n\\tvar footer = document.createElement(\\\"div\\\");\\n\\tfooter.setAttribute(\\\"style\\\", \\\"display:flex;justify-content:flex-end;gap:8px;margin-top:10px;\\\");\\n\\tvar cancel = document.createElement(\\\"button\\\");\\n\\tcancel.type = \\\"button\\\";\\n\\tcancel.textContent = \\\"取消\\\";\\n\\tcancel.setAttribute(\\\"style\\\", \\\"padding:6px 14px;border:1px solid #ccc;border-radius:6px;background:#fff;cursor:pointer;font-size:13px;\\\");\\n\\tcancel.addEventListener(\\\"click\\\", function() { closeOverlay(overlay, escHandler); });\\n\\tfooter.appendChild(cancel);\\n\\tbox.appendChild(footer);\\n\\n\\toverlay.appendChild(box);\\n\\tdocument.body.appendChild(overlay);\\n\\n\\t// backdrop click + Esc close\\n\\toverlay.addEventListener(\\\"click\\\", function(e) {\\n\\t\\tif (e.target === overlay) { closeOverlay(overlay, escHandler); }\\n\\t});\\n\\tvar escHandler = function(e) { if (e.key === \\\"Escape\\\") { closeOverlay(overlay, escHandler); } };\\n\\tdocument.addEventListener(\\\"keydown\\\", escHandler);\\n\\n\\t// group sessions by workspace (cwd)\\n\\tvar groups = {};\\n\\tvar order = [];\\n\\t(items || []).forEach(function(s) {\\n\\t\\tvar key = (s.cwd && String(s.cwd).length > 0) ? s.cwd : \\\"__default__\\\";\\n\\t\\tif (!groups[key]) { groups[key] = { cwd: s.cwd, sessions: [], max: 0 }; order.push(key); }\\n\\t\\tgroups[key].sessions.push(s);\\n\\t\\tif ((s.updatedAt || 0) > groups[key].max) { groups[key].max = s.updatedAt || 0; }\\n\\t});\\n\\torder.sort(function(a, b) { return groups[b].max - groups[a].max; });\\n\\n\\tfunction btnStyle() {\\n\\t\\treturn \\\"display:block;width:100%;text-align:left;padding:8px 10px;margin:4px 0;border:1px solid #ddd;border-radius:6px;background:#f7f7f7;cursor:pointer;font-size:13px;\\\";\\n\\t}\\n\\tfunction primaryBtnStyle() {\\n\\t\\treturn \\\"display:block;width:100%;text-align:center;padding:7px 10px;margin:6px 0 0;border:1px solid #4a90d9;border-radius:6px;background:#4a90d9;color:#fff;cursor:pointer;font-size:13px;\\\";\\n\\t}\\n\\tfunction smallBtnStyle() {\\n\\t\\treturn \\\"margin:0 0 6px;padding:4px 10px;border:1px solid #ccc;border-radius:6px;background:#fff;cursor:pointer;font-size:12px;color:#555;\\\";\\n\\t}\\n\\n\\tfunction shortSessionLabel(s) {\\n\\t\\tvar id = s.sessionId || \\\"\\\";\\n\\t\\tvar parts = id.split(\\\"-\\\");\\n\\t\\tvar short = (parts.length > 1 ? parts[parts.length - 1] : id).slice(-10);\\n\\t\\tvar when = \\\"\\\";\\n\\t\\tif (s.updatedAt) {\\n\\t\\t\\tvar diff = Date.now() - s.updatedAt;\\n\\t\\t\\tif (diff < 60000) { when = \\\"刚刚\\\"; }\\n\\t\\t\\telse if (diff < 3600000) { when = Math.floor(diff / 60000) + \\\" 分钟前\\\"; }\\n\\t\\t\\telse if (diff < 86400000) { when = Math.floor(diff / 3600000) + \\\" 小时前\\\"; }\\n\\t\\t\\telse {\\n\\t\\t\\t\\tvar d = new Date(s.updatedAt);\\n\\t\\t\\t\\twhen = (d.getMonth() + 1) + \\\"-\\\" + d.getDate() + \\\" \\\" + (\\\"0\\\" + d.getHours()).slice(-2) + \\\":\\\" + (\\\"0\\\" + d.getMinutes()).slice(-2);\\n\\t\\t\\t}\\n\\t\\t}\\n\\t\\treturn \\\"#\\\" + short + (when ? \\\" · \\\" + when : \\\"\\\") + (s.running ? \\\" · ●运行中\\\" : \\\"\\\") + (s.blank ? \\\" · (空)\\\" : \\\"\\\");\\n\\t}\\n\\n\\tfunction modeName(id) {\\n\\t\\tif (!id) { return \\\"\\\"; }\\n\\t\\tfor (var i = 0; i < (modes || []).length; i++) {\\n\\t\\t\\tif (modes[i].id === id) { return modes[i].name || modes[i].id; }\\n\\t\\t}\\n\\t\\treturn id;\\n\\t}\\n\\n\\tfunction render() {\\n\\t\\tbody.innerHTML = \\\"\\\";\\n\\t\\tif (state.workspace === null) { renderWorkspaces(); } else { renderSessions(state.workspace); }\\n\\t}\\n\\n\\tfunction renderWorkspaces() {\\n\\t\\tif (order.length === 0) {\\n\\t\\t\\tvar none = document.createElement(\\\"p\\\");\\n\\t\\t\\tnone.textContent = \\\"还没有任何会话——在下方新建一个吧\\\";\\n\\t\\t\\tnone.setAttribute(\\\"style\\\", \\\"color:#999;font-size:13px;margin:0 0 8px;\\\");\\n\\t\\t\\tbody.appendChild(none);\\n\\t\\t} else {\\n\\t\\t\\torder.forEach(function(key) {\\n\\t\\t\\t\\tvar g = groups[key];\\n\\t\\t\\t\\tvar label = g.cwd || \\\"(默认工作区)\\\";\\n\\t\\t\\t\\tvar b = document.createElement(\\\"button\\\");\\n\\t\\t\\t\\tb.type = \\\"button\\\";\\n\\t\\t\\t\\tb.setAttribute(\\\"style\\\", btnStyle());\\n\\t\\t\\t\\tb.textContent = \\\"📁 \\\" + label + \\\" · \\\" + g.sessions.length + \\\" 个会话\\\";\\n\\t\\t\\t\\tb.title = g.cwd || \\\"默认工作区\\\";\\n\\t\\t\\t\\tb.addEventListener(\\\"click\\\", function() { state.workspace = key; render(); });\\n\\t\\t\\t\\tbody.appendChild(b);\\n\\t\\t\\t});\\n\\t\\t}\\n\\t\\t// new workspace / session\\n\\t\\tvar newRow = document.createElement(\\\"div\\\");\\n\\t\\tnewRow.setAttribute(\\\"style\\\", \\\"margin-top:10px;border-top:1px solid #eee;padding-top:8px;\\\");\\n\\t\\tvar lbl = document.createElement(\\\"p\\\");\\n\\t\\tlbl.textContent = \\\"新建工作区 / 会话\\\";\\n\\t\\tlbl.setAttribute(\\\"style\\\", \\\"margin:0 0 6px;font-size:12px;color:#666;\\\");\\n\\t\\tnewRow.appendChild(lbl);\\n\\t\\tvar input = document.createElement(\\\"input\\\");\\n\\t\\tinput.type = \\\"text\\\";\\n\\t\\tinput.placeholder = \\\"输入工作区路径,留空为默认工作区\\\";\\n\\t\\tinput.setAttribute(\\\"style\\\", \\\"width:100%;box-sizing:border-box;padding:6px 8px;border:1px solid #ccc;border-radius:6px;font-size:13px;\\\");\\n\\t\\tnewRow.appendChild(input);\\n\\t\\tvar go = document.createElement(\\\"button\\\");\\n\\t\\tgo.type = \\\"button\\\";\\n\\t\\tgo.textContent = \\\"创建并发送\\\";\\n\\t\\tgo.setAttribute(\\\"style\\\", primaryBtnStyle());\\n\\t\\tgo.addEventListener(\\\"click\\\", function() { createAndSend(payload, input.value.trim()); });\\n\\t\\tnewRow.appendChild(go);\\n\\t\\tinput.addEventListener(\\\"keydown\\\", function(e) { if (e.key === \\\"Enter\\\") { createAndSend(payload, input.value.trim()); } });\\n\\t\\tbody.appendChild(newRow);\\n\\t}\\n\\n\\tfunction renderSessions(key) {\\n\\t\\tvar g = groups[key];\\n\\t\\tvar back = document.createElement(\\\"button\\\");\\n\\t\\tback.type = \\\"button\\\";\\n\\t\\tback.textContent = \\\"← 返回工作区列表\\\";\\n\\t\\tback.setAttribute(\\\"style\\\", smallBtnStyle());\\n\\t\\tback.addEventListener(\\\"click\\\", function() { state.workspace = null; render(); });\\n\\t\\tbody.appendChild(back);\\n\\n\\t\\tvar wsName = document.createElement(\\\"p\\\");\\n\\t\\twsName.textContent = g.cwd ? \\\"📁 \\\" + g.cwd : \\\"(默认工作区)\\\";\\n\\t\\twsName.setAttribute(\\\"style\\\", \\\"margin:0 0 6px;font-size:13px;font-weight:600;word-break:break-all;\\\");\\n\\t\\tbody.appendChild(wsName);\\n\\n\\t\\t(g.sessions || []).forEach(function(s) {\\n\\t\\t\\tvar b = document.createElement(\\\"button\\\");\\n\\t\\t\\tb.type = \\\"button\\\";\\n\\t\\t\\tb.setAttribute(\\\"style\\\", btnStyle());\\n\\t\\t\\tvar label = shortSessionLabel(s);\\n\\t\\t\\tif (s.agentPreset) { label += \\\" · 🧭 \\\" + modeName(s.agentPreset); }\\n\\t\\t\\tb.textContent = label;\\n\\t\\t\\tb.title = \\\"会话 \\\" + s.sessionId + (s.agentPreset ? \\\" · 工作模式 \\\" + modeName(s.agentPreset) : \\\"\\\");\\n\\t\\t\\tb.addEventListener(\\\"click\\\", function() { closeOverlay(overlay, escHandler); doSend(payload, s.sessionId, state.note); });\\n\\t\\t\\tbody.appendChild(b);\\n\\t\\t});\\n\\n\\t\\tvar newBtn = document.createElement(\\\"button\\\");\\n\\t\\tnewBtn.type = \\\"button\\\";\\n\\t\\tnewBtn.textContent = \\\"➕ 在此工作区新建会话并发送\\\";\\n\\t\\tnewBtn.setAttribute(\\\"style\\\", primaryBtnStyle());\\n\\t\\tnewBtn.addEventListener(\\\"click\\\", function() { createAndSend(payload, g.cwd); });\\n\\t\\tbody.appendChild(newBtn);\\n\\t}\\n\\n\\tfunction createAndSend(payload2, cwd) {\\n\\t\\tvar config = readConfig();\\n\\t\\tvar headers = { \\\"Content-Type\\\": \\\"application/json\\\" };\\n\\t\\tif (config.token.length > 0) { headers[\\\"x-send-to-agent-token\\\"] = config.token; }\\n\\t\\tvar body = { cwd: cwd || \\\"\\\" };\\n\\t\\tif (state.mode && String(state.mode).length > 0) { body.mode = state.mode; }\\n\\t\\tif (state.permission && String(state.permission).length > 0) { body.permission = state.permission; }\\n\\t\\t$tw.utils.httpRequest({\\n\\t\\t\\turl: baseEndpoint() + \\\"/agent/create\\\",\\n\\t\\t\\ttype: \\\"POST\\\",\\n\\t\\t\\theaders: headers,\\n\\t\\t\\tdata: JSON.stringify(body),\\n\\t\\t\\tcallback: function(err, data) {\\n\\t\\t\\t\\tvar parsed = null;\\n\\t\\t\\t\\ttry { parsed = JSON.parse(data || \\\"\\\"); } catch (e) {}\\n\\t\\t\\t\\tif (err) { notify(\\\"新建会话失败:\\\" + err); return; }\\n\\t\\t\\t\\tif (parsed && parsed.ok && parsed.sessionId) {\\n\\t\\t\\t\\t\\tcloseOverlay(overlay, escHandler);\\n\\t\\t\\t\\t\\tvar modeNote = parsed.mode ? \\\"(模式 \\\" + modeName(parsed.mode) + \\\")\\\" : \\\"\\\";\\n\\t\\t\\t\\t\\tvar permNote = parsed.permissionApplied ? \\\"(权限 \\\" + (parsed.permission || \\\"\\\") + \\\")\\\" : \\\"\\\";\\n\\t\\t\\t\\t\\tnotify(\\\"已创建会话并发送 ✓ \\\" + parsed.sessionId.slice(0, 8) + modeNote + permNote);\\n\\t\\t\\t\\t\\tdoSend(payload2, parsed.sessionId, state.note);\\n\\t\\t\\t\\t} else {\\n\\t\\t\\t\\t\\tnotify(\\\"新建会话失败:\\\" + ((parsed && parsed.error) || data || \\\"未知错误\\\"));\\n\\t\\t\\t\\t}\\n\\t\\t\\t}\\n\\t\\t});\\n\\t}\\n\\n\\trender();\\n}\\n\\nfunction handleSend(title) {\\n\\tif (!title) { notify(\\\"无法确定当前笔记标题\\\"); return; }\\n\\tvar tiddler = $tw.wiki.getTiddler(title);\\n\\tif (!tiddler) { notify(\\\"找不到笔记:\\\" + title); return; }\\n\\tvar config = readConfig();\\n\\tif (!config.enabled) { notify(\\\"「发送给 Agent」特性未启用(可在 TW 配置中打开)\\\"); return; }\\n\\tvar payload = {\\n\\t\\ttitle: title,\\n\\t\\ttext: tiddler.fields.text || \\\"\\\",\\n\\t\\ttags: tiddler.fields.tags || [],\\n\\t\\ttype: tiddler.fields.type || \\\"\\\"\\n\\t};\\n\\tvar headers = {};\\n\\tif (config.token.length > 0) { headers[\\\"x-send-to-agent-token\\\"] = config.token; }\\n\\t$tw.utils.httpRequest({\\n\\t\\turl: baseEndpoint() + \\\"/agent/sessions\\\",\\n\\t\\ttype: \\\"GET\\\",\\n\\t\\theaders: headers,\\n\\t\\tcallback: function(err, data) {\\n\\t\\t\\tvar parsed = null;\\n\\t\\t\\ttry { parsed = JSON.parse(data || \\\"\\\"); } catch (e) {}\\n\\t\\t\\tif (err) { notify(\\\"获取会话列表失败:\\\" + err); return; }\\n\\t\\t\\tif (!parsed || !parsed.ok || !parsed.items) { notify(\\\"获取会话列表失败:\\\" + ((parsed && parsed.error) || \\\"未知错误\\\")); return; }\\n\\t\\t\\t// fetch the available 工作模式 (Agent presets) alongside; degrade to\\n\\t\\t\\t// an empty roster (default mode) when the endpoint is missing.\\n\\t\\t\\t$tw.utils.httpRequest({\\n\\t\\t\\t\\turl: baseEndpoint() + \\\"/agent/modes\\\",\\n\\t\\t\\t\\ttype: \\\"GET\\\",\\n\\t\\t\\t\\theaders: headers,\\n\\t\\t\\t\\tcallback: function(err2, data2) {\\n\\t\\t\\t\\t\\tvar modes = [];\\n\\t\\t\\t\\t\\tvar defaultId = \\\"\\\";\\n\\t\\t\\t\\t\\tvar permissions = null;\\n\\t\\t\\t\\t\\tvar parsed2 = null;\\n\\t\\t\\t\\t\\ttry { parsed2 = JSON.parse(data2 || \\\"\\\"); } catch (e) {}\\n\\t\\t\\t\\t\\tif (!err2 && parsed2 && parsed2.ok) {\\n\\t\\t\\t\\t\\t\\tif (Array.isArray(parsed2.items)) {\\n\\t\\t\\t\\t\\t\\t\\tmodes = parsed2.items;\\n\\t\\t\\t\\t\\t\\t\\tdefaultId = parsed2.defaultId || \\\"\\\";\\n\\t\\t\\t\\t\\t\\t}\\n\\t\\t\\t\\t\\t\\tif (parsed2.permissions && Array.isArray(parsed2.permissions.items)) {\\n\\t\\t\\t\\t\\t\\t\\tpermissions = parsed2.permissions;\\n\\t\\t\\t\\t\\t\\t}\\n\\t\\t\\t\\t\\t}\\n\\t\\t\\t\\t\\tshowPicker(payload, parsed.items, modes, defaultId, permissions);\\n\\t\\t\\t\\t}\\n\\t\\t\\t});\\n\\t\\t}\\n\\t});\\n}\\n\\nexports.startup = function() {\\n\\tconsole.log(\\\"[dsh-send-to-agent] startup ran\\\");\\n\\tif (!$tw.rootWidget || typeof $tw.rootWidget.addEventListener !== \\\"function\\\") { return; }\\n\\t$tw.rootWidget.addEventListener(\\\"dsh-send-to-agent\\\", function(event) {\\n\\t\\thandleSend(event.param);\\n\\t});\\n};\\n\\n})();\\n\"\n },\n \"$:/plugins/dsh/send-to-agent/ui/icon\": {\n \"title\": \"$:/plugins/dsh/send-to-agent/ui/icon\",\n \"tags\": [\n \"$:/tags/Image\"\n ],\n \"text\": \"\\\\parameters (size:\\\"22pt\\\")\\n<svg width=<<size>> height=<<size>> class=\\\"tc-image-send-to-agent tc-image-button\\\" viewBox=\\\"0 0 24 24\\\"><path d=\\\"M3.478 2.404a.75.75 0 0 0-.926.941l2.432 7.905H13.5a.75.75 0 0 1 0 1.5H4.984l-2.432 7.905a.75.75 0 0 0 .926.94 60.519 60.519 0 0 0 18.445-8.986.75.75 0 0 0 0-1.218A60.517 60.517 0 0 0 3.478 2.404Z\\\"/></svg>\\n\"\n },\n \"$:/plugins/dsh/send-to-agent/ui/ViewToolbar/SendToAgent\": {\n \"title\": \"$:/plugins/dsh/send-to-agent/ui/ViewToolbar/SendToAgent\",\n \"type\": \"text/vnd.tiddlywiki\",\n \"tags\": [\n \"$:/tags/ViewToolbar\"\n ],\n \"icon\": \"$:/plugins/dsh/send-to-agent/ui/icon\",\n \"caption\": \"发送给 Agent\",\n \"description\": \"把当前笔记一键发送给 DSH Agent(TiddlyWiki → DSH 会话注入)\",\n \"text\": \"\\\\whitespace trim\\n<$button message=\\\"dsh-send-to-agent\\\"\\n\\tparam=<<currentTiddler>>\\n\\ttooltip=\\\"把当前笔记一键发送给 DSH Agent\\\"\\n\\taria-label=\\\"把当前笔记一键发送给 DSH Agent\\\"\\n\\tclass=<<tv-config-toolbar-class>>\\n>\\n\\t<%if [<tv-config-toolbar-icons>match[yes]] %>\\n\\t\\t{{$:/plugins/dsh/send-to-agent/ui/icon}}\\n\\t<%endif%>\\n\\t<%if [<tv-config-toolbar-text>match[yes]] %>\\n\\t\\t<span class=\\\"tc-btn-text\\\">\\n\\t\\t<$text text=\\\"发送给 Agent\\\"/>\\n\\t</span>\\n\\t<%endif%>\\n</$button>\\n\"\n },\n \"$:/core/ui/ControlPanel/Toolbars/ItemTemplate\": {\n \"title\": \"$:/core/ui/ControlPanel/Toolbars/ItemTemplate\",\n \"type\": \"text/vnd.tiddlywiki\",\n \"text\": \"title: $:/core/ui/ControlPanel/Toolbars/ItemTemplate\\n\\n\\\\define config-title()\\n$(config-base)$$(currentTiddler)$\\n\\\\end\\n\\\\whitespace trim\\n\\n<$draggable tiddler=<<currentTiddler>>>\\n<$checkbox tiddler=<<config-title>> field=\\\"text\\\" checked=\\\"show\\\" unchecked=\\\"hide\\\" default=\\\"show\\\"/>\\n&#32;\\n<span class=\\\"tc-icon-wrapper\\\"><$transclude tiddler={{!!icon}}/></span>\\n&#32;\\n<$transclude field=\\\"caption\\\"/>\\n&#32;--&#32;\\n<i class=\\\"tc-muted\\\"><$transclude field=\\\"description\\\"/></i>\\n</$draggable>\\n\"\n }\n }\n}";
2199
2240
  /**
2200
2241
  * Seed the "发送给 Agent" TW button exactly once per wiki (mirrors the doc-note
2201
2242
  * one-shot policy). The marker records the offer; afterwards the bundle is
@@ -4159,6 +4200,8 @@ function apply(ctx, rawConfig = {}) {
4159
4200
  const getWorkspaceRegistry = () => ctx.get("workspaceRegistry");
4160
4201
  const getAgentPresets = () => ctx.get("agentPresets");
4161
4202
  const getSessionPersistence = () => ctx.get("sessionPersistence");
4203
+ const getPermissionPresets = () => ctx.get("permissionPresets");
4204
+ const getSessions = () => ctx.get("sessions");
4162
4205
  const disposeRoutes = registerRoutes({ webServer: ws }, {
4163
4206
  server,
4164
4207
  getClient: client,
@@ -4171,6 +4214,8 @@ function apply(ctx, rawConfig = {}) {
4171
4214
  getWorkspaceRegistry,
4172
4215
  getAgentPresets,
4173
4216
  getSessionPersistence,
4217
+ getPermissionPresets,
4218
+ getSessions,
4174
4219
  sendToAgentEnabled: () => eff().ui?.sendToAgent?.enabled !== false,
4175
4220
  sendToAgentToken: () => {
4176
4221
  const token = eff().ui?.sendToAgent?.token;