<script lang="ts">
import { onMount, untrack } from "svelte";
import { Application, type Arrangement } from "@wsb/uikit/surfaces/application";
import {
Button,
EmptyState,
Field,
IconButton,
Input,
Menu,
Panel,
Popover,
Segmented,
StatusItem,
type Crumb,
type MenuEntry,
} from "@wsb/uikit/components";
import StatusReadings from "./lib/components/layout/StatusReadings.svelte";
import TabContent from "./lib/components/workbench/TabContent.svelte";
import WorkshopBoards from "./lib/components/workbench/WorkshopBoards.svelte";
import WorkshopProjects from "./lib/components/workbench/WorkshopProjects.svelte";
import WorkshopMachine from "./lib/components/workbench/WorkshopMachine.svelte";
import OverviewSidebar from "./lib/components/sidebar/panels/OverviewSidebar.svelte";
import SessionsPanel from "./lib/components/sidebar/SessionsPanel.svelte";
import GistsSidebar from "./lib/components/sidebar/panels/GistsSidebar.svelte";
import ExtensionsSidebar from "./lib/components/sidebar/panels/ExtensionsSidebar.svelte";
import SettingsSidebar from "./lib/components/sidebar/panels/SettingsSidebar.svelte";
import ExtensionViewContainer from "./lib/components/sidebar/panels/ExtensionViewContainer.svelte";
import BottomPanel from "./lib/components/panel/BottomPanel.svelte";
import SearchPalette from "./lib/components/command-palette/CommandPalette.svelte";
import AppCommandPalette from "./lib/components/command-palette/AppCommandPalette.svelte";
import ThemePicker from "./lib/components/command-palette/ThemePicker.svelte";
import AboutModal from "./lib/components/modals/AboutModal.svelte";
import ShortcutsModal from "./lib/components/modals/ShortcutsModal.svelte";
import ResyncModal from "./lib/components/modals/ResyncModal.svelte";
import RefactorModal from "./lib/components/modals/RefactorModal.svelte";
import ConfirmDeleteModal from "./lib/components/modals/ConfirmDeleteModal.svelte";
import WorktreeSwitcher from "./lib/components/levels/WorktreeSwitcher.svelte";
import { activityBar } from "./lib/stores/activity-bar.svelte.js";
import { annotations } from "./lib/stores/annotations.svelte.js";
import { auth } from "./lib/stores/auth.svelte.js";
import { boards } from "./lib/stores/boards.svelte.js";
import { commands } from "./lib/stores/commands.svelte.js";
import { context } from "./lib/stores/context.svelte.js";
import { events } from "./lib/stores/events.svelte.js";
import { explorer } from "./lib/stores/explorer.svelte.js";
import { gists } from "./lib/stores/gists.svelte.js";
import { github } from "./lib/stores/github.svelte.js";
import { issues } from "./lib/stores/issues.svelte.js";
import { levels } from "./lib/stores/levels.svelte.js";
import { notes } from "./lib/stores/notes.svelte.js";
import { router } from "./lib/stores/router.svelte.js";
import { sessions } from "./lib/stores/sessions.svelte.js";
import { settings } from "./lib/stores/settings.svelte.js";
import { starred } from "./lib/stores/starred.svelte.js";
import { sync } from "./lib/stores/sync.svelte.js";
import { ui } from "./lib/stores/ui.svelte.js";
import { workbench } from "./lib/stores/workbench.svelte.js";
import { setAuthToken, getAuthToken, setServerUrl } from "./lib/api/client.js";
import { registerShortcuts } from "./lib/utils/keyboard.js";
const bench = workbench.bench;
const WORKSHOP_VIEWS = [
{ id: "boards", icon: "boards", label: "Boards" },
{ id: "projects", icon: "projects", label: "Projects" },
{ id: "gists", icon: "gists", label: "Gists" },
{ id: "machine", icon: "machine", label: "This machine" },
{ id: "settings", icon: "settings", label: "Settings" },
] as const;
const worktreeViews = $derived(
activityBar.sortedContainers.map((container) => ({
id: container.id,
label: container.title,
icon: container.source === "builtin" ? (container.icon as "explorer" | "sessions" | "extensions") : ("extensions" as const),
})),
);
const SIDE_MIN = 180;
const SIDE_MAX = 600;
const PANEL_MIN = 80;
const PANEL_MAX = 600;
const clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(max, Math.round(value)));
let token = $state("");
function authenticate() {
const given = token.trim();
if (!given) return;
setAuthToken(given);
window.location.reload();
}
function showAbout() {
if (ui.activeModal === "resync" && sync.syncing) return;
ui.activeModal = "about";
}
onMount(() => {
token = getAuthToken();
window.addEventListener("show-about", showAbout);
const stopShortcuts = registerShortcuts();
const stopWatching = levels.watch();
void annotations.load();
starred.load();
sync.loadStatus();
sync.loadStats();
sync.loadVersion();
sync.startPolling();
void sessions.ensureLoaded();
sessions.loadPlaces();
sessions.loadAgents();
const arrival = { place: router.place, params: router.params };
let stopFollowing: (() => void) | null = null;
void (async () => {
await levels.ensureLoaded();
await workbench.load();
await context.arrive(arrival.place, arrival.params);
stopFollowing = context.follow();
arrived = true;
})();
router.onarrive = (place, params) => void context.arrive(place, params);
return () => {
stopShortcuts();
stopWatching();
stopFollowing?.();
router.onarrive = null;
window.removeEventListener("show-about", showAbout);
sync.stopPolling();
events.disconnect();
};
});
let arrived = $state(false);
$effect(() => {
if (!arrived) return;
const place = context.place;
untrack(() => router.show(place));
});
let mirrorRead = false;
$effect(() => {
const worktree = context.worktree;
untrack(() => {
if (worktree === null) {
events.disconnect();
return;
}
settings.load();
void explorer.restoreFolders();
void notes.loadAll();
void issues.load();
void github.loadOverview();
if (!mirrorRead) {
mirrorRead = true;
void github.loadNotifications();
void github.loadRateLimit();
void gists.loadList();
}
events.connect();
});
});
$effect(() => {
const tab = bench.focusedTab;
untrack(() => {
if (!tab) return;
const input = tab.input;
if (input.kind === "gist") void gists.selectGist(input.gistId);
else if (input.kind === "gist-new") gists.clearSelection();
else if (input.kind === "issue") void issues.selectIssue(input.issue);
else if (input.kind === "file" && input.worktree === context.worktree) void explorer.ensureFile(input.path);
});
});
let chooser = $state<HTMLElement | null>(null);
const worktreeName = $derived(context.worktree ? (levels.worktree(context.worktree)?.name ?? context.worktree) : null);
const crumbs = $derived.by((): Crumb[] => {
const steps: Crumb[] = [];
if (context.board) {
const board = context.board;
steps.push({ value: "board", label: boards.find(board.owner, board.number)?.title ?? `${board.owner}/${board.number}`, icon: "board" });
}
if (context.project) steps.push({ value: "project", label: context.project, icon: "project" });
steps.push(worktreeName ? { value: "worktree", label: worktreeName, icon: "worktree", chooser: true } : { value: "worktree", label: "No worktree", icon: "worktree", chooser: true });
return steps;
});
function pickCrumb(value: string, anchor: HTMLElement) {
if (value === "worktree") chooser = anchor;
else if (value === "project" && context.project) workbench.openProject(context.project);
else if (value === "board" && context.board) workbench.openBoard(context.board.owner, context.board.number);
}
const ALL_WORKTREES = "\u0000all";
const chooserItems = $derived.by((): MenuEntry[] => {
const project = context.project ? levels.project(context.project) : null;
const siblings = project
? [{ kind: "label" as const, label: project.name }, ...project.worktrees.map((tree) => ({ value: tree.id, label: tree.name, icon: "worktree" as const, checked: tree.id === context.worktree }))]
: [];
return [...siblings, ...(siblings.length > 0 ? [{ kind: "separator" as const }] : []), { value: ALL_WORKTREES, label: "Every worktree…", icon: "search" }];
});
function choose(value: string) {
chooser = null;
if (value === ALL_WORKTREES) ui.activeModal = "worktreeSwitcher";
else void context.enter(value);
}
let restoring = $state(false);
async function undoDelete() {
const last = sessions.recentlyDeleted[sessions.recentlyDeleted.length - 1];
if (!last || restoring) return;
restoring = true;
try {
await sessions.restoreSession(last.id);
} finally {
restoring = false;
}
}
</script>
{#snippet workshopView(view: string)}
{#if view === "boards"}
<WorkshopBoards />
{:else if view === "projects"}
<WorkshopProjects />
{:else if view === "gists"}
<GistsSidebar />
{:else if view === "machine"}
<WorkshopMachine />
{:else if view === "settings"}
<SettingsSidebar />
{/if}
{/snippet}
{#snippet worktreeView(view: string)}
{#if context.worktree === null}
<Panel title="No worktree" handle={`worktree.none#${view}`}>
<EmptyState icon="worktree" title="No worktree" detail="Pick a project's worktree in the workshop to work in it." />
</Panel>
{:else}
{#key context.worktree}
{#if view === "explorer"}
<OverviewSidebar />
{:else if view === "sessions"}
<SessionsPanel />
{:else if view === "extensions"}
<ExtensionsSidebar />
{:else}
<ExtensionViewContainer containerId={view} />
{/if}
{/key}
{/if}
{/snippet}
{#snippet content(group: string, key: string)}
{@const found = bench.find(key)}
{#if found}
{#key key}
<TabContent {group} tab={found.tab} />
{/key}
{/if}
{/snippet}
{#snippet empty(_group: string)}
<EmptyState icon="files" title="Nothing open here" detail="Open a board or a project from the workshop, or a file from the worktree." />
{/snippet}
{#snippet contextActions()}
<IconButton handle="application.quick-open" icon="search" label="Search sessions, messages and files" onpress={() => void commands.execute("workbench.action.quickOpen")} />
<Segmented
handle="application.arrangement"
ariaLabel="Arrange the editor groups"
items={[{ value: "beside", label: "Side by side" }, { value: "stacked", label: "Stacked" }]}
selected={bench.arrangement}
onselect={(value) => bench.setArrangement(value as Arrangement)}
/>
<IconButton handle="application.toggle-panel" icon="panel-bottom" label="Toggle the panel" pressed={workbench.panelOpen} onpress={() => void commands.execute("workbench.action.togglePanel")} />
<IconButton handle="application.commands" icon="more" label="Show every command" onpress={() => void commands.execute("workbench.action.showCommands")} />
{/snippet}
{#snippet panelContent()}
{#key context.worktree}
<BottomPanel />
{/key}
{/snippet}
{#snippet statusStart()}
{#if context.worktree}
<StatusItem handle="application.status.worktree" icon="worktree" label={worktreeName ?? context.worktree} title="The worktree the right sidebar works in" onpress={() => { ui.activeModal = "worktreeSwitcher"; }} />
{#if levels.worktree(context.worktree)?.branch}
<StatusItem handle="application.status.branch" icon="branch" label={levels.worktree(context.worktree)!.branch!} />
{/if}
{:else}
<StatusItem handle="application.status.worktree" icon="worktree" label="No worktree" onpress={() => { ui.activeModal = "worktreeSwitcher"; }} />
{/if}
<StatusReadings at="start" />
{#if sessions.recentlyDeleted.length > 0}
<StatusItem handle="application.status.undo-delete" icon="history" tone="info" label={restoring ? "Restoring the session…" : "Session deleted · Undo"} onpress={() => void undoDelete()} />
{/if}
{/snippet}
{#snippet statusEnd()}
<StatusReadings at="end" />
{/snippet}
{#if auth.needsToken}
{#snippet tokenInput()}
<Input
handle="application.auth.token"
id="auth-token"
value={token}
mono
placeholder="Paste the auth token"
oninput={(value) => { token = value; }}
onkeydown={(event) => { if (event.key === "Enter") authenticate(); }}
/>
{/snippet}
<Panel title="Authentication required" handle="application.auth">
<Field label="Token" for="auth-token" hint="The token the server printed on its console, or the one on its settings page." control={tokenInput} />
<Button handle="application.auth.submit" variant="primary" label="Authenticate" disabled={!token.trim()} onpress={authenticate} />
<Button
handle="application.auth.reset"
variant="link"
label="Disconnect and reset"
onpress={() => {
setAuthToken("");
setServerUrl("");
auth.needsToken = false;
void levels.load();
}}
/>
</Panel>
{:else}
<Application
context={{ crumbs, onselect: pickCrumb, actions: contextActions }}
workshop={{
views: [...WORKSHOP_VIEWS],
active: workbench.workshopView,
width: workbench.workshopWidth,
minWidth: SIDE_MIN,
maxWidth: SIDE_MAX,
onselect: (view) => workbench.toggleView("workshop", view),
onresize: (delta) => { workbench.workshopWidth = clamp(workbench.workshopWidth + delta, SIDE_MIN, SIDE_MAX); },
view: workshopView,
}}
worktree={{
views: worktreeViews,
active: workbench.worktreeView,
width: workbench.worktreeWidth,
minWidth: SIDE_MIN,
maxWidth: SIDE_MAX,
onselect: (view) => workbench.toggleView("worktree", view),
onresize: (delta) => { workbench.worktreeWidth = clamp(workbench.worktreeWidth + delta, SIDE_MIN, SIDE_MAX); },
view: worktreeView,
// Remaking a view on every switch tore down and rebuilt thousands of
// explorer rows, and lost scroll positions and half-done work.
keep: true,
}}
centre={{
groups: bench.groups,
focused: bench.focused,
arrangement: bench.arrangement,
onfocus: (group) => bench.focus(group),
onactivate: (group, tab) => bench.activate(group, tab),
onkeep: (group, tab) => bench.keep(group, tab),
onclose: (group, tab) => bench.close(group, tab),
ondrop: (from, to) => bench.drop(from, to),
onresize: (index, delta, extent) => bench.resize(index, delta, extent),
content,
empty,
}}
panel={workbench.panelOpen && context.worktree !== null
? {
height: workbench.panelHeight,
minHeight: PANEL_MIN,
maxHeight: PANEL_MAX,
onresize: (delta) => { workbench.panelHeight = clamp(workbench.panelHeight + delta, PANEL_MIN, PANEL_MAX); },
content: panelContent,
}
: undefined}
status={{ start: statusStart, end: statusEnd }}
/>
{#if chooser}
<Popover anchor={chooser} ariaLabel="Choose a worktree" onclose={() => { chooser = null; }}>
<Menu handle="application.worktree-chooser" ariaLabel="Worktrees" items={chooserItems} onselect={choose} />
</Popover>
{/if}
{#if ui.activeModal === "about"}<AboutModal />{/if}
{#if ui.activeModal === "searchPalette"}<SearchPalette />{/if}
{#if ui.activeModal === "commandPalette"}<AppCommandPalette />{/if}
{#if ui.activeModal === "themePicker"}<ThemePicker />{/if}
{#if ui.activeModal === "shortcuts"}<ShortcutsModal />{/if}
{#if ui.activeModal === "refactor"}<RefactorModal />{/if}
{#if ui.activeModal === "resync"}<ResyncModal />{/if}
{#if ui.activeModal === "confirmDelete"}<ConfirmDeleteModal />{/if}
{#if ui.activeModal === "worktreeSwitcher"}<WorktreeSwitcher />{/if}
{/if}