dsh-ui-tweaks 0.11.0 → 0.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/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 dsh-ui-tweaks contributors
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsh-ui-tweaks contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.en.md CHANGED
@@ -36,8 +36,8 @@ A [DeepSeek Harness](https://deepseek-harness.github.io/deepseek-harness/) (DSH)
36
36
  - The list refreshes live via the `host/archived-sessions-changed` event and a session-list re-pull, with no page reload.
37
37
  - **MCP manager (toggleable, off by default)** — an **MCP** page in the Settings dialog listing every configured MCP server (`@deepseek-ai/dsh-mcp-client` loader entries) with its live status, command/url, env vars and registered tools, plus full management: **Add / Edit** (a structured form — instance id, name, stdio or HTTP type, timeout ms, command, args, env — OR raw YAML, both validated), **Enable / Disable / Delete**, and **Restart** (runtime-only). Changes persist to the profile's `cordis.patch.yml` and DSH's built-in patch watcher hot-reloads just that server.
38
38
  - **`/init` slash command (toggleable, off by default)** — type `/init` in the composer (the slash menu shows "Analyze this project and generate an AGENTS.md"), pick a prompt language from the popup (**Chinese / English**), and a complete AGENTS.md bootstrap prompt is submitted into the current session: the agent explores the project on its own (README, manifests, build scripts, key directories), then writes or improves a root `AGENTS.md` addressed to future AI coding agents (overview, common commands, conventions, directory guide, gotchas; existing files are improved in place). Pure client-side contribution; enable it in the UI Tweaks settings section.
39
- - **Whale indicator (toggleable, off by default)** — the brand whale perched on the composer card's **top-right corner** over drifting blue waves (the Claude Desktop crab spot). It stays in its original colour the whole time: idle it floats still (hover or click it to make it swim — an easter egg); while the model works it swims (bob + sway) until the turn finishes. Working state = the session's `running` flag plus the input machine's claimed/submitting phases, so the swim starts the moment you press Enter. Pure CSS animation on DSH theme tokens; honors `prefers-reduced-motion`.
40
- - **Task alerts (toggleable, off by default)** — call you back while the tab sits in the background. Watches **all sessions** (background included) for two event kinds: **finish** (the `running` flag drops, or the host's green `completed` reminder rises) and **interaction** (the session starts waiting for your approval / plan review / answer — the same `pendingInteraction` source as the sidebar amber dot). Three independent channels:
39
+ - **Whale indicator (toggleable, off by default)** — the brand whale perched on the composer card's **top-right corner** over drifting blue waves (the Claude Desktop crab spot). It stays in its original colour the whole time: idle it floats still (hover or click it to make it swim — an easter egg); while the model works it swims (bob + sway) and **breathes between blue and its original colour** — bluest at the top of each bob, back to normal on landing, in sync with the 1.9s swim cycle — until the turn finishes. Working state = the session's `running` flag plus the input machine's claimed/submitting phases, so the swim starts the moment you press Enter. Pure CSS animation on DSH theme tokens; honors `prefers-reduced-motion`.
40
+ - **Task alerts (toggleable, off by default)** — call you back while the tab sits in the background. Watches **all sessions** (background included) for two event kinds: **finish** (the `running` flag drops, or the host's green `completed` reminder rises; a host projection of the logged `turn/end` reason tells **completed / interrupted / failed** apart, and failure alerts carry a truncated error summary) and **interaction** (the session starts waiting for your approval / plan review / answer — the same `pendingInteraction` source as the sidebar amber dot). Three independent channels:
41
41
  - **Tab title flash** — blinks an unread counter `(2) 🔔 …` into the tab title until you come back, then restores it;
42
42
  - **System notifications** (Web Notifications API) — desktop-level; **click one to jump straight to that session**; permission is requested from the settings toggle's click gesture; the OS bark and the chime are mutually exclusive so they never double-ring;
43
43
  - **Chime** — a two-note WebAudio motif synthesized in-process (rising = done, falling = needs you); no audio assets.
package/README.md CHANGED
@@ -39,8 +39,8 @@
39
39
  - **启用 / 停用 / 删除 / 重启**:改动直接写入 profile 的 `cordis.patch.yml`(保持注释与结构),DSH 内置补丁监视器热重载加载器——被改动的那个服务器**实时**启动/停止/重启,不影响其它服务器;重启单独运行时生效、不改配置。环境变量值仅在本机浏览器可见,用于编辑。
40
40
  - 新增后请确认服务器能成功连接(状态为「运行中」并注册了工具);启动失败的实例会显示「错误」并可重启重试。
41
41
  - **`/init` 斜杠命令(可开关,默认关闭)**:在输入框键入 `/init`(斜杠菜单中可见「分析当前项目并生成 AGENTS.md」),回车或点击后弹出语言选择——**中文提示词 / 英文提示词**,选中即向当前会话提交一段完整的 AGENTS.md 引导提示词:代理会自行探索项目(README、清单文件、构建脚本、关键目录),然后在仓库根目录**生成或改进**一份面向未来 AI 编码代理的 `AGENTS.md`(项目简介、常用命令、代码风格约定、目录导览、注意事项;已存在时原地改进不丢内容)。纯客户端实现;在「界面调整」中开启后生效。
42
- - **鲸鱼指示器(可开关,默认关闭)**:输入框卡片**右上角**停一只品牌小鲸鱼(侧边栏同款 FishLogo,Claude Desktop 小螃蟹的同款位置),身下有**蓝色海浪**缓缓漂移。鲸鱼**常驻原色**(主题主标签色,浅色模式即黑色):**空闲时静静浮在海浪上,悬停或点击它也会游起来(彩蛋)**;**模型工作时开始游泳动画**(原地上下浮动 + 左右轻摆),直到干完活。模型工作状态取自当前会话的 `running` 标志,并叠加输入机的 claimed/submitting 阶段,覆盖「按下回车到代理真正开跑」的间隙。鲸鱼几何为内置的 FishLogo 路径(与 DSH 主程序逐字节一致),挂载时仍会尝试从侧边栏实时徽标抓取最新版以跟随官方换标;动画纯 CSS、颜色走 DSH 主题变量(深浅色模式均正常),`prefers-reduced-motion` 时降级为静止。
43
- - **任务提醒(可开关,默认关闭)**:挂着任务切去干别的时,让浏览器把你喊回来。监听**所有会话**(含后台),两类事件:**完成提醒**(`running` 落下,或宿主 `completed` 绿点升沿)与**交互提醒**(会话开始等待你的审批 / 计划确认 / 模型提问——与侧边栏黄点同一数据源 `pendingInteraction`)。三个独立通道:
42
+ - **鲸鱼指示器(可开关,默认关闭)**:输入框卡片**右上角**停一只品牌小鲸鱼(侧边栏同款 FishLogo,Claude Desktop 小螃蟹的同款位置),身下有**蓝色海浪**缓缓漂移。鲸鱼**常驻原色**(主题主标签色,浅色模式即黑色):**空闲时静静浮在海浪上,悬停或点击它也会游起来(彩蛋)**;**模型工作时开始游泳动画**(原地上下浮动 + 左右轻摆),并在**蓝色与原色之间呼吸变幻**——上扬到顶点时最蓝、落回时归黑,与游泳同周期,直到干完活。模型工作状态取自当前会话的 `running` 标志,并叠加输入机的 claimed/submitting 阶段,覆盖「按下回车到代理真正开跑」的间隙。鲸鱼几何为内置的 FishLogo 路径(与 DSH 主程序逐字节一致),挂载时仍会尝试从侧边栏实时徽标抓取最新版以跟随官方换标;动画纯 CSS、颜色走 DSH 主题变量(深浅色模式均正常),`prefers-reduced-motion` 时降级为静止。
43
+ - **任务提醒(可开关,默认关闭)**:挂着任务切去干别的时,让浏览器把你喊回来。监听**所有会话**(含后台),两类事件:**完成提醒**(`running` 落下,或宿主 `completed` 绿点升沿;并经 `turn/end` 事件的宿主投影区分**完成 / 被中断 / 请求失败**三种结局,失败通知附截断的错误摘要)与**交互提醒**(会话开始等待你的审批 / 计划确认 / 模型提问——与侧边栏黄点同一数据源 `pendingInteraction`)。三个独立通道:
44
44
  - **标题闪烁**:`(2) 🔔 原标题` 交替闪烁未读计数,回到页面自动恢复原标题并清零;
45
45
  - **系统通知**(Web Notifications API):桌面级通知,**点击直达对应会话**;权限申请挂在设置开关的点击手势上;系统响铃与自有提示音互斥,绝不双重响;
46
46
  - **提示音**:WebAudio 现场合成的双音动机——上行=完成、下行=需要你处理,无音频资源文件。
@@ -112,7 +112,7 @@ npx -y @deepseek-ai/dsh plugin --profile web add . # 从本目录作为 b
112
112
  - **归档管理**(服务端 `src/archive.ts` + 浏览器端 `src/client/archive.tsx`):作为 `settings.section` 插槽(设置面板中的「归档」页面)。列表数据直接来自框架标准 hook `useSessions` + `useWorkspaces`(`archivedSessionIds`),无需额外查询;操作走同源路由 `/_dsh/ui-tweaks/archive`。**恢复**把会话 id 从工作区存储域的 `archivedSessionIds` 全局单例中移除(DSH 只暴露单向 `archiveSession`,无公开的取消归档 API,故直接写活体存储域句柄并同步工作区注册表的内存缓存)。**彻底删除**依次:拒绝正在运行的会话(agent `status === 'running'` 才拒绝,空闲会话先 `cancel` + `whenIdle`)→ 用持久化后端自身的 `findLog` 定位并 `rm` 会话日志目录 → 调用公开的 `WorkspaceEntity.detachSession` 摘除工作区记账 → 从归档集合移除并同步注册表内存缓存与 header 索引 → 清理 `session_projcache` → 从内存 SessionStore 摘除该会话(触发 `host/session-removed` 实时消失)。
113
113
  - **MCP 管理**(服务端 `src/mcp.ts` + 浏览器端 `src/client/mcp.tsx`):同源路由 `/_dsh/ui-tweaks/mcp`。**列表**枚举 `ctx.loader.entries()` 中 `@deepseek-ai/dsh-mcp-client` 实例(id / config / fiber 状态:active=2、failed=3 等)并按 `mcp__<serverName>__` 前缀从工具注册表统计工具。**重启**调用 `entry.fiber.restart()`(仅运行时)。**添加 / 编辑 / 删除 / 启用停用**通过 `yaml`(eemeli)的 Document API 直接编辑 profile 的 `cordis.patch.yml`(保留注释与未知补丁结构,原子写 tmp+rename),随后由 DSH 内置的 `watchUserPatches` 热重载监视器重新应用补丁——`cordis-plugin-include` 对根组做**增量** `root.update`,因此只有被改动的 MCP 实例会重启,其它不受影响;环境变量值返回给同源浏览器(本机配置编辑需要),YAML 模式在服务端用 `yaml.parse` + 白名单校验。
114
114
  - **鲸鱼指示器**(`src/client/whale.tsx`):挂在 `conversation.input.dock`(输入框卡片上方的整行插槽)。dock 行本身比卡片宽(卡片被 InputBar root 以 `--dsh-composer-card-max-width` 封顶居中、两侧留 `--dsh-composer-side-clearance`),因此鲸鱼行**复刻卡片的几何**——同 max-width、同样居中、同样侧留白——再右对齐 + 6px `translateY`,鲸鱼恰好「骑」在卡片右上角(Claude Desktop 小螃蟹位置),自定义对话框宽度时也自动跟随。开关走与 /init 相同的「按需注册」编排——`whaleIndicatorEnabled` 打开时才注册插槽与样式,关闭即卸载。工作状态 = 会话列表 feed 中当前会话的 `running`(`useSyncExternalStore` 直订)∪ 输入机 `phase` 的 claimed/submitting;鲸鱼 SVG 路径内置为常量,挂载时以 `[class*="brandMark"]` 从侧边栏实时徽标尝试刷新(class 前缀是构建哈希,按后缀匹配以抗版本变化),取不到则回退内置几何。
115
- - **任务提醒**(`src/client/notifier.ts`):纯逻辑模块(无 React),在 `ctx.effect` 中直订 `ctx.sessions.list` 快照流(鲸鱼同款数据源),对全部非空、非子代理会话做前后对比:`running` true→false 且无挂起交互 → 完成事件;`pendingInteraction`('approval' / 'plan-review' / 'question',即侧边栏黄点分类)出现 → 交互事件;后台会话的宿主 `completed` 绿点升沿同样计为完成。三通道各自静默降级——标题闪烁用 `setInterval` 交替写 `document.title`,`focus`/`visibilitychange` 时恢复;系统通知带 `tag` 去重、`onclick` 里 `window.focus()` + `sessionsService.open(id)` 直达会话、`silent` 跟随提示音开关避免双重响铃;提示音由 WebAudio 振荡器现场合成双音包络。防打扰:首帧只武装基线、只在跳变沿触发、同会话同类 2s 冷却;「仅页面不可见时」等开关经 `readState` 每拍重读实时生效,改动无需重装监视器。
115
+ - **任务提醒**(`src/client/notifier.ts`):纯逻辑模块(无 React),在 `ctx.effect` 中直订 `ctx.sessions.list` 快照流(鲸鱼同款数据源),对全部非空、非子代理会话做前后对比:`running` true→false 且无挂起交互 → 完成事件;`pendingInteraction`('approval' / 'plan-review' / 'question',即侧边栏黄点分类)出现 → 交互事件;后台会话的宿主 `completed` 绿点升沿同样计为完成。结束原因来自服务端 [`dshTurnOutcome` 投影](D:/dsh-project/dsh-ui-tweaks/src/turn-outcome.ts)(last-wins fold 会话日志的 `turn/end.reason`)并随列表行 `projectionValues` 下发——completed/max-tokens 按「任务完成」、aborted/interrupted 按「任务已中断」、error 按「请求失败」(附截断错误消息)播报;投影缺失或未随本次跳变更新时退回普通完成播报。三通道各自静默降级——标题闪烁用 `setInterval` 交替写 `document.title`,`focus`/`visibilitychange` 时恢复;系统通知带 `tag` 去重、`onclick` 里 `window.focus()` + `sessionsService.open(id)` 直达会话、`silent` 跟随提示音开关避免双重响铃;提示音由 WebAudio 振荡器现场合成双音包络。防打扰:首帧只武装基线、只在跳变沿触发、同会话同类 2s 冷却;「仅页面不可见时」等开关经 `readState` 每拍重读实时生效,改动无需重装监视器。
116
116
  - **缓存命中率两位小数**(`src/client/cachehit.tsx`):挂在 `conversation.composer.dock`(输入框卡片下方承载原生统计条的横条)的**空渲染座位**——组件本身不画任何东西,只通过框架第五标准 hook `useProjection('tokenUsage')` 读取会话的 token 用量投影(未缓存输入 / 缓存读取 / 缓存写入 / 输出四个不相交桶),按 `缓存读取 ÷ (未缓存输入 + 缓存读取 + 缓存写入)` 算出精确占比后 `.toFixed(2)`,再把统计条里匹配「缓存命中 N%」/ "Cache hit N%" 的 span 原地改写为两位小数——布局、截断省略与 tooltip 行为全部保留 DSH 原样。一个 MutationObserver 监听统计条子树,React 重绘统计行时自动重打(写入幂等,改写一次后即收敛);开关关闭或会话切换时恢复原始文本并断开监听。注册编排与鲸鱼一致:`preciseCacheHitEnabled` 打开才挂载,关闭即卸载。
117
117
 
118
118
  ## 协议
package/cordis.patch.yml CHANGED
@@ -1,12 +1,12 @@
1
- # dsh-ui-tweaks bundle patch
2
- #
3
- # Declared by `dsh.bundle.patch` in package.json: when the plugin is installed
4
- # through the official CLI —
5
- #
6
- # dsh plugin --profile <name> add dsh-ui-tweaks
7
- #
8
- # — the command appends `dsh-ui-tweaks` to the profile's bundle stack and this
9
- # patch mounts the server-side plugin row.
10
- - insert:
11
- - id: ui-tweaks
12
- name: 'dsh-ui-tweaks'
1
+ # dsh-ui-tweaks bundle patch
2
+ #
3
+ # Declared by `dsh.bundle.patch` in package.json: when the plugin is installed
4
+ # through the official CLI —
5
+ #
6
+ # dsh plugin --profile <name> add dsh-ui-tweaks
7
+ #
8
+ # — the command appends `dsh-ui-tweaks` to the profile's bundle stack and this
9
+ # patch mounts the server-side plugin row.
10
+ - insert:
11
+ - id: ui-tweaks
12
+ name: 'dsh-ui-tweaks'
package/lib/client.js CHANGED
@@ -107,27 +107,27 @@ function relativeTime(ms, now, formatter) {
107
107
  return formatter.format(Math.round(diffSeconds / 86400), 'day');
108
108
  return formatter.format(Math.round(diffSeconds / 604800), 'week');
109
109
  }
110
- exports.ARCHIVE_CSS = `
111
- .dut-arc{display:grid;gap:12px;max-width:680px;padding:6px 2px 36px}
112
- .dut-arc-head{display:flex;align-items:center;gap:10px;padding:14px 16px 0}
113
- .dut-arc-head h2{margin:0;font-size:15px;font-weight:600;letter-spacing:-.01em;color:var(--dsw-alias-label-primary)}
114
- .dut-arc-count{font-size:11.5px;color:var(--dsw-alias-label-secondary);padding:2px 8px;border-radius:999px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1)}
115
- .dut-arc-spacer{flex:1}
116
- .dut-arc-btn{display:inline-flex;align-items:center;height:28px;padding:0 12px;border-radius:999px;border:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;cursor:pointer;transition:background .15s ease,color .15s ease,border-color .15s ease}
117
- .dut-arc-btn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
118
- .dut-arc-btn:disabled{opacity:.5;cursor:default}
119
- .dut-arc-btn.dut-arc-del:hover{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 55%,transparent);color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent)}
120
- .dut-arc-btn.dut-arc-confirm{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 55%,transparent);color:var(--dsw-alias-state-error-primary);font-weight:600}
121
- .dut-arc-body{display:grid;gap:2px;margin-top:4px}
122
- .dut-arc-row{display:flex;align-items:center;gap:8px;padding:10px 16px;border-radius:12px}
123
- .dut-arc-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
124
- .dut-arc-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;text-align:left;background:transparent;border:none;padding:0;cursor:default;color:var(--dsw-alias-label-primary);font:inherit}
125
- .dut-arc-title{font-size:13.5px;font-weight:600;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}
126
- .dut-arc-sub{font-size:11.5px;color:var(--dsw-alias-label-secondary);white-space:nowrap;text-overflow:ellipsis;overflow:hidden}
127
- .dut-arc-empty{padding:32px 16px;text-align:center;font-size:12.5px;color:var(--dsw-alias-label-secondary)}
128
- .dut-arc-alert{margin:8px 16px 0;padding:8px 12px;border-radius:10px;font-size:12px;line-height:1.5;background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent);color:var(--dsw-alias-state-error-primary)}
129
- .dut-arc-off{padding:20px 16px;display:grid;gap:12px}
130
- .dut-arc-off p{margin:0;font-size:12.5px;line-height:1.6;color:var(--dsw-alias-label-secondary)}
110
+ exports.ARCHIVE_CSS = `
111
+ .dut-arc{display:grid;gap:12px;max-width:680px;padding:6px 2px 36px}
112
+ .dut-arc-head{display:flex;align-items:center;gap:10px;padding:14px 16px 0}
113
+ .dut-arc-head h2{margin:0;font-size:15px;font-weight:600;letter-spacing:-.01em;color:var(--dsw-alias-label-primary)}
114
+ .dut-arc-count{font-size:11.5px;color:var(--dsw-alias-label-secondary);padding:2px 8px;border-radius:999px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1)}
115
+ .dut-arc-spacer{flex:1}
116
+ .dut-arc-btn{display:inline-flex;align-items:center;height:28px;padding:0 12px;border-radius:999px;border:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;cursor:pointer;transition:background .15s ease,color .15s ease,border-color .15s ease}
117
+ .dut-arc-btn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
118
+ .dut-arc-btn:disabled{opacity:.5;cursor:default}
119
+ .dut-arc-btn.dut-arc-del:hover{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 55%,transparent);color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent)}
120
+ .dut-arc-btn.dut-arc-confirm{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 55%,transparent);color:var(--dsw-alias-state-error-primary);font-weight:600}
121
+ .dut-arc-body{display:grid;gap:2px;margin-top:4px}
122
+ .dut-arc-row{display:flex;align-items:center;gap:8px;padding:10px 16px;border-radius:12px}
123
+ .dut-arc-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
124
+ .dut-arc-main{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;text-align:left;background:transparent;border:none;padding:0;cursor:default;color:var(--dsw-alias-label-primary);font:inherit}
125
+ .dut-arc-title{font-size:13.5px;font-weight:600;white-space:nowrap;text-overflow:ellipsis;overflow:hidden}
126
+ .dut-arc-sub{font-size:11.5px;color:var(--dsw-alias-label-secondary);white-space:nowrap;text-overflow:ellipsis;overflow:hidden}
127
+ .dut-arc-empty{padding:32px 16px;text-align:center;font-size:12.5px;color:var(--dsw-alias-label-secondary)}
128
+ .dut-arc-alert{margin:8px 16px 0;padding:8px 12px;border-radius:10px;font-size:12px;line-height:1.5;background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent);color:var(--dsw-alias-state-error-primary)}
129
+ .dut-arc-off{padding:20px 16px;display:grid;gap:12px}
130
+ .dut-arc-off p{margin:0;font-size:12.5px;line-height:1.6;color:var(--dsw-alias-label-secondary)}
131
131
  `;
132
132
  /** Install the Archive stylesheet once (idempotent); returns the disposer. */
133
133
  function installArchiveStyles() {
@@ -2233,7 +2233,7 @@ const en = {
2233
2233
  initCommandOn: 'On',
2234
2234
  initCommandOff: 'Off',
2235
2235
  whaleIndicator: 'Whale indicator',
2236
- whaleIndicatorHint: 'A little whale above the input box: translucent while idle, swimming while the model works.',
2236
+ whaleIndicatorHint: 'A little whale above the input box: translucent while idle; while the model works it swims and breathes blue.',
2237
2237
  whaleIndicatorOn: 'On',
2238
2238
  whaleIndicatorOff: 'Off',
2239
2239
  preciseCacheHit: 'Precise cache hit',
@@ -2246,7 +2246,7 @@ const en = {
2246
2246
  notifyOn: 'On',
2247
2247
  notifyOff: 'Off',
2248
2248
  notifyOnComplete: 'Alert on finish',
2249
- notifyOnCompleteHint: 'Fire when a session completes its turn — the one you watch or any running in the background.',
2249
+ notifyOnCompleteHint: 'Fire when a session ends its turn — completed, interrupted or failed; the notification says which.',
2250
2250
  notifyOnInteraction: 'Alert on interaction',
2251
2251
  notifyOnInteractionHint: 'Fire when a session waits on you: an approval, a plan review, or a question from the agent.',
2252
2252
  notifyOnlyWhenHidden: 'Only when hidden',
@@ -2259,8 +2259,12 @@ const en = {
2259
2259
  notifySoundHint: 'A soft two-note motif — rising when work finishes, falling when it needs you.',
2260
2260
  notifyTest: 'Test',
2261
2261
  notifyTitleDone: 'Task finished',
2262
+ notifyTitleAborted: 'Task interrupted',
2263
+ notifyTitleFailed: 'Request failed',
2262
2264
  notifyTitlePending: 'Needs you',
2263
2265
  bodyComplete: '✅ {title} finished its turn.',
2266
+ bodyAborted: '⏹ {title} was interrupted before finishing.',
2267
+ bodyFailed: '❌ {title} failed: {error}',
2264
2268
  bodyApproval: '⏸ {title} is waiting for your approval.',
2265
2269
  bodyPlan: '📋 {title} has a plan awaiting your review.',
2266
2270
  bodyQuestion: '❓ {title} asked you a question.',
@@ -2438,7 +2442,7 @@ const zh = {
2438
2442
  initCommandOn: '开启',
2439
2443
  initCommandOff: '关闭',
2440
2444
  whaleIndicator: '鲸鱼指示器',
2441
- whaleIndicatorHint: '输入框上方的小鲸鱼:空闲时半透明静止,模型工作时开始游泳动画。',
2445
+ whaleIndicatorHint: '输入框上方的小鲸鱼:空闲时半透明静止,模型工作时游动并在蓝黑之间呼吸变幻。',
2442
2446
  whaleIndicatorOn: '开启',
2443
2447
  whaleIndicatorOff: '关闭',
2444
2448
  preciseCacheHit: '缓存命中率两位小数',
@@ -2451,7 +2455,7 @@ const zh = {
2451
2455
  notifyOn: '开启',
2452
2456
  notifyOff: '关闭',
2453
2457
  notifyOnComplete: '完成提醒',
2454
- notifyOnCompleteHint: '会话结束一轮任务时提醒——无论你正看着它,还是它在后台运行。',
2458
+ notifyOnCompleteHint: '会话结束一轮任务时提醒——完成、被中断或出错都会通知,并注明是哪种情况。',
2455
2459
  notifyOnInteraction: '交互提醒',
2456
2460
  notifyOnInteractionHint: '会话等待你操作时提醒:审批、计划确认,或模型向你提问。',
2457
2461
  notifyOnlyWhenHidden: '仅页面不可见时',
@@ -2464,8 +2468,12 @@ const zh = {
2464
2468
  notifySoundHint: '轻柔的双音提示——上行表示完成,下行表示需要你处理。',
2465
2469
  notifyTest: '测试',
2466
2470
  notifyTitleDone: '任务完成',
2471
+ notifyTitleAborted: '任务已中断',
2472
+ notifyTitleFailed: '请求失败',
2467
2473
  notifyTitlePending: '需要你处理',
2468
2474
  bodyComplete: '✅ 「{title}」的任务已完成。',
2475
+ bodyAborted: '⏹ 「{title}」的任务被中断了。',
2476
+ bodyFailed: '❌ 「{title}」的任务出错了:{error}',
2469
2477
  bodyApproval: '⏸ 「{title}」正在等待你的审批。',
2470
2478
  bodyPlan: '📋 「{title}」有计划待确认。',
2471
2479
  bodyQuestion: '❓ 「{title}」向你提问了。',
@@ -3081,8 +3089,12 @@ function SettingsSection({ controller, t }) {
3081
3089
  sound: resolved.notifySound,
3082
3090
  }, {
3083
3091
  notifyTitleDone: t('notifyTitleDone'),
3092
+ notifyTitleAborted: t('notifyTitleAborted'),
3093
+ notifyTitleFailed: t('notifyTitleFailed'),
3084
3094
  notifyTitlePending: t('notifyTitlePending'),
3085
3095
  bodyComplete: t('bodyComplete'),
3096
+ bodyAborted: t('bodyAborted'),
3097
+ bodyFailed: t('bodyFailed'),
3086
3098
  bodyApproval: t('bodyApproval'),
3087
3099
  bodyPlan: t('bodyPlan'),
3088
3100
  bodyQuestion: t('bodyQuestion'),
@@ -3391,8 +3403,12 @@ function apply(ctx) {
3391
3403
  sessionsService: ctx.sessions,
3392
3404
  text: {
3393
3405
  notifyTitleDone: t('notifyTitleDone'),
3406
+ notifyTitleAborted: t('notifyTitleAborted'),
3407
+ notifyTitleFailed: t('notifyTitleFailed'),
3394
3408
  notifyTitlePending: t('notifyTitlePending'),
3395
3409
  bodyComplete: t('bodyComplete'),
3410
+ bodyAborted: t('bodyAborted'),
3411
+ bodyFailed: t('bodyFailed'),
3396
3412
  bodyApproval: t('bodyApproval'),
3397
3413
  bodyPlan: t('bodyPlan'),
3398
3414
  bodyQuestion: t('bodyQuestion'),
@@ -3495,63 +3511,63 @@ function linesToRecord(text, separator) {
3495
3511
  function recordToLines(record, separator) {
3496
3512
  return Object.entries(record).map(([key, value]) => `${key}${separator}${value}`).join('\n');
3497
3513
  }
3498
- exports.MCP_CSS = `
3499
- .dut-mcp{display:grid;gap:12px;max-width:680px;padding:6px 2px 36px}
3500
- .dut-mcp-head{display:flex;align-items:center;gap:10px;padding:14px 16px 0}
3501
- .dut-mcp-head h2{margin:0;font-size:15px;font-weight:600;letter-spacing:-.01em;color:var(--dsw-alias-label-primary)}
3502
- .dut-mcp-count{font-size:11.5px;color:var(--dsw-alias-label-secondary);padding:2px 8px;border-radius:999px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1)}
3503
- .dut-mcp-spacer{flex:1}
3504
- .dut-mcp-btn{display:inline-flex;align-items:center;height:28px;padding:0 12px;border-radius:999px;border:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;cursor:pointer;transition:background .15s ease,color .15s ease,border-color .15s ease}
3505
- .dut-mcp-btn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
3506
- .dut-mcp-btn:disabled{opacity:.5;cursor:default}
3507
- .dut-mcp-btn.dut-mcp-del:hover{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 55%,transparent);color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent)}
3508
- .dut-mcp-btn.dut-mcp-confirm{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 55%,transparent);color:var(--dsw-alias-state-error-primary);font-weight:600}
3509
- .dut-mcp-body{display:grid;gap:2px;margin-top:4px}
3510
- .dut-mcp-row{display:grid;gap:6px;padding:10px 16px;border-radius:12px}
3511
- .dut-mcp-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
3512
- .dut-mcp-row-main{display:flex;align-items:center;gap:8px;min-width:0}
3513
- .dut-mcp-name{font-size:13.5px;font-weight:600;color:var(--dsw-alias-label-primary);white-space:nowrap;text-overflow:ellipsis;overflow:hidden}
3514
- .dut-mcp-badge{flex:none;font-size:11px;line-height:1;padding:4px 9px;border-radius:999px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-secondary)}
3515
- .dut-mcp-badge.dut-mcp-ok{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 12%,transparent);border-color:transparent;color:var(--dsw-alias-state-success-primary)}
3516
- .dut-mcp-badge.dut-mcp-err{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent);border-color:transparent;color:var(--dsw-alias-state-error-primary)}
3517
- .dut-mcp-badge.dut-mcp-warn{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 14%,transparent);border-color:transparent;color:var(--dsw-alias-state-warn-label)}
3518
- .dut-mcp-sub{font-size:11.5px;color:var(--dsw-alias-label-secondary);white-space:nowrap;text-overflow:ellipsis;overflow:hidden}
3519
- .dut-mcp-toggle{flex:none;font-size:11.5px;color:var(--dsw-alias-label-tertiary);background:transparent;border:none;padding:0;cursor:pointer;text-align:left}
3520
- .dut-mcp-toggle:hover{color:var(--dsw-alias-label-primary)}
3521
- .dut-mcp-tools{margin-left:2px;display:flex;flex-wrap:wrap;gap:4px}
3522
- .dut-mcp-tool{font-size:11px;line-height:1;padding:3px 8px;border-radius:999px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-secondary)}
3523
- .dut-mcp-note{margin:2px 16px 0;padding:8px 12px;border-radius:10px;font-size:11.5px;line-height:1.55;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1)}
3524
- .dut-mcp-empty{padding:32px 16px;text-align:center;font-size:12.5px;color:var(--dsw-alias-label-secondary)}
3525
- .dut-mcp-alert{margin:8px 16px 0;padding:8px 12px;border-radius:10px;font-size:12px;line-height:1.5;background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent);color:var(--dsw-alias-state-error-primary)}
3526
- .dut-mcp-notice{margin:8px 16px 0;padding:8px 12px;border-radius:10px;font-size:12px;line-height:1.5;background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 12%,transparent);color:var(--dsw-alias-state-success-primary)}
3527
- .dut-mcp-off{padding:20px 16px;display:grid;gap:12px}
3528
- .dut-mcp-off p{margin:0;font-size:12.5px;line-height:1.6;color:var(--dsw-alias-label-secondary)}
3529
- .dut-mcp-overlay{z-index:1100;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}
3530
- .dut-mcp-mask{background:var(--dsw-alias-bg-mask-1);backdrop-filter:var(--dsw-mask-blur);position:absolute;inset:0}
3531
- .dut-mcp-panel{z-index:1;background:var(--dsw-alias-bg-layer-2);box-sizing:border-box;width:720px;max-width:calc(100vw - 48px);height:min(700px,100vh - 48px);box-shadow:var(--dsw-shadow-lv3);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border-radius:20px;display:flex;flex-direction:column;overflow:hidden}
3532
- .dut-mcp-panel-head{box-sizing:border-box;flex:none;display:flex;align-items:center;gap:10px;height:52px;padding:0 12px 0 20px}
3533
- .dut-mcp-panel-head h2{margin:0;font-size:16px;font-weight:600;letter-spacing:-.01em;color:var(--dsw-alias-label-primary)}
3534
- .dut-mcp-panel-close{cursor:pointer;width:28px;height:28px;color:var(--dsw-alias-label-primary);background:transparent;border:none;border-radius:28px;display:inline-flex;justify-content:center;align-items:center;padding:0;font-size:13px}
3535
- .dut-mcp-panel-close:hover{background:var(--dsw-alias-interactive-bg-hover)}
3536
- .dut-mcp-tabs{flex:none;display:inline-flex;padding:3px;gap:3px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-2);margin:0 20px 10px;align-self:flex-start}
3537
- .dut-mcp-tabs button{border:none;border-radius:7px;padding:5px 14px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12.5px;cursor:pointer}
3538
- .dut-mcp-tabs button.dut-mcp-tab-on{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 12%,transparent);color:var(--dsw-alias-state-business-primary);font-weight:600}
3539
- .dut-mcp-form{flex:1;min-height:0;overflow-y:auto;padding:0 20px 20px;display:grid;gap:12px;align-content:start}
3540
- .dut-mcp-field{display:grid;gap:6px}
3541
- .dut-mcp-field>span{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary)}
3542
- .dut-mcp-field small{font-size:11px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}
3543
- .dut-mcp-input{box-sizing:border-box;width:100%;height:34px;padding:0 10px;border-radius:9px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px}
3544
- .dut-mcp-input:focus{outline:none;border-color:color-mix(in srgb,var(--dsw-alias-state-business-primary) 55%,transparent)}
3545
- .dut-mcp-input:disabled{opacity:.55;cursor:not-allowed}
3546
- textarea.dut-mcp-input{height:auto;min-height:72px;padding:8px 10px;line-height:1.5;resize:vertical;font-family:var(--dsw-font-markdown-code-block-font-family, Consolas, monospace);font-size:12px}
3547
- textarea.dut-mcp-input-lg{min-height:120px}
3548
- textarea.dut-mcp-yaml{min-height:320px}
3549
- .dut-mcp-row2{display:grid;grid-template-columns:1fr 1fr;gap:12px;align-items:start}
3550
- .dut-mcp-field .dut-seg{width:fit-content}
3551
- .dut-mcp-row2 .dut-seg{height:34px;box-sizing:border-box;align-items:center}
3552
- .dut-mcp-row2 .dut-seg button{flex:none;white-space:nowrap;padding:0 16px;height:26px}
3553
- .dut-mcp-foot{flex:none;display:flex;align-items:center;gap:8px;justify-content:flex-end;padding:12px 20px 16px;border-top:1px solid var(--dsw-alias-border-l1)}
3554
- .dut-mcp-error{margin:0 20px 10px;padding:8px 12px;border-radius:10px;font-size:12px;line-height:1.5;background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent);color:var(--dsw-alias-state-error-primary)}
3514
+ exports.MCP_CSS = `
3515
+ .dut-mcp{display:grid;gap:12px;max-width:680px;padding:6px 2px 36px}
3516
+ .dut-mcp-head{display:flex;align-items:center;gap:10px;padding:14px 16px 0}
3517
+ .dut-mcp-head h2{margin:0;font-size:15px;font-weight:600;letter-spacing:-.01em;color:var(--dsw-alias-label-primary)}
3518
+ .dut-mcp-count{font-size:11.5px;color:var(--dsw-alias-label-secondary);padding:2px 8px;border-radius:999px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1)}
3519
+ .dut-mcp-spacer{flex:1}
3520
+ .dut-mcp-btn{display:inline-flex;align-items:center;height:28px;padding:0 12px;border-radius:999px;border:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;cursor:pointer;transition:background .15s ease,color .15s ease,border-color .15s ease}
3521
+ .dut-mcp-btn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
3522
+ .dut-mcp-btn:disabled{opacity:.5;cursor:default}
3523
+ .dut-mcp-btn.dut-mcp-del:hover{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 55%,transparent);color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent)}
3524
+ .dut-mcp-btn.dut-mcp-confirm{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 55%,transparent);color:var(--dsw-alias-state-error-primary);font-weight:600}
3525
+ .dut-mcp-body{display:grid;gap:2px;margin-top:4px}
3526
+ .dut-mcp-row{display:grid;gap:6px;padding:10px 16px;border-radius:12px}
3527
+ .dut-mcp-row:hover{background:var(--dsw-alias-interactive-bg-hover)}
3528
+ .dut-mcp-row-main{display:flex;align-items:center;gap:8px;min-width:0}
3529
+ .dut-mcp-name{font-size:13.5px;font-weight:600;color:var(--dsw-alias-label-primary);white-space:nowrap;text-overflow:ellipsis;overflow:hidden}
3530
+ .dut-mcp-badge{flex:none;font-size:11px;line-height:1;padding:4px 9px;border-radius:999px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-secondary)}
3531
+ .dut-mcp-badge.dut-mcp-ok{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 12%,transparent);border-color:transparent;color:var(--dsw-alias-state-success-primary)}
3532
+ .dut-mcp-badge.dut-mcp-err{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent);border-color:transparent;color:var(--dsw-alias-state-error-primary)}
3533
+ .dut-mcp-badge.dut-mcp-warn{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 14%,transparent);border-color:transparent;color:var(--dsw-alias-state-warn-label)}
3534
+ .dut-mcp-sub{font-size:11.5px;color:var(--dsw-alias-label-secondary);white-space:nowrap;text-overflow:ellipsis;overflow:hidden}
3535
+ .dut-mcp-toggle{flex:none;font-size:11.5px;color:var(--dsw-alias-label-tertiary);background:transparent;border:none;padding:0;cursor:pointer;text-align:left}
3536
+ .dut-mcp-toggle:hover{color:var(--dsw-alias-label-primary)}
3537
+ .dut-mcp-tools{margin-left:2px;display:flex;flex-wrap:wrap;gap:4px}
3538
+ .dut-mcp-tool{font-size:11px;line-height:1;padding:3px 8px;border-radius:999px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-secondary)}
3539
+ .dut-mcp-note{margin:2px 16px 0;padding:8px 12px;border-radius:10px;font-size:11.5px;line-height:1.55;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1)}
3540
+ .dut-mcp-empty{padding:32px 16px;text-align:center;font-size:12.5px;color:var(--dsw-alias-label-secondary)}
3541
+ .dut-mcp-alert{margin:8px 16px 0;padding:8px 12px;border-radius:10px;font-size:12px;line-height:1.5;background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent);color:var(--dsw-alias-state-error-primary)}
3542
+ .dut-mcp-notice{margin:8px 16px 0;padding:8px 12px;border-radius:10px;font-size:12px;line-height:1.5;background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 12%,transparent);color:var(--dsw-alias-state-success-primary)}
3543
+ .dut-mcp-off{padding:20px 16px;display:grid;gap:12px}
3544
+ .dut-mcp-off p{margin:0;font-size:12.5px;line-height:1.6;color:var(--dsw-alias-label-secondary)}
3545
+ .dut-mcp-overlay{z-index:1100;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}
3546
+ .dut-mcp-mask{background:var(--dsw-alias-bg-mask-1);backdrop-filter:var(--dsw-mask-blur);position:absolute;inset:0}
3547
+ .dut-mcp-panel{z-index:1;background:var(--dsw-alias-bg-layer-2);box-sizing:border-box;width:720px;max-width:calc(100vw - 48px);height:min(700px,100vh - 48px);box-shadow:var(--dsw-shadow-lv3);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border-radius:20px;display:flex;flex-direction:column;overflow:hidden}
3548
+ .dut-mcp-panel-head{box-sizing:border-box;flex:none;display:flex;align-items:center;gap:10px;height:52px;padding:0 12px 0 20px}
3549
+ .dut-mcp-panel-head h2{margin:0;font-size:16px;font-weight:600;letter-spacing:-.01em;color:var(--dsw-alias-label-primary)}
3550
+ .dut-mcp-panel-close{cursor:pointer;width:28px;height:28px;color:var(--dsw-alias-label-primary);background:transparent;border:none;border-radius:28px;display:inline-flex;justify-content:center;align-items:center;padding:0;font-size:13px}
3551
+ .dut-mcp-panel-close:hover{background:var(--dsw-alias-interactive-bg-hover)}
3552
+ .dut-mcp-tabs{flex:none;display:inline-flex;padding:3px;gap:3px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-2);margin:0 20px 10px;align-self:flex-start}
3553
+ .dut-mcp-tabs button{border:none;border-radius:7px;padding:5px 14px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12.5px;cursor:pointer}
3554
+ .dut-mcp-tabs button.dut-mcp-tab-on{background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 12%,transparent);color:var(--dsw-alias-state-business-primary);font-weight:600}
3555
+ .dut-mcp-form{flex:1;min-height:0;overflow-y:auto;padding:0 20px 20px;display:grid;gap:12px;align-content:start}
3556
+ .dut-mcp-field{display:grid;gap:6px}
3557
+ .dut-mcp-field>span{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary)}
3558
+ .dut-mcp-field small{font-size:11px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}
3559
+ .dut-mcp-input{box-sizing:border-box;width:100%;height:34px;padding:0 10px;border-radius:9px;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px}
3560
+ .dut-mcp-input:focus{outline:none;border-color:color-mix(in srgb,var(--dsw-alias-state-business-primary) 55%,transparent)}
3561
+ .dut-mcp-input:disabled{opacity:.55;cursor:not-allowed}
3562
+ textarea.dut-mcp-input{height:auto;min-height:72px;padding:8px 10px;line-height:1.5;resize:vertical;font-family:var(--dsw-font-markdown-code-block-font-family, Consolas, monospace);font-size:12px}
3563
+ textarea.dut-mcp-input-lg{min-height:120px}
3564
+ textarea.dut-mcp-yaml{min-height:320px}
3565
+ .dut-mcp-row2{display:grid;grid-template-columns:1fr 1fr;gap:12px;align-items:start}
3566
+ .dut-mcp-field .dut-seg{width:fit-content}
3567
+ .dut-mcp-row2 .dut-seg{height:34px;box-sizing:border-box;align-items:center}
3568
+ .dut-mcp-row2 .dut-seg button{flex:none;white-space:nowrap;padding:0 16px;height:26px}
3569
+ .dut-mcp-foot{flex:none;display:flex;align-items:center;gap:8px;justify-content:flex-end;padding:12px 20px 16px;border-top:1px solid var(--dsw-alias-border-l1)}
3570
+ .dut-mcp-error{margin:0 20px 10px;padding:8px 12px;border-radius:10px;font-size:12px;line-height:1.5;background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent);color:var(--dsw-alias-state-error-primary)}
3555
3571
  `;
3556
3572
  /** Install the MCP stylesheet once (idempotent); returns the disposer. */
3557
3573
  function installMcpStyles() {
@@ -3792,7 +3808,11 @@ __modules["./notifier.js"] = function(module, exports, require, __load_) {
3792
3808
  *
3793
3809
  * - "Finished" = a session's `running` flag drops (true → false) without an
3794
3810
  * interaction taking over, or its host-tracked `completed` reminder rises
3795
- * (finished while not selected).
3811
+ * (finished while not selected). The host `dshTurnOutcome` session
3812
+ * projection (`src/turn-outcome.ts`, folded from `turn/end` reasons) says
3813
+ * WHY it finished, so the copy distinguishes a clean completion from a
3814
+ * user abort and from a failed request (with the error text); when the
3815
+ * projection is missing or stale, falls back to a plain-finish announce.
3796
3816
  * - "Interaction" = a session gains `pendingInteraction`
3797
3817
  * ('approval' | 'plan-review' | 'question') — exactly the sidebar's
3798
3818
  * amber-dot classification.
@@ -3823,6 +3843,25 @@ exports.requestNotifyPermission = requestNotifyPermission;
3823
3843
  exports.previewTitleFlash = previewTitleFlash;
3824
3844
  exports.previewAlerts = previewAlerts;
3825
3845
  exports.installTaskNotifier = installTaskNotifier;
3846
+ /** List-row key carrying the turn outcome; keep in sync with src/turn-outcome.ts. */
3847
+ const TURN_OUTCOME_KEY = 'dshTurnOutcome';
3848
+ /** Terminal-turn announcement kinds — they all answer "your task ended". */
3849
+ const TERMINAL_KINDS = ['complete', 'aborted', 'failed'];
3850
+ /**
3851
+ * Map a logged `turn/end` reason onto an announcement kind. Unknown or absent
3852
+ * outcomes (older host, projection lag) degrade to a plain completion.
3853
+ */
3854
+ const classifyFinish = (outcome) => {
3855
+ switch (outcome?.kind) {
3856
+ case 'aborted':
3857
+ case 'interrupted':
3858
+ return 'aborted';
3859
+ case 'error':
3860
+ return 'failed';
3861
+ default:
3862
+ return 'complete';
3863
+ }
3864
+ };
3826
3865
  /** Same event kind for one session never repeats inside this window. */
3827
3866
  const COOLDOWN_MS = 2000;
3828
3867
  /**
@@ -3833,9 +3872,9 @@ const COOLDOWN_MS = 2000;
3833
3872
  const COMPLETE_RENOTIFY_MS = 30000;
3834
3873
  /** Title-flash blink period. */
3835
3874
  const FLASH_INTERVAL_MS = 900;
3836
- /** Replace the `{title}` placeholder (split/join — no regex escaping worries). */
3837
- function fill(template, title) {
3838
- return template.split('{title}').join(title);
3875
+ /** Replace the `{title}` / `{error}` placeholders (split/join — no regex escaping worries). */
3876
+ function fill(template, title, error) {
3877
+ return template.split('{error}').join(error ?? '').split('{title}').join(title);
3839
3878
  }
3840
3879
  /**
3841
3880
  * Ask the browser for desktop-notification permission. Must run inside a user
@@ -3902,7 +3941,7 @@ function previewAlerts(channels, text) {
3902
3941
  if (Notification.permission !== 'granted')
3903
3942
  return;
3904
3943
  try {
3905
- const sample = fill(text.bodyComplete, 'dsh-ui-tweaks');
3944
+ const sample = fill(text.bodyComplete, 'dsh-ui-tweaks', undefined);
3906
3945
  new Notification(text.notifyTitleDone, { body: sample, tag: 'dsh-ui-tweaks:preview', silent: true });
3907
3946
  }
3908
3947
  catch {
@@ -4029,7 +4068,7 @@ function installTaskNotifier(input) {
4029
4068
  }
4030
4069
  };
4031
4070
  // --- delivery ------------------------------------------------------------
4032
- const deliver = (sessionId, displayTitle, kind, pending, channels, now) => {
4071
+ const deliver = (sessionId, displayTitle, kind, pending, detail, channels, now) => {
4033
4072
  const cooldownKey = `${sessionId}:${kind}`;
4034
4073
  const last = lastFired.get(cooldownKey);
4035
4074
  if (last !== undefined && now - last < COOLDOWN_MS)
@@ -4040,8 +4079,18 @@ function installTaskNotifier(input) {
4040
4079
  startTitleFlash();
4041
4080
  if (!channels.systemNotification && !channels.sound)
4042
4081
  return;
4043
- const heading = kind === 'complete' ? text.notifyTitleDone : text.notifyTitlePending;
4044
- let body = text.bodyComplete;
4082
+ const heading = kind === 'complete'
4083
+ ? text.notifyTitleDone
4084
+ : kind === 'aborted'
4085
+ ? text.notifyTitleAborted
4086
+ : kind === 'failed'
4087
+ ? text.notifyTitleFailed
4088
+ : text.notifyTitlePending;
4089
+ let body = kind === 'aborted'
4090
+ ? text.bodyAborted
4091
+ : kind === 'failed'
4092
+ ? text.bodyFailed
4093
+ : text.bodyComplete;
4045
4094
  if (kind === 'interaction') {
4046
4095
  body = pending === 'approval'
4047
4096
  ? text.bodyApproval
@@ -4049,7 +4098,7 @@ function installTaskNotifier(input) {
4049
4098
  ? text.bodyPlan
4050
4099
  : text.bodyQuestion;
4051
4100
  }
4052
- fireSystemNotification(kind, sessionId, heading, fill(body, displayTitle), channels.sound);
4101
+ fireSystemNotification(kind, sessionId, heading, fill(body, displayTitle, detail), channels.sound);
4053
4102
  if (channels.sound)
4054
4103
  playChime(kind === 'complete' ? 'up' : 'down');
4055
4104
  };
@@ -4060,7 +4109,8 @@ function installTaskNotifier(input) {
4060
4109
  for (const row of Object.values(list.getSnapshot().byId)) {
4061
4110
  if (row.parentId !== undefined || row.blank)
4062
4111
  continue;
4063
- watch.set(row.id, { running: row.running, pending: row.pendingInteraction, completed: row.completed === true });
4112
+ const outcome = row.projectionValues?.[TURN_OUTCOME_KEY] ?? undefined;
4113
+ watch.set(row.id, { running: row.running, pending: row.pendingInteraction, completed: row.completed === true, outcome });
4064
4114
  }
4065
4115
  };
4066
4116
  // --- feed tick -----------------------------------------------------------
@@ -4075,14 +4125,15 @@ function installTaskNotifier(input) {
4075
4125
  continue;
4076
4126
  seen.add(row.id);
4077
4127
  const prev = watch.get(row.id);
4078
- watch.set(row.id, { running: row.running, pending: row.pendingInteraction, completed: row.completed === true });
4128
+ const outcome = row.projectionValues?.[TURN_OUTCOME_KEY] ?? undefined;
4129
+ watch.set(row.id, { running: row.running, pending: row.pendingInteraction, completed: row.completed === true, outcome });
4079
4130
  if (prev === undefined)
4080
4131
  continue;
4081
4132
  // Interaction outranks completion: a turn ending ON a question must
4082
4133
  // announce the question, not a completion.
4083
4134
  if (prev.pending === undefined && row.pendingInteraction !== undefined) {
4084
4135
  if (options.onInteraction && deliverable) {
4085
- deliver(row.id, row.displayTitle, 'interaction', row.pendingInteraction, channels, now);
4136
+ deliver(row.id, row.displayTitle, 'interaction', row.pendingInteraction, undefined, channels, now);
4086
4137
  }
4087
4138
  continue;
4088
4139
  }
@@ -4094,11 +4145,21 @@ function installTaskNotifier(input) {
4094
4145
  // re-notify window it is a duplicate, not a new completion.
4095
4146
  let shouldFire = finished;
4096
4147
  if (!shouldFire) {
4097
- const lastComplete = lastFired.get(`${row.id}:complete`);
4098
- shouldFire = lastComplete === undefined || now - lastComplete >= COMPLETE_RENOTIFY_MS;
4148
+ // Any recent terminal announcement (of any outcome kind) makes the
4149
+ // green-dot reminder a duplicate of the same finish.
4150
+ shouldFire = TERMINAL_KINDS.every((kind) => {
4151
+ const last = lastFired.get(`${row.id}:${kind}`);
4152
+ return last === undefined || now - last >= COMPLETE_RENOTIFY_MS;
4153
+ });
4099
4154
  }
4100
4155
  if (shouldFire && options.onComplete && deliverable) {
4101
- deliver(row.id, row.displayTitle, 'complete', undefined, channels, now);
4156
+ // Trust the outcome only when it CHANGED on this very transition —
4157
+ // a stale value from an earlier turn would mislabel the announce.
4158
+ const fresh = prev.outcome === undefined
4159
+ || outcome?.time !== prev.outcome.time
4160
+ || outcome?.kind !== prev.outcome.kind;
4161
+ const announcement = fresh && outcome !== undefined ? classifyFinish(outcome) : 'complete';
4162
+ deliver(row.id, row.displayTitle, announcement, undefined, outcome?.errorMessage, channels, now);
4102
4163
  }
4103
4164
  }
4104
4165
  }
@@ -4603,8 +4664,8 @@ const jsx_runtime_1 = require("react/jsx-runtime");
4603
4664
  * card's top-right corner over drifting blue waves — Claude Desktop's little
4604
4665
  * crab, but a whale — resident the whole time the feature is on. Idle: the
4605
4666
  * whale keeps its original colour and floats still on the waves; hovering or
4606
- * clicking it swims it too (easter egg). Working: it swims until the model
4607
- * finishes.
4667
+ * clicking it swims it too (easter egg). Working: it swims and breathes
4668
+ * between its original colour and the waves' blue until the model finishes.
4608
4669
  *
4609
4670
  * - Mounted in `conversation.input.dock` (the full-width row stacked by the
4610
4671
  * composer column right above the card), registered only while the
@@ -4641,28 +4702,33 @@ const WHALE_VIEW_H = 17.04;
4641
4702
  // ---------------------------------------------------------------------------
4642
4703
  // Styles — resident (translucent, still) vs working (opaque, swimming).
4643
4704
  // ---------------------------------------------------------------------------
4644
- exports.WHALE_CSS = `
4645
- .duwi-row{box-sizing:border-box;width:100%;max-width:calc(var(--dsh-composer-card-max-width) + 2 * var(--dsh-composer-side-clearance, 0px));margin:0 auto;padding:0 calc(var(--dsh-composer-side-clearance, 0px) + 14px) 0 var(--dsh-composer-side-clearance, 0px);display:flex;align-items:flex-end;justify-content:flex-end;height:27px;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
4646
- .duwi-stack{display:flex;flex-direction:column;align-items:center;transform:translateY(6px)}
4647
- .duwi-whale{display:block;color:var(--dsw-alias-label-primary);transform-origin:50% 60%;pointer-events:auto;cursor:pointer}
4648
- .duwi-row.duwi-swim .duwi-whale,.duwi-whale:hover{animation:duwi-swim 1.9s ease-in-out infinite}
4649
- .duwi-waves{display:block;width:26px;height:9px;margin-top:-3px;overflow:hidden}
4650
- .duwi-wave-back{stroke:#3d8bfd;opacity:.35}
4651
- .duwi-wave-front{stroke:#3d8bfd;opacity:.8}
4652
- .duwi-wave-front,.duwi-wave-back{animation:duwi-drift-front 1.9s linear infinite}
4653
- .duwi-wave-back{animation-duration:3.4s;animation-direction:reverse}
4705
+ exports.WHALE_CSS = `
4706
+ .duwi-row{box-sizing:border-box;width:100%;max-width:calc(var(--dsh-composer-card-max-width) + 2 * var(--dsh-composer-side-clearance, 0px));margin:0 auto;padding:0 calc(var(--dsh-composer-side-clearance, 0px) + 14px) 0 var(--dsh-composer-side-clearance, 0px);display:flex;align-items:flex-end;justify-content:flex-end;height:27px;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
4707
+ .duwi-stack{display:flex;flex-direction:column;align-items:center;transform:translateY(6px)}
4708
+ .duwi-whale{display:block;color:var(--dsw-alias-label-primary);transform-origin:50% 60%;pointer-events:auto;cursor:pointer}
4709
+ .duwi-row.duwi-swim .duwi-whale{animation:duwi-swim 1.9s ease-in-out infinite,duwi-breathe 1.9s ease-in-out infinite}
4710
+ .duwi-whale:hover{animation:duwi-swim 1.9s ease-in-out infinite}
4711
+ .duwi-waves{display:block;width:26px;height:9px;margin-top:-3px;overflow:hidden}
4712
+ .duwi-wave-back{stroke:#3d8bfd;opacity:.35}
4713
+ .duwi-wave-front{stroke:#3d8bfd;opacity:.8}
4714
+ .duwi-wave-front,.duwi-wave-back{animation:duwi-drift-front 1.9s linear infinite}
4715
+ .duwi-wave-back{animation-duration:3.4s;animation-direction:reverse}
4654
4716
  @keyframes duwi-swim{
4655
4717
  0%,100%{transform:translateY(0) rotate(-4deg)}
4656
4718
  50%{transform:translateY(-3.5px) rotate(4deg)}
4657
4719
  }
4658
- @keyframes duwi-drift-front{
4659
- from{transform:translateX(0)}
4660
- to{transform:translateX(-8px)}
4661
- }
4662
- @media (prefers-reduced-motion:reduce){
4663
- .duwi-row.duwi-swim .duwi-whale,.duwi-whale:hover{animation:none}
4664
- .duwi-wave-front,.duwi-wave-back{animation:none}
4720
+ @keyframes duwi-breathe{
4721
+ 0%,100%{color:var(--dsw-alias-label-primary)}
4722
+ 50%{color:#3d8bfd}
4665
4723
  }
4724
+ @keyframes duwi-drift-front{
4725
+ from{transform:translateX(0)}
4726
+ to{transform:translateX(-8px)}
4727
+ }
4728
+ @media (prefers-reduced-motion:reduce){
4729
+ .duwi-row.duwi-swim .duwi-whale,.duwi-whale:hover{animation:none}
4730
+ .duwi-wave-front,.duwi-wave-back{animation:none}
4731
+ }
4666
4732
  `;
4667
4733
  /** Idempotent installer for the indicator styles (same pattern as the other features). */
4668
4734
  function installWhaleStyles() {