dsh-side-chat-plus 0.3.4 → 0.4.0-alpha.1

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/lib/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { mkdir, writeFile } from "node:fs/promises";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import { dirname } from "node:path";
4
4
  import * as agentApi from "@deepseek-ai/dsh-agent";
5
5
  import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
@@ -171,7 +171,6 @@ const inject = [
171
171
  "agents",
172
172
  "workspaceRegistry",
173
173
  "sessionQuery",
174
- "sandboxPolicy",
175
174
  "permissionPresets",
176
175
  "agentPresets",
177
176
  "llm",
@@ -191,6 +190,24 @@ const RECORD_FILE = "dsh-side-chat-sessions.json";
191
190
  function recordFilePath() {
192
191
  return dshHomePath(RECORD_FILE);
193
192
  }
193
+ /** Read the durable side-chat record list (absent/malformed → empty). */
194
+ async function readRecords() {
195
+ try {
196
+ const raw = await readFile(recordFilePath(), "utf8");
197
+ const parsed = JSON.parse(raw);
198
+ if (!Array.isArray(parsed)) return [];
199
+ return parsed.filter((entry) => {
200
+ const record = entry;
201
+ return typeof record?.childId === "string" && typeof record?.parentSessionId === "string";
202
+ }).map((entry) => ({
203
+ childId: entry.childId,
204
+ parentSessionId: entry.parentSessionId,
205
+ createdAt: typeof entry.createdAt === "number" ? entry.createdAt : 0
206
+ }));
207
+ } catch {
208
+ return [];
209
+ }
210
+ }
194
211
  /** Persist the live side-chat record list (the "record" a future cleanup consumes). */
195
212
  async function writeRecords(records) {
196
213
  const path = recordFilePath();
@@ -269,14 +286,6 @@ function openTurnStart(events) {
269
286
  if (lastEnd !== void 0 && lastEnd >= lastStart) return void 0;
270
287
  return lastStart;
271
288
  }
272
- /**
273
- * Read a session's event log across DSH versions: 0.1.5+ exposes
274
- * `snapshotEvents()`; 0.1.2-era sessions carried a plain `events` getter.
275
- */
276
- function sessionEvents(session) {
277
- if (typeof session.snapshotEvents === "function") return session.snapshotEvents();
278
- return session.events ?? [];
279
- }
280
289
  /** Schemastery schema for the user-facing preferences (validated by the settings service). */
281
290
  const PrefsSchema = z.object({
282
291
  lookupDefault: z.boolean().default(SUBCHAT_PREFS_DEFAULTS.lookupDefault),
@@ -399,10 +408,18 @@ function buildApi(ctx, sideChats, getSettings) {
399
408
  const child = childOf(childId);
400
409
  return { executed: await ctx.commands.execute(child, line, [], new AbortController().signal) !== void 0 };
401
410
  };
411
+ /**
412
+ * Read a side chat's event log through the session-query service. DSH 0.1.6
413
+ * deprecated the Session's synchronous event getters (`snapshotEvents`,
414
+ * `eventAt`, `ownEvents`); `readSession` is the live-preferred asynchronous
415
+ * read that supersedes them, and the transcript routes already use it.
416
+ */
417
+ const sessionEvents = async (sessionId) => (await ctx.sessionQuery.readSession(sessionId)).events;
402
418
  /** Fold one side-chat agent's plan/goal state for its composer chrome. */
403
- const state = (payload) => {
419
+ const state = async (payload) => {
404
420
  const childId = requireString(payload, "childId");
405
- const events = sessionEvents(childOf(childId).session);
421
+ childOf(childId);
422
+ const events = await sessionEvents(childId);
406
423
  let planActive = false;
407
424
  let planWanted = null;
408
425
  let goal = null;
@@ -524,15 +541,14 @@ function buildApi(ctx, sideChats, getSettings) {
524
541
  return { accepted: true };
525
542
  };
526
543
  /** List the side chats launched by one parent conversation. */
527
- const list = (payload) => {
544
+ const list = async (payload) => {
528
545
  const parentSessionId = requireString(payload, "parentSessionId");
529
546
  const items = [];
530
547
  for (const record of sideChats.values()) {
531
548
  if (record.parentSessionId !== parentSessionId) continue;
532
- const agent = record.handle.agent;
533
- const status = agent.status;
549
+ const status = record.handle.agent.status;
534
550
  const statusRunning = status === "running";
535
- const eventRunningSince = openTurnStart(sessionEvents(agent.session));
551
+ const eventRunningSince = status === void 0 ? openTurnStart(await sessionEvents(record.childId)) : void 0;
536
552
  const running = statusRunning || status === void 0 && eventRunningSince !== void 0;
537
553
  const runningSince = running ? eventRunningSince ?? Date.now() : void 0;
538
554
  items.push({
@@ -562,7 +578,8 @@ function buildApi(ctx, sideChats, getSettings) {
562
578
  maxImageBytes: l.maxImageBytes,
563
579
  maxImagesPerMessage: l.maxImagesPerMessage,
564
580
  maxMessageImageBytes: l.maxMessageImageBytes,
565
- maxImagePixels: l.maxImagePixels
581
+ maxImagePixels: l.maxImagePixels,
582
+ ...l.maxImageDimension === void 0 ? {} : { maxImageDimension: l.maxImageDimension }
566
583
  };
567
584
  };
568
585
  /** Read one durable image's bytes for transcript rendering. */
@@ -707,15 +724,26 @@ function buildApi(ctx, sideChats, getSettings) {
707
724
  };
708
725
  /** Permission-preset options for the side-chat selector. */
709
726
  const permissions = () => {
710
- const select = ctx.permissionPresets.selectFor({});
711
- return {
712
- options: select.options.map((o) => ({
713
- value: o.value,
714
- name: o.name,
715
- ...o.description === void 0 ? {} : { description: o.description }
716
- })),
717
- current: select.currentValue
727
+ const presets = ctx.permissionPresets;
728
+ const toOption = (o) => ({
729
+ value: o.value,
730
+ name: o.name,
731
+ ...o.description === void 0 ? {} : { description: o.description }
732
+ });
733
+ const catalog = presets.catalog;
734
+ if (typeof catalog === "function") return {
735
+ options: catalog.call(presets).options.map(toOption),
736
+ current: presets.defaultPreset ?? "custom"
718
737
  };
738
+ const selectFor = presets.selectFor;
739
+ if (typeof selectFor === "function") {
740
+ const select = selectFor.call(presets, {});
741
+ return {
742
+ options: select.options.map(toOption),
743
+ current: select.currentValue
744
+ };
745
+ }
746
+ throw new SidechatError("permissions-unavailable", "the permission-presets service exposes neither catalog() nor selectFor()", 503);
719
747
  };
720
748
  return {
721
749
  "sidechat.start": start,
@@ -796,6 +824,13 @@ function apply(ctx) {
796
824
  sideChats.clear();
797
825
  };
798
826
  }, "dsh-side-chat: dispose side chats");
827
+ ctx.effect(() => {
828
+ (async () => {
829
+ for (const record of await readRecords()) try {
830
+ await ctx.workspaceRegistry.archiveSession(record.childId);
831
+ } catch {}
832
+ })();
833
+ }, "dsh-side-chat: re-archive recorded side chats");
799
834
  ctx.effect(() => ctx.webServer.register({
800
835
  kind: "prefix",
801
836
  path: "/sidechat/api",
@@ -161,6 +161,7 @@ export declare const api: {
161
161
  maxImagesPerMessage: number;
162
162
  maxMessageImageBytes: number;
163
163
  maxImagePixels: number;
164
+ maxImageDimension?: number;
164
165
  }>>;
165
166
  attachment: (args: {
166
167
  childId: string;
@@ -52,6 +52,7 @@ export declare const zh: {
52
52
  'image.unsupported': string;
53
53
  'image.tooMany': string;
54
54
  'image.fileTooLarge': string;
55
+ 'image.dimensionTooLarge': string;
55
56
  settingsNav: string;
56
57
  'settings.lookupTitle': string;
57
58
  'settings.lookupDesc': string;
@@ -147,6 +148,7 @@ export declare const en: {
147
148
  'image.unsupported': string;
148
149
  'image.tooMany': string;
149
150
  'image.fileTooLarge': string;
151
+ 'image.dimensionTooLarge': string;
150
152
  settingsNav: string;
151
153
  'settings.lookupTitle': string;
152
154
  'settings.lookupDesc': string;
@@ -38,17 +38,14 @@ export interface SideSessionEvent {
38
38
  surfaceOp?: 'append' | 'replace' | 'remove';
39
39
  sourceEventSeqs?: number[];
40
40
  }
41
- /** The live session face the host reads (header + append-only event log). */
41
+ /**
42
+ * The live session face the host reads. Event reads go through
43
+ * `sessionQuery.readSession` instead: DSH 0.1.6 deprecated the Session's
44
+ * synchronous event getters (`events` before 0.1.5, `snapshotEvents` after).
45
+ */
42
46
  export interface SideSession {
43
47
  id: string;
44
48
  header: SideSessionHeader;
45
- /**
46
- * 0.1.2-era event getter. Removed in 0.1.5 (snapshotEvents takes over), so
47
- * host code reads through `sessionEvents()` which prefers the new method.
48
- */
49
- events?: readonly SideSessionEvent[];
50
- /** 0.1.5+ snapshot of the full event log; absent on 0.1.2-era sessions. */
51
- snapshotEvents?: (fromSeq?: number, toSeqExclusive?: number) => readonly SideSessionEvent[];
52
49
  requestHeader?: () => {
53
50
  config?: {
54
51
  provider?: string;
@@ -198,12 +195,6 @@ export interface SideSessionLogSnapshot {
198
195
  export interface SideSessionQuery {
199
196
  readSession(sessionId: string): Promise<SideSessionLogSnapshot>;
200
197
  }
201
- /** Sandbox mode vocabulary (read from the session override). */
202
- export type SideSandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access';
203
- /** The sandbox policy face. */
204
- export interface SideSandboxPolicy {
205
- overrideOf(session: SideSession): SideSandboxMode | undefined;
206
- }
207
198
  /** One permission preset option. */
208
199
  export interface SidePresetOption {
209
200
  value: string;
@@ -214,7 +205,14 @@ export interface SidePresetOption {
214
205
  export interface SidePermissionPresets {
215
206
  current(session: SideSession): string;
216
207
  set(session: SideSession, name: string): void;
217
- selectFor(state: unknown): {
208
+ /** 0.1.6+: process-level catalog of selectable presets. */
209
+ catalog?(): {
210
+ options: SidePresetOption[];
211
+ };
212
+ /** 0.1.6+: the preset selected as the default for future sessions. */
213
+ defaultPreset?: string;
214
+ /** 0.1.5 and earlier: combined catalog plus current value. Removed in 0.1.6. */
215
+ selectFor?(state: unknown): {
218
216
  options: SidePresetOption[];
219
217
  currentValue: string;
220
218
  };
@@ -350,6 +348,8 @@ export interface SideAttachmentStore {
350
348
  maxImagesPerMessage: number;
351
349
  maxMessageImageBytes: number;
352
350
  maxImagePixels: number;
351
+ /** 0.1.6+ per-image intrinsic width/height ceiling; absent on earlier lines. */
352
+ maxImageDimension?: number;
353
353
  mediaTypes: readonly string[];
354
354
  };
355
355
  validateImage(input: {
@@ -402,7 +402,6 @@ declare module 'cordis' {
402
402
  agents: SideAgentsService;
403
403
  workspaceRegistry: SideWorkspaceRegistry;
404
404
  sessionQuery: SideSessionQuery;
405
- sandboxPolicy: SideSandboxPolicy;
406
405
  permissionPresets: SidePermissionPresets;
407
406
  agentPresets: SideAgentPresets;
408
407
  llm: SideLlm;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-side-chat-plus",
3
- "version": "0.3.4",
3
+ "version": "0.4.0-alpha.1",
4
4
  "description": "侧边聊天 Plus (Side chat Plus) — DSH web plugin: select text in a conversation and ask it in a per-session side chat (hidden ordinary session, own right-side panel, inherits model/effort/permissions, lookup toggle).",
5
5
  "private": false,
6
6
  "type": "module",
@@ -67,47 +67,47 @@
67
67
  "schemastery": "^3.18.0"
68
68
  },
69
69
  "peerDependencies": {
70
- "@deepseek-ai/dsh-agent": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-alpha.1",
71
- "@deepseek-ai/dsh-attachment": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-alpha.1",
72
- "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-alpha.1",
73
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-alpha.1",
74
- "@deepseek-ai/dsh-client-ui-renderer": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-alpha.1",
75
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-alpha.1",
76
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-alpha.1",
77
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-alpha.1",
78
- "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-alpha.1",
79
- "@deepseek-ai/dsh-llm": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-alpha.1",
80
- "@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-alpha.1",
81
- "@deepseek-ai/dsh-session-query": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-alpha.1",
82
- "@deepseek-ai/dsh-workspace": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-alpha.1",
83
- "@deepseek-ai/dsh-permission-presets": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-alpha.1",
84
- "@deepseek-ai/dsh-sandbox-policy": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-alpha.1",
85
- "@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-alpha.1",
86
- "@deepseek-ai/dsh-settings": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-alpha.1",
87
- "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-alpha.1",
70
+ "@deepseek-ai/dsh-agent": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-alpha.1 || ^0.1.6-0",
71
+ "@deepseek-ai/dsh-attachment": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-alpha.1 || ^0.1.6-0",
72
+ "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-alpha.1 || ^0.1.6-0",
73
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-alpha.1 || ^0.1.6-0",
74
+ "@deepseek-ai/dsh-client-ui-renderer": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-alpha.1 || ^0.1.6-0",
75
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-alpha.1 || ^0.1.6-0",
76
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-alpha.1 || ^0.1.6-0",
77
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-alpha.1 || ^0.1.6-0",
78
+ "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-alpha.1 || ^0.1.6-0",
79
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-alpha.1 || ^0.1.6-0",
80
+ "@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-alpha.1 || ^0.1.6-0",
81
+ "@deepseek-ai/dsh-session-query": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-alpha.1 || ^0.1.6-0",
82
+ "@deepseek-ai/dsh-workspace": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-alpha.1 || ^0.1.6-0",
83
+ "@deepseek-ai/dsh-permission-presets": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-alpha.1 || ^0.1.6-0",
84
+ "@deepseek-ai/dsh-sandbox-policy": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-alpha.1 || ^0.1.6-0",
85
+ "@deepseek-ai/dsh-agent-presets": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-alpha.1 || ^0.1.6-0",
86
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-alpha.1 || ^0.1.6-0",
87
+ "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6 || ^0.1.1-0 || ^0.1.2-0 || ^0.1.5-alpha.1 || ^0.1.6-0",
88
88
  "cordis": "^4.0.0-rc.7",
89
89
  "react": "^18.2.0",
90
90
  "react-dom": "^18.2.0"
91
91
  },
92
92
  "devDependencies": {
93
- "@deepseek-ai/dsh-agent": "^0.1.5-alpha.2",
94
- "@deepseek-ai/dsh-attachment": "^0.1.5-alpha.2",
95
- "@deepseek-ai/dsh-client-locale": "^0.1.5-alpha.2",
96
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-alpha.2",
97
- "@deepseek-ai/dsh-client-ui-renderer": "^0.1.5-alpha.2",
98
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.5-alpha.2",
99
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.5-alpha.2",
100
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-alpha.2",
101
- "@deepseek-ai/dsh-host-webserver": "^0.1.5-alpha.2",
102
- "@deepseek-ai/dsh-llm": "^0.1.5-alpha.2",
103
- "@deepseek-ai/dsh-session": "^0.1.5-alpha.2",
104
- "@deepseek-ai/dsh-session-query": "^0.1.5-alpha.2",
105
- "@deepseek-ai/dsh-workspace": "^0.1.5-alpha.2",
106
- "@deepseek-ai/dsh-permission-presets": "^0.1.5-alpha.2",
107
- "@deepseek-ai/dsh-sandbox-policy": "^0.1.5-alpha.2",
108
- "@deepseek-ai/dsh-agent-presets": "^0.1.5-alpha.2",
109
- "@deepseek-ai/dsh-settings": "^0.1.5-alpha.2",
110
- "@deepseek-ai/dsh-home-paths": "^0.1.5-alpha.2",
93
+ "@deepseek-ai/dsh-agent": "^0.1.5-alpha.2 || ^0.1.6-0",
94
+ "@deepseek-ai/dsh-attachment": "^0.1.5-alpha.2 || ^0.1.6-0",
95
+ "@deepseek-ai/dsh-client-locale": "^0.1.5-alpha.2 || ^0.1.6-0",
96
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-alpha.2 || ^0.1.6-0",
97
+ "@deepseek-ai/dsh-client-ui-renderer": "^0.1.5-alpha.2 || ^0.1.6-0",
98
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.5-alpha.2 || ^0.1.6-0",
99
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.5-alpha.2 || ^0.1.6-0",
100
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-alpha.2 || ^0.1.6-0",
101
+ "@deepseek-ai/dsh-host-webserver": "^0.1.5-alpha.2 || ^0.1.6-0",
102
+ "@deepseek-ai/dsh-llm": "^0.1.5-alpha.2 || ^0.1.6-0",
103
+ "@deepseek-ai/dsh-session": "^0.1.5-alpha.2 || ^0.1.6-0",
104
+ "@deepseek-ai/dsh-session-query": "^0.1.5-alpha.2 || ^0.1.6-0",
105
+ "@deepseek-ai/dsh-workspace": "^0.1.5-alpha.2 || ^0.1.6-0",
106
+ "@deepseek-ai/dsh-permission-presets": "^0.1.5-alpha.2 || ^0.1.6-0",
107
+ "@deepseek-ai/dsh-sandbox-policy": "^0.1.5-alpha.2 || ^0.1.6-0",
108
+ "@deepseek-ai/dsh-agent-presets": "^0.1.5-alpha.2 || ^0.1.6-0",
109
+ "@deepseek-ai/dsh-settings": "^0.1.5-alpha.2 || ^0.1.6-0",
110
+ "@deepseek-ai/dsh-home-paths": "^0.1.5-alpha.2 || ^0.1.6-0",
111
111
  "@types/node": "^24.0.0",
112
112
  "@types/react": "~18.3.1",
113
113
  "@types/react-dom": "~18.3.1",
@@ -120,7 +120,8 @@
120
120
  "vitest": "^4.1.8"
121
121
  },
122
122
  "scripts": {
123
- "build": "node -e \"require('node:fs').rmSync('lib',{recursive:true,force:true})\" && tsc -p tsconfig.build.json && tsdown",
123
+ "build": "node -e \"require('node:fs').rmSync('lib',{recursive:true,force:true})\" && tsc -p tsconfig.build.json && tsdown && node scripts/check-seed-exports.mjs",
124
+ "check:seed": "node scripts/check-seed-exports.mjs",
124
125
  "typecheck": "tsc --noEmit",
125
126
  "bundle": "tsdown",
126
127
  "watch": "tsdown --watch",
package/src/client/api.ts CHANGED
@@ -93,7 +93,7 @@ export const api = {
93
93
  call<{ accepted: true }>('sidechat.inject', args),
94
94
  directory: () => call<SidechatDirectory>('sidechat.directory', {}),
95
95
  permissions: () => call<SidechatPermissions>('sidechat.permissions', {}),
96
- limits: () => call<{ mediaTypes: string[]; maxImageBytes: number; maxImagesPerMessage: number; maxMessageImageBytes: number; maxImagePixels: number }>('sidechat.limits', {}),
96
+ limits: () => call<{ mediaTypes: string[]; maxImageBytes: number; maxImagesPerMessage: number; maxMessageImageBytes: number; maxImagePixels: number; maxImageDimension?: number }>('sidechat.limits', {}),
97
97
  attachment: (args: { childId: string; attachmentId: string }) =>
98
98
  call<{ mediaType: string; data: string }>('sidechat.attachment', args),
99
99
  inherit: (args: { parentSessionId: string }) =>
@@ -14,7 +14,7 @@ import {
14
14
  IconChevronDownOutline14,
15
15
  IconChevronRightOutline14,
16
16
  IconPanelLeftOutline16,
17
- IconSendOutline16,
17
+ IconSendOutline14,
18
18
  IconStopFill16,
19
19
  IconThinkOutline14,
20
20
  MarkdownText,
@@ -264,6 +264,22 @@ function createDraftImages(files: readonly File[]): ComposerAttachment[] {
264
264
  })
265
265
  }
266
266
 
267
+ /**
268
+ * Read one image file's intrinsic pixel size, releasing the decoded bitmap.
269
+ * Returns null when the browser cannot decode it: the host's `validateImage`
270
+ * stays the authority either way.
271
+ */
272
+ async function imageSize(file: File): Promise<{ width: number; height: number } | null> {
273
+ try {
274
+ const bitmap = await createImageBitmap(file)
275
+ const size = { width: bitmap.width, height: bitmap.height }
276
+ bitmap.close()
277
+ return size
278
+ } catch {
279
+ return null
280
+ }
281
+ }
282
+
267
283
  /** Revoke one draft image's preview URL. */
268
284
  function releaseDraftImage(attachment: ComposerAttachment): void {
269
285
  URL.revokeObjectURL(attachment.previewUrl)
@@ -952,7 +968,7 @@ function SidechatPanel(props: {
952
968
  const [now, setNow] = useState(() => Date.now())
953
969
  const [dragActive, setDragActive] = useState(false)
954
970
  const [lightbox, setLightbox] = useState<ComposerAttachment | null>(null)
955
- const [limits, setLimits] = useState<{ mediaTypes: string[]; maxImageBytes: number; maxImagesPerMessage: number; maxMessageImageBytes: number } | null>(null)
971
+ const [limits, setLimits] = useState<{ mediaTypes: string[]; maxImageBytes: number; maxImagesPerMessage: number; maxMessageImageBytes: number; maxImagePixels: number; maxImageDimension?: number } | null>(null)
956
972
  /** Index of the assistant message whose "summarize then insert" is in flight. */
957
973
  const [summarizingIndex, setSummarizingIndex] = useState<number | null>(null)
958
974
  /** Which question-dialog item is being brought into the side chat ('all' or an option label). */
@@ -1214,26 +1230,41 @@ function SidechatPanel(props: {
1214
1230
  props.store.patch({ error: props.t('image.unsupported') })
1215
1231
  return
1216
1232
  }
1217
- try {
1218
- if (limits !== null) {
1219
- if (images.some((f) => !limits.mediaTypes.includes(f.type))) {
1220
- props.store.patch({ error: props.t('image.unsupported') })
1221
- return
1222
- }
1223
- if (panel.attachments.length + images.length > limits.maxImagesPerMessage) {
1224
- props.store.patch({ error: props.t('image.tooMany') })
1225
- return
1226
- }
1227
- if (images.some((f) => f.size > limits.maxImageBytes)) {
1228
- props.store.patch({ error: props.t('image.fileTooLarge') })
1229
- return
1233
+ void (async () => {
1234
+ try {
1235
+ if (limits !== null) {
1236
+ if (images.some((f) => !limits.mediaTypes.includes(f.type))) {
1237
+ props.store.patch({ error: props.t('image.unsupported') })
1238
+ return
1239
+ }
1240
+ if (panel.attachments.length + images.length > limits.maxImagesPerMessage) {
1241
+ props.store.patch({ error: props.t('image.tooMany') })
1242
+ return
1243
+ }
1244
+ if (images.some((f) => f.size > limits.maxImageBytes)) {
1245
+ props.store.patch({ error: props.t('image.fileTooLarge') })
1246
+ return
1247
+ }
1248
+ // DSH 0.1.6 added a per-image intrinsic width/height ceiling; older
1249
+ // lines omit it. Decoding is async, so this check is awaited rather
1250
+ // than folded into the size-type ones above.
1251
+ const maxDimension = limits.maxImageDimension
1252
+ if (typeof maxDimension === 'number' && maxDimension > 0) {
1253
+ for (const file of images) {
1254
+ const size = await imageSize(file)
1255
+ if (size !== null && (size.width > maxDimension || size.height > maxDimension)) {
1256
+ props.store.patch({ error: props.t('image.dimensionTooLarge') })
1257
+ return
1258
+ }
1259
+ }
1260
+ }
1230
1261
  }
1262
+ const created = createDraftImages(images)
1263
+ props.store.patch({ attachments: [...panel.attachments, ...created], error: null })
1264
+ } catch (error) {
1265
+ props.store.patch({ error: error instanceof Error ? error.message : String(error) })
1231
1266
  }
1232
- const created = createDraftImages(images)
1233
- props.store.patch({ attachments: [...panel.attachments, ...created], error: null })
1234
- } catch (error) {
1235
- props.store.patch({ error: error instanceof Error ? error.message : String(error) })
1236
- }
1267
+ })()
1237
1268
  }, [limits, panel.attachments, props.store, props.t])
1238
1269
 
1239
1270
  // Load the deployment image policy once (fast-path checks mirror the host).
@@ -1603,7 +1634,7 @@ function SidechatPanel(props: {
1603
1634
  aria-label={activeRunning ? props.t('panel.stop') : props.t('panel.send')}
1604
1635
  onClick={activeRunning ? stop : send}
1605
1636
  >
1606
- {activeRunning ? <IconStopFill16 size={16} /> : <IconSendOutline16 size={16} />}
1637
+ {activeRunning ? <IconStopFill16 size={16} /> : <IconSendOutline14 size={16} />}
1607
1638
  </button>
1608
1639
  </Tooltip>
1609
1640
  </div>
@@ -53,6 +53,7 @@ export const zh = {
53
53
  'image.unsupported': '不支持的图片格式',
54
54
  'image.tooMany': '图片数量超出限制',
55
55
  'image.fileTooLarge': '图片过大',
56
+ 'image.dimensionTooLarge': '图片尺寸超出限制',
56
57
  'settingsNav': '侧边聊天',
57
58
  'settings.lookupTitle': '默认勾选「需要时从工作区 / 主会话查信息」',
58
59
  'settings.lookupDesc': '新会话的侧边聊天默认打开此开关;可在每次聊天中临时更改',
@@ -149,6 +150,7 @@ export const en = {
149
150
  'image.unsupported': 'Unsupported image type',
150
151
  'image.tooMany': 'Too many images',
151
152
  'image.fileTooLarge': 'Image too large',
153
+ 'image.dimensionTooLarge': 'Image dimensions exceed the limit',
152
154
  'settingsNav': 'Side chat',
153
155
  'settings.lookupTitle': 'Default on: "look up workspace / parent when needed"',
154
156
  'settings.lookupDesc': 'New conversations start with this switch on; you can still change it per chat',
@@ -43,17 +43,14 @@ export interface SideSessionEvent {
43
43
  sourceEventSeqs?: number[]
44
44
  }
45
45
 
46
- /** The live session face the host reads (header + append-only event log). */
46
+ /**
47
+ * The live session face the host reads. Event reads go through
48
+ * `sessionQuery.readSession` instead: DSH 0.1.6 deprecated the Session's
49
+ * synchronous event getters (`events` before 0.1.5, `snapshotEvents` after).
50
+ */
47
51
  export interface SideSession {
48
52
  id: string
49
53
  header: SideSessionHeader
50
- /**
51
- * 0.1.2-era event getter. Removed in 0.1.5 (snapshotEvents takes over), so
52
- * host code reads through `sessionEvents()` which prefers the new method.
53
- */
54
- events?: readonly SideSessionEvent[]
55
- /** 0.1.5+ snapshot of the full event log; absent on 0.1.2-era sessions. */
56
- snapshotEvents?: (fromSeq?: number, toSeqExclusive?: number) => readonly SideSessionEvent[]
57
54
  requestHeader?: () => { config?: { provider?: string; model?: string; reasoningEffort?: string; maxTokens?: number } } | undefined
58
55
  }
59
56
 
@@ -212,14 +209,6 @@ export interface SideSessionQuery {
212
209
  readSession(sessionId: string): Promise<SideSessionLogSnapshot>
213
210
  }
214
211
 
215
- /** Sandbox mode vocabulary (read from the session override). */
216
- export type SideSandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access'
217
-
218
- /** The sandbox policy face. */
219
- export interface SideSandboxPolicy {
220
- overrideOf(session: SideSession): SideSandboxMode | undefined
221
- }
222
-
223
212
  /** One permission preset option. */
224
213
  export interface SidePresetOption {
225
214
  value: string
@@ -231,7 +220,12 @@ export interface SidePresetOption {
231
220
  export interface SidePermissionPresets {
232
221
  current(session: SideSession): string
233
222
  set(session: SideSession, name: string): void
234
- selectFor(state: unknown): { options: SidePresetOption[]; currentValue: string }
223
+ /** 0.1.6+: process-level catalog of selectable presets. */
224
+ catalog?(): { options: SidePresetOption[] }
225
+ /** 0.1.6+: the preset selected as the default for future sessions. */
226
+ defaultPreset?: string
227
+ /** 0.1.5 and earlier: combined catalog plus current value. Removed in 0.1.6. */
228
+ selectFor?(state: unknown): { options: SidePresetOption[]; currentValue: string }
235
229
  }
236
230
 
237
231
  /** The agent presets face (composeFrom joins a child to the parent's composition). */
@@ -356,6 +350,8 @@ export interface SideAttachmentStore {
356
350
  maxImagesPerMessage: number
357
351
  maxMessageImageBytes: number
358
352
  maxImagePixels: number
353
+ /** 0.1.6+ per-image intrinsic width/height ceiling; absent on earlier lines. */
354
+ maxImageDimension?: number
359
355
  mediaTypes: readonly string[]
360
356
  }
361
357
  validateImage(input: { data: Uint8Array; mediaType: string; name?: string }): Promise<void>
@@ -394,7 +390,6 @@ declare module 'cordis' {
394
390
  agents: SideAgentsService
395
391
  workspaceRegistry: SideWorkspaceRegistry
396
392
  sessionQuery: SideSessionQuery
397
- sandboxPolicy: SideSandboxPolicy
398
393
  permissionPresets: SidePermissionPresets
399
394
  agentPresets: SideAgentPresets
400
395
  llm: SideLlm