deepseek-harness-background 0.3.1 → 0.3.2

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
@@ -45,6 +45,12 @@ const OPACITY_MIN = 0;
45
45
  const OPACITY_MAX = 1;
46
46
  /** Default panel opacity (0..1); 1 = fully opaque official surfaces, 0 = fully transparent glass. */
47
47
  const DEFAULT_PANEL_OPACITY = .15;
48
+ /**
49
+ * Session-projection key this plugin owns in the client-visible value map —
50
+ * the host fold registers it (src/projection.ts) and the rail reads it
51
+ * (src/client/timeline.tsx); one shared constant so the two can never drift.
52
+ */
53
+ const TIMELINE_PROJECTION_KEY = "bgTimeline";
48
54
  /** Default background rendering mode. */
49
55
  const DEFAULT_FIT = "cover";
50
56
  /** Image positioning modes. `cover` fills the frame (cropping as needed); `contain` fits the whole image inside the frame (letterboxing). */
@@ -190,9 +196,11 @@ function sniffImage(bytes) {
190
196
  }
191
197
  /** Per-upload size cap (20 MiB). */
192
198
  const MAX_UPLOAD_BYTES = 20971520;
193
- /** A stored upload id is `<up-` + hex + `>`; the on-disk file appends its extension. */
199
+ /** A stored upload id is `<up-` + hex + `>`; the on-disk file appends its extension.
200
+ * Length-capped (ids are 'up-' + 24 hex chars today): the cap keeps the
201
+ * path-escape fence from admitting absurdly long filesystem probes. */
194
202
  function isUploadId(id) {
195
- return /^up-[a-f0-9]+$/.test(id);
203
+ return /^up-[a-f0-9]{1,64}$/.test(id);
196
204
  }
197
205
  /** One JSON response. */
198
206
  function json(res, status, body) {
@@ -450,6 +458,13 @@ function makeBackgroundRoutes(settings, opts = {}) {
450
458
  ...pickKnown(readSection()),
451
459
  ...pickKnown(body)
452
460
  };
461
+ if (merged.uploadId !== "" && merged.url !== "") {
462
+ json(res, 400, {
463
+ ok: false,
464
+ error: "mutually-exclusive-source"
465
+ });
466
+ return;
467
+ }
453
468
  await settings.replace(BACKGROUND_SETTINGS_NAMESPACE, merged);
454
469
  const after = readSection();
455
470
  if (before.uploadId !== "" && before.uploadId !== after.uploadId) deleteUploadFile(before.uploadId, opts.home);
@@ -565,6 +580,98 @@ function makeBackgroundRoutes(settings, opts = {}) {
565
580
  ];
566
581
  }
567
582
  //#endregion
583
+ //#region src/projection.ts
584
+ /** Preview cap so projection payloads stay small (~1-2 lines per row). */
585
+ const MAX_TEXT_CHARS = 80;
586
+ /** Join the text blocks of a host-side ContentBlock list. */
587
+ function textOf(content) {
588
+ if (!Array.isArray(content)) return "";
589
+ let out = "";
590
+ for (const block of content) if (block !== null && typeof block === "object" && block.type === "text") {
591
+ const text = block.text;
592
+ if (typeof text === "string") out += text;
593
+ }
594
+ return out.trim().slice(0, MAX_TEXT_CHARS);
595
+ }
596
+ /**
597
+ * A surface replace cuts nodes out of the model-visible surface so indexed
598
+ * user messages whose seq is cited in sourceEventSeqs must leave the index.
599
+ * SurfaceOp.replace is the OBJECT form `{ op: 'replace', start, end }`
600
+ * (core/session types) — never the bare string 'replace'; matching the whole
601
+ * value against a string can never fire.
602
+ */
603
+ function dropShadowedMessages(state, event) {
604
+ const op = event.surfaceOp;
605
+ if (op === null || typeof op !== "object" || op.op !== "replace") return state;
606
+ const shadowed = event.sourceEventSeqs;
607
+ if (!Array.isArray(shadowed) || shadowed.length === 0) return state;
608
+ const hide = /* @__PURE__ */ new Set();
609
+ for (const seq of shadowed) if (typeof seq === "number" && Number.isFinite(seq)) hide.add(seq);
610
+ if (hide.size === 0) return state;
611
+ const next = state.messages.filter((m) => !hide.has(m.seq));
612
+ return next.length === state.messages.length ? state : { messages: next };
613
+ }
614
+ /** Validate the persisted-state shape before a cache row seeds the fold. */
615
+ const stateSchema = { parse(value) {
616
+ if (value === null || typeof value !== "object" || !Array.isArray(value.messages)) throw new Error("bgTimeline: persisted state is not a message index");
617
+ return value;
618
+ } };
619
+ /**
620
+ * The projection definition in the runtime shape the registry drives (key /
621
+ * stateSchema / init / apply / wire / stateVersion). Typed loosely here and
622
+ * registered through a structural face: the typed contract lives behind
623
+ * @deepseek-ai/dsh-session-projection, which the shell provides at runtime
624
+ * but this package deliberately does not build against.
625
+ */
626
+ const timelineProjectionDefinition = {
627
+ key: TIMELINE_PROJECTION_KEY,
628
+ stateSchema,
629
+ init: () => ({ messages: [] }),
630
+ apply: (state, event) => {
631
+ const e = event ?? {};
632
+ let next = state;
633
+ if (e.type === "user/message") {
634
+ const source = e.data?.source;
635
+ const seq = typeof e.seq === "number" ? e.seq : void 0;
636
+ if (source !== null && typeof source === "object" && source.kind === "user" && seq !== void 0 && Number.isFinite(seq)) {
637
+ const last = state.messages[state.messages.length - 1];
638
+ if (!(last !== void 0 && last.seq === seq)) {
639
+ const rawTime = typeof e.time === "number" ? e.time : typeof e.data?.time === "number" ? e.data.time : 0;
640
+ const id = typeof e.data?.id === "string" && e.data.id !== "" ? e.data.id : void 0;
641
+ const entry = {
642
+ seq,
643
+ time: rawTime,
644
+ text: textOf(e.data?.content),
645
+ ...id === void 0 ? {} : { id }
646
+ };
647
+ next = { messages: [...state.messages, entry] };
648
+ }
649
+ }
650
+ }
651
+ return dropShadowedMessages(next, e);
652
+ },
653
+ wire: {
654
+ viewSchema: stateSchema,
655
+ view: (state) => state
656
+ },
657
+ stateVersion: 1
658
+ };
659
+ /**
660
+ * Register the timeline projection with the host registry (when mounted).
661
+ * Safe to call from the plugin root apply; the inject waits for the service
662
+ * and never throws when the deployment has none.
663
+ * @param ctx - Host context.
664
+ */
665
+ function registerTimelineProjection(ctx) {
666
+ ctx.inject(["sessionProjections"], (hostCtx) => {
667
+ try {
668
+ hostCtx.sessionProjections?.register(timelineProjectionDefinition);
669
+ } catch (error) {
670
+ console.error("[deepseek-harness-background] timeline projection registration failed:", error);
671
+ }
672
+ });
673
+ }
674
+ //#endregion
568
675
  //#region src/index.ts
569
676
  const BACKGROUND_NAMESPACE = settingsNamespace(BACKGROUND_SETTINGS_NAMESPACE);
570
677
  /**
@@ -574,6 +681,7 @@ const BACKGROUND_NAMESPACE = settingsNamespace(BACKGROUND_SETTINGS_NAMESPACE);
574
681
  * @param ctx - Host context that may acquire the settings and webServer services.
575
682
  */
576
683
  function apply(ctx) {
684
+ registerTimelineProjection(ctx);
577
685
  ctx.inject(["settings", "webServer"], (hostCtx) => {
578
686
  if (!hostCtx.settings.describe().some((descriptor) => descriptor.ns === "ui-background")) hostCtx.settings.register(BACKGROUND_NAMESPACE, BackgroundSettingsSchema, { applies: "live" });
579
687
  try {
@@ -584,4 +692,4 @@ function apply(ctx) {
584
692
  });
585
693
  }
586
694
  //#endregion
587
- export { BACKGROUND_API_PREFIX, BACKGROUND_SETTINGS_NAMESPACE, BackgroundSettingsSchema, DEFAULT_FIT, DEFAULT_OPACITY, DEFAULT_PANEL_OPACITY, DEFAULT_SCRIM, FIT_MODES, OPACITY_MAX, OPACITY_MIN, PLUGIN_HOME_REL, SCRIM_MAX, SCRIM_MIN, apply, makeBackgroundRoutes, resolveHarnessHome };
695
+ export { BACKGROUND_API_PREFIX, BACKGROUND_SETTINGS_NAMESPACE, BackgroundSettingsSchema, DEFAULT_FIT, DEFAULT_OPACITY, DEFAULT_PANEL_OPACITY, DEFAULT_SCRIM, FIT_MODES, OPACITY_MAX, OPACITY_MIN, PLUGIN_HOME_REL, SCRIM_MAX, SCRIM_MIN, TIMELINE_PROJECTION_KEY, apply, makeBackgroundRoutes, resolveHarnessHome, timelineProjectionDefinition };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "deepseek-harness-background",
3
3
  "description": "Custom background image plugin for the DeepSeek Harness web GUI: upload a local picture or paste an image URL, and render it behind the whole app surface with adjustable opacity, scrim, panel transparency and frosted-glass blur.",
4
- "version": "0.3.1",
4
+ "version": "0.3.2",
5
5
  "keywords": [
6
6
  "deepseek-harness",
7
7
  "dsh",