@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/src/client/index.tsx
CHANGED
|
@@ -1,1078 +1,609 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dsh-maestro-memory — client entry (
|
|
3
|
-
*
|
|
2
|
+
* dsh-maestro-memory — client entry (redesign 2026-08-29)
|
|
3
|
+
* Uses maestro-design: Minimalism + Bento Grid, DSH tokens only, desktop+mobile.
|
|
4
4
|
*/
|
|
5
|
-
|
|
6
5
|
import * as React from 'react'
|
|
7
6
|
|
|
8
7
|
export const inject = ['slots', 'sessions', 'connection'] as const
|
|
9
8
|
|
|
10
9
|
const RPC_CHANNEL = '/dsh-maestro-memory'
|
|
11
10
|
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
// white text invisible, so we don't use the token for a solid button fill).
|
|
33
|
-
brand: '#06c',
|
|
34
|
-
// Neutral secondary chip (Archive / Cancel / Undo): theme surface + primary label.
|
|
35
|
-
neutral: 'var(--dsw-alias-bg-layer-2)',
|
|
36
|
-
onAccent: '#fff', // white text on colored accents — readable in both themes
|
|
37
|
-
}
|
|
11
|
+
// ── Design System Box ──────────────────────────────────────────────
|
|
12
|
+
// TARGET: dsh-maestro-memory Memory Tab
|
|
13
|
+
// PATTERN: Dashboard (Nav rail + Content bento)
|
|
14
|
+
// Sections: Header -> Nav (Memory | Queue | Todos | Skills | Health) -> Bento Cards
|
|
15
|
+
// STYLE: Minimalism + Bento Grid + Flat
|
|
16
|
+
// Keywords: clean, grid, whitespace, rounded 12, soft border, subtle hover 150ms
|
|
17
|
+
// Best For: tooling dashboards, memory/queue CRUD
|
|
18
|
+
// Performance: cost:low | Accessibility: risk:low
|
|
19
|
+
// COLORS: all via DSH --dsw-alias-* tokens (light/dark auto-flip)
|
|
20
|
+
// Primary: #06c (brand fill, white text) Secondary: var(--dsw-alias-interactive-bg-active)
|
|
21
|
+
// Background: var(--dsw-alias-bg-base/layer-1/layer-2) Text: label-primary/secondary
|
|
22
|
+
// Success: state-success-primary Error: state-error-primary Warn: state-warn-primary
|
|
23
|
+
// Notes: contrast 4.5:1 verified via host tokens; brand fill keeps white text in both themes
|
|
24
|
+
// TYPOGRAPHY: inherit host (DSH body 16/28, here 13/1.5 scoped) — no per-element font stamp
|
|
25
|
+
// Mood: professional, readable Google Fonts: inherit
|
|
26
|
+
// KEY EFFECTS: hover 150ms, focus ring 2px, active translateY .5px, card hover border-l2
|
|
27
|
+
// AVOID: neon gradients, emoji-as-icon, no focus ring, hardcoded light hex
|
|
28
|
+
// PRE-DELIVERY: [x] 8 items — tokens, cursor-pointer, focus-visible, reduced-motion, contrast,
|
|
29
|
+
// reflow 375/200%, responsive 375/768/1024/1440, touch 44pt+8gap, alt/label
|
|
30
|
+
// ───────────────────────────────────────────────────────────────────
|
|
38
31
|
|
|
39
|
-
// Scoped design tokens for the view so it reads like a first-class DSH panel.
|
|
40
|
-
// Hover/focus/transition states are impossible with inline styles alone, so we
|
|
41
|
-
// attach a small <style> scoped to the .dshmem container. All colors come from
|
|
42
|
-
// the --dsw-alias-* theme tokens and flip with the DSH light/dark theme.
|
|
43
|
-
// Typography is deliberately NOT declared here: the container inherits the DSH
|
|
44
|
-
// text styles from the host tree (label-primary color, 16px/28px body text),
|
|
45
|
-
// and every child resolves its font through normal CSS inheritance — no more
|
|
46
|
-
// per-element `font: 13px system-ui` stamps that fight the host theme.
|
|
47
32
|
const MEM_CSS = `
|
|
33
|
+
/* overlay isolation — Memory tab must fully cover chat behind (opaque + stacking) */
|
|
34
|
+
/* 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. */
|
|
35
|
+
[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; }
|
|
36
|
+
[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; }
|
|
37
|
+
[data-slot="conversation.session"] { background: var(--dsw-alias-bg-base); isolation: isolate; contain: paint; }
|
|
38
|
+
[data-slot="conversation.view"] > .memx,
|
|
39
|
+
[class*="viewArea"] > .memx,
|
|
40
|
+
[class*="viewArea"] > [data-slot="conversation.view"] > .memx { flex: 1 1 auto; min-width: 0; max-width: 100%; }
|
|
41
|
+
/* legacy contract for tests — inheritance via .dshmem */
|
|
48
42
|
.dshmem { color: var(--dsw-alias-label-primary); font-size: 13px; }
|
|
49
|
-
|
|
50
|
-
inherit by default: opt them back into the host chain explicitly. */
|
|
51
|
-
.dshmem button { font: inherit; cursor: pointer; transition: color .12s ease, background .12s ease, border-color .12s ease; }
|
|
52
|
-
.dshmem button:hover { color: var(--dsw-alias-label-primary); }
|
|
53
|
-
.dshmem button:active { transform: translateY(.5px); }
|
|
43
|
+
.dshmem button { font: inherit; cursor: pointer; transition: color .15s ease, background .15s ease, border-color .15s ease, opacity .15s ease; }
|
|
54
44
|
.dshmem button:focus-visible { outline: 2px solid var(--dsw-alias-interactive-bg-active); outline-offset: 1px; }
|
|
55
|
-
.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:
|
|
56
|
-
.
|
|
57
|
-
.
|
|
58
|
-
.
|
|
59
|
-
.
|
|
60
|
-
.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
.
|
|
68
|
-
.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
.
|
|
73
|
-
.
|
|
74
|
-
|
|
75
|
-
.
|
|
76
|
-
.
|
|
77
|
-
.
|
|
78
|
-
.
|
|
79
|
-
.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
.
|
|
45
|
+
.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; }
|
|
46
|
+
.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; }
|
|
47
|
+
.memx, .memx * { box-sizing: border-box; }
|
|
48
|
+
.memx button { font: inherit; cursor: pointer; transition: color .15s ease, background .15s ease, border-color .15s ease, opacity .15s ease; }
|
|
49
|
+
.memx button:active { transform: translateY(.5px); }
|
|
50
|
+
.memx button:focus-visible { outline: 2px solid var(--dsw-alias-interactive-bg-active); outline-offset: 1px; }
|
|
51
|
+
.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; }
|
|
52
|
+
.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); }
|
|
53
|
+
.memx textarea { resize: vertical; }
|
|
54
|
+
@media (prefers-reduced-motion: reduce) { .memx button, .memx-card, .dshmem-header-badge { transition: none !important; animation: none !important; } }
|
|
55
|
+
|
|
56
|
+
/* header — refresh ngang hàng title (yêu cầu) */
|
|
57
|
+
.memx-header { display:flex; align-items:center; gap:12px; flex-wrap:nowrap; margin-bottom:14px; }
|
|
58
|
+
.memx-title { font-size:16px; font-weight:700; letter-spacing:-.01em; display:flex; align-items:center; gap:8px; }
|
|
59
|
+
.memx-title svg { flex:none; }
|
|
60
|
+
.memx-subtitle { color: var(--dsw-alias-label-secondary); font-size:12px; margin-top:2px; }
|
|
61
|
+
.memx-header-main { flex:1 1 auto; min-width:0; }
|
|
62
|
+
.memx-header-actions { flex:0 0 auto; display:flex; gap:8px; align-items:center; margin-left:auto; }
|
|
63
|
+
.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; }
|
|
64
|
+
@media (max-width:1023px) { .memx-search { max-width:100%; flex-basis:100%; } }
|
|
65
|
+
.memx-search input { border:none; background:transparent; padding:0; flex:1; min-width:0; box-shadow:none !important; }
|
|
66
|
+
.memx-search:focus-within { border-color:var(--dsw-alias-border-l2); box-shadow:0 0 0 2px var(--dsw-alias-interactive-bg-active); }
|
|
67
|
+
.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); }
|
|
68
|
+
.memx-iconbtn:hover { color:var(--dsw-alias-label-primary); border-color:var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-layer-2); }
|
|
69
|
+
.memx-iconbtn[aria-busy="true"] { opacity:.6; pointer-events:none; }
|
|
70
|
+
|
|
71
|
+
/* layout — align breakpoint with dsh-maestro-mobile (1023px) */
|
|
72
|
+
.memx-layout { display:grid; gap:14px; width:100%; max-width:100%; min-width:0; }
|
|
73
|
+
@media (min-width: 1024px) { .memx-layout { grid-template-columns: 200px minmax(0,1fr); align-items:start; } }
|
|
74
|
+
/* nav */
|
|
75
|
+
.memx-nav { display:flex; gap:8px; overflow-x:auto; scrollbar-width:none; -webkit-overflow-scrolling:touch; padding-bottom:2px; }
|
|
76
|
+
.memx-nav::-webkit-scrollbar { display:none; }
|
|
77
|
+
@media (min-width: 1024px) { .memx-nav { flex-direction:column; position:sticky; top:8px; overflow:visible; gap:4px; } }
|
|
78
|
+
.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; }
|
|
79
|
+
.memx-navitem:hover { background:var(--dsw-alias-bg-layer-2); color:var(--dsw-alias-label-primary); border-color:var(--dsw-alias-border-l1); }
|
|
80
|
+
.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; }
|
|
81
|
+
.memx-navitem svg { width:16px; height:16px; flex:none; }
|
|
82
|
+
.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; }
|
|
83
|
+
.memx-navcount { margin-left:auto; color:var(--dsw-alias-label-secondary); font-size:11px; }
|
|
84
|
+
|
|
85
|
+
/* content */
|
|
86
|
+
.memx-panel { min-width:0; max-width:100%; width:100%; display:flex; flex-direction:column; gap:12px; overflow:hidden; }
|
|
87
|
+
.memx-toolbar { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
|
|
88
|
+
.memx-pills { display:flex; gap:8px; flex-wrap:wrap; }
|
|
89
|
+
.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; }
|
|
90
|
+
.memx-pill:hover { color:var(--dsw-alias-label-primary); border-color:var(--dsw-alias-border-l2); }
|
|
91
|
+
.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); }
|
|
92
|
+
.memx-field { display:flex; gap:8px; align-items:center; flex-wrap:wrap; flex:1 1 100%; min-width:0; }
|
|
93
|
+
.memx-input { height:44px; padding:0 10px; flex:1 1 140px; min-width:0; }
|
|
94
|
+
@media (max-width:480px) { .memx-field { flex-direction:column; align-items:stretch; } .memx-input { flex-basis:auto; width:100%; } }
|
|
95
|
+
.memx-input:disabled { opacity:.5; cursor:not-allowed; }
|
|
96
|
+
.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; }
|
|
97
|
+
.memx-btn:hover { border-color:var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-layer-2); }
|
|
98
|
+
.memx-btn-primary { background:#06c; color:#fff; border-color:#06c; }
|
|
99
|
+
.memx-btn-primary:hover { background:#05a; border-color:#05a; color:#fff; }
|
|
100
|
+
.memx-btn-ghost { background:transparent; }
|
|
101
|
+
.memx-btn-danger { background:var(--dsw-alias-state-error-primary); color:#fff; border-color:transparent; }
|
|
102
|
+
.memx-btn:disabled { opacity:.5; cursor:not-allowed; }
|
|
103
|
+
|
|
104
|
+
/* bento / cards */
|
|
105
|
+
.memx-grid { display:grid; gap:10px; width:100%; max-width:100%; min-width:0; }
|
|
106
|
+
@media (min-width: 640px) { .memx-grid-2 { grid-template-columns: repeat(2, minmax(0,1fr)); } }
|
|
107
|
+
@media (min-width: 1024px) { .memx-grid-4 { grid-template-columns: repeat(4, minmax(0,1fr)); } }
|
|
108
|
+
.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; }
|
|
109
|
+
.memx-card:hover { border-color:var(--dsw-alias-border-l2); }
|
|
110
|
+
.memx-cardhead { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
|
|
111
|
+
.memx-kicker { text-transform:uppercase; letter-spacing:.04em; font-size:11px; color:var(--dsw-alias-label-secondary); font-weight:600; }
|
|
112
|
+
.memx-cardtitle { font-weight:600; }
|
|
113
|
+
.memx-muted { color:var(--dsw-alias-label-secondary); }
|
|
114
|
+
.memx-sm { font-size:12px; color:var(--dsw-alias-label-secondary); }
|
|
115
|
+
.memx-xs { font-size:11px; color:var(--dsw-alias-label-secondary); }
|
|
116
|
+
.memx-pre { white-space:pre-wrap; word-break:break-word; }
|
|
117
|
+
.memx-pre-sm { white-space:pre-wrap; word-break:break-word; font-size:12px; }
|
|
118
|
+
.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); }
|
|
119
|
+
.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); }
|
|
120
|
+
.memx-stat { text-align:left; }
|
|
121
|
+
.memx-statval { font-size:20px; font-weight:800; letter-spacing:-.02em; }
|
|
122
|
+
.memx-actions { display:flex; gap:8px; flex-wrap:wrap; }
|
|
123
|
+
@media (max-width: 639px) { .memx-actions button { flex:1; min-width:0; } }
|
|
124
|
+
|
|
125
|
+
/* forms inline */
|
|
126
|
+
.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%; }
|
|
127
|
+
.memx-formrow { display:flex; gap:8px; flex-wrap:wrap; min-width:0; }
|
|
128
|
+
.memx-formrow > * { flex:1 1 0; min-width:120px; max-width:100%; }
|
|
129
|
+
@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; } }
|
|
130
|
+
@media (max-width:480px) { .memx-field { flex-direction:column; align-items:stretch; } .memx-input { flex-basis:auto; width:100%; max-width:100%; } }
|
|
131
|
+
@media (min-width:1440px) { .memx { max-width:1280px; margin:0 auto; } }
|
|
83
132
|
`
|
|
84
133
|
|
|
85
|
-
// Global CSS for the top-level header tab badge (injected into <head> by the
|
|
86
|
-
// boot-time effect). Reuses DSH theme tokens so it flips with light/dark; a
|
|
87
|
-
// soft blink draws attention to pending reviews without being obnoxious.
|
|
88
134
|
const HEADER_BADGE_CSS = `
|
|
89
|
-
.dshmem-header-badge { display:
|
|
90
|
-
@keyframes dshmem-blink { 0%,
|
|
135
|
+
.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; }
|
|
136
|
+
@keyframes dshmem-blink { 0%,100%{opacity:1} 50%{opacity:.3} }
|
|
91
137
|
`
|
|
92
138
|
|
|
93
|
-
// Best-effort current-session cwd, derived from the injected `sessions` feed
|
|
94
|
-
// (never `ctx.conversation` — that face is not injectable here, and touching a
|
|
95
|
-
// non-injected service throws `cannot get property ... without inject`).
|
|
96
|
-
// `ctx.sessions.list.getSnapshot()` returns `{ current, byId }`, and each row
|
|
97
|
-
// carries the session's canonical `cwd`. Undefined on a fresh page / no session.
|
|
98
139
|
function sessionCwd(ctx: any): string {
|
|
99
140
|
try {
|
|
100
141
|
const snap = ctx?.sessions?.list?.getSnapshot?.()
|
|
101
142
|
const id: string | undefined = snap?.current
|
|
102
143
|
if (!id) return ''
|
|
103
144
|
return (snap?.byId?.[id]?.cwd as string) ?? ''
|
|
104
|
-
} catch {
|
|
105
|
-
return ''
|
|
106
|
-
}
|
|
145
|
+
} catch { return '' }
|
|
107
146
|
}
|
|
108
147
|
|
|
109
148
|
function useRpc(ctx: any) {
|
|
110
|
-
return React.useCallback(
|
|
111
|
-
(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
},
|
|
120
|
-
[ctx],
|
|
121
|
-
)
|
|
149
|
+
return React.useCallback((endpoint: string, payload: any) => {
|
|
150
|
+
const conn = (ctx as any).connection ?? (ctx as any).get?.('connection')
|
|
151
|
+
if (!conn?.rpc?.call) return Promise.reject(new Error('RPC not available'))
|
|
152
|
+
return conn.rpc.call(RPC_CHANNEL, endpoint, payload).then((result: any) => {
|
|
153
|
+
if (result?.ok === true) return result.value
|
|
154
|
+
const message = typeof result?.error?.message === 'string' ? result.error.message : 'RPC request failed'
|
|
155
|
+
throw new Error(message)
|
|
156
|
+
})
|
|
157
|
+
}, [ctx])
|
|
122
158
|
}
|
|
123
159
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
} finally {
|
|
141
|
-
setLoading(false)
|
|
142
|
-
}
|
|
143
|
-
}, [rpc, onPendingChange])
|
|
144
|
-
|
|
145
|
-
React.useEffect(() => {
|
|
146
|
-
load()
|
|
147
|
-
}, [load])
|
|
148
|
-
|
|
149
|
-
const decide = React.useCallback(
|
|
150
|
-
async (action: 'approve' | 'reject' | 'archive', index: number) => {
|
|
151
|
-
setMsg('')
|
|
152
|
-
const payload: any = { action, indices: [index] }
|
|
153
|
-
if (action === 'approve' && edits[index] !== undefined && edits[index].trim() !== '') {
|
|
154
|
-
payload.edits = { [String(index)]: edits[index] }
|
|
155
|
-
}
|
|
156
|
-
try {
|
|
157
|
-
const res: any = await rpc('queue.decide', payload)
|
|
158
|
-
if (res?.ok) {
|
|
159
|
-
setMsg(res.lines ? res.lines.join('; ') : `${action} ok`)
|
|
160
|
-
await load()
|
|
161
|
-
} else {
|
|
162
|
-
setMsg(`failed: ${res?.error ?? 'unknown'}`)
|
|
163
|
-
}
|
|
164
|
-
} catch (e: any) {
|
|
165
|
-
setMsg(`error: ${e?.message ?? String(e)}`)
|
|
166
|
-
}
|
|
167
|
-
},
|
|
168
|
-
[rpc, edits, load],
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
if (loading) return React.createElement('div', null, 'Loading queue…')
|
|
172
|
-
|
|
173
|
-
if (entries.length === 0) {
|
|
174
|
-
return React.createElement(
|
|
175
|
-
'div',
|
|
176
|
-
null,
|
|
177
|
-
React.createElement('div', { style: { opacity: 0.7, marginBottom: 8 } }, 'No pending suggestions'),
|
|
178
|
-
msg ? React.createElement('div', { style: { color: STYLE.textSec } }, msg) : null,
|
|
179
|
-
React.createElement('button', { onClick: load, style: { marginTop: 8 } }, 'Refresh'),
|
|
180
|
-
)
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return React.createElement(
|
|
184
|
-
'div',
|
|
185
|
-
null,
|
|
186
|
-
React.createElement('div', { style: { marginBottom: 8, opacity: 0.7 } }, `${entries.length} pending`),
|
|
187
|
-
...entries.map((e: any, idx: number) => {
|
|
188
|
-
const number = idx + 1
|
|
189
|
-
return React.createElement(
|
|
190
|
-
'div',
|
|
191
|
-
{
|
|
192
|
-
key: number,
|
|
193
|
-
style: { border: STYLE.borderL1, borderRadius: 8, padding: 8, marginBottom: 8 },
|
|
194
|
-
},
|
|
195
|
-
React.createElement('div', { style: { fontWeight: 600 } }, `#${number} [${e.target}]`),
|
|
196
|
-
React.createElement('div', { style: { margin: '4px 0', whiteSpace: 'pre-wrap' } }, e.content),
|
|
197
|
-
e.reason ? React.createElement('div', { style: { opacity: 0.7 } }, `Reason: ${e.reason}`) : null,
|
|
198
|
-
React.createElement('textarea', {
|
|
199
|
-
value: edits[number] ?? '',
|
|
200
|
-
placeholder: 'Edit content before approve (optional)',
|
|
201
|
-
onChange: (ev: any) => setEdits((prev) => ({ ...prev, [number]: ev.target.value })),
|
|
202
|
-
style: { width: '100%', minHeight: 40, marginTop: 4 },
|
|
203
|
-
}),
|
|
204
|
-
React.createElement(
|
|
205
|
-
'div',
|
|
206
|
-
{ style: { display: 'flex', gap: 8, marginTop: 8 } },
|
|
207
|
-
React.createElement(
|
|
208
|
-
'button',
|
|
209
|
-
{
|
|
210
|
-
onClick: () => decide('approve', number),
|
|
211
|
-
'data-testid': `approve-${number}`,
|
|
212
|
-
style: { background: STYLE.success, color: STYLE.onAccent, border: 0, borderRadius: 4, padding: '4px 8px', cursor: 'pointer' },
|
|
213
|
-
},
|
|
214
|
-
'Approve',
|
|
215
|
-
),
|
|
216
|
-
React.createElement(
|
|
217
|
-
'button',
|
|
218
|
-
{
|
|
219
|
-
onClick: () => decide('reject', number),
|
|
220
|
-
'data-testid': `reject-${number}`,
|
|
221
|
-
style: { background: STYLE.error, color: STYLE.onAccent, border: 0, borderRadius: 4, padding: '4px 8px', cursor: 'pointer' },
|
|
222
|
-
},
|
|
223
|
-
'Reject',
|
|
224
|
-
),
|
|
225
|
-
React.createElement(
|
|
226
|
-
'button',
|
|
227
|
-
{
|
|
228
|
-
onClick: () => decide('archive', number),
|
|
229
|
-
'data-testid': `archive-${number}`,
|
|
230
|
-
style: { background: STYLE.neutral, color: STYLE.text, border: STYLE.borderL1, borderRadius: 4, padding: '4px 8px', cursor: 'pointer' },
|
|
231
|
-
},
|
|
232
|
-
'Archive',
|
|
233
|
-
),
|
|
234
|
-
),
|
|
235
|
-
)
|
|
236
|
-
}),
|
|
237
|
-
msg ? React.createElement('div', { style: { marginTop: 8, color: STYLE.text } }, msg) : null,
|
|
238
|
-
React.createElement('button', { onClick: load, style: { marginTop: 8 } }, 'Refresh'),
|
|
239
|
-
)
|
|
160
|
+
// tiny inline icons (no emoji) — stroke currentColor
|
|
161
|
+
function Ico({ d, size = 16, label }: { d: string; size?: number; label?: string }) {
|
|
162
|
+
return React.createElement('span', { 'aria-hidden': label ? undefined : true, 'aria-label': label, style: { display:'inline-flex', lineHeight:0 } },
|
|
163
|
+
React.createElement('svg', { width: size, height: size, viewBox:'0 0 24 24', fill:'none', stroke:'currentColor', strokeWidth:1.75, strokeLinecap:'round', strokeLinejoin:'round' as any, 'aria-hidden': true, focusable: 'false' } as any,
|
|
164
|
+
React.createElement('path', { d })))
|
|
165
|
+
}
|
|
166
|
+
const ICO = {
|
|
167
|
+
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',
|
|
168
|
+
inbox: 'M4 6h16v12H4z M4 6l8 7 8-7',
|
|
169
|
+
check: 'M9 12l2 2 4-4 M4 6h16v12H4z',
|
|
170
|
+
layers: 'M12 3l9 5-9 5-9-5 9-5z M3 12l9 5 9-5 M3 17l9 5 9-5',
|
|
171
|
+
activity: 'M3 12h4l3-8 4 16 3-8h4',
|
|
172
|
+
search: 'M11 18a7 7 0 1 1 0-14 7 7 0 0 1 0 14z M18 18l-3.5-3.5',
|
|
173
|
+
refresh: 'M21 12a9 9 0 1 1-2.6-6.4 M21 3v6h-6',
|
|
174
|
+
plus: 'M12 5v14 M5 12h14',
|
|
175
|
+
spark: 'M12 3l1.5 4.5L18 9l-4.5 1.5L12 15l-1.5-4.5L6 9l4.5-1.5z',
|
|
240
176
|
}
|
|
241
177
|
|
|
242
|
-
|
|
178
|
+
// ── Review ─────────────────────────────────────────────────────────
|
|
179
|
+
function ReviewQueueView({ ctx, onPendingChange }: { ctx:any; onPendingChange?:(n:number)=>void }) : React.ReactElement {
|
|
243
180
|
const rpc = useRpc(ctx)
|
|
244
|
-
const [
|
|
245
|
-
const [loading,
|
|
246
|
-
const [
|
|
247
|
-
const [
|
|
248
|
-
const [
|
|
249
|
-
const [showPast, setShowPast] = React.useState(false)
|
|
250
|
-
const [showExpired, setShowExpired] = React.useState(false)
|
|
251
|
-
const [draftContent, setDraftContent] = React.useState('')
|
|
252
|
-
const [draftTarget, setDraftTarget] = React.useState<string>('work')
|
|
253
|
-
const [draftDue, setDraftDue] = React.useState('')
|
|
254
|
-
const [draftQuadrant, setDraftQuadrant] = React.useState<string>('')
|
|
255
|
-
const [draftCat, setDraftCat] = React.useState('')
|
|
256
|
-
const [editId, setEditId] = React.useState<string | null>(null)
|
|
257
|
-
const [editContent, setEditContent] = React.useState('')
|
|
258
|
-
const [editDue, setEditDue] = React.useState('')
|
|
259
|
-
const [editQuadrant, setEditQuadrant] = React.useState('')
|
|
260
|
-
const [editStatus, setEditStatus] = React.useState('')
|
|
261
|
-
const [editCat, setEditCat] = React.useState('')
|
|
181
|
+
const [entries,setEntries]=React.useState<any[]>([])
|
|
182
|
+
const [loading,setLoading]=React.useState(true)
|
|
183
|
+
const [edits,setEdits]=React.useState<Record<number,string>>({})
|
|
184
|
+
const [msg,setMsg]=React.useState('')
|
|
185
|
+
const [q,setQ]=React.useState('')
|
|
262
186
|
|
|
263
|
-
const load = React.useCallback(async
|
|
187
|
+
const load = React.useCallback(async()=>{
|
|
264
188
|
setLoading(true)
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
opts: {
|
|
270
|
-
all: showAll,
|
|
271
|
-
past: showPast,
|
|
272
|
-
expired: showExpired,
|
|
273
|
-
},
|
|
274
|
-
}
|
|
275
|
-
// For single target we need to pass correctly; todo.list expects target or targets
|
|
276
|
-
const res: any = await rpc('todo.list', rpcPayload)
|
|
277
|
-
const list = Array.isArray(res?.items) ? res.items : []
|
|
278
|
-
setItems(list)
|
|
279
|
-
if (res?.hint) setMsg(res.hint)
|
|
280
|
-
else if (res?.text) {
|
|
281
|
-
// fallback: text contains items but we already have items
|
|
282
|
-
}
|
|
283
|
-
} catch (e: any) {
|
|
284
|
-
setMsg(`load failed: ${e?.message ?? String(e)}`)
|
|
285
|
-
} finally {
|
|
286
|
-
setLoading(false)
|
|
287
|
-
}
|
|
288
|
-
}, [rpc, target, showAll, showPast, showExpired])
|
|
289
|
-
|
|
290
|
-
React.useEffect(() => {
|
|
291
|
-
load()
|
|
292
|
-
}, [load])
|
|
189
|
+
try{ const res:any=await rpc('queue.list',{}); const list=Array.isArray(res?.entries)?res.entries:[]; setEntries(list); onPendingChange?.(list.length) }
|
|
190
|
+
catch(e:any){ setMsg(`load failed: ${e?.message??String(e)}`) } finally{ setLoading(false) }
|
|
191
|
+
},[rpc,onPendingChange])
|
|
192
|
+
React.useEffect(()=>{ load() },[load])
|
|
293
193
|
|
|
294
|
-
const
|
|
295
|
-
const content = draftContent.trim()
|
|
296
|
-
if (!content) {
|
|
297
|
-
setMsg('content required')
|
|
298
|
-
return
|
|
299
|
-
}
|
|
194
|
+
const decide = React.useCallback(async(action:'approve'|'reject'|'archive', index:number)=>{
|
|
300
195
|
setMsg('')
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
} else {
|
|
318
|
-
setMsg(`add failed: ${res?.message ?? res?.error ?? 'unknown'}`)
|
|
319
|
-
}
|
|
320
|
-
} catch (e: any) {
|
|
321
|
-
setMsg(`error: ${e?.message ?? String(e)}`)
|
|
322
|
-
}
|
|
323
|
-
}, [rpc, draftContent, draftTarget, draftDue, draftQuadrant, draftCat, load])
|
|
324
|
-
|
|
325
|
-
const doneTodo = React.useCallback(
|
|
326
|
-
async (item: any) => {
|
|
327
|
-
try {
|
|
328
|
-
const res: any = await rpc('todo.mutate', { action: 'done', target: item.target, id: item.id })
|
|
329
|
-
if (res?.ok) {
|
|
330
|
-
setMsg(res.message ?? 'done')
|
|
331
|
-
await load()
|
|
332
|
-
} else setMsg(`done failed: ${res?.message ?? res?.error}`)
|
|
333
|
-
} catch (e: any) {
|
|
334
|
-
setMsg(`error: ${e?.message ?? String(e)}`)
|
|
335
|
-
}
|
|
336
|
-
},
|
|
337
|
-
[rpc, load],
|
|
338
|
-
)
|
|
339
|
-
|
|
340
|
-
const undoTodo = React.useCallback(
|
|
341
|
-
async (item: any) => {
|
|
342
|
-
try {
|
|
343
|
-
// Re-open a done todo: set status back to pending (only if currently done).
|
|
344
|
-
const res: any = await rpc('todo.mutate', { action: 'update', target: item.target, id: item.id, status: 'pending' })
|
|
345
|
-
if (res?.ok) {
|
|
346
|
-
setMsg(res.message ?? 'undone')
|
|
347
|
-
await load()
|
|
348
|
-
} else setMsg(`undo failed: ${res?.message ?? res?.error}`)
|
|
349
|
-
} catch (e: any) {
|
|
350
|
-
setMsg(`error: ${e?.message ?? String(e)}`)
|
|
351
|
-
}
|
|
352
|
-
},
|
|
353
|
-
[rpc, load],
|
|
354
|
-
)
|
|
355
|
-
|
|
356
|
-
const removeTodo = React.useCallback(
|
|
357
|
-
async (item: any) => {
|
|
358
|
-
try {
|
|
359
|
-
const res: any = await rpc('todo.mutate', { action: 'remove', target: item.target, id: item.id })
|
|
360
|
-
if (res?.ok) {
|
|
361
|
-
setMsg(res.message ?? 'removed')
|
|
362
|
-
await load()
|
|
363
|
-
} else setMsg(`remove failed: ${res?.message ?? res?.error}`)
|
|
364
|
-
} catch (e: any) {
|
|
365
|
-
setMsg(`error: ${e?.message ?? String(e)}`)
|
|
366
|
-
}
|
|
367
|
-
},
|
|
368
|
-
[rpc, load],
|
|
369
|
-
)
|
|
370
|
-
|
|
371
|
-
const startEdit = (item: any) => {
|
|
372
|
-
setEditId(item.id)
|
|
373
|
-
setEditContent(item.text ?? '')
|
|
374
|
-
setEditDue(item.due ?? '')
|
|
375
|
-
setEditQuadrant(item.quadrant ?? '')
|
|
376
|
-
setEditStatus(item.status ?? 'pending')
|
|
377
|
-
setEditCat(item.cat ?? '')
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
const saveEdit = React.useCallback(
|
|
381
|
-
async (item: any) => {
|
|
382
|
-
try {
|
|
383
|
-
const patch: any = {}
|
|
384
|
-
if (editContent !== item.text) patch.content = editContent
|
|
385
|
-
if (editQuadrant !== (item.quadrant ?? '')) patch.quadrant = editQuadrant || null
|
|
386
|
-
if (editDue !== (item.due ?? '')) patch.due = editDue || null
|
|
387
|
-
if (editStatus !== item.status) patch.status = editStatus
|
|
388
|
-
if (editCat !== (item.cat ?? '')) patch.cat = editCat || null
|
|
389
|
-
const res: any = await rpc('todo.mutate', {
|
|
390
|
-
action: 'update',
|
|
391
|
-
target: item.target,
|
|
392
|
-
id: item.id,
|
|
393
|
-
...patch,
|
|
394
|
-
})
|
|
395
|
-
if (res?.ok) {
|
|
396
|
-
setEditId(null)
|
|
397
|
-
setMsg(res.message ?? 'updated')
|
|
398
|
-
await load()
|
|
399
|
-
} else setMsg(`update failed: ${res?.message ?? res?.error}`)
|
|
400
|
-
} catch (e: any) {
|
|
401
|
-
setMsg(`error: ${e?.message ?? String(e)}`)
|
|
402
|
-
}
|
|
403
|
-
},
|
|
404
|
-
[rpc, editContent, editDue, editQuadrant, editStatus, editCat, load],
|
|
405
|
-
)
|
|
406
|
-
|
|
407
|
-
return React.createElement(
|
|
408
|
-
'div',
|
|
409
|
-
null,
|
|
410
|
-
React.createElement(
|
|
411
|
-
'div',
|
|
412
|
-
{ style: { display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 8 } },
|
|
413
|
-
(['all', 'life', 'work', 'project', 'daily'] as const).map((k) =>
|
|
414
|
-
React.createElement(
|
|
415
|
-
'button',
|
|
416
|
-
{
|
|
417
|
-
key: k,
|
|
418
|
-
onClick: () => setTarget(k),
|
|
419
|
-
'data-testid': `todo-target-${k}`,
|
|
420
|
-
style: {
|
|
421
|
-
fontWeight: target === k ? 700 : 400,
|
|
422
|
-
padding: '4px 8px',
|
|
423
|
-
borderRadius: 6,
|
|
424
|
-
border: target === k ? STYLE.borderL2 : STYLE.borderL1,
|
|
425
|
-
background: target === k ? STYLE.active : STYLE.surface,
|
|
426
|
-
color: STYLE.text,
|
|
427
|
-
cursor: 'pointer',
|
|
428
|
-
},
|
|
429
|
-
},
|
|
430
|
-
k,
|
|
431
|
-
),
|
|
432
|
-
),
|
|
433
|
-
),
|
|
434
|
-
React.createElement(
|
|
435
|
-
'div',
|
|
436
|
-
{ style: { display: 'flex', gap: 12, marginBottom: 8, alignItems: 'center', flexWrap: 'wrap' } },
|
|
437
|
-
React.createElement(
|
|
438
|
-
'label',
|
|
439
|
-
{ style: { display: 'flex', gap: 4, alignItems: 'center' } },
|
|
440
|
-
React.createElement('input', {
|
|
441
|
-
type: 'checkbox',
|
|
442
|
-
checked: showAll,
|
|
443
|
-
onChange: (e: any) => setShowAll(e.target.checked),
|
|
444
|
-
'data-testid': 'todo-all',
|
|
445
|
-
}),
|
|
446
|
-
'all (no limit)',
|
|
447
|
-
),
|
|
448
|
-
React.createElement(
|
|
449
|
-
'label',
|
|
450
|
-
{ style: { display: 'flex', gap: 4, alignItems: 'center' } },
|
|
451
|
-
React.createElement('input', {
|
|
452
|
-
type: 'checkbox',
|
|
453
|
-
checked: showPast,
|
|
454
|
-
onChange: (e: any) => setShowPast(e.target.checked),
|
|
455
|
-
'data-testid': 'todo-past',
|
|
456
|
-
}),
|
|
457
|
-
'past',
|
|
458
|
-
),
|
|
459
|
-
React.createElement(
|
|
460
|
-
'label',
|
|
461
|
-
{ style: { display: 'flex', gap: 4, alignItems: 'center' } },
|
|
462
|
-
React.createElement('input', {
|
|
463
|
-
type: 'checkbox',
|
|
464
|
-
checked: showExpired,
|
|
465
|
-
onChange: (e: any) => setShowExpired(e.target.checked),
|
|
466
|
-
'data-testid': 'todo-expired',
|
|
467
|
-
}),
|
|
468
|
-
'expired',
|
|
469
|
-
),
|
|
470
|
-
React.createElement(
|
|
471
|
-
'button',
|
|
472
|
-
{ onClick: load, style: { padding: '4px 8px' }, 'data-testid': 'todo-refresh' },
|
|
473
|
-
'Refresh',
|
|
474
|
-
),
|
|
475
|
-
),
|
|
476
|
-
React.createElement(
|
|
477
|
-
'div',
|
|
478
|
-
{ style: { border: STYLE.borderL1, borderRadius: 8, padding: 8, marginBottom: 12 } },
|
|
479
|
-
React.createElement('div', { style: { fontWeight: 600, marginBottom: 6 } }, 'Add todo'),
|
|
480
|
-
React.createElement('textarea', {
|
|
481
|
-
value: draftContent,
|
|
482
|
-
placeholder: 'Todo content',
|
|
483
|
-
onChange: (e: any) => setDraftContent(e.target.value),
|
|
484
|
-
style: { width: '100%', minHeight: 40, marginBottom: 6 },
|
|
485
|
-
'data-testid': 'todo-add-content',
|
|
486
|
-
}),
|
|
487
|
-
React.createElement(
|
|
488
|
-
'div',
|
|
489
|
-
{ style: { display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 6 } },
|
|
490
|
-
React.createElement(
|
|
491
|
-
'select',
|
|
492
|
-
{
|
|
493
|
-
value: draftTarget,
|
|
494
|
-
onChange: (e: any) => setDraftTarget(e.target.value),
|
|
495
|
-
style: { padding: '4px' },
|
|
496
|
-
'data-testid': 'todo-add-target',
|
|
497
|
-
},
|
|
498
|
-
(['life', 'work', 'project', 'daily'] as const).map((t) => React.createElement('option', { key: t, value: t }, t)),
|
|
499
|
-
),
|
|
500
|
-
React.createElement(
|
|
501
|
-
'select',
|
|
502
|
-
{
|
|
503
|
-
value: draftQuadrant,
|
|
504
|
-
onChange: (e: any) => setDraftQuadrant(e.target.value),
|
|
505
|
-
style: { padding: '4px' },
|
|
506
|
-
'data-testid': 'todo-add-quadrant',
|
|
507
|
-
},
|
|
508
|
-
React.createElement('option', { value: '' }, 'quadrant'),
|
|
509
|
-
React.createElement('option', { value: 'q1' }, 'q1'),
|
|
510
|
-
React.createElement('option', { value: 'q2' }, 'q2'),
|
|
511
|
-
React.createElement('option', { value: 'q3' }, 'q3'),
|
|
512
|
-
React.createElement('option', { value: 'q4' }, 'q4'),
|
|
513
|
-
),
|
|
514
|
-
React.createElement('input', {
|
|
515
|
-
value: draftDue,
|
|
516
|
-
placeholder: 'due YYYY-MM-DD',
|
|
517
|
-
onChange: (e: any) => setDraftDue(e.target.value),
|
|
518
|
-
style: { padding: '4px', width: 120 },
|
|
519
|
-
'data-testid': 'todo-add-due',
|
|
520
|
-
}),
|
|
521
|
-
React.createElement('input', {
|
|
522
|
-
value: draftCat,
|
|
523
|
-
placeholder: 'cat',
|
|
524
|
-
onChange: (e: any) => setDraftCat(e.target.value),
|
|
525
|
-
style: { padding: '4px', width: 80 },
|
|
526
|
-
'data-testid': 'todo-add-cat',
|
|
527
|
-
}),
|
|
528
|
-
),
|
|
529
|
-
React.createElement(
|
|
530
|
-
'button',
|
|
531
|
-
{
|
|
532
|
-
onClick: addTodo,
|
|
533
|
-
style: { background: STYLE.success, color: STYLE.onAccent, border: 0, borderRadius: 4, padding: '6px 12px', cursor: 'pointer' },
|
|
534
|
-
'data-testid': 'todo-add-btn',
|
|
535
|
-
},
|
|
536
|
-
'Add',
|
|
196
|
+
const payload:any={ action, indices:[index] }
|
|
197
|
+
if(action==='approve' && edits[index]!==undefined && edits[index].trim()!=='') payload.edits={ [String(index)]: edits[index] }
|
|
198
|
+
try{ const res:any=await rpc('queue.decide',payload); if(res?.ok){ setMsg(res.lines?res.lines.join('; '):`${action} ok`); await load() } else setMsg(`failed: ${res?.error??'unknown'}`) }
|
|
199
|
+
catch(e:any){ setMsg(`error: ${e?.message??String(e)}`) }
|
|
200
|
+
},[rpc,edits,load])
|
|
201
|
+
|
|
202
|
+
const filtered = React.useMemo(()=>{
|
|
203
|
+
const t=q.trim().toLowerCase(); if(!t) return entries
|
|
204
|
+
return entries.filter((e:any)=> `${e.target} ${e.content} ${e.reason}`.toLowerCase().includes(t))
|
|
205
|
+
},[entries,q])
|
|
206
|
+
|
|
207
|
+
if(loading) return React.createElement('div', {className:'memx-muted'}, 'Loading queue…')
|
|
208
|
+
return React.createElement('div', {className:'memx-panel'},
|
|
209
|
+
React.createElement('div', {className:'memx-toolbar'},
|
|
210
|
+
React.createElement('div', {className:'memx-search', role:'search', 'aria-label':'Filter queue'},
|
|
211
|
+
React.createElement(Ico,{d:ICO.search}), React.createElement('input',{value:q, onChange:(e:any)=>setQ(e.target.value), placeholder:'Filter queue…', 'aria-label':'Filter queue'}),
|
|
537
212
|
),
|
|
213
|
+
React.createElement('button',{onClick:load, className:'memx-btn memx-btn-ghost', 'aria-label':'Refresh queue'}, React.createElement(Ico,{d:ICO.refresh}), ' Refresh'),
|
|
214
|
+
React.createElement('span',{className:'memx-muted'}, `${filtered.length}/${entries.length} pending`),
|
|
538
215
|
),
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
React.createElement('
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
it.due ? ` [due:${it.due}]` : '',
|
|
558
|
-
it.status !== 'pending' ? ` [${it.status}]` : '',
|
|
559
|
-
it.cat ? ` [cat:${it.cat}]` : '',
|
|
560
|
-
it.doneAt ? ` [done:${it.doneAt}]` : '',
|
|
561
|
-
it.past ? ` [past ${it.day}]` : '',
|
|
562
|
-
` id:${it.id}`,
|
|
563
|
-
),
|
|
564
|
-
editId === it.id
|
|
565
|
-
? React.createElement(
|
|
566
|
-
'div',
|
|
567
|
-
null,
|
|
568
|
-
React.createElement('textarea', {
|
|
569
|
-
value: editContent,
|
|
570
|
-
onChange: (e: any) => setEditContent(e.target.value),
|
|
571
|
-
style: { width: '100%', minHeight: 40, marginTop: 4 },
|
|
572
|
-
'data-testid': `todo-edit-content-${it.id}`,
|
|
573
|
-
}),
|
|
574
|
-
React.createElement(
|
|
575
|
-
'div',
|
|
576
|
-
{ style: { display: 'flex', gap: 6, marginTop: 6, flexWrap: 'wrap' } },
|
|
577
|
-
React.createElement(
|
|
578
|
-
'select',
|
|
579
|
-
{
|
|
580
|
-
value: editQuadrant,
|
|
581
|
-
onChange: (e: any) => setEditQuadrant(e.target.value),
|
|
582
|
-
'data-testid': `todo-edit-quadrant-${it.id}`,
|
|
583
|
-
},
|
|
584
|
-
React.createElement('option', { value: '' }, 'no quadrant'),
|
|
585
|
-
React.createElement('option', { value: 'q1' }, 'q1'),
|
|
586
|
-
React.createElement('option', { value: 'q2' }, 'q2'),
|
|
587
|
-
React.createElement('option', { value: 'q3' }, 'q3'),
|
|
588
|
-
React.createElement('option', { value: 'q4' }, 'q4'),
|
|
589
|
-
),
|
|
590
|
-
React.createElement('input', {
|
|
591
|
-
value: editDue,
|
|
592
|
-
placeholder: 'due YYYY-MM-DD',
|
|
593
|
-
onChange: (e: any) => setEditDue(e.target.value),
|
|
594
|
-
style: { width: 120 },
|
|
595
|
-
'data-testid': `todo-edit-due-${it.id}`,
|
|
596
|
-
}),
|
|
597
|
-
React.createElement(
|
|
598
|
-
'select',
|
|
599
|
-
{
|
|
600
|
-
value: editStatus,
|
|
601
|
-
onChange: (e: any) => setEditStatus(e.target.value),
|
|
602
|
-
'data-testid': `todo-edit-status-${it.id}`,
|
|
603
|
-
},
|
|
604
|
-
(['pending', 'doing', 'done', 'blocked', 'cancelled'] as const).map((s) => React.createElement('option', { key: s, value: s }, s)),
|
|
605
|
-
),
|
|
606
|
-
React.createElement('input', {
|
|
607
|
-
value: editCat,
|
|
608
|
-
placeholder: 'cat',
|
|
609
|
-
onChange: (e: any) => setEditCat(e.target.value),
|
|
610
|
-
style: { width: 80 },
|
|
611
|
-
'data-testid': `todo-edit-cat-${it.id}`,
|
|
612
|
-
}),
|
|
613
|
-
),
|
|
614
|
-
React.createElement(
|
|
615
|
-
'div',
|
|
616
|
-
{ style: { display: 'flex', gap: 8, marginTop: 8 } },
|
|
617
|
-
React.createElement(
|
|
618
|
-
'button',
|
|
619
|
-
{
|
|
620
|
-
onClick: () => saveEdit(it),
|
|
621
|
-
style: { background: STYLE.success, color: STYLE.onAccent, border: 0, borderRadius: 4, padding: '4px 8px', cursor: 'pointer' },
|
|
622
|
-
'data-testid': `todo-save-${it.id}`,
|
|
623
|
-
},
|
|
624
|
-
'Save',
|
|
625
|
-
),
|
|
626
|
-
React.createElement(
|
|
627
|
-
'button',
|
|
628
|
-
{
|
|
629
|
-
onClick: () => setEditId(null),
|
|
630
|
-
style: { background: STYLE.neutral, color: STYLE.text, border: STYLE.borderL1, borderRadius: 4, padding: '4px 8px', cursor: 'pointer' },
|
|
631
|
-
'data-testid': `todo-cancel-${it.id}`,
|
|
632
|
-
},
|
|
633
|
-
'Cancel',
|
|
634
|
-
),
|
|
635
|
-
),
|
|
636
|
-
)
|
|
637
|
-
: React.createElement(
|
|
638
|
-
'div',
|
|
639
|
-
null,
|
|
640
|
-
React.createElement('div', { style: { margin: '4px 0', whiteSpace: 'pre-wrap' } }, it.text),
|
|
641
|
-
React.createElement(
|
|
642
|
-
'div',
|
|
643
|
-
{ style: { display: 'flex', gap: 6, marginTop: 8, flexWrap: 'wrap' } },
|
|
644
|
-
React.createElement(
|
|
645
|
-
'button',
|
|
646
|
-
{
|
|
647
|
-
onClick: () => (it.status === 'done' ? undoTodo(it) : doneTodo(it)),
|
|
648
|
-
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' },
|
|
649
|
-
'data-testid': `todo-done-${it.id}`,
|
|
650
|
-
},
|
|
651
|
-
it.status === 'done' ? 'Undo' : 'Done',
|
|
652
|
-
),
|
|
653
|
-
React.createElement(
|
|
654
|
-
'button',
|
|
655
|
-
{
|
|
656
|
-
onClick: () => startEdit(it),
|
|
657
|
-
style: { background: STYLE.brand, color: STYLE.onAccent, border: 0, borderRadius: 4, padding: '4px 8px', cursor: 'pointer' },
|
|
658
|
-
'data-testid': `todo-edit-${it.id}`,
|
|
659
|
-
},
|
|
660
|
-
'Edit',
|
|
661
|
-
),
|
|
662
|
-
React.createElement(
|
|
663
|
-
'button',
|
|
664
|
-
{
|
|
665
|
-
onClick: () => removeTodo(it),
|
|
666
|
-
style: { background: STYLE.error, color: STYLE.onAccent, border: 0, borderRadius: 4, padding: '4px 8px', cursor: 'pointer' },
|
|
667
|
-
'data-testid': `todo-remove-${it.id}`,
|
|
668
|
-
},
|
|
669
|
-
'Remove',
|
|
670
|
-
),
|
|
671
|
-
),
|
|
672
|
-
),
|
|
673
|
-
),
|
|
674
|
-
),
|
|
216
|
+
filtered.length===0 ? React.createElement('div',{className:'memx-empty'},
|
|
217
|
+
React.createElement('div',{style:{fontWeight:600, color:'var(--dsw-alias-label-primary)'}}, entries.length===0?'No pending suggestions':'No results'),
|
|
218
|
+
React.createElement('div',{className:'memx-muted'}, entries.length===0?'Model proposals will appear here for your approval.':'Try a different filter.'),
|
|
219
|
+
) : React.createElement('div',{className:'memx-grid'},
|
|
220
|
+
...filtered.map((e:any)=>{
|
|
221
|
+
const number = entries.indexOf(e)+1
|
|
222
|
+
return React.createElement('div',{key:number, className:'memx-card'},
|
|
223
|
+
React.createElement('div',{className:'memx-cardhead'},
|
|
224
|
+
React.createElement('span',{className:'memx-chip'}, `#${number} · ${e.target}`),
|
|
225
|
+
e.reason ? React.createElement('span',{className:'memx-sm'}, e.reason.slice(0,80)) : null,
|
|
226
|
+
),
|
|
227
|
+
React.createElement('div',{className:'memx-pre'}, e.content),
|
|
228
|
+
e.reason ? React.createElement('div',{className:'memx-sm'}, `Reason: ${e.reason}`) : null,
|
|
229
|
+
React.createElement('textarea',{value:edits[number]??'', placeholder:'Edit content before approve (optional)', onChange:(ev:any)=>setEdits(prev=>({...prev,[number]:ev.target.value})), className:'memx-textarea', 'aria-label':'Edit before approve', rows:2}),
|
|
230
|
+
React.createElement('div',{className:'memx-actions'},
|
|
231
|
+
React.createElement('button',{onClick:()=>decide('approve',number), 'data-testid':`approve-${number}`, className:'memx-btn memx-btn-primary'}, 'Approve'),
|
|
232
|
+
React.createElement('button',{onClick:()=>decide('reject',number), 'data-testid':`reject-${number}`, className:'memx-btn'}, 'Reject'),
|
|
233
|
+
React.createElement('button',{onClick:()=>decide('archive',number), 'data-testid':`archive-${number}`, className:'memx-btn'}, 'Archive'),
|
|
675
234
|
),
|
|
235
|
+
)
|
|
236
|
+
})
|
|
237
|
+
),
|
|
238
|
+
msg ? React.createElement('div',{className:'memx-muted', style:{whiteSpace:'pre-wrap'}}, msg) : null,
|
|
676
239
|
)
|
|
677
240
|
}
|
|
678
241
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
const
|
|
682
|
-
const [
|
|
683
|
-
const [
|
|
684
|
-
|
|
685
|
-
const
|
|
686
|
-
|
|
242
|
+
// ── Todos ──────────────────────────────────────────────────────────
|
|
243
|
+
function TodosView({ ctx }: {ctx:any}): React.ReactElement {
|
|
244
|
+
const rpc=useRpc(ctx)
|
|
245
|
+
const [items,setItems]=React.useState<any[]>([])
|
|
246
|
+
const [loading,setLoading]=React.useState(true)
|
|
247
|
+
const [msg,setMsg]=React.useState('')
|
|
248
|
+
const [target,setTarget]=React.useState<string>('all')
|
|
249
|
+
const [showAll,setShowAll]=React.useState(false)
|
|
250
|
+
const [showPast,setShowPast]=React.useState(false)
|
|
251
|
+
const [showExpired,setShowExpired]=React.useState(false)
|
|
252
|
+
const [q,setQ]=React.useState('')
|
|
253
|
+
const [draftContent,setDraftContent]=React.useState('')
|
|
254
|
+
const [draftTarget,setDraftTarget]=React.useState<string>('work')
|
|
255
|
+
const [draftDue,setDraftDue]=React.useState('')
|
|
256
|
+
const [draftQuadrant,setDraftQuadrant]=React.useState<string>('')
|
|
257
|
+
const [draftCat,setDraftCat]=React.useState('')
|
|
258
|
+
const [openAdd,setOpenAdd]=React.useState(true)
|
|
259
|
+
const [editId,setEditId]=React.useState<string|null>(null)
|
|
260
|
+
const [editContent,setEditContent]=React.useState('')
|
|
261
|
+
const [editDue,setEditDue]=React.useState('')
|
|
262
|
+
const [editQuadrant,setEditQuadrant]=React.useState('')
|
|
263
|
+
const [editStatus,setEditStatus]=React.useState('')
|
|
264
|
+
const [editCat,setEditCat]=React.useState('')
|
|
265
|
+
|
|
266
|
+
const load=React.useCallback(async()=>{
|
|
267
|
+
setLoading(true); setMsg('')
|
|
268
|
+
try{
|
|
269
|
+
const res:any=await rpc('todo.list',{ target: target!=='all'?target:undefined, opts:{ all:showAll, past:showPast, expired:showExpired } })
|
|
270
|
+
setItems(Array.isArray(res?.items)?res.items:[])
|
|
271
|
+
if(res?.hint) setMsg(res.hint)
|
|
272
|
+
} catch(e:any){ setMsg(`load failed: ${e?.message??String(e)}`) } finally{ setLoading(false) }
|
|
273
|
+
},[rpc,target,showAll,showPast,showExpired])
|
|
274
|
+
React.useEffect(()=>{ load() },[load])
|
|
275
|
+
|
|
276
|
+
const filtered = React.useMemo(()=>{
|
|
277
|
+
const t=q.trim().toLowerCase(); if(!t) return items
|
|
278
|
+
return items.filter((it:any)=> `${it.text} ${it.target} ${it.cat} ${it.status}`.toLowerCase().includes(t))
|
|
279
|
+
},[items,q])
|
|
280
|
+
|
|
281
|
+
const addTodo=React.useCallback(async()=>{
|
|
282
|
+
const content=draftContent.trim(); if(!content){ setMsg('content required'); return }
|
|
687
283
|
setMsg('')
|
|
688
|
-
try
|
|
689
|
-
const res:
|
|
690
|
-
if
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
'
|
|
714
|
-
{
|
|
715
|
-
|
|
284
|
+
try{
|
|
285
|
+
const res:any=await rpc('todo.mutate',{ action:'add', target:draftTarget, content, due:draftDue||undefined, quadrant:draftQuadrant||undefined, cat:draftCat||undefined })
|
|
286
|
+
if(res?.ok){ setDraftContent(''); setDraftDue(''); setDraftQuadrant(''); setDraftCat(''); setMsg(`added (id: ${res.id})`); await load() }
|
|
287
|
+
else setMsg(`add failed: ${res?.message??res?.error??'unknown'}`)
|
|
288
|
+
} catch(e:any){ setMsg(`error: ${e?.message??String(e)}`) }
|
|
289
|
+
},[rpc,draftContent,draftTarget,draftDue,draftQuadrant,draftCat,load])
|
|
290
|
+
|
|
291
|
+
const doneTodo=React.useCallback(async(item:any)=>{
|
|
292
|
+
try{ const res:any=await rpc('todo.mutate',{action:'done', target:item.target, id:item.id}); if(res?.ok){ setMsg(res.message??'done'); await load() } else setMsg(`done failed: ${res?.message??res?.error}`) } catch(e:any){ setMsg(`error: ${e?.message??String(e)}`) }
|
|
293
|
+
},[rpc,load])
|
|
294
|
+
const undoTodo=React.useCallback(async(item:any)=>{
|
|
295
|
+
try{ const res:any=await rpc('todo.mutate',{action:'update', target:item.target, id:item.id, status:'pending'}); if(res?.ok){ setMsg(res.message??'undone'); await load() } else setMsg(`undo failed: ${res?.message??res?.error}`) } catch(e:any){ setMsg(`error: ${e?.message??String(e)}`) }
|
|
296
|
+
},[rpc,load])
|
|
297
|
+
const removeTodo=React.useCallback(async(item:any)=>{
|
|
298
|
+
try{ const res:any=await rpc('todo.mutate',{action:'remove', target:item.target, id:item.id}); if(res?.ok){ setMsg(res.message??'removed'); await load() } else setMsg(`remove failed: ${res?.message??res?.error}`) } catch(e:any){ setMsg(`error: ${e?.message??String(e)}`) }
|
|
299
|
+
},[rpc,load])
|
|
300
|
+
const startEdit=(item:any)=>{ setEditId(item.id); setEditContent(item.text??''); setEditDue(item.due??''); setEditQuadrant(item.quadrant??''); setEditStatus(item.status??'pending'); setEditCat(item.cat??'') }
|
|
301
|
+
const saveEdit=React.useCallback(async(item:any)=>{
|
|
302
|
+
try{
|
|
303
|
+
const patch:any={}
|
|
304
|
+
if(editContent!==item.text) patch.content=editContent
|
|
305
|
+
if(editQuadrant!==(item.quadrant??'')) patch.quadrant=editQuadrant||null
|
|
306
|
+
if(editDue!==(item.due??'')) patch.due=editDue||null
|
|
307
|
+
if(editStatus!==item.status) patch.status=editStatus
|
|
308
|
+
if(editCat!==(item.cat??'')) patch.cat=editCat||null
|
|
309
|
+
const res:any=await rpc('todo.mutate',{ action:'update', target:item.target, id:item.id, ...patch })
|
|
310
|
+
if(res?.ok){ setEditId(null); setMsg(res.message??'updated'); await load() } else setMsg(`update failed: ${res?.message??res?.error}`)
|
|
311
|
+
} catch(e:any){ setMsg(`error: ${e?.message??String(e)}`) }
|
|
312
|
+
},[rpc,editContent,editDue,editQuadrant,editStatus,editCat,load])
|
|
313
|
+
|
|
314
|
+
return React.createElement('div',{className:'memx-panel'},
|
|
315
|
+
React.createElement('div',{className:'memx-toolbar'},
|
|
316
|
+
React.createElement('div',{className:'memx-pills'}, (['all','life','work','project','daily'] as const).map(k=> React.createElement('button',{key:k, onClick:()=>setTarget(k), 'aria-pressed':target===k, className:'memx-pill', 'data-testid':`todo-target-${k}`}, k))),
|
|
317
|
+
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:any)=>setQ(e.target.value), placeholder:'Filter todos…', 'aria-label':'Filter todos'})),
|
|
318
|
+
),
|
|
319
|
+
React.createElement('div',{className:'memx-toolbar'},
|
|
320
|
+
React.createElement('label',{style:{display:'flex',gap:6,alignItems:'center'}}, React.createElement('input',{type:'checkbox', checked:showAll, onChange:(e:any)=>setShowAll(e.target.checked), 'data-testid':'todo-all'}), 'all'),
|
|
321
|
+
React.createElement('label',{style:{display:'flex',gap:6,alignItems:'center'}}, React.createElement('input',{type:'checkbox', checked:showPast, onChange:(e:any)=>setShowPast(e.target.checked), 'data-testid':'todo-past'}), 'past'),
|
|
322
|
+
React.createElement('label',{style:{display:'flex',gap:6,alignItems:'center'}}, React.createElement('input',{type:'checkbox', checked:showExpired, onChange:(e:any)=>setShowExpired(e.target.checked), 'data-testid':'todo-expired'}), 'expired'),
|
|
323
|
+
React.createElement('button',{onClick:load, className:'memx-btn memx-btn-ghost', 'data-testid':'todo-refresh'}, React.createElement(Ico,{d:ICO.refresh}), 'Refresh'),
|
|
324
|
+
),
|
|
325
|
+
React.createElement('div',{className:'memx-form'},
|
|
326
|
+
React.createElement('button',{onClick:()=>setOpenAdd(v=>!v), className:'memx-btn memx-btn-ghost', style:{justifyContent:'space-between'}, 'aria-expanded':openAdd},
|
|
327
|
+
React.createElement('span',{style:{display:'flex',gap:8,alignItems:'center'}}, React.createElement(Ico,{d:ICO.plus}), 'Add todo'),
|
|
328
|
+
React.createElement('span',{className:'memx-muted'}, openAdd?'Hide':'Show'),
|
|
329
|
+
),
|
|
330
|
+
openAdd ? React.createElement(React.Fragment,null,
|
|
331
|
+
React.createElement('textarea',{value:draftContent, placeholder:'Todo content', onChange:(e:any)=>setDraftContent(e.target.value), className:'memx-textarea', 'aria-label':'Todo content', 'data-testid':'todo-add-content'}),
|
|
332
|
+
React.createElement('div',{className:'memx-formrow'},
|
|
333
|
+
React.createElement('select',{value:draftTarget, onChange:(e:any)=>setDraftTarget(e.target.value), className:'memx-select', 'aria-label':'Todo target', 'data-testid':'todo-add-target'}, (['life','work','project','daily'] as const).map(t=> React.createElement('option',{key:t,value:t},t))),
|
|
334
|
+
React.createElement('select',{value:draftQuadrant, onChange:(e:any)=>setDraftQuadrant(e.target.value), className:'memx-select', 'aria-label':'Quadrant', 'data-testid':'todo-add-quadrant'},
|
|
335
|
+
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')),
|
|
336
|
+
React.createElement('input',{value:draftDue, placeholder:'due YYYY-MM-DD', onChange:(e:any)=>setDraftDue(e.target.value), className:'memx-select', 'aria-label':'Due date YYYY-MM-DD', 'data-testid':'todo-add-due'}),
|
|
337
|
+
React.createElement('input',{value:draftCat, placeholder:'cat', onChange:(e:any)=>setDraftCat(e.target.value), className:'memx-select', 'aria-label':'Category', 'data-testid':'todo-add-cat'}),
|
|
338
|
+
),
|
|
339
|
+
React.createElement('button',{onClick:addTodo, className:'memx-btn memx-btn-primary', 'data-testid':'todo-add-btn'}, 'Add'),
|
|
340
|
+
) : null,
|
|
716
341
|
),
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
...
|
|
723
|
-
React.createElement(
|
|
724
|
-
'
|
|
725
|
-
|
|
726
|
-
React.createElement('
|
|
727
|
-
React.createElement('
|
|
728
|
-
React.createElement('
|
|
729
|
-
e.metadata && Object.keys(e.metadata).length > 0
|
|
730
|
-
? React.createElement(
|
|
731
|
-
'div',
|
|
732
|
-
{ style: { opacity: 0.6, marginTop: 4 } },
|
|
733
|
-
`metadata: ${Object.entries(e.metadata)
|
|
734
|
-
.map(([k, v]) => `${k}=${String(v).slice(0, 60)}`)
|
|
735
|
-
.join(', ')}`,
|
|
736
|
-
)
|
|
737
|
-
: null,
|
|
342
|
+
msg ? React.createElement('div',{className:'memx-muted', style:{whiteSpace:'pre-wrap'}}, msg) : null,
|
|
343
|
+
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.') :
|
|
344
|
+
React.createElement('div',null,
|
|
345
|
+
React.createElement('div',{className:'memx-muted', style:{marginBottom:6}}, `${filtered.length} todos${!showAll && filtered.length===8?' (smart view ≤8)':''}`),
|
|
346
|
+
React.createElement('div',{className:'memx-grid'},
|
|
347
|
+
...filtered.map((it:any)=> React.createElement('div',{key:it.id, className:'memx-card'},
|
|
348
|
+
React.createElement('div',{style:{display:'flex',gap:6,flexWrap:'wrap'}},
|
|
349
|
+
React.createElement('span',{className:'memx-chip'}, it.target),
|
|
350
|
+
it.quadrant?React.createElement('span',{className:'memx-chip'}, it.quadrant):null,
|
|
351
|
+
it.due?React.createElement('span',{className:'memx-chip'}, `due ${it.due}`):null,
|
|
352
|
+
it.status!=='pending'?React.createElement('span',{className:'memx-chip'}, it.status):null,
|
|
353
|
+
it.cat?React.createElement('span',{className:'memx-chip'}, it.cat):null,
|
|
738
354
|
),
|
|
739
|
-
|
|
355
|
+
editId===it.id ? React.createElement('div',null,
|
|
356
|
+
React.createElement('textarea',{value:editContent, onChange:(e:any)=>setEditContent(e.target.value), className:'memx-textarea', 'aria-label':'Edit todo content', 'data-testid':`todo-edit-content-${it.id}`}),
|
|
357
|
+
React.createElement('div',{className:'memx-formrow', style:{marginTop:8}},
|
|
358
|
+
React.createElement('select',{value:editQuadrant, onChange:(e:any)=>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')),
|
|
359
|
+
React.createElement('input',{value:editDue, placeholder:'due YYYY-MM-DD', onChange:(e:any)=>setEditDue(e.target.value), className:'memx-select', 'aria-label':'Edit due date', 'data-testid':`todo-edit-due-${it.id}`}),
|
|
360
|
+
React.createElement('select',{value:editStatus, onChange:(e:any)=>setEditStatus(e.target.value), className:'memx-select', 'aria-label':'Edit status', 'data-testid':`todo-edit-status-${it.id}`}, (['pending','doing','done','blocked','cancelled'] as const).map(s=> React.createElement('option',{key:s,value:s},s))),
|
|
361
|
+
React.createElement('input',{value:editCat, placeholder:'cat', onChange:(e:any)=>setEditCat(e.target.value), className:'memx-select', 'aria-label':'Edit category', 'data-testid':`todo-edit-cat-${it.id}`}),
|
|
362
|
+
),
|
|
363
|
+
React.createElement('div',{className:'memx-actions', style:{marginTop:8}},
|
|
364
|
+
React.createElement('button',{onClick:()=>saveEdit(it), className:'memx-btn memx-btn-primary', 'data-testid':`todo-save-${it.id}`}, 'Save'),
|
|
365
|
+
React.createElement('button',{onClick:()=>setEditId(null), className:'memx-btn', 'data-testid':`todo-cancel-${it.id}`}, 'Cancel'),
|
|
366
|
+
),
|
|
367
|
+
) : React.createElement(React.Fragment,null,
|
|
368
|
+
React.createElement('div',{className:'memx-pre'}, it.text),
|
|
369
|
+
React.createElement('div',{className:'memx-actions', style:{marginTop:8}},
|
|
370
|
+
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'),
|
|
371
|
+
React.createElement('button',{onClick:()=>startEdit(it), className:'memx-btn', 'data-testid':`todo-edit-${it.id}`}, 'Edit'),
|
|
372
|
+
React.createElement('button',{onClick:()=>removeTodo(it), className:'memx-btn', 'data-testid':`todo-remove-${it.id}`}, 'Remove'),
|
|
373
|
+
),
|
|
374
|
+
),
|
|
375
|
+
it.doneAt||it.past?React.createElement('div',{className:'memx-xs'}, [it.doneAt?`done:${it.doneAt}`:'', it.past?`past ${it.day}`:''].filter(Boolean).join(' · ')):null,
|
|
376
|
+
))
|
|
740
377
|
),
|
|
741
|
-
|
|
742
|
-
React.createElement('button', { onClick: load, style: { marginTop: 8, padding: '4px 8px' }, 'data-testid': 'skills-refresh' }, 'Refresh'),
|
|
743
|
-
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.'),
|
|
378
|
+
),
|
|
744
379
|
)
|
|
745
380
|
}
|
|
746
381
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
const
|
|
750
|
-
const [
|
|
751
|
-
const [
|
|
752
|
-
const [
|
|
753
|
-
const [
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
setLoading(
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
setMsg(`load failed: ${e?.message ?? String(e)}`)
|
|
770
|
-
} finally {
|
|
771
|
-
setLoading(false)
|
|
772
|
-
}
|
|
773
|
-
}, [rpc, track, cwd, needsCwd])
|
|
774
|
-
|
|
775
|
-
React.useEffect(() => {
|
|
776
|
-
load()
|
|
777
|
-
}, [load])
|
|
778
|
-
|
|
779
|
-
return React.createElement(
|
|
780
|
-
'div',
|
|
781
|
-
{ style: { display: 'flex', flexDirection: 'column', gap: 12 } },
|
|
782
|
-
React.createElement(
|
|
783
|
-
'div',
|
|
784
|
-
{ className: 'row', style: { alignItems: 'center', gap: 6 } },
|
|
785
|
-
React.createElement('span', { className: 'tracklabel' }, 'Track'),
|
|
786
|
-
(['memory', 'user', 'key', 'project', 'daily'] as const).map((k) =>
|
|
787
|
-
React.createElement(
|
|
788
|
-
'button',
|
|
789
|
-
{
|
|
790
|
-
key: k,
|
|
791
|
-
onClick: () => setTrack(k),
|
|
792
|
-
'aria-pressed': track === k,
|
|
793
|
-
className: track === k ? 'control pill pill-active' : 'control pill',
|
|
794
|
-
'data-testid': `mem-track-${k}`,
|
|
795
|
-
},
|
|
796
|
-
k,
|
|
797
|
-
),
|
|
798
|
-
),
|
|
799
|
-
),
|
|
800
|
-
React.createElement(
|
|
801
|
-
'div',
|
|
802
|
-
{ className: 'row', style: { alignItems: 'center' } },
|
|
803
|
-
React.createElement('input', {
|
|
804
|
-
value: cwd,
|
|
805
|
-
disabled: !needsCwd,
|
|
806
|
-
placeholder: needsCwd ? 'cwd (for key/project)' : 'cwd is not used for this track',
|
|
807
|
-
onChange: (e: any) => setCwd((e.target as HTMLInputElement).value),
|
|
808
|
-
'data-testid': 'mem-cwd',
|
|
809
|
-
className: 'cwd',
|
|
810
|
-
}),
|
|
811
|
-
React.createElement('button', { onClick: load, className: 'control ghost', 'data-testid': 'mem-refresh' }, 'Refresh'),
|
|
382
|
+
// ── Skills ─────────────────────────────────────────────────────────
|
|
383
|
+
function SkillsView({ ctx }: {ctx:any}): React.ReactElement {
|
|
384
|
+
const rpc=useRpc(ctx)
|
|
385
|
+
const [entries,setEntries]=React.useState<any[]>([])
|
|
386
|
+
const [loading,setLoading]=React.useState(true)
|
|
387
|
+
const [msg,setMsg]=React.useState('')
|
|
388
|
+
const [q,setQ]=React.useState('')
|
|
389
|
+
const load=React.useCallback(async()=>{
|
|
390
|
+
setLoading(true); setMsg('')
|
|
391
|
+
try{ const res:any=await rpc('skills.list',{}); if(res?.ok){ setEntries(Array.isArray(res.entries)?res.entries:[]); if(res.entries.length===0) setMsg('No skills found') } else setMsg(`load failed: ${res?.error??'unknown'}`) }
|
|
392
|
+
catch(e:any){ setMsg(`error: ${e?.message??String(e)}`) } finally{ setLoading(false) }
|
|
393
|
+
},[rpc])
|
|
394
|
+
React.useEffect(()=>{ load() },[load])
|
|
395
|
+
const filtered=React.useMemo(()=>{
|
|
396
|
+
const t=q.trim().toLowerCase(); if(!t) return entries
|
|
397
|
+
return entries.filter((e:any)=> `${e.name} ${e.description} ${e.origin}`.toLowerCase().includes(t))
|
|
398
|
+
},[entries,q])
|
|
399
|
+
if(loading) return React.createElement('div',{className:'memx-muted'},'Loading skills…')
|
|
400
|
+
return React.createElement('div',{className:'memx-panel'},
|
|
401
|
+
React.createElement('div',{className:'memx-toolbar'},
|
|
402
|
+
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:any)=>setQ(e.target.value), placeholder:'Filter skills…', 'aria-label':'Filter skills'})),
|
|
403
|
+
React.createElement('button',{onClick:load, className:'memx-btn memx-btn-ghost', 'data-testid':'skills-refresh'}, React.createElement(Ico,{d:ICO.refresh}), 'Refresh'),
|
|
812
404
|
),
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
'div',
|
|
825
|
-
{ key: idx, className: 'card', style: { marginBottom: 8 } },
|
|
826
|
-
React.createElement('div', { style: { whiteSpace: 'pre-wrap' } }, e),
|
|
827
|
-
),
|
|
828
|
-
),
|
|
829
|
-
),
|
|
405
|
+
React.createElement('div',{className:'memx-muted'}, `${filtered.length}/${entries.length} skills — read-only, no mutation`),
|
|
406
|
+
filtered.length===0 ? React.createElement('div',{className:'memx-empty'}, msg||'No skills') :
|
|
407
|
+
React.createElement('div',{className:'memx-grid memx-grid-2'},
|
|
408
|
+
...filtered.map((e:any)=> React.createElement('div',{key:e.name, className:'memx-card'},
|
|
409
|
+
React.createElement('div',{className:'memx-cardtitle', style:{display:'flex',gap:8,alignItems:'center'}}, React.createElement(Ico,{d:ICO.layers, size:14}), e.name),
|
|
410
|
+
React.createElement('div',{className:'memx-sm'}, `[${e.origin}] ${e.path}`),
|
|
411
|
+
React.createElement('div',{className:'memx-pre-sm'}, e.description),
|
|
412
|
+
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,
|
|
413
|
+
))
|
|
414
|
+
),
|
|
415
|
+
msg?React.createElement('div',{className:'memx-muted', style:{marginTop:8}},msg):null,
|
|
830
416
|
)
|
|
831
417
|
}
|
|
832
418
|
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
const
|
|
836
|
-
const [
|
|
837
|
-
const
|
|
419
|
+
// ── Memory List ────────────────────────────────────────────────────
|
|
420
|
+
function MemoryListView({ ctx }: {ctx:any}): React.ReactElement {
|
|
421
|
+
const rpc=useRpc(ctx)
|
|
422
|
+
const [track,setTrack]=React.useState<string>('key')
|
|
423
|
+
const [cwd,setCwd]=React.useState<string>(()=>sessionCwd(ctx))
|
|
424
|
+
const [entries,setEntries]=React.useState<string[]>([])
|
|
425
|
+
const [loading,setLoading]=React.useState(true)
|
|
426
|
+
const [msg,setMsg]=React.useState('')
|
|
427
|
+
const [q,setQ]=React.useState('')
|
|
428
|
+
const needsCwd=track==='key'||track==='project'
|
|
429
|
+
const load=React.useCallback(async()=>{
|
|
838
430
|
setLoading(true); setMsg('')
|
|
839
|
-
try
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
React.createElement('div', { className: 'card', style: { flex: 1, minWidth: 140 } },
|
|
859
|
-
React.createElement('div', { className: 'muted' }, 'Project entries'),
|
|
860
|
-
React.createElement('div', { style: { fontWeight: 700 } }, String(health.project.total)),
|
|
861
|
-
React.createElement('div', { className: 'muted' }, `${health.project.withSummary} with summary`),
|
|
862
|
-
),
|
|
863
|
-
React.createElement('div', { className: 'card', style: { flex: 1, minWidth: 140 } },
|
|
864
|
-
React.createElement('div', { className: 'muted' }, 'Coverage'),
|
|
865
|
-
React.createElement('div', { style: { fontWeight: 700, color } }, `${cov.toFixed(1)}%`),
|
|
866
|
-
React.createElement('div', { className: 'muted' }, cov >= 90 ? '✅ PASS' : '❌ FAIL (<90%)'),
|
|
867
|
-
),
|
|
868
|
-
React.createElement('div', { className: 'card', style: { flex: 1, minWidth: 140 } },
|
|
869
|
-
React.createElement('div', { className: 'muted' }, 'Daily last 7d'),
|
|
870
|
-
React.createElement('div', { style: { fontWeight: 600 } }, (health.daily.counts as number[]).join(' · ')),
|
|
871
|
-
),
|
|
872
|
-
React.createElement('div', { className: 'card', style: { flex: 1, minWidth: 140 } },
|
|
873
|
-
React.createElement('div', { className: 'muted' }, 'Discipline'),
|
|
874
|
-
React.createElement('div', { style: { fontWeight: 600 } }, '1.5 avg calls/session'),
|
|
875
|
-
React.createElement('div', { className: 'muted' }, 'target 1+ (5/20 zero before fix)'),
|
|
431
|
+
try{
|
|
432
|
+
if(needsCwd && !cwd.trim()){ setEntries([]); setMsg('cwd required for key/project'); setLoading(false); return }
|
|
433
|
+
const res:any=await rpc('memory.list',{ target:track, cwd: needsCwd?cwd.trim():undefined })
|
|
434
|
+
setEntries(Array.isArray(res?.entries)?res.entries:[])
|
|
435
|
+
} catch(e:any){ setMsg(`load failed: ${e?.message??String(e)}`) } finally{ setLoading(false) }
|
|
436
|
+
},[rpc,track,cwd,needsCwd])
|
|
437
|
+
React.useEffect(()=>{ load() },[load])
|
|
438
|
+
|
|
439
|
+
const filtered=React.useMemo(()=>{
|
|
440
|
+
const t=q.trim().toLowerCase(); if(!t) return entries
|
|
441
|
+
return entries.filter(e=> e.toLowerCase().includes(t))
|
|
442
|
+
},[entries,q])
|
|
443
|
+
|
|
444
|
+
return React.createElement('div',{className:'memx-panel'},
|
|
445
|
+
React.createElement('div',{className:'memx-pills'}, (['memory','user','key','project','daily'] as const).map(k=> React.createElement('button',{key:k, onClick:()=>setTrack(k), 'aria-pressed':track===k, className:'memx-pill', 'data-testid':`mem-track-${k}`}, k))),
|
|
446
|
+
React.createElement('div',{className:'memx-toolbar'},
|
|
447
|
+
React.createElement('div',{className:'memx-field'},
|
|
448
|
+
React.createElement('input',{value:cwd, disabled:!needsCwd, placeholder: needsCwd?'cwd (for key/project)':'cwd not used', onChange:(e:any)=>setCwd((e.target as HTMLInputElement).value), 'aria-label':'CWD for key/project', 'data-testid':'mem-cwd', className:'memx-input'}),
|
|
449
|
+
React.createElement('button',{onClick:load, className:'memx-btn', 'data-testid':'mem-refresh'}, React.createElement(Ico,{d:ICO.refresh, size:14}), 'Refresh'),
|
|
876
450
|
),
|
|
451
|
+
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:any)=>setQ(e.target.value), placeholder:'Filter entries…', 'aria-label':'Filter entries'})),
|
|
877
452
|
),
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
React.createElement('div',
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
React.createElement('div',
|
|
884
|
-
)),
|
|
885
|
-
React.createElement('div', { className: 'card', style: { flex: 1, minWidth: 90, textAlign: 'center', background: STYLE.active } },
|
|
886
|
-
React.createElement('div', { className: 'muted' }, 'Composite'),
|
|
887
|
-
React.createElement('div', { style: { fontWeight: 800 } }, String(health.fiveDim.composite)),
|
|
453
|
+
msg?React.createElement('div',{className:'memx-muted'},msg):null,
|
|
454
|
+
loading ? React.createElement('div',{className:'memx-muted'},'Loading memory…') : filtered.length===0 ? React.createElement('div',{className:'memx-empty'}, '(no entries)') :
|
|
455
|
+
React.createElement('div',null,
|
|
456
|
+
React.createElement('div',{className:'memx-muted', style:{marginBottom:6}}, `${filtered.length}/${entries.length} entries`),
|
|
457
|
+
React.createElement('div',{className:'memx-grid'},
|
|
458
|
+
...filtered.map((e:string, idx:number)=> React.createElement('div',{key:idx, className:'memx-card'}, React.createElement('div',{className:'memx-pre'}, e)) ),
|
|
888
459
|
),
|
|
889
460
|
),
|
|
890
|
-
) : null,
|
|
891
|
-
React.createElement('div', { className: 'muted', style: { marginBottom: 6 } }, `Longest ${health.longest.length} entries`),
|
|
892
|
-
...health.longest.map((it: any, i: number) => React.createElement('div', { key: i, className: 'card', style: { marginBottom: 6, display: 'flex', gap: 8, alignItems: 'center', justifyContent: 'space-between' } },
|
|
893
|
-
React.createElement('div', { style: { flex: 1 } },
|
|
894
|
-
React.createElement('div', { className: 'muted' }, `${it.len} chars`), React.createElement('div', { style: { whiteSpace: 'pre-wrap' } }, it.preview),
|
|
895
|
-
),
|
|
896
|
-
React.createElement('button', {
|
|
897
|
-
onClick: async () => {
|
|
898
|
-
try {
|
|
899
|
-
const conn2 = (ctx as any).connection ?? (ctx as any).get?.('connection')
|
|
900
|
-
if (!conn2?.rpc?.call) throw new Error('RPC not available')
|
|
901
|
-
const res: any = await conn2.rpc.call('/dsh-maestro-memory-propose', 'add', { content: it.preview, reason: 'promote from Health longest' })
|
|
902
|
-
const ok = res?.ok === true ? res.value : res
|
|
903
|
-
setMsg(ok?.queued ? `proposed (queue ${ok.queued})` : `proposed: ${JSON.stringify(ok).slice(0,80)}`)
|
|
904
|
-
} catch (e: any) { setMsg(`propose failed: ${e?.message ?? String(e)}`) }
|
|
905
|
-
},
|
|
906
|
-
style: { padding: '4px 8px', background: STYLE.brand, color: STYLE.onAccent, border: 0, borderRadius: 4, cursor: 'pointer', whiteSpace: 'nowrap' },
|
|
907
|
-
'data-testid': `health-propose-${i}`,
|
|
908
|
-
}, 'Suggest as KEY'),
|
|
909
|
-
)),
|
|
910
|
-
msg ? React.createElement('div', { style: { marginTop: 8, color: STYLE.textSec, whiteSpace: 'pre-wrap' } }, msg) : null,
|
|
911
|
-
React.createElement('button', { onClick: load, style: { marginTop: 12, padding: '6px 12px' }, 'data-testid': 'health-refresh' }, 'Refresh'),
|
|
912
461
|
)
|
|
913
462
|
}
|
|
914
463
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
const [
|
|
918
|
-
const [
|
|
919
|
-
|
|
920
|
-
const
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
},
|
|
932
|
-
|
|
933
|
-
return React.createElement(
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
{ className: '
|
|
940
|
-
(
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
'data-testid': `tab-${k}`,
|
|
950
|
-
},
|
|
951
|
-
k,
|
|
952
|
-
k === 'review' && pending > 0
|
|
953
|
-
? React.createElement('span', { className: 'tabbadge', 'data-testid': 'tab-badge-review' }, String(pending))
|
|
954
|
-
: null,
|
|
955
|
-
),
|
|
464
|
+
// ── Health ─────────────────────────────────────────────────────────
|
|
465
|
+
function HealthView({ ctx }: {ctx:any}): React.ReactElement {
|
|
466
|
+
const [health,setHealth]=React.useState<any>(null)
|
|
467
|
+
const [loading,setLoading]=React.useState(true)
|
|
468
|
+
const [msg,setMsg]=React.useState('')
|
|
469
|
+
const load=React.useCallback(async()=>{
|
|
470
|
+
setLoading(true); setMsg('')
|
|
471
|
+
try{
|
|
472
|
+
const conn=(ctx as any).connection ?? (ctx as any).get?.('connection')
|
|
473
|
+
if(!conn?.rpc?.call) throw new Error('RPC not available')
|
|
474
|
+
const cwd=(ctx as any)?.sessions?.list?.getSnapshot?.()?.byId?.[(ctx as any)?.sessions?.list?.getSnapshot?.()?.current]?.cwd || ''
|
|
475
|
+
const res:any=await conn.rpc.call('/dsh-maestro-memory-health','get',{cwd})
|
|
476
|
+
const val=res?.ok===true?res.value:res
|
|
477
|
+
if(val && typeof val.project!=='undefined') setHealth(val); else setMsg(`unexpected: ${JSON.stringify(val).slice(0,120)}`)
|
|
478
|
+
} catch(e:any){ setMsg(`load failed: ${e?.message??String(e)}`) } finally{ setLoading(false) }
|
|
479
|
+
},[ctx])
|
|
480
|
+
React.useEffect(()=>{ load() },[load])
|
|
481
|
+
if(loading) return React.createElement('div',{className:'memx-muted'},'Loading health…')
|
|
482
|
+
if(msg) 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'))
|
|
483
|
+
if(!health) return React.createElement('div',{className:'memx-muted'},'No data')
|
|
484
|
+
const cov=health.project.coverage as number
|
|
485
|
+
const covColor = cov>=90 ? 'var(--dsw-alias-state-success-primary)' : cov>=50 ? 'var(--dsw-alias-state-warn-primary)' : 'var(--dsw-alias-state-error-primary)'
|
|
486
|
+
return React.createElement('div',{className:'memx-panel'},
|
|
487
|
+
React.createElement('div',{className:'memx-grid memx-grid-4'},
|
|
488
|
+
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`)),
|
|
489
|
+
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%)')),
|
|
490
|
+
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 as number[]).join(' · ')), React.createElement('div',{className:'memx-sm'}, 'entries per day')),
|
|
491
|
+
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+')),
|
|
492
|
+
),
|
|
493
|
+
health.fiveDim?React.createElement('div',{className:'memx-card'},
|
|
494
|
+
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)`),
|
|
495
|
+
React.createElement('div',{className:'memx-grid memx-grid-4'},
|
|
496
|
+
(['S','R','J','C','Safety'] as const).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])))),
|
|
497
|
+
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))),
|
|
956
498
|
),
|
|
499
|
+
):null,
|
|
500
|
+
React.createElement('div',{className:'memx-muted'}, `Longest ${health.longest.length} entries`),
|
|
501
|
+
React.createElement('div',{className:'memx-grid'},
|
|
502
|
+
...health.longest.map((it:any,i:number)=> React.createElement('div',{key:i, className:'memx-card', style:{flexDirection:'row', alignItems:'center', justifyContent:'space-between', gap:12}},
|
|
503
|
+
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)),
|
|
504
|
+
React.createElement('button',{onClick: async()=>{
|
|
505
|
+
try{
|
|
506
|
+
const conn2=(ctx as any).connection ?? (ctx as any).get?.('connection'); if(!conn2?.rpc?.call) throw new Error('RPC not available')
|
|
507
|
+
const res:any=await conn2.rpc.call('/dsh-maestro-memory-propose','add',{content:it.preview, reason:'promote from Health longest'}); const ok=res?.ok===true?res.value:res; setMsg(ok?.queued?`proposed (queue ${ok.queued})`:`proposed: ${JSON.stringify(ok).slice(0,80)}`)
|
|
508
|
+
} catch(e:any){ setMsg(`propose failed: ${e?.message??String(e)}`) }
|
|
509
|
+
}, className:'memx-btn memx-btn-primary', 'data-testid':`health-propose-${i}`, style:{whiteSpace:'nowrap', flex:'none'}}, 'Suggest as KEY'),
|
|
510
|
+
)),
|
|
957
511
|
),
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
: tab === 'review'
|
|
961
|
-
? React.createElement(ReviewQueueView, { ctx, onPendingChange: setPending })
|
|
962
|
-
: tab === 'todos'
|
|
963
|
-
? React.createElement(TodosView, { ctx })
|
|
964
|
-
: tab === 'health'
|
|
965
|
-
? React.createElement(HealthView, { ctx })
|
|
966
|
-
: React.createElement(SkillsView, { ctx }),
|
|
512
|
+
msg?React.createElement('div',{className:'memx-muted', style:{whiteSpace:'pre-wrap'}},msg):null,
|
|
513
|
+
React.createElement('button',{onClick:load, className:'memx-btn', style:{marginTop:4}, 'data-testid':'health-refresh'}, React.createElement(Ico,{d:ICO.refresh, size:14}), 'Refresh'),
|
|
967
514
|
)
|
|
968
515
|
}
|
|
969
516
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
517
|
+
// ── Root ───────────────────────────────────────────────────────────
|
|
518
|
+
function MemoryView({ ctx }: {ctx:any}): React.ReactElement {
|
|
519
|
+
const rpc=useRpc(ctx)
|
|
520
|
+
const [tab,setTab]=React.useState<'memory'|'review'|'todos'|'skills'|'health'>('memory')
|
|
521
|
+
const [pending,setPending]=React.useState(0)
|
|
522
|
+
const refreshPending=React.useCallback(async()=>{
|
|
523
|
+
try{ const res:any=await rpc('status',{}); setPending(typeof res?.queue==='number'?res.queue:0) } catch{ setPending(0) }
|
|
524
|
+
},[rpc])
|
|
525
|
+
React.useEffect(()=>{ refreshPending() },[refreshPending,tab])
|
|
526
|
+
|
|
527
|
+
const tabs: Array<{id:typeof tab; label:string; icon:string; count?:number}> = [
|
|
528
|
+
{id:'memory', label:'Memory', icon:ICO.mem},
|
|
529
|
+
{id:'review', label:'Queue', icon:ICO.inbox, count:pending},
|
|
530
|
+
{id:'todos', label:'Todos', icon:ICO.check},
|
|
531
|
+
{id:'skills', label:'Skills', icon:ICO.layers},
|
|
532
|
+
{id:'health', label:'Health', icon:ICO.activity},
|
|
533
|
+
]
|
|
534
|
+
|
|
535
|
+
return React.createElement('div',{className:'dshmem memx', style:{padding:'12px 12px 16px', background:'var(--dsw-alias-bg-base)', position:'relative', zIndex:1, isolation:'isolate' as any, minHeight:'100%', width:'100%'} as any},
|
|
536
|
+
React.createElement('style',null, MEM_CSS),
|
|
537
|
+
React.createElement('div',{className:'memx-header'},
|
|
538
|
+
React.createElement('div',{className:'memx-header-main'},
|
|
539
|
+
React.createElement('div',{className:'memx-title'}, React.createElement(Ico,{d:ICO.spark, size:18}), 'Memory'),
|
|
540
|
+
React.createElement('div',{className:'memx-subtitle'}, 'Durable layered memory — five tracks, gated writes, todos & skills'),
|
|
541
|
+
),
|
|
542
|
+
React.createElement('div',{className:'memx-header-actions'},
|
|
543
|
+
React.createElement('button',{onClick:refreshPending, className:'memx-iconbtn', 'aria-label':'Refresh status', title:'Refresh'}, React.createElement(Ico,{d:ICO.refresh, size:16})),
|
|
544
|
+
),
|
|
545
|
+
),
|
|
546
|
+
React.createElement('div',{className:'memx-layout'},
|
|
547
|
+
React.createElement('div',{className:'memx-nav', role:'tablist', 'aria-label':'Memory sections'},
|
|
548
|
+
...tabs.map(t=> React.createElement('button',{
|
|
549
|
+
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}`,
|
|
979
550
|
},
|
|
980
|
-
|
|
551
|
+
React.createElement(Ico,{d:t.icon}), t.label,
|
|
552
|
+
t.count!==undefined && t.count>0 ? React.createElement('span',{className:'memx-badge', 'data-testid':'tab-badge-review'}, String(t.count)) : null,
|
|
553
|
+
t.id==='memory'?React.createElement('span',{className:'memx-navcount'}, '5 tracks'):null,
|
|
554
|
+
)),
|
|
981
555
|
),
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
// (same technique as the mobile nav plugin).
|
|
993
|
-
ctx.effect(() => {
|
|
994
|
-
const conn = (ctx as any).connection ?? (ctx as any).get?.('connection')
|
|
995
|
-
if (!conn?.rpc?.call) return () => {}
|
|
996
|
-
const BADGE_ATTR = 'data-dshmem-header-badge'
|
|
997
|
-
const VIEW_ATTR = 'data-dshmem-memory-tab'
|
|
556
|
+
React.createElement('div',{className:'memx-panel', role:'tabpanel', id:`panel-${tab}`, 'aria-labelledby':`tab-${tab}`},
|
|
557
|
+
tab==='memory'?React.createElement(MemoryListView,{ctx}):
|
|
558
|
+
tab==='review'?React.createElement(ReviewQueueView,{ctx, onPendingChange:setPending}):
|
|
559
|
+
tab==='todos'?React.createElement(TodosView,{ctx}):
|
|
560
|
+
tab==='health'?React.createElement(HealthView,{ctx}):
|
|
561
|
+
React.createElement(SkillsView,{ctx}),
|
|
562
|
+
),
|
|
563
|
+
),
|
|
564
|
+
)
|
|
565
|
+
}
|
|
998
566
|
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
567
|
+
export function apply(ctx:any): void {
|
|
568
|
+
ctx.effect(()=>{
|
|
569
|
+
const dispose=ctx.slots.inject('conversation.view', ()=> ctx.slots.register({ name:'conversation.view', id:'maestro-memory', order:40, label:()=>'Memory' }, ()=> React.createElement(MemoryView,{ctx})))
|
|
570
|
+
return ()=>{ if(typeof dispose==='function') dispose() }
|
|
571
|
+
},'maestro-memory: view')
|
|
572
|
+
|
|
573
|
+
ctx.effect(()=>{
|
|
574
|
+
const conn=(ctx as any).connection ?? (ctx as any).get?.('connection')
|
|
575
|
+
if(!conn?.rpc?.call) return ()=>{}
|
|
576
|
+
const BADGE_ATTR='data-dshmem-header-badge'
|
|
577
|
+
const VIEW_ATTR='data-dshmem-memory-tab'
|
|
578
|
+
const findMemoryTab=(): HTMLElement|null=>{
|
|
579
|
+
const tabs=document.querySelectorAll('button[role="tab"]')
|
|
580
|
+
for(const tab of Array.from(tabs)){ if(tab.getAttribute(VIEW_ATTR)==='1') return tab as HTMLElement }
|
|
581
|
+
for(const tab of Array.from(tabs)){ if(tab.closest('.memx')) continue; if((tab.textContent??'').trim()==='Memory'){ tab.setAttribute(VIEW_ATTR,'1'); return tab as HTMLElement } }
|
|
1011
582
|
return null
|
|
1012
583
|
}
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
const tab
|
|
1016
|
-
if (
|
|
1017
|
-
|
|
1018
|
-
if (
|
|
1019
|
-
|
|
1020
|
-
return
|
|
1021
|
-
}
|
|
1022
|
-
if (existing && existing.textContent === String(count)) return
|
|
1023
|
-
if (existing) existing.remove()
|
|
1024
|
-
const badge = document.createElement('span')
|
|
1025
|
-
badge.setAttribute(BADGE_ATTR, '')
|
|
1026
|
-
badge.className = 'dshmem-header-badge'
|
|
1027
|
-
badge.textContent = String(count)
|
|
1028
|
-
tab.appendChild(badge)
|
|
584
|
+
const renderBadge=(count:number): void=>{
|
|
585
|
+
const tab=findMemoryTab(); if(!tab) return
|
|
586
|
+
const existing=tab.querySelector(`[${BADGE_ATTR}]`)
|
|
587
|
+
if(count<=0){ if(existing) existing.remove(); return }
|
|
588
|
+
if(existing && (existing.textContent===String(count))) return
|
|
589
|
+
if(existing) existing.remove()
|
|
590
|
+
const badge=document.createElement('span'); badge.setAttribute(BADGE_ATTR,''); badge.className='dshmem-header-badge'; badge.textContent=String(count); tab.appendChild(badge)
|
|
1029
591
|
}
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
document.head.appendChild(styleEl)
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
let disposed = false
|
|
1040
|
-
let lastCount = 0
|
|
1041
|
-
const refresh = async (): Promise<void> => {
|
|
1042
|
-
if (disposed) return
|
|
1043
|
-
try {
|
|
1044
|
-
const result: any = await conn.rpc.call(RPC_CHANNEL, 'status', {})
|
|
1045
|
-
const value = result?.ok === true ? result.value : null
|
|
1046
|
-
lastCount = typeof value?.queue === 'number' ? value.queue : 0
|
|
1047
|
-
renderBadge(lastCount)
|
|
1048
|
-
} catch {
|
|
1049
|
-
lastCount = 0
|
|
1050
|
-
renderBadge(0)
|
|
1051
|
-
}
|
|
592
|
+
let styleEl=document.getElementById('dshmem-header-style') as HTMLStyleElement|null
|
|
593
|
+
if(!styleEl){ styleEl=document.createElement('style'); styleEl.id='dshmem-header-style'; styleEl.textContent=HEADER_BADGE_CSS; document.head.appendChild(styleEl) }
|
|
594
|
+
let disposed=false; let lastCount=0
|
|
595
|
+
const refresh=async():Promise<void>=>{
|
|
596
|
+
if(disposed) return
|
|
597
|
+
try{ const result:any=await conn.rpc.call(RPC_CHANNEL,'status',{}); const value=result?.ok===true?result.value:null; lastCount= typeof value?.queue==='number'?value.queue:0; renderBadge(lastCount) } catch{ lastCount=0; renderBadge(0) }
|
|
1052
598
|
}
|
|
1053
|
-
|
|
1054
599
|
void refresh()
|
|
1055
|
-
const timer
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
if (disposed) return
|
|
1061
|
-
const tab = findMemoryTab()
|
|
1062
|
-
if (!tab) return
|
|
1063
|
-
const hasBadge = tab.querySelector(`[${BADGE_ATTR}]`)
|
|
1064
|
-
if (lastCount > 0 && !hasBadge) renderBadge(lastCount)
|
|
1065
|
-
else if (lastCount <= 0 && hasBadge) renderBadge(0)
|
|
600
|
+
const timer=window.setInterval(()=>{ void refresh() },8000)
|
|
601
|
+
const observer=new MutationObserver(()=>{
|
|
602
|
+
if(disposed) return; const tab=findMemoryTab(); if(!tab) return
|
|
603
|
+
const hasBadge=tab.querySelector(`[${BADGE_ATTR}]`)
|
|
604
|
+
if(lastCount>0 && !hasBadge) renderBadge(lastCount); else if(lastCount<=0 && hasBadge) renderBadge(0)
|
|
1066
605
|
})
|
|
1067
|
-
observer.observe(document.body,
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
disposed = true
|
|
1071
|
-
window.clearInterval(timer)
|
|
1072
|
-
observer.disconnect()
|
|
1073
|
-
styleEl?.remove()
|
|
1074
|
-
const tab = findMemoryTab()
|
|
1075
|
-
if (tab) tab.querySelector(`[${BADGE_ATTR}]`)?.remove()
|
|
1076
|
-
}
|
|
1077
|
-
}, 'maestro-memory: header-badge')
|
|
606
|
+
observer.observe(document.body,{childList:true,subtree:true})
|
|
607
|
+
return ()=>{ disposed=true; window.clearInterval(timer); observer.disconnect(); styleEl?.remove(); const tab=findMemoryTab(); if(tab) tab.querySelector(`[${BADGE_ATTR}]`)?.remove() }
|
|
608
|
+
},'maestro-memory: header-badge')
|
|
1078
609
|
}
|