Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
07a6b8a
feat: run cloned Next.js repos on webpack instead of Turbopack
rijulshrestha Aug 19, 2026
373f0d8
fix: apply the rolldown wasm binding only from Vite 8.2
rijulshrestha Aug 24, 2026
ed4871c
feat: render image files in an image viewer instead of raw bytes
rijulshrestha Aug 24, 2026
ba25384
feat: revamp the preview toolbar with hide and reload controls
rijulshrestha Aug 24, 2026
c60fa26
feat: add an editor settings menu with switchable TextMate themes
rijulshrestha Aug 24, 2026
7547c51
refactor: pass PortalState to Portal instead of forwarding its fields
rijulshrestha Sep 1, 2026
13fc393
refactor: gate agent boots on a config-declared credential instead of…
rijulshrestha Sep 1, 2026
b387850
refactor: share pane-drag logic
rijulshrestha Sep 1, 2026
29f2505
refactor: move the agent page lifecycle into an AgentSession controller
rijulshrestha Sep 2, 2026
185c054
refactor: extract AgentShell and its overlays from the agents route
rijulshrestha Sep 2, 2026
e1dadc7
refactor: run one IDE boot pipeline over a ProjectSource
rijulshrestha Sep 2, 2026
62d6c51
refactor: rebuild the QR panel around the code and a clickable address
rijulshrestha Sep 3, 2026
f3c7731
perf: lighten the wavy grid background animation
rijulshrestha Sep 7, 2026
bce5e05
fix: make the theme menu and zoom-reset legible to screen readers
rijulshrestha Sep 7, 2026
4f8c489
fix: stop the editor from corrupting binary files
rijulshrestha Sep 7, 2026
0c558b9
fix: re-read an open tab when a rename changes its file type
rijulshrestha Sep 7, 2026
3b61052
feat: cap images opened in the editor at 10 MB
rijulshrestha Sep 7, 2026
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
582 changes: 579 additions & 3 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "browsercode",
"private": true,
"version": "1.1.0",
"version": "1.2.0",
"type": "module",
"scripts": {
"dev": "vite dev",
Expand Down Expand Up @@ -39,9 +39,13 @@
"@iconify-json/mingcute": "^1.2.7",
"@iconify/svelte": "^5.2.1",
"@leaningtech/browserpod": "3.0.1",
"@shikijs/langs": "^4.4.3",
"@shikijs/monaco": "^4.4.3",
"@shikijs/themes": "^4.4.3",
"@sveltejs/adapter-static": "^3.0.10",
"fflate": "^0.8.3",
"monaco-editor": "^0.55.1",
"qrcode": "^1.5.4"
"qrcode": "^1.5.4",
"shiki": "^4.4.3"
}
}
75 changes: 75 additions & 0 deletions src/lib/agents/credential-gate.svelte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import type { CredentialSpec } from '$lib/config/tools';
import { describeError } from './boot';

/** `idle` renders no overlay; every other stage covers the terminal. */
export type GateStage = 'idle' | 'loading' | 'signin' | 'error';

export type CredentialGateOptions = {
/** A newly saved secret only reaches the CLI through a fresh launch. */
onRestart: () => void;
};

/**
* Drives the boot overlay for a CLI that declares a `CredentialSpec`. It surfaces a failed boot
* itself because the terminal `bootCLI` wrote that failure into sits behind this overlay.
*/
export class CredentialGate {
stage = $state<GateStage>('idle');
error = $state('');
/** Assumed until `begin()` reads storage, so the loading card does not flash a false prompt. */
hasCredential = $state(true);
/** Not a stage: the toolbar can open this over an idle gate, after a boot has finished. */
changeOpen = $state(false);

private resolveSignIn: ((value: string) => void) | null = null;

constructor(
private credential: CredentialSpec,
private options: CredentialGateOptions
) {}

get overlayVisible(): boolean {
return this.stage !== 'idle' || this.changeOpen;
}

/** Call before `bootCLI`, so the overlay is up before the image starts streaming. */
begin = (): void => {
this.hasCredential = this.credential.get() !== null;
this.stage = 'loading';
};

/**
* Blocks the launch until a secret is stored, rather than overlaying a CLI already running
* without one, which could not be handed it afterwards.
*/
beforeLaunch = async (): Promise<void> => {
if (!this.credential.get()) {
this.stage = 'signin';
this.credential.set(await new Promise<string>((resolve) => (this.resolveSignIn = resolve)));
this.hasCredential = true;
}
this.stage = 'idle';
};

submit = (value: string): void => {
this.resolveSignIn?.(value);
};

saveAndRestart = (value: string): void => {
this.credential.set(value);
this.options.onRestart();
};

reportBootFailure = (error: unknown): void => {
this.error = describeError(error);
this.stage = 'error';
};

openChange = (): void => {
this.changeOpen = true;
};

closeChange = (): void => {
this.changeOpen = false;
};
}
98 changes: 98 additions & 0 deletions src/lib/agents/session.svelte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import type { PortalUpdate } from '$lib/pod/portals';
import {
cliConfigs,
isEnabledTool,
resolveToolId,
toolItems,
type CredentialSpec,
type ToolId,
type ToolItem
} from '$lib/config/tools';
import {
installLeaveGuard,
markIntentionalNavigation,
navigateWithLeaveGuard
} from '$lib/stores/leaveWarning.svelte';
import { requestSingleTabLock } from '$lib/utils/tabLock';
import { bootCLI } from './boot';
import { CredentialGate } from './credential-gate.svelte';

/** `pending` covers the wait on the lock, which resolves after a grace period rather than at once. */
export type LockState = 'pending' | 'held' | 'taken';

/** Owns one agent CLI session. */
export class AgentSession {
readonly id: ToolId;
readonly tool: ToolItem;
readonly credential: CredentialSpec | undefined;
/** Null for a CLI that declares no credential; those boot with no overlay at all. */
readonly gate: CredentialGate | null;

lock = $state<LockState>('pending');

private releaseLock: () => void = () => {};
private disposeLeaveGuard: () => void = () => {};

constructor(requestedTool: string | undefined) {
this.id = resolveToolId(requestedTool);
// resolveToolId only ever returns an id that is in toolItems, so this always resolves.
this.tool = toolItems.find((item) => item.id === this.id)!;
// Mirrors bootCLI's own resolution, so the gate matches the config that actually launches.
this.credential = (cliConfigs[this.id] ?? cliConfigs.claude).credential;
this.gate = this.credential
? new CredentialGate(this.credential, { onRestart: this.restart })
: null;
}

async boot(
terminalEl: HTMLElement,
onPortalUpdate: (update: PortalUpdate) => void
): Promise<void> {
const lock = requestSingleTabLock(`agent-session:${this.id}`);
this.releaseLock = lock.release;

if (!(await lock.acquired)) {
this.lock = 'taken';
return;
}
this.lock = 'held';

// Only warn on tab close/refresh/back-button once there is work here to lose.
this.disposeLeaveGuard = installLeaveGuard();

// Covers pod boot, the image streaming in, and any warm-up probe.
this.gate?.begin();

try {
await bootCLI(
this.id,
terminalEl,
onPortalUpdate,
this.gate ? { beforeLaunch: this.gate.beforeLaunch } : undefined
);
} catch (error) {
// bootCLI already logged this and wrote it into the terminal, but a gated boot's overlay
// covers that terminal until it is told to show the failure.
this.gate?.reportBootFailure(error);
}
}

/** The CLI reads its credential at launch, so both a new secret and a retry need a fresh load. */
restart = (): void => {
markIntentionalNavigation();
window.location.reload();
};

/** Always confirms: there is a live session here to tear down. */
switchTo = (id: string): void => {
if (isEnabledTool(id)) navigateWithLeaveGuard(`/agents/${id}`, true);
};

/** No confirmation, unlike `switchTo` */
leave = (): void => navigateWithLeaveGuard('/agents', false);

shutdown(): void {
this.disposeLeaveGuard();
this.releaseLock();
}
}
Loading