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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user