Fix/2.7.2 fixes (#300)

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Andy
2025-12-26 13:47:19 +01:00
committed by GitHub
parent 26beefe39d
commit 7a51cbd5e5
23 changed files with 210 additions and 186 deletions
+34 -52
View File
@@ -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
@@ -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")'
@@ -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;
}
}
@@ -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;
}
}
+14 -3
View File
@@ -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) {
+3 -8
View File
@@ -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;
}
}
@@ -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;
@@ -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) {
+7 -9
View File
@@ -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;
}
}
@@ -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;
}
+4 -2
View File
@@ -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();
}
@@ -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;
}
}
+3 -8
View File
@@ -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;
}
}
@@ -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;
}
@@ -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) {
+3 -10
View File
@@ -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);
@@ -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]);
@@ -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<ClaudeProfile[]>([]);
const [activeProfileId, setActiveProfileId] = useState<string | null>(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) {
</CardContent>
</Card>
{/* Keychain explanation - macOS only */}
{navigator.platform.toLowerCase().includes('mac') && (
<Card className="border border-border bg-muted/30">
<CardContent className="p-5">
<div className="flex items-start gap-4">
<Lock className="h-5 w-5 text-muted-foreground shrink-0 mt-0.5" />
<div className="flex-1">
<p className="text-sm font-medium text-foreground mb-1">
{t('oauth.keychainTitle')}
</p>
<p className="text-sm text-muted-foreground">
{t('oauth.keychainDescription')}
</p>
</div>
</div>
</CardContent>
</Card>
)}
{/* Profile list */}
<div className="rounded-lg bg-muted/30 border border-border p-4">
{claudeProfiles.length === 0 ? (
@@ -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 && (
<div>
<p className="text-sm text-foreground/90 leading-relaxed">
{sanitizeMarkdownForDisplay(task.description, 800)}
</p>
<div className="prose prose-sm prose-invert max-w-none prose-p:text-foreground/90 prose-p:leading-relaxed prose-headings:text-foreground prose-strong:text-foreground prose-li:text-foreground/90">
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{task.description}
</ReactMarkdown>
</div>
)}
@@ -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<HTMLDivElement>) => {
@@ -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<Terminal>) => 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<TerminalState>((set, get) => ({
@@ -62,7 +64,7 @@ export const useTerminalStore = create<TerminalState>((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<TerminalState>((set, get) => ({
createdAt: new Date(),
isClaudeMode: false,
// outputBuffer removed - managed by terminalBufferManager
projectPath,
};
set((state) => ({
@@ -105,6 +108,7 @@ export const useTerminalStore = create<TerminalState>((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<TerminalState>((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<TerminalState>((set, get) => ({
export async function restoreTerminalSessions(projectPath: string): Promise<void> {
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;
}
@@ -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",
@@ -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",