@huanlin/dsh-plugin-input-history 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -115
- package/lib/client.js +641 -641
- package/lib/types/client/HistoryDock.d.ts +43 -43
- package/lib/types/client/HistoryDock.js +187 -187
- package/lib/types/client/dom.d.ts +81 -81
- package/lib/types/client/dom.js +142 -142
- package/lib/types/client/index.d.ts +34 -34
- package/lib/types/client/index.js +62 -62
- package/lib/types/index.d.ts +24 -24
- package/lib/types/index.js +25 -25
- package/package.json +6 -6
package/lib/client.js
CHANGED
|
@@ -1,642 +1,642 @@
|
|
|
1
|
-
window.__ModuleLoader__.load({ id: "@huanlin/dsh-plugin-input-history", factory: (require) => {
|
|
2
|
-
var module = { exports: {} }; var exports = module.exports;
|
|
3
|
-
//#region rolldown:runtime
|
|
4
|
-
var __create = Object.create;
|
|
5
|
-
var __defProp = Object.defineProperty;
|
|
6
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
7
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
8
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
9
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
10
|
-
var __copyProps = (to, from, except, desc) => {
|
|
11
|
-
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
12
|
-
key = keys[i];
|
|
13
|
-
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
14
|
-
get: ((k) => from[k]).bind(null, key),
|
|
15
|
-
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
return to;
|
|
19
|
-
};
|
|
20
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
21
|
-
value: mod,
|
|
22
|
-
enumerable: true
|
|
23
|
-
}) : target, mod));
|
|
24
|
-
|
|
25
|
-
//#endregion
|
|
26
|
-
let react = require("react");
|
|
27
|
-
react = __toESM(react);
|
|
28
|
-
let react_jsx_runtime = require("react/jsx-runtime");
|
|
29
|
-
react_jsx_runtime = __toESM(react_jsx_runtime);
|
|
30
|
-
|
|
31
|
-
//#region src/client/history.ts
|
|
32
|
-
/**
|
|
33
|
-
* Prompt history store — pure functions over a string array.
|
|
34
|
-
*
|
|
35
|
-
* The store is a FIFO list of unique prompt strings, persisted to
|
|
36
|
-
* `localStorage`. Newest entries are at the end of the array. The
|
|
37
|
-
* navigation cursor walks backwards from the end (ArrowUp = older,
|
|
38
|
-
* ArrowDown = newer).
|
|
39
|
-
*
|
|
40
|
-
* The functions in this module are pure (no `localStorage` access) so
|
|
41
|
-
* they can be unit-tested without jsdom. The `HistoryStore` class below
|
|
42
|
-
* wires them to `localStorage` with try/catch containment — a quota
|
|
43
|
-
* exception or a disabled storage (private mode) degrades gracefully to
|
|
44
|
-
* an in-memory list that lives for the page lifetime.
|
|
45
|
-
*
|
|
46
|
-
* @module @huanlin/dsh-plugin-input-history/client/history
|
|
47
|
-
*/
|
|
48
|
-
/** localStorage key (versioned; bump on schema changes to start fresh). */
|
|
49
|
-
const STORAGE_KEY = "dsh-plugin-input-history:v1";
|
|
50
|
-
/** Default capacity when none is configured. */
|
|
51
|
-
const DEFAULT_CAPACITY = 500;
|
|
52
|
-
/**
|
|
53
|
-
* Append a prompt to the history.
|
|
54
|
-
*
|
|
55
|
-
* Rules:
|
|
56
|
-
* - Empty / whitespace-only strings are ignored (the InputBar already
|
|
57
|
-
* rejects them at submit, but defensive).
|
|
58
|
-
* - When the new entry equals the most recent one, it is a no-op
|
|
59
|
-
* (avoids stacking duplicates from rapid resends).
|
|
60
|
-
* - When the new entry already exists earlier in the history, that
|
|
61
|
-
* earlier occurrence is removed (recency wins; the prompt moves to
|
|
62
|
-
* the end). This mirrors terminal shell behaviour.
|
|
63
|
-
* - When the array would exceed `capacity`, the oldest entries are
|
|
64
|
-
* dropped from the front (FIFO).
|
|
65
|
-
*
|
|
66
|
-
* @param history - the current history array (newest at end).
|
|
67
|
-
* @param prompt - the prompt to append.
|
|
68
|
-
* @param capacity - the maximum number of entries to retain.
|
|
69
|
-
* @returns the new history array (may be the same reference if no-op).
|
|
70
|
-
*/
|
|
71
|
-
function appendHistory(history, prompt, capacity = DEFAULT_CAPACITY) {
|
|
72
|
-
const trimmed = prompt.trim();
|
|
73
|
-
if (trimmed === "") return history;
|
|
74
|
-
const lastIndex = history.lastIndexOf(trimmed);
|
|
75
|
-
if (lastIndex !== -1 && lastIndex === history.length - 1 && history.indexOf(trimmed) === lastIndex) return history;
|
|
76
|
-
const filtered = history.filter((item) => item !== trimmed);
|
|
77
|
-
filtered.push(trimmed);
|
|
78
|
-
const cap = Math.max(1, capacity);
|
|
79
|
-
if (filtered.length > cap) return filtered.slice(filtered.length - cap);
|
|
80
|
-
return filtered;
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Navigation cursor for walking the history.
|
|
84
|
-
*
|
|
85
|
-
* The cursor is `null` when the user is not navigating (i.e. they are
|
|
86
|
-
* typing a fresh draft). ArrowUp sets it to the last index, then
|
|
87
|
-
* decrements; ArrowDown increments; when it would exceed `history.length
|
|
88
|
-
* - 1`, it returns to `null` (meaning "restore the in-progress draft").
|
|
89
|
-
*
|
|
90
|
-
* @param current - the current cursor (null = not navigating).
|
|
91
|
-
* @param total - the total number of history entries.
|
|
92
|
-
* @param dir - `'up'` (older) or `'down'` (newer).
|
|
93
|
-
* @returns the next cursor, or `null` when navigation falls off the
|
|
94
|
-
* newest end (caller should restore the saved draft).
|
|
95
|
-
*/
|
|
96
|
-
function nextIndex(current, total, dir) {
|
|
97
|
-
if (total === 0) return null;
|
|
98
|
-
if (dir === "up") {
|
|
99
|
-
if (current === null) return total - 1;
|
|
100
|
-
if (current <= 0) return 0;
|
|
101
|
-
return current - 1;
|
|
102
|
-
}
|
|
103
|
-
if (current === null) return null;
|
|
104
|
-
if (current >= total - 1) return null;
|
|
105
|
-
return current + 1;
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* Read the history entry at a cursor, or `null` when the cursor is null.
|
|
109
|
-
*
|
|
110
|
-
* @param history - the history array.
|
|
111
|
-
* @param cursor - the navigation cursor.
|
|
112
|
-
* @returns the prompt at the cursor, or `null`.
|
|
113
|
-
*/
|
|
114
|
-
function entryAt(history, cursor) {
|
|
115
|
-
if (cursor === null) return null;
|
|
116
|
-
if (cursor < 0 || cursor >= history.length) return null;
|
|
117
|
-
return history[cursor] ?? null;
|
|
118
|
-
}
|
|
119
|
-
/**
|
|
120
|
-
* History store bound to `localStorage`.
|
|
121
|
-
*
|
|
122
|
-
* The store reads once on construction (or on `reload()`) and keeps an
|
|
123
|
-
* in-memory copy. Writes go to both memory and `localStorage` inside a
|
|
124
|
-
* try/catch — a quota exception leaves the in-memory copy authoritative
|
|
125
|
-
* for the rest of the page lifetime. This trades cross-tab consistency
|
|
126
|
-
* for resilience: the store never throws on a write, and the worst case
|
|
127
|
-
* is that a tab keeps its own view until refresh.
|
|
128
|
-
*
|
|
129
|
-
* Cross-tab sync is intentionally NOT implemented: prompt history is
|
|
130
|
-
* append-mostly and a stale read across tabs is harmless (the next
|
|
131
|
-
* append corrects it). Listening to the `storage` event would add
|
|
132
|
-
* reactivity that the navigation UI does not need.
|
|
133
|
-
*/
|
|
134
|
-
var HistoryStore = class {
|
|
135
|
-
items;
|
|
136
|
-
storage;
|
|
137
|
-
key;
|
|
138
|
-
/**
|
|
139
|
-
* @param capacity - maximum entries to retain (FIFO).
|
|
140
|
-
* @param storage - the storage backend (defaults to `localStorage` when available).
|
|
141
|
-
* @param key - the storage key (defaults to {@link STORAGE_KEY}).
|
|
142
|
-
*/
|
|
143
|
-
constructor(capacity = DEFAULT_CAPACITY, storage, key = STORAGE_KEY) {
|
|
144
|
-
this.capacity = capacity;
|
|
145
|
-
this.storage = storage ?? safeLocalStorage();
|
|
146
|
-
this.key = key;
|
|
147
|
-
this.items = this.readFromStorage();
|
|
148
|
-
}
|
|
149
|
-
/** Current history snapshot (newest at end). */
|
|
150
|
-
get list() {
|
|
151
|
-
return this.items;
|
|
152
|
-
}
|
|
153
|
-
/** Number of entries currently stored. */
|
|
154
|
-
get length() {
|
|
155
|
-
return this.items.length;
|
|
156
|
-
}
|
|
157
|
-
/** Reload from storage (e.g. after a suspected external edit). Truncates to the current capacity. */
|
|
158
|
-
reload() {
|
|
159
|
-
const loaded = this.readFromStorage();
|
|
160
|
-
const cap = Math.max(1, this.capacity);
|
|
161
|
-
this.items = loaded.length > cap ? loaded.slice(loaded.length - cap) : loaded;
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* Append a prompt and persist. See {@link appendHistory} for rules.
|
|
165
|
-
* @returns the new history snapshot.
|
|
166
|
-
*/
|
|
167
|
-
append(prompt) {
|
|
168
|
-
this.items = appendHistory(this.items, prompt, this.capacity);
|
|
169
|
-
this.writeToStorage();
|
|
170
|
-
return this.items;
|
|
171
|
-
}
|
|
172
|
-
/** Clear all history (used by tests and a future "clear" UI). */
|
|
173
|
-
clear() {
|
|
174
|
-
this.items = [];
|
|
175
|
-
this.writeToStorage();
|
|
176
|
-
}
|
|
177
|
-
readFromStorage() {
|
|
178
|
-
if (this.storage === null) return [];
|
|
179
|
-
try {
|
|
180
|
-
const raw = this.storage.getItem(this.key);
|
|
181
|
-
if (raw === null) return [];
|
|
182
|
-
const parsed = JSON.parse(raw);
|
|
183
|
-
if (!Array.isArray(parsed)) return [];
|
|
184
|
-
return parsed.filter((item) => typeof item === "string");
|
|
185
|
-
} catch {
|
|
186
|
-
return [];
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
writeToStorage() {
|
|
190
|
-
if (this.storage === null) return;
|
|
191
|
-
try {
|
|
192
|
-
this.storage.setItem(this.key, JSON.stringify(this.items));
|
|
193
|
-
} catch {}
|
|
194
|
-
}
|
|
195
|
-
};
|
|
196
|
-
/** Safe accessor for `localStorage` that returns null on any failure. */
|
|
197
|
-
function safeLocalStorage() {
|
|
198
|
-
try {
|
|
199
|
-
if (typeof localStorage === "undefined") return null;
|
|
200
|
-
return localStorage;
|
|
201
|
-
} catch {
|
|
202
|
-
return null;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
//#endregion
|
|
207
|
-
//#region src/client/dom.ts
|
|
208
|
-
/**
|
|
209
|
-
* Pure decision over caret geometry: where a caret resting at `caretTop`
|
|
210
|
-
* sits relative to the box whose visual line tops are `lineTops` (ascending,
|
|
211
|
-
* one entry per visual line, viewport coordinates).
|
|
212
|
-
*
|
|
213
|
-
* @param caretTop - viewport `top` of the collapsed caret's box.
|
|
214
|
-
* @param lineTops - viewport `top` of each visual line, ascending.
|
|
215
|
-
* @param tolerance - px slop absorbing subpixel rounding between the caret
|
|
216
|
-
* rect and its line's rect.
|
|
217
|
-
* @returns the boundary flags; an empty `lineTops` (empty editable) is
|
|
218
|
-
* treated as a single virtual line, so both flags are true.
|
|
219
|
-
*/
|
|
220
|
-
function boundaryFromLineTops(caretTop, lineTops, tolerance) {
|
|
221
|
-
if (lineTops.length === 0) return {
|
|
222
|
-
atFirstLine: true,
|
|
223
|
-
atLastLine: true
|
|
224
|
-
};
|
|
225
|
-
return {
|
|
226
|
-
atFirstLine: caretTop <= lineTops[0] + tolerance,
|
|
227
|
-
atLastLine: caretTop >= lineTops[lineTops.length - 1] - tolerance
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
/**
|
|
231
|
-
* Locate the DSH composer editable the event targeted.
|
|
232
|
-
*
|
|
233
|
-
* Walks from the event target up to the closest `[data-composer-card]`
|
|
234
|
-
* ancestor, queries the `[data-composer-input]` contenteditable inside it,
|
|
235
|
-
* and confirms the target sits inside that editable (keystrokes on the
|
|
236
|
-
* card's buttons and chrome do not navigate history). Returns `null` when
|
|
237
|
-
* the target is not inside the composer editable.
|
|
238
|
-
*
|
|
239
|
-
* @param from - the event target (or any node inside the composer editable).
|
|
240
|
-
* @returns the editable element, or `null` when not found.
|
|
241
|
-
*/
|
|
242
|
-
function findComposerEditable(from) {
|
|
243
|
-
if (typeof document === "undefined") return null;
|
|
244
|
-
if (from === null || !(from instanceof Element)) return null;
|
|
245
|
-
const card = from.closest("[data-composer-card]");
|
|
246
|
-
if (card === null) return null;
|
|
247
|
-
const editable = card.querySelector("[data-composer-input]");
|
|
248
|
-
if (editable === null) return null;
|
|
249
|
-
return editable.contains(from) ? editable : null;
|
|
250
|
-
}
|
|
251
|
-
/**
|
|
252
|
-
* Detect an open trigger (slash-command / @-mention) menu inside the
|
|
253
|
-
* composer card that owns `editable`.
|
|
254
|
-
*
|
|
255
|
-
* While the menu is open, ArrowUp/ArrowDown move the highlighted row and
|
|
256
|
-
* must not recall history. The menu renders inside the same
|
|
257
|
-
* `[data-composer-card]` as the editable and carries the stable
|
|
258
|
-
* `data-trigger-menu` marker.
|
|
259
|
-
*
|
|
260
|
-
* @param editable - the composer editable element.
|
|
261
|
-
* @returns the menu element, or `null` when no menu is open.
|
|
262
|
-
*/
|
|
263
|
-
function findTriggerMenu(editable) {
|
|
264
|
-
const card = editable.closest("[data-composer-card]");
|
|
265
|
-
return card === null ? null : card.querySelector("[data-trigger-menu]");
|
|
266
|
-
}
|
|
267
|
-
/**
|
|
268
|
-
* Decide the collapsed caret's line boundary inside the composer editable.
|
|
269
|
-
*
|
|
270
|
-
* Compares the caret's viewport box against the editable content's visual
|
|
271
|
-
* line boxes (`Range.getClientRects()` yields one rect per line fragment;
|
|
272
|
-
* fragments of the same visual line share a top within subpixel slop, so
|
|
273
|
-
* tops are deduped with a 2px threshold). A non-collapsed selection and a
|
|
274
|
-
* geometry-less environment (headless/jsdom) both return `null`, which the
|
|
275
|
-
* caller must treat as "do not navigate".
|
|
276
|
-
*
|
|
277
|
-
* @param editable - the composer editable element.
|
|
278
|
-
* @param tolerance - px slop between the caret rect and its line rect
|
|
279
|
-
* (defaults to 4px).
|
|
280
|
-
* @returns the boundary flags, or `null` when they cannot be determined.
|
|
281
|
-
*/
|
|
282
|
-
function caretLineBoundary(editable, tolerance = 4) {
|
|
283
|
-
const selection = window.getSelection();
|
|
284
|
-
if (selection === null || selection.rangeCount === 0) return null;
|
|
285
|
-
if (!selection.isCollapsed) return null;
|
|
286
|
-
const caretTop = caretTopOf(selection);
|
|
287
|
-
if (caretTop === null) return null;
|
|
288
|
-
const lineTops = contentLineTops(editable);
|
|
289
|
-
if (lineTops === null) return null;
|
|
290
|
-
return boundaryFromLineTops(caretTop, lineTops, tolerance);
|
|
291
|
-
}
|
|
292
|
-
/** Viewport `top` of the collapsed caret's box, or `null` when unmeasurable. */
|
|
293
|
-
function caretTopOf(selection) {
|
|
294
|
-
const rects = selection.getRangeAt(0).getClientRects();
|
|
295
|
-
for (let i = 0; i < rects.length; i++) {
|
|
296
|
-
const rect = rects[i];
|
|
297
|
-
if (rect.height === 0 && rect.width === 0) continue;
|
|
298
|
-
return rect.top;
|
|
299
|
-
}
|
|
300
|
-
const anchor = selection.anchorNode;
|
|
301
|
-
const el = anchor instanceof HTMLElement ? anchor : anchor?.parentElement;
|
|
302
|
-
return el === void 0 || el === null ? null : el.getBoundingClientRect().top;
|
|
303
|
-
}
|
|
304
|
-
/** Ascending, deduped tops of the editable content's visual lines; `null` without geometry. Empty for an empty editable. */
|
|
305
|
-
function contentLineTops(editable) {
|
|
306
|
-
const range = document.createRange();
|
|
307
|
-
range.selectNodeContents(editable);
|
|
308
|
-
const rects = range.getClientRects();
|
|
309
|
-
const tops = [];
|
|
310
|
-
for (let i = 0; i < rects.length; i++) {
|
|
311
|
-
const rect = rects[i];
|
|
312
|
-
if (rect.height === 0 && rect.width === 0) continue;
|
|
313
|
-
const top = rect.top;
|
|
314
|
-
if (tops.length === 0 || Math.abs(top - tops[tops.length - 1]) > 2) tops.push(top);
|
|
315
|
-
}
|
|
316
|
-
return tops;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
//#endregion
|
|
320
|
-
//#region src/client/ime.ts
|
|
321
|
-
/**
|
|
322
|
-
* IME-composition key guard.
|
|
323
|
-
*
|
|
324
|
-
* While a Chinese/Japanese/Korean input method is composing (the user is
|
|
325
|
-
* picking a candidate from the IME window), every pressed key BELONGS to
|
|
326
|
-
* the input method: arrows move the candidate highlight, Enter/Space
|
|
327
|
-
* confirm the composition, Escape cancels it. Page code must not process
|
|
328
|
-
* those keys — a history-navigation handler that calls `preventDefault()`
|
|
329
|
-
* on ArrowUp/ArrowDown during composition would silently break the IME:
|
|
330
|
-
* candidates stop responding, the composition gets torn apart, and only
|
|
331
|
-
* bare letters come out.
|
|
332
|
-
*
|
|
333
|
-
* The composition signal follows the DSH core convention (InputBar's IME
|
|
334
|
-
* guard, issue #535): `isComposing` for modern engines, keyCode 229 as
|
|
335
|
-
* the legacy signal engines emit without isComposing.
|
|
336
|
-
*
|
|
337
|
-
* @module @huanlin/dsh-plugin-input-history/client/ime
|
|
338
|
-
*/
|
|
339
|
-
/** The pure decision: is this keyboard event part of an IME composition? */
|
|
340
|
-
function isImeComposition(event) {
|
|
341
|
-
return event.isComposing || event.keyCode === 229;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
//#endregion
|
|
345
|
-
//#region src/client/HistoryDock.tsx
|
|
346
|
-
/**
|
|
347
|
-
* Module-scope history store, initialized once on first dock mount.
|
|
348
|
-
* Shared across dock mount/unmount cycles; the underlying data persists
|
|
349
|
-
* in `localStorage`.
|
|
350
|
-
*/
|
|
351
|
-
let historyStore = null;
|
|
352
|
-
/** Get the shared history store (initializes lazily on first call). */
|
|
353
|
-
function getHistoryStore() {
|
|
354
|
-
if (historyStore === null) historyStore = new HistoryStore(DEFAULT_CAPACITY);
|
|
355
|
-
return historyStore;
|
|
356
|
-
}
|
|
357
|
-
/**
|
|
358
|
-
* Render the invisible history dock entry: collection + navigation.
|
|
359
|
-
*
|
|
360
|
-
* @param props - dock runtime share (standard hooks) + locale seat.
|
|
361
|
-
* @returns an `aria-hidden` anchor with zero layout footprint.
|
|
362
|
-
*/
|
|
363
|
-
function HistoryDock({ useInput, useChat, inputActions, sessionId }) {
|
|
364
|
-
const input = useInput((s) => s);
|
|
365
|
-
const nodes = useChat((s) => s.legacy.nodes);
|
|
366
|
-
const lastText = (0, react.useMemo)(() => latestUserOrSteeringText(nodes), [nodes]);
|
|
367
|
-
(0, react.useEffect)(() => {
|
|
368
|
-
if (lastText !== null) getHistoryStore().append(lastText);
|
|
369
|
-
}, [lastText]);
|
|
370
|
-
const navCursorRef = (0, react.useRef)(null);
|
|
371
|
-
const savedDraftRef = (0, react.useRef)(null);
|
|
372
|
-
const prevSessionRef = (0, react.useRef)(sessionId);
|
|
373
|
-
if (prevSessionRef.current !== sessionId) {
|
|
374
|
-
prevSessionRef.current = sessionId;
|
|
375
|
-
navCursorRef.current = null;
|
|
376
|
-
savedDraftRef.current = null;
|
|
377
|
-
}
|
|
378
|
-
const inputRef = (0, react.useRef)(input);
|
|
379
|
-
inputRef.current = input;
|
|
380
|
-
const actionsRef = (0, react.useRef)(inputActions);
|
|
381
|
-
actionsRef.current = inputActions;
|
|
382
|
-
(0, react.useEffect)(() => {
|
|
383
|
-
if (typeof document === "undefined") return void 0;
|
|
384
|
-
const handler = (event) => {
|
|
385
|
-
if (event.key !== "ArrowUp" && event.key !== "ArrowDown") return;
|
|
386
|
-
if (isImeComposition(event)) return;
|
|
387
|
-
if (event.defaultPrevented) return;
|
|
388
|
-
if (actionsRef.current === void 0 || inputRef.current === void 0) return;
|
|
389
|
-
const editable = findComposerEditable(event.target);
|
|
390
|
-
if (editable === null) return;
|
|
391
|
-
if (findTriggerMenu(editable) !== null) return;
|
|
392
|
-
if (inputRef.current.phase !== "plain") return;
|
|
393
|
-
const boundary = caretLineBoundary(editable);
|
|
394
|
-
if (boundary === null) return;
|
|
395
|
-
if (event.key === "ArrowUp" && !boundary.atFirstLine) return;
|
|
396
|
-
if (event.key === "ArrowDown" && !boundary.atLastLine) return;
|
|
397
|
-
const history = getHistoryStore().list;
|
|
398
|
-
const dir = event.key === "ArrowUp" ? "up" : "down";
|
|
399
|
-
const next = nextIndex(navCursorRef.current, history.length, dir);
|
|
400
|
-
if (next === null) {
|
|
401
|
-
const saved = savedDraftRef.current;
|
|
402
|
-
navCursorRef.current = null;
|
|
403
|
-
if (saved !== null) {
|
|
404
|
-
actionsRef.current.setDraft(saved);
|
|
405
|
-
savedDraftRef.current = null;
|
|
406
|
-
}
|
|
407
|
-
consume(event);
|
|
408
|
-
return;
|
|
409
|
-
}
|
|
410
|
-
if (navCursorRef.current === null && savedDraftRef.current === null) savedDraftRef.current = inputRef.current.draft;
|
|
411
|
-
const entry = entryAt(history, next);
|
|
412
|
-
if (entry === null) return;
|
|
413
|
-
navCursorRef.current = next;
|
|
414
|
-
actionsRef.current.setDraft(entry);
|
|
415
|
-
consume(event);
|
|
416
|
-
};
|
|
417
|
-
document.addEventListener("keydown", handler, true);
|
|
418
|
-
return () => {
|
|
419
|
-
document.removeEventListener("keydown", handler, true);
|
|
420
|
-
};
|
|
421
|
-
}, []);
|
|
422
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
423
|
-
"aria-hidden": true,
|
|
424
|
-
style: { display: "none" },
|
|
425
|
-
"data-dsh-plugin-input-history": ""
|
|
426
|
-
});
|
|
427
|
-
}
|
|
428
|
-
/**
|
|
429
|
-
* Consume a navigated keystroke: `preventDefault` stops the browser's own
|
|
430
|
-
* gesture, `stopPropagation` (capture phase, document level) keeps the
|
|
431
|
-
* event from ever reaching Lexical's editable keydown listener — otherwise
|
|
432
|
-
* the keymap would move the caret after the draft was already replaced.
|
|
433
|
-
*/
|
|
434
|
-
function consume(event) {
|
|
435
|
-
event.preventDefault();
|
|
436
|
-
event.stopPropagation();
|
|
437
|
-
}
|
|
438
|
-
/**
|
|
439
|
-
* Extract the text of the latest `user` or `steering` node from the Chat
|
|
440
|
-
* target's legacy node list.
|
|
441
|
-
*
|
|
442
|
-
* Returns the concatenated text of all `type: 'text'` content blocks.
|
|
443
|
-
* Returns `null` when no user/steering node is present (e.g. a fresh
|
|
444
|
-
* session with only a system/context message).
|
|
445
|
-
*
|
|
446
|
-
* @param nodes - the Chat snapshot's legacy `nodes` array (newest last).
|
|
447
|
-
*/
|
|
448
|
-
function latestUserOrSteeringText(nodes) {
|
|
449
|
-
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
450
|
-
const node = nodes[i];
|
|
451
|
-
if (node.kind !== "user" && node.kind !== "steering") continue;
|
|
452
|
-
const content = node.content;
|
|
453
|
-
if (content === void 0) continue;
|
|
454
|
-
let text = "";
|
|
455
|
-
for (const block of content) if (block.type === "text" && typeof block.text === "string") text += block.text;
|
|
456
|
-
return text;
|
|
457
|
-
}
|
|
458
|
-
return null;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
//#endregion
|
|
462
|
-
//#region src/client/locales.ts
|
|
463
|
-
/** Locale namespace id (matches the cordis.patch.yml plugin id). */
|
|
464
|
-
const NS = "dsh-plugin-input-history";
|
|
465
|
-
/** English dictionary. */
|
|
466
|
-
const en = {
|
|
467
|
-
ariaLabel: "Prompt history navigation (ArrowUp/ArrowDown)",
|
|
468
|
-
restoredDraft: "Restored in-progress draft",
|
|
469
|
-
noHistory: "No prompt history yet"
|
|
470
|
-
};
|
|
471
|
-
/** Chinese dictionary. */
|
|
472
|
-
const zh = {
|
|
473
|
-
ariaLabel: "提示词历史导航(上/下方向键)",
|
|
474
|
-
restoredDraft: "已恢复正在编辑的草稿",
|
|
475
|
-
noHistory: "暂无提示词历史"
|
|
476
|
-
};
|
|
477
|
-
|
|
478
|
-
//#endregion
|
|
479
|
-
//#region src/client/dictionaries.ts
|
|
480
|
-
const ja = {
|
|
481
|
-
ariaLabel: "プロンプト履歴のナビゲーション(↑/↓キー)",
|
|
482
|
-
restoredDraft: "編集中の下書きを復元しました",
|
|
483
|
-
noHistory: "プロンプト履歴はまだありません"
|
|
484
|
-
};
|
|
485
|
-
const de = {
|
|
486
|
-
ariaLabel: "Befehlsverlauf-Navigation (Pfeil hoch/runter)",
|
|
487
|
-
restoredDraft: "In Bearbeitung befindlicher Entwurf wiederhergestellt",
|
|
488
|
-
noHistory: "Noch kein Befehlsverlauf vorhanden"
|
|
489
|
-
};
|
|
490
|
-
const fr = {
|
|
491
|
-
ariaLabel: "Navigation dans l'historique des invites (flèche haut/bas)",
|
|
492
|
-
restoredDraft: "Brouillon en cours d'édition restauré",
|
|
493
|
-
noHistory: "Pas encore d'historique d'invites"
|
|
494
|
-
};
|
|
495
|
-
const pt = {
|
|
496
|
-
ariaLabel: "Navegação no histórico de prompts (seta para cima/baixo)",
|
|
497
|
-
restoredDraft: "Rascunho em edição restaurado",
|
|
498
|
-
noHistory: "Ainda não há histórico de prompts"
|
|
499
|
-
};
|
|
500
|
-
const ko = {
|
|
501
|
-
ariaLabel: "프롬프트 기록 탐색 (위/아래 화살표)",
|
|
502
|
-
restoredDraft: "편집 중이던 초안을 복원했습니다",
|
|
503
|
-
noHistory: "아직 프롬프트 기록이 없습니다"
|
|
504
|
-
};
|
|
505
|
-
const ar = {
|
|
506
|
-
ariaLabel: "التنقل في سجل الأوامر (السهم لأعلى/لأسفل)",
|
|
507
|
-
restoredDraft: "تمت استعادة المسودة قيد التحرير",
|
|
508
|
-
noHistory: "لا يوجد سجل أوامر بعد"
|
|
509
|
-
};
|
|
510
|
-
const hi = {
|
|
511
|
-
ariaLabel: "प्रॉम्प्ट इतिहास नेविगेशन (ऊपर/नीचे तीर)",
|
|
512
|
-
restoredDraft: "संपादन में मौजूद ड्राफ्ट पुनर्स्थापित किया गया",
|
|
513
|
-
noHistory: "अभी तक कोई प्रॉम्प्ट इतिहास नहीं"
|
|
514
|
-
};
|
|
515
|
-
const id = {
|
|
516
|
-
ariaLabel: "Navigasi riwayat prompt (panah atas/bawah)",
|
|
517
|
-
restoredDraft: "Draf yang sedang diedit dipulihkan",
|
|
518
|
-
noHistory: "Belum ada riwayat prompt"
|
|
519
|
-
};
|
|
520
|
-
const tr = {
|
|
521
|
-
ariaLabel: "Komut geçmişinde gezinme (yukarı/aşağı ok)",
|
|
522
|
-
restoredDraft: "Düzenlenmekte olan taslak geri yüklendi",
|
|
523
|
-
noHistory: "Henüz komut geçmişi yok"
|
|
524
|
-
};
|
|
525
|
-
const vi = {
|
|
526
|
-
ariaLabel: "Điều hướng lịch sử lệnh (mũi tên lên/xuống)",
|
|
527
|
-
restoredDraft: "Đã khôi phục bản nháp đang soạn",
|
|
528
|
-
noHistory: "Chưa có lịch sử lệnh"
|
|
529
|
-
};
|
|
530
|
-
const th = {
|
|
531
|
-
ariaLabel: "นำทางประวัติคำสั่ง (ลูกศรขึ้น/ลง)",
|
|
532
|
-
restoredDraft: "กู้คืนฉบับร่างที่กำลังแก้ไขแล้ว",
|
|
533
|
-
noHistory: "ยังไม่มีประวัติคำสั่ง"
|
|
534
|
-
};
|
|
535
|
-
const ru = {
|
|
536
|
-
ariaLabel: "Навигация по истории запросов (стрелки вверх/вниз)",
|
|
537
|
-
restoredDraft: "Текущий черновик восстановлен",
|
|
538
|
-
noHistory: "Истории запросов пока нет"
|
|
539
|
-
};
|
|
540
|
-
const it = {
|
|
541
|
-
ariaLabel: "Navigazione cronologia prompt (freccia su/giù)",
|
|
542
|
-
restoredDraft: "Bozza in corso ripristinata",
|
|
543
|
-
noHistory: "Nessuna cronologia prompt finora"
|
|
544
|
-
};
|
|
545
|
-
const nl = {
|
|
546
|
-
ariaLabel: "Navigatie door promptgeschiedenis (pijl omhoog/omlaag)",
|
|
547
|
-
restoredDraft: "Lopende concept hersteld",
|
|
548
|
-
noHistory: "Nog geen promptgeschiedenis"
|
|
549
|
-
};
|
|
550
|
-
const sv = {
|
|
551
|
-
ariaLabel: "Navigera i prompthistorik (pil upp/ner)",
|
|
552
|
-
restoredDraft: "Utkast under arbete återställt",
|
|
553
|
-
noHistory: "Ingen prompthistorik ännu"
|
|
554
|
-
};
|
|
555
|
-
const pl = {
|
|
556
|
-
ariaLabel: "Nawigacja po historii promptów (strzałka w górę/w dół)",
|
|
557
|
-
restoredDraft: "Przywrócono edytowany szkic",
|
|
558
|
-
noHistory: "Brak jeszcze historii promptów"
|
|
559
|
-
};
|
|
560
|
-
const zhHK = {
|
|
561
|
-
ariaLabel: "提示詞歷史導覽(上/下方向鍵)",
|
|
562
|
-
restoredDraft: "已還原正在編輯的草稿",
|
|
563
|
-
noHistory: "暫無提示詞歷史"
|
|
564
|
-
};
|
|
565
|
-
const zhTW = {
|
|
566
|
-
ariaLabel: "提示詞歷史導覽(上/下方向鍵)",
|
|
567
|
-
restoredDraft: "已還原正在編輯的草稿",
|
|
568
|
-
noHistory: "暫無提示詞歷史"
|
|
569
|
-
};
|
|
570
|
-
const zhMO = {
|
|
571
|
-
ariaLabel: "提示詞歷史導覽(上/下方向鍵)",
|
|
572
|
-
restoredDraft: "已還原正在編輯的草稿",
|
|
573
|
-
noHistory: "暫無提示詞歷史"
|
|
574
|
-
};
|
|
575
|
-
/**
|
|
576
|
-
* All override dictionaries, keyed by language id, covering the full key
|
|
577
|
-
* set. Registered with better-locale under the plugin namespace.
|
|
578
|
-
*/
|
|
579
|
-
const dicts = {
|
|
580
|
-
ja,
|
|
581
|
-
de,
|
|
582
|
-
fr,
|
|
583
|
-
pt,
|
|
584
|
-
ko,
|
|
585
|
-
ar,
|
|
586
|
-
hi,
|
|
587
|
-
id,
|
|
588
|
-
tr,
|
|
589
|
-
vi,
|
|
590
|
-
th,
|
|
591
|
-
ru,
|
|
592
|
-
it,
|
|
593
|
-
nl,
|
|
594
|
-
sv,
|
|
595
|
-
pl,
|
|
596
|
-
"zh-HK": zhHK,
|
|
597
|
-
"zh-TW": zhTW,
|
|
598
|
-
"zh-MO": zhMO
|
|
599
|
-
};
|
|
600
|
-
|
|
601
|
-
//#endregion
|
|
602
|
-
//#region src/client/index.ts
|
|
603
|
-
/** Required services: slots + locale. */
|
|
604
|
-
const inject = ["slots", "locale"];
|
|
605
|
-
/**
|
|
606
|
-
* Client plugin body: register the dock + locale dictionaries.
|
|
607
|
-
*
|
|
608
|
-
* @param ctx - client root context.
|
|
609
|
-
*/
|
|
610
|
-
function apply(ctx) {
|
|
611
|
-
ctx.effect(() => ctx.locale.register(NS, {
|
|
612
|
-
zh,
|
|
613
|
-
en
|
|
614
|
-
}), "dsh-plugin-input-history: dictionaries");
|
|
615
|
-
ctx.effect(() => {
|
|
616
|
-
let dispose;
|
|
617
|
-
const sync = () => {
|
|
618
|
-
dispose?.();
|
|
619
|
-
dispose = void 0;
|
|
620
|
-
const store = ctx.get("betterLocale");
|
|
621
|
-
if (store !== void 0) dispose = store.register(NS, dicts);
|
|
622
|
-
};
|
|
623
|
-
sync();
|
|
624
|
-
const unsubscribe = ctx.locale.subscribe(sync);
|
|
625
|
-
return () => {
|
|
626
|
-
unsubscribe();
|
|
627
|
-
dispose?.();
|
|
628
|
-
};
|
|
629
|
-
}, "dsh-plugin-input-history: better-locale override dicts");
|
|
630
|
-
ctx.slots.inject("conversation.composer.dock", () => ctx.slots.register({
|
|
631
|
-
name: "conversation.composer.dock",
|
|
632
|
-
id: "dsh-plugin-input-history",
|
|
633
|
-
order: 100,
|
|
634
|
-
locale: NS
|
|
635
|
-
}, HistoryDock));
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
//#endregion
|
|
639
|
-
exports.apply = apply;
|
|
640
|
-
exports.inject = inject;
|
|
641
|
-
return module.exports; } });
|
|
1
|
+
window.__ModuleLoader__.load({ id: "@huanlin/dsh-plugin-input-history", factory: (require) => {
|
|
2
|
+
var module = { exports: {} }; var exports = module.exports;
|
|
3
|
+
//#region rolldown:runtime
|
|
4
|
+
var __create = Object.create;
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
7
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
8
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
9
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
12
|
+
key = keys[i];
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
14
|
+
get: ((k) => from[k]).bind(null, key),
|
|
15
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
21
|
+
value: mod,
|
|
22
|
+
enumerable: true
|
|
23
|
+
}) : target, mod));
|
|
24
|
+
|
|
25
|
+
//#endregion
|
|
26
|
+
let react = require("react");
|
|
27
|
+
react = __toESM(react);
|
|
28
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
29
|
+
react_jsx_runtime = __toESM(react_jsx_runtime);
|
|
30
|
+
|
|
31
|
+
//#region src/client/history.ts
|
|
32
|
+
/**
|
|
33
|
+
* Prompt history store — pure functions over a string array.
|
|
34
|
+
*
|
|
35
|
+
* The store is a FIFO list of unique prompt strings, persisted to
|
|
36
|
+
* `localStorage`. Newest entries are at the end of the array. The
|
|
37
|
+
* navigation cursor walks backwards from the end (ArrowUp = older,
|
|
38
|
+
* ArrowDown = newer).
|
|
39
|
+
*
|
|
40
|
+
* The functions in this module are pure (no `localStorage` access) so
|
|
41
|
+
* they can be unit-tested without jsdom. The `HistoryStore` class below
|
|
42
|
+
* wires them to `localStorage` with try/catch containment — a quota
|
|
43
|
+
* exception or a disabled storage (private mode) degrades gracefully to
|
|
44
|
+
* an in-memory list that lives for the page lifetime.
|
|
45
|
+
*
|
|
46
|
+
* @module @huanlin/dsh-plugin-input-history/client/history
|
|
47
|
+
*/
|
|
48
|
+
/** localStorage key (versioned; bump on schema changes to start fresh). */
|
|
49
|
+
const STORAGE_KEY = "dsh-plugin-input-history:v1";
|
|
50
|
+
/** Default capacity when none is configured. */
|
|
51
|
+
const DEFAULT_CAPACITY = 500;
|
|
52
|
+
/**
|
|
53
|
+
* Append a prompt to the history.
|
|
54
|
+
*
|
|
55
|
+
* Rules:
|
|
56
|
+
* - Empty / whitespace-only strings are ignored (the InputBar already
|
|
57
|
+
* rejects them at submit, but defensive).
|
|
58
|
+
* - When the new entry equals the most recent one, it is a no-op
|
|
59
|
+
* (avoids stacking duplicates from rapid resends).
|
|
60
|
+
* - When the new entry already exists earlier in the history, that
|
|
61
|
+
* earlier occurrence is removed (recency wins; the prompt moves to
|
|
62
|
+
* the end). This mirrors terminal shell behaviour.
|
|
63
|
+
* - When the array would exceed `capacity`, the oldest entries are
|
|
64
|
+
* dropped from the front (FIFO).
|
|
65
|
+
*
|
|
66
|
+
* @param history - the current history array (newest at end).
|
|
67
|
+
* @param prompt - the prompt to append.
|
|
68
|
+
* @param capacity - the maximum number of entries to retain.
|
|
69
|
+
* @returns the new history array (may be the same reference if no-op).
|
|
70
|
+
*/
|
|
71
|
+
function appendHistory(history, prompt, capacity = DEFAULT_CAPACITY) {
|
|
72
|
+
const trimmed = prompt.trim();
|
|
73
|
+
if (trimmed === "") return history;
|
|
74
|
+
const lastIndex = history.lastIndexOf(trimmed);
|
|
75
|
+
if (lastIndex !== -1 && lastIndex === history.length - 1 && history.indexOf(trimmed) === lastIndex) return history;
|
|
76
|
+
const filtered = history.filter((item) => item !== trimmed);
|
|
77
|
+
filtered.push(trimmed);
|
|
78
|
+
const cap = Math.max(1, capacity);
|
|
79
|
+
if (filtered.length > cap) return filtered.slice(filtered.length - cap);
|
|
80
|
+
return filtered;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Navigation cursor for walking the history.
|
|
84
|
+
*
|
|
85
|
+
* The cursor is `null` when the user is not navigating (i.e. they are
|
|
86
|
+
* typing a fresh draft). ArrowUp sets it to the last index, then
|
|
87
|
+
* decrements; ArrowDown increments; when it would exceed `history.length
|
|
88
|
+
* - 1`, it returns to `null` (meaning "restore the in-progress draft").
|
|
89
|
+
*
|
|
90
|
+
* @param current - the current cursor (null = not navigating).
|
|
91
|
+
* @param total - the total number of history entries.
|
|
92
|
+
* @param dir - `'up'` (older) or `'down'` (newer).
|
|
93
|
+
* @returns the next cursor, or `null` when navigation falls off the
|
|
94
|
+
* newest end (caller should restore the saved draft).
|
|
95
|
+
*/
|
|
96
|
+
function nextIndex(current, total, dir) {
|
|
97
|
+
if (total === 0) return null;
|
|
98
|
+
if (dir === "up") {
|
|
99
|
+
if (current === null) return total - 1;
|
|
100
|
+
if (current <= 0) return 0;
|
|
101
|
+
return current - 1;
|
|
102
|
+
}
|
|
103
|
+
if (current === null) return null;
|
|
104
|
+
if (current >= total - 1) return null;
|
|
105
|
+
return current + 1;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Read the history entry at a cursor, or `null` when the cursor is null.
|
|
109
|
+
*
|
|
110
|
+
* @param history - the history array.
|
|
111
|
+
* @param cursor - the navigation cursor.
|
|
112
|
+
* @returns the prompt at the cursor, or `null`.
|
|
113
|
+
*/
|
|
114
|
+
function entryAt(history, cursor) {
|
|
115
|
+
if (cursor === null) return null;
|
|
116
|
+
if (cursor < 0 || cursor >= history.length) return null;
|
|
117
|
+
return history[cursor] ?? null;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* History store bound to `localStorage`.
|
|
121
|
+
*
|
|
122
|
+
* The store reads once on construction (or on `reload()`) and keeps an
|
|
123
|
+
* in-memory copy. Writes go to both memory and `localStorage` inside a
|
|
124
|
+
* try/catch — a quota exception leaves the in-memory copy authoritative
|
|
125
|
+
* for the rest of the page lifetime. This trades cross-tab consistency
|
|
126
|
+
* for resilience: the store never throws on a write, and the worst case
|
|
127
|
+
* is that a tab keeps its own view until refresh.
|
|
128
|
+
*
|
|
129
|
+
* Cross-tab sync is intentionally NOT implemented: prompt history is
|
|
130
|
+
* append-mostly and a stale read across tabs is harmless (the next
|
|
131
|
+
* append corrects it). Listening to the `storage` event would add
|
|
132
|
+
* reactivity that the navigation UI does not need.
|
|
133
|
+
*/
|
|
134
|
+
var HistoryStore = class {
|
|
135
|
+
items;
|
|
136
|
+
storage;
|
|
137
|
+
key;
|
|
138
|
+
/**
|
|
139
|
+
* @param capacity - maximum entries to retain (FIFO).
|
|
140
|
+
* @param storage - the storage backend (defaults to `localStorage` when available).
|
|
141
|
+
* @param key - the storage key (defaults to {@link STORAGE_KEY}).
|
|
142
|
+
*/
|
|
143
|
+
constructor(capacity = DEFAULT_CAPACITY, storage, key = STORAGE_KEY) {
|
|
144
|
+
this.capacity = capacity;
|
|
145
|
+
this.storage = storage ?? safeLocalStorage();
|
|
146
|
+
this.key = key;
|
|
147
|
+
this.items = this.readFromStorage();
|
|
148
|
+
}
|
|
149
|
+
/** Current history snapshot (newest at end). */
|
|
150
|
+
get list() {
|
|
151
|
+
return this.items;
|
|
152
|
+
}
|
|
153
|
+
/** Number of entries currently stored. */
|
|
154
|
+
get length() {
|
|
155
|
+
return this.items.length;
|
|
156
|
+
}
|
|
157
|
+
/** Reload from storage (e.g. after a suspected external edit). Truncates to the current capacity. */
|
|
158
|
+
reload() {
|
|
159
|
+
const loaded = this.readFromStorage();
|
|
160
|
+
const cap = Math.max(1, this.capacity);
|
|
161
|
+
this.items = loaded.length > cap ? loaded.slice(loaded.length - cap) : loaded;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Append a prompt and persist. See {@link appendHistory} for rules.
|
|
165
|
+
* @returns the new history snapshot.
|
|
166
|
+
*/
|
|
167
|
+
append(prompt) {
|
|
168
|
+
this.items = appendHistory(this.items, prompt, this.capacity);
|
|
169
|
+
this.writeToStorage();
|
|
170
|
+
return this.items;
|
|
171
|
+
}
|
|
172
|
+
/** Clear all history (used by tests and a future "clear" UI). */
|
|
173
|
+
clear() {
|
|
174
|
+
this.items = [];
|
|
175
|
+
this.writeToStorage();
|
|
176
|
+
}
|
|
177
|
+
readFromStorage() {
|
|
178
|
+
if (this.storage === null) return [];
|
|
179
|
+
try {
|
|
180
|
+
const raw = this.storage.getItem(this.key);
|
|
181
|
+
if (raw === null) return [];
|
|
182
|
+
const parsed = JSON.parse(raw);
|
|
183
|
+
if (!Array.isArray(parsed)) return [];
|
|
184
|
+
return parsed.filter((item) => typeof item === "string");
|
|
185
|
+
} catch {
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
writeToStorage() {
|
|
190
|
+
if (this.storage === null) return;
|
|
191
|
+
try {
|
|
192
|
+
this.storage.setItem(this.key, JSON.stringify(this.items));
|
|
193
|
+
} catch {}
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
/** Safe accessor for `localStorage` that returns null on any failure. */
|
|
197
|
+
function safeLocalStorage() {
|
|
198
|
+
try {
|
|
199
|
+
if (typeof localStorage === "undefined") return null;
|
|
200
|
+
return localStorage;
|
|
201
|
+
} catch {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
//#endregion
|
|
207
|
+
//#region src/client/dom.ts
|
|
208
|
+
/**
|
|
209
|
+
* Pure decision over caret geometry: where a caret resting at `caretTop`
|
|
210
|
+
* sits relative to the box whose visual line tops are `lineTops` (ascending,
|
|
211
|
+
* one entry per visual line, viewport coordinates).
|
|
212
|
+
*
|
|
213
|
+
* @param caretTop - viewport `top` of the collapsed caret's box.
|
|
214
|
+
* @param lineTops - viewport `top` of each visual line, ascending.
|
|
215
|
+
* @param tolerance - px slop absorbing subpixel rounding between the caret
|
|
216
|
+
* rect and its line's rect.
|
|
217
|
+
* @returns the boundary flags; an empty `lineTops` (empty editable) is
|
|
218
|
+
* treated as a single virtual line, so both flags are true.
|
|
219
|
+
*/
|
|
220
|
+
function boundaryFromLineTops(caretTop, lineTops, tolerance) {
|
|
221
|
+
if (lineTops.length === 0) return {
|
|
222
|
+
atFirstLine: true,
|
|
223
|
+
atLastLine: true
|
|
224
|
+
};
|
|
225
|
+
return {
|
|
226
|
+
atFirstLine: caretTop <= lineTops[0] + tolerance,
|
|
227
|
+
atLastLine: caretTop >= lineTops[lineTops.length - 1] - tolerance
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Locate the DSH composer editable the event targeted.
|
|
232
|
+
*
|
|
233
|
+
* Walks from the event target up to the closest `[data-composer-card]`
|
|
234
|
+
* ancestor, queries the `[data-composer-input]` contenteditable inside it,
|
|
235
|
+
* and confirms the target sits inside that editable (keystrokes on the
|
|
236
|
+
* card's buttons and chrome do not navigate history). Returns `null` when
|
|
237
|
+
* the target is not inside the composer editable.
|
|
238
|
+
*
|
|
239
|
+
* @param from - the event target (or any node inside the composer editable).
|
|
240
|
+
* @returns the editable element, or `null` when not found.
|
|
241
|
+
*/
|
|
242
|
+
function findComposerEditable(from) {
|
|
243
|
+
if (typeof document === "undefined") return null;
|
|
244
|
+
if (from === null || !(from instanceof Element)) return null;
|
|
245
|
+
const card = from.closest("[data-composer-card]");
|
|
246
|
+
if (card === null) return null;
|
|
247
|
+
const editable = card.querySelector("[data-composer-input]");
|
|
248
|
+
if (editable === null) return null;
|
|
249
|
+
return editable.contains(from) ? editable : null;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Detect an open trigger (slash-command / @-mention) menu inside the
|
|
253
|
+
* composer card that owns `editable`.
|
|
254
|
+
*
|
|
255
|
+
* While the menu is open, ArrowUp/ArrowDown move the highlighted row and
|
|
256
|
+
* must not recall history. The menu renders inside the same
|
|
257
|
+
* `[data-composer-card]` as the editable and carries the stable
|
|
258
|
+
* `data-trigger-menu` marker.
|
|
259
|
+
*
|
|
260
|
+
* @param editable - the composer editable element.
|
|
261
|
+
* @returns the menu element, or `null` when no menu is open.
|
|
262
|
+
*/
|
|
263
|
+
function findTriggerMenu(editable) {
|
|
264
|
+
const card = editable.closest("[data-composer-card]");
|
|
265
|
+
return card === null ? null : card.querySelector("[data-trigger-menu]");
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Decide the collapsed caret's line boundary inside the composer editable.
|
|
269
|
+
*
|
|
270
|
+
* Compares the caret's viewport box against the editable content's visual
|
|
271
|
+
* line boxes (`Range.getClientRects()` yields one rect per line fragment;
|
|
272
|
+
* fragments of the same visual line share a top within subpixel slop, so
|
|
273
|
+
* tops are deduped with a 2px threshold). A non-collapsed selection and a
|
|
274
|
+
* geometry-less environment (headless/jsdom) both return `null`, which the
|
|
275
|
+
* caller must treat as "do not navigate".
|
|
276
|
+
*
|
|
277
|
+
* @param editable - the composer editable element.
|
|
278
|
+
* @param tolerance - px slop between the caret rect and its line rect
|
|
279
|
+
* (defaults to 4px).
|
|
280
|
+
* @returns the boundary flags, or `null` when they cannot be determined.
|
|
281
|
+
*/
|
|
282
|
+
function caretLineBoundary(editable, tolerance = 4) {
|
|
283
|
+
const selection = window.getSelection();
|
|
284
|
+
if (selection === null || selection.rangeCount === 0) return null;
|
|
285
|
+
if (!selection.isCollapsed) return null;
|
|
286
|
+
const caretTop = caretTopOf(selection);
|
|
287
|
+
if (caretTop === null) return null;
|
|
288
|
+
const lineTops = contentLineTops(editable);
|
|
289
|
+
if (lineTops === null) return null;
|
|
290
|
+
return boundaryFromLineTops(caretTop, lineTops, tolerance);
|
|
291
|
+
}
|
|
292
|
+
/** Viewport `top` of the collapsed caret's box, or `null` when unmeasurable. */
|
|
293
|
+
function caretTopOf(selection) {
|
|
294
|
+
const rects = selection.getRangeAt(0).getClientRects();
|
|
295
|
+
for (let i = 0; i < rects.length; i++) {
|
|
296
|
+
const rect = rects[i];
|
|
297
|
+
if (rect.height === 0 && rect.width === 0) continue;
|
|
298
|
+
return rect.top;
|
|
299
|
+
}
|
|
300
|
+
const anchor = selection.anchorNode;
|
|
301
|
+
const el = anchor instanceof HTMLElement ? anchor : anchor?.parentElement;
|
|
302
|
+
return el === void 0 || el === null ? null : el.getBoundingClientRect().top;
|
|
303
|
+
}
|
|
304
|
+
/** Ascending, deduped tops of the editable content's visual lines; `null` without geometry. Empty for an empty editable. */
|
|
305
|
+
function contentLineTops(editable) {
|
|
306
|
+
const range = document.createRange();
|
|
307
|
+
range.selectNodeContents(editable);
|
|
308
|
+
const rects = range.getClientRects();
|
|
309
|
+
const tops = [];
|
|
310
|
+
for (let i = 0; i < rects.length; i++) {
|
|
311
|
+
const rect = rects[i];
|
|
312
|
+
if (rect.height === 0 && rect.width === 0) continue;
|
|
313
|
+
const top = rect.top;
|
|
314
|
+
if (tops.length === 0 || Math.abs(top - tops[tops.length - 1]) > 2) tops.push(top);
|
|
315
|
+
}
|
|
316
|
+
return tops;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
//#endregion
|
|
320
|
+
//#region src/client/ime.ts
|
|
321
|
+
/**
|
|
322
|
+
* IME-composition key guard.
|
|
323
|
+
*
|
|
324
|
+
* While a Chinese/Japanese/Korean input method is composing (the user is
|
|
325
|
+
* picking a candidate from the IME window), every pressed key BELONGS to
|
|
326
|
+
* the input method: arrows move the candidate highlight, Enter/Space
|
|
327
|
+
* confirm the composition, Escape cancels it. Page code must not process
|
|
328
|
+
* those keys — a history-navigation handler that calls `preventDefault()`
|
|
329
|
+
* on ArrowUp/ArrowDown during composition would silently break the IME:
|
|
330
|
+
* candidates stop responding, the composition gets torn apart, and only
|
|
331
|
+
* bare letters come out.
|
|
332
|
+
*
|
|
333
|
+
* The composition signal follows the DSH core convention (InputBar's IME
|
|
334
|
+
* guard, issue #535): `isComposing` for modern engines, keyCode 229 as
|
|
335
|
+
* the legacy signal engines emit without isComposing.
|
|
336
|
+
*
|
|
337
|
+
* @module @huanlin/dsh-plugin-input-history/client/ime
|
|
338
|
+
*/
|
|
339
|
+
/** The pure decision: is this keyboard event part of an IME composition? */
|
|
340
|
+
function isImeComposition(event) {
|
|
341
|
+
return event.isComposing || event.keyCode === 229;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
//#endregion
|
|
345
|
+
//#region src/client/HistoryDock.tsx
|
|
346
|
+
/**
|
|
347
|
+
* Module-scope history store, initialized once on first dock mount.
|
|
348
|
+
* Shared across dock mount/unmount cycles; the underlying data persists
|
|
349
|
+
* in `localStorage`.
|
|
350
|
+
*/
|
|
351
|
+
let historyStore = null;
|
|
352
|
+
/** Get the shared history store (initializes lazily on first call). */
|
|
353
|
+
function getHistoryStore() {
|
|
354
|
+
if (historyStore === null) historyStore = new HistoryStore(DEFAULT_CAPACITY);
|
|
355
|
+
return historyStore;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Render the invisible history dock entry: collection + navigation.
|
|
359
|
+
*
|
|
360
|
+
* @param props - dock runtime share (standard hooks) + locale seat.
|
|
361
|
+
* @returns an `aria-hidden` anchor with zero layout footprint.
|
|
362
|
+
*/
|
|
363
|
+
function HistoryDock({ useInput, useChat, inputActions, sessionId }) {
|
|
364
|
+
const input = useInput((s) => s);
|
|
365
|
+
const nodes = useChat((s) => s.legacy.nodes);
|
|
366
|
+
const lastText = (0, react.useMemo)(() => latestUserOrSteeringText(nodes), [nodes]);
|
|
367
|
+
(0, react.useEffect)(() => {
|
|
368
|
+
if (lastText !== null) getHistoryStore().append(lastText);
|
|
369
|
+
}, [lastText]);
|
|
370
|
+
const navCursorRef = (0, react.useRef)(null);
|
|
371
|
+
const savedDraftRef = (0, react.useRef)(null);
|
|
372
|
+
const prevSessionRef = (0, react.useRef)(sessionId);
|
|
373
|
+
if (prevSessionRef.current !== sessionId) {
|
|
374
|
+
prevSessionRef.current = sessionId;
|
|
375
|
+
navCursorRef.current = null;
|
|
376
|
+
savedDraftRef.current = null;
|
|
377
|
+
}
|
|
378
|
+
const inputRef = (0, react.useRef)(input);
|
|
379
|
+
inputRef.current = input;
|
|
380
|
+
const actionsRef = (0, react.useRef)(inputActions);
|
|
381
|
+
actionsRef.current = inputActions;
|
|
382
|
+
(0, react.useEffect)(() => {
|
|
383
|
+
if (typeof document === "undefined") return void 0;
|
|
384
|
+
const handler = (event) => {
|
|
385
|
+
if (event.key !== "ArrowUp" && event.key !== "ArrowDown") return;
|
|
386
|
+
if (isImeComposition(event)) return;
|
|
387
|
+
if (event.defaultPrevented) return;
|
|
388
|
+
if (actionsRef.current === void 0 || inputRef.current === void 0) return;
|
|
389
|
+
const editable = findComposerEditable(event.target);
|
|
390
|
+
if (editable === null) return;
|
|
391
|
+
if (findTriggerMenu(editable) !== null) return;
|
|
392
|
+
if (inputRef.current.phase !== "plain") return;
|
|
393
|
+
const boundary = caretLineBoundary(editable);
|
|
394
|
+
if (boundary === null) return;
|
|
395
|
+
if (event.key === "ArrowUp" && !boundary.atFirstLine) return;
|
|
396
|
+
if (event.key === "ArrowDown" && !boundary.atLastLine) return;
|
|
397
|
+
const history = getHistoryStore().list;
|
|
398
|
+
const dir = event.key === "ArrowUp" ? "up" : "down";
|
|
399
|
+
const next = nextIndex(navCursorRef.current, history.length, dir);
|
|
400
|
+
if (next === null) {
|
|
401
|
+
const saved = savedDraftRef.current;
|
|
402
|
+
navCursorRef.current = null;
|
|
403
|
+
if (saved !== null) {
|
|
404
|
+
actionsRef.current.setDraft(saved);
|
|
405
|
+
savedDraftRef.current = null;
|
|
406
|
+
}
|
|
407
|
+
consume(event);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (navCursorRef.current === null && savedDraftRef.current === null) savedDraftRef.current = inputRef.current.draft;
|
|
411
|
+
const entry = entryAt(history, next);
|
|
412
|
+
if (entry === null) return;
|
|
413
|
+
navCursorRef.current = next;
|
|
414
|
+
actionsRef.current.setDraft(entry);
|
|
415
|
+
consume(event);
|
|
416
|
+
};
|
|
417
|
+
document.addEventListener("keydown", handler, true);
|
|
418
|
+
return () => {
|
|
419
|
+
document.removeEventListener("keydown", handler, true);
|
|
420
|
+
};
|
|
421
|
+
}, []);
|
|
422
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
423
|
+
"aria-hidden": true,
|
|
424
|
+
style: { display: "none" },
|
|
425
|
+
"data-dsh-plugin-input-history": ""
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Consume a navigated keystroke: `preventDefault` stops the browser's own
|
|
430
|
+
* gesture, `stopPropagation` (capture phase, document level) keeps the
|
|
431
|
+
* event from ever reaching Lexical's editable keydown listener — otherwise
|
|
432
|
+
* the keymap would move the caret after the draft was already replaced.
|
|
433
|
+
*/
|
|
434
|
+
function consume(event) {
|
|
435
|
+
event.preventDefault();
|
|
436
|
+
event.stopPropagation();
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Extract the text of the latest `user` or `steering` node from the Chat
|
|
440
|
+
* target's legacy node list.
|
|
441
|
+
*
|
|
442
|
+
* Returns the concatenated text of all `type: 'text'` content blocks.
|
|
443
|
+
* Returns `null` when no user/steering node is present (e.g. a fresh
|
|
444
|
+
* session with only a system/context message).
|
|
445
|
+
*
|
|
446
|
+
* @param nodes - the Chat snapshot's legacy `nodes` array (newest last).
|
|
447
|
+
*/
|
|
448
|
+
function latestUserOrSteeringText(nodes) {
|
|
449
|
+
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
450
|
+
const node = nodes[i];
|
|
451
|
+
if (node.kind !== "user" && node.kind !== "steering") continue;
|
|
452
|
+
const content = node.content;
|
|
453
|
+
if (content === void 0) continue;
|
|
454
|
+
let text = "";
|
|
455
|
+
for (const block of content) if (block.type === "text" && typeof block.text === "string") text += block.text;
|
|
456
|
+
return text;
|
|
457
|
+
}
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
//#endregion
|
|
462
|
+
//#region src/client/locales.ts
|
|
463
|
+
/** Locale namespace id (matches the cordis.patch.yml plugin id). */
|
|
464
|
+
const NS = "dsh-plugin-input-history";
|
|
465
|
+
/** English dictionary. */
|
|
466
|
+
const en = {
|
|
467
|
+
ariaLabel: "Prompt history navigation (ArrowUp/ArrowDown)",
|
|
468
|
+
restoredDraft: "Restored in-progress draft",
|
|
469
|
+
noHistory: "No prompt history yet"
|
|
470
|
+
};
|
|
471
|
+
/** Chinese dictionary. */
|
|
472
|
+
const zh = {
|
|
473
|
+
ariaLabel: "提示词历史导航(上/下方向键)",
|
|
474
|
+
restoredDraft: "已恢复正在编辑的草稿",
|
|
475
|
+
noHistory: "暂无提示词历史"
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
//#endregion
|
|
479
|
+
//#region src/client/dictionaries.ts
|
|
480
|
+
const ja = {
|
|
481
|
+
ariaLabel: "プロンプト履歴のナビゲーション(↑/↓キー)",
|
|
482
|
+
restoredDraft: "編集中の下書きを復元しました",
|
|
483
|
+
noHistory: "プロンプト履歴はまだありません"
|
|
484
|
+
};
|
|
485
|
+
const de = {
|
|
486
|
+
ariaLabel: "Befehlsverlauf-Navigation (Pfeil hoch/runter)",
|
|
487
|
+
restoredDraft: "In Bearbeitung befindlicher Entwurf wiederhergestellt",
|
|
488
|
+
noHistory: "Noch kein Befehlsverlauf vorhanden"
|
|
489
|
+
};
|
|
490
|
+
const fr = {
|
|
491
|
+
ariaLabel: "Navigation dans l'historique des invites (flèche haut/bas)",
|
|
492
|
+
restoredDraft: "Brouillon en cours d'édition restauré",
|
|
493
|
+
noHistory: "Pas encore d'historique d'invites"
|
|
494
|
+
};
|
|
495
|
+
const pt = {
|
|
496
|
+
ariaLabel: "Navegação no histórico de prompts (seta para cima/baixo)",
|
|
497
|
+
restoredDraft: "Rascunho em edição restaurado",
|
|
498
|
+
noHistory: "Ainda não há histórico de prompts"
|
|
499
|
+
};
|
|
500
|
+
const ko = {
|
|
501
|
+
ariaLabel: "프롬프트 기록 탐색 (위/아래 화살표)",
|
|
502
|
+
restoredDraft: "편집 중이던 초안을 복원했습니다",
|
|
503
|
+
noHistory: "아직 프롬프트 기록이 없습니다"
|
|
504
|
+
};
|
|
505
|
+
const ar = {
|
|
506
|
+
ariaLabel: "التنقل في سجل الأوامر (السهم لأعلى/لأسفل)",
|
|
507
|
+
restoredDraft: "تمت استعادة المسودة قيد التحرير",
|
|
508
|
+
noHistory: "لا يوجد سجل أوامر بعد"
|
|
509
|
+
};
|
|
510
|
+
const hi = {
|
|
511
|
+
ariaLabel: "प्रॉम्प्ट इतिहास नेविगेशन (ऊपर/नीचे तीर)",
|
|
512
|
+
restoredDraft: "संपादन में मौजूद ड्राफ्ट पुनर्स्थापित किया गया",
|
|
513
|
+
noHistory: "अभी तक कोई प्रॉम्प्ट इतिहास नहीं"
|
|
514
|
+
};
|
|
515
|
+
const id = {
|
|
516
|
+
ariaLabel: "Navigasi riwayat prompt (panah atas/bawah)",
|
|
517
|
+
restoredDraft: "Draf yang sedang diedit dipulihkan",
|
|
518
|
+
noHistory: "Belum ada riwayat prompt"
|
|
519
|
+
};
|
|
520
|
+
const tr = {
|
|
521
|
+
ariaLabel: "Komut geçmişinde gezinme (yukarı/aşağı ok)",
|
|
522
|
+
restoredDraft: "Düzenlenmekte olan taslak geri yüklendi",
|
|
523
|
+
noHistory: "Henüz komut geçmişi yok"
|
|
524
|
+
};
|
|
525
|
+
const vi = {
|
|
526
|
+
ariaLabel: "Điều hướng lịch sử lệnh (mũi tên lên/xuống)",
|
|
527
|
+
restoredDraft: "Đã khôi phục bản nháp đang soạn",
|
|
528
|
+
noHistory: "Chưa có lịch sử lệnh"
|
|
529
|
+
};
|
|
530
|
+
const th = {
|
|
531
|
+
ariaLabel: "นำทางประวัติคำสั่ง (ลูกศรขึ้น/ลง)",
|
|
532
|
+
restoredDraft: "กู้คืนฉบับร่างที่กำลังแก้ไขแล้ว",
|
|
533
|
+
noHistory: "ยังไม่มีประวัติคำสั่ง"
|
|
534
|
+
};
|
|
535
|
+
const ru = {
|
|
536
|
+
ariaLabel: "Навигация по истории запросов (стрелки вверх/вниз)",
|
|
537
|
+
restoredDraft: "Текущий черновик восстановлен",
|
|
538
|
+
noHistory: "Истории запросов пока нет"
|
|
539
|
+
};
|
|
540
|
+
const it = {
|
|
541
|
+
ariaLabel: "Navigazione cronologia prompt (freccia su/giù)",
|
|
542
|
+
restoredDraft: "Bozza in corso ripristinata",
|
|
543
|
+
noHistory: "Nessuna cronologia prompt finora"
|
|
544
|
+
};
|
|
545
|
+
const nl = {
|
|
546
|
+
ariaLabel: "Navigatie door promptgeschiedenis (pijl omhoog/omlaag)",
|
|
547
|
+
restoredDraft: "Lopende concept hersteld",
|
|
548
|
+
noHistory: "Nog geen promptgeschiedenis"
|
|
549
|
+
};
|
|
550
|
+
const sv = {
|
|
551
|
+
ariaLabel: "Navigera i prompthistorik (pil upp/ner)",
|
|
552
|
+
restoredDraft: "Utkast under arbete återställt",
|
|
553
|
+
noHistory: "Ingen prompthistorik ännu"
|
|
554
|
+
};
|
|
555
|
+
const pl = {
|
|
556
|
+
ariaLabel: "Nawigacja po historii promptów (strzałka w górę/w dół)",
|
|
557
|
+
restoredDraft: "Przywrócono edytowany szkic",
|
|
558
|
+
noHistory: "Brak jeszcze historii promptów"
|
|
559
|
+
};
|
|
560
|
+
const zhHK = {
|
|
561
|
+
ariaLabel: "提示詞歷史導覽(上/下方向鍵)",
|
|
562
|
+
restoredDraft: "已還原正在編輯的草稿",
|
|
563
|
+
noHistory: "暫無提示詞歷史"
|
|
564
|
+
};
|
|
565
|
+
const zhTW = {
|
|
566
|
+
ariaLabel: "提示詞歷史導覽(上/下方向鍵)",
|
|
567
|
+
restoredDraft: "已還原正在編輯的草稿",
|
|
568
|
+
noHistory: "暫無提示詞歷史"
|
|
569
|
+
};
|
|
570
|
+
const zhMO = {
|
|
571
|
+
ariaLabel: "提示詞歷史導覽(上/下方向鍵)",
|
|
572
|
+
restoredDraft: "已還原正在編輯的草稿",
|
|
573
|
+
noHistory: "暫無提示詞歷史"
|
|
574
|
+
};
|
|
575
|
+
/**
|
|
576
|
+
* All override dictionaries, keyed by language id, covering the full key
|
|
577
|
+
* set. Registered with better-locale under the plugin namespace.
|
|
578
|
+
*/
|
|
579
|
+
const dicts = {
|
|
580
|
+
ja,
|
|
581
|
+
de,
|
|
582
|
+
fr,
|
|
583
|
+
pt,
|
|
584
|
+
ko,
|
|
585
|
+
ar,
|
|
586
|
+
hi,
|
|
587
|
+
id,
|
|
588
|
+
tr,
|
|
589
|
+
vi,
|
|
590
|
+
th,
|
|
591
|
+
ru,
|
|
592
|
+
it,
|
|
593
|
+
nl,
|
|
594
|
+
sv,
|
|
595
|
+
pl,
|
|
596
|
+
"zh-HK": zhHK,
|
|
597
|
+
"zh-TW": zhTW,
|
|
598
|
+
"zh-MO": zhMO
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
//#endregion
|
|
602
|
+
//#region src/client/index.ts
|
|
603
|
+
/** Required services: slots + locale. */
|
|
604
|
+
const inject = ["slots", "locale"];
|
|
605
|
+
/**
|
|
606
|
+
* Client plugin body: register the dock + locale dictionaries.
|
|
607
|
+
*
|
|
608
|
+
* @param ctx - client root context.
|
|
609
|
+
*/
|
|
610
|
+
function apply(ctx) {
|
|
611
|
+
ctx.effect(() => ctx.locale.register(NS, {
|
|
612
|
+
zh,
|
|
613
|
+
en
|
|
614
|
+
}), "dsh-plugin-input-history: dictionaries");
|
|
615
|
+
ctx.effect(() => {
|
|
616
|
+
let dispose;
|
|
617
|
+
const sync = () => {
|
|
618
|
+
dispose?.();
|
|
619
|
+
dispose = void 0;
|
|
620
|
+
const store = ctx.get("betterLocale");
|
|
621
|
+
if (store !== void 0) dispose = store.register(NS, dicts);
|
|
622
|
+
};
|
|
623
|
+
sync();
|
|
624
|
+
const unsubscribe = ctx.locale.subscribe(sync);
|
|
625
|
+
return () => {
|
|
626
|
+
unsubscribe();
|
|
627
|
+
dispose?.();
|
|
628
|
+
};
|
|
629
|
+
}, "dsh-plugin-input-history: better-locale override dicts");
|
|
630
|
+
ctx.slots.inject("conversation.composer.dock", () => ctx.slots.register({
|
|
631
|
+
name: "conversation.composer.dock",
|
|
632
|
+
id: "dsh-plugin-input-history",
|
|
633
|
+
order: 100,
|
|
634
|
+
locale: NS
|
|
635
|
+
}, HistoryDock));
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
//#endregion
|
|
639
|
+
exports.apply = apply;
|
|
640
|
+
exports.inject = inject;
|
|
641
|
+
return module.exports; } });
|
|
642
642
|
//# sourceMappingURL=client.js.map
|