mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-03 13:24:41 +00:00
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 * 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>
238 lines
6.3 KiB
TypeScript
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,
|
|
};
|
|
}
|