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/README.md +9 -2
- package/README.zh.md +9 -2
- package/lib/client.js +1423 -77
- package/lib/client.js.map +1 -1
- package/lib/index.js +119 -6
- package/package.json +8 -8
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]
|
|
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
|
|
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.
|
|
58
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.
|
|
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.
|
|
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.
|
|
68
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.
|
|
69
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.1-rc.
|
|
70
|
-
"@deepseek-ai/dsh-settings": "^0.1.1-rc.
|
|
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",
|