Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@
"dockerfile-utils": "0.16.3",
"dotenv": "^17.3.1",
"drizzle-orm": "0.45.1",
"electron-updater": "^6.7.3",
"electron-updater": "^6.8.3",
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "# package.json の version range"
sed -n '172,175p' apps/desktop/package.json

echo
echo "# repo 内の private electron-updater 依存箇所"
rg -n "downloadedUpdateHelper|clearCachedUpdate" apps/desktop/src/main/lib/auto-updater.ts

echo
echo "# electron-updater 6.8.3 tarball 内に private symbol が存在するか"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
curl -Ls https://registry.npmjs.org/electron-updater/-/electron-updater-6.8.3.tgz | tar -xz -C "$TMP"
rg -n "downloadedUpdateHelper" "$TMP/package"

Repository: MocA-Love/superset

Length of output: 50375


非公開 internals に依存するなら electron-updater は固定した方が安全です。

apps/desktop/src/main/lib/auto-updater.tsdownloadedUpdateHelper に直接触る前提になったので、ここが ^6.8.3 のままだと lockfile 再生成だけで private API 変更の影響を受けます。少なくともこの依存だけは exact pin にしておく方が事故りにくいです。

🔧 提案差分
-		"electron-updater": "^6.8.3",
+		"electron-updater": "6.8.3",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"electron-updater": "^6.8.3",
"electron-updater": "6.8.3",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/package.json` at line 174, Change the "electron-updater"
dependency from a caret range to an exact version in package.json (replace
^6.8.3 with 6.8.3) because your auto-updater code touches the private API
downloadedUpdateHelper; after updating the dependency spec, regenerate the
lockfile (run install) so the lockfile pins the exact electron-updater release
to avoid accidental private-API breaks.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

保留: この PR は upstream からの取り込みが主目的で、exact pin にすると将来の upstream sync で ^6.8.3 と衝突します。upstream 側で pin すべきか議論したいので、このフォークでは caret を維持します。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MocA-Love、ご説明ありがとうございます。upstream sync との互換性を優先してキャレット範囲を維持するという判断、承知しました。この点は今後のレビューで指摘しないよう記録しておきます。

( ´・ω・`)ノ


✏️ Learnings added
Learnt from: MocA-Love
Repo: MocA-Love/superset PR: 204
File: apps/desktop/package.json:174-174
Timestamp: 2026-04-16T09:32:50.995Z
Learning: In MocA-Love/superset (fork of upstream), `electron-updater` in `apps/desktop/package.json` intentionally uses a caret range (e.g. `^6.8.3`) rather than an exact pin. Exact pinning is avoided because this fork closely tracks upstream and a pinned version would conflict during future upstream sync merges. Do not suggest exact pinning for `electron-updater` in this repository unless upstream itself adopts exact pinning.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: CR
Repo: MocA-Love/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-14T09:00:06.303Z
Learning: Use published upstream `mastracode` and `mastra/*` packages - do not add fork tarball overrides or custom patch steps unless explicitly requested

"elkjs": "^0.11.1",
"exceljs": "^4.4.0",
"execa": "^9.6.0",
Expand Down
21 changes: 21 additions & 0 deletions apps/desktop/src/main/lib/auto-updater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,26 @@ import { gt, prerelease, valid } from "semver";
import { AUTO_UPDATE_STATUS, type AutoUpdateStatus } from "shared/auto-update";
import { PLATFORM } from "shared/constants";

// electron-updater's internal cache only self-invalidates when the remote
// sha512 differs from cached metadata, so a corrupt cached download (e.g.
// failed Squirrel install) gets retried indefinitely until the user
// manually reinstalls. Reach into the protected helper to clear it.
interface AppUpdaterInternals {
downloadedUpdateHelper: { clear(): Promise<void> } | null;
}

async function clearCachedUpdate(reason: string): Promise<void> {
const helper = (autoUpdater as unknown as AppUpdaterInternals)
.downloadedUpdateHelper;
if (!helper) return;
try {
await helper.clear();
console.info(`[auto-updater] Cleared cached update (${reason})`);
} catch (error) {
console.error("[auto-updater] Failed to clear cached update:", error);
}
}

const UPDATE_CHECK_INTERVAL_MS = 1000 * 60 * 60 * 4; // 4 hours

/**
Expand Down Expand Up @@ -405,6 +425,7 @@ export function setupAutoUpdater(): void {
`[auto-updater] Error during update (currentVersion=${app.getVersion()}):`,
error?.message || error,
);
void clearCachedUpdate(`error: ${error?.message ?? "unknown"}`);
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "# currentStatus の更新箇所"
sed -n '97,110p' apps/desktop/src/main/lib/auto-updater.ts

echo
echo "# error handler の現在実装"
sed -n '418,430p' apps/desktop/src/main/lib/auto-updater.ts

Repository: MocA-Love/superset

Length of output: 863


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# ファイル全体の行数確認
wc -l apps/desktop/src/main/lib/auto-updater.ts

# clearCachedUpdate の定義を確認
echo "=== clearCachedUpdate 関数 ==="
rg -A 8 "function clearCachedUpdate|const clearCachedUpdate" apps/desktop/src/main/lib/auto-updater.ts

# currentStatus の定義と使用方法
echo ""
echo "=== currentStatus 定義 ==="
rg -B 2 "currentStatus\s*=" apps/desktop/src/main/lib/auto-updater.ts | head -30

# error handler 周辺の full context
echo ""
echo "=== error handler 完全コンテキスト (lines 415-435) ==="
sed -n '415,435p' apps/desktop/src/main/lib/auto-updater.ts

# AUTO_UPDATE_STATUS の定義確認
echo ""
echo "=== AUTO_UPDATE_STATUS 定義 ==="
rg -A 10 "enum AUTO_UPDATE_STATUS|const AUTO_UPDATE_STATUS" apps/desktop/src/main/lib/auto-updater.ts | head -20

Repository: MocA-Love/superset

Length of output: 1577


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# AUTO_UPDATE_STATUS 定義を見つける
echo "=== AUTO_UPDATE_STATUS 定義 ==="
sed -n '1,100p' apps/desktop/src/main/lib/auto-updater.ts | grep -A 15 "AUTO_UPDATE_STATUS"

# イベントハンドラ一覧で状態遷移を理解
echo ""
echo "=== autoUpdater イベントハンドラ一覧 ==="
rg "autoUpdater\.on\(" apps/desktop/src/main/lib/auto-updater.ts | head -20

# download-progress, update-downloaded などのイベントハンドラ
echo ""
echo "=== update-downloaded ハンドラ ==="
rg -A 6 'autoUpdater\.on\("update-downloaded"' apps/desktop/src/main/lib/auto-updater.ts

# download-progress ハンドラ
echo ""
echo "=== download-progress ハンドラ ==="
rg -A 6 'autoUpdater\.on\("download-progress"' apps/desktop/src/main/lib/auto-updater.ts

# checking-for-update ハンドラ
echo ""
echo "=== checking-for-update ハンドラ ==="
rg -A 6 'autoUpdater\.on\("checking-for-update"' apps/desktop/src/main/lib/auto-updater.ts

Repository: MocA-Love/superset

Length of output: 2178


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# AUTO_UPDATE_STATUS enum in shared package
echo "=== AUTO_UPDATE_STATUS enum definition ==="
find apps -name "auto-update.ts" -o -name "auto-update.tsx" | while read f; do
  if [[ -f "$f" ]]; then
    echo "File: $f"
    cat -n "$f" | head -50
  fi
done

# Also check in packages
echo ""
echo "=== shared/auto-update search ==="
fd "auto-update" --type f | grep -E "(shared|packages)" | head -10

# Full file listing to find where it's defined
echo ""
echo "=== searching for AUTO_UPDATE_STATUS enum ==="
rg "enum AUTO_UPDATE_STATUS|export.*AUTO_UPDATE_STATUS" --type ts --type tsx

Repository: MocA-Love/superset

Length of output: 724


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check update-available handler to see when DOWNLOADING state is set
echo "=== update-available ハンドラ ==="
rg -A 10 'autoUpdater\.on\("update-available"' apps/desktop/src/main/lib/auto-updater.ts

# Check if there's any explicit DOWNLOADING state setting
echo ""
echo "=== DOWNLOADING state transitions ==="
rg "DOWNLOADING" apps/desktop/src/main/lib/auto-updater.ts

# Check the full sequence of state changes
echo ""
echo "=== 状態遷移フロー ==="
sed -n '420,480p' apps/desktop/src/main/lib/auto-updater.ts

# Check if isNetworkError is defined and what it matches
echo ""
echo "=== isNetworkError 関数 ==="
rg -B 2 -A 10 "function isNetworkError|const isNetworkError" apps/desktop/src/main/lib/auto-updater.ts

Repository: MocA-Love/superset

Length of output: 3079


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check SILENT_ERROR_PATTERNS to understand what errors are considered non-network
echo "=== SILENT_ERROR_PATTERNS 定義 ==="
rg -B 5 "SILENT_ERROR_PATTERNS" apps/desktop/src/main/lib/auto-updater.ts

# Look for all places where clearCachedUpdate is called
echo ""
echo "=== clearCachedUpdate の呼び出し全体 ==="
rg "clearCachedUpdate" apps/desktop/src/main/lib/auto-updater.ts -B 2 -A 1

# Check download-progress to see if errors can occur during download
echo ""
echo "=== ダウンロード進行中のエラーハンドリング ==="
sed -n '460,475p' apps/desktop/src/main/lib/auto-updater.ts

# Check if there are any other state-dependent error handling
echo ""
echo "=== state を条件にした処理がないか確認 ==="
rg "currentStatus.*==|currentStatus.*===|switch.*currentStatus" apps/desktop/src/main/lib/auto-updater.ts

Repository: MocA-Love/superset

Length of output: 1392


🏁 Script executed:

#!/bin/bash
set -euo typefail

# Get full SILENT_ERROR_PATTERNS list to understand what non-network errors include
echo "=== SILENT_ERROR_PATTERNS 完全リスト ==="
sed -n '1,100p' apps/desktop/src/main/lib/auto-updater.ts | grep -A 20 "SILENT_ERROR_PATTERNS"

# Check if error handler can be triggered during CHECKING phase (before download starts)
echo ""
echo "=== checkForUpdates 関数 ==="
rg -A 20 "async function checkForUpdates|function checkForUpdates" apps/desktop/src/main/lib/auto-updater.ts

Repository: MocA-Love/superset

Length of output: 115


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Get full SILENT_ERROR_PATTERNS list to understand what non-network errors include
echo "=== SILENT_ERROR_PATTERNS 完全リスト ==="
sed -n '1,100p' apps/desktop/src/main/lib/auto-updater.ts | grep -A 20 "SILENT_ERROR_PATTERNS"

# Check if error handler can be triggered during CHECKING phase (before download starts)
echo ""
echo "=== checkForUpdates 関数 ==="
rg -A 20 "async function checkForUpdates|function checkForUpdates" apps/desktop/src/main/lib/auto-updater.ts

Repository: MocA-Love/superset

Length of output: 1820


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check what errors can occur during the CHECKING phase
# Look at the broader context around checkForUpdates and error handling
echo "=== checkForUpdates と error handler の全コンテキスト ==="
sed -n '340,410p' apps/desktop/src/main/lib/auto-updater.ts

# Check if there are metadata/feed validation errors that could occur
echo ""
echo "=== UPDATE_FEED_URL と feed 検証 ==="
rg "UPDATE_FEED_URL|sha512|checksum" apps/desktop/src/main/lib/auto-updater.ts | head -20

# Verify the state flow: when can error be triggered?
echo ""
echo "=== Error が発生しうるフェーズ分析 ==="
# Get lines around line 415-435 to see state when error fires
sed -n '410,440p' apps/desktop/src/main/lib/auto-updater.ts

Repository: MocA-Love/superset

Length of output: 3375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check if there are any feed parsing or metadata validation errors that could occur
# during CHECKING phase
echo "=== electron-updater の error イベント発生条件 ==="
# Look for comments or documentation about when error is emitted
sed -n '1,50p' apps/desktop/src/main/lib/auto-updater.ts

# Verify: can error occur during CHECKING phase (before DOWNLOADING)?
# If yes, then the concern is valid
echo ""
echo "=== CHECKING フェーズ中のエラー可能性 ==="
# The autoUpdater.on("error") is global and fires for ANY error
# CHECKING phase: autoUpdater.checkForUpdates() could fail on feed validation
# This would trigger error handler while currentStatus === CHECKING

# Final verification: confirm the flow
echo "フロー:"
echo "1. checkForUpdates() -> CHECKING 状態に変更"
echo "2. autoUpdater.checkForUpdates() 実行"
echo "3a. 成功: update-available または update-not-available イベント"
echo "3b. 失敗: error イベント(currentStatus はまだ CHECKING)"
echo ""
echo "現在の実装では 3b の時点で clearCachedUpdate() が呼ばれるため、"
echo "CHECKING フェーズのエラー(feed 検証失敗など)でもキャッシュが削除される"
echo ""
echo "proposed fix では currentStatus === DOWNLOADING の時のみ削除するので、"
echo "CHECKING フェーズのエラーではキャッシュを残す"

Repository: MocA-Love/superset

Length of output: 2525


キャッシュ削除の条件が広すぎます。

ここだと「非ネットワーク系エラー」なら全部キャッシュを消すので、CHECKING フェーズの feed 検証エラーなど、キャッシュ破損と無関係なエラーでも、既に落とせていた更新を捨てる可能性があります。clearCachedUpdate() は corrupt 済みファイルのクリアが目的なので、実際にダウンロードが進行中の DOWNLOADING フェーズに限定すべきです。

🔧 提案差分
+function shouldClearCachedUpdate(status: AutoUpdateStatus): boolean {
+	return status === AUTO_UPDATE_STATUS.DOWNLOADING;
+}
+
 	autoUpdater.on("error", (error) => {
 		if (isNetworkError(error)) {
 			console.info("[auto-updater] Network unavailable, will retry later");
 			emitStatus(AUTO_UPDATE_STATUS.IDLE);
 			return;
@@
-		void clearCachedUpdate(`error: ${error?.message ?? "unknown"}`);
+		if (shouldClearCachedUpdate(currentStatus)) {
+			void clearCachedUpdate(`error: ${error?.message ?? "unknown"}`);
+		}
 		emitStatus(AUTO_UPDATE_STATUS.ERROR, undefined, error.message);
 	});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void clearCachedUpdate(`error: ${error?.message ?? "unknown"}`);
function shouldClearCachedUpdate(status: AutoUpdateStatus): boolean {
return status === AUTO_UPDATE_STATUS.DOWNLOADING;
}
autoUpdater.on("error", (error) => {
if (isNetworkError(error)) {
console.info("[auto-updater] Network unavailable, will retry later");
emitStatus(AUTO_UPDATE_STATUS.IDLE);
return;
}
if (shouldClearCachedUpdate(currentStatus)) {
void clearCachedUpdate(`error: ${error?.message ?? "unknown"}`);
}
emitStatus(AUTO_UPDATE_STATUS.ERROR, undefined, error.message);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/main/lib/auto-updater.ts` at line 428, clearCachedUpdate is
being called too broadly on any non-network error; restrict cache clearing to
when a download is actually in progress by checking the updater phase/state
first. Update the call site that currently invokes clearCachedUpdate(`error:
${error?.message ?? "unknown"}`) so it only runs when the updater's phase/state
equals DOWNLOADING (or the equivalent variable/enum used in this module, e.g.
updatePhase, currentState, or UpdateState.DOWNLOADING); otherwise skip clearing
the cached update so CHECKING/FEED validation errors don't discard a valid
downloaded update.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

保留: upstream 共通の挙動なので、この PR ではそのまま取り込みます。upstream 側で修正されたら次回同期で取り込む方針。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MocA-Love 了解しました。upstream 共通の挙動として本 PR はそのまま取り込み、upstream 側で修正が入った際に次回同期で対応する方針ですね。追跡のため、必要であれば後でフォローアップ Issue を作成することもできますのでお知らせください。


🧠 Learnings used
Learnt from: CR
Repo: MocA-Love/superset PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-04-14T09:00:06.303Z
Learning: Use published upstream `mastracode` and `mastra/*` packages - do not add fork tarball overrides or custom patch steps unless explicitly requested

emitStatus(AUTO_UPDATE_STATUS.ERROR, undefined, error.message);
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { workspaceTrpc } from "@superset/workspace-client";
import { useCallback } from "react";
import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider";
import { useWorkspaceEvent } from "../useWorkspaceEvent";

/**
Expand All @@ -15,20 +14,25 @@ import { useWorkspaceEvent } from "../useWorkspaceEvent";
* debounce needed.
*/
export function useGitStatus(workspaceId: string) {
const collections = useCollections();
const baseBranch: string | null =
collections.v2WorkspaceLocalState.get(workspaceId)?.sidebarState
?.baseBranch ?? null;

const utils = workspaceTrpc.useUtils();

const baseBranchQuery = workspaceTrpc.git.getBaseBranch.useQuery(
{ workspaceId },
{ staleTime: Number.POSITIVE_INFINITY, enabled: Boolean(workspaceId) },
);
const baseBranch = baseBranchQuery.data?.baseBranch ?? null;

const query = workspaceTrpc.git.getStatus.useQuery(
{ workspaceId, baseBranch: baseBranch ?? undefined },
{ refetchOnWindowFocus: true, enabled: Boolean(workspaceId) },
Comment on lines +19 to 27
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

保存済み base branch の取得前に getStatus が既定ブランチで走ります。

初回レンダーでは baseBranchQuery.data がまだ無いので、このクエリは一度 undefined で実行されます。保存済み base branch が既定と異なる workspace では、Changes 一覧が既定ブランチ基準 → 保存済み base branch 基準に一瞬ジャンプします。getBaseBranch が一度解決するまでは getStatus を待たせた方が安定します。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/hooks/host-service/useGitStatus/useGitStatus.ts`
around lines 19 - 27, getStatus is running before getBaseBranch resolves because
baseBranchQuery.data is initially undefined; update the
workspaceTrpc.git.getStatus.useQuery call to only enable when the base-branch
query has completed. Change the enabled flag from Boolean(workspaceId) to
Boolean(workspaceId) && baseBranchQuery.isFetched (or
baseBranchQuery.isSuccess), so getStatus (workspaceTrpc.git.getStatus.useQuery)
waits for baseBranchQuery (workspaceTrpc.git.getBaseBranch.useQuery) to resolve
before executing.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

保留: upstream 共通の挙動で、軽微な UX ちらつき。upstream 側の判断を待つ方針です。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MocA-Love、了解しました。upstream 共通の挙動であり、軽微な UX ちらつきとのことですので、upstream 側の対応を待つ方針を尊重します。必要であれば後続 PR でフォローアップしてください。

);

const invalidate = useCallback(() => {
void utils.git.getStatus.invalidate({ workspaceId });
// Current branch may have changed (external checkout), and
// branch.<name>.base is per-branch — drop the cache so the next read
// picks up the new branch's base.
void utils.git.getBaseBranch.invalidate({ workspaceId });
}, [utils, workspaceId]);

useWorkspaceEvent("git:changed", workspaceId, invalidate);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,12 @@ function PendingWorkspacePage() {
// pendingId actually dispatches — otherwise the second page sticks in
// "creating" forever.
const prevPendingIdRef = useRef(pendingId);
const [syncTimedOut, setSyncTimedOut] = useState(false);
if (prevPendingIdRef.current !== pendingId) {
prevPendingIdRef.current = pendingId;
firedRef.current = false;
navigatedRef.current = false;
setSyncTimedOut(false);
}

const { data: pendingRows } = useLiveQuery(
Expand Down Expand Up @@ -215,70 +217,55 @@ function PendingWorkspacePage() {
const isStale =
pending?.status === "creating" && elapsedMs > STALE_THRESHOLD_MS;

// Fallback: if the collection never syncs (offline, slow Electric),
// navigate anyway after a bounded wait. Target page will show its own
// loading state.
const [syncTimedOut, setSyncTimedOut] = useState(false);
// FORK NOTE: reset syncTimedOut when pendingId switches — otherwise a
// sticky `true` from the previous pending would skip the local-sync
// wait on the next pending and reintroduce the `workspace not found`
// race this fallback is trying to paper over. Biome flags [pendingId]
// as unnecessary because the effect body doesn't read it, but we want
// the effect to *re-run on change* — that's the whole point.
// biome-ignore lint/correctness/useExhaustiveDependencies: intentional re-run on pendingId change
useEffect(() => {
setSyncTimedOut(false);
}, [pendingId]);
// If sync stalls past this, swap the spinner for a recoverable stall UI
// rather than silently navigating into "Workspace not found". syncTimedOut
// must stay in the deps + guard below so "Keep waiting" (which flips it
// false) re-arms a fresh timer instead of leaving the user stranded.
const SYNC_TIMEOUT_MS = 10_000;
useEffect(() => {
if (
pending?.status !== "succeeded" ||
!pending.workspaceId ||
workspaceSynced ||
syncTimedOut ||
navigatedRef.current
) {
return;
}
const timer = setTimeout(() => setSyncTimedOut(true), 3000);
const timer = setTimeout(() => setSyncTimedOut(true), SYNC_TIMEOUT_MS);
return () => clearTimeout(timer);
}, [pending?.status, pending?.workspaceId, workspaceSynced]);
}, [pending?.status, pending?.workspaceId, workspaceSynced, syncTimedOut]);

const doNavigate = useCallback(() => {
if (!pending?.workspaceId || navigatedRef.current) return;
navigatedRef.current = true;
ensureWorkspaceInSidebar(pending.workspaceId, pending.projectId);

if (pending.terminals.length > 0) {
const paneLayout = buildSetupPaneLayout(pending.terminals);
collections.v2WorkspaceLocalState.update(pending.workspaceId, (draft) => {
draft.paneLayout = paneLayout;
});
}

void navigate({
to: "/v2-workspace/$workspaceId",
params: { workspaceId: pending.workspaceId },
});
setTimeout(() => {
collections.pendingWorkspaces.delete(pendingId);
}, 1000);
}, [collections, ensureWorkspaceInSidebar, navigate, pending, pendingId]);

useEffect(() => {
if (
pending?.status === "succeeded" &&
pending.workspaceId &&
(workspaceSynced || syncTimedOut) &&
!navigatedRef.current
workspaceSynced
) {
navigatedRef.current = true;
ensureWorkspaceInSidebar(pending.workspaceId, pending.projectId);

if (pending.terminals.length > 0) {
const paneLayout = buildSetupPaneLayout(pending.terminals);
collections.v2WorkspaceLocalState.update(
pending.workspaceId,
(draft) => {
draft.paneLayout = paneLayout;
},
);
}

void navigate({
to: "/v2-workspace/$workspaceId",
params: { workspaceId: pending.workspaceId },
});
setTimeout(() => {
collections.pendingWorkspaces.delete(pendingId);
}, 1000);
doNavigate();
}
}, [
collections,
ensureWorkspaceInSidebar,
navigate,
pending,
pendingId,
workspaceSynced,
syncTimedOut,
]);
}, [pending?.status, pending?.workspaceId, workspaceSynced, doNavigate]);

if (!pending) {
return (
Expand Down Expand Up @@ -374,24 +361,62 @@ function PendingWorkspacePage() {
</div>
)}

{pending.status === "succeeded" && (
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm text-emerald-500">
<HiCheck className="size-4" />
<span>Workspace ready — opening...</span>
{pending.status === "succeeded" &&
(syncTimedOut && !workspaceSynced ? (
<div className="space-y-4">
<div className="flex items-start gap-2 text-sm text-amber-500">
<HiExclamationTriangle className="size-4 mt-0.5 shrink-0" />
<span>
Workspace was created but hasn't synced to this device yet.
Check your connection.
</span>
</div>
<div className="flex gap-2">
<button
type="button"
className="rounded-md bg-primary px-3 py-1.5 text-sm text-primary-foreground hover:bg-primary/90"
onClick={() => setSyncTimedOut(false)}
>
Keep waiting
</button>
<button
type="button"
className="rounded-md border px-3 py-1.5 text-sm hover:bg-accent"
onClick={doNavigate}
>
Open anyway
</button>
<button
type="button"
className="rounded-md border px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground hover:bg-accent"
onClick={() => {
collections.pendingWorkspaces.delete(pendingId);
void clearAttachments(pendingId);
void navigate({ to: "/" });
}}
>
Dismiss
</button>
</div>
</div>
{pending.warnings.length > 0 && (
<ul className="space-y-1 text-xs text-amber-500">
{pending.warnings.map((w) => (
<li key={w} className="flex items-start gap-1.5">
<HiExclamationTriangle className="size-3.5 mt-0.5 shrink-0" />
<span>{w}</span>
</li>
))}
</ul>
)}
</div>
)}
) : (
<div className="space-y-2">
<div className="flex items-center gap-2 text-sm text-emerald-500">
<HiCheck className="size-4" />
<span>Workspace ready — opening...</span>
</div>
{pending.warnings.length > 0 && (
<ul className="space-y-1 text-xs text-amber-500">
{pending.warnings.map((w) => (
<li key={w} className="flex items-start gap-1.5">
<HiExclamationTriangle className="size-3.5 mt-0.5 shrink-0" />
<span>{w}</span>
</li>
))}
</ul>
)}
</div>
))}

{pending.status === "failed" && (
<div className="space-y-4">
Expand Down
Loading
Loading