ReMe/integrations/codex/plugins/reme/ui/status.html
Sen Huang 749c653f23
feat(integrations): add automatic memory plugins for Claude Code and Codex (#575)
* feat(integrations): add native Claude Code and Codex memory plugins

* docs(integrations): make plugin installation the coding agent entry point

* feat(integrations): configure plugin connections from user settings

* refactor(codex): use MCP tools and native plugin settings

* fix(codex): restore plugin hooks on current runtimes

* docs(codex): expand setup guide and reserve screenshot slots

* feat(codex): add configurable memory consolidation and status

* feat(codex): merge connection checks into a tabbed status app

* docs(codex): diagnose blocked status panel sandbox loading

* fix(codex): simplify status panel layout for narrow cards

* docs(codex): focus guides on connecting and using ReMe

* docs(codex): specify validated CLI and desktop version requirements

* feat(codex): add ReMe branding to plugin icons

* docs(codex): prepare manual screenshot workflow

* fix(codex): simplify ReMe status layout and activity

* docs(codex): explain Hook loading and local project context

* fix(codex): keep status refresh clear of host close control

* fix(codex): retain status resources for active MCP connections

* docs(codex): add real desktop screenshots to usage guides

* fix(integrations): address memory capture and consolidation review

* fix(codex): preserve capture endpoint during background delivery
2026-10-12 20:12:47 +08:00

680 lines
35 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: clamp(16px, 4vw, 32px); background: var(--color-background-primary, var(--bg)); color: var(--color-text-primary, var(--ink)); font: 14px/1.6 system-ui, -apple-system, sans-serif; }
main { max-width: 880px; margin: auto; overflow-wrap: anywhere; }
header { display: grid; gap: 12px; margin-bottom: 28px; }
header > div { min-width: 0; }
h1 { font-size: 22px; letter-spacing: -.4px; margin: 0; padding-inline-end: 48px; font-weight: 650; }
.status-toolbar { display: flex; align-items: center; flex-wrap: wrap; gap: 8px 16px; }
#checked { margin: 0; }
h2 { font-size: 15px; line-height: 1.5; margin: 0 0 24px; font-weight: 600; }
p { margin: 6px 0; }
.muted, small { color: var(--muted); }
.label { font-size: 13px; color: var(--muted); }
.endpoint { overflow-wrap: anywhere; font-size: 12px; font-family: ui-monospace, monospace; }
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: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; border-bottom: 1px solid var(--line); margin-bottom: 28px; }
.tabs button { min-width: 0; min-height: 44px; color: var(--muted); background: transparent; border: 0; border-bottom: 2px solid transparent; border-radius: 0; padding: 12px 8px; font-weight: 500; white-space: normal; line-height: 1.4; }
.tabs button[aria-selected="true"] { color: var(--blue); border-bottom-color: var(--blue); font-weight: 600; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 360px), 1fr)); gap: 24px; align-items: start; }
.card { background: var(--surface); border: 1px solid var(--line); border-radius: 14px; padding: 28px; min-width: 0; }
.full { grid-column: 1 / -1; }
.value { font-size: 26px; line-height: 1.35; font-weight: 650; margin-top: 8px; }
.compact { font-size: 16px; overflow-wrap: anywhere; }
.pill { display: inline-block; flex-shrink: 0; 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); }
.section-heading { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px 20px; margin-bottom: 24px; }
.section-heading h2 { margin: 0; }
.metrics { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px; }
.service-metrics { margin-top: 24px; padding-top: 24px; border-top: 1px solid var(--line); }
.properties { margin: 0; }
.properties > div { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: baseline; gap: 8px 24px; padding: 16px 0; border-bottom: 1px solid var(--line); }
.properties > div:first-child { padding-top: 0; }
.properties > div:last-child { padding-bottom: 0; border: 0; }
.properties dt { color: var(--muted); }
.properties dd { margin: 0; text-align: right; font-weight: 600; min-width: 0; }
.rows { padding: 0; margin: 0; list-style: none; }
.rows li { border-bottom: 1px solid var(--line); }
.rows li:last-child { border: 0; }
.rows .activity-entry { margin: 0; }
.activity-entry > summary { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: baseline; gap: 8px 20px; padding: 16px 0; color: var(--ink); list-style: none; }
.activity-entry > summary::-webkit-details-marker { display: none; }
.activity-title { display: flex; align-items: baseline; gap: 10px; min-width: 0; }
.activity-title::before { content: "▸"; color: var(--muted); flex-shrink: 0; }
.activity-entry[open] .activity-title::before { content: "▾"; }
.activity-entry[open] > summary { margin-bottom: 0; }
.activity-meta { display: block; color: var(--muted); font-size: 12px; margin: 0 0 16px 20px; }
time { color: var(--muted); font-size: 12px; }
#activity-more { margin-top: 16px; }
.component-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr)); gap: 16px; }
.component { border: 1px solid var(--line); background: var(--panel); padding: 18px; border-radius: 9px; min-width: 0; }
.component strong { display: block; overflow-wrap: anywhere; font-size: 13px; }
details { margin-top: 20px; }
details.card { margin-top: 0; }
details[open] > summary { margin-bottom: 16px; }
.card > summary { color: var(--ink); font-weight: 600; font-size: 15px; }
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: 20px 0 0; font-size: 13px; 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: 600px) { .card { padding: 24px; } header { gap: 16px; } .activity-entry > summary { grid-template-columns: 1fr; gap: 4px; } .activity-entry time { padding-left: 20px; } }
@media (max-width: 560px) { .tabs { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 380px) { .card { padding: 20px; } .properties > div { grid-template-columns: 1fr; gap: 4px; } .properties dd { text-align: left; } .metrics { grid-template-columns: 1fr; gap: 16px; } .metrics > div { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; } .metrics .value { margin: 0; } }
</style>
</head>
<body>
<main aria-busy="true">
<header>
<h1>ReMe status</h1>
<div class="status-toolbar">
<button id="refresh" type="button" disabled>Refresh</button>
<p id="checked" class="muted label" aria-live="polite">Waiting for status…</p>
</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">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 full">
<div class="section-heading"><h2 data-i18n="service">ReMe service</h2><span id="connection-state" class="pill" hidden></span></div>
<div id="service-health" class="value"></div><p id="health" class="muted"></p>
<div class="metrics service-metrics">
<div><div class="label" data-i18n="queuedTurns">Queued turns</div><div id="overview-queue" class="value"></div></div>
<div><div class="label" data-i18n="queuedSessions">Queued sessions</div><div id="overview-sessions" class="value"></div></div>
</div>
<details id="connection-details"><summary data-i18n="connectionDetails">Connection details</summary><p id="endpoint" class="endpoint muted"></p><small id="plugin-version"></small></details>
</article>
</div>
</section>
<section id="panel-memory" role="tabpanel" aria-labelledby="tab-memory" hidden>
<div class="grid">
<article class="card">
<div class="section-heading"><h2 data-i18n="memory">Automatic memory</h2><span id="memory-state" class="pill"></span></div>
<dl class="properties">
<div><dt data-i18n="interval">Batch interval</dt><dd id="interval"></dd></div>
<div><dt data-i18n="queuedTurns">Queued turns</dt><dd id="queue"></dd></div>
<div><dt data-i18n="queuedSessions">Queued sessions</dt><dd id="sessions"></dd></div>
</dl><p id="other-queue" class="note" hidden></p>
</article>
<article class="card"><div class="section-heading"><h2 data-i18n="recall">Automatic recall</h2><span id="recall-state" class="pill"></span></div><p id="recall-result"></p><p id="recall-time" class="muted label"></p></article>
<details id="activity-details" class="card full"><summary data-i18n="activity">Recent activity</summary><ul id="activity" class="rows"></ul><p id="no-activity" class="muted" hidden></p><button id="activity-more" type="button" aria-controls="activity" aria-expanded="false" hidden></button></details>
</div>
</section>
<section id="panel-dream" role="tabpanel" aria-labelledby="tab-dream" hidden>
<div class="grid">
<article class="card full">
<div class="section-heading"><h2>Auto Dream</h2><span id="dream-state" class="pill"></span></div>
<dl class="properties">
<div><dt data-i18n="schedule">Schedule</dt><dd id="schedule"></dd></div>
<div><dt data-i18n="timezone">Timezone</dt><dd id="timezone"></dd></div>
<div><dt data-i18n="lastResult">Last result</dt><dd id="last-result"></dd></div>
</dl>
</article>
<article class="card"><h2 data-i18n="next">Next run</h2><div id="next" class="value compact"></div><p class="note" data-i18n="dreamHelp">The daily schedule runs while the ReMe MCP connection is alive.</p></article>
<article class="card"><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"><div class="label" data-i18n="processMemory">Process memory (RSS)</div><div id="rss" class="value"></div></article>
<article class="card"><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 id="service-details"><summary data-i18n="details">Service details</summary><pre id="details"></pre></details>
</section>
<details id="view-help"><summary data-i18n="viewHelp">About this view</summary>
<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>
</details>
</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 recentActivity = [];
var showAllActivity = false;
var ACTIVITY_PREVIEW_LIMIT = 5;
var words = {
en: {
overview: "Overview",
autoMemory: "Auto Memory",
consolidation: "Consolidation",
components: "Components",
service: "ReMe service",
queuedTurns: "Queued turns",
queuedSessions: "Queued sessions",
connectionDetails: "Connection details",
viewHelp: "About this view",
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.",
showActivity: (n) => `Show all ${n} events`,
lessActivity: "Show fewer events",
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",
queuedTurns: "\u5F85\u63D0\u4EA4\u8F6E\u6570",
queuedSessions: "\u5F85\u63D0\u4EA4\u4F1A\u8BDD",
connectionDetails: "\u8FDE\u63A5\u8BE6\u60C5",
viewHelp: "\u4F7F\u7528\u8BF4\u660E",
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",
showActivity: (n) => `\u67E5\u770B\u5168\u90E8 ${n} \u6761\u6D3B\u52A8`,
lessActivity: "\u6536\u8D77\u66F4\u591A\u6D3B\u52A8",
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, brief = false) {
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",
...brief ? {} : { 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("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, true)}`);
$("checked").title = 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, true) : "\u2014");
$("recall-time").title = recall ? formatTime(recall.time) : "";
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, true) : t("paused"));
$("next").title = dream.next_run_at ? formatTime(dream.next_run_at) : "";
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"));
recentActivity = activity.slice(-20).reverse();
renderActivity();
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 renderActivity() {
const expanded = new Set([...$("activity").querySelectorAll("details[open]")].map((node) => node.dataset.key));
$("activity").replaceChildren();
for (const row of recentActivity.slice(0, showAllActivity ? recentActivity.length : ACTIVITY_PREVIEW_LIMIT)) {
const item = document.createElement("li");
const entry = document.createElement("details");
entry.className = "activity-entry";
entry.dataset.key = JSON.stringify([row.time, row.event, row.turns, row.error]);
entry.open = expanded.has(entry.dataset.key);
const summary = document.createElement("summary");
const description = document.createElement("span");
description.className = "activity-title";
description.textContent = t(row.event);
const time = document.createElement("time");
time.textContent = formatTime(row.time, true);
time.title = formatTime(row.time);
const code = document.createElement("code");
code.className = "activity-meta";
code.textContent = `${row.event}${row.turns ? ` \xB7 ${row.turns}` : ""}${row.error ? ` \xB7 ${row.error}` : ""} \xB7 ${formatTime(row.time)}`;
summary.append(description, time);
entry.append(summary, code);
item.append(entry);
$("activity").append(item);
}
$("no-activity").hidden = recentActivity.length > 0;
text("no-activity", t("empty"));
$("activity-more").hidden = recentActivity.length <= ACTIVITY_PREVIEW_LIMIT;
$("activity-more").setAttribute("aria-expanded", String(showAllActivity));
text("activity-more", showAllActivity ? t("lessActivity") : t("showActivity")(recentActivity.length));
}
$("activity-more").addEventListener("click", () => {
showAllActivity = !showAllActivity;
renderActivity();
});
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.8" },
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>