Skip to content

Commit 418e2f6

Browse files
committed
Allow disabling copilot commit generation
Closes #179
1 parent a49d19e commit 418e2f6

8 files changed

Lines changed: 89 additions & 3 deletions

File tree

app/src/lib/stores/app-store.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import {
5151
UpstreamRemoteName,
5252
} from '.'
5353
import type { CopilotFeature, CopilotModelSelections } from './copilot-store'
54+
import { DisabledCopilotModel } from './copilot-store'
5455
import {
5556
IBYOKProvider,
5657
loadBYOKProviders,
@@ -10714,6 +10715,12 @@ export class AppStore extends TypedBaseStore<IAppState> {
1071410715
if (raw === undefined) {
1071510716
continue
1071610717
}
10718+
// The sentinel that disables a feature isn't a real model, so it would
10719+
// otherwise be scrubbed as "missing". Preserve it verbatim.
10720+
if (raw === DisabledCopilotModel) {
10721+
updated[feature as CopilotFeature] = raw
10722+
continue
10723+
}
1071710724
const key = parseModelKey(raw)
1071810725
if (key.kind === 'byok') {
1071910726
const provider = this.byokProviders.find(p => p.id === key.providerId)

app/src/lib/stores/copilot-store.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ export type CopilotModelRequest =
8787
/** Copilot features that support per-model selection. */
8888
export type CopilotFeature = 'commit-message-generation'
8989

90+
/**
91+
* Sentinel value for hiding the Copilot button
92+
*/
93+
export const DisabledCopilotModel = 'hide-copilot-button'
94+
9095
/**
9196
* Per-feature model selections. An absent key means the default model
9297
* will be used for that feature.

app/src/ui/app.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
} from '../lib/app-state'
1313
import { Dispatcher } from './dispatcher'
1414
import { AppStore, GitHubUserStore, IssuesStore } from '../lib/stores'
15+
import { DisabledCopilotModel } from '../lib/stores/copilot-store'
1516
import { assertNever } from '../lib/fatal-error'
1617
import { shell } from '../lib/app-shell'
1718
import { updateStore, UpdateStatus } from './lib/update-store'
@@ -3991,6 +3992,10 @@ export class App extends React.Component<IAppProps, IAppState> {
39913992
shouldShowGenerateCommitMessageCallOut={
39923993
!this.state.commitMessageGenerationButtonClicked
39933994
}
3995+
commitMessageGenerationDisabled={
3996+
this.state.selectedCopilotModels['commit-message-generation'] ===
3997+
DisabledCopilotModel
3998+
}
39943999
skipCommitHooks={selectedState.state.skipCommitHooks}
39954000
signOffCommits={selectedState.state.signOffCommits}
39964001
allowEmptyCommit={selectedState.state.allowEmptyCommit}

app/src/ui/changes/filter-changes-list.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ interface IFilterChangesListProps {
171171
readonly onShowCommitProgress?: (() => void) | undefined
172172
readonly isGeneratingCommitMessage: boolean
173173
readonly shouldShowGenerateCommitMessageCallOut: boolean
174+
readonly commitMessageGenerationDisabled: boolean
174175
readonly commitToAmend: Commit | null
175176
readonly currentBranchProtected: boolean
176177
readonly currentRepoRulesInfo: RepoRulesInfo
@@ -1073,7 +1074,11 @@ export class FilterChangesList extends React.Component<
10731074
this.onConfirmCommitWithUnknownCoAuthors
10741075
}
10751076
onPersistCommitMessage={this.onPersistCommitMessage}
1076-
onGenerateCommitMessage={this.onGenerateCommitMessage}
1077+
onGenerateCommitMessage={
1078+
this.props.commitMessageGenerationDisabled
1079+
? undefined
1080+
: this.onGenerateCommitMessage
1081+
}
10771082
onCommitMessageFocusSet={this.onCommitMessageFocusSet}
10781083
onRefreshAuthor={this.onRefreshAuthor}
10791084
onShowPopup={this.onShowPopup}

app/src/ui/changes/sidebar.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ interface IChangesSidebarProps {
6363
readonly onShowCommitProgress: (() => void) | undefined
6464
readonly isGeneratingCommitMessage: boolean
6565
readonly shouldShowGenerateCommitMessageCallOut: boolean
66+
readonly commitMessageGenerationDisabled: boolean
6667
readonly commitToAmend: Commit | null
6768
readonly isPushPullFetchInProgress: boolean
6869
// Used in receiveProps, no-unused-prop-types doesn't know that
@@ -481,6 +482,9 @@ export class ChangesSidebar extends React.Component<IChangesSidebarProps, {}> {
481482
shouldShowGenerateCommitMessageCallOut={
482483
this.props.shouldShowGenerateCommitMessageCallOut
483484
}
485+
commitMessageGenerationDisabled={
486+
this.props.commitMessageGenerationDisabled
487+
}
484488
commitToAmend={this.props.commitToAmend}
485489
showCoAuthoredBy={showCoAuthoredBy}
486490
coAuthors={coAuthors}

app/src/ui/preferences/copilot.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { TabBar } from '../tab-bar'
1010
import type { ModelInfo } from '@github/copilot-sdk'
1111
import {
1212
DefaultCopilotModel,
13+
DisabledCopilotModel,
1314
type CopilotFeature,
1415
type CopilotModelSelections,
1516
} from '../../lib/stores/copilot-store'
@@ -159,6 +160,9 @@ export class CopilotPreferences extends React.Component<
159160
value={value}
160161
onChange={this.onCommitMessageModelChanged}
161162
>
163+
<option value={DisabledCopilotModel}>
164+
None (hide Copilot button)
165+
</option>
162166
{copilotModels.length > 0 && (
163167
<optgroup label="GitHub Copilot">
164168
{copilotModels.map(m => (
@@ -204,6 +208,10 @@ export class CopilotPreferences extends React.Component<
204208
byokProviders: ReadonlyArray<IBYOKProvider>,
205209
raw: string | null
206210
): string {
211+
if (raw === DisabledCopilotModel) {
212+
return DisabledCopilotModel
213+
}
214+
207215
if (raw !== null) {
208216
const key = parseModelKey(raw)
209217
if (key.kind === 'byok') {

app/src/ui/repository.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,12 @@ interface IRepositoryViewProps {
7070
readonly accounts: ReadonlyArray<Account>
7171
readonly shouldShowGenerateCommitMessageCallOut: boolean
7272

73+
/**
74+
* Whether the user has disabled Copilot commit message generation by
75+
* selecting "None" as the model. When true, the Copilot button is hidden.
76+
*/
77+
readonly commitMessageGenerationDisabled: boolean
78+
7379
/**
7480
* A value indicating whether or not the application is currently presenting
7581
* a modal dialog such as the preferences, or an error dialog
@@ -361,6 +367,9 @@ export class RepositoryView extends React.Component<
361367
shouldShowGenerateCommitMessageCallOut={
362368
this.props.shouldShowGenerateCommitMessageCallOut
363369
}
370+
commitMessageGenerationDisabled={
371+
this.props.commitMessageGenerationDisabled
372+
}
364373
commitToAmend={this.props.state.commitToAmend}
365374
isPushPullFetchInProgress={this.props.state.isPushPullFetchInProgress}
366375
focusCommitMessage={this.props.focusCommitMessage}

app/test/unit/ui/copilot-preferences-test.tsx

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { render, screen, fireEvent } from '../../helpers/ui/render'
55
import { CopilotPreferences } from '../../../src/ui/preferences/copilot'
66
import {
77
DefaultCopilotModel,
8+
DisabledCopilotModel,
89
type CopilotFeature,
910
} from '../../../src/lib/stores/copilot-store'
1011
import type { ModelInfo } from '@github/copilot-sdk'
@@ -103,8 +104,50 @@ describe('CopilotPreferences', () => {
103104
assert.strictEqual(optgroups[0].label, 'GitHub Copilot')
104105

105106
const options = view.container.querySelectorAll('option')
106-
assert.strictEqual(options[0].textContent, 'GPT-5 mini (default)')
107-
assert.strictEqual(options[1].textContent, 'Claude Sonnet')
107+
assert.strictEqual(options[0].textContent, 'None (hide Copilot button)')
108+
assert.strictEqual(options[1].textContent, 'GPT-5 mini (default)')
109+
assert.strictEqual(options[2].textContent, 'Claude Sonnet')
110+
})
111+
112+
it('offers a "None" option to disable commit message generation', () => {
113+
const view = render(<CopilotPreferences {...defaults()} />)
114+
const options = Array.from(view.container.querySelectorAll('option'))
115+
const none = options.find(o => o.value === DisabledCopilotModel)
116+
assert.ok(none)
117+
assert.strictEqual(none!.textContent, 'None (hide Copilot button)')
118+
})
119+
120+
it('selects the None option when generation is disabled', () => {
121+
const view = render(
122+
<CopilotPreferences
123+
{...defaults()}
124+
selectedCopilotModels={{
125+
'commit-message-generation': DisabledCopilotModel,
126+
}}
127+
/>
128+
)
129+
const select = view.container.querySelector('select') as HTMLSelectElement
130+
assert.strictEqual(select.value, DisabledCopilotModel)
131+
})
132+
133+
it('emits the None value when generation is disabled', () => {
134+
const changed: Array<{ feature: CopilotFeature; model: string | null }> = []
135+
const view = render(
136+
<CopilotPreferences
137+
{...defaults()}
138+
onSelectedCopilotModelChanged={(f, m) =>
139+
changed.push({ feature: f, model: m })
140+
}
141+
/>
142+
)
143+
const select = view.container.querySelector('select') as HTMLSelectElement
144+
fireEvent.change(select, { target: { value: DisabledCopilotModel } })
145+
assert.deepStrictEqual(changed, [
146+
{
147+
feature: 'commit-message-generation',
148+
model: DisabledCopilotModel,
149+
},
150+
])
108151
})
109152

110153
it('renders a BYOK optgroup per provider', () => {

0 commit comments

Comments
 (0)