deepseek-harness-background 0.2.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
@@ -24,7 +24,8 @@ const BACKGROUND_SETTINGS_FIELDS = [
24
24
  "panelOpacity",
25
25
  "blur",
26
26
  "wallpaperBlur",
27
- "fit"
27
+ "fit",
28
+ "timeline"
28
29
  ];
29
30
  /** Browser-facing base path of the background API (shared with the host routes). */
30
31
  const BACKGROUND_API_PREFIX = "/api/bg-wallpaper";
@@ -44,6 +45,12 @@ const OPACITY_MIN = 0;
44
45
  const OPACITY_MAX = 1;
45
46
  /** Default panel opacity (0..1); 1 = fully opaque official surfaces, 0 = fully transparent glass. */
46
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";
47
54
  /** Default background rendering mode. */
48
55
  const DEFAULT_FIT = "cover";
49
56
  /** Image positioning modes. `cover` fills the frame (cropping as needed); `contain` fits the whole image inside the frame (letterboxing). */
@@ -68,7 +75,8 @@ const BackgroundSettingsSchema = z.object({
68
75
  panelOpacity: z.number().min(0).max(1).default(DEFAULT_PANEL_OPACITY),
69
76
  blur: z.number().min(0).max(40).default(16),
70
77
  wallpaperBlur: z.number().min(0).max(60).default(0),
71
- fit: z.union([...FIT_MODES]).default(DEFAULT_FIT)
78
+ fit: z.union([...FIT_MODES]).default(DEFAULT_FIT),
79
+ timeline: z.boolean().default(true)
72
80
  });
73
81
  //#endregion
74
82
  //#region src/harness-home.ts
@@ -135,7 +143,8 @@ const DEFAULTS = {
135
143
  panelOpacity: DEFAULT_PANEL_OPACITY,
136
144
  blur: 16,
137
145
  wallpaperBlur: 0,
138
- fit: DEFAULT_FIT
146
+ fit: DEFAULT_FIT,
147
+ timeline: true
139
148
  };
140
149
  /** Keep only the schema's known fields (see BACKGROUND_SETTINGS_FIELDS). */
141
150
  function pickKnown(section) {
@@ -187,9 +196,11 @@ function sniffImage(bytes) {
187
196
  }
188
197
  /** Per-upload size cap (20 MiB). */
189
198
  const MAX_UPLOAD_BYTES = 20971520;
190
- /** 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. */
191
202
  function isUploadId(id) {
192
- return /^up-[a-f0-9]+$/.test(id);
203
+ return /^up-[a-f0-9]{1,64}$/.test(id);
193
204
  }
194
205
  /** One JSON response. */
195
206
  function json(res, status, body) {
@@ -330,6 +341,8 @@ function validateSectionBody(body, home) {
330
341
  }
331
342
  const enabled = body.enabled;
332
343
  if (enabled !== void 0 && typeof enabled !== "boolean") return "invalid-enabled";
344
+ const timeline = body.timeline;
345
+ if (timeline !== void 0 && typeof timeline !== "boolean") return "invalid-timeline";
333
346
  const fit = body.fit;
334
347
  if (fit !== void 0 && fit !== "" && !FIT_MODES.includes(fit)) return "invalid-fit";
335
348
  return null;
@@ -445,6 +458,13 @@ function makeBackgroundRoutes(settings, opts = {}) {
445
458
  ...pickKnown(readSection()),
446
459
  ...pickKnown(body)
447
460
  };
461
+ if (merged.uploadId !== "" && merged.url !== "") {
462
+ json(res, 400, {
463
+ ok: false,
464
+ error: "mutually-exclusive-source"
465
+ });
466
+ return;
467
+ }
448
468
  await settings.replace(BACKGROUND_SETTINGS_NAMESPACE, merged);
449
469
  const after = readSection();
450
470
  if (before.uploadId !== "" && before.uploadId !== after.uploadId) deleteUploadFile(before.uploadId, opts.home);
@@ -560,6 +580,98 @@ function makeBackgroundRoutes(settings, opts = {}) {
560
580
  ];
561
581
  }
562
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
563
675
  //#region src/index.ts
564
676
  const BACKGROUND_NAMESPACE = settingsNamespace(BACKGROUND_SETTINGS_NAMESPACE);
565
677
  /**
@@ -569,6 +681,7 @@ const BACKGROUND_NAMESPACE = settingsNamespace(BACKGROUND_SETTINGS_NAMESPACE);
569
681
  * @param ctx - Host context that may acquire the settings and webServer services.
570
682
  */
571
683
  function apply(ctx) {
684
+ registerTimelineProjection(ctx);
572
685
  ctx.inject(["settings", "webServer"], (hostCtx) => {
573
686
  if (!hostCtx.settings.describe().some((descriptor) => descriptor.ns === "ui-background")) hostCtx.settings.register(BACKGROUND_NAMESPACE, BackgroundSettingsSchema, { applies: "live" });
574
687
  try {
@@ -579,4 +692,4 @@ function apply(ctx) {
579
692
  });
580
693
  }
581
694
  //#endregion
582
- 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.2.1",
4
+ "version": "0.3.2",
5
5
  "keywords": [
6
6
  "deepseek-harness",
7
7
  "dsh",
@@ -54,20 +54,20 @@
54
54
  ],
55
55
  "peerDependencies": {
56
56
  "@deepseek-ai/cordis": "^4.0.1",
57
- "@deepseek-ai/dsh-client-locale": "^0.1.1-rc.1",
58
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.1",
57
+ "@deepseek-ai/dsh-client-locale": "^0.1.1-rc.2",
58
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.2",
59
59
  "react": "^18.2.0"
60
60
  },
61
61
  "dependencies": {
62
- "@deepseek-ai/dsh-settings": "^0.1.1-rc.1",
62
+ "@deepseek-ai/dsh-settings": "^0.1.1-rc.2",
63
63
  "@deepseek-ai/schemastery": "^3.18.1"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@deepseek-ai/cordis": "^4.0.1",
67
- "@deepseek-ai/dsh-client-locale": "^0.1.1-rc.1",
68
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.1",
69
- "@deepseek-ai/dsh-host-webserver": "^0.1.1-rc.1",
70
- "@deepseek-ai/dsh-settings": "^0.1.1-rc.1",
67
+ "@deepseek-ai/dsh-client-locale": "^0.1.1-rc.2",
68
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.2",
69
+ "@deepseek-ai/dsh-host-webserver": "^0.1.1-rc.2",
70
+ "@deepseek-ai/dsh-settings": "^0.1.1-rc.2",
71
71
  "@deepseek-ai/schemastery": "^3.18.1",
72
72
  "@testing-library/dom": "^10.4.1",
73
73
  "@testing-library/react": "^16.3.0",