diff --git a/integrations/codex/README.md b/integrations/codex/README.md
index 7dcfae82..23578811 100644
--- a/integrations/codex/README.md
+++ b/integrations/codex/README.md
@@ -129,6 +129,7 @@ Use ReMe memory and include the source path in your answer.
Expect Friday at 16:45 UTC, `amber-lynx-7319`, and a source path such as `daily/...md`.
To confirm automatic recall, check **Auto Memory → Recent activity** for **Relevant memory recalled**
(`recall_found`). A correct answer without that event does not confirm that automatic recall is enabled.
+Use **Show all … events** if it is outside the latest five activities.
> **Screenshot placeholder — `memory-recalled.png`:** Separate conversation B with the recalled facts and source path.
@@ -168,7 +169,10 @@ Open **ReMe status** from MCP settings and choose the tab for what you want to c
| Consolidation | When will memories next be organized, and did the last run succeed? |
| Components | How much memory is ReMe using? Expand **Service details** when troubleshooting. |
-Click **Refresh** for an updated view. If you prefer a text answer, ask Codex:
+Click **Refresh** for an updated view. Recent activity initially shows the latest five events.
+Expand an event for its full timestamp and diagnostic details; use **Show all … events** to see the available history.
+
+If you prefer a text answer, ask Codex:
“Call `reme_status` and summarize my connection, pending memories, and next consolidation.”
> **Screenshot placeholder — `plugin-status.png`:** Real ReMe status panel on Auto Memory, with queued turns and Recent activity expanded to show saved and recalled memories.
diff --git a/integrations/codex/README_ZH.md b/integrations/codex/README_ZH.md
index df6a963d..1c7e4651 100644
--- a/integrations/codex/README_ZH.md
+++ b/integrations/codex/README_ZH.md
@@ -125,6 +125,7 @@ ReMe-Example-7319 的评审时间和项目关键词是什么?
预期答复包含每周五 16:45 UTC、`amber-lynx-7319`,以及 `daily/...md` 等来源路径。
要确认自动召回生效,再查看 **自动记忆 → 最近活动** 中的 **已召回相关记忆**(`recall_found`)。
仅答对问题、却没有这条活动记录,不能证明自动召回已开启。
+如果不在最近五条活动中,点击 **查看全部 … 条活动**。
> **截图槽位 — `memory-recalled.png`:** 独立会话 B 中的正确事实和 ReMe 来源路径。
@@ -162,7 +163,10 @@ ReMe-Example-7319 的评审时间和项目关键词是什么?
| 记忆整理 | 下次什么时候整理、最近一次是否成功。 |
| 组件 | ReMe 的内存用量;排错时可展开 **服务详情**。 |
-点击 **刷新** 更新状态。也可以在对话中要求:“调用 `reme_status`,用文字告诉我连接状态、
+点击 **刷新** 更新状态。最近活动默认展示五条;展开单条活动可查看完整时间和诊断信息,
+点击 **查看全部 … 条活动** 可浏览其余记录。
+
+也可以在对话中要求:“调用 `reme_status`,用文字告诉我连接状态、
待保存记忆和下次整理时间。”
> **截图槽位 — `plugin-status.png`:** 真实 ReMe status 面板的自动记忆页签,保留待提交轮数,并展开最近活动展示保存和召回记录。
diff --git a/integrations/codex/figures/README.md b/integrations/codex/figures/README.md
index f373bbb6..f173cf51 100644
--- a/integrations/codex/figures/README.md
+++ b/integrations/codex/figures/README.md
@@ -134,10 +134,12 @@ Use ReMe memory and include the source path in your answer.
确认回答包含 **每周五 16:45 UTC**、**amber-lynx-7319** 和真实的 ReMe 来源路径,例如 `daily/...md`。
再检查最近活动中出现 **已召回相关记忆 / Relevant memory recalled**(`recall_found`)。
+若不在最近五条中,点击 **查看全部 … 条活动 / Show all … events**;点击单条活动可查看事件码和完整时间。
答复正确且自动召回活动已确认后,截取 B 的问题、回答和来源路径,保存为 `memory-recalled.png`。
Expect Friday at 16:45 UTC, `amber-lynx-7319`, and a real ReMe source path. Also confirm Relevant memory recalled
-in Recent activity. A correct answer alone does not establish that automatic recall ran.
+in Recent activity. Use Show all … events if it is outside the latest five; expand an event for its code and full timestamp.
+A correct answer alone does not establish that automatic recall ran.
### 6. 展示状态面板 / Capture the status panel
@@ -145,9 +147,11 @@ in Recent activity. A correct answer alone does not establish that automatic rec
等待会话 B 的记录也保存完成、待提交轮数回到 `0`,再截取标题、页签、自动记录与召回状态及本次保存和召回活动,
保存为 `plugin-status.png`。
在最近活动仍保留这两条记录时拍摄,避免继续发送无关测试消息。
+若需要展示的记录不在最近五条中,先点击 **查看全部 … 条活动 / Show all … events**。
Open the real status panel, select Auto Memory, refresh, and expand Recent activity. Capture the tabs,
automatic switches, zero pending turns after B is also saved, and the saved/recalled events before unrelated activity replaces them.
+Use Show all … events if the needed entries are outside the latest five.
状态面板的总览、自动记忆和记忆整理是不同页签,不需要把所有内容挤到一张图里。
可另拍以下真实页面;没有开启每日计划时,显示暂停或没有计划是有效状态,无需为截图改动计划或执行整理。
diff --git a/integrations/codex/plugins/reme/.codex-plugin/plugin.json b/integrations/codex/plugins/reme/.codex-plugin/plugin.json
index 99608b95..c18a883e 100644
--- a/integrations/codex/plugins/reme/.codex-plugin/plugin.json
+++ b/integrations/codex/plugins/reme/.codex-plugin/plugin.json
@@ -1,6 +1,6 @@
{
"name": "reme",
- "version": "0.2.3",
+ "version": "0.2.4",
"description": "Automatic memory, focused recall, runtime status, and consolidation through ReMe MCP.",
"author": {
"name": "EconML team of Alibaba Tongyi Lab",
diff --git a/integrations/codex/plugins/reme/ui/status.html b/integrations/codex/plugins/reme/ui/status.html
index 021d8ccc..7621d342 100644
--- a/integrations/codex/plugins/reme/ui/status.html
+++ b/integrations/codex/plugins/reme/ui/status.html
@@ -11,12 +11,12 @@
: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: 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; }
+ 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: flex; align-items: start; justify-content: space-between; gap: 24px; margin-bottom: 28px; }
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; }
+ 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); }
@@ -25,35 +25,44 @@
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: 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; }
+ .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; color: var(--green); background: var(--green-bg); border-radius: 99px; padding: 4px 10px; font-size: 12px; font-weight: 600; }
+ .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; margin-bottom: 20px; }
+ .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: flex; align-items: baseline; justify-content: space-between; gap: 24px; padding: 14px 0; border-bottom: 1px solid var(--line); }
+ .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 { 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; flex-shrink: 0; max-width: 45%; }
- .component-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
+ .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; }
@@ -65,9 +74,9 @@
.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: 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; } }
+ @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; } }
@@ -90,12 +99,12 @@
ReMe service
+
Connection details
-
@@ -109,7 +118,7 @@
Automatic recall
- Recent activity
+ Recent activity
@@ -248,6 +257,9 @@
var zone = "UTC";
var hasSnapshot = false;
var refreshing = false;
+ var recentActivity = [];
+ var showAllActivity = false;
+ var ACTIVITY_PREVIEW_LIMIT = 5;
var words = {
en: {
overview: "Overview",
@@ -287,6 +299,8 @@
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.",
@@ -372,6 +386,8 @@
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",
@@ -420,7 +436,7 @@
var text = (id, value) => {
$(id).textContent = value;
};
- function formatTime(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";
@@ -431,8 +447,7 @@
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
- second: "2-digit",
- timeZoneName: "short"
+ ...brief ? {} : { second: "2-digit", timeZoneName: "short" }
}).format(date);
}
function pill(id, value, tone = "good") {
@@ -481,7 +496,8 @@
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)}`);
+ 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);
@@ -492,31 +508,20 @@
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");
+ 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) : t("paused"));
+ 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"));
- $("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"));
+ recentActivity = activity.slice(-20).reverse();
+ renderActivity();
renderComponents(data.service.status);
text("details", JSON.stringify(data.service, null, 2));
$("error").hidden = true;
@@ -525,6 +530,40 @@
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))";
@@ -614,7 +653,7 @@
}
const host = await transport.request("ui/initialize", {
protocolVersion: "2026-01-26",
- appInfo: { name: "reme-status", version: "0.2.3" },
+ appInfo: { name: "reme-status", version: "0.2.4" },
appCapabilities: { availableDisplayModes: ["fullscreen"] }
});
if (host.protocolVersion !== "2026-01-26") throw new Error("Unsupported UI protocol");
diff --git a/integrations/codex/status-app/src/status.html b/integrations/codex/status-app/src/status.html
index f948e40e..18e769b6 100644
--- a/integrations/codex/status-app/src/status.html
+++ b/integrations/codex/status-app/src/status.html
@@ -11,12 +11,12 @@
: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: 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; }
+ 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: flex; align-items: start; justify-content: space-between; gap: 24px; margin-bottom: 28px; }
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; }
+ 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); }
@@ -25,35 +25,44 @@
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: 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; }
+ .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; color: var(--green); background: var(--green-bg); border-radius: 99px; padding: 4px 10px; font-size: 12px; font-weight: 600; }
+ .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; margin-bottom: 20px; }
+ .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: flex; align-items: baseline; justify-content: space-between; gap: 24px; padding: 14px 0; border-bottom: 1px solid var(--line); }
+ .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 { 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; flex-shrink: 0; max-width: 45%; }
- .component-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
+ .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; }
@@ -65,9 +74,9 @@
.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: 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; } }
+ @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; } }
@@ -90,12 +99,12 @@
ReMe service
+
Connection details
-
@@ -109,7 +118,7 @@
Automatic recall
- Recent activity
+ Recent activity
diff --git a/integrations/codex/status-app/src/status.js b/integrations/codex/status-app/src/status.js
index 822bac82..8a7c0665 100644
--- a/integrations/codex/status-app/src/status.js
+++ b/integrations/codex/status-app/src/status.js
@@ -8,6 +8,9 @@ let language = "en";
let zone = "UTC";
let hasSnapshot = false;
let refreshing = false;
+let recentActivity = [];
+let showAllActivity = false;
+const ACTIVITY_PREVIEW_LIMIT = 5;
const words = {
en: {
overview: "Overview", autoMemory: "Auto Memory", consolidation: "Consolidation", components: "Components",
@@ -22,6 +25,7 @@ const words = {
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…", checked: "Checked", empty: "No activity recorded yet.",
+ showActivity: (n) => `Show all ${n} events`, lessActivity: "Show fewer events",
waiting: "Waiting for the first status result…", 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.",
@@ -52,6 +56,7 @@ const words = {
scheduler: "调度状态", last: "最近整理", activity: "最近活动", details: "服务详情", on: "已开启", off: "已关闭",
connected: "连接正常", offline: "无法连接", refresh: "刷新", refreshing: "刷新中…", checked: "检查时间",
empty: "暂无活动记录。", waiting: "正在等待首次状态结果…", unavailable: "未能刷新状态,请重试或重新打开 ReMe status。",
+ showActivity: (n) => `查看全部 ${n} 条活动`, lessActivity: "收起更多活动",
noHost: "请从 Codex 的 MCP 设置中打开 ReMe status 查看实时数据。",
trust: "服务连接正常不代表 Hook 已获信任,请在 Codex Hooks 设置中审核 ReMe 条目。",
saved: "此页面使用已保存的配置。修改 MCP 设置后,请先保存,再刷新此页面。",
@@ -69,12 +74,13 @@ const words = {
};
const t = (key) => words[language][key] ?? key;
const text = (id, value) => { $(id).textContent = value; };
-function formatTime(value) {
+function formatTime(value, brief = false) {
if (value == null) return "—";
const date = new Date(typeof value === "number" ? value * 1000 : value);
if (!Number.isFinite(date.getTime())) return "—";
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",
+ 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; }
@@ -112,7 +118,8 @@ function render(result) {
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 · ${data.plugin_version}`);
- text("checked", `${t("checked")} ${formatTime(data.checked_at)}`);
+ 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);
@@ -123,32 +130,49 @@ function render(result) {
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) : "—");
+ text("recall-time", recall ? formatTime(recall.time, true) : "—");
+ $("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) : t("paused"));
+ 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) : "—");
text("last", last ? `${t(last.origin)} · ${t(last.status)} · ${formatTime(last.completed_at ?? last.started_at)}${last.error ? ` · ${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 ? ` · ${row.turns}` : ""}${row.error ? ` · ${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"));
+ 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 ? ` · ${row.turns}` : ""}${row.error ? ` · ${row.error}` : ""} · ${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) {
// ReMe's MCP contract returns the human-readable answer, not HTTP response metadata.
const report = status?.reachable && typeof status.answer === "string" ? status.answer : "";
@@ -211,7 +235,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.3" },
+ protocolVersion: "2026-01-26", appInfo: { name: "reme-status", version: "0.2.4" },
appCapabilities: { availableDisplayModes: ["fullscreen"] },
});
if (host.protocolVersion !== "2026-01-26") throw new Error("Unsupported UI protocol");
diff --git a/tests/integration/test_codex_plugin_status_ui.py b/tests/integration/test_codex_plugin_status_ui.py
index a172f0df..87120b05 100644
--- a/tests/integration/test_codex_plugin_status_ui.py
+++ b/tests/integration/test_codex_plugin_status_ui.py
@@ -22,7 +22,7 @@ def snapshot(language):
return {
"checked_at": 1790757300,
"language": language,
- "plugin_version": "0.2.2",
+ "plugin_version": "0.2.4",
"mcpUrl": "http://localhost:2333/mcp",
"service": {
"health_check": {"reachable": True, "answer": "ReMe v0.4.1.13 - healthy"},
@@ -57,6 +57,57 @@ def snapshot(language):
}
+def check_activity_history(page, frame, language, theme, tmp_path):
+ playwright = pytest.importorskip("playwright.sync_api")
+ dense = snapshot(language)
+ dense["mcpUrl"] = "https://memory.example/" + "workspace/" * 40 + "mcp"
+ dense["auto_dream"]["timezone"] = "America/Argentina/Buenos_Aires"
+ dense["recent_activity"] = [
+ {
+ "event": "memory_failed" if index == 19 else "memory_saved",
+ "time": 1790757300 + index,
+ "turns": 1,
+ **({"error": "ConnectionError_" + "detail" * 50} if index == 19 else {}),
+ }
+ for index in range(20)
+ ]
+ page.evaluate("value => window.result = {structuredContent:value}", dense)
+ frame.locator("#refresh").click()
+ frame.locator("#tab-memory").click()
+ frame.locator("#activity-details > summary").click()
+ playwright.expect(frame.locator("#activity li")).to_have_count(5)
+ assert frame.locator("#activity-more").is_visible()
+ assert frame.locator("#activity .activity-meta").first.is_hidden()
+ frame.locator("#activity .activity-entry > summary").first.click()
+ assert "ConnectionError_" in frame.locator("#activity .activity-meta").first.inner_text()
+ assert frame.locator("#activity .activity-meta").first.is_visible()
+ assert "GMT-3" in frame.locator("#activity time").first.get_attribute("title")
+ for width in (320, 480, 768, 940):
+ page.set_viewport_size({"width": width + 16, "height": 1250})
+ page.evaluate("width => document.querySelector('iframe').style.width = `${width}px`", width)
+ assert frame.locator("body").evaluate("node => node.scrollWidth <= window.innerWidth")
+ assert frame.locator("#activity-details").evaluate("node => node.scrollWidth <= node.clientWidth")
+ page.screenshot(path=str(tmp_path / f"status-test-host-{theme}-activity-{width}.png"), full_page=True)
+ frame.locator("#tab-dream").click()
+ assert frame.locator("body").evaluate("node => node.scrollWidth <= window.innerWidth")
+ assert frame.locator("#panel-dream .card").evaluate_all(
+ "nodes => nodes.every(node => node.scrollWidth <= node.clientWidth)",
+ )
+ frame.locator("#tab-memory").click()
+ frame.locator("#activity-more").click()
+ playwright.expect(frame.locator("#activity li")).to_have_count(20)
+ assert frame.locator("#activity-more").get_attribute("aria-expanded") == "true"
+ frame.locator("#refresh").click()
+ playwright.expect(frame.locator("#activity li")).to_have_count(20) # Preserve the chosen history length.
+ assert frame.locator("#activity .activity-meta").first.is_visible() # Preserve expanded event details.
+ frame.locator("#activity-more").click()
+ playwright.expect(frame.locator("#activity li")).to_have_count(5)
+ assert frame.locator("#activity-more").get_attribute("aria-expanded") == "false"
+ assert len(page.evaluate("window.calls.filter(m => m.method === 'tools/call')")) == 2
+ frame.locator("#activity-details > summary").click()
+ frame.locator("#tab-overview").click()
+
+
@pytest.mark.parametrize(("language", "theme"), [("en", "light"), ("zh", "dark")])
def test_initial_result_refresh_offline_failure_theme_and_untrusted_text(language, theme, tmp_path):
playwright = pytest.importorskip("playwright.sync_api")
@@ -122,7 +173,7 @@ 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()
- for width in (320, 480, 640, 940):
+ for width in (320, 480, 640, 768, 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"):
@@ -131,10 +182,19 @@ def test_initial_result_refresh_offline_failure_theme_and_untrusted_text(languag
assert frame.locator(f"#tab-{name}").evaluate(
"node => node.scrollWidth <= node.clientWidth && node.scrollHeight <= node.clientHeight",
)
+ assert frame.locator(f"#panel-{name} .card").evaluate_all(
+ "nodes => nodes.every(node => node.scrollWidth <= node.clientWidth)",
+ )
+ if width == 768 and name == "memory":
+ cards = frame.locator("#panel-memory article").all()
+ first, second = (card.bounding_box() for card in cards)
+ assert second["y"] >= first["y"] + first["height"] # Avoid squeezed side-by-side cards.
frame.locator("#tab-overview").click()
if width == 480:
page.screenshot(path=str(tmp_path / f"status-test-host-{theme}-narrow.png"), full_page=True)
+ check_activity_history(page, frame, language, theme, tmp_path)
+
offline = snapshot(language)
offline["service"]["health_check"] = {"reachable": False, "error": "ConnectionError"}
offline["service"]["status"] = {"reachable": False, "error": "ConnectionError"}
@@ -148,11 +208,11 @@ def test_initial_result_refresh_offline_failure_theme_and_untrusted_text(languag
assert frame.locator("#endpoint img").count() == 0
assert "3" in frame.locator("#queue").inner_text()
calls = page.evaluate("window.calls.filter(m => m.method === 'tools/call')")
- assert len(calls) == 1 and calls[0]["params"] == {"name": "reme_status", "arguments": {}}
+ assert len(calls) == 3 and all(call["params"] == {"name": "reme_status", "arguments": {}} for call in calls)
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()
+ frame.locator("#activity-details > summary").click()
assert frame.locator("#activity li").first.is_visible()
page.evaluate("window.failRefresh = true")
frame.locator("#refresh").click()