feat(web): whole-page image drop, projected intake limits, and thumbnail tiling

Second alignment step for #2248: document-level drag intake behind the new
DropOverlay atom, lightbox close icon and shared dialog mask, DeepSeek Chat
thumbnail rules (single 240px long edge with ratio clamp, 64px tiles, merged
consecutive assistant images), image limits raised to 20/10MiB/100MiB and
published to clients as the imageLimits projection, whole-batch intake
pre-check with product-copy banners, and attachment-error reasons mapped to
localized copy.
This commit is contained in:
creatixchu
2026-08-12 12:31:52 +08:00
parent 4a8ee7674a
commit 2c9036b102
46 changed files with 999 additions and 171 deletions

View File

@@ -85,6 +85,7 @@ import type { ApprovalOutcome, ApprovalRequestId } from '@deepseek-ai/dsh-user-a
// `ctx.get('approval')` without a value dependency on the seam (optional composition).
import type {} from '@deepseek-ai/dsh-user-approval'
import { approvalResponsePayloadSchema } from './api/approvals.schema.ts'
import { imageLimitsProjectionSchema } from './api/sessions.schema.ts'
import { questionResponsePayloadSchema } from './api/questions.schema.ts'
import type { ClientResponse, RpcError, RpcReceipt, RpcRequest, RpcResponse } from './api/rpc.ts'
import { RpcId } from './api/rpc.ts'
@@ -1227,6 +1228,26 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro
})
})
// The imageLimits projection unit: the attachments config this proxy
// enforces at prompt admission, constant per host boot. `apply` keeps the
// same state reference for every event, so no change frames are ever
// pushed — baselines alone carry the value — and clients pre-check intake
// and label upload affordances from it. Registered here, not in the
// attachment Service Definition: dsh-llm depends on dsh-attachment, so the
// 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.
ctx.inject(['sessionProjections', 'attachments'], (projectionCtx) => {
projectionCtx.sessionProjections.register<'imageLimits', null>({
key: 'imageLimits',
schema: imageLimitsProjectionSchema,
init: () => null,
apply: state => state,
view: () => projectionCtx.attachments.imageLimits,
stateVersion: 1,
})
})
/** Project both durable inbox lists, optionally including the splice currently being emitted. */
const queueItems = (
agent: Agent,

View File

@@ -15,7 +15,7 @@ import type {
ModelReasoningEffort, ModelSelection, SessionProjectionsBlock, SessionSearchItem, SessionSummary,
} from './sessions.ts'
import type { ToolEventView } from './events.ts'
import type { AttachmentIdType, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
import type { AttachmentIdType, ImageAttachmentLimits, ImageAttachmentRef } from '@deepseek-ai/dsh-attachment'
import type { WorkspaceId } from './workspace.ts'
import {
SESSION_SEARCH_RESULT_LIMIT,
@@ -213,7 +213,20 @@ export const sessionProjectionsBlockSchema = z.object({
// -1 = empty log (the lastSeq convention of session/subscribed).
asOfSeq: z.number().int().min(-1),
values: z.record(z.string(), z.unknown()),
}) as unknown as z.ZodType<SessionProjectionsBlock>
}) as unknown as z.ZodType<Wire<SessionProjectionsBlock>>
/**
* imageLimits projection unit schema (host-side view validation). zod widens
* `readonly ImageMediaType[]` to `string[]`; on the JSON wire the two
* serialize identically, so the cast records exactly that widening.
*/
export const imageLimitsProjectionSchema = z.object({
maxImageBytes: z.number().int().positive(),
maxImagesPerMessage: z.number().int().positive(),
maxMessageImageBytes: z.number().int().positive(),
maxImagePixels: z.number().int().positive(),
mediaTypes: z.array(z.string()),
}) as unknown as z.ZodType<ImageAttachmentLimits>
/** session.history response value (projections rides the tail page only). */
export const sessionHistoryValueSchema: z.ZodType<Wire<ResponseValue<'session.history'>>> = z.object({

View File

@@ -5,7 +5,7 @@
*/
import type { MessageId } from '@deepseek-ai/dsh-llm/brand'
import type { AttachmentIdType, ImageAttachmentRef, ImageMediaType } from '@deepseek-ai/dsh-attachment'
import type { AttachmentIdType, ImageAttachmentLimits, ImageAttachmentRef, ImageMediaType } from '@deepseek-ai/dsh-attachment'
import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session/types'
// The pure-type outlet: api/ is browser-importable, and the package root's
@@ -15,6 +15,19 @@ import type { RpcId, RpcRequest, RpcResponse } from './rpc.ts'
import type { ToolEventView } from './events.ts'
import type { WorkspaceId } from './workspace.ts'
declare module '@deepseek-ai/dsh-session-projection/types' {
interface SessionProjectionMap {
/**
* The deployment's image-intake limits: the attachments service's config
* as this proxy enforces it at prompt admission, constant per host boot.
* Clients pre-check count and bytes at intake and show the limits in
* upload affordances. Key absence means no attachment service is
* composed — clients skip the pre-check and let the host answer.
*/
imageLimits: ImageAttachmentLimits
}
}
declare module '@deepseek-ai/dsh-llm' {
interface MessageSourceMap {
/**