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:
ZiyaZhang
2026-08-01 03:15:54 -07:00
parent 59bfe77fb8
commit 8fb6c2bd69
50 changed files with 317 additions and 1211 deletions

View File

@@ -25,7 +25,6 @@
"vitest": "^4.1.8"
},
"peerDependencies": {
"@deepseek-ai/dsh-client-connection": "^0.0.1",
"@deepseek-ai/dsh-client-runtime": "^0.0.1",
"@deepseek-ai/dsh-client-ui-slots": "^0.0.1",
"@deepseek-ai/dsh-client-web-react": "^0.0.1",
@@ -36,7 +35,6 @@
"react-dom": "^18.2.0"
},
"devDependencies": {
"@deepseek-ai/dsh-client-connection": "workspace:^",
"@deepseek-ai/dsh-client-runtime": "workspace:^",
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
"@deepseek-ai/dsh-client-web-react": "workspace:^",

View File

@@ -1,48 +0,0 @@
/** Test-owned connection face: the transport members features read off `ctx.connection`. */
import { workspaceFileSegments, workspaceFileUrl } from '@deepseek-ai/dsh-host-apiproxy/api'
import type { ConnectionHandle, IApiClient, SessionId } from '@deepseek-ai/dsh-client-connection/client'
/**
* Connection test double. Implements the same `ConnectionHandle` face features
* receive as `ctx.connection`, so a production face change breaks this double
* at compile time. The wire client is not modelled — a feature that needs one
* composes its own connection over a fake api client; this double exists for
* the transport facts features read synchronously, above all the
* workspace-file URL.
*/
export class TestConnection implements ConnectionHandle {
/**
* The workspace-file port the host would have published into the page.
* Unset — the default, and the keyless fixture lane's real state — makes
* {@link TestConnection.fileUrl} answer `undefined`, which is the signal a
* caller falls back to the Host opener on.
*/
filesPort: number | undefined
/** The wire client; unused by this double's consumers and absent by construction. */
readonly api: IApiClient = undefined as unknown as IApiClient
/**
* Stream-loop starter (inert).
* @returns a stop handle that does nothing.
*/
start(): { stop(): void } {
return { stop: () => {} }
}
/**
* Workspace-file URL, deriving exactly as production does so a feature test
* sees the real inside/outside-workspace split.
* @param sessionId - the Session whose cwd anchors the path.
* @param cwd - that Session's working directory.
* @param path - the path a tool reported.
* @returns the absolute URL on the workspace-file origin, or undefined when
* the path leaves the workspace or no port is published.
*/
fileUrl(sessionId: SessionId, cwd: string | undefined, path: string): string | undefined {
if (this.filesPort === undefined) return undefined
const segments = workspaceFileSegments(cwd, path)
if (segments === undefined) return undefined
return `http://localhost:${String(this.filesPort)}${workspaceFileUrl(sessionId, segments)}`
}
}

View File

@@ -29,13 +29,11 @@ import type {
} from '@deepseek-ai/dsh-client-ui-slots'
import { registerDomSnapshotSerializer } from './snapshot.ts'
import { TestSessions } from './sessions.ts'
import { TestConnection } from './connection.ts'
import { TestWorkspaces } from './workspaces.ts'
import type { Stabilizer } from './fixtures.ts'
export { domSnapshotSerializer, registerDomSnapshotSerializer } from './snapshot.ts'
export { FixtureSession, TestSessions } from './sessions.ts'
export { TestConnection } from './connection.ts'
export { TestWorkspaces } from './workspaces.ts'
export { conversationSnapshot, workspaceListState } from './fixtures.ts'
export type { SessionBehaviorOverrides, SessionFixture, Stabilizer } from './fixtures.ts'
@@ -177,8 +175,6 @@ export class SlotTestRuntime {
readonly sessions: TestSessions
/** Workspaces double (list observable, recorded intent actions). */
readonly workspaces: TestWorkspaces
/** The transport double features read as `ctx.connection`. */
readonly connection: TestConnection
private readonly stabilizer: Stabilizer = async (fn) => {
await act(async () => { await fn() })
@@ -199,10 +195,8 @@ export class SlotTestRuntime {
this.root = new TestRoot(slots, this.stabilizer)
this.sessions = new TestSessions(this.stabilizer, ctx)
this.workspaces = new TestWorkspaces(this.stabilizer)
this.connection = new TestConnection()
ctx.provide('sessions', this.sessions)
ctx.provide('workspaces', this.workspaces)
ctx.provide('connection', this.connection)
// Capturing install: the production renderer does the rendering; the
// wrapper only takes the host face for storeOf (no machinery copied).
const renderer = createSlotRenderer()

View File

@@ -17,9 +17,6 @@
{
"path": "../web-react"
},
{
"path": "../connection"
},
{
"path": "../runtime"
},