refactor(web): open produced files through the Host, not over HTTP
Scope decision: previews for a browser that is not on the Host machine are not supported. With that settled, host.openPath answers the supported case completely — a file:// document in a real browser has full page capabilities and no reach into /api — and the HTTP serving this branch had built answered only the unsupported one. Removed: the /f route and its listener, the workspace-file URL shape, ApiProxy.workspaceRootOf, ConnectionHandle.fileUrl, and the port published into the index page. Kept, and finished: - the produced-files row a turn ends with, derived from mutation locations; - the path link now reads as a link at rest, not only on hover — the reported "I can't open what it made" was this, sitting on a working capability; - the Host opener prefers the default BROWSER for .html/.htm/.xhtml/.svg, so a developer who binds .html to an editor still gets a rendered page (macOS via the LaunchServices https handler, Linux via $BROWSER, every failure falling back to the default application). The retired designs and their measurements stay in the Agent Note, including why same-origin serving was unsafe and why the sandbox that fixed it broke the pages invisibly.
This commit is contained in:
@@ -218,25 +218,13 @@ describe('conversation slot inject surface', () => {
|
||||
await b.runtime.dispose()
|
||||
})
|
||||
|
||||
it('openFile (chat view face) opens a workspace file in a tab and falls back to the host opener outside it', async () => {
|
||||
it('openFile (chat view face) resolves against session cwd and calls workspaces.openPath', async () => {
|
||||
const b = await bench()
|
||||
// A host that publishes a workspace-file port: previews come from that
|
||||
// origin, which is what keeps them off the API's.
|
||||
b.runtime.connection.filesPort = 4321
|
||||
const open = vi.spyOn(window, 'open').mockReturnValue(null)
|
||||
const { injected } = b.chatViewSurface(ROOT)
|
||||
// Inside the session cwd: served on the workspace-file origin, so a browser
|
||||
// anywhere on the network sees the file the agent produced.
|
||||
injected.openFile('src/a.ts')
|
||||
expect(open).toHaveBeenCalledWith(`http://localhost:4321/f/${ROOT}/src/a.ts`, '_blank', 'noopener,noreferrer')
|
||||
expect(b.runtime.workspaces.calls.some(c => c.method === 'openPath')).toBe(false)
|
||||
// Outside it there is no served URL, so the Host's own opener answers —
|
||||
// resolved against the session cwd exactly as before.
|
||||
injected.openFile('/etc/hosts')
|
||||
await vi.waitFor(() => {
|
||||
expect(b.runtime.workspaces.calls).toContainEqual({ method: 'openPath', args: ['/etc/hosts'] })
|
||||
expect(b.runtime.workspaces.calls).toContainEqual({ method: 'openPath', args: ['/proj/src/a.ts'] })
|
||||
})
|
||||
open.mockRestore()
|
||||
await b.runtime.dispose()
|
||||
})
|
||||
|
||||
|
||||
@@ -136,9 +136,6 @@ async function bench(snapshot: ConversationSnapshot) {
|
||||
openPath: vi.fn(async () => {}),
|
||||
}
|
||||
ctx.provide('workspaces', workspaces)
|
||||
// The transport face the chat view reads its workspace-file URLs from.
|
||||
const connection = { fileUrl: vi.fn((_s: unknown, _cwd: string | undefined, path: string) => `http://localhost:4321/f/s-1/${path}`) }
|
||||
ctx.provide('connection', connection)
|
||||
ctx.provide('layout', layout)
|
||||
const locale = new LocaleService(ctx)
|
||||
ctx.provide('locale', locale)
|
||||
@@ -246,14 +243,12 @@ describe('run_code sub-calls through the real chat machinery', () => {
|
||||
subCall(12, parent, 2, 'bash', { command: 'ls notes', description: 'List notes' }, 'demo.txt'),
|
||||
]]])
|
||||
const b = await bench(snapshotWith([codeResult(10, parent)], dispatches))
|
||||
const open = vi.spyOn(window, 'open').mockReturnValue(null)
|
||||
const view = mountApp(b.slots)
|
||||
view.getByText('notes/demo.txt').click()
|
||||
expect(b.layout.openDetails).not.toHaveBeenCalled()
|
||||
await vi.waitFor(() => {
|
||||
expect(open).toHaveBeenCalledWith('http://localhost:4321/f/s-1/notes/demo.txt', '_blank', 'noopener,noreferrer')
|
||||
expect(b.workspaces.openPath).toHaveBeenCalledWith('notes/demo.txt')
|
||||
})
|
||||
open.mockRestore()
|
||||
view.getByText('List notes').click()
|
||||
expect(b.layout.openDetails).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -119,17 +119,14 @@ describe('keyed toolview hole through the real machinery', () => {
|
||||
await b.runtime.dispose()
|
||||
})
|
||||
|
||||
it('file-path clicks travel owner openFile → chat inject → the served workspace URL', async () => {
|
||||
it('file-path clicks travel owner openFile → chat inject → workspaces.openPath', async () => {
|
||||
const b = await bench([toolResult(3, 'c1', 'read', '{"path":"src/a.ts"}')])
|
||||
b.runtime.connection.filesPort = 4321
|
||||
const open = vi.spyOn(window, 'open').mockReturnValue(null)
|
||||
const view = b.runtime.renderRoot()
|
||||
view.getByText('src/a.ts').click()
|
||||
expect(b.layout.openDetails).not.toHaveBeenCalled()
|
||||
await vi.waitFor(() => {
|
||||
expect(open).toHaveBeenCalledWith(expect.stringContaining('/src/a.ts'), '_blank', 'noopener,noreferrer')
|
||||
expect(b.runtime.workspaces.calls).toContainEqual({ method: 'openPath', args: ['src/a.ts'] })
|
||||
})
|
||||
open.mockRestore()
|
||||
await b.runtime.dispose()
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user