Feature: worktree view customization and stability fixes (#805)

* Changes from feature/worktree-view-customization

* Feature: Git sync, set-tracking, and push divergence handling (#796)

* Add quick-add feature with improved workflows (#802)

* Changes from feature/quick-add

* feat: Clarify system prompt and improve error handling across services. Address PR Feedback

* feat: Improve PR description parsing and refactor event handling

* feat: Add context options to pipeline orchestrator initialization

* fix: Deduplicate React and handle CJS interop for use-sync-external-store

Resolve "Cannot read properties of null (reading 'useState')" errors by
deduplicating React/react-dom and ensuring use-sync-external-store is
bundled together with React to prevent CJS packages from resolving to
different React instances.

* Changes from feature/worktree-view-customization

* refactor: Remove unused worktree swap and highlight props

* refactor: Consolidate feature completion logic and improve thinking level defaults

* feat: Increase max turn limit to 10000

- Update DEFAULT_MAX_TURNS from 1000 to 10000 in settings-helpers.ts and agent-executor.ts
- Update MAX_ALLOWED_TURNS from 2000 to 10000 in settings-helpers.ts
- Update UI clamping logic from 2000 to 10000 in app-store.ts
- Update fallback values from 1000 to 10000 in use-settings-sync.ts
- Update default value from 1000 to 10000 in DEFAULT_GLOBAL_SETTINGS
- Update documentation to reflect new range: 1-10000

Allows agents to perform up to 10000 turns for complex feature execution.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* feat: Add model resolution, improve session handling, and enhance UI stability

* refactor: Remove unused sync and tracking branch props from worktree components

* feat: Add PR number update functionality to worktrees. Address pr feedback

* feat: Optimize Gemini CLI startup and add tool result tracking

* refactor: Improve error handling and simplify worktree task cleanup

---------

Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-02-23 20:31:25 -08:00
committed by GitHub
parent e7504b247f
commit 0330c70261
72 changed files with 3667 additions and 1173 deletions

View File

@@ -44,7 +44,11 @@ export function createFeaturesRoutes(
validatePathParams('projectPath'),
createCreateHandler(featureLoader, events)
);
router.post('/update', validatePathParams('projectPath'), createUpdateHandler(featureLoader));
router.post(
'/update',
validatePathParams('projectPath'),
createUpdateHandler(featureLoader, events)
);
router.post(
'/bulk-update',
validatePathParams('projectPath'),

View File

@@ -5,6 +5,7 @@
import type { Request, Response } from 'express';
import { FeatureLoader } from '../../../services/feature-loader.js';
import type { Feature, FeatureStatus } from '@automaker/types';
import type { EventEmitter } from '../../../lib/events.js';
import { getErrorMessage, logError } from '../common.js';
import { createLogger } from '@automaker/utils';
@@ -13,7 +14,7 @@ const logger = createLogger('features/update');
// Statuses that should trigger syncing to app_spec.txt
const SYNC_TRIGGER_STATUSES: FeatureStatus[] = ['verified', 'completed'];
export function createUpdateHandler(featureLoader: FeatureLoader) {
export function createUpdateHandler(featureLoader: FeatureLoader, events?: EventEmitter) {
return async (req: Request, res: Response): Promise<void> => {
try {
const {
@@ -54,8 +55,18 @@ export function createUpdateHandler(featureLoader: FeatureLoader) {
preEnhancementDescription
);
// Trigger sync to app_spec.txt when status changes to verified or completed
// Emit completion event and sync to app_spec.txt when status transitions to verified/completed
if (newStatus && SYNC_TRIGGER_STATUSES.includes(newStatus) && previousStatus !== newStatus) {
events?.emit('feature:completed', {
featureId,
featureName: updated.title,
projectPath,
passes: true,
message:
newStatus === 'verified' ? 'Feature verified manually' : 'Feature completed manually',
executionMode: 'manual',
});
try {
const synced = await featureLoader.syncFeatureToAppSpec(projectPath, updated);
if (synced) {

View File

@@ -69,6 +69,7 @@ import { createStageFilesHandler } from './routes/stage-files.js';
import { createCheckChangesHandler } from './routes/check-changes.js';
import { createSetTrackingHandler } from './routes/set-tracking.js';
import { createSyncHandler } from './routes/sync.js';
import { createUpdatePRNumberHandler } from './routes/update-pr-number.js';
import type { SettingsService } from '../../services/settings-service.js';
export function createWorktreeRoutes(
@@ -96,6 +97,12 @@ export function createWorktreeRoutes(
router.post('/delete', validatePathParams('projectPath', 'worktreePath'), createDeleteHandler());
router.post('/create-pr', createCreatePRHandler());
router.post('/pr-info', createPRInfoHandler());
router.post(
'/update-pr-number',
validatePathParams('worktreePath', 'projectPath?'),
requireValidWorktree,
createUpdatePRNumberHandler()
);
router.post(
'/commit',
validatePathParams('worktreePath'),

View File

@@ -0,0 +1,163 @@
/**
* POST /update-pr-number endpoint - Update the tracked PR number for a worktree
*
* Allows users to manually change which PR number is tracked for a worktree branch.
* Fetches updated PR info from GitHub when available, or updates metadata with the
* provided number only if GitHub CLI is unavailable.
*/
import type { Request, Response } from 'express';
import { getErrorMessage, logError, execAsync, execEnv, isGhCliAvailable } from '../common.js';
import { updateWorktreePRInfo } from '../../../lib/worktree-metadata.js';
import { createLogger } from '@automaker/utils';
import { validatePRState } from '@automaker/types';
const logger = createLogger('UpdatePRNumber');
export function createUpdatePRNumberHandler() {
return async (req: Request, res: Response): Promise<void> => {
try {
const { worktreePath, projectPath, prNumber } = req.body as {
worktreePath: string;
projectPath?: string;
prNumber: number;
};
if (!worktreePath) {
res.status(400).json({ success: false, error: 'worktreePath required' });
return;
}
if (
!prNumber ||
typeof prNumber !== 'number' ||
prNumber <= 0 ||
!Number.isInteger(prNumber)
) {
res.status(400).json({ success: false, error: 'prNumber must be a positive integer' });
return;
}
const effectiveProjectPath = projectPath || worktreePath;
// Get current branch name
const { stdout: branchOutput } = await execAsync('git rev-parse --abbrev-ref HEAD', {
cwd: worktreePath,
env: execEnv,
});
const branchName = branchOutput.trim();
if (!branchName || branchName === 'HEAD') {
res.status(400).json({
success: false,
error: 'Cannot update PR number in detached HEAD state',
});
return;
}
// Try to fetch PR info from GitHub for the given PR number
const ghCliAvailable = await isGhCliAvailable();
if (ghCliAvailable) {
try {
// Detect repository for gh CLI
let repoFlag = '';
try {
const { stdout: remotes } = await execAsync('git remote -v', {
cwd: worktreePath,
env: execEnv,
});
const lines = remotes.split(/\r?\n/);
let upstreamRepo: string | null = null;
let originOwner: string | null = null;
let originRepo: string | null = null;
for (const line of lines) {
const match =
line.match(/^(\w+)\s+.*[:/]([^/]+)\/([^/\s]+?)(?:\.git)?\s+\(fetch\)/) ||
line.match(/^(\w+)\s+git@[^:]+:([^/]+)\/([^\s]+?)(?:\.git)?\s+\(fetch\)/) ||
line.match(/^(\w+)\s+https?:\/\/[^/]+\/([^/]+)\/([^\s]+?)(?:\.git)?\s+\(fetch\)/);
if (match) {
const [, remoteName, owner, repo] = match;
if (remoteName === 'upstream') {
upstreamRepo = `${owner}/${repo}`;
} else if (remoteName === 'origin') {
originOwner = owner;
originRepo = repo;
}
}
}
const targetRepo =
upstreamRepo || (originOwner && originRepo ? `${originOwner}/${originRepo}` : null);
if (targetRepo) {
repoFlag = ` --repo "${targetRepo}"`;
}
} catch {
// Ignore remote parsing errors
}
// Fetch PR info from GitHub using the PR number
const viewCmd = `gh pr view ${prNumber}${repoFlag} --json number,title,url,state,createdAt`;
const { stdout: prOutput } = await execAsync(viewCmd, {
cwd: worktreePath,
env: execEnv,
});
const prData = JSON.parse(prOutput);
const prInfo = {
number: prData.number,
url: prData.url,
title: prData.title,
state: validatePRState(prData.state),
createdAt: prData.createdAt || new Date().toISOString(),
};
await updateWorktreePRInfo(effectiveProjectPath, branchName, prInfo);
logger.info(`Updated PR tracking to #${prNumber} for branch ${branchName}`);
res.json({
success: true,
result: {
branch: branchName,
prInfo,
},
});
return;
} catch (error) {
logger.warn(`Failed to fetch PR #${prNumber} from GitHub:`, error);
// Fall through to simple update below
}
}
// Fallback: update with just the number, preserving existing PR info structure
// or creating minimal info if no GitHub data available
const prInfo = {
number: prNumber,
url: `https://github.com/pulls/${prNumber}`,
title: `PR #${prNumber}`,
state: validatePRState('OPEN'),
createdAt: new Date().toISOString(),
};
await updateWorktreePRInfo(effectiveProjectPath, branchName, prInfo);
logger.info(`Updated PR tracking to #${prNumber} for branch ${branchName} (no GitHub data)`);
res.json({
success: true,
result: {
branch: branchName,
prInfo,
ghCliUnavailable: !ghCliAvailable,
},
});
} catch (error) {
logError(error, 'Update PR number failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}