Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions ui-web/src/features/desk/DeskSurface.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,30 @@ describe('the pane of a delivered file', () => {
expect(deliveries.byPath('/w/gone.md')?.missing).toBe(true)
expect(deliveries.byPath('/w/denied.md')?.missing).toBe(false)
})

/* A document that drew a picture from one URL draws it again from memory for
the same URL without asking -- the HTML list of available images, which no
Cache-Control reaches -- so a picture the agent rewrote under the same path
came back as the one it replaced, whichever door re-opened it. Only a
browser shows the pixels; pinned here is the mechanism: each open asks
under a URL of its own, and a repaint of the same open does not. */
it.each([
['/w/out/shot.png', '/file?path=%2Fw%2Fout%2Fshot.png'],
['/w/out/logo.svg', '/file?path=%2Fw%2Fout%2Flogo.svg'],
])('asks for %s under a URL of its own on every open', async (path, route) => {
const view = render(<DeskSurface />)
const src = (): string => document.querySelector('.desk-pane .fview .shot img')?.getAttribute('src') || ''
await act(async () => { desk.openDeskFile(path) })
const first = src()
view.rerender(<DeskSurface />)
expect(src()).toBe(first)

await act(async () => { desk.openDeskFile(path) })

expect(first).toContain(route)
expect(src()).toContain(route)
expect(src()).not.toBe(first)
})
})

/* happy-dom lays nothing out, so the geometry the gesture reads is stubbed at
Expand Down
37 changes: 37 additions & 0 deletions ui-web/src/features/transcript/TranscriptPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1960,6 +1960,43 @@ describe("the turn's delivered files", () => {
expect(src).toContain('render=thumb')
})

/* A tile is a picture, and a document that drew one URL draws it again from
memory without asking, whatever Cache-Control says. The registry hands a
path delivered again in one conversation the same token, and a thumb is
named by its path, so the second card's tile could come back as the first
card's picture while the file on disk held the new one. Each delivery's own
stamp keeps the two URLs apart. */
it.each([
['shot.png', '/files/download?token=shot.png'],
['report.pdf', '/file?path=%2Fw%2Freport.pdf'],
])('draws %s, delivered twice, under a URL per delivery', async (name, route) => {
vi.stubGlobal('fetch', () => Promise.resolve({ ok: true }))
const stamped = (at: number): Record<string, unknown> => ({
raven_delivery: { ...(manifest([name]).raven_delivery as Record<string, unknown>), delivered_at: at },
})
act(() => {
mount.history([
{ role: 'user', text: 'draw it', timestamp: iso(Date.now() - 9000) },
{ role: 'tool', name: 'deliver_files', text: 'ok', metadata: stamped(1000) },
{ role: 'assistant', text: 'done', timestamp: iso(Date.now() - 8000) },
{ role: 'user', text: 'make it blue', timestamp: iso(Date.now() - 5000) },
{ role: 'tool', name: 'deliver_files', text: 'ok', metadata: stamped(2000) },
{ role: 'assistant', text: 'done', timestamp: iso(Date.now()) },
])
})
await act(async () => { await Promise.resolve() })
await act(async () => { await Promise.resolve() })
const srcs = $$('.atile .pic.shot img').map((img) => img.getAttribute('src') || '')
expect(srcs).toHaveLength(2)
expect(srcs.every((src) => src.includes(route))).toBe(true)
expect(srcs[0]).not.toBe(srcs[1])
/* By the delivery's stamp rather than its turn: a delegated stream numbers
its turns from one as well, so one turn number can name two deliveries
of the same path. */
expect(srcs[0]).toContain('&v=1000')
expect(srcs[1]).toContain('&v=2000')
})

it('falls back to the type mark when the deck cannot be rendered', async () => {
vi.stubGlobal('fetch', (_url: string, init?: RequestInit) => {
if (init?.method === 'HEAD') return Promise.resolve({ ok: true })
Expand Down
7 changes: 5 additions & 2 deletions ui-web/src/features/transcript/TranscriptPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { ds } from '../../state/sources'
import * as dag from '../dag/graph'
import { getVersion as deliveriesVersion, humanSize, subscribe as deliveriesSubscribe } from '../workspace/deliveries'
import {
fileKind, fileURL, openDelivery as wsOpenDelivery, openPath as wsOpenPath, thumbURL,
fileKind, fileURL, openDelivery as wsOpenDelivery, openPath as wsOpenPath, thumbURL, versioned,
} from '../workspace/store'
import * as store from './store'
import * as tail from './tail'
Expand Down Expand Up @@ -1308,9 +1308,12 @@ const DeliveryShot = memo(function DeliveryShot({ row, broken, src }: {
row: DeliveryRow; broken: () => void; src?: string
}): ReactElement {
const [ready, setReady] = useState(false)
/* Versioned by the delivery: a path delivered again keeps its token and a
thumb is named by its path, so without the stamp the new card's picture
is the URL the old card already drew. */
return (
<span className={'pic shot' + (ready ? '' : ' skel')}>
<img src={src || row.downloadPath} alt="" loading="lazy" decoding="async"
<img src={versioned(src || row.downloadPath, row.when)} alt="" loading="lazy" decoding="async"
onLoad={() => setReady(true)} onError={broken} />
{ready ? null : <span className="sk" />}
</span>
Expand Down
9 changes: 6 additions & 3 deletions ui-web/src/features/workspace/WorkspacePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { show as toast } from '../../state/toast'
import * as deliveries from './deliveries'
import {
RENDERED, TWO_VIEWS, appFor, canOpenInApp, copyToClip, extOf, fileURL, pageURL,
hostPlatform, looksBinary, mdHtml, openInApp, runURL, setAppFor,
hostPlatform, looksBinary, mdHtml, openInApp, runURL, setAppFor, versioned,
} from './store'
import * as store from './store'
import './styles.css'
Expand Down Expand Up @@ -549,11 +549,14 @@ function FileBody({ f }: { f: WsFile }): JSX.Element {
if (f.err) {
body = <div className="verr">{f.err}</div>
} else if (asImage) {
/* Versioned by the open, not by the paint: every open is a fresh `seq`, so
a re-open draws the file as it stands now -- what the text kinds get from
their own fetch -- and a repaint of the same open asks for nothing. */
body = (
<div className="shot">
{broken
? <div className="verr">{broken}</div>
: <img src={fileURL(f.path)} alt={f.path}
: <img src={versioned(fileURL(f.path), f.seq)} alt={f.path}
/* The picture kinds never read text, so this is the only place they
learn why nothing came -- and the error itself does not say, which
is why the probe asks for a status: gone, over the ceiling, or
Expand Down Expand Up @@ -620,7 +623,7 @@ function DeckBody({ f }: { f: WsFile }): JSX.Element {
/* A deck the agent rebuilt and delivered again keeps its path; the delivery's
own stamp is what tells this view the bytes behind the path moved. */
useSyncExternalStore(deliveries.subscribe, deliveries.getVersion)
const version = (deliveries.byPath(f.path) as { when?: number | null } | null)?.when ?? null
const version = deliveries.byPath(f.path)?.when ?? null
const first = pageURL(f.path, 1, version)
useEffect(() => {
setPages(null)
Expand Down
10 changes: 9 additions & 1 deletion ui-web/src/features/workspace/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,14 @@ export const renderURL = (p: string): string => fileURL(p) + '&render=pdf'
/* The first page of a deck as a picture, for a tile that has room for one
picture and not for a viewer. */
export const thumbURL = (p: string): string => fileURL(p) + '&render=thumb'
/* A picture the document has drawn from one URL is drawn again from memory
for that URL without asking -- the HTML list of available images, which no
Cache-Control reaches. So a file rewritten under the same path needs a URL
the document has not drawn yet, or the old picture comes back. `version` is
whatever moves when the bytes may have, riding along as a query the gateway
ignores. */
export const versioned = (url: string, version?: number | null): string =>
url + (version ? '&v=' + version : '')
/* One page of the rendering as a picture. The viewer draws a PDF this way
rather than framing the document: Safari does not draw a framed PDF served
under the sandbox policy these files carry, and that policy is what keeps an
Expand All @@ -185,7 +193,7 @@ export const thumbURL = (p: string): string => fileURL(p) + '&render=thumb'
`version` rides along as a query the gateway ignores, so a deck delivered
again under the same path is fetched again rather than shown from cache. */
export const pageURL = (p: string, n: number, version?: number | null): string =>
fileURL(p) + '&render=page&p=' + n + (version ? '&v=' + version : '')
versioned(fileURL(p) + '&render=page&p=' + n, version)

/* The same route, asked to serve the file under a policy that lets its scripts
run. One view of one file: the route remembers nothing, so the next request
Expand Down
6 changes: 6 additions & 0 deletions ui-web/src/features/workspace/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ export interface DeliveryRow {
carry one, and that one belongs to the conversation; every row the registry
gives back has it, filled in on the way in. */
scope?: string
/* When the gateway says the delivery happened (`delivered_at`, epoch
milliseconds), or null for a manifest that predates the field and for a
row recovered from the registry. Declared here because a card's picture is
named by it: a path delivered again keeps its token, and the stamp is what
tells the two cards' pictures apart. */
when?: number | null
}

/* The stable workspace record owned by the island store. */
Expand Down
Loading