acdev 1.0.8 → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/public/index.html CHANGED
@@ -138,6 +138,24 @@
138
138
  or a browse link. Press <kbd class="enqueue-kbd">Enter</kbd> to add.
139
139
  </p>
140
140
 
141
+ <div class="enqueue-branch-field">
142
+ <div class="enqueue-branch-label-row">
143
+ <label class="field-label" for="preferred-branch">Branch name</label>
144
+ <span class="enqueue-branch-optional">Optional</span>
145
+ </div>
146
+ <input
147
+ id="preferred-branch"
148
+ class="input enqueue-branch-input"
149
+ type="text"
150
+ spellcheck="false"
151
+ autocomplete="off"
152
+ placeholder="your-branch-name"
153
+ >
154
+ <p class="enqueue-input-hint">
155
+ Leave empty to keep feat/fix + ticket title. If set, that name is used instead.
156
+ </p>
157
+ </div>
158
+
141
159
  <div class="enqueue-info-banner" id="enqueue-info-banner" role="note">
142
160
  <svg class="enqueue-info-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
143
161
  <circle cx="12" cy="12" r="10"/>
@@ -155,10 +173,6 @@
155
173
 
156
174
  <div class="enqueue-actions">
157
175
  <div class="agent-picker" id="overview-agent-picker" aria-label="Agent">
158
- <select id="overview-llm-provider" class="sr-only" aria-hidden="true" tabindex="-1">
159
- <option value="claude">Claude</option>
160
- <option value="openrouter">OpenRouter</option>
161
- </select>
162
176
  <input type="hidden" id="overview-model" value="">
163
177
  <button
164
178
  type="button"
@@ -171,15 +185,11 @@
171
185
  <span class="agent-pill-icon agent-pill-icon--claude" id="overview-agent-icon" aria-hidden="true"></span>
172
186
  <span class="agent-pill-body">
173
187
  <span class="agent-pill-label">Agent</span>
174
- <span class="agent-pill-value" id="overview-agent-value">Claude • —</span>
188
+ <span class="agent-pill-value" id="overview-agent-value">—</span>
175
189
  </span>
176
190
  <svg class="agent-pill-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>
177
191
  </button>
178
192
  <div class="agent-picker-panel" id="overview-agent-panel" role="dialog" aria-label="Choose agent model" aria-hidden="true">
179
- <div class="agent-picker-tabs" role="tablist" aria-label="Provider">
180
- <button type="button" class="agent-picker-tab is-active" data-provider="claude" role="tab" aria-selected="true">Claude</button>
181
- <button type="button" class="agent-picker-tab" data-provider="openrouter" role="tab" aria-selected="false">OpenRouter</button>
182
- </div>
183
193
  <label class="sr-only" for="overview-agent-search">Search models</label>
184
194
  <input
185
195
  id="overview-agent-search"
@@ -538,21 +548,6 @@
538
548
  </div>
539
549
  </div>
540
550
  </div>
541
-
542
- <div class="card settings-card">
543
- <div class="rules-heading">OpenRouter authentication</div>
544
- <p id="settings-openrouter-status" class="auth-status" role="status">Checking OpenRouter auth…</p>
545
- <div class="settings-grid">
546
- <div class="settings-field settings-field-full">
547
- <label class="field-label" for="settings-openrouter-key">OpenRouter API key</label>
548
- <input id="settings-openrouter-key" class="input" type="password" name="openrouterApiKey" autocomplete="new-password" placeholder="Leave blank to keep existing">
549
- <button type="button" class="auth-clear hidden" id="settings-openrouter-key-clear">Clear saved API key</button>
550
- <p class="field-hint" id="settings-openrouter-key-hint">
551
- Required when LLM provider is OpenRouter. Get a key at <a href="https://openrouter.ai/keys" target="_blank" rel="noopener noreferrer">openrouter.ai/keys</a>. Stored as <code>OPENROUTER_API_KEY</code> in <code>.acdev/.env</code>.
552
- </p>
553
- </div>
554
- </div>
555
- </div>
556
551
  </div>
557
552
  </div>
558
553
 
@@ -649,15 +644,6 @@
649
644
  </p>
650
645
  <div class="card settings-card">
651
646
  <div class="settings-grid">
652
- <div class="settings-field">
653
- <label class="field-label" for="settings-llm-provider">LLM provider</label>
654
- <select id="settings-llm-provider" class="input" name="llmProvider">
655
- <option value="claude">Claude (Anthropic / Claude Code)</option>
656
- <option value="openrouter">OpenRouter</option>
657
- </select>
658
- <p class="field-hint" id="settings-llm-provider-hint">Agent runs use the selected provider and model below.</p>
659
- </div>
660
-
661
647
  <div class="settings-field">
662
648
  <label class="field-label" for="settings-base-branch">Base branch</label>
663
649
  <input id="settings-base-branch" class="input" type="text" name="baseBranch" autocomplete="off" required>
package/public/styles.css CHANGED
@@ -459,6 +459,38 @@ a { color: var(--primary); text-underline-offset: 3px; }
459
459
  color: var(--text-muted);
460
460
  }
461
461
 
462
+ .enqueue-branch-field {
463
+ display: flex;
464
+ flex-direction: column;
465
+ gap: 6px;
466
+ }
467
+
468
+ .enqueue-branch-field .field-label {
469
+ margin-bottom: 0;
470
+ }
471
+
472
+ .enqueue-branch-field .enqueue-input-hint {
473
+ margin: 0;
474
+ }
475
+
476
+ .enqueue-branch-label-row {
477
+ display: flex;
478
+ align-items: baseline;
479
+ justify-content: space-between;
480
+ gap: 8px;
481
+ }
482
+
483
+ .enqueue-branch-optional {
484
+ font-size: 11px;
485
+ font-weight: 500;
486
+ color: var(--text-muted);
487
+ }
488
+
489
+ .enqueue-branch-input {
490
+ font-family: 'JetBrains Mono', ui-monospace, monospace;
491
+ font-size: 13px;
492
+ }
493
+
462
494
  .enqueue-kbd {
463
495
  display: inline-block;
464
496
  padding: 1px 6px;
@@ -556,10 +588,6 @@ a { color: var(--primary); text-underline-offset: 3px; }
556
588
  background: linear-gradient(145deg, #e8a87c 0%, #d97706 100%);
557
589
  }
558
590
 
559
- .agent-pill-icon--openrouter {
560
- background: linear-gradient(145deg, #7c8cf5 0%, #5b4fe8 100%);
561
- }
562
-
563
591
  .agent-pill-body {
564
592
  flex: 1;
565
593
  min-width: 0;
@@ -2298,13 +2326,6 @@ body.diff-fs-open {
2298
2326
  cursor: pointer;
2299
2327
  }
2300
2328
 
2301
- .model-picker-option-title-row {
2302
- display: flex;
2303
- align-items: center;
2304
- justify-content: space-between;
2305
- gap: 8px;
2306
- }
2307
-
2308
2329
  .model-picker-option-title {
2309
2330
  min-width: 0;
2310
2331
  overflow: hidden;
@@ -2312,26 +2333,6 @@ body.diff-fs-open {
2312
2333
  white-space: nowrap;
2313
2334
  }
2314
2335
 
2315
- .model-picker-badge {
2316
- flex-shrink: 0;
2317
- padding: 1px 6px;
2318
- border-radius: 999px;
2319
- font-size: 10px;
2320
- font-weight: 600;
2321
- letter-spacing: 0.02em;
2322
- text-transform: uppercase;
2323
- }
2324
-
2325
- .model-picker-badge--agent {
2326
- color: var(--primary);
2327
- background: color-mix(in srgb, var(--primary) 14%, transparent);
2328
- }
2329
-
2330
- .model-picker-badge--reference {
2331
- color: var(--text-muted);
2332
- background: color-mix(in srgb, var(--text-muted) 12%, transparent);
2333
- }
2334
-
2335
2336
  .model-picker-option:hover,
2336
2337
  .model-picker-option.is-active {
2337
2338
  background: var(--surface-2);
package/src/agent.js CHANGED
@@ -2,7 +2,6 @@ import { query } from '@anthropic-ai/claude-agent-sdk';
2
2
  import { execFile } from 'node:child_process';
3
3
  import { promisify } from 'node:util';
4
4
  import { ensureNoAiAttributionSettings, getIssueTitle } from './git.js';
5
- import { isValidLlmProvider } from './models.js';
6
5
  import { extractUsageFromResult } from './usage.js';
7
6
 
8
7
  const execFileAsync = promisify(execFile);
@@ -397,66 +396,6 @@ async function fetchIssueTitle(issueUrl, worktreePath) {
397
396
  */
398
397
  export const DISALLOWED_AGENT_TOOLS = ['Task', 'TaskOutput', 'AskUserQuestion'];
399
398
 
400
- const OPENROUTER_ANTHROPIC_BASE_URL = 'https://openrouter.ai/api';
401
-
402
- /**
403
- * Run an agent query with provider-specific Anthropic env overrides.
404
- * OpenRouter uses Claude Agent SDK via Anthropic-compatible "skin" routing.
405
- * @template T
406
- * @param {object} config
407
- * @param {() => Promise<T>} fn
408
- * @returns {Promise<T>}
409
- */
410
- export async function withLlmProviderEnv(config, fn) {
411
- const provider = isValidLlmProvider(config.llmProvider) ? config.llmProvider : 'claude';
412
- if (provider !== 'openrouter') {
413
- return fn();
414
- }
415
-
416
- const apiKey = (process.env.OPENROUTER_API_KEY || '').trim();
417
- if (!apiKey) {
418
- throw new Error(
419
- 'OpenRouter is not authenticated. Add OPENROUTER_API_KEY in Settings → Authentication.'
420
- );
421
- }
422
-
423
- /** @type {Record<string, string | undefined>} */
424
- const saved = {
425
- ANTHROPIC_BASE_URL: process.env.ANTHROPIC_BASE_URL,
426
- ANTHROPIC_AUTH_TOKEN: process.env.ANTHROPIC_AUTH_TOKEN,
427
- ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
428
- HTTP_REFERER: process.env.HTTP_REFERER,
429
- X_TITLE: process.env.X_TITLE,
430
- CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS: process.env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS,
431
- };
432
-
433
- process.env.ANTHROPIC_BASE_URL = OPENROUTER_ANTHROPIC_BASE_URL;
434
- process.env.ANTHROPIC_AUTH_TOKEN = apiKey;
435
- process.env.ANTHROPIC_API_KEY = '';
436
- // OpenRouter rejects Anthropic-only beta headers on some models.
437
- if (!process.env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS) {
438
- process.env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS = '1';
439
- }
440
- if (!process.env.HTTP_REFERER) {
441
- process.env.HTTP_REFERER = 'https://github.com/acdev';
442
- }
443
- if (!process.env.X_TITLE) {
444
- process.env.X_TITLE = 'acdev';
445
- }
446
-
447
- try {
448
- return await fn();
449
- } finally {
450
- for (const [key, value] of Object.entries(saved)) {
451
- if (value === undefined) {
452
- delete process.env[key];
453
- } else {
454
- process.env[key] = value;
455
- }
456
- }
457
- }
458
- }
459
-
460
399
  /**
461
400
  * Prefer the first successful SDK result. A known SDK bug can emit a follow-up
462
401
  * `error_during_execution` right after success when async subagents finish.
@@ -483,78 +422,76 @@ export function mergeAgentResult(current, incoming) {
483
422
  * }} params
484
423
  */
485
424
  async function runAgentQuery({ prompt, worktreePath, config, onEvent, queryFn = query }) {
486
- return withLlmProviderEnv(config, async () => {
487
- const timeoutMs = config.agentTimeoutMs ?? 900_000;
488
- let lastMessage = null;
489
- let resultMessage = null;
490
- const abortController = new AbortController();
491
-
492
- // Claude Agent SDK defaults to injecting Co-Authored-By trailers into commit
493
- // instructions unless attribution is cleared via Claude settings.
494
- ensureNoAiAttributionSettings(worktreePath);
495
-
496
- const runLoop = async () => {
497
- try {
498
- for await (const message of queryFn({
499
- // String prompt = single-turn SDK input (still uses stream-json transport).
500
- prompt,
501
- options: {
502
- cwd: worktreePath,
503
- allowedTools: config.allowedTools,
504
- disallowedTools: DISALLOWED_AGENT_TOOLS,
505
- permissionMode: 'acceptEdits',
506
- maxTurns: config.maxAgentTurns,
507
- model: config.model || 'claude-sonnet-5',
508
- // Load only local settings so our empty attribution wins without
509
- // pulling in unrelated user settings.
510
- settingSources: ['local'],
511
- abortController,
512
- },
513
- })) {
514
- onEvent(message);
515
- lastMessage = message;
516
- if (message?.type === 'result') {
517
- resultMessage = mergeAgentResult(resultMessage, message);
518
- // First success is authoritative — stop before SDK async-task teardown
519
- // can overwrite it with a spurious streaming-mode error.
520
- if (message.subtype === 'success') {
521
- abortController.abort();
522
- break;
523
- }
524
- }
525
- }
526
- } catch (err) {
527
- // Aborting after success can surface as an iterator/abort error.
528
- if (resultMessage?.subtype === 'success') return;
529
- throw err;
530
- }
531
- };
425
+ const timeoutMs = config.agentTimeoutMs ?? 900_000;
426
+ let lastMessage = null;
427
+ let resultMessage = null;
428
+ const abortController = new AbortController();
429
+
430
+ // Claude Agent SDK defaults to injecting Co-Authored-By trailers into commit
431
+ // instructions unless attribution is cleared via Claude settings.
432
+ ensureNoAiAttributionSettings(worktreePath);
532
433
 
533
- let timeoutId;
434
+ const runLoop = async () => {
534
435
  try {
535
- await Promise.race([
536
- runLoop(),
537
- new Promise((_, reject) => {
538
- timeoutId = setTimeout(() => {
436
+ for await (const message of queryFn({
437
+ // String prompt = single-turn SDK input (still uses stream-json transport).
438
+ prompt,
439
+ options: {
440
+ cwd: worktreePath,
441
+ allowedTools: config.allowedTools,
442
+ disallowedTools: DISALLOWED_AGENT_TOOLS,
443
+ permissionMode: 'acceptEdits',
444
+ maxTurns: config.maxAgentTurns,
445
+ model: config.model || 'claude-sonnet-5',
446
+ // Load only local settings so our empty attribution wins without
447
+ // pulling in unrelated user settings.
448
+ settingSources: ['local'],
449
+ abortController,
450
+ },
451
+ })) {
452
+ onEvent(message);
453
+ lastMessage = message;
454
+ if (message?.type === 'result') {
455
+ resultMessage = mergeAgentResult(resultMessage, message);
456
+ // First success is authoritative — stop before SDK async-task teardown
457
+ // can overwrite it with a spurious streaming-mode error.
458
+ if (message.subtype === 'success') {
539
459
  abortController.abort();
540
- reject(new Error(`Agent timed out after ${timeoutMs}ms`));
541
- }, timeoutMs);
542
- }),
543
- ]);
544
- } finally {
545
- if (timeoutId) clearTimeout(timeoutId);
460
+ break;
461
+ }
462
+ }
463
+ }
464
+ } catch (err) {
465
+ // Aborting after success can surface as an iterator/abort error.
466
+ if (resultMessage?.subtype === 'success') return;
467
+ throw err;
546
468
  }
469
+ };
547
470
 
548
- if (!resultMessage || resultMessage.subtype !== 'success') {
549
- const detail = lastMessage ? JSON.stringify(lastMessage) : 'no result received';
550
- throw new Error(`Agent did not complete successfully: ${detail}`);
551
- }
471
+ let timeoutId;
472
+ try {
473
+ await Promise.race([
474
+ runLoop(),
475
+ new Promise((_, reject) => {
476
+ timeoutId = setTimeout(() => {
477
+ abortController.abort();
478
+ reject(new Error(`Agent timed out after ${timeoutMs}ms`));
479
+ }, timeoutMs);
480
+ }),
481
+ ]);
482
+ } finally {
483
+ if (timeoutId) clearTimeout(timeoutId);
484
+ }
552
485
 
553
- const resultText = resultMessage.result ?? '';
554
- const meta = extractPrMetadata(resultText);
555
- const usage = extractUsageFromResult(resultMessage);
556
- return { resultText, meta, usage };
557
- });
486
+ if (!resultMessage || resultMessage.subtype !== 'success') {
487
+ const detail = lastMessage ? JSON.stringify(lastMessage) : 'no result received';
488
+ throw new Error(`Agent did not complete successfully: ${detail}`);
489
+ }
490
+
491
+ const resultText = resultMessage.result ?? '';
492
+ const meta = extractPrMetadata(resultText);
493
+ const usage = extractUsageFromResult(resultMessage);
494
+ return { resultText, meta, usage };
558
495
  }
559
496
 
560
497
  function stubAgentResult(onEvent, title, body) {