mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-28 11:32:22 +00:00
Sticky human bubbles park at --sticky-human-top (~4px), sliding under the
titlebar's -webkit-app-region:drag strips. Electron resolves drag regions at
the compositor level — z-index and pointer-events don't apply — so clicking a
stuck bubble dragged the window instead of opening the edit composer. Add
no-drag to the shared bubble base class (read-only bubble + edit composer).
Covers the runtime side with a test: clicking a user bubble opens the inline
edit composer through both the incremental external-store runtime and the
stock one.
(cherry picked from commit db4e1f4f3e)
141 lines
4.3 KiB
TypeScript
141 lines
4.3 KiB
TypeScript
import { ExportedMessageRepository } from '@assistant-ui/core/internal'
|
|
// Clicking a user bubble must open the inline edit composer — through the
|
|
// app's incremental external-store runtime (which reimplements capability
|
|
// resolution, incl. `edit: onEdit !== undefined`) and the stock runtime.
|
|
//
|
|
// Note: this covers the React/runtime wiring only. The Electron-level failure
|
|
// mode (titlebar -webkit-app-region:drag swallowing clicks on *stuck* sticky
|
|
// bubbles) is not reproducible in jsdom — see USER_BUBBLE_BASE_CLASS's no-drag
|
|
// carve-out in thread.tsx.
|
|
import { AssistantRuntimeProvider, type ThreadMessage, useExternalStoreRuntime } from '@assistant-ui/react'
|
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
|
|
import { useIncrementalExternalStoreRuntime } from '@/lib/incremental-external-store-runtime'
|
|
|
|
import { Thread } from './thread'
|
|
|
|
const createdAt = new Date('2026-05-01T00:00:00.000Z')
|
|
|
|
class TestResizeObserver {
|
|
observe() {}
|
|
unobserve() {}
|
|
disconnect() {}
|
|
}
|
|
|
|
vi.stubGlobal('ResizeObserver', TestResizeObserver)
|
|
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) =>
|
|
window.setTimeout(() => callback(performance.now()), 0)
|
|
)
|
|
vi.stubGlobal('cancelAnimationFrame', (id: number) => window.clearTimeout(id))
|
|
|
|
Element.prototype.scrollTo = function scrollTo() {}
|
|
|
|
function stubOffsetDimension(
|
|
prop: 'offsetHeight' | 'offsetWidth',
|
|
clientProp: 'clientHeight' | 'clientWidth',
|
|
fallback: number
|
|
) {
|
|
const previous = Object.getOwnPropertyDescriptor(HTMLElement.prototype, prop)
|
|
|
|
Object.defineProperty(HTMLElement.prototype, prop, {
|
|
configurable: true,
|
|
get() {
|
|
return previous?.get?.call(this) || (this as HTMLElement)[clientProp] || fallback
|
|
}
|
|
})
|
|
}
|
|
|
|
stubOffsetDimension('offsetWidth', 'clientWidth', 800)
|
|
stubOffsetDimension('offsetHeight', 'clientHeight', 600)
|
|
|
|
function userMessage(): ThreadMessage {
|
|
return {
|
|
id: 'user-1',
|
|
role: 'user',
|
|
content: [{ type: 'text', text: 'edit me please' }],
|
|
attachments: [],
|
|
createdAt,
|
|
metadata: { custom: {} }
|
|
} as ThreadMessage
|
|
}
|
|
|
|
function assistantMessage(): ThreadMessage {
|
|
return {
|
|
id: 'assistant-1',
|
|
role: 'assistant',
|
|
content: [{ type: 'text', text: 'done' }],
|
|
status: { type: 'complete', reason: 'stop' },
|
|
createdAt,
|
|
metadata: {
|
|
unstable_state: null,
|
|
unstable_annotations: [],
|
|
unstable_data: [],
|
|
steps: [],
|
|
custom: {}
|
|
}
|
|
} as ThreadMessage
|
|
}
|
|
|
|
// Mirrors chat/index.tsx: incremental runtime + messageRepository + onEdit.
|
|
function IncrementalHarness({ onEdit }: { onEdit: () => Promise<void> }) {
|
|
const repository = ExportedMessageRepository.fromArray([userMessage(), assistantMessage()])
|
|
|
|
const runtime = useIncrementalExternalStoreRuntime<ThreadMessage>({
|
|
messageRepository: repository,
|
|
isRunning: false,
|
|
setMessages: () => {},
|
|
onNew: async () => {},
|
|
onEdit,
|
|
onCancel: async () => {},
|
|
onReload: async () => {}
|
|
})
|
|
|
|
return (
|
|
<AssistantRuntimeProvider runtime={runtime}>
|
|
<Thread />
|
|
</AssistantRuntimeProvider>
|
|
)
|
|
}
|
|
|
|
// Control: stock external store runtime.
|
|
function StockHarness({ onEdit }: { onEdit: () => Promise<void> }) {
|
|
const runtime = useExternalStoreRuntime<ThreadMessage>({
|
|
messages: [userMessage(), assistantMessage()],
|
|
isRunning: false,
|
|
onNew: async () => {},
|
|
onEdit
|
|
})
|
|
|
|
return (
|
|
<AssistantRuntimeProvider runtime={runtime}>
|
|
<Thread />
|
|
</AssistantRuntimeProvider>
|
|
)
|
|
}
|
|
|
|
describe('click-to-edit user message', () => {
|
|
it('opens the edit composer with the incremental runtime', async () => {
|
|
const { container } = render(<IncrementalHarness onEdit={async () => {}} />)
|
|
|
|
const bubble = await screen.findByRole('button', { name: 'Edit message' })
|
|
|
|
fireEvent.click(bubble)
|
|
|
|
await waitFor(() => {
|
|
expect(container.querySelector('[data-slot="aui_edit-composer-root"]')).toBeTruthy()
|
|
})
|
|
})
|
|
|
|
it('opens the edit composer with the stock runtime', async () => {
|
|
const { container } = render(<StockHarness onEdit={async () => {}} />)
|
|
|
|
const bubble = await screen.findByRole('button', { name: 'Edit message' })
|
|
|
|
fireEvent.click(bubble)
|
|
|
|
await waitFor(() => {
|
|
expect(container.querySelector('[data-slot="aui_edit-composer-root"]')).toBeTruthy()
|
|
})
|
|
})
|
|
})
|