Compare commits

...

15 Commits

Author SHA1 Message Date
Shantur Rathore
6961efde0b Merge pull request #224 from Pagecran/upstream/tauri-prebuild-sync
fix(tauri): sync server UI bundle during prebuild
2026-03-18 20:39:22 +00:00
Shantur Rathore
b3e0233f4b Merge pull request #232 from NeuralNomadsAI/codenomad/issue-231
fix(tauri): stop CLI process group on exit
2026-03-18 20:33:55 +00:00
Pascal André
fcebcb0174 fix(tauri): sync server UI bundle during prebuild
Ensure the Tauri prebuild step refreshes packages/server/public from the current UI renderer bundle so the packaged desktop app does not serve a stale folder-selection UI.
2026-03-18 20:45:08 +01:00
Shantur Rathore
eaab5e2e9f fix(tauri): stop CLI process group on exit 2026-03-18 19:43:41 +00:00
Shantur Rathore
b12825f923 Merge pull request #227 from Pagecran/upstream/tauri-windows-runtime
fix(tauri): improve Windows desktop runtime behavior
2026-03-18 19:37:31 +00:00
Pascal André
8245f474b8 fix(tauri): avoid non-Windows spawn warning 2026-03-18 20:21:40 +01:00
Pascal André
3a15b311a8 fix(tauri): hide taskkill during Windows cleanup 2026-03-18 20:19:10 +01:00
Pascal André
6cb6c0af32 fix(tauri): align desktop bundle identifier 2026-03-18 20:19:10 +01:00
Pascal André
7f631611fd fix(tauri): hide Windows CLI console window
Set CREATE_NO_WINDOW on the spawned local CLI process so the packaged Windows Tauri app does not flash an extra console window when it launches Node.
2026-03-18 20:19:10 +01:00
Pascal André
9d91ecc649 fix(tauri): kill Windows CLI process trees on shutdown
Use taskkill /T /F for the local server process on Windows so child Node/Bun processes do not survive app shutdown or startup timeouts.
2026-03-18 20:19:10 +01:00
Pascal André
87afb06d34 fix(tauri): restore Windows app identity
Set the same explicit AppUserModelID that the legacy Electron app used so Windows taskbar grouping and notification attribution stay consistent in the Tauri desktop build.
2026-03-18 20:18:59 +01:00
Pascal André
4402d9afb0 fix(tauri): align desktop version metadata
Match the Tauri package, Cargo, and bundle version metadata to the current legacy desktop version so About dialogs and installer artifacts stop reporting 0.1.0.
2026-03-18 20:18:07 +01:00
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
20 changed files with 187 additions and 48 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

@@ -626,7 +626,7 @@ dependencies = [
[[package]] [[package]]
name = "codenomad-tauri" name = "codenomad-tauri"
version = "0.1.0" version = "0.12.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"dirs 5.0.1", "dirs 5.0.1",
@@ -646,6 +646,7 @@ dependencies = [
"thiserror 1.0.69", "thiserror 1.0.69",
"url", "url",
"which", "which",
"windows-sys 0.59.0",
] ]
[[package]] [[package]]

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

@@ -20,6 +20,7 @@ const serverDevInstallCommand =
"npm install --workspace @neuralnomads/codenomad --include-workspace-root=false --install-strategy=nested --fund=false --audit=false" "npm install --workspace @neuralnomads/codenomad --include-workspace-root=false --install-strategy=nested --fund=false --audit=false"
const uiDevInstallCommand = const uiDevInstallCommand =
"npm install --workspace @codenomad/ui --include-workspace-root=false --install-strategy=nested --fund=false --audit=false" "npm install --workspace @codenomad/ui --include-workspace-root=false --install-strategy=nested --fund=false --audit=false"
const serverPrepareUiCommand = "npm run prepare-ui --workspace @neuralnomads/codenomad"
const envWithRootBin = { const envWithRootBin = {
...process.env, ...process.env,
@@ -91,6 +92,15 @@ function ensureUiBuild() {
} }
} }
function syncServerUiBundle() {
console.log("[prebuild] syncing server public UI bundle...")
execSync(serverPrepareUiCommand, {
cwd: workspaceRoot,
stdio: "inherit",
env: envWithRootBin,
})
}
function ensureServerDevDependencies() { function ensureServerDevDependencies() {
if (fs.existsSync(braceExpansionPath)) { if (fs.existsSync(braceExpansionPath)) {
return return
@@ -246,6 +256,7 @@ function copyUiLoadingAssets() {
ensureServerDependencies() ensureServerDependencies()
ensureServerBuild() ensureServerBuild()
ensureUiBuild() ensureUiBuild()
syncServerUiBundle()
copyServerArtifacts() copyServerArtifacts()
stripNodeModuleBins() stripNodeModuleBins()
copyUiLoadingAssets() copyUiLoadingAssets()

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "codenomad-tauri" name = "codenomad-tauri"
version = "0.1.0" version = "0.12.3"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
@@ -25,3 +25,6 @@ tauri-plugin-opener = "2"
url = "2" url = "2"
tauri-plugin-keepawake = "0.1.1" tauri-plugin-keepawake = "0.1.1"
tauri-plugin-notification = "2" tauri-plugin-notification = "2"
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.59", features = ["Win32_UI_Shell"] }

View File

@@ -9,6 +9,8 @@ use std::ffi::OsStr;
use std::fs; use std::fs;
use std::io::{BufRead, BufReader, Read, Write}; use std::io::{BufRead, BufReader, Read, Write};
use std::net::TcpStream; use std::net::TcpStream;
#[cfg(unix)]
use std::os::unix::process::CommandExt;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::{Child, Command, Stdio}; use std::process::{Child, Command, Stdio};
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
@@ -17,10 +19,24 @@ use std::thread;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use tauri::{webview::cookie::Cookie, AppHandle, Emitter, Manager, Url}; use tauri::{webview::cookie::Cookie, AppHandle, Emitter, Manager, Url};
#[cfg(windows)]
use std::os::windows::process::CommandExt;
#[cfg(windows)]
const CREATE_NO_WINDOW: u32 = 0x08000000;
fn log_line(message: &str) { fn log_line(message: &str) {
println!("[tauri-cli] {message}"); println!("[tauri-cli] {message}");
} }
#[cfg(windows)]
fn configure_spawn(command: &mut Command) {
command.creation_flags(CREATE_NO_WINDOW);
}
#[cfg(not(windows))]
fn configure_spawn(_command: &mut Command) {}
fn workspace_root() -> Option<PathBuf> { fn workspace_root() -> Option<PathBuf> {
std::env::current_dir().ok().and_then(|mut dir| { std::env::current_dir().ok().and_then(|mut dir| {
for _ in 0..3 { for _ in 0..3 {
@@ -36,6 +52,46 @@ const SESSION_COOKIE_NAME: &str = "codenomad_session";
const CLI_STOP_GRACE_SECS: u64 = 30; const CLI_STOP_GRACE_SECS: u64 = 30;
#[cfg(unix)]
fn configure_posix_process_group(command: &mut Command) {
// Ensure the CLI runs in its own process group so we can terminate wrapper
// processes (login shell/tsx) without leaving the server orphaned.
unsafe {
command.pre_exec(|| {
if libc::setpgid(0, 0) != 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
});
}
}
#[cfg(windows)]
fn kill_process_tree_windows(pid: u32, force: bool) -> bool {
let mut args = vec!["/PID".to_string(), pid.to_string(), "/T".to_string()];
if force {
args.push("/F".to_string());
}
let mut command = Command::new("taskkill");
command.args(&args);
configure_spawn(&mut command);
match command.output() {
Ok(output) => {
if output.status.success() {
return true;
}
// If the PID is already gone, treat it as success.
let stdout = String::from_utf8_lossy(&output.stdout).to_lowercase();
let stderr = String::from_utf8_lossy(&output.stderr).to_lowercase();
let combined = format!("{stdout}\n{stderr}");
combined.contains("not found") || combined.contains("no running instance")
}
Err(_) => false,
}
}
fn navigate_main(app: &AppHandle, url: &str) { fn navigate_main(app: &AppHandle, url: &str) {
if let Some(win) = app.webview_windows().get("main") { if let Some(win) = app.webview_windows().get("main") {
let mut display = url.to_string(); let mut display = url.to_string();
@@ -348,11 +404,19 @@ impl CliProcessManager {
log_line(&format!("stopping CLI pid={}", child.id())); log_line(&format!("stopping CLI pid={}", child.id()));
#[cfg(unix)] #[cfg(unix)]
unsafe { unsafe {
libc::kill(child.id() as i32, libc::SIGTERM); let pid = child.id() as i32;
// Prefer signaling the process group to avoid orphaning children
// when the CLI was launched via a wrapper shell.
let group_res = libc::kill(-pid, libc::SIGTERM);
if group_res != 0 {
let _ = libc::kill(pid, libc::SIGTERM);
}
} }
#[cfg(windows)] #[cfg(windows)]
{ {
let _ = child.kill(); if !kill_process_tree_windows(child.id(), false) {
let _ = child.kill();
}
} }
let start = Instant::now(); let start = Instant::now();
@@ -368,11 +432,17 @@ impl CliProcessManager {
)); ));
#[cfg(unix)] #[cfg(unix)]
unsafe { unsafe {
libc::kill(child.id() as i32, libc::SIGKILL); let pid = child.id() as i32;
let group_res = libc::kill(-pid, libc::SIGKILL);
if group_res != 0 {
let _ = libc::kill(pid, libc::SIGKILL);
}
} }
#[cfg(windows)] #[cfg(windows)]
{ {
let _ = child.kill(); if !kill_process_tree_windows(child.id(), true) {
let _ = child.kill();
}
} }
break; break;
} }
@@ -450,9 +520,12 @@ impl CliProcessManager {
.env("ELECTRON_RUN_AS_NODE", "1") .env("ELECTRON_RUN_AS_NODE", "1")
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.stderr(Stdio::piped()); .stderr(Stdio::piped());
configure_spawn(&mut c);
if let Some(ref cwd) = cwd { if let Some(ref cwd) = cwd {
c.current_dir(cwd); c.current_dir(cwd);
} }
#[cfg(unix)]
configure_posix_process_group(&mut c);
c.spawn()? c.spawn()?
} }
ShellCommandType::Direct(cmd) => { ShellCommandType::Direct(cmd) => {
@@ -462,9 +535,12 @@ impl CliProcessManager {
.env("ELECTRON_RUN_AS_NODE", "1") .env("ELECTRON_RUN_AS_NODE", "1")
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.stderr(Stdio::piped()); .stderr(Stdio::piped());
configure_spawn(&mut c);
if let Some(ref cwd) = cwd { if let Some(ref cwd) = cwd {
c.current_dir(cwd); c.current_dir(cwd);
} }
#[cfg(unix)]
configure_posix_process_group(&mut c);
c.spawn()? c.spawn()?
} }
}; };
@@ -537,7 +613,24 @@ impl CliProcessManager {
locked.error = Some("CLI did not start in time".to_string()); locked.error = Some("CLI did not start in time".to_string());
log_line("timeout waiting for CLI readiness"); log_line("timeout waiting for CLI readiness");
if let Some(child) = child_holder_clone.lock().as_mut() { if let Some(child) = child_holder_clone.lock().as_mut() {
let _ = child.kill(); #[cfg(unix)]
unsafe {
let pid = child.id() as i32;
let group_res = libc::kill(-pid, libc::SIGKILL);
if group_res != 0 {
let _ = libc::kill(pid, libc::SIGKILL);
}
}
#[cfg(windows)]
{
if !kill_process_tree_windows(child.id(), true) {
let _ = child.kill();
}
}
#[cfg(not(any(unix, windows)))]
{
let _ = child.kill();
}
} }
let _ = app_clone.emit("cli:error", json!({"message": "CLI did not start in time"})); let _ = app_clone.emit("cli:error", json!({"message": "CLI did not start in time"}));
Self::emit_status(&app_clone, &locked); Self::emit_status(&app_clone, &locked);

View File

@@ -12,8 +12,20 @@ use tauri::{AppHandle, Emitter, Manager, Runtime, Wry};
use tauri_plugin_opener::OpenerExt; use tauri_plugin_opener::OpenerExt;
use url::Url; use url::Url;
#[cfg(windows)]
use std::ffi::OsStr;
#[cfg(windows)]
use std::iter;
#[cfg(windows)]
use std::os::windows::ffi::OsStrExt;
#[cfg(windows)]
use windows_sys::Win32::UI::Shell::SetCurrentProcessExplicitAppUserModelID;
static QUIT_REQUESTED: AtomicBool = AtomicBool::new(false); static QUIT_REQUESTED: AtomicBool = AtomicBool::new(false);
#[cfg(windows)]
const WINDOWS_APP_USER_MODEL_ID: &str = "ai.neuralnomads.codenomad.client";
#[derive(Clone)] #[derive(Clone)]
pub struct AppState { pub struct AppState {
pub manager: CliProcessManager, pub manager: CliProcessManager,
@@ -101,6 +113,22 @@ fn emit_folder_drop_event(
} }
} }
#[cfg(windows)]
fn set_windows_app_user_model_id() {
let app_id: Vec<u16> = OsStr::new(WINDOWS_APP_USER_MODEL_ID)
.encode_wide()
.chain(iter::once(0))
.collect();
let result = unsafe { SetCurrentProcessExplicitAppUserModelID(app_id.as_ptr()) };
if result < 0 {
eprintln!("[tauri] failed to set AppUserModelID: {result}");
}
}
#[cfg(not(windows))]
fn set_windows_app_user_model_id() {}
fn main() { fn main() {
let navigation_guard: TauriPlugin<Wry, ()> = PluginBuilder::new("external-link-guard") let navigation_guard: TauriPlugin<Wry, ()> = PluginBuilder::new("external-link-guard")
.on_navigation(|webview, url| intercept_navigation(webview, url)) .on_navigation(|webview, url| intercept_navigation(webview, url))
@@ -116,6 +144,7 @@ fn main() {
manager: CliProcessManager::new(), manager: CliProcessManager::new(),
}) })
.setup(|app| { .setup(|app| {
set_windows_app_user_model_id();
build_menu(&app.handle())?; build_menu(&app.handle())?;
let dev_mode = is_dev_mode(); let dev_mode = is_dev_mode();
let app_handle = app.handle().clone(); let app_handle = app.handle().clone();

View File

@@ -1,8 +1,8 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "CodeNomad", "productName": "CodeNomad",
"version": "0.1.0", "version": "0.12.3",
"identifier": "ai.opencode.client", "identifier": "ai.neuralnomads.codenomad.client",
"build": { "build": {
"beforeDevCommand": "npm run dev:bootstrap", "beforeDevCommand": "npm run dev:bootstrap",
"beforeBuildCommand": "npm run bundle:server", "beforeBuildCommand": "npm run bundle:server",

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)
} }
} }