Skip to content

Quick Start

This guide creates a single browser-side session with the first available provider. It also shows the minimum lifecycle handling needed for a usable UI.

Create the client after your application is running in the browser:

import {
Pedelec,
type PedelecError,
type PedelecSession,
} from "@kaoruisaac/pedelec";
const pedelec = new Pedelec();

A page should normally share this client instead of creating a new instance for every button click.

const approval = await pedelec.getApprovalStatus();
if (!approval.installed) {
throw new Error("Install or enable the Pedelec Chrome Extension.");
}
const providers = await pedelec.listProviders();
const provider = providers.find((item) => item.available);
if (!provider) {
throw new Error("Pedelec Desktop could not start or connect; open it manually or repair installation, then configure a provider.");
}

The site does not need to be approved just to read approval status. Session creation or resume starts the approval flow when approval is still required.

const session = await pedelec.createSession({
provider: provider.code,
});

When only provider is supplied, the SDK uses effort level default; Core resolves that profile from Desktop Settings. You can pass effortLevel: "low" or "high" with or without an explicit provider. Ollama requires a model in the selected profile.

let assistantText = "";
const unsubscribeChatDelta = session.onChatDelta((delta, ctx) => {
assistantText += delta;
renderAssistantMessage(assistantText);
console.debug("turn", ctx.turnId, "received", delta.length, "characters");
});
const unsubscribeChat = session.onChat((text, ctx) => {
console.debug("completed message", ctx.turnId, text);
});
const unsubscribeStatus = session.onStatus((status, ctx) => {
renderSessionStatus(status);
setComposerDisabled(status !== "idle");
console.debug(ctx.previousStatus, "", status, ctx.source);
});
const unsubscribeError = session.onError((error, ctx) => {
renderError(`${error.code}: ${error.message}`);
console.error(ctx.type, error.details);
});
const unsubscribeEnded = session.onEnded(() => {
setComposerDisabled(true);
});

onStatus() only fires when the status changes. Use session.getStatus() for the initial state.

try {
assistantText = "";
setComposerDisabled(true);
await session.sendText("Explain what this application can do.");
console.log("The agent turn is complete.");
} catch (error) {
const pedelecError = error as PedelecError;
renderError(`${pedelecError.code}: ${pedelecError.message}`);
} finally {
if (session.getStatus() === "idle") {
setComposerDisabled(false);
}
}

sendText() resolves after Core emits the matching semantic operation completion. It does not resolve merely because the request reached the transport or the session reported idle.

A session accepts only one active turn. Disable the send button while a turn is active and still handle SESSION_BUSY in case two application paths race.

await session.end();
unsubscribeChatDelta();
unsubscribeChat();
unsubscribeStatus();
unsubscribeError();
unsubscribeEnded();

end() is idempotent on an already-ended session. While ended, the same handle cannot accept another message until session.resume() successfully returns it to idle.

import {
Pedelec,
type PedelecError,
type PedelecSession,
} from "@kaoruisaac/pedelec";
let activeSession: PedelecSession | null = null;
let disposeSessionHandlers: (() => void) | null = null;
export async function connectAndRun(prompt: string) {
const pedelec = new Pedelec();
const approval = await pedelec.getApprovalStatus();
if (!approval.installed) {
throw new Error("Pedelec extension is unavailable.");
}
const providers = await pedelec.listProviders();
const provider = providers.find((item) => item.available);
if (!provider) {
throw new Error("No provider is available.");
}
const session = await pedelec.createSession({
provider: provider.code,
});
activeSession = session;
let text = "";
const offChatDelta = session.onChatDelta((delta) => {
text += delta;
document.querySelector("#answer")!.textContent = text;
});
const offStatus = session.onStatus((status) => {
document.querySelector("#status")!.textContent = status;
});
const offError = session.onError((error) => {
document.querySelector("#error")!.textContent =
`${error.code}: ${error.message}`;
});
const offEnded = session.onEnded(() => {
document.querySelector("#status")!.textContent = "ended";
});
disposeSessionHandlers = () => {
offChatDelta();
offStatus();
offError();
offEnded();
};
try {
await session.sendText(prompt);
} catch (error) {
const value = error as PedelecError;
console.error(value.code, value.message, value.details);
throw error;
}
}
export async function disconnect() {
const session = activeSession;
activeSession = null;
try {
await session?.end();
} finally {
disposeSessionHandlers?.();
disposeSessionHandlers = null;
}
}

A production UI should also:

  • present approval instructions rather than throwing generic errors;
  • let users choose a provider and provider-independent effort level when the product needs a local override;
  • define tools for page-specific actions;
  • preserve or deliberately discard sessionId based on lifecycle requirements; and
  • handle extension, native host, Core, and provider failures separately.

Continue with Origin approval and connection state or start defining browser-side tools.