Compare commits

...

3 Commits

Author SHA1 Message Date
Shantur Rathore
7c3f808d69 Minium server 0.12.3 2026-03-13 20:06:41 +00:00
Shantur Rathore
a59e929b12 Release v0.12.3 2026-03-13 20:04:20 +00:00
Shantur Rathore
8ff4019839 fix(ui): stabilize prompt async optimistic messages
Reconcile optimistic user messages by replacing the oldest synthetic pending message when the server-backed message arrives. Stop sending prompt part ids and rely on message-level replacement so v1.2.25 validation passes without duplicating optimistic content.
2026-03-13 19:17:55 +00:00
14 changed files with 41 additions and 39 deletions

12
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "codenomad-workspace", "name": "codenomad-workspace",
"version": "0.12.2", "version": "0.12.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "codenomad-workspace", "name": "codenomad-workspace",
"version": "0.12.2", "version": "0.12.3",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"7zip-bin": "^5.2.0", "7zip-bin": "^5.2.0",
@@ -12002,7 +12002,7 @@
}, },
"packages/electron-app": { "packages/electron-app": {
"name": "@neuralnomads/codenomad-electron-app", "name": "@neuralnomads/codenomad-electron-app",
"version": "0.12.2", "version": "0.12.3",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@codenomad/ui": "file:../ui", "@codenomad/ui": "file:../ui",
@@ -12039,7 +12039,7 @@
}, },
"packages/server": { "packages/server": {
"name": "@neuralnomads/codenomad", "name": "@neuralnomads/codenomad",
"version": "0.12.2", "version": "0.12.3",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@fastify/cors": "^8.5.0", "@fastify/cors": "^8.5.0",
@@ -12080,7 +12080,7 @@
}, },
"packages/tauri-app": { "packages/tauri-app": {
"name": "@codenomad/tauri-app", "name": "@codenomad/tauri-app",
"version": "0.12.2", "version": "0.12.3",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^2.9.4" "@tauri-apps/cli": "^2.9.4"
@@ -12088,7 +12088,7 @@
}, },
"packages/ui": { "packages/ui": {
"name": "@codenomad/ui", "name": "@codenomad/ui",
"version": "0.12.2", "version": "0.12.3",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@git-diff-view/solid": "^0.0.8", "@git-diff-view/solid": "^0.0.8",

View File

@@ -1,6 +1,6 @@
{ {
"name": "codenomad-workspace", "name": "codenomad-workspace",
"version": "0.12.2", "version": "0.12.3",
"private": true, "private": true,
"description": "CodeNomad monorepo workspace", "description": "CodeNomad monorepo workspace",
"license": "MIT", "license": "MIT",

View File

@@ -1,4 +1,4 @@
{ {
"minServerVersion": "0.11.4", "minServerVersion": "0.12.3",
"latestServerUrl": "https://github.com/NeuralNomadsAI/CodeNomad/releases/latest" "latestServerUrl": "https://github.com/NeuralNomadsAI/CodeNomad/releases/latest"
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@neuralnomads/codenomad-electron-app", "name": "@neuralnomads/codenomad-electron-app",
"version": "0.12.2", "version": "0.12.3",
"description": "CodeNomad - AI coding assistant", "description": "CodeNomad - AI coding assistant",
"license": "MIT", "license": "MIT",
"author": { "author": {

View File

@@ -4,6 +4,6 @@
"private": true, "private": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@opencode-ai/plugin": "1.2.14" "@opencode-ai/plugin": "1.2.25"
} }
} }

View File

@@ -1,12 +1,12 @@
{ {
"name": "@neuralnomads/codenomad", "name": "@neuralnomads/codenomad",
"version": "0.12.2", "version": "0.12.3",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@neuralnomads/codenomad", "name": "@neuralnomads/codenomad",
"version": "0.12.2", "version": "0.12.3",
"dependencies": { "dependencies": {
"@fastify/cors": "^8.5.0", "@fastify/cors": "^8.5.0",
"@fastify/reply-from": "^9.8.0", "@fastify/reply-from": "^9.8.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@neuralnomads/codenomad", "name": "@neuralnomads/codenomad",
"version": "0.12.2", "version": "0.12.3",
"description": "CodeNomad Server", "description": "CodeNomad Server",
"license": "MIT", "license": "MIT",
"author": { "author": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@codenomad/tauri-app", "name": "@codenomad/tauri-app",
"version": "0.12.2", "version": "0.12.3",
"private": true, "private": true,
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@codenomad/ui", "name": "@codenomad/ui",
"version": "0.12.2", "version": "0.12.3",
"private": true, "private": true,
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",

View File

@@ -5,7 +5,7 @@ import { getQuestionCallId, getQuestionMessageId } from "../../types/question"
import type { Message, MessageInfo, ClientPart } from "../../types/message" import type { Message, MessageInfo, ClientPart } from "../../types/message"
import type { Session } from "../../types/session" import type { Session } from "../../types/session"
import { messageStoreBus } from "./bus" import { messageStoreBus } from "./bus"
import type { MessageStatus, SessionRevertState } from "./types" import type { MessageStatus, ReplaceMessageIdOptions, SessionRevertState } from "./types"
interface SessionMetadata { interface SessionMetadata {
id: string id: string
@@ -121,10 +121,10 @@ export function applyPartDeltaV2(
}) })
} }
export function replaceMessageIdV2(instanceId: string, oldId: string, newId: string): void { export function replaceMessageIdV2(instanceId: string, oldId: string, newId: string, options?: Omit<ReplaceMessageIdOptions, "oldId" | "newId">): void {
if (!oldId || !newId || oldId === newId) return if (!oldId || !newId || oldId === newId) return
const store = messageStoreBus.getOrCreate(instanceId) const store = messageStoreBus.getOrCreate(instanceId)
store.replaceMessageId({ oldId, newId }) store.replaceMessageId({ oldId, newId, ...(options ?? {}) })
} }
function extractPermissionMessageId(permission: PermissionRequestLike): string | undefined { function extractPermissionMessageId(permission: PermissionRequestLike): string | undefined {

View File

@@ -792,6 +792,8 @@ export function createInstanceMessageStore(instanceId: string, hooks?: MessageSt
id: options.newId, id: options.newId,
isEphemeral: false, isEphemeral: false,
updatedAt: Date.now(), updatedAt: Date.now(),
partIds: options.clearParts ? [] : existing.partIds,
parts: options.clearParts ? {} : existing.parts,
} }
setState("messages", options.newId, cloned) setState("messages", options.newId, cloned)

View File

@@ -152,6 +152,7 @@ export interface PartUpdateInput {
export interface ReplaceMessageIdOptions { export interface ReplaceMessageIdOptions {
oldId: string oldId: string
newId: string newId: string
clearParts?: boolean
} }
export interface ScrollCacheKey { export interface ScrollCacheKey {

View File

@@ -94,7 +94,7 @@ async function sendMessage(
} }
const messageId = createId("msg") const messageId = createId("msg")
const textPartId = createId("part") const textPartId = createId("prt")
const resolvedPrompt = resolvePastedPlaceholders(prompt, attachments) const resolvedPrompt = resolvePastedPlaceholders(prompt, attachments)
@@ -110,7 +110,6 @@ async function sendMessage(
const requestParts: any[] = [ const requestParts: any[] = [
{ {
id: textPartId,
type: "text" as const, type: "text" as const,
text: resolvedPrompt, text: resolvedPrompt,
}, },
@@ -120,9 +119,8 @@ async function sendMessage(
for (const att of attachments) { for (const att of attachments) {
const source = att.source const source = att.source
if (source.type === "file") { if (source.type === "file") {
const partId = createId("part") const partId = createId("prt")
requestParts.push({ requestParts.push({
id: partId,
type: "file" as const, type: "file" as const,
url: att.url, url: att.url,
mime: source.mime, mime: source.mime,
@@ -148,9 +146,8 @@ async function sendMessage(
continue continue
} }
const partId = createId("part") const partId = createId("prt")
requestParts.push({ requestParts.push({
id: partId,
type: "text" as const, type: "text" as const,
text: value, text: value,
}) })
@@ -184,7 +181,6 @@ async function sendMessage(
}) })
const requestBody = { const requestBody = {
messageID: messageId,
parts: requestParts, parts: requestParts,
...(session.agent && { agent: session.agent }), ...(session.agent && { agent: session.agent }),
...(session.model.providerId && ...(session.model.providerId &&

View File

@@ -240,19 +240,22 @@ function resolveMessageRole(info?: MessageInfo | null): MessageRole {
return info?.role === "user" ? "user" : "assistant" return info?.role === "user" ? "user" : "assistant"
} }
function findPendingMessageId( function findPendingSyntheticMessageId(
store: InstanceMessageStore, store: InstanceMessageStore,
sessionId: string, sessionId: string,
role: MessageRole, role: MessageRole,
): string | undefined { ): string | undefined {
const messageIds = store.getSessionMessageIds(sessionId) const messageIds = store.getSessionMessageIds(sessionId)
const lastId = messageIds[messageIds.length - 1] for (const messageId of messageIds) {
if (!lastId) return undefined const record = store.getMessage(messageId)
const record = store.getMessage(lastId) if (!record) continue
if (!record) return undefined if (record.sessionId !== sessionId) continue
if (record.sessionId !== sessionId) return undefined if (record.role !== role) continue
if (record.role !== role) return undefined if (record.status !== "sending") continue
return record.status === "sending" ? record.id : undefined if (!record.isEphemeral) continue
return record.id
}
return undefined
} }
function handleMessageUpdate(instanceId: string, event: MessageUpdateEvent | MessagePartUpdatedEvent): void { function handleMessageUpdate(instanceId: string, event: MessageUpdateEvent | MessagePartUpdatedEvent): void {
@@ -282,9 +285,9 @@ function handleMessageUpdate(instanceId: string, event: MessageUpdateEvent | Mes
let record = store.getMessage(messageId) let record = store.getMessage(messageId)
if (!record) { if (!record) {
const pendingId = findPendingMessageId(store, sessionId, role) const pendingId = findPendingSyntheticMessageId(store, sessionId, role)
if (pendingId && pendingId !== messageId) { if (pendingId && pendingId !== messageId) {
replaceMessageIdV2(instanceId, pendingId, messageId) replaceMessageIdV2(instanceId, pendingId, messageId, { clearParts: role === "user" })
record = store.getMessage(messageId) record = store.getMessage(messageId)
} }
} }
@@ -345,9 +348,9 @@ function handleMessageUpdate(instanceId: string, event: MessageUpdateEvent | Mes
let record = store.getMessage(messageId) let record = store.getMessage(messageId)
if (!record) { if (!record) {
const pendingId = findPendingMessageId(store, sessionId, role) const pendingId = findPendingSyntheticMessageId(store, sessionId, role)
if (pendingId && pendingId !== messageId) { if (pendingId && pendingId !== messageId) {
replaceMessageIdV2(instanceId, pendingId, messageId) replaceMessageIdV2(instanceId, pendingId, messageId, { clearParts: role === "user" })
record = store.getMessage(messageId) record = store.getMessage(messageId)
} }
} }