qwen-code/packages/web-shell/client/hooks/useInputHistory.ts
ytahdn 6a432ad2eb
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
E2E Tests / cron-interactive E2E (nightly) (push) Waiting to run
E2E Tests / web-shell Browser Regression (push) Waiting to run
SDK Java / ubuntu-latest / Java 11 (push) Waiting to run
SDK Java / ubuntu-latest / Java 17 (push) Waiting to run
SDK Java / macos-latest / Java 21 (push) Waiting to run
SDK Java / ubuntu-latest / Java 21 (push) Waiting to run
SDK Java / windows-latest / Java 21 (push) Waiting to run
SDK Java / Real daemon E2E / Java 11 (push) Waiting to run
fix(web-shell): isolate history and session drafts (#7810)
* fix(web-shell): isolate history and session drafts

* fix(web-shell): reset history-browse flag on early commit return (#7810)

* fix(web-shell): address review feedback on paste and draft handling (#7810)

* fix(web-shell): address review feedback on paste pruning, draft flush, and mobile draft notify (#7810)

* fix(web-shell): update smoke test for large paste placeholder behavior (#7810)

---------

Co-authored-by: 钉萁 <dingqi.jww@alibaba-inc.com>
Co-authored-by: Qwen Code Bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen Code <qwen-code@users.noreply.github.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
2026-07-28 01:45:52 +00:00

238 lines
6.3 KiB
TypeScript

import { useCallback, useEffect, useRef, useState } from 'react';
const DEFAULT_STORAGE_KEY = 'qwen-web-shell-history';
const MAX_HISTORY = 100;
export function getPromptHistoryStorageKey(workspaceCwd?: string): string {
return workspaceCwd
? `${DEFAULT_STORAGE_KEY}:${encodeURIComponent(workspaceCwd)}`
: DEFAULT_STORAGE_KEY;
}
function readHistory(storageKey: string): string[] {
try {
const raw = localStorage.getItem(storageKey);
if (!raw) return [];
const parsed: unknown = JSON.parse(raw);
return Array.isArray(parsed)
? parsed.filter((v) => typeof v === 'string')
: [];
} catch {
return [];
}
}
function loadHistory(
storageKey: string,
fallbackStorageKey?: string,
): string[] {
const history = readHistory(storageKey);
return history.length === 0 && fallbackStorageKey
? readHistory(fallbackStorageKey)
: history;
}
function saveHistory(storageKey: string, history: string[]) {
try {
localStorage.setItem(
storageKey,
JSON.stringify(history.slice(-MAX_HISTORY)),
);
} catch {
// Ignore storage failures in private browsing or restricted contexts.
}
}
export function pushInputHistoryEntry(
storageKey: string,
text: string,
fallbackStorageKey?: string,
): void {
const history = loadHistory(storageKey, fallbackStorageKey);
if (history[history.length - 1] === text) {
if (fallbackStorageKey && readHistory(storageKey).length === 0) {
saveHistory(storageKey, history);
}
return;
}
history.push(text);
if (history.length > MAX_HISTORY) history.shift();
saveHistory(storageKey, history);
}
export function useInputHistory(
storageKey = DEFAULT_STORAGE_KEY,
fallbackStorageKey?: string,
) {
const storageKeyRef = useRef(storageKey);
const fallbackStorageKeyRef = useRef(fallbackStorageKey);
const loadedStorageKeyRef = useRef(storageKey);
const loadedFallbackStorageKeyRef = useRef(fallbackStorageKey);
storageKeyRef.current = storageKey;
fallbackStorageKeyRef.current = fallbackStorageKey;
const historyRef = useRef<string[]>([]);
const historyLoadedRef = useRef(false);
if (!historyLoadedRef.current) {
historyRef.current = loadHistory(storageKey, fallbackStorageKey);
historyLoadedRef.current = true;
}
const indexRef = useRef<number>(-1);
const draftRef = useRef<string>('');
const searchIndexRef = useRef<number>(-1);
// Drives the enabled/disabled state of the history nav buttons. canUp: an
// older entry exists to recall; canDown: currently browsing history (a newer
// entry or the saved draft to return to).
const [nav, setNav] = useState(() => ({
canUp: historyRef.current.length > 0,
canDown: false,
}));
const syncNav = useCallback(() => {
const h = historyRef.current;
const i = indexRef.current;
setNav({
canUp: h.length > 0 && (i === -1 || i > 0),
canDown: i !== -1,
});
}, []);
useEffect(() => {
if (
loadedStorageKeyRef.current === storageKey &&
loadedFallbackStorageKeyRef.current === fallbackStorageKey
) {
return;
}
loadedStorageKeyRef.current = storageKey;
loadedFallbackStorageKeyRef.current = fallbackStorageKey;
historyRef.current = loadHistory(storageKey, fallbackStorageKey);
indexRef.current = -1;
draftRef.current = '';
searchIndexRef.current = -1;
syncNav();
}, [fallbackStorageKey, storageKey, syncNav]);
const push = useCallback(
(text: string) => {
const h = historyRef.current;
if (h[h.length - 1] === text) {
if (
fallbackStorageKeyRef.current &&
readHistory(storageKeyRef.current).length === 0
) {
saveHistory(storageKeyRef.current, h);
}
return;
}
h.push(text);
if (h.length > MAX_HISTORY) h.shift();
saveHistory(storageKeyRef.current, h);
indexRef.current = -1;
syncNav();
},
[syncNav],
);
const navigateUp = useCallback(
(currentText: string): string | null => {
const h = historyRef.current;
if (h.length === 0) return null;
if (indexRef.current === -1) {
draftRef.current = currentText;
indexRef.current = h.length - 1;
} else if (indexRef.current > 0) {
indexRef.current--;
} else {
return null;
}
syncNav();
return h[indexRef.current];
},
[syncNav],
);
const navigateDown = useCallback((): string | null => {
const h = historyRef.current;
if (indexRef.current === -1) return null;
if (indexRef.current < h.length - 1) {
indexRef.current++;
syncNav();
return h[indexRef.current];
} else {
indexRef.current = -1;
syncNav();
return draftRef.current;
}
}, [syncNav]);
const isNavigating = useCallback(() => indexRef.current !== -1, []);
const reset = useCallback(() => {
indexRef.current = -1;
searchIndexRef.current = -1;
syncNav();
}, [syncNav]);
const searchReverse = useCallback((query: string): string | null => {
const h = historyRef.current;
if (h.length === 0 || !query) return null;
const startIdx =
searchIndexRef.current === -1 ? h.length - 1 : searchIndexRef.current - 1;
if (startIdx < 0) {
searchIndexRef.current = -1;
return null;
}
const lowerQuery = query.toLowerCase();
for (let i = startIdx; i >= 0; i--) {
if (h[i].toLowerCase().includes(lowerQuery)) {
searchIndexRef.current = i;
return h[i];
}
}
searchIndexRef.current = -1;
return null;
}, []);
const getReverseMatches = useCallback((query: string): string[] => {
const lowerQuery = query.trim().toLowerCase();
return historyRef.current
.slice()
.reverse()
.filter((item) => !lowerQuery || item.toLowerCase().includes(lowerQuery));
}, []);
const getLastEntry = useCallback(
(filter?: (entry: string) => boolean): string | null => {
const h = historyRef.current;
if (!filter) return h.length > 0 ? h[h.length - 1] : null;
for (let i = h.length - 1; i >= 0; i--) {
if (filter(h[i])) return h[i];
}
return null;
},
[],
);
const resetSearch = useCallback(() => {
searchIndexRef.current = -1;
}, []);
return {
push,
navigateUp,
navigateDown,
isNavigating,
reset,
searchReverse,
getReverseMatches,
getLastEntry,
resetSearch,
nav,
};
}