mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-10-13 04:01:35 +00:00
628 lines
33 KiB
HTML
628 lines
33 KiB
HTML
<!doctype html>
|
|
<!-- Built from status-app/src. Includes @openai/mcp-extensions 0.1.0 app transport (OpenAI, Apache-2.0); see LICENSE-mcp-extensions.txt. -->
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>ReMe status</title>
|
|
<style>
|
|
:root { color-scheme: light dark; --bg: #f6f8fc; --surface: #fff; --panel: #f9faff; --ink: #20283b; --muted: #66718a; --line: #dfe5ee; --blue: #426ccd; --green: #23704b; --green-bg: #e6f2e9; --amber: #875811; --amber-bg: #fff2d9; --red: #a33036; --red-bg: #fbecee; }
|
|
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --bg: #0b0d12; --surface: #12151d; --panel: #181c26; --ink: #f3f5f7; --muted: #979dab; --line: #292e3b; --blue: #79a8ff; --green: #63d4a3; --green-bg: #25382f; --amber: #f2be5c; --amber-bg: #3c3526; --red: #ff7b86; --red-bg: #40262c; } }
|
|
:root[data-theme="dark"] { color-scheme: dark; --bg: #0b0d12; --surface: #12151d; --panel: #181c26; --ink: #f3f5f7; --muted: #979dab; --line: #292e3b; --blue: #79a8ff; --green: #63d4a3; --green-bg: #25382f; --amber: #f2be5c; --amber-bg: #3c3526; --red: #ff7b86; --red-bg: #40262c; }
|
|
:root[data-theme="light"] { color-scheme: light; }
|
|
* { box-sizing: border-box; }
|
|
body { margin: 0; padding: 28px; background: var(--color-background-primary, var(--bg)); color: var(--color-text-primary, var(--ink)); font: 14px/1.5 system-ui, -apple-system, sans-serif; }
|
|
main { max-width: 1100px; margin: auto; }
|
|
header { display: flex; align-items: start; justify-content: space-between; gap: 20px; margin-bottom: 22px; }
|
|
.brand { display: flex; gap: 12px; align-items: center; }
|
|
.logo { display: grid; place-items: center; width: 44px; height: 44px; flex: 0 0 44px; border-radius: 13px; background: linear-gradient(145deg, #678ff7, #725de3); color: #fff; font-size: 26px; }
|
|
.eyebrow { color: var(--blue); font-size: 10px; font-weight: 700; letter-spacing: .14em; text-transform: uppercase; }
|
|
h1 { font-size: 25px; letter-spacing: -.5px; margin: 0; font-weight: 650; }
|
|
h2 { font-size: 14px; margin: 0 0 12px; font-weight: 600; }
|
|
p { margin: 6px 0; }
|
|
.muted, small { color: var(--muted); }
|
|
.label { font-size: 12px; color: var(--muted); }
|
|
.endpoint { overflow-wrap: anywhere; font-size: 12px; font-family: ui-monospace, monospace; }
|
|
.actions { display: flex; gap: 10px; align-items: center; }
|
|
button { background: var(--surface); color: var(--ink); border: 1px solid var(--line); border-radius: 8px; padding: 8px 14px; font: inherit; cursor: pointer; white-space: nowrap; }
|
|
button:hover:not(:disabled) { border-color: var(--blue); }
|
|
button:focus-visible, summary:focus-visible { outline: 2px solid var(--blue); outline-offset: 3px; }
|
|
button:disabled { opacity: .55; cursor: wait; }
|
|
.tabs { display: flex; gap: 4px; overflow-x: auto; border-bottom: 1px solid var(--line); margin-bottom: 20px; }
|
|
.tabs button { color: var(--muted); background: transparent; border: 0; border-radius: 0; border-bottom: 2px solid transparent; padding: 11px 14px; font-weight: 600; }
|
|
.tabs button[aria-selected="true"] { color: var(--ink); border-bottom-color: var(--blue); }
|
|
.grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; }
|
|
.card { background: linear-gradient(150deg, var(--panel), var(--surface)); border: 1px solid var(--line); border-radius: 12px; padding: 18px; min-width: 0; }
|
|
.wide { grid-column: span 2; }
|
|
.full { grid-column: 1 / -1; }
|
|
.value { font-size: 25px; line-height: 1.3; font-weight: 700; margin-top: 8px; }
|
|
.compact { font-size: 16px; overflow-wrap: anywhere; }
|
|
.pill { display: inline-block; color: var(--green); background: var(--green-bg); border-radius: 99px; padding: 4px 10px; font-size: 12px; font-weight: 600; }
|
|
[data-tone="good"] { color: var(--green); }
|
|
[data-tone="bad"] { color: var(--red); }
|
|
.pill[data-tone="neutral"] { color: var(--muted); background: var(--bg); }
|
|
.pill[data-tone="warn"] { color: var(--amber); background: var(--amber-bg); }
|
|
.pill[data-tone="bad"] { color: var(--red); background: var(--red-bg); }
|
|
.flow { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
|
.step { background: var(--surface); border: 1px solid var(--line); padding: 10px; border-radius: 8px; flex: 1 1 100px; }
|
|
.arrow { color: var(--muted); }
|
|
.rows { padding: 0; margin: 0; list-style: none; }
|
|
.rows li { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; padding: 10px 0; border-bottom: 1px solid var(--line); }
|
|
.rows li:last-child { border: 0; padding-bottom: 0; }
|
|
.rows code { display: block; color: var(--muted); font-size: 11px; }
|
|
time { color: var(--muted); font-size: 12px; white-space: nowrap; }
|
|
.component-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
|
|
.component { border: 1px solid var(--line); background: var(--surface); padding: 14px; border-radius: 9px; min-width: 0; }
|
|
.component strong { display: block; overflow-wrap: anywhere; font-size: 13px; }
|
|
details { margin-top: 18px; }
|
|
summary { cursor: pointer; color: var(--muted); }
|
|
pre { white-space: pre-wrap; overflow-wrap: anywhere; background: var(--surface); padding: 14px; border: 1px solid var(--line); border-radius: 8px; font-size: 12px; max-height: 280px; overflow: auto; }
|
|
.note { margin: 18px 0 0; font-size: 12px; color: var(--muted); }
|
|
#error { border: 1px solid var(--red); color: var(--red); padding: 12px; border-radius: 8px; margin-bottom: 16px; }
|
|
[hidden] { display: none !important; }
|
|
@media (max-width: 850px) { .grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
|
@media (max-width: 540px) { body { padding: 18px; } .grid, .component-grid { grid-template-columns: 1fr; } .wide { grid-column: 1 / -1; } .card { padding: 16px; } header { flex-wrap: wrap; gap: 12px; } h1 { font-size: 23px; } .rows li { flex-direction: column; gap: 2px; } }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main aria-busy="true">
|
|
<header>
|
|
<div class="brand"><div class="logo" aria-hidden="true">◈</div><div><div class="eyebrow">Codex memory</div><h1>ReMe status</h1><p id="checked" class="muted label" aria-live="polite">Waiting for status…</p></div></div>
|
|
<div class="actions"><span id="connection-state" class="pill" hidden></span><button id="refresh" type="button" disabled>Refresh</button></div>
|
|
</header>
|
|
<p id="error" role="alert" hidden></p>
|
|
<div id="loading" class="card muted">Opening the connection and memory overview…</div>
|
|
<div id="overview" hidden>
|
|
<nav class="tabs" role="tablist" aria-label="ReMe status">
|
|
<button id="tab-overview" role="tab" aria-controls="panel-overview" aria-selected="true" data-i18n="overview">Overview</button>
|
|
<button id="tab-memory" role="tab" aria-controls="panel-memory" aria-selected="false" tabindex="-1" data-i18n="autoMemory">Auto Memory</button>
|
|
<button id="tab-dream" role="tab" aria-controls="panel-dream" aria-selected="false" tabindex="-1" data-i18n="consolidation">Memory Consolidation</button>
|
|
<button id="tab-components" role="tab" aria-controls="panel-components" aria-selected="false" tabindex="-1" data-i18n="components">Components</button>
|
|
</nav>
|
|
<section id="panel-overview" role="tabpanel" aria-labelledby="tab-overview">
|
|
<div class="grid">
|
|
<article class="card"><div class="label" data-i18n="service">ReMe service</div><div id="service-health" class="value"></div></article>
|
|
<article class="card"><div class="label" data-i18n="runtime">Runtime phase</div><div id="runtime" class="value compact"></div></article>
|
|
<article class="card"><div class="label" data-i18n="queuedTurns">Queued turns</div><div id="overview-queue" class="value"></div></article>
|
|
<article class="card"><div class="label" data-i18n="queuedSessions">Queued sessions</div><div id="overview-sessions" class="value"></div></article>
|
|
<article class="card wide"><h2 data-i18n="automation">Memory automation</h2><div class="flow"><div class="step"><div class="label" data-i18n="prompt">1 · Prompt</div><span data-i18n="recall">Automatic recall</span></div><span class="arrow">→</span><div class="step"><div class="label" data-i18n="response">2 · Response</div><span data-i18n="capture">Turn capture</span></div><span class="arrow">→</span><div class="step"><div class="label" data-i18n="workspace">3 · Workspace</div><span data-i18n="files">Memory files</span></div></div></article>
|
|
<article class="card wide"><h2 data-i18n="connection">Connection</h2><p id="health"></p><p id="endpoint" class="endpoint muted"></p><small id="plugin-version"></small></article>
|
|
</div>
|
|
</section>
|
|
<section id="panel-memory" role="tabpanel" aria-labelledby="tab-memory" hidden>
|
|
<div class="grid">
|
|
<article class="card"><div class="label" data-i18n="memory">Automatic memory</div><p><span id="memory-state" class="pill"></span></p></article>
|
|
<article class="card"><div class="label" data-i18n="interval">Batch interval</div><div id="interval" class="value"></div></article>
|
|
<article class="card"><div class="label" data-i18n="queuedTurns">Queued turns</div><div id="queue" class="value"></div></article>
|
|
<article class="card"><div class="label" data-i18n="queuedSessions">Queued sessions</div><div id="sessions" class="value"></div></article>
|
|
<article class="card full"><h2 data-i18n="recall">Automatic recall</h2><span id="recall-state" class="pill"></span><p id="recall-result"></p><p id="recall-time" class="muted label"></p><p id="other-queue" class="muted label" hidden></p></article>
|
|
<article class="card full"><h2 data-i18n="activity">Recent activity</h2><ul id="activity" class="rows"></ul><p id="no-activity" class="muted" hidden></p></article>
|
|
</div>
|
|
</section>
|
|
<section id="panel-dream" role="tabpanel" aria-labelledby="tab-dream" hidden>
|
|
<div class="grid">
|
|
<article class="card"><div class="label">Auto Dream</div><p><span id="dream-state" class="pill"></span></p></article>
|
|
<article class="card"><div class="label" data-i18n="schedule">Schedule</div><div id="schedule" class="value compact"></div></article>
|
|
<article class="card"><div class="label" data-i18n="timezone">Timezone</div><div id="timezone" class="value compact"></div></article>
|
|
<article class="card"><div class="label" data-i18n="lastResult">Last result</div><div id="last-result" class="value compact"></div></article>
|
|
<article class="card wide"><h2 data-i18n="next">Next run</h2><div id="next" class="value compact"></div><p class="muted" data-i18n="dreamHelp">The daily schedule runs while the ReMe MCP connection is alive.</p></article>
|
|
<article class="card wide"><h2 data-i18n="last">Last consolidation</h2><p id="last"></p><p id="scheduler" class="muted"></p></article>
|
|
</div>
|
|
</section>
|
|
<section id="panel-components" role="tabpanel" aria-labelledby="tab-components" hidden>
|
|
<div class="grid">
|
|
<article class="card wide"><div class="label" data-i18n="processMemory">Process memory (RSS)</div><div id="rss" class="value"></div></article>
|
|
<article class="card wide"><div class="label" data-i18n="componentMemory">Estimated component memory</div><div id="component-total" class="value"></div></article>
|
|
<article class="card full"><h2 data-i18n="components">Components</h2><div id="components" class="component-grid"></div><p id="no-components" class="muted" hidden></p><p class="note" data-i18n="componentHelp">Memory estimates from ReMe status. Component-level health is not included in the MCP response.</p></article>
|
|
</div>
|
|
<details><summary data-i18n="details">Service details</summary><pre id="details"></pre></details>
|
|
</section>
|
|
<p class="note" data-i18n="trust">Connection health does not establish Hook trust. Review ReMe in Codex Hooks settings.</p>
|
|
<p class="note" data-i18n="saved">This view uses saved settings. Save changes in MCP settings, then refresh here.</p>
|
|
</div>
|
|
</main>
|
|
<script>(() => {
|
|
// node_modules/@openai/mcp-extensions/dist/app/transport.js
|
|
function createAppTransport() {
|
|
let sequence = 0;
|
|
const pending = /* @__PURE__ */ new Map();
|
|
const handlers = /* @__PURE__ */ new Map();
|
|
const listeners = /* @__PURE__ */ new Map();
|
|
const send = (message) => window.parent.postMessage({ jsonrpc: "2.0", ...message }, "*");
|
|
const receive = async (event) => {
|
|
if (event.source !== window.parent || event.data?.jsonrpc !== "2.0")
|
|
return;
|
|
const message = event.data;
|
|
if (!message.method && message.id != null) {
|
|
const request = pending.get(message.id);
|
|
if (!request)
|
|
return;
|
|
pending.delete(message.id);
|
|
clearTimeout(request.timer);
|
|
if (message.error)
|
|
request.reject(new Error(message.error.message));
|
|
else
|
|
request.resolve(message.result);
|
|
} else if (message.method && message.id != null) {
|
|
const handler = handlers.get(message.method);
|
|
if (!handler) {
|
|
send({
|
|
id: message.id,
|
|
error: {
|
|
code: -32601,
|
|
message: `Unsupported app method: ${message.method}`
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
try {
|
|
send({
|
|
id: message.id,
|
|
result: await handler(message.params ?? {})
|
|
});
|
|
} catch (error) {
|
|
send({
|
|
id: message.id,
|
|
error: {
|
|
code: -32603,
|
|
message: error instanceof Error ? error.message : String(error)
|
|
}
|
|
});
|
|
}
|
|
} else if (message.method) {
|
|
for (const listener of listeners.get(message.method) ?? [])
|
|
listener(message.params ?? {});
|
|
}
|
|
};
|
|
window.addEventListener("message", receive);
|
|
return {
|
|
request(method, params = {}, timeoutMs = 15e3) {
|
|
if (window.parent === window)
|
|
return Promise.reject(new Error(`Open this app in an MCP host to use ${method}`));
|
|
const id = ++sequence;
|
|
return new Promise((resolve, reject) => {
|
|
const timer = setTimeout(() => {
|
|
pending.delete(id);
|
|
reject(new Error(`${method} timed out`));
|
|
}, timeoutMs);
|
|
pending.set(id, {
|
|
resolve: (value) => resolve(value),
|
|
reject,
|
|
timer
|
|
});
|
|
send({ id, method, params });
|
|
});
|
|
},
|
|
notify(method, params = {}) {
|
|
send({ method, params });
|
|
},
|
|
handle(method, handler) {
|
|
handlers.set(method, handler);
|
|
},
|
|
on(method, listener) {
|
|
if (!listeners.has(method))
|
|
listeners.set(method, /* @__PURE__ */ new Set());
|
|
listeners.get(method).add(listener);
|
|
return () => {
|
|
listeners.get(method)?.delete(listener);
|
|
};
|
|
},
|
|
dispose() {
|
|
window.removeEventListener("message", receive);
|
|
for (const request of pending.values()) {
|
|
clearTimeout(request.timer);
|
|
request.reject(new Error("App disposed"));
|
|
}
|
|
pending.clear();
|
|
handlers.clear();
|
|
listeners.clear();
|
|
}
|
|
};
|
|
}
|
|
|
|
// src/status.js
|
|
var $ = (id) => document.getElementById(id);
|
|
var transport = createAppTransport();
|
|
var connected = false;
|
|
var disposed = false;
|
|
var language = "en";
|
|
var zone = "UTC";
|
|
var hasSnapshot = false;
|
|
var refreshing = false;
|
|
var words = {
|
|
en: {
|
|
overview: "Overview",
|
|
autoMemory: "Auto Memory",
|
|
consolidation: "Memory Consolidation",
|
|
components: "Components",
|
|
service: "ReMe service",
|
|
runtime: "Runtime phase",
|
|
queuedTurns: "Queued turns",
|
|
queuedSessions: "Queued sessions",
|
|
automation: "Memory automation",
|
|
prompt: "1 \xB7 Prompt",
|
|
response: "2 \xB7 Response",
|
|
workspace: "3 \xB7 Workspace",
|
|
capture: "Turn capture",
|
|
files: "Memory files",
|
|
interval: "Batch interval",
|
|
timezone: "Timezone",
|
|
lastResult: "Last result",
|
|
healthy: "Healthy",
|
|
unhealthy: "Unhealthy",
|
|
unknown: "Unknown",
|
|
processMemory: "Process memory (RSS)",
|
|
componentMemory: "Estimated component memory",
|
|
noComponents: "No component memory details returned.",
|
|
componentHelp: "Memory estimates from ReMe status. Component-level health is not included in the MCP response.",
|
|
dreamHelp: "The daily schedule runs while the ReMe MCP connection is alive.",
|
|
connection: "Connection",
|
|
memory: "Automatic memory",
|
|
recall: "Automatic recall",
|
|
schedule: "Schedule",
|
|
next: "Next run",
|
|
scheduler: "Scheduler",
|
|
last: "Last consolidation",
|
|
activity: "Recent activity",
|
|
details: "Service details",
|
|
on: "Enabled",
|
|
off: "Disabled",
|
|
connected: "Connected",
|
|
offline: "Unavailable",
|
|
refresh: "Refresh",
|
|
refreshing: "Refreshing\u2026",
|
|
checked: "Checked",
|
|
empty: "No activity recorded yet.",
|
|
waiting: "Waiting for the first status result\u2026",
|
|
unavailable: "Status could not be refreshed. Try again or reopen ReMe status.",
|
|
noHost: "Open ReMe status from Codex MCP settings to view live data.",
|
|
trust: "Connection health does not establish Hook trust. Review ReMe in Codex Hooks settings.",
|
|
saved: "This view uses saved settings. Save changes in MCP settings, then refresh here.",
|
|
pending: (n) => `${n} queued turns`,
|
|
batch: (n, s) => `Batch size: ${n} turns \xB7 ${s} queued sessions`,
|
|
other: (n) => `${n} turns are queued for other service addresses.`,
|
|
noRecall: "No recall activity yet",
|
|
noDream: "No consolidation recorded yet",
|
|
paused: "No run scheduled",
|
|
running: "Running",
|
|
starting: "Starting",
|
|
updating: "Applying settings",
|
|
stopped: "Stopped",
|
|
scheduled: "Scheduled",
|
|
manual: "Manual",
|
|
completed: "Completed",
|
|
failed: "Failed",
|
|
cancelled: "Cancelled",
|
|
interrupted: "Interrupted",
|
|
recall_found: "Relevant memory recalled",
|
|
recall_empty: "No matching memory",
|
|
recall_failed: "Recall failed",
|
|
recall_disabled: "Recall disabled",
|
|
memory_queued: "Turn queued",
|
|
memory_saved: "Memory saved",
|
|
memory_failed: "Memory delivery failed",
|
|
memory_disabled: "Recording disabled",
|
|
retry_failed: "Delivery retry failed",
|
|
dream_started: "Consolidation started",
|
|
dream_completed: "Consolidation completed",
|
|
dream_failed: "Consolidation failed",
|
|
dream_cancelled: "Consolidation cancelled",
|
|
scheduled_dream_failed: "Scheduled consolidation failed",
|
|
search_found: "Search found memory",
|
|
search_empty: "Search found no memory",
|
|
search_failed: "Search failed",
|
|
capture_skipped: "No completed turn captured",
|
|
SessionStart: "Session started",
|
|
SessionEnd: "Session ended",
|
|
Stop: "Turn completed",
|
|
SubagentStop: "Subagent turn completed",
|
|
UserPromptSubmit: "Prompt submitted"
|
|
},
|
|
zh: {
|
|
overview: "\u603B\u89C8",
|
|
autoMemory: "\u81EA\u52A8\u8BB0\u5FC6",
|
|
consolidation: "\u8BB0\u5FC6\u6574\u7406",
|
|
components: "\u7EC4\u4EF6",
|
|
service: "ReMe \u670D\u52A1",
|
|
runtime: "\u8FD0\u884C\u72B6\u6001",
|
|
queuedTurns: "\u5F85\u63D0\u4EA4\u8F6E\u6570",
|
|
queuedSessions: "\u5F85\u63D0\u4EA4\u4F1A\u8BDD",
|
|
automation: "\u8BB0\u5FC6\u6D41\u7A0B",
|
|
prompt: "1 \xB7 \u63D0\u95EE",
|
|
response: "2 \xB7 \u56DE\u590D",
|
|
workspace: "3 \xB7 \u5DE5\u4F5C\u533A",
|
|
capture: "\u8BB0\u5F55\u5B8C\u6210\u8F6E\u6B21",
|
|
files: "\u8BB0\u5FC6\u6587\u4EF6",
|
|
interval: "\u6279\u6B21\u8F6E\u6570",
|
|
timezone: "\u65F6\u533A",
|
|
lastResult: "\u6700\u8FD1\u7ED3\u679C",
|
|
healthy: "\u5065\u5EB7",
|
|
unhealthy: "\u5F02\u5E38",
|
|
unknown: "\u672A\u77E5",
|
|
processMemory: "\u8FDB\u7A0B\u5185\u5B58\uFF08RSS\uFF09",
|
|
componentMemory: "\u7EC4\u4EF6\u5185\u5B58\u4F30\u7B97",
|
|
noComponents: "\u6682\u65E0\u7EC4\u4EF6\u5185\u5B58\u4FE1\u606F\u3002",
|
|
componentHelp: "\u5185\u5B58\u4E3A ReMe status \u8FD4\u56DE\u7684\u4F30\u7B97\u503C\uFF1BMCP \u54CD\u5E94\u4E0D\u5305\u542B\u7EC4\u4EF6\u7EA7\u5065\u5EB7\u4FE1\u606F\u3002",
|
|
dreamHelp: "\u53EA\u6709 ReMe MCP \u8FDE\u63A5\u5B58\u6D3B\u65F6\uFF0C\u6BCF\u65E5\u8BA1\u5212\u624D\u4F1A\u89E6\u53D1\u3002",
|
|
connection: "\u670D\u52A1\u8FDE\u63A5",
|
|
memory: "\u81EA\u52A8\u8BB0\u5F55",
|
|
recall: "\u81EA\u52A8\u53EC\u56DE",
|
|
schedule: "\u6574\u7406\u8BA1\u5212",
|
|
next: "\u4E0B\u6B21\u8FD0\u884C",
|
|
scheduler: "\u8C03\u5EA6\u72B6\u6001",
|
|
last: "\u6700\u8FD1\u6574\u7406",
|
|
activity: "\u6700\u8FD1\u6D3B\u52A8",
|
|
details: "\u670D\u52A1\u8BE6\u60C5",
|
|
on: "\u5DF2\u5F00\u542F",
|
|
off: "\u5DF2\u5173\u95ED",
|
|
connected: "\u8FDE\u63A5\u6B63\u5E38",
|
|
offline: "\u65E0\u6CD5\u8FDE\u63A5",
|
|
refresh: "\u5237\u65B0",
|
|
refreshing: "\u5237\u65B0\u4E2D\u2026",
|
|
checked: "\u68C0\u67E5\u65F6\u95F4",
|
|
empty: "\u6682\u65E0\u6D3B\u52A8\u8BB0\u5F55\u3002",
|
|
waiting: "\u6B63\u5728\u7B49\u5F85\u9996\u6B21\u72B6\u6001\u7ED3\u679C\u2026",
|
|
unavailable: "\u672A\u80FD\u5237\u65B0\u72B6\u6001\uFF0C\u8BF7\u91CD\u8BD5\u6216\u91CD\u65B0\u6253\u5F00 ReMe status\u3002",
|
|
noHost: "\u8BF7\u4ECE Codex \u7684 MCP \u8BBE\u7F6E\u4E2D\u6253\u5F00 ReMe status \u67E5\u770B\u5B9E\u65F6\u6570\u636E\u3002",
|
|
trust: "\u670D\u52A1\u8FDE\u63A5\u6B63\u5E38\u4E0D\u4EE3\u8868 Hook \u5DF2\u83B7\u4FE1\u4EFB\uFF0C\u8BF7\u5728 Codex Hooks \u8BBE\u7F6E\u4E2D\u5BA1\u6838 ReMe \u6761\u76EE\u3002",
|
|
saved: "\u6B64\u9875\u9762\u4F7F\u7528\u5DF2\u4FDD\u5B58\u7684\u914D\u7F6E\u3002\u4FEE\u6539 MCP \u8BBE\u7F6E\u540E\uFF0C\u8BF7\u5148\u4FDD\u5B58\uFF0C\u518D\u5237\u65B0\u6B64\u9875\u9762\u3002",
|
|
pending: (n) => `${n} \u8F6E\u5F85\u63D0\u4EA4`,
|
|
batch: (n, s) => `\u6BCF\u6279 ${n} \u8F6E \xB7 ${s} \u4E2A\u4F1A\u8BDD\u5F85\u63D0\u4EA4`,
|
|
other: (n) => `\u53E6\u6709 ${n} \u8F6E\u7B49\u5F85\u63D0\u4EA4\u81F3\u5176\u4ED6\u670D\u52A1\u5730\u5740\u3002`,
|
|
noRecall: "\u6682\u65E0\u53EC\u56DE\u6D3B\u52A8",
|
|
noDream: "\u6682\u65E0\u6574\u7406\u8BB0\u5F55",
|
|
paused: "\u6682\u65E0\u8BA1\u5212",
|
|
running: "\u8FD0\u884C\u4E2D",
|
|
starting: "\u542F\u52A8\u4E2D",
|
|
updating: "\u6B63\u5728\u5E94\u7528\u914D\u7F6E",
|
|
stopped: "\u5DF2\u505C\u6B62",
|
|
scheduled: "\u5B9A\u65F6",
|
|
manual: "\u624B\u52A8",
|
|
completed: "\u5DF2\u5B8C\u6210",
|
|
failed: "\u5931\u8D25",
|
|
cancelled: "\u5DF2\u53D6\u6D88",
|
|
interrupted: "\u5DF2\u4E2D\u65AD",
|
|
recall_found: "\u5DF2\u53EC\u56DE\u76F8\u5173\u8BB0\u5FC6",
|
|
recall_empty: "\u672A\u627E\u5230\u76F8\u5173\u8BB0\u5FC6",
|
|
recall_failed: "\u53EC\u56DE\u5931\u8D25",
|
|
recall_disabled: "\u81EA\u52A8\u53EC\u56DE\u5DF2\u5173\u95ED",
|
|
memory_queued: "\u5DF2\u52A0\u5165\u8BB0\u5F55\u961F\u5217",
|
|
memory_saved: "\u8BB0\u5FC6\u5DF2\u4FDD\u5B58",
|
|
memory_failed: "\u8BB0\u5FC6\u63D0\u4EA4\u5931\u8D25",
|
|
memory_disabled: "\u81EA\u52A8\u8BB0\u5F55\u5DF2\u5173\u95ED",
|
|
retry_failed: "\u91CD\u8BD5\u63D0\u4EA4\u5931\u8D25",
|
|
dream_started: "\u5F00\u59CB\u6574\u7406",
|
|
dream_completed: "\u6574\u7406\u5DF2\u5B8C\u6210",
|
|
dream_failed: "\u6574\u7406\u5931\u8D25",
|
|
dream_cancelled: "\u6574\u7406\u5DF2\u53D6\u6D88",
|
|
scheduled_dream_failed: "\u5B9A\u65F6\u6574\u7406\u5931\u8D25",
|
|
search_found: "\u68C0\u7D22\u5230\u76F8\u5173\u8BB0\u5FC6",
|
|
search_empty: "\u672A\u68C0\u7D22\u5230\u76F8\u5173\u8BB0\u5FC6",
|
|
search_failed: "\u68C0\u7D22\u5931\u8D25",
|
|
capture_skipped: "\u672A\u6355\u83B7\u5230\u5B8C\u6210\u8F6E\u6B21",
|
|
SessionStart: "\u4F1A\u8BDD\u5F00\u59CB",
|
|
SessionEnd: "\u4F1A\u8BDD\u7ED3\u675F",
|
|
Stop: "\u8F6E\u6B21\u5B8C\u6210",
|
|
SubagentStop: "\u5B50\u4EE3\u7406\u8F6E\u6B21\u5B8C\u6210",
|
|
UserPromptSubmit: "\u5DF2\u63D0\u4EA4\u63D0\u95EE"
|
|
}
|
|
};
|
|
var t = (key) => words[language][key] ?? key;
|
|
var text = (id, value) => {
|
|
$(id).textContent = value;
|
|
};
|
|
function formatTime(value) {
|
|
if (value == null) return "\u2014";
|
|
const date = new Date(typeof value === "number" ? value * 1e3 : value);
|
|
if (!Number.isFinite(date.getTime())) return "\u2014";
|
|
return new Intl.DateTimeFormat(language === "zh" ? "zh-CN" : "en-GB", {
|
|
timeZone: zone,
|
|
year: "numeric",
|
|
month: "2-digit",
|
|
day: "2-digit",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
second: "2-digit",
|
|
timeZoneName: "short"
|
|
}).format(date);
|
|
}
|
|
function pill(id, value, tone = "good") {
|
|
text(id, value);
|
|
$(id).dataset.tone = tone;
|
|
}
|
|
function toggle(id, enabled) {
|
|
pill(id, t(enabled ? "on" : "off"), enabled ? "good" : "neutral");
|
|
}
|
|
function applyHost(context = {}) {
|
|
if (["dark", "light"].includes(context.theme)) document.documentElement.dataset.theme = context.theme;
|
|
for (const key of ["--color-background-primary", "--color-text-primary"]) {
|
|
const value = context.styles?.variables?.[key];
|
|
if (typeof value === "string") document.documentElement.style.setProperty(key, value);
|
|
}
|
|
}
|
|
function fail() {
|
|
if (disposed) return;
|
|
text("error", t("unavailable"));
|
|
$("error").hidden = false;
|
|
$("loading").hidden = true;
|
|
document.querySelector("main").setAttribute("aria-busy", "false");
|
|
}
|
|
function render(result) {
|
|
if (disposed) return;
|
|
const data = result?.structuredContent;
|
|
if (result?.isError || !data?.service?.health_check || !data?.auto_memory || !data?.auto_dream) {
|
|
fail();
|
|
return;
|
|
}
|
|
language = data.language === "zh" ? "zh" : "en";
|
|
zone = data.auto_dream.timezone;
|
|
document.documentElement.lang = language === "zh" ? "zh-CN" : "en";
|
|
document.querySelectorAll("[data-i18n]").forEach((node) => {
|
|
node.textContent = t(node.dataset.i18n);
|
|
});
|
|
text("refresh", t(refreshing ? "refreshing" : "refresh"));
|
|
const health = data.service.health_check;
|
|
pill("connection-state", t(health.reachable ? "connected" : "offline"), health.reachable ? "good" : "bad");
|
|
$("connection-state").hidden = false;
|
|
const summary = typeof health.answer === "string" ? health.answer.trim().match(/(?:^|-\s*)(unhealthy|healthy)$/) : null;
|
|
text("service-health", t(!health.reachable ? "offline" : summary?.[1] ?? "unknown"));
|
|
$("service-health").dataset.tone = !health.reachable || summary?.[1] === "unhealthy" ? "bad" : summary ? "good" : "neutral";
|
|
text("runtime", t(data.auto_dream.phase));
|
|
text("overview-queue", data.auto_memory.queued_turns);
|
|
text("overview-sessions", data.auto_memory.queued_sessions);
|
|
text("health", health.reachable ? typeof health.answer === "string" ? health.answer : JSON.stringify(health.answer) : health.error);
|
|
text("endpoint", data.mcpUrl);
|
|
text("plugin-version", `ReMe for Codex \xB7 ${data.plugin_version}`);
|
|
text("checked", `${t("checked")} ${formatTime(data.checked_at)}`);
|
|
toggle("memory-state", data.auto_memory.enabled);
|
|
text("queue", data.auto_memory.queued_turns);
|
|
text("interval", data.auto_memory.interval);
|
|
text("sessions", data.auto_memory.queued_sessions);
|
|
$("other-queue").hidden = !data.auto_memory.other_endpoint_queued_turns;
|
|
text("other-queue", t("other")(data.auto_memory.other_endpoint_queued_turns));
|
|
toggle("recall-state", data.auto_recall.enabled);
|
|
const activity = data.recent_activity ?? [];
|
|
const recall = activity.findLast((row) => row.event?.startsWith("recall_"));
|
|
text("recall-result", recall ? t(recall.event) : t("noRecall"));
|
|
text("recall-time", recall ? formatTime(recall.time) : "\u2014");
|
|
const dream = data.auto_dream;
|
|
toggle("dream-state", dream.enabled);
|
|
text("schedule", dream.cron);
|
|
text("timezone", dream.timezone);
|
|
text("next", dream.next_run_at ? formatTime(dream.next_run_at) : t("paused"));
|
|
text("scheduler", `${t("scheduler")}: ${t(dream.phase)}`);
|
|
const last = dream.last_run;
|
|
text("last-result", last ? t(last.status) : "\u2014");
|
|
text("last", last ? `${t(last.origin)} \xB7 ${t(last.status)} \xB7 ${formatTime(last.completed_at ?? last.started_at)}${last.error ? ` \xB7 ${last.error}` : ""}` : t("noDream"));
|
|
$("activity").replaceChildren();
|
|
for (const row of activity.slice(-20).reverse()) {
|
|
const item = document.createElement("li");
|
|
const description = document.createElement("span");
|
|
description.textContent = t(row.event);
|
|
const code = document.createElement("code");
|
|
code.textContent = `${row.event}${row.turns ? ` \xB7 ${row.turns}` : ""}${row.error ? ` \xB7 ${row.error}` : ""}`;
|
|
description.append(code);
|
|
const time = document.createElement("time");
|
|
time.textContent = formatTime(row.time);
|
|
item.append(description, time);
|
|
$("activity").append(item);
|
|
}
|
|
$("no-activity").hidden = activity.length > 0;
|
|
text("no-activity", t("empty"));
|
|
renderComponents(data.service.status);
|
|
text("details", JSON.stringify(data.service, null, 2));
|
|
$("error").hidden = true;
|
|
$("loading").hidden = true;
|
|
$("overview").hidden = false;
|
|
document.querySelector("main").setAttribute("aria-busy", "false");
|
|
hasSnapshot = true;
|
|
}
|
|
function renderComponents(status) {
|
|
const report = status?.reachable && typeof status.answer === "string" ? status.answer : "";
|
|
const amount = "([0-9]+(?:\\.[0-9]+)? (?:B|KiB|MiB|GiB|TiB))";
|
|
text("rss", report.match(new RegExp(`^ Process RSS +${amount}$`, "m"))?.[1] ?? "\u2014");
|
|
text("component-total", report.match(new RegExp(`^ Components total +${amount}$`, "m"))?.[1] ?? "\u2014");
|
|
$("components").replaceChildren();
|
|
for (const row of report.matchAll(new RegExp(`^ ([a-z_]+):([^\\s:]+) +${amount}$`, "gm"))) {
|
|
const card = document.createElement("div");
|
|
card.className = "component";
|
|
const title = document.createElement("strong");
|
|
title.textContent = `${row[1].replaceAll("_", " ")} \xB7 ${row[2]}`;
|
|
const size = document.createElement("div");
|
|
size.className = "value compact";
|
|
size.textContent = row[3];
|
|
card.append(title, size);
|
|
$("components").append(card);
|
|
}
|
|
$("no-components").hidden = $("components").childElementCount > 0;
|
|
text("no-components", status?.reachable === false ? status.error : t("noComponents"));
|
|
}
|
|
var tabs = [...document.querySelectorAll('[role="tab"]')];
|
|
function selectTab(selected) {
|
|
for (const tab of tabs) {
|
|
const active = tab === selected;
|
|
tab.setAttribute("aria-selected", String(active));
|
|
tab.tabIndex = active ? 0 : -1;
|
|
$(tab.getAttribute("aria-controls")).hidden = !active;
|
|
}
|
|
}
|
|
for (const tab of tabs) {
|
|
tab.addEventListener("click", () => selectTab(tab));
|
|
tab.addEventListener("keydown", (event) => {
|
|
const index = tabs.indexOf(tab);
|
|
const next = { ArrowRight: (index + 1) % tabs.length, ArrowLeft: (index + tabs.length - 1) % tabs.length, Home: 0, End: tabs.length - 1 }[event.key];
|
|
if (next == null) return;
|
|
event.preventDefault();
|
|
selectTab(tabs[next]);
|
|
tabs[next].focus();
|
|
});
|
|
}
|
|
var resizeFrame;
|
|
var resizeObserver = new ResizeObserver(() => {
|
|
cancelAnimationFrame(resizeFrame);
|
|
resizeFrame = requestAnimationFrame(() => {
|
|
if (connected && !disposed) transport.notify("ui/notifications/size-changed", { height: Math.ceil(document.body.getBoundingClientRect().height) });
|
|
});
|
|
});
|
|
resizeObserver.observe(document.body);
|
|
transport.on("ui/notifications/tool-result", (result) => {
|
|
try {
|
|
render(result);
|
|
} catch {
|
|
fail();
|
|
}
|
|
});
|
|
transport.on("ui/notifications/tool-cancelled", fail);
|
|
transport.on("ui/notifications/host-context-changed", applyHost);
|
|
transport.handle("ui/resource-teardown", () => {
|
|
disposed = true;
|
|
connected = false;
|
|
$("refresh").disabled = true;
|
|
resizeObserver.disconnect();
|
|
cancelAnimationFrame(resizeFrame);
|
|
setTimeout(() => transport.dispose(), 0);
|
|
return {};
|
|
});
|
|
$("refresh").addEventListener("click", async () => {
|
|
if (!connected || refreshing) return;
|
|
refreshing = true;
|
|
$("refresh").disabled = true;
|
|
text("refresh", t("refreshing"));
|
|
document.querySelector("main").setAttribute("aria-busy", "true");
|
|
try {
|
|
render(await transport.request("tools/call", { name: "reme_status", arguments: {} }, 125e3));
|
|
} catch {
|
|
fail();
|
|
} finally {
|
|
refreshing = false;
|
|
$("refresh").disabled = !connected;
|
|
text("refresh", t("refresh"));
|
|
}
|
|
});
|
|
async function connect() {
|
|
if (window.parent === window) {
|
|
text("loading", t("noHost"));
|
|
return;
|
|
}
|
|
const host = await transport.request("ui/initialize", {
|
|
protocolVersion: "2026-01-26",
|
|
appInfo: { name: "reme-status", version: "0.2.1" },
|
|
appCapabilities: { availableDisplayModes: ["fullscreen"] }
|
|
});
|
|
if (host.protocolVersion !== "2026-01-26") throw new Error("Unsupported UI protocol");
|
|
connected = true;
|
|
applyHost(host.hostContext);
|
|
$("refresh").disabled = false;
|
|
if (!hasSnapshot) text("loading", t("waiting"));
|
|
transport.notify("ui/notifications/initialized");
|
|
const context = host.hostContext ?? {};
|
|
if (context.displayMode !== "fullscreen" && context.availableDisplayModes?.includes("fullscreen")) {
|
|
await transport.request("ui/request-display-mode", { mode: "fullscreen" }).catch(() => {
|
|
});
|
|
}
|
|
}
|
|
connect().catch(fail);
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|