| 59 | } |
| 60 | |
| 61 | export function useInputHistory(deps: InputHistoryDeps) { |
| 62 | const { text, textareaRef, autosize, sessionId } = deps; |
| 63 | |
| 64 | const historyMap = ref<Record<string, string[]>>(loadMap(sessionId())); |
| 65 | const currentList = computed(() => historyMap.value[sessionId() ?? ''] ?? []); |
| 66 | // -1 = browsing nothing (live draft). Otherwise an index into currentList. |
| 67 | let historyIndex = -1; |
| 68 | let draftBeforeHistory = ''; |
| 69 | |
| 70 | function push(entry: string): void { |
| 71 | const sid = sessionId(); |
| 72 | historyIndex = -1; |
| 73 | // Draft sessions have no id yet — drop the entry (see file header). |
| 74 | if (!sid) return; |
| 75 | const trimmed = entry.trim(); |
| 76 | if (!trimmed) return; |
| 77 | const list = historyMap.value[sid] ?? []; |
| 78 | // Skip consecutive duplicates so repeated sends don't pad the history. |
| 79 | if (list.at(-1) === trimmed) return; |
| 80 | const next = [...list, trimmed]; |
| 81 | const capped = next.length > MAX_HISTORY ? next.slice(-MAX_HISTORY) : next; |
| 82 | historyMap.value = { ...historyMap.value, [sid]: capped }; |
| 83 | safeSetJson(STORAGE_KEYS.inputHistory, historyMap.value); |
| 84 | } |
| 85 | |
| 86 | function caretAtTextStart(): boolean { |
| 87 | const el = textareaRef.value; |
| 88 | if (!el) return false; |
| 89 | // Only recall when the caret sits at the very start of the text. Otherwise |
| 90 | // ArrowUp while navigating a multi-line draft would hijack the caret and |
| 91 | // jump to a previous message instead of moving within the draft. |
| 92 | return (el.selectionStart ?? 0) === 0; |
| 93 | } |
| 94 | |
| 95 | function applyHistoryText(value: string): void { |
| 96 | text.value = value; |
| 97 | void nextTick(() => { |
| 98 | const el = textareaRef.value; |
| 99 | if (!el) return; |
| 100 | autosize(); |
| 101 | const pos = value.length; |
| 102 | el.setSelectionRange(pos, pos); |
| 103 | }); |
| 104 | } |
| 105 | |
| 106 | function recallOlder(): void { |
| 107 | const list = currentList.value; |
| 108 | if (list.length === 0) return; |
| 109 | if (historyIndex === -1) { |
| 110 | draftBeforeHistory = text.value; |
| 111 | historyIndex = list.length - 1; |
| 112 | } else if (historyIndex > 0) { |
| 113 | historyIndex -= 1; |
| 114 | } else { |
| 115 | return; // already at the oldest entry |
| 116 | } |
| 117 | applyHistoryText(list[historyIndex]!); |
| 118 | } |