@hienlh/ppm 0.17.54 → 0.17.56
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/CHANGELOG.md +13 -0
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/http-api.md +1 -1
- package/dist/web/assets/{ai-resource-editor-Bc1JjqQ2.js → ai-resource-editor-DL011EDu.js} +1 -1
- package/dist/web/assets/{audio-preview-DkMX9OJj.js → audio-preview-H16VO99g.js} +1 -1
- package/dist/web/assets/{chat-tab-DbLlGvwV.js → chat-tab-D2nZCpAo.js} +3 -3
- package/dist/web/assets/{code-editor-Dlzl3dNT.js → code-editor-DqQy2Qm8.js} +2 -2
- package/dist/web/assets/{conflict-editor-DUHhHqpE.js → conflict-editor-B_wAb0Kb.js} +1 -1
- package/dist/web/assets/{csv-preview-CS_r0b6o.js → csv-preview-BL0I3CKz.js} +1 -1
- package/dist/web/assets/{database-viewer-CVYeQeUc.js → database-viewer-awmnuDCt.js} +1 -1
- package/dist/web/assets/{diff-viewer-CCrzns7w.js → diff-viewer-_yQtutHR.js} +1 -1
- package/dist/web/assets/{docx-preview-BLQB-hLE.js → docx-preview-DDw3duw9.js} +1 -1
- package/dist/web/assets/{extension-webview-z-UAr801.js → extension-webview-DkTI51P3.js} +1 -1
- package/dist/web/assets/{git-log-panel-cfFgTJhr.js → git-log-panel-NkghB_rO.js} +1 -1
- package/dist/web/assets/{glide-data-grid-DLI4he-v.js → glide-data-grid-Bpgp72Hg.js} +1 -1
- package/dist/web/assets/{group-chat-tab-C2AhbwL2.js → group-chat-tab-CQOVBmVp.js} +1 -1
- package/dist/web/assets/{image-preview-B8al9rrY.js → image-preview-Dwg0EuJ2.js} +1 -1
- package/dist/web/assets/{index-DlzNfQJh.js → index-CJ0Wp_rW.js} +16 -16
- package/dist/web/assets/{keybindings-store-DtAKiCMw.js → keybindings-store-ClNYe9vD.js} +1 -1
- package/dist/web/assets/{markdown-renderer-DHnbHEFr.js → markdown-renderer-C7dlPs2S.js} +1 -1
- package/dist/web/assets/{markdown-renderer-CNSv3Olb.js → markdown-renderer-CwpP5Ola.js} +1 -1
- package/dist/web/assets/{notification-store-DGrzDj7x.js → notification-store-DwAhNzTy.js} +1 -1
- package/dist/web/assets/{pdf-preview-P8Ar0W1A.js → pdf-preview-CEWYMxdp.js} +1 -1
- package/dist/web/assets/{postgres-viewer-CqjPnnlc.js → postgres-viewer-D7PggslU.js} +1 -1
- package/dist/web/assets/{settings-tab-LZPNBhSb.js → settings-tab-CeSJQIfF.js} +1 -1
- package/dist/web/assets/{sql-query-editor-BpDMqI2d.js → sql-query-editor-ZnKcxCyj.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-D6sC2hw7.js → sqlite-viewer-8fk_wut8.js} +1 -1
- package/dist/web/assets/{system-monitor-tab-C3Ef8jqO.js → system-monitor-tab-Ck3REHUg.js} +1 -1
- package/dist/web/assets/{terminal-tab-BR-71TAe.js → terminal-tab-D6tYDFSF.js} +1 -1
- package/dist/web/assets/{tool-cards-Bmr07Non.js → tool-cards-C3ka1Y7I.js} +2 -2
- package/dist/web/assets/{use-monaco-theme-DPVuNCPp.js → use-monaco-theme-wKbyNwIn.js} +1 -1
- package/dist/web/assets/{video-preview-BwnO8msG.js → video-preview-DwrfenNA.js} +1 -1
- package/dist/web/index.html +1 -1
- package/dist/web/monacoeditorwork/css.worker.bundle.js +122 -122
- package/dist/web/monacoeditorwork/editor.worker.bundle.js +78 -78
- package/dist/web/monacoeditorwork/html.worker.bundle.js +110 -110
- package/dist/web/monacoeditorwork/json.worker.bundle.js +108 -108
- package/dist/web/monacoeditorwork/ts.worker.bundle.js +81 -81
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/src/providers/mock-provider.ts +10 -0
- package/src/server/index.ts +41 -15
- package/src/server/routes/upgrade.ts +23 -5
- package/src/server/ws/chat.ts +8 -1
- package/src/services/autostart-generator.ts +24 -0
- package/src/services/autostart-register.ts +33 -4
- package/src/services/upgrade.service.ts +13 -3
- package/src/web/app.tsx +1 -16
- package/src/web/components/layout/upgrade-button.tsx +70 -33
- package/src/web/hooks/use-chat.ts +3 -2
- package/src/web/hooks/use-global-events.ts +8 -2
- package/src/web/lib/changelog.ts +13 -0
- package/src/web/lib/sync-running-sessions.ts +27 -0
- package/src/web/stores/streaming-store.ts +29 -7
- package/.rollout-mark +0 -2
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback } from "react";
|
|
1
|
+
import { useState, useEffect, useCallback, useMemo } from "react";
|
|
2
2
|
import { api } from "@/lib/api-client";
|
|
3
3
|
import { toast } from "sonner";
|
|
4
4
|
import { Loader2, ArrowUpCircle, RefreshCw, Download, ExternalLink, History } from "lucide-react";
|
|
5
5
|
import { useSettingsStore } from "@/stores/settings-store";
|
|
6
|
-
import {
|
|
6
|
+
import { fetchRecentChangelog, newestSectionVersion, compareSemver, type ChangelogSection } from "@/lib/changelog";
|
|
7
7
|
import { cn } from "@/lib/utils";
|
|
8
8
|
|
|
9
9
|
const POLL_INTERVAL_MS = 60_000;
|
|
@@ -113,26 +113,36 @@ export function UpgradeButton({ align = "right" }: { align?: "left" | "right" })
|
|
|
113
113
|
const [notes, setNotes] = useState<ChangelogSection[] | null>(null);
|
|
114
114
|
const [notesLoading, setNotesLoading] = useState(false);
|
|
115
115
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
// ignore — chip just shows the known version
|
|
116
|
+
// `force` asks the server to bypass its registry cache. The background poll
|
|
117
|
+
// stays cached (one request per client per minute must not hit the registry),
|
|
118
|
+
// but a user opening the popover is asking "is there one *now*" and must not
|
|
119
|
+
// be answered from an answer taken up to five minutes ago.
|
|
120
|
+
const check = useCallback(async (force = false) => {
|
|
121
|
+
try {
|
|
122
|
+
const data = await api.get<UpgradeStatus>(force ? "/api/upgrade?refresh=1" : "/api/upgrade");
|
|
123
|
+
setCurrentVersion(data.currentVersion);
|
|
124
|
+
if (data.availableVersion) {
|
|
125
|
+
setAvailableVersion(data.availableVersion);
|
|
126
|
+
setDismissed(!!sessionStorage.getItem(DISMISS_KEY_PREFIX + data.availableVersion));
|
|
127
|
+
} else {
|
|
128
|
+
setAvailableVersion(null);
|
|
130
129
|
}
|
|
130
|
+
} catch {
|
|
131
|
+
// ignore — chip just shows the known version
|
|
131
132
|
}
|
|
133
|
+
}, []);
|
|
134
|
+
|
|
135
|
+
useEffect(() => {
|
|
132
136
|
check();
|
|
133
|
-
timer = setInterval(check, POLL_INTERVAL_MS);
|
|
137
|
+
const timer = setInterval(() => check(), POLL_INTERVAL_MS);
|
|
134
138
|
return () => clearInterval(timer);
|
|
135
|
-
}, []);
|
|
139
|
+
}, [check]);
|
|
140
|
+
|
|
141
|
+
// Opening the popover re-checks immediately, so the panel never disagrees
|
|
142
|
+
// with itself: release notes and the Update button are resolved together.
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
if (open) check(true);
|
|
145
|
+
}, [open, check]);
|
|
136
146
|
|
|
137
147
|
const current = currentVersion ?? version;
|
|
138
148
|
// QA hook (latched in sessionStorage — see getTestTarget). Forces the update
|
|
@@ -145,25 +155,44 @@ export function UpgradeButton({ align = "right" }: { align?: "left" | "right" })
|
|
|
145
155
|
const realUpdate = !!availableVersion && !!current && compareSemver(availableVersion, current) > 0
|
|
146
156
|
? availableVersion
|
|
147
157
|
: null;
|
|
148
|
-
const effectiveAvailable = realUpdate ?? (isTest ? testTarget : null);
|
|
149
|
-
const hasUpdate = !!effectiveAvailable && !dismissed;
|
|
150
158
|
|
|
151
159
|
// Load release notes each time the popover opens. Refetching (not caching for
|
|
152
160
|
// the session) avoids latching a stale/empty result from a transient fetch:
|
|
153
161
|
// the changelog source can briefly lag the version signal after a release.
|
|
162
|
+
// Always the recent list, unfiltered — the sections themselves are what tell
|
|
163
|
+
// us whether a newer release exists, so filtering here would hide the signal.
|
|
154
164
|
useEffect(() => {
|
|
155
165
|
if (!open || !current) return;
|
|
156
166
|
setNotesLoading(true);
|
|
157
|
-
|
|
158
|
-
// No update → recent releases so clicking the version chip still shows notes.
|
|
159
|
-
const load = hasUpdate
|
|
160
|
-
? fetchChangelogSince(isTest ? "0.0.0" : current)
|
|
161
|
-
: fetchRecentChangelog();
|
|
162
|
-
load
|
|
167
|
+
fetchRecentChangelog()
|
|
163
168
|
.then(setNotes)
|
|
164
169
|
.catch(() => setNotes([]))
|
|
165
170
|
.finally(() => setNotesLoading(false));
|
|
166
|
-
}, [open, current
|
|
171
|
+
}, [open, current]);
|
|
172
|
+
|
|
173
|
+
// The published CHANGELOG is the earliest signal of a release: it updates the
|
|
174
|
+
// moment the release is pushed, while the registry only flips once the
|
|
175
|
+
// package is published. Whenever the notes name a version newer than the
|
|
176
|
+
// installed one, offer the upgrade — seeing a release in this panel and not
|
|
177
|
+
// being able to take it is never the right outcome, so the panel's own
|
|
178
|
+
// content is a first-class update signal, not decoration.
|
|
179
|
+
const changelogUpdate = useMemo(() => {
|
|
180
|
+
if (!notes || !current) return null;
|
|
181
|
+
const newest = newestSectionVersion(notes);
|
|
182
|
+
return newest && compareSemver(newest, current) > 0 ? newest : null;
|
|
183
|
+
}, [notes, current]);
|
|
184
|
+
|
|
185
|
+
const effectiveAvailable = realUpdate ?? changelogUpdate ?? (isTest ? testTarget : null);
|
|
186
|
+
const hasUpdate = !!effectiveAvailable && !dismissed;
|
|
187
|
+
|
|
188
|
+
// In update mode the list answers "what am I about to get", so drop anything
|
|
189
|
+
// already installed. If that leaves nothing (registry ahead of the changelog)
|
|
190
|
+
// keep the full list rather than an empty panel.
|
|
191
|
+
const visibleNotes = useMemo(() => {
|
|
192
|
+
if (!notes || !hasUpdate || isTest || !current) return notes;
|
|
193
|
+
const newer = notes.filter((s) => compareSemver(s.version, current) > 0);
|
|
194
|
+
return newer.length > 0 ? newer : notes;
|
|
195
|
+
}, [notes, hasUpdate, isTest, current]);
|
|
167
196
|
|
|
168
197
|
// The server is restarting into the new version — stop showing "Updating…",
|
|
169
198
|
// switch to the reconnect state, and auto-reload once it's listening again
|
|
@@ -185,7 +214,7 @@ export function UpgradeButton({ align = "right" }: { align?: "left" | "right" })
|
|
|
185
214
|
} else {
|
|
186
215
|
toast.info(data.message || "Upgrade installed. Restart PPM manually.");
|
|
187
216
|
setUpgrading(false);
|
|
188
|
-
if (
|
|
217
|
+
if (effectiveAvailable) sessionStorage.setItem(DISMISS_KEY_PREFIX + effectiveAvailable, "1");
|
|
189
218
|
setDismissed(true);
|
|
190
219
|
}
|
|
191
220
|
} catch (e) {
|
|
@@ -193,18 +222,26 @@ export function UpgradeButton({ align = "right" }: { align?: "left" | "right" })
|
|
|
193
222
|
// Expected: self-replace killed the server before the response flushed.
|
|
194
223
|
// The upgrade is in progress — wait for it, don't report a failure.
|
|
195
224
|
enterRestartWait();
|
|
225
|
+
} else if (/already on latest/i.test((e as Error).message)) {
|
|
226
|
+
// Offered from the changelog, but the package registry has not caught up
|
|
227
|
+
// yet — the release is announced and not yet installable. Say so, and
|
|
228
|
+
// leave the offer standing so the next attempt can succeed.
|
|
229
|
+
toast.info(`v${effectiveAvailable} is announced but not published yet — try again shortly.`);
|
|
230
|
+
setUpgrading(false);
|
|
196
231
|
} else {
|
|
197
232
|
toast.error(`Upgrade failed: ${(e as Error).message}`);
|
|
198
233
|
setUpgrading(false);
|
|
199
234
|
}
|
|
200
235
|
}
|
|
201
|
-
}, [
|
|
236
|
+
}, [effectiveAvailable, enterRestartWait]);
|
|
202
237
|
|
|
203
238
|
const handleDismiss = useCallback(() => {
|
|
204
|
-
|
|
239
|
+
// Key on the offered version, not just the server-reported one: an update
|
|
240
|
+
// known only from the changelog must be dismissable too.
|
|
241
|
+
if (effectiveAvailable) sessionStorage.setItem(DISMISS_KEY_PREFIX + effectiveAvailable, "1");
|
|
205
242
|
setDismissed(true);
|
|
206
243
|
setOpen(false);
|
|
207
|
-
}, [
|
|
244
|
+
}, [effectiveAvailable]);
|
|
208
245
|
|
|
209
246
|
if (!current && !hasUpdate) return null;
|
|
210
247
|
|
|
@@ -277,9 +314,9 @@ export function UpgradeButton({ align = "right" }: { align?: "left" | "right" })
|
|
|
277
314
|
<span className="flex items-center gap-2 text-text-3">
|
|
278
315
|
<Loader2 className="size-3.5 animate-spin" /> Loading release notes…
|
|
279
316
|
</span>
|
|
280
|
-
) :
|
|
317
|
+
) : visibleNotes && visibleNotes.length > 0 ? (
|
|
281
318
|
<div className="space-y-2.5">
|
|
282
|
-
{
|
|
319
|
+
{visibleNotes.map((s) => (
|
|
283
320
|
<ChangelogEntry key={s.version} version={s.version} body={s.body} />
|
|
284
321
|
))}
|
|
285
322
|
</div>
|
|
@@ -216,9 +216,10 @@ export function useChat(sessionId: string | null, providerId = "claude", project
|
|
|
216
216
|
// Sync streaming state to global store (for favicon + tab icon indicators)
|
|
217
217
|
useEffect(() => {
|
|
218
218
|
if (!sessionId) return;
|
|
219
|
-
|
|
219
|
+
// projectName tags the entry so a project-scoped sync can reconcile it.
|
|
220
|
+
useStreamingStore.getState().setStreaming(sessionId, phase !== "idle", projectName);
|
|
220
221
|
return () => { useStreamingStore.getState().setStreaming(sessionId, false); };
|
|
221
|
-
}, [sessionId, phase]);
|
|
222
|
+
}, [sessionId, phase, projectName]);
|
|
222
223
|
|
|
223
224
|
/**
|
|
224
225
|
* Route a child event to its parent Agent/Task tool_use's children array.
|
|
@@ -3,6 +3,7 @@ import { WsClient } from "@/lib/ws-client";
|
|
|
3
3
|
import { getAuthToken } from "@/lib/api-client";
|
|
4
4
|
import { useNotificationStore } from "@/stores/notification-store";
|
|
5
5
|
import { useStreamingStore } from "@/stores/streaming-store";
|
|
6
|
+
import { syncRunningSessions } from "@/lib/sync-running-sessions";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* App-wide event bus client (`/ws/global`).
|
|
@@ -18,6 +19,9 @@ import { useStreamingStore } from "@/stores/streaming-store";
|
|
|
18
19
|
* - `session:phase_changed` → keeps the tab-strip spinner and title indicator
|
|
19
20
|
* correct for sessions whose tab is not mounted, and — critically — clears them
|
|
20
21
|
* when the turn ends. Nothing else can: `useChat` only runs while mounted.
|
|
22
|
+
* On every (re)connect the indicators are also reconciled against the server
|
|
23
|
+
* registry, since a phase change that happened while this socket was down was
|
|
24
|
+
* never delivered and would otherwise stick until a full reload.
|
|
21
25
|
* - `jira:*` → re-dispatched as window events.
|
|
22
26
|
*
|
|
23
27
|
* Also tells the server which project to watch, so file watching follows the
|
|
@@ -53,6 +57,7 @@ export function useGlobalEvents(enabled: boolean, projectName?: string): void {
|
|
|
53
57
|
if (projectRef.current) {
|
|
54
58
|
client.send(JSON.stringify({ type: "watch", projectName: projectRef.current }));
|
|
55
59
|
}
|
|
60
|
+
void syncRunningSessions(projectRef.current);
|
|
56
61
|
return;
|
|
57
62
|
}
|
|
58
63
|
|
|
@@ -73,8 +78,8 @@ export function useGlobalEvents(enabled: boolean, projectName?: string): void {
|
|
|
73
78
|
}
|
|
74
79
|
|
|
75
80
|
if (type === "session:phase_changed") {
|
|
76
|
-
const d = data as unknown as { sessionId: string; phase: string };
|
|
77
|
-
useStreamingStore.getState().setStreaming(d.sessionId, d.phase !== "idle");
|
|
81
|
+
const d = data as unknown as { sessionId: string; phase: string; projectName?: string };
|
|
82
|
+
useStreamingStore.getState().setStreaming(d.sessionId, d.phase !== "idle", d.projectName);
|
|
78
83
|
return;
|
|
79
84
|
}
|
|
80
85
|
|
|
@@ -97,5 +102,6 @@ export function useGlobalEvents(enabled: boolean, projectName?: string): void {
|
|
|
97
102
|
useEffect(() => {
|
|
98
103
|
if (!enabled || !projectName) return;
|
|
99
104
|
clientRef.current?.send(JSON.stringify({ type: "watch", projectName }));
|
|
105
|
+
void syncRunningSessions(projectName);
|
|
100
106
|
}, [enabled, projectName]);
|
|
101
107
|
}
|
package/src/web/lib/changelog.ts
CHANGED
|
@@ -51,6 +51,19 @@ export function parseChangelog(md: string): ChangelogSection[] {
|
|
|
51
51
|
return sections;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Newest version among the given sections, or null when there are none.
|
|
56
|
+
* Order-independent on purpose: the file is authored newest-first by
|
|
57
|
+
* convention, and a release that lands out of order must not read as "no newer
|
|
58
|
+
* version" to the upgrade check that consumes this.
|
|
59
|
+
*/
|
|
60
|
+
export function newestSectionVersion(sections: ChangelogSection[]): string | null {
|
|
61
|
+
return sections.reduce<string | null>(
|
|
62
|
+
(best, s) => (best && compareSemver(s.version, best) <= 0 ? best : s.version),
|
|
63
|
+
null,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
54
67
|
/** Parse CHANGELOG markdown into version sections strictly newer than `since`. */
|
|
55
68
|
export function parseChangelogSince(md: string, since: string): ChangelogSection[] {
|
|
56
69
|
return parseChangelog(md).filter((s) => compareSemver(s.version, since) > 0);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { api, projectUrl } from "@/lib/api-client";
|
|
2
|
+
import { useStreamingStore } from "@/stores/streaming-store";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Reconcile a project's streaming indicators (tab-strip spinner, title/favicon)
|
|
6
|
+
* against the server's session registry.
|
|
7
|
+
*
|
|
8
|
+
* Runs on every `/ws/global` (re)connect and on project switch. Two gaps it
|
|
9
|
+
* closes, both of which nothing else can:
|
|
10
|
+
* - a turn running in a tab that is not mounted (tabs mount lazily, so its phase
|
|
11
|
+
* never reaches this client) → indicator missing;
|
|
12
|
+
* - an `idle` broadcast that landed while the global socket was down → indicator
|
|
13
|
+
* stuck on until a full page reload.
|
|
14
|
+
*/
|
|
15
|
+
export async function syncRunningSessions(projectName: string | undefined): Promise<void> {
|
|
16
|
+
if (!projectName) return;
|
|
17
|
+
try {
|
|
18
|
+
const running = await api.get<{ sessionId: string }[]>(
|
|
19
|
+
`${projectUrl(projectName)}/chat/sessions/running`,
|
|
20
|
+
);
|
|
21
|
+
useStreamingStore
|
|
22
|
+
.getState()
|
|
23
|
+
.replaceProjectStreaming(projectName, running.map((s) => s.sessionId));
|
|
24
|
+
} catch {
|
|
25
|
+
// Never block boot or a reconnect on an indicator.
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -2,21 +2,43 @@ import { create } from "zustand";
|
|
|
2
2
|
|
|
3
3
|
/** Tracks which chat sessions are currently streaming AI responses */
|
|
4
4
|
interface StreamingStore {
|
|
5
|
-
/**
|
|
6
|
-
|
|
5
|
+
/** sessionId → projectName ("" when unknown) for sessions actively streaming.
|
|
6
|
+
* The project is recorded so a project-scoped sync can drop stale entries
|
|
7
|
+
* without touching sessions that belong to another project. */
|
|
8
|
+
sessions: Map<string, string>;
|
|
7
9
|
/** Mark a session as streaming or idle */
|
|
8
|
-
setStreaming: (sessionId: string, streaming: boolean) => void;
|
|
10
|
+
setStreaming: (sessionId: string, streaming: boolean, projectName?: string) => void;
|
|
11
|
+
/**
|
|
12
|
+
* Reconcile one project's streaming set against the server's registry, which
|
|
13
|
+
* is authoritative. Needed because an `idle` broadcast missed while
|
|
14
|
+
* `/ws/global` was down would otherwise leave a spinner running forever —
|
|
15
|
+
* nothing else ever clears it. Scoped per project: the running list only
|
|
16
|
+
* covers one project, so entries from other projects must survive.
|
|
17
|
+
*/
|
|
18
|
+
replaceProjectStreaming: (projectName: string, sessionIds: string[]) => void;
|
|
9
19
|
}
|
|
10
20
|
|
|
11
21
|
export const useStreamingStore = create<StreamingStore>((set) => ({
|
|
12
|
-
sessions: new
|
|
13
|
-
setStreaming: (sessionId, streaming) =>
|
|
22
|
+
sessions: new Map(),
|
|
23
|
+
setStreaming: (sessionId, streaming, projectName) =>
|
|
14
24
|
set((state) => {
|
|
15
|
-
const next = new
|
|
16
|
-
|
|
25
|
+
const next = new Map(state.sessions);
|
|
26
|
+
// Keep a previously recorded project when a caller omits it, so the entry
|
|
27
|
+
// stays reconcilable.
|
|
28
|
+
if (streaming) next.set(sessionId, projectName ?? next.get(sessionId) ?? "");
|
|
17
29
|
else next.delete(sessionId);
|
|
18
30
|
return { sessions: next };
|
|
19
31
|
}),
|
|
32
|
+
replaceProjectStreaming: (projectName, sessionIds) =>
|
|
33
|
+
set((state) => {
|
|
34
|
+
const running = new Set(sessionIds);
|
|
35
|
+
const next = new Map(state.sessions);
|
|
36
|
+
for (const [id, project] of state.sessions) {
|
|
37
|
+
if (project === projectName && !running.has(id)) next.delete(id);
|
|
38
|
+
}
|
|
39
|
+
for (const id of sessionIds) next.set(id, projectName);
|
|
40
|
+
return { sessions: next };
|
|
41
|
+
}),
|
|
20
42
|
}));
|
|
21
43
|
|
|
22
44
|
/** Selector: true if any session is streaming */
|
package/.rollout-mark
DELETED