Skip to content

Commit 3b1a43a

Browse files
author
A.R.
committed
feat(ui): reconnecting spinner while the daemon reinits on profile switch / refresh
After a profile switch or Refresh state, the extension re-supplies the vault passphrase and hot-reloads the daemon — a headed reinit that takes ~30s. The daemon badge previously kept showing the stale 'anonymous' message during that window, so it looked stuck. Add a transient 'reconnecting' store flag set when profile:switch / dashboard:refresh is sent and cleared when the daemon next reports authenticated (or a 60s safety timeout); the badge shows a spinning 'Reconnecting the daemon to this profile… ~30s' instead of the stale status.
1 parent 8cd1edc commit 3b1a43a

6 files changed

Lines changed: 57 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ All notable changes to this project are documented here. Format follows
66

77
## [Unreleased]
88

9+
## [0.8.51] — 2026-06-04 — "Reconnecting…" spinner during daemon reinit
10+
11+
> Follow-up UX for the 0.8.49 passphrase fix. After a profile switch or "Refresh state" the extension re-supplies the vault passphrase and hot-reloads the daemon — a headed reinit that takes ~30s. The daemon badge kept showing the stale "anonymous" message during that window, so it looked stuck even though it was working (confirmed in the field: the daemon goes green ~30s after a switch).
12+
13+
### Added
14+
15+
- **Reconnecting spinner on the daemon badge** ([`views.tsx`](packages/webview/src/views.tsx), [`store.ts`](packages/webview/src/store.ts), [`App.tsx`](packages/webview/src/App.tsx)). A transient `reconnecting` store flag is set when `profile:switch` / `dashboard:refresh` is sent and cleared when the daemon next reports `authenticated` (or a 60s safety timeout). While active and the daemon is not yet authenticated, the badge shows a spinning **"Reconnecting the daemon to this profile… this can take ~30s."** instead of the stale anonymous/vault-locked status — so the wait reads as progress, not a hang.
16+
917
## [0.8.49] — 2026-06-04 — Daemon unlocks passphrase-protected profiles on switch/refresh
1018

1119
> Reported via a diagnostics bundle: after switching to a **passphrase-protected** profile, the dashboard showed "Session active and ready" while the daemon showed "Daemon sees anonymous session — use Refresh state to reconnect", and Refresh did not help. Root cause: the long-lived daemon is spawned with `PERPLEXITY_VAULT_PASSPHRASE` read from SecretStorage **once at spawn**; a profile switch only touched `.reinit`, which re-ran `init()` with the *stale/absent* passphrase, so the daemon could not unseal the new profile's vault (`Vault locked: no keychain, no env var, no TTY`). The vault unseal cache also pinned the first profile's material (`Vault decrypt failed: wrong passphrase`). The extension could read the vault (the user had typed the passphrase) — hence the two badges disagreed.

packages/extension/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "perplexity-vscode",
33
"displayName": "Perplexity MCP",
4-
"version": "0.8.50",
4+
"version": "0.8.51",
55
"publisher": "Nskha",
66
"private": true,
77
"description": "Perplexity AI search, reasoning, research, and compute — MCP server, dashboard, and multi-IDE auto-config for VS Code.",

packages/mcp-server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "perplexity-user-mcp",
3-
"version": "0.8.50",
3+
"version": "0.8.51",
44
"mcpName": "io.github.Automations-Project/perplexity-user-mcp",
55
"type": "module",
66
"description": "Perplexity AI MCP server — browser automation for search, reasoning, research, and compute. Not affiliated with Perplexity AI, Inc.",

packages/webview/src/App.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ function nextActionId(prefix: string): string {
4242
* auto-generated and the action is registered as pending in the store.
4343
*/
4444
function send(message: WebviewMessage | Omit<Extract<WebviewMessage, { id: string }>, "id">): void {
45+
// Profile switch + "Refresh state" both re-supply the vault passphrase and
46+
// hot-reload the daemon (a headed reinit that takes ~30s). Flag it so the
47+
// daemon badge shows a "Reconnecting…" spinner instead of looking stuck.
48+
if (message.type === "profile:switch" || message.type === "dashboard:refresh") {
49+
useDashboardStore.getState().startReconnecting();
50+
}
4551
if (ACTION_TYPES.has(message.type) && !("id" in message && message.id)) {
4652
const id = nextActionId(message.type);
4753
const full = { ...message, id } as WebviewMessage;

packages/webview/src/store.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,16 @@ interface DashboardStore {
5454
daemonStatus: DaemonStatusState | null;
5555
daemonAuditTail: DaemonAuditEntry[];
5656
daemonTokenRotatedAt: string | null;
57+
/**
58+
* Transient "the daemon is reinitializing" flag. Set when the user triggers a
59+
* profile switch or "Refresh state" — both re-supply the vault passphrase and
60+
* hot-reload the daemon, a headed reinit that takes ~30s. Cleared when the
61+
* daemon next reports `authenticated`, or by a timeout in DashboardView. Lets
62+
* the UI show a "Reconnecting…" spinner instead of a stale "anonymous" badge.
63+
*/
64+
reconnecting: { active: boolean; since: number };
65+
startReconnecting: () => void;
66+
stopReconnecting: () => void;
5767
/**
5868
* Live revealed bearer. Populated ONLY by the `daemon:bearer:reveal:response`
5969
* ExtensionMessage (which itself requires a modal-confirmed
@@ -178,6 +188,9 @@ export const useDashboardStore = create<DashboardStore>((set) => ({
178188
daemonStatus: null,
179189
daemonAuditTail: [],
180190
daemonTokenRotatedAt: null,
191+
reconnecting: { active: false, since: 0 },
192+
startReconnecting: () => set({ reconnecting: { active: true, since: Date.now() } }),
193+
stopReconnecting: () => set({ reconnecting: { active: false, since: 0 } }),
181194
revealedBearer: null,
182195
setRevealedBearer: (r) => set({ revealedBearer: r }),
183196
clearRevealedBearer: () => set({ revealedBearer: null }),
@@ -198,7 +211,13 @@ export const useDashboardStore = create<DashboardStore>((set) => ({
198211
setDoctorReportingOptOut: (v) => set((s) => ({ doctor: { ...s.doctor, reportingOptOut: v } })),
199212
hydrate: (message) => {
200213
if (message.type === "dashboard:state") {
201-
set({ state: message.payload });
214+
set((store) => {
215+
const authed = !!message.payload?.snapshot?.daemonAuth?.authenticated;
216+
// Clear the "reconnecting" spinner once the daemon reports it's back.
217+
return authed && store.reconnecting.active
218+
? { state: message.payload, reconnecting: { active: false, since: 0 } }
219+
: { state: message.payload };
220+
});
202221
return;
203222
}
204223

packages/webview/src/views.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,16 @@ export function DashboardView({
219219
}) {
220220
const snapshot = state.snapshot;
221221
const activeProfile = useDashboardStore((store) => store.activeProfile);
222+
const reconnecting = useDashboardStore((store) => store.reconnecting);
223+
const stopReconnecting = useDashboardStore((store) => store.stopReconnecting);
224+
// Safety net: never spin forever. If the daemon hasn't reported back in 60s
225+
// (reinit failed / vault still locked), drop the spinner so the real status
226+
// message shows again.
227+
useEffect(() => {
228+
if (!reconnecting.active) return;
229+
const t = setTimeout(() => stopReconnecting(), 60_000);
230+
return () => clearTimeout(t);
231+
}, [reconnecting.active, reconnecting.since, stopReconnecting]);
222232
const authAction = activeProfile ? { type: "auth:login" as const, label: "Login", title: "Use the active profile's saved login mode." } : { type: "profile:add-prompt" as const, label: "Add account", title: "Create a profile and start sign-in." };
223233
const recentQueries = state.history.slice(0, 3);
224234
const rateLimitEntries = Object.entries(snapshot.rateLimits?.modes ?? {}) as Array<
@@ -244,6 +254,17 @@ export function DashboardView({
244254
{snapshot.daemonAuth && (() => {
245255
const da = snapshot.daemonAuth;
246256
if (!da) return null;
257+
// While a switch/refresh-triggered reinit is in flight and the daemon
258+
// is not yet authenticated, show a spinner so the ~30s headed reinit
259+
// reads as "working", not "stuck".
260+
if (reconnecting.active && !da.authenticated) {
261+
return (
262+
<div className="flex items-center gap-2">
263+
<RefreshCcw size={13} className="animate-spin" />
264+
<div className="dashboard-status-text">Reconnecting the daemon to this profile… this can take ~30s.</div>
265+
</div>
266+
);
267+
}
247268
let text: string;
248269
if (da.authenticated) {
249270
text = `Daemon: ${da.tier}`;

0 commit comments

Comments
 (0)