dsh-prompt-enhance 0.2.7 → 0.3.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/README.md +3 -3
- package/README.zh-CN.md +3 -3
- package/cordis.patch.yml +3 -2
- package/docs/release-0.2.5.md +28 -0
- package/docs/release-0.2.7.md +22 -0
- package/docs/release-0.3.0.md +36 -0
- package/docs/release-0.3.1.md +50 -0
- package/lib/client.js +199 -28
- package/lib/index.js +213 -79
- package/lib/index.js.map +3 -3
- package/package.json +14 -15
- package/src/client/EnhanceButton.tsx +18 -11
- package/src/client/ResultPanel.tsx +11 -2
- package/src/client/enhance-client.ts +31 -5
- package/src/client/index.tsx +78 -30
- package/src/client/locales.ts +4 -2
- package/src/client/settings-source.ts +269 -0
- package/src/client/ui-state.ts +9 -0
- package/src/config.ts +40 -19
- package/src/enhance-command.ts +13 -8
- package/src/enhance-routes.ts +137 -33
- package/src/enhancer.ts +19 -1
- package/src/http.ts +22 -0
- package/src/index.ts +45 -62
- package/src/settings-source.ts +221 -0
- package/src/shared/protocol.ts +18 -0
package/README.md
CHANGED
|
@@ -47,14 +47,14 @@ flowchart LR
|
|
|
47
47
|
|
|
48
48
|
The plugin is one npm package with two halves, following the dsh plugin conventions:
|
|
49
49
|
|
|
50
|
-
- **Host half** (`exports "."`, Node):
|
|
50
|
+
- **Host half** (`exports "."`, Node): declares the `prompt-enhance` settings schema (schemastery, every field marked `.volatile()` — the host's `describe()` **drops any entry with no volatile field**, so that mark is both what makes the config page appear and the contract for "edits apply to the next request"; the plugin **re-reads it on every request** instead of caching the boot snapshot), registers the `POST /prompt-enhance/enhance` route on the shared webserver (loopback-fenced, body-capped), and the `/enhance` slash command. The model call goes through `ctx.llm.stream` with a normalized output pass — the same auxiliary-call discipline the harness applies to session titles: deadline + caller cancellation rechecked during and after the stream, terminal-finish validation, tool-call rejection.
|
|
51
51
|
- **Browser half** (`exports "./client"`): registers the enhance button into the `conversation.input.right` slot and the undo bar into `conversation.input.dock`, binds a live mirror of the settings namespace, and installs the global shortcut. All copy is localized (zh/en) through the harness locale system.
|
|
52
52
|
|
|
53
53
|
## Requirements
|
|
54
54
|
|
|
55
55
|
- `dsh >= 0.1.1-rc.2 < 0.3.0` — the range states what has actually been exercised, not an open-ended promise.
|
|
56
|
-
- Boot-verified on `0.1.1-rc.2`, `0.1.2-alpha.3`, `0.1.7-rc.2`,
|
|
57
|
-
- The ceiling covers the whole `0.2.x` line (including its prereleases); a `0.3.x` is untested and therefore outside the range. `dsh.engines` is
|
|
56
|
+
- Boot-verified on `0.1.1-rc.2`, `0.1.2-alpha.3`, `0.1.7-rc.2`, `0.2.0-rc.2`, and **`0.2.1-rc.2`** (layer mounts, enhance route answers, client bundle builds; on alpha the plugin takes the newer `ctx.settings.installSection` registration path, and on `0.2.0-rc.2` it also passes the runtime's plugin-compatibility gate). The `0.2.1-rc.2` check is a **real-host end-to-end smoke**: with the local build installed into a temporary profile, the enhance route returned a real model result; setting that profile's `maxInputChars` to `300` made a 400-character draft fail with `422 {count: 400, max: 300}`, and after editing the same profile to `20000` — **without restarting** — the identical draft went through, so a settings edit reaches the *next* request instead of staying at the boot-time snapshot. The `0.2.0-rc.2` checks include the **dsh desktop application** — the environment this plugin is normally used in. On a `0.2.x` runtime expect some *other* plugins to disappear from the UI: the gate skips any bundle whose `peerDependencies` pin an older `@deepseek-ai/dsh-*` range (observed: `dshmarket`, `dsh-find-plugin`, `dsh-better-sidebar`). The harness has broken several of the APIs this plugin touches: `@deepseek-ai/dsh-settings` changed its registration entry between the two early lines, from `0.1.5-rc.3` the settings service became `SettingsForms` (no synchronous `get`), and `0.2.x` **deleted the `installSettingsSection` / `settingsNamespace` exports outright**; `dsh-agent-default-model` moved the harness default model behind its own `ctx.agentDefaultModel.currentSelection()` service; and the browser-side settings mirror moved from `ctx.settingsScope` to `ctx.configForms`. The plugin probes every generation at runtime and adapts, no configuration needed.
|
|
57
|
+
- The ceiling covers the whole `0.2.x` line (including its prereleases); a `0.3.x` is untested and therefore outside the range. `dsh.engines` **is enforced**: the host's boot preflight marks a mismatched entry `disabled = true`, and only an explicit `--accept-risk` gets past the throw (`Incompatible plugins may cause crashes or data loss`). A wrong range means the plugin does not load — not that it loads with a warning.
|
|
58
58
|
- Node `^22.19.0 || >=24.0.0` (for building from source)
|
|
59
59
|
- Browser peers `react` / `react-dom` (`^18.2.0`) come from the web shell's module registry. The host-half module `@deepseek-ai/dsh-llm` is a **runtime module supplied by the dsh host and is not installed from npm** (the `lib/` bundles keep every package import external), so it is declared as an *optional* peer: an install that does not provide it produces no warning, and the plugin reports a normal error at enhance time rather than failing to load.
|
|
60
60
|
|
package/README.zh-CN.md
CHANGED
|
@@ -47,14 +47,14 @@ flowchart LR
|
|
|
47
47
|
|
|
48
48
|
插件是单个 npm 包、双半区结构,完全遵循 dsh 插件规范:
|
|
49
49
|
|
|
50
|
-
- **宿主半区**(`exports "."`,Node)
|
|
50
|
+
- **宿主半区**(`exports "."`,Node):声明 `prompt-enhance` 设置 schema(schemastery,每个字段带 `.volatile()`——宿主的 `describe()` 会**跳过没有任何 volatile 字段的条目**,这个标记既是"设置页会出现"的前提,也是"改完下次请求就生效"的契约;插件在**每次请求时**重新读取,而不是缓存启动快照),注册共享 webserver 上的 `POST /prompt-enhance/enhance` 路由(仅回环、限长),以及 `/enhance` 斜杠命令。模型调用走 `ctx.llm.stream` 并做规范化处理——与 harness 对会话标题相同的辅助调用纪律:超时与调用方取消在流过程中和结束后复查、终结 finish 校验、拒绝工具调用。
|
|
51
51
|
- **浏览器半区**(`exports "./client"`):把增强按钮注册进 `conversation.input.right` 插槽、撤销条注册进 `conversation.input.dock` 插槽,绑定设置命名空间的实时镜像,并安装全局快捷键。全部文案经 harness locale 系统提供中英双语。
|
|
52
52
|
|
|
53
53
|
## 环境要求
|
|
54
54
|
|
|
55
55
|
- `dsh >= 0.1.1-rc.2 < 0.3.0` —— 这个区间表示**实际验证过的范围**,而不是无限期的承诺。
|
|
56
|
-
- 实测环境:`0.1.1-rc.2`、`0.1.2-alpha.3`、`0.1.7-rc.2`、`0.2.0-rc.2
|
|
57
|
-
- 上界覆盖整条 `0.2.x` 线(含其预发布版本);`0.3.x` 尚未验证,因此不在区间内。`dsh.engines`
|
|
56
|
+
- 实测环境:`0.1.1-rc.2`、`0.1.2-alpha.3`、`0.1.7-rc.2`、`0.2.0-rc.2`、**`0.2.1-rc.2`** 均已 boot 验证(插件层挂载、增强路由应答、客户端 bundle 构建通过;alpha 上走新版 `ctx.settings.installSection` 注册路径,`0.2.0-rc.2` 上还通过了运行时的插件兼容性闸门)。`0.2.1-rc.2` 的验证是**真实宿主的端到端冒烟**:在临时 profile 里装入本地构建后,增强路由对着在线模型返回了真实结果;把该 profile 的 `maxInputChars` 设为 `300` 后,400 字草稿被 `422 {count:400,max:300}` 拒绝,随后**不重启**把它改成 `20000`,同一份草稿立即通过——设置改动确实作用到下一次请求,而不是停在启动时的快照。其中 `0.2.0-rc.2` 的验证包含 **dsh 桌面应用**——也就是本插件日常实际运行的环境。在 `0.2.x` 运行时上,可能会发现**别的**插件从界面消失:凡 `peerDependencies` 把 `@deepseek-ai/dsh-*` 锁在旧区间的 bundle 都会被闸门跳过(实测被跳过的是 `dshmarket`、`dsh-find-plugin`、`dsh-better-sidebar`)。宿主在插件用到的几处 API 上做过破坏性变更:`@deepseek-ai/dsh-settings` 的注册入口在两条早期版本线之间变过,`0.1.5-rc.3` 起 settings 服务改为 `SettingsForms`(不再有同步 `get`),而 `0.2.x` 干脆**删掉了 `installSettingsSection` / `settingsNamespace` 导出**;`dsh-agent-default-model` 把全局默认模型收进了自己的 `ctx.agentDefaultModel.currentSelection()` 服务;浏览器侧的设置镜像则从 `ctx.settingsScope` 迁到了 `ctx.configForms`。插件对每一代形态均做运行时探测并自动适配,无需配置。
|
|
57
|
+
- 上界覆盖整条 `0.2.x` 线(含其预发布版本);`0.3.x` 尚未验证,因此不在区间内。`dsh.engines` **是强制的**:宿主启动预检会把不匹配的条目直接 `disabled = true`,要放行必须显式 `--accept-risk`,否则抛 `Incompatible plugins may cause crashes or data loss`。所以这个区间写错的结果是**插件不加载**,而不是"只提示一下"。
|
|
58
58
|
- Node `^22.19.0 || >=24.0.0`(仅从源码构建时需要)
|
|
59
59
|
- 浏览器侧 peer `react` / `react-dom`(`^18.2.0`)由 Web 壳的模块注册表提供。宿主半区用到的 `@deepseek-ai/dsh-llm` 是**由 dsh 宿主提供的运行时模块,不从 npm 安装**(`lib/` 产物把包引用全部保持为外部),因此声明为**可选** peer:未提供它的环境不会出现安装警告,增强时按正常错误路径报错,而不是加载失败。
|
|
60
60
|
|
package/cordis.patch.yml
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
# `dsh plugin --profile web add link:<repo>/dsh-prompt-enhance`.
|
|
5
5
|
#
|
|
6
6
|
# One package, both halves: the node half (exports ".") runs in the host
|
|
7
|
-
# process and registers the POST /prompt-enhance/enhance route
|
|
8
|
-
# /enhance slash command, and the "prompt-enhance" settings
|
|
7
|
+
# process and registers the POST /prompt-enhance/enhance route and the
|
|
8
|
+
# /enhance slash command, and declares the "prompt-enhance" settings schema
|
|
9
|
+
# this entry's config page renders (read live, per request); the
|
|
9
10
|
# browser half (declared by `dsh.client`, served from lib/client.js) adds
|
|
10
11
|
# the composer enhance button (conversation.input.right), the preview
|
|
11
12
|
# panel, and the undo bar (conversation.input.dock).
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
## Two metadata corrections
|
|
2
|
+
|
|
3
|
+
No runtime behavior changes. Both corrections remove claims the project could not back.
|
|
4
|
+
|
|
5
|
+
### `dsh.engines.dsh` is now bounded: `>=0.1.1-rc.2 <0.2.0`
|
|
6
|
+
|
|
7
|
+
It previously read `>=0.1.1-rc.2` — an open-ended claim that *any* future harness version is supported. Boot verification at the time covered `0.1.1-rc.2`, `0.1.2-alpha.3`, and `0.1.7-rc.2`, while the current npm line was already `0.2.0-rc.2`, so the open ceiling was over-promising.
|
|
8
|
+
|
|
9
|
+
`<0.2.0` rather than `<0.1.8` is deliberate: per semver, a prerelease only matches a range that names a prerelease, so `<0.1.8` would have **excluded the verified `0.1.7-rc.2`** — the exact version the range has to keep. `<0.2.0` keeps every `0.1.x` and excludes the then-unverified `0.2.0-rc.1` / `0.2.0-rc.2`.
|
|
10
|
+
|
|
11
|
+
> Superseded by **0.2.6**, which widened the ceiling to `<0.3.0` after `0.2.0-rc.2` was verified on the dsh desktop application. This release is the honest intermediate step, not the final state.
|
|
12
|
+
|
|
13
|
+
### `dsh.client.inject` no longer lists `@deepseek-ai/dsh-client-ui-settings`
|
|
14
|
+
|
|
15
|
+
That entry dated from the layout in which the browser half hard-declared the settings surface — the declaration that produced, on a profile without that package:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
web boot: 1 entry did not activate
|
|
19
|
+
dsh-prompt-enhance: pending (waiting for service: settingsScope)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
…and took the whole GUI down with it (fixed in 0.2.3 by reaching every optional service through `ctx.inject`). With that fix in place the module no longer belongs in the required-provider list.
|
|
23
|
+
|
|
24
|
+
### Verification
|
|
25
|
+
|
|
26
|
+
- `npm run typecheck`, `npm run build` clean.
|
|
27
|
+
- **207 tests across 20 files** pass.
|
|
28
|
+
- Metadata-only: `lib/` is byte-identical to 0.2.4, which is why the tarball contents are unchanged apart from `package.json`.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
## Version bump carrying the desktop-app verification record
|
|
2
|
+
|
|
3
|
+
No runtime behavior changes. This version exists because a **docs-only** change followed 0.2.6: the source had already been verified on the current dsh line, but the record of *where* it was verified needed correcting.
|
|
4
|
+
|
|
5
|
+
### What this version carries
|
|
6
|
+
|
|
7
|
+
The `0.2.6` release notes described the `0.2.0-rc.2` verification as having happened on the global CLI. That was incomplete: the environment the plugin is **actually used in day to day** is the dsh **desktop application**, and its `FileVersion` is `0.2.0-rc.2` too. The record now names both hosts, and the READMEs say so as well.
|
|
8
|
+
|
|
9
|
+
This matters for anyone reading the support claim: "verified on `0.2.0-rc.2`" should not rest on a CLI smoke test when the desktop app is the real target. Both were checked, and the evidence is the same three signals:
|
|
10
|
+
|
|
11
|
+
1. **The runtime's plugin-compatibility gate accepts this plugin.** `0.2.0-rc.2` refuses bundles whose `peerDependencies` pin an older `@deepseek-ai/dsh-*` range. This plugin passes, thanks to the optional `@deepseek-ai/dsh-llm` peer (`>=0.1.1-rc.2`) introduced in 0.2.4. The gate skipped `dshmarket@1.45.0`, `dsh-find-plugin@0.4.0`, and `dsh-better-sidebar@0.15.2` on the same host.
|
|
12
|
+
2. **No `pending (waiting for service: …)` and no `1 entry did not activate`.**
|
|
13
|
+
3. **A real enhancement completed end to end**: `[prompt-enhance] … in=2 out=64 ctx=0 1928ms ok`.
|
|
14
|
+
|
|
15
|
+
### Documentation-only
|
|
16
|
+
|
|
17
|
+
`src/` and `lib/` are unchanged from 0.2.6; the shipped bundles are identical. The declared range is `>=0.1.1-rc.2 <0.3.0`, boot-verified on `0.1.1-rc.2`, `0.1.2-alpha.3`, `0.1.7-rc.2`, and `0.2.0-rc.2` (desktop app and CLI).
|
|
18
|
+
|
|
19
|
+
### Verification
|
|
20
|
+
|
|
21
|
+
- `npm run typecheck`, `npm run build` clean.
|
|
22
|
+
- **207 tests across 20 files** pass.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
## Adapting to the `0.2.1-rc.2` settings contract
|
|
2
|
+
|
|
3
|
+
No new features. This release exists because the harness moved the two services that carry the plugin's settings, and the plugin's probes for them **failed silently**: the settings page kept rendering, edits kept landing on disk, and nothing ever reached a request or the composer. Five defects are fixed; three of them predate the update but only became reachable on the current line.
|
|
4
|
+
|
|
5
|
+
### What was silently broken
|
|
6
|
+
|
|
7
|
+
| Surface | What `0.2.x` did | What the user saw | Fix |
|
|
8
|
+
|---|---|---|---|
|
|
9
|
+
| Host: the section | Deleted the `installSettingsSection` / `settingsNamespace` exports outright; `SettingsForms.describe()` replaced them | Settings edits applied on disk, never to the next enhance | Read the row through `describe()` **per request** |
|
|
10
|
+
| Browser: the mirror | `ctx.settingsScope` no longer exists | `enabled` / `maxInputChars` / `shortcut` / `streaming` stuck at the bundled defaults | `configForms.get(entryId)` first, then the aggregate binder, then the legacy scope |
|
|
11
|
+
| Browser: the attachment guard | `InputState.imageIds` renamed `attachmentIds` | The "attachments only" guard never fired | Read both names |
|
|
12
|
+
|
|
13
|
+
The host fix is verified end to end, not just in unit tests: on a real `dsh web` at `0.2.1-rc.2`, a profile with `maxInputChars: 300` refused a 400-character draft with `422 {count: 400, max: 300}`, and editing the same profile to `20000` — **without restarting** — let the identical draft through.
|
|
14
|
+
|
|
15
|
+
### Two defects in the new browser mirror
|
|
16
|
+
|
|
17
|
+
Found on a second pass over the code this release introduces.
|
|
18
|
+
|
|
19
|
+
1. **It disposed an object it does not own.** `configForms.get()` does not create a form per caller: the settings provider caches one controller per entry and hands the same instance to the settings page, and `dispose()` permanently stops that controller deriving — the provider keeps the dead instance in its cache. Tearing it down on plugin unload would have frozen this entry's settings page for the rest of the session. The mirror now releases only its own subscription.
|
|
20
|
+
2. **It resolved the entry id once.** With the describe mirror not yet loaded, the namespace list was empty, so the plugin bound its configured candidate — which on an aggregate bundle is not the real row, and a namespace that does not exist answers `unavailable` forever. Resolution now re-runs on every describe tick and rebinds when the served id changes, which also covers a rename the first pass could not see.
|
|
21
|
+
|
|
22
|
+
### Three behaviour fixes
|
|
23
|
+
|
|
24
|
+
- **Retry storms.** The rate window counts successful calls only, by design — a run of timeouts must not lock the user out when the model recovers. That left deterministic failures (wrong key, mistyped provider) bounded by nothing but the concurrency cap. Three consecutive failures now trip a 30-second cool-down answering `429 {code: 'cooldown', retryAfterSeconds, failures}` before a body is read or the model is called; a success clears the streak.
|
|
25
|
+
- **`413` killed the socket.** The over-length answer used `res.socket.destroy()`, which also tore down anything else riding the same keep-alive connection. It now answers `connection: close` and lets Node discard the unread body.
|
|
26
|
+
- **A rebuilt webserver left the plugin routeless.** Registration was guarded by a bare "already mounted on this context" flag, so a host that replaced its `webServer` service kept the button on screen while every click 404'd. The guard now compares service identity and re-mounts.
|
|
27
|
+
|
|
28
|
+
### Migration
|
|
29
|
+
|
|
30
|
+
Nothing to do. The package is a drop-in replacement for `0.2.7`; the declared range is unchanged at `>=0.1.1-rc.2 <0.3.0`, and both older settings APIs are still probed, so a `0.1.x` host keeps working.
|
|
31
|
+
|
|
32
|
+
### Verification
|
|
33
|
+
|
|
34
|
+
- `npm run typecheck` and `npm run build` clean against `@deepseek-ai/dsh-*@0.2.1-rc.2`; a clean rebuild of `lib/` is byte-identical.
|
|
35
|
+
- **231 tests across 22 files** pass, including two new files (`tests/settings-source.test.ts`, `tests/client-settings-source.test.ts`) that pin the probe order, the rebind-on-settle path, and the "never dispose the provider's form" contract.
|
|
36
|
+
- Real-host smoke on `dsh web` at `0.2.1-rc.2`: no `pending` / `did not activate`, the enhance route answered with a real model result (`deepseek-official` / `deepseek-flash`), and the settings schema appeared in the config page with Chinese descriptions.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# 0.3.1 — 设置页真的会出现,失败流真的会关
|
|
2
|
+
|
|
3
|
+
0.3.0 把插件搬到了 `0.2.1-rc.2` 的契约上,但有两条最关键的路径**在真实宿主上从未生效**:设置页不存在,失败流不结束。两条都属于"看着在、其实不在",所以这次每条都配了一个**在旧代码上必然失败**的测试。
|
|
4
|
+
|
|
5
|
+
## 两个致命项
|
|
6
|
+
|
|
7
|
+
| 面 | 0.2.x 的真实行为 | 用户看到什么 | 修法 |
|
|
8
|
+
|---|---|---|---|
|
|
9
|
+
| **设置** | `SettingsForms.describe()` 用内部 `volatileForm()` 过滤每个条目:**没有任何字段带 `meta.volatile` 时整条被丢弃** | 设置页里没有本插件;即使写入也会抛 `Plugin entry "prompt-enhance" has no volatile fields` | 16 个字段全部加 `.volatile()`;schema 库从裸 `schemastery` 换成 `@deepseek-ai/schemastery`(只有它带 `.volatile()` 与配套的位置校验) |
|
|
10
|
+
| **流式失败** | 错误分支写了 `event: error` 帧,**没有 `res.end()`** | 浏览器因为收到帧而正常显示错误;`curl` / `response.text()` / 代理**永久挂起** | 成功与失败共用 `endResponse()` 收尾 |
|
|
11
|
+
|
|
12
|
+
复现与验证(不是推断):
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
宿主 volatileForm(Config) → 修复前: undefined(条目被跳过)
|
|
16
|
+
→ 修复后: present, 16 个字段
|
|
17
|
+
真宿主 dsh web @0.2.1-rc.2 → 插件激活、POST /prompt-enhance/enhance
|
|
18
|
+
返回 deepseek-official / deepseek-flash 的真实结果
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
失败流那条的证伪:把 `endResponse(res)` 注释掉,新用例在 2.07 秒被 abort 判失败——正是"永久挂起"。
|
|
22
|
+
|
|
23
|
+
## 生命周期与失败路径
|
|
24
|
+
|
|
25
|
+
- **路由不随卸载回收**:`webServer.register` 只返回"从表里删项"的函数,disposer 却存在模块级 map 里、无人调用 ⇒ 卸载后路由仍用旧配置与旧限流窗答卷,重载撞 `duplicate prefix route`。改为 `ctx.inject(['webServer'])` + `ctx.effect`,服务按身份跟随、随光纤回收。
|
|
26
|
+
- **`/enhance` 在无 webServer 的组合里整体消失**:`inject` 里硬写了 `['llm','webServer']`。`webServer` 移出硬依赖、改按需注入后,斜杠命令在纯 CLI 组合里依然可用。
|
|
27
|
+
- **`/enhance` 遇配置异常直接抛**:`readConfig()` 在 `try` 之外,provider/model 半填时抛出的是未捕获异常,而不是路由侧那句中英双语提示。已移入 `try`。
|
|
28
|
+
- **流式端点把"服务端拒绝"当成"宿主不支持流式"**:`response.ok === false` 时丢弃整个错误信封并**原样重发一次**——429 反而打两次、结构化原因(`limit`/`retryAfterSeconds`)全丢。现在先按 `ok` 分流:拒绝即报拒绝,只有"200 但不是流"才回退一次性。
|
|
29
|
+
- **面板打开时快捷键打到别的会话**:结果面板 `createPortal` 挂在 `body` 上并接管焦点,上溯循环走到 `body` 时 `body.contains(每个 composer)` 恒真 ⇒ 返回注册表里的第一个会话。现在遇 `body`/`documentElement` 即停,退回"最近挂载的会话"。证伪:去掉这行判断,用例立刻变成"点了另一个会话"。
|
|
30
|
+
- **设置镜像可能永久停在默认值**:`boundId` 在 `forms.get()` **之前**落值,取不到时后续每次 describe tick 都因 `entryId === boundId` 直接返回 ⇒ 永不重试。改为取到 form 之后才认领 id。
|
|
31
|
+
|
|
32
|
+
## 可诊断性与小项
|
|
33
|
+
|
|
34
|
+
- 客户端三处空 `catch` 改为 `console.warn`(模块注释早就承诺 "caught and logged",代码里一行都没有)——"按钮不出现"这类报告唯一的线索来源。
|
|
35
|
+
- 对话框 Tab 焦点陷阱的**反向**漏:面板根节点是刻意 `tabIndex={-1}`,既不是 first 也不是 last,`Shift+Tab` 能直接逃出。已把根节点视为"first 之前"。
|
|
36
|
+
- 关闭插件后不再抢键:此前仍 `preventDefault` 后跑一个空动作,等于静默吃掉用户绑给同一组合的宿主快捷键。
|
|
37
|
+
- `scripts/build.mjs` 列在 `tsconfig.include` 里但 `allowJs` 没开 ⇒ 从未被检查。现在开 `checkJs`,并给 esbuild 选项补 JSDoc(否则 `packages: 'external'` 会退化成 `string`)。
|
|
38
|
+
- README 不再称 `dsh.engines` 为"声明性、不强制":宿主启动预检会直接 `disabled = true`,放行必须显式 `--accept-risk`。
|
|
39
|
+
|
|
40
|
+
## 验证
|
|
41
|
+
|
|
42
|
+
- `npm run typecheck` 干净(`build.mjs` 现在确实在检查范围内,`tsc --listFilesOnly` 命中 1)
|
|
43
|
+
- `npm test` **24 文件 / 242 用例全绿**(0.3.0 为 22 / 231,新增 `client-enhance-client`、`client-shortcut-target` 及配置/路由/镜像若干条)
|
|
44
|
+
- 真实宿主冒烟:临时 profile 装本地包 → 插件激活、无 `pending` / `did not activate` → 增强路由返回真实模型结果 → 临时 profile 与临时目录已删除
|
|
45
|
+
|
|
46
|
+
## 未做(有意)
|
|
47
|
+
|
|
48
|
+
- **快捷键没有迁移到宿主的 `shortcuts` 服务**(`dsh-client-shortcuts` 支持用户改键与冲突消解)。迁移需要先确认该服务的注册契约与冲突语义,属于独立改动,不在这次"失败路径闭包"的范围内。
|
|
49
|
+
- **`Origin` 只比对 hostname、不比对端口**:任意 `localhost:任意端口` 的页面都被信任。收紧需要把服务实际监听的端口传进栅栏,同样属独立改动。
|
|
50
|
+
- 跨代探测的兼容死分支(`installSection`、旧 settings 模块、`webUiSettings`、`settingsScope`)保留:它们在旧版本线上仍是唯一的路径。
|
package/lib/client.js
CHANGED
|
@@ -65,6 +65,11 @@ function checkInputText(text, maxChars) {
|
|
|
65
65
|
// src/shared/protocol.ts
|
|
66
66
|
var ENHANCE_ENDPOINT = "/prompt-enhance/enhance";
|
|
67
67
|
var ENHANCE_STREAM_ENDPOINT = "/prompt-enhance/enhance-stream";
|
|
68
|
+
var PROMPT_ENHANCE_ROW_IDS = [
|
|
69
|
+
"prompt-enhance",
|
|
70
|
+
"dsh-prompt-enhance",
|
|
71
|
+
"web-ui-prompt-enhance"
|
|
72
|
+
];
|
|
68
73
|
|
|
69
74
|
// src/client/enhance-client.ts
|
|
70
75
|
var EnhanceClientError = class extends Error {
|
|
@@ -80,6 +85,7 @@ var KNOWN_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
|
80
85
|
"rejected",
|
|
81
86
|
"rate-limit",
|
|
82
87
|
"concurrency-limit",
|
|
88
|
+
"cooldown",
|
|
83
89
|
"timeout",
|
|
84
90
|
"upstream",
|
|
85
91
|
"unconfigured",
|
|
@@ -118,6 +124,16 @@ async function readEnvelope(response) {
|
|
|
118
124
|
}
|
|
119
125
|
throw new EnhanceClientError({ code: "internal", message: `\u5BBF\u4E3B\u670D\u52A1\u8FD4\u56DE\u5F02\u5E38\uFF08HTTP ${response.status}\uFF09\u3002` });
|
|
120
126
|
}
|
|
127
|
+
async function rejectionOf(response) {
|
|
128
|
+
try {
|
|
129
|
+
const parsed = await response.json();
|
|
130
|
+
if (parsed !== null && typeof parsed === "object" && parsed.error !== void 0) {
|
|
131
|
+
return new EnhanceClientError(parseError(parsed.error));
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
}
|
|
135
|
+
return new EnhanceClientError({ code: "internal", message: `\u5BBF\u4E3B\u670D\u52A1\u62D2\u7EDD\u4E86\u8BF7\u6C42\uFF08HTTP ${response.status}\uFF09\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002` });
|
|
136
|
+
}
|
|
121
137
|
function takeFrames(buffer) {
|
|
122
138
|
const frames = [];
|
|
123
139
|
let rest = buffer;
|
|
@@ -164,9 +180,9 @@ async function requestEnhanceStream(body, options) {
|
|
|
164
180
|
throw new EnhanceClientError({ code: "internal", message: "\u65E0\u6CD5\u8FDE\u63A5\u5BBF\u4E3B\u670D\u52A1\uFF0C\u8BF7\u786E\u8BA4 dsh web \u6B63\u5728\u8FD0\u884C\u540E\u91CD\u8BD5\u3002" });
|
|
165
181
|
}
|
|
166
182
|
const contentType = String(response.headers.get("content-type") ?? "").toLowerCase();
|
|
183
|
+
if (!response.ok) throw await rejectionOf(response);
|
|
167
184
|
const stream = response.body;
|
|
168
|
-
|
|
169
|
-
if (stream === null || stream === void 0 || !streamable) {
|
|
185
|
+
if (stream === null || stream === void 0 || !contentType.includes("event-stream")) {
|
|
170
186
|
void Promise.resolve().then(() => stream?.cancel()).catch(() => {
|
|
171
187
|
});
|
|
172
188
|
return requestEnhance(body, options.signal);
|
|
@@ -243,6 +259,8 @@ function localizedErrorMessage(t, code, params) {
|
|
|
243
259
|
return t("error.rateLimit", params);
|
|
244
260
|
case "concurrency-limit":
|
|
245
261
|
return t("error.concurrencyLimit", params);
|
|
262
|
+
case "cooldown":
|
|
263
|
+
return t("error.cooldown", params);
|
|
246
264
|
case "timeout":
|
|
247
265
|
return t("error.timeout", params);
|
|
248
266
|
case "unconfigured":
|
|
@@ -287,10 +305,12 @@ function ResultPanel(props) {
|
|
|
287
305
|
const first = focusables[0];
|
|
288
306
|
const last = focusables[focusables.length - 1];
|
|
289
307
|
if (first === void 0 || last === void 0) return;
|
|
290
|
-
|
|
308
|
+
const active = document.activeElement;
|
|
309
|
+
const atRoot = active === panel;
|
|
310
|
+
if (event.shiftKey && (active === first || atRoot)) {
|
|
291
311
|
event.preventDefault();
|
|
292
312
|
last.focus();
|
|
293
|
-
} else if (!event.shiftKey &&
|
|
313
|
+
} else if (!event.shiftKey && active === last) {
|
|
294
314
|
event.preventDefault();
|
|
295
315
|
first.focus();
|
|
296
316
|
}
|
|
@@ -554,6 +574,7 @@ function shortcutTarget() {
|
|
|
554
574
|
if (active !== null && active instanceof Element) {
|
|
555
575
|
let node = active;
|
|
556
576
|
for (let depth = 0; depth < 8 && node !== null; depth++) {
|
|
577
|
+
if (node === document.body || node === document.documentElement) break;
|
|
557
578
|
for (const entry of sessions.values()) {
|
|
558
579
|
if (entry.root !== null && (node === entry.root || node.contains(entry.root))) {
|
|
559
580
|
const found = entry;
|
|
@@ -637,7 +658,10 @@ function EnhanceButton(props) {
|
|
|
637
658
|
const draft = useInput((state) => stringOf(state.draft));
|
|
638
659
|
const phase = useInput((state) => state.phase);
|
|
639
660
|
const occurrenceCount = useInput((state) => countOf(state.occurrences));
|
|
640
|
-
const
|
|
661
|
+
const attachmentCount = useInput((state) => {
|
|
662
|
+
const record = state;
|
|
663
|
+
return countOf(record.attachmentIds) + countOf(record.imageIds);
|
|
664
|
+
});
|
|
641
665
|
const settings = (0, import_react3.useSyncExternalStore)(subscribeClientSettings, getClientSettings);
|
|
642
666
|
const panel = (0, import_react3.useSyncExternalStore)(subscribe, getPanel);
|
|
643
667
|
const rootRef = (0, import_react3.useRef)(null);
|
|
@@ -651,9 +675,9 @@ function EnhanceButton(props) {
|
|
|
651
675
|
openError(uiKey, draft, { code: "rejected", localized: disabled });
|
|
652
676
|
return;
|
|
653
677
|
}
|
|
654
|
-
if (
|
|
655
|
-
const
|
|
656
|
-
openError(uiKey, draft, { code: "rejected", localized:
|
|
678
|
+
if (attachmentCount > 0 && draft.trim() === "") {
|
|
679
|
+
const attachmentsOnly = t("error.attachmentsOnly");
|
|
680
|
+
openError(uiKey, draft, { code: "rejected", localized: attachmentsOnly });
|
|
657
681
|
return;
|
|
658
682
|
}
|
|
659
683
|
const check = checkInputText(draft, settings.maxInputChars);
|
|
@@ -687,7 +711,7 @@ function EnhanceButton(props) {
|
|
|
687
711
|
return;
|
|
688
712
|
}
|
|
689
713
|
requestEnhance({ sessionId: wireId, text: draft }, controller.signal).then(settle, fail);
|
|
690
|
-
}, [anyBusy, draft,
|
|
714
|
+
}, [anyBusy, draft, attachmentCount, occurrenceCount, phase, settings, uiKey, wireId, t]);
|
|
691
715
|
const runRef = (0, import_react3.useRef)(start);
|
|
692
716
|
(0, import_react3.useEffect)(() => {
|
|
693
717
|
runRef.current = start;
|
|
@@ -799,7 +823,7 @@ var zh = {
|
|
|
799
823
|
"undo.dismiss": "\u5173\u95ED\u63D0\u793A",
|
|
800
824
|
"error.empty": "\u8F93\u5165\u6846\u4E3A\u7A7A\uFF0C\u8BF7\u5148\u8F93\u5165\u8981\u589E\u5F3A\u7684\u63D0\u793A\u8BCD\u3002",
|
|
801
825
|
"error.tooLong": "\u5185\u5BB9\u5171 {count} \u4E2A\u5B57\u7B26\uFF0C\u8D85\u8FC7 {max} \u4E2A\u5B57\u7B26\u4E0A\u9650\uFF08\u6309 Unicode \u5B57\u7B26\u6570\u7EDF\u8BA1\uFF0C\u4E0D\u662F token \u6570\uFF09\u3002\u4E3A\u907F\u514D\u6539\u53D8\u539F\u610F\u4E0D\u4F1A\u81EA\u52A8\u622A\u65AD\uFF0C\u8BF7\u7CBE\u7B80\u540E\u518D\u8BD5\u3002",
|
|
802
|
-
"error.
|
|
826
|
+
"error.attachmentsOnly": "\u8F93\u5165\u6846\u53EA\u9644\u52A0\u4E86\u9644\u4EF6\uFF08\u56FE\u7247\u6216\u6587\u4EF6\uFF09\uFF0C\u8BF7\u8865\u5145\u8981\u589E\u5F3A\u7684\u6587\u672C\u3002",
|
|
803
827
|
"error.occurrences": "\u8F93\u5165\u5185\u5BB9\u5305\u542B\u547D\u4EE4\u6216\u6587\u4EF6\u5F15\u7528\uFF0C\u6682\u4E0D\u652F\u6301\u589E\u5F3A\uFF1B\u8BF7\u5148\u79FB\u9664\u540E\u518D\u8BD5\u3002",
|
|
804
828
|
"error.phase": "\u5F53\u524D\u8F93\u5165\u6B63\u88AB\u5360\u7528\uFF08\u63D0\u4EA4\u4E2D\uFF09\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002",
|
|
805
829
|
"error.disabled": "\u63D0\u793A\u8BCD\u589E\u5F3A\u5DF2\u5728\u63D2\u4EF6\u8BBE\u7F6E\u4E2D\u5173\u95ED\u3002",
|
|
@@ -810,6 +834,7 @@ var zh = {
|
|
|
810
834
|
"error.internal": "\u589E\u5F3A\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5\uFF1B\u539F\u8F93\u5165\u672A\u6539\u52A8\u3002",
|
|
811
835
|
"error.rateLimit": "\u8BF7\u6C42\u8FC7\u4E8E\u9891\u7E41\uFF1A\u6BCF\u5206\u949F\u6700\u591A {limit} \u6B21\u589E\u5F3A\uFF0C\u8BF7\u5728 {retryAfterSeconds} \u79D2\u540E\u518D\u8BD5\u3002",
|
|
812
836
|
"error.concurrencyLimit": "\u5DF2\u6709 {max} \u4E2A\u589E\u5F3A\u6B63\u5728\u8FDB\u884C\uFF0C\u8BF7\u7B49\u5176\u4E2D\u4E00\u4E2A\u5B8C\u6210\u540E\u518D\u8BD5\u3002",
|
|
837
|
+
"error.cooldown": "\u8FDE\u7EED {failures} \u6B21\u589E\u5F3A\u5931\u8D25\uFF0C\u5DF2\u6682\u505C\u8C03\u7528\u4EE5\u514D\u6301\u7EED\u51B2\u51FB\u6A21\u578B\u670D\u52A1\uFF1B\u8BF7 {retryAfterSeconds} \u79D2\u540E\u91CD\u8BD5\uFF0C\u6216\u5148\u68C0\u67E5\u8BBE\u7F6E\u91CC\u7684\u6A21\u578B\u914D\u7F6E\u3002",
|
|
813
838
|
"error.upstream.auth": "\u9274\u6743\u5931\u8D25\uFF1A\u8BF7\u68C0\u67E5\u8BE5 provider \u7684 API Key \u914D\u7F6E\u3002",
|
|
814
839
|
"error.upstream.invalidCredential": "\u9274\u6743\u5931\u8D25\uFF1A\u5B58\u50A8\u7684 API Key \u4E0D\u53EF\u7528\uFF0C\u8BF7\u4FEE\u6B63\u540E\u91CD\u8BD5\u3002",
|
|
815
840
|
"error.upstream.rateLimit": "\u6A21\u578B\u670D\u52A1\u9650\u6D41\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5\u3002",
|
|
@@ -844,7 +869,7 @@ var en = {
|
|
|
844
869
|
"undo.dismiss": "Dismiss",
|
|
845
870
|
"error.empty": "The input box is empty \u2014 type a prompt to enhance first.",
|
|
846
871
|
"error.tooLong": "The draft is {count} characters, above the {max} cap (Unicode characters, not tokens). It is never auto-truncated (that would change your meaning) \u2014 please shorten it.",
|
|
847
|
-
"error.
|
|
872
|
+
"error.attachmentsOnly": "The composer only has attachments; add the text you want enhanced.",
|
|
848
873
|
"error.occurrences": "The draft contains commands or file references, which are not supported yet \u2014 remove them first.",
|
|
849
874
|
"error.phase": "The input box is busy (submitting) \u2014 try again in a moment.",
|
|
850
875
|
"error.disabled": "Prompt enhance is disabled in the plugin settings.",
|
|
@@ -855,6 +880,7 @@ var en = {
|
|
|
855
880
|
"error.internal": "Enhancement failed. Retry; your draft is untouched.",
|
|
856
881
|
"error.rateLimit": "Too many requests \u2014 at most {limit} enhancements per minute; retry in {retryAfterSeconds} seconds.",
|
|
857
882
|
"error.concurrencyLimit": "{max} enhancements are already running \u2014 wait for one to finish, then retry.",
|
|
883
|
+
"error.cooldown": "{failures} enhancements failed in a row; calls are paused to stop hammering the model service. Retry in {retryAfterSeconds}s, or check the model settings first.",
|
|
858
884
|
"error.upstream.auth": "Authentication failed \u2014 check the API key configured for this provider.",
|
|
859
885
|
"error.upstream.invalidCredential": "Authentication failed \u2014 the stored API key is invalid; fix it and retry.",
|
|
860
886
|
"error.upstream.rateLimit": "The model provider is rate-limiting; retry later.",
|
|
@@ -1108,61 +1134,206 @@ function matchesShortcut(event, combo) {
|
|
|
1108
1134
|
return event.ctrlKey === combo.ctrl && event.altKey === combo.alt && event.shiftKey === combo.shift && event.metaKey === combo.meta && (event.code === combo.code || event.key.toLowerCase() === combo.key);
|
|
1109
1135
|
}
|
|
1110
1136
|
|
|
1137
|
+
// src/client/settings-source.ts
|
|
1138
|
+
var RANK = {
|
|
1139
|
+
none: 0,
|
|
1140
|
+
legacyScope: 1,
|
|
1141
|
+
binder: 2,
|
|
1142
|
+
configForms: 3
|
|
1143
|
+
};
|
|
1144
|
+
function sectionOf(snapshot) {
|
|
1145
|
+
if (snapshot === null || typeof snapshot !== "object") return void 0;
|
|
1146
|
+
const record = snapshot;
|
|
1147
|
+
if (record.status === "unavailable") return void 0;
|
|
1148
|
+
return record.value;
|
|
1149
|
+
}
|
|
1150
|
+
function servedNamespaces(forms) {
|
|
1151
|
+
try {
|
|
1152
|
+
const namespaces = forms.describe?.()?.getSnapshot?.()?.view?.namespaces;
|
|
1153
|
+
return Array.isArray(namespaces) ? namespaces.map((entry) => String(entry?.ns ?? "")) : [];
|
|
1154
|
+
} catch {
|
|
1155
|
+
return [];
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
function installClientSettingsSource(ctx, options) {
|
|
1159
|
+
let rank = RANK.none;
|
|
1160
|
+
const adopt = (next, section) => {
|
|
1161
|
+
if (section === void 0 || next < rank) return;
|
|
1162
|
+
rank = next;
|
|
1163
|
+
options.onSection(section);
|
|
1164
|
+
};
|
|
1165
|
+
const adoptSnapshot = (next, form) => {
|
|
1166
|
+
try {
|
|
1167
|
+
const section = sectionOf(form.getSnapshot?.());
|
|
1168
|
+
if (section !== void 0) adopt(next, section);
|
|
1169
|
+
} catch {
|
|
1170
|
+
}
|
|
1171
|
+
};
|
|
1172
|
+
ctx.inject(["configForms"], (formsCtx) => {
|
|
1173
|
+
const forms = formsCtx.configForms;
|
|
1174
|
+
if (forms === void 0 || typeof forms.get !== "function") return;
|
|
1175
|
+
let boundId;
|
|
1176
|
+
let unsubscribeForm;
|
|
1177
|
+
const resolveEntryId = () => {
|
|
1178
|
+
const served = servedNamespaces(forms);
|
|
1179
|
+
return options.rowIds.find((id) => served.includes(id)) ?? served.find((ns) => ns.includes("prompt-enhance")) ?? options.rowIds[0];
|
|
1180
|
+
};
|
|
1181
|
+
const bind = () => {
|
|
1182
|
+
const entryId = resolveEntryId();
|
|
1183
|
+
if (entryId === void 0 || entryId === boundId) return;
|
|
1184
|
+
let form;
|
|
1185
|
+
try {
|
|
1186
|
+
form = forms.get?.(entryId);
|
|
1187
|
+
} catch {
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
if (form === void 0) return;
|
|
1191
|
+
try {
|
|
1192
|
+
unsubscribeForm?.();
|
|
1193
|
+
} catch {
|
|
1194
|
+
}
|
|
1195
|
+
unsubscribeForm = void 0;
|
|
1196
|
+
boundId = entryId;
|
|
1197
|
+
adoptSnapshot(RANK.configForms, form);
|
|
1198
|
+
try {
|
|
1199
|
+
unsubscribeForm = form.subscribe?.(() => adoptSnapshot(RANK.configForms, form));
|
|
1200
|
+
} catch {
|
|
1201
|
+
unsubscribeForm = void 0;
|
|
1202
|
+
}
|
|
1203
|
+
};
|
|
1204
|
+
bind();
|
|
1205
|
+
ctx.effect(() => () => {
|
|
1206
|
+
try {
|
|
1207
|
+
unsubscribeForm?.();
|
|
1208
|
+
} catch {
|
|
1209
|
+
}
|
|
1210
|
+
}, "prompt-enhance: settings form mirror");
|
|
1211
|
+
try {
|
|
1212
|
+
const watch = forms.describe?.()?.subscribe?.(() => bind());
|
|
1213
|
+
if (typeof watch === "function") {
|
|
1214
|
+
ctx.effect(() => () => {
|
|
1215
|
+
try {
|
|
1216
|
+
watch();
|
|
1217
|
+
} catch {
|
|
1218
|
+
}
|
|
1219
|
+
}, "prompt-enhance: settings namespace watch");
|
|
1220
|
+
}
|
|
1221
|
+
} catch {
|
|
1222
|
+
}
|
|
1223
|
+
});
|
|
1224
|
+
ctx.inject(["webUiSettings"], (binderCtx) => {
|
|
1225
|
+
const binder = binderCtx.webUiSettings;
|
|
1226
|
+
if (binder === void 0 || typeof binder.bind !== "function") return;
|
|
1227
|
+
let bound;
|
|
1228
|
+
for (const namespace of options.rowIds) {
|
|
1229
|
+
try {
|
|
1230
|
+
bound = binder.bind({ namespace });
|
|
1231
|
+
} catch {
|
|
1232
|
+
bound = void 0;
|
|
1233
|
+
}
|
|
1234
|
+
if (bound !== void 0) break;
|
|
1235
|
+
}
|
|
1236
|
+
if (bound === void 0) return;
|
|
1237
|
+
adoptSnapshot(RANK.binder, bound);
|
|
1238
|
+
let unsubscribe;
|
|
1239
|
+
try {
|
|
1240
|
+
unsubscribe = bound.subscribe?.(() => adoptSnapshot(RANK.binder, bound));
|
|
1241
|
+
} catch {
|
|
1242
|
+
unsubscribe = void 0;
|
|
1243
|
+
}
|
|
1244
|
+
ctx.effect(() => () => {
|
|
1245
|
+
try {
|
|
1246
|
+
unsubscribe?.();
|
|
1247
|
+
} catch {
|
|
1248
|
+
}
|
|
1249
|
+
}, "prompt-enhance: settings binder mirror");
|
|
1250
|
+
});
|
|
1251
|
+
ctx.inject(["settingsScope"], (scopeCtx) => {
|
|
1252
|
+
const scopeCtxLike = scopeCtx;
|
|
1253
|
+
const scope = scopeCtxLike.settingsScope;
|
|
1254
|
+
if (scope === void 0 || typeof scope.bind !== "function") return;
|
|
1255
|
+
let bound;
|
|
1256
|
+
try {
|
|
1257
|
+
bound = scope.bind({ namespace: options.rowIds[0] ?? "prompt-enhance", decode: (value) => value });
|
|
1258
|
+
} catch {
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
adoptSnapshot(RANK.legacyScope, bound);
|
|
1262
|
+
let unsubscribe;
|
|
1263
|
+
try {
|
|
1264
|
+
unsubscribe = bound.subscribe?.(() => adoptSnapshot(RANK.legacyScope, bound));
|
|
1265
|
+
} catch {
|
|
1266
|
+
unsubscribe = void 0;
|
|
1267
|
+
}
|
|
1268
|
+
ctx.effect(() => () => {
|
|
1269
|
+
try {
|
|
1270
|
+
unsubscribe?.();
|
|
1271
|
+
} catch {
|
|
1272
|
+
}
|
|
1273
|
+
}, "prompt-enhance: settings scope mirror");
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1111
1277
|
// src/client/index.tsx
|
|
1112
1278
|
var inject = [];
|
|
1279
|
+
function report(step, error) {
|
|
1280
|
+
try {
|
|
1281
|
+
console.warn(`[prompt-enhance] ${step} failed; that feature stays off.`, error);
|
|
1282
|
+
} catch {
|
|
1283
|
+
}
|
|
1284
|
+
return () => {
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1113
1287
|
function apply(ctx) {
|
|
1114
1288
|
ensureStyles();
|
|
1115
1289
|
ctx.inject(["locale"], (localeCtx) => {
|
|
1116
1290
|
ctx.effect(() => {
|
|
1117
1291
|
try {
|
|
1118
1292
|
return localeCtx.locale.register(NS, dictionaries);
|
|
1119
|
-
} catch {
|
|
1120
|
-
return (
|
|
1121
|
-
};
|
|
1293
|
+
} catch (error) {
|
|
1294
|
+
return report("dictionary registration", error);
|
|
1122
1295
|
}
|
|
1123
1296
|
}, "dsh-prompt-enhance: dictionaries");
|
|
1124
1297
|
});
|
|
1125
1298
|
setClientSettings(decodeClientSettings(void 0));
|
|
1126
|
-
ctx
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
setClientSettings(scope.getSnapshot().value ?? decodeClientSettings(void 0));
|
|
1130
|
-
};
|
|
1131
|
-
sync();
|
|
1132
|
-
ctx.effect(() => scope.subscribe(sync), "dsh-prompt-enhance: settings mirror");
|
|
1299
|
+
installClientSettingsSource(ctx, {
|
|
1300
|
+
rowIds: PROMPT_ENHANCE_ROW_IDS,
|
|
1301
|
+
onSection: (section) => setClientSettings(decodeClientSettings(section))
|
|
1133
1302
|
});
|
|
1134
1303
|
ctx.inject(["slots"], (slotsCtx) => {
|
|
1135
1304
|
const slots = slotsCtx.slots;
|
|
1305
|
+
if (slots === void 0) return;
|
|
1136
1306
|
return slots.inject("conversation.input.right", () => {
|
|
1137
1307
|
try {
|
|
1138
1308
|
return slots.register(
|
|
1139
1309
|
{ name: "conversation.input.right", id: "prompt-enhance", order: 60, locale: NS },
|
|
1140
1310
|
EnhanceButton
|
|
1141
1311
|
);
|
|
1142
|
-
} catch {
|
|
1143
|
-
return (
|
|
1144
|
-
};
|
|
1312
|
+
} catch (error) {
|
|
1313
|
+
return report("composer enhance button slot", error);
|
|
1145
1314
|
}
|
|
1146
1315
|
});
|
|
1147
1316
|
});
|
|
1148
1317
|
ctx.inject(["slots"], (slotsCtx) => {
|
|
1149
1318
|
const slots = slotsCtx.slots;
|
|
1319
|
+
if (slots === void 0) return;
|
|
1150
1320
|
return slots.inject("conversation.input.dock", () => {
|
|
1151
1321
|
try {
|
|
1152
1322
|
return slots.register(
|
|
1153
1323
|
{ name: "conversation.input.dock", id: "prompt-enhance-undo", order: 90, locale: NS },
|
|
1154
1324
|
UndoBar
|
|
1155
1325
|
);
|
|
1156
|
-
} catch {
|
|
1157
|
-
return (
|
|
1158
|
-
};
|
|
1326
|
+
} catch (error) {
|
|
1327
|
+
return report("undo bar slot", error);
|
|
1159
1328
|
}
|
|
1160
1329
|
});
|
|
1161
1330
|
});
|
|
1162
1331
|
ctx.effect(() => {
|
|
1163
1332
|
const onKeyDown = (event) => {
|
|
1164
1333
|
if (event.defaultPrevented || event.isComposing) return;
|
|
1165
|
-
const
|
|
1334
|
+
const settings = getClientSettings();
|
|
1335
|
+
if (!settings.enabled) return;
|
|
1336
|
+
const combo = parseShortcut(settings.shortcut);
|
|
1166
1337
|
if (!matchesShortcut(event, combo)) return;
|
|
1167
1338
|
const target = shortcutTarget();
|
|
1168
1339
|
if (target === void 0) return;
|