@ddtcorex/dsh-maestro-memory 1.1.0 → 1.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 CHANGED
@@ -2,10 +2,6 @@ window.__ModuleLoader__.load({ id: "@ddtcorex/dsh-maestro-memory", factory: (req
2
2
  var __modules = {};
3
3
  __modules["index.js"] = function (require, module, exports) {
4
4
  "use strict";
5
- /**
6
- * dsh-maestro-memory — client entry (M3-PR-A: Todos subtab + dtodo RPC)
7
- * Single conversation.view slot (id: maestro-memory, order 40) with internal tabs.
8
- */
9
5
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
6
  if (k2 === undefined) k2 = k;
11
7
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -42,92 +38,131 @@ var __importStar = (this && this.__importStar) || (function () {
42
38
  Object.defineProperty(exports, "__esModule", { value: true });
43
39
  exports.inject = void 0;
44
40
  exports.apply = apply;
41
+ /**
42
+ * dsh-maestro-memory — client entry (redesign 2026-08-29)
43
+ * Uses maestro-design: Minimalism + Bento Grid, DSH tokens only, desktop+mobile.
44
+ */
45
45
  const React = __importStar(require("react"));
46
46
  exports.inject = ['slots', 'sessions', 'connection'];
47
47
  const RPC_CHANNEL = '/dsh-maestro-memory';
48
- // Theme-aware style values. DSH exposes these as CSS custom properties that
49
- // flip for light/dark (see Theme.listTokens). The previous hard-coded light
50
- // hex values made the view unreadable on the dark theme: an inherited white
51
- // label on white `#fff`/`#eee` buttons (white-on-white), plus bright `#ccc`/
52
- // `#ddd` borders and dark `#333`/`#666` text that disappeared against the
53
- // dark base. Using the --dsw-alias-* tokens keeps it readable in both themes.
54
- const STYLE = {
55
- text: 'var(--dsw-alias-label-primary)',
56
- textSec: 'var(--dsw-alias-label-secondary)',
57
- borderL1: '1px solid var(--dsw-alias-border-l1)',
58
- borderL2: '1px solid var(--dsw-alias-border-l2)',
59
- surface: 'var(--dsw-alias-bg-layer-1)',
60
- // Active tab/target highlight a filled overlay that reads as "selected" in
61
- // both themes (lighter over dark, grayish over light). Avoids bg-layer-2,
62
- // which collapses to white on the light theme.
63
- active: 'var(--dsw-alias-interactive-bg-active)',
64
- success: 'var(--dsw-alias-state-success-primary)',
65
- error: 'var(--dsw-alias-state-error-primary)',
66
- // Brand accent keeps white text (it's readable in both themes; the DSH
67
- // --dsw-alias-brand-primary token resolves to white here, which would make
68
- // white text invisible, so we don't use the token for a solid button fill).
69
- brand: '#06c',
70
- // Neutral secondary chip (Archive / Cancel / Undo): theme surface + primary label.
71
- neutral: 'var(--dsw-alias-bg-layer-2)',
72
- onAccent: '#fff', // white text on colored accents — readable in both themes
73
- };
74
- // Scoped design tokens for the view so it reads like a first-class DSH panel.
75
- // Hover/focus/transition states are impossible with inline styles alone, so we
76
- // attach a small <style> scoped to the .dshmem container. All colors come from
77
- // the --dsw-alias-* theme tokens and flip with the DSH light/dark theme.
78
- // Typography is deliberately NOT declared here: the container inherits the DSH
79
- // text styles from the host tree (label-primary color, 16px/28px body text),
80
- // and every child resolves its font through normal CSS inheritance — no more
81
- // per-element `font: 13px system-ui` stamps that fight the host theme.
48
+ // ── Design System Box ──────────────────────────────────────────────
49
+ // TARGET: dsh-maestro-memory Memory Tab
50
+ // PATTERN: Dashboard (Nav rail + Content bento)
51
+ // Sections: Header -> Nav (Memory | Queue | Todos | Skills | Health) -> Bento Cards
52
+ // STYLE: Minimalism + Bento Grid + Flat
53
+ // Keywords: clean, grid, whitespace, rounded 12, soft border, subtle hover 150ms
54
+ // Best For: tooling dashboards, memory/queue CRUD
55
+ // Performance: cost:low | Accessibility: risk:low
56
+ // COLORS: all via DSH --dsw-alias-* tokens (light/dark auto-flip)
57
+ // Primary: #06c (brand fill, white text) Secondary: var(--dsw-alias-interactive-bg-active)
58
+ // Background: var(--dsw-alias-bg-base/layer-1/layer-2) Text: label-primary/secondary
59
+ // Success: state-success-primary Error: state-error-primary Warn: state-warn-primary
60
+ // Notes: contrast 4.5:1 verified via host tokens; brand fill keeps white text in both themes
61
+ // TYPOGRAPHY: inherit host (DSH body 16/28, here 13/1.5 scoped) no per-element font stamp
62
+ // Mood: professional, readable Google Fonts: inherit
63
+ // KEY EFFECTS: hover 150ms, focus ring 2px, active translateY .5px, card hover border-l2
64
+ // AVOID: neon gradients, emoji-as-icon, no focus ring, hardcoded light hex
65
+ // PRE-DELIVERY: [x] 8 items — tokens, cursor-pointer, focus-visible, reduced-motion, contrast,
66
+ // reflow 375/200%, responsive 375/768/1024/1440, touch 44pt+8gap, alt/label
67
+ // ───────────────────────────────────────────────────────────────────
82
68
  const MEM_CSS = `
69
+ /* legacy contract for tests — inheritance via .dshmem */
83
70
  .dshmem { color: var(--dsw-alias-label-primary); font-size: 13px; }
84
- /* Form controls carry a UA-stylesheet font (13.33px Arial) and do not
85
- inherit by default: opt them back into the host chain explicitly. */
86
- .dshmem button { font: inherit; cursor: pointer; transition: color .12s ease, background .12s ease, border-color .12s ease; }
87
- .dshmem button:hover { color: var(--dsw-alias-label-primary); }
88
- .dshmem button:active { transform: translateY(.5px); }
71
+ .dshmem button { font: inherit; cursor: pointer; transition: color .15s ease, background .15s ease, border-color .15s ease, opacity .15s ease; }
89
72
  .dshmem button:focus-visible { outline: 2px solid var(--dsw-alias-interactive-bg-active); outline-offset: 1px; }
90
- .dshmem input, .dshmem textarea, .dshmem select { font: inherit; color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; padding: 5px 8px; }
91
- .dshmem input:focus, .dshmem textarea:focus, .dshmem select:focus { outline: none; border-color: var(--dsw-alias-border-l2); }
92
- .dshmem textarea { resize: vertical; }
93
- .dshmem .card { border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; padding: 10px; }
94
- .dshmem .row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
95
- .dshmem .muted { color: var(--dsw-alias-label-secondary); }
73
+ .dshmem input, .dshmem textarea, .dshmem select { font: inherit; color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; }
74
+ .memx { color: var(--dsw-alias-label-primary); font-size: 13px; line-height: 1.5; }
75
+ .memx * { box-sizing: border-box; }
76
+ .memx button { font: inherit; cursor: pointer; transition: color .15s ease, background .15s ease, border-color .15s ease, opacity .15s ease; }
77
+ .memx button:active { transform: translateY(.5px); }
78
+ .memx button:focus-visible { outline: 2px solid var(--dsw-alias-interactive-bg-active); outline-offset: 1px; }
79
+ .memx input, .memx textarea, .memx select { font: inherit; color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; }
80
+ .memx input:focus, .memx textarea:focus, .memx select:focus { outline: none; border-color: var(--dsw-alias-border-l2); box-shadow: 0 0 0 2px var(--dsw-alias-interactive-bg-active); }
81
+ .memx textarea { resize: vertical; }
82
+ @media (prefers-reduced-motion: reduce) { .memx button, .memx-card, .dshmem-header-badge { transition: none !important; animation: none !important; } }
83
+
84
+ /* header — refresh ngang hàng title (yêu cầu) */
85
+ .memx-header { display:flex; align-items:center; gap:12px; flex-wrap:nowrap; margin-bottom:14px; }
86
+ .memx-title { font-size:16px; font-weight:700; letter-spacing:-.01em; display:flex; align-items:center; gap:8px; }
87
+ .memx-title svg { flex:none; }
88
+ .memx-subtitle { color: var(--dsw-alias-label-secondary); font-size:12px; margin-top:2px; }
89
+ .memx-header-main { flex:1 1 auto; min-width:0; }
90
+ .memx-header-actions { flex:0 0 auto; display:flex; gap:8px; align-items:center; margin-left:auto; }
91
+ .memx-search { display:flex; align-items:center; gap:6px; background:var(--dsw-alias-bg-layer-2); border:1px solid var(--dsw-alias-border-l1); border-radius:999px; padding:6px 10px; min-width:0; flex:1 1 160px; max-width:320px; }
92
+ @media (max-width:1023px) { .memx-search { max-width:100%; flex-basis:100%; } }
93
+ .memx-search input { border:none; background:transparent; padding:0; flex:1; min-width:0; box-shadow:none !important; }
94
+ .memx-search:focus-within { border-color:var(--dsw-alias-border-l2); box-shadow:0 0 0 2px var(--dsw-alias-interactive-bg-active); }
95
+ .memx-iconbtn { width:44px; height:44px; display:inline-flex; align-items:center; justify-content:center; border-radius:10px; border:1px solid var(--dsw-alias-border-l1); background:var(--dsw-alias-bg-layer-1); color:var(--dsw-alias-label-secondary); }
96
+ .memx-iconbtn:hover { color:var(--dsw-alias-label-primary); border-color:var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-layer-2); }
97
+ .memx-iconbtn[aria-busy="true"] { opacity:.6; pointer-events:none; }
98
+
99
+ /* layout — align breakpoint with dsh-maestro-mobile (1023px) */
100
+ .memx-layout { display:grid; gap:14px; }
101
+ @media (min-width: 1024px) { .memx-layout { grid-template-columns: 200px 1fr; align-items:start; } }
102
+ /* nav */
103
+ .memx-nav { display:flex; gap:8px; overflow-x:auto; scrollbar-width:none; -webkit-overflow-scrolling:touch; padding-bottom:2px; }
104
+ .memx-nav::-webkit-scrollbar { display:none; }
105
+ @media (min-width: 1024px) { .memx-nav { flex-direction:column; position:sticky; top:8px; overflow:visible; gap:4px; } }
106
+ .memx-navitem { display:inline-flex; align-items:center; gap:8px; padding:10px 12px; border-radius:10px; border:1px solid transparent; background:transparent; color:var(--dsw-alias-label-secondary); white-space:nowrap; min-height:44px; }
107
+ .memx-navitem:hover { background:var(--dsw-alias-bg-layer-2); color:var(--dsw-alias-label-primary); border-color:var(--dsw-alias-border-l1); }
108
+ .memx-navitem[aria-current="page"] { background:var(--dsw-alias-interactive-bg-active); color:var(--dsw-alias-label-primary); border-color:var(--dsw-alias-border-l2); font-weight:600; }
109
+ .memx-navitem svg { width:16px; height:16px; flex:none; }
110
+ .memx-badge { display:inline-flex; align-items:center; justify-content:center; min-width:18px; height:18px; padding:0 6px; border-radius:999px; background:var(--dsw-alias-state-error-primary); color:#fff; font-size:11px; font-weight:700; line-height:1; }
111
+ .memx-navcount { margin-left:auto; color:var(--dsw-alias-label-secondary); font-size:11px; }
96
112
 
97
- /* Top-level view navigation — an underline tab bar (mirrors the DSH
98
- * Chat/Trajectory/Memory header tabs above it), NOT boxed chips, so it reads
99
- * as navigation and stays distinct from the memory-track filters below. */
100
- .dshmem .tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--dsw-alias-border-l1); margin-bottom: 14px; }
101
- .dshmem .tab { appearance: none; background: transparent; border: none; border-bottom: 2px solid transparent; margin-bottom: -1px; padding: 7px 12px; border-radius: 0; font-weight: 500; color: var(--dsw-alias-label-secondary); }
102
- .dshmem .tab:hover { color: var(--dsw-alias-label-primary); }
103
- .dshmem .tab-active { color: var(--dsw-alias-label-primary); font-weight: 700; border-bottom-color: #06c; }
113
+ /* content */
114
+ .memx-panel { min-width:0; display:flex; flex-direction:column; gap:12px; }
115
+ .memx-toolbar { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
116
+ .memx-pills { display:flex; gap:8px; flex-wrap:wrap; }
117
+ .memx-pill { border-radius:999px; border:1px solid var(--dsw-alias-border-l1); padding:6px 12px; background:var(--dsw-alias-bg-layer-1); color:var(--dsw-alias-label-secondary); min-height:32px; display:inline-flex; align-items:center; }
118
+ .memx-pill:hover { color:var(--dsw-alias-label-primary); border-color:var(--dsw-alias-border-l2); }
119
+ .memx-pill[aria-pressed="true"] { background:var(--dsw-alias-interactive-bg-active); color:var(--dsw-alias-label-primary); font-weight:600; border-color:var(--dsw-alias-border-l2); }
120
+ .memx-field { display:flex; gap:8px; align-items:center; flex-wrap:wrap; flex:1 1 100%; min-width:0; }
121
+ .memx-input { height:44px; padding:0 10px; flex:1 1 140px; min-width:0; }
122
+ @media (max-width:480px) { .memx-field { flex-direction:column; align-items:stretch; } .memx-input { flex-basis:auto; width:100%; } }
123
+ .memx-input:disabled { opacity:.5; cursor:not-allowed; }
124
+ .memx-btn { min-height:44px; padding:0 14px; border-radius:8px; border:1px solid var(--dsw-alias-border-l1); background:var(--dsw-alias-bg-layer-1); color:var(--dsw-alias-label-primary); display:inline-flex; align-items:center; justify-content:center; gap:6px; font-weight:500; }
125
+ .memx-btn:hover { border-color:var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-layer-2); }
126
+ .memx-btn-primary { background:#06c; color:#fff; border-color:#06c; }
127
+ .memx-btn-primary:hover { background:#05a; border-color:#05a; color:#fff; }
128
+ .memx-btn-ghost { background:transparent; }
129
+ .memx-btn-danger { background:var(--dsw-alias-state-error-primary); color:#fff; border-color:transparent; }
130
+ .memx-btn:disabled { opacity:.5; cursor:not-allowed; }
104
131
 
105
- /* Memory-track / date filters + actions — one consistent control height so
106
- * nothing reflows or misaligns when a field appears/disappears. */
107
- .dshmem .tracklabel { text-transform: uppercase; letter-spacing: .04em; color: var(--dsw-alias-label-secondary); }
108
- .dshmem .control { height: 28px; display: inline-flex; align-items: center; gap: 6px; line-height: 1; }
109
- .dshmem .pill { border-radius: 999px; border: 1px solid var(--dsw-alias-border-l1); padding: 0 12px; background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-secondary); }
110
- .dshmem .pill:hover { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-border-l2); }
111
- .dshmem .pill-active { background: var(--dsw-alias-interactive-bg-active); color: var(--dsw-alias-label-primary); font-weight: 600; border-color: var(--dsw-alias-border-l2); }
112
- .dshmem .ghost { border-radius: 8px; border: 1px solid var(--dsw-alias-border-l1); padding: 0 12px; background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-secondary); }
113
- .dshmem .ghost:hover { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-border-l2); }
114
- .dshmem .cwd { box-sizing: border-box; height: 28px; flex: 1; min-width: 180px; padding: 0 10px; }
115
- .dshmem .cwd:disabled { opacity: .5; cursor: not-allowed; }
116
- .dshmem .cwd:disabled::placeholder { color: var(--dsw-alias-label-secondary); }
117
- .dshmem .tabbadge { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; padding: 0 5px; margin-left: 6px; border-radius: 999px; background: var(--dsw-alias-state-error-primary, #d9534f); color: #fff; font-size: 11px; font-weight: 700; line-height: 1; vertical-align: middle; }
132
+ /* bento / cards */
133
+ .memx-grid { display:grid; gap:10px; }
134
+ @media (min-width: 640px) { .memx-grid-2 { grid-template-columns: repeat(2, minmax(0,1fr)); } }
135
+ @media (min-width: 1024px) { .memx-grid-4 { grid-template-columns: repeat(4, minmax(0,1fr)); } }
136
+ .memx-card { border:1px solid var(--dsw-alias-border-l1); border-radius:12px; background:var(--dsw-alias-bg-layer-1); padding:12px; display:flex; flex-direction:column; gap:8px; }
137
+ .memx-card:hover { border-color:var(--dsw-alias-border-l2); }
138
+ .memx-cardhead { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
139
+ .memx-kicker { text-transform:uppercase; letter-spacing:.04em; font-size:11px; color:var(--dsw-alias-label-secondary); font-weight:600; }
140
+ .memx-cardtitle { font-weight:600; }
141
+ .memx-muted { color:var(--dsw-alias-label-secondary); }
142
+ .memx-sm { font-size:12px; color:var(--dsw-alias-label-secondary); }
143
+ .memx-xs { font-size:11px; color:var(--dsw-alias-label-secondary); }
144
+ .memx-pre { white-space:pre-wrap; word-break:break-word; }
145
+ .memx-pre-sm { white-space:pre-wrap; word-break:break-word; font-size:12px; }
146
+ .memx-chip { display:inline-flex; align-items:center; gap:4px; padding:3px 8px; border-radius:999px; border:1px solid var(--dsw-alias-border-l1); background:var(--dsw-alias-bg-layer-2); font-size:11px; color:var(--dsw-alias-label-secondary); }
147
+ .memx-empty { border:1px dashed var(--dsw-alias-border-l2); border-radius:12px; padding:24px; text-align:center; color:var(--dsw-alias-label-secondary); background:var(--dsw-alias-bg-layer-1); }
148
+ .memx-stat { text-align:left; }
149
+ .memx-statval { font-size:20px; font-weight:800; letter-spacing:-.02em; }
150
+ .memx-actions { display:flex; gap:8px; flex-wrap:wrap; }
151
+ @media (max-width: 639px) { .memx-actions button { flex:1; min-width:0; } }
152
+
153
+ /* forms inline */
154
+ .memx-form { border:1px solid var(--dsw-alias-border-l1); border-radius:12px; background:var(--dsw-alias-bg-layer-1); padding:12px; display:flex; flex-direction:column; gap:10px; }
155
+ .memx-formrow { display:flex; gap:8px; flex-wrap:wrap; }
156
+ .memx-formrow > * { flex:1; min-width:120px; }
157
+ .memx-textarea { min-height:72px; padding:8px 10px; width:100%; }
158
+ .memx-select, .memx-date { height:44px; padding:0 8px; }
159
+ @media (max-width:1023px) { .memx { padding:12px 12px calc(16px + env(safe-area-inset-bottom,0px)) !important; } .memx-layout { gap:12px; } }
160
+ @media (min-width:1440px) { .memx { max-width:1280px; margin:0 auto; } }
118
161
  `;
119
- // Global CSS for the top-level header tab badge (injected into <head> by the
120
- // boot-time effect). Reuses DSH theme tokens so it flips with light/dark; a
121
- // soft blink draws attention to pending reviews without being obnoxious.
122
162
  const HEADER_BADGE_CSS = `
123
- .dshmem-header-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; padding: 0 5px; margin-left: 6px; border-radius: 999px; background: var(--dsw-alias-state-error-primary, #d9534f); color: #fff; font-size: 11px; font-weight: 700; line-height: 1; vertical-align: middle; animation: dshmem-blink 1.2s ease-in-out infinite; }
124
- @keyframes dshmem-blink { 0%, 100% { opacity: 1; } 50% { opacity: .3; } }
163
+ .dshmem-header-badge { display:inline-flex; align-items:center; justify-content:center; min-width:16px; height:16px; padding:0 5px; margin-left:6px; border-radius:999px; background:var(--dsw-alias-state-error-primary,#d9534f); color:#fff; font-size:11px; font-weight:700; line-height:1; vertical-align:middle; animation:dshmem-blink 1.2s ease-in-out infinite; }
164
+ @keyframes dshmem-blink { 0%,100%{opacity:1} 50%{opacity:.3} }
125
165
  `;
126
- // Best-effort current-session cwd, derived from the injected `sessions` feed
127
- // (never `ctx.conversation` — that face is not injectable here, and touching a
128
- // non-injected service throws `cannot get property ... without inject`).
129
- // `ctx.sessions.list.getSnapshot()` returns `{ current, byId }`, and each row
130
- // carries the session's canonical `cwd`. Undefined on a fresh page / no session.
131
166
  function sessionCwd(ctx) {
132
167
  try {
133
168
  const snap = ctx?.sessions?.list?.getSnapshot?.();
@@ -153,12 +188,29 @@ function useRpc(ctx) {
153
188
  });
154
189
  }, [ctx]);
155
190
  }
191
+ // tiny inline icons (no emoji) — stroke currentColor
192
+ function Ico({ d, size = 16, label }) {
193
+ return React.createElement('span', { 'aria-hidden': label ? undefined : true, 'aria-label': label, style: { display: 'inline-flex', lineHeight: 0 } }, React.createElement('svg', { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.75, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true, focusable: 'false' }, React.createElement('path', { d })));
194
+ }
195
+ const ICO = {
196
+ mem: 'M4 6a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6z M8 8h4 M8 12h4 M8 16h4 M16 8h2 M16 12h2 M16 16h2',
197
+ inbox: 'M4 6h16v12H4z M4 6l8 7 8-7',
198
+ check: 'M9 12l2 2 4-4 M4 6h16v12H4z',
199
+ layers: 'M12 3l9 5-9 5-9-5 9-5z M3 12l9 5 9-5 M3 17l9 5 9-5',
200
+ activity: 'M3 12h4l3-8 4 16 3-8h4',
201
+ search: 'M11 18a7 7 0 1 1 0-14 7 7 0 0 1 0 14z M18 18l-3.5-3.5',
202
+ refresh: 'M21 12a9 9 0 1 1-2.6-6.4 M21 3v6h-6',
203
+ plus: 'M12 5v14 M5 12h14',
204
+ spark: 'M12 3l1.5 4.5L18 9l-4.5 1.5L12 15l-1.5-4.5L6 9l4.5-1.5z',
205
+ };
206
+ // ── Review ─────────────────────────────────────────────────────────
156
207
  function ReviewQueueView({ ctx, onPendingChange }) {
157
208
  const rpc = useRpc(ctx);
158
209
  const [entries, setEntries] = React.useState([]);
159
210
  const [loading, setLoading] = React.useState(true);
160
211
  const [edits, setEdits] = React.useState({});
161
212
  const [msg, setMsg] = React.useState('');
213
+ const [q, setQ] = React.useState('');
162
214
  const load = React.useCallback(async () => {
163
215
  setLoading(true);
164
216
  try {
@@ -174,59 +226,39 @@ function ReviewQueueView({ ctx, onPendingChange }) {
174
226
  setLoading(false);
175
227
  }
176
228
  }, [rpc, onPendingChange]);
177
- React.useEffect(() => {
178
- load();
179
- }, [load]);
229
+ React.useEffect(() => { load(); }, [load]);
180
230
  const decide = React.useCallback(async (action, index) => {
181
231
  setMsg('');
182
232
  const payload = { action, indices: [index] };
183
- if (action === 'approve' && edits[index] !== undefined && edits[index].trim() !== '') {
233
+ if (action === 'approve' && edits[index] !== undefined && edits[index].trim() !== '')
184
234
  payload.edits = { [String(index)]: edits[index] };
185
- }
186
235
  try {
187
236
  const res = await rpc('queue.decide', payload);
188
237
  if (res?.ok) {
189
238
  setMsg(res.lines ? res.lines.join('; ') : `${action} ok`);
190
239
  await load();
191
240
  }
192
- else {
241
+ else
193
242
  setMsg(`failed: ${res?.error ?? 'unknown'}`);
194
- }
195
243
  }
196
244
  catch (e) {
197
245
  setMsg(`error: ${e?.message ?? String(e)}`);
198
246
  }
199
247
  }, [rpc, edits, load]);
248
+ const filtered = React.useMemo(() => {
249
+ const t = q.trim().toLowerCase();
250
+ if (!t)
251
+ return entries;
252
+ return entries.filter((e) => `${e.target} ${e.content} ${e.reason}`.toLowerCase().includes(t));
253
+ }, [entries, q]);
200
254
  if (loading)
201
- return React.createElement('div', null, 'Loading queue…');
202
- if (entries.length === 0) {
203
- return React.createElement('div', null, React.createElement('div', { style: { opacity: 0.7, marginBottom: 8 } }, 'No pending suggestions'), msg ? React.createElement('div', { style: { color: STYLE.textSec } }, msg) : null, React.createElement('button', { onClick: load, style: { marginTop: 8 } }, 'Refresh'));
204
- }
205
- return React.createElement('div', null, React.createElement('div', { style: { marginBottom: 8, opacity: 0.7 } }, `${entries.length} pending`), ...entries.map((e, idx) => {
206
- const number = idx + 1;
207
- return React.createElement('div', {
208
- key: number,
209
- style: { border: STYLE.borderL1, borderRadius: 8, padding: 8, marginBottom: 8 },
210
- }, React.createElement('div', { style: { fontWeight: 600 } }, `#${number} [${e.target}]`), React.createElement('div', { style: { margin: '4px 0', whiteSpace: 'pre-wrap' } }, e.content), e.reason ? React.createElement('div', { style: { opacity: 0.7 } }, `Reason: ${e.reason}`) : null, React.createElement('textarea', {
211
- value: edits[number] ?? '',
212
- placeholder: 'Edit content before approve (optional)',
213
- onChange: (ev) => setEdits((prev) => ({ ...prev, [number]: ev.target.value })),
214
- style: { width: '100%', minHeight: 40, marginTop: 4 },
215
- }), React.createElement('div', { style: { display: 'flex', gap: 8, marginTop: 8 } }, React.createElement('button', {
216
- onClick: () => decide('approve', number),
217
- 'data-testid': `approve-${number}`,
218
- style: { background: STYLE.success, color: STYLE.onAccent, border: 0, borderRadius: 4, padding: '4px 8px', cursor: 'pointer' },
219
- }, 'Approve'), React.createElement('button', {
220
- onClick: () => decide('reject', number),
221
- 'data-testid': `reject-${number}`,
222
- style: { background: STYLE.error, color: STYLE.onAccent, border: 0, borderRadius: 4, padding: '4px 8px', cursor: 'pointer' },
223
- }, 'Reject'), React.createElement('button', {
224
- onClick: () => decide('archive', number),
225
- 'data-testid': `archive-${number}`,
226
- style: { background: STYLE.neutral, color: STYLE.text, border: STYLE.borderL1, borderRadius: 4, padding: '4px 8px', cursor: 'pointer' },
227
- }, 'Archive')));
228
- }), msg ? React.createElement('div', { style: { marginTop: 8, color: STYLE.text } }, msg) : null, React.createElement('button', { onClick: load, style: { marginTop: 8 } }, 'Refresh'));
255
+ return React.createElement('div', { className: 'memx-muted' }, 'Loading queue…');
256
+ return React.createElement('div', { className: 'memx-panel' }, React.createElement('div', { className: 'memx-toolbar' }, React.createElement('div', { className: 'memx-search', role: 'search', 'aria-label': 'Filter queue' }, React.createElement(Ico, { d: ICO.search }), React.createElement('input', { value: q, onChange: (e) => setQ(e.target.value), placeholder: 'Filter queue…', 'aria-label': 'Filter queue' })), React.createElement('button', { onClick: load, className: 'memx-btn memx-btn-ghost', 'aria-label': 'Refresh queue' }, React.createElement(Ico, { d: ICO.refresh }), ' Refresh'), React.createElement('span', { className: 'memx-muted' }, `${filtered.length}/${entries.length} pending`)), filtered.length === 0 ? React.createElement('div', { className: 'memx-empty' }, React.createElement('div', { style: { fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } }, entries.length === 0 ? 'No pending suggestions' : 'No results'), React.createElement('div', { className: 'memx-muted' }, entries.length === 0 ? 'Model proposals will appear here for your approval.' : 'Try a different filter.')) : React.createElement('div', { className: 'memx-grid' }, ...filtered.map((e) => {
257
+ const number = entries.indexOf(e) + 1;
258
+ return React.createElement('div', { key: number, className: 'memx-card' }, React.createElement('div', { className: 'memx-cardhead' }, React.createElement('span', { className: 'memx-chip' }, `#${number} · ${e.target}`), e.reason ? React.createElement('span', { className: 'memx-sm' }, e.reason.slice(0, 80)) : null), React.createElement('div', { className: 'memx-pre' }, e.content), e.reason ? React.createElement('div', { className: 'memx-sm' }, `Reason: ${e.reason}`) : null, React.createElement('textarea', { value: edits[number] ?? '', placeholder: 'Edit content before approve (optional)', onChange: (ev) => setEdits(prev => ({ ...prev, [number]: ev.target.value })), className: 'memx-textarea', 'aria-label': 'Edit before approve', rows: 2 }), React.createElement('div', { className: 'memx-actions' }, React.createElement('button', { onClick: () => decide('approve', number), 'data-testid': `approve-${number}`, className: 'memx-btn memx-btn-primary' }, 'Approve'), React.createElement('button', { onClick: () => decide('reject', number), 'data-testid': `reject-${number}`, className: 'memx-btn' }, 'Reject'), React.createElement('button', { onClick: () => decide('archive', number), 'data-testid': `archive-${number}`, className: 'memx-btn' }, 'Archive')));
259
+ })), msg ? React.createElement('div', { className: 'memx-muted', style: { whiteSpace: 'pre-wrap' } }, msg) : null);
229
260
  }
261
+ // ── Todos ──────────────────────────────────────────────────────────
230
262
  function TodosView({ ctx }) {
231
263
  const rpc = useRpc(ctx);
232
264
  const [items, setItems] = React.useState([]);
@@ -236,11 +268,13 @@ function TodosView({ ctx }) {
236
268
  const [showAll, setShowAll] = React.useState(false);
237
269
  const [showPast, setShowPast] = React.useState(false);
238
270
  const [showExpired, setShowExpired] = React.useState(false);
271
+ const [q, setQ] = React.useState('');
239
272
  const [draftContent, setDraftContent] = React.useState('');
240
273
  const [draftTarget, setDraftTarget] = React.useState('work');
241
274
  const [draftDue, setDraftDue] = React.useState('');
242
275
  const [draftQuadrant, setDraftQuadrant] = React.useState('');
243
276
  const [draftCat, setDraftCat] = React.useState('');
277
+ const [openAdd, setOpenAdd] = React.useState(true);
244
278
  const [editId, setEditId] = React.useState(null);
245
279
  const [editContent, setEditContent] = React.useState('');
246
280
  const [editDue, setEditDue] = React.useState('');
@@ -251,23 +285,10 @@ function TodosView({ ctx }) {
251
285
  setLoading(true);
252
286
  setMsg('');
253
287
  try {
254
- const rpcPayload = {
255
- target: target !== 'all' ? target : undefined,
256
- opts: {
257
- all: showAll,
258
- past: showPast,
259
- expired: showExpired,
260
- },
261
- };
262
- // For single target we need to pass correctly; todo.list expects target or targets
263
- const res = await rpc('todo.list', rpcPayload);
264
- const list = Array.isArray(res?.items) ? res.items : [];
265
- setItems(list);
288
+ const res = await rpc('todo.list', { target: target !== 'all' ? target : undefined, opts: { all: showAll, past: showPast, expired: showExpired } });
289
+ setItems(Array.isArray(res?.items) ? res.items : []);
266
290
  if (res?.hint)
267
291
  setMsg(res.hint);
268
- else if (res?.text) {
269
- // fallback: text contains items but we already have items
270
- }
271
292
  }
272
293
  catch (e) {
273
294
  setMsg(`load failed: ${e?.message ?? String(e)}`);
@@ -276,9 +297,13 @@ function TodosView({ ctx }) {
276
297
  setLoading(false);
277
298
  }
278
299
  }, [rpc, target, showAll, showPast, showExpired]);
279
- React.useEffect(() => {
280
- load();
281
- }, [load]);
300
+ React.useEffect(() => { load(); }, [load]);
301
+ const filtered = React.useMemo(() => {
302
+ const t = q.trim().toLowerCase();
303
+ if (!t)
304
+ return items;
305
+ return items.filter((it) => `${it.text} ${it.target} ${it.cat} ${it.status}`.toLowerCase().includes(t));
306
+ }, [items, q]);
282
307
  const addTodo = React.useCallback(async () => {
283
308
  const content = draftContent.trim();
284
309
  if (!content) {
@@ -287,14 +312,7 @@ function TodosView({ ctx }) {
287
312
  }
288
313
  setMsg('');
289
314
  try {
290
- const res = await rpc('todo.mutate', {
291
- action: 'add',
292
- target: draftTarget,
293
- content,
294
- due: draftDue || undefined,
295
- quadrant: draftQuadrant || undefined,
296
- cat: draftCat || undefined,
297
- });
315
+ const res = await rpc('todo.mutate', { action: 'add', target: draftTarget, content, due: draftDue || undefined, quadrant: draftQuadrant || undefined, cat: draftCat || undefined });
298
316
  if (res?.ok) {
299
317
  setDraftContent('');
300
318
  setDraftDue('');
@@ -303,9 +321,8 @@ function TodosView({ ctx }) {
303
321
  setMsg(`added (id: ${res.id})`);
304
322
  await load();
305
323
  }
306
- else {
324
+ else
307
325
  setMsg(`add failed: ${res?.message ?? res?.error ?? 'unknown'}`);
308
- }
309
326
  }
310
327
  catch (e) {
311
328
  setMsg(`error: ${e?.message ?? String(e)}`);
@@ -327,7 +344,6 @@ function TodosView({ ctx }) {
327
344
  }, [rpc, load]);
328
345
  const undoTodo = React.useCallback(async (item) => {
329
346
  try {
330
- // Re-open a done todo: set status back to pending (only if currently done).
331
347
  const res = await rpc('todo.mutate', { action: 'update', target: item.target, id: item.id, status: 'pending' });
332
348
  if (res?.ok) {
333
349
  setMsg(res.message ?? 'undone');
@@ -354,14 +370,7 @@ function TodosView({ ctx }) {
354
370
  setMsg(`error: ${e?.message ?? String(e)}`);
355
371
  }
356
372
  }, [rpc, load]);
357
- const startEdit = (item) => {
358
- setEditId(item.id);
359
- setEditContent(item.text ?? '');
360
- setEditDue(item.due ?? '');
361
- setEditQuadrant(item.quadrant ?? '');
362
- setEditStatus(item.status ?? 'pending');
363
- setEditCat(item.cat ?? '');
364
- };
373
+ const startEdit = (item) => { setEditId(item.id); setEditContent(item.text ?? ''); setEditDue(item.due ?? ''); setEditQuadrant(item.quadrant ?? ''); setEditStatus(item.status ?? 'pending'); setEditCat(item.cat ?? ''); };
365
374
  const saveEdit = React.useCallback(async (item) => {
366
375
  try {
367
376
  const patch = {};
@@ -375,12 +384,7 @@ function TodosView({ ctx }) {
375
384
  patch.status = editStatus;
376
385
  if (editCat !== (item.cat ?? ''))
377
386
  patch.cat = editCat || null;
378
- const res = await rpc('todo.mutate', {
379
- action: 'update',
380
- target: item.target,
381
- id: item.id,
382
- ...patch,
383
- });
387
+ const res = await rpc('todo.mutate', { action: 'update', target: item.target, id: item.id, ...patch });
384
388
  if (res?.ok) {
385
389
  setEditId(null);
386
390
  setMsg(res.message ?? 'updated');
@@ -393,124 +397,16 @@ function TodosView({ ctx }) {
393
397
  setMsg(`error: ${e?.message ?? String(e)}`);
394
398
  }
395
399
  }, [rpc, editContent, editDue, editQuadrant, editStatus, editCat, load]);
396
- return React.createElement('div', null, React.createElement('div', { style: { display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 8 } }, ['all', 'life', 'work', 'project', 'daily'].map((k) => React.createElement('button', {
397
- key: k,
398
- onClick: () => setTarget(k),
399
- 'data-testid': `todo-target-${k}`,
400
- style: {
401
- fontWeight: target === k ? 700 : 400,
402
- padding: '4px 8px',
403
- borderRadius: 6,
404
- border: target === k ? STYLE.borderL2 : STYLE.borderL1,
405
- background: target === k ? STYLE.active : STYLE.surface,
406
- color: STYLE.text,
407
- cursor: 'pointer',
408
- },
409
- }, k))), React.createElement('div', { style: { display: 'flex', gap: 12, marginBottom: 8, alignItems: 'center', flexWrap: 'wrap' } }, React.createElement('label', { style: { display: 'flex', gap: 4, alignItems: 'center' } }, React.createElement('input', {
410
- type: 'checkbox',
411
- checked: showAll,
412
- onChange: (e) => setShowAll(e.target.checked),
413
- 'data-testid': 'todo-all',
414
- }), 'all (no limit)'), React.createElement('label', { style: { display: 'flex', gap: 4, alignItems: 'center' } }, React.createElement('input', {
415
- type: 'checkbox',
416
- checked: showPast,
417
- onChange: (e) => setShowPast(e.target.checked),
418
- 'data-testid': 'todo-past',
419
- }), 'past'), React.createElement('label', { style: { display: 'flex', gap: 4, alignItems: 'center' } }, React.createElement('input', {
420
- type: 'checkbox',
421
- checked: showExpired,
422
- onChange: (e) => setShowExpired(e.target.checked),
423
- 'data-testid': 'todo-expired',
424
- }), 'expired'), React.createElement('button', { onClick: load, style: { padding: '4px 8px' }, 'data-testid': 'todo-refresh' }, 'Refresh')), React.createElement('div', { style: { border: STYLE.borderL1, borderRadius: 8, padding: 8, marginBottom: 12 } }, React.createElement('div', { style: { fontWeight: 600, marginBottom: 6 } }, 'Add todo'), React.createElement('textarea', {
425
- value: draftContent,
426
- placeholder: 'Todo content',
427
- onChange: (e) => setDraftContent(e.target.value),
428
- style: { width: '100%', minHeight: 40, marginBottom: 6 },
429
- 'data-testid': 'todo-add-content',
430
- }), React.createElement('div', { style: { display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 6 } }, React.createElement('select', {
431
- value: draftTarget,
432
- onChange: (e) => setDraftTarget(e.target.value),
433
- style: { padding: '4px' },
434
- 'data-testid': 'todo-add-target',
435
- }, ['life', 'work', 'project', 'daily'].map((t) => React.createElement('option', { key: t, value: t }, t))), React.createElement('select', {
436
- value: draftQuadrant,
437
- onChange: (e) => setDraftQuadrant(e.target.value),
438
- style: { padding: '4px' },
439
- 'data-testid': 'todo-add-quadrant',
440
- }, React.createElement('option', { value: '' }, 'quadrant'), React.createElement('option', { value: 'q1' }, 'q1'), React.createElement('option', { value: 'q2' }, 'q2'), React.createElement('option', { value: 'q3' }, 'q3'), React.createElement('option', { value: 'q4' }, 'q4')), React.createElement('input', {
441
- value: draftDue,
442
- placeholder: 'due YYYY-MM-DD',
443
- onChange: (e) => setDraftDue(e.target.value),
444
- style: { padding: '4px', width: 120 },
445
- 'data-testid': 'todo-add-due',
446
- }), React.createElement('input', {
447
- value: draftCat,
448
- placeholder: 'cat',
449
- onChange: (e) => setDraftCat(e.target.value),
450
- style: { padding: '4px', width: 80 },
451
- 'data-testid': 'todo-add-cat',
452
- })), React.createElement('button', {
453
- onClick: addTodo,
454
- style: { background: STYLE.success, color: STYLE.onAccent, border: 0, borderRadius: 4, padding: '6px 12px', cursor: 'pointer' },
455
- 'data-testid': 'todo-add-btn',
456
- }, 'Add')), msg ? React.createElement('div', { style: { color: STYLE.text, marginBottom: 8, whiteSpace: 'pre-wrap' } }, msg) : null, loading
457
- ? React.createElement('div', null, 'Loading todos…')
458
- : items.length === 0
459
- ? React.createElement('div', { style: { opacity: 0.7 } }, 'No todos (smart view: overdue / due today / current project / Q1-Q2, max 8). Try "all" or "past".')
460
- : React.createElement('div', null, React.createElement('div', { style: { opacity: 0.7, marginBottom: 6 } }, `${items.length} todos${!showAll && items.length === 8 ? ' (smart view limited to 8)' : ''}`), ...items.map((it) => React.createElement('div', { key: it.id, style: { border: STYLE.borderL1, borderRadius: 8, padding: 8, marginBottom: 8 } }, React.createElement('div', { style: { opacity: 0.7 } }, `[${it.target}]`, it.quadrant ? ` [${it.quadrant}]` : '', it.due ? ` [due:${it.due}]` : '', it.status !== 'pending' ? ` [${it.status}]` : '', it.cat ? ` [cat:${it.cat}]` : '', it.doneAt ? ` [done:${it.doneAt}]` : '', it.past ? ` [past ${it.day}]` : '', ` id:${it.id}`), editId === it.id
461
- ? React.createElement('div', null, React.createElement('textarea', {
462
- value: editContent,
463
- onChange: (e) => setEditContent(e.target.value),
464
- style: { width: '100%', minHeight: 40, marginTop: 4 },
465
- 'data-testid': `todo-edit-content-${it.id}`,
466
- }), React.createElement('div', { style: { display: 'flex', gap: 6, marginTop: 6, flexWrap: 'wrap' } }, React.createElement('select', {
467
- value: editQuadrant,
468
- onChange: (e) => setEditQuadrant(e.target.value),
469
- 'data-testid': `todo-edit-quadrant-${it.id}`,
470
- }, React.createElement('option', { value: '' }, 'no quadrant'), React.createElement('option', { value: 'q1' }, 'q1'), React.createElement('option', { value: 'q2' }, 'q2'), React.createElement('option', { value: 'q3' }, 'q3'), React.createElement('option', { value: 'q4' }, 'q4')), React.createElement('input', {
471
- value: editDue,
472
- placeholder: 'due YYYY-MM-DD',
473
- onChange: (e) => setEditDue(e.target.value),
474
- style: { width: 120 },
475
- 'data-testid': `todo-edit-due-${it.id}`,
476
- }), React.createElement('select', {
477
- value: editStatus,
478
- onChange: (e) => setEditStatus(e.target.value),
479
- 'data-testid': `todo-edit-status-${it.id}`,
480
- }, ['pending', 'doing', 'done', 'blocked', 'cancelled'].map((s) => React.createElement('option', { key: s, value: s }, s))), React.createElement('input', {
481
- value: editCat,
482
- placeholder: 'cat',
483
- onChange: (e) => setEditCat(e.target.value),
484
- style: { width: 80 },
485
- 'data-testid': `todo-edit-cat-${it.id}`,
486
- })), React.createElement('div', { style: { display: 'flex', gap: 8, marginTop: 8 } }, React.createElement('button', {
487
- onClick: () => saveEdit(it),
488
- style: { background: STYLE.success, color: STYLE.onAccent, border: 0, borderRadius: 4, padding: '4px 8px', cursor: 'pointer' },
489
- 'data-testid': `todo-save-${it.id}`,
490
- }, 'Save'), React.createElement('button', {
491
- onClick: () => setEditId(null),
492
- style: { background: STYLE.neutral, color: STYLE.text, border: STYLE.borderL1, borderRadius: 4, padding: '4px 8px', cursor: 'pointer' },
493
- 'data-testid': `todo-cancel-${it.id}`,
494
- }, 'Cancel')))
495
- : React.createElement('div', null, React.createElement('div', { style: { margin: '4px 0', whiteSpace: 'pre-wrap' } }, it.text), React.createElement('div', { style: { display: 'flex', gap: 6, marginTop: 8, flexWrap: 'wrap' } }, React.createElement('button', {
496
- onClick: () => (it.status === 'done' ? undoTodo(it) : doneTodo(it)),
497
- style: { background: it.status === 'done' ? STYLE.neutral : STYLE.success, color: it.status === 'done' ? STYLE.text : STYLE.onAccent, border: it.status === 'done' ? STYLE.borderL1 : 0, borderRadius: 4, padding: '4px 8px', cursor: 'pointer' },
498
- 'data-testid': `todo-done-${it.id}`,
499
- }, it.status === 'done' ? 'Undo' : 'Done'), React.createElement('button', {
500
- onClick: () => startEdit(it),
501
- style: { background: STYLE.brand, color: STYLE.onAccent, border: 0, borderRadius: 4, padding: '4px 8px', cursor: 'pointer' },
502
- 'data-testid': `todo-edit-${it.id}`,
503
- }, 'Edit'), React.createElement('button', {
504
- onClick: () => removeTodo(it),
505
- style: { background: STYLE.error, color: STYLE.onAccent, border: 0, borderRadius: 4, padding: '4px 8px', cursor: 'pointer' },
506
- 'data-testid': `todo-remove-${it.id}`,
507
- }, 'Remove')))))));
400
+ return React.createElement('div', { className: 'memx-panel' }, React.createElement('div', { className: 'memx-toolbar' }, React.createElement('div', { className: 'memx-pills' }, ['all', 'life', 'work', 'project', 'daily'].map(k => React.createElement('button', { key: k, onClick: () => setTarget(k), 'aria-pressed': target === k, className: 'memx-pill', 'data-testid': `todo-target-${k}` }, k))), React.createElement('div', { className: 'memx-search', role: 'search', 'aria-label': 'Filter todos' }, React.createElement(Ico, { d: ICO.search }), React.createElement('input', { value: q, onChange: (e) => setQ(e.target.value), placeholder: 'Filter todos…', 'aria-label': 'Filter todos' }))), React.createElement('div', { className: 'memx-toolbar' }, React.createElement('label', { style: { display: 'flex', gap: 6, alignItems: 'center' } }, React.createElement('input', { type: 'checkbox', checked: showAll, onChange: (e) => setShowAll(e.target.checked), 'data-testid': 'todo-all' }), 'all'), React.createElement('label', { style: { display: 'flex', gap: 6, alignItems: 'center' } }, React.createElement('input', { type: 'checkbox', checked: showPast, onChange: (e) => setShowPast(e.target.checked), 'data-testid': 'todo-past' }), 'past'), React.createElement('label', { style: { display: 'flex', gap: 6, alignItems: 'center' } }, React.createElement('input', { type: 'checkbox', checked: showExpired, onChange: (e) => setShowExpired(e.target.checked), 'data-testid': 'todo-expired' }), 'expired'), React.createElement('button', { onClick: load, className: 'memx-btn memx-btn-ghost', 'data-testid': 'todo-refresh' }, React.createElement(Ico, { d: ICO.refresh }), 'Refresh')), React.createElement('div', { className: 'memx-form' }, React.createElement('button', { onClick: () => setOpenAdd(v => !v), className: 'memx-btn memx-btn-ghost', style: { justifyContent: 'space-between' }, 'aria-expanded': openAdd }, React.createElement('span', { style: { display: 'flex', gap: 8, alignItems: 'center' } }, React.createElement(Ico, { d: ICO.plus }), 'Add todo'), React.createElement('span', { className: 'memx-muted' }, openAdd ? 'Hide' : 'Show')), openAdd ? React.createElement(React.Fragment, null, React.createElement('textarea', { value: draftContent, placeholder: 'Todo content', onChange: (e) => setDraftContent(e.target.value), className: 'memx-textarea', 'aria-label': 'Todo content', 'data-testid': 'todo-add-content' }), React.createElement('div', { className: 'memx-formrow' }, React.createElement('select', { value: draftTarget, onChange: (e) => setDraftTarget(e.target.value), className: 'memx-select', 'aria-label': 'Todo target', 'data-testid': 'todo-add-target' }, ['life', 'work', 'project', 'daily'].map(t => React.createElement('option', { key: t, value: t }, t))), React.createElement('select', { value: draftQuadrant, onChange: (e) => setDraftQuadrant(e.target.value), className: 'memx-select', 'aria-label': 'Quadrant', 'data-testid': 'todo-add-quadrant' }, React.createElement('option', { value: '' }, 'quadrant'), React.createElement('option', { value: 'q1' }, 'q1'), React.createElement('option', { value: 'q2' }, 'q2'), React.createElement('option', { value: 'q3' }, 'q3'), React.createElement('option', { value: 'q4' }, 'q4')), React.createElement('input', { value: draftDue, placeholder: 'due YYYY-MM-DD', onChange: (e) => setDraftDue(e.target.value), className: 'memx-select', 'aria-label': 'Due date YYYY-MM-DD', 'data-testid': 'todo-add-due' }), React.createElement('input', { value: draftCat, placeholder: 'cat', onChange: (e) => setDraftCat(e.target.value), className: 'memx-select', 'aria-label': 'Category', 'data-testid': 'todo-add-cat' })), React.createElement('button', { onClick: addTodo, className: 'memx-btn memx-btn-primary', 'data-testid': 'todo-add-btn' }, 'Add')) : null), msg ? React.createElement('div', { className: 'memx-muted', style: { whiteSpace: 'pre-wrap' } }, msg) : null, loading ? React.createElement('div', { className: 'memx-muted' }, 'Loading todos…') : filtered.length === 0 ? React.createElement('div', { className: 'memx-empty' }, 'No todos — try "all" or change filter.') :
401
+ React.createElement('div', null, React.createElement('div', { className: 'memx-muted', style: { marginBottom: 6 } }, `${filtered.length} todos${!showAll && filtered.length === 8 ? ' (smart view ≤8)' : ''}`), React.createElement('div', { className: 'memx-grid' }, ...filtered.map((it) => React.createElement('div', { key: it.id, className: 'memx-card' }, React.createElement('div', { style: { display: 'flex', gap: 6, flexWrap: 'wrap' } }, React.createElement('span', { className: 'memx-chip' }, it.target), it.quadrant ? React.createElement('span', { className: 'memx-chip' }, it.quadrant) : null, it.due ? React.createElement('span', { className: 'memx-chip' }, `due ${it.due}`) : null, it.status !== 'pending' ? React.createElement('span', { className: 'memx-chip' }, it.status) : null, it.cat ? React.createElement('span', { className: 'memx-chip' }, it.cat) : null), editId === it.id ? React.createElement('div', null, React.createElement('textarea', { value: editContent, onChange: (e) => setEditContent(e.target.value), className: 'memx-textarea', 'aria-label': 'Edit todo content', 'data-testid': `todo-edit-content-${it.id}` }), React.createElement('div', { className: 'memx-formrow', style: { marginTop: 8 } }, React.createElement('select', { value: editQuadrant, onChange: (e) => setEditQuadrant(e.target.value), className: 'memx-select', 'aria-label': 'Edit quadrant', 'data-testid': `todo-edit-quadrant-${it.id}` }, React.createElement('option', { value: '' }, 'no quadrant'), React.createElement('option', { value: 'q1' }, 'q1'), React.createElement('option', { value: 'q2' }, 'q2'), React.createElement('option', { value: 'q3' }, 'q3'), React.createElement('option', { value: 'q4' }, 'q4')), React.createElement('input', { value: editDue, placeholder: 'due YYYY-MM-DD', onChange: (e) => setEditDue(e.target.value), className: 'memx-select', 'aria-label': 'Edit due date', 'data-testid': `todo-edit-due-${it.id}` }), React.createElement('select', { value: editStatus, onChange: (e) => setEditStatus(e.target.value), className: 'memx-select', 'aria-label': 'Edit status', 'data-testid': `todo-edit-status-${it.id}` }, ['pending', 'doing', 'done', 'blocked', 'cancelled'].map(s => React.createElement('option', { key: s, value: s }, s))), React.createElement('input', { value: editCat, placeholder: 'cat', onChange: (e) => setEditCat(e.target.value), className: 'memx-select', 'aria-label': 'Edit category', 'data-testid': `todo-edit-cat-${it.id}` })), React.createElement('div', { className: 'memx-actions', style: { marginTop: 8 } }, React.createElement('button', { onClick: () => saveEdit(it), className: 'memx-btn memx-btn-primary', 'data-testid': `todo-save-${it.id}` }, 'Save'), React.createElement('button', { onClick: () => setEditId(null), className: 'memx-btn', 'data-testid': `todo-cancel-${it.id}` }, 'Cancel'))) : React.createElement(React.Fragment, null, React.createElement('div', { className: 'memx-pre' }, it.text), React.createElement('div', { className: 'memx-actions', style: { marginTop: 8 } }, React.createElement('button', { onClick: () => (it.status === 'done' ? undoTodo(it) : doneTodo(it)), className: it.status === 'done' ? 'memx-btn' : 'memx-btn memx-btn-primary', 'data-testid': `todo-done-${it.id}` }, it.status === 'done' ? 'Undo' : 'Done'), React.createElement('button', { onClick: () => startEdit(it), className: 'memx-btn', 'data-testid': `todo-edit-${it.id}` }, 'Edit'), React.createElement('button', { onClick: () => removeTodo(it), className: 'memx-btn', 'data-testid': `todo-remove-${it.id}` }, 'Remove'))), it.doneAt || it.past ? React.createElement('div', { className: 'memx-xs' }, [it.doneAt ? `done:${it.doneAt}` : '', it.past ? `past ${it.day}` : ''].filter(Boolean).join(' · ')) : null)))));
508
402
  }
403
+ // ── Skills ─────────────────────────────────────────────────────────
509
404
  function SkillsView({ ctx }) {
510
405
  const rpc = useRpc(ctx);
511
406
  const [entries, setEntries] = React.useState([]);
512
407
  const [loading, setLoading] = React.useState(true);
513
408
  const [msg, setMsg] = React.useState('');
409
+ const [q, setQ] = React.useState('');
514
410
  const load = React.useCallback(async () => {
515
411
  setLoading(true);
516
412
  setMsg('');
@@ -519,11 +415,10 @@ function SkillsView({ ctx }) {
519
415
  if (res?.ok) {
520
416
  setEntries(Array.isArray(res.entries) ? res.entries : []);
521
417
  if (res.entries.length === 0)
522
- setMsg('No skills found (maestro-skills not installed or empty)');
418
+ setMsg('No skills found');
523
419
  }
524
- else {
420
+ else
525
421
  setMsg(`load failed: ${res?.error ?? 'unknown'}`);
526
- }
527
422
  }
528
423
  catch (e) {
529
424
  setMsg(`error: ${e?.message ?? String(e)}`);
@@ -532,19 +427,19 @@ function SkillsView({ ctx }) {
532
427
  setLoading(false);
533
428
  }
534
429
  }, [rpc]);
535
- React.useEffect(() => {
536
- load();
537
- }, [load]);
430
+ React.useEffect(() => { load(); }, [load]);
431
+ const filtered = React.useMemo(() => {
432
+ const t = q.trim().toLowerCase();
433
+ if (!t)
434
+ return entries;
435
+ return entries.filter((e) => `${e.name} ${e.description} ${e.origin}`.toLowerCase().includes(t));
436
+ }, [entries, q]);
538
437
  if (loading)
539
- return React.createElement('div', null, 'Loading skills…');
540
- return React.createElement('div', null, React.createElement('div', { style: { opacity: 0.7, marginBottom: 8 } }, `${entries.length} skills (read-only browser, metadata/origin onlymaestro-skills discovery unchanged)`), entries.length === 0
541
- ? React.createElement('div', { style: { opacity: 0.7 } }, msg || 'No skills')
542
- : React.createElement('div', null, ...entries.map((e) => React.createElement('div', { key: e.name, style: { border: STYLE.borderL1, borderRadius: 8, padding: 8, marginBottom: 8 } }, React.createElement('div', { style: { fontWeight: 600 } }, e.name), React.createElement('div', { style: { opacity: 0.7 } }, `[${e.origin}] ${e.path}`), React.createElement('div', { style: { margin: '4px 0', whiteSpace: 'pre-wrap' } }, e.description), e.metadata && Object.keys(e.metadata).length > 0
543
- ? React.createElement('div', { style: { opacity: 0.6, marginTop: 4 } }, `metadata: ${Object.entries(e.metadata)
544
- .map(([k, v]) => `${k}=${String(v).slice(0, 60)}`)
545
- .join(', ')}`)
546
- : null))), msg ? React.createElement('div', { style: { marginTop: 8, color: STYLE.text } }, msg) : null, React.createElement('button', { onClick: load, style: { marginTop: 8, padding: '4px 8px' }, 'data-testid': 'skills-refresh' }, 'Refresh'), React.createElement('div', { style: { marginTop: 8, opacity: 0.6 } }, 'Read-only — model suggestions cannot change skills. Future edits, if approved, will require explicit user action + path containment.'));
438
+ return React.createElement('div', { className: 'memx-muted' }, 'Loading skills…');
439
+ return React.createElement('div', { className: 'memx-panel' }, React.createElement('div', { className: 'memx-toolbar' }, React.createElement('div', { className: 'memx-search', role: 'search', 'aria-label': 'Filter skills' }, React.createElement(Ico, { d: ICO.search }), React.createElement('input', { value: q, onChange: (e) => setQ(e.target.value), placeholder: 'Filter skills…', 'aria-label': 'Filter skills' })), React.createElement('button', { onClick: load, className: 'memx-btn memx-btn-ghost', 'data-testid': 'skills-refresh' }, React.createElement(Ico, { d: ICO.refresh }), 'Refresh')), React.createElement('div', { className: 'memx-muted' }, `${filtered.length}/${entries.length} skillsread-only, no mutation`), filtered.length === 0 ? React.createElement('div', { className: 'memx-empty' }, msg || 'No skills') :
440
+ React.createElement('div', { className: 'memx-grid memx-grid-2' }, ...filtered.map((e) => React.createElement('div', { key: e.name, className: 'memx-card' }, React.createElement('div', { className: 'memx-cardtitle', style: { display: 'flex', gap: 8, alignItems: 'center' } }, React.createElement(Ico, { d: ICO.layers, size: 14 }), e.name), React.createElement('div', { className: 'memx-sm' }, `[${e.origin}] ${e.path}`), React.createElement('div', { className: 'memx-pre-sm' }, e.description), e.metadata && Object.keys(e.metadata).length > 0 ? React.createElement('div', { className: 'memx-xs' }, `metadata: ${Object.entries(e.metadata).map(([k, v]) => `${k}=${String(v).slice(0, 40)}`).join(', ')}`) : null))), msg ? React.createElement('div', { className: 'memx-muted', style: { marginTop: 8 } }, msg) : null);
547
441
  }
442
+ // ── Memory List ────────────────────────────────────────────────────
548
443
  function MemoryListView({ ctx }) {
549
444
  const rpc = useRpc(ctx);
550
445
  const [track, setTrack] = React.useState('key');
@@ -552,6 +447,7 @@ function MemoryListView({ ctx }) {
552
447
  const [entries, setEntries] = React.useState([]);
553
448
  const [loading, setLoading] = React.useState(true);
554
449
  const [msg, setMsg] = React.useState('');
450
+ const [q, setQ] = React.useState('');
555
451
  const needsCwd = track === 'key' || track === 'project';
556
452
  const load = React.useCallback(async () => {
557
453
  setLoading(true);
@@ -559,7 +455,7 @@ function MemoryListView({ ctx }) {
559
455
  try {
560
456
  if (needsCwd && !cwd.trim()) {
561
457
  setEntries([]);
562
- setMsg('cwd required for the key/project track');
458
+ setMsg('cwd required for key/project');
563
459
  setLoading(false);
564
460
  return;
565
461
  }
@@ -573,28 +469,17 @@ function MemoryListView({ ctx }) {
573
469
  setLoading(false);
574
470
  }
575
471
  }, [rpc, track, cwd, needsCwd]);
576
- React.useEffect(() => {
577
- load();
578
- }, [load]);
579
- return React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 12 } }, React.createElement('div', { className: 'row', style: { alignItems: 'center', gap: 6 } }, React.createElement('span', { className: 'tracklabel' }, 'Track'), ['memory', 'user', 'key', 'project', 'daily'].map((k) => React.createElement('button', {
580
- key: k,
581
- onClick: () => setTrack(k),
582
- 'aria-pressed': track === k,
583
- className: track === k ? 'control pill pill-active' : 'control pill',
584
- 'data-testid': `mem-track-${k}`,
585
- }, k))), React.createElement('div', { className: 'row', style: { alignItems: 'center' } }, React.createElement('input', {
586
- value: cwd,
587
- disabled: !needsCwd,
588
- placeholder: needsCwd ? 'cwd (for key/project)' : 'cwd is not used for this track',
589
- onChange: (e) => setCwd(e.target.value),
590
- 'data-testid': 'mem-cwd',
591
- className: 'cwd',
592
- }), React.createElement('button', { onClick: load, className: 'control ghost', 'data-testid': 'mem-refresh' }, 'Refresh')), msg ? React.createElement('div', { className: 'muted' }, msg) : null, loading
593
- ? React.createElement('div', null, 'Loading memory…')
594
- : entries.length === 0
595
- ? React.createElement('div', { className: 'muted' }, '(no entries)')
596
- : React.createElement('div', null, React.createElement('div', { className: 'muted', style: { marginBottom: 6 } }, `${entries.length} entries`), ...entries.map((e, idx) => React.createElement('div', { key: idx, className: 'card', style: { marginBottom: 8 } }, React.createElement('div', { style: { whiteSpace: 'pre-wrap' } }, e)))));
472
+ React.useEffect(() => { load(); }, [load]);
473
+ const filtered = React.useMemo(() => {
474
+ const t = q.trim().toLowerCase();
475
+ if (!t)
476
+ return entries;
477
+ return entries.filter(e => e.toLowerCase().includes(t));
478
+ }, [entries, q]);
479
+ return React.createElement('div', { className: 'memx-panel' }, React.createElement('div', { className: 'memx-pills' }, ['memory', 'user', 'key', 'project', 'daily'].map(k => React.createElement('button', { key: k, onClick: () => setTrack(k), 'aria-pressed': track === k, className: 'memx-pill', 'data-testid': `mem-track-${k}` }, k))), React.createElement('div', { className: 'memx-toolbar' }, React.createElement('div', { className: 'memx-field' }, React.createElement('input', { value: cwd, disabled: !needsCwd, placeholder: needsCwd ? 'cwd (for key/project)' : 'cwd not used', onChange: (e) => setCwd(e.target.value), 'aria-label': 'CWD for key/project', 'data-testid': 'mem-cwd', className: 'memx-input' }), React.createElement('button', { onClick: load, className: 'memx-btn', 'data-testid': 'mem-refresh' }, React.createElement(Ico, { d: ICO.refresh, size: 14 }), 'Refresh')), React.createElement('div', { className: 'memx-search', role: 'search', 'aria-label': 'Filter entries', style: { maxWidth: 240 } }, React.createElement(Ico, { d: ICO.search }), React.createElement('input', { value: q, onChange: (e) => setQ(e.target.value), placeholder: 'Filter entries…', 'aria-label': 'Filter entries' }))), msg ? React.createElement('div', { className: 'memx-muted' }, msg) : null, loading ? React.createElement('div', { className: 'memx-muted' }, 'Loading memory…') : filtered.length === 0 ? React.createElement('div', { className: 'memx-empty' }, '(no entries)') :
480
+ React.createElement('div', null, React.createElement('div', { className: 'memx-muted', style: { marginBottom: 6 } }, `${filtered.length}/${entries.length} entries`), React.createElement('div', { className: 'memx-grid' }, ...filtered.map((e, idx) => React.createElement('div', { key: idx, className: 'memx-card' }, React.createElement('div', { className: 'memx-pre' }, e))))));
597
481
  }
482
+ // ── Health ─────────────────────────────────────────────────────────
598
483
  function HealthView({ ctx }) {
599
484
  const [health, setHealth] = React.useState(null);
600
485
  const [loading, setLoading] = React.useState(true);
@@ -623,15 +508,14 @@ function HealthView({ ctx }) {
623
508
  }, [ctx]);
624
509
  React.useEffect(() => { load(); }, [load]);
625
510
  if (loading)
626
- return React.createElement('div', null, 'Loading health…');
511
+ return React.createElement('div', { className: 'memx-muted' }, 'Loading health…');
627
512
  if (msg)
628
- return React.createElement('div', null, React.createElement('div', { style: { color: STYLE.error } }, msg), React.createElement('button', { onClick: load, style: { marginTop: 8 } }, 'Retry'));
513
+ return React.createElement('div', null, React.createElement('div', { style: { color: 'var(--dsw-alias-state-error-primary)' } }, msg), React.createElement('button', { onClick: load, className: 'memx-btn', style: { marginTop: 8 } }, 'Retry'));
629
514
  if (!health)
630
- return React.createElement('div', null, 'No data');
515
+ return React.createElement('div', { className: 'memx-muted' }, 'No data');
631
516
  const cov = health.project.coverage;
632
- const color = cov >= 90 ? STYLE.success : cov >= 50 ? '#e6a23c' : STYLE.error;
633
- return React.createElement('div', null, React.createElement('div', { style: { display: 'flex', gap: 12, flexWrap: 'wrap', marginBottom: 12 } }, React.createElement('div', { className: 'card', style: { flex: 1, minWidth: 140 } }, React.createElement('div', { className: 'muted' }, 'Project entries'), React.createElement('div', { style: { fontWeight: 700 } }, String(health.project.total)), React.createElement('div', { className: 'muted' }, `${health.project.withSummary} with summary`)), React.createElement('div', { className: 'card', style: { flex: 1, minWidth: 140 } }, React.createElement('div', { className: 'muted' }, 'Coverage'), React.createElement('div', { style: { fontWeight: 700, color } }, `${cov.toFixed(1)}%`), React.createElement('div', { className: 'muted' }, cov >= 90 ? 'PASS' : 'FAIL (<90%)')), React.createElement('div', { className: 'card', style: { flex: 1, minWidth: 140 } }, React.createElement('div', { className: 'muted' }, 'Daily last 7d'), React.createElement('div', { style: { fontWeight: 600 } }, health.daily.counts.join(' · '))), React.createElement('div', { className: 'card', style: { flex: 1, minWidth: 140 } }, React.createElement('div', { className: 'muted' }, 'Discipline'), React.createElement('div', { style: { fontWeight: 600 } }, '1.5 avg calls/session'), React.createElement('div', { className: 'muted' }, 'target 1+ (5/20 zero before fix)'))), health.fiveDim ? React.createElement('div', { className: 'card', style: { marginBottom: 12 } }, React.createElement('div', { className: 'muted', style: { marginBottom: 6 } }, `5-Dim Score (composite ${Number(health.fiveDim.composite).toFixed(1)} = min*0.4+mean*0.6)`), React.createElement('div', { style: { display: 'flex', gap: 8, flexWrap: 'wrap' } }, ['S', 'R', 'J', 'C', 'Safety'].map((k) => React.createElement('div', { key: k, className: 'card', style: { flex: 1, minWidth: 90, textAlign: 'center' } }, React.createElement('div', { className: 'muted' }, k), React.createElement('div', { style: { fontWeight: 700 } }, String(health.fiveDim[k])))), React.createElement('div', { className: 'card', style: { flex: 1, minWidth: 90, textAlign: 'center', background: STYLE.active } }, React.createElement('div', { className: 'muted' }, 'Composite'), React.createElement('div', { style: { fontWeight: 800 } }, String(health.fiveDim.composite))))) : null, React.createElement('div', { className: 'muted', style: { marginBottom: 6 } }, `Longest ${health.longest.length} entries`), ...health.longest.map((it, i) => React.createElement('div', { key: i, className: 'card', style: { marginBottom: 6, display: 'flex', gap: 8, alignItems: 'center', justifyContent: 'space-between' } }, React.createElement('div', { style: { flex: 1 } }, React.createElement('div', { className: 'muted' }, `${it.len} chars`), React.createElement('div', { style: { whiteSpace: 'pre-wrap' } }, it.preview)), React.createElement('button', {
634
- onClick: async () => {
517
+ const covColor = cov >= 90 ? 'var(--dsw-alias-state-success-primary)' : cov >= 50 ? 'var(--dsw-alias-state-warn-primary)' : 'var(--dsw-alias-state-error-primary)';
518
+ return React.createElement('div', { className: 'memx-panel' }, React.createElement('div', { className: 'memx-grid memx-grid-4' }, React.createElement('div', { className: 'memx-card memx-stat' }, React.createElement('div', { className: 'memx-kicker' }, 'Project entries'), React.createElement('div', { className: 'memx-statval' }, String(health.project.total)), React.createElement('div', { className: 'memx-sm' }, `${health.project.withSummary} with summary`)), React.createElement('div', { className: 'memx-card memx-stat' }, React.createElement('div', { className: 'memx-kicker' }, 'Coverage'), React.createElement('div', { className: 'memx-statval', style: { color: covColor } }, `${cov.toFixed(1)}%`), React.createElement('div', { className: 'memx-sm' }, cov >= 90 ? 'PASS' : 'FAIL (<90%)')), React.createElement('div', { className: 'memx-card memx-stat' }, React.createElement('div', { className: 'memx-kicker' }, 'Daily last 7d'), React.createElement('div', { style: { fontWeight: 600 } }, health.daily.counts.join(' · ')), React.createElement('div', { className: 'memx-sm' }, 'entries per day')), React.createElement('div', { className: 'memx-card memx-stat' }, React.createElement('div', { className: 'memx-kicker' }, 'Discipline'), React.createElement('div', { style: { fontWeight: 600 } }, '1.5 avg / session'), React.createElement('div', { className: 'memx-sm' }, 'target 1+'))), health.fiveDim ? React.createElement('div', { className: 'memx-card' }, React.createElement('div', { className: 'memx-muted', style: { marginBottom: 8 } }, `5-Dim Score (composite ${Number(health.fiveDim.composite).toFixed(1)} = min×0.4+mean×0.6)`), React.createElement('div', { className: 'memx-grid memx-grid-4' }, ['S', 'R', 'J', 'C', 'Safety'].map(k => React.createElement('div', { key: k, className: 'memx-card', style: { textAlign: 'center', background: 'var(--dsw-alias-bg-layer-2)' } }, React.createElement('div', { className: 'memx-kicker' }, k), React.createElement('div', { style: { fontWeight: 700 } }, String(health.fiveDim[k])))), React.createElement('div', { className: 'memx-card', style: { textAlign: 'center', background: 'var(--dsw-alias-interactive-bg-active)', borderColor: 'var(--dsw-alias-border-l2)' } }, React.createElement('div', { className: 'memx-kicker' }, 'Composite'), React.createElement('div', { style: { fontWeight: 800 } }, String(health.fiveDim.composite))))) : null, React.createElement('div', { className: 'memx-muted' }, `Longest ${health.longest.length} entries`), React.createElement('div', { className: 'memx-grid' }, ...health.longest.map((it, i) => React.createElement('div', { key: i, className: 'memx-card', style: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 12 } }, React.createElement('div', { style: { flex: 1, minWidth: 0 } }, React.createElement('div', { className: 'memx-xs' }, `${it.len} chars`), React.createElement('div', { className: 'memx-pre-sm' }, it.preview)), React.createElement('button', { onClick: async () => {
635
519
  try {
636
520
  const conn2 = ctx.connection ?? ctx.get?.('connection');
637
521
  if (!conn2?.rpc?.call)
@@ -643,11 +527,9 @@ function HealthView({ ctx }) {
643
527
  catch (e) {
644
528
  setMsg(`propose failed: ${e?.message ?? String(e)}`);
645
529
  }
646
- },
647
- style: { padding: '4px 8px', background: STYLE.brand, color: STYLE.onAccent, border: 0, borderRadius: 4, cursor: 'pointer', whiteSpace: 'nowrap' },
648
- 'data-testid': `health-propose-${i}`,
649
- }, 'Suggest as KEY'))), msg ? React.createElement('div', { style: { marginTop: 8, color: STYLE.textSec, whiteSpace: 'pre-wrap' } }, msg) : null, React.createElement('button', { onClick: load, style: { marginTop: 12, padding: '6px 12px' }, 'data-testid': 'health-refresh' }, 'Refresh'));
530
+ }, className: 'memx-btn memx-btn-primary', 'data-testid': `health-propose-${i}`, style: { whiteSpace: 'nowrap', flex: 'none' } }, 'Suggest as KEY')))), msg ? React.createElement('div', { className: 'memx-muted', style: { whiteSpace: 'pre-wrap' } }, msg) : null, React.createElement('button', { onClick: load, className: 'memx-btn', style: { marginTop: 4 }, 'data-testid': 'health-refresh' }, React.createElement(Ico, { d: ICO.refresh, size: 14 }), 'Refresh'));
650
531
  }
532
+ // ── Root ───────────────────────────────────────────────────────────
651
533
  function MemoryView({ ctx }) {
652
534
  const rpc = useRpc(ctx);
653
535
  const [tab, setTab] = React.useState('memory');
@@ -661,46 +543,28 @@ function MemoryView({ ctx }) {
661
543
  setPending(0);
662
544
  }
663
545
  }, [rpc]);
664
- React.useEffect(() => {
665
- refreshPending();
666
- }, [refreshPending, tab]);
667
- return React.createElement('div', { className: 'dshmem', style: { padding: 16, color: STYLE.text, display: 'flex', flexDirection: 'column' } }, React.createElement('style', null, MEM_CSS), React.createElement('div', { className: 'tabs', role: 'tablist' }, ['memory', 'review', 'todos', 'skills', 'health'].map((k) => React.createElement('button', {
668
- key: k,
669
- role: 'tab',
670
- 'aria-selected': tab === k,
671
- onClick: () => setTab(k),
672
- className: tab === k ? 'tab tab-active' : 'tab',
673
- 'data-testid': `tab-${k}`,
674
- }, k, k === 'review' && pending > 0
675
- ? React.createElement('span', { className: 'tabbadge', 'data-testid': 'tab-badge-review' }, String(pending))
676
- : null))), tab === 'memory'
677
- ? React.createElement(MemoryListView, { ctx })
678
- : tab === 'review'
679
- ? React.createElement(ReviewQueueView, { ctx, onPendingChange: setPending })
680
- : tab === 'todos'
681
- ? React.createElement(TodosView, { ctx })
682
- : tab === 'health'
683
- ? React.createElement(HealthView, { ctx })
684
- : React.createElement(SkillsView, { ctx }));
546
+ React.useEffect(() => { refreshPending(); }, [refreshPending, tab]);
547
+ const tabs = [
548
+ { id: 'memory', label: 'Memory', icon: ICO.mem },
549
+ { id: 'review', label: 'Queue', icon: ICO.inbox, count: pending },
550
+ { id: 'todos', label: 'Todos', icon: ICO.check },
551
+ { id: 'skills', label: 'Skills', icon: ICO.layers },
552
+ { id: 'health', label: 'Health', icon: ICO.activity },
553
+ ];
554
+ return React.createElement('div', { className: 'dshmem memx', style: { padding: '12px 12px 16px' } }, React.createElement('style', null, MEM_CSS), React.createElement('div', { className: 'memx-header' }, React.createElement('div', { className: 'memx-header-main' }, React.createElement('div', { className: 'memx-title' }, React.createElement(Ico, { d: ICO.spark, size: 18 }), 'Memory'), React.createElement('div', { className: 'memx-subtitle' }, 'Durable layered memory — five tracks, gated writes, todos & skills')), React.createElement('div', { className: 'memx-header-actions' }, React.createElement('button', { onClick: refreshPending, className: 'memx-iconbtn', 'aria-label': 'Refresh status', title: 'Refresh' }, React.createElement(Ico, { d: ICO.refresh, size: 16 })))), React.createElement('div', { className: 'memx-layout' }, React.createElement('div', { className: 'memx-nav', role: 'tablist', 'aria-label': 'Memory sections' }, ...tabs.map(t => React.createElement('button', {
555
+ key: t.id, role: 'tab', id: `tab-${t.id}`, 'aria-selected': tab === t.id, 'aria-controls': `panel-${t.id}`, onClick: () => setTab(t.id), className: 'memx-navitem', 'data-testid': `tab-${t.id}`,
556
+ }, React.createElement(Ico, { d: t.icon }), t.label, t.count !== undefined && t.count > 0 ? React.createElement('span', { className: 'memx-badge', 'data-testid': 'tab-badge-review' }, String(t.count)) : null, t.id === 'memory' ? React.createElement('span', { className: 'memx-navcount' }, '5 tracks') : null))), React.createElement('div', { className: 'memx-panel', role: 'tabpanel', id: `panel-${tab}`, 'aria-labelledby': `tab-${tab}` }, tab === 'memory' ? React.createElement(MemoryListView, { ctx }) :
557
+ tab === 'review' ? React.createElement(ReviewQueueView, { ctx, onPendingChange: setPending }) :
558
+ tab === 'todos' ? React.createElement(TodosView, { ctx }) :
559
+ tab === 'health' ? React.createElement(HealthView, { ctx }) :
560
+ React.createElement(SkillsView, { ctx }))));
685
561
  }
686
562
  function apply(ctx) {
687
563
  ctx.effect(() => {
688
- const dispose = ctx.slots.inject('conversation.view', () => ctx.slots.register({
689
- name: 'conversation.view',
690
- id: 'maestro-memory',
691
- order: 40,
692
- label: () => 'Memory',
693
- }, () => React.createElement(MemoryView, { ctx })));
694
- return () => {
695
- if (typeof dispose === 'function')
696
- dispose();
697
- };
564
+ const dispose = ctx.slots.inject('conversation.view', () => ctx.slots.register({ name: 'conversation.view', id: 'maestro-memory', order: 40, label: () => 'Memory' }, () => React.createElement(MemoryView, { ctx })));
565
+ return () => { if (typeof dispose === 'function')
566
+ dispose(); };
698
567
  }, 'maestro-memory: view');
699
- // Top-level header tab badge: pending-suggestion count, with a soft blink,
700
- // injected into the DSH shell header tab labeled "Memory". DSH projects
701
- // 'conversation.view' tabs as plain string labels (ViewTab.label), so there is
702
- // no React slot to render into — the plugin reaches the shell DOM directly
703
- // (same technique as the mobile nav plugin).
704
568
  ctx.effect(() => {
705
569
  const conn = ctx.connection ?? ctx.get?.('connection');
706
570
  if (!conn?.rpc?.call)
@@ -714,7 +578,7 @@ function apply(ctx) {
714
578
  return tab;
715
579
  }
716
580
  for (const tab of Array.from(tabs)) {
717
- if (tab.closest('.dshmem'))
581
+ if (tab.closest('.memx'))
718
582
  continue;
719
583
  if ((tab.textContent ?? '').trim() === 'Memory') {
720
584
  tab.setAttribute(VIEW_ATTR, '1');
@@ -733,7 +597,7 @@ function apply(ctx) {
733
597
  existing.remove();
734
598
  return;
735
599
  }
736
- if (existing && existing.textContent === String(count))
600
+ if (existing && (existing.textContent === String(count)))
737
601
  return;
738
602
  if (existing)
739
603
  existing.remove();
@@ -767,9 +631,7 @@ function apply(ctx) {
767
631
  }
768
632
  };
769
633
  void refresh();
770
- const timer = window.setInterval(() => {
771
- void refresh();
772
- }, 8000);
634
+ const timer = window.setInterval(() => { void refresh(); }, 8000);
773
635
  const observer = new MutationObserver(() => {
774
636
  if (disposed)
775
637
  return;
@@ -783,15 +645,8 @@ function apply(ctx) {
783
645
  renderBadge(0);
784
646
  });
785
647
  observer.observe(document.body, { childList: true, subtree: true });
786
- return () => {
787
- disposed = true;
788
- window.clearInterval(timer);
789
- observer.disconnect();
790
- styleEl?.remove();
791
- const tab = findMemoryTab();
792
- if (tab)
793
- tab.querySelector(`[${BADGE_ATTR}]`)?.remove();
794
- };
648
+ return () => { disposed = true; window.clearInterval(timer); observer.disconnect(); styleEl?.remove(); const tab = findMemoryTab(); if (tab)
649
+ tab.querySelector(`[${BADGE_ATTR}]`)?.remove(); };
795
650
  }, 'maestro-memory: header-badge');
796
651
  }
797
652
  };