Compare commits
31 Commits
v0.11.3-de
...
v0.11.5-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8607fab5b5 | ||
|
|
0368fe8248 | ||
|
|
b970281fa7 | ||
|
|
8e5a7fc213 | ||
|
|
15f362e8b5 | ||
|
|
7bbd0a1787 | ||
|
|
f8aae56728 | ||
|
|
027d7fc97d | ||
|
|
e90aef4b3c | ||
|
|
e4e89008b2 | ||
|
|
90baefbb7e | ||
|
|
1c138f4489 | ||
|
|
d36e568ed0 | ||
|
|
d6462ef524 | ||
|
|
a06884ebce | ||
|
|
62bd88f6a4 | ||
|
|
6479561779 | ||
|
|
635237c258 | ||
|
|
33f0aa5714 | ||
|
|
7ca6285d58 | ||
|
|
14c60fef6c | ||
|
|
336de6a19e | ||
|
|
377c8e2249 | ||
|
|
697dea21f8 | ||
|
|
34d3f803d5 | ||
|
|
f824a063a5 | ||
|
|
96fe1b86dd | ||
|
|
5fabf286e8 | ||
|
|
e8947d61b1 | ||
|
|
1ccd14eae8 | ||
|
|
b162764ccb |
105
.github/workflows/build-and-upload.yml
vendored
105
.github/workflows/build-and-upload.yml
vendored
@@ -3,6 +3,11 @@ name: Build and Upload Binaries
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
ref:
|
||||
description: "Git ref (branch, tag, or SHA) to build from"
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
version:
|
||||
description: "Version to apply to workspace packages (release builds)"
|
||||
required: false
|
||||
@@ -45,6 +50,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.ref }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
@@ -65,6 +72,88 @@ jobs:
|
||||
- name: Build macOS binaries (Electron)
|
||||
run: npm run build:mac --workspace @neuralnomads/codenomad-electron-app
|
||||
|
||||
- name: Repackage Electron macOS zips (ditto)
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Prefer the workflow-provided version; fall back to package.json.
|
||||
VERSION_TO_USE="${VERSION:-}"
|
||||
if [ -z "$VERSION_TO_USE" ]; then
|
||||
VERSION_TO_USE=$(node -p "require('./packages/electron-app/package.json').version")
|
||||
fi
|
||||
|
||||
release_root="packages/electron-app/release"
|
||||
# macOS GitHub runners ship /bin/bash 3.2 which doesn't support `shopt -s globstar`.
|
||||
# Use find to locate built app bundles instead of ** globs.
|
||||
apps=()
|
||||
while IFS= read -r -d '' app; do
|
||||
apps+=("$app")
|
||||
done < <(find "$release_root" -type d -name 'CodeNomad.app' -print0)
|
||||
if [ "${#apps[@]}" -eq 0 ]; then
|
||||
echo "No CodeNomad.app found under $release_root" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for app in "${apps[@]}"; do
|
||||
bundle_dir=$(basename "$(dirname "$app")")
|
||||
arch="x64"
|
||||
if [[ "$bundle_dir" == *"arm64"* ]]; then
|
||||
arch="arm64"
|
||||
fi
|
||||
|
||||
out_zip="$release_root/CodeNomad-${VERSION_TO_USE}-mac-${arch}.zip"
|
||||
rm -f "$out_zip"
|
||||
echo "ditto -ck: $app -> $out_zip"
|
||||
ditto -ck --sequesterRsrc --keepParent "$app" "$out_zip"
|
||||
done
|
||||
|
||||
- name: Validate Electron macOS codesign (unzipped)
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
shopt -s nullglob
|
||||
|
||||
# Dev CI builds typically don't have a macOS signing identity available.
|
||||
# When no identity is present, electron-builder skips signing and strict verification will fail.
|
||||
if security find-identity -p codesigning -v | grep -q "0 valid identities found"; then
|
||||
echo "No valid macOS codesigning identity found; skipping codesign verification"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
tmp_dir=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp_dir"' EXIT
|
||||
|
||||
zips=(packages/electron-app/release/CodeNomad-*-mac-*.zip)
|
||||
if [ "${#zips[@]}" -eq 0 ]; then
|
||||
echo "No Electron macOS zip artifacts found to validate" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for zip in "${zips[@]}"; do
|
||||
echo "Validating codesign for: $zip"
|
||||
extract_dir="$tmp_dir/$(basename "$zip" .zip)"
|
||||
mkdir -p "$extract_dir"
|
||||
|
||||
# Use ditto for extraction as well to preserve bundle metadata.
|
||||
ditto -x -k "$zip" "$extract_dir"
|
||||
|
||||
app_path=""
|
||||
for candidate in "$extract_dir"/*.app "$extract_dir"/*/*.app; do
|
||||
if [ -d "$candidate" ]; then
|
||||
app_path="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$app_path" ]; then
|
||||
echo "No .app found after extracting $zip" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
codesign --verify --deep --strict --verbose=2 "$app_path"
|
||||
done
|
||||
|
||||
- name: Upload release assets
|
||||
if: ${{ inputs.upload && inputs.tag != '' }}
|
||||
run: |
|
||||
@@ -85,6 +174,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.ref }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
@@ -124,6 +215,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.ref }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
@@ -164,6 +257,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.ref }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
@@ -237,6 +332,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.ref }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
@@ -310,6 +407,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.ref }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
@@ -388,6 +487,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.ref }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
@@ -490,6 +591,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.ref }}
|
||||
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
@@ -587,6 +690,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.ref }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
61
.github/workflows/dev-release.yml
vendored
61
.github/workflows/dev-release.yml
vendored
@@ -1,12 +1,13 @@
|
||||
name: Develop Pre-Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
schedule:
|
||||
# Nightly build of dev (only if dev has new commits)
|
||||
- cron: "0 1 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
id-token: write
|
||||
contents: write
|
||||
|
||||
@@ -15,25 +16,63 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
gate:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version_suffix: ${{ steps.vars.outputs.version_suffix }}
|
||||
run: ${{ steps.gate.outputs.run }}
|
||||
dev_sha: ${{ steps.gate.outputs.dev_sha }}
|
||||
version_suffix: ${{ steps.gate.outputs.version_suffix }}
|
||||
steps:
|
||||
- name: Compute version suffix
|
||||
id: vars
|
||||
- name: Decide whether to run
|
||||
id: gate
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SHA8="${GITHUB_SHA::8}"
|
||||
|
||||
api() {
|
||||
curl -sS \
|
||||
-H "Authorization: Bearer ${GH_TOKEN}" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
"$1"
|
||||
}
|
||||
|
||||
DEV_SHA=$(api "https://api.github.com/repos/${GITHUB_REPOSITORY}/git/ref/heads/dev" | jq -r '.object.sha')
|
||||
if [ -z "$DEV_SHA" ] || [ "$DEV_SHA" = "null" ]; then
|
||||
echo "Failed to resolve dev head SHA" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DATE=$(date -u +%Y%m%d)
|
||||
echo "version_suffix=-dev-${DATE}-${SHA8}" >> "$GITHUB_OUTPUT"
|
||||
SHA8="${DEV_SHA::8}"
|
||||
VERSION_SUFFIX="-dev-${DATE}-${SHA8}"
|
||||
|
||||
SHOULD_RUN="false"
|
||||
if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then
|
||||
SHOULD_RUN="true"
|
||||
else
|
||||
# Nightly: only run if dev has advanced since last successful dev-release build.
|
||||
LAST_SHA=$(api "https://api.github.com/repos/${GITHUB_REPOSITORY}/actions/workflows/dev-release.yml/runs?branch=dev&status=success&per_page=1" | jq -r '.workflow_runs[0].head_sha // empty')
|
||||
if [ -z "${LAST_SHA}" ]; then
|
||||
SHOULD_RUN="true"
|
||||
elif [ "${LAST_SHA}" != "${DEV_SHA}" ]; then
|
||||
SHOULD_RUN="true"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "run=${SHOULD_RUN}" >> "$GITHUB_OUTPUT"
|
||||
echo "dev_sha=${DEV_SHA}" >> "$GITHUB_OUTPUT"
|
||||
echo "version_suffix=${VERSION_SUFFIX}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
prerelease:
|
||||
needs: prepare
|
||||
needs: gate
|
||||
if: ${{ needs.gate.outputs.run == 'true' }}
|
||||
uses: ./.github/workflows/reusable-release.yml
|
||||
with:
|
||||
version_suffix: ${{ needs.prepare.outputs.version_suffix }}
|
||||
ref: ${{ needs.gate.outputs.dev_sha }}
|
||||
version_suffix: ${{ needs.gate.outputs.version_suffix }}
|
||||
npm_package_name: "@neuralnomads/codenomad-dev"
|
||||
dist_tag: latest
|
||||
prerelease: true
|
||||
|
||||
6
.github/workflows/manual-npm-publish.yml
vendored
6
.github/workflows/manual-npm-publish.yml
vendored
@@ -19,6 +19,10 @@ on:
|
||||
type: string
|
||||
workflow_call:
|
||||
inputs:
|
||||
ref:
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
version:
|
||||
required: true
|
||||
type: string
|
||||
@@ -46,6 +50,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.ref }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
10
.github/workflows/release-ui.yml
vendored
10
.github/workflows/release-ui.yml
vendored
@@ -1,7 +1,13 @@
|
||||
name: Release UI
|
||||
|
||||
on:
|
||||
workflow_call: {}
|
||||
workflow_call:
|
||||
inputs:
|
||||
ref:
|
||||
description: "Git ref (branch, tag, or SHA) to build from"
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
workflow_dispatch: {}
|
||||
|
||||
permissions:
|
||||
@@ -18,6 +24,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.ref }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
11
.github/workflows/reusable-release.yml
vendored
11
.github/workflows/reusable-release.yml
vendored
@@ -3,6 +3,11 @@ name: Reusable Release
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
ref:
|
||||
description: "Git ref (branch, tag, or SHA) to build from"
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
version_suffix:
|
||||
description: "Suffix appended to package.json version"
|
||||
required: false
|
||||
@@ -46,6 +51,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.ref }}
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
@@ -84,6 +91,7 @@ jobs:
|
||||
needs: prepare-release
|
||||
uses: ./.github/workflows/build-and-upload.yml
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.ref }}
|
||||
version: ${{ needs.prepare-release.outputs.version }}
|
||||
tag: ${{ needs.prepare-release.outputs.tag }}
|
||||
release_name: ${{ needs.prepare-release.outputs.release_name }}
|
||||
@@ -95,6 +103,8 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
uses: ./.github/workflows/release-ui.yml
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.ref }}
|
||||
secrets: inherit
|
||||
|
||||
publish-server:
|
||||
@@ -103,6 +113,7 @@ jobs:
|
||||
- build-and-upload
|
||||
uses: ./.github/workflows/manual-npm-publish.yml
|
||||
with:
|
||||
ref: ${{ inputs.ref || github.ref }}
|
||||
version: ${{ needs.prepare-release.outputs.version }}
|
||||
dist_tag: ${{ inputs.dist_tag }}
|
||||
package_name: ${{ inputs.npm_package_name }}
|
||||
|
||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "codenomad-workspace",
|
||||
"version": "0.11.3",
|
||||
"version": "0.11.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "codenomad-workspace",
|
||||
"version": "0.11.3",
|
||||
"version": "0.11.5",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"7zip-bin": "^5.2.0",
|
||||
@@ -11985,7 +11985,7 @@
|
||||
},
|
||||
"packages/electron-app": {
|
||||
"name": "@neuralnomads/codenomad-electron-app",
|
||||
"version": "0.11.3",
|
||||
"version": "0.11.5",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@codenomad/ui": "file:../ui",
|
||||
@@ -12021,7 +12021,7 @@
|
||||
},
|
||||
"packages/server": {
|
||||
"name": "@neuralnomads/codenomad",
|
||||
"version": "0.11.3",
|
||||
"version": "0.11.5",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fastify/cors": "^8.5.0",
|
||||
@@ -12062,7 +12062,7 @@
|
||||
},
|
||||
"packages/tauri-app": {
|
||||
"name": "@codenomad/tauri-app",
|
||||
"version": "0.11.3",
|
||||
"version": "0.11.5",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.9.4"
|
||||
@@ -12070,7 +12070,7 @@
|
||||
},
|
||||
"packages/ui": {
|
||||
"name": "@codenomad/ui",
|
||||
"version": "0.11.3",
|
||||
"version": "0.11.5",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@git-diff-view/solid": "^0.0.8",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "codenomad-workspace",
|
||||
"version": "0.11.3",
|
||||
"version": "0.11.5",
|
||||
"private": true,
|
||||
"description": "CodeNomad monorepo workspace",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"minServerVersion": "0.11.1",
|
||||
"minServerVersion": "0.11.4",
|
||||
"latestServerUrl": "https://github.com/NeuralNomadsAI/CodeNomad/releases/latest"
|
||||
}
|
||||
|
||||
@@ -431,7 +431,9 @@ export class CliProcessManager extends EventEmitter {
|
||||
|
||||
if (options.dev) {
|
||||
const devServer = process.env.VITE_DEV_SERVER_URL || process.env.ELECTRON_RENDERER_URL || "http://localhost:3000"
|
||||
args.push("--ui-dev-server", devServer, "--log-level", "debug")
|
||||
const rawLogLevel = (process.env.CLI_LOG_LEVEL ?? "info").trim()
|
||||
const logLevel = rawLogLevel.length > 0 ? rawLogLevel.toLowerCase() : "info"
|
||||
args.push("--ui-dev-server", devServer, "--log-level", logLevel)
|
||||
}
|
||||
|
||||
return args
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@neuralnomads/codenomad-electron-app",
|
||||
"version": "0.11.3",
|
||||
"version": "0.11.5",
|
||||
"description": "CodeNomad - AI coding assistant",
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
@@ -15,7 +15,10 @@
|
||||
},
|
||||
"homepage": "https://github.com/NeuralNomadsAI/CodeNomad",
|
||||
"scripts": {
|
||||
"dev": "electron-vite dev",
|
||||
"dev": "npm run dev:info",
|
||||
"dev:info": "cross-env CLI_LOG_LEVEL=info electron-vite dev",
|
||||
"dev:debug": "cross-env CLI_LOG_LEVEL=debug electron-vite dev",
|
||||
"dev:trace": "cross-env CLI_LOG_LEVEL=trace electron-vite dev",
|
||||
"dev:electron": "NODE_ENV=development ELECTRON_ENABLE_LOGGING=1 NODE_OPTIONS=\"--import tsx\" electron electron/main/main.ts",
|
||||
"build": "electron-vite build",
|
||||
"typecheck": "tsc --noEmit -p tsconfig.json",
|
||||
@@ -42,6 +45,7 @@
|
||||
"devDependencies": {
|
||||
"7zip-bin": "^5.2.0",
|
||||
"app-builder-bin": "^4.2.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"electron": "39.0.0",
|
||||
"electron-builder": "^24.0.0",
|
||||
"electron-vite": "4.0.1",
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@opencode-ai/plugin": "1.2.6"
|
||||
"@opencode-ai/plugin": "1.2.10"
|
||||
}
|
||||
}
|
||||
@@ -5,18 +5,21 @@
|
||||
## Features & Capabilities
|
||||
|
||||
### 🌍 Deployment Freedom
|
||||
|
||||
- **Remote Access**: Host CodeNomad on a powerful workstation and access it from your lightweight laptop.
|
||||
- **Code Anywhere**: Tunnel in via VPN or SSH to code securely from coffee shops or while traveling.
|
||||
- **Multi-Device**: The responsive web client works on tablets and iPads, turning any screen into a dev terminal.
|
||||
- **Always-On**: Run as a background service so your sessions are always ready when you connect.
|
||||
|
||||
### ⚡️ Workspace Power
|
||||
|
||||
- **Multi-Instance**: Juggle multiple OpenCode sessions side-by-side with per-instance tabs.
|
||||
- **Long-Context Native**: Scroll through massive transcripts without hitches.
|
||||
- **Deep Task Awareness**: Monitor background tasks and child sessions without losing your flow.
|
||||
- **Command Palette**: A single, global palette to jump tabs, launch tools, and fire shortcuts.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **OpenCode**: `opencode` must be installed and configured on your system.
|
||||
- Node.js 18+ and npm (for running or building from source).
|
||||
- A workspace folder on disk you want to serve.
|
||||
@@ -25,6 +28,7 @@
|
||||
## Usage
|
||||
|
||||
### Run via npx (Recommended)
|
||||
|
||||
You can run CodeNomad directly without installing it:
|
||||
|
||||
```sh
|
||||
@@ -43,6 +47,7 @@ On startup, CodeNomad prints two URLs:
|
||||
- `Remote Connection URL : ...` (used by browsers/other machines when remote access is enabled)
|
||||
|
||||
### Install Globally
|
||||
|
||||
Or install it globally to use the `codenomad` command:
|
||||
|
||||
```sh
|
||||
@@ -51,6 +56,7 @@ codenomad --launch
|
||||
```
|
||||
|
||||
### Install Locally (per-project)
|
||||
|
||||
If you prefer to install CodeNomad into a project and run the local binary:
|
||||
|
||||
```sh
|
||||
@@ -61,6 +67,7 @@ npx codenomad --launch
|
||||
(`npx codenomad ...` will use `./node_modules/.bin/codenomad` when present.)
|
||||
|
||||
### Common Flags
|
||||
|
||||
You can configure the server using flags or environment variables:
|
||||
|
||||
| Flag | Env Variable | Description |
|
||||
@@ -74,7 +81,7 @@ You can configure the server using flags or environment variables:
|
||||
| `--tls-ca <path>` | `CLI_TLS_CA` | Optional CA chain/bundle (PEM) |
|
||||
| `--tlsSANs <list>` | `CLI_TLS_SANS` | Additional TLS SANs (comma-separated) |
|
||||
| `--host <addr>` | `CLI_HOST` | Interface to bind (default 127.0.0.1) |
|
||||
| `--workspace-root <path>` | `CLI_WORKSPACE_ROOT` | Default root for new workspaces |
|
||||
| `--workspace-root <path>` | `CLI_WORKSPACE_ROOT` | Restricts the root path where new workspaces can be opened. Git worktrees are created in `.codenomad/worktrees` inside the project folder. |
|
||||
| `--unrestricted-root` | `CLI_UNRESTRICTED_ROOT` | Allow full-filesystem browsing |
|
||||
| `--config <path>` | `CLI_CONFIG` | Config file location |
|
||||
| `--launch` | `CLI_LAUNCH` | Open the UI in a Chromium-based browser |
|
||||
@@ -87,10 +94,11 @@ You can configure the server using flags or environment variables:
|
||||
| `--ui-dir <path>` | `CLI_UI_DIR` | Directory containing the built UI bundle |
|
||||
| `--ui-dev-server <url>` | `CLI_UI_DEV_SERVER` | Proxy UI requests to a running dev server (requires `--https=false --http=true`) |
|
||||
| `--ui-no-update` | `CLI_UI_NO_UPDATE` | Disable remote UI updates |
|
||||
| `--ui-auto-update <enabled>` | `CLI_UI_AUTO_UPDATE` | Enable remote UI updates (true|false) |
|
||||
| `--ui-auto-update <enabled>` | `CLI_UI_AUTO_UPDATE` | Enable remote UI updates (`true` |
|
||||
| `--ui-manifest-url <url>` | `CLI_UI_MANIFEST_URL` | Remote UI manifest URL |
|
||||
|
||||
### Dev Releases (Advanced)
|
||||
|
||||
If you want the latest bleeding-edge builds (published as GitHub pre-releases), use the dev package:
|
||||
|
||||
```sh
|
||||
@@ -141,12 +149,14 @@ codenomad --tlsSANs "localhost,127.0.0.1,my-hostname,192.168.1.10"
|
||||
```
|
||||
|
||||
### Authentication
|
||||
|
||||
- Default behavior: CodeNomad requires a login (username/password) and stores a session cookie in the browser.
|
||||
- `--dangerously-skip-auth` / `CODENOMAD_SKIP_AUTH=true` disables the login prompt and treats all requests as authenticated.
|
||||
Use this only when access is already protected by another layer (SSO proxy, VPN, Coder workspace auth, etc.).
|
||||
If you bind to `0.0.0.0` while skipping auth, anyone who can reach the port can access the API.
|
||||
|
||||
### Progressive Web App (PWA)
|
||||
|
||||
When running as a server CodeNomad can also be installed as a PWA from any supported browser, giving you a native app experience just like the Electron installation but executing on the remote server instead.
|
||||
|
||||
1. Open the CodeNomad UI in a Chromium-based browser (Chrome, Edge, Brave, etc.).
|
||||
@@ -158,5 +168,6 @@ When running as a server CodeNomad can also be installed as a PWA from any suppo
|
||||
> If you host CodeNomad on a remote machine, use HTTPS. Self-signed certificates generally won't work unless they are explicitly trusted by the device/browser (e.g., via a custom CA).
|
||||
|
||||
### Data Storage
|
||||
|
||||
- **Config**: `~/.config/codenomad/config.json`
|
||||
- **Instance Data**: `~/.config/codenomad/instances` (chat history, etc.)
|
||||
|
||||
4
packages/server/package-lock.json
generated
4
packages/server/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@neuralnomads/codenomad",
|
||||
"version": "0.11.3",
|
||||
"version": "0.11.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@neuralnomads/codenomad",
|
||||
"version": "0.11.3",
|
||||
"version": "0.11.5",
|
||||
"dependencies": {
|
||||
"@fastify/cors": "^8.5.0",
|
||||
"@fastify/reply-from": "^9.8.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@neuralnomads/codenomad",
|
||||
"version": "0.11.3",
|
||||
"version": "0.11.5",
|
||||
"description": "CodeNomad Server",
|
||||
"license": "MIT",
|
||||
"author": {
|
||||
|
||||
@@ -78,7 +78,7 @@ function parseCliOptions(argv: string[]): CliOptions {
|
||||
.addOption(new Option("--tls-ca <path>", "TLS CA chain (PEM)").env("CLI_TLS_CA"))
|
||||
.addOption(new Option("--tlsSANs <list>", "Additional TLS SANs (comma-separated)").env("CLI_TLS_SANS"))
|
||||
.addOption(
|
||||
new Option("--workspace-root <path>", "Workspace root directory").env("CLI_WORKSPACE_ROOT").default(process.cwd()),
|
||||
new Option("--workspace-root <path>", "Restricts root path where workspaces can be opened").env("CLI_WORKSPACE_ROOT").default(process.cwd()),
|
||||
)
|
||||
.addOption(new Option("--root <path>").env("CLI_ROOT").hideHelp(true))
|
||||
.addOption(new Option("--unrestricted-root", "Allow browsing the full filesystem").env("CLI_UNRESTRICTED_ROOT").default(false))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@codenomad/tauri-app",
|
||||
"version": "0.11.3",
|
||||
"version": "0.11.5",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@codenomad/ui",
|
||||
"version": "0.11.3",
|
||||
"version": "0.11.5",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
@@ -61,6 +61,11 @@ export function MonacoDiffViewer(props: MonacoDiffViewerProps) {
|
||||
// Keep enough gutter space so unified diffs don't overlap `+`/`-` markers.
|
||||
lineNumbersMinChars: 4,
|
||||
lineDecorationsWidth: 12,
|
||||
// Use legacy diff algorithm for better performance with large files
|
||||
// See: https://github.com/microsoft/vscode/issues/184037
|
||||
diffAlgorithm: "legacy",
|
||||
// Limit computation time to avoid freezing on large files
|
||||
maxComputationTime: 10000,
|
||||
})
|
||||
|
||||
setReady(true)
|
||||
|
||||
@@ -115,6 +115,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
const desktopQuery = useMediaQuery("(min-width: 1280px)")
|
||||
|
||||
const tabletQuery = useMediaQuery("(min-width: 768px)")
|
||||
const compactHeaderQuery = useMediaQuery("(max-width: 1024px)")
|
||||
|
||||
const layoutMode = createMemo<LayoutMode>(() => {
|
||||
if (desktopQuery()) return "desktop"
|
||||
@@ -123,6 +124,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
})
|
||||
|
||||
const isPhoneLayout = createMemo(() => layoutMode() === "phone")
|
||||
const compactHeaderLayout = createMemo(() => isPhoneLayout() || compactHeaderQuery())
|
||||
const mobileFullscreen = createMemo(() => props.mobileFullscreenMode && isPhoneLayout())
|
||||
const compactPromptLayout = createMemo(() => layoutMode() !== "desktop")
|
||||
const leftPinningSupported = createMemo(() => layoutMode() !== "phone")
|
||||
@@ -596,7 +598,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
<AppBar position="sticky" color="default" elevation={0} class="border-b border-base">
|
||||
<Toolbar variant="dense" class="session-toolbar flex flex-wrap items-center gap-2 py-0 min-h-[40px]">
|
||||
<Show
|
||||
when={!isPhoneLayout()}
|
||||
when={!compactHeaderLayout()}
|
||||
fallback={
|
||||
<div class="flex flex-col w-full gap-1.5">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2 w-full">
|
||||
@@ -625,7 +627,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
<div class="flex flex-wrap items-center justify-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
class="connection-status-button px-2 py-0.5 text-xs"
|
||||
class="connection-status-button command-palette-button"
|
||||
onClick={handleCommandPaletteClick}
|
||||
aria-label={t("instanceShell.commandPalette.openAriaLabel")}
|
||||
style={{ flex: "0 0 auto", width: "auto" }}
|
||||
@@ -634,8 +636,8 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
</button>
|
||||
<span class="connection-status-shortcut-hint kbd-hint">
|
||||
<Kbd shortcut="cmd+shift+p" />
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 flex items-center justify-center min-w-0">
|
||||
<span
|
||||
@@ -646,7 +648,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Show when={!props.mobileFullscreenMode}>
|
||||
<Show when={isPhoneLayout() && !props.mobileFullscreenMode}>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
onClick={props.onEnterMobileFullscreen}
|
||||
@@ -670,16 +672,18 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
{rightAppBarButtonIcon()}
|
||||
</IconButton>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-center gap-2 pb-1">
|
||||
<ContextMeter
|
||||
usedTokens={tokenStats().used}
|
||||
availableTokens={tokenStats().avail}
|
||||
formatTokens={formatTokenTotal}
|
||||
usedLabel={t("instanceShell.metrics.usedLabel")}
|
||||
availableLabel={t("instanceShell.metrics.availableLabel")}
|
||||
/>
|
||||
<Show when={!showingInfoView()}>
|
||||
<ContextMeter
|
||||
usedTokens={tokenStats().used}
|
||||
availableTokens={tokenStats().avail}
|
||||
formatTokens={formatTokenTotal}
|
||||
usedLabel={t("instanceShell.metrics.usedLabel")}
|
||||
availableLabel={t("instanceShell.metrics.availableLabel")}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -721,7 +725,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
<div class="session-toolbar-center flex items-center justify-center gap-2 min-w-[160px]">
|
||||
<button
|
||||
type="button"
|
||||
class="connection-status-button px-2 py-0.5 text-xs"
|
||||
class="connection-status-button command-palette-button"
|
||||
onClick={handleCommandPaletteClick}
|
||||
aria-label={t("instanceShell.commandPalette.openAriaLabel")}
|
||||
style={{ flex: "0 0 auto", width: "auto" }}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { For, Show, type Accessor, type Component } from "solid-js"
|
||||
import type { ToolState } from "@opencode-ai/sdk"
|
||||
import { Accordion } from "@kobalte/core"
|
||||
import { Tooltip } from "@kobalte/core/tooltip"
|
||||
|
||||
import { ChevronDown, TerminalSquare, Trash2, XOctagon } from "lucide-solid"
|
||||
import { ChevronDown, Info, TerminalSquare, Trash2, XOctagon } from "lucide-solid"
|
||||
|
||||
import type { Instance } from "../../../../../types/instance"
|
||||
import type { BackgroundProcess } from "../../../../../../../server/src/api-types"
|
||||
@@ -206,21 +207,25 @@ const StatusTab: Component<StatusTabProps> = (props) => {
|
||||
{
|
||||
id: "session-changes",
|
||||
labelKey: "instanceShell.rightPanel.sections.sessionChanges",
|
||||
tooltipKey: "instanceShell.rightPanel.sections.sessionChanges.tooltip",
|
||||
render: renderStatusSessionChanges,
|
||||
},
|
||||
{
|
||||
id: "plan",
|
||||
labelKey: "instanceShell.rightPanel.sections.plan",
|
||||
tooltipKey: "instanceShell.rightPanel.sections.plan.tooltip",
|
||||
render: renderPlanSectionContent,
|
||||
},
|
||||
{
|
||||
id: "background-processes",
|
||||
labelKey: "instanceShell.rightPanel.sections.backgroundProcesses",
|
||||
tooltipKey: "instanceShell.rightPanel.sections.backgroundProcesses.tooltip",
|
||||
render: renderBackgroundProcesses,
|
||||
},
|
||||
{
|
||||
id: "mcp",
|
||||
labelKey: "instanceShell.rightPanel.sections.mcp",
|
||||
tooltipKey: "instanceShell.rightPanel.sections.mcp.tooltip",
|
||||
render: () => (
|
||||
<InstanceServiceStatus
|
||||
initialInstance={props.instance}
|
||||
@@ -233,6 +238,7 @@ const StatusTab: Component<StatusTabProps> = (props) => {
|
||||
{
|
||||
id: "lsp",
|
||||
labelKey: "instanceShell.rightPanel.sections.lsp",
|
||||
tooltipKey: "instanceShell.rightPanel.sections.lsp.tooltip",
|
||||
render: () => (
|
||||
<InstanceServiceStatus
|
||||
initialInstance={props.instance}
|
||||
@@ -245,6 +251,7 @@ const StatusTab: Component<StatusTabProps> = (props) => {
|
||||
{
|
||||
id: "plugins",
|
||||
labelKey: "instanceShell.rightPanel.sections.plugins",
|
||||
tooltipKey: "instanceShell.rightPanel.sections.plugins.tooltip",
|
||||
render: () => (
|
||||
<InstanceServiceStatus
|
||||
initialInstance={props.instance}
|
||||
@@ -276,7 +283,23 @@ const StatusTab: Component<StatusTabProps> = (props) => {
|
||||
<Accordion.Item value={section.id} class="right-panel-accordion-item">
|
||||
<Accordion.Header>
|
||||
<Accordion.Trigger class="right-panel-accordion-trigger">
|
||||
<span>{props.t(section.labelKey)}</span>
|
||||
<span class="section-left">
|
||||
<Tooltip openDelay={200} gutter={4} placement="top">
|
||||
<Tooltip.Trigger
|
||||
class="section-info-trigger"
|
||||
aria-label={props.t(section.tooltipKey)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Info class="section-info-icon" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content class="section-info-tooltip">
|
||||
{props.t(section.tooltipKey)}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip>
|
||||
<span class="section-label">{props.t(section.labelKey)}</span>
|
||||
</span>
|
||||
<ChevronDown
|
||||
class={`right-panel-accordion-chevron ${isSectionExpanded(section.id) ? "right-panel-accordion-chevron-expanded" : ""}`}
|
||||
/>
|
||||
|
||||
@@ -51,7 +51,7 @@ export default function MessageListHeader(props: MessageListHeaderProps) {
|
||||
<div class="connection-status-shortcut-action">
|
||||
<button
|
||||
type="button"
|
||||
class="connection-status-button"
|
||||
class="connection-status-button command-palette-button"
|
||||
onClick={props.onCommandPalette}
|
||||
aria-label={t("messageListHeader.commandPalette.ariaLabel")}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { For, Show, createEffect, createMemo, createSignal, onCleanup, type Component } from "solid-js"
|
||||
import { For, Show, createEffect, createMemo, createSignal, onCleanup, on, untrack, type Component } from "solid-js"
|
||||
import MessagePreview from "./message-preview"
|
||||
import { messageStoreBus } from "../stores/message-v2/bus"
|
||||
import type { ClientPart } from "../types/message"
|
||||
@@ -350,11 +350,9 @@ const MessageTimeline: Component<MessageTimelineProps> = (props) => {
|
||||
clearCloseTimer()
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
const activeId = props.activeMessageId
|
||||
|
||||
createEffect(on(() => props.activeMessageId, (activeId) => {
|
||||
if (!activeId) return
|
||||
const targetSegment = props.segments.find((segment) => segment.messageId === activeId)
|
||||
const targetSegment = untrack(() => props.segments).find((segment) => segment.messageId === activeId)
|
||||
if (!targetSegment) return
|
||||
const element = buttonRefs.get(targetSegment.id)
|
||||
if (!element) return
|
||||
@@ -366,7 +364,7 @@ const MessageTimeline: Component<MessageTimelineProps> = (props) => {
|
||||
window.clearTimeout(timer)
|
||||
}
|
||||
})
|
||||
})
|
||||
}))
|
||||
|
||||
createEffect(() => {
|
||||
const element = tooltipElement()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createSignal, Show, createEffect, createMemo, onCleanup } from "solid-js"
|
||||
import { ArrowRightSquare, Copy } from "lucide-solid"
|
||||
import { ArrowRightSquare, Check, Copy, Hourglass, Loader2, XCircle } from "lucide-solid"
|
||||
import { stringify as stringifyYaml } from "yaml"
|
||||
import { messageStoreBus } from "../stores/message-v2/bus"
|
||||
import { useTheme } from "../lib/theme"
|
||||
@@ -576,13 +576,13 @@ export default function ToolCall(props: ToolCallProps) {
|
||||
const status = toolState()?.status || ""
|
||||
switch (status) {
|
||||
case "pending":
|
||||
return "⏳"
|
||||
return <Hourglass class="w-4 h-4" />
|
||||
case "running":
|
||||
return "🔄"
|
||||
return <Loader2 class="w-4 h-4 animate-spin" />
|
||||
case "completed":
|
||||
return "✅"
|
||||
return <Check class="w-4 h-4" />
|
||||
case "error":
|
||||
return "⚠️"
|
||||
return <XCircle class="w-4 h-4" />
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -287,7 +287,9 @@ export const taskRenderer: ToolRenderer = {
|
||||
content: promptContent()!,
|
||||
cacheKey: "task:prompt",
|
||||
disableScrollTracking: true,
|
||||
disableHighlight: true,
|
||||
// Always use the normal markdown render path for prompt (even while running)
|
||||
// so the prompt doesn't visually change between running/completed states.
|
||||
disableHighlight: false,
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -96,11 +96,17 @@ export const instanceMessages = {
|
||||
"instanceShell.rightPanel.tabs.ariaLabel": "Right panel tabs",
|
||||
"instanceShell.rightPanel.actions.refresh": "Refresh",
|
||||
"instanceShell.rightPanel.sections.sessionChanges": "Session Changes",
|
||||
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Files modified in the current session. Shows additions and deletions for each file.",
|
||||
"instanceShell.rightPanel.sections.plan": "Plan",
|
||||
"instanceShell.rightPanel.sections.plan.tooltip": "The agent's roadmap for this session. Tracks tasks, subtasks, and their completion status.",
|
||||
"instanceShell.rightPanel.sections.backgroundProcesses": "Background Shells",
|
||||
"instanceShell.rightPanel.sections.backgroundProcesses.tooltip": "Long-running processes started by the agent. You can monitor their output, stop, or terminate them.",
|
||||
"instanceShell.rightPanel.sections.mcp": "MCP Servers",
|
||||
"instanceShell.rightPanel.sections.mcp.tooltip": "Model Context Protocol servers that extend the agent's capabilities with external tools and services.",
|
||||
"instanceShell.rightPanel.sections.lsp": "LSP Servers",
|
||||
"instanceShell.rightPanel.sections.lsp.tooltip": "Language Server Protocol servers providing code intelligence, diagnostics, and language-specific features.",
|
||||
"instanceShell.rightPanel.sections.plugins": "Plugins",
|
||||
"instanceShell.rightPanel.sections.plugins.tooltip": "Plugins that customize the UI and server behavior, adding features beyond MCP and LSP.",
|
||||
|
||||
"instanceShell.sessionChanges.noSessionSelected": "Select a session to view changes.",
|
||||
"instanceShell.sessionChanges.loading": "Fetching session changes...",
|
||||
|
||||
@@ -48,7 +48,7 @@ export const instanceMessages = {
|
||||
"instanceShell.commandPalette.openAriaLabel": "Abrir paleta de comandos",
|
||||
"instanceShell.commandPalette.button": "Paleta de comandos",
|
||||
|
||||
"instanceShell.connection.ariaLabel": "Connection {status}",
|
||||
"instanceShell.connection.ariaLabel": "Conexión {status}",
|
||||
"instanceShell.connection.connected": "Conectada",
|
||||
"instanceShell.connection.connecting": "Conectando...",
|
||||
"instanceShell.connection.disconnected": "Desconectada",
|
||||
@@ -93,16 +93,22 @@ export const instanceMessages = {
|
||||
"instanceShell.rightPanel.tabs.files": "Archivos",
|
||||
"instanceShell.rightPanel.tabs.status": "Estado",
|
||||
"instanceShell.rightPanel.tabs.ariaLabel": "Pestañas del panel derecho",
|
||||
"instanceShell.rightPanel.sections.sessionChanges": "Cambios de sesion",
|
||||
"instanceShell.rightPanel.sections.sessionChanges": "Cambios de sesión",
|
||||
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Archivos modificados en la sesión actual. Muestra las adiciones y eliminaciones de cada archivo.",
|
||||
"instanceShell.rightPanel.sections.plan": "Plan",
|
||||
"instanceShell.rightPanel.sections.plan.tooltip": "Hoja de ruta del agente para esta sesión. Realiza el seguimiento de tareas, subtareas y su estado de finalización.",
|
||||
"instanceShell.rightPanel.sections.backgroundProcesses": "Shells en segundo plano",
|
||||
"instanceShell.rightPanel.sections.backgroundProcesses.tooltip": "Procesos de larga duración iniciados por el agente. Puedes supervisar su salida, detenerlos o terminarlos.",
|
||||
"instanceShell.rightPanel.sections.mcp": "Servidores MCP",
|
||||
"instanceShell.rightPanel.sections.mcp.tooltip": "Servidores del Model Context Protocol (MCP) que amplían las capacidades del agente con herramientas y servicios externos.",
|
||||
"instanceShell.rightPanel.sections.lsp": "Servidores LSP",
|
||||
"instanceShell.rightPanel.sections.lsp.tooltip": "Servidores del Language Server Protocol (LSP) que proporcionan inteligencia de código, diagnósticos y funciones específicas del lenguaje.",
|
||||
"instanceShell.rightPanel.sections.plugins": "Plugins",
|
||||
"instanceShell.rightPanel.sections.plugins.tooltip": "Plugins que personalizan el comportamiento de la UI y del servidor, y añaden funciones más allá de MCP y LSP.",
|
||||
|
||||
"instanceShell.sessionChanges.noSessionSelected": "Selecciona una sesion para ver los cambios.",
|
||||
"instanceShell.sessionChanges.loading": "Obteniendo cambios de la sesion...",
|
||||
"instanceShell.sessionChanges.empty": "Aun no hay cambios.",
|
||||
"instanceShell.sessionChanges.noSessionSelected": "Selecciona una sesión para ver los cambios.",
|
||||
"instanceShell.sessionChanges.loading": "Obteniendo cambios de la sesión...",
|
||||
"instanceShell.sessionChanges.empty": "Aún no hay cambios.",
|
||||
"instanceShell.sessionChanges.filesChanged": "{count} archivos cambiados",
|
||||
"instanceShell.sessionChanges.actions.show": "Mostrar cambios",
|
||||
|
||||
|
||||
@@ -94,11 +94,17 @@ export const instanceMessages = {
|
||||
"instanceShell.rightPanel.tabs.status": "Statut",
|
||||
"instanceShell.rightPanel.tabs.ariaLabel": "Onglets du panneau droit",
|
||||
"instanceShell.rightPanel.sections.sessionChanges": "Changements de session",
|
||||
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Fichiers modifiés dans la session actuelle. Affiche les ajouts et suppressions pour chaque fichier.",
|
||||
"instanceShell.rightPanel.sections.plan": "Plan",
|
||||
"instanceShell.rightPanel.sections.plan.tooltip": "Feuille de route de l'agent pour cette session. Suit les tâches et leur statut d'achèvement.",
|
||||
"instanceShell.rightPanel.sections.backgroundProcesses": "Shells en arrière-plan",
|
||||
"instanceShell.rightPanel.sections.backgroundProcesses.tooltip": "Processus longs démarrés par l'agent. Vous pouvez surveiller leur sortie, les arrêter ou les terminer.",
|
||||
"instanceShell.rightPanel.sections.mcp": "Serveurs MCP",
|
||||
"instanceShell.rightPanel.sections.mcp.tooltip": "Serveurs du protocole Model Context Protocol qui étendent les capacités de l'agent avec des outils externes.",
|
||||
"instanceShell.rightPanel.sections.lsp": "Serveurs LSP",
|
||||
"instanceShell.rightPanel.sections.lsp.tooltip": "Serveurs du protocole Language Server Protocol fournissant l'intelligence de code et les diagnostics.",
|
||||
"instanceShell.rightPanel.sections.plugins": "Plugins",
|
||||
"instanceShell.rightPanel.sections.plugins.tooltip": "Plugins qui personnalisent le comportement de l'UI et du serveur, ajoutant des fonctionnalités au-delà de MCP et LSP.",
|
||||
|
||||
"instanceShell.sessionChanges.noSessionSelected": "Sélectionnez une session pour voir les changements.",
|
||||
"instanceShell.sessionChanges.loading": "Récupération des changements...",
|
||||
|
||||
@@ -94,11 +94,17 @@ export const instanceMessages = {
|
||||
"instanceShell.rightPanel.tabs.status": "ステータス",
|
||||
"instanceShell.rightPanel.tabs.ariaLabel": "右パネルのタブ",
|
||||
"instanceShell.rightPanel.sections.sessionChanges": "セッション変更",
|
||||
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "現在のセッションで変更されたファイル。各ファイルの追加と削除を表示します。",
|
||||
"instanceShell.rightPanel.sections.plan": "計画",
|
||||
"instanceShell.rightPanel.sections.plan.tooltip": "このセッションにおけるエージェントのロードマップ。タスクやサブタスク、および完了状況を追跡します。",
|
||||
"instanceShell.rightPanel.sections.backgroundProcesses": "バックグラウンドシェル",
|
||||
"instanceShell.rightPanel.sections.backgroundProcesses.tooltip": "エージェントが開始した長時間実行プロセス。出力を監視し、停止または終了できます。",
|
||||
"instanceShell.rightPanel.sections.mcp": "MCP サーバー",
|
||||
"instanceShell.rightPanel.sections.mcp.tooltip": "Model Context Protocol (MCP) サーバー。外部ツールやサービスでエージェントの機能を拡張します。",
|
||||
"instanceShell.rightPanel.sections.lsp": "LSP サーバー",
|
||||
"instanceShell.rightPanel.sections.lsp.tooltip": "Language Server Protocolサーバーがコードインテリジェンス、診断、言語固有の機能を提供します。",
|
||||
"instanceShell.rightPanel.sections.plugins": "プラグイン",
|
||||
"instanceShell.rightPanel.sections.plugins.tooltip": "UI とサーバーの動作をカスタマイズし、MCP や LSP 以外の機能も追加できるプラグイン。",
|
||||
|
||||
"instanceShell.sessionChanges.noSessionSelected": "変更を表示するにはセッションを選択してください。",
|
||||
"instanceShell.sessionChanges.loading": "変更を取得中...",
|
||||
|
||||
@@ -94,11 +94,17 @@ export const instanceMessages = {
|
||||
"instanceShell.rightPanel.tabs.status": "Статус",
|
||||
"instanceShell.rightPanel.tabs.ariaLabel": "Вкладки правой панели",
|
||||
"instanceShell.rightPanel.sections.sessionChanges": "Изменения сессии",
|
||||
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "Файлы, измененные в текущей сессии. Показывает добавления и удаления для каждого файла.",
|
||||
"instanceShell.rightPanel.sections.plan": "План",
|
||||
"instanceShell.rightPanel.sections.backgroundProcesses": "Фоновые Shell",
|
||||
"instanceShell.rightPanel.sections.plan.tooltip": "Дорожная карта агента для этой сессии. Отслеживает задачи и их статус выполнения.",
|
||||
"instanceShell.rightPanel.sections.backgroundProcesses": "Фоновые оболочки",
|
||||
"instanceShell.rightPanel.sections.backgroundProcesses.tooltip": "Долгоработающие процессы, запущенные агентом. Вы можете следить за их выводом, останавливать или завершать их.",
|
||||
"instanceShell.rightPanel.sections.mcp": "MCP-серверы",
|
||||
"instanceShell.rightPanel.sections.mcp.tooltip": "Серверы протокола Model Context Protocol, расширяющие возможности агента внешними инструментами.",
|
||||
"instanceShell.rightPanel.sections.lsp": "LSP-серверы",
|
||||
"instanceShell.rightPanel.sections.lsp.tooltip": "Серверы протокола Language Server Protocol, обеспечивающие интеллектуальную поддержку кода и диагностику.",
|
||||
"instanceShell.rightPanel.sections.plugins": "Плагины",
|
||||
"instanceShell.rightPanel.sections.plugins.tooltip": "Плагины, настраивающие поведение интерфейса и сервера, добавляющие функции поверх MCP и LSP.",
|
||||
|
||||
"instanceShell.sessionChanges.noSessionSelected": "Выберите сессию, чтобы просмотреть изменения.",
|
||||
"instanceShell.sessionChanges.loading": "Загрузка изменений...",
|
||||
@@ -128,7 +134,7 @@ export const instanceMessages = {
|
||||
"versionPill.uiWithVersion": "UI {version}",
|
||||
"versionPill.source": " ({source})",
|
||||
|
||||
"opencodeBinarySelector.title": "OpenCode Binary",
|
||||
"opencodeBinarySelector.title": "Бинарник OpenCode",
|
||||
"opencodeBinarySelector.subtitle": "Выберите, какой исполняемый файл OpenCode запускать",
|
||||
"opencodeBinarySelector.customPath.placeholder": "Введите путь к бинарнику opencode…",
|
||||
"opencodeBinarySelector.actions.add": "Добавить",
|
||||
|
||||
@@ -94,11 +94,17 @@ export const instanceMessages = {
|
||||
"instanceShell.rightPanel.tabs.status": "状态",
|
||||
"instanceShell.rightPanel.tabs.ariaLabel": "右侧面板标签页",
|
||||
"instanceShell.rightPanel.sections.sessionChanges": "会话更改",
|
||||
"instanceShell.rightPanel.sections.sessionChanges.tooltip": "当前会话中修改的文件。显示每个文件的添加和删除。",
|
||||
"instanceShell.rightPanel.sections.plan": "计划",
|
||||
"instanceShell.rightPanel.sections.plan.tooltip": "代理的路线图。跟踪任务、子任务及其完成状态。",
|
||||
"instanceShell.rightPanel.sections.backgroundProcesses": "后台 Shell",
|
||||
"instanceShell.rightPanel.sections.backgroundProcesses.tooltip": "代理启动的后台进程。您可以监控其输出、停止或终止它们。",
|
||||
"instanceShell.rightPanel.sections.mcp": "MCP 服务器",
|
||||
"instanceShell.rightPanel.sections.mcp.tooltip": "模型上下文协议服务器,使用外部工具和服务扩展代理能力。",
|
||||
"instanceShell.rightPanel.sections.lsp": "LSP 服务器",
|
||||
"instanceShell.rightPanel.sections.lsp.tooltip": "语言服务器协议服务器,提供代码智能、诊断和语言特定的功能。",
|
||||
"instanceShell.rightPanel.sections.plugins": "插件",
|
||||
"instanceShell.rightPanel.sections.plugins.tooltip": "自定义 UI 和服务器行为的插件,添加超出 MCP 和 LSP 的功能。",
|
||||
|
||||
"instanceShell.sessionChanges.noSessionSelected": "选择会话以查看更改。",
|
||||
"instanceShell.sessionChanges.loading": "正在获取会话更改...",
|
||||
|
||||
@@ -127,17 +127,23 @@ async function ensureLanguages(content: string) {
|
||||
if (highlightSuppressed) {
|
||||
return
|
||||
}
|
||||
// Parse code fences to extract language tokens
|
||||
// Updated regex to capture optional language tokens and handle trailing annotations
|
||||
const codeBlockRegex = /```[ \t]*([A-Za-z0-9_.+#-]+)?[^`]*?```/g
|
||||
const foundLanguages = new Set<string>()
|
||||
let match
|
||||
|
||||
while ((match = codeBlockRegex.exec(content)) !== null) {
|
||||
const langToken = match[1]
|
||||
if (langToken && langToken.trim()) {
|
||||
foundLanguages.add(langToken.trim())
|
||||
}
|
||||
// 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>()
|
||||
try {
|
||||
const tokens = marked.lexer(content) as any
|
||||
marked.walkTokens(tokens, (token: any) => {
|
||||
if (token?.type !== "code") return
|
||||
const langToken = typeof token.lang === "string" ? token.lang : ""
|
||||
if (langToken.trim()) {
|
||||
foundLanguages.add(langToken.trim())
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
// If tokenization fails for any reason, skip language preloading.
|
||||
return
|
||||
}
|
||||
|
||||
// Queue language loading tasks
|
||||
|
||||
@@ -40,7 +40,7 @@ import {
|
||||
} from "./instances"
|
||||
import { showAlertDialog } from "./alerts"
|
||||
import { createClientSession, mapSdkSessionStatus, type Session, type SessionStatus } from "../types/session"
|
||||
import { sessions, setSessions, syncInstanceSessionIndicator, withSession } from "./session-state"
|
||||
import { ensureSessionParentExpanded, sessions, setSessions, syncInstanceSessionIndicator, withSession } from "./session-state"
|
||||
import { normalizeMessagePart } from "./message-v2/normalizers"
|
||||
import { updateSessionInfo } from "./message-v2/session-info"
|
||||
import { tGlobal } from "../lib/i18n"
|
||||
@@ -108,6 +108,8 @@ interface TuiToastEvent {
|
||||
const ALLOWED_TOAST_VARIANTS = new Set<ToastVariant>(["info", "success", "warning", "error"])
|
||||
|
||||
function applySessionStatus(instanceId: string, sessionId: string, status: SessionStatus) {
|
||||
let parentToExpand: string | null = null
|
||||
|
||||
withSession(instanceId, sessionId, (session) => {
|
||||
const current = session.status ?? "idle"
|
||||
if (current === status) return false
|
||||
@@ -117,7 +119,17 @@ function applySessionStatus(instanceId: string, sessionId: string, status: Sessi
|
||||
}
|
||||
|
||||
session.status = status
|
||||
|
||||
// Auto-expand the parent thread when a child session starts working.
|
||||
// Users can still collapse it; we only expand on the transition.
|
||||
if (session.parentId && status === "working" && current !== "working") {
|
||||
parentToExpand = session.parentId
|
||||
}
|
||||
})
|
||||
|
||||
if (parentToExpand) {
|
||||
ensureSessionParentExpanded(instanceId, parentToExpand)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchSessionInfo(instanceId: string, sessionId: string, directory?: string): Promise<Session | null> {
|
||||
@@ -158,6 +170,7 @@ async function fetchSessionInfo(instanceId: string, sessionId: string, directory
|
||||
const fetched = createClientSession(info, instanceId, "", { providerId: "", modelId: "" }, fetchedStatus)
|
||||
|
||||
let updatedInstanceSessions: Map<string, Session> | undefined
|
||||
let shouldExpandParent: string | null = null
|
||||
|
||||
setSessions((prev) => {
|
||||
const next = new Map(prev)
|
||||
@@ -174,11 +187,19 @@ async function fetchSessionInfo(instanceId: string, sessionId: string, directory
|
||||
instanceSessions.set(sessionId, merged)
|
||||
next.set(instanceId, instanceSessions)
|
||||
updatedInstanceSessions = instanceSessions
|
||||
|
||||
if (merged.parentId && merged.status === "working" && (existing?.status ?? "idle") !== "working") {
|
||||
shouldExpandParent = merged.parentId
|
||||
}
|
||||
return next
|
||||
})
|
||||
|
||||
syncInstanceSessionIndicator(instanceId, updatedInstanceSessions)
|
||||
|
||||
if (shouldExpandParent) {
|
||||
ensureSessionParentExpanded(instanceId, shouldExpandParent)
|
||||
}
|
||||
|
||||
return fetched
|
||||
} catch (error) {
|
||||
log.error("Failed to fetch session info", error)
|
||||
|
||||
@@ -347,10 +347,23 @@ function clearActiveParentSession(instanceId: string): void {
|
||||
}
|
||||
|
||||
function setSessionStatus(instanceId: string, sessionId: string, status: SessionStatus): void {
|
||||
let parentToExpand: string | null = null
|
||||
|
||||
withSession(instanceId, sessionId, (session) => {
|
||||
if (session.status === status) return false
|
||||
const previous = session.status
|
||||
session.status = status
|
||||
|
||||
// If a child session starts working, auto-expand its parent thread once.
|
||||
// Users can still collapse it afterwards; we only expand on the transition.
|
||||
if (session.parentId && status === "working" && previous !== "working") {
|
||||
parentToExpand = session.parentId
|
||||
}
|
||||
})
|
||||
|
||||
if (parentToExpand) {
|
||||
ensureSessionParentExpanded(instanceId, parentToExpand)
|
||||
}
|
||||
}
|
||||
|
||||
function getActiveParentSession(instanceId: string): Session | null {
|
||||
|
||||
@@ -130,6 +130,19 @@
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Make the command palette trigger stand out in the header. */
|
||||
.connection-status-button.command-palette-button {
|
||||
border-radius: 0;
|
||||
@apply text-sm px-2 py-1 border border-base transition-colors;
|
||||
background-color: var(--surface-base);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.connection-status-button.command-palette-button:hover {
|
||||
background-color: var(--surface-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.connection-status-button:hover {
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
@@ -412,7 +412,7 @@
|
||||
}
|
||||
|
||||
.right-panel-accordion-trigger {
|
||||
@apply w-full flex items-center justify-between gap-3 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);
|
||||
background-color: transparent;
|
||||
}
|
||||
@@ -422,6 +422,11 @@
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.section-left {
|
||||
@apply flex items-center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.right-panel-accordion-chevron {
|
||||
@apply h-4 w-4 transition-transform duration-200;
|
||||
color: var(--text-muted);
|
||||
@@ -441,6 +446,51 @@
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Section info tooltip */
|
||||
.section-info-trigger {
|
||||
@apply inline-flex items-center justify-center p-0.5 rounded transition-all duration-150;
|
||||
color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.section-info-trigger:hover {
|
||||
color: var(--text-primary);
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
.section-label {
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.section-info-icon {
|
||||
@apply w-3.5 h-3.5;
|
||||
}
|
||||
|
||||
.section-info-tooltip {
|
||||
@apply max-w-xs px-3 py-2 text-xs rounded-lg border shadow-lg;
|
||||
background-color: var(--surface-base);
|
||||
border-color: var(--border-base);
|
||||
color: var(--text-primary);
|
||||
animation: tooltipShow 150ms ease-out;
|
||||
transform-origin: var(--kb-tooltip-content-transform-origin);
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.section-info-tooltip[data-expanded] {
|
||||
animation: tooltipShow 150ms ease-out;
|
||||
}
|
||||
|
||||
@keyframes tooltipShow {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Background process cards in status panel */
|
||||
.status-process-card {
|
||||
@apply rounded-lg border flex flex-col gap-2 p-3 transition-all duration-150;
|
||||
|
||||
Reference in New Issue
Block a user