dsh-prompt-rewriter 1.6.1 → 1.6.3
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/CHANGELOG.md +23 -1
- package/README.md +7 -5
- package/README.zh.md +7 -5
- package/client.js +203 -56
- package/docs/images/03-settings-en.png +0 -0
- package/docs/images/03-settings.png +0 -0
- package/docs/images/05-button-states-en.gif +0 -0
- package/docs/images/05-button-states.gif +0 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
记录每个**已发布**版本对使用者可见的变化。中英双语并列,中文在前。
|
|
4
4
|
|
|
5
|
+
## 1.6.3
|
|
6
|
+
|
|
7
|
+
**中文**
|
|
8
|
+
|
|
9
|
+
- 快捷键服务不可用时插件不再整体失效。此前宿主的快捷键服务若没有注册(例如与另一个锁定旧版客户端的插件共存),整个插件会保持休眠,输入框旁的按钮也随之消失;现在只有快捷键降级为不可用,改写按钮照常工作。
|
|
10
|
+
- 快捷键服务晚于插件注册时也能读到当前绑定,不再把它显示成「未设置」或「不可用」。
|
|
11
|
+
|
|
12
|
+
**English**
|
|
13
|
+
|
|
14
|
+
- The plugin no longer goes dormant when the host's shortcut service is missing. Previously, if that service never registered — for example when another plugin pinned the client runtime to an older range — the whole plugin stayed inactive and the composer button disappeared with it. Only the shortcut degrades now; the Enhance button keeps working.
|
|
15
|
+
- A shortcut service that registers after the plugin now resolves correctly, instead of being reported as unset or unavailable.
|
|
16
|
+
## 1.6.2
|
|
17
|
+
|
|
18
|
+
**中文**
|
|
19
|
+
|
|
20
|
+
- 增强进行中的鱼尾动效改用品牌蓝。此前它跟随按钮的文字色,深色主题下接近白色,与旁边的鱼形图标不同色。
|
|
21
|
+
- 输入框里的图片读取更可靠:宿主服务取不到时改从输入框自身的通道取图;两处都无法回答时不再静默跳过,文字改写照常完成,状态区会告诉你少读了几张。
|
|
22
|
+
|
|
23
|
+
**English**
|
|
24
|
+
|
|
25
|
+
- The tail animation shown while an enhancement runs now uses the brand blue. It previously followed the button's text colour, which is near white in the dark theme and did not match the fish icon beside it.
|
|
26
|
+
- Composer images are read more reliably: when the host service cannot answer, the composer's own channel is used instead; when neither can, the images are no longer skipped silently — the text-only rewrite still completes and the status area reports how many were missed.
|
|
5
27
|
## 1.6.0
|
|
6
28
|
|
|
7
29
|
**中文**
|
|
@@ -86,4 +108,4 @@
|
|
|
86
108
|
|
|
87
109
|
**English**
|
|
88
110
|
|
|
89
|
-
- Added the bilingual README and screenshots; fixed images and the Chinese guide missing from the npm package.
|
|
111
|
+
- Added the bilingual README and screenshots; fixed images and the Chinese guide missing from the npm package.
|
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ An enhancement is a real model call, and **how fast the result comes back depend
|
|
|
43
43
|
|
|
44
44
|
If it feels slow, switch the session to a model that is more stable and responds faster — the same draft can take a few seconds on one model and well over ten on another. The button does not favour any particular model; it uses whichever one you picked.
|
|
45
45
|
|
|
46
|
-
> Want a fixed model just for enhancement, without affecting normal chat? Open `Settings → Prompt
|
|
46
|
+
> Want a fixed model just for enhancement, without affecting normal chat? Open `Settings → Prompt Enhance` and turn on **Use a fixed model**.
|
|
47
47
|
|
|
48
48
|
### You can navigate away mid-rewrite
|
|
49
49
|
|
|
@@ -55,7 +55,7 @@ That wasn't true in 1.0.x: switching sessions aborted the task, and coming back
|
|
|
55
55
|
|
|
56
56
|
### Keyboard shortcut (off by default)
|
|
57
57
|
|
|
58
|
-
Turn it on at **Settings → Prompt
|
|
58
|
+
Turn it on at **Settings → Prompt Enhance → Shortcut**. Desktop uses `Ctrl+E` (`Cmd+E` on macOS); the browser uses `Ctrl+Alt+E` (`Cmd+Alt+E`).
|
|
59
59
|
|
|
60
60
|
- Press once to enhance; press again while it runs to pause. It acts on the composer of the current conversation.
|
|
61
61
|
- **Same as clicking the icon** — the shortcut runs the icon's own click handler, including its undo state, so the two paths cannot drift. Restoring a draft stays on the button or `Ctrl+Z`.
|
|
@@ -95,7 +95,7 @@ Only images **currently sitting in the composer** — **never images from earlie
|
|
|
95
95
|
|
|
96
96
|
## Settings
|
|
97
97
|
|
|
98
|
-
`Settings → Prompt
|
|
98
|
+
`Settings → Prompt Enhance`. Four switches and one dropdown, affecting **only this button, never normal sending**.
|
|
99
99
|
|
|
100
100
|

|
|
101
101
|
|
|
@@ -104,6 +104,7 @@ Only images **currently sitting in the composer** — **never images from earlie
|
|
|
104
104
|
| Use a fixed model | Off | Off: follow the model of the current conversation. On: always use the model picked below. |
|
|
105
105
|
| Reference the conversation | **On** | Read earlier turns. More accurate, but extra tokens and slower on long chats. |
|
|
106
106
|
| Reference composer images | Off | Images in the composer take part. Earlier messages' images are never read. |
|
|
107
|
+
| Shortcut | Off | Lets the keyboard trigger a rewrite. Off by default, so no global key fires unasked. |
|
|
107
108
|
|
|
108
109
|
- The model dropdown lists **the providers and models you actually have** — you can't type in one that doesn't exist.
|
|
109
110
|
- Turning the model switch off does not clear your selection; flipping it back on shows the same choice.
|
|
@@ -137,7 +138,7 @@ Download the zip from [Releases](https://github.com/huaguoxingkong/dsh-prompt-re
|
|
|
137
138
|
|
|
138
139
|
## FAQ
|
|
139
140
|
|
|
140
|
-
**Clicked it and nothing happened.** The composer is empty, or *Use a fixed model* is on with no model picked.
|
|
141
|
+
**Clicked it and nothing happened.** The composer is empty, or *Use a fixed model* is on with no model picked. If you used the shortcut instead, check that the Shortcut switch is on and that no conflict is reported.
|
|
141
142
|
|
|
142
143
|
**"No credentials configured for the current model."** That model has no key set. Switch models, or pin one that works in settings.
|
|
143
144
|
|
|
@@ -147,7 +148,7 @@ Download the zip from [Releases](https://github.com/huaguoxingkong/dsh-prompt-re
|
|
|
147
148
|
|
|
148
149
|
## Configuration
|
|
149
150
|
|
|
150
|
-
Every `config` key on the profile plugin line is optional. The first
|
|
151
|
+
Every `config` key on the profile plugin line is optional. The first six are managed by the settings page; the last seven are operational ceilings and are not exposed in the UI.
|
|
151
152
|
|
|
152
153
|
```yaml
|
|
153
154
|
- id: dsh-prompt-rewriter
|
|
@@ -159,6 +160,7 @@ Every `config` key on the profile plugin line is optional. The first five are ma
|
|
|
159
160
|
enhanceModel: ""
|
|
160
161
|
referenceImages: false # read composer images
|
|
161
162
|
referenceContext: true # read earlier turns
|
|
163
|
+
shortcutEnabled: false # keyboard shortcut (off by default)
|
|
162
164
|
|
|
163
165
|
# operational ceilings (not exposed in the UI)
|
|
164
166
|
maxDraftChars: 8000 # draft length cap
|
package/README.zh.md
CHANGED
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
|
|
45
45
|
如果觉得等得久,把会话用的模型换成一个更稳定、响应更快的——同一个草稿,换个模型往往就是几秒和十几秒的差别。这个按钮不挑模型,你选哪个它就用哪个。
|
|
46
46
|
|
|
47
|
-
> 想固定用某个模型专门做增强(不影响正常对话)?去 `设置 →
|
|
47
|
+
> 想固定用某个模型专门做增强(不影响正常对话)?去 `设置 → 提示词增强` 打开「用哪个模型改写」。
|
|
48
48
|
|
|
49
49
|
### 增强过程中可以切走
|
|
50
50
|
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
|
|
57
57
|
### 键盘快捷键(默认关闭)
|
|
58
58
|
|
|
59
|
-
到 **设置 →
|
|
59
|
+
到 **设置 → 提示词增强 → 快捷键** 打开开关。桌面端为 `Ctrl+E`(macOS `Cmd+E`),浏览器端为 `Ctrl+Alt+E`(macOS `Cmd+Alt+E`)。
|
|
60
60
|
|
|
61
61
|
- 按一次增强,运行中再按一次暂停。作用对象是当前会话的输入框。
|
|
62
62
|
- **与点击图标完全等价**:走的是图标自身的点击入口,含「撤销」态,不存在第二套逻辑。回退仍由按钮或 `Ctrl+Z` 负责。
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
|
|
97
97
|
## 设置
|
|
98
98
|
|
|
99
|
-
`设置 →
|
|
99
|
+
`设置 → 提示词增强`。四个开关一个下拉,**只影响这个按钮,不影响正常发送**。
|
|
100
100
|
|
|
101
101
|

|
|
102
102
|
|
|
@@ -105,6 +105,7 @@
|
|
|
105
105
|
| 用哪个模型改写 | 关 | 关:跟随当前对话的模型。开:固定用下拉选定的模型。 |
|
|
106
106
|
| 参考之前的对话 | **开** | 读前面的内容。更准,但多耗 token、长对话更慢。 |
|
|
107
107
|
| 参考输入框里的图片 | 关 | 输入框里的图一起参与。历史消息里的图片始终不读。 |
|
|
108
|
+
| 快捷键 | 关 | 打开后可用键盘触发改写。默认关闭,不会在你没要求时抢占按键。 |
|
|
108
109
|
|
|
109
110
|
- 模型下拉列出你**实际拥有**的渠道与模型,填不进不存在的模型。
|
|
110
111
|
- 关掉模型开关不会清空你的选择,重新打开还是同一个。
|
|
@@ -138,7 +139,7 @@ dsh plugin --profile web add github:huaguoxingkong/dsh-prompt-rewriter
|
|
|
138
139
|
|
|
139
140
|
## 常见问题
|
|
140
141
|
|
|
141
|
-
**点了没反应?**
|
|
142
|
+
**点了没反应?** 输入框是空的,或者「用哪个模型改写」打开了但没选模型。如果用的是快捷键,先确认「快捷键」开关已打开、且没有提示冲突。
|
|
142
143
|
|
|
143
144
|
**报「当前模型尚未配置可用的凭据」?** 这个模型没配 key。换个模型,或在设置里固定一个能用的。
|
|
144
145
|
|
|
@@ -148,7 +149,7 @@ dsh plugin --profile web add github:huaguoxingkong/dsh-prompt-rewriter
|
|
|
148
149
|
|
|
149
150
|
## 配置
|
|
150
151
|
|
|
151
|
-
profile 插件行的 `config`
|
|
152
|
+
profile 插件行的 `config` 全部可选。前六项由设置页托管,后七项是运维上限,不暴露在界面上:
|
|
152
153
|
|
|
153
154
|
```yaml
|
|
154
155
|
- id: dsh-prompt-rewriter
|
|
@@ -160,6 +161,7 @@ profile 插件行的 `config` 全部可选。前五项由设置页托管,后
|
|
|
160
161
|
enhanceModel: ""
|
|
161
162
|
referenceImages: false # 是否参考输入框图片
|
|
162
163
|
referenceContext: true # 是否参考前文
|
|
164
|
+
shortcutEnabled: false # 键盘快捷键(默认关闭)
|
|
163
165
|
|
|
164
166
|
# 运维上限(不暴露在设置页)
|
|
165
167
|
maxDraftChars: 8000 # 草稿字符上限
|
package/client.js
CHANGED
|
@@ -75,7 +75,7 @@ window.__ModuleLoader__.load({
|
|
|
75
75
|
// Colour: the official brand blue, not `currentColor`. The tail is a graphic,
|
|
76
76
|
// not text, so inheriting the button's label colour made it render white on
|
|
77
77
|
// the dark composer — visually detached from the fish mark it replaces.
|
|
78
|
-
'.dsh-pe_twist{display:block;width:20px;height:16px;overflow:visible;color:
|
|
78
|
+
'.dsh-pe_twist{display:block;width:20px;height:16px;overflow:visible;color:var(--dsw-alias-brand-primary-new-colorprimary-new-color,#4176e6)}',
|
|
79
79
|
'.dsh-pe_twist path{fill:currentColor;fill-rule:evenodd}',
|
|
80
80
|
'.dsh-pe_icon{display:block;width:15px;height:15px}',
|
|
81
81
|
// Context-mode mark: a dot on the seat that reports an EXPECTATION — "if you
|
|
@@ -460,6 +460,7 @@ window.__ModuleLoader__.load({
|
|
|
460
460
|
*/
|
|
461
461
|
let conversationResolver = () => undefined;
|
|
462
462
|
|
|
463
|
+
|
|
463
464
|
/**
|
|
464
465
|
* Resolve the client `conversation` service AT CALL TIME.
|
|
465
466
|
*
|
|
@@ -708,10 +709,6 @@ window.__ModuleLoader__.load({
|
|
|
708
709
|
// feature is opt-in, so a global key never fires on a user who did not ask for
|
|
709
710
|
// it. Read from the same settings response as the other switches.
|
|
710
711
|
const [shortcutOn, setShortcutOn] = useState(false);
|
|
711
|
-
// Exposed on the seat as data-ctx-debug so a missing dot can be diagnosed
|
|
712
|
-
// without guessing: it records which of the two facts was false, and whether
|
|
713
|
-
// either request failed. Not shown to users; harmless in the DOM.
|
|
714
|
-
const [ctxProbe, setCtxProbe] = useState('pending');
|
|
715
712
|
useEffect(() => {
|
|
716
713
|
let cancelled = false;
|
|
717
714
|
const read = async () => {
|
|
@@ -730,16 +727,11 @@ window.__ModuleLoader__.load({
|
|
|
730
727
|
const allowed = settings?.ok === true && settings.values?.shortcutEnabled === true;
|
|
731
728
|
setShortcutOn(allowed);
|
|
732
729
|
taskStore.setShortcutEnabled(allowed);
|
|
733
|
-
setCtxProbe(
|
|
734
|
-
`sw=${switchOn ? 1 : 0},hc=${hasContext ? 1 : 0},`
|
|
735
|
-
+ `sHttp=${settingsResponse.status},cHttp=${statusResponse.status}`,
|
|
736
|
-
);
|
|
737
730
|
} catch (error) {
|
|
738
731
|
// An unreachable route means "we cannot promise context"; showing no dot
|
|
739
732
|
// is the honest default rather than promising a context-aware run.
|
|
740
733
|
if (!cancelled) {
|
|
741
734
|
setWillUseContext(false);
|
|
742
|
-
setCtxProbe(`error:${error?.message ?? 'unknown'}`);
|
|
743
735
|
}
|
|
744
736
|
}
|
|
745
737
|
};
|
|
@@ -819,19 +811,52 @@ window.__ModuleLoader__.load({
|
|
|
819
811
|
* @param signal - cancellation.
|
|
820
812
|
* @returns `{ images, skipped }`.
|
|
821
813
|
*/
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
814
|
+
/**
|
|
815
|
+
* Resolve the composer draft descriptors for these ids.
|
|
816
|
+
*
|
|
817
|
+
* Two owners can answer, and which one is reachable depends on the shell:
|
|
818
|
+
*
|
|
819
|
+
* 1. the `conversation` client service, which owns the draft map;
|
|
820
|
+
* 2. the composer action face handed to the slot, which exposes the same
|
|
821
|
+
* lookup when the host chooses to publish it there.
|
|
822
|
+
*
|
|
823
|
+
* Trying both removes the single point of failure that made every image come
|
|
824
|
+
* back unreadable when the first owner was absent.
|
|
825
|
+
*
|
|
826
|
+
* @param inputActions - the composer action face from slot props.
|
|
827
|
+
* @param ids - draft attachment ids.
|
|
828
|
+
* @returns `{ descriptors, source }` where source is the owner that answered.
|
|
829
|
+
*/
|
|
830
|
+
function resolveDraftDescriptors(inputActions, ids) {
|
|
831
|
+
const conversation = conversationResolver();
|
|
832
|
+
if (conversation !== undefined && typeof conversation.resolveDraftAttachments === 'function') {
|
|
833
|
+
try {
|
|
834
|
+
return { descriptors: conversation.resolveDraftAttachments(ids) ?? [], source: 'service' };
|
|
835
|
+
} catch {
|
|
836
|
+
// Fall through to the action face.
|
|
837
|
+
}
|
|
828
838
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
839
|
+
if (inputActions !== undefined && typeof inputActions.resolveDraftAttachments === 'function') {
|
|
840
|
+
try {
|
|
841
|
+
return { descriptors: inputActions.resolveDraftAttachments(ids) ?? [], source: 'actions' };
|
|
842
|
+
} catch {
|
|
843
|
+
// Neither owner could answer.
|
|
844
|
+
}
|
|
834
845
|
}
|
|
846
|
+
return { descriptors: [], source: 'none' };
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/**
|
|
850
|
+
* Turn resolved draft descriptors into request images.
|
|
851
|
+
*
|
|
852
|
+
* @param descriptors - draft descriptors carrying the original browser `File`.
|
|
853
|
+
* @param signal - cancellation.
|
|
854
|
+
* @returns `{ images, skipped }`.
|
|
855
|
+
*/
|
|
856
|
+
async function readComposerImages(descriptors, signal) {
|
|
857
|
+
const images = [];
|
|
858
|
+
let skipped = 0;
|
|
859
|
+
if (!Array.isArray(descriptors) || descriptors.length === 0) return { images, skipped };
|
|
835
860
|
for (const descriptor of descriptors.slice(0, 4)) {
|
|
836
861
|
if (signal.aborted) return { images, skipped };
|
|
837
862
|
if (descriptor?.kind !== 'image') continue;
|
|
@@ -900,10 +925,17 @@ window.__ModuleLoader__.load({
|
|
|
900
925
|
// race entirely and keeps one authority for the switch.
|
|
901
926
|
// Resolved here, not at activation: the service mounts with the
|
|
902
927
|
// conversation plugin, which can come up after this plugin.
|
|
903
|
-
const
|
|
928
|
+
const drafts = attachmentIds.length > 0
|
|
929
|
+
? resolveDraftDescriptors(inputActions, attachmentIds)
|
|
930
|
+
: { descriptors: [], source: 'none' };
|
|
904
931
|
const collected = attachmentIds.length > 0
|
|
905
|
-
? await readComposerImages(
|
|
932
|
+
? await readComposerImages(drafts.descriptors, controller.signal)
|
|
906
933
|
: { images: [], skipped: 0 };
|
|
934
|
+
if (attachmentIds.length > 0 && drafts.source === 'none') {
|
|
935
|
+
// Nothing could read the attachments. Report it through the same notice
|
|
936
|
+
// the Host uses for a skipped image, instead of dropping them silently.
|
|
937
|
+
collected.skipped = attachmentIds.length;
|
|
938
|
+
}
|
|
907
939
|
if (controller.signal.aborted) return;
|
|
908
940
|
const response = await fetch('/api/prompt-enhance', {
|
|
909
941
|
method: 'POST',
|
|
@@ -1112,7 +1144,6 @@ window.__ModuleLoader__.load({
|
|
|
1112
1144
|
className: 'dsh-pe_root',
|
|
1113
1145
|
'data-prompt-enhance': '1',
|
|
1114
1146
|
'data-context': showContextMark ? 'true' : 'false',
|
|
1115
|
-
'data-ctx-debug': ctxProbe,
|
|
1116
1147
|
'data-notice': failure !== null ? 'true' : 'false',
|
|
1117
1148
|
},
|
|
1118
1149
|
h(
|
|
@@ -1158,6 +1189,61 @@ window.__ModuleLoader__.load({
|
|
|
1158
1189
|
/** The command id this plugin registers with the host shortcuts service. */
|
|
1159
1190
|
const SHORTCUT_ID = 'prompt-enhance.run';
|
|
1160
1191
|
|
|
1192
|
+
/**
|
|
1193
|
+
* Whether a value behaves like the host `shortcuts` service.
|
|
1194
|
+
*
|
|
1195
|
+
* The service is OPTIONAL: the host builds it only when the desktop keyboard
|
|
1196
|
+
* bridge exists, and a third-party shortcut plugin that pins the client runtime
|
|
1197
|
+
* to an older range can keep it from registering at all. Losing it must cost
|
|
1198
|
+
* only the shortcut settings row — never the composer button.
|
|
1199
|
+
*
|
|
1200
|
+
* @param candidate - value from `ctx.get('shortcuts')`.
|
|
1201
|
+
* @returns whether the face can be used.
|
|
1202
|
+
*/
|
|
1203
|
+
function usableShortcuts(candidate) {
|
|
1204
|
+
if (candidate === undefined || candidate === null) return false;
|
|
1205
|
+
if (typeof candidate !== 'object' && typeof candidate !== 'function') return false;
|
|
1206
|
+
return typeof candidate.register === 'function';
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
/**
|
|
1210
|
+
* Read a snapshot from a face that may expose it as a method or as a plain value.
|
|
1211
|
+
*
|
|
1212
|
+
* Host builds differ here: one publishes `catalog` as an object with
|
|
1213
|
+
* `getSnapshot()`, another as the array itself. Both shapes are accepted, and
|
|
1214
|
+
* anything unexpected collapses to an empty list so callers stay simple.
|
|
1215
|
+
*
|
|
1216
|
+
* @param face - `shortcuts.catalog` or `shortcuts.config`.
|
|
1217
|
+
* @param key - array field to use when the value is a wrapper object.
|
|
1218
|
+
* @returns the snapshot array, or an empty array.
|
|
1219
|
+
*/
|
|
1220
|
+
function readSnapshot(face, key) {
|
|
1221
|
+
if (face === undefined || face === null) return [];
|
|
1222
|
+
try {
|
|
1223
|
+
const raw = typeof face.getSnapshot === 'function' ? face.getSnapshot() : face;
|
|
1224
|
+
if (Array.isArray(raw)) return raw;
|
|
1225
|
+
if (Array.isArray(raw?.[key])) return raw[key];
|
|
1226
|
+
} catch {
|
|
1227
|
+
// A throwing getter means "not ready"; callers treat that as empty.
|
|
1228
|
+
}
|
|
1229
|
+
return [];
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
* Read the binding revision, tolerating both shapes and any throw.
|
|
1234
|
+
*
|
|
1235
|
+
* @param shortcuts - the service face.
|
|
1236
|
+
* @returns the revision, or undefined.
|
|
1237
|
+
*/
|
|
1238
|
+
function readRevision(shortcuts) {
|
|
1239
|
+
try {
|
|
1240
|
+
const config = shortcuts?.config;
|
|
1241
|
+
const raw = typeof config?.getSnapshot === 'function' ? config.getSnapshot() : config;
|
|
1242
|
+
return raw?.revision;
|
|
1243
|
+
} catch {
|
|
1244
|
+
return undefined;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1161
1247
|
/**
|
|
1162
1248
|
* Read and edit this plugin's keyboard binding through the host shortcuts service.
|
|
1163
1249
|
*
|
|
@@ -1170,7 +1256,11 @@ window.__ModuleLoader__.load({
|
|
|
1170
1256
|
* @param t - localized translator, for host-reported failure states.
|
|
1171
1257
|
* @returns `{ keys, isDefault, assign, reset, refresh }`.
|
|
1172
1258
|
*/
|
|
1173
|
-
function useShortcutBinding(
|
|
1259
|
+
function useShortcutBinding(resolveShortcuts, t) {
|
|
1260
|
+
// Resolved on every read, never captured: the host may register the service
|
|
1261
|
+
// after this section mounts, and a value captured during render would stay
|
|
1262
|
+
// undefined for the life of the component.
|
|
1263
|
+
const shortcuts = typeof resolveShortcuts === 'function' ? resolveShortcuts() : resolveShortcuts;
|
|
1174
1264
|
const [state, setState] = useState({ keys: '', isDefault: true, revision: undefined });
|
|
1175
1265
|
const alive = useRef(false);
|
|
1176
1266
|
alive.current = true;
|
|
@@ -1179,10 +1269,11 @@ window.__ModuleLoader__.load({
|
|
|
1179
1269
|
const refresh = useCallback(() => {
|
|
1180
1270
|
if (shortcuts === undefined) return;
|
|
1181
1271
|
try {
|
|
1182
|
-
const snapshot = shortcuts
|
|
1183
|
-
const row =
|
|
1184
|
-
const
|
|
1185
|
-
|
|
1272
|
+
const snapshot = readSnapshot(shortcuts?.catalog, 'commands');
|
|
1273
|
+
const row = snapshot.find((entry) => entry?.id === SHORTCUT_ID);
|
|
1274
|
+
const revision = readRevision(shortcuts);
|
|
1275
|
+
// The command is not in the host catalog (yet, or at all). Leaving the row
|
|
1276
|
+
// as it stands is correct: nothing is bound, so nothing can be shown.
|
|
1186
1277
|
if (row === undefined) return;
|
|
1187
1278
|
setState({
|
|
1188
1279
|
// `keys` is the host's own display form of the resolved binding.
|
|
@@ -1206,10 +1297,7 @@ window.__ModuleLoader__.load({
|
|
|
1206
1297
|
* refuses it with `if (revision !== this.snapshot.revision) return result("stale")`.
|
|
1207
1298
|
* That is why 恢复默认 did nothing and the old override kept showing.
|
|
1208
1299
|
*/
|
|
1209
|
-
const revisionOf = useCallback(() =>
|
|
1210
|
-
const config = shortcuts?.config?.getSnapshot?.() ?? shortcuts?.config;
|
|
1211
|
-
return config?.revision;
|
|
1212
|
-
}, [shortcuts]);
|
|
1300
|
+
const revisionOf = useCallback(() => readRevision(shortcuts), [shortcuts]);
|
|
1213
1301
|
|
|
1214
1302
|
/**
|
|
1215
1303
|
* Submit one edit against the live revision, retrying once on "stale".
|
|
@@ -1220,23 +1308,48 @@ window.__ModuleLoader__.load({
|
|
|
1220
1308
|
* and then trying to reset it.
|
|
1221
1309
|
*/
|
|
1222
1310
|
const applyEdit = useCallback(async (operation) => {
|
|
1223
|
-
if (shortcuts
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
result
|
|
1311
|
+
if (!usableShortcuts(shortcuts)) return { status: 'unavailable' };
|
|
1312
|
+
if (typeof shortcuts.edit !== 'function') return { status: 'unavailable' };
|
|
1313
|
+
try {
|
|
1314
|
+
let result = await shortcuts.edit(operation, revisionOf());
|
|
1315
|
+
if (result?.status === 'stale') {
|
|
1316
|
+
refresh();
|
|
1317
|
+
result = await shortcuts.edit(operation, revisionOf());
|
|
1318
|
+
}
|
|
1319
|
+
if (result?.status === 'saved') refresh();
|
|
1320
|
+
return result;
|
|
1321
|
+
} catch (error) {
|
|
1322
|
+
// A throwing edit is a broken host, not a user mistake: report it as
|
|
1323
|
+
// failed so the row shows a reason instead of crashing the page.
|
|
1324
|
+
return { status: 'failed', reason: error?.message ?? 'exception' };
|
|
1228
1325
|
}
|
|
1229
|
-
if (result?.status === 'saved') refresh();
|
|
1230
|
-
return result;
|
|
1231
1326
|
}, [refresh, revisionOf, shortcuts]);
|
|
1232
1327
|
|
|
1233
1328
|
useEffect(() => {
|
|
1234
1329
|
refresh();
|
|
1330
|
+
// The host may register the service a beat after this section mounts. A
|
|
1331
|
+
// captured-at-render lookup would then leave the row permanently blank,
|
|
1332
|
+
// which is exactly the empty box users saw. Re-reading a few times covers
|
|
1333
|
+
// that window without polling forever.
|
|
1334
|
+
let tries = 0;
|
|
1335
|
+
const retry = setInterval(() => {
|
|
1336
|
+
tries += 1;
|
|
1337
|
+
refresh();
|
|
1338
|
+
if (tries >= 10) clearInterval(retry);
|
|
1339
|
+
}, 120);
|
|
1235
1340
|
// The host publishes catalog changes; following them keeps this row honest
|
|
1236
1341
|
// when a binding is changed from the general Settings page instead.
|
|
1237
|
-
|
|
1342
|
+
let unsubscribe;
|
|
1343
|
+
try {
|
|
1344
|
+
if (typeof shortcuts?.catalog?.subscribe === 'function') {
|
|
1345
|
+
unsubscribe = shortcuts.catalog.subscribe(refresh);
|
|
1346
|
+
}
|
|
1347
|
+
} catch {
|
|
1348
|
+
// No subscription means the row does not live-update; it still works.
|
|
1349
|
+
}
|
|
1238
1350
|
return () => {
|
|
1239
1351
|
alive.current = false;
|
|
1352
|
+
clearInterval(retry);
|
|
1240
1353
|
if (typeof unsubscribe === 'function') unsubscribe();
|
|
1241
1354
|
};
|
|
1242
1355
|
}, [refresh, shortcuts]);
|
|
@@ -1251,19 +1364,20 @@ window.__ModuleLoader__.load({
|
|
|
1251
1364
|
* @returns a promise resolving to an error message, or null on success.
|
|
1252
1365
|
*/
|
|
1253
1366
|
const assign = useCallback(async (binding) => {
|
|
1254
|
-
if (shortcuts
|
|
1367
|
+
if (!usableShortcuts(shortcuts)) return t('settings.shortcut.unavailable');
|
|
1255
1368
|
try {
|
|
1256
1369
|
// The host exposes the collision report for a candidate without applying
|
|
1257
1370
|
// it; naming the colliding commands is what makes the failure readable.
|
|
1258
|
-
const described = shortcuts.describeBinding
|
|
1371
|
+
const described = typeof shortcuts.describeBinding === 'function' ? shortcuts.describeBinding(binding) : undefined;
|
|
1259
1372
|
if (described?.issue !== null && described?.issue !== undefined) {
|
|
1260
1373
|
return t('settings.shortcut.rejected', { reason: described.issue });
|
|
1261
1374
|
}
|
|
1262
1375
|
const conflicts = (described?.conflicts ?? [])
|
|
1263
1376
|
.filter((id) => id !== SHORTCUT_ID)
|
|
1264
1377
|
.map((id) => {
|
|
1265
|
-
|
|
1266
|
-
const
|
|
1378
|
+
// A broken catalog only costs the friendly names in the message.
|
|
1379
|
+
const snapshot = readSnapshot(shortcuts?.catalog, 'commands');
|
|
1380
|
+
const row = snapshot.find((entry) => entry?.id === id);
|
|
1267
1381
|
const label = typeof row?.label === 'function' ? row.label() : row?.label;
|
|
1268
1382
|
return label ?? id;
|
|
1269
1383
|
});
|
|
@@ -1289,7 +1403,7 @@ window.__ModuleLoader__.load({
|
|
|
1289
1403
|
* @returns a promise resolving to an error message, or null on success.
|
|
1290
1404
|
*/
|
|
1291
1405
|
const reset = useCallback(async () => {
|
|
1292
|
-
if (shortcuts
|
|
1406
|
+
if (!usableShortcuts(shortcuts)) return t('settings.shortcut.unavailable');
|
|
1293
1407
|
try {
|
|
1294
1408
|
const result = await applyEdit({ type: 'reset', id: SHORTCUT_ID });
|
|
1295
1409
|
if (result !== undefined && result?.status !== 'saved') {
|
|
@@ -1315,7 +1429,7 @@ window.__ModuleLoader__.load({
|
|
|
1315
1429
|
* @param props.t - localized translator.
|
|
1316
1430
|
* @param props.getRemote - thunk returning the injected `remote.session` face.
|
|
1317
1431
|
*/
|
|
1318
|
-
function SettingsSection({ t, getRemote,
|
|
1432
|
+
function SettingsSection({ t, getRemote, getShortcuts }) {
|
|
1319
1433
|
const [values, setValues] = useState(null);
|
|
1320
1434
|
// `{ version, url }` reported by the Host from the package manifest. Never
|
|
1321
1435
|
// hard-coded here: a release only bumps package.json.
|
|
@@ -1371,7 +1485,6 @@ window.__ModuleLoader__.load({
|
|
|
1371
1485
|
// Keep the reason: an empty picker with no explanation is
|
|
1372
1486
|
// indistinguishable from a slow load, and the message is shown to the
|
|
1373
1487
|
// user instead of being swallowed.
|
|
1374
|
-
if (typeof console !== 'undefined') console.warn('[dsh-prompt-rewriter] model catalog failed:', failure?.message ?? failure);
|
|
1375
1488
|
setCatalog({ status: 'error', groups: [] });
|
|
1376
1489
|
}
|
|
1377
1490
|
})();
|
|
@@ -1436,7 +1549,9 @@ window.__ModuleLoader__.load({
|
|
|
1436
1549
|
// it after that return meant the loading render used fewer hooks than the
|
|
1437
1550
|
// loaded render, which React rejects with "Rendered more hooks than during the
|
|
1438
1551
|
// previous render" — and the settings page went completely blank.
|
|
1439
|
-
|
|
1552
|
+
// The hook resolves the face on every read: the host may register it after
|
|
1553
|
+
// this section mounts, and it may never register it at all.
|
|
1554
|
+
const shortcut = useShortcutBinding(getShortcuts, t);
|
|
1440
1555
|
|
|
1441
1556
|
if (values === null) {
|
|
1442
1557
|
return h(
|
|
@@ -1717,7 +1832,16 @@ window.__ModuleLoader__.load({
|
|
|
1717
1832
|
// detection and the user's binding overrides. Registering through it — instead
|
|
1718
1833
|
// of adding our own keydown listener — is what makes a custom binding persist
|
|
1719
1834
|
// across restarts and collide visibly with a host command.
|
|
1720
|
-
|
|
1835
|
+
//
|
|
1836
|
+
// It is deliberately NOT in this list. Cordis activates a plugin only when
|
|
1837
|
+
// every injected service exists, so listing it made a missing keyboard
|
|
1838
|
+
// bridge fatal: the whole plugin stayed dormant and the composer button
|
|
1839
|
+
// vanished along with the shortcut row. The host builds that service
|
|
1840
|
+
// conditionally (`window.dshDesktop.keyboard`), and a third-party shortcut
|
|
1841
|
+
// plugin that pins the client runtime to an older range can keep it from
|
|
1842
|
+
// ever registering. Resolving it through `ctx.get` at call time makes the
|
|
1843
|
+
// keyboard binding optional while the rewrite itself stays unconditional.
|
|
1844
|
+
inject: ['slots', 'locale', 'remote', 'remote.session'],
|
|
1721
1845
|
apply(ctx) {
|
|
1722
1846
|
ctx.effect(installStyles, 'dsh-prompt-rewriter: styles');
|
|
1723
1847
|
ctx.effect(
|
|
@@ -1835,7 +1959,12 @@ window.__ModuleLoader__.load({
|
|
|
1835
1959
|
// The registered component is the section itself, bound to this context.
|
|
1836
1960
|
// Keeping ONE stable component identity (no per-render wrapper) means the
|
|
1837
1961
|
// owner mounts a stable type and the section owns its own hook slots.
|
|
1838
|
-
|
|
1962
|
+
// Resolved through `ctx.get` instead of `ctx.shortcuts`: once `shortcuts`
|
|
1963
|
+
// leaves the inject list the ctx proxy no longer exposes it as a property,
|
|
1964
|
+
// describing then throws. A thunk also lets the section re-read the face
|
|
1965
|
+
// after the host finishes mounting it.
|
|
1966
|
+
const getShortcuts = () => (typeof ctx.get === 'function' ? ctx.get('shortcuts') : ctx.shortcuts);
|
|
1967
|
+
const Section = (props) => h(SettingsSection, { ...props, t, getRemote: getSessionRemote, getShortcuts });
|
|
1839
1968
|
ctx.slots.inject('settings.section', () =>
|
|
1840
1969
|
ctx.slots.register(
|
|
1841
1970
|
{ name: 'settings.section', id: 'prompt-enhance', order: 50, label: () => t('nav'), locale: NS },
|
|
@@ -1864,7 +1993,17 @@ window.__ModuleLoader__.load({
|
|
|
1864
1993
|
// three specific punctuation chords,
|
|
1865
1994
|
// so the combination must carry three modifiers AND include Alt.
|
|
1866
1995
|
// Ctrl+Shift+Alt+Enter satisfies both and is unused by the host.
|
|
1867
|
-
|
|
1996
|
+
// Registration waits for the host shortcut service WITHOUT gating activation.
|
|
1997
|
+
//
|
|
1998
|
+
// `ctx.get(name)` does not bypass the inject gate: a service outside `inject`
|
|
1999
|
+
// resolves to an Error object, so the previous guard tripped and the command
|
|
2000
|
+
// was never registered (the host catalog listed its own commands, none ours).
|
|
2001
|
+
// Every name in `inject` is REQUIRED for activation, so listing `shortcuts`
|
|
2002
|
+
// there would keep the whole plugin dormant without a keyboard bridge.
|
|
2003
|
+
// `ctx.inject([...], cb)` is the middle path: it waits for the service and
|
|
2004
|
+
// leaves the plugin active either way.
|
|
2005
|
+
ctx.inject(['shortcuts'], (scope) => {
|
|
2006
|
+
scope.effect(() => {
|
|
1868
2007
|
try {
|
|
1869
2008
|
// One implementation, reached from two places: `resolve` is what dispatch
|
|
1870
2009
|
// invokes, `run` is kept for any caller that uses the command object directly.
|
|
@@ -1884,7 +2023,11 @@ window.__ModuleLoader__.load({
|
|
|
1884
2023
|
trigger();
|
|
1885
2024
|
}
|
|
1886
2025
|
};
|
|
1887
|
-
|
|
2026
|
+
// No service means no host-owned binding: the fallback listener below
|
|
2027
|
+
// still runs the command, so the key keeps working where it can.
|
|
2028
|
+
const shortcutsFace = scope.shortcuts;
|
|
2029
|
+
if (typeof shortcutsFace?.register !== 'function') return undefined;
|
|
2030
|
+
const dispose = shortcutsFace.register({
|
|
1888
2031
|
id: 'prompt-enhance.run',
|
|
1889
2032
|
label: () => t('shortcut.run'),
|
|
1890
2033
|
aliases: ['prompt enhance', 'enhance prompt', 'rewrite prompt'],
|
|
@@ -1925,12 +2068,13 @@ window.__ModuleLoader__.load({
|
|
|
1925
2068
|
modals: [],
|
|
1926
2069
|
});
|
|
1927
2070
|
return dispose;
|
|
1928
|
-
} catch {
|
|
2071
|
+
} catch (error) {
|
|
1929
2072
|
// A throw here is swallowed by the effect wrapper; the keydown fallback
|
|
1930
2073
|
// below still serves the shortcut, so it must not fail activation.
|
|
1931
2074
|
return undefined;
|
|
1932
2075
|
}
|
|
1933
2076
|
}, 'dsh-prompt-rewriter: shortcut');
|
|
2077
|
+
});
|
|
1934
2078
|
|
|
1935
2079
|
// Keyboard fallback.
|
|
1936
2080
|
//
|
|
@@ -1951,7 +2095,10 @@ window.__ModuleLoader__.load({
|
|
|
1951
2095
|
/** The binding the host currently resolves for our command, if any. */
|
|
1952
2096
|
const resolvedBinding = () => {
|
|
1953
2097
|
try {
|
|
1954
|
-
|
|
2098
|
+
// A missing service just means "no host binding to compare against";
|
|
2099
|
+
// the fallback listener then relies on its own defaults.
|
|
2100
|
+
const face = typeof ctx.get === 'function' ? ctx.get('shortcuts') : ctx.shortcuts;
|
|
2101
|
+
const snapshot = readSnapshot(face?.catalog, 'commands');
|
|
1955
2102
|
const row = Array.isArray(snapshot)
|
|
1956
2103
|
? snapshot.find((entry) => entry.id === 'prompt-enhance.run')
|
|
1957
2104
|
: undefined;
|
|
@@ -2017,7 +2164,7 @@ window.__ModuleLoader__.load({
|
|
|
2017
2164
|
|
|
2018
2165
|
let candidate = fromEvent(event);
|
|
2019
2166
|
try {
|
|
2020
|
-
const described =
|
|
2167
|
+
const described = typeof face?.describeBinding === 'function' ? face.describeBinding(candidate) : undefined;
|
|
2021
2168
|
if (described?.binding !== undefined && described?.binding !== null) candidate = described.binding;
|
|
2022
2169
|
} catch {
|
|
2023
2170
|
// Keep the symbolic form; `same` compares modifier sets.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED