dsh-loci 3.0.0 → 3.1.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/CHANGELOG.md CHANGED
@@ -4,6 +4,86 @@ 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.1.0] — 2026-11-15
8
+
9
+ Four things from looking at the live sidebar and panel.
10
+
11
+ ### Changed
12
+
13
+ - **The sidebar icon now has the same weight as the menu it sits in.** Native
14
+ `sidebar.panellist` entries receive `{ size, active }`; this one used `size` correctly but
15
+ drew inside 4–20 of a 24 viewBox with a small off-centre dot, so beside the shipped icons
16
+ it read thinner and smaller. Redrawn as a symmetric **layers** glyph spanning 3–21 at
17
+ stroke 1.8 — the optical weight of its neighbours, and a shape that says layered memory.
18
+
19
+ - **The panel is a readable column, not edge-to-edge text.** The scroll container's
20
+ horizontal padding became `max(16px, calc((100% - 860px) / 2))`, so rows sit in a centred
21
+ measure on a wide window without touching any tab component.
22
+
23
+ - **"N item(s) waiting for you in Review" is now a way in.** It was a sentence telling a
24
+ person to go somewhere else and find the tab. It is a button: it opens the panel already
25
+ on Review. The card and the panel mount separately, so the request travels through module
26
+ scope and is consumed once.
27
+
28
+ - **The card says what it knows, not only how much.** Commit `4065c98` (2026-11-13) recorded
29
+ the brief for this surface: *"not a dashboard … read the card and think 'it got that, it
30
+ recorded that, we agreed on that'"*, with one short paragraph of what the agent
31
+ understands in this project and pending work as one quiet line, never a number. 3.0.0
32
+ replaced that paragraph with counters. The counters stay — they were asked for and they
33
+ are useful — and one line of content goes back under them: a `Latest` row showing the most
34
+ recently trusted entry, from `GET /api/loci/knowledge?limit=1`.
35
+
36
+ ### Not changed
37
+
38
+ - `docs/PRIOR-ART.md` still records that the card should be scoped to **this project's read
39
+ set** rather than every scope. It currently is not: `/stats` and `/knowledge` called
40
+ without parameters enumerate the whole store, and the sidebar card has no access to the
41
+ session's cwd. Scoping the card is a real open item, not something to assert.
42
+
43
+ ## [3.0.1] — 2026-11-15
44
+
45
+ The 3.0.0 card was correct markup with no stylesheet, and the screenshot showed it.
46
+
47
+ ### Fixed
48
+
49
+ - **The card was never styled outside the panel.** Every `.loci-*` rule lives in one CSS
50
+ template string that only `MemoryPanel` and `SettingsSection` render as a `<style>`
51
+ element. In the sidebar, with the panel closed, no `<style>` existed — so the card's DOM
52
+ was right and its layout was simply absent. That is what "bare text in the sidebar"
53
+ looked like, and it predates 3.0.0: the first screenshot showed it too. The card now
54
+ renders the stylesheet itself, and so does its rail variant.
55
+
56
+ - **`card.pending` was defined twice.** The old value was the sentence
57
+ `'{n} still need your review'`; 3.0.0 added a bare `'Pending'` label for the metric row,
58
+ and the later definition won — so the card printed the sentence with an unsubstituted
59
+ `{n}`. The sentence is now `card.pendingSentence` and the label stays `Pending`.
60
+
61
+ - **The version chip rendered as `v3. 0. 0`.** The chip used
62
+ `ui-monospace, SFMono-Regular, Menlo, monospace`, which on Windows falls back to a
63
+ monospace face whose period is centred in a wide advance; at 8.5px that reads as gaps
64
+ between the digits. Measured with `getComputedStyle`: `letter-spacing: normal`,
65
+ `word-spacing: 0`, width 25.50 — the spacing was correct, the **glyph** was not. The chip
66
+ now inherits the UI font with tabular figures, at 9px.
67
+
68
+ ### Added
69
+
70
+ - **`scripts/shot-card.mjs`** renders the sidebar card to a PNG using the stylesheet
71
+ extracted from `client.js` and the DOM the component produces, through headless Edge. It
72
+ cannot catch a logic mistake, only a drawing one — but a drawing mistake is exactly what
73
+ shipped twice, and a screenshot is the only review that shows it.
74
+
75
+ ### Not added
76
+
77
+ Four guard files from the previous draft (a separate client-i18n test, an English-only test,
78
+ a CSS-shape assertion and a manifest check) were collapsed into **one** test file and one
79
+ manifest assertion. Guards that outnumber the behaviour they protect get deleted by the next
80
+ person, which is worse than not having them.
81
+
82
+ ### Corrected in the record
83
+
84
+ The 3.0.0 log entry said the card had been verified. It had not been looked at. The entry
85
+ above records what the screenshot showed.
86
+
7
87
  ## [3.0.0] — 2026-11-15
8
88
 
9
89
  The sidebar card was not a card, and the plugin had a second language it did not need.
package/client.js CHANGED
@@ -27,6 +27,9 @@ window.__ModuleLoader__.load({
27
27
  const API = '/api/loci';
28
28
  const ENTRY_ID = 'dsh-loci';
29
29
  const PANEL_ID = 'loci';
30
+ // The card and the panel mount separately, so a request to open a specific tab has
31
+ // to travel through module scope. The panel consumes it once, then clears it.
32
+ let requestedTab;
30
33
 
31
34
  const EN = {
32
35
  'meta.title': 'Loci memory palace',
@@ -44,8 +47,9 @@ window.__ModuleLoader__.load({
44
47
  'card.opened': 'Opened',
45
48
  'card.dead': 'Dead',
46
49
  'card.pendingHint': '{n} item(s) waiting for you in Review.',
50
+ 'card.latest': 'Latest',
47
51
  'card.updated': 'updated',
48
- 'card.pending': '{n} still need your review',
52
+ 'card.pendingSentence': '{n} still need your review',
49
53
  'card.pinned': 'pinned',
50
54
  'card.open': 'Open memory',
51
55
  'card.check': 'Check for updates',
@@ -172,7 +176,7 @@ window.__ModuleLoader__.load({
172
176
  .loci-card-root.is-rail { flex: 0 0 auto; padding: 6px; align-items: center; justify-content: center; }
173
177
  .loci-card-head { display: flex; align-items: center; gap: 6px; min-width: 0; width: 100%; }
174
178
  .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: 8.5px; line-height: 1.2; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; cursor: pointer; white-space: nowrap; }
179
+ .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
180
  .loci-card-ver:hover { color: var(--dsw-alias-label-primary); }
177
181
  .loci-card-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--dsw-alias-brand-primary); flex: 0 0 auto; }
178
182
  .loci-card-dot.is-warn { background: var(--dsw-alias-state-warn-primary); }
@@ -194,6 +198,11 @@ window.__ModuleLoader__.load({
194
198
  .loci-card-foot-grow { flex: 1 1 auto; }
195
199
  .loci-card-empty { font-size: 9.5px; color: var(--dsw-alias-label-secondary); }
196
200
  .loci-card-hint { font-size: 9px; color: var(--dsw-alias-label-secondary); }
201
+ .loci-card-known { display: flex; gap: 5px; align-items: baseline; min-width: 0; }
202
+ .loci-card-known-tag { flex: 0 0 auto; font-size: 8px; letter-spacing: .05em; text-transform: uppercase; color: var(--dsw-alias-label-secondary); }
203
+ .loci-card-known-text { min-width: 0; font-size: 9.5px; color: var(--dsw-alias-label-primary); overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
204
+ .loci-card-hint-action { align-self: flex-start; margin: 0; padding: 2px 6px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); font: inherit; font-size: 9px; line-height: 1.2; cursor: pointer; text-align: left; }
205
+ .loci-card-hint-action:hover { border-color: var(--dsw-alias-brand-primary); color: var(--dsw-alias-brand-primary); }
197
206
  .loci-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--dsw-alias-brand-primary); flex: 0 0 auto; }
198
207
 
199
208
  .loci-bar { display: flex; align-items: center; gap: 12px; padding: 10px 16px; border-bottom: 1px solid var(--dsw-alias-border-l1); }
@@ -203,7 +212,7 @@ window.__ModuleLoader__.load({
203
212
  .loci-tab:hover { background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); }
204
213
  .loci-tab.is-active { background: var(--dsw-alias-bg-layer-2); color: var(--dsw-alias-label-primary); font-weight: 600; }
205
214
  .loci-grow { flex: 1 1 auto; }
206
- .loci-body { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 12px 16px 24px; }
215
+ .loci-body { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 14px max(16px, calc((100% - 860px) / 2)) 28px; }
207
216
  .loci-btn { border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); border-radius: 6px; padding: 3px 9px; font: inherit; cursor: pointer; }
208
217
  .loci-btn:hover { border-color: var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-2); }
209
218
  .loci-btn:disabled { opacity: .5; cursor: default; }
@@ -438,6 +447,7 @@ window.__ModuleLoader__.load({
438
447
  // read as bare text inside the sidebar rather than as a card — and the audit's own
439
448
  // "no dashboard" objection was about refusing to interrupt, not about hiding data.
440
449
  const stats = useJson('/stats', { intervalMs: 30_000 });
450
+ const knowledge = useJson('/knowledge?limit=1', { intervalMs: 30_000 });
441
451
  const config = useJson('/config');
442
452
  const update = useJson('/update');
443
453
  const [checking, setChecking] = useState(false);
@@ -468,6 +478,10 @@ window.__ModuleLoader__.load({
468
478
  const injected = usage.reduce((total, row) => total + Number(row.injections ?? 0), 0);
469
479
  const opened = usage.reduce((total, row) => total + Number(row.reads ?? 0), 0);
470
480
  const dead = usage.filter((row) => Number(row.injections ?? 0) > 0 && Number(row.reads ?? 0) === 0).length;
481
+ // What the agent currently understands, newest first. Numbers answer "how much";
482
+ // this answers "what".
483
+ const top = knowledge.data?.items?.[0];
484
+ const latest = typeof top?.title === 'string' && top.title.length > 0 ? top.title : null;
471
485
 
472
486
  if (!wide) {
473
487
  return h(
@@ -479,6 +493,7 @@ window.__ModuleLoader__.load({
479
493
  'aria-label': t('card.open'),
480
494
  onClick: () => open?.(),
481
495
  },
496
+ h('style', null, CSS),
482
497
  h('svg', { width: 18, height: 18, viewBox: '0 0 24 24', fill: 'none', 'aria-hidden': true },
483
498
  h('path', { d: 'M4 6.5h16M4 12h16M4 17.5h10', stroke: 'currentColor', strokeWidth: 1.7, strokeLinecap: 'round' }),
484
499
  h('circle', { cx: 18.5, cy: 17.5, r: 2.2, stroke: 'currentColor', strokeWidth: 1.5 }),
@@ -507,6 +522,7 @@ window.__ModuleLoader__.load({
507
522
  if (event.key === 'Enter' || event.key === ' ') open?.();
508
523
  },
509
524
  },
525
+ h('style', null, CSS),
510
526
  h(
511
527
  'div',
512
528
  { className: 'loci-card-head' },
@@ -546,6 +562,14 @@ window.__ModuleLoader__.load({
546
562
  metric(t('card.pending'), pending, pending > 0 ? 'is-warn' : ''),
547
563
  metric(t('card.disputed'), disputed, disputed > 0 ? 'is-error' : ''),
548
564
  ),
565
+ data !== undefined && latest !== null
566
+ ? h(
567
+ 'div',
568
+ { className: 'loci-card-known' },
569
+ h('span', { className: 'loci-card-known-tag' }, t('card.latest')),
570
+ h('span', { className: 'loci-card-known-text' }, latest),
571
+ )
572
+ : null,
549
573
  data !== undefined && entries > 0 && budget > 0
550
574
  ? h(
551
575
  'div',
@@ -571,7 +595,21 @@ window.__ModuleLoader__.load({
571
595
  h('span', { className: 'loci-card-foot-grow' }),
572
596
  )
573
597
  : null,
574
- pending > 0 ? h('div', { className: 'loci-card-hint' }, t('card.pendingHint').replace('{n}', String(pending))) : null,
598
+ pending > 0
599
+ ? h(
600
+ 'button',
601
+ {
602
+ type: 'button',
603
+ className: 'loci-card-hint loci-card-hint-action',
604
+ onClick: (event) => {
605
+ event.stopPropagation();
606
+ requestedTab = 'review';
607
+ open?.();
608
+ },
609
+ },
610
+ t('card.pendingHint').replace('{n}', String(pending)),
611
+ )
612
+ : null,
575
613
  );
576
614
  }
577
615
  // ---------------------------------------------------------------- panel
@@ -1134,7 +1172,12 @@ window.__ModuleLoader__.load({
1134
1172
  }
1135
1173
 
1136
1174
  function MemoryPanel({ t }) {
1137
- const [tab, setTab] = useState('feed');
1175
+ const [tab, setTab] = useState(() => {
1176
+ // "N item(s) waiting for you in Review" in the card is a way in, not a remark.
1177
+ const wanted = requestedTab;
1178
+ requestedTab = undefined;
1179
+ return wanted ?? 'feed';
1180
+ });
1138
1181
  const [selected, setSelected] = useState(undefined);
1139
1182
  const [nonce, setNonce] = useState(0);
1140
1183
  const tabs = useMemo(
@@ -1178,6 +1221,10 @@ window.__ModuleLoader__.load({
1178
1221
  }
1179
1222
 
1180
1223
  function PanelIcon({ size, active }) {
1224
+ // Three stacked sheets: the product is a layered index, and the glyph has to read at
1225
+ // the same optical weight as the shipped icons beside it. The previous drawing lived
1226
+ // in 4–20 of the viewBox with a small off-centre dot, which made it look thinner and
1227
+ // smaller than its neighbours in the sidebar.
1181
1228
  return h(
1182
1229
  'svg',
1183
1230
  {
@@ -1186,10 +1233,28 @@ window.__ModuleLoader__.load({
1186
1233
  viewBox: '0 0 24 24',
1187
1234
  fill: 'none',
1188
1235
  'aria-hidden': true,
1189
- style: { opacity: active ? 1 : 0.75, display: 'block' },
1236
+ style: { opacity: active ? 1 : 0.72, display: 'block' },
1190
1237
  },
1191
- h('path', { d: 'M4 6.5h16M4 12h16M4 17.5h10', stroke: 'currentColor', strokeWidth: 1.7, strokeLinecap: 'round' }),
1192
- h('circle', { cx: 18.5, cy: 17.5, r: 2.2, stroke: 'currentColor', strokeWidth: 1.5 }),
1238
+ h('path', {
1239
+ d: 'M12 3 21 7.5 12 12 3 7.5 12 3Z',
1240
+ stroke: 'currentColor',
1241
+ strokeWidth: 1.8,
1242
+ strokeLinejoin: 'round',
1243
+ }),
1244
+ h('path', {
1245
+ d: 'M3.6 12.2 12 16.4l8.4-4.2',
1246
+ stroke: 'currentColor',
1247
+ strokeWidth: 1.8,
1248
+ strokeLinecap: 'round',
1249
+ strokeLinejoin: 'round',
1250
+ }),
1251
+ h('path', {
1252
+ d: 'M3.6 16.8 12 21l8.4-4.2',
1253
+ stroke: 'currentColor',
1254
+ strokeWidth: 1.8,
1255
+ strokeLinecap: 'round',
1256
+ strokeLinejoin: 'round',
1257
+ }),
1193
1258
  );
1194
1259
  }
1195
1260
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-loci",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
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 = '3.0.0';
26
+ export const VERSION = '3.1.0';
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: '3.0.0',
39
+ pluginVersion: '3.1.0',
40
40
  readOnly: options.readOnly === true,
41
41
  });
42
42
  return { store, paths };