review: theme-owned drop mask token, overlay fade-in, stable gallery keys, close-button stacking, stale note defaults
This commit is contained in:
10 files changed
+33
-12
No files matched your search
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-22-web-multimodal-image-input-and-durable-attachments.md
|
||||
2026-07-22-web-multimodal-image-input-and-durable-attachments.md: f1fbcbd29b188505e647265c4c974886c066e936
|
||||
2026-07-22-web-multimodal-image-input-and-durable-attachments.zh.md: a55467cacc412b6f313dd932a0e4868965c039ea
|
||||
2026-07-22-web-multimodal-image-input-and-durable-attachments.md: 672270856ccf7d9d785783284bc1b865a221d548
|
||||
2026-07-22-web-multimodal-image-input-and-durable-attachments.zh.md: aba8a8d948d0472f94089b747465f1cd32674dd8
|
||||
+1
-1
@@ -140,7 +140,7 @@ Composer thumbnails and each `MessageImage` own ephemeral original-preview state
|
||||
|
||||
### Limits and trust boundaries
|
||||
|
||||
Version one accepts PNG, JPEG, WebP, and GIF only. SVG and remote URLs are excluded. Default limits are 5 MiB per image, 10 images and 20 MiB aggregate image bytes per message, and 40 million intrinsic pixels per image. These deployment-varying limits are validated backend configuration and enforced by the host before persistence. The client connection carrier has an independent configurable `maxRequestBodyBytes` cap (32 MiB by default) for every API request and fails load if it cannot hold the attachment service's aggregate image limit after base64 and envelope expansion; lowering image policy therefore never silently lowers the carrier limit for valid text or other RPCs. A body without a declared length is rejected the moment it crosses the cap rather than drained to its end.
|
||||
Version one accepts PNG, JPEG, WebP, and GIF only. SVG and remote URLs are excluded. Default limits are 10 MiB per image, 20 images and 100 MiB aggregate image bytes per message, and 40 million intrinsic pixels per image. These deployment-varying limits are validated backend configuration and enforced by the host before persistence. The client connection carrier has an independent configurable `maxRequestBodyBytes` cap (160 MiB by default) for every API request and fails load if it cannot hold the attachment service's aggregate image limit after base64 and envelope expansion; lowering image policy therefore never silently lowers the carrier limit for valid text or other RPCs. A body without a declared length is rejected the moment it crosses the cap rather than drained to its end.
|
||||
|
||||
Malformed base64, unsupported or mismatched media, truncated image payloads, excess bytes, excess image count, excess pixels, missing objects, and integrity mismatches return stable structured failures. Original filenames are reduced to a display basename, control characters are removed, and no local path is logged or returned to the browser.
|
||||
|
||||
|
||||
+1
-1
@@ -140,7 +140,7 @@ Pi-AI 适配器是首条视觉输入路径:它在请求时解析 `ctx.attachme
|
||||
|
||||
### 限制与信任边界
|
||||
|
||||
第一版仅接受 PNG、JPEG、WebP 和 GIF。不接受 SVG 和远程 URL。默认限制为每张图片 5 MiB、每条消息 10 张图片和 20 MiB 图片总字节数,以及每张图片 4,000 万个固有像素。这些随部署变化的限制属于经过校验的后端配置,并由宿主在持久化前强制执行。客户端连接载体为每个 API 请求设置独立且可配置的 `maxRequestBodyBytes` 上限(默认 32 MiB);如果该上限无法容纳附件服务的图片总量限制经 base64 和请求封装膨胀后的大小,加载就会失败。因此,降低图片策略绝不会静默降低有效文本或其他 RPC 的载体上限。未声明长度的请求体在越过上限的瞬间即被拒绝,而不是先读完再拒。
|
||||
第一版仅接受 PNG、JPEG、WebP 和 GIF。不接受 SVG 和远程 URL。默认限制为每张图片 10 MiB、每条消息 20 张图片和 100 MiB 图片总字节数,以及每张图片 4,000 万个固有像素。这些随部署变化的限制属于经过校验的后端配置,并由宿主在持久化前强制执行。客户端连接载体为每个 API 请求设置独立且可配置的 `maxRequestBodyBytes` 上限(默认 160 MiB);如果该上限无法容纳附件服务的图片总量限制经 base64 和请求封装膨胀后的大小,加载就会失败。因此,降低图片策略绝不会静默降低有效文本或其他 RPC 的载体上限。未声明长度的请求体在越过上限的瞬间即被拒绝,而不是先读完再拒。
|
||||
|
||||
格式错误的 base64、不支持或不匹配的媒体、截断的图片数据、超出字节限制、超出图片数量、超出像素限制、对象缺失和完整性不匹配都会返回稳定的结构化错误。原始文件名只保留用于显示的末段,控制字符会被移除,并且任何本地路径都不会写入日志或返回浏览器。
|
||||
|
||||
|
||||
@@ -981,6 +981,9 @@ function projectionValuesOf(log: readonly SessionEvent[]): Record<string, unknow
|
||||
values['contextBreakdown'] = contextBreakdownOf(log)
|
||||
// Always present (attachment service composed): the deployment image
|
||||
// limits, constant per boot (mirrors the attachment-local defaults).
|
||||
// Deliberate host divergence: the real gateway never pushes an imageLimits
|
||||
// change frame (constant unit), but the fixture's uniform baseline replay
|
||||
// frames every key here, incidentally exercising higher-seq-wins.
|
||||
values['imageLimits'] = {
|
||||
maxImageBytes: 10 * 1024 * 1024,
|
||||
maxImagesPerMessage: 20,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* Full-viewport drop invitation (DeepSeek Chat DragMask). pointer-events:
|
||||
none — the layer is decoration; drag events must keep hitting the page so
|
||||
the owner's enter/leave count stays balanced. */
|
||||
the owner's enter/leave count stays balanced. The frosted sheet color is
|
||||
the theme's drop-mask alias (dark override lives with the theme owner). */
|
||||
.mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
@@ -9,12 +10,20 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
background-color: var(--dsw-alias-bg-mask-drop);
|
||||
backdrop-filter: blur(10px);
|
||||
animation: fade-in 160ms ease-out;
|
||||
}
|
||||
|
||||
:global(body[data-ds-dark-theme]) .mask {
|
||||
background-color: rgba(39, 39, 48, 0.7);
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.mask {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
.wrap {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { createPortal } from 'react-dom'
|
||||
import clsx from 'clsx'
|
||||
import css from './DropOverlay.module.css'
|
||||
|
||||
/** Drop-overlay strings the owner resolves from its own locale namespace. */
|
||||
@@ -27,7 +26,7 @@ export function DropOverlay({ disabled, labels }: {
|
||||
labels: DropOverlayLabels
|
||||
}) {
|
||||
return createPortal(
|
||||
<div className={clsx(css.mask, disabled && css.disabled)} role="status">
|
||||
<div className={css.mask} role="status">
|
||||
<div className={css.wrap}>
|
||||
<div className={css.illustration} aria-hidden="true">
|
||||
{disabled ? <UploadDisabledIllustration /> : <UploadIllustration />}
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
.image {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: min(100%, 1600px);
|
||||
max-height: calc(100vh - 80px);
|
||||
object-fit: contain;
|
||||
@@ -32,6 +31,7 @@
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 36px;
|
||||
|
||||
@@ -71,6 +71,10 @@ export const AssistantMarkdown = memo(function AssistantMarkdown({
|
||||
case 'image': {
|
||||
// Consecutive image blocks share one gallery so several images tile
|
||||
// into rows instead of each opening a one-image group of its own.
|
||||
// Keyed by the group's FIRST block index: a streaming append that
|
||||
// extends the group then only grows `images` instead of remounting
|
||||
// the gallery under a shifted key.
|
||||
const start = i
|
||||
const group = [block]
|
||||
while (i + 1 < blocks.length) {
|
||||
const next = blocks[i + 1]
|
||||
@@ -78,7 +82,7 @@ export const AssistantMarkdown = memo(function AssistantMarkdown({
|
||||
group.push(next)
|
||||
i += 1
|
||||
}
|
||||
rendered.push(<ImageGallery key={i} images={group} load={imageLoader} align="start" labels={messageImageLabels(t)} />)
|
||||
rendered.push(<ImageGallery key={start} images={group} load={imageLoader} align="start" labels={messageImageLabels(t)} />)
|
||||
break
|
||||
}
|
||||
// Grouped into tool rows by ChatView; hasVisible above skips an empty shell.
|
||||
|
||||
@@ -162,6 +162,7 @@ body {
|
||||
--dsw-alias-bg-mask-2: rgba(0, 0, 0, 0.12);
|
||||
--dsw-alias-bg-mask-3: rgba(0, 0, 0, 0.48);
|
||||
--dsw-alias-bg-mask-photo: rgba(0, 0, 0, 0.88);
|
||||
--dsw-alias-bg-mask-drop: rgba(255, 255, 255, 0.7);
|
||||
--dsw-alias-bg-module-platform: var(--dsw-static-neutral-bluish-60);
|
||||
--dsw-alias-bg-multi-select: var(--dsw-static-neutral-bluish-60);
|
||||
--dsw-alias-bg-overlay: var(--dsw-static-neutral-bluish-150);
|
||||
@@ -253,6 +254,7 @@ body[data-ds-dark-theme] {
|
||||
--dsw-alias-bg-mask-2: rgba(0, 0, 0, 0.2);
|
||||
--dsw-alias-bg-mask-3: rgba(0, 0, 0, 0.48);
|
||||
--dsw-alias-bg-mask-photo: rgba(0, 0, 0, 0.88);
|
||||
--dsw-alias-bg-mask-drop: rgba(39, 39, 48, 0.7);
|
||||
--dsw-alias-bg-module-platform: var(--dsw-static-neutral-bluish-800);
|
||||
--dsw-alias-bg-multi-select: var(--dsw-static-neutral-850);
|
||||
--dsw-alias-bg-overlay: var(--dsw-static-neutral-bluish-700);
|
||||
|
||||
@@ -1237,6 +1237,10 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
|
||||
// seam package cannot reference the projection registry without a cycle,
|
||||
// and the per-message rules the value describes are this proxy's own
|
||||
// admission checks. The child activates only while both seams are composed.
|
||||
// `view` reading the live service instead of the (null) state is sanctioned
|
||||
// exactly for boot-constant units: the value cannot change within a process
|
||||
// lifetime, so the fold stays observationally pure, and a stale persisted
|
||||
// cache row re-viewing to the current config is the correct outcome.
|
||||
ctx.inject(['sessionProjections', 'attachments'], (projectionCtx) => {
|
||||
projectionCtx.sessionProjections.register<'imageLimits', null>({
|
||||
key: 'imageLimits',
|
||||
|
||||
Reference in New Issue
Block a user