feat(webui): switch model presets from the composer (#5077)

This commit is contained in:
chengyongru
2026-07-24 17:34:26 +08:00
committed by GitHub
parent 9aab94c766
commit 5be176a6a0
16 changed files with 991 additions and 138 deletions
+45 -8
View File
@@ -88,16 +88,16 @@ export type StreamError =
type ErrorHandler = (error: StreamError) => void;
interface PendingNewChat {
resolve: (chatId: string) => void;
interface PendingRequest<T> {
resolve: (value: T) => void;
reject: (err: Error) => void;
timer: ReturnType<typeof setTimeout>;
}
interface PendingTranscription {
resolve: (text: string) => void;
reject: (err: Error) => void;
timer: ReturnType<typeof setTimeout>;
const SYSTEM_COMMAND_TURN_PREFIX = "webui-system:";
export function isSystemCommandTurnId(value: string | null | undefined): value is string {
return typeof value === "string" && value.startsWith(SYSTEM_COMMAND_TURN_PREFIX);
}
export interface NanobotClientOptions {
@@ -136,8 +136,9 @@ export class NanobotClient {
private runStartedAtByChatId = new Map<string, number>();
/** Latest ``goal_state`` snapshot per ``chat_id`` (multi-session isolation). */
private goalStateByChatId = new Map<string, GoalStateWsPayload>();
private pendingNewChat: PendingNewChat | null = null;
private pendingTranscriptions = new Map<string, PendingTranscription>();
private pendingNewChat: PendingRequest<string> | null = null;
private pendingTranscriptions = new Map<string, PendingRequest<string>>();
private pendingSystemCommands = new Map<string, PendingRequest<void>>();
// Frames queued while the socket is not yet OPEN
private sendQueue: Outbound[] = [];
private reconnectAttempts = 0;
@@ -407,6 +408,19 @@ export class NanobotClient {
this.queueSend(frame);
}
sendSystemCommand(chatId: string, command: string, timeoutMs = 5_000): Promise<void> {
const normalized = command.trim();
const turnId = `${SYSTEM_COMMAND_TURN_PREFIX}${crypto.randomUUID()}`;
return new Promise<void>((resolve, reject) => {
const timer = setTimeout(() => {
this.pendingSystemCommands.delete(turnId);
reject(new Error("system command timed out"));
}, timeoutMs);
this.pendingSystemCommands.set(turnId, { resolve, reject, timer });
this.sendMessage(chatId, normalized, undefined, { turnId });
});
}
setWorkspaceScope(chatId: string, workspaceScope: WorkspaceScopePayload): void {
this.knownChats.add(chatId);
this.queueSend({
@@ -462,6 +476,16 @@ export class NanobotClient {
console.log("[nanobot ws inbound]", summarizeInboundWsPayload(parsed));
}
const turnId = "turn_id" in parsed && typeof parsed.turn_id === "string"
? parsed.turn_id
: null;
if (isSystemCommandTurnId(turnId)) {
if (parsed.event === "message" || parsed.event === "turn_end") {
this.resolveSystemCommand(turnId);
}
return;
}
if (parsed.event === "ready") {
this.readyChatId = parsed.chat_id;
this.knownChats.add(parsed.chat_id);
@@ -578,6 +602,11 @@ export class NanobotClient {
this.pendingNewChat = null;
}
this.rejectAllTranscriptions("socket closed");
for (const pending of this.pendingSystemCommands.values()) {
clearTimeout(pending.timer);
pending.reject(new Error("socket closed"));
}
this.pendingSystemCommands.clear();
// Surface structured reasons *before* reconnect logic so the UI can
// display the error even while the client transparently reconnects.
// Browsers populate ``CloseEvent.code`` with the wire-level close code;
@@ -634,6 +663,14 @@ export class NanobotClient {
}
}
private resolveSystemCommand(turnId: string): void {
const pending = this.pendingSystemCommands.get(turnId);
if (!pending) return;
clearTimeout(pending.timer);
this.pendingSystemCommands.delete(turnId);
pending.resolve();
}
private scheduleReconnect(): void {
this.clearRunStatusesForReconnect();
this.setStatus("reconnecting");