feat: support multiple camera previews and concurrent agent code navigation

This commit is contained in:
2026-10-20 10:10:48 +09:00
parent c17763f59d
commit af54e875df
13 changed files with 525 additions and 160 deletions
+45
View File
@@ -98,6 +98,51 @@ test('legacy cancelled references can be opened with the same read-only navigati
assert.equal(h.message().codeReferenceStates.location.status, 'opened')
})
test('code navigation does not block or abort a pending model response', async () => {
const h = consentHarness()
const controller = new AbortController()
h.globals.abortControllerRef.current = controller
let opened = 0
await h.openCodeReference('answer', 'location', async (_reference, isCurrent) => {
assert.equal(isCurrent(), true)
opened++
})
assert.equal(opened, 1)
assert.equal(controller.signal.aborted, false)
assert.equal(h.globals.abortControllerRef.current, controller)
assert.equal(h.message().codeReferenceStates.location.status, 'opened')
})
test('an arriving model response does not cancel an in-flight code navigation', async () => {
const h = consentHarness()
h.globals.abortControllerRef.current = new AbortController()
let finish, current
const pending = h.openCodeReference('answer', 'location', (_reference, isCurrent) => {
current = isCurrent
return new Promise(resolve => { finish = resolve })
})
h.globals.messageStateRef.current.messages.push({ id: 'new-answer', role: 'assistant' })
h.globals.abortControllerRef.current = null
assert.equal(current(), true)
finish()
await pending
assert.equal(h.globals.messageStateRef.current.messages[0].codeReferenceStates.location.status, 'opened')
})
test('starting file application invalidates a navigation that is still reading', async () => {
const h = consentHarness()
let finish, current
const pending = h.openCodeReference('answer', 'location', (_reference, isCurrent) => {
current = isCurrent
return new Promise((_resolve, reject) => { finish = () => reject(Error('applying')) })
})
h.globals.applyingRef.current = true
assert.equal(current(), false)
finish()
await pending
assert.equal(h.message().codeReferenceStates.location.status, 'error')
})
test('duplicate clicks, invalid IDs and busy tasks do not open more files', async () => {
const h = consentHarness()
let finish, opened = 0
+23
View File
@@ -22,6 +22,7 @@ const panelSource = ts.createSourceFile('WorkspaceQaPanel.tsx', fs.readFileSync(
const declarations = panelSource.statements.filter(node => ts.isFunctionDeclaration(node) && ['AiMessages', 'AgentMessages', 'TeacherMessages'].includes(node.name?.text))
const ProgressiveChatResponse = props => React.createElement('div', { 'data-reveal-start': props.message.revealStartedAt, 'data-collapsed': props.defaultCollapsed }, props.message.content)
const ChatImageAttachments = props => React.createElement('div', { 'data-chat-image-attachments': props.variant }, props.images.map(image => React.createElement('img', { key: image.url, src: image.url, alt: image.name })))
const AgentCodeReferences = () => null
const { AgentWorkDuration } = compile(fs.readFileSync(require.resolve('../src/renderer/src/components/AgentWorkDuration.tsx'), 'utf8'), {
'../i18n': { t: (key, values = {}) => key.replace(/\{\{(\w+)\}\}/g, (_, name) => values[name] ?? ''), useLocaleRender() {} }
})
@@ -36,6 +37,7 @@ const messages = compile(declarations.map(node => `export ${node.getText(panelSo
ChatEmptyState: widgets.ChatEmptyState,
ProgressiveChatResponse,
ChatImageAttachments,
AgentCodeReferences,
AgentProgressBubble,
AgentWorkDuration,
MessageCopyButton: () => null,
@@ -43,6 +45,27 @@ const messages = compile(declarations.map(node => `export ${node.getText(panelSo
Bot: () => React.createElement('svg'), WandSparkles: () => React.createElement('svg')
})
test('existing code locations stay usable during model and command work but not file application', () => {
for (const applying of [false, true]) {
const onOpen = () => {}
const tree = messages.AgentMessages({
dark: true, enabled: true, statusLoading: false, pending: true, busy: true,
messages: [{ id: 'answer', role: 'assistant', content: '코드 위치', applyState: applying ? 'applying' : undefined, plan: { codeReferences: [{ id: 'code-1' }], changes: [], commands: [], actionPlan: [], createModule: null } }],
onOpenCodeReference: onOpen
})
let references
function visit(element) {
if (!React.isValidElement(element)) return
if (element.type === AgentCodeReferences) references = element
React.Children.toArray(element.props.children).forEach(visit)
}
visit(tree)
assert.ok(references)
assert.equal(references.props.busy, applying)
assert.equal(typeof references.props.onOpen, 'function')
}
})
test('agent image thumbnails are siblings below the user bubble rather than bubble contents', () => {
const tree = messages.AgentMessages({
dark: true, enabled: true, statusLoading: false, pending: false,