Compare commits

..

5 Commits

Author SHA1 Message Date
Shantur Rathore
03ed3d3b2c Merge branch 'dev' of github.com:NeuralNomadsAI/CodeNomad into dev 2026-04-16 08:43:33 +01:00
Shantur Rathore
a111de1af8 Minimum version to 0.14.0 2026-04-16 08:43:16 +01:00
Shantur Rathore
8a3b162be9 Bump version to 0.14.0 2026-04-16 08:42:33 +01:00
Shantur Rathore
c62cb3ce4a fix(server): share voice mode state across listeners 2026-04-13 21:36:49 +01:00
Shantur Rathore
d9811e735d fix(server): reject stale voice mode enables 2026-04-13 20:37:31 +01:00
15 changed files with 60 additions and 32 deletions

12
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "codenomad-workspace", "name": "codenomad-workspace",
"version": "0.13.3", "version": "0.14.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "codenomad-workspace", "name": "codenomad-workspace",
"version": "0.13.3", "version": "0.14.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"7zip-bin": "^5.2.0", "7zip-bin": "^5.2.0",
@@ -12068,7 +12068,7 @@
}, },
"packages/electron-app": { "packages/electron-app": {
"name": "@neuralnomads/codenomad-electron-app", "name": "@neuralnomads/codenomad-electron-app",
"version": "0.13.3", "version": "0.14.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@codenomad/ui": "file:../ui", "@codenomad/ui": "file:../ui",
@@ -12105,7 +12105,7 @@
}, },
"packages/server": { "packages/server": {
"name": "@neuralnomads/codenomad", "name": "@neuralnomads/codenomad",
"version": "0.13.3", "version": "0.14.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@fastify/cors": "^8.5.0", "@fastify/cors": "^8.5.0",
@@ -12147,7 +12147,7 @@
}, },
"packages/tauri-app": { "packages/tauri-app": {
"name": "@codenomad/tauri-app", "name": "@codenomad/tauri-app",
"version": "0.13.3", "version": "0.14.0",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^2.9.4" "@tauri-apps/cli": "^2.9.4"
@@ -12155,7 +12155,7 @@
}, },
"packages/ui": { "packages/ui": {
"name": "@codenomad/ui", "name": "@codenomad/ui",
"version": "0.13.3", "version": "0.14.0",
"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.13.3", "version": "0.14.0",
"private": true, "private": true,
"description": "CodeNomad monorepo workspace", "description": "CodeNomad monorepo workspace",
"license": "MIT", "license": "MIT",

View File

@@ -1,4 +1,4 @@
{ {
"minServerVersion": "0.13.3", "minServerVersion": "0.14.0",
"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.13.3", "version": "0.14.0",
"description": "CodeNomad - AI coding assistant", "description": "CodeNomad - AI coding assistant",
"license": "MIT", "license": "MIT",
"author": { "author": {

View File

@@ -1,12 +1,12 @@
{ {
"name": "@neuralnomads/codenomad", "name": "@neuralnomads/codenomad",
"version": "0.13.3", "version": "0.14.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@neuralnomads/codenomad", "name": "@neuralnomads/codenomad",
"version": "0.13.3", "version": "0.14.0",
"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.13.3", "version": "0.14.0",
"description": "CodeNomad Server", "description": "CodeNomad Server",
"license": "MIT", "license": "MIT",
"author": { "author": {

View File

@@ -25,6 +25,9 @@ import { resolveNetworkAddresses, resolveRemoteAddresses } from "./server/networ
import { startDevReleaseMonitor } from "./releases/dev-release-monitor" import { startDevReleaseMonitor } from "./releases/dev-release-monitor"
import { SpeechService } from "./speech/service" import { SpeechService } from "./speech/service"
import { SideCarManager } from "./sidecars/manager" import { SideCarManager } from "./sidecars/manager"
import { ClientConnectionManager } from "./clients/connection-manager"
import { PluginChannelManager } from "./plugins/channel"
import { VoiceModeManager } from "./plugins/voice-mode"
const require = createRequire(import.meta.url) const require = createRequire(import.meta.url)
@@ -378,6 +381,14 @@ async function main() {
const remoteAccessEnabled = options.host === "0.0.0.0" || !isLoopbackHost(options.host) const remoteAccessEnabled = options.host === "0.0.0.0" || !isLoopbackHost(options.host)
const clientConnectionManager = new ClientConnectionManager(logger.child({ component: "client-connections" }))
const pluginChannel = new PluginChannelManager(logger.child({ component: "plugin-channel" }))
const voiceModeManager = new VoiceModeManager({
connections: clientConnectionManager,
channel: pluginChannel,
logger: logger.child({ component: "voice-mode" }),
})
const httpsPortExplicit = programHasArg(process.argv.slice(2), "--https-port") || Boolean(process.env.CLI_HTTPS_PORT) const httpsPortExplicit = programHasArg(process.argv.slice(2), "--https-port") || Boolean(process.env.CLI_HTTPS_PORT)
const httpPortExplicit = programHasArg(process.argv.slice(2), "--http-port") || Boolean(process.env.CLI_HTTP_PORT) const httpPortExplicit = programHasArg(process.argv.slice(2), "--http-port") || Boolean(process.env.CLI_HTTP_PORT)
@@ -408,6 +419,9 @@ async function main() {
speechService, speechService,
sidecarManager, sidecarManager,
authManager, authManager,
clientConnectionManager,
pluginChannel,
voiceModeManager,
uiStaticDir: uiResolution.uiStaticDir ?? DEFAULT_UI_STATIC_DIR, uiStaticDir: uiResolution.uiStaticDir ?? DEFAULT_UI_STATIC_DIR,
uiDevServerUrl: uiResolution.uiDevServerUrl, uiDevServerUrl: uiResolution.uiDevServerUrl,
logger, logger,
@@ -430,6 +444,9 @@ async function main() {
speechService, speechService,
sidecarManager, sidecarManager,
authManager, authManager,
clientConnectionManager,
pluginChannel,
voiceModeManager,
uiStaticDir: uiResolution.uiStaticDir ?? DEFAULT_UI_STATIC_DIR, uiStaticDir: uiResolution.uiStaticDir ?? DEFAULT_UI_STATIC_DIR,
uiDevServerUrl: undefined, uiDevServerUrl: undefined,
logger, logger,
@@ -534,6 +551,12 @@ async function main() {
logger.error({ err: error }, "SideCar manager shutdown failed") logger.error({ err: error }, "SideCar manager shutdown failed")
} }
try {
clientConnectionManager.shutdown()
} catch (error) {
logger.warn({ err: error }, "Client connection manager shutdown failed")
}
try { try {
await workspaceManager.shutdown() await workspaceManager.shutdown()
logger.info("Workspace manager shutdown complete") logger.info("Workspace manager shutdown complete")

View File

@@ -19,13 +19,13 @@ export class VoiceModeManager {
}) })
} }
setEnabled(instanceId: string, connection: ClientConnectionRef, enabled: boolean): void { setEnabled(instanceId: string, connection: ClientConnectionRef, enabled: boolean): boolean {
if (enabled && !this.options.connections.isConnected(connection)) { if (enabled && !this.options.connections.isConnected(connection)) {
this.options.logger.debug( this.options.logger.debug(
{ instanceId, clientId: connection.clientId, connectionId: connection.connectionId }, { instanceId, clientId: connection.clientId, connectionId: connection.connectionId },
"Ignoring voice mode enable for disconnected client connection", "Ignoring voice mode enable for disconnected client connection",
) )
return return false
} }
const key = getConnectionKey(connection) const key = getConnectionKey(connection)
@@ -44,6 +44,7 @@ export class VoiceModeManager {
this.options.logger.debug({ instanceId, clientId: connection.clientId, connectionId: connection.connectionId, enabled }, "Voice mode updated for client connection") this.options.logger.debug({ instanceId, clientId: connection.clientId, connectionId: connection.connectionId, enabled }, "Voice mode updated for client connection")
this.publishIfChanged(instanceId) this.publishIfChanged(instanceId)
return true
} }
syncInstance(instanceId: string): void { syncInstance(instanceId: string): void {
@@ -76,7 +77,10 @@ export class VoiceModeManager {
this.aggregateByInstance.delete(instanceId) this.aggregateByInstance.delete(instanceId)
} }
this.options.logger.debug({ instanceId, enabled }, "Broadcasting aggregate voice mode") this.options.logger.debug(
{ instanceId, enabled },
"Broadcasting aggregate voice mode",
)
this.options.channel.send(instanceId, buildVoiceModeEvent(enabled)) this.options.channel.send(instanceId, buildVoiceModeEvent(enabled))
} }
} }

View File

@@ -54,6 +54,9 @@ interface HttpServerDeps {
speechService: SpeechService speechService: SpeechService
sidecarManager: SideCarManager sidecarManager: SideCarManager
authManager: AuthManager authManager: AuthManager
clientConnectionManager: ClientConnectionManager
pluginChannel: PluginChannelManager
voiceModeManager: VoiceModeManager
uiStaticDir: string uiStaticDir: string
uiDevServerUrl?: string uiDevServerUrl?: string
logger: Logger logger: Logger
@@ -182,13 +185,6 @@ export function createHttpServer(deps: HttpServerDeps) {
eventBus: deps.eventBus, eventBus: deps.eventBus,
logger: deps.logger.child({ component: "background-processes" }), logger: deps.logger.child({ component: "background-processes" }),
}) })
const clientConnectionManager = new ClientConnectionManager(deps.logger.child({ component: "client-connections" }))
const pluginChannel = new PluginChannelManager(deps.logger.child({ component: "plugin-channel" }))
const voiceModeManager = new VoiceModeManager({
connections: clientConnectionManager,
channel: pluginChannel,
logger: deps.logger.child({ component: "voice-mode" }),
})
registerAuthRoutes(app, { authManager: deps.authManager }) registerAuthRoutes(app, { authManager: deps.authManager })
@@ -268,7 +264,7 @@ export function createHttpServer(deps: HttpServerDeps) {
eventBus: deps.eventBus, eventBus: deps.eventBus,
registerClient: registerSseClient, registerClient: registerSseClient,
logger: sseLogger, logger: sseLogger,
connectionManager: clientConnectionManager, connectionManager: deps.clientConnectionManager,
}) })
registerWorktreeRoutes(app, { workspaceManager: deps.workspaceManager }) registerWorktreeRoutes(app, { workspaceManager: deps.workspaceManager })
registerStorageRoutes(app, { registerStorageRoutes(app, {
@@ -289,8 +285,8 @@ export function createHttpServer(deps: HttpServerDeps) {
workspaceManager: deps.workspaceManager, workspaceManager: deps.workspaceManager,
eventBus: deps.eventBus, eventBus: deps.eventBus,
logger: proxyLogger, logger: proxyLogger,
channel: pluginChannel, channel: deps.pluginChannel,
voiceModeManager, voiceModeManager: deps.voiceModeManager,
}) })
registerBackgroundProcessRoutes(app, { backgroundProcessManager }) registerBackgroundProcessRoutes(app, { backgroundProcessManager })
registerInstanceProxyRoutes(app, { workspaceManager: deps.workspaceManager, logger: proxyLogger }) registerInstanceProxyRoutes(app, { workspaceManager: deps.workspaceManager, logger: proxyLogger })
@@ -356,7 +352,6 @@ export function createHttpServer(deps: HttpServerDeps) {
}, },
stop: () => { stop: () => {
closeSseClients() closeSseClients()
clientConnectionManager.shutdown()
return app.close() return app.close()
}, },
} }

View File

@@ -66,11 +66,17 @@ export function registerPluginRoutes(app: FastifyInstance, deps: RouteDeps) {
} }
const payload = VoiceModeStateSchema.parse(request.body ?? {}) const payload = VoiceModeStateSchema.parse(request.body ?? {})
deps.voiceModeManager.setEnabled( const applied = deps.voiceModeManager.setEnabled(
request.params.id, request.params.id,
{ clientId: payload.clientId, connectionId: payload.connectionId }, { clientId: payload.clientId, connectionId: payload.connectionId },
payload.enabled, payload.enabled,
) )
if (payload.enabled && !applied) {
reply.code(409).send({ error: "Client connection not active for voice mode enable" })
return
}
return { enabled: payload.enabled } return { enabled: payload.enabled }
}) })

View File

@@ -458,7 +458,7 @@ dependencies = [
[[package]] [[package]]
name = "codenomad-tauri" name = "codenomad-tauri"
version = "0.13.3" version = "0.14.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"dirs 5.0.1", "dirs 5.0.1",

View File

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

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "codenomad-tauri" name = "codenomad-tauri"
version = "0.13.3" version = "0.14.0"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "CodeNomad", "productName": "CodeNomad",
"version": "0.13.3", "version": "0.14.0",
"identifier": "ai.neuralnomads.codenomad.client", "identifier": "ai.neuralnomads.codenomad.client",
"build": { "build": {
"beforeDevCommand": "npm run dev:bootstrap", "beforeDevCommand": "npm run dev:bootstrap",

View File

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