Compare commits
13 Commits
v0.13.1-de
...
v0.13.3-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d6a5bcdc0 | ||
|
|
514b187b00 | ||
|
|
240acb7729 | ||
|
|
0af79002ed | ||
|
|
f3981a1cce | ||
|
|
031e8d5717 | ||
|
|
995fb3b6a3 | ||
|
|
aeb0ff11b3 | ||
|
|
b61cfbd9f9 | ||
|
|
481dd1a88a | ||
|
|
3f6cdd36f3 | ||
|
|
fe932c8307 | ||
|
|
64ac885157 |
171
README.md
171
README.md
@@ -1,128 +1,127 @@
|
||||
# CodeNomad
|
||||
|
||||
## A fast, multi-instance workspace for running OpenCode sessions.
|
||||
## The AI Coding Cockpit for OpenCode
|
||||
|
||||
CodeNomad is built for people who live inside OpenCode for hours on end and need a cockpit, not a kiosk. It delivers a premium, low-latency workspace that favors speed, clarity, and direct control.
|
||||
CodeNomad transforms OpenCode from a terminal tool into a **premium desktop workspace** — built for developers who live inside AI coding sessions for hours and need control, speed, and clarity.
|
||||
|
||||
> OpenCode gives you the engine. CodeNomad gives you the cockpit.
|
||||
|
||||

|
||||
_Manage multiple OpenCode sessions side-by-side._
|
||||
|
||||
<details>
|
||||
<summary>📸 More Screenshots</summary>
|
||||
---
|
||||
|
||||

|
||||
_Global command palette for keyboard-first control._
|
||||
## Features
|
||||
|
||||

|
||||
_Rich media previews for images and assets._
|
||||
- **🚀 Multi-Instance Workspace**
|
||||
- **🌐 Remote Access**
|
||||
- **🧠 Session Management**
|
||||
- **🎙️ Voice Input & Speech**
|
||||
- **🌳 Git Worktrees**
|
||||
- **💬 Rich Message Experience**
|
||||
- **⌨️ Command Palette**
|
||||
- **📁 File System Browser**
|
||||
- **🔐 Authentication & Security**
|
||||
- **🔔 Notifications**
|
||||
- **🎨 Theming**
|
||||
- **🌍 Internationalization**
|
||||
|
||||

|
||||
_Browser support via CodeNomad Server._
|
||||
|
||||
</details>
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
Choose the way that fits your workflow:
|
||||
### 🖥️ Desktop App
|
||||
|
||||
### 🖥️ Desktop App (Recommended)
|
||||
The best experience. A native application (Electron-based) with global shortcuts, deeper system integration, and a dedicated window.
|
||||
Available as both Electron and Tauri builds — choose based on your preference.
|
||||
|
||||
- **Download**: Grab the latest installer for macOS, Windows, or Linux from the [Releases Page](https://github.com/shantur/CodeNomad/releases).
|
||||
- **Run**: Install and launch like any other app.
|
||||
Download the latest installer for your platform from [Releases](https://github.com/shantur/CodeNomad/releases).
|
||||
|
||||
### 🦀 Tauri App (Experimental)
|
||||
We are also working on a lightweight, high-performance version built with [Tauri](https://tauri.app). It is currently in active development.
|
||||
|
||||
- **Download**: Experimental builds are available on the [Releases Page](https://github.com/shantur/CodeNomad/releases).
|
||||
- **Source**: Check out `packages/tauri-app` if you're interested in contributing.
|
||||
| Platform | Formats |
|
||||
|----------|---------|
|
||||
| macOS | DMG, ZIP (Universal: Intel + Apple Silicon) |
|
||||
| Windows | NSIS Installer, ZIP (x64, ARM64) |
|
||||
| Linux | AppImage, deb, tar.gz (x64, ARM64) |
|
||||
|
||||
### 💻 CodeNomad Server
|
||||
Run CodeNomad as a local server and access it via your web browser. Perfect for remote development (SSH/VPN) or running as a service.
|
||||
|
||||
Run as a local server and access via browser. Perfect for remote development.
|
||||
|
||||
```bash
|
||||
npx @neuralnomads/codenomad --launch
|
||||
```
|
||||
|
||||
Full server/CLI documentation (flags + env vars, TLS, auth, remote access):
|
||||
- [packages/server/README.md](packages/server/README.md)
|
||||
|
||||
To see all available options:
|
||||
|
||||
```bash
|
||||
npx @neuralnomads/codenomad --help
|
||||
```
|
||||
See [Server Documentation](packages/server/README.md) for flags, TLS, auth, and remote access.
|
||||
|
||||
### 🧪 Dev Releases
|
||||
Bleeding-edge builds are published as GitHub pre-releases and are generated automatically from the `dev` branch.
|
||||
|
||||
Bleeding-edge builds from the `dev` branch:
|
||||
|
||||
```bash
|
||||
npx @neuralnomads/codenomad-dev --launch
|
||||
```
|
||||
|
||||
## Highlights
|
||||
|
||||
- **Multi-Instance**: Juggle several OpenCode sessions side-by-side with tabs.
|
||||
- **Long-Session Native**: Scroll through massive transcripts without hitches.
|
||||
- **Command Palette**: A single global palette to jump tabs, launch tools, and control everything.
|
||||
- **Deep Task Awareness**: Monitor background tasks and child sessions without losing flow.
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
- **[OpenCode CLI](https://opencode.ai)**: Must be installed and available in your `PATH`.
|
||||
- **Node.js 18+**: Required if running the CLI server or building from source.
|
||||
- **[OpenCode CLI](https://opencode.ai)** — must be installed and in your `PATH`
|
||||
- **Node.js 18+** — for server mode or building from source
|
||||
|
||||
## Troubleshooting
|
||||
---
|
||||
|
||||
### macOS says the app is damaged
|
||||
If macOS reports that "CodeNomad.app is damaged and can't be opened," Gatekeeper flagged the download because the app is not yet notarized. You can clear the quarantine flag after moving CodeNomad into `/Applications`:
|
||||
## Development
|
||||
|
||||
```bash
|
||||
xattr -l /Applications/CodeNomad.app
|
||||
xattr -dr com.apple.quarantine /Applications/CodeNomad.app
|
||||
```
|
||||
|
||||
After removing the quarantine attribute, launch the app normally. On Intel Macs you may also need to approve CodeNomad from **System Settings → Privacy & Security** the first time you run it.
|
||||
|
||||
### Linux (Wayland + NVIDIA): Tauri AppImage closes immediately
|
||||
On some Wayland compositor + NVIDIA driver setups, WebKitGTK can fail to initialize its DMA-BUF/GBM path and the Tauri build may exit right away.
|
||||
|
||||
Try running with one of these environment variables:
|
||||
|
||||
```bash
|
||||
# Most reliable workaround (can reduce rendering performance)
|
||||
WEBKIT_DISABLE_DMABUF_RENDERER=1 codenomad
|
||||
|
||||
# Alternative for some Wayland setups
|
||||
__NV_DISABLE_EXPLICIT_SYNC=1 codenomad
|
||||
```
|
||||
|
||||
If you're running the Tauri AppImage and want the workaround applied every time, create a tiny wrapper script on your `PATH`:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
export WEBKIT_DISABLE_DMABUF_RENDERER=1
|
||||
exec ~/.local/share/bauh/appimage/installed/codenomad/CodeNomad-Tauri-0.4.0-linux-x64.AppImage "$@"
|
||||
```
|
||||
|
||||
Upstream tracking: https://github.com/tauri-apps/tauri/issues/10702
|
||||
|
||||
## Architecture & Development
|
||||
|
||||
CodeNomad is a monorepo split into specialized packages. If you want to contribute or build from source, check out the individual package documentation:
|
||||
CodeNomad is a monorepo built with:
|
||||
|
||||
| Package | Description |
|
||||
|---------|-------------|
|
||||
| **[packages/electron-app](packages/electron-app/README.md)** | The native desktop application shell. Wraps the UI and Server. |
|
||||
| **[packages/server](packages/server/README.md)** | The core logic and CLI. Manages workspaces, proxies OpenCode, and serves the API. |
|
||||
| **[packages/ui](packages/ui/README.md)** | The SolidJS-based frontend. Fast, reactive, and beautiful. |
|
||||
| **[packages/server](packages/server/README.md)** | Core logic & CLI — workspaces, OpenCode proxy, API, auth, speech |
|
||||
| **[packages/ui](packages/ui/README.md)** | SolidJS frontend — reactive, fast, beautiful |
|
||||
| **[packages/electron-app](packages/electron-app/README.md)** | Desktop shell — process management, IPC, native dialogs |
|
||||
| **[packages/tauri-app](packages/tauri-app)** | Tauri desktop shell (experimental) |
|
||||
|
||||
### Quick Build
|
||||
To build the Desktop App from source:
|
||||
### Quick Start
|
||||
|
||||
1. Clone the repo.
|
||||
2. Run `npm install` (requires pnpm or npm 7+ for workspaces).
|
||||
3. Run `npm run build --workspace @neuralnomads/codenomad-electron-app`.
|
||||
```bash
|
||||
git clone https://github.com/NeuralNomadsAI/CodeNomad.git
|
||||
cd CodeNomad
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
[](https://star-history.com/#NeuralNomadsAI/CodeNomad&Date)
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
<details>
|
||||
<summary><strong>macOS: "CodeNomad.app is damaged and can't be opened"</strong></summary>
|
||||
|
||||
Gatekeeper flag due to missing notarization. Clear the quarantine attribute:
|
||||
|
||||
```bash
|
||||
xattr -dr com.apple.quarantine /Applications/CodeNomad.app
|
||||
```
|
||||
|
||||
On Intel Macs, also check **System Settings → Privacy & Security** on first launch.
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><strong>Linux (Wayland + NVIDIA): Tauri App closes immediately</strong></summary>
|
||||
|
||||
WebKitGTK DMA-BUF/GBM issue. Run with:
|
||||
|
||||
```bash
|
||||
WEBKIT_DISABLE_DMABUF_RENDERER=1 codenomad
|
||||
```
|
||||
|
||||
See full workaround in the original README.
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## Community
|
||||
|
||||
[](https://star-history.com/#NeuralNomadsAI/CodeNomad&Date)
|
||||
|
||||
---
|
||||
|
||||
**Built with ♥ by [Neural Nomads](https://github.com/NeuralNomadsAI)** · [MIT License](LICENSE)
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 845 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 835 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 966 KiB After Width: | Height: | Size: 1.1 MiB |
81
package-lock.json
generated
81
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "codenomad-workspace",
|
||||
"version": "0.13.1",
|
||||
"version": "0.13.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "codenomad-workspace",
|
||||
"version": "0.13.1",
|
||||
"version": "0.13.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"7zip-bin": "^5.2.0",
|
||||
@@ -64,7 +64,6 @@
|
||||
"version": "7.28.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
@@ -3381,7 +3380,6 @@
|
||||
"version": "7.20.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.20.7",
|
||||
"@babel/types": "^7.20.7",
|
||||
@@ -3483,7 +3481,6 @@
|
||||
"version": "22.19.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
@@ -3558,7 +3555,6 @@
|
||||
"integrity": "sha512-MCbrb508JZHqe7bUibmZj/lyojdhLRnfkmyXnkrCM2zVrjTgL89U8UEfInpKTvPeTnxsw2hmyZxnhsdNR6yhwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cac": "^6.7.14",
|
||||
"colorette": "^2.0.20",
|
||||
@@ -3641,7 +3637,6 @@
|
||||
"version": "6.12.6",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -3844,6 +3839,7 @@
|
||||
"version": "5.3.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"archiver-utils": "^2.1.0",
|
||||
"async": "^3.2.4",
|
||||
@@ -3861,6 +3857,7 @@
|
||||
"version": "2.1.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"glob": "^7.1.4",
|
||||
"graceful-fs": "^4.2.0",
|
||||
@@ -3881,6 +3878,7 @@
|
||||
"version": "2.3.8",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
@@ -3894,12 +3892,14 @@
|
||||
"node_modules/archiver-utils/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/archiver-utils/node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
@@ -4213,6 +4213,7 @@
|
||||
"version": "4.1.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"buffer": "^5.5.0",
|
||||
"inherits": "^2.0.4",
|
||||
@@ -4276,7 +4277,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -4767,6 +4767,7 @@
|
||||
"version": "4.1.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"buffer-crc32": "^0.2.13",
|
||||
"crc32-stream": "^4.0.2",
|
||||
@@ -4896,6 +4897,7 @@
|
||||
"version": "1.2.2",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"crc32": "bin/crc32.njs"
|
||||
},
|
||||
@@ -4907,6 +4909,7 @@
|
||||
"version": "4.0.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"crc-32": "^1.2.0",
|
||||
"readable-stream": "^3.4.0"
|
||||
@@ -5272,7 +5275,6 @@
|
||||
"version": "24.13.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"app-builder-lib": "24.13.3",
|
||||
"builder-util": "24.13.1",
|
||||
@@ -5439,6 +5441,7 @@
|
||||
"version": "24.13.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"app-builder-lib": "24.13.3",
|
||||
"archiver": "^5.3.1",
|
||||
@@ -5450,6 +5453,7 @@
|
||||
"version": "10.1.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
"jsonfile": "^6.0.1",
|
||||
@@ -5463,6 +5467,7 @@
|
||||
"version": "6.2.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"universalify": "^2.0.0"
|
||||
},
|
||||
@@ -5474,6 +5479,7 @@
|
||||
"version": "2.0.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
@@ -6191,7 +6197,8 @@
|
||||
"node_modules/fs-constants": {
|
||||
"version": "1.0.0",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "8.1.0",
|
||||
@@ -7408,7 +7415,8 @@
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/isbinaryfile": {
|
||||
"version": "5.0.6",
|
||||
@@ -7458,7 +7466,6 @@
|
||||
"version": "1.21.7",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
@@ -7590,6 +7597,7 @@
|
||||
"version": "1.0.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"readable-stream": "^2.0.5"
|
||||
},
|
||||
@@ -7601,6 +7609,7 @@
|
||||
"version": "2.3.8",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
@@ -7614,12 +7623,14 @@
|
||||
"node_modules/lazystream/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lazystream/node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
@@ -7684,22 +7695,26 @@
|
||||
"node_modules/lodash.defaults": {
|
||||
"version": "4.2.0",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash.difference": {
|
||||
"version": "4.5.0",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash.flatten": {
|
||||
"version": "4.4.0",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lodash.sortby": {
|
||||
"version": "4.7.0",
|
||||
@@ -7711,7 +7726,8 @@
|
||||
"node_modules/lodash.union": {
|
||||
"version": "4.6.0",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/lowercase-keys": {
|
||||
"version": "2.0.0",
|
||||
@@ -8515,7 +8531,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -8663,7 +8678,8 @@
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/process-warning": {
|
||||
"version": "3.0.0",
|
||||
@@ -8912,6 +8928,7 @@
|
||||
"version": "3.6.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"string_decoder": "^1.1.1",
|
||||
@@ -8925,6 +8942,7 @@
|
||||
"version": "1.1.3",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"minimatch": "^5.1.0"
|
||||
}
|
||||
@@ -9227,7 +9245,6 @@
|
||||
"version": "4.52.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@@ -9451,7 +9468,6 @@
|
||||
"node_modules/seroval": {
|
||||
"version": "1.3.2",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
@@ -9775,7 +9791,6 @@
|
||||
"node_modules/solid-js": {
|
||||
"version": "1.9.10",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.1.0",
|
||||
"seroval": "~1.3.0",
|
||||
@@ -9916,6 +9931,7 @@
|
||||
"version": "1.3.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.2.0"
|
||||
}
|
||||
@@ -10249,6 +10265,7 @@
|
||||
"version": "2.2.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"bl": "^4.0.3",
|
||||
"end-of-stream": "^1.4.1",
|
||||
@@ -10441,7 +10458,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -10691,7 +10707,6 @@
|
||||
"version": "5.9.3",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -11039,7 +11054,6 @@
|
||||
"version": "5.4.21",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
@@ -11524,7 +11538,6 @@
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
@@ -11719,7 +11732,6 @@
|
||||
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
@@ -12008,6 +12020,7 @@
|
||||
"version": "4.1.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"archiver-utils": "^3.0.4",
|
||||
"compress-commons": "^4.1.2",
|
||||
@@ -12021,6 +12034,7 @@
|
||||
"version": "3.0.4",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"glob": "^7.2.3",
|
||||
"graceful-fs": "^4.2.0",
|
||||
@@ -12040,7 +12054,6 @@
|
||||
"node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
@@ -12055,7 +12068,7 @@
|
||||
},
|
||||
"packages/electron-app": {
|
||||
"name": "@neuralnomads/codenomad-electron-app",
|
||||
"version": "0.13.1",
|
||||
"version": "0.13.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codenomad/ui": "file:../ui",
|
||||
@@ -12092,7 +12105,7 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@neuralnomads/codenomad",
|
||||
"version": "0.13.1",
|
||||
"version": "0.13.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fastify/cors": "^8.5.0",
|
||||
@@ -12134,7 +12147,7 @@
|
||||
},
|
||||
"packages/tauri-app": {
|
||||
"name": "@codenomad/tauri-app",
|
||||
"version": "0.13.1",
|
||||
"version": "0.13.3",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.9.4"
|
||||
@@ -12142,7 +12155,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@codenomad/ui",
|
||||
"version": "0.13.1",
|
||||
"version": "0.13.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@git-diff-view/solid": "^0.0.8",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "codenomad-workspace",
|
||||
"version": "0.13.1",
|
||||
"version": "0.13.3",
|
||||
"private": true,
|
||||
"description": "CodeNomad monorepo workspace",
|
||||
"license": "MIT",
|
||||
@@ -22,7 +22,7 @@
|
||||
"build:mac-x64": "npm run build:mac-x64 --workspace @neuralnomads/codenomad-electron-app",
|
||||
"build:binaries": "npm run build:binaries --workspace @neuralnomads/codenomad-electron-app",
|
||||
"typecheck": "npm run typecheck --workspace @codenomad/ui && npm run typecheck --workspace @neuralnomads/codenomad-electron-app",
|
||||
"bumpVersion": "npm version --workspaces --include-workspace-root --no-git-tag-version && npm run sync:version --workspace @codenomad/tauri-app"
|
||||
"bumpVersion": "node ./scripts/bump-version.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"7zip-bin": "^5.2.0",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"minServerVersion": "0.13.1",
|
||||
"minServerVersion": "0.13.3",
|
||||
"latestServerUrl": "https://github.com/NeuralNomadsAI/CodeNomad/releases/latest"
|
||||
}
|
||||
|
||||
@@ -4,6 +4,23 @@ export interface Env {
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
const url = new URL(request.url)
|
||||
|
||||
if (url.pathname === "/version.json") {
|
||||
const response = await env.ASSETS.fetch(request)
|
||||
|
||||
const newHeaders = new Headers(response.headers)
|
||||
newHeaders.set("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate")
|
||||
newHeaders.set("Pragma", "no-cache")
|
||||
newHeaders.set("Expires", "0")
|
||||
|
||||
return new Response(response.body, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers: newHeaders,
|
||||
})
|
||||
}
|
||||
|
||||
return env.ASSETS.fetch(request)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@neuralnomads/codenomad-electron-app",
|
||||
"version": "0.13.1",
|
||||
"version": "0.13.3",
|
||||
"description": "CodeNomad - AI coding assistant",
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@opencode-ai/plugin": "1.3.2"
|
||||
"@opencode-ai/plugin": "1.3.7"
|
||||
}
|
||||
}
|
||||
4
packages/server/package-lock.json
generated
4
packages/server/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@neuralnomads/codenomad",
|
||||
"version": "0.13.1",
|
||||
"version": "0.13.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@neuralnomads/codenomad",
|
||||
"version": "0.13.1",
|
||||
"version": "0.13.3",
|
||||
"dependencies": {
|
||||
"@fastify/cors": "^8.5.0",
|
||||
"@fastify/reply-from": "^9.8.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@neuralnomads/codenomad",
|
||||
"version": "0.13.1",
|
||||
"version": "0.13.3",
|
||||
"description": "CodeNomad Server",
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
|
||||
2
packages/tauri-app/Cargo.lock
generated
2
packages/tauri-app/Cargo.lock
generated
@@ -458,7 +458,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "codenomad-tauri"
|
||||
version = "0.12.3"
|
||||
version = "0.13.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"dirs 5.0.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@codenomad/tauri-app",
|
||||
"version": "0.13.1",
|
||||
"version": "0.13.3",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "codenomad-tauri"
|
||||
version = "0.12.3"
|
||||
version = "0.13.3"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "CodeNomad",
|
||||
"version": "0.12.3",
|
||||
"version": "0.13.3",
|
||||
"identifier": "ai.neuralnomads.codenomad.client",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev:bootstrap",
|
||||
"beforeBuildCommand": "npm run bundle:server",
|
||||
"frontendDist": "resources/ui-loading"
|
||||
},
|
||||
|
||||
|
||||
|
||||
"app": {
|
||||
"withGlobalTauri": true,
|
||||
"windows": [
|
||||
@@ -33,9 +30,13 @@
|
||||
],
|
||||
"security": {
|
||||
"assetProtocol": {
|
||||
"scope": ["**"]
|
||||
"scope": [
|
||||
"**"
|
||||
]
|
||||
},
|
||||
"capabilities": ["main-window-native-dialogs"]
|
||||
"capabilities": [
|
||||
"main-window-native-dialogs"
|
||||
]
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
@@ -44,7 +45,17 @@
|
||||
"resources/server",
|
||||
"resources/ui-loading"
|
||||
],
|
||||
"icon": ["icon.icns", "icon.ico", "icon.png"],
|
||||
"targets": ["app", "appimage", "deb", "rpm", "nsis"]
|
||||
"icon": [
|
||||
"icon.icns",
|
||||
"icon.ico",
|
||||
"icon.png"
|
||||
],
|
||||
"targets": [
|
||||
"app",
|
||||
"appimage",
|
||||
"deb",
|
||||
"rpm",
|
||||
"nsis"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@codenomad/ui",
|
||||
"version": "0.13.1",
|
||||
"version": "0.13.3",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
@@ -443,7 +443,7 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
||||
rel="noreferrer"
|
||||
class="selector-button selector-button-secondary w-auto px-3 py-1.5 inline-flex items-center justify-center gap-1.5"
|
||||
aria-label={t("folderSelection.links.githubStars")}
|
||||
title={t("folderSelection.links.githubStars")}
|
||||
title={githubStars() !== null ? `${t("folderSelection.links.githubStars")}: ${githubStars()!.toLocaleString()}` : t("folderSelection.links.githubStars")}
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
void openExternalUrl(GITHUB_URL, "folder-selection")
|
||||
|
||||
@@ -36,12 +36,12 @@ import { serverApi } from "../../lib/api-client"
|
||||
import { loadBackgroundProcesses } from "../../stores/background-processes"
|
||||
import { BackgroundProcessOutputDialog } from "../background-process-output-dialog"
|
||||
import { useI18n } from "../../lib/i18n"
|
||||
import { getPermissionQueueLength, getQuestionQueueLength } from "../../stores/instances"
|
||||
import { getPermissionQueue, getPermissionQueueLength, getQuestionQueueLength, sendPermissionResponse } from "../../stores/instances"
|
||||
import SessionSidebar from "./shell/SessionSidebar"
|
||||
import { useSessionSidebarRequests } from "./shell/useSessionSidebarRequests"
|
||||
import RightPanel from "./shell/right-panel/RightPanel"
|
||||
import { useDrawerChrome } from "./shell/useDrawerChrome"
|
||||
import { getSessionStatus } from "../../stores/session-status"
|
||||
import { getRetrySeconds, getSessionRetry, getSessionStatus } from "../../stores/session-status"
|
||||
import { Maximize2, ShieldAlert } from "lucide-solid"
|
||||
|
||||
import type { LayoutMode } from "./shell/types"
|
||||
@@ -57,6 +57,13 @@ import { useDrawerHostMeasure } from "./shell/useDrawerHostMeasure"
|
||||
import { useDrawerResize } from "./shell/useDrawerResize"
|
||||
import { useSessionCache } from "./shell/useSessionCache"
|
||||
import { useInstanceSessionContext } from "./shell/useInstanceSessionContext"
|
||||
import { getPermissionSessionId } from "../../types/permission"
|
||||
import {
|
||||
canAutoRespondPermission,
|
||||
finishAutoRespondPermission,
|
||||
getPermissionAutoAcceptInFlightVersion,
|
||||
isPermissionAutoAcceptEnabled,
|
||||
} from "../../stores/permission-auto-accept"
|
||||
|
||||
const log = getLogger("session")
|
||||
|
||||
@@ -97,6 +104,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
const [selectedBackgroundProcess, setSelectedBackgroundProcess] = createSignal<BackgroundProcess | null>(null)
|
||||
const [showBackgroundOutput, setShowBackgroundOutput] = createSignal(false)
|
||||
const [permissionModalOpen, setPermissionModalOpen] = createSignal(false)
|
||||
const [now, setNow] = createSignal(Date.now())
|
||||
|
||||
// Worktree selector manages its own dialogs.
|
||||
const [showSessionSearch, setShowSessionSearch] = createSignal(false)
|
||||
@@ -230,6 +238,12 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
window.localStorage.setItem(RIGHT_DRAWER_STORAGE_KEY, rightDrawerWidth().toString())
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (typeof window === "undefined") return
|
||||
const timer = window.setInterval(() => setNow(Date.now()), 1000)
|
||||
onCleanup(() => window.clearInterval(timer))
|
||||
})
|
||||
|
||||
const connectionStatus = () => sseManager.getStatus(props.instance.id)
|
||||
const connectionStatusClass = () => {
|
||||
const status = connectionStatus()
|
||||
@@ -252,6 +266,33 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
return permissions + questions > 0
|
||||
})
|
||||
|
||||
const permissionQueue = createMemo(() => getPermissionQueue(props.instance.id))
|
||||
|
||||
createEffect(() => {
|
||||
getPermissionAutoAcceptInFlightVersion()
|
||||
|
||||
for (const permission of permissionQueue()) {
|
||||
const sessionId = getPermissionSessionId(permission)
|
||||
if (!sessionId) continue
|
||||
if (!permission?.id) continue
|
||||
if (!canAutoRespondPermission(props.instance.id, sessionId, permission.id)) continue
|
||||
|
||||
void sendPermissionResponse(props.instance.id, sessionId, permission.id, "once")
|
||||
.catch((error) => {
|
||||
log.error("Failed to auto-accept permission", error)
|
||||
})
|
||||
.finally(() => {
|
||||
finishAutoRespondPermission(props.instance.id, sessionId, permission.id)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const yoloModeEnabled = createMemo(() => {
|
||||
const session = activeSessionForInstance()
|
||||
if (!session) return false
|
||||
return isPermissionAutoAcceptEnabled(props.instance.id, session.id)
|
||||
})
|
||||
|
||||
const activeSessionStatusPill = createMemo(() => {
|
||||
const activeSessionId = activeSessionIdForInstance()
|
||||
if (!activeSessionId || activeSessionId === "info") return null
|
||||
@@ -272,17 +313,28 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
}
|
||||
|
||||
const status = getSessionStatus(props.instance.id, activeSessionId)
|
||||
const text =
|
||||
status === "working"
|
||||
const retry = getSessionRetry(props.instance.id, activeSessionId)
|
||||
const text = retry
|
||||
? (() => {
|
||||
const seconds = getRetrySeconds(retry.next, now())
|
||||
return seconds > 0 ? t("sessionList.status.retryingIn", { seconds: String(seconds) }) : t("sessionList.status.retrying")
|
||||
})()
|
||||
: status === "working"
|
||||
? t("sessionList.status.working")
|
||||
: status === "compacting"
|
||||
? t("sessionList.status.compacting")
|
||||
: t("sessionList.status.idle")
|
||||
|
||||
return {
|
||||
className: `session-${status}`,
|
||||
className: `session-${retry ? "retrying" : status}`,
|
||||
text,
|
||||
showAlertIcon: false,
|
||||
title: retry
|
||||
? t("sessionList.status.retryTooltip", {
|
||||
message: retry.message,
|
||||
attempt: String(retry.attempt),
|
||||
})
|
||||
: undefined,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -290,13 +342,39 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
const pill = activeSessionStatusPill()
|
||||
if (!pill) return null
|
||||
return (
|
||||
<span class={`status-indicator session-status session-status-list ${pill.className}`}>
|
||||
<span class={`status-indicator session-status session-status-list ${pill.className}`} title={pill.title}>
|
||||
{pill.showAlertIcon ? <ShieldAlert class="w-3.5 h-3.5" aria-hidden="true" /> : <span class="status-dot" />}
|
||||
{pill.text}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const renderYoloModePill = () => {
|
||||
if (!yoloModeEnabled()) return null
|
||||
return (
|
||||
<span
|
||||
class="status-indicator session-status session-status-list session-yolo-mode"
|
||||
aria-label={t("instanceShell.yoloMode.badgeAriaLabel")}
|
||||
title={t("instanceShell.yoloMode.badgeAriaLabel")}
|
||||
>
|
||||
<span class="status-dot" />
|
||||
{t("instanceShell.yoloMode.badge")}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const renderSessionHeaderIndicators = () => (
|
||||
<div class="flex items-center flex-wrap justify-center gap-2">
|
||||
{renderYoloModePill()}
|
||||
<Show when={hasPendingRequests()} fallback={renderActiveSessionStatusPill()}>
|
||||
<PermissionNotificationBanner
|
||||
instanceId={props.instance.id}
|
||||
onClick={() => setPermissionModalOpen(true)}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
|
||||
const handleCommandPaletteClick = () => {
|
||||
showCommandPalette(props.instance.id)
|
||||
}
|
||||
@@ -622,12 +700,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
</Show>
|
||||
|
||||
<div class="flex-1 flex items-center justify-center min-w-0">
|
||||
<Show when={hasPendingRequests()} fallback={renderActiveSessionStatusPill()}>
|
||||
<PermissionNotificationBanner
|
||||
instanceId={props.instance.id}
|
||||
onClick={() => setPermissionModalOpen(true)}
|
||||
/>
|
||||
</Show>
|
||||
{renderSessionHeaderIndicators()}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-center gap-1">
|
||||
@@ -719,12 +792,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
</Show>
|
||||
|
||||
<div class="ml-auto flex items-center session-header-hints">
|
||||
<Show when={hasPendingRequests()} fallback={renderActiveSessionStatusPill()}>
|
||||
<PermissionNotificationBanner
|
||||
instanceId={props.instance.id}
|
||||
onClick={() => setPermissionModalOpen(true)}
|
||||
/>
|
||||
</Show>
|
||||
{renderSessionHeaderIndicators()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -48,104 +48,103 @@ interface SessionSidebarProps {
|
||||
}
|
||||
|
||||
const SessionSidebar: Component<SessionSidebarProps> = (props) => (
|
||||
<div class="flex flex-col h-full min-h-0" ref={props.setContentEl}>
|
||||
<div class="flex flex-col gap-2 px-4 py-3 border-b border-base">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="session-sidebar-title text-sm font-semibold uppercase text-primary">
|
||||
{props.t("instanceShell.leftPanel.sessionsTitle")}
|
||||
</span>
|
||||
<div class="flex items-center gap-2 text-primary">
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
aria-label={props.t("sessionList.actions.newSession.ariaLabel")}
|
||||
title={props.t("sessionList.actions.newSession.title")}
|
||||
onClick={() => {
|
||||
const result = props.onNewSession()
|
||||
if (result instanceof Promise) {
|
||||
void result.catch((error) => log.error("Failed to create session:", error))
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PlusSquare class="w-5 h-5" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
aria-label={props.t("sessionList.filter.ariaLabel")}
|
||||
title={props.t("sessionList.filter.ariaLabel")}
|
||||
aria-pressed={props.showSearch()}
|
||||
onClick={props.onToggleSearch}
|
||||
sx={{
|
||||
color: props.showSearch() ? "var(--text-primary)" : "inherit",
|
||||
backgroundColor: props.showSearch() ? "var(--surface-hover)" : "transparent",
|
||||
"&:hover": {
|
||||
backgroundColor: "var(--surface-hover)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Search class="w-5 h-5" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
aria-label={props.t("instanceShell.leftPanel.instanceInfo")}
|
||||
title={props.t("instanceShell.leftPanel.instanceInfo")}
|
||||
onClick={() => props.onSelectSession("info")}
|
||||
>
|
||||
<InfoOutlinedIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<Show when={!props.isPhoneLayout()}>
|
||||
<div class="flex flex-col h-full min-h-0" ref={props.setContentEl}>
|
||||
<div class="flex flex-col gap-2 px-4 py-3 border-b border-base">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="session-sidebar-title text-sm font-semibold uppercase text-primary">
|
||||
{props.t("instanceShell.leftPanel.sessionsTitle")}
|
||||
</span>
|
||||
<div class="flex items-center gap-2 text-primary">
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
aria-label={props.leftPinned() ? props.t("instanceShell.leftDrawer.unpin") : props.t("instanceShell.leftDrawer.pin")}
|
||||
onClick={() => (props.leftPinned() ? props.onUnpinLeftDrawer() : props.onPinLeftDrawer())}
|
||||
aria-label={props.t("sessionList.actions.newSession.ariaLabel")}
|
||||
title={props.t("sessionList.actions.newSession.title")}
|
||||
onClick={() => {
|
||||
const result = props.onNewSession()
|
||||
if (result instanceof Promise) {
|
||||
void result.catch((error) => log.error("Failed to create session:", error))
|
||||
}
|
||||
}}
|
||||
>
|
||||
{props.leftPinned() ? <PushPinIcon fontSize="small" /> : <PushPinOutlinedIcon fontSize="small" />}
|
||||
<PlusSquare class="w-5 h-5" />
|
||||
</IconButton>
|
||||
</Show>
|
||||
<Show when={props.drawerState() === "floating-open"}>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
aria-label={props.t("instanceShell.leftDrawer.toggle.close")}
|
||||
title={props.t("instanceShell.leftDrawer.toggle.close")}
|
||||
onClick={props.onCloseLeftDrawer}
|
||||
aria-label={props.t("sessionList.filter.ariaLabel")}
|
||||
title={props.t("sessionList.filter.ariaLabel")}
|
||||
aria-pressed={props.showSearch()}
|
||||
onClick={props.onToggleSearch}
|
||||
sx={{
|
||||
color: props.showSearch() ? "var(--text-primary)" : "inherit",
|
||||
backgroundColor: props.showSearch() ? "var(--surface-hover)" : "transparent",
|
||||
"&:hover": {
|
||||
backgroundColor: "var(--surface-hover)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MenuOpenIcon fontSize="small" />
|
||||
<Search class="w-5 h-5" />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
aria-label={props.t("instanceShell.leftPanel.instanceInfo")}
|
||||
title={props.t("instanceShell.leftPanel.instanceInfo")}
|
||||
onClick={() => props.onSelectSession("info")}
|
||||
>
|
||||
<InfoOutlinedIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<Show when={!props.isPhoneLayout()}>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
aria-label={props.leftPinned() ? props.t("instanceShell.leftDrawer.unpin") : props.t("instanceShell.leftDrawer.pin")}
|
||||
onClick={() => (props.leftPinned() ? props.onUnpinLeftDrawer() : props.onPinLeftDrawer())}
|
||||
>
|
||||
{props.leftPinned() ? <PushPinIcon fontSize="small" /> : <PushPinOutlinedIcon fontSize="small" />}
|
||||
</IconButton>
|
||||
</Show>
|
||||
<Show when={props.drawerState() === "floating-open"}>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
aria-label={props.t("instanceShell.leftDrawer.toggle.close")}
|
||||
title={props.t("instanceShell.leftDrawer.toggle.close")}
|
||||
onClick={props.onCloseLeftDrawer}
|
||||
>
|
||||
<MenuOpenIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<div class="session-sidebar-shortcuts">
|
||||
<Show when={props.keyboardShortcuts().length}>
|
||||
<KeyboardHint shortcuts={props.keyboardShortcuts()} separator=" " showDescription={false} />
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<div class="session-sidebar-shortcuts">
|
||||
<Show when={props.keyboardShortcuts().length}>
|
||||
<KeyboardHint shortcuts={props.keyboardShortcuts()} separator=" " showDescription={false} />
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="session-sidebar flex flex-col flex-1 min-h-0">
|
||||
<SessionList
|
||||
instanceId={props.instanceId}
|
||||
threads={props.threads()}
|
||||
activeSessionId={props.activeSessionId()}
|
||||
onSelect={props.onSelectSession}
|
||||
onNew={() => {
|
||||
const result = props.onNewSession()
|
||||
if (result instanceof Promise) {
|
||||
void result.catch((error) => log.error("Failed to create session:", error))
|
||||
}
|
||||
}}
|
||||
enableFilterBar={props.showSearch()}
|
||||
showHeader={false}
|
||||
showFooter={false}
|
||||
/>
|
||||
<div class="session-sidebar flex flex-col flex-1 min-h-0">
|
||||
<SessionList
|
||||
instanceId={props.instanceId}
|
||||
threads={props.threads()}
|
||||
activeSessionId={props.activeSessionId()}
|
||||
onSelect={props.onSelectSession}
|
||||
onNew={() => {
|
||||
const result = props.onNewSession()
|
||||
if (result instanceof Promise) {
|
||||
void result.catch((error) => log.error("Failed to create session:", error))
|
||||
}
|
||||
}}
|
||||
enableFilterBar={props.showSearch()}
|
||||
showHeader={false}
|
||||
showFooter={false}
|
||||
/>
|
||||
|
||||
<div class="session-sidebar-separator" />
|
||||
<Show when={props.activeSession()}>
|
||||
{(activeSession) => (
|
||||
<>
|
||||
<div class="session-sidebar-separator" />
|
||||
<Show when={props.activeSession()}>
|
||||
{(activeSession) => (
|
||||
<div class="session-sidebar-controls px-4 py-4 border-t border-base flex flex-col gap-3">
|
||||
<WorktreeSelector instanceId={props.instanceId} sessionId={activeSession().id} />
|
||||
|
||||
@@ -177,11 +176,10 @@ const SessionSidebar: Component<SessionSidebarProps> = (props) => (
|
||||
showDescription={false}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
|
||||
export default SessionSidebar
|
||||
|
||||
@@ -89,6 +89,7 @@ interface RightPanelProps {
|
||||
const RightPanel: Component<RightPanelProps> = (props) => {
|
||||
const [rightPanelTab, setRightPanelTab] = createSignal<RightPanelTab>(readStoredRightPanelTab("changes"))
|
||||
const [rightPanelExpandedItems, setRightPanelExpandedItems] = createSignal<string[]>([
|
||||
"yolo-mode",
|
||||
"plan",
|
||||
"background-processes",
|
||||
"mcp",
|
||||
@@ -787,7 +788,7 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
||||
setRightPanelTab("changes")
|
||||
}
|
||||
|
||||
const statusSectionIds = ["session-changes", "plan", "background-processes", "mcp", "lsp", "plugins"]
|
||||
const statusSectionIds = ["yolo-mode", "session-changes", "plan", "background-processes", "mcp", "lsp", "plugins"]
|
||||
|
||||
createEffect(() => {
|
||||
const currentExpanded = new Set(rightPanelExpandedItems())
|
||||
|
||||
@@ -2,6 +2,7 @@ import { For, Show, type Accessor, type Component } from "solid-js"
|
||||
import type { ToolState } from "@opencode-ai/sdk/v2"
|
||||
import { Accordion } from "@kobalte/core"
|
||||
import { Tooltip } from "@kobalte/core/tooltip"
|
||||
import Switch from "@suid/material/Switch"
|
||||
|
||||
import { ChevronDown, Info, TerminalSquare, Trash2, XOctagon } from "lucide-solid"
|
||||
|
||||
@@ -12,6 +13,7 @@ import type { Session } from "../../../../../types/session"
|
||||
import ContextUsagePanel from "../../../../session/context-usage-panel"
|
||||
import { TodoListView } from "../../../../tool-call/renderers/todo"
|
||||
import InstanceServiceStatus from "../../../../instance-service-status"
|
||||
import { isPermissionAutoAcceptEnabled, togglePermissionAutoAccept } from "../../../../../stores/permission-auto-accept"
|
||||
|
||||
interface StatusTabProps {
|
||||
t: (key: string, vars?: Record<string, any>) => string
|
||||
@@ -39,6 +41,35 @@ interface StatusTabProps {
|
||||
const StatusTab: Component<StatusTabProps> = (props) => {
|
||||
const isSectionExpanded = (id: string) => props.expandedItems().includes(id)
|
||||
|
||||
const renderYoloModeSection = () => {
|
||||
const session = props.activeSession()
|
||||
if (!session) {
|
||||
return (
|
||||
<div class="right-panel-empty right-panel-empty--left">
|
||||
<span class="text-xs">{props.t("instanceShell.yoloMode.noSessionSelected")}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="rounded-md border border-base bg-surface-secondary px-3 py-2">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<div class="text-sm font-medium text-primary">{props.t("instanceShell.yoloMode.title")}</div>
|
||||
<p class="mt-1 text-xs text-secondary">{props.t("instanceShell.yoloMode.description")}</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={isPermissionAutoAcceptEnabled(props.instanceId, session.id)}
|
||||
color="warning"
|
||||
size="small"
|
||||
inputProps={{ "aria-label": props.t("instanceShell.yoloMode.title") }}
|
||||
onChange={() => togglePermissionAutoAccept(props.instanceId, session.id)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const renderStatusSessionChanges = () => {
|
||||
const sessionId = props.activeSessionId()
|
||||
if (!sessionId || sessionId === "info") {
|
||||
@@ -204,6 +235,12 @@ const StatusTab: Component<StatusTabProps> = (props) => {
|
||||
}
|
||||
|
||||
const statusSections = [
|
||||
{
|
||||
id: "yolo-mode",
|
||||
labelKey: "instanceShell.rightPanel.sections.yoloMode",
|
||||
tooltipKey: "instanceShell.rightPanel.sections.yoloMode.tooltip",
|
||||
render: renderYoloModeSection,
|
||||
},
|
||||
{
|
||||
id: "session-changes",
|
||||
labelKey: "instanceShell.rightPanel.sections.sessionChanges",
|
||||
@@ -281,29 +318,23 @@ const StatusTab: Component<StatusTabProps> = (props) => {
|
||||
<For each={statusSections}>
|
||||
{(section) => (
|
||||
<Accordion.Item value={section.id} class="right-panel-accordion-item">
|
||||
<Accordion.Header>
|
||||
<Accordion.Header class="right-panel-accordion-header-row">
|
||||
<Accordion.Trigger class="right-panel-accordion-trigger">
|
||||
<span class="section-left">
|
||||
<Tooltip openDelay={200} gutter={4} placement="top">
|
||||
<Tooltip.Trigger
|
||||
class="section-info-trigger"
|
||||
aria-label={props.t(section.tooltipKey)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Info class="section-info-icon" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content class="section-info-tooltip">
|
||||
{props.t(section.tooltipKey)}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip>
|
||||
<span class="section-label">{props.t(section.labelKey)}</span>
|
||||
</span>
|
||||
<ChevronDown
|
||||
class={`right-panel-accordion-chevron ${isSectionExpanded(section.id) ? "right-panel-accordion-chevron-expanded" : ""}`}
|
||||
/>
|
||||
</Accordion.Trigger>
|
||||
<Tooltip openDelay={200} gutter={4} placement="top">
|
||||
<Tooltip.Trigger as="button" type="button" class="section-info-trigger" aria-label={props.t(section.tooltipKey)}>
|
||||
<Info class="section-info-icon" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content class="section-info-tooltip">{props.t(section.tooltipKey)}</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip>
|
||||
</Accordion.Header>
|
||||
<Accordion.Content class="right-panel-accordion-content">{section.render()}</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
|
||||
@@ -123,7 +123,11 @@ export function Markdown(props: MarkdownProps) {
|
||||
version: () => resolved().version,
|
||||
})
|
||||
|
||||
const commitCacheEntry = (snapshot: ReturnType<typeof resolved>, renderedHtml: string) => {
|
||||
const commitCacheEntry = (
|
||||
snapshot: ReturnType<typeof resolved>,
|
||||
renderedHtml: string,
|
||||
options?: { cache?: boolean },
|
||||
) => {
|
||||
const cacheEntry: RenderCache = {
|
||||
text: snapshot.text,
|
||||
html: renderedHtml,
|
||||
@@ -131,7 +135,9 @@ export function Markdown(props: MarkdownProps) {
|
||||
mode: `${snapshot.version}:${snapshot.escapeRawHtml ? "escaped" : "raw"}`,
|
||||
}
|
||||
setHtml(renderedHtml)
|
||||
cacheHandle.set(cacheEntry)
|
||||
if (options?.cache ?? true) {
|
||||
cacheHandle.set(cacheEntry)
|
||||
}
|
||||
notifyRendered()
|
||||
}
|
||||
|
||||
@@ -142,9 +148,10 @@ export function Markdown(props: MarkdownProps) {
|
||||
suppressHighlight: !snapshot.highlightEnabled,
|
||||
escapeRawHtml: snapshot.escapeRawHtml,
|
||||
})
|
||||
const shouldCache = !snapshot.highlightEnabled || !markdown.hasPendingCodeHighlight(snapshot.text)
|
||||
|
||||
if (latestRequestKey === snapshot.requestKey) {
|
||||
commitCacheEntry(snapshot, rendered)
|
||||
commitCacheEntry(snapshot, rendered, { cache: shouldCache })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,12 @@ import { usePromptAttachments } from "./prompt-input/usePromptAttachments"
|
||||
import { usePromptPicker } from "./prompt-input/usePromptPicker"
|
||||
import { usePromptKeyDown } from "./prompt-input/usePromptKeyDown"
|
||||
import { usePromptVoiceInput } from "./prompt-input/usePromptVoiceInput"
|
||||
import { canUseConversationMode, isConversationModeEnabled, toggleConversationMode } from "../stores/conversation-speech"
|
||||
import {
|
||||
canUseConversationMode,
|
||||
clearConversationPlaybackForInstance,
|
||||
isConversationModeEnabled,
|
||||
toggleConversationMode,
|
||||
} from "../stores/conversation-speech"
|
||||
const log = getLogger("actions")
|
||||
const LazyUnifiedPicker = lazy(() => import("./unified-picker"))
|
||||
|
||||
@@ -492,6 +497,8 @@ export default function PromptInput(props: PromptInputProps) {
|
||||
const beginVoicePress = (event?: PointerEvent | KeyboardEvent) => {
|
||||
if (voiceButtonPressed || props.disabled || voiceInput.isTranscribing() || !voiceInput.canUseVoiceInput()) return
|
||||
voiceButtonPressed = true
|
||||
// Treat a mic press as barge-in: stop any active assistant speech before listening.
|
||||
clearConversationPlaybackForInstance(props.instanceId)
|
||||
|
||||
if (event instanceof PointerEvent) {
|
||||
const target = event.currentTarget
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, For, Show, createSignal, createMemo, createEffect, JSX, onCleanup } from "solid-js"
|
||||
import type { SessionStatus } from "../types/session"
|
||||
import type { SessionThread } from "../stores/session-state"
|
||||
import { getSessionStatus } from "../stores/session-status"
|
||||
import { getRetrySeconds, getSessionRetry, getSessionStatus } from "../stores/session-status"
|
||||
import { Bot, User, Copy, Trash2, Pencil, ShieldAlert, ChevronDown, Search, Square, CheckSquare, MinusSquare, Split, RotateCw } from "lucide-solid"
|
||||
import KeyboardHint from "./keyboard-hint"
|
||||
import SessionRenameDialog from "./session-rename-dialog"
|
||||
@@ -55,6 +55,13 @@ const SessionList: Component<SessionListProps> = (props) => {
|
||||
|
||||
const [selectedSessionIds, setSelectedSessionIds] = createSignal<Set<string>>(new Set())
|
||||
const [reloadingSessionIds, setReloadingSessionIds] = createSignal<Set<string>>(new Set())
|
||||
const [now, setNow] = createSignal(Date.now())
|
||||
|
||||
createEffect(() => {
|
||||
if (typeof window === "undefined") return
|
||||
const timer = window.setInterval(() => setNow(Date.now()), 1000)
|
||||
onCleanup(() => window.clearInterval(timer))
|
||||
})
|
||||
|
||||
const normalizeSessionLabel = (sessionId: string) => {
|
||||
const session = sessionStateSessions().get(props.instanceId)?.get(sessionId)
|
||||
@@ -400,7 +407,13 @@ const SessionList: Component<SessionListProps> = (props) => {
|
||||
const isActive = () => props.activeSessionId === rowProps.sessionId
|
||||
const title = () => session()?.title || t("sessionList.session.untitled")
|
||||
const status = () => getSessionStatus(props.instanceId, rowProps.sessionId)
|
||||
const retry = () => getSessionRetry(props.instanceId, rowProps.sessionId)
|
||||
const statusLabel = () => {
|
||||
const retryState = retry()
|
||||
if (retryState) {
|
||||
const seconds = getRetrySeconds(retryState.next, now())
|
||||
return seconds > 0 ? t("sessionList.status.retryingIn", { seconds: String(seconds) }) : t("sessionList.status.retrying")
|
||||
}
|
||||
switch (formatSessionStatus(status())) {
|
||||
case "working":
|
||||
return t("sessionList.status.working")
|
||||
@@ -413,13 +426,21 @@ const SessionList: Component<SessionListProps> = (props) => {
|
||||
const needsPermission = () => Boolean(session()?.pendingPermission)
|
||||
const needsQuestion = () => Boolean((session() as any)?.pendingQuestion)
|
||||
const needsInput = () => needsPermission() || needsQuestion()
|
||||
const statusClassName = () => (needsInput() ? "session-permission" : `session-${status()}`)
|
||||
const statusClassName = () => (needsInput() ? "session-permission" : `session-${retry() ? "retrying" : status()}`)
|
||||
const statusText = () =>
|
||||
needsPermission()
|
||||
? t("sessionList.status.needsPermission")
|
||||
: needsQuestion()
|
||||
? t("sessionList.status.needsInput")
|
||||
: statusLabel()
|
||||
const statusTooltip = () => {
|
||||
const retryState = retry()
|
||||
if (!retryState) return undefined
|
||||
return t("sessionList.status.retryTooltip", {
|
||||
message: retryState.message,
|
||||
attempt: String(retryState.attempt),
|
||||
})
|
||||
}
|
||||
|
||||
const isSelected = () => selectedSessionIds().has(rowProps.sessionId)
|
||||
|
||||
@@ -499,7 +520,7 @@ const SessionList: Component<SessionListProps> = (props) => {
|
||||
<ChevronDown class={`w-3.5 h-3.5 transition-transform ${rowProps.expanded ? "" : "-rotate-90"}`} />
|
||||
</span>
|
||||
</Show>
|
||||
<span class={`status-indicator session-status session-status-list ${statusClassName()}`}>
|
||||
<span class={`status-indicator session-status session-status-list ${statusClassName()}`} title={statusTooltip()}>
|
||||
{needsInput() ? <ShieldAlert class="w-3.5 h-3.5" aria-hidden="true" /> : <span class="status-dot" />}
|
||||
{statusText()}
|
||||
</span>
|
||||
|
||||
@@ -19,9 +19,6 @@ export function formatCompactCount(value: number): string {
|
||||
return `${(value / 1_000_000).toFixed(1)}M`
|
||||
}
|
||||
if (value >= 10_000) {
|
||||
return `${Math.round(value / 1_000)}K`
|
||||
}
|
||||
if (value >= 1_000) {
|
||||
const label = `${(value / 1_000).toFixed(1)}K`
|
||||
return label.replace(/\.0K$/, "K")
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ export const instanceMessages = {
|
||||
|
||||
"instanceShell.leftPanel.sessionsTitle": "Sessions",
|
||||
"instanceShell.leftPanel.instanceInfo": "Instance Info",
|
||||
|
||||
"instanceShell.leftDrawer.pin": "Pin left drawer",
|
||||
"instanceShell.leftDrawer.unpin": "Unpin left drawer",
|
||||
"instanceShell.leftDrawer.toggle.pinned": "Left drawer pinned",
|
||||
@@ -107,6 +106,8 @@ export const instanceMessages = {
|
||||
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "Cancel",
|
||||
"instanceShell.rightPanel.toast.saveSuccess": "File saved successfully",
|
||||
"instanceShell.rightPanel.toast.saveError": "Failed to save file",
|
||||
"instanceShell.rightPanel.sections.yoloMode": "Yolo Mode",
|
||||
"instanceShell.rightPanel.sections.yoloMode.tooltip": "Automatically approves permission requests for the current session. Use it only when you trust the tools being run.",
|
||||
"instanceShell.rightPanel.sections.sessionChanges": "Session Changes",
|
||||
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Files modified in the current session. Shows additions and deletions for each file.",
|
||||
"instanceShell.rightPanel.sections.plan": "Plan",
|
||||
@@ -150,6 +151,12 @@ export const instanceMessages = {
|
||||
"instanceShell.plan.noSessionSelected": "Select a session to view plan.",
|
||||
"instanceShell.plan.empty": "Nothing planned yet.",
|
||||
|
||||
"instanceShell.yoloMode.noSessionSelected": "Select a session to configure Yolo mode.",
|
||||
"instanceShell.yoloMode.title": "Yolo mode",
|
||||
"instanceShell.yoloMode.description": "Automatically approve permission requests for this session. Disabled by default.",
|
||||
"instanceShell.yoloMode.badge": "Yolo mode",
|
||||
"instanceShell.yoloMode.badgeAriaLabel": "Yolo mode enabled",
|
||||
|
||||
"instanceShell.backgroundProcesses.empty": "No background processes.",
|
||||
"instanceShell.backgroundProcesses.status": "Status: {status}",
|
||||
"instanceShell.backgroundProcesses.output": "Output: {sizeKb}KB",
|
||||
|
||||
@@ -15,6 +15,10 @@ export const sessionMessages = {
|
||||
"sessionList.status.working": "Working",
|
||||
"sessionList.status.compacting": "Compacting",
|
||||
"sessionList.status.idle": "Idle",
|
||||
"sessionList.status.retrying": "Retrying",
|
||||
"sessionList.status.retryingIn": "Retrying in {seconds}s",
|
||||
"sessionList.status.retryTooltip": "{message} (Attempt {attempt})",
|
||||
"sessionList.status.retryToast": "{countdown}: {message} (Attempt {attempt})",
|
||||
"sessionList.status.needsPermission": "Needs Permission",
|
||||
"sessionList.status.needsInput": "Needs Input",
|
||||
"sessionList.expand.collapseAriaLabel": "Collapse session",
|
||||
|
||||
@@ -26,7 +26,6 @@ export const instanceMessages = {
|
||||
|
||||
"instanceShell.leftPanel.sessionsTitle": "Sesiones",
|
||||
"instanceShell.leftPanel.instanceInfo": "Info de la instancia",
|
||||
|
||||
"instanceShell.leftDrawer.pin": "Fijar panel izquierdo",
|
||||
"instanceShell.leftDrawer.unpin": "Desfijar panel izquierdo",
|
||||
"instanceShell.leftDrawer.toggle.pinned": "Panel izquierdo fijado",
|
||||
@@ -107,6 +106,8 @@ export const instanceMessages = {
|
||||
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "Cancelar",
|
||||
"instanceShell.rightPanel.toast.saveSuccess": "Archivo guardado exitosamente",
|
||||
"instanceShell.rightPanel.toast.saveError": "Error al guardar el archivo",
|
||||
"instanceShell.rightPanel.sections.yoloMode": "Modo yolo",
|
||||
"instanceShell.rightPanel.sections.yoloMode.tooltip": "Aprueba automaticamente las solicitudes de permiso de la sesion actual. Usalo solo si confias en las herramientas que se estan ejecutando.",
|
||||
"instanceShell.rightPanel.sections.sessionChanges": "Cambios de sesión",
|
||||
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Archivos modificados en la sesión actual. Muestra las adiciones y eliminaciones de cada archivo.",
|
||||
"instanceShell.rightPanel.sections.plan": "Plan",
|
||||
@@ -140,6 +141,12 @@ export const instanceMessages = {
|
||||
"instanceShell.plan.noSessionSelected": "Selecciona una sesión para ver el plan.",
|
||||
"instanceShell.plan.empty": "Aún no hay nada planificado.",
|
||||
|
||||
"instanceShell.yoloMode.noSessionSelected": "Selecciona una sesion para configurar el modo yolo.",
|
||||
"instanceShell.yoloMode.title": "Modo yolo",
|
||||
"instanceShell.yoloMode.description": "Aprueba automaticamente las solicitudes de permiso de esta sesion. Esta desactivado por defecto.",
|
||||
"instanceShell.yoloMode.badge": "Modo yolo",
|
||||
"instanceShell.yoloMode.badgeAriaLabel": "Modo yolo activado",
|
||||
|
||||
"instanceShell.backgroundProcesses.empty": "No hay procesos en segundo plano.",
|
||||
"instanceShell.backgroundProcesses.status": "Estado: {status}",
|
||||
"instanceShell.backgroundProcesses.output": "Salida: {sizeKb} KB",
|
||||
|
||||
@@ -15,6 +15,10 @@ export const sessionMessages = {
|
||||
"sessionList.status.working": "Trabajando",
|
||||
"sessionList.status.compacting": "Compactando",
|
||||
"sessionList.status.idle": "Inactiva",
|
||||
"sessionList.status.retrying": "Reintentando",
|
||||
"sessionList.status.retryingIn": "Reintentando en {seconds}s",
|
||||
"sessionList.status.retryTooltip": "{message} (Intento {attempt})",
|
||||
"sessionList.status.retryToast": "{countdown}: {message} (Intento {attempt})",
|
||||
"sessionList.status.needsPermission": "Requiere permiso",
|
||||
"sessionList.status.needsInput": "Requiere entrada",
|
||||
"sessionList.expand.collapseAriaLabel": "Colapsar sesión",
|
||||
|
||||
@@ -26,7 +26,6 @@ export const instanceMessages = {
|
||||
|
||||
"instanceShell.leftPanel.sessionsTitle": "Sessions",
|
||||
"instanceShell.leftPanel.instanceInfo": "Infos de l'instance",
|
||||
|
||||
"instanceShell.leftDrawer.pin": "Épingler le tiroir gauche",
|
||||
"instanceShell.leftDrawer.unpin": "Désépingler le tiroir gauche",
|
||||
"instanceShell.leftDrawer.toggle.pinned": "Tiroir gauche épinglé",
|
||||
@@ -107,6 +106,8 @@ export const instanceMessages = {
|
||||
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "Annuler",
|
||||
"instanceShell.rightPanel.toast.saveSuccess": "Fichier enregistré avec succès",
|
||||
"instanceShell.rightPanel.toast.saveError": "Échec de l'enregistrement du fichier",
|
||||
"instanceShell.rightPanel.sections.yoloMode": "Mode yolo",
|
||||
"instanceShell.rightPanel.sections.yoloMode.tooltip": "Approuve automatiquement les demandes d'autorisation pour la session actuelle. A utiliser seulement si vous faites confiance aux outils executes.",
|
||||
"instanceShell.rightPanel.sections.sessionChanges": "Changements de session",
|
||||
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Fichiers modifiés dans la session actuelle. Affiche les ajouts et suppressions pour chaque fichier.",
|
||||
"instanceShell.rightPanel.sections.plan": "Plan",
|
||||
@@ -140,6 +141,12 @@ export const instanceMessages = {
|
||||
"instanceShell.plan.noSessionSelected": "Sélectionnez une session pour voir le plan.",
|
||||
"instanceShell.plan.empty": "Aucun plan pour l'instant.",
|
||||
|
||||
"instanceShell.yoloMode.noSessionSelected": "Selectionnez une session pour configurer le mode yolo.",
|
||||
"instanceShell.yoloMode.title": "Mode yolo",
|
||||
"instanceShell.yoloMode.description": "Approuve automatiquement les demandes d'autorisation pour cette session. Desactive par defaut.",
|
||||
"instanceShell.yoloMode.badge": "Mode yolo",
|
||||
"instanceShell.yoloMode.badgeAriaLabel": "Mode yolo active",
|
||||
|
||||
"instanceShell.backgroundProcesses.empty": "Aucun processus en arrière-plan.",
|
||||
"instanceShell.backgroundProcesses.status": "Statut : {status}",
|
||||
"instanceShell.backgroundProcesses.output": "Sortie : {sizeKb}KB",
|
||||
|
||||
@@ -15,6 +15,10 @@ export const sessionMessages = {
|
||||
"sessionList.status.working": "En cours",
|
||||
"sessionList.status.compacting": "Compactage",
|
||||
"sessionList.status.idle": "Inactif",
|
||||
"sessionList.status.retrying": "Nouvelle tentative",
|
||||
"sessionList.status.retryingIn": "Nouvelle tentative dans {seconds}s",
|
||||
"sessionList.status.retryTooltip": "{message} (Tentative {attempt})",
|
||||
"sessionList.status.retryToast": "{countdown} : {message} (Tentative {attempt})",
|
||||
"sessionList.status.needsPermission": "Autorisation requise",
|
||||
"sessionList.status.needsInput": "Entrée requise",
|
||||
"sessionList.expand.collapseAriaLabel": "Réduire la session",
|
||||
|
||||
@@ -26,7 +26,6 @@ export const instanceMessages = {
|
||||
|
||||
"instanceShell.leftPanel.sessionsTitle": "סשנים",
|
||||
"instanceShell.leftPanel.instanceInfo": "מידע על המופע",
|
||||
|
||||
"instanceShell.leftDrawer.pin": "נעץ מגירה שמאלית",
|
||||
"instanceShell.leftDrawer.unpin": "שחרר נעיצת מגירה שמאלית",
|
||||
"instanceShell.leftDrawer.toggle.pinned": "המגירה השמאלית נעוצה",
|
||||
@@ -107,6 +106,8 @@ export const instanceMessages = {
|
||||
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "בטל",
|
||||
"instanceShell.rightPanel.toast.saveSuccess": "הקובץ נשמר בהצלחה",
|
||||
"instanceShell.rightPanel.toast.saveError": "כשלון בשמירת הקובץ",
|
||||
"instanceShell.rightPanel.sections.yoloMode": "מצב Yolo",
|
||||
"instanceShell.rightPanel.sections.yoloMode.tooltip": "מאשר אוטומטית בקשות הרשאה עבור הסשן הנוכחי. השתמשו בזה רק אם אתם סומכים על הכלים שרצים.",
|
||||
"instanceShell.rightPanel.sections.sessionChanges": "שינויי סשן",
|
||||
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "קבצים שהשתנו בסשן הנוכחי. מציג הוספות ומחיקות לכל קובץ.",
|
||||
"instanceShell.rightPanel.sections.plan": "תוכנית",
|
||||
@@ -148,6 +149,12 @@ export const instanceMessages = {
|
||||
"instanceShell.plan.noSessionSelected": "בחר סשן לצפייה בתוכנית.",
|
||||
"instanceShell.plan.empty": "עדיין לא תוכנן דבר.",
|
||||
|
||||
"instanceShell.yoloMode.noSessionSelected": "בחרו סשן כדי להגדיר מצב Yolo.",
|
||||
"instanceShell.yoloMode.title": "מצב Yolo",
|
||||
"instanceShell.yoloMode.description": "מאשר אוטומטית בקשות הרשאה עבור הסשן הזה. כבוי כברירת מחדל.",
|
||||
"instanceShell.yoloMode.badge": "Yolo",
|
||||
"instanceShell.yoloMode.badgeAriaLabel": "מצב Yolo פעיל",
|
||||
|
||||
"instanceShell.backgroundProcesses.empty": "אין תהליכי רקע.",
|
||||
"instanceShell.backgroundProcesses.status": "סטטוס: {status}",
|
||||
"instanceShell.backgroundProcesses.output": "פלט: {sizeKb}KB",
|
||||
|
||||
@@ -15,6 +15,10 @@ export const sessionMessages = {
|
||||
"sessionList.status.working": "עובד",
|
||||
"sessionList.status.compacting": "מסכם",
|
||||
"sessionList.status.idle": "מוכן",
|
||||
"sessionList.status.retrying": "מנסה שוב",
|
||||
"sessionList.status.retryingIn": "מנסה שוב בעוד {seconds}ש׳",
|
||||
"sessionList.status.retryTooltip": "{message} (ניסיון {attempt})",
|
||||
"sessionList.status.retryToast": "{countdown}: {message} (ניסיון {attempt})",
|
||||
"sessionList.status.needsPermission": "נדרש אישור",
|
||||
"sessionList.status.needsInput": "נדרש קלט",
|
||||
"sessionList.expand.collapseAriaLabel": "כווץ סשן",
|
||||
|
||||
@@ -26,7 +26,6 @@ export const instanceMessages = {
|
||||
|
||||
"instanceShell.leftPanel.sessionsTitle": "セッション",
|
||||
"instanceShell.leftPanel.instanceInfo": "インスタンス情報",
|
||||
|
||||
"instanceShell.leftDrawer.pin": "左ドロワーを固定",
|
||||
"instanceShell.leftDrawer.unpin": "左ドロワーの固定を解除",
|
||||
"instanceShell.leftDrawer.toggle.pinned": "左ドロワーを固定しました",
|
||||
@@ -107,6 +106,8 @@ export const instanceMessages = {
|
||||
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "キャンセル",
|
||||
"instanceShell.rightPanel.toast.saveSuccess": "ファイルを保存しました",
|
||||
"instanceShell.rightPanel.toast.saveError": "ファイルの保存に失敗しました",
|
||||
"instanceShell.rightPanel.sections.yoloMode": "Yoloモード",
|
||||
"instanceShell.rightPanel.sections.yoloMode.tooltip": "現在のセッションの権限リクエストを自動承認します。実行中のツールを信頼できる場合にのみ使用してください。",
|
||||
"instanceShell.rightPanel.sections.sessionChanges": "セッション変更",
|
||||
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "現在のセッションで変更されたファイル。各ファイルの追加と削除を表示します。",
|
||||
"instanceShell.rightPanel.sections.plan": "計画",
|
||||
@@ -140,6 +141,12 @@ export const instanceMessages = {
|
||||
"instanceShell.plan.noSessionSelected": "計画を表示するにはセッションを選択してください。",
|
||||
"instanceShell.plan.empty": "まだ計画はありません。",
|
||||
|
||||
"instanceShell.yoloMode.noSessionSelected": "Yoloモードを設定するにはセッションを選択してください。",
|
||||
"instanceShell.yoloMode.title": "Yoloモード",
|
||||
"instanceShell.yoloMode.description": "このセッションの権限リクエストを自動承認します。デフォルトでは無効です。",
|
||||
"instanceShell.yoloMode.badge": "Yolo",
|
||||
"instanceShell.yoloMode.badgeAriaLabel": "Yoloモードが有効",
|
||||
|
||||
"instanceShell.backgroundProcesses.empty": "バックグラウンドプロセスはありません。",
|
||||
"instanceShell.backgroundProcesses.status": "状態: {status}",
|
||||
"instanceShell.backgroundProcesses.output": "出力: {sizeKb}KB",
|
||||
|
||||
@@ -15,6 +15,10 @@ export const sessionMessages = {
|
||||
"sessionList.status.working": "作業中",
|
||||
"sessionList.status.compacting": "圧縮中",
|
||||
"sessionList.status.idle": "待機中",
|
||||
"sessionList.status.retrying": "再試行中",
|
||||
"sessionList.status.retryingIn": "{seconds}秒後に再試行",
|
||||
"sessionList.status.retryTooltip": "{message}({attempt}回目)",
|
||||
"sessionList.status.retryToast": "{countdown}: {message}({attempt}回目)",
|
||||
"sessionList.status.needsPermission": "許可待ち",
|
||||
"sessionList.status.needsInput": "入力待ち",
|
||||
"sessionList.expand.collapseAriaLabel": "セッションを折りたたむ",
|
||||
|
||||
@@ -26,7 +26,6 @@ export const instanceMessages = {
|
||||
|
||||
"instanceShell.leftPanel.sessionsTitle": "Сессии",
|
||||
"instanceShell.leftPanel.instanceInfo": "Информация об экземпляре",
|
||||
|
||||
"instanceShell.leftDrawer.pin": "Закрепить левую панель",
|
||||
"instanceShell.leftDrawer.unpin": "Открепить левую панель",
|
||||
"instanceShell.leftDrawer.toggle.pinned": "Левая панель закреплена",
|
||||
@@ -107,6 +106,8 @@ export const instanceMessages = {
|
||||
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "Отмена",
|
||||
"instanceShell.rightPanel.toast.saveSuccess": "Файл успешно сохранён",
|
||||
"instanceShell.rightPanel.toast.saveError": "Не удалось сохранить файл",
|
||||
"instanceShell.rightPanel.sections.yoloMode": "Режим Yolo",
|
||||
"instanceShell.rightPanel.sections.yoloMode.tooltip": "Автоматически одобряет запросы разрешений для текущей сессии. Включайте только если доверяете запускаемым инструментам.",
|
||||
"instanceShell.rightPanel.sections.sessionChanges": "Изменения сессии",
|
||||
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Файлы, измененные в текущей сессии. Показывает добавления и удаления для каждого файла.",
|
||||
"instanceShell.rightPanel.sections.plan": "План",
|
||||
@@ -140,6 +141,12 @@ export const instanceMessages = {
|
||||
"instanceShell.plan.noSessionSelected": "Выберите сессию, чтобы просмотреть план.",
|
||||
"instanceShell.plan.empty": "Пока ничего не запланировано.",
|
||||
|
||||
"instanceShell.yoloMode.noSessionSelected": "Выберите сессию, чтобы настроить режим Yolo.",
|
||||
"instanceShell.yoloMode.title": "Режим Yolo",
|
||||
"instanceShell.yoloMode.description": "Автоматически одобряет запросы разрешений для этой сессии. По умолчанию выключен.",
|
||||
"instanceShell.yoloMode.badge": "Yolo",
|
||||
"instanceShell.yoloMode.badgeAriaLabel": "Режим Yolo включен",
|
||||
|
||||
"instanceShell.backgroundProcesses.empty": "Нет фоновых процессов.",
|
||||
"instanceShell.backgroundProcesses.status": "Статус: {status}",
|
||||
"instanceShell.backgroundProcesses.output": "Вывод: {sizeKb}KB",
|
||||
|
||||
@@ -15,6 +15,10 @@ export const sessionMessages = {
|
||||
"sessionList.status.working": "Работает",
|
||||
"sessionList.status.compacting": "Компактация",
|
||||
"sessionList.status.idle": "Простой",
|
||||
"sessionList.status.retrying": "Повтор",
|
||||
"sessionList.status.retryingIn": "Повтор через {seconds}с",
|
||||
"sessionList.status.retryTooltip": "{message} (Попытка {attempt})",
|
||||
"sessionList.status.retryToast": "{countdown}: {message} (Попытка {attempt})",
|
||||
"sessionList.status.needsPermission": "Требуется разрешение",
|
||||
"sessionList.status.needsInput": "Требуется ввод",
|
||||
"sessionList.expand.collapseAriaLabel": "Свернуть сессию",
|
||||
|
||||
@@ -26,7 +26,6 @@ export const instanceMessages = {
|
||||
|
||||
"instanceShell.leftPanel.sessionsTitle": "会话",
|
||||
"instanceShell.leftPanel.instanceInfo": "实例信息",
|
||||
|
||||
"instanceShell.leftDrawer.pin": "固定左侧抽屉",
|
||||
"instanceShell.leftDrawer.unpin": "取消固定左侧抽屉",
|
||||
"instanceShell.leftDrawer.toggle.pinned": "左侧抽屉已固定",
|
||||
@@ -107,6 +106,8 @@ export const instanceMessages = {
|
||||
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "取消",
|
||||
"instanceShell.rightPanel.toast.saveSuccess": "文件保存成功",
|
||||
"instanceShell.rightPanel.toast.saveError": "保存文件失败",
|
||||
"instanceShell.rightPanel.sections.yoloMode": "Yolo 模式",
|
||||
"instanceShell.rightPanel.sections.yoloMode.tooltip": "自动批准当前会话的权限请求。仅在你信任正在运行的工具时启用。",
|
||||
"instanceShell.rightPanel.sections.sessionChanges": "会话更改",
|
||||
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "当前会话中修改的文件。显示每个文件的添加和删除。",
|
||||
"instanceShell.rightPanel.sections.plan": "计划",
|
||||
@@ -140,6 +141,12 @@ export const instanceMessages = {
|
||||
"instanceShell.plan.noSessionSelected": "选择会话以查看计划。",
|
||||
"instanceShell.plan.empty": "暂无计划。",
|
||||
|
||||
"instanceShell.yoloMode.noSessionSelected": "请选择一个会话来配置 Yolo 模式。",
|
||||
"instanceShell.yoloMode.title": "Yolo 模式",
|
||||
"instanceShell.yoloMode.description": "自动批准此会话的权限请求。默认关闭。",
|
||||
"instanceShell.yoloMode.badge": "Yolo",
|
||||
"instanceShell.yoloMode.badgeAriaLabel": "Yolo 模式已启用",
|
||||
|
||||
"instanceShell.backgroundProcesses.empty": "没有后台进程。",
|
||||
"instanceShell.backgroundProcesses.status": "状态:{status}",
|
||||
"instanceShell.backgroundProcesses.output": "输出:{sizeKb}KB",
|
||||
|
||||
@@ -15,6 +15,10 @@ export const sessionMessages = {
|
||||
"sessionList.status.working": "工作中",
|
||||
"sessionList.status.compacting": "压缩中",
|
||||
"sessionList.status.idle": "空闲",
|
||||
"sessionList.status.retrying": "重试中",
|
||||
"sessionList.status.retryingIn": "{seconds} 秒后重试",
|
||||
"sessionList.status.retryTooltip": "{message}(第 {attempt} 次尝试)",
|
||||
"sessionList.status.retryToast": "{countdown}: {message}(第 {attempt} 次尝试)",
|
||||
"sessionList.status.needsPermission": "需要权限",
|
||||
"sessionList.status.needsInput": "需要输入",
|
||||
"sessionList.expand.collapseAriaLabel": "折叠会话",
|
||||
|
||||
@@ -120,14 +120,7 @@ function resolveLanguage(token: string): { canonical: string | null; raw: string
|
||||
return { canonical: null, raw: normalized }
|
||||
}
|
||||
|
||||
async function ensureLanguages(content: string) {
|
||||
if (highlightSuppressed) {
|
||||
return
|
||||
}
|
||||
|
||||
// Extract code-fence language tokens via `marked` so we correctly handle code blocks
|
||||
// that contain backticks (e.g. JS template literals). Regex-based fence scans tend
|
||||
// to miss these and prevent languages from loading.
|
||||
function collectCodeFenceLanguages(content: string): string[] {
|
||||
const foundLanguages = new Set<string>()
|
||||
try {
|
||||
const tokens = marked.lexer(content) as any
|
||||
@@ -139,10 +132,44 @@ async function ensureLanguages(content: string) {
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
// If tokenization fails for any reason, skip language preloading.
|
||||
return []
|
||||
}
|
||||
|
||||
return [...foundLanguages]
|
||||
}
|
||||
|
||||
export function hasPendingCodeHighlight(content: string): boolean {
|
||||
const languages = collectCodeFenceLanguages(content)
|
||||
for (const token of languages) {
|
||||
const rawToken = normalizeLanguageToken(token)
|
||||
if (!rawToken || rawToken === "text") {
|
||||
continue
|
||||
}
|
||||
|
||||
const { canonical, raw } = resolveLanguage(token)
|
||||
const langKey = canonical || raw
|
||||
if (langKey === "text" || raw === "text") {
|
||||
continue
|
||||
}
|
||||
|
||||
if (!highlighter || !loadedLanguages.has(langKey)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
async function ensureLanguages(content: string) {
|
||||
if (highlightSuppressed) {
|
||||
return
|
||||
}
|
||||
|
||||
// Extract code-fence language tokens via `marked` so we correctly handle code blocks
|
||||
// that contain backticks (e.g. JS template literals). Regex-based fence scans tend
|
||||
// to miss these and prevent languages from loading.
|
||||
const foundLanguages = collectCodeFenceLanguages(content)
|
||||
|
||||
// Queue language loading tasks
|
||||
for (const token of foundLanguages) {
|
||||
const rawToken = normalizeLanguageToken(token)
|
||||
|
||||
@@ -102,9 +102,11 @@ export function showToastNotification(payload: ToastPayload): ToastHandle {
|
||||
</button>
|
||||
<div class="flex items-start gap-3 pr-6">
|
||||
<span class={`mt-1 inline-block h-2.5 w-2.5 rounded-full ${accent.badge}`} />
|
||||
<div class="flex-1 text-sm leading-snug">
|
||||
{payload.title && <p class={`font-semibold ${accent.headline}`}>{payload.title}</p>}
|
||||
<p class={`${accent.body} ${payload.title ? "mt-1" : ""}`}>{payload.message}</p>
|
||||
<div class="min-w-0 flex-1 text-sm leading-snug">
|
||||
{payload.title && <p class={`break-words ${accent.headline} font-semibold`}>{payload.title}</p>}
|
||||
<p class={`${accent.body} ${payload.title ? "mt-1" : ""} whitespace-pre-wrap break-words [overflow-wrap:anywhere]`}>
|
||||
{payload.message}
|
||||
</p>
|
||||
{payload.action && (
|
||||
<button
|
||||
type="button"
|
||||
|
||||
81
packages/ui/src/stores/permission-auto-accept.ts
Normal file
81
packages/ui/src/stores/permission-auto-accept.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { createSignal } from "solid-js"
|
||||
|
||||
const STORAGE_KEY = "codenomad:permission-auto-accept:v1"
|
||||
|
||||
function makeKey(instanceId: string, sessionId: string) {
|
||||
return `${instanceId}:${sessionId}`
|
||||
}
|
||||
|
||||
function readInitialState() {
|
||||
if (typeof window === "undefined" || !window.localStorage) {
|
||||
return new Map<string, boolean>()
|
||||
}
|
||||
|
||||
try {
|
||||
const raw = window.localStorage.getItem(STORAGE_KEY)
|
||||
if (!raw) return new Map<string, boolean>()
|
||||
const parsed = JSON.parse(raw) as Record<string, boolean>
|
||||
return new Map(Object.entries(parsed).filter((entry): entry is [string, boolean] => entry[1] === true))
|
||||
} catch {
|
||||
return new Map<string, boolean>()
|
||||
}
|
||||
}
|
||||
|
||||
function persist(next: Map<string, boolean>) {
|
||||
if (typeof window === "undefined" || !window.localStorage) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(Object.fromEntries(next)))
|
||||
} catch {
|
||||
// ignore persistence failures
|
||||
}
|
||||
}
|
||||
|
||||
const [autoAcceptState, setAutoAcceptState] = createSignal(readInitialState())
|
||||
const [inFlightVersion, setInFlightVersion] = createSignal(0)
|
||||
|
||||
const inFlight = new Set<string>()
|
||||
|
||||
export function isPermissionAutoAcceptEnabled(instanceId: string, sessionId: string) {
|
||||
return autoAcceptState().get(makeKey(instanceId, sessionId)) ?? false
|
||||
}
|
||||
|
||||
export function setPermissionAutoAcceptEnabled(instanceId: string, sessionId: string, enabled: boolean) {
|
||||
const key = makeKey(instanceId, sessionId)
|
||||
setAutoAcceptState((prev) => {
|
||||
const next = new Map(prev)
|
||||
if (enabled) {
|
||||
next.set(key, true)
|
||||
} else {
|
||||
next.delete(key)
|
||||
}
|
||||
persist(next)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
export function togglePermissionAutoAccept(instanceId: string, sessionId: string) {
|
||||
setPermissionAutoAcceptEnabled(instanceId, sessionId, !isPermissionAutoAcceptEnabled(instanceId, sessionId))
|
||||
}
|
||||
|
||||
export function canAutoRespondPermission(instanceId: string, sessionId: string, requestId: string) {
|
||||
const key = makeKey(instanceId, sessionId)
|
||||
if (!autoAcceptState().get(key)) return false
|
||||
const requestKey = `${key}:${requestId}`
|
||||
if (inFlight.has(requestKey)) return false
|
||||
inFlight.add(requestKey)
|
||||
return true
|
||||
}
|
||||
|
||||
export function getPermissionAutoAcceptInFlightVersion() {
|
||||
return inFlightVersion()
|
||||
}
|
||||
|
||||
export function finishAutoRespondPermission(instanceId: string, sessionId: string, requestId: string) {
|
||||
if (!inFlight.delete(`${makeKey(instanceId, sessionId)}:${requestId}`)) {
|
||||
return
|
||||
}
|
||||
setInFlightVersion((value) => value + 1)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { mapSdkSessionStatus, type Session, type SessionStatus } from "../types/session"
|
||||
import { mapSdkSessionRetry, mapSdkSessionStatus, type Session, type SessionStatus } from "../types/session"
|
||||
import type { Message } from "../types/message"
|
||||
import type { FileDiff } from "@opencode-ai/sdk/v2/client"
|
||||
|
||||
@@ -149,12 +149,15 @@ async function fetchSessions(instanceId: string): Promise<void> {
|
||||
const existingStatus = existingSession?.status
|
||||
|
||||
let status: SessionStatus
|
||||
let retry = existingSession?.retry ?? null
|
||||
if (existingStatus === "compacting") {
|
||||
status = "compacting"
|
||||
retry = null
|
||||
} else {
|
||||
const rawStatus = (apiSession as any)?.status ?? statusById[apiSession.id]
|
||||
const hasType = rawStatus && typeof rawStatus === "object" && typeof rawStatus.type === "string"
|
||||
status = hasType ? mapSdkSessionStatus(rawStatus) : existingStatus ?? "idle"
|
||||
retry = hasType ? mapSdkSessionRetry(rawStatus) : retry
|
||||
}
|
||||
|
||||
sessionMap.set(apiSession.id, {
|
||||
@@ -165,6 +168,7 @@ async function fetchSessions(instanceId: string): Promise<void> {
|
||||
agent: existingSession?.agent ?? "",
|
||||
model: existingSession?.model ?? { providerId: "", modelId: "" },
|
||||
status,
|
||||
retry,
|
||||
version: apiSession.version,
|
||||
time: {
|
||||
...apiSession.time,
|
||||
|
||||
@@ -28,7 +28,7 @@ import type { PermissionReplyEventPropertiesLike, PermissionRequestLike } from "
|
||||
import { getQuestionId, getQuestionSessionId, getRequestIdFromQuestionReply } from "../types/question"
|
||||
import type { QuestionRequest } from "../types/question"
|
||||
import type { EventQuestionReplied, EventQuestionRejected } from "@opencode-ai/sdk/v2"
|
||||
import { showToastNotification, ToastVariant } from "../lib/notifications"
|
||||
import { showToastNotification, type ToastHandle, ToastVariant } from "../lib/notifications"
|
||||
import { sendOsNotification } from "../lib/os-notifications"
|
||||
import { preferences } from "./preferences"
|
||||
import {
|
||||
@@ -39,7 +39,14 @@ import {
|
||||
removeQuestionFromQueue,
|
||||
} from "./instances"
|
||||
import { showAlertDialog } from "./alerts"
|
||||
import { createClientSession, mapSdkSessionStatus, type Session, type SessionStatus } from "../types/session"
|
||||
import {
|
||||
createClientSession,
|
||||
mapSdkSessionRetry,
|
||||
mapSdkSessionStatus,
|
||||
type Session,
|
||||
type SessionRetryState,
|
||||
type SessionStatus,
|
||||
} from "../types/session"
|
||||
import { ensureSessionParentExpanded, sessions, setSessions, syncInstanceSessionIndicator, withSession } from "./session-state"
|
||||
import { normalizeMessagePart } from "./message-v2/normalizers"
|
||||
import { updateSessionInfo } from "./message-v2/session-info"
|
||||
@@ -67,6 +74,15 @@ import { handleConversationAssistantPartUpdated } from "./conversation-speech"
|
||||
|
||||
const log = getLogger("sse")
|
||||
const pendingSessionFetches = new Map<string, Promise<void>>()
|
||||
let activeRetryToast: ToastHandle | null = null
|
||||
|
||||
function isSameRetryState(left: SessionRetryState | null | undefined, right: SessionRetryState | null | undefined): boolean {
|
||||
const a = left ?? null
|
||||
const b = right ?? null
|
||||
if (a === b) return true
|
||||
if (!a || !b) return false
|
||||
return a.attempt === b.attempt && a.message === b.message && a.next === b.next
|
||||
}
|
||||
|
||||
function shouldSendOsNotification(kind: "needsInput" | "idle"): boolean {
|
||||
if (typeof document === "undefined") return false
|
||||
@@ -131,18 +147,20 @@ interface TuiToastEvent {
|
||||
|
||||
const ALLOWED_TOAST_VARIANTS = new Set<ToastVariant>(["info", "success", "warning", "error"])
|
||||
|
||||
function applySessionStatus(instanceId: string, sessionId: string, status: SessionStatus) {
|
||||
function applySessionStatus(instanceId: string, sessionId: string, status: SessionStatus, retry?: SessionRetryState | null) {
|
||||
let parentToExpand: string | null = null
|
||||
|
||||
withSession(instanceId, sessionId, (session) => {
|
||||
const current = session.status ?? "idle"
|
||||
if (current === status) return false
|
||||
const nextRetry = retry ?? null
|
||||
if (current === status && isSameRetryState(session.retry, nextRetry)) return false
|
||||
|
||||
if (current === "compacting" && status !== "compacting") {
|
||||
return false
|
||||
}
|
||||
|
||||
session.status = status
|
||||
session.retry = status === "working" ? nextRetry : null
|
||||
|
||||
// Auto-expand the parent thread when a child session starts working.
|
||||
// Users can still collapse it; we only expand on the transition.
|
||||
@@ -172,6 +190,7 @@ async function fetchSessionInfo(instanceId: string, sessionId: string, directory
|
||||
)
|
||||
|
||||
let fetchedStatus: SessionStatus = "idle"
|
||||
let fetchedRetry: SessionRetryState | null = null
|
||||
try {
|
||||
let statuses: Record<string, any> = {}
|
||||
try {
|
||||
@@ -187,11 +206,13 @@ async function fetchSessionInfo(instanceId: string, sessionId: string, directory
|
||||
const rawStatus = (info as any)?.status ?? statuses?.[sessionId]
|
||||
const hasType = rawStatus && typeof rawStatus === "object" && typeof rawStatus.type === "string"
|
||||
fetchedStatus = hasType ? mapSdkSessionStatus(rawStatus) : "idle"
|
||||
fetchedRetry = hasType ? mapSdkSessionRetry(rawStatus) : null
|
||||
} catch (error) {
|
||||
log.error("Failed to fetch session status", error)
|
||||
}
|
||||
|
||||
const fetched = createClientSession(info, instanceId, "", { providerId: "", modelId: "" }, fetchedStatus)
|
||||
fetched.retry = fetchedRetry
|
||||
|
||||
let updatedInstanceSessions: Map<string, Session> | undefined
|
||||
let shouldExpandParent: string | null = null
|
||||
@@ -205,6 +226,7 @@ async function fetchSessionInfo(instanceId: string, sessionId: string, directory
|
||||
agent: existing?.agent ?? fetched.agent,
|
||||
model: existing?.model ?? fetched.model,
|
||||
status: existing?.status === "compacting" ? "compacting" : fetched.status,
|
||||
retry: existing?.status === "compacting" ? null : fetched.retry,
|
||||
pendingPermission: existing?.pendingPermission ?? fetched.pendingPermission,
|
||||
pendingQuestion: existing?.pendingQuestion ?? false,
|
||||
}
|
||||
@@ -231,14 +253,20 @@ async function fetchSessionInfo(instanceId: string, sessionId: string, directory
|
||||
}
|
||||
}
|
||||
|
||||
function ensureSessionStatus(instanceId: string, sessionId: string, status: SessionStatus, directory?: string) {
|
||||
function ensureSessionStatus(
|
||||
instanceId: string,
|
||||
sessionId: string,
|
||||
status: SessionStatus,
|
||||
directory?: string,
|
||||
retry?: SessionRetryState | null,
|
||||
) {
|
||||
const instanceSessions = sessions().get(instanceId)
|
||||
const existing = instanceSessions?.get(sessionId)
|
||||
if (existing) {
|
||||
if ((existing.status ?? "idle") === status) {
|
||||
if ((existing.status ?? "idle") === status && isSameRetryState(existing.retry, retry)) {
|
||||
return
|
||||
}
|
||||
applySessionStatus(instanceId, sessionId, status)
|
||||
applySessionStatus(instanceId, sessionId, status, retry)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -250,7 +278,7 @@ function ensureSessionStatus(instanceId: string, sessionId: string, status: Sess
|
||||
const pending = (async () => {
|
||||
const fetched = await fetchSessionInfo(instanceId, sessionId, directory)
|
||||
if (!fetched) return
|
||||
applySessionStatus(instanceId, sessionId, status)
|
||||
applySessionStatus(instanceId, sessionId, status, retry)
|
||||
})()
|
||||
|
||||
pendingSessionFetches.set(key, pending)
|
||||
@@ -428,6 +456,7 @@ function handleSessionUpdate(instanceId: string, event: EventSessionUpdated): vo
|
||||
modelId: "",
|
||||
},
|
||||
status: "idle",
|
||||
retry: null,
|
||||
version: info.version || "0",
|
||||
time: info.time
|
||||
? { ...info.time }
|
||||
@@ -461,6 +490,7 @@ function handleSessionUpdate(instanceId: string, event: EventSessionUpdated): vo
|
||||
...existingSession,
|
||||
title: info.title || existingSession.title,
|
||||
status: existingSession.status ?? "idle",
|
||||
retry: existingSession.retry ?? null,
|
||||
time: mergedTime,
|
||||
revert: info.revert
|
||||
? {
|
||||
@@ -532,8 +562,29 @@ function handleSessionStatus(instanceId: string, event: EventSessionStatus): voi
|
||||
const sessionId = event.properties?.sessionID
|
||||
if (!sessionId) return
|
||||
|
||||
const status = mapSdkSessionStatus(event.properties.status)
|
||||
ensureSessionStatus(instanceId, sessionId, status, (event as any)?.directory)
|
||||
const rawStatus = event.properties.status
|
||||
const status = mapSdkSessionStatus(rawStatus)
|
||||
const retry = mapSdkSessionRetry(rawStatus)
|
||||
ensureSessionStatus(instanceId, sessionId, status, (event as any)?.directory, retry)
|
||||
if (retry) {
|
||||
const remainingSeconds = Math.max(0, Math.round((retry.next - Date.now()) / 1000))
|
||||
const countdown =
|
||||
remainingSeconds > 0
|
||||
? tGlobal("sessionList.status.retryingIn", { seconds: String(remainingSeconds) })
|
||||
: tGlobal("sessionList.status.retrying")
|
||||
const label = getSessionTitle(instanceId, sessionId)
|
||||
activeRetryToast?.dismiss()
|
||||
activeRetryToast = showToastNotification({
|
||||
title: label || getInstanceDisplayName(instanceId),
|
||||
message: tGlobal("sessionList.status.retryToast", {
|
||||
countdown,
|
||||
message: retry.message,
|
||||
attempt: String(retry.attempt),
|
||||
}),
|
||||
variant: "error",
|
||||
duration: 7000,
|
||||
})
|
||||
}
|
||||
log.info(`[SSE] Session status updated: ${sessionId}`, { status })
|
||||
}
|
||||
|
||||
@@ -547,6 +598,7 @@ function handleSessionCompacted(instanceId: string, event: EventSessionCompacted
|
||||
if (existing) {
|
||||
withSession(instanceId, sessionID, (session) => {
|
||||
session.status = "working"
|
||||
session.retry = null
|
||||
})
|
||||
} else {
|
||||
ensureSessionStatus(instanceId, sessionID, "working", (event as any)?.directory)
|
||||
|
||||
@@ -353,6 +353,9 @@ function setSessionStatus(instanceId: string, sessionId: string, status: Session
|
||||
if (session.status === status) return false
|
||||
const previous = session.status
|
||||
session.status = status
|
||||
if (status !== "working") {
|
||||
session.retry = null
|
||||
}
|
||||
|
||||
// If a child session starts working, auto-expand its parent thread once.
|
||||
// Users can still collapse it afterwards; we only expand on the transition.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Session, SessionStatus } from "../types/session"
|
||||
import type { Session, SessionRetryState, SessionStatus } from "../types/session"
|
||||
import { getInstanceSessionIndicatorStatusCached, sessions } from "./session-state"
|
||||
|
||||
function getSession(instanceId: string, sessionId: string): Session | null {
|
||||
@@ -14,6 +14,15 @@ export function getSessionStatus(instanceId: string, sessionId: string): Session
|
||||
return session.status ?? "idle"
|
||||
}
|
||||
|
||||
export function getSessionRetry(instanceId: string, sessionId: string): SessionRetryState | null {
|
||||
const session = getSession(instanceId, sessionId)
|
||||
return session?.retry ?? null
|
||||
}
|
||||
|
||||
export function getRetrySeconds(next: number, now = Date.now()): number {
|
||||
return Math.max(0, Math.round((next - now) / 1000))
|
||||
}
|
||||
|
||||
export type InstanceSessionIndicatorStatus = "permission" | SessionStatus
|
||||
|
||||
export function getInstanceSessionIndicatorStatus(instanceId: string): InstanceSessionIndicatorStatus {
|
||||
|
||||
@@ -184,6 +184,7 @@
|
||||
}
|
||||
|
||||
.status-indicator.session-status.session-working,
|
||||
.status-indicator.session-status.session-retrying,
|
||||
.status-indicator.session-status.session-compacting,
|
||||
.status-indicator.session-status.session-idle {
|
||||
font-weight: var(--font-weight-medium);
|
||||
@@ -194,6 +195,11 @@
|
||||
--session-status-dot: var(--session-status-working-fg);
|
||||
}
|
||||
|
||||
.status-indicator.session-status.session-retrying {
|
||||
color: var(--status-error);
|
||||
--session-status-dot: var(--status-error);
|
||||
}
|
||||
|
||||
.status-indicator.session-status.session-compacting {
|
||||
color: var(--session-status-compacting-fg);
|
||||
--session-status-dot: var(--session-status-compacting-fg);
|
||||
@@ -222,6 +228,10 @@
|
||||
background-color: var(--session-status-working-bg);
|
||||
}
|
||||
|
||||
.status-indicator.session-status.session-retrying.session-status-list {
|
||||
background-color: var(--status-error-bg);
|
||||
}
|
||||
|
||||
.status-indicator.session-status.session-compacting.session-status-list {
|
||||
background-color: var(--session-status-compacting-bg);
|
||||
}
|
||||
|
||||
@@ -412,6 +412,19 @@
|
||||
background-color: var(--surface-secondary);
|
||||
}
|
||||
|
||||
.right-panel-accordion-header-row {
|
||||
@apply flex items-center gap-2;
|
||||
}
|
||||
|
||||
.right-panel-accordion-header-row .right-panel-accordion-trigger {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.right-panel-accordion-header-row .section-info-trigger {
|
||||
flex: 0 0 auto;
|
||||
margin-inline-end: 0.75rem;
|
||||
}
|
||||
|
||||
.right-panel-accordion-trigger {
|
||||
@apply w-full flex items-center justify-between px-3 py-2.5 text-[11px] font-semibold uppercase tracking-wide transition-colors duration-150;
|
||||
color: var(--text-secondary);
|
||||
@@ -452,6 +465,8 @@
|
||||
@apply inline-flex items-center justify-center p-0.5 rounded transition-all duration-150;
|
||||
color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.section-info-trigger:hover {
|
||||
@@ -459,6 +474,12 @@
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
.section-info-trigger:focus-visible {
|
||||
@apply ring-2 ring-offset-1;
|
||||
ring-color: var(--accent-primary);
|
||||
ring-offset-color: var(--surface-secondary);
|
||||
}
|
||||
|
||||
.section-label {
|
||||
margin-inline-start: 2px;
|
||||
}
|
||||
|
||||
@@ -107,6 +107,28 @@
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
.session-sidebar-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
padding: 0.4rem 0.65rem;
|
||||
border: 1px solid var(--border-base);
|
||||
border-radius: 0.75rem;
|
||||
background: var(--surface-base);
|
||||
min-height: 2rem;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.session-sidebar-toggle-title {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.session-sidebar-controls .selector-trigger,
|
||||
.session-sidebar-controls [data-model-selector-control],
|
||||
.session-sidebar-controls .selector-trigger-label,
|
||||
@@ -394,6 +416,7 @@ session-sidebar-controls .selector-trigger-primary {
|
||||
}
|
||||
|
||||
.status-indicator.session-status.session-working,
|
||||
.status-indicator.session-status.session-retrying,
|
||||
.status-indicator.session-status.session-compacting,
|
||||
.status-indicator.session-status.session-idle {
|
||||
font-weight: var(--font-weight-medium);
|
||||
@@ -404,6 +427,11 @@ session-sidebar-controls .selector-trigger-primary {
|
||||
--session-status-dot: var(--session-status-working-fg);
|
||||
}
|
||||
|
||||
.status-indicator.session-status.session-retrying {
|
||||
color: var(--status-error);
|
||||
--session-status-dot: var(--status-error);
|
||||
}
|
||||
|
||||
.status-indicator.session-status.session-compacting {
|
||||
color: var(--session-status-compacting-fg);
|
||||
--session-status-dot: var(--session-status-compacting-fg);
|
||||
@@ -432,6 +460,10 @@ session-sidebar-controls .selector-trigger-primary {
|
||||
background-color: var(--session-status-working-bg);
|
||||
}
|
||||
|
||||
.status-indicator.session-status.session-retrying.session-status-list {
|
||||
background-color: var(--status-error-bg);
|
||||
}
|
||||
|
||||
.status-indicator.session-status.session-compacting.session-status-list {
|
||||
background-color: var(--session-status-compacting-bg);
|
||||
}
|
||||
@@ -458,6 +490,16 @@ session-sidebar-controls .selector-trigger-primary {
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.status-indicator.session-yolo-mode {
|
||||
color: var(--accent-primary);
|
||||
background-color: color-mix(in oklab, var(--accent-primary) 14%, transparent);
|
||||
border-color: color-mix(in oklab, var(--accent-primary) 28%, transparent);
|
||||
}
|
||||
|
||||
.status-indicator.session-yolo-mode .status-dot {
|
||||
background-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.session-list-container {
|
||||
min-width: 200px;
|
||||
|
||||
@@ -17,6 +17,12 @@ export type {
|
||||
|
||||
export type SessionStatus = "idle" | "working" | "compacting"
|
||||
|
||||
export interface SessionRetryState {
|
||||
attempt: number
|
||||
message: string
|
||||
next: number
|
||||
}
|
||||
|
||||
export function mapSdkSessionStatus(status: SDKSessionStatus | null | undefined): SessionStatus {
|
||||
if (!status || status.type === "idle") {
|
||||
return "idle"
|
||||
@@ -26,6 +32,18 @@ export function mapSdkSessionStatus(status: SDKSessionStatus | null | undefined)
|
||||
return "working"
|
||||
}
|
||||
|
||||
export function mapSdkSessionRetry(status: SDKSessionStatus | null | undefined): SessionRetryState | null {
|
||||
if (!status || status.type !== "retry") {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
attempt: typeof status.attempt === "number" ? status.attempt : 1,
|
||||
message: typeof status.message === "string" ? status.message : "",
|
||||
next: typeof status.next === "number" ? status.next : Date.now(),
|
||||
}
|
||||
}
|
||||
|
||||
// Our client-specific Session interface extending SDK Session
|
||||
export interface Session
|
||||
extends Omit<import("@opencode-ai/sdk").Session, "projectID" | "directory" | "parentID"> {
|
||||
@@ -40,6 +58,7 @@ export interface Session
|
||||
pendingPermission?: boolean // Indicates if session is waiting on user permission
|
||||
pendingQuestion?: boolean // Indicates if session is waiting on user input
|
||||
status: SessionStatus // Single source of truth for session status
|
||||
retry?: SessionRetryState | null // Retry metadata for transient backoff states
|
||||
diff?: FileDiff[] // Session-level file diffs (hydrated via session.diff)
|
||||
}
|
||||
|
||||
|
||||
40
scripts/bump-version.js
Normal file
40
scripts/bump-version.js
Normal file
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const { spawnSync } = require("child_process")
|
||||
|
||||
const versionArgs = process.argv.slice(2)
|
||||
|
||||
if (versionArgs.length === 0) {
|
||||
console.error("[bumpVersion] missing version argument (example: npm run bumpVersion -- patch)")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm"
|
||||
|
||||
function runStep(args, label) {
|
||||
const result = spawnSync(npmCommand, args, {
|
||||
stdio: "inherit",
|
||||
})
|
||||
|
||||
if (result.error) {
|
||||
console.error(`[bumpVersion] failed during ${label}: ${result.error.message}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (result.status !== 0) {
|
||||
process.exit(result.status ?? 1)
|
||||
}
|
||||
}
|
||||
|
||||
runStep(
|
||||
[
|
||||
"version",
|
||||
...versionArgs,
|
||||
"--workspaces",
|
||||
"--include-workspace-root",
|
||||
"--no-git-tag-version",
|
||||
],
|
||||
"npm version"
|
||||
)
|
||||
|
||||
runStep(["run", "sync:version", "--workspace", "@codenomad/tauri-app"], "tauri version sync")
|
||||
Reference in New Issue
Block a user