dsh-immune-system 0.9.2 → 1.0.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.
Files changed (28) hide show
  1. package/README.md +185 -16
  2. package/dsh-immune-system/client/client.js +375 -159
  3. package/dsh-immune-system/dist/core/distiller.js +78 -0
  4. package/dsh-immune-system/dist/core/pairing.js +178 -0
  5. package/dsh-immune-system/dist/core/prompts.js +88 -2
  6. package/dsh-immune-system/dist/core/similarity.js +51 -0
  7. package/dsh-immune-system/dist/core/store/antibodies.js +37 -8
  8. package/dsh-immune-system/dist/core/store/episodes.js +19 -5
  9. package/dsh-immune-system/dist/core/store/index.js +58 -35
  10. package/dsh-immune-system/dist/core/store/jobs.js +17 -0
  11. package/dsh-immune-system/dist/core/store/open.js +1 -0
  12. package/dsh-immune-system/dist/core/store/rerank.js +57 -0
  13. package/dsh-immune-system/dist/core/store/schema.js +9 -0
  14. package/dsh-immune-system/dist/core/store/turn-state.js +52 -0
  15. package/dsh-immune-system/dist/core/ui/page.js +481 -132
  16. package/dsh-immune-system/dist/core/ui/query.js +12 -2
  17. package/dsh-immune-system/dist/core/ui/server.js +2 -2
  18. package/dsh-immune-system/dist/dsh/src/dsh-adapter.js +66 -17
  19. package/dsh-immune-system/dist/dsh/src/index.js +143 -9
  20. package/dsh-immune-system/dist/dsh/src/ui-route.js +7 -3
  21. package/dsh-immune-system/dist/zcode/src/doctor.js +51 -3
  22. package/dsh-immune-system/dist/zcode/src/drain.js +73 -20
  23. package/dsh-immune-system/dist/zcode/src/hooks/post-tool-use-failure.js +3 -1
  24. package/dsh-immune-system/dist/zcode/src/hooks/post-tool-use.js +4 -2
  25. package/dsh-immune-system/dist/zcode/src/hooks/session-start.js +6 -0
  26. package/dsh-immune-system/dist/zcode/src/hooks/user-prompt-submit.js +3 -0
  27. package/dsh-immune-system/package.json +7 -7
  28. package/package.json +4 -3
@@ -9,6 +9,11 @@
9
9
  * Written without JSX for the same reason — `React.createElement` is enough for
10
10
  * one tab, and it keeps "build" out of the client entirely.
11
11
  *
12
+ * Styling: one injected <style> block (scoped under `.imm`) instead of dozens of
13
+ * inline style objects, so hover states, the responsive card layout and the
14
+ * light/dark tokens are expressible at all. The block is static text this file
15
+ * owns, so there is no interpolation and nothing external.
16
+ *
12
17
  * Data comes from the host half's route (`/plugins/dsh-immune-system/api`),
13
18
  * registered through `ctx.inject(['webServer'], …)`. Nothing here talks to
14
19
  * SQLite directly; the browser never sees a database path or a file handle.
@@ -20,8 +25,106 @@ window.__ModuleLoader__.load({
20
25
  var exports = module.exports;
21
26
  var React = require('react');
22
27
  var h = React.createElement;
28
+ var ReactDOM = null;
29
+ try {
30
+ ReactDOM = require('react-dom');
31
+ } catch (error) {
32
+ ReactDOM = null;
33
+ }
23
34
 
24
35
  var API = '/plugins/dsh-immune-system/api';
36
+ var STYLE_ID = 'dsh-immune-system-style';
37
+
38
+ var CSS = [
39
+ '.imm{--i-line:var(--dsh-border,#2c3240);--i-line-soft:var(--dsh-border-subtle,#242a36);',
40
+ '--i-panel:var(--dsh-bg-secondary,#141821);--i-panel2:var(--dsh-bg-tertiary,#1b2231);',
41
+ '--i-text:var(--dsh-text-primary,inherit);--i-muted:var(--dsh-text-secondary,#8b94a3);',
42
+ '--i-faint:var(--dsh-text-tertiary,#6b7789);--i-accent:var(--dsh-accent,#4f9cf9);',
43
+ '--i-ok:#37c98a;--i-warn:#e3a53f;--i-bad:#e56565;',
44
+ 'display:flex;flex-direction:column;gap:12px;padding:2px;font-size:13px;color:var(--i-text)}',
45
+ '.imm .row{display:flex;gap:10px;align-items:center;flex-wrap:wrap}',
46
+ '.imm .kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(132px,1fr));gap:10px}',
47
+ '.imm .kpi{position:relative;background:var(--i-panel);border:1px solid var(--i-line);',
48
+ 'border-radius:11px;padding:10px 13px;overflow:hidden}',
49
+ '.imm .kpi::after{content:"";position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--i-line)}',
50
+ '.imm .kpi.ok::after{background:var(--i-ok)}.imm .kpi.warn::after{background:var(--i-warn)}',
51
+ '.imm .kpi.accent::after{background:var(--i-accent)}',
52
+ '.imm .kpi .v{font-size:20px;font-weight:650;letter-spacing:-.2px;font-variant-numeric:tabular-nums}',
53
+ '.imm .kpi .k{color:var(--i-muted);font-size:10.5px;text-transform:uppercase;letter-spacing:.05em;margin-top:1px}',
54
+ '.imm .search{position:relative;flex:1 1 260px;min-width:180px;display:flex;align-items:center}',
55
+ '.imm .search input{width:100%;padding:8px 32px 8px 30px;font-size:13px;color:inherit;',
56
+ 'background:var(--i-panel);border:1px solid var(--i-line);border-radius:8px;transition:border-color .15s,box-shadow .15s}',
57
+ '.imm .search input:focus{outline:none;border-color:var(--i-accent);box-shadow:0 0 0 3px rgba(79,156,249,.15)}',
58
+ '.imm .search .glass{position:absolute;left:10px;width:12px;height:12px;border:2px solid var(--i-faint);border-radius:50%;pointer-events:none}',
59
+ '.imm .search .glass::after{content:"";position:absolute;right:-4px;bottom:-3px;width:6px;height:2px;background:var(--i-faint);transform:rotate(45deg);border-radius:2px}',
60
+ '.imm .search .x{position:absolute;right:5px;width:22px;height:22px;border:0;border-radius:6px;background:transparent;',
61
+ 'color:var(--i-faint);cursor:pointer;font-size:15px;line-height:1;display:none}',
62
+ '.imm .search .x.on{display:block}.imm .search .x:hover{background:var(--i-panel2);color:var(--i-text)}',
63
+ '.imm .seg{display:inline-flex;background:var(--i-panel);border:1px solid var(--i-line);border-radius:999px;padding:2px}',
64
+ '.imm .seg button{border:0;background:transparent;color:var(--i-muted);cursor:pointer;padding:6px 12px;',
65
+ 'border-radius:999px;font-size:12.5px;font-variant-numeric:tabular-nums;transition:background .15s,color .15s}',
66
+ '.imm .seg button.on{background:var(--i-panel2);color:var(--i-text);box-shadow:inset 0 0 0 1px var(--i-line)}',
67
+ '.imm .seg button b{font-weight:600;margin-left:5px;color:var(--i-faint)}.imm .seg button.on b{color:var(--i-accent)}',
68
+ '.imm select{appearance:none;padding:7px 26px 7px 10px;border-radius:8px;border:1px solid var(--i-line);',
69
+ 'background:var(--i-panel);color:inherit;font-size:12.5px;cursor:pointer}',
70
+ '.imm .panel{background:var(--i-panel);border:1px solid var(--i-line);border-radius:11px;overflow:hidden}',
71
+ '.imm .meta-row{display:flex;justify-content:space-between;gap:10px;padding:8px 13px;border-bottom:1px solid var(--i-line-soft);',
72
+ 'color:var(--i-faint);font-size:11.5px}',
73
+ '.imm table{width:100%;border-collapse:collapse}',
74
+ '.imm thead th{position:sticky;top:0;z-index:1;text-align:left;padding:8px 13px;background:var(--i-panel2);',
75
+ 'color:var(--i-muted);font-weight:550;font-size:10.5px;text-transform:uppercase;letter-spacing:.05em;',
76
+ 'border-bottom:1px solid var(--i-line);white-space:nowrap}',
77
+ '.imm tbody td{padding:10px 13px;border-bottom:1px solid var(--i-line-soft);vertical-align:top}',
78
+ '.imm tbody tr:last-child td{border-bottom:0}.imm tbody tr{transition:background .12s}',
79
+ '.imm tbody tr:hover{background:var(--i-panel2)}.imm tr.retired td{opacity:.62}',
80
+ '.imm td.id{color:var(--i-faint);width:44px;font-variant-numeric:tabular-nums}',
81
+ '.imm td.trigger{font-family:ui-monospace,Consolas,monospace;font-size:12px;max-width:280px;word-break:break-word}',
82
+ '.imm td.txt{max-width:270px;word-break:break-word;color:var(--i-muted)}',
83
+ '.imm td.txt.do{color:var(--i-text)}',
84
+ '.imm .counts{font-variant-numeric:tabular-nums;white-space:nowrap;color:var(--i-faint)}',
85
+ '.imm .counts b{color:var(--i-ok);font-weight:600}.imm .counts .f{color:var(--i-bad);font-weight:600}',
86
+ '.imm .pill{display:inline-flex;align-items:center;gap:6px;padding:2px 9px;border-radius:999px;font-size:11.5px;white-space:nowrap}',
87
+ '.imm .pill::before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor}',
88
+ '.imm .pill.active{color:var(--i-ok);background:rgba(55,201,138,.13)}',
89
+ '.imm .pill.retired{color:var(--i-muted);background:rgba(139,148,163,.13)}',
90
+ '.imm .acts{display:flex;gap:6px;flex-wrap:nowrap}',
91
+ '.imm td.ops{white-space:nowrap}',
92
+ '.imm button.act{border:1px solid var(--i-line);background:transparent;color:var(--i-muted);border-radius:7px;',
93
+ 'padding:4px 9px;cursor:pointer;font-size:12px;transition:all .12s}',
94
+ '.imm button.act:hover{color:var(--i-text);border-color:var(--i-muted);background:var(--i-panel2)}',
95
+ '.imm button.act.danger:hover{color:var(--i-bad);border-color:var(--i-bad);background:rgba(229,101,101,.1)}',
96
+ '.imm button.act:disabled{opacity:.45;cursor:progress}',
97
+ '.imm .empty{padding:38px 18px 42px;text-align:center}',
98
+ '.imm .empty .glyph{width:42px;height:42px;margin:0 auto 10px;border-radius:11px;border:1.5px dashed var(--i-line);position:relative}',
99
+ '.imm .empty .glyph::before{content:"";position:absolute;left:12px;right:12px;top:19px;height:2px;background:var(--i-line);border-radius:2px}',
100
+ '.imm .empty .glyph::after{content:"";position:absolute;top:12px;bottom:12px;left:19px;width:2px;background:var(--i-line);border-radius:2px}',
101
+ '.imm .empty .t{font-weight:600}.imm .empty .hd{color:var(--i-muted);margin-top:4px;font-size:12px}',
102
+ '.imm .skel .r{height:36px;border-bottom:1px solid var(--i-line-soft);display:flex;align-items:center;gap:12px;padding:0 13px}',
103
+ '.imm .skel .b{height:9px;border-radius:5px;background:linear-gradient(90deg,var(--i-line-soft),var(--i-line),var(--i-line-soft));',
104
+ 'background-size:200% 100%;animation:imm-shimmer 1.2s linear infinite}',
105
+ '.imm .skel .b:nth-child(1){width:30px}.imm .skel .b:nth-child(2){width:26%}',
106
+ '.imm .skel .b:nth-child(3){width:22%}.imm .skel .b:nth-child(4){width:16%}',
107
+ '@keyframes imm-shimmer{from{background-position:200% 0}to{background-position:-200% 0}}',
108
+ '@media (prefers-reduced-motion:reduce){.imm .skel .b{animation:none}}',
109
+ '.imm .note{font-size:11.5px;color:var(--i-faint)}',
110
+ '.imm .err{color:var(--i-bad)}',
111
+ '@media (max-width:720px){.imm thead{display:none}.imm table,.imm tbody,.imm tr,.imm td{display:block;width:100%}',
112
+ '.imm tbody tr{border-bottom:1px solid var(--i-line);padding:8px 2px}',
113
+ '.imm tbody td{border:0;padding:3px 11px;display:flex;gap:10px}',
114
+ '.imm tbody td::before{content:attr(data-label);flex:0 0 76px;color:var(--i-faint);font-size:10.5px;',
115
+ 'text-transform:uppercase;letter-spacing:.05em;padding-top:2px}',
116
+ '.imm td.id{width:auto}.imm td.trigger,.imm td.txt{max-width:none}}',
117
+ // Defensive layout: the settings modal is an unknown host, so nothing here
118
+ // may depend on inherited line-height, block-ness or table display.
119
+ '.imm{line-height:1.45;font-family:inherit}',
120
+ '.imm .kpi .v,.imm .kpi .k{display:block}',
121
+ '.imm table{display:table;table-layout:auto}',
122
+ '.imm thead{display:table-header-group}.imm tbody{display:table-row-group}',
123
+ '.imm tr{display:table-row}.imm th,.imm td{display:table-cell}',
124
+ '.imm select{color:var(--i-text)}',
125
+ '.imm select option{color:var(--i-text);background:var(--i-panel)}',
126
+ '.imm .shield{display:block;width:100%;min-width:0}',
127
+ ].join('');
25
128
 
26
129
  function request(path, init) {
27
130
  return fetch(API + path, Object.assign({ credentials: 'same-origin' }, init)).then(
@@ -34,76 +137,25 @@ window.__ModuleLoader__.load({
34
137
  );
35
138
  }
36
139
 
37
- function formatTime(ms) {
38
- try {
39
- return new Date(ms).toLocaleString();
40
- } catch (e) {
41
- return '';
42
- }
140
+ function formatBudget(ms) {
141
+ if (typeof ms !== 'number' || !isFinite(ms)) return '—';
142
+ return ms >= 1000 ? (ms / 1000) + ' s' : Math.round(ms) + ' ms';
43
143
  }
44
144
 
45
- var S = {
46
- root: { display: 'flex', flexDirection: 'column', gap: '12px', padding: '4px 2px', fontSize: '13px' },
47
- row: { display: 'flex', gap: '8px', alignItems: 'center', flexWrap: 'wrap' },
48
- stats: { display: 'flex', gap: '14px', flexWrap: 'wrap', color: 'var(--dsh-text-secondary, #8b94a3)' },
49
- statB: { color: 'var(--dsh-text-primary, inherit)', marginRight: '4px' },
50
- input: {
51
- flex: '1 1 220px',
52
- minWidth: '160px',
53
- padding: '7px 10px',
54
- borderRadius: '8px',
55
- border: '1px solid var(--dsh-border, #2c3240)',
56
- background: 'var(--dsh-bg-secondary, #141821)',
57
- color: 'inherit',
58
- fontSize: '13px',
59
- },
60
- tab: {
61
- padding: '5px 12px',
62
- borderRadius: '999px',
63
- border: '1px solid var(--dsh-border, #2c3240)',
64
- background: 'transparent',
65
- color: 'inherit',
66
- cursor: 'pointer',
67
- fontSize: '12px',
68
- },
69
- tabOn: {
70
- borderColor: 'var(--dsh-accent, #4f9cf9)',
71
- color: 'var(--dsh-accent, #4f9cf9)',
72
- },
73
- table: { width: '100%', borderCollapse: 'collapse' },
74
- th: {
75
- textAlign: 'left',
76
- padding: '6px 8px',
77
- fontSize: '11px',
78
- textTransform: 'uppercase',
79
- letterSpacing: '.04em',
80
- color: 'var(--dsh-text-secondary, #8b94a3)',
81
- borderBottom: '1px solid var(--dsh-border, #2c3240)',
82
- },
83
- td: {
84
- padding: '8px',
85
- borderBottom: '1px solid var(--dsh-border, #242a36)',
86
- verticalAlign: 'top',
87
- },
88
- mono: { fontFamily: 'ui-monospace, Consolas, monospace', fontSize: '12px' },
89
- btn: {
90
- padding: '3px 8px',
91
- marginRight: '6px',
92
- borderRadius: '6px',
93
- border: '1px solid var(--dsh-border, #2c3240)',
94
- background: 'transparent',
95
- color: 'inherit',
96
- cursor: 'pointer',
97
- fontSize: '12px',
98
- },
99
- muted: { color: 'var(--dsh-text-secondary, #8b94a3)' },
100
- note: { fontSize: '12px', color: 'var(--dsh-text-secondary, #8b94a3)' },
101
- };
145
+ function kpi(value, label, tone) {
146
+ return h(
147
+ 'div',
148
+ { key: label, className: 'kpi' + (tone ? ' ' + tone : '') },
149
+ h('div', { className: 'v' }, String(value)),
150
+ h('div', { className: 'k' }, label),
151
+ );
152
+ }
102
153
 
103
- function ImmuneTab() {
104
- var state = React.useState({ loading: true, error: '', stats: null, rows: [] });
154
+ function ImmunePanel() {
155
+ var state = React.useState({ loading: true, error: '', stats: null, rows: [], budgetMs: null });
105
156
  var view = React.useState('project');
106
157
  var query = React.useState('');
158
+ var order = React.useState('recent');
107
159
 
108
160
  var data = state[0];
109
161
  var setData = state[1];
@@ -111,6 +163,8 @@ window.__ModuleLoader__.load({
111
163
  var setTab = view[1];
112
164
  var q = query[0];
113
165
  var setQ = query[1];
166
+ var sort = order[0];
167
+ var setSort = order[1];
114
168
 
115
169
  var load = React.useCallback(
116
170
  function (nextTab, nextQuery) {
@@ -123,10 +177,22 @@ window.__ModuleLoader__.load({
123
177
  });
124
178
  request(path)
125
179
  .then(function (body) {
126
- setData({ loading: false, error: '', stats: body.stats, rows: body.antibodies });
180
+ setData({
181
+ loading: false,
182
+ error: '',
183
+ stats: body.stats,
184
+ rows: body.antibodies,
185
+ budgetMs: body.budgetMs,
186
+ });
127
187
  })
128
188
  .catch(function (error) {
129
- setData({ loading: false, error: String(error.message || error), stats: null, rows: [] });
189
+ setData({
190
+ loading: false,
191
+ error: String(error.message || error),
192
+ stats: null,
193
+ rows: [],
194
+ budgetMs: null,
195
+ });
130
196
  });
131
197
  },
132
198
  [setData],
@@ -166,120 +232,267 @@ window.__ModuleLoader__.load({
166
232
  }
167
233
 
168
234
  var stats = data.stats;
169
- var header = h(
235
+
236
+ var kpis = h(
170
237
  'div',
171
- { style: S.stats },
238
+ { className: 'kpis' },
172
239
  stats
173
240
  ? [
174
- h('span', { key: 'a' }, h('b', { style: S.statB }, String(stats.active)), '生效 / active'),
175
- h('span', { key: 'r' }, h('b', { style: S.statB }, String(stats.retired)), '已退役 / retired'),
176
- h('span', { key: 'h' }, h('b', { style: S.statB }, String(stats.hits)), '注入 / injections'),
177
- h('span', { key: 's' }, h('b', { style: S.statB }, String(stats.shared)), '共享 / shared'),
241
+ kpi(stats.active, '生效 / active', 'ok'),
242
+ kpi(stats.retired, '已退役 / retired', 'warn'),
243
+ kpi(stats.hits, '注入 / injections', 'accent'),
244
+ kpi(stats.shared, '共享 / shared', ''),
245
+ kpi(formatBudget(data.budgetMs), '蒸馏预算 / budget', ''),
178
246
  ]
179
- : [h('span', { key: 'l' }, data.loading ? '加载中… / loading…' : '')],
247
+ : [h('div', { key: 'l', className: 'note' }, data.loading ? '加载中… / loading…' : '')],
180
248
  );
181
249
 
182
- var rows = data.rows.map(function (a) {
183
- return h(
184
- 'tr',
185
- { key: a.id },
186
- h('td', { style: S.td }, String(a.id)),
187
- h('td', { style: Object.assign({}, S.td, S.mono) }, a.trigger_pattern),
188
- h('td', { style: S.td }, a.symptom),
189
- h('td', { style: S.td }, a.prescription),
190
- h('td', { style: Object.assign({}, S.td, S.muted) }, a.hit_count + ' / ' + a.fail_count),
191
- h('td', { style: Object.assign({}, S.td, S.muted) }, a.status),
250
+ var sorted = data.rows.slice().sort(function (a, b) {
251
+ if (sort === 'hits') return b.hit_count - a.hit_count || a.id - b.id;
252
+ if (sort === 'fails') return b.fail_count - a.fail_count || a.id - b.id;
253
+ return String(b.updated_at).localeCompare(String(a.updated_at)) || b.id - a.id;
254
+ });
255
+
256
+ var body;
257
+ if (data.error) {
258
+ body = h(
259
+ 'div',
260
+ { className: 'empty' },
261
+ h('div', { className: 'glyph' }),
262
+ h('div', { className: 't' }, '加载失败 / load failed'),
263
+ h('div', { className: 'hd err' }, data.error),
264
+ );
265
+ } else if (data.loading && data.rows.length === 0) {
266
+ body = h(
267
+ 'div',
268
+ { className: 'skel' },
269
+ [0, 1, 2, 3].map(function (n) {
270
+ return h(
271
+ 'div',
272
+ { className: 'r', key: n },
273
+ h('div', { className: 'b' }),
274
+ h('div', { className: 'b' }),
275
+ h('div', { className: 'b' }),
276
+ h('div', { className: 'b' }),
277
+ );
278
+ }),
279
+ );
280
+ } else if (sorted.length === 0) {
281
+ body = h(
282
+ 'div',
283
+ { className: 'empty' },
284
+ h('div', { className: 'glyph' }),
285
+ h(
286
+ 'div',
287
+ { className: 't' },
288
+ tab === 'shared' ? '共享库为空 / shared library is empty' : '还没有抗体 / no antibodies yet',
289
+ ),
192
290
  h(
193
- 'td',
194
- { style: S.td },
195
- tab === 'project'
196
- ? [
197
- h(
198
- 'button',
199
- {
200
- key: 't',
201
- style: S.btn,
202
- onClick: function () {
203
- act(a.status === 'active' ? 'retire' : 'restore', a.id);
204
- },
205
- },
206
- a.status === 'active' ? '退役 / retire' : '恢复 / restore',
207
- ),
208
- h(
209
- 'button',
210
- { key: 'd', style: S.btn, onClick: function () { act('delete', a.id); } },
211
- '删除 / delete',
212
- ),
213
- ]
214
- : null,
291
+ 'div',
292
+ { className: 'hd' },
293
+ tab === 'shared'
294
+ ? '泛型坑在多个项目反复命中后才会晋升到这里。'
295
+ : '工具先失败、随后用另一种方式成功之后才会产生。',
215
296
  ),
216
297
  );
217
- });
298
+ } else {
299
+ body = h(
300
+ 'table',
301
+ null,
302
+ h(
303
+ 'thead',
304
+ null,
305
+ h(
306
+ 'tr',
307
+ null,
308
+ ['#', '触发 / trigger', '忌 / avoid', '宜 / do', 'hits / fails', '状态 / status', ''].map(function (label) {
309
+ return h('th', { key: label }, label);
310
+ }),
311
+ ),
312
+ ),
313
+ h(
314
+ 'tbody',
315
+ null,
316
+ sorted.map(function (a) {
317
+ return h(
318
+ 'tr',
319
+ { key: a.id, className: a.status === 'retired' ? 'retired' : '' },
320
+ h('td', { className: 'id', 'data-label': '#' }, String(a.id)),
321
+ h('td', { className: 'trigger', 'data-label': '触发' }, a.trigger_pattern),
322
+ h('td', { className: 'txt', 'data-label': '忌' }, a.symptom),
323
+ h('td', { className: 'txt do', 'data-label': '宜' }, a.prescription),
324
+ h(
325
+ 'td',
326
+ { className: 'counts', 'data-label': 'hits/fails' },
327
+ h('b', null, String(a.hit_count)),
328
+ ' / ',
329
+ h('span', { className: 'f' }, String(a.fail_count)),
330
+ ),
331
+ h(
332
+ 'td',
333
+ { 'data-label': '状态' },
334
+ h('span', { className: 'pill ' + a.status }, a.status === 'active' ? '生效 / active' : '已退役 / retired'),
335
+ ),
336
+ h(
337
+ 'td',
338
+ { className: 'ops', 'data-label': '' },
339
+ tab === 'project'
340
+ ? h(
341
+ 'div',
342
+ { className: 'acts' },
343
+ h(
344
+ 'button',
345
+ {
346
+ className: 'act',
347
+ type: 'button',
348
+ onClick: function () {
349
+ act(a.status === 'active' ? 'retire' : 'restore', a.id);
350
+ },
351
+ },
352
+ a.status === 'active' ? '退役 / retire' : '恢复 / restore',
353
+ ),
354
+ h(
355
+ 'button',
356
+ { className: 'act danger', type: 'button', onClick: function () { act('delete', a.id); } },
357
+ '删除 / delete',
358
+ ),
359
+ )
360
+ : null,
361
+ ),
362
+ );
363
+ }),
364
+ ),
365
+ );
366
+ }
218
367
 
219
368
  return h(
220
369
  'div',
221
- { style: S.root },
222
- header,
370
+ { className: 'imm' },
371
+ h('style', { id: STYLE_ID }, CSS),
372
+ kpis,
223
373
  h(
224
374
  'div',
225
- { style: S.row },
226
- h('input', {
227
- style: S.input,
228
- type: 'search',
229
- placeholder: '搜索触发词、症状或处方… / search…',
230
- value: q,
231
- onChange: function (e) {
232
- onQuery(e.target.value);
233
- },
234
- }),
375
+ { className: 'row' },
235
376
  h(
236
- 'button',
237
- {
238
- style: Object.assign({}, S.tab, tab === 'project' ? S.tabOn : null),
239
- onClick: function () {
240
- setTab('project');
377
+ 'div',
378
+ { className: 'search' },
379
+ h('span', { className: 'glass', 'aria-hidden': 'true' }),
380
+ h('input', {
381
+ type: 'search',
382
+ placeholder: '搜索触发词、症状或处方… / search…',
383
+ value: q,
384
+ 'aria-label': 'search antibodies',
385
+ onChange: function (e) {
386
+ onQuery(e.target.value);
241
387
  },
242
- },
243
- '项目 / project',
388
+ }),
389
+ h(
390
+ 'button',
391
+ {
392
+ className: q ? 'x on' : 'x',
393
+ type: 'button',
394
+ title: '清除 / clear',
395
+ onClick: function () {
396
+ onQuery('');
397
+ },
398
+ },
399
+ '×',
400
+ ),
244
401
  ),
245
402
  h(
246
- 'button',
247
- {
248
- style: Object.assign({}, S.tab, tab === 'shared' ? S.tabOn : null),
249
- onClick: function () {
250
- setTab('shared');
403
+ 'div',
404
+ { className: 'seg', role: 'tablist' },
405
+ h(
406
+ 'button',
407
+ {
408
+ className: tab === 'project' ? 'on' : '',
409
+ type: 'button',
410
+ role: 'tab',
411
+ 'aria-selected': tab === 'project' ? 'true' : 'false',
412
+ onClick: function () { setTab('project'); },
413
+ },
414
+ '项目 / project',
415
+ h('b', null, stats ? String(stats.active + stats.retired) : ''),
416
+ ),
417
+ h(
418
+ 'button',
419
+ {
420
+ className: tab === 'shared' ? 'on' : '',
421
+ type: 'button',
422
+ role: 'tab',
423
+ 'aria-selected': tab === 'shared' ? 'true' : 'false',
424
+ onClick: function () { setTab('shared'); },
251
425
  },
426
+ '共享 / shared',
427
+ h('b', null, stats ? String(stats.shared) : ''),
428
+ ),
429
+ ),
430
+ h(
431
+ 'select',
432
+ {
433
+ value: sort,
434
+ 'aria-label': 'sort antibodies',
435
+ onChange: function (e) { setSort(e.target.value); },
252
436
  },
253
- '共享 / shared',
437
+ h('option', { value: 'recent' }, '最近更新 / recent'),
438
+ h('option', { value: 'hits' }, '命中最多 / most hits'),
439
+ h('option', { value: 'fails' }, '失效最多 / most fails'),
254
440
  ),
255
441
  ),
256
- data.error
257
- ? h('div', { style: S.note }, '加载失败 / load failed: ' + data.error)
258
- : data.rows.length === 0
259
- ? h(
260
- 'div',
261
- { style: S.note },
262
- tab === 'shared'
263
- ? '共享库为空。泛型坑在多次命中后才会晋升。/ Shared library is empty.'
264
- : '还没有抗体。工具先失败、随后成功之后才会产生。/ No antibodies yet.',
265
- )
266
- : h(
267
- 'table',
268
- { style: S.table },
269
- h(
270
- 'thead',
271
- null,
272
- h(
273
- 'tr',
274
- null,
275
- ['#', '触发 / trigger', '忌 / avoid', '宜 / do', 'hits/fails', 'status', ''].map(function (label) {
276
- return h('th', { key: label, style: S.th }, label);
277
- }),
278
- ),
279
- ),
280
- h('tbody', null, rows),
281
- ),
282
- h('div', { style: S.note }, '浏览不计入命中数 / browsing does not count as injection.'),
442
+ h(
443
+ 'div',
444
+ { className: 'panel' },
445
+ h(
446
+ 'div',
447
+ { className: 'meta-row' },
448
+ h('span', null, sorted.length + ' 条结果 / results'),
449
+ h('span', null, stats ? '蒸馏预算 / budget ' + formatBudget(data.budgetMs) : ''),
450
+ ),
451
+ body,
452
+ ),
453
+ h('div', { className: 'note' }, '浏览不计入命中数——只有真正注入给模型才算一次命中。 / browsing is not injection.'),
454
+ );
455
+ }
456
+
457
+ /**
458
+ * Render the panel inside a shadow root.
459
+ *
460
+ * The settings modal is an unknown host: its global stylesheet can (and in
461
+ * practice does) reshape bare `table`/`td`/`div` inside a plugin tab, which
462
+ * collapsed our rows and hid the sort control. A shadow root seals both
463
+ * directions — the host CSS cannot reach in, and our tokens cannot leak out.
464
+ * Only inherited properties (font, colour, line-height) cross the boundary,
465
+ * so `.imm` still pins those explicitly.
466
+ *
467
+ * If the browser has no shadow DOM, or react-dom is unavailable for portals,
468
+ * fall back to plain rendering; the defensive CSS in the style block keeps
469
+ * that readable.
470
+ */
471
+ function ImmuneTab() {
472
+ var host = React.useRef(null);
473
+ var target = React.useState(null);
474
+ var mount = target[0];
475
+ var setMount = target[1];
476
+
477
+ React.useEffect(function () {
478
+ var node = host.current;
479
+ if (!node || !ReactDOM || typeof node.attachShadow !== 'function') return undefined;
480
+ var shadow = node.shadowRoot || node.attachShadow({ mode: 'open' });
481
+ var slot = shadow.querySelector('div[data-immune-root]');
482
+ if (!slot) {
483
+ slot = document.createElement('div');
484
+ slot.setAttribute('data-immune-root', '1');
485
+ shadow.appendChild(slot);
486
+ }
487
+ setMount(slot);
488
+ return undefined;
489
+ }, []);
490
+
491
+ var ready = mount !== null && ReactDOM !== null && typeof ReactDOM.createPortal === 'function';
492
+ return h(
493
+ 'div',
494
+ { ref: host, className: 'shield', style: { display: 'block', width: '100%', minWidth: 0 } },
495
+ ready ? ReactDOM.createPortal(h(ImmunePanel, null), mount) : null,
283
496
  );
284
497
  }
285
498
 
@@ -302,6 +515,9 @@ window.__ModuleLoader__.load({
302
515
  exports.name = 'dsh-immune-system';
303
516
  exports.inject = ['slots'];
304
517
  exports.apply = apply;
518
+ // Exposed for the offline render harness (scripts/render-tab.mjs); the host
519
+ // only uses `apply`. Harmless if a future host ignores it.
520
+ exports.__tab = ImmuneTab;
305
521
  return module.exports;
306
522
  },
307
523
  });