Compare commits
51 Commits
v0.13.1-de
...
v0.13.3-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e82e529a8f | ||
|
|
4f236ce36f | ||
|
|
2ffeb45a9c | ||
|
|
df16b64a95 | ||
|
|
f3c54df283 | ||
|
|
5658a9f62d | ||
|
|
9d6a5bcdc0 | ||
|
|
514b187b00 | ||
|
|
240acb7729 | ||
|
|
278b563c1a | ||
|
|
0af79002ed | ||
|
|
f3981a1cce | ||
|
|
031e8d5717 | ||
|
|
995fb3b6a3 | ||
|
|
aeb0ff11b3 | ||
|
|
b61cfbd9f9 | ||
|
|
481dd1a88a | ||
|
|
3f6cdd36f3 | ||
|
|
fe932c8307 | ||
|
|
64ac885157 | ||
|
|
1d953dfe64 | ||
|
|
42589464e5 | ||
|
|
27bccb8d6b | ||
|
|
153065d025 | ||
|
|
2abda0e6b4 | ||
|
|
800133361d | ||
|
|
034cb5dea9 | ||
|
|
d7ab84f245 | ||
|
|
201988b97c | ||
|
|
6a6fcff2c8 | ||
|
|
f29f197b9a | ||
|
|
dbde403b3e | ||
|
|
230c981cc2 | ||
|
|
34978c87fb | ||
|
|
3e6d0a402c | ||
|
|
e81c5f6443 | ||
|
|
b0d27bd127 | ||
|
|
7576470295 | ||
|
|
6d32e09db0 | ||
|
|
503cb3a02e | ||
|
|
0250c6350f | ||
|
|
24cc8fe939 | ||
|
|
282b234a7c | ||
|
|
4ba088a876 | ||
|
|
7b1817d606 | ||
|
|
5bc3c23ec5 | ||
|
|
127a51e3c3 | ||
|
|
daa22b6d8c | ||
|
|
23f2de2d7e | ||
|
|
80c9b76709 | ||
|
|
a29b77d60b |
5
.github/workflows/comment-pr-artifacts.yml
vendored
5
.github/workflows/comment-pr-artifacts.yml
vendored
@@ -4,6 +4,7 @@ on:
|
|||||||
pull_request_target:
|
pull_request_target:
|
||||||
types:
|
types:
|
||||||
- opened
|
- opened
|
||||||
|
- edited
|
||||||
- synchronize
|
- synchronize
|
||||||
- reopened
|
- reopened
|
||||||
- ready_for_review
|
- ready_for_review
|
||||||
@@ -19,7 +20,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
ALLOWED_ACTORS: ${{ vars.ALLOWED_NON_DEV_PR_ACTORS }}
|
ALLOWED_ACTORS: ${{ vars.ALLOWED_NON_DEV_PR_ACTORS }}
|
||||||
ACTOR: ${{ github.actor }}
|
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
||||||
BASE_REF: ${{ github.event.pull_request.base.ref }}
|
BASE_REF: ${{ github.event.pull_request.base.ref }}
|
||||||
IS_DRAFT: ${{ github.event.pull_request.draft }}
|
IS_DRAFT: ${{ github.event.pull_request.draft }}
|
||||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
@@ -37,7 +38,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
normalized=",${ALLOWED_ACTORS},"
|
normalized=",${ALLOWED_ACTORS},"
|
||||||
if [[ "$normalized" == *",${ACTOR},"* ]]; then
|
if [[ "$normalized" == *",${PR_AUTHOR},"* ]]; then
|
||||||
echo "allowed=true" >> "$GITHUB_OUTPUT"
|
echo "allowed=true" >> "$GITHUB_OUTPUT"
|
||||||
else
|
else
|
||||||
echo "allowed=false" >> "$GITHUB_OUTPUT"
|
echo "allowed=false" >> "$GITHUB_OUTPUT"
|
||||||
|
|||||||
7
.github/workflows/pr-build.yml
vendored
7
.github/workflows/pr-build.yml
vendored
@@ -4,6 +4,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
types:
|
types:
|
||||||
- opened
|
- opened
|
||||||
|
- edited
|
||||||
- synchronize
|
- synchronize
|
||||||
- reopened
|
- reopened
|
||||||
- ready_for_review
|
- ready_for_review
|
||||||
@@ -23,7 +24,7 @@ jobs:
|
|||||||
allowed: ${{ steps.auth.outputs.allowed }}
|
allowed: ${{ steps.auth.outputs.allowed }}
|
||||||
env:
|
env:
|
||||||
ALLOWED_ACTORS: ${{ vars.ALLOWED_NON_DEV_PR_ACTORS }}
|
ALLOWED_ACTORS: ${{ vars.ALLOWED_NON_DEV_PR_ACTORS }}
|
||||||
ACTOR: ${{ github.actor }}
|
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
||||||
BASE_REF: ${{ github.event.pull_request.base.ref }}
|
BASE_REF: ${{ github.event.pull_request.base.ref }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check PR authorization
|
- name: Check PR authorization
|
||||||
@@ -37,11 +38,11 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
normalized=",${ALLOWED_ACTORS},"
|
normalized=",${ALLOWED_ACTORS},"
|
||||||
if [[ "$normalized" == *",${ACTOR},"* ]]; then
|
if [[ "$normalized" == *",${PR_AUTHOR},"* ]]; then
|
||||||
echo "allowed=true" >> "$GITHUB_OUTPUT"
|
echo "allowed=true" >> "$GITHUB_OUTPUT"
|
||||||
else
|
else
|
||||||
echo "allowed=false" >> "$GITHUB_OUTPUT"
|
echo "allowed=false" >> "$GITHUB_OUTPUT"
|
||||||
echo "Skipping builds for unauthorized PR targeting $BASE_REF" >&2
|
echo "Skipping builds for PR by unauthorized author targeting $BASE_REF" >&2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
build:
|
build:
|
||||||
|
|||||||
7
.github/workflows/restrict-non-dev-prs.yml
vendored
7
.github/workflows/restrict-non-dev-prs.yml
vendored
@@ -4,6 +4,7 @@ on:
|
|||||||
pull_request_target:
|
pull_request_target:
|
||||||
types:
|
types:
|
||||||
- opened
|
- opened
|
||||||
|
- edited
|
||||||
- reopened
|
- reopened
|
||||||
- synchronize
|
- synchronize
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
ALLOWED_ACTORS: ${{ vars.ALLOWED_NON_DEV_PR_ACTORS }}
|
ALLOWED_ACTORS: ${{ vars.ALLOWED_NON_DEV_PR_ACTORS }}
|
||||||
ACTOR: ${{ github.actor }}
|
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
||||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
BASE_REF: ${{ github.event.pull_request.base.ref }}
|
BASE_REF: ${{ github.event.pull_request.base.ref }}
|
||||||
steps:
|
steps:
|
||||||
@@ -27,7 +28,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
normalized=",${ALLOWED_ACTORS},"
|
normalized=",${ALLOWED_ACTORS},"
|
||||||
if [[ "$normalized" == *",${ACTOR},"* ]]; then
|
if [[ "$normalized" == *",${PR_AUTHOR},"* ]]; then
|
||||||
echo "authorized=true" >> "$GITHUB_OUTPUT"
|
echo "authorized=true" >> "$GITHUB_OUTPUT"
|
||||||
else
|
else
|
||||||
echo "authorized=false" >> "$GITHUB_OUTPUT"
|
echo "authorized=false" >> "$GITHUB_OUTPUT"
|
||||||
@@ -50,5 +51,5 @@ jobs:
|
|||||||
- name: Fail unauthorized PR
|
- name: Fail unauthorized PR
|
||||||
if: ${{ steps.auth.outputs.authorized != 'true' }}
|
if: ${{ steps.auth.outputs.authorized != 'true' }}
|
||||||
run: |
|
run: |
|
||||||
echo "Actor $ACTOR is not allowed to open PRs targeting $BASE_REF" >&2
|
echo "PR author $PR_AUTHOR is not allowed to open PRs targeting $BASE_REF" >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
171
README.md
171
README.md
@@ -1,128 +1,127 @@
|
|||||||
# CodeNomad
|
# 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>
|
|
||||||
|
|
||||||

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

|
- **🚀 Multi-Instance Workspace**
|
||||||
_Rich media previews for images and assets._
|
- **🌐 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
|
## Getting Started
|
||||||
|
|
||||||
Choose the way that fits your workflow:
|
### 🖥️ Desktop App
|
||||||
|
|
||||||
### 🖥️ Desktop App (Recommended)
|
Available as both Electron and Tauri builds — choose based on your preference.
|
||||||
The best experience. A native application (Electron-based) with global shortcuts, deeper system integration, and a dedicated window.
|
|
||||||
|
|
||||||
- **Download**: Grab the latest installer for macOS, Windows, or Linux from the [Releases Page](https://github.com/shantur/CodeNomad/releases).
|
Download the latest installer for your platform from [Releases](https://github.com/shantur/CodeNomad/releases).
|
||||||
- **Run**: Install and launch like any other app.
|
|
||||||
|
|
||||||
### 🦀 Tauri App (Experimental)
|
| Platform | Formats |
|
||||||
We are also working on a lightweight, high-performance version built with [Tauri](https://tauri.app). It is currently in active development.
|
|----------|---------|
|
||||||
|
| macOS | DMG, ZIP (Universal: Intel + Apple Silicon) |
|
||||||
- **Download**: Experimental builds are available on the [Releases Page](https://github.com/shantur/CodeNomad/releases).
|
| Windows | NSIS Installer, ZIP (x64, ARM64) |
|
||||||
- **Source**: Check out `packages/tauri-app` if you're interested in contributing.
|
| Linux | AppImage, deb, tar.gz (x64, ARM64) |
|
||||||
|
|
||||||
### 💻 CodeNomad Server
|
### 💻 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
|
```bash
|
||||||
npx @neuralnomads/codenomad --launch
|
npx @neuralnomads/codenomad --launch
|
||||||
```
|
```
|
||||||
|
|
||||||
Full server/CLI documentation (flags + env vars, TLS, auth, remote access):
|
See [Server Documentation](packages/server/README.md) for flags, TLS, auth, and remote access.
|
||||||
- [packages/server/README.md](packages/server/README.md)
|
|
||||||
|
|
||||||
To see all available options:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx @neuralnomads/codenomad --help
|
|
||||||
```
|
|
||||||
|
|
||||||
### 🧪 Dev Releases
|
### 🧪 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
|
```bash
|
||||||
npx @neuralnomads/codenomad-dev --launch
|
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
|
## Requirements
|
||||||
|
|
||||||
- **[OpenCode CLI](https://opencode.ai)**: Must be installed and available in your `PATH`.
|
- **[OpenCode CLI](https://opencode.ai)** — must be installed and in your `PATH`
|
||||||
- **Node.js 18+**: Required if running the CLI server or building from source.
|
- **Node.js 18+** — for server mode or building from source
|
||||||
|
|
||||||
## Troubleshooting
|
---
|
||||||
|
|
||||||
### macOS says the app is damaged
|
## Development
|
||||||
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`:
|
|
||||||
|
|
||||||
```bash
|
CodeNomad is a monorepo built with:
|
||||||
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:
|
|
||||||
|
|
||||||
| Package | Description |
|
| 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)** | Core logic & CLI — workspaces, OpenCode proxy, API, auth, speech |
|
||||||
| **[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)** | SolidJS frontend — reactive, fast, beautiful |
|
||||||
| **[packages/ui](packages/ui/README.md)** | The SolidJS-based frontend. Fast, reactive, and 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
|
### Quick Start
|
||||||
To build the Desktop App from source:
|
|
||||||
|
|
||||||
1. Clone the repo.
|
```bash
|
||||||
2. Run `npm install` (requires pnpm or npm 7+ for workspaces).
|
git clone https://github.com/NeuralNomadsAI/CodeNomad.git
|
||||||
3. Run `npm run build --workspace @neuralnomads/codenomad-electron-app`.
|
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",
|
"name": "codenomad-workspace",
|
||||||
"version": "0.13.1",
|
"version": "0.13.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "codenomad-workspace",
|
"name": "codenomad-workspace",
|
||||||
"version": "0.13.1",
|
"version": "0.13.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"7zip-bin": "^5.2.0",
|
"7zip-bin": "^5.2.0",
|
||||||
@@ -64,7 +64,6 @@
|
|||||||
"version": "7.28.5",
|
"version": "7.28.5",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.5",
|
"@babel/generator": "^7.28.5",
|
||||||
@@ -3381,7 +3380,6 @@
|
|||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.20.7",
|
"@babel/parser": "^7.20.7",
|
||||||
"@babel/types": "^7.20.7",
|
"@babel/types": "^7.20.7",
|
||||||
@@ -3483,7 +3481,6 @@
|
|||||||
"version": "22.19.0",
|
"version": "22.19.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
@@ -3558,7 +3555,6 @@
|
|||||||
"integrity": "sha512-MCbrb508JZHqe7bUibmZj/lyojdhLRnfkmyXnkrCM2zVrjTgL89U8UEfInpKTvPeTnxsw2hmyZxnhsdNR6yhwg==",
|
"integrity": "sha512-MCbrb508JZHqe7bUibmZj/lyojdhLRnfkmyXnkrCM2zVrjTgL89U8UEfInpKTvPeTnxsw2hmyZxnhsdNR6yhwg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cac": "^6.7.14",
|
"cac": "^6.7.14",
|
||||||
"colorette": "^2.0.20",
|
"colorette": "^2.0.20",
|
||||||
@@ -3641,7 +3637,6 @@
|
|||||||
"version": "6.12.6",
|
"version": "6.12.6",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
"fast-json-stable-stringify": "^2.0.0",
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
@@ -3844,6 +3839,7 @@
|
|||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"archiver-utils": "^2.1.0",
|
"archiver-utils": "^2.1.0",
|
||||||
"async": "^3.2.4",
|
"async": "^3.2.4",
|
||||||
@@ -3861,6 +3857,7 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"glob": "^7.1.4",
|
"glob": "^7.1.4",
|
||||||
"graceful-fs": "^4.2.0",
|
"graceful-fs": "^4.2.0",
|
||||||
@@ -3881,6 +3878,7 @@
|
|||||||
"version": "2.3.8",
|
"version": "2.3.8",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"core-util-is": "~1.0.0",
|
"core-util-is": "~1.0.0",
|
||||||
"inherits": "~2.0.3",
|
"inherits": "~2.0.3",
|
||||||
@@ -3894,12 +3892,14 @@
|
|||||||
"node_modules/archiver-utils/node_modules/safe-buffer": {
|
"node_modules/archiver-utils/node_modules/safe-buffer": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/archiver-utils/node_modules/string_decoder": {
|
"node_modules/archiver-utils/node_modules/string_decoder": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safe-buffer": "~5.1.0"
|
"safe-buffer": "~5.1.0"
|
||||||
}
|
}
|
||||||
@@ -4213,6 +4213,7 @@
|
|||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"buffer": "^5.5.0",
|
"buffer": "^5.5.0",
|
||||||
"inherits": "^2.0.4",
|
"inherits": "^2.0.4",
|
||||||
@@ -4276,7 +4277,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@@ -4767,6 +4767,7 @@
|
|||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"buffer-crc32": "^0.2.13",
|
"buffer-crc32": "^0.2.13",
|
||||||
"crc32-stream": "^4.0.2",
|
"crc32-stream": "^4.0.2",
|
||||||
@@ -4896,6 +4897,7 @@
|
|||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"crc32": "bin/crc32.njs"
|
"crc32": "bin/crc32.njs"
|
||||||
},
|
},
|
||||||
@@ -4907,6 +4909,7 @@
|
|||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"crc-32": "^1.2.0",
|
"crc-32": "^1.2.0",
|
||||||
"readable-stream": "^3.4.0"
|
"readable-stream": "^3.4.0"
|
||||||
@@ -5272,7 +5275,6 @@
|
|||||||
"version": "24.13.3",
|
"version": "24.13.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"app-builder-lib": "24.13.3",
|
"app-builder-lib": "24.13.3",
|
||||||
"builder-util": "24.13.1",
|
"builder-util": "24.13.1",
|
||||||
@@ -5439,6 +5441,7 @@
|
|||||||
"version": "24.13.3",
|
"version": "24.13.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"app-builder-lib": "24.13.3",
|
"app-builder-lib": "24.13.3",
|
||||||
"archiver": "^5.3.1",
|
"archiver": "^5.3.1",
|
||||||
@@ -5450,6 +5453,7 @@
|
|||||||
"version": "10.1.0",
|
"version": "10.1.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"graceful-fs": "^4.2.0",
|
"graceful-fs": "^4.2.0",
|
||||||
"jsonfile": "^6.0.1",
|
"jsonfile": "^6.0.1",
|
||||||
@@ -5463,6 +5467,7 @@
|
|||||||
"version": "6.2.0",
|
"version": "6.2.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"universalify": "^2.0.0"
|
"universalify": "^2.0.0"
|
||||||
},
|
},
|
||||||
@@ -5474,6 +5479,7 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10.0.0"
|
"node": ">= 10.0.0"
|
||||||
}
|
}
|
||||||
@@ -6191,7 +6197,8 @@
|
|||||||
"node_modules/fs-constants": {
|
"node_modules/fs-constants": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/fs-extra": {
|
"node_modules/fs-extra": {
|
||||||
"version": "8.1.0",
|
"version": "8.1.0",
|
||||||
@@ -7408,7 +7415,8 @@
|
|||||||
"node_modules/isarray": {
|
"node_modules/isarray": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/isbinaryfile": {
|
"node_modules/isbinaryfile": {
|
||||||
"version": "5.0.6",
|
"version": "5.0.6",
|
||||||
@@ -7458,7 +7466,6 @@
|
|||||||
"version": "1.21.7",
|
"version": "1.21.7",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"jiti": "bin/jiti.js"
|
"jiti": "bin/jiti.js"
|
||||||
}
|
}
|
||||||
@@ -7590,6 +7597,7 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"readable-stream": "^2.0.5"
|
"readable-stream": "^2.0.5"
|
||||||
},
|
},
|
||||||
@@ -7601,6 +7609,7 @@
|
|||||||
"version": "2.3.8",
|
"version": "2.3.8",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"core-util-is": "~1.0.0",
|
"core-util-is": "~1.0.0",
|
||||||
"inherits": "~2.0.3",
|
"inherits": "~2.0.3",
|
||||||
@@ -7614,12 +7623,14 @@
|
|||||||
"node_modules/lazystream/node_modules/safe-buffer": {
|
"node_modules/lazystream/node_modules/safe-buffer": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/lazystream/node_modules/string_decoder": {
|
"node_modules/lazystream/node_modules/string_decoder": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safe-buffer": "~5.1.0"
|
"safe-buffer": "~5.1.0"
|
||||||
}
|
}
|
||||||
@@ -7684,22 +7695,26 @@
|
|||||||
"node_modules/lodash.defaults": {
|
"node_modules/lodash.defaults": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/lodash.difference": {
|
"node_modules/lodash.difference": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/lodash.flatten": {
|
"node_modules/lodash.flatten": {
|
||||||
"version": "4.4.0",
|
"version": "4.4.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/lodash.isplainobject": {
|
"node_modules/lodash.isplainobject": {
|
||||||
"version": "4.0.6",
|
"version": "4.0.6",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/lodash.sortby": {
|
"node_modules/lodash.sortby": {
|
||||||
"version": "4.7.0",
|
"version": "4.7.0",
|
||||||
@@ -7711,7 +7726,8 @@
|
|||||||
"node_modules/lodash.union": {
|
"node_modules/lodash.union": {
|
||||||
"version": "4.6.0",
|
"version": "4.6.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/lowercase-keys": {
|
"node_modules/lowercase-keys": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
@@ -8515,7 +8531,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -8663,7 +8678,8 @@
|
|||||||
"node_modules/process-nextick-args": {
|
"node_modules/process-nextick-args": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/process-warning": {
|
"node_modules/process-warning": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
@@ -8912,6 +8928,7 @@
|
|||||||
"version": "3.6.2",
|
"version": "3.6.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"inherits": "^2.0.3",
|
"inherits": "^2.0.3",
|
||||||
"string_decoder": "^1.1.1",
|
"string_decoder": "^1.1.1",
|
||||||
@@ -8925,6 +8942,7 @@
|
|||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"minimatch": "^5.1.0"
|
"minimatch": "^5.1.0"
|
||||||
}
|
}
|
||||||
@@ -9227,7 +9245,6 @@
|
|||||||
"version": "4.52.5",
|
"version": "4.52.5",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "1.0.8"
|
"@types/estree": "1.0.8"
|
||||||
},
|
},
|
||||||
@@ -9451,7 +9468,6 @@
|
|||||||
"node_modules/seroval": {
|
"node_modules/seroval": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
@@ -9775,7 +9791,6 @@
|
|||||||
"node_modules/solid-js": {
|
"node_modules/solid-js": {
|
||||||
"version": "1.9.10",
|
"version": "1.9.10",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.1.0",
|
"csstype": "^3.1.0",
|
||||||
"seroval": "~1.3.0",
|
"seroval": "~1.3.0",
|
||||||
@@ -9916,6 +9931,7 @@
|
|||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safe-buffer": "~5.2.0"
|
"safe-buffer": "~5.2.0"
|
||||||
}
|
}
|
||||||
@@ -10249,6 +10265,7 @@
|
|||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bl": "^4.0.3",
|
"bl": "^4.0.3",
|
||||||
"end-of-stream": "^1.4.1",
|
"end-of-stream": "^1.4.1",
|
||||||
@@ -10441,7 +10458,6 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -10691,7 +10707,6 @@
|
|||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -11039,7 +11054,6 @@
|
|||||||
"version": "5.4.21",
|
"version": "5.4.21",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.21.3",
|
"esbuild": "^0.21.3",
|
||||||
"postcss": "^8.4.43",
|
"postcss": "^8.4.43",
|
||||||
@@ -11524,7 +11538,6 @@
|
|||||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"fast-uri": "^3.0.1",
|
"fast-uri": "^3.0.1",
|
||||||
@@ -11719,7 +11732,6 @@
|
|||||||
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
|
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"rollup": "dist/bin/rollup"
|
"rollup": "dist/bin/rollup"
|
||||||
},
|
},
|
||||||
@@ -12008,6 +12020,7 @@
|
|||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"archiver-utils": "^3.0.4",
|
"archiver-utils": "^3.0.4",
|
||||||
"compress-commons": "^4.1.2",
|
"compress-commons": "^4.1.2",
|
||||||
@@ -12021,6 +12034,7 @@
|
|||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"glob": "^7.2.3",
|
"glob": "^7.2.3",
|
||||||
"graceful-fs": "^4.2.0",
|
"graceful-fs": "^4.2.0",
|
||||||
@@ -12040,7 +12054,6 @@
|
|||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "3.25.76",
|
"version": "3.25.76",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
@@ -12055,7 +12068,7 @@
|
|||||||
},
|
},
|
||||||
"packages/electron-app": {
|
"packages/electron-app": {
|
||||||
"name": "@neuralnomads/codenomad-electron-app",
|
"name": "@neuralnomads/codenomad-electron-app",
|
||||||
"version": "0.13.1",
|
"version": "0.13.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codenomad/ui": "file:../ui",
|
"@codenomad/ui": "file:../ui",
|
||||||
@@ -12092,7 +12105,7 @@
|
|||||||
},
|
},
|
||||||
"packages/server": {
|
"packages/server": {
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.13.1",
|
"version": "0.13.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cors": "^8.5.0",
|
"@fastify/cors": "^8.5.0",
|
||||||
@@ -12134,7 +12147,7 @@
|
|||||||
},
|
},
|
||||||
"packages/tauri-app": {
|
"packages/tauri-app": {
|
||||||
"name": "@codenomad/tauri-app",
|
"name": "@codenomad/tauri-app",
|
||||||
"version": "0.13.1",
|
"version": "0.13.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2.9.4"
|
"@tauri-apps/cli": "^2.9.4"
|
||||||
@@ -12142,7 +12155,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@codenomad/ui",
|
"name": "@codenomad/ui",
|
||||||
"version": "0.13.1",
|
"version": "0.13.3",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@git-diff-view/solid": "^0.0.8",
|
"@git-diff-view/solid": "^0.0.8",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "codenomad-workspace",
|
"name": "codenomad-workspace",
|
||||||
"version": "0.13.1",
|
"version": "0.13.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "CodeNomad monorepo workspace",
|
"description": "CodeNomad monorepo workspace",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"build:mac-x64": "npm run build:mac-x64 --workspace @neuralnomads/codenomad-electron-app",
|
"build:mac-x64": "npm run build:mac-x64 --workspace @neuralnomads/codenomad-electron-app",
|
||||||
"build:binaries": "npm run build:binaries --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",
|
"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": {
|
"dependencies": {
|
||||||
"7zip-bin": "^5.2.0",
|
"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"
|
"latestServerUrl": "https://github.com/NeuralNomadsAI/CodeNomad/releases/latest"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,23 @@ export interface Env {
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
async fetch(request: Request, env: Env): Promise<Response> {
|
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)
|
return env.ASSETS.fetch(request)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -328,7 +328,6 @@ function finalizeCliSwap(url: string) {
|
|||||||
mainWindow.loadURL(url).catch((error) => console.error("[cli] failed to load CLI view:", error))
|
mainWindow.loadURL(url).catch((error) => console.error("[cli] failed to load CLI view:", error))
|
||||||
}
|
}
|
||||||
|
|
||||||
const SESSION_COOKIE_NAME = "codenomad_session"
|
|
||||||
let bootstrapExchangeInFlight = false
|
let bootstrapExchangeInFlight = false
|
||||||
|
|
||||||
function extractCookieValue(setCookieHeader: string | string[] | undefined, name: string): string | null {
|
function extractCookieValue(setCookieHeader: string | string[] | undefined, name: string): string | null {
|
||||||
@@ -351,6 +350,7 @@ function extractCookieValue(setCookieHeader: string | string[] | undefined, name
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function exchangeBootstrapToken(baseUrl: string, token: string): Promise<boolean> {
|
async function exchangeBootstrapToken(baseUrl: string, token: string): Promise<boolean> {
|
||||||
|
const sessionCookieName = cliManager.getAuthCookieName()
|
||||||
const target = new URL("/api/auth/token", baseUrl)
|
const target = new URL("/api/auth/token", baseUrl)
|
||||||
const body = JSON.stringify({ token })
|
const body = JSON.stringify({ token })
|
||||||
|
|
||||||
@@ -381,14 +381,14 @@ async function exchangeBootstrapToken(baseUrl: string, token: string): Promise<b
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const sessionId = extractCookieValue(result.setCookie, SESSION_COOKIE_NAME)
|
const sessionId = extractCookieValue(result.setCookie, sessionCookieName)
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
await session.defaultSession.cookies.set({
|
await session.defaultSession.cookies.set({
|
||||||
url: baseUrl,
|
url: baseUrl,
|
||||||
name: SESSION_COOKIE_NAME,
|
name: sessionCookieName,
|
||||||
value: sessionId,
|
value: sessionId,
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
path: "/",
|
path: "/",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const mainFilename = fileURLToPath(import.meta.url)
|
|||||||
const mainDirname = path.dirname(mainFilename)
|
const mainDirname = path.dirname(mainFilename)
|
||||||
|
|
||||||
const BOOTSTRAP_TOKEN_PREFIX = "CODENOMAD_BOOTSTRAP_TOKEN:"
|
const BOOTSTRAP_TOKEN_PREFIX = "CODENOMAD_BOOTSTRAP_TOKEN:"
|
||||||
|
const SESSION_COOKIE_NAME_PREFIX = "codenomad_session"
|
||||||
|
|
||||||
type CliState = "starting" | "ready" | "error" | "stopped"
|
type CliState = "starting" | "ready" | "error" | "stopped"
|
||||||
type ListeningMode = "local" | "all"
|
type ListeningMode = "local" | "all"
|
||||||
@@ -129,6 +130,7 @@ export class CliProcessManager extends EventEmitter {
|
|||||||
private stdoutBuffer = ""
|
private stdoutBuffer = ""
|
||||||
private stderrBuffer = ""
|
private stderrBuffer = ""
|
||||||
private bootstrapToken: string | null = null
|
private bootstrapToken: string | null = null
|
||||||
|
private authCookieName = `${SESSION_COOKIE_NAME_PREFIX}_${process.pid}_${Date.now()}`
|
||||||
private requestedStop = false
|
private requestedStop = false
|
||||||
|
|
||||||
async start(options: StartOptions): Promise<CliStatus> {
|
async start(options: StartOptions): Promise<CliStatus> {
|
||||||
@@ -139,6 +141,7 @@ export class CliProcessManager extends EventEmitter {
|
|||||||
this.stdoutBuffer = ""
|
this.stdoutBuffer = ""
|
||||||
this.stderrBuffer = ""
|
this.stderrBuffer = ""
|
||||||
this.bootstrapToken = null
|
this.bootstrapToken = null
|
||||||
|
this.authCookieName = `${SESSION_COOKIE_NAME_PREFIX}_${process.pid}_${Date.now()}`
|
||||||
this.requestedStop = false
|
this.requestedStop = false
|
||||||
this.updateStatus({ state: "starting", port: undefined, pid: undefined, url: undefined, error: undefined })
|
this.updateStatus({ state: "starting", port: undefined, pid: undefined, url: undefined, error: undefined })
|
||||||
|
|
||||||
@@ -436,6 +439,10 @@ export class CliProcessManager extends EventEmitter {
|
|||||||
return { ...this.status }
|
return { ...this.status }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAuthCookieName(): string {
|
||||||
|
return this.authCookieName
|
||||||
|
}
|
||||||
|
|
||||||
private resolveListeningMode(): ListeningMode {
|
private resolveListeningMode(): ListeningMode {
|
||||||
return readListeningModeFromConfig()
|
return readListeningModeFromConfig()
|
||||||
}
|
}
|
||||||
@@ -532,7 +539,7 @@ export class CliProcessManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private buildCliArgs(options: StartOptions, host: string): string[] {
|
private buildCliArgs(options: StartOptions, host: string): string[] {
|
||||||
const args = ["serve", "--host", host, "--generate-token"]
|
const args = ["serve", "--host", host, "--generate-token", "--auth-cookie-name", this.authCookieName]
|
||||||
|
|
||||||
if (options.dev) {
|
if (options.dev) {
|
||||||
// Dev: run plain HTTP + Vite dev server proxy.
|
// Dev: run plain HTTP + Vite dev server proxy.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@neuralnomads/codenomad-electron-app",
|
"name": "@neuralnomads/codenomad-electron-app",
|
||||||
"version": "0.13.1",
|
"version": "0.13.3",
|
||||||
"description": "CodeNomad - AI coding assistant",
|
"description": "CodeNomad - AI coding assistant",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": {
|
"author": {
|
||||||
|
|||||||
@@ -4,6 +4,6 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"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",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.13.1",
|
"version": "0.13.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.13.1",
|
"version": "0.13.3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cors": "^8.5.0",
|
"@fastify/cors": "^8.5.0",
|
||||||
"@fastify/reply-from": "^9.8.0",
|
"@fastify/reply-from": "^9.8.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@neuralnomads/codenomad",
|
"name": "@neuralnomads/codenomad",
|
||||||
"version": "0.13.1",
|
"version": "0.13.3",
|
||||||
"description": "CodeNomad Server",
|
"description": "CodeNomad Server",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": {
|
"author": {
|
||||||
|
|||||||
@@ -16,16 +16,18 @@ export interface AuthManagerInit {
|
|||||||
password?: string
|
password?: string
|
||||||
generateToken: boolean
|
generateToken: boolean
|
||||||
dangerouslySkipAuth?: boolean
|
dangerouslySkipAuth?: boolean
|
||||||
|
cookieName?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AuthManager {
|
export class AuthManager {
|
||||||
private readonly authStore: AuthStore | null
|
private readonly authStore: AuthStore | null
|
||||||
private readonly tokenManager: TokenManager | null
|
private readonly tokenManager: TokenManager | null
|
||||||
private readonly sessionManager = new SessionManager()
|
private readonly sessionManager = new SessionManager()
|
||||||
private readonly cookieName = DEFAULT_AUTH_COOKIE_NAME
|
private readonly cookieName: string
|
||||||
private readonly authEnabled: boolean
|
private readonly authEnabled: boolean
|
||||||
|
|
||||||
constructor(private readonly init: AuthManagerInit, private readonly logger: Logger) {
|
constructor(private readonly init: AuthManagerInit, private readonly logger: Logger) {
|
||||||
|
this.cookieName = sanitizeCookieName(init.cookieName)
|
||||||
this.authEnabled = !Boolean(init.dangerouslySkipAuth)
|
this.authEnabled = !Boolean(init.dangerouslySkipAuth)
|
||||||
|
|
||||||
if (!this.authEnabled) {
|
if (!this.authEnabled) {
|
||||||
@@ -139,6 +141,16 @@ export class AuthManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sanitizeCookieName(value: string | undefined): string {
|
||||||
|
const trimmed = value?.trim()
|
||||||
|
if (!trimmed) {
|
||||||
|
return DEFAULT_AUTH_COOKIE_NAME
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitized = trimmed.replace(/[^A-Za-z0-9_-]/g, "_")
|
||||||
|
return sanitized.length > 0 ? sanitized : DEFAULT_AUTH_COOKIE_NAME
|
||||||
|
}
|
||||||
|
|
||||||
function resolveAuthFilePath(configPath: string) {
|
function resolveAuthFilePath(configPath: string) {
|
||||||
const resolvedConfigPath = resolvePath(configPath)
|
const resolvedConfigPath = resolvePath(configPath)
|
||||||
return path.join(path.dirname(resolvedConfigPath), "auth.json")
|
return path.join(path.dirname(resolvedConfigPath), "auth.json")
|
||||||
|
|||||||
128
packages/server/src/clients/connection-manager.ts
Normal file
128
packages/server/src/clients/connection-manager.ts
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import type { Logger } from "../logger"
|
||||||
|
|
||||||
|
const STALE_CONNECTION_TIMEOUT_MS = 45000
|
||||||
|
const STALE_SWEEP_INTERVAL_MS = 5000
|
||||||
|
|
||||||
|
export interface ClientConnectionRef {
|
||||||
|
clientId: string
|
||||||
|
connectionId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClientConnectionRecord extends ClientConnectionRef {
|
||||||
|
key: string
|
||||||
|
connectedAt: number
|
||||||
|
lastSeenAt: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConnectionChangeEvent = {
|
||||||
|
type: "connected" | "disconnected"
|
||||||
|
connection: ClientConnectionRecord
|
||||||
|
reason?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RegisteredConnection extends ClientConnectionRecord {
|
||||||
|
close: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ClientConnectionManager {
|
||||||
|
private readonly connections = new Map<string, RegisteredConnection>()
|
||||||
|
private readonly subscribers = new Set<(event: ConnectionChangeEvent) => void>()
|
||||||
|
private readonly sweepTimer: NodeJS.Timeout
|
||||||
|
|
||||||
|
constructor(private readonly logger: Logger) {
|
||||||
|
this.sweepTimer = setInterval(() => this.sweepStaleConnections(), STALE_SWEEP_INTERVAL_MS)
|
||||||
|
this.sweepTimer.unref?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
shutdown(): void {
|
||||||
|
clearInterval(this.sweepTimer)
|
||||||
|
for (const connection of Array.from(this.connections.values())) {
|
||||||
|
this.disconnect(connection.key, "shutdown", false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(listener: (event: ConnectionChangeEvent) => void): () => void {
|
||||||
|
this.subscribers.add(listener)
|
||||||
|
return () => this.subscribers.delete(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
register(input: ClientConnectionRef & { close: () => void }): () => void {
|
||||||
|
const key = getConnectionKey(input)
|
||||||
|
const now = Date.now()
|
||||||
|
const existing = this.connections.get(key)
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
this.logger.debug({ clientId: input.clientId, connectionId: input.connectionId }, "Replacing existing client connection")
|
||||||
|
this.disconnect(key, "replaced")
|
||||||
|
}
|
||||||
|
|
||||||
|
const connection: RegisteredConnection = {
|
||||||
|
key,
|
||||||
|
clientId: input.clientId,
|
||||||
|
connectionId: input.connectionId,
|
||||||
|
connectedAt: now,
|
||||||
|
lastSeenAt: now,
|
||||||
|
close: input.close,
|
||||||
|
}
|
||||||
|
this.connections.set(key, connection)
|
||||||
|
this.logger.debug({ clientId: input.clientId, connectionId: input.connectionId }, "Client connected")
|
||||||
|
this.notify({ type: "connected", connection })
|
||||||
|
return () => this.disconnect(key, "closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
pong(input: ClientConnectionRef): boolean {
|
||||||
|
const key = getConnectionKey(input)
|
||||||
|
const connection = this.connections.get(key)
|
||||||
|
if (!connection) {
|
||||||
|
this.logger.debug({ clientId: input.clientId, connectionId: input.connectionId }, "Ignoring pong for unknown client connection")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.lastSeenAt = Date.now()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
isConnected(input: ClientConnectionRef): boolean {
|
||||||
|
return this.connections.has(getConnectionKey(input))
|
||||||
|
}
|
||||||
|
|
||||||
|
private sweepStaleConnections(): void {
|
||||||
|
const cutoff = Date.now() - STALE_CONNECTION_TIMEOUT_MS
|
||||||
|
for (const connection of Array.from(this.connections.values())) {
|
||||||
|
if (connection.lastSeenAt > cutoff) continue
|
||||||
|
this.logger.debug({ clientId: connection.clientId, connectionId: connection.connectionId }, "Client connection timed out")
|
||||||
|
this.disconnect(connection.key, "timeout")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private disconnect(key: string, reason: string, invokeClose = true): void {
|
||||||
|
const connection = this.connections.get(key)
|
||||||
|
if (!connection) return
|
||||||
|
this.connections.delete(key)
|
||||||
|
this.logger.debug({ clientId: connection.clientId, connectionId: connection.connectionId, reason }, "Client disconnected")
|
||||||
|
|
||||||
|
if (invokeClose) {
|
||||||
|
try {
|
||||||
|
connection.close()
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn({ err: error, clientId: connection.clientId, connectionId: connection.connectionId }, "Failed to close stale client connection")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.notify({ type: "disconnected", connection, reason })
|
||||||
|
}
|
||||||
|
|
||||||
|
private notify(event: ConnectionChangeEvent): void {
|
||||||
|
for (const subscriber of this.subscribers) {
|
||||||
|
try {
|
||||||
|
subscriber(event)
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn({ err: error, eventType: event.type }, "Client connection subscriber failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConnectionKey(input: ClientConnectionRef): string {
|
||||||
|
return `${input.clientId}:${input.connectionId}`
|
||||||
|
}
|
||||||
@@ -19,9 +19,9 @@ import { InstanceEventBridge } from "./workspaces/instance-events"
|
|||||||
import { createLogger } from "./logger"
|
import { createLogger } from "./logger"
|
||||||
import { launchInBrowser } from "./launcher"
|
import { launchInBrowser } from "./launcher"
|
||||||
import { resolveUi } from "./ui/remote-ui"
|
import { resolveUi } from "./ui/remote-ui"
|
||||||
import { AuthManager, BOOTSTRAP_TOKEN_STDOUT_PREFIX, DEFAULT_AUTH_USERNAME } from "./auth/manager"
|
import { AuthManager, BOOTSTRAP_TOKEN_STDOUT_PREFIX, DEFAULT_AUTH_COOKIE_NAME, DEFAULT_AUTH_USERNAME } from "./auth/manager"
|
||||||
import { resolveHttpsOptions } from "./server/tls"
|
import { resolveHttpsOptions } from "./server/tls"
|
||||||
import { resolveNetworkAddresses } from "./server/network-addresses"
|
import { resolveNetworkAddresses, resolveRemoteAddresses } from "./server/network-addresses"
|
||||||
import { startDevReleaseMonitor } from "./releases/dev-release-monitor"
|
import { startDevReleaseMonitor } from "./releases/dev-release-monitor"
|
||||||
import { SpeechService } from "./speech/service"
|
import { SpeechService } from "./speech/service"
|
||||||
|
|
||||||
@@ -55,6 +55,7 @@ interface CliOptions {
|
|||||||
launch: boolean
|
launch: boolean
|
||||||
authUsername: string
|
authUsername: string
|
||||||
authPassword?: string
|
authPassword?: string
|
||||||
|
authCookieName: string
|
||||||
generateToken: boolean
|
generateToken: boolean
|
||||||
dangerouslySkipAuth: boolean
|
dangerouslySkipAuth: boolean
|
||||||
}
|
}
|
||||||
@@ -100,6 +101,11 @@ function parseCliOptions(argv: string[]): CliOptions {
|
|||||||
.default(DEFAULT_AUTH_USERNAME),
|
.default(DEFAULT_AUTH_USERNAME),
|
||||||
)
|
)
|
||||||
.addOption(new Option("--password <password>", "Password for server authentication").env("CODENOMAD_SERVER_PASSWORD"))
|
.addOption(new Option("--password <password>", "Password for server authentication").env("CODENOMAD_SERVER_PASSWORD"))
|
||||||
|
.addOption(
|
||||||
|
new Option("--auth-cookie-name <name>", "Cookie name for server authentication")
|
||||||
|
.env("CODENOMAD_AUTH_COOKIE_NAME")
|
||||||
|
.default(DEFAULT_AUTH_COOKIE_NAME),
|
||||||
|
)
|
||||||
.addOption(
|
.addOption(
|
||||||
new Option("--generate-token", "Emit a one-time bootstrap token for desktop")
|
new Option("--generate-token", "Emit a one-time bootstrap token for desktop")
|
||||||
.env("CODENOMAD_GENERATE_TOKEN")
|
.env("CODENOMAD_GENERATE_TOKEN")
|
||||||
@@ -139,6 +145,7 @@ function parseCliOptions(argv: string[]): CliOptions {
|
|||||||
launch?: boolean
|
launch?: boolean
|
||||||
username: string
|
username: string
|
||||||
password?: string
|
password?: string
|
||||||
|
authCookieName: string
|
||||||
generateToken?: boolean
|
generateToken?: boolean
|
||||||
dangerouslySkipAuth?: boolean
|
dangerouslySkipAuth?: boolean
|
||||||
}>()
|
}>()
|
||||||
@@ -185,6 +192,7 @@ function parseCliOptions(argv: string[]): CliOptions {
|
|||||||
launch: Boolean(parsed.launch),
|
launch: Boolean(parsed.launch),
|
||||||
authUsername: parsed.username,
|
authUsername: parsed.username,
|
||||||
authPassword: parsed.password,
|
authPassword: parsed.password,
|
||||||
|
authCookieName: parsed.authCookieName,
|
||||||
generateToken: Boolean(parsed.generateToken),
|
generateToken: Boolean(parsed.generateToken),
|
||||||
dangerouslySkipAuth: Boolean(parsed.dangerouslySkipAuth),
|
dangerouslySkipAuth: Boolean(parsed.dangerouslySkipAuth),
|
||||||
}
|
}
|
||||||
@@ -266,6 +274,7 @@ async function main() {
|
|||||||
configPath: configLocation.configYamlPath,
|
configPath: configLocation.configYamlPath,
|
||||||
username: options.authUsername,
|
username: options.authUsername,
|
||||||
password: options.authPassword,
|
password: options.authPassword,
|
||||||
|
cookieName: options.authCookieName,
|
||||||
generateToken: options.generateToken,
|
generateToken: options.generateToken,
|
||||||
dangerouslySkipAuth: options.dangerouslySkipAuth,
|
dangerouslySkipAuth: options.dangerouslySkipAuth,
|
||||||
},
|
},
|
||||||
@@ -442,18 +451,22 @@ async function main() {
|
|||||||
// which can lead clients to talk to the wrong process.
|
// which can lead clients to talk to the wrong process.
|
||||||
const localUrl = `${localProtocol}://127.0.0.1:${localStart.port}`
|
const localUrl = `${localProtocol}://127.0.0.1:${localStart.port}`
|
||||||
let remoteUrl: string | undefined
|
let remoteUrl: string | undefined
|
||||||
|
let remoteAddresses = [] as ReturnType<typeof resolveNetworkAddresses>
|
||||||
if (remoteStart) {
|
if (remoteStart) {
|
||||||
const wantsAll = options.host === "0.0.0.0" || !isLoopbackHost(options.host)
|
const wantsAll = options.host === "0.0.0.0" || !isLoopbackHost(options.host)
|
||||||
let remoteHost = options.host
|
let remoteHost = options.host
|
||||||
if (wantsAll) {
|
if (wantsAll) {
|
||||||
if (options.host === "0.0.0.0") {
|
if (options.host === "0.0.0.0") {
|
||||||
const candidates = resolveNetworkAddresses({ host: options.host, protocol: remoteProtocol, port: remoteStart.port })
|
const resolved = resolveRemoteAddresses({ host: options.host, protocol: remoteProtocol, port: remoteStart.port })
|
||||||
remoteHost = candidates.find((addr) => addr.scope === "external")?.ip ?? "localhost"
|
remoteAddresses = resolved.userVisible
|
||||||
|
remoteUrl = resolved.primaryRemoteUrl ?? `${remoteProtocol}://localhost:${remoteStart.port}`
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
remoteHost = "localhost"
|
remoteHost = "localhost"
|
||||||
}
|
}
|
||||||
remoteUrl = `${remoteProtocol}://${remoteHost}:${remoteStart.port}`
|
if (!remoteUrl) {
|
||||||
|
remoteUrl = `${remoteProtocol}://${remoteHost}:${remoteStart.port}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
serverMeta.localUrl = localUrl
|
serverMeta.localUrl = localUrl
|
||||||
@@ -464,7 +477,9 @@ async function main() {
|
|||||||
serverMeta.listeningMode = options.host === "0.0.0.0" || !isLoopbackHost(options.host) ? "all" : "local"
|
serverMeta.listeningMode = options.host === "0.0.0.0" || !isLoopbackHost(options.host) ? "all" : "local"
|
||||||
|
|
||||||
if (serverMeta.remotePort && remoteUrl) {
|
if (serverMeta.remotePort && remoteUrl) {
|
||||||
serverMeta.addresses = resolveNetworkAddresses({ host: options.host, protocol: remoteProtocol, port: serverMeta.remotePort })
|
serverMeta.addresses = remoteAddresses.length
|
||||||
|
? remoteAddresses
|
||||||
|
: resolveNetworkAddresses({ host: options.host, protocol: remoteProtocol, port: serverMeta.remotePort })
|
||||||
} else {
|
} else {
|
||||||
serverMeta.addresses = []
|
serverMeta.addresses = []
|
||||||
}
|
}
|
||||||
@@ -472,6 +487,16 @@ async function main() {
|
|||||||
console.log(`Local Connection URL : ${serverMeta.localUrl}`)
|
console.log(`Local Connection URL : ${serverMeta.localUrl}`)
|
||||||
if (serverMeta.remoteUrl) {
|
if (serverMeta.remoteUrl) {
|
||||||
console.log(`Remote Connection URL : ${serverMeta.remoteUrl}`)
|
console.log(`Remote Connection URL : ${serverMeta.remoteUrl}`)
|
||||||
|
const additionalRemoteUrls = serverMeta.addresses
|
||||||
|
.map((addr) => addr.remoteUrl)
|
||||||
|
.filter((url) => url !== serverMeta.remoteUrl)
|
||||||
|
|
||||||
|
if (additionalRemoteUrls.length > 0) {
|
||||||
|
console.log("Other Accessible URLs:")
|
||||||
|
for (const url of additionalRemoteUrls) {
|
||||||
|
console.log(` - ${url}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.launch) {
|
if (options.launch) {
|
||||||
|
|||||||
96
packages/server/src/plugins/voice-mode.ts
Normal file
96
packages/server/src/plugins/voice-mode.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import type { Logger } from "../logger"
|
||||||
|
import type { ClientConnectionManager, ClientConnectionRef } from "../clients/connection-manager"
|
||||||
|
import type { PluginChannelManager } from "./channel"
|
||||||
|
|
||||||
|
interface VoiceModeManagerOptions {
|
||||||
|
connections: ClientConnectionManager
|
||||||
|
channel: PluginChannelManager
|
||||||
|
logger: Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
export class VoiceModeManager {
|
||||||
|
private readonly enabledConnectionsByInstance = new Map<string, Set<string>>()
|
||||||
|
private readonly aggregateByInstance = new Map<string, boolean>()
|
||||||
|
|
||||||
|
constructor(private readonly options: VoiceModeManagerOptions) {
|
||||||
|
this.options.connections.subscribe((event) => {
|
||||||
|
if (event.type !== "disconnected") return
|
||||||
|
this.clearConnection(event.connection)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setEnabled(instanceId: string, connection: ClientConnectionRef, enabled: boolean): void {
|
||||||
|
if (enabled && !this.options.connections.isConnected(connection)) {
|
||||||
|
this.options.logger.debug(
|
||||||
|
{ instanceId, clientId: connection.clientId, connectionId: connection.connectionId },
|
||||||
|
"Ignoring voice mode enable for disconnected client connection",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = getConnectionKey(connection)
|
||||||
|
const current = this.enabledConnectionsByInstance.get(instanceId) ?? new Set<string>()
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
current.add(key)
|
||||||
|
this.enabledConnectionsByInstance.set(instanceId, current)
|
||||||
|
} else if (current.delete(key)) {
|
||||||
|
if (current.size === 0) {
|
||||||
|
this.enabledConnectionsByInstance.delete(instanceId)
|
||||||
|
} else {
|
||||||
|
this.enabledConnectionsByInstance.set(instanceId, current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.options.logger.debug({ instanceId, clientId: connection.clientId, connectionId: connection.connectionId, enabled }, "Voice mode updated for client connection")
|
||||||
|
this.publishIfChanged(instanceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
syncInstance(instanceId: string): void {
|
||||||
|
this.options.channel.send(instanceId, buildVoiceModeEvent(this.isEnabled(instanceId)))
|
||||||
|
}
|
||||||
|
|
||||||
|
isEnabled(instanceId: string): boolean {
|
||||||
|
return this.aggregateByInstance.get(instanceId) === true
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearConnection(connection: ClientConnectionRef): void {
|
||||||
|
const key = getConnectionKey(connection)
|
||||||
|
for (const [instanceId, enabledConnections] of Array.from(this.enabledConnectionsByInstance.entries())) {
|
||||||
|
if (!enabledConnections.delete(key)) continue
|
||||||
|
if (enabledConnections.size === 0) {
|
||||||
|
this.enabledConnectionsByInstance.delete(instanceId)
|
||||||
|
}
|
||||||
|
this.publishIfChanged(instanceId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private publishIfChanged(instanceId: string): void {
|
||||||
|
const enabled = (this.enabledConnectionsByInstance.get(instanceId)?.size ?? 0) > 0
|
||||||
|
const previous = this.aggregateByInstance.get(instanceId) === true
|
||||||
|
if (enabled === previous) return
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
this.aggregateByInstance.set(instanceId, true)
|
||||||
|
} else {
|
||||||
|
this.aggregateByInstance.delete(instanceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.options.logger.debug({ instanceId, enabled }, "Broadcasting aggregate voice mode")
|
||||||
|
this.options.channel.send(instanceId, buildVoiceModeEvent(enabled))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildVoiceModeEvent(enabled: boolean) {
|
||||||
|
return {
|
||||||
|
type: "codenomad.voiceMode",
|
||||||
|
properties: {
|
||||||
|
enabled,
|
||||||
|
formatVersion: "v1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConnectionKey(connection: ClientConnectionRef): string {
|
||||||
|
return `${connection.clientId}:${connection.connectionId}`
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import assert from "node:assert/strict"
|
||||||
|
import os from "node:os"
|
||||||
|
import { describe, it } from "node:test"
|
||||||
|
|
||||||
|
import { resolveNetworkAddresses, resolveRemoteAddresses } from "../network-addresses"
|
||||||
|
|
||||||
|
describe("resolveNetworkAddresses", () => {
|
||||||
|
it("preserves interface order among external addresses", () => {
|
||||||
|
const addresses = [
|
||||||
|
{ address: "172.24.0.1", family: "IPv4", internal: false },
|
||||||
|
{ address: "192.168.1.128", family: "IPv4", internal: false },
|
||||||
|
{ address: "10.0.0.8", family: 4, internal: false },
|
||||||
|
{ address: "127.0.0.1", family: "IPv4", internal: true },
|
||||||
|
{ address: "169.254.10.20", family: "IPv4", internal: false },
|
||||||
|
]
|
||||||
|
|
||||||
|
usingMockedNetworkInterfaces(addresses, () => {
|
||||||
|
const result = resolveNetworkAddresses({ host: "0.0.0.0", protocol: "https", port: 9898 })
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
result.map((entry) => entry.ip),
|
||||||
|
["172.24.0.1", "192.168.1.128", "10.0.0.8", "169.254.10.20", "127.0.0.1"],
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("resolveRemoteAddresses", () => {
|
||||||
|
it("keeps all external addresses user-visible while preferring non-link-local addresses for the primary URL", () => {
|
||||||
|
const addresses = [
|
||||||
|
{ address: "169.254.10.20", family: "IPv4", internal: false },
|
||||||
|
{ address: "192.168.1.128", family: "IPv4", internal: false },
|
||||||
|
{ address: "172.24.0.1", family: "IPv4", internal: false },
|
||||||
|
]
|
||||||
|
|
||||||
|
usingMockedNetworkInterfaces(addresses, () => {
|
||||||
|
const result = resolveRemoteAddresses({ host: "0.0.0.0", protocol: "https", port: 9898 })
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
result.userVisible.map((entry) => entry.ip),
|
||||||
|
["192.168.1.128", "172.24.0.1", "169.254.10.20"],
|
||||||
|
)
|
||||||
|
assert.equal(result.primaryRemoteUrl, "https://192.168.1.128:9898")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("prefers private LAN addresses over public addresses", () => {
|
||||||
|
const addresses = [
|
||||||
|
{ address: "203.0.113.40", family: "IPv4", internal: false },
|
||||||
|
{ address: "192.168.1.128", family: "IPv4", internal: false },
|
||||||
|
{ address: "8.8.8.8", family: "IPv4", internal: false },
|
||||||
|
]
|
||||||
|
|
||||||
|
usingMockedNetworkInterfaces(addresses, () => {
|
||||||
|
const result = resolveRemoteAddresses({ host: "0.0.0.0", protocol: "https", port: 9898 })
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
result.userVisible.map((entry) => entry.ip),
|
||||||
|
["192.168.1.128", "203.0.113.40", "8.8.8.8"],
|
||||||
|
)
|
||||||
|
assert.equal(result.primaryRemoteUrl, "https://192.168.1.128:9898")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("uses a public address when no private LAN address is available", () => {
|
||||||
|
const addresses = [
|
||||||
|
{ address: "169.254.10.20", family: "IPv4", internal: false },
|
||||||
|
{ address: "203.0.113.40", family: "IPv4", internal: false },
|
||||||
|
]
|
||||||
|
|
||||||
|
usingMockedNetworkInterfaces(addresses, () => {
|
||||||
|
const result = resolveRemoteAddresses({ host: "0.0.0.0", protocol: "https", port: 9898 })
|
||||||
|
|
||||||
|
assert.deepEqual(result.userVisible.map((entry) => entry.ip), ["203.0.113.40", "169.254.10.20"])
|
||||||
|
assert.equal(result.primaryRemoteUrl, "https://203.0.113.40:9898")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function usingMockedNetworkInterfaces(
|
||||||
|
addresses: Array<{ address: string; family: string | number; internal: boolean }>,
|
||||||
|
callback: () => void,
|
||||||
|
) {
|
||||||
|
const original = os.networkInterfaces
|
||||||
|
os.networkInterfaces = (() => ({
|
||||||
|
ethernet0: addresses as unknown as ReturnType<typeof os.networkInterfaces>[string],
|
||||||
|
})) as typeof os.networkInterfaces
|
||||||
|
|
||||||
|
try {
|
||||||
|
callback()
|
||||||
|
} finally {
|
||||||
|
os.networkInterfaces = original
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,7 +29,9 @@ import type { AuthManager } from "../auth/manager"
|
|||||||
import { registerAuthRoutes } from "./routes/auth"
|
import { registerAuthRoutes } from "./routes/auth"
|
||||||
import { sendUnauthorized, wantsHtml } from "../auth/http-auth"
|
import { sendUnauthorized, wantsHtml } from "../auth/http-auth"
|
||||||
import type { SpeechService } from "../speech/service"
|
import type { SpeechService } from "../speech/service"
|
||||||
|
import { ClientConnectionManager } from "../clients/connection-manager"
|
||||||
import { PluginChannelManager } from "../plugins/channel"
|
import { PluginChannelManager } from "../plugins/channel"
|
||||||
|
import { VoiceModeManager } from "../plugins/voice-mode"
|
||||||
|
|
||||||
interface HttpServerDeps {
|
interface HttpServerDeps {
|
||||||
bindHost: string
|
bindHost: string
|
||||||
@@ -174,7 +176,13 @@ export function createHttpServer(deps: HttpServerDeps) {
|
|||||||
eventBus: deps.eventBus,
|
eventBus: deps.eventBus,
|
||||||
logger: deps.logger.child({ component: "background-processes" }),
|
logger: deps.logger.child({ component: "background-processes" }),
|
||||||
})
|
})
|
||||||
|
const clientConnectionManager = new ClientConnectionManager(deps.logger.child({ component: "client-connections" }))
|
||||||
const pluginChannel = new PluginChannelManager(deps.logger.child({ component: "plugin-channel" }))
|
const pluginChannel = new PluginChannelManager(deps.logger.child({ component: "plugin-channel" }))
|
||||||
|
const voiceModeManager = new VoiceModeManager({
|
||||||
|
connections: clientConnectionManager,
|
||||||
|
channel: pluginChannel,
|
||||||
|
logger: deps.logger.child({ component: "voice-mode" }),
|
||||||
|
})
|
||||||
|
|
||||||
registerAuthRoutes(app, { authManager: deps.authManager })
|
registerAuthRoutes(app, { authManager: deps.authManager })
|
||||||
|
|
||||||
@@ -250,7 +258,12 @@ export function createHttpServer(deps: HttpServerDeps) {
|
|||||||
registerSettingsRoutes(app, { settings: deps.settings, logger: apiLogger })
|
registerSettingsRoutes(app, { settings: deps.settings, logger: apiLogger })
|
||||||
registerFilesystemRoutes(app, { fileSystemBrowser: deps.fileSystemBrowser })
|
registerFilesystemRoutes(app, { fileSystemBrowser: deps.fileSystemBrowser })
|
||||||
registerMetaRoutes(app, { serverMeta: deps.serverMeta })
|
registerMetaRoutes(app, { serverMeta: deps.serverMeta })
|
||||||
registerEventRoutes(app, { eventBus: deps.eventBus, registerClient: registerSseClient, logger: sseLogger })
|
registerEventRoutes(app, {
|
||||||
|
eventBus: deps.eventBus,
|
||||||
|
registerClient: registerSseClient,
|
||||||
|
logger: sseLogger,
|
||||||
|
connectionManager: clientConnectionManager,
|
||||||
|
})
|
||||||
registerWorktreeRoutes(app, { workspaceManager: deps.workspaceManager })
|
registerWorktreeRoutes(app, { workspaceManager: deps.workspaceManager })
|
||||||
registerStorageRoutes(app, {
|
registerStorageRoutes(app, {
|
||||||
instanceStore: deps.instanceStore,
|
instanceStore: deps.instanceStore,
|
||||||
@@ -263,6 +276,7 @@ export function createHttpServer(deps: HttpServerDeps) {
|
|||||||
eventBus: deps.eventBus,
|
eventBus: deps.eventBus,
|
||||||
logger: proxyLogger,
|
logger: proxyLogger,
|
||||||
channel: pluginChannel,
|
channel: pluginChannel,
|
||||||
|
voiceModeManager,
|
||||||
})
|
})
|
||||||
registerBackgroundProcessRoutes(app, { backgroundProcessManager })
|
registerBackgroundProcessRoutes(app, { backgroundProcessManager })
|
||||||
registerInstanceProxyRoutes(app, { workspaceManager: deps.workspaceManager, logger: proxyLogger })
|
registerInstanceProxyRoutes(app, { workspaceManager: deps.workspaceManager, logger: proxyLogger })
|
||||||
@@ -328,6 +342,7 @@ export function createHttpServer(deps: HttpServerDeps) {
|
|||||||
},
|
},
|
||||||
stop: () => {
|
stop: () => {
|
||||||
closeSseClients()
|
closeSseClients()
|
||||||
|
clientConnectionManager.shutdown()
|
||||||
return app.close()
|
return app.close()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import os from "os"
|
import os from "os"
|
||||||
import type { NetworkAddress } from "../api-types"
|
import type { NetworkAddress } from "../api-types"
|
||||||
|
|
||||||
|
export interface ResolvedRemoteAddresses {
|
||||||
|
all: NetworkAddress[]
|
||||||
|
userVisible: NetworkAddress[]
|
||||||
|
primaryRemoteUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveNetworkAddresses(args: {
|
export function resolveNetworkAddresses(args: {
|
||||||
host: string
|
host: string
|
||||||
protocol: "http" | "https"
|
protocol: "http" | "https"
|
||||||
@@ -58,10 +64,57 @@ export function resolveNetworkAddresses(args: {
|
|||||||
return results.sort((a, b) => {
|
return results.sort((a, b) => {
|
||||||
const scopeDelta = scopeWeight[a.scope] - scopeWeight[b.scope]
|
const scopeDelta = scopeWeight[a.scope] - scopeWeight[b.scope]
|
||||||
if (scopeDelta !== 0) return scopeDelta
|
if (scopeDelta !== 0) return scopeDelta
|
||||||
return a.ip.localeCompare(b.ip)
|
|
||||||
|
return 0
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveRemoteAddresses(args: {
|
||||||
|
host: string
|
||||||
|
protocol: "http" | "https"
|
||||||
|
port: number
|
||||||
|
}): ResolvedRemoteAddresses {
|
||||||
|
const all = resolveNetworkAddresses(args)
|
||||||
|
const userVisible = sortUserVisibleAddresses(all.filter((address) => address.scope === "external"))
|
||||||
|
return {
|
||||||
|
all,
|
||||||
|
userVisible,
|
||||||
|
primaryRemoteUrl: userVisible[0]?.remoteUrl,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortUserVisibleAddresses(addresses: NetworkAddress[]): NetworkAddress[] {
|
||||||
|
return [...addresses].sort((left, right) => getUserVisiblePriority(left.ip) - getUserVisiblePriority(right.ip))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUserVisiblePriority(ip: string): number {
|
||||||
|
if (isPrivateIPv4(ip)) return 0
|
||||||
|
if (isLinkLocalIPv4(ip)) return 2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLinkLocalIPv4(ip: string): boolean {
|
||||||
|
const octets = parseIPv4(ip)
|
||||||
|
if (!octets) return false
|
||||||
|
const [first, second] = octets
|
||||||
|
return first === 169 && second === 254
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPrivateIPv4(ip: string): boolean {
|
||||||
|
const octets = parseIPv4(ip)
|
||||||
|
if (!octets) return false
|
||||||
|
const [first, second] = octets
|
||||||
|
|
||||||
|
if (first === 10) return true
|
||||||
|
if (first === 192 && second === 168) return true
|
||||||
|
return first === 172 && second >= 16 && second <= 31
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseIPv4(value: string): number[] | null {
|
||||||
|
if (!isIPv4Address(value)) return null
|
||||||
|
return value.split(".").map((part) => Number(part))
|
||||||
|
}
|
||||||
|
|
||||||
function isIPv4Address(value: string | undefined): value is string {
|
function isIPv4Address(value: string | undefined): value is string {
|
||||||
if (!value) return false
|
if (!value) return false
|
||||||
const parts = value.split(".")
|
const parts = value.split(".")
|
||||||
|
|||||||
@@ -1,19 +1,32 @@
|
|||||||
import { FastifyInstance } from "fastify"
|
import { FastifyInstance } from "fastify"
|
||||||
|
import { z } from "zod"
|
||||||
import { EventBus } from "../../events/bus"
|
import { EventBus } from "../../events/bus"
|
||||||
import { WorkspaceEventPayload } from "../../api-types"
|
import { WorkspaceEventPayload } from "../../api-types"
|
||||||
|
import type { ClientConnectionManager } from "../../clients/connection-manager"
|
||||||
import { Logger } from "../../logger"
|
import { Logger } from "../../logger"
|
||||||
|
|
||||||
interface RouteDeps {
|
interface RouteDeps {
|
||||||
eventBus: EventBus
|
eventBus: EventBus
|
||||||
registerClient: (cleanup: () => void) => () => void
|
registerClient: (cleanup: () => void) => () => void
|
||||||
logger: Logger
|
logger: Logger
|
||||||
|
connectionManager: ClientConnectionManager
|
||||||
}
|
}
|
||||||
|
|
||||||
let nextClientId = 0
|
let nextClientId = 0
|
||||||
|
|
||||||
|
const ConnectionQuerySchema = z.object({
|
||||||
|
clientId: z.string().trim().min(1),
|
||||||
|
connectionId: z.string().trim().min(1),
|
||||||
|
})
|
||||||
|
|
||||||
|
const PongBodySchema = ConnectionQuerySchema.extend({
|
||||||
|
pingTs: z.number().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
export function registerEventRoutes(app: FastifyInstance, deps: RouteDeps) {
|
export function registerEventRoutes(app: FastifyInstance, deps: RouteDeps) {
|
||||||
app.get("/api/events", (request, reply) => {
|
app.get("/api/events", (request, reply) => {
|
||||||
const clientId = ++nextClientId
|
const clientId = ++nextClientId
|
||||||
|
const connection = ConnectionQuerySchema.parse(request.query ?? {})
|
||||||
deps.logger.debug({ clientId }, "SSE client connected")
|
deps.logger.debug({ clientId }, "SSE client connected")
|
||||||
|
|
||||||
const origin = request.headers.origin ?? "*"
|
const origin = request.headers.origin ?? "*"
|
||||||
@@ -35,7 +48,8 @@ export function registerEventRoutes(app: FastifyInstance, deps: RouteDeps) {
|
|||||||
|
|
||||||
const unsubscribe = deps.eventBus.onEvent(send)
|
const unsubscribe = deps.eventBus.onEvent(send)
|
||||||
const heartbeat = setInterval(() => {
|
const heartbeat = setInterval(() => {
|
||||||
reply.raw.write(`:hb ${Date.now()}\n\n`)
|
const ping = { ts: Date.now() }
|
||||||
|
reply.raw.write(`event: codenomad.client.ping\ndata: ${JSON.stringify(ping)}\n\n`)
|
||||||
}, 15000)
|
}, 15000)
|
||||||
|
|
||||||
let closed = false
|
let closed = false
|
||||||
@@ -49,13 +63,27 @@ export function registerEventRoutes(app: FastifyInstance, deps: RouteDeps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const unregister = deps.registerClient(close)
|
const unregister = deps.registerClient(close)
|
||||||
|
const unregisterConnection = deps.connectionManager.register({
|
||||||
|
...connection,
|
||||||
|
close,
|
||||||
|
})
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
close()
|
close()
|
||||||
unregister()
|
unregister()
|
||||||
|
unregisterConnection()
|
||||||
}
|
}
|
||||||
|
|
||||||
request.raw.on("close", handleClose)
|
request.raw.on("close", handleClose)
|
||||||
request.raw.on("error", handleClose)
|
request.raw.on("error", handleClose)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
app.post("/api/client-connections/pong", (request, reply) => {
|
||||||
|
const body = PongBodySchema.parse(request.body ?? {})
|
||||||
|
if (!deps.connectionManager.pong(body)) {
|
||||||
|
reply.code(404).send({ error: "Client connection not found" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
reply.code(204).send()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { FastifyInstance } from "fastify"
|
import { FastifyInstance } from "fastify"
|
||||||
import { ServerMeta } from "../../api-types"
|
import { ServerMeta } from "../../api-types"
|
||||||
import { resolveNetworkAddresses } from "../network-addresses"
|
|
||||||
|
|
||||||
interface RouteDeps {
|
interface RouteDeps {
|
||||||
serverMeta: ServerMeta
|
serverMeta: ServerMeta
|
||||||
@@ -13,14 +13,12 @@ export function registerMetaRoutes(app: FastifyInstance, deps: RouteDeps) {
|
|||||||
function buildMetaResponse(meta: ServerMeta): ServerMeta {
|
function buildMetaResponse(meta: ServerMeta): ServerMeta {
|
||||||
const localPort = resolveLocalPort(meta)
|
const localPort = resolveLocalPort(meta)
|
||||||
const remote = resolveRemote(meta)
|
const remote = resolveRemote(meta)
|
||||||
const addresses = remote && remote.port > 0 ? resolveNetworkAddresses({ host: meta.host, protocol: remote.protocol, port: remote.port }) : []
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...meta,
|
...meta,
|
||||||
localPort,
|
localPort,
|
||||||
remotePort: remote?.port,
|
remotePort: remote?.port,
|
||||||
listeningMode: meta.host === "0.0.0.0" || !isLoopbackHost(meta.host) ? "all" : "local",
|
listeningMode: meta.host === "0.0.0.0" || !isLoopbackHost(meta.host) ? "all" : "local",
|
||||||
addresses,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ import type { EventBus } from "../../events/bus"
|
|||||||
import type { Logger } from "../../logger"
|
import type { Logger } from "../../logger"
|
||||||
import { PluginChannelManager } from "../../plugins/channel"
|
import { PluginChannelManager } from "../../plugins/channel"
|
||||||
import { buildPingEvent, handlePluginEvent } from "../../plugins/handlers"
|
import { buildPingEvent, handlePluginEvent } from "../../plugins/handlers"
|
||||||
|
import { VoiceModeManager } from "../../plugins/voice-mode"
|
||||||
|
|
||||||
interface RouteDeps {
|
interface RouteDeps {
|
||||||
workspaceManager: WorkspaceManager
|
workspaceManager: WorkspaceManager
|
||||||
eventBus: EventBus
|
eventBus: EventBus
|
||||||
logger: Logger
|
logger: Logger
|
||||||
channel: PluginChannelManager
|
channel: PluginChannelManager
|
||||||
|
voiceModeManager: VoiceModeManager
|
||||||
}
|
}
|
||||||
|
|
||||||
const PluginEventSchema = z.object({
|
const PluginEventSchema = z.object({
|
||||||
@@ -21,6 +23,8 @@ const PluginEventSchema = z.object({
|
|||||||
|
|
||||||
const VoiceModeStateSchema = z.object({
|
const VoiceModeStateSchema = z.object({
|
||||||
enabled: z.boolean(),
|
enabled: z.boolean(),
|
||||||
|
clientId: z.string().trim().min(1),
|
||||||
|
connectionId: z.string().trim().min(1),
|
||||||
})
|
})
|
||||||
|
|
||||||
export function registerPluginRoutes(app: FastifyInstance, deps: RouteDeps) {
|
export function registerPluginRoutes(app: FastifyInstance, deps: RouteDeps) {
|
||||||
@@ -38,6 +42,7 @@ export function registerPluginRoutes(app: FastifyInstance, deps: RouteDeps) {
|
|||||||
reply.hijack()
|
reply.hijack()
|
||||||
|
|
||||||
const registration = deps.channel.register(request.params.id, reply)
|
const registration = deps.channel.register(request.params.id, reply)
|
||||||
|
deps.voiceModeManager.syncInstance(request.params.id)
|
||||||
|
|
||||||
const heartbeat = setInterval(() => {
|
const heartbeat = setInterval(() => {
|
||||||
deps.channel.send(request.params.id, buildPingEvent())
|
deps.channel.send(request.params.id, buildPingEvent())
|
||||||
@@ -61,13 +66,11 @@ export function registerPluginRoutes(app: FastifyInstance, deps: RouteDeps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const payload = VoiceModeStateSchema.parse(request.body ?? {})
|
const payload = VoiceModeStateSchema.parse(request.body ?? {})
|
||||||
deps.channel.send(request.params.id, {
|
deps.voiceModeManager.setEnabled(
|
||||||
type: "codenomad.voiceMode",
|
request.params.id,
|
||||||
properties: {
|
{ clientId: payload.clientId, connectionId: payload.connectionId },
|
||||||
enabled: payload.enabled,
|
payload.enabled,
|
||||||
formatVersion: "v1",
|
)
|
||||||
},
|
|
||||||
})
|
|
||||||
return { enabled: payload.enabled }
|
return { enabled: payload.enabled }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
2
packages/tauri-app/Cargo.lock
generated
2
packages/tauri-app/Cargo.lock
generated
@@ -458,7 +458,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "codenomad-tauri"
|
name = "codenomad-tauri"
|
||||||
version = "0.12.3"
|
version = "0.13.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@codenomad/tauri-app",
|
"name": "@codenomad/tauri-app",
|
||||||
"version": "0.13.1",
|
"version": "0.13.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "codenomad-tauri"
|
name = "codenomad-tauri"
|
||||||
version = "0.12.3"
|
version = "0.13.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use std::process::{Child, Command, Stdio};
|
|||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||||
use tauri::{webview::cookie::Cookie, AppHandle, Emitter, Manager, Url};
|
use tauri::{webview::cookie::Cookie, AppHandle, Emitter, Manager, Url};
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
@@ -48,7 +48,7 @@ fn workspace_root() -> Option<PathBuf> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const SESSION_COOKIE_NAME: &str = "codenomad_session";
|
const SESSION_COOKIE_NAME_PREFIX: &str = "codenomad_session";
|
||||||
|
|
||||||
const CLI_STOP_GRACE_SECS: u64 = 30;
|
const CLI_STOP_GRACE_SECS: u64 = 30;
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
@@ -124,7 +124,11 @@ fn extract_cookie_value(set_cookie: &str, name: &str) -> Option<String> {
|
|||||||
Some(value.to_string())
|
Some(value.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn exchange_bootstrap_token(base_url: &str, token: &str) -> anyhow::Result<Option<String>> {
|
fn exchange_bootstrap_token(
|
||||||
|
base_url: &str,
|
||||||
|
token: &str,
|
||||||
|
cookie_name: &str,
|
||||||
|
) -> anyhow::Result<Option<String>> {
|
||||||
let parsed = Url::parse(base_url)?;
|
let parsed = Url::parse(base_url)?;
|
||||||
let host = parsed.host_str().unwrap_or("127.0.0.1");
|
let host = parsed.host_str().unwrap_or("127.0.0.1");
|
||||||
let port = parsed.port_or_known_default().unwrap_or(80);
|
let port = parsed.port_or_known_default().unwrap_or(80);
|
||||||
@@ -159,11 +163,11 @@ fn exchange_bootstrap_token(base_url: &str, token: &str) -> anyhow::Result<Optio
|
|||||||
for line in lines {
|
for line in lines {
|
||||||
// handle case-insensitive header name
|
// handle case-insensitive header name
|
||||||
if let Some(value) = line.strip_prefix("Set-Cookie:") {
|
if let Some(value) = line.strip_prefix("Set-Cookie:") {
|
||||||
if let Some(session_id) = extract_cookie_value(value.trim(), SESSION_COOKIE_NAME) {
|
if let Some(session_id) = extract_cookie_value(value.trim(), cookie_name) {
|
||||||
return Ok(Some(session_id));
|
return Ok(Some(session_id));
|
||||||
}
|
}
|
||||||
} else if let Some(value) = line.strip_prefix("set-cookie:") {
|
} else if let Some(value) = line.strip_prefix("set-cookie:") {
|
||||||
if let Some(session_id) = extract_cookie_value(value.trim(), SESSION_COOKIE_NAME) {
|
if let Some(session_id) = extract_cookie_value(value.trim(), cookie_name) {
|
||||||
return Ok(Some(session_id));
|
return Ok(Some(session_id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -172,11 +176,16 @@ fn exchange_bootstrap_token(base_url: &str, token: &str) -> anyhow::Result<Optio
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_session_cookie(app: &AppHandle, base_url: &str, session_id: &str) -> anyhow::Result<()> {
|
fn set_session_cookie(
|
||||||
|
app: &AppHandle,
|
||||||
|
base_url: &str,
|
||||||
|
cookie_name: &str,
|
||||||
|
session_id: &str,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
let parsed = Url::parse(base_url)?;
|
let parsed = Url::parse(base_url)?;
|
||||||
let domain = parsed.host_str().unwrap_or("127.0.0.1").to_string();
|
let domain = parsed.host_str().unwrap_or("127.0.0.1").to_string();
|
||||||
|
|
||||||
let cookie = Cookie::build((SESSION_COOKIE_NAME, session_id))
|
let cookie = Cookie::build((cookie_name.to_string(), session_id.to_string()))
|
||||||
.domain(domain)
|
.domain(domain)
|
||||||
.path("/")
|
.path("/")
|
||||||
.http_only(true)
|
.http_only(true)
|
||||||
@@ -190,6 +199,16 @@ fn set_session_cookie(app: &AppHandle, base_url: &str, session_id: &str) -> anyh
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn generate_auth_cookie_name() -> String {
|
||||||
|
let pid = std::process::id();
|
||||||
|
let timestamp = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|duration| duration.as_millis())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
format!("{SESSION_COOKIE_NAME_PREFIX}_{pid}_{timestamp}")
|
||||||
|
}
|
||||||
|
|
||||||
const DEFAULT_CONFIG_PATH: &str = "~/.config/codenomad/config.json";
|
const DEFAULT_CONFIG_PATH: &str = "~/.config/codenomad/config.json";
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@@ -503,7 +522,8 @@ impl CliProcessManager {
|
|||||||
"resolved CLI entry runner={:?} entry={} host={}",
|
"resolved CLI entry runner={:?} entry={} host={}",
|
||||||
resolution.runner, resolution.entry, host
|
resolution.runner, resolution.entry, host
|
||||||
));
|
));
|
||||||
let args = resolution.build_args(dev, &host);
|
let auth_cookie_name = Arc::new(generate_auth_cookie_name());
|
||||||
|
let args = resolution.build_args(dev, &host, auth_cookie_name.as_str());
|
||||||
log_line(&format!("CLI args: {:?}", args));
|
log_line(&format!("CLI args: {:?}", args));
|
||||||
if dev {
|
if dev {
|
||||||
log_line("development mode: will prefer tsx + source if present");
|
log_line("development mode: will prefer tsx + source if present");
|
||||||
@@ -584,6 +604,7 @@ impl CliProcessManager {
|
|||||||
let app_clone = app.clone();
|
let app_clone = app.clone();
|
||||||
let ready_clone = ready.clone();
|
let ready_clone = ready.clone();
|
||||||
let token_clone = bootstrap_token.clone();
|
let token_clone = bootstrap_token.clone();
|
||||||
|
let auth_cookie_name_clone = auth_cookie_name.clone();
|
||||||
|
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let stdout = child_clone
|
let stdout = child_clone
|
||||||
@@ -605,6 +626,7 @@ impl CliProcessManager {
|
|||||||
&status_clone,
|
&status_clone,
|
||||||
&ready_clone,
|
&ready_clone,
|
||||||
&token_clone,
|
&token_clone,
|
||||||
|
auth_cookie_name_clone.as_str(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if let Some(reader) = stderr {
|
if let Some(reader) = stderr {
|
||||||
@@ -615,6 +637,7 @@ impl CliProcessManager {
|
|||||||
&status_clone,
|
&status_clone,
|
||||||
&ready_clone,
|
&ready_clone,
|
||||||
&token_clone,
|
&token_clone,
|
||||||
|
auth_cookie_name_clone.as_str(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -731,6 +754,7 @@ impl CliProcessManager {
|
|||||||
status: &Arc<Mutex<CliStatus>>,
|
status: &Arc<Mutex<CliStatus>>,
|
||||||
ready: &Arc<AtomicBool>,
|
ready: &Arc<AtomicBool>,
|
||||||
bootstrap_token: &Arc<Mutex<Option<String>>>,
|
bootstrap_token: &Arc<Mutex<Option<String>>>,
|
||||||
|
auth_cookie_name: &str,
|
||||||
) {
|
) {
|
||||||
let mut buffer = String::new();
|
let mut buffer = String::new();
|
||||||
let local_url_regex = Regex::new(r"^Local\s+Connection\s+URL\s*:\s*(https?://\S+)").ok();
|
let local_url_regex = Regex::new(r"^Local\s+Connection\s+URL\s*:\s*(https?://\S+)").ok();
|
||||||
@@ -766,7 +790,14 @@ impl CliProcessManager {
|
|||||||
.and_then(|re| re.captures(line).and_then(|c| c.get(1)))
|
.and_then(|re| re.captures(line).and_then(|c| c.get(1)))
|
||||||
.map(|m| m.as_str().to_string())
|
.map(|m| m.as_str().to_string())
|
||||||
{
|
{
|
||||||
Self::mark_ready(app, status, ready, bootstrap_token, url);
|
Self::mark_ready(
|
||||||
|
app,
|
||||||
|
status,
|
||||||
|
ready,
|
||||||
|
bootstrap_token,
|
||||||
|
auth_cookie_name,
|
||||||
|
url,
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -781,6 +812,7 @@ impl CliProcessManager {
|
|||||||
status,
|
status,
|
||||||
ready,
|
ready,
|
||||||
bootstrap_token,
|
bootstrap_token,
|
||||||
|
auth_cookie_name,
|
||||||
format!("http://localhost:{port}"),
|
format!("http://localhost:{port}"),
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
@@ -793,6 +825,7 @@ impl CliProcessManager {
|
|||||||
status,
|
status,
|
||||||
ready,
|
ready,
|
||||||
bootstrap_token,
|
bootstrap_token,
|
||||||
|
auth_cookie_name,
|
||||||
format!("http://localhost:{}", port),
|
format!("http://localhost:{}", port),
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
@@ -811,6 +844,7 @@ impl CliProcessManager {
|
|||||||
status: &Arc<Mutex<CliStatus>>,
|
status: &Arc<Mutex<CliStatus>>,
|
||||||
ready: &Arc<AtomicBool>,
|
ready: &Arc<AtomicBool>,
|
||||||
bootstrap_token: &Arc<Mutex<Option<String>>>,
|
bootstrap_token: &Arc<Mutex<Option<String>>>,
|
||||||
|
auth_cookie_name: &str,
|
||||||
base_url: String,
|
base_url: String,
|
||||||
) {
|
) {
|
||||||
ready.store(true, Ordering::SeqCst);
|
ready.store(true, Ordering::SeqCst);
|
||||||
@@ -834,9 +868,11 @@ impl CliProcessManager {
|
|||||||
if scheme.as_deref() != Some("http") {
|
if scheme.as_deref() != Some("http") {
|
||||||
navigate_main(app, &base_url);
|
navigate_main(app, &base_url);
|
||||||
} else {
|
} else {
|
||||||
match exchange_bootstrap_token(&base_url, &token) {
|
match exchange_bootstrap_token(&base_url, &token, &auth_cookie_name) {
|
||||||
Ok(Some(session_id)) => {
|
Ok(Some(session_id)) => {
|
||||||
if let Err(err) = set_session_cookie(app, &base_url, &session_id) {
|
if let Err(err) =
|
||||||
|
set_session_cookie(app, &base_url, &auth_cookie_name, &session_id)
|
||||||
|
{
|
||||||
log_line(&format!("failed to set session cookie: {err}"));
|
log_line(&format!("failed to set session cookie: {err}"));
|
||||||
navigate_main(app, &format!("{base_url}/login"));
|
navigate_main(app, &format!("{base_url}/login"));
|
||||||
} else {
|
} else {
|
||||||
@@ -932,11 +968,13 @@ impl CliEntry {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_args(&self, dev: bool, host: &str) -> Vec<String> {
|
fn build_args(&self, dev: bool, host: &str, auth_cookie_name: &str) -> Vec<String> {
|
||||||
let mut args = vec![
|
let mut args = vec![
|
||||||
"serve".to_string(),
|
"serve".to_string(),
|
||||||
"--host".to_string(),
|
"--host".to_string(),
|
||||||
host.to_string(),
|
host.to_string(),
|
||||||
|
"--auth-cookie-name".to_string(),
|
||||||
|
auth_cookie_name.to_string(),
|
||||||
"--generate-token".to_string(),
|
"--generate-token".to_string(),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "CodeNomad",
|
"productName": "CodeNomad",
|
||||||
"version": "0.12.3",
|
"version": "0.13.3",
|
||||||
"identifier": "ai.neuralnomads.codenomad.client",
|
"identifier": "ai.neuralnomads.codenomad.client",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev:bootstrap",
|
"beforeDevCommand": "npm run dev:bootstrap",
|
||||||
"beforeBuildCommand": "npm run bundle:server",
|
"beforeBuildCommand": "npm run bundle:server",
|
||||||
"frontendDist": "resources/ui-loading"
|
"frontendDist": "resources/ui-loading"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"app": {
|
"app": {
|
||||||
"withGlobalTauri": true,
|
"withGlobalTauri": true,
|
||||||
"windows": [
|
"windows": [
|
||||||
@@ -33,9 +30,13 @@
|
|||||||
],
|
],
|
||||||
"security": {
|
"security": {
|
||||||
"assetProtocol": {
|
"assetProtocol": {
|
||||||
"scope": ["**"]
|
"scope": [
|
||||||
|
"**"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"capabilities": ["main-window-native-dialogs"]
|
"capabilities": [
|
||||||
|
"main-window-native-dialogs"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
@@ -44,7 +45,17 @@
|
|||||||
"resources/server",
|
"resources/server",
|
||||||
"resources/ui-loading"
|
"resources/ui-loading"
|
||||||
],
|
],
|
||||||
"icon": ["icon.icns", "icon.ico", "icon.png"],
|
"icon": [
|
||||||
"targets": ["app", "appimage", "deb", "rpm", "nsis"]
|
"icon.icns",
|
||||||
|
"icon.ico",
|
||||||
|
"icon.png"
|
||||||
|
],
|
||||||
|
"targets": [
|
||||||
|
"app",
|
||||||
|
"appimage",
|
||||||
|
"deb",
|
||||||
|
"rpm",
|
||||||
|
"nsis"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@codenomad/ui",
|
"name": "@codenomad/ui",
|
||||||
"version": "0.13.1",
|
"version": "0.13.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createMemo, Show, createEffect, onCleanup } from "solid-js"
|
import { createMemo, Show, createEffect } from "solid-js"
|
||||||
import { DiffView, DiffModeEnum } from "@git-diff-view/solid"
|
import { DiffView, DiffModeEnum } from "@git-diff-view/solid"
|
||||||
import "@git-diff-view/solid/styles/diff-view-pure.css"
|
import "@git-diff-view/solid/styles/diff-view-pure.css"
|
||||||
import { disableCache } from "@git-diff-view/core"
|
import { disableCache } from "@git-diff-view/core"
|
||||||
@@ -20,6 +20,7 @@ interface ToolCallDiffViewerProps {
|
|||||||
filePath?: string
|
filePath?: string
|
||||||
theme: "light" | "dark"
|
theme: "light" | "dark"
|
||||||
mode: DiffViewMode
|
mode: DiffViewMode
|
||||||
|
wrap?: boolean
|
||||||
onRendered?: () => void
|
onRendered?: () => void
|
||||||
cachedHtml?: string
|
cachedHtml?: string
|
||||||
cacheEntryParams?: CacheEntryParams
|
cacheEntryParams?: CacheEntryParams
|
||||||
@@ -31,11 +32,183 @@ type DiffData = {
|
|||||||
hunks: string[]
|
hunks: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
type CaptureContext = {
|
function measureTextWidth(container: HTMLElement, text: string, source: HTMLElement) {
|
||||||
theme: ToolCallDiffViewerProps["theme"]
|
const computed = window.getComputedStyle(source)
|
||||||
mode: DiffViewMode
|
const probe = document.createElement("span")
|
||||||
diffText: string
|
probe.textContent = text || ""
|
||||||
cacheEntryParams?: CacheEntryParams
|
probe.style.position = "absolute"
|
||||||
|
probe.style.visibility = "hidden"
|
||||||
|
probe.style.pointerEvents = "none"
|
||||||
|
probe.style.display = "inline-block"
|
||||||
|
probe.style.width = "auto"
|
||||||
|
probe.style.maxWidth = "none"
|
||||||
|
probe.style.whiteSpace = "nowrap"
|
||||||
|
probe.style.fontFamily = computed.fontFamily
|
||||||
|
probe.style.fontSize = computed.fontSize
|
||||||
|
probe.style.fontWeight = computed.fontWeight
|
||||||
|
probe.style.fontStyle = computed.fontStyle
|
||||||
|
probe.style.letterSpacing = computed.letterSpacing
|
||||||
|
probe.style.fontVariant = computed.fontVariant
|
||||||
|
probe.style.textTransform = computed.textTransform
|
||||||
|
probe.style.lineHeight = computed.lineHeight
|
||||||
|
container.appendChild(probe)
|
||||||
|
const width = Math.ceil(probe.getBoundingClientRect().width)
|
||||||
|
probe.remove()
|
||||||
|
return width
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeCompactWidth(
|
||||||
|
container: HTMLElement,
|
||||||
|
entries: Array<{ text: string; source: HTMLElement }>,
|
||||||
|
maxWidthPx = 40,
|
||||||
|
) {
|
||||||
|
const measuredLabelWidthPx = entries.reduce((max, entry) => {
|
||||||
|
return Math.max(max, measureTextWidth(container, entry.text, entry.source))
|
||||||
|
}, 0)
|
||||||
|
const fallbackTextLength = entries.reduce((max, entry) => Math.max(max, entry.text.length), 1)
|
||||||
|
const fallbackWidthPx = Math.round(fallbackTextLength * 7 + 4)
|
||||||
|
return Math.max(2, Math.min(maxWidthPx, measuredLabelWidthPx > 0 ? measuredLabelWidthPx + 2 : fallbackWidthPx))
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyCompactUnifiedGutter(container: HTMLElement, wrap: boolean) {
|
||||||
|
const tableWrapper = container.querySelector<HTMLElement>(".unified-diff-table-wrapper")
|
||||||
|
const table = container.querySelector<HTMLTableElement>(".unified-diff-table")
|
||||||
|
const numberCol = container.querySelector<HTMLTableColElement>(".unified-diff-table-num-col")
|
||||||
|
const gutterRows = container.querySelectorAll<HTMLElement>(".diff-line-num")
|
||||||
|
const hunkGutters = container.querySelectorAll<HTMLElement>(".diff-line-hunk-action, .diff-line-widget-wrapper, .diff-line-extend-wrapper")
|
||||||
|
const entries: Array<{ gutter: HTMLElement; label: HTMLElement; text: string }> = []
|
||||||
|
|
||||||
|
if (table) {
|
||||||
|
if (wrap) {
|
||||||
|
table.classList.add("table-fixed")
|
||||||
|
table.style.tableLayout = "fixed"
|
||||||
|
table.style.width = "100%"
|
||||||
|
table.style.minWidth = "100%"
|
||||||
|
} else {
|
||||||
|
table.classList.remove("table-fixed")
|
||||||
|
table.style.tableLayout = "auto"
|
||||||
|
table.style.width = "max-content"
|
||||||
|
table.style.minWidth = "100%"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gutterRows.forEach((gutter) => {
|
||||||
|
const oldSpan = gutter.querySelector<HTMLElement>("[data-line-old-num]")
|
||||||
|
const newSpan = gutter.querySelector<HTMLElement>("[data-line-new-num]")
|
||||||
|
const spacer = gutter.querySelector<HTMLElement>(".shrink-0")
|
||||||
|
const flexWrapper = gutter.querySelector<HTMLElement>(":scope > .flex")
|
||||||
|
const currentLabel = gutter.querySelector<HTMLElement>(":scope > .tool-call-diff-compact-line-number")
|
||||||
|
|
||||||
|
const oldText = oldSpan?.textContent?.trim() ?? ""
|
||||||
|
const newText = newSpan?.textContent?.trim() ?? ""
|
||||||
|
const hasUsableNew = newText.length > 0 && newText !== "0"
|
||||||
|
const hasUsableOld = oldText.length > 0 && oldText !== "0"
|
||||||
|
const visibleText = hasUsableNew ? newText : hasUsableOld ? oldText : newText || oldText
|
||||||
|
|
||||||
|
if (flexWrapper) flexWrapper.style.display = "none"
|
||||||
|
if (spacer) spacer.style.display = "none"
|
||||||
|
if (oldSpan) { oldSpan.style.display = "none"; oldSpan.style.width = "auto" }
|
||||||
|
if (newSpan) { newSpan.style.display = "none"; newSpan.style.width = "auto" }
|
||||||
|
|
||||||
|
gutter.style.paddingLeft = "1px"
|
||||||
|
gutter.style.paddingRight = "1px"
|
||||||
|
gutter.style.textAlign = "left"
|
||||||
|
|
||||||
|
const label = currentLabel ?? document.createElement("span")
|
||||||
|
label.className = "tool-call-diff-compact-line-number"
|
||||||
|
label.textContent = visibleText
|
||||||
|
label.setAttribute("aria-hidden", visibleText ? "false" : "true")
|
||||||
|
if (!currentLabel) gutter.appendChild(label)
|
||||||
|
|
||||||
|
entries.push({ gutter, label, text: visibleText })
|
||||||
|
})
|
||||||
|
|
||||||
|
const gutterWidthPx = computeCompactWidth(container, entries.map((entry) => ({ text: entry.text, source: entry.label })))
|
||||||
|
const gutterWidth = `${gutterWidthPx}px`
|
||||||
|
const compactAsideWidth = `${Math.max(8, gutterWidthPx - 10)}px`
|
||||||
|
|
||||||
|
if (tableWrapper) {
|
||||||
|
tableWrapper.style.setProperty("--diff-aside-width", compactAsideWidth)
|
||||||
|
tableWrapper.style.setProperty("--diff-aside-width--", compactAsideWidth)
|
||||||
|
}
|
||||||
|
if (numberCol) {
|
||||||
|
numberCol.style.width = gutterWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.forEach(({ gutter, label }) => {
|
||||||
|
gutter.style.width = gutterWidth
|
||||||
|
gutter.style.minWidth = gutterWidth
|
||||||
|
gutter.style.maxWidth = gutterWidth
|
||||||
|
label.style.width = "auto"
|
||||||
|
label.style.maxWidth = "none"
|
||||||
|
})
|
||||||
|
|
||||||
|
hunkGutters.forEach((gutter) => {
|
||||||
|
gutter.style.width = gutterWidth
|
||||||
|
gutter.style.minWidth = gutterWidth
|
||||||
|
gutter.style.maxWidth = gutterWidth
|
||||||
|
gutter.style.paddingLeft = "0"
|
||||||
|
gutter.style.paddingRight = "0"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyCompactSplitGutter(container: HTMLElement) {
|
||||||
|
const oldWrapper = container.querySelector<HTMLElement>(".old-diff-table-wrapper")
|
||||||
|
const newWrapper = container.querySelector<HTMLElement>(".new-diff-table-wrapper")
|
||||||
|
const numberCells = Array.from(container.querySelectorAll<HTMLElement>(".diff-line-old-num, .diff-line-new-num"))
|
||||||
|
const hunkActions = Array.from(container.querySelectorAll<HTMLElement>(".diff-line-hunk-action, .diff-line-widget-wrapper, .diff-line-extend-wrapper"))
|
||||||
|
const numberSpans = numberCells
|
||||||
|
.map((cell) => ({ cell, span: cell.querySelector<HTMLElement>("[data-line-num]") }))
|
||||||
|
.filter((entry): entry is { cell: HTMLElement; span: HTMLElement } => Boolean(entry.span))
|
||||||
|
|
||||||
|
const gutterWidthPx = computeCompactWidth(
|
||||||
|
container,
|
||||||
|
numberSpans.map(({ span }) => ({ text: span.textContent?.trim() ?? "", source: span })),
|
||||||
|
64,
|
||||||
|
)
|
||||||
|
const gutterWidth = `${gutterWidthPx}px`
|
||||||
|
|
||||||
|
;[oldWrapper, newWrapper].forEach((wrapper) => {
|
||||||
|
if (wrapper) {
|
||||||
|
wrapper.style.setProperty("--diff-aside-width", gutterWidth)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
numberCells.forEach((cell) => {
|
||||||
|
cell.style.width = gutterWidth
|
||||||
|
cell.style.minWidth = gutterWidth
|
||||||
|
cell.style.maxWidth = gutterWidth
|
||||||
|
cell.style.paddingLeft = "2px"
|
||||||
|
cell.style.paddingRight = "2px"
|
||||||
|
cell.style.textAlign = "left"
|
||||||
|
cell.style.whiteSpace = "nowrap"
|
||||||
|
cell.style.overflowWrap = "normal"
|
||||||
|
cell.style.wordBreak = "normal"
|
||||||
|
})
|
||||||
|
|
||||||
|
numberSpans.forEach(({ span }) => {
|
||||||
|
span.style.whiteSpace = "nowrap"
|
||||||
|
span.style.overflowWrap = "normal"
|
||||||
|
span.style.wordBreak = "normal"
|
||||||
|
})
|
||||||
|
|
||||||
|
hunkActions.forEach((cell) => {
|
||||||
|
cell.style.width = gutterWidth
|
||||||
|
cell.style.minWidth = gutterWidth
|
||||||
|
cell.style.maxWidth = gutterWidth
|
||||||
|
cell.style.paddingLeft = "0"
|
||||||
|
cell.style.paddingRight = "0"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyCompactDiffLayout(container: HTMLElement, mode: DiffViewMode, wrap = false) {
|
||||||
|
if (mode === "unified") {
|
||||||
|
applyCompactUnifiedGutter(container, wrap)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (mode === "split") {
|
||||||
|
applyCompactSplitGutter(container)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToolCallDiffViewer(props: ToolCallDiffViewerProps) {
|
export function ToolCallDiffViewer(props: ToolCallDiffViewerProps) {
|
||||||
@@ -67,12 +240,15 @@ export function ToolCallDiffViewer(props: ToolCallDiffViewerProps) {
|
|||||||
const contextKey = createMemo(() => {
|
const contextKey = createMemo(() => {
|
||||||
const data = diffData()
|
const data = diffData()
|
||||||
if (!data) return ""
|
if (!data) return ""
|
||||||
return `${props.theme}|${props.mode}|${props.diffText}`
|
return `${props.theme}|${props.mode}|${props.wrap ? "wrap" : "nowrap"}|${props.diffText}`
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const cachedHtml = props.cachedHtml
|
const cachedHtml = props.cachedHtml
|
||||||
if (cachedHtml) {
|
if (cachedHtml) {
|
||||||
|
if (diffContainerRef) {
|
||||||
|
applyCompactDiffLayout(diffContainerRef, props.mode, Boolean(props.wrap))
|
||||||
|
}
|
||||||
// When we are given cached HTML, we rely on the caller's cache
|
// When we are given cached HTML, we rely on the caller's cache
|
||||||
// and simply notify once rendered.
|
// and simply notify once rendered.
|
||||||
props.onRendered?.()
|
props.onRendered?.()
|
||||||
@@ -83,9 +259,10 @@ export function ToolCallDiffViewer(props: ToolCallDiffViewerProps) {
|
|||||||
if (!key) return
|
if (!key) return
|
||||||
if (!diffContainerRef) return
|
if (!diffContainerRef) return
|
||||||
if (lastCapturedKey === key) return
|
if (lastCapturedKey === key) return
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
if (!diffContainerRef) return
|
if (!diffContainerRef) return
|
||||||
|
applyCompactDiffLayout(diffContainerRef, props.mode, Boolean(props.wrap))
|
||||||
const markup = diffContainerRef.innerHTML
|
const markup = diffContainerRef.innerHTML
|
||||||
if (!markup) return
|
if (!markup) return
|
||||||
lastCapturedKey = key
|
lastCapturedKey = key
|
||||||
@@ -95,6 +272,7 @@ export function ToolCallDiffViewer(props: ToolCallDiffViewerProps) {
|
|||||||
html: markup,
|
html: markup,
|
||||||
theme: props.theme,
|
theme: props.theme,
|
||||||
mode: props.mode,
|
mode: props.mode,
|
||||||
|
wrap: props.wrap,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
props.onRendered?.()
|
props.onRendered?.()
|
||||||
@@ -122,7 +300,7 @@ export function ToolCallDiffViewer(props: ToolCallDiffViewerProps) {
|
|||||||
diffViewMode={props.mode === "split" ? DiffModeEnum.Split : DiffModeEnum.Unified}
|
diffViewMode={props.mode === "split" ? DiffModeEnum.Split : DiffModeEnum.Unified}
|
||||||
diffViewTheme={props.theme}
|
diffViewTheme={props.theme}
|
||||||
diffViewHighlight
|
diffViewHighlight
|
||||||
diffViewWrap={false}
|
diffViewWrap={Boolean(props.wrap)}
|
||||||
diffViewFontSize={13}
|
diffViewFontSize={13}
|
||||||
/>
|
/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
@@ -131,7 +309,7 @@ export function ToolCallDiffViewer(props: ToolCallDiffViewerProps) {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div innerHTML={props.cachedHtml} />
|
<div ref={diffContainerRef} innerHTML={props.cachedHtml} />
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -443,7 +443,7 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
class="selector-button selector-button-secondary w-auto px-3 py-1.5 inline-flex items-center justify-center gap-1.5"
|
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")}
|
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) => {
|
onClick={(event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
void openExternalUrl(GITHUB_URL, "folder-selection")
|
void openExternalUrl(GITHUB_URL, "folder-selection")
|
||||||
|
|||||||
@@ -36,12 +36,12 @@ import { serverApi } from "../../lib/api-client"
|
|||||||
import { loadBackgroundProcesses } from "../../stores/background-processes"
|
import { loadBackgroundProcesses } from "../../stores/background-processes"
|
||||||
import { BackgroundProcessOutputDialog } from "../background-process-output-dialog"
|
import { BackgroundProcessOutputDialog } from "../background-process-output-dialog"
|
||||||
import { useI18n } from "../../lib/i18n"
|
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 SessionSidebar from "./shell/SessionSidebar"
|
||||||
import { useSessionSidebarRequests } from "./shell/useSessionSidebarRequests"
|
import { useSessionSidebarRequests } from "./shell/useSessionSidebarRequests"
|
||||||
import RightPanel from "./shell/right-panel/RightPanel"
|
import RightPanel from "./shell/right-panel/RightPanel"
|
||||||
import { useDrawerChrome } from "./shell/useDrawerChrome"
|
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 { Maximize2, ShieldAlert } from "lucide-solid"
|
||||||
|
|
||||||
import type { LayoutMode } from "./shell/types"
|
import type { LayoutMode } from "./shell/types"
|
||||||
@@ -57,6 +57,13 @@ import { useDrawerHostMeasure } from "./shell/useDrawerHostMeasure"
|
|||||||
import { useDrawerResize } from "./shell/useDrawerResize"
|
import { useDrawerResize } from "./shell/useDrawerResize"
|
||||||
import { useSessionCache } from "./shell/useSessionCache"
|
import { useSessionCache } from "./shell/useSessionCache"
|
||||||
import { useInstanceSessionContext } from "./shell/useInstanceSessionContext"
|
import { useInstanceSessionContext } from "./shell/useInstanceSessionContext"
|
||||||
|
import { getPermissionSessionId } from "../../types/permission"
|
||||||
|
import {
|
||||||
|
canAutoRespondPermission,
|
||||||
|
finishAutoRespondPermission,
|
||||||
|
getPermissionAutoAcceptInFlightVersion,
|
||||||
|
isPermissionAutoAcceptEnabled,
|
||||||
|
} from "../../stores/permission-auto-accept"
|
||||||
|
|
||||||
const log = getLogger("session")
|
const log = getLogger("session")
|
||||||
|
|
||||||
@@ -97,6 +104,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
const [selectedBackgroundProcess, setSelectedBackgroundProcess] = createSignal<BackgroundProcess | null>(null)
|
const [selectedBackgroundProcess, setSelectedBackgroundProcess] = createSignal<BackgroundProcess | null>(null)
|
||||||
const [showBackgroundOutput, setShowBackgroundOutput] = createSignal(false)
|
const [showBackgroundOutput, setShowBackgroundOutput] = createSignal(false)
|
||||||
const [permissionModalOpen, setPermissionModalOpen] = createSignal(false)
|
const [permissionModalOpen, setPermissionModalOpen] = createSignal(false)
|
||||||
|
const [now, setNow] = createSignal(Date.now())
|
||||||
|
|
||||||
// Worktree selector manages its own dialogs.
|
// Worktree selector manages its own dialogs.
|
||||||
const [showSessionSearch, setShowSessionSearch] = createSignal(false)
|
const [showSessionSearch, setShowSessionSearch] = createSignal(false)
|
||||||
@@ -230,6 +238,12 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
window.localStorage.setItem(RIGHT_DRAWER_STORAGE_KEY, rightDrawerWidth().toString())
|
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 connectionStatus = () => sseManager.getStatus(props.instance.id)
|
||||||
const connectionStatusClass = () => {
|
const connectionStatusClass = () => {
|
||||||
const status = connectionStatus()
|
const status = connectionStatus()
|
||||||
@@ -252,6 +266,33 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
return permissions + questions > 0
|
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 activeSessionStatusPill = createMemo(() => {
|
||||||
const activeSessionId = activeSessionIdForInstance()
|
const activeSessionId = activeSessionIdForInstance()
|
||||||
if (!activeSessionId || activeSessionId === "info") return null
|
if (!activeSessionId || activeSessionId === "info") return null
|
||||||
@@ -272,17 +313,28 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const status = getSessionStatus(props.instance.id, activeSessionId)
|
const status = getSessionStatus(props.instance.id, activeSessionId)
|
||||||
const text =
|
const retry = getSessionRetry(props.instance.id, activeSessionId)
|
||||||
status === "working"
|
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")
|
? t("sessionList.status.working")
|
||||||
: status === "compacting"
|
: status === "compacting"
|
||||||
? t("sessionList.status.compacting")
|
? t("sessionList.status.compacting")
|
||||||
: t("sessionList.status.idle")
|
: t("sessionList.status.idle")
|
||||||
|
|
||||||
return {
|
return {
|
||||||
className: `session-${status}`,
|
className: `session-${retry ? "retrying" : status}`,
|
||||||
text,
|
text,
|
||||||
showAlertIcon: false,
|
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()
|
const pill = activeSessionStatusPill()
|
||||||
if (!pill) return null
|
if (!pill) return null
|
||||||
return (
|
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.showAlertIcon ? <ShieldAlert class="w-3.5 h-3.5" aria-hidden="true" /> : <span class="status-dot" />}
|
||||||
{pill.text}
|
{pill.text}
|
||||||
</span>
|
</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 = () => {
|
const handleCommandPaletteClick = () => {
|
||||||
showCommandPalette(props.instance.id)
|
showCommandPalette(props.instance.id)
|
||||||
}
|
}
|
||||||
@@ -622,12 +700,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div class="flex-1 flex items-center justify-center min-w-0">
|
<div class="flex-1 flex items-center justify-center min-w-0">
|
||||||
<Show when={hasPendingRequests()} fallback={renderActiveSessionStatusPill()}>
|
{renderSessionHeaderIndicators()}
|
||||||
<PermissionNotificationBanner
|
|
||||||
instanceId={props.instance.id}
|
|
||||||
onClick={() => setPermissionModalOpen(true)}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center justify-center gap-1">
|
<div class="flex flex-wrap items-center justify-center gap-1">
|
||||||
@@ -719,12 +792,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div class="ml-auto flex items-center session-header-hints">
|
<div class="ml-auto flex items-center session-header-hints">
|
||||||
<Show when={hasPendingRequests()} fallback={renderActiveSessionStatusPill()}>
|
{renderSessionHeaderIndicators()}
|
||||||
<PermissionNotificationBanner
|
|
||||||
instanceId={props.instance.id}
|
|
||||||
onClick={() => setPermissionModalOpen(true)}
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -48,104 +48,103 @@ interface SessionSidebarProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SessionSidebar: Component<SessionSidebarProps> = (props) => (
|
const SessionSidebar: Component<SessionSidebarProps> = (props) => (
|
||||||
<div class="flex flex-col h-full min-h-0" ref={props.setContentEl}>
|
<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 flex-col gap-2 px-4 py-3 border-b border-base">
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<span class="session-sidebar-title text-sm font-semibold uppercase text-primary">
|
<span class="session-sidebar-title text-sm font-semibold uppercase text-primary">
|
||||||
{props.t("instanceShell.leftPanel.sessionsTitle")}
|
{props.t("instanceShell.leftPanel.sessionsTitle")}
|
||||||
</span>
|
</span>
|
||||||
<div class="flex items-center gap-2 text-primary">
|
<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()}>
|
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
aria-label={props.leftPinned() ? props.t("instanceShell.leftDrawer.unpin") : props.t("instanceShell.leftDrawer.pin")}
|
aria-label={props.t("sessionList.actions.newSession.ariaLabel")}
|
||||||
onClick={() => (props.leftPinned() ? props.onUnpinLeftDrawer() : props.onPinLeftDrawer())}
|
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>
|
</IconButton>
|
||||||
</Show>
|
|
||||||
<Show when={props.drawerState() === "floating-open"}>
|
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
color="inherit"
|
color="inherit"
|
||||||
aria-label={props.t("instanceShell.leftDrawer.toggle.close")}
|
aria-label={props.t("sessionList.filter.ariaLabel")}
|
||||||
title={props.t("instanceShell.leftDrawer.toggle.close")}
|
title={props.t("sessionList.filter.ariaLabel")}
|
||||||
onClick={props.onCloseLeftDrawer}
|
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>
|
||||||
|
<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>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="session-sidebar flex flex-col flex-1 min-h-0">
|
||||||
<SessionList
|
<SessionList
|
||||||
instanceId={props.instanceId}
|
instanceId={props.instanceId}
|
||||||
threads={props.threads()}
|
threads={props.threads()}
|
||||||
activeSessionId={props.activeSessionId()}
|
activeSessionId={props.activeSessionId()}
|
||||||
onSelect={props.onSelectSession}
|
onSelect={props.onSelectSession}
|
||||||
onNew={() => {
|
onNew={() => {
|
||||||
const result = props.onNewSession()
|
const result = props.onNewSession()
|
||||||
if (result instanceof Promise) {
|
if (result instanceof Promise) {
|
||||||
void result.catch((error) => log.error("Failed to create session:", error))
|
void result.catch((error) => log.error("Failed to create session:", error))
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
enableFilterBar={props.showSearch()}
|
enableFilterBar={props.showSearch()}
|
||||||
showHeader={false}
|
showHeader={false}
|
||||||
showFooter={false}
|
showFooter={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="session-sidebar-separator" />
|
<div class="session-sidebar-separator" />
|
||||||
<Show when={props.activeSession()}>
|
<Show when={props.activeSession()}>
|
||||||
{(activeSession) => (
|
{(activeSession) => (
|
||||||
<>
|
|
||||||
<div class="session-sidebar-controls px-4 py-4 border-t border-base flex flex-col gap-3">
|
<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} />
|
<WorktreeSelector instanceId={props.instanceId} sessionId={activeSession().id} />
|
||||||
|
|
||||||
@@ -177,11 +176,10 @@ const SessionSidebar: Component<SessionSidebarProps> = (props) => (
|
|||||||
showDescription={false}
|
showDescription={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
)}
|
||||||
)}
|
</Show>
|
||||||
</Show>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
)
|
|
||||||
|
|
||||||
export default SessionSidebar
|
export default SessionSidebar
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ interface RightPanelProps {
|
|||||||
const RightPanel: Component<RightPanelProps> = (props) => {
|
const RightPanel: Component<RightPanelProps> = (props) => {
|
||||||
const [rightPanelTab, setRightPanelTab] = createSignal<RightPanelTab>(readStoredRightPanelTab("changes"))
|
const [rightPanelTab, setRightPanelTab] = createSignal<RightPanelTab>(readStoredRightPanelTab("changes"))
|
||||||
const [rightPanelExpandedItems, setRightPanelExpandedItems] = createSignal<string[]>([
|
const [rightPanelExpandedItems, setRightPanelExpandedItems] = createSignal<string[]>([
|
||||||
|
"yolo-mode",
|
||||||
"plan",
|
"plan",
|
||||||
"background-processes",
|
"background-processes",
|
||||||
"mcp",
|
"mcp",
|
||||||
@@ -787,7 +788,7 @@ const RightPanel: Component<RightPanelProps> = (props) => {
|
|||||||
setRightPanelTab("changes")
|
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(() => {
|
createEffect(() => {
|
||||||
const currentExpanded = new Set(rightPanelExpandedItems())
|
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 type { ToolState } from "@opencode-ai/sdk/v2"
|
||||||
import { Accordion } from "@kobalte/core"
|
import { Accordion } from "@kobalte/core"
|
||||||
import { Tooltip } from "@kobalte/core/tooltip"
|
import { Tooltip } from "@kobalte/core/tooltip"
|
||||||
|
import Switch from "@suid/material/Switch"
|
||||||
|
|
||||||
import { ChevronDown, Info, TerminalSquare, Trash2, XOctagon } from "lucide-solid"
|
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 ContextUsagePanel from "../../../../session/context-usage-panel"
|
||||||
import { TodoListView } from "../../../../tool-call/renderers/todo"
|
import { TodoListView } from "../../../../tool-call/renderers/todo"
|
||||||
import InstanceServiceStatus from "../../../../instance-service-status"
|
import InstanceServiceStatus from "../../../../instance-service-status"
|
||||||
|
import { isPermissionAutoAcceptEnabled, togglePermissionAutoAccept } from "../../../../../stores/permission-auto-accept"
|
||||||
|
|
||||||
interface StatusTabProps {
|
interface StatusTabProps {
|
||||||
t: (key: string, vars?: Record<string, any>) => string
|
t: (key: string, vars?: Record<string, any>) => string
|
||||||
@@ -39,6 +41,35 @@ interface StatusTabProps {
|
|||||||
const StatusTab: Component<StatusTabProps> = (props) => {
|
const StatusTab: Component<StatusTabProps> = (props) => {
|
||||||
const isSectionExpanded = (id: string) => props.expandedItems().includes(id)
|
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 renderStatusSessionChanges = () => {
|
||||||
const sessionId = props.activeSessionId()
|
const sessionId = props.activeSessionId()
|
||||||
if (!sessionId || sessionId === "info") {
|
if (!sessionId || sessionId === "info") {
|
||||||
@@ -204,6 +235,12 @@ const StatusTab: Component<StatusTabProps> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const statusSections = [
|
const statusSections = [
|
||||||
|
{
|
||||||
|
id: "yolo-mode",
|
||||||
|
labelKey: "instanceShell.rightPanel.sections.yoloMode",
|
||||||
|
tooltipKey: "instanceShell.rightPanel.sections.yoloMode.tooltip",
|
||||||
|
render: renderYoloModeSection,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "session-changes",
|
id: "session-changes",
|
||||||
labelKey: "instanceShell.rightPanel.sections.sessionChanges",
|
labelKey: "instanceShell.rightPanel.sections.sessionChanges",
|
||||||
@@ -281,29 +318,23 @@ const StatusTab: Component<StatusTabProps> = (props) => {
|
|||||||
<For each={statusSections}>
|
<For each={statusSections}>
|
||||||
{(section) => (
|
{(section) => (
|
||||||
<Accordion.Item value={section.id} class="right-panel-accordion-item">
|
<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">
|
<Accordion.Trigger class="right-panel-accordion-trigger">
|
||||||
<span class="section-left">
|
<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 class="section-label">{props.t(section.labelKey)}</span>
|
||||||
</span>
|
</span>
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
class={`right-panel-accordion-chevron ${isSectionExpanded(section.id) ? "right-panel-accordion-chevron-expanded" : ""}`}
|
class={`right-panel-accordion-chevron ${isSectionExpanded(section.id) ? "right-panel-accordion-chevron-expanded" : ""}`}
|
||||||
/>
|
/>
|
||||||
</Accordion.Trigger>
|
</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.Header>
|
||||||
<Accordion.Content class="right-panel-accordion-content">{section.render()}</Accordion.Content>
|
<Accordion.Content class="right-panel-accordion-content">{section.render()}</Accordion.Content>
|
||||||
</Accordion.Item>
|
</Accordion.Item>
|
||||||
|
|||||||
@@ -123,7 +123,11 @@ export function Markdown(props: MarkdownProps) {
|
|||||||
version: () => resolved().version,
|
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 = {
|
const cacheEntry: RenderCache = {
|
||||||
text: snapshot.text,
|
text: snapshot.text,
|
||||||
html: renderedHtml,
|
html: renderedHtml,
|
||||||
@@ -131,7 +135,9 @@ export function Markdown(props: MarkdownProps) {
|
|||||||
mode: `${snapshot.version}:${snapshot.escapeRawHtml ? "escaped" : "raw"}`,
|
mode: `${snapshot.version}:${snapshot.escapeRawHtml ? "escaped" : "raw"}`,
|
||||||
}
|
}
|
||||||
setHtml(renderedHtml)
|
setHtml(renderedHtml)
|
||||||
cacheHandle.set(cacheEntry)
|
if (options?.cache ?? true) {
|
||||||
|
cacheHandle.set(cacheEntry)
|
||||||
|
}
|
||||||
notifyRendered()
|
notifyRendered()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,9 +148,10 @@ export function Markdown(props: MarkdownProps) {
|
|||||||
suppressHighlight: !snapshot.highlightEnabled,
|
suppressHighlight: !snapshot.highlightEnabled,
|
||||||
escapeRawHtml: snapshot.escapeRawHtml,
|
escapeRawHtml: snapshot.escapeRawHtml,
|
||||||
})
|
})
|
||||||
|
const shouldCache = !snapshot.highlightEnabled || !markdown.hasPendingCodeHighlight(snapshot.text)
|
||||||
|
|
||||||
if (latestRequestKey === snapshot.requestKey) {
|
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 { usePromptPicker } from "./prompt-input/usePromptPicker"
|
||||||
import { usePromptKeyDown } from "./prompt-input/usePromptKeyDown"
|
import { usePromptKeyDown } from "./prompt-input/usePromptKeyDown"
|
||||||
import { usePromptVoiceInput } from "./prompt-input/usePromptVoiceInput"
|
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 log = getLogger("actions")
|
||||||
const LazyUnifiedPicker = lazy(() => import("./unified-picker"))
|
const LazyUnifiedPicker = lazy(() => import("./unified-picker"))
|
||||||
|
|
||||||
@@ -492,6 +497,8 @@ export default function PromptInput(props: PromptInputProps) {
|
|||||||
const beginVoicePress = (event?: PointerEvent | KeyboardEvent) => {
|
const beginVoicePress = (event?: PointerEvent | KeyboardEvent) => {
|
||||||
if (voiceButtonPressed || props.disabled || voiceInput.isTranscribing() || !voiceInput.canUseVoiceInput()) return
|
if (voiceButtonPressed || props.disabled || voiceInput.isTranscribing() || !voiceInput.canUseVoiceInput()) return
|
||||||
voiceButtonPressed = true
|
voiceButtonPressed = true
|
||||||
|
// Treat a mic press as barge-in: stop any active assistant speech before listening.
|
||||||
|
clearConversationPlaybackForInstance(props.instanceId)
|
||||||
|
|
||||||
if (event instanceof PointerEvent) {
|
if (event instanceof PointerEvent) {
|
||||||
const target = event.currentTarget
|
const target = event.currentTarget
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Dialog } from "@kobalte/core/dialog"
|
|||||||
import { Switch } from "@kobalte/core/switch"
|
import { Switch } from "@kobalte/core/switch"
|
||||||
import { For, Show, createEffect, createMemo, createSignal } from "solid-js"
|
import { For, Show, createEffect, createMemo, createSignal } from "solid-js"
|
||||||
import { toDataURL } from "qrcode"
|
import { toDataURL } from "qrcode"
|
||||||
import { ExternalLink, Link2, Loader2, RefreshCw, Shield, Wifi } from "lucide-solid"
|
import { ChevronDown, ExternalLink, Link2, Loader2, RefreshCw, Shield, Wifi } from "lucide-solid"
|
||||||
import type { NetworkAddress, ServerMeta } from "../../../server/src/api-types"
|
import type { NetworkAddress, ServerMeta } from "../../../server/src/api-types"
|
||||||
import { serverApi } from "../lib/api-client"
|
import { serverApi } from "../lib/api-client"
|
||||||
import { restartCli } from "../lib/native/cli"
|
import { restartCli } from "../lib/native/cli"
|
||||||
@@ -10,6 +10,7 @@ import { serverSettings, setListeningMode } from "../stores/preferences"
|
|||||||
import { showConfirmDialog } from "../stores/alerts"
|
import { showConfirmDialog } from "../stores/alerts"
|
||||||
import { getLogger } from "../lib/logger"
|
import { getLogger } from "../lib/logger"
|
||||||
import { useI18n } from "../lib/i18n"
|
import { useI18n } from "../lib/i18n"
|
||||||
|
import { splitRemoteAddresses, type RemoteAddressGroups } from "../lib/remote-access-addresses"
|
||||||
const log = getLogger("actions")
|
const log = getLogger("actions")
|
||||||
|
|
||||||
|
|
||||||
@@ -32,17 +33,17 @@ export function RemoteAccessOverlay(props: RemoteAccessOverlayProps) {
|
|||||||
const [passwordConfirm, setPasswordConfirm] = createSignal("")
|
const [passwordConfirm, setPasswordConfirm] = createSignal("")
|
||||||
const [passwordError, setPasswordError] = createSignal<string | null>(null)
|
const [passwordError, setPasswordError] = createSignal<string | null>(null)
|
||||||
const [savingPassword, setSavingPassword] = createSignal(false)
|
const [savingPassword, setSavingPassword] = createSignal(false)
|
||||||
|
const [showAllAddresses, setShowAllAddresses] = createSignal(false)
|
||||||
|
|
||||||
const addresses = createMemo<NetworkAddress[]>(() => meta()?.addresses ?? [])
|
const addresses = createMemo<NetworkAddress[]>(() => meta()?.addresses ?? [])
|
||||||
const currentMode = createMemo(() => meta()?.listeningMode ?? serverSettings().listeningMode)
|
const currentMode = createMemo(() => meta()?.listeningMode ?? serverSettings().listeningMode)
|
||||||
const allowExternalConnections = createMemo(() => currentMode() === "all")
|
const allowExternalConnections = createMemo(() => currentMode() === "all")
|
||||||
const displayAddresses = createMemo(() => {
|
const displayAddresses = createMemo<RemoteAddressGroups>(() => {
|
||||||
const list = addresses()
|
const list = addresses()
|
||||||
if (!allowExternalConnections()) {
|
if (!allowExternalConnections()) {
|
||||||
return []
|
return { recommended: null, hidden: [] }
|
||||||
}
|
}
|
||||||
// Local URL is displayed separately; list only remote-friendly addresses.
|
return splitRemoteAddresses(list)
|
||||||
return list.filter((address) => address.scope !== "loopback")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const refreshMeta = async () => {
|
const refreshMeta = async () => {
|
||||||
@@ -53,6 +54,7 @@ export function RemoteAccessOverlay(props: RemoteAccessOverlayProps) {
|
|||||||
const [metaResult, authResult] = await Promise.all([serverApi.fetchServerMeta(), serverApi.fetchAuthStatus()])
|
const [metaResult, authResult] = await Promise.all([serverApi.fetchServerMeta(), serverApi.fetchAuthStatus()])
|
||||||
setMeta(metaResult)
|
setMeta(metaResult)
|
||||||
setAuthStatus(authResult)
|
setAuthStatus(authResult)
|
||||||
|
setShowAllAddresses(false)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : String(err))
|
setError(err instanceof Error ? err.message : String(err))
|
||||||
} finally {
|
} finally {
|
||||||
@@ -326,7 +328,7 @@ export function RemoteAccessOverlay(props: RemoteAccessOverlayProps) {
|
|||||||
|
|
||||||
<Show when={!loading()} fallback={<div class="remote-card">{t("remoteAccess.addresses.loading")}</div>}>
|
<Show when={!loading()} fallback={<div class="remote-card">{t("remoteAccess.addresses.loading")}</div>}>
|
||||||
<Show when={!error()} fallback={<div class="remote-error">{error()}</div>}>
|
<Show when={!error()} fallback={<div class="remote-error">{error()}</div>}>
|
||||||
<Show when={displayAddresses().length > 0} fallback={<div class="remote-card">{t("remoteAccess.addresses.none")}</div>}>
|
<Show when={displayAddresses().recommended || meta()?.localUrl} fallback={<div class="remote-card">{t("remoteAccess.addresses.none")}</div>}>
|
||||||
<div class="remote-address-list">
|
<div class="remote-address-list">
|
||||||
<Show when={meta()?.localUrl}>
|
<Show when={meta()?.localUrl}>
|
||||||
{(url) => {
|
{(url) => {
|
||||||
@@ -373,8 +375,9 @@ export function RemoteAccessOverlay(props: RemoteAccessOverlayProps) {
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</Show>
|
</Show>
|
||||||
<For each={displayAddresses()}>
|
<Show when={displayAddresses().recommended}>
|
||||||
{(address) => {
|
{(addressAccessor) => {
|
||||||
|
const address = addressAccessor()
|
||||||
const url = address.remoteUrl
|
const url = address.remoteUrl
|
||||||
const expandedState = () => expandedUrl() === url
|
const expandedState = () => expandedUrl() === url
|
||||||
const qr = () => qrCodes()[url]
|
const qr = () => qrCodes()[url]
|
||||||
@@ -384,13 +387,14 @@ export function RemoteAccessOverlay(props: RemoteAccessOverlayProps) {
|
|||||||
: address.scope === "loopback"
|
: address.scope === "loopback"
|
||||||
? t("remoteAccess.address.scope.loopback")
|
? t("remoteAccess.address.scope.loopback")
|
||||||
: t("remoteAccess.address.scope.internal")
|
: t("remoteAccess.address.scope.internal")
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="remote-address">
|
<div class="remote-address">
|
||||||
<div class="remote-address-main">
|
<div class="remote-address-main">
|
||||||
<div>
|
<div>
|
||||||
<p class="remote-address-url">{url}</p>
|
<p class="remote-address-url">{url}</p>
|
||||||
<p class="remote-address-meta">
|
<p class="remote-address-meta">
|
||||||
{address.family.toUpperCase()} • {scopeLabel()} • {address.ip}
|
{address.family.toUpperCase()} - {scopeLabel()} - {address.ip}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="remote-actions">
|
<div class="remote-actions">
|
||||||
@@ -425,7 +429,83 @@ export function RemoteAccessOverlay(props: RemoteAccessOverlayProps) {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</For>
|
</Show>
|
||||||
|
|
||||||
|
<Show when={displayAddresses().hidden.length > 0}>
|
||||||
|
<div class="remote-address-disclosure" data-expanded={showAllAddresses()}>
|
||||||
|
<button
|
||||||
|
class="remote-address-disclosure-trigger"
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowAllAddresses(!showAllAddresses())}
|
||||||
|
aria-expanded={showAllAddresses()}
|
||||||
|
>
|
||||||
|
<span class="remote-address-disclosure-label">
|
||||||
|
{showAllAddresses()
|
||||||
|
? t("remoteAccess.addresses.actions.hideOther")
|
||||||
|
: t("remoteAccess.addresses.actions.showOther", { count: String(displayAddresses().hidden.length) })}
|
||||||
|
</span>
|
||||||
|
<ChevronDown class={`remote-address-disclosure-chevron ${showAllAddresses() ? "is-expanded" : ""}`} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Show when={showAllAddresses()}>
|
||||||
|
<div class="remote-address-disclosure-content">
|
||||||
|
<For each={displayAddresses().hidden}>
|
||||||
|
{(address) => {
|
||||||
|
const url = address.remoteUrl
|
||||||
|
const expandedState = () => expandedUrl() === url
|
||||||
|
const qr = () => qrCodes()[url]
|
||||||
|
const scopeLabel = () =>
|
||||||
|
address.scope === "external"
|
||||||
|
? t("remoteAccess.address.scope.network")
|
||||||
|
: address.scope === "loopback"
|
||||||
|
? t("remoteAccess.address.scope.loopback")
|
||||||
|
: t("remoteAccess.address.scope.internal")
|
||||||
|
return (
|
||||||
|
<div class="remote-address">
|
||||||
|
<div class="remote-address-main">
|
||||||
|
<div>
|
||||||
|
<p class="remote-address-url">{url}</p>
|
||||||
|
<p class="remote-address-meta">
|
||||||
|
{address.family.toUpperCase()} • {scopeLabel()} • {address.ip}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="remote-actions">
|
||||||
|
<button class="remote-pill" type="button" onClick={() => handleOpenUrl(url)}>
|
||||||
|
<ExternalLink class="remote-icon" />
|
||||||
|
{t("remoteAccess.address.open")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="remote-pill"
|
||||||
|
type="button"
|
||||||
|
onClick={() => void toggleExpanded(url)}
|
||||||
|
aria-expanded={expandedState()}
|
||||||
|
>
|
||||||
|
<Link2 class="remote-icon" />
|
||||||
|
{expandedState() ? t("remoteAccess.address.hideQr") : t("remoteAccess.address.showQr")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Show when={expandedState()}>
|
||||||
|
<div class="remote-qr">
|
||||||
|
<Show when={qr()} fallback={<Loader2 class="remote-icon remote-spin" aria-hidden="true" />}>
|
||||||
|
{(dataUrl) => (
|
||||||
|
<img
|
||||||
|
src={dataUrl()}
|
||||||
|
alt={t("remoteAccess.address.qrAlt", { url })}
|
||||||
|
class="remote-qr-img"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Component, For, Show, createSignal, createMemo, createEffect, JSX, onCleanup } from "solid-js"
|
import { Component, For, Show, createSignal, createMemo, createEffect, JSX, onCleanup } from "solid-js"
|
||||||
import type { SessionStatus } from "../types/session"
|
import type { SessionStatus } from "../types/session"
|
||||||
import type { SessionThread } from "../stores/session-state"
|
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 } from "lucide-solid"
|
import { Bot, User, Copy, Trash2, Pencil, ShieldAlert, ChevronDown, Search, Square, CheckSquare, MinusSquare, Split, RotateCw } from "lucide-solid"
|
||||||
import KeyboardHint from "./keyboard-hint"
|
import KeyboardHint from "./keyboard-hint"
|
||||||
import SessionRenameDialog from "./session-rename-dialog"
|
import SessionRenameDialog from "./session-rename-dialog"
|
||||||
import { keyboardRegistry } from "../lib/keyboard-registry"
|
import { keyboardRegistry } from "../lib/keyboard-registry"
|
||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
ensureSessionParentExpanded,
|
ensureSessionParentExpanded,
|
||||||
getVisibleSessionIds,
|
getVisibleSessionIds,
|
||||||
isSessionParentExpanded,
|
isSessionParentExpanded,
|
||||||
|
loadMessages,
|
||||||
loading,
|
loading,
|
||||||
renameSession,
|
renameSession,
|
||||||
sessions as sessionStateSessions,
|
sessions as sessionStateSessions,
|
||||||
@@ -53,6 +54,14 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
const normalizedQuery = createMemo(() => (props.enableFilterBar ? filterQuery().trim().toLowerCase() : ""))
|
const normalizedQuery = createMemo(() => (props.enableFilterBar ? filterQuery().trim().toLowerCase() : ""))
|
||||||
|
|
||||||
const [selectedSessionIds, setSelectedSessionIds] = createSignal<Set<string>>(new Set())
|
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 normalizeSessionLabel = (sessionId: string) => {
|
||||||
const session = sessionStateSessions().get(props.instanceId)?.get(sessionId)
|
const session = sessionStateSessions().get(props.instanceId)?.get(sessionId)
|
||||||
@@ -213,6 +222,32 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
setRenameTarget({ id: sessionId, title: session.title ?? "", label })
|
setRenameTarget({ id: sessionId, title: session.title ?? "", label })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isSessionReloading = (sessionId: string) => reloadingSessionIds().has(sessionId)
|
||||||
|
|
||||||
|
const handleReloadSession = async (event: MouseEvent, sessionId: string) => {
|
||||||
|
event.stopPropagation()
|
||||||
|
if (isSessionReloading(sessionId)) return
|
||||||
|
|
||||||
|
setReloadingSessionIds((prev) => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
next.add(sessionId)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
await loadMessages(props.instanceId, sessionId, true)
|
||||||
|
} catch (error) {
|
||||||
|
log.error(`Failed to reload session ${sessionId}:`, error)
|
||||||
|
showToastNotification({ message: t("sessionList.reload.error"), variant: "error" })
|
||||||
|
} finally {
|
||||||
|
setReloadingSessionIds((prev) => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
next.delete(sessionId)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const closeRenameDialog = () => {
|
const closeRenameDialog = () => {
|
||||||
setRenameTarget(null)
|
setRenameTarget(null)
|
||||||
}
|
}
|
||||||
@@ -372,7 +407,13 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
const isActive = () => props.activeSessionId === rowProps.sessionId
|
const isActive = () => props.activeSessionId === rowProps.sessionId
|
||||||
const title = () => session()?.title || t("sessionList.session.untitled")
|
const title = () => session()?.title || t("sessionList.session.untitled")
|
||||||
const status = () => getSessionStatus(props.instanceId, rowProps.sessionId)
|
const status = () => getSessionStatus(props.instanceId, rowProps.sessionId)
|
||||||
|
const retry = () => getSessionRetry(props.instanceId, rowProps.sessionId)
|
||||||
const statusLabel = () => {
|
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())) {
|
switch (formatSessionStatus(status())) {
|
||||||
case "working":
|
case "working":
|
||||||
return t("sessionList.status.working")
|
return t("sessionList.status.working")
|
||||||
@@ -385,13 +426,21 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
const needsPermission = () => Boolean(session()?.pendingPermission)
|
const needsPermission = () => Boolean(session()?.pendingPermission)
|
||||||
const needsQuestion = () => Boolean((session() as any)?.pendingQuestion)
|
const needsQuestion = () => Boolean((session() as any)?.pendingQuestion)
|
||||||
const needsInput = () => needsPermission() || needsQuestion()
|
const needsInput = () => needsPermission() || needsQuestion()
|
||||||
const statusClassName = () => (needsInput() ? "session-permission" : `session-${status()}`)
|
const statusClassName = () => (needsInput() ? "session-permission" : `session-${retry() ? "retrying" : status()}`)
|
||||||
const statusText = () =>
|
const statusText = () =>
|
||||||
needsPermission()
|
needsPermission()
|
||||||
? t("sessionList.status.needsPermission")
|
? t("sessionList.status.needsPermission")
|
||||||
: needsQuestion()
|
: needsQuestion()
|
||||||
? t("sessionList.status.needsInput")
|
? t("sessionList.status.needsInput")
|
||||||
: statusLabel()
|
: 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)
|
const isSelected = () => selectedSessionIds().has(rowProps.sessionId)
|
||||||
|
|
||||||
@@ -471,7 +520,7 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
<ChevronDown class={`w-3.5 h-3.5 transition-transform ${rowProps.expanded ? "" : "-rotate-90"}`} />
|
<ChevronDown class={`w-3.5 h-3.5 transition-transform ${rowProps.expanded ? "" : "-rotate-90"}`} />
|
||||||
</span>
|
</span>
|
||||||
</Show>
|
</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" />}
|
{needsInput() ? <ShieldAlert class="w-3.5 h-3.5" aria-hidden="true" /> : <span class="status-dot" />}
|
||||||
{statusText()}
|
{statusText()}
|
||||||
</span>
|
</span>
|
||||||
@@ -493,6 +542,21 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
>
|
>
|
||||||
<Copy class="w-3 h-3" />
|
<Copy class="w-3 h-3" />
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
class={`session-item-close opacity-80 hover:opacity-100 ${isActive() ? "hover:bg-white/20" : "hover:bg-surface-hover"}`}
|
||||||
|
onClick={(event) => handleReloadSession(event, rowProps.sessionId)}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label={t("sessionList.actions.reload.ariaLabel")}
|
||||||
|
title={t("sessionList.actions.reload.title")}
|
||||||
|
>
|
||||||
|
<Show
|
||||||
|
when={!isSessionReloading(rowProps.sessionId)}
|
||||||
|
fallback={<RotateCw class="w-3 h-3 animate-spin" />}
|
||||||
|
>
|
||||||
|
<RotateCw class="w-3 h-3" />
|
||||||
|
</Show>
|
||||||
|
</span>
|
||||||
<span
|
<span
|
||||||
class={`session-item-close opacity-80 hover:opacity-100 ${isActive() ? "hover:bg-white/20" : "hover:bg-surface-hover"}`}
|
class={`session-item-close opacity-80 hover:opacity-100 ${isActive() ? "hover:bg-white/20" : "hover:bg-surface-hover"}`}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Switch } from "@kobalte/core/switch"
|
import { Switch } from "@kobalte/core/switch"
|
||||||
import { For, Show, createMemo, createSignal, type Component, onMount } from "solid-js"
|
import { For, Show, createMemo, createSignal, type Component, onMount } from "solid-js"
|
||||||
import { toDataURL } from "qrcode"
|
import { toDataURL } from "qrcode"
|
||||||
import { ExternalLink, Link2, Loader2, RefreshCw, Shield, Wifi } from "lucide-solid"
|
import { ChevronDown, ExternalLink, Link2, Loader2, RefreshCw, Shield, Wifi } from "lucide-solid"
|
||||||
import type { NetworkAddress, ServerMeta } from "../../../../server/src/api-types"
|
import type { NetworkAddress, ServerMeta } from "../../../../server/src/api-types"
|
||||||
import { serverApi } from "../../lib/api-client"
|
import { serverApi } from "../../lib/api-client"
|
||||||
import { restartCli } from "../../lib/native/cli"
|
import { restartCli } from "../../lib/native/cli"
|
||||||
@@ -9,6 +9,7 @@ import { serverSettings, setListeningMode } from "../../stores/preferences"
|
|||||||
import { showConfirmDialog } from "../../stores/alerts"
|
import { showConfirmDialog } from "../../stores/alerts"
|
||||||
import { getLogger } from "../../lib/logger"
|
import { getLogger } from "../../lib/logger"
|
||||||
import { useI18n } from "../../lib/i18n"
|
import { useI18n } from "../../lib/i18n"
|
||||||
|
import { splitRemoteAddresses, type RemoteAddressGroups } from "../../lib/remote-access-addresses"
|
||||||
|
|
||||||
const log = getLogger("actions")
|
const log = getLogger("actions")
|
||||||
|
|
||||||
@@ -30,14 +31,15 @@ export const RemoteAccessSettingsSection: Component = () => {
|
|||||||
const [passwordConfirm, setPasswordConfirm] = createSignal("")
|
const [passwordConfirm, setPasswordConfirm] = createSignal("")
|
||||||
const [passwordError, setPasswordError] = createSignal<string | null>(null)
|
const [passwordError, setPasswordError] = createSignal<string | null>(null)
|
||||||
const [savingPassword, setSavingPassword] = createSignal(false)
|
const [savingPassword, setSavingPassword] = createSignal(false)
|
||||||
|
const [showAllAddresses, setShowAllAddresses] = createSignal(false)
|
||||||
|
|
||||||
const addresses = createMemo<NetworkAddress[]>(() => meta()?.addresses ?? [])
|
const addresses = createMemo<NetworkAddress[]>(() => meta()?.addresses ?? [])
|
||||||
const currentMode = createMemo(() => meta()?.listeningMode ?? serverSettings().listeningMode)
|
const currentMode = createMemo(() => meta()?.listeningMode ?? serverSettings().listeningMode)
|
||||||
const allowExternalConnections = createMemo(() => currentMode() === "all")
|
const allowExternalConnections = createMemo(() => currentMode() === "all")
|
||||||
const displayAddresses = createMemo(() => {
|
const displayAddresses = createMemo<RemoteAddressGroups>(() => {
|
||||||
const list = addresses()
|
const list = addresses()
|
||||||
if (!allowExternalConnections()) return []
|
if (!allowExternalConnections()) return { recommended: null, hidden: [] }
|
||||||
return list.filter((address) => address.scope !== "loopback")
|
return splitRemoteAddresses(list)
|
||||||
})
|
})
|
||||||
|
|
||||||
const refreshMeta = async () => {
|
const refreshMeta = async () => {
|
||||||
@@ -48,6 +50,7 @@ export const RemoteAccessSettingsSection: Component = () => {
|
|||||||
const [metaResult, authResult] = await Promise.all([serverApi.fetchServerMeta(), serverApi.fetchAuthStatus()])
|
const [metaResult, authResult] = await Promise.all([serverApi.fetchServerMeta(), serverApi.fetchAuthStatus()])
|
||||||
setMeta(metaResult)
|
setMeta(metaResult)
|
||||||
setAuthStatus(authResult)
|
setAuthStatus(authResult)
|
||||||
|
setShowAllAddresses(false)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : String(err))
|
setError(err instanceof Error ? err.message : String(err))
|
||||||
} finally {
|
} finally {
|
||||||
@@ -218,31 +221,35 @@ export const RemoteAccessSettingsSection: Component = () => {
|
|||||||
fallback={<div class="settings-card-message">{t("remoteAccess.authStatus.unavailable")}</div>}
|
fallback={<div class="settings-card-message">{t("remoteAccess.authStatus.unavailable")}</div>}
|
||||||
>
|
>
|
||||||
<div class="settings-card-content">
|
<div class="settings-card-content">
|
||||||
<p class="settings-help-text">{t("remoteAccess.username", { username: authStatus()!.username ?? "codenomad" })}</p>
|
<div class="settings-password-summary-row">
|
||||||
<p class="settings-help-text">
|
<div class="settings-password-summary-copy">
|
||||||
{authStatus()!.passwordUserProvided
|
<p class="settings-help-text">{t("remoteAccess.username", { username: authStatus()!.username ?? "codenomad" })}</p>
|
||||||
? t("remoteAccess.password.status.set")
|
<p class="settings-help-text">
|
||||||
: t("remoteAccess.password.status.unset")}
|
{authStatus()!.passwordUserProvided
|
||||||
</p>
|
? t("remoteAccess.password.status.set")
|
||||||
|
: t("remoteAccess.password.status.unset")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="settings-password-actions">
|
<div class="settings-password-actions">
|
||||||
<button
|
<button
|
||||||
class="settings-pill-button"
|
class="settings-pill-button"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPasswordFormOpen(!passwordFormOpen())
|
setPasswordFormOpen(!passwordFormOpen())
|
||||||
setPasswordError(null)
|
setPasswordError(null)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{passwordFormOpen()
|
{passwordFormOpen()
|
||||||
? t("remoteAccess.password.actions.cancel")
|
? t("remoteAccess.password.actions.cancel")
|
||||||
: authStatus()!.passwordUserProvided
|
: authStatus()!.passwordUserProvided
|
||||||
? t("remoteAccess.password.actions.change")
|
? t("remoteAccess.password.actions.change")
|
||||||
: t("remoteAccess.password.actions.set")}
|
: t("remoteAccess.password.actions.set")}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={passwordFormOpen()}>
|
<Show when={passwordFormOpen()}>
|
||||||
<div class="settings-form-group">
|
<div class="settings-form-group">
|
||||||
<label class="settings-form-label">{t("remoteAccess.password.form.newPassword")}</label>
|
<label class="settings-form-label">{t("remoteAccess.password.form.newPassword")}</label>
|
||||||
<input
|
<input
|
||||||
@@ -292,7 +299,7 @@ export const RemoteAccessSettingsSection: Component = () => {
|
|||||||
<Show when={!loading()} fallback={<div class="remote-card">{t("remoteAccess.addresses.loading")}</div>}>
|
<Show when={!loading()} fallback={<div class="remote-card">{t("remoteAccess.addresses.loading")}</div>}>
|
||||||
<Show when={!error()} fallback={<div class="remote-error">{error()}</div>}>
|
<Show when={!error()} fallback={<div class="remote-error">{error()}</div>}>
|
||||||
<Show
|
<Show
|
||||||
when={displayAddresses().length > 0 || meta()?.localUrl}
|
when={Boolean(displayAddresses().recommended) || meta()?.localUrl}
|
||||||
fallback={<div class="remote-card">{t("remoteAccess.addresses.none")}</div>}
|
fallback={<div class="remote-card">{t("remoteAccess.addresses.none")}</div>}
|
||||||
>
|
>
|
||||||
<div class="remote-address-list">
|
<div class="remote-address-list">
|
||||||
@@ -342,8 +349,9 @@ export const RemoteAccessSettingsSection: Component = () => {
|
|||||||
}}
|
}}
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<For each={displayAddresses()}>
|
<Show when={displayAddresses().recommended}>
|
||||||
{(address) => {
|
{(addressAccessor) => {
|
||||||
|
const address = addressAccessor()
|
||||||
const url = address.remoteUrl
|
const url = address.remoteUrl
|
||||||
const expandedState = () => expandedUrl() === url
|
const expandedState = () => expandedUrl() === url
|
||||||
const qr = () => qrCodes()[url]
|
const qr = () => qrCodes()[url]
|
||||||
@@ -383,7 +391,11 @@ export const RemoteAccessSettingsSection: Component = () => {
|
|||||||
<div class="remote-qr">
|
<div class="remote-qr">
|
||||||
<Show when={qr()} fallback={<Loader2 class="remote-icon remote-spin" aria-hidden="true" />}>
|
<Show when={qr()} fallback={<Loader2 class="remote-icon remote-spin" aria-hidden="true" />}>
|
||||||
{(dataUrl) => (
|
{(dataUrl) => (
|
||||||
<img src={dataUrl()} alt={t("remoteAccess.address.qrAlt", { url })} class="remote-qr-img" />
|
<img
|
||||||
|
src={dataUrl()}
|
||||||
|
alt={t("remoteAccess.address.qrAlt", { url })}
|
||||||
|
class="remote-qr-img"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
@@ -391,7 +403,80 @@ export const RemoteAccessSettingsSection: Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</For>
|
</Show>
|
||||||
|
|
||||||
|
<Show when={displayAddresses().hidden.length > 0}>
|
||||||
|
<div class="remote-address-disclosure" data-expanded={showAllAddresses()}>
|
||||||
|
<button
|
||||||
|
class="remote-address-disclosure-trigger"
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowAllAddresses(!showAllAddresses())}
|
||||||
|
aria-expanded={showAllAddresses()}
|
||||||
|
>
|
||||||
|
<span class="remote-address-disclosure-label">
|
||||||
|
{showAllAddresses()
|
||||||
|
? t("remoteAccess.addresses.actions.hideOther")
|
||||||
|
: t("remoteAccess.addresses.actions.showOther", { count: String(displayAddresses().hidden.length) })}
|
||||||
|
</span>
|
||||||
|
<ChevronDown class={`remote-address-disclosure-chevron ${showAllAddresses() ? "is-expanded" : ""}`} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Show when={showAllAddresses()}>
|
||||||
|
<div class="remote-address-disclosure-content">
|
||||||
|
<For each={displayAddresses().hidden}>
|
||||||
|
{(address) => {
|
||||||
|
const url = address.remoteUrl
|
||||||
|
const expandedState = () => expandedUrl() === url
|
||||||
|
const qr = () => qrCodes()[url]
|
||||||
|
const scopeLabel = () =>
|
||||||
|
address.scope === "external"
|
||||||
|
? t("remoteAccess.address.scope.network")
|
||||||
|
: address.scope === "loopback"
|
||||||
|
? t("remoteAccess.address.scope.loopback")
|
||||||
|
: t("remoteAccess.address.scope.internal")
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="remote-address">
|
||||||
|
<div class="remote-address-main">
|
||||||
|
<div>
|
||||||
|
<p class="remote-address-url">{url}</p>
|
||||||
|
<p class="remote-address-meta">
|
||||||
|
{address.family.toUpperCase()} - {scopeLabel()} - {address.ip}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="remote-actions">
|
||||||
|
<button class="remote-pill" type="button" onClick={() => handleOpenUrl(url)}>
|
||||||
|
<ExternalLink class="remote-icon" />
|
||||||
|
{t("remoteAccess.address.open")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="remote-pill"
|
||||||
|
type="button"
|
||||||
|
onClick={() => void toggleExpanded(url)}
|
||||||
|
aria-expanded={expandedState()}
|
||||||
|
>
|
||||||
|
<Link2 class="remote-icon" />
|
||||||
|
{expandedState() ? t("remoteAccess.address.hideQr") : t("remoteAccess.address.showQr")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Show when={expandedState()}>
|
||||||
|
<div class="remote-qr">
|
||||||
|
<Show when={qr()} fallback={<Loader2 class="remote-icon remote-spin" aria-hidden="true" />}>
|
||||||
|
{(dataUrl) => (
|
||||||
|
<img src={dataUrl()} alt={t("remoteAccess.address.qrAlt", { url })} class="remote-qr-img" />
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { Suspense, lazy, onMount, type Accessor, type JSXElement } from "solid-js"
|
import { Suspense, createEffect, createMemo, createSignal, lazy, onMount, type Accessor, type JSXElement } from "solid-js"
|
||||||
import type { ToolState } from "@opencode-ai/sdk/v2"
|
import type { ToolState } from "@opencode-ai/sdk/v2"
|
||||||
|
import useMediaQuery from "@suid/material/useMediaQuery"
|
||||||
|
import { AlignJustify, Copy, Split, WrapText } from "lucide-solid"
|
||||||
import type { RenderCache } from "../../types/message"
|
import type { RenderCache } from "../../types/message"
|
||||||
import type { DiffViewMode } from "../../stores/preferences"
|
import type { DiffViewMode } from "../../stores/preferences"
|
||||||
import type { DiffPayload, DiffRenderOptions, ToolScrollHelpers } from "./types"
|
import type { DiffPayload, DiffRenderOptions, ToolScrollHelpers } from "./types"
|
||||||
import { getRelativePath } from "./utils"
|
import { getRelativePath } from "./utils"
|
||||||
import { getCacheEntry } from "../../lib/global-cache"
|
import { getCacheEntry } from "../../lib/global-cache"
|
||||||
|
import { copyToClipboard } from "../../lib/clipboard"
|
||||||
|
|
||||||
const LazyToolCallDiffViewer = lazy(() =>
|
const LazyToolCallDiffViewer = lazy(() =>
|
||||||
import("../diff-viewer").then((module) => ({ default: module.ToolCallDiffViewer })),
|
import("../diff-viewer").then((module) => ({ default: module.ToolCallDiffViewer })),
|
||||||
@@ -43,6 +46,16 @@ export function createDiffContentRenderer(params: {
|
|||||||
handleScrollRendered: () => void
|
handleScrollRendered: () => void
|
||||||
onContentRendered?: () => void
|
onContentRendered?: () => void
|
||||||
}) {
|
}) {
|
||||||
|
const compactDiffQuery = useMediaQuery("(max-width: 640px)")
|
||||||
|
const [mobileModeOverride, setMobileModeOverride] = createSignal<DiffViewMode | undefined>(undefined)
|
||||||
|
const [wordWrapEnabled, setWordWrapEnabled] = createSignal(true)
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (!compactDiffQuery()) {
|
||||||
|
setMobileModeOverride(undefined)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const registerTracked = (element: HTMLDivElement | null) => {
|
const registerTracked = (element: HTMLDivElement | null) => {
|
||||||
params.scrollHelpers.registerContainer(element)
|
params.scrollHelpers.registerContainer(element)
|
||||||
}
|
}
|
||||||
@@ -58,7 +71,12 @@ export function createDiffContentRenderer(params: {
|
|||||||
: params.t("toolCall.diff.label"))
|
: params.t("toolCall.diff.label"))
|
||||||
const selectedVariant = options?.variant === "permission-diff" ? "permission-diff" : "diff"
|
const selectedVariant = options?.variant === "permission-diff" ? "permission-diff" : "diff"
|
||||||
const cacheHandle = selectedVariant === "permission-diff" ? params.permissionDiffCache : params.diffCache
|
const cacheHandle = selectedVariant === "permission-diff" ? params.permissionDiffCache : params.diffCache
|
||||||
const diffMode = () => (params.preferences().diffViewMode || "split") as DiffViewMode
|
const preferredMode = () => (params.preferences().diffViewMode || "split") as DiffViewMode
|
||||||
|
const effectiveMode = () => {
|
||||||
|
if (!compactDiffQuery()) return preferredMode()
|
||||||
|
return mobileModeOverride() || "unified"
|
||||||
|
}
|
||||||
|
const shouldWrap = () => wordWrapEnabled()
|
||||||
const themeKey = params.isDark() ? "dark" : "light"
|
const themeKey = params.isDark() ? "dark" : "light"
|
||||||
const state = params.toolState()
|
const state = params.toolState()
|
||||||
const disableScrollTracking = Boolean(
|
const disableScrollTracking = Boolean(
|
||||||
@@ -76,17 +94,40 @@ export function createDiffContentRenderer(params: {
|
|||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|
||||||
let cachedHtml: string | undefined
|
const currentMode = createMemo(() => effectiveMode())
|
||||||
const cached = getCacheEntry<RenderCache>(cacheEntryParams)
|
const currentWrap = createMemo(() => shouldWrap())
|
||||||
const currentMode = diffMode()
|
const cachedHtml = createMemo(() => {
|
||||||
if (cached && cached.text === payload.diffText && cached.theme === themeKey && cached.mode === currentMode) {
|
const cached = getCacheEntry<RenderCache>(cacheEntryParams)
|
||||||
cachedHtml = cached.html
|
if (
|
||||||
}
|
cached
|
||||||
|
&& cached.text === payload.diffText
|
||||||
|
&& cached.theme === themeKey
|
||||||
|
&& cached.mode === currentMode()
|
||||||
|
&& cached.wrap === currentWrap()
|
||||||
|
) {
|
||||||
|
return cached.html
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
|
||||||
const handleModeChange = (mode: DiffViewMode) => {
|
const handleModeChange = (mode: DiffViewMode) => {
|
||||||
|
if (compactDiffQuery()) {
|
||||||
|
setMobileModeOverride(mode)
|
||||||
|
}
|
||||||
params.setDiffViewMode(mode)
|
params.setDiffViewMode(mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nextViewMode = (): DiffViewMode => (currentMode() === "split" ? "unified" : "split")
|
||||||
|
const viewModeTitle = () =>
|
||||||
|
nextViewMode() === "split"
|
||||||
|
? params.t("toolCall.diff.switchToSplit")
|
||||||
|
: params.t("toolCall.diff.switchToUnified")
|
||||||
|
const wordWrapTitle = () =>
|
||||||
|
wordWrapEnabled()
|
||||||
|
? params.t("toolCall.diff.disableWordWrap")
|
||||||
|
: params.t("toolCall.diff.enableWordWrap")
|
||||||
|
const copyPatchTitle = () => params.t("toolCall.diff.copyPatch")
|
||||||
|
|
||||||
const handleDiffRendered = () => {
|
const handleDiffRendered = () => {
|
||||||
if (!disableScrollTracking) {
|
if (!disableScrollTracking) {
|
||||||
params.handleScrollRendered()
|
params.handleScrollRendered()
|
||||||
@@ -95,41 +136,54 @@ export function createDiffContentRenderer(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class="message-text tool-call-markdown tool-call-markdown-large tool-call-diff-shell"
|
class="message-text tool-call-markdown tool-call-markdown-large tool-call-diff-shell"
|
||||||
ref={registerRef}
|
data-diff-mode={currentMode()}
|
||||||
onScroll={disableScrollTracking ? undefined : params.scrollHelpers.handleScroll}
|
ref={registerRef}
|
||||||
>
|
onScroll={disableScrollTracking ? undefined : params.scrollHelpers.handleScroll}
|
||||||
|
>
|
||||||
<div class="tool-call-diff-toolbar" role="group" aria-label={params.t("toolCall.diff.viewMode.ariaLabel")}>
|
<div class="tool-call-diff-toolbar" role="group" aria-label={params.t("toolCall.diff.viewMode.ariaLabel")}>
|
||||||
<span class="tool-call-diff-toolbar-label">{toolbarLabel}</span>
|
<span class="tool-call-diff-toolbar-label">{toolbarLabel}</span>
|
||||||
<div class="tool-call-diff-toggle">
|
<div class="file-viewer-toolbar">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={`tool-call-diff-mode-button${diffMode() === "split" ? " active" : ""}`}
|
class="file-viewer-toolbar-icon-button"
|
||||||
aria-pressed={diffMode() === "split"}
|
onClick={() => void copyToClipboard(payload.diffText)}
|
||||||
onClick={() => handleModeChange("split")}
|
aria-label={copyPatchTitle()}
|
||||||
|
title={copyPatchTitle()}
|
||||||
>
|
>
|
||||||
{params.t("toolCall.diff.viewMode.split")}
|
<Copy class="h-4 w-4" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={`tool-call-diff-mode-button${diffMode() === "unified" ? " active" : ""}`}
|
class="file-viewer-toolbar-icon-button"
|
||||||
aria-pressed={diffMode() === "unified"}
|
onClick={() => handleModeChange(nextViewMode())}
|
||||||
onClick={() => handleModeChange("unified")}
|
aria-label={viewModeTitle()}
|
||||||
|
title={viewModeTitle()}
|
||||||
>
|
>
|
||||||
{params.t("toolCall.diff.viewMode.unified")}
|
{nextViewMode() === "split" ? <Split class="h-4 w-4" aria-hidden="true" /> : <AlignJustify class="h-4 w-4" aria-hidden="true" />}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class={`file-viewer-toolbar-icon-button${wordWrapEnabled() ? " active" : ""}`}
|
||||||
|
onClick={() => setWordWrapEnabled((enabled) => !enabled)}
|
||||||
|
aria-label={wordWrapTitle()}
|
||||||
|
title={wordWrapTitle()}
|
||||||
|
>
|
||||||
|
<WrapText class="h-4 w-4" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{cachedHtml ? (
|
{cachedHtml() ? (
|
||||||
<CachedDiffMarkup html={cachedHtml} onRendered={handleDiffRendered} />
|
<CachedDiffMarkup html={cachedHtml()!} onRendered={handleDiffRendered} />
|
||||||
) : (
|
) : (
|
||||||
<Suspense fallback={<pre class="tool-call-diff-fallback">{payload.diffText}</pre>}>
|
<Suspense fallback={<pre class="tool-call-diff-fallback">{payload.diffText}</pre>}>
|
||||||
<LazyToolCallDiffViewer
|
<LazyToolCallDiffViewer
|
||||||
diffText={payload.diffText}
|
diffText={payload.diffText}
|
||||||
filePath={payload.filePath}
|
filePath={payload.filePath}
|
||||||
theme={themeKey}
|
theme={themeKey}
|
||||||
mode={diffMode()}
|
mode={currentMode()}
|
||||||
|
wrap={currentWrap()}
|
||||||
cacheEntryParams={cacheEntryParams as any}
|
cacheEntryParams={cacheEntryParams as any}
|
||||||
onRendered={handleDiffRendered}
|
onRendered={handleDiffRendered}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import type {
|
|||||||
WorktreeMap,
|
WorktreeMap,
|
||||||
WorktreeCreateRequest,
|
WorktreeCreateRequest,
|
||||||
} from "../../../server/src/api-types"
|
} from "../../../server/src/api-types"
|
||||||
|
import { getClientIdentity } from "./client-identity"
|
||||||
import { getLogger } from "./logger"
|
import { getLogger } from "./logger"
|
||||||
|
|
||||||
const RUNTIME_BASE = typeof window !== "undefined" ? window.location?.origin : undefined
|
const RUNTIME_BASE = typeof window !== "undefined" ? window.location?.origin : undefined
|
||||||
@@ -350,9 +351,16 @@ export const serverApi = {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
updateVoiceMode(instanceId: string, enabled: boolean): Promise<VoiceModeStateResponse> {
|
updateVoiceMode(instanceId: string, enabled: boolean): Promise<VoiceModeStateResponse> {
|
||||||
|
const identity = getClientIdentity()
|
||||||
return request<VoiceModeStateResponse>(`/workspaces/${encodeURIComponent(instanceId)}/plugin/voice-mode`, {
|
return request<VoiceModeStateResponse>(`/workspaces/${encodeURIComponent(instanceId)}/plugin/voice-mode`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ enabled }),
|
body: JSON.stringify({ ...identity, enabled }),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
sendClientConnectionPong(payload: { clientId: string; connectionId: string; pingTs?: number }): Promise<void> {
|
||||||
|
return request<void>("/api/client-connections/pong", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(payload),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
fetchBackgroundProcessOutput(
|
fetchBackgroundProcessOutput(
|
||||||
@@ -379,9 +387,15 @@ export const serverApi = {
|
|||||||
`/workspaces/${encodeURIComponent(instanceId)}/plugin/background-processes/${encodeURIComponent(processId)}/output${suffix}`,
|
`/workspaces/${encodeURIComponent(instanceId)}/plugin/background-processes/${encodeURIComponent(processId)}/output${suffix}`,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
connectEvents(onEvent: (event: WorkspaceEventPayload) => void, onError?: () => void) {
|
connectEvents(
|
||||||
sseLogger.info(`Connecting to ${EVENTS_URL}`)
|
onEvent: (event: WorkspaceEventPayload) => void,
|
||||||
const source = new EventSource(EVENTS_URL, { withCredentials: true } as any)
|
onError?: () => void,
|
||||||
|
onPing?: (payload: { ts?: number }) => void,
|
||||||
|
) {
|
||||||
|
const identity = getClientIdentity()
|
||||||
|
const url = buildClientEventsUrl(identity)
|
||||||
|
sseLogger.info(`Connecting to ${url}`)
|
||||||
|
const source = new EventSource(url, { withCredentials: true } as any)
|
||||||
source.onmessage = (event) => {
|
source.onmessage = (event) => {
|
||||||
try {
|
try {
|
||||||
const payload = JSON.parse(event.data) as WorkspaceEventPayload
|
const payload = JSON.parse(event.data) as WorkspaceEventPayload
|
||||||
@@ -394,8 +408,26 @@ export const serverApi = {
|
|||||||
sseLogger.warn("EventSource error, closing stream")
|
sseLogger.warn("EventSource error, closing stream")
|
||||||
onError?.()
|
onError?.()
|
||||||
}
|
}
|
||||||
|
source.addEventListener("codenomad.client.ping", (event: MessageEvent) => {
|
||||||
|
try {
|
||||||
|
const payload = event.data ? (JSON.parse(event.data) as { ts?: number }) : {}
|
||||||
|
onPing?.(payload)
|
||||||
|
} catch (error) {
|
||||||
|
sseLogger.error("Failed to parse ping event", error)
|
||||||
|
}
|
||||||
|
})
|
||||||
return source
|
return source
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildClientEventsUrl(identity: { clientId: string; connectionId: string }): string {
|
||||||
|
const url = new URL(EVENTS_URL, typeof window !== "undefined" ? window.location.origin : "http://localhost")
|
||||||
|
url.searchParams.set("clientId", identity.clientId)
|
||||||
|
url.searchParams.set("connectionId", identity.connectionId)
|
||||||
|
if (EVENTS_URL.startsWith("http://") || EVENTS_URL.startsWith("https://")) {
|
||||||
|
return url.toString()
|
||||||
|
}
|
||||||
|
return `${url.pathname}${url.search}`
|
||||||
|
}
|
||||||
|
|
||||||
export type { WorkspaceDescriptor, WorkspaceLogEntry, WorkspaceEventPayload, WorkspaceEventType }
|
export type { WorkspaceDescriptor, WorkspaceLogEntry, WorkspaceEventPayload, WorkspaceEventType }
|
||||||
|
|||||||
58
packages/ui/src/lib/client-identity.ts
Normal file
58
packages/ui/src/lib/client-identity.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
const CLIENT_ID_STORAGE_KEY = "codenomad.client-id"
|
||||||
|
const CONNECTION_ID_STORAGE_KEY = "codenomad.connection-id"
|
||||||
|
|
||||||
|
let cachedClientId: string | null = null
|
||||||
|
let cachedConnectionId: string | null = null
|
||||||
|
|
||||||
|
export function getClientIdentity(): { clientId: string; connectionId: string } {
|
||||||
|
return {
|
||||||
|
clientId: getOrCreateClientId(),
|
||||||
|
connectionId: getOrCreateConnectionId(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOrCreateClientId(): string {
|
||||||
|
if (cachedClientId) return cachedClientId
|
||||||
|
cachedClientId = getOrCreateStoredValue(CLIENT_ID_STORAGE_KEY, window.localStorage)
|
||||||
|
return cachedClientId
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOrCreateConnectionId(): string {
|
||||||
|
if (cachedConnectionId) return cachedConnectionId
|
||||||
|
cachedConnectionId = getOrCreateStoredValue(CONNECTION_ID_STORAGE_KEY, window.sessionStorage)
|
||||||
|
return cachedConnectionId
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOrCreateStoredValue(key: string, storage: Storage): string {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return generateUUID()
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const existing = storage.getItem(key)
|
||||||
|
if (existing && existing.trim()) {
|
||||||
|
return existing.trim()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return generateUUID()
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = generateUUID()
|
||||||
|
try {
|
||||||
|
storage.setItem(key, next)
|
||||||
|
} catch {
|
||||||
|
// Ignore storage failures and fall back to the in-memory value.
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateUUID(): string {
|
||||||
|
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
||||||
|
return crypto.randomUUID()
|
||||||
|
}
|
||||||
|
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (char) => {
|
||||||
|
const random = (Math.random() * 16) | 0
|
||||||
|
const value = char === "x" ? random : (random & 0x3) | 0x8
|
||||||
|
return value.toString(16)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -19,9 +19,6 @@ export function formatCompactCount(value: number): string {
|
|||||||
return `${(value / 1_000_000).toFixed(1)}M`
|
return `${(value / 1_000_000).toFixed(1)}M`
|
||||||
}
|
}
|
||||||
if (value >= 10_000) {
|
if (value >= 10_000) {
|
||||||
return `${Math.round(value / 1_000)}K`
|
|
||||||
}
|
|
||||||
if (value >= 1_000) {
|
|
||||||
const label = `${(value / 1_000).toFixed(1)}K`
|
const label = `${(value / 1_000).toFixed(1)}K`
|
||||||
return label.replace(/\.0K$/, "K")
|
return label.replace(/\.0K$/, "K")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export const instanceMessages = {
|
|||||||
|
|
||||||
"instanceShell.leftPanel.sessionsTitle": "Sessions",
|
"instanceShell.leftPanel.sessionsTitle": "Sessions",
|
||||||
"instanceShell.leftPanel.instanceInfo": "Instance Info",
|
"instanceShell.leftPanel.instanceInfo": "Instance Info",
|
||||||
|
|
||||||
"instanceShell.leftDrawer.pin": "Pin left drawer",
|
"instanceShell.leftDrawer.pin": "Pin left drawer",
|
||||||
"instanceShell.leftDrawer.unpin": "Unpin left drawer",
|
"instanceShell.leftDrawer.unpin": "Unpin left drawer",
|
||||||
"instanceShell.leftDrawer.toggle.pinned": "Left drawer pinned",
|
"instanceShell.leftDrawer.toggle.pinned": "Left drawer pinned",
|
||||||
@@ -107,6 +106,8 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "Cancel",
|
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "Cancel",
|
||||||
"instanceShell.rightPanel.toast.saveSuccess": "File saved successfully",
|
"instanceShell.rightPanel.toast.saveSuccess": "File saved successfully",
|
||||||
"instanceShell.rightPanel.toast.saveError": "Failed to save file",
|
"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": "Session Changes",
|
||||||
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Files modified in the current session. Shows additions and deletions for each file.",
|
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Files modified in the current session. Shows additions and deletions for each file.",
|
||||||
"instanceShell.rightPanel.sections.plan": "Plan",
|
"instanceShell.rightPanel.sections.plan": "Plan",
|
||||||
@@ -150,6 +151,12 @@ export const instanceMessages = {
|
|||||||
"instanceShell.plan.noSessionSelected": "Select a session to view plan.",
|
"instanceShell.plan.noSessionSelected": "Select a session to view plan.",
|
||||||
"instanceShell.plan.empty": "Nothing planned yet.",
|
"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.empty": "No background processes.",
|
||||||
"instanceShell.backgroundProcesses.status": "Status: {status}",
|
"instanceShell.backgroundProcesses.status": "Status: {status}",
|
||||||
"instanceShell.backgroundProcesses.output": "Output: {sizeKb}KB",
|
"instanceShell.backgroundProcesses.output": "Output: {sizeKb}KB",
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ export const remoteAccessMessages = {
|
|||||||
"remoteAccess.sections.addresses.help": "Launch or scan from another machine to hand over control.",
|
"remoteAccess.sections.addresses.help": "Launch or scan from another machine to hand over control.",
|
||||||
"remoteAccess.addresses.loading": "Loading addresses…",
|
"remoteAccess.addresses.loading": "Loading addresses…",
|
||||||
"remoteAccess.addresses.none": "No addresses available yet.",
|
"remoteAccess.addresses.none": "No addresses available yet.",
|
||||||
|
"remoteAccess.addresses.actions.showOther": "Show {count} other addresses",
|
||||||
|
"remoteAccess.addresses.actions.hideOther": "Hide other addresses",
|
||||||
"remoteAccess.address.scope.network": "Network",
|
"remoteAccess.address.scope.network": "Network",
|
||||||
"remoteAccess.address.scope.loopback": "Loopback",
|
"remoteAccess.address.scope.loopback": "Loopback",
|
||||||
"remoteAccess.address.scope.internal": "Internal",
|
"remoteAccess.address.scope.internal": "Internal",
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ export const sessionMessages = {
|
|||||||
"sessionList.status.working": "Working",
|
"sessionList.status.working": "Working",
|
||||||
"sessionList.status.compacting": "Compacting",
|
"sessionList.status.compacting": "Compacting",
|
||||||
"sessionList.status.idle": "Idle",
|
"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.needsPermission": "Needs Permission",
|
||||||
"sessionList.status.needsInput": "Needs Input",
|
"sessionList.status.needsInput": "Needs Input",
|
||||||
"sessionList.expand.collapseAriaLabel": "Collapse session",
|
"sessionList.expand.collapseAriaLabel": "Collapse session",
|
||||||
@@ -25,12 +29,15 @@ export const sessionMessages = {
|
|||||||
"sessionList.actions.newSession.title": "New session",
|
"sessionList.actions.newSession.title": "New session",
|
||||||
"sessionList.actions.copyId.ariaLabel": "Copy session ID",
|
"sessionList.actions.copyId.ariaLabel": "Copy session ID",
|
||||||
"sessionList.actions.copyId.title": "Copy session ID",
|
"sessionList.actions.copyId.title": "Copy session ID",
|
||||||
|
"sessionList.actions.reload.ariaLabel": "Reload session",
|
||||||
|
"sessionList.actions.reload.title": "Reload session",
|
||||||
"sessionList.actions.rename.ariaLabel": "Rename session",
|
"sessionList.actions.rename.ariaLabel": "Rename session",
|
||||||
"sessionList.actions.rename.title": "Rename session",
|
"sessionList.actions.rename.title": "Rename session",
|
||||||
"sessionList.actions.delete.ariaLabel": "Delete session",
|
"sessionList.actions.delete.ariaLabel": "Delete session",
|
||||||
"sessionList.actions.delete.title": "Delete session",
|
"sessionList.actions.delete.title": "Delete session",
|
||||||
"sessionList.copyId.success": "Session ID copied",
|
"sessionList.copyId.success": "Session ID copied",
|
||||||
"sessionList.copyId.error": "Unable to copy session ID",
|
"sessionList.copyId.error": "Unable to copy session ID",
|
||||||
|
"sessionList.reload.error": "Unable to reload session",
|
||||||
"sessionList.delete.error": "Unable to delete session",
|
"sessionList.delete.error": "Unable to delete session",
|
||||||
"sessionList.delete.title": "Delete session",
|
"sessionList.delete.title": "Delete session",
|
||||||
"sessionList.delete.confirmMessage": "Delete \"{label}\"? This cannot be undone.",
|
"sessionList.delete.confirmMessage": "Delete \"{label}\"? This cannot be undone.",
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ export const toolCallMessages = {
|
|||||||
"toolCall.diff.viewMode.ariaLabel": "Diff view mode",
|
"toolCall.diff.viewMode.ariaLabel": "Diff view mode",
|
||||||
"toolCall.diff.viewMode.split": "Split",
|
"toolCall.diff.viewMode.split": "Split",
|
||||||
"toolCall.diff.viewMode.unified": "Unified",
|
"toolCall.diff.viewMode.unified": "Unified",
|
||||||
|
"toolCall.diff.switchToSplit": "Switch to split view",
|
||||||
|
"toolCall.diff.switchToUnified": "Switch to unified view",
|
||||||
|
"toolCall.diff.enableWordWrap": "Enable word wrap",
|
||||||
|
"toolCall.diff.disableWordWrap": "Disable word wrap",
|
||||||
|
"toolCall.diff.copyPatch": "Copy patch",
|
||||||
|
|
||||||
"toolCall.diagnostics.title": "Diagnostics",
|
"toolCall.diagnostics.title": "Diagnostics",
|
||||||
"toolCall.diagnostics.ariaLabel": "Diagnostics",
|
"toolCall.diagnostics.ariaLabel": "Diagnostics",
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export const instanceMessages = {
|
|||||||
|
|
||||||
"instanceShell.leftPanel.sessionsTitle": "Sesiones",
|
"instanceShell.leftPanel.sessionsTitle": "Sesiones",
|
||||||
"instanceShell.leftPanel.instanceInfo": "Info de la instancia",
|
"instanceShell.leftPanel.instanceInfo": "Info de la instancia",
|
||||||
|
|
||||||
"instanceShell.leftDrawer.pin": "Fijar panel izquierdo",
|
"instanceShell.leftDrawer.pin": "Fijar panel izquierdo",
|
||||||
"instanceShell.leftDrawer.unpin": "Desfijar panel izquierdo",
|
"instanceShell.leftDrawer.unpin": "Desfijar panel izquierdo",
|
||||||
"instanceShell.leftDrawer.toggle.pinned": "Panel izquierdo fijado",
|
"instanceShell.leftDrawer.toggle.pinned": "Panel izquierdo fijado",
|
||||||
@@ -107,6 +106,8 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "Cancelar",
|
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "Cancelar",
|
||||||
"instanceShell.rightPanel.toast.saveSuccess": "Archivo guardado exitosamente",
|
"instanceShell.rightPanel.toast.saveSuccess": "Archivo guardado exitosamente",
|
||||||
"instanceShell.rightPanel.toast.saveError": "Error al guardar el archivo",
|
"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": "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.sessionChanges.tooltip": "Archivos modificados en la sesión actual. Muestra las adiciones y eliminaciones de cada archivo.",
|
||||||
"instanceShell.rightPanel.sections.plan": "Plan",
|
"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.noSessionSelected": "Selecciona una sesión para ver el plan.",
|
||||||
"instanceShell.plan.empty": "Aún no hay nada planificado.",
|
"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.empty": "No hay procesos en segundo plano.",
|
||||||
"instanceShell.backgroundProcesses.status": "Estado: {status}",
|
"instanceShell.backgroundProcesses.status": "Estado: {status}",
|
||||||
"instanceShell.backgroundProcesses.output": "Salida: {sizeKb} KB",
|
"instanceShell.backgroundProcesses.output": "Salida: {sizeKb} KB",
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ export const remoteAccessMessages = {
|
|||||||
"remoteAccess.sections.addresses.help": "Abre o escanea desde otra máquina para transferir el control.",
|
"remoteAccess.sections.addresses.help": "Abre o escanea desde otra máquina para transferir el control.",
|
||||||
"remoteAccess.addresses.loading": "Cargando direcciones…",
|
"remoteAccess.addresses.loading": "Cargando direcciones…",
|
||||||
"remoteAccess.addresses.none": "Aún no hay direcciones disponibles.",
|
"remoteAccess.addresses.none": "Aún no hay direcciones disponibles.",
|
||||||
|
"remoteAccess.addresses.actions.showOther": "Mostrar {count} direcciones más",
|
||||||
|
"remoteAccess.addresses.actions.hideOther": "Ocultar otras direcciones",
|
||||||
"remoteAccess.address.scope.network": "Red",
|
"remoteAccess.address.scope.network": "Red",
|
||||||
"remoteAccess.address.scope.loopback": "Loopback",
|
"remoteAccess.address.scope.loopback": "Loopback",
|
||||||
"remoteAccess.address.scope.internal": "Interna",
|
"remoteAccess.address.scope.internal": "Interna",
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ export const sessionMessages = {
|
|||||||
"sessionList.status.working": "Trabajando",
|
"sessionList.status.working": "Trabajando",
|
||||||
"sessionList.status.compacting": "Compactando",
|
"sessionList.status.compacting": "Compactando",
|
||||||
"sessionList.status.idle": "Inactiva",
|
"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.needsPermission": "Requiere permiso",
|
||||||
"sessionList.status.needsInput": "Requiere entrada",
|
"sessionList.status.needsInput": "Requiere entrada",
|
||||||
"sessionList.expand.collapseAriaLabel": "Colapsar sesión",
|
"sessionList.expand.collapseAriaLabel": "Colapsar sesión",
|
||||||
@@ -25,12 +29,15 @@ export const sessionMessages = {
|
|||||||
"sessionList.actions.newSession.title": "Nueva sesión",
|
"sessionList.actions.newSession.title": "Nueva sesión",
|
||||||
"sessionList.actions.copyId.ariaLabel": "Copiar ID de sesión",
|
"sessionList.actions.copyId.ariaLabel": "Copiar ID de sesión",
|
||||||
"sessionList.actions.copyId.title": "Copiar ID de sesión",
|
"sessionList.actions.copyId.title": "Copiar ID de sesión",
|
||||||
|
"sessionList.actions.reload.ariaLabel": "Recargar sesión",
|
||||||
|
"sessionList.actions.reload.title": "Recargar sesión",
|
||||||
"sessionList.actions.rename.ariaLabel": "Renombrar sesión",
|
"sessionList.actions.rename.ariaLabel": "Renombrar sesión",
|
||||||
"sessionList.actions.rename.title": "Renombrar sesión",
|
"sessionList.actions.rename.title": "Renombrar sesión",
|
||||||
"sessionList.actions.delete.ariaLabel": "Eliminar sesión",
|
"sessionList.actions.delete.ariaLabel": "Eliminar sesión",
|
||||||
"sessionList.actions.delete.title": "Eliminar sesión",
|
"sessionList.actions.delete.title": "Eliminar sesión",
|
||||||
"sessionList.copyId.success": "ID de sesión copiado",
|
"sessionList.copyId.success": "ID de sesión copiado",
|
||||||
"sessionList.copyId.error": "No se pudo copiar el ID de sesión",
|
"sessionList.copyId.error": "No se pudo copiar el ID de sesión",
|
||||||
|
"sessionList.reload.error": "No se pudo recargar la sesión",
|
||||||
"sessionList.delete.error": "No se pudo eliminar la sesión",
|
"sessionList.delete.error": "No se pudo eliminar la sesión",
|
||||||
"sessionList.delete.title": "Eliminar sesión",
|
"sessionList.delete.title": "Eliminar sesión",
|
||||||
"sessionList.delete.confirmMessage": "¿Eliminar \"{label}\"? Esto no se puede deshacer.",
|
"sessionList.delete.confirmMessage": "¿Eliminar \"{label}\"? Esto no se puede deshacer.",
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ export const toolCallMessages = {
|
|||||||
"toolCall.diff.viewMode.ariaLabel": "Modo de vista de diff",
|
"toolCall.diff.viewMode.ariaLabel": "Modo de vista de diff",
|
||||||
"toolCall.diff.viewMode.split": "Dividida",
|
"toolCall.diff.viewMode.split": "Dividida",
|
||||||
"toolCall.diff.viewMode.unified": "Unificada",
|
"toolCall.diff.viewMode.unified": "Unificada",
|
||||||
|
"toolCall.diff.switchToSplit": "Cambiar a vista dividida",
|
||||||
|
"toolCall.diff.switchToUnified": "Cambiar a vista unificada",
|
||||||
|
"toolCall.diff.enableWordWrap": "Activar ajuste de línea",
|
||||||
|
"toolCall.diff.disableWordWrap": "Desactivar ajuste de línea",
|
||||||
|
"toolCall.diff.copyPatch": "Copiar patch",
|
||||||
|
|
||||||
"toolCall.diagnostics.title": "Diagnósticos",
|
"toolCall.diagnostics.title": "Diagnósticos",
|
||||||
"toolCall.diagnostics.ariaLabel": "Diagnósticos",
|
"toolCall.diagnostics.ariaLabel": "Diagnósticos",
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export const instanceMessages = {
|
|||||||
|
|
||||||
"instanceShell.leftPanel.sessionsTitle": "Sessions",
|
"instanceShell.leftPanel.sessionsTitle": "Sessions",
|
||||||
"instanceShell.leftPanel.instanceInfo": "Infos de l'instance",
|
"instanceShell.leftPanel.instanceInfo": "Infos de l'instance",
|
||||||
|
|
||||||
"instanceShell.leftDrawer.pin": "Épingler le tiroir gauche",
|
"instanceShell.leftDrawer.pin": "Épingler le tiroir gauche",
|
||||||
"instanceShell.leftDrawer.unpin": "Désépingler le tiroir gauche",
|
"instanceShell.leftDrawer.unpin": "Désépingler le tiroir gauche",
|
||||||
"instanceShell.leftDrawer.toggle.pinned": "Tiroir gauche épinglé",
|
"instanceShell.leftDrawer.toggle.pinned": "Tiroir gauche épinglé",
|
||||||
@@ -107,6 +106,8 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "Annuler",
|
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "Annuler",
|
||||||
"instanceShell.rightPanel.toast.saveSuccess": "Fichier enregistré avec succès",
|
"instanceShell.rightPanel.toast.saveSuccess": "Fichier enregistré avec succès",
|
||||||
"instanceShell.rightPanel.toast.saveError": "Échec de l'enregistrement du fichier",
|
"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": "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.sessionChanges.tooltip": "Fichiers modifiés dans la session actuelle. Affiche les ajouts et suppressions pour chaque fichier.",
|
||||||
"instanceShell.rightPanel.sections.plan": "Plan",
|
"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.noSessionSelected": "Sélectionnez une session pour voir le plan.",
|
||||||
"instanceShell.plan.empty": "Aucun plan pour l'instant.",
|
"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.empty": "Aucun processus en arrière-plan.",
|
||||||
"instanceShell.backgroundProcesses.status": "Statut : {status}",
|
"instanceShell.backgroundProcesses.status": "Statut : {status}",
|
||||||
"instanceShell.backgroundProcesses.output": "Sortie : {sizeKb}KB",
|
"instanceShell.backgroundProcesses.output": "Sortie : {sizeKb}KB",
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ export const remoteAccessMessages = {
|
|||||||
"remoteAccess.sections.addresses.help": "Lancez ou scannez depuis une autre machine pour passer le contrôle.",
|
"remoteAccess.sections.addresses.help": "Lancez ou scannez depuis une autre machine pour passer le contrôle.",
|
||||||
"remoteAccess.addresses.loading": "Chargement des adresses…",
|
"remoteAccess.addresses.loading": "Chargement des adresses…",
|
||||||
"remoteAccess.addresses.none": "Aucune adresse disponible pour le moment.",
|
"remoteAccess.addresses.none": "Aucune adresse disponible pour le moment.",
|
||||||
|
"remoteAccess.addresses.actions.showOther": "Afficher {count} autres adresses",
|
||||||
|
"remoteAccess.addresses.actions.hideOther": "Masquer les autres adresses",
|
||||||
"remoteAccess.address.scope.network": "Réseau",
|
"remoteAccess.address.scope.network": "Réseau",
|
||||||
"remoteAccess.address.scope.loopback": "Boucle locale",
|
"remoteAccess.address.scope.loopback": "Boucle locale",
|
||||||
"remoteAccess.address.scope.internal": "Interne",
|
"remoteAccess.address.scope.internal": "Interne",
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ export const sessionMessages = {
|
|||||||
"sessionList.status.working": "En cours",
|
"sessionList.status.working": "En cours",
|
||||||
"sessionList.status.compacting": "Compactage",
|
"sessionList.status.compacting": "Compactage",
|
||||||
"sessionList.status.idle": "Inactif",
|
"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.needsPermission": "Autorisation requise",
|
||||||
"sessionList.status.needsInput": "Entrée requise",
|
"sessionList.status.needsInput": "Entrée requise",
|
||||||
"sessionList.expand.collapseAriaLabel": "Réduire la session",
|
"sessionList.expand.collapseAriaLabel": "Réduire la session",
|
||||||
@@ -25,12 +29,15 @@ export const sessionMessages = {
|
|||||||
"sessionList.actions.newSession.title": "Nouvelle session",
|
"sessionList.actions.newSession.title": "Nouvelle session",
|
||||||
"sessionList.actions.copyId.ariaLabel": "Copier l'ID de session",
|
"sessionList.actions.copyId.ariaLabel": "Copier l'ID de session",
|
||||||
"sessionList.actions.copyId.title": "Copier l'ID de session",
|
"sessionList.actions.copyId.title": "Copier l'ID de session",
|
||||||
|
"sessionList.actions.reload.ariaLabel": "Recharger la session",
|
||||||
|
"sessionList.actions.reload.title": "Recharger la session",
|
||||||
"sessionList.actions.rename.ariaLabel": "Renommer la session",
|
"sessionList.actions.rename.ariaLabel": "Renommer la session",
|
||||||
"sessionList.actions.rename.title": "Renommer la session",
|
"sessionList.actions.rename.title": "Renommer la session",
|
||||||
"sessionList.actions.delete.ariaLabel": "Supprimer la session",
|
"sessionList.actions.delete.ariaLabel": "Supprimer la session",
|
||||||
"sessionList.actions.delete.title": "Supprimer la session",
|
"sessionList.actions.delete.title": "Supprimer la session",
|
||||||
"sessionList.copyId.success": "ID de session copié",
|
"sessionList.copyId.success": "ID de session copié",
|
||||||
"sessionList.copyId.error": "Impossible de copier l'ID de session",
|
"sessionList.copyId.error": "Impossible de copier l'ID de session",
|
||||||
|
"sessionList.reload.error": "Impossible de recharger la session",
|
||||||
"sessionList.delete.error": "Impossible de supprimer la session",
|
"sessionList.delete.error": "Impossible de supprimer la session",
|
||||||
"sessionList.delete.title": "Supprimer la session",
|
"sessionList.delete.title": "Supprimer la session",
|
||||||
"sessionList.delete.confirmMessage": "Supprimer \"{label}\" ? Cette action est irréversible.",
|
"sessionList.delete.confirmMessage": "Supprimer \"{label}\" ? Cette action est irréversible.",
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ export const toolCallMessages = {
|
|||||||
"toolCall.diff.viewMode.ariaLabel": "Mode d'affichage du diff",
|
"toolCall.diff.viewMode.ariaLabel": "Mode d'affichage du diff",
|
||||||
"toolCall.diff.viewMode.split": "Côte à côte",
|
"toolCall.diff.viewMode.split": "Côte à côte",
|
||||||
"toolCall.diff.viewMode.unified": "Unifié",
|
"toolCall.diff.viewMode.unified": "Unifié",
|
||||||
|
"toolCall.diff.switchToSplit": "Passer à la vue côte à côte",
|
||||||
|
"toolCall.diff.switchToUnified": "Passer à la vue unifiée",
|
||||||
|
"toolCall.diff.enableWordWrap": "Activer le retour à la ligne",
|
||||||
|
"toolCall.diff.disableWordWrap": "Désactiver le retour à la ligne",
|
||||||
|
"toolCall.diff.copyPatch": "Copier le patch",
|
||||||
|
|
||||||
"toolCall.diagnostics.title": "Diagnostics",
|
"toolCall.diagnostics.title": "Diagnostics",
|
||||||
"toolCall.diagnostics.ariaLabel": "Diagnostics",
|
"toolCall.diagnostics.ariaLabel": "Diagnostics",
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export const instanceMessages = {
|
|||||||
|
|
||||||
"instanceShell.leftPanel.sessionsTitle": "סשנים",
|
"instanceShell.leftPanel.sessionsTitle": "סשנים",
|
||||||
"instanceShell.leftPanel.instanceInfo": "מידע על המופע",
|
"instanceShell.leftPanel.instanceInfo": "מידע על המופע",
|
||||||
|
|
||||||
"instanceShell.leftDrawer.pin": "נעץ מגירה שמאלית",
|
"instanceShell.leftDrawer.pin": "נעץ מגירה שמאלית",
|
||||||
"instanceShell.leftDrawer.unpin": "שחרר נעיצת מגירה שמאלית",
|
"instanceShell.leftDrawer.unpin": "שחרר נעיצת מגירה שמאלית",
|
||||||
"instanceShell.leftDrawer.toggle.pinned": "המגירה השמאלית נעוצה",
|
"instanceShell.leftDrawer.toggle.pinned": "המגירה השמאלית נעוצה",
|
||||||
@@ -107,6 +106,8 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "בטל",
|
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "בטל",
|
||||||
"instanceShell.rightPanel.toast.saveSuccess": "הקובץ נשמר בהצלחה",
|
"instanceShell.rightPanel.toast.saveSuccess": "הקובץ נשמר בהצלחה",
|
||||||
"instanceShell.rightPanel.toast.saveError": "כשלון בשמירת הקובץ",
|
"instanceShell.rightPanel.toast.saveError": "כשלון בשמירת הקובץ",
|
||||||
|
"instanceShell.rightPanel.sections.yoloMode": "מצב Yolo",
|
||||||
|
"instanceShell.rightPanel.sections.yoloMode.tooltip": "מאשר אוטומטית בקשות הרשאה עבור הסשן הנוכחי. השתמשו בזה רק אם אתם סומכים על הכלים שרצים.",
|
||||||
"instanceShell.rightPanel.sections.sessionChanges": "שינויי סשן",
|
"instanceShell.rightPanel.sections.sessionChanges": "שינויי סשן",
|
||||||
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "קבצים שהשתנו בסשן הנוכחי. מציג הוספות ומחיקות לכל קובץ.",
|
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "קבצים שהשתנו בסשן הנוכחי. מציג הוספות ומחיקות לכל קובץ.",
|
||||||
"instanceShell.rightPanel.sections.plan": "תוכנית",
|
"instanceShell.rightPanel.sections.plan": "תוכנית",
|
||||||
@@ -148,6 +149,12 @@ export const instanceMessages = {
|
|||||||
"instanceShell.plan.noSessionSelected": "בחר סשן לצפייה בתוכנית.",
|
"instanceShell.plan.noSessionSelected": "בחר סשן לצפייה בתוכנית.",
|
||||||
"instanceShell.plan.empty": "עדיין לא תוכנן דבר.",
|
"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.empty": "אין תהליכי רקע.",
|
||||||
"instanceShell.backgroundProcesses.status": "סטטוס: {status}",
|
"instanceShell.backgroundProcesses.status": "סטטוס: {status}",
|
||||||
"instanceShell.backgroundProcesses.output": "פלט: {sizeKb}KB",
|
"instanceShell.backgroundProcesses.output": "פלט: {sizeKb}KB",
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ export const remoteAccessMessages = {
|
|||||||
"remoteAccess.sections.addresses.help": "הפעל או סרוק ממכונה אחרת להעברת שליטה.",
|
"remoteAccess.sections.addresses.help": "הפעל או סרוק ממכונה אחרת להעברת שליטה.",
|
||||||
"remoteAccess.addresses.loading": "טוען כתובות…",
|
"remoteAccess.addresses.loading": "טוען כתובות…",
|
||||||
"remoteAccess.addresses.none": "אין כתובות זמינות עדיין.",
|
"remoteAccess.addresses.none": "אין כתובות זמינות עדיין.",
|
||||||
|
"remoteAccess.addresses.actions.showOther": "הצג עוד {count} כתובות",
|
||||||
|
"remoteAccess.addresses.actions.hideOther": "הסתר כתובות נוספות",
|
||||||
"remoteAccess.address.scope.network": "רשת",
|
"remoteAccess.address.scope.network": "רשת",
|
||||||
"remoteAccess.address.scope.loopback": "לולאה מקומית",
|
"remoteAccess.address.scope.loopback": "לולאה מקומית",
|
||||||
"remoteAccess.address.scope.internal": "פנימי",
|
"remoteAccess.address.scope.internal": "פנימי",
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ export const sessionMessages = {
|
|||||||
"sessionList.status.working": "עובד",
|
"sessionList.status.working": "עובד",
|
||||||
"sessionList.status.compacting": "מסכם",
|
"sessionList.status.compacting": "מסכם",
|
||||||
"sessionList.status.idle": "מוכן",
|
"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.needsPermission": "נדרש אישור",
|
||||||
"sessionList.status.needsInput": "נדרש קלט",
|
"sessionList.status.needsInput": "נדרש קלט",
|
||||||
"sessionList.expand.collapseAriaLabel": "כווץ סשן",
|
"sessionList.expand.collapseAriaLabel": "כווץ סשן",
|
||||||
@@ -25,12 +29,15 @@ export const sessionMessages = {
|
|||||||
"sessionList.actions.newSession.title": "סשן חדש",
|
"sessionList.actions.newSession.title": "סשן חדש",
|
||||||
"sessionList.actions.copyId.ariaLabel": "העתק מזהה סשן",
|
"sessionList.actions.copyId.ariaLabel": "העתק מזהה סשן",
|
||||||
"sessionList.actions.copyId.title": "העתק מזהה סשן",
|
"sessionList.actions.copyId.title": "העתק מזהה סשן",
|
||||||
|
"sessionList.actions.reload.ariaLabel": "טען מחדש סשן",
|
||||||
|
"sessionList.actions.reload.title": "טען מחדש סשן",
|
||||||
"sessionList.actions.rename.ariaLabel": "שנה שם סשן",
|
"sessionList.actions.rename.ariaLabel": "שנה שם סשן",
|
||||||
"sessionList.actions.rename.title": "שנה שם סשן",
|
"sessionList.actions.rename.title": "שנה שם סשן",
|
||||||
"sessionList.actions.delete.ariaLabel": "מחק סשן",
|
"sessionList.actions.delete.ariaLabel": "מחק סשן",
|
||||||
"sessionList.actions.delete.title": "מחק סשן",
|
"sessionList.actions.delete.title": "מחק סשן",
|
||||||
"sessionList.copyId.success": "מזהה סשן הועתק",
|
"sessionList.copyId.success": "מזהה סשן הועתק",
|
||||||
"sessionList.copyId.error": "לא ניתן להעתיק מזהה סשן",
|
"sessionList.copyId.error": "לא ניתן להעתיק מזהה סשן",
|
||||||
|
"sessionList.reload.error": "לא ניתן לטעון מחדש את הסשן",
|
||||||
"sessionList.delete.error": "לא ניתן למחוק סשן",
|
"sessionList.delete.error": "לא ניתן למחוק סשן",
|
||||||
"sessionList.delete.title": "מחק סשן",
|
"sessionList.delete.title": "מחק סשן",
|
||||||
"sessionList.delete.confirmMessage": "למחוק את \"{label}\"? לא ניתן לבטל פעולה זו.",
|
"sessionList.delete.confirmMessage": "למחוק את \"{label}\"? לא ניתן לבטל פעולה זו.",
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ export const toolCallMessages = {
|
|||||||
"toolCall.diff.viewMode.ariaLabel": "מצב תצוגת diff",
|
"toolCall.diff.viewMode.ariaLabel": "מצב תצוגת diff",
|
||||||
"toolCall.diff.viewMode.split": "מפוצל",
|
"toolCall.diff.viewMode.split": "מפוצל",
|
||||||
"toolCall.diff.viewMode.unified": "מאוחד",
|
"toolCall.diff.viewMode.unified": "מאוחד",
|
||||||
|
"toolCall.diff.switchToSplit": "עבור לתצוגה מפוצלת",
|
||||||
|
"toolCall.diff.switchToUnified": "עבור לתצוגה מאוחדת",
|
||||||
|
"toolCall.diff.enableWordWrap": "הפעל גלישת מילים",
|
||||||
|
"toolCall.diff.disableWordWrap": "כבה גלישת מילים",
|
||||||
|
"toolCall.diff.copyPatch": "העתק patch",
|
||||||
|
|
||||||
"toolCall.diagnostics.title": "אבחון",
|
"toolCall.diagnostics.title": "אבחון",
|
||||||
"toolCall.diagnostics.ariaLabel": "אבחון",
|
"toolCall.diagnostics.ariaLabel": "אבחון",
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export const instanceMessages = {
|
|||||||
|
|
||||||
"instanceShell.leftPanel.sessionsTitle": "セッション",
|
"instanceShell.leftPanel.sessionsTitle": "セッション",
|
||||||
"instanceShell.leftPanel.instanceInfo": "インスタンス情報",
|
"instanceShell.leftPanel.instanceInfo": "インスタンス情報",
|
||||||
|
|
||||||
"instanceShell.leftDrawer.pin": "左ドロワーを固定",
|
"instanceShell.leftDrawer.pin": "左ドロワーを固定",
|
||||||
"instanceShell.leftDrawer.unpin": "左ドロワーの固定を解除",
|
"instanceShell.leftDrawer.unpin": "左ドロワーの固定を解除",
|
||||||
"instanceShell.leftDrawer.toggle.pinned": "左ドロワーを固定しました",
|
"instanceShell.leftDrawer.toggle.pinned": "左ドロワーを固定しました",
|
||||||
@@ -107,6 +106,8 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "キャンセル",
|
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "キャンセル",
|
||||||
"instanceShell.rightPanel.toast.saveSuccess": "ファイルを保存しました",
|
"instanceShell.rightPanel.toast.saveSuccess": "ファイルを保存しました",
|
||||||
"instanceShell.rightPanel.toast.saveError": "ファイルの保存に失敗しました",
|
"instanceShell.rightPanel.toast.saveError": "ファイルの保存に失敗しました",
|
||||||
|
"instanceShell.rightPanel.sections.yoloMode": "Yoloモード",
|
||||||
|
"instanceShell.rightPanel.sections.yoloMode.tooltip": "現在のセッションの権限リクエストを自動承認します。実行中のツールを信頼できる場合にのみ使用してください。",
|
||||||
"instanceShell.rightPanel.sections.sessionChanges": "セッション変更",
|
"instanceShell.rightPanel.sections.sessionChanges": "セッション変更",
|
||||||
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "現在のセッションで変更されたファイル。各ファイルの追加と削除を表示します。",
|
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "現在のセッションで変更されたファイル。各ファイルの追加と削除を表示します。",
|
||||||
"instanceShell.rightPanel.sections.plan": "計画",
|
"instanceShell.rightPanel.sections.plan": "計画",
|
||||||
@@ -140,6 +141,12 @@ export const instanceMessages = {
|
|||||||
"instanceShell.plan.noSessionSelected": "計画を表示するにはセッションを選択してください。",
|
"instanceShell.plan.noSessionSelected": "計画を表示するにはセッションを選択してください。",
|
||||||
"instanceShell.plan.empty": "まだ計画はありません。",
|
"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.empty": "バックグラウンドプロセスはありません。",
|
||||||
"instanceShell.backgroundProcesses.status": "状態: {status}",
|
"instanceShell.backgroundProcesses.status": "状態: {status}",
|
||||||
"instanceShell.backgroundProcesses.output": "出力: {sizeKb}KB",
|
"instanceShell.backgroundProcesses.output": "出力: {sizeKb}KB",
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ export const remoteAccessMessages = {
|
|||||||
"remoteAccess.sections.addresses.help": "別の端末から起動またはスキャンして操作を引き継ぎます。",
|
"remoteAccess.sections.addresses.help": "別の端末から起動またはスキャンして操作を引き継ぎます。",
|
||||||
"remoteAccess.addresses.loading": "アドレスを読み込み中…",
|
"remoteAccess.addresses.loading": "アドレスを読み込み中…",
|
||||||
"remoteAccess.addresses.none": "まだ利用可能なアドレスがありません。",
|
"remoteAccess.addresses.none": "まだ利用可能なアドレスがありません。",
|
||||||
|
"remoteAccess.addresses.actions.showOther": "他の {count} 件のアドレスを表示",
|
||||||
|
"remoteAccess.addresses.actions.hideOther": "他のアドレスを隠す",
|
||||||
"remoteAccess.address.scope.network": "ネットワーク",
|
"remoteAccess.address.scope.network": "ネットワーク",
|
||||||
"remoteAccess.address.scope.loopback": "ループバック",
|
"remoteAccess.address.scope.loopback": "ループバック",
|
||||||
"remoteAccess.address.scope.internal": "内部",
|
"remoteAccess.address.scope.internal": "内部",
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ export const sessionMessages = {
|
|||||||
"sessionList.status.working": "作業中",
|
"sessionList.status.working": "作業中",
|
||||||
"sessionList.status.compacting": "圧縮中",
|
"sessionList.status.compacting": "圧縮中",
|
||||||
"sessionList.status.idle": "待機中",
|
"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.needsPermission": "許可待ち",
|
||||||
"sessionList.status.needsInput": "入力待ち",
|
"sessionList.status.needsInput": "入力待ち",
|
||||||
"sessionList.expand.collapseAriaLabel": "セッションを折りたたむ",
|
"sessionList.expand.collapseAriaLabel": "セッションを折りたたむ",
|
||||||
@@ -25,12 +29,15 @@ export const sessionMessages = {
|
|||||||
"sessionList.actions.newSession.title": "新しいセッション",
|
"sessionList.actions.newSession.title": "新しいセッション",
|
||||||
"sessionList.actions.copyId.ariaLabel": "セッション ID をコピー",
|
"sessionList.actions.copyId.ariaLabel": "セッション ID をコピー",
|
||||||
"sessionList.actions.copyId.title": "セッション ID をコピー",
|
"sessionList.actions.copyId.title": "セッション ID をコピー",
|
||||||
|
"sessionList.actions.reload.ariaLabel": "セッションを再読み込み",
|
||||||
|
"sessionList.actions.reload.title": "セッションを再読み込み",
|
||||||
"sessionList.actions.rename.ariaLabel": "セッション名を変更",
|
"sessionList.actions.rename.ariaLabel": "セッション名を変更",
|
||||||
"sessionList.actions.rename.title": "セッション名を変更",
|
"sessionList.actions.rename.title": "セッション名を変更",
|
||||||
"sessionList.actions.delete.ariaLabel": "セッションを削除",
|
"sessionList.actions.delete.ariaLabel": "セッションを削除",
|
||||||
"sessionList.actions.delete.title": "セッションを削除",
|
"sessionList.actions.delete.title": "セッションを削除",
|
||||||
"sessionList.copyId.success": "セッション ID をコピーしました",
|
"sessionList.copyId.success": "セッション ID をコピーしました",
|
||||||
"sessionList.copyId.error": "セッション ID をコピーできません",
|
"sessionList.copyId.error": "セッション ID をコピーできません",
|
||||||
|
"sessionList.reload.error": "セッションを再読み込みできません",
|
||||||
"sessionList.delete.error": "セッションを削除できません",
|
"sessionList.delete.error": "セッションを削除できません",
|
||||||
"sessionList.delete.title": "セッションを削除",
|
"sessionList.delete.title": "セッションを削除",
|
||||||
"sessionList.delete.confirmMessage": "\"{label}\" を削除しますか?この操作は元に戻せません。",
|
"sessionList.delete.confirmMessage": "\"{label}\" を削除しますか?この操作は元に戻せません。",
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ export const toolCallMessages = {
|
|||||||
"toolCall.diff.viewMode.ariaLabel": "diff 表示モード",
|
"toolCall.diff.viewMode.ariaLabel": "diff 表示モード",
|
||||||
"toolCall.diff.viewMode.split": "分割",
|
"toolCall.diff.viewMode.split": "分割",
|
||||||
"toolCall.diff.viewMode.unified": "ユニファイド",
|
"toolCall.diff.viewMode.unified": "ユニファイド",
|
||||||
|
"toolCall.diff.switchToSplit": "分割表示に切り替え",
|
||||||
|
"toolCall.diff.switchToUnified": "ユニファイド表示に切り替え",
|
||||||
|
"toolCall.diff.enableWordWrap": "折り返しを有効化",
|
||||||
|
"toolCall.diff.disableWordWrap": "折り返しを無効化",
|
||||||
|
"toolCall.diff.copyPatch": "パッチをコピー",
|
||||||
|
|
||||||
"toolCall.diagnostics.title": "診断",
|
"toolCall.diagnostics.title": "診断",
|
||||||
"toolCall.diagnostics.ariaLabel": "診断",
|
"toolCall.diagnostics.ariaLabel": "診断",
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export const instanceMessages = {
|
|||||||
|
|
||||||
"instanceShell.leftPanel.sessionsTitle": "Сессии",
|
"instanceShell.leftPanel.sessionsTitle": "Сессии",
|
||||||
"instanceShell.leftPanel.instanceInfo": "Информация об экземпляре",
|
"instanceShell.leftPanel.instanceInfo": "Информация об экземпляре",
|
||||||
|
|
||||||
"instanceShell.leftDrawer.pin": "Закрепить левую панель",
|
"instanceShell.leftDrawer.pin": "Закрепить левую панель",
|
||||||
"instanceShell.leftDrawer.unpin": "Открепить левую панель",
|
"instanceShell.leftDrawer.unpin": "Открепить левую панель",
|
||||||
"instanceShell.leftDrawer.toggle.pinned": "Левая панель закреплена",
|
"instanceShell.leftDrawer.toggle.pinned": "Левая панель закреплена",
|
||||||
@@ -107,6 +106,8 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "Отмена",
|
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "Отмена",
|
||||||
"instanceShell.rightPanel.toast.saveSuccess": "Файл успешно сохранён",
|
"instanceShell.rightPanel.toast.saveSuccess": "Файл успешно сохранён",
|
||||||
"instanceShell.rightPanel.toast.saveError": "Не удалось сохранить файл",
|
"instanceShell.rightPanel.toast.saveError": "Не удалось сохранить файл",
|
||||||
|
"instanceShell.rightPanel.sections.yoloMode": "Режим Yolo",
|
||||||
|
"instanceShell.rightPanel.sections.yoloMode.tooltip": "Автоматически одобряет запросы разрешений для текущей сессии. Включайте только если доверяете запускаемым инструментам.",
|
||||||
"instanceShell.rightPanel.sections.sessionChanges": "Изменения сессии",
|
"instanceShell.rightPanel.sections.sessionChanges": "Изменения сессии",
|
||||||
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Файлы, измененные в текущей сессии. Показывает добавления и удаления для каждого файла.",
|
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Файлы, измененные в текущей сессии. Показывает добавления и удаления для каждого файла.",
|
||||||
"instanceShell.rightPanel.sections.plan": "План",
|
"instanceShell.rightPanel.sections.plan": "План",
|
||||||
@@ -140,6 +141,12 @@ export const instanceMessages = {
|
|||||||
"instanceShell.plan.noSessionSelected": "Выберите сессию, чтобы просмотреть план.",
|
"instanceShell.plan.noSessionSelected": "Выберите сессию, чтобы просмотреть план.",
|
||||||
"instanceShell.plan.empty": "Пока ничего не запланировано.",
|
"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.empty": "Нет фоновых процессов.",
|
||||||
"instanceShell.backgroundProcesses.status": "Статус: {status}",
|
"instanceShell.backgroundProcesses.status": "Статус: {status}",
|
||||||
"instanceShell.backgroundProcesses.output": "Вывод: {sizeKb}KB",
|
"instanceShell.backgroundProcesses.output": "Вывод: {sizeKb}KB",
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ export const remoteAccessMessages = {
|
|||||||
"remoteAccess.sections.addresses.help": "Откройте или отсканируйте с другой машины, чтобы передать управление.",
|
"remoteAccess.sections.addresses.help": "Откройте или отсканируйте с другой машины, чтобы передать управление.",
|
||||||
"remoteAccess.addresses.loading": "Загрузка адресов…",
|
"remoteAccess.addresses.loading": "Загрузка адресов…",
|
||||||
"remoteAccess.addresses.none": "Пока нет доступных адресов.",
|
"remoteAccess.addresses.none": "Пока нет доступных адресов.",
|
||||||
|
"remoteAccess.addresses.actions.showOther": "Показать еще {count} адресов",
|
||||||
|
"remoteAccess.addresses.actions.hideOther": "Скрыть остальные адреса",
|
||||||
"remoteAccess.address.scope.network": "Сеть",
|
"remoteAccess.address.scope.network": "Сеть",
|
||||||
"remoteAccess.address.scope.loopback": "Loopback",
|
"remoteAccess.address.scope.loopback": "Loopback",
|
||||||
"remoteAccess.address.scope.internal": "Внутренний",
|
"remoteAccess.address.scope.internal": "Внутренний",
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ export const sessionMessages = {
|
|||||||
"sessionList.status.working": "Работает",
|
"sessionList.status.working": "Работает",
|
||||||
"sessionList.status.compacting": "Компактация",
|
"sessionList.status.compacting": "Компактация",
|
||||||
"sessionList.status.idle": "Простой",
|
"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.needsPermission": "Требуется разрешение",
|
||||||
"sessionList.status.needsInput": "Требуется ввод",
|
"sessionList.status.needsInput": "Требуется ввод",
|
||||||
"sessionList.expand.collapseAriaLabel": "Свернуть сессию",
|
"sessionList.expand.collapseAriaLabel": "Свернуть сессию",
|
||||||
@@ -25,12 +29,15 @@ export const sessionMessages = {
|
|||||||
"sessionList.actions.newSession.title": "Новая сессия",
|
"sessionList.actions.newSession.title": "Новая сессия",
|
||||||
"sessionList.actions.copyId.ariaLabel": "Скопировать ID сессии",
|
"sessionList.actions.copyId.ariaLabel": "Скопировать ID сессии",
|
||||||
"sessionList.actions.copyId.title": "Скопировать ID сессии",
|
"sessionList.actions.copyId.title": "Скопировать ID сессии",
|
||||||
|
"sessionList.actions.reload.ariaLabel": "Обновить сессию",
|
||||||
|
"sessionList.actions.reload.title": "Обновить сессию",
|
||||||
"sessionList.actions.rename.ariaLabel": "Переименовать сессию",
|
"sessionList.actions.rename.ariaLabel": "Переименовать сессию",
|
||||||
"sessionList.actions.rename.title": "Переименовать сессию",
|
"sessionList.actions.rename.title": "Переименовать сессию",
|
||||||
"sessionList.actions.delete.ariaLabel": "Удалить сессию",
|
"sessionList.actions.delete.ariaLabel": "Удалить сессию",
|
||||||
"sessionList.actions.delete.title": "Удалить сессию",
|
"sessionList.actions.delete.title": "Удалить сессию",
|
||||||
"sessionList.copyId.success": "ID сессии скопирован",
|
"sessionList.copyId.success": "ID сессии скопирован",
|
||||||
"sessionList.copyId.error": "Не удалось скопировать ID сессии",
|
"sessionList.copyId.error": "Не удалось скопировать ID сессии",
|
||||||
|
"sessionList.reload.error": "Не удалось обновить сессию",
|
||||||
"sessionList.delete.error": "Не удалось удалить сессию",
|
"sessionList.delete.error": "Не удалось удалить сессию",
|
||||||
"sessionList.delete.title": "Удалить сессию",
|
"sessionList.delete.title": "Удалить сессию",
|
||||||
"sessionList.delete.confirmMessage": "Удалить \"{label}\"? Это действие нельзя отменить.",
|
"sessionList.delete.confirmMessage": "Удалить \"{label}\"? Это действие нельзя отменить.",
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ export const toolCallMessages = {
|
|||||||
"toolCall.diff.viewMode.ariaLabel": "Режим просмотра diff",
|
"toolCall.diff.viewMode.ariaLabel": "Режим просмотра diff",
|
||||||
"toolCall.diff.viewMode.split": "Раздельный",
|
"toolCall.diff.viewMode.split": "Раздельный",
|
||||||
"toolCall.diff.viewMode.unified": "Единый",
|
"toolCall.diff.viewMode.unified": "Единый",
|
||||||
|
"toolCall.diff.switchToSplit": "Переключить на раздельный вид",
|
||||||
|
"toolCall.diff.switchToUnified": "Переключить на единый вид",
|
||||||
|
"toolCall.diff.enableWordWrap": "Включить перенос слов",
|
||||||
|
"toolCall.diff.disableWordWrap": "Выключить перенос слов",
|
||||||
|
"toolCall.diff.copyPatch": "Скопировать patch",
|
||||||
|
|
||||||
"toolCall.diagnostics.title": "Диагностика",
|
"toolCall.diagnostics.title": "Диагностика",
|
||||||
"toolCall.diagnostics.ariaLabel": "Диагностика",
|
"toolCall.diagnostics.ariaLabel": "Диагностика",
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ export const instanceMessages = {
|
|||||||
|
|
||||||
"instanceShell.leftPanel.sessionsTitle": "会话",
|
"instanceShell.leftPanel.sessionsTitle": "会话",
|
||||||
"instanceShell.leftPanel.instanceInfo": "实例信息",
|
"instanceShell.leftPanel.instanceInfo": "实例信息",
|
||||||
|
|
||||||
"instanceShell.leftDrawer.pin": "固定左侧抽屉",
|
"instanceShell.leftDrawer.pin": "固定左侧抽屉",
|
||||||
"instanceShell.leftDrawer.unpin": "取消固定左侧抽屉",
|
"instanceShell.leftDrawer.unpin": "取消固定左侧抽屉",
|
||||||
"instanceShell.leftDrawer.toggle.pinned": "左侧抽屉已固定",
|
"instanceShell.leftDrawer.toggle.pinned": "左侧抽屉已固定",
|
||||||
@@ -107,6 +106,8 @@ export const instanceMessages = {
|
|||||||
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "取消",
|
"instanceShell.rightPanel.actions.refreshDirty.cancelLabel": "取消",
|
||||||
"instanceShell.rightPanel.toast.saveSuccess": "文件保存成功",
|
"instanceShell.rightPanel.toast.saveSuccess": "文件保存成功",
|
||||||
"instanceShell.rightPanel.toast.saveError": "保存文件失败",
|
"instanceShell.rightPanel.toast.saveError": "保存文件失败",
|
||||||
|
"instanceShell.rightPanel.sections.yoloMode": "Yolo 模式",
|
||||||
|
"instanceShell.rightPanel.sections.yoloMode.tooltip": "自动批准当前会话的权限请求。仅在你信任正在运行的工具时启用。",
|
||||||
"instanceShell.rightPanel.sections.sessionChanges": "会话更改",
|
"instanceShell.rightPanel.sections.sessionChanges": "会话更改",
|
||||||
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "当前会话中修改的文件。显示每个文件的添加和删除。",
|
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "当前会话中修改的文件。显示每个文件的添加和删除。",
|
||||||
"instanceShell.rightPanel.sections.plan": "计划",
|
"instanceShell.rightPanel.sections.plan": "计划",
|
||||||
@@ -140,6 +141,12 @@ export const instanceMessages = {
|
|||||||
"instanceShell.plan.noSessionSelected": "选择会话以查看计划。",
|
"instanceShell.plan.noSessionSelected": "选择会话以查看计划。",
|
||||||
"instanceShell.plan.empty": "暂无计划。",
|
"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.empty": "没有后台进程。",
|
||||||
"instanceShell.backgroundProcesses.status": "状态:{status}",
|
"instanceShell.backgroundProcesses.status": "状态:{status}",
|
||||||
"instanceShell.backgroundProcesses.output": "输出:{sizeKb}KB",
|
"instanceShell.backgroundProcesses.output": "输出:{sizeKb}KB",
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ export const remoteAccessMessages = {
|
|||||||
"remoteAccess.sections.addresses.help": "从另一台设备打开或扫描,以接管控制权。",
|
"remoteAccess.sections.addresses.help": "从另一台设备打开或扫描,以接管控制权。",
|
||||||
"remoteAccess.addresses.loading": "正在加载地址…",
|
"remoteAccess.addresses.loading": "正在加载地址…",
|
||||||
"remoteAccess.addresses.none": "暂时没有可用地址。",
|
"remoteAccess.addresses.none": "暂时没有可用地址。",
|
||||||
|
"remoteAccess.addresses.actions.showOther": "显示另外 {count} 个地址",
|
||||||
|
"remoteAccess.addresses.actions.hideOther": "隐藏其他地址",
|
||||||
"remoteAccess.address.scope.network": "网络",
|
"remoteAccess.address.scope.network": "网络",
|
||||||
"remoteAccess.address.scope.loopback": "回环",
|
"remoteAccess.address.scope.loopback": "回环",
|
||||||
"remoteAccess.address.scope.internal": "内部",
|
"remoteAccess.address.scope.internal": "内部",
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ export const sessionMessages = {
|
|||||||
"sessionList.status.working": "工作中",
|
"sessionList.status.working": "工作中",
|
||||||
"sessionList.status.compacting": "压缩中",
|
"sessionList.status.compacting": "压缩中",
|
||||||
"sessionList.status.idle": "空闲",
|
"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.needsPermission": "需要权限",
|
||||||
"sessionList.status.needsInput": "需要输入",
|
"sessionList.status.needsInput": "需要输入",
|
||||||
"sessionList.expand.collapseAriaLabel": "折叠会话",
|
"sessionList.expand.collapseAriaLabel": "折叠会话",
|
||||||
@@ -25,12 +29,15 @@ export const sessionMessages = {
|
|||||||
"sessionList.actions.newSession.title": "新建会话",
|
"sessionList.actions.newSession.title": "新建会话",
|
||||||
"sessionList.actions.copyId.ariaLabel": "复制会话 ID",
|
"sessionList.actions.copyId.ariaLabel": "复制会话 ID",
|
||||||
"sessionList.actions.copyId.title": "复制会话 ID",
|
"sessionList.actions.copyId.title": "复制会话 ID",
|
||||||
|
"sessionList.actions.reload.ariaLabel": "重新加载会话",
|
||||||
|
"sessionList.actions.reload.title": "重新加载会话",
|
||||||
"sessionList.actions.rename.ariaLabel": "重命名会话",
|
"sessionList.actions.rename.ariaLabel": "重命名会话",
|
||||||
"sessionList.actions.rename.title": "重命名会话",
|
"sessionList.actions.rename.title": "重命名会话",
|
||||||
"sessionList.actions.delete.ariaLabel": "删除会话",
|
"sessionList.actions.delete.ariaLabel": "删除会话",
|
||||||
"sessionList.actions.delete.title": "删除会话",
|
"sessionList.actions.delete.title": "删除会话",
|
||||||
"sessionList.copyId.success": "已复制会话 ID",
|
"sessionList.copyId.success": "已复制会话 ID",
|
||||||
"sessionList.copyId.error": "无法复制会话 ID",
|
"sessionList.copyId.error": "无法复制会话 ID",
|
||||||
|
"sessionList.reload.error": "无法重新加载会话",
|
||||||
"sessionList.delete.error": "无法删除会话",
|
"sessionList.delete.error": "无法删除会话",
|
||||||
"sessionList.delete.title": "删除会话",
|
"sessionList.delete.title": "删除会话",
|
||||||
"sessionList.delete.confirmMessage": "删除“{label}”?此操作无法撤销。",
|
"sessionList.delete.confirmMessage": "删除“{label}”?此操作无法撤销。",
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ export const toolCallMessages = {
|
|||||||
"toolCall.diff.viewMode.ariaLabel": "Diff 视图模式",
|
"toolCall.diff.viewMode.ariaLabel": "Diff 视图模式",
|
||||||
"toolCall.diff.viewMode.split": "分栏",
|
"toolCall.diff.viewMode.split": "分栏",
|
||||||
"toolCall.diff.viewMode.unified": "统一",
|
"toolCall.diff.viewMode.unified": "统一",
|
||||||
|
"toolCall.diff.switchToSplit": "切换到分栏视图",
|
||||||
|
"toolCall.diff.switchToUnified": "切换到统一视图",
|
||||||
|
"toolCall.diff.enableWordWrap": "启用自动换行",
|
||||||
|
"toolCall.diff.disableWordWrap": "禁用自动换行",
|
||||||
|
"toolCall.diff.copyPatch": "复制补丁",
|
||||||
|
|
||||||
"toolCall.diagnostics.title": "诊断",
|
"toolCall.diagnostics.title": "诊断",
|
||||||
"toolCall.diagnostics.ariaLabel": "诊断",
|
"toolCall.diagnostics.ariaLabel": "诊断",
|
||||||
|
|||||||
@@ -120,14 +120,7 @@ function resolveLanguage(token: string): { canonical: string | null; raw: string
|
|||||||
return { canonical: null, raw: normalized }
|
return { canonical: null, raw: normalized }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureLanguages(content: string) {
|
function collectCodeFenceLanguages(content: string): 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 = new Set<string>()
|
const foundLanguages = new Set<string>()
|
||||||
try {
|
try {
|
||||||
const tokens = marked.lexer(content) as any
|
const tokens = marked.lexer(content) as any
|
||||||
@@ -139,10 +132,44 @@ async function ensureLanguages(content: string) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch {
|
} 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
|
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
|
// Queue language loading tasks
|
||||||
for (const token of foundLanguages) {
|
for (const token of foundLanguages) {
|
||||||
const rawToken = normalizeLanguageToken(token)
|
const rawToken = normalizeLanguageToken(token)
|
||||||
|
|||||||
@@ -102,9 +102,11 @@ export function showToastNotification(payload: ToastPayload): ToastHandle {
|
|||||||
</button>
|
</button>
|
||||||
<div class="flex items-start gap-3 pr-6">
|
<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}`} />
|
<span class={`mt-1 inline-block h-2.5 w-2.5 rounded-full ${accent.badge}`} />
|
||||||
<div class="flex-1 text-sm leading-snug">
|
<div class="min-w-0 flex-1 text-sm leading-snug">
|
||||||
{payload.title && <p class={`font-semibold ${accent.headline}`}>{payload.title}</p>}
|
{payload.title && <p class={`break-words ${accent.headline} font-semibold`}>{payload.title}</p>}
|
||||||
<p class={`${accent.body} ${payload.title ? "mt-1" : ""}`}>{payload.message}</p>
|
<p class={`${accent.body} ${payload.title ? "mt-1" : ""} whitespace-pre-wrap break-words [overflow-wrap:anywhere]`}>
|
||||||
|
{payload.message}
|
||||||
|
</p>
|
||||||
{payload.action && (
|
{payload.action && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
17
packages/ui/src/lib/remote-access-addresses.test.ts
Normal file
17
packages/ui/src/lib/remote-access-addresses.test.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import assert from "node:assert/strict"
|
||||||
|
import { describe, it } from "node:test"
|
||||||
|
|
||||||
|
import { splitRemoteAddresses } from "./remote-access-addresses"
|
||||||
|
|
||||||
|
describe("splitRemoteAddresses", () => {
|
||||||
|
it("keeps the first remote address visible and collapses the rest", () => {
|
||||||
|
const result = splitRemoteAddresses([
|
||||||
|
{ ip: "127.0.0.1", family: "ipv4", scope: "loopback", remoteUrl: "https://127.0.0.1:9898" },
|
||||||
|
{ ip: "192.168.1.128", family: "ipv4", scope: "external", remoteUrl: "https://192.168.1.128:9898" },
|
||||||
|
{ ip: "172.24.96.1", family: "ipv4", scope: "external", remoteUrl: "https://172.24.96.1:9898" },
|
||||||
|
])
|
||||||
|
|
||||||
|
assert.equal(result.recommended?.ip, "192.168.1.128")
|
||||||
|
assert.deepEqual(result.hidden.map((address) => address.ip), ["172.24.96.1"])
|
||||||
|
})
|
||||||
|
})
|
||||||
14
packages/ui/src/lib/remote-access-addresses.ts
Normal file
14
packages/ui/src/lib/remote-access-addresses.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import type { NetworkAddress } from "../../../server/src/api-types"
|
||||||
|
|
||||||
|
export interface RemoteAddressGroups {
|
||||||
|
recommended: NetworkAddress | null
|
||||||
|
hidden: NetworkAddress[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function splitRemoteAddresses(addresses: NetworkAddress[]): RemoteAddressGroups {
|
||||||
|
const remoteAddresses = addresses.filter((address) => address.scope !== "loopback")
|
||||||
|
return {
|
||||||
|
recommended: remoteAddresses[0] ?? null,
|
||||||
|
hidden: remoteAddresses.slice(1),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { WorkspaceEventPayload, WorkspaceEventType } from "../../../server/src/api-types"
|
import type { WorkspaceEventPayload, WorkspaceEventType } from "../../../server/src/api-types"
|
||||||
import { serverApi } from "./api-client"
|
import { serverApi } from "./api-client"
|
||||||
|
import { getClientIdentity } from "./client-identity"
|
||||||
import { getLogger } from "./logger"
|
import { getLogger } from "./logger"
|
||||||
|
|
||||||
const RETRY_BASE_DELAY = 1000
|
const RETRY_BASE_DELAY = 1000
|
||||||
@@ -16,6 +17,7 @@ function logSse(message: string, context?: Record<string, unknown>) {
|
|||||||
|
|
||||||
class ServerEvents {
|
class ServerEvents {
|
||||||
private handlers = new Map<WorkspaceEventType | "*", Set<(event: WorkspaceEventPayload) => void>>()
|
private handlers = new Map<WorkspaceEventType | "*", Set<(event: WorkspaceEventPayload) => void>>()
|
||||||
|
private openHandlers = new Set<() => void>()
|
||||||
private source: EventSource | null = null
|
private source: EventSource | null = null
|
||||||
private retryDelay = RETRY_BASE_DELAY
|
private retryDelay = RETRY_BASE_DELAY
|
||||||
|
|
||||||
@@ -28,10 +30,24 @@ class ServerEvents {
|
|||||||
this.source.close()
|
this.source.close()
|
||||||
}
|
}
|
||||||
logSse("Connecting to backend events stream")
|
logSse("Connecting to backend events stream")
|
||||||
this.source = serverApi.connectEvents((event) => this.dispatch(event), () => this.scheduleReconnect())
|
this.source = serverApi.connectEvents(
|
||||||
|
(event) => this.dispatch(event),
|
||||||
|
() => this.scheduleReconnect(),
|
||||||
|
(payload) => {
|
||||||
|
void serverApi
|
||||||
|
.sendClientConnectionPong({
|
||||||
|
...getClientIdentity(),
|
||||||
|
pingTs: payload.ts,
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
log.error("Failed to send client connection pong", error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
this.source.onopen = () => {
|
this.source.onopen = () => {
|
||||||
logSse("Events stream connected")
|
logSse("Events stream connected")
|
||||||
this.retryDelay = RETRY_BASE_DELAY
|
this.retryDelay = RETRY_BASE_DELAY
|
||||||
|
this.openHandlers.forEach((handler) => handler())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,6 +77,11 @@ class ServerEvents {
|
|||||||
bucket.add(handler)
|
bucket.add(handler)
|
||||||
return () => bucket.delete(handler)
|
return () => bucket.delete(handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onOpen(handler: () => void): () => void {
|
||||||
|
this.openHandlers.add(handler)
|
||||||
|
return () => this.openHandlers.delete(handler)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const serverEvents = new ServerEvents()
|
export const serverEvents = new ServerEvents()
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { showToastNotification } from "../lib/notifications"
|
|||||||
import { serverApi } from "../lib/api-client"
|
import { serverApi } from "../lib/api-client"
|
||||||
import { getLogger } from "../lib/logger"
|
import { getLogger } from "../lib/logger"
|
||||||
import { formatToMimeType, getSpeechPlaybackSupport } from "../lib/speech-playback-support"
|
import { formatToMimeType, getSpeechPlaybackSupport } from "../lib/speech-playback-support"
|
||||||
|
import { serverEvents } from "../lib/server-events"
|
||||||
import { serverSettings } from "./preferences"
|
import { serverSettings } from "./preferences"
|
||||||
import { loadSpeechCapabilities, speechCapabilities } from "./speech"
|
import { loadSpeechCapabilities, speechCapabilities } from "./speech"
|
||||||
import { getActiveSession, sessions } from "./session-state"
|
import { getActiveSession, sessions } from "./session-state"
|
||||||
@@ -44,6 +45,10 @@ let currentPlayback:
|
|||||||
let queueRunner: Promise<void> | null = null
|
let queueRunner: Promise<void> | null = null
|
||||||
let playbackErrorShown = false
|
let playbackErrorShown = false
|
||||||
|
|
||||||
|
serverEvents.onOpen(() => {
|
||||||
|
void syncConversationModesToServer()
|
||||||
|
})
|
||||||
|
|
||||||
function getEntryKey(instanceId: string, sessionId: string, messageId: string, partId: string): string {
|
function getEntryKey(instanceId: string, sessionId: string, messageId: string, partId: string): string {
|
||||||
return `${instanceId}:${sessionId}:${messageId}:${partId}`
|
return `${instanceId}:${sessionId}:${messageId}:${partId}`
|
||||||
}
|
}
|
||||||
@@ -532,3 +537,12 @@ function extractLeadingSpokenBlock(text: string): string {
|
|||||||
if (!match?.[1]) return ""
|
if (!match?.[1]) return ""
|
||||||
return match[1].trim()
|
return match[1].trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function syncConversationModesToServer(): Promise<void> {
|
||||||
|
const updates: Promise<unknown>[] = []
|
||||||
|
for (const [instanceId, enabled] of conversationModeInstances()) {
|
||||||
|
if (!enabled) continue
|
||||||
|
updates.push(serverApi.updateVoiceMode(instanceId, true))
|
||||||
|
}
|
||||||
|
await Promise.allSettled(updates)
|
||||||
|
}
|
||||||
|
|||||||
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 { Message } from "../types/message"
|
||||||
import type { FileDiff } from "@opencode-ai/sdk/v2/client"
|
import type { FileDiff } from "@opencode-ai/sdk/v2/client"
|
||||||
|
|
||||||
@@ -149,12 +149,15 @@ async function fetchSessions(instanceId: string): Promise<void> {
|
|||||||
const existingStatus = existingSession?.status
|
const existingStatus = existingSession?.status
|
||||||
|
|
||||||
let status: SessionStatus
|
let status: SessionStatus
|
||||||
|
let retry = existingSession?.retry ?? null
|
||||||
if (existingStatus === "compacting") {
|
if (existingStatus === "compacting") {
|
||||||
status = "compacting"
|
status = "compacting"
|
||||||
|
retry = null
|
||||||
} else {
|
} else {
|
||||||
const rawStatus = (apiSession as any)?.status ?? statusById[apiSession.id]
|
const rawStatus = (apiSession as any)?.status ?? statusById[apiSession.id]
|
||||||
const hasType = rawStatus && typeof rawStatus === "object" && typeof rawStatus.type === "string"
|
const hasType = rawStatus && typeof rawStatus === "object" && typeof rawStatus.type === "string"
|
||||||
status = hasType ? mapSdkSessionStatus(rawStatus) : existingStatus ?? "idle"
|
status = hasType ? mapSdkSessionStatus(rawStatus) : existingStatus ?? "idle"
|
||||||
|
retry = hasType ? mapSdkSessionRetry(rawStatus) : retry
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionMap.set(apiSession.id, {
|
sessionMap.set(apiSession.id, {
|
||||||
@@ -165,6 +168,7 @@ async function fetchSessions(instanceId: string): Promise<void> {
|
|||||||
agent: existingSession?.agent ?? "",
|
agent: existingSession?.agent ?? "",
|
||||||
model: existingSession?.model ?? { providerId: "", modelId: "" },
|
model: existingSession?.model ?? { providerId: "", modelId: "" },
|
||||||
status,
|
status,
|
||||||
|
retry,
|
||||||
version: apiSession.version,
|
version: apiSession.version,
|
||||||
time: {
|
time: {
|
||||||
...apiSession.time,
|
...apiSession.time,
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import type { PermissionReplyEventPropertiesLike, PermissionRequestLike } from "
|
|||||||
import { getQuestionId, getQuestionSessionId, getRequestIdFromQuestionReply } from "../types/question"
|
import { getQuestionId, getQuestionSessionId, getRequestIdFromQuestionReply } from "../types/question"
|
||||||
import type { QuestionRequest } from "../types/question"
|
import type { QuestionRequest } from "../types/question"
|
||||||
import type { EventQuestionReplied, EventQuestionRejected } from "@opencode-ai/sdk/v2"
|
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 { sendOsNotification } from "../lib/os-notifications"
|
||||||
import { preferences } from "./preferences"
|
import { preferences } from "./preferences"
|
||||||
import {
|
import {
|
||||||
@@ -39,7 +39,14 @@ import {
|
|||||||
removeQuestionFromQueue,
|
removeQuestionFromQueue,
|
||||||
} from "./instances"
|
} from "./instances"
|
||||||
import { showAlertDialog } from "./alerts"
|
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 { ensureSessionParentExpanded, sessions, setSessions, syncInstanceSessionIndicator, withSession } from "./session-state"
|
||||||
import { normalizeMessagePart } from "./message-v2/normalizers"
|
import { normalizeMessagePart } from "./message-v2/normalizers"
|
||||||
import { updateSessionInfo } from "./message-v2/session-info"
|
import { updateSessionInfo } from "./message-v2/session-info"
|
||||||
@@ -67,6 +74,15 @@ import { handleConversationAssistantPartUpdated } from "./conversation-speech"
|
|||||||
|
|
||||||
const log = getLogger("sse")
|
const log = getLogger("sse")
|
||||||
const pendingSessionFetches = new Map<string, Promise<void>>()
|
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 {
|
function shouldSendOsNotification(kind: "needsInput" | "idle"): boolean {
|
||||||
if (typeof document === "undefined") return false
|
if (typeof document === "undefined") return false
|
||||||
@@ -131,18 +147,20 @@ interface TuiToastEvent {
|
|||||||
|
|
||||||
const ALLOWED_TOAST_VARIANTS = new Set<ToastVariant>(["info", "success", "warning", "error"])
|
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
|
let parentToExpand: string | null = null
|
||||||
|
|
||||||
withSession(instanceId, sessionId, (session) => {
|
withSession(instanceId, sessionId, (session) => {
|
||||||
const current = session.status ?? "idle"
|
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") {
|
if (current === "compacting" && status !== "compacting") {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
session.status = status
|
session.status = status
|
||||||
|
session.retry = status === "working" ? nextRetry : null
|
||||||
|
|
||||||
// Auto-expand the parent thread when a child session starts working.
|
// Auto-expand the parent thread when a child session starts working.
|
||||||
// Users can still collapse it; we only expand on the transition.
|
// 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 fetchedStatus: SessionStatus = "idle"
|
||||||
|
let fetchedRetry: SessionRetryState | null = null
|
||||||
try {
|
try {
|
||||||
let statuses: Record<string, any> = {}
|
let statuses: Record<string, any> = {}
|
||||||
try {
|
try {
|
||||||
@@ -187,11 +206,13 @@ async function fetchSessionInfo(instanceId: string, sessionId: string, directory
|
|||||||
const rawStatus = (info as any)?.status ?? statuses?.[sessionId]
|
const rawStatus = (info as any)?.status ?? statuses?.[sessionId]
|
||||||
const hasType = rawStatus && typeof rawStatus === "object" && typeof rawStatus.type === "string"
|
const hasType = rawStatus && typeof rawStatus === "object" && typeof rawStatus.type === "string"
|
||||||
fetchedStatus = hasType ? mapSdkSessionStatus(rawStatus) : "idle"
|
fetchedStatus = hasType ? mapSdkSessionStatus(rawStatus) : "idle"
|
||||||
|
fetchedRetry = hasType ? mapSdkSessionRetry(rawStatus) : null
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("Failed to fetch session status", error)
|
log.error("Failed to fetch session status", error)
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetched = createClientSession(info, instanceId, "", { providerId: "", modelId: "" }, fetchedStatus)
|
const fetched = createClientSession(info, instanceId, "", { providerId: "", modelId: "" }, fetchedStatus)
|
||||||
|
fetched.retry = fetchedRetry
|
||||||
|
|
||||||
let updatedInstanceSessions: Map<string, Session> | undefined
|
let updatedInstanceSessions: Map<string, Session> | undefined
|
||||||
let shouldExpandParent: string | null = null
|
let shouldExpandParent: string | null = null
|
||||||
@@ -205,6 +226,7 @@ async function fetchSessionInfo(instanceId: string, sessionId: string, directory
|
|||||||
agent: existing?.agent ?? fetched.agent,
|
agent: existing?.agent ?? fetched.agent,
|
||||||
model: existing?.model ?? fetched.model,
|
model: existing?.model ?? fetched.model,
|
||||||
status: existing?.status === "compacting" ? "compacting" : fetched.status,
|
status: existing?.status === "compacting" ? "compacting" : fetched.status,
|
||||||
|
retry: existing?.status === "compacting" ? null : fetched.retry,
|
||||||
pendingPermission: existing?.pendingPermission ?? fetched.pendingPermission,
|
pendingPermission: existing?.pendingPermission ?? fetched.pendingPermission,
|
||||||
pendingQuestion: existing?.pendingQuestion ?? false,
|
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 instanceSessions = sessions().get(instanceId)
|
||||||
const existing = instanceSessions?.get(sessionId)
|
const existing = instanceSessions?.get(sessionId)
|
||||||
if (existing) {
|
if (existing) {
|
||||||
if ((existing.status ?? "idle") === status) {
|
if ((existing.status ?? "idle") === status && isSameRetryState(existing.retry, retry)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
applySessionStatus(instanceId, sessionId, status)
|
applySessionStatus(instanceId, sessionId, status, retry)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,7 +278,7 @@ function ensureSessionStatus(instanceId: string, sessionId: string, status: Sess
|
|||||||
const pending = (async () => {
|
const pending = (async () => {
|
||||||
const fetched = await fetchSessionInfo(instanceId, sessionId, directory)
|
const fetched = await fetchSessionInfo(instanceId, sessionId, directory)
|
||||||
if (!fetched) return
|
if (!fetched) return
|
||||||
applySessionStatus(instanceId, sessionId, status)
|
applySessionStatus(instanceId, sessionId, status, retry)
|
||||||
})()
|
})()
|
||||||
|
|
||||||
pendingSessionFetches.set(key, pending)
|
pendingSessionFetches.set(key, pending)
|
||||||
@@ -428,6 +456,7 @@ function handleSessionUpdate(instanceId: string, event: EventSessionUpdated): vo
|
|||||||
modelId: "",
|
modelId: "",
|
||||||
},
|
},
|
||||||
status: "idle",
|
status: "idle",
|
||||||
|
retry: null,
|
||||||
version: info.version || "0",
|
version: info.version || "0",
|
||||||
time: info.time
|
time: info.time
|
||||||
? { ...info.time }
|
? { ...info.time }
|
||||||
@@ -461,6 +490,7 @@ function handleSessionUpdate(instanceId: string, event: EventSessionUpdated): vo
|
|||||||
...existingSession,
|
...existingSession,
|
||||||
title: info.title || existingSession.title,
|
title: info.title || existingSession.title,
|
||||||
status: existingSession.status ?? "idle",
|
status: existingSession.status ?? "idle",
|
||||||
|
retry: existingSession.retry ?? null,
|
||||||
time: mergedTime,
|
time: mergedTime,
|
||||||
revert: info.revert
|
revert: info.revert
|
||||||
? {
|
? {
|
||||||
@@ -532,8 +562,29 @@ function handleSessionStatus(instanceId: string, event: EventSessionStatus): voi
|
|||||||
const sessionId = event.properties?.sessionID
|
const sessionId = event.properties?.sessionID
|
||||||
if (!sessionId) return
|
if (!sessionId) return
|
||||||
|
|
||||||
const status = mapSdkSessionStatus(event.properties.status)
|
const rawStatus = event.properties.status
|
||||||
ensureSessionStatus(instanceId, sessionId, status, (event as any)?.directory)
|
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 })
|
log.info(`[SSE] Session status updated: ${sessionId}`, { status })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -547,6 +598,7 @@ function handleSessionCompacted(instanceId: string, event: EventSessionCompacted
|
|||||||
if (existing) {
|
if (existing) {
|
||||||
withSession(instanceId, sessionID, (session) => {
|
withSession(instanceId, sessionID, (session) => {
|
||||||
session.status = "working"
|
session.status = "working"
|
||||||
|
session.retry = null
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
ensureSessionStatus(instanceId, sessionID, "working", (event as any)?.directory)
|
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
|
if (session.status === status) return false
|
||||||
const previous = session.status
|
const previous = session.status
|
||||||
session.status = status
|
session.status = status
|
||||||
|
if (status !== "working") {
|
||||||
|
session.retry = null
|
||||||
|
}
|
||||||
|
|
||||||
// If a child session starts working, auto-expand its parent thread once.
|
// If a child session starts working, auto-expand its parent thread once.
|
||||||
// Users can still collapse it afterwards; we only expand on the transition.
|
// 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"
|
import { getInstanceSessionIndicatorStatusCached, sessions } from "./session-state"
|
||||||
|
|
||||||
function getSession(instanceId: string, sessionId: string): Session | null {
|
function getSession(instanceId: string, sessionId: string): Session | null {
|
||||||
@@ -14,6 +14,15 @@ export function getSessionStatus(instanceId: string, sessionId: string): Session
|
|||||||
return session.status ?? "idle"
|
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 type InstanceSessionIndicatorStatus = "permission" | SessionStatus
|
||||||
|
|
||||||
export function getInstanceSessionIndicatorStatus(instanceId: string): InstanceSessionIndicatorStatus {
|
export function getInstanceSessionIndicatorStatus(instanceId: string): InstanceSessionIndicatorStatus {
|
||||||
|
|||||||
@@ -256,6 +256,55 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.remote-address-disclosure {
|
||||||
|
border: 1px solid var(--border-base);
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--surface-primary);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-address-disclosure-trigger {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 40px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto 1fr;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-address-disclosure-label {
|
||||||
|
grid-column: 2;
|
||||||
|
justify-self: center;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-address-disclosure-chevron {
|
||||||
|
grid-column: 3;
|
||||||
|
justify-self: end;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-address-disclosure-chevron.is-expanded {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.remote-address-disclosure-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 0 10px 10px;
|
||||||
|
border-top: 1px solid var(--border-base);
|
||||||
|
}
|
||||||
|
|
||||||
.remote-qr {
|
.remote-qr {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
.settings-screen-frame {
|
.settings-screen-frame {
|
||||||
@apply fixed inset-0 z-50 flex items-center justify-center p-4;
|
@apply fixed inset-0 z-50 flex items-center justify-center px-4;
|
||||||
|
padding-block: 5dvh;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Override .modal-surface (defined later in panels.css). */
|
/* Override .modal-surface (defined later in panels.css). */
|
||||||
.modal-surface.settings-screen-shell {
|
.modal-surface.settings-screen-shell {
|
||||||
width: min(1120px, 100%);
|
width: min(1120px, 100%);
|
||||||
height: min(88vh, 920px);
|
height: 100%;
|
||||||
max-height: none;
|
max-height: none;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(220px, 260px) minmax(0, 1fr);
|
grid-template-columns: minmax(220px, 260px) minmax(0, 1fr);
|
||||||
@@ -278,10 +279,25 @@
|
|||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-password-summary-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-password-summary-copy {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.settings-password-actions {
|
.settings-password-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-start;
|
justify-content: flex-end;
|
||||||
margin-top: 0.75rem;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-form-group {
|
.settings-form-group {
|
||||||
|
|||||||
@@ -321,6 +321,7 @@
|
|||||||
|
|
||||||
.tool-call-diff-shell {
|
.tool-call-diff-shell {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
scrollbar-gutter: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-diff-viewer {
|
.tool-call-diff-viewer {
|
||||||
@@ -343,6 +344,8 @@
|
|||||||
.tool-call-diff-shell .tool-call-diff-viewer {
|
.tool-call-diff-shell .tool-call-diff-viewer {
|
||||||
max-height: none;
|
max-height: none;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-call-diff-toolbar-label {
|
.tool-call-diff-toolbar-label {
|
||||||
@@ -513,6 +516,84 @@
|
|||||||
font-size: var(--font-size-xs);
|
font-size: var(--font-size-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tool-call-diff-shell .tool-call-diff-viewer .diff-line-content,
|
||||||
|
.tool-call-diff-shell .tool-call-diff-viewer .diff-line-hunk-content,
|
||||||
|
.tool-call-diff-shell .tool-call-diff-viewer .diff-line-old-content,
|
||||||
|
.tool-call-diff-shell .tool-call-diff-viewer .diff-line-new-content {
|
||||||
|
padding-right: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-diff-shell[data-diff-mode="unified"] .tool-call-diff-viewer .diff-line-num {
|
||||||
|
padding-left: 1px !important;
|
||||||
|
padding-right: 1px !important;
|
||||||
|
text-align: left !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-diff-shell[data-diff-mode="unified"] .tool-call-diff-viewer .unified-diff-table {
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-diff-shell[data-diff-mode="unified"] .tool-call-diff-viewer .unified-diff-table-num-col {
|
||||||
|
width: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-diff-shell[data-diff-mode="unified"] .tool-call-diff-viewer .tool-call-diff-compact-line-number {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-diff-shell[data-diff-mode="split"] .tool-call-diff-viewer .diff-line-old-num,
|
||||||
|
.tool-call-diff-shell[data-diff-mode="split"] .tool-call-diff-viewer .diff-line-new-num,
|
||||||
|
.tool-call-diff-shell[data-diff-mode="split"] .tool-call-diff-viewer .diff-line-hunk-action {
|
||||||
|
padding-left: 2px !important;
|
||||||
|
padding-right: 2px !important;
|
||||||
|
text-align: left !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-diff-shell[data-diff-mode="split"] .tool-call-diff-viewer .diff-line-old-num,
|
||||||
|
.tool-call-diff-shell[data-diff-mode="split"] .tool-call-diff-viewer .diff-line-new-num,
|
||||||
|
.tool-call-diff-shell[data-diff-mode="split"] .tool-call-diff-viewer .diff-line-old-num [data-line-num],
|
||||||
|
.tool-call-diff-shell[data-diff-mode="split"] .tool-call-diff-viewer .diff-line-new-num [data-line-num] {
|
||||||
|
white-space: nowrap !important;
|
||||||
|
word-break: normal !important;
|
||||||
|
overflow-wrap: normal !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-diff-shell[data-diff-mode="split"] .tool-call-diff-viewer .diff-line-hunk-action {
|
||||||
|
padding-top: 1px !important;
|
||||||
|
padding-bottom: 1px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-diff-shell .tool-call-diff-viewer .diff-line-content-item {
|
||||||
|
padding-left: 1.1em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-diff-shell .tool-call-diff-viewer .diff-line-content-operator {
|
||||||
|
margin-left: -1.1em !important;
|
||||||
|
width: 0.9em !important;
|
||||||
|
min-width: 0.9em !important;
|
||||||
|
text-indent: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.tool-call-diff-shell[data-diff-mode="unified"] .tool-call-diff-viewer .unified-diff-table-wrapper {
|
||||||
|
--diff-aside-width: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-diff-shell .tool-call-diff-viewer .diff-line-content-item {
|
||||||
|
padding-left: 1.5em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-call-diff-shell .tool-call-diff-viewer .diff-line-content-operator {
|
||||||
|
margin-left: -1.5em !important;
|
||||||
|
width: 1.1em !important;
|
||||||
|
min-width: 1.1em !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.tool-call-markdown .markdown-code-block {
|
.tool-call-markdown .markdown-code-block {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
|
|||||||
@@ -184,6 +184,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.status-indicator.session-status.session-working,
|
.status-indicator.session-status.session-working,
|
||||||
|
.status-indicator.session-status.session-retrying,
|
||||||
.status-indicator.session-status.session-compacting,
|
.status-indicator.session-status.session-compacting,
|
||||||
.status-indicator.session-status.session-idle {
|
.status-indicator.session-status.session-idle {
|
||||||
font-weight: var(--font-weight-medium);
|
font-weight: var(--font-weight-medium);
|
||||||
@@ -194,6 +195,11 @@
|
|||||||
--session-status-dot: var(--session-status-working-fg);
|
--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 {
|
.status-indicator.session-status.session-compacting {
|
||||||
color: var(--session-status-compacting-fg);
|
color: var(--session-status-compacting-fg);
|
||||||
--session-status-dot: var(--session-status-compacting-fg);
|
--session-status-dot: var(--session-status-compacting-fg);
|
||||||
@@ -222,6 +228,10 @@
|
|||||||
background-color: var(--session-status-working-bg);
|
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 {
|
.status-indicator.session-status.session-compacting.session-status-list {
|
||||||
background-color: var(--session-status-compacting-bg);
|
background-color: var(--session-status-compacting-bg);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -412,6 +412,19 @@
|
|||||||
background-color: var(--surface-secondary);
|
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 {
|
.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;
|
@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);
|
color: var(--text-secondary);
|
||||||
@@ -452,6 +465,8 @@
|
|||||||
@apply inline-flex items-center justify-center p-0.5 rounded transition-all duration-150;
|
@apply inline-flex items-center justify-center p-0.5 rounded transition-all duration-150;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-info-trigger:hover {
|
.section-info-trigger:hover {
|
||||||
@@ -459,6 +474,12 @@
|
|||||||
background-color: var(--surface-hover);
|
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 {
|
.section-label {
|
||||||
margin-inline-start: 2px;
|
margin-inline-start: 2px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,6 +107,28 @@
|
|||||||
@apply w-full;
|
@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 .selector-trigger,
|
||||||
.session-sidebar-controls [data-model-selector-control],
|
.session-sidebar-controls [data-model-selector-control],
|
||||||
.session-sidebar-controls .selector-trigger-label,
|
.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-working,
|
||||||
|
.status-indicator.session-status.session-retrying,
|
||||||
.status-indicator.session-status.session-compacting,
|
.status-indicator.session-status.session-compacting,
|
||||||
.status-indicator.session-status.session-idle {
|
.status-indicator.session-status.session-idle {
|
||||||
font-weight: var(--font-weight-medium);
|
font-weight: var(--font-weight-medium);
|
||||||
@@ -404,6 +427,11 @@ session-sidebar-controls .selector-trigger-primary {
|
|||||||
--session-status-dot: var(--session-status-working-fg);
|
--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 {
|
.status-indicator.session-status.session-compacting {
|
||||||
color: var(--session-status-compacting-fg);
|
color: var(--session-status-compacting-fg);
|
||||||
--session-status-dot: 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);
|
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 {
|
.status-indicator.session-status.session-compacting.session-status-list {
|
||||||
background-color: var(--session-status-compacting-bg);
|
background-color: var(--session-status-compacting-bg);
|
||||||
}
|
}
|
||||||
@@ -458,6 +490,16 @@ session-sidebar-controls .selector-trigger-primary {
|
|||||||
border: 1px solid transparent;
|
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) {
|
@media (max-width: 768px) {
|
||||||
.session-list-container {
|
.session-list-container {
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export interface RenderCache {
|
|||||||
html: string
|
html: string
|
||||||
theme?: string
|
theme?: string
|
||||||
mode?: string
|
mode?: string
|
||||||
|
wrap?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PendingPermissionState {
|
export interface PendingPermissionState {
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ export type {
|
|||||||
|
|
||||||
export type SessionStatus = "idle" | "working" | "compacting"
|
export type SessionStatus = "idle" | "working" | "compacting"
|
||||||
|
|
||||||
|
export interface SessionRetryState {
|
||||||
|
attempt: number
|
||||||
|
message: string
|
||||||
|
next: number
|
||||||
|
}
|
||||||
|
|
||||||
export function mapSdkSessionStatus(status: SDKSessionStatus | null | undefined): SessionStatus {
|
export function mapSdkSessionStatus(status: SDKSessionStatus | null | undefined): SessionStatus {
|
||||||
if (!status || status.type === "idle") {
|
if (!status || status.type === "idle") {
|
||||||
return "idle"
|
return "idle"
|
||||||
@@ -26,6 +32,18 @@ export function mapSdkSessionStatus(status: SDKSessionStatus | null | undefined)
|
|||||||
return "working"
|
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
|
// Our client-specific Session interface extending SDK Session
|
||||||
export interface Session
|
export interface Session
|
||||||
extends Omit<import("@opencode-ai/sdk").Session, "projectID" | "directory" | "parentID"> {
|
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
|
pendingPermission?: boolean // Indicates if session is waiting on user permission
|
||||||
pendingQuestion?: boolean // Indicates if session is waiting on user input
|
pendingQuestion?: boolean // Indicates if session is waiting on user input
|
||||||
status: SessionStatus // Single source of truth for session status
|
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)
|
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