dsh-context-memory 0.2.1 → 0.4.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.
@@ -1,59 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  /**
3
- * Pure rendering half of the contextMemory page.
4
- *
5
- * Separate from `index.tsx` so a static render can assert in Node what the page draws —
6
- * the shipped bundle is a loader factory only a browser can run. Every user-visible
7
- * string comes from the `t` seat the renderer binds from this plugin's namespace, so the
8
- * page follows the UI language; no copy is hardcoded here.
9
- *
3
+ * Pure rendering half of the contextMemory page. Uses shared UI kit.
10
4
  * @module client/view
11
5
  */
12
- import { useCallback, useEffect, useState } from 'react';
13
- /** Panel route registered by the host half on the web connection. */
14
- const PANEL_PATH = "/api/context-memory.panel";
15
- const wrap = { display: 'flex', flexDirection: 'column', gap: 12, maxWidth: 760, fontFamily: 'inherit' };
16
- const head = { display: 'flex', alignItems: 'baseline', gap: 12, flexWrap: 'wrap' };
17
- const muted = { fontSize: 12, opacity: 0.75 };
18
- const table = { borderCollapse: 'collapse', width: '100%' };
19
- const th = { textAlign: 'left', padding: '4px 10px 4px 0', fontWeight: 600, fontSize: 12, opacity: 0.8, borderBottom: '0.5px solid rgba(128,128,128,0.4)' };
20
- const td = { padding: '6px 10px 6px 0', fontSize: 13, borderBottom: '0.5px solid rgba(128,128,128,0.18)' };
21
- const list = { margin: 0, paddingLeft: 18, fontSize: 13 };
22
- /** Fetch the host panel payload; `reload` re-runs the request. */
23
- export function usePanel() {
24
- const [state, setState] = useState({ payload: null, error: null });
25
- const [tick, setTick] = useState(0);
26
- const reload = useCallback(() => setTick((value) => value + 1), []);
27
- useEffect(() => {
28
- const controller = new AbortController();
29
- setState((previous) => ({ ...previous, error: null }));
30
- fetch(PANEL_PATH, { signal: controller.signal })
31
- .then(async (response) => {
32
- if (!response.ok)
33
- throw new Error(String(response.status));
34
- return response.json();
35
- })
36
- .then((payload) => {
37
- if (!controller.signal.aborted)
38
- setState({ payload, error: null });
39
- })
40
- .catch((cause) => {
41
- if (controller.signal.aborted)
42
- return;
43
- setState({ payload: null, error: cause instanceof Error ? cause.message : String(cause) });
44
- });
45
- return () => controller.abort();
46
- }, [tick]);
47
- return { ...state, reload };
48
- }
49
- /**
50
- * A memory's verification label.
51
- *
52
- * Four states, not two, because "no check exists" and "the check could not run" are
53
- * different answers from "the check failed" — collapsing them would report an
54
- * unverifiable memory and a false one identically, which is the failure this plugin
55
- * exists to avoid.
56
- */
6
+ import { useCallback, useState } from 'react';
7
+ import { Badge, Button, Card, ConfirmDialog, EmptyState, Field, Input, Modal, SectionTitle, Select, Spinner, StatCard, ToastProvider, tableStyles, usePanel, useToast, } from './ui.js';
8
+ const PANEL_PATH = '/api/context-memory.panel';
9
+ const RECALL_PATH = '/api/context-memory.recall';
10
+ const REMEMBER_PATH = '/api/context-memory.remember';
11
+ const FORGET_PATH = '/api/context-memory.forget';
57
12
  export function checkLabel(memory, t) {
58
13
  if (!memory.guard)
59
14
  return t('checkNone');
@@ -64,23 +19,75 @@ export function checkLabel(memory, t) {
64
19
  default: return t('checkPending');
65
20
  }
66
21
  }
67
- /** Where a memory came from: the answer to "why does the agent think it knows this". */
68
22
  export function provenance(memory) {
69
- return [
70
- memory.source,
71
- memory.sessionId !== undefined ? `session ${memory.sessionId}` : null,
72
- memory.step !== undefined ? `step ${String(memory.step)}` : null,
73
- ].filter((part) => part !== null).join(' · ');
23
+ return [memory.source, memory.sessionId !== undefined ? `session ${memory.sessionId}` : null, memory.step !== undefined ? `step ${String(memory.step)}` : null].filter((part) => part !== null).join(' · ');
74
24
  }
75
- export function ContextMemoryPanel({ t }) {
76
- const { payload, error, reload } = usePanel();
77
- const header = (_jsxs("header", { style: head, children: [_jsx("strong", { style: { fontSize: 13 }, children: t('title') }), _jsx("span", { style: { flex: 1 } }), _jsx("button", { type: "button", onClick: reload, style: { fontSize: 12 }, children: t('refresh') })] }));
78
- if (error !== null) {
79
- return (_jsxs("div", { style: wrap, children: [header, _jsxs("p", { role: "alert", style: { margin: 0, fontSize: 13 }, children: [t('failed'), ": ", error] }), _jsx("button", { type: "button", onClick: reload, style: { alignSelf: 'flex-start', fontSize: 12 }, children: t('retry') })] }));
80
- }
25
+ function AddMemoryModal({ t, onClose, onAdded }) {
26
+ const toast = useToast();
27
+ const [text, setText] = useState('');
28
+ const [subject, setSubject] = useState('');
29
+ const [kind, setKind] = useState('fact');
30
+ const [adding, setAdding] = useState(false);
31
+ const handleAdd = useCallback(async () => {
32
+ if (!text.trim())
33
+ return;
34
+ setAdding(true);
35
+ try {
36
+ const r = await fetch(REMEMBER_PATH, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ text, subject: subject || undefined, kind, source: 'user' }) });
37
+ if (r.ok) {
38
+ toast('success', t('memoryAdded'));
39
+ onAdded();
40
+ onClose();
41
+ }
42
+ }
43
+ finally {
44
+ setAdding(false);
45
+ }
46
+ }, [text, subject, kind, t, toast, onAdded, onClose]);
47
+ return (_jsx(Modal, { title: t('addMemory'), onClose: onClose, width: 500, footer: _jsxs(_Fragment, { children: [_jsx(Button, { variant: "secondary", onClick: onClose, children: t('cancel') }), _jsxs(Button, { variant: "primary", disabled: adding || !text.trim(), onClick: handleAdd, children: [adding ? _jsx(Spinner, { size: 14 }) : null, " ", t('add')] })] }), children: _jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 14 }, children: [_jsx(Field, { label: t('memoryText'), children: _jsx(Input, { value: text, onChange: (e) => setText(e.target.value), placeholder: t('memoryTextPlaceholder') }) }), _jsx(Field, { label: t('subjectOptional'), children: _jsx(Input, { value: subject, onChange: (e) => setSubject(e.target.value), placeholder: t('subjectPlaceholder') }) }), _jsx(Field, { label: t('kind'), children: _jsxs(Select, { value: kind, onChange: (e) => setKind(e.target.value), style: { width: '100%' }, children: [_jsx("option", { value: "fact", children: t('kindFact') }), _jsx("option", { value: "decision", children: t('kindDecision') }), _jsx("option", { value: "preference", children: t('kindPreference') }), _jsx("option", { value: "pitfall", children: t('kindPitfall') })] }) })] }) }));
48
+ }
49
+ function ContextMemoryPanelInner({ t }) {
50
+ const { payload, error, reload } = usePanel(PANEL_PATH);
51
+ const toast = useToast();
52
+ const [searchQuery, setSearchQuery] = useState('');
53
+ const [searchHits, setSearchHits] = useState(null);
54
+ const [searching, setSearching] = useState(false);
55
+ const [addOpen, setAddOpen] = useState(false);
56
+ const [forgetTarget, setForgetTarget] = useState(null);
57
+ const handleSearch = useCallback(async () => {
58
+ if (!searchQuery.trim()) {
59
+ setSearchHits(null);
60
+ return;
61
+ }
62
+ setSearching(true);
63
+ try {
64
+ const r = await fetch(`${RECALL_PATH}?q=${encodeURIComponent(searchQuery)}&limit=10`);
65
+ if (r.ok) {
66
+ setSearchHits((await r.json()).hits);
67
+ }
68
+ }
69
+ finally {
70
+ setSearching(false);
71
+ }
72
+ }, [searchQuery]);
73
+ const handleForget = useCallback(async () => {
74
+ if (forgetTarget === null)
75
+ return;
76
+ const r = await fetch(FORGET_PATH, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(forgetTarget) });
77
+ if (r.ok) {
78
+ toast('success', t('forgotten'));
79
+ setForgetTarget(null);
80
+ reload();
81
+ }
82
+ }, [forgetTarget, t, toast, reload]);
83
+ const header = (_jsxs("header", { style: { display: 'flex', alignItems: 'center', gap: 12, marginBottom: 4 }, children: [_jsxs("strong", { style: { fontSize: 15 }, children: ["\uD83E\uDDE0 ", t('title')] }), _jsx("span", { style: { flex: 1 } }), _jsxs(Button, { variant: "primary", size: "sm", onClick: () => setAddOpen(true), children: ["\u2795 ", t('addMemory')] }), _jsx(Button, { variant: "secondary", size: "sm", onClick: reload, children: t('refresh') })] }));
84
+ if (error !== null)
85
+ return _jsxs("div", { style: { maxWidth: 820 }, children: [header, _jsx(Card, { children: _jsxs("p", { role: "alert", style: { margin: 0, fontSize: 13, color: 'var(--error, #e53935)' }, children: [t('failed'), ": ", error] }) })] });
81
86
  if (payload === null)
82
- return _jsx("p", { style: muted, "aria-live": "polite", children: t('loading') });
83
- const rows = (memories) => memories.map((memory) => (_jsxs("tr", { children: [_jsx("td", { style: td, children: _jsx("code", { style: { fontSize: 11 }, children: memory.id }) }), _jsx("td", { style: td, children: memory.text }), _jsx("td", { style: td, children: memory.kind }), _jsx("td", { style: td, children: checkLabel(memory, t) }), _jsx("td", { style: td, children: provenance(memory) })] }, memory.id)));
84
- const { totals } = payload;
85
- return (_jsxs("div", { style: wrap, children: [header, _jsxs("span", { style: muted, children: [t('active'), " ", totals.active, " \u00B7 ", t('checkPassed'), " ", totals.verified, " \u00B7 ", t('unverified'), " ", totals.unverified, " \u00B7 ", t('superseded'), " ", totals.superseded] }), _jsx("span", { style: muted, children: t('checkLegend') }), _jsx("strong", { style: { fontSize: 13 }, children: t('conflicts') }), payload.conflicts.length === 0 ? (_jsx("span", { style: muted, children: t('noConflicts') })) : (_jsx("ul", { style: list, children: payload.conflicts.map((conflict) => (_jsxs("li", { children: [_jsx("strong", { children: conflict.subject }), _jsx("ul", { style: { ...list, fontSize: 12, opacity: 0.85 }, children: conflict.memories.map((memory) => (_jsxs("li", { children: [_jsx("code", { style: { fontSize: 11 }, children: memory.id }), " ", memory.text] }, memory.id))) })] }, conflict.subject))) })), _jsx("strong", { style: { fontSize: 13 }, children: t('active') }), payload.active.length === 0 ? (_jsx("p", { style: { margin: 0, fontSize: 13, opacity: 0.8 }, children: t('empty') })) : (_jsxs("table", { style: table, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { style: th, children: "Id" }), _jsx("th", { style: th, children: t('memory') }), _jsx("th", { style: th, children: t('kind') }), _jsx("th", { style: th, children: t('status') }), _jsx("th", { style: th, children: t('from') })] }) }), _jsx("tbody", { children: rows(payload.active) })] })), payload.superseded.length > 0 && (_jsxs(_Fragment, { children: [_jsxs("strong", { style: { fontSize: 13 }, children: [t('superseded'), " (", payload.superseded.length, ")"] }), _jsxs("table", { style: table, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { style: th, children: "Id" }), _jsx("th", { style: th, children: t('memory') }), _jsx("th", { style: th, children: t('kind') }), _jsx("th", { style: th, children: t('status') }), _jsx("th", { style: th, children: t('from') })] }) }), _jsx("tbody", { children: rows(payload.superseded) })] })] }))] }));
87
+ return _jsxs("div", { style: { maxWidth: 820 }, children: [header, _jsx("div", { style: { display: 'flex', justifyContent: 'center', padding: 40 }, children: _jsx(Spinner, { size: 28 }) })] });
88
+ return (_jsxs("div", { style: { display: 'flex', flexDirection: 'column', gap: 14, maxWidth: 820 }, children: [header, _jsxs("div", { style: { display: 'flex', gap: 10, flexWrap: 'wrap' }, children: [_jsx(StatCard, { value: payload.totals.active, label: t('active') }), _jsx(StatCard, { value: payload.totals.verified, label: t('checkPassed') }), _jsx(StatCard, { value: payload.totals.unverified, label: t('unverified') }), _jsx(StatCard, { value: payload.totals.superseded, label: t('superseded') })] }), _jsxs(Card, { title: t('search'), icon: "\uD83D\uDD0D", children: [_jsxs("div", { style: { display: 'flex', gap: 8 }, children: [_jsx(Input, { value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), placeholder: t('searchPlaceholder'), onKeyDown: (e) => { if (e.key === 'Enter')
89
+ handleSearch(); } }), _jsxs(Button, { variant: "secondary", onClick: handleSearch, disabled: searching, children: [searching ? _jsx(Spinner, { size: 14 }) : null, " ", t('search')] })] }), searchHits !== null && (_jsx("div", { style: { marginTop: 10 }, children: searchHits.length === 0 ? _jsx(EmptyState, { message: t('noResults') }) : (_jsx("div", { style: { display: 'flex', flexDirection: 'column', gap: 6 }, children: searchHits.map((hit) => (_jsxs("div", { style: { display: 'flex', gap: 8, alignItems: 'baseline' }, children: [_jsx(Badge, { color: "info", children: hit.score.toFixed(2) }), _jsx("span", { style: { fontSize: 12 }, children: hit.memory.text }), _jsxs("span", { style: { fontSize: 10, opacity: 0.5 }, children: ["\u2014 ", hit.why] })] }, hit.memory.id))) })) }))] }), payload.conflicts.length > 0 && (_jsxs(_Fragment, { children: [_jsx(SectionTitle, { icon: "\u26A0\uFE0F", children: t('conflicts') }), payload.conflicts.map((conflict) => (_jsx(Card, { title: conflict.subject, children: conflict.memories.map((memory) => (_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }, children: [_jsx("code", { style: { fontSize: 11 }, children: memory.id }), _jsx("span", { style: { fontSize: 12, flex: 1 }, children: memory.text }), _jsx(Button, { variant: "danger", size: "sm", onClick: () => setForgetTarget({ id: memory.id, reason: `conflict resolved: ${conflict.subject}` }), children: t('resolve') })] }, memory.id))) }, conflict.subject)))] })), _jsx(SectionTitle, { icon: "\u2705", children: t('active') }), payload.active.length === 0 ? _jsx(EmptyState, { icon: "\uD83D\uDCED", message: t('empty') }) : (_jsx(Card, { padding: 0, children: _jsxs("table", { style: tableStyles.table, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { style: tableStyles.th, children: "Id" }), _jsx("th", { style: tableStyles.th, children: t('memory') }), _jsx("th", { style: tableStyles.th, children: t('kind') }), _jsx("th", { style: tableStyles.th, children: t('status') }), _jsx("th", { style: tableStyles.th, children: t('from') }), _jsx("th", { style: tableStyles.th })] }) }), _jsx("tbody", { children: payload.active.map((memory) => (_jsxs("tr", { children: [_jsx("td", { style: tableStyles.td, children: _jsx("code", { style: { fontSize: 11 }, children: memory.id }) }), _jsx("td", { style: tableStyles.td, children: memory.text }), _jsx("td", { style: tableStyles.td, children: _jsx(Badge, { children: memory.kind }) }), _jsx("td", { style: tableStyles.td, children: checkLabel(memory, t) }), _jsx("td", { style: { ...tableStyles.td, fontSize: 11, opacity: 0.6 }, children: provenance(memory) }), _jsx("td", { style: tableStyles.td, children: _jsx(Button, { variant: "danger", size: "sm", onClick: () => setForgetTarget({ id: memory.id, reason: 'superseded from panel' }), children: "\uD83D\uDDD1" }) })] }, memory.id))) })] }) })), payload.superseded.length > 0 && (_jsxs(_Fragment, { children: [_jsxs(SectionTitle, { icon: "\uD83D\uDCE6", children: [t('superseded'), " (", payload.superseded.length, ")"] }), _jsx(Card, { padding: 0, children: _jsxs("table", { style: tableStyles.table, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { style: tableStyles.th, children: "Id" }), _jsx("th", { style: tableStyles.th, children: t('memory') }), _jsx("th", { style: tableStyles.th, children: t('kind') }), _jsx("th", { style: tableStyles.th, children: t('status') })] }) }), _jsx("tbody", { children: payload.superseded.map((memory) => (_jsxs("tr", { children: [_jsx("td", { style: tableStyles.td, children: _jsx("code", { style: { fontSize: 11 }, children: memory.id }) }), _jsx("td", { style: tableStyles.td, children: memory.text }), _jsx("td", { style: tableStyles.td, children: _jsx(Badge, { children: memory.kind }) }), _jsx("td", { style: tableStyles.td, children: checkLabel(memory, t) })] }, memory.id))) })] }) })] })), addOpen && _jsx(AddMemoryModal, { t: t, onClose: () => setAddOpen(false), onAdded: reload }), forgetTarget !== null && _jsx(ConfirmDialog, { title: t('forget'), message: t('confirmForget'), confirmLabel: t('forget'), danger: true, onConfirm: handleForget, onClose: () => setForgetTarget(null) })] }));
90
+ }
91
+ export function ContextMemoryPanel({ t }) {
92
+ return _jsx(ToastProvider, { children: _jsx(ContextMemoryPanelInner, { t: t }) });
86
93
  }