chore: rebrand client and add icon tooling

This commit is contained in:
Shantur Rathore
2025-11-13 22:58:57 +00:00
parent 8233aee7a8
commit a71c8ab34f
27 changed files with 432 additions and 205 deletions

View File

@@ -1,6 +1,6 @@
# Building OpenCode Client Binaries # Building CodeNomad Binaries
This guide explains how to build distributable binaries for OpenCode Client. This guide explains how to build distributable binaries for CodeNomad.
## Prerequisites ## Prerequisites
@@ -81,17 +81,17 @@ Binaries are generated in the `release/` directory:
``` ```
release/ release/
├── OpenCode Client-0.1.0-mac-universal.dmg ├── CodeNomad-0.1.0-mac-universal.dmg
├── OpenCode Client-0.1.0-mac-universal.zip ├── CodeNomad-0.1.0-mac-universal.zip
├── OpenCode Client-0.1.0-win-x64.exe ├── CodeNomad-0.1.0-win-x64.exe
├── OpenCode Client-0.1.0-linux-x64.AppImage ├── CodeNomad-0.1.0-linux-x64.AppImage
└── ... └── ...
``` ```
## File Naming Convention ## File Naming Convention
``` ```
OpenCode Client-{version}-{os}-{arch}.{ext} CodeNomad-{version}-{os}-{arch}.{ext}
``` ```
- **version**: From package.json (e.g., `0.1.0`) - **version**: From package.json (e.g., `0.1.0`)
@@ -215,6 +215,16 @@ Edit `package.json` → `build` section to customize:
See [electron-builder docs](https://www.electron.build/) for details. See [electron-builder docs](https://www.electron.build/) for details.
## Brand Assets
- `images/CodeNomad-Icon.png` — primary asset for in-app logo placements and the 1024×1024 master icon used to generate packaged app icons
To update the binaries:
1. Run `node scripts/generate-icons.js images/CodeNomad-Icon.png electron/resources` to round the corners and emit fresh `icon.icns`, `icon.ico`, and `icon.png` files.
2. (Optional) Pass `--radius` to tweak the corner curvature or `--name` to change the filename prefix.
3. If you prefer manual control, export `images/CodeNomad-Icon.png` with your tool of choice and place the generated files in `electron/resources/`.
## Clean Build ## Clean Build
Remove previous builds: Remove previous builds:

View File

@@ -1,4 +1,4 @@
# OpenCode Client - Development Progress # CodeNomad - Development Progress
## Completed Tasks ## Completed Tasks

View File

@@ -1,10 +1,10 @@
# OpenCode Client # CodeNomad
A cross-platform desktop application for interacting with OpenCode servers, built with Electron and SolidJS. A cross-platform desktop application for interacting with OpenCode servers, built with Electron and SolidJS.
## Overview ## Overview
OpenCode Client provides a multi-instance, multi-session interface for working with AI-powered coding assistants. It manages OpenCode server processes, handles real-time message streaming, and provides an intuitive UI for coding with AI. CodeNomad provides a multi-instance, multi-session interface for working with AI-powered coding assistants. It manages OpenCode server processes, handles real-time message streaming, and provides an intuitive UI for coding with AI.
**🎯 MVP Focus:** This project prioritizes functionality over performance. Performance optimization is intentionally deferred to post-MVP phases. See [docs/MVP-PRINCIPLES.md](docs/MVP-PRINCIPLES.md) for details. **🎯 MVP Focus:** This project prioritizes functionality over performance. Performance optimization is intentionally deferred to post-MVP phases. See [docs/MVP-PRINCIPLES.md](docs/MVP-PRINCIPLES.md) for details.

View File

@@ -1,6 +1,6 @@
# Tool Call Rendering Implementation # Tool Call Rendering Implementation
This document describes how tool calls are rendered in the OpenCode Client, following the patterns established in the TUI. This document describes how tool calls are rendered in the CodeNomad, following the patterns established in the TUI.
## Overview ## Overview

View File

@@ -298,7 +298,7 @@ When the time comes:
> **Make it work, then make it better, then make it fast.** > **Make it work, then make it better, then make it fast.**
For OpenCode Client MVP: For CodeNomad MVP:
- **Phase 1-7:** Make it work, make it better - **Phase 1-7:** Make it work, make it better
- **Phase 8+:** Make it fast - **Phase 8+:** Make it fast

View File

@@ -1,4 +1,4 @@
# OpenCode Client - Project Summary # CodeNomad - Project Summary
## Current Status ## Current Status
@@ -6,7 +6,7 @@ We have completed the MVP milestones (Phases 1-3) and are now operating in post-
## What We've Created ## What We've Created
A comprehensive specification and task breakdown for building the OpenCode Client desktop application. A comprehensive specification and task breakdown for building the CodeNomad desktop application.
## Directory Structure ## Directory Structure

View File

@@ -1,8 +1,8 @@
# OpenCode Client Architecture # CodeNomad Architecture
## Overview ## Overview
OpenCode Client is a cross-platform desktop application built with Electron that provides a multi-instance, multi-session interface for interacting with OpenCode servers. Each instance manages its own OpenCode server process and can handle multiple concurrent sessions. CodeNomad is a cross-platform desktop application built with Electron that provides a multi-instance, multi-session interface for interacting with OpenCode servers. Each instance manages its own OpenCode server process and can handle multiple concurrent sessions.
## High-Level Architecture ## High-Level Architecture

View File

@@ -1,8 +1,8 @@
# OpenCode Client Build Roadmap # CodeNomad Build Roadmap
## Overview ## Overview
This document outlines the phased approach to building the OpenCode Client desktop application. Each phase builds incrementally on the previous, with clear deliverables and milestones. This document outlines the phased approach to building the CodeNomad desktop application. Each phase builds incrementally on the previous, with clear deliverables and milestones.
**Status:** MVP (Phases 1-3) is complete. Focus now shifts to post-MVP phases starting with multi-instance support and advanced input refinements. **Status:** MVP (Phases 1-3) is complete. Focus now shifts to post-MVP phases starting with multi-instance support and advanced input refinements.

View File

@@ -2,7 +2,7 @@
## Overview ## Overview
The OpenCode Client interface consists of a two-level tabbed layout with instance tabs at the top and session tabs below. Each session displays a message stream and prompt input. The CodeNomad interface consists of a two-level tabbed layout with instance tabs at the top and session tabs below. Each session displays a message stream and prompt input.
## Layout Structure ## Layout Structure
@@ -346,7 +346,7 @@ Appears when instance starts:
│ │ │ │
│ [Folder Icon] │ │ [Folder Icon] │
│ │ │ │
Welcome to OpenCode Client Start Coding with AI
│ │ │ │
│ Select a folder to start coding with AI │ │ Select a folder to start coding with AI │
│ │ │ │

View File

@@ -1,4 +1,4 @@
import { app, BrowserWindow, dialog, ipcMain, nativeTheme, session } from "electron" import { app, BrowserWindow, dialog, ipcMain, nativeImage, nativeTheme, session } from "electron"
import { join } from "path" import { join } from "path"
import { createApplicationMenu } from "./menu" import { createApplicationMenu } from "./menu"
import { setupInstanceIPC } from "./ipc" import { setupInstanceIPC } from "./ipc"
@@ -15,9 +15,18 @@ setupStorageIPC()
let mainWindow: BrowserWindow | null = null let mainWindow: BrowserWindow | null = null
function getIconPath() {
if (app.isPackaged) {
return join(process.resourcesPath, "icon.png")
}
return join(app.getAppPath(), "electron/resources/icon.png")
}
function createWindow() { function createWindow() {
const prefersDark = true //nativeTheme.shouldUseDarkColors const prefersDark = true //nativeTheme.shouldUseDarkColors
const backgroundColor = prefersDark ? "#1a1a1a" : "#ffffff" const backgroundColor = prefersDark ? "#1a1a1a" : "#ffffff"
const iconPath = getIconPath()
mainWindow = new BrowserWindow({ mainWindow = new BrowserWindow({
width: 1400, width: 1400,
@@ -25,6 +34,7 @@ function createWindow() {
minWidth: 800, minWidth: 800,
minHeight: 600, minHeight: 600,
backgroundColor, backgroundColor,
icon: iconPath,
webPreferences: { webPreferences: {
preload: join(__dirname, "../preload/index.js"), preload: join(__dirname, "../preload/index.js"),
contextIsolation: true, contextIsolation: true,
@@ -65,6 +75,13 @@ app.whenReady().then(() => {
app.on("browser-window-created", (_, window) => { app.on("browser-window-created", (_, window) => {
window.webContents.session.setSpellCheckerEnabled(false) window.webContents.session.setSpellCheckerEnabled(false)
}) })
if (app.dock) {
const dockIcon = nativeImage.createFromPath(getIconPath())
if (!dockIcon.isEmpty()) {
app.dock.setIcon(dockIcon)
}
}
} }
console.log("[spellcheck] default session enabled:", session.defaultSession.isSpellCheckerEnabled()) console.log("[spellcheck] default session enabled:", session.defaultSession.isSpellCheckerEnabled())

View File

@@ -7,7 +7,7 @@ export function createApplicationMenu(mainWindow: BrowserWindow) {
...(isMac ...(isMac
? [ ? [
{ {
label: "OpenCode Client", label: "CodeNomad",
submenu: [ submenu: [
{ role: "about" as const }, { role: "about" as const },
{ type: "separator" as const }, { type: "separator" as const },

Binary file not shown.

BIN
electron/resources/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 KiB

BIN
electron/resources/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
images/CodeNomad-Icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

85
package-lock.json generated
View File

@@ -26,6 +26,8 @@
"electron": "39.0.0", "electron": "39.0.0",
"electron-builder": "^24.0.0", "electron-builder": "^24.0.0",
"electron-vite": "4.0.1", "electron-vite": "4.0.1",
"png2icons": "^2.0.1",
"pngjs": "^7.0.0",
"postcss": "8.5.6", "postcss": "8.5.6",
"tailwindcss": "3", "tailwindcss": "3",
"typescript": "^5.3.0", "typescript": "^5.3.0",
@@ -77,7 +79,6 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.27.1", "@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5", "@babel/generator": "^7.28.5",
@@ -2288,7 +2289,6 @@
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.1", "fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0", "fast-json-stable-stringify": "^2.0.0",
@@ -2464,6 +2464,7 @@
"integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"archiver-utils": "^2.1.0", "archiver-utils": "^2.1.0",
"async": "^3.2.4", "async": "^3.2.4",
@@ -2483,6 +2484,7 @@
"integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"glob": "^7.1.4", "glob": "^7.1.4",
"graceful-fs": "^4.2.0", "graceful-fs": "^4.2.0",
@@ -2505,6 +2507,7 @@
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"core-util-is": "~1.0.0", "core-util-is": "~1.0.0",
"inherits": "~2.0.3", "inherits": "~2.0.3",
@@ -2520,7 +2523,8 @@
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/archiver-utils/node_modules/string_decoder": { "node_modules/archiver-utils/node_modules/string_decoder": {
"version": "1.1.1", "version": "1.1.1",
@@ -2528,6 +2532,7 @@
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"safe-buffer": "~5.1.0" "safe-buffer": "~5.1.0"
} }
@@ -2746,6 +2751,7 @@
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"buffer": "^5.5.0", "buffer": "^5.5.0",
"inherits": "^2.0.4", "inherits": "^2.0.4",
@@ -2821,7 +2827,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.8.19", "baseline-browser-mapping": "^2.8.19",
"caniuse-lite": "^1.0.30001751", "caniuse-lite": "^1.0.30001751",
@@ -3285,6 +3290,7 @@
"integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"buffer-crc32": "^0.2.13", "buffer-crc32": "^0.2.13",
"crc32-stream": "^4.0.2", "crc32-stream": "^4.0.2",
@@ -3391,6 +3397,7 @@
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"crc32": "bin/crc32.njs" "crc32": "bin/crc32.njs"
}, },
@@ -3404,6 +3411,7 @@
"integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"crc-32": "^1.2.0", "crc-32": "^1.2.0",
"readable-stream": "^3.4.0" "readable-stream": "^3.4.0"
@@ -3636,7 +3644,6 @@
"integrity": "sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==", "integrity": "sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"app-builder-lib": "24.13.3", "app-builder-lib": "24.13.3",
"builder-util": "24.13.1", "builder-util": "24.13.1",
@@ -3821,6 +3828,7 @@
"integrity": "sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==", "integrity": "sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"app-builder-lib": "24.13.3", "app-builder-lib": "24.13.3",
"archiver": "^5.3.1", "archiver": "^5.3.1",
@@ -3834,6 +3842,7 @@
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"graceful-fs": "^4.2.0", "graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1", "jsonfile": "^6.0.1",
@@ -3849,6 +3858,7 @@
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"universalify": "^2.0.0" "universalify": "^2.0.0"
}, },
@@ -3862,6 +3872,7 @@
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">= 10.0.0" "node": ">= 10.0.0"
} }
@@ -4343,7 +4354,8 @@
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/fs-extra": { "node_modules/fs-extra": {
"version": "8.1.0", "version": "8.1.0",
@@ -5062,7 +5074,8 @@
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/isbinaryfile": { "node_modules/isbinaryfile": {
"version": "5.0.6", "version": "5.0.6",
@@ -5124,7 +5137,6 @@
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"jiti": "bin/jiti.js" "jiti": "bin/jiti.js"
} }
@@ -5230,6 +5242,7 @@
"integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"readable-stream": "^2.0.5" "readable-stream": "^2.0.5"
}, },
@@ -5243,6 +5256,7 @@
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"core-util-is": "~1.0.0", "core-util-is": "~1.0.0",
"inherits": "~2.0.3", "inherits": "~2.0.3",
@@ -5258,7 +5272,8 @@
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/lazystream/node_modules/string_decoder": { "node_modules/lazystream/node_modules/string_decoder": {
"version": "1.1.1", "version": "1.1.1",
@@ -5266,6 +5281,7 @@
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"safe-buffer": "~5.1.0" "safe-buffer": "~5.1.0"
} }
@@ -5302,35 +5318,40 @@
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/lodash.difference": { "node_modules/lodash.difference": {
"version": "4.5.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
"integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/lodash.flatten": { "node_modules/lodash.flatten": {
"version": "4.4.0", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
"integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/lodash.isplainobject": { "node_modules/lodash.isplainobject": {
"version": "4.0.6", "version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/lodash.union": { "node_modules/lodash.union": {
"version": "4.6.0", "version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
"integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/lowercase-keys": { "node_modules/lowercase-keys": {
"version": "2.0.0", "version": "2.0.0",
@@ -5985,6 +6006,26 @@
"node": ">=10.4.0" "node": ">=10.4.0"
} }
}, },
"node_modules/png2icons": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/png2icons/-/png2icons-2.0.1.tgz",
"integrity": "sha512-GDEQJr8OG4e6JMp7mABtXFSEpgJa1CCpbQiAR+EjhkHJHnUL9zPPtbOrjsMD8gUbikgv3j7x404b0YJsV3aVFA==",
"dev": true,
"license": "MIT",
"bin": {
"png2icons": "png2icons-cli.js"
}
},
"node_modules/pngjs": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
"integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.19.0"
}
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.6", "version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@@ -6005,7 +6046,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.11",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
@@ -6154,7 +6194,8 @@
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/progress": { "node_modules/progress": {
"version": "2.0.3", "version": "2.0.3",
@@ -6303,6 +6344,7 @@
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"inherits": "^2.0.3", "inherits": "^2.0.3",
"string_decoder": "^1.1.1", "string_decoder": "^1.1.1",
@@ -6318,6 +6360,7 @@
"integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"minimatch": "^5.1.0" "minimatch": "^5.1.0"
} }
@@ -6535,7 +6578,8 @@
"url": "https://feross.org/support" "url": "https://feross.org/support"
} }
], ],
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/safer-buffer": { "node_modules/safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
@@ -6601,7 +6645,6 @@
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz", "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz",
"integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==", "integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=10" "node": ">=10"
} }
@@ -6729,7 +6772,6 @@
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.10.tgz", "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.10.tgz",
"integrity": "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew==", "integrity": "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.1.0", "csstype": "^3.1.0",
"seroval": "~1.3.0", "seroval": "~1.3.0",
@@ -6849,6 +6891,7 @@
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"safe-buffer": "~5.2.0" "safe-buffer": "~5.2.0"
} }
@@ -7106,6 +7149,7 @@
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"bl": "^4.0.3", "bl": "^4.0.3",
"end-of-stream": "^1.4.1", "end-of-stream": "^1.4.1",
@@ -7489,7 +7533,6 @@
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.21.3", "esbuild": "^0.21.3",
"postcss": "^8.4.43", "postcss": "^8.4.43",
@@ -8152,6 +8195,7 @@
"integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"archiver-utils": "^3.0.4", "archiver-utils": "^3.0.4",
"compress-commons": "^4.1.2", "compress-commons": "^4.1.2",
@@ -8167,6 +8211,7 @@
"integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"glob": "^7.2.3", "glob": "^7.2.3",
"graceful-fs": "^4.2.0", "graceful-fs": "^4.2.0",

View File

@@ -1,7 +1,7 @@
{ {
"name": "@opencode-ai/client", "name": "@opencode-ai/client",
"version": "0.1.0", "version": "0.1.0",
"description": "OpenCode desktop client - multi-instance, multi-session AI coding interface", "description": "CodeNomad desktop client - multi-instance, multi-session AI coding interface",
"author": "OpenCode Team", "author": "OpenCode Team",
"type": "module", "type": "module",
"main": "dist/main/main.js", "main": "dist/main/main.js",
@@ -41,6 +41,8 @@
"@tsconfig/bun": "^1.0.9", "@tsconfig/bun": "^1.0.9",
"autoprefixer": "10.4.21", "autoprefixer": "10.4.21",
"electron": "39.0.0", "electron": "39.0.0",
"png2icons": "^2.0.1",
"pngjs": "^7.0.0",
"electron-builder": "^24.0.0", "electron-builder": "^24.0.0",
"electron-vite": "4.0.1", "electron-vite": "4.0.1",
"postcss": "8.5.6", "postcss": "8.5.6",
@@ -51,7 +53,7 @@
}, },
"build": { "build": {
"appId": "ai.opencode.client", "appId": "ai.opencode.client",
"productName": "OpenCode Client", "productName": "CodeNomad",
"directories": { "directories": {
"output": "release", "output": "release",
"buildResources": "electron/resources" "buildResources": "electron/resources"

View File

@@ -111,7 +111,7 @@ const platform = process.argv[2] || "mac"
console.log(` console.log(`
╔════════════════════════════════════════╗ ╔════════════════════════════════════════╗
OpenCode Client - Binary Builder ║ CodeNomad - Binary Builder
╚════════════════════════════════════════╝ ╚════════════════════════════════════════╝
`) `)

155
scripts/generate-icons.js Normal file
View File

@@ -0,0 +1,155 @@
#!/usr/bin/env node
import { mkdirSync, readFileSync, writeFileSync } from "fs"
import { resolve, join, basename } from "path"
import { PNG } from "pngjs"
import png2icons from "png2icons"
function printUsage() {
console.log(`\nUsage: node scripts/generate-icons.js <input.png> [outputDir] [--name icon] [--radius 0.22]\n\nOptions:\n --name Base filename for generated assets (default: icon)\n --radius Corner radius ratio between 0 and 0.5 (default: 0.22)\n --help Show this message\n`)
}
function parseArgs(argv) {
const args = [...argv]
const options = {
name: "icon",
radius: 0.22,
}
for (let i = 0; i < args.length; i++) {
const token = args[i]
if (token === "--help" || token === "-h") {
options.help = true
continue
}
if (token === "--name" && i + 1 < args.length) {
options.name = args[i + 1]
i++
continue
}
if (token === "--radius" && i + 1 < args.length) {
options.radius = Number(args[i + 1])
i++
continue
}
if (!options.input) {
options.input = token
continue
}
if (!options.output) {
options.output = token
continue
}
}
return options
}
function applyRoundedCorners(png, ratio) {
const { width, height, data } = png
const clamped = Math.max(0, Math.min(ratio, 0.5))
if (clamped === 0) return png
const radius = Math.max(1, Math.min(width, height) * clamped)
const radiusSq = radius * radius
const rightThreshold = width - radius
const bottomThreshold = height - radius
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const idx = (width * y + x) * 4
if (data[idx + 3] === 0) continue
const px = x + 0.5
const py = y + 0.5
const inLeft = px < radius
const inRight = px > rightThreshold
const inTop = py < radius
const inBottom = py > bottomThreshold
let outside = false
if (inLeft && inTop) {
outside = (px - radius) ** 2 + (py - radius) ** 2 > radiusSq
} else if (inRight && inTop) {
outside = (px - rightThreshold) ** 2 + (py - radius) ** 2 > radiusSq
} else if (inLeft && inBottom) {
outside = (px - radius) ** 2 + (py - bottomThreshold) ** 2 > radiusSq
} else if (inRight && inBottom) {
outside = (px - rightThreshold) ** 2 + (py - bottomThreshold) ** 2 > radiusSq
}
if (outside) {
data[idx + 3] = 0
}
}
}
return png
}
async function main() {
const args = parseArgs(process.argv.slice(2))
if (args.help || !args.input) {
printUsage()
process.exit(args.help ? 0 : 1)
}
const inputPath = resolve(args.input)
const outputDir = resolve(args.output || "electron/resources")
const baseName = args.name || basename(inputPath, ".png")
const radiusRatio = Number.isFinite(args.radius) ? args.radius : 0.22
let buffer
try {
buffer = readFileSync(inputPath)
} catch (error) {
console.error(`Failed to read ${inputPath}:`, error.message)
process.exit(1)
}
let png
try {
png = PNG.sync.read(buffer)
} catch (error) {
console.error("Input must be a valid PNG:", error.message)
process.exit(1)
}
applyRoundedCorners(png, radiusRatio)
const roundedBuffer = PNG.sync.write(png)
try {
mkdirSync(outputDir, { recursive: true })
} catch (error) {
console.error("Failed to create output directory:", error.message)
process.exit(1)
}
const pngPath = join(outputDir, `${baseName}.png`)
writeFileSync(pngPath, roundedBuffer)
const icns = png2icons.createICNS(roundedBuffer, png2icons.BICUBIC, false)
if (!icns) {
console.error("Failed to create ICNS file. Make sure the source PNG is at least 256x256.")
process.exit(1)
}
writeFileSync(join(outputDir, `${baseName}.icns`), icns)
const ico = png2icons.createICO(roundedBuffer, png2icons.BICUBIC, false)
if (!ico) {
console.error("Failed to create ICO file. Make sure the source PNG is at least 256x256.")
process.exit(1)
}
writeFileSync(join(outputDir, `${baseName}.ico`), ico)
console.log(`\nGenerated assets in ${outputDir}:`)
console.log(`- ${baseName}.png`)
console.log(`- ${baseName}.icns`)
console.log(`- ${baseName}.ico`)
}
main()

View File

@@ -20,9 +20,6 @@ const AdvancedSettingsModal: Component<AdvancedSettingsModalProps> = (props) =>
<Dialog.Content class="modal-surface w-full max-w-5xl max-h-[90vh] flex flex-col overflow-hidden"> <Dialog.Content class="modal-surface w-full max-w-5xl max-h-[90vh] flex flex-col overflow-hidden">
<header class="px-6 py-4 border-b" style={{ "border-color": "var(--border-base)" }}> <header class="px-6 py-4 border-b" style={{ "border-color": "var(--border-base)" }}>
<Dialog.Title class="text-xl font-semibold text-primary">Advanced Settings</Dialog.Title> <Dialog.Title class="text-xl font-semibold text-primary">Advanced Settings</Dialog.Title>
<Dialog.Description class="text-sm text-secondary mt-1">
Configure the OpenCode binary and environment variables used when launching new instances.
</Dialog.Description>
</header> </header>
<div class="flex-1 overflow-y-auto p-6 space-y-6"> <div class="flex-1 overflow-y-auto p-6 space-y-6">

View File

@@ -1,5 +1,7 @@
import { Component } from "solid-js" import { Component } from "solid-js"
import { Folder, Loader2 } from "lucide-solid" import { Loader2 } from "lucide-solid"
const codeNomadIcon = new URL("../../images/CodeNomad-Icon.png", import.meta.url).href
interface EmptyStateProps { interface EmptyStateProps {
onSelectFolder: () => void onSelectFolder: () => void
@@ -11,13 +13,13 @@ const EmptyState: Component<EmptyStateProps> = (props) => {
<div class="flex h-full w-full items-center justify-center bg-surface-secondary"> <div class="flex h-full w-full items-center justify-center bg-surface-secondary">
<div class="max-w-[500px] px-8 py-12 text-center"> <div class="max-w-[500px] px-8 py-12 text-center">
<div class="mb-8 flex justify-center"> <div class="mb-8 flex justify-center">
<Folder class="h-16 w-16 icon-muted" /> <img src={codeNomadIcon} alt="CodeNomad logo" class="h-24 w-auto" loading="lazy" />
</div> </div>
<h1 class="mb-4 text-2xl font-semibold text-primary">Welcome to OpenCode Client</h1> <h1 class="mb-3 text-3xl font-semibold text-primary">CodeNomad</h1>
<p class="mb-8 text-base text-secondary">Select a folder to start coding with AI</p> <p class="mb-8 text-base text-secondary">Select a folder to start coding with AI</p>
<button <button
onClick={props.onSelectFolder} onClick={props.onSelectFolder}
disabled={props.isLoading} disabled={props.isLoading}

View File

@@ -4,6 +4,8 @@ import { recentFolders, removeRecentFolder, preferences, updateLastUsedBinary }
import AdvancedSettingsModal from "./advanced-settings-modal" import AdvancedSettingsModal from "./advanced-settings-modal"
import Kbd from "./kbd" import Kbd from "./kbd"
const codeNomadLogo = new URL("../../images/CodeNomad-Icon.png", import.meta.url).href
interface FolderSelectionViewProps { interface FolderSelectionViewProps {
onSelectFolder: (folder?: string, binaryPath?: string) => void onSelectFolder: (folder?: string, binaryPath?: string) => void
isLoading?: boolean isLoading?: boolean
@@ -202,171 +204,169 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
class="flex h-screen w-full items-start justify-center overflow-hidden py-6 relative" class="flex h-screen w-full items-start justify-center overflow-hidden py-6 relative"
style="background-color: var(--surface-secondary)" style="background-color: var(--surface-secondary)"
> >
<div <div
class="w-full max-w-3xl h-full max-h-[90vh] px-8 flex flex-col overflow-hidden" class="w-full max-w-3xl h-full px-8 pb-2 flex flex-col overflow-hidden"
aria-busy={isLoading() ? "true" : "false"} aria-busy={isLoading() ? "true" : "false"}
> >
<div class="mb-6 text-center shrink-0"> <div class="mb-6 text-center shrink-0">
<div class="mb-3 flex justify-center"> <div class="mb-3 flex justify-center">
<Folder class="h-16 w-16 icon-muted" /> <img src={codeNomadLogo} alt="CodeNomad logo" class="h-48 w-auto" loading="lazy" />
</div> </div>
<h1 class="mb-2 text-2xl font-semibold text-primary">Welcome to OpenCode</h1> <h1 class="mb-2 text-3xl font-semibold text-primary">CodeNomad</h1>
<p class="text-base text-secondary">Select a folder to start coding with AI</p> <p class="text-base text-secondary">Select a folder to start coding with AI</p>
</div> </div>
<div class="space-y-4 flex-1 min-h-0 overflow-hidden flex flex-col"> <div class="space-y-4 flex-1 min-h-0 overflow-hidden flex flex-col">
<Show <Show
when={folders().length > 0} when={folders().length > 0}
fallback={ fallback={
<div class="panel panel-empty-state flex-1"> <div class="panel panel-empty-state flex-1">
<div class="panel-empty-state-icon"> <div class="panel-empty-state-icon">
<Clock class="w-12 h-12 mx-auto" /> <Clock class="w-12 h-12 mx-auto" />
</div>
<p class="panel-empty-state-title">No Recent Folders</p>
<p class="panel-empty-state-description">Browse for a folder to get started</p>
</div> </div>
<p class="panel-empty-state-title">No Recent Folders</p> }
<p class="panel-empty-state-description">Browse for a folder to get started</p> >
</div> <div class="panel flex flex-col flex-1 min-h-0">
} <div class="panel-header">
> <h2 class="panel-title">Recent Folders</h2>
<div class="panel flex flex-col flex-1 min-h-0"> <p class="panel-subtitle">
<div class="panel-header"> {folders().length} {folders().length === 1 ? "folder" : "folders"} available
<h2 class="panel-title">Recent Folders</h2> </p>
<p class="panel-subtitle"> </div>
{folders().length} {folders().length === 1 ? "folder" : "folders"} available <div class="panel-list panel-list--fill flex-1 min-h-0 overflow-auto" ref={(el) => (recentListRef = el)}>
</p> <For each={folders()}>
</div> {(folder, index) => (
<div class="panel-list panel-list--fill flex-1 min-h-0 overflow-auto" ref={(el) => (recentListRef = el)}> <div
<For each={folders()}> class="panel-list-item"
{(folder, index) => ( classList={{
<div "panel-list-item-highlight": focusMode() === "recent" && selectedIndex() === index(),
class="panel-list-item" "panel-list-item-disabled": isLoading(),
classList={{ }}
"panel-list-item-highlight": focusMode() === "recent" && selectedIndex() === index(), >
"panel-list-item-disabled": isLoading(), <div class="flex items-center gap-2 w-full px-1">
}} <button
> data-folder-index={index()}
<div class="flex items-center gap-2 w-full px-1"> class="panel-list-item-content flex-1"
<button disabled={isLoading()}
data-folder-index={index()} onClick={() => handleFolderSelect(folder.path)}
class="panel-list-item-content flex-1" onMouseEnter={() => {
disabled={isLoading()} if (isLoading()) return
onClick={() => handleFolderSelect(folder.path)} setFocusMode("recent")
onMouseEnter={() => { setSelectedIndex(index())
if (isLoading()) return }}
setFocusMode("recent") >
setSelectedIndex(index()) <div class="flex items-center justify-between gap-3 w-full">
}} <div class="flex-1 min-w-0">
> <div class="flex items-center gap-2 mb-1">
<div class="flex items-center justify-between gap-3 w-full"> <Folder class="w-4 h-4 flex-shrink-0 icon-muted" />
<div class="flex-1 min-w-0"> <span class="text-sm font-medium truncate text-primary">
<div class="flex items-center gap-2 mb-1"> {folder.path.split("/").pop()}
<Folder class="w-4 h-4 flex-shrink-0 icon-muted" /> </span>
<span class="text-sm font-medium truncate text-primary"> </div>
{folder.path.split("/").pop()} <div class="text-xs font-mono truncate pl-6 text-muted">
</span> {getDisplayPath(folder.path)}
</div> </div>
<div class="text-xs font-mono truncate pl-6 text-muted"> <div class="text-xs mt-1 pl-6 text-muted">
{getDisplayPath(folder.path)} {formatRelativeTime(folder.lastAccessed)}
</div> </div>
<div class="text-xs mt-1 pl-6 text-muted">
{formatRelativeTime(folder.lastAccessed)}
</div> </div>
<Show when={focusMode() === "recent" && selectedIndex() === index()}>
<kbd class="kbd"></kbd>
</Show>
</div> </div>
<Show when={focusMode() === "recent" && selectedIndex() === index()}> </button>
<kbd class="kbd"></kbd> <button
</Show> onClick={(e) => handleRemove(folder.path, e)}
</div> disabled={isLoading()}
</button> class="p-2 transition-all hover:bg-red-100 dark:hover:bg-red-900/30 opacity-70 hover:opacity-100 rounded"
<button title="Remove from recent"
onClick={(e) => handleRemove(folder.path, e)} >
disabled={isLoading()} <Trash2 class="w-3.5 h-3.5 transition-colors icon-muted hover:text-red-600 dark:hover:text-red-400" />
class="p-2 transition-all hover:bg-red-100 dark:hover:bg-red-900/30 opacity-70 hover:opacity-100 rounded" </button>
title="Remove from recent" </div>
>
<Trash2 class="w-3.5 h-3.5 transition-colors icon-muted hover:text-red-600 dark:hover:text-red-400" />
</button>
</div> </div>
</div> )}
)} </For>
</For>
</div>
</div>
</Show>
<div class="panel shrink-0">
<div class="panel-header">
<h2 class="panel-title">Browse for Folder</h2>
<p class="panel-subtitle">Select any folder on your computer</p>
</div>
<div class="panel-body">
<button
onClick={handleBrowse}
disabled={props.isLoading}
class="button-primary w-full flex items-center justify-center text-sm disabled:cursor-not-allowed"
onMouseEnter={() => setFocusMode("new")}
>
<div class="flex items-center gap-2">
<FolderPlus class="w-4 h-4" />
<span>{props.isLoading ? "Opening..." : "Browse Folders"}</span>
</div> </div>
<Kbd shortcut="cmd+n" class="ml-2" />
</button>
</div>
{/* Advanced settings section */}
<div class="panel-section w-full">
<button
onClick={() => setIsAdvancedModalOpen(true)}
class="panel-section-header w-full justify-between"
>
<div class="flex items-center gap-2">
<Settings class="w-4 h-4 icon-muted" />
<span class="text-sm font-medium text-secondary">Advanced Settings</span>
</div>
<ChevronRight class="w-4 h-4 icon-muted" />
</button>
<div class="panel-section-content text-sm text-muted">
Configure the OpenCode binary and environment variables.
</div>
</div>
</div>
</div>
<div class="mt-4 panel panel-footer shrink-0">
<div class="panel-footer-hints">
<Show when={folders().length > 0}>
<div class="flex items-center gap-1.5">
<kbd class="kbd"></kbd>
<kbd class="kbd"></kbd>
<span>Navigate</span>
</div>
<div class="flex items-center gap-1.5">
<kbd class="kbd">Enter</kbd>
<span>Select</span>
</div>
<div class="flex items-center gap-1.5">
<kbd class="kbd">Del</kbd>
<span>Remove</span>
</div> </div>
</Show> </Show>
<div class="flex items-center gap-1.5">
<Kbd shortcut="cmd+n" /> <div class="panel shrink-0">
<span>Browse</span> <div class="panel-header">
<h2 class="panel-title">Browse for Folder</h2>
<p class="panel-subtitle">Select any folder on your computer</p>
</div>
<div class="panel-body">
<button
onClick={handleBrowse}
disabled={props.isLoading}
class="button-primary w-full flex items-center justify-center text-sm disabled:cursor-not-allowed"
onMouseEnter={() => setFocusMode("new")}
>
<div class="flex items-center gap-2">
<FolderPlus class="w-4 h-4" />
<span>{props.isLoading ? "Opening..." : "Browse Folders"}</span>
</div>
<Kbd shortcut="cmd+n" class="ml-2" />
</button>
</div>
{/* Advanced settings section */}
<div class="panel-section w-full">
<button
onClick={() => setIsAdvancedModalOpen(true)}
class="panel-section-header w-full justify-between"
>
<div class="flex items-center gap-2">
<Settings class="w-4 h-4 icon-muted" />
<span class="text-sm font-medium text-secondary">Advanced Settings</span>
</div>
<ChevronRight class="w-4 h-4 icon-muted" />
</button>
</div>
</div>
</div>
<div class="mt-1 panel panel-footer shrink-0">
<div class="panel-footer-hints">
<Show when={folders().length > 0}>
<div class="flex items-center gap-1.5">
<kbd class="kbd"></kbd>
<kbd class="kbd"></kbd>
<span>Navigate</span>
</div>
<div class="flex items-center gap-1.5">
<kbd class="kbd">Enter</kbd>
<span>Select</span>
</div>
<div class="flex items-center gap-1.5">
<kbd class="kbd">Del</kbd>
<span>Remove</span>
</div>
</Show>
<div class="flex items-center gap-1.5">
<Kbd shortcut="cmd+n" />
<span>Browse</span>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> <Show when={isLoading()}>
<Show when={isLoading()}> <div class="folder-loading-overlay">
<div class="folder-loading-overlay"> <div class="folder-loading-indicator">
<div class="folder-loading-indicator"> <div class="spinner" />
<div class="spinner" /> <p class="folder-loading-text">Starting instance</p>
<p class="folder-loading-text">Starting instance</p> <p class="folder-loading-subtext">Hang tight while we prepare your workspace.</p>
<p class="folder-loading-subtext">Hang tight while we prepare your workspace.</p> </div>
</div> </div>
</div> </Show>
</Show> </div>
</div>
<AdvancedSettingsModal <AdvancedSettingsModal
open={isAdvancedModalOpen()} open={isAdvancedModalOpen()}

View File

@@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OpenCode Client</title> <title>CodeNomad</title>
<style> <style>
:root { :root {
color-scheme: light dark; color-scheme: light dark;

View File

@@ -1,6 +1,6 @@
# Task Management # Task Management
This directory contains the task breakdown for building the OpenCode Client. This directory contains the task breakdown for building CodeNomad.
## Structure ## Structure

View File

@@ -173,7 +173,7 @@ packages/opencode-client/
**src/App.tsx:** **src/App.tsx:**
- Basic component with "Hello OpenCode Client" - Basic component with "Hello CodeNomad"
- Display environment info - Display environment info
- Basic styling with TailwindCSS - Basic styling with TailwindCSS
@@ -203,7 +203,7 @@ packages/opencode-client/
**electron-builder.yml** or in package.json: **electron-builder.yml** or in package.json:
- appId: ai.opencode.client - appId: ai.opencode.client
- Product name: OpenCode Client - Product name: CodeNomad
- Build resources: electron/resources - Build resources: electron/resources
- Files to include: dist/, package.json - Files to include: dist/, package.json
- Directories: - Directories:
@@ -237,7 +237,7 @@ release/
1. Run `bun install` 1. Run `bun install`
2. Run `bun run dev` 2. Run `bun run dev`
3. Verify Electron window opens 3. Verify Electron window opens
4. Verify "Hello OpenCode Client" displays 4. Verify "Hello CodeNomad" displays
5. Make a change to App.tsx 5. Make a change to App.tsx
6. Verify hot reload updates UI 6. Verify hot reload updates UI
7. Run `bun run typecheck` 7. Run `bun run typecheck`

View File

@@ -30,7 +30,6 @@ Create the initial empty state interface that appears when no instances are runn
- Centered container - Centered container
- Large folder icon (from lucide-solid) - Large folder icon (from lucide-solid)
- Heading: "Welcome to OpenCode Client"
- Subheading: "Select a folder to start coding with AI" - Subheading: "Select a folder to start coding with AI"
- Primary button: "Select Folder" - Primary button: "Select Folder"
- Helper text: "Keyboard shortcut: Cmd/Ctrl+N" - Helper text: "Keyboard shortcut: Cmd/Ctrl+N"