Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
7765a12
Feat: Add z.ai usage tracking
gsxdsm Jan 20, 2026
7d5bc72
Feat: Show Gemini Usage in usage dropdown and mobile sidebar
gsxdsm Jan 25, 2026
ac2e8cf
Feat: Add z.ai usage tracking
gsxdsm Jan 20, 2026
bea26a6
style: Fix inconsistent indentation in components and imports
gsxdsm Feb 16, 2026
8bb1063
Merge remote-tracking branch 'upstream/v0.15.0rc' into feat/add-zai-u…
gsxdsm Feb 17, 2026
de021f9
fix: Remove unused vars and improve type safety. Improve task recovery
gsxdsm Feb 17, 2026
7fcf3c1
feat: Mobile improvements and Add selective file staging and improve …
gsxdsm Feb 17, 2026
cb44f8a
Comprehensive set of mobile and all improvements phase 1
gsxdsm Feb 18, 2026
1df778a
chore: Add PageTransitionEvent and APP_BUILD_HASH to eslint globals
gsxdsm Feb 18, 2026
c7f515a
feat: Add auto-fix for SSH URLs in lockfile before linting
gsxdsm Feb 18, 2026
f4e87d4
Update apps/ui/src/styles/global.css
gsxdsm Feb 18, 2026
9af63bc
refactor: Improve all git operations, add stash support, add improved…
gsxdsm Feb 18, 2026
cb99c4b
feat: Replace Select with Popover+Command for branch selection UI
gsxdsm Feb 18, 2026
43c19c7
Update apps/server/src/routes/worktree/routes/discard-changes.ts
gsxdsm Feb 18, 2026
dd4c738
fix: Address code review comments
gsxdsm Feb 18, 2026
887e2ea
fix: Correct parsing of git output blocks and improve stash UI access…
gsxdsm Feb 18, 2026
bddf1a4
fix: Handle staged-new files correctly in discard changes
gsxdsm Feb 18, 2026
854ba6e
fix: Add symlink validation to prevent path traversal attacks
gsxdsm Feb 18, 2026
13261b7
Update apps/ui/src/components/dialogs/project-file-selector-dialog.tsx
gsxdsm Feb 18, 2026
829c161
Update apps/ui/src/components/views/board-view/dialogs/discard-worktr…
gsxdsm Feb 18, 2026
e6e04d5
Update apps/ui/src/components/views/board-view/worktree-panel/worktre…
gsxdsm Feb 18, 2026
d30296d
feat: Add git log parsing and rebase endpoint with input validation
gsxdsm Feb 18, 2026
5c441f2
feat: Add GPT-5 model variants and improve Codex execution logic. Add…
gsxdsm Feb 18, 2026
6903d3c
fix: Standardize event name and import path
gsxdsm Feb 18, 2026
df9a631
refactor: Enhance session management and error handling in AgentServi…
gsxdsm Feb 19, 2026
983eb21
feat: Address review comments, add stage/unstage functionality, confl…
gsxdsm Feb 19, 2026
4ba0026
feat: Add conflict resolution event types
gsxdsm Feb 19, 2026
4ee160f
fix: Address review comments
gsxdsm Feb 19, 2026
15ca1eb
feat: Add process abort control and improve auth detection
gsxdsm Feb 19, 2026
2d90793
feat: Add TypeScript type annotation and fix session_id default value
gsxdsm Feb 19, 2026
53d07fe
feat: Fix new branch issues and address code review comments
gsxdsm Feb 19, 2026
205f662
fix: Improve error handling and validation across multiple services
gsxdsm Feb 19, 2026
a144a63
fix: Resolve git operation error handling and conflict detection issues
gsxdsm Feb 19, 2026
be4153c
fix: Improve error handling and state management in auto-mode and uti…
gsxdsm Feb 19, 2026
ae10dea
feat: Add includeUntracked option and improve error handling for stas…
gsxdsm Feb 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions .github/actions/setup-project/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,24 @@ runs:
cache: 'npm'
cache-dependency-path: package-lock.json

- name: Check for SSH URLs in lockfile
if: inputs.check-lockfile == 'true'
shell: bash
run: npm run lint:lockfile

- name: Configure Git for HTTPS
shell: bash
# Convert SSH URLs to HTTPS for git dependencies (e.g., @electron/node-gyp)
# This is needed because SSH authentication isn't available in CI
run: git config --global url."https://github.com/".insteadOf "git@github.com:"

- name: Auto-fix SSH URLs in lockfile
if: inputs.check-lockfile == 'true'
shell: bash
# Auto-fix any git+ssh:// URLs in package-lock.json before linting
# This handles cases where npm reintroduces SSH URLs for git dependencies
run: node scripts/fix-lockfile-urls.mjs

- name: Check for SSH URLs in lockfile
if: inputs.check-lockfile == 'true'
shell: bash
run: npm run lint:lockfile

- name: Install dependencies
shell: bash
# Use npm install instead of npm ci to correctly resolve platform-specific
Expand Down
12 changes: 12 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ else
export PATH="$PATH:/usr/local/bin:/opt/homebrew/bin:/usr/bin"
fi

# Auto-fix git+ssh:// URLs in package-lock.json if it's being committed
# This prevents CI failures from SSH URLs that npm introduces for git dependencies
if git diff --cached --name-only | grep -q "^package-lock.json$"; then
if command -v node >/dev/null 2>&1; then
if grep -q "git+ssh://" package-lock.json 2>/dev/null; then
echo "Fixing git+ssh:// URLs in package-lock.json..."
node scripts/fix-lockfile-urls.mjs
git add package-lock.json
fi
fi
fi

# Run lint-staged - works with or without nvm
# Prefer npx, fallback to npm exec, both work with system-installed Node.js
if command -v npx >/dev/null 2>&1; then
Expand Down
43 changes: 37 additions & 6 deletions apps/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ import { createCodexRoutes } from './routes/codex/index.js';
import { CodexUsageService } from './services/codex-usage-service.js';
import { CodexAppServerService } from './services/codex-app-server-service.js';
import { CodexModelCacheService } from './services/codex-model-cache-service.js';
import { createZaiRoutes } from './routes/zai/index.js';
import { ZaiUsageService } from './services/zai-usage-service.js';
import { createGeminiRoutes } from './routes/gemini/index.js';
import { GeminiUsageService } from './services/gemini-usage-service.js';
import { createGitHubRoutes } from './routes/github/index.js';
import { createContextRoutes } from './routes/context/index.js';
import { createBacklogPlanRoutes } from './routes/backlog-plan/index.js';
Expand Down Expand Up @@ -300,7 +304,7 @@ app.use(
callback(null, origin);
return;
}
} catch (err) {
} catch {
// Ignore URL parsing errors
}

Expand Down Expand Up @@ -328,6 +332,8 @@ const claudeUsageService = new ClaudeUsageService();
const codexAppServerService = new CodexAppServerService();
const codexModelCacheService = new CodexModelCacheService(DATA_DIR, codexAppServerService);
const codexUsageService = new CodexUsageService(codexAppServerService);
const zaiUsageService = new ZaiUsageService();
const geminiUsageService = new GeminiUsageService();
const mcpTestService = new MCPTestService(settingsService);
const ideationService = new IdeationService(events, settingsService, featureLoader);

Expand Down Expand Up @@ -372,7 +378,7 @@ eventHookService.initialize(events, settingsService, eventHistoryService, featur
let globalSettings: Awaited<ReturnType<typeof settingsService.getGlobalSettings>> | null = null;
try {
globalSettings = await settingsService.getGlobalSettings();
} catch (err) {
} catch {
logger.warn('Failed to load global settings, using defaults');
}

Expand All @@ -390,7 +396,7 @@ eventHookService.initialize(events, settingsService, eventHistoryService, featur
const enableRequestLog = globalSettings.enableRequestLogging ?? true;
setRequestLoggingEnabled(enableRequestLog);
logger.info(`HTTP request logging: ${enableRequestLog ? 'enabled' : 'disabled'}`);
} catch (err) {
} catch {
logger.warn('Failed to apply logging settings, using defaults');
}
}
Expand All @@ -417,6 +423,22 @@ eventHookService.initialize(events, settingsService, eventHistoryService, featur
} else {
logger.info('[STARTUP] Feature state reconciliation complete - no stale states found');
}

// Resume interrupted features in the background after reconciliation.
// This uses the saved execution state to identify features that were running
// before the restart (their statuses have been reset to ready/backlog by
// reconciliation above). Running in background so it doesn't block startup.
if (totalReconciled > 0) {
for (const project of globalSettings.projects) {
autoModeService.resumeInterruptedFeatures(project.path).catch((err) => {
logger.warn(
`[STARTUP] Failed to resume interrupted features for ${project.path}:`,
err
);
});
}
logger.info('[STARTUP] Initiated background resume of interrupted features');
}
}
} catch (err) {
logger.warn('[STARTUP] Failed to reconcile feature states:', err);
Expand Down Expand Up @@ -473,6 +495,8 @@ app.use('/api/terminal', createTerminalRoutes());
app.use('/api/settings', createSettingsRoutes(settingsService));
app.use('/api/claude', createClaudeRoutes(claudeUsageService));
app.use('/api/codex', createCodexRoutes(codexUsageService, codexModelCacheService));
app.use('/api/zai', createZaiRoutes(zaiUsageService, settingsService));
app.use('/api/gemini', createGeminiRoutes(geminiUsageService, events));
app.use('/api/github', createGitHubRoutes(events, settingsService));
app.use('/api/context', createContextRoutes(settingsService));
app.use('/api/backlog-plan', createBacklogPlanRoutes(events, settingsService));
Expand Down Expand Up @@ -575,7 +599,7 @@ wss.on('connection', (ws: WebSocket) => {
logger.info('Sending event to client:', {
type,
messageLength: message.length,
sessionId: (payload as any)?.sessionId,
sessionId: (payload as Record<string, unknown>)?.sessionId,
});
ws.send(message);
} else {
Expand Down Expand Up @@ -641,8 +665,15 @@ terminalWss.on('connection', (ws: WebSocket, req: import('http').IncomingMessage
// Check if session exists
const session = terminalService.getSession(sessionId);
if (!session) {
logger.info(`Session ${sessionId} not found`);
ws.close(4004, 'Session not found');
logger.warn(
`Terminal session ${sessionId} not found. ` +
`The session may have exited, been deleted, or was never created. ` +
`Active terminal sessions: ${terminalService.getSessionCount()}`
);
ws.close(
4004,
'Session not found. The terminal session may have expired or been closed. Please create a new terminal.'
);
return;
}

Expand Down
5 changes: 1 addition & 4 deletions apps/server/src/lib/cli-detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ import { spawn, execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { createLogger } from '@automaker/utils';

const logger = createLogger('CliDetection');

export interface CliInfo {
name: string;
Expand Down Expand Up @@ -86,7 +83,7 @@ export async function detectCli(
options: CliDetectionOptions = {}
): Promise<CliDetectionResult> {
const config = CLI_CONFIGS[provider];
const { timeout = 5000, includeWsl = false, wslDistribution } = options;
const { timeout = 5000 } = options;
const issues: string[] = [];

const cliInfo: CliInfo = {
Expand Down
21 changes: 11 additions & 10 deletions apps/server/src/lib/error-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export interface ErrorClassification {
suggestedAction?: string;
retryable: boolean;
provider?: string;
context?: Record<string, any>;
context?: Record<string, unknown>;
}

export interface ErrorPattern {
Expand Down Expand Up @@ -180,7 +180,7 @@ const ERROR_PATTERNS: ErrorPattern[] = [
export function classifyError(
error: unknown,
provider?: string,
context?: Record<string, any>
context?: Record<string, unknown>
): ErrorClassification {
const errorText = getErrorText(error);

Expand Down Expand Up @@ -281,18 +281,19 @@ function getErrorText(error: unknown): string {

if (typeof error === 'object' && error !== null) {
// Handle structured error objects
const errorObj = error as any;
const errorObj = error as Record<string, unknown>;

if (errorObj.message) {
if (typeof errorObj.message === 'string') {
return errorObj.message;
}

if (errorObj.error?.message) {
return errorObj.error.message;
const nestedError = errorObj.error;
if (typeof nestedError === 'object' && nestedError !== null && 'message' in nestedError) {
return String((nestedError as Record<string, unknown>).message);
}

if (errorObj.error) {
return typeof errorObj.error === 'string' ? errorObj.error : JSON.stringify(errorObj.error);
if (nestedError) {
return typeof nestedError === 'string' ? nestedError : JSON.stringify(nestedError);
}

return JSON.stringify(error);
Expand All @@ -307,7 +308,7 @@ function getErrorText(error: unknown): string {
export function createErrorResponse(
error: unknown,
provider?: string,
context?: Record<string, any>
context?: Record<string, unknown>
): {
success: false;
error: string;
Expand Down Expand Up @@ -335,7 +336,7 @@ export function logError(
error: unknown,
provider?: string,
operation?: string,
additionalContext?: Record<string, any>
additionalContext?: Record<string, unknown>
): void {
const classification = classifyError(error, provider, {
operation,
Expand Down
62 changes: 62 additions & 0 deletions apps/server/src/lib/git-log-parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
export interface CommitFields {
hash: string;
shortHash: string;
author: string;
authorEmail: string;
date: string;
subject: string;
body: string;
}

export function parseGitLogOutput(output: string): CommitFields[] {
const commits: CommitFields[] = [];

// Split by NUL character to separate commits
const commitBlocks = output.split('\0').filter((block) => block.trim());

for (const block of commitBlocks) {
const allLines = block.split('\n');

// Skip leading empty lines that may appear at block boundaries
let startIndex = 0;
while (startIndex < allLines.length && allLines[startIndex].trim() === '') {
startIndex++;
}
const fields = allLines.slice(startIndex);

// Validate we have all expected fields (at least hash, shortHash, author, authorEmail, date, subject)
if (fields.length < 6) {
continue; // Skip malformed blocks
}

const commit: CommitFields = {
hash: fields[0].trim(),
shortHash: fields[1].trim(),
author: fields[2].trim(),
authorEmail: fields[3].trim(),
date: fields[4].trim(),
subject: fields[5].trim(),
body: fields.slice(6).join('\n').trim(),
};

commits.push(commit);
}

return commits;
}

/**
* Creates a commit object from parsed fields, matching the expected API response format
*/
export function createCommitFromFields(fields: CommitFields, files?: string[]) {
return {
hash: fields.hash,
shortHash: fields.shortHash,
author: fields.author,
authorEmail: fields.authorEmail,
date: fields.date,
subject: fields.subject,
body: fields.body,
files: files || [],
};
}
Loading