Skip to content

Commit 8405b5d

Browse files
feat: model picker.
1 parent 5e6be89 commit 8405b5d

6 files changed

Lines changed: 199 additions & 33 deletions

File tree

playwright/app.spec.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,7 @@ test('BYOT remembers selected repository across reloads', async ({ page }) => {
433433
test('renders default playground preview', async ({ page }) => {
434434
await waitForInitialRender(page)
435435

436-
await page.getByLabel('ShadowRoot (open)').uncheck()
436+
await page.getByLabel('ShadowRoot').uncheck()
437437
await expect(page.locator('#status')).toHaveText('Rendered')
438438
await expectPreviewHasRenderedContent(page)
439439
})
@@ -668,7 +668,7 @@ test('renders in react mode with css modules', async ({ page }) => {
668668
await ensurePanelToolsVisible(page, 'component')
669669
await ensurePanelToolsVisible(page, 'styles')
670670

671-
await page.getByLabel('ShadowRoot (open)').uncheck()
671+
await page.getByLabel('ShadowRoot').uncheck()
672672
await page.locator('#render-mode').selectOption('react')
673673
await page.locator('#style-mode').selectOption('module')
674674
await expect(page.locator('#status')).toHaveText('Rendered')
@@ -678,7 +678,7 @@ test('renders in react mode with css modules', async ({ page }) => {
678678
test('transpiles TypeScript annotations in component source', async ({ page }) => {
679679
await waitForInitialRender(page)
680680

681-
await page.getByLabel('ShadowRoot (open)').uncheck()
681+
await page.getByLabel('ShadowRoot').uncheck()
682682
await setComponentEditorSource(
683683
page,
684684
[
@@ -762,7 +762,7 @@ test('react mode executes default React import without TDZ runtime failure', asy
762762

763763
await ensurePanelToolsVisible(page, 'component')
764764

765-
await page.getByLabel('ShadowRoot (open)').uncheck()
765+
await page.getByLabel('ShadowRoot').uncheck()
766766
await page.locator('#render-mode').selectOption('react')
767767
await setComponentEditorSource(
768768
page,
@@ -854,7 +854,7 @@ test('renders with less style mode', async ({ page }) => {
854854

855855
await ensurePanelToolsVisible(page, 'styles')
856856

857-
await page.getByLabel('ShadowRoot (open)').uncheck()
857+
await page.getByLabel('ShadowRoot').uncheck()
858858
await page.locator('#style-mode').selectOption('less')
859859
await expect(page.locator('#status')).toHaveText('Rendered')
860860
await expectPreviewHasRenderedContent(page)
@@ -865,7 +865,7 @@ test('renders with sass style mode', async ({ page }) => {
865865

866866
await ensurePanelToolsVisible(page, 'styles')
867867

868-
await page.getByLabel('ShadowRoot (open)').uncheck()
868+
await page.getByLabel('ShadowRoot').uncheck()
869869
await page.locator('#style-mode').selectOption('sass')
870870
await expect(page.locator('#status')).toHaveText('Rendered')
871871
await expectPreviewHasRenderedContent(page)

src/app.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const aiChatDrawer = document.getElementById('ai-chat-drawer')
3030
const aiChatClose = document.getElementById('ai-chat-close')
3131
const aiChatClear = document.getElementById('ai-chat-clear')
3232
const aiChatPrompt = document.getElementById('ai-chat-prompt')
33+
const aiChatModel = document.getElementById('ai-chat-model')
3334
const aiChatIncludeEditors = document.getElementById('ai-chat-include-editors')
3435
const aiChatSend = document.getElementById('ai-chat-send')
3536
const aiChatStatus = document.getElementById('ai-chat-status')
@@ -477,6 +478,7 @@ const githubAiContextState = {
477478
let chatDrawerController = {
478479
setOpen: () => {},
479480
setSelectedRepository: () => {},
481+
setToken: () => {},
480482
dispose: () => {},
481483
}
482484

@@ -509,6 +511,7 @@ const byotControls = createGitHubByotControls({
509511
onTokenChange: token => {
510512
githubAiContextState.token = token
511513
syncAiChatTokenVisibility(token)
514+
chatDrawerController.setToken(token)
512515
},
513516
setStatus,
514517
})
@@ -527,6 +530,7 @@ chatDrawerController = createGitHubChatDrawer({
527530
drawer: aiChatDrawer,
528531
closeButton: aiChatClose,
529532
promptInput: aiChatPrompt,
533+
modelSelect: aiChatModel,
530534
includeEditorsContextToggle: aiChatIncludeEditors,
531535
sendButton: aiChatSend,
532536
clearButton: aiChatClear,

src/index.html

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,7 @@ <h2>Preview</h2>
455455
</label>
456456
<label class="toggle">
457457
<input id="shadow-toggle" type="checkbox" checked />
458-
ShadowRoot (open)
458+
ShadowRoot
459459
</label>
460460
<button
461461
class="hint-icon shadow-hint"
@@ -560,6 +560,13 @@ <h2>AI Chat</h2>
560560
placeholder="Ask for help developing your component and styles"
561561
></textarea>
562562

563+
<label class="ai-chat-model-picker" for="ai-chat-model">
564+
<span class="sr-only">Model</span>
565+
<select id="ai-chat-model" aria-label="Chat model" disabled>
566+
<option value="openai/gpt-4.1-mini" selected>openai/gpt-4.1-mini</option>
567+
</select>
568+
</label>
569+
563570
<label class="ai-chat-context-toggle" for="ai-chat-include-editors">
564571
<input type="checkbox" id="ai-chat-include-editors" checked />
565572
Send JSX + CSS editor context

src/modules/github-api.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,51 @@ const githubModelsApiUrl = 'https://models.github.ai/inference/chat/completions'
33

44
export const defaultGitHubChatModel = 'openai/gpt-4.1-mini'
55

6+
/* Local model options avoid browser CORS failures when calling catalog endpoints directly. */
7+
export const githubChatModelOptions = [
8+
'openai/gpt-4.1-mini',
9+
'openai/gpt-4.1',
10+
'openai/gpt-4.1-nano',
11+
'openai/gpt-4o',
12+
'openai/gpt-4o-mini',
13+
'openai/gpt-5',
14+
'openai/gpt-5-chat',
15+
'openai/gpt-5-mini',
16+
'openai/gpt-5-nano',
17+
'openai/o1',
18+
'openai/o1-mini',
19+
'openai/o1-preview',
20+
'openai/o3',
21+
'openai/o3-mini',
22+
'openai/o4-mini',
23+
'ai21-labs/ai21-jamba-1.5-large',
24+
'cohere/cohere-command-a',
25+
'cohere/cohere-command-r-08-2024',
26+
'cohere/cohere-command-r-plus-08-2024',
27+
'xai/grok-3',
28+
'xai/grok-3-mini',
29+
'deepseek/deepseek-r1',
30+
'deepseek/deepseek-r1-0528',
31+
'deepseek/deepseek-v3-0324',
32+
'meta/llama-3.2-11b-vision-instruct',
33+
'meta/llama-3.2-90b-vision-instruct',
34+
'meta/llama-3.3-70b-instruct',
35+
'meta/llama-4-maverick-17b-128e-instruct-fp8',
36+
'meta/llama-4-scout-17b-16e-instruct',
37+
'meta/meta-llama-3.1-405b-instruct',
38+
'meta/meta-llama-3.1-8b-instruct',
39+
'mistral-ai/codestral-2501',
40+
'mistral-ai/ministral-3b',
41+
'mistral-ai/mistral-medium-2505',
42+
'mistral-ai/mistral-small-2503',
43+
'microsoft/mai-ds-r1',
44+
'microsoft/phi-4',
45+
'microsoft/phi-4-mini-instruct',
46+
'microsoft/phi-4-mini-reasoning',
47+
'microsoft/phi-4-multimodal-instruct',
48+
'microsoft/phi-4-reasoning',
49+
]
50+
651
const parseNextPageUrlFromLinkHeader = linkHeader => {
752
if (typeof linkHeader !== 'string' || !linkHeader.trim()) {
853
return null

src/modules/github-chat-drawer.js

Lines changed: 106 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
defaultGitHubChatModel,
3+
githubChatModelOptions,
34
requestGitHubChatCompletion,
45
streamGitHubChatCompletion,
56
} from './github-api.js'
@@ -12,17 +13,34 @@ const toChatText = value => {
1213
return value.trim()
1314
}
1415

15-
const toModelLabelText = value => {
16+
const toModelId = value => {
1617
if (typeof value !== 'string') {
17-
return ''
18+
return defaultGitHubChatModel
1819
}
1920

2021
const model = value.trim()
21-
if (!model) {
22-
return ''
22+
return model || defaultGitHubChatModel
23+
}
24+
25+
const isModelAccessError = error => {
26+
const message = error instanceof Error ? error.message.toLowerCase() : ''
27+
if (!message) {
28+
return false
2329
}
2430

25-
return `(${model})`
31+
return (
32+
(message.includes('model') && message.includes('access')) ||
33+
(message.includes('model') && message.includes('permission')) ||
34+
(message.includes('model') && message.includes('not available')) ||
35+
(message.includes('model') && message.includes('not found')) ||
36+
(message.includes('model') && message.includes('not enabled')) ||
37+
(message.includes('forbidden') && message.includes('model'))
38+
)
39+
}
40+
41+
const formatModelAccessErrorMessage = selectedModel => {
42+
const model = toModelId(selectedModel)
43+
return `Selected model "${model}" is not available for this token. Choose a different model.`
2644
}
2745

2846
const toRepositoryLabel = repository => {
@@ -59,6 +77,7 @@ export const createGitHubChatDrawer = ({
5977
drawer,
6078
closeButton,
6179
promptInput,
80+
modelSelect,
6281
sendButton,
6382
clearButton,
6483
statusNode,
@@ -82,6 +101,7 @@ export const createGitHubChatDrawer = ({
82101
setOpen: () => {},
83102
isOpen: () => false,
84103
setSelectedRepository: () => {},
104+
setToken: () => {},
85105
dispose: () => {},
86106
}
87107
}
@@ -140,6 +160,53 @@ export const createGitHubChatDrawer = ({
140160
pendingAbortController = null
141161
}
142162

163+
const setModelSelectDisabled = isDisabled => {
164+
if (!(modelSelect instanceof HTMLSelectElement)) {
165+
return
166+
}
167+
168+
modelSelect.disabled = isDisabled
169+
}
170+
171+
const replaceModelOptions = ({ modelIds, selectedModel }) => {
172+
if (!(modelSelect instanceof HTMLSelectElement)) {
173+
return
174+
}
175+
176+
const nextSelectedModel = toModelId(selectedModel)
177+
const nextModelIds = [...new Set([defaultGitHubChatModel, ...modelIds])]
178+
179+
modelSelect.replaceChildren()
180+
181+
for (const modelId of nextModelIds) {
182+
const option = document.createElement('option')
183+
option.value = modelId
184+
option.textContent = modelId
185+
option.selected = modelId === nextSelectedModel
186+
modelSelect.append(option)
187+
}
188+
189+
if (!nextModelIds.includes(nextSelectedModel)) {
190+
modelSelect.value = defaultGitHubChatModel
191+
}
192+
}
193+
194+
const getSelectedModel = () => {
195+
if (!(modelSelect instanceof HTMLSelectElement)) {
196+
return defaultGitHubChatModel
197+
}
198+
199+
return toModelId(modelSelect.value)
200+
}
201+
202+
const initializeModelOptions = () => {
203+
replaceModelOptions({
204+
modelIds: githubChatModelOptions,
205+
selectedModel: defaultGitHubChatModel,
206+
})
207+
setModelSelectDisabled(false)
208+
}
209+
143210
const setOpen = nextOpen => {
144211
open = nextOpen === true
145212

@@ -238,16 +305,6 @@ export const createGitHubChatDrawer = ({
238305
label.className = 'ai-chat-message__label'
239306
label.textContent = message.role === 'assistant' ? 'Assistant' : 'You'
240307

241-
if (message.role === 'assistant') {
242-
const modelLabelText = toModelLabelText(message.model)
243-
if (modelLabelText) {
244-
const modelLabel = document.createElement('span')
245-
modelLabel.className = 'ai-chat-message__label-model'
246-
modelLabel.textContent = modelLabelText
247-
label.append(modelLabel)
248-
}
249-
}
250-
251308
item.append(label)
252309

253310
const body = document.createElement('p')
@@ -367,6 +424,10 @@ export const createGitHubChatDrawer = ({
367424
if (promptInput instanceof HTMLTextAreaElement) {
368425
promptInput.disabled = isPending
369426
}
427+
428+
if (modelSelect instanceof HTMLSelectElement) {
429+
modelSelect.disabled = isPending
430+
}
370431
}
371432

372433
const runChatRequest = async () => {
@@ -389,13 +450,15 @@ export const createGitHubChatDrawer = ({
389450
return
390451
}
391452

453+
const selectedModel = getSelectedModel()
454+
392455
stopPendingRequest()
393456
const requestAbortController = new AbortController()
394457
const requestSignal = requestAbortController.signal
395458
pendingAbortController = requestAbortController
396459

397460
appendMessage({ role: 'user', content: prompt })
398-
appendMessage({ role: 'assistant', content: '', model: defaultGitHubChatModel })
461+
appendMessage({ role: 'assistant', content: '', model: selectedModel })
399462
if (promptInput instanceof HTMLTextAreaElement) {
400463
promptInput.value = ''
401464
}
@@ -418,6 +481,7 @@ export const createGitHubChatDrawer = ({
418481
const streamResult = await streamGitHubChatCompletion({
419482
token,
420483
messages: outboundMessages,
484+
model: selectedModel,
421485
signal: requestSignal,
422486
onToken: tokenChunk => {
423487
streamedContent += tokenChunk
@@ -447,6 +511,24 @@ export const createGitHubChatDrawer = ({
447511
return
448512
}
449513

514+
if (isModelAccessError(streamError)) {
515+
const modelAccessMessage = formatModelAccessErrorMessage(selectedModel)
516+
517+
updateLastAssistantMessage(modelAccessMessage)
518+
const lastMessage = messages[messages.length - 1]
519+
if (lastMessage) {
520+
lastMessage.level = 'error'
521+
}
522+
renderMessages()
523+
setChatStatus(modelAccessMessage, 'error')
524+
525+
if (pendingAbortController === requestAbortController) {
526+
pendingAbortController = null
527+
setPendingState(false)
528+
}
529+
return
530+
}
531+
450532
setChatStatus(
451533
'Streaming unavailable. Retrying with fallback response...',
452534
'pending',
@@ -465,6 +547,7 @@ export const createGitHubChatDrawer = ({
465547
const fallbackResult = await requestGitHubChatCompletion({
466548
token,
467549
messages: outboundMessages,
550+
model: selectedModel,
468551
signal: requestSignal,
469552
})
470553

@@ -487,8 +570,11 @@ export const createGitHubChatDrawer = ({
487570
return
488571
}
489572

490-
const fallbackMessage =
491-
fallbackError instanceof Error ? fallbackError.message : 'Chat request failed.'
573+
const fallbackMessage = isModelAccessError(fallbackError)
574+
? formatModelAccessErrorMessage(selectedModel)
575+
: fallbackError instanceof Error
576+
? fallbackError.message
577+
: 'Chat request failed.'
492578

493579
setRateMetadata(fallbackError?.rateLimit)
494580

@@ -509,6 +595,7 @@ export const createGitHubChatDrawer = ({
509595

510596
toggleButton?.setAttribute('aria-expanded', 'false')
511597
drawer?.setAttribute('hidden', '')
598+
initializeModelOptions()
512599
syncRepositoryLabel()
513600
renderMessages()
514601
setChatStatus('Idle', 'neutral')
@@ -560,6 +647,7 @@ export const createGitHubChatDrawer = ({
560647
setSelectedRepository: () => {
561648
syncRepositoryLabel()
562649
},
650+
setToken: () => {},
563651
dispose: () => {
564652
stopPendingRequest()
565653
setPendingState(false)

0 commit comments

Comments
 (0)