From 331352b038968be18797bd13219aa9aee8f3bce8 Mon Sep 17 00:00:00 2001 From: fairy <3054158379@qq.com> Date: Tue, 15 Sep 2026 12:04:23 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E4=B8=8E=E5=9B=9E=E7=AD=94=E4=B8=AD=E7=9A=84=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E9=A2=84=E8=A7=88=EF=BC=9A=E8=B5=B0=E9=99=84=E4=BB=B6=E7=9C=9F?= =?UTF-8?q?=E5=AE=9E=E8=B7=AF=E5=BE=84=EF=BC=8C=E5=A4=B1=E8=B4=A5=E5=88=99?= =?UTF-8?q?=E6=9C=AC=E6=9C=BA=E6=89=93=E5=BC=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 官方已支持输入框拖文件、回形针和默认工作区,不再重复实现。预览统一通过 fileHostPath 打开右侧面板,插件关闭或渲染失败时回退系统应用。 Co-authored-by: Cursor --- ...sh-api-session-controller+0.1.5-rc.2.patch | 585 +++++++++++++++--- ...ai+dsh-client-file-upload+0.1.5-rc.2.patch | 24 + ...+dsh-client-ui-attachment+0.1.5-rc.2.patch | 65 ++ ...eek-ai+dsh-client-ui-chat+0.1.5-rc.2.patch | 224 ++++++- ...sh-client-ui-conversation+0.1.5-rc.2.patch | 70 ++- ...sh-client-ui-deliverables+0.1.5-rc.2.patch | 28 +- ...i-sidebar-documentpreview+0.1.5-rc.2.patch | 119 ++++ test/file-preview.test.ts | 121 ++++ test/local-path-links.test.ts | 18 + 9 files changed, 1118 insertions(+), 136 deletions(-) create mode 100644 patches/@deepseek-ai+dsh-client-file-upload+0.1.5-rc.2.patch create mode 100644 patches/@deepseek-ai+dsh-client-ui-attachment+0.1.5-rc.2.patch create mode 100644 patches/@deepseek-ai+dsh-client-ui-sidebar-documentpreview+0.1.5-rc.2.patch create mode 100644 test/file-preview.test.ts diff --git a/patches/@deepseek-ai+dsh-api-session-controller+0.1.5-rc.2.patch b/patches/@deepseek-ai+dsh-api-session-controller+0.1.5-rc.2.patch index 82bf8f82e..8c1c1b3d5 100644 --- a/patches/@deepseek-ai+dsh-api-session-controller+0.1.5-rc.2.patch +++ b/patches/@deepseek-ai+dsh-api-session-controller+0.1.5-rc.2.patch @@ -1,7 +1,72 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/client.js b/node_modules/@deepseek-ai/dsh-api-session-controller/lib/client.js +index 6226c83..38862c8 100644 --- a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/client.js +++ b/node_modules/@deepseek-ai/dsh-api-session-controller/lib/client.js -@@ -2595,6 +2595,15 @@ +@@ -1720,6 +1720,63 @@ window.__ModuleLoader__.load({ + } + }; + } ++ /** ++ * Resolve one session-authorized uploaded file to its Host filesystem path. ++ * Used to open the stock document-preview pane; never treat the display name as a workspace path. ++ * @param attachmentIdOrName - opaque id from a staged receipt / session file block, or `{ name }` for a unique display-name lookup. ++ * @returns the absolute host path and display name. ++ */ ++ async readFileHostPath(attachmentIdOrName) { ++ const query = new URLSearchParams({ sessionId: this.sessionId }); ++ if (typeof attachmentIdOrName === "object" && attachmentIdOrName !== null && typeof attachmentIdOrName.name === "string") query.set("name", attachmentIdOrName.name); ++ else query.set("attachmentId", String(attachmentIdOrName)); ++ const response = await fetch(`/api/session/fileHostPath?${query.toString()}`, { credentials: "include" }); ++ if (response.status !== 200) return { ++ ok: false, ++ error: { ++ code: "gateway/internal", ++ message: `file host path failed with HTTP ${String(response.status)}`, ++ details: {} ++ } ++ }; ++ const body = await response.json(); ++ if (typeof body !== "object" || body === null || typeof body.ok !== "boolean") return { ++ ok: false, ++ error: { ++ code: "gateway/internal", ++ message: "file host path returned an invalid result", ++ details: {} ++ } ++ }; ++ if (!body.ok) { ++ const error = body.error; ++ return { ++ ok: false, ++ error: { ++ code: typeof error?.code === "string" ? error.code : "gateway/internal", ++ message: typeof error?.message === "string" ? error.message : "file host path failed", ++ details: typeof error?.details === "object" && error.details !== null ? error.details : {} ++ } ++ }; ++ } ++ const value = body.value; ++ if (typeof value?.path !== "string" || typeof value?.name !== "string" || typeof value?.bytes !== "number") return { ++ ok: false, ++ error: { ++ code: "gateway/internal", ++ message: "file host path returned an invalid value", ++ details: {} ++ } ++ }; ++ return { ++ ok: true, ++ value: { ++ path: value.path, ++ name: value.name, ++ bytes: value.bytes ++ } ++ }; ++ } + /** Apply one operation to a still-pending queue occurrence. */ + async updateQueue(itemId, action) { + return this.remote.session.updateQueue({ +@@ -2595,6 +2652,15 @@ window.__ModuleLoader__.load({ } return result; } @@ -17,7 +82,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/client.js /** * Contract session.fork; on success merge the child into summaries * immediately (same synchronous-addressability guarantee as create). The -@@ -2989,6 +2998,17 @@ +@@ -2989,6 +3055,17 @@ window.__ModuleLoader__.load({ this.sourceSessionId = sourceSessionId; } }; @@ -35,11 +100,10 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/client.js /** * Display title projection: durable title, project directory basename, then * the raw id. -@@ -3213,6 +3233,16 @@ - if (!result.ok) throw new SessionCreateError(result.error, opts.sessionId); +@@ -3214,6 +3291,16 @@ window.__ModuleLoader__.load({ this.projectList(); return result.value.sessionId; -+ } + } + /** Permanently delete one ordinary Session and clear its selected stage. */ + async delete(sessionId) { + const deletingCurrent = this.list.getSnapshot().current === sessionId; @@ -49,13 +113,24 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/client.js + if (deletingCurrent && (current === void 0 || current === sessionId)) this.clear(); + if (this.watched === sessionId) this.watched = void 0; + this.projectList(); - } ++ } /** * Fork a session from a completed-turn prefix of the source (same + * synchronous-addressability guarantee as {@link ClientSessions.create}: diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/index.js b/node_modules/@deepseek-ai/dsh-api-session-controller/lib/index.js +index 0e94dd3..a6875fa 100644 --- a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/index.js +++ b/node_modules/@deepseek-ai/dsh-api-session-controller/lib/index.js -@@ -177,11 +177,15 @@ +@@ -3,7 +3,7 @@ import z from "@deepseek-ai/schemastery"; + import { AssistantStreamAccumulator, ReasoningEffortId, assistantStreamChunks, createUserMessage, errorChain, freezeMessage } from "@deepseek-ai/dsh-llm"; + import { canOpenNativePath, nativeFileManager, openNativePath, revealNativePath } from "@deepseek-ai/dsh-native-command"; + import { Remote, RemoteError, TypertRemoteService, remoteErrorOf } from "@deepseek-ai/dsh-typert-protocol"; +-import { mkdir } from "node:fs/promises"; ++import { link, mkdir } from "node:fs/promises"; + import { installModelSelection } from "@deepseek-ai/dsh-agent"; + import { SessionQueryError } from "@deepseek-ai/dsh-session-query"; + import { randomUUID } from "node:crypto"; +@@ -177,11 +177,15 @@ var ApiSessionAgentController = class { ctx; resumes = /* @__PURE__ */ new Map(); creations = /* @__PURE__ */ new Map(); @@ -71,11 +146,10 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/index.js b ctx.typert.lookups.configure("agent", async (sessionId) => { const found = await this.resolveAgent(sessionId); if ("error" in found) throw found.error; -@@ -213,6 +217,21 @@ - */ +@@ -214,6 +218,21 @@ var ApiSessionAgentController = class { async resolveObservedAgent(observation) { return this.resolve(observation.header.id, observation); -+ } + } + /** Retain the teardown capability for an Agent created outside the normal create path. */ + retainHandle(handle) { + this.handles.set(handle.agent.id, handle); @@ -90,10 +164,11 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/index.js b + await handle.dispose(); + this.handles.delete(sessionId); + return true; - } ++ } async resolve(sessionId, observation) { const live = this.liveAgent(sessionId); -@@ -399,11 +418,11 @@ + if (live !== void 0) return live; +@@ -399,11 +418,11 @@ var ApiSessionAgentController = class { const published = this.ctx.sessions.get(sessionId); const live = this.ctx.agents.get(sessionId); if (published !== void 0 && hasApiSessionSubagentOwner(this.ctx, published, live)) throw new ApiSessionSubagentOwnership(sessionId); @@ -107,7 +182,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/index.js b } async createOrAdopt(sessionId, cwd, checkPersistedIdentity, presetId) { const attached = this.ctx.sessions.get(sessionId); -@@ -423,11 +442,11 @@ +@@ -423,11 +442,11 @@ var ApiSessionAgentController = class { const storedPreset = this.presetForObservation(observation); this.assertPresetUnchanged(sessionId, presetId, storedPreset); const composition = await this.composeAgent(storedPreset); @@ -121,7 +196,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/index.js b } catch (e_3) { env_3.error = e_3; env_3.hasError = true; -@@ -443,7 +462,7 @@ +@@ -443,7 +462,7 @@ var ApiSessionAgentController = class { throw new Error(`failed to ensure project directory "${cwd}": ${String(error)}`, { cause: error }); } const composition = await this.composeAgent(presetId); @@ -130,7 +205,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/index.js b sessionId, agentOptions: this.agentOptions(), meta: { -@@ -451,7 +470,7 @@ +@@ -451,7 +470,7 @@ var ApiSessionAgentController = class { ...composition.agentPreset === void 0 ? {} : { agentPreset: composition.agentPreset } }, setup: composition.setup @@ -139,7 +214,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/index.js b } agentOptions() { const { provider, model } = this.ctx.agentDefaultModel.currentSelection(); -@@ -652,6 +671,32 @@ +@@ -652,6 +671,32 @@ var SessionCommandController = class { throw new RemoteError("gateway/internal", `failed to rename session "${request.sessionId}": ${String(error)}`, {}); } } @@ -172,7 +247,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/index.js b /** * Create a new ordinary Session from one completed-turn prefix. * @param request - source Session and optional event anchor. -@@ -693,7 +738,7 @@ +@@ -693,7 +738,7 @@ var SessionCommandController = class { const composition = await this.agents.composeAgent(this.agents.presetForObservation(source)); try { const { provider, model } = this.ctx.agentDefaultModel.currentSelection(); @@ -181,7 +256,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/index.js b sessionId: childId, seed: source.events.slice(0, cut), inheritedEventCount: cut, -@@ -709,6 +754,7 @@ +@@ -709,6 +754,7 @@ var SessionCommandController = class { }, setup: composition.setup }); @@ -189,7 +264,297 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/index.js b } catch (error) { throw new RemoteError("gateway/internal", `failed to fork session "${request.sessionId}": ${String(error)}`, {}); } -@@ -2485,6 +2531,7 @@ +@@ -985,6 +1031,118 @@ function referencedImage(events, attachmentId) { + if (found !== void 0) return found; + } + } ++function fileBlockIn(content, match) { ++ if (!Array.isArray(content)) return void 0; ++ for (const value of content) { ++ if (typeof value !== "object" || value === null || Array.isArray(value)) continue; ++ const block = value; ++ if (block.type === "file" && typeof block.attachment === "object" && block.attachment !== null) { ++ const ref = block.attachment; ++ if (match(ref)) return ref; ++ } ++ if (block.type === "tool-result") { ++ const nested = fileBlockIn(block.content, match); ++ if (nested !== void 0) return nested; ++ } ++ } ++} ++function fileInEvent(event, match) { ++ const data = event.data; ++ const direct = fileBlockIn(data.content, match); ++ if (direct !== void 0) return direct; ++ const message = fileBlockIn(data.message?.content, match); ++ if (message !== void 0) return message; ++ for (const inserted of data.inserted ?? []) { ++ const found = fileBlockIn(inserted.content, match); ++ if (found !== void 0) return found; ++ } ++ if (event.type === "assistant/message" || event.type === "assistant/attempt") for (const chunk of assistantStreamChunks(event.data.stream, "block-end")) { ++ const found = fileBlockIn([chunk.block], match); ++ if (found !== void 0) return found; ++ } ++} ++function referencedFile(events, attachmentId) { ++ for (const event of events) { ++ const found = fileInEvent(event, (ref) => String(ref.attachmentId) === attachmentId); ++ if (found !== void 0) return found; ++ } ++} ++/** Leaf name of a path-like display string (POSIX or Windows separators). */ ++function basenameLeaf(value) { ++ const parts = String(value).split(/[/\\]/); ++ return parts[parts.length - 1] || String(value); ++} ++function fileNameMatches(refName, needle) { ++ const name = String(refName ?? ""); ++ const needleBase = basenameLeaf(needle); ++ return name === needle || basenameLeaf(name) === needle || basenameLeaf(name) === needleBase; ++} ++/** ++* Collect every distinct file attachment whose display name or basename matches. ++* Used when assistant prose mentions a file by name rather than attachment id. ++*/ ++function collectReferencedFilesByName(events, name) { ++ const matches = []; ++ const seen = /* @__PURE__ */ new Set(); ++ const visit = (ref) => { ++ if (!fileNameMatches(ref.name, name)) return; ++ const id = String(ref.attachmentId); ++ if (seen.has(id)) return; ++ seen.add(id); ++ matches.push(ref); ++ }; ++ for (const event of events) fileInEventCollect(event, visit); ++ return matches; ++} ++/** ++* Collect every distinct image attachment whose display name or basename matches. ++* Assistant prose often cites uploaded screenshots by filename. ++*/ ++function collectReferencedImagesByName(events, name) { ++ const matches = []; ++ const seen = /* @__PURE__ */ new Set(); ++ const visit = (ref) => { ++ if (!fileNameMatches(ref.name, name)) return; ++ const id = String(ref.attachmentId); ++ if (seen.has(id)) return; ++ seen.add(id); ++ matches.push(ref); ++ }; ++ for (const event of events) imageInEventCollect(event, visit); ++ return matches; ++} ++function fileBlockInCollect(content, visit) { ++ if (!Array.isArray(content)) return; ++ for (const value of content) { ++ if (typeof value !== "object" || value === null || Array.isArray(value)) continue; ++ const block = value; ++ if (block.type === "file" && typeof block.attachment === "object" && block.attachment !== null) visit(block.attachment); ++ if (block.type === "tool-result") fileBlockInCollect(block.content, visit); ++ } ++} ++function imageBlockInCollect(content, visit) { ++ if (!Array.isArray(content)) return; ++ for (const value of content) { ++ if (typeof value !== "object" || value === null || Array.isArray(value)) continue; ++ const block = value; ++ if (block.type === "image" && typeof block.attachment === "object" && block.attachment !== null) visit(block.attachment); ++ if (block.type === "tool-result") imageBlockInCollect(block.content, visit); ++ } ++} ++function fileInEventCollect(event, visit) { ++ const data = event.data; ++ fileBlockInCollect(data.content, visit); ++ fileBlockInCollect(data.message?.content, visit); ++ for (const inserted of data.inserted ?? []) fileBlockInCollect(inserted.content, visit); ++ if (event.type === "assistant/message" || event.type === "assistant/attempt") for (const chunk of assistantStreamChunks(event.data.stream, "block-end")) fileBlockInCollect([chunk.block], visit); ++} ++function imageInEventCollect(event, visit) { ++ const data = event.data; ++ imageBlockInCollect(data.content, visit); ++ imageBlockInCollect(data.message?.content, visit); ++ for (const inserted of data.inserted ?? []) imageBlockInCollect(inserted.content, visit); ++ if (event.type === "assistant/message" || event.type === "assistant/attempt") for (const chunk of assistantStreamChunks(event.data.stream, "block-end")) imageBlockInCollect([chunk.block], visit); ++} + function routeServed(ctx, provider) { + return ctx.llm.listProviders().some((entry) => entry.id === provider); + } +@@ -2374,6 +2532,170 @@ const SessionMediaReferences = { + }), "session-controller: /api/file"); + } + }; ++const FILE_HOST_PATH = "/api/session/fileHostPath"; ++function fileHostPathResult(ok, payload) { ++ return new Response(JSON.stringify(ok ? { ok: true, value: payload } : { ok: false, error: payload }), { ++ status: 200, ++ headers: { ++ "content-type": "application/json; charset=utf-8", ++ "cache-control": "no-store" ++ } ++ }); ++} ++async function resolveAuthorizedFileRef(ctx, sessionId, attachmentId) { ++ const live = ctx.agents.get(sessionId); ++ if (live !== void 0) { ++ const staged = ctx.fileUploads.findStagedFile(live, attachmentId); ++ if (staged !== void 0) return staged; ++ } ++ let events; ++ const attached = ctx.sessions.get(sessionId); ++ if (attached !== void 0) events = attached.snapshotEvents(); ++ else { ++ try { ++ events = (await inspectApiSession(ctx, sessionId)).events; ++ } catch (error) { ++ if (error instanceof ApiSessionNotFound) throw new RemoteError("session/not-found", error.message, { sessionId }); ++ if (error instanceof SessionQueryError && error.code === "SESSION_QUERY_SESSION_NOT_FOUND") throw new RemoteError("session/not-found", `session "${sessionId}" not found`, { sessionId }); ++ throw new RemoteError("gateway/internal", `attachment authorization unavailable for session "${sessionId}": ${String(error)}`, {}); ++ } ++ } ++ const ref = referencedFile(events, attachmentId); ++ if (ref === void 0) throw new RemoteError("session/attachment-invalid", "File is not referenced by this session.", { reason: "ATTACHMENT_NOT_REFERENCED" }); ++ return ref; ++} ++async function resolveAuthorizedFileRefByName(ctx, sessionId, name) { ++ const matches = []; ++ const seen = /* @__PURE__ */ new Set(); ++ const add = (kind, ref) => { ++ const id = String(ref.attachmentId); ++ if (seen.has(id)) return; ++ seen.add(id); ++ matches.push({ kind, ref }); ++ }; ++ let events; ++ const attached = ctx.sessions.get(sessionId); ++ if (attached !== void 0) events = attached.snapshotEvents(); ++ else { ++ try { ++ events = (await inspectApiSession(ctx, sessionId)).events; ++ } catch (error) { ++ if (error instanceof ApiSessionNotFound) throw new RemoteError("session/not-found", error.message, { sessionId }); ++ if (error instanceof SessionQueryError && error.code === "SESSION_QUERY_SESSION_NOT_FOUND") throw new RemoteError("session/not-found", `session "${sessionId}" not found`, { sessionId }); ++ throw new RemoteError("gateway/internal", `attachment authorization unavailable for session "${sessionId}": ${String(error)}`, {}); ++ } ++ } ++ for (const ref of collectReferencedFilesByName(events, name)) add("file", ref); ++ for (const ref of collectReferencedImagesByName(events, name)) add("image", ref); ++ if (matches.length === 0) throw new RemoteError("session/attachment-invalid", "File is not referenced by this session.", { reason: "ATTACHMENT_NOT_REFERENCED" }); ++ if (matches.length > 1) throw new RemoteError("session/attachment-invalid", "Multiple uploaded files share this name.", { reason: "ATTACHMENT_NAME_AMBIGUOUS" }); ++ return matches[0]; ++} ++function attachmentHostPath(ctx, match, fallbackName) { ++ if (match.kind === "image") { ++ const host = ctx.attachments.imageHostPath(match.ref); ++ if (host === void 0) return; ++ return ensureImagePreviewPath(host, match.ref, fallbackName); ++ } ++ return ctx.attachments.fileHostPath(match.ref); ++} ++/** ++* Document preview picks an image body from the path suffix. Attachment objects ++* are extensionless digests, so mint a same-inode alias that ends in `.png` etc. ++*/ ++async function ensureImagePreviewPath(hostPath, ref, fallbackName) { ++ const ext = imagePreviewExtension(ref, fallbackName); ++ if (ext === "" || hostPath.toLowerCase().endsWith(ext)) return hostPath; ++ const aliased = `${hostPath}${ext}`; ++ try { ++ await link(hostPath, aliased); ++ } catch (error) { ++ if (error && typeof error === "object" && "code" in error && error.code === "EEXIST") return aliased; ++ throw error; ++ } ++ return aliased; ++} ++function imagePreviewExtension(ref, fallbackName) { ++ for (const candidate of [ref.name, fallbackName]) { ++ const leaf = basenameLeaf(String(candidate ?? "")); ++ const dot = leaf.lastIndexOf("."); ++ if (dot > 0 && dot < leaf.length - 1) { ++ const ext = leaf.slice(dot).toLowerCase(); ++ if (/^\.[a-z0-9]{1,8}$/.test(ext)) return ext; ++ } ++ } ++ switch (String(ref.mediaType ?? "")) { ++ case "image/jpeg": return ".jpg"; ++ case "image/png": return ".png"; ++ case "image/gif": return ".gif"; ++ case "image/webp": return ".webp"; ++ case "image/bmp": return ".bmp"; ++ case "image/svg+xml": return ".svg"; ++ case "image/x-icon": return ".ico"; ++ default: return ".png"; ++ } ++} ++async function handleFileHostPath(ctx, request) { ++ if (request.method !== "GET") return new Response(null, { ++ status: 405, ++ headers: { allow: "GET" } ++ }); ++ const url = new URL(request.url); ++ const sessionId = url.searchParams.get("sessionId"); ++ const attachmentId = url.searchParams.get("attachmentId"); ++ const name = url.searchParams.get("name"); ++ if (sessionId === null || sessionId === "") return new Response("sessionId is required", { status: 400 }); ++ const hasAttachmentId = attachmentId !== null && attachmentId !== ""; ++ const hasName = name !== null && name !== ""; ++ if (!hasAttachmentId && !hasName) return new Response("attachmentId or name is required", { status: 400 }); ++ try { ++ let path; ++ let ref; ++ if (hasAttachmentId) { ++ ref = await resolveAuthorizedFileRef(ctx, brandString(sessionId), attachmentId); ++ path = ctx.attachments.fileHostPath(ref); ++ } else { ++ const match = await resolveAuthorizedFileRefByName(ctx, brandString(sessionId), name); ++ ref = match.ref; ++ path = await attachmentHostPath(ctx, match, name); ++ } ++ if (path === void 0) throw new RemoteError("session/attachment-invalid", "File host path is unavailable.", { reason: "FILE_HOST_PATH_UNAVAILABLE" }); ++ return fileHostPathResult(true, { ++ path, ++ name: typeof ref.name === "string" && ref.name !== "" ? ref.name : name ?? String(ref.attachmentId), ++ bytes: typeof ref.bytes === "number" ? ref.bytes : 0 ++ }); ++ } catch (error) { ++ const failure = remoteErrorOf(error); ++ return fileHostPathResult(false, failure !== void 0 ? { ++ code: failure.code, ++ message: failure.message, ++ details: failure.details ++ } : { ++ code: "gateway/internal", ++ message: error instanceof Error ? error.message : String(error), ++ details: {} ++ }); ++ } ++} ++const SessionFileHostPath = { ++ inject: [ ++ "connection", ++ "attachments", ++ "fileUploads", ++ "agents", ++ "sessions", ++ "sessionQuery" ++ ], ++ apply(ctx) { ++ ctx.effect(() => ctx.connection.fetch.register({ ++ path: FILE_HOST_PATH, ++ methods: ["GET"], ++ requestBody: "buffered", ++ fetch: (request) => handleFileHostPath(ctx, request) ++ }), "session-controller: /api/session/fileHostPath"); ++ } ++}; + //#endregion + //#region lib/types/index.js + /** Session Remote owner: cold reads, explicit Agent commands, and live control state. */ +@@ -2485,6 +2807,7 @@ let SessionController = (() => { let _canOpenWorkspacePath_decorators; let _openWorkspacePath_decorators; let _rename_decorators; @@ -197,7 +562,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/index.js b let _fork_decorators; let _prompt_decorators; let _attachment_decorators; -@@ -2504,6 +2551,7 @@ +@@ -2504,6 +2827,7 @@ let SessionController = (() => { _canOpenWorkspacePath_decorators = [Remote]; _openWorkspacePath_decorators = [Remote("openWorkspacePath")]; _rename_decorators = [Remote("rename")]; @@ -205,13 +570,10 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/index.js b _fork_decorators = [Remote("fork")]; _prompt_decorators = [Remote("prompt")]; _attachment_decorators = [Remote("attachment")]; -@@ -2597,6 +2645,17 @@ - access: { - has: (obj) => "rename" in obj, - get: (obj) => obj.rename -+ }, -+ metadata: _metadata -+ }, null, _instanceExtraInitializers); +@@ -2600,6 +2924,17 @@ let SessionController = (() => { + }, + metadata: _metadata + }, null, _instanceExtraInitializers); + __esDecorate(this, null, _delete_decorators, { + kind: "method", + name: "delete", @@ -220,11 +582,17 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/index.js b + access: { + has: (obj) => "delete" in obj, + get: (obj) => obj.delete - }, - metadata: _metadata - }, null, _instanceExtraInitializers); -@@ -2747,7 +2806,8 @@ ++ }, ++ metadata: _metadata ++ }, null, _instanceExtraInitializers); + __esDecorate(this, null, _fork_decorators, { + kind: "method", + name: "fork", +@@ -2745,9 +3080,11 @@ let SessionController = (() => { + this.canOpenPath = internals.canOpenPath ?? (() => config.nativeOpen ?? (internals.openPath !== void 0 || canOpenNativePath())); + ctx.plugin(SessionFileReferences); ctx.plugin(SessionMediaReferences); ++ ctx.plugin(SessionFileHostPath); ctx.plugin(SessionSkillCatalog); ctx.on("session/created", (session) => { - ctx.emit("api-session/added", this.listState.summaryFor(session)); @@ -233,7 +601,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/index.js b }); ctx.on("session/disposed", (session) => { ctx.emit("api-session/removed", session.id); -@@ -2903,6 +2963,10 @@ +@@ -2903,6 +3240,10 @@ let SessionController = (() => { rename(request) { return this.commands.rename(request); } @@ -245,25 +613,27 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/index.js b * Fork one cold-readable completed-turn prefix into a new Session. * @param request - source Session and optional event anchor. diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/typert.host.js b/node_modules/@deepseek-ai/dsh-api-session-controller/lib/typert.host.js +index f619491..e3a2704 100644 --- a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/typert.host.js +++ b/node_modules/@deepseek-ai/dsh-api-session-controller/lib/typert.host.js -@@ -222,6 +222,12 @@ - const _deepseek_ai_dsh_api_session_controller_session_create_result$schema = z.object({ +@@ -223,6 +223,12 @@ const _deepseek_ai_dsh_api_session_controller_session_create_result$schema = z.o 'sessionId': z.intersection(z.string(), z.unknown()).readonly(), 'agentPreset': z.string().readonly().optional(), -+}) + }) +const _deepseek_ai_dsh_api_session_controller_session_delete_parameter_0$schema = z.object({ + 'sessionId': z.intersection(z.string(), z.unknown()).readonly(), +}) +const _deepseek_ai_dsh_api_session_controller_session_delete_result$schema = z.object({ + 'deleted': z.literal(true).readonly(), - }) ++}) const _deepseek_ai_dsh_api_session_controller_session_follow_parameter_0$schema = z.object({ 'address': z.union([z.object({ -@@ -816,6 +822,31 @@ + 'kind': z.literal("session").readonly(), +@@ -815,6 +821,31 @@ export const TYPERT = { + }, sourceLocation: {"file":"packages/api/session-controller/src/index.ts","line":410,"column":3}, }, - { ++ { + id: '@deepseek-ai/dsh-api-session-controller#session/delete', + service: 'sessionController', + namespace: 'session', @@ -288,29 +658,27 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/typert.hos + }, + sourceLocation: {"file":"packages/api/session-controller/src/index.ts","line":312,"column":3}, + }, -+ { + { id: '@deepseek-ai/dsh-api-session-controller#session/create', service: 'sessionController', - namespace: 'session', -@@ -1228,6 +1259,13 @@ +@@ -1226,6 +1257,13 @@ export const TYPERT = { + "summary": "Rename one Session after explicitly resuming it.", + "jsDoc": "/**\n * Rename one Session after explicitly resuming it.\n * @param request - Session identity and proposed title.\n * @returns the accepted title and durable event sequence.\n */" }, - { - "kind": "method", ++ { ++ "kind": "method", + "name": "delete", + "signature": "@Remote('delete') delete(request: SessionDeleteRequest): Promise", + "summary": "Permanently delete one ordinary Session while retaining Workspace files.", + "jsDoc": "/** Permanently delete one ordinary Session while retaining Workspace files. */" + }, -+ { -+ "kind": "method", - "name": "fork", - "signature": "@Remote('fork') fork(request: SessionForkRequest): Promise", - "summary": "Fork one cold-readable completed-turn prefix into a new Session.", -@@ -1703,6 +1741,14 @@ { + "kind": "method", + "name": "fork", +@@ -1704,6 +1742,14 @@ export const TYPERT = { "name": "SessionCreateValue", "declaration": "export interface SessionCreateValue {\n readonly sessionId: SessionId;\n readonly agentPreset?: string;\n}" -+ }, + }, + { + "name": "SessionDeleteRequest", + "declaration": "export interface SessionDeleteRequest {\n readonly sessionId: SessionId;\n}" @@ -318,13 +686,15 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/typert.hos + { + "name": "SessionDeleteValue", + "declaration": "export interface SessionDeleteValue {\n readonly deleted: true;\n}" - }, ++ }, { "name": "SessionEvent", + "declaration": "export type SessionEvent = { [K in SessionEventType]: { type: K; seq: SessionSeq; time: number; data: SessionEventMap[K]; ignorable?: true; } & (K extends SurfaceEventType ? SurfaceIntent : { surfaceOp?: never; sourceEventSeqs?: never; }) }[T];" diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/typert.remote-client.d.ts b/node_modules/@deepseek-ai/dsh-api-session-controller/lib/typert.remote-client.d.ts +index 16606ef..ebc8266 100644 --- a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/typert.remote-client.d.ts +++ b/node_modules/@deepseek-ai/dsh-api-session-controller/lib/typert.remote-client.d.ts -@@ -3,7 +3,7 @@ +@@ -3,7 +3,7 @@ import type { RemoteResult, TypertRemoteContribution, } from '@deepseek-ai/dsh-typert-protocol' @@ -333,7 +703,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/typert.rem import type { FileReferenceCandidate } from '@deepseek-ai/dsh-file-reference/types' import type { SessionId } from '@deepseek-ai/dsh-session/types' -@@ -17,6 +17,7 @@ +@@ -17,6 +17,7 @@ declare module '@deepseek-ai/dsh-typert-protocol' { canOpenWorkspacePath: () => Promise> control: (signal?: AbortSignal) => AsyncIterable create: (request: SessionCreateRequest) => Promise> @@ -341,7 +711,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/typert.rem follow: (request: SessionFollowRequest, signal?: AbortSignal) => AsyncIterable fork: (request: SessionForkRequest) => Promise> list: (_request: SessionListRequest, signal?: AbortSignal) => Promise> -@@ -39,6 +40,7 @@ +@@ -39,6 +40,7 @@ declare module '@deepseek-ai/dsh-typert-protocol' { 'session/canOpenWorkspacePath': () => Promise> 'session/control': (signal?: AbortSignal) => AsyncIterable 'session/create': (request: SessionCreateRequest) => Promise> @@ -350,25 +720,27 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/typert.rem 'session/fork': (request: SessionForkRequest) => Promise> 'session/list': (_request: SessionListRequest, signal?: AbortSignal) => Promise> diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/typert.remote-client.js b/node_modules/@deepseek-ai/dsh-api-session-controller/lib/typert.remote-client.js +index 4009608..7fc3096 100644 --- a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/typert.remote-client.js +++ b/node_modules/@deepseek-ai/dsh-api-session-controller/lib/typert.remote-client.js -@@ -222,6 +222,12 @@ - const _deepseek_ai_dsh_api_session_controller_session_create_result$schema = z.object({ +@@ -223,6 +223,12 @@ const _deepseek_ai_dsh_api_session_controller_session_create_result$schema = z.o 'sessionId': z.intersection(z.string(), z.unknown()).readonly(), 'agentPreset': z.string().readonly().optional(), -+}) + }) +const _deepseek_ai_dsh_api_session_controller_session_delete_parameter_0$schema = z.object({ + 'sessionId': z.intersection(z.string(), z.unknown()).readonly(), +}) +const _deepseek_ai_dsh_api_session_controller_session_delete_result$schema = z.object({ + 'deleted': z.literal(true).readonly(), - }) ++}) const _deepseek_ai_dsh_api_session_controller_session_follow_parameter_0$schema = z.object({ 'address': z.union([z.object({ -@@ -813,6 +819,31 @@ + 'kind': z.literal("session").readonly(), +@@ -812,6 +818,31 @@ export const TYPERT_REMOTE = { + }, sourceLocation: {"file":"packages/api/session-controller/src/index.ts","line":410,"column":3}, }, - { ++ { + id: '@deepseek-ai/dsh-api-session-controller#session/delete', + service: 'sessionController', + namespace: 'session', @@ -393,11 +765,11 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/typert.rem + }, + sourceLocation: {"file":"packages/api/session-controller/src/index.ts","line":312,"column":3}, + }, -+ { + { id: '@deepseek-ai/dsh-api-session-controller#session/create', service: 'sessionController', - namespace: 'session', diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/agent.d.ts b/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/agent.d.ts +index cd2b002..637eeb9 100644 --- a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/agent.d.ts +++ b/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/agent.d.ts @@ -1,6 +1,6 @@ @@ -408,7 +780,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/agen import type { Session, SessionId } from '@deepseek-ai/dsh-session'; import type { SessionInspection } from '@deepseek-ai/dsh-session-persistence'; import { type SessionObservation } from '@deepseek-ai/dsh-session-query'; -@@ -67,6 +67,7 @@ +@@ -67,6 +67,7 @@ export declare class ApiSessionAgentController { private readonly ctx; private readonly resumes; private readonly creations; @@ -416,7 +788,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/agen private readonly selections; private readonly imageAdmissionChains; /** @param ctx - Host context carrying Agent, model, persistence, and Typert services. */ -@@ -83,6 +84,10 @@ +@@ -83,6 +84,10 @@ export declare class ApiSessionAgentController { * @returns the live Agent or a stable Session-domain failure. */ resolveObservedAgent(observation: SessionObservation): Promise; @@ -428,9 +800,10 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/agen /** * Resolve one requested identity, creating or resuming it once. diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/agent.js b/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/agent.js +index cd98ec7..27e9f2e 100644 --- a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/agent.js +++ b/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/agent.js -@@ -165,11 +165,13 @@ +@@ -165,11 +165,13 @@ export class ApiSessionAgentController { ctx; resumes = new Map(); creations = new Map(); @@ -444,11 +817,10 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/agen ctx.typert.lookups.configure('agent', async (sessionId) => { const found = await this.resolveAgent(sessionId); if ('error' in found) -@@ -204,6 +206,23 @@ - */ +@@ -205,6 +207,23 @@ export class ApiSessionAgentController { async resolveObservedAgent(observation) { return this.resolve(observation.header.id, observation); -+ } + } + /** Retain the teardown capability for an Agent created outside the normal create path. */ + retainHandle(handle) { + this.handles.set(handle.agent.id, handle); @@ -465,10 +837,11 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/agen + await handle.dispose(); + this.handles.delete(sessionId); + return true; - } ++ } async resolve(sessionId, observation) { const live = this.liveAgent(sessionId); -@@ -438,11 +457,11 @@ + if (live !== undefined) +@@ -438,11 +457,11 @@ export class ApiSessionAgentController { if (published !== undefined && hasApiSessionSubagentOwner(this.ctx, published, live)) { throw new ApiSessionSubagentOwnership(sessionId); } @@ -482,7 +855,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/agen } async createOrAdopt(sessionId, cwd, checkPersistedIdentity, presetId) { const attached = this.ctx.sessions.get(sessionId); -@@ -466,11 +485,11 @@ +@@ -466,11 +485,11 @@ export class ApiSessionAgentController { const storedPreset = this.presetForObservation(observation); this.assertPresetUnchanged(sessionId, presetId, storedPreset); const composition = await this.composeAgent(storedPreset); @@ -496,7 +869,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/agen } catch (e_3) { env_3.error = e_3; -@@ -493,7 +512,7 @@ +@@ -493,7 +512,7 @@ export class ApiSessionAgentController { throw new Error(`failed to ensure project directory "${cwd}": ${String(error)}`, { cause: error }); } const composition = await this.composeAgent(presetId); @@ -505,7 +878,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/agen sessionId, agentOptions: this.agentOptions(), meta: { -@@ -501,7 +520,7 @@ +@@ -501,7 +520,7 @@ export class ApiSessionAgentController { ...(composition.agentPreset === undefined ? {} : { agentPreset: composition.agentPreset }), }, setup: composition.setup, @@ -515,9 +888,10 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/agen agentOptions() { const { provider, model } = this.ctx.agentDefaultModel.currentSelection(); diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/client/contract/sessions.d.ts b/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/client/contract/sessions.d.ts +index 037b920..7b1996d 100644 --- a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/client/contract/sessions.d.ts +++ b/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/client/contract/sessions.d.ts -@@ -35,6 +35,8 @@ +@@ -35,6 +35,8 @@ export interface ISessions { cwd?: string; sessionId?: SessionId; }): Promise; @@ -527,9 +901,10 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/clie * Select a session as current. * @param id - session id (must exist in the list; unknown ids fail loud). diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/client/sessions/manager.d.ts b/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/client/sessions/manager.d.ts +index f6f616a..e59e0e9 100644 --- a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/client/sessions/manager.d.ts +++ b/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/client/sessions/manager.d.ts -@@ -182,6 +182,10 @@ +@@ -182,6 +182,10 @@ export declare class SessionManager { }): Promise>; @@ -541,9 +916,10 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/clie * Contract session.fork; on success merge the child into summaries * immediately (same synchronous-addressability guarantee as create). The diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/client/sessions/manager.js b/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/client/sessions/manager.js +index 73d85a5..e19b001 100644 --- a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/client/sessions/manager.js +++ b/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/client/sessions/manager.js -@@ -484,6 +484,13 @@ +@@ -484,6 +484,13 @@ export class SessionManager { } return result; } @@ -558,9 +934,10 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/clie * Contract session.fork; on success merge the child into summaries * immediately (same synchronous-addressability guarantee as create). The diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/client/sessions/service.d.ts b/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/client/sessions/service.d.ts +index e6fb2c6..73fdb2a 100644 --- a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/client/sessions/service.d.ts +++ b/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/client/sessions/service.d.ts -@@ -99,6 +99,13 @@ +@@ -99,6 +99,13 @@ export declare class SessionForkError extends Error { */ constructor(rpcError: RemoteFailure, sourceSessionId: SessionId); } @@ -574,7 +951,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/clie /** Identity-stable logical binding for one materialized Client Session. */ export interface SessionBinding { readonly sessionId: SessionId; -@@ -249,6 +256,8 @@ +@@ -249,6 +256,8 @@ export declare class ClientSessions implements ISessions { cwd?: string; sessionId?: SessionId; }): Promise; @@ -584,14 +961,13 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/clie * Fork a session from a completed-turn prefix of the source (same * synchronous-addressability guarantee as {@link ClientSessions.create}: diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/client/sessions/service.js b/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/client/sessions/service.js +index 6786a43..3e68a2d 100644 --- a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/client/sessions/service.js +++ b/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/client/sessions/service.js -@@ -32,6 +32,17 @@ - super(`session fork failed: ${rpcError.code}: ${rpcError.message}`); - this.rpcError = rpcError; +@@ -34,6 +34,17 @@ export class SessionForkError extends Error { this.sourceSessionId = sourceSessionId; -+ } -+} + } + } +/** Structured permanent Session-deletion failure. */ +export class SessionDeleteError extends Error { + rpcError; @@ -601,10 +977,12 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/clie + super(`session delete failed: ${rpcError.code}: ${rpcError.message}`); + this.rpcError = rpcError; + this.sessionId = sessionId; - } - } ++ } ++} // Scope primitives live in ../scope.ts (the client mirror of host -@@ -274,6 +285,19 @@ + // dsh-scope, keyed by Agent identity); re-exported here so existing + // consumers keep their import site. +@@ -274,6 +285,19 @@ export class ClientSessions { this.projectList(); return result.value.sessionId; } @@ -625,6 +1003,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/clie * Fork a session from a completed-turn prefix of the source (same * synchronous-addressability guarantee as {@link ClientSessions.create}: diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/commands.d.ts b/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/commands.d.ts +index df33d9d..c27794f 100644 --- a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/commands.d.ts +++ b/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/commands.d.ts @@ -1,7 +1,7 @@ @@ -636,7 +1015,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/comm /** Implements Session business commands delegated by the Session Controller Remote service. */ export declare class SessionCommandController { private readonly ctx; -@@ -31,6 +31,8 @@ +@@ -31,6 +31,8 @@ export declare class SessionCommandController { * @returns the accepted title and durable event sequence. */ rename(request: SessionRenameRequest): Promise; @@ -646,9 +1025,10 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/comm * Create a new ordinary Session from one completed-turn prefix. * @param request - source Session and optional event anchor. diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/commands.js b/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/commands.js +index c210650..806174e 100644 --- a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/commands.js +++ b/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/commands.js -@@ -179,6 +179,47 @@ +@@ -179,6 +179,47 @@ export class SessionCommandController { throw new RemoteError('gateway/internal', `failed to rename session "${request.sessionId}": ${String(error)}`, {}); } } @@ -696,7 +1076,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/comm /** * Create a new ordinary Session from one completed-turn prefix. * @param request - source Session and optional event anchor. -@@ -236,7 +277,7 @@ +@@ -236,7 +277,7 @@ export class SessionCommandController { const composition = await this.agents.composeAgent(this.agents.presetForObservation(source)); try { const { provider, model } = this.ctx.agentDefaultModel.currentSelection(); @@ -705,7 +1085,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/comm sessionId: childId, seed: source.events.slice(0, cut), inheritedEventCount: cut, -@@ -251,6 +292,7 @@ +@@ -251,6 +292,7 @@ export class SessionCommandController { agentOptions: { provider, model }, setup: composition.setup, }); @@ -714,9 +1094,10 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/comm catch (error) { throw new RemoteError('gateway/internal', `failed to fork session "${request.sessionId}": ${String(error)}`, {}); diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/index.d.ts b/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/index.d.ts +index f2c0f34..3930842 100644 --- a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/index.d.ts +++ b/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/index.d.ts -@@ -6,7 +6,7 @@ +@@ -6,7 +6,7 @@ import type { SessionInspection } from '@deepseek-ai/dsh-session-persistence'; import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'; import { type ApiSessionAgentResult } from './agent.ts'; import { buildModelCatalog } from './catalog.ts'; @@ -725,7 +1106,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/inde export type * from './types.ts'; export { ApiSessionNotFound } from './agent.ts'; export { SessionFileReferences } from './file-references.ts'; -@@ -123,6 +123,8 @@ +@@ -123,6 +123,8 @@ export declare class SessionController extends TypertRemoteService { * @returns the accepted title and durable event sequence. */ rename(request: SessionRenameRequest): Promise; @@ -735,9 +1116,10 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/inde * Fork one cold-readable completed-turn prefix into a new Session. * @param request - source Session and optional event anchor. diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/index.js b/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/index.js +index 2a7cabd..e40d2c4 100644 --- a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/index.js +++ b/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/index.js -@@ -115,6 +115,7 @@ +@@ -115,6 +115,7 @@ let SessionController = (() => { let _canOpenWorkspacePath_decorators; let _openWorkspacePath_decorators; let _rename_decorators; @@ -745,7 +1127,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/inde let _fork_decorators; let _prompt_decorators; let _attachment_decorators; -@@ -134,6 +135,7 @@ +@@ -134,6 +135,7 @@ let SessionController = (() => { _canOpenWorkspacePath_decorators = [Remote]; _openWorkspacePath_decorators = [Remote('openWorkspacePath')]; _rename_decorators = [Remote('rename')]; @@ -753,7 +1135,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/inde _fork_decorators = [Remote('fork')]; _prompt_decorators = [Remote('prompt')]; _attachment_decorators = [Remote('attachment')]; -@@ -150,6 +152,7 @@ +@@ -150,6 +152,7 @@ let SessionController = (() => { __esDecorate(this, null, _canOpenWorkspacePath_decorators, { kind: "method", name: "canOpenWorkspacePath", static: false, private: false, access: { has: obj => "canOpenWorkspacePath" in obj, get: obj => obj.canOpenWorkspacePath }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(this, null, _openWorkspacePath_decorators, { kind: "method", name: "openWorkspacePath", static: false, private: false, access: { has: obj => "openWorkspacePath" in obj, get: obj => obj.openWorkspacePath }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(this, null, _rename_decorators, { kind: "method", name: "rename", static: false, private: false, access: { has: obj => "rename" in obj, get: obj => obj.rename }, metadata: _metadata }, null, _instanceExtraInitializers); @@ -761,7 +1143,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/inde __esDecorate(this, null, _fork_decorators, { kind: "method", name: "fork", static: false, private: false, access: { has: obj => "fork" in obj, get: obj => obj.fork }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(this, null, _prompt_decorators, { kind: "method", name: "prompt", static: false, private: false, access: { has: obj => "prompt" in obj, get: obj => obj.prompt }, metadata: _metadata }, null, _instanceExtraInitializers); __esDecorate(this, null, _attachment_decorators, { kind: "method", name: "attachment", static: false, private: false, access: { has: obj => "attachment" in obj, get: obj => obj.attachment }, metadata: _metadata }, null, _instanceExtraInitializers); -@@ -216,7 +219,8 @@ +@@ -216,7 +219,8 @@ let SessionController = (() => { ctx.plugin(SessionMediaReferences); ctx.plugin(SessionSkillCatalog); ctx.on('session/created', (session) => { @@ -771,7 +1153,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/inde }); ctx.on('session/disposed', (session) => { ctx.emit('api-session/removed', session.id); -@@ -375,6 +379,10 @@ +@@ -375,6 +379,10 @@ let SessionController = (() => { rename(request) { return this.commands.rename(request); } @@ -783,13 +1165,13 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/inde * Fork one cold-readable completed-turn prefix into a new Session. * @param request - source Session and optional event anchor. diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/types.d.ts b/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/types.d.ts +index b468dba..cc60213 100644 --- a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/types.d.ts +++ b/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/types.d.ts -@@ -278,6 +278,14 @@ - export interface SessionRenameValue { +@@ -279,6 +279,14 @@ export interface SessionRenameValue { readonly title: string; readonly seq: number; -+} + } +/** Session permanent-deletion request. */ +export interface SessionDeleteRequest { + readonly sessionId: SessionId; @@ -797,6 +1179,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/types/type +/** Session permanent-deletion response. */ +export interface SessionDeleteValue { + readonly deleted: true; - } ++} /** Session fork request. */ export interface SessionForkRequest { + readonly sessionId: SessionId; diff --git a/patches/@deepseek-ai+dsh-client-file-upload+0.1.5-rc.2.patch b/patches/@deepseek-ai+dsh-client-file-upload+0.1.5-rc.2.patch new file mode 100644 index 000000000..6e2671fd9 --- /dev/null +++ b/patches/@deepseek-ai+dsh-client-file-upload+0.1.5-rc.2.patch @@ -0,0 +1,24 @@ +diff --git a/node_modules/@deepseek-ai/dsh-client-file-upload/lib/index.js b/node_modules/@deepseek-ai/dsh-client-file-upload/lib/index.js +index 167cc5d..780f957 100644 +--- a/node_modules/@deepseek-ai/dsh-client-file-upload/lib/index.js ++++ b/node_modules/@deepseek-ai/dsh-client-file-upload/lib/index.js +@@ -230,6 +230,19 @@ let FileUploads = (() => { + return this.stagedFiles.get(agent.session)?.get(receiptId)?.file; + } + /** ++ * Find one staged durable file by attachment id inside the Agent scope. ++ * @param agent - receiving Agent. ++ * @param attachmentId - content-addressed file identity. ++ * @returns durable file reference, or `undefined` when nothing staged matches. ++ */ ++ findStagedFile(agent, attachmentId) { ++ this.assertAgentScope(agent); ++ const staged = this.stagedFiles.get(agent.session); ++ if (staged === void 0) return; ++ const id = String(attachmentId); ++ for (const upload of staged.values()) if (String(upload.file.attachmentId) === id) return upload.file; ++ } ++ /** + * Bind receipts while one prompt enters an Agent inbox. + * Disposal restores every prior binding unless the caller commits successful delivery. + * @param agent - receiving Agent. diff --git a/patches/@deepseek-ai+dsh-client-ui-attachment+0.1.5-rc.2.patch b/patches/@deepseek-ai+dsh-client-ui-attachment+0.1.5-rc.2.patch new file mode 100644 index 000000000..d54c07a96 --- /dev/null +++ b/patches/@deepseek-ai+dsh-client-ui-attachment+0.1.5-rc.2.patch @@ -0,0 +1,65 @@ +diff --git a/node_modules/@deepseek-ai/dsh-client-ui-attachment/lib/client.js b/node_modules/@deepseek-ai/dsh-client-ui-attachment/lib/client.js +index 0b50b91..6cab321 100644 +--- a/node_modules/@deepseek-ai/dsh-client-ui-attachment/lib/client.js ++++ b/node_modules/@deepseek-ai/dsh-client-ui-attachment/lib/client.js +@@ -357,7 +357,7 @@ window.__ModuleLoader__.load({ + }); + //#endregion + //#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-attachment/src/FileCard.module.css.mjs +- const css$3 = ".gSkjMW_card{border:.5px solid var(--dsw-alias-border-l2,#0000001f);background:var(--dsw-specific-input-major,transparent);box-sizing:border-box;text-align:left;border-radius:16px;align-items:center;gap:10px;width:240px;height:64px;padding:0 12px;display:inline-flex;position:relative}.gSkjMW_failed{border-color:var(--dsw-alias-state-error-primary,#d54941)}.gSkjMW_icon{flex:none;justify-content:center;align-items:center;width:28px;height:28px;display:inline-flex}.gSkjMW_spinner{corner-shape:round;border:2px solid;border-top-color:#0000;border-radius:50%;width:20px;height:20px;animation:.8s linear infinite gSkjMW_file-card-spin}@keyframes gSkjMW_file-card-spin{to{transform:rotate(360deg)}}.gSkjMW_body{flex-direction:column;flex:1;min-width:0;padding:8px 0;display:flex}.gSkjMW_retry{color:inherit;font:inherit;text-align:left;cursor:pointer;background:0 0;border:0;padding:0}.gSkjMW_name{white-space:nowrap;text-overflow:ellipsis;color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500;line-height:22px;overflow:hidden}.gSkjMW_meta{white-space:nowrap;text-overflow:ellipsis;color:var(--dsw-alias-label-tertiary,#00000073);font-size:12px;line-height:15px;overflow:hidden}.gSkjMW_metaFailed{color:var(--dsw-alias-state-error-primary,#d54941)}.gSkjMW_remove{corner-shape:round;background:var(--dsw-alias-button-contrast-fill,#000000b8);width:18px;height:18px;color:var(--dsw-alias-label-primary-inverted,#fff);opacity:0;cursor:pointer;border:none;border-radius:50%;justify-content:center;align-items:center;padding:0;transition:opacity .2s ease-in-out;display:inline-flex;position:absolute;top:6px;right:6px}.gSkjMW_card:hover .gSkjMW_remove,.gSkjMW_remove:focus-visible{opacity:1}@media (pointer:coarse){.gSkjMW_remove{opacity:1}}@media (prefers-reduced-motion:reduce){.gSkjMW_remove{transition:none}}.gSkjMW_card:hover .gSkjMW_name,.gSkjMW_card:focus-within .gSkjMW_name{padding-right:18px}.gSkjMW_removeFailed{background:var(--dsw-alias-state-error-primary,#d54941);color:#fff;opacity:1}.gSkjMW_progressTrack{background:var(--dsw-alias-fill-tertiary,#00000014);border-radius:1px;height:2px;position:absolute;bottom:5px;left:12px;right:12px;overflow:hidden}.gSkjMW_progressBar{border-radius:inherit;background:var(--dsw-alias-brand-primary,#4d6bfe);width:35%;height:100%;animation:1.2s ease-in-out infinite alternate gSkjMW_file-card-progress;display:block}.gSkjMW_progressBar[style]{animation:none}@keyframes gSkjMW_file-card-progress{0%{transform:translate(-70%)}to{transform:translate(220%)}}@media (pointer:coarse){.gSkjMW_remove{opacity:1}}"; ++ const css$3 = ".gSkjMW_card{border:.5px solid var(--dsw-alias-border-l2,#0000001f);background:var(--dsw-specific-input-major,transparent);box-sizing:border-box;text-align:left;border-radius:16px;align-items:center;gap:10px;width:240px;height:64px;padding:0 12px;display:inline-flex;position:relative}.gSkjMW_preview{color:inherit;font:inherit;text-align:left;cursor:pointer;background:0 0;border:0;padding:0}.gSkjMW_failed{border-color:var(--dsw-alias-state-error-primary,#d54941)}.gSkjMW_icon{flex:none;justify-content:center;align-items:center;width:28px;height:28px;display:inline-flex}.gSkjMW_spinner{corner-shape:round;border:2px solid;border-top-color:#0000;border-radius:50%;width:20px;height:20px;animation:.8s linear infinite gSkjMW_file-card-spin}@keyframes gSkjMW_file-card-spin{to{transform:rotate(360deg)}}.gSkjMW_body{flex-direction:column;flex:1;min-width:0;padding:8px 0;display:flex}.gSkjMW_retry{color:inherit;font:inherit;text-align:left;cursor:pointer;background:0 0;border:0;padding:0}.gSkjMW_name{white-space:nowrap;text-overflow:ellipsis;color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500;line-height:22px;overflow:hidden}.gSkjMW_meta{white-space:nowrap;text-overflow:ellipsis;color:var(--dsw-alias-label-tertiary,#00000073);font-size:12px;line-height:15px;overflow:hidden}.gSkjMW_metaFailed{color:var(--dsw-alias-state-error-primary,#d54941)}.gSkjMW_remove{corner-shape:round;background:var(--dsw-alias-button-contrast-fill,#000000b8);width:18px;height:18px;color:var(--dsw-alias-label-primary-inverted,#fff);opacity:0;cursor:pointer;border:none;border-radius:50%;justify-content:center;align-items:center;padding:0;transition:opacity .2s ease-in-out;display:inline-flex;position:absolute;top:6px;right:6px}.gSkjMW_card:hover .gSkjMW_remove,.gSkjMW_remove:focus-visible{opacity:1}@media (pointer:coarse){.gSkjMW_remove{opacity:1}}@media (prefers-reduced-motion:reduce){.gSkjMW_remove{transition:none}}.gSkjMW_card:hover .gSkjMW_name,.gSkjMW_card:focus-within .gSkjMW_name{padding-right:18px}.gSkjMW_removeFailed{background:var(--dsw-alias-state-error-primary,#d54941);color:#fff;opacity:1}.gSkjMW_progressTrack{background:var(--dsw-alias-fill-tertiary,#00000014);border-radius:1px;height:2px;position:absolute;bottom:5px;left:12px;right:12px;overflow:hidden}.gSkjMW_progressBar{border-radius:inherit;background:var(--dsw-alias-brand-primary,#4d6bfe);width:35%;height:100%;animation:1.2s ease-in-out infinite alternate gSkjMW_file-card-progress;display:block}.gSkjMW_progressBar[style]{animation:none}@keyframes gSkjMW_file-card-progress{0%{transform:translate(-70%)}to{transform:translate(220%)}}@media (pointer:coarse){.gSkjMW_remove{opacity:1}}"; + const tagId$3 = "@deepseek-ai/dsh-client-ui-attachment/FileCard.module.css"; + if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]") === null) { + const tag = document.createElement("style"); +@@ -376,6 +376,7 @@ window.__ModuleLoader__.load({ + "meta": "gSkjMW_meta", + "metaFailed": "gSkjMW_metaFailed", + "name": "gSkjMW_name", ++ "preview": "gSkjMW_preview", + "progressBar": "gSkjMW_progressBar", + "progressTrack": "gSkjMW_progressTrack", + "remove": "gSkjMW_remove", +@@ -386,7 +387,7 @@ window.__ModuleLoader__.load({ + //#endregion + //#region lib/types/FileCard.js + /** One pending file card: type glyph, name, size or upload status, remove, retry. */ +- function FileCard({ name, bytes, state, progress, labels, onRemove, onRetry }) { ++ function FileCard({ name, bytes, state, progress, labels, onRemove, onRetry, onOpen }) { + const extension = (0, _deepseek_ai_dsh_client_ui_primitives.fileExtension)(name).toUpperCase().slice(0, 8); + const meta = state === "uploading" ? labels.uploading : state === "error" ? labels.failed : [extension, (0, _deepseek_ai_dsh_client_ui_primitives.fileSizeText)(bytes)].filter((part) => part !== "").join(" "); + const retryable = state === "error"; +@@ -411,9 +412,11 @@ window.__ModuleLoader__.load({ + className: `${FileCard_module_css_default.meta} ${FileCard_module_css_default.metaFailed}`, + children: meta + })] +- }) : (0, react_jsx_runtime.jsxs)("span", { +- className: FileCard_module_css_default.body, ++ }) : (0, react_jsx_runtime.jsxs)(onOpen === void 0 ? "span" : "button", { ++ type: onOpen === void 0 ? void 0 : "button", ++ className: onOpen === void 0 ? FileCard_module_css_default.body : `${FileCard_module_css_default.body} ${FileCard_module_css_default.preview}`, + "aria-label": labels.label, ++ onClick: onOpen, + children: [(0, react_jsx_runtime.jsx)("span", { + className: FileCard_module_css_default.name, + children: name +@@ -603,7 +606,7 @@ window.__ModuleLoader__.load({ + //#endregion + //#region lib/types/client/ComposerAttachments.js + /** Draft image previews, pending-file cards, drop target, and original-image preview. */ +- function ComposerAttachments({ attachments, canAcceptDrop, onAddFiles, onRemoveAttachment, uploads, onRetryFile, dropLimits, t }) { ++ function ComposerAttachments({ attachments, canAcceptDrop, onAddFiles, onRemoveAttachment, uploads, onRetryFile, onOpenUploadedFile, dropLimits, t }) { + const [preview, setPreview] = (0, react.useState)(null); + const [dragActive, setDragActive] = (0, react.useState)(false); + const dragDepth = (0, react.useRef)(0); +@@ -691,7 +694,10 @@ window.__ModuleLoader__.load({ + }, + onRetry: () => { + onRetryFile(attachment.id); +- } ++ }, ++ onOpen: upload?.status === "ready" && upload.file !== void 0 && onOpenUploadedFile !== void 0 ? () => { ++ onOpenUploadedFile(upload.file.attachmentId); ++ } : void 0 + }); + } + return (0, react_jsx_runtime.jsxs)("div", { diff --git a/patches/@deepseek-ai+dsh-client-ui-chat+0.1.5-rc.2.patch b/patches/@deepseek-ai+dsh-client-ui-chat+0.1.5-rc.2.patch index 4d6c5f044..e9b209996 100644 --- a/patches/@deepseek-ai+dsh-client-ui-chat+0.1.5-rc.2.patch +++ b/patches/@deepseek-ai+dsh-client-ui-chat+0.1.5-rc.2.patch @@ -1,7 +1,48 @@ diff --git a/node_modules/@deepseek-ai/dsh-client-ui-chat/lib/client.js b/node_modules/@deepseek-ai/dsh-client-ui-chat/lib/client.js +index 306645f..50e44ad 100644 --- a/node_modules/@deepseek-ai/dsh-client-ui-chat/lib/client.js +++ b/node_modules/@deepseek-ai/dsh-client-ui-chat/lib/client.js -@@ -1124,7 +1124,10 @@ +@@ -66,6 +66,22 @@ window.__ModuleLoader__.load({ + if (root !== "" && normalized.startsWith(`${root}/`)) return sessionFileAddress(sessionId, normalized.slice(root.length + 1)); + return sessionFileAddress(sessionId, normalized); + } ++ function hostPathForOpen(cwd, path) { ++ const normalized = path.replace(/\\/g, "/"); ++ if (isAbsoluteWorkspacePath(normalized)) return path; ++ const root = cwd === void 0 ? "" : cwd.replace(/\\/g, "/").replace(/\/+$/, ""); ++ if (root === "") return path; ++ return `${root}/${normalized.replace(/^\/+/, "")}`; ++ } ++ async function openResolvedHostFile(ctx, sessionId, cwd, hostPath, options) { ++ const address = fileAddressFor(sessionId, cwd, hostPath); ++ if (ctx.sidebarRightTabs.candidates(address).length === 0) { ++ await ctx.remote.session.openWorkspacePath({ path: hostPath }); ++ return; ++ } ++ if (options?.line === void 0) ctx.sidebarRight.openResource(address); ++ else ctx.sidebarRight.openResource(address, { params: { line: options.line } }); ++ } + //#endregion + //#region lib/types/client/contract/snapshot.js + const EMPTY_LIST$1 = []; +@@ -152,7 +168,7 @@ window.__ModuleLoader__.load({ + } + //#endregion + //#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-chat/src/client/chat/MessageItem.module.css.mjs +- const css$15 = ".Sixlwa_userRow{flex-direction:column;align-items:flex-end;gap:6px;display:flex}.Sixlwa_userStack{min-width:0;max-width:min(calc(var(--dsh-chat-content-width,748px) * .702), 82%);flex-direction:column;align-items:flex-end;gap:8px;display:flex}.Sixlwa_bubble{background:var(--dsw-specific-bubble);max-width:100%;font-size:var(--dsh-content-font-size,14px);line-height:calc(22px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-primary);white-space:pre-wrap;word-break:break-word;border-radius:22px;padding:10px 16px}.Sixlwa_referenceSummary{color:var(--dsw-alias-label-tertiary);font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(18px + var(--dsh-content-font-delta-secondary,0px))}.Sixlwa_contextRow,.Sixlwa_compactionRow{padding:2px 0}.Sixlwa_compactionButton{width:100%;height:calc(24px + var(--dsh-content-font-delta,0px));min-width:0;color:inherit;font:inherit;text-align:left;background:0 0;border:none;border-radius:6px;align-items:center;padding:0;display:flex}.Sixlwa_compactionButton:not(:disabled){cursor:pointer}.Sixlwa_compactionButton:not(:disabled):hover{background:var(--dsw-alias-interactive-bg-hover)}.Sixlwa_compactionLeading{width:calc(16px + var(--dsh-content-font-delta,0px));height:calc(16px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-secondary);flex:none;place-items:center;margin-right:6px;display:inline-grid}.Sixlwa_compactionLeading svg{width:calc(14px + var(--dsh-content-font-delta,0px));height:calc(14px + var(--dsh-content-font-delta,0px))}.Sixlwa_compactionContextIcon,.Sixlwa_compactionDisclosureIcon{grid-area:1/1;justify-content:center;align-items:center;display:inline-flex}.Sixlwa_compactionDisclosureIcon,.Sixlwa_compactionButton:not(:disabled):hover .Sixlwa_compactionContextIcon,.Sixlwa_compactionButton:not(:disabled):focus-visible .Sixlwa_compactionContextIcon{opacity:0}.Sixlwa_compactionButton:not(:disabled):hover .Sixlwa_compactionDisclosureIcon,.Sixlwa_compactionButton:not(:disabled):focus-visible .Sixlwa_compactionDisclosureIcon{opacity:1}.Sixlwa_compactionTitle{font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(24px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-primary-dimmed);flex:none}.Sixlwa_compactionSep{background:var(--dsw-alias-label-caption);border-radius:1px;flex:none;width:2px;height:2px;margin:0 8px}.Sixlwa_compactionSummary{min-width:0;color:var(--dsw-alias-label-tertiary);font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(24px + var(--dsh-content-font-delta,0px));text-overflow:ellipsis;white-space:nowrap;flex:auto;overflow:hidden}.Sixlwa_compactionBody{padding:4px 0 4px calc(22px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-tertiary);font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(24px + var(--dsh-content-font-delta,0px))}.Sixlwa_retryRow{color:var(--dsw-alias-label-tertiary);font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(20px + var(--dsh-content-font-delta-secondary,0px))}.Sixlwa_retrySummary{width:fit-content;color:inherit;cursor:pointer;user-select:none;border-radius:3px;align-items:center;gap:7px;padding:2px 0;list-style:none;display:inline-flex}.Sixlwa_retrySummary::-webkit-details-marker{display:none}.Sixlwa_retrySummary:after{content:\"\";opacity:.8;border-bottom:1.5px solid;border-right:1.5px solid;width:6px;height:6px;transition:transform .12s;transform:rotate(-45deg)}.Sixlwa_retrySummary:hover{color:var(--dsw-alias-label-secondary)}.Sixlwa_retrySummary:focus-visible{outline:1.5px solid var(--dsw-alias-button-info-fill);outline-offset:2px}.Sixlwa_retryText{color:inherit}.Sixlwa_retryRow[data-active] .Sixlwa_retryText{background:linear-gradient(90deg, var(--dsw-alias-label-tertiary) 0%, var(--dsw-alias-label-tertiary) 40%, var(--dsw-alias-label-secondary) 50%, var(--dsw-alias-label-tertiary) 60%, var(--dsw-alias-label-tertiary) 100%);color:#0000;background-position:100%;background-size:200% 100%;background-clip:text;animation:1.6s ease-in-out infinite Sixlwa_retry-shimmer}.Sixlwa_retryRow[open] .Sixlwa_retrySummary:after{transform:rotate(45deg)}.Sixlwa_retryDetails{overflow-wrap:anywhere;font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(18px + var(--dsh-content-font-delta-secondary,0px));gap:2px;margin-top:3px;padding-left:14px;display:grid}.Sixlwa_retryDetailLabel{color:var(--dsw-alias-label-secondary)}.Sixlwa_turnErrorRow{font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(20px + var(--dsh-content-font-delta-secondary,0px));grid-template-columns:10px minmax(0,1fr) auto;align-items:start;gap:8px;padding:2px 0;display:grid}.Sixlwa_turnErrorDot{margin-top:5px}.Sixlwa_turnErrorCopy{overflow-wrap:anywhere;min-width:0}.Sixlwa_turnErrorTitle{color:var(--dsw-alias-state-error-primary);margin-right:6px;font-weight:600}.Sixlwa_turnErrorMessage{color:var(--dsw-alias-label-secondary)}.Sixlwa_turnErrorCode{color:var(--dsw-alias-label-tertiary);font:var(--dsw-font-markdown-code-block-small)}.Sixlwa_maxTokensTitle{color:var(--dsw-alias-state-warn-primary);margin-right:6px;font-weight:600}@keyframes Sixlwa_retry-shimmer{0%{background-position:100%}to{background-position:0}}@media (prefers-reduced-motion:reduce){.Sixlwa_retryRow[data-active] .Sixlwa_retryText{color:inherit;background:0 0;animation:none}}.Sixlwa_attachmentRow{flex-wrap:wrap;justify-content:flex-end;gap:8px;max-width:100%;display:flex}.Sixlwa_fileCard{border:.5px solid var(--dsw-alias-border-l2,#0000001f);background:var(--dsw-specific-input-major,transparent);box-sizing:border-box;border-radius:16px;flex:0 0 240px;align-items:center;gap:10px;width:240px;min-height:64px;padding:8px 12px;display:inline-flex}.Sixlwa_fileIcon{flex:none;width:28px;height:28px}.Sixlwa_fileContent{flex-direction:column;flex:1;min-width:0;display:flex}.Sixlwa_fileName{white-space:nowrap;text-overflow:ellipsis;color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500;line-height:22px;overflow:hidden}.Sixlwa_fileMeta{white-space:nowrap;text-overflow:ellipsis;color:var(--dsw-alias-label-tertiary,#00000073);font-size:12px;line-height:15px;overflow:hidden}"; ++ const css$15 = ".Sixlwa_userRow{flex-direction:column;align-items:flex-end;gap:6px;display:flex}.Sixlwa_userStack{min-width:0;max-width:min(calc(var(--dsh-chat-content-width,748px) * .702), 82%);flex-direction:column;align-items:flex-end;gap:8px;display:flex}.Sixlwa_bubble{background:var(--dsw-specific-bubble);max-width:100%;font-size:var(--dsh-content-font-size,14px);line-height:calc(22px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-primary);white-space:pre-wrap;word-break:break-word;border-radius:22px;padding:10px 16px}.Sixlwa_referenceSummary{color:var(--dsw-alias-label-tertiary);font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(18px + var(--dsh-content-font-delta-secondary,0px))}.Sixlwa_contextRow,.Sixlwa_compactionRow{padding:2px 0}.Sixlwa_compactionButton{width:100%;height:calc(24px + var(--dsh-content-font-delta,0px));min-width:0;color:inherit;font:inherit;text-align:left;background:0 0;border:none;border-radius:6px;align-items:center;padding:0;display:flex}.Sixlwa_compactionButton:not(:disabled){cursor:pointer}.Sixlwa_compactionButton:not(:disabled):hover{background:var(--dsw-alias-interactive-bg-hover)}.Sixlwa_compactionLeading{width:calc(16px + var(--dsh-content-font-delta,0px));height:calc(16px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-secondary);flex:none;place-items:center;margin-right:6px;display:inline-grid}.Sixlwa_compactionLeading svg{width:calc(14px + var(--dsh-content-font-delta,0px));height:calc(14px + var(--dsh-content-font-delta,0px))}.Sixlwa_compactionContextIcon,.Sixlwa_compactionDisclosureIcon{grid-area:1/1;justify-content:center;align-items:center;display:inline-flex}.Sixlwa_compactionDisclosureIcon,.Sixlwa_compactionButton:not(:disabled):hover .Sixlwa_compactionContextIcon,.Sixlwa_compactionButton:not(:disabled):focus-visible .Sixlwa_compactionContextIcon{opacity:0}.Sixlwa_compactionButton:not(:disabled):hover .Sixlwa_compactionDisclosureIcon,.Sixlwa_compactionButton:not(:disabled):focus-visible .Sixlwa_compactionDisclosureIcon{opacity:1}.Sixlwa_compactionTitle{font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(24px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-primary-dimmed);flex:none}.Sixlwa_compactionSep{background:var(--dsw-alias-label-caption);border-radius:1px;flex:none;width:2px;height:2px;margin:0 8px}.Sixlwa_compactionSummary{min-width:0;color:var(--dsw-alias-label-tertiary);font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(24px + var(--dsh-content-font-delta,0px));text-overflow:ellipsis;white-space:nowrap;flex:auto;overflow:hidden}.Sixlwa_compactionBody{padding:4px 0 4px calc(22px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-tertiary);font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(24px + var(--dsh-content-font-delta,0px))}.Sixlwa_retryRow{color:var(--dsw-alias-label-tertiary);font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(20px + var(--dsh-content-font-delta-secondary,0px))}.Sixlwa_retrySummary{width:fit-content;color:inherit;cursor:pointer;user-select:none;border-radius:3px;align-items:center;gap:7px;padding:2px 0;list-style:none;display:inline-flex}.Sixlwa_retrySummary::-webkit-details-marker{display:none}.Sixlwa_retrySummary:after{content:\"\";opacity:.8;border-bottom:1.5px solid;border-right:1.5px solid;width:6px;height:6px;transition:transform .12s;transform:rotate(-45deg)}.Sixlwa_retrySummary:hover{color:var(--dsw-alias-label-secondary)}.Sixlwa_retrySummary:focus-visible{outline:1.5px solid var(--dsw-alias-button-info-fill);outline-offset:2px}.Sixlwa_retryText{color:inherit}.Sixlwa_retryRow[data-active] .Sixlwa_retryText{background:linear-gradient(90deg, var(--dsw-alias-label-tertiary) 0%, var(--dsw-alias-label-tertiary) 40%, var(--dsw-alias-label-secondary) 50%, var(--dsw-alias-label-tertiary) 60%, var(--dsw-alias-label-tertiary) 100%);color:#0000;background-position:100%;background-size:200% 100%;background-clip:text;animation:1.6s ease-in-out infinite Sixlwa_retry-shimmer}.Sixlwa_retryRow[open] .Sixlwa_retrySummary:after{transform:rotate(45deg)}.Sixlwa_retryDetails{overflow-wrap:anywhere;font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(18px + var(--dsh-content-font-delta-secondary,0px));gap:2px;margin-top:3px;padding-left:14px;display:grid}.Sixlwa_retryDetailLabel{color:var(--dsw-alias-label-secondary)}.Sixlwa_turnErrorRow{font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(20px + var(--dsh-content-font-delta-secondary,0px));grid-template-columns:10px minmax(0,1fr) auto;align-items:start;gap:8px;padding:2px 0;display:grid}.Sixlwa_turnErrorDot{margin-top:5px}.Sixlwa_turnErrorCopy{overflow-wrap:anywhere;min-width:0}.Sixlwa_turnErrorTitle{color:var(--dsw-alias-state-error-primary);margin-right:6px;font-weight:600}.Sixlwa_turnErrorMessage{color:var(--dsw-alias-label-secondary)}.Sixlwa_turnErrorCode{color:var(--dsw-alias-label-tertiary);font:var(--dsw-font-markdown-code-block-small)}.Sixlwa_maxTokensTitle{color:var(--dsw-alias-state-warn-primary);margin-right:6px;font-weight:600}@keyframes Sixlwa_retry-shimmer{0%{background-position:100%}to{background-position:0}}@media (prefers-reduced-motion:reduce){.Sixlwa_retryRow[data-active] .Sixlwa_retryText{color:inherit;background:0 0;animation:none}}.Sixlwa_attachmentRow{flex-wrap:wrap;justify-content:flex-end;gap:8px;max-width:100%;display:flex}.Sixlwa_fileCard{border:.5px solid var(--dsw-alias-border-l2,#0000001f);background:var(--dsw-specific-input-major,transparent);box-sizing:border-box;border-radius:16px;flex:0 0 240px;align-items:center;gap:10px;width:240px;min-height:64px;padding:8px 12px;display:inline-flex}.Sixlwa_fileCardButton{color:inherit;font:inherit;cursor:pointer;appearance:none}.Sixlwa_fileIcon{flex:none;width:28px;height:28px}.Sixlwa_fileContent{flex-direction:column;flex:1;min-width:0;display:flex}.Sixlwa_fileName{white-space:nowrap;text-overflow:ellipsis;color:var(--dsw-alias-label-primary);font-size:14px;font-weight:500;line-height:22px;overflow:hidden}.Sixlwa_fileMeta{white-space:nowrap;text-overflow:ellipsis;color:var(--dsw-alias-label-tertiary,#00000073);font-size:12px;line-height:15px;overflow:hidden}"; + const tagId$15 = "@deepseek-ai/dsh-client-ui-chat/MessageItem.module.css"; + if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$15) + "]") === null) { + const tag = document.createElement("style"); +@@ -175,6 +191,7 @@ window.__ModuleLoader__.load({ + "compactionTitle": "Sixlwa_compactionTitle", + "contextRow": "Sixlwa_contextRow", + "fileCard": "Sixlwa_fileCard", ++ "fileCardButton": "Sixlwa_fileCardButton", + "fileContent": "Sixlwa_fileContent", + "fileIcon": "Sixlwa_fileIcon", + "fileMeta": "Sixlwa_fileMeta", +@@ -1124,7 +1141,10 @@ window.__ModuleLoader__.load({ return Math.max(1, Math.ceil(milliseconds / 1e3)); } function failureMessage(message, code, t) { @@ -13,7 +54,137 @@ diff --git a/node_modules/@deepseek-ai/dsh-client-ui-chat/lib/client.js b/node_m } function ModelRetryItem({ node, active, t }) { const deadline = (0, react.useMemo)(() => Date.now() + node.delayMs, [node.delayMs, node.seq]); -@@ -2694,6 +2697,8 @@ +@@ -1230,7 +1250,7 @@ window.__ModuleLoader__.load({ + }); + } + /** Right-aligned bubble shared by user and steering rows. */ +- function UserStyleBubble({ content, renderMessageImages, actions, pending = false, echo = false, referenceLabels = [], skillNames = [], previewAttachments, t }) { ++ function UserStyleBubble({ content, renderMessageImages, actions, pending = false, echo = false, referenceLabels = [], skillNames = [], previewAttachments, onOpenUploadedFile, t }) { + const { text, attachments: contentAttachments, rest } = contentParts(content); + const attachments = previewAttachments ?? contentAttachments; + const compactImages = attachments.length > 1; +@@ -1250,9 +1270,13 @@ window.__ModuleLoader__.load({ + images: [attachment.image], + align: "end", + compact: compactImages +- }) }, `image:${index}`) : (0, react_jsx_runtime.jsxs)("span", { +- className: MessageItem_module_css_default.fileCard, ++ }) }, `image:${index}`) : (0, react_jsx_runtime.jsxs)(onOpenUploadedFile !== void 0 && attachment.file.attachmentId !== void 0 ? "button" : "span", { ++ type: onOpenUploadedFile !== void 0 && attachment.file.attachmentId !== void 0 ? "button" : void 0, ++ className: onOpenUploadedFile !== void 0 && attachment.file.attachmentId !== void 0 ? `${MessageItem_module_css_default.fileCard} ${MessageItem_module_css_default.fileCardButton}` : MessageItem_module_css_default.fileCard, + title: attachment.file.name, ++ onClick: onOpenUploadedFile === void 0 || attachment.file.attachmentId === void 0 ? void 0 : () => { ++ onOpenUploadedFile(attachment.file.attachmentId); ++ }, + children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.FileTypeIcon, { + path: attachment.file.name, + className: MessageItem_module_css_default.fileIcon +@@ -1312,7 +1336,7 @@ window.__ModuleLoader__.load({ + * @param props - the session snapshot's pending submission and render seats. + * @returns the echoed user bubble. + */ +- function PendingSubmissionBubble({ submission, renderMessageImages, t }) { ++ function PendingSubmissionBubble({ submission, renderMessageImages, onOpenUploadedFile, t }) { + return (0, react_jsx_runtime.jsx)(UserStyleBubble, { + content: (0, react.useMemo)(() => submission.text === "" ? [] : [{ + type: "text", +@@ -1331,6 +1355,7 @@ window.__ModuleLoader__.load({ + file: attachment.value + }), [submission.attachments]), + renderMessageImages, ++ onOpenUploadedFile, + pending: submission.placement === "steering", + echo: true, + t, +@@ -1344,11 +1369,12 @@ window.__ModuleLoader__.load({ + }); + } + /** User and admitted-steering keyed Chat renderer. */ +- const UserMessageNodeView = (0, react.memo)(function UserMessageNodeView({ node, renderMessageImages, t }) { ++ const UserMessageNodeView = (0, react.memo)(function UserMessageNodeView({ node, renderMessageImages, onOpenUploadedFile, t }) { + const data = node.data; + return (0, react_jsx_runtime.jsx)(UserStyleBubble, { + content: data.content, + renderMessageImages, ++ onOpenUploadedFile, + ...data.referenceLabels === void 0 ? {} : { referenceLabels: data.referenceLabels }, + ...data.skillNames === void 0 ? {} : { skillNames: data.skillNames }, + t, +@@ -1541,7 +1567,7 @@ window.__ModuleLoader__.load({ + return location?.kind === "turn" || location?.kind === "step" ? location.turn.turn : void 0; + } + /** Subscribe, apply Turn-process visibility, and dispatch one stable Context key. */ +- const ChatNodeSeat = (0, react.memo)(function ChatNodeSeat({ nodeKey, useChatNode, useChatNodeProcess, historyIncomplete, compactTranscript, cwd, openFile, inspectCall, forkAt, loadImage, renderMessageImages, fileMentions, useStore, actions, renderSlot, t }) { ++ const ChatNodeSeat = (0, react.memo)(function ChatNodeSeat({ nodeKey, useChatNode, useChatNodeProcess, historyIncomplete, compactTranscript, cwd, openFile, onOpenUploadedFile, inspectCall, forkAt, loadImage, renderMessageImages, fileMentions, useStore, actions, renderSlot, t }) { + const node = useChatNode(nodeKey); + const routedNode = node; + const turn = turnOf(routedNode); +@@ -1577,6 +1603,7 @@ window.__ModuleLoader__.load({ + const owner = (0, react.useMemo)(() => node === void 0 ? null : { + cwd, + openFile, ++ onOpenUploadedFile, + inspectCall, + forkAt, + loadImage, +@@ -1587,6 +1614,7 @@ window.__ModuleLoader__.load({ + node, + cwd, + openFile, ++ onOpenUploadedFile, + inspectCall, + forkAt, + loadImage, +@@ -2069,7 +2097,7 @@ window.__ModuleLoader__.load({ + * The chat view slot entry: pure component over the composed props; each + * ordered business Node crosses the keyed renderer seat. + */ +- function ChatView({ useSession, useChat, useChatNode, useChatNodeProcess, useSessions, useStore, actions, renderSlot, sessionId, openFile, loadOlder, loadThrough, loadImage, openView, chatScroll, forkAt, fileMentions, useTranscriptView, useProjection, t }) { ++ function ChatView({ useSession, useChat, useChatNode, useChatNodeProcess, useSessions, useStore, actions, renderSlot, sessionId, openFile, openUploadedFile, loadOlder, loadThrough, loadImage, openView, chatScroll, forkAt, fileMentions, useTranscriptView, useProjection, t }) { + const order = useChat((s) => s.order); + const nodeStore = useChat((s) => s.nodes); + const turnNavigationItems = useChat((s) => s.navigation.items()); +@@ -2106,6 +2134,23 @@ window.__ModuleLoader__.load({ + setFileOpenBusy(false); + }); + }, [openFile, t]); ++ const requestOpenUploadedFile = (0, react.useCallback)((attachmentId) => { ++ if (openUploadedFile === void 0) return; ++ const id = ++fileOpenRequest.current; ++ setFileOpenBusy(true); ++ openUploadedFile(attachmentId).then(() => { ++ if (id !== fileOpenRequest.current) return; ++ setFileOpenError(null); ++ setFileOpenBusy(false); ++ }, (error) => { ++ if (id !== fileOpenRequest.current) return; ++ setFileOpenError({ ++ path: String(attachmentId), ++ message: openFailureMessage(error, t("fileOpen.unknown")) ++ }); ++ setFileOpenBusy(false); ++ }); ++ }, [openUploadedFile, t]); + const closeFileOpenError = (0, react.useCallback)(() => { + fileOpenRequest.current += 1; + setFileOpenError(null); +@@ -2542,6 +2587,7 @@ window.__ModuleLoader__.load({ + actions, + cwd, + openFile: requestOpenFile, ++ onOpenUploadedFile: requestOpenUploadedFile, + inspectCall, + forkAt, + loadImage, +@@ -2562,6 +2608,7 @@ window.__ModuleLoader__.load({ + visibleSubmissions.map((submission) => (0, react_jsx_runtime.jsx)(PendingSubmissionBubble, { + submission, + renderMessageImages, ++ onOpenUploadedFile: requestOpenUploadedFile, + t + }, submission.requestId)) + ] +@@ -2694,6 +2741,8 @@ window.__ModuleLoader__.load({ "message.retry.delay": "重试延迟:", "message.retry.failure": "失败原因:", "message.failure.auth": "API 密钥无效", @@ -22,7 +193,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-client-ui-chat/lib/client.js b/node_m "message.turnError": "本轮运行失败", "message.maxTokens": "已达到输出 token 上限", "message.maxTokens.hint": "回答被截断,已有输出保留在对话中。发送“继续”可让模型接着输出。", -@@ -2800,6 +2805,8 @@ +@@ -2800,6 +2849,8 @@ window.__ModuleLoader__.load({ "message.retry.delay": "Retry delay: ", "message.retry.failure": "Failure reason: ", "message.failure.auth": "API key is invalid", @@ -31,3 +202,50 @@ diff --git a/node_modules/@deepseek-ai/dsh-client-ui-chat/lib/client.js b/node_m "message.turnError": "This turn failed", "message.maxTokens": "Output token limit reached", "message.maxTokens.hint": "The reply was cut off; earlier output is preserved in the conversation. Send \"continue\" to let the model resume.", +@@ -8239,7 +8290,8 @@ window.__ModuleLoader__.load({ + "settingsScope", + "remote", + "remote.session", +- "sidebarRight" ++ "sidebarRight", ++ "sidebarRightTabs" + ]; + /** + * Mount all Chat-owned contributions. +@@ -8273,6 +8325,14 @@ window.__ModuleLoader__.load({ + const chatStore = createChatStore(); + const chatScrollPositions = /* @__PURE__ */ new Map(); + const transcriptView = new TranscriptViewPolicy(ctx.settingsScope.bind({ namespace: CHAT_SETTINGS_NAMESPACE })); ++ ctx.provide("openUploadedAttachment", { open: async (sessionId, attachmentId) => { ++ const binding = ctx.sessions.binding(sessionId); ++ if (binding === void 0) throw new Error(`ui-chat: unknown session "${sessionId}"`); ++ const result = await binding.session.readFileHostPath(attachmentId); ++ if (!result.ok) throw new Error(`${result.error.code}: ${result.error.message}`); ++ const cwd = ctx.sessions.list.getSnapshot().byId[sessionId]?.cwd; ++ await openResolvedHostFile(ctx, sessionId, cwd, result.value.path); ++ } }); + ctx.slots.inject("settings.general.item", () => ctx.slots.register({ + name: "settings.general.item", + id: "transcript-view", +@@ -8318,10 +8378,17 @@ window.__ModuleLoader__.load({ + fileMentions: (owner) => ctx.get("chatFileMentions")?.forClosing(owner, sessionId), + openFile: async (path, options) => { + const cwd = ctx.sessions.list.getSnapshot().byId[sessionId]?.cwd; +- const url = fileAddressFor(sessionId, cwd, path); +- if (options?.line === void 0) ctx.sidebarRight.openResource(url); +- else ctx.sidebarRight.openResource(url, { params: { line: options.line } }); +- await Promise.resolve(); ++ const byName = await session.readFileHostPath({ name: path }); ++ if (byName.ok) { ++ await openResolvedHostFile(ctx, sessionId, cwd, byName.value.path); ++ return; ++ } ++ await openResolvedHostFile(ctx, sessionId, cwd, hostPathForOpen(cwd, path), options); ++ }, ++ openUploadedFile: async (attachmentId) => { ++ const opener = ctx.get("openUploadedAttachment"); ++ if (opener === void 0) throw new Error("ui-chat: openUploadedAttachment is unavailable"); ++ await opener.open(sessionId, attachmentId); + }, + loadOlder: () => { + session.loadOlder(); diff --git a/patches/@deepseek-ai+dsh-client-ui-conversation+0.1.5-rc.2.patch b/patches/@deepseek-ai+dsh-client-ui-conversation+0.1.5-rc.2.patch index 6c6d83aa7..3cf1aa31b 100644 --- a/patches/@deepseek-ai+dsh-client-ui-conversation+0.1.5-rc.2.patch +++ b/patches/@deepseek-ai+dsh-client-ui-conversation+0.1.5-rc.2.patch @@ -1,7 +1,8 @@ diff --git a/node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/client.js b/node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/client.js +index 1b7f134..794d405 100644 --- a/node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/client.js +++ b/node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/client.js -@@ -14650,12 +14650,13 @@ +@@ -14650,12 +14650,13 @@ window.__ModuleLoader__.load({ //#endregion //#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-conversation/src/client/skeleton/ConversationRoot.module.css.mjs const css$4 = ".wSkVaW_root{background:var(--dsw-alias-bg-base);--dsh-chat-content-width:var(--dsh-chat-user-width,clamp(680px, calc(var(--dsh-conversation-column-width,0px) * .64), 920px));--dsh-composer-card-max-width:calc(var(--dsh-chat-content-width) + 32px);--dsh-composer-side-clearance:16px;--dsh-composer-dock-inset:8px;flex-direction:column;min-width:0;height:100%;display:flex;position:relative}.wSkVaW_header{box-sizing:border-box;border-bottom:.5px solid var(--dsw-alias-border-l3);flex:none;min-height:76px;padding:10px 28px 0 20px}.wSkVaW_headerHidden{display:none}.wSkVaW_titleRow{align-items:center;gap:0;min-height:30px;display:flex}.wSkVaW_titleCluster{flex:1;align-items:center;gap:10px;min-width:0;display:flex}.wSkVaW_crumbs{white-space:nowrap;align-items:center;gap:4px;min-width:0;display:flex;overflow:hidden}.wSkVaW_crumbSeg{align-items:center;gap:4px;min-width:0;display:inline-flex}.wSkVaW_crumbSep{color:var(--dsw-alias-label-caption);font-size:14px;line-height:20px}.wSkVaW_crumb{max-width:220px;color:var(--dsw-alias-label-tertiary);text-overflow:ellipsis;white-space:nowrap;cursor:pointer;background:0 0;border:none;border-radius:12px;padding:4px 8px;font-size:14px;line-height:20px;overflow:hidden}.wSkVaW_crumbSubagent{font-size:12px;line-height:18px}.wSkVaW_crumb:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.wSkVaW_crumbCurrent{color:var(--dsw-alias-label-primary);cursor:default;font-weight:500}.wSkVaW_headerActions{flex:none;align-items:center;gap:8px;display:flex}.wSkVaW_headerUtilities{flex:none;align-items:center;gap:8px;margin-left:20px;display:flex}.wSkVaW_headerUtilities:empty{display:none}.wSkVaW_headerCorner{flex:none;align-items:center;margin-left:8px;margin-right:-16px;display:flex}.wSkVaW_headerCorner:empty{display:none}.wSkVaW_tabs{z-index:1;gap:36px;margin-top:10px;padding-left:8px;display:flex;position:relative}.wSkVaW_tab{color:var(--dsw-alias-label-tertiary);cursor:pointer;background:0 0;border:none;padding:0 0 9px;font-size:13px;font-weight:500;line-height:16px;position:relative}.wSkVaW_tab:after{content:\"\";background:0 0;border-radius:2px;height:2px;position:absolute;bottom:-1px;left:0;right:0}.wSkVaW_tabActive{color:var(--dsw-alias-state-business-primary)}.wSkVaW_tabActive:after{background:var(--dsw-alias-state-business-primary)}.wSkVaW_viewArea{flex-direction:column;flex:1;min-height:0;display:flex}.wSkVaW_widthHandle{z-index:8;width:min(40px, calc((100% - var(--dsh-chat-content-width)) / 2 - 24px - 24px));cursor:col-resize;position:absolute;top:0;bottom:0}.wSkVaW_widthHandle[data-side=left]{right:calc(50% + var(--dsh-chat-content-width) / 2 + 24px)}.wSkVaW_widthHandle[data-side=right]{left:calc(50% + var(--dsh-chat-content-width) / 2 + 24px)}.wSkVaW_widthHandle:after{content:\"\";background:linear-gradient(to bottom, transparent calc(var(--dsh-width-handle-pointer-y,50%) - 52px), var(--dsw-alias-scrollbar-hover-l1) calc(var(--dsh-width-handle-pointer-y,50%) - 12px), var(--dsw-alias-scrollbar-hover-l1) calc(var(--dsh-width-handle-pointer-y,50%) + 12px), transparent calc(var(--dsh-width-handle-pointer-y,50%) + 52px));opacity:0;pointer-events:none;border-radius:3px;width:3px;position:absolute;top:0;bottom:0}.wSkVaW_widthHandle[data-side=left]:after{right:16px}.wSkVaW_widthHandle[data-side=right]:after{left:16px}.wSkVaW_widthHandle:hover:after,.wSkVaW_widthHandle[data-dragging]:after{opacity:1}.wSkVaW_root:has([data-conversation-composer-overlay]) .wSkVaW_widthHandle{display:none}.wSkVaW_composerStack{--dsh-composer-stack-gap:6px;gap:var(--dsh-composer-stack-gap);flex-direction:column;display:flex}.wSkVaW_composerSeat{--dsh-composer-text-max-height:336px;flex-direction:column;flex:none;display:flex}.wSkVaW_root[data-phase=active]{overflow:hidden}.wSkVaW_root[data-phase=active] .wSkVaW_header{flex:none}.wSkVaW_body{flex-direction:column;flex:1;min-height:0;display:flex;position:relative}.wSkVaW_scrollBody{scrollbar-gutter:stable;flex-direction:column;flex:1;min-height:0;margin-right:2px;display:flex;overflow-y:auto}.wSkVaW_scrollBody::-webkit-scrollbar-track{margin:2px}.wSkVaW_root[data-phase=active] .wSkVaW_viewArea{flex:1 0 auto;min-height:auto}.wSkVaW_root[data-phase=active] .wSkVaW_composerSeat{z-index:7;background:linear-gradient(180deg, color-mix(in srgb, var(--dsw-alias-bg-base) 0%, transparent) 0px, var(--dsw-alias-bg-base) 36px);position:sticky;bottom:0}.wSkVaW_root[data-phase=active] .wSkVaW_composerSeat:has([data-trigger-menu]){z-index:9}.wSkVaW_scrollBody:has([data-conversation-composer-overlay]){scrollbar-gutter:auto;position:relative;overflow:hidden auto}.wSkVaW_scrollBody:has([data-conversation-composer-overlay])>[data-slot=conversation\\.session]>.wSkVaW_viewArea{flex:1 1 0;min-height:0;overflow:hidden}.wSkVaW_scrollBody:has([data-conversation-composer-overlay])>.wSkVaW_composerSeat{right:var(--dsh-scrollbar-width);position:absolute;bottom:0;left:0}.wSkVaW_composerHero{width:min(calc(var(--dsh-composer-card-max-width) + 2 * var(--dsh-composer-side-clearance)), 100%);z-index:1;align-self:center;gap:8px;padding-bottom:32px}.wSkVaW_heroWorkspaceRow{align-items:center;gap:2px;min-width:0;margin-top:4px;padding:0 16px 0 20px;display:flex}.wSkVaW_root[data-phase=hero] .wSkVaW_scrollBody{justify-content:center;overflow-y:auto}.wSkVaW_root[data-phase=settling] .wSkVaW_composerSeat{visibility:hidden}"; @@ -16,7 +17,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/client.js document.head.appendChild(tag); } var ConversationRoot_module_css_default = { -@@ -14674,6 +14675,7 @@ +@@ -14674,6 +14675,7 @@ window.__ModuleLoader__.load({ "headerCorner": "wSkVaW_headerCorner", "headerHidden": "wSkVaW_headerHidden", "headerUtilities": "wSkVaW_headerUtilities", @@ -24,7 +25,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/client.js "heroWorkspaceRow": "wSkVaW_heroWorkspaceRow", "root": "wSkVaW_root", "scrollBody": "wSkVaW_scrollBody", -@@ -14898,12 +14900,17 @@ +@@ -14898,12 +14900,17 @@ window.__ModuleLoader__.load({ setPickerOpen(false); } }), @@ -43,7 +44,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/client.js ...inert ? { disabled: true, placeholder: t("placeholder.workspace"), -@@ -15755,12 +15762,13 @@ +@@ -15755,12 +15762,13 @@ window.__ModuleLoader__.load({ //#endregion //#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-conversation/src/client/skeleton/InputBar.module.css.mjs const css$1 = ".uV2eYG_root{padding:0 var(--dsh-composer-side-clearance) 8px;flex-direction:column;align-items:center;display:flex}.uV2eYG_root:has([data-composer-stats]){padding-bottom:4px}.uV2eYG_hero{padding:0 var(--dsh-composer-side-clearance)}.uV2eYG_notice{width:100%;max-width:var(--dsh-composer-card-max-width);background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary);border-radius:8px;margin-bottom:6px;padding:4px 8px;font-size:12px;line-height:18px}.uV2eYG_card{box-sizing:border-box;width:100%;max-width:var(--dsh-composer-card-max-width);--dsw-elevation-stroke-color:var(--dsw-alias-border-l2);background:var(--dsw-specific-input-major);box-shadow:var(--dsw-elevation-soft);font-size:var(--dsh-content-font-size,14px);line-height:calc(24px + var(--dsh-content-font-delta,0px));--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border:0;border-radius:22px;flex-direction:column;gap:12px;padding-top:8px;display:flex;position:relative}.uV2eYG_cardWorkspaceTrigger{--dsw-elevation-stroke-color:transparent;cursor:pointer}.uV2eYG_cardWorkspaceTrigger:after{content:\"\";background:var(--dsw-alias-border-l4);pointer-events:none;border-radius:22px;transition:background-color .1s;position:absolute;inset:-1px;-webkit-mask:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='100%25' height='100%25' fill='none' rx='22' ry='22' stroke='black' stroke-width='2' stroke-dasharray='4 4'/%3E%3C/svg%3E\");mask:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='100%25' height='100%25' fill='none' rx='22' ry='22' stroke='black' stroke-width='2' stroke-dasharray='4 4'/%3E%3C/svg%3E\")}.uV2eYG_cardWorkspaceTrigger :disabled{pointer-events:none}.uV2eYG_cardWorkspaceTrigger:hover:after{background:var(--dsw-alias-state-business-primary)}.uV2eYG_accessory{align-items:center;gap:8px;padding:10px 12px 0;display:flex}.uV2eYG_overlayAnchor{height:0;position:absolute;inset:0 0 auto}.uV2eYG_scroll{max-height:var(--dsh-composer-text-max-height);margin-right:4px;overflow-y:auto}.uV2eYG_scroll::-webkit-scrollbar-track{margin-top:8px}.uV2eYG_grow{position:relative}.uV2eYG_pending{corner-shape:round;background:var(--dsw-alias-state-business-primary);border-radius:50%;width:8px;height:8px;animation:1s ease-in-out infinite alternate uV2eYG_input-pending}@keyframes uV2eYG_input-pending{0%{opacity:.35}to{opacity:1}}.uV2eYG_input{box-sizing:border-box;min-height:36px;font-family:var(--dsw-font-family);font-size:inherit;line-height:inherit;white-space:pre-wrap;word-break:break-word;overflow-wrap:anywhere;color:var(--dsw-alias-label-primary);caret-color:var(--dsw-alias-state-business-primary);outline:none;padding:4px 8px 0 14px}.uV2eYG_input p{margin:0}.uV2eYG_input p:last-child:after{content:var(--dsh-composer-hint);color:var(--dsw-alias-label-caption)}.uV2eYG_placeholder{color:var(--dsw-alias-label-caption);white-space:nowrap;text-overflow:ellipsis;pointer-events:none;user-select:none;position:absolute;inset:4px 8px auto 14px;overflow:hidden}.uV2eYG_inputDisabled{color:var(--dsw-alias-label-tertiary);cursor:not-allowed}.uV2eYG_input[aria-haspopup=menu]{cursor:pointer}.uV2eYG_hero .uV2eYG_input{min-height:52px}.uV2eYG_hero .uV2eYG_placeholder{white-space:normal;-webkit-line-clamp:2;line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box}.uV2eYG_row{flex-wrap:wrap;justify-content:space-between;align-items:center;gap:12px;min-width:0;padding:2px 8px 6px;display:flex;container-type:inline-size}.uV2eYG_tools,.uV2eYG_modes,.uV2eYG_trailing{align-items:center;min-width:0;display:flex}.uV2eYG_tools,.uV2eYG_modes{gap:12px}.uV2eYG_trailing{flex:none;gap:12px;margin-left:auto}.uV2eYG_add{corner-shape:round;background:var(--dsw-specific-selector);width:28px;height:28px;color:var(--dsw-alias-label-primary);cursor:pointer;border:none;border-radius:999px;flex:none;place-items:center;display:grid}.uV2eYG_add:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover-solid)}.uV2eYG_add:disabled{opacity:.5;cursor:default}.uV2eYG_select{max-width:220px;height:28px;color:var(--dsw-alias-label-secondary);white-space:nowrap;cursor:pointer;appearance:none;background-color:#0000;background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M3 4.5L6 7.5L9 4.5' stroke='%2381858C' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\");background-position:right 4px center;background-repeat:no-repeat;background-size:12px 12px;border:none;border-radius:8px;outline:none;padding:0 20px 0 8px;font-size:13px;font-weight:500;line-height:20px}.uV2eYG_select:hover:not(:disabled){background-color:var(--dsw-alias-interactive-bg-hover)}.uV2eYG_select:disabled{opacity:.5;cursor:default}.uV2eYG_primary{corner-shape:round;background:var(--dsw-alias-button-info-fill);color:#fff;cursor:pointer;border:none;border-radius:999px;flex:none;place-items:center;width:34px;height:34px;transition:background-color .1s;display:grid;transform:translateY(-2px)}.uV2eYG_primary:hover:not(:disabled){background:var(--dsw-alias-button-info-hover)}.uV2eYG_primary:disabled{opacity:.4;cursor:default}.uV2eYG_retry{color:inherit;cursor:pointer;background:0 0;border:1px solid;border-radius:4px;margin-left:8px;padding:1px 8px;font-size:12px}"; @@ -58,7 +59,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/client.js document.head.appendChild(tag); } var InputBar_module_css_default = { -@@ -15779,6 +15787,7 @@ +@@ -15779,6 +15787,7 @@ window.__ModuleLoader__.load({ "pending": "uV2eYG_pending", "placeholder": "uV2eYG_placeholder", "primary": "uV2eYG_primary", @@ -66,16 +67,16 @@ diff --git a/node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/client.js "retry": "uV2eYG_retry", "root": "uV2eYG_root", "row": "uV2eYG_row", -@@ -15803,7 +15812,7 @@ +@@ -15803,7 +15812,7 @@ window.__ModuleLoader__.load({ * The no-session state renders the SAME div inert as the Workspace-picker * trigger instead of a parallel tree. */ - const InputBar = (0, react.memo)(function InputBar({ useSession, useInput, inputActions, keyboard, addFiles, removeAttachment, resolveDraftAttachments, retryFileUpload, toggleCommandMenu, stop, command, t, renderSlot, useBusyEnter, useFileUploads, useNotices, useLexicon, useMenuLauncher, useProjection, sessionId, variant, disabled: inert = false, blocked, workspacePickerOpen = false, onRequestWorkspace, placeholder, accessory }) { -+ const InputBar = (0, react.memo)(function InputBar({ useSession, useInput, inputActions, keyboard, addFiles, removeAttachment, resolveDraftAttachments, retryFileUpload, toggleCommandMenu, stop, command, t, renderSlot, useBusyEnter, useFileUploads, useNotices, useLexicon, useMenuLauncher, useProjection, sessionId, variant, disabled: inert = false, blocked, workspacePickerOpen = false, onRequestWorkspace, placeholder, accessory, extensionZone }) { ++ const InputBar = (0, react.memo)(function InputBar({ useSession, useInput, inputActions, keyboard, addFiles, removeAttachment, resolveDraftAttachments, retryFileUpload, toggleCommandMenu, stop, command, t, renderSlot, useBusyEnter, useFileUploads, useNotices, useLexicon, useMenuLauncher, useProjection, sessionId, variant, disabled: inert = false, blocked, workspacePickerOpen = false, onRequestWorkspace, placeholder, accessory, extensionZone, openUploadedFile }) { const input = useInput((s) => s); const notice = useNotices((s) => s); const busyEnter = useBusyEnter((s) => s); -@@ -16075,10 +16084,6 @@ +@@ -16075,10 +16084,6 @@ window.__ModuleLoader__.load({ className: InputBar_module_css_default.overlayAnchor, children: renderSlot("conversation.input.overlay", {}) }), @@ -86,7 +87,13 @@ diff --git a/node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/client.js renderSlot("conversation.input.attachments", { attachments, canAcceptDrop, -@@ -16095,13 +16100,19 @@ +@@ -16090,18 +16095,25 @@ window.__ModuleLoader__.load({ + onRetryFile: (id) => { + retryFileUpload?.(id); + }, ++ onOpenUploadedFile: openUploadedFile, + dropLimits: imageLimits === void 0 ? void 0 : { + count: imageLimits.maxImagesPerMessage, size: imageSizeText(imageLimits.maxImageBytes) } }), @@ -113,7 +120,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/client.js (0, react_jsx_runtime.jsx)(ComposerContentEditable, { editor: workspaceTrigger ? null : editor, editable, -@@ -16123,8 +16134,9 @@ +@@ -16123,8 +16135,9 @@ window.__ModuleLoader__.load({ children: placeholderText }), (0, react_jsx_runtime.jsx)(DecoratorPortals, { editor: workspaceTrigger ? null : editor }) @@ -125,7 +132,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/client.js }), (0, react_jsx_runtime.jsxs)("div", { className: InputBar_module_css_default.row, -@@ -16256,7 +16268,7 @@ +@@ -16256,7 +16269,7 @@ window.__ModuleLoader__.load({ }) ] }), @@ -134,7 +141,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/client.js ] }); }); -@@ -16641,6 +16653,10 @@ +@@ -16641,6 +16654,10 @@ window.__ModuleLoader__.load({ "conversation.hero.agentPreset": { kind: "single", scope: "root" @@ -145,21 +152,42 @@ diff --git a/node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/client.js } }, inject: (sessionId) => ({ -@@ -16720,6 +16736,10 @@ - "conversation.input.attachments": { +@@ -16721,6 +16738,10 @@ window.__ModuleLoader__.load({ kind: "single", scope: "session-maybe" -+ }, + }, + "conversation.input.accessory": { + kind: "list", + scope: "session" - }, ++ }, "conversation.input.overlay": { kind: "list", + scope: "session" +@@ -16753,6 +16774,7 @@ window.__ModuleLoader__.load({ + removeAttachment: void 0, + resolveDraftAttachments: void 0, + retryFileUpload: void 0, ++ openUploadedFile: void 0, + toggleCommandMenu: void 0, + stop: void 0, + command: void 0, +@@ -16787,6 +16809,11 @@ window.__ModuleLoader__.load({ + retryFileUpload: (id) => { + if (sessions.binding(sessionId) !== void 0) conversation.retryFileUpload(sessionId, id); + }, ++ openUploadedFile: async (attachmentId) => { ++ const opener = ctx.get("openUploadedAttachment"); ++ if (opener === void 0) throw new Error("ui-conversation: openUploadedAttachment is unavailable"); ++ await opener.open(sessionId, attachmentId); ++ }, + toggleCommandMenu: inputTriggers === void 0 ? void 0 : (selection) => { + shell.dismissPopup(); + const snapshot = shell.snapshot; diff --git a/node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/types/client/contract/slots.d.ts b/node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/types/client/contract/slots.d.ts +index 4a1daf8..14db6ba 100644 --- a/node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/types/client/contract/slots.d.ts +++ b/node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/types/client/contract/slots.d.ts -@@ -183,6 +183,12 @@ +@@ -183,6 +183,12 @@ declare module '@deepseek-ai/dsh-client-ui-slots' { scope: 'root'; owner: HeroAgentPresetOwnerProps; }; @@ -172,7 +200,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/types/clie /** Full-width entries above the composer card. */ 'conversation.input.dock': { kind: 'list'; -@@ -194,10 +200,17 @@ +@@ -194,10 +200,17 @@ declare module '@deepseek-ai/dsh-client-ui-slots' { kind: 'list'; scope: 'session'; }; @@ -190,7 +218,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/types/clie }; /** Compact controls at the left of the composer tool row. */ 'conversation.input.left': { -@@ -341,8 +354,10 @@ +@@ -341,8 +354,10 @@ export interface ComposerBarOwnerProps { /** Open the Workspace picker from the inert composer surface. */ onRequestWorkspace?: () => void; placeholder?: string; @@ -202,7 +230,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/types/clie } /** Package-private operations injected into the resident composer bar. */ export interface ComposerBarInjected { -@@ -374,7 +389,7 @@ +@@ -374,7 +389,7 @@ export interface InputControlOwnerProps { locked: boolean; } /** Full props of the resident composer bar. */ @@ -211,7 +239,7 @@ diff --git a/node_modules/@deepseek-ai/dsh-client-ui-conversation/lib/types/clie /** Owner values used to elect a composer takeover. */ export interface ComposerChainProps { /** Current Session identity used by temporary business-owned entries. */ -@@ -392,7 +407,7 @@ +@@ -392,7 +407,7 @@ export interface HeroBrandMarkOwnerProps { className?: string | undefined; } /** Full props of the resident optional-Session Conversation shell. */ diff --git a/patches/@deepseek-ai+dsh-client-ui-deliverables+0.1.5-rc.2.patch b/patches/@deepseek-ai+dsh-client-ui-deliverables+0.1.5-rc.2.patch index 3d3db9838..56ea67591 100644 --- a/patches/@deepseek-ai+dsh-client-ui-deliverables+0.1.5-rc.2.patch +++ b/patches/@deepseek-ai+dsh-client-ui-deliverables+0.1.5-rc.2.patch @@ -1,7 +1,8 @@ diff --git a/node_modules/@deepseek-ai/dsh-client-ui-deliverables/lib/client.js b/node_modules/@deepseek-ai/dsh-client-ui-deliverables/lib/client.js +index 88a659e..99c2370 100644 --- a/node_modules/@deepseek-ai/dsh-client-ui-deliverables/lib/client.js +++ b/node_modules/@deepseek-ai/dsh-client-ui-deliverables/lib/client.js -@@ -468,7 +468,7 @@ +@@ -468,7 +468,7 @@ window.__ModuleLoader__.load({ */ function producedFileMentions(paths, openFile, label) { return { resolve(value) { @@ -10,36 +11,41 @@ diff --git a/node_modules/@deepseek-ai/dsh-client-ui-deliverables/lib/client.js if (path === void 0) return void 0; return { open: () => { -@@ -478,6 +478,28 @@ - title: path +@@ -479,6 +479,33 @@ window.__ModuleLoader__.load({ }; } }; -+ } + } + /** + * Resolve Codex-style local path references that were not produced by a tool + * in the current turn. Keep bare identifiers inert: without a slash, an + * absolute-path prefix, or a conventional filename suffix, inline code is + * much more likely to be a command, package, or symbol than a local path. + * -+ * A leading `@` plus a single slash is a scoped package name, and a bare -+ * `local@domain.tld` is an email address; both are far more common in -+ * assistant prose than files with those exact shapes, and neither is -+ * openable. An explicit `./@scope/pkg` still resolves as a path above. ++ * A leading `@` plus a single slash is a scoped package name (optionally ++ * with `@version`), and a bare `local@domain.tld` is an email address; ++ * both are far more common in assistant prose than files with those exact ++ * shapes, and neither is openable. An explicit `./@scope/pkg` or a path ++ * that continues past the package (`@scope/pkg@1.0.0/dist/index.js`) ++ * still resolves as a path above. + */ + function localPathReference(value) { + const candidate = value.trim(); + if (candidate === "" || /[\r\n]/.test(candidate) || /^(?:https?|data|javascript):/i.test(candidate)) return void 0; + const path = candidate.replace(/(?:#L\d+(?:C\d+)?|:\d+(?::\d+)?)$/, ""); + if (/^(?:\/|~[\\/]|\.{1,2}[\\/]|[A-Za-z]:[\\/]|\\\\)/.test(path)) return path; -+ if (/^@[^\\/@\s]+\/[^\\/@\s]+$/.test(path)) return void 0; ++ if (/^@[^\\/@\s]+\/[^\\/@\s]+(?:@[^\\/\s]+)?$/.test(path)) return void 0; + if (/^[^@\s\\/]+@[^@\s\\/]+\.[A-Za-z]{2,}$/.test(path)) return void 0; ++ // Semver / package versions (v0.8.0, 1.2.3-rc.1) look like dotted filenames ++ // but are not openable workspace paths. ++ if (/^v?\d+(?:\.\d+){1,4}(?:-[A-Za-z0-9._-]+)?$/i.test(path)) return void 0; + if (/[\\/]/.test(path) || /[\\/]$/.test(path)) return path; + if (/^[^\\/]+\.[A-Za-z0-9][A-Za-z0-9._-]{0,15}$/.test(path)) return path; + return void 0; - } ++ } /** The single produced path whose basename is exactly `value`, else undefined. */ function onlyPathWithBasename(paths, value) { -@@ -956,7 +978,6 @@ + const matches = paths.filter((path) => basename(path) === value); +@@ -956,7 +983,6 @@ window.__ModuleLoader__.load({ ctx.provide("chatFileMentions", { forClosing(owner, sessionId) { const paths = selectProducedFiles(owner); const presented = presentedForClosing(owner); diff --git a/patches/@deepseek-ai+dsh-client-ui-sidebar-documentpreview+0.1.5-rc.2.patch b/patches/@deepseek-ai+dsh-client-ui-sidebar-documentpreview+0.1.5-rc.2.patch new file mode 100644 index 000000000..55edcb331 --- /dev/null +++ b/patches/@deepseek-ai+dsh-client-ui-sidebar-documentpreview+0.1.5-rc.2.patch @@ -0,0 +1,119 @@ +diff --git a/node_modules/@deepseek-ai/dsh-client-ui-sidebar-documentpreview/lib/client.js b/node_modules/@deepseek-ai/dsh-client-ui-sidebar-documentpreview/lib/client.js +index 6f532d1..c1a2725 100644 +--- a/node_modules/@deepseek-ai/dsh-client-ui-sidebar-documentpreview/lib/client.js ++++ b/node_modules/@deepseek-ai/dsh-client-ui-sidebar-documentpreview/lib/client.js +@@ -1057,6 +1057,26 @@ window.__ModuleLoader__.load({ + default: return t("error.unavailable", { message: failure.message }); + } + } ++ function isAbsoluteHostPath(path) { ++ return path.startsWith("/") || /^[A-Za-z]:[/\\]/.test(path) || path.startsWith("\\\\"); ++ } ++ function canOfferLocalOpen(failure, path) { ++ return failure.code !== "workspace-file/not-found" && isAbsoluteHostPath(path); ++ } ++ function localOpenButton(t, className, path, failure, openWorkspacePath) { ++ if (openWorkspacePath === void 0 || !canOfferLocalOpen(failure, path)) return null; ++ return (0, react_jsx_runtime.jsx)("button", { ++ type: "button", ++ className, ++ "data-textpreview-open-local": true, ++ onClick: () => { ++ void Promise.resolve(openWorkspacePath(path)).catch((reason) => { ++ console.warn("ui-sidebar-documentpreview: local open failed:", reason); ++ }); ++ }, ++ children: t("openLocally") ++ }); ++ } + //#endregion + //#region lib/types/client/icons.js + /** Two margin bars, a straight arrow running to the right one: lines run past the edge. */ +@@ -1406,7 +1426,7 @@ window.__ModuleLoader__.load({ + * @param props - composed slot props. + * @returns the content read so far with its controls, or a progress line. + */ +- function TextPreview({ useTabInfo, useResource, useStore, actions, loadPage, reloadPages, loadAll, reloadAll, useDocumentPreviews, renderSlot, t }) { ++ function TextPreview({ useTabInfo, useResource, useStore, actions, loadPage, reloadPages, loadAll, reloadAll, useDocumentPreviews, renderSlot, openWorkspacePath, t }) { + const { tab } = useTabInfo(); + const { navigation, signal } = tab; + const meta = useResource(tab.contentId); +@@ -1683,7 +1703,7 @@ window.__ModuleLoader__.load({ + "data-textpreview-retry": true, + onClick: loadNext, + children: t("retry") +- })] ++ }), localOpenButton(t, TextPreview_module_css_default.action, displayPath, current.failure, openWorkspacePath)] + }) : (0, react_jsx_runtime.jsxs)("div", { + className: TextPreview_module_css_default.empty, + "data-textpreview-failed": current.failure.code, +@@ -1703,7 +1723,8 @@ window.__ModuleLoader__.load({ + "data-textpreview-retry": true, + onClick: reload, + children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutline16, { size: 14 }), t("retry")] +- }) ++ }), ++ localOpenButton(t, TextPreview_module_css_default.retry, displayPath, current.failure, openWorkspacePath) + ] + })), + mode === "text-pages" && current !== void 0 && loaded.length > 0 && !current.eof && current.failure === void 0 && (0, react_jsx_runtime.jsx)("button", { +@@ -2050,7 +2071,8 @@ window.__ModuleLoader__.load({ + "error.notText": "非文本文件,暂时无法预览。", + "error.notRegularFile": "该路径不是普通文件,没有可显示的内容。", + "error.unavailable": "读取失败:{message}", +- retry: "重试" ++ retry: "重试", ++ openLocally: "用本地应用打开" + }; + /** English dictionary, checked against the Chinese key set. */ + const en$5 = { +@@ -2071,7 +2093,8 @@ window.__ModuleLoader__.load({ + "error.notText": "Not a text file, preview is unavailable for now.", + "error.notRegularFile": "Not a regular file, nothing to display.", + "error.unavailable": "Read failed: {message}", +- retry: "Retry" ++ retry: "Retry", ++ openLocally: "Open with local app" + }; + //#endregion + //#region lib/types/client/document/contract.js +@@ -26922,7 +26945,8 @@ fn fs_main(in : VertexOutput) -> @location(0) vec4 { + "locale", + "sidebarRightTabs", + "remote", +- "remote.workspaceFiles" ++ "remote.workspaceFiles", ++ "remote.session" + ]; + /** + * Client plugin body: register the type, its dictionaries, its body, and its chip title. +@@ -26955,7 +26979,8 @@ fn fs_main(in : VertexOutput) -> @location(0) vec4 { + } }, + inject: (sessionId, actions) => ({ + ...face(sessionId, actions), +- hooks: { documentPreviews: source } ++ hooks: { documentPreviews: source }, ++ openWorkspacePath: (path) => ctx.remote.session.openWorkspacePath({ path }) + }) + }, TextPreview)), "ui-sidebar-documentpreview: text body"); + ctx.effect(() => ctx.slots.inject("sidebar.right.pane.tab.title", () => ctx.slots.register({ +diff --git a/node_modules/@deepseek-ai/dsh-client-ui-sidebar-documentpreview/lib/types/client/locales.d.ts b/node_modules/@deepseek-ai/dsh-client-ui-sidebar-documentpreview/lib/types/client/locales.d.ts +index 7dc2cf3..43dfed1 100644 +--- a/node_modules/@deepseek-ai/dsh-client-ui-sidebar-documentpreview/lib/types/client/locales.d.ts ++++ b/node_modules/@deepseek-ai/dsh-client-ui-sidebar-documentpreview/lib/types/client/locales.d.ts +@@ -25,6 +25,7 @@ export declare const zh: { + 'error.notRegularFile': string; + 'error.unavailable': string; + retry: string; ++ openLocally: string; + }; + /** Text-preview dictionary key union. */ + export type SidebarDocumentPreviewKey = keyof typeof zh; +@@ -48,5 +49,6 @@ export declare const en: { + 'error.notRegularFile': string; + 'error.unavailable': string; + retry: string; ++ openLocally: string; + }; + //# sourceMappingURL=locales.d.ts.map +\ No newline at end of file diff --git a/test/file-preview.test.ts b/test/file-preview.test.ts new file mode 100644 index 000000000..1df9cf07a --- /dev/null +++ b/test/file-preview.test.ts @@ -0,0 +1,121 @@ +import { readFile } from 'node:fs/promises' +import path from 'node:path' +import { describe, expect, it } from 'vitest' +import { patchPath, projectRoot } from './patch-path' + +async function packageClient(name: string, file = 'lib/client.js'): Promise { + return readFile(path.join(projectRoot, 'node_modules', '@deepseek-ai', name, file), 'utf8') +} + +describe('uploaded file preview', () => { + it('exposes a session-authorized host path instead of file bytes', async () => { + const host = await packageClient('dsh-api-session-controller', 'lib/index.js') + const client = await packageClient('dsh-api-session-controller') + const uploads = await readFile( + path.join(projectRoot, 'node_modules', '@deepseek-ai', 'dsh-client-file-upload', 'lib', 'index.js'), + 'utf8' + ) + + expect(host).toContain('function referencedFile') + expect(host).toContain('collectReferencedFilesByName') + expect(host).toContain('collectReferencedImagesByName') + expect(host).toContain('resolveAuthorizedFileRefByName') + expect(host).toContain('ensureImagePreviewPath') + expect(host).toContain('imageHostPath(match.ref)') + expect(host).toContain('path: FILE_HOST_PATH') + expect(host).toContain('/api/session/fileHostPath') + expect(host).toContain('ctx.attachments.fileHostPath(ref)') + expect(host).toContain('findStagedFile') + expect(host).not.toContain('findStagedFilesByName') + expect(host).toContain('attachmentId or name is required') + expect(uploads).toContain('findStagedFile(agent, attachmentId)') + expect(uploads).not.toContain('findStagedFilesByName') + expect(client).toContain('async readFileHostPath(attachmentIdOrName)') + expect(client).toContain('query.set("name", attachmentIdOrName.name)') + expect(client).toContain('/api/session/fileHostPath') + }) + + it('opens uploaded files through one shared opener', async () => { + const chat = await packageClient('dsh-client-ui-chat') + const conversation = await packageClient('dsh-client-ui-conversation') + const attachment = await packageClient('dsh-client-ui-attachment') + + expect(chat).toContain('ctx.provide("openUploadedAttachment"') + expect(chat).toContain('binding.session.readFileHostPath(attachmentId)') + expect(chat).toContain('openResolvedHostFile(ctx, sessionId, cwd, result.value.path)') + expect(chat).toContain('sidebarRightTabs.candidates(address)') + expect(chat).toContain('ctx.remote.session.openWorkspacePath({ path: hostPath })') + expect(chat).toContain('ctx.sidebarRight.openResource') + expect(chat).toContain('"sidebarRightTabs"') + expect(chat).toContain('opener.open(sessionId, attachmentId)') + expect(chat).toContain('onOpenUploadedFile(attachment.file.attachmentId)') + expect(chat).not.toContain('openFile(attachment.file.name)') + expect(chat).not.toContain('openFile(attachment.name)') + + expect(conversation).toContain('ctx.get("openUploadedAttachment")') + expect(conversation).toContain('opener.open(sessionId, attachmentId)') + expect(conversation).toContain('onOpenUploadedFile: openUploadedFile') + expect(conversation).not.toContain('binding.session.readFileHostPath(attachmentId)') + expect(conversation).not.toContain('dsh-resource://file/session/') + + expect(attachment).toContain('onOpen: upload?.status === "ready"') + expect(attachment).toContain('onOpenUploadedFile(upload.file.attachmentId)') + }) + + it('resolves assistant file mentions via unique attachment name before workspace path', async () => { + const chat = await packageClient('dsh-client-ui-chat') + + expect(chat).toContain('session.readFileHostPath({ name: path })') + expect(chat).toContain('openResolvedHostFile(ctx, sessionId, cwd, byName.value.path)') + expect(chat).toContain('hostPathForOpen(cwd, path)') + expect(chat).not.toContain('openFile(attachment.file.name)') + }) + + it('records the preview patches', async () => { + const sessionPatch = await readFile(patchPath('@deepseek-ai/dsh-api-session-controller'), 'utf8') + const chatPatch = await readFile(patchPath('@deepseek-ai/dsh-client-ui-chat'), 'utf8') + const conversationPatch = await readFile(patchPath('@deepseek-ai/dsh-client-ui-conversation'), 'utf8') + const attachmentPatch = await readFile(patchPath('@deepseek-ai/dsh-client-ui-attachment'), 'utf8') + const uploadPatch = await readFile(patchPath('@deepseek-ai/dsh-client-file-upload'), 'utf8') + const deliverablesPatch = await readFile( + patchPath('@deepseek-ai/dsh-client-ui-deliverables'), + 'utf8' + ) + + expect(sessionPatch).toContain('/api/session/fileHostPath') + expect(sessionPatch).toContain('resolveAuthorizedFileRefByName') + expect(sessionPatch).toContain('collectReferencedImagesByName') + expect(sessionPatch).toContain('ensureImagePreviewPath') + expect(sessionPatch).not.toContain('findStagedFilesByName') + expect(chatPatch).toContain('openUploadedAttachment') + expect(chatPatch).toContain('readFileHostPath({ name: path })') + expect(chatPatch).toContain('openResolvedHostFile') + expect(chatPatch).toContain('openWorkspacePath') + expect(conversationPatch).toContain('openUploadedAttachment') + expect(conversationPatch).not.toContain('binding.session.readFileHostPath') + expect(attachmentPatch).toContain('onOpenUploadedFile') + expect(uploadPatch).toContain('findStagedFile') + expect(uploadPatch).not.toContain('findStagedFilesByName') + expect(deliverablesPatch).toContain('v?\\d+(?:\\.\\d+){1,4}') + expect(deliverablesPatch).toContain('@[^\\\\/@\\s]+\\/[^\\\\/@\\s]+(?:@[^\\\\/\\s]+)?') + }) + + it('offers a local-open action when sidebar preview cannot render the file', async () => { + const preview = await packageClient('dsh-client-ui-sidebar-documentpreview') + const previewPatch = await readFile( + patchPath('@deepseek-ai/dsh-client-ui-sidebar-documentpreview'), + 'utf8' + ) + + expect(preview).toContain('function canOfferLocalOpen') + expect(preview).toContain('failure.code !== "workspace-file/not-found"') + expect(preview).toContain('data-textpreview-open-local') + expect(preview).toContain('openLocally: "用本地应用打开"') + expect(preview).toContain('openLocally: "Open with local app"') + expect(preview).toContain('ctx.remote.session.openWorkspacePath({ path })') + expect(preview).toContain('"remote.session"') + expect(preview).not.toContain('window.dshDesktop.openInFinder') + expect(previewPatch).toContain('openLocally') + expect(previewPatch).toContain('data-textpreview-open-local') + }) +}) diff --git a/test/local-path-links.test.ts b/test/local-path-links.test.ts index fa295edb2..2eb7f9956 100644 --- a/test/local-path-links.test.ts +++ b/test/local-path-links.test.ts @@ -71,8 +71,11 @@ describe('assistant local path links', () => { for (const value of [ '@deepseek-ai/cordis', '@deepseek-ai/dsh-client-ui-deliverables', + '@deepseek-ai/dsh@0.1.2-rc.1', '@foo/bar', + '@foo/bar@1.0.0', '@plugin/name', + '@plugin/name@2.3.4-beta.1', 'user@example.com', 'first.last@sub.example.co', ]) { @@ -80,6 +83,20 @@ describe('assistant local path links', () => { } }) + it('keeps bare version tokens inert', async () => { + const localPathReference = await loadLocalPathReference() + + for (const value of [ + 'v0.8.0', + '0.8.0', + '1.2.3-rc.1', + '0.1.5-rc.1', + 'v1.0.0-beta.2', + ]) { + expect(localPathReference(value), value).toBeUndefined() + } + }) + it('still resolves paths that merely contain an @ segment', async () => { const localPathReference = await loadLocalPathReference() @@ -88,6 +105,7 @@ describe('assistant local path links', () => { '/tmp/@scope/pkg/index.js', 'patches/@deepseek-ai+dsh-client-ui-deliverables+0.1.5-rc.2.patch', 'node_modules/@foo/bar/lib/client.js', + '@scope/pkg@1.2.3/dist/index.js', ]) { expect(localPathReference(value), value).toBe(value) } From 4f59de4aa673bb369001342fed26a06120d5fcbe Mon Sep 17 00:00:00 2001 From: fairy <3054158379@qq.com> Date: Tue, 15 Sep 2026 14:58:46 +0800 Subject: [PATCH 2/2] fix(preview): match Windows attachment names without case and keep images if aliasing fails Explorer treats names as case-insensitive, and a failed hard-link should not block opening the original image path. Co-authored-by: Cursor --- ...sh-api-session-controller+0.1.5-rc.2.patch | 29 +++++++++------ test/file-preview.test.ts | 36 +++++++++++++++++++ 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/patches/@deepseek-ai+dsh-api-session-controller+0.1.5-rc.2.patch b/patches/@deepseek-ai+dsh-api-session-controller+0.1.5-rc.2.patch index 8c1c1b3d5..6bc4f6787 100644 --- a/patches/@deepseek-ai+dsh-api-session-controller+0.1.5-rc.2.patch +++ b/patches/@deepseek-ai+dsh-api-session-controller+0.1.5-rc.2.patch @@ -118,7 +118,7 @@ index 6226c83..38862c8 100644 * Fork a session from a completed-turn prefix of the source (same * synchronous-addressability guarantee as {@link ClientSessions.create}: diff --git a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/index.js b/node_modules/@deepseek-ai/dsh-api-session-controller/lib/index.js -index 0e94dd3..a6875fa 100644 +index 0e94dd3..258c578 100644 --- a/node_modules/@deepseek-ai/dsh-api-session-controller/lib/index.js +++ b/node_modules/@deepseek-ai/dsh-api-session-controller/lib/index.js @@ -3,7 +3,7 @@ import z from "@deepseek-ai/schemastery"; @@ -264,7 +264,7 @@ index 0e94dd3..a6875fa 100644 } catch (error) { throw new RemoteError("gateway/internal", `failed to fork session "${request.sessionId}": ${String(error)}`, {}); } -@@ -985,6 +1031,118 @@ function referencedImage(events, attachmentId) { +@@ -985,6 +1031,126 @@ function referencedImage(events, attachmentId) { if (found !== void 0) return found; } } @@ -309,10 +309,18 @@ index 0e94dd3..a6875fa 100644 + const parts = String(value).split(/[/\\]/); + return parts[parts.length - 1] || String(value); +} ++function fileNameEquals(left, right) { ++ if (left === right) return true; ++ // NTFS / Explorer treat names as case-insensitive; assistant mentions often ++ // disagree with the stored attachment casing on Windows. ++ if (process.platform === "win32") return left.toLowerCase() === right.toLowerCase(); ++ return false; ++} +function fileNameMatches(refName, needle) { + const name = String(refName ?? ""); + const needleBase = basenameLeaf(needle); -+ return name === needle || basenameLeaf(name) === needle || basenameLeaf(name) === needleBase; ++ const nameBase = basenameLeaf(name); ++ return fileNameEquals(name, needle) || fileNameEquals(nameBase, needle) || fileNameEquals(nameBase, needleBase); +} +/** +* Collect every distinct file attachment whose display name or basename matches. @@ -383,7 +391,7 @@ index 0e94dd3..a6875fa 100644 function routeServed(ctx, provider) { return ctx.llm.listProviders().some((entry) => entry.id === provider); } -@@ -2374,6 +2532,170 @@ const SessionMediaReferences = { +@@ -2374,6 +2540,171 @@ const SessionMediaReferences = { }), "session-controller: /api/file"); } }; @@ -466,7 +474,8 @@ index 0e94dd3..a6875fa 100644 + await link(hostPath, aliased); + } catch (error) { + if (error && typeof error === "object" && "code" in error && error.code === "EEXIST") return aliased; -+ throw error; ++ console.warn("session-controller: image preview alias failed:", error); ++ return hostPath; + } + return aliased; +} @@ -554,7 +563,7 @@ index 0e94dd3..a6875fa 100644 //#endregion //#region lib/types/index.js /** Session Remote owner: cold reads, explicit Agent commands, and live control state. */ -@@ -2485,6 +2807,7 @@ let SessionController = (() => { +@@ -2485,6 +2816,7 @@ let SessionController = (() => { let _canOpenWorkspacePath_decorators; let _openWorkspacePath_decorators; let _rename_decorators; @@ -562,7 +571,7 @@ index 0e94dd3..a6875fa 100644 let _fork_decorators; let _prompt_decorators; let _attachment_decorators; -@@ -2504,6 +2827,7 @@ let SessionController = (() => { +@@ -2504,6 +2836,7 @@ let SessionController = (() => { _canOpenWorkspacePath_decorators = [Remote]; _openWorkspacePath_decorators = [Remote("openWorkspacePath")]; _rename_decorators = [Remote("rename")]; @@ -570,7 +579,7 @@ index 0e94dd3..a6875fa 100644 _fork_decorators = [Remote("fork")]; _prompt_decorators = [Remote("prompt")]; _attachment_decorators = [Remote("attachment")]; -@@ -2600,6 +2924,17 @@ let SessionController = (() => { +@@ -2600,6 +2933,17 @@ let SessionController = (() => { }, metadata: _metadata }, null, _instanceExtraInitializers); @@ -588,7 +597,7 @@ index 0e94dd3..a6875fa 100644 __esDecorate(this, null, _fork_decorators, { kind: "method", name: "fork", -@@ -2745,9 +3080,11 @@ let SessionController = (() => { +@@ -2745,9 +3089,11 @@ let SessionController = (() => { this.canOpenPath = internals.canOpenPath ?? (() => config.nativeOpen ?? (internals.openPath !== void 0 || canOpenNativePath())); ctx.plugin(SessionFileReferences); ctx.plugin(SessionMediaReferences); @@ -601,7 +610,7 @@ index 0e94dd3..a6875fa 100644 }); ctx.on("session/disposed", (session) => { ctx.emit("api-session/removed", session.id); -@@ -2903,6 +3240,10 @@ let SessionController = (() => { +@@ -2903,6 +3249,10 @@ let SessionController = (() => { rename(request) { return this.commands.rename(request); } diff --git a/test/file-preview.test.ts b/test/file-preview.test.ts index 1df9cf07a..d8cf1817f 100644 --- a/test/file-preview.test.ts +++ b/test/file-preview.test.ts @@ -67,10 +67,42 @@ describe('uploaded file preview', () => { expect(chat).toContain('session.readFileHostPath({ name: path })') expect(chat).toContain('openResolvedHostFile(ctx, sessionId, cwd, byName.value.path)') + expect(chat).not.toContain('openResolvedHostFile(ctx, sessionId, cwd, byName.value.path, options)') expect(chat).toContain('hostPathForOpen(cwd, path)') expect(chat).not.toContain('openFile(attachment.file.name)') }) + it('matches attachment names case-insensitively on Windows only', async () => { + const host = await packageClient('dsh-api-session-controller', 'lib/index.js') + const source = host.match( + /function basenameLeaf\(value\) \{[\s\S]*?\nfunction fileNameMatches\(refName, needle\) \{[\s\S]*?\n\}/ + )?.[0] + expect(source).toBeDefined() + expect(source).toContain('process.platform === "win32"') + expect(source).toContain('left.toLowerCase() === right.toLowerCase()') + + const load = (platform: string) => + new Function( + 'process', + `${source}; return fileNameMatches` + )({ platform }) as (refName: string, needle: string) => boolean + + const onWindows = load('win32') + const onPosix = load('darwin') + expect(onWindows('周报.docx', '周报.DOCX')).toBe(true) + expect(onWindows('README.md', 'readme.md')).toBe(true) + expect(onWindows('src/Main.ts', 'main.ts')).toBe(true) + expect(onPosix('README.md', 'readme.md')).toBe(false) + expect(onPosix('周报.docx', '周报.docx')).toBe(true) + }) + + it('keeps the original image path when the preview alias cannot be created', async () => { + const host = await packageClient('dsh-api-session-controller', 'lib/index.js') + expect(host).toContain('session-controller: image preview alias failed:') + expect(host).toContain('return hostPath') + expect(host).not.toContain('if (error && typeof error === "object" && "code" in error && error.code === "EEXIST") return aliased;\n\t\tthrow error;') + }) + it('records the preview patches', async () => { const sessionPatch = await readFile(patchPath('@deepseek-ai/dsh-api-session-controller'), 'utf8') const chatPatch = await readFile(patchPath('@deepseek-ai/dsh-client-ui-chat'), 'utf8') @@ -86,9 +118,13 @@ describe('uploaded file preview', () => { expect(sessionPatch).toContain('resolveAuthorizedFileRefByName') expect(sessionPatch).toContain('collectReferencedImagesByName') expect(sessionPatch).toContain('ensureImagePreviewPath') + expect(sessionPatch).toContain('function fileNameEquals') + expect(sessionPatch).toContain('image preview alias failed') expect(sessionPatch).not.toContain('findStagedFilesByName') expect(chatPatch).toContain('openUploadedAttachment') expect(chatPatch).toContain('readFileHostPath({ name: path })') + expect(chatPatch).toContain('openResolvedHostFile(ctx, sessionId, cwd, byName.value.path)') + expect(chatPatch).not.toContain('openResolvedHostFile(ctx, sessionId, cwd, byName.value.path, options)') expect(chatPatch).toContain('openResolvedHostFile') expect(chatPatch).toContain('openWorkspacePath') expect(conversationPatch).toContain('openUploadedAttachment')