dsh-system-prompt-insertion 0.0.1 → 0.2.0

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/client.js ADDED
@@ -0,0 +1,1414 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-system-prompt-insertion",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ // src/client.ts
8
+ /**
9
+ * `dsh-system-prompt-insertion` — browser half.
10
+ *
11
+ * ## What this module is
12
+ *
13
+ * A sidebar panel for the user-owned text the host half registers as sections —
14
+ * not a settings page. The shell's own panel family works through two seats that
15
+ * this module fills together:
16
+ *
17
+ * - `sidebar.panellist` gives us a row in the left column (the rail the Plugins,
18
+ * Schedules, Market and Context entries live in). The shell owns the button,
19
+ * the label, the tooltip, the active highlight and the collapsed rail; all we
20
+ * draw is the glyph, at the size and selection state it asks for.
21
+ * - `main` renders the panel body once the row with the same id is selected.
22
+ * Reserving `conversation` is not our business: any other key receives no
23
+ * Session binding, so this page works with no session open.
24
+ *
25
+ * Both registrations go through `ctx.slots.inject`, which fires only after the
26
+ * owning shell entry declares the seat, so load order between this package and
27
+ * the shell does not matter.
28
+ *
29
+ * It is loaded as a lazy-CJS factory (`window.__ModuleLoader__.load`) whose id
30
+ * is the package name; `react` comes from the page's module table, and NOTHING
31
+ * else is imported — no harness client package, no component kit. The panel
32
+ * therefore cannot break when an internal client API moves, and it is
33
+ * theme-consistent through `--dsw-alias-*` tokens rather than by copying host
34
+ * components.
35
+ *
36
+ * ## Shape of the page
37
+ *
38
+ * One column, top to bottom: heading, preview, section editor. The preview
39
+ * comes first because it is the outcome the editor exists to shape. Save,
40
+ * discard and Add section live at the end of the editor rather than in a pinned
41
+ * footer, so the whole page scrolls as one document. The preview sits above
42
+ * instead of beside the editor: at a 960px content width two columns forced the
43
+ * prompt text into a third of the window, and the list of assembled sections
44
+ * was clipped. A single column keeps every control at the width the shell gives
45
+ * its own pages.
46
+ *
47
+ * The panel borrows the shell's visual grammar deliberately: a 960px content
48
+ * column behind responsive gutters, 20px/500 page title, 0.5px hairline
49
+ * borders, pill-shaped primary actions, the theme's switch geometry (36×20),
50
+ * focus rings from `--dsw-focus-ring-*`, and hover fills from
51
+ * `--dsw-alias-interactive-*`. Nothing hard-codes a colour: the stylesheet
52
+ * below only reads theme tokens, so light, dark and skin themes all follow.
53
+ *
54
+ * ## Data flow
55
+ *
56
+ * The panel reads and writes the same JSON API the host half serves
57
+ * (`/dsh-system-prompt-insertion/state`, `/preview`). Edits stage locally, so a
58
+ * half-typed rule never reaches a running session; Save commits the whole
59
+ * document at once. The master switch is the exception: it applies immediately,
60
+ * because the preview must agree with the control. Preview calls the host's
61
+ * real `assemble()`+`renderPrompt()`, which is why it can honestly list the
62
+ * host's own sections and flag our text as hidden under a complete-prompt
63
+ * preset. Entry, Save and switch changes refresh it automatically; the refresh
64
+ * control beside its heading covers prompt changes made by other plugins.
65
+ *
66
+ * @module dsh-system-prompt-insertion/client
67
+ */
68
+ /** Locale namespace owned by this panel. */
69
+ const NS = "dsh-system-prompt-insertion";
70
+ /** URL prefix served by the host half (kept in sync with `routes.ts`). */
71
+ const API = "/dsh-system-prompt-insertion";
72
+ /**
73
+ * The one panel id shared by the sidebar row and the `main` occupant.
74
+ *
75
+ * The shell projects the row's label and dispatches `main` by this exact key, so
76
+ * both registrations read it instead of repeating a literal.
77
+ */
78
+ const PANEL_ID = "system-prompt-insertion";
79
+ /**
80
+ * Position among the shell's global panel rows.
81
+ *
82
+ * Shipped rows sit at 0 (Plugins), 10 (Schedules) and 20 (Context); 30 keeps
83
+ * this entry after them without claiming a shipped slot.
84
+ */
85
+ const PANEL_ORDER = 30;
86
+ /** Placement bands, mirroring the host's `POSITIONS`. */
87
+ const POSITIONS = [
88
+ "head",
89
+ "body",
90
+ "tail"
91
+ ];
92
+ /** Copy, in the two languages the harness ships. */
93
+ const zh = {
94
+ nav: "提示词注入",
95
+ title: "提示词注入",
96
+ description: "把你自己写的文字放到系统提示词的开头、中间或末尾。保存后下一个请求生效,不用重启。",
97
+ enabled: "启用",
98
+ enabledHint: "关闭后所有段落都不再进入系统提示词,不必逐段停用;切换会立即保存。",
99
+ add: "添加段落",
100
+ addFirst: "添加第一个段落",
101
+ save: "保存",
102
+ saving: "保存中…",
103
+ saveHint: "保存到状态文件;也可以按 {keys}",
104
+ revert: "撤销修改",
105
+ refreshPreview: "刷新预览",
106
+ previewing: "组装中…",
107
+ statusSaved: "已保存",
108
+ statusDirty: "有未保存的修改",
109
+ pathLabel: "状态文件",
110
+ originFile: "来自文件",
111
+ originMissing: "文件尚未创建",
112
+ originMemory: "刚保存",
113
+ originUnreadable: "文件无法读取",
114
+ tailBase: "宿主末段 {order}",
115
+ tailBaseHint: "宿主最后一个段落的 order;本插件末尾段从它 +300 开始,预览里能看到每段的最终 order。",
116
+ section: "段落",
117
+ sectionsTitle: "段落",
118
+ sectionsSummary: "{count} 段 · 已注册 {registered}",
119
+ sectionTitle: "名称",
120
+ sectionTitleOf: "第 {index} 段名称",
121
+ sectionTextOf: "第 {index} 段正文",
122
+ positionOf: "第 {index} 段位置",
123
+ moveUpNamed: "上移「{title}」",
124
+ moveDownNamed: "下移「{title}」",
125
+ textPlaceholder: "例如:回答前先复述我的目标;不确定的事实先检索再回答;代码注释保持简短。",
126
+ positionHead: "开头",
127
+ positionBody: "中间",
128
+ positionTail: "末尾",
129
+ positionHintHead: "紧跟人格设定,排在计划策略与工具说明之前。",
130
+ positionHintBody: "排在所有工具说明之后、交付物引用之前。",
131
+ positionHintTail: "排在宿主最后一段之后,是提示词真正的结尾。",
132
+ interpolate: "解析 {{变量}}",
133
+ interpolateHint: "关闭时正文原样进入提示词;开启后可引用 {{model}} / {{provider}},未知变量会让请求失败。",
134
+ removeNamed: "删除「{title}」",
135
+ toggleNamed: "启停「{title}」",
136
+ bytes: "{bytes} 字节",
137
+ bytesShort: "字节",
138
+ orderShort: "order",
139
+ emptyListTitle: "还没有段落",
140
+ emptyListHint: "写下的文字只进入你自己的系统提示词,保存后从下一个请求开始生效。",
141
+ emptyChip: "正文为空",
142
+ disabledChip: "已停用",
143
+ previewTitle: "当前系统提示词",
144
+ previewSummary: "共 {count} 段,{bytes} 字节",
145
+ previewStale: "预览是已保存的版本;保存后会自动刷新。",
146
+ previewNone: "还没有预览。点标题旁的刷新按钮,看看下一次请求真正发出去的完整提示词。",
147
+ warnings: "检查结果",
148
+ noWarnings: "没有发现问题。",
149
+ ours: "本插件",
150
+ variables: "可用变量",
151
+ loading: "加载中…",
152
+ loadFailed: "读取失败",
153
+ retry: "重试",
154
+ saveFailed: "保存失败",
155
+ previewFailed: "预览失败",
156
+ dismiss: "关闭",
157
+ copy: "复制全文",
158
+ copied: "已复制",
159
+ copyFailed: "复制失败",
160
+ copyFailedHint: "浏览器拒绝了剪贴板访问,请手动选择全文。",
161
+ warnEmpty: "「{title}」已启用但正文为空,不会被注册。",
162
+ warnOversize: "「{title}」{bytes} 字节,超过软上限 {cap} 字节。",
163
+ warnDuplicate: "「{title}」有一行与「{other}」重复。",
164
+ warnUnknownVariable: "「{title}」引用了 {{name}},当前组装没有可用值(已知:{known})。",
165
+ warnHidden: "有段落已注册但不在当前组装里——当前预设可能用 complete: true 接管了整份提示词。",
166
+ warnStoreError: "状态文件读取失败,仍在使用上一份内容:{message}",
167
+ warnRenderError: "渲染失败:{message}",
168
+ warnPreviewPartial: "预览没有带上 agent scope,{names} 这类按 agent 取值的变量会按原样显示。"
169
+ };
170
+ const en = {
171
+ nav: "Prompt insertion",
172
+ title: "Prompt insertion",
173
+ description: "Place your own text at the head, middle, or tail of the system prompt. Saved text applies on the next request, with no restart.",
174
+ enabled: "Enabled",
175
+ enabledHint: "Turning this off keeps every section out of the prompt without disabling them one by one. The switch is saved immediately.",
176
+ add: "Add section",
177
+ addFirst: "Add the first section",
178
+ save: "Save",
179
+ saving: "Saving…",
180
+ saveHint: "Save to the state file; {keys} works too",
181
+ revert: "Discard changes",
182
+ refreshPreview: "Refresh preview",
183
+ previewing: "Assembling…",
184
+ statusSaved: "Saved",
185
+ statusDirty: "Unsaved changes",
186
+ pathLabel: "State file",
187
+ originFile: "from file",
188
+ originMissing: "file not created yet",
189
+ originMemory: "just saved",
190
+ originUnreadable: "file unreadable",
191
+ tailBase: "Host tail {order}",
192
+ tailBaseHint: "The order of the host's last section; our tail sections start 300 after it. The preview shows each final order.",
193
+ section: "Section",
194
+ sectionsTitle: "Sections",
195
+ sectionsSummary: "{count} total · {registered} registered",
196
+ sectionTitle: "Name",
197
+ sectionTitleOf: "Name of section {index}",
198
+ sectionTextOf: "Body of section {index}",
199
+ positionOf: "Placement of section {index}",
200
+ moveUpNamed: "Move \"{title}\" up",
201
+ moveDownNamed: "Move \"{title}\" down",
202
+ textPlaceholder: "For example: restate my goal before answering; verify uncertain facts before asserting them; keep code comments short.",
203
+ positionHead: "Head",
204
+ positionBody: "Middle",
205
+ positionTail: "Tail",
206
+ positionHintHead: "Right after the persona, before plan policy and tool guidance.",
207
+ positionHintBody: "After every tool section, before deliverable references.",
208
+ positionHintTail: "After the host's own last section (persona suffix) — the true end of the prompt.",
209
+ interpolate: "Resolve {{variables}}",
210
+ interpolateHint: "Off keeps the body literal; on lets you use {{model}} / {{provider}}, and an unknown name fails the request.",
211
+ removeNamed: "Remove \"{title}\"",
212
+ toggleNamed: "Toggle \"{title}\"",
213
+ bytes: "{bytes} bytes",
214
+ bytesShort: "Bytes",
215
+ orderShort: "order",
216
+ emptyListTitle: "No sections yet",
217
+ emptyListHint: "Text you write here enters your own system prompt only, starting with the first request after you save.",
218
+ emptyChip: "Empty body",
219
+ disabledChip: "Off",
220
+ previewTitle: "The current system prompt",
221
+ previewSummary: "{count} sections, {bytes} bytes",
222
+ previewStale: "The preview shows what is SAVED; it refreshes after you save.",
223
+ previewNone: "Nothing previewed yet. Use the refresh button next to the title to see the full prompt the next request sends.",
224
+ warnings: "Checks",
225
+ noWarnings: "Nothing looks wrong.",
226
+ ours: "this plugin",
227
+ variables: "Variables",
228
+ loading: "Loading…",
229
+ loadFailed: "Could not load",
230
+ retry: "Retry",
231
+ saveFailed: "Could not save",
232
+ previewFailed: "Could not preview",
233
+ dismiss: "Dismiss",
234
+ copy: "Copy all",
235
+ copied: "Copied",
236
+ copyFailed: "Copy failed",
237
+ copyFailedHint: "The browser refused clipboard access; select the text and copy it manually.",
238
+ warnEmpty: "\"{title}\" is enabled but empty, so it is not registered.",
239
+ warnOversize: "\"{title}\" is {bytes} bytes, above the {cap}-byte soft cap.",
240
+ warnDuplicate: "\"{title}\" repeats a line already present in \"{other}\".",
241
+ warnUnknownVariable: "\"{title}\" references {{name}}, which has no value in this assembly (known: {known}).",
242
+ warnHidden: "A section is registered but absent from the current assembly — the active preset may be taking over the whole prompt with complete: true.",
243
+ warnStoreError: "The state file could not be read; the previous content is still live: {message}",
244
+ warnRenderError: "Rendering failed: {message}",
245
+ warnPreviewPartial: "Previewed without an agent scope, so per-agent variables such as {names} stayed literal."
246
+ };
247
+ /** Localize one preview warning, falling back to the host's English message. */
248
+ function warningText(t, warning) {
249
+ const key = {
250
+ empty: "warnEmpty",
251
+ oversize: "warnOversize",
252
+ duplicate: "warnDuplicate",
253
+ "unknown-variable": "warnUnknownVariable",
254
+ hidden: "warnHidden",
255
+ "store-error": "warnStoreError",
256
+ "render-error": "warnRenderError",
257
+ "preview-partial": "warnPreviewPartial"
258
+ }[warning.code];
259
+ if (key === void 0) return warning.message;
260
+ const template = t(key);
261
+ if (template === key) return warning.message;
262
+ return format(template, {
263
+ ...warning.params ?? {},
264
+ message: warning.message
265
+ });
266
+ }
267
+ /** Fill `{name}` placeholders in a locale string. */
268
+ function format(template, values) {
269
+ return template.replace(/\{([a-z][a-z0-9]*)\}/gi, (match, key) => String(values[key] ?? match));
270
+ }
271
+ /** Monospace stack for prompt text, following the shell's configured code font. */
272
+ const MONO = "var(--dsh-font-family-code, \"SF Mono\", \"JetBrains Mono\", Menlo, Consolas, monospace)";
273
+ /** UTF-8 size, matching how the host counts section bytes. */
274
+ function byteLength(text) {
275
+ return new TextEncoder().encode(text).length;
276
+ }
277
+ /** The hint for one placement band. */
278
+ function placementHint(t, position) {
279
+ return position === "head" ? t("positionHintHead") : position === "body" ? t("positionHintBody") : t("positionHintTail");
280
+ }
281
+ /** Human-facing state of the backing file. */
282
+ function originLabel(t, origin) {
283
+ if (origin === "file") return t("originFile");
284
+ if (origin === "memory") return t("originMemory");
285
+ if (origin === "unreadable") return t("originUnreadable");
286
+ return t("originMissing");
287
+ }
288
+ /** Localize one API error envelope. */
289
+ function errorText(body, fallback) {
290
+ const field = body.error;
291
+ if (typeof field === "string" && field !== "") return `${fallback}: ${field}`;
292
+ if (field !== null && typeof field === "object") {
293
+ const detail = field;
294
+ const text = detail.message ?? detail.code;
295
+ if (text !== void 0) return `${fallback}: ${String(text)}`;
296
+ }
297
+ return fallback;
298
+ }
299
+ /** Build the panel payload from one `/state` response body. */
300
+ function payloadFrom(body, fallbackOrigin) {
301
+ return {
302
+ state: body.state,
303
+ path: String(body.path ?? ""),
304
+ origin: body.origin ?? fallbackOrigin,
305
+ ...typeof body.error === "string" ? { error: body.error } : {},
306
+ orderBases: body.orderBases ?? { tailBase: 10200 },
307
+ registered: body.registered ?? []
308
+ };
309
+ }
310
+ /** Remove a leading localized prefix so a titled card does not repeat itself. */
311
+ function withoutPrefix(text, prefix) {
312
+ if (text === void 0) return void 0;
313
+ const lead = `${prefix}: `;
314
+ return text.startsWith(lead) ? text.slice(lead.length) : text;
315
+ }
316
+ /** Read a JSON response, tolerating an HTML error page. */
317
+ async function readJson(response) {
318
+ const text = await response.text();
319
+ try {
320
+ return JSON.parse(text);
321
+ } catch {
322
+ return {
323
+ ok: false,
324
+ error: {
325
+ code: `http-${response.status}`,
326
+ message: text.slice(0, 300)
327
+ }
328
+ };
329
+ }
330
+ }
331
+ /** Make a fresh section id that satisfies the host's `SECTION_ID` pattern. */
332
+ function newId() {
333
+ return `s-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
334
+ }
335
+ /**
336
+ * The panel stylesheet.
337
+ *
338
+ * One scoped block instead of dozens of inline style objects: hover, focus,
339
+ * `:disabled` and media-query states cannot be expressed inline, and every
340
+ * global panel is judged against the shell's own pages. Every colour comes from
341
+ * a `--dsw-*` token (the accompanying test refuses literals), so a theme or a
342
+ * skin changes the panel without touching it.
343
+ */
344
+ const PANEL_CSS = `
345
+ .dspi-root {
346
+ --dspi-gutter: clamp(24px, 4vw, 48px);
347
+ --dspi-content: 960px;
348
+ box-sizing: border-box;
349
+ display: flex;
350
+ flex-direction: column;
351
+ width: 100%;
352
+ min-width: 0;
353
+ height: 100%;
354
+ min-height: 0;
355
+ overflow: hidden;
356
+ background: var(--dsw-alias-bg-base);
357
+ color: var(--dsw-alias-label-primary);
358
+ font-size: 14px;
359
+ line-height: 1.6;
360
+ }
361
+ .dspi-root *, .dspi-root *:before, .dspi-root *:after { box-sizing: border-box; }
362
+ .dspi-root button, .dspi-root input, .dspi-root select, .dspi-root textarea { font-family: inherit; }
363
+ .dspi-root button, .dspi-root input, .dspi-root select, .dspi-root textarea, .dspi-root pre { -webkit-app-region: no-drag; }
364
+ .dspi-scroll { flex: 1 1 auto; min-height: 0; overflow: auto; scrollbar-gutter: stable; }
365
+ .dspi-content { width: 100%; max-width: var(--dspi-content); margin: 0 auto; padding: 0 var(--dspi-gutter) 36px; }
366
+ .dspi-head {
367
+ display: flex;
368
+ align-items: flex-start;
369
+ justify-content: space-between;
370
+ gap: 16px 24px;
371
+ flex-wrap: wrap;
372
+ padding-top: 28px;
373
+ margin-bottom: 22px;
374
+ }
375
+ [data-platform='darwin'] .dspi-head { padding-top: calc(28px + var(--dsh-frame-top-clearance, 0px)); }
376
+ .dspi-head-main { flex: 1 1 360px; min-width: 0; }
377
+ .dspi-title { margin: 0; font-size: 20px; font-weight: 500; line-height: 28px; color: var(--dsw-alias-label-primary); }
378
+ .dspi-subtitle { max-width: 640px; margin: 4px 0 0; font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-tertiary); }
379
+ .dspi-meta {
380
+ display: flex;
381
+ align-items: center;
382
+ flex-wrap: wrap;
383
+ gap: 2px 14px;
384
+ margin-top: 10px;
385
+ font-size: 12px;
386
+ line-height: 18px;
387
+ color: var(--dsw-alias-label-caption);
388
+ }
389
+ .dspi-meta code {
390
+ max-width: min(100%, 420px);
391
+ overflow: hidden;
392
+ text-overflow: ellipsis;
393
+ white-space: nowrap;
394
+ font-family: ${MONO};
395
+ font-size: 12px;
396
+ }
397
+ .dspi-origin[data-origin='missing'] { color: var(--dsw-alias-state-warn-label); }
398
+ .dspi-origin[data-origin='unreadable'] { color: var(--dsw-alias-state-error-primary); }
399
+ .dspi-head-actions { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
400
+ .dspi-master {
401
+ display: inline-flex;
402
+ align-items: center;
403
+ gap: 8px;
404
+ font-size: 13px;
405
+ line-height: 20px;
406
+ color: var(--dsw-alias-label-secondary);
407
+ user-select: none;
408
+ }
409
+ .dspi-btn {
410
+ display: inline-flex;
411
+ align-items: center;
412
+ justify-content: center;
413
+ gap: 6px;
414
+ height: 32px;
415
+ padding: 0 14px;
416
+ border: 0;
417
+ border-radius: 16px;
418
+ background: transparent;
419
+ color: var(--dsw-alias-label-primary);
420
+ font-size: 13px;
421
+ line-height: 20px;
422
+ white-space: nowrap;
423
+ cursor: pointer;
424
+ transition: background-color .15s, border-color .15s, opacity .15s;
425
+ }
426
+ .dspi-btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
427
+ .dspi-btn:disabled { opacity: .4; cursor: not-allowed; }
428
+ .dspi-btn--primary { background: var(--dsw-alias-button-primary-fill); color: var(--dsw-alias-label-primary-foreground); }
429
+ .dspi-btn--primary:hover:not(:disabled) { background: var(--dsw-alias-button-primary-hover); }
430
+ .dspi-btn--outline { box-shadow: inset 0 0 0 .5px var(--dsw-alias-border-l3); }
431
+ .dspi-btn--sm { height: 28px; padding: 0 10px; border-radius: 8px; font-size: 12px; line-height: 18px; }
432
+ .dspi-switch {
433
+ position: relative;
434
+ flex: none;
435
+ width: 36px;
436
+ height: 20px;
437
+ padding: 2px;
438
+ border: 0;
439
+ border-radius: 999px;
440
+ background: var(--dsw-alias-border-l3);
441
+ cursor: pointer;
442
+ transition: background-color .15s;
443
+ }
444
+ .dspi-switch[aria-checked='true'] { background: var(--dsw-alias-brand-primary); }
445
+ .dspi-switch:disabled { opacity: .5; cursor: default; }
446
+ .dspi-switch-thumb {
447
+ display: block;
448
+ width: 16px;
449
+ height: 16px;
450
+ border-radius: 50%;
451
+ background: var(--dsw-alias-label-primary-foreground);
452
+ transition: transform .12s ease;
453
+ }
454
+ .dspi-switch[aria-checked='false'] .dspi-switch-thumb { background: var(--dsw-alias-switch-thumb); }
455
+ .dspi-switch[aria-checked='true'] .dspi-switch-thumb { transform: translateX(16px); }
456
+ .dspi-root button:focus-visible,
457
+ .dspi-root select:focus-visible,
458
+ .dspi-root input[type='checkbox']:focus-visible,
459
+ .dspi-root pre:focus-visible {
460
+ outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
461
+ outline-offset: 2px;
462
+ }
463
+ .dspi-switch:focus-visible {
464
+ outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
465
+ outline-offset: 2px;
466
+ }
467
+ .dspi-banner {
468
+ display: flex;
469
+ align-items: flex-start;
470
+ gap: 8px;
471
+ margin: 0 0 16px;
472
+ padding: 10px 12px;
473
+ border-radius: 12px;
474
+ background: var(--dsw-alias-bg-module-platform);
475
+ color: var(--dsw-alias-label-secondary);
476
+ font-size: 13px;
477
+ line-height: 20px;
478
+ }
479
+ .dspi-banner--error { background: var(--dsw-alias-interactive-bg-hover-danger); color: var(--dsw-alias-state-error-primary); }
480
+ .dspi-banner--warn { background: var(--dsw-alias-state-warn-tertiary); color: var(--dsw-alias-state-warn-label); }
481
+ .dspi-banner-icon { flex: none; margin-top: 3px; }
482
+ .dspi-banner-text { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
483
+ .dspi-banner .dspi-btn { flex: none; margin-left: auto; }
484
+ .dspi-section { margin-top: 26px; }
485
+ .dspi-section--preview { margin-top: 0; }
486
+ .dspi-section-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 10px; }
487
+ .dspi-section-title { margin: 0; font-size: 13px; font-weight: 600; line-height: 20px; color: var(--dsw-alias-label-secondary); }
488
+ .dspi-section-meta { font-size: 12px; line-height: 20px; color: var(--dsw-alias-label-caption); }
489
+ .dspi-section-actions { display: flex; align-items: center; gap: 8px; margin-left: auto; }
490
+ .dspi-icon-btn.dspi-preview-refresh[data-busy='true'] { opacity: .55; }
491
+ .dspi-list { display: flex; flex-direction: column; gap: 10px; margin: 0; padding: 0; list-style: none; }
492
+ .dspi-list-item { display: block; min-width: 0; }
493
+ .dspi-add {
494
+ display: flex;
495
+ align-items: center;
496
+ justify-content: center;
497
+ gap: 6px;
498
+ width: 100%;
499
+ height: 40px;
500
+ margin-top: 10px;
501
+ border: 1px dashed var(--dsw-alias-border-l3);
502
+ border-radius: 12px;
503
+ background: transparent;
504
+ color: var(--dsw-alias-label-secondary);
505
+ font-size: 13px;
506
+ line-height: 20px;
507
+ cursor: pointer;
508
+ transition: background-color .15s, border-color .15s, color .15s;
509
+ }
510
+ .dspi-add:hover {
511
+ border-color: var(--dsw-alias-border-l4);
512
+ background: var(--dsw-alias-interactive-bg-hover);
513
+ color: var(--dsw-alias-label-primary);
514
+ }
515
+ .dspi-card {
516
+ padding: 12px 14px 10px;
517
+ border: .5px solid var(--dsw-alias-border-l3);
518
+ border-radius: 16px;
519
+ background: var(--dsw-alias-bg-layer-1);
520
+ transition: border-color .15s;
521
+ }
522
+ .dspi-card:hover, .dspi-card:focus-within { border-color: var(--dsw-alias-border-l4); }
523
+ .dspi-card-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-height: 32px; }
524
+ .dspi-card-head-main { display: flex; align-items: center; gap: 8px; flex: 1 1 220px; min-width: 0; }
525
+ .dspi-card-head-side { display: flex; align-items: center; gap: 4px; flex: none; margin-left: auto; }
526
+ .dspi-title-input {
527
+ flex: 1 1 auto;
528
+ min-width: 0;
529
+ height: 28px;
530
+ padding: 0 2px;
531
+ border: 0;
532
+ border-bottom: 1px solid transparent;
533
+ border-radius: 0;
534
+ background: transparent;
535
+ color: var(--dsw-alias-label-primary);
536
+ font-size: 14px;
537
+ font-weight: 500;
538
+ line-height: 22px;
539
+ }
540
+ .dspi-title-input:hover { border-bottom-color: var(--dsw-alias-border-l3); }
541
+ .dspi-title-input:focus { outline: none; border-bottom-color: var(--dsw-alias-state-business-primary); }
542
+ .dspi-title-input::placeholder { color: var(--dsw-alias-label-caption); font-weight: 400; }
543
+ .dspi-card[data-enabled='false'] .dspi-title-input { color: var(--dsw-alias-label-secondary); }
544
+ .dspi-select-wrap { position: relative; display: inline-flex; align-items: center; flex: none; }
545
+ .dspi-select {
546
+ height: 28px;
547
+ padding: 0 24px 0 10px;
548
+ border: .5px solid var(--dsw-alias-border-l3);
549
+ border-radius: 8px;
550
+ background: transparent;
551
+ color: var(--dsw-alias-label-secondary);
552
+ font-size: 12px;
553
+ line-height: 18px;
554
+ cursor: pointer;
555
+ appearance: none;
556
+ }
557
+ .dspi-select:hover { background: var(--dsw-alias-interactive-bg-hover); }
558
+ .dspi-caret { position: absolute; right: 8px; top: 50%; width: 12px; height: 12px; transform: translateY(-50%); color: var(--dsw-alias-label-tertiary); pointer-events: none; }
559
+ .dspi-icon-btn {
560
+ display: inline-flex;
561
+ align-items: center;
562
+ justify-content: center;
563
+ width: 28px;
564
+ height: 28px;
565
+ padding: 0;
566
+ border: 0;
567
+ border-radius: 8px;
568
+ background: transparent;
569
+ color: var(--dsw-alias-label-tertiary);
570
+ cursor: pointer;
571
+ transition: background-color .15s, color .15s;
572
+ }
573
+ .dspi-icon-btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
574
+ .dspi-icon-btn:disabled { opacity: .3; cursor: default; }
575
+ .dspi-icon-btn--danger:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover-danger); color: var(--dsw-alias-state-error-primary); }
576
+ .dspi-textarea {
577
+ display: block;
578
+ width: 100%;
579
+ min-height: 96px;
580
+ max-height: 360px;
581
+ margin-top: 6px;
582
+ padding: 10px 12px;
583
+ border: .5px solid var(--dsw-alias-border-l3);
584
+ border-radius: 12px;
585
+ background: var(--dsw-alias-bg-base);
586
+ color: var(--dsw-alias-label-primary);
587
+ font-family: ${MONO};
588
+ font-size: 12.5px;
589
+ line-height: 1.7;
590
+ resize: vertical;
591
+ field-sizing: content;
592
+ transition: border-color .15s;
593
+ }
594
+ .dspi-textarea:hover { border-color: var(--dsw-alias-border-l2); }
595
+ .dspi-textarea:focus { outline: none; border-color: var(--dsw-alias-state-business-primary); }
596
+ .dspi-textarea::placeholder { color: var(--dsw-alias-label-caption); }
597
+ .dspi-card-meta { display: flex; align-items: center; gap: 4px 12px; flex-wrap: wrap; margin-top: 6px; font-size: 12px; line-height: 20px; color: var(--dsw-alias-label-caption); }
598
+ .dspi-card-hint { flex: 1 1 220px; min-width: 0; color: var(--dsw-alias-label-tertiary); }
599
+ .dspi-check { display: inline-flex; align-items: center; gap: 6px; flex: none; margin-left: auto; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 20px; cursor: pointer; white-space: nowrap; }
600
+ .dspi-check input { width: 14px; height: 14px; margin: 0; accent-color: var(--dsw-alias-brand-primary); cursor: inherit; }
601
+ .dspi-chip {
602
+ display: inline-flex;
603
+ align-items: center;
604
+ gap: 4px;
605
+ height: 20px;
606
+ padding: 0 8px;
607
+ border-radius: 10px;
608
+ background: var(--dsw-alias-bg-module-platform);
609
+ color: var(--dsw-alias-label-secondary);
610
+ font-size: 11px;
611
+ line-height: 20px;
612
+ white-space: nowrap;
613
+ }
614
+ .dspi-chip--warn { background: var(--dsw-alias-state-warn-tertiary); color: var(--dsw-alias-state-warn-label); }
615
+ .dspi-empty {
616
+ display: flex;
617
+ flex-direction: column;
618
+ align-items: center;
619
+ gap: 4px;
620
+ padding: 36px 20px;
621
+ border: 1px dashed var(--dsw-alias-border-l3);
622
+ border-radius: 16px;
623
+ text-align: center;
624
+ }
625
+ .dspi-empty-glyph { margin-bottom: 6px; color: var(--dsw-alias-label-caption); }
626
+ .dspi-empty-title { margin: 0; font-size: 14px; font-weight: 500; line-height: 22px; color: var(--dsw-alias-label-secondary); }
627
+ .dspi-empty-hint { margin: 0; max-width: 440px; font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-tertiary); }
628
+ .dspi-empty-action { margin-top: 14px; }
629
+ .dspi-panel {
630
+ display: flex;
631
+ flex-direction: column;
632
+ gap: 12px;
633
+ padding: 14px;
634
+ border: .5px solid var(--dsw-alias-border-l3);
635
+ border-radius: 16px;
636
+ background: var(--dsw-alias-bg-layer-1);
637
+ }
638
+ .dspi-panel--idle { padding: 20px; color: var(--dsw-alias-label-tertiary); font-size: 13px; line-height: 20px; }
639
+ .dspi-panel--idle p { margin: 0; }
640
+ .dspi-note { margin: 0; font-size: 12px; line-height: 20px; color: var(--dsw-alias-label-tertiary); }
641
+ .dspi-note--warn { color: var(--dsw-alias-state-warn-label); }
642
+ .dspi-subhead { margin: 0; font-size: 12px; font-weight: 600; line-height: 20px; color: var(--dsw-alias-label-secondary); }
643
+ .dspi-inline { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-size: 12px; line-height: 20px; color: var(--dsw-alias-label-tertiary); }
644
+ .dspi-var {
645
+ padding: 1px 6px;
646
+ border-radius: 6px;
647
+ background: var(--dsw-alias-bg-module-platform);
648
+ color: var(--dsw-alias-label-secondary);
649
+ font-family: ${MONO};
650
+ font-size: 11.5px;
651
+ }
652
+ .dspi-checks { display: flex; flex-direction: column; gap: 6px; margin: 0; padding: 0; list-style: none; }
653
+ .dspi-check-row { display: flex; align-items: flex-start; gap: 8px; font-size: 12px; line-height: 20px; color: var(--dsw-alias-state-warn-label); }
654
+ .dspi-check-dot { flex: none; width: 5px; height: 5px; margin-top: 7px; border-radius: 50%; background: currentColor; }
655
+ .dspi-check-row--ok { color: var(--dsw-alias-state-success-primary); }
656
+ .dspi-check-row .dspi-check-icon { flex: none; margin-top: 4px; }
657
+ .dspi-table-scroll {
658
+ overflow: hidden;
659
+ border: .5px solid var(--dsw-alias-border-l3);
660
+ border-radius: 12px;
661
+ background: var(--dsw-alias-bg-base);
662
+ }
663
+ .dspi-table { width: 100%; border-collapse: collapse; table-layout: fixed; font-family: ${MONO}; font-size: 11.5px; }
664
+ .dspi-table th {
665
+ position: sticky;
666
+ top: 0;
667
+ z-index: 1;
668
+ padding: 6px 8px;
669
+ border-bottom: .5px solid var(--dsw-alias-border-l3);
670
+ background: var(--dsw-alias-bg-base);
671
+ color: var(--dsw-alias-label-caption);
672
+ font-weight: 500;
673
+ text-align: left;
674
+ }
675
+ .dspi-table td {
676
+ padding: 5px 8px;
677
+ border-bottom: .5px solid var(--dsw-alias-border-l1);
678
+ color: var(--dsw-alias-label-tertiary);
679
+ white-space: nowrap;
680
+ overflow: hidden;
681
+ text-overflow: ellipsis;
682
+ }
683
+ .dspi-table tr:last-child td { border-bottom: 0; }
684
+ .dspi-table tr[data-ours='true'] td { color: var(--dsw-alias-label-primary); }
685
+ .dspi-table .dspi-col-index { width: 38px; }
686
+ .dspi-table .dspi-col-order { width: 64px; }
687
+ .dspi-table .dspi-col-bytes { width: 64px; text-align: right; }
688
+ .dspi-name-cell { display: flex; align-items: center; gap: 6px; min-width: 0; }
689
+ .dspi-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
690
+ .dspi-tag {
691
+ flex: none;
692
+ height: 18px;
693
+ padding: 0 6px;
694
+ border-radius: 9px;
695
+ background: var(--dsw-alias-state-business-tertiary);
696
+ color: var(--dsw-alias-state-business-primary);
697
+ font-family: var(--dsw-font-family);
698
+ font-size: 10.5px;
699
+ line-height: 18px;
700
+ }
701
+ .dspi-pre {
702
+ margin: 0;
703
+ max-height: 340px;
704
+ overflow: auto;
705
+ padding: 12px;
706
+ border: .5px solid var(--dsw-alias-border-l3);
707
+ border-radius: 12px;
708
+ background: var(--dsw-alias-bg-base);
709
+ color: var(--dsw-alias-label-secondary);
710
+ font-family: ${MONO};
711
+ font-size: 12px;
712
+ line-height: 1.7;
713
+ white-space: pre-wrap;
714
+ overflow-wrap: anywhere;
715
+ }
716
+ .dspi-settings-actions {
717
+ display: flex;
718
+ align-items: center;
719
+ gap: 8px;
720
+ flex-wrap: wrap;
721
+ margin-top: 14px;
722
+ }
723
+ .dspi-settings-actions .dspi-status { margin-right: auto; }
724
+ .dspi-status { display: inline-flex; align-items: center; gap: 6px; flex: 0 1 auto; min-width: 0; font-size: 12px; line-height: 20px; color: var(--dsw-alias-label-tertiary); }
725
+ .dspi-status-dot { flex: none; width: 6px; height: 6px; border-radius: 50%; background: var(--dsw-alias-state-success-primary); }
726
+ .dspi-status-dot[data-dirty='true'] { background: var(--dsw-alias-state-warn-primary); }
727
+ .dspi-loading { display: flex; align-items: center; gap: 8px; margin-top: 8px; font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-tertiary); }
728
+ .dspi-spinner {
729
+ width: 14px;
730
+ height: 14px;
731
+ border: 2px solid var(--dsw-alias-border-l3);
732
+ border-top-color: var(--dsw-alias-state-business-primary);
733
+ border-radius: 50%;
734
+ animation: dspi-spin .8s linear infinite;
735
+ }
736
+ @keyframes dspi-spin { to { transform: rotate(360deg); } }
737
+ .dspi-spin { display: inline-flex; animation: dspi-spin 1s linear infinite; }
738
+ .dspi-skeleton { height: 118px; margin-top: 12px; border-radius: 16px; background: var(--dsw-alias-bg-skeleton, var(--dsw-alias-bg-layer-2)); }
739
+ .dspi-state { display: flex; align-items: flex-start; gap: 12px; padding: 16px; border: .5px solid var(--dsw-alias-border-l3); border-radius: 16px; background: var(--dsw-alias-bg-layer-1); }
740
+ .dspi-state-icon { flex: none; margin-top: 1px; color: var(--dsw-alias-state-error-primary); }
741
+ .dspi-state-body { display: flex; flex-direction: column; gap: 4px; flex: 1 1 auto; min-width: 0; }
742
+ .dspi-state-title { margin: 0; font-size: 14px; font-weight: 500; line-height: 22px; color: var(--dsw-alias-label-primary); }
743
+ .dspi-state-hint { margin: 0; font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-tertiary); overflow-wrap: anywhere; }
744
+ @media (max-width: 720px) {
745
+ .dspi-head { gap: 12px; }
746
+ .dspi-head-actions { width: 100%; }
747
+ }
748
+ @media (max-width: 560px) {
749
+ .dspi-card { padding: 10px 12px 8px; }
750
+ .dspi-card-head-main { flex-basis: 100%; }
751
+ .dspi-card-head-side { margin-left: 0; }
752
+ .dspi-check { margin-left: 0; }
753
+ .dspi-meta code { max-width: 100%; }
754
+ }
755
+ @media (prefers-reduced-motion: reduce) {
756
+ .dspi-spinner { animation: none; }
757
+ .dspi-card, .dspi-btn, .dspi-switch, .dspi-switch-thumb, .dspi-icon-btn { transition: none; }
758
+ }
759
+ `;
760
+ /**
761
+ * One 16×16 stroke glyph.
762
+ *
763
+ * @param h - React's createElement.
764
+ * @param name - which path set to draw.
765
+ * @param size - rendered square edge.
766
+ * @returns the SVG element.
767
+ */
768
+ function glyph(h, name, size = 14) {
769
+ const svg = (children) => h("svg", {
770
+ viewBox: "0 0 16 16",
771
+ width: size,
772
+ height: size,
773
+ fill: "none",
774
+ stroke: "currentColor",
775
+ strokeWidth: "1.3",
776
+ strokeLinecap: "round",
777
+ strokeLinejoin: "round",
778
+ "aria-hidden": "true"
779
+ }, ...children);
780
+ switch (name) {
781
+ case "up": return svg([h("path", { d: "M4 9.5 8 5.5l4 4" })]);
782
+ case "down": return svg([h("path", { d: "M4 6.5 8 10.5l4-4" })]);
783
+ case "trash": return svg([h("path", { d: "M3 4.5h10M6.2 4.5V3.3h3.6v1.2M4.6 4.5l.5 8.2h5.8l.5-8.2M6.8 6.8v3.6M9.2 6.8v3.6" })]);
784
+ case "warning": return svg([h("path", { d: "M8 2.2 14 13H2L8 2.2Z" }), h("path", { d: "M8 6.3v3.3M8 11.6v.1" })]);
785
+ case "check": return svg([h("path", { d: "M3.2 8.4 6.4 11.6 12.8 4.8" })]);
786
+ case "close": return svg([h("path", { d: "M4.2 4.2l7.6 7.6M11.8 4.2l-7.6 7.6" })]);
787
+ case "copy": return svg([h("rect", {
788
+ x: "5.5",
789
+ y: "5.5",
790
+ width: "7",
791
+ height: "7",
792
+ rx: "1.2"
793
+ }), h("path", { d: "M10.5 5.5V3.8a1 1 0 0 0-1-1H4.3a1.5 1.5 0 0 0-1.5 1.5v5.2a1 1 0 0 0 1 1h1.7" })]);
794
+ case "refresh": return svg([h("path", { d: "M13.5 8a5.5 5.5 0 1 1-1.6-3.9" }), h("path", { d: "M13.5 3v4h-4" })]);
795
+ default: return svg([h("rect", {
796
+ x: "2",
797
+ y: "2.5",
798
+ width: "12",
799
+ height: "11",
800
+ rx: "1.5"
801
+ }), h("path", { d: "M4.9 6h6.2M4.9 8.4h6.2M4.9 10.8h3.4" })]);
802
+ }
803
+ }
804
+ /**
805
+ * The sidebar row's glyph.
806
+ *
807
+ * The shell draws the row box, the label, the tooltip and the collapsed rail,
808
+ * then asks this component for a `size`-square, `currentColor` glyph; `active`
809
+ * is the shell's selection state and the shell already paints it, so the glyph
810
+ * stays decorative. `data-dsh-panel-entry` marks the only DOM this plugin owns
811
+ * inside that row, which is how skins and diagnostics identify the entry.
812
+ *
813
+ * @param props - the shell's icon share: square edge and selection state.
814
+ * @returns the decorative section-list glyph.
815
+ */
816
+ function PanelIcon(props) {
817
+ const h = require("react").createElement;
818
+ return h("svg", {
819
+ "data-dsh-panel-entry": PANEL_ID,
820
+ viewBox: "0 0 16 16",
821
+ width: props.size,
822
+ height: props.size,
823
+ fill: "none",
824
+ stroke: "currentColor",
825
+ strokeWidth: "1.3",
826
+ strokeLinecap: "round",
827
+ strokeLinejoin: "round",
828
+ "aria-hidden": "true"
829
+ }, h("rect", {
830
+ x: "2",
831
+ y: "2.5",
832
+ width: "12",
833
+ height: "11",
834
+ rx: "1.5"
835
+ }), h("path", { d: "M4.9 6h6.2M4.9 8.4h6.2M4.9 10.8h3.4" }));
836
+ }
837
+ /** The shell's switch geometry, drawn from theme tokens. */
838
+ function Switch(props) {
839
+ const h = require("react").createElement;
840
+ return h("button", {
841
+ type: "button",
842
+ role: "switch",
843
+ className: "dspi-switch",
844
+ "aria-checked": props.checked,
845
+ "aria-label": props.label,
846
+ ...props.title === void 0 ? {} : { title: props.title },
847
+ ...props.disabled === true ? { disabled: true } : {},
848
+ onClick: () => props.onChange(!props.checked)
849
+ }, h("span", {
850
+ className: "dspi-switch-thumb",
851
+ "aria-hidden": "true"
852
+ }));
853
+ }
854
+ /** One section editor card. */
855
+ function SectionCard(props) {
856
+ const h = require("react").createElement;
857
+ const { section, t } = props;
858
+ const bytes = byteLength(section.text);
859
+ const hint = placementHint(t, section.position);
860
+ const empty = section.text.trim() === "";
861
+ return h("div", {
862
+ className: "dspi-card",
863
+ "data-enabled": section.enabled ? "true" : "false",
864
+ "data-section-id": section.id
865
+ }, h("div", { className: "dspi-card-head" }, h("div", { className: "dspi-card-head-main" }, h(Switch, {
866
+ checked: section.enabled,
867
+ label: format(t("toggleNamed"), { title: section.title || t("section") }),
868
+ title: section.enabled ? t("enabled") : t("disabledChip"),
869
+ onChange: (next) => props.onChange({
870
+ ...section,
871
+ enabled: next
872
+ })
873
+ }), h("input", {
874
+ className: "dspi-title-input",
875
+ value: section.title,
876
+ maxLength: 80,
877
+ autoFocus: props.autoFocus,
878
+ placeholder: t("sectionTitle"),
879
+ "aria-label": format(t("sectionTitleOf"), { index: props.index + 1 }),
880
+ onChange: (event) => props.onChange({
881
+ ...section,
882
+ title: event.target.value
883
+ })
884
+ })), h("div", { className: "dspi-card-head-side" }, h("span", { className: "dspi-select-wrap" }, h("select", {
885
+ className: "dspi-select",
886
+ value: section.position,
887
+ title: hint,
888
+ "aria-label": format(t("positionOf"), { index: props.index + 1 }),
889
+ onChange: (event) => props.onChange({
890
+ ...section,
891
+ position: event.target.value
892
+ })
893
+ }, h("option", { value: "head" }, t("positionHead")), h("option", { value: "body" }, t("positionBody")), h("option", { value: "tail" }, t("positionTail"))), h("svg", {
894
+ className: "dspi-caret",
895
+ viewBox: "0 0 12 12",
896
+ fill: "none",
897
+ stroke: "currentColor",
898
+ strokeWidth: "1.2",
899
+ strokeLinecap: "round",
900
+ strokeLinejoin: "round",
901
+ "aria-hidden": "true"
902
+ }, h("path", { d: "M3 4.6 6 7.6 9 4.6" }))), h("button", {
903
+ type: "button",
904
+ className: "dspi-icon-btn",
905
+ title: format(t("moveUpNamed"), { title: section.title || t("section") }),
906
+ "aria-label": format(t("moveUpNamed"), { title: section.title || t("section") }),
907
+ disabled: props.index === 0,
908
+ onClick: () => props.onMove(-1)
909
+ }, glyph(h, "up", 15)), h("button", {
910
+ type: "button",
911
+ className: "dspi-icon-btn",
912
+ title: format(t("moveDownNamed"), { title: section.title || t("section") }),
913
+ "aria-label": format(t("moveDownNamed"), { title: section.title || t("section") }),
914
+ disabled: props.index === props.total - 1,
915
+ onClick: () => props.onMove(1)
916
+ }, glyph(h, "down", 15)), h("button", {
917
+ type: "button",
918
+ className: "dspi-icon-btn dspi-icon-btn--danger",
919
+ title: format(t("removeNamed"), { title: section.title || t("section") }),
920
+ "aria-label": format(t("removeNamed"), { title: section.title || t("section") }),
921
+ onClick: props.onRemove
922
+ }, glyph(h, "trash", 15)))), h("textarea", {
923
+ className: "dspi-textarea",
924
+ value: section.text,
925
+ rows: Math.min(18, Math.max(5, section.text.split("\n").length + 2)),
926
+ placeholder: t("textPlaceholder"),
927
+ "aria-label": format(t("sectionTextOf"), { index: props.index + 1 }),
928
+ spellCheck: false,
929
+ onChange: (event) => props.onChange({
930
+ ...section,
931
+ text: event.target.value
932
+ })
933
+ }), h("div", { className: "dspi-card-meta" }, h("span", null, format(t("bytes"), { bytes })), h("span", { className: "dspi-card-hint" }, hint), section.enabled ? empty ? h("span", { className: "dspi-chip dspi-chip--warn" }, t("emptyChip")) : null : h("span", { className: "dspi-chip" }, t("disabledChip")), h("label", {
934
+ className: "dspi-check",
935
+ title: t("interpolateHint")
936
+ }, h("input", {
937
+ type: "checkbox",
938
+ checked: section.interpolate,
939
+ onChange: (event) => props.onChange({
940
+ ...section,
941
+ interpolate: event.target.checked
942
+ })
943
+ }), t("interpolate"))));
944
+ }
945
+ /** The section list, including its empty state. */
946
+ function SectionList(props) {
947
+ const h = require("react").createElement;
948
+ const { sections, t } = props;
949
+ if (sections.length === 0) return h("div", { className: "dspi-empty" }, h("span", { className: "dspi-empty-glyph" }, glyph(h, "sections", 28)), h("p", { className: "dspi-empty-title" }, t("emptyListTitle")), h("p", { className: "dspi-empty-hint" }, t("emptyListHint")), h("div", { className: "dspi-empty-action" }, h("button", {
950
+ type: "button",
951
+ className: "dspi-btn dspi-btn--primary",
952
+ onClick: props.onAdd
953
+ }, `+ ${t("addFirst")}`)));
954
+ return h("ol", { className: "dspi-list" }, sections.map((section, index) => h("li", {
955
+ key: section.id,
956
+ className: "dspi-list-item"
957
+ }, h(SectionCard, {
958
+ section,
959
+ index,
960
+ total: sections.length,
961
+ autoFocus: section.id === props.justAdded,
962
+ t,
963
+ onChange: (next) => props.onChange(index, next),
964
+ onRemove: () => props.onRemove(index),
965
+ onMove: (delta) => props.onMove(index, delta)
966
+ }))));
967
+ }
968
+ /** The assembled-prompt preview, shown above the editor. */
969
+ function PreviewPanel(props) {
970
+ const React = require("react");
971
+ const h = React.createElement;
972
+ const { preview, t } = props;
973
+ const copyLabel = props.copying === "copied" ? t("copied") : props.copying === "failed" ? t("copyFailed") : t("copy");
974
+ const copyGlyph = props.copying === "copied" ? "check" : props.copying === "failed" ? "warning" : "copy";
975
+ return h(React.Fragment, null, h("div", {
976
+ className: "dspi-section-head",
977
+ id: "dsh-system-prompt-insertion-preview"
978
+ }, h("h2", { className: "dspi-section-title" }, t("previewTitle")), h("button", {
979
+ type: "button",
980
+ className: "dspi-icon-btn dspi-preview-refresh",
981
+ onClick: props.onRefresh,
982
+ disabled: props.previewing,
983
+ "data-busy": props.previewing ? "true" : "false",
984
+ title: t("refreshPreview"),
985
+ "aria-label": t("refreshPreview")
986
+ }, h("span", {
987
+ className: props.previewing ? "dspi-spin" : void 0,
988
+ "aria-hidden": "true"
989
+ }, glyph(h, "refresh", 15))), preview === void 0 ? null : h("span", { className: "dspi-section-meta" }, format(t("previewSummary"), {
990
+ count: preview.sections.length,
991
+ bytes: preview.bytes
992
+ })), preview === void 0 ? null : h("div", { className: "dspi-section-actions" }, h("button", {
993
+ type: "button",
994
+ className: "dspi-btn dspi-btn--sm",
995
+ onClick: props.onCopy,
996
+ disabled: preview.rendered === "",
997
+ title: props.copying === "failed" ? t("copyFailedHint") : t("copy")
998
+ }, glyph(h, copyGlyph, 12), copyLabel))), props.previewError === void 0 ? null : h("div", {
999
+ className: "dspi-banner dspi-banner--error",
1000
+ role: "alert"
1001
+ }, h("span", { className: "dspi-banner-icon" }, glyph(h, "warning")), h("div", { className: "dspi-banner-text" }, props.previewError), h("button", {
1002
+ type: "button",
1003
+ className: "dspi-btn dspi-btn--sm",
1004
+ onClick: props.onRefresh
1005
+ }, t("retry"))), preview === void 0 ? props.previewError === void 0 ? h("div", { className: "dspi-panel dspi-panel--idle" }, props.previewing ? h("div", { className: "dspi-loading" }, h("span", {
1006
+ className: "dspi-spinner",
1007
+ "aria-hidden": "true"
1008
+ }), h("span", null, t("previewing"))) : h("p", null, t("previewNone"))) : null : h("div", { className: "dspi-panel" }, props.dirty ? h("p", { className: "dspi-note dspi-note--warn" }, t("previewStale")) : null, h("div", { className: "dspi-inline" }, h("span", null, t("variables")), preview.variables.length === 0 ? h("span", null, "—") : preview.variables.map((name) => h("code", {
1009
+ key: name,
1010
+ className: "dspi-var"
1011
+ }, name))), h("div", null, h("h3", { className: "dspi-subhead" }, t("warnings")), preview.warnings.length === 0 ? h("div", { className: "dspi-check-row dspi-check-row--ok" }, h("span", { className: "dspi-check-icon" }, glyph(h, "check", 12)), h("span", null, t("noWarnings"))) : h("ul", { className: "dspi-checks" }, preview.warnings.map((warning, index) => h("li", {
1012
+ key: `${warning.code}-${warning.sectionId ?? index}`,
1013
+ className: "dspi-check-row"
1014
+ }, h("span", {
1015
+ className: "dspi-check-dot",
1016
+ "aria-hidden": "true"
1017
+ }), h("span", null, warningText(t, warning)))))), h("div", { className: "dspi-table-scroll" }, h("table", { className: "dspi-table" }, h("thead", null, h("tr", null, h("th", {
1018
+ scope: "col",
1019
+ className: "dspi-col-index"
1020
+ }, "#"), h("th", {
1021
+ scope: "col",
1022
+ className: "dspi-col-order"
1023
+ }, t("orderShort")), h("th", { scope: "col" }, t("section")), h("th", {
1024
+ scope: "col",
1025
+ className: "dspi-col-bytes"
1026
+ }, t("bytesShort")))), h("tbody", null, preview.sections.map((section) => h("tr", {
1027
+ key: `${section.name}-${section.index}`,
1028
+ "data-ours": section.ours ? "true" : "false"
1029
+ }, h("td", null, String(section.index + 1).padStart(2, "0")), h("td", null, section.order === void 0 ? "—" : String(section.order)), h("td", null, h("span", { className: "dspi-name-cell" }, h("span", { className: "dspi-name" }, section.name), section.ours ? h("span", { className: "dspi-tag" }, t("ours")) : null)), h("td", { className: "dspi-col-bytes" }, `${section.bytes}B`)))))), h("pre", {
1030
+ className: "dspi-pre",
1031
+ tabIndex: 0,
1032
+ "aria-label": t("previewTitle")
1033
+ }, preview.rendered === "" ? "—" : preview.rendered)));
1034
+ }
1035
+ /** The sidebar panel body. */
1036
+ function PromptInsertionPanel(props) {
1037
+ const React = require("react");
1038
+ const h = React.createElement;
1039
+ const t = props.t ?? ((key) => key);
1040
+ const saveKeys = typeof document !== "undefined" && document.documentElement.dataset.platform === "darwin" ? "⌘S" : "Ctrl+S";
1041
+ const [phase, setPhase] = React.useState("loading");
1042
+ const [payload, setPayload] = React.useState(void 0);
1043
+ const [draft, setDraft] = React.useState(void 0);
1044
+ const [error, setError] = React.useState(void 0);
1045
+ const [saving, setSaving] = React.useState(false);
1046
+ const [preview, setPreview] = React.useState(void 0);
1047
+ const [previewing, setPreviewing] = React.useState(false);
1048
+ const [previewError, setPreviewError] = React.useState(void 0);
1049
+ const [copying, setCopying] = React.useState("idle");
1050
+ const [justAdded, setJustAdded] = React.useState(void 0);
1051
+ const adopt = React.useCallback((next) => {
1052
+ setPayload(next);
1053
+ setDraft(next.state);
1054
+ setPhase("ready");
1055
+ setError(void 0);
1056
+ }, []);
1057
+ /** Bumped on every preview request; only the newest answer may land. */
1058
+ const previewTicket = React.useRef(0);
1059
+ const runPreview = React.useCallback(() => {
1060
+ const ticket = ++previewTicket.current;
1061
+ setPreviewing(true);
1062
+ setPreviewError(void 0);
1063
+ fetch(`${API}/preview`, { headers: { accept: "application/json" } }).then((response) => readJson(response)).then((body) => {
1064
+ if (ticket !== previewTicket.current) return;
1065
+ setPreviewing(false);
1066
+ if (body.ok === true) {
1067
+ setPreview(body);
1068
+ return;
1069
+ }
1070
+ setPreviewError(errorText(body, t("previewFailed")));
1071
+ }).catch((cause) => {
1072
+ if (ticket !== previewTicket.current) return;
1073
+ setPreviewing(false);
1074
+ setPreviewError(`${t("previewFailed")}: ${cause instanceof Error ? cause.message : String(cause)}`);
1075
+ });
1076
+ }, [t]);
1077
+ const refresh = React.useCallback(() => {
1078
+ setPhase("loading");
1079
+ fetch(`${API}/state`, { headers: { accept: "application/json" } }).then((response) => readJson(response)).then((body) => {
1080
+ if (body.ok === true) {
1081
+ adopt(payloadFrom(body, "missing"));
1082
+ runPreview();
1083
+ return;
1084
+ }
1085
+ setPhase("error");
1086
+ setError(errorText(body, t("loadFailed")));
1087
+ }).catch((cause) => {
1088
+ setPhase("error");
1089
+ setError(`${t("loadFailed")}: ${cause instanceof Error ? cause.message : String(cause)}`);
1090
+ });
1091
+ }, [
1092
+ adopt,
1093
+ runPreview,
1094
+ t
1095
+ ]);
1096
+ React.useEffect(() => {
1097
+ refresh();
1098
+ }, [refresh]);
1099
+ const dirty = draft !== void 0 && payload !== void 0 && JSON.stringify(draft) !== JSON.stringify(payload.state);
1100
+ const save = React.useCallback(() => {
1101
+ if (draft === void 0 || saving) return;
1102
+ setSaving(true);
1103
+ setError(void 0);
1104
+ fetch(`${API}/state`, {
1105
+ method: "POST",
1106
+ headers: {
1107
+ "content-type": "application/json",
1108
+ accept: "application/json"
1109
+ },
1110
+ body: JSON.stringify(draft)
1111
+ }).then((response) => readJson(response)).then((body) => {
1112
+ setSaving(false);
1113
+ if (body.ok === true) {
1114
+ adopt(payloadFrom(body, "memory"));
1115
+ runPreview();
1116
+ return;
1117
+ }
1118
+ setError(errorText(body, t("saveFailed")));
1119
+ }).catch((cause) => {
1120
+ setSaving(false);
1121
+ setError(`${t("saveFailed")}: ${cause instanceof Error ? cause.message : String(cause)}`);
1122
+ });
1123
+ }, [
1124
+ adopt,
1125
+ draft,
1126
+ runPreview,
1127
+ saving,
1128
+ t
1129
+ ]);
1130
+ /**
1131
+ * Apply the master switch at once instead of staging it.
1132
+ *
1133
+ * The switch controls what the host registers, so a staged toggle would leave
1134
+ * the preview disagreeing with the control until Save. Only the saved
1135
+ * document's `enabled` field is sent; unsaved section edits stay staged.
1136
+ */
1137
+ const toggleEnabled = React.useCallback((next) => {
1138
+ if (payload === void 0 || draft === void 0 || saving) return;
1139
+ const previous = payload.state.enabled;
1140
+ setSaving(true);
1141
+ setError(void 0);
1142
+ setDraft({
1143
+ ...draft,
1144
+ enabled: next
1145
+ });
1146
+ fetch(`${API}/state`, {
1147
+ method: "POST",
1148
+ headers: {
1149
+ "content-type": "application/json",
1150
+ accept: "application/json"
1151
+ },
1152
+ body: JSON.stringify({
1153
+ ...payload.state,
1154
+ enabled: next
1155
+ })
1156
+ }).then((response) => readJson(response)).then((body) => {
1157
+ setSaving(false);
1158
+ if (body.ok === true) {
1159
+ const applied = payloadFrom(body, "memory");
1160
+ setPayload(applied);
1161
+ setDraft((current) => current === void 0 ? applied.state : {
1162
+ ...current,
1163
+ enabled: next
1164
+ });
1165
+ runPreview();
1166
+ return;
1167
+ }
1168
+ setDraft((current) => current === void 0 ? current : {
1169
+ ...current,
1170
+ enabled: previous
1171
+ });
1172
+ setError(errorText(body, t("saveFailed")));
1173
+ }).catch((cause) => {
1174
+ setSaving(false);
1175
+ setDraft((current) => current === void 0 ? current : {
1176
+ ...current,
1177
+ enabled: previous
1178
+ });
1179
+ setError(`${t("saveFailed")}: ${cause instanceof Error ? cause.message : String(cause)}`);
1180
+ });
1181
+ }, [
1182
+ draft,
1183
+ payload,
1184
+ runPreview,
1185
+ saving,
1186
+ t
1187
+ ]);
1188
+ React.useEffect(() => {
1189
+ if (!dirty) return;
1190
+ const onKeyDown = (event) => {
1191
+ if (!(event.metaKey || event.ctrlKey) || event.key.toLowerCase() !== "s") return;
1192
+ if (document.querySelector("[role=\"dialog\"], [role=\"alertdialog\"]") !== null) return;
1193
+ event.preventDefault();
1194
+ save();
1195
+ };
1196
+ document.addEventListener("keydown", onKeyDown);
1197
+ return () => document.removeEventListener("keydown", onKeyDown);
1198
+ }, [dirty, save]);
1199
+ const updateSection = React.useCallback((index, next) => {
1200
+ setDraft((current) => {
1201
+ if (current === void 0) return current;
1202
+ const sections = [...current.sections];
1203
+ sections[index] = next;
1204
+ return {
1205
+ ...current,
1206
+ sections
1207
+ };
1208
+ });
1209
+ }, []);
1210
+ const removeSection = React.useCallback((index) => {
1211
+ setDraft((current) => current === void 0 ? current : {
1212
+ ...current,
1213
+ sections: current.sections.filter((_, position) => position !== index)
1214
+ });
1215
+ }, []);
1216
+ const moveSection = React.useCallback((index, delta) => {
1217
+ setDraft((current) => {
1218
+ if (current === void 0) return current;
1219
+ const target = index + delta;
1220
+ if (target < 0 || target >= current.sections.length) return current;
1221
+ const sections = [...current.sections];
1222
+ const [moved] = sections.splice(index, 1);
1223
+ if (moved === void 0) return current;
1224
+ sections.splice(target, 0, moved);
1225
+ return {
1226
+ ...current,
1227
+ sections
1228
+ };
1229
+ });
1230
+ }, []);
1231
+ const addSection = React.useCallback(() => {
1232
+ const id = newId();
1233
+ setJustAdded(id);
1234
+ setDraft((current) => current === void 0 ? current : {
1235
+ ...current,
1236
+ sections: [...current.sections, {
1237
+ id,
1238
+ title: `${t("section")} ${current.sections.length + 1}`,
1239
+ text: "",
1240
+ position: "tail",
1241
+ order: null,
1242
+ enabled: true,
1243
+ interpolate: false
1244
+ }]
1245
+ });
1246
+ }, [t]);
1247
+ const copyRendered = React.useCallback(() => {
1248
+ if (preview === void 0) return;
1249
+ const clipboard = typeof navigator === "undefined" ? void 0 : navigator.clipboard;
1250
+ if (clipboard === void 0) {
1251
+ setCopying("failed");
1252
+ return;
1253
+ }
1254
+ clipboard.writeText(preview.rendered).then(() => {
1255
+ setCopying("copied");
1256
+ window.setTimeout(() => setCopying("idle"), 1600);
1257
+ }, () => setCopying("failed"));
1258
+ }, [preview]);
1259
+ const shell = (content) => h("div", { className: "dspi-root" }, h("style", { "data-dsh-system-prompt-insertion": "styles" }, PANEL_CSS), h("div", { className: "dspi-scroll" }, h("div", { className: "dspi-content" }, ...content)));
1260
+ const heading = (extra, actions) => h("header", { className: "dspi-head" }, h("div", { className: "dspi-head-main" }, h("h1", { className: "dspi-title" }, t("title")), h("p", { className: "dspi-subtitle" }, t("description")), extra ?? null), actions ?? null);
1261
+ if (phase === "loading") return shell([
1262
+ heading(),
1263
+ h("div", {
1264
+ className: "dspi-loading",
1265
+ role: "status"
1266
+ }, h("span", {
1267
+ className: "dspi-spinner",
1268
+ "aria-hidden": "true"
1269
+ }), h("span", null, t("loading"))),
1270
+ h("div", {
1271
+ className: "dspi-skeleton",
1272
+ "aria-hidden": "true"
1273
+ }),
1274
+ h("div", {
1275
+ className: "dspi-skeleton",
1276
+ "aria-hidden": "true"
1277
+ })
1278
+ ]);
1279
+ if (phase === "error" || draft === void 0 || payload === void 0) return shell([heading(), h("div", { className: "dspi-state" }, h("span", { className: "dspi-state-icon" }, glyph(h, "warning", 16)), h("div", { className: "dspi-state-body" }, h("p", { className: "dspi-state-title" }, t("loadFailed")), h("p", { className: "dspi-state-hint" }, withoutPrefix(error, t("loadFailed")) ?? t("loadFailed"))), h("button", {
1280
+ type: "button",
1281
+ className: "dspi-btn dspi-btn--primary",
1282
+ onClick: refresh
1283
+ }, t("retry")))]);
1284
+ const origin = originLabel(t, payload.origin);
1285
+ const registered = payload.registered.length;
1286
+ const showStoreWarning = payload.error !== void 0;
1287
+ const saveHint = format(t("saveHint"), { keys: saveKeys });
1288
+ return shell([
1289
+ heading(h("div", { className: "dspi-meta" }, h("span", null, `${t("pathLabel")} `, h("code", { title: payload.path }, payload.path)), h("span", {
1290
+ className: "dspi-origin",
1291
+ "data-origin": payload.origin
1292
+ }, origin), h("span", { title: t("tailBaseHint") }, format(t("tailBase"), { order: payload.orderBases.tailBase }))), h("div", { className: "dspi-head-actions" }, h("div", {
1293
+ className: "dspi-master",
1294
+ title: t("enabledHint")
1295
+ }, h("span", null, t("enabled")), h(Switch, {
1296
+ checked: draft.enabled,
1297
+ label: t("enabled"),
1298
+ title: t("enabledHint"),
1299
+ disabled: saving,
1300
+ onChange: toggleEnabled
1301
+ })))),
1302
+ error === void 0 ? null : h("div", {
1303
+ className: "dspi-banner dspi-banner--error",
1304
+ role: "alert"
1305
+ }, h("span", { className: "dspi-banner-icon" }, glyph(h, "warning")), h("div", { className: "dspi-banner-text" }, error), h("button", {
1306
+ type: "button",
1307
+ className: "dspi-icon-btn",
1308
+ "aria-label": t("dismiss"),
1309
+ title: t("dismiss"),
1310
+ onClick: () => setError(void 0)
1311
+ }, glyph(h, "close"))),
1312
+ showStoreWarning ? h("div", {
1313
+ className: "dspi-banner dspi-banner--warn",
1314
+ role: "status"
1315
+ }, h("span", { className: "dspi-banner-icon" }, glyph(h, "warning")), h("div", { className: "dspi-banner-text" }, format(t("warnStoreError"), { message: payload.error ?? "" }))) : null,
1316
+ h("section", { className: "dspi-section dspi-section--preview" }, h(PreviewPanel, {
1317
+ preview,
1318
+ previewing,
1319
+ previewError,
1320
+ dirty,
1321
+ copying,
1322
+ t,
1323
+ onCopy: copyRendered,
1324
+ onRefresh: runPreview
1325
+ })),
1326
+ h("section", { className: "dspi-section" }, h("div", { className: "dspi-section-head" }, h("h2", { className: "dspi-section-title" }, t("sectionsTitle")), h("span", { className: "dspi-section-meta" }, format(t("sectionsSummary"), {
1327
+ count: draft.sections.length,
1328
+ registered
1329
+ }))), h(SectionList, {
1330
+ sections: draft.sections,
1331
+ justAdded,
1332
+ t,
1333
+ onAdd: addSection,
1334
+ onChange: updateSection,
1335
+ onRemove: removeSection,
1336
+ onMove: moveSection
1337
+ }), draft.sections.length === 0 ? null : h("button", {
1338
+ type: "button",
1339
+ className: "dspi-add",
1340
+ onClick: addSection
1341
+ }, `+ ${t("add")}`), h("div", { className: "dspi-settings-actions" }, h("div", {
1342
+ className: "dspi-status",
1343
+ role: "status",
1344
+ "aria-live": "polite"
1345
+ }, h("span", {
1346
+ className: "dspi-status-dot",
1347
+ "data-dirty": dirty || saving ? "true" : "false",
1348
+ "aria-hidden": "true"
1349
+ }), h("span", null, saving ? t("saving") : dirty ? t("statusDirty") : t("statusSaved"))), dirty ? h("button", {
1350
+ type: "button",
1351
+ className: "dspi-btn",
1352
+ disabled: saving,
1353
+ onClick: () => setDraft(payload.state)
1354
+ }, t("revert")) : null, h("button", {
1355
+ type: "button",
1356
+ className: "dspi-btn dspi-btn--primary",
1357
+ onClick: save,
1358
+ disabled: !dirty || saving,
1359
+ title: saveHint
1360
+ }, saving ? t("saving") : t("save"))))
1361
+ ]);
1362
+ }
1363
+ /** Locale + slot registration. */
1364
+ const inject = ["slots", "locale"];
1365
+ /**
1366
+ * Register this package's sidebar row and main-column panel.
1367
+ *
1368
+ * The two seats are declared by shell plugins, so each registration is wrapped
1369
+ * in `ctx.slots.inject`: the callback runs only after the owning entry declares
1370
+ * the seat, and a shell that cannot serve the seats leaves the panel simply
1371
+ * absent instead of failing boot.
1372
+ *
1373
+ * @param ctx - the client context carrying `slots` and `locale`.
1374
+ */
1375
+ function apply(ctx) {
1376
+ ctx.effect(() => ctx.locale.register(NS, {
1377
+ zh,
1378
+ en
1379
+ }), "dsh-system-prompt-insertion: dictionaries");
1380
+ const t = ctx.locale.bind(NS);
1381
+ ctx.effect(() => {
1382
+ const offRow = ctx.slots.inject("sidebar.panellist", () => ctx.slots.register({
1383
+ name: "sidebar.panellist",
1384
+ id: PANEL_ID,
1385
+ order: 30,
1386
+ label: () => t("nav")
1387
+ }, PanelIcon));
1388
+ const offPanel = ctx.slots.inject("main", () => ctx.slots.register({
1389
+ name: "main",
1390
+ key: PANEL_ID,
1391
+ inject: () => ({ t })
1392
+ }, PromptInsertionPanel));
1393
+ return () => {
1394
+ offPanel();
1395
+ offRow();
1396
+ };
1397
+ }, "dsh-system-prompt-insertion: sidebar panel");
1398
+ }
1399
+ exports.NS = NS;
1400
+ exports.PANEL_CSS = PANEL_CSS;
1401
+ exports.PANEL_ID = PANEL_ID;
1402
+ exports.PANEL_ORDER = PANEL_ORDER;
1403
+ exports.POSITIONS = POSITIONS;
1404
+ exports.PanelIcon = PanelIcon;
1405
+ exports.PreviewPanel = PreviewPanel;
1406
+ exports.PromptInsertionPanel = PromptInsertionPanel;
1407
+ exports.SectionCard = SectionCard;
1408
+ exports.SectionList = SectionList;
1409
+ exports.Switch = Switch;
1410
+ exports.apply = apply;
1411
+ exports.inject = inject;
1412
+ return module.exports;
1413
+ }
1414
+ });