@huanlin/dsh-plugin-input-history 0.1.2 → 0.2.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 +30 -30
- package/lib/client.js +202 -165
- package/lib/client.js.map +1 -1
- package/lib/types/client/HistoryDock.d.ts +27 -15
- package/lib/types/client/HistoryDock.js +136 -32
- package/lib/types/client/dom.d.ts +63 -39
- package/lib/types/client/dom.js +121 -78
- package/lib/types/client/index.d.ts +14 -22
- package/lib/types/client/index.js +16 -124
- package/lib/types/index.d.ts +6 -7
- package/lib/types/index.js +6 -7
- package/package.json +16 -19
- package/lib/invariant.js +0 -24
- package/lib/types/invariant.d.ts +0 -16
- package/lib/types/invariant.js +0 -26
package/lib/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":["capacity: number","parsed: unknown","historyStore: HistoryStore | null","en: Record<InputHistoryKey, string>","zh: Record<InputHistoryKey, string>","ja: Record<InputHistoryKey, string>","de: Record<InputHistoryKey, string>","fr: Record<InputHistoryKey, string>","pt: Record<InputHistoryKey, string>","ko: Record<InputHistoryKey, string>","ar: Record<InputHistoryKey, string>","hi: Record<InputHistoryKey, string>","id: Record<InputHistoryKey, string>","tr: Record<InputHistoryKey, string>","vi: Record<InputHistoryKey, string>","th: Record<InputHistoryKey, string>","ru: Record<InputHistoryKey, string>","it: Record<InputHistoryKey, string>","nl: Record<InputHistoryKey, string>","sv: Record<InputHistoryKey, string>","pl: Record<InputHistoryKey, string>","zhHK: Record<InputHistoryKey, string>","zhTW: Record<InputHistoryKey, string>","zhMO: Record<InputHistoryKey, string>","dicts: Record<string, Record<InputHistoryKey, string>>","navCursor: number | null","savedDraft: string | null","dispose: (() => void) | undefined"],"sources":["../src/client/history.ts","../src/client/HistoryDock.tsx","../src/client/ime.ts","../src/client/dom.ts","../src/client/locales.ts","../src/client/dictionaries.ts","../src/client/index.ts"],"sourcesContent":["/**\n * Prompt history store — pure functions over a string array.\n *\n * The store is a FIFO list of unique prompt strings, persisted to\n * `localStorage`. Newest entries are at the end of the array. The\n * navigation cursor walks backwards from the end (ArrowUp = older,\n * ArrowDown = newer).\n *\n * The functions in this module are pure (no `localStorage` access) so\n * they can be unit-tested without jsdom. The `HistoryStore` class below\n * wires them to `localStorage` with try/catch containment — a quota\n * exception or a disabled storage (private mode) degrades gracefully to\n * an in-memory list that lives for the page lifetime.\n *\n * @module @huanlin/dsh-plugin-input-history/client/history\n */\n\n/** localStorage key (versioned; bump on schema changes to start fresh). */\nexport const STORAGE_KEY = 'dsh-plugin-input-history:v1'\n\n/** Default capacity when none is configured. */\nexport const DEFAULT_CAPACITY = 500\n\n/**\n * Append a prompt to the history.\n *\n * Rules:\n * - Empty / whitespace-only strings are ignored (the InputBar already\n * rejects them at submit, but defensive).\n * - When the new entry equals the most recent one, it is a no-op\n * (avoids stacking duplicates from rapid resends).\n * - When the new entry already exists earlier in the history, that\n * earlier occurrence is removed (recency wins; the prompt moves to\n * the end). This mirrors terminal shell behaviour.\n * - When the array would exceed `capacity`, the oldest entries are\n * dropped from the front (FIFO).\n *\n * @param history - the current history array (newest at end).\n * @param prompt - the prompt to append.\n * @param capacity - the maximum number of entries to retain.\n * @returns the new history array (may be the same reference if no-op).\n */\nexport function appendHistory(\n history: readonly string[],\n prompt: string,\n capacity: number = DEFAULT_CAPACITY,\n): string[] {\n const trimmed = prompt.trim()\n if (trimmed === '') return history as string[]\n // Latest-equal with no earlier duplicate: true no-op (same reference).\n // When an earlier duplicate exists, the filter below removes it so the\n // entry moves to the end (recency wins).\n const lastIndex = history.lastIndexOf(trimmed)\n if (lastIndex !== -1 && lastIndex === history.length - 1 && history.indexOf(trimmed) === lastIndex) {\n return history as string[]\n }\n // Remove any earlier occurrence (recency wins).\n const filtered = history.filter(item => item !== trimmed)\n filtered.push(trimmed)\n // FIFO: drop oldest entries from the front.\n const cap = Math.max(1, capacity)\n if (filtered.length > cap) {\n return filtered.slice(filtered.length - cap)\n }\n return filtered\n}\n\n/**\n * Navigation cursor for walking the history.\n *\n * The cursor is `null` when the user is not navigating (i.e. they are\n * typing a fresh draft). ArrowUp sets it to the last index, then\n * decrements; ArrowDown increments; when it would exceed `history.length\n * - 1`, it returns to `null` (meaning \"restore the in-progress draft\").\n *\n * @param current - the current cursor (null = not navigating).\n * @param total - the total number of history entries.\n * @param dir - `'up'` (older) or `'down'` (newer).\n * @returns the next cursor, or `null` when navigation falls off the\n * newest end (caller should restore the saved draft).\n */\nexport function nextIndex(\n current: number | null,\n total: number,\n dir: 'up' | 'down',\n): number | null {\n if (total === 0) return null\n if (dir === 'up') {\n if (current === null) return total - 1\n if (current <= 0) return 0\n return current - 1\n }\n // dir === 'down'\n if (current === null) return null\n if (current >= total - 1) return null\n return current + 1\n}\n\n/**\n * Read the history entry at a cursor, or `null` when the cursor is null.\n *\n * @param history - the history array.\n * @param cursor - the navigation cursor.\n * @returns the prompt at the cursor, or `null`.\n */\nexport function entryAt(\n history: readonly string[],\n cursor: number | null,\n): string | null {\n if (cursor === null) return null\n if (cursor < 0 || cursor >= history.length) return null\n return history[cursor] ?? null\n}\n\n/**\n * History store bound to `localStorage`.\n *\n * The store reads once on construction (or on `reload()`) and keeps an\n * in-memory copy. Writes go to both memory and `localStorage` inside a\n * try/catch — a quota exception leaves the in-memory copy authoritative\n * for the rest of the page lifetime. This trades cross-tab consistency\n * for resilience: the store never throws on a write, and the worst case\n * is that a tab keeps its own view until refresh.\n *\n * Cross-tab sync is intentionally NOT implemented: prompt history is\n * append-mostly and a stale read across tabs is harmless (the next\n * append corrects it). Listening to the `storage` event would add\n * reactivity that the navigation UI does not need.\n */\nexport class HistoryStore {\n private items: string[]\n private readonly storage: Storage | null\n private readonly key: string\n\n /**\n * @param capacity - maximum entries to retain (FIFO).\n * @param storage - the storage backend (defaults to `localStorage` when available).\n * @param key - the storage key (defaults to {@link STORAGE_KEY}).\n */\n constructor(\n private readonly capacity: number = DEFAULT_CAPACITY,\n storage?: Storage | null,\n key: string = STORAGE_KEY,\n ) {\n this.storage = storage ?? safeLocalStorage()\n this.key = key\n this.items = this.readFromStorage()\n }\n\n /** Current history snapshot (newest at end). */\n get list(): readonly string[] {\n return this.items\n }\n\n /** Number of entries currently stored. */\n get length(): number {\n return this.items.length\n }\n\n /** Reload from storage (e.g. after a suspected external edit). Truncates to the current capacity. */\n reload(): void {\n const loaded = this.readFromStorage()\n const cap = Math.max(1, this.capacity)\n this.items = loaded.length > cap ? loaded.slice(loaded.length - cap) : loaded\n }\n\n /**\n * Append a prompt and persist. See {@link appendHistory} for rules.\n * @returns the new history snapshot.\n */\n append(prompt: string): readonly string[] {\n this.items = appendHistory(this.items, prompt, this.capacity)\n this.writeToStorage()\n return this.items\n }\n\n /** Clear all history (used by tests and a future \"clear\" UI). */\n clear(): void {\n this.items = []\n this.writeToStorage()\n }\n\n private readFromStorage(): string[] {\n if (this.storage === null) return []\n try {\n const raw = this.storage.getItem(this.key)\n if (raw === null) return []\n const parsed: unknown = JSON.parse(raw)\n if (!Array.isArray(parsed)) return []\n return parsed.filter((item): item is string => typeof item === 'string')\n } catch {\n return []\n }\n }\n\n private writeToStorage(): void {\n if (this.storage === null) return\n try {\n this.storage.setItem(this.key, JSON.stringify(this.items))\n } catch {\n // Quota exceeded, private mode, or disabled storage: keep the\n // in-memory copy authoritative for the rest of the page lifetime.\n }\n }\n}\n\n/** Safe accessor for `localStorage` that returns null on any failure. */\nfunction safeLocalStorage(): Storage | null {\n try {\n if (typeof localStorage === 'undefined') return null\n return localStorage\n } catch {\n return null\n }\n}\n","/**\n * HistoryDock — invisible dock entry that collects prompt history.\n *\n * Registers as a `conversation.composer.dock` list entry and renders an\n * `aria-hidden` anchor (zero layout footprint). The dock's only job is\n * history collection: every render reads `props.session.nodes` and\n * appends new user/steering text to the module-scope `HistoryStore`.\n *\n * The keydown listener that drives navigation lives in `apply` (module\n * scope), NOT in this dock — because the dock is session-scoped and\n * DSH treats blank sessions as \"hero\" (ConversationRoot.tsx:79-80),\n * which suppresses the dock entirely (`!hero` guard at line 156).\n * Moving the listener to `apply` ensures it is always attached,\n * regardless of hero/blank/active session state.\n *\n * @module @huanlin/dsh-plugin-input-history/client/HistoryDock\n */\n\nimport { useRef } from 'react'\nimport type { PropsRuntime, PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'\n// Type-only: SlotMap merge for 'conversation.composer.dock' + SessionStandardProps.\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\nimport { HistoryStore, DEFAULT_CAPACITY } from './history.ts'\n\n/** Full props: dock runtime share + locale seat. */\ntype HistoryDockProps = PropsRuntime<'conversation.composer.dock'> & PropsLocale<'dsh-plugin-input-history'>\n\n/**\n * Module-scope history store, initialized once on first dock mount.\n * Shared with the keydown listener in `apply` via `getHistoryStore()`.\n */\nlet historyStore: HistoryStore | null = null\n\n/** Get the shared history store (initializes lazily on first call). */\nexport function getHistoryStore(): HistoryStore {\n if (historyStore === null) {\n historyStore = new HistoryStore(DEFAULT_CAPACITY)\n }\n return historyStore\n}\n\n/**\n * Render the invisible history-collection dock entry.\n *\n * @param props - dock runtime share (InputZone owner + session kit) + locale seat.\n * @returns an `aria-hidden` anchor with zero layout footprint.\n */\nexport function HistoryDock({ session }: HistoryDockProps) {\n const store = getHistoryStore()\n\n // History collection: diff the last user/steering text against the\n // previously-seen tail and append on change. Runs every render (cheap:\n // O(n) over the tail nodes, breaks early once a user/steering node is\n // found). The store dedupes internally, so re-appends are no-ops.\n const lastSeenTextRef = useRef<string | null>(null)\n const lastText = latestUserOrSteeringText(session.nodes)\n if (lastText !== null && lastText !== lastSeenTextRef.current) {\n lastSeenTextRef.current = lastText\n store.append(lastText)\n }\n\n // `display: none` keeps the anchor out of layout and out of the\n // a11y tree. The dock is purely a lifecycle anchor for history\n // collection; the keydown listener lives in `apply`.\n return <div aria-hidden style={{ display: 'none' }} data-dsh-plugin-input-history=\"\" />\n}\n\n/**\n * Extract the text of the latest `user` or `steering` node from a\n * conversation snapshot's nodes list.\n *\n * Returns the concatenated text of all `type: 'text'` content blocks.\n * Returns `null` when no user/steering node is present (e.g. a fresh\n * session with only a system/context message).\n *\n * @param nodes - the conversation snapshot's `nodes` array.\n */\nfunction latestUserOrSteeringText(\n nodes: ReadonlyArray<{\n kind: string\n content?: ReadonlyArray<{ type: string; text?: string }>\n }>,\n): string | null {\n for (let i = nodes.length - 1; i >= 0; i--) {\n const node = nodes[i]!\n if (node.kind !== 'user' && node.kind !== 'steering') continue\n const content = node.content\n if (content === undefined) continue\n let text = ''\n for (const block of content) {\n if (block.type === 'text' && typeof block.text === 'string') {\n text += block.text\n }\n }\n return text\n }\n return null\n}\n","/**\n * IME-composition key guard.\n *\n * While a Chinese/Japanese/Korean input method is composing (the user is\n * picking a candidate from the IME window), every pressed key BELONGS to\n * the input method: arrows move the candidate highlight, Enter/Space\n * confirm the composition, Escape cancels it. Page code must not process\n * those keys — a history-navigation handler that calls `preventDefault()`\n * on ArrowUp/ArrowDown during composition would silently break the IME:\n * candidates stop responding, the composition gets torn apart, and only\n * bare letters come out.\n *\n * The composition signal follows the DSH core convention (InputBar's IME\n * guard, issue #535): `isComposing` for modern engines, keyCode 229 as\n * the legacy signal engines emit without isComposing.\n *\n * @module @huanlin/dsh-plugin-input-history/client/ime\n */\n\n/** The pure decision: is this keyboard event part of an IME composition? */\nexport function isImeComposition(event: { isComposing: boolean; keyCode: number }): boolean {\n return event.isComposing || event.keyCode === 229\n}\n","/**\n * DOM helpers for the composer textarea.\n *\n * The DSH InputBar's textarea is not exposed through any public API —\n * plugins cannot obtain a React ref or a slot-currency handle to it.\n * The two operations this plugin needs (locate the textarea, decide\n * whether the caret is on the first/last line of a multi-line draft) are\n * pure functions over DOM and string state, kept here for unit testing.\n *\n * The locator queries the stable (but undocumented) `data-composer-card`\n * attribute on the composer card root (`InputBar.tsx:629`) and returns\n * the descendant `<textarea>`. The attribute is internal to\n * `@deepseek-ai/dsh-client-ui-conversation` and may change across\n * upstream versions; the locator is the single point to update.\n *\n * @module @huanlin/dsh-plugin-input-history/client/dom\n */\n\n/** Caret line information for a multi-line textarea value. */\nexport interface CursorLineInfo {\n /** 0-based index of the line the caret is on. */\n readonly currentLine: number\n /** Total number of lines in the value (>= 1). */\n readonly totalLines: number\n /** True when the caret is collapsed and on the first line. */\n readonly atFirstLine: boolean\n /** True when the caret is collapsed and on the last line. */\n readonly atLastLine: boolean\n}\n\n/**\n * Compute the caret's line position in a textarea value.\n *\n * Lines are split on `\\n` (the textarea's own line break character). The\n * caret must be collapsed (`selectionStart === selectionEnd`) for the\n * `atFirstLine` / `atLastLine` flags to be true — a non-collapsed\n * selection spanning multiple lines should not trigger history navigation.\n *\n * @param value - the textarea's current value.\n * @param selectionStart - the textarea's `selectionStart`.\n * @param selectionEnd - the textarea's `selectionEnd` (defaults to `selectionStart`).\n * @returns the caret's line information.\n */\nexport function cursorLineInfo(\n value: string,\n selectionStart: number,\n selectionEnd: number = selectionStart,\n): CursorLineInfo {\n // Swap if reversed (the browser allows selectionStart > selectionEnd when\n // the user drags upwards); clamp to value bounds.\n const rawStart = Math.min(selectionStart, selectionEnd)\n const rawEnd = Math.max(selectionStart, selectionEnd)\n const clampedStart = Math.max(0, Math.min(rawStart, value.length))\n const clampedEnd = Math.max(clampedStart, Math.min(rawEnd, value.length))\n const collapsed = clampedStart === clampedEnd\n const lines = value.split('\\n')\n const totalLines = lines.length\n let currentLine = 0\n let runningLength = 0\n for (let i = 0; i < totalLines; i++) {\n const line = lines[i]!\n // The caret at position `p` belongs to line `i` if `p` is in\n // [runningLength, runningLength + line.length + 1) — the `+1` covers\n // the position immediately after the line's last character, which is\n // still on this line (right before the `\\n`). The very end of the\n // value (after the last line's last char) belongs to the last line.\n const lineEnd = runningLength + line.length\n const isLastLine = i === totalLines - 1\n const upperBound = isLastLine ? lineEnd + 1 : lineEnd + 1 // include the `\\n` position\n if (clampedStart >= runningLength && clampedStart < upperBound) {\n currentLine = i\n break\n }\n runningLength = lineEnd + 1 // +1 for the `\\n`\n }\n return {\n currentLine,\n totalLines,\n atFirstLine: collapsed && currentLine === 0,\n atLastLine: collapsed && currentLine === totalLines - 1,\n }\n}\n\n/**\n * Locate the DSH composer textarea in the current document.\n *\n * Walks from the event target up to find the closest `[data-composer-card]`\n * ancestor, then queries the descendant `<textarea>` inside it. Returns\n * `null` when the target is not inside the composer card (e.g. the user\n * is typing in another input or the textarea is momentarily absent).\n *\n * When called without an event target, falls back to a document-wide\n * query — used in tests and ad-hoc probing.\n *\n * @param from - the event target (or any node inside the composer card).\n * @returns the textarea element, or `null` when not found.\n */\nexport function findComposerTextarea(from?: EventTarget | null): HTMLTextAreaElement | null {\n if (typeof document === 'undefined') return null\n if (from === undefined) {\n // No argument: document-wide query.\n return document.querySelector<HTMLTextAreaElement>('[data-composer-card] textarea')\n }\n // `null` or an actual target: do NOT fall back to document-wide query.\n if (from === null) return null\n // `closest` is on Element; EventTarget may be a Text node or other\n // non-Element node. Narrow with an instanceof check.\n const card = from instanceof Element ? from.closest('[data-composer-card]') : null\n if (card !== null) {\n const ta = card.querySelector<HTMLTextAreaElement>('textarea')\n if (ta !== null) return ta\n }\n return null\n}\n","/**\n * Locale dictionaries for dsh-plugin-input-history.\n *\n * The plugin renders nothing visible — the only user-facing copy is the\n * `aria-label` on the invisible dock anchor (for screen readers) and a\n * future settings row label.\n *\n * @module @huanlin/dsh-plugin-input-history/client/locales\n */\n\n/** All copy keys for the dsh-plugin-input-history namespace. */\nexport type InputHistoryKey =\n | 'ariaLabel'\n | 'restoredDraft'\n | 'noHistory'\n\n/** Locale namespace id (matches the cordis.patch.yml plugin id). */\nexport const NS = 'dsh-plugin-input-history'\n\n/** English dictionary. */\nexport const en: Record<InputHistoryKey, string> = {\n ariaLabel: 'Prompt history navigation (ArrowUp/ArrowDown)',\n restoredDraft: 'Restored in-progress draft',\n noHistory: 'No prompt history yet',\n}\n\n/** Chinese dictionary. */\nexport const zh: Record<InputHistoryKey, string> = {\n ariaLabel: '提示词历史导航(上/下方向键)',\n restoredDraft: '已恢复正在编辑的草稿',\n noHistory: '暂无提示词历史',\n}\n","/**\n * Override dictionaries for the 19 languages better-locale ships. Each\n * dict covers the full `dsh-plugin-input-history` key set (ariaLabel /\n * restoredDraft / noHistory), no placeholders — these are plain strings.\n *\n * Registered with better-locale only (see [index.ts](./index.ts)): the\n * override borrows DSH's English slot, so these render when the user\n * selected an override language AND DSH's active locale is `'en'`. zh-HK /\n * zh-TW / zh-MO have no regional variants for this copy, so the three\n * Traditional Chinese dicts are identical.\n */\n\nimport type { InputHistoryKey } from './locales.ts'\n\nconst ja: Record<InputHistoryKey, string> = {\n ariaLabel: 'プロンプト履歴のナビゲーション(↑/↓キー)',\n restoredDraft: '編集中の下書きを復元しました',\n noHistory: 'プロンプト履歴はまだありません',\n}\n\nconst de: Record<InputHistoryKey, string> = {\n ariaLabel: 'Befehlsverlauf-Navigation (Pfeil hoch/runter)',\n restoredDraft: 'In Bearbeitung befindlicher Entwurf wiederhergestellt',\n noHistory: 'Noch kein Befehlsverlauf vorhanden',\n}\n\nconst fr: Record<InputHistoryKey, string> = {\n ariaLabel: 'Navigation dans l\\'historique des invites (flèche haut/bas)',\n restoredDraft: 'Brouillon en cours d\\'édition restauré',\n noHistory: 'Pas encore d\\'historique d\\'invites',\n}\n\nconst pt: Record<InputHistoryKey, string> = {\n ariaLabel: 'Navegação no histórico de prompts (seta para cima/baixo)',\n restoredDraft: 'Rascunho em edição restaurado',\n noHistory: 'Ainda não há histórico de prompts',\n}\n\nconst ko: Record<InputHistoryKey, string> = {\n ariaLabel: '프롬프트 기록 탐색 (위/아래 화살표)',\n restoredDraft: '편집 중이던 초안을 복원했습니다',\n noHistory: '아직 프롬프트 기록이 없습니다',\n}\n\nconst ar: Record<InputHistoryKey, string> = {\n ariaLabel: 'التنقل في سجل الأوامر (السهم لأعلى/لأسفل)',\n restoredDraft: 'تمت استعادة المسودة قيد التحرير',\n noHistory: 'لا يوجد سجل أوامر بعد',\n}\n\nconst hi: Record<InputHistoryKey, string> = {\n ariaLabel: 'प्रॉम्प्ट इतिहास नेविगेशन (ऊपर/नीचे तीर)',\n restoredDraft: 'संपादन में मौजूद ड्राफ्ट पुनर्स्थापित किया गया',\n noHistory: 'अभी तक कोई प्रॉम्प्ट इतिहास नहीं',\n}\n\nconst id: Record<InputHistoryKey, string> = {\n ariaLabel: 'Navigasi riwayat prompt (panah atas/bawah)',\n restoredDraft: 'Draf yang sedang diedit dipulihkan',\n noHistory: 'Belum ada riwayat prompt',\n}\n\nconst tr: Record<InputHistoryKey, string> = {\n ariaLabel: 'Komut geçmişinde gezinme (yukarı/aşağı ok)',\n restoredDraft: 'Düzenlenmekte olan taslak geri yüklendi',\n noHistory: 'Henüz komut geçmişi yok',\n}\n\nconst vi: Record<InputHistoryKey, string> = {\n ariaLabel: 'Điều hướng lịch sử lệnh (mũi tên lên/xuống)',\n restoredDraft: 'Đã khôi phục bản nháp đang soạn',\n noHistory: 'Chưa có lịch sử lệnh',\n}\n\nconst th: Record<InputHistoryKey, string> = {\n ariaLabel: 'นำทางประวัติคำสั่ง (ลูกศรขึ้น/ลง)',\n restoredDraft: 'กู้คืนฉบับร่างที่กำลังแก้ไขแล้ว',\n noHistory: 'ยังไม่มีประวัติคำสั่ง',\n}\n\nconst ru: Record<InputHistoryKey, string> = {\n ariaLabel: 'Навигация по истории запросов (стрелки вверх/вниз)',\n restoredDraft: 'Текущий черновик восстановлен',\n noHistory: 'Истории запросов пока нет',\n}\n\nconst it: Record<InputHistoryKey, string> = {\n ariaLabel: 'Navigazione cronologia prompt (freccia su/giù)',\n restoredDraft: 'Bozza in corso ripristinata',\n noHistory: 'Nessuna cronologia prompt finora',\n}\n\nconst nl: Record<InputHistoryKey, string> = {\n ariaLabel: 'Navigatie door promptgeschiedenis (pijl omhoog/omlaag)',\n restoredDraft: 'Lopende concept hersteld',\n noHistory: 'Nog geen promptgeschiedenis',\n}\n\nconst sv: Record<InputHistoryKey, string> = {\n ariaLabel: 'Navigera i prompthistorik (pil upp/ner)',\n restoredDraft: 'Utkast under arbete återställt',\n noHistory: 'Ingen prompthistorik ännu',\n}\n\nconst pl: Record<InputHistoryKey, string> = {\n ariaLabel: 'Nawigacja po historii promptów (strzałka w górę/w dół)',\n restoredDraft: 'Przywrócono edytowany szkic',\n noHistory: 'Brak jeszcze historii promptów',\n}\n\nconst zhHK: Record<InputHistoryKey, string> = {\n ariaLabel: '提示詞歷史導覽(上/下方向鍵)',\n restoredDraft: '已還原正在編輯的草稿',\n noHistory: '暫無提示詞歷史',\n}\n\nconst zhTW: Record<InputHistoryKey, string> = {\n ariaLabel: '提示詞歷史導覽(上/下方向鍵)',\n restoredDraft: '已還原正在編輯的草稿',\n noHistory: '暫無提示詞歷史',\n}\n\nconst zhMO: Record<InputHistoryKey, string> = {\n ariaLabel: '提示詞歷史導覽(上/下方向鍵)',\n restoredDraft: '已還原正在編輯的草稿',\n noHistory: '暫無提示詞歷史',\n}\n\n/**\n * All override dictionaries, keyed by language id, covering the full key\n * set. Registered with better-locale under the plugin namespace.\n */\nexport const dicts: Record<string, Record<InputHistoryKey, string>> = {\n ja, de, fr, pt, ko, ar, hi, id, tr, vi, th, ru, it, nl, sv, pl,\n 'zh-HK': zhHK, 'zh-TW': zhTW, 'zh-MO': zhMO,\n}","/**\n * dsh-plugin-input-history — browser half.\n *\n * Two registrations:\n * - `conversation.composer.dock` list slot (id `dsh-plugin-input-history`,\n * order 100) — renders an invisible anchor that collects history from\n * `session.nodes` every render. The dock is session-scoped; DSH treats\n * blank sessions as \"hero\" and suppresses the dock, so history\n * collection only runs in active sessions. That is fine: the first\n * message in a blank session is collected after the session becomes\n * active (the message makes it non-blank).\n * - A document-level `keydown` listener attached in `apply` (NOT in the\n * dock component) — this ensures the listener is always active,\n * including in hero/blank mode where the dock is suppressed. The\n * listener uses the native `value` setter + `dispatchEvent('input')`\n * to feed history text into the textarea, which triggers InputBar's\n * `onChange` → `keyboard.setDraft` — the same path the user's typing\n * takes.\n *\n * History is collected from `user` and `steering` conversation nodes as\n * they appear in any session's `ConversationSnapshot`, persisted to\n * `localStorage` (FIFO, 500 entries), and shared across all sessions\n * in the same browser profile.\n *\n * @module @huanlin/dsh-plugin-input-history/client\n */\n\nimport type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'\n// Type-only: pulls the locale plugin's Context merge (ctx.locale).\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\n// Type-only: pulls the shell's SlotMap merge (conversation.composer.dock)\n// + SessionStandardProps (useInput, inputActions).\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\nimport { HistoryDock, getHistoryStore } from './HistoryDock.tsx'\nimport { isImeComposition } from './ime.ts'\nimport { cursorLineInfo, findComposerTextarea } from './dom.ts'\nimport { nextIndex, entryAt } from './history.ts'\nimport { en, NS, zh, type InputHistoryKey } from './locales.ts'\nimport { dicts } from './dictionaries.ts'\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** The dock's aria-label + future settings row copy. */\n 'dsh-plugin-input-history': InputHistoryKey\n }\n}\n\n/** Required services: slots + locale. */\nexport const inject = ['slots', 'locale']\n\n/** Structural view of better-locale's override store (optional; no runtime dep). */\ninterface BetterLocaleOverrideStore {\n register(ns: string, dicts: Record<string, Record<string, string>>): () => void\n}\n\n/**\n * Navigation cursor + saved draft for the keydown listener. Module-scoped\n * because the listener is attached once in `apply` and must persist across\n * dock mount/unmount cycles.\n */\nlet navCursor: number | null = null\nlet savedDraft: string | null = null\n\n/**\n * Client plugin body: register the dock + attach the keydown listener.\n *\n * @param ctx - client root context.\n */\nexport function apply(ctx: ClientContext): void {\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-plugin-input-history: dictionaries')\n\n // better-locale override: register the 19-language dicts so a selected\n // override language (with DSH on 'en') replaces the plugin's copy. The\n // service is optional — no better-locale, no dicts.\n // Activation-order-safe: re-check ctx.get('betterLocale') on every locale\n // revision bump (better-locale bumps on activation + override switch).\n ctx.effect(() => {\n let dispose: (() => void) | undefined\n const sync = (): void => {\n dispose?.()\n dispose = undefined\n const store = ctx.get('betterLocale') as BetterLocaleOverrideStore | undefined\n if (store !== undefined) {\n dispose = store.register(NS, dicts)\n }\n }\n sync()\n const unsubscribe = ctx.locale.subscribe(sync)\n return () => {\n unsubscribe()\n dispose?.()\n }\n }, 'dsh-plugin-input-history: better-locale override dicts')\n\n // The dock collects history from session.nodes. It is session-scoped;\n // in hero/blank mode it is suppressed, but the keydown listener below\n // still works (it reads from the module-scope HistoryStore, which\n // persists across dock mount/unmount cycles via localStorage).\n ctx.slots.inject('conversation.composer.dock', () =>\n ctx.slots.register(\n {\n name: 'conversation.composer.dock',\n id: 'dsh-plugin-input-history',\n order: 100,\n locale: NS,\n },\n HistoryDock,\n ),\n )\n\n // Attach the document-level keydown listener. This lives in `apply`\n // (not in the dock component) so it stays active even when the dock is\n // suppressed (hero/blank sessions — ConversationRoot.tsx:79-80 treats\n // blank sessions as hero, and line 156 skips the dock render).\n ctx.effect(() => {\n if (typeof document === 'undefined') return () => {}\n const handler = (event: KeyboardEvent): void => {\n if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return\n if (isImeComposition(event)) return\n if (event.defaultPrevented) return\n const textarea = findComposerTextarea(event.target)\n if (textarea === null) return\n if (event.target !== textarea) return\n // Skip if the textarea is readOnly or disabled — hero mode's\n // workspace-picker trigger, or a submitting machine phase.\n if (textarea.readOnly || textarea.disabled) return\n\n const store = getHistoryStore()\n const history = store.list\n\n // Multi-line boundary check.\n const info = cursorLineInfo(textarea.value, textarea.selectionStart, textarea.selectionEnd)\n if (event.key === 'ArrowUp' && !info.atFirstLine) return\n if (event.key === 'ArrowDown' && !info.atLastLine) return\n\n const dir = event.key === 'ArrowUp' ? 'up' : 'down'\n const next = nextIndex(navCursor, history.length, dir)\n\n // Down off the newest end: restore the saved draft (if any).\n if (next === null) {\n const saved = savedDraft\n navCursor = null\n if (saved !== null) {\n setNativeTextareaValue(textarea, saved)\n savedDraft = null\n }\n event.preventDefault()\n return\n }\n\n // Entering history: save the current draft the first time we\n // navigate away from \"not navigating\".\n if (navCursor === null && savedDraft === null) {\n savedDraft = textarea.value\n }\n\n const entry = entryAt(history, next)\n if (entry === null) return\n navCursor = next\n setNativeTextareaValue(textarea, entry)\n event.preventDefault()\n }\n document.addEventListener('keydown', handler, false)\n return () => {\n document.removeEventListener('keydown', handler, false)\n }\n }, 'dsh-plugin-input-history: keydown listener')\n}\n\n/**\n * Set the textarea value via the native prototype setter and dispatch an\n * `input` event so React's controlled-component onChange fires.\n *\n * React 18 tracks the textarea's value internally; directly assigning\n * `textarea.value = x` does NOT trigger React's onChange because React's\n * value tracker compares against its last-seen value. Using the native\n * prototype setter bypasses React's tracker, and the dispatched `input`\n * event makes React detect the change and run InputBar's `onChange` →\n * `keyboard.setDraft(next)`. This is the same technique used by\n * browser automation libraries (Playwright, Testing Library) to simulate\n * user typing in React controlled inputs.\n *\n * @param textarea - the target textarea element.\n * @param value - the new value to set.\n */\nfunction setNativeTextareaValue(textarea: HTMLTextAreaElement, value: string): void {\n const proto = window.HTMLTextAreaElement.prototype\n const descriptor = Object.getOwnPropertyDescriptor(proto, 'value')\n if (descriptor === undefined || descriptor.set === undefined) {\n // Fallback: direct assignment (may not trigger React onChange in\n // all browsers, but better than nothing).\n textarea.value = value\n return\n }\n descriptor.set.call(textarea, value)\n textarea.dispatchEvent(new Event('input', { bubbles: true }))\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client.js","names":["capacity: number","parsed: unknown","tops: number[]","historyStore: HistoryStore | null","en: Record<InputHistoryKey, string>","zh: Record<InputHistoryKey, string>","ja: Record<InputHistoryKey, string>","de: Record<InputHistoryKey, string>","fr: Record<InputHistoryKey, string>","pt: Record<InputHistoryKey, string>","ko: Record<InputHistoryKey, string>","ar: Record<InputHistoryKey, string>","hi: Record<InputHistoryKey, string>","id: Record<InputHistoryKey, string>","tr: Record<InputHistoryKey, string>","vi: Record<InputHistoryKey, string>","th: Record<InputHistoryKey, string>","ru: Record<InputHistoryKey, string>","it: Record<InputHistoryKey, string>","nl: Record<InputHistoryKey, string>","sv: Record<InputHistoryKey, string>","pl: Record<InputHistoryKey, string>","zhHK: Record<InputHistoryKey, string>","zhTW: Record<InputHistoryKey, string>","zhMO: Record<InputHistoryKey, string>","dicts: Record<string, Record<InputHistoryKey, string>>","dispose: (() => void) | undefined"],"sources":["../src/client/history.ts","../src/client/dom.ts","../src/client/ime.ts","../src/client/HistoryDock.tsx","../src/client/locales.ts","../src/client/dictionaries.ts","../src/client/index.ts"],"sourcesContent":["/**\n * Prompt history store — pure functions over a string array.\n *\n * The store is a FIFO list of unique prompt strings, persisted to\n * `localStorage`. Newest entries are at the end of the array. The\n * navigation cursor walks backwards from the end (ArrowUp = older,\n * ArrowDown = newer).\n *\n * The functions in this module are pure (no `localStorage` access) so\n * they can be unit-tested without jsdom. The `HistoryStore` class below\n * wires them to `localStorage` with try/catch containment — a quota\n * exception or a disabled storage (private mode) degrades gracefully to\n * an in-memory list that lives for the page lifetime.\n *\n * @module @huanlin/dsh-plugin-input-history/client/history\n */\n\n/** localStorage key (versioned; bump on schema changes to start fresh). */\nexport const STORAGE_KEY = 'dsh-plugin-input-history:v1'\n\n/** Default capacity when none is configured. */\nexport const DEFAULT_CAPACITY = 500\n\n/**\n * Append a prompt to the history.\n *\n * Rules:\n * - Empty / whitespace-only strings are ignored (the InputBar already\n * rejects them at submit, but defensive).\n * - When the new entry equals the most recent one, it is a no-op\n * (avoids stacking duplicates from rapid resends).\n * - When the new entry already exists earlier in the history, that\n * earlier occurrence is removed (recency wins; the prompt moves to\n * the end). This mirrors terminal shell behaviour.\n * - When the array would exceed `capacity`, the oldest entries are\n * dropped from the front (FIFO).\n *\n * @param history - the current history array (newest at end).\n * @param prompt - the prompt to append.\n * @param capacity - the maximum number of entries to retain.\n * @returns the new history array (may be the same reference if no-op).\n */\nexport function appendHistory(\n history: readonly string[],\n prompt: string,\n capacity: number = DEFAULT_CAPACITY,\n): string[] {\n const trimmed = prompt.trim()\n if (trimmed === '') return history as string[]\n // Latest-equal with no earlier duplicate: true no-op (same reference).\n // When an earlier duplicate exists, the filter below removes it so the\n // entry moves to the end (recency wins).\n const lastIndex = history.lastIndexOf(trimmed)\n if (lastIndex !== -1 && lastIndex === history.length - 1 && history.indexOf(trimmed) === lastIndex) {\n return history as string[]\n }\n // Remove any earlier occurrence (recency wins).\n const filtered = history.filter(item => item !== trimmed)\n filtered.push(trimmed)\n // FIFO: drop oldest entries from the front.\n const cap = Math.max(1, capacity)\n if (filtered.length > cap) {\n return filtered.slice(filtered.length - cap)\n }\n return filtered\n}\n\n/**\n * Navigation cursor for walking the history.\n *\n * The cursor is `null` when the user is not navigating (i.e. they are\n * typing a fresh draft). ArrowUp sets it to the last index, then\n * decrements; ArrowDown increments; when it would exceed `history.length\n * - 1`, it returns to `null` (meaning \"restore the in-progress draft\").\n *\n * @param current - the current cursor (null = not navigating).\n * @param total - the total number of history entries.\n * @param dir - `'up'` (older) or `'down'` (newer).\n * @returns the next cursor, or `null` when navigation falls off the\n * newest end (caller should restore the saved draft).\n */\nexport function nextIndex(\n current: number | null,\n total: number,\n dir: 'up' | 'down',\n): number | null {\n if (total === 0) return null\n if (dir === 'up') {\n if (current === null) return total - 1\n if (current <= 0) return 0\n return current - 1\n }\n // dir === 'down'\n if (current === null) return null\n if (current >= total - 1) return null\n return current + 1\n}\n\n/**\n * Read the history entry at a cursor, or `null` when the cursor is null.\n *\n * @param history - the history array.\n * @param cursor - the navigation cursor.\n * @returns the prompt at the cursor, or `null`.\n */\nexport function entryAt(\n history: readonly string[],\n cursor: number | null,\n): string | null {\n if (cursor === null) return null\n if (cursor < 0 || cursor >= history.length) return null\n return history[cursor] ?? null\n}\n\n/**\n * History store bound to `localStorage`.\n *\n * The store reads once on construction (or on `reload()`) and keeps an\n * in-memory copy. Writes go to both memory and `localStorage` inside a\n * try/catch — a quota exception leaves the in-memory copy authoritative\n * for the rest of the page lifetime. This trades cross-tab consistency\n * for resilience: the store never throws on a write, and the worst case\n * is that a tab keeps its own view until refresh.\n *\n * Cross-tab sync is intentionally NOT implemented: prompt history is\n * append-mostly and a stale read across tabs is harmless (the next\n * append corrects it). Listening to the `storage` event would add\n * reactivity that the navigation UI does not need.\n */\nexport class HistoryStore {\n private items: string[]\n private readonly storage: Storage | null\n private readonly key: string\n\n /**\n * @param capacity - maximum entries to retain (FIFO).\n * @param storage - the storage backend (defaults to `localStorage` when available).\n * @param key - the storage key (defaults to {@link STORAGE_KEY}).\n */\n constructor(\n private readonly capacity: number = DEFAULT_CAPACITY,\n storage?: Storage | null,\n key: string = STORAGE_KEY,\n ) {\n this.storage = storage ?? safeLocalStorage()\n this.key = key\n this.items = this.readFromStorage()\n }\n\n /** Current history snapshot (newest at end). */\n get list(): readonly string[] {\n return this.items\n }\n\n /** Number of entries currently stored. */\n get length(): number {\n return this.items.length\n }\n\n /** Reload from storage (e.g. after a suspected external edit). Truncates to the current capacity. */\n reload(): void {\n const loaded = this.readFromStorage()\n const cap = Math.max(1, this.capacity)\n this.items = loaded.length > cap ? loaded.slice(loaded.length - cap) : loaded\n }\n\n /**\n * Append a prompt and persist. See {@link appendHistory} for rules.\n * @returns the new history snapshot.\n */\n append(prompt: string): readonly string[] {\n this.items = appendHistory(this.items, prompt, this.capacity)\n this.writeToStorage()\n return this.items\n }\n\n /** Clear all history (used by tests and a future \"clear\" UI). */\n clear(): void {\n this.items = []\n this.writeToStorage()\n }\n\n private readFromStorage(): string[] {\n if (this.storage === null) return []\n try {\n const raw = this.storage.getItem(this.key)\n if (raw === null) return []\n const parsed: unknown = JSON.parse(raw)\n if (!Array.isArray(parsed)) return []\n return parsed.filter((item): item is string => typeof item === 'string')\n } catch {\n return []\n }\n }\n\n private writeToStorage(): void {\n if (this.storage === null) return\n try {\n this.storage.setItem(this.key, JSON.stringify(this.items))\n } catch {\n // Quota exceeded, private mode, or disabled storage: keep the\n // in-memory copy authoritative for the rest of the page lifetime.\n }\n }\n}\n\n/** Safe accessor for `localStorage` that returns null on any failure. */\nfunction safeLocalStorage(): Storage | null {\n try {\n if (typeof localStorage === 'undefined') return null\n return localStorage\n } catch {\n return null\n }\n}\n","/**\n * DOM helpers for the Lexical composer surface.\n *\n * The DSH composer's text surface is a Lexical-bound contenteditable div,\n * not a textarea: plugins cannot obtain a React ref or a slot-currency\n * handle to it, and writing text goes through `inputActions.setDraft` (the\n * public machine action), not the DOM. What remains DOM-bound is geometry\n * and focus: locating the editable the keystroke targeted, detecting an\n * open trigger menu, and deciding whether the collapsed caret sits on the\n * first/last visual line of a multi-line draft.\n *\n * All markers queried here are internal to `@deepseek-ai/dsh-client-ui-conversation`\n * (`InputBar.tsx` / `ComposerContentEditable.tsx`) or\n * `@deepseek-ai/dsh-client-ui-input-trigger` (`MenuView.tsx`); they are\n * stable but undocumented, and the locators below are the single point to\n * update if upstream changes them.\n *\n * @module @huanlin/dsh-plugin-input-history/client/dom\n */\n\n/** Line-boundary decision for a collapsed caret. */\nexport interface LineBoundary {\n /** True when the caret is collapsed and on the first visual line. */\n readonly atFirstLine: boolean\n /** True when the caret is collapsed and on the last visual line. */\n readonly atLastLine: boolean\n}\n\n/**\n * Pure decision over caret geometry: where a caret resting at `caretTop`\n * sits relative to the box whose visual line tops are `lineTops` (ascending,\n * one entry per visual line, viewport coordinates).\n *\n * @param caretTop - viewport `top` of the collapsed caret's box.\n * @param lineTops - viewport `top` of each visual line, ascending.\n * @param tolerance - px slop absorbing subpixel rounding between the caret\n * rect and its line's rect.\n * @returns the boundary flags; an empty `lineTops` (empty editable) is\n * treated as a single virtual line, so both flags are true.\n */\nexport function boundaryFromLineTops(\n caretTop: number,\n lineTops: readonly number[],\n tolerance: number,\n): LineBoundary {\n if (lineTops.length === 0) return { atFirstLine: true, atLastLine: true }\n return {\n atFirstLine: caretTop <= lineTops[0]! + tolerance,\n atLastLine: caretTop >= lineTops[lineTops.length - 1]! - tolerance,\n }\n}\n\n/**\n * Locate the DSH composer editable the event targeted.\n *\n * Walks from the event target up to the closest `[data-composer-card]`\n * ancestor, queries the `[data-composer-input]` contenteditable inside it,\n * and confirms the target sits inside that editable (keystrokes on the\n * card's buttons and chrome do not navigate history). Returns `null` when\n * the target is not inside the composer editable.\n *\n * @param from - the event target (or any node inside the composer editable).\n * @returns the editable element, or `null` when not found.\n */\nexport function findComposerEditable(from: EventTarget | null): HTMLElement | null {\n if (typeof document === 'undefined') return null\n if (from === null || !(from instanceof Element)) return null\n const card = from.closest('[data-composer-card]')\n if (card === null) return null\n const editable = card.querySelector<HTMLElement>('[data-composer-input]')\n if (editable === null) return null\n return editable.contains(from) ? editable : null\n}\n\n/**\n * Detect an open trigger (slash-command / @-mention) menu inside the\n * composer card that owns `editable`.\n *\n * While the menu is open, ArrowUp/ArrowDown move the highlighted row and\n * must not recall history. The menu renders inside the same\n * `[data-composer-card]` as the editable and carries the stable\n * `data-trigger-menu` marker.\n *\n * @param editable - the composer editable element.\n * @returns the menu element, or `null` when no menu is open.\n */\nexport function findTriggerMenu(editable: HTMLElement): Element | null {\n const card = editable.closest('[data-composer-card]')\n return card === null ? null : card.querySelector('[data-trigger-menu]')\n}\n\n/**\n * Decide the collapsed caret's line boundary inside the composer editable.\n *\n * Compares the caret's viewport box against the editable content's visual\n * line boxes (`Range.getClientRects()` yields one rect per line fragment;\n * fragments of the same visual line share a top within subpixel slop, so\n * tops are deduped with a 2px threshold). A non-collapsed selection and a\n * geometry-less environment (headless/jsdom) both return `null`, which the\n * caller must treat as \"do not navigate\".\n *\n * @param editable - the composer editable element.\n * @param tolerance - px slop between the caret rect and its line rect\n * (defaults to 4px).\n * @returns the boundary flags, or `null` when they cannot be determined.\n */\nexport function caretLineBoundary(editable: HTMLElement, tolerance: number = 4): LineBoundary | null {\n const selection = window.getSelection()\n if (selection === null || selection.rangeCount === 0) return null\n if (!selection.isCollapsed) return null\n const caretTop = caretTopOf(selection)\n if (caretTop === null) return null\n const lineTops = contentLineTops(editable)\n if (lineTops === null) return null\n return boundaryFromLineTops(caretTop, lineTops, tolerance)\n}\n\n/** Viewport `top` of the collapsed caret's box, or `null` when unmeasurable. */\nfunction caretTopOf(selection: Selection): number | null {\n const rects = selection.getRangeAt(0).getClientRects()\n for (let i = 0; i < rects.length; i++) {\n const rect = rects[i]!\n if (rect.height === 0 && rect.width === 0) continue\n return rect.top\n }\n // Some engines report a zero-box collapsed caret; the anchor's element\n // box is the line the caret sits on (the same ruler InputBar's reveal uses).\n const anchor = selection.anchorNode\n const el = anchor instanceof HTMLElement ? anchor : anchor?.parentElement\n return el === undefined || el === null ? null : el.getBoundingClientRect().top\n}\n\n/** Ascending, deduped tops of the editable content's visual lines; `null` without geometry. Empty for an empty editable. */\nfunction contentLineTops(editable: HTMLElement): number[] | null {\n const range = document.createRange()\n range.selectNodeContents(editable)\n const rects = range.getClientRects()\n const tops: number[] = []\n for (let i = 0; i < rects.length; i++) {\n const rect = rects[i]!\n if (rect.height === 0 && rect.width === 0) continue\n const top = rect.top\n // Rects come in document order; fragments of one visual line differ by\n // subpixel amounts, real lines by a full line height.\n if (tops.length === 0 || Math.abs(top - tops[tops.length - 1]!) > 2) tops.push(top)\n }\n return tops\n}\n","/**\n * IME-composition key guard.\n *\n * While a Chinese/Japanese/Korean input method is composing (the user is\n * picking a candidate from the IME window), every pressed key BELONGS to\n * the input method: arrows move the candidate highlight, Enter/Space\n * confirm the composition, Escape cancels it. Page code must not process\n * those keys — a history-navigation handler that calls `preventDefault()`\n * on ArrowUp/ArrowDown during composition would silently break the IME:\n * candidates stop responding, the composition gets torn apart, and only\n * bare letters come out.\n *\n * The composition signal follows the DSH core convention (InputBar's IME\n * guard, issue #535): `isComposing` for modern engines, keyCode 229 as\n * the legacy signal engines emit without isComposing.\n *\n * @module @huanlin/dsh-plugin-input-history/client/ime\n */\n\n/** The pure decision: is this keyboard event part of an IME composition? */\nexport function isImeComposition(event: { isComposing: boolean; keyCode: number }): boolean {\n return event.isComposing || event.keyCode === 229\n}\n","/**\n * HistoryDock — invisible dock entry that collects prompt history and\n * drives terminal-style navigation over the composer.\n *\n * Registers as a `conversation.composer.dock` list entry and renders an\n * `aria-hidden` anchor (zero layout footprint). This component owns both\n * plugin behaviors, because both need per-Session machine faces that only\n * session-scoped slot components receive (rc.1: the dock slot no longer\n * carries an `InputZone` owner — `input` is read through the standard\n * `useInput` selector hook, alongside `useChat`/`inputActions`):\n *\n * - **Collection**: every Chat update re-reads the legacy node slice via\n * `useChat` and appends the latest user/steering text to the shared\n * `HistoryStore` (the store dedupes, so repeated appends are no-ops).\n * - **Navigation**: a capture-phase document `keydown` listener. Capture\n * is required because the composer is a Lexical contenteditable — its\n * keymap moves the caret synchronously in JS on the editable element,\n * so a bubble-phase listener would observe the keystroke only after the\n * caret already moved. The listener intercepts ArrowUp/ArrowDown before\n * Lexical, replaces the draft through `inputActions.setDraft` (the\n * public machine action — no DOM writes), and consumes the event.\n *\n * The dock is session-scoped and DSH suppresses it in hero/blank mode, so\n * neither behavior runs without an active session — and navigation could\n * not run there anyway: the input machine (and `inputActions`) exists only\n * for a current session.\n *\n * @module @huanlin/dsh-plugin-input-history/client/HistoryDock\n */\n\nimport { useEffect, useMemo, useRef } from 'react'\nimport type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'\n// Type-only: SlotMap merge for 'conversation.composer.dock' (rc.1: owner\n// props removed; `useInput`/`inputActions` come from the standard kit)\n// + ui-conversation's SessionStandardProps merge.\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\n// Type-only: ui-chat's SessionStandardProps merge (useChat).\nimport type {} from '@deepseek-ai/dsh-client-ui-chat/client'\nimport { DEFAULT_CAPACITY, HistoryStore, entryAt, nextIndex } from './history.ts'\nimport { caretLineBoundary, findComposerEditable, findTriggerMenu } from './dom.ts'\nimport { isImeComposition } from './ime.ts'\n\n/** Full props: dock runtime share (standard kit — rc.1 removed the InputZone owner) + locale seat. */\ntype HistoryDockProps = PropsRuntime<'conversation.composer.dock'> & PropsLocale<'dsh-plugin-input-history'>\n\n/**\n * Module-scope history store, initialized once on first dock mount.\n * Shared across dock mount/unmount cycles; the underlying data persists\n * in `localStorage`.\n */\nlet historyStore: HistoryStore | null = null\n\n/** Get the shared history store (initializes lazily on first call). */\nexport function getHistoryStore(): HistoryStore {\n if (historyStore === null) {\n historyStore = new HistoryStore(DEFAULT_CAPACITY)\n }\n return historyStore\n}\n\n/**\n * Render the invisible history dock entry: collection + navigation.\n *\n * @param props - dock runtime share (standard hooks) + locale seat.\n * @returns an `aria-hidden` anchor with zero layout footprint.\n */\nexport function HistoryDock({ useInput, useChat, inputActions, sessionId }: HistoryDockProps) {\n // Live machine faces for the keydown handler; `input` is read through the\n // standard selector hook (rc.1 dropped the dock slot's InputZone owner).\n const input = useInput(s => s)\n // History collection: the Chat target's legacy node slice (plain\n // ConversationNode list, newest last). The store dedupes, so re-appending\n // an unchanged latest text is a no-op.\n const nodes = useChat(s => s.legacy.nodes)\n const lastText = useMemo(() => latestUserOrSteeringText(nodes), [nodes])\n useEffect(() => {\n if (lastText !== null) getHistoryStore().append(lastText)\n }, [lastText])\n\n // Navigation cursor + saved draft. Reset on session switch: the saved\n // draft belonged to the previous session's composer and must not be\n // restored into the new one.\n const navCursorRef = useRef<number | null>(null)\n const savedDraftRef = useRef<string | null>(null)\n const prevSessionRef = useRef(sessionId)\n if (prevSessionRef.current !== sessionId) {\n prevSessionRef.current = sessionId\n navCursorRef.current = null\n savedDraftRef.current = null\n }\n\n // Live machine faces for the keydown handler; the refs refresh each\n // render so the handler (attached once) always reads current values.\n const inputRef = useRef(input)\n inputRef.current = input\n const actionsRef = useRef(inputActions)\n actionsRef.current = inputActions\n\n useEffect(() => {\n if (typeof document === 'undefined') return undefined\n const handler = (event: KeyboardEvent): void => {\n if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return\n if (isImeComposition(event)) return\n if (event.defaultPrevented) return\n if (actionsRef.current === undefined || inputRef.current === undefined) return\n // The keystroke must originate inside the composer's editable surface\n // (not the card's buttons or chrome).\n const editable = findComposerEditable(event.target)\n if (editable === null) return\n // Trigger menu open: arrows belong to menu highlight arbitration.\n if (findTriggerMenu(editable) !== null) return\n // Do not interfere with the submit transaction.\n if (inputRef.current.phase !== 'plain') return\n // Multi-line boundary: ArrowUp only on the first visual line,\n // ArrowDown only on the last; no geometry means do not navigate.\n const boundary = caretLineBoundary(editable)\n if (boundary === null) return\n if (event.key === 'ArrowUp' && !boundary.atFirstLine) return\n if (event.key === 'ArrowDown' && !boundary.atLastLine) return\n\n const history = getHistoryStore().list\n const dir = event.key === 'ArrowUp' ? 'up' : 'down'\n const next = nextIndex(navCursorRef.current, history.length, dir)\n\n // Down off the newest end: restore the saved draft (if any).\n if (next === null) {\n const saved = savedDraftRef.current\n navCursorRef.current = null\n if (saved !== null) {\n actionsRef.current.setDraft(saved)\n savedDraftRef.current = null\n }\n consume(event)\n return\n }\n\n // Entering history: save the current draft the first time we\n // navigate away from \"not navigating\".\n if (navCursorRef.current === null && savedDraftRef.current === null) {\n savedDraftRef.current = inputRef.current.draft\n }\n\n const entry = entryAt(history, next)\n if (entry === null) return\n navCursorRef.current = next\n actionsRef.current.setDraft(entry)\n consume(event)\n }\n document.addEventListener('keydown', handler, true)\n return () => {\n document.removeEventListener('keydown', handler, true)\n }\n }, [])\n\n // `display: none` keeps the anchor out of layout and out of the a11y tree.\n return <div aria-hidden style={{ display: 'none' }} data-dsh-plugin-input-history=\"\" />\n}\n\n/**\n * Consume a navigated keystroke: `preventDefault` stops the browser's own\n * gesture, `stopPropagation` (capture phase, document level) keeps the\n * event from ever reaching Lexical's editable keydown listener — otherwise\n * the keymap would move the caret after the draft was already replaced.\n */\nfunction consume(event: KeyboardEvent): void {\n event.preventDefault()\n event.stopPropagation()\n}\n\n/**\n * Extract the text of the latest `user` or `steering` node from the Chat\n * target's legacy node list.\n *\n * Returns the concatenated text of all `type: 'text'` content blocks.\n * Returns `null` when no user/steering node is present (e.g. a fresh\n * session with only a system/context message).\n *\n * @param nodes - the Chat snapshot's legacy `nodes` array (newest last).\n */\nfunction latestUserOrSteeringText(\n nodes: ReadonlyArray<{\n kind: string\n content?: ReadonlyArray<{ type: string; text?: string }>\n }>,\n): string | null {\n for (let i = nodes.length - 1; i >= 0; i--) {\n const node = nodes[i]!\n if (node.kind !== 'user' && node.kind !== 'steering') continue\n const content = node.content\n if (content === undefined) continue\n let text = ''\n for (const block of content) {\n if (block.type === 'text' && typeof block.text === 'string') {\n text += block.text\n }\n }\n return text\n }\n return null\n}\n","/**\n * Locale dictionaries for dsh-plugin-input-history.\n *\n * The plugin renders nothing visible — the only user-facing copy is the\n * `aria-label` on the invisible dock anchor (for screen readers) and a\n * future settings row label.\n *\n * @module @huanlin/dsh-plugin-input-history/client/locales\n */\n\n/** All copy keys for the dsh-plugin-input-history namespace. */\nexport type InputHistoryKey =\n | 'ariaLabel'\n | 'restoredDraft'\n | 'noHistory'\n\n/** Locale namespace id (matches the cordis.patch.yml plugin id). */\nexport const NS = 'dsh-plugin-input-history'\n\n/** English dictionary. */\nexport const en: Record<InputHistoryKey, string> = {\n ariaLabel: 'Prompt history navigation (ArrowUp/ArrowDown)',\n restoredDraft: 'Restored in-progress draft',\n noHistory: 'No prompt history yet',\n}\n\n/** Chinese dictionary. */\nexport const zh: Record<InputHistoryKey, string> = {\n ariaLabel: '提示词历史导航(上/下方向键)',\n restoredDraft: '已恢复正在编辑的草稿',\n noHistory: '暂无提示词历史',\n}\n","/**\n * Override dictionaries for the 19 languages better-locale ships. Each\n * dict covers the full `dsh-plugin-input-history` key set (ariaLabel /\n * restoredDraft / noHistory), no placeholders — these are plain strings.\n *\n * Registered with better-locale only (see [index.ts](./index.ts)): the\n * override borrows DSH's English slot, so these render when the user\n * selected an override language AND DSH's active locale is `'en'`. zh-HK /\n * zh-TW / zh-MO have no regional variants for this copy, so the three\n * Traditional Chinese dicts are identical.\n */\n\nimport type { InputHistoryKey } from './locales.ts'\n\nconst ja: Record<InputHistoryKey, string> = {\n ariaLabel: 'プロンプト履歴のナビゲーション(↑/↓キー)',\n restoredDraft: '編集中の下書きを復元しました',\n noHistory: 'プロンプト履歴はまだありません',\n}\n\nconst de: Record<InputHistoryKey, string> = {\n ariaLabel: 'Befehlsverlauf-Navigation (Pfeil hoch/runter)',\n restoredDraft: 'In Bearbeitung befindlicher Entwurf wiederhergestellt',\n noHistory: 'Noch kein Befehlsverlauf vorhanden',\n}\n\nconst fr: Record<InputHistoryKey, string> = {\n ariaLabel: 'Navigation dans l\\'historique des invites (flèche haut/bas)',\n restoredDraft: 'Brouillon en cours d\\'édition restauré',\n noHistory: 'Pas encore d\\'historique d\\'invites',\n}\n\nconst pt: Record<InputHistoryKey, string> = {\n ariaLabel: 'Navegação no histórico de prompts (seta para cima/baixo)',\n restoredDraft: 'Rascunho em edição restaurado',\n noHistory: 'Ainda não há histórico de prompts',\n}\n\nconst ko: Record<InputHistoryKey, string> = {\n ariaLabel: '프롬프트 기록 탐색 (위/아래 화살표)',\n restoredDraft: '편집 중이던 초안을 복원했습니다',\n noHistory: '아직 프롬프트 기록이 없습니다',\n}\n\nconst ar: Record<InputHistoryKey, string> = {\n ariaLabel: 'التنقل في سجل الأوامر (السهم لأعلى/لأسفل)',\n restoredDraft: 'تمت استعادة المسودة قيد التحرير',\n noHistory: 'لا يوجد سجل أوامر بعد',\n}\n\nconst hi: Record<InputHistoryKey, string> = {\n ariaLabel: 'प्रॉम्प्ट इतिहास नेविगेशन (ऊपर/नीचे तीर)',\n restoredDraft: 'संपादन में मौजूद ड्राफ्ट पुनर्स्थापित किया गया',\n noHistory: 'अभी तक कोई प्रॉम्प्ट इतिहास नहीं',\n}\n\nconst id: Record<InputHistoryKey, string> = {\n ariaLabel: 'Navigasi riwayat prompt (panah atas/bawah)',\n restoredDraft: 'Draf yang sedang diedit dipulihkan',\n noHistory: 'Belum ada riwayat prompt',\n}\n\nconst tr: Record<InputHistoryKey, string> = {\n ariaLabel: 'Komut geçmişinde gezinme (yukarı/aşağı ok)',\n restoredDraft: 'Düzenlenmekte olan taslak geri yüklendi',\n noHistory: 'Henüz komut geçmişi yok',\n}\n\nconst vi: Record<InputHistoryKey, string> = {\n ariaLabel: 'Điều hướng lịch sử lệnh (mũi tên lên/xuống)',\n restoredDraft: 'Đã khôi phục bản nháp đang soạn',\n noHistory: 'Chưa có lịch sử lệnh',\n}\n\nconst th: Record<InputHistoryKey, string> = {\n ariaLabel: 'นำทางประวัติคำสั่ง (ลูกศรขึ้น/ลง)',\n restoredDraft: 'กู้คืนฉบับร่างที่กำลังแก้ไขแล้ว',\n noHistory: 'ยังไม่มีประวัติคำสั่ง',\n}\n\nconst ru: Record<InputHistoryKey, string> = {\n ariaLabel: 'Навигация по истории запросов (стрелки вверх/вниз)',\n restoredDraft: 'Текущий черновик восстановлен',\n noHistory: 'Истории запросов пока нет',\n}\n\nconst it: Record<InputHistoryKey, string> = {\n ariaLabel: 'Navigazione cronologia prompt (freccia su/giù)',\n restoredDraft: 'Bozza in corso ripristinata',\n noHistory: 'Nessuna cronologia prompt finora',\n}\n\nconst nl: Record<InputHistoryKey, string> = {\n ariaLabel: 'Navigatie door promptgeschiedenis (pijl omhoog/omlaag)',\n restoredDraft: 'Lopende concept hersteld',\n noHistory: 'Nog geen promptgeschiedenis',\n}\n\nconst sv: Record<InputHistoryKey, string> = {\n ariaLabel: 'Navigera i prompthistorik (pil upp/ner)',\n restoredDraft: 'Utkast under arbete återställt',\n noHistory: 'Ingen prompthistorik ännu',\n}\n\nconst pl: Record<InputHistoryKey, string> = {\n ariaLabel: 'Nawigacja po historii promptów (strzałka w górę/w dół)',\n restoredDraft: 'Przywrócono edytowany szkic',\n noHistory: 'Brak jeszcze historii promptów',\n}\n\nconst zhHK: Record<InputHistoryKey, string> = {\n ariaLabel: '提示詞歷史導覽(上/下方向鍵)',\n restoredDraft: '已還原正在編輯的草稿',\n noHistory: '暫無提示詞歷史',\n}\n\nconst zhTW: Record<InputHistoryKey, string> = {\n ariaLabel: '提示詞歷史導覽(上/下方向鍵)',\n restoredDraft: '已還原正在編輯的草稿',\n noHistory: '暫無提示詞歷史',\n}\n\nconst zhMO: Record<InputHistoryKey, string> = {\n ariaLabel: '提示詞歷史導覽(上/下方向鍵)',\n restoredDraft: '已還原正在編輯的草稿',\n noHistory: '暫無提示詞歷史',\n}\n\n/**\n * All override dictionaries, keyed by language id, covering the full key\n * set. Registered with better-locale under the plugin namespace.\n */\nexport const dicts: Record<string, Record<InputHistoryKey, string>> = {\n ja, de, fr, pt, ko, ar, hi, id, tr, vi, th, ru, it, nl, sv, pl,\n 'zh-HK': zhHK, 'zh-TW': zhTW, 'zh-MO': zhMO,\n}","/**\n * dsh-plugin-input-history — browser half.\n *\n * One registration: the `conversation.composer.dock` list slot (id\n * `dsh-plugin-input-history`, order 100) mounts the invisible dock entry\n * that owns both plugin behaviors — prompt-history collection from the\n * Chat target's user/steering nodes, and the capture-phase document\n * keydown listener that navigates the composer draft through\n * `inputActions.setDraft`. See [HistoryDock.tsx](./HistoryDock.tsx) for\n * the data flow; the dock is session-scoped, so in hero/blank mode the\n * plugin is dormant (no input machine exists there to drive).\n *\n * History is collected from `user` and `steering` chat nodes of any active\n * session, persisted to `localStorage` (FIFO, 500 entries), and shared\n * across all sessions in the same browser profile.\n *\n * @module @huanlin/dsh-plugin-input-history/client\n */\n\nimport type { Context } from '@deepseek-ai/cordis'\n// Type-only: pulls the locale plugin's Context merge (ctx.locale).\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\n// Type-only: pulls the renderer's Context merge (ctx.slots).\nimport type {} from '@deepseek-ai/dsh-client-ui-renderer/client'\n// Type-only: SlotMap merge (conversation.composer.dock) + ui-conversation's\n// SessionStandardProps merge (useInput, inputActions) used by the dock.\nimport type {} from '@deepseek-ai/dsh-client-ui-conversation/client'\n// Type-only: ui-chat's SessionStandardProps merge (useChat) used by the dock.\nimport type {} from '@deepseek-ai/dsh-client-ui-chat/client'\nimport { HistoryDock } from './HistoryDock.tsx'\nimport { en, NS, zh, type InputHistoryKey } from './locales.ts'\nimport { dicts } from './dictionaries.ts'\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** The dock's aria-label + future settings row copy. */\n 'dsh-plugin-input-history': InputHistoryKey\n }\n}\n\n/** Required services: slots + locale. */\nexport const inject = ['slots', 'locale']\n\n/** Structural view of better-locale's override store (optional; no runtime dep). */\ninterface BetterLocaleOverrideStore {\n register(ns: string, dicts: Record<string, Record<string, string>>): () => void\n}\n\n/**\n * Client plugin body: register the dock + locale dictionaries.\n *\n * @param ctx - client root context.\n */\nexport function apply(ctx: Context): void {\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-plugin-input-history: dictionaries')\n\n // better-locale override: register the 19-language dicts so a selected\n // override language (with DSH on 'en') replaces the plugin's copy. The\n // service is optional — no better-locale, no dicts.\n // Activation-order-safe: re-check ctx.get('betterLocale') on every locale\n // revision bump (better-locale bumps on activation + override switch).\n ctx.effect(() => {\n let dispose: (() => void) | undefined\n const sync = (): void => {\n dispose?.()\n dispose = undefined\n const store = ctx.get('betterLocale') as BetterLocaleOverrideStore | undefined\n if (store !== undefined) {\n dispose = store.register(NS, dicts)\n }\n }\n sync()\n const unsubscribe = ctx.locale.subscribe(sync)\n return () => {\n unsubscribe()\n dispose?.()\n }\n }, 'dsh-plugin-input-history: better-locale override dicts')\n\n // The dock collects history from the Chat target's nodes and attaches the\n // capture-phase keydown listener. It is session-scoped; in hero/blank mode\n // it is unmounted and the plugin is dormant (no input machine exists there).\n ctx.slots.inject('conversation.composer.dock', () =>\n ctx.slots.register(\n {\n name: 'conversation.composer.dock',\n id: 'dsh-plugin-input-history',\n order: 100,\n locale: NS,\n },\n HistoryDock,\n ),\n )\n}\n"],"mappings":"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"}
|
|
@@ -1,31 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* HistoryDock — invisible dock entry that collects prompt history
|
|
2
|
+
* HistoryDock — invisible dock entry that collects prompt history and
|
|
3
|
+
* drives terminal-style navigation over the composer.
|
|
3
4
|
*
|
|
4
5
|
* Registers as a `conversation.composer.dock` list entry and renders an
|
|
5
|
-
* `aria-hidden` anchor (zero layout footprint).
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* `aria-hidden` anchor (zero layout footprint). This component owns both
|
|
7
|
+
* plugin behaviors, because both need per-Session machine faces that only
|
|
8
|
+
* session-scoped slot components receive (rc.1: the dock slot no longer
|
|
9
|
+
* carries an `InputZone` owner — `input` is read through the standard
|
|
10
|
+
* `useInput` selector hook, alongside `useChat`/`inputActions`):
|
|
8
11
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* - **Collection**: every Chat update re-reads the legacy node slice via
|
|
13
|
+
* `useChat` and appends the latest user/steering text to the shared
|
|
14
|
+
* `HistoryStore` (the store dedupes, so repeated appends are no-ops).
|
|
15
|
+
* - **Navigation**: a capture-phase document `keydown` listener. Capture
|
|
16
|
+
* is required because the composer is a Lexical contenteditable — its
|
|
17
|
+
* keymap moves the caret synchronously in JS on the editable element,
|
|
18
|
+
* so a bubble-phase listener would observe the keystroke only after the
|
|
19
|
+
* caret already moved. The listener intercepts ArrowUp/ArrowDown before
|
|
20
|
+
* Lexical, replaces the draft through `inputActions.setDraft` (the
|
|
21
|
+
* public machine action — no DOM writes), and consumes the event.
|
|
22
|
+
*
|
|
23
|
+
* The dock is session-scoped and DSH suppresses it in hero/blank mode, so
|
|
24
|
+
* neither behavior runs without an active session — and navigation could
|
|
25
|
+
* not run there anyway: the input machine (and `inputActions`) exists only
|
|
26
|
+
* for a current session.
|
|
15
27
|
*
|
|
16
28
|
* @module @huanlin/dsh-plugin-input-history/client/HistoryDock
|
|
17
29
|
*/
|
|
18
|
-
import type {
|
|
30
|
+
import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
19
31
|
import { HistoryStore } from './history.ts';
|
|
20
|
-
/** Full props: dock runtime share + locale seat. */
|
|
32
|
+
/** Full props: dock runtime share (standard kit — rc.1 removed the InputZone owner) + locale seat. */
|
|
21
33
|
type HistoryDockProps = PropsRuntime<'conversation.composer.dock'> & PropsLocale<'dsh-plugin-input-history'>;
|
|
22
34
|
/** Get the shared history store (initializes lazily on first call). */
|
|
23
35
|
export declare function getHistoryStore(): HistoryStore;
|
|
24
36
|
/**
|
|
25
|
-
* Render the invisible history
|
|
37
|
+
* Render the invisible history dock entry: collection + navigation.
|
|
26
38
|
*
|
|
27
|
-
* @param props - dock runtime share (
|
|
39
|
+
* @param props - dock runtime share (standard hooks) + locale seat.
|
|
28
40
|
* @returns an `aria-hidden` anchor with zero layout footprint.
|
|
29
41
|
*/
|
|
30
|
-
export declare function HistoryDock({
|
|
42
|
+
export declare function HistoryDock({ useInput, useChat, inputActions, sessionId }: HistoryDockProps): import("react").JSX.Element;
|
|
31
43
|
export {};
|
|
@@ -1,26 +1,41 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
|
-
* HistoryDock — invisible dock entry that collects prompt history
|
|
3
|
+
* HistoryDock — invisible dock entry that collects prompt history and
|
|
4
|
+
* drives terminal-style navigation over the composer.
|
|
4
5
|
*
|
|
5
6
|
* Registers as a `conversation.composer.dock` list entry and renders an
|
|
6
|
-
* `aria-hidden` anchor (zero layout footprint).
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* `aria-hidden` anchor (zero layout footprint). This component owns both
|
|
8
|
+
* plugin behaviors, because both need per-Session machine faces that only
|
|
9
|
+
* session-scoped slot components receive (rc.1: the dock slot no longer
|
|
10
|
+
* carries an `InputZone` owner — `input` is read through the standard
|
|
11
|
+
* `useInput` selector hook, alongside `useChat`/`inputActions`):
|
|
9
12
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* - **Collection**: every Chat update re-reads the legacy node slice via
|
|
14
|
+
* `useChat` and appends the latest user/steering text to the shared
|
|
15
|
+
* `HistoryStore` (the store dedupes, so repeated appends are no-ops).
|
|
16
|
+
* - **Navigation**: a capture-phase document `keydown` listener. Capture
|
|
17
|
+
* is required because the composer is a Lexical contenteditable — its
|
|
18
|
+
* keymap moves the caret synchronously in JS on the editable element,
|
|
19
|
+
* so a bubble-phase listener would observe the keystroke only after the
|
|
20
|
+
* caret already moved. The listener intercepts ArrowUp/ArrowDown before
|
|
21
|
+
* Lexical, replaces the draft through `inputActions.setDraft` (the
|
|
22
|
+
* public machine action — no DOM writes), and consumes the event.
|
|
23
|
+
*
|
|
24
|
+
* The dock is session-scoped and DSH suppresses it in hero/blank mode, so
|
|
25
|
+
* neither behavior runs without an active session — and navigation could
|
|
26
|
+
* not run there anyway: the input machine (and `inputActions`) exists only
|
|
27
|
+
* for a current session.
|
|
16
28
|
*
|
|
17
29
|
* @module @huanlin/dsh-plugin-input-history/client/HistoryDock
|
|
18
30
|
*/
|
|
19
|
-
import { useRef } from 'react';
|
|
20
|
-
import { HistoryStore,
|
|
31
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
32
|
+
import { DEFAULT_CAPACITY, HistoryStore, entryAt, nextIndex } from "./history.js";
|
|
33
|
+
import { caretLineBoundary, findComposerEditable, findTriggerMenu } from "./dom.js";
|
|
34
|
+
import { isImeComposition } from "./ime.js";
|
|
21
35
|
/**
|
|
22
36
|
* Module-scope history store, initialized once on first dock mount.
|
|
23
|
-
* Shared
|
|
37
|
+
* Shared across dock mount/unmount cycles; the underlying data persists
|
|
38
|
+
* in `localStorage`.
|
|
24
39
|
*/
|
|
25
40
|
let historyStore = null;
|
|
26
41
|
/** Get the shared history store (initializes lazily on first call). */
|
|
@@ -31,37 +46,126 @@ export function getHistoryStore() {
|
|
|
31
46
|
return historyStore;
|
|
32
47
|
}
|
|
33
48
|
/**
|
|
34
|
-
* Render the invisible history
|
|
49
|
+
* Render the invisible history dock entry: collection + navigation.
|
|
35
50
|
*
|
|
36
|
-
* @param props - dock runtime share (
|
|
51
|
+
* @param props - dock runtime share (standard hooks) + locale seat.
|
|
37
52
|
* @returns an `aria-hidden` anchor with zero layout footprint.
|
|
38
53
|
*/
|
|
39
|
-
export function HistoryDock({
|
|
40
|
-
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
54
|
+
export function HistoryDock({ useInput, useChat, inputActions, sessionId }) {
|
|
55
|
+
// Live machine faces for the keydown handler; `input` is read through the
|
|
56
|
+
// standard selector hook (rc.1 dropped the dock slot's InputZone owner).
|
|
57
|
+
const input = useInput(s => s);
|
|
58
|
+
// History collection: the Chat target's legacy node slice (plain
|
|
59
|
+
// ConversationNode list, newest last). The store dedupes, so re-appending
|
|
60
|
+
// an unchanged latest text is a no-op.
|
|
61
|
+
const nodes = useChat(s => s.legacy.nodes);
|
|
62
|
+
const lastText = useMemo(() => latestUserOrSteeringText(nodes), [nodes]);
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (lastText !== null)
|
|
65
|
+
getHistoryStore().append(lastText);
|
|
66
|
+
}, [lastText]);
|
|
67
|
+
// Navigation cursor + saved draft. Reset on session switch: the saved
|
|
68
|
+
// draft belonged to the previous session's composer and must not be
|
|
69
|
+
// restored into the new one.
|
|
70
|
+
const navCursorRef = useRef(null);
|
|
71
|
+
const savedDraftRef = useRef(null);
|
|
72
|
+
const prevSessionRef = useRef(sessionId);
|
|
73
|
+
if (prevSessionRef.current !== sessionId) {
|
|
74
|
+
prevSessionRef.current = sessionId;
|
|
75
|
+
navCursorRef.current = null;
|
|
76
|
+
savedDraftRef.current = null;
|
|
50
77
|
}
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
|
|
78
|
+
// Live machine faces for the keydown handler; the refs refresh each
|
|
79
|
+
// render so the handler (attached once) always reads current values.
|
|
80
|
+
const inputRef = useRef(input);
|
|
81
|
+
inputRef.current = input;
|
|
82
|
+
const actionsRef = useRef(inputActions);
|
|
83
|
+
actionsRef.current = inputActions;
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
if (typeof document === 'undefined')
|
|
86
|
+
return undefined;
|
|
87
|
+
const handler = (event) => {
|
|
88
|
+
if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown')
|
|
89
|
+
return;
|
|
90
|
+
if (isImeComposition(event))
|
|
91
|
+
return;
|
|
92
|
+
if (event.defaultPrevented)
|
|
93
|
+
return;
|
|
94
|
+
if (actionsRef.current === undefined || inputRef.current === undefined)
|
|
95
|
+
return;
|
|
96
|
+
// The keystroke must originate inside the composer's editable surface
|
|
97
|
+
// (not the card's buttons or chrome).
|
|
98
|
+
const editable = findComposerEditable(event.target);
|
|
99
|
+
if (editable === null)
|
|
100
|
+
return;
|
|
101
|
+
// Trigger menu open: arrows belong to menu highlight arbitration.
|
|
102
|
+
if (findTriggerMenu(editable) !== null)
|
|
103
|
+
return;
|
|
104
|
+
// Do not interfere with the submit transaction.
|
|
105
|
+
if (inputRef.current.phase !== 'plain')
|
|
106
|
+
return;
|
|
107
|
+
// Multi-line boundary: ArrowUp only on the first visual line,
|
|
108
|
+
// ArrowDown only on the last; no geometry means do not navigate.
|
|
109
|
+
const boundary = caretLineBoundary(editable);
|
|
110
|
+
if (boundary === null)
|
|
111
|
+
return;
|
|
112
|
+
if (event.key === 'ArrowUp' && !boundary.atFirstLine)
|
|
113
|
+
return;
|
|
114
|
+
if (event.key === 'ArrowDown' && !boundary.atLastLine)
|
|
115
|
+
return;
|
|
116
|
+
const history = getHistoryStore().list;
|
|
117
|
+
const dir = event.key === 'ArrowUp' ? 'up' : 'down';
|
|
118
|
+
const next = nextIndex(navCursorRef.current, history.length, dir);
|
|
119
|
+
// Down off the newest end: restore the saved draft (if any).
|
|
120
|
+
if (next === null) {
|
|
121
|
+
const saved = savedDraftRef.current;
|
|
122
|
+
navCursorRef.current = null;
|
|
123
|
+
if (saved !== null) {
|
|
124
|
+
actionsRef.current.setDraft(saved);
|
|
125
|
+
savedDraftRef.current = null;
|
|
126
|
+
}
|
|
127
|
+
consume(event);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
// Entering history: save the current draft the first time we
|
|
131
|
+
// navigate away from "not navigating".
|
|
132
|
+
if (navCursorRef.current === null && savedDraftRef.current === null) {
|
|
133
|
+
savedDraftRef.current = inputRef.current.draft;
|
|
134
|
+
}
|
|
135
|
+
const entry = entryAt(history, next);
|
|
136
|
+
if (entry === null)
|
|
137
|
+
return;
|
|
138
|
+
navCursorRef.current = next;
|
|
139
|
+
actionsRef.current.setDraft(entry);
|
|
140
|
+
consume(event);
|
|
141
|
+
};
|
|
142
|
+
document.addEventListener('keydown', handler, true);
|
|
143
|
+
return () => {
|
|
144
|
+
document.removeEventListener('keydown', handler, true);
|
|
145
|
+
};
|
|
146
|
+
}, []);
|
|
147
|
+
// `display: none` keeps the anchor out of layout and out of the a11y tree.
|
|
54
148
|
return _jsx("div", { "aria-hidden": true, style: { display: 'none' }, "data-dsh-plugin-input-history": "" });
|
|
55
149
|
}
|
|
56
150
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
151
|
+
* Consume a navigated keystroke: `preventDefault` stops the browser's own
|
|
152
|
+
* gesture, `stopPropagation` (capture phase, document level) keeps the
|
|
153
|
+
* event from ever reaching Lexical's editable keydown listener — otherwise
|
|
154
|
+
* the keymap would move the caret after the draft was already replaced.
|
|
155
|
+
*/
|
|
156
|
+
function consume(event) {
|
|
157
|
+
event.preventDefault();
|
|
158
|
+
event.stopPropagation();
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Extract the text of the latest `user` or `steering` node from the Chat
|
|
162
|
+
* target's legacy node list.
|
|
59
163
|
*
|
|
60
164
|
* Returns the concatenated text of all `type: 'text'` content blocks.
|
|
61
165
|
* Returns `null` when no user/steering node is present (e.g. a fresh
|
|
62
166
|
* session with only a system/context message).
|
|
63
167
|
*
|
|
64
|
-
* @param nodes - the
|
|
168
|
+
* @param nodes - the Chat snapshot's legacy `nodes` array (newest last).
|
|
65
169
|
*/
|
|
66
170
|
function latestUserOrSteeringText(nodes) {
|
|
67
171
|
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
@@ -1,57 +1,81 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* DOM helpers for the composer
|
|
2
|
+
* DOM helpers for the Lexical composer surface.
|
|
3
3
|
*
|
|
4
|
-
* The DSH
|
|
5
|
-
* plugins cannot obtain a React ref or a slot-currency
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* The DSH composer's text surface is a Lexical-bound contenteditable div,
|
|
5
|
+
* not a textarea: plugins cannot obtain a React ref or a slot-currency
|
|
6
|
+
* handle to it, and writing text goes through `inputActions.setDraft` (the
|
|
7
|
+
* public machine action), not the DOM. What remains DOM-bound is geometry
|
|
8
|
+
* and focus: locating the editable the keystroke targeted, detecting an
|
|
9
|
+
* open trigger menu, and deciding whether the collapsed caret sits on the
|
|
10
|
+
* first/last visual line of a multi-line draft.
|
|
9
11
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* All markers queried here are internal to `@deepseek-ai/dsh-client-ui-conversation`
|
|
13
|
+
* (`InputBar.tsx` / `ComposerContentEditable.tsx`) or
|
|
14
|
+
* `@deepseek-ai/dsh-client-ui-input-trigger` (`MenuView.tsx`); they are
|
|
15
|
+
* stable but undocumented, and the locators below are the single point to
|
|
16
|
+
* update if upstream changes them.
|
|
15
17
|
*
|
|
16
18
|
* @module @huanlin/dsh-plugin-input-history/client/dom
|
|
17
19
|
*/
|
|
18
|
-
/**
|
|
19
|
-
export interface
|
|
20
|
-
/**
|
|
21
|
-
readonly currentLine: number;
|
|
22
|
-
/** Total number of lines in the value (>= 1). */
|
|
23
|
-
readonly totalLines: number;
|
|
24
|
-
/** True when the caret is collapsed and on the first line. */
|
|
20
|
+
/** Line-boundary decision for a collapsed caret. */
|
|
21
|
+
export interface LineBoundary {
|
|
22
|
+
/** True when the caret is collapsed and on the first visual line. */
|
|
25
23
|
readonly atFirstLine: boolean;
|
|
26
|
-
/** True when the caret is collapsed and on the last line. */
|
|
24
|
+
/** True when the caret is collapsed and on the last visual line. */
|
|
27
25
|
readonly atLastLine: boolean;
|
|
28
26
|
}
|
|
29
27
|
/**
|
|
30
|
-
*
|
|
28
|
+
* Pure decision over caret geometry: where a caret resting at `caretTop`
|
|
29
|
+
* sits relative to the box whose visual line tops are `lineTops` (ascending,
|
|
30
|
+
* one entry per visual line, viewport coordinates).
|
|
31
31
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
32
|
+
* @param caretTop - viewport `top` of the collapsed caret's box.
|
|
33
|
+
* @param lineTops - viewport `top` of each visual line, ascending.
|
|
34
|
+
* @param tolerance - px slop absorbing subpixel rounding between the caret
|
|
35
|
+
* rect and its line's rect.
|
|
36
|
+
* @returns the boundary flags; an empty `lineTops` (empty editable) is
|
|
37
|
+
* treated as a single virtual line, so both flags are true.
|
|
38
|
+
*/
|
|
39
|
+
export declare function boundaryFromLineTops(caretTop: number, lineTops: readonly number[], tolerance: number): LineBoundary;
|
|
40
|
+
/**
|
|
41
|
+
* Locate the DSH composer editable the event targeted.
|
|
42
|
+
*
|
|
43
|
+
* Walks from the event target up to the closest `[data-composer-card]`
|
|
44
|
+
* ancestor, queries the `[data-composer-input]` contenteditable inside it,
|
|
45
|
+
* and confirms the target sits inside that editable (keystrokes on the
|
|
46
|
+
* card's buttons and chrome do not navigate history). Returns `null` when
|
|
47
|
+
* the target is not inside the composer editable.
|
|
36
48
|
*
|
|
37
|
-
* @param
|
|
38
|
-
* @
|
|
39
|
-
* @param selectionEnd - the textarea's `selectionEnd` (defaults to `selectionStart`).
|
|
40
|
-
* @returns the caret's line information.
|
|
49
|
+
* @param from - the event target (or any node inside the composer editable).
|
|
50
|
+
* @returns the editable element, or `null` when not found.
|
|
41
51
|
*/
|
|
42
|
-
export declare function
|
|
52
|
+
export declare function findComposerEditable(from: EventTarget | null): HTMLElement | null;
|
|
43
53
|
/**
|
|
44
|
-
*
|
|
54
|
+
* Detect an open trigger (slash-command / @-mention) menu inside the
|
|
55
|
+
* composer card that owns `editable`.
|
|
45
56
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* `
|
|
49
|
-
*
|
|
57
|
+
* While the menu is open, ArrowUp/ArrowDown move the highlighted row and
|
|
58
|
+
* must not recall history. The menu renders inside the same
|
|
59
|
+
* `[data-composer-card]` as the editable and carries the stable
|
|
60
|
+
* `data-trigger-menu` marker.
|
|
61
|
+
*
|
|
62
|
+
* @param editable - the composer editable element.
|
|
63
|
+
* @returns the menu element, or `null` when no menu is open.
|
|
64
|
+
*/
|
|
65
|
+
export declare function findTriggerMenu(editable: HTMLElement): Element | null;
|
|
66
|
+
/**
|
|
67
|
+
* Decide the collapsed caret's line boundary inside the composer editable.
|
|
50
68
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
69
|
+
* Compares the caret's viewport box against the editable content's visual
|
|
70
|
+
* line boxes (`Range.getClientRects()` yields one rect per line fragment;
|
|
71
|
+
* fragments of the same visual line share a top within subpixel slop, so
|
|
72
|
+
* tops are deduped with a 2px threshold). A non-collapsed selection and a
|
|
73
|
+
* geometry-less environment (headless/jsdom) both return `null`, which the
|
|
74
|
+
* caller must treat as "do not navigate".
|
|
53
75
|
*
|
|
54
|
-
* @param
|
|
55
|
-
* @
|
|
76
|
+
* @param editable - the composer editable element.
|
|
77
|
+
* @param tolerance - px slop between the caret rect and its line rect
|
|
78
|
+
* (defaults to 4px).
|
|
79
|
+
* @returns the boundary flags, or `null` when they cannot be determined.
|
|
56
80
|
*/
|
|
57
|
-
export declare function
|
|
81
|
+
export declare function caretLineBoundary(editable: HTMLElement, tolerance?: number): LineBoundary | null;
|