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
7 changes: 5 additions & 2 deletions plugins/web-ui/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1653,11 +1653,13 @@ const apiRoutes: readonly WebRoute[] = [
method: "GET",
path: "/api/loops/:id/items/:itemId",
handle: async (c) => {
const { res, user, params } = c;
const { res, user, params, url } = c;
const qs = new URLSearchParams({ principalId: user });
if (url.searchParams.get("refreshSource") === "1") qs.set("refreshSource", "1");
return relayCore(
res,
"GET",
`/v1/loops/${encodeURIComponent(params.id!)}/items/${encodeURIComponent(params.itemId!)}?principalId=${encodeURIComponent(user)}`,
`/v1/loops/${encodeURIComponent(params.id!)}/items/${encodeURIComponent(params.itemId!)}?${qs.toString()}`,
);
},
},
Expand Down Expand Up @@ -1686,6 +1688,7 @@ const apiRoutes: readonly WebRoute[] = [
const { res, url, user } = c;
const scopeId = url.searchParams.get("scopeId") || `personal:${user}`;
const qs = new URLSearchParams({ principalId: user, scopeId });
if (url.searchParams.get("account") === "company") qs.set("account", "company");
return relayCore(res, "GET", `/v1/runtime-config?${qs.toString()}`);
},
},
Expand Down
8 changes: 4 additions & 4 deletions plugins/web-ui/src/composer-loadout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,17 +120,17 @@ export function modelLoadoutOptions(

const LOADOUT_STORAGE_KEY = "web-ui:loadout";

export function loadLoadout(): LoadoutEntry[] {
export function loadLoadout(key = LOADOUT_STORAGE_KEY): LoadoutEntry[] {
try {
return parseLoadout(localStorage.getItem(LOADOUT_STORAGE_KEY));
return parseLoadout(localStorage.getItem(key));
} catch {
return [];
}
}

export function saveLoadout(entries: LoadoutEntry[]): void {
export function saveLoadout(entries: LoadoutEntry[], key = LOADOUT_STORAGE_KEY): void {
try {
localStorage.setItem(LOADOUT_STORAGE_KEY, JSON.stringify(entries.slice(0, LOADOUT_CAP)));
localStorage.setItem(key, JSON.stringify(entries.slice(0, LOADOUT_CAP)));
} catch {
return;
}
Expand Down
99 changes: 85 additions & 14 deletions plugins/web-ui/src/composer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { appEditSlug } from "./app-edit";
import { getRuntimeConfig, loadRuntimeConfig, saveRuntimeConfig, subscribeRuntimeConfig } from "./runtime-config-store";
import {
runtimeConfigKey,
getRuntimeConfig,
loadRuntimeConfig,
saveRuntimeConfig,
subscribeRuntimeConfig,
} from "./runtime-config-store";
import type { Agent, AgentMessage } from "@earendil-works/pi-agent-core";
import { createFileDragState } from "./file-drag";
import type { Attachment } from "@earendil-works/pi-web-ui";
Expand Down Expand Up @@ -173,7 +179,24 @@ export function resyncModelSelection(): void {
}
}

export function createComposerSurface(ctx: ConvCtx): ComposerSurface {
export interface ComposerSubmission {
model: string;
harness: string;
thinkingLevel?: string;
fastMode?: boolean;
attachments?: CoreAttachment[];
}

export interface ComposerOptions {
prepareSubmit?: () => (text: string, options: ComposerSubmission) => Promise<void>;
placeholder?: string;
preferenceKey?: string;
runtimeAccount?: "company";
}

export function createComposerSurface(ctx: ConvCtx, options: ComposerOptions = {}): ComposerSurface {
let submitting = false;
const loadoutKey = options.preferenceKey ? `web-ui:loadout:${options.preferenceKey}` : undefined;
const refreshAccount = () => {
loadoutRestored = false;
void refreshRuntimeSelection(ctx.chat.state.scopeId, ctx.chat.state.agent ?? undefined, true);
Expand All @@ -192,6 +215,7 @@ export function createComposerSurface(ctx: ConvCtx): ComposerSurface {

function isUnsentNewChat(): boolean {
return (
!options.prepareSubmit &&
ctx.chat.state.sessionId === null &&
!(ctx.chat.state.agent?.state.messages ?? []).some((m) => !(m as { opener?: boolean }).opener)
);
Expand All @@ -205,7 +229,7 @@ export function createComposerSurface(ctx: ConvCtx): ComposerSurface {

function clearActiveDraft(): void {
if (ctx.chat.state.threadRef) clearDraft(ctx.chat.state.threadRef);
if (ctx.chat.state.sessionId === null) clearDraft(newChatDraftKey(appState.me?.user));
if (!options.prepareSubmit && ctx.chat.state.sessionId === null) clearDraft(newChatDraftKey(appState.me?.user));
}

const composerState = {
Expand Down Expand Up @@ -294,7 +318,7 @@ export function createComposerSurface(ctx: ConvCtx): ComposerSurface {
}

function scopeKey(): string | null {
return runtimeScopeKey(ctx.chat.state.scopeId);
return runtimeConfigKey(runtimeScopeKey(ctx.chat.state.scopeId), options.runtimeAccount);
}

function currentModelOption(): ModelOption | undefined {
Expand All @@ -304,7 +328,8 @@ export function createComposerSurface(ctx: ConvCtx): ComposerSurface {

async function refreshRuntimeSelection(scopeId: string | null, agent?: Agent, refresh = false): Promise<void> {
const request = ++runtimeRequest;
const key = runtimeScopeKey(scopeId);
const actualScope = runtimeScopeKey(scopeId);
const key = runtimeConfigKey(actualScope, options.runtimeAccount);
const identity = `${key}:${ctx.chat.state.threadRef}`;
const changedIdentity = identity !== runtimeIdentity;
if (changedIdentity) {
Expand Down Expand Up @@ -334,7 +359,7 @@ export function createComposerSurface(ctx: ConvCtx): ComposerSurface {
});
composerState.error = "";
syncRuntimeSelection(agent);
const config = key === null ? null : await loadRuntimeConfig(key, refresh);
const config = actualScope === null ? null : await loadRuntimeConfig(actualScope, refresh, options.runtimeAccount);
if (request !== runtimeRequest) return;
if (!config) composerState.error = "Could not load runtime settings.";
if (config && !loadoutRestored) {
Expand All @@ -345,7 +370,7 @@ export function createComposerSurface(ctx: ConvCtx): ComposerSurface {
}

function restoreLoadoutSelection(): void {
loadout = loadLoadout();
loadout = loadLoadout(loadoutKey);
let selected = currentModelOption();
const threadRef = ctx.chat.state.threadRef;
if (selected && threadRef && !threadModelPicks.has(threadRef)) {
Expand Down Expand Up @@ -388,6 +413,10 @@ export function createComposerSurface(ctx: ConvCtx): ComposerSurface {
agent: Agent,
preserveSelection = false,
): Promise<void> {
if (options.prepareSubmit) {
if (change.harnessId && change.modelId) selectModel(`${change.harnessId}:${change.modelId}`, agent);
return;
}
const request = ++runtimeRequest;
const scopeId = scopeKey();
if (!scopeId) return;
Expand Down Expand Up @@ -468,7 +497,9 @@ export function createComposerSurface(ctx: ConvCtx): ComposerSurface {
const runtimePending = activeRuntimeConfig === null;
const inputBlocked = runtimePending || ctx.chat.state.resolvingApprovals.size > 0 || blockingPauses.length > 0;
const attachingDisabled = inputBlocked;
let placeholder = appEditSlug(ctx.chat.state.threadRef, appState.me?.user) ? "Describe a change…" : "Ask anything";
let placeholder =
options.placeholder ??
(appEditSlug(ctx.chat.state.threadRef, appState.me?.user) ? "Describe a change…" : "Ask anything");
if (inputBlocked) placeholder = runtimePending ? "Loading runtime…" : "Approve or deny to continue";
else if (agent.state.isStreaming) placeholder = "Queue a message for after this turn…";
let composerNotice: TemplateResult | typeof nothing = nothing;
Expand Down Expand Up @@ -497,7 +528,7 @@ export function createComposerSurface(ctx: ConvCtx): ComposerSurface {
>
${header} ${slashMenu(agent)}
${
activeRuntimeConfig?.upgradeAvailable
!options.prepareSubmit && activeRuntimeConfig?.upgradeAvailable
? html`<div class="runtime-upgrade">
<span
>The org now recommends
Expand Down Expand Up @@ -913,7 +944,7 @@ export function createComposerSurface(ctx: ConvCtx): ComposerSurface {
</div>`;
}

let loadout = loadLoadout();
let loadout = loadLoadout(loadoutKey);
const modelPicker = createModelPicker<Agent>({
host: () => ctx.chat.state.host,
redraw: () => ctx.chat.drawActiveChat(),
Expand All @@ -923,7 +954,7 @@ export function createComposerSurface(ctx: ConvCtx): ComposerSurface {
activeEntry: activeLoadoutEntry,
saveEntries: (entries) => {
loadout = entries;
saveLoadout(entries);
saveLoadout(entries, loadoutKey);
},
apply: applyLoadout,
add: addLoadoutEntry,
Expand All @@ -932,6 +963,8 @@ export function createComposerSurface(ctx: ConvCtx): ComposerSurface {
toggleFastMode,
effectiveFastMode,
changeDefault: changeScopeRuntime,
showDefaultAction: !options.prepareSubmit,
showInheritAction: !options.prepareSubmit,
});
const placeLoadout = modelPicker.place;

Expand Down Expand Up @@ -959,7 +992,7 @@ export function createComposerSurface(ctx: ConvCtx): ComposerSurface {
}

function seededLoadout(selected: ModelOption): LoadoutEntry[] {
const latest = loadLoadout();
const latest = loadLoadout(loadoutKey);
if (latest.length) loadout = latest;
const active = activeLoadoutEntry(selected);
if (!loadout.length) {
Expand All @@ -976,7 +1009,7 @@ export function createComposerSurface(ctx: ConvCtx): ComposerSurface {

function rememberActiveTweaks(selected: ModelOption): void {
loadout = upsertLoadout(seededLoadout(selected), activeLoadoutEntry(selected));
saveLoadout(loadout);
saveLoadout(loadout, loadoutKey);
}

function applyLoadout(entry: LoadoutEntry, agent: Agent): void {
Expand All @@ -990,7 +1023,7 @@ export function createComposerSurface(ctx: ConvCtx): ComposerSurface {
composerState.effortLevel = normalized.effort;
composerState.fastMode = normalized.fast;
loadout = upsertLoadout(loadout, activeLoadoutEntry(option));
saveLoadout(loadout);
saveLoadout(loadout, loadoutKey);
modelPicker.resetSection();
composerState.openMenu = wasOpen ? "loadout" : null;
ctx.chat.drawActiveChat(agent);
Expand Down Expand Up @@ -1177,6 +1210,7 @@ export function createComposerSurface(ctx: ConvCtx): ComposerSurface {
return (
Boolean(composerState.draft.trim() || composerState.attachments.length) &&
!composerState.processingFiles &&
!submitting &&
getRuntimeConfig(scopeKey()) !== null &&
ctx.chat.state.resolvingApprovals.size === 0 &&
!ctx.chat.hasUnresolvedApproval()
Expand Down Expand Up @@ -1418,6 +1452,36 @@ export function createComposerSurface(ctx: ConvCtx): ComposerSurface {
if (composerState.pasteView) closePasteView(agent);
if (agent.state.isStreaming) return queueDraft(agent);
const text = composerState.draft.trim();
if (!text && composerState.attachments.length === 0) return;
const submit = options.prepareSubmit?.();
if (submit) {
const selected = currentModelOption()!;
const attachments = [...composerState.attachments];
const selection: ComposerSubmission = {
model: selected.model.id,
harness: selected.harnessId,
thinkingLevel: composerState.effortLevel,
...(harnessSupportsFastMode(selected.harnessId)
? { fastMode: modelSupportsFastMode(scopeKey(), selected.model.id) && effectiveFastMode() }
: {}),
};
submitting = true;
clearActiveDraft();
resetComposer();
ctx.chat.drawActiveChat(agent);
try {
const { uploaded, skipped } = await uploadAttachments(attachments);
if (skipped.length) throw new Error(skipped.map((file) => file.note).join(" "));
await submit(text, { ...selection, ...(uploaded.length ? { attachments: uploaded } : {}) });
} catch (error) {
restoreStagedOnFailure(text, attachments, errMessage(error, "Could not send message."));
persistDraft();
} finally {
submitting = false;
ctx.chat.drawActiveChat(agent);
}
return;
}
if (ctx.chat.state.threadRef) {
bumpSessionActivity(ctx.chat.state.threadRef);
ctx.chat.state.pendingSend = ctx.chat.state.threadRef;
Expand Down Expand Up @@ -1850,6 +1914,13 @@ export function createComposerSurface(ctx: ConvCtx): ComposerSurface {

return {
state: composerState,
submit: async (instruction = "") => {
if (submitting || !getRuntimeConfig(scopeKey())) return;
const agent = ctx.chat.state.agent;
if (!agent) return;
if (instruction) composerState.draft = [composerState.draft.trim(), instruction].filter(Boolean).join("\n\n");
await sendPrompt(agent);
},
restageAttachments,
composerForm,
composerApprovalPanel,
Expand Down
1 change: 1 addition & 0 deletions plugins/web-ui/src/conv-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ interface ComposerState {

export interface ComposerSurface {
composerApprovalPanel(approvals: PendingApproval[]): TemplateResult;
submit(instruction?: string): Promise<void>;
restageAttachments(attachments: Attachment[], note: string): void;
state: ComposerState;
composerForm(agent: Agent, header?: TemplateResult | typeof nothing): TemplateResult;
Expand Down
7 changes: 5 additions & 2 deletions plugins/web-ui/src/core-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -755,9 +755,12 @@ export interface RuntimeConfig {
fastModeModelIds?: string[];
}

export async function fetchRuntimeConfig(scopeId?: string | null): Promise<RuntimeConfig | null> {
export async function fetchRuntimeConfig(scopeId?: string | null, account?: "company"): Promise<RuntimeConfig | null> {
try {
const query = scopeId ? `?scopeId=${encodeURIComponent(scopeId)}` : "";
const params = new URLSearchParams();
if (scopeId) params.set("scopeId", scopeId);
if (account) params.set("account", account);
const query = params.size ? `?${params}` : "";
return await api<RuntimeConfig>(`/api/runtime-config${query}`);
} catch (e) {
swallow("web-ui: fetch runtime config", e);
Expand Down
Loading
Loading