Skip to content

Commit b544050

Browse files
feat: tab-scoped ai chat baseline.
1 parent d3dfd9b commit b544050

11 files changed

Lines changed: 753 additions & 320 deletions

File tree

playwright/github-byot-ai.spec.ts

Lines changed: 142 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
ensureAiChatDrawerOpen,
88
ensureOpenPrDrawerOpen,
99
mockRepositoryBranches,
10+
openWorkspaceTab,
1011
setComponentEditorSource,
1112
setStylesEditorSource,
1213
waitForAppReady,
@@ -241,6 +242,15 @@ test('AI chat prefers streaming responses when available', async ({ page }) => {
241242
expect(streamRequestBody?.messages?.[0]?.content).toContain(
242243
'expert software development assistant focused on CSS dialects and JSX syntax',
243244
)
245+
expect(streamRequestBody?.messages?.[0]?.content).toContain(
246+
'JSX is compiled for @knighted/jsx DOM runtime',
247+
)
248+
expect(streamRequestBody?.messages?.[0]?.content).toContain(
249+
'Do not suggest React imports, hooks, or React-only runtime APIs',
250+
)
251+
expect(streamRequestBody?.messages?.[0]?.content).toContain(
252+
'Preserve the selected style dialect and avoid cross-dialect rewrites',
253+
)
244254
const systemMessages = streamRequestBody?.messages?.filter(
245255
(message: ChatRequestMessage) => message.role === 'system',
246256
)
@@ -258,6 +268,23 @@ test('AI chat prefers streaming responses when available', async ({ page }) => {
258268
message.content?.includes('Editor context:'),
259269
),
260270
).toBe(true)
271+
expect(
272+
systemMessages?.some(
273+
(message: ChatRequestMessage) =>
274+
message.content?.includes('- Active tab:') &&
275+
message.content?.includes('App.tsx'),
276+
),
277+
).toBe(true)
278+
expect(
279+
systemMessages?.some((message: ChatRequestMessage) =>
280+
message.content?.includes('Available tab targets (id and path):'),
281+
),
282+
).toBe(true)
283+
expect(
284+
systemMessages?.some((message: ChatRequestMessage) =>
285+
message.content?.includes('Active tab source:'),
286+
),
287+
).toBe(true)
261288
})
262289

263290
test('AI chat can disable editor context payload via checkbox', async ({ page }) => {
@@ -282,7 +309,7 @@ test('AI chat can disable editor context payload via checkbox', async ({ page })
282309
await connectByotWithSingleRepo(page)
283310
await ensureAiChatDrawerOpen(page)
284311

285-
const includeEditorsToggle = page.getByLabel('Send JSX + CSS editor context')
312+
const includeEditorsToggle = page.getByLabel('Send tab content')
286313
await expect(includeEditorsToggle).toBeChecked()
287314
await includeEditorsToggle.uncheck()
288315

@@ -316,7 +343,7 @@ test('AI chat can disable editor context payload via checkbox', async ({ page })
316343
).toBe(false)
317344
})
318345

319-
test('AI chat proposals can be confirmed, applied, and undone for component and styles editors', async ({
346+
test('AI chat proposals can be confirmed, applied, and undone per active tab', async ({
320347
page,
321348
}) => {
322349
await page.route('https://models.github.ai/inference/chat/completions', async route => {
@@ -347,7 +374,7 @@ test('AI chat proposals can be confirmed, applied, and undone for component and
347374
function: {
348375
name: 'propose_editor_update',
349376
arguments: JSON.stringify({
350-
target: 'component',
377+
target: 'src/components/App.tsx',
351378
content: 'const App = () => <button type="button">Updated</button>',
352379
rationale: 'Use explicit App component output.',
353380
}),
@@ -359,7 +386,7 @@ test('AI chat proposals can be confirmed, applied, and undone for component and
359386
function: {
360387
name: 'propose_editor_update',
361388
arguments: JSON.stringify({
362-
target: 'styles',
389+
target: 'src/styles/app.css',
363390
content: '.button { color: rgb(10 20 30); }',
364391
rationale: 'Provide deterministic button styling.',
365392
}),
@@ -377,6 +404,7 @@ test('AI chat proposals can be confirmed, applied, and undone for component and
377404
await connectByotWithSingleRepo(page)
378405
await setComponentEditorSource(page, 'const App = () => <button>Before</button>')
379406
await setStylesEditorSource(page, '.button { color: red; }')
407+
await openWorkspaceTab(page, 'App.tsx')
380408
await ensureAiChatDrawerOpen(page)
381409

382410
await page.getByLabel('Ask AI assistant').fill('Suggest updates for both editors.')
@@ -386,55 +414,65 @@ test('AI chat proposals can be confirmed, applied, and undone for component and
386414
page.getByText('Prepared updates for both editors.', { exact: true }),
387415
).toBeVisible()
388416

389-
const assistantResponseMessage = page
390-
.locator('.ai-chat-message--assistant')
391-
.filter({ hasText: 'Prepared updates for both editors.' })
392-
.first()
417+
await expect(
418+
page.getByRole('button', { name: 'Apply update to App.tsx' }),
419+
).toBeVisible()
420+
await expect(
421+
page.getByRole('button', { name: 'Apply update to app.css' }),
422+
).toBeVisible()
423+
424+
await page.getByRole('button', { name: 'Apply update to App.tsx' }).click()
393425

426+
await expect(page.getByRole('button', { name: 'Apply update to App.tsx' })).toBeHidden()
394427
await expect(
395-
page.getByRole('button', { name: 'Apply update to both editors' }),
428+
page.getByRole('button', { name: 'Undo last apply for App.tsx' }),
396429
).toBeVisible()
397-
await page.getByRole('button', { name: 'Apply update to both editors' }).click()
398430
await expect(
399-
page.getByRole('button', { name: 'Apply update to both editors' }),
431+
page.getByRole('button', { name: 'Undo last apply for app.css' }),
400432
).toBeHidden()
401433
await expect(
402-
page.getByRole('button', { name: 'Apply update to Component editor' }),
403-
).toBeHidden()
434+
page.locator('.editor-panel[data-editor-kind="component"] .cm-content').first(),
435+
).toContainText('Updated')
436+
437+
await openWorkspaceTab(page, 'app.css')
404438
await expect(
405-
page.getByRole('button', { name: 'Apply update to Styles editor' }),
439+
page.getByRole('button', { name: 'Undo last apply for App.tsx' }),
406440
).toBeHidden()
407441
await expect(
408-
assistantResponseMessage.getByRole('button', { name: 'Undo last Component apply' }),
409-
).toHaveCount(0)
442+
page.getByRole('button', { name: 'Apply update to app.css' }),
443+
).toBeVisible()
444+
await page.getByRole('button', { name: 'Apply update to app.css' }).click()
445+
410446
await expect(
411-
assistantResponseMessage.getByRole('button', { name: 'Undo last Styles apply' }),
412-
).toHaveCount(0)
447+
page.locator('.editor-panel[data-editor-kind="styles"] .cm-content').first(),
448+
).toContainText('rgb(10 20 30)')
413449
await expect(
414-
page.getByRole('button', { name: 'Undo last Component apply' }),
450+
page.getByRole('button', { name: 'Undo last apply for app.css' }),
415451
).toBeVisible()
416-
await expect(page.getByRole('button', { name: 'Undo last Styles apply' })).toBeVisible()
417452
await expect(
418-
page.locator('.editor-panel[data-editor-kind="component"] .cm-content').first(),
419-
).toContainText('Updated')
453+
page.getByRole('button', { name: 'Undo last apply for App.tsx' }),
454+
).toBeHidden()
455+
456+
await page.getByRole('button', { name: 'Undo last apply for app.css' }).click()
420457
await expect(
421458
page.locator('.editor-panel[data-editor-kind="styles"] .cm-content').first(),
422-
).toContainText('rgb(10 20 30)')
459+
).toContainText('red')
423460

424-
await page.getByRole('button', { name: 'Undo last Component apply' }).click()
461+
await openWorkspaceTab(page, 'App.tsx')
425462
await expect(
426-
page.locator('.editor-panel[data-editor-kind="component"] .cm-content').first(),
427-
).toContainText('Before')
463+
page.getByRole('button', { name: 'Undo last apply for App.tsx' }),
464+
).toBeVisible()
465+
await expect(
466+
page.getByRole('button', { name: 'Undo last apply for app.css' }),
467+
).toBeHidden()
428468

429-
await page.getByRole('button', { name: 'Undo last Styles apply' }).click()
469+
await page.getByRole('button', { name: 'Undo last apply for App.tsx' }).click()
430470
await expect(
431-
page.locator('.editor-panel[data-editor-kind="styles"] .cm-content').first(),
432-
).toContainText('red')
471+
page.locator('.editor-panel[data-editor-kind="component"] .cm-content').first(),
472+
).toContainText('Before')
433473
})
434474

435-
test('AI chat shows a single apply action when both editor proposals are available', async ({
436-
page,
437-
}) => {
475+
test('AI chat apply actions resolve dynamic tab targets', async ({ page }) => {
438476
await page.route('https://models.github.ai/inference/chat/completions', async route => {
439477
const body = route.request().postDataJSON() as ChatRequestBody | null
440478

@@ -463,7 +501,7 @@ test('AI chat shows a single apply action when both editor proposals are availab
463501
function: {
464502
name: 'propose_editor_update',
465503
arguments: JSON.stringify({
466-
target: 'component',
504+
target: 'src/components/App.tsx',
467505
content: 'const App = () => <button type="button">Updated</button>',
468506
}),
469507
},
@@ -474,7 +512,7 @@ test('AI chat shows a single apply action when both editor proposals are availab
474512
function: {
475513
name: 'propose_editor_update',
476514
arguments: JSON.stringify({
477-
target: 'styles',
515+
target: 'src/styles/app.css',
478516
content: '.button { color: rgb(10 20 30); }',
479517
}),
480518
},
@@ -491,6 +529,7 @@ test('AI chat shows a single apply action when both editor proposals are availab
491529
await connectByotWithSingleRepo(page)
492530
await setComponentEditorSource(page, 'const App = () => <button>Before</button>')
493531
await setStylesEditorSource(page, '.button { color: red; }')
532+
await openWorkspaceTab(page, 'App.tsx')
494533
await ensureAiChatDrawerOpen(page)
495534

496535
await page.getByLabel('Ask AI assistant').fill('Suggest updates for both editors.')
@@ -501,14 +540,73 @@ test('AI chat shows a single apply action when both editor proposals are availab
501540
).toBeVisible()
502541

503542
await expect(
504-
page.getByRole('button', { name: 'Apply update to both editors' }),
543+
page.getByRole('button', { name: 'Apply update to App.tsx' }),
505544
).toBeVisible()
506545
await expect(
507-
page.getByRole('button', { name: 'Apply update to Component editor' }),
508-
).toBeHidden()
546+
page.getByRole('button', { name: 'Apply update to app.css' }),
547+
).toBeVisible()
548+
549+
await openWorkspaceTab(page, 'app.css')
550+
509551
await expect(
510-
page.getByRole('button', { name: 'Apply update to Styles editor' }),
511-
).toBeHidden()
552+
page.getByRole('button', { name: 'Apply update to App.tsx' }),
553+
).toBeVisible()
554+
await expect(
555+
page.getByRole('button', { name: 'Apply update to app.css' }),
556+
).toBeVisible()
557+
})
558+
559+
test('AI chat sends the currently active tab when context is enabled', async ({
560+
page,
561+
}) => {
562+
let streamRequestBody: ChatRequestBody | undefined
563+
564+
await page.route('https://models.github.ai/inference/chat/completions', async route => {
565+
streamRequestBody = route.request().postDataJSON() as ChatRequestBody
566+
567+
await route.fulfill({
568+
status: 200,
569+
contentType: 'text/event-stream',
570+
body: [
571+
'data: {"choices":[{"delta":{"content":"ok"}}]}',
572+
'',
573+
'data: [DONE]',
574+
'',
575+
].join('\n'),
576+
})
577+
})
578+
579+
await waitForAppReady(page, `${appEntryPath}`)
580+
await connectByotWithSingleRepo(page)
581+
await setStylesEditorSource(page, '.button { color: red; }')
582+
await ensureAiChatDrawerOpen(page)
583+
584+
await page.getByLabel('Ask AI assistant').fill('Use active tab context only.')
585+
await page.getByRole('button', { name: 'Send' }).click()
586+
await expect(
587+
page.getByText('Response streamed from GitHub.', { exact: true }),
588+
).toHaveText('Response streamed from GitHub.')
589+
590+
const systemMessages = streamRequestBody?.messages?.filter(
591+
(message: ChatRequestMessage) => message.role === 'system',
592+
)
593+
expect(
594+
systemMessages?.some(
595+
(message: ChatRequestMessage) =>
596+
message.content?.includes('- Active tab:') &&
597+
message.content?.includes('app.css'),
598+
),
599+
).toBe(true)
600+
expect(
601+
systemMessages?.some((message: ChatRequestMessage) =>
602+
message.content?.includes('Active tab source:'),
603+
),
604+
).toBe(true)
605+
expect(
606+
systemMessages?.some((message: ChatRequestMessage) =>
607+
message.content?.includes('Available tab targets (id and path):'),
608+
),
609+
).toBe(true)
512610
})
513611

514612
test('AI chat streaming text still updates while latest undo actions are visible', async ({
@@ -546,7 +644,7 @@ test('AI chat streaming text still updates while latest undo actions are visible
546644
function: {
547645
name: 'propose_editor_update',
548646
arguments: JSON.stringify({
549-
target: 'styles',
647+
target: 'src/styles/app.css',
550648
content: '.button { color: rgb(10 20 30); }',
551649
}),
552650
},
@@ -596,8 +694,10 @@ test('AI chat streaming text still updates while latest undo actions are visible
596694
await expect(
597695
page.getByText('Prepared updates for styles editor.', { exact: true }),
598696
).toBeVisible()
599-
await page.getByRole('button', { name: 'Apply update to Styles editor' }).click()
600-
await expect(page.getByRole('button', { name: 'Undo last Styles apply' })).toBeVisible()
697+
await page.getByRole('button', { name: 'Apply update to app.css' }).click()
698+
await expect(
699+
page.getByRole('button', { name: 'Undo last apply for app.css' }),
700+
).toBeVisible()
601701

602702
await page
603703
.getByLabel('Ask AI assistant')

0 commit comments

Comments
 (0)