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:
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 {
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user