diff --git a/.github/workflows/release-ui.yml b/.github/workflows/release-ui.yml index 2f13e7e7..57195dca 100644 --- a/.github/workflows/release-ui.yml +++ b/.github/workflows/release-ui.yml @@ -12,8 +12,8 @@ env: jobs: release-ui: - # Automated via reusable call (main releases); manual runs allowed on dev. - if: ${{ github.event_name == 'workflow_call' || github.ref == 'refs/heads/dev' }} + # Automated via reusable call (main releases); manual runs allowed on dev/main. + if: ${{ github.event_name == 'workflow_call' || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' }} runs-on: ubuntu-24.04 steps: - name: Checkout diff --git a/.opencode/commands/release-notes.md b/.opencode/commands/release-notes.md new file mode 100644 index 00000000..e0c7734c --- /dev/null +++ b/.opencode/commands/release-notes.md @@ -0,0 +1,7 @@ +--- +description: Creates release notes +agent: build +--- + +Check how I do prepare release notes here - https://github.com/NeuralNomadsAI/CodeNomad/releases/tag/v0.7.0 +Use the same format to create release notes from users perspective for new release by looking at changes from last tagged release to tip of branch \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1d49000c..d361270c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "codenomad-workspace", - "version": "0.9.0", + "version": "0.9.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codenomad-workspace", - "version": "0.9.0", + "version": "0.9.1", "dependencies": { "7zip-bin": "^5.2.0", "google-auth-library": "^10.5.0" @@ -7384,7 +7384,7 @@ }, "packages/electron-app": { "name": "@neuralnomads/codenomad-electron-app", - "version": "0.9.0", + "version": "0.9.1", "dependencies": { "@codenomad/ui": "file:../ui", "@neuralnomads/codenomad": "file:../server" @@ -7418,7 +7418,7 @@ }, "packages/server": { "name": "@neuralnomads/codenomad", - "version": "0.9.0", + "version": "0.9.1", "dependencies": { "@fastify/cors": "^8.5.0", "@fastify/reply-from": "^9.8.0", @@ -7455,14 +7455,14 @@ }, "packages/tauri-app": { "name": "@codenomad/tauri-app", - "version": "0.9.0", + "version": "0.9.1", "devDependencies": { "@tauri-apps/cli": "^2.9.4" } }, "packages/ui": { "name": "@codenomad/ui", - "version": "0.9.0", + "version": "0.9.1", "dependencies": { "@git-diff-view/solid": "^0.0.8", "@kobalte/core": "0.13.11", diff --git a/package.json b/package.json index c24a5e70..8e3a3340 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codenomad-workspace", - "version": "0.9.0", + "version": "0.9.1", "private": true, "description": "CodeNomad monorepo workspace", "workspaces": { diff --git a/packages/cloudflare/release-config.json b/packages/cloudflare/release-config.json index 8d1da1a7..c2b2d832 100644 --- a/packages/cloudflare/release-config.json +++ b/packages/cloudflare/release-config.json @@ -1,4 +1,4 @@ { - "minServerVersion": "0.8.1", + "minServerVersion": "0.9.1", "latestServerUrl": "https://github.com/NeuralNomadsAI/CodeNomad/releases/latest" } diff --git a/packages/electron-app/electron/main/process-manager.ts b/packages/electron-app/electron/main/process-manager.ts index 20134f98..80cbd71e 100644 --- a/packages/electron-app/electron/main/process-manager.ts +++ b/packages/electron-app/electron/main/process-manager.ts @@ -177,8 +177,11 @@ export class CliProcessManager extends EventEmitter { return new Promise((resolve) => { const killTimeout = setTimeout(() => { + console.warn( + `[cli] stop timed out after 30000ms; sending SIGKILL (pid=${child.pid ?? "unknown"})`, + ) child.kill("SIGKILL") - }, 4000) + }, 30000) child.on("exit", () => { clearTimeout(killTimeout) @@ -376,4 +379,3 @@ export class CliProcessManager extends EventEmitter { throw new Error("Unable to locate CodeNomad CLI build (dist/bin.js). Run npm run build --workspace @neuralnomads/codenomad.") } } - diff --git a/packages/electron-app/package.json b/packages/electron-app/package.json index 03431b22..cf752f00 100644 --- a/packages/electron-app/package.json +++ b/packages/electron-app/package.json @@ -1,6 +1,6 @@ { "name": "@neuralnomads/codenomad-electron-app", - "version": "0.9.0", + "version": "0.9.1", "description": "CodeNomad - AI coding assistant", "author": { "name": "Neural Nomads", diff --git a/packages/server/package-lock.json b/packages/server/package-lock.json index 27f4104b..65779937 100644 --- a/packages/server/package-lock.json +++ b/packages/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "@neuralnomads/codenomad", - "version": "0.9.0", + "version": "0.9.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@neuralnomads/codenomad", - "version": "0.9.0", + "version": "0.9.1", "dependencies": { "@fastify/cors": "^8.5.0", "@fastify/reply-from": "^9.8.0", diff --git a/packages/server/package.json b/packages/server/package.json index 1b10afc9..7ee1c9fe 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@neuralnomads/codenomad", - "version": "0.9.0", + "version": "0.9.1", "description": "CodeNomad Server", "author": { "name": "Neural Nomads", diff --git a/packages/server/src/config/schema.ts b/packages/server/src/config/schema.ts index 09dadf66..1efa49b9 100644 --- a/packages/server/src/config/schema.ts +++ b/packages/server/src/config/schema.ts @@ -15,6 +15,7 @@ const PreferencesSchema = z.object({ lastUsedBinary: z.string().optional(), environmentVariables: z.record(z.string()).default({}), modelRecents: z.array(ModelPreferenceSchema).default([]), + modelThinkingSelections: z.record(z.string(), z.string()).default({}), diffViewMode: z.enum(["split", "unified"]).default("split"), toolOutputExpansion: z.enum(["expanded", "collapsed"]).default("expanded"), diagnosticsExpansion: z.enum(["expanded", "collapsed"]).default("expanded"), diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index ac0c34e0..389b6198 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -286,21 +286,33 @@ async function main() { return } shuttingDown = true - logger.info("Received shutdown signal, closing server") - try { - await server.stop() - logger.info("HTTP server stopped") - } catch (error) { - logger.error({ err: error }, "Failed to stop HTTP server") - } + logger.info("Received shutdown signal, stopping workspaces and server") - try { - instanceEventBridge.shutdown() - await workspaceManager.shutdown() - logger.info("Workspace manager shutdown complete") - } catch (error) { - logger.error({ err: error }, "Workspace manager shutdown failed") - } + const shutdownWorkspaces = (async () => { + try { + instanceEventBridge.shutdown() + } catch (error) { + logger.warn({ err: error }, "Instance event bridge shutdown failed") + } + + try { + await workspaceManager.shutdown() + logger.info("Workspace manager shutdown complete") + } catch (error) { + logger.error({ err: error }, "Workspace manager shutdown failed") + } + })() + + const shutdownHttp = (async () => { + try { + await server.stop() + logger.info("HTTP server stopped") + } catch (error) { + logger.error({ err: error }, "Failed to stop HTTP server") + } + })() + + await Promise.allSettled([shutdownWorkspaces, shutdownHttp]) // no-op: remote UI manifest replaces GitHub release monitor diff --git a/packages/server/src/workspaces/manager.ts b/packages/server/src/workspaces/manager.ts index 1b1d863f..623667b7 100644 --- a/packages/server/src/workspaces/manager.ts +++ b/packages/server/src/workspaces/manager.ts @@ -187,16 +187,27 @@ export class WorkspaceManager { async shutdown() { this.options.logger.info("Shutting down all workspaces") + + const stopTasks: Array> = [] + for (const [id, workspace] of this.workspaces) { - if (workspace.pid) { - this.options.logger.info({ workspaceId: id }, "Stopping workspace during shutdown") - await this.runtime.stop(id).catch((error) => { - this.options.logger.error({ workspaceId: id, err: error }, "Failed to stop workspace during shutdown") - }) - } else { + if (!workspace.pid) { this.options.logger.debug({ workspaceId: id }, "Workspace already stopped") + continue } + + this.options.logger.info({ workspaceId: id }, "Stopping workspace during shutdown") + stopTasks.push( + this.runtime.stop(id).catch((error) => { + this.options.logger.error({ workspaceId: id, err: error }, "Failed to stop workspace during shutdown") + }), + ) } + + if (stopTasks.length > 0) { + await Promise.allSettled(stopTasks) + } + this.workspaces.clear() this.opencodeAuth.clear() this.options.logger.info("All workspaces cleared") diff --git a/packages/tauri-app/package.json b/packages/tauri-app/package.json index 4c26c3cf..423e64d6 100644 --- a/packages/tauri-app/package.json +++ b/packages/tauri-app/package.json @@ -1,6 +1,6 @@ { "name": "@codenomad/tauri-app", - "version": "0.9.0", + "version": "0.9.1", "private": true, "scripts": { "dev": "tauri dev", diff --git a/packages/tauri-app/src-tauri/src/cli_manager.rs b/packages/tauri-app/src-tauri/src/cli_manager.rs index 14475910..a1b8013a 100644 --- a/packages/tauri-app/src-tauri/src/cli_manager.rs +++ b/packages/tauri-app/src-tauri/src/cli_manager.rs @@ -34,6 +34,8 @@ fn workspace_root() -> Option { const SESSION_COOKIE_NAME: &str = "codenomad_session"; +const CLI_STOP_GRACE_SECS: u64 = 30; + fn navigate_main(app: &AppHandle, url: &str) { if let Some(win) = app.webview_windows().get("main") { let mut display = url.to_string(); @@ -276,6 +278,7 @@ impl CliProcessManager { pub fn stop(&self) -> anyhow::Result<()> { let mut child_opt = self.child.lock(); if let Some(mut child) = child_opt.take() { + log_line(&format!("stopping CLI pid={}", child.id())); #[cfg(unix)] unsafe { libc::kill(child.id() as i32, libc::SIGTERM); @@ -290,7 +293,12 @@ impl CliProcessManager { match child.try_wait() { Ok(Some(_)) => break, Ok(None) => { - if start.elapsed() > Duration::from_secs(4) { + if start.elapsed() > Duration::from_secs(CLI_STOP_GRACE_SECS) { + log_line(&format!( + "stop timed out after {}s; sending SIGKILL pid={}", + CLI_STOP_GRACE_SECS, + child.id() + )); #[cfg(unix)] unsafe { libc::kill(child.id() as i32, libc::SIGKILL); diff --git a/packages/tauri-app/src-tauri/src/main.rs b/packages/tauri-app/src-tauri/src/main.rs index e92cce9d..81645233 100644 --- a/packages/tauri-app/src-tauri/src/main.rs +++ b/packages/tauri-app/src-tauri/src/main.rs @@ -163,7 +163,8 @@ fn main() { .build(tauri::generate_context!()) .expect("error while building tauri application") .run(|app_handle, event| match event { - tauri::RunEvent::ExitRequested { .. } => { + tauri::RunEvent::ExitRequested { api, .. } => { + api.prevent_exit(); let app = app_handle.clone(); std::thread::spawn(move || { if let Some(state) = app.try_state::() { @@ -173,18 +174,18 @@ fn main() { }); } tauri::RunEvent::WindowEvent { - event: tauri::WindowEvent::Destroyed, + event: tauri::WindowEvent::CloseRequested { api, .. }, .. } => { - if app_handle.webview_windows().len() <= 1 { - let app = app_handle.clone(); - std::thread::spawn(move || { - if let Some(state) = app.try_state::() { - let _ = state.manager.stop(); - } - app.exit(0); - }); - } + // Ensure we have time to stop the CLI process before the app exits. + api.prevent_close(); + let app = app_handle.clone(); + std::thread::spawn(move || { + if let Some(state) = app.try_state::() { + let _ = state.manager.stop(); + } + app.exit(0); + }); } _ => {} }); diff --git a/packages/ui/package.json b/packages/ui/package.json index 2cae67fa..3226937a 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@codenomad/ui", - "version": "0.9.0", + "version": "0.9.1", "private": true, "type": "module", "scripts": { diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index 78488de8..382cbe46 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -10,6 +10,7 @@ import InstanceShell from "./components/instance/instance-shell2" import { RemoteAccessOverlay } from "./components/remote-access-overlay" import { InstanceMetadataProvider } from "./lib/contexts/instance-metadata-context" import { initMarkdown } from "./lib/markdown" +import { initGithubStars } from "./stores/github-stars" import { useTheme } from "./lib/theme" import { useCommands } from "./lib/hooks/use-commands" @@ -94,6 +95,7 @@ const App: Component = () => { }) onMount(() => { + void initGithubStars() updateInstanceTabBarHeight() const handleResize = () => updateInstanceTabBarHeight() window.addEventListener("resize", handleResize) diff --git a/packages/ui/src/components/agent-selector.tsx b/packages/ui/src/components/agent-selector.tsx index 25112669..6541a88e 100644 --- a/packages/ui/src/components/agent-selector.tsx +++ b/packages/ui/src/components/agent-selector.tsx @@ -4,6 +4,7 @@ import { agents, fetchAgents, sessions } from "../stores/sessions" import { ChevronDown } from "lucide-solid" import type { Agent } from "../types/session" import { getLogger } from "../lib/logger" +import Kbd from "./kbd" const log = getLogger("session") @@ -99,15 +100,20 @@ export default function AgentSelector(props: AgentSelectorProps) { data-agent-selector class="selector-trigger" > - > - {(state) => ( -
- - Agent: {state.selectedOption()?.name ?? "None"} - -
- )} - +
+ > + {(state) => ( +
+ + Agent: {state.selectedOption()?.name ?? "None"} + +
+ )} + +
+ diff --git a/packages/ui/src/components/brand-icons.tsx b/packages/ui/src/components/brand-icons.tsx new file mode 100644 index 00000000..c8a0bd88 --- /dev/null +++ b/packages/ui/src/components/brand-icons.tsx @@ -0,0 +1,38 @@ +import type { Component } from "solid-js" + +type BrandIconProps = { + class?: string + title?: string +} + +export const GitHubMarkIcon: Component = (props) => ( + + {props.title ? {props.title} : null} + + +) + +export const DiscordSymbolIcon: Component = (props) => ( + + {props.title ? {props.title} : null} + + +) diff --git a/packages/ui/src/components/folder-selection-view.tsx b/packages/ui/src/components/folder-selection-view.tsx index c4e445e6..14fd32eb 100644 --- a/packages/ui/src/components/folder-selection-view.tsx +++ b/packages/ui/src/components/folder-selection-view.tsx @@ -1,11 +1,14 @@ import { Component, createSignal, Show, For, onMount, onCleanup, createEffect } from "solid-js" -import { Folder, Clock, Trash2, FolderPlus, Settings, ChevronRight, MonitorUp } from "lucide-solid" +import { Folder, Clock, Trash2, FolderPlus, Settings, ChevronRight, MonitorUp, Star } from "lucide-solid" import { useConfig } from "../stores/preferences" import AdvancedSettingsModal from "./advanced-settings-modal" import DirectoryBrowserDialog from "./directory-browser-dialog" import Kbd from "./kbd" import { openNativeFolderDialog, supportsNativeDialogs } from "../lib/native/native-functions" import VersionPill from "./version-pill" +import { DiscordSymbolIcon, GitHubMarkIcon } from "./brand-icons" +import { githubStars } from "../stores/github-stars" +import { formatCompactCount } from "../lib/formatters" const codeNomadLogo = new URL("../images/CodeNomad-Icon.png", import.meta.url).href @@ -188,6 +191,11 @@ const FolderSelectionView: Component = (props) => { if (isLoading()) return props.onSelectFolder(path, selectedBinary()) } + + const openExternalLink = (url: string) => { + if (typeof window === "undefined") return + window.open(url, "_blank", "noopener,noreferrer") + } async function handleBrowse() { if (isLoading()) return @@ -242,170 +250,228 @@ const FolderSelectionView: Component = (props) => { style="background-color: var(--surface-secondary)" >
-
-
- CodeNomad logo -
-

CodeNomad

-

Select a folder to start coding with AI

-
- -
-
- - -
- - 0} - fallback={ -
-
- -
-

No Recent Folders

-

Browse for a folder to get started

-
- } - > -
-
-

Recent Folders

-

- {folders().length} {folders().length === 1 ? "folder" : "folders"} available -

-
-
(recentListRef = el)}> - - {(folder, index) => ( -
-
- - -
-
- )} -
-
-
-
- -
- - -
- -
- - {/* Advanced settings section */} -
- -
+ -