dsh-loci 2.11.0 → 3.0.1

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/CHANGELOG.md CHANGED
@@ -4,6 +4,120 @@ All notable changes to this project are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); the project follows
5
5
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html) from 1.0.0.
6
6
 
7
+ ## [3.0.1] — 2026-11-15
8
+
9
+ The 3.0.0 card was correct markup with no stylesheet, and the screenshot showed it.
10
+
11
+ ### Fixed
12
+
13
+ - **The card was never styled outside the panel.** Every `.loci-*` rule lives in one CSS
14
+ template string that only `MemoryPanel` and `SettingsSection` render as a `<style>`
15
+ element. In the sidebar, with the panel closed, no `<style>` existed — so the card's DOM
16
+ was right and its layout was simply absent. That is what "bare text in the sidebar"
17
+ looked like, and it predates 3.0.0: the first screenshot showed it too. The card now
18
+ renders the stylesheet itself, and so does its rail variant.
19
+
20
+ - **`card.pending` was defined twice.** The old value was the sentence
21
+ `'{n} still need your review'`; 3.0.0 added a bare `'Pending'` label for the metric row,
22
+ and the later definition won — so the card printed the sentence with an unsubstituted
23
+ `{n}`. The sentence is now `card.pendingSentence` and the label stays `Pending`.
24
+
25
+ - **The version chip rendered as `v3. 0. 0`.** The chip used
26
+ `ui-monospace, SFMono-Regular, Menlo, monospace`, which on Windows falls back to a
27
+ monospace face whose period is centred in a wide advance; at 8.5px that reads as gaps
28
+ between the digits. Measured with `getComputedStyle`: `letter-spacing: normal`,
29
+ `word-spacing: 0`, width 25.50 — the spacing was correct, the **glyph** was not. The chip
30
+ now inherits the UI font with tabular figures, at 9px.
31
+
32
+ ### Added
33
+
34
+ - **`scripts/shot-card.mjs`** renders the sidebar card to a PNG using the stylesheet
35
+ extracted from `client.js` and the DOM the component produces, through headless Edge. It
36
+ cannot catch a logic mistake, only a drawing one — but a drawing mistake is exactly what
37
+ shipped twice, and a screenshot is the only review that shows it.
38
+
39
+ ### Not added
40
+
41
+ Four guard files from the previous draft (a separate client-i18n test, an English-only test,
42
+ a CSS-shape assertion and a manifest check) were collapsed into **one** test file and one
43
+ manifest assertion. Guards that outnumber the behaviour they protect get deleted by the next
44
+ person, which is worse than not having them.
45
+
46
+ ### Corrected in the record
47
+
48
+ The 3.0.0 log entry said the card had been verified. It had not been looked at. The entry
49
+ above records what the screenshot showed.
50
+
51
+ ## [3.0.0] — 2026-11-15
52
+
53
+ The sidebar card was not a card, and the plugin had a second language it did not need.
54
+
55
+ ### Changed — the card
56
+
57
+ The screenshot that prompted this showed three plugin cards in a row: two with white
58
+ rounded shells, a status dot, a version chip and numeric rows; one — this one — as bare
59
+ text on the sidebar background, two sentences of prose and nothing clickable.
60
+
61
+ The difference was measurable, not aesthetic. `dsh-ustc-quota`'s shell carries
62
+ `box-shadow: 0 3px 12px`, `flex: 0 0 100%`, `order: 1`, `grid-column: 1 / -1`,
63
+ `border-radius: 12px` and `font-variant-numeric: tabular-nums`. This card had none of them,
64
+ so it sat in the same visual plane as the sidebar and looked like a note pasted over it.
65
+
66
+ Now:
67
+
68
+ - **Shell** — the same box model the neighbouring plugins use, so three cards in one column
69
+ read as three cards.
70
+ - **Header** — title, a dim monospace version chip, a 5px status dot (brand / warn / error),
71
+ and a right-aligned `updated <time>` that truncates instead of wrapping.
72
+ - **Numbers** — `Verified / Pending / Disputed` in a three-column grid, tabular figures,
73
+ amber for pending and red for disputed.
74
+ - **Budget** — a thin bar of index bytes against `sectionMaxBytes`, turning amber past 85%.
75
+ - **Usage** — `Injected / Opened / Dead` (the last one only when non-zero), which is the row
76
+ that makes schema v2's counters visible where a person will actually see them.
77
+ - **Empty state** — one line and one instruction instead of two sentences of apology.
78
+
79
+ This reverses the earlier "a page of notes, not a dashboard" stance *for this surface*. The
80
+ audit had already called that stance out: an aesthetic refusal to interrupt, applied to the
81
+ one place where a person must act. The panel keeps the prose; the card gets the numbers.
82
+
83
+ ### Changed — one language
84
+
85
+ Chinese was not in the client because anyone chose it; the client shipped a full `ZH`
86
+ dictionary, so a browser whose locale resolved to `zh` rendered the UI in Chinese. The
87
+ English dictionary was already complete. The second dictionary was the only reason the
88
+ product had a language at all.
89
+
90
+ - `client.js`: the 128-line `ZH` dictionary is gone, the locale registration is `{ en: EN }`,
91
+ and four Chinese comments are rewritten.
92
+ - `locale/zh.json`: deleted; `locale/en.json` is the only locale file.
93
+ - `package.json` / `manifests`: no longer advertise a second language. `README.md` loses the
94
+ `记忆宫殿` gloss and the `更新于` label description; the Chinese *examples* of stored
95
+ content stay, because they demonstrate CJK retrieval.
96
+ - `scripts/capture-panel.mjs`: its panel selector matched `记忆|Memory` and can only ever
97
+ match the latter now.
98
+
99
+ Kept, deliberately: the Chinese regexes in `capture.js` (they match Chinese input) and in
100
+ `sanitize.js` (they match Chinese prompt-injection phrasings). Deleting those would disable
101
+ the features for the language most of the user's memories are written in. They are pattern
102
+ data, not output.
103
+
104
+ ### Added
105
+
106
+ - `test/unit/client-i18n.test.mjs` — no Chinese in `client.js`, no `ZH` dictionary, no
107
+ `locale/zh.json`, every `t()` key defined, and the card still has its shell/metric pieces.
108
+ - `test/unit/english-only.test.mjs` — no Chinese in emitted strings under `src/` outside a
109
+ two-file allowlist, and the allowlisted files must still contain their patterns (a guard
110
+ that silently allows a file would hide the deletion it exists to catch).
111
+ - `scripts/check-manifest.mjs` now **fails if `locale/zh.json` reappears**, so the language
112
+ cannot come back in a version that never touched `client.js`.
113
+
114
+ ### Still Chinese, on purpose
115
+
116
+ `README.zh.md`, `docs/*.md`, and the `titleZh`/`descriptionZh` fields in
117
+ `marketplace/entry.json` are documentation and storefront metadata, not output the plugin
118
+ emits at runtime. They were left alone rather than deleted; if they should go too, that is a
119
+ separate decision about the deliverable, not about the product's language.
120
+
7
121
  ## [2.11.0] — 2026-11-15
8
122
 
9
123
  One fix, made because it is the last thing that could put another project's facts into a
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # dsh-loci · Loci 记忆宫殿
1
+ # dsh-loci · Loci
2
2
 
3
3
  **A long-term memory for DeepSeek Harness where the prompt only ever sees the palace's index — every room is one lookup away.**
4
4
 
@@ -86,7 +86,7 @@ one of them.
86
86
  ## Where you see it
87
87
 
88
88
  - **Sidebar note** — a small card beside Settings, like the quota and advisor
89
- cards, that reads like a page of notes rather than a dashboard: `更新于 <time>`
89
+ cards: a shadowed card in the sidebar footer with a status dot, a `updated <time>` stamp and numeric rows (verified / pending / disputed, index bytes against the budget, injected / opened / dead weight)
90
90
  over one short paragraph of what the agent currently understands in this
91
91
  project (a consensus, a lesson, a fact), plus one quiet line if something is
92
92
  waiting for your review. It answers "did it take that in?" — no counters.
package/client.js CHANGED
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * - a **sidebar mirror** (`sidebar.footer.action`): a flat, dense card that
7
7
  * shows what memory changed today and last week, with the last few changes
8
- * as one-liners. It is the "副屏" — glanceable, never modal;
8
+ * as one-liners. It is the sidebar mirror — glanceable, never modal;
9
9
  * - a **panel** (`sidebar.panellist` + `main`): the full view, with a feed,
10
10
  * the exact injected index, search, the review queue and statistics.
11
11
  *
@@ -34,10 +34,18 @@ window.__ModuleLoader__.load({
34
34
  'Layered long-term memory: the prompt holds a stable index, full text is one lookup away, and only verified facts are injected.',
35
35
 
36
36
  'card.title': 'Memory',
37
- 'card.empty': 'Nothing understood yet',
38
- 'card.emptyHint': 'Confirm a fact and it appears here',
37
+ 'card.empty': 'Nothing saved yet',
38
+ 'card.metrics': 'Memory at a glance',
39
+ 'card.emptyHint': 'Ask the agent to remember something, then confirm it here.',
40
+ 'card.verified': 'Verified',
41
+ 'card.pending': 'Pending',
42
+ 'card.disputed': 'Disputed',
43
+ 'card.injected': 'Injected',
44
+ 'card.opened': 'Opened',
45
+ 'card.dead': 'Dead',
46
+ 'card.pendingHint': '{n} item(s) waiting for you in Review.',
39
47
  'card.updated': 'updated',
40
- 'card.pending': '{n} still need your review',
48
+ 'card.pendingSentence': '{n} still need your review',
41
49
  'card.pinned': 'pinned',
42
50
  'card.open': 'Open memory',
43
51
  'card.check': 'Check for updates',
@@ -157,151 +165,35 @@ window.__ModuleLoader__.load({
157
165
  'panel.version': 'version',
158
166
  };
159
167
 
160
- const ZH = {
161
- 'meta.title': 'Loci 记忆宫殿',
162
- 'meta.description':
163
- '分层长期记忆:提示词里只放稳定索引,全文按需一次取回;只有已核实的记忆才会被注入。',
164
-
165
- 'card.title': '记忆',
166
- 'card.empty': '还没有真正记下的东西',
167
- 'card.emptyHint': '你确认过的事实会出现在这里',
168
- 'card.updated': '更新于',
169
- 'card.pending': '另有 {n} 条我拿不准,等你看一眼',
170
- 'card.pinned': '置顶',
171
- 'card.open': '打开记忆',
172
- 'card.check': '检查更新',
173
- 'card.upToDate': '已是最新',
174
- 'card.updateTo': '有新版本',
175
- 'card.checkOff': '更新检查已关闭',
176
- 'card.checking': '检查中…',
177
- 'card.unknown': '无法检查版本',
178
- 'card.unpublished': '尚未发布',
179
-
180
- 'prose.agreed': '已定:',
181
- 'prose.lesson': '经验:',
182
-
183
- 'kind.rule': '共识',
184
- 'kind.procedural': '经验',
185
- 'kind.semantic': '事实',
186
- 'kind.episodic': '事件',
187
- 'kind.working': '草稿',
188
-
189
- 'since.now': '刚才',
190
- 'since.minutes': '{n} 分钟前',
191
- 'since.hours': '{n} 小时前',
192
- 'since.yesterday': '昨天',
193
- 'since.days': '{n} 天前',
194
-
195
- 'action.added': '新增',
196
- 'action.revised': '更正',
197
- 'action.verified': '核实',
198
- 'action.disputed': '存疑',
199
- 'action.pinned': '置顶',
200
- 'action.unpinned': '取消置顶',
201
- 'action.deleted': '删除',
202
- 'action.purged': '彻底删除',
203
- 'action.restored': '恢复',
204
- 'action.imported': '导入',
205
- 'action.maintained': '维护',
206
-
207
- 'settings.title': 'Loci',
208
- 'settings.intro': '给 agent 的长期记忆。标注「即时」的改动立即生效。',
209
- 'settings.unavailable': '当前缺少设置服务,以下显示实际生效的配置。',
210
- 'settings.saved': '已保存',
211
- 'settings.rejected': '宿主拒绝写入',
212
- 'settings.failed': '写入失败',
213
- 'settings.budget': '索引预算',
214
- 'settings.budgetHint': '整段注入的字节上限',
215
- 'settings.scope': '新事实的默认作用域',
216
- 'settings.capture': '会话捕获',
217
- 'settings.assist': '首轮辅助召回',
218
- 'settings.maintenance': '后台维护',
219
- 'settings.secret': '记忆中的密钥',
220
- 'settings.vector': '向量通道',
221
- 'settings.diagnostics': '诊断',
222
- 'settings.danger': '清空记忆',
223
- 'settings.dangerHint': '输入确认短语才会执行。条目为软删除:历史仍可读、可导出。',
224
- 'settings.dangerPhrase': 'forget everything',
225
- 'settings.dangerRun': '删除全部记忆',
226
- 'settings.dangerBusy': '删除中…',
227
- 'settings.dangerDone': '已删除 {n} 条。',
228
- 'off': '关闭',
229
- 'captureRules': '仅规则',
230
- 'secretRedact': '脱敏',
231
- 'secretReject': '拒绝写入',
232
- 'vectorOff': '关闭(仅词法)',
233
- 'vectorHttp': 'HTTP 端点',
234
-
235
- 'panel.title': '记忆',
236
- 'panel.feed': '动态',
237
- 'panel.index': '索引',
238
- 'panel.search': '检索',
239
- 'panel.review': '待审',
240
- 'panel.stats': '统计',
241
- 'panel.refresh': '刷新',
242
- 'panel.loading': '加载中…',
243
- 'panel.empty': '这里还没有内容。',
244
- 'panel.feedHint': '记住了什么、更正了什么、核实或删除了什么。最新的在前。',
245
- 'panel.indexHint': '与模型系统提示里完全一致的那一段,以及它的字节开销。',
246
- 'panel.bytes': '字节',
247
- 'panel.listed': '已列出',
248
- 'panel.hidden': '未列出',
249
- 'panel.searchPlaceholder': '检索标题与正文…',
250
- 'panel.searchEmpty': '没有匹配。',
251
- 'panel.more': '加载更多',
252
- 'panel.total': '总数',
253
- 'panel.verify': '核实',
254
- 'panel.dispute': '标记争议',
255
- 'panel.delete': '删除',
256
- 'panel.restore': '恢复',
257
- 'panel.pin': '置顶',
258
- 'panel.unpin': '取消置顶',
259
- 'panel.back': '返回',
260
- 'panel.history': '历史',
261
- 'panel.evidence': '证据',
262
- 'panel.source': '出处',
263
- 'panel.lesson': '教训',
264
- 'panel.lesson.pattern': '问题形态',
265
- 'panel.lesson.fix': '做法',
266
- 'panel.lesson.prevents': '不做会怎样',
267
- 'panel.lesson.when': '适用场景',
268
- 'panel.relations': '关联',
269
- 'panel.candidates': '候选',
270
- 'panel.disputed': '争议',
271
- 'panel.noCandidates': '没有待审条目。',
272
- 'panel.reviewHint': '这里只列可能被注入的条目;过去会话的摘要属于历史,不是主张。',
273
- 'panel.reject': '驳回',
274
- 'panel.digestOf': '同一批次提出 {n} 条',
275
- 'panel.verifyGroup': '核实这 {n} 条',
276
- 'panel.verifyAll': '全部核实({n})',
277
- 'panel.actionFailed': '操作失败',
278
- 'panel.injections': '最近注入',
279
- 'panel.deadWeight': '注入了但从未被打开',
280
- 'panel.injected': '次注入',
281
- 'panel.opened': '次打开',
282
- 'panel.store': '存储',
283
- 'panel.confirmDelete': '软删除这条记忆?',
284
- 'panel.error': '请求失败',
285
- 'panel.updated': '更新于',
286
- 'panel.version': '版本',
287
- };
288
-
289
168
  const CSS = `
290
169
  .loci-root { display: flex; flex-direction: column; height: 100%; min-height: 0; color: var(--dsw-alias-label-primary); font-size: 13px; }
291
- .loci-card-root { display: flex; flex-direction: column; gap: 6px; width: 100%; box-sizing: border-box; padding: 9px 10px; border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-1); border-radius: 8px; cursor: pointer; text-align: left; font: inherit; color: inherit; }
170
+ .loci-card-root { display: flex; flex-direction: column; gap: 5px; flex: 0 0 100%; order: 1; grid-column: 1 / -1; width: 100%; min-width: 0; box-sizing: border-box; margin: 0 0 6px; padding: 10px 9px 8px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 12px; background: var(--dsw-alias-bg-layer-1); box-shadow: 0 3px 12px rgba(0, 0, 0, .16); cursor: pointer; text-align: left; font: inherit; color: var(--dsw-alias-label-primary); font-size: 11px; line-height: 1.3; font-variant-numeric: tabular-nums; }
292
171
  .loci-card-root:hover { background: var(--dsw-alias-bg-layer-2); }
293
- .loci-card-root.is-rail { padding: 6px; align-items: center; justify-content: center; }
294
- .loci-card-head { display: flex; align-items: center; gap: 6px; width: 100%; }
295
- .loci-card-title { font-size: 12px; font-weight: 600; letter-spacing: .01em; }
296
- .loci-card-grow { flex: 1 1 auto; }
297
- .loci-card-ver { font-size: 10px; color: var(--dsw-alias-label-secondary); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
172
+ .loci-card-root.is-rail { flex: 0 0 auto; padding: 6px; align-items: center; justify-content: center; }
173
+ .loci-card-head { display: flex; align-items: center; gap: 6px; min-width: 0; width: 100%; }
174
+ .loci-card-title { font-size: 13px; font-weight: 700; letter-spacing: -.01em; white-space: nowrap; }
175
+ .loci-card-ver { flex: 0 0 auto; margin: 0; padding: 0; border: 0; background: transparent; color: var(--dsw-alias-label-secondary); font-size: 9px; line-height: 1.2; font-family: inherit; font-variant-numeric: tabular-nums; cursor: pointer; white-space: nowrap; }
176
+ .loci-card-ver:hover { color: var(--dsw-alias-label-primary); }
177
+ .loci-card-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--dsw-alias-brand-primary); flex: 0 0 auto; }
178
+ .loci-card-dot.is-warn { background: var(--dsw-alias-state-warn-primary); }
179
+ .loci-card-dot.is-error { background: var(--dsw-alias-state-error-primary); }
180
+ .loci-card-meta { flex: 1 1 auto; min-width: 0; font-size: 8.5px; color: var(--dsw-alias-label-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: right; }
298
181
  .loci-card-mark { display: flex; align-items: center; color: var(--dsw-alias-brand-primary); flex: 0 0 auto; }
299
- .loci-card-note { display: flex; flex-direction: column; gap: 5px; }
300
- .loci-card-stamp { font-size: 10.5px; color: var(--dsw-alias-label-secondary); letter-spacing: .01em; }
301
- .loci-card-prose { margin: 0; font-size: 12px; line-height: 1.62; color: var(--dsw-alias-label-primary); display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden; }
302
- .loci-card-hint { font-size: 10.5px; color: var(--dsw-alias-label-secondary); margin-top: 1px; }
303
- .loci-card-empty { font-size: 11px; color: var(--dsw-alias-label-secondary); }
304
- .loci-card-line { display: flex; gap: 6px; align-items: baseline; min-width: 0; }
182
+ .loci-card-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 3px 8px; width: 100%; }
183
+ .loci-card-metric { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
184
+ .loci-card-metric-label { font-size: 8px; color: var(--dsw-alias-label-secondary); text-transform: uppercase; letter-spacing: .05em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
185
+ .loci-card-metric-value { font-size: 12px; font-weight: 600; color: var(--dsw-alias-label-primary); font-variant-numeric: tabular-nums; }
186
+ .loci-card-metric-value.is-warn { color: var(--dsw-alias-state-warn-primary); }
187
+ .loci-card-metric-value.is-error { color: var(--dsw-alias-state-error-primary); }
188
+ .loci-card-barline { display: flex; align-items: center; gap: 6px; width: 100%; }
189
+ .loci-card-bar { flex: 1 1 auto; min-width: 20px; height: 4px; border-radius: 999px; background: var(--dsw-alias-bg-layer-2); overflow: hidden; }
190
+ .loci-card-bar-fill { height: 100%; border-radius: 999px; background: var(--dsw-alias-brand-primary); }
191
+ .loci-card-bar-fill.is-warn { background: var(--dsw-alias-state-warn-primary); }
192
+ .loci-card-bar-label { font-size: 8.5px; color: var(--dsw-alias-label-secondary); white-space: nowrap; }
193
+ .loci-card-foot { display: flex; align-items: center; gap: 6px; width: 100%; font-size: 8.5px; color: var(--dsw-alias-label-secondary); }
194
+ .loci-card-foot-grow { flex: 1 1 auto; }
195
+ .loci-card-empty { font-size: 9.5px; color: var(--dsw-alias-label-secondary); }
196
+ .loci-card-hint { font-size: 9px; color: var(--dsw-alias-label-secondary); }
305
197
  .loci-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--dsw-alias-brand-primary); flex: 0 0 auto; }
306
198
 
307
199
  .loci-bar { display: flex; align-items: center; gap: 12px; padding: 10px 16px; border-bottom: 1px solid var(--dsw-alias-border-l1); }
@@ -420,7 +312,7 @@ window.__ModuleLoader__.load({
420
312
  *
421
313
  * The card is a page of notes, so this reads as prose rather than a list: one
422
314
  * clause per entry, joined with 。or , and cut when it stops fitting. Consensus
423
- * items lead with 已定 and a fact leads with its own subject, because "we
315
+ * items lead with the agreement verb and a fact leads with its own subject, because "we
424
316
  * agreed" and "it happens to be true" are different kinds of knowing.
425
317
  */
426
318
  function proseOf(notes, t) {
@@ -452,7 +344,7 @@ window.__ModuleLoader__.load({
452
344
  /**
453
345
  * How long ago something was understood, in words rather than numbers.
454
346
  *
455
- * "昨天" and "刚才" are the whole point of the card: they are what makes a
347
+ * "yesterday" and "just now" are the whole point of the card: they are what makes a
456
348
  * reader believe the memory is alive. A bare timestamp makes them do the
457
349
  * arithmetic instead.
458
350
  */
@@ -503,41 +395,35 @@ window.__ModuleLoader__.load({
503
395
  }
504
396
 
505
397
  /** The small version line shared by the card and the panel footer. */
506
- function VersionLine({ t, update, onCheck, checking }) {
507
- const version = typeof update?.version === 'string' ? update.version.replace(/^v/i, '') : null;
398
+ function VersionLabel({ t, update, onCheck, checking }) {
399
+ // A chip, not a sentence. The old label said "update check is off" inline, which
400
+ // is why the card header read like body copy; the state now lives in the tooltip.
401
+ const version = update?.version;
508
402
  const hasUpdate = update?.updateAvailable === true;
509
- const unknown = update !== undefined && update.status === 'unavailable';
510
- // The check is opt-in, so "off" is a normal state, not a failure: it must read
511
- // as switched off rather than as a button that does nothing.
512
- const checkDisabled = update?.checkEnabled === false;
513
- const text = hasUpdate
514
- ? `${t('card.updateTo')}${update.latestVersion ? ` · v${update.latestVersion}` : ''}`
515
- : unknown
403
+ const note = checking
404
+ ? t('card.checking')
405
+ : update === undefined || update === null
516
406
  ? t('card.unknown')
517
- : checkDisabled
518
- ? t('card.checkOff')
519
- : update?.status === 'ok'
520
- ? t('card.upToDate')
521
- : t('card.check');
407
+ : hasUpdate
408
+ ? t('card.updateTo')
409
+ : update.status === 'disabled'
410
+ ? t('card.checkOff')
411
+ : t('card.upToDate');
522
412
  return h(
523
- 'span',
524
- { className: 'loci-card-line', style: { gap: '5px' } },
525
- version ? h('span', { className: 'loci-card-ver' }, `v${version}`) : null,
526
- h(
527
- 'button',
528
- {
529
- type: 'button',
530
- className: 'loci-btn is-quiet',
531
- title: text,
532
- disabled: checking === true || checkDisabled,
533
- onClick: (event) => {
534
- event.stopPropagation();
535
- onCheck?.();
536
- },
413
+ 'button',
414
+ {
415
+ type: 'button',
416
+ className: 'loci-card-ver',
417
+ title: note,
418
+ 'aria-label': note,
419
+ disabled: checking,
420
+ onClick: (event) => {
421
+ event.stopPropagation();
422
+ onCheck?.();
537
423
  },
538
- checking === true ? t('card.checking') : text,
539
- ),
540
- hasUpdate ? h('span', { className: 'loci-dot' }) : null,
424
+ },
425
+ version ? `v${version}` : t('card.unpublished'),
426
+ hasUpdate ? h('span', { className: 'loci-card-dot is-error', 'aria-hidden': true }) : null,
541
427
  );
542
428
  }
543
429
 
@@ -547,10 +433,12 @@ window.__ModuleLoader__.load({
547
433
  const t = props?.t ?? ((key) => key);
548
434
  const wide = props?.wide !== false;
549
435
  const open = props?.openPanel;
550
- // The card asks "what does it know", not "what happened": a page of notes,
551
- // not a dashboard. No counters — a number tells you something changed, a
552
- // sentence tells you what was understood.
553
- const knowledge = useJson('/knowledge?limit=4', { intervalMs: 30_000 });
436
+ // A compact readout, not a note: the counts are the reason a person looks at this
437
+ // strip at all. The earlier version showed one prose sentence and no numbers, which
438
+ // read as bare text inside the sidebar rather than as a card — and the audit's own
439
+ // "no dashboard" objection was about refusing to interrupt, not about hiding data.
440
+ const stats = useJson('/stats', { intervalMs: 30_000 });
441
+ const config = useJson('/config');
554
442
  const update = useJson('/update');
555
443
  const [checking, setChecking] = useState(false);
556
444
  const [fresh, setFresh] = useState(undefined);
@@ -567,9 +455,19 @@ window.__ModuleLoader__.load({
567
455
  }
568
456
  }, []);
569
457
 
570
- const data = knowledge.data;
571
- const notes = data?.items ?? [];
572
- const pending = Number(data?.pending ?? 0);
458
+ const data = stats.data;
459
+ const byStatus = data?.byStatus ?? {};
460
+ const verified = Number(byStatus.verified ?? 0);
461
+ const pending = Number(data?.pendingCandidates ?? byStatus.candidate ?? 0);
462
+ const disputed = Number(data?.disputed ?? byStatus.disputed ?? 0);
463
+ const entries = Number(data?.heads ?? 0);
464
+ const injection = data?.lastInjection ?? null;
465
+ const indexBytes = Number(injection?.bytes ?? 0);
466
+ const budget = Number(config.data?.config?.sectionMaxBytes ?? 0);
467
+ const usage = Array.isArray(data?.usage) ? data.usage : [];
468
+ const injected = usage.reduce((total, row) => total + Number(row.injections ?? 0), 0);
469
+ const opened = usage.reduce((total, row) => total + Number(row.reads ?? 0), 0);
470
+ const dead = usage.filter((row) => Number(row.injections ?? 0) > 0 && Number(row.reads ?? 0) === 0).length;
573
471
 
574
472
  if (!wide) {
575
473
  return h(
@@ -581,6 +479,7 @@ window.__ModuleLoader__.load({
581
479
  'aria-label': t('card.open'),
582
480
  onClick: () => open?.(),
583
481
  },
482
+ h('style', null, CSS),
584
483
  h('svg', { width: 18, height: 18, viewBox: '0 0 24 24', fill: 'none', 'aria-hidden': true },
585
484
  h('path', { d: 'M4 6.5h16M4 12h16M4 17.5h10', stroke: 'currentColor', strokeWidth: 1.7, strokeLinecap: 'round' }),
586
485
  h('circle', { cx: 18.5, cy: 17.5, r: 2.2, stroke: 'currentColor', strokeWidth: 1.5 }),
@@ -588,6 +487,16 @@ window.__ModuleLoader__.load({
588
487
  );
589
488
  }
590
489
 
490
+ const dotClass = stats.error !== undefined ? 'is-error' : pending > 0 || disputed > 0 ? 'is-warn' : '';
491
+ const stamp = data === undefined ? '' : stampOf(injection?.ts ?? data.generatedAt ?? Date.now());
492
+ const metric = (label, value, tone) =>
493
+ h(
494
+ 'div',
495
+ { className: 'loci-card-metric' },
496
+ h('span', { className: 'loci-card-metric-label' }, label),
497
+ h('span', { className: `loci-card-metric-value${tone ? ` ${tone}` : ''}` }, String(value)),
498
+ );
499
+
591
500
  return h(
592
501
  'div',
593
502
  {
@@ -599,6 +508,7 @@ window.__ModuleLoader__.load({
599
508
  if (event.key === 'Enter' || event.key === ' ') open?.();
600
509
  },
601
510
  },
511
+ h('style', null, CSS),
602
512
  h(
603
513
  'div',
604
514
  { className: 'loci-card-head' },
@@ -616,34 +526,56 @@ window.__ModuleLoader__.load({
616
526
  ),
617
527
  ),
618
528
  h('span', { className: 'loci-card-title' }, t('card.title')),
619
- h('span', { className: 'loci-card-grow' }),
620
- h(VersionLine, { t, update: fresh ?? update.data, onCheck: check, checking }),
529
+ h(VersionLabel, { t, update: fresh ?? update.data, onCheck: check, checking }),
530
+ h('span', { className: `loci-card-dot ${dotClass}`.trim(), 'aria-hidden': true }),
531
+ h('span', { className: 'loci-card-meta' }, stamp ? `${t('card.updated')} ${stamp}` : ''),
621
532
  ),
622
- data === undefined
623
- ? h('div', { className: 'loci-card-empty' }, knowledge.error ?? t('panel.loading'))
624
- : notes.length === 0
625
- ? h(
626
- 'div',
627
- null,
628
- h('div', { className: 'loci-card-empty' }, t('card.empty')),
629
- h('div', { className: 'loci-card-hint' }, t('card.emptyHint')),
630
- )
631
- : h(
632
- 'div',
633
- { className: 'loci-card-note' },
634
- h(
533
+ stats.error !== undefined
534
+ ? h('div', { className: 'loci-card-empty' }, stats.error)
535
+ : data === undefined
536
+ ? h('div', { className: 'loci-card-empty' }, t('panel.loading'))
537
+ : entries === 0 && pending === 0 && disputed === 0
538
+ ? h(
635
539
  'div',
636
- { className: 'loci-card-stamp' },
637
- `${t('card.updated')} ${stampOf(notes[0]?.at ?? data.generatedAt)}`,
540
+ null,
541
+ h('div', { className: 'loci-card-empty' }, t('card.empty')),
542
+ h('div', { className: 'loci-card-hint' }, t('card.emptyHint')),
543
+ )
544
+ : h(
545
+ 'div',
546
+ { className: 'loci-card-grid' },
547
+ metric(t('card.verified'), verified),
548
+ metric(t('card.pending'), pending, pending > 0 ? 'is-warn' : ''),
549
+ metric(t('card.disputed'), disputed, disputed > 0 ? 'is-error' : ''),
638
550
  ),
639
- h('p', { className: 'loci-card-prose' }, proseOf(notes, t)),
640
- pending > 0
641
- ? h('div', { className: 'loci-card-hint' }, t('card.pending').replace('{n}', String(pending)))
642
- : null,
551
+ data !== undefined && entries > 0 && budget > 0
552
+ ? h(
553
+ 'div',
554
+ { className: 'loci-card-barline' },
555
+ h(
556
+ 'div',
557
+ { className: 'loci-card-bar', role: 'presentation' },
558
+ h('div', {
559
+ className: `loci-card-bar-fill${indexBytes / budget > 0.85 ? ' is-warn' : ''}`,
560
+ style: { width: `${Math.min(100, Math.round((indexBytes / budget) * 100))}%` },
561
+ }),
643
562
  ),
563
+ h('span', { className: 'loci-card-bar-label' }, `${indexBytes} / ${budget} B`),
564
+ )
565
+ : null,
566
+ data !== undefined && injected > 0
567
+ ? h(
568
+ 'div',
569
+ { className: 'loci-card-foot' },
570
+ h('span', null, `${t('card.injected')} ${injected}`),
571
+ h('span', null, `${t('card.opened')} ${opened}`),
572
+ dead > 0 ? h('span', null, `${t('card.dead')} ${dead}`) : null,
573
+ h('span', { className: 'loci-card-foot-grow' }),
574
+ )
575
+ : null,
576
+ pending > 0 ? h('div', { className: 'loci-card-hint' }, t('card.pendingHint').replace('{n}', String(pending))) : null,
644
577
  );
645
578
  }
646
-
647
579
  // ---------------------------------------------------------------- panel
648
580
 
649
581
  function FeedTab({ t, onOpen }) {
@@ -1434,11 +1366,11 @@ window.__ModuleLoader__.load({
1434
1366
  }
1435
1367
 
1436
1368
  function apply(ctx) {
1437
- ctx.effect(() => ctx.locale.register(NS, { zh: ZH, en: EN }), 'dsh-loci: locale dictionaries');
1369
+ ctx.effect(() => ctx.locale.register(NS, { en: EN }), 'dsh-loci: locale dictionaries');
1438
1370
  const bind = ctx.locale.bind(NS);
1439
1371
  const t = tOf(bind);
1440
1372
 
1441
- // The "副屏": a card beside Settings, always visible while the sidebar is.
1373
+ // The sidebar mirror: a card beside Settings, always visible while the sidebar is.
1442
1374
  ctx.slots.inject('sidebar.footer.action', () =>
1443
1375
  ctx.slots.register(
1444
1376
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-loci",
3
- "version": "2.11.0",
3
+ "version": "3.0.1",
4
4
  "description": "Layered long-term memory for DeepSeek Harness: the prompt sees only a stable index, full text is one lookup away. Bi-temporal facts, strict scope isolation, verified-only injection.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/host/index.js CHANGED
@@ -23,7 +23,7 @@ import { registerCommands } from './commands.js';
23
23
  import { runDoctor } from './doctor.js';
24
24
 
25
25
  /** Version stamped into the database and diagnostics. */
26
- export const VERSION = '2.11.0';
26
+ export const VERSION = '3.0.1';
27
27
 
28
28
  /** Optional collaborators; without them the plugin still serves tools. */
29
29
  export const inject = ['systemPrompt', 'tools'];
@@ -36,7 +36,7 @@ export function openFacadeStore(options = {}) {
36
36
  memoryRoot: root,
37
37
  paths,
38
38
  config,
39
- pluginVersion: '2.11.0',
39
+ pluginVersion: '3.0.1',
40
40
  readOnly: options.readOnly === true,
41
41
  });
42
42
  return { store, paths };
package/locale/zh.json DELETED
@@ -1,12 +0,0 @@
1
- {
2
- "meta": {
3
- "title": "Loci 记忆宫殿",
4
- "description": "分层长期记忆:提示词里只放稳定索引,全文按需一次取回;只有已核实的记忆才会被注入。"
5
- },
6
- "panel": {
7
- "title": "记忆"
8
- },
9
- "settings": {
10
- "title": "Loci"
11
- }
12
- }