chore: rebrand client and add icon tooling
This commit is contained in:
24
BUILD.md
24
BUILD.md
@@ -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
|
||||
|
||||
@@ -81,17 +81,17 @@ Binaries are generated in the `release/` directory:
|
||||
|
||||
```
|
||||
release/
|
||||
├── OpenCode Client-0.1.0-mac-universal.dmg
|
||||
├── OpenCode Client-0.1.0-mac-universal.zip
|
||||
├── OpenCode Client-0.1.0-win-x64.exe
|
||||
├── OpenCode Client-0.1.0-linux-x64.AppImage
|
||||
├── CodeNomad-0.1.0-mac-universal.dmg
|
||||
├── CodeNomad-0.1.0-mac-universal.zip
|
||||
├── CodeNomad-0.1.0-win-x64.exe
|
||||
├── CodeNomad-0.1.0-linux-x64.AppImage
|
||||
└── ...
|
||||
```
|
||||
|
||||
## File Naming Convention
|
||||
|
||||
```
|
||||
OpenCode Client-{version}-{os}-{arch}.{ext}
|
||||
CodeNomad-{version}-{os}-{arch}.{ext}
|
||||
```
|
||||
|
||||
- **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.
|
||||
|
||||
## 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
|
||||
|
||||
Remove previous builds:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# OpenCode Client - Development Progress
|
||||
# CodeNomad - Development Progress
|
||||
|
||||
## Completed Tasks
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# OpenCode Client
|
||||
# CodeNomad
|
||||
|
||||
A cross-platform desktop application for interacting with OpenCode servers, built with Electron and SolidJS.
|
||||
|
||||
## 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.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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
|
||||
|
||||
|
||||
@@ -298,7 +298,7 @@ When the time comes:
|
||||
|
||||
> **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 8+:** Make it fast
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# OpenCode Client - Project Summary
|
||||
# CodeNomad - Project Summary
|
||||
|
||||
## 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
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# OpenCode Client Architecture
|
||||
# CodeNomad Architecture
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# OpenCode Client Build Roadmap
|
||||
# CodeNomad Build Roadmap
|
||||
|
||||
## 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.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## 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
|
||||
|
||||
@@ -346,7 +346,7 @@ Appears when instance starts:
|
||||
│ │
|
||||
│ [Folder Icon] │
|
||||
│ │
|
||||
│ Welcome to OpenCode Client │
|
||||
│ Start Coding with AI │
|
||||
│ │
|
||||
│ Select a folder to start coding with AI │
|
||||
│ │
|
||||
|
||||
@@ -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 { createApplicationMenu } from "./menu"
|
||||
import { setupInstanceIPC } from "./ipc"
|
||||
@@ -15,9 +15,18 @@ setupStorageIPC()
|
||||
|
||||
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() {
|
||||
const prefersDark = true //nativeTheme.shouldUseDarkColors
|
||||
const backgroundColor = prefersDark ? "#1a1a1a" : "#ffffff"
|
||||
const iconPath = getIconPath()
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1400,
|
||||
@@ -25,6 +34,7 @@ function createWindow() {
|
||||
minWidth: 800,
|
||||
minHeight: 600,
|
||||
backgroundColor,
|
||||
icon: iconPath,
|
||||
webPreferences: {
|
||||
preload: join(__dirname, "../preload/index.js"),
|
||||
contextIsolation: true,
|
||||
@@ -65,6 +75,13 @@ app.whenReady().then(() => {
|
||||
app.on("browser-window-created", (_, window) => {
|
||||
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())
|
||||
|
||||
@@ -7,7 +7,7 @@ export function createApplicationMenu(mainWindow: BrowserWindow) {
|
||||
...(isMac
|
||||
? [
|
||||
{
|
||||
label: "OpenCode Client",
|
||||
label: "CodeNomad",
|
||||
submenu: [
|
||||
{ role: "about" as const },
|
||||
{ type: "separator" as const },
|
||||
|
||||
BIN
electron/resources/icon.icns
Normal file
BIN
electron/resources/icon.icns
Normal file
Binary file not shown.
BIN
electron/resources/icon.ico
Normal file
BIN
electron/resources/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 422 KiB |
BIN
electron/resources/icon.png
Normal file
BIN
electron/resources/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
BIN
images/CodeNomad-Icon-original.png
Normal file
BIN
images/CodeNomad-Icon-original.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
BIN
images/CodeNomad-Icon.png
Normal file
BIN
images/CodeNomad-Icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
85
package-lock.json
generated
85
package-lock.json
generated
@@ -26,6 +26,8 @@
|
||||
"electron": "39.0.0",
|
||||
"electron-builder": "^24.0.0",
|
||||
"electron-vite": "4.0.1",
|
||||
"png2icons": "^2.0.1",
|
||||
"pngjs": "^7.0.0",
|
||||
"postcss": "8.5.6",
|
||||
"tailwindcss": "3",
|
||||
"typescript": "^5.3.0",
|
||||
@@ -77,7 +79,6 @@
|
||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
@@ -2288,7 +2289,6 @@
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -2464,6 +2464,7 @@
|
||||
"integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"archiver-utils": "^2.1.0",
|
||||
"async": "^3.2.4",
|
||||
@@ -2483,6 +2484,7 @@
|
||||
"integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"glob": "^7.1.4",
|
||||
"graceful-fs": "^4.2.0",
|
||||
@@ -2505,6 +2507,7 @@
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
@@ -2520,7 +2523,8 @@
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/archiver-utils/node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
@@ -2528,6 +2532,7 @@
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
@@ -2746,6 +2751,7 @@
|
||||
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"buffer": "^5.5.0",
|
||||
"inherits": "^2.0.4",
|
||||
@@ -2821,7 +2827,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.19",
|
||||
"caniuse-lite": "^1.0.30001751",
|
||||
@@ -3285,6 +3290,7 @@
|
||||
"integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"buffer-crc32": "^0.2.13",
|
||||
"crc32-stream": "^4.0.2",
|
||||
@@ -3391,6 +3397,7 @@
|
||||
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"crc32": "bin/crc32.njs"
|
||||
},
|
||||
@@ -3404,6 +3411,7 @@
|
||||
"integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"crc-32": "^1.2.0",
|
||||
"readable-stream": "^3.4.0"
|
||||
@@ -3636,7 +3644,6 @@
|
||||
"integrity": "sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"app-builder-lib": "24.13.3",
|
||||
"builder-util": "24.13.1",
|
||||
@@ -3821,6 +3828,7 @@
|
||||
"integrity": "sha512-oHkV0iogWfyK+ah9ZIvMDpei1m9ZRpdXcvde1wTpra2U8AFDNNpqJdnin5z+PM1GbQ5BoaKCWas2HSjtR0HwMg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"app-builder-lib": "24.13.3",
|
||||
"archiver": "^5.3.1",
|
||||
@@ -3834,6 +3842,7 @@
|
||||
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^6.0.1",
|
||||
@@ -3849,6 +3858,7 @@
|
||||
"integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
@@ -3862,6 +3872,7 @@
|
||||
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
@@ -4343,7 +4354,8 @@
|
||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "8.1.0",
|
||||
@@ -5062,7 +5074,8 @@
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/isbinaryfile": {
|
||||
"version": "5.0.6",
|
||||
@@ -5124,7 +5137,6 @@
|
||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
@@ -5230,6 +5242,7 @@
|
||||
"integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"readable-stream": "^2.0.5"
|
||||
},
|
||||
@@ -5243,6 +5256,7 @@
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
@@ -5258,7 +5272,8 @@
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lazystream/node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
@@ -5266,6 +5281,7 @@
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
@@ -5302,35 +5318,40 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
||||
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash.difference": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
|
||||
"integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash.flatten": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
|
||||
"integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash.union": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
|
||||
"integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lowercase-keys": {
|
||||
"version": "2.0.0",
|
||||
@@ -5985,6 +6006,26 @@
|
||||
"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": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
@@ -6005,7 +6046,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -6154,7 +6194,8 @@
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/progress": {
|
||||
"version": "2.0.3",
|
||||
@@ -6303,6 +6344,7 @@
|
||||
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
@@ -6318,6 +6360,7 @@
|
||||
"integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"minimatch": "^5.1.0"
|
||||
}
|
||||
@@ -6535,7 +6578,8 @@
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
@@ -6601,7 +6645,6 @@
|
||||
"resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz",
|
||||
"integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
@@ -6729,7 +6772,6 @@
|
||||
"resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.10.tgz",
|
||||
"integrity": "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.1.0",
|
||||
"seroval": "~1.3.0",
|
||||
@@ -6849,6 +6891,7 @@
|
||||
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
@@ -7106,6 +7149,7 @@
|
||||
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"bl": "^4.0.3",
|
||||
"end-of-stream": "^1.4.1",
|
||||
@@ -7489,7 +7533,6 @@
|
||||
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
@@ -8152,6 +8195,7 @@
|
||||
"integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"archiver-utils": "^3.0.4",
|
||||
"compress-commons": "^4.1.2",
|
||||
@@ -8167,6 +8211,7 @@
|
||||
"integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"glob": "^7.2.3",
|
||||
"graceful-fs": "^4.2.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@opencode-ai/client",
|
||||
"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",
|
||||
"type": "module",
|
||||
"main": "dist/main/main.js",
|
||||
@@ -41,6 +41,8 @@
|
||||
"@tsconfig/bun": "^1.0.9",
|
||||
"autoprefixer": "10.4.21",
|
||||
"electron": "39.0.0",
|
||||
"png2icons": "^2.0.1",
|
||||
"pngjs": "^7.0.0",
|
||||
"electron-builder": "^24.0.0",
|
||||
"electron-vite": "4.0.1",
|
||||
"postcss": "8.5.6",
|
||||
@@ -51,7 +53,7 @@
|
||||
},
|
||||
"build": {
|
||||
"appId": "ai.opencode.client",
|
||||
"productName": "OpenCode Client",
|
||||
"productName": "CodeNomad",
|
||||
"directories": {
|
||||
"output": "release",
|
||||
"buildResources": "electron/resources"
|
||||
|
||||
@@ -111,7 +111,7 @@ const platform = process.argv[2] || "mac"
|
||||
|
||||
console.log(`
|
||||
╔════════════════════════════════════════╗
|
||||
║ OpenCode Client - Binary Builder ║
|
||||
║ CodeNomad - Binary Builder ║
|
||||
╚════════════════════════════════════════╝
|
||||
`)
|
||||
|
||||
|
||||
155
scripts/generate-icons.js
Normal file
155
scripts/generate-icons.js
Normal 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()
|
||||
@@ -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">
|
||||
<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.Description class="text-sm text-secondary mt-1">
|
||||
Configure the OpenCode binary and environment variables used when launching new instances.
|
||||
</Dialog.Description>
|
||||
</header>
|
||||
|
||||
<div class="flex-1 overflow-y-auto p-6 space-y-6">
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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 {
|
||||
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="max-w-[500px] px-8 py-12 text-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>
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
<button
|
||||
onClick={props.onSelectFolder}
|
||||
disabled={props.isLoading}
|
||||
|
||||
@@ -4,6 +4,8 @@ import { recentFolders, removeRecentFolder, preferences, updateLastUsedBinary }
|
||||
import AdvancedSettingsModal from "./advanced-settings-modal"
|
||||
import Kbd from "./kbd"
|
||||
|
||||
const codeNomadLogo = new URL("../../images/CodeNomad-Icon.png", import.meta.url).href
|
||||
|
||||
interface FolderSelectionViewProps {
|
||||
onSelectFolder: (folder?: string, binaryPath?: string) => void
|
||||
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"
|
||||
style="background-color: var(--surface-secondary)"
|
||||
>
|
||||
<div
|
||||
class="w-full max-w-3xl h-full max-h-[90vh] px-8 flex flex-col overflow-hidden"
|
||||
aria-busy={isLoading() ? "true" : "false"}
|
||||
>
|
||||
<div
|
||||
class="w-full max-w-3xl h-full px-8 pb-2 flex flex-col overflow-hidden"
|
||||
aria-busy={isLoading() ? "true" : "false"}
|
||||
>
|
||||
<div class="mb-6 text-center shrink-0">
|
||||
<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>
|
||||
<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>
|
||||
</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
|
||||
when={folders().length > 0}
|
||||
fallback={
|
||||
<div class="panel panel-empty-state flex-1">
|
||||
<div class="panel-empty-state-icon">
|
||||
<Clock class="w-12 h-12 mx-auto" />
|
||||
<Show
|
||||
when={folders().length > 0}
|
||||
fallback={
|
||||
<div class="panel panel-empty-state flex-1">
|
||||
<div class="panel-empty-state-icon">
|
||||
<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>
|
||||
<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>
|
||||
<p class="panel-subtitle">
|
||||
{folders().length} {folders().length === 1 ? "folder" : "folders"} available
|
||||
</p>
|
||||
</div>
|
||||
<div class="panel-list panel-list--fill flex-1 min-h-0 overflow-auto" ref={(el) => (recentListRef = el)}>
|
||||
<For each={folders()}>
|
||||
{(folder, index) => (
|
||||
<div
|
||||
class="panel-list-item"
|
||||
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()}
|
||||
class="panel-list-item-content flex-1"
|
||||
disabled={isLoading()}
|
||||
onClick={() => handleFolderSelect(folder.path)}
|
||||
onMouseEnter={() => {
|
||||
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">
|
||||
<Folder class="w-4 h-4 flex-shrink-0 icon-muted" />
|
||||
<span class="text-sm font-medium truncate text-primary">
|
||||
{folder.path.split("/").pop()}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs font-mono truncate pl-6 text-muted">
|
||||
{getDisplayPath(folder.path)}
|
||||
</div>
|
||||
<div class="text-xs mt-1 pl-6 text-muted">
|
||||
{formatRelativeTime(folder.lastAccessed)}
|
||||
}
|
||||
>
|
||||
<div class="panel flex flex-col flex-1 min-h-0">
|
||||
<div class="panel-header">
|
||||
<h2 class="panel-title">Recent Folders</h2>
|
||||
<p class="panel-subtitle">
|
||||
{folders().length} {folders().length === 1 ? "folder" : "folders"} available
|
||||
</p>
|
||||
</div>
|
||||
<div class="panel-list panel-list--fill flex-1 min-h-0 overflow-auto" ref={(el) => (recentListRef = el)}>
|
||||
<For each={folders()}>
|
||||
{(folder, index) => (
|
||||
<div
|
||||
class="panel-list-item"
|
||||
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()}
|
||||
class="panel-list-item-content flex-1"
|
||||
disabled={isLoading()}
|
||||
onClick={() => handleFolderSelect(folder.path)}
|
||||
onMouseEnter={() => {
|
||||
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">
|
||||
<Folder class="w-4 h-4 flex-shrink-0 icon-muted" />
|
||||
<span class="text-sm font-medium truncate text-primary">
|
||||
{folder.path.split("/").pop()}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs font-mono truncate pl-6 text-muted">
|
||||
{getDisplayPath(folder.path)}
|
||||
</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>
|
||||
<Show when={focusMode() === "recent" && selectedIndex() === index()}>
|
||||
<kbd class="kbd">↵</kbd>
|
||||
</Show>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => handleRemove(folder.path, e)}
|
||||
disabled={isLoading()}
|
||||
class="p-2 transition-all hover:bg-red-100 dark:hover:bg-red-900/30 opacity-70 hover:opacity-100 rounded"
|
||||
title="Remove from recent"
|
||||
>
|
||||
<Trash2 class="w-3.5 h-3.5 transition-colors icon-muted hover:text-red-600 dark:hover:text-red-400" />
|
||||
</button>
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => handleRemove(folder.path, e)}
|
||||
disabled={isLoading()}
|
||||
class="p-2 transition-all hover:bg-red-100 dark:hover:bg-red-900/30 opacity-70 hover:opacity-100 rounded"
|
||||
title="Remove from recent"
|
||||
>
|
||||
<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>
|
||||
</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>
|
||||
)}
|
||||
</For>
|
||||
</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>
|
||||
</Show>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<Kbd shortcut="cmd+n" />
|
||||
<span>Browse</span>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
<Show when={isLoading()}>
|
||||
<div class="folder-loading-overlay">
|
||||
<div class="folder-loading-indicator">
|
||||
<div class="spinner" />
|
||||
<p class="folder-loading-text">Starting instance…</p>
|
||||
<p class="folder-loading-subtext">Hang tight while we prepare your workspace.</p>
|
||||
<Show when={isLoading()}>
|
||||
<div class="folder-loading-overlay">
|
||||
<div class="folder-loading-indicator">
|
||||
<div class="spinner" />
|
||||
<p class="folder-loading-text">Starting instance…</p>
|
||||
<p class="folder-loading-subtext">Hang tight while we prepare your workspace.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<AdvancedSettingsModal
|
||||
open={isAdvancedModalOpen()}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>OpenCode Client</title>
|
||||
<title>CodeNomad</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Task Management
|
||||
|
||||
This directory contains the task breakdown for building the OpenCode Client.
|
||||
This directory contains the task breakdown for building CodeNomad.
|
||||
|
||||
## Structure
|
||||
|
||||
|
||||
@@ -173,7 +173,7 @@ packages/opencode-client/
|
||||
|
||||
**src/App.tsx:**
|
||||
|
||||
- Basic component with "Hello OpenCode Client"
|
||||
- Basic component with "Hello CodeNomad"
|
||||
- Display environment info
|
||||
- Basic styling with TailwindCSS
|
||||
|
||||
@@ -203,7 +203,7 @@ packages/opencode-client/
|
||||
**electron-builder.yml** or in package.json:
|
||||
|
||||
- appId: ai.opencode.client
|
||||
- Product name: OpenCode Client
|
||||
- Product name: CodeNomad
|
||||
- Build resources: electron/resources
|
||||
- Files to include: dist/, package.json
|
||||
- Directories:
|
||||
@@ -237,7 +237,7 @@ release/
|
||||
1. Run `bun install`
|
||||
2. Run `bun run dev`
|
||||
3. Verify Electron window opens
|
||||
4. Verify "Hello OpenCode Client" displays
|
||||
4. Verify "Hello CodeNomad" displays
|
||||
5. Make a change to App.tsx
|
||||
6. Verify hot reload updates UI
|
||||
7. Run `bun run typecheck`
|
||||
|
||||
@@ -30,7 +30,6 @@ Create the initial empty state interface that appears when no instances are runn
|
||||
|
||||
- Centered container
|
||||
- Large folder icon (from lucide-solid)
|
||||
- Heading: "Welcome to OpenCode Client"
|
||||
- Subheading: "Select a folder to start coding with AI"
|
||||
- Primary button: "Select Folder"
|
||||
- Helper text: "Keyboard shortcut: Cmd/Ctrl+N"
|
||||
|
||||
Reference in New Issue
Block a user