@huanlin/dsh-plugin-input-history 0.1.2 → 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/lib/client.js CHANGED
@@ -1,605 +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/HistoryDock.tsx
208
- /**
209
- * Module-scope history store, initialized once on first dock mount.
210
- * Shared with the keydown listener in `apply` via `getHistoryStore()`.
211
- */
212
- let historyStore = null;
213
- /** Get the shared history store (initializes lazily on first call). */
214
- function getHistoryStore() {
215
- if (historyStore === null) historyStore = new HistoryStore(DEFAULT_CAPACITY);
216
- return historyStore;
217
- }
218
- /**
219
- * Render the invisible history-collection dock entry.
220
- *
221
- * @param props - dock runtime share (InputZone owner + session kit) + locale seat.
222
- * @returns an `aria-hidden` anchor with zero layout footprint.
223
- */
224
- function HistoryDock({ session }) {
225
- const store = getHistoryStore();
226
- const lastSeenTextRef = (0, react.useRef)(null);
227
- const lastText = latestUserOrSteeringText(session.nodes);
228
- if (lastText !== null && lastText !== lastSeenTextRef.current) {
229
- lastSeenTextRef.current = lastText;
230
- store.append(lastText);
231
- }
232
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
233
- "aria-hidden": true,
234
- style: { display: "none" },
235
- "data-dsh-plugin-input-history": ""
236
- });
237
- }
238
- /**
239
- * Extract the text of the latest `user` or `steering` node from a
240
- * conversation snapshot's nodes list.
241
- *
242
- * Returns the concatenated text of all `type: 'text'` content blocks.
243
- * Returns `null` when no user/steering node is present (e.g. a fresh
244
- * session with only a system/context message).
245
- *
246
- * @param nodes - the conversation snapshot's `nodes` array.
247
- */
248
- function latestUserOrSteeringText(nodes) {
249
- for (let i = nodes.length - 1; i >= 0; i--) {
250
- const node = nodes[i];
251
- if (node.kind !== "user" && node.kind !== "steering") continue;
252
- const content = node.content;
253
- if (content === void 0) continue;
254
- let text = "";
255
- for (const block of content) if (block.type === "text" && typeof block.text === "string") text += block.text;
256
- return text;
257
- }
258
- return null;
259
- }
260
-
261
- //#endregion
262
- //#region src/client/ime.ts
263
- /**
264
- * IME-composition key guard.
265
- *
266
- * While a Chinese/Japanese/Korean input method is composing (the user is
267
- * picking a candidate from the IME window), every pressed key BELONGS to
268
- * the input method: arrows move the candidate highlight, Enter/Space
269
- * confirm the composition, Escape cancels it. Page code must not process
270
- * those keys a history-navigation handler that calls `preventDefault()`
271
- * on ArrowUp/ArrowDown during composition would silently break the IME:
272
- * candidates stop responding, the composition gets torn apart, and only
273
- * bare letters come out.
274
- *
275
- * The composition signal follows the DSH core convention (InputBar's IME
276
- * guard, issue #535): `isComposing` for modern engines, keyCode 229 as
277
- * the legacy signal engines emit without isComposing.
278
- *
279
- * @module @huanlin/dsh-plugin-input-history/client/ime
280
- */
281
- /** The pure decision: is this keyboard event part of an IME composition? */
282
- function isImeComposition(event) {
283
- return event.isComposing || event.keyCode === 229;
284
- }
285
-
286
- //#endregion
287
- //#region src/client/dom.ts
288
- /**
289
- * Compute the caret's line position in a textarea value.
290
- *
291
- * Lines are split on `\n` (the textarea's own line break character). The
292
- * caret must be collapsed (`selectionStart === selectionEnd`) for the
293
- * `atFirstLine` / `atLastLine` flags to be true — a non-collapsed
294
- * selection spanning multiple lines should not trigger history navigation.
295
- *
296
- * @param value - the textarea's current value.
297
- * @param selectionStart - the textarea's `selectionStart`.
298
- * @param selectionEnd - the textarea's `selectionEnd` (defaults to `selectionStart`).
299
- * @returns the caret's line information.
300
- */
301
- function cursorLineInfo(value, selectionStart, selectionEnd = selectionStart) {
302
- const rawStart = Math.min(selectionStart, selectionEnd);
303
- const rawEnd = Math.max(selectionStart, selectionEnd);
304
- const clampedStart = Math.max(0, Math.min(rawStart, value.length));
305
- const collapsed = clampedStart === Math.max(clampedStart, Math.min(rawEnd, value.length));
306
- const lines = value.split("\n");
307
- const totalLines = lines.length;
308
- let currentLine = 0;
309
- let runningLength = 0;
310
- for (let i = 0; i < totalLines; i++) {
311
- const line = lines[i];
312
- const lineEnd = runningLength + line.length;
313
- const upperBound = i === totalLines - 1 ? lineEnd + 1 : lineEnd + 1;
314
- if (clampedStart >= runningLength && clampedStart < upperBound) {
315
- currentLine = i;
316
- break;
317
- }
318
- runningLength = lineEnd + 1;
319
- }
320
- return {
321
- currentLine,
322
- totalLines,
323
- atFirstLine: collapsed && currentLine === 0,
324
- atLastLine: collapsed && currentLine === totalLines - 1
325
- };
326
- }
327
- /**
328
- * Locate the DSH composer textarea in the current document.
329
- *
330
- * Walks from the event target up to find the closest `[data-composer-card]`
331
- * ancestor, then queries the descendant `<textarea>` inside it. Returns
332
- * `null` when the target is not inside the composer card (e.g. the user
333
- * is typing in another input or the textarea is momentarily absent).
334
- *
335
- * When called without an event target, falls back to a document-wide
336
- * query — used in tests and ad-hoc probing.
337
- *
338
- * @param from - the event target (or any node inside the composer card).
339
- * @returns the textarea element, or `null` when not found.
340
- */
341
- function findComposerTextarea(from) {
342
- if (typeof document === "undefined") return null;
343
- if (from === void 0) return document.querySelector("[data-composer-card] textarea");
344
- if (from === null) return null;
345
- const card = from instanceof Element ? from.closest("[data-composer-card]") : null;
346
- if (card !== null) {
347
- const ta = card.querySelector("textarea");
348
- if (ta !== null) return ta;
349
- }
350
- return null;
351
- }
352
-
353
- //#endregion
354
- //#region src/client/locales.ts
355
- /** Locale namespace id (matches the cordis.patch.yml plugin id). */
356
- const NS = "dsh-plugin-input-history";
357
- /** English dictionary. */
358
- const en = {
359
- ariaLabel: "Prompt history navigation (ArrowUp/ArrowDown)",
360
- restoredDraft: "Restored in-progress draft",
361
- noHistory: "No prompt history yet"
362
- };
363
- /** Chinese dictionary. */
364
- const zh = {
365
- ariaLabel: "提示词历史导航(上/下方向键)",
366
- restoredDraft: "已恢复正在编辑的草稿",
367
- noHistory: "暂无提示词历史"
368
- };
369
-
370
- //#endregion
371
- //#region src/client/dictionaries.ts
372
- const ja = {
373
- ariaLabel: "プロンプト履歴のナビゲーション(↑/↓キー)",
374
- restoredDraft: "編集中の下書きを復元しました",
375
- noHistory: "プロンプト履歴はまだありません"
376
- };
377
- const de = {
378
- ariaLabel: "Befehlsverlauf-Navigation (Pfeil hoch/runter)",
379
- restoredDraft: "In Bearbeitung befindlicher Entwurf wiederhergestellt",
380
- noHistory: "Noch kein Befehlsverlauf vorhanden"
381
- };
382
- const fr = {
383
- ariaLabel: "Navigation dans l'historique des invites (flèche haut/bas)",
384
- restoredDraft: "Brouillon en cours d'édition restauré",
385
- noHistory: "Pas encore d'historique d'invites"
386
- };
387
- const pt = {
388
- ariaLabel: "Navegação no histórico de prompts (seta para cima/baixo)",
389
- restoredDraft: "Rascunho em edição restaurado",
390
- noHistory: "Ainda não histórico de prompts"
391
- };
392
- const ko = {
393
- ariaLabel: "프롬프트 기록 탐색 (위/아래 화살표)",
394
- restoredDraft: "편집 중이던 초안을 복원했습니다",
395
- noHistory: "아직 프롬프트 기록이 없습니다"
396
- };
397
- const ar = {
398
- ariaLabel: "التنقل في سجل الأوامر (السهم لأعلى/لأسفل)",
399
- restoredDraft: "تمت استعادة المسودة قيد التحرير",
400
- noHistory: "لا يوجد سجل أوامر بعد"
401
- };
402
- const hi = {
403
- ariaLabel: "प्रॉम्प्ट इतिहास नेविगेशन (ऊपर/नीचे तीर)",
404
- restoredDraft: "संपादन में मौजूद ड्राफ्ट पुनर्स्थापित किया गया",
405
- noHistory: "अभी तक कोई प्रॉम्प्ट इतिहास नहीं"
406
- };
407
- const id = {
408
- ariaLabel: "Navigasi riwayat prompt (panah atas/bawah)",
409
- restoredDraft: "Draf yang sedang diedit dipulihkan",
410
- noHistory: "Belum ada riwayat prompt"
411
- };
412
- const tr = {
413
- ariaLabel: "Komut geçmişinde gezinme (yukarı/aşağı ok)",
414
- restoredDraft: "Düzenlenmekte olan taslak geri yüklendi",
415
- noHistory: "Henüz komut geçmişi yok"
416
- };
417
- const vi = {
418
- ariaLabel: "Điều hướng lịch sử lệnh (mũi tên lên/xuống)",
419
- restoredDraft: "Đã khôi phục bản nháp đang soạn",
420
- noHistory: "Chưa có lịch sử lệnh"
421
- };
422
- const th = {
423
- ariaLabel: "นำทางประวัติคำสั่ง (ลูกศรขึ้น/ลง)",
424
- restoredDraft: "กู้คืนฉบับร่างที่กำลังแก้ไขแล้ว",
425
- noHistory: "ยังไม่มีประวัติคำสั่ง"
426
- };
427
- const ru = {
428
- ariaLabel: "Навигация по истории запросов (стрелки вверх/вниз)",
429
- restoredDraft: "Текущий черновик восстановлен",
430
- noHistory: "Истории запросов пока нет"
431
- };
432
- const it = {
433
- ariaLabel: "Navigazione cronologia prompt (freccia su/giù)",
434
- restoredDraft: "Bozza in corso ripristinata",
435
- noHistory: "Nessuna cronologia prompt finora"
436
- };
437
- const nl = {
438
- ariaLabel: "Navigatie door promptgeschiedenis (pijl omhoog/omlaag)",
439
- restoredDraft: "Lopende concept hersteld",
440
- noHistory: "Nog geen promptgeschiedenis"
441
- };
442
- const sv = {
443
- ariaLabel: "Navigera i prompthistorik (pil upp/ner)",
444
- restoredDraft: "Utkast under arbete återställt",
445
- noHistory: "Ingen prompthistorik ännu"
446
- };
447
- const pl = {
448
- ariaLabel: "Nawigacja po historii promptów (strzałka w górę/w dół)",
449
- restoredDraft: "Przywrócono edytowany szkic",
450
- noHistory: "Brak jeszcze historii promptów"
451
- };
452
- const zhHK = {
453
- ariaLabel: "提示詞歷史導覽(上/下方向鍵)",
454
- restoredDraft: "已還原正在編輯的草稿",
455
- noHistory: "暫無提示詞歷史"
456
- };
457
- const zhTW = {
458
- ariaLabel: "提示詞歷史導覽(上/下方向鍵)",
459
- restoredDraft: "已還原正在編輯的草稿",
460
- noHistory: "暫無提示詞歷史"
461
- };
462
- const zhMO = {
463
- ariaLabel: "提示詞歷史導覽(上/下方向鍵)",
464
- restoredDraft: "已還原正在編輯的草稿",
465
- noHistory: "暫無提示詞歷史"
466
- };
467
- /**
468
- * All override dictionaries, keyed by language id, covering the full key
469
- * set. Registered with better-locale under the plugin namespace.
470
- */
471
- const dicts = {
472
- ja,
473
- de,
474
- fr,
475
- pt,
476
- ko,
477
- ar,
478
- hi,
479
- id,
480
- tr,
481
- vi,
482
- th,
483
- ru,
484
- it,
485
- nl,
486
- sv,
487
- pl,
488
- "zh-HK": zhHK,
489
- "zh-TW": zhTW,
490
- "zh-MO": zhMO
491
- };
492
-
493
- //#endregion
494
- //#region src/client/index.ts
495
- /** Required services: slots + locale. */
496
- const inject = ["slots", "locale"];
497
- /**
498
- * Navigation cursor + saved draft for the keydown listener. Module-scoped
499
- * because the listener is attached once in `apply` and must persist across
500
- * dock mount/unmount cycles.
501
- */
502
- let navCursor = null;
503
- let savedDraft = null;
504
- /**
505
- * Client plugin body: register the dock + attach the keydown listener.
506
- *
507
- * @param ctx - client root context.
508
- */
509
- function apply(ctx) {
510
- ctx.effect(() => ctx.locale.register(NS, {
511
- zh,
512
- en
513
- }), "dsh-plugin-input-history: dictionaries");
514
- ctx.effect(() => {
515
- let dispose;
516
- const sync = () => {
517
- dispose?.();
518
- dispose = void 0;
519
- const store = ctx.get("betterLocale");
520
- if (store !== void 0) dispose = store.register(NS, dicts);
521
- };
522
- sync();
523
- const unsubscribe = ctx.locale.subscribe(sync);
524
- return () => {
525
- unsubscribe();
526
- dispose?.();
527
- };
528
- }, "dsh-plugin-input-history: better-locale override dicts");
529
- ctx.slots.inject("conversation.composer.dock", () => ctx.slots.register({
530
- name: "conversation.composer.dock",
531
- id: "dsh-plugin-input-history",
532
- order: 100,
533
- locale: NS
534
- }, HistoryDock));
535
- ctx.effect(() => {
536
- if (typeof document === "undefined") return () => {};
537
- const handler = (event) => {
538
- if (event.key !== "ArrowUp" && event.key !== "ArrowDown") return;
539
- if (isImeComposition(event)) return;
540
- if (event.defaultPrevented) return;
541
- const textarea = findComposerTextarea(event.target);
542
- if (textarea === null) return;
543
- if (event.target !== textarea) return;
544
- if (textarea.readOnly || textarea.disabled) return;
545
- const history = getHistoryStore().list;
546
- const info = cursorLineInfo(textarea.value, textarea.selectionStart, textarea.selectionEnd);
547
- if (event.key === "ArrowUp" && !info.atFirstLine) return;
548
- if (event.key === "ArrowDown" && !info.atLastLine) return;
549
- const dir = event.key === "ArrowUp" ? "up" : "down";
550
- const next = nextIndex(navCursor, history.length, dir);
551
- if (next === null) {
552
- const saved = savedDraft;
553
- navCursor = null;
554
- if (saved !== null) {
555
- setNativeTextareaValue(textarea, saved);
556
- savedDraft = null;
557
- }
558
- event.preventDefault();
559
- return;
560
- }
561
- if (navCursor === null && savedDraft === null) savedDraft = textarea.value;
562
- const entry = entryAt(history, next);
563
- if (entry === null) return;
564
- navCursor = next;
565
- setNativeTextareaValue(textarea, entry);
566
- event.preventDefault();
567
- };
568
- document.addEventListener("keydown", handler, false);
569
- return () => {
570
- document.removeEventListener("keydown", handler, false);
571
- };
572
- }, "dsh-plugin-input-history: keydown listener");
573
- }
574
- /**
575
- * Set the textarea value via the native prototype setter and dispatch an
576
- * `input` event so React's controlled-component onChange fires.
577
- *
578
- * React 18 tracks the textarea's value internally; directly assigning
579
- * `textarea.value = x` does NOT trigger React's onChange because React's
580
- * value tracker compares against its last-seen value. Using the native
581
- * prototype setter bypasses React's tracker, and the dispatched `input`
582
- * event makes React detect the change and run InputBar's `onChange` →
583
- * `keyboard.setDraft(next)`. This is the same technique used by
584
- * browser automation libraries (Playwright, Testing Library) to simulate
585
- * user typing in React controlled inputs.
586
- *
587
- * @param textarea - the target textarea element.
588
- * @param value - the new value to set.
589
- */
590
- function setNativeTextareaValue(textarea, value) {
591
- const proto = window.HTMLTextAreaElement.prototype;
592
- const descriptor = Object.getOwnPropertyDescriptor(proto, "value");
593
- if (descriptor === void 0 || descriptor.set === void 0) {
594
- textarea.value = value;
595
- return;
596
- }
597
- descriptor.set.call(textarea, value);
598
- textarea.dispatchEvent(new Event("input", { bubbles: true }));
599
- }
600
-
601
- //#endregion
602
- exports.apply = apply;
603
- exports.inject = inject;
604
- 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 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 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; } });
605
642
  //# sourceMappingURL=client.js.map