feat(web): replace the @ mention with an /agent command, and stage both switches until send (#122)
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
*
|
||||
* The key must include userId (#68): if the same browser logs into different accounts in
|
||||
* succession and the key only contains the Project/Session ID, the later user would recover the
|
||||
* previous user's text, Workspace, model selection, and @ target — a cross-account information leak.
|
||||
* previous user's text, Workspace, model selection, and handoff target — a cross-account information leak.
|
||||
*/
|
||||
import type { ApprovalMode } from "@prismshadow/penguin-server/api";
|
||||
|
||||
@@ -25,8 +25,17 @@ export interface DraftCache {
|
||||
* (product hasn't shipped, so no migration is done).
|
||||
*/
|
||||
modelRef?: { provider: string; modelId: string };
|
||||
/** The @ handoff target (chip) at the front of the input box: resolved again by id on restore, dropped if no longer valid. */
|
||||
/** The `/agent` handoff target (chip) at the front of the input box: resolved again by id on restore, dropped if no longer valid. */
|
||||
handoffAgentId?: string;
|
||||
/**
|
||||
* The `/model` switch target (the other switch chip; a paired reference, same shape as
|
||||
* modelRef above): cached for exactly the same reason as handoffAgentId — the composer text
|
||||
* is cached and ChatInput remounts on every session switch, so a chip left in component state
|
||||
* would vanish while the text it belongs to came back, and the next Enter would post to the
|
||||
* current session on the old model. Resolved again against the model list on restore and
|
||||
* dropped when that model is no longer available.
|
||||
*/
|
||||
switchModelRef?: { provider: string; modelId: string };
|
||||
/**
|
||||
* Preselected skill names (written by the quick-invoke action on the Skill library page):
|
||||
* used as the initial selection when ChatInput mounts, then trimmed to remove names not in the
|
||||
@@ -46,10 +55,22 @@ export interface DraftStorage {
|
||||
export const draftKey = (userId: string, projectId: string): string =>
|
||||
`penguin.chatDraft.${userId}.${projectId}`;
|
||||
|
||||
/** Cache key for an existing session's input area: one per "user × Session" (only stores text and @ target; everything else is locked to the Session). */
|
||||
/** Cache key for an existing session's input area: one per "user × Session" (only stores text and the handoff target; everything else is locked to the Session). */
|
||||
export const sessionDraftKey = (userId: string, sessionId: string): string =>
|
||||
`penguin.chatDraft.session.${userId}.${sessionId}`;
|
||||
|
||||
/**
|
||||
* A cached model reference must be a paired `{ provider, modelId }` object; anything else — the
|
||||
* old string-typed modelId, a half reference, a non-object — yields undefined and the field is
|
||||
* dropped. Shared by the two model fields (the draft's selection and the staged `/model` switch).
|
||||
*/
|
||||
function parseModelRef(value: unknown): { provider: string; modelId: string } | undefined {
|
||||
if (typeof value !== "object" || value === null) return undefined;
|
||||
const r = value as Record<string, unknown>;
|
||||
if (typeof r.provider !== "string" || typeof r.modelId !== "string") return undefined;
|
||||
return { provider: r.provider, modelId: r.modelId };
|
||||
}
|
||||
|
||||
/** Parses and validates raw JSON field-by-field: null / malformed JSON / non-object / invalid fields are all dropped. */
|
||||
export function parseDraft(raw: string | null): DraftCache {
|
||||
if (!raw) return {};
|
||||
@@ -61,15 +82,11 @@ export function parseDraft(raw: string | null): DraftCache {
|
||||
if (typeof o.text === "string") out.text = o.text;
|
||||
if (typeof o.agentId === "string") out.agentId = o.agentId;
|
||||
if (typeof o.workspace === "string") out.workspace = o.workspace;
|
||||
// The model reference must be a paired { provider, modelId } object; the old string-typed
|
||||
// modelId and any malformed shape are dropped.
|
||||
if (typeof o.modelRef === "object" && o.modelRef !== null) {
|
||||
const r = o.modelRef as Record<string, unknown>;
|
||||
if (typeof r.provider === "string" && typeof r.modelId === "string") {
|
||||
out.modelRef = { provider: r.provider, modelId: r.modelId };
|
||||
}
|
||||
}
|
||||
const modelRef = parseModelRef(o.modelRef);
|
||||
if (modelRef) out.modelRef = modelRef;
|
||||
if (typeof o.handoffAgentId === "string") out.handoffAgentId = o.handoffAgentId;
|
||||
const switchModelRef = parseModelRef(o.switchModelRef);
|
||||
if (switchModelRef) out.switchModelRef = switchModelRef;
|
||||
if (Array.isArray(o.skills)) {
|
||||
// Elements are validated one by one: non-string items are filtered out; if empty after
|
||||
// filtering, the whole field is omitted.
|
||||
|
||||
Reference in New Issue
Block a user