Compare commits
18 Commits
feature/st
...
feature/do
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b6bf61ea4 | |||
| 5974347913 | |||
| 805f815876 | |||
| 5360f22b65 | |||
| 0316234329 | |||
| ee68cc820c | |||
| 7f6655fbcf | |||
| b907ad0239 | |||
| de1d809de5 | |||
| 3c7852544b | |||
| ddf44d97e5 | |||
| d60124f1bd | |||
| 4f23951379 | |||
| d6ac3ae6c6 | |||
| ef67b447b3 | |||
| 15b03173a5 | |||
| a0b4dca0bd | |||
| 17c5d699f9 |
317
.gitea/workflows/build-app-preview.yml
Normal file
317
.gitea/workflows/build-app-preview.yml
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
name: Build App (Preview)
|
||||||
|
|
||||||
|
# Builds the Tauri app for branches other than main and exposes the bundles as
|
||||||
|
# workflow artifacts. No Gitea release, no GitHub sync — intended for local
|
||||||
|
# smoke-testing of feature branches before they merge.
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
compute-version:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.version.outputs.VERSION }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Compute preview version
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
MAJOR_MINOR=$(cat VERSION | tr -d '[:space:]')
|
||||||
|
SHORT_SHA=$(git rev-parse --short HEAD)
|
||||||
|
VERSION="${MAJOR_MINOR}.0-preview.${SHORT_SHA}"
|
||||||
|
echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT
|
||||||
|
echo "Computed preview version: ${VERSION}"
|
||||||
|
|
||||||
|
build-linux:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [compute-version]
|
||||||
|
steps:
|
||||||
|
- name: Install Node.js 22
|
||||||
|
run: |
|
||||||
|
NEED_INSTALL=false
|
||||||
|
if command -v node >/dev/null 2>&1; then
|
||||||
|
NODE_MAJOR=$(node --version | sed 's/v\([0-9]*\).*/\1/')
|
||||||
|
OLD_NODE_DIR=$(dirname "$(which node)")
|
||||||
|
echo "Found Node.js $(node --version) at $(which node) (major: ${NODE_MAJOR})"
|
||||||
|
if [ "$NODE_MAJOR" -lt 22 ]; then
|
||||||
|
echo "Node.js ${NODE_MAJOR} is too old, removing before installing 22..."
|
||||||
|
sudo rm -f "${OLD_NODE_DIR}/node" "${OLD_NODE_DIR}/npm" "${OLD_NODE_DIR}/npx" "${OLD_NODE_DIR}/corepack"
|
||||||
|
hash -r
|
||||||
|
NEED_INSTALL=true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Node.js not found, installing 22..."
|
||||||
|
NEED_INSTALL=true
|
||||||
|
fi
|
||||||
|
if [ "$NEED_INSTALL" = true ]; then
|
||||||
|
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
|
||||||
|
sudo apt-get install -y nodejs
|
||||||
|
hash -r
|
||||||
|
fi
|
||||||
|
node --version
|
||||||
|
npm --version
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set app version
|
||||||
|
run: |
|
||||||
|
# Tauri / Cargo require a strict semver; strip the preview suffix for
|
||||||
|
# the bundle version but keep it in the artifact filename.
|
||||||
|
BASE_VERSION="$(echo '${{ needs.compute-version.outputs.version }}' | cut -d'-' -f1)"
|
||||||
|
sed -i "s/\"version\": \".*\"/\"version\": \"${BASE_VERSION}\"/" app/src-tauri/tauri.conf.json
|
||||||
|
sed -i "s/\"version\": \".*\"/\"version\": \"${BASE_VERSION}\"/" app/package.json
|
||||||
|
sed -i "s/^version = \".*\"/version = \"${BASE_VERSION}\"/" app/src-tauri/Cargo.toml
|
||||||
|
echo "Patched version to ${BASE_VERSION}"
|
||||||
|
|
||||||
|
- name: Install system dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y \
|
||||||
|
libgtk-3-dev \
|
||||||
|
libwebkit2gtk-4.1-dev \
|
||||||
|
libayatana-appindicator3-dev \
|
||||||
|
librsvg2-dev \
|
||||||
|
libsoup-3.0-dev \
|
||||||
|
libssl-dev \
|
||||||
|
libxdo-dev \
|
||||||
|
patchelf \
|
||||||
|
pkg-config \
|
||||||
|
build-essential \
|
||||||
|
curl \
|
||||||
|
wget \
|
||||||
|
file \
|
||||||
|
xdg-utils
|
||||||
|
|
||||||
|
- name: Install Rust stable
|
||||||
|
run: |
|
||||||
|
if command -v rustup >/dev/null 2>&1; then
|
||||||
|
rustup update stable
|
||||||
|
rustup default stable
|
||||||
|
else
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
|
||||||
|
fi
|
||||||
|
export PATH="$HOME/.cargo/bin:$PATH"
|
||||||
|
rustc --version
|
||||||
|
cargo --version
|
||||||
|
|
||||||
|
- name: Install frontend dependencies
|
||||||
|
working-directory: ./app
|
||||||
|
run: |
|
||||||
|
rm -rf node_modules package-lock.json
|
||||||
|
npm install
|
||||||
|
|
||||||
|
- name: Install Tauri CLI
|
||||||
|
working-directory: ./app
|
||||||
|
run: |
|
||||||
|
export PATH="$HOME/.cargo/bin:$PATH"
|
||||||
|
npx tauri --version || npm install @tauri-apps/cli
|
||||||
|
|
||||||
|
- name: Build Tauri app
|
||||||
|
working-directory: ./app
|
||||||
|
run: |
|
||||||
|
export PATH="$HOME/.cargo/bin:$PATH"
|
||||||
|
npx tauri build
|
||||||
|
|
||||||
|
- name: Collect artifacts
|
||||||
|
run: |
|
||||||
|
mkdir -p artifacts
|
||||||
|
cp app/src-tauri/target/release/bundle/appimage/*.AppImage artifacts/ 2>/dev/null || true
|
||||||
|
cp app/src-tauri/target/release/bundle/deb/*.deb artifacts/ 2>/dev/null || true
|
||||||
|
cp app/src-tauri/target/release/bundle/rpm/*.rpm artifacts/ 2>/dev/null || true
|
||||||
|
ls -la artifacts/
|
||||||
|
|
||||||
|
- name: Upload Linux artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: triple-c-${{ needs.compute-version.outputs.version }}-linux
|
||||||
|
path: artifacts/
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 14
|
||||||
|
|
||||||
|
build-macos:
|
||||||
|
runs-on: macos-latest
|
||||||
|
needs: [compute-version]
|
||||||
|
steps:
|
||||||
|
- name: Install Node.js 22
|
||||||
|
run: |
|
||||||
|
NEED_INSTALL=false
|
||||||
|
if command -v node >/dev/null 2>&1; then
|
||||||
|
NODE_MAJOR=$(node --version | sed 's/v\([0-9]*\).*/\1/')
|
||||||
|
if [ "$NODE_MAJOR" -lt 22 ]; then
|
||||||
|
NEED_INSTALL=true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
NEED_INSTALL=true
|
||||||
|
fi
|
||||||
|
if [ "$NEED_INSTALL" = true ]; then
|
||||||
|
brew install node@22
|
||||||
|
brew link --overwrite node@22
|
||||||
|
fi
|
||||||
|
node --version
|
||||||
|
npm --version
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set app version
|
||||||
|
run: |
|
||||||
|
BASE_VERSION="$(echo '${{ needs.compute-version.outputs.version }}' | cut -d'-' -f1)"
|
||||||
|
sed -i '' "s/\"version\": \".*\"/\"version\": \"${BASE_VERSION}\"/" app/src-tauri/tauri.conf.json
|
||||||
|
sed -i '' "s/\"version\": \".*\"/\"version\": \"${BASE_VERSION}\"/" app/package.json
|
||||||
|
sed -i '' "s/^version = \".*\"/version = \"${BASE_VERSION}\"/" app/src-tauri/Cargo.toml
|
||||||
|
echo "Patched version to ${BASE_VERSION}"
|
||||||
|
|
||||||
|
- name: Install Rust stable
|
||||||
|
run: |
|
||||||
|
if command -v rustup >/dev/null 2>&1; then
|
||||||
|
rustup update stable
|
||||||
|
rustup default stable
|
||||||
|
else
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
|
||||||
|
fi
|
||||||
|
export PATH="$HOME/.cargo/bin:$PATH"
|
||||||
|
rustup target add aarch64-apple-darwin x86_64-apple-darwin
|
||||||
|
rustc --version
|
||||||
|
cargo --version
|
||||||
|
|
||||||
|
- name: Install frontend dependencies
|
||||||
|
working-directory: ./app
|
||||||
|
run: |
|
||||||
|
rm -rf node_modules
|
||||||
|
npm install
|
||||||
|
|
||||||
|
- name: Install Tauri CLI
|
||||||
|
working-directory: ./app
|
||||||
|
run: |
|
||||||
|
export PATH="$HOME/.cargo/bin:$PATH"
|
||||||
|
npx tauri --version || npm install @tauri-apps/cli
|
||||||
|
|
||||||
|
- name: Build Tauri app (universal)
|
||||||
|
working-directory: ./app
|
||||||
|
run: |
|
||||||
|
export PATH="$HOME/.cargo/bin:$PATH"
|
||||||
|
npx tauri build --target universal-apple-darwin
|
||||||
|
|
||||||
|
- name: Collect artifacts
|
||||||
|
run: |
|
||||||
|
mkdir -p artifacts
|
||||||
|
cp app/src-tauri/target/universal-apple-darwin/release/bundle/dmg/*.dmg artifacts/ 2>/dev/null || true
|
||||||
|
cp app/src-tauri/target/universal-apple-darwin/release/bundle/macos/*.app.tar.gz artifacts/ 2>/dev/null || true
|
||||||
|
ls -la artifacts/
|
||||||
|
|
||||||
|
- name: Upload macOS artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: triple-c-${{ needs.compute-version.outputs.version }}-macos
|
||||||
|
path: artifacts/
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 14
|
||||||
|
|
||||||
|
build-windows:
|
||||||
|
runs-on: windows-latest
|
||||||
|
needs: [compute-version]
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: cmd
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set app version
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
$raw = "${{ needs.compute-version.outputs.version }}"
|
||||||
|
$version = $raw.Split('-')[0]
|
||||||
|
(Get-Content app/src-tauri/tauri.conf.json) -replace '"version": ".*?"', "`"version`": `"$version`"" | Set-Content app/src-tauri/tauri.conf.json
|
||||||
|
(Get-Content app/package.json) -replace '"version": ".*?"', "`"version`": `"$version`"" | Set-Content app/package.json
|
||||||
|
(Get-Content app/src-tauri/Cargo.toml) -replace '^version = ".*?"', "version = `"$version`"" | Set-Content app/src-tauri/Cargo.toml
|
||||||
|
Write-Host "Patched version to $version"
|
||||||
|
|
||||||
|
- name: Install Rust stable
|
||||||
|
run: |
|
||||||
|
where rustup >nul 2>&1 && (
|
||||||
|
rustup update stable
|
||||||
|
rustup default stable
|
||||||
|
) || (
|
||||||
|
curl -fSL -o rustup-init.exe https://win.rustup.rs/x86_64
|
||||||
|
rustup-init.exe -y --default-toolchain stable
|
||||||
|
del rustup-init.exe
|
||||||
|
)
|
||||||
|
|
||||||
|
- name: Install Node.js
|
||||||
|
run: |
|
||||||
|
where node >nul 2>&1 && (
|
||||||
|
node --version
|
||||||
|
) || (
|
||||||
|
curl -fSL -o node-install.msi "https://nodejs.org/dist/v22.14.0/node-v22.14.0-x64.msi"
|
||||||
|
msiexec /i node-install.msi /quiet /norestart
|
||||||
|
del node-install.msi
|
||||||
|
)
|
||||||
|
|
||||||
|
- name: Verify tools
|
||||||
|
run: |
|
||||||
|
set "PATH=%USERPROFILE%\.cargo\bin;C:\Program Files\nodejs;%PATH%"
|
||||||
|
rustc --version
|
||||||
|
cargo --version
|
||||||
|
node --version
|
||||||
|
npm --version
|
||||||
|
|
||||||
|
- name: Install Tauri CLI via cargo
|
||||||
|
run: |
|
||||||
|
set "PATH=%USERPROFILE%\.cargo\bin;C:\Program Files\nodejs;%PATH%"
|
||||||
|
cargo install tauri-cli --version "^2"
|
||||||
|
|
||||||
|
- name: Fix npm platform detection
|
||||||
|
run: |
|
||||||
|
set "PATH=%USERPROFILE%\.cargo\bin;C:\Program Files\nodejs;%PATH%"
|
||||||
|
npm config set os win32
|
||||||
|
npm config list
|
||||||
|
|
||||||
|
- name: Install frontend dependencies
|
||||||
|
working-directory: ./app
|
||||||
|
run: |
|
||||||
|
set "PATH=%USERPROFILE%\.cargo\bin;C:\Program Files\nodejs;%PATH%"
|
||||||
|
if exist node_modules rmdir /s /q node_modules
|
||||||
|
npm ci
|
||||||
|
|
||||||
|
- name: Build frontend
|
||||||
|
working-directory: ./app
|
||||||
|
run: |
|
||||||
|
set "PATH=%USERPROFILE%\.cargo\bin;C:\Program Files\nodejs;%PATH%"
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
- name: Build Tauri app
|
||||||
|
working-directory: ./app
|
||||||
|
env:
|
||||||
|
TAURI_CONFIG: "{\"build\":{\"beforeBuildCommand\":\"\"}}"
|
||||||
|
run: |
|
||||||
|
set "PATH=%USERPROFILE%\.cargo\bin;C:\Program Files\nodejs;%PATH%"
|
||||||
|
cargo tauri build
|
||||||
|
|
||||||
|
- name: Collect artifacts
|
||||||
|
run: |
|
||||||
|
set "PATH=%USERPROFILE%\.cargo\bin;C:\Program Files\nodejs;%PATH%"
|
||||||
|
mkdir artifacts
|
||||||
|
copy app\src-tauri\target\release\bundle\msi\*.msi artifacts\ 2>nul
|
||||||
|
copy app\src-tauri\target\release\bundle\nsis\*.exe artifacts\ 2>nul
|
||||||
|
dir artifacts\
|
||||||
|
|
||||||
|
- name: Upload Windows artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: triple-c-${{ needs.compute-version.outputs.version }}-windows
|
||||||
|
path: artifacts/
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 14
|
||||||
@@ -40,7 +40,8 @@ jobs:
|
|||||||
echo "Major.Minor: ${MAJOR_MINOR}"
|
echo "Major.Minor: ${MAJOR_MINOR}"
|
||||||
|
|
||||||
# Find the latest tag matching v{MAJOR_MINOR}.N (exclude -mac, -win suffixes)
|
# Find the latest tag matching v{MAJOR_MINOR}.N (exclude -mac, -win suffixes)
|
||||||
LATEST_TAG=$(git tag -l "v${MAJOR_MINOR}.*" --sort=-v:refname | grep -E "^v${MAJOR_MINOR}\.[0-9]+$" | head -1)
|
# `|| true` so an empty grep result doesn't fail the step under pipefail.
|
||||||
|
LATEST_TAG=$(git tag -l "v${MAJOR_MINOR}.*" --sort=-v:refname | grep -E "^v${MAJOR_MINOR}\.[0-9]+$" | head -1 || true)
|
||||||
|
|
||||||
if [ -n "$LATEST_TAG" ]; then
|
if [ -n "$LATEST_TAG" ]; then
|
||||||
echo "Latest matching tag: ${LATEST_TAG}"
|
echo "Latest matching tag: ${LATEST_TAG}"
|
||||||
|
|||||||
@@ -76,13 +76,13 @@ docker exec stdout → tokio task → emit("terminal-output-{sessionId}") → li
|
|||||||
- `server.rs` — Axum server lifecycle (start/stop), serves embedded HTML and handles WS upgrades
|
- `server.rs` — Axum server lifecycle (start/stop), serves embedded HTML and handles WS upgrades
|
||||||
- `ws_handler.rs` — Per-connection WebSocket handler with JSON protocol, session management, cleanup on disconnect
|
- `ws_handler.rs` — Per-connection WebSocket handler with JSON protocol, session management, cleanup on disconnect
|
||||||
- `terminal.html` — Self-contained xterm.js web UI embedded via `include_str!()`
|
- `terminal.html` — Self-contained xterm.js web UI embedded via `include_str!()`
|
||||||
- **`models/`** — Serde structs (`Project`, `Backend`, `BedrockConfig`, `OllamaConfig`, `OpenAiCompatibleConfig`, `ContainerInfo`, `AppSettings`, `WebTerminalSettings`). These define the IPC contract with the frontend.
|
- **`models/`** — Serde structs (`Project`, `Backend`, `BedrockConfig`, `OllamaConfig`, `OpenAiCompatibleConfig`, `ClaudeCodeSettings`, `ContainerInfo`, `AppSettings`, `WebTerminalSettings`). These define the IPC contract with the frontend.
|
||||||
- **`storage/`** — Persistence: `projects_store.rs` (JSON file with atomic writes), `secure.rs` (OS keychain via `keyring` crate), `settings_store.rs`
|
- **`storage/`** — Persistence: `projects_store.rs` (JSON file with atomic writes), `secure.rs` (OS keychain via `keyring` crate), `settings_store.rs`
|
||||||
|
|
||||||
### Container (`container/`)
|
### Container (`container/`)
|
||||||
|
|
||||||
- **`Dockerfile`** — Ubuntu 24.04 base with Claude Code, Node.js 22, Python 3.12, Rust, Docker CLI, git, gh, AWS CLI v2, ripgrep, pnpm, uv, ruff pre-installed
|
- **`Dockerfile`** — Ubuntu 24.04 base with Claude Code, Node.js 22, Python 3.12, Rust, Docker CLI, git, gh, AWS CLI v2, ripgrep, pnpm, uv, ruff pre-installed
|
||||||
- **`entrypoint.sh`** — UID/GID remapping to match host user, SSH key setup, git config, docker socket permissions, then `sleep infinity`
|
- **`entrypoint.sh`** — UID/GID remapping to match host user, SSH key setup, git config, docker socket permissions, Claude Code settings.json injection, then `sleep infinity`
|
||||||
- **`triple-c-scheduler`** — Bash-based scheduled task system for recurring Claude Code invocations
|
- **`triple-c-scheduler`** — Bash-based scheduled task system for recurring Claude Code invocations
|
||||||
|
|
||||||
### Container Lifecycle
|
### Container Lifecycle
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ When **disabled** (default), Claude prompts you for approval before executing ea
|
|||||||
|
|
||||||
Click **Edit** to open the environment variables modal. Add key-value pairs that will be injected into the container. Per-project variables override global variables with the same key.
|
Click **Edit** to open the environment variables modal. Add key-value pairs that will be injected into the container. Per-project variables override global variables with the same key.
|
||||||
|
|
||||||
> Reserved prefixes (`ANTHROPIC_`, `AWS_`, `GIT_`, `HOST_`, `CLAUDE_`, `TRIPLE_C_`) are filtered out to prevent conflicts with internal variables.
|
> Reserved prefixes (`ANTHROPIC_`, `AWS_`, `GIT_`, `HOST_`, `TRIPLE_C_`) and specific internal variables (`CLAUDE_INSTRUCTIONS`, `MCP_SERVERS_JSON`, etc.) are filtered out to prevent conflicts. `CLAUDE_CODE_*` variables are now allowed, so you can set Claude Code feature flags directly (e.g., `CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1`).
|
||||||
|
|
||||||
### Port Mappings
|
### Port Mappings
|
||||||
|
|
||||||
@@ -268,6 +268,25 @@ Each mapping specifies:
|
|||||||
|
|
||||||
Click **Edit** to write per-project instructions for Claude Code. These are written to `~/.claude/CLAUDE.md` inside the container and provide project-specific context. If you also have global instructions (in Settings), the global instructions come first, followed by the per-project instructions.
|
Click **Edit** to write per-project instructions for Claude Code. These are written to `~/.claude/CLAUDE.md` inside the container and provide project-specific context. If you also have global instructions (in Settings), the global instructions come first, followed by the per-project instructions.
|
||||||
|
|
||||||
|
### Claude Code Settings
|
||||||
|
|
||||||
|
Click **Edit** next to "Claude Code Settings" to configure Claude Code CLI behavior for this project. These settings control how Claude Code operates inside the container:
|
||||||
|
|
||||||
|
| Setting | What It Does |
|
||||||
|
|---------|-------------|
|
||||||
|
| **TUI Mode** | Set to **Fullscreen** for flicker-free alt-screen rendering (uses `CLAUDE_CODE_NO_FLICKER=1`) |
|
||||||
|
| **Effort Level** | Controls reasoning depth: **Low** (fast, less thorough), **Medium**, **High** (deep reasoning) |
|
||||||
|
| **Focus Mode** | Collapses tool output to one-line summaries, showing only the prompt and final response |
|
||||||
|
| **Thinking Summaries** | Shows Claude's thinking process as summaries during responses |
|
||||||
|
| **Session Recap** | Provides context when returning to a session after being away |
|
||||||
|
| **Auto-Scroll Disabled** | Disables auto-scroll when in fullscreen TUI mode |
|
||||||
|
| **Env Scrub** | Strips credentials from subprocess environments for security |
|
||||||
|
| **Prompt Caching (1h)** | Enables 1-hour prompt cache TTL instead of the default 5 minutes |
|
||||||
|
|
||||||
|
Per-project settings override global defaults set in Settings. If all settings are at their defaults, no configuration is injected.
|
||||||
|
|
||||||
|
> These settings map to Claude Code environment variables and `~/.claude/settings.json` entries. Changes require stopping and restarting the container to take effect.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## MCP Servers (Beta)
|
## MCP Servers (Beta)
|
||||||
@@ -481,6 +500,18 @@ Instructions applied to **all** projects. Written to `~/.claude/CLAUDE.md` in ev
|
|||||||
|
|
||||||
Environment variables applied to **all** project containers. Per-project variables with the same key take precedence.
|
Environment variables applied to **all** project containers. Per-project variables with the same key take precedence.
|
||||||
|
|
||||||
|
### Default SSH Key Directory
|
||||||
|
|
||||||
|
Path to your SSH key directory (typically `~/.ssh`). This is mounted into **all** containers that don't have a per-project SSH path set. Per-project SSH paths take precedence.
|
||||||
|
|
||||||
|
### Default Git Name / Email
|
||||||
|
|
||||||
|
Sets `git user.name` and `git user.email` inside all containers. Per-project Git Name / Email settings take precedence. This is useful so you don't have to set the same name and email on every project.
|
||||||
|
|
||||||
|
### Claude Code Settings (Global Defaults)
|
||||||
|
|
||||||
|
Default Claude Code CLI settings applied to all projects. See [Claude Code Settings](#claude-code-settings) in the Project Configuration section for a description of each setting. Per-project settings override these global defaults.
|
||||||
|
|
||||||
### Web Terminal
|
### Web Terminal
|
||||||
|
|
||||||
Enable remote access to your project terminals from any device on the local network (tablets, phones, other computers).
|
Enable remote access to your project terminals from any device on the local network (tablets, phones, other computers).
|
||||||
@@ -543,7 +574,7 @@ The web terminal UI mirrors the desktop app's terminal experience:
|
|||||||
|
|
||||||
### Multiple Sessions
|
### Multiple Sessions
|
||||||
|
|
||||||
You can open multiple terminal sessions (even for the same project). Each session gets its own tab in the top bar. Click a tab to switch, or click the **x** on a tab to close it. Tabs show the project name, with a "(bash)" suffix for shell sessions.
|
You can open multiple terminal sessions (even for the same project). Each session gets its own tab in the top bar. Click a tab to switch, or click the **x** on a tab to close it. Tabs show the project name (or custom session name if provided), with a "(bash)" suffix for shell sessions.
|
||||||
|
|
||||||
### Bash Shell Sessions
|
### Bash Shell Sessions
|
||||||
|
|
||||||
@@ -668,6 +699,24 @@ You can install additional tools at runtime with `sudo apt install`, `pip instal
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Claude Code Tips
|
||||||
|
|
||||||
|
These features are built into Claude Code and work inside Triple-C containers with no extra configuration:
|
||||||
|
|
||||||
|
| Feature | How to Use |
|
||||||
|
|---------|-----------|
|
||||||
|
| **Focus Mode** | Run `/focus` or press `Ctrl+O` in the terminal to toggle collapsed tool output |
|
||||||
|
| **Session Recap** | Run `/recap` to get a summary of what happened in the current session |
|
||||||
|
| **Session Color** | Run `/color red` (or any color) to color-code your terminal prompt bar |
|
||||||
|
| **Recurring Tasks** | Run `/loop 5m check the deploy` to repeat a prompt every 5 minutes |
|
||||||
|
| **Interactive Lessons** | Run `/powerup` to learn Claude Code features with animated demos |
|
||||||
|
| **Team Onboarding** | Run `/team-onboarding` to generate a teammate ramp-up guide |
|
||||||
|
| **Bedrock Setup** | Select "3rd-party platform" on the login screen for an interactive Bedrock setup wizard |
|
||||||
|
| **Vertex AI Setup** | Select "3rd-party platform" on the login screen for an interactive Vertex AI setup wizard |
|
||||||
|
| **MCP Elicitation** | MCP servers can now request structured user input mid-task — works automatically |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Docker is "Not Available"
|
### Docker is "Not Available"
|
||||||
|
|||||||
31
README.md
31
README.md
@@ -27,7 +27,7 @@ Triple-C is a cross-platform desktop application that sandboxes Claude Code insi
|
|||||||
### Container Lifecycle
|
### Container Lifecycle
|
||||||
|
|
||||||
1. **Create**: New container created with bind mounts, env vars, and labels
|
1. **Create**: New container created with bind mounts, env vars, and labels
|
||||||
2. **Start**: Container started, entrypoint remaps UID/GID, sets up SSH, configures Docker group, sets up MCP servers
|
2. **Start**: Container started, entrypoint remaps UID/GID, sets up SSH, configures Docker group, sets up MCP servers, injects Claude Code settings
|
||||||
3. **Terminal**: `docker exec` launches Claude Code (or bash shell) with a PTY
|
3. **Terminal**: `docker exec` launches Claude Code (or bash shell) with a PTY
|
||||||
4. **Stop**: Container halted (filesystem persists in named volume); MCP containers stopped
|
4. **Stop**: Container halted (filesystem persists in named volume); MCP containers stopped
|
||||||
5. **Restart**: Existing container restarted; recreated if settings changed (detected via SHA-256 fingerprint)
|
5. **Restart**: Existing container restarted; recreated if settings changed (detected via SHA-256 fingerprint)
|
||||||
@@ -97,6 +97,19 @@ Triple-C includes an optional web terminal server for accessing project terminal
|
|||||||
|
|
||||||
The web terminal shares the existing `ExecSessionManager` via `Arc`-wrapped stores — same Docker exec sessions, different transport (WebSocket instead of Tauri IPC events).
|
The web terminal shares the existing `ExecSessionManager` via `Arc`-wrapped stores — same Docker exec sessions, different transport (WebSocket instead of Tauri IPC events).
|
||||||
|
|
||||||
|
### Speech-to-Text (Voice Mode)
|
||||||
|
|
||||||
|
Triple-C includes optional speech-to-text powered by [Faster Whisper](https://github.com/SYSTRAN/faster-whisper) running in a separate Docker container. When enabled, a microphone button appears in the bottom-left corner of each terminal view.
|
||||||
|
|
||||||
|
- **Hotkey**: `Ctrl+Shift+M` to toggle recording
|
||||||
|
- **Models**: `tiny`, `small`, or `medium` (configurable in Settings)
|
||||||
|
- **Port**: Default `9876` (configurable)
|
||||||
|
- **Language**: Optional language hint for transcription
|
||||||
|
- **Auto-start**: When STT is enabled in Settings, the container starts automatically with the app — no need to manually start it after each restart
|
||||||
|
- **On-demand fallback**: If not auto-started, the container starts automatically when you first click the mic button
|
||||||
|
|
||||||
|
**How it works**: Audio is captured in the browser via the Web Audio API, encoded as WAV, and sent to the Faster Whisper container's `/transcribe` endpoint. The transcribed text is inserted directly into the active terminal. The STT container uses a named Docker volume (`triple-c-stt-model-cache`) to cache Whisper models across restarts.
|
||||||
|
|
||||||
### Docker Socket Path
|
### Docker Socket Path
|
||||||
|
|
||||||
The socket path is OS-aware:
|
The socket path is OS-aware:
|
||||||
@@ -115,6 +128,7 @@ Users can override this in Settings via the global `docker_socket_path` option.
|
|||||||
| `app/src/components/layout/Sidebar.tsx` | Responsive sidebar (25% width, min 224px, max 320px) |
|
| `app/src/components/layout/Sidebar.tsx` | Responsive sidebar (25% width, min 224px, max 320px) |
|
||||||
| `app/src/components/layout/StatusBar.tsx` | Running project/terminal counts |
|
| `app/src/components/layout/StatusBar.tsx` | Running project/terminal counts |
|
||||||
| `app/src/components/projects/ProjectCard.tsx` | Project config, backend selector, action buttons |
|
| `app/src/components/projects/ProjectCard.tsx` | Project config, backend selector, action buttons |
|
||||||
|
| `app/src/components/projects/ClaudeCodeSettingsModal.tsx` | Claude Code CLI settings modal (TUI mode, effort, focus, caching) |
|
||||||
| `app/src/components/projects/ProjectList.tsx` | Project list in sidebar |
|
| `app/src/components/projects/ProjectList.tsx` | Project list in sidebar |
|
||||||
| `app/src/components/projects/FileManagerModal.tsx` | File browser modal (browse, download, upload) |
|
| `app/src/components/projects/FileManagerModal.tsx` | File browser modal (browse, download, upload) |
|
||||||
| `app/src/components/projects/ContainerProgressModal.tsx` | Real-time container operation progress |
|
| `app/src/components/projects/ContainerProgressModal.tsx` | Real-time container operation progress |
|
||||||
@@ -122,12 +136,14 @@ Users can override this in Settings via the global `docker_socket_path` option.
|
|||||||
| `app/src/components/mcp/McpServerCard.tsx` | Individual MCP server configuration card |
|
| `app/src/components/mcp/McpServerCard.tsx` | Individual MCP server configuration card |
|
||||||
| `app/src/components/settings/SettingsPanel.tsx` | Docker, AWS, timezone, web terminal, and global settings |
|
| `app/src/components/settings/SettingsPanel.tsx` | Docker, AWS, timezone, web terminal, and global settings |
|
||||||
| `app/src/components/settings/WebTerminalSettings.tsx` | Web terminal toggle, URL, token management |
|
| `app/src/components/settings/WebTerminalSettings.tsx` | Web terminal toggle, URL, token management |
|
||||||
|
| `app/src/components/settings/SttSettings.tsx` | STT settings panel (model, port, language, container controls) |
|
||||||
| `app/src/components/terminal/TerminalView.tsx` | xterm.js terminal with WebGL, URL detection, OSC 52 clipboard, image paste |
|
| `app/src/components/terminal/TerminalView.tsx` | xterm.js terminal with WebGL, URL detection, OSC 52 clipboard, image paste |
|
||||||
|
| `app/src/components/terminal/SttButton.tsx` | Mic button overlay with on-demand container start |
|
||||||
| `app/src/components/terminal/TerminalTabs.tsx` | Tab bar for multiple terminal sessions (claude + bash) |
|
| `app/src/components/terminal/TerminalTabs.tsx` | Tab bar for multiple terminal sessions (claude + bash) |
|
||||||
| `app/src/hooks/useTerminal.ts` | Terminal session management (claude and bash modes) |
|
| `app/src/hooks/useTerminal.ts` | Terminal session management (claude and bash modes) |
|
||||||
| `app/src/hooks/useFileManager.ts` | File manager operations (list, download, upload) |
|
| `app/src/hooks/useFileManager.ts` | File manager operations (list, download, upload) |
|
||||||
| `app/src/hooks/useMcpServers.ts` | MCP server CRUD operations |
|
| `app/src/hooks/useMcpServers.ts` | MCP server CRUD operations |
|
||||||
| `app/src/hooks/useVoice.ts` | Voice mode audio capture (currently hidden) |
|
| `app/src/hooks/useSTT.ts` | Speech-to-text recording, transcription, and container management |
|
||||||
| `app/src-tauri/src/docker/container.rs` | Container creation, mounts, env vars, MCP injection, fingerprinting |
|
| `app/src-tauri/src/docker/container.rs` | Container creation, mounts, env vars, MCP injection, fingerprinting |
|
||||||
| `app/src-tauri/src/docker/exec.rs` | PTY exec sessions, file upload/download via tar |
|
| `app/src-tauri/src/docker/exec.rs` | PTY exec sessions, file upload/download via tar |
|
||||||
| `app/src-tauri/src/docker/image.rs` | Image building/pulling |
|
| `app/src-tauri/src/docker/image.rs` | Image building/pulling |
|
||||||
@@ -135,16 +151,21 @@ Users can override this in Settings via the global `docker_socket_path` option.
|
|||||||
| `app/src-tauri/src/commands/project_commands.rs` | Start/stop/rebuild Tauri command handlers |
|
| `app/src-tauri/src/commands/project_commands.rs` | Start/stop/rebuild Tauri command handlers |
|
||||||
| `app/src-tauri/src/commands/file_commands.rs` | File manager Tauri commands (list, download, upload) |
|
| `app/src-tauri/src/commands/file_commands.rs` | File manager Tauri commands (list, download, upload) |
|
||||||
| `app/src-tauri/src/commands/mcp_commands.rs` | MCP server CRUD Tauri commands |
|
| `app/src-tauri/src/commands/mcp_commands.rs` | MCP server CRUD Tauri commands |
|
||||||
| `app/src-tauri/src/models/project.rs` | Project struct (backend, Docker access, MCP servers, Mission Control) |
|
| `app/src-tauri/src/models/project.rs` | Project struct (backend, Docker access, Claude Code settings, MCP servers, Mission Control) |
|
||||||
| `app/src-tauri/src/models/mcp_server.rs` | MCP server struct (transport, Docker image, env vars) |
|
| `app/src-tauri/src/models/mcp_server.rs` | MCP server struct (transport, Docker image, env vars) |
|
||||||
| `app/src-tauri/src/models/app_settings.rs` | Global settings (image source, Docker socket, AWS, web terminal) |
|
| `app/src-tauri/src/models/app_settings.rs` | Global settings (image source, Docker socket, AWS, Claude Code settings, web terminal, STT) |
|
||||||
| `app/src-tauri/src/web_terminal/server.rs` | Axum HTTP+WS server for remote terminal access |
|
| `app/src-tauri/src/web_terminal/server.rs` | Axum HTTP+WS server for remote terminal access |
|
||||||
| `app/src-tauri/src/web_terminal/ws_handler.rs` | WebSocket connection handler and session management |
|
| `app/src-tauri/src/web_terminal/ws_handler.rs` | WebSocket connection handler and session management |
|
||||||
| `app/src-tauri/src/web_terminal/terminal.html` | Embedded web UI (xterm.js, project picker, tabs) |
|
| `app/src-tauri/src/web_terminal/terminal.html` | Embedded web UI (xterm.js, project picker, tabs) |
|
||||||
|
| `app/src-tauri/src/commands/stt_commands.rs` | STT start/stop/transcribe Tauri commands |
|
||||||
| `app/src-tauri/src/commands/web_terminal_commands.rs` | Web terminal start/stop/status Tauri commands |
|
| `app/src-tauri/src/commands/web_terminal_commands.rs` | Web terminal start/stop/status Tauri commands |
|
||||||
| `app/src-tauri/src/storage/mcp_store.rs` | MCP server persistence (JSON with atomic writes) |
|
| `app/src-tauri/src/storage/mcp_store.rs` | MCP server persistence (JSON with atomic writes) |
|
||||||
|
| `app/src-tauri/src/docker/stt.rs` | STT Docker container lifecycle (create, start, stop, build, pull) |
|
||||||
|
| `app/src/lib/wav.ts` | WAV audio encoding for STT transcription |
|
||||||
|
| `stt-container/Dockerfile` | Faster Whisper STT container image (Python 3.11 + FastAPI) |
|
||||||
|
| `stt-container/server.py` | STT HTTP server (POST /transcribe endpoint) |
|
||||||
| `container/Dockerfile` | Ubuntu 24.04 sandbox image with Claude Code + dev tools + clipboard/audio shims |
|
| `container/Dockerfile` | Ubuntu 24.04 sandbox image with Claude Code + dev tools + clipboard/audio shims |
|
||||||
| `container/entrypoint.sh` | UID/GID remap, SSH setup, Docker group config, MCP injection, Mission Control setup |
|
| `container/entrypoint.sh` | UID/GID remap, SSH setup, Docker group config, MCP injection, Claude Code settings injection, Mission Control setup |
|
||||||
| `container/osc52-clipboard` | Clipboard shim (xclip/xsel/pbcopy via OSC 52) |
|
| `container/osc52-clipboard` | Clipboard shim (xclip/xsel/pbcopy via OSC 52) |
|
||||||
| `container/audio-shim` | Audio capture shim (rec/arecord via FIFO) for voice mode |
|
| `container/audio-shim` | Audio capture shim (rec/arecord via FIFO) for voice mode |
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "triple-c",
|
"name": "triple-c",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
456
app/src-tauri/Cargo.lock
generated
456
app/src-tauri/Cargo.lock
generated
@@ -280,6 +280,21 @@ version = "0.22.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bit-set"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
|
||||||
|
dependencies = [
|
||||||
|
"bit-vec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bit-vec"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.3.2"
|
version = "1.3.2"
|
||||||
@@ -617,9 +632,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-graphics"
|
name = "core-graphics"
|
||||||
version = "0.24.0"
|
version = "0.25.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
|
checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"core-foundation 0.10.1",
|
"core-foundation 0.10.1",
|
||||||
@@ -699,6 +714,19 @@ dependencies = [
|
|||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cssparser"
|
||||||
|
version = "0.36.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2"
|
||||||
|
dependencies = [
|
||||||
|
"cssparser-macros",
|
||||||
|
"dtoa-short",
|
||||||
|
"itoa",
|
||||||
|
"phf 0.13.1",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cssparser-macros"
|
name = "cssparser-macros"
|
||||||
version = "0.6.1"
|
version = "0.6.1"
|
||||||
@@ -711,14 +739,20 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ctor"
|
name = "ctor"
|
||||||
version = "0.2.9"
|
version = "0.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
|
checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"ctor-proc-macro",
|
||||||
"syn 2.0.117",
|
"dtor",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ctor-proc-macro"
|
||||||
|
version = "0.0.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darling"
|
name = "darling"
|
||||||
version = "0.20.11"
|
version = "0.20.11"
|
||||||
@@ -795,6 +829,17 @@ version = "2.10.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
|
checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dbus"
|
||||||
|
version = "0.9.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"libdbus-sys",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.5.8"
|
version = "0.5.8"
|
||||||
@@ -849,6 +894,27 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_more"
|
||||||
|
version = "2.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
|
||||||
|
dependencies = [
|
||||||
|
"derive_more-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_more-impl"
|
||||||
|
version = "2.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rustc_version",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.10.7"
|
version = "0.10.7"
|
||||||
@@ -880,12 +946,6 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dispatch"
|
|
||||||
version = "0.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dispatch2"
|
name = "dispatch2"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@@ -932,6 +992,21 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dom_query"
|
||||||
|
version = "0.27.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89"
|
||||||
|
dependencies = [
|
||||||
|
"bit-set",
|
||||||
|
"cssparser 0.36.0",
|
||||||
|
"foldhash 0.2.0",
|
||||||
|
"html5ever 0.38.0",
|
||||||
|
"precomputed-hash",
|
||||||
|
"selectors 0.36.1",
|
||||||
|
"tendril 0.5.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dpi"
|
name = "dpi"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@@ -956,6 +1031,21 @@ dependencies = [
|
|||||||
"dtoa",
|
"dtoa",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dtor"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4"
|
||||||
|
dependencies = [
|
||||||
|
"dtor-proc-macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dtor-proc-macro"
|
||||||
|
version = "0.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dunce"
|
name = "dunce"
|
||||||
version = "1.0.5"
|
version = "1.0.5"
|
||||||
@@ -1143,6 +1233,12 @@ version = "0.1.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foldhash"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "foreign-types"
|
name = "foreign-types"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -1614,7 +1710,7 @@ version = "0.15.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"foldhash",
|
"foldhash 0.1.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1655,10 +1751,20 @@ checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"mac",
|
"mac",
|
||||||
"markup5ever",
|
"markup5ever 0.14.1",
|
||||||
"match_token",
|
"match_token",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "html5ever"
|
||||||
|
version = "0.38.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"markup5ever 0.38.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
@@ -2158,18 +2264,12 @@ version = "0.8.8-speedreader"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2"
|
checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cssparser",
|
"cssparser 0.29.6",
|
||||||
"html5ever",
|
"html5ever 0.29.1",
|
||||||
"indexmap 2.13.0",
|
"indexmap 2.13.0",
|
||||||
"selectors",
|
"selectors 0.24.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "lazy_static"
|
|
||||||
version = "1.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "leb128fmt"
|
name = "leb128fmt"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -2206,6 +2306,15 @@ version = "0.2.182"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
|
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libdbus-sys"
|
||||||
|
version = "0.2.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043"
|
||||||
|
dependencies = [
|
||||||
|
"pkg-config",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libloading"
|
name = "libloading"
|
||||||
version = "0.7.4"
|
version = "0.7.4"
|
||||||
@@ -2296,9 +2405,20 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"phf 0.11.3",
|
"phf 0.11.3",
|
||||||
"phf_codegen 0.11.3",
|
"phf_codegen 0.11.3",
|
||||||
"string_cache",
|
"string_cache 0.8.9",
|
||||||
"string_cache_codegen",
|
"string_cache_codegen 0.5.4",
|
||||||
"tendril",
|
"tendril 0.4.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "markup5ever"
|
||||||
|
version = "0.38.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"tendril 0.5.0",
|
||||||
|
"web_atoms",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2388,9 +2508,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "muda"
|
name = "muda"
|
||||||
version = "0.17.1"
|
version = "0.19.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a"
|
checksum = "0ae8844f63b5b118e334e205585b8c5c17b984121dbdb179d44aeb087ffad3cb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"dpi",
|
"dpi",
|
||||||
@@ -2401,10 +2521,10 @@ dependencies = [
|
|||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"png 0.17.16",
|
"png 0.18.1",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2422,12 +2542,6 @@ dependencies = [
|
|||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ndk-context"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ndk-sys"
|
name = "ndk-sys"
|
||||||
version = "0.6.0+11769913"
|
version = "0.6.0+11769913"
|
||||||
@@ -2533,17 +2647,9 @@ checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"block2",
|
"block2",
|
||||||
"libc",
|
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-cloud-kit",
|
|
||||||
"objc2-core-data",
|
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
"objc2-core-graphics",
|
|
||||||
"objc2-core-image",
|
|
||||||
"objc2-core-text",
|
|
||||||
"objc2-core-video",
|
|
||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
"objc2-quartz-core",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2563,7 +2669,6 @@ version = "0.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
|
checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.0",
|
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
]
|
]
|
||||||
@@ -2602,6 +2707,16 @@ dependencies = [
|
|||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-core-location"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009"
|
||||||
|
dependencies = [
|
||||||
|
"objc2",
|
||||||
|
"objc2-foundation",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2-core-text"
|
name = "objc2-core-text"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
@@ -2614,19 +2729,6 @@ dependencies = [
|
|||||||
"objc2-core-graphics",
|
"objc2-core-graphics",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "objc2-core-video"
|
|
||||||
version = "0.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.11.0",
|
|
||||||
"objc2",
|
|
||||||
"objc2-core-foundation",
|
|
||||||
"objc2-core-graphics",
|
|
||||||
"objc2-io-surface",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2-encode"
|
name = "objc2-encode"
|
||||||
version = "4.1.0"
|
version = "4.1.0"
|
||||||
@@ -2666,16 +2768,6 @@ dependencies = [
|
|||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "objc2-javascript-core"
|
|
||||||
version = "0.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586"
|
|
||||||
dependencies = [
|
|
||||||
"objc2",
|
|
||||||
"objc2-core-foundation",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2-quartz-core"
|
name = "objc2-quartz-core"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
@@ -2688,17 +2780,6 @@ dependencies = [
|
|||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "objc2-security"
|
|
||||||
version = "0.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.11.0",
|
|
||||||
"objc2",
|
|
||||||
"objc2-core-foundation",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2-ui-kit"
|
name = "objc2-ui-kit"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
@@ -2706,8 +2787,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22"
|
checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
|
"block2",
|
||||||
"objc2",
|
"objc2",
|
||||||
|
"objc2-cloud-kit",
|
||||||
|
"objc2-core-data",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
|
"objc2-core-graphics",
|
||||||
|
"objc2-core-image",
|
||||||
|
"objc2-core-location",
|
||||||
|
"objc2-core-text",
|
||||||
|
"objc2-foundation",
|
||||||
|
"objc2-quartz-core",
|
||||||
|
"objc2-user-notifications",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-user-notifications"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e"
|
||||||
|
dependencies = [
|
||||||
|
"objc2",
|
||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2723,8 +2823,6 @@ dependencies = [
|
|||||||
"objc2-app-kit",
|
"objc2-app-kit",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
"objc2-javascript-core",
|
|
||||||
"objc2-security",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2857,6 +2955,17 @@ dependencies = [
|
|||||||
"phf_shared 0.11.3",
|
"phf_shared 0.11.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
|
||||||
|
dependencies = [
|
||||||
|
"phf_macros 0.13.1",
|
||||||
|
"phf_shared 0.13.1",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "phf_codegen"
|
name = "phf_codegen"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
@@ -2877,6 +2986,16 @@ dependencies = [
|
|||||||
"phf_shared 0.11.3",
|
"phf_shared 0.11.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_codegen"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1"
|
||||||
|
dependencies = [
|
||||||
|
"phf_generator 0.13.1",
|
||||||
|
"phf_shared 0.13.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "phf_generator"
|
name = "phf_generator"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
@@ -2907,6 +3026,16 @@ dependencies = [
|
|||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_generator"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
|
||||||
|
dependencies = [
|
||||||
|
"fastrand",
|
||||||
|
"phf_shared 0.13.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "phf_macros"
|
name = "phf_macros"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
@@ -2934,6 +3063,19 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_macros"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"
|
||||||
|
dependencies = [
|
||||||
|
"phf_generator 0.13.1",
|
||||||
|
"phf_shared 0.13.1",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "phf_shared"
|
name = "phf_shared"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
@@ -2961,6 +3103,15 @@ dependencies = [
|
|||||||
"siphasher 1.0.2",
|
"siphasher 1.0.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "phf_shared"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
|
||||||
|
dependencies = [
|
||||||
|
"siphasher 1.0.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project-lite"
|
name = "pin-project-lite"
|
||||||
version = "0.2.16"
|
version = "0.2.16"
|
||||||
@@ -3751,14 +3902,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416"
|
checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 1.3.2",
|
"bitflags 1.3.2",
|
||||||
"cssparser",
|
"cssparser 0.29.6",
|
||||||
"derive_more",
|
"derive_more 0.99.20",
|
||||||
"fxhash",
|
"fxhash",
|
||||||
"log",
|
"log",
|
||||||
"phf 0.8.0",
|
"phf 0.8.0",
|
||||||
"phf_codegen 0.8.0",
|
"phf_codegen 0.8.0",
|
||||||
"precomputed-hash",
|
"precomputed-hash",
|
||||||
"servo_arc",
|
"servo_arc 0.2.0",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "selectors"
|
||||||
|
version = "0.36.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.11.0",
|
||||||
|
"cssparser 0.36.0",
|
||||||
|
"derive_more 2.1.1",
|
||||||
|
"log",
|
||||||
|
"new_debug_unreachable",
|
||||||
|
"phf 0.13.1",
|
||||||
|
"phf_codegen 0.13.1",
|
||||||
|
"precomputed-hash",
|
||||||
|
"rustc-hash",
|
||||||
|
"servo_arc 0.4.3",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3953,6 +4123,15 @@ dependencies = [
|
|||||||
"stable_deref_trait",
|
"stable_deref_trait",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "servo_arc"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930"
|
||||||
|
dependencies = [
|
||||||
|
"stable_deref_trait",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha1"
|
name = "sha1"
|
||||||
version = "0.10.6"
|
version = "0.10.6"
|
||||||
@@ -4098,6 +4277,18 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "string_cache"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901"
|
||||||
|
dependencies = [
|
||||||
|
"new_debug_unreachable",
|
||||||
|
"parking_lot",
|
||||||
|
"phf_shared 0.13.1",
|
||||||
|
"precomputed-hash",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "string_cache_codegen"
|
name = "string_cache_codegen"
|
||||||
version = "0.5.4"
|
version = "0.5.4"
|
||||||
@@ -4110,6 +4301,18 @@ dependencies = [
|
|||||||
"quote",
|
"quote",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "string_cache_codegen"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69"
|
||||||
|
dependencies = [
|
||||||
|
"phf_generator 0.13.1",
|
||||||
|
"phf_shared 0.13.1",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
@@ -4190,35 +4393,35 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tao"
|
name = "tao"
|
||||||
version = "0.34.5"
|
version = "0.35.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7"
|
checksum = "1cf65722394c2ac443e80120064987f8914ee1d4e4e36e63cdf10f2990f01159"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"block2",
|
"block2",
|
||||||
"core-foundation 0.10.1",
|
"core-foundation 0.10.1",
|
||||||
"core-graphics",
|
"core-graphics",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"dispatch",
|
"dbus",
|
||||||
|
"dispatch2",
|
||||||
"dlopen2",
|
"dlopen2",
|
||||||
"dpi",
|
"dpi",
|
||||||
"gdkwayland-sys",
|
"gdkwayland-sys",
|
||||||
"gdkx11-sys",
|
"gdkx11-sys",
|
||||||
"gtk",
|
"gtk",
|
||||||
"jni",
|
"jni",
|
||||||
"lazy_static",
|
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"ndk",
|
"ndk",
|
||||||
"ndk-context",
|
|
||||||
"ndk-sys",
|
"ndk-sys",
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-app-kit",
|
"objc2-app-kit",
|
||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
|
"objc2-ui-kit",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
|
"percent-encoding",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
"scopeguard",
|
|
||||||
"tao-macros",
|
"tao-macros",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
"url",
|
"url",
|
||||||
@@ -4258,9 +4461,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri"
|
name = "tauri"
|
||||||
version = "2.10.2"
|
version = "2.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "463ae8677aa6d0f063a900b9c41ecd4ac2b7ca82f0b058cc4491540e55b20129"
|
checksum = "d059f2527558d9dba6f186dec4772610e1aecfd3f94002397613e7e648752b66"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -4310,9 +4513,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-build"
|
name = "tauri-build"
|
||||||
version = "2.5.5"
|
version = "2.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ca7bd893329425df750813e95bd2b643d5369d929438da96d5bbb7cc2c918f74"
|
checksum = "be9aa8c59a894f76c29a002501c589de5eb4987a5913d62a6e0a47f320901988"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"cargo_toml",
|
"cargo_toml",
|
||||||
@@ -4326,15 +4529,14 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri-utils",
|
"tauri-utils",
|
||||||
"tauri-winres",
|
"tauri-winres",
|
||||||
"toml 0.9.12+spec-1.1.0",
|
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-codegen"
|
name = "tauri-codegen"
|
||||||
version = "2.5.4"
|
version = "2.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "aac423e5859d9f9ccdd32e3cf6a5866a15bedbf25aa6630bcb2acde9468f6ae3"
|
checksum = "d3e4e8230d565106aa19dfbaa01a7ed01abf78047fe0577a83377224bd1bf20e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"brotli",
|
"brotli",
|
||||||
@@ -4359,9 +4561,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-macros"
|
name = "tauri-macros"
|
||||||
version = "2.5.4"
|
version = "2.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1b6a1bd2861ff0c8766b1d38b32a6a410f6dc6532d4ef534c47cfb2236092f59"
|
checksum = "bc8de2cddbbc33dbdf4c84f170121886595efdbcc9cb4b3d76342b79d082cedc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -4470,9 +4672,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime"
|
name = "tauri-runtime"
|
||||||
version = "2.10.0"
|
version = "2.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b885ffeac82b00f1f6fd292b6e5aabfa7435d537cef57d11e38a489956535651"
|
checksum = "1e42bbcb76237351fbaa02f08d808c537dc12eb5a6eabbf3e517b50056334d95"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cookie",
|
"cookie",
|
||||||
"dpi",
|
"dpi",
|
||||||
@@ -4495,9 +4697,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime-wry"
|
name = "tauri-runtime-wry"
|
||||||
version = "2.10.0"
|
version = "2.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5204682391625e867d16584fedc83fc292fb998814c9f7918605c789cd876314"
|
checksum = "2cadb13dad0c681e1e0a2c49ae488f0e2906ded3d57e7a0017f4aaf46e387117"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gtk",
|
"gtk",
|
||||||
"http",
|
"http",
|
||||||
@@ -4505,7 +4707,6 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-app-kit",
|
"objc2-app-kit",
|
||||||
"objc2-foundation",
|
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
@@ -4522,17 +4723,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-utils"
|
name = "tauri-utils"
|
||||||
version = "2.8.2"
|
version = "2.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fcd169fccdff05eff2c1033210b9b94acd07a47e6fa9a3431cf09cfd4f01c87e"
|
checksum = "55f61d2bf7188fbcf2b0ed095b67a6bc498f713c939314bb19eb700118a573b7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"brotli",
|
"brotli",
|
||||||
"cargo_metadata",
|
"cargo_metadata",
|
||||||
"ctor",
|
"ctor",
|
||||||
|
"dom_query",
|
||||||
"dunce",
|
"dunce",
|
||||||
"glob",
|
"glob",
|
||||||
"html5ever",
|
"html5ever 0.29.1",
|
||||||
"http",
|
"http",
|
||||||
"infer",
|
"infer",
|
||||||
"json-patch",
|
"json-patch",
|
||||||
@@ -4540,6 +4742,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"memchr",
|
"memchr",
|
||||||
"phf 0.11.3",
|
"phf 0.11.3",
|
||||||
|
"plist",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"regex",
|
"regex",
|
||||||
@@ -4593,6 +4796,16 @@ dependencies = [
|
|||||||
"utf-8",
|
"utf-8",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tendril"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24"
|
||||||
|
dependencies = [
|
||||||
|
"new_debug_unreachable",
|
||||||
|
"utf-8",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.69"
|
version = "1.0.69"
|
||||||
@@ -4928,9 +5141,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tray-icon"
|
name = "tray-icon"
|
||||||
version = "0.21.3"
|
version = "0.23.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c"
|
checksum = "15edbb0d80583e85ee8df283410038e17314df5cba30da2087a54a85216c0773"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"dirs",
|
"dirs",
|
||||||
@@ -4942,15 +5155,15 @@ dependencies = [
|
|||||||
"objc2-core-graphics",
|
"objc2-core-graphics",
|
||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"png 0.17.16",
|
"png 0.18.1",
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "triple-c"
|
name = "triple-c"
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
@@ -5353,6 +5566,18 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "web_atoms"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538"
|
||||||
|
dependencies = [
|
||||||
|
"phf 0.13.1",
|
||||||
|
"phf_codegen 0.13.1",
|
||||||
|
"string_cache 0.9.0",
|
||||||
|
"string_cache_codegen 0.6.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webkit2gtk"
|
name = "webkit2gtk"
|
||||||
version = "2.0.2"
|
version = "2.0.2"
|
||||||
@@ -6000,24 +6225,23 @@ checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wry"
|
name = "wry"
|
||||||
version = "0.54.2"
|
version = "0.55.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bb26159b420aa77684589a744ae9a9461a95395b848764ad12290a14d960a11a"
|
checksum = "3013fd6116aac351dd2e18f349b28b2cfef3a5ff3253a9d0ce2d7193bb1b4429"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"block2",
|
"block2",
|
||||||
"cookie",
|
"cookie",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"dirs",
|
"dirs",
|
||||||
|
"dom_query",
|
||||||
"dpi",
|
"dpi",
|
||||||
"dunce",
|
"dunce",
|
||||||
"gdkx11",
|
"gdkx11",
|
||||||
"gtk",
|
"gtk",
|
||||||
"html5ever",
|
|
||||||
"http",
|
"http",
|
||||||
"javascriptcore-rs",
|
"javascriptcore-rs",
|
||||||
"jni",
|
"jni",
|
||||||
"kuchikiki",
|
|
||||||
"libc",
|
"libc",
|
||||||
"ndk",
|
"ndk",
|
||||||
"objc2",
|
"objc2",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "triple-c"
|
name = "triple-c"
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -351,10 +351,10 @@
|
|||||||
"markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`"
|
"markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`",
|
"description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "core:app:default",
|
"const": "core:app:default",
|
||||||
"markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`"
|
"markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the app_hide command without any pre-configured scope.",
|
"description": "Enables the app_hide command without any pre-configured scope.",
|
||||||
@@ -428,6 +428,12 @@
|
|||||||
"const": "core:app:allow-set-dock-visibility",
|
"const": "core:app:allow-set-dock-visibility",
|
||||||
"markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope."
|
"markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the supports_multiple_windows command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:app:allow-supports-multiple-windows",
|
||||||
|
"markdownDescription": "Enables the supports_multiple_windows command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the tauri_version command without any pre-configured scope.",
|
"description": "Enables the tauri_version command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -512,6 +518,12 @@
|
|||||||
"const": "core:app:deny-set-dock-visibility",
|
"const": "core:app:deny-set-dock-visibility",
|
||||||
"markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope."
|
"markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the supports_multiple_windows command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:app:deny-supports-multiple-windows",
|
||||||
|
"markdownDescription": "Denies the supports_multiple_windows command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the tauri_version command without any pre-configured scope.",
|
"description": "Denies the tauri_version command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -1035,10 +1047,10 @@
|
|||||||
"markdownDescription": "Denies the close command without any pre-configured scope."
|
"markdownDescription": "Denies the close command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`",
|
"description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "core:tray:default",
|
"const": "core:tray:default",
|
||||||
"markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`"
|
"markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the get_by_id command without any pre-configured scope.",
|
"description": "Enables the get_by_id command without any pre-configured scope.",
|
||||||
@@ -1070,6 +1082,12 @@
|
|||||||
"const": "core:tray:allow-set-icon-as-template",
|
"const": "core:tray:allow-set-icon-as-template",
|
||||||
"markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope."
|
"markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the set_icon_with_as_template command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:tray:allow-set-icon-with-as-template",
|
||||||
|
"markdownDescription": "Enables the set_icon_with_as_template command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the set_menu command without any pre-configured scope.",
|
"description": "Enables the set_menu command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -1136,6 +1154,12 @@
|
|||||||
"const": "core:tray:deny-set-icon-as-template",
|
"const": "core:tray:deny-set-icon-as-template",
|
||||||
"markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope."
|
"markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the set_icon_with_as_template command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:tray:deny-set-icon-with-as-template",
|
||||||
|
"markdownDescription": "Denies the set_icon_with_as_template command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the set_menu command without any pre-configured scope.",
|
"description": "Denies the set_menu command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -1395,10 +1419,16 @@
|
|||||||
"markdownDescription": "Denies the webview_size command without any pre-configured scope."
|
"markdownDescription": "Denies the webview_size command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`",
|
"description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "core:window:default",
|
"const": "core:window:default",
|
||||||
"markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`"
|
"markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the activity_name command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:window:allow-activity-name",
|
||||||
|
"markdownDescription": "Enables the activity_name command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the available_monitors command without any pre-configured scope.",
|
"description": "Enables the available_monitors command without any pre-configured scope.",
|
||||||
@@ -1592,6 +1622,12 @@
|
|||||||
"const": "core:window:allow-scale-factor",
|
"const": "core:window:allow-scale-factor",
|
||||||
"markdownDescription": "Enables the scale_factor command without any pre-configured scope."
|
"markdownDescription": "Enables the scale_factor command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the scene_identifier command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:window:allow-scene-identifier",
|
||||||
|
"markdownDescription": "Enables the scene_identifier command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the set_always_on_bottom command without any pre-configured scope.",
|
"description": "Enables the set_always_on_bottom command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -1856,6 +1892,12 @@
|
|||||||
"const": "core:window:allow-unminimize",
|
"const": "core:window:allow-unminimize",
|
||||||
"markdownDescription": "Enables the unminimize command without any pre-configured scope."
|
"markdownDescription": "Enables the unminimize command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the activity_name command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:window:deny-activity-name",
|
||||||
|
"markdownDescription": "Denies the activity_name command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the available_monitors command without any pre-configured scope.",
|
"description": "Denies the available_monitors command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -2048,6 +2090,12 @@
|
|||||||
"const": "core:window:deny-scale-factor",
|
"const": "core:window:deny-scale-factor",
|
||||||
"markdownDescription": "Denies the scale_factor command without any pre-configured scope."
|
"markdownDescription": "Denies the scale_factor command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the scene_identifier command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:window:deny-scene-identifier",
|
||||||
|
"markdownDescription": "Denies the scene_identifier command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the set_always_on_bottom command without any pre-configured scope.",
|
"description": "Denies the set_always_on_bottom command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -2313,22 +2361,22 @@
|
|||||||
"markdownDescription": "Denies the unminimize command without any pre-configured scope."
|
"markdownDescription": "Denies the unminimize command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`",
|
"description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "dialog:default",
|
"const": "dialog:default",
|
||||||
"markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`"
|
"markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the ask command without any pre-configured scope.",
|
"description": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "dialog:allow-ask",
|
"const": "dialog:allow-ask",
|
||||||
"markdownDescription": "Enables the ask command without any pre-configured scope."
|
"markdownDescription": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the confirm command without any pre-configured scope.",
|
"description": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "dialog:allow-confirm",
|
"const": "dialog:allow-confirm",
|
||||||
"markdownDescription": "Enables the confirm command without any pre-configured scope."
|
"markdownDescription": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the message command without any pre-configured scope.",
|
"description": "Enables the message command without any pre-configured scope.",
|
||||||
@@ -2349,16 +2397,16 @@
|
|||||||
"markdownDescription": "Enables the save command without any pre-configured scope."
|
"markdownDescription": "Enables the save command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the ask command without any pre-configured scope.",
|
"description": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "dialog:deny-ask",
|
"const": "dialog:deny-ask",
|
||||||
"markdownDescription": "Denies the ask command without any pre-configured scope."
|
"markdownDescription": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the confirm command without any pre-configured scope.",
|
"description": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "dialog:deny-confirm",
|
"const": "dialog:deny-confirm",
|
||||||
"markdownDescription": "Denies the confirm command without any pre-configured scope."
|
"markdownDescription": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the message command without any pre-configured scope.",
|
"description": "Denies the message command without any pre-configured scope.",
|
||||||
|
|||||||
@@ -351,10 +351,10 @@
|
|||||||
"markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`"
|
"markdownDescription": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`",
|
"description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "core:app:default",
|
"const": "core:app:default",
|
||||||
"markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`"
|
"markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-version`\n- `allow-name`\n- `allow-tauri-version`\n- `allow-identifier`\n- `allow-bundle-type`\n- `allow-register-listener`\n- `allow-remove-listener`\n- `allow-supports-multiple-windows`"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the app_hide command without any pre-configured scope.",
|
"description": "Enables the app_hide command without any pre-configured scope.",
|
||||||
@@ -428,6 +428,12 @@
|
|||||||
"const": "core:app:allow-set-dock-visibility",
|
"const": "core:app:allow-set-dock-visibility",
|
||||||
"markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope."
|
"markdownDescription": "Enables the set_dock_visibility command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the supports_multiple_windows command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:app:allow-supports-multiple-windows",
|
||||||
|
"markdownDescription": "Enables the supports_multiple_windows command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the tauri_version command without any pre-configured scope.",
|
"description": "Enables the tauri_version command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -512,6 +518,12 @@
|
|||||||
"const": "core:app:deny-set-dock-visibility",
|
"const": "core:app:deny-set-dock-visibility",
|
||||||
"markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope."
|
"markdownDescription": "Denies the set_dock_visibility command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the supports_multiple_windows command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:app:deny-supports-multiple-windows",
|
||||||
|
"markdownDescription": "Denies the supports_multiple_windows command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the tauri_version command without any pre-configured scope.",
|
"description": "Denies the tauri_version command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -1035,10 +1047,10 @@
|
|||||||
"markdownDescription": "Denies the close command without any pre-configured scope."
|
"markdownDescription": "Denies the close command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`",
|
"description": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "core:tray:default",
|
"const": "core:tray:default",
|
||||||
"markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-show-menu-on-left-click`"
|
"markdownDescription": "Default permissions for the plugin, which enables all commands.\n#### This default permission set includes:\n\n- `allow-new`\n- `allow-get-by-id`\n- `allow-remove-by-id`\n- `allow-set-icon`\n- `allow-set-menu`\n- `allow-set-tooltip`\n- `allow-set-title`\n- `allow-set-visible`\n- `allow-set-temp-dir-path`\n- `allow-set-icon-as-template`\n- `allow-set-icon-with-as-template`\n- `allow-set-show-menu-on-left-click`"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the get_by_id command without any pre-configured scope.",
|
"description": "Enables the get_by_id command without any pre-configured scope.",
|
||||||
@@ -1070,6 +1082,12 @@
|
|||||||
"const": "core:tray:allow-set-icon-as-template",
|
"const": "core:tray:allow-set-icon-as-template",
|
||||||
"markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope."
|
"markdownDescription": "Enables the set_icon_as_template command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the set_icon_with_as_template command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:tray:allow-set-icon-with-as-template",
|
||||||
|
"markdownDescription": "Enables the set_icon_with_as_template command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the set_menu command without any pre-configured scope.",
|
"description": "Enables the set_menu command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -1136,6 +1154,12 @@
|
|||||||
"const": "core:tray:deny-set-icon-as-template",
|
"const": "core:tray:deny-set-icon-as-template",
|
||||||
"markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope."
|
"markdownDescription": "Denies the set_icon_as_template command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the set_icon_with_as_template command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:tray:deny-set-icon-with-as-template",
|
||||||
|
"markdownDescription": "Denies the set_icon_with_as_template command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the set_menu command without any pre-configured scope.",
|
"description": "Denies the set_menu command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -1395,10 +1419,16 @@
|
|||||||
"markdownDescription": "Denies the webview_size command without any pre-configured scope."
|
"markdownDescription": "Denies the webview_size command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`",
|
"description": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "core:window:default",
|
"const": "core:window:default",
|
||||||
"markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-internal-toggle-maximize`"
|
"markdownDescription": "Default permissions for the plugin.\n#### This default permission set includes:\n\n- `allow-get-all-windows`\n- `allow-scale-factor`\n- `allow-inner-position`\n- `allow-outer-position`\n- `allow-inner-size`\n- `allow-outer-size`\n- `allow-is-fullscreen`\n- `allow-is-minimized`\n- `allow-is-maximized`\n- `allow-is-focused`\n- `allow-is-decorated`\n- `allow-is-resizable`\n- `allow-is-maximizable`\n- `allow-is-minimizable`\n- `allow-is-closable`\n- `allow-is-visible`\n- `allow-is-enabled`\n- `allow-title`\n- `allow-current-monitor`\n- `allow-primary-monitor`\n- `allow-monitor-from-point`\n- `allow-available-monitors`\n- `allow-cursor-position`\n- `allow-theme`\n- `allow-is-always-on-top`\n- `allow-activity-name`\n- `allow-scene-identifier`\n- `allow-internal-toggle-maximize`"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the activity_name command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:window:allow-activity-name",
|
||||||
|
"markdownDescription": "Enables the activity_name command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the available_monitors command without any pre-configured scope.",
|
"description": "Enables the available_monitors command without any pre-configured scope.",
|
||||||
@@ -1592,6 +1622,12 @@
|
|||||||
"const": "core:window:allow-scale-factor",
|
"const": "core:window:allow-scale-factor",
|
||||||
"markdownDescription": "Enables the scale_factor command without any pre-configured scope."
|
"markdownDescription": "Enables the scale_factor command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the scene_identifier command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:window:allow-scene-identifier",
|
||||||
|
"markdownDescription": "Enables the scene_identifier command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the set_always_on_bottom command without any pre-configured scope.",
|
"description": "Enables the set_always_on_bottom command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -1856,6 +1892,12 @@
|
|||||||
"const": "core:window:allow-unminimize",
|
"const": "core:window:allow-unminimize",
|
||||||
"markdownDescription": "Enables the unminimize command without any pre-configured scope."
|
"markdownDescription": "Enables the unminimize command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the activity_name command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:window:deny-activity-name",
|
||||||
|
"markdownDescription": "Denies the activity_name command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the available_monitors command without any pre-configured scope.",
|
"description": "Denies the available_monitors command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -2048,6 +2090,12 @@
|
|||||||
"const": "core:window:deny-scale-factor",
|
"const": "core:window:deny-scale-factor",
|
||||||
"markdownDescription": "Denies the scale_factor command without any pre-configured scope."
|
"markdownDescription": "Denies the scale_factor command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the scene_identifier command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "core:window:deny-scene-identifier",
|
||||||
|
"markdownDescription": "Denies the scene_identifier command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the set_always_on_bottom command without any pre-configured scope.",
|
"description": "Denies the set_always_on_bottom command without any pre-configured scope.",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -2313,22 +2361,22 @@
|
|||||||
"markdownDescription": "Denies the unminimize command without any pre-configured scope."
|
"markdownDescription": "Denies the unminimize command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`",
|
"description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "dialog:default",
|
"const": "dialog:default",
|
||||||
"markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`"
|
"markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the ask command without any pre-configured scope.",
|
"description": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "dialog:allow-ask",
|
"const": "dialog:allow-ask",
|
||||||
"markdownDescription": "Enables the ask command without any pre-configured scope."
|
"markdownDescription": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the confirm command without any pre-configured scope.",
|
"description": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "dialog:allow-confirm",
|
"const": "dialog:allow-confirm",
|
||||||
"markdownDescription": "Enables the confirm command without any pre-configured scope."
|
"markdownDescription": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Enables the message command without any pre-configured scope.",
|
"description": "Enables the message command without any pre-configured scope.",
|
||||||
@@ -2349,16 +2397,16 @@
|
|||||||
"markdownDescription": "Enables the save command without any pre-configured scope."
|
"markdownDescription": "Enables the save command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the ask command without any pre-configured scope.",
|
"description": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "dialog:deny-ask",
|
"const": "dialog:deny-ask",
|
||||||
"markdownDescription": "Denies the ask command without any pre-configured scope."
|
"markdownDescription": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the confirm command without any pre-configured scope.",
|
"description": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"const": "dialog:deny-confirm",
|
"const": "dialog:deny-confirm",
|
||||||
"markdownDescription": "Denies the confirm command without any pre-configured scope."
|
"markdownDescription": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Denies the message command without any pre-configured scope.",
|
"description": "Denies the message command without any pre-configured scope.",
|
||||||
|
|||||||
@@ -1,23 +1,58 @@
|
|||||||
use tauri::State;
|
use tauri::State;
|
||||||
|
|
||||||
|
use crate::models::Project;
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
|
||||||
#[tauri::command]
|
/// Resolve AWS profile: project-level → global settings → "default".
|
||||||
pub async fn aws_sso_refresh(
|
pub fn resolve_profile_for_project(project: &Project, global_profile: Option<&str>) -> String {
|
||||||
project_id: String,
|
project
|
||||||
state: State<'_, AppState>,
|
.bedrock_config
|
||||||
) -> Result<(), String> {
|
.as_ref()
|
||||||
let project = state.projects_store.get(&project_id)
|
|
||||||
.ok_or_else(|| format!("Project {} not found", project_id))?;
|
|
||||||
|
|
||||||
let profile = project.bedrock_config.as_ref()
|
|
||||||
.and_then(|b| b.aws_profile.clone())
|
.and_then(|b| b.aws_profile.clone())
|
||||||
.or_else(|| state.settings_store.get().global_aws.aws_profile.clone())
|
.or_else(|| global_profile.map(|s| s.to_string()))
|
||||||
.unwrap_or_else(|| "default".to_string());
|
.unwrap_or_else(|| "default".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the AWS session is valid for the given profile on the host.
|
||||||
|
/// Returns `Ok(true)` if valid, `Ok(false)` if expired/invalid.
|
||||||
|
pub async fn check_sso_session(profile: &str) -> Result<bool, String> {
|
||||||
|
let output = tokio::process::Command::new("aws")
|
||||||
|
.args(["sts", "get-caller-identity", "--profile", profile])
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("Failed to run aws sts get-caller-identity: {}", e))?;
|
||||||
|
Ok(output.status.success())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the given AWS profile uses SSO (has sso_start_url or sso_session configured).
|
||||||
|
pub async fn is_sso_profile(profile: &str) -> Result<bool, String> {
|
||||||
|
let check_start_url = tokio::process::Command::new("aws")
|
||||||
|
.args(["configure", "get", "sso_start_url", "--profile", profile])
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
if let Ok(out) = check_start_url {
|
||||||
|
if out.status.success() {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let check_session = tokio::process::Command::new("aws")
|
||||||
|
.args(["configure", "get", "sso_session", "--profile", profile])
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
if let Ok(out) = check_session {
|
||||||
|
if out.status.success() {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run `aws sso login --profile X` on the host. This is interactive (opens a browser).
|
||||||
|
pub async fn run_sso_login(profile: &str) -> Result<(), String> {
|
||||||
log::info!("Running host-side AWS SSO login for profile '{}'", profile);
|
log::info!("Running host-side AWS SSO login for profile '{}'", profile);
|
||||||
|
|
||||||
let status = tokio::process::Command::new("aws")
|
let status = tokio::process::Command::new("aws")
|
||||||
.args(["sso", "login", "--profile", &profile])
|
.args(["sso", "login", "--profile", profile])
|
||||||
.status()
|
.status()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("Failed to run aws sso login: {}", e))?;
|
.map_err(|e| format!("Failed to run aws sso login: {}", e))?;
|
||||||
@@ -28,3 +63,19 @@ pub async fn aws_sso_refresh(
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn aws_sso_refresh(
|
||||||
|
project_id: String,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let project = state.projects_store.get(&project_id)
|
||||||
|
.ok_or_else(|| format!("Project {} not found", project_id))?;
|
||||||
|
|
||||||
|
let profile = resolve_profile_for_project(
|
||||||
|
&project,
|
||||||
|
state.settings_store.get().global_aws.aws_profile.as_deref(),
|
||||||
|
);
|
||||||
|
|
||||||
|
run_sso_login(&profile).await
|
||||||
|
}
|
||||||
|
|||||||
11
app/src-tauri/src/commands/install_helper_commands.rs
Normal file
11
app/src-tauri/src/commands/install_helper_commands.rs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
use crate::install_helper::{self, InstallOptions};
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn detect_install_options() -> Result<InstallOptions, String> {
|
||||||
|
Ok(install_helper::detect_install_options())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn run_docker_install(app_handle: tauri::AppHandle) -> Result<(), String> {
|
||||||
|
install_helper::platform::run_install(&app_handle).await
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ pub mod aws_commands;
|
|||||||
pub mod docker_commands;
|
pub mod docker_commands;
|
||||||
pub mod file_commands;
|
pub mod file_commands;
|
||||||
pub mod help_commands;
|
pub mod help_commands;
|
||||||
|
pub mod install_helper_commands;
|
||||||
pub mod mcp_commands;
|
pub mod mcp_commands;
|
||||||
pub mod project_commands;
|
pub mod project_commands;
|
||||||
pub mod settings_commands;
|
pub mod settings_commands;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use tauri::{Emitter, State};
|
use tauri::{Emitter, State};
|
||||||
|
|
||||||
|
use crate::commands::aws_commands;
|
||||||
use crate::docker;
|
use crate::docker;
|
||||||
use crate::models::{container_config, Backend, McpServer, Project, ProjectPath, ProjectStatus};
|
use crate::models::{container_config, Backend, BedrockAuthMethod, McpServer, Project, ProjectPath, ProjectStatus};
|
||||||
use crate::storage::secure;
|
use crate::storage::secure;
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
|
||||||
@@ -208,6 +209,76 @@ pub async fn start_project_container(
|
|||||||
// Update status to starting
|
// Update status to starting
|
||||||
state.projects_store.update_status(&project_id, ProjectStatus::Starting)?;
|
state.projects_store.update_status(&project_id, ProjectStatus::Starting)?;
|
||||||
|
|
||||||
|
// Pre-validate AWS SSO session on the host for Bedrock Profile projects.
|
||||||
|
// If the session is expired, trigger `aws sso login` before starting the container
|
||||||
|
// so the entrypoint copies already-fresh credentials from the host mount.
|
||||||
|
if project.backend == Backend::Bedrock {
|
||||||
|
if let Some(ref bedrock) = project.bedrock_config {
|
||||||
|
if bedrock.auth_method == BedrockAuthMethod::Profile {
|
||||||
|
let profile = aws_commands::resolve_profile_for_project(
|
||||||
|
&project,
|
||||||
|
settings.global_aws.aws_profile.as_deref(),
|
||||||
|
);
|
||||||
|
|
||||||
|
emit_progress(&app_handle, &project_id, "Validating AWS session...");
|
||||||
|
|
||||||
|
let session_valid = tokio::time::timeout(
|
||||||
|
std::time::Duration::from_secs(10),
|
||||||
|
aws_commands::check_sso_session(&profile),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match session_valid {
|
||||||
|
Ok(Ok(true)) => {
|
||||||
|
emit_progress(&app_handle, &project_id, "AWS session valid.");
|
||||||
|
}
|
||||||
|
Ok(Ok(false)) => {
|
||||||
|
// Session expired — check if this is an SSO profile
|
||||||
|
if aws_commands::is_sso_profile(&profile).await.unwrap_or(false) {
|
||||||
|
emit_progress(
|
||||||
|
&app_handle,
|
||||||
|
&project_id,
|
||||||
|
"AWS session expired. Starting SSO login (check your browser)...",
|
||||||
|
);
|
||||||
|
match aws_commands::run_sso_login(&profile).await {
|
||||||
|
Ok(()) => {
|
||||||
|
emit_progress(
|
||||||
|
&app_handle,
|
||||||
|
&project_id,
|
||||||
|
"SSO login successful.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!(
|
||||||
|
"SSO login failed for profile '{}': {} — continuing anyway",
|
||||||
|
profile,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
emit_progress(
|
||||||
|
&app_handle,
|
||||||
|
&project_id,
|
||||||
|
"SSO login failed or cancelled. Continuing...",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::warn!(
|
||||||
|
"AWS session invalid for profile '{}' (not SSO). Continuing...",
|
||||||
|
profile
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
log::warn!("Failed to check AWS session: {} — continuing anyway", e);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
log::warn!("AWS session check timed out — continuing anyway");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Wrap container operations so that any failure resets status to Stopped.
|
// Wrap container operations so that any failure resets status to Stopped.
|
||||||
let result: Result<String, String> = async {
|
let result: Result<String, String> = async {
|
||||||
// Ensure image exists
|
// Ensure image exists
|
||||||
@@ -267,6 +338,10 @@ pub async fn start_project_container(
|
|||||||
&settings.global_custom_env_vars,
|
&settings.global_custom_env_vars,
|
||||||
settings.timezone.as_deref(),
|
settings.timezone.as_deref(),
|
||||||
&enabled_mcp,
|
&enabled_mcp,
|
||||||
|
settings.global_claude_code_settings.as_ref(),
|
||||||
|
settings.default_ssh_key_path.as_deref(),
|
||||||
|
settings.default_git_user_name.as_deref(),
|
||||||
|
settings.default_git_user_email.as_deref(),
|
||||||
).await.unwrap_or(false);
|
).await.unwrap_or(false);
|
||||||
|
|
||||||
if needs_recreate {
|
if needs_recreate {
|
||||||
@@ -299,6 +374,10 @@ pub async fn start_project_container(
|
|||||||
settings.timezone.as_deref(),
|
settings.timezone.as_deref(),
|
||||||
&enabled_mcp,
|
&enabled_mcp,
|
||||||
network_name.as_deref(),
|
network_name.as_deref(),
|
||||||
|
settings.global_claude_code_settings.as_ref(),
|
||||||
|
settings.default_ssh_key_path.as_deref(),
|
||||||
|
settings.default_git_user_name.as_deref(),
|
||||||
|
settings.default_git_user_email.as_deref(),
|
||||||
).await?;
|
).await?;
|
||||||
emit_progress(&app_handle, &project_id, "Starting container...");
|
emit_progress(&app_handle, &project_id, "Starting container...");
|
||||||
docker::start_container(&new_id).await?;
|
docker::start_container(&new_id).await?;
|
||||||
@@ -332,6 +411,10 @@ pub async fn start_project_container(
|
|||||||
settings.timezone.as_deref(),
|
settings.timezone.as_deref(),
|
||||||
&enabled_mcp,
|
&enabled_mcp,
|
||||||
network_name.as_deref(),
|
network_name.as_deref(),
|
||||||
|
settings.global_claude_code_settings.as_ref(),
|
||||||
|
settings.default_ssh_key_path.as_deref(),
|
||||||
|
settings.default_git_user_name.as_deref(),
|
||||||
|
settings.default_git_user_email.as_deref(),
|
||||||
).await?;
|
).await?;
|
||||||
emit_progress(&app_handle, &project_id, "Starting container...");
|
emit_progress(&app_handle, &project_id, "Starting container...");
|
||||||
docker::start_container(&new_id).await?;
|
docker::start_container(&new_id).await?;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use tauri::{AppHandle, Emitter, State};
|
use tauri::{AppHandle, Emitter, State};
|
||||||
|
|
||||||
|
use crate::commands::aws_commands;
|
||||||
use crate::models::{Backend, BedrockAuthMethod, Project};
|
use crate::models::{Backend, BedrockAuthMethod, Project};
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
|
||||||
@@ -8,7 +9,7 @@ use crate::AppState;
|
|||||||
/// For Bedrock Profile projects, wraps `claude` in a bash script that validates
|
/// For Bedrock Profile projects, wraps `claude` in a bash script that validates
|
||||||
/// the AWS session first. If the SSO session is expired, runs `aws sso login`
|
/// the AWS session first. If the SSO session is expired, runs `aws sso login`
|
||||||
/// so the user can re-authenticate (the URL is clickable via xterm.js WebLinksAddon).
|
/// so the user can re-authenticate (the URL is clickable via xterm.js WebLinksAddon).
|
||||||
fn build_terminal_cmd(project: &Project, state: &AppState) -> Vec<String> {
|
fn build_terminal_cmd(project: &Project, state: &AppState, session_name: Option<&str>) -> Vec<String> {
|
||||||
let is_bedrock_profile = project.backend == Backend::Bedrock
|
let is_bedrock_profile = project.backend == Backend::Bedrock
|
||||||
&& project
|
&& project
|
||||||
.bedrock_config
|
.bedrock_config
|
||||||
@@ -21,23 +22,30 @@ fn build_terminal_cmd(project: &Project, state: &AppState) -> Vec<String> {
|
|||||||
if project.full_permissions {
|
if project.full_permissions {
|
||||||
cmd.push("--dangerously-skip-permissions".to_string());
|
cmd.push("--dangerously-skip-permissions".to_string());
|
||||||
}
|
}
|
||||||
|
if let Some(name) = session_name {
|
||||||
|
if !name.is_empty() {
|
||||||
|
cmd.push("-n".to_string());
|
||||||
|
cmd.push(name.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
return cmd;
|
return cmd;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve AWS profile: project-level → global settings → "default"
|
let profile = aws_commands::resolve_profile_for_project(
|
||||||
let profile = project
|
project,
|
||||||
.bedrock_config
|
state.settings_store.get().global_aws.aws_profile.as_deref(),
|
||||||
.as_ref()
|
);
|
||||||
.and_then(|b| b.aws_profile.clone())
|
|
||||||
.or_else(|| state.settings_store.get().global_aws.aws_profile.clone())
|
|
||||||
.unwrap_or_else(|| "default".to_string());
|
|
||||||
|
|
||||||
// Build a bash wrapper that validates credentials, re-auths if needed,
|
// Build a bash wrapper that validates credentials, re-auths if needed,
|
||||||
// then exec's into claude.
|
// then exec's into claude.
|
||||||
|
let name_flag = session_name
|
||||||
|
.filter(|n| !n.is_empty())
|
||||||
|
.map(|n| format!(" -n '{}'", n.replace('\'', "'\\''")))
|
||||||
|
.unwrap_or_default();
|
||||||
let claude_cmd = if project.full_permissions {
|
let claude_cmd = if project.full_permissions {
|
||||||
"exec claude --dangerously-skip-permissions"
|
format!("exec claude --dangerously-skip-permissions{}", name_flag)
|
||||||
} else {
|
} else {
|
||||||
"exec claude"
|
format!("exec claude{}", name_flag)
|
||||||
};
|
};
|
||||||
|
|
||||||
let script = format!(
|
let script = format!(
|
||||||
@@ -83,6 +91,7 @@ pub async fn open_terminal_session(
|
|||||||
project_id: String,
|
project_id: String,
|
||||||
session_id: String,
|
session_id: String,
|
||||||
session_type: Option<String>,
|
session_type: Option<String>,
|
||||||
|
session_name: Option<String>,
|
||||||
app_handle: AppHandle,
|
app_handle: AppHandle,
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
@@ -98,7 +107,7 @@ pub async fn open_terminal_session(
|
|||||||
|
|
||||||
let cmd = match session_type.as_deref() {
|
let cmd = match session_type.as_deref() {
|
||||||
Some("bash") => vec!["bash".to_string(), "-l".to_string()],
|
Some("bash") => vec!["bash".to_string(), "-l".to_string()],
|
||||||
_ => build_terminal_cmd(&project, &state),
|
_ => build_terminal_cmd(&project, &state, session_name.as_deref()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let output_event = format!("terminal-output-{}", session_id);
|
let output_event = format!("terminal-output-{}", session_id);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use std::collections::HashMap;
|
|||||||
use sha2::{Sha256, Digest};
|
use sha2::{Sha256, Digest};
|
||||||
|
|
||||||
use super::client::get_docker;
|
use super::client::get_docker;
|
||||||
use crate::models::{Backend, BedrockAuthMethod, ContainerInfo, EnvVar, GlobalAwsSettings, McpServer, McpTransportType, PortMapping, Project, ProjectPath};
|
use crate::models::{Backend, BedrockAuthMethod, ClaudeCodeSettings, ContainerInfo, EnvVar, GlobalAwsSettings, McpServer, McpTransportType, PortMapping, Project, ProjectPath};
|
||||||
|
|
||||||
const SCHEDULER_INSTRUCTIONS: &str = r#"## Scheduled Tasks
|
const SCHEDULER_INSTRUCTIONS: &str = r#"## Scheduled Tasks
|
||||||
|
|
||||||
@@ -88,6 +88,40 @@ This project uses **Flight Control** (bundled with Triple-C) for structured deve
|
|||||||
3. `.flightops/ARTIFACTS.md` — Where all artifacts are stored
|
3. `.flightops/ARTIFACTS.md` — Where all artifacts are stored
|
||||||
4. `.flightops/agent-crews/` — Project crew definitions for each phase (read the relevant crew file)"#;
|
4. `.flightops/agent-crews/` — Project crew definitions for each phase (read the relevant crew file)"#;
|
||||||
|
|
||||||
|
const SANDBOX_INSTRUCTIONS: &str = r#"## Sandbox Mode
|
||||||
|
|
||||||
|
This container has Claude Code's bash sandbox enabled, managed by Triple-C
|
||||||
|
(toggle it from the project's "Sandbox mode" switch in the Triple-C UI).
|
||||||
|
Bash commands run inside `bubblewrap` with filesystem and network isolation
|
||||||
|
(`enableWeakerNestedSandbox` is on because we are inside Docker).
|
||||||
|
|
||||||
|
### When a command fails because of sandbox restrictions
|
||||||
|
|
||||||
|
Triple-C disables the `dangerouslyDisableSandbox` escape hatch
|
||||||
|
(`allowUnsandboxedCommands: false`), so failing commands cannot bypass the
|
||||||
|
sandbox at runtime. To make a blocked command work, edit
|
||||||
|
`~/.claude/settings.json` and restart Claude Code:
|
||||||
|
|
||||||
|
| Need | Setting |
|
||||||
|
|---|---|
|
||||||
|
| Write to a path outside the project (e.g. `~/.kube`) | Add to `sandbox.filesystem.allowWrite` |
|
||||||
|
| Reach a new domain | Will prompt; or add permanently to `sandbox.allowedDomains` |
|
||||||
|
| Run a specific tool entirely outside the sandbox | Add a glob (e.g. `"docker *"`) to `sandbox.excludedCommands` |
|
||||||
|
|
||||||
|
### Docker commands
|
||||||
|
|
||||||
|
The `docker` CLI does not work inside the sandbox. If this project has
|
||||||
|
"Allow container spawning" enabled in Triple-C and you need to run
|
||||||
|
`docker` commands, add `"docker *"` to `sandbox.excludedCommands` in
|
||||||
|
`~/.claude/settings.json`. Other tools known to be sandbox-incompatible
|
||||||
|
include `watchman` — pass `--no-watchman` to `jest`.
|
||||||
|
|
||||||
|
### Disabling sandbox mode
|
||||||
|
|
||||||
|
Do not change `sandbox.enabled` in `settings.json` — Triple-C overwrites it
|
||||||
|
on every container start. To turn sandbox off, stop the container in
|
||||||
|
Triple-C, flip the "Sandbox mode" switch off, then start the container."#;
|
||||||
|
|
||||||
/// Build the full CLAUDE_INSTRUCTIONS value by merging global + project
|
/// Build the full CLAUDE_INSTRUCTIONS value by merging global + project
|
||||||
/// instructions, appending port mapping docs, and appending scheduler docs.
|
/// instructions, appending port mapping docs, and appending scheduler docs.
|
||||||
/// Used by both create_container() and container_needs_recreation() to ensure
|
/// Used by both create_container() and container_needs_recreation() to ensure
|
||||||
@@ -97,6 +131,7 @@ fn build_claude_instructions(
|
|||||||
project_instructions: Option<&str>,
|
project_instructions: Option<&str>,
|
||||||
port_mappings: &[PortMapping],
|
port_mappings: &[PortMapping],
|
||||||
mission_control_enabled: bool,
|
mission_control_enabled: bool,
|
||||||
|
sandbox_enabled: bool,
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
let mut combined = merge_claude_instructions(
|
let mut combined = merge_claude_instructions(
|
||||||
global_instructions,
|
global_instructions,
|
||||||
@@ -126,20 +161,30 @@ fn build_claude_instructions(
|
|||||||
None => SCHEDULER_INSTRUCTIONS.to_string(),
|
None => SCHEDULER_INSTRUCTIONS.to_string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if sandbox_enabled {
|
||||||
|
combined = Some(match combined {
|
||||||
|
Some(existing) => format!("{}\n\n{}", existing, SANDBOX_INSTRUCTIONS),
|
||||||
|
None => SANDBOX_INSTRUCTIONS.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
combined
|
combined
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute a fingerprint string for the custom environment variables.
|
/// Compute a fingerprint string for the custom environment variables.
|
||||||
/// Sorted alphabetically so order changes do not cause spurious recreation.
|
/// Sorted alphabetically so order changes do not cause spurious recreation.
|
||||||
fn compute_env_fingerprint(custom_env_vars: &[EnvVar]) -> String {
|
fn compute_env_fingerprint(custom_env_vars: &[EnvVar]) -> String {
|
||||||
let reserved_prefixes = ["ANTHROPIC_", "AWS_", "GIT_", "HOST_", "CLAUDE_", "TRIPLE_C_"];
|
let reserved_prefixes = ["ANTHROPIC_", "AWS_", "GIT_", "HOST_", "TRIPLE_C_"];
|
||||||
|
let reserved_exact = ["CLAUDE_INSTRUCTIONS", "MCP_SERVERS_JSON", "CLAUDE_CODE_SETTINGS_JSON", "MISSION_CONTROL_ENABLED"];
|
||||||
let mut parts: Vec<String> = Vec::new();
|
let mut parts: Vec<String> = Vec::new();
|
||||||
for env_var in custom_env_vars {
|
for env_var in custom_env_vars {
|
||||||
let key = env_var.key.trim();
|
let key = env_var.key.trim();
|
||||||
if key.is_empty() {
|
if key.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let is_reserved = reserved_prefixes.iter().any(|p| key.to_uppercase().starts_with(p));
|
let upper = key.to_uppercase();
|
||||||
|
let is_reserved = reserved_prefixes.iter().any(|p| upper.starts_with(p))
|
||||||
|
|| reserved_exact.iter().any(|e| upper == *e);
|
||||||
if is_reserved {
|
if is_reserved {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -224,6 +269,7 @@ fn compute_bedrock_fingerprint(project: &Project) -> String {
|
|||||||
bedrock.aws_bearer_token.as_deref().unwrap_or("").to_string(),
|
bedrock.aws_bearer_token.as_deref().unwrap_or("").to_string(),
|
||||||
bedrock.model_id.as_deref().unwrap_or("").to_string(),
|
bedrock.model_id.as_deref().unwrap_or("").to_string(),
|
||||||
format!("{}", bedrock.disable_prompt_caching),
|
format!("{}", bedrock.disable_prompt_caching),
|
||||||
|
bedrock.service_tier.as_deref().unwrap_or("").to_string(),
|
||||||
];
|
];
|
||||||
sha256_hex(&parts.join("|"))
|
sha256_hex(&parts.join("|"))
|
||||||
} else {
|
} else {
|
||||||
@@ -282,6 +328,117 @@ fn compute_ports_fingerprint(port_mappings: &[PortMapping]) -> String {
|
|||||||
sha256_hex(&joined)
|
sha256_hex(&joined)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Merge global and per-project ClaudeCodeSettings.
|
||||||
|
/// Per-project fields override global fields when set (non-default).
|
||||||
|
fn merge_claude_code_settings(
|
||||||
|
global: Option<&ClaudeCodeSettings>,
|
||||||
|
project: Option<&ClaudeCodeSettings>,
|
||||||
|
) -> Option<ClaudeCodeSettings> {
|
||||||
|
match (global, project) {
|
||||||
|
(None, None) => None,
|
||||||
|
(Some(g), None) => Some(g.clone()),
|
||||||
|
(None, Some(p)) => Some(p.clone()),
|
||||||
|
(Some(g), Some(p)) => {
|
||||||
|
// Project overrides global for each field when the project value is non-default
|
||||||
|
Some(ClaudeCodeSettings {
|
||||||
|
tui_mode: p.tui_mode.clone().or_else(|| g.tui_mode.clone()),
|
||||||
|
effort: p.effort.clone().or_else(|| g.effort.clone()),
|
||||||
|
auto_scroll_disabled: if p.auto_scroll_disabled { true } else { g.auto_scroll_disabled },
|
||||||
|
focus_mode: if p.focus_mode { true } else { g.focus_mode },
|
||||||
|
show_thinking_summaries: if p.show_thinking_summaries { true } else { g.show_thinking_summaries },
|
||||||
|
enable_session_recap: if p.enable_session_recap { true } else { g.enable_session_recap },
|
||||||
|
env_scrub: if p.env_scrub { true } else { g.env_scrub },
|
||||||
|
prompt_caching_1h: if p.prompt_caching_1h { true } else { g.prompt_caching_1h },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compute a fingerprint for the Claude Code settings so we can detect changes.
|
||||||
|
/// The `sandbox_enabled` flag is included so that toggling sandbox mode forces
|
||||||
|
/// a container recreation (re-injecting the merged settings.json). When
|
||||||
|
/// sandbox is off the historical fingerprint is preserved unchanged so that
|
||||||
|
/// upgrading triple-c does not spuriously flag every existing container for
|
||||||
|
/// recreation.
|
||||||
|
fn compute_claude_code_settings_fingerprint(
|
||||||
|
settings: Option<&ClaudeCodeSettings>,
|
||||||
|
sandbox_enabled: bool,
|
||||||
|
) -> String {
|
||||||
|
let base_fp = match settings {
|
||||||
|
None => String::new(),
|
||||||
|
Some(s) => {
|
||||||
|
let parts = vec![
|
||||||
|
s.tui_mode.as_deref().unwrap_or("").to_string(),
|
||||||
|
s.effort.as_deref().unwrap_or("").to_string(),
|
||||||
|
format!("{}", s.auto_scroll_disabled),
|
||||||
|
format!("{}", s.focus_mode),
|
||||||
|
format!("{}", s.show_thinking_summaries),
|
||||||
|
format!("{}", s.enable_session_recap),
|
||||||
|
format!("{}", s.env_scrub),
|
||||||
|
format!("{}", s.prompt_caching_1h),
|
||||||
|
];
|
||||||
|
sha256_hex(&parts.join("|"))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if sandbox_enabled {
|
||||||
|
sha256_hex(&format!("{}|sandbox=true", base_fp))
|
||||||
|
} else {
|
||||||
|
base_fp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the settings.json content for Claude Code.
|
||||||
|
/// Returns a JSON string of the settings to be written to ~/.claude/settings.json.
|
||||||
|
/// Always emits a `sandbox.enabled` key reflecting the current per-project
|
||||||
|
/// toggle so that flipping it off in triple-c overrides any prior on-state
|
||||||
|
/// stored in the persisted settings.json (which lives in a named volume).
|
||||||
|
fn build_claude_code_settings_json(
|
||||||
|
settings: Option<&ClaudeCodeSettings>,
|
||||||
|
sandbox_enabled: bool,
|
||||||
|
) -> Option<String> {
|
||||||
|
let mut map = serde_json::Map::new();
|
||||||
|
|
||||||
|
if let Some(s) = settings {
|
||||||
|
if let Some(ref tui) = s.tui_mode {
|
||||||
|
map.insert("tui".to_string(), serde_json::json!(tui));
|
||||||
|
}
|
||||||
|
if let Some(ref effort) = s.effort {
|
||||||
|
map.insert("effort".to_string(), serde_json::json!(effort));
|
||||||
|
}
|
||||||
|
if s.auto_scroll_disabled {
|
||||||
|
map.insert("autoScrollEnabled".to_string(), serde_json::json!(false));
|
||||||
|
}
|
||||||
|
if s.focus_mode {
|
||||||
|
map.insert("focusMode".to_string(), serde_json::json!(true));
|
||||||
|
}
|
||||||
|
if s.show_thinking_summaries {
|
||||||
|
map.insert("showThinkingSummaries".to_string(), serde_json::json!(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always emit `sandbox.enabled` so that toggling the per-project sandbox
|
||||||
|
// off in triple-c clears any prior on-state in the persisted
|
||||||
|
// settings.json (which lives in a named volume that survives recreation).
|
||||||
|
// Inside a Docker container we can't rely on privileged user namespaces,
|
||||||
|
// so `enableWeakerNestedSandbox` is required when sandbox is on.
|
||||||
|
let sandbox_obj = if sandbox_enabled {
|
||||||
|
serde_json::json!({
|
||||||
|
"enabled": true,
|
||||||
|
"enableWeakerNestedSandbox": true,
|
||||||
|
"allowUnsandboxedCommands": false,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
serde_json::json!({ "enabled": false })
|
||||||
|
};
|
||||||
|
map.insert("sandbox".to_string(), sandbox_obj);
|
||||||
|
|
||||||
|
if map.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(serde_json::Value::Object(map).to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Build the JSON value for MCP servers config to be injected into ~/.claude.json.
|
/// Build the JSON value for MCP servers config to be injected into ~/.claude.json.
|
||||||
/// Produces `{"mcpServers": {"name": {"type": "stdio", ...}, ...}}`.
|
/// Produces `{"mcpServers": {"name": {"type": "stdio", ...}, ...}}`.
|
||||||
///
|
///
|
||||||
@@ -400,6 +557,10 @@ pub async fn create_container(
|
|||||||
timezone: Option<&str>,
|
timezone: Option<&str>,
|
||||||
mcp_servers: &[McpServer],
|
mcp_servers: &[McpServer],
|
||||||
network_name: Option<&str>,
|
network_name: Option<&str>,
|
||||||
|
global_claude_code_settings: Option<&ClaudeCodeSettings>,
|
||||||
|
default_ssh_key_path: Option<&str>,
|
||||||
|
default_git_user_name: Option<&str>,
|
||||||
|
default_git_user_email: Option<&str>,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
let docker = get_docker()?;
|
let docker = get_docker()?;
|
||||||
let container_name = project.container_name();
|
let container_name = project.container_name();
|
||||||
@@ -445,10 +606,13 @@ pub async fn create_container(
|
|||||||
if let Some(ref token) = project.git_token {
|
if let Some(ref token) = project.git_token {
|
||||||
env_vars.push(format!("GIT_TOKEN={}", token));
|
env_vars.push(format!("GIT_TOKEN={}", token));
|
||||||
}
|
}
|
||||||
if let Some(ref name) = project.git_user_name {
|
// Per-project git user overrides global defaults
|
||||||
|
let effective_git_name = project.git_user_name.as_deref().or(default_git_user_name);
|
||||||
|
let effective_git_email = project.git_user_email.as_deref().or(default_git_user_email);
|
||||||
|
if let Some(name) = effective_git_name {
|
||||||
env_vars.push(format!("GIT_USER_NAME={}", name));
|
env_vars.push(format!("GIT_USER_NAME={}", name));
|
||||||
}
|
}
|
||||||
if let Some(ref email) = project.git_user_email {
|
if let Some(email) = effective_git_email {
|
||||||
env_vars.push(format!("GIT_USER_EMAIL={}", email));
|
env_vars.push(format!("GIT_USER_EMAIL={}", email));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -502,6 +666,13 @@ pub async fn create_container(
|
|||||||
if bedrock.disable_prompt_caching {
|
if bedrock.disable_prompt_caching {
|
||||||
env_vars.push("DISABLE_PROMPT_CACHING=1".to_string());
|
env_vars.push("DISABLE_PROMPT_CACHING=1".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(ref tier) = bedrock.service_tier {
|
||||||
|
let trimmed = tier.trim();
|
||||||
|
if !trimmed.is_empty() {
|
||||||
|
env_vars.push(format!("ANTHROPIC_BEDROCK_SERVICE_TIER={}", trimmed));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -531,13 +702,16 @@ pub async fn create_container(
|
|||||||
|
|
||||||
// Custom environment variables (global + per-project, project overrides global for same key)
|
// Custom environment variables (global + per-project, project overrides global for same key)
|
||||||
let merged_env = merge_custom_env_vars(global_custom_env_vars, &project.custom_env_vars);
|
let merged_env = merge_custom_env_vars(global_custom_env_vars, &project.custom_env_vars);
|
||||||
let reserved_prefixes = ["ANTHROPIC_", "AWS_", "GIT_", "HOST_", "CLAUDE_", "TRIPLE_C_"];
|
let reserved_prefixes = ["ANTHROPIC_", "AWS_", "GIT_", "HOST_", "TRIPLE_C_"];
|
||||||
|
let reserved_exact = ["CLAUDE_INSTRUCTIONS", "MCP_SERVERS_JSON", "CLAUDE_CODE_SETTINGS_JSON", "MISSION_CONTROL_ENABLED"];
|
||||||
for env_var in &merged_env {
|
for env_var in &merged_env {
|
||||||
let key = env_var.key.trim();
|
let key = env_var.key.trim();
|
||||||
if key.is_empty() {
|
if key.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let is_reserved = reserved_prefixes.iter().any(|p| key.to_uppercase().starts_with(p));
|
let upper = key.to_uppercase();
|
||||||
|
let is_reserved = reserved_prefixes.iter().any(|p| upper.starts_with(p))
|
||||||
|
|| reserved_exact.iter().any(|e| upper == *e);
|
||||||
if is_reserved {
|
if is_reserved {
|
||||||
log::warn!("Skipping reserved env var: {}", key);
|
log::warn!("Skipping reserved env var: {}", key);
|
||||||
continue;
|
continue;
|
||||||
@@ -565,6 +739,7 @@ pub async fn create_container(
|
|||||||
project.claude_instructions.as_deref(),
|
project.claude_instructions.as_deref(),
|
||||||
&project.port_mappings,
|
&project.port_mappings,
|
||||||
project.mission_control_enabled,
|
project.mission_control_enabled,
|
||||||
|
project.sandbox_mode_enabled,
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(ref instructions) = combined_instructions {
|
if let Some(ref instructions) = combined_instructions {
|
||||||
@@ -577,6 +752,37 @@ pub async fn create_container(
|
|||||||
env_vars.push(format!("MCP_SERVERS_JSON={}", mcp_json));
|
env_vars.push(format!("MCP_SERVERS_JSON={}", mcp_json));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Claude Code settings (global + per-project merged)
|
||||||
|
let merged_cc_settings = merge_claude_code_settings(
|
||||||
|
global_claude_code_settings,
|
||||||
|
project.claude_code_settings.as_ref(),
|
||||||
|
);
|
||||||
|
if let Some(ref cc) = merged_cc_settings {
|
||||||
|
// Env-var-based settings (these are read directly by Claude Code)
|
||||||
|
if cc.tui_mode.as_deref() == Some("fullscreen") {
|
||||||
|
env_vars.push("CLAUDE_CODE_NO_FLICKER=1".to_string());
|
||||||
|
}
|
||||||
|
if cc.enable_session_recap {
|
||||||
|
env_vars.push("CLAUDE_CODE_ENABLE_AWAY_SUMMARY=1".to_string());
|
||||||
|
}
|
||||||
|
if cc.env_scrub {
|
||||||
|
env_vars.push("CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1".to_string());
|
||||||
|
}
|
||||||
|
if cc.prompt_caching_1h {
|
||||||
|
env_vars.push("ENABLE_PROMPT_CACHING_1H=1".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// settings.json-based settings (written by the entrypoint).
|
||||||
|
// Always invoked so per-project sandbox state is injected even when no
|
||||||
|
// ClaudeCodeSettings struct is present.
|
||||||
|
if let Some(settings_json) = build_claude_code_settings_json(
|
||||||
|
merged_cc_settings.as_ref(),
|
||||||
|
project.sandbox_mode_enabled,
|
||||||
|
) {
|
||||||
|
env_vars.push(format!("CLAUDE_CODE_SETTINGS_JSON={}", settings_json));
|
||||||
|
}
|
||||||
|
|
||||||
let mut mounts: Vec<Mount> = Vec::new();
|
let mut mounts: Vec<Mount> = Vec::new();
|
||||||
|
|
||||||
// Project directories -> /workspace/{mount_name}
|
// Project directories -> /workspace/{mount_name}
|
||||||
@@ -612,10 +818,12 @@ pub async fn create_container(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// SSH keys mount (read-only staging; entrypoint copies to ~/.ssh with correct perms)
|
// SSH keys mount (read-only staging; entrypoint copies to ~/.ssh with correct perms)
|
||||||
if let Some(ref ssh_path) = project.ssh_key_path {
|
// Per-project ssh_key_path overrides global default_ssh_key_path
|
||||||
|
let effective_ssh_path = project.ssh_key_path.as_deref().or(default_ssh_key_path);
|
||||||
|
if let Some(ssh_path) = effective_ssh_path {
|
||||||
mounts.push(Mount {
|
mounts.push(Mount {
|
||||||
target: Some("/tmp/.host-ssh".to_string()),
|
target: Some("/tmp/.host-ssh".to_string()),
|
||||||
source: Some(ssh_path.clone()),
|
source: Some(ssh_path.to_string()),
|
||||||
typ: Some(MountTypeEnum::BIND),
|
typ: Some(MountTypeEnum::BIND),
|
||||||
read_only: Some(true),
|
read_only: Some(true),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -705,10 +913,12 @@ pub async fn create_container(
|
|||||||
labels.insert("triple-c.mcp-fingerprint".to_string(), compute_mcp_fingerprint(mcp_servers));
|
labels.insert("triple-c.mcp-fingerprint".to_string(), compute_mcp_fingerprint(mcp_servers));
|
||||||
labels.insert("triple-c.mission-control".to_string(), project.mission_control_enabled.to_string());
|
labels.insert("triple-c.mission-control".to_string(), project.mission_control_enabled.to_string());
|
||||||
labels.insert("triple-c.custom-env-fingerprint".to_string(), custom_env_fingerprint.clone());
|
labels.insert("triple-c.custom-env-fingerprint".to_string(), custom_env_fingerprint.clone());
|
||||||
|
labels.insert("triple-c.claude-code-settings-fingerprint".to_string(),
|
||||||
|
compute_claude_code_settings_fingerprint(merged_cc_settings.as_ref(), project.sandbox_mode_enabled));
|
||||||
labels.insert("triple-c.instructions-fingerprint".to_string(),
|
labels.insert("triple-c.instructions-fingerprint".to_string(),
|
||||||
combined_instructions.as_ref().map(|s| sha256_hex(s)).unwrap_or_default());
|
combined_instructions.as_ref().map(|s| sha256_hex(s)).unwrap_or_default());
|
||||||
labels.insert("triple-c.git-user-name".to_string(), project.git_user_name.clone().unwrap_or_default());
|
labels.insert("triple-c.git-user-name".to_string(), effective_git_name.unwrap_or_default().to_string());
|
||||||
labels.insert("triple-c.git-user-email".to_string(), project.git_user_email.clone().unwrap_or_default());
|
labels.insert("triple-c.git-user-email".to_string(), effective_git_email.unwrap_or_default().to_string());
|
||||||
labels.insert("triple-c.git-token-hash".to_string(),
|
labels.insert("triple-c.git-token-hash".to_string(),
|
||||||
project.git_token.as_ref().map(|t| sha256_hex(t)).unwrap_or_default());
|
project.git_token.as_ref().map(|t| sha256_hex(t)).unwrap_or_default());
|
||||||
|
|
||||||
@@ -877,6 +1087,10 @@ pub async fn container_needs_recreation(
|
|||||||
global_custom_env_vars: &[EnvVar],
|
global_custom_env_vars: &[EnvVar],
|
||||||
timezone: Option<&str>,
|
timezone: Option<&str>,
|
||||||
mcp_servers: &[McpServer],
|
mcp_servers: &[McpServer],
|
||||||
|
global_claude_code_settings: Option<&ClaudeCodeSettings>,
|
||||||
|
default_ssh_key_path: Option<&str>,
|
||||||
|
default_git_user_name: Option<&str>,
|
||||||
|
default_git_user_email: Option<&str>,
|
||||||
) -> Result<bool, String> {
|
) -> Result<bool, String> {
|
||||||
let docker = get_docker()?;
|
let docker = get_docker()?;
|
||||||
let info = docker
|
let info = docker
|
||||||
@@ -997,28 +1211,34 @@ pub async fn container_needs_recreation(
|
|||||||
.find(|mount| mount.target.as_deref() == Some("/tmp/.host-ssh"))
|
.find(|mount| mount.target.as_deref() == Some("/tmp/.host-ssh"))
|
||||||
})
|
})
|
||||||
.and_then(|mount| mount.source.as_deref());
|
.and_then(|mount| mount.source.as_deref());
|
||||||
let project_ssh = project.ssh_key_path.as_deref();
|
let effective_ssh = project.ssh_key_path.as_deref().or(default_ssh_key_path);
|
||||||
if ssh_mount_source != project_ssh {
|
if ssh_mount_source != effective_ssh {
|
||||||
log::info!(
|
log::info!(
|
||||||
"SSH key path mismatch (container={:?}, project={:?})",
|
"SSH key path mismatch (container={:?}, expected={:?})",
|
||||||
ssh_mount_source,
|
ssh_mount_source,
|
||||||
project_ssh
|
effective_ssh
|
||||||
);
|
);
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Git settings (label-based to avoid stale snapshot env vars) ─────
|
// ── Git settings (label-based to avoid stale snapshot env vars) ─────
|
||||||
let expected_git_name = project.git_user_name.clone().unwrap_or_default();
|
let expected_git_name = project.git_user_name.as_deref()
|
||||||
|
.or(default_git_user_name)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string();
|
||||||
let container_git_name = get_label("triple-c.git-user-name").unwrap_or_default();
|
let container_git_name = get_label("triple-c.git-user-name").unwrap_or_default();
|
||||||
if container_git_name != expected_git_name {
|
if container_git_name != expected_git_name {
|
||||||
log::info!("GIT_USER_NAME mismatch (container={:?}, project={:?})", container_git_name, expected_git_name);
|
log::info!("GIT_USER_NAME mismatch (container={:?}, expected={:?})", container_git_name, expected_git_name);
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
let expected_git_email = project.git_user_email.clone().unwrap_or_default();
|
let expected_git_email = project.git_user_email.as_deref()
|
||||||
|
.or(default_git_user_email)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string();
|
||||||
let container_git_email = get_label("triple-c.git-user-email").unwrap_or_default();
|
let container_git_email = get_label("triple-c.git-user-email").unwrap_or_default();
|
||||||
if container_git_email != expected_git_email {
|
if container_git_email != expected_git_email {
|
||||||
log::info!("GIT_USER_EMAIL mismatch (container={:?}, project={:?})", container_git_email, expected_git_email);
|
log::info!("GIT_USER_EMAIL mismatch (container={:?}, expected={:?})", container_git_email, expected_git_email);
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1052,6 +1272,7 @@ pub async fn container_needs_recreation(
|
|||||||
project.claude_instructions.as_deref(),
|
project.claude_instructions.as_deref(),
|
||||||
&project.port_mappings,
|
&project.port_mappings,
|
||||||
project.mission_control_enabled,
|
project.mission_control_enabled,
|
||||||
|
project.sandbox_mode_enabled,
|
||||||
);
|
);
|
||||||
let expected_instructions_fp = expected_instructions.as_ref().map(|s| sha256_hex(s)).unwrap_or_default();
|
let expected_instructions_fp = expected_instructions.as_ref().map(|s| sha256_hex(s)).unwrap_or_default();
|
||||||
let container_instructions_fp = get_label("triple-c.instructions-fingerprint").unwrap_or_default();
|
let container_instructions_fp = get_label("triple-c.instructions-fingerprint").unwrap_or_default();
|
||||||
@@ -1060,6 +1281,18 @@ pub async fn container_needs_recreation(
|
|||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Claude Code settings fingerprint ───────────────────────────────
|
||||||
|
let merged_cc = merge_claude_code_settings(
|
||||||
|
global_claude_code_settings,
|
||||||
|
project.claude_code_settings.as_ref(),
|
||||||
|
);
|
||||||
|
let expected_cc_fp = compute_claude_code_settings_fingerprint(merged_cc.as_ref(), project.sandbox_mode_enabled);
|
||||||
|
let container_cc_fp = get_label("triple-c.claude-code-settings-fingerprint").unwrap_or_default();
|
||||||
|
if container_cc_fp != expected_cc_fp {
|
||||||
|
log::info!("Claude Code settings mismatch (container={:?}, expected={:?})", container_cc_fp, expected_cc_fp);
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
// ── MCP servers fingerprint ─────────────────────────────────────────
|
// ── MCP servers fingerprint ─────────────────────────────────────────
|
||||||
let expected_mcp_fp = compute_mcp_fingerprint(mcp_servers);
|
let expected_mcp_fp = compute_mcp_fingerprint(mcp_servers);
|
||||||
let container_mcp_fp = get_label("triple-c.mcp-fingerprint").unwrap_or_default();
|
let container_mcp_fp = get_label("triple-c.mcp-fingerprint").unwrap_or_default();
|
||||||
|
|||||||
53
app/src-tauri/src/install_helper/mod.rs
Normal file
53
app/src-tauri/src/install_helper/mod.rs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
// Helpers for detecting whether Docker (or a Docker-compatible runtime) is
|
||||||
|
// installed on the host and, when missing, offering to install it for the user.
|
||||||
|
//
|
||||||
|
// We use the Docker convenience script on Linux and Rancher Desktop on macOS /
|
||||||
|
// Windows. On every platform we also surface an official documentation URL so
|
||||||
|
// users without a recognised package manager can install manually.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub mod platform;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct InstallOptions {
|
||||||
|
/// "linux" | "macos" | "windows" | "unknown"
|
||||||
|
pub os: String,
|
||||||
|
/// User-facing name of what we'd install ("Docker Engine" / "Rancher Desktop").
|
||||||
|
pub product_name: String,
|
||||||
|
/// Whether we can kick off a one-click install with what's on this machine.
|
||||||
|
pub can_auto_install: bool,
|
||||||
|
/// Short identifier of the method we'd use ("pkexec", "brew", "winget", or None).
|
||||||
|
pub auto_install_method: Option<String>,
|
||||||
|
/// If auto-install isn't possible, a human-readable reason to show the user.
|
||||||
|
pub auto_install_blocker: Option<String>,
|
||||||
|
/// Official documentation URL for manual install.
|
||||||
|
pub docs_url: String,
|
||||||
|
/// Ordered manual install steps (plain text lines).
|
||||||
|
pub manual_steps: Vec<String>,
|
||||||
|
/// Notes to display after a successful auto-install (e.g. log out/back in).
|
||||||
|
pub post_install_notes: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn detect_install_options() -> InstallOptions {
|
||||||
|
if cfg!(target_os = "linux") {
|
||||||
|
platform::linux_options()
|
||||||
|
} else if cfg!(target_os = "macos") {
|
||||||
|
platform::macos_options()
|
||||||
|
} else if cfg!(target_os = "windows") {
|
||||||
|
platform::windows_options()
|
||||||
|
} else {
|
||||||
|
InstallOptions {
|
||||||
|
os: "unknown".into(),
|
||||||
|
product_name: "Docker".into(),
|
||||||
|
can_auto_install: false,
|
||||||
|
auto_install_method: None,
|
||||||
|
auto_install_blocker: Some("Unsupported operating system".into()),
|
||||||
|
docs_url: "https://docs.docker.com/get-docker/".into(),
|
||||||
|
manual_steps: vec![
|
||||||
|
"Visit the Docker documentation and follow the install guide for your OS.".into(),
|
||||||
|
],
|
||||||
|
post_install_notes: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
288
app/src-tauri/src/install_helper/platform.rs
Normal file
288
app/src-tauri/src/install_helper/platform.rs
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::Stdio;
|
||||||
|
|
||||||
|
use tauri::{AppHandle, Emitter};
|
||||||
|
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||||
|
use tokio::process::Command;
|
||||||
|
|
||||||
|
use super::InstallOptions;
|
||||||
|
|
||||||
|
const PROGRESS_EVENT: &str = "docker-install-progress";
|
||||||
|
|
||||||
|
fn which(cmd: &str) -> bool {
|
||||||
|
find_on_path(cmd).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Search PATH for an executable, plus a handful of well-known locations that
|
||||||
|
/// GUI-launched apps on macOS/Linux typically miss (Homebrew prefixes, etc.).
|
||||||
|
fn find_on_path(cmd: &str) -> Option<PathBuf> {
|
||||||
|
#[cfg(unix)]
|
||||||
|
let extra: &[&str] = &[
|
||||||
|
"/opt/homebrew/bin",
|
||||||
|
"/usr/local/bin",
|
||||||
|
"/usr/bin",
|
||||||
|
"/bin",
|
||||||
|
];
|
||||||
|
#[cfg(windows)]
|
||||||
|
let extra: &[&str] = &[];
|
||||||
|
|
||||||
|
if let Ok(path) = std::env::var("PATH") {
|
||||||
|
let sep = if cfg!(windows) { ';' } else { ':' };
|
||||||
|
for dir in path.split(sep).chain(extra.iter().copied()) {
|
||||||
|
let candidate = PathBuf::from(dir).join(cmd);
|
||||||
|
if candidate.is_file() {
|
||||||
|
return Some(candidate);
|
||||||
|
}
|
||||||
|
#[cfg(windows)]
|
||||||
|
for ext in ["exe", "cmd", "bat"] {
|
||||||
|
let mut with_ext = candidate.clone();
|
||||||
|
with_ext.set_extension(ext);
|
||||||
|
if with_ext.is_file() {
|
||||||
|
return Some(with_ext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for dir in extra {
|
||||||
|
let candidate = PathBuf::from(dir).join(cmd);
|
||||||
|
if candidate.is_file() {
|
||||||
|
return Some(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn stream(app: &AppHandle, mut child: tokio::process::Child) -> Result<(), String> {
|
||||||
|
let stdout = child.stdout.take();
|
||||||
|
let stderr = child.stderr.take();
|
||||||
|
|
||||||
|
let app_out = app.clone();
|
||||||
|
let out_task = tokio::spawn(async move {
|
||||||
|
if let Some(out) = stdout {
|
||||||
|
let mut lines = BufReader::new(out).lines();
|
||||||
|
while let Ok(Some(line)) = lines.next_line().await {
|
||||||
|
let _ = app_out.emit(PROGRESS_EVENT, line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let app_err = app.clone();
|
||||||
|
let err_task = tokio::spawn(async move {
|
||||||
|
if let Some(err) = stderr {
|
||||||
|
let mut lines = BufReader::new(err).lines();
|
||||||
|
while let Ok(Some(line)) = lines.next_line().await {
|
||||||
|
let _ = app_err.emit(PROGRESS_EVENT, line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let status = child
|
||||||
|
.wait()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("install process failed: {}", e))?;
|
||||||
|
let _ = out_task.await;
|
||||||
|
let _ = err_task.await;
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
return Err(format!(
|
||||||
|
"installer exited with status {}",
|
||||||
|
status.code().map(|c| c.to_string()).unwrap_or_else(|| "signal".into())
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Linux ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
pub fn linux_options() -> InstallOptions {
|
||||||
|
let has_pkexec = which("pkexec");
|
||||||
|
let has_curl = which("curl");
|
||||||
|
|
||||||
|
let (can_auto, blocker) = match (has_pkexec, has_curl) {
|
||||||
|
(true, true) => (true, None),
|
||||||
|
(false, _) => (
|
||||||
|
false,
|
||||||
|
Some("pkexec not found — install policykit-1 or follow manual steps.".into()),
|
||||||
|
),
|
||||||
|
(_, false) => (
|
||||||
|
false,
|
||||||
|
Some("curl not found — install curl or follow manual steps.".into()),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
InstallOptions {
|
||||||
|
os: "linux".into(),
|
||||||
|
product_name: "Docker Engine".into(),
|
||||||
|
can_auto_install: can_auto,
|
||||||
|
auto_install_method: if can_auto { Some("pkexec".into()) } else { None },
|
||||||
|
auto_install_blocker: blocker,
|
||||||
|
docs_url: "https://docs.docker.com/engine/install/".into(),
|
||||||
|
manual_steps: vec![
|
||||||
|
"Open a terminal.".into(),
|
||||||
|
"Run: curl -fsSL https://get.docker.com | sh".into(),
|
||||||
|
"Add yourself to the docker group: sudo usermod -aG docker $USER".into(),
|
||||||
|
"Log out and log back in for group changes to take effect.".into(),
|
||||||
|
],
|
||||||
|
post_install_notes: vec![
|
||||||
|
"Log out and log back in (or reboot) so your user picks up the docker group.".into(),
|
||||||
|
"If Docker isn't detected after re-login, start the service: sudo systemctl start docker".into(),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_linux_install(app: &AppHandle) -> Result<(), String> {
|
||||||
|
// Grab the current username so pkexec (which runs as root) can add the
|
||||||
|
// original invoking user to the docker group.
|
||||||
|
let invoking_user = std::env::var("USER")
|
||||||
|
.or_else(|_| std::env::var("LOGNAME"))
|
||||||
|
.map_err(|_| "could not determine invoking username".to_string())?;
|
||||||
|
|
||||||
|
// Write a self-contained installer script to a temp file. Running the
|
||||||
|
// Docker convenience script then appending the user to the docker group
|
||||||
|
// and enabling the service.
|
||||||
|
let script = format!(
|
||||||
|
r#"#!/bin/sh
|
||||||
|
set -e
|
||||||
|
echo "[triple-c] Downloading Docker install script..."
|
||||||
|
curl -fsSL https://get.docker.com -o /tmp/triple-c-get-docker.sh
|
||||||
|
echo "[triple-c] Running Docker install script (may take a few minutes)..."
|
||||||
|
sh /tmp/triple-c-get-docker.sh
|
||||||
|
rm -f /tmp/triple-c-get-docker.sh
|
||||||
|
echo "[triple-c] Adding {user} to docker group..."
|
||||||
|
usermod -aG docker "{user}" || true
|
||||||
|
echo "[triple-c] Enabling docker service..."
|
||||||
|
systemctl enable --now docker 2>/dev/null || service docker start 2>/dev/null || true
|
||||||
|
echo "[triple-c] Install complete. Log out and back in to use Docker without sudo."
|
||||||
|
"#,
|
||||||
|
user = invoking_user
|
||||||
|
);
|
||||||
|
|
||||||
|
let script_path: PathBuf = std::env::temp_dir().join("triple-c-install-docker.sh");
|
||||||
|
tokio::fs::write(&script_path, script)
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("failed to write install script: {}", e))?;
|
||||||
|
|
||||||
|
let _ = app.emit(
|
||||||
|
PROGRESS_EVENT,
|
||||||
|
format!("Requesting administrator privileges via pkexec..."),
|
||||||
|
);
|
||||||
|
|
||||||
|
let child = Command::new("pkexec")
|
||||||
|
.arg("sh")
|
||||||
|
.arg(&script_path)
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| format!("failed to launch pkexec: {}", e))?;
|
||||||
|
|
||||||
|
let result = stream(app, child).await;
|
||||||
|
let _ = tokio::fs::remove_file(&script_path).await;
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── macOS ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
pub fn macos_options() -> InstallOptions {
|
||||||
|
let has_brew = which("brew");
|
||||||
|
InstallOptions {
|
||||||
|
os: "macos".into(),
|
||||||
|
product_name: "Rancher Desktop".into(),
|
||||||
|
can_auto_install: has_brew,
|
||||||
|
auto_install_method: if has_brew { Some("brew".into()) } else { None },
|
||||||
|
auto_install_blocker: if has_brew {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some("Homebrew not found — use the manual download.".into())
|
||||||
|
},
|
||||||
|
docs_url: "https://docs.rancherdesktop.io/getting-started/installation/".into(),
|
||||||
|
manual_steps: vec![
|
||||||
|
"Download the Rancher Desktop .dmg from the official site.".into(),
|
||||||
|
"Open the .dmg and drag Rancher Desktop into Applications.".into(),
|
||||||
|
"Launch Rancher Desktop and complete the first-run setup (choose dockerd/moby).".into(),
|
||||||
|
"Once the Docker socket is available, come back and click Refresh.".into(),
|
||||||
|
],
|
||||||
|
post_install_notes: vec![
|
||||||
|
"Launch Rancher Desktop from Applications if it didn't open automatically.".into(),
|
||||||
|
"In Preferences, make sure the container engine is set to dockerd (moby).".into(),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_macos_install(app: &AppHandle) -> Result<(), String> {
|
||||||
|
let brew = find_on_path("brew")
|
||||||
|
.ok_or_else(|| "Homebrew not found — follow the manual steps instead.".to_string())?;
|
||||||
|
let _ = app.emit(
|
||||||
|
PROGRESS_EVENT,
|
||||||
|
format!("Running: {} install --cask rancher", brew.display()),
|
||||||
|
);
|
||||||
|
let child = Command::new(&brew)
|
||||||
|
.args(["install", "--cask", "rancher"])
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| format!("failed to launch brew: {}", e))?;
|
||||||
|
stream(app, child).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Windows ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
pub fn windows_options() -> InstallOptions {
|
||||||
|
let has_winget = which("winget");
|
||||||
|
InstallOptions {
|
||||||
|
os: "windows".into(),
|
||||||
|
product_name: "Rancher Desktop".into(),
|
||||||
|
can_auto_install: has_winget,
|
||||||
|
auto_install_method: if has_winget { Some("winget".into()) } else { None },
|
||||||
|
auto_install_blocker: if has_winget {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some("winget not found — use the manual download.".into())
|
||||||
|
},
|
||||||
|
docs_url: "https://docs.rancherdesktop.io/getting-started/installation/".into(),
|
||||||
|
manual_steps: vec![
|
||||||
|
"Download the Rancher Desktop .msi from the official site.".into(),
|
||||||
|
"Run the installer and accept the WSL2 prompts if asked.".into(),
|
||||||
|
"Launch Rancher Desktop and complete the first-run setup (choose dockerd/moby).".into(),
|
||||||
|
"Once the Docker engine is running, come back and click Refresh.".into(),
|
||||||
|
],
|
||||||
|
post_install_notes: vec![
|
||||||
|
"Launch Rancher Desktop from the Start menu if it didn't open automatically.".into(),
|
||||||
|
"In Preferences > Container Engine, make sure dockerd (moby) is selected.".into(),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_windows_install(app: &AppHandle) -> Result<(), String> {
|
||||||
|
let _ = app.emit(
|
||||||
|
PROGRESS_EVENT,
|
||||||
|
"Running: winget install --id SUSE.RancherDesktop -e --accept-package-agreements --accept-source-agreements".to_string(),
|
||||||
|
);
|
||||||
|
let child = Command::new("winget")
|
||||||
|
.args([
|
||||||
|
"install",
|
||||||
|
"--id",
|
||||||
|
"SUSE.RancherDesktop",
|
||||||
|
"-e",
|
||||||
|
"--accept-package-agreements",
|
||||||
|
"--accept-source-agreements",
|
||||||
|
])
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| format!("failed to launch winget: {}", e))?;
|
||||||
|
stream(app, child).await
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Dispatcher ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
pub async fn run_install(app: &AppHandle) -> Result<(), String> {
|
||||||
|
if cfg!(target_os = "linux") {
|
||||||
|
run_linux_install(app).await
|
||||||
|
} else if cfg!(target_os = "macos") {
|
||||||
|
run_macos_install(app).await
|
||||||
|
} else if cfg!(target_os = "windows") {
|
||||||
|
run_windows_install(app).await
|
||||||
|
} else {
|
||||||
|
Err("auto-install is not supported on this OS".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
mod commands;
|
mod commands;
|
||||||
mod docker;
|
mod docker;
|
||||||
|
mod install_helper;
|
||||||
mod logging;
|
mod logging;
|
||||||
mod models;
|
mod models;
|
||||||
mod storage;
|
mod storage;
|
||||||
@@ -111,6 +112,25 @@ pub fn run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-start STT container if enabled in settings
|
||||||
|
if settings.stt.enabled {
|
||||||
|
let stt_settings = settings.stt.clone();
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
match docker::stt::ensure_stt_running(&stt_settings).await {
|
||||||
|
Ok(status) => {
|
||||||
|
if status.running {
|
||||||
|
log::info!("STT container auto-started on port {}", stt_settings.port);
|
||||||
|
} else {
|
||||||
|
log::warn!("STT auto-start: container not running after ensure_stt_running");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to auto-start STT container: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
.on_window_event(|window, event| {
|
.on_window_event(|window, event| {
|
||||||
@@ -178,6 +198,9 @@ pub fn run() {
|
|||||||
commands::update_commands::check_image_update,
|
commands::update_commands::check_image_update,
|
||||||
// Help
|
// Help
|
||||||
commands::help_commands::get_help_content,
|
commands::help_commands::get_help_content,
|
||||||
|
// Install helper
|
||||||
|
commands::install_helper_commands::detect_install_options,
|
||||||
|
commands::install_helper_commands::run_docker_install,
|
||||||
// Web Terminal
|
// Web Terminal
|
||||||
commands::web_terminal_commands::start_web_terminal,
|
commands::web_terminal_commands::start_web_terminal,
|
||||||
commands::web_terminal_commands::stop_web_terminal,
|
commands::web_terminal_commands::stop_web_terminal,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::project::EnvVar;
|
use super::project::{ClaudeCodeSettings, EnvVar};
|
||||||
|
|
||||||
fn default_true() -> bool {
|
fn default_true() -> bool {
|
||||||
true
|
true
|
||||||
@@ -78,6 +78,8 @@ pub struct AppSettings {
|
|||||||
pub web_terminal: WebTerminalSettings,
|
pub web_terminal: WebTerminalSettings,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub stt: SttSettings,
|
pub stt: SttSettings,
|
||||||
|
#[serde(default)]
|
||||||
|
pub global_claude_code_settings: Option<ClaudeCodeSettings>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_stt_model() -> String {
|
fn default_stt_model() -> String {
|
||||||
@@ -163,6 +165,7 @@ impl Default for AppSettings {
|
|||||||
dismissed_image_digest: None,
|
dismissed_image_digest: None,
|
||||||
web_terminal: WebTerminalSettings::default(),
|
web_terminal: WebTerminalSettings::default(),
|
||||||
stt: SttSettings::default(),
|
stt: SttSettings::default(),
|
||||||
|
global_claude_code_settings: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,36 @@ fn default_full_permissions() -> bool {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Settings for Claude Code CLI behavior inside the container.
|
||||||
|
/// These map to Claude Code env vars and ~/.claude/settings.json entries.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
||||||
|
pub struct ClaudeCodeSettings {
|
||||||
|
/// TUI rendering mode: None = default, Some("fullscreen") = flicker-free alt-screen
|
||||||
|
#[serde(default)]
|
||||||
|
pub tui_mode: Option<String>,
|
||||||
|
/// Effort level: None = default, Some("low"|"medium"|"high")
|
||||||
|
#[serde(default)]
|
||||||
|
pub effort: Option<String>,
|
||||||
|
/// Disable auto-scroll in fullscreen TUI mode
|
||||||
|
#[serde(default)]
|
||||||
|
pub auto_scroll_disabled: bool,
|
||||||
|
/// Enable focus mode (collapsed tool output)
|
||||||
|
#[serde(default)]
|
||||||
|
pub focus_mode: bool,
|
||||||
|
/// Show thinking summaries in responses
|
||||||
|
#[serde(default)]
|
||||||
|
pub show_thinking_summaries: bool,
|
||||||
|
/// Enable session recap when returning to a session
|
||||||
|
#[serde(default)]
|
||||||
|
pub enable_session_recap: bool,
|
||||||
|
/// Strip credentials from subprocess environments
|
||||||
|
#[serde(default)]
|
||||||
|
pub env_scrub: bool,
|
||||||
|
/// Enable 1-hour prompt cache TTL (vs default 5-minute)
|
||||||
|
#[serde(default)]
|
||||||
|
pub prompt_caching_1h: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Project {
|
pub struct Project {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
@@ -43,6 +73,8 @@ pub struct Project {
|
|||||||
pub openai_compatible_config: Option<OpenAiCompatibleConfig>,
|
pub openai_compatible_config: Option<OpenAiCompatibleConfig>,
|
||||||
pub allow_docker_access: bool,
|
pub allow_docker_access: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub sandbox_mode_enabled: bool,
|
||||||
|
#[serde(default)]
|
||||||
pub mission_control_enabled: bool,
|
pub mission_control_enabled: bool,
|
||||||
#[serde(default = "default_full_permissions")]
|
#[serde(default = "default_full_permissions")]
|
||||||
pub full_permissions: bool,
|
pub full_permissions: bool,
|
||||||
@@ -59,6 +91,8 @@ pub struct Project {
|
|||||||
pub claude_instructions: Option<String>,
|
pub claude_instructions: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub enabled_mcp_servers: Vec<String>,
|
pub enabled_mcp_servers: Vec<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub claude_code_settings: Option<ClaudeCodeSettings>,
|
||||||
pub created_at: String,
|
pub created_at: String,
|
||||||
pub updated_at: String,
|
pub updated_at: String,
|
||||||
}
|
}
|
||||||
@@ -127,6 +161,10 @@ pub struct BedrockConfig {
|
|||||||
pub aws_bearer_token: Option<String>,
|
pub aws_bearer_token: Option<String>,
|
||||||
pub model_id: Option<String>,
|
pub model_id: Option<String>,
|
||||||
pub disable_prompt_caching: bool,
|
pub disable_prompt_caching: bool,
|
||||||
|
/// Optional value for the `ANTHROPIC_BEDROCK_SERVICE_TIER` env var
|
||||||
|
/// (e.g. "priority"). Empty/None means leave unset.
|
||||||
|
#[serde(default)]
|
||||||
|
pub service_tier: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ollama configuration for a project.
|
/// Ollama configuration for a project.
|
||||||
@@ -167,6 +205,7 @@ impl Project {
|
|||||||
ollama_config: None,
|
ollama_config: None,
|
||||||
openai_compatible_config: None,
|
openai_compatible_config: None,
|
||||||
allow_docker_access: false,
|
allow_docker_access: false,
|
||||||
|
sandbox_mode_enabled: false,
|
||||||
mission_control_enabled: false,
|
mission_control_enabled: false,
|
||||||
full_permissions: false,
|
full_permissions: false,
|
||||||
ssh_key_path: None,
|
ssh_key_path: None,
|
||||||
@@ -177,6 +216,7 @@ impl Project {
|
|||||||
port_mappings: Vec::new(),
|
port_mappings: Vec::new(),
|
||||||
claude_instructions: None,
|
claude_instructions: None,
|
||||||
enabled_mcp_servers: Vec::new(),
|
enabled_mcp_servers: Vec::new(),
|
||||||
|
claude_code_settings: None,
|
||||||
created_at: now.clone(),
|
created_at: now.clone(),
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,7 +159,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
display: none;
|
display: none;
|
||||||
padding: 4px;
|
padding: 4px 4px 16px 4px;
|
||||||
}
|
}
|
||||||
.terminal-container.active { display: block; }
|
.terminal-container.active { display: block; }
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use futures_util::{SinkExt, StreamExt};
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
use crate::commands::aws_commands;
|
||||||
use crate::models::{Backend, BedrockAuthMethod, Project, ProjectStatus};
|
use crate::models::{Backend, BedrockAuthMethod, Project, ProjectStatus};
|
||||||
|
|
||||||
use super::server::WebTerminalState;
|
use super::server::WebTerminalState;
|
||||||
@@ -212,12 +213,10 @@ fn build_terminal_cmd(project: &Project, settings_store: &crate::storage::settin
|
|||||||
return cmd;
|
return cmd;
|
||||||
}
|
}
|
||||||
|
|
||||||
let profile = project
|
let profile = aws_commands::resolve_profile_for_project(
|
||||||
.bedrock_config
|
project,
|
||||||
.as_ref()
|
settings_store.get().global_aws.aws_profile.as_deref(),
|
||||||
.and_then(|b| b.aws_profile.clone())
|
);
|
||||||
.or_else(|| settings_store.get().global_aws.aws_profile.clone())
|
|
||||||
.unwrap_or_else(|| "default".to_string());
|
|
||||||
|
|
||||||
let claude_cmd = if project.full_permissions {
|
let claude_cmd = if project.full_permissions {
|
||||||
"exec claude --dangerously-skip-permissions"
|
"exec claude --dangerously-skip-permissions"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-cli/schema.json",
|
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-cli/schema.json",
|
||||||
"productName": "Triple-C",
|
"productName": "Triple-C",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"identifier": "com.triple-c.desktop",
|
"identifier": "com.triple-c.desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useShallow } from "zustand/react/shallow";
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import Sidebar from "./components/layout/Sidebar";
|
import Sidebar from "./components/layout/Sidebar";
|
||||||
import TopBar from "./components/layout/TopBar";
|
import TopBar from "./components/layout/TopBar";
|
||||||
import StatusBar from "./components/layout/StatusBar";
|
import StatusBar from "./components/layout/StatusBar";
|
||||||
import TerminalView from "./components/terminal/TerminalView";
|
import TerminalView from "./components/terminal/TerminalView";
|
||||||
|
import DockerInstallDialog from "./components/DockerInstallDialog";
|
||||||
import { useDocker } from "./hooks/useDocker";
|
import { useDocker } from "./hooks/useDocker";
|
||||||
import { useSettings } from "./hooks/useSettings";
|
import { useSettings } from "./hooks/useSettings";
|
||||||
import { useProjects } from "./hooks/useProjects";
|
import { useProjects } from "./hooks/useProjects";
|
||||||
@@ -21,6 +22,7 @@ export default function App() {
|
|||||||
const { sessions, activeSessionId, setProjects } = useAppState(
|
const { sessions, activeSessionId, setProjects } = useAppState(
|
||||||
useShallow(s => ({ sessions: s.sessions, activeSessionId: s.activeSessionId, setProjects: s.setProjects }))
|
useShallow(s => ({ sessions: s.sessions, activeSessionId: s.activeSessionId, setProjects: s.setProjects }))
|
||||||
);
|
);
|
||||||
|
const [showInstallDialog, setShowInstallDialog] = useState(false);
|
||||||
|
|
||||||
// Initialize on mount
|
// Initialize on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -38,6 +40,7 @@ export default function App() {
|
|||||||
refresh();
|
refresh();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
setShowInstallDialog(true);
|
||||||
stopPolling = startDockerPolling();
|
stopPolling = startDockerPolling();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -80,6 +83,9 @@ export default function App() {
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<StatusBar />
|
<StatusBar />
|
||||||
|
{showInstallDialog && (
|
||||||
|
<DockerInstallDialog onClose={() => setShowInstallDialog(false)} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
211
app/src/components/DockerInstallDialog.tsx
Normal file
211
app/src/components/DockerInstallDialog.tsx
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||||
|
import { useInstallHelper } from "../hooks/useInstallHelper";
|
||||||
|
import { useDocker } from "../hooks/useDocker";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Phase = "idle" | "installing" | "done" | "error";
|
||||||
|
|
||||||
|
export default function DockerInstallDialog({ onClose }: Props) {
|
||||||
|
const { options, loadOptions, runInstall } = useInstallHelper();
|
||||||
|
const { checkDocker } = useDocker();
|
||||||
|
const [showManual, setShowManual] = useState(false);
|
||||||
|
const [phase, setPhase] = useState<Phase>("idle");
|
||||||
|
const [log, setLog] = useState<string[]>([]);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const overlayRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadOptions();
|
||||||
|
}, [loadOptions]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onKey = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape" && phase !== "installing") onClose();
|
||||||
|
};
|
||||||
|
document.addEventListener("keydown", onKey);
|
||||||
|
return () => document.removeEventListener("keydown", onKey);
|
||||||
|
}, [onClose, phase]);
|
||||||
|
|
||||||
|
const handleOverlayClick = useCallback(
|
||||||
|
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (e.target === overlayRef.current && phase !== "installing") onClose();
|
||||||
|
},
|
||||||
|
[onClose, phase],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleInstall = async () => {
|
||||||
|
setPhase("installing");
|
||||||
|
setLog([]);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
await runInstall((line) => setLog((prev) => [...prev, line]));
|
||||||
|
setPhase("done");
|
||||||
|
// Re-check Docker so the rest of the app can proceed without a reload.
|
||||||
|
await checkDocker();
|
||||||
|
} catch (e) {
|
||||||
|
setError(String(e));
|
||||||
|
setPhase("error");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenDocs = async () => {
|
||||||
|
if (!options) return;
|
||||||
|
try {
|
||||||
|
await openUrl(options.docs_url);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to open docs URL:", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRecheck = async () => {
|
||||||
|
const available = await checkDocker();
|
||||||
|
if (available) onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!options) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const installVerb = phase === "installing" ? "Installing…" : `Install ${options.product_name}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={overlayRef}
|
||||||
|
onClick={handleOverlayClick}
|
||||||
|
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
|
||||||
|
>
|
||||||
|
<div className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg p-6 w-[32rem] max-h-[85vh] overflow-y-auto shadow-xl">
|
||||||
|
<h2 className="text-lg font-semibold mb-1">Docker not detected</h2>
|
||||||
|
<p className="text-sm text-[var(--text-secondary)] mb-4">
|
||||||
|
Triple-C needs a Docker-compatible runtime to manage sandboxed project containers.
|
||||||
|
We can install <span className="text-[var(--text-primary)]">{options.product_name}</span>{" "}
|
||||||
|
for you, or you can follow the official instructions.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{phase === "idle" && (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{options.can_auto_install ? (
|
||||||
|
<button
|
||||||
|
onClick={handleInstall}
|
||||||
|
className="px-3 py-2 text-sm bg-[var(--accent)] text-white rounded hover:bg-[var(--accent-hover)] transition-colors"
|
||||||
|
>
|
||||||
|
{installVerb} ({options.auto_install_method})
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div className="text-xs text-[var(--text-secondary)] bg-[var(--bg-primary)] border border-[var(--border-color)] rounded p-2">
|
||||||
|
One-click install unavailable:{" "}
|
||||||
|
<span className="text-[var(--text-primary)]">
|
||||||
|
{options.auto_install_blocker ?? "required tooling missing."}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setShowManual((s) => !s)}
|
||||||
|
className="px-3 py-2 text-sm bg-[var(--bg-tertiary)] border border-[var(--border-color)] rounded hover:bg-[var(--border-color)] transition-colors"
|
||||||
|
>
|
||||||
|
{showManual ? "Hide manual instructions" : "Show manual instructions"}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleOpenDocs}
|
||||||
|
className="px-3 py-2 text-sm bg-[var(--bg-tertiary)] border border-[var(--border-color)] rounded hover:bg-[var(--border-color)] transition-colors"
|
||||||
|
>
|
||||||
|
Open official documentation ↗
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{phase === "installing" && (
|
||||||
|
<div className="text-xs text-[var(--text-secondary)]">
|
||||||
|
Installing… a system password prompt may appear. Do not close this window.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{phase === "done" && (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="text-sm text-[var(--success)]">Install finished.</div>
|
||||||
|
{options.post_install_notes.length > 0 && (
|
||||||
|
<ul className="text-xs text-[var(--text-secondary)] list-disc list-inside space-y-1">
|
||||||
|
{options.post_install_notes.map((note, i) => (
|
||||||
|
<li key={i}>{note}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
<div className="flex gap-2 mt-2">
|
||||||
|
<button
|
||||||
|
onClick={handleRecheck}
|
||||||
|
className="px-3 py-2 text-sm bg-[var(--accent)] text-white rounded hover:bg-[var(--accent-hover)] transition-colors"
|
||||||
|
>
|
||||||
|
Re-check Docker
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-3 py-2 text-sm bg-[var(--bg-tertiary)] border border-[var(--border-color)] rounded hover:bg-[var(--border-color)] transition-colors"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{phase === "error" && (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="text-sm text-[var(--error)]">Install failed.</div>
|
||||||
|
{error && <div className="text-xs font-mono text-[var(--error)]">{error}</div>}
|
||||||
|
<div className="flex gap-2 mt-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setPhase("idle")}
|
||||||
|
className="px-3 py-2 text-sm bg-[var(--bg-tertiary)] border border-[var(--border-color)] rounded hover:bg-[var(--border-color)] transition-colors"
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleOpenDocs}
|
||||||
|
className="px-3 py-2 text-sm bg-[var(--accent)] text-white rounded hover:bg-[var(--accent-hover)] transition-colors"
|
||||||
|
>
|
||||||
|
Open official docs ↗
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(showManual || phase === "error") && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<div className="text-xs font-medium mb-1.5 text-[var(--text-secondary)]">
|
||||||
|
Manual install steps
|
||||||
|
</div>
|
||||||
|
<ol className="text-xs text-[var(--text-secondary)] list-decimal list-inside space-y-1 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded p-2">
|
||||||
|
{options.manual_steps.map((step, i) => (
|
||||||
|
<li key={i}>{step}</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{log.length > 0 && (
|
||||||
|
<div className="mt-4 max-h-48 overflow-y-auto bg-[var(--bg-primary)] border border-[var(--border-color)] rounded p-2 text-xs font-mono text-[var(--text-secondary)]">
|
||||||
|
{log.map((line, i) => (
|
||||||
|
<div key={i}>{line}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{phase === "idle" && (
|
||||||
|
<div className="mt-4 flex justify-end">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-xs text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
||||||
|
>
|
||||||
|
Dismiss
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -23,7 +23,9 @@ export default function StatusBar() {
|
|||||||
{terminalHasSelection && (
|
{terminalHasSelection && (
|
||||||
<>
|
<>
|
||||||
<span className="mx-2">|</span>
|
<span className="mx-2">|</span>
|
||||||
<span className="text-[var(--accent)]">Ctrl+Shift+C to copy</span>
|
<span className="text-[var(--accent)]">
|
||||||
|
Ctrl+Shift+C: copy trimmed · Ctrl+Shift+Alt+C: copy raw
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
191
app/src/components/projects/ClaudeCodeSettingsModal.tsx
Normal file
191
app/src/components/projects/ClaudeCodeSettingsModal.tsx
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
import { useState, useEffect, useRef, useCallback } from "react";
|
||||||
|
import type { ClaudeCodeSettings } from "../../lib/types";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
settings: ClaudeCodeSettings | null;
|
||||||
|
disabled: boolean;
|
||||||
|
onSave: (settings: ClaudeCodeSettings | null) => Promise<void>;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULTS: ClaudeCodeSettings = {
|
||||||
|
tui_mode: null,
|
||||||
|
effort: null,
|
||||||
|
auto_scroll_disabled: false,
|
||||||
|
focus_mode: false,
|
||||||
|
show_thinking_summaries: false,
|
||||||
|
enable_session_recap: false,
|
||||||
|
env_scrub: false,
|
||||||
|
prompt_caching_1h: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
function isAllDefaults(s: ClaudeCodeSettings): boolean {
|
||||||
|
return (
|
||||||
|
s.tui_mode === null &&
|
||||||
|
s.effort === null &&
|
||||||
|
s.auto_scroll_disabled === false &&
|
||||||
|
s.focus_mode === false &&
|
||||||
|
s.show_thinking_summaries === false &&
|
||||||
|
s.enable_session_recap === false &&
|
||||||
|
s.env_scrub === false &&
|
||||||
|
s.prompt_caching_1h === false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ClaudeCodeSettingsModal({ settings, disabled, onSave, onClose }: Props) {
|
||||||
|
const [local, setLocal] = useState<ClaudeCodeSettings>(settings ?? { ...DEFAULTS });
|
||||||
|
const overlayRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") onClose();
|
||||||
|
};
|
||||||
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
const handleOverlayClick = useCallback(
|
||||||
|
(e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (e.target === overlayRef.current) onClose();
|
||||||
|
},
|
||||||
|
[onClose],
|
||||||
|
);
|
||||||
|
|
||||||
|
const update = async (patch: Partial<ClaudeCodeSettings>) => {
|
||||||
|
const next = { ...local, ...patch };
|
||||||
|
setLocal(next);
|
||||||
|
try {
|
||||||
|
await onSave(isAllDefaults(next) ? null : next);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to save Claude Code settings:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleButton = (label: string, description: string, value: boolean, onChange: (v: boolean) => void) => (
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="text-sm font-medium text-[var(--text-primary)]">{label}</div>
|
||||||
|
<div className="text-xs text-[var(--text-secondary)]">{description}</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => onChange(!value)}
|
||||||
|
disabled={disabled}
|
||||||
|
className={`px-2 py-0.5 text-xs rounded transition-colors disabled:opacity-50 shrink-0 ${
|
||||||
|
value
|
||||||
|
? "bg-[var(--success)] text-white"
|
||||||
|
: "bg-[var(--bg-primary)] border border-[var(--border-color)] text-[var(--text-secondary)]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{value ? "ON" : "OFF"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={overlayRef}
|
||||||
|
onClick={handleOverlayClick}
|
||||||
|
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
|
||||||
|
>
|
||||||
|
<div className="bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg p-6 w-[32rem] shadow-xl max-h-[80vh] overflow-y-auto">
|
||||||
|
<h2 className="text-lg font-semibold mb-4">Claude Code Settings</h2>
|
||||||
|
|
||||||
|
{disabled && (
|
||||||
|
<div className="px-2 py-1.5 mb-3 bg-[var(--warning)]/15 border border-[var(--warning)]/30 rounded text-xs text-[var(--warning)]">
|
||||||
|
Container must be stopped to change Claude Code settings.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-4 mb-6">
|
||||||
|
{/* TUI Mode */}
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="text-sm font-medium text-[var(--text-primary)]">TUI Mode</div>
|
||||||
|
<div className="text-xs text-[var(--text-secondary)]">Enables flicker-free alt-screen rendering</div>
|
||||||
|
</div>
|
||||||
|
<select
|
||||||
|
value={local.tui_mode ?? ""}
|
||||||
|
onChange={(e) => update({ tui_mode: e.target.value || null })}
|
||||||
|
disabled={disabled}
|
||||||
|
className="px-2 py-1 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-sm text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)] disabled:opacity-50 shrink-0"
|
||||||
|
>
|
||||||
|
<option value="">Default</option>
|
||||||
|
<option value="fullscreen">Fullscreen</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Effort Level */}
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="text-sm font-medium text-[var(--text-primary)]">Effort Level</div>
|
||||||
|
<div className="text-xs text-[var(--text-secondary)]">Controls how much reasoning Claude applies</div>
|
||||||
|
</div>
|
||||||
|
<select
|
||||||
|
value={local.effort ?? ""}
|
||||||
|
onChange={(e) => update({ effort: e.target.value || null })}
|
||||||
|
disabled={disabled}
|
||||||
|
className="px-2 py-1 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded text-sm text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)] disabled:opacity-50 shrink-0"
|
||||||
|
>
|
||||||
|
<option value="">Default</option>
|
||||||
|
<option value="low">Low</option>
|
||||||
|
<option value="medium">Medium</option>
|
||||||
|
<option value="high">High</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Boolean toggles */}
|
||||||
|
{toggleButton(
|
||||||
|
"Focus Mode",
|
||||||
|
"Collapses tool output to one-line summaries",
|
||||||
|
local.focus_mode,
|
||||||
|
(v) => update({ focus_mode: v }),
|
||||||
|
)}
|
||||||
|
|
||||||
|
{toggleButton(
|
||||||
|
"Thinking Summaries",
|
||||||
|
"Shows thinking process as summaries",
|
||||||
|
local.show_thinking_summaries,
|
||||||
|
(v) => update({ show_thinking_summaries: v }),
|
||||||
|
)}
|
||||||
|
|
||||||
|
{toggleButton(
|
||||||
|
"Session Recap",
|
||||||
|
"Provides context when returning to a session",
|
||||||
|
local.enable_session_recap,
|
||||||
|
(v) => update({ enable_session_recap: v }),
|
||||||
|
)}
|
||||||
|
|
||||||
|
{toggleButton(
|
||||||
|
"Auto-Scroll Disabled",
|
||||||
|
"Disables auto-scroll when in fullscreen TUI mode",
|
||||||
|
local.auto_scroll_disabled,
|
||||||
|
(v) => update({ auto_scroll_disabled: v }),
|
||||||
|
)}
|
||||||
|
|
||||||
|
{toggleButton(
|
||||||
|
"Env Scrub",
|
||||||
|
"Strips credentials from subprocess environments for security",
|
||||||
|
local.env_scrub,
|
||||||
|
(v) => update({ env_scrub: v }),
|
||||||
|
)}
|
||||||
|
|
||||||
|
{toggleButton(
|
||||||
|
"Prompt Caching (1h)",
|
||||||
|
"Enables 1-hour prompt cache TTL instead of 5 minutes",
|
||||||
|
local.prompt_caching_1h,
|
||||||
|
(v) => update({ prompt_caching_1h: v }),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-colors"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import { useAppState } from "../../store/appState";
|
|||||||
import EnvVarsModal from "./EnvVarsModal";
|
import EnvVarsModal from "./EnvVarsModal";
|
||||||
import PortMappingsModal from "./PortMappingsModal";
|
import PortMappingsModal from "./PortMappingsModal";
|
||||||
import ClaudeInstructionsModal from "./ClaudeInstructionsModal";
|
import ClaudeInstructionsModal from "./ClaudeInstructionsModal";
|
||||||
|
import ClaudeCodeSettingsModal from "./ClaudeCodeSettingsModal";
|
||||||
import ContainerProgressModal from "./ContainerProgressModal";
|
import ContainerProgressModal from "./ContainerProgressModal";
|
||||||
import FileManagerModal from "./FileManagerModal";
|
import FileManagerModal from "./FileManagerModal";
|
||||||
import ConfirmRemoveModal from "./ConfirmRemoveModal";
|
import ConfirmRemoveModal from "./ConfirmRemoveModal";
|
||||||
@@ -30,6 +31,7 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
const [showEnvVarsModal, setShowEnvVarsModal] = useState(false);
|
const [showEnvVarsModal, setShowEnvVarsModal] = useState(false);
|
||||||
const [showPortMappingsModal, setShowPortMappingsModal] = useState(false);
|
const [showPortMappingsModal, setShowPortMappingsModal] = useState(false);
|
||||||
const [showClaudeInstructionsModal, setShowClaudeInstructionsModal] = useState(false);
|
const [showClaudeInstructionsModal, setShowClaudeInstructionsModal] = useState(false);
|
||||||
|
const [showClaudeCodeSettingsModal, setShowClaudeCodeSettingsModal] = useState(false);
|
||||||
const [showFileManager, setShowFileManager] = useState(false);
|
const [showFileManager, setShowFileManager] = useState(false);
|
||||||
const [progressMsg, setProgressMsg] = useState<string | null>(null);
|
const [progressMsg, setProgressMsg] = useState<string | null>(null);
|
||||||
const [activeOperation, setActiveOperation] = useState<"starting" | "stopping" | "resetting" | null>(null);
|
const [activeOperation, setActiveOperation] = useState<"starting" | "stopping" | "resetting" | null>(null);
|
||||||
@@ -58,6 +60,7 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
const [bedrockProfile, setBedrockProfile] = useState(project.bedrock_config?.aws_profile ?? "");
|
const [bedrockProfile, setBedrockProfile] = useState(project.bedrock_config?.aws_profile ?? "");
|
||||||
const [bedrockBearerToken, setBedrockBearerToken] = useState(project.bedrock_config?.aws_bearer_token ?? "");
|
const [bedrockBearerToken, setBedrockBearerToken] = useState(project.bedrock_config?.aws_bearer_token ?? "");
|
||||||
const [bedrockModelId, setBedrockModelId] = useState(project.bedrock_config?.model_id ?? "");
|
const [bedrockModelId, setBedrockModelId] = useState(project.bedrock_config?.model_id ?? "");
|
||||||
|
const [bedrockServiceTier, setBedrockServiceTier] = useState(project.bedrock_config?.service_tier ?? "");
|
||||||
|
|
||||||
// Ollama local state
|
// Ollama local state
|
||||||
const [ollamaBaseUrl, setOllamaBaseUrl] = useState(project.ollama_config?.base_url ?? "http://host.docker.internal:11434");
|
const [ollamaBaseUrl, setOllamaBaseUrl] = useState(project.ollama_config?.base_url ?? "http://host.docker.internal:11434");
|
||||||
@@ -86,6 +89,7 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
setBedrockProfile(project.bedrock_config?.aws_profile ?? "");
|
setBedrockProfile(project.bedrock_config?.aws_profile ?? "");
|
||||||
setBedrockBearerToken(project.bedrock_config?.aws_bearer_token ?? "");
|
setBedrockBearerToken(project.bedrock_config?.aws_bearer_token ?? "");
|
||||||
setBedrockModelId(project.bedrock_config?.model_id ?? "");
|
setBedrockModelId(project.bedrock_config?.model_id ?? "");
|
||||||
|
setBedrockServiceTier(project.bedrock_config?.service_tier ?? "");
|
||||||
setOllamaBaseUrl(project.ollama_config?.base_url ?? "http://host.docker.internal:11434");
|
setOllamaBaseUrl(project.ollama_config?.base_url ?? "http://host.docker.internal:11434");
|
||||||
setOllamaModelId(project.ollama_config?.model_id ?? "");
|
setOllamaModelId(project.ollama_config?.model_id ?? "");
|
||||||
setOpenaiCompatibleBaseUrl(project.openai_compatible_config?.base_url ?? "http://host.docker.internal:4000");
|
setOpenaiCompatibleBaseUrl(project.openai_compatible_config?.base_url ?? "http://host.docker.internal:4000");
|
||||||
@@ -190,6 +194,7 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
aws_bearer_token: null,
|
aws_bearer_token: null,
|
||||||
model_id: null,
|
model_id: null,
|
||||||
disable_prompt_caching: false,
|
disable_prompt_caching: false,
|
||||||
|
service_tier: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultOllamaConfig: OllamaConfig = {
|
const defaultOllamaConfig: OllamaConfig = {
|
||||||
@@ -337,6 +342,16 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleBedrockServiceTierBlur = async () => {
|
||||||
|
try {
|
||||||
|
const current = project.bedrock_config ?? defaultBedrockConfig;
|
||||||
|
const trimmed = bedrockServiceTier.trim();
|
||||||
|
await update({ ...project, bedrock_config: { ...current, service_tier: trimmed || null } });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to update Bedrock service tier:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleOllamaBaseUrlBlur = async () => {
|
const handleOllamaBaseUrlBlur = async () => {
|
||||||
try {
|
try {
|
||||||
const current = project.ollama_config ?? defaultOllamaConfig;
|
const current = project.ollama_config ?? defaultOllamaConfig;
|
||||||
@@ -690,6 +705,28 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Sandbox mode toggle */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<label className="text-xs text-[var(--text-secondary)]">Sandbox mode<Tooltip text="Enables Claude Code's bash sandbox (bubblewrap-based filesystem and network isolation). Triple-C is the source of truth for the on/off state — toggling this overrides any manual /sandbox configuration in the container's settings.json on next start. Uses enableWeakerNestedSandbox since the container runs without privileged user namespaces." /></label>
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
await update({ ...project, sandbox_mode_enabled: !project.sandbox_mode_enabled });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to update sandbox mode setting:", err);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!isStopped}
|
||||||
|
className={`px-2 py-0.5 text-xs rounded transition-colors disabled:opacity-50 ${
|
||||||
|
project.sandbox_mode_enabled
|
||||||
|
? "bg-[var(--success)] text-white"
|
||||||
|
: "bg-[var(--bg-primary)] border border-[var(--border-color)] text-[var(--text-secondary)]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{project.sandbox_mode_enabled ? "ON" : "OFF"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Mission Control toggle */}
|
{/* Mission Control toggle */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<label className="text-xs text-[var(--text-secondary)]">Mission Control<Tooltip text="Enables a web dashboard for monitoring and managing Claude sessions remotely." /></label>
|
<label className="text-xs text-[var(--text-secondary)]">Mission Control<Tooltip text="Enables a web dashboard for monitoring and managing Claude sessions remotely." /></label>
|
||||||
@@ -777,6 +814,19 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Claude Code Settings */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-xs text-[var(--text-secondary)]">
|
||||||
|
Claude Code Settings{project.claude_code_settings ? " (set)" : ""}<Tooltip text="Configure Claude Code CLI behavior: TUI mode, effort level, focus mode, prompt caching, and more. These override global defaults for this project." />
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowClaudeCodeSettingsModal(true)}
|
||||||
|
className="text-xs px-2 py-0.5 text-[var(--accent)] hover:text-[var(--accent-hover)] hover:bg-[var(--bg-primary)] rounded transition-colors"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* MCP Servers */}
|
{/* MCP Servers */}
|
||||||
{mcpServers.length > 0 && (
|
{mcpServers.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
@@ -938,6 +988,19 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
className={inputCls}
|
className={inputCls}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Service tier */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Service Tier (optional)<Tooltip text="Sets ANTHROPIC_BEDROCK_SERVICE_TIER. Valid values are determined by AWS Bedrock (e.g. 'priority'). Leave blank for the account default." /></label>
|
||||||
|
<input
|
||||||
|
value={bedrockServiceTier}
|
||||||
|
onChange={(e) => setBedrockServiceTier(e.target.value)}
|
||||||
|
onBlur={handleBedrockServiceTierBlur}
|
||||||
|
placeholder="(default)"
|
||||||
|
disabled={!isStopped}
|
||||||
|
className={inputCls}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
@@ -1079,6 +1142,17 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showClaudeCodeSettingsModal && (
|
||||||
|
<ClaudeCodeSettingsModal
|
||||||
|
settings={project.claude_code_settings}
|
||||||
|
disabled={!isStopped}
|
||||||
|
onSave={async (ccSettings) => {
|
||||||
|
await update({ ...project, claude_code_settings: ccSettings });
|
||||||
|
}}
|
||||||
|
onClose={() => setShowClaudeCodeSettingsModal(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{showFileManager && (
|
{showFileManager && (
|
||||||
<FileManagerModal
|
<FileManagerModal
|
||||||
projectId={project.id}
|
projectId={project.id}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import AwsSettings from "./AwsSettings";
|
|||||||
import { useSettings } from "../../hooks/useSettings";
|
import { useSettings } from "../../hooks/useSettings";
|
||||||
import { useUpdates } from "../../hooks/useUpdates";
|
import { useUpdates } from "../../hooks/useUpdates";
|
||||||
import ClaudeInstructionsModal from "../projects/ClaudeInstructionsModal";
|
import ClaudeInstructionsModal from "../projects/ClaudeInstructionsModal";
|
||||||
|
import ClaudeCodeSettingsModal from "../projects/ClaudeCodeSettingsModal";
|
||||||
import EnvVarsModal from "../projects/EnvVarsModal";
|
import EnvVarsModal from "../projects/EnvVarsModal";
|
||||||
import { detectHostTimezone } from "../../lib/tauri-commands";
|
import { detectHostTimezone } from "../../lib/tauri-commands";
|
||||||
import type { EnvVar } from "../../lib/types";
|
import type { EnvVar } from "../../lib/types";
|
||||||
@@ -18,15 +19,22 @@ export default function SettingsPanel() {
|
|||||||
const [globalEnvVars, setGlobalEnvVars] = useState<EnvVar[]>(appSettings?.global_custom_env_vars ?? []);
|
const [globalEnvVars, setGlobalEnvVars] = useState<EnvVar[]>(appSettings?.global_custom_env_vars ?? []);
|
||||||
const [checkingUpdates, setCheckingUpdates] = useState(false);
|
const [checkingUpdates, setCheckingUpdates] = useState(false);
|
||||||
const [timezone, setTimezone] = useState(appSettings?.timezone ?? "");
|
const [timezone, setTimezone] = useState(appSettings?.timezone ?? "");
|
||||||
|
const [sshKeyPath, setSshKeyPath] = useState(appSettings?.default_ssh_key_path ?? "");
|
||||||
|
const [gitName, setGitName] = useState(appSettings?.default_git_user_name ?? "");
|
||||||
|
const [gitEmail, setGitEmail] = useState(appSettings?.default_git_user_email ?? "");
|
||||||
const [showInstructionsModal, setShowInstructionsModal] = useState(false);
|
const [showInstructionsModal, setShowInstructionsModal] = useState(false);
|
||||||
const [showEnvVarsModal, setShowEnvVarsModal] = useState(false);
|
const [showEnvVarsModal, setShowEnvVarsModal] = useState(false);
|
||||||
|
const [showClaudeCodeSettingsModal, setShowClaudeCodeSettingsModal] = useState(false);
|
||||||
|
|
||||||
// Sync local state when appSettings change
|
// Sync local state when appSettings change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setGlobalInstructions(appSettings?.global_claude_instructions ?? "");
|
setGlobalInstructions(appSettings?.global_claude_instructions ?? "");
|
||||||
setGlobalEnvVars(appSettings?.global_custom_env_vars ?? []);
|
setGlobalEnvVars(appSettings?.global_custom_env_vars ?? []);
|
||||||
setTimezone(appSettings?.timezone ?? "");
|
setTimezone(appSettings?.timezone ?? "");
|
||||||
}, [appSettings?.global_claude_instructions, appSettings?.global_custom_env_vars, appSettings?.timezone]);
|
setSshKeyPath(appSettings?.default_ssh_key_path ?? "");
|
||||||
|
setGitName(appSettings?.default_git_user_name ?? "");
|
||||||
|
setGitEmail(appSettings?.default_git_user_email ?? "");
|
||||||
|
}, [appSettings?.global_claude_instructions, appSettings?.global_custom_env_vars, appSettings?.timezone, appSettings?.default_ssh_key_path, appSettings?.default_git_user_name, appSettings?.default_git_user_email]);
|
||||||
|
|
||||||
// Auto-detect timezone on first load if not yet set
|
// Auto-detect timezone on first load if not yet set
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -60,6 +68,60 @@ export default function SettingsPanel() {
|
|||||||
<DockerSettings />
|
<DockerSettings />
|
||||||
<AwsSettings />
|
<AwsSettings />
|
||||||
|
|
||||||
|
{/* Default SSH Key Directory */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1">Default SSH Key Directory<Tooltip text="Global default SSH key directory. Mounted into containers that don't have a per-project SSH path set." /></label>
|
||||||
|
<p className="text-xs text-[var(--text-secondary)] mb-1.5">
|
||||||
|
Mounted into all containers unless overridden by a per-project setting.
|
||||||
|
</p>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={sshKeyPath}
|
||||||
|
onChange={(e) => setSshKeyPath(e.target.value)}
|
||||||
|
onBlur={async () => {
|
||||||
|
if (appSettings) {
|
||||||
|
await saveSettings({ ...appSettings, default_ssh_key_path: sshKeyPath || null });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder="~/.ssh"
|
||||||
|
className="w-full px-2 py-1 text-sm bg-[var(--bg-primary)] border border-[var(--border-color)] rounded focus:outline-none focus:border-[var(--accent)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Default Git Name */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1">Default Git Name<Tooltip text="Sets git user.name inside containers. Per-project Git Name takes precedence." /></label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={gitName}
|
||||||
|
onChange={(e) => setGitName(e.target.value)}
|
||||||
|
onBlur={async () => {
|
||||||
|
if (appSettings) {
|
||||||
|
await saveSettings({ ...appSettings, default_git_user_name: gitName || null });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder="Your Name"
|
||||||
|
className="w-full px-2 py-1 text-sm bg-[var(--bg-primary)] border border-[var(--border-color)] rounded focus:outline-none focus:border-[var(--accent)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Default Git Email */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1">Default Git Email<Tooltip text="Sets git user.email inside containers. Per-project Git Email takes precedence." /></label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={gitEmail}
|
||||||
|
onChange={(e) => setGitEmail(e.target.value)}
|
||||||
|
onBlur={async () => {
|
||||||
|
if (appSettings) {
|
||||||
|
await saveSettings({ ...appSettings, default_git_user_email: gitEmail || null });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder="you@example.com"
|
||||||
|
className="w-full px-2 py-1 text-sm bg-[var(--bg-primary)] border border-[var(--border-color)] rounded focus:outline-none focus:border-[var(--accent)]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Container Timezone */}
|
{/* Container Timezone */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1">Container Timezone<Tooltip text="Sets the timezone inside containers. Affects scheduled task timing and log timestamps." /></label>
|
<label className="block text-sm font-medium mb-1">Container Timezone<Tooltip text="Sets the timezone inside containers. Affects scheduled task timing and log timestamps." /></label>
|
||||||
@@ -118,6 +180,25 @@ export default function SettingsPanel() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Global Claude Code Settings */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1">Claude Code Settings<Tooltip text="Global defaults for Claude Code CLI behavior (TUI mode, effort, focus mode, caching, etc.). Per-project settings override these." /></label>
|
||||||
|
<p className="text-xs text-[var(--text-secondary)] mb-1.5">
|
||||||
|
Default Claude Code CLI settings applied to all projects. Per-project settings take precedence.
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs text-[var(--text-secondary)]">
|
||||||
|
{appSettings?.global_claude_code_settings ? "Configured" : "Using defaults"}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowClaudeCodeSettingsModal(true)}
|
||||||
|
className="text-xs px-2 py-0.5 text-[var(--accent)] hover:text-[var(--accent-hover)] hover:bg-[var(--bg-primary)] rounded transition-colors"
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Web Terminal */}
|
{/* Web Terminal */}
|
||||||
<WebTerminalSettings />
|
<WebTerminalSettings />
|
||||||
|
|
||||||
@@ -189,6 +270,19 @@ export default function SettingsPanel() {
|
|||||||
onClose={() => setShowEnvVarsModal(false)}
|
onClose={() => setShowEnvVarsModal(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showClaudeCodeSettingsModal && (
|
||||||
|
<ClaudeCodeSettingsModal
|
||||||
|
settings={appSettings?.global_claude_code_settings ?? null}
|
||||||
|
disabled={false}
|
||||||
|
onSave={async (ccSettings) => {
|
||||||
|
if (appSettings) {
|
||||||
|
await saveSettings({ ...appSettings, global_claude_code_settings: ccSettings });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onClose={() => setShowClaudeCodeSettingsModal(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export default function SttButton({ state, error, onToggle, onCancel }: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute bottom-1 left-1 z-50 flex items-center gap-2">
|
<div className="absolute bottom-2 left-2 z-50 flex items-center gap-2">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<button
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
|||||||
58
app/src/components/terminal/TerminalContextMenu.tsx
Normal file
58
app/src/components/terminal/TerminalContextMenu.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
onCopyTrimmed: () => void;
|
||||||
|
onCopyRaw: () => void;
|
||||||
|
onDismiss: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TerminalContextMenu({ x, y, onCopyTrimmed, onCopyRaw, onDismiss }: Props) {
|
||||||
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleOutsideClick = (e: MouseEvent) => {
|
||||||
|
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
||||||
|
onDismiss();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleKey = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") onDismiss();
|
||||||
|
};
|
||||||
|
document.addEventListener("mousedown", handleOutsideClick, true);
|
||||||
|
document.addEventListener("keydown", handleKey);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", handleOutsideClick, true);
|
||||||
|
document.removeEventListener("keydown", handleKey);
|
||||||
|
};
|
||||||
|
}, [onDismiss]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={menuRef}
|
||||||
|
className="fixed z-[60] min-w-[160px] py-1 rounded-md border border-[#30363d] bg-[#1f2937] shadow-lg text-xs text-[#e6edf3]"
|
||||||
|
style={{ left: x, top: y }}
|
||||||
|
onContextMenu={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="w-full text-left px-3 py-1.5 hover:bg-[#2d3748] cursor-pointer"
|
||||||
|
onClick={onCopyTrimmed}
|
||||||
|
>
|
||||||
|
Copy trimmed
|
||||||
|
<kbd className="float-right ml-3 px-1 py-0.5 text-[10px] bg-[#0d1117] border border-[#30363d] rounded font-mono">
|
||||||
|
Ctrl+Shift+C
|
||||||
|
</kbd>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="w-full text-left px-3 py-1.5 hover:bg-[#2d3748] cursor-pointer"
|
||||||
|
onClick={onCopyRaw}
|
||||||
|
>
|
||||||
|
Copy raw
|
||||||
|
<kbd className="float-right ml-3 px-1 py-0.5 text-[10px] bg-[#0d1117] border border-[#30363d] rounded font-mono">
|
||||||
|
Ctrl+Shift+Alt+C
|
||||||
|
</kbd>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -23,8 +23,8 @@ export default function TerminalTabs() {
|
|||||||
: "text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
: "text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="truncate max-w-[120px]">
|
<span className="truncate max-w-[140px]">
|
||||||
{session.projectName}{session.sessionType === "bash" ? " (bash)" : ""}
|
{session.sessionName ?? session.projectName}{session.sessionType === "bash" ? " (bash)" : ""}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import SttButton from "./SttButton";
|
|||||||
import { awsSsoRefresh } from "../../lib/tauri-commands";
|
import { awsSsoRefresh } from "../../lib/tauri-commands";
|
||||||
import { UrlDetector } from "../../lib/urlDetector";
|
import { UrlDetector } from "../../lib/urlDetector";
|
||||||
import UrlToast from "./UrlToast";
|
import UrlToast from "./UrlToast";
|
||||||
|
import { trimSelection } from "./trimSelection";
|
||||||
|
import TerminalContextMenu from "./TerminalContextMenu";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
@@ -42,6 +44,7 @@ export default function TerminalView({ sessionId, active }: Props) {
|
|||||||
const [imagePasteMsg, setImagePasteMsg] = useState<string | null>(null);
|
const [imagePasteMsg, setImagePasteMsg] = useState<string | null>(null);
|
||||||
const [isAtBottom, setIsAtBottom] = useState(true);
|
const [isAtBottom, setIsAtBottom] = useState(true);
|
||||||
const [isAutoFollow, setIsAutoFollow] = useState(true);
|
const [isAutoFollow, setIsAutoFollow] = useState(true);
|
||||||
|
const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null);
|
||||||
const isAtBottomRef = useRef(true);
|
const isAtBottomRef = useRef(true);
|
||||||
// Tracks user intent to follow output — only set to false by explicit user
|
// Tracks user intent to follow output — only set to false by explicit user
|
||||||
// actions (mouse wheel up), not by xterm scroll events during writes.
|
// actions (mouse wheel up), not by xterm scroll events during writes.
|
||||||
@@ -93,14 +96,16 @@ export default function TerminalView({ sessionId, active }: Props) {
|
|||||||
|
|
||||||
term.open(containerRef.current);
|
term.open(containerRef.current);
|
||||||
|
|
||||||
// Ctrl+Shift+C copies selected terminal text to clipboard.
|
// Ctrl+Shift+C copies the selection with whitespace trimmed (UI padding
|
||||||
// This prevents the keystroke from reaching the container (where
|
// stripped, internal indentation preserved). Ctrl+Shift+Alt+C copies raw.
|
||||||
// Ctrl+C would send SIGINT and cancel running work).
|
// Both prevent the keystroke from reaching the container (where Ctrl+C
|
||||||
|
// would send SIGINT and cancel running work).
|
||||||
term.attachCustomKeyEventHandler((event) => {
|
term.attachCustomKeyEventHandler((event) => {
|
||||||
if (event.type === "keydown" && event.ctrlKey && event.shiftKey && event.key === "C") {
|
if (event.type === "keydown" && event.ctrlKey && event.shiftKey && event.key === "C") {
|
||||||
const sel = term.getSelection();
|
const sel = term.getSelection();
|
||||||
if (sel) {
|
if (sel) {
|
||||||
navigator.clipboard.writeText(sel).catch((e) =>
|
const out = event.altKey ? sel : trimSelection(sel);
|
||||||
|
navigator.clipboard.writeText(out).catch((e) =>
|
||||||
console.error("Ctrl+Shift+C clipboard write failed:", e),
|
console.error("Ctrl+Shift+C clipboard write failed:", e),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -388,6 +393,23 @@ export default function TerminalView({ sessionId, active }: Props) {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const writeSelection = useCallback((mode: "trimmed" | "raw") => {
|
||||||
|
const term = termRef.current;
|
||||||
|
if (!term) return;
|
||||||
|
const sel = term.getSelection();
|
||||||
|
if (!sel) return;
|
||||||
|
const out = mode === "raw" ? sel : trimSelection(sel);
|
||||||
|
navigator.clipboard.writeText(out).catch((e) =>
|
||||||
|
console.error("Context menu clipboard write failed:", e),
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleContextMenu = useCallback((e: React.MouseEvent) => {
|
||||||
|
if (!termRef.current?.hasSelection()) return; // let default menu happen
|
||||||
|
e.preventDefault();
|
||||||
|
setContextMenu({ x: e.clientX, y: e.clientY });
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleToggleAutoFollow = useCallback(() => {
|
const handleToggleAutoFollow = useCallback(() => {
|
||||||
const next = !autoFollowRef.current;
|
const next = !autoFollowRef.current;
|
||||||
autoFollowRef.current = next;
|
autoFollowRef.current = next;
|
||||||
@@ -449,8 +471,24 @@ export default function TerminalView({ sessionId, active }: Props) {
|
|||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className="w-full h-full"
|
className="w-full h-full"
|
||||||
style={{ padding: "8px" }}
|
style={{ padding: "8px 12px 48px 16px" }}
|
||||||
|
onContextMenu={handleContextMenu}
|
||||||
/>
|
/>
|
||||||
|
{contextMenu && (
|
||||||
|
<TerminalContextMenu
|
||||||
|
x={contextMenu.x}
|
||||||
|
y={contextMenu.y}
|
||||||
|
onCopyTrimmed={() => {
|
||||||
|
writeSelection("trimmed");
|
||||||
|
setContextMenu(null);
|
||||||
|
}}
|
||||||
|
onCopyRaw={() => {
|
||||||
|
writeSelection("raw");
|
||||||
|
setContextMenu(null);
|
||||||
|
}}
|
||||||
|
onDismiss={() => setContextMenu(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
65
app/src/components/terminal/trimSelection.test.ts
Normal file
65
app/src/components/terminal/trimSelection.test.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { trimSelection } from "./trimSelection";
|
||||||
|
|
||||||
|
describe("trimSelection", () => {
|
||||||
|
it("returns empty string unchanged", () => {
|
||||||
|
expect(trimSelection("")).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("trims leading and trailing whitespace on a single line", () => {
|
||||||
|
expect(trimSelection(" hello ")).toBe("hello");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("dedents common leading whitespace while preserving inner indent", () => {
|
||||||
|
const input = " def foo():\n return 1\n";
|
||||||
|
expect(trimSelection(input)).toBe("def foo():\n return 1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips leading and trailing blank lines", () => {
|
||||||
|
const input = "\n\n hello\n\n";
|
||||||
|
expect(trimSelection(input)).toBe("hello");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves interior blank lines", () => {
|
||||||
|
const input = " line1\n\n line2";
|
||||||
|
expect(trimSelection(input)).toBe("line1\n\nline2");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("is idempotent on already-clean text", () => {
|
||||||
|
const clean = "def foo():\n return 1";
|
||||||
|
expect(trimSelection(clean)).toBe(clean);
|
||||||
|
expect(trimSelection(trimSelection(clean))).toBe(clean);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ignores blank lines when computing the common indent", () => {
|
||||||
|
// The blank line has 0 leading whitespace but shouldn't force minIndent to 0.
|
||||||
|
const input = " a\n\n b";
|
||||||
|
expect(trimSelection(input)).toBe("a\n\nb");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips trailing whitespace per line", () => {
|
||||||
|
const input = "alpha \nbeta\t\t\ngamma";
|
||||||
|
expect(trimSelection(input)).toBe("alpha\nbeta\ngamma");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles mixed-width padding (pads to min)", () => {
|
||||||
|
const input = " one\n two\n three";
|
||||||
|
// minIndent = 2
|
||||||
|
expect(trimSelection(input)).toBe(" one\ntwo\n three");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles tabs as leading whitespace", () => {
|
||||||
|
const input = "\tfoo\n\t\tbar";
|
||||||
|
// minIndent = 1 tab
|
||||||
|
expect(trimSelection(input)).toBe("foo\n\tbar");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns empty when input is only whitespace", () => {
|
||||||
|
expect(trimSelection(" \n \n")).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leaves a zero-indent line alone (no false dedent)", () => {
|
||||||
|
const input = "no-indent\n indented";
|
||||||
|
expect(trimSelection(input)).toBe("no-indent\n indented");
|
||||||
|
});
|
||||||
|
});
|
||||||
42
app/src/components/terminal/trimSelection.ts
Normal file
42
app/src/components/terminal/trimSelection.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Cleans up terminal selections for pasting into other tools.
|
||||||
|
*
|
||||||
|
* Terminal UI padding (left margin from the xterm container, alignment spaces
|
||||||
|
* at end of line) ends up in the copied text. This helper removes that cruft
|
||||||
|
* while preserving the *relative* indentation of the content — so code blocks
|
||||||
|
* keep their shape but lose the wrapper padding.
|
||||||
|
*
|
||||||
|
* Steps:
|
||||||
|
* 1. Dedent — strip the common leading whitespace count from every line.
|
||||||
|
* 2. trimEnd — drop trailing whitespace per line.
|
||||||
|
* 3. Drop fully-blank leading and trailing lines.
|
||||||
|
*
|
||||||
|
* Internal newlines and relative indentation are preserved. Pure function.
|
||||||
|
*/
|
||||||
|
export function trimSelection(text: string): string {
|
||||||
|
if (!text) return text;
|
||||||
|
|
||||||
|
const lines = text.split("\n");
|
||||||
|
|
||||||
|
let minIndent = Infinity;
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.trim() === "") continue;
|
||||||
|
const match = line.match(/^[ \t]*/);
|
||||||
|
const indent = match ? match[0].length : 0;
|
||||||
|
if (indent < minIndent) minIndent = indent;
|
||||||
|
if (minIndent === 0) break;
|
||||||
|
}
|
||||||
|
if (!Number.isFinite(minIndent)) minIndent = 0;
|
||||||
|
|
||||||
|
const processed = lines.map((line) => {
|
||||||
|
const afterDedent = line.length >= minIndent ? line.slice(minIndent) : "";
|
||||||
|
return afterDedent.trimEnd();
|
||||||
|
});
|
||||||
|
|
||||||
|
let start = 0;
|
||||||
|
let end = processed.length;
|
||||||
|
while (start < end && processed[start] === "") start++;
|
||||||
|
while (end > start && processed[end - 1] === "") end--;
|
||||||
|
|
||||||
|
return processed.slice(start, end).join("\n");
|
||||||
|
}
|
||||||
35
app/src/hooks/useInstallHelper.ts
Normal file
35
app/src/hooks/useInstallHelper.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import { listen } from "@tauri-apps/api/event";
|
||||||
|
import * as commands from "../lib/tauri-commands";
|
||||||
|
import type { InstallOptions } from "../lib/types";
|
||||||
|
|
||||||
|
export function useInstallHelper() {
|
||||||
|
const [options, setOptions] = useState<InstallOptions | null>(null);
|
||||||
|
|
||||||
|
const loadOptions = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const opts = await commands.detectInstallOptions();
|
||||||
|
setOptions(opts);
|
||||||
|
return opts;
|
||||||
|
} catch {
|
||||||
|
setOptions(null);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const runInstall = useCallback(
|
||||||
|
async (onProgress?: (line: string) => void) => {
|
||||||
|
const unlisten = onProgress
|
||||||
|
? await listen<string>("docker-install-progress", (e) => onProgress(e.payload))
|
||||||
|
: null;
|
||||||
|
try {
|
||||||
|
await commands.runDockerInstall();
|
||||||
|
} finally {
|
||||||
|
unlisten?.();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { options, loadOptions, runInstall };
|
||||||
|
}
|
||||||
@@ -17,10 +17,10 @@ export function useTerminal() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const open = useCallback(
|
const open = useCallback(
|
||||||
async (projectId: string, projectName: string, sessionType: "claude" | "bash" = "claude") => {
|
async (projectId: string, projectName: string, sessionType: "claude" | "bash" = "claude", sessionName?: string) => {
|
||||||
const sessionId = crypto.randomUUID();
|
const sessionId = crypto.randomUUID();
|
||||||
await commands.openTerminalSession(projectId, sessionId, sessionType);
|
await commands.openTerminalSession(projectId, sessionId, sessionType, sessionName);
|
||||||
addSession({ id: sessionId, projectId, projectName, sessionType });
|
addSession({ id: sessionId, projectId, projectName, sessionType, sessionName: sessionName ?? null });
|
||||||
return sessionId;
|
return sessionId;
|
||||||
},
|
},
|
||||||
[addSession],
|
[addSession],
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import type { Project, ProjectPath, ContainerInfo, SiblingContainer, AppSettings, UpdateInfo, ImageUpdateInfo, McpServer, FileEntry, WebTerminalInfo, SttStatus } from "./types";
|
import type { Project, ProjectPath, ContainerInfo, SiblingContainer, AppSettings, UpdateInfo, ImageUpdateInfo, McpServer, FileEntry, WebTerminalInfo, SttStatus, InstallOptions } from "./types";
|
||||||
|
|
||||||
// Docker
|
// Docker
|
||||||
export const checkDocker = () => invoke<boolean>("check_docker");
|
export const checkDocker = () => invoke<boolean>("check_docker");
|
||||||
@@ -45,8 +45,8 @@ export const awsSsoRefresh = (projectId: string) =>
|
|||||||
invoke<void>("aws_sso_refresh", { projectId });
|
invoke<void>("aws_sso_refresh", { projectId });
|
||||||
|
|
||||||
// Terminal
|
// Terminal
|
||||||
export const openTerminalSession = (projectId: string, sessionId: string, sessionType?: string) =>
|
export const openTerminalSession = (projectId: string, sessionId: string, sessionType?: string, sessionName?: string) =>
|
||||||
invoke<void>("open_terminal_session", { projectId, sessionId, sessionType });
|
invoke<void>("open_terminal_session", { projectId, sessionId, sessionType, sessionName });
|
||||||
export const terminalInput = (sessionId: string, data: number[]) =>
|
export const terminalInput = (sessionId: string, data: number[]) =>
|
||||||
invoke<void>("terminal_input", { sessionId, data });
|
invoke<void>("terminal_input", { sessionId, data });
|
||||||
export const terminalResize = (sessionId: string, cols: number, rows: number) =>
|
export const terminalResize = (sessionId: string, cols: number, rows: number) =>
|
||||||
@@ -107,3 +107,8 @@ export const buildSttImage = () => invoke<void>("build_stt_image");
|
|||||||
export const pullSttImage = () => invoke<void>("pull_stt_image");
|
export const pullSttImage = () => invoke<void>("pull_stt_image");
|
||||||
export const transcribeAudio = (audioData: number[]) =>
|
export const transcribeAudio = (audioData: number[]) =>
|
||||||
invoke<string>("transcribe_audio", { audioData });
|
invoke<string>("transcribe_audio", { audioData });
|
||||||
|
|
||||||
|
// Docker install helper
|
||||||
|
export const detectInstallOptions = () =>
|
||||||
|
invoke<InstallOptions>("detect_install_options");
|
||||||
|
export const runDockerInstall = () => invoke<void>("run_docker_install");
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export interface Project {
|
|||||||
ollama_config: OllamaConfig | null;
|
ollama_config: OllamaConfig | null;
|
||||||
openai_compatible_config: OpenAiCompatibleConfig | null;
|
openai_compatible_config: OpenAiCompatibleConfig | null;
|
||||||
allow_docker_access: boolean;
|
allow_docker_access: boolean;
|
||||||
|
sandbox_mode_enabled: boolean;
|
||||||
mission_control_enabled: boolean;
|
mission_control_enabled: boolean;
|
||||||
full_permissions: boolean;
|
full_permissions: boolean;
|
||||||
ssh_key_path: string | null;
|
ssh_key_path: string | null;
|
||||||
@@ -35,6 +36,7 @@ export interface Project {
|
|||||||
port_mappings: PortMapping[];
|
port_mappings: PortMapping[];
|
||||||
claude_instructions: string | null;
|
claude_instructions: string | null;
|
||||||
enabled_mcp_servers: string[];
|
enabled_mcp_servers: string[];
|
||||||
|
claude_code_settings: ClaudeCodeSettings | null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
@@ -60,6 +62,7 @@ export interface BedrockConfig {
|
|||||||
aws_bearer_token: string | null;
|
aws_bearer_token: string | null;
|
||||||
model_id: string | null;
|
model_id: string | null;
|
||||||
disable_prompt_caching: boolean;
|
disable_prompt_caching: boolean;
|
||||||
|
service_tier: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OllamaConfig {
|
export interface OllamaConfig {
|
||||||
@@ -73,6 +76,17 @@ export interface OpenAiCompatibleConfig {
|
|||||||
model_id: string | null;
|
model_id: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ClaudeCodeSettings {
|
||||||
|
tui_mode: string | null;
|
||||||
|
effort: string | null;
|
||||||
|
auto_scroll_disabled: boolean;
|
||||||
|
focus_mode: boolean;
|
||||||
|
show_thinking_summaries: boolean;
|
||||||
|
enable_session_recap: boolean;
|
||||||
|
env_scrub: boolean;
|
||||||
|
prompt_caching_1h: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ContainerInfo {
|
export interface ContainerInfo {
|
||||||
container_id: string;
|
container_id: string;
|
||||||
project_id: string;
|
project_id: string;
|
||||||
@@ -93,6 +107,7 @@ export interface TerminalSession {
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
projectName: string;
|
projectName: string;
|
||||||
sessionType: "claude" | "bash";
|
sessionType: "claude" | "bash";
|
||||||
|
sessionName: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ImageSource = "registry" | "local_build" | "custom";
|
export type ImageSource = "registry" | "local_build" | "custom";
|
||||||
@@ -120,6 +135,7 @@ export interface AppSettings {
|
|||||||
dismissed_image_digest: string | null;
|
dismissed_image_digest: string | null;
|
||||||
web_terminal: WebTerminalSettings;
|
web_terminal: WebTerminalSettings;
|
||||||
stt: SttSettings;
|
stt: SttSettings;
|
||||||
|
global_claude_code_settings: ClaudeCodeSettings | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SttSettings {
|
export interface SttSettings {
|
||||||
@@ -197,3 +213,14 @@ export interface FileEntry {
|
|||||||
modified: string;
|
modified: string;
|
||||||
permissions: string;
|
permissions: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface InstallOptions {
|
||||||
|
os: "linux" | "macos" | "windows" | "unknown";
|
||||||
|
product_name: string;
|
||||||
|
can_auto_install: boolean;
|
||||||
|
auto_install_method: string | null;
|
||||||
|
auto_install_blocker: string | null;
|
||||||
|
docs_url: string;
|
||||||
|
manual_steps: string[];
|
||||||
|
post_install_notes: string[];
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,17 @@ FROM ubuntu:24.04
|
|||||||
ENV DEBIAN_FRONTEND=noninteractive
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
# ── System packages ──────────────────────────────────────────────────────────
|
# ── System packages ──────────────────────────────────────────────────────────
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
# The shell retry loop handles transient mirror-sync failures where
|
||||||
|
# archive.ubuntu.com returns stale Packages.gz files with mismatched hashes
|
||||||
|
# during hourly resyncs. Clearing /var/lib/apt/lists/* between attempts
|
||||||
|
# forces a fresh fetch.
|
||||||
|
RUN for i in 1 2 3 4 5; do \
|
||||||
|
apt-get -o Acquire::Retries=3 update && break; \
|
||||||
|
echo "apt-get update failed (attempt $i), retrying in 10s..."; \
|
||||||
|
rm -rf /var/lib/apt/lists/*; \
|
||||||
|
sleep 10; \
|
||||||
|
done \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
git \
|
git \
|
||||||
curl \
|
curl \
|
||||||
wget \
|
wget \
|
||||||
@@ -21,6 +31,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
pkg-config \
|
pkg-config \
|
||||||
libssl-dev \
|
libssl-dev \
|
||||||
cron \
|
cron \
|
||||||
|
bubblewrap \
|
||||||
|
socat \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Remove default ubuntu user to free UID 1000 for host-user remapping
|
# Remove default ubuntu user to free UID 1000 for host-user remapping
|
||||||
@@ -38,17 +50,42 @@ RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
|
|||||||
&& chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg \
|
&& chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg \
|
||||||
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
|
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
|
||||||
> /etc/apt/sources.list.d/github-cli.list \
|
> /etc/apt/sources.list.d/github-cli.list \
|
||||||
&& apt-get update && apt-get install -y gh \
|
&& for i in 1 2 3 4 5; do \
|
||||||
|
apt-get -o Acquire::Retries=3 update && break; \
|
||||||
|
echo "apt-get update failed (attempt $i), retrying in 10s..."; \
|
||||||
|
rm -rf /var/lib/apt/lists/*; \
|
||||||
|
sleep 10; \
|
||||||
|
done \
|
||||||
|
&& apt-get install -y gh \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# ── Node.js LTS (22.x) + pnpm ───────────────────────────────────────────────
|
# ── Node.js LTS (22.x) + pnpm ───────────────────────────────────────────────
|
||||||
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
# Configure NodeSource repo manually (not via their setup_22.x script, which
|
||||||
|
# runs an internal apt-get update without retries and silently falls through
|
||||||
|
# to Ubuntu's default nodejs 18 — missing npm — on mirror-sync failures).
|
||||||
|
RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
|
||||||
|
| gpg --dearmor -o /usr/share/keyrings/nodesource.gpg \
|
||||||
|
&& chmod a+r /usr/share/keyrings/nodesource.gpg \
|
||||||
|
&& echo "deb [signed-by=/usr/share/keyrings/nodesource.gpg] https://deb.nodesource.com/node_22.x nodistro main" \
|
||||||
|
> /etc/apt/sources.list.d/nodesource.list \
|
||||||
|
&& for i in 1 2 3 4 5; do \
|
||||||
|
apt-get -o Acquire::Retries=3 update && break; \
|
||||||
|
echo "apt-get update failed (attempt $i), retrying in 10s..."; \
|
||||||
|
rm -rf /var/lib/apt/lists/*; \
|
||||||
|
sleep 10; \
|
||||||
|
done \
|
||||||
&& apt-get install -y nodejs \
|
&& apt-get install -y nodejs \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
&& npm install -g pnpm
|
&& npm install -g pnpm
|
||||||
|
|
||||||
# ── Python 3 + pip + uv + ruff ──────────────────────────────────────────────
|
# ── Python 3 + pip + uv + ruff ──────────────────────────────────────────────
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN for i in 1 2 3 4 5; do \
|
||||||
|
apt-get -o Acquire::Retries=3 update && break; \
|
||||||
|
echo "apt-get update failed (attempt $i), retrying in 10s..."; \
|
||||||
|
rm -rf /var/lib/apt/lists/*; \
|
||||||
|
sleep 10; \
|
||||||
|
done \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
python3 \
|
python3 \
|
||||||
python3-pip \
|
python3-pip \
|
||||||
python3-venv \
|
python3-venv \
|
||||||
@@ -61,7 +98,13 @@ RUN install -m 0755 -d /etc/apt/keyrings \
|
|||||||
&& chmod a+r /etc/apt/keyrings/docker.gpg \
|
&& chmod a+r /etc/apt/keyrings/docker.gpg \
|
||||||
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
|
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
|
||||||
> /etc/apt/sources.list.d/docker.list \
|
> /etc/apt/sources.list.d/docker.list \
|
||||||
&& apt-get update && apt-get install -y docker-ce-cli \
|
&& for i in 1 2 3 4 5; do \
|
||||||
|
apt-get -o Acquire::Retries=3 update && break; \
|
||||||
|
echo "apt-get update failed (attempt $i), retrying in 10s..."; \
|
||||||
|
rm -rf /var/lib/apt/lists/*; \
|
||||||
|
sleep 10; \
|
||||||
|
done \
|
||||||
|
&& apt-get install -y docker-ce-cli \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# ── AWS CLI v2 ───────────────────────────────────────────────────────────────
|
# ── AWS CLI v2 ───────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -188,6 +188,29 @@ if [ -n "$MCP_SERVERS_JSON" ]; then
|
|||||||
unset MCP_SERVERS_JSON
|
unset MCP_SERVERS_JSON
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── Claude Code settings ────────────────────────────────────────────────────
|
||||||
|
# Merge Claude Code settings into ~/.claude/settings.json (preserves existing
|
||||||
|
# keys). Creates the file if it doesn't exist. These control TUI mode, effort
|
||||||
|
# level, focus mode, thinking summaries, and other CLI behavior.
|
||||||
|
if [ -n "$CLAUDE_CODE_SETTINGS_JSON" ]; then
|
||||||
|
SETTINGS_FILE="/home/claude/.claude/settings.json"
|
||||||
|
mkdir -p /home/claude/.claude
|
||||||
|
if [ -f "$SETTINGS_FILE" ]; then
|
||||||
|
# Merge: existing settings + new settings (new keys override on conflict)
|
||||||
|
MERGED=$(jq -s '.[0] * .[1]' "$SETTINGS_FILE" <(printf '%s' "$CLAUDE_CODE_SETTINGS_JSON") 2>/dev/null)
|
||||||
|
if [ -n "$MERGED" ]; then
|
||||||
|
printf '%s\n' "$MERGED" > "$SETTINGS_FILE"
|
||||||
|
else
|
||||||
|
echo "entrypoint: warning — failed to merge Claude Code settings into $SETTINGS_FILE"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
printf '%s\n' "$CLAUDE_CODE_SETTINGS_JSON" > "$SETTINGS_FILE"
|
||||||
|
fi
|
||||||
|
chown claude:claude "$SETTINGS_FILE"
|
||||||
|
chmod 600 "$SETTINGS_FILE"
|
||||||
|
unset CLAUDE_CODE_SETTINGS_JSON
|
||||||
|
fi
|
||||||
|
|
||||||
# ── AWS SSO auth refresh command ──────────────────────────────────────────────
|
# ── AWS SSO auth refresh command ──────────────────────────────────────────────
|
||||||
# When set, inject awsAuthRefresh into ~/.claude.json so Claude Code calls
|
# When set, inject awsAuthRefresh into ~/.claude.json so Claude Code calls
|
||||||
# triple-c-sso-refresh when AWS credentials expire mid-session.
|
# triple-c-sso-refresh when AWS credentials expire mid-session.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Flight Operations
|
# Flight Operations
|
||||||
|
|
||||||
This directory contains reference materials for the [Flight Control](https://github.com/anthropics/flight-control) development methodology.
|
This directory contains reference materials for the [Flight Control](https://github.com/msieurthenardier/mission-control) development methodology.
|
||||||
|
|
||||||
## Contents
|
## Contents
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user