dsh-canvas-workspace 0.2.4-experimental.3 → 0.2.4-experimental.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,9 +25,15 @@
25
25
 
26
26
  Canvas 只在显示层处理历史和布局,不修改已有 DSH 会话事件。图片通过宿主授权的资源加载接口读取,不从展示文字推测附件。普通发送由原生输入框负责;测试不发送真实模型请求。
27
27
 
28
+ ## DSH 0.2.2 边界兼容
29
+
30
+ 修复新版宿主新增主面板包装层后,画布左侧仍保留原生侧栏轨道而裁切内容的问题。侧栏展开或收起进入画布都使用完整主区域,退出后恢复原来的侧栏状态、宽度和未发送草稿;不通过取消全局 `overflow` 掩盖边界。
31
+
32
+ `node tests/frame-bounds.browser.mjs` 覆盖新旧主区域结构、不同侧栏/右栏宽度,并验证旧选择器会复现空白边界。`node tests/frame-bounds.host.mjs` 使用隔离的真实宿主,覆盖展开、收起、再展开进入与退出画布。可通过 `DSH_TEST_NODE`、`DSH_TEST_CLI`、`CHROMIUM_PATH` 指定测试运行时,不读取个人会话或调用模型。
33
+
28
34
  ## 开发与检查
29
35
 
30
- 要求 Node.js `^22.19.0 || >=24`。当前开发依赖固定为 DSH `0.2.1-rc.3`;声明支持 `0.2.0-rc.2`、`0.2.1-rc.2` 与 `0.2.1-rc.3`,其它版本需单独验证。`0.2.4-experimental.2` 起直接支持 rc.3,安装时不需要版本兼容豁免。
36
+ 要求 Node.js `^22.19.0 || >=24`。当前开发依赖固定为 DSH `0.2.2-rc.1` 与 Cordis `4.1.0`;声明支持 `0.2.0-rc.2`、`0.2.1-rc.2`、`0.2.1-rc.3` 与 `0.2.2-rc.1`,其它版本需单独验证。源码修复版已适配 rc.1 新增的原生图片拖放接口,文件传输仍由宿主负责;旧宿主没有此服务时不阻塞画布加载。注意:该声明针对当前源码构建,不代表旧的 npm `0.2.4-experimental.2` 已包含这些适配。
31
37
 
32
38
  ```bash
33
39
  npm ci
@@ -44,6 +50,8 @@ npm run build
44
50
 
45
51
  桌面版的 `npm run test:host` 使用已安装的 macOS DSH、全新隔离 profile 和合成历史,覆盖原生画布页签与侧栏恢复。首次运行可用 `npm install --prefix .research/pnpm --ignore-scripts pnpm@11.28.5` 准备局部测试工具;不会改全局 pnpm 或个人 DSH 配置。
46
52
 
53
+ `npm run test:touch` 增加两个画布入口的持续单指拖动回归(每轮 30 个采样、连续 3 轮,并检查松手后无回跳),避免只发送一次 move 漏掉触摸事件可取消性变化。相机移动不再重复渲染未变化的历史 Markdown/IUI 内容;12 条历史回复、60 次移动的 DOM 回归从 720 次正文渲染降为仅初始 12 次。
54
+
47
55
  `npm run test:touch` 使用 Chromium CDP 的真实多触点输入,覆盖横屏、竖屏、198×242 小屏及桌面视口,两个画布入口、双指中心缩放/平移、触点增减与替换、原生滚动/控件边界及取消恢复。浏览器已接管的滚动不会再转换为画布手势。本地自动化不等于 iOS Safari 真机验证;本次未做 iPhone、iPad 或 Apple Watch 真机测试。插件只调整页面内画布,不能修改或隐藏 Safari 的系统地址栏、标签栏与工具栏。
48
56
 
49
57
  `npm run dev` 启动独立 MOCK 演示。浏览器测试使用本地 Vite 页面,不连接用户当前运行中的 DSH;宿主测试脚本另需隔离的官方 DSH 测试环境。`npm run build` 生成宿主插件 `dist/` 与本地演示 `site/`。仓库包含可直接由现有 DSH 插件链接读取的 `dist/`,`site/` 和测试截图、日志不提交。
package/dist/client.js CHANGED
@@ -418,7 +418,7 @@ function resolveWorkspacePath(cwd2, path2) {
418
418
  if (isAbsoluteWorkspacePath(path2)) return path2;
419
419
  if (cwd2 === void 0 || cwd2 === "") return path2;
420
420
  const separator = isWindowsStylePath(cwd2) && cwd2.includes("\\") ? "\\" : "/";
421
- return `${cwd2.replace(/[/\\]+$/, "")}${separator}${path2.replace(/^[/\\]+/, "")}`;
421
+ return `${cwd2.replace(/[/\\]+$/, "")}${separator}${path2.replace(/^(?:\.?[/\\])+/, "")}`;
422
422
  }
423
423
  function fileMediaUrl(base, path2) {
424
424
  if (!/^https?:/u.test(base) && !base.startsWith("dsh-app://app/") || !isAbsoluteWorkspacePath(path2) || /^[/\\]{2}/u.test(path2) || /[\u0000-\u001f\u007f]/u.test(path2)) return void 0;
@@ -468,6 +468,28 @@ async function resolveNativeImageResource(base, cwd2, request) {
468
468
  return { ok: true, value: { src, alt } };
469
469
  }
470
470
 
471
+ // src/host/native-file-drag.ts
472
+ function createNativeFileDrag(ctx) {
473
+ let actions;
474
+ ctx.inject(["fileDrag"], (scope) => {
475
+ const current2 = scope.fileDrag.actions;
476
+ actions = current2;
477
+ scope.effect(() => () => {
478
+ if (actions === current2) actions = void 0;
479
+ });
480
+ });
481
+ return {
482
+ prepare(source, signal) {
483
+ if (!actions)
484
+ return Promise.reject(new Error("DSH file drag service unavailable"));
485
+ return actions.prepare(source, signal);
486
+ },
487
+ reportFailure() {
488
+ actions?.reportFailure();
489
+ }
490
+ };
491
+ }
492
+
471
493
  // src/client.tsx
472
494
  var import_dsh_client_ui_primitives3 = require("@deepseek-ai/dsh-client-ui-primitives");
473
495
 
@@ -878,7 +900,8 @@ function useTouchViewport(stage, options) {
878
900
  cancelRef.current = cancel;
879
901
  const sample = (event) => {
880
902
  if ((event.type === "touchstart" || event.type === "touchmove") && !event.cancelable) {
881
- cancel();
903
+ if (gesture.current.active) cancel();
904
+ else eligible.clear();
882
905
  return;
883
906
  }
884
907
  const rect = element3.getBoundingClientRect();
@@ -24429,7 +24452,7 @@ function ReplyBody({
24429
24452
  ] })
24430
24453
  ] });
24431
24454
  }
24432
- function ReplyContent(props) {
24455
+ var ReplyContent = (0, import_react12.memo)(function ReplyContent2(props) {
24433
24456
  return /* @__PURE__ */ (0, import_jsx_runtime60.jsx)(
24434
24457
  ReplyBoundary,
24435
24458
  {
@@ -24438,7 +24461,7 @@ function ReplyContent(props) {
24438
24461
  children: /* @__PURE__ */ (0, import_jsx_runtime60.jsx)(ReplyBody, { ...props }, props.messageId)
24439
24462
  }
24440
24463
  );
24441
- }
24464
+ });
24442
24465
 
24443
24466
  // src/ui/UserMessageMedia.tsx
24444
24467
  var import_react13 = require("react");
@@ -24885,6 +24908,25 @@ function ChatCanvas({
24885
24908
  if (rail && typeof window.requestAnimationFrame === "function")
24886
24909
  rail.scrollTo({ top: rail.scrollTop, behavior: "instant" });
24887
24910
  };
24911
+ (0, import_react15.useLayoutEffect)(() => {
24912
+ const rail = userRail.current;
24913
+ if (!rail) return;
24914
+ const updateEdges = () => {
24915
+ rail.dataset.overflowStart = String(rail.scrollTop > 1);
24916
+ rail.dataset.overflowEnd = String(
24917
+ rail.scrollTop + rail.clientHeight < rail.scrollHeight - 1
24918
+ );
24919
+ };
24920
+ updateEdges();
24921
+ rail.addEventListener("scroll", updateEdges, { passive: true });
24922
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(updateEdges);
24923
+ observer?.observe(rail);
24924
+ for (const child of rail.children) observer?.observe(child);
24925
+ return () => {
24926
+ rail.removeEventListener("scroll", updateEdges);
24927
+ observer?.disconnect();
24928
+ };
24929
+ }, [pairScope, userMessageIds]);
24888
24930
  (0, import_react15.useLayoutEffect)(() => {
24889
24931
  const ids = pairs.userMessages.map((message) => message.id);
24890
24932
  const previous3 = seenRailMessages.current;
@@ -26065,6 +26107,16 @@ function ChatCanvas({
26065
26107
  iuiActions.current.set(key, pending);
26066
26108
  return pending;
26067
26109
  };
26110
+ const iuiActionRef = (0, import_react15.useRef)(onIuiAction);
26111
+ iuiActionRef.current = onIuiAction;
26112
+ const handleIuiAction = (0, import_react15.useCallback)(
26113
+ (action) => iuiActionRef.current(action),
26114
+ []
26115
+ );
26116
+ const handleIuiResource = (0, import_react15.useMemo)(
26117
+ () => resolveImage ? (request) => resolveImage(request, state.sessionId) : void 0,
26118
+ [resolveImage, state.sessionId]
26119
+ );
26068
26120
  const stop = async () => {
26069
26121
  const active = running.current;
26070
26122
  if (!active) return;
@@ -26469,8 +26521,8 @@ function ChatCanvas({
26469
26521
  iuiEnabled,
26470
26522
  renderMarkdown,
26471
26523
  renderImagePreview,
26472
- onIuiAction,
26473
- onIuiResource: resolveImage ? (request) => resolveImage(request, state.sessionId) : void 0
26524
+ onIuiAction: handleIuiAction,
26525
+ onIuiResource: handleIuiResource
26474
26526
  }
26475
26527
  ),
26476
26528
  reply.status === "running" && /* @__PURE__ */ (0, import_jsx_runtime63.jsx)(
@@ -30387,29 +30439,34 @@ var styles_default = `.canvas-app {
30387
30439
  border-radius: 20px;
30388
30440
  pointer-events: none;
30389
30441
  }
30442
+ /* A soft reading scrim, not a grey rectangular pane over the canvas. */
30390
30443
  .canvas-app .chat-user-history-glass {
30391
30444
  position: absolute;
30392
- /* Keep blur inside the rounded message-list background, without a top halo. */
30393
30445
  inset: 0;
30394
30446
  z-index: 0;
30395
30447
  border-radius: inherit;
30396
30448
  clip-path: inset(0 round 20px);
30397
- background: color-mix(in srgb, var(--cw-surface) 18%, transparent);
30398
- -webkit-backdrop-filter: blur(22px) saturate(0.95);
30399
- backdrop-filter: blur(22px) saturate(0.95);
30449
+ background: linear-gradient(
30450
+ to right,
30451
+ transparent,
30452
+ color-mix(in srgb, var(--cw-bg) 42%, transparent) 28%,
30453
+ color-mix(in srgb, var(--cw-bg) 72%, transparent)
30454
+ );
30455
+ -webkit-backdrop-filter: blur(12px);
30456
+ backdrop-filter: blur(12px);
30400
30457
  -webkit-mask-image:
30401
30458
  linear-gradient(
30402
30459
  to right,
30403
30460
  transparent,
30404
- #000 8px,
30405
- #000 calc(100% - 8px),
30461
+ #000 28px,
30462
+ #000 calc(100% - 12px),
30406
30463
  transparent
30407
30464
  ),
30408
30465
  linear-gradient(
30409
30466
  to bottom,
30410
30467
  transparent,
30411
- #000 8px,
30412
- #000 calc(100% - 8px),
30468
+ #000 24px,
30469
+ #000 calc(100% - 24px),
30413
30470
  transparent
30414
30471
  );
30415
30472
  -webkit-mask-composite: source-in;
@@ -30417,15 +30474,15 @@ var styles_default = `.canvas-app {
30417
30474
  linear-gradient(
30418
30475
  to right,
30419
30476
  transparent,
30420
- #000 8px,
30421
- #000 calc(100% - 8px),
30477
+ #000 28px,
30478
+ #000 calc(100% - 12px),
30422
30479
  transparent
30423
30480
  ),
30424
30481
  linear-gradient(
30425
30482
  to bottom,
30426
30483
  transparent,
30427
- #000 8px,
30428
- #000 calc(100% - 8px),
30484
+ #000 24px,
30485
+ #000 calc(100% - 24px),
30429
30486
  transparent
30430
30487
  );
30431
30488
  mask-composite: intersect;
@@ -30454,6 +30511,39 @@ var styles_default = `.canvas-app {
30454
30511
  -webkit-user-select: none;
30455
30512
  user-select: none;
30456
30513
  }
30514
+ /* Overflow is a scroll affordance, not a hard cut through a blue bubble.
30515
+ Keep fades proportional on tiny watch/landscape rails. */
30516
+ .canvas-app .chat-user-history {
30517
+ --rail-fade-start: 0px;
30518
+ --rail-fade-end: 0px;
30519
+ -webkit-mask-image: linear-gradient(
30520
+ to bottom,
30521
+ transparent,
30522
+ #000 var(--rail-fade-start),
30523
+ #000 calc(100% - var(--rail-fade-end)),
30524
+ transparent
30525
+ );
30526
+ mask-image: linear-gradient(
30527
+ to bottom,
30528
+ transparent,
30529
+ #000 var(--rail-fade-start),
30530
+ #000 calc(100% - var(--rail-fade-end)),
30531
+ transparent
30532
+ );
30533
+ }
30534
+ .canvas-app .chat-user-history[data-overflow-start="true"] {
30535
+ --rail-fade-start: min(24px, 16%);
30536
+ }
30537
+ .canvas-app .chat-user-history[data-overflow-end="true"] {
30538
+ --rail-fade-end: min(24px, 16%);
30539
+ }
30540
+ @media (prefers-reduced-transparency: reduce) {
30541
+ .canvas-app .chat-user-history-glass {
30542
+ background: var(--cw-surface);
30543
+ -webkit-backdrop-filter: none;
30544
+ backdrop-filter: none;
30545
+ }
30546
+ }
30457
30547
  .canvas-app .chat-user-message-group {
30458
30548
  /* Give percentage bubble limits a definite rail width, not shrink-to-fit. */
30459
30549
  width: 100%;
@@ -31251,6 +31341,9 @@ var styles_default = `.canvas-app {
31251
31341
  }
31252
31342
 
31253
31343
  /* Match public slot/region markers only while our native View is mounted.
31344
+ DSH 0.2.2 wraps the main slot in MainPanel. Match the frame child containing
31345
+ the slot, not only a direct slot child, so the hidden sidebar leaves no
31346
+ phantom clipping column. Do not remove the real viewport clipping boundary.
31254
31347
  Keep the right track and stored widths intact; exit restores the host layout. */
31255
31348
  :has(> [data-shell-overlay]):has([data-canvas-conversation-view])
31256
31349
  > :has(> [data-slot="sidebar"]),
@@ -31261,7 +31354,7 @@ var styles_default = `.canvas-app {
31261
31354
  display: none;
31262
31355
  }
31263
31356
  :has(> [data-shell-overlay]):has([data-canvas-conversation-view])
31264
- > :has(> [data-slot="main"]) {
31357
+ > :has([data-slot="main"]) {
31265
31358
  grid-column: 1 / 3;
31266
31359
  grid-row: 1;
31267
31360
  border-left: none;
@@ -31516,14 +31609,17 @@ function NativeNavigationIcon() {
31516
31609
  function NativeSendIcon() {
31517
31610
  return /* @__PURE__ */ (0, import_jsx_runtime66.jsx)(import_dsh_client_ui_primitives2.IconSendOutlineMedium, { size: 16 });
31518
31611
  }
31519
- function renderNativeMarkdown(text8, streaming = false, cwd2) {
31612
+ function renderNativeMarkdown(text8, streaming, cwd2, fileDrag) {
31520
31613
  const pathImages = typeof document === "undefined" ? void 0 : createNativePathImages(document.baseURI, cwd2);
31521
31614
  return /* @__PURE__ */ (0, import_jsx_runtime66.jsx)(
31522
31615
  import_dsh_client_ui_primitives2.MarkdownDelegateProvider,
31523
31616
  {
31524
31617
  fileImages: pathImages ? {
31525
31618
  resolve: pathImages.resolve,
31526
- labels: imageLabels
31619
+ labels: imageLabels,
31620
+ // Before 0.2.2 the renderer ignores this extra property. Current
31621
+ // hosts receive their own transfer service through the adapter.
31622
+ fileDrag
31527
31623
  } : void 0,
31528
31624
  children: /* @__PURE__ */ (0, import_jsx_runtime66.jsx)(
31529
31625
  import_dsh_client_ui_primitives2.MarkdownText,
@@ -31680,7 +31776,8 @@ function CanvasChat(props) {
31680
31776
  renderMarkdown: (text8, streaming, sessionId) => renderNativeMarkdown(
31681
31777
  text8,
31682
31778
  streaming,
31683
- sessionId ? hostSessions.find((session) => session.id === sessionId)?.cwd : void 0
31779
+ sessionId ? hostSessions.find((session) => session.id === sessionId)?.cwd : void 0,
31780
+ props.fileDrag
31684
31781
  ),
31685
31782
  renderImagePreview: renderNativeImagePreview,
31686
31783
  loadUserImage: (attachment, sessionId) => props.loadUserImage(sessionId, attachment),
@@ -31728,12 +31825,14 @@ function CanvasConversationView(props) {
31728
31825
  ) });
31729
31826
  }
31730
31827
  function apply(ctx) {
31828
+ const fileDrag = createNativeFileDrag(ctx);
31731
31829
  const adapter = createDshAdapter(ctx);
31732
31830
  const demo = new URLSearchParams(location.search).get("canvasMock") === "1";
31733
31831
  const selected = demo ? createMockAdapter() : adapter;
31734
31832
  const returnToNative = () => ctx.layout.selectPanel(null);
31735
31833
  const accountActions = createNativeAccountActions(returnToNative);
31736
31834
  const canvasFace = () => ({
31835
+ fileDrag,
31737
31836
  adapter: selected,
31738
31837
  mode: selected.kind,
31739
31838
  onExit: returnToNative,