Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
6d5cdcb
MUL-7488: fix(execenv): exclude committed local HEAD changes from res…
Dante-dan Oct 14, 2026
52b2690
MUL-8012: fix(inbox): restrict notifications to current workspace rec…
xiaoyaner0201 Oct 14, 2026
d3c1b44
MUL-8014: fix(realtime): revoke workspace WebSocket access (#9249)
multica-eve Oct 15, 2026
83422dc
MUL-7464: fix(agents): recover completed builder drafts and align rec…
yyclaw Oct 15, 2026
640788c
MUL-6945 & MUL-6944: fix(daemon): preserve Git content during artifac…
sandeepl337 Oct 15, 2026
e326a56
MUL-8000: fix(agents): persist builder draft warning dismissal (#9265)
multica-eve Oct 15, 2026
d3730f2
fix(editor): render mixed RTL/LTR text with per-block direction (#9261)
AbdulrhmanKassemSalla Oct 15, 2026
8352787
MUL-7658: fix(daemon): recover undelivered batch claims after timeout…
vicksiyi Oct 15, 2026
231dedb
MUL-8034: chore: clean historical comments across the codebase (#9274)
forrestchang Oct 15, 2026
9f82479
MUL-8018: feat(runtimes): price Claude Sonnet 5.5 and Haiku 5.5 (#9253)
aspcartman Oct 16, 2026
e603190
MUL-8046: feat(issues): per-run cost bars and token detail in the run…
forrestchang Oct 16, 2026
86020b8
MUL-8047: chore(server): remove the pre-v3 desktop onboarding shim (#…
forrestchang Oct 16, 2026
130aaa5
MUL-8047: feat(onboarding): open the runtime guide issue directly ins…
forrestchang Oct 16, 2026
da1671c
MUL-7420: Project workflows — columns and handoffs on the workflow (#…
forrestchang Oct 16, 2026
ec1e931
MUL-8049: fix(workflows): validate the handler in the brief preview (…
forrestchang Oct 16, 2026
700d25e
MUL-7420: render the workflow brief once on the server, for the assig…
forrestchang Oct 16, 2026
ffce31a
docs(changelog): add v0.6.3 release entry (2026-10-16) (MUL-8049) (#9…
multica-eve Oct 16, 2026
18fa9f7
MUL-8050: price Cursor runtime catalog variants (#9295)
forrestchang Oct 16, 2026
1fea916
chore(fork): sync upstream multica v0.6.3 (CHE-1837)
Oct 17, 2026
2874e8b
Merge origin/main into v0.6.3 sync (CHE-1837)
Oct 17, 2026
7835713
chore: untrack accidentally added PR body draft
Oct 17, 2026
29b3b16
style(server): satisfy go-lint in upstream v0.6.3 code (CHE-1837)
Oct 17, 2026
53e74d3
test(server): pin workflow assignee on combined updates and ping for …
Oct 17, 2026
dd338e4
fix(server): keep workflow handoff assignee on combined updates and m…
Oct 17, 2026
2703e4e
Merge origin/main into v0.6.3 sync (CHE-1837)
Oct 17, 2026
2bf873e
fix(migrations): renumber the nine upstream v0.6.3 workflow migration…
Oct 17, 2026
746b0a9
chore(sqlc): regenerate after renumbering the workflow migrations (CH…
Oct 17, 2026
81c9ad2
Merge origin/main into v0.6.3 sync (CHE-1837)
Oct 17, 2026
74c403b
test(workflow): cover workflow races, queue eligibility, copy naming …
Oct 17, 2026
bcdf401
fix(workflow): recheck workflow decisions under locks, keep a run for…
Oct 17, 2026
91ee35c
Merge origin/main into v0.6.3 sync (CHE-1837)
Oct 17, 2026
0cb70ad
test(workflow): cover the issue's actual project, create races, durab…
Oct 17, 2026
c46f297
fix(workflow): lock the issue's own project, hold workflow locks thro…
Oct 17, 2026
c8ae94c
Merge origin/main into v0.6.3 sync (CHE-1837)
Oct 17, 2026
8374469
test(workflow): cover blocked board drops, duplicate-run delivery, st…
Oct 17, 2026
9c3ae7b
fix(workflow): judge board drops on the released column, count only u…
Oct 17, 2026
4fc9bd7
Merge origin/main into v0.6.3 sync (CHE-1837)
Oct 18, 2026
d3ea959
test(workflow): cover builder draft rollback, editor refresh, stale-s…
Oct 18, 2026
be5f4e5
fix(workflow): stop builder draft rollback, keep dirty workflow edits…
Oct 18, 2026
e8e2fe5
test(workflow): give the race helper its own connections so a small p…
Oct 18, 2026
c1a5fdc
test(workflow): cover workflow edit versions, board drops on a strand…
Oct 18, 2026
f72ba4e
fix(workflow): version workflow saves, follow the release target on a…
Oct 18, 2026
c81b526
test(workflow): cover same-second editor versions and the editor befo…
Oct 18, 2026
7d7f362
fix(workflow): bind the saved version to the accepted revision and ga…
Oct 18, 2026
65d9f3d
test(workflow): cover editor drafts during a save, overlapping stale-…
Oct 18, 2026
ee86e7f
fix(workflow): keep editor drafts through a save, cancel stale runs o…
Oct 18, 2026
b423fa5
test(workflow): type the dialog test's client double (CHE-1837)
Oct 18, 2026
4500802
Merge origin/main into v0.6.3 sync (CHE-1837)
Oct 18, 2026
5a8f7e2
chore(migrations): move the workflow migrations to 657-666 so main's …
Oct 18, 2026
e5870fd
test(workflow): cover unresolved workflows, handoff preview binding, …
Oct 18, 2026
5d01ae1
fix(workflow): hold status writes until workflows resolve, bind hando…
Oct 18, 2026
1dceec2
test(agent): the escaped-holder identity reader must wait out an empt…
Oct 18, 2026
7ff70f6
fix(agent): wait out an empty escaped-holder state file and publish i…
Oct 18, 2026
5f1eb05
test(workflow): cover batch handoffs, project-catalog board moves, si…
Oct 19, 2026
f9c68d0
test(workflow): repair batch toolbar fixtures before the fix — mocks …
Oct 19, 2026
74818ed
test(workflow): a no-handoff preview applies the move bound to that r…
Oct 19, 2026
3222c53
fix(workflow): confirm batch handoffs, hold board moves for the proje…
Oct 19, 2026
78c1aa6
test(workflow): failing regressions for batch handoff binding, incomp…
Oct 19, 2026
1087097
fix(workflow): bind batch status writes to a no-handoff preview, reso…
Oct 19, 2026
02d6b45
merge: bring main (CHE-1839 Jev routing) into the v0.6.3 sync branch …
Oct 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ Workspace-scoped queries filter by `workspace_id`; membership gates access and `
## Change and Delivery Rules

- Keep changes scoped; reuse existing patterns. Code comments are English.
- Use names, types, and tests to express behavior. Keep comments for non-obvious rationale and constraints; remove superseded plans and incident narratives when updating code. Preserve tool directives and mixed-version contracts.
- Do not add internal compatibility shims, dual writes, fallback paths, or legacy adapters unless requested. This does not relax API response compatibility above.
- New global pre-workspace routes use a single word or `/{noun}/{verb}`, not hyphenated root names. Update `server/internal/handler/reserved_slugs.json`, run `pnpm generate:reserved-slugs`, and commit `packages/core/paths/reserved-slugs.ts` when changing reserved slugs.
- Use atomic conventional commits and the repository PR template. For releases, follow [.github/RELEASING.md](.github/RELEASING.md); default to a patch bump unless specified otherwise.
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@multica/desktop",
"productName": "Multica",
"version": "0.6.2",
"version": "0.6.3",
"private": true,
"description": "Multica Desktop — native desktop client for the Multica platform.",
"homepage": "https://multica.ai",
Expand Down
26 changes: 4 additions & 22 deletions apps/desktop/src/main/keyboard-shortcuts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,28 +26,10 @@ const ZOOM_MIN = -3;
const ZOOM_MAX = 4.5;

/**
* Inspect a `before-input-event` key and apply (or block) the matching
* window-level shortcut. Returns `true` when the caller should call
* `event.preventDefault()` — that both swallows the renderer keydown and
* prevents the application menu accelerator from firing, so we don't
* double-trigger zoom on macOS where the default menu also binds these
* keys.
*
* Why we don't rely on the menu's `zoomIn` / `zoomOut` roles: on macOS the
* default `Cmd+-` accelerator does not fire reliably across keyboard
* layouts (issue MUL-2354 — Cmd+= zooms in but Cmd+- doesn't undo it).
* Handling the shortcuts here gives identical behavior on every platform
* and every layout.
*/
/**
* Result of handleAppShortcut:
* - `false`: not handled, let Electron continue
* - `true`: handled (preventDefault), no further action
* - `"close-tab"`: Cmd/Ctrl+W intercepted — caller should send IPC to renderer
* - `"open-settings"`: Cmd/Ctrl+, intercepted — caller should route the
* request to the tabbed main window
* - `{ action: "select-tab", key }`: Cmd/Ctrl+1..9 intercepted — caller
* should route the requested browser-style tab position to the main window
* Window shortcut outcomes tell the caller whether to continue, preventDefault,
* or dispatch a renderer action. Prevent handled input from also firing the menu
* accelerator. Handle zoom here for consistent keyboard-layout behavior.
* close-tab, open-settings, and select-tab actions route to the main window.
*/
export type ShortcutResult =
| boolean
Expand Down
24 changes: 4 additions & 20 deletions apps/desktop/src/main/navigation-guard.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,8 @@
/**
* Top-level navigation hardening for renderer windows.
*
* Scope note (MUL-4899) — this is origin hardening ONLY, and deliberately NOT
* the mechanism that handles in-app links.
*
* Client-side routing never fires `will-navigate`: React Router pushes history
* entries without a document load. So an in-app path — a good one like
* `/acme/issues/1` or a bad one like `/Users/me/shot.png` pasted by an agent —
* never reaches this guard at all. Bad in-app paths are a routing concern,
* answered by the renderer's 404 page. Trying to police application paths here
* would be both ineffective (the hook does not see them) and wrong (it would
* break legitimate reloads).
*
* What it does catch is a real document-level navigation away from the trusted
* renderer: `window.location = 'https://evil.example'`, an anchor Chromium
* resolves to a remote URL in the same window, a dropped file URL. These windows
* run with `webSecurity: false` (the process sandbox is enabled), so a
* foreign document landing in one would inherit privileged context. New
* windows and external links already route through `setWindowOpenHandler` →
* `openExternalSafely`; this closes the same-window path.
* Guard document-level renderer navigation to trusted origins or the packaged
* entry file. Same-window foreign documents must not inherit renderer privileges.
* Client-side route changes do not fire will-navigate and remain the router's
* responsibility. New windows and external links use openExternalSafely.
*/

/** The subset of BrowserWindow this module needs — keeps it unit-testable. */
Expand Down
4 changes: 0 additions & 4 deletions apps/desktop/src/renderer/src/App.auth-recovery.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,6 @@ vi.mock("@multica/core/auth", () => ({
selector(state.auth),
}));

vi.mock("@multica/core/onboarding", () => ({
useWelcomeStore: { getState: () => ({ reset: vi.fn() }) },
}));

vi.mock("@multica/core/workspace/queries", () => ({
workspaceKeys: { list: () => ["workspace-list"] },
}));
Expand Down
2 changes: 0 additions & 2 deletions apps/desktop/src/renderer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { useQueryClient } from "@tanstack/react-query";
import { CoreProvider } from "@multica/core/platform";
import { pickLocale, type SupportedLocale } from "@multica/core/i18n";
import { useAuthStore } from "@multica/core/auth";
import { useWelcomeStore } from "@multica/core/onboarding";
import { workspaceKeys } from "@multica/core/workspace/queries";
import { useWorkspaceList } from "@multica/core/workspace";
import { api } from "@multica/core/api";
Expand Down Expand Up @@ -364,7 +363,6 @@ const sessionTeardown: SessionTeardown = {
window.desktopAPI.reportAuthSession?.(userId),
resetTabs: () => useTabStore.getState().reset(),
closeOverlay: () => useWindowOverlayStore.getState().close(),
resetWelcome: () => useWelcomeStore.getState().reset(),
clearDaemonToken: () => window.daemonAPI.clearToken(),
stopDaemon: () => window.daemonAPI.stop(),
};
Expand Down
32 changes: 5 additions & 27 deletions apps/desktop/src/renderer/src/components/tab-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,33 +47,11 @@ import { parseIssueWindowPath } from "../../../shared/issue-window";
const TAB_SCROLL_FADE_SIZE = 24;
const TAB_ENTRY_EASE = [0.22, 1, 0.36, 1] as const;

// Chrome-style merged tab: the active tab shares the content surface's fill and
// flares into it through concave bottom corners. Each flare is a small square
// whose radial gradient carves a quarter-circle notch (transparent, so the
// strip behind the flare shows through), strokes a 1px arc that continues the
// tab's side border into the content card's top ring, and fills the rest with
// the surface colour. The 0.4px spread on either side of the --surface-border
// pair anti-aliases that arc.
//
// That background-color is load-bearing, not decoration: the flare's bottom row
// overlaps the content card's top ring, and in dark mode --surface-border is
// translucent (oklch(1 0 0 / 10%)), so without an opaque layer beneath it the
// arc would composite over the ring instead of replacing it and the two keylines
// would stack into a markedly lighter line right where the straight ring meets
// the curve. It therefore has to be the strip's real backdrop — the sidebar
// wrapper's fill, which is conditional and not this file's to re-derive.
// SIDEBAR_WRAPPER_FILL_CLASS reads it off the wrapper itself.
//
// The backing may only reach as far as it is needed, though. A flare hangs 9px
// past the tab's edge, over the neighbouring tab, so an opaque square there
// prints over whatever that neighbour draws: its hover pill lost the whole
// corner it shares with the flare and read as a dark bite (MUL-6160). Masking
// the notch away removes the backing exactly where the gradient is transparent
// anyway, so the notch is a hole rather than a painted-on copy of the strip and
// shows whatever is actually behind it — bare strip usually, the neighbour's
// pill where it reaches in. The mask is opaque again by the radius where the
// arc's anti-aliasing starts, so every pixel the arc and the canvas fill cover
// keeps its backing.
// Concave tab flares need an opaque backing beneath translucent border arcs
// to avoid compositing a second keyline over the content ring. Read that backdrop
// from SIDEBAR_WRAPPER_FILL_CLASS rather than duplicating the wrapper's fill rule.
// Mask the transparent notch so the backing cannot paint over a neighboring hover
// pill, while retaining backing beneath the arc's antialiasing.
const TAB_FLARE_RADIUS = 10;
const tabFlareGradient = (side: "left" | "right") => {
const r = TAB_FLARE_RADIUS;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,6 @@ vi.mock("@multica/views/workspace/use-workspace-seen", () => ({
useWorkspaceSeen: () => state.workspaceSeen,
}));

vi.mock("@multica/views/workspace/welcome-after-onboarding", () => ({
WelcomeAfterOnboarding: () => null,
}));

vi.mock("@multica/views/layout", () => ({
WorkspacePresencePrefetch: () => null,
}));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { getCurrentSlug, setCurrentWorkspace } from "@multica/core/platform";
import { isWorkspaceDeletePending } from "@multica/core/workspace/pending-delete";
import { useAuthStore } from "@multica/core/auth";
import { useWorkspaceSeen } from "@multica/views/workspace/use-workspace-seen";
import { WelcomeAfterOnboarding } from "@multica/views/workspace/welcome-after-onboarding";
import { WorkspacePresencePrefetch } from "@multica/views/layout";
import { SourceBackfillModal } from "@multica/views/onboarding";
import { useTabStore } from "@/stores/tab-store";
Expand Down Expand Up @@ -57,12 +56,11 @@ export function WorkspaceRouteLayout() {
const isAuthLoading = useAuthStore((s) => s.isLoading);
// While a WindowOverlay is open (onboarding, accept-invite, new-workspace),
// the underlying tab is still mounted in the React tree — so this layout
// and its WelcomeAfterOnboarding Modal would render UNDER the overlay.
// and its SourceBackfillModal would render UNDER the overlay.
// Because the modal uses a Portal that targets document.body, it ends up
// rendered LATER in the DOM and visually outranks the overlay's z-50.
// Suppress the modal whenever any overlay is active; the moment the
// overlay closes the welcome hook re-evaluates and pops if its store
// signal is still set.
// Suppress the modal whenever any overlay is active; it re-evaluates the
// moment the overlay closes.
const overlayActive = useWindowOverlayStore((s) => s.overlay !== null);

// Workspace routes require auth. App.tsx renders <DesktopLoginPage>
Expand Down Expand Up @@ -179,20 +177,12 @@ export function WorkspaceRouteLayout() {
<WorkspaceSlugProvider slug={workspaceSlug}>
<WorkspacePresencePrefetch />
<Outlet />
{/* Reads the welcome-store transient signal parked by
* OnboardingFlow.handleRuntimeNext. Suppressed while a WindowOverlay
* (onboarding / accept-invite / new-workspace) is open so the modal
* doesn't portal-jump in front of an active pre-workspace flow.
* Once the overlay closes the hook re-evaluates and pops the
* Modal — unless the store signal has already been consumed, in
* which case the hook renders null. */}
{!overlayActive && <WelcomeAfterOnboarding />}
{/* Source-attribution backfill: same Dialog the web shell mounts
* inside DashboardLayout. Desktop's WorkspaceRouteLayout doesn't
* wrap DashboardLayout, so the modal has to be wired in directly
* here. Same overlay-suppression rule as WelcomeAfterOnboarding —
* a portal-rendered Dialog at z-50 would otherwise sit above an
* active pre-workspace overlay. */}
* here. Suppressed while a WindowOverlay is open — a portal-rendered
* Dialog at z-50 would otherwise sit above an active pre-workspace
* overlay. */}
{!overlayActive && <SourceBackfillModal />}
</WorkspaceSlugProvider>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ function makeTeardown(
reportAuthSession: vi.fn(),
resetTabs: vi.fn(),
closeOverlay: vi.fn(),
resetWelcome: vi.fn(),
clearDaemonToken: vi.fn(async () => {}),
stopDaemon: vi.fn(async () => {}),
...overrides,
Expand All @@ -33,7 +32,6 @@ describe("tearDownOnLogout", () => {
expect(t.reportAuthSession).toHaveBeenCalledWith(null);
expect(t.resetTabs).toHaveBeenCalledOnce();
expect(t.closeOverlay).toHaveBeenCalledOnce();
expect(t.resetWelcome).toHaveBeenCalledOnce();
expect(t.clearDaemonToken).toHaveBeenCalledOnce();
expect(t.stopDaemon).toHaveBeenCalledOnce();
});
Expand Down Expand Up @@ -82,6 +80,5 @@ describe("tearDownOnSessionExpiry", () => {
expect(t.reportAuthSession).toHaveBeenCalledWith(null);
expect(t.resetTabs).toHaveBeenCalledOnce();
expect(t.closeOverlay).toHaveBeenCalledOnce();
expect(t.resetWelcome).toHaveBeenCalledOnce();
});
});
4 changes: 0 additions & 4 deletions apps/desktop/src/renderer/src/platform/session-teardown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ export interface SessionTeardown {
resetTabs: () => void;
/** Any pre-workspace overlay left open (invite, onboarding, …). */
closeOverlay: () => void;
/** The one-shot post-onboarding welcome signal. */
resetWelcome: () => void;
/** Credential the local daemon authenticates with. */
clearDaemonToken: () => Promise<unknown>;
/** The local daemon process itself. Resolves to an IPC result we ignore —
Expand All @@ -47,7 +45,6 @@ export async function tearDownOnLogout(t: SessionTeardown): Promise<void> {
t.reportAuthSession(null);
t.resetTabs();
t.closeOverlay();
t.resetWelcome();
try {
await t.clearDaemonToken();
} catch {
Expand Down Expand Up @@ -75,5 +72,4 @@ export function tearDownOnSessionExpiry(t: SessionTeardown): void {
t.reportAuthSession(null);
t.resetTabs();
t.closeOverlay();
t.resetWelcome();
}
22 changes: 4 additions & 18 deletions apps/desktop/src/renderer/src/platform/tab-coordinator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,10 @@ import {
} from "@/stores/tab-store";

/**
* Tab Coordinator (MUL-4741 Phase 2) — the ONLY writer of the single app
* router.
*
* Architecture: the tab store is the source of truth; the router is a
* projection of the active session's URL. The Coordinator subscribes to the
* store and *reconciles* the router to `activeSession.url`, stamping every
* navigation with a token. All entry points (tab bar clicks, adapter
* push/replace, shell back/forward, error recovery) mutate the store — none
* of them touch the router. This makes invariant 1 hold by construction:
*
* - Router location change WITH a pending token → expected, consume it.
* - Router location change WITHOUT a pending token → protocol error →
* bounded recovery (re-reconcile toward the session URL, capped).
*
* The router's own history is never used: the Coordinator always navigates
* with `replace`, and per-tab back/forward is a session operation over the
* session's virtual history stack. A POP therefore can only come from
* unforeseen code and is handled by the same protocol-error path.
* Project the active tab session onto the single app router. Navigation entry
* points mutate the store; only this coordinator writes the router, using replace
* and a pending token. Untokened location changes trigger bounded reconciliation.
* Per-tab virtual history owns back/forward; router POP is a protocol error.
*/

let router: DataRouter | null = null;
Expand Down
28 changes: 3 additions & 25 deletions apps/desktop/src/renderer/src/stores/tab-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,9 @@ import { createPersistStorage, defaultStorage } from "@multica/core/platform";
import { createSafeId } from "@multica/core/utils";
import { isReservedSlug } from "@multica/core/paths";

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
//
// MUL-4741 Phase 2: tabs are *sessions*, not routers. A TabSession is pure
// serializable state — URL, identity, virtual history, and a memento of
// restorable view state. The single app router is a projection of the active
// session, driven exclusively by the tab Coordinator (src/platform/
// tab-coordinator.ts) which subscribes to this store and reconciles the
// router to `activeSession.url` with a navigation token. Nothing in this
// file touches react-router.
// Tab sessions hold serializable URLs, identity, virtual history, and view
// mementos. The coordinator projects the active session onto the single router;
// this store does not operate the router.

/**
* One captured scroll position.
Expand Down Expand Up @@ -293,9 +285,7 @@ interface TabStore {
reset: () => void;
}

// ---------------------------------------------------------------------------
// Session identity helpers
// ---------------------------------------------------------------------------
//
// A tab's icon is NOT part of this model. It is derived from `tab.url` at
// render time via `routeIconForPath` (@multica/views/layout), which shares the
Expand All @@ -312,9 +302,7 @@ function extractWorkspaceSlug(path: string): string | null {
return first;
}

// ---------------------------------------------------------------------------
// URL helpers
// ---------------------------------------------------------------------------

/** Split a tab URL into pathname and the search+hash suffix. */
export function splitTabUrl(url: string): { pathname: string; suffix: string } {
Expand Down Expand Up @@ -405,9 +393,7 @@ export function sanitizeTabPath(path: string): string | null {
return path;
}

// ---------------------------------------------------------------------------
// Session factory
// ---------------------------------------------------------------------------

function createId(): string {
return createSafeId();
Expand Down Expand Up @@ -618,9 +604,7 @@ function normalizeBrowsingHistoryTitles(
return result;
}

// ---------------------------------------------------------------------------
// Group helpers
// ---------------------------------------------------------------------------

/**
* Build a group with its MRU order reconciled against the new (tabs,
Expand Down Expand Up @@ -711,9 +695,7 @@ function buildCloseOtherTabsResult(
};
}

// ---------------------------------------------------------------------------
// Store
// ---------------------------------------------------------------------------

export const useTabStore = create<TabStore>()(
persist(
Expand Down Expand Up @@ -1497,9 +1479,7 @@ function setHistoryIndex(
});
}

// ---------------------------------------------------------------------------
// Persisted shapes (for migration)
// ---------------------------------------------------------------------------

interface V1Tab {
id: string;
Expand Down Expand Up @@ -1711,9 +1691,7 @@ export function migrateV1ToV2(v1: Partial<V1Persisted>): V2Persisted {
return { activeWorkspaceSlug, byWorkspace };
}

// ---------------------------------------------------------------------------
// Selectors (convenience hooks)
// ---------------------------------------------------------------------------

/**
* Pure non-hook helper — useful from event handlers / effects that already
Expand Down
Loading
Loading