From 9fa5c96dd463bb0d5f4744541234d4991137a924 Mon Sep 17 00:00:00 2001 From: kumarabhirup Date: Thu, 5 Feb 2026 21:27:51 -0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=98=85=20TRY:=20/new?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/auto-reply/reply/get-reply-run.ts | 24 ++++-- src/gateway/server-methods/chat.ts | 115 ++++++++++++++++++++++++-- src/hooks/llm-slug-generator.ts | 11 +++ ui/src/ui/app-chat.ts | 3 +- 4 files changed, 138 insertions(+), 15 deletions(-) diff --git a/src/auto-reply/reply/get-reply-run.ts b/src/auto-reply/reply/get-reply-run.ts index a3dc5e9e01c..a2ca9de73d6 100644 --- a/src/auto-reply/reply/get-reply-run.ts +++ b/src/auto-reply/reply/get-reply-run.ts @@ -23,6 +23,7 @@ import { import { logVerbose } from "../../globals.js"; import { clearCommandLane, getQueueSize } from "../../process/command-queue.js"; import { normalizeMainKey } from "../../routing/session-key.js"; +import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js"; import { isReasoningTagProvider } from "../../utils/provider-utils.js"; import { hasControlCommand } from "../command-detection.js"; import { buildInboundMediaNote } from "../media-note.js"; @@ -294,15 +295,20 @@ export async function runPreparedReply( modelLabel === defaultLabel ? `✅ New session started · model: ${modelLabel}` : `✅ New session started · model: ${modelLabel} (default: ${defaultLabel})`; - await routeReply({ - payload: { text }, - channel, - to, - sessionKey, - accountId: ctx.AccountId, - threadId: ctx.MessageThreadId, - cfg, - }); + // Webchat isn't supported by routeReply; use onBlockReply callback instead. + if (channel === INTERNAL_MESSAGE_CHANNEL && opts?.onBlockReply) { + await opts.onBlockReply({ text }); + } else { + await routeReply({ + payload: { text }, + channel, + to, + sessionKey, + accountId: ctx.AccountId, + threadId: ctx.MessageThreadId, + cfg, + }); + } } } const sessionIdFinal = sessionId ?? crypto.randomUUID(); diff --git a/src/gateway/server-methods/chat.ts b/src/gateway/server-methods/chat.ts index ba5347dc3fb..8e533abaefd 100644 --- a/src/gateway/server-methods/chat.ts +++ b/src/gateway/server-methods/chat.ts @@ -144,6 +144,73 @@ function appendAssistantTranscriptMessage(params: { return { ok: true, messageId, message: transcriptEntry.message }; } +function appendUserTranscriptMessage(params: { + message: string; + images: ChatImageContent[]; + sessionId: string; + storePath: string | undefined; + sessionFile?: string; + createIfMissing?: boolean; +}): TranscriptAppendResult { + const transcriptPath = resolveTranscriptPath({ + sessionId: params.sessionId, + storePath: params.storePath, + sessionFile: params.sessionFile, + }); + if (!transcriptPath) { + return { ok: false, error: "transcript path not resolved" }; + } + + if (!fs.existsSync(transcriptPath)) { + if (!params.createIfMissing) { + return { ok: false, error: "transcript file not found" }; + } + const ensured = ensureTranscriptFile({ + transcriptPath, + sessionId: params.sessionId, + }); + if (!ensured.ok) { + return { ok: false, error: ensured.error ?? "failed to create transcript file" }; + } + } + + const now = Date.now(); + const messageId = randomUUID().slice(0, 8); + const content: Array> = []; + if (params.message.trim()) { + content.push({ type: "text", text: params.message }); + } + for (const image of params.images) { + content.push({ + type: "image", + source: { + type: "base64", + media_type: image.mimeType, + data: image.data, + }, + }); + } + const messageBody: Record = { + role: "user", + content: content.length > 0 ? content : [], + timestamp: now, + }; + const transcriptEntry = { + type: "message", + id: messageId, + timestamp: new Date(now).toISOString(), + message: messageBody, + }; + + try { + fs.appendFileSync(transcriptPath, `${JSON.stringify(transcriptEntry)}\n`, "utf-8"); + } catch (err) { + return { ok: false, error: err instanceof Error ? err.message : String(err) }; + } + + return { ok: true, messageId, message: transcriptEntry.message }; +} + function nextChatSeq(context: { agentRunSeq: Map }, runId: string) { const next = (context.agentRunSeq.get(runId) ?? 0) + 1; context.agentRunSeq.set(runId, next); @@ -487,7 +554,7 @@ export const chatHandlers: GatewayRequestHandlers = { context.logGateway.warn(`webchat dispatch failed: ${formatForLog(err)}`); }, deliver: async (payload, info) => { - if (info.kind !== "final") { + if (info.kind !== "final" && info.kind !== "block") { return; } const text = payload.text?.trim() ?? ""; @@ -498,7 +565,28 @@ export const chatHandlers: GatewayRequestHandlers = { }, }); + const shouldPersistUser = parsedMessage.trim().startsWith("/"); let agentRunStarted = false; + let userAppended = false; + const appendUserMessage = () => { + if (!shouldPersistUser || userAppended) return; + userAppended = true; + const { storePath: latestStorePath, entry: latestEntry } = loadSessionEntry(p.sessionKey); + const sessionId = latestEntry?.sessionId ?? entry?.sessionId ?? clientRunId; + const appendedUser = appendUserTranscriptMessage({ + message: parsedMessage, + images: parsedImages, + sessionId, + storePath: latestStorePath, + sessionFile: latestEntry?.sessionFile, + createIfMissing: true, + }); + if (!appendedUser.ok) { + context.logGateway.warn( + `webchat transcript user append failed: ${appendedUser.error ?? "unknown error"}`, + ); + } + }; void dispatchInboundMessage({ ctx, cfg, @@ -510,6 +598,7 @@ export const chatHandlers: GatewayRequestHandlers = { disableBlockStreaming: true, onAgentRunStart: () => { agentRunStarted = true; + appendUserMessage(); }, onModelSelected: (ctx) => { prefixContext.provider = ctx.provider; @@ -527,11 +616,27 @@ export const chatHandlers: GatewayRequestHandlers = { .join("\n\n") .trim(); let message: Record | undefined; + const { storePath: latestStorePath, entry: latestEntry } = loadSessionEntry( + p.sessionKey, + ); + const sessionId = latestEntry?.sessionId ?? entry?.sessionId ?? clientRunId; + if (!userAppended) { + const appendedUser = appendUserTranscriptMessage({ + message: parsedMessage, + images: parsedImages, + sessionId, + storePath: latestStorePath, + sessionFile: latestEntry?.sessionFile, + createIfMissing: true, + }); + userAppended = true; + if (!appendedUser.ok) { + context.logGateway.warn( + `webchat transcript user append failed: ${appendedUser.error ?? "unknown error"}`, + ); + } + } if (combinedReply) { - const { storePath: latestStorePath, entry: latestEntry } = loadSessionEntry( - p.sessionKey, - ); - const sessionId = latestEntry?.sessionId ?? entry?.sessionId ?? clientRunId; const appended = appendAssistantTranscriptMessage({ message: combinedReply, sessionId, diff --git a/src/hooks/llm-slug-generator.ts b/src/hooks/llm-slug-generator.ts index 95161b66b4e..5c878649f1c 100644 --- a/src/hooks/llm-slug-generator.ts +++ b/src/hooks/llm-slug-generator.ts @@ -11,6 +11,8 @@ import { resolveAgentWorkspaceDir, resolveAgentDir, } from "../agents/agent-scope.js"; +import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js"; +import { resolveConfiguredModelRef } from "../agents/model-selection.js"; import { runEmbeddedPiAgent } from "../agents/pi-embedded.js"; /** @@ -38,6 +40,13 @@ ${params.sessionContent.slice(0, 2000)} Reply with ONLY the slug, nothing else. Examples: "vendor-pitch", "api-design", "bug-fix"`; + // Resolve the configured model to avoid falling back to DEFAULT_PROVIDER + const modelRef = resolveConfiguredModelRef({ + cfg: params.cfg, + defaultProvider: DEFAULT_PROVIDER, + defaultModel: DEFAULT_MODEL, + }); + const result = await runEmbeddedPiAgent({ sessionId: `slug-generator-${Date.now()}`, sessionKey: "temp:slug-generator", @@ -45,6 +54,8 @@ Reply with ONLY the slug, nothing else. Examples: "vendor-pitch", "api-design", workspaceDir, agentDir, config: params.cfg, + provider: modelRef.provider, + model: modelRef.model, prompt, timeoutMs: 15_000, // 15 second timeout runId: `slug-gen-${Date.now()}`, diff --git a/ui/src/ui/app-chat.ts b/ui/src/ui/app-chat.ts index cd2c8e8e066..405171cb447 100644 --- a/ui/src/ui/app-chat.ts +++ b/ui/src/ui/app-chat.ts @@ -161,6 +161,7 @@ export async function handleSendChat( } const refreshSessions = isChatResetCommand(message); + if (messageOverride == null) { host.chatMessage = ""; // Clear attachments when sending @@ -172,7 +173,7 @@ export async function handleSendChat( return; } - await sendChatMessageNow(host, message, { + return await sendChatMessageNow(host, message, { previousDraft: messageOverride == null ? previousDraft : undefined, restoreDraft: Boolean(messageOverride && opts?.restoreDraft), attachments: hasAttachments ? attachmentsToSend : undefined,