@sudodevstudio/astro-ai 0.1.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/LICENSE +21 -0
- package/README.md +114 -0
- package/dist/agent/agent-fallback.d.ts +50 -0
- package/dist/agent/agent-fallback.d.ts.map +1 -0
- package/dist/agent/agent-fallback.js +31 -0
- package/dist/agent/agent-fallback.js.map +1 -0
- package/dist/agent/cli-agent-fallback.d.ts +79 -0
- package/dist/agent/cli-agent-fallback.d.ts.map +1 -0
- package/dist/agent/cli-agent-fallback.js +1056 -0
- package/dist/agent/cli-agent-fallback.js.map +1 -0
- package/dist/integration/index.d.ts +28 -0
- package/dist/integration/index.d.ts.map +1 -0
- package/dist/integration/index.js +389 -0
- package/dist/integration/index.js.map +1 -0
- package/dist/resolver/astro-resolver.d.ts +28 -0
- package/dist/resolver/astro-resolver.d.ts.map +1 -0
- package/dist/resolver/astro-resolver.js +835 -0
- package/dist/resolver/astro-resolver.js.map +1 -0
- package/dist/resolver/types.d.ts +39 -0
- package/dist/resolver/types.d.ts.map +1 -0
- package/dist/resolver/types.js +2 -0
- package/dist/resolver/types.js.map +1 -0
- package/dist/shared/protocol.d.ts +150 -0
- package/dist/shared/protocol.d.ts.map +1 -0
- package/dist/shared/protocol.js +28 -0
- package/dist/shared/protocol.js.map +1 -0
- package/dist/shared/selection-context.d.ts +75 -0
- package/dist/shared/selection-context.d.ts.map +1 -0
- package/dist/shared/selection-context.js +2 -0
- package/dist/shared/selection-context.js.map +1 -0
- package/dist/shared/visual-components.d.ts +15 -0
- package/dist/shared/visual-components.d.ts.map +1 -0
- package/dist/shared/visual-components.js +2 -0
- package/dist/shared/visual-components.js.map +1 -0
- package/dist/toolbar/action-model.d.ts +20 -0
- package/dist/toolbar/action-model.d.ts.map +1 -0
- package/dist/toolbar/action-model.js +49 -0
- package/dist/toolbar/action-model.js.map +1 -0
- package/dist/toolbar/app.d.ts +3 -0
- package/dist/toolbar/app.d.ts.map +1 -0
- package/dist/toolbar/app.js +274 -0
- package/dist/toolbar/app.js.map +1 -0
- package/dist/toolbar/chat-drawer.d.ts +111 -0
- package/dist/toolbar/chat-drawer.d.ts.map +1 -0
- package/dist/toolbar/chat-drawer.js +1575 -0
- package/dist/toolbar/chat-drawer.js.map +1 -0
- package/dist/toolbar/chat-windows.d.ts +62 -0
- package/dist/toolbar/chat-windows.d.ts.map +1 -0
- package/dist/toolbar/chat-windows.js +287 -0
- package/dist/toolbar/chat-windows.js.map +1 -0
- package/dist/toolbar/contextual-actions.d.ts +24 -0
- package/dist/toolbar/contextual-actions.d.ts.map +1 -0
- package/dist/toolbar/contextual-actions.js +371 -0
- package/dist/toolbar/contextual-actions.js.map +1 -0
- package/dist/toolbar/diagnostic-actions.d.ts +22 -0
- package/dist/toolbar/diagnostic-actions.d.ts.map +1 -0
- package/dist/toolbar/diagnostic-actions.js +127 -0
- package/dist/toolbar/diagnostic-actions.js.map +1 -0
- package/dist/toolbar/layers.d.ts +26 -0
- package/dist/toolbar/layers.d.ts.map +1 -0
- package/dist/toolbar/layers.js +31 -0
- package/dist/toolbar/layers.js.map +1 -0
- package/dist/toolbar/overlay.d.ts +40 -0
- package/dist/toolbar/overlay.d.ts.map +1 -0
- package/dist/toolbar/overlay.js +619 -0
- package/dist/toolbar/overlay.js.map +1 -0
- package/dist/visual/capability-resolver.d.ts +11 -0
- package/dist/visual/capability-resolver.d.ts.map +1 -0
- package/dist/visual/capability-resolver.js +69 -0
- package/dist/visual/capability-resolver.js.map +1 -0
- package/dist/visual/command-engine.d.ts +10 -0
- package/dist/visual/command-engine.d.ts.map +1 -0
- package/dist/visual/command-engine.js +199 -0
- package/dist/visual/command-engine.js.map +1 -0
- package/dist/visual/commands.d.ts +36 -0
- package/dist/visual/commands.d.ts.map +1 -0
- package/dist/visual/commands.js +2 -0
- package/dist/visual/commands.js.map +1 -0
- package/dist/visual/patch-transactions.d.ts +57 -0
- package/dist/visual/patch-transactions.d.ts.map +1 -0
- package/dist/visual/patch-transactions.js +259 -0
- package/dist/visual/patch-transactions.js.map +1 -0
- package/dist/vite/build-ai-plugin.d.ts +11 -0
- package/dist/vite/build-ai-plugin.d.ts.map +1 -0
- package/dist/vite/build-ai-plugin.js +45 -0
- package/dist/vite/build-ai-plugin.js.map +1 -0
- package/package.json +85 -0
- package/src/agent/agent-fallback.ts +95 -0
- package/src/agent/cli-agent-fallback.ts +1382 -0
- package/src/integration/index.ts +475 -0
- package/src/resolver/astro-resolver.ts +1024 -0
- package/src/resolver/types.ts +47 -0
- package/src/shared/protocol.ts +181 -0
- package/src/shared/selection-context.ts +91 -0
- package/src/shared/visual-components.ts +16 -0
- package/src/toolbar/action-model.ts +67 -0
- package/src/toolbar/app.ts +294 -0
- package/src/toolbar/chat-drawer.ts +1742 -0
- package/src/toolbar/chat-windows.ts +331 -0
- package/src/toolbar/contextual-actions.ts +401 -0
- package/src/toolbar/diagnostic-actions.ts +151 -0
- package/src/toolbar/layers.ts +32 -0
- package/src/toolbar/overlay.ts +638 -0
- package/src/visual/capability-resolver.ts +83 -0
- package/src/visual/command-engine.ts +250 -0
- package/src/visual/commands.ts +37 -0
- package/src/visual/patch-transactions.ts +270 -0
- package/src/vite/build-ai-plugin.ts +46 -0
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentExternalContext,
|
|
3
|
+
AgentOperationEvent,
|
|
4
|
+
ServerReadyMessage,
|
|
5
|
+
} from '../shared/protocol.js';
|
|
6
|
+
import { DEFAULT_SESSION_ID } from '../shared/protocol.js';
|
|
7
|
+
import type { SelectionContext } from '../shared/selection-context.js';
|
|
8
|
+
import { ChatDrawer, type AgentSubmit } from './chat-drawer.js';
|
|
9
|
+
import { chatWindowLayer } from './layers.js';
|
|
10
|
+
|
|
11
|
+
export type ChatSessionRecord = { id: string; title: string };
|
|
12
|
+
|
|
13
|
+
export type ChatWindowCallbacks = {
|
|
14
|
+
onSubmit(request: AgentSubmit): void;
|
|
15
|
+
onCancel(requestId: string): void;
|
|
16
|
+
onUndo(): void;
|
|
17
|
+
onRedo(): void;
|
|
18
|
+
/** The server may release the conversation and workspace held for this window. */
|
|
19
|
+
onSessionClose(sessionId: string): void;
|
|
20
|
+
/** The last window was closed, so the toolbar app should close too. */
|
|
21
|
+
onEmpty(): void;
|
|
22
|
+
/** True once every window is collapsed, which pauses page selection mode. */
|
|
23
|
+
onSelectionPaused(paused: boolean): void;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const SESSIONS_KEY = 'astro-ai:chat-sessions';
|
|
27
|
+
const FOCUS_KEY = 'astro-ai:chat-focused';
|
|
28
|
+
export const MAX_CHAT_WINDOWS = 6;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Owns every open chat window. Each window is an independent conversation with
|
|
32
|
+
* its own server session; the shared source history is mirrored into all of
|
|
33
|
+
* them because every window edits the same project files.
|
|
34
|
+
*/
|
|
35
|
+
export class ChatWindowManager {
|
|
36
|
+
readonly element: HTMLElement;
|
|
37
|
+
readonly #callbacks: ChatWindowCallbacks;
|
|
38
|
+
readonly #drawers = new Map<string, ChatDrawer>();
|
|
39
|
+
#order: string[] = [];
|
|
40
|
+
#focusedId: string | undefined;
|
|
41
|
+
#provider: ServerReadyMessage['agent'] | undefined;
|
|
42
|
+
#history: ServerReadyMessage['history'] | undefined;
|
|
43
|
+
/** Session ids, least recently focused first, deciding the stacking order. */
|
|
44
|
+
#stack: string[] = [];
|
|
45
|
+
#visible = false;
|
|
46
|
+
|
|
47
|
+
constructor(callbacks: ChatWindowCallbacks) {
|
|
48
|
+
this.#callbacks = callbacks;
|
|
49
|
+
this.element = document.createElement('div');
|
|
50
|
+
this.element.className = 'ai-chat-windows';
|
|
51
|
+
for (const record of restoreSessions()) this.#mount(record);
|
|
52
|
+
if (this.#drawers.size === 0) {
|
|
53
|
+
this.#mount({ id: DEFAULT_SESSION_ID, title: 'Chat 1' });
|
|
54
|
+
}
|
|
55
|
+
const persistedFocus = readSession(FOCUS_KEY);
|
|
56
|
+
const initialFocus = persistedFocus !== null && this.#drawers.has(persistedFocus)
|
|
57
|
+
? persistedFocus
|
|
58
|
+
: this.#order[0];
|
|
59
|
+
if (initialFocus !== undefined) this.focus(initialFocus);
|
|
60
|
+
this.#persist();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
get size(): number {
|
|
64
|
+
return this.#drawers.size;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
sessionIds(): string[] {
|
|
68
|
+
return [...this.#order];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
focused(): ChatDrawer | undefined {
|
|
72
|
+
return this.#focusedId === undefined ? undefined : this.#drawers.get(this.#focusedId);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get(sessionId: string): ChatDrawer | undefined {
|
|
76
|
+
return this.#drawers.get(sessionId);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
focus(sessionId: string): void {
|
|
80
|
+
const drawer = this.#drawers.get(sessionId);
|
|
81
|
+
if (drawer === undefined) return;
|
|
82
|
+
this.#focusedId = sessionId;
|
|
83
|
+
this.#stack = [...this.#stack.filter((id) => id !== sessionId), sessionId];
|
|
84
|
+
this.#applyLayers();
|
|
85
|
+
writeSession(FOCUS_KEY, sessionId);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Restacks every window. The focused one takes the top slot and the rest keep
|
|
90
|
+
* their recency order below it — all still above the selection overlays, so a
|
|
91
|
+
* chat window is never painted over by the outlines or connectors.
|
|
92
|
+
*/
|
|
93
|
+
#applyLayers(): void {
|
|
94
|
+
for (const [id, drawer] of this.#drawers) {
|
|
95
|
+
const rank = this.#stack.indexOf(id);
|
|
96
|
+
drawer.setFocused(id === this.#focusedId, chatWindowLayer(id === this.#focusedId, rank));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Opens an additional chat window with its own conversation. */
|
|
101
|
+
create(): ChatDrawer | undefined {
|
|
102
|
+
if (this.#drawers.size >= MAX_CHAT_WINDOWS) {
|
|
103
|
+
this.focused()?.setNotice(
|
|
104
|
+
`Chat windows are limited to ${MAX_CHAT_WINDOWS}. Close one to open another.`,
|
|
105
|
+
true,
|
|
106
|
+
);
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
const drawer = this.#mount({
|
|
110
|
+
id: createSessionId(),
|
|
111
|
+
title: nextChatWindowTitle(this.titles()),
|
|
112
|
+
});
|
|
113
|
+
this.#persist();
|
|
114
|
+
if (this.#provider !== undefined) drawer.setProvider(this.#provider);
|
|
115
|
+
if (this.#history !== undefined) drawer.setHistory(this.#history);
|
|
116
|
+
this.focus(drawer.sessionId);
|
|
117
|
+
if (this.#visible) drawer.open();
|
|
118
|
+
return drawer;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
close(sessionId: string): void {
|
|
122
|
+
const drawer = this.#drawers.get(sessionId);
|
|
123
|
+
if (drawer === undefined) return;
|
|
124
|
+
drawer.clearStoredState();
|
|
125
|
+
drawer.destroy();
|
|
126
|
+
this.#drawers.delete(sessionId);
|
|
127
|
+
this.#order = this.#order.filter((id) => id !== sessionId);
|
|
128
|
+
this.#stack = this.#stack.filter((id) => id !== sessionId);
|
|
129
|
+
this.#callbacks.onSessionClose(sessionId);
|
|
130
|
+
this.#persist();
|
|
131
|
+
if (this.#focusedId === sessionId) {
|
|
132
|
+
this.#focusedId = undefined;
|
|
133
|
+
const next = this.#order.at(-1);
|
|
134
|
+
if (next !== undefined) this.focus(next);
|
|
135
|
+
}
|
|
136
|
+
// With no windows left the app closes outright, so resuming selection mode
|
|
137
|
+
// first would only enable an overlay that is about to be torn down.
|
|
138
|
+
if (this.#drawers.size === 0) this.#callbacks.onEmpty();
|
|
139
|
+
else this.#syncSelectionPaused();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
titles(): string[] {
|
|
143
|
+
return this.#order.map((id) => this.#drawers.get(id)?.title ?? '');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
setProvider(provider: ServerReadyMessage['agent']): void {
|
|
147
|
+
this.#provider = provider;
|
|
148
|
+
for (const drawer of this.#drawers.values()) drawer.setProvider(provider);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
setHistory(history: ServerReadyMessage['history']): void {
|
|
152
|
+
this.#history = history;
|
|
153
|
+
for (const drawer of this.#drawers.values()) {
|
|
154
|
+
drawer.setHistory(history);
|
|
155
|
+
// A committed transaction re-renders the page, so every window has to
|
|
156
|
+
// re-measure the element its arrow points at.
|
|
157
|
+
drawer.refreshConnector();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Ambient page status that applies to every window. */
|
|
162
|
+
broadcastNotice(message: string, error = false): void {
|
|
163
|
+
for (const drawer of this.#drawers.values()) drawer.setNotice(message, error);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Run status that belongs to one conversation only. */
|
|
167
|
+
noticeFor(sessionId: string, message: string, error = false): void {
|
|
168
|
+
this.#drawers.get(sessionId)?.setNotice(message, error);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
setCurrentSelections(contexts: SelectionContext[], elements: readonly HTMLElement[] = []): void {
|
|
172
|
+
// Only the focused window can attach the page selection; the others keep
|
|
173
|
+
// whatever context their own conversation was started with.
|
|
174
|
+
this.focused()?.setCurrentSelections(contexts, elements);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
setSelectionAnchor(rect?: Parameters<ChatDrawer['setSelectionAnchor']>[0]): void {
|
|
178
|
+
for (const drawer of this.#drawers.values()) {
|
|
179
|
+
drawer.setSelectionAnchor(drawer === this.focused() ? rect : undefined);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
openWithSelections(contexts: SelectionContext[], elements: readonly HTMLElement[] = []): void {
|
|
184
|
+
const drawer = this.focused() ?? this.#drawers.values().next().value;
|
|
185
|
+
drawer?.openWithSelections(contexts, elements);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
openWithExternalContext(context: AgentExternalContext): void {
|
|
189
|
+
const drawer = this.focused() ?? this.#drawers.values().next().value;
|
|
190
|
+
drawer?.openWithExternalContext(context);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
handleAgentEvent(event: AgentOperationEvent): ChatDrawer | undefined {
|
|
194
|
+
const drawer = this.#drawers.get(event.sessionId ?? DEFAULT_SESSION_ID);
|
|
195
|
+
if (drawer === undefined) return undefined;
|
|
196
|
+
drawer.handleAgentEvent(event);
|
|
197
|
+
return drawer;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
pendingRequestIds(): string[] {
|
|
201
|
+
return [...this.#drawers.values()].flatMap((drawer) => drawer.pendingRequestIds());
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Drops every window's stale page references after a client-side navigation. */
|
|
205
|
+
handleNavigation(): void {
|
|
206
|
+
for (const drawer of this.#drawers.values()) drawer.handleNavigation();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** True once the windows have been shown, so navigation can restore them. */
|
|
210
|
+
get visible(): boolean {
|
|
211
|
+
return this.#visible;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
openAll(focus: boolean, persist: boolean, expand: boolean): void {
|
|
215
|
+
this.#visible = true;
|
|
216
|
+
for (const drawer of this.#drawers.values()) {
|
|
217
|
+
drawer.open(focus && drawer === this.focused(), persist, expand);
|
|
218
|
+
}
|
|
219
|
+
this.#syncSelectionPaused();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
hideAll(persist = true): void {
|
|
223
|
+
this.#visible = false;
|
|
224
|
+
for (const drawer of this.#drawers.values()) drawer.hide(persist);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
destroy(): void {
|
|
228
|
+
for (const drawer of this.#drawers.values()) drawer.destroy();
|
|
229
|
+
this.#drawers.clear();
|
|
230
|
+
this.#order = [];
|
|
231
|
+
this.element.remove();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
#mount(record: ChatSessionRecord): ChatDrawer {
|
|
235
|
+
const index = this.#order.length;
|
|
236
|
+
const drawer = new ChatDrawer(
|
|
237
|
+
{
|
|
238
|
+
onSubmit: (request) => this.#callbacks.onSubmit(request),
|
|
239
|
+
onCancel: (requestId) => this.#callbacks.onCancel(requestId),
|
|
240
|
+
onUndo: () => this.#callbacks.onUndo(),
|
|
241
|
+
onRedo: () => this.#callbacks.onRedo(),
|
|
242
|
+
onClose: () => this.close(record.id),
|
|
243
|
+
onNewWindow: () => {
|
|
244
|
+
this.create();
|
|
245
|
+
},
|
|
246
|
+
onRename: () => this.#persist(),
|
|
247
|
+
onFocus: () => this.focus(record.id),
|
|
248
|
+
onMinimizedChange: () => this.#syncSelectionPaused(),
|
|
249
|
+
},
|
|
250
|
+
{ sessionId: record.id, title: record.title, index },
|
|
251
|
+
);
|
|
252
|
+
this.#drawers.set(record.id, drawer);
|
|
253
|
+
this.#order.push(record.id);
|
|
254
|
+
this.#stack.push(record.id);
|
|
255
|
+
this.element.append(drawer.element);
|
|
256
|
+
drawer.hide(false);
|
|
257
|
+
this.#applyLayers();
|
|
258
|
+
return drawer;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
#syncSelectionPaused(): void {
|
|
262
|
+
const drawers = [...this.#drawers.values()];
|
|
263
|
+
this.#callbacks.onSelectionPaused(
|
|
264
|
+
drawers.length > 0 && drawers.every((drawer) => drawer.minimized),
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
#persist(): void {
|
|
269
|
+
writeSession(
|
|
270
|
+
SESSIONS_KEY,
|
|
271
|
+
JSON.stringify(
|
|
272
|
+
this.#order.map((id) => ({ id, title: this.#drawers.get(id)?.title ?? 'Chat' })),
|
|
273
|
+
),
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function nextChatWindowTitle(existing: readonly string[]): string {
|
|
279
|
+
const used = new Set(existing);
|
|
280
|
+
for (let index = 1; index <= MAX_CHAT_WINDOWS + existing.length; index += 1) {
|
|
281
|
+
const candidate = `Chat ${index}`;
|
|
282
|
+
if (!used.has(candidate)) return candidate;
|
|
283
|
+
}
|
|
284
|
+
return 'Chat';
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function createSessionId(): string {
|
|
288
|
+
return `chat-${crypto.randomUUID()}`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function parseSessionRecords(value: string | null): ChatSessionRecord[] {
|
|
292
|
+
if (value === null) return [];
|
|
293
|
+
let parsed: unknown;
|
|
294
|
+
try {
|
|
295
|
+
parsed = JSON.parse(value);
|
|
296
|
+
} catch {
|
|
297
|
+
return [];
|
|
298
|
+
}
|
|
299
|
+
if (!Array.isArray(parsed)) return [];
|
|
300
|
+
const seen = new Set<string>();
|
|
301
|
+
const records: ChatSessionRecord[] = [];
|
|
302
|
+
for (const entry of parsed) {
|
|
303
|
+
if (typeof entry !== 'object' || entry === null) continue;
|
|
304
|
+
const { id, title } = entry as Partial<ChatSessionRecord>;
|
|
305
|
+
if (typeof id !== 'string' || id === '' || seen.has(id)) continue;
|
|
306
|
+
seen.add(id);
|
|
307
|
+
records.push({ id, title: typeof title === 'string' && title !== '' ? title : 'Chat' });
|
|
308
|
+
if (records.length >= MAX_CHAT_WINDOWS) break;
|
|
309
|
+
}
|
|
310
|
+
return records;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function restoreSessions(): ChatSessionRecord[] {
|
|
314
|
+
return parseSessionRecords(readSession(SESSIONS_KEY));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function readSession(key: string): string | null {
|
|
318
|
+
try {
|
|
319
|
+
return sessionStorage.getItem(key);
|
|
320
|
+
} catch {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function writeSession(key: string, value: string): void {
|
|
326
|
+
try {
|
|
327
|
+
sessionStorage.setItem(key, value);
|
|
328
|
+
} catch {
|
|
329
|
+
// Session persistence is a progressive enhancement in restricted browsers.
|
|
330
|
+
}
|
|
331
|
+
}
|