@ddtcorex/dsh-maestro-memory 1.1.1 → 1.2.2

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,138 @@ 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
+ /* overlay isolation — Memory tab must fully cover chat behind (opaque + stacking) */
70
+ /* SlotOutlet [data-slot="conversation.view"] is display:contents by default (ui-slots/scoped-slots.tsx), so background/isolation on it is ignored. Override to block + paint. */
71
+ [class*="viewArea"] { background: var(--dsw-alias-bg-base) !important; isolation: isolate; contain: paint; position: relative; z-index: 1; flex: 1 1 auto; min-height: 100%; width: 100%; max-width: 100%; overflow: hidden; }
72
+ [data-slot="conversation.view"] { display: block !important; background: var(--dsw-alias-bg-base) !important; isolation: isolate; contain: paint; position: relative; z-index: 1; flex: 1 1 auto; min-height: 0; min-width: 0; width: 100%; max-width: 100%; pointer-events: auto; }
73
+ [data-slot="conversation.session"] { background: var(--dsw-alias-bg-base); isolation: isolate; contain: paint; }
74
+ [data-slot="conversation.view"] > .memx,
75
+ [class*="viewArea"] > .memx,
76
+ [class*="viewArea"] > [data-slot="conversation.view"] > .memx { flex: 1 1 auto; min-width: 0; max-width: 100%; }
77
+ /* legacy contract for tests — inheritance via .dshmem */
83
78
  .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); }
79
+ .dshmem button { font: inherit; cursor: pointer; transition: color .15s ease, background .15s ease, border-color .15s ease, opacity .15s ease; }
89
80
  .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); }
81
+ .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; }
82
+ .memx { color: var(--dsw-alias-label-primary); font-size: 13px; line-height: 1.5; background: var(--dsw-alias-bg-base) !important; position: relative; z-index: 2; isolation: isolate; contain: paint; min-height: 100%; width: 100%; max-width: 100%; box-sizing: border-box; align-self: stretch; flex: 1 1 auto; pointer-events: auto; overflow-x: clip; overflow-y: auto; overflow-wrap: break-word; }
83
+ .memx, .memx * { box-sizing: border-box; }
84
+ .memx button { font: inherit; cursor: pointer; transition: color .15s ease, background .15s ease, border-color .15s ease, opacity .15s ease; }
85
+ .memx button:active { transform: translateY(.5px); }
86
+ .memx button:focus-visible { outline: 2px solid var(--dsw-alias-interactive-bg-active); outline-offset: 1px; }
87
+ .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; }
88
+ .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); }
89
+ .memx textarea { resize: vertical; }
90
+ @media (prefers-reduced-motion: reduce) { .memx button, .memx-card, .dshmem-header-badge { transition: none !important; animation: none !important; } }
91
+
92
+ /* header — refresh ngang hàng title (yêu cầu) */
93
+ .memx-header { display:flex; align-items:center; gap:12px; flex-wrap:nowrap; margin-bottom:14px; }
94
+ .memx-title { font-size:16px; font-weight:700; letter-spacing:-.01em; display:flex; align-items:center; gap:8px; }
95
+ .memx-title svg { flex:none; }
96
+ .memx-subtitle { color: var(--dsw-alias-label-secondary); font-size:12px; margin-top:2px; }
97
+ .memx-header-main { flex:1 1 auto; min-width:0; }
98
+ .memx-header-actions { flex:0 0 auto; display:flex; gap:8px; align-items:center; margin-left:auto; }
99
+ .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; }
100
+ @media (max-width:1023px) { .memx-search { max-width:100%; flex-basis:100%; } }
101
+ .memx-search input { border:none; background:transparent; padding:0; flex:1; min-width:0; box-shadow:none !important; }
102
+ .memx-search:focus-within { border-color:var(--dsw-alias-border-l2); box-shadow:0 0 0 2px var(--dsw-alias-interactive-bg-active); }
103
+ .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); }
104
+ .memx-iconbtn:hover { color:var(--dsw-alias-label-primary); border-color:var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-layer-2); }
105
+ .memx-iconbtn[aria-busy="true"] { opacity:.6; pointer-events:none; }
106
+
107
+ /* layout — align breakpoint with dsh-maestro-mobile (1023px) */
108
+ .memx-layout { display:grid; gap:14px; width:100%; max-width:100%; min-width:0; }
109
+ @media (min-width: 1024px) { .memx-layout { grid-template-columns: 200px minmax(0,1fr); align-items:start; } }
110
+ /* nav */
111
+ .memx-nav { display:flex; gap:8px; overflow-x:auto; scrollbar-width:none; -webkit-overflow-scrolling:touch; padding-bottom:2px; }
112
+ .memx-nav::-webkit-scrollbar { display:none; }
113
+ @media (min-width: 1024px) { .memx-nav { flex-direction:column; position:sticky; top:8px; overflow:visible; gap:4px; } }
114
+ .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; }
115
+ .memx-navitem:hover { background:var(--dsw-alias-bg-layer-2); color:var(--dsw-alias-label-primary); border-color:var(--dsw-alias-border-l1); }
116
+ .memx-navitem[aria-selected="true"], .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; }
117
+ .memx-navitem svg { width:16px; height:16px; flex:none; }
118
+ .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; }
119
+ .memx-navcount { margin-left:auto; color:var(--dsw-alias-label-secondary); font-size:11px; }
96
120
 
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; }
121
+ /* content */
122
+ .memx-panel { min-width:0; max-width:100%; width:100%; display:flex; flex-direction:column; gap:12px; overflow:hidden; }
123
+ .memx-toolbar { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
124
+ .memx-pills { display:flex; gap:8px; flex-wrap:wrap; }
125
+ .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; }
126
+ .memx-pill:hover { color:var(--dsw-alias-label-primary); border-color:var(--dsw-alias-border-l2); }
127
+ .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); }
128
+ .memx-field { display:flex; gap:8px; align-items:center; flex-wrap:wrap; flex:1 1 100%; min-width:0; }
129
+ .memx-input { height:44px; padding:0 10px; flex:1 1 140px; min-width:0; }
130
+ @media (max-width:480px) { .memx-field { flex-direction:column; align-items:stretch; } .memx-input { flex-basis:auto; width:100%; } }
131
+ .memx-input:disabled { opacity:.5; cursor:not-allowed; }
132
+ .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; }
133
+ .memx-btn:hover { border-color:var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-layer-2); }
134
+ .memx-btn-primary { background:#06c; color:#fff; border-color:#06c; }
135
+ .memx-btn-primary:hover { background:#05a; border-color:#05a; color:#fff; }
136
+ .memx-btn-ghost { background:transparent; }
137
+ .memx-btn-danger { background:var(--dsw-alias-state-error-primary); color:#fff; border-color:transparent; }
138
+ .memx-btn:disabled { opacity:.5; cursor:not-allowed; }
104
139
 
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; }
140
+ /* bento / cards */
141
+ .memx-grid { display:grid; gap:10px; width:100%; max-width:100%; min-width:0; }
142
+ @media (min-width: 640px) { .memx-grid-2 { grid-template-columns: repeat(2, minmax(0,1fr)); } }
143
+ @media (min-width: 1024px) { .memx-grid-4 { grid-template-columns: repeat(4, minmax(0,1fr)); } }
144
+ .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; min-width:0; max-width:100%; overflow:hidden; }
145
+ .memx-card:hover { border-color:var(--dsw-alias-border-l2); }
146
+ .memx-cardhead { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
147
+ .memx-kicker { text-transform:uppercase; letter-spacing:.04em; font-size:11px; color:var(--dsw-alias-label-secondary); font-weight:600; }
148
+ .memx-cardtitle { font-weight:600; }
149
+ .memx-muted { color:var(--dsw-alias-label-secondary); }
150
+ .memx-sm { font-size:12px; color:var(--dsw-alias-label-secondary); }
151
+ .memx-xs { font-size:11px; color:var(--dsw-alias-label-secondary); }
152
+ .memx-pre { white-space:pre-wrap; word-break:break-word; }
153
+ .memx-pre-sm { white-space:pre-wrap; word-break:break-word; font-size:12px; }
154
+ .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); }
155
+ .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); }
156
+ .memx-stat { text-align:left; }
157
+ .memx-statval { font-size:20px; font-weight:800; letter-spacing:-.02em; }
158
+ .memx-actions { display:flex; gap:8px; flex-wrap:wrap; }
159
+ @media (max-width: 639px) { .memx-actions button { flex:1; min-width:0; } }
160
+
161
+ /* forms inline */
162
+ .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; min-width:0; max-width:100%; }
163
+ .memx-formrow { display:flex; gap:8px; flex-wrap:wrap; min-width:0; }
164
+ .memx-formrow > * { flex:1 1 0; min-width:120px; max-width:100%; }
165
+ @media (max-width:1023px) { .memx { padding:12px 12px calc(16px + env(safe-area-inset-bottom,0px)) !important; } .memx-layout { gap:12px; grid-template-columns:1fr !important; } .memx-formrow > *{ min-width:0; } }
166
+ @media (max-width:480px) { .memx-field { flex-direction:column; align-items:stretch; } .memx-input { flex-basis:auto; width:100%; max-width:100%; } }
167
+ @media (min-width:1440px) { .memx { max-width:1280px; margin:0 auto; } }
118
168
  `;
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
169
  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; } }
170
+ .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; }
171
+ @keyframes dshmem-blink { 0%,100%{opacity:1} 50%{opacity:.3} }
125
172
  `;
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
173
  function sessionCwd(ctx) {
132
174
  try {
133
175
  const snap = ctx?.sessions?.list?.getSnapshot?.();
@@ -153,12 +195,29 @@ function useRpc(ctx) {
153
195
  });
154
196
  }, [ctx]);
155
197
  }
198
+ // tiny inline icons (no emoji) — stroke currentColor
199
+ function Ico({ d, size = 16, label }) {
200
+ 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 })));
201
+ }
202
+ const ICO = {
203
+ 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',
204
+ inbox: 'M4 6h16v12H4z M4 6l8 7 8-7',
205
+ check: 'M9 12l2 2 4-4 M4 6h16v12H4z',
206
+ layers: 'M12 3l9 5-9 5-9-5 9-5z M3 12l9 5 9-5 M3 17l9 5 9-5',
207
+ activity: 'M3 12h4l3-8 4 16 3-8h4',
208
+ search: 'M11 18a7 7 0 1 1 0-14 7 7 0 0 1 0 14z M18 18l-3.5-3.5',
209
+ refresh: 'M21 12a9 9 0 1 1-2.6-6.4 M21 3v6h-6',
210
+ plus: 'M12 5v14 M5 12h14',
211
+ spark: 'M12 3l1.5 4.5L18 9l-4.5 1.5L12 15l-1.5-4.5L6 9l4.5-1.5z',
212
+ };
213
+ // ── Review ─────────────────────────────────────────────────────────
156
214
  function ReviewQueueView({ ctx, onPendingChange }) {
157
215
  const rpc = useRpc(ctx);
158
216
  const [entries, setEntries] = React.useState([]);
159
217
  const [loading, setLoading] = React.useState(true);
160
218
  const [edits, setEdits] = React.useState({});
161
219
  const [msg, setMsg] = React.useState('');
220
+ const [q, setQ] = React.useState('');
162
221
  const load = React.useCallback(async () => {
163
222
  setLoading(true);
164
223
  try {
@@ -174,59 +233,39 @@ function ReviewQueueView({ ctx, onPendingChange }) {
174
233
  setLoading(false);
175
234
  }
176
235
  }, [rpc, onPendingChange]);
177
- React.useEffect(() => {
178
- load();
179
- }, [load]);
236
+ React.useEffect(() => { load(); }, [load]);
180
237
  const decide = React.useCallback(async (action, index) => {
181
238
  setMsg('');
182
239
  const payload = { action, indices: [index] };
183
- if (action === 'approve' && edits[index] !== undefined && edits[index].trim() !== '') {
240
+ if (action === 'approve' && edits[index] !== undefined && edits[index].trim() !== '')
184
241
  payload.edits = { [String(index)]: edits[index] };
185
- }
186
242
  try {
187
243
  const res = await rpc('queue.decide', payload);
188
244
  if (res?.ok) {
189
245
  setMsg(res.lines ? res.lines.join('; ') : `${action} ok`);
190
246
  await load();
191
247
  }
192
- else {
248
+ else
193
249
  setMsg(`failed: ${res?.error ?? 'unknown'}`);
194
- }
195
250
  }
196
251
  catch (e) {
197
252
  setMsg(`error: ${e?.message ?? String(e)}`);
198
253
  }
199
254
  }, [rpc, edits, load]);
255
+ const filtered = React.useMemo(() => {
256
+ const t = q.trim().toLowerCase();
257
+ if (!t)
258
+ return entries;
259
+ return entries.filter((e) => `${e.target} ${e.content} ${e.reason}`.toLowerCase().includes(t));
260
+ }, [entries, q]);
200
261
  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'));
262
+ return React.createElement('div', { className: 'memx-muted' }, 'Loading queue…');
263
+ 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) => {
264
+ const number = entries.indexOf(e) + 1;
265
+ 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')));
266
+ })), msg ? React.createElement('div', { className: 'memx-muted', style: { whiteSpace: 'pre-wrap' } }, msg) : null);
229
267
  }
268
+ // ── Todos ──────────────────────────────────────────────────────────
230
269
  function TodosView({ ctx }) {
231
270
  const rpc = useRpc(ctx);
232
271
  const [items, setItems] = React.useState([]);
@@ -236,11 +275,13 @@ function TodosView({ ctx }) {
236
275
  const [showAll, setShowAll] = React.useState(false);
237
276
  const [showPast, setShowPast] = React.useState(false);
238
277
  const [showExpired, setShowExpired] = React.useState(false);
278
+ const [q, setQ] = React.useState('');
239
279
  const [draftContent, setDraftContent] = React.useState('');
240
280
  const [draftTarget, setDraftTarget] = React.useState('work');
241
281
  const [draftDue, setDraftDue] = React.useState('');
242
282
  const [draftQuadrant, setDraftQuadrant] = React.useState('');
243
283
  const [draftCat, setDraftCat] = React.useState('');
284
+ const [openAdd, setOpenAdd] = React.useState(true);
244
285
  const [editId, setEditId] = React.useState(null);
245
286
  const [editContent, setEditContent] = React.useState('');
246
287
  const [editDue, setEditDue] = React.useState('');
@@ -251,23 +292,10 @@ function TodosView({ ctx }) {
251
292
  setLoading(true);
252
293
  setMsg('');
253
294
  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);
295
+ const res = await rpc('todo.list', { target: target !== 'all' ? target : undefined, opts: { all: showAll, past: showPast, expired: showExpired } });
296
+ setItems(Array.isArray(res?.items) ? res.items : []);
266
297
  if (res?.hint)
267
298
  setMsg(res.hint);
268
- else if (res?.text) {
269
- // fallback: text contains items but we already have items
270
- }
271
299
  }
272
300
  catch (e) {
273
301
  setMsg(`load failed: ${e?.message ?? String(e)}`);
@@ -276,9 +304,13 @@ function TodosView({ ctx }) {
276
304
  setLoading(false);
277
305
  }
278
306
  }, [rpc, target, showAll, showPast, showExpired]);
279
- React.useEffect(() => {
280
- load();
281
- }, [load]);
307
+ React.useEffect(() => { load(); }, [load]);
308
+ const filtered = React.useMemo(() => {
309
+ const t = q.trim().toLowerCase();
310
+ if (!t)
311
+ return items;
312
+ return items.filter((it) => `${it.text} ${it.target} ${it.cat} ${it.status}`.toLowerCase().includes(t));
313
+ }, [items, q]);
282
314
  const addTodo = React.useCallback(async () => {
283
315
  const content = draftContent.trim();
284
316
  if (!content) {
@@ -287,14 +319,7 @@ function TodosView({ ctx }) {
287
319
  }
288
320
  setMsg('');
289
321
  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
- });
322
+ const res = await rpc('todo.mutate', { action: 'add', target: draftTarget, content, due: draftDue || undefined, quadrant: draftQuadrant || undefined, cat: draftCat || undefined });
298
323
  if (res?.ok) {
299
324
  setDraftContent('');
300
325
  setDraftDue('');
@@ -303,9 +328,8 @@ function TodosView({ ctx }) {
303
328
  setMsg(`added (id: ${res.id})`);
304
329
  await load();
305
330
  }
306
- else {
331
+ else
307
332
  setMsg(`add failed: ${res?.message ?? res?.error ?? 'unknown'}`);
308
- }
309
333
  }
310
334
  catch (e) {
311
335
  setMsg(`error: ${e?.message ?? String(e)}`);
@@ -327,7 +351,6 @@ function TodosView({ ctx }) {
327
351
  }, [rpc, load]);
328
352
  const undoTodo = React.useCallback(async (item) => {
329
353
  try {
330
- // Re-open a done todo: set status back to pending (only if currently done).
331
354
  const res = await rpc('todo.mutate', { action: 'update', target: item.target, id: item.id, status: 'pending' });
332
355
  if (res?.ok) {
333
356
  setMsg(res.message ?? 'undone');
@@ -354,14 +377,7 @@ function TodosView({ ctx }) {
354
377
  setMsg(`error: ${e?.message ?? String(e)}`);
355
378
  }
356
379
  }, [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
- };
380
+ const startEdit = (item) => { setEditId(item.id); setEditContent(item.text ?? ''); setEditDue(item.due ?? ''); setEditQuadrant(item.quadrant ?? ''); setEditStatus(item.status ?? 'pending'); setEditCat(item.cat ?? ''); };
365
381
  const saveEdit = React.useCallback(async (item) => {
366
382
  try {
367
383
  const patch = {};
@@ -375,12 +391,7 @@ function TodosView({ ctx }) {
375
391
  patch.status = editStatus;
376
392
  if (editCat !== (item.cat ?? ''))
377
393
  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
- });
394
+ const res = await rpc('todo.mutate', { action: 'update', target: item.target, id: item.id, ...patch });
384
395
  if (res?.ok) {
385
396
  setEditId(null);
386
397
  setMsg(res.message ?? 'updated');
@@ -393,124 +404,16 @@ function TodosView({ ctx }) {
393
404
  setMsg(`error: ${e?.message ?? String(e)}`);
394
405
  }
395
406
  }, [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')))))));
407
+ 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.') :
408
+ 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
409
  }
410
+ // ── Skills ─────────────────────────────────────────────────────────
509
411
  function SkillsView({ ctx }) {
510
412
  const rpc = useRpc(ctx);
511
413
  const [entries, setEntries] = React.useState([]);
512
414
  const [loading, setLoading] = React.useState(true);
513
415
  const [msg, setMsg] = React.useState('');
416
+ const [q, setQ] = React.useState('');
514
417
  const load = React.useCallback(async () => {
515
418
  setLoading(true);
516
419
  setMsg('');
@@ -519,11 +422,10 @@ function SkillsView({ ctx }) {
519
422
  if (res?.ok) {
520
423
  setEntries(Array.isArray(res.entries) ? res.entries : []);
521
424
  if (res.entries.length === 0)
522
- setMsg('No skills found (maestro-skills not installed or empty)');
425
+ setMsg('No skills found');
523
426
  }
524
- else {
427
+ else
525
428
  setMsg(`load failed: ${res?.error ?? 'unknown'}`);
526
- }
527
429
  }
528
430
  catch (e) {
529
431
  setMsg(`error: ${e?.message ?? String(e)}`);
@@ -532,19 +434,19 @@ function SkillsView({ ctx }) {
532
434
  setLoading(false);
533
435
  }
534
436
  }, [rpc]);
535
- React.useEffect(() => {
536
- load();
537
- }, [load]);
437
+ React.useEffect(() => { load(); }, [load]);
438
+ const filtered = React.useMemo(() => {
439
+ const t = q.trim().toLowerCase();
440
+ if (!t)
441
+ return entries;
442
+ return entries.filter((e) => `${e.name} ${e.description} ${e.origin}`.toLowerCase().includes(t));
443
+ }, [entries, q]);
538
444
  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.'));
445
+ return React.createElement('div', { className: 'memx-muted' }, 'Loading skills…');
446
+ 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') :
447
+ 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
448
  }
449
+ // ── Memory List ────────────────────────────────────────────────────
548
450
  function MemoryListView({ ctx }) {
549
451
  const rpc = useRpc(ctx);
550
452
  const [track, setTrack] = React.useState('key');
@@ -552,6 +454,7 @@ function MemoryListView({ ctx }) {
552
454
  const [entries, setEntries] = React.useState([]);
553
455
  const [loading, setLoading] = React.useState(true);
554
456
  const [msg, setMsg] = React.useState('');
457
+ const [q, setQ] = React.useState('');
555
458
  const needsCwd = track === 'key' || track === 'project';
556
459
  const load = React.useCallback(async () => {
557
460
  setLoading(true);
@@ -559,7 +462,7 @@ function MemoryListView({ ctx }) {
559
462
  try {
560
463
  if (needsCwd && !cwd.trim()) {
561
464
  setEntries([]);
562
- setMsg('cwd required for the key/project track');
465
+ setMsg('cwd required for key/project');
563
466
  setLoading(false);
564
467
  return;
565
468
  }
@@ -573,28 +476,17 @@ function MemoryListView({ ctx }) {
573
476
  setLoading(false);
574
477
  }
575
478
  }, [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)))));
479
+ React.useEffect(() => { load(); }, [load]);
480
+ const filtered = React.useMemo(() => {
481
+ const t = q.trim().toLowerCase();
482
+ if (!t)
483
+ return entries;
484
+ return entries.filter(e => e.toLowerCase().includes(t));
485
+ }, [entries, q]);
486
+ 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)') :
487
+ 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
488
  }
489
+ // ── Health ─────────────────────────────────────────────────────────
598
490
  function HealthView({ ctx }) {
599
491
  const [health, setHealth] = React.useState(null);
600
492
  const [loading, setLoading] = React.useState(true);
@@ -623,15 +515,14 @@ function HealthView({ ctx }) {
623
515
  }, [ctx]);
624
516
  React.useEffect(() => { load(); }, [load]);
625
517
  if (loading)
626
- return React.createElement('div', null, 'Loading health…');
518
+ return React.createElement('div', { className: 'memx-muted' }, 'Loading health…');
627
519
  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'));
520
+ 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
521
  if (!health)
630
- return React.createElement('div', null, 'No data');
522
+ return React.createElement('div', { className: 'memx-muted' }, 'No data');
631
523
  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 () => {
524
+ const covColor = cov >= 90 ? 'var(--dsw-alias-state-success-primary)' : cov >= 50 ? 'var(--dsw-alias-state-warn-primary)' : 'var(--dsw-alias-state-error-primary)';
525
+ 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
526
  try {
636
527
  const conn2 = ctx.connection ?? ctx.get?.('connection');
637
528
  if (!conn2?.rpc?.call)
@@ -643,11 +534,9 @@ function HealthView({ ctx }) {
643
534
  catch (e) {
644
535
  setMsg(`propose failed: ${e?.message ?? String(e)}`);
645
536
  }
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'));
537
+ }, 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
538
  }
539
+ // ── Root ───────────────────────────────────────────────────────────
651
540
  function MemoryView({ ctx }) {
652
541
  const rpc = useRpc(ctx);
653
542
  const [tab, setTab] = React.useState('memory');
@@ -661,46 +550,28 @@ function MemoryView({ ctx }) {
661
550
  setPending(0);
662
551
  }
663
552
  }, [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 }));
553
+ React.useEffect(() => { refreshPending(); }, [refreshPending, tab]);
554
+ const tabs = [
555
+ { id: 'memory', label: 'Memory', icon: ICO.mem },
556
+ { id: 'review', label: 'Queue', icon: ICO.inbox, count: pending },
557
+ { id: 'todos', label: 'Todos', icon: ICO.check },
558
+ { id: 'skills', label: 'Skills', icon: ICO.layers },
559
+ { id: 'health', label: 'Health', icon: ICO.activity },
560
+ ];
561
+ return React.createElement('div', { className: 'dshmem memx', style: { padding: '12px 12px 16px', background: 'var(--dsw-alias-bg-base)', position: 'relative', zIndex: 1, isolation: 'isolate', minHeight: '100%', width: '100%' } }, 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', {
562
+ 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}`,
563
+ }, 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 }) :
564
+ tab === 'review' ? React.createElement(ReviewQueueView, { ctx, onPendingChange: setPending }) :
565
+ tab === 'todos' ? React.createElement(TodosView, { ctx }) :
566
+ tab === 'health' ? React.createElement(HealthView, { ctx }) :
567
+ React.createElement(SkillsView, { ctx }))));
685
568
  }
686
569
  function apply(ctx) {
687
570
  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
- };
571
+ const dispose = ctx.slots.inject('conversation.view', () => ctx.slots.register({ name: 'conversation.view', id: 'maestro-memory', order: 40, label: () => 'Memory' }, () => React.createElement(MemoryView, { ctx })));
572
+ return () => { if (typeof dispose === 'function')
573
+ dispose(); };
698
574
  }, '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
575
  ctx.effect(() => {
705
576
  const conn = ctx.connection ?? ctx.get?.('connection');
706
577
  if (!conn?.rpc?.call)
@@ -714,7 +585,7 @@ function apply(ctx) {
714
585
  return tab;
715
586
  }
716
587
  for (const tab of Array.from(tabs)) {
717
- if (tab.closest('.dshmem'))
588
+ if (tab.closest('.memx'))
718
589
  continue;
719
590
  if ((tab.textContent ?? '').trim() === 'Memory') {
720
591
  tab.setAttribute(VIEW_ATTR, '1');
@@ -733,7 +604,7 @@ function apply(ctx) {
733
604
  existing.remove();
734
605
  return;
735
606
  }
736
- if (existing && existing.textContent === String(count))
607
+ if (existing && (existing.textContent === String(count)))
737
608
  return;
738
609
  if (existing)
739
610
  existing.remove();
@@ -767,9 +638,7 @@ function apply(ctx) {
767
638
  }
768
639
  };
769
640
  void refresh();
770
- const timer = window.setInterval(() => {
771
- void refresh();
772
- }, 8000);
641
+ const timer = window.setInterval(() => { void refresh(); }, 8000);
773
642
  const observer = new MutationObserver(() => {
774
643
  if (disposed)
775
644
  return;
@@ -783,15 +652,8 @@ function apply(ctx) {
783
652
  renderBadge(0);
784
653
  });
785
654
  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
- };
655
+ return () => { disposed = true; window.clearInterval(timer); observer.disconnect(); styleEl?.remove(); const tab = findMemoryTab(); if (tab)
656
+ tab.querySelector(`[${BADGE_ATTR}]`)?.remove(); };
795
657
  }, 'maestro-memory: header-badge');
796
658
  }
797
659
  };