@ddtcorex/dsh-maestro-memory 1.1.1 → 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/CHANGELOG.md +15 -0
- package/lib/client.js +220 -365
- package/lib/prompt/snapshot.d.ts.map +1 -1
- package/lib/prompt/snapshot.js +4 -1
- package/lib/prompt/snapshot.js.map +1 -1
- package/lib/types/client/index.d.ts +0 -4
- package/lib/types/client/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client/index.tsx +527 -1003
- package/src/host/prompt/snapshot.ts +4 -1
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
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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:
|
|
91
|
-
.
|
|
92
|
-
.
|
|
93
|
-
.
|
|
94
|
-
.
|
|
95
|
-
.
|
|
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
|
-
/*
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
.
|
|
101
|
-
.
|
|
102
|
-
.
|
|
103
|
-
.
|
|
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
|
-
/*
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
.
|
|
110
|
-
.
|
|
111
|
-
.
|
|
112
|
-
.
|
|
113
|
-
.
|
|
114
|
-
.
|
|
115
|
-
.
|
|
116
|
-
.
|
|
117
|
-
.
|
|
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:
|
|
124
|
-
@keyframes dshmem-blink { 0%,
|
|
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',
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
|
255
|
-
|
|
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
|
-
|
|
281
|
-
|
|
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',
|
|
397
|
-
key:
|
|
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
|
|
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
|
-
|
|
537
|
-
|
|
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',
|
|
540
|
-
return React.createElement('div',
|
|
541
|
-
|
|
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} skills — read-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
|
|
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
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
'
|
|
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',
|
|
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:
|
|
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',
|
|
515
|
+
return React.createElement('div', { className: 'memx-muted' }, 'No data');
|
|
631
516
|
const cov = health.project.coverage;
|
|
632
|
-
const
|
|
633
|
-
return React.createElement('div',
|
|
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
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
'
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
'data-testid': `tab-${
|
|
674
|
-
},
|
|
675
|
-
? React.createElement(
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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
|
-
|
|
690
|
-
|
|
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('.
|
|
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
|
-
|
|
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
|
};
|