@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/CHANGELOG.md +28 -0
- package/lib/client.js +227 -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 +534 -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,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
|
-
//
|
|
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
|
+
/* 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
|
-
|
|
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:
|
|
91
|
-
.
|
|
92
|
-
.
|
|
93
|
-
.
|
|
94
|
-
.
|
|
95
|
-
.
|
|
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
|
-
/*
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
.
|
|
101
|
-
.
|
|
102
|
-
.
|
|
103
|
-
.
|
|
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
|
-
/*
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
.
|
|
110
|
-
.
|
|
111
|
-
.
|
|
112
|
-
.
|
|
113
|
-
.
|
|
114
|
-
.
|
|
115
|
-
.
|
|
116
|
-
.
|
|
117
|
-
.
|
|
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:
|
|
124
|
-
@keyframes dshmem-blink { 0%,
|
|
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',
|
|
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'));
|
|
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
|
|
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);
|
|
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
|
-
|
|
281
|
-
|
|
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',
|
|
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')))))));
|
|
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
|
|
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
|
-
|
|
537
|
-
|
|
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',
|
|
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.'));
|
|
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} skills — read-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
|
|
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
|
-
|
|
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)))));
|
|
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',
|
|
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:
|
|
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',
|
|
522
|
+
return React.createElement('div', { className: 'memx-muted' }, 'No data');
|
|
631
523
|
const cov = health.project.coverage;
|
|
632
|
-
const
|
|
633
|
-
return React.createElement('div',
|
|
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
|
-
|
|
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 }));
|
|
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
|
-
|
|
690
|
-
|
|
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('.
|
|
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
|
-
|
|
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
|
};
|