Compare commits
16 Commits
v0.3.4-win
...
v0.3.20-ma
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a5fbd6be4 | |||
| 2fa6abeae0 | |||
| 5b1c801cf1 | |||
| 9b78b4bc62 | |||
| 7acc8b8d39 | |||
| 7840bddbb4 | |||
| 4588bdf40c | |||
| b607cf3681 | |||
| 21a85dc977 | |||
| 272eb28863 | |||
| 1ef6efca9f | |||
| 5974347913 | |||
| 805f815876 | |||
| 5360f22b65 | |||
| 0316234329 | |||
| ee68cc820c |
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
|
||||
@@ -264,21 +264,72 @@ jobs:
|
||||
env:
|
||||
TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG="v${{ needs.compute-version.outputs.version }}-mac"
|
||||
# Create release
|
||||
curl -s -X POST \
|
||||
|
||||
# Idempotent get-or-create. macOS upload has historically failed
|
||||
# mid-stream (curl exit 92, exit 28), leaving the release record
|
||||
# with empty assets. A naive POST /releases on the next run hits
|
||||
# 409 from Gitea for the duplicate tag, the JSON parse below
|
||||
# then yields an empty RELEASE_ID, and pipefail aborts with an
|
||||
# opaque exit 1. Look the release up by tag first; create only
|
||||
# if it doesn't exist; reuse the existing id otherwise.
|
||||
HTTP_CODE=$(curl -sS -o release.json -w '%{http_code}' \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
"${GITEA_URL}/api/v1/repos/${REPO}/releases/tags/${TAG}")
|
||||
case "${HTTP_CODE}" in
|
||||
200)
|
||||
echo "Release ${TAG} already exists, reusing"
|
||||
;;
|
||||
404)
|
||||
echo "Release ${TAG} not found, creating"
|
||||
curl -fsS -X POST \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\": \"${TAG}\", \"name\": \"Triple-C v${{ needs.compute-version.outputs.version }} (macOS)\", \"body\": \"Automated build from commit ${{ gitea.sha }}\"}" \
|
||||
"${GITEA_URL}/api/v1/repos/${REPO}/releases" > release.json
|
||||
RELEASE_ID=$(cat release.json | grep -o '"id":[0-9]*' | head -1 | grep -o '[0-9]*')
|
||||
;;
|
||||
*)
|
||||
echo "Unexpected HTTP ${HTTP_CODE} from get-release-by-tag" >&2
|
||||
cat release.json >&2 || true
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
RELEASE_ID=$(grep -o '"id":[0-9]*' release.json | head -1 | grep -o '[0-9]*' || true)
|
||||
if [ -z "${RELEASE_ID}" ]; then
|
||||
echo "Failed to parse release id; response was:" >&2
|
||||
cat release.json >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Release ID: ${RELEASE_ID}"
|
||||
# Upload each artifact
|
||||
|
||||
# Upload each artifact. If an asset with the same name already
|
||||
# exists on the release (left over from a partial prior run),
|
||||
# delete it first so the upload is replace-not-conflict.
|
||||
# Network hardening: HTTP/1.1 to dodge HTTP/2 stream flakes
|
||||
# the macOS runner has hit, retries with backoff for transient
|
||||
# drops, and -f so HTTP errors stop being silently swallowed.
|
||||
for file in artifacts/*; do
|
||||
[ -f "$file" ] || continue
|
||||
filename=$(basename "$file")
|
||||
|
||||
EXISTING_ID=$(curl -sS \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
"${GITEA_URL}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets" \
|
||||
| python3 -c "import json,sys; t=sys.argv[1]; print(next((a['id'] for a in json.load(sys.stdin) if a.get('name')==t), ''))" "${filename}" || true)
|
||||
if [ -n "${EXISTING_ID}" ]; then
|
||||
echo "Deleting existing asset ${filename} (id ${EXISTING_ID})"
|
||||
curl -fsS -X DELETE \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
"${GITEA_URL}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets/${EXISTING_ID}"
|
||||
fi
|
||||
|
||||
echo "Uploading ${filename}..."
|
||||
curl -s -X POST \
|
||||
curl -fsS --http1.1 \
|
||||
--retry 5 --retry-all-errors --retry-delay 5 \
|
||||
--max-time 600 \
|
||||
-X POST \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary "@${file}" \
|
||||
|
||||
104
app/package-lock.json
generated
104
app/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "triple-c",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "triple-c",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-dialog": "^2.7.0",
|
||||
@@ -1757,9 +1757,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/api": {
|
||||
"version": "2.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.10.1.tgz",
|
||||
"integrity": "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==",
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.0.tgz",
|
||||
"integrity": "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -1767,9 +1767,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.10.0.tgz",
|
||||
"integrity": "sha512-ZwT0T+7bw4+DPCSWzmviwq5XbXlM0cNoleDKOYPFYqcZqeKY31KlpoMW/MOON/tOFBPgi31a2v3w9gliqwL2+Q==",
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.0.tgz",
|
||||
"integrity": "sha512-W5Wbuqsb2pHFPTj4TaRNKTj5rwXhDShPiLSY9T18y4ouSR/NNCptAEFxFsBtyNRgL6Vs1a/q9LzfqqYzEwC+Jw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"bin": {
|
||||
@@ -1783,23 +1783,23 @@
|
||||
"url": "https://opencollective.com/tauri"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tauri-apps/cli-darwin-arm64": "2.10.0",
|
||||
"@tauri-apps/cli-darwin-x64": "2.10.0",
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf": "2.10.0",
|
||||
"@tauri-apps/cli-linux-arm64-gnu": "2.10.0",
|
||||
"@tauri-apps/cli-linux-arm64-musl": "2.10.0",
|
||||
"@tauri-apps/cli-linux-riscv64-gnu": "2.10.0",
|
||||
"@tauri-apps/cli-linux-x64-gnu": "2.10.0",
|
||||
"@tauri-apps/cli-linux-x64-musl": "2.10.0",
|
||||
"@tauri-apps/cli-win32-arm64-msvc": "2.10.0",
|
||||
"@tauri-apps/cli-win32-ia32-msvc": "2.10.0",
|
||||
"@tauri-apps/cli-win32-x64-msvc": "2.10.0"
|
||||
"@tauri-apps/cli-darwin-arm64": "2.11.0",
|
||||
"@tauri-apps/cli-darwin-x64": "2.11.0",
|
||||
"@tauri-apps/cli-linux-arm-gnueabihf": "2.11.0",
|
||||
"@tauri-apps/cli-linux-arm64-gnu": "2.11.0",
|
||||
"@tauri-apps/cli-linux-arm64-musl": "2.11.0",
|
||||
"@tauri-apps/cli-linux-riscv64-gnu": "2.11.0",
|
||||
"@tauri-apps/cli-linux-x64-gnu": "2.11.0",
|
||||
"@tauri-apps/cli-linux-x64-musl": "2.11.0",
|
||||
"@tauri-apps/cli-win32-arm64-msvc": "2.11.0",
|
||||
"@tauri-apps/cli-win32-ia32-msvc": "2.11.0",
|
||||
"@tauri-apps/cli-win32-x64-msvc": "2.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-arm64": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.10.0.tgz",
|
||||
"integrity": "sha512-avqHD4HRjrMamE/7R/kzJPcAJnZs0IIS+1nkDP5b+TNBn3py7N2aIo9LIpy+VQq0AkN8G5dDpZtOOBkmWt/zjA==",
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.0.tgz",
|
||||
"integrity": "sha512-UfMeDNlgIP252rm/KSTuu8yHatPua5TjtUEUf+jyIzVwBNcIl7Ywkdpfj+e5jVVg3EfCTp+4gwuL1dNpgF8clg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1814,9 +1814,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-darwin-x64": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.10.0.tgz",
|
||||
"integrity": "sha512-keDmlvJRStzVFjZTd0xYkBONLtgBC9eMTpmXnBXzsHuawV2q9PvDo2x6D5mhuoMVrJ9QWjgaPKBBCFks4dK71Q==",
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.0.tgz",
|
||||
"integrity": "sha512-lY1+aPlgyMN7vgjtCdQ3+WODfZkebAcxnrCrO0HjqDpKSXieDkrJbimqeaoM4RwhTSrCLRHfVYiYrfE5E131tg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1831,9 +1831,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.10.0.tgz",
|
||||
"integrity": "sha512-e5u0VfLZsMAC9iHaOEANumgl6lfnJx0Dtjkd8IJpysZ8jp0tJ6wrIkto2OzQgzcYyRCKgX72aKE0PFgZputA8g==",
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.0.tgz",
|
||||
"integrity": "sha512-5uCP0AusgN3NrKC8EpkuJwjek1k8pEffBdugJSpXPey/QGbPEb8vZ542n/giJ2mZPjMSllDkdhG2QIDpBY4PpQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1848,9 +1848,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-gnu": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.10.0.tgz",
|
||||
"integrity": "sha512-YrYYk2dfmBs5m+OIMCrb+JH/oo+4FtlpcrTCgiFYc7vcs6m3QDd1TTyWu0u01ewsCtK2kOdluhr/zKku+KP7HA==",
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.0.tgz",
|
||||
"integrity": "sha512-loDPqtRHMSbIcrH2VBd4GgHoQlF7jJnrZj7MxA2lj1cixS/jEgMAPFqj83U6Wvjete4HfYplbE/gCpSFifA9jw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1865,9 +1865,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-arm64-musl": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.10.0.tgz",
|
||||
"integrity": "sha512-GUoPdVJmrJRIXFfW3Rkt+eGK9ygOdyISACZfC/bCSfOnGt8kNdQIQr5WRH9QUaTVFIwxMlQyV3m+yXYP+xhSVA==",
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.0.tgz",
|
||||
"integrity": "sha512-DtSE8ZBlB9H+L+eHkfZ3myt00EVEyAB3e41juEHoE2qT88fgVlJvyrwa9SZYc/xTwCS9TnmK+R84tpg+ZsAg7Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1882,9 +1882,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-riscv64-gnu": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.10.0.tgz",
|
||||
"integrity": "sha512-JO7s3TlSxshwsoKNCDkyvsx5gw2QAs/Y2GbR5UE2d5kkU138ATKoPOtxn8G1fFT1aDW4LH0rYAAfBpGkDyJJnw==",
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.0.tgz",
|
||||
"integrity": "sha512-5QdgS4LD+kntClI1aj2JmwjW38LosNXxwCe8viIHEwqYIWuMPdNEIau6/cLogI38Yzx9DnfCPRfEWLyI+5li8Q==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -1899,9 +1899,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-gnu": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.10.0.tgz",
|
||||
"integrity": "sha512-Uvh4SUUp4A6DVRSMWjelww0GnZI3PlVy7VS+DRF5napKuIehVjGl9XD0uKoCoxwAQBLctvipyEK+pDXpJeoHng==",
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.0.tgz",
|
||||
"integrity": "sha512-5UynPXo3Zq9khjVdAbD+YogeLltdVUeOah2ioSIM3tu6H7wY9vMy6rgGJhv9r5R8ZXmk9GttMippdqYJWrnLnA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1916,9 +1916,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-linux-x64-musl": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.10.0.tgz",
|
||||
"integrity": "sha512-AP0KRK6bJuTpQ8kMNWvhIpKUkQJfcPFeba7QshOQZjJ8wOS6emwTN4K5g/d3AbCMo0RRdnZWwu67MlmtJyxC1Q==",
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.0.tgz",
|
||||
"integrity": "sha512-CNz7fHbApz1Zyhhq73jtGn9JqgNEV/lIWnTnUo6h6ujw+mHsTmkLszvJSM8W6JBaDjNpTTFr/RSNoVL5FMwcTg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1933,9 +1933,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-arm64-msvc": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.10.0.tgz",
|
||||
"integrity": "sha512-97DXVU3dJystrq7W41IX+82JEorLNY+3+ECYxvXWqkq7DBN6FsA08x/EFGE8N/b0LTOui9X2dvpGGoeZKKV08g==",
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.0.tgz",
|
||||
"integrity": "sha512-K+br+VXZ+Xx0n/9FdWohpW5Ugq+2FQUpJScqcPl1hTxXfh3fgjYgt4qA2NgrjlJo+zZPNrmUMl+NLvm0ufEqBQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1950,9 +1950,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-ia32-msvc": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.10.0.tgz",
|
||||
"integrity": "sha512-EHyQ1iwrWy1CwMalEm9z2a6L5isQ121pe7FcA2xe4VWMJp+GHSDDGvbTv/OPdkt2Lyr7DAZBpZHM6nvlHXEc4A==",
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.0.tgz",
|
||||
"integrity": "sha512-OFV+s3MLZnd75zl0ZAFU5riMpGK4waUEA8ZDuijDsnkU0btz/gHhqh5jVlOn8thyvgdtT3Xyoxqo099MMifH3g==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -1967,9 +1967,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/cli-win32-x64-msvc": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.10.0.tgz",
|
||||
"integrity": "sha512-NTpyQxkpzGmU6ceWBTY2xRIEaS0ZLbVx1HE1zTA3TY/pV3+cPoPPOs+7YScr4IMzXMtOw7tLw5LEXo5oIG3qaQ==",
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.0.tgz",
|
||||
"integrity": "sha512-AeDTWBd2cOZ6TX133BWsoo+LutG9o0JRcgjMsIfLE13ZugpgCMv/2dJbUiBGeRvbPOGin5A3aYmsArPVV6ZSHQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
||||
454
app/src-tauri/Cargo.lock
generated
454
app/src-tauri/Cargo.lock
generated
@@ -280,6 +280,21 @@ version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
@@ -617,9 +632,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "core-graphics"
|
||||
version = "0.24.0"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
|
||||
checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"core-foundation 0.10.1",
|
||||
@@ -699,6 +714,19 @@ dependencies = [
|
||||
"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]]
|
||||
name = "cssparser-macros"
|
||||
version = "0.6.1"
|
||||
@@ -711,14 +739,20 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ctor"
|
||||
version = "0.2.9"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
|
||||
checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"ctor-proc-macro",
|
||||
"dtor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctor-proc-macro"
|
||||
version = "0.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1"
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.11"
|
||||
@@ -795,6 +829,17 @@ version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "deranged"
|
||||
version = "0.5.8"
|
||||
@@ -849,6 +894,27 @@ dependencies = [
|
||||
"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]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
@@ -880,12 +946,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "dispatch2"
|
||||
version = "0.3.1"
|
||||
@@ -932,6 +992,21 @@ dependencies = [
|
||||
"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]]
|
||||
name = "dpi"
|
||||
version = "0.1.2"
|
||||
@@ -956,6 +1031,21 @@ dependencies = [
|
||||
"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]]
|
||||
name = "dunce"
|
||||
version = "1.0.5"
|
||||
@@ -1143,6 +1233,12 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.5.0"
|
||||
@@ -1614,7 +1710,7 @@ version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"foldhash",
|
||||
"foldhash 0.1.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1655,10 +1751,20 @@ checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c"
|
||||
dependencies = [
|
||||
"log",
|
||||
"mac",
|
||||
"markup5ever",
|
||||
"markup5ever 0.14.1",
|
||||
"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]]
|
||||
name = "http"
|
||||
version = "1.4.0"
|
||||
@@ -2158,18 +2264,12 @@ version = "0.8.8-speedreader"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2"
|
||||
dependencies = [
|
||||
"cssparser",
|
||||
"html5ever",
|
||||
"cssparser 0.29.6",
|
||||
"html5ever 0.29.1",
|
||||
"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]]
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
@@ -2206,6 +2306,15 @@ version = "0.2.182"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "libloading"
|
||||
version = "0.7.4"
|
||||
@@ -2296,9 +2405,20 @@ dependencies = [
|
||||
"log",
|
||||
"phf 0.11.3",
|
||||
"phf_codegen 0.11.3",
|
||||
"string_cache",
|
||||
"string_cache_codegen",
|
||||
"tendril",
|
||||
"string_cache 0.8.9",
|
||||
"string_cache_codegen 0.5.4",
|
||||
"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]]
|
||||
@@ -2388,9 +2508,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "muda"
|
||||
version = "0.17.1"
|
||||
version = "0.19.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a"
|
||||
checksum = "0ae8844f63b5b118e334e205585b8c5c17b984121dbdb179d44aeb087ffad3cb"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"dpi",
|
||||
@@ -2401,10 +2521,10 @@ dependencies = [
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
"once_cell",
|
||||
"png 0.17.16",
|
||||
"png 0.18.1",
|
||||
"serde",
|
||||
"thiserror 2.0.18",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2422,12 +2542,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "ndk-sys"
|
||||
version = "0.6.0+11769913"
|
||||
@@ -2533,17 +2647,9 @@ checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
"objc2-cloud-kit",
|
||||
"objc2-core-data",
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-graphics",
|
||||
"objc2-core-image",
|
||||
"objc2-core-text",
|
||||
"objc2-core-video",
|
||||
"objc2-foundation",
|
||||
"objc2-quartz-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2563,7 +2669,6 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
]
|
||||
@@ -2602,6 +2707,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "objc2-core-text"
|
||||
version = "0.3.2"
|
||||
@@ -2614,19 +2729,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "objc2-encode"
|
||||
version = "4.1.0"
|
||||
@@ -2666,16 +2768,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "objc2-quartz-core"
|
||||
version = "0.3.2"
|
||||
@@ -2688,17 +2780,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "objc2-ui-kit"
|
||||
version = "0.3.2"
|
||||
@@ -2706,8 +2787,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"block2",
|
||||
"objc2",
|
||||
"objc2-cloud-kit",
|
||||
"objc2-core-data",
|
||||
"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",
|
||||
]
|
||||
|
||||
@@ -2723,8 +2823,6 @@ dependencies = [
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
"objc2-javascript-core",
|
||||
"objc2-security",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2857,6 +2955,17 @@ dependencies = [
|
||||
"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]]
|
||||
name = "phf_codegen"
|
||||
version = "0.8.0"
|
||||
@@ -2877,6 +2986,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "phf_generator"
|
||||
version = "0.8.0"
|
||||
@@ -2907,6 +3026,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "phf_macros"
|
||||
version = "0.10.0"
|
||||
@@ -2934,6 +3063,19 @@ dependencies = [
|
||||
"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]]
|
||||
name = "phf_shared"
|
||||
version = "0.8.0"
|
||||
@@ -2961,6 +3103,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.16"
|
||||
@@ -3751,14 +3902,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"cssparser",
|
||||
"derive_more",
|
||||
"cssparser 0.29.6",
|
||||
"derive_more 0.99.20",
|
||||
"fxhash",
|
||||
"log",
|
||||
"phf 0.8.0",
|
||||
"phf_codegen 0.8.0",
|
||||
"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",
|
||||
]
|
||||
|
||||
@@ -3953,6 +4123,15 @@ dependencies = [
|
||||
"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]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
@@ -4098,6 +4277,18 @@ dependencies = [
|
||||
"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]]
|
||||
name = "string_cache_codegen"
|
||||
version = "0.5.4"
|
||||
@@ -4110,6 +4301,18 @@ dependencies = [
|
||||
"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]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
@@ -4190,35 +4393,35 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tao"
|
||||
version = "0.34.5"
|
||||
version = "0.35.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7"
|
||||
checksum = "1cf65722394c2ac443e80120064987f8914ee1d4e4e36e63cdf10f2990f01159"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"block2",
|
||||
"core-foundation 0.10.1",
|
||||
"core-graphics",
|
||||
"crossbeam-channel",
|
||||
"dispatch",
|
||||
"dbus",
|
||||
"dispatch2",
|
||||
"dlopen2",
|
||||
"dpi",
|
||||
"gdkwayland-sys",
|
||||
"gdkx11-sys",
|
||||
"gtk",
|
||||
"jni",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
"ndk",
|
||||
"ndk-context",
|
||||
"ndk-sys",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation",
|
||||
"objc2-ui-kit",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"raw-window-handle",
|
||||
"scopeguard",
|
||||
"tao-macros",
|
||||
"unicode-segmentation",
|
||||
"url",
|
||||
@@ -4258,9 +4461,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
||||
|
||||
[[package]]
|
||||
name = "tauri"
|
||||
version = "2.10.2"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "463ae8677aa6d0f063a900b9c41ecd4ac2b7ca82f0b058cc4491540e55b20129"
|
||||
checksum = "d059f2527558d9dba6f186dec4772610e1aecfd3f94002397613e7e648752b66"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -4310,9 +4513,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-build"
|
||||
version = "2.5.5"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca7bd893329425df750813e95bd2b643d5369d929438da96d5bbb7cc2c918f74"
|
||||
checksum = "be9aa8c59a894f76c29a002501c589de5eb4987a5913d62a6e0a47f320901988"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cargo_toml",
|
||||
@@ -4326,15 +4529,14 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tauri-utils",
|
||||
"tauri-winres",
|
||||
"toml 0.9.12+spec-1.1.0",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-codegen"
|
||||
version = "2.5.4"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aac423e5859d9f9ccdd32e3cf6a5866a15bedbf25aa6630bcb2acde9468f6ae3"
|
||||
checksum = "d3e4e8230d565106aa19dfbaa01a7ed01abf78047fe0577a83377224bd1bf20e"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"brotli",
|
||||
@@ -4359,9 +4561,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-macros"
|
||||
version = "2.5.4"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b6a1bd2861ff0c8766b1d38b32a6a410f6dc6532d4ef534c47cfb2236092f59"
|
||||
checksum = "bc8de2cddbbc33dbdf4c84f170121886595efdbcc9cb4b3d76342b79d082cedc"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
@@ -4470,9 +4672,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "2.10.0"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b885ffeac82b00f1f6fd292b6e5aabfa7435d537cef57d11e38a489956535651"
|
||||
checksum = "1e42bbcb76237351fbaa02f08d808c537dc12eb5a6eabbf3e517b50056334d95"
|
||||
dependencies = [
|
||||
"cookie",
|
||||
"dpi",
|
||||
@@ -4495,9 +4697,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime-wry"
|
||||
version = "2.10.0"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5204682391625e867d16584fedc83fc292fb998814c9f7918605c789cd876314"
|
||||
checksum = "2cadb13dad0c681e1e0a2c49ae488f0e2906ded3d57e7a0017f4aaf46e387117"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
@@ -4505,7 +4707,6 @@ dependencies = [
|
||||
"log",
|
||||
"objc2",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"raw-window-handle",
|
||||
@@ -4522,17 +4723,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-utils"
|
||||
version = "2.8.2"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fcd169fccdff05eff2c1033210b9b94acd07a47e6fa9a3431cf09cfd4f01c87e"
|
||||
checksum = "55f61d2bf7188fbcf2b0ed095b67a6bc498f713c939314bb19eb700118a573b7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"brotli",
|
||||
"cargo_metadata",
|
||||
"ctor",
|
||||
"dom_query",
|
||||
"dunce",
|
||||
"glob",
|
||||
"html5ever",
|
||||
"html5ever 0.29.1",
|
||||
"http",
|
||||
"infer",
|
||||
"json-patch",
|
||||
@@ -4540,6 +4742,7 @@ dependencies = [
|
||||
"log",
|
||||
"memchr",
|
||||
"phf 0.11.3",
|
||||
"plist",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
@@ -4593,6 +4796,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
@@ -4928,9 +5141,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tray-icon"
|
||||
version = "0.21.3"
|
||||
version = "0.23.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c"
|
||||
checksum = "15edbb0d80583e85ee8df283410038e17314df5cba30da2087a54a85216c0773"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"dirs",
|
||||
@@ -4942,10 +5155,10 @@ dependencies = [
|
||||
"objc2-core-graphics",
|
||||
"objc2-foundation",
|
||||
"once_cell",
|
||||
"png 0.17.16",
|
||||
"png 0.18.1",
|
||||
"serde",
|
||||
"thiserror 2.0.18",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5353,6 +5566,18 @@ dependencies = [
|
||||
"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]]
|
||||
name = "webkit2gtk"
|
||||
version = "2.0.2"
|
||||
@@ -6000,24 +6225,23 @@ checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
|
||||
|
||||
[[package]]
|
||||
name = "wry"
|
||||
version = "0.54.2"
|
||||
version = "0.55.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb26159b420aa77684589a744ae9a9461a95395b848764ad12290a14d960a11a"
|
||||
checksum = "3013fd6116aac351dd2e18f349b28b2cfef3a5ff3253a9d0ce2d7193bb1b4429"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"block2",
|
||||
"cookie",
|
||||
"crossbeam-channel",
|
||||
"dirs",
|
||||
"dom_query",
|
||||
"dpi",
|
||||
"dunce",
|
||||
"gdkx11",
|
||||
"gtk",
|
||||
"html5ever",
|
||||
"http",
|
||||
"javascriptcore-rs",
|
||||
"jni",
|
||||
"kuchikiki",
|
||||
"libc",
|
||||
"ndk",
|
||||
"objc2",
|
||||
|
||||
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`"
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"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.",
|
||||
@@ -428,6 +428,12 @@
|
||||
"const": "core:app:allow-set-dock-visibility",
|
||||
"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.",
|
||||
"type": "string",
|
||||
@@ -512,6 +518,12 @@
|
||||
"const": "core:app:deny-set-dock-visibility",
|
||||
"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.",
|
||||
"type": "string",
|
||||
@@ -1035,10 +1047,10 @@
|
||||
"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",
|
||||
"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.",
|
||||
@@ -1070,6 +1082,12 @@
|
||||
"const": "core:tray:allow-set-icon-as-template",
|
||||
"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.",
|
||||
"type": "string",
|
||||
@@ -1136,6 +1154,12 @@
|
||||
"const": "core:tray:deny-set-icon-as-template",
|
||||
"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.",
|
||||
"type": "string",
|
||||
@@ -1395,10 +1419,16 @@
|
||||
"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",
|
||||
"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.",
|
||||
@@ -1592,6 +1622,12 @@
|
||||
"const": "core:window:allow-scale-factor",
|
||||
"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.",
|
||||
"type": "string",
|
||||
@@ -1856,6 +1892,12 @@
|
||||
"const": "core:window:allow-unminimize",
|
||||
"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.",
|
||||
"type": "string",
|
||||
@@ -2048,6 +2090,12 @@
|
||||
"const": "core:window:deny-scale-factor",
|
||||
"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.",
|
||||
"type": "string",
|
||||
@@ -2313,22 +2361,22 @@
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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.",
|
||||
@@ -2349,16 +2397,16 @@
|
||||
"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",
|
||||
"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",
|
||||
"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.",
|
||||
|
||||
@@ -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`"
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"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.",
|
||||
@@ -428,6 +428,12 @@
|
||||
"const": "core:app:allow-set-dock-visibility",
|
||||
"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.",
|
||||
"type": "string",
|
||||
@@ -512,6 +518,12 @@
|
||||
"const": "core:app:deny-set-dock-visibility",
|
||||
"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.",
|
||||
"type": "string",
|
||||
@@ -1035,10 +1047,10 @@
|
||||
"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",
|
||||
"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.",
|
||||
@@ -1070,6 +1082,12 @@
|
||||
"const": "core:tray:allow-set-icon-as-template",
|
||||
"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.",
|
||||
"type": "string",
|
||||
@@ -1136,6 +1154,12 @@
|
||||
"const": "core:tray:deny-set-icon-as-template",
|
||||
"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.",
|
||||
"type": "string",
|
||||
@@ -1395,10 +1419,16 @@
|
||||
"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",
|
||||
"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.",
|
||||
@@ -1592,6 +1622,12 @@
|
||||
"const": "core:window:allow-scale-factor",
|
||||
"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.",
|
||||
"type": "string",
|
||||
@@ -1856,6 +1892,12 @@
|
||||
"const": "core:window:allow-unminimize",
|
||||
"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.",
|
||||
"type": "string",
|
||||
@@ -2048,6 +2090,12 @@
|
||||
"const": "core:window:deny-scale-factor",
|
||||
"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.",
|
||||
"type": "string",
|
||||
@@ -2313,22 +2361,22 @@
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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.",
|
||||
@@ -2349,16 +2397,16 @@
|
||||
"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",
|
||||
"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",
|
||||
"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.",
|
||||
|
||||
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 file_commands;
|
||||
pub mod help_commands;
|
||||
pub mod install_helper_commands;
|
||||
pub mod mcp_commands;
|
||||
pub mod project_commands;
|
||||
pub mod settings_commands;
|
||||
|
||||
@@ -193,16 +193,20 @@ pub async fn start_project_container(
|
||||
if project.backend == Backend::Ollama {
|
||||
let ollama = project.ollama_config.as_ref()
|
||||
.ok_or_else(|| "Ollama backend selected but no Ollama configuration found.".to_string())?;
|
||||
if ollama.base_url.is_empty() {
|
||||
return Err("Ollama base URL is required.".to_string());
|
||||
if ollama.base_url.is_empty()
|
||||
&& settings.global_ollama.base_url.as_deref().map(str::trim).unwrap_or("").is_empty()
|
||||
{
|
||||
return Err("Ollama base URL is required. Set it per-project or in global Ollama settings.".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if project.backend == Backend::OpenAiCompatible {
|
||||
let oai_config = project.openai_compatible_config.as_ref()
|
||||
.ok_or_else(|| "OpenAI Compatible backend selected but no configuration found.".to_string())?;
|
||||
if oai_config.base_url.is_empty() {
|
||||
return Err("OpenAI Compatible base URL is required.".to_string());
|
||||
if oai_config.base_url.is_empty()
|
||||
&& settings.global_openai_compatible.base_url.as_deref().map(str::trim).unwrap_or("").is_empty()
|
||||
{
|
||||
return Err("OpenAI Compatible base URL is required. Set it per-project or in global settings.".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,6 +338,9 @@ pub async fn start_project_container(
|
||||
let needs_recreate = docker::container_needs_recreation(
|
||||
&existing_id,
|
||||
&project,
|
||||
&settings.global_aws,
|
||||
&settings.global_ollama,
|
||||
&settings.global_openai_compatible,
|
||||
settings.global_claude_instructions.as_deref(),
|
||||
&settings.global_custom_env_vars,
|
||||
settings.timezone.as_deref(),
|
||||
@@ -369,6 +376,8 @@ pub async fn start_project_container(
|
||||
&create_image,
|
||||
aws_config_path.as_deref(),
|
||||
&settings.global_aws,
|
||||
&settings.global_ollama,
|
||||
&settings.global_openai_compatible,
|
||||
settings.global_claude_instructions.as_deref(),
|
||||
&settings.global_custom_env_vars,
|
||||
settings.timezone.as_deref(),
|
||||
@@ -406,6 +415,8 @@ pub async fn start_project_container(
|
||||
&create_image,
|
||||
aws_config_path.as_deref(),
|
||||
&settings.global_aws,
|
||||
&settings.global_ollama,
|
||||
&settings.global_openai_compatible,
|
||||
settings.global_claude_instructions.as_deref(),
|
||||
&settings.global_custom_env_vars,
|
||||
settings.timezone.as_deref(),
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::collections::HashMap;
|
||||
use sha2::{Sha256, Digest};
|
||||
|
||||
use super::client::get_docker;
|
||||
use crate::models::{Backend, BedrockAuthMethod, ClaudeCodeSettings, ContainerInfo, EnvVar, GlobalAwsSettings, McpServer, McpTransportType, PortMapping, Project, ProjectPath};
|
||||
use crate::models::{Backend, BedrockAuthMethod, ClaudeCodeSettings, ContainerInfo, EnvVar, GlobalAwsSettings, GlobalOllamaSettings, GlobalOpenAiCompatibleSettings, McpServer, McpTransportType, PortMapping, Project, ProjectPath};
|
||||
|
||||
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
|
||||
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
|
||||
/// instructions, appending port mapping docs, and appending scheduler docs.
|
||||
/// Used by both create_container() and container_needs_recreation() to ensure
|
||||
@@ -97,6 +131,7 @@ fn build_claude_instructions(
|
||||
project_instructions: Option<&str>,
|
||||
port_mappings: &[PortMapping],
|
||||
mission_control_enabled: bool,
|
||||
sandbox_enabled: bool,
|
||||
) -> Option<String> {
|
||||
let mut combined = merge_claude_instructions(
|
||||
global_instructions,
|
||||
@@ -126,6 +161,13 @@ fn build_claude_instructions(
|
||||
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
|
||||
}
|
||||
|
||||
@@ -214,9 +256,25 @@ fn sha256_hex(input: &str) -> String {
|
||||
format!("{:x}", hasher.finalize())
|
||||
}
|
||||
|
||||
/// Resolve a per-project string value with a global fallback. Returns `None`
|
||||
/// when both are blank, otherwise the per-project value if set, else the global.
|
||||
fn resolve_with_global<'a>(per_project: Option<&'a str>, global: Option<&'a str>) -> Option<&'a str> {
|
||||
let project_val = per_project.map(str::trim).filter(|s| !s.is_empty());
|
||||
if project_val.is_some() {
|
||||
return project_val;
|
||||
}
|
||||
global.map(str::trim).filter(|s| !s.is_empty())
|
||||
}
|
||||
|
||||
/// Compute a fingerprint for the Bedrock configuration so we can detect changes.
|
||||
fn compute_bedrock_fingerprint(project: &Project) -> String {
|
||||
/// Includes the resolved model_id (per-project blank → global default) so that
|
||||
/// changing the global default forces a container recreation.
|
||||
fn compute_bedrock_fingerprint(project: &Project, global_aws: &GlobalAwsSettings) -> String {
|
||||
if let Some(ref bedrock) = project.bedrock_config {
|
||||
let effective_model = resolve_with_global(
|
||||
bedrock.model_id.as_deref(),
|
||||
global_aws.default_model_id.as_deref(),
|
||||
).unwrap_or("").to_string();
|
||||
let parts = vec![
|
||||
format!("{:?}", bedrock.auth_method),
|
||||
bedrock.aws_region.clone(),
|
||||
@@ -225,8 +283,9 @@ fn compute_bedrock_fingerprint(project: &Project) -> String {
|
||||
bedrock.aws_session_token.as_deref().unwrap_or("").to_string(),
|
||||
bedrock.aws_profile.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(),
|
||||
effective_model,
|
||||
format!("{}", bedrock.disable_prompt_caching),
|
||||
bedrock.service_tier.as_deref().unwrap_or("").to_string(),
|
||||
];
|
||||
sha256_hex(&parts.join("|"))
|
||||
} else {
|
||||
@@ -235,12 +294,18 @@ fn compute_bedrock_fingerprint(project: &Project) -> String {
|
||||
}
|
||||
|
||||
/// Compute a fingerprint for the Ollama configuration so we can detect changes.
|
||||
fn compute_ollama_fingerprint(project: &Project) -> String {
|
||||
/// Includes the resolved base_url and model_id (per-project blank → global default).
|
||||
fn compute_ollama_fingerprint(project: &Project, global_ollama: &GlobalOllamaSettings) -> String {
|
||||
if let Some(ref ollama) = project.ollama_config {
|
||||
let parts = vec![
|
||||
ollama.base_url.clone(),
|
||||
ollama.model_id.as_deref().unwrap_or("").to_string(),
|
||||
];
|
||||
let effective_url = resolve_with_global(
|
||||
Some(&ollama.base_url),
|
||||
global_ollama.base_url.as_deref(),
|
||||
).unwrap_or("").to_string();
|
||||
let effective_model = resolve_with_global(
|
||||
ollama.model_id.as_deref(),
|
||||
global_ollama.default_model_id.as_deref(),
|
||||
).unwrap_or("").to_string();
|
||||
let parts = vec![effective_url, effective_model];
|
||||
sha256_hex(&parts.join("|"))
|
||||
} else {
|
||||
String::new()
|
||||
@@ -248,12 +313,24 @@ fn compute_ollama_fingerprint(project: &Project) -> String {
|
||||
}
|
||||
|
||||
/// Compute a fingerprint for the OpenAI Compatible configuration so we can detect changes.
|
||||
fn compute_openai_compatible_fingerprint(project: &Project) -> String {
|
||||
/// Includes the resolved base_url and model_id (per-project blank → global default).
|
||||
fn compute_openai_compatible_fingerprint(
|
||||
project: &Project,
|
||||
global_openai_compatible: &GlobalOpenAiCompatibleSettings,
|
||||
) -> String {
|
||||
if let Some(ref config) = project.openai_compatible_config {
|
||||
let effective_url = resolve_with_global(
|
||||
Some(&config.base_url),
|
||||
global_openai_compatible.base_url.as_deref(),
|
||||
).unwrap_or("").to_string();
|
||||
let effective_model = resolve_with_global(
|
||||
config.model_id.as_deref(),
|
||||
global_openai_compatible.default_model_id.as_deref(),
|
||||
).unwrap_or("").to_string();
|
||||
let parts = vec![
|
||||
config.base_url.clone(),
|
||||
effective_url,
|
||||
config.api_key.as_deref().unwrap_or("").to_string(),
|
||||
config.model_id.as_deref().unwrap_or("").to_string(),
|
||||
effective_model,
|
||||
];
|
||||
sha256_hex(&parts.join("|"))
|
||||
} else {
|
||||
@@ -312,8 +389,16 @@ fn merge_claude_code_settings(
|
||||
}
|
||||
|
||||
/// Compute a fingerprint for the Claude Code settings so we can detect changes.
|
||||
fn compute_claude_code_settings_fingerprint(settings: Option<&ClaudeCodeSettings>) -> String {
|
||||
match settings {
|
||||
/// 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![
|
||||
@@ -328,29 +413,58 @@ fn compute_claude_code_settings_fingerprint(settings: Option<&ClaudeCodeSettings
|
||||
];
|
||||
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 from ClaudeCodeSettings.
|
||||
/// Build the settings.json content for Claude Code.
|
||||
/// Returns a JSON string of the settings to be written to ~/.claude/settings.json.
|
||||
fn build_claude_code_settings_json(settings: &ClaudeCodeSettings) -> Option<String> {
|
||||
/// 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(ref tui) = settings.tui_mode {
|
||||
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) = settings.effort {
|
||||
if let Some(ref effort) = s.effort {
|
||||
map.insert("effort".to_string(), serde_json::json!(effort));
|
||||
}
|
||||
if settings.auto_scroll_disabled {
|
||||
if s.auto_scroll_disabled {
|
||||
map.insert("autoScrollEnabled".to_string(), serde_json::json!(false));
|
||||
}
|
||||
if settings.focus_mode {
|
||||
if s.focus_mode {
|
||||
map.insert("focusMode".to_string(), serde_json::json!(true));
|
||||
}
|
||||
if settings.show_thinking_summaries {
|
||||
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
|
||||
@@ -472,6 +586,8 @@ pub async fn create_container(
|
||||
image_name: &str,
|
||||
aws_config_path: Option<&str>,
|
||||
global_aws: &GlobalAwsSettings,
|
||||
global_ollama: &GlobalOllamaSettings,
|
||||
global_openai_compatible: &GlobalOpenAiCompatibleSettings,
|
||||
global_claude_instructions: Option<&str>,
|
||||
global_custom_env_vars: &[EnvVar],
|
||||
timezone: Option<&str>,
|
||||
@@ -579,22 +695,40 @@ pub async fn create_container(
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref model) = bedrock.model_id {
|
||||
if let Some(model) = resolve_with_global(
|
||||
bedrock.model_id.as_deref(),
|
||||
global_aws.default_model_id.as_deref(),
|
||||
) {
|
||||
env_vars.push(format!("ANTHROPIC_MODEL={}", model));
|
||||
}
|
||||
|
||||
if bedrock.disable_prompt_caching {
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ollama configuration
|
||||
if project.backend == Backend::Ollama {
|
||||
if let Some(ref ollama) = project.ollama_config {
|
||||
env_vars.push(format!("ANTHROPIC_BASE_URL={}", ollama.base_url));
|
||||
if let Some(url) = resolve_with_global(
|
||||
Some(&ollama.base_url),
|
||||
global_ollama.base_url.as_deref(),
|
||||
) {
|
||||
env_vars.push(format!("ANTHROPIC_BASE_URL={}", url));
|
||||
}
|
||||
env_vars.push("ANTHROPIC_AUTH_TOKEN=ollama".to_string());
|
||||
if let Some(ref model) = ollama.model_id {
|
||||
if let Some(model) = resolve_with_global(
|
||||
ollama.model_id.as_deref(),
|
||||
global_ollama.default_model_id.as_deref(),
|
||||
) {
|
||||
env_vars.push(format!("ANTHROPIC_MODEL={}", model));
|
||||
}
|
||||
}
|
||||
@@ -603,11 +737,19 @@ pub async fn create_container(
|
||||
// OpenAI Compatible configuration
|
||||
if project.backend == Backend::OpenAiCompatible {
|
||||
if let Some(ref config) = project.openai_compatible_config {
|
||||
env_vars.push(format!("ANTHROPIC_BASE_URL={}", config.base_url));
|
||||
if let Some(url) = resolve_with_global(
|
||||
Some(&config.base_url),
|
||||
global_openai_compatible.base_url.as_deref(),
|
||||
) {
|
||||
env_vars.push(format!("ANTHROPIC_BASE_URL={}", url));
|
||||
}
|
||||
if let Some(ref key) = config.api_key {
|
||||
env_vars.push(format!("ANTHROPIC_AUTH_TOKEN={}", key));
|
||||
}
|
||||
if let Some(ref model) = config.model_id {
|
||||
if let Some(model) = resolve_with_global(
|
||||
config.model_id.as_deref(),
|
||||
global_openai_compatible.default_model_id.as_deref(),
|
||||
) {
|
||||
env_vars.push(format!("ANTHROPIC_MODEL={}", model));
|
||||
}
|
||||
}
|
||||
@@ -652,6 +794,7 @@ pub async fn create_container(
|
||||
project.claude_instructions.as_deref(),
|
||||
&project.port_mappings,
|
||||
project.mission_control_enabled,
|
||||
project.sandbox_mode_enabled,
|
||||
);
|
||||
|
||||
if let Some(ref instructions) = combined_instructions {
|
||||
@@ -683,11 +826,16 @@ pub async fn create_container(
|
||||
if cc.prompt_caching_1h {
|
||||
env_vars.push("ENABLE_PROMPT_CACHING_1H=1".to_string());
|
||||
}
|
||||
|
||||
// settings.json-based settings (written by the entrypoint)
|
||||
if let Some(settings_json) = build_claude_code_settings_json(cc) {
|
||||
env_vars.push(format!("CLAUDE_CODE_SETTINGS_JSON={}", settings_json));
|
||||
}
|
||||
|
||||
// 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();
|
||||
@@ -811,9 +959,9 @@ pub async fn create_container(
|
||||
labels.insert("triple-c.project-name".to_string(), project.name.clone());
|
||||
labels.insert("triple-c.backend".to_string(), format!("{:?}", project.backend));
|
||||
labels.insert("triple-c.paths-fingerprint".to_string(), compute_paths_fingerprint(&project.paths));
|
||||
labels.insert("triple-c.bedrock-fingerprint".to_string(), compute_bedrock_fingerprint(project));
|
||||
labels.insert("triple-c.ollama-fingerprint".to_string(), compute_ollama_fingerprint(project));
|
||||
labels.insert("triple-c.openai-compatible-fingerprint".to_string(), compute_openai_compatible_fingerprint(project));
|
||||
labels.insert("triple-c.bedrock-fingerprint".to_string(), compute_bedrock_fingerprint(project, global_aws));
|
||||
labels.insert("triple-c.ollama-fingerprint".to_string(), compute_ollama_fingerprint(project, global_ollama));
|
||||
labels.insert("triple-c.openai-compatible-fingerprint".to_string(), compute_openai_compatible_fingerprint(project, global_openai_compatible));
|
||||
labels.insert("triple-c.ports-fingerprint".to_string(), compute_ports_fingerprint(&project.port_mappings));
|
||||
labels.insert("triple-c.image".to_string(), image_name.to_string());
|
||||
labels.insert("triple-c.timezone".to_string(), timezone.unwrap_or("").to_string());
|
||||
@@ -821,7 +969,7 @@ pub async fn create_container(
|
||||
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.claude-code-settings-fingerprint".to_string(),
|
||||
compute_claude_code_settings_fingerprint(merged_cc_settings.as_ref()));
|
||||
compute_claude_code_settings_fingerprint(merged_cc_settings.as_ref(), project.sandbox_mode_enabled));
|
||||
labels.insert("triple-c.instructions-fingerprint".to_string(),
|
||||
combined_instructions.as_ref().map(|s| sha256_hex(s)).unwrap_or_default());
|
||||
labels.insert("triple-c.git-user-name".to_string(), effective_git_name.unwrap_or_default().to_string());
|
||||
@@ -990,6 +1138,9 @@ pub async fn remove_project_volumes(project: &Project) -> Result<(), String> {
|
||||
pub async fn container_needs_recreation(
|
||||
container_id: &str,
|
||||
project: &Project,
|
||||
global_aws: &GlobalAwsSettings,
|
||||
global_ollama: &GlobalOllamaSettings,
|
||||
global_openai_compatible: &GlobalOpenAiCompatibleSettings,
|
||||
global_claude_instructions: Option<&str>,
|
||||
global_custom_env_vars: &[EnvVar],
|
||||
timezone: Option<&str>,
|
||||
@@ -1061,7 +1212,7 @@ pub async fn container_needs_recreation(
|
||||
}
|
||||
|
||||
// ── Bedrock config fingerprint ───────────────────────────────────────
|
||||
let expected_bedrock_fp = compute_bedrock_fingerprint(project);
|
||||
let expected_bedrock_fp = compute_bedrock_fingerprint(project, global_aws);
|
||||
let container_bedrock_fp = get_label("triple-c.bedrock-fingerprint").unwrap_or_default();
|
||||
if container_bedrock_fp != expected_bedrock_fp {
|
||||
log::info!("Bedrock config mismatch");
|
||||
@@ -1069,7 +1220,7 @@ pub async fn container_needs_recreation(
|
||||
}
|
||||
|
||||
// ── Ollama config fingerprint ────────────────────────────────────────
|
||||
let expected_ollama_fp = compute_ollama_fingerprint(project);
|
||||
let expected_ollama_fp = compute_ollama_fingerprint(project, global_ollama);
|
||||
let container_ollama_fp = get_label("triple-c.ollama-fingerprint").unwrap_or_default();
|
||||
if container_ollama_fp != expected_ollama_fp {
|
||||
log::info!("Ollama config mismatch");
|
||||
@@ -1077,7 +1228,7 @@ pub async fn container_needs_recreation(
|
||||
}
|
||||
|
||||
// ── OpenAI Compatible config fingerprint ────────────────────────────
|
||||
let expected_oai_fp = compute_openai_compatible_fingerprint(project);
|
||||
let expected_oai_fp = compute_openai_compatible_fingerprint(project, global_openai_compatible);
|
||||
let container_oai_fp = get_label("triple-c.openai-compatible-fingerprint").unwrap_or_default();
|
||||
if container_oai_fp != expected_oai_fp {
|
||||
log::info!("OpenAI Compatible config mismatch");
|
||||
@@ -1179,6 +1330,7 @@ pub async fn container_needs_recreation(
|
||||
project.claude_instructions.as_deref(),
|
||||
&project.port_mappings,
|
||||
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 container_instructions_fp = get_label("triple-c.instructions-fingerprint").unwrap_or_default();
|
||||
@@ -1192,7 +1344,7 @@ pub async fn container_needs_recreation(
|
||||
global_claude_code_settings,
|
||||
project.claude_code_settings.as_ref(),
|
||||
);
|
||||
let expected_cc_fp = compute_claude_code_settings_fingerprint(merged_cc.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);
|
||||
|
||||
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 docker;
|
||||
mod install_helper;
|
||||
mod logging;
|
||||
mod models;
|
||||
mod storage;
|
||||
@@ -197,6 +198,9 @@ pub fn run() {
|
||||
commands::update_commands::check_image_update,
|
||||
// Help
|
||||
commands::help_commands::get_help_content,
|
||||
// Install helper
|
||||
commands::install_helper_commands::detect_install_options,
|
||||
commands::install_helper_commands::run_docker_install,
|
||||
// Web Terminal
|
||||
commands::web_terminal_commands::start_web_terminal,
|
||||
commands::web_terminal_commands::stop_web_terminal,
|
||||
|
||||
@@ -32,6 +32,8 @@ pub struct GlobalAwsSettings {
|
||||
pub aws_profile: Option<String>,
|
||||
#[serde(default)]
|
||||
pub aws_region: Option<String>,
|
||||
#[serde(default)]
|
||||
pub default_model_id: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for GlobalAwsSettings {
|
||||
@@ -40,10 +42,27 @@ impl Default for GlobalAwsSettings {
|
||||
aws_config_path: None,
|
||||
aws_profile: None,
|
||||
aws_region: None,
|
||||
default_model_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct GlobalOllamaSettings {
|
||||
#[serde(default)]
|
||||
pub base_url: Option<String>,
|
||||
#[serde(default)]
|
||||
pub default_model_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct GlobalOpenAiCompatibleSettings {
|
||||
#[serde(default)]
|
||||
pub base_url: Option<String>,
|
||||
#[serde(default)]
|
||||
pub default_model_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AppSettings {
|
||||
#[serde(default)]
|
||||
@@ -60,6 +79,10 @@ pub struct AppSettings {
|
||||
pub custom_image_name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub global_aws: GlobalAwsSettings,
|
||||
#[serde(default)]
|
||||
pub global_ollama: GlobalOllamaSettings,
|
||||
#[serde(default)]
|
||||
pub global_openai_compatible: GlobalOpenAiCompatibleSettings,
|
||||
#[serde(default = "default_global_instructions")]
|
||||
pub global_claude_instructions: Option<String>,
|
||||
#[serde(default)]
|
||||
@@ -156,6 +179,8 @@ impl Default for AppSettings {
|
||||
image_source: ImageSource::default(),
|
||||
custom_image_name: None,
|
||||
global_aws: GlobalAwsSettings::default(),
|
||||
global_ollama: GlobalOllamaSettings::default(),
|
||||
global_openai_compatible: GlobalOpenAiCompatibleSettings::default(),
|
||||
global_claude_instructions: default_global_instructions(),
|
||||
global_custom_env_vars: Vec::new(),
|
||||
auto_check_updates: true,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
@@ -73,6 +75,8 @@ pub struct Project {
|
||||
pub openai_compatible_config: Option<OpenAiCompatibleConfig>,
|
||||
pub allow_docker_access: bool,
|
||||
#[serde(default)]
|
||||
pub sandbox_mode_enabled: bool,
|
||||
#[serde(default)]
|
||||
pub mission_control_enabled: bool,
|
||||
#[serde(default = "default_full_permissions")]
|
||||
pub full_permissions: bool,
|
||||
@@ -91,6 +95,9 @@ pub struct Project {
|
||||
pub enabled_mcp_servers: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub claude_code_settings: Option<ClaudeCodeSettings>,
|
||||
/// User-defined display names for terminal tabs, keyed by session id.
|
||||
#[serde(default)]
|
||||
pub renamed_session_names: HashMap<String, String>,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
@@ -159,6 +166,10 @@ pub struct BedrockConfig {
|
||||
pub aws_bearer_token: Option<String>,
|
||||
pub model_id: Option<String>,
|
||||
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.
|
||||
@@ -199,6 +210,7 @@ impl Project {
|
||||
ollama_config: None,
|
||||
openai_compatible_config: None,
|
||||
allow_docker_access: false,
|
||||
sandbox_mode_enabled: false,
|
||||
mission_control_enabled: false,
|
||||
full_permissions: false,
|
||||
ssh_key_path: None,
|
||||
@@ -210,6 +222,7 @@ impl Project {
|
||||
claude_instructions: None,
|
||||
enabled_mcp_servers: Vec::new(),
|
||||
claude_code_settings: None,
|
||||
renamed_session_names: HashMap::new(),
|
||||
created_at: now.clone(),
|
||||
updated_at: now,
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import Sidebar from "./components/layout/Sidebar";
|
||||
import TopBar from "./components/layout/TopBar";
|
||||
import StatusBar from "./components/layout/StatusBar";
|
||||
import TerminalView from "./components/terminal/TerminalView";
|
||||
import DockerInstallDialog from "./components/DockerInstallDialog";
|
||||
import { useDocker } from "./hooks/useDocker";
|
||||
import { useSettings } from "./hooks/useSettings";
|
||||
import { useProjects } from "./hooks/useProjects";
|
||||
@@ -21,6 +22,7 @@ export default function App() {
|
||||
const { sessions, activeSessionId, setProjects } = useAppState(
|
||||
useShallow(s => ({ sessions: s.sessions, activeSessionId: s.activeSessionId, setProjects: s.setProjects }))
|
||||
);
|
||||
const [showInstallDialog, setShowInstallDialog] = useState(false);
|
||||
|
||||
// Initialize on mount
|
||||
useEffect(() => {
|
||||
@@ -38,6 +40,7 @@ export default function App() {
|
||||
refresh();
|
||||
});
|
||||
} else {
|
||||
setShowInstallDialog(true);
|
||||
stopPolling = startDockerPolling();
|
||||
}
|
||||
});
|
||||
@@ -80,6 +83,9 @@ export default function App() {
|
||||
</main>
|
||||
</div>
|
||||
<StatusBar />
|
||||
{showInstallDialog && (
|
||||
<DockerInstallDialog onClose={() => setShowInstallDialog(false)} />
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -8,6 +8,9 @@ vi.mock("../../store/appState", () => ({
|
||||
selector({
|
||||
sidebarView: "projects",
|
||||
setSidebarView: vi.fn(),
|
||||
sidebarCollapsed: false,
|
||||
setSidebarCollapsed: vi.fn(),
|
||||
toggleSidebarCollapsed: vi.fn(),
|
||||
})
|
||||
),
|
||||
}));
|
||||
|
||||
@@ -1,15 +1,100 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { useShallow } from "zustand/react/shallow";
|
||||
import { useAppState } from "../../store/appState";
|
||||
import ProjectList from "../projects/ProjectList";
|
||||
import McpPanel from "../mcp/McpPanel";
|
||||
import SettingsPanel from "../settings/SettingsPanel";
|
||||
|
||||
type SidebarView = "projects" | "mcp" | "settings";
|
||||
|
||||
const RAIL_ICONS: { view: SidebarView; label: string; icon: ReactNode }[] = [
|
||||
{
|
||||
view: "projects",
|
||||
label: "Projects",
|
||||
icon: (
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
view: "mcp",
|
||||
label: "MCP",
|
||||
icon: (
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M9 2v6" />
|
||||
<path d="M15 2v6" />
|
||||
<path d="M7 8h10v4a5 5 0 0 1-10 0V8z" />
|
||||
<path d="M12 17v5" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
view: "settings",
|
||||
label: "Settings",
|
||||
icon: (
|
||||
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
export default function Sidebar() {
|
||||
const { sidebarView, setSidebarView } = useAppState(
|
||||
useShallow(s => ({ sidebarView: s.sidebarView, setSidebarView: s.setSidebarView }))
|
||||
const { sidebarView, setSidebarView, sidebarCollapsed, setSidebarCollapsed, toggleSidebarCollapsed } = useAppState(
|
||||
useShallow(s => ({
|
||||
sidebarView: s.sidebarView,
|
||||
setSidebarView: s.setSidebarView,
|
||||
sidebarCollapsed: s.sidebarCollapsed,
|
||||
setSidebarCollapsed: s.setSidebarCollapsed,
|
||||
toggleSidebarCollapsed: s.toggleSidebarCollapsed,
|
||||
}))
|
||||
);
|
||||
|
||||
const tabCls = (view: typeof sidebarView) =>
|
||||
if (sidebarCollapsed) {
|
||||
const railBtn = (view: SidebarView, label: string, icon: ReactNode) => {
|
||||
const active = sidebarView === view;
|
||||
return (
|
||||
<button
|
||||
key={view}
|
||||
onClick={() => {
|
||||
setSidebarView(view);
|
||||
setSidebarCollapsed(false);
|
||||
}}
|
||||
title={label}
|
||||
aria-label={label}
|
||||
className={`flex items-center justify-center h-10 w-full transition-colors ${
|
||||
active
|
||||
? "text-[var(--accent)]"
|
||||
: "text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
||||
}`}
|
||||
>
|
||||
{icon}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full w-12 bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded-lg overflow-hidden">
|
||||
<button
|
||||
onClick={toggleSidebarCollapsed}
|
||||
title="Expand sidebar"
|
||||
aria-label="Expand sidebar"
|
||||
className="flex items-center justify-center h-10 border-b border-[var(--border-color)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="9 18 15 12 9 6" />
|
||||
</svg>
|
||||
</button>
|
||||
<div className="flex flex-col py-1">
|
||||
{RAIL_ICONS.map(({ view, label, icon }) => railBtn(view, label, icon))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const tabCls = (view: SidebarView) =>
|
||||
`flex-1 px-3 py-2 text-sm font-medium transition-colors ${
|
||||
sidebarView === view
|
||||
? "text-[var(--accent)] border-b-2 border-[var(--accent)]"
|
||||
@@ -29,6 +114,16 @@ export default function Sidebar() {
|
||||
<button onClick={() => setSidebarView("settings")} className={tabCls("settings")}>
|
||||
Settings
|
||||
</button>
|
||||
<button
|
||||
onClick={toggleSidebarCollapsed}
|
||||
title="Collapse sidebar"
|
||||
aria-label="Collapse sidebar"
|
||||
className="px-2 text-[var(--text-secondary)] hover:text-[var(--text-primary)] transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="15 18 9 12 15 6" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
|
||||
@@ -60,6 +60,7 @@ export default function ProjectCard({ project }: Props) {
|
||||
const [bedrockProfile, setBedrockProfile] = useState(project.bedrock_config?.aws_profile ?? "");
|
||||
const [bedrockBearerToken, setBedrockBearerToken] = useState(project.bedrock_config?.aws_bearer_token ?? "");
|
||||
const [bedrockModelId, setBedrockModelId] = useState(project.bedrock_config?.model_id ?? "");
|
||||
const [bedrockServiceTier, setBedrockServiceTier] = useState(project.bedrock_config?.service_tier ?? "");
|
||||
|
||||
// Ollama local state
|
||||
const [ollamaBaseUrl, setOllamaBaseUrl] = useState(project.ollama_config?.base_url ?? "http://host.docker.internal:11434");
|
||||
@@ -88,6 +89,7 @@ export default function ProjectCard({ project }: Props) {
|
||||
setBedrockProfile(project.bedrock_config?.aws_profile ?? "");
|
||||
setBedrockBearerToken(project.bedrock_config?.aws_bearer_token ?? "");
|
||||
setBedrockModelId(project.bedrock_config?.model_id ?? "");
|
||||
setBedrockServiceTier(project.bedrock_config?.service_tier ?? "");
|
||||
setOllamaBaseUrl(project.ollama_config?.base_url ?? "http://host.docker.internal:11434");
|
||||
setOllamaModelId(project.ollama_config?.model_id ?? "");
|
||||
setOpenaiCompatibleBaseUrl(project.openai_compatible_config?.base_url ?? "http://host.docker.internal:4000");
|
||||
@@ -192,6 +194,7 @@ export default function ProjectCard({ project }: Props) {
|
||||
aws_bearer_token: null,
|
||||
model_id: null,
|
||||
disable_prompt_caching: false,
|
||||
service_tier: null,
|
||||
};
|
||||
|
||||
const defaultOllamaConfig: OllamaConfig = {
|
||||
@@ -339,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 () => {
|
||||
try {
|
||||
const current = project.ollama_config ?? defaultOllamaConfig;
|
||||
@@ -692,6 +705,28 @@ export default function ProjectCard({ project }: Props) {
|
||||
</button>
|
||||
</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 */}
|
||||
<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>
|
||||
@@ -953,6 +988,19 @@ export default function ProjectCard({ project }: Props) {
|
||||
className={inputCls}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
})()}
|
||||
|
||||
@@ -12,6 +12,7 @@ export default function AwsSettings() {
|
||||
aws_config_path: null,
|
||||
aws_profile: null,
|
||||
aws_region: null,
|
||||
default_model_id: null,
|
||||
};
|
||||
|
||||
// Load profiles when component mounts or aws_config_path changes
|
||||
@@ -105,6 +106,18 @@ export default function AwsSettings() {
|
||||
className="w-full px-2 py-1.5 text-xs bg-[var(--bg-primary)] border border-[var(--border-color)] rounded focus:outline-none focus:border-[var(--accent)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Default Model ID */}
|
||||
<div>
|
||||
<span className="text-[var(--text-secondary)] text-xs block mb-1">Default Model ID<Tooltip text="Default Bedrock model ID. Used when a Bedrock project doesn't set its own Model ID." /></span>
|
||||
<input
|
||||
type="text"
|
||||
value={globalAws.default_model_id ?? ""}
|
||||
onChange={(e) => handleChange("default_model_id", e.target.value)}
|
||||
placeholder="anthropic.claude-sonnet-4-20250514-v1:0"
|
||||
className="w-full px-2 py-1.5 text-xs bg-[var(--bg-primary)] border border-[var(--border-color)] rounded focus:outline-none focus:border-[var(--accent)]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
53
app/src/components/settings/OllamaSettings.tsx
Normal file
53
app/src/components/settings/OllamaSettings.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { useSettings } from "../../hooks/useSettings";
|
||||
import Tooltip from "../ui/Tooltip";
|
||||
|
||||
export default function OllamaSettings() {
|
||||
const { appSettings, saveSettings } = useSettings();
|
||||
|
||||
const globalOllama = appSettings?.global_ollama ?? {
|
||||
base_url: null,
|
||||
default_model_id: null,
|
||||
};
|
||||
|
||||
const handleChange = async (field: "base_url" | "default_model_id", value: string) => {
|
||||
if (!appSettings) return;
|
||||
await saveSettings({
|
||||
...appSettings,
|
||||
global_ollama: { ...globalOllama, [field]: value || null },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Ollama Configuration</label>
|
||||
<div className="space-y-3 text-sm">
|
||||
<p className="text-xs text-[var(--text-secondary)]">
|
||||
Global Ollama defaults. Used when a per-project field is blank.
|
||||
Changes here require a container rebuild to take effect.
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<span className="text-[var(--text-secondary)] text-xs block mb-1">Default Base URL<Tooltip text="URL of your Ollama server. Used when a per-project Ollama base URL is blank." /></span>
|
||||
<input
|
||||
type="text"
|
||||
value={globalOllama.base_url ?? ""}
|
||||
onChange={(e) => handleChange("base_url", e.target.value)}
|
||||
placeholder="http://host.docker.internal:11434"
|
||||
className="w-full px-2 py-1.5 text-xs bg-[var(--bg-primary)] border border-[var(--border-color)] rounded focus:outline-none focus:border-[var(--accent)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="text-[var(--text-secondary)] text-xs block mb-1">Default Model<Tooltip text="Default Ollama model name. Used when a per-project Ollama model is blank." /></span>
|
||||
<input
|
||||
type="text"
|
||||
value={globalOllama.default_model_id ?? ""}
|
||||
onChange={(e) => handleChange("default_model_id", e.target.value)}
|
||||
placeholder="qwen3.5:27b"
|
||||
className="w-full px-2 py-1.5 text-xs bg-[var(--bg-primary)] border border-[var(--border-color)] rounded focus:outline-none focus:border-[var(--accent)]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
53
app/src/components/settings/OpenAiCompatibleSettings.tsx
Normal file
53
app/src/components/settings/OpenAiCompatibleSettings.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { useSettings } from "../../hooks/useSettings";
|
||||
import Tooltip from "../ui/Tooltip";
|
||||
|
||||
export default function OpenAiCompatibleSettings() {
|
||||
const { appSettings, saveSettings } = useSettings();
|
||||
|
||||
const globalOai = appSettings?.global_openai_compatible ?? {
|
||||
base_url: null,
|
||||
default_model_id: null,
|
||||
};
|
||||
|
||||
const handleChange = async (field: "base_url" | "default_model_id", value: string) => {
|
||||
if (!appSettings) return;
|
||||
await saveSettings({
|
||||
...appSettings,
|
||||
global_openai_compatible: { ...globalOai, [field]: value || null },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">OpenAI Compatible Configuration</label>
|
||||
<div className="space-y-3 text-sm">
|
||||
<p className="text-xs text-[var(--text-secondary)]">
|
||||
Global defaults for any OpenAI-compatible endpoint (LiteLLM, OpenRouter, vLLM, etc.).
|
||||
Used when a per-project field is blank. Changes require a container rebuild.
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<span className="text-[var(--text-secondary)] text-xs block mb-1">Default Base URL<Tooltip text="Default OpenAI-compatible endpoint URL. Used when a per-project base URL is blank." /></span>
|
||||
<input
|
||||
type="text"
|
||||
value={globalOai.base_url ?? ""}
|
||||
onChange={(e) => handleChange("base_url", e.target.value)}
|
||||
placeholder="http://host.docker.internal:4000"
|
||||
className="w-full px-2 py-1.5 text-xs bg-[var(--bg-primary)] border border-[var(--border-color)] rounded focus:outline-none focus:border-[var(--accent)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span className="text-[var(--text-secondary)] text-xs block mb-1">Default Model<Tooltip text="Default model identifier. Used when a per-project model is blank." /></span>
|
||||
<input
|
||||
type="text"
|
||||
value={globalOai.default_model_id ?? ""}
|
||||
onChange={(e) => handleChange("default_model_id", e.target.value)}
|
||||
placeholder="gpt-4o / gemini-pro / etc."
|
||||
className="w-full px-2 py-1.5 text-xs bg-[var(--bg-primary)] border border-[var(--border-color)] rounded focus:outline-none focus:border-[var(--accent)]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import DockerSettings from "./DockerSettings";
|
||||
import AwsSettings from "./AwsSettings";
|
||||
import OllamaSettings from "./OllamaSettings";
|
||||
import OpenAiCompatibleSettings from "./OpenAiCompatibleSettings";
|
||||
import { useSettings } from "../../hooks/useSettings";
|
||||
import { useUpdates } from "../../hooks/useUpdates";
|
||||
import ClaudeInstructionsModal from "../projects/ClaudeInstructionsModal";
|
||||
@@ -9,6 +11,7 @@ import EnvVarsModal from "../projects/EnvVarsModal";
|
||||
import { detectHostTimezone } from "../../lib/tauri-commands";
|
||||
import type { EnvVar } from "../../lib/types";
|
||||
import Tooltip from "../ui/Tooltip";
|
||||
import AccordionSection from "../ui/AccordionSection";
|
||||
import WebTerminalSettings from "./WebTerminalSettings";
|
||||
import SttSettings from "./SttSettings";
|
||||
|
||||
@@ -61,67 +64,12 @@ export default function SettingsPanel() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-4 space-y-6">
|
||||
<div className="p-4 space-y-3">
|
||||
<h2 className="text-xs font-semibold uppercase text-[var(--text-secondary)]">
|
||||
Settings
|
||||
</h2>
|
||||
<DockerSettings />
|
||||
<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>
|
||||
|
||||
<AccordionSection id="general" title="General">
|
||||
{/* Container Timezone */}
|
||||
<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>
|
||||
@@ -198,16 +146,82 @@ export default function SettingsPanel() {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionSection>
|
||||
|
||||
{/* Web Terminal */}
|
||||
<WebTerminalSettings />
|
||||
<AccordionSection id="backends" title="Backends" defaultOpen={false}>
|
||||
<AwsSettings />
|
||||
<div className="pt-3 border-t border-[var(--border-color)]" />
|
||||
<OllamaSettings />
|
||||
<div className="pt-3 border-t border-[var(--border-color)]" />
|
||||
<OpenAiCompatibleSettings />
|
||||
</AccordionSection>
|
||||
|
||||
{/* Speech to Text */}
|
||||
<SttSettings />
|
||||
<AccordionSection id="container" title="Container" defaultOpen={false}>
|
||||
<DockerSettings />
|
||||
</AccordionSection>
|
||||
|
||||
{/* Updates section */}
|
||||
<AccordionSection id="git-ssh" title="Git / SSH" defaultOpen={false}>
|
||||
{/* Default SSH Key Directory */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Updates<Tooltip text="Check for new versions of the Triple-C app and container image." /></label>
|
||||
<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>
|
||||
</AccordionSection>
|
||||
|
||||
<AccordionSection id="tools" title="Tools" defaultOpen={false}>
|
||||
<WebTerminalSettings />
|
||||
<SttSettings />
|
||||
</AccordionSection>
|
||||
|
||||
<AccordionSection id="updates" title="Updates" defaultOpen={false}>
|
||||
<div className="space-y-2">
|
||||
{appVersion && (
|
||||
<p className="text-xs text-[var(--text-secondary)]">
|
||||
@@ -237,11 +251,11 @@ export default function SettingsPanel() {
|
||||
{imageUpdateInfo && (
|
||||
<div className="flex items-center gap-2 px-3 py-2 text-xs bg-[var(--bg-primary)] border border-[var(--warning,#f59e0b)] rounded">
|
||||
<span className="inline-block w-2 h-2 rounded-full bg-[var(--warning,#f59e0b)]" />
|
||||
<span>A newer container image is available. Re-pull the image in Docker settings above to update.</span>
|
||||
<span>A newer container image is available. Re-pull the image in Container settings above to update.</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AccordionSection>
|
||||
|
||||
{showInstructionsModal && (
|
||||
<ClaudeInstructionsModal
|
||||
|
||||
@@ -1,7 +1,38 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTerminal } from "../../hooks/useTerminal";
|
||||
import { useProjects } from "../../hooks/useProjects";
|
||||
|
||||
interface ContextMenuState {
|
||||
sessionId: string;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export default function TerminalTabs() {
|
||||
const { sessions, activeSessionId, setActiveSession, close } = useTerminal();
|
||||
const { projects, update } = useProjects();
|
||||
const [menu, setMenu] = useState<ContextMenuState | null>(null);
|
||||
const [renamingId, setRenamingId] = useState<string | null>(null);
|
||||
const [renameDraft, setRenameDraft] = useState("");
|
||||
const renameInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!menu) return;
|
||||
const dismiss = () => setMenu(null);
|
||||
window.addEventListener("click", dismiss);
|
||||
window.addEventListener("scroll", dismiss, true);
|
||||
return () => {
|
||||
window.removeEventListener("click", dismiss);
|
||||
window.removeEventListener("scroll", dismiss, true);
|
||||
};
|
||||
}, [menu]);
|
||||
|
||||
useEffect(() => {
|
||||
if (renamingId) {
|
||||
renameInputRef.current?.focus();
|
||||
renameInputRef.current?.select();
|
||||
}
|
||||
}, [renamingId]);
|
||||
|
||||
if (sessions.length === 0) {
|
||||
return (
|
||||
@@ -11,21 +42,111 @@ export default function TerminalTabs() {
|
||||
);
|
||||
}
|
||||
|
||||
const getCustomName = (projectId: string, sessionId: string): string | null => {
|
||||
const project = projects.find((p) => p.id === projectId);
|
||||
return project?.renamed_session_names?.[sessionId] ?? null;
|
||||
};
|
||||
|
||||
const startRename = (sessionId: string) => {
|
||||
const session = sessions.find((s) => s.id === sessionId);
|
||||
if (!session) return;
|
||||
const current = getCustomName(session.projectId, sessionId) ?? session.sessionName ?? session.projectName;
|
||||
setRenameDraft(current);
|
||||
setRenamingId(sessionId);
|
||||
setMenu(null);
|
||||
};
|
||||
|
||||
const commitRename = async (sessionId: string) => {
|
||||
const session = sessions.find((s) => s.id === sessionId);
|
||||
if (!session) {
|
||||
setRenamingId(null);
|
||||
return;
|
||||
}
|
||||
const project = projects.find((p) => p.id === session.projectId);
|
||||
if (!project) {
|
||||
setRenamingId(null);
|
||||
return;
|
||||
}
|
||||
const trimmed = renameDraft.trim();
|
||||
const map = { ...(project.renamed_session_names ?? {}) };
|
||||
if (trimmed) {
|
||||
map[sessionId] = trimmed;
|
||||
} else {
|
||||
delete map[sessionId];
|
||||
}
|
||||
try {
|
||||
await update({ ...project, renamed_session_names: map });
|
||||
} catch (err) {
|
||||
console.error("Failed to rename terminal tab:", err);
|
||||
} finally {
|
||||
setRenamingId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const clearCustomName = async (sessionId: string) => {
|
||||
const session = sessions.find((s) => s.id === sessionId);
|
||||
if (!session) return;
|
||||
const project = projects.find((p) => p.id === session.projectId);
|
||||
if (!project) return;
|
||||
const map = { ...(project.renamed_session_names ?? {}) };
|
||||
if (!(sessionId in map)) {
|
||||
setMenu(null);
|
||||
return;
|
||||
}
|
||||
delete map[sessionId];
|
||||
try {
|
||||
await update({ ...project, renamed_session_names: map });
|
||||
} catch (err) {
|
||||
console.error("Failed to reset terminal tab name:", err);
|
||||
} finally {
|
||||
setMenu(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center h-full">
|
||||
{sessions.map((session) => (
|
||||
{sessions.map((session) => {
|
||||
const customName = getCustomName(session.projectId, session.id);
|
||||
const baseLabel =
|
||||
(session.sessionName ?? session.projectName) +
|
||||
(session.sessionType === "bash" ? " (bash)" : "");
|
||||
const displayLabel = customName
|
||||
? `${session.projectName}: ${customName}`
|
||||
: baseLabel;
|
||||
const isRenaming = renamingId === session.id;
|
||||
return (
|
||||
<div
|
||||
key={session.id}
|
||||
onClick={() => setActiveSession(session.id)}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
setMenu({ sessionId: session.id, x: e.clientX, y: e.clientY });
|
||||
}}
|
||||
onDoubleClick={() => startRename(session.id)}
|
||||
className={`flex items-center gap-2 px-3 h-full text-xs cursor-pointer border-r border-[var(--border-color)] transition-colors ${
|
||||
activeSessionId === session.id
|
||||
? "bg-[var(--bg-primary)] text-[var(--text-primary)]"
|
||||
: "text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
||||
}`}
|
||||
>
|
||||
<span className="truncate max-w-[140px]">
|
||||
{session.sessionName ?? session.projectName}{session.sessionType === "bash" ? " (bash)" : ""}
|
||||
{isRenaming ? (
|
||||
<input
|
||||
ref={renameInputRef}
|
||||
value={renameDraft}
|
||||
onChange={(e) => setRenameDraft(e.target.value)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onBlur={() => commitRename(session.id)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") (e.target as HTMLInputElement).blur();
|
||||
if (e.key === "Escape") setRenamingId(null);
|
||||
}}
|
||||
className="max-w-[180px] px-1 py-0 bg-[var(--bg-primary)] border border-[var(--accent)] rounded text-xs text-[var(--text-primary)] focus:outline-none"
|
||||
/>
|
||||
) : (
|
||||
<span className="truncate max-w-[200px]" title={displayLabel}>
|
||||
{displayLabel}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
@@ -37,7 +158,44 @@ export default function TerminalTabs() {
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
{menu && (() => {
|
||||
const session = sessions.find((s) => s.id === menu.sessionId);
|
||||
const hasCustom = session ? !!getCustomName(session.projectId, menu.sessionId) : false;
|
||||
return (
|
||||
<div
|
||||
className="fixed z-50 min-w-[160px] py-1 bg-[var(--bg-secondary)] border border-[var(--border-color)] rounded shadow-lg text-xs"
|
||||
style={{ top: menu.y, left: menu.x }}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<button
|
||||
className="w-full text-left px-3 py-1.5 text-[var(--text-primary)] hover:bg-[var(--bg-primary)] transition-colors"
|
||||
onClick={() => startRename(menu.sessionId)}
|
||||
>
|
||||
Rename tab
|
||||
</button>
|
||||
{hasCustom && (
|
||||
<button
|
||||
className="w-full text-left px-3 py-1.5 text-[var(--text-secondary)] hover:bg-[var(--bg-primary)] transition-colors"
|
||||
onClick={() => clearCustomName(menu.sessionId)}
|
||||
>
|
||||
Reset name
|
||||
</button>
|
||||
)}
|
||||
<div className="border-t border-[var(--border-color)] my-1" />
|
||||
<button
|
||||
className="w-full text-left px-3 py-1.5 text-[var(--error)] hover:bg-[var(--bg-primary)] transition-colors"
|
||||
onClick={() => {
|
||||
close(menu.sessionId);
|
||||
setMenu(null);
|
||||
}}
|
||||
>
|
||||
Close tab
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
65
app/src/components/ui/AccordionSection.tsx
Normal file
65
app/src/components/ui/AccordionSection.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { useEffect, useState, type ReactNode } from "react";
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
title: string;
|
||||
defaultOpen?: boolean;
|
||||
storageKey?: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
function loadOpenState(storageKey: string, fallback: boolean): boolean {
|
||||
try {
|
||||
const stored = localStorage.getItem(storageKey);
|
||||
if (stored === null) return fallback;
|
||||
return stored === "1";
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function persistOpenState(storageKey: string, open: boolean) {
|
||||
try {
|
||||
localStorage.setItem(storageKey, open ? "1" : "0");
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
export default function AccordionSection({ id, title, defaultOpen = true, storageKey, children }: Props) {
|
||||
const key = storageKey ?? `triple-c.accordion.${id}`;
|
||||
const [open, setOpen] = useState<boolean>(() => loadOpenState(key, defaultOpen));
|
||||
|
||||
useEffect(() => {
|
||||
persistOpenState(key, open);
|
||||
}, [key, open]);
|
||||
|
||||
return (
|
||||
<div className="border border-[var(--border-color)] rounded">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(o => !o)}
|
||||
aria-expanded={open}
|
||||
className="w-full flex items-center justify-between px-3 py-2 text-left text-sm font-medium text-[var(--text-primary)] hover:bg-[var(--bg-primary)] transition-colors"
|
||||
>
|
||||
<span>{title}</span>
|
||||
<svg
|
||||
className={`w-4 h-4 text-[var(--text-secondary)] transition-transform ${open ? "rotate-90" : ""}`}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<polyline points="9 18 15 12 9 6" />
|
||||
</svg>
|
||||
</button>
|
||||
{open && (
|
||||
<div className="px-3 py-3 border-t border-[var(--border-color)] space-y-4">
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
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 };
|
||||
}
|
||||
@@ -28,8 +28,25 @@ export function useTerminal() {
|
||||
|
||||
const close = useCallback(
|
||||
async (sessionId: string) => {
|
||||
// Capture session/project info before we drop it from local state.
|
||||
const { sessions: currentSessions, projects } = useAppState.getState();
|
||||
const session = currentSessions.find((s) => s.id === sessionId);
|
||||
const project = session ? projects.find((p) => p.id === session.projectId) : undefined;
|
||||
|
||||
await commands.closeTerminalSession(sessionId);
|
||||
removeSession(sessionId);
|
||||
|
||||
// Drop any persisted custom name for this session.
|
||||
if (project && project.renamed_session_names && sessionId in project.renamed_session_names) {
|
||||
const map = { ...project.renamed_session_names };
|
||||
delete map[sessionId];
|
||||
try {
|
||||
const updated = await commands.updateProject({ ...project, renamed_session_names: map });
|
||||
useAppState.getState().updateProjectInList(updated);
|
||||
} catch (err) {
|
||||
console.error("Failed to clear renamed tab name on close:", err);
|
||||
}
|
||||
}
|
||||
},
|
||||
[removeSession],
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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
|
||||
export const checkDocker = () => invoke<boolean>("check_docker");
|
||||
@@ -107,3 +107,8 @@ export const buildSttImage = () => invoke<void>("build_stt_image");
|
||||
export const pullSttImage = () => invoke<void>("pull_stt_image");
|
||||
export const transcribeAudio = (audioData: number[]) =>
|
||||
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;
|
||||
openai_compatible_config: OpenAiCompatibleConfig | null;
|
||||
allow_docker_access: boolean;
|
||||
sandbox_mode_enabled: boolean;
|
||||
mission_control_enabled: boolean;
|
||||
full_permissions: boolean;
|
||||
ssh_key_path: string | null;
|
||||
@@ -36,6 +37,7 @@ export interface Project {
|
||||
claude_instructions: string | null;
|
||||
enabled_mcp_servers: string[];
|
||||
claude_code_settings: ClaudeCodeSettings | null;
|
||||
renamed_session_names: Record<string, string>;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
@@ -61,6 +63,7 @@ export interface BedrockConfig {
|
||||
aws_bearer_token: string | null;
|
||||
model_id: string | null;
|
||||
disable_prompt_caching: boolean;
|
||||
service_tier: string | null;
|
||||
}
|
||||
|
||||
export interface OllamaConfig {
|
||||
@@ -114,6 +117,17 @@ export interface GlobalAwsSettings {
|
||||
aws_config_path: string | null;
|
||||
aws_profile: string | null;
|
||||
aws_region: string | null;
|
||||
default_model_id: string | null;
|
||||
}
|
||||
|
||||
export interface GlobalOllamaSettings {
|
||||
base_url: string | null;
|
||||
default_model_id: string | null;
|
||||
}
|
||||
|
||||
export interface GlobalOpenAiCompatibleSettings {
|
||||
base_url: string | null;
|
||||
default_model_id: string | null;
|
||||
}
|
||||
|
||||
export interface AppSettings {
|
||||
@@ -124,6 +138,8 @@ export interface AppSettings {
|
||||
image_source: ImageSource;
|
||||
custom_image_name: string | null;
|
||||
global_aws: GlobalAwsSettings;
|
||||
global_ollama: GlobalOllamaSettings;
|
||||
global_openai_compatible: GlobalOpenAiCompatibleSettings;
|
||||
global_claude_instructions: string | null;
|
||||
global_custom_env_vars: EnvVar[];
|
||||
auto_check_updates: boolean;
|
||||
@@ -211,3 +227,14 @@ export interface FileEntry {
|
||||
modified: 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[];
|
||||
}
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
import { create } from "zustand";
|
||||
import type { Project, TerminalSession, AppSettings, UpdateInfo, ImageUpdateInfo, McpServer } from "../lib/types";
|
||||
|
||||
const SIDEBAR_COLLAPSED_KEY = "triple-c.sidebar.collapsed";
|
||||
|
||||
function loadSidebarCollapsed(): boolean {
|
||||
try {
|
||||
return localStorage.getItem(SIDEBAR_COLLAPSED_KEY) === "1";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function persistSidebarCollapsed(value: boolean) {
|
||||
try {
|
||||
localStorage.setItem(SIDEBAR_COLLAPSED_KEY, value ? "1" : "0");
|
||||
} catch {
|
||||
// ignore — storage may be unavailable
|
||||
}
|
||||
}
|
||||
|
||||
interface AppState {
|
||||
// Projects
|
||||
projects: Project[];
|
||||
@@ -28,6 +46,9 @@ interface AppState {
|
||||
setTerminalHasSelection: (has: boolean) => void;
|
||||
sidebarView: "projects" | "mcp" | "settings";
|
||||
setSidebarView: (view: "projects" | "mcp" | "settings") => void;
|
||||
sidebarCollapsed: boolean;
|
||||
setSidebarCollapsed: (collapsed: boolean) => void;
|
||||
toggleSidebarCollapsed: () => void;
|
||||
dockerAvailable: boolean | null;
|
||||
setDockerAvailable: (available: boolean | null) => void;
|
||||
imageExists: boolean | null;
|
||||
@@ -106,6 +127,17 @@ export const useAppState = create<AppState>((set) => ({
|
||||
setTerminalHasSelection: (has) => set({ terminalHasSelection: has }),
|
||||
sidebarView: "projects",
|
||||
setSidebarView: (view) => set({ sidebarView: view }),
|
||||
sidebarCollapsed: loadSidebarCollapsed(),
|
||||
setSidebarCollapsed: (collapsed) => {
|
||||
persistSidebarCollapsed(collapsed);
|
||||
set({ sidebarCollapsed: collapsed });
|
||||
},
|
||||
toggleSidebarCollapsed: () =>
|
||||
set((state) => {
|
||||
const next = !state.sidebarCollapsed;
|
||||
persistSidebarCollapsed(next);
|
||||
return { sidebarCollapsed: next };
|
||||
}),
|
||||
dockerAvailable: null,
|
||||
setDockerAvailable: (available) => set({ dockerAvailable: available }),
|
||||
imageExists: null,
|
||||
|
||||
@@ -31,6 +31,8 @@ RUN for i in 1 2 3 4 5; do \
|
||||
pkg-config \
|
||||
libssl-dev \
|
||||
cron \
|
||||
bubblewrap \
|
||||
socat \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Remove default ubuntu user to free UID 1000 for host-user remapping
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
name: agentic-workflow
|
||||
description: Active orchestrator for multi-agent flight execution. Drives the full leg cycle (design, implement, review, commit) using three separate Claude instances.
|
||||
description: Active orchestrator for multi-agent flight execution. Drives leg design per leg, then batches implementation across all autonomous legs, with a single code review and commit at the end of the flight.
|
||||
---
|
||||
|
||||
# Agentic Workflow
|
||||
|
||||
Orchestrate multi-agent flight execution. You drive the full leg cycle — designing legs, spawning Developer and Reviewer agents, and managing git workflow — for a target project's flight.
|
||||
Orchestrate multi-agent flight execution. You drive the full leg cycle — designing legs, spawning Developer and Reviewer agents, and managing git workflow — for a target project's flight. Leg design is reviewed per leg, but code review and commit are deferred until after the last autonomous leg completes. This eliminates per-leg review/commit overhead while keeping the same leg design and implementation structure.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -33,8 +33,6 @@ Example: `/agentic-workflow flight 03 for epipen mission 04`
|
||||
6. **Read the flight log** — ground truth from prior execution
|
||||
7. **Count total legs** from the flight spec — track progress throughout
|
||||
8. **Determine starting point** — which leg is next based on flight log and leg statuses
|
||||
9. **Read git strategy** from `{target-project}/.flightops/ARTIFACTS.md` `## Git Workflow` section. Default to `branch` if the section is absent.
|
||||
10. **Set `{working-directory}`** — `branch`: the target project root; `worktree`: the worktree path (see Git Workflow section below)
|
||||
|
||||
**Mark flight as in-flight**: After loading the flight artifact, if the flight status is `ready`, update it to `in-flight` before proceeding. If already `in-flight`, leave it as-is.
|
||||
|
||||
@@ -46,13 +44,15 @@ If resuming a flight already in progress, verify state consistency:
|
||||
|
||||
Repeat for each leg in the flight.
|
||||
|
||||
**Mid-execution scope changes**: if the work in this flight stops serving its original purpose (operator pivots, prior assumptions invalidated), don't rewrite the mission/flight artifacts in place. Preserve the original framing as commentary, record the pivot decision in the flight-log Flight Director Notes with rationale, and treat the new framing as the live spec going forward. If the pivot supersedes content in an upstream artifact (maintenance report, prior debrief), annotate at the artifact header rather than rewriting the body — inspection records are snapshots, not living plans.
|
||||
|
||||
### 2a: Leg Design
|
||||
|
||||
1. **Design the leg** using the `/leg` skill (if the Skill tool is unavailable, read `.claude/skills/leg/SKILL.md` and follow the workflow directly)
|
||||
- Read the flight spec, flight log, and relevant source code
|
||||
- Create the leg artifact with acceptance criteria
|
||||
2. **Spawn a Developer agent for design review** (Task tool, `subagent_type: "general-purpose"`)
|
||||
- Working directory: `{working-directory}`
|
||||
- Working directory: `{target-project}`
|
||||
- Provide the "Review Leg Design" prompt from the leg-execution phase file's Prompts section
|
||||
- The Developer reads the leg artifact and cross-references against actual codebase state
|
||||
- The Developer provides a structured assessment: approve, approve with changes, or needs rework
|
||||
@@ -71,41 +71,42 @@ Repeat for each leg in the flight.
|
||||
|
||||
**NEVER implement code directly.** Spawn a Developer agent via the Task tool.
|
||||
|
||||
**Interactive/UAT legs**: If the leg is a UAT, alignment, or other interactive leg (identified by slug like `uat-*`, `alignment-*`, or explicit marking in the flight spec), do NOT spawn agents to execute it autonomously. The human performs verification — the Flight Director guides them through it:
|
||||
**Interactive/HAT legs**: If the leg is a HAT (human acceptance test), alignment, or other interactive leg (identified by slug like `hat-*`, `alignment-*`, or explicit marking in the flight spec), do NOT spawn agents to execute it autonomously. The human performs verification — the Flight Director guides them through it:
|
||||
1. **Design the leg** normally (2a), but keep it lightweight — the acceptance criteria are verification steps, not implementation tasks
|
||||
2. **Skip the autonomous implementation cycle** (no Developer/Reviewer agents)
|
||||
3. **Guide the human through verification steps one at a time** — present a single step, wait for the human to perform it and report results, then proceed to the next step
|
||||
4. **Fix issues inline** — if the human reports a failure, diagnose and fix it (spawning a Developer agent if code changes are needed), then re-verify that step before moving on
|
||||
5. **Commit when all steps pass** — spawn a Developer agent to update artifacts and commit
|
||||
5. **Commit when all steps pass** — update artifacts and commit
|
||||
|
||||
**Standard (autonomous) legs**: Follow the Developer/Reviewer cycle below.
|
||||
**Standard (autonomous) legs**: Spawn a Developer agent — but do NOT review or commit after each leg.
|
||||
|
||||
1. **Spawn a Developer agent** (Task tool, `subagent_type: "general-purpose"`)
|
||||
- Working directory: `{working-directory}`
|
||||
- Working directory: `{target-project}`
|
||||
- Provide the "Implement" prompt from the leg-execution phase file's Prompts section
|
||||
- The Developer updates leg status to `in-flight`, implements to acceptance criteria
|
||||
- When done, the Developer updates leg status to `landed`, updates flight log, and signals `[HANDOFF:review-needed]` — do NOT let it commit
|
||||
2. **Spawn a Reviewer agent** (Task tool, `subagent_type: "general-purpose"`)
|
||||
- Working directory: `{working-directory}`
|
||||
- Provide the "Review" prompt from the leg-execution phase file's Prompts section
|
||||
- The Reviewer evaluates ALL uncommitted changes against acceptance criteria and code quality
|
||||
- The Reviewer signals `[HANDOFF:confirmed]` or lists issues with severity
|
||||
3. **If issues found**, spawn a new Developer agent to fix them
|
||||
- Provide the "Fix Review Issues" prompt from the leg-execution phase file with the Reviewer's feedback
|
||||
- Loop review/fix until the Reviewer confirms
|
||||
4. **Spawn the Developer agent to commit** after review passes
|
||||
- Provide the "Commit" prompt from the leg-execution phase file's Prompts section
|
||||
- The commit must include code changes, updated flight log, and leg status updated to `completed`
|
||||
- When done, the Developer updates leg status to `landed` and updates flight log — do NOT let it commit or signal `[HANDOFF:review-needed]`
|
||||
|
||||
### 2c: Leg Transition
|
||||
|
||||
After `[COMPLETE:leg]` (all git/PR operations run from `{working-directory}`):
|
||||
After the Developer completes a leg:
|
||||
1. Increment `legs_completed`
|
||||
2. **Manage PR**:
|
||||
- **First leg**: Open a draft PR with the leg checklist in the body (see PR Body Format below), then check off the completed leg
|
||||
- **Subsequent legs**: Use `gh pr edit --body` to check off the newly completed leg in the existing PR body
|
||||
3. If more legs remain → return to 2a
|
||||
4. If all legs complete → proceed to Phase 3
|
||||
2. If more autonomous legs remain → return to 2a
|
||||
3. If this was the last autonomous leg → proceed to Phase 2d
|
||||
|
||||
### 2d: Flight Review and Commit
|
||||
|
||||
After all autonomous legs are implemented (all uncommitted):
|
||||
|
||||
1. **Spawn a Reviewer agent** (Task tool, `subagent_type: "general-purpose"`)
|
||||
- Working directory: `{target-project}`
|
||||
- Provide the "Review" prompt from the leg-execution phase file's Prompts section
|
||||
- The Reviewer evaluates ALL uncommitted changes against acceptance criteria and code quality
|
||||
- The Reviewer signals `[HANDOFF:confirmed]` or lists issues with severity
|
||||
2. **If issues found**, spawn a new Developer agent to fix them
|
||||
- Provide the "Fix Review Issues" prompt from the leg-execution phase file with the Reviewer's feedback
|
||||
- Loop review/fix until the Reviewer confirms
|
||||
3. **Commit** after review passes — include all code changes, updated flight log, and all leg statuses updated to `completed`
|
||||
4. **Manage PR**: Open a draft PR with the leg checklist in the body (see PR Body Format below), all legs checked off
|
||||
|
||||
## Phase 3: Flight Completion
|
||||
|
||||
@@ -114,8 +115,7 @@ After `[COMPLETE:leg]` (all git/PR operations run from `{working-directory}`):
|
||||
3. **Verify documentation** — check that CLAUDE.md, README, and other project docs reflect any new commands, endpoints, configuration, or APIs introduced during the flight. If not, spawn a Developer agent to update them.
|
||||
4. **Update flight status** to `landed`
|
||||
5. **Check off flight** in mission artifact
|
||||
6. **Clean up worktree** (worktree strategy only) — run `git worktree remove` after the PR is marked ready for review
|
||||
7. **Signal `[COMPLETE:flight]`**
|
||||
6. **Signal `[COMPLETE:flight]`**
|
||||
|
||||
The flight debrief is a separate step run via `/flight-debrief` after the flight lands. The debrief transitions the flight to `completed`.
|
||||
|
||||
@@ -141,33 +141,20 @@ Signals are part of the Flight Control methodology and are NOT configurable per-
|
||||
|
||||
## Flight Director Decision Log
|
||||
|
||||
The Flight Director must maintain transparency about its own decisions. After each major orchestration step, log what happened and why in the flight log under a `### Flight Director Notes` subsection:
|
||||
|
||||
1. **Phase file loading** — Record which phase file was loaded (project or default fallback) and what crew was extracted
|
||||
2. **Agent spawning** — Record which agent was spawned, with what prompt, and what model
|
||||
3. **Review cycle decisions** — When incorporating feedback, note what was accepted/rejected and why
|
||||
4. **Escalation decisions** — When choosing between "fix and re-review" vs "escalate to human," note the reasoning
|
||||
5. **Signal interpretation** — When a crew agent's output is ambiguous, note how it was interpreted
|
||||
|
||||
This is not a separate file — it goes in the flight log alongside leg entries. The goal is that anyone reviewing the flight log can understand not just what the crew did, but why the Flight Director made the orchestration choices it did.
|
||||
Log orchestration decisions in the flight log under `### Flight Director Notes` — phase file loaded, agents spawned, review-cycle calls, escalations, signal interpretations. Anyone reading the log should understand not just what the crew did but why MC made the choices it did.
|
||||
|
||||
## Git Workflow
|
||||
|
||||
### Strategy Selection
|
||||
|
||||
Read the `## Git Workflow` section from `{target-project}/.flightops/ARTIFACTS.md`. The `Strategy` property determines which workflow to use. If the section is absent, default to `branch`.
|
||||
|
||||
### Shared Elements
|
||||
|
||||
Both strategies use the same branch naming, commit format, PR lifecycle, and PR body format.
|
||||
All agents work in the target project root on a feature branch created at flight start.
|
||||
|
||||
**Branch naming**: `flight/{number}-{slug}`
|
||||
|
||||
**Flight start**: `git checkout -b flight/{number}-{slug}`
|
||||
|
||||
**Commit message format:**
|
||||
```
|
||||
leg/{number}: {description}
|
||||
flight/{number}: {description}
|
||||
|
||||
Flight: {flight-number}
|
||||
Mission: {mission-number}
|
||||
```
|
||||
|
||||
@@ -175,8 +162,7 @@ Mission: {mission-number}
|
||||
|
||||
| Event | Action |
|
||||
|-------|--------|
|
||||
| First leg complete | Open draft PR with leg checklist in body |
|
||||
| Each leg complete | Commit code + artifacts, update PR checklist |
|
||||
| All legs complete | Open draft PR with all legs checked off |
|
||||
| Flight landed | Mark PR ready for review |
|
||||
|
||||
**PR body format:**
|
||||
@@ -190,35 +176,10 @@ Mission: {mission-number}
|
||||
|
||||
## Legs
|
||||
|
||||
- [ ] `{leg-slug}` — {brief description}
|
||||
- [ ] `{leg-slug}` — {brief description}
|
||||
- [x] `{leg-slug}` — {brief description}
|
||||
- [x] `{leg-slug}` — {brief description}
|
||||
```
|
||||
|
||||
### Strategy: Branch
|
||||
|
||||
The default single-checkout workflow. One flight at a time per working copy.
|
||||
|
||||
| Step | Command |
|
||||
|------|---------|
|
||||
| Flight start | `git checkout -b flight/{number}-{slug}` |
|
||||
| Set `{working-directory}` | Target project root |
|
||||
| Agents work in | Project root |
|
||||
| Flight landed | PR marked ready for review |
|
||||
|
||||
### Strategy: Worktree
|
||||
|
||||
Worktree isolation enables parallel flights on a single repo clone.
|
||||
|
||||
| Step | Command |
|
||||
|------|---------|
|
||||
| Flight start | `git worktree add .worktrees/flight-{number}-{slug} -b flight/{number}-{slug}` |
|
||||
| Set `{working-directory}` | `.worktrees/flight-{number}-{slug}` |
|
||||
| Orchestrator stays on | Main branch (does not checkout the flight branch) |
|
||||
| Agents work in | Worktree path |
|
||||
| Flight landed | PR marked ready for review, then `git worktree remove .worktrees/flight-{number}-{slug}` |
|
||||
|
||||
**Note:** The `.worktrees/` directory must be in `.gitignore` when using this strategy.
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Situation | Action |
|
||||
@@ -229,5 +190,4 @@ Worktree isolation enables parallel flights on a single repo clone.
|
||||
| Leg marked aborted | Escalate to human with abort details |
|
||||
| Artifact discrepancy | Remediate before proceeding |
|
||||
| Off the rails | Roll back to last leg commit, escalate |
|
||||
| Stale worktree (worktree strategy) | Run `git worktree prune`, recreate if needed |
|
||||
| Agent hangs on tests | Kill the agent, spawn new Developer to isolate and fix hanging tests |
|
||||
|
||||
@@ -52,6 +52,7 @@ Read `{target-project}/.flightops/agent-crews/flight-debrief.md` for crew defini
|
||||
1. **Spawn a Developer agent** in the target project context (Task tool, `subagent_type: "general-purpose"`)
|
||||
- Provide the "Debrief Interview" prompt from the flight-debrief phase file's Prompts section
|
||||
- The Developer examines code changes, test coverage, patterns, and technical debt
|
||||
- In addition to the crew prompt, the Flight Director directly instructs the Developer to: run the full test suite once, capture per-suite wall-clock timing, pass/fail counts (with names of any failing tests), skipped/ignored counts, and any flakes observed on the run; then read prior flight debriefs in the project and compare current metrics against any earlier numbers they find. Report findings as narrative — naming suites, quoting deltas, and proposing likely causes when visible from this flight's changes.
|
||||
- The Developer provides structured debrief input
|
||||
|
||||
#### Architect Interview
|
||||
@@ -93,6 +94,12 @@ Synthesize Developer input, Architect input, human input, and document analysis
|
||||
- What technical debt was introduced?
|
||||
- Does implementation align with project conventions?
|
||||
|
||||
#### Test Metrics Capture
|
||||
- Run the full test suite as part of the debrief and record metrics: per-suite wall-clock timing, pass/fail counts, skipped/ignored tests, and any flakes observed on this run.
|
||||
- Read prior flight debriefs in this project for earlier metrics observations. Compare current numbers against whatever priors exist.
|
||||
- Surface meaningful changes — significant slowdowns, new failures, growing skip lists, recurring flakes — as narrative observations in whichever existing section best fits (Technical, Key Learnings, or What Could Be Improved).
|
||||
- If this is the first flight to capture metrics, the numbers seed comparisons for future flights.
|
||||
|
||||
#### Deviation Analysis
|
||||
- What deviations occurred and why?
|
||||
- Were deviations captured in the flight log?
|
||||
|
||||
@@ -37,6 +37,49 @@ Create a technical flight spec from a mission.
|
||||
- What's been completed vs. in progress?
|
||||
- Are there dependencies on other flights?
|
||||
|
||||
6. **Review recent flight debriefs for relevant observations**
|
||||
- Read recent flight debriefs in the project (within and adjacent to this mission)
|
||||
- Look for test metrics observations: known slow suites, recurring flakes, persistent failures, growing skip lists — anything narrated by prior debriefs
|
||||
- Look for unresolved technical concerns or recommendations that touch this flight's likely scope
|
||||
- Carry forward into the technical approach and risk picture: e.g. "this area has slow integration tests, account for that in time estimates"; "known flake in suite X, plan to fix or quarantine if touching that area"; "prior debrief flagged Y — verify whether still relevant"
|
||||
|
||||
### Phase 1b: Upstream Reconnaissance
|
||||
|
||||
**Applies when**: The flight sources work items from a prior artifact that cites specific code locations — a maintenance report, a flight or mission debrief that enumerates outstanding follow-ups, an issue tracker, or a security audit. Skip this phase for greenfield flights where no source artifact pre-enumerates findings.
|
||||
|
||||
Source artifacts go stale. Items cited weeks or even days ago may have been incidentally fixed by intervening flights, partially addressed, or the cited file/line may have moved. Without a recon pass, stale items get drafted into legs and only get caught at design review or implementation — wasted artifact churn and rework.
|
||||
|
||||
**Goal**: Before designing legs, walk every cited item against current code and classify it.
|
||||
|
||||
1. **Enumerate source items**
|
||||
- Read the source artifact in full and identify every item it treats as outstanding, actionable work — regardless of how the artifact organizes them. Different projects use different headings, severity labels, and status conventions; identify items by intent, not by literal section name.
|
||||
- Capture each item's cited file paths, line numbers, and the change it describes
|
||||
|
||||
2. **Verify each item against current code**
|
||||
- Read the cited locations (or grep for the cited symbols if line numbers have drifted)
|
||||
- Determine whether the described gap still exists
|
||||
|
||||
3. **Classify each item** into one of:
|
||||
- **`confirmed-live`** — gap still exists, item is real work
|
||||
- **`already-satisfied`** — code now reflects what the item asked for; recommend retiring
|
||||
- **`partially-satisfied`** — some sub-points done, others not; scope down
|
||||
- **`needs-human-recheck`** — cannot be verified mechanically (runtime state, external system, credentials, infrastructure that isn't in the repo)
|
||||
- **`drifted`** — cited location moved or symbol renamed; needs re-locating before classification
|
||||
|
||||
4. **Produce a Reconnaissance Report**
|
||||
- Append the report to the flight log under a clearly-titled section (something like `## Reconnaissance Report` if the project's flight-log conventions don't dictate otherwise)
|
||||
- One row per source item: `{item-id} | {classification} | {evidence: file:line or "cannot verify from repo"} | {recommendation}`
|
||||
- For `already-satisfied`: cite the specific code that satisfies the item, so the user can audit your call
|
||||
|
||||
5. **Default to flag-for-human, not auto-retire**
|
||||
- Do NOT silently drop items classified as `already-satisfied` or `partially-satisfied`
|
||||
- Present the recon report to the user before proceeding to Phase 2 and ask: "Confirm retirements / accept partial scope / override any classifications?"
|
||||
- The user has authority to keep an item live even if you think it's satisfied (e.g., the satisfying code is incomplete in ways you can't see)
|
||||
|
||||
6. **Carry retired items into the flight artifact**
|
||||
- In the section of the flight artifact the project uses to enumerate scope items contributing to mission criteria, retired items appear as completed `[x]` entries with the satisfying evidence inline — they are NOT silently dropped from the spec
|
||||
- This preserves traceability: a future reader can see all source items were considered, and which ones were judged already-satisfied during reconnaissance
|
||||
|
||||
### Phase 2: Code Interrogation
|
||||
|
||||
Explore the target project's codebase to inform the technical approach:
|
||||
@@ -158,9 +201,11 @@ Break flights into legs based on technical boundaries:
|
||||
|
||||
**For documentation**: Consider whether README, CLAUDE.md, or other docs need updates as part of this flight — especially for flights adding new CLI commands, API endpoints, or configuration options
|
||||
|
||||
**For carry-forward debt from prior debriefs**: when multiple small items touch the same files or directories, bundle them into a single shared-surface leg rather than scheduling separate flights or letting them decay. Items that don't share a surface stay separate — bundling unrelated work just to "pull forward" is how scope creep enters maintenance work.
|
||||
|
||||
**For schema changes**: Include explicit migration legs and verify against the live database, not just mocks
|
||||
|
||||
**For UAT and alignment**: During the crew interview, ask the user whether they'd like to include a UAT and alignment leg. Explain that this optional leg is a guided UAT session — the agent walks the user through a series of tests and verification steps, fixing issues along the way until the user is satisfied with the results. If the user opts in, include it in the breakdown, marked as optional.
|
||||
**For HAT and alignment**: During the crew interview, ask the user whether they'd like to include an interactive HAT (human acceptance test) and alignment leg. Explain that this optional leg is a guided HAT session — the agent walks the user through a series of tests and verification steps, fixing issues along the way until the user is satisfied with the results. If the user opts in, include it in the breakdown, marked as optional.
|
||||
|
||||
### Pre-Flight Rigor
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ Check for legacy directory layouts and offer to migrate them.
|
||||
2. **Run detection checks** for each migration in order (001, 002, ...)
|
||||
3. **If no migrations are needed**, proceed silently to the next step
|
||||
4. **If any migrations are needed**, present a summary to the user:
|
||||
> "Detected legacy directory layout in {project}. The following migrations are available:"
|
||||
> "Detected legacy directory layout in {target-project}. The following migrations are available:"
|
||||
>
|
||||
> - _Each applicable migration's user message_
|
||||
>
|
||||
@@ -61,13 +61,13 @@ The script outputs one of:
|
||||
Based on the status:
|
||||
|
||||
**If `missing`**:
|
||||
> "Flight operations directory not found. Create `{project}/.flightops/` with methodology references?"
|
||||
> "Flight operations directory not found. Create `{target-project}/.flightops/` with methodology references?"
|
||||
|
||||
**If `outdated`**:
|
||||
> "Flight operations references in {project} are outdated. Update?"
|
||||
> "Flight operations references in {target-project} are outdated. Update?"
|
||||
|
||||
**If `current`**:
|
||||
> "Flight operations references are up-to-date in {project}."
|
||||
> "Flight operations references are up-to-date in {target-project}."
|
||||
|
||||
If the user confirms, create/update the directory:
|
||||
|
||||
@@ -79,34 +79,13 @@ cp "${SKILL_DIR}/README.md" "{target-project}/.flightops/"
|
||||
|
||||
### 5. Configure Artifact System (New Projects Only)
|
||||
|
||||
**Only if ARTIFACTS.md doesn't exist**, ask the user to select an artifact system:
|
||||
|
||||
> "How should mission, flight, and leg artifacts be stored?"
|
||||
|
||||
Available templates:
|
||||
- **files** — Markdown files in the repository (`templates/ARTIFACTS-files.md`)
|
||||
- **jira** — Jira issues: Epics, Stories, Sub-tasks (`templates/ARTIFACTS-jira.md`)
|
||||
|
||||
#### 5a. Check for Setup Questions
|
||||
|
||||
After the user selects a template, read the template file and check if it contains a `## Setup Questions` section with a table of questions.
|
||||
|
||||
If setup questions exist:
|
||||
1. Parse the questions from the table (first column contains the questions)
|
||||
2. Ask the user each question interactively
|
||||
3. Replace the placeholder answers in the table with the user's responses
|
||||
|
||||
#### 5b. Copy and Populate Template
|
||||
|
||||
Copy the selected template, with answers populated if setup questions were asked:
|
||||
**Only if ARTIFACTS.md doesn't exist**, copy the template:
|
||||
|
||||
```bash
|
||||
cp "${SKILL_DIR}/templates/ARTIFACTS-{selection}.md" \
|
||||
cp "${SKILL_DIR}/templates/ARTIFACTS-files.md" \
|
||||
"{target-project}/.flightops/ARTIFACTS.md"
|
||||
```
|
||||
|
||||
If setup questions were answered, update the ARTIFACTS.md file to replace the placeholder answers with the user's responses.
|
||||
|
||||
**If ARTIFACTS.md already exists**, do not modify it — it's project-specific and may have been customized.
|
||||
|
||||
### 6. Configure Project Crew
|
||||
@@ -166,7 +145,7 @@ This project uses [Flight Control](https://github.com/msieurthenardier/mission-c
|
||||
|
||||
After creating or updating the directory, inform the user:
|
||||
|
||||
> "If you have Claude Code running in {project}, restart it to pick up the new flight operations references."
|
||||
> "If you have Claude Code running in {target-project}, restart it to pick up the new flight operations references."
|
||||
|
||||
This ensures Claude Code loads the new files into its context when working in the target project.
|
||||
|
||||
@@ -175,7 +154,7 @@ This ensures Claude Code loads the new files into its context when working in th
|
||||
This skill creates/updates the following at project root:
|
||||
|
||||
```
|
||||
{project}/
|
||||
{target-project}/
|
||||
├── CLAUDE.md # Updated with Flight Operations section
|
||||
└── .flightops/ # Hidden directory for Flight Control
|
||||
├── README.md # Explains the directory purpose
|
||||
|
||||
@@ -6,7 +6,7 @@ human and project-side agents to capture both execution and design perspectives.
|
||||
## Crew
|
||||
|
||||
### Developer
|
||||
- **Context**: {project}/
|
||||
- **Context**: {target-project}/
|
||||
- **Model**: Sonnet
|
||||
- **Role**: Provides developer perspective on flight execution. Reviews what was
|
||||
built, identifies technical debt introduced, evaluates implementation quality,
|
||||
@@ -14,7 +14,7 @@ human and project-side agents to capture both execution and design perspectives.
|
||||
- **Actions**: debrief-interview
|
||||
|
||||
### Architect
|
||||
- **Context**: {project}/
|
||||
- **Context**: {target-project}/
|
||||
- **Model**: Sonnet
|
||||
- **Role**: Closes the design feedback loop. Evaluates whether the design decisions
|
||||
made during flight planning held up in practice. Reviews architectural impact of
|
||||
|
||||
@@ -6,7 +6,7 @@ technical spec and uses project-side agents to validate against the real codebas
|
||||
## Crew
|
||||
|
||||
### Architect
|
||||
- **Context**: {project}/
|
||||
- **Context**: {target-project}/
|
||||
- **Model**: Sonnet
|
||||
- **Role**: Reviews flight specs for technical soundness. Validates design
|
||||
decisions, prerequisites, technical approach, and leg breakdown against
|
||||
@@ -56,6 +56,20 @@ Evaluate:
|
||||
5. Codebase state — does the spec account for current working tree, existing tooling,
|
||||
and conventions that might affect implementation?
|
||||
6. Architecture — does the approach maintain or improve system structure?
|
||||
7. State-machine reachability — for every state, status, or lifecycle value the flight
|
||||
introduces or relies on (e.g. "agent_deleted", "draft", "queued"), audit which
|
||||
infrastructure layers could foreclose it: DB constraints (FK ON DELETE behaviors,
|
||||
NOT NULL, CHECK), application caches and their invalidation rules, API/protocol
|
||||
versions, fallback handlers that mask the state, and existing tests that pin
|
||||
contradictory behavior. A state that the schema or a cache can silently prevent
|
||||
is a design hole, not an implementation detail.
|
||||
8. Cache freshness contracts — for every cache (in-memory dict, query result cache,
|
||||
derived state, frontend session storage) the flight reads from or populates,
|
||||
the design must declare source of truth, rebuild trigger (per-call / TTL /
|
||||
invalidation event / accepted permanent staleness), maximum staleness, and which
|
||||
user actions should invalidate it. Vague answers ("eventually", "on next cycle")
|
||||
without a concrete trigger are a flag. Conflating "cached object works" with
|
||||
"cached object reflects current source" is a common category error worth catching.
|
||||
|
||||
Provide structured output:
|
||||
|
||||
|
||||
@@ -7,21 +7,21 @@ The Flight Director (Mission Control) orchestrates this phase using the
|
||||
## Crew
|
||||
|
||||
### Developer
|
||||
- **Context**: {working-directory}/
|
||||
- **Context**: {target-project}/
|
||||
- **Model**: Sonnet
|
||||
- **Role**: Implements code changes. Also performs design reviews against real
|
||||
codebase to validate leg specs before implementation.
|
||||
- **Actions**: implement, fix-review-issues, commit, review-leg-design
|
||||
|
||||
### Reviewer
|
||||
- **Context**: {working-directory}/
|
||||
- **Context**: {target-project}/
|
||||
- **Model**: Sonnet (NEVER Opus)
|
||||
- **Role**: Reviews code changes for quality, correctness, and criteria compliance.
|
||||
Has NO knowledge of Developer's reasoning — only sees resulting changes.
|
||||
- **Actions**: review
|
||||
|
||||
### Accessibility Reviewer (optional)
|
||||
- **Context**: {working-directory}/
|
||||
- **Context**: {target-project}/
|
||||
- **Model**: Sonnet
|
||||
- **Enabled**: false
|
||||
- **Role**: Reviews UI changes for accessibility compliance. Evaluates against
|
||||
@@ -72,7 +72,6 @@ The Flight Director substitutes these variables in prompts at runtime:
|
||||
| `{flight-number}` | Current flight number | All prompts |
|
||||
| `{leg-number}` | Current leg number | Leg-scoped prompts |
|
||||
| `{leg-artifact-path}` | Path to the leg artifact file | review-leg-design |
|
||||
| `{working-directory}` | Resolved working directory for the agent (project root for branch strategy, worktree path for worktree strategy) | All prompts |
|
||||
| `{reviewer-issues}` | Full text of reviewer feedback (dynamic) | fix-review-issues |
|
||||
|
||||
## Prompts
|
||||
|
||||
@@ -6,7 +6,7 @@ both the human and a project-side Architect to capture strategic technical persp
|
||||
## Crew
|
||||
|
||||
### Architect
|
||||
- **Context**: {project}/
|
||||
- **Context**: {target-project}/
|
||||
- **Model**: Sonnet
|
||||
- **Role**: Provides architectural perspective on mission outcomes. Evaluates
|
||||
whether the system evolved well across flights, identifies structural issues,
|
||||
|
||||
@@ -6,7 +6,7 @@ and uses project-side agents to validate technical viability.
|
||||
## Crew
|
||||
|
||||
### Architect
|
||||
- **Context**: {project}/
|
||||
- **Context**: {target-project}/
|
||||
- **Model**: Sonnet
|
||||
- **Role**: Validates technical viability of proposed outcomes. Ensures business
|
||||
goals align with what's actually possible given the codebase, stack, and
|
||||
|
||||
@@ -7,7 +7,7 @@ for severity assessment and roundtable moderation.
|
||||
## Crew
|
||||
|
||||
### Inspector
|
||||
- **Context**: {project}/
|
||||
- **Context**: {target-project}/
|
||||
- **Model**: Sonnet
|
||||
- **Role**: Performs broad read-only codebase inspection across all applicable
|
||||
categories. Runs test suites, linters, type checkers, audit commands, and
|
||||
@@ -15,7 +15,7 @@ for severity assessment and roundtable moderation.
|
||||
- **Actions**: inspect-codebase
|
||||
|
||||
### Security Reviewer
|
||||
- **Context**: {project}/
|
||||
- **Context**: {target-project}/
|
||||
- **Model**: Sonnet
|
||||
- **Role**: Performs focused manual security review of authentication flows,
|
||||
injection surfaces, secrets handling, CORS/CSP configuration, and data
|
||||
@@ -24,7 +24,7 @@ for severity assessment and roundtable moderation.
|
||||
- **Actions**: review-security
|
||||
|
||||
### CI/CD Reviewer (optional)
|
||||
- **Context**: {project}/
|
||||
- **Context**: {target-project}/
|
||||
- **Model**: Sonnet
|
||||
- **Enabled**: false (enable when project has CI/CD pipelines)
|
||||
- **Role**: Reviews CI/CD pipeline configuration, build security, deployment
|
||||
@@ -33,7 +33,7 @@ for severity assessment and roundtable moderation.
|
||||
- **Actions**: review-cicd
|
||||
|
||||
### Accessibility Reviewer (optional)
|
||||
- **Context**: {project}/
|
||||
- **Context**: {target-project}/
|
||||
- **Model**: Sonnet
|
||||
- **Enabled**: false (enable when project has user-facing UI)
|
||||
- **Role**: Reviews codebase for accessibility compliance against WCAG 2.1 AA
|
||||
@@ -42,7 +42,7 @@ for severity assessment and roundtable moderation.
|
||||
- **Actions**: review-accessibility
|
||||
|
||||
### Architect
|
||||
- **Context**: {project}/
|
||||
- **Context**: {target-project}/
|
||||
- **Model**: Opus
|
||||
- **Role**: Reviews all reviewer findings alongside debrief context. Assigns
|
||||
severity per finding, challenges questionable assessments, moderates
|
||||
@@ -463,6 +463,7 @@ For each finding, assign one of:
|
||||
- Does this finding represent a real risk, or is it noise?
|
||||
- Is the severity proportional to the actual impact?
|
||||
- Would this compound if left for another cycle?
|
||||
- Is the infrastructure or framing this finding pertains to still serving its original purpose, or has it drifted into "maybe-someday" territory?
|
||||
- Is this a new discovery or previously acknowledged debt?
|
||||
- Do multiple reviewers corroborate the same issue?
|
||||
- Are any reviewer assessments questionable — too alarmist or too dismissive?
|
||||
|
||||
@@ -5,7 +5,7 @@ This project stores Flight Control artifacts as markdown files in the repository
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
{project}/
|
||||
{target-project}/
|
||||
├── missions/
|
||||
│ └── {NN}-{mission-slug}/
|
||||
│ ├── mission.md
|
||||
@@ -159,7 +159,7 @@ How the objective will be achieved.
|
||||
|
||||
- [ ] `{leg-slug}` - {Brief description}
|
||||
- [ ] `{leg-slug}` - {Brief description}
|
||||
- [ ] `uat-and-alignment` *(optional)* - Guided UAT session with iterative fixes
|
||||
- [ ] `hat-and-alignment` *(optional)* - Guided HAT (human acceptance test) session with iterative fixes
|
||||
|
||||
---
|
||||
|
||||
@@ -560,16 +560,3 @@ States are tracked in the frontmatter or status field of each artifact:
|
||||
- **Flight briefings**: Created before execution, not modified after
|
||||
- **Debriefs**: Created after completion, may be updated with follow-up notes
|
||||
- **Mission as briefing**: The mission.md document serves as both definition and briefing (no separate mission-briefing.md)
|
||||
|
||||
## Git Workflow
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Strategy | `branch` |
|
||||
|
||||
**Options:**
|
||||
|
||||
- **`branch`** (default) — Single-checkout workflow. The orchestrator creates a feature branch and all agents work in the project root. One flight at a time per working copy.
|
||||
- **`worktree`** — Worktree isolation. The orchestrator creates a git worktree under `.worktrees/` for each flight. Agents work in the worktree path. Parallel flights are possible on a single repo clone.
|
||||
|
||||
When using the `worktree` strategy, add `.worktrees/` to `.gitignore`.
|
||||
|
||||
@@ -1,408 +0,0 @@
|
||||
# Artifact System: Jira
|
||||
|
||||
This project stores Flight Control artifacts as Jira issues.
|
||||
|
||||
## Issue Type Mapping
|
||||
|
||||
| Flight Control | Jira Issue Type | Hierarchy |
|
||||
|----------------|-----------------|-----------|
|
||||
| Mission | Epic | Parent |
|
||||
| Flight | Story | Child of Epic |
|
||||
| Leg | Sub-task | Child of Story |
|
||||
|
||||
## Setup Questions
|
||||
|
||||
Answer these questions when configuring Jira artifacts for your project:
|
||||
|
||||
| Question | Answer |
|
||||
|----------|--------|
|
||||
| What is the Jira project key? | `PROJECT` |
|
||||
| JQL query for discovering flight documentation? | (e.g., `project = PROJECT AND labels = flight-control`) |
|
||||
|
||||
## Configuration
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Project Key | `PROJECT` |
|
||||
| Board | (specify board name or ID) |
|
||||
| Labels | `flight-control` |
|
||||
|
||||
---
|
||||
|
||||
## Custom Fields
|
||||
|
||||
<!-- Add your project's custom Jira fields here -->
|
||||
|
||||
| Custom Field | Jira Field ID | Required | Used For | Notes |
|
||||
|--------------|---------------|----------|----------|-------|
|
||||
| (example) Team | `customfield_10001` | Yes | All issues | Select from predefined teams |
|
||||
| (example) Sprint | `customfield_10002` | No | Stories, Sub-tasks | Assign to sprint |
|
||||
|
||||
## Project Rules
|
||||
|
||||
<!-- Document project-specific Jira rules and conventions here -->
|
||||
|
||||
### Required Fields by Issue Type
|
||||
|
||||
**Epic (Mission):**
|
||||
- (list required fields for your project)
|
||||
|
||||
**Story (Flight):**
|
||||
- (list required fields for your project)
|
||||
|
||||
**Sub-task (Leg):**
|
||||
- (list required fields for your project)
|
||||
|
||||
### Workflow Rules
|
||||
|
||||
- (document any workflow restrictions or automation rules)
|
||||
- (e.g., "Stories cannot move to In Progress without Epic Link")
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
- (document any naming patterns required by your project)
|
||||
- (e.g., "Epic summaries must start with [MISSION]")
|
||||
|
||||
---
|
||||
|
||||
## Core Artifacts
|
||||
|
||||
### Mission → Epic
|
||||
|
||||
| Field | Mapping |
|
||||
|-------|---------|
|
||||
| Summary | Mission title |
|
||||
| Description | See format below |
|
||||
| Labels | `flight-control`, `mission` |
|
||||
|
||||
**Description Format:**
|
||||
|
||||
```
|
||||
## Outcome
|
||||
{What success looks like in human terms}
|
||||
|
||||
## Context
|
||||
{Why this mission matters now}
|
||||
|
||||
## Success Criteria
|
||||
- [ ] {Criterion 1}
|
||||
- [ ] {Criterion 2}
|
||||
|
||||
## Stakeholders
|
||||
{Who cares about this outcome}
|
||||
|
||||
## Constraints
|
||||
{Non-negotiable boundaries}
|
||||
|
||||
## Environment Requirements
|
||||
{Development and runtime requirements}
|
||||
|
||||
## Open Questions
|
||||
{Unknowns needing resolution}
|
||||
|
||||
## Known Issues
|
||||
Emergent blockers and issues discovered during execution. Add items here as flights surface problems that affect the broader mission — things not anticipated during planning but visible at the mission level.
|
||||
|
||||
- [ ] {Issue description} — discovered in Flight {N}, affects {scope}
|
||||
|
||||
## Flights
|
||||
> **Note:** These are tentative suggestions, not commitments. Flights are planned and created one at a time as work progresses. This list will evolve based on discoveries during implementation.
|
||||
|
||||
- [ ] Flight 1: {description}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Flight → Story
|
||||
|
||||
| Field | Mapping |
|
||||
|-------|---------|
|
||||
| Summary | Flight title |
|
||||
| Description | See format below |
|
||||
| Epic Link | Parent mission epic |
|
||||
| Labels | `flight-control`, `flight` |
|
||||
|
||||
**Description Format:**
|
||||
|
||||
```
|
||||
## Objective
|
||||
{What this flight accomplishes}
|
||||
|
||||
## Contributing to Criteria
|
||||
- {Relevant success criterion 1}
|
||||
- {Relevant success criterion 2}
|
||||
|
||||
## Design Decisions
|
||||
{Key technical decisions and rationale}
|
||||
|
||||
## Prerequisites
|
||||
- [ ] {What must be true before execution}
|
||||
|
||||
## Technical Approach
|
||||
{How the objective will be achieved}
|
||||
|
||||
## Legs
|
||||
> **Note:** These are tentative suggestions, not commitments. Legs are planned and created one at a time as the flight progresses. This list will evolve based on discoveries during implementation.
|
||||
|
||||
- [ ] {leg-slug} - {description}
|
||||
|
||||
## Validation Approach
|
||||
{How will this flight be validated? Manual testing, automated tests, or both?}
|
||||
|
||||
## Verification
|
||||
{How to confirm success}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Leg → Sub-task
|
||||
|
||||
| Field | Mapping |
|
||||
|-------|---------|
|
||||
| Summary | Leg title |
|
||||
| Description | See format below |
|
||||
| Parent | Flight story |
|
||||
| Labels | `flight-control`, `leg` |
|
||||
|
||||
**Description Format:**
|
||||
|
||||
```
|
||||
## Objective
|
||||
{Single sentence: what this leg accomplishes}
|
||||
|
||||
## Context
|
||||
{Design decisions and learnings from prior legs}
|
||||
|
||||
## Inputs
|
||||
{What must exist before this leg runs}
|
||||
|
||||
## Outputs
|
||||
{What exists after completion}
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] {Criterion 1}
|
||||
- [ ] {Criterion 2}
|
||||
|
||||
## Verification Steps
|
||||
How to confirm each criterion is met:
|
||||
- {Command or manual check for criterion 1}
|
||||
- {Command or manual check for criterion 2}
|
||||
|
||||
## Implementation Guidance
|
||||
{Step-by-step guidance}
|
||||
|
||||
## Files Affected
|
||||
{List of files to modify}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Supporting Artifacts
|
||||
|
||||
### Flight Log → Story Comments
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Location | Comments on the Flight (Story) |
|
||||
| Format | Timestamped comments with prefix |
|
||||
| Update pattern | Append new comments during execution |
|
||||
|
||||
**Comment Format:**
|
||||
|
||||
```
|
||||
[Flight Log] {YYYY-MM-DD HH:MM}
|
||||
|
||||
## {Entry Type}: {Title}
|
||||
|
||||
{Content based on entry type - see below}
|
||||
```
|
||||
|
||||
**Entry Types:**
|
||||
|
||||
- `Leg Progress` - Status updates for leg completion
|
||||
- `Decision` - Runtime decisions not in original plan
|
||||
- `Deviation` - Departures from planned approach
|
||||
- `Anomaly` - Unexpected issues encountered
|
||||
- `Session Notes` - General progress notes
|
||||
|
||||
---
|
||||
|
||||
### Flight Briefing → Story Comment
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Location | Comment on the Flight (Story) |
|
||||
| Created | Before flight execution begins |
|
||||
| Label | `[Flight Briefing]` |
|
||||
|
||||
**Comment Format:**
|
||||
|
||||
```
|
||||
[Flight Briefing] {YYYY-MM-DD}
|
||||
|
||||
## Mission Context
|
||||
{How this flight contributes to mission}
|
||||
|
||||
## Objective
|
||||
{What this flight will accomplish}
|
||||
|
||||
## Key Decisions
|
||||
{Critical decisions crew should know}
|
||||
|
||||
## Risks
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| {risk} | {mitigation} |
|
||||
|
||||
## Legs Overview
|
||||
1. {leg} - {description}
|
||||
2. {leg} - {description}
|
||||
|
||||
## Success Criteria
|
||||
{How we'll know the flight succeeded}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Flight Debrief → Story Comment
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Location | Comment on the Flight (Story) |
|
||||
| Created | After flight lands or diverts |
|
||||
| Label | `[Flight Debrief]` |
|
||||
|
||||
**Comment Format:**
|
||||
|
||||
```
|
||||
[Flight Debrief] {YYYY-MM-DD}
|
||||
|
||||
**Status**: {landed | aborted}
|
||||
**Duration**: {start} - {end}
|
||||
**Legs Completed**: {X of Y}
|
||||
|
||||
## Outcome Assessment
|
||||
{What the flight accomplished}
|
||||
|
||||
## What Went Well
|
||||
{Effective patterns}
|
||||
|
||||
## What Could Be Improved
|
||||
{Recommendations}
|
||||
|
||||
## Deviations
|
||||
| Deviation | Reason | Standardize? |
|
||||
|-----------|--------|--------------|
|
||||
| {what} | {why} | {yes/no} |
|
||||
|
||||
## Key Learnings
|
||||
{Insights for future flights}
|
||||
|
||||
## Recommendations
|
||||
1. {Most impactful recommendation}
|
||||
2. {Second recommendation}
|
||||
3. {Third recommendation}
|
||||
|
||||
## Action Items
|
||||
- [ ] {action}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Mission Debrief → Epic Comment
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Location | Comment on the Mission (Epic) |
|
||||
| Created | After mission completes or aborts |
|
||||
| Label | `[Mission Debrief]` |
|
||||
|
||||
**Comment Format:**
|
||||
|
||||
```
|
||||
[Mission Debrief] {YYYY-MM-DD}
|
||||
|
||||
**Status**: {completed | aborted}
|
||||
**Duration**: {start} - {end}
|
||||
**Flights Completed**: {X of Y}
|
||||
|
||||
## Success Criteria Results
|
||||
| Criterion | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| {criterion} | {met/not met} | {notes} |
|
||||
|
||||
## Flight Summary
|
||||
| Flight | Status | Outcome |
|
||||
|--------|--------|---------|
|
||||
| {flight} | {status} | {outcome} |
|
||||
|
||||
## What Went Well
|
||||
{Successes}
|
||||
|
||||
## What Could Be Improved
|
||||
{Improvements}
|
||||
|
||||
## Lessons Learned
|
||||
{Insights}
|
||||
|
||||
## Action Items
|
||||
- [ ] {action}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## State Mapping
|
||||
|
||||
### Mission (Epic)
|
||||
|
||||
| Flight Control | Jira Status |
|
||||
|----------------|-------------|
|
||||
| planning | To Do |
|
||||
| active | In Progress |
|
||||
| completed | Done |
|
||||
| aborted | Cancelled |
|
||||
|
||||
### Flight (Story)
|
||||
|
||||
| Flight Control | Jira Status |
|
||||
|----------------|-------------|
|
||||
| planning | To Do |
|
||||
| ready | Ready |
|
||||
| in-flight | In Progress |
|
||||
| landed | In Review |
|
||||
| completed | Done |
|
||||
| aborted | Cancelled |
|
||||
|
||||
### Leg (Sub-task)
|
||||
|
||||
| Flight Control | Jira Status |
|
||||
|----------------|-------------|
|
||||
| planning | To Do |
|
||||
| ready | Ready |
|
||||
| in-flight | In Progress |
|
||||
| landed | In Review |
|
||||
| completed | Done |
|
||||
| aborted | Cancelled |
|
||||
|
||||
---
|
||||
|
||||
## Conventions
|
||||
|
||||
- **Naming**: Use clear, action-oriented summaries
|
||||
- **Linking**: Always link Stories to Epic, Sub-tasks to Story
|
||||
- **Labels**: Apply `flight-control` label to all artifacts
|
||||
- **Immutability**: Never modify Sub-tasks once In Progress; create new ones
|
||||
- **Comments**: Use prefixes (`[Flight Log]`, `[Flight Briefing]`, etc.) for filtering
|
||||
|
||||
## Git Workflow
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Strategy | `branch` |
|
||||
|
||||
**Options:**
|
||||
|
||||
- **`branch`** (default) — Single-checkout workflow. The orchestrator creates a feature branch and all agents work in the project root. One flight at a time per working copy.
|
||||
- **`worktree`** — Worktree isolation. The orchestrator creates a git worktree under `.worktrees/` for each flight. Agents work in the worktree path. Parallel flights are possible on a single repo clone.
|
||||
|
||||
When using the `worktree` strategy, add `.worktrees/` to `.gitignore`.
|
||||
@@ -79,12 +79,31 @@ Deep dive into the specific implementation:
|
||||
- What error handling is required?
|
||||
- If this leg modifies database schemas: does it include migration creation AND execution? Both must happen in the same leg — a schema defined but never migrated is a gap.
|
||||
|
||||
5. **Identify dependent code** (for interface changes)
|
||||
5. **State-machine reachability audit**
|
||||
|
||||
For every state, status, or lifecycle value this leg introduces or relies on, verify nothing in a lower layer makes the state unreachable.
|
||||
|
||||
- Enumerate the infrastructure layers that could foreclose the state: DB constraints (FK ON DELETE behaviors, NOT NULL, CHECK constraints, triggers), application caches and their invalidation rules, API/protocol version compatibility, fallback handlers that silently mask the state, and indexes that imply assumptions.
|
||||
- Audit existing tests for assertions that contradict the new design. A test that pins behavior the new state machine breaks must be inverted, renamed, or deleted as part of this leg — not after. The rename pattern (e.g., `test_X_does_Y` → `test_X_does_not_do_Y` with the assertion inverted) is preferred over delete-and-readd because it documents the intent shift in git blame.
|
||||
- When a design requires a row to persist past a referenced entity's deletion, explicitly inspect the schema's FK behaviors and any tests that pin them. A `ON DELETE CASCADE` on a referencing column will silently delete the row your design needs to keep.
|
||||
|
||||
6. **Cache freshness contract**
|
||||
|
||||
For every cache (in-memory dict, query result cache, computed derived state, frontend session storage) this leg reads from or populates, declare an explicit freshness contract.
|
||||
|
||||
- **Source of truth**: which underlying data does this cache reflect?
|
||||
- **Rebuild trigger**: pick exactly one — per-call rebuild, TTL with value, invalidation event, or accepted permanent staleness until process restart.
|
||||
- **Maximum staleness acceptable to the user**: be specific. "Until process restart" is rarely acceptable for user-facing state where users expect their edits to apply.
|
||||
- **User-action invalidation map**: list every user action that mutates the source-of-truth, and confirm each one invalidates or refreshes the cache. If a user can edit X in one UI surface but the cached X never sees the edit elsewhere, that mismatch will surface as a bug — name it now.
|
||||
|
||||
Conflating "the cached object works fine" with "the cached object reflects current config" is a common error. State health and state freshness are different contracts.
|
||||
|
||||
7. **Identify dependent code** (for interface changes)
|
||||
- Does this leg modify shared interfaces?
|
||||
- What files consume these interfaces?
|
||||
- What files consume these interfaces? Run `grep -rn '<changed_symbol>' tests/ src/`; if any test or non-leg-scope source imports or calls it, signature changes break any "prior tests pass UNMODIFIED" acceptance criterion.
|
||||
- Should updating consumers be part of this leg?
|
||||
|
||||
6. **Identify platform considerations**
|
||||
8. **Identify platform considerations**
|
||||
- Does this leg touch OS-specific features?
|
||||
- What platform differences might affect implementation?
|
||||
|
||||
@@ -92,6 +111,34 @@ Deep dive into the specific implementation:
|
||||
|
||||
Create the leg artifact using the format defined in `.flightops/ARTIFACTS.md`.
|
||||
|
||||
### Phase 3b: Citation Verification
|
||||
|
||||
Before marking the leg `ready`, mechanically validate every code-location citation in the draft artifact against current code. Citations drift between when a flight is designed and when its legs are designed (intervening legs commit changes; source artifacts age). Catching drift here prevents the implementing agent from chasing a stale `file:line` into the wrong code.
|
||||
|
||||
1. **Extract citations**
|
||||
- Scan the leg artifact for code-location references matching `path/to/file.ext:line` or `path/to/file.ext:line-line` patterns
|
||||
- Also collect symbol-form citations: `path/to/file.ext:symbol_name`
|
||||
- Skip references to non-source artifacts (`mission.md:42`, `flight.md:100`) — those are out of scope
|
||||
|
||||
2. **Verify each citation**
|
||||
- Read the cited location in current code
|
||||
- Compare against the snippet, symbol, or surrounding description provided in the leg
|
||||
- Classify each:
|
||||
- **`OK`** — content at the cited location matches the description
|
||||
- **`drifted`** — content moved; the description is still accurate but the line number is wrong
|
||||
- **`gone`** — described content no longer exists in the file (or the file itself is gone)
|
||||
- **`unverifiable`** — citation has no snippet/symbol and the description is too vague to confirm
|
||||
|
||||
3. **Repair drift inline**
|
||||
- For `drifted`: locate the new line number via grep on the snippet/symbol and update the citation in the leg artifact
|
||||
- For `gone`: do not silently retire — flag for human review (the gap may have been independently fixed, OR the leg may now be obsolete)
|
||||
- For `unverifiable`: rewrite the citation using one of the durable forms (see "Citing Code Locations" guideline)
|
||||
|
||||
4. **Append a Citation Audit summary**
|
||||
- At the bottom of the leg artifact, append a clearly-titled section (something like `## Citation Audit` if the project's leg conventions don't dictate otherwise) summarizing the audit
|
||||
- If all citations verified clean: one sentence — `N citations verified against current code at leg design time.`
|
||||
- If any drift was repaired or flagged: list each one with classification and resolution
|
||||
|
||||
## Guidelines
|
||||
|
||||
### Writing Effective Objectives
|
||||
@@ -132,6 +179,21 @@ For accessibility work, include specific checks:
|
||||
- Screen reader commands to test
|
||||
- Automated tool commands (Lighthouse, axe-core)
|
||||
|
||||
### Citing Code Locations
|
||||
|
||||
When the leg artifact references specific code, prefer durable forms over bare line numbers. Line numbers drift; symbols and snippets do not.
|
||||
|
||||
| Form | Example | When to use |
|
||||
|------|---------|-------------|
|
||||
| `file:symbol` | `the_one/api.py:create_provider` | Most cases — symbol names survive line shifts |
|
||||
| `file:line — "snippet"` | `the_one/api.py:805 — "raise ProviderConfigError"` | When a specific line matters; the snippet is a self-verifier |
|
||||
| `file:CONSTANT_NAME` | `web/middleware.py:GATED_METHODS` | Module-level constants and assignments |
|
||||
| `file:line` (bare) | `api.py:805` | **Avoid** — brittle, no way to verify drift |
|
||||
|
||||
The snippet form is especially valuable: it lets Phase 3b mechanically confirm the cited content didn't move, and it tells the implementing agent exactly what they're looking at without needing to chase the line number.
|
||||
|
||||
When in doubt, include both — `the_one/api.py:805 (in create_provider) — "raise ProviderConfigError if base_url is empty"` — symbol + line + snippet covers all three drift modes.
|
||||
|
||||
### Implementation Guidance
|
||||
|
||||
Be explicit, not implicit:
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
name: routine-maintenance
|
||||
description: Codebase health assessment and maintenance recommendation. Use after a mission or ad-hoc to verify codebase is flight-ready or scaffold a maintenance mission.
|
||||
description: Post-mission codebase health assessment. Run after `/mission-debrief` to verify the codebase is flight-ready or scaffold a maintenance mission. Per-flight findings instead roll into the next flight or accumulate into an end-of-mission maintenance flight — not into this skill.
|
||||
---
|
||||
|
||||
# Routine Maintenance
|
||||
|
||||
Perform an exhaustive, aviation-style codebase inspection. Can be triggered after a mission completes or run ad-hoc at any time. Produces a findings report and optionally scaffolds a maintenance mission for significant issues.
|
||||
Perform an exhaustive, aviation-style codebase inspection after a mission completes. Produces a findings report and optionally scaffolds a maintenance mission for significant issues.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -31,12 +31,11 @@ Perform an exhaustive, aviation-style codebase inspection. Can be triggered afte
|
||||
- Deferred findings are those documented in prior reports but not addressed by a maintenance mission
|
||||
- This ensures recurring issues are tracked across cycles rather than re-discovered as "new"
|
||||
|
||||
5. **Load mission and debrief documentation (if available)**
|
||||
- If a recent mission exists, read it for outcome, success criteria, and known issues
|
||||
- If a mission debrief exists, read it for lessons learned and action items
|
||||
- If flight debriefs exist, read them for per-flight technical debt and recommendations
|
||||
5. **Load mission and debrief documentation**
|
||||
- Read the most recently completed mission for outcome, success criteria, and known issues
|
||||
- Read its mission debrief for lessons learned and action items
|
||||
- Read its flight debriefs for per-flight technical debt and recommendations
|
||||
- This provides known-debt context so the inspection can distinguish new issues from acknowledged ones
|
||||
- If no mission context is available (ad-hoc run), proceed without known-debt context
|
||||
|
||||
6. **Identify project stack**
|
||||
- Read `README.md`, `CLAUDE.md`, and package files (`package.json`, `Cargo.toml`, `go.mod`, etc.)
|
||||
@@ -178,6 +177,7 @@ Each agent receives:
|
||||
- Provide the "Inspect Codebase" prompt from the crew file's Prompts section
|
||||
- Include: applicable category list, project stack info, known debt from debriefs, user's areas of concern, and **scope assignment from the delegation plan**
|
||||
- For partitioned inspections: each Inspector agent receives its module/area scope and only the categories relevant to its assignment
|
||||
- In addition to the crew prompt, the Flight Director directly instructs the Inspector to: scan recent flight debriefs for any test metrics observations (timing, failures, skipped tests, flakes) and look for trends across them — rising suite times, recurring flakes, growing skip lists, persistent failures. Surface concrete optimization recommendations as Category 2 findings (e.g. parallelization, mocking, fixture hoisting, slow-test extraction, runner config), each tied to evidence from the trend.
|
||||
- The Inspector performs broad automated checks and returns structured findings per category
|
||||
|
||||
#### Security Reviewer
|
||||
|
||||
1
container/mission-control/.gitignore
vendored
1
container/mission-control/.gitignore
vendored
@@ -2,6 +2,7 @@
|
||||
projects.md
|
||||
daily-briefings/
|
||||
.claude/settings.local.json
|
||||
.mcp.json
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
|
||||
@@ -49,7 +49,7 @@ Eleven skills automate the planning, execution, debrief, and oversight workflow:
|
||||
| `/mission` | Create outcome-driven missions through research and interview |
|
||||
| `/flight` | Create technical flight specs from missions |
|
||||
| `/leg` | Generate implementation guidance for LLM execution |
|
||||
| `/agentic-workflow` | Drive multi-agent flight execution (design, implement, review, commit) |
|
||||
| `/agentic-workflow` | Drive multi-agent flight execution (design per leg, batch implement, single review and commit) |
|
||||
| `/flight-debrief` | Post-flight analysis for continuous improvement |
|
||||
| `/mission-debrief` | Post-mission retrospective for outcomes assessment |
|
||||
| `/routine-maintenance` | Post-mission codebase health assessment and maintenance recommendation |
|
||||
@@ -90,6 +90,27 @@ The registry provides:
|
||||
- **Flights**: `planning` → `ready` → `in-flight` → `landed` → `completed` (or `aborted`)
|
||||
- **Legs**: `planning` → `ready` → `in-flight` → `landed` → `completed` (or `aborted`)
|
||||
|
||||
## Skill–Project Boundary
|
||||
|
||||
Mission Control skills run in projects whose owners can customize `.flightops/ARTIFACTS.md` and `.flightops/agent-crews/*.md` freely. Skills must not couple to project-owned shape:
|
||||
|
||||
- **Do not read project-owned artifacts by section heading.** When a skill needs to extract information from a prior debrief, maintenance report, or other project-owned artifact, frame the instruction by intent — what the agent is looking for — and let the agent locate it within whatever structure the project uses. Reading by literal heading name (e.g. `## Action Items`, `## Test Suite Timing`) breaks silently the moment a project owner renames or removes that section.
|
||||
- **Do not write into project-owned artifacts at named anchors.** When a skill inserts content into a project artifact, describe the destination semantically ("in the section the project uses for X") rather than by literal heading. If the skill is appending a new section, suggest a heading without prescribing it as a contract.
|
||||
- **Do not rely on crew prompt files to carry skill-required instructions.** The Flight Director must issue per-spawn instructions directly from the SKILL.md, even when the crew file also contains an overlapping prompt. Crew files are project-modifiable scaffolding; SKILL.md is the protocol.
|
||||
|
||||
See `docs/artifacts-md-ambiguities.md` for the full review of how the current ARTIFACTS.md template muddles this boundary.
|
||||
|
||||
## Project Information Stays in Project Artifacts
|
||||
|
||||
**Never store project-specific information in Claude Code memories** — not in mission-control's memory directory, not in any project's memory directory. Project-specific issues, bugs, technical debt, design gaps, known issues, and lessons learned belong exclusively in the project's own Flight Control artifacts:
|
||||
|
||||
- **Flight logs** — runtime decisions, deviations, anomalies
|
||||
- **Flight debriefs** — post-flight analysis, recommendations, action items
|
||||
- **Mission known issues** — cross-flight concerns discovered during execution
|
||||
- **Design decision sections** — in flight and mission artifacts
|
||||
|
||||
Mission-control is a neutral methodology tool. Its memory (if used at all) is reserved for methodology preferences, user collaboration preferences, and cross-cutting tooling notes — never for project-specific content.
|
||||
|
||||
## Public Repository
|
||||
|
||||
This is a public repository. Keep all committed content anonymized:
|
||||
|
||||
@@ -32,7 +32,7 @@ Aviation succeeds through layered planning and clear handoffs. Pilots follow fli
|
||||
|
||||
## Agentic Workflow
|
||||
|
||||
**LLM orchestrators**: Run `/agentic-workflow` to drive multi-agent flight execution with Claude Code. The skill orchestrates the full leg cycle — design, implement, review, commit — using three separate Claude instances.
|
||||
**LLM orchestrators**: Run `/agentic-workflow` to drive multi-agent flight execution with Claude Code. The skill designs and implements each leg in turn, then runs a single code review and commit across the whole flight, using separate Claude instances for the Flight Director, Developer, and Reviewer roles.
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -49,13 +49,13 @@ Aviation succeeds through layered planning and clear handoffs. Pilots follow fli
|
||||
|
||||
3. **Initialize your project** — Run `/init-project` and select your project. This creates `.flightops/` in your target project with artifact configuration, methodology reference, and crew definitions.
|
||||
|
||||
4. **Review agent crew files** — Check the files in `{project}/.flightops/agent-crews/`. These define the crew composition (roles, models, prompts) for each phase. Customize them to your needs.
|
||||
4. **Review agent crew files** — Check the files in `{target-project}/.flightops/agent-crews/`. These define the crew composition (roles, models, prompts) for each phase. Customize them to your needs.
|
||||
|
||||
5. **Create a mission** — Run `/mission`. This interviews you about desired outcomes and creates a mission artifact in your target project.
|
||||
|
||||
6. **Design a flight** — Run `/flight` to break the mission into a technical specification with pre/in/post-flight checklists.
|
||||
|
||||
7. **Execute** — Run `/agentic-workflow` to drive multi-agent implementation. This orchestrates design, implement, review, and commit cycles across legs.
|
||||
7. **Execute** — Run `/agentic-workflow` to drive multi-agent implementation. This designs and implements each leg in turn, then reviews and commits the whole flight in one pass at the end.
|
||||
|
||||
8. **Debrief** — Run `/flight-debrief` and `/mission-debrief` after completion to capture lessons learned.
|
||||
|
||||
@@ -106,13 +106,7 @@ Mission
|
||||
└── Leg
|
||||
```
|
||||
|
||||
How you store these artifacts depends on your project's needs. Flight Control supports multiple artifact systems:
|
||||
|
||||
- **Markdown files** — Version-controlled documentation in your repository
|
||||
- **Issue trackers** — Jira, Linear, GitHub Issues with linked relationships
|
||||
- **Hybrid** — Missions in markdown, flights/legs as tickets
|
||||
|
||||
Each project configures its artifact system during initialization. The methodology and Claude Code skills adapt to your choice.
|
||||
By default, artifacts are stored as version-controlled markdown files in your project's repository. Each project's `.flightops/ARTIFACTS.md` describes where and how artifacts live — skills read this file to determine locations and formats. You can adapt it to other backends (Jira, Linear, GitHub Issues, hybrid setups) by editing this file directly; only the markdown-files template ships out of the box.
|
||||
|
||||
## Claude Code Skills
|
||||
|
||||
|
||||
91
container/mission-control/docs/artifacts-md-ambiguities.md
Normal file
91
container/mission-control/docs/artifacts-md-ambiguities.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# ARTIFACTS.md Template — Ambiguities
|
||||
|
||||
A review of `.claude/skills/init-project/templates/ARTIFACTS-files.md` (the template copied into each project's `.flightops/ARTIFACTS.md`) against the implicit contract Mission Control skills rely on.
|
||||
|
||||
## Context
|
||||
|
||||
Mission Control skills make assumptions about what they can read, write, and depend on inside a project. ARTIFACTS.md is the document that's supposed to encode the contract between skills and project-side customization. In practice, the template muddles three different layers:
|
||||
|
||||
1. **Mission Control protocol invariants** — fixed across all projects; skills depend on these by name (state values, lifecycle rules, artifact taxonomy).
|
||||
2. **Project conventions skills must read** — locations, file naming, status field encoding.
|
||||
3. **Project presentation** — section structure, prose templates, headings; never read by skills.
|
||||
|
||||
The template doesn't distinguish between these layers, which causes two failure modes:
|
||||
|
||||
- Project owners cannot tell what they can safely customize and what is fixed.
|
||||
- Skill authors are not steered away from depending on project-owned shape (the most recent example: a draft flight-debrief change that read prior debriefs by `## Test Suite Timing` heading — a heading that lives in the project-owned format and could be removed or renamed at any time).
|
||||
|
||||
This document lists the specific ambiguities that motivated the finding.
|
||||
|
||||
## Findings
|
||||
|
||||
### 1. State values are protocol but presented as project-customizable
|
||||
|
||||
The `## State Tracking` table lists `planning → ready → in-flight → landed → completed (or aborted)` as part of the project's local convention. Skills depend on these literal strings:
|
||||
|
||||
- `flight-debrief/SKILL.md`: "A flight must have status `landed` before debriefing"; "update the flight artifact's status from `landed` to `completed`"
|
||||
- `routine-maintenance/SKILL.md`: scaffolds new flights with `Status: ready`
|
||||
|
||||
If a project owner renames `landed` to `arrived` in their ARTIFACTS.md, skills break silently. The template gives no signal that this list is fixed by the protocol.
|
||||
|
||||
### 2. State transition ownership is unspecified
|
||||
|
||||
The state diagrams show transitions but don't say who triggers each one:
|
||||
|
||||
- `planning → active` for missions: which actor performs this? No skill currently does it.
|
||||
- `ready → in-flight` for legs: skill or human?
|
||||
- `landed → completed`: `flight-debrief` does this conditionally on user confirmation.
|
||||
|
||||
A project owner reading the template cannot tell which transitions they own vs which the skills handle.
|
||||
|
||||
### 3. "Format" is overloaded
|
||||
|
||||
Skills routinely say "use the format defined in `.flightops/ARTIFACTS.md`." The template conflates three different things under that one word:
|
||||
|
||||
- **Locations** (`missions/{NN}-{slug}/mission.md`) — skills genuinely depend on these
|
||||
- **Status field encoding** (e.g. `**Status**: planning`) — skills need to read this
|
||||
- **Section structure** (`## Outcome`, `## Context`, etc.) — skills should NOT depend on this
|
||||
|
||||
There is no rule stated anywhere that skills read locations and status fields but never read by section heading. Without that rule, every new skill edit risks coupling itself to project-owned section structure.
|
||||
|
||||
### 4. Conventions section mixes protocol invariants with format choices
|
||||
|
||||
In `## Conventions` (lines 556–562 of the template):
|
||||
|
||||
- "Never modify legs once `in-flight`" — protocol invariant; project owners cannot opt out
|
||||
- "Flight logs are append-only during execution" — protocol invariant
|
||||
- "Mission as briefing: mission.md serves as both" — project choice (a different project might have a separate briefing file)
|
||||
|
||||
A project owner cannot tell which lines are invariants vs which are policies they accepted at init time and could revise.
|
||||
|
||||
### 5. The taxonomy itself isn't declared fixed
|
||||
|
||||
The artifact types — Mission, Flight, Leg, Debrief, Log, Briefing, Maintenance Report — are protocol concepts. Skills speak their names. But the template is structured as if it's describing one project's choices ("This project stores Flight Control artifacts as markdown files"), which invites a reader to think they could rename "Flight" to "Sprint" or drop "Leg" entirely.
|
||||
|
||||
### 6. Status field placement is ambiguously specified
|
||||
|
||||
"States are tracked in the frontmatter or status field of each artifact." Skills need a deterministic read path. If a project mixes the two — frontmatter for missions but `**Status**:` lines for flights — every skill that reads status must handle both. The template offers a choice without forcing the project to commit to one.
|
||||
|
||||
### 7. Template placeholders read ambiguously
|
||||
|
||||
`{NN}`, `{slug}`, `{Title}` — are these substituted by skills at artifact-generation time, or by the project owner during init configuration? They are the former, but the file presents them as if they are placeholders awaiting human substitution. A naive project owner might "fill in" `{slug}` with a literal value.
|
||||
|
||||
### 8. The init-project / migrations contradiction
|
||||
|
||||
`init-project/SKILL.md` says ARTIFACTS.md is project-specific and "Never overwrite — it may contain customizations." But `init-project/migrations.md` describes Mission Control modifying ARTIFACTS.md to update state definitions. Mission Control reserves the right to edit a "project-owned" file when its own protocol changes. The template does not disclose this boundary.
|
||||
|
||||
### 9. No statement of the skill/project contract
|
||||
|
||||
The largest gap. Nowhere does the template say *what skills will and will not do with this file*. Adding a preamble that explicitly states the contract would resolve most of the issues above:
|
||||
|
||||
> Mission Control skills read this file for: artifact locations, file naming, and status field encoding. Skills do not read artifacts by section heading. The State Tracking values and lifecycle rules are fixed by Mission Control and should not be edited. Everything else is yours to customize.
|
||||
|
||||
## Proposed direction (not yet implemented)
|
||||
|
||||
A small restructure of the template that sorts every line into one of three buckets:
|
||||
|
||||
- **`[protocol: do not edit]`** — state values, lifecycle invariants, artifact taxonomy
|
||||
- **`[project: skills read this]`** — locations, file naming, status field encoding
|
||||
- **`[project: presentation only]`** — section structure, prose, headings
|
||||
|
||||
Plus a preamble that names the skill/project contract explicitly. This clarifies the boundary in the document where new project owners and new skill authors actually look.
|
||||
Reference in New Issue
Block a user