* feat(BottomTerminalDock): add close button next to the fullscreen toggle in the dock * style: replace hardcoded gradient with theme value in shine text variant * fix(header): adapt instance button for desktop only * refactor(chat): polish sticky turn UX and message action rows for better readability Switch to stable sticky-only turn behavior and redesign user/assistant action controls (placement, hover rules, ordering, spacing, selection-safe clamp) to reduce visual noise and improve interaction flow. * feat(chat/message): refactor buttons in messages footer * feat: enhance image preview functionality in chat messages - Added a new ImagePreviewDialog component to handle image previews with navigation support. - Updated ToolOutputDialog to utilize the new ImagePreviewDialog for displaying images. - Modified the ToolPopupContent type to include a gallery of images and an index for the current image. - Removed the old inline image display logic from ToolOutputDialog. - Improved file handling in the file store, including better MIME type guessing and handling of server paths. - Introduced a new API endpoint for handling large session message payloads, allowing for better management of multi-file attachments. - Updated the VSCode bridge to support session message requests with appropriate headers and body handling. * feat(proxy): implement SSE forwarding and enhance generic API request handling * feat(chat): support submitting only queued messages * feat: add image preview transition state * fix: default VSCode view to draft and fixed sessions list regression
225 lines
5.7 KiB
TypeScript
225 lines
5.7 KiB
TypeScript
let safeStorageInstance: Storage | null = null;
|
|
let safeSessionStorageInstance: Storage | null = null;
|
|
|
|
const createInMemoryStorage = (): Storage => {
|
|
const store = new Map<string, string>();
|
|
return {
|
|
getItem: (key: string) => store.get(key) ?? null,
|
|
setItem: (key: string, value: string) => {
|
|
store.set(key, value);
|
|
},
|
|
removeItem: (key: string) => {
|
|
store.delete(key);
|
|
},
|
|
clear: () => {
|
|
store.clear();
|
|
},
|
|
key: (index: number) => Array.from(store.keys())[index] ?? null,
|
|
get length() {
|
|
return store.size;
|
|
},
|
|
} as Storage;
|
|
};
|
|
|
|
const createSafeStorage = (): Storage => {
|
|
if (typeof window === 'undefined' || !window.localStorage) {
|
|
return createInMemoryStorage();
|
|
}
|
|
|
|
const baseStorage = window.localStorage;
|
|
const fallback = createInMemoryStorage();
|
|
let storageAvailable = true;
|
|
|
|
const disableStorage = () => {
|
|
storageAvailable = false;
|
|
};
|
|
|
|
const safeGet = (key: string): string | null => {
|
|
if (storageAvailable) {
|
|
try {
|
|
const value = baseStorage.getItem(key);
|
|
if (value !== null) {
|
|
return value;
|
|
}
|
|
} catch {
|
|
disableStorage();
|
|
}
|
|
}
|
|
return fallback.getItem(key);
|
|
};
|
|
|
|
const safeSet = (key: string, value: string) => {
|
|
if (storageAvailable) {
|
|
try {
|
|
baseStorage.setItem(key, value);
|
|
fallback.removeItem(key);
|
|
return;
|
|
} catch {
|
|
disableStorage();
|
|
// Prevent stale previous value from surviving when writes fail (e.g. quota).
|
|
try {
|
|
baseStorage.removeItem(key);
|
|
} catch {
|
|
// noop
|
|
}
|
|
}
|
|
}
|
|
fallback.setItem(key, value);
|
|
};
|
|
|
|
const safeRemove = (key: string) => {
|
|
try {
|
|
baseStorage.removeItem(key);
|
|
} catch {
|
|
disableStorage();
|
|
}
|
|
fallback.removeItem(key);
|
|
};
|
|
|
|
const safeClear = () => {
|
|
try {
|
|
baseStorage.clear();
|
|
} catch {
|
|
disableStorage();
|
|
}
|
|
fallback.clear();
|
|
};
|
|
|
|
const safeKey = (index: number): string | null => {
|
|
if (storageAvailable) {
|
|
try {
|
|
return baseStorage.key(index);
|
|
} catch {
|
|
disableStorage();
|
|
}
|
|
}
|
|
return fallback.key(index);
|
|
};
|
|
|
|
return {
|
|
getItem: safeGet,
|
|
setItem: safeSet,
|
|
removeItem: safeRemove,
|
|
clear: safeClear,
|
|
key: safeKey,
|
|
get length() {
|
|
if (storageAvailable) {
|
|
try {
|
|
return baseStorage.length + fallback.length;
|
|
} catch {
|
|
disableStorage();
|
|
}
|
|
}
|
|
return fallback.length;
|
|
},
|
|
} as Storage;
|
|
};
|
|
|
|
export const getSafeStorage = (): Storage => {
|
|
if (!safeStorageInstance) {
|
|
safeStorageInstance = createSafeStorage();
|
|
}
|
|
return safeStorageInstance;
|
|
};
|
|
|
|
const createSafeSessionStorage = (): Storage => {
|
|
if (typeof window === 'undefined' || !window.sessionStorage) {
|
|
return createInMemoryStorage();
|
|
}
|
|
|
|
const baseStorage = window.sessionStorage;
|
|
const fallback = createInMemoryStorage();
|
|
let storageAvailable = true;
|
|
|
|
const disableStorage = () => {
|
|
storageAvailable = false;
|
|
};
|
|
|
|
const safeGet = (key: string): string | null => {
|
|
if (storageAvailable) {
|
|
try {
|
|
const value = baseStorage.getItem(key);
|
|
if (value !== null) {
|
|
return value;
|
|
}
|
|
} catch {
|
|
disableStorage();
|
|
}
|
|
}
|
|
return fallback.getItem(key);
|
|
};
|
|
|
|
const safeSet = (key: string, value: string) => {
|
|
if (storageAvailable) {
|
|
try {
|
|
baseStorage.setItem(key, value);
|
|
fallback.removeItem(key);
|
|
return;
|
|
} catch {
|
|
disableStorage();
|
|
// Prevent stale previous value from surviving when writes fail (e.g. quota).
|
|
try {
|
|
baseStorage.removeItem(key);
|
|
} catch {
|
|
// noop
|
|
}
|
|
}
|
|
}
|
|
fallback.setItem(key, value);
|
|
};
|
|
|
|
const safeRemove = (key: string) => {
|
|
try {
|
|
baseStorage.removeItem(key);
|
|
} catch {
|
|
disableStorage();
|
|
}
|
|
fallback.removeItem(key);
|
|
};
|
|
|
|
const safeClear = () => {
|
|
try {
|
|
baseStorage.clear();
|
|
} catch {
|
|
disableStorage();
|
|
}
|
|
fallback.clear();
|
|
};
|
|
|
|
const safeKey = (index: number): string | null => {
|
|
if (storageAvailable) {
|
|
try {
|
|
return baseStorage.key(index);
|
|
} catch {
|
|
disableStorage();
|
|
}
|
|
}
|
|
return fallback.key(index);
|
|
};
|
|
|
|
return {
|
|
getItem: safeGet,
|
|
setItem: safeSet,
|
|
removeItem: safeRemove,
|
|
clear: safeClear,
|
|
key: safeKey,
|
|
get length() {
|
|
if (storageAvailable) {
|
|
try {
|
|
return baseStorage.length + fallback.length;
|
|
} catch {
|
|
disableStorage();
|
|
}
|
|
}
|
|
return fallback.length;
|
|
},
|
|
} as Storage;
|
|
};
|
|
|
|
export const getSafeSessionStorage = (): Storage => {
|
|
if (!safeSessionStorageInstance) {
|
|
safeSessionStorageInstance = createSafeSessionStorage();
|
|
}
|
|
return safeSessionStorageInstance;
|
|
};
|