guito 1.0.7 → 1.0.8

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/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@ All notable changes to Guito are documented in this file. The project follows [S
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.0.8] - 2026-10-02
8
+
9
+ ### Changed
10
+
11
+ - Pull requests are now reviewed with the latest Opus model by default (the **Claude model** setting, `guito.aiReview.model` inside the extension). Pick **Claude Code's default** to go back to whatever Claude Code itself is set to.
12
+
13
+ ### Added
14
+
15
+ - VS Code extension: a Generate Commit Message button in the Source Control view's title bar drafts the message with Claude Code, using the same server call as the working panel's Generate message button, and fills the Git commit message box.
16
+ - Choose how hard Claude thinks. New **Review effort** and **Commit message effort** settings (`guito.aiReview.effort` and `guito.aiReview.commitMessageEffort` inside the extension) pass Claude Code's `--effort` level, from low to max, to pull request reviews and commit message drafts. Reviews default to high and commit messages to low; pick **Claude Code's default** to use whatever Claude Code itself is set to. An effort picker next to the model picker in a pull request's **Review** tab, preselected from the setting, overrides it for a single run, and the review's summary line shows which effort produced it.
17
+ - Choose how long drafted commit messages are. The new **Commit message style** setting (`guito.aiReview.commitMessageStyle` inside the extension) picks Oneliner, Brief or Descriptive. Oneliner is the default and drafts only the subject line; Brief adds a short description and Descriptive explains what changed and why in full.
18
+
19
+ ### Fixed
20
+
21
+ - The **Claude model** and **Commit message model** pickers in Settings, and the model picker in a pull request's Review tab, now show the model actually selected instead of the first option.
22
+
7
23
  ## [1.0.7] - 2026-10-01
8
24
 
9
25
  ### Added
package/README.md CHANGED
@@ -60,7 +60,7 @@ Click **Uncommitted changes** to open the staged and unstaged file lists. Click
60
60
 
61
61
  Enter a commit message and optional description, then choose **Commit staged changes**. Only staged changes are committed; unstaged edits remain on disk. Commit drafts stay available when you close and reopen the panel during the session.
62
62
 
63
- **Generate message** drafts a subject and description with Claude Code running on your machine, from the staged diff when anything is staged and from every uncommitted change otherwise, matching the language and style of your recent commits. The draft fills the boxes for you to review and edit; nothing is committed automatically. The **Commit message model** setting picks the model.
63
+ **Generate message** drafts a subject and description with Claude Code running on your machine, from the staged diff when anything is staged and from every uncommitted change otherwise, matching the language and style of your recent commits. The draft fills the boxes for you to review and edit; nothing is committed automatically. The **Commit message model** setting picks the model, and **Commit message style** picks how much it writes: Oneliner (the default) drafts only the subject line, Brief adds a short description, and Descriptive explains the change in full. In the VS Code extension, the same button is also in the Source Control view's title bar, and it fills the Git commit message box.
64
64
 
65
65
  ## CLI options
66
66
 
@@ -142,8 +142,11 @@ Inside VS Code the reviewer also runs while no Guito panel is open, so pull requ
142
142
  | `guito.aiReview.pollMinutes` | Minutes between checks. Defaults to 30. |
143
143
  | `guito.aiReview.includeDrafts` | Also review draft pull requests. Off by default. |
144
144
  | `guito.aiReview.claudePath` | Path to the Claude Code executable. Empty means Guito looks for it (see above). |
145
- | `guito.aiReview.model` | Which Claude model reviews pull requests. Default uses whatever Claude Code is set to. |
145
+ | `guito.aiReview.model` | Which Claude model reviews pull requests. Defaults to opus. |
146
146
  | `guito.aiReview.commitMessageModel` | Which Claude model drafts commit messages. Defaults to haiku. |
147
+ | `guito.aiReview.effort` | Claude Code effort level for reviews (`low` to `max`). Defaults to high; empty uses Claude Code's default. |
148
+ | `guito.aiReview.commitMessageEffort` | Claude Code effort level for commit messages. Defaults to low; empty uses Claude Code's default. |
149
+ | `guito.aiReview.commitMessageStyle` | `oneliner` (default), `brief`, or `descriptive`. |
147
150
  | `guito.aiReview.timeoutSeconds` | How long one review may take. Defaults to 600. |
148
151
  | `guito.aiReview.instructions` | Extra reviewing instructions, such as your team's conventions. |
149
152
 
package/bin/ai-review.js CHANGED
@@ -3,18 +3,38 @@ import { createHash } from 'node:crypto';
3
3
  import { homedir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  const SEVERITIES = ['blocker', 'concern', 'suggestion', 'nit'];
6
+ /** Effort levels Claude Code accepts for --effort; '' leaves it to Claude Code. */
7
+ export const AI_REVIEW_EFFORTS = ['', 'low', 'medium', 'high', 'xhigh', 'max'];
8
+ /** How much a drafted commit message says beyond its subject line. */
9
+ export const COMMIT_MESSAGE_STYLES = ['oneliner', 'brief', 'descriptive'];
10
+ /** The --effort arguments for one run, or none when Claude Code decides. */
11
+ export function effortArgs(effort) {
12
+ return effort ? ['--effort', effort] : [];
13
+ }
6
14
  export const DEFAULT_AI_REVIEW_CONFIG = {
7
15
  enabled: false,
8
16
  scope: 'reviewer',
9
17
  pollMinutes: 30,
10
18
  includeDrafts: false,
11
19
  claudePath: '',
12
- model: '',
20
+ model: 'opus',
13
21
  commitMessageModel: 'haiku',
22
+ effort: 'high',
23
+ commitMessageEffort: 'low',
24
+ commitMessageStyle: 'oneliner',
14
25
  timeoutSeconds: 600,
15
26
  maxDiffChars: 300000,
16
27
  instructions: '',
17
28
  };
29
+ /** A known effort level, or undefined so an unknown value keeps the default. */
30
+ export function normalizeEffort(value) {
31
+ if (typeof value !== 'string')
32
+ return undefined;
33
+ const effort = value.trim().toLowerCase();
34
+ return AI_REVIEW_EFFORTS.includes(effort)
35
+ ? effort
36
+ : undefined;
37
+ }
18
38
  /** Normalizes a partial config (settings file or VS Code) onto the defaults. */
19
39
  export function resolveAiReviewConfig(...sources) {
20
40
  const config = { ...DEFAULT_AI_REVIEW_CONFIG };
@@ -40,6 +60,16 @@ export function resolveAiReviewConfig(...sources) {
40
60
  if (typeof source.commitMessageModel === 'string') {
41
61
  config.commitMessageModel = source.commitMessageModel.trim();
42
62
  }
63
+ const effort = normalizeEffort(source.effort);
64
+ if (effort !== undefined)
65
+ config.effort = effort;
66
+ const commitMessageEffort = normalizeEffort(source.commitMessageEffort);
67
+ if (commitMessageEffort !== undefined)
68
+ config.commitMessageEffort = commitMessageEffort;
69
+ const commitMessageStyle = String(source.commitMessageStyle ?? '').trim().toLowerCase();
70
+ if (COMMIT_MESSAGE_STYLES.includes(commitMessageStyle)) {
71
+ config.commitMessageStyle = commitMessageStyle;
72
+ }
43
73
  if (Number.isFinite(source.timeoutSeconds)) {
44
74
  config.timeoutSeconds = Math.min(Math.max(Number(source.timeoutSeconds), 30), 3600);
45
75
  }
@@ -475,9 +505,14 @@ export function createAiReviewer(deps, initialConfig = DEFAULT_AI_REVIEW_CONFIG,
475
505
  const model = typeof options.model === 'string' && options.model.trim()
476
506
  ? options.model.trim()
477
507
  : config.model;
478
- // A different model earns a fresh look at code already reviewed at this
479
- // commit; when new commits exist, they alone are reviewed with it.
480
- const freshLook = (previousState.model ?? '') !== model &&
508
+ // A per-review effort wins too, '' included: it asks for Claude Code's
509
+ // own default rather than the configured level.
510
+ const effort = normalizeEffort(options.effort) ?? config.effort;
511
+ // A different model or effort earns a fresh look at code already reviewed
512
+ // at this commit; when new commits exist, they alone are reviewed with it.
513
+ // Reviews stored before effort was recorded are not re-run just for that.
514
+ const freshLook = ((previousState.model ?? '') !== model ||
515
+ (previousState.effort !== undefined && previousState.effort !== effort)) &&
481
516
  previousState.reviewedCommit === pullRequest.sourceCommit;
482
517
  if (!options.force &&
483
518
  !freshLook &&
@@ -522,8 +557,16 @@ export function createAiReviewer(deps, initialConfig = DEFAULT_AI_REVIEW_CONFIG,
522
557
  instructions: config.instructions,
523
558
  });
524
559
  const command = await resolveClaudeCommand(config.claudePath);
525
- const args = ['-p', '--output-format', 'json', ...(model ? ['--model', model] : [])];
526
- log(`AI review: reviewing pull request ${pullRequestId} with ${command}${model ? ` (${model})` : ''}`);
560
+ const args = [
561
+ '-p',
562
+ // Background reviews should not show up in the user's Claude Code session history.
563
+ '--no-session-persistence',
564
+ '--output-format',
565
+ 'json',
566
+ ...(model ? ['--model', model] : []),
567
+ ...effortArgs(effort),
568
+ ];
569
+ log(`AI review: reviewing pull request ${pullRequestId} with ${command}${model ? ` (${model})` : ''}${effort ? `, ${effort} effort` : ''}`);
527
570
  const raw = await runModel(prompt, {
528
571
  command,
529
572
  args,
@@ -545,6 +588,7 @@ export function createAiReviewer(deps, initialConfig = DEFAULT_AI_REVIEW_CONFIG,
545
588
  title: pullRequest.title,
546
589
  reviewedCommit: pullRequest.sourceCommit,
547
590
  model,
591
+ effort,
548
592
  reviewedAt: now().toISOString(),
549
593
  passes: current.passes + 1,
550
594
  status: 'idle',
@@ -1,4 +1,4 @@
1
- import { resolveClaudeCommand, runClaudeCli, } from './ai-review.js';
1
+ import { effortArgs, resolveClaudeCommand, runClaudeCli, } from './ai-review.js';
2
2
  /**
3
3
  * One-shot commit message drafting.
4
4
  *
@@ -42,9 +42,15 @@ function renderFile(file, budget) {
42
42
  rows.push('(diff truncated: file too large)');
43
43
  return rows.join('\n');
44
44
  }
45
- const COMMIT_MESSAGE_CONTRACT = `Reply with one JSON object and nothing else:
45
+ /** What the "description" field holds for each commit message style. */
46
+ const DESCRIPTION_BY_STYLE = {
47
+ oneliner: '\\"\\" always; the subject line is the whole message',
48
+ brief: 'one to three short sentences or bullet points on what changed and why, wrapped at 72 characters; \\"\\" when the subject alone is clear',
49
+ descriptive: 'a full body explaining what changed, why, and anything a reviewer should know, wrapped at 72 characters, using bullet points for separate changes',
50
+ };
51
+ const commitMessageContract = (style) => `Reply with one JSON object and nothing else:
46
52
  {"subject": "<one line, imperative mood, at most 72 characters, no trailing period>",
47
- "description": "<body explaining what and why, wrapped at 72 characters; \\"\\" when the subject alone is clear>"}
53
+ "description": "<${DESCRIPTION_BY_STYLE[style]}>"}
48
54
 
49
55
  Rules:
50
56
  - Summarize the change as a whole; do not walk through the diff file by file.
@@ -67,7 +73,7 @@ export function buildCommitMessagePrompt(input) {
67
73
  input.recentSubjects.map((subject) => ` - ${subject}`).join('\n')
68
74
  : '',
69
75
  '',
70
- COMMIT_MESSAGE_CONTRACT,
76
+ commitMessageContract(input.style),
71
77
  '',
72
78
  'DIFF',
73
79
  '====',
@@ -130,17 +136,34 @@ export async function generateCommitMessage(options) {
130
136
  files: options.files,
131
137
  recentSubjects: options.recentSubjects,
132
138
  maxDiffChars: Math.min(options.config.maxDiffChars, COMMIT_MESSAGE_MAX_DIFF_CHARS),
139
+ style: options.config.commitMessageStyle,
133
140
  });
134
141
  const command = await resolveClaudeCommand(options.config.claudePath);
135
- const args = ['-p', '--output-format', 'json', ...(model ? ['--model', model] : [])];
136
- options.log?.(`AI commit message: drafting with ${command} (${model})`);
142
+ const effort = options.config.commitMessageEffort;
143
+ const args = [
144
+ '-p',
145
+ // Background drafts should not show up in the user's Claude Code session history.
146
+ '--no-session-persistence',
147
+ '--output-format',
148
+ 'json',
149
+ ...(model ? ['--model', model] : []),
150
+ ...effortArgs(effort),
151
+ ];
152
+ options.log?.(`AI commit message: drafting with ${command} (${model}${effort ? `, ${effort} effort` : ''})`);
137
153
  const runModel = options.runModel ?? runClaudeCli;
138
154
  const raw = await runModel(prompt, {
139
155
  command,
140
156
  args,
141
157
  cwd: options.cwd,
142
158
  timeoutMs: Math.min(options.config.timeoutSeconds, COMMIT_MESSAGE_TIMEOUT_SECONDS) * 1000,
159
+ signal: options.signal,
143
160
  });
144
161
  const { subject, description } = parseCommitMessage(raw);
145
- return { subject, description, scope: options.scope, model };
162
+ // A one-liner stays one line even when the model adds a body anyway.
163
+ return {
164
+ subject,
165
+ description: options.config.commitMessageStyle === 'oneliner' ? '' : description,
166
+ scope: options.scope,
167
+ model,
168
+ };
146
169
  }
@@ -3319,13 +3319,14 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
3319
3319
  }
3320
3320
  });
3321
3321
  // Reviews the pull request now; "force" re-reviews the whole diff even when
3322
- // the merge source commit has not moved, and "model" overrides the
3323
- // configured Claude model for this one run.
3322
+ // the merge source commit has not moved, and "model" and "effort" override
3323
+ // the configured Claude model and effort for this one run.
3324
3324
  app.post('/api/azure-devops/pullrequests/:id/ai-review', async (req, resp) => {
3325
3325
  try {
3326
3326
  const state = await aiReviewer.review(Number(req.params?.id), {
3327
3327
  force: req.body?.force === true,
3328
3328
  model: typeof req.body?.model === 'string' ? req.body.model : '',
3329
+ effort: typeof req.body?.effort === 'string' ? req.body.effort : undefined,
3329
3330
  });
3330
3331
  if (state.status === 'error') {
3331
3332
  return resp.status(400).type('application/json').send({ error: state.error, state });
@@ -3371,44 +3372,49 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
3371
3372
  // ==================== AI commit message ====================
3372
3373
  // One Claude Code run over the pending changes; the reply fills the working
3373
3374
  // panel's message box. Nothing is committed and no Azure DevOps call is made.
3375
+ // The VS Code Source Control view drafts through the same function.
3376
+ const draftCommitMessage = async (model = '', signal) => {
3377
+ await mutationQueue;
3378
+ const snapshot = await working.snapshot();
3379
+ const staged = snapshot.stagedFiles;
3380
+ const scope = staged.length ? 'staged' : 'working';
3381
+ const files = scope === 'staged' ? staged : [...staged, ...snapshot.unstagedFiles];
3382
+ if (!files.length) {
3383
+ throw new Error('There are no changes to write a commit message about.');
3384
+ }
3385
+ let branch = '';
3386
+ try {
3387
+ branch = await currentBranch();
3388
+ }
3389
+ catch {
3390
+ branch = '';
3391
+ }
3392
+ let recentSubjects = [];
3393
+ try {
3394
+ recentSubjects = (await git.raw(['log', '-n', '12', '--pretty=format:%s']))
3395
+ .split(/\r?\n/)
3396
+ .map((subject) => subject.trim())
3397
+ .filter(Boolean);
3398
+ }
3399
+ catch {
3400
+ // No commits yet (fresh repository); the model manages without style.
3401
+ }
3402
+ return generateCommitMessage({
3403
+ branch,
3404
+ scope,
3405
+ files,
3406
+ recentSubjects,
3407
+ config: await refreshAiReviewConfig(),
3408
+ model,
3409
+ cwd: repositoryPath,
3410
+ signal,
3411
+ runModel: aiReviewModelImpl,
3412
+ log: (line) => onLog?.(line),
3413
+ });
3414
+ };
3374
3415
  app.post('/api/commit-message', async (req, resp) => {
3375
3416
  try {
3376
- await mutationQueue;
3377
- const snapshot = await working.snapshot();
3378
- const staged = snapshot.stagedFiles;
3379
- const scope = staged.length ? 'staged' : 'working';
3380
- const files = scope === 'staged' ? staged : [...staged, ...snapshot.unstagedFiles];
3381
- if (!files.length) {
3382
- throw new Error('There are no changes to write a commit message about.');
3383
- }
3384
- let branch = '';
3385
- try {
3386
- branch = await currentBranch();
3387
- }
3388
- catch {
3389
- branch = '';
3390
- }
3391
- let recentSubjects = [];
3392
- try {
3393
- recentSubjects = (await git.raw(['log', '-n', '12', '--pretty=format:%s']))
3394
- .split(/\r?\n/)
3395
- .map((subject) => subject.trim())
3396
- .filter(Boolean);
3397
- }
3398
- catch {
3399
- // No commits yet (fresh repository); the model manages without style.
3400
- }
3401
- const message = await generateCommitMessage({
3402
- branch,
3403
- scope,
3404
- files,
3405
- recentSubjects,
3406
- config: await refreshAiReviewConfig(),
3407
- model: strField(req.body?.model),
3408
- cwd: repositoryPath,
3409
- runModel: aiReviewModelImpl,
3410
- log: (line) => onLog?.(line),
3411
- });
3417
+ const message = await draftCommitMessage(strField(req.body?.model));
3412
3418
  return resp.type('application/json').send(message);
3413
3419
  }
3414
3420
  catch (err) {
@@ -3606,6 +3612,7 @@ export async function startGuitoServer({ repositoryPath, uiRoot, host, port = 80
3606
3612
  return {
3607
3613
  address,
3608
3614
  aiReview: aiReviewer,
3615
+ generateCommitMessage: draftCommitMessage,
3609
3616
  close: async () => {
3610
3617
  aiReviewer.stop();
3611
3618
  await app.close();
@@ -9,5 +9,5 @@
9
9
  <style>:root{--bg:#1e1e1e;--bg-alt:#232323;--bg-hover:#2a2d2e;--border:#3c3c3c;--text:#cccccc;--text-bright:#e8e8e8;--text-dim:#9d9d9d;--accent:#3794ff;--red:#f14c4c;--mono:ui-monospace, "Cascadia Code", "SF Mono", Menlo, Consolas, monospace}*{box-sizing:border-box}html,body{height:100%;margin:0}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;font-size:13px;color:var(--text);background:var(--bg);overflow:hidden}</style><link rel="stylesheet" href="styles-I7K2YZTW.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" href="styles-I7K2YZTW.css"></noscript></head>
10
10
  <body>
11
11
  <app-root></app-root>
12
- <script src="polyfills-5CFQRCPP.js" type="module"></script><script src="main-2QNMM6BC.js" type="module"></script></body>
12
+ <script src="polyfills-5CFQRCPP.js" type="module"></script><script src="main-YO57LHBH.js" type="module"></script></body>
13
13
  </html>