dsh-prompt-rewriter 1.6.2 → 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 +11 -0
- package/client.js +149 -31
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
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.
|
|
5
16
|
## 1.6.2
|
|
6
17
|
|
|
7
18
|
**中文**
|
package/client.js
CHANGED
|
@@ -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
|
*
|
|
@@ -1188,6 +1189,61 @@ window.__ModuleLoader__.load({
|
|
|
1188
1189
|
/** The command id this plugin registers with the host shortcuts service. */
|
|
1189
1190
|
const SHORTCUT_ID = 'prompt-enhance.run';
|
|
1190
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
|
+
}
|
|
1191
1247
|
/**
|
|
1192
1248
|
* Read and edit this plugin's keyboard binding through the host shortcuts service.
|
|
1193
1249
|
*
|
|
@@ -1200,7 +1256,11 @@ window.__ModuleLoader__.load({
|
|
|
1200
1256
|
* @param t - localized translator, for host-reported failure states.
|
|
1201
1257
|
* @returns `{ keys, isDefault, assign, reset, refresh }`.
|
|
1202
1258
|
*/
|
|
1203
|
-
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;
|
|
1204
1264
|
const [state, setState] = useState({ keys: '', isDefault: true, revision: undefined });
|
|
1205
1265
|
const alive = useRef(false);
|
|
1206
1266
|
alive.current = true;
|
|
@@ -1209,10 +1269,11 @@ window.__ModuleLoader__.load({
|
|
|
1209
1269
|
const refresh = useCallback(() => {
|
|
1210
1270
|
if (shortcuts === undefined) return;
|
|
1211
1271
|
try {
|
|
1212
|
-
const snapshot = shortcuts
|
|
1213
|
-
const row =
|
|
1214
|
-
const
|
|
1215
|
-
|
|
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.
|
|
1216
1277
|
if (row === undefined) return;
|
|
1217
1278
|
setState({
|
|
1218
1279
|
// `keys` is the host's own display form of the resolved binding.
|
|
@@ -1236,10 +1297,7 @@ window.__ModuleLoader__.load({
|
|
|
1236
1297
|
* refuses it with `if (revision !== this.snapshot.revision) return result("stale")`.
|
|
1237
1298
|
* That is why 恢复默认 did nothing and the old override kept showing.
|
|
1238
1299
|
*/
|
|
1239
|
-
const revisionOf = useCallback(() =>
|
|
1240
|
-
const config = shortcuts?.config?.getSnapshot?.() ?? shortcuts?.config;
|
|
1241
|
-
return config?.revision;
|
|
1242
|
-
}, [shortcuts]);
|
|
1300
|
+
const revisionOf = useCallback(() => readRevision(shortcuts), [shortcuts]);
|
|
1243
1301
|
|
|
1244
1302
|
/**
|
|
1245
1303
|
* Submit one edit against the live revision, retrying once on "stale".
|
|
@@ -1250,23 +1308,48 @@ window.__ModuleLoader__.load({
|
|
|
1250
1308
|
* and then trying to reset it.
|
|
1251
1309
|
*/
|
|
1252
1310
|
const applyEdit = useCallback(async (operation) => {
|
|
1253
|
-
if (shortcuts
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
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' };
|
|
1258
1325
|
}
|
|
1259
|
-
if (result?.status === 'saved') refresh();
|
|
1260
|
-
return result;
|
|
1261
1326
|
}, [refresh, revisionOf, shortcuts]);
|
|
1262
1327
|
|
|
1263
1328
|
useEffect(() => {
|
|
1264
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);
|
|
1265
1340
|
// The host publishes catalog changes; following them keeps this row honest
|
|
1266
1341
|
// when a binding is changed from the general Settings page instead.
|
|
1267
|
-
|
|
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
|
+
}
|
|
1268
1350
|
return () => {
|
|
1269
1351
|
alive.current = false;
|
|
1352
|
+
clearInterval(retry);
|
|
1270
1353
|
if (typeof unsubscribe === 'function') unsubscribe();
|
|
1271
1354
|
};
|
|
1272
1355
|
}, [refresh, shortcuts]);
|
|
@@ -1281,19 +1364,20 @@ window.__ModuleLoader__.load({
|
|
|
1281
1364
|
* @returns a promise resolving to an error message, or null on success.
|
|
1282
1365
|
*/
|
|
1283
1366
|
const assign = useCallback(async (binding) => {
|
|
1284
|
-
if (shortcuts
|
|
1367
|
+
if (!usableShortcuts(shortcuts)) return t('settings.shortcut.unavailable');
|
|
1285
1368
|
try {
|
|
1286
1369
|
// The host exposes the collision report for a candidate without applying
|
|
1287
1370
|
// it; naming the colliding commands is what makes the failure readable.
|
|
1288
|
-
const described = shortcuts.describeBinding
|
|
1371
|
+
const described = typeof shortcuts.describeBinding === 'function' ? shortcuts.describeBinding(binding) : undefined;
|
|
1289
1372
|
if (described?.issue !== null && described?.issue !== undefined) {
|
|
1290
1373
|
return t('settings.shortcut.rejected', { reason: described.issue });
|
|
1291
1374
|
}
|
|
1292
1375
|
const conflicts = (described?.conflicts ?? [])
|
|
1293
1376
|
.filter((id) => id !== SHORTCUT_ID)
|
|
1294
1377
|
.map((id) => {
|
|
1295
|
-
|
|
1296
|
-
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);
|
|
1297
1381
|
const label = typeof row?.label === 'function' ? row.label() : row?.label;
|
|
1298
1382
|
return label ?? id;
|
|
1299
1383
|
});
|
|
@@ -1319,7 +1403,7 @@ window.__ModuleLoader__.load({
|
|
|
1319
1403
|
* @returns a promise resolving to an error message, or null on success.
|
|
1320
1404
|
*/
|
|
1321
1405
|
const reset = useCallback(async () => {
|
|
1322
|
-
if (shortcuts
|
|
1406
|
+
if (!usableShortcuts(shortcuts)) return t('settings.shortcut.unavailable');
|
|
1323
1407
|
try {
|
|
1324
1408
|
const result = await applyEdit({ type: 'reset', id: SHORTCUT_ID });
|
|
1325
1409
|
if (result !== undefined && result?.status !== 'saved') {
|
|
@@ -1345,7 +1429,7 @@ window.__ModuleLoader__.load({
|
|
|
1345
1429
|
* @param props.t - localized translator.
|
|
1346
1430
|
* @param props.getRemote - thunk returning the injected `remote.session` face.
|
|
1347
1431
|
*/
|
|
1348
|
-
function SettingsSection({ t, getRemote,
|
|
1432
|
+
function SettingsSection({ t, getRemote, getShortcuts }) {
|
|
1349
1433
|
const [values, setValues] = useState(null);
|
|
1350
1434
|
// `{ version, url }` reported by the Host from the package manifest. Never
|
|
1351
1435
|
// hard-coded here: a release only bumps package.json.
|
|
@@ -1465,7 +1549,9 @@ window.__ModuleLoader__.load({
|
|
|
1465
1549
|
// it after that return meant the loading render used fewer hooks than the
|
|
1466
1550
|
// loaded render, which React rejects with "Rendered more hooks than during the
|
|
1467
1551
|
// previous render" — and the settings page went completely blank.
|
|
1468
|
-
|
|
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);
|
|
1469
1555
|
|
|
1470
1556
|
if (values === null) {
|
|
1471
1557
|
return h(
|
|
@@ -1746,7 +1832,16 @@ window.__ModuleLoader__.load({
|
|
|
1746
1832
|
// detection and the user's binding overrides. Registering through it — instead
|
|
1747
1833
|
// of adding our own keydown listener — is what makes a custom binding persist
|
|
1748
1834
|
// across restarts and collide visibly with a host command.
|
|
1749
|
-
|
|
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'],
|
|
1750
1845
|
apply(ctx) {
|
|
1751
1846
|
ctx.effect(installStyles, 'dsh-prompt-rewriter: styles');
|
|
1752
1847
|
ctx.effect(
|
|
@@ -1864,7 +1959,12 @@ window.__ModuleLoader__.load({
|
|
|
1864
1959
|
// The registered component is the section itself, bound to this context.
|
|
1865
1960
|
// Keeping ONE stable component identity (no per-render wrapper) means the
|
|
1866
1961
|
// owner mounts a stable type and the section owns its own hook slots.
|
|
1867
|
-
|
|
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 });
|
|
1868
1968
|
ctx.slots.inject('settings.section', () =>
|
|
1869
1969
|
ctx.slots.register(
|
|
1870
1970
|
{ name: 'settings.section', id: 'prompt-enhance', order: 50, label: () => t('nav'), locale: NS },
|
|
@@ -1893,7 +1993,17 @@ window.__ModuleLoader__.load({
|
|
|
1893
1993
|
// three specific punctuation chords,
|
|
1894
1994
|
// so the combination must carry three modifiers AND include Alt.
|
|
1895
1995
|
// Ctrl+Shift+Alt+Enter satisfies both and is unused by the host.
|
|
1896
|
-
|
|
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(() => {
|
|
1897
2007
|
try {
|
|
1898
2008
|
// One implementation, reached from two places: `resolve` is what dispatch
|
|
1899
2009
|
// invokes, `run` is kept for any caller that uses the command object directly.
|
|
@@ -1913,7 +2023,11 @@ window.__ModuleLoader__.load({
|
|
|
1913
2023
|
trigger();
|
|
1914
2024
|
}
|
|
1915
2025
|
};
|
|
1916
|
-
|
|
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({
|
|
1917
2031
|
id: 'prompt-enhance.run',
|
|
1918
2032
|
label: () => t('shortcut.run'),
|
|
1919
2033
|
aliases: ['prompt enhance', 'enhance prompt', 'rewrite prompt'],
|
|
@@ -1954,12 +2068,13 @@ window.__ModuleLoader__.load({
|
|
|
1954
2068
|
modals: [],
|
|
1955
2069
|
});
|
|
1956
2070
|
return dispose;
|
|
1957
|
-
} catch {
|
|
2071
|
+
} catch (error) {
|
|
1958
2072
|
// A throw here is swallowed by the effect wrapper; the keydown fallback
|
|
1959
2073
|
// below still serves the shortcut, so it must not fail activation.
|
|
1960
2074
|
return undefined;
|
|
1961
2075
|
}
|
|
1962
2076
|
}, 'dsh-prompt-rewriter: shortcut');
|
|
2077
|
+
});
|
|
1963
2078
|
|
|
1964
2079
|
// Keyboard fallback.
|
|
1965
2080
|
//
|
|
@@ -1980,7 +2095,10 @@ window.__ModuleLoader__.load({
|
|
|
1980
2095
|
/** The binding the host currently resolves for our command, if any. */
|
|
1981
2096
|
const resolvedBinding = () => {
|
|
1982
2097
|
try {
|
|
1983
|
-
|
|
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');
|
|
1984
2102
|
const row = Array.isArray(snapshot)
|
|
1985
2103
|
? snapshot.find((entry) => entry.id === 'prompt-enhance.run')
|
|
1986
2104
|
: undefined;
|
|
@@ -2046,7 +2164,7 @@ window.__ModuleLoader__.load({
|
|
|
2046
2164
|
|
|
2047
2165
|
let candidate = fromEvent(event);
|
|
2048
2166
|
try {
|
|
2049
|
-
const described =
|
|
2167
|
+
const described = typeof face?.describeBinding === 'function' ? face.describeBinding(candidate) : undefined;
|
|
2050
2168
|
if (described?.binding !== undefined && described?.binding !== null) candidate = described.binding;
|
|
2051
2169
|
} catch {
|
|
2052
2170
|
// Keep the symbolic form; `same` compares modifier sets.
|
package/package.json
CHANGED