Merge remote-tracking branch 'origin/master' into worktree/web-session-model-selector

# Conflicts:
#	apps/cli/package.json
#	apps/web/tests/smoke-real.e2e.ts
#	apps/web/tests/snapshots/fresh-round-trip/ui.expected.md
#	apps/web/tests/snapshots/seeded-history/ui.expected.md
#	docs/config-catalog.md
#	packages/client/connection/tests/fake-api.ts
#	packages/client/runtime/src/client/index.ts
#	packages/client/runtime/src/client/sessions/conversation.ts
#	packages/client/runtime/src/client/sessions/session.ts
#	packages/client/runtime/tests/fake-api.ts
#	packages/client/ui-conversation/src/client/apply.ts
#	packages/client/ui-conversation/src/client/contract/slots.ts
#	packages/client/ui-conversation/src/client/index.ts
#	packages/client/ui-conversation/src/client/skeleton/ConversationRoot.tsx
#	packages/client/ui-conversation/src/client/skeleton/InputBar.tsx
#	packages/client/ui-conversation/tests/chat-stats-bash-sample.spec.tsx
#	packages/client/ui-conversation/tests/chat-toolview-slot.spec.tsx
#	packages/client/ui-conversation/tests/chat-view.spec.tsx
#	packages/client/ui-conversation/tests/gate-branch-tails.spec.tsx
#	packages/client/ui-conversation/tests/input-bar.spec.tsx
#	packages/client/ui-conversation/tests/skeleton.spec.tsx
#	packages/host/apiproxy/README.md
#	packages/host/apiproxy/src/api-proxy.ts
#	pnpm-lock.yaml
#	scripts/verify-package-readme-model-experience.ts
#	tsconfig.base.json
This commit is contained in:
Yichen Jiang
2026-07-27 12:17:38 +08:00
1623 changed files with 49589 additions and 11464 deletions
+41 -7
View File
@@ -18,7 +18,7 @@
// (the plugin-row path discards the ReplayHandle; the direct install keeps
// assertConsumed for the teardown fixture-consumption check).
import { existsSync, readFileSync } from 'node:fs'
import { mkdtemp, readFile, readdir, rm, utimes, writeFile } from 'node:fs/promises'
import { mkdtemp, readFile, readdir, realpath, rm, utimes, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
@@ -31,8 +31,14 @@ import { scrubRequestHeaders } from '@deepseek-ai/dsh-acp-snapshot'
import { assertEntriesLoaded } from '@deepseek-ai/dsh-app-boot'
import type { ReplayHandle } from '@deepseek-ai/dsh-llm-replay'
import { installLlmReplay, parseSessionLog } from '@deepseek-ai/dsh-llm-replay'
import SessionStore, { SESSION_FORMAT_VERSION, SessionId } from '@deepseek-ai/dsh-session'
import type { Session, SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session'
import SessionStore, {
packChunkRuns,
SESSION_FORMAT_VERSION,
SessionId,
type Session,
type SessionEvent,
type SessionHeader,
} from '@deepseek-ai/dsh-session'
import SessionPersistenceJsonl from '@deepseek-ai/dsh-session-persistence-jsonl'
// Empty type imports carry the httpServer/agents/sessionPersistence Context merges.
import type {} from '@deepseek-ai/dsh-host-webserver'
@@ -105,8 +111,21 @@ export interface LaunchOptions {
* mounts).
*/
replayFixture?: string
/**
* Optional replay.override.json sidecar (whole-script replacement or
* `{ patches }` augmentation) for throw/hang scenarios not expressible as
* recorded chunks; replay/refresh only.
*/
replayOverride?: string
/** Per-chunk replay pacing (ms) so the browser observes genuinely incremental SSE; replay/refresh only. */
paceMs?: number
/**
* Tool presentation mode patched onto the shipped `tools` row (`code`
* collapses the wire to run_code + the SDK prompt section). Omit for the
* yml default. The code runtime row is always in the tree, so no extra
* insertion is needed.
*/
toolsMode?: 'native' | 'code' | 'both'
}
/** Dispose the booted tree and remove both owned temp roots, reporting every independent cleanup failure. */
@@ -132,7 +151,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
throw new Error('web e2e record mode needs DEEPSEEK_API_KEY (env or repo-root .env)')
}
}
const workspaceCwd = await mkdtemp(join(tmpdir(), 'dsh-web-e2e-ws-'))
const workspaceCwd = await realpath(await mkdtemp(join(tmpdir(), 'dsh-web-e2e-ws-')))
let persistenceRoot: string
try {
persistenceRoot = await mkdtemp(join(tmpdir(), 'dsh-web-e2e-sessions-'))
@@ -158,6 +177,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
{ id: 'workspace-context', disabled: true },
{ id: 'session-title-llm', disabled: true },
{ id: 'webserver', config: { host: '127.0.0.1', port: 0, distIndex: DIST_INDEX } },
...options.toolsMode === undefined ? [] : [{ id: 'tools', config: { mode: options.toolsMode } }],
...mode === 'record' ? [] : [{ id: 'llm-deepseek', disabled: true }],
]
@@ -192,6 +212,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
replayHandle = installLlmReplay(ctx, {
file: options.replayFixture,
providers: REPLAY_PROVIDERS,
...(options.replayOverride === undefined ? {} : { overrideFile: options.replayOverride }),
...(options.paceMs === undefined ? {} : { paceMs: options.paceMs }),
})
}
@@ -252,14 +273,13 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
}
/**
* Serialize a live session back to raw session-JSONL (header + events) — the
* Serialize a live session to the canonical raw session-JSONL layout — the
* in-memory record-mode harvest, so the on-disk zstd default never matters.
* Mirrors the TUI suite's rawSessionLog.
*/
function rawSessionLog(session: Session): string {
return [
JSON.stringify({ type: 'session', ...session.header }),
...session.events.map(event => JSON.stringify(event)),
...packChunkRuns(session.events).map(record => JSON.stringify(record)),
'',
].join('\n')
}
@@ -442,3 +462,17 @@ export function watchConsole(page: Page): { warnings: string[]; pageErrors: stri
page.on('pageerror', (error) => { pageErrors.push(String(error)) })
return { warnings, pageErrors }
}
/**
* Remove only connection-loss warnings emitted after an intentional reload.
* Earlier warnings and all gap-repair/discontinuity warnings remain fatal.
* @param tripwire - the live console-warning collector.
* @param warningStart - warning count captured immediately before reloading.
*/
export function acknowledgeReloadConnectionLoss(
tripwire: ReturnType<typeof watchConsole>,
warningStart: number,
): void {
const reloadWarnings = tripwire.warnings.splice(warningStart)
tripwire.warnings.push(...reloadWarnings.filter(text => !/connection lost/i.test(text)))
}