@tt-a1i/openpi 0.6.1 → 0.7.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/THIRD_PARTY_NOTICES.md +242 -0
- package/extensions/ai-providers/cursor/connect-frame-reader.ts +76 -0
- package/extensions/ai-providers/cursor/provider.ts +6 -19
- package/extensions/file-mutation-display/index.ts +13 -9
- package/extensions/shared/agent-transcript.ts +3 -2
- package/extensions/web/index.ts +69 -5
- package/extensions/workflows/artifacts.ts +362 -23
- package/extensions/workflows/dashboard.ts +2 -0
- package/package.json +28 -4
- package/web/dist/app.js +87 -0
- package/web/dist/favicon.svg +9 -0
- package/web/dist/index.html +15 -0
- package/web/dist/styles.css +3 -0
- package/web/host/web-host.ts +6 -9
- package/web/ui/index.html +3 -131
- package/web/ui/public/favicon.svg +9 -0
- package/web/ui/src/app/App.tsx +134 -0
- package/web/ui/src/app/providers.tsx +38 -0
- package/web/ui/src/components/Markdown.tsx +58 -0
- package/web/ui/src/components/OpenPiLogo.tsx +41 -0
- package/web/ui/src/features/activity/ActivityBar.tsx +120 -0
- package/web/ui/src/features/composer/Composer.tsx +237 -0
- package/web/ui/src/features/sessions/SessionSidebar.tsx +418 -0
- package/web/ui/src/features/transcript/Transcript.tsx +860 -0
- package/web/ui/src/i18n.ts +159 -0
- package/web/ui/src/lib/format.ts +57 -0
- package/web/ui/src/main.tsx +16 -0
- package/web/ui/src/protocol/client.ts +199 -0
- package/web/ui/src/protocol/event-stream.ts +88 -0
- package/web/ui/src/store/web-store.ts +926 -0
- package/web/ui/src/styles.css +420 -0
- package/web/ui/tsconfig.json +12 -0
- package/web/ui/vite-env.d.ts +1 -0
- package/web/vite.config.mjs +21 -1
- package/web/host/static-assets.ts +0 -4
- package/web/ui/app.js +0 -1700
- package/web/ui/styles.css +0 -680
|
@@ -0,0 +1,926 @@
|
|
|
1
|
+
import { createStore } from "zustand/vanilla";
|
|
2
|
+
import type {
|
|
3
|
+
WebEvent,
|
|
4
|
+
WebLiveMessage,
|
|
5
|
+
WebSnapshot,
|
|
6
|
+
} from "../../../protocol/types.ts";
|
|
7
|
+
import { WebApiError, WebClient } from "../protocol/client.ts";
|
|
8
|
+
import { consumeEventStream } from "../protocol/event-stream.ts";
|
|
9
|
+
|
|
10
|
+
const collapsedWorkspacesStorageKey = "openpi.collapsed-workspaces";
|
|
11
|
+
const sidebarCollapsedStorageKey = "openpi.sidebar-collapsed";
|
|
12
|
+
const refreshEventTypes = new Set([
|
|
13
|
+
"agent_start",
|
|
14
|
+
"turn_started",
|
|
15
|
+
"turn_settled",
|
|
16
|
+
"agent_settled",
|
|
17
|
+
"prompt_settled",
|
|
18
|
+
"message_end",
|
|
19
|
+
"tool_execution_end",
|
|
20
|
+
"session_start",
|
|
21
|
+
"session_switched",
|
|
22
|
+
"session_progress",
|
|
23
|
+
"prompt_failed",
|
|
24
|
+
"model_select",
|
|
25
|
+
"workspace_imported",
|
|
26
|
+
"workspace_removed",
|
|
27
|
+
"workspace_renamed",
|
|
28
|
+
"session_renamed",
|
|
29
|
+
"session_archived",
|
|
30
|
+
"session_created",
|
|
31
|
+
"prompt_accepted",
|
|
32
|
+
"runtime_changed",
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
function readStringSet(key: string) {
|
|
36
|
+
try {
|
|
37
|
+
const value: unknown = JSON.parse(
|
|
38
|
+
window.sessionStorage.getItem(key) || "[]",
|
|
39
|
+
);
|
|
40
|
+
return new Set(
|
|
41
|
+
Array.isArray(value)
|
|
42
|
+
? value.filter((item): item is string => typeof item === "string")
|
|
43
|
+
: [],
|
|
44
|
+
);
|
|
45
|
+
} catch {
|
|
46
|
+
return new Set<string>();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readBoolean(key: string) {
|
|
51
|
+
try {
|
|
52
|
+
return window.sessionStorage.getItem(key) === "true";
|
|
53
|
+
} catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function persist(key: string, value: unknown) {
|
|
59
|
+
try {
|
|
60
|
+
window.sessionStorage.setItem(key, JSON.stringify(value));
|
|
61
|
+
} catch {}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface LiveEntry {
|
|
65
|
+
key: string;
|
|
66
|
+
message: WebLiveMessage;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface SessionActivation {
|
|
70
|
+
epoch: number;
|
|
71
|
+
kind: "create" | "select";
|
|
72
|
+
commandId?: string;
|
|
73
|
+
expectedPath: string | null;
|
|
74
|
+
observedPath?: string | null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface WebStoreState {
|
|
78
|
+
activeTurn: WebSnapshot["runtime"]["activeTurn"] | null;
|
|
79
|
+
turnCancellationPending: boolean;
|
|
80
|
+
turnTerminalStatus: string | null;
|
|
81
|
+
pendingFollowUpsReceipt: number | null;
|
|
82
|
+
snapshot: WebSnapshot | null;
|
|
83
|
+
cursor: number | null;
|
|
84
|
+
selectedPath: string | null;
|
|
85
|
+
selectedWorkspace: string | null;
|
|
86
|
+
collapsed: Set<string>;
|
|
87
|
+
sidebarCollapsed: boolean;
|
|
88
|
+
mobileSidebarOpen: boolean;
|
|
89
|
+
query: string;
|
|
90
|
+
searchOpen: boolean;
|
|
91
|
+
connection: "connected" | "connecting" | "reconnecting" | "unavailable";
|
|
92
|
+
notice: string | null;
|
|
93
|
+
liveMessages: LiveEntry[];
|
|
94
|
+
liveRunning: boolean;
|
|
95
|
+
livePhase: "idle" | "preparing" | "running";
|
|
96
|
+
liveRetry: { attempt: number; maxAttempts: number } | null;
|
|
97
|
+
thinkingStarts: Record<string, number>;
|
|
98
|
+
thinkingDurations: Record<string, number>;
|
|
99
|
+
promptAdmissionPending: boolean;
|
|
100
|
+
sessionSwitching: boolean;
|
|
101
|
+
scrollToBottom: number;
|
|
102
|
+
actions: WebStoreActions;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface WebStoreActions {
|
|
106
|
+
start: () => void;
|
|
107
|
+
stop: () => void;
|
|
108
|
+
refreshSnapshot: (options?: {
|
|
109
|
+
resetCursor?: boolean;
|
|
110
|
+
epoch?: number;
|
|
111
|
+
canonicalRetry?: boolean;
|
|
112
|
+
}) => Promise<boolean>;
|
|
113
|
+
chooseWorkspace: () => Promise<void>;
|
|
114
|
+
setWorkspace: (path: string | null) => void;
|
|
115
|
+
renameWorkspace: (path: string, name: string) => Promise<void>;
|
|
116
|
+
removeWorkspace: (path: string) => Promise<void>;
|
|
117
|
+
createSession: (workspacePath: string) => Promise<void>;
|
|
118
|
+
selectSession: (path: string) => Promise<void>;
|
|
119
|
+
renameSession: (path: string, name: string) => Promise<void>;
|
|
120
|
+
archiveSession: (path: string) => Promise<void>;
|
|
121
|
+
selectModel: (value: string) => Promise<void>;
|
|
122
|
+
cancelActiveTurn: () => Promise<void>;
|
|
123
|
+
sendPrompt: (content: string) => Promise<boolean>;
|
|
124
|
+
setQuery: (query: string) => void;
|
|
125
|
+
setSearchOpen: (open: boolean) => void;
|
|
126
|
+
toggleWorkspace: (path: string) => void;
|
|
127
|
+
toggleSidebar: (narrow: boolean) => void;
|
|
128
|
+
closeMobileSidebar: () => void;
|
|
129
|
+
clearNotice: () => void;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface WebStoreDependencies {
|
|
133
|
+
consumeEvents?: typeof consumeEventStream;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function waitForReconnect(delay: number, signal: AbortSignal) {
|
|
137
|
+
if (signal.aborted) return Promise.resolve();
|
|
138
|
+
return new Promise<void>((resolve) => {
|
|
139
|
+
const finish = () => {
|
|
140
|
+
window.clearTimeout(timer);
|
|
141
|
+
signal.removeEventListener("abort", finish);
|
|
142
|
+
resolve();
|
|
143
|
+
};
|
|
144
|
+
const timer = window.setTimeout(finish, delay);
|
|
145
|
+
signal.addEventListener("abort", finish, { once: true });
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function createWebStore(
|
|
150
|
+
client = new WebClient(),
|
|
151
|
+
dependencies: WebStoreDependencies = {},
|
|
152
|
+
) {
|
|
153
|
+
const consumeEvents = dependencies.consumeEvents ?? consumeEventStream;
|
|
154
|
+
let sessionEpoch = 0;
|
|
155
|
+
let snapshotGeneration = 0;
|
|
156
|
+
let promptAdmissionSequence = 0;
|
|
157
|
+
let promptAdmissionToken: number | null = null;
|
|
158
|
+
let promptAdmission: {
|
|
159
|
+
sessionId: string;
|
|
160
|
+
content: string;
|
|
161
|
+
commandId: string;
|
|
162
|
+
optimisticKey: string;
|
|
163
|
+
} | null = null;
|
|
164
|
+
let sessionActivation: SessionActivation | null = null;
|
|
165
|
+
let sessionSelectionTail = Promise.resolve();
|
|
166
|
+
let refreshTimer: number | null = null;
|
|
167
|
+
let refreshInFlight = false;
|
|
168
|
+
let refreshPending = false;
|
|
169
|
+
let streamController: AbortController | null = null;
|
|
170
|
+
const terminalPromptIds = new Set<string>();
|
|
171
|
+
const completedActivationIds = new Set<string>();
|
|
172
|
+
|
|
173
|
+
const rememberBounded = (set: Set<string>, value: unknown) => {
|
|
174
|
+
if (typeof value !== "string") return;
|
|
175
|
+
set.add(value);
|
|
176
|
+
while (set.size > 32) {
|
|
177
|
+
const first = set.values().next().value;
|
|
178
|
+
if (first) set.delete(first);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const resetLivePatch = () => ({
|
|
183
|
+
activeTurn: null,
|
|
184
|
+
turnCancellationPending: false,
|
|
185
|
+
turnTerminalStatus: null,
|
|
186
|
+
pendingFollowUpsReceipt: null,
|
|
187
|
+
liveMessages: [] as LiveEntry[],
|
|
188
|
+
liveRunning: false,
|
|
189
|
+
livePhase: "idle" as const,
|
|
190
|
+
liveRetry: null,
|
|
191
|
+
thinkingStarts: {},
|
|
192
|
+
thinkingDurations: {},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const promptAcceptedLivePatch = (
|
|
196
|
+
settled: boolean,
|
|
197
|
+
currentPhase: WebStoreState["livePhase"],
|
|
198
|
+
) => ({
|
|
199
|
+
liveRunning: currentPhase === "running" || !settled,
|
|
200
|
+
livePhase:
|
|
201
|
+
currentPhase === "running"
|
|
202
|
+
? ("running" as const)
|
|
203
|
+
: settled
|
|
204
|
+
? ("idle" as const)
|
|
205
|
+
: ("preparing" as const),
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const store = createStore<WebStoreState>((set, get) => {
|
|
209
|
+
const showError = (error: unknown) => {
|
|
210
|
+
set({
|
|
211
|
+
notice: error instanceof Error ? error.message : String(error),
|
|
212
|
+
});
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const scheduleSnapshotRefresh = (delay = 160) => {
|
|
216
|
+
if (refreshInFlight) {
|
|
217
|
+
refreshPending = true;
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (refreshTimer !== null) return;
|
|
221
|
+
refreshTimer = window.setTimeout(async () => {
|
|
222
|
+
refreshTimer = null;
|
|
223
|
+
refreshInFlight = true;
|
|
224
|
+
try {
|
|
225
|
+
await get().actions.refreshSnapshot();
|
|
226
|
+
} finally {
|
|
227
|
+
refreshInFlight = false;
|
|
228
|
+
if (refreshPending) {
|
|
229
|
+
refreshPending = false;
|
|
230
|
+
scheduleSnapshotRefresh();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}, delay);
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const applyRuntimeEvent = (event: WebEvent) => {
|
|
237
|
+
const current = get();
|
|
238
|
+
const detail = event.detail ?? {};
|
|
239
|
+
const eventSessionId = detail.sessionId;
|
|
240
|
+
const sessionTransition = [
|
|
241
|
+
"session_start",
|
|
242
|
+
"session_switched",
|
|
243
|
+
"session_created",
|
|
244
|
+
].includes(event.type);
|
|
245
|
+
set({ cursor: event.sequence });
|
|
246
|
+
|
|
247
|
+
if (current.sessionSwitching && !sessionTransition) {
|
|
248
|
+
scheduleSnapshotRefresh();
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (
|
|
252
|
+
typeof eventSessionId === "string" &&
|
|
253
|
+
eventSessionId !== current.snapshot?.currentSessionId &&
|
|
254
|
+
!sessionTransition
|
|
255
|
+
) {
|
|
256
|
+
scheduleSnapshotRefresh();
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (sessionTransition) {
|
|
261
|
+
const eventCommandId = detail.commandId;
|
|
262
|
+
if (
|
|
263
|
+
typeof eventCommandId === "string" &&
|
|
264
|
+
completedActivationIds.has(eventCommandId)
|
|
265
|
+
) {
|
|
266
|
+
scheduleSnapshotRefresh();
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const eventPath = detail.sessionPath;
|
|
270
|
+
const knownPath = current.snapshot?.sessions.some(
|
|
271
|
+
(session) => session.path === eventPath,
|
|
272
|
+
);
|
|
273
|
+
let belongs = false;
|
|
274
|
+
if (sessionActivation?.kind === "select") {
|
|
275
|
+
belongs = sessionActivation.expectedPath === eventPath;
|
|
276
|
+
} else if (
|
|
277
|
+
sessionActivation?.kind === "create" &&
|
|
278
|
+
sessionActivation.commandId === eventCommandId &&
|
|
279
|
+
event.type === "session_switched" &&
|
|
280
|
+
typeof eventPath === "string" &&
|
|
281
|
+
!knownPath
|
|
282
|
+
) {
|
|
283
|
+
sessionActivation.observedPath = eventPath;
|
|
284
|
+
belongs = true;
|
|
285
|
+
} else if (
|
|
286
|
+
sessionActivation?.kind === "create" &&
|
|
287
|
+
sessionActivation.commandId === eventCommandId &&
|
|
288
|
+
event.type === "session_created" &&
|
|
289
|
+
typeof sessionActivation.observedPath === "string"
|
|
290
|
+
) {
|
|
291
|
+
belongs = true;
|
|
292
|
+
}
|
|
293
|
+
if (belongs && sessionActivation?.epoch !== sessionEpoch) return;
|
|
294
|
+
if (!belongs) {
|
|
295
|
+
const epoch = ++sessionEpoch;
|
|
296
|
+
promptAdmissionToken = null;
|
|
297
|
+
promptAdmission = null;
|
|
298
|
+
set({
|
|
299
|
+
...resetLivePatch(),
|
|
300
|
+
promptAdmissionPending: false,
|
|
301
|
+
selectedPath: typeof eventPath === "string" ? eventPath : null,
|
|
302
|
+
sessionSwitching: true,
|
|
303
|
+
});
|
|
304
|
+
void get()
|
|
305
|
+
.actions.refreshSnapshot({ epoch })
|
|
306
|
+
.then((refreshed) => {
|
|
307
|
+
if (epoch !== sessionEpoch) return;
|
|
308
|
+
set({
|
|
309
|
+
selectedPath: refreshed ? get().selectedPath : null,
|
|
310
|
+
sessionSwitching: false,
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
} else {
|
|
314
|
+
set(resetLivePatch());
|
|
315
|
+
}
|
|
316
|
+
} else if (event.type === "prompt_accepted") {
|
|
317
|
+
const settled = terminalPromptIds.has(String(detail.commandId ?? ""));
|
|
318
|
+
set({
|
|
319
|
+
...promptAcceptedLivePatch(settled, current.livePhase),
|
|
320
|
+
liveRetry: null,
|
|
321
|
+
pendingFollowUpsReceipt: Number.isInteger(detail.pendingFollowUps)
|
|
322
|
+
? Number(detail.pendingFollowUps)
|
|
323
|
+
: current.pendingFollowUpsReceipt,
|
|
324
|
+
});
|
|
325
|
+
} else if (event.type === "turn_started") {
|
|
326
|
+
set({
|
|
327
|
+
activeTurn: {
|
|
328
|
+
sessionId: String(detail.sessionId),
|
|
329
|
+
commandId: String(detail.commandId),
|
|
330
|
+
epoch: Number(detail.epoch),
|
|
331
|
+
},
|
|
332
|
+
liveRunning: true,
|
|
333
|
+
livePhase: "running",
|
|
334
|
+
liveRetry: null,
|
|
335
|
+
turnTerminalStatus: null,
|
|
336
|
+
});
|
|
337
|
+
} else if (event.type === "agent_start") {
|
|
338
|
+
set({
|
|
339
|
+
...(detail.activeTurn
|
|
340
|
+
? { activeTurn: detail.activeTurn as WebStoreState["activeTurn"] }
|
|
341
|
+
: {}),
|
|
342
|
+
liveRunning: true,
|
|
343
|
+
livePhase: "running",
|
|
344
|
+
liveRetry: null,
|
|
345
|
+
});
|
|
346
|
+
} else if (event.type === "turn_settled") {
|
|
347
|
+
rememberBounded(terminalPromptIds, detail.commandId);
|
|
348
|
+
const turn = current.activeTurn;
|
|
349
|
+
if (
|
|
350
|
+
turn?.sessionId === detail.sessionId &&
|
|
351
|
+
turn?.commandId === detail.commandId &&
|
|
352
|
+
turn?.epoch === detail.epoch
|
|
353
|
+
) {
|
|
354
|
+
set({
|
|
355
|
+
activeTurn: null,
|
|
356
|
+
liveRunning: false,
|
|
357
|
+
livePhase: "idle",
|
|
358
|
+
liveRetry: null,
|
|
359
|
+
turnTerminalStatus:
|
|
360
|
+
typeof detail.outcome === "string" ? detail.outcome : null,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
} else if (event.type === "agent_settled") {
|
|
364
|
+
set({
|
|
365
|
+
pendingFollowUpsReceipt: null,
|
|
366
|
+
...(!current.activeTurn
|
|
367
|
+
? {
|
|
368
|
+
liveRunning: false,
|
|
369
|
+
livePhase: "idle" as const,
|
|
370
|
+
liveRetry: null,
|
|
371
|
+
}
|
|
372
|
+
: {}),
|
|
373
|
+
});
|
|
374
|
+
} else if (event.type === "prompt_settled") {
|
|
375
|
+
rememberBounded(terminalPromptIds, detail.commandId);
|
|
376
|
+
if (current.livePhase !== "running")
|
|
377
|
+
set({ liveRunning: false, livePhase: "idle", liveRetry: null });
|
|
378
|
+
} else if (detail.message && typeof detail.message === "object") {
|
|
379
|
+
const message = detail.message as WebLiveMessage;
|
|
380
|
+
let liveMessages = current.liveMessages;
|
|
381
|
+
if (message.role === "user") {
|
|
382
|
+
liveMessages = liveMessages.filter(
|
|
383
|
+
(entry) =>
|
|
384
|
+
!entry.key.startsWith("optimistic-") ||
|
|
385
|
+
entry.message.content !== message.content,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
const key =
|
|
389
|
+
typeof detail.messageKey === "string"
|
|
390
|
+
? detail.messageKey
|
|
391
|
+
: `${message.role || "message"}-${event.sequence}`;
|
|
392
|
+
const live = { key, message };
|
|
393
|
+
const index = liveMessages.findIndex((entry) => entry.key === key);
|
|
394
|
+
liveMessages =
|
|
395
|
+
index >= 0
|
|
396
|
+
? liveMessages.map((entry, entryIndex) =>
|
|
397
|
+
entryIndex === index ? live : entry,
|
|
398
|
+
)
|
|
399
|
+
: [...liveMessages, live].slice(-8);
|
|
400
|
+
const thinkingStarts = { ...current.thinkingStarts };
|
|
401
|
+
const thinkingDurations = { ...current.thinkingDurations };
|
|
402
|
+
if (message.parts?.some((part) => part.type === "thinking")) {
|
|
403
|
+
thinkingStarts[key] ??= Date.now();
|
|
404
|
+
if (event.type === "message_end") {
|
|
405
|
+
thinkingDurations[key] = Date.now() - thinkingStarts[key];
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
set({ liveMessages, thinkingDurations, thinkingStarts });
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (event.type === "prompt_failed") {
|
|
412
|
+
rememberBounded(terminalPromptIds, detail.commandId);
|
|
413
|
+
set({
|
|
414
|
+
liveMessages: get().liveMessages.filter(
|
|
415
|
+
(entry) => !entry.key.startsWith("optimistic-"),
|
|
416
|
+
),
|
|
417
|
+
liveRunning: false,
|
|
418
|
+
livePhase: "idle",
|
|
419
|
+
liveRetry: null,
|
|
420
|
+
notice:
|
|
421
|
+
typeof detail.error === "string" ? detail.error : "Prompt failed",
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
if (event.type === "auto_retry_start") {
|
|
425
|
+
set({
|
|
426
|
+
liveRunning: true,
|
|
427
|
+
livePhase: "running",
|
|
428
|
+
liveRetry: {
|
|
429
|
+
attempt: Number(detail.attempt) || 0,
|
|
430
|
+
maxAttempts: Number(detail.maxAttempts) || 0,
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
if (refreshEventTypes.has(event.type)) scheduleSnapshotRefresh();
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const runEventLoop = async (signal: AbortSignal) => {
|
|
438
|
+
let reconnectDelay = 500;
|
|
439
|
+
while (!signal.aborted) {
|
|
440
|
+
let recoveryAttempted = false;
|
|
441
|
+
try {
|
|
442
|
+
if (get().cursor === null) {
|
|
443
|
+
recoveryAttempted = true;
|
|
444
|
+
const ready = await get().actions.refreshSnapshot({
|
|
445
|
+
resetCursor: true,
|
|
446
|
+
});
|
|
447
|
+
if (!ready) throw new Error("snapshot unavailable");
|
|
448
|
+
recoveryAttempted = false;
|
|
449
|
+
}
|
|
450
|
+
if (signal.aborted) return;
|
|
451
|
+
await consumeEvents({
|
|
452
|
+
client,
|
|
453
|
+
cursor: get().cursor ?? 0,
|
|
454
|
+
onConnected: () => {
|
|
455
|
+
reconnectDelay = 500;
|
|
456
|
+
set({ connection: "connected", notice: null });
|
|
457
|
+
},
|
|
458
|
+
onEvent: applyRuntimeEvent,
|
|
459
|
+
onHeartbeat: () => scheduleSnapshotRefresh(0),
|
|
460
|
+
signal,
|
|
461
|
+
});
|
|
462
|
+
} catch (error) {
|
|
463
|
+
if (signal.aborted) return;
|
|
464
|
+
set({ connection: "reconnecting" });
|
|
465
|
+
const recovered =
|
|
466
|
+
!recoveryAttempted &&
|
|
467
|
+
(await get().actions.refreshSnapshot({ resetCursor: true }));
|
|
468
|
+
if (!recovered) set(resetLivePatch());
|
|
469
|
+
await waitForReconnect(reconnectDelay, signal);
|
|
470
|
+
reconnectDelay = Math.min(reconnectDelay * 2, 5_000);
|
|
471
|
+
if (error instanceof SyntaxError)
|
|
472
|
+
set({ notice: "Invalid event data" });
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const actions: WebStoreActions = {
|
|
478
|
+
start() {
|
|
479
|
+
if (streamController) return;
|
|
480
|
+
streamController = new AbortController();
|
|
481
|
+
void runEventLoop(streamController.signal);
|
|
482
|
+
},
|
|
483
|
+
stop() {
|
|
484
|
+
streamController?.abort();
|
|
485
|
+
streamController = null;
|
|
486
|
+
if (refreshTimer !== null) window.clearTimeout(refreshTimer);
|
|
487
|
+
refreshTimer = null;
|
|
488
|
+
},
|
|
489
|
+
async refreshSnapshot(options = {}) {
|
|
490
|
+
const epoch = options.epoch ?? sessionEpoch;
|
|
491
|
+
const generation = ++snapshotGeneration;
|
|
492
|
+
const requestedPath = get().selectedPath;
|
|
493
|
+
try {
|
|
494
|
+
const snapshot = await client.snapshot(requestedPath);
|
|
495
|
+
if (epoch !== sessionEpoch || generation !== snapshotGeneration)
|
|
496
|
+
return false;
|
|
497
|
+
const hasCurrent = typeof snapshot.currentSessionId === "string";
|
|
498
|
+
const currentSession = hasCurrent
|
|
499
|
+
? snapshot.sessions.find(
|
|
500
|
+
(session) => session.id === snapshot.currentSessionId,
|
|
501
|
+
)
|
|
502
|
+
: undefined;
|
|
503
|
+
const selectedIsCurrent = hasCurrent
|
|
504
|
+
? snapshot.selectedSession?.id === snapshot.currentSessionId
|
|
505
|
+
: snapshot.selectedSession === undefined;
|
|
506
|
+
const requestedExists =
|
|
507
|
+
!requestedPath ||
|
|
508
|
+
snapshot.sessions.some((session) => session.path === requestedPath);
|
|
509
|
+
const requestedMatches =
|
|
510
|
+
!requestedPath || snapshot.selectedSession?.path === requestedPath;
|
|
511
|
+
if (!requestedExists || !requestedMatches || !selectedIsCurrent) {
|
|
512
|
+
set({ selectedPath: null });
|
|
513
|
+
if (!options.canonicalRetry) {
|
|
514
|
+
return actions.refreshSnapshot({
|
|
515
|
+
...options,
|
|
516
|
+
canonicalRetry: true,
|
|
517
|
+
epoch,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
const selectedSessionWorkspace = snapshot.selectedSession?.cwd;
|
|
523
|
+
const activeWorkspace = snapshot.workspaces.find(
|
|
524
|
+
(workspace) => workspace.current,
|
|
525
|
+
)?.path;
|
|
526
|
+
const retainedWorkspace = snapshot.workspaces.some(
|
|
527
|
+
(workspace) => workspace.path === get().selectedWorkspace,
|
|
528
|
+
)
|
|
529
|
+
? get().selectedWorkspace
|
|
530
|
+
: undefined;
|
|
531
|
+
const selectedWorkspace = snapshot.workspaces.some(
|
|
532
|
+
(workspace) => workspace.path === selectedSessionWorkspace,
|
|
533
|
+
)
|
|
534
|
+
? selectedSessionWorkspace
|
|
535
|
+
: (activeWorkspace ?? retainedWorkspace ?? null);
|
|
536
|
+
const shouldReset = options.resetCursor;
|
|
537
|
+
set({
|
|
538
|
+
...(shouldReset ? resetLivePatch() : {}),
|
|
539
|
+
connection:
|
|
540
|
+
get().connection === "connecting"
|
|
541
|
+
? "connecting"
|
|
542
|
+
: get().connection,
|
|
543
|
+
cursor:
|
|
544
|
+
shouldReset || get().cursor === null
|
|
545
|
+
? snapshot.cursor
|
|
546
|
+
: Math.max(get().cursor ?? 0, snapshot.cursor),
|
|
547
|
+
activeTurn: snapshot.runtime.activeTurn ?? null,
|
|
548
|
+
livePhase:
|
|
549
|
+
snapshot.runtime.status !== "running" &&
|
|
550
|
+
!get().promptAdmissionPending &&
|
|
551
|
+
!promptAdmission
|
|
552
|
+
? "idle"
|
|
553
|
+
: get().livePhase,
|
|
554
|
+
liveRetry:
|
|
555
|
+
snapshot.runtime.status !== "running" &&
|
|
556
|
+
!get().promptAdmissionPending &&
|
|
557
|
+
!promptAdmission
|
|
558
|
+
? null
|
|
559
|
+
: get().liveRetry,
|
|
560
|
+
liveRunning:
|
|
561
|
+
snapshot.runtime.status === "running"
|
|
562
|
+
? true
|
|
563
|
+
: !get().promptAdmissionPending && !promptAdmission
|
|
564
|
+
? false
|
|
565
|
+
: get().liveRunning,
|
|
566
|
+
selectedPath:
|
|
567
|
+
currentSession?.path ?? snapshot.selectedSession?.path ?? null,
|
|
568
|
+
selectedWorkspace,
|
|
569
|
+
snapshot,
|
|
570
|
+
});
|
|
571
|
+
return true;
|
|
572
|
+
} catch (error) {
|
|
573
|
+
if (epoch !== sessionEpoch || generation !== snapshotGeneration)
|
|
574
|
+
return false;
|
|
575
|
+
set({ connection: "unavailable" });
|
|
576
|
+
showError(error);
|
|
577
|
+
return false;
|
|
578
|
+
}
|
|
579
|
+
},
|
|
580
|
+
async chooseWorkspace() {
|
|
581
|
+
try {
|
|
582
|
+
const result = await client.chooseWorkspace();
|
|
583
|
+
if (result.cancelled || !result.path) return;
|
|
584
|
+
set({ selectedWorkspace: result.path });
|
|
585
|
+
await actions.refreshSnapshot();
|
|
586
|
+
} catch (error) {
|
|
587
|
+
showError(error);
|
|
588
|
+
}
|
|
589
|
+
},
|
|
590
|
+
setWorkspace(path) {
|
|
591
|
+
set({ selectedWorkspace: path });
|
|
592
|
+
void actions.refreshSnapshot();
|
|
593
|
+
},
|
|
594
|
+
async renameWorkspace(path, name) {
|
|
595
|
+
try {
|
|
596
|
+
await client.renameWorkspace(path, name);
|
|
597
|
+
await actions.refreshSnapshot();
|
|
598
|
+
} catch (error) {
|
|
599
|
+
showError(error);
|
|
600
|
+
throw error;
|
|
601
|
+
}
|
|
602
|
+
},
|
|
603
|
+
async removeWorkspace(path) {
|
|
604
|
+
try {
|
|
605
|
+
await client.removeWorkspace(path);
|
|
606
|
+
set({
|
|
607
|
+
selectedPath: null,
|
|
608
|
+
selectedWorkspace:
|
|
609
|
+
get().selectedWorkspace === path ? null : get().selectedWorkspace,
|
|
610
|
+
});
|
|
611
|
+
await actions.refreshSnapshot();
|
|
612
|
+
} catch (error) {
|
|
613
|
+
showError(error);
|
|
614
|
+
}
|
|
615
|
+
},
|
|
616
|
+
async createSession(workspacePath) {
|
|
617
|
+
if (!workspacePath) return;
|
|
618
|
+
const epoch = ++sessionEpoch;
|
|
619
|
+
const commandId =
|
|
620
|
+
globalThis.crypto?.randomUUID?.() ??
|
|
621
|
+
`web-create-${Date.now()}-${epoch}`;
|
|
622
|
+
promptAdmissionToken = null;
|
|
623
|
+
promptAdmission = null;
|
|
624
|
+
set({
|
|
625
|
+
...resetLivePatch(),
|
|
626
|
+
mobileSidebarOpen: false,
|
|
627
|
+
promptAdmissionPending: false,
|
|
628
|
+
selectedPath: null,
|
|
629
|
+
selectedWorkspace: workspacePath,
|
|
630
|
+
sessionSwitching: true,
|
|
631
|
+
});
|
|
632
|
+
const creation = sessionSelectionTail.then(async () => {
|
|
633
|
+
if (epoch !== sessionEpoch) return;
|
|
634
|
+
sessionActivation = {
|
|
635
|
+
commandId,
|
|
636
|
+
epoch,
|
|
637
|
+
expectedPath: null,
|
|
638
|
+
kind: "create",
|
|
639
|
+
observedPath: null,
|
|
640
|
+
};
|
|
641
|
+
try {
|
|
642
|
+
await client.createSession(workspacePath, commandId);
|
|
643
|
+
if (epoch !== sessionEpoch) return;
|
|
644
|
+
set({ selectedPath: null });
|
|
645
|
+
await actions.refreshSnapshot({ epoch });
|
|
646
|
+
} catch (error) {
|
|
647
|
+
if (epoch !== sessionEpoch) return;
|
|
648
|
+
set({ selectedPath: null });
|
|
649
|
+
showError(error);
|
|
650
|
+
await actions.refreshSnapshot({ epoch });
|
|
651
|
+
} finally {
|
|
652
|
+
rememberBounded(completedActivationIds, commandId);
|
|
653
|
+
if (sessionActivation?.epoch === epoch) sessionActivation = null;
|
|
654
|
+
if (epoch === sessionEpoch) set({ sessionSwitching: false });
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
sessionSelectionTail = creation.catch(() => undefined);
|
|
658
|
+
await creation;
|
|
659
|
+
},
|
|
660
|
+
async selectSession(path) {
|
|
661
|
+
if (!path) return;
|
|
662
|
+
const epoch = ++sessionEpoch;
|
|
663
|
+
promptAdmissionToken = null;
|
|
664
|
+
promptAdmission = null;
|
|
665
|
+
set({
|
|
666
|
+
...resetLivePatch(),
|
|
667
|
+
mobileSidebarOpen: false,
|
|
668
|
+
promptAdmissionPending: false,
|
|
669
|
+
selectedPath: path,
|
|
670
|
+
sessionSwitching: true,
|
|
671
|
+
});
|
|
672
|
+
const selection = sessionSelectionTail.then(async () => {
|
|
673
|
+
if (epoch !== sessionEpoch) return;
|
|
674
|
+
sessionActivation = { epoch, expectedPath: path, kind: "select" };
|
|
675
|
+
try {
|
|
676
|
+
await client.selectSession(path);
|
|
677
|
+
if (epoch !== sessionEpoch) return;
|
|
678
|
+
if (!(await actions.refreshSnapshot({ epoch }))) {
|
|
679
|
+
set({ selectedPath: null });
|
|
680
|
+
}
|
|
681
|
+
} catch (error) {
|
|
682
|
+
if (epoch !== sessionEpoch) return;
|
|
683
|
+
set({ selectedPath: null });
|
|
684
|
+
showError(error);
|
|
685
|
+
await actions.refreshSnapshot({ epoch });
|
|
686
|
+
} finally {
|
|
687
|
+
if (sessionActivation?.epoch === epoch) sessionActivation = null;
|
|
688
|
+
if (epoch === sessionEpoch) set({ sessionSwitching: false });
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
sessionSelectionTail = selection.catch(() => undefined);
|
|
692
|
+
await selection;
|
|
693
|
+
},
|
|
694
|
+
async renameSession(path, name) {
|
|
695
|
+
try {
|
|
696
|
+
await client.renameSession(path, name);
|
|
697
|
+
await actions.refreshSnapshot();
|
|
698
|
+
} catch (error) {
|
|
699
|
+
showError(error);
|
|
700
|
+
throw error;
|
|
701
|
+
}
|
|
702
|
+
},
|
|
703
|
+
async archiveSession(path) {
|
|
704
|
+
try {
|
|
705
|
+
await client.archiveSession(path);
|
|
706
|
+
await actions.refreshSnapshot();
|
|
707
|
+
} catch (error) {
|
|
708
|
+
showError(error);
|
|
709
|
+
}
|
|
710
|
+
},
|
|
711
|
+
async selectModel(value) {
|
|
712
|
+
const [provider, ...idParts] = value.split("/");
|
|
713
|
+
const modelId = idParts.join("/");
|
|
714
|
+
const epoch = sessionEpoch;
|
|
715
|
+
const sessionId = get().snapshot?.selectedSession?.id;
|
|
716
|
+
if (!provider || !modelId || !sessionId || get().sessionSwitching)
|
|
717
|
+
return;
|
|
718
|
+
try {
|
|
719
|
+
await client.selectModel(provider, modelId, sessionId);
|
|
720
|
+
if (
|
|
721
|
+
epoch !== sessionEpoch ||
|
|
722
|
+
sessionId !== get().snapshot?.selectedSession?.id
|
|
723
|
+
) {
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
await actions.refreshSnapshot({ epoch });
|
|
727
|
+
} catch (error) {
|
|
728
|
+
if (
|
|
729
|
+
epoch !== sessionEpoch ||
|
|
730
|
+
sessionId !== get().snapshot?.selectedSession?.id
|
|
731
|
+
) {
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
showError(error);
|
|
735
|
+
await actions.refreshSnapshot({ epoch });
|
|
736
|
+
}
|
|
737
|
+
},
|
|
738
|
+
async cancelActiveTurn() {
|
|
739
|
+
const turn = get().activeTurn ?? get().snapshot?.runtime.activeTurn;
|
|
740
|
+
if (!turn || get().turnCancellationPending || get().sessionSwitching)
|
|
741
|
+
return;
|
|
742
|
+
const epoch = sessionEpoch;
|
|
743
|
+
set({ turnCancellationPending: true });
|
|
744
|
+
try {
|
|
745
|
+
await client.cancelActiveTurn(turn);
|
|
746
|
+
} catch (error) {
|
|
747
|
+
if (epoch !== sessionEpoch) return;
|
|
748
|
+
await actions.refreshSnapshot({ epoch });
|
|
749
|
+
if (epoch === sessionEpoch) showError(error);
|
|
750
|
+
} finally {
|
|
751
|
+
if (epoch === sessionEpoch) set({ turnCancellationPending: false });
|
|
752
|
+
}
|
|
753
|
+
},
|
|
754
|
+
async sendPrompt(rawContent) {
|
|
755
|
+
const content = rawContent.trim();
|
|
756
|
+
const workspace = get().selectedWorkspace;
|
|
757
|
+
if (
|
|
758
|
+
!workspace ||
|
|
759
|
+
!content ||
|
|
760
|
+
get().sessionSwitching ||
|
|
761
|
+
get().promptAdmissionPending
|
|
762
|
+
) {
|
|
763
|
+
return false;
|
|
764
|
+
}
|
|
765
|
+
if (!get().snapshot?.selectedSession?.id)
|
|
766
|
+
await actions.createSession(workspace);
|
|
767
|
+
const sessionId = get().snapshot?.selectedSession?.id;
|
|
768
|
+
if (
|
|
769
|
+
!sessionId ||
|
|
770
|
+
get().sessionSwitching ||
|
|
771
|
+
get().promptAdmissionPending
|
|
772
|
+
) {
|
|
773
|
+
return false;
|
|
774
|
+
}
|
|
775
|
+
const epoch = sessionEpoch;
|
|
776
|
+
const admission = ++promptAdmissionSequence;
|
|
777
|
+
const retrying =
|
|
778
|
+
promptAdmission?.sessionId === sessionId &&
|
|
779
|
+
promptAdmission.content === content;
|
|
780
|
+
const commandId = retrying
|
|
781
|
+
? promptAdmission!.commandId
|
|
782
|
+
: (globalThis.crypto?.randomUUID?.() ??
|
|
783
|
+
`web-prompt-${Date.now()}-${admission}`);
|
|
784
|
+
const optimisticKey = retrying
|
|
785
|
+
? promptAdmission!.optimisticKey
|
|
786
|
+
: `optimistic-${commandId}`;
|
|
787
|
+
promptAdmission = { sessionId, content, commandId, optimisticKey };
|
|
788
|
+
promptAdmissionToken = admission;
|
|
789
|
+
set({
|
|
790
|
+
liveMessages: retrying
|
|
791
|
+
? get().liveMessages
|
|
792
|
+
: [
|
|
793
|
+
...get().liveMessages,
|
|
794
|
+
{ key: optimisticKey, message: { role: "user", content } },
|
|
795
|
+
].slice(-8),
|
|
796
|
+
notice: null,
|
|
797
|
+
pendingFollowUpsReceipt: null,
|
|
798
|
+
turnTerminalStatus: null,
|
|
799
|
+
promptAdmissionPending: true,
|
|
800
|
+
scrollToBottom: get().scrollToBottom + 1,
|
|
801
|
+
});
|
|
802
|
+
try {
|
|
803
|
+
const receipt = await client.prompt(
|
|
804
|
+
sessionId,
|
|
805
|
+
content,
|
|
806
|
+
commandId,
|
|
807
|
+
retrying,
|
|
808
|
+
);
|
|
809
|
+
if (epoch !== sessionEpoch || promptAdmissionToken !== admission)
|
|
810
|
+
return false;
|
|
811
|
+
const settled = terminalPromptIds.has(receipt.id);
|
|
812
|
+
if (promptAdmission?.commandId === commandId) promptAdmission = null;
|
|
813
|
+
set({
|
|
814
|
+
...promptAcceptedLivePatch(settled, get().livePhase),
|
|
815
|
+
pendingFollowUpsReceipt: receipt.pendingFollowUps ?? null,
|
|
816
|
+
});
|
|
817
|
+
scheduleSnapshotRefresh(120);
|
|
818
|
+
return true;
|
|
819
|
+
} catch (error) {
|
|
820
|
+
if (epoch !== sessionEpoch || promptAdmissionToken !== admission)
|
|
821
|
+
return false;
|
|
822
|
+
const knownRejection =
|
|
823
|
+
error instanceof WebApiError &&
|
|
824
|
+
[
|
|
825
|
+
"WORKSPACE_REQUIRED",
|
|
826
|
+
"SESSION_CONFLICT",
|
|
827
|
+
"PROMPT_REJECTED",
|
|
828
|
+
"COMMAND_CONFLICT",
|
|
829
|
+
"PROMPT_ADMISSION_CAPACITY",
|
|
830
|
+
].includes(error.code ?? "");
|
|
831
|
+
if (!knownRejection) {
|
|
832
|
+
set({
|
|
833
|
+
liveRunning: true,
|
|
834
|
+
livePhase:
|
|
835
|
+
get().livePhase === "running" ? "running" : "preparing",
|
|
836
|
+
liveRetry: null,
|
|
837
|
+
});
|
|
838
|
+
showError(error);
|
|
839
|
+
return false;
|
|
840
|
+
}
|
|
841
|
+
if (promptAdmission?.commandId === commandId) promptAdmission = null;
|
|
842
|
+
set({
|
|
843
|
+
liveMessages: get().liveMessages.filter(
|
|
844
|
+
(entry) => entry.key !== optimisticKey,
|
|
845
|
+
),
|
|
846
|
+
livePhase: "idle",
|
|
847
|
+
liveRetry: null,
|
|
848
|
+
liveRunning: false,
|
|
849
|
+
});
|
|
850
|
+
showError(error);
|
|
851
|
+
return false;
|
|
852
|
+
} finally {
|
|
853
|
+
if (epoch === sessionEpoch && promptAdmissionToken === admission) {
|
|
854
|
+
promptAdmissionToken = null;
|
|
855
|
+
set({ promptAdmissionPending: false });
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
},
|
|
859
|
+
setQuery(query) {
|
|
860
|
+
set({ query });
|
|
861
|
+
},
|
|
862
|
+
setSearchOpen(open) {
|
|
863
|
+
set({ searchOpen: open, ...(open ? {} : { query: "" }) });
|
|
864
|
+
},
|
|
865
|
+
toggleWorkspace(path) {
|
|
866
|
+
const collapsed = new Set(get().collapsed);
|
|
867
|
+
if (collapsed.has(path)) collapsed.delete(path);
|
|
868
|
+
else collapsed.add(path);
|
|
869
|
+
persist(collapsedWorkspacesStorageKey, [...collapsed]);
|
|
870
|
+
set({ collapsed });
|
|
871
|
+
},
|
|
872
|
+
toggleSidebar(narrow) {
|
|
873
|
+
if (narrow) {
|
|
874
|
+
set({ mobileSidebarOpen: !get().mobileSidebarOpen });
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
const sidebarCollapsed = !get().sidebarCollapsed;
|
|
878
|
+
try {
|
|
879
|
+
window.sessionStorage.setItem(
|
|
880
|
+
sidebarCollapsedStorageKey,
|
|
881
|
+
String(sidebarCollapsed),
|
|
882
|
+
);
|
|
883
|
+
} catch {}
|
|
884
|
+
set({ sidebarCollapsed });
|
|
885
|
+
},
|
|
886
|
+
closeMobileSidebar() {
|
|
887
|
+
set({ mobileSidebarOpen: false });
|
|
888
|
+
},
|
|
889
|
+
clearNotice() {
|
|
890
|
+
set({ notice: null });
|
|
891
|
+
},
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
return {
|
|
895
|
+
activeTurn: null,
|
|
896
|
+
turnCancellationPending: false,
|
|
897
|
+
turnTerminalStatus: null,
|
|
898
|
+
pendingFollowUpsReceipt: null,
|
|
899
|
+
snapshot: null,
|
|
900
|
+
cursor: null,
|
|
901
|
+
selectedPath: null,
|
|
902
|
+
selectedWorkspace: null,
|
|
903
|
+
collapsed: readStringSet(collapsedWorkspacesStorageKey),
|
|
904
|
+
sidebarCollapsed: readBoolean(sidebarCollapsedStorageKey),
|
|
905
|
+
mobileSidebarOpen: false,
|
|
906
|
+
query: "",
|
|
907
|
+
searchOpen: false,
|
|
908
|
+
connection: "connecting",
|
|
909
|
+
notice: null,
|
|
910
|
+
liveMessages: [],
|
|
911
|
+
liveRunning: false,
|
|
912
|
+
livePhase: "idle",
|
|
913
|
+
liveRetry: null,
|
|
914
|
+
thinkingStarts: {},
|
|
915
|
+
thinkingDurations: {},
|
|
916
|
+
promptAdmissionPending: false,
|
|
917
|
+
sessionSwitching: false,
|
|
918
|
+
scrollToBottom: 0,
|
|
919
|
+
actions,
|
|
920
|
+
};
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
return store;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
export const webStore = createWebStore();
|