feat: workspace→session picker and /model switch (v0.3.0)

Align Telegram session list with Web via apiProxy (exclude archived), resume cold sessions on bind, and allow model selection from the phone.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
owo
2026-08-14 18:46:56 +08:00
co-authored by Cursor
parent 9bae1a3702
commit ef0e7f34e3
18 changed files with 1486 additions and 108 deletions
+52
View File
@@ -0,0 +1,52 @@
import { randomUUID } from 'node:crypto';
function unwrap(res) {
const r = res;
if (r?.result?.ok === true)
return r.result.value;
return undefined;
}
async function call(fn, payload) {
if (typeof fn !== 'function')
throw new Error('apiProxy sessions API unavailable');
return fn({ rpcId: randomUUID(), payload });
}
export async function loadSessionModels(ctx, sessionId) {
const api = ctx.apiProxy;
const raw = await call(api?.sessions?.models, { sessionId });
const value = unwrap(raw);
if (!value)
throw new Error('failed to load session models');
const options = [];
for (const group of value.groups ?? []) {
for (const m of group.models ?? []) {
options.push({
provider: group.id,
model: m.id,
label: `${group.name}/${m.name}`,
efforts: m.reasoning?.efforts?.map((e) => ({ id: e.id, name: e.name })),
});
}
}
return {
current: value.current,
routable: value.routable,
options,
};
}
export async function selectSessionModel(ctx, sessionId, selection) {
const api = ctx.apiProxy;
const raw = await call(api?.sessions?.selectModel, {
sessionId,
provider: selection.provider,
model: selection.model,
reasoningEffort: selection.reasoningEffort,
});
const value = unwrap(raw);
if (!value?.selected)
throw new Error('failed to select model');
return value.selected;
}
export function formatModel(sel) {
const base = `${sel.provider}/${sel.model}`;
return sel.reasoningEffort ? `${base} (${sel.reasoningEffort})` : base;
}