dsh-side-chat-plus 0.4.1 → 0.4.3

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
@@ -336,20 +336,24 @@ function buildApi(ctx, sideChats, getSettings) {
336
336
  });
337
337
  else {
338
338
  const data = decodeBase64(part.data);
339
- await ctx.attachments.validateImage({
340
- data,
341
- mediaType: part.mediaType,
342
- ...part.name === void 0 ? {} : { name: part.name }
343
- });
344
- const attachment = await ctx.attachments.saveImage({
345
- data,
346
- mediaType: part.mediaType,
347
- ...part.name === void 0 ? {} : { name: part.name }
348
- });
349
- blocks.push({
350
- type: "image",
351
- attachment
352
- });
339
+ try {
340
+ await ctx.attachments.validateImage({
341
+ data,
342
+ mediaType: part.mediaType,
343
+ ...part.name === void 0 ? {} : { name: part.name }
344
+ });
345
+ const attachment = await ctx.attachments.saveImage({
346
+ data,
347
+ mediaType: part.mediaType,
348
+ ...part.name === void 0 ? {} : { name: part.name }
349
+ });
350
+ blocks.push({
351
+ type: "image",
352
+ attachment
353
+ });
354
+ } catch (error) {
355
+ throw new SidechatError("bad-image", error instanceof Error ? error.message : String(error), 400);
356
+ }
353
357
  }
354
358
  blocks.push({
355
359
  type: "text",
@@ -512,6 +516,7 @@ function buildApi(ctx, sideChats, getSettings) {
512
516
  const content = requireContent(payload);
513
517
  const lookupEnabled = optionalBoolean(payload, "lookupEnabled");
514
518
  const parent = parentOf(parentSessionId);
519
+ const blocks = await durableContent(content, lookupEnabled);
515
520
  const record = payload;
516
521
  const parentConfig = parent.session.requestHeader?.()?.config;
517
522
  const parentProvider = parentConfig?.provider ?? parent.options.provider ?? "";
@@ -580,7 +585,7 @@ function buildApi(ctx, sideChats, getSettings) {
580
585
  };
581
586
  sideChats.set(childId, child);
582
587
  persist();
583
- await deliver(child, userMessage(await durableContent(content, lookupEnabled)));
588
+ await deliver(child, userMessage(blocks));
584
589
  return {
585
590
  childId,
586
591
  provider,
@@ -637,7 +642,13 @@ function buildApi(ctx, sideChats, getSettings) {
637
642
  /** Fold one side chat's transcript. */
638
643
  const history = async (payload) => {
639
644
  const childId = requireString(payload, "childId");
640
- return { messages: foldTranscript((await ctx.sessionQuery.readSession(childId)).events) };
645
+ let events;
646
+ try {
647
+ events = (await ctx.sessionQuery.readSession(childId)).events;
648
+ } catch (error) {
649
+ throw new SidechatError("not-found", error instanceof Error ? error.message : String(error), 404);
650
+ }
651
+ return { messages: foldTranscript(events) };
641
652
  };
642
653
  /** The deployment-resolved image policy (for client-side fast-path checks). */
643
654
  const limits = () => {
@@ -854,6 +865,21 @@ function buildApi(ctx, sideChats, getSettings) {
854
865
  }
855
866
  };
856
867
  }
868
+ /**
869
+ * Resolve this plugin's settings namespace: the Loader entry id that owns the
870
+ * running fiber. Both reads are defensive because this package builds against
871
+ * its own cordis, so the upstream `Fiber.entry` augmentation is not visible
872
+ * here. `undefined` (no owning entry, e.g. a bare programmatic mount) leaves
873
+ * the settings face unbound: the plugin still runs, only its preferences stay
874
+ * unavailable instead of being written to a namespace nobody owns.
875
+ */
876
+ function settingsNamespaceOf(ctx) {
877
+ const owner = ctx.fiber?.entry;
878
+ const fromFiber = owner?.options?.id ?? owner?.id;
879
+ if (typeof fromFiber === "string" && fromFiber !== "") return fromFiber;
880
+ const located = ctx.get("loader")?.locate?.(ctx.fiber);
881
+ return typeof located === "string" && located !== "" ? located : void 0;
882
+ }
857
883
  /** Read the connection row's trustedHosts, or empty (loopback-only fence). */
858
884
  function trustedHostsOf(ctx) {
859
885
  const loader = ctx.get("loader");
@@ -865,7 +891,7 @@ function apply(ctx) {
865
891
  const sideChats = /* @__PURE__ */ new Map();
866
892
  let settingsFace;
867
893
  ctx.inject(["settings"], (sctx) => {
868
- const ns = ctx.entry?.id;
894
+ const ns = settingsNamespaceOf(ctx);
869
895
  if (ns === void 0) return;
870
896
  sctx.effect(() => sctx.settings.configure({ auto: false }, ctx.fiber));
871
897
  const viewOf = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-side-chat-plus",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
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",
@@ -92,23 +92,23 @@
92
92
  "@deepseek-ai/dsh-agent-preset-registry": "^0.1.7-0"
93
93
  },
94
94
  "devDependencies": {
95
- "@deepseek-ai/dsh-agent": "0.1.7-alpha.1",
96
- "@deepseek-ai/dsh-attachment": "0.1.7-alpha.1",
97
- "@deepseek-ai/dsh-client-locale": "0.1.7-alpha.1",
98
- "@deepseek-ai/dsh-client-ui-conversation": "0.1.7-alpha.1",
99
- "@deepseek-ai/dsh-client-ui-renderer": "0.1.7-alpha.1",
100
- "@deepseek-ai/dsh-client-ui-slots": "0.1.7-alpha.1",
101
- "@deepseek-ai/dsh-client-ui-settings": "0.1.7-alpha.1",
102
- "@deepseek-ai/dsh-client-ui-primitives": "0.1.7-alpha.1",
103
- "@deepseek-ai/dsh-host-webserver": "0.1.7-alpha.1",
104
- "@deepseek-ai/dsh-llm": "0.1.7-alpha.1",
105
- "@deepseek-ai/dsh-session": "0.1.7-alpha.1",
106
- "@deepseek-ai/dsh-session-query": "0.1.7-alpha.1",
107
- "@deepseek-ai/dsh-workspace": "0.1.7-alpha.1",
108
- "@deepseek-ai/dsh-permission-presets": "0.1.7-alpha.1",
109
- "@deepseek-ai/dsh-sandbox-policy": "0.1.7-alpha.1",
110
- "@deepseek-ai/dsh-settings": "0.1.7-alpha.1",
111
- "@deepseek-ai/dsh-home-paths": "0.1.7-alpha.1",
95
+ "@deepseek-ai/dsh-agent": "0.1.7-rc.1",
96
+ "@deepseek-ai/dsh-attachment": "0.1.7-rc.1",
97
+ "@deepseek-ai/dsh-client-locale": "0.1.7-rc.1",
98
+ "@deepseek-ai/dsh-client-ui-conversation": "0.1.7-rc.1",
99
+ "@deepseek-ai/dsh-client-ui-renderer": "0.1.7-rc.1",
100
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.7-rc.1",
101
+ "@deepseek-ai/dsh-client-ui-settings": "0.1.7-rc.1",
102
+ "@deepseek-ai/dsh-client-ui-primitives": "0.1.7-rc.1",
103
+ "@deepseek-ai/dsh-host-webserver": "0.1.7-rc.1",
104
+ "@deepseek-ai/dsh-llm": "0.1.7-rc.1",
105
+ "@deepseek-ai/dsh-session": "0.1.7-rc.1",
106
+ "@deepseek-ai/dsh-session-query": "0.1.7-rc.1",
107
+ "@deepseek-ai/dsh-workspace": "0.1.7-rc.1",
108
+ "@deepseek-ai/dsh-permission-presets": "0.1.7-rc.1",
109
+ "@deepseek-ai/dsh-sandbox-policy": "0.1.7-rc.1",
110
+ "@deepseek-ai/dsh-settings": "0.1.7-rc.1",
111
+ "@deepseek-ai/dsh-home-paths": "0.1.7-rc.1",
112
112
  "@types/node": "^24.0.0",
113
113
  "@types/react": "~18.3.1",
114
114
  "@types/react-dom": "~18.3.1",
@@ -119,8 +119,8 @@
119
119
  "tsdown": "^0.22.2",
120
120
  "typescript": "^5.6.0",
121
121
  "vitest": "^4.1.8",
122
- "@deepseek-ai/dsh-agent-preset-registry": "0.1.7-alpha.1",
123
- "@deepseek-ai/dsh-session-format-catalog": "0.1.7-alpha.1"
122
+ "@deepseek-ai/dsh-agent-preset-registry": "0.1.7-rc.1",
123
+ "@deepseek-ai/dsh-session-format-catalog": "0.1.7-rc.1"
124
124
  },
125
125
  "scripts": {
126
126
  "build": "node -e \"require('node:fs').rmSync('lib',{recursive:true,force:true})\" && tsc -p tsconfig.build.json && tsdown && node scripts/check-seed-exports.mjs",
@@ -90,6 +90,12 @@ interface SidechatSnapshot {
90
90
  mainQuestion: SideQuestionItem[] | null
91
91
  /** Question ids the user deleted from the panel list. */
92
92
  dismissedQuestionIds: string[]
93
+ /**
94
+ * Whether this deployment offers the built-in right sidebar. False means
95
+ * `panelHome: 'sidebar-right'` cannot be honoured and the panel falls back to
96
+ * the classic floating container instead of becoming unreachable.
97
+ */
98
+ dockAvailable: boolean
93
99
  }
94
100
 
95
101
  /** The whole browser-side store (one per activation). */
@@ -106,6 +112,8 @@ interface SidechatStore {
106
112
  closePanel(): void
107
113
  /** Install (or clear) the dock-mode launcher; openPanel() fires it after opening. */
108
114
  setDockLaunch(fn: (() => void) | null): void
115
+ /** Record whether the built-in right sidebar exists in this deployment. */
116
+ setDockAvailable(available: boolean): void
109
117
  setActive(childId: string): void
110
118
  patch(partial: Partial<PanelState>): void
111
119
  }
@@ -143,6 +151,10 @@ function createStore(): SidechatStore {
143
151
  let prefs: SubchatPrefs = { ...SUBCHAT_PREFS_DEFAULTS }
144
152
  let mainQuestion: SideQuestionItem[] | null = null
145
153
  let dismissedQuestionIds: string[] = []
154
+ // Optimistic until the dock effect has inspected the services: a deployment
155
+ // whose sidebar services are missing flips this to false and the panel
156
+ // renders floating instead of nowhere.
157
+ let dockAvailable = true
146
158
  // Per-conversation panel state so switching away and back restores the side
147
159
  // chats instead of resetting them. The side chats stay live on the host, so
148
160
  // the client must remember each conversation's open panel + active child.
@@ -153,10 +165,10 @@ function createStore(): SidechatStore {
153
165
  let dockLaunch: (() => void) | null = null
154
166
  // Cached snapshot: useSyncExternalStore compares identity, so the object is
155
167
  // only rebuilt on a mutation — never inside getSnapshot itself.
156
- let snapshot: SidechatSnapshot = { current, panel, anchor, prefs, mainQuestion, dismissedQuestionIds }
168
+ let snapshot: SidechatSnapshot = { current, panel, anchor, prefs, mainQuestion, dismissedQuestionIds, dockAvailable }
157
169
 
158
170
  const notify = (): void => {
159
- snapshot = { current, panel, anchor, prefs, mainQuestion, dismissedQuestionIds }
171
+ snapshot = { current, panel, anchor, prefs, mainQuestion, dismissedQuestionIds, dockAvailable }
160
172
  for (const fn of [...listeners]) fn()
161
173
  }
162
174
 
@@ -214,6 +226,11 @@ function createStore(): SidechatStore {
214
226
  setDockLaunch(fn) {
215
227
  dockLaunch = fn
216
228
  },
229
+ setDockAvailable(available) {
230
+ if (available === dockAvailable) return
231
+ dockAvailable = available
232
+ notify()
233
+ },
217
234
  setActive(childId) {
218
235
  panel = { ...panel, activeChildId: childId, messages: [], error: null }
219
236
  notify()
@@ -1755,7 +1772,7 @@ function SideChatShell(props: {
1755
1772
  onOpen={() => { props.store.openPanel(snap.panel.parentSessionId) }}
1756
1773
  />
1757
1774
  )}
1758
- {snap.prefs.panelHome === 'floating' && (
1775
+ {(snap.prefs.panelHome === 'floating' || !snap.dockAvailable) && (
1759
1776
  <SidechatPanel
1760
1777
  store={props.store}
1761
1778
  t={props.t}
@@ -2187,9 +2204,14 @@ export function apply(ctx: Context): void {
2187
2204
  ?? sidebarRight?.tabs
2188
2205
  if (sidebarRight === undefined || sidebarRightTabs === undefined) {
2189
2206
  phase = 'unavailable'
2207
+ // The floating container takes over (SideChatShell renders it while
2208
+ // `dockAvailable` is false); the notice only explains why the dock
2209
+ // preference is not in effect.
2210
+ store.setDockAvailable(false)
2190
2211
  store.patch({ error: translate(activeLocale, 'dock.unavailable') })
2191
2212
  return
2192
2213
  }
2214
+ store.setDockAvailable(true)
2193
2215
  const dockT = (key: SidechatLocaleKey): string => translate(activeLocale, key)
2194
2216
 
2195
2217
  const disposeType = sidebarRightTabs.register({
package/src/index.ts CHANGED
@@ -270,9 +270,16 @@ function buildApi(ctx: Context, sideChats: Map<string, SidechatRecord>, getSetti
270
270
  blocks.push({ type: 'text', text: part.text })
271
271
  } else {
272
272
  const data = decodeBase64(part.data)
273
- await ctx.attachments.validateImage({ data, mediaType: part.mediaType, ...(part.name === undefined ? {} : { name: part.name }) })
274
- const attachment = await ctx.attachments.saveImage({ data, mediaType: part.mediaType, ...(part.name === undefined ? {} : { name: part.name }) })
275
- blocks.push({ type: 'image', attachment })
273
+ try {
274
+ await ctx.attachments.validateImage({ data, mediaType: part.mediaType, ...(part.name === undefined ? {} : { name: part.name }) })
275
+ const attachment = await ctx.attachments.saveImage({ data, mediaType: part.mediaType, ...(part.name === undefined ? {} : { name: part.name }) })
276
+ blocks.push({ type: 'image', attachment })
277
+ } catch (error) {
278
+ // The attachment store rejects a malformed/oversized image with its
279
+ // own error type; report it as a client-side bad request (400) rather
280
+ // than an unexplained host fault (500).
281
+ throw new SidechatError('bad-image', error instanceof Error ? error.message : String(error), 400)
282
+ }
276
283
  }
277
284
  }
278
285
  blocks.push({ type: 'text', text: lookupEnabled ? LOOKUP_GUIDANCE : NO_LOOKUP_GUIDANCE })
@@ -445,6 +452,10 @@ function buildApi(ctx: Context, sideChats: Map<string, SidechatRecord>, getSetti
445
452
  const content = requireContent(payload)
446
453
  const lookupEnabled = optionalBoolean(payload, 'lookupEnabled')
447
454
  const parent = parentOf(parentSessionId)
455
+ // Validate and durably store the prompt's images BEFORE the side chat
456
+ // exists: a rejected image must not leave a live, empty side chat (and a
457
+ // persisted record) behind for a request that never ran.
458
+ const blocks = await durableContent(content, lookupEnabled)
448
459
 
449
460
  const record = payload as { provider?: unknown; model?: unknown; reasoningEffort?: unknown; preset?: unknown }
450
461
  const parentConfig = parent.session.requestHeader?.()?.config
@@ -539,7 +550,7 @@ function buildApi(ctx: Context, sideChats: Map<string, SidechatRecord>, getSetti
539
550
  sideChats.set(childId, child)
540
551
  persist()
541
552
 
542
- await deliver(child, userMessage(await durableContent(content, lookupEnabled)))
553
+ await deliver(child, userMessage(blocks))
543
554
  return { childId, provider, model, ...(reasoningEffort === undefined ? {} : { reasoningEffort }) }
544
555
  }
545
556
 
@@ -603,8 +614,15 @@ function buildApi(ctx: Context, sideChats: Map<string, SidechatRecord>, getSetti
603
614
  /** Fold one side chat's transcript. */
604
615
  const history = async (payload: unknown): Promise<{ messages: Array<{ role: 'user' | 'assistant'; blocks: TranscriptBlock[] }> }> => {
605
616
  const childId = requireString(payload, 'childId')
606
- const snapshot = await ctx.sessionQuery.readSession(childId)
607
- return { messages: foldTranscript(snapshot.events) }
617
+ let events: readonly SideSessionEvent[]
618
+ try {
619
+ events = (await ctx.sessionQuery.readSession(childId)).events
620
+ } catch (error) {
621
+ // A side chat whose session is gone (deleted, disposed, or an id from a
622
+ // previous run) is a client-side absence, not a host fault.
623
+ throw new SidechatError('not-found', error instanceof Error ? error.message : String(error), 404)
624
+ }
625
+ return { messages: foldTranscript(events) }
608
626
  }
609
627
 
610
628
  /** The deployment-resolved image policy (for client-side fast-path checks). */
@@ -882,6 +900,40 @@ function buildApi(ctx: Context, sideChats: Map<string, SidechatRecord>, getSetti
882
900
  }
883
901
  }
884
902
 
903
+ /**
904
+ * The Loader entry that owns a fiber, as the Loader records it. The owning
905
+ * entry lives on the FIBER (`fiber.entry = fiber.parent[Entry.key]` in
906
+ * cordis-plugin-loader), never as a plain `ctx.entry`; `Entry.options.id` is
907
+ * the raw configured id and `Entry.id` the nested-qualified one, and the
908
+ * settings service keys namespaces by the raw one
909
+ * (`configEditor.entries().find(row => row.options.id === ns)`).
910
+ */
911
+ interface SideEntryOwner {
912
+ entry?: { id?: string; options?: { id?: string } }
913
+ }
914
+
915
+ /** The loader face this plugin needs to locate its own entry (best-effort). */
916
+ interface SideLoaderFace {
917
+ locate?: (fiber?: unknown) => string | undefined
918
+ }
919
+
920
+ /**
921
+ * Resolve this plugin's settings namespace: the Loader entry id that owns the
922
+ * running fiber. Both reads are defensive because this package builds against
923
+ * its own cordis, so the upstream `Fiber.entry` augmentation is not visible
924
+ * here. `undefined` (no owning entry, e.g. a bare programmatic mount) leaves
925
+ * the settings face unbound: the plugin still runs, only its preferences stay
926
+ * unavailable instead of being written to a namespace nobody owns.
927
+ */
928
+ function settingsNamespaceOf(ctx: Context): string | undefined {
929
+ const owner = (ctx.fiber as SideEntryOwner | undefined)?.entry
930
+ const fromFiber = owner?.options?.id ?? owner?.id
931
+ if (typeof fromFiber === 'string' && fromFiber !== '') return fromFiber
932
+ const loader = ctx.get('loader') as SideLoaderFace | undefined
933
+ const located = loader?.locate?.(ctx.fiber)
934
+ return typeof located === 'string' && located !== '' ? located : undefined
935
+ }
936
+
885
937
  /** Read the connection row's trustedHosts, or empty (loopback-only fence). */
886
938
  function trustedHostsOf(ctx: Context): string[] {
887
939
  const loader = ctx.get('loader') as { entries?: () => Iterable<{ options: { name: string; config?: unknown } }> } | undefined
@@ -899,9 +951,10 @@ export function apply(ctx: Context): void {
899
951
  const sideChats = new Map<string, SidechatRecord>()
900
952
  let settingsFace: SubchatSettingsFace | undefined
901
953
 
902
- // Settings namespaces are now Loader entry ids, including user-renamed rows.
954
+ // Settings namespaces are Loader entry ids, including user-renamed rows: the
955
+ // id is read off the owning fiber (see `settingsNamespaceOf`).
903
956
  ctx.inject(['settings'], (sctx: Context) => {
904
- const ns = (ctx as Context & { entry?: { id: string } }).entry?.id
957
+ const ns = settingsNamespaceOf(ctx)
905
958
  if (ns === undefined) return
906
959
  sctx.effect(() => sctx.settings.configure({ auto: false }, ctx.fiber))
907
960
  const viewOf = (): { value?: unknown; revision?: number } => {