@ddtcorex/dsh-maestro-memory 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/lib/client.js +220 -365
- package/lib/prompt/snapshot.d.ts.map +1 -1
- package/lib/prompt/snapshot.js +4 -1
- package/lib/prompt/snapshot.js.map +1 -1
- package/lib/types/client/index.d.ts +0 -4
- package/lib/types/client/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/client/index.tsx +527 -1003
- package/src/host/prompt/snapshot.ts +4 -1
package/src/client/index.tsx
CHANGED
|
@@ -1,1078 +1,602 @@
|
|
|
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
|
+
/* legacy contract for tests — inheritance via .dshmem */
|
|
48
34
|
.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); }
|
|
35
|
+
.dshmem button { font: inherit; cursor: pointer; transition: color .15s ease, background .15s ease, border-color .15s ease, opacity .15s ease; }
|
|
54
36
|
.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
|
-
.
|
|
37
|
+
.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; }
|
|
38
|
+
.memx { color: var(--dsw-alias-label-primary); font-size: 13px; line-height: 1.5; }
|
|
39
|
+
.memx * { box-sizing: border-box; }
|
|
40
|
+
.memx button { font: inherit; cursor: pointer; transition: color .15s ease, background .15s ease, border-color .15s ease, opacity .15s ease; }
|
|
41
|
+
.memx button:active { transform: translateY(.5px); }
|
|
42
|
+
.memx button:focus-visible { outline: 2px solid var(--dsw-alias-interactive-bg-active); outline-offset: 1px; }
|
|
43
|
+
.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; }
|
|
44
|
+
.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); }
|
|
45
|
+
.memx textarea { resize: vertical; }
|
|
46
|
+
@media (prefers-reduced-motion: reduce) { .memx button, .memx-card, .dshmem-header-badge { transition: none !important; animation: none !important; } }
|
|
47
|
+
|
|
48
|
+
/* header — refresh ngang hàng title (yêu cầu) */
|
|
49
|
+
.memx-header { display:flex; align-items:center; gap:12px; flex-wrap:nowrap; margin-bottom:14px; }
|
|
50
|
+
.memx-title { font-size:16px; font-weight:700; letter-spacing:-.01em; display:flex; align-items:center; gap:8px; }
|
|
51
|
+
.memx-title svg { flex:none; }
|
|
52
|
+
.memx-subtitle { color: var(--dsw-alias-label-secondary); font-size:12px; margin-top:2px; }
|
|
53
|
+
.memx-header-main { flex:1 1 auto; min-width:0; }
|
|
54
|
+
.memx-header-actions { flex:0 0 auto; display:flex; gap:8px; align-items:center; margin-left:auto; }
|
|
55
|
+
.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; }
|
|
56
|
+
@media (max-width:1023px) { .memx-search { max-width:100%; flex-basis:100%; } }
|
|
57
|
+
.memx-search input { border:none; background:transparent; padding:0; flex:1; min-width:0; box-shadow:none !important; }
|
|
58
|
+
.memx-search:focus-within { border-color:var(--dsw-alias-border-l2); box-shadow:0 0 0 2px var(--dsw-alias-interactive-bg-active); }
|
|
59
|
+
.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); }
|
|
60
|
+
.memx-iconbtn:hover { color:var(--dsw-alias-label-primary); border-color:var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-layer-2); }
|
|
61
|
+
.memx-iconbtn[aria-busy="true"] { opacity:.6; pointer-events:none; }
|
|
62
|
+
|
|
63
|
+
/* layout — align breakpoint with dsh-maestro-mobile (1023px) */
|
|
64
|
+
.memx-layout { display:grid; gap:14px; }
|
|
65
|
+
@media (min-width: 1024px) { .memx-layout { grid-template-columns: 200px 1fr; align-items:start; } }
|
|
66
|
+
/* nav */
|
|
67
|
+
.memx-nav { display:flex; gap:8px; overflow-x:auto; scrollbar-width:none; -webkit-overflow-scrolling:touch; padding-bottom:2px; }
|
|
68
|
+
.memx-nav::-webkit-scrollbar { display:none; }
|
|
69
|
+
@media (min-width: 1024px) { .memx-nav { flex-direction:column; position:sticky; top:8px; overflow:visible; gap:4px; } }
|
|
70
|
+
.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; }
|
|
71
|
+
.memx-navitem:hover { background:var(--dsw-alias-bg-layer-2); color:var(--dsw-alias-label-primary); border-color:var(--dsw-alias-border-l1); }
|
|
72
|
+
.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; }
|
|
73
|
+
.memx-navitem svg { width:16px; height:16px; flex:none; }
|
|
74
|
+
.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; }
|
|
75
|
+
.memx-navcount { margin-left:auto; color:var(--dsw-alias-label-secondary); font-size:11px; }
|
|
76
|
+
|
|
77
|
+
/* content */
|
|
78
|
+
.memx-panel { min-width:0; display:flex; flex-direction:column; gap:12px; }
|
|
79
|
+
.memx-toolbar { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
|
|
80
|
+
.memx-pills { display:flex; gap:8px; flex-wrap:wrap; }
|
|
81
|
+
.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; }
|
|
82
|
+
.memx-pill:hover { color:var(--dsw-alias-label-primary); border-color:var(--dsw-alias-border-l2); }
|
|
83
|
+
.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); }
|
|
84
|
+
.memx-field { display:flex; gap:8px; align-items:center; flex-wrap:wrap; flex:1 1 100%; min-width:0; }
|
|
85
|
+
.memx-input { height:44px; padding:0 10px; flex:1 1 140px; min-width:0; }
|
|
86
|
+
@media (max-width:480px) { .memx-field { flex-direction:column; align-items:stretch; } .memx-input { flex-basis:auto; width:100%; } }
|
|
87
|
+
.memx-input:disabled { opacity:.5; cursor:not-allowed; }
|
|
88
|
+
.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; }
|
|
89
|
+
.memx-btn:hover { border-color:var(--dsw-alias-border-l2); background:var(--dsw-alias-bg-layer-2); }
|
|
90
|
+
.memx-btn-primary { background:#06c; color:#fff; border-color:#06c; }
|
|
91
|
+
.memx-btn-primary:hover { background:#05a; border-color:#05a; color:#fff; }
|
|
92
|
+
.memx-btn-ghost { background:transparent; }
|
|
93
|
+
.memx-btn-danger { background:var(--dsw-alias-state-error-primary); color:#fff; border-color:transparent; }
|
|
94
|
+
.memx-btn:disabled { opacity:.5; cursor:not-allowed; }
|
|
95
|
+
|
|
96
|
+
/* bento / cards */
|
|
97
|
+
.memx-grid { display:grid; gap:10px; }
|
|
98
|
+
@media (min-width: 640px) { .memx-grid-2 { grid-template-columns: repeat(2, minmax(0,1fr)); } }
|
|
99
|
+
@media (min-width: 1024px) { .memx-grid-4 { grid-template-columns: repeat(4, minmax(0,1fr)); } }
|
|
100
|
+
.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; }
|
|
101
|
+
.memx-card:hover { border-color:var(--dsw-alias-border-l2); }
|
|
102
|
+
.memx-cardhead { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
|
|
103
|
+
.memx-kicker { text-transform:uppercase; letter-spacing:.04em; font-size:11px; color:var(--dsw-alias-label-secondary); font-weight:600; }
|
|
104
|
+
.memx-cardtitle { font-weight:600; }
|
|
105
|
+
.memx-muted { color:var(--dsw-alias-label-secondary); }
|
|
106
|
+
.memx-sm { font-size:12px; color:var(--dsw-alias-label-secondary); }
|
|
107
|
+
.memx-xs { font-size:11px; color:var(--dsw-alias-label-secondary); }
|
|
108
|
+
.memx-pre { white-space:pre-wrap; word-break:break-word; }
|
|
109
|
+
.memx-pre-sm { white-space:pre-wrap; word-break:break-word; font-size:12px; }
|
|
110
|
+
.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); }
|
|
111
|
+
.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); }
|
|
112
|
+
.memx-stat { text-align:left; }
|
|
113
|
+
.memx-statval { font-size:20px; font-weight:800; letter-spacing:-.02em; }
|
|
114
|
+
.memx-actions { display:flex; gap:8px; flex-wrap:wrap; }
|
|
115
|
+
@media (max-width: 639px) { .memx-actions button { flex:1; min-width:0; } }
|
|
116
|
+
|
|
117
|
+
/* forms inline */
|
|
118
|
+
.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; }
|
|
119
|
+
.memx-formrow { display:flex; gap:8px; flex-wrap:wrap; }
|
|
120
|
+
.memx-formrow > * { flex:1; min-width:120px; }
|
|
121
|
+
.memx-textarea { min-height:72px; padding:8px 10px; width:100%; }
|
|
122
|
+
.memx-select, .memx-date { height:44px; padding:0 8px; }
|
|
123
|
+
@media (max-width:1023px) { .memx { padding:12px 12px calc(16px + env(safe-area-inset-bottom,0px)) !important; } .memx-layout { gap:12px; } }
|
|
124
|
+
@media (min-width:1440px) { .memx { max-width:1280px; margin:0 auto; } }
|
|
83
125
|
`
|
|
84
126
|
|
|
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
127
|
const HEADER_BADGE_CSS = `
|
|
89
|
-
.dshmem-header-badge { display:
|
|
90
|
-
@keyframes dshmem-blink { 0%,
|
|
128
|
+
.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; }
|
|
129
|
+
@keyframes dshmem-blink { 0%,100%{opacity:1} 50%{opacity:.3} }
|
|
91
130
|
`
|
|
92
131
|
|
|
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
132
|
function sessionCwd(ctx: any): string {
|
|
99
133
|
try {
|
|
100
134
|
const snap = ctx?.sessions?.list?.getSnapshot?.()
|
|
101
135
|
const id: string | undefined = snap?.current
|
|
102
136
|
if (!id) return ''
|
|
103
137
|
return (snap?.byId?.[id]?.cwd as string) ?? ''
|
|
104
|
-
} catch {
|
|
105
|
-
return ''
|
|
106
|
-
}
|
|
138
|
+
} catch { return '' }
|
|
107
139
|
}
|
|
108
140
|
|
|
109
141
|
function useRpc(ctx: any) {
|
|
110
|
-
return React.useCallback(
|
|
111
|
-
(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
},
|
|
120
|
-
[ctx],
|
|
121
|
-
)
|
|
142
|
+
return React.useCallback((endpoint: string, payload: any) => {
|
|
143
|
+
const conn = (ctx as any).connection ?? (ctx as any).get?.('connection')
|
|
144
|
+
if (!conn?.rpc?.call) return Promise.reject(new Error('RPC not available'))
|
|
145
|
+
return conn.rpc.call(RPC_CHANNEL, endpoint, payload).then((result: any) => {
|
|
146
|
+
if (result?.ok === true) return result.value
|
|
147
|
+
const message = typeof result?.error?.message === 'string' ? result.error.message : 'RPC request failed'
|
|
148
|
+
throw new Error(message)
|
|
149
|
+
})
|
|
150
|
+
}, [ctx])
|
|
122
151
|
}
|
|
123
152
|
|
|
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
|
-
)
|
|
153
|
+
// tiny inline icons (no emoji) — stroke currentColor
|
|
154
|
+
function Ico({ d, size = 16, label }: { d: string; size?: number; label?: string }) {
|
|
155
|
+
return React.createElement('span', { 'aria-hidden': label ? undefined : true, 'aria-label': label, style: { display:'inline-flex', lineHeight:0 } },
|
|
156
|
+
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,
|
|
157
|
+
React.createElement('path', { d })))
|
|
158
|
+
}
|
|
159
|
+
const ICO = {
|
|
160
|
+
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',
|
|
161
|
+
inbox: 'M4 6h16v12H4z M4 6l8 7 8-7',
|
|
162
|
+
check: 'M9 12l2 2 4-4 M4 6h16v12H4z',
|
|
163
|
+
layers: 'M12 3l9 5-9 5-9-5 9-5z M3 12l9 5 9-5 M3 17l9 5 9-5',
|
|
164
|
+
activity: 'M3 12h4l3-8 4 16 3-8h4',
|
|
165
|
+
search: 'M11 18a7 7 0 1 1 0-14 7 7 0 0 1 0 14z M18 18l-3.5-3.5',
|
|
166
|
+
refresh: 'M21 12a9 9 0 1 1-2.6-6.4 M21 3v6h-6',
|
|
167
|
+
plus: 'M12 5v14 M5 12h14',
|
|
168
|
+
spark: 'M12 3l1.5 4.5L18 9l-4.5 1.5L12 15l-1.5-4.5L6 9l4.5-1.5z',
|
|
240
169
|
}
|
|
241
170
|
|
|
242
|
-
|
|
171
|
+
// ── Review ─────────────────────────────────────────────────────────
|
|
172
|
+
function ReviewQueueView({ ctx, onPendingChange }: { ctx:any; onPendingChange?:(n:number)=>void }) : React.ReactElement {
|
|
243
173
|
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('')
|
|
174
|
+
const [entries,setEntries]=React.useState<any[]>([])
|
|
175
|
+
const [loading,setLoading]=React.useState(true)
|
|
176
|
+
const [edits,setEdits]=React.useState<Record<number,string>>({})
|
|
177
|
+
const [msg,setMsg]=React.useState('')
|
|
178
|
+
const [q,setQ]=React.useState('')
|
|
262
179
|
|
|
263
|
-
const load = React.useCallback(async
|
|
180
|
+
const load = React.useCallback(async()=>{
|
|
264
181
|
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])
|
|
182
|
+
try{ const res:any=await rpc('queue.list',{}); const list=Array.isArray(res?.entries)?res.entries:[]; setEntries(list); onPendingChange?.(list.length) }
|
|
183
|
+
catch(e:any){ setMsg(`load failed: ${e?.message??String(e)}`) } finally{ setLoading(false) }
|
|
184
|
+
},[rpc,onPendingChange])
|
|
185
|
+
React.useEffect(()=>{ load() },[load])
|
|
293
186
|
|
|
294
|
-
const
|
|
295
|
-
const content = draftContent.trim()
|
|
296
|
-
if (!content) {
|
|
297
|
-
setMsg('content required')
|
|
298
|
-
return
|
|
299
|
-
}
|
|
187
|
+
const decide = React.useCallback(async(action:'approve'|'reject'|'archive', index:number)=>{
|
|
300
188
|
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',
|
|
189
|
+
const payload:any={ action, indices:[index] }
|
|
190
|
+
if(action==='approve' && edits[index]!==undefined && edits[index].trim()!=='') payload.edits={ [String(index)]: edits[index] }
|
|
191
|
+
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'}`) }
|
|
192
|
+
catch(e:any){ setMsg(`error: ${e?.message??String(e)}`) }
|
|
193
|
+
},[rpc,edits,load])
|
|
194
|
+
|
|
195
|
+
const filtered = React.useMemo(()=>{
|
|
196
|
+
const t=q.trim().toLowerCase(); if(!t) return entries
|
|
197
|
+
return entries.filter((e:any)=> `${e.target} ${e.content} ${e.reason}`.toLowerCase().includes(t))
|
|
198
|
+
},[entries,q])
|
|
199
|
+
|
|
200
|
+
if(loading) return React.createElement('div', {className:'memx-muted'}, 'Loading queue…')
|
|
201
|
+
return React.createElement('div', {className:'memx-panel'},
|
|
202
|
+
React.createElement('div', {className:'memx-toolbar'},
|
|
203
|
+
React.createElement('div', {className:'memx-search', role:'search', 'aria-label':'Filter queue'},
|
|
204
|
+
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
205
|
),
|
|
206
|
+
React.createElement('button',{onClick:load, className:'memx-btn memx-btn-ghost', 'aria-label':'Refresh queue'}, React.createElement(Ico,{d:ICO.refresh}), ' Refresh'),
|
|
207
|
+
React.createElement('span',{className:'memx-muted'}, `${filtered.length}/${entries.length} pending`),
|
|
538
208
|
),
|
|
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
|
-
),
|
|
209
|
+
filtered.length===0 ? React.createElement('div',{className:'memx-empty'},
|
|
210
|
+
React.createElement('div',{style:{fontWeight:600, color:'var(--dsw-alias-label-primary)'}}, entries.length===0?'No pending suggestions':'No results'),
|
|
211
|
+
React.createElement('div',{className:'memx-muted'}, entries.length===0?'Model proposals will appear here for your approval.':'Try a different filter.'),
|
|
212
|
+
) : React.createElement('div',{className:'memx-grid'},
|
|
213
|
+
...filtered.map((e:any)=>{
|
|
214
|
+
const number = entries.indexOf(e)+1
|
|
215
|
+
return React.createElement('div',{key:number, className:'memx-card'},
|
|
216
|
+
React.createElement('div',{className:'memx-cardhead'},
|
|
217
|
+
React.createElement('span',{className:'memx-chip'}, `#${number} · ${e.target}`),
|
|
218
|
+
e.reason ? React.createElement('span',{className:'memx-sm'}, e.reason.slice(0,80)) : null,
|
|
219
|
+
),
|
|
220
|
+
React.createElement('div',{className:'memx-pre'}, e.content),
|
|
221
|
+
e.reason ? React.createElement('div',{className:'memx-sm'}, `Reason: ${e.reason}`) : null,
|
|
222
|
+
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}),
|
|
223
|
+
React.createElement('div',{className:'memx-actions'},
|
|
224
|
+
React.createElement('button',{onClick:()=>decide('approve',number), 'data-testid':`approve-${number}`, className:'memx-btn memx-btn-primary'}, 'Approve'),
|
|
225
|
+
React.createElement('button',{onClick:()=>decide('reject',number), 'data-testid':`reject-${number}`, className:'memx-btn'}, 'Reject'),
|
|
226
|
+
React.createElement('button',{onClick:()=>decide('archive',number), 'data-testid':`archive-${number}`, className:'memx-btn'}, 'Archive'),
|
|
675
227
|
),
|
|
228
|
+
)
|
|
229
|
+
})
|
|
230
|
+
),
|
|
231
|
+
msg ? React.createElement('div',{className:'memx-muted', style:{whiteSpace:'pre-wrap'}}, msg) : null,
|
|
676
232
|
)
|
|
677
233
|
}
|
|
678
234
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
const
|
|
682
|
-
const [
|
|
683
|
-
const [
|
|
684
|
-
|
|
685
|
-
const
|
|
686
|
-
|
|
235
|
+
// ── Todos ──────────────────────────────────────────────────────────
|
|
236
|
+
function TodosView({ ctx }: {ctx:any}): React.ReactElement {
|
|
237
|
+
const rpc=useRpc(ctx)
|
|
238
|
+
const [items,setItems]=React.useState<any[]>([])
|
|
239
|
+
const [loading,setLoading]=React.useState(true)
|
|
240
|
+
const [msg,setMsg]=React.useState('')
|
|
241
|
+
const [target,setTarget]=React.useState<string>('all')
|
|
242
|
+
const [showAll,setShowAll]=React.useState(false)
|
|
243
|
+
const [showPast,setShowPast]=React.useState(false)
|
|
244
|
+
const [showExpired,setShowExpired]=React.useState(false)
|
|
245
|
+
const [q,setQ]=React.useState('')
|
|
246
|
+
const [draftContent,setDraftContent]=React.useState('')
|
|
247
|
+
const [draftTarget,setDraftTarget]=React.useState<string>('work')
|
|
248
|
+
const [draftDue,setDraftDue]=React.useState('')
|
|
249
|
+
const [draftQuadrant,setDraftQuadrant]=React.useState<string>('')
|
|
250
|
+
const [draftCat,setDraftCat]=React.useState('')
|
|
251
|
+
const [openAdd,setOpenAdd]=React.useState(true)
|
|
252
|
+
const [editId,setEditId]=React.useState<string|null>(null)
|
|
253
|
+
const [editContent,setEditContent]=React.useState('')
|
|
254
|
+
const [editDue,setEditDue]=React.useState('')
|
|
255
|
+
const [editQuadrant,setEditQuadrant]=React.useState('')
|
|
256
|
+
const [editStatus,setEditStatus]=React.useState('')
|
|
257
|
+
const [editCat,setEditCat]=React.useState('')
|
|
258
|
+
|
|
259
|
+
const load=React.useCallback(async()=>{
|
|
260
|
+
setLoading(true); setMsg('')
|
|
261
|
+
try{
|
|
262
|
+
const res:any=await rpc('todo.list',{ target: target!=='all'?target:undefined, opts:{ all:showAll, past:showPast, expired:showExpired } })
|
|
263
|
+
setItems(Array.isArray(res?.items)?res.items:[])
|
|
264
|
+
if(res?.hint) setMsg(res.hint)
|
|
265
|
+
} catch(e:any){ setMsg(`load failed: ${e?.message??String(e)}`) } finally{ setLoading(false) }
|
|
266
|
+
},[rpc,target,showAll,showPast,showExpired])
|
|
267
|
+
React.useEffect(()=>{ load() },[load])
|
|
268
|
+
|
|
269
|
+
const filtered = React.useMemo(()=>{
|
|
270
|
+
const t=q.trim().toLowerCase(); if(!t) return items
|
|
271
|
+
return items.filter((it:any)=> `${it.text} ${it.target} ${it.cat} ${it.status}`.toLowerCase().includes(t))
|
|
272
|
+
},[items,q])
|
|
273
|
+
|
|
274
|
+
const addTodo=React.useCallback(async()=>{
|
|
275
|
+
const content=draftContent.trim(); if(!content){ setMsg('content required'); return }
|
|
687
276
|
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
|
-
|
|
277
|
+
try{
|
|
278
|
+
const res:any=await rpc('todo.mutate',{ action:'add', target:draftTarget, content, due:draftDue||undefined, quadrant:draftQuadrant||undefined, cat:draftCat||undefined })
|
|
279
|
+
if(res?.ok){ setDraftContent(''); setDraftDue(''); setDraftQuadrant(''); setDraftCat(''); setMsg(`added (id: ${res.id})`); await load() }
|
|
280
|
+
else setMsg(`add failed: ${res?.message??res?.error??'unknown'}`)
|
|
281
|
+
} catch(e:any){ setMsg(`error: ${e?.message??String(e)}`) }
|
|
282
|
+
},[rpc,draftContent,draftTarget,draftDue,draftQuadrant,draftCat,load])
|
|
283
|
+
|
|
284
|
+
const doneTodo=React.useCallback(async(item:any)=>{
|
|
285
|
+
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)}`) }
|
|
286
|
+
},[rpc,load])
|
|
287
|
+
const undoTodo=React.useCallback(async(item:any)=>{
|
|
288
|
+
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)}`) }
|
|
289
|
+
},[rpc,load])
|
|
290
|
+
const removeTodo=React.useCallback(async(item:any)=>{
|
|
291
|
+
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)}`) }
|
|
292
|
+
},[rpc,load])
|
|
293
|
+
const startEdit=(item:any)=>{ setEditId(item.id); setEditContent(item.text??''); setEditDue(item.due??''); setEditQuadrant(item.quadrant??''); setEditStatus(item.status??'pending'); setEditCat(item.cat??'') }
|
|
294
|
+
const saveEdit=React.useCallback(async(item:any)=>{
|
|
295
|
+
try{
|
|
296
|
+
const patch:any={}
|
|
297
|
+
if(editContent!==item.text) patch.content=editContent
|
|
298
|
+
if(editQuadrant!==(item.quadrant??'')) patch.quadrant=editQuadrant||null
|
|
299
|
+
if(editDue!==(item.due??'')) patch.due=editDue||null
|
|
300
|
+
if(editStatus!==item.status) patch.status=editStatus
|
|
301
|
+
if(editCat!==(item.cat??'')) patch.cat=editCat||null
|
|
302
|
+
const res:any=await rpc('todo.mutate',{ action:'update', target:item.target, id:item.id, ...patch })
|
|
303
|
+
if(res?.ok){ setEditId(null); setMsg(res.message??'updated'); await load() } else setMsg(`update failed: ${res?.message??res?.error}`)
|
|
304
|
+
} catch(e:any){ setMsg(`error: ${e?.message??String(e)}`) }
|
|
305
|
+
},[rpc,editContent,editDue,editQuadrant,editStatus,editCat,load])
|
|
306
|
+
|
|
307
|
+
return React.createElement('div',{className:'memx-panel'},
|
|
308
|
+
React.createElement('div',{className:'memx-toolbar'},
|
|
309
|
+
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))),
|
|
310
|
+
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'})),
|
|
311
|
+
),
|
|
312
|
+
React.createElement('div',{className:'memx-toolbar'},
|
|
313
|
+
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'),
|
|
314
|
+
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'),
|
|
315
|
+
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'),
|
|
316
|
+
React.createElement('button',{onClick:load, className:'memx-btn memx-btn-ghost', 'data-testid':'todo-refresh'}, React.createElement(Ico,{d:ICO.refresh}), 'Refresh'),
|
|
317
|
+
),
|
|
318
|
+
React.createElement('div',{className:'memx-form'},
|
|
319
|
+
React.createElement('button',{onClick:()=>setOpenAdd(v=>!v), className:'memx-btn memx-btn-ghost', style:{justifyContent:'space-between'}, 'aria-expanded':openAdd},
|
|
320
|
+
React.createElement('span',{style:{display:'flex',gap:8,alignItems:'center'}}, React.createElement(Ico,{d:ICO.plus}), 'Add todo'),
|
|
321
|
+
React.createElement('span',{className:'memx-muted'}, openAdd?'Hide':'Show'),
|
|
322
|
+
),
|
|
323
|
+
openAdd ? React.createElement(React.Fragment,null,
|
|
324
|
+
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'}),
|
|
325
|
+
React.createElement('div',{className:'memx-formrow'},
|
|
326
|
+
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))),
|
|
327
|
+
React.createElement('select',{value:draftQuadrant, onChange:(e:any)=>setDraftQuadrant(e.target.value), className:'memx-select', 'aria-label':'Quadrant', 'data-testid':'todo-add-quadrant'},
|
|
328
|
+
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')),
|
|
329
|
+
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'}),
|
|
330
|
+
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'}),
|
|
331
|
+
),
|
|
332
|
+
React.createElement('button',{onClick:addTodo, className:'memx-btn memx-btn-primary', 'data-testid':'todo-add-btn'}, 'Add'),
|
|
333
|
+
) : null,
|
|
716
334
|
),
|
|
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,
|
|
335
|
+
msg ? React.createElement('div',{className:'memx-muted', style:{whiteSpace:'pre-wrap'}}, msg) : null,
|
|
336
|
+
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.') :
|
|
337
|
+
React.createElement('div',null,
|
|
338
|
+
React.createElement('div',{className:'memx-muted', style:{marginBottom:6}}, `${filtered.length} todos${!showAll && filtered.length===8?' (smart view ≤8)':''}`),
|
|
339
|
+
React.createElement('div',{className:'memx-grid'},
|
|
340
|
+
...filtered.map((it:any)=> React.createElement('div',{key:it.id, className:'memx-card'},
|
|
341
|
+
React.createElement('div',{style:{display:'flex',gap:6,flexWrap:'wrap'}},
|
|
342
|
+
React.createElement('span',{className:'memx-chip'}, it.target),
|
|
343
|
+
it.quadrant?React.createElement('span',{className:'memx-chip'}, it.quadrant):null,
|
|
344
|
+
it.due?React.createElement('span',{className:'memx-chip'}, `due ${it.due}`):null,
|
|
345
|
+
it.status!=='pending'?React.createElement('span',{className:'memx-chip'}, it.status):null,
|
|
346
|
+
it.cat?React.createElement('span',{className:'memx-chip'}, it.cat):null,
|
|
738
347
|
),
|
|
739
|
-
|
|
348
|
+
editId===it.id ? React.createElement('div',null,
|
|
349
|
+
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}`}),
|
|
350
|
+
React.createElement('div',{className:'memx-formrow', style:{marginTop:8}},
|
|
351
|
+
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')),
|
|
352
|
+
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}`}),
|
|
353
|
+
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))),
|
|
354
|
+
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}`}),
|
|
355
|
+
),
|
|
356
|
+
React.createElement('div',{className:'memx-actions', style:{marginTop:8}},
|
|
357
|
+
React.createElement('button',{onClick:()=>saveEdit(it), className:'memx-btn memx-btn-primary', 'data-testid':`todo-save-${it.id}`}, 'Save'),
|
|
358
|
+
React.createElement('button',{onClick:()=>setEditId(null), className:'memx-btn', 'data-testid':`todo-cancel-${it.id}`}, 'Cancel'),
|
|
359
|
+
),
|
|
360
|
+
) : React.createElement(React.Fragment,null,
|
|
361
|
+
React.createElement('div',{className:'memx-pre'}, it.text),
|
|
362
|
+
React.createElement('div',{className:'memx-actions', style:{marginTop:8}},
|
|
363
|
+
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'),
|
|
364
|
+
React.createElement('button',{onClick:()=>startEdit(it), className:'memx-btn', 'data-testid':`todo-edit-${it.id}`}, 'Edit'),
|
|
365
|
+
React.createElement('button',{onClick:()=>removeTodo(it), className:'memx-btn', 'data-testid':`todo-remove-${it.id}`}, 'Remove'),
|
|
366
|
+
),
|
|
367
|
+
),
|
|
368
|
+
it.doneAt||it.past?React.createElement('div',{className:'memx-xs'}, [it.doneAt?`done:${it.doneAt}`:'', it.past?`past ${it.day}`:''].filter(Boolean).join(' · ')):null,
|
|
369
|
+
))
|
|
740
370
|
),
|
|
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.'),
|
|
371
|
+
),
|
|
744
372
|
)
|
|
745
373
|
}
|
|
746
374
|
|
|
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'),
|
|
375
|
+
// ── Skills ─────────────────────────────────────────────────────────
|
|
376
|
+
function SkillsView({ ctx }: {ctx:any}): React.ReactElement {
|
|
377
|
+
const rpc=useRpc(ctx)
|
|
378
|
+
const [entries,setEntries]=React.useState<any[]>([])
|
|
379
|
+
const [loading,setLoading]=React.useState(true)
|
|
380
|
+
const [msg,setMsg]=React.useState('')
|
|
381
|
+
const [q,setQ]=React.useState('')
|
|
382
|
+
const load=React.useCallback(async()=>{
|
|
383
|
+
setLoading(true); setMsg('')
|
|
384
|
+
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'}`) }
|
|
385
|
+
catch(e:any){ setMsg(`error: ${e?.message??String(e)}`) } finally{ setLoading(false) }
|
|
386
|
+
},[rpc])
|
|
387
|
+
React.useEffect(()=>{ load() },[load])
|
|
388
|
+
const filtered=React.useMemo(()=>{
|
|
389
|
+
const t=q.trim().toLowerCase(); if(!t) return entries
|
|
390
|
+
return entries.filter((e:any)=> `${e.name} ${e.description} ${e.origin}`.toLowerCase().includes(t))
|
|
391
|
+
},[entries,q])
|
|
392
|
+
if(loading) return React.createElement('div',{className:'memx-muted'},'Loading skills…')
|
|
393
|
+
return React.createElement('div',{className:'memx-panel'},
|
|
394
|
+
React.createElement('div',{className:'memx-toolbar'},
|
|
395
|
+
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'})),
|
|
396
|
+
React.createElement('button',{onClick:load, className:'memx-btn memx-btn-ghost', 'data-testid':'skills-refresh'}, React.createElement(Ico,{d:ICO.refresh}), 'Refresh'),
|
|
812
397
|
),
|
|
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
|
-
),
|
|
398
|
+
React.createElement('div',{className:'memx-muted'}, `${filtered.length}/${entries.length} skills — read-only, no mutation`),
|
|
399
|
+
filtered.length===0 ? React.createElement('div',{className:'memx-empty'}, msg||'No skills') :
|
|
400
|
+
React.createElement('div',{className:'memx-grid memx-grid-2'},
|
|
401
|
+
...filtered.map((e:any)=> React.createElement('div',{key:e.name, className:'memx-card'},
|
|
402
|
+
React.createElement('div',{className:'memx-cardtitle', style:{display:'flex',gap:8,alignItems:'center'}}, React.createElement(Ico,{d:ICO.layers, size:14}), e.name),
|
|
403
|
+
React.createElement('div',{className:'memx-sm'}, `[${e.origin}] ${e.path}`),
|
|
404
|
+
React.createElement('div',{className:'memx-pre-sm'}, e.description),
|
|
405
|
+
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,
|
|
406
|
+
))
|
|
407
|
+
),
|
|
408
|
+
msg?React.createElement('div',{className:'memx-muted', style:{marginTop:8}},msg):null,
|
|
830
409
|
)
|
|
831
410
|
}
|
|
832
411
|
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
const
|
|
836
|
-
const [
|
|
837
|
-
const
|
|
412
|
+
// ── Memory List ────────────────────────────────────────────────────
|
|
413
|
+
function MemoryListView({ ctx }: {ctx:any}): React.ReactElement {
|
|
414
|
+
const rpc=useRpc(ctx)
|
|
415
|
+
const [track,setTrack]=React.useState<string>('key')
|
|
416
|
+
const [cwd,setCwd]=React.useState<string>(()=>sessionCwd(ctx))
|
|
417
|
+
const [entries,setEntries]=React.useState<string[]>([])
|
|
418
|
+
const [loading,setLoading]=React.useState(true)
|
|
419
|
+
const [msg,setMsg]=React.useState('')
|
|
420
|
+
const [q,setQ]=React.useState('')
|
|
421
|
+
const needsCwd=track==='key'||track==='project'
|
|
422
|
+
const load=React.useCallback(async()=>{
|
|
838
423
|
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)'),
|
|
424
|
+
try{
|
|
425
|
+
if(needsCwd && !cwd.trim()){ setEntries([]); setMsg('cwd required for key/project'); setLoading(false); return }
|
|
426
|
+
const res:any=await rpc('memory.list',{ target:track, cwd: needsCwd?cwd.trim():undefined })
|
|
427
|
+
setEntries(Array.isArray(res?.entries)?res.entries:[])
|
|
428
|
+
} catch(e:any){ setMsg(`load failed: ${e?.message??String(e)}`) } finally{ setLoading(false) }
|
|
429
|
+
},[rpc,track,cwd,needsCwd])
|
|
430
|
+
React.useEffect(()=>{ load() },[load])
|
|
431
|
+
|
|
432
|
+
const filtered=React.useMemo(()=>{
|
|
433
|
+
const t=q.trim().toLowerCase(); if(!t) return entries
|
|
434
|
+
return entries.filter(e=> e.toLowerCase().includes(t))
|
|
435
|
+
},[entries,q])
|
|
436
|
+
|
|
437
|
+
return React.createElement('div',{className:'memx-panel'},
|
|
438
|
+
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))),
|
|
439
|
+
React.createElement('div',{className:'memx-toolbar'},
|
|
440
|
+
React.createElement('div',{className:'memx-field'},
|
|
441
|
+
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'}),
|
|
442
|
+
React.createElement('button',{onClick:load, className:'memx-btn', 'data-testid':'mem-refresh'}, React.createElement(Ico,{d:ICO.refresh, size:14}), 'Refresh'),
|
|
876
443
|
),
|
|
444
|
+
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
445
|
),
|
|
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)),
|
|
446
|
+
msg?React.createElement('div',{className:'memx-muted'},msg):null,
|
|
447
|
+
loading ? React.createElement('div',{className:'memx-muted'},'Loading memory…') : filtered.length===0 ? React.createElement('div',{className:'memx-empty'}, '(no entries)') :
|
|
448
|
+
React.createElement('div',null,
|
|
449
|
+
React.createElement('div',{className:'memx-muted', style:{marginBottom:6}}, `${filtered.length}/${entries.length} entries`),
|
|
450
|
+
React.createElement('div',{className:'memx-grid'},
|
|
451
|
+
...filtered.map((e:string, idx:number)=> React.createElement('div',{key:idx, className:'memx-card'}, React.createElement('div',{className:'memx-pre'}, e)) ),
|
|
888
452
|
),
|
|
889
453
|
),
|
|
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
454
|
)
|
|
913
455
|
}
|
|
914
456
|
|
|
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
|
-
),
|
|
457
|
+
// ── Health ─────────────────────────────────────────────────────────
|
|
458
|
+
function HealthView({ ctx }: {ctx:any}): React.ReactElement {
|
|
459
|
+
const [health,setHealth]=React.useState<any>(null)
|
|
460
|
+
const [loading,setLoading]=React.useState(true)
|
|
461
|
+
const [msg,setMsg]=React.useState('')
|
|
462
|
+
const load=React.useCallback(async()=>{
|
|
463
|
+
setLoading(true); setMsg('')
|
|
464
|
+
try{
|
|
465
|
+
const conn=(ctx as any).connection ?? (ctx as any).get?.('connection')
|
|
466
|
+
if(!conn?.rpc?.call) throw new Error('RPC not available')
|
|
467
|
+
const cwd=(ctx as any)?.sessions?.list?.getSnapshot?.()?.byId?.[(ctx as any)?.sessions?.list?.getSnapshot?.()?.current]?.cwd || ''
|
|
468
|
+
const res:any=await conn.rpc.call('/dsh-maestro-memory-health','get',{cwd})
|
|
469
|
+
const val=res?.ok===true?res.value:res
|
|
470
|
+
if(val && typeof val.project!=='undefined') setHealth(val); else setMsg(`unexpected: ${JSON.stringify(val).slice(0,120)}`)
|
|
471
|
+
} catch(e:any){ setMsg(`load failed: ${e?.message??String(e)}`) } finally{ setLoading(false) }
|
|
472
|
+
},[ctx])
|
|
473
|
+
React.useEffect(()=>{ load() },[load])
|
|
474
|
+
if(loading) return React.createElement('div',{className:'memx-muted'},'Loading health…')
|
|
475
|
+
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'))
|
|
476
|
+
if(!health) return React.createElement('div',{className:'memx-muted'},'No data')
|
|
477
|
+
const cov=health.project.coverage as number
|
|
478
|
+
const covColor = cov>=90 ? 'var(--dsw-alias-state-success-primary)' : cov>=50 ? 'var(--dsw-alias-state-warn-primary)' : 'var(--dsw-alias-state-error-primary)'
|
|
479
|
+
return React.createElement('div',{className:'memx-panel'},
|
|
480
|
+
React.createElement('div',{className:'memx-grid memx-grid-4'},
|
|
481
|
+
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`)),
|
|
482
|
+
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%)')),
|
|
483
|
+
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')),
|
|
484
|
+
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+')),
|
|
485
|
+
),
|
|
486
|
+
health.fiveDim?React.createElement('div',{className:'memx-card'},
|
|
487
|
+
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)`),
|
|
488
|
+
React.createElement('div',{className:'memx-grid memx-grid-4'},
|
|
489
|
+
(['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])))),
|
|
490
|
+
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
491
|
),
|
|
492
|
+
):null,
|
|
493
|
+
React.createElement('div',{className:'memx-muted'}, `Longest ${health.longest.length} entries`),
|
|
494
|
+
React.createElement('div',{className:'memx-grid'},
|
|
495
|
+
...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}},
|
|
496
|
+
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)),
|
|
497
|
+
React.createElement('button',{onClick: async()=>{
|
|
498
|
+
try{
|
|
499
|
+
const conn2=(ctx as any).connection ?? (ctx as any).get?.('connection'); if(!conn2?.rpc?.call) throw new Error('RPC not available')
|
|
500
|
+
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)}`)
|
|
501
|
+
} catch(e:any){ setMsg(`propose failed: ${e?.message??String(e)}`) }
|
|
502
|
+
}, className:'memx-btn memx-btn-primary', 'data-testid':`health-propose-${i}`, style:{whiteSpace:'nowrap', flex:'none'}}, 'Suggest as KEY'),
|
|
503
|
+
)),
|
|
957
504
|
),
|
|
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 }),
|
|
505
|
+
msg?React.createElement('div',{className:'memx-muted', style:{whiteSpace:'pre-wrap'}},msg):null,
|
|
506
|
+
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
507
|
)
|
|
968
508
|
}
|
|
969
509
|
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
510
|
+
// ── Root ───────────────────────────────────────────────────────────
|
|
511
|
+
function MemoryView({ ctx }: {ctx:any}): React.ReactElement {
|
|
512
|
+
const rpc=useRpc(ctx)
|
|
513
|
+
const [tab,setTab]=React.useState<'memory'|'review'|'todos'|'skills'|'health'>('memory')
|
|
514
|
+
const [pending,setPending]=React.useState(0)
|
|
515
|
+
const refreshPending=React.useCallback(async()=>{
|
|
516
|
+
try{ const res:any=await rpc('status',{}); setPending(typeof res?.queue==='number'?res.queue:0) } catch{ setPending(0) }
|
|
517
|
+
},[rpc])
|
|
518
|
+
React.useEffect(()=>{ refreshPending() },[refreshPending,tab])
|
|
519
|
+
|
|
520
|
+
const tabs: Array<{id:typeof tab; label:string; icon:string; count?:number}> = [
|
|
521
|
+
{id:'memory', label:'Memory', icon:ICO.mem},
|
|
522
|
+
{id:'review', label:'Queue', icon:ICO.inbox, count:pending},
|
|
523
|
+
{id:'todos', label:'Todos', icon:ICO.check},
|
|
524
|
+
{id:'skills', label:'Skills', icon:ICO.layers},
|
|
525
|
+
{id:'health', label:'Health', icon:ICO.activity},
|
|
526
|
+
]
|
|
527
|
+
|
|
528
|
+
return React.createElement('div',{className:'dshmem memx', style:{padding:'12px 12px 16px'}},
|
|
529
|
+
React.createElement('style',null, MEM_CSS),
|
|
530
|
+
React.createElement('div',{className:'memx-header'},
|
|
531
|
+
React.createElement('div',{className:'memx-header-main'},
|
|
532
|
+
React.createElement('div',{className:'memx-title'}, React.createElement(Ico,{d:ICO.spark, size:18}), 'Memory'),
|
|
533
|
+
React.createElement('div',{className:'memx-subtitle'}, 'Durable layered memory — five tracks, gated writes, todos & skills'),
|
|
534
|
+
),
|
|
535
|
+
React.createElement('div',{className:'memx-header-actions'},
|
|
536
|
+
React.createElement('button',{onClick:refreshPending, className:'memx-iconbtn', 'aria-label':'Refresh status', title:'Refresh'}, React.createElement(Ico,{d:ICO.refresh, size:16})),
|
|
537
|
+
),
|
|
538
|
+
),
|
|
539
|
+
React.createElement('div',{className:'memx-layout'},
|
|
540
|
+
React.createElement('div',{className:'memx-nav', role:'tablist', 'aria-label':'Memory sections'},
|
|
541
|
+
...tabs.map(t=> React.createElement('button',{
|
|
542
|
+
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
543
|
},
|
|
980
|
-
|
|
544
|
+
React.createElement(Ico,{d:t.icon}), t.label,
|
|
545
|
+
t.count!==undefined && t.count>0 ? React.createElement('span',{className:'memx-badge', 'data-testid':'tab-badge-review'}, String(t.count)) : null,
|
|
546
|
+
t.id==='memory'?React.createElement('span',{className:'memx-navcount'}, '5 tracks'):null,
|
|
547
|
+
)),
|
|
981
548
|
),
|
|
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'
|
|
549
|
+
React.createElement('div',{className:'memx-panel', role:'tabpanel', id:`panel-${tab}`, 'aria-labelledby':`tab-${tab}`},
|
|
550
|
+
tab==='memory'?React.createElement(MemoryListView,{ctx}):
|
|
551
|
+
tab==='review'?React.createElement(ReviewQueueView,{ctx, onPendingChange:setPending}):
|
|
552
|
+
tab==='todos'?React.createElement(TodosView,{ctx}):
|
|
553
|
+
tab==='health'?React.createElement(HealthView,{ctx}):
|
|
554
|
+
React.createElement(SkillsView,{ctx}),
|
|
555
|
+
),
|
|
556
|
+
),
|
|
557
|
+
)
|
|
558
|
+
}
|
|
998
559
|
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
560
|
+
export function apply(ctx:any): void {
|
|
561
|
+
ctx.effect(()=>{
|
|
562
|
+
const dispose=ctx.slots.inject('conversation.view', ()=> ctx.slots.register({ name:'conversation.view', id:'maestro-memory', order:40, label:()=>'Memory' }, ()=> React.createElement(MemoryView,{ctx})))
|
|
563
|
+
return ()=>{ if(typeof dispose==='function') dispose() }
|
|
564
|
+
},'maestro-memory: view')
|
|
565
|
+
|
|
566
|
+
ctx.effect(()=>{
|
|
567
|
+
const conn=(ctx as any).connection ?? (ctx as any).get?.('connection')
|
|
568
|
+
if(!conn?.rpc?.call) return ()=>{}
|
|
569
|
+
const BADGE_ATTR='data-dshmem-header-badge'
|
|
570
|
+
const VIEW_ATTR='data-dshmem-memory-tab'
|
|
571
|
+
const findMemoryTab=(): HTMLElement|null=>{
|
|
572
|
+
const tabs=document.querySelectorAll('button[role="tab"]')
|
|
573
|
+
for(const tab of Array.from(tabs)){ if(tab.getAttribute(VIEW_ATTR)==='1') return tab as HTMLElement }
|
|
574
|
+
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
575
|
return null
|
|
1012
576
|
}
|
|
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)
|
|
577
|
+
const renderBadge=(count:number): void=>{
|
|
578
|
+
const tab=findMemoryTab(); if(!tab) return
|
|
579
|
+
const existing=tab.querySelector(`[${BADGE_ATTR}]`)
|
|
580
|
+
if(count<=0){ if(existing) existing.remove(); return }
|
|
581
|
+
if(existing && (existing.textContent===String(count))) return
|
|
582
|
+
if(existing) existing.remove()
|
|
583
|
+
const badge=document.createElement('span'); badge.setAttribute(BADGE_ATTR,''); badge.className='dshmem-header-badge'; badge.textContent=String(count); tab.appendChild(badge)
|
|
1029
584
|
}
|
|
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
|
-
}
|
|
585
|
+
let styleEl=document.getElementById('dshmem-header-style') as HTMLStyleElement|null
|
|
586
|
+
if(!styleEl){ styleEl=document.createElement('style'); styleEl.id='dshmem-header-style'; styleEl.textContent=HEADER_BADGE_CSS; document.head.appendChild(styleEl) }
|
|
587
|
+
let disposed=false; let lastCount=0
|
|
588
|
+
const refresh=async():Promise<void>=>{
|
|
589
|
+
if(disposed) return
|
|
590
|
+
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
591
|
}
|
|
1053
|
-
|
|
1054
592
|
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)
|
|
593
|
+
const timer=window.setInterval(()=>{ void refresh() },8000)
|
|
594
|
+
const observer=new MutationObserver(()=>{
|
|
595
|
+
if(disposed) return; const tab=findMemoryTab(); if(!tab) return
|
|
596
|
+
const hasBadge=tab.querySelector(`[${BADGE_ATTR}]`)
|
|
597
|
+
if(lastCount>0 && !hasBadge) renderBadge(lastCount); else if(lastCount<=0 && hasBadge) renderBadge(0)
|
|
1066
598
|
})
|
|
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')
|
|
599
|
+
observer.observe(document.body,{childList:true,subtree:true})
|
|
600
|
+
return ()=>{ disposed=true; window.clearInterval(timer); observer.disconnect(); styleEl?.remove(); const tab=findMemoryTab(); if(tab) tab.querySelector(`[${BADGE_ATTR}]`)?.remove() }
|
|
601
|
+
},'maestro-memory: header-badge')
|
|
1078
602
|
}
|