From 7a51cbd5e5758535059c8e5a0a3764bfdec8875c Mon Sep 17 00:00:00 2001 From: Andy <119136210+AndyMik90@users.noreply.github.com> Date: Fri, 26 Dec 2025 13:47:19 +0100 Subject: [PATCH] Fix/2.7.2 fixes (#300) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(frontend): prevent false stuck detection for ai_review tasks Tasks in ai_review status were incorrectly showing "Task Appears Stuck" in the detail modal. This happened because the isRunning check included ai_review status, triggering stuck detection when no process was found. However, ai_review means "all subtasks completed, awaiting QA" - no build process is expected to be running. This aligns the detail modal logic with TaskCard which correctly only checks for in_progress status. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 * ci(beta-release): use tag-based versioning instead of modifying package.json Previously the beta-release workflow committed version changes to package.json on the develop branch, which caused two issues: 1. Permission errors (github-actions[bot] denied push access) 2. Beta versions polluted develop, making merges to main unclean Now the workflow creates only a git tag and injects the version at build time using electron-builder's --config.extraMetadata.version flag. This keeps package.json at the next stable version and avoids any commits to develop. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 * fix(ollama): add packaged app path resolution for Ollama detector script The Ollama detection was failing in packaged builds because the Python script path resolution only checked development paths. In packaged apps, __dirname points to the app bundle, and the relative path "../../../backend" doesn't resolve correctly. Added process.resourcesPath for packaged builds (checked first via app.isPackaged) which correctly locates the backend scripts in the Resources folder. Also added DEBUG-only logging to help troubleshoot script location issues. Closes #129 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 * refactor(paths): remove legacy auto-claude path fallbacks Replace all legacy 'auto-claude/' source path detection with 'apps/backend'. Services now validate paths using runners/spec_runner.py as the marker instead of requirements.txt, ensuring only valid backend directories match. - Remove legacy fallback paths from all getAutoBuildSourcePath() implementations - Add startup validation in index.ts to skip invalid saved paths - Update project-initializer to detect apps/backend for local dev projects - Standardize path detection across all services 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 * fix(frontend): address PR review feedback from Auto Claude and bots Fixes from PR #300 reviews: CRITICAL: - path-resolver.ts: Update marker from requirements.txt to runners/spec_runner.py for consistent backend detection across all files HIGH: - useTaskDetail.ts: Restore stuck task detection for ai_review status (CHANGELOG documents this feature) - TerminalGrid.tsx: Include legacy terminals without projectPath (prevents hiding terminals after upgrade) - memory-handlers.ts: Add packaged app path in OLLAMA_PULL_MODEL handler (fixes production builds) MEDIUM: - OAuthStep.tsx: Stricter profile slug sanitization (only allow alphanumeric and dashes) - project-store.ts: Fix regex to not truncate at # in code blocks (uses \n#{1,6}\s to match valid markdown headings only) - memory-service.ts: Add backend structure validation with spec_runner.py marker - subprocess-spawn.test.ts: Update test to use new marker pattern 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 * fix(frontend): validate empty profile slug after sanitization Add validation to prevent empty config directory path when profile name contains only special characters (e.g., "!!!"). Shows user-friendly error message requiring at least one letter or number. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --------- Co-authored-by: Claude Opus 4.5 --- .github/workflows/beta-release.yml | 86 ++++++++----------- .../integration/subprocess-spawn.test.ts | 7 +- apps/frontend/src/main/agent/agent-process.ts | 14 +-- .../src/main/changelog/changelog-service.ts | 11 +-- apps/frontend/src/main/index.ts | 17 +++- apps/frontend/src/main/insights/config.ts | 11 +-- .../src/main/ipc-handlers/memory-handlers.ts | 33 ++++--- .../main/ipc-handlers/settings-handlers.ts | 22 ++--- apps/frontend/src/main/memory-service.ts | 16 ++-- apps/frontend/src/main/project-initializer.ts | 12 +-- apps/frontend/src/main/project-store.ts | 6 +- .../src/main/terminal-name-generator.ts | 11 +-- apps/frontend/src/main/title-generator.ts | 11 +-- .../src/main/updater/path-resolver.ts | 14 +-- .../src/main/updater/version-manager.ts | 8 +- apps/frontend/src/renderer/App.tsx | 13 +-- .../src/renderer/components/TerminalGrid.tsx | 14 ++- .../components/onboarding/OAuthStep.tsx | 39 ++++++++- .../components/task-detail/TaskMetadata.tsx | 12 +-- .../task-detail/hooks/useTaskDetail.ts | 12 +-- .../src/renderer/stores/terminal-store.ts | 19 ++-- .../shared/i18n/locales/en/onboarding.json | 4 +- .../shared/i18n/locales/fr/onboarding.json | 4 +- 23 files changed, 210 insertions(+), 186 deletions(-) diff --git a/.github/workflows/beta-release.yml b/.github/workflows/beta-release.yml index 7b494acb..bf7328c4 100644 --- a/.github/workflows/beta-release.yml +++ b/.github/workflows/beta-release.yml @@ -32,65 +32,38 @@ jobs: echo "Valid beta version: $VERSION" - update-version: - name: Update package.json version + create-tag: + name: Create beta tag needs: validate-version runs-on: ubuntu-latest permissions: contents: write outputs: - version: ${{ steps.version.outputs.version }} + version: ${{ github.event.inputs.version }} steps: - uses: actions/checkout@v4 with: ref: develop - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '24' - - - name: Update package.json version - id: version - run: | - VERSION="${{ github.event.inputs.version }}" - - # Update frontend package.json - cd apps/frontend - npm version "$VERSION" --no-git-tag-version - - echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "Updated package.json to version $VERSION" - - - name: Commit version bump - if: ${{ github.event.inputs.dry_run != 'true' }} - run: | - VERSION="${{ github.event.inputs.version }}" - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - # Stage all changed files in frontend directory (handles package-lock.json if it exists) - git add -A apps/frontend/ - git commit -m "chore: bump version to $VERSION for beta release" - git push origin develop - name: Create and push tag if: ${{ github.event.inputs.dry_run != 'true' }} run: | VERSION="${{ github.event.inputs.version }}" + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" git tag -a "v$VERSION" -m "Beta release v$VERSION" git push origin "v$VERSION" echo "Created tag v$VERSION" # Intel build on Intel runner for native compilation build-macos-intel: - needs: update-version + needs: create-tag runs-on: macos-15-intel steps: - uses: actions/checkout@v4 with: # Use tag for real releases, develop branch for dry runs - ref: ${{ github.event.inputs.dry_run == 'true' && 'develop' || format('v{0}', needs.update-version.outputs.version) }} + ref: ${{ github.event.inputs.dry_run == 'true' && 'develop' || format('v{0}', needs.create-tag.outputs.version) }} - name: Setup Node.js uses: actions/setup-node@v4 @@ -122,7 +95,9 @@ jobs: run: cd apps/frontend && npm run build - name: Package macOS (Intel) - run: cd apps/frontend && npm run package:mac -- --x64 + run: | + VERSION="${{ needs.create-tag.outputs.version }}" + cd apps/frontend && npm run package:mac -- --x64 --config.extraMetadata.version="$VERSION" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} CSC_LINK: ${{ secrets.MAC_CERTIFICATE }} @@ -160,13 +135,13 @@ jobs: # Apple Silicon build on ARM64 runner for native compilation build-macos-arm64: - needs: update-version + needs: create-tag runs-on: macos-15 steps: - uses: actions/checkout@v4 with: # Use tag for real releases, develop branch for dry runs - ref: ${{ github.event.inputs.dry_run == 'true' && 'develop' || format('v{0}', needs.update-version.outputs.version) }} + ref: ${{ github.event.inputs.dry_run == 'true' && 'develop' || format('v{0}', needs.create-tag.outputs.version) }} - name: Setup Node.js uses: actions/setup-node@v4 @@ -198,7 +173,9 @@ jobs: run: cd apps/frontend && npm run build - name: Package macOS (Apple Silicon) - run: cd apps/frontend && npm run package:mac -- --arm64 + run: | + VERSION="${{ needs.create-tag.outputs.version }}" + cd apps/frontend && npm run package:mac -- --arm64 --config.extraMetadata.version="$VERSION" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} CSC_LINK: ${{ secrets.MAC_CERTIFICATE }} @@ -235,13 +212,13 @@ jobs: apps/frontend/dist/*.zip build-windows: - needs: update-version + needs: create-tag runs-on: windows-latest steps: - uses: actions/checkout@v4 with: # Use tag for real releases, develop branch for dry runs - ref: ${{ github.event.inputs.dry_run == 'true' && 'develop' || format('v{0}', needs.update-version.outputs.version) }} + ref: ${{ github.event.inputs.dry_run == 'true' && 'develop' || format('v{0}', needs.create-tag.outputs.version) }} - name: Setup Node.js uses: actions/setup-node@v4 @@ -274,7 +251,10 @@ jobs: run: cd apps/frontend && npm run build - name: Package Windows - run: cd apps/frontend && npm run package:win + shell: bash + run: | + VERSION="${{ needs.create-tag.outputs.version }}" + cd apps/frontend && npm run package:win -- --config.extraMetadata.version="$VERSION" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} CSC_LINK: ${{ secrets.WIN_CERTIFICATE }} @@ -288,13 +268,13 @@ jobs: apps/frontend/dist/*.exe build-linux: - needs: update-version + needs: create-tag runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: # Use tag for real releases, develop branch for dry runs - ref: ${{ github.event.inputs.dry_run == 'true' && 'develop' || format('v{0}', needs.update-version.outputs.version) }} + ref: ${{ github.event.inputs.dry_run == 'true' && 'develop' || format('v{0}', needs.create-tag.outputs.version) }} - name: Setup Node.js uses: actions/setup-node@v4 @@ -326,7 +306,9 @@ jobs: run: cd apps/frontend && npm run build - name: Package Linux - run: cd apps/frontend && npm run package:linux + run: | + VERSION="${{ needs.create-tag.outputs.version }}" + cd apps/frontend && npm run package:linux -- --config.extraMetadata.version="$VERSION" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -339,7 +321,7 @@ jobs: apps/frontend/dist/*.deb create-release: - needs: [update-version, build-macos-intel, build-macos-arm64, build-windows, build-linux] + needs: [create-tag, build-macos-intel, build-macos-arm64, build-windows, build-linux] runs-on: ubuntu-latest if: ${{ github.event.inputs.dry_run != 'true' }} permissions: @@ -347,7 +329,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: v${{ needs.update-version.outputs.version }} + ref: v${{ needs.create-tag.outputs.version }} fetch-depth: 0 - name: Download all artifacts @@ -379,10 +361,10 @@ jobs: - name: Create Beta Release uses: softprops/action-gh-release@v2 with: - tag_name: v${{ needs.update-version.outputs.version }} - name: v${{ needs.update-version.outputs.version }} (Beta) + tag_name: v${{ needs.create-tag.outputs.version }} + name: v${{ needs.create-tag.outputs.version }} (Beta) body: | - ## Beta Release v${{ needs.update-version.outputs.version }} + ## Beta Release v${{ needs.create-tag.outputs.version }} This is a **beta release** for testing new features. It may contain bugs or incomplete functionality. @@ -396,7 +378,7 @@ jobs: --- - **Full Changelog**: https://github.com/${{ github.repository }}/compare/main...v${{ needs.update-version.outputs.version }} + **Full Changelog**: https://github.com/${{ github.repository }}/compare/main...v${{ needs.create-tag.outputs.version }} files: release-assets/* draft: false prerelease: true @@ -404,7 +386,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} dry-run-summary: - needs: [update-version, build-macos-intel, build-macos-arm64, build-windows, build-linux] + needs: [create-tag, build-macos-intel, build-macos-arm64, build-windows, build-linux] runs-on: ubuntu-latest if: ${{ github.event.inputs.dry_run == 'true' }} steps: @@ -417,7 +399,7 @@ jobs: run: | echo "## Beta Release Dry Run Complete" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "**Version:** ${{ needs.update-version.outputs.version }}" >> $GITHUB_STEP_SUMMARY + echo "**Version:** ${{ needs.create-tag.outputs.version }}" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "Build artifacts created successfully:" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY diff --git a/apps/frontend/src/__tests__/integration/subprocess-spawn.test.ts b/apps/frontend/src/__tests__/integration/subprocess-spawn.test.ts index dad5449a..7ad289e2 100644 --- a/apps/frontend/src/__tests__/integration/subprocess-spawn.test.ts +++ b/apps/frontend/src/__tests__/integration/subprocess-spawn.test.ts @@ -52,13 +52,10 @@ function setupTestDirs(): void { // Create auto-claude source directory that getAutoBuildSourcePath looks for mkdirSync(AUTO_CLAUDE_SOURCE, { recursive: true }); - // Create requirements.txt file (used as marker by getAutoBuildSourcePath) - writeFileSync(path.join(AUTO_CLAUDE_SOURCE, 'requirements.txt'), '# Mock requirements'); - - // Create runners subdirectory (where spec_runner.py lives after restructure) + // Create runners subdirectory with spec_runner.py marker (used by getAutoBuildSourcePath) mkdirSync(path.join(AUTO_CLAUDE_SOURCE, 'runners'), { recursive: true }); - // Create mock spec_runner.py in runners/ subdirectory + // Create mock spec_runner.py in runners/ subdirectory (used as backend marker) writeFileSync( path.join(AUTO_CLAUDE_SOURCE, 'runners', 'spec_runner.py'), '# Mock spec runner\nprint("Starting spec creation")' diff --git a/apps/frontend/src/main/agent/agent-process.ts b/apps/frontend/src/main/agent/agent-process.ts index b99dda61..c606bf01 100644 --- a/apps/frontend/src/main/agent/agent-process.ts +++ b/apps/frontend/src/main/agent/agent-process.ts @@ -51,12 +51,17 @@ export class AgentProcessManager { * Get the auto-claude source path (detects automatically if not configured) */ getAutoBuildSourcePath(): string | null { - // If manually configured, use that - if (this.autoBuildSourcePath && existsSync(this.autoBuildSourcePath)) { + // Use runners/spec_runner.py as the validation marker - this is the file actually needed + const validatePath = (p: string): boolean => { + return existsSync(p) && existsSync(path.join(p, 'runners', 'spec_runner.py')); + }; + + // If manually configured AND valid, use that + if (this.autoBuildSourcePath && validatePath(this.autoBuildSourcePath)) { return this.autoBuildSourcePath; } - // Auto-detect from app location + // Auto-detect from app location (configured path was invalid or not set) const possiblePaths = [ // Dev mode: from dist/main -> ../../backend (apps/frontend/out/main -> apps/backend) path.resolve(__dirname, '..', '..', '..', 'backend'), @@ -67,8 +72,7 @@ export class AgentProcessManager { ]; for (const p of possiblePaths) { - // Use requirements.txt as marker - it always exists in auto-claude source - if (existsSync(p) && existsSync(path.join(p, 'requirements.txt'))) { + if (validatePath(p)) { return p; } } diff --git a/apps/frontend/src/main/changelog/changelog-service.ts b/apps/frontend/src/main/changelog/changelog-service.ts index 5c0fbd64..214e0ccb 100644 --- a/apps/frontend/src/main/changelog/changelog-service.ts +++ b/apps/frontend/src/main/changelog/changelog-service.ts @@ -146,19 +146,14 @@ export class ChangelogService extends EventEmitter { } const possiblePaths = [ - // New apps structure: from out/main -> apps/backend + // Apps structure: from out/main -> apps/backend path.resolve(__dirname, '..', '..', '..', 'backend'), path.resolve(app.getAppPath(), '..', 'backend'), - path.resolve(process.cwd(), 'apps', 'backend'), - // Legacy paths for backwards compatibility - path.resolve(__dirname, '..', '..', '..', 'auto-claude'), - path.resolve(app.getAppPath(), '..', 'auto-claude'), - path.resolve(process.cwd(), 'auto-claude') + path.resolve(process.cwd(), 'apps', 'backend') ]; for (const p of possiblePaths) { - // Use requirements.txt as marker - it always exists in auto-claude source - if (existsSync(p) && existsSync(path.join(p, 'requirements.txt'))) { + if (existsSync(p) && existsSync(path.join(p, 'runners', 'spec_runner.py'))) { return p; } } diff --git a/apps/frontend/src/main/index.ts b/apps/frontend/src/main/index.ts index 0a5a3423..99ef0858 100644 --- a/apps/frontend/src/main/index.ts +++ b/apps/frontend/src/main/index.ts @@ -138,12 +138,23 @@ app.whenReady().then(() => { const settingsPath = join(app.getPath('userData'), 'settings.json'); if (existsSync(settingsPath)) { const settings = JSON.parse(readFileSync(settingsPath, 'utf-8')); - if (settings.pythonPath || settings.autoBuildPath) { + + // Validate autoBuildPath before using it - must contain runners/spec_runner.py + let validAutoBuildPath = settings.autoBuildPath; + if (validAutoBuildPath) { + const specRunnerPath = join(validAutoBuildPath, 'runners', 'spec_runner.py'); + if (!existsSync(specRunnerPath)) { + console.warn('[main] Configured autoBuildPath is invalid (missing runners/spec_runner.py), will use auto-detection:', validAutoBuildPath); + validAutoBuildPath = undefined; // Let auto-detection find the correct path + } + } + + if (settings.pythonPath || validAutoBuildPath) { console.warn('[main] Configuring AgentManager with settings:', { pythonPath: settings.pythonPath, - autoBuildPath: settings.autoBuildPath + autoBuildPath: validAutoBuildPath }); - agentManager.configure(settings.pythonPath, settings.autoBuildPath); + agentManager.configure(settings.pythonPath, validAutoBuildPath); } } } catch (error) { diff --git a/apps/frontend/src/main/insights/config.ts b/apps/frontend/src/main/insights/config.ts index c615b63a..a937dbed 100644 --- a/apps/frontend/src/main/insights/config.ts +++ b/apps/frontend/src/main/insights/config.ts @@ -41,19 +41,14 @@ export class InsightsConfig { } const possiblePaths = [ - // New apps structure: from out/main -> apps/backend + // Apps structure: from out/main -> apps/backend path.resolve(__dirname, '..', '..', '..', 'backend'), path.resolve(app.getAppPath(), '..', 'backend'), - path.resolve(process.cwd(), 'apps', 'backend'), - // Legacy paths for backwards compatibility - path.resolve(__dirname, '..', '..', '..', 'auto-claude'), - path.resolve(app.getAppPath(), '..', 'auto-claude'), - path.resolve(process.cwd(), 'auto-claude') + path.resolve(process.cwd(), 'apps', 'backend') ]; for (const p of possiblePaths) { - // Use requirements.txt as marker - it always exists in auto-claude source - if (existsSync(p) && existsSync(path.join(p, 'requirements.txt'))) { + if (existsSync(p) && existsSync(path.join(p, 'runners', 'spec_runner.py'))) { return p; } } diff --git a/apps/frontend/src/main/ipc-handlers/memory-handlers.ts b/apps/frontend/src/main/ipc-handlers/memory-handlers.ts index 842418b0..965696a7 100644 --- a/apps/frontend/src/main/ipc-handlers/memory-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/memory-handlers.ts @@ -5,7 +5,7 @@ * Uses LadybugDB (embedded Kuzu-based database) - no Docker required. */ -import { ipcMain } from 'electron'; +import { ipcMain, app } from 'electron'; import { spawn } from 'child_process'; import * as path from 'path'; import * as fs from 'fs'; @@ -111,12 +111,13 @@ async function executeOllamaDetector( // Find the ollama_model_detector.py script const possiblePaths = [ + // Packaged app paths (check FIRST for packaged builds) + ...(app.isPackaged + ? [path.join(process.resourcesPath, 'backend', 'ollama_model_detector.py')] + : []), // Development paths path.resolve(__dirname, '..', '..', '..', 'backend', 'ollama_model_detector.py'), - path.resolve(process.cwd(), 'apps', 'backend', 'ollama_model_detector.py'), - // Legacy paths (for backwards compatibility) - path.resolve(__dirname, '..', '..', '..', 'auto-claude', 'ollama_model_detector.py'), - path.resolve(process.cwd(), 'auto-claude', 'ollama_model_detector.py'), + path.resolve(process.cwd(), 'apps', 'backend', 'ollama_model_detector.py') ]; let scriptPath: string | null = null; @@ -128,9 +129,19 @@ async function executeOllamaDetector( } if (!scriptPath) { + if (process.env.DEBUG) { + console.error( + '[OllamaDetector] Python script not found. Searched paths:', + possiblePaths + ); + } return { success: false, error: 'ollama_model_detector.py script not found' }; } + if (process.env.DEBUG) { + console.log('[OllamaDetector] Using script at:', scriptPath); + } + const [pythonExe, baseArgs] = parsePythonCommand(pythonCmd); const args = [...baseArgs, scriptPath, command]; if (baseUrl) { @@ -539,13 +550,13 @@ export function registerMemoryHandlers(): void { // Find the ollama_model_detector.py script const possiblePaths = [ - // New apps structure + // Packaged app paths (check FIRST for packaged builds) + ...(app.isPackaged + ? [path.join(process.resourcesPath, 'backend', 'ollama_model_detector.py')] + : []), + // Development paths path.resolve(__dirname, '..', '..', '..', 'backend', 'ollama_model_detector.py'), - path.resolve(process.cwd(), 'apps', 'backend', 'ollama_model_detector.py'), - // Legacy paths for backwards compatibility - path.resolve(__dirname, '..', '..', '..', 'auto-claude', 'ollama_model_detector.py'), - path.resolve(process.cwd(), 'auto-claude', 'ollama_model_detector.py'), - path.resolve(process.cwd(), '..', 'auto-claude', 'ollama_model_detector.py'), + path.resolve(process.cwd(), 'apps', 'backend', 'ollama_model_detector.py') ]; let scriptPath: string | null = null; diff --git a/apps/frontend/src/main/ipc-handlers/settings-handlers.ts b/apps/frontend/src/main/ipc-handlers/settings-handlers.ts index 22528d65..d1b47692 100644 --- a/apps/frontend/src/main/ipc-handlers/settings-handlers.ts +++ b/apps/frontend/src/main/ipc-handlers/settings-handlers.ts @@ -29,12 +29,7 @@ const detectAutoBuildSourcePath = (): string | null => { // We need to go up to find apps/backend possiblePaths.push( path.resolve(__dirname, '..', '..', '..', 'backend'), // From out/main -> apps/backend - path.resolve(process.cwd(), 'apps', 'backend'), // From cwd (repo root) - // Legacy paths for backwards compatibility - path.resolve(__dirname, '..', '..', '..', 'auto-claude'), // Legacy: from out/main up 3 levels - path.resolve(__dirname, '..', '..', 'auto-claude'), // Legacy: from out/main up 2 levels - path.resolve(process.cwd(), 'auto-claude'), // Legacy: from cwd (project root) - path.resolve(process.cwd(), '..', 'auto-claude') // Legacy: from cwd parent + path.resolve(process.cwd(), 'apps', 'backend') // From cwd (repo root) ); } else { // Production mode paths (packaged app) @@ -42,20 +37,14 @@ const detectAutoBuildSourcePath = (): string | null => { // We check common locations relative to the app bundle const appPath = app.getAppPath(); possiblePaths.push( - path.resolve(appPath, '..', 'backend'), // Sibling to app (new structure) + path.resolve(appPath, '..', 'backend'), // Sibling to app path.resolve(appPath, '..', '..', 'backend'), // Up 2 from app - // Legacy paths for backwards compatibility - path.resolve(appPath, '..', 'auto-claude'), // Sibling to app - path.resolve(appPath, '..', '..', 'auto-claude'), // Up 2 from app - path.resolve(appPath, '..', '..', '..', 'auto-claude'), // Up 3 from app - path.resolve(process.resourcesPath, '..', 'auto-claude'), // Relative to resources - path.resolve(process.resourcesPath, '..', '..', 'auto-claude') + path.resolve(process.resourcesPath, '..', 'backend') // Relative to resources ); } // Add process.cwd() as last resort on all platforms possiblePaths.push(path.resolve(process.cwd(), 'apps', 'backend')); - possiblePaths.push(path.resolve(process.cwd(), 'auto-claude')); // Enable debug logging with DEBUG=1 const debug = process.env.DEBUG === '1' || process.env.DEBUG === 'true'; @@ -70,8 +59,9 @@ const detectAutoBuildSourcePath = (): string | null => { } for (const p of possiblePaths) { - // Use requirements.txt as marker - it always exists in auto-claude source - const markerPath = path.join(p, 'requirements.txt'); + // Use runners/spec_runner.py as marker - this is the file actually needed for task execution + // This prevents matching legacy 'auto-claude/' directories that don't have the runners + const markerPath = path.join(p, 'runners', 'spec_runner.py'); const exists = existsSync(p) && existsSync(markerPath); if (debug) { diff --git a/apps/frontend/src/main/memory-service.ts b/apps/frontend/src/main/memory-service.ts index 70cea47e..006284c8 100644 --- a/apps/frontend/src/main/memory-service.ts +++ b/apps/frontend/src/main/memory-service.ts @@ -93,21 +93,19 @@ export function getDefaultDbPath(): string { * Get the path to the query_memory.py script */ function getQueryScriptPath(): string | null { - // Look for the script in backend directory (new apps structure) + // Look for the script in backend directory - validate using spec_runner.py marker const possiblePaths = [ - // New apps structure: from dist/main -> apps/backend + // Apps structure: from dist/main -> apps/backend path.resolve(__dirname, '..', '..', '..', 'backend', 'query_memory.py'), path.resolve(app.getAppPath(), '..', 'backend', 'query_memory.py'), - path.resolve(process.cwd(), 'apps', 'backend', 'query_memory.py'), - // Legacy paths for backwards compatibility - path.resolve(__dirname, '..', '..', '..', 'auto-claude', 'query_memory.py'), - path.resolve(app.getAppPath(), '..', 'auto-claude', 'query_memory.py'), - path.resolve(process.cwd(), 'auto-claude', 'query_memory.py'), - path.resolve(process.cwd(), '..', 'auto-claude', 'query_memory.py'), + path.resolve(process.cwd(), 'apps', 'backend', 'query_memory.py') ]; for (const p of possiblePaths) { - if (fs.existsSync(p)) { + // Validate backend structure by checking for spec_runner.py marker + const backendPath = path.dirname(p); + const specRunnerPath = path.join(backendPath, 'runners', 'spec_runner.py'); + if (fs.existsSync(p) && fs.existsSync(specRunnerPath)) { return p; } } diff --git a/apps/frontend/src/main/project-initializer.ts b/apps/frontend/src/main/project-initializer.ts index 40d41cca..9e46edbd 100644 --- a/apps/frontend/src/main/project-initializer.ts +++ b/apps/frontend/src/main/project-initializer.ts @@ -228,13 +228,13 @@ export interface InitializationResult { } /** - * Check if the project has a local auto-claude source directory - * This indicates it's the auto-claude development project itself + * Check if the project has a local backend source directory + * This indicates it's the development project itself */ export function hasLocalSource(projectPath: string): boolean { - const localSourcePath = path.join(projectPath, 'auto-claude'); - // Use requirements.txt as marker - it always exists in auto-claude source - const markerFile = path.join(localSourcePath, 'requirements.txt'); + const localSourcePath = path.join(projectPath, 'apps', 'backend'); + // Use runners/spec_runner.py as marker - ensures valid backend + const markerFile = path.join(localSourcePath, 'runners', 'spec_runner.py'); return existsSync(localSourcePath) && existsSync(markerFile); } @@ -242,7 +242,7 @@ export function hasLocalSource(projectPath: string): boolean { * Get the local source path for a project (if it exists) */ export function getLocalSourcePath(projectPath: string): string | null { - const localSourcePath = path.join(projectPath, 'auto-claude'); + const localSourcePath = path.join(projectPath, 'apps', 'backend'); if (hasLocalSource(projectPath)) { return localSourcePath; } diff --git a/apps/frontend/src/main/project-store.ts b/apps/frontend/src/main/project-store.ts index 7c6b0995..e4a16917 100644 --- a/apps/frontend/src/main/project-store.ts +++ b/apps/frontend/src/main/project-store.ts @@ -342,8 +342,10 @@ export class ProjectStore { if (existsSync(specFilePath)) { try { const content = readFileSync(specFilePath, 'utf-8'); - // Extract first paragraph after "## Overview" - handle both with and without blank line - const overviewMatch = content.match(/## Overview\s*\n+([^\n#]+)/); + // Extract full Overview section until next heading or end of file + // Use \n#{1,6}\s to match valid markdown headings (# to ######) with required space + // This avoids truncating at # in code blocks (e.g., Python comments) + const overviewMatch = content.match(/## Overview\s*\n+([\s\S]*?)(?=\n#{1,6}\s|$)/); if (overviewMatch) { description = overviewMatch[1].trim(); } diff --git a/apps/frontend/src/main/terminal-name-generator.ts b/apps/frontend/src/main/terminal-name-generator.ts index a2b68c78..afe31de1 100644 --- a/apps/frontend/src/main/terminal-name-generator.ts +++ b/apps/frontend/src/main/terminal-name-generator.ts @@ -47,19 +47,14 @@ export class TerminalNameGenerator extends EventEmitter { } const possiblePaths = [ - // New apps structure: from out/main -> apps/backend + // Apps structure: from out/main -> apps/backend path.resolve(__dirname, '..', '..', '..', 'backend'), path.resolve(app.getAppPath(), '..', 'backend'), - path.resolve(process.cwd(), 'apps', 'backend'), - // Legacy paths for backwards compatibility - path.resolve(__dirname, '..', '..', '..', 'auto-claude'), - path.resolve(app.getAppPath(), '..', 'auto-claude'), - path.resolve(process.cwd(), 'auto-claude') + path.resolve(process.cwd(), 'apps', 'backend') ]; for (const p of possiblePaths) { - // Use requirements.txt as marker - it always exists in auto-claude source - if (existsSync(p) && existsSync(path.join(p, 'requirements.txt'))) { + if (existsSync(p) && existsSync(path.join(p, 'runners', 'spec_runner.py'))) { return p; } } diff --git a/apps/frontend/src/main/title-generator.ts b/apps/frontend/src/main/title-generator.ts index ca048ccb..d4e1f712 100644 --- a/apps/frontend/src/main/title-generator.ts +++ b/apps/frontend/src/main/title-generator.ts @@ -51,19 +51,14 @@ export class TitleGenerator extends EventEmitter { } const possiblePaths = [ - // New apps structure: from out/main -> apps/backend + // Apps structure: from out/main -> apps/backend path.resolve(__dirname, '..', '..', '..', 'backend'), path.resolve(app.getAppPath(), '..', 'backend'), - path.resolve(process.cwd(), 'apps', 'backend'), - // Legacy paths for backwards compatibility - path.resolve(__dirname, '..', '..', '..', 'auto-claude'), - path.resolve(app.getAppPath(), '..', 'auto-claude'), - path.resolve(process.cwd(), 'auto-claude') + path.resolve(process.cwd(), 'apps', 'backend') ]; for (const p of possiblePaths) { - // Use requirements.txt as marker - it always exists in auto-claude source - if (existsSync(p) && existsSync(path.join(p, 'requirements.txt'))) { + if (existsSync(p) && existsSync(path.join(p, 'runners', 'spec_runner.py'))) { return p; } } diff --git a/apps/frontend/src/main/updater/path-resolver.ts b/apps/frontend/src/main/updater/path-resolver.ts index a99ef6e6..6c149a5b 100644 --- a/apps/frontend/src/main/updater/path-resolver.ts +++ b/apps/frontend/src/main/updater/path-resolver.ts @@ -26,8 +26,8 @@ export function getBundledSourcePath(): string { ]; for (const p of possiblePaths) { - // Validate it's a proper backend source (must have requirements.txt) - const markerPath = path.join(p, 'requirements.txt'); + // Validate it's a proper backend source (must have runners/spec_runner.py) + const markerPath = path.join(p, 'runners', 'spec_runner.py'); if (existsSync(p) && existsSync(markerPath)) { return p; } @@ -35,7 +35,7 @@ export function getBundledSourcePath(): string { // Fallback - warn if this path is also invalid const fallback = path.join(app.getAppPath(), '..', 'backend'); - const fallbackMarker = path.join(fallback, 'requirements.txt'); + const fallbackMarker = path.join(fallback, 'runners', 'spec_runner.py'); if (!existsSync(fallbackMarker)) { console.warn( `[path-resolver] No valid backend source found in development paths, fallback "${fallback}" may be invalid` @@ -61,14 +61,14 @@ export function getEffectiveSourcePath(): string { if (existsSync(settingsPath)) { const settings = JSON.parse(readFileSync(settingsPath, 'utf-8')); if (settings.autoBuildPath && existsSync(settings.autoBuildPath)) { - // Validate it's a proper backend source (must have requirements.txt) - const markerPath = path.join(settings.autoBuildPath, 'requirements.txt'); + // Validate it's a proper backend source (must have runners/spec_runner.py) + const markerPath = path.join(settings.autoBuildPath, 'runners', 'spec_runner.py'); if (existsSync(markerPath)) { return settings.autoBuildPath; } // Invalid path - log warning and fall through to auto-detection console.warn( - `[path-resolver] Configured autoBuildPath "${settings.autoBuildPath}" is missing requirements.txt, falling back to bundled source` + `[path-resolver] Configured autoBuildPath "${settings.autoBuildPath}" is missing runners/spec_runner.py, falling back to bundled source` ); } } @@ -79,7 +79,7 @@ export function getEffectiveSourcePath(): string { if (app.isPackaged) { // Check for user-updated source first const overridePath = path.join(app.getPath('userData'), 'backend-source'); - const overrideMarker = path.join(overridePath, 'requirements.txt'); + const overrideMarker = path.join(overridePath, 'runners', 'spec_runner.py'); if (existsSync(overridePath) && existsSync(overrideMarker)) { return overridePath; } diff --git a/apps/frontend/src/main/updater/version-manager.ts b/apps/frontend/src/main/updater/version-manager.ts index 2a9ea0a8..80401872 100644 --- a/apps/frontend/src/main/updater/version-manager.ts +++ b/apps/frontend/src/main/updater/version-manager.ts @@ -36,14 +36,10 @@ export function getEffectiveVersion(): string { } else { // Development: check the actual source paths where updates are written const possibleSourcePaths = [ - // New apps structure + // Apps structure: apps/backend path.join(app.getAppPath(), '..', 'backend'), path.join(process.cwd(), 'apps', 'backend'), - // Legacy paths for backwards compatibility - path.join(app.getAppPath(), '..', 'auto-claude'), - path.join(app.getAppPath(), '..', '..', 'auto-claude'), - path.join(process.cwd(), 'auto-claude'), - path.join(process.cwd(), '..', 'auto-claude') + path.resolve(__dirname, '..', '..', '..', 'backend') ]; for (const sourcePath of possibleSourcePaths) { diff --git a/apps/frontend/src/renderer/App.tsx b/apps/frontend/src/renderer/App.tsx index 24c3282d..0f070f8b 100644 --- a/apps/frontend/src/renderer/App.tsx +++ b/apps/frontend/src/renderer/App.tsx @@ -303,16 +303,9 @@ export function App() { useTaskStore.getState().clearTasks(); } - // Handle terminals on project change - const currentTerminals = useTerminalStore.getState().terminals; - - // Close existing terminals (they belong to the previous project) - currentTerminals.forEach((t) => { - window.electronAPI.destroyTerminal(t.id); - }); - useTerminalStore.getState().clearAllTerminals(); - - // Try to restore saved sessions for the new project + // Handle terminals on project change - DON'T destroy, just restore if needed + // Terminals are now filtered by projectPath in TerminalGrid, so each project + // sees only its own terminals. PTY processes stay alive across project switches. if (selectedProject?.path) { restoreTerminalSessions(selectedProject.path).catch((err) => { console.error('[App] Failed to restore sessions:', err); diff --git a/apps/frontend/src/renderer/components/TerminalGrid.tsx b/apps/frontend/src/renderer/components/TerminalGrid.tsx index ef9c1fd2..4f2b930e 100644 --- a/apps/frontend/src/renderer/components/TerminalGrid.tsx +++ b/apps/frontend/src/renderer/components/TerminalGrid.tsx @@ -37,7 +37,15 @@ interface TerminalGridProps { } export function TerminalGrid({ projectPath, onNewTaskClick, isActive = false }: TerminalGridProps) { - const terminals = useTerminalStore((state) => state.terminals); + const allTerminals = useTerminalStore((state) => state.terminals); + // Filter terminals to show only those belonging to the current project + // Also include legacy terminals without projectPath (created before this change) + const terminals = useMemo(() => + projectPath + ? allTerminals.filter(t => t.projectPath === projectPath || !t.projectPath) + : allTerminals, + [allTerminals, projectPath] + ); const activeTerminalId = useTerminalStore((state) => state.activeTerminalId); const addTerminal = useTerminalStore((state) => state.addTerminal); const removeTerminal = useTerminalStore((state) => state.removeTerminal); @@ -177,7 +185,7 @@ export function TerminalGrid({ projectPath, onNewTaskClick, isActive = false }: if ((e.ctrlKey || e.metaKey) && e.key === 't') { e.preventDefault(); if (canAddTerminal()) { - addTerminal(projectPath); + addTerminal(projectPath, projectPath); } } // Ctrl+W or Cmd+W to close active terminal @@ -193,7 +201,7 @@ export function TerminalGrid({ projectPath, onNewTaskClick, isActive = false }: const handleAddTerminal = useCallback(() => { if (canAddTerminal()) { - addTerminal(projectPath); + addTerminal(projectPath, projectPath); } }, [addTerminal, canAddTerminal, projectPath]); diff --git a/apps/frontend/src/renderer/components/onboarding/OAuthStep.tsx b/apps/frontend/src/renderer/components/onboarding/OAuthStep.tsx index 75035f66..7584f864 100644 --- a/apps/frontend/src/renderer/components/onboarding/OAuthStep.tsx +++ b/apps/frontend/src/renderer/components/onboarding/OAuthStep.tsx @@ -1,4 +1,5 @@ import { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; import { Key, Eye, @@ -16,7 +17,8 @@ import { LogIn, ChevronDown, ChevronRight, - Users + Users, + Lock } from 'lucide-react'; import { Button } from '../ui/button'; import { Input } from '../ui/input'; @@ -38,6 +40,8 @@ interface OAuthStepProps { * reusing patterns from IntegrationSettings.tsx. */ export function OAuthStep({ onNext, onBack, onSkip }: OAuthStepProps) { + const { t } = useTranslation('onboarding'); + // Claude Profiles state const [claudeProfiles, setClaudeProfiles] = useState([]); const [activeProfileId, setActiveProfileId] = useState(null); @@ -110,7 +114,19 @@ export function OAuthStep({ onNext, onBack, onSkip }: OAuthStepProps) { setError(null); try { const profileName = newProfileName.trim(); - const profileSlug = profileName.toLowerCase().replace(/\s+/g, '-'); + // Sanitize slug: only allow alphanumeric and dashes, remove leading/trailing dashes + const profileSlug = profileName + .toLowerCase() + .replace(/[^a-z0-9]/g, '-') + .replace(/-+/g, '-') + .replace(/^-|-$/g, ''); + + // Validate that sanitized slug is not empty (e.g., "!!!" becomes "") + if (!profileSlug) { + setError('Profile name must contain at least one letter or number'); + setIsAddingProfile(false); + return; + } const result = await window.electronAPI.saveClaudeProfile({ id: `profile-${Date.now()}`, @@ -323,6 +339,25 @@ export function OAuthStep({ onNext, onBack, onSkip }: OAuthStepProps) { + {/* Keychain explanation - macOS only */} + {navigator.platform.toLowerCase().includes('mac') && ( + + +
+ +
+

+ {t('oauth.keychainTitle')} +

+

+ {t('oauth.keychainDescription')} +

+
+
+
+
+ )} + {/* Profile list */}
{claudeProfiles.length === 0 ? ( diff --git a/apps/frontend/src/renderer/components/task-detail/TaskMetadata.tsx b/apps/frontend/src/renderer/components/task-detail/TaskMetadata.tsx index 7c6588c0..2deab047 100644 --- a/apps/frontend/src/renderer/components/task-detail/TaskMetadata.tsx +++ b/apps/frontend/src/renderer/components/task-detail/TaskMetadata.tsx @@ -12,9 +12,11 @@ import { ListChecks, Clock } from 'lucide-react'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; import { Badge } from '../ui/badge'; import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'; -import { cn, formatRelativeTime, sanitizeMarkdownForDisplay } from '../../lib/utils'; +import { cn, formatRelativeTime } from '../../lib/utils'; import { TASK_CATEGORY_LABELS, TASK_CATEGORY_COLORS, @@ -136,10 +138,10 @@ export function TaskMetadata({ task }: TaskMetadataProps) { {/* Description - Primary Content */} {task.description && ( -
-

- {sanitizeMarkdownForDisplay(task.description, 800)} -

+
+ + {task.description} +
)} diff --git a/apps/frontend/src/renderer/components/task-detail/hooks/useTaskDetail.ts b/apps/frontend/src/renderer/components/task-detail/hooks/useTaskDetail.ts index 676a099a..bc8ab1ea 100644 --- a/apps/frontend/src/renderer/components/task-detail/hooks/useTaskDetail.ts +++ b/apps/frontend/src/renderer/components/task-detail/hooks/useTaskDetail.ts @@ -48,19 +48,21 @@ export function useTaskDetail({ task }: UseTaskDetailOptions) { const [showConflictDialog, setShowConflictDialog] = useState(false); const selectedProject = useProjectStore((state) => state.getSelectedProject()); - const isRunning = task.status === 'in_progress' || task.status === 'ai_review'; + const isRunning = task.status === 'in_progress'; + // isActiveTask includes ai_review for stuck detection (CHANGELOG documents this feature) + const isActiveTask = task.status === 'in_progress' || task.status === 'ai_review'; const needsReview = task.status === 'human_review'; const executionPhase = task.executionProgress?.phase; const hasActiveExecution = executionPhase && executionPhase !== 'idle' && executionPhase !== 'complete' && executionPhase !== 'failed'; const isIncomplete = isIncompleteHumanReview(task); const taskProgress = getTaskProgress(task); - // Check if task is stuck (status says in_progress but no actual process) + // Check if task is stuck (status says in_progress/ai_review but no actual process) // Add a grace period to avoid false positives during process spawn useEffect(() => { let timeoutId: NodeJS.Timeout | undefined; - if (isRunning && !hasCheckedRunning) { + if (isActiveTask && !hasCheckedRunning) { // Wait 2 seconds before checking - gives process time to spawn and register timeoutId = setTimeout(() => { checkTaskRunning(task.id).then((actuallyRunning) => { @@ -68,7 +70,7 @@ export function useTaskDetail({ task }: UseTaskDetailOptions) { setHasCheckedRunning(true); }); }, 2000); - } else if (!isRunning) { + } else if (!isActiveTask) { setIsStuck(false); setHasCheckedRunning(false); } @@ -76,7 +78,7 @@ export function useTaskDetail({ task }: UseTaskDetailOptions) { return () => { if (timeoutId) clearTimeout(timeoutId); }; - }, [task.id, isRunning, hasCheckedRunning]); + }, [task.id, isActiveTask, hasCheckedRunning]); // Handle scroll events in logs to detect if user scrolled up const handleLogsScroll = (e: React.UIEvent) => { diff --git a/apps/frontend/src/renderer/stores/terminal-store.ts b/apps/frontend/src/renderer/stores/terminal-store.ts index 48ef8c0a..5e03779d 100644 --- a/apps/frontend/src/renderer/stores/terminal-store.ts +++ b/apps/frontend/src/renderer/stores/terminal-store.ts @@ -17,6 +17,7 @@ export interface Terminal { // outputBuffer removed - now managed by terminalBufferManager singleton isRestored?: boolean; // Whether this terminal was restored from a saved session associatedTaskId?: string; // ID of task associated with this terminal (for context loading) + projectPath?: string; // Project this terminal belongs to (for multi-project support) } interface TerminalLayout { @@ -35,7 +36,7 @@ interface TerminalState { hasRestoredSessions: boolean; // Track if we've restored sessions for this project // Actions - addTerminal: (cwd?: string) => Terminal | null; + addTerminal: (cwd?: string, projectPath?: string) => Terminal | null; addRestoredTerminal: (session: TerminalSession) => Terminal; removeTerminal: (id: string) => void; updateTerminal: (id: string, updates: Partial) => void; @@ -53,6 +54,7 @@ interface TerminalState { getTerminal: (id: string) => Terminal | undefined; getActiveTerminal: () => Terminal | undefined; canAddTerminal: () => boolean; + getTerminalsForProject: (projectPath: string) => Terminal[]; } export const useTerminalStore = create((set, get) => ({ @@ -62,7 +64,7 @@ export const useTerminalStore = create((set, get) => ({ maxTerminals: 12, hasRestoredSessions: false, - addTerminal: (cwd?: string) => { + addTerminal: (cwd?: string, projectPath?: string) => { const state = get(); if (state.terminals.length >= state.maxTerminals) { return null; @@ -76,6 +78,7 @@ export const useTerminalStore = create((set, get) => ({ createdAt: new Date(), isClaudeMode: false, // outputBuffer removed - managed by terminalBufferManager + projectPath, }; set((state) => ({ @@ -105,6 +108,7 @@ export const useTerminalStore = create((set, get) => ({ claudeSessionId: session.claudeSessionId, // outputBuffer now stored in terminalBufferManager isRestored: true, + projectPath: session.projectPath, }; // Restore buffer to buffer manager @@ -216,6 +220,10 @@ export const useTerminalStore = create((set, get) => ({ const state = get(); return state.terminals.length < state.maxTerminals; }, + + getTerminalsForProject: (projectPath: string) => { + return get().terminals.filter(t => t.projectPath === projectPath); + }, })); /** @@ -224,9 +232,10 @@ export const useTerminalStore = create((set, get) => ({ export async function restoreTerminalSessions(projectPath: string): Promise { const store = useTerminalStore.getState(); - // Don't restore if we already have terminals (user might have opened some manually) - if (store.terminals.length > 0) { - debugLog('[TerminalStore] Terminals already exist, skipping session restore'); + // Don't restore if we already have terminals for THIS project + const projectTerminals = store.terminals.filter(t => t.projectPath === projectPath); + if (projectTerminals.length > 0) { + debugLog('[TerminalStore] Terminals already exist for this project, skipping session restore'); return; } diff --git a/apps/frontend/src/shared/i18n/locales/en/onboarding.json b/apps/frontend/src/shared/i18n/locales/en/onboarding.json index 6c8aa0c7..8d6f29e3 100644 --- a/apps/frontend/src/shared/i18n/locales/en/onboarding.json +++ b/apps/frontend/src/shared/i18n/locales/en/onboarding.json @@ -30,7 +30,9 @@ }, "oauth": { "title": "Claude Authentication", - "description": "Connect your Claude account to enable AI features" + "description": "Connect your Claude account to enable AI features", + "keychainTitle": "Secure Storage", + "keychainDescription": "Your tokens are encrypted using your system's keychain. You may see a password prompt from macOS — click \"Always Allow\" to avoid seeing it again." }, "memory": { "title": "Memory", diff --git a/apps/frontend/src/shared/i18n/locales/fr/onboarding.json b/apps/frontend/src/shared/i18n/locales/fr/onboarding.json index 2763781e..4e51fdcd 100644 --- a/apps/frontend/src/shared/i18n/locales/fr/onboarding.json +++ b/apps/frontend/src/shared/i18n/locales/fr/onboarding.json @@ -30,7 +30,9 @@ }, "oauth": { "title": "Authentification Claude", - "description": "Connectez votre compte Claude pour activer les fonctionnalités IA" + "description": "Connectez votre compte Claude pour activer les fonctionnalités IA", + "keychainTitle": "Stockage sécurisé", + "keychainDescription": "Vos jetons sont chiffrés à l'aide du trousseau de clés de votre système. Une demande de mot de passe macOS peut apparaître — cliquez sur « Toujours autoriser » pour ne plus la revoir." }, "memory": { "title": "Mémoire",