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

This commit is contained in:
huangsen 2026-09-30 17:38:46 +08:00
parent f8d5e34ab1
commit 47287c09a6
7 changed files with 189 additions and 145 deletions

View file

@ -173,10 +173,11 @@ already sent to ReMe; a timeout also does not prove that server-side processing
Click **ReMe status** to open a dedicated panel from MCP settings. Connection checks and memory
status share this one entry. Four tabs organize the panel: **Overview**, **Auto Memory**,
**Memory Consolidation**, and **Components**. They show service health and the memory flow;
**Consolidation**, and **Components**. They show service health and the pending queue;
capture settings, queued turns/sessions, recall and recent activity; Dream scheduling and results;
and component memory estimates plus process RSS. Click **Refresh** to check again;
service details are collapsed in Components until needed. The initial view uses the opening call's result without
connection details, recent activity, usage notes, and raw service details expand when needed. Narrow cards use a
single content column and two rows of tabs. The initial view uses the opening call's result without
a second request. The panel follows the host theme and the saved `language` setting.
You can also ask Codex to call `reme_status` in a conversation. Hosts without an App view receive

View file

@ -164,9 +164,10 @@ ReMe-Example-7319 的评审时间和校验词是什么?
## 6. 查看状态和调整配置
点击 **ReMe status**,从 MCP 设置打开独立状态面板。连接检查和记忆状态共用这一个入口,
面板分为 **总览、自动记忆、记忆整理、组件** 四个页签,分别展示连接健康与记忆流程,
面板分为 **总览、自动记忆、记忆整理、组件** 四个页签,分别展示连接健康与待提交队列,
记录开关、批次和队列、召回及最近活动,Dream 计划和结果,以及组件内存估算与进程 RSS。
点击 **刷新** 可重新检查,组件页的服务详情默认折叠。首次展示直接使用打开时返回的结果,不重复请求;面板适配宿主主题及保存的 `language`。
点击 **刷新** 可重新检查;连接详情、最近活动、使用说明和原始服务详情按需展开。
窄卡片使用单列内容和两行页签。首次展示直接使用打开时返回的结果,不重复请求;面板适配宿主主题及保存的 `language`。
也可以在对话中让 Codex 调用 `reme_status`;不展示 App 的宿主仍会收到文字结果。
打开或刷新面板不会记录或整理记忆。服务不可达时仍可查看本地队列与活动。

View file

@ -1,6 +1,6 @@
{
"name": "reme",
"version": "0.2.1",
"version": "0.2.2",
"description": "Automatic memory, focused recall, runtime status, and consolidation through ReMe MCP.",
"author": {
"name": "EconML team of Alibaba Tongyi Lab",

View file

@ -11,31 +11,27 @@
: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; }
body { margin: 0; padding: 24px; 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: 920px; margin: auto; overflow-wrap: anywhere; }
header { display: flex; align-items: start; justify-content: space-between; gap: 20px; margin-bottom: 24px; }
header > div { min-width: 0; }
h1 { font-size: 22px; letter-spacing: -.4px; margin: 0; font-weight: 650; }
h2 { font-size: 15px; margin: 0 0 20px; font-weight: 600; }
p { margin: 6px 0; }
.muted, small { color: var(--muted); }
.label { font-size: 12px; color: var(--muted); }
.label { font-size: 13px; 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; }
.tabs { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; padding: 5px; background: var(--panel); border: 1px solid var(--line); border-radius: 12px; margin-bottom: 24px; }
.tabs button { min-width: 0; color: var(--muted); background: transparent; border-color: transparent; padding: 10px 8px; font-weight: 600; white-space: normal; line-height: 1.4; }
.tabs button[aria-selected="true"] { color: var(--ink); background: var(--surface); border-color: var(--line); box-shadow: 0 1px 3px #0000000a; }
.grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; }
.card { background: var(--surface); border: 1px solid var(--line); border-radius: 12px; padding: 24px; min-width: 0; }
.full { grid-column: 1 / -1; }
.value { font-size: 25px; line-height: 1.3; font-weight: 700; margin-top: 8px; }
.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; 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); }
@ -43,32 +39,42 @@
.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); }
.section-heading { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; margin-bottom: 20px; }
.section-heading h2 { margin: 0; }
.metrics { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px; }
.properties { margin: 0; }
.properties > div { display: flex; align-items: baseline; justify-content: space-between; gap: 24px; padding: 14px 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 { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; padding: 10px 0; border-bottom: 1px solid var(--line); }
.rows li { display: flex; justify-content: space-between; align-items: baseline; gap: 20px; padding: 16px 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; }
time { color: var(--muted); font-size: 12px; flex-shrink: 0; max-width: 45%; }
.component-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 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: 18px; }
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: 18px 0 0; font-size: 12px; color: var(--muted); }
.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: 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; } }
@media (max-width: 700px) { .tabs { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 640px) { body { padding: 16px; } .grid, .component-grid { grid-template-columns: 1fr; } .card { padding: 20px; } header { gap: 12px; } .rows li { flex-direction: column; gap: 6px; } time { max-width: 100%; } }
@media (max-width: 380px) { .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>
<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>
<div><h1>ReMe status</h1><p id="checked" class="muted label" aria-live="polite">Waiting for status…</p></div>
<button id="refresh" type="button" disabled>Refresh</button>
</header>
<p id="error" role="alert" hidden></p>
<div id="loading" class="card muted">Opening the connection and memory overview…</div>
@ -76,49 +82,62 @@
<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-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"><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>
<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>
<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>
<article class="card full"><div class="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></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>
<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></details>
</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>
<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 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"><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><summary data-i18n="details">Service details</summary><pre id="details"></pre></details>
<details id="service-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>
<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>(() => {
@ -233,18 +252,13 @@
en: {
overview: "Overview",
autoMemory: "Auto Memory",
consolidation: "Memory Consolidation",
consolidation: "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",
connectionDetails: "Connection details",
viewHelp: "About this view",
interval: "Batch interval",
timezone: "Timezone",
lastResult: "Last result",
@ -324,15 +338,10 @@
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",
connectionDetails: "\u8FDE\u63A5\u8BE6\u60C5",
viewHelp: "\u4F7F\u7528\u8BF4\u660E",
interval: "\u6279\u6B21\u8F6E\u6570",
timezone: "\u65F6\u533A",
lastResult: "\u6700\u8FD1\u7ED3\u679C",
@ -467,7 +476,6 @@
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);
@ -606,7 +614,7 @@
}
const host = await transport.request("ui/initialize", {
protocolVersion: "2026-01-26",
appInfo: { name: "reme-status", version: "0.2.1" },
appInfo: { name: "reme-status", version: "0.2.2" },
appCapabilities: { availableDisplayModes: ["fullscreen"] }
});
if (host.protocolVersion !== "2026-01-26") throw new Error("Unsupported UI protocol");

View file

@ -11,31 +11,27 @@
: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; }
body { margin: 0; padding: 24px; 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: 920px; margin: auto; overflow-wrap: anywhere; }
header { display: flex; align-items: start; justify-content: space-between; gap: 20px; margin-bottom: 24px; }
header > div { min-width: 0; }
h1 { font-size: 22px; letter-spacing: -.4px; margin: 0; font-weight: 650; }
h2 { font-size: 15px; margin: 0 0 20px; font-weight: 600; }
p { margin: 6px 0; }
.muted, small { color: var(--muted); }
.label { font-size: 12px; color: var(--muted); }
.label { font-size: 13px; 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; }
.tabs { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; padding: 5px; background: var(--panel); border: 1px solid var(--line); border-radius: 12px; margin-bottom: 24px; }
.tabs button { min-width: 0; color: var(--muted); background: transparent; border-color: transparent; padding: 10px 8px; font-weight: 600; white-space: normal; line-height: 1.4; }
.tabs button[aria-selected="true"] { color: var(--ink); background: var(--surface); border-color: var(--line); box-shadow: 0 1px 3px #0000000a; }
.grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px; }
.card { background: var(--surface); border: 1px solid var(--line); border-radius: 12px; padding: 24px; min-width: 0; }
.full { grid-column: 1 / -1; }
.value { font-size: 25px; line-height: 1.3; font-weight: 700; margin-top: 8px; }
.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; 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); }
@ -43,32 +39,42 @@
.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); }
.section-heading { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; margin-bottom: 20px; }
.section-heading h2 { margin: 0; }
.metrics { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px; }
.properties { margin: 0; }
.properties > div { display: flex; align-items: baseline; justify-content: space-between; gap: 24px; padding: 14px 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 { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; padding: 10px 0; border-bottom: 1px solid var(--line); }
.rows li { display: flex; justify-content: space-between; align-items: baseline; gap: 20px; padding: 16px 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; }
time { color: var(--muted); font-size: 12px; flex-shrink: 0; max-width: 45%; }
.component-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 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: 18px; }
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: 18px 0 0; font-size: 12px; color: var(--muted); }
.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: 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; } }
@media (max-width: 700px) { .tabs { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 640px) { body { padding: 16px; } .grid, .component-grid { grid-template-columns: 1fr; } .card { padding: 20px; } header { gap: 12px; } .rows li { flex-direction: column; gap: 6px; } time { max-width: 100%; } }
@media (max-width: 380px) { .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>
<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>
<div><h1>ReMe status</h1><p id="checked" class="muted label" aria-live="polite">Waiting for status…</p></div>
<button id="refresh" type="button" disabled>Refresh</button>
</header>
<p id="error" role="alert" hidden></p>
<div id="loading" class="card muted">Opening the connection and memory overview…</div>
@ -76,49 +82,62 @@
<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-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"><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>
<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>
<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>
<article class="card full"><div class="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></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>
<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></details>
</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>
<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 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"><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><summary data-i18n="details">Service details</summary><pre id="details"></pre></details>
<details id="service-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>
<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>/* STATUS_APP */</script>

View file

@ -10,10 +10,10 @@ let hasSnapshot = false;
let refreshing = false;
const 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 · Prompt", response: "2 · Response", workspace: "3 · Workspace",
capture: "Turn capture", files: "Memory files", interval: "Batch interval", timezone: "Timezone", lastResult: "Last result",
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.",
@ -41,9 +41,9 @@ const words = {
},
zh: {
overview: "总览", autoMemory: "自动记忆", consolidation: "记忆整理", components: "组件",
service: "ReMe 服务", runtime: "运行状态", queuedTurns: "待提交轮数", queuedSessions: "待提交会话",
automation: "记忆流程", prompt: "1 · 提问", response: "2 · 回复", workspace: "3 · 工作区",
capture: "记录完成轮次", files: "记忆文件", interval: "批次轮数", timezone: "时区", lastResult: "最近结果",
service: "ReMe 服务", queuedTurns: "待提交轮数", queuedSessions: "待提交会话",
connectionDetails: "连接详情", viewHelp: "使用说明",
interval: "批次轮数", timezone: "时区", lastResult: "最近结果",
healthy: "健康", unhealthy: "异常", unknown: "未知", processMemory: "进程内存(RSS)",
componentMemory: "组件内存估算", noComponents: "暂无组件内存信息。",
componentHelp: "内存为 ReMe status 返回的估算值;MCP 响应不包含组件级健康信息。",
@ -107,7 +107,6 @@ function render(result) {
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);
@ -212,7 +211,7 @@ $("refresh").addEventListener("click", async () => {
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" },
protocolVersion: "2026-01-26", appInfo: { name: "reme-status", version: "0.2.2" },
appCapabilities: { availableDisplayModes: ["fullscreen"] },
});
if (host.protocolVersion !== "2026-01-26") throw new Error("Unsupported UI protocol");

View file

@ -22,7 +22,7 @@ def snapshot(language):
return {
"checked_at": 1790757300,
"language": language,
"plugin_version": "0.2.1",
"plugin_version": "0.2.2",
"mcpUrl": "http://localhost:2333/mcp",
"service": {
"health_check": {"reachable": True, "answer": "ReMe v0.4.1.13 - healthy"},
@ -105,6 +105,9 @@ def test_initial_result_refresh_offline_failure_theme_and_untrusted_text(languag
assert "healthy" in frame.locator("#health").inner_text()
assert "23:00" in frame.locator("#next").inner_text()
assert frame.locator("#activity li").count() == 2
assert not frame.locator("#connection-details").get_attribute("open")
assert not frame.locator("#activity-details").get_attribute("open")
assert not frame.locator("#view-help").get_attribute("open")
assert not page.evaluate(
"window.calls.some(m => m.method === 'tools/call')",
) # Opening reuses the supplied result.
@ -119,9 +122,18 @@ def test_initial_result_refresh_offline_failure_theme_and_untrusted_text(languag
assert frame.locator("#component-total").inner_text() == "1.25 MiB"
frame.locator("#tab-components").press("Home")
assert frame.locator("#panel-overview").is_visible()
page.set_viewport_size({"width": 420, "height": 1250})
page.evaluate("document.querySelector('iframe').style.width = '100%'")
assert frame.locator("body").evaluate("node => node.scrollWidth <= window.innerWidth")
for width in (320, 480, 640, 940):
page.set_viewport_size({"width": width + 16, "height": 1250})
page.evaluate("width => document.querySelector('iframe').style.width = `${width}px`", width)
for name in ("overview", "memory", "dream", "components"):
frame.locator(f"#tab-{name}").click()
assert frame.locator("body").evaluate("node => node.scrollWidth <= window.innerWidth")
assert frame.locator(f"#tab-{name}").evaluate(
"node => node.scrollWidth <= node.clientWidth && node.scrollHeight <= node.clientHeight",
)
frame.locator("#tab-overview").click()
if width == 480:
page.screenshot(path=str(tmp_path / f"status-test-host-{theme}-narrow.png"), full_page=True)
offline = snapshot(language)
offline["service"]["health_check"] = {"reachable": False, "error": "ConnectionError"}
@ -131,6 +143,7 @@ def test_initial_result_refresh_offline_failure_theme_and_untrusted_text(languag
page.evaluate("value => window.result = {structuredContent:value}", offline)
frame.locator("#refresh").click()
playwright.expect(frame.locator("#connection-state")).to_have_attribute("data-tone", "bad")
frame.locator("#connection-details summary").click()
assert frame.locator("#endpoint").inner_text() == offline["mcpUrl"]
assert frame.locator("#endpoint img").count() == 0
assert "3" in frame.locator("#queue").inner_text()
@ -139,17 +152,20 @@ def test_initial_result_refresh_offline_failure_theme_and_untrusted_text(languag
assert frame.locator("#error").is_hidden() # Offline is a useful status result, not a broken panel.
frame.locator("#tab-memory").click()
frame.locator("#activity-details summary").click()
assert frame.locator("#activity li").first.is_visible()
page.evaluate("window.failRefresh = true")
frame.locator("#refresh").click()
frame.locator("#error").wait_for(state="visible")
assert "3" in frame.locator("#queue").inner_text() # Failed refresh keeps the last snapshot.
assert frame.locator("#refresh").is_enabled()
assert frame.locator("#panel-memory").is_visible() # Refresh preserves navigation.
assert frame.locator("#activity li").first.is_visible() # Refresh preserves expanded details.
frame.locator("#tab-components").click()
assert frame.locator("#rss").inner_text() == "—"
assert frame.locator("#components .component").count() == 0
assert not frame.locator("details").get_attribute("open")
frame.locator("summary").click()
assert not frame.locator("#service-details").get_attribute("open")
frame.locator("#service-details summary").click()
assert "ConnectionError" in frame.locator("#details").inner_text()
unhealthy = snapshot(language)
unhealthy["service"]["health_check"]["answer"] = "ReMe v0.4.1.13 - unhealthy"