@ddtcorex/dsh-maestro-memory 1.0.1
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 +96 -0
- package/LICENSE +21 -0
- package/README.md +448 -0
- package/cordis.patch.yml +13 -0
- package/lib/client.js +756 -0
- package/lib/index.d.ts +10 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +627 -0
- package/lib/index.js.map +1 -0
- package/lib/memory/batch.d.ts +44 -0
- package/lib/memory/batch.d.ts.map +1 -0
- package/lib/memory/batch.js +75 -0
- package/lib/memory/batch.js.map +1 -0
- package/lib/memory/feedback.d.ts +18 -0
- package/lib/memory/feedback.d.ts.map +1 -0
- package/lib/memory/feedback.js +29 -0
- package/lib/memory/feedback.js.map +1 -0
- package/lib/memory/store.d.ts +154 -0
- package/lib/memory/store.d.ts.map +1 -0
- package/lib/memory/store.js +606 -0
- package/lib/memory/store.js.map +1 -0
- package/lib/migration/cli.d.ts +18 -0
- package/lib/migration/cli.d.ts.map +1 -0
- package/lib/migration/cli.js +143 -0
- package/lib/migration/cli.js.map +1 -0
- package/lib/migration/fixture.d.ts +45 -0
- package/lib/migration/fixture.d.ts.map +1 -0
- package/lib/migration/fixture.js +177 -0
- package/lib/migration/fixture.js.map +1 -0
- package/lib/migration/service.d.ts +75 -0
- package/lib/migration/service.d.ts.map +1 -0
- package/lib/migration/service.js +713 -0
- package/lib/migration/service.js.map +1 -0
- package/lib/prompt/snapshot.d.ts +26 -0
- package/lib/prompt/snapshot.d.ts.map +1 -0
- package/lib/prompt/snapshot.js +74 -0
- package/lib/prompt/snapshot.js.map +1 -0
- package/lib/review/queue.d.ts +72 -0
- package/lib/review/queue.d.ts.map +1 -0
- package/lib/review/queue.js +306 -0
- package/lib/review/queue.js.map +1 -0
- package/lib/skills-browser.d.ts +67 -0
- package/lib/skills-browser.d.ts.map +1 -0
- package/lib/skills-browser.js +185 -0
- package/lib/skills-browser.js.map +1 -0
- package/lib/storage/atomic-store.d.ts +211 -0
- package/lib/storage/atomic-store.d.ts.map +1 -0
- package/lib/storage/atomic-store.js +582 -0
- package/lib/storage/atomic-store.js.map +1 -0
- package/lib/storage/layout.d.ts +81 -0
- package/lib/storage/layout.d.ts.map +1 -0
- package/lib/storage/layout.js +162 -0
- package/lib/storage/layout.js.map +1 -0
- package/lib/storage/legacy-format.d.ts +104 -0
- package/lib/storage/legacy-format.d.ts.map +1 -0
- package/lib/storage/legacy-format.js +262 -0
- package/lib/storage/legacy-format.js.map +1 -0
- package/lib/sync/config.d.ts +22 -0
- package/lib/sync/config.d.ts.map +1 -0
- package/lib/sync/config.js +57 -0
- package/lib/sync/config.js.map +1 -0
- package/lib/sync/git.d.ts +97 -0
- package/lib/sync/git.d.ts.map +1 -0
- package/lib/sync/git.js +282 -0
- package/lib/sync/git.js.map +1 -0
- package/lib/sync/index.d.ts +6 -0
- package/lib/sync/index.d.ts.map +1 -0
- package/lib/sync/index.js +6 -0
- package/lib/sync/index.js.map +1 -0
- package/lib/sync/layout.d.ts +15 -0
- package/lib/sync/layout.d.ts.map +1 -0
- package/lib/sync/layout.js +33 -0
- package/lib/sync/layout.js.map +1 -0
- package/lib/sync/merge.d.ts +42 -0
- package/lib/sync/merge.d.ts.map +1 -0
- package/lib/sync/merge.js +159 -0
- package/lib/sync/merge.js.map +1 -0
- package/lib/sync/service.d.ts +70 -0
- package/lib/sync/service.d.ts.map +1 -0
- package/lib/sync/service.js +508 -0
- package/lib/sync/service.js.map +1 -0
- package/lib/todo/store.d.ts +142 -0
- package/lib/todo/store.d.ts.map +1 -0
- package/lib/todo/store.js +452 -0
- package/lib/todo/store.js.map +1 -0
- package/lib/types/client/index.d.ts +7 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/package.json +63 -0
- package/src/client/index.tsx +994 -0
- package/src/host/index.ts +591 -0
- package/src/host/memory/batch.ts +107 -0
- package/src/host/memory/feedback.ts +36 -0
- package/src/host/memory/store.ts +650 -0
- package/src/host/migration/cli.ts +139 -0
- package/src/host/migration/fixture.ts +218 -0
- package/src/host/migration/service.ts +786 -0
- package/src/host/prompt/snapshot.ts +95 -0
- package/src/host/review/queue.ts +325 -0
- package/src/host/skills-browser.ts +191 -0
- package/src/host/storage/atomic-store.ts +605 -0
- package/src/host/storage/layout.ts +191 -0
- package/src/host/storage/legacy-format.ts +308 -0
- package/src/host/sync/config.ts +67 -0
- package/src/host/sync/git.ts +280 -0
- package/src/host/sync/index.ts +5 -0
- package/src/host/sync/layout.ts +39 -0
- package/src/host/sync/merge.ts +188 -0
- package/src/host/sync/service.ts +494 -0
- package/src/host/todo/store.ts +480 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({ id: "@ddtcorex/dsh-maestro-memory", factory: (require) => {
|
|
2
|
+
var __modules = {};
|
|
3
|
+
__modules["index.js"] = function (require, module, exports) {
|
|
4
|
+
"use strict";
|
|
5
|
+
/**
|
|
6
|
+
* dsh-maestro-memory — client entry (M3-PR-A: Todos subtab + dtodo RPC)
|
|
7
|
+
* Single conversation.view slot (id: maestro-memory, order 40) with internal tabs.
|
|
8
|
+
*/
|
|
9
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
12
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
13
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
14
|
+
}
|
|
15
|
+
Object.defineProperty(o, k2, desc);
|
|
16
|
+
}) : (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
o[k2] = m[k];
|
|
19
|
+
}));
|
|
20
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
21
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
22
|
+
}) : function(o, v) {
|
|
23
|
+
o["default"] = v;
|
|
24
|
+
});
|
|
25
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
26
|
+
var ownKeys = function(o) {
|
|
27
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
28
|
+
var ar = [];
|
|
29
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
30
|
+
return ar;
|
|
31
|
+
};
|
|
32
|
+
return ownKeys(o);
|
|
33
|
+
};
|
|
34
|
+
return function (mod) {
|
|
35
|
+
if (mod && mod.__esModule) return mod;
|
|
36
|
+
var result = {};
|
|
37
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
38
|
+
__setModuleDefault(result, mod);
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
})();
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.inject = void 0;
|
|
44
|
+
exports.apply = apply;
|
|
45
|
+
const React = __importStar(require("react"));
|
|
46
|
+
exports.inject = ['slots', 'sessions', 'connection'];
|
|
47
|
+
const RPC_CHANNEL = '/dsh-maestro-memory';
|
|
48
|
+
// Theme-aware style values. DSH exposes these as CSS custom properties that
|
|
49
|
+
// flip for light/dark (see Theme.listTokens). The previous hard-coded light
|
|
50
|
+
// hex values made the view unreadable on the dark theme: an inherited white
|
|
51
|
+
// label on white `#fff`/`#eee` buttons (white-on-white), plus bright `#ccc`/
|
|
52
|
+
// `#ddd` borders and dark `#333`/`#666` text that disappeared against the
|
|
53
|
+
// dark base. Using the --dsw-alias-* tokens keeps it readable in both themes.
|
|
54
|
+
const STYLE = {
|
|
55
|
+
text: 'var(--dsw-alias-label-primary)',
|
|
56
|
+
textSec: 'var(--dsw-alias-label-secondary)',
|
|
57
|
+
borderL1: '1px solid var(--dsw-alias-border-l1)',
|
|
58
|
+
borderL2: '1px solid var(--dsw-alias-border-l2)',
|
|
59
|
+
surface: 'var(--dsw-alias-bg-layer-1)',
|
|
60
|
+
// Active tab/target highlight — a filled overlay that reads as "selected" in
|
|
61
|
+
// both themes (lighter over dark, grayish over light). Avoids bg-layer-2,
|
|
62
|
+
// which collapses to white on the light theme.
|
|
63
|
+
active: 'var(--dsw-alias-interactive-bg-active)',
|
|
64
|
+
success: 'var(--dsw-alias-state-success-primary)',
|
|
65
|
+
error: 'var(--dsw-alias-state-error-primary)',
|
|
66
|
+
// Brand accent keeps white text (it's readable in both themes; the DSH
|
|
67
|
+
// --dsw-alias-brand-primary token resolves to white here, which would make
|
|
68
|
+
// white text invisible, so we don't use the token for a solid button fill).
|
|
69
|
+
brand: '#06c',
|
|
70
|
+
// Neutral secondary chip (Archive / Cancel / Undo): theme surface + primary label.
|
|
71
|
+
neutral: 'var(--dsw-alias-bg-layer-2)',
|
|
72
|
+
onAccent: '#fff', // white text on colored accents — readable in both themes
|
|
73
|
+
};
|
|
74
|
+
// Scoped design tokens for the view so it reads like a first-class DSH panel.
|
|
75
|
+
// Hover/focus/transition states are impossible with inline styles alone, so we
|
|
76
|
+
// attach a small <style> scoped to the .dshmem container. All colors come from
|
|
77
|
+
// the --dsw-alias-* theme tokens and flip with the DSH light/dark theme.
|
|
78
|
+
// Typography is deliberately NOT declared here: the container inherits the DSH
|
|
79
|
+
// text styles from the host tree (label-primary color, 16px/28px body text),
|
|
80
|
+
// and every child resolves its font through normal CSS inheritance — no more
|
|
81
|
+
// per-element `font: 13px system-ui` stamps that fight the host theme.
|
|
82
|
+
const MEM_CSS = `
|
|
83
|
+
.dshmem { color: var(--dsw-alias-label-primary); font-size: 13px; }
|
|
84
|
+
/* Form controls carry a UA-stylesheet font (13.33px Arial) and do not
|
|
85
|
+
inherit by default: opt them back into the host chain explicitly. */
|
|
86
|
+
.dshmem button { font: inherit; cursor: pointer; transition: color .12s ease, background .12s ease, border-color .12s ease; }
|
|
87
|
+
.dshmem button:hover { color: var(--dsw-alias-label-primary); }
|
|
88
|
+
.dshmem button:active { transform: translateY(.5px); }
|
|
89
|
+
.dshmem button:focus-visible { outline: 2px solid var(--dsw-alias-interactive-bg-active); outline-offset: 1px; }
|
|
90
|
+
.dshmem input, .dshmem textarea, .dshmem select { font: inherit; color: var(--dsw-alias-label-primary); background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); border-radius: 6px; padding: 5px 8px; }
|
|
91
|
+
.dshmem input:focus, .dshmem textarea:focus, .dshmem select:focus { outline: none; border-color: var(--dsw-alias-border-l2); }
|
|
92
|
+
.dshmem textarea { resize: vertical; }
|
|
93
|
+
.dshmem .card { border: 1px solid var(--dsw-alias-border-l1); border-radius: 8px; padding: 10px; }
|
|
94
|
+
.dshmem .row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
|
95
|
+
.dshmem .muted { color: var(--dsw-alias-label-secondary); }
|
|
96
|
+
|
|
97
|
+
/* Top-level view navigation — an underline tab bar (mirrors the DSH
|
|
98
|
+
* Chat/Trajectory/Memory header tabs above it), NOT boxed chips, so it reads
|
|
99
|
+
* as navigation and stays distinct from the memory-track filters below. */
|
|
100
|
+
.dshmem .tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--dsw-alias-border-l1); margin-bottom: 14px; }
|
|
101
|
+
.dshmem .tab { appearance: none; background: transparent; border: none; border-bottom: 2px solid transparent; margin-bottom: -1px; padding: 7px 12px; border-radius: 0; font-weight: 500; color: var(--dsw-alias-label-secondary); }
|
|
102
|
+
.dshmem .tab:hover { color: var(--dsw-alias-label-primary); }
|
|
103
|
+
.dshmem .tab-active { color: var(--dsw-alias-label-primary); font-weight: 700; border-bottom-color: #06c; }
|
|
104
|
+
|
|
105
|
+
/* Memory-track / date filters + actions — one consistent control height so
|
|
106
|
+
* nothing reflows or misaligns when a field appears/disappears. */
|
|
107
|
+
.dshmem .tracklabel { text-transform: uppercase; letter-spacing: .04em; color: var(--dsw-alias-label-secondary); }
|
|
108
|
+
.dshmem .control { height: 28px; display: inline-flex; align-items: center; gap: 6px; line-height: 1; }
|
|
109
|
+
.dshmem .pill { border-radius: 999px; border: 1px solid var(--dsw-alias-border-l1); padding: 0 12px; background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-secondary); }
|
|
110
|
+
.dshmem .pill:hover { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-border-l2); }
|
|
111
|
+
.dshmem .pill-active { background: var(--dsw-alias-interactive-bg-active); color: var(--dsw-alias-label-primary); font-weight: 600; border-color: var(--dsw-alias-border-l2); }
|
|
112
|
+
.dshmem .ghost { border-radius: 8px; border: 1px solid var(--dsw-alias-border-l1); padding: 0 12px; background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-secondary); }
|
|
113
|
+
.dshmem .ghost:hover { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-border-l2); }
|
|
114
|
+
.dshmem .cwd { box-sizing: border-box; height: 28px; flex: 1; min-width: 180px; padding: 0 10px; }
|
|
115
|
+
.dshmem .cwd:disabled { opacity: .5; cursor: not-allowed; }
|
|
116
|
+
.dshmem .cwd:disabled::placeholder { color: var(--dsw-alias-label-secondary); }
|
|
117
|
+
.dshmem .tabbadge { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; padding: 0 5px; margin-left: 6px; border-radius: 999px; background: var(--dsw-alias-state-error-primary, #d9534f); color: #fff; font-size: 11px; font-weight: 700; line-height: 1; vertical-align: middle; }
|
|
118
|
+
`;
|
|
119
|
+
// Global CSS for the top-level header tab badge (injected into <head> by the
|
|
120
|
+
// boot-time effect). Reuses DSH theme tokens so it flips with light/dark; a
|
|
121
|
+
// soft blink draws attention to pending reviews without being obnoxious.
|
|
122
|
+
const HEADER_BADGE_CSS = `
|
|
123
|
+
.dshmem-header-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; padding: 0 5px; margin-left: 6px; border-radius: 999px; background: var(--dsw-alias-state-error-primary, #d9534f); color: #fff; font-size: 11px; font-weight: 700; line-height: 1; vertical-align: middle; animation: dshmem-blink 1.2s ease-in-out infinite; }
|
|
124
|
+
@keyframes dshmem-blink { 0%, 100% { opacity: 1; } 50% { opacity: .3; } }
|
|
125
|
+
`;
|
|
126
|
+
// Best-effort current-session cwd, derived from the injected `sessions` feed
|
|
127
|
+
// (never `ctx.conversation` — that face is not injectable here, and touching a
|
|
128
|
+
// non-injected service throws `cannot get property ... without inject`).
|
|
129
|
+
// `ctx.sessions.list.getSnapshot()` returns `{ current, byId }`, and each row
|
|
130
|
+
// carries the session's canonical `cwd`. Undefined on a fresh page / no session.
|
|
131
|
+
function sessionCwd(ctx) {
|
|
132
|
+
try {
|
|
133
|
+
const snap = ctx?.sessions?.list?.getSnapshot?.();
|
|
134
|
+
const id = snap?.current;
|
|
135
|
+
if (!id)
|
|
136
|
+
return '';
|
|
137
|
+
return snap?.byId?.[id]?.cwd ?? '';
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return '';
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function useRpc(ctx) {
|
|
144
|
+
return React.useCallback((endpoint, payload) => {
|
|
145
|
+
const conn = ctx.connection ?? ctx.get?.('connection');
|
|
146
|
+
if (!conn?.rpc?.call)
|
|
147
|
+
return Promise.reject(new Error('RPC not available'));
|
|
148
|
+
return conn.rpc.call(RPC_CHANNEL, endpoint, payload).then((result) => {
|
|
149
|
+
if (result?.ok === true)
|
|
150
|
+
return result.value;
|
|
151
|
+
const message = typeof result?.error?.message === 'string' ? result.error.message : 'RPC request failed';
|
|
152
|
+
throw new Error(message);
|
|
153
|
+
});
|
|
154
|
+
}, [ctx]);
|
|
155
|
+
}
|
|
156
|
+
function ReviewQueueView({ ctx, onPendingChange }) {
|
|
157
|
+
const rpc = useRpc(ctx);
|
|
158
|
+
const [entries, setEntries] = React.useState([]);
|
|
159
|
+
const [loading, setLoading] = React.useState(true);
|
|
160
|
+
const [edits, setEdits] = React.useState({});
|
|
161
|
+
const [msg, setMsg] = React.useState('');
|
|
162
|
+
const load = React.useCallback(async () => {
|
|
163
|
+
setLoading(true);
|
|
164
|
+
try {
|
|
165
|
+
const res = await rpc('queue.list', {});
|
|
166
|
+
const list = Array.isArray(res?.entries) ? res.entries : [];
|
|
167
|
+
setEntries(list);
|
|
168
|
+
onPendingChange?.(list.length);
|
|
169
|
+
}
|
|
170
|
+
catch (e) {
|
|
171
|
+
setMsg(`load failed: ${e?.message ?? String(e)}`);
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
setLoading(false);
|
|
175
|
+
}
|
|
176
|
+
}, [rpc, onPendingChange]);
|
|
177
|
+
React.useEffect(() => {
|
|
178
|
+
load();
|
|
179
|
+
}, [load]);
|
|
180
|
+
const decide = React.useCallback(async (action, index) => {
|
|
181
|
+
setMsg('');
|
|
182
|
+
const payload = { action, indices: [index] };
|
|
183
|
+
if (action === 'approve' && edits[index] !== undefined && edits[index].trim() !== '') {
|
|
184
|
+
payload.edits = { [String(index)]: edits[index] };
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
const res = await rpc('queue.decide', payload);
|
|
188
|
+
if (res?.ok) {
|
|
189
|
+
setMsg(res.lines ? res.lines.join('; ') : `${action} ok`);
|
|
190
|
+
await load();
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
setMsg(`failed: ${res?.error ?? 'unknown'}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch (e) {
|
|
197
|
+
setMsg(`error: ${e?.message ?? String(e)}`);
|
|
198
|
+
}
|
|
199
|
+
}, [rpc, edits, load]);
|
|
200
|
+
if (loading)
|
|
201
|
+
return React.createElement('div', null, 'Loading queue…');
|
|
202
|
+
if (entries.length === 0) {
|
|
203
|
+
return React.createElement('div', null, React.createElement('div', { style: { opacity: 0.7, marginBottom: 8 } }, 'No pending suggestions'), msg ? React.createElement('div', { style: { color: STYLE.textSec } }, msg) : null, React.createElement('button', { onClick: load, style: { marginTop: 8 } }, 'Refresh'));
|
|
204
|
+
}
|
|
205
|
+
return React.createElement('div', null, React.createElement('div', { style: { marginBottom: 8, opacity: 0.7 } }, `${entries.length} pending`), ...entries.map((e, idx) => {
|
|
206
|
+
const number = idx + 1;
|
|
207
|
+
return React.createElement('div', {
|
|
208
|
+
key: number,
|
|
209
|
+
style: { border: STYLE.borderL1, borderRadius: 8, padding: 8, marginBottom: 8 },
|
|
210
|
+
}, React.createElement('div', { style: { fontWeight: 600 } }, `#${number} [${e.target}]`), React.createElement('div', { style: { margin: '4px 0', whiteSpace: 'pre-wrap' } }, e.content), e.reason ? React.createElement('div', { style: { opacity: 0.7 } }, `Reason: ${e.reason}`) : null, React.createElement('textarea', {
|
|
211
|
+
value: edits[number] ?? '',
|
|
212
|
+
placeholder: 'Edit content before approve (optional)',
|
|
213
|
+
onChange: (ev) => setEdits((prev) => ({ ...prev, [number]: ev.target.value })),
|
|
214
|
+
style: { width: '100%', minHeight: 40, marginTop: 4 },
|
|
215
|
+
}), React.createElement('div', { style: { display: 'flex', gap: 8, marginTop: 8 } }, React.createElement('button', {
|
|
216
|
+
onClick: () => decide('approve', number),
|
|
217
|
+
'data-testid': `approve-${number}`,
|
|
218
|
+
style: { background: STYLE.success, color: STYLE.onAccent, border: 0, borderRadius: 4, padding: '4px 8px', cursor: 'pointer' },
|
|
219
|
+
}, 'Approve'), React.createElement('button', {
|
|
220
|
+
onClick: () => decide('reject', number),
|
|
221
|
+
'data-testid': `reject-${number}`,
|
|
222
|
+
style: { background: STYLE.error, color: STYLE.onAccent, border: 0, borderRadius: 4, padding: '4px 8px', cursor: 'pointer' },
|
|
223
|
+
}, 'Reject'), React.createElement('button', {
|
|
224
|
+
onClick: () => decide('archive', number),
|
|
225
|
+
'data-testid': `archive-${number}`,
|
|
226
|
+
style: { background: STYLE.neutral, color: STYLE.text, border: STYLE.borderL1, borderRadius: 4, padding: '4px 8px', cursor: 'pointer' },
|
|
227
|
+
}, 'Archive')));
|
|
228
|
+
}), msg ? React.createElement('div', { style: { marginTop: 8, color: STYLE.text } }, msg) : null, React.createElement('button', { onClick: load, style: { marginTop: 8 } }, 'Refresh'));
|
|
229
|
+
}
|
|
230
|
+
function TodosView({ ctx }) {
|
|
231
|
+
const rpc = useRpc(ctx);
|
|
232
|
+
const [items, setItems] = React.useState([]);
|
|
233
|
+
const [loading, setLoading] = React.useState(true);
|
|
234
|
+
const [msg, setMsg] = React.useState('');
|
|
235
|
+
const [target, setTarget] = React.useState('all');
|
|
236
|
+
const [showAll, setShowAll] = React.useState(false);
|
|
237
|
+
const [showPast, setShowPast] = React.useState(false);
|
|
238
|
+
const [showExpired, setShowExpired] = React.useState(false);
|
|
239
|
+
const [draftContent, setDraftContent] = React.useState('');
|
|
240
|
+
const [draftTarget, setDraftTarget] = React.useState('work');
|
|
241
|
+
const [draftDue, setDraftDue] = React.useState('');
|
|
242
|
+
const [draftQuadrant, setDraftQuadrant] = React.useState('');
|
|
243
|
+
const [draftCat, setDraftCat] = React.useState('');
|
|
244
|
+
const [editId, setEditId] = React.useState(null);
|
|
245
|
+
const [editContent, setEditContent] = React.useState('');
|
|
246
|
+
const [editDue, setEditDue] = React.useState('');
|
|
247
|
+
const [editQuadrant, setEditQuadrant] = React.useState('');
|
|
248
|
+
const [editStatus, setEditStatus] = React.useState('');
|
|
249
|
+
const [editCat, setEditCat] = React.useState('');
|
|
250
|
+
const load = React.useCallback(async () => {
|
|
251
|
+
setLoading(true);
|
|
252
|
+
setMsg('');
|
|
253
|
+
try {
|
|
254
|
+
const rpcPayload = {
|
|
255
|
+
target: target !== 'all' ? target : undefined,
|
|
256
|
+
opts: {
|
|
257
|
+
all: showAll,
|
|
258
|
+
past: showPast,
|
|
259
|
+
expired: showExpired,
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
// For single target we need to pass correctly; todo.list expects target or targets
|
|
263
|
+
const res = await rpc('todo.list', rpcPayload);
|
|
264
|
+
const list = Array.isArray(res?.items) ? res.items : [];
|
|
265
|
+
setItems(list);
|
|
266
|
+
if (res?.hint)
|
|
267
|
+
setMsg(res.hint);
|
|
268
|
+
else if (res?.text) {
|
|
269
|
+
// fallback: text contains items but we already have items
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
catch (e) {
|
|
273
|
+
setMsg(`load failed: ${e?.message ?? String(e)}`);
|
|
274
|
+
}
|
|
275
|
+
finally {
|
|
276
|
+
setLoading(false);
|
|
277
|
+
}
|
|
278
|
+
}, [rpc, target, showAll, showPast, showExpired]);
|
|
279
|
+
React.useEffect(() => {
|
|
280
|
+
load();
|
|
281
|
+
}, [load]);
|
|
282
|
+
const addTodo = React.useCallback(async () => {
|
|
283
|
+
const content = draftContent.trim();
|
|
284
|
+
if (!content) {
|
|
285
|
+
setMsg('content required');
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
setMsg('');
|
|
289
|
+
try {
|
|
290
|
+
const res = await rpc('todo.mutate', {
|
|
291
|
+
action: 'add',
|
|
292
|
+
target: draftTarget,
|
|
293
|
+
content,
|
|
294
|
+
due: draftDue || undefined,
|
|
295
|
+
quadrant: draftQuadrant || undefined,
|
|
296
|
+
cat: draftCat || undefined,
|
|
297
|
+
});
|
|
298
|
+
if (res?.ok) {
|
|
299
|
+
setDraftContent('');
|
|
300
|
+
setDraftDue('');
|
|
301
|
+
setDraftQuadrant('');
|
|
302
|
+
setDraftCat('');
|
|
303
|
+
setMsg(`added (id: ${res.id})`);
|
|
304
|
+
await load();
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
setMsg(`add failed: ${res?.message ?? res?.error ?? 'unknown'}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
catch (e) {
|
|
311
|
+
setMsg(`error: ${e?.message ?? String(e)}`);
|
|
312
|
+
}
|
|
313
|
+
}, [rpc, draftContent, draftTarget, draftDue, draftQuadrant, draftCat, load]);
|
|
314
|
+
const doneTodo = React.useCallback(async (item) => {
|
|
315
|
+
try {
|
|
316
|
+
const res = await rpc('todo.mutate', { action: 'done', target: item.target, id: item.id });
|
|
317
|
+
if (res?.ok) {
|
|
318
|
+
setMsg(res.message ?? 'done');
|
|
319
|
+
await load();
|
|
320
|
+
}
|
|
321
|
+
else
|
|
322
|
+
setMsg(`done failed: ${res?.message ?? res?.error}`);
|
|
323
|
+
}
|
|
324
|
+
catch (e) {
|
|
325
|
+
setMsg(`error: ${e?.message ?? String(e)}`);
|
|
326
|
+
}
|
|
327
|
+
}, [rpc, load]);
|
|
328
|
+
const undoTodo = React.useCallback(async (item) => {
|
|
329
|
+
try {
|
|
330
|
+
// Re-open a done todo: set status back to pending (only if currently done).
|
|
331
|
+
const res = await rpc('todo.mutate', { action: 'update', target: item.target, id: item.id, status: 'pending' });
|
|
332
|
+
if (res?.ok) {
|
|
333
|
+
setMsg(res.message ?? 'undone');
|
|
334
|
+
await load();
|
|
335
|
+
}
|
|
336
|
+
else
|
|
337
|
+
setMsg(`undo failed: ${res?.message ?? res?.error}`);
|
|
338
|
+
}
|
|
339
|
+
catch (e) {
|
|
340
|
+
setMsg(`error: ${e?.message ?? String(e)}`);
|
|
341
|
+
}
|
|
342
|
+
}, [rpc, load]);
|
|
343
|
+
const removeTodo = React.useCallback(async (item) => {
|
|
344
|
+
try {
|
|
345
|
+
const res = await rpc('todo.mutate', { action: 'remove', target: item.target, id: item.id });
|
|
346
|
+
if (res?.ok) {
|
|
347
|
+
setMsg(res.message ?? 'removed');
|
|
348
|
+
await load();
|
|
349
|
+
}
|
|
350
|
+
else
|
|
351
|
+
setMsg(`remove failed: ${res?.message ?? res?.error}`);
|
|
352
|
+
}
|
|
353
|
+
catch (e) {
|
|
354
|
+
setMsg(`error: ${e?.message ?? String(e)}`);
|
|
355
|
+
}
|
|
356
|
+
}, [rpc, load]);
|
|
357
|
+
const startEdit = (item) => {
|
|
358
|
+
setEditId(item.id);
|
|
359
|
+
setEditContent(item.text ?? '');
|
|
360
|
+
setEditDue(item.due ?? '');
|
|
361
|
+
setEditQuadrant(item.quadrant ?? '');
|
|
362
|
+
setEditStatus(item.status ?? 'pending');
|
|
363
|
+
setEditCat(item.cat ?? '');
|
|
364
|
+
};
|
|
365
|
+
const saveEdit = React.useCallback(async (item) => {
|
|
366
|
+
try {
|
|
367
|
+
const patch = {};
|
|
368
|
+
if (editContent !== item.text)
|
|
369
|
+
patch.content = editContent;
|
|
370
|
+
if (editQuadrant !== (item.quadrant ?? ''))
|
|
371
|
+
patch.quadrant = editQuadrant || null;
|
|
372
|
+
if (editDue !== (item.due ?? ''))
|
|
373
|
+
patch.due = editDue || null;
|
|
374
|
+
if (editStatus !== item.status)
|
|
375
|
+
patch.status = editStatus;
|
|
376
|
+
if (editCat !== (item.cat ?? ''))
|
|
377
|
+
patch.cat = editCat || null;
|
|
378
|
+
const res = await rpc('todo.mutate', {
|
|
379
|
+
action: 'update',
|
|
380
|
+
target: item.target,
|
|
381
|
+
id: item.id,
|
|
382
|
+
...patch,
|
|
383
|
+
});
|
|
384
|
+
if (res?.ok) {
|
|
385
|
+
setEditId(null);
|
|
386
|
+
setMsg(res.message ?? 'updated');
|
|
387
|
+
await load();
|
|
388
|
+
}
|
|
389
|
+
else
|
|
390
|
+
setMsg(`update failed: ${res?.message ?? res?.error}`);
|
|
391
|
+
}
|
|
392
|
+
catch (e) {
|
|
393
|
+
setMsg(`error: ${e?.message ?? String(e)}`);
|
|
394
|
+
}
|
|
395
|
+
}, [rpc, editContent, editDue, editQuadrant, editStatus, editCat, load]);
|
|
396
|
+
return React.createElement('div', null, React.createElement('div', { style: { display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 8 } }, ['all', 'life', 'work', 'project', 'daily'].map((k) => React.createElement('button', {
|
|
397
|
+
key: k,
|
|
398
|
+
onClick: () => setTarget(k),
|
|
399
|
+
'data-testid': `todo-target-${k}`,
|
|
400
|
+
style: {
|
|
401
|
+
fontWeight: target === k ? 700 : 400,
|
|
402
|
+
padding: '4px 8px',
|
|
403
|
+
borderRadius: 6,
|
|
404
|
+
border: target === k ? STYLE.borderL2 : STYLE.borderL1,
|
|
405
|
+
background: target === k ? STYLE.active : STYLE.surface,
|
|
406
|
+
color: STYLE.text,
|
|
407
|
+
cursor: 'pointer',
|
|
408
|
+
},
|
|
409
|
+
}, k))), React.createElement('div', { style: { display: 'flex', gap: 12, marginBottom: 8, alignItems: 'center', flexWrap: 'wrap' } }, React.createElement('label', { style: { display: 'flex', gap: 4, alignItems: 'center' } }, React.createElement('input', {
|
|
410
|
+
type: 'checkbox',
|
|
411
|
+
checked: showAll,
|
|
412
|
+
onChange: (e) => setShowAll(e.target.checked),
|
|
413
|
+
'data-testid': 'todo-all',
|
|
414
|
+
}), 'all (no limit)'), React.createElement('label', { style: { display: 'flex', gap: 4, alignItems: 'center' } }, React.createElement('input', {
|
|
415
|
+
type: 'checkbox',
|
|
416
|
+
checked: showPast,
|
|
417
|
+
onChange: (e) => setShowPast(e.target.checked),
|
|
418
|
+
'data-testid': 'todo-past',
|
|
419
|
+
}), 'past'), React.createElement('label', { style: { display: 'flex', gap: 4, alignItems: 'center' } }, React.createElement('input', {
|
|
420
|
+
type: 'checkbox',
|
|
421
|
+
checked: showExpired,
|
|
422
|
+
onChange: (e) => setShowExpired(e.target.checked),
|
|
423
|
+
'data-testid': 'todo-expired',
|
|
424
|
+
}), 'expired'), React.createElement('button', { onClick: load, style: { padding: '4px 8px' }, 'data-testid': 'todo-refresh' }, 'Refresh')), React.createElement('div', { style: { border: STYLE.borderL1, borderRadius: 8, padding: 8, marginBottom: 12 } }, React.createElement('div', { style: { fontWeight: 600, marginBottom: 6 } }, 'Add todo'), React.createElement('textarea', {
|
|
425
|
+
value: draftContent,
|
|
426
|
+
placeholder: 'Todo content',
|
|
427
|
+
onChange: (e) => setDraftContent(e.target.value),
|
|
428
|
+
style: { width: '100%', minHeight: 40, marginBottom: 6 },
|
|
429
|
+
'data-testid': 'todo-add-content',
|
|
430
|
+
}), React.createElement('div', { style: { display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 6 } }, React.createElement('select', {
|
|
431
|
+
value: draftTarget,
|
|
432
|
+
onChange: (e) => setDraftTarget(e.target.value),
|
|
433
|
+
style: { padding: '4px' },
|
|
434
|
+
'data-testid': 'todo-add-target',
|
|
435
|
+
}, ['life', 'work', 'project', 'daily'].map((t) => React.createElement('option', { key: t, value: t }, t))), React.createElement('select', {
|
|
436
|
+
value: draftQuadrant,
|
|
437
|
+
onChange: (e) => setDraftQuadrant(e.target.value),
|
|
438
|
+
style: { padding: '4px' },
|
|
439
|
+
'data-testid': 'todo-add-quadrant',
|
|
440
|
+
}, React.createElement('option', { value: '' }, 'quadrant'), React.createElement('option', { value: 'q1' }, 'q1'), React.createElement('option', { value: 'q2' }, 'q2'), React.createElement('option', { value: 'q3' }, 'q3'), React.createElement('option', { value: 'q4' }, 'q4')), React.createElement('input', {
|
|
441
|
+
value: draftDue,
|
|
442
|
+
placeholder: 'due YYYY-MM-DD',
|
|
443
|
+
onChange: (e) => setDraftDue(e.target.value),
|
|
444
|
+
style: { padding: '4px', width: 120 },
|
|
445
|
+
'data-testid': 'todo-add-due',
|
|
446
|
+
}), React.createElement('input', {
|
|
447
|
+
value: draftCat,
|
|
448
|
+
placeholder: 'cat',
|
|
449
|
+
onChange: (e) => setDraftCat(e.target.value),
|
|
450
|
+
style: { padding: '4px', width: 80 },
|
|
451
|
+
'data-testid': 'todo-add-cat',
|
|
452
|
+
})), React.createElement('button', {
|
|
453
|
+
onClick: addTodo,
|
|
454
|
+
style: { background: STYLE.success, color: STYLE.onAccent, border: 0, borderRadius: 4, padding: '6px 12px', cursor: 'pointer' },
|
|
455
|
+
'data-testid': 'todo-add-btn',
|
|
456
|
+
}, 'Add')), msg ? React.createElement('div', { style: { color: STYLE.text, marginBottom: 8, whiteSpace: 'pre-wrap' } }, msg) : null, loading
|
|
457
|
+
? React.createElement('div', null, 'Loading todos…')
|
|
458
|
+
: items.length === 0
|
|
459
|
+
? React.createElement('div', { style: { opacity: 0.7 } }, 'No todos (smart view: overdue / due today / current project / Q1-Q2, max 8). Try "all" or "past".')
|
|
460
|
+
: React.createElement('div', null, React.createElement('div', { style: { opacity: 0.7, marginBottom: 6 } }, `${items.length} todos${!showAll && items.length === 8 ? ' (smart view limited to 8)' : ''}`), ...items.map((it) => React.createElement('div', { key: it.id, style: { border: STYLE.borderL1, borderRadius: 8, padding: 8, marginBottom: 8 } }, React.createElement('div', { style: { opacity: 0.7 } }, `[${it.target}]`, it.quadrant ? ` [${it.quadrant}]` : '', it.due ? ` [due:${it.due}]` : '', it.status !== 'pending' ? ` [${it.status}]` : '', it.cat ? ` [cat:${it.cat}]` : '', it.doneAt ? ` [done:${it.doneAt}]` : '', it.past ? ` [past ${it.day}]` : '', ` id:${it.id}`), editId === it.id
|
|
461
|
+
? React.createElement('div', null, React.createElement('textarea', {
|
|
462
|
+
value: editContent,
|
|
463
|
+
onChange: (e) => setEditContent(e.target.value),
|
|
464
|
+
style: { width: '100%', minHeight: 40, marginTop: 4 },
|
|
465
|
+
'data-testid': `todo-edit-content-${it.id}`,
|
|
466
|
+
}), React.createElement('div', { style: { display: 'flex', gap: 6, marginTop: 6, flexWrap: 'wrap' } }, React.createElement('select', {
|
|
467
|
+
value: editQuadrant,
|
|
468
|
+
onChange: (e) => setEditQuadrant(e.target.value),
|
|
469
|
+
'data-testid': `todo-edit-quadrant-${it.id}`,
|
|
470
|
+
}, React.createElement('option', { value: '' }, 'no quadrant'), React.createElement('option', { value: 'q1' }, 'q1'), React.createElement('option', { value: 'q2' }, 'q2'), React.createElement('option', { value: 'q3' }, 'q3'), React.createElement('option', { value: 'q4' }, 'q4')), React.createElement('input', {
|
|
471
|
+
value: editDue,
|
|
472
|
+
placeholder: 'due YYYY-MM-DD',
|
|
473
|
+
onChange: (e) => setEditDue(e.target.value),
|
|
474
|
+
style: { width: 120 },
|
|
475
|
+
'data-testid': `todo-edit-due-${it.id}`,
|
|
476
|
+
}), React.createElement('select', {
|
|
477
|
+
value: editStatus,
|
|
478
|
+
onChange: (e) => setEditStatus(e.target.value),
|
|
479
|
+
'data-testid': `todo-edit-status-${it.id}`,
|
|
480
|
+
}, ['pending', 'doing', 'done', 'blocked', 'cancelled'].map((s) => React.createElement('option', { key: s, value: s }, s))), React.createElement('input', {
|
|
481
|
+
value: editCat,
|
|
482
|
+
placeholder: 'cat',
|
|
483
|
+
onChange: (e) => setEditCat(e.target.value),
|
|
484
|
+
style: { width: 80 },
|
|
485
|
+
'data-testid': `todo-edit-cat-${it.id}`,
|
|
486
|
+
})), React.createElement('div', { style: { display: 'flex', gap: 8, marginTop: 8 } }, React.createElement('button', {
|
|
487
|
+
onClick: () => saveEdit(it),
|
|
488
|
+
style: { background: STYLE.success, color: STYLE.onAccent, border: 0, borderRadius: 4, padding: '4px 8px', cursor: 'pointer' },
|
|
489
|
+
'data-testid': `todo-save-${it.id}`,
|
|
490
|
+
}, 'Save'), React.createElement('button', {
|
|
491
|
+
onClick: () => setEditId(null),
|
|
492
|
+
style: { background: STYLE.neutral, color: STYLE.text, border: STYLE.borderL1, borderRadius: 4, padding: '4px 8px', cursor: 'pointer' },
|
|
493
|
+
'data-testid': `todo-cancel-${it.id}`,
|
|
494
|
+
}, 'Cancel')))
|
|
495
|
+
: React.createElement('div', null, React.createElement('div', { style: { margin: '4px 0', whiteSpace: 'pre-wrap' } }, it.text), React.createElement('div', { style: { display: 'flex', gap: 6, marginTop: 8, flexWrap: 'wrap' } }, React.createElement('button', {
|
|
496
|
+
onClick: () => (it.status === 'done' ? undoTodo(it) : doneTodo(it)),
|
|
497
|
+
style: { background: it.status === 'done' ? STYLE.neutral : STYLE.success, color: it.status === 'done' ? STYLE.text : STYLE.onAccent, border: it.status === 'done' ? STYLE.borderL1 : 0, borderRadius: 4, padding: '4px 8px', cursor: 'pointer' },
|
|
498
|
+
'data-testid': `todo-done-${it.id}`,
|
|
499
|
+
}, it.status === 'done' ? 'Undo' : 'Done'), React.createElement('button', {
|
|
500
|
+
onClick: () => startEdit(it),
|
|
501
|
+
style: { background: STYLE.brand, color: STYLE.onAccent, border: 0, borderRadius: 4, padding: '4px 8px', cursor: 'pointer' },
|
|
502
|
+
'data-testid': `todo-edit-${it.id}`,
|
|
503
|
+
}, 'Edit'), React.createElement('button', {
|
|
504
|
+
onClick: () => removeTodo(it),
|
|
505
|
+
style: { background: STYLE.error, color: STYLE.onAccent, border: 0, borderRadius: 4, padding: '4px 8px', cursor: 'pointer' },
|
|
506
|
+
'data-testid': `todo-remove-${it.id}`,
|
|
507
|
+
}, 'Remove')))))));
|
|
508
|
+
}
|
|
509
|
+
function SkillsView({ ctx }) {
|
|
510
|
+
const rpc = useRpc(ctx);
|
|
511
|
+
const [entries, setEntries] = React.useState([]);
|
|
512
|
+
const [loading, setLoading] = React.useState(true);
|
|
513
|
+
const [msg, setMsg] = React.useState('');
|
|
514
|
+
const load = React.useCallback(async () => {
|
|
515
|
+
setLoading(true);
|
|
516
|
+
setMsg('');
|
|
517
|
+
try {
|
|
518
|
+
const res = await rpc('skills.list', {});
|
|
519
|
+
if (res?.ok) {
|
|
520
|
+
setEntries(Array.isArray(res.entries) ? res.entries : []);
|
|
521
|
+
if (res.entries.length === 0)
|
|
522
|
+
setMsg('No skills found (maestro-skills not installed or empty)');
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
setMsg(`load failed: ${res?.error ?? 'unknown'}`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
catch (e) {
|
|
529
|
+
setMsg(`error: ${e?.message ?? String(e)}`);
|
|
530
|
+
}
|
|
531
|
+
finally {
|
|
532
|
+
setLoading(false);
|
|
533
|
+
}
|
|
534
|
+
}, [rpc]);
|
|
535
|
+
React.useEffect(() => {
|
|
536
|
+
load();
|
|
537
|
+
}, [load]);
|
|
538
|
+
if (loading)
|
|
539
|
+
return React.createElement('div', null, 'Loading skills…');
|
|
540
|
+
return React.createElement('div', null, React.createElement('div', { style: { opacity: 0.7, marginBottom: 8 } }, `${entries.length} skills (read-only browser, metadata/origin only — maestro-skills discovery unchanged)`), entries.length === 0
|
|
541
|
+
? React.createElement('div', { style: { opacity: 0.7 } }, msg || 'No skills')
|
|
542
|
+
: React.createElement('div', null, ...entries.map((e) => React.createElement('div', { key: e.name, style: { border: STYLE.borderL1, borderRadius: 8, padding: 8, marginBottom: 8 } }, React.createElement('div', { style: { fontWeight: 600 } }, e.name), React.createElement('div', { style: { opacity: 0.7 } }, `[${e.origin}] ${e.path}`), React.createElement('div', { style: { margin: '4px 0', whiteSpace: 'pre-wrap' } }, e.description), e.metadata && Object.keys(e.metadata).length > 0
|
|
543
|
+
? React.createElement('div', { style: { opacity: 0.6, marginTop: 4 } }, `metadata: ${Object.entries(e.metadata)
|
|
544
|
+
.map(([k, v]) => `${k}=${String(v).slice(0, 60)}`)
|
|
545
|
+
.join(', ')}`)
|
|
546
|
+
: null))), msg ? React.createElement('div', { style: { marginTop: 8, color: STYLE.text } }, msg) : null, React.createElement('button', { onClick: load, style: { marginTop: 8, padding: '4px 8px' }, 'data-testid': 'skills-refresh' }, 'Refresh'), React.createElement('div', { style: { marginTop: 8, opacity: 0.6 } }, 'Read-only — model suggestions cannot change skills. Future edits, if approved, will require explicit user action + path containment.'));
|
|
547
|
+
}
|
|
548
|
+
function MemoryListView({ ctx }) {
|
|
549
|
+
const rpc = useRpc(ctx);
|
|
550
|
+
const [track, setTrack] = React.useState('key');
|
|
551
|
+
const [cwd, setCwd] = React.useState(() => sessionCwd(ctx));
|
|
552
|
+
const [entries, setEntries] = React.useState([]);
|
|
553
|
+
const [loading, setLoading] = React.useState(true);
|
|
554
|
+
const [msg, setMsg] = React.useState('');
|
|
555
|
+
const needsCwd = track === 'key' || track === 'project';
|
|
556
|
+
const load = React.useCallback(async () => {
|
|
557
|
+
setLoading(true);
|
|
558
|
+
setMsg('');
|
|
559
|
+
try {
|
|
560
|
+
if (needsCwd && !cwd.trim()) {
|
|
561
|
+
setEntries([]);
|
|
562
|
+
setMsg('cwd required for the key/project track');
|
|
563
|
+
setLoading(false);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
const res = await rpc('memory.list', { target: track, cwd: needsCwd ? cwd.trim() : undefined });
|
|
567
|
+
setEntries(Array.isArray(res?.entries) ? res.entries : []);
|
|
568
|
+
}
|
|
569
|
+
catch (e) {
|
|
570
|
+
setMsg(`load failed: ${e?.message ?? String(e)}`);
|
|
571
|
+
}
|
|
572
|
+
finally {
|
|
573
|
+
setLoading(false);
|
|
574
|
+
}
|
|
575
|
+
}, [rpc, track, cwd, needsCwd]);
|
|
576
|
+
React.useEffect(() => {
|
|
577
|
+
load();
|
|
578
|
+
}, [load]);
|
|
579
|
+
return React.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 12 } }, React.createElement('div', { className: 'row', style: { alignItems: 'center', gap: 6 } }, React.createElement('span', { className: 'tracklabel' }, 'Track'), ['memory', 'user', 'key', 'project', 'daily'].map((k) => React.createElement('button', {
|
|
580
|
+
key: k,
|
|
581
|
+
onClick: () => setTrack(k),
|
|
582
|
+
'aria-pressed': track === k,
|
|
583
|
+
className: track === k ? 'control pill pill-active' : 'control pill',
|
|
584
|
+
'data-testid': `mem-track-${k}`,
|
|
585
|
+
}, k))), React.createElement('div', { className: 'row', style: { alignItems: 'center' } }, React.createElement('input', {
|
|
586
|
+
value: cwd,
|
|
587
|
+
disabled: !needsCwd,
|
|
588
|
+
placeholder: needsCwd ? 'cwd (for key/project)' : 'cwd is not used for this track',
|
|
589
|
+
onChange: (e) => setCwd(e.target.value),
|
|
590
|
+
'data-testid': 'mem-cwd',
|
|
591
|
+
className: 'cwd',
|
|
592
|
+
}), React.createElement('button', { onClick: load, className: 'control ghost', 'data-testid': 'mem-refresh' }, 'Refresh')), msg ? React.createElement('div', { className: 'muted' }, msg) : null, loading
|
|
593
|
+
? React.createElement('div', null, 'Loading memory…')
|
|
594
|
+
: entries.length === 0
|
|
595
|
+
? React.createElement('div', { className: 'muted' }, '(no entries)')
|
|
596
|
+
: React.createElement('div', null, React.createElement('div', { className: 'muted', style: { marginBottom: 6 } }, `${entries.length} entries`), ...entries.map((e, idx) => React.createElement('div', { key: idx, className: 'card', style: { marginBottom: 8 } }, React.createElement('div', { style: { whiteSpace: 'pre-wrap' } }, e)))));
|
|
597
|
+
}
|
|
598
|
+
function MemoryView({ ctx }) {
|
|
599
|
+
const rpc = useRpc(ctx);
|
|
600
|
+
const [tab, setTab] = React.useState('memory');
|
|
601
|
+
const [pending, setPending] = React.useState(0);
|
|
602
|
+
const refreshPending = React.useCallback(async () => {
|
|
603
|
+
try {
|
|
604
|
+
const res = await rpc('status', {});
|
|
605
|
+
setPending(typeof res?.queue === 'number' ? res.queue : 0);
|
|
606
|
+
}
|
|
607
|
+
catch {
|
|
608
|
+
setPending(0);
|
|
609
|
+
}
|
|
610
|
+
}, [rpc]);
|
|
611
|
+
React.useEffect(() => {
|
|
612
|
+
refreshPending();
|
|
613
|
+
}, [refreshPending, tab]);
|
|
614
|
+
return React.createElement('div', { className: 'dshmem', style: { padding: 16, color: STYLE.text, display: 'flex', flexDirection: 'column' } }, React.createElement('style', null, MEM_CSS), React.createElement('div', { className: 'tabs', role: 'tablist' }, ['memory', 'review', 'todos', 'skills'].map((k) => React.createElement('button', {
|
|
615
|
+
key: k,
|
|
616
|
+
role: 'tab',
|
|
617
|
+
'aria-selected': tab === k,
|
|
618
|
+
onClick: () => setTab(k),
|
|
619
|
+
className: tab === k ? 'tab tab-active' : 'tab',
|
|
620
|
+
'data-testid': `tab-${k}`,
|
|
621
|
+
}, k, k === 'review' && pending > 0
|
|
622
|
+
? React.createElement('span', { className: 'tabbadge', 'data-testid': 'tab-badge-review' }, String(pending))
|
|
623
|
+
: null))), tab === 'memory'
|
|
624
|
+
? React.createElement(MemoryListView, { ctx })
|
|
625
|
+
: tab === 'review'
|
|
626
|
+
? React.createElement(ReviewQueueView, { ctx, onPendingChange: setPending })
|
|
627
|
+
: tab === 'todos'
|
|
628
|
+
? React.createElement(TodosView, { ctx })
|
|
629
|
+
: React.createElement(SkillsView, { ctx }));
|
|
630
|
+
}
|
|
631
|
+
function apply(ctx) {
|
|
632
|
+
ctx.effect(() => {
|
|
633
|
+
const dispose = ctx.slots.inject('conversation.view', () => ctx.slots.register({
|
|
634
|
+
name: 'conversation.view',
|
|
635
|
+
id: 'maestro-memory',
|
|
636
|
+
order: 40,
|
|
637
|
+
label: () => 'Memory',
|
|
638
|
+
}, () => React.createElement(MemoryView, { ctx })));
|
|
639
|
+
return () => {
|
|
640
|
+
if (typeof dispose === 'function')
|
|
641
|
+
dispose();
|
|
642
|
+
};
|
|
643
|
+
}, 'maestro-memory: view');
|
|
644
|
+
// Top-level header tab badge: pending-suggestion count, with a soft blink,
|
|
645
|
+
// injected into the DSH shell header tab labeled "Memory". DSH projects
|
|
646
|
+
// 'conversation.view' tabs as plain string labels (ViewTab.label), so there is
|
|
647
|
+
// no React slot to render into — the plugin reaches the shell DOM directly
|
|
648
|
+
// (same technique as the mobile nav plugin).
|
|
649
|
+
ctx.effect(() => {
|
|
650
|
+
const conn = ctx.connection ?? ctx.get?.('connection');
|
|
651
|
+
if (!conn?.rpc?.call)
|
|
652
|
+
return () => { };
|
|
653
|
+
const BADGE_ATTR = 'data-dshmem-header-badge';
|
|
654
|
+
const VIEW_ATTR = 'data-dshmem-memory-tab';
|
|
655
|
+
const findMemoryTab = () => {
|
|
656
|
+
const tabs = document.querySelectorAll('button[role="tab"]');
|
|
657
|
+
for (const tab of Array.from(tabs)) {
|
|
658
|
+
if (tab.getAttribute(VIEW_ATTR) === '1')
|
|
659
|
+
return tab;
|
|
660
|
+
}
|
|
661
|
+
for (const tab of Array.from(tabs)) {
|
|
662
|
+
if (tab.closest('.dshmem'))
|
|
663
|
+
continue;
|
|
664
|
+
if ((tab.textContent ?? '').trim() === 'Memory') {
|
|
665
|
+
tab.setAttribute(VIEW_ATTR, '1');
|
|
666
|
+
return tab;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
return null;
|
|
670
|
+
};
|
|
671
|
+
const renderBadge = (count) => {
|
|
672
|
+
const tab = findMemoryTab();
|
|
673
|
+
if (!tab)
|
|
674
|
+
return;
|
|
675
|
+
const existing = tab.querySelector(`[${BADGE_ATTR}]`);
|
|
676
|
+
if (count <= 0) {
|
|
677
|
+
if (existing)
|
|
678
|
+
existing.remove();
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
if (existing && existing.textContent === String(count))
|
|
682
|
+
return;
|
|
683
|
+
if (existing)
|
|
684
|
+
existing.remove();
|
|
685
|
+
const badge = document.createElement('span');
|
|
686
|
+
badge.setAttribute(BADGE_ATTR, '');
|
|
687
|
+
badge.className = 'dshmem-header-badge';
|
|
688
|
+
badge.textContent = String(count);
|
|
689
|
+
tab.appendChild(badge);
|
|
690
|
+
};
|
|
691
|
+
let styleEl = document.getElementById('dshmem-header-style');
|
|
692
|
+
if (!styleEl) {
|
|
693
|
+
styleEl = document.createElement('style');
|
|
694
|
+
styleEl.id = 'dshmem-header-style';
|
|
695
|
+
styleEl.textContent = HEADER_BADGE_CSS;
|
|
696
|
+
document.head.appendChild(styleEl);
|
|
697
|
+
}
|
|
698
|
+
let disposed = false;
|
|
699
|
+
let lastCount = 0;
|
|
700
|
+
const refresh = async () => {
|
|
701
|
+
if (disposed)
|
|
702
|
+
return;
|
|
703
|
+
try {
|
|
704
|
+
const result = await conn.rpc.call(RPC_CHANNEL, 'status', {});
|
|
705
|
+
const value = result?.ok === true ? result.value : null;
|
|
706
|
+
lastCount = typeof value?.queue === 'number' ? value.queue : 0;
|
|
707
|
+
renderBadge(lastCount);
|
|
708
|
+
}
|
|
709
|
+
catch {
|
|
710
|
+
lastCount = 0;
|
|
711
|
+
renderBadge(0);
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
void refresh();
|
|
715
|
+
const timer = window.setInterval(() => {
|
|
716
|
+
void refresh();
|
|
717
|
+
}, 8000);
|
|
718
|
+
const observer = new MutationObserver(() => {
|
|
719
|
+
if (disposed)
|
|
720
|
+
return;
|
|
721
|
+
const tab = findMemoryTab();
|
|
722
|
+
if (!tab)
|
|
723
|
+
return;
|
|
724
|
+
const hasBadge = tab.querySelector(`[${BADGE_ATTR}]`);
|
|
725
|
+
if (lastCount > 0 && !hasBadge)
|
|
726
|
+
renderBadge(lastCount);
|
|
727
|
+
else if (lastCount <= 0 && hasBadge)
|
|
728
|
+
renderBadge(0);
|
|
729
|
+
});
|
|
730
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
731
|
+
return () => {
|
|
732
|
+
disposed = true;
|
|
733
|
+
window.clearInterval(timer);
|
|
734
|
+
observer.disconnect();
|
|
735
|
+
styleEl?.remove();
|
|
736
|
+
const tab = findMemoryTab();
|
|
737
|
+
if (tab)
|
|
738
|
+
tab.querySelector(`[${BADGE_ATTR}]`)?.remove();
|
|
739
|
+
};
|
|
740
|
+
}, 'maestro-memory: header-badge');
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
var __cache = {};
|
|
744
|
+
function __localRequire(id) {
|
|
745
|
+
if (id.charCodeAt(0) !== 46) return require(id);
|
|
746
|
+
id = id.slice(2);
|
|
747
|
+
var cached = __cache[id];
|
|
748
|
+
if (cached) return cached.exports;
|
|
749
|
+
var module = { exports: {} };
|
|
750
|
+
__cache[id] = module;
|
|
751
|
+
__modules[id](__localRequire, module, module.exports);
|
|
752
|
+
return module.exports;
|
|
753
|
+
}
|
|
754
|
+
var module = { exports: {} };
|
|
755
|
+
__modules["index.js"](__localRequire, module, module.exports);
|
|
756
|
+
return module.exports; } });
|