mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-10-13 04:01:35 +00:00
feat(codex): merge connection checks into a tabbed status app
This commit is contained in:
parent
b9d44ca339
commit
e5fb2628bb
18 changed files with 3428 additions and 45 deletions
|
|
@ -99,11 +99,11 @@ For a first test, save these values:
|
|||
| Capture batch size (`autoMemoryInterval`) | `1` |
|
||||
| Memory guidance language (`language`) | `en` or `zh` |
|
||||
|
||||
Save before using an action. Click **Check connection**: the result appears beside the action as a
|
||||
status or tooltip, including the ReMe version and `healthy`. This verifies service access; recording
|
||||
and recall still need the conversation test below.
|
||||
Save before using an action. Click **ReMe status** to open the status panel. Its Connection section checks the saved service
|
||||
address and shows the ReMe version and `healthy` when reachable. This verifies service access;
|
||||
recording and recall still need the conversation test below.
|
||||
|
||||
> **Screenshot placeholder — `mcp-settings.png`:** Actual native form with the address, capture switches, batch size, and successful Check connection tooltip.
|
||||
> **Screenshot placeholder — `mcp-settings.png`:** Actual native form with the address, capture switches, batch size, and the single ReMe status entry.
|
||||
|
||||
<!--
|
||||

|
||||
|
|
@ -119,7 +119,7 @@ at 16:45 UTC. The verification phrase is amber-lynx-7319.
|
|||
Do not use tools or write files yourself; just acknowledge the facts.
|
||||
```
|
||||
|
||||
Keep the conversation open while recording finishes. In MCP settings, **View status** should show
|
||||
Keep the conversation open while recording finishes. In MCP settings, **ReMe status** should show
|
||||
`memory_saved` and zero queued turns. In your ReMe workspace, confirm a `daily/` Markdown note
|
||||
contains the fact. An acknowledgement by Codex alone is not evidence of a successful write.
|
||||
|
||||
|
|
@ -137,7 +137,7 @@ Use supplied ReMe memory and cite its source path. Do not call tools;
|
|||
if no memory was supplied, say you do not know.
|
||||
```
|
||||
|
||||
Expect the correct time, phrase, and a ReMe source path such as `daily/...md`. **View status** should
|
||||
Expect the correct time, phrase, and a ReMe source path such as `daily/...md`. **ReMe status** should
|
||||
include `recall_found`. This tests automatic recall independently of explicit search tools.
|
||||
For everyday use, you can also ask Codex to search past decisions with `reme_search`.
|
||||
|
||||
|
|
@ -159,7 +159,7 @@ and optionally **Auto Dream hint**, then save. Defaults are `0 23 * * *` and `As
|
|||
Only the daily form `minute hour * * *` is supported; for 02:30 use `30 2 * * *`.
|
||||
The hint guides how ReMe consolidates existing daily notes into durable memory.
|
||||
|
||||
**View status** shows the active schedule, timezone, next run, and latest result. The schedule runs
|
||||
**ReMe status** shows the active schedule, timezone, next run, and latest result. The schedule runs
|
||||
while Codex keeps the ReMe MCP connection alive. Closing Codex stops the timer; missed occurrences
|
||||
are not replayed on startup. Multiple sessions sharing one `CODEX_HOME` use one scheduler. Separate
|
||||
profiles or machines need their own coordination: enable only one scheduler for the same ReMe workspace.
|
||||
|
|
@ -171,12 +171,22 @@ already sent to ReMe; a timeout also does not prove that server-side processing
|
|||
|
||||
## 6. Inspect status and adjust settings
|
||||
|
||||
Click **View status**, or ask Codex to call `reme_status` for a larger result in the conversation.
|
||||
It reports service health, queued turns, recent recall/write events, and scheduled/manual consolidation.
|
||||
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;
|
||||
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
|
||||
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
|
||||
the same status as text. Opening or refreshing the panel does not record or consolidate memory.
|
||||
Component health and indexed-document counts are not exposed by the current ReMe MCP response;
|
||||
the panel shows the available memory estimates without substituting other counts.
|
||||
Queue and activity information remain available when the service is unreachable. Hook trust is
|
||||
checked separately in Codex's Hooks settings.
|
||||
|
||||
> **Screenshot placeholder — `plugin-status.png`:** Real View status result or `reme_status` output with queued turns, recent activity, and the next Dream run.
|
||||
> **Screenshot placeholder — `plugin-status.png`:** Real ReMe status panel or `reme_status` output with connection health, queued turns, recent activity, and the next Dream run.
|
||||
|
||||
<!--
|
||||

|
||||
|
|
|
|||
|
|
@ -96,10 +96,10 @@ codex plugin add reme@reme-codex
|
|||
| Capture batch size(`autoMemoryInterval`) | `1` |
|
||||
| Memory guidance language(`language`) | `zh` 或 `en` |
|
||||
|
||||
先保存,再点击 **Check connection**。结果会出现在按钮旁的状态或浮层中,包含 ReMe 版本及
|
||||
`healthy`。这说明服务可访问;记录和召回还需要通过下一步的对话验证。
|
||||
先保存,再点击 **ReMe status** 打开状态面板。顶部的服务连接区域会检查已保存的地址,
|
||||
服务可达时显示 ReMe 版本及 `healthy`。这说明服务可访问;记录和召回还需要通过下一步的对话验证。
|
||||
|
||||
> **截图槽位 — `mcp-settings.png`:** 真实原生表单,保留服务地址、自动开关、批次大小及连接成功浮层。
|
||||
> **截图槽位 — `mcp-settings.png`:** 真实原生表单,保留服务地址、自动开关、批次大小及唯一的 ReMe status 入口。
|
||||
|
||||
<!--
|
||||

|
||||
|
|
@ -115,7 +115,7 @@ codex plugin add reme@reme-codex
|
|||
不要调用工具或自己写文件,只需确认这些事实。
|
||||
```
|
||||
|
||||
保持会话打开,等待自动记录完成。在 MCP 设置中点击 **View status**,应看到 `memory_saved`,
|
||||
保持会话打开,等待自动记录完成。在 MCP 设置中点击 **ReMe status**,应看到 `memory_saved`,
|
||||
待提交轮数为零;同时检查 ReMe 工作区的 `daily/` Markdown 文件中出现了这条事实。
|
||||
Codex 的确认回复本身不能证明记忆已写入。
|
||||
|
||||
|
|
@ -133,7 +133,7 @@ ReMe-Example-7319 的评审时间和校验词是什么?
|
|||
如果没有提供相关记忆,就说不知道。
|
||||
```
|
||||
|
||||
预期答复包含正确时间、校验词及 `daily/...md` 等来源路径;**View status** 中应有 `recall_found`。
|
||||
预期答复包含正确时间、校验词及 `daily/...md` 等来源路径;**ReMe status** 中应有 `recall_found`。
|
||||
这样可以单独确认自动召回生效。日常使用时,也可以让 Codex 通过 `reme_search` 主动检索过去的决策。
|
||||
|
||||
> **截图槽位 — `memory-recalled.png`:** 独立会话 B 中的正确事实和 ReMe 来源路径。
|
||||
|
|
@ -153,7 +153,7 @@ ReMe-Example-7319 的评审时间和校验词是什么?
|
|||
`Asia/Shanghai`,即每天上海时间 23:00。仅支持 `分钟 小时 * * *` 的每日计划,例如每天 02:30 为
|
||||
`30 2 * * *`。Hint 用于指导 ReMe 将现有日常记录整理为长期记忆。
|
||||
|
||||
**View status** 会展示计划、时区、下次运行时间和最近结果。只有 Codex 保持 ReMe MCP 连接时,
|
||||
**ReMe status** 会展示计划、时区、下次运行时间和最近结果。只有 Codex 保持 ReMe MCP 连接时,
|
||||
定时任务才会触发;关闭 Codex 后停止计时,重启时不补跑错过的时点。同一 `CODEX_HOME` 下的多个会话
|
||||
共用一个调度器;不同配置目录或机器需自行协调,同一个 ReMe 工作区仅启用一处定时整理。
|
||||
|
||||
|
|
@ -163,11 +163,17 @@ ReMe-Example-7319 的评审时间和校验词是什么?
|
|||
|
||||
## 6. 查看状态和调整配置
|
||||
|
||||
点击 **View status**,或让 Codex 调用 `reme_status`,在对话里查看较完整的结果:
|
||||
服务健康、待提交轮数、最近召回/写入活动、定时和手动整理情况。服务不可达时仍可查看本地队列与活动。
|
||||
点击 **ReMe status**,从 MCP 设置打开独立状态面板。连接检查和记忆状态共用这一个入口,
|
||||
面板分为 **总览、自动记忆、记忆整理、组件** 四个页签,分别展示连接健康与记忆流程,
|
||||
记录开关、批次和队列、召回及最近活动,Dream 计划和结果,以及组件内存估算与进程 RSS。
|
||||
点击 **刷新** 可重新检查,组件页的服务详情默认折叠。首次展示直接使用打开时返回的结果,不重复请求;面板适配宿主主题及保存的 `language`。
|
||||
|
||||
也可以在对话中让 Codex 调用 `reme_status`;不展示 App 的宿主仍会收到文字结果。
|
||||
打开或刷新面板不会记录或整理记忆。服务不可达时仍可查看本地队列与活动。
|
||||
当前 ReMe MCP 响应不包含组件级健康和索引文档数量,面板仅展示可获取的内存估算,不用其他计数替代。
|
||||
Hook 是否可信,需另到 Codex 的 Hooks 设置中检查。
|
||||
|
||||
> **截图槽位 — `plugin-status.png`:** 真实 View status 结果或 `reme_status` 输出,保留待提交轮数、最近活动和下次 Dream 时间。
|
||||
> **截图槽位 — `plugin-status.png`:** 真实 ReMe status 面板或 `reme_status` 输出,保留连接健康、待提交轮数、最近活动和下次 Dream 时间。
|
||||
|
||||
<!--
|
||||

|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ Use real Codex screenshots only. The English and Chinese guides share these PNG
|
|||
| --- | --- |
|
||||
| `plugin-installed.png` | Installed ReMe plugin detail: name, enabled state, and `reme` MCP card. / 已安装的 ReMe 插件详情:名称、启用状态及 MCP 卡片。 |
|
||||
| `hooks-trusted.png` | All seven ReMe handlers across five events enabled and trusted, in desktop settings or CLI `/hooks`. / 五类事件下的七个 ReMe Hook 条目均已启用、已信任,可使用桌面设置或 CLI。 |
|
||||
| `mcp-settings.png` | Native MCP form: URL, auto switches, batch size, and successful Check connection tooltip. / 原生 MCP 表单:地址、自动开关、批次大小及连接健康浮层。 |
|
||||
| `mcp-settings.png` | Native MCP form: URL, auto switches, batch size, and the single ReMe status entry. / 原生 MCP 表单:地址、自动开关、批次大小及唯一的 ReMe status 入口。 |
|
||||
| `memory-recorded.png` | Conversation A: the test fact and final acknowledgement, without explicit tool calls. / 会话 A:测试事实及最终确认,不显式调用工具。 |
|
||||
| `memory-recalled.png` | Separate conversation B: recall question, correct fact, and ReMe source path. / 独立会话 B:召回问题、正确事实和 ReMe 来源路径。 |
|
||||
| `plugin-status.png` | View status result in native settings, or a real `reme_status` call in Codex: queued turns, recent activity, and the next Dream run. / 原生设置的状态结果或真实 Codex 对话中的 `reme_status` 调用,保留待提交轮数、最近活动和下次 Dream 时间。 |
|
||||
| `plugin-status.png` | ReMe status panel opened from native settings, or a real `reme_status` call in Codex: queued turns, recent activity, and the next Dream run. / 从原生设置打开的 ReMe status 面板或真实 Codex 对话中的 `reme_status` 调用,保留待提交轮数、最近活动和下次 Dream 时间。 |
|
||||
|
||||
## Add the screenshots / 补图步骤
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "reme",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"description": "Automatic memory, focused recall, runtime status, and consolidation through ReMe MCP.",
|
||||
"author": {
|
||||
"name": "EconML team of Alibaba Tongyi Lab",
|
||||
|
|
|
|||
|
|
@ -13,14 +13,12 @@ from fastmcp.tools import FunctionTool, ToolResult
|
|||
from mcp.types import ToolAnnotations
|
||||
|
||||
from reme_config import DEFAULTS, load_config, settings_values, update_settings
|
||||
from reme_mcp import call_async
|
||||
from reme_runtime import ReMeRuntime
|
||||
from reme_tools import LOCAL_TOOLS, RUN_DREAM, STATUS, register_tools
|
||||
|
||||
SETTINGS_READ = "reme_settings_read"
|
||||
SETTINGS_UPDATE = "reme_settings_update"
|
||||
CHECK_CONNECTION = "reme_check_connection"
|
||||
RESERVED_TOOLS = LOCAL_TOOLS | {SETTINGS_READ, SETTINGS_UPDATE, CHECK_CONNECTION}
|
||||
RESERVED_TOOLS = LOCAL_TOOLS | {SETTINGS_READ, SETTINGS_UPDATE}
|
||||
|
||||
# OpenAI MCP Extensions node-v0.1.0, docs/spec.md#structured-settings.
|
||||
# Use its legacy capability advertisement with ReMe's existing FastMCP runtime.
|
||||
|
|
@ -70,8 +68,12 @@ LAYOUT = [
|
|||
"title": "Connection",
|
||||
"items": [
|
||||
{"kind": "property", "property": "mcpUrl"},
|
||||
{"kind": "tool", "tool": CHECK_CONNECTION, "title": "Check connection"},
|
||||
{"kind": "tool", "tool": STATUS, "title": "View status"},
|
||||
{
|
||||
"kind": "tool",
|
||||
"tool": STATUS,
|
||||
"title": "ReMe status",
|
||||
"description": "Check the connection and inspect memory activity and the Dream schedule.",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -222,13 +224,6 @@ def create_server() -> FastMCP:
|
|||
),
|
||||
)
|
||||
|
||||
@server.tool(name=CHECK_CONNECTION, title="Check ReMe connection", annotations={"readOnlyHint": True})
|
||||
async def check_connection() -> str:
|
||||
"""Check the saved MCP endpoint without sending conversation content."""
|
||||
config = load_config()
|
||||
result = await call_async(config, "health_check", {}, config["requestTimeoutMs"] / 1000)
|
||||
return str(result["answer"])
|
||||
|
||||
assert FIELDS.keys() == DEFAULTS.keys()
|
||||
return server
|
||||
|
||||
|
|
|
|||
|
|
@ -4,10 +4,12 @@ from __future__ import annotations
|
|||
|
||||
import asyncio
|
||||
import json
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.resources import ResourceContent, ResourceResult
|
||||
from fastmcp.tools import ToolResult
|
||||
from pydantic import Field
|
||||
|
||||
|
|
@ -20,6 +22,11 @@ SEARCH = "reme_search"
|
|||
STATUS = "reme_status"
|
||||
RUN_DREAM = "reme_run_dream"
|
||||
LOCAL_TOOLS = {SEARCH, STATUS, RUN_DREAM}
|
||||
STATUS_URI = "ui://reme/status.html"
|
||||
STATUS_META = {
|
||||
"ui": {"csp": {"connectDomains": [], "resourceDomains": []}},
|
||||
"openai/ui": {"preferredDisplayMode": "fullscreen", "availableDisplayModes": ["fullscreen"]},
|
||||
}
|
||||
|
||||
|
||||
def answer_text(answer) -> str:
|
||||
|
|
@ -100,7 +107,18 @@ def register_tools(server: FastMCP) -> None:
|
|||
empty = "没有找到相关记忆。" if config["language"] == "zh" else "No relevant memory found."
|
||||
return answer_text(answer) if answer else empty
|
||||
|
||||
@server.tool(name=STATUS, title="ReMe status", annotations={"readOnlyHint": True})
|
||||
@server.resource(STATUS_URI, title="ReMe status", mime_type="text/html;profile=mcp-app", meta=STATUS_META)
|
||||
def status_view() -> ResourceResult:
|
||||
"""Serve a self-contained read-only view without exposing a local HTTP server."""
|
||||
html = (Path(__file__).parent / "ui/status.html").read_text(encoding="utf-8")
|
||||
return ResourceResult([ResourceContent(html, mime_type="text/html;profile=mcp-app", meta=STATUS_META)])
|
||||
|
||||
@server.tool(
|
||||
name=STATUS,
|
||||
title="ReMe status",
|
||||
annotations={"readOnlyHint": True},
|
||||
app={"resourceUri": STATUS_URI, "visibility": ["app", "model"]},
|
||||
)
|
||||
async def status() -> ToolResult:
|
||||
"""Inspect service health, queued turns, hook activity, and scheduled/manual consolidation."""
|
||||
config = load_config()
|
||||
|
|
@ -110,6 +128,8 @@ def register_tools(server: FastMCP) -> None:
|
|||
)
|
||||
manifest = Path(__file__).parent / ".codex-plugin/plugin.json"
|
||||
snapshot = {
|
||||
"checked_at": time.time(),
|
||||
"language": config["language"],
|
||||
"plugin_version": json.loads(manifest.read_text(encoding="utf-8"))["version"],
|
||||
"mcpUrl": config["mcpUrl"],
|
||||
"service": {"health_check": health, "status": components},
|
||||
|
|
|
|||
|
|
@ -21,9 +21,10 @@ relevant is found, say so; do not present inference as recalled memory.
|
|||
|
||||
## Status and recording
|
||||
|
||||
Use `reme_check_connection` for a quick health check. Use `reme_status` for queued turns, recent
|
||||
Use `reme_status` for connection health, queued turns, recent
|
||||
recall/write activity, the daily schedule, next run, and last consolidation result. Local queue
|
||||
information remains available when ReMe is offline. Health does not establish Hook trust or prove
|
||||
information remains available when ReMe is offline. The native **ReMe status** action opens a
|
||||
read-only panel with Refresh; the same tool also returns text for conversation/CLI use. Health does not establish Hook trust or prove
|
||||
memory was saved. Direct the user to Codex's Hooks settings to review every ReMe entry.
|
||||
|
||||
Users configure the connection and memory options in the ReMe MCP server's native settings form.
|
||||
|
|
|
|||
201
integrations/codex/plugins/reme/ui/LICENSE-mcp-extensions.txt
Normal file
201
integrations/codex/plugins/reme/ui/LICENSE-mcp-extensions.txt
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
628
integrations/codex/plugins/reme/ui/status.html
Normal file
628
integrations/codex/plugins/reme/ui/status.html
Normal file
|
|
@ -0,0 +1,628 @@
|
|||
<!doctype html>
|
||||
<!-- Built from status-app/src. Includes @openai/mcp-extensions 0.1.0 app transport (OpenAI, Apache-2.0); see LICENSE-mcp-extensions.txt. -->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>ReMe status</title>
|
||||
<style>
|
||||
:root { color-scheme: light dark; --bg: #f6f8fc; --surface: #fff; --panel: #f9faff; --ink: #20283b; --muted: #66718a; --line: #dfe5ee; --blue: #426ccd; --green: #23704b; --green-bg: #e6f2e9; --amber: #875811; --amber-bg: #fff2d9; --red: #a33036; --red-bg: #fbecee; }
|
||||
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --bg: #0b0d12; --surface: #12151d; --panel: #181c26; --ink: #f3f5f7; --muted: #979dab; --line: #292e3b; --blue: #79a8ff; --green: #63d4a3; --green-bg: #25382f; --amber: #f2be5c; --amber-bg: #3c3526; --red: #ff7b86; --red-bg: #40262c; } }
|
||||
:root[data-theme="dark"] { color-scheme: dark; --bg: #0b0d12; --surface: #12151d; --panel: #181c26; --ink: #f3f5f7; --muted: #979dab; --line: #292e3b; --blue: #79a8ff; --green: #63d4a3; --green-bg: #25382f; --amber: #f2be5c; --amber-bg: #3c3526; --red: #ff7b86; --red-bg: #40262c; }
|
||||
:root[data-theme="light"] { color-scheme: light; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; padding: 28px; background: var(--color-background-primary, var(--bg)); color: var(--color-text-primary, var(--ink)); font: 14px/1.5 system-ui, -apple-system, sans-serif; }
|
||||
main { max-width: 1100px; margin: auto; }
|
||||
header { display: flex; align-items: start; justify-content: space-between; gap: 20px; margin-bottom: 22px; }
|
||||
.brand { display: flex; gap: 12px; align-items: center; }
|
||||
.logo { display: grid; place-items: center; width: 44px; height: 44px; flex: 0 0 44px; border-radius: 13px; background: linear-gradient(145deg, #678ff7, #725de3); color: #fff; font-size: 26px; }
|
||||
.eyebrow { color: var(--blue); font-size: 10px; font-weight: 700; letter-spacing: .14em; text-transform: uppercase; }
|
||||
h1 { font-size: 25px; letter-spacing: -.5px; margin: 0; font-weight: 650; }
|
||||
h2 { font-size: 14px; margin: 0 0 12px; font-weight: 600; }
|
||||
p { margin: 6px 0; }
|
||||
.muted, small { color: var(--muted); }
|
||||
.label { font-size: 12px; color: var(--muted); }
|
||||
.endpoint { overflow-wrap: anywhere; font-size: 12px; font-family: ui-monospace, monospace; }
|
||||
.actions { display: flex; gap: 10px; align-items: center; }
|
||||
button { background: var(--surface); color: var(--ink); border: 1px solid var(--line); border-radius: 8px; padding: 8px 14px; font: inherit; cursor: pointer; white-space: nowrap; }
|
||||
button:hover:not(:disabled) { border-color: var(--blue); }
|
||||
button:focus-visible, summary:focus-visible { outline: 2px solid var(--blue); outline-offset: 3px; }
|
||||
button:disabled { opacity: .55; cursor: wait; }
|
||||
.tabs { display: flex; gap: 4px; overflow-x: auto; border-bottom: 1px solid var(--line); margin-bottom: 20px; }
|
||||
.tabs button { color: var(--muted); background: transparent; border: 0; border-radius: 0; border-bottom: 2px solid transparent; padding: 11px 14px; font-weight: 600; }
|
||||
.tabs button[aria-selected="true"] { color: var(--ink); border-bottom-color: var(--blue); }
|
||||
.grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; }
|
||||
.card { background: linear-gradient(150deg, var(--panel), var(--surface)); border: 1px solid var(--line); border-radius: 12px; padding: 18px; min-width: 0; }
|
||||
.wide { grid-column: span 2; }
|
||||
.full { grid-column: 1 / -1; }
|
||||
.value { font-size: 25px; line-height: 1.3; font-weight: 700; margin-top: 8px; }
|
||||
.compact { font-size: 16px; overflow-wrap: anywhere; }
|
||||
.pill { display: inline-block; color: var(--green); background: var(--green-bg); border-radius: 99px; padding: 4px 10px; font-size: 12px; font-weight: 600; }
|
||||
[data-tone="good"] { color: var(--green); }
|
||||
[data-tone="bad"] { color: var(--red); }
|
||||
.pill[data-tone="neutral"] { color: var(--muted); background: var(--bg); }
|
||||
.pill[data-tone="warn"] { color: var(--amber); background: var(--amber-bg); }
|
||||
.pill[data-tone="bad"] { color: var(--red); background: var(--red-bg); }
|
||||
.flow { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
||||
.step { background: var(--surface); border: 1px solid var(--line); padding: 10px; border-radius: 8px; flex: 1 1 100px; }
|
||||
.arrow { color: var(--muted); }
|
||||
.rows { padding: 0; margin: 0; list-style: none; }
|
||||
.rows li { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; padding: 10px 0; border-bottom: 1px solid var(--line); }
|
||||
.rows li:last-child { border: 0; padding-bottom: 0; }
|
||||
.rows code { display: block; color: var(--muted); font-size: 11px; }
|
||||
time { color: var(--muted); font-size: 12px; white-space: nowrap; }
|
||||
.component-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
|
||||
.component { border: 1px solid var(--line); background: var(--surface); padding: 14px; border-radius: 9px; min-width: 0; }
|
||||
.component strong { display: block; overflow-wrap: anywhere; font-size: 13px; }
|
||||
details { margin-top: 18px; }
|
||||
summary { cursor: pointer; color: var(--muted); }
|
||||
pre { white-space: pre-wrap; overflow-wrap: anywhere; background: var(--surface); padding: 14px; border: 1px solid var(--line); border-radius: 8px; font-size: 12px; max-height: 280px; overflow: auto; }
|
||||
.note { margin: 18px 0 0; font-size: 12px; color: var(--muted); }
|
||||
#error { border: 1px solid var(--red); color: var(--red); padding: 12px; border-radius: 8px; margin-bottom: 16px; }
|
||||
[hidden] { display: none !important; }
|
||||
@media (max-width: 850px) { .grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
@media (max-width: 540px) { body { padding: 18px; } .grid, .component-grid { grid-template-columns: 1fr; } .wide { grid-column: 1 / -1; } .card { padding: 16px; } header { flex-wrap: wrap; gap: 12px; } h1 { font-size: 23px; } .rows li { flex-direction: column; gap: 2px; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main aria-busy="true">
|
||||
<header>
|
||||
<div class="brand"><div class="logo" aria-hidden="true">◈</div><div><div class="eyebrow">Codex memory</div><h1>ReMe status</h1><p id="checked" class="muted label" aria-live="polite">Waiting for status…</p></div></div>
|
||||
<div class="actions"><span id="connection-state" class="pill" hidden></span><button id="refresh" type="button" disabled>Refresh</button></div>
|
||||
</header>
|
||||
<p id="error" role="alert" hidden></p>
|
||||
<div id="loading" class="card muted">Opening the connection and memory overview…</div>
|
||||
<div id="overview" hidden>
|
||||
<nav class="tabs" role="tablist" aria-label="ReMe status">
|
||||
<button id="tab-overview" role="tab" aria-controls="panel-overview" aria-selected="true" data-i18n="overview">Overview</button>
|
||||
<button id="tab-memory" role="tab" aria-controls="panel-memory" aria-selected="false" tabindex="-1" data-i18n="autoMemory">Auto Memory</button>
|
||||
<button id="tab-dream" role="tab" aria-controls="panel-dream" aria-selected="false" tabindex="-1" data-i18n="consolidation">Memory Consolidation</button>
|
||||
<button id="tab-components" role="tab" aria-controls="panel-components" aria-selected="false" tabindex="-1" data-i18n="components">Components</button>
|
||||
</nav>
|
||||
<section id="panel-overview" role="tabpanel" aria-labelledby="tab-overview">
|
||||
<div class="grid">
|
||||
<article class="card"><div class="label" data-i18n="service">ReMe service</div><div id="service-health" class="value"></div></article>
|
||||
<article class="card"><div class="label" data-i18n="runtime">Runtime phase</div><div id="runtime" class="value compact"></div></article>
|
||||
<article class="card"><div class="label" data-i18n="queuedTurns">Queued turns</div><div id="overview-queue" class="value"></div></article>
|
||||
<article class="card"><div class="label" data-i18n="queuedSessions">Queued sessions</div><div id="overview-sessions" class="value"></div></article>
|
||||
<article class="card wide"><h2 data-i18n="automation">Memory automation</h2><div class="flow"><div class="step"><div class="label" data-i18n="prompt">1 · Prompt</div><span data-i18n="recall">Automatic recall</span></div><span class="arrow">→</span><div class="step"><div class="label" data-i18n="response">2 · Response</div><span data-i18n="capture">Turn capture</span></div><span class="arrow">→</span><div class="step"><div class="label" data-i18n="workspace">3 · Workspace</div><span data-i18n="files">Memory files</span></div></div></article>
|
||||
<article class="card wide"><h2 data-i18n="connection">Connection</h2><p id="health"></p><p id="endpoint" class="endpoint muted"></p><small id="plugin-version"></small></article>
|
||||
</div>
|
||||
</section>
|
||||
<section id="panel-memory" role="tabpanel" aria-labelledby="tab-memory" hidden>
|
||||
<div class="grid">
|
||||
<article class="card"><div class="label" data-i18n="memory">Automatic memory</div><p><span id="memory-state" class="pill"></span></p></article>
|
||||
<article class="card"><div class="label" data-i18n="interval">Batch interval</div><div id="interval" class="value"></div></article>
|
||||
<article class="card"><div class="label" data-i18n="queuedTurns">Queued turns</div><div id="queue" class="value"></div></article>
|
||||
<article class="card"><div class="label" data-i18n="queuedSessions">Queued sessions</div><div id="sessions" class="value"></div></article>
|
||||
<article class="card full"><h2 data-i18n="recall">Automatic recall</h2><span id="recall-state" class="pill"></span><p id="recall-result"></p><p id="recall-time" class="muted label"></p><p id="other-queue" class="muted label" hidden></p></article>
|
||||
<article class="card full"><h2 data-i18n="activity">Recent activity</h2><ul id="activity" class="rows"></ul><p id="no-activity" class="muted" hidden></p></article>
|
||||
</div>
|
||||
</section>
|
||||
<section id="panel-dream" role="tabpanel" aria-labelledby="tab-dream" hidden>
|
||||
<div class="grid">
|
||||
<article class="card"><div class="label">Auto Dream</div><p><span id="dream-state" class="pill"></span></p></article>
|
||||
<article class="card"><div class="label" data-i18n="schedule">Schedule</div><div id="schedule" class="value compact"></div></article>
|
||||
<article class="card"><div class="label" data-i18n="timezone">Timezone</div><div id="timezone" class="value compact"></div></article>
|
||||
<article class="card"><div class="label" data-i18n="lastResult">Last result</div><div id="last-result" class="value compact"></div></article>
|
||||
<article class="card wide"><h2 data-i18n="next">Next run</h2><div id="next" class="value compact"></div><p class="muted" data-i18n="dreamHelp">The daily schedule runs while the ReMe MCP connection is alive.</p></article>
|
||||
<article class="card wide"><h2 data-i18n="last">Last consolidation</h2><p id="last"></p><p id="scheduler" class="muted"></p></article>
|
||||
</div>
|
||||
</section>
|
||||
<section id="panel-components" role="tabpanel" aria-labelledby="tab-components" hidden>
|
||||
<div class="grid">
|
||||
<article class="card wide"><div class="label" data-i18n="processMemory">Process memory (RSS)</div><div id="rss" class="value"></div></article>
|
||||
<article class="card wide"><div class="label" data-i18n="componentMemory">Estimated component memory</div><div id="component-total" class="value"></div></article>
|
||||
<article class="card full"><h2 data-i18n="components">Components</h2><div id="components" class="component-grid"></div><p id="no-components" class="muted" hidden></p><p class="note" data-i18n="componentHelp">Memory estimates from ReMe status. Component-level health is not included in the MCP response.</p></article>
|
||||
</div>
|
||||
<details><summary data-i18n="details">Service details</summary><pre id="details"></pre></details>
|
||||
</section>
|
||||
<p class="note" data-i18n="trust">Connection health does not establish Hook trust. Review ReMe in Codex Hooks settings.</p>
|
||||
<p class="note" data-i18n="saved">This view uses saved settings. Save changes in MCP settings, then refresh here.</p>
|
||||
</div>
|
||||
</main>
|
||||
<script>(() => {
|
||||
// node_modules/@openai/mcp-extensions/dist/app/transport.js
|
||||
function createAppTransport() {
|
||||
let sequence = 0;
|
||||
const pending = /* @__PURE__ */ new Map();
|
||||
const handlers = /* @__PURE__ */ new Map();
|
||||
const listeners = /* @__PURE__ */ new Map();
|
||||
const send = (message) => window.parent.postMessage({ jsonrpc: "2.0", ...message }, "*");
|
||||
const receive = async (event) => {
|
||||
if (event.source !== window.parent || event.data?.jsonrpc !== "2.0")
|
||||
return;
|
||||
const message = event.data;
|
||||
if (!message.method && message.id != null) {
|
||||
const request = pending.get(message.id);
|
||||
if (!request)
|
||||
return;
|
||||
pending.delete(message.id);
|
||||
clearTimeout(request.timer);
|
||||
if (message.error)
|
||||
request.reject(new Error(message.error.message));
|
||||
else
|
||||
request.resolve(message.result);
|
||||
} else if (message.method && message.id != null) {
|
||||
const handler = handlers.get(message.method);
|
||||
if (!handler) {
|
||||
send({
|
||||
id: message.id,
|
||||
error: {
|
||||
code: -32601,
|
||||
message: `Unsupported app method: ${message.method}`
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
send({
|
||||
id: message.id,
|
||||
result: await handler(message.params ?? {})
|
||||
});
|
||||
} catch (error) {
|
||||
send({
|
||||
id: message.id,
|
||||
error: {
|
||||
code: -32603,
|
||||
message: error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (message.method) {
|
||||
for (const listener of listeners.get(message.method) ?? [])
|
||||
listener(message.params ?? {});
|
||||
}
|
||||
};
|
||||
window.addEventListener("message", receive);
|
||||
return {
|
||||
request(method, params = {}, timeoutMs = 15e3) {
|
||||
if (window.parent === window)
|
||||
return Promise.reject(new Error(`Open this app in an MCP host to use ${method}`));
|
||||
const id = ++sequence;
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
pending.delete(id);
|
||||
reject(new Error(`${method} timed out`));
|
||||
}, timeoutMs);
|
||||
pending.set(id, {
|
||||
resolve: (value) => resolve(value),
|
||||
reject,
|
||||
timer
|
||||
});
|
||||
send({ id, method, params });
|
||||
});
|
||||
},
|
||||
notify(method, params = {}) {
|
||||
send({ method, params });
|
||||
},
|
||||
handle(method, handler) {
|
||||
handlers.set(method, handler);
|
||||
},
|
||||
on(method, listener) {
|
||||
if (!listeners.has(method))
|
||||
listeners.set(method, /* @__PURE__ */ new Set());
|
||||
listeners.get(method).add(listener);
|
||||
return () => {
|
||||
listeners.get(method)?.delete(listener);
|
||||
};
|
||||
},
|
||||
dispose() {
|
||||
window.removeEventListener("message", receive);
|
||||
for (const request of pending.values()) {
|
||||
clearTimeout(request.timer);
|
||||
request.reject(new Error("App disposed"));
|
||||
}
|
||||
pending.clear();
|
||||
handlers.clear();
|
||||
listeners.clear();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// src/status.js
|
||||
var $ = (id) => document.getElementById(id);
|
||||
var transport = createAppTransport();
|
||||
var connected = false;
|
||||
var disposed = false;
|
||||
var language = "en";
|
||||
var zone = "UTC";
|
||||
var hasSnapshot = false;
|
||||
var refreshing = false;
|
||||
var words = {
|
||||
en: {
|
||||
overview: "Overview",
|
||||
autoMemory: "Auto Memory",
|
||||
consolidation: "Memory Consolidation",
|
||||
components: "Components",
|
||||
service: "ReMe service",
|
||||
runtime: "Runtime phase",
|
||||
queuedTurns: "Queued turns",
|
||||
queuedSessions: "Queued sessions",
|
||||
automation: "Memory automation",
|
||||
prompt: "1 \xB7 Prompt",
|
||||
response: "2 \xB7 Response",
|
||||
workspace: "3 \xB7 Workspace",
|
||||
capture: "Turn capture",
|
||||
files: "Memory files",
|
||||
interval: "Batch interval",
|
||||
timezone: "Timezone",
|
||||
lastResult: "Last result",
|
||||
healthy: "Healthy",
|
||||
unhealthy: "Unhealthy",
|
||||
unknown: "Unknown",
|
||||
processMemory: "Process memory (RSS)",
|
||||
componentMemory: "Estimated component memory",
|
||||
noComponents: "No component memory details returned.",
|
||||
componentHelp: "Memory estimates from ReMe status. Component-level health is not included in the MCP response.",
|
||||
dreamHelp: "The daily schedule runs while the ReMe MCP connection is alive.",
|
||||
connection: "Connection",
|
||||
memory: "Automatic memory",
|
||||
recall: "Automatic recall",
|
||||
schedule: "Schedule",
|
||||
next: "Next run",
|
||||
scheduler: "Scheduler",
|
||||
last: "Last consolidation",
|
||||
activity: "Recent activity",
|
||||
details: "Service details",
|
||||
on: "Enabled",
|
||||
off: "Disabled",
|
||||
connected: "Connected",
|
||||
offline: "Unavailable",
|
||||
refresh: "Refresh",
|
||||
refreshing: "Refreshing\u2026",
|
||||
checked: "Checked",
|
||||
empty: "No activity recorded yet.",
|
||||
waiting: "Waiting for the first status result\u2026",
|
||||
unavailable: "Status could not be refreshed. Try again or reopen ReMe status.",
|
||||
noHost: "Open ReMe status from Codex MCP settings to view live data.",
|
||||
trust: "Connection health does not establish Hook trust. Review ReMe in Codex Hooks settings.",
|
||||
saved: "This view uses saved settings. Save changes in MCP settings, then refresh here.",
|
||||
pending: (n) => `${n} queued turns`,
|
||||
batch: (n, s) => `Batch size: ${n} turns \xB7 ${s} queued sessions`,
|
||||
other: (n) => `${n} turns are queued for other service addresses.`,
|
||||
noRecall: "No recall activity yet",
|
||||
noDream: "No consolidation recorded yet",
|
||||
paused: "No run scheduled",
|
||||
running: "Running",
|
||||
starting: "Starting",
|
||||
updating: "Applying settings",
|
||||
stopped: "Stopped",
|
||||
scheduled: "Scheduled",
|
||||
manual: "Manual",
|
||||
completed: "Completed",
|
||||
failed: "Failed",
|
||||
cancelled: "Cancelled",
|
||||
interrupted: "Interrupted",
|
||||
recall_found: "Relevant memory recalled",
|
||||
recall_empty: "No matching memory",
|
||||
recall_failed: "Recall failed",
|
||||
recall_disabled: "Recall disabled",
|
||||
memory_queued: "Turn queued",
|
||||
memory_saved: "Memory saved",
|
||||
memory_failed: "Memory delivery failed",
|
||||
memory_disabled: "Recording disabled",
|
||||
retry_failed: "Delivery retry failed",
|
||||
dream_started: "Consolidation started",
|
||||
dream_completed: "Consolidation completed",
|
||||
dream_failed: "Consolidation failed",
|
||||
dream_cancelled: "Consolidation cancelled",
|
||||
scheduled_dream_failed: "Scheduled consolidation failed",
|
||||
search_found: "Search found memory",
|
||||
search_empty: "Search found no memory",
|
||||
search_failed: "Search failed",
|
||||
capture_skipped: "No completed turn captured",
|
||||
SessionStart: "Session started",
|
||||
SessionEnd: "Session ended",
|
||||
Stop: "Turn completed",
|
||||
SubagentStop: "Subagent turn completed",
|
||||
UserPromptSubmit: "Prompt submitted"
|
||||
},
|
||||
zh: {
|
||||
overview: "\u603B\u89C8",
|
||||
autoMemory: "\u81EA\u52A8\u8BB0\u5FC6",
|
||||
consolidation: "\u8BB0\u5FC6\u6574\u7406",
|
||||
components: "\u7EC4\u4EF6",
|
||||
service: "ReMe \u670D\u52A1",
|
||||
runtime: "\u8FD0\u884C\u72B6\u6001",
|
||||
queuedTurns: "\u5F85\u63D0\u4EA4\u8F6E\u6570",
|
||||
queuedSessions: "\u5F85\u63D0\u4EA4\u4F1A\u8BDD",
|
||||
automation: "\u8BB0\u5FC6\u6D41\u7A0B",
|
||||
prompt: "1 \xB7 \u63D0\u95EE",
|
||||
response: "2 \xB7 \u56DE\u590D",
|
||||
workspace: "3 \xB7 \u5DE5\u4F5C\u533A",
|
||||
capture: "\u8BB0\u5F55\u5B8C\u6210\u8F6E\u6B21",
|
||||
files: "\u8BB0\u5FC6\u6587\u4EF6",
|
||||
interval: "\u6279\u6B21\u8F6E\u6570",
|
||||
timezone: "\u65F6\u533A",
|
||||
lastResult: "\u6700\u8FD1\u7ED3\u679C",
|
||||
healthy: "\u5065\u5EB7",
|
||||
unhealthy: "\u5F02\u5E38",
|
||||
unknown: "\u672A\u77E5",
|
||||
processMemory: "\u8FDB\u7A0B\u5185\u5B58\uFF08RSS\uFF09",
|
||||
componentMemory: "\u7EC4\u4EF6\u5185\u5B58\u4F30\u7B97",
|
||||
noComponents: "\u6682\u65E0\u7EC4\u4EF6\u5185\u5B58\u4FE1\u606F\u3002",
|
||||
componentHelp: "\u5185\u5B58\u4E3A ReMe status \u8FD4\u56DE\u7684\u4F30\u7B97\u503C\uFF1BMCP \u54CD\u5E94\u4E0D\u5305\u542B\u7EC4\u4EF6\u7EA7\u5065\u5EB7\u4FE1\u606F\u3002",
|
||||
dreamHelp: "\u53EA\u6709 ReMe MCP \u8FDE\u63A5\u5B58\u6D3B\u65F6\uFF0C\u6BCF\u65E5\u8BA1\u5212\u624D\u4F1A\u89E6\u53D1\u3002",
|
||||
connection: "\u670D\u52A1\u8FDE\u63A5",
|
||||
memory: "\u81EA\u52A8\u8BB0\u5F55",
|
||||
recall: "\u81EA\u52A8\u53EC\u56DE",
|
||||
schedule: "\u6574\u7406\u8BA1\u5212",
|
||||
next: "\u4E0B\u6B21\u8FD0\u884C",
|
||||
scheduler: "\u8C03\u5EA6\u72B6\u6001",
|
||||
last: "\u6700\u8FD1\u6574\u7406",
|
||||
activity: "\u6700\u8FD1\u6D3B\u52A8",
|
||||
details: "\u670D\u52A1\u8BE6\u60C5",
|
||||
on: "\u5DF2\u5F00\u542F",
|
||||
off: "\u5DF2\u5173\u95ED",
|
||||
connected: "\u8FDE\u63A5\u6B63\u5E38",
|
||||
offline: "\u65E0\u6CD5\u8FDE\u63A5",
|
||||
refresh: "\u5237\u65B0",
|
||||
refreshing: "\u5237\u65B0\u4E2D\u2026",
|
||||
checked: "\u68C0\u67E5\u65F6\u95F4",
|
||||
empty: "\u6682\u65E0\u6D3B\u52A8\u8BB0\u5F55\u3002",
|
||||
waiting: "\u6B63\u5728\u7B49\u5F85\u9996\u6B21\u72B6\u6001\u7ED3\u679C\u2026",
|
||||
unavailable: "\u672A\u80FD\u5237\u65B0\u72B6\u6001\uFF0C\u8BF7\u91CD\u8BD5\u6216\u91CD\u65B0\u6253\u5F00 ReMe status\u3002",
|
||||
noHost: "\u8BF7\u4ECE Codex \u7684 MCP \u8BBE\u7F6E\u4E2D\u6253\u5F00 ReMe status \u67E5\u770B\u5B9E\u65F6\u6570\u636E\u3002",
|
||||
trust: "\u670D\u52A1\u8FDE\u63A5\u6B63\u5E38\u4E0D\u4EE3\u8868 Hook \u5DF2\u83B7\u4FE1\u4EFB\uFF0C\u8BF7\u5728 Codex Hooks \u8BBE\u7F6E\u4E2D\u5BA1\u6838 ReMe \u6761\u76EE\u3002",
|
||||
saved: "\u6B64\u9875\u9762\u4F7F\u7528\u5DF2\u4FDD\u5B58\u7684\u914D\u7F6E\u3002\u4FEE\u6539 MCP \u8BBE\u7F6E\u540E\uFF0C\u8BF7\u5148\u4FDD\u5B58\uFF0C\u518D\u5237\u65B0\u6B64\u9875\u9762\u3002",
|
||||
pending: (n) => `${n} \u8F6E\u5F85\u63D0\u4EA4`,
|
||||
batch: (n, s) => `\u6BCF\u6279 ${n} \u8F6E \xB7 ${s} \u4E2A\u4F1A\u8BDD\u5F85\u63D0\u4EA4`,
|
||||
other: (n) => `\u53E6\u6709 ${n} \u8F6E\u7B49\u5F85\u63D0\u4EA4\u81F3\u5176\u4ED6\u670D\u52A1\u5730\u5740\u3002`,
|
||||
noRecall: "\u6682\u65E0\u53EC\u56DE\u6D3B\u52A8",
|
||||
noDream: "\u6682\u65E0\u6574\u7406\u8BB0\u5F55",
|
||||
paused: "\u6682\u65E0\u8BA1\u5212",
|
||||
running: "\u8FD0\u884C\u4E2D",
|
||||
starting: "\u542F\u52A8\u4E2D",
|
||||
updating: "\u6B63\u5728\u5E94\u7528\u914D\u7F6E",
|
||||
stopped: "\u5DF2\u505C\u6B62",
|
||||
scheduled: "\u5B9A\u65F6",
|
||||
manual: "\u624B\u52A8",
|
||||
completed: "\u5DF2\u5B8C\u6210",
|
||||
failed: "\u5931\u8D25",
|
||||
cancelled: "\u5DF2\u53D6\u6D88",
|
||||
interrupted: "\u5DF2\u4E2D\u65AD",
|
||||
recall_found: "\u5DF2\u53EC\u56DE\u76F8\u5173\u8BB0\u5FC6",
|
||||
recall_empty: "\u672A\u627E\u5230\u76F8\u5173\u8BB0\u5FC6",
|
||||
recall_failed: "\u53EC\u56DE\u5931\u8D25",
|
||||
recall_disabled: "\u81EA\u52A8\u53EC\u56DE\u5DF2\u5173\u95ED",
|
||||
memory_queued: "\u5DF2\u52A0\u5165\u8BB0\u5F55\u961F\u5217",
|
||||
memory_saved: "\u8BB0\u5FC6\u5DF2\u4FDD\u5B58",
|
||||
memory_failed: "\u8BB0\u5FC6\u63D0\u4EA4\u5931\u8D25",
|
||||
memory_disabled: "\u81EA\u52A8\u8BB0\u5F55\u5DF2\u5173\u95ED",
|
||||
retry_failed: "\u91CD\u8BD5\u63D0\u4EA4\u5931\u8D25",
|
||||
dream_started: "\u5F00\u59CB\u6574\u7406",
|
||||
dream_completed: "\u6574\u7406\u5DF2\u5B8C\u6210",
|
||||
dream_failed: "\u6574\u7406\u5931\u8D25",
|
||||
dream_cancelled: "\u6574\u7406\u5DF2\u53D6\u6D88",
|
||||
scheduled_dream_failed: "\u5B9A\u65F6\u6574\u7406\u5931\u8D25",
|
||||
search_found: "\u68C0\u7D22\u5230\u76F8\u5173\u8BB0\u5FC6",
|
||||
search_empty: "\u672A\u68C0\u7D22\u5230\u76F8\u5173\u8BB0\u5FC6",
|
||||
search_failed: "\u68C0\u7D22\u5931\u8D25",
|
||||
capture_skipped: "\u672A\u6355\u83B7\u5230\u5B8C\u6210\u8F6E\u6B21",
|
||||
SessionStart: "\u4F1A\u8BDD\u5F00\u59CB",
|
||||
SessionEnd: "\u4F1A\u8BDD\u7ED3\u675F",
|
||||
Stop: "\u8F6E\u6B21\u5B8C\u6210",
|
||||
SubagentStop: "\u5B50\u4EE3\u7406\u8F6E\u6B21\u5B8C\u6210",
|
||||
UserPromptSubmit: "\u5DF2\u63D0\u4EA4\u63D0\u95EE"
|
||||
}
|
||||
};
|
||||
var t = (key) => words[language][key] ?? key;
|
||||
var text = (id, value) => {
|
||||
$(id).textContent = value;
|
||||
};
|
||||
function formatTime(value) {
|
||||
if (value == null) return "\u2014";
|
||||
const date = new Date(typeof value === "number" ? value * 1e3 : value);
|
||||
if (!Number.isFinite(date.getTime())) return "\u2014";
|
||||
return new Intl.DateTimeFormat(language === "zh" ? "zh-CN" : "en-GB", {
|
||||
timeZone: zone,
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
timeZoneName: "short"
|
||||
}).format(date);
|
||||
}
|
||||
function pill(id, value, tone = "good") {
|
||||
text(id, value);
|
||||
$(id).dataset.tone = tone;
|
||||
}
|
||||
function toggle(id, enabled) {
|
||||
pill(id, t(enabled ? "on" : "off"), enabled ? "good" : "neutral");
|
||||
}
|
||||
function applyHost(context = {}) {
|
||||
if (["dark", "light"].includes(context.theme)) document.documentElement.dataset.theme = context.theme;
|
||||
for (const key of ["--color-background-primary", "--color-text-primary"]) {
|
||||
const value = context.styles?.variables?.[key];
|
||||
if (typeof value === "string") document.documentElement.style.setProperty(key, value);
|
||||
}
|
||||
}
|
||||
function fail() {
|
||||
if (disposed) return;
|
||||
text("error", t("unavailable"));
|
||||
$("error").hidden = false;
|
||||
$("loading").hidden = true;
|
||||
document.querySelector("main").setAttribute("aria-busy", "false");
|
||||
}
|
||||
function render(result) {
|
||||
if (disposed) return;
|
||||
const data = result?.structuredContent;
|
||||
if (result?.isError || !data?.service?.health_check || !data?.auto_memory || !data?.auto_dream) {
|
||||
fail();
|
||||
return;
|
||||
}
|
||||
language = data.language === "zh" ? "zh" : "en";
|
||||
zone = data.auto_dream.timezone;
|
||||
document.documentElement.lang = language === "zh" ? "zh-CN" : "en";
|
||||
document.querySelectorAll("[data-i18n]").forEach((node) => {
|
||||
node.textContent = t(node.dataset.i18n);
|
||||
});
|
||||
text("refresh", t(refreshing ? "refreshing" : "refresh"));
|
||||
const health = data.service.health_check;
|
||||
pill("connection-state", t(health.reachable ? "connected" : "offline"), health.reachable ? "good" : "bad");
|
||||
$("connection-state").hidden = false;
|
||||
const summary = typeof health.answer === "string" ? health.answer.trim().match(/(?:^|-\s*)(unhealthy|healthy)$/) : null;
|
||||
text("service-health", t(!health.reachable ? "offline" : summary?.[1] ?? "unknown"));
|
||||
$("service-health").dataset.tone = !health.reachable || summary?.[1] === "unhealthy" ? "bad" : summary ? "good" : "neutral";
|
||||
text("runtime", t(data.auto_dream.phase));
|
||||
text("overview-queue", data.auto_memory.queued_turns);
|
||||
text("overview-sessions", data.auto_memory.queued_sessions);
|
||||
text("health", health.reachable ? typeof health.answer === "string" ? health.answer : JSON.stringify(health.answer) : health.error);
|
||||
text("endpoint", data.mcpUrl);
|
||||
text("plugin-version", `ReMe for Codex \xB7 ${data.plugin_version}`);
|
||||
text("checked", `${t("checked")} ${formatTime(data.checked_at)}`);
|
||||
toggle("memory-state", data.auto_memory.enabled);
|
||||
text("queue", data.auto_memory.queued_turns);
|
||||
text("interval", data.auto_memory.interval);
|
||||
text("sessions", data.auto_memory.queued_sessions);
|
||||
$("other-queue").hidden = !data.auto_memory.other_endpoint_queued_turns;
|
||||
text("other-queue", t("other")(data.auto_memory.other_endpoint_queued_turns));
|
||||
toggle("recall-state", data.auto_recall.enabled);
|
||||
const activity = data.recent_activity ?? [];
|
||||
const recall = activity.findLast((row) => row.event?.startsWith("recall_"));
|
||||
text("recall-result", recall ? t(recall.event) : t("noRecall"));
|
||||
text("recall-time", recall ? formatTime(recall.time) : "\u2014");
|
||||
const dream = data.auto_dream;
|
||||
toggle("dream-state", dream.enabled);
|
||||
text("schedule", dream.cron);
|
||||
text("timezone", dream.timezone);
|
||||
text("next", dream.next_run_at ? formatTime(dream.next_run_at) : t("paused"));
|
||||
text("scheduler", `${t("scheduler")}: ${t(dream.phase)}`);
|
||||
const last = dream.last_run;
|
||||
text("last-result", last ? t(last.status) : "\u2014");
|
||||
text("last", last ? `${t(last.origin)} \xB7 ${t(last.status)} \xB7 ${formatTime(last.completed_at ?? last.started_at)}${last.error ? ` \xB7 ${last.error}` : ""}` : t("noDream"));
|
||||
$("activity").replaceChildren();
|
||||
for (const row of activity.slice(-20).reverse()) {
|
||||
const item = document.createElement("li");
|
||||
const description = document.createElement("span");
|
||||
description.textContent = t(row.event);
|
||||
const code = document.createElement("code");
|
||||
code.textContent = `${row.event}${row.turns ? ` \xB7 ${row.turns}` : ""}${row.error ? ` \xB7 ${row.error}` : ""}`;
|
||||
description.append(code);
|
||||
const time = document.createElement("time");
|
||||
time.textContent = formatTime(row.time);
|
||||
item.append(description, time);
|
||||
$("activity").append(item);
|
||||
}
|
||||
$("no-activity").hidden = activity.length > 0;
|
||||
text("no-activity", t("empty"));
|
||||
renderComponents(data.service.status);
|
||||
text("details", JSON.stringify(data.service, null, 2));
|
||||
$("error").hidden = true;
|
||||
$("loading").hidden = true;
|
||||
$("overview").hidden = false;
|
||||
document.querySelector("main").setAttribute("aria-busy", "false");
|
||||
hasSnapshot = true;
|
||||
}
|
||||
function renderComponents(status) {
|
||||
const report = status?.reachable && typeof status.answer === "string" ? status.answer : "";
|
||||
const amount = "([0-9]+(?:\\.[0-9]+)? (?:B|KiB|MiB|GiB|TiB))";
|
||||
text("rss", report.match(new RegExp(`^ Process RSS +${amount}$`, "m"))?.[1] ?? "\u2014");
|
||||
text("component-total", report.match(new RegExp(`^ Components total +${amount}$`, "m"))?.[1] ?? "\u2014");
|
||||
$("components").replaceChildren();
|
||||
for (const row of report.matchAll(new RegExp(`^ ([a-z_]+):([^\\s:]+) +${amount}$`, "gm"))) {
|
||||
const card = document.createElement("div");
|
||||
card.className = "component";
|
||||
const title = document.createElement("strong");
|
||||
title.textContent = `${row[1].replaceAll("_", " ")} \xB7 ${row[2]}`;
|
||||
const size = document.createElement("div");
|
||||
size.className = "value compact";
|
||||
size.textContent = row[3];
|
||||
card.append(title, size);
|
||||
$("components").append(card);
|
||||
}
|
||||
$("no-components").hidden = $("components").childElementCount > 0;
|
||||
text("no-components", status?.reachable === false ? status.error : t("noComponents"));
|
||||
}
|
||||
var tabs = [...document.querySelectorAll('[role="tab"]')];
|
||||
function selectTab(selected) {
|
||||
for (const tab of tabs) {
|
||||
const active = tab === selected;
|
||||
tab.setAttribute("aria-selected", String(active));
|
||||
tab.tabIndex = active ? 0 : -1;
|
||||
$(tab.getAttribute("aria-controls")).hidden = !active;
|
||||
}
|
||||
}
|
||||
for (const tab of tabs) {
|
||||
tab.addEventListener("click", () => selectTab(tab));
|
||||
tab.addEventListener("keydown", (event) => {
|
||||
const index = tabs.indexOf(tab);
|
||||
const next = { ArrowRight: (index + 1) % tabs.length, ArrowLeft: (index + tabs.length - 1) % tabs.length, Home: 0, End: tabs.length - 1 }[event.key];
|
||||
if (next == null) return;
|
||||
event.preventDefault();
|
||||
selectTab(tabs[next]);
|
||||
tabs[next].focus();
|
||||
});
|
||||
}
|
||||
var resizeFrame;
|
||||
var resizeObserver = new ResizeObserver(() => {
|
||||
cancelAnimationFrame(resizeFrame);
|
||||
resizeFrame = requestAnimationFrame(() => {
|
||||
if (connected && !disposed) transport.notify("ui/notifications/size-changed", { height: Math.ceil(document.body.getBoundingClientRect().height) });
|
||||
});
|
||||
});
|
||||
resizeObserver.observe(document.body);
|
||||
transport.on("ui/notifications/tool-result", (result) => {
|
||||
try {
|
||||
render(result);
|
||||
} catch {
|
||||
fail();
|
||||
}
|
||||
});
|
||||
transport.on("ui/notifications/tool-cancelled", fail);
|
||||
transport.on("ui/notifications/host-context-changed", applyHost);
|
||||
transport.handle("ui/resource-teardown", () => {
|
||||
disposed = true;
|
||||
connected = false;
|
||||
$("refresh").disabled = true;
|
||||
resizeObserver.disconnect();
|
||||
cancelAnimationFrame(resizeFrame);
|
||||
setTimeout(() => transport.dispose(), 0);
|
||||
return {};
|
||||
});
|
||||
$("refresh").addEventListener("click", async () => {
|
||||
if (!connected || refreshing) return;
|
||||
refreshing = true;
|
||||
$("refresh").disabled = true;
|
||||
text("refresh", t("refreshing"));
|
||||
document.querySelector("main").setAttribute("aria-busy", "true");
|
||||
try {
|
||||
render(await transport.request("tools/call", { name: "reme_status", arguments: {} }, 125e3));
|
||||
} catch {
|
||||
fail();
|
||||
} finally {
|
||||
refreshing = false;
|
||||
$("refresh").disabled = !connected;
|
||||
text("refresh", t("refresh"));
|
||||
}
|
||||
});
|
||||
async function connect() {
|
||||
if (window.parent === window) {
|
||||
text("loading", t("noHost"));
|
||||
return;
|
||||
}
|
||||
const host = await transport.request("ui/initialize", {
|
||||
protocolVersion: "2026-01-26",
|
||||
appInfo: { name: "reme-status", version: "0.2.1" },
|
||||
appCapabilities: { availableDisplayModes: ["fullscreen"] }
|
||||
});
|
||||
if (host.protocolVersion !== "2026-01-26") throw new Error("Unsupported UI protocol");
|
||||
connected = true;
|
||||
applyHost(host.hostContext);
|
||||
$("refresh").disabled = false;
|
||||
if (!hasSnapshot) text("loading", t("waiting"));
|
||||
transport.notify("ui/notifications/initialized");
|
||||
const context = host.hostContext ?? {};
|
||||
if (context.displayMode !== "fullscreen" && context.availableDisplayModes?.includes("fullscreen")) {
|
||||
await transport.request("ui/request-display-mode", { mode: "fullscreen" }).catch(() => {
|
||||
});
|
||||
}
|
||||
}
|
||||
connect().catch(fail);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1
integrations/codex/status-app/.gitignore
vendored
Normal file
1
integrations/codex/status-app/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
node_modules/
|
||||
28
integrations/codex/status-app/README.md
Normal file
28
integrations/codex/status-app/README.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# ReMe status view maintenance
|
||||
|
||||
The settings action `reme_status` returns one read-only snapshot and opens the packaged MCP App.
|
||||
The view consumes that initial result; only the Refresh button makes a further `reme_status` call.
|
||||
It never calls memory-write tools, changes settings, or contacts the ReMe URL directly.
|
||||
|
||||
Edit `src/`, then build the self-contained installed asset:
|
||||
|
||||
```bash
|
||||
npm ci
|
||||
npm run build
|
||||
```
|
||||
|
||||
Commit the generated `../plugins/reme/ui/status.html` with its source so installing from a checkout
|
||||
requires only the existing Python runtime. Node is needed only to rebuild the view. The bundle uses
|
||||
OpenAI MCP Extensions 0.1.0's app transport, includes its license, and loads no external scripts.
|
||||
The resource advertises fullscreen; the host controls actual placement, including a settings modal.
|
||||
|
||||
Validation from the repository root:
|
||||
|
||||
```bash
|
||||
pytest tests/unit/test_codex_plugin_mcp.py -q
|
||||
REME_CODEX_STATUS_UI=1 pytest tests/integration/test_codex_plugin_status_ui.py -q
|
||||
```
|
||||
|
||||
The opt-in browser test needs Playwright with Chromium and uses a synthetic host for protocol and
|
||||
rendering checks. Its output is not a real Codex screenshot. Native Codex placement must be checked
|
||||
in the app when access is available; user documentation keeps genuine screenshot placeholders.
|
||||
18
integrations/codex/status-app/build.mjs
Normal file
18
integrations/codex/status-app/build.mjs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { build } from "esbuild";
|
||||
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
|
||||
const result = await build({
|
||||
entryPoints: [new URL("./src/status.js", import.meta.url).pathname],
|
||||
bundle: true,
|
||||
write: false,
|
||||
format: "iife",
|
||||
target: "es2022",
|
||||
legalComments: "inline",
|
||||
});
|
||||
const template = await readFile(new URL("./src/status.html", import.meta.url), "utf8");
|
||||
const script = result.outputFiles[0].text.replaceAll("</script", "<\\/script");
|
||||
const output = new URL("../plugins/reme/ui/", import.meta.url);
|
||||
await mkdir(output, { recursive: true });
|
||||
await writeFile(new URL("status.html", output), template.replace("/* STATUS_APP */", () => script));
|
||||
await copyFile(new URL("./node_modules/@openai/mcp-extensions/LICENSE", import.meta.url), new URL("LICENSE-mcp-extensions.txt", output));
|
||||
console.log("Built plugins/reme/ui/status.html (self-contained; no runtime Node dependency)");
|
||||
1914
integrations/codex/status-app/package-lock.json
generated
Normal file
1914
integrations/codex/status-app/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
13
integrations/codex/status-app/package.json
Normal file
13
integrations/codex/status-app/package.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "reme-codex-status",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"build": "node build.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openai/mcp-extensions": "0.1.0",
|
||||
"esbuild": "0.28.2"
|
||||
}
|
||||
}
|
||||
126
integrations/codex/status-app/src/status.html
Normal file
126
integrations/codex/status-app/src/status.html
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
<!doctype html>
|
||||
<!-- Built from status-app/src. Includes @openai/mcp-extensions 0.1.0 app transport (OpenAI, Apache-2.0); see LICENSE-mcp-extensions.txt. -->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>ReMe status</title>
|
||||
<style>
|
||||
:root { color-scheme: light dark; --bg: #f6f8fc; --surface: #fff; --panel: #f9faff; --ink: #20283b; --muted: #66718a; --line: #dfe5ee; --blue: #426ccd; --green: #23704b; --green-bg: #e6f2e9; --amber: #875811; --amber-bg: #fff2d9; --red: #a33036; --red-bg: #fbecee; }
|
||||
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) { --bg: #0b0d12; --surface: #12151d; --panel: #181c26; --ink: #f3f5f7; --muted: #979dab; --line: #292e3b; --blue: #79a8ff; --green: #63d4a3; --green-bg: #25382f; --amber: #f2be5c; --amber-bg: #3c3526; --red: #ff7b86; --red-bg: #40262c; } }
|
||||
:root[data-theme="dark"] { color-scheme: dark; --bg: #0b0d12; --surface: #12151d; --panel: #181c26; --ink: #f3f5f7; --muted: #979dab; --line: #292e3b; --blue: #79a8ff; --green: #63d4a3; --green-bg: #25382f; --amber: #f2be5c; --amber-bg: #3c3526; --red: #ff7b86; --red-bg: #40262c; }
|
||||
:root[data-theme="light"] { color-scheme: light; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; padding: 28px; background: var(--color-background-primary, var(--bg)); color: var(--color-text-primary, var(--ink)); font: 14px/1.5 system-ui, -apple-system, sans-serif; }
|
||||
main { max-width: 1100px; margin: auto; }
|
||||
header { display: flex; align-items: start; justify-content: space-between; gap: 20px; margin-bottom: 22px; }
|
||||
.brand { display: flex; gap: 12px; align-items: center; }
|
||||
.logo { display: grid; place-items: center; width: 44px; height: 44px; flex: 0 0 44px; border-radius: 13px; background: linear-gradient(145deg, #678ff7, #725de3); color: #fff; font-size: 26px; }
|
||||
.eyebrow { color: var(--blue); font-size: 10px; font-weight: 700; letter-spacing: .14em; text-transform: uppercase; }
|
||||
h1 { font-size: 25px; letter-spacing: -.5px; margin: 0; font-weight: 650; }
|
||||
h2 { font-size: 14px; margin: 0 0 12px; font-weight: 600; }
|
||||
p { margin: 6px 0; }
|
||||
.muted, small { color: var(--muted); }
|
||||
.label { font-size: 12px; color: var(--muted); }
|
||||
.endpoint { overflow-wrap: anywhere; font-size: 12px; font-family: ui-monospace, monospace; }
|
||||
.actions { display: flex; gap: 10px; align-items: center; }
|
||||
button { background: var(--surface); color: var(--ink); border: 1px solid var(--line); border-radius: 8px; padding: 8px 14px; font: inherit; cursor: pointer; white-space: nowrap; }
|
||||
button:hover:not(:disabled) { border-color: var(--blue); }
|
||||
button:focus-visible, summary:focus-visible { outline: 2px solid var(--blue); outline-offset: 3px; }
|
||||
button:disabled { opacity: .55; cursor: wait; }
|
||||
.tabs { display: flex; gap: 4px; overflow-x: auto; border-bottom: 1px solid var(--line); margin-bottom: 20px; }
|
||||
.tabs button { color: var(--muted); background: transparent; border: 0; border-radius: 0; border-bottom: 2px solid transparent; padding: 11px 14px; font-weight: 600; }
|
||||
.tabs button[aria-selected="true"] { color: var(--ink); border-bottom-color: var(--blue); }
|
||||
.grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px; }
|
||||
.card { background: linear-gradient(150deg, var(--panel), var(--surface)); border: 1px solid var(--line); border-radius: 12px; padding: 18px; min-width: 0; }
|
||||
.wide { grid-column: span 2; }
|
||||
.full { grid-column: 1 / -1; }
|
||||
.value { font-size: 25px; line-height: 1.3; font-weight: 700; margin-top: 8px; }
|
||||
.compact { font-size: 16px; overflow-wrap: anywhere; }
|
||||
.pill { display: inline-block; color: var(--green); background: var(--green-bg); border-radius: 99px; padding: 4px 10px; font-size: 12px; font-weight: 600; }
|
||||
[data-tone="good"] { color: var(--green); }
|
||||
[data-tone="bad"] { color: var(--red); }
|
||||
.pill[data-tone="neutral"] { color: var(--muted); background: var(--bg); }
|
||||
.pill[data-tone="warn"] { color: var(--amber); background: var(--amber-bg); }
|
||||
.pill[data-tone="bad"] { color: var(--red); background: var(--red-bg); }
|
||||
.flow { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
||||
.step { background: var(--surface); border: 1px solid var(--line); padding: 10px; border-radius: 8px; flex: 1 1 100px; }
|
||||
.arrow { color: var(--muted); }
|
||||
.rows { padding: 0; margin: 0; list-style: none; }
|
||||
.rows li { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; padding: 10px 0; border-bottom: 1px solid var(--line); }
|
||||
.rows li:last-child { border: 0; padding-bottom: 0; }
|
||||
.rows code { display: block; color: var(--muted); font-size: 11px; }
|
||||
time { color: var(--muted); font-size: 12px; white-space: nowrap; }
|
||||
.component-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
|
||||
.component { border: 1px solid var(--line); background: var(--surface); padding: 14px; border-radius: 9px; min-width: 0; }
|
||||
.component strong { display: block; overflow-wrap: anywhere; font-size: 13px; }
|
||||
details { margin-top: 18px; }
|
||||
summary { cursor: pointer; color: var(--muted); }
|
||||
pre { white-space: pre-wrap; overflow-wrap: anywhere; background: var(--surface); padding: 14px; border: 1px solid var(--line); border-radius: 8px; font-size: 12px; max-height: 280px; overflow: auto; }
|
||||
.note { margin: 18px 0 0; font-size: 12px; color: var(--muted); }
|
||||
#error { border: 1px solid var(--red); color: var(--red); padding: 12px; border-radius: 8px; margin-bottom: 16px; }
|
||||
[hidden] { display: none !important; }
|
||||
@media (max-width: 850px) { .grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
@media (max-width: 540px) { body { padding: 18px; } .grid, .component-grid { grid-template-columns: 1fr; } .wide { grid-column: 1 / -1; } .card { padding: 16px; } header { flex-wrap: wrap; gap: 12px; } h1 { font-size: 23px; } .rows li { flex-direction: column; gap: 2px; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main aria-busy="true">
|
||||
<header>
|
||||
<div class="brand"><div class="logo" aria-hidden="true">◈</div><div><div class="eyebrow">Codex memory</div><h1>ReMe status</h1><p id="checked" class="muted label" aria-live="polite">Waiting for status…</p></div></div>
|
||||
<div class="actions"><span id="connection-state" class="pill" hidden></span><button id="refresh" type="button" disabled>Refresh</button></div>
|
||||
</header>
|
||||
<p id="error" role="alert" hidden></p>
|
||||
<div id="loading" class="card muted">Opening the connection and memory overview…</div>
|
||||
<div id="overview" hidden>
|
||||
<nav class="tabs" role="tablist" aria-label="ReMe status">
|
||||
<button id="tab-overview" role="tab" aria-controls="panel-overview" aria-selected="true" data-i18n="overview">Overview</button>
|
||||
<button id="tab-memory" role="tab" aria-controls="panel-memory" aria-selected="false" tabindex="-1" data-i18n="autoMemory">Auto Memory</button>
|
||||
<button id="tab-dream" role="tab" aria-controls="panel-dream" aria-selected="false" tabindex="-1" data-i18n="consolidation">Memory Consolidation</button>
|
||||
<button id="tab-components" role="tab" aria-controls="panel-components" aria-selected="false" tabindex="-1" data-i18n="components">Components</button>
|
||||
</nav>
|
||||
<section id="panel-overview" role="tabpanel" aria-labelledby="tab-overview">
|
||||
<div class="grid">
|
||||
<article class="card"><div class="label" data-i18n="service">ReMe service</div><div id="service-health" class="value"></div></article>
|
||||
<article class="card"><div class="label" data-i18n="runtime">Runtime phase</div><div id="runtime" class="value compact"></div></article>
|
||||
<article class="card"><div class="label" data-i18n="queuedTurns">Queued turns</div><div id="overview-queue" class="value"></div></article>
|
||||
<article class="card"><div class="label" data-i18n="queuedSessions">Queued sessions</div><div id="overview-sessions" class="value"></div></article>
|
||||
<article class="card wide"><h2 data-i18n="automation">Memory automation</h2><div class="flow"><div class="step"><div class="label" data-i18n="prompt">1 · Prompt</div><span data-i18n="recall">Automatic recall</span></div><span class="arrow">→</span><div class="step"><div class="label" data-i18n="response">2 · Response</div><span data-i18n="capture">Turn capture</span></div><span class="arrow">→</span><div class="step"><div class="label" data-i18n="workspace">3 · Workspace</div><span data-i18n="files">Memory files</span></div></div></article>
|
||||
<article class="card wide"><h2 data-i18n="connection">Connection</h2><p id="health"></p><p id="endpoint" class="endpoint muted"></p><small id="plugin-version"></small></article>
|
||||
</div>
|
||||
</section>
|
||||
<section id="panel-memory" role="tabpanel" aria-labelledby="tab-memory" hidden>
|
||||
<div class="grid">
|
||||
<article class="card"><div class="label" data-i18n="memory">Automatic memory</div><p><span id="memory-state" class="pill"></span></p></article>
|
||||
<article class="card"><div class="label" data-i18n="interval">Batch interval</div><div id="interval" class="value"></div></article>
|
||||
<article class="card"><div class="label" data-i18n="queuedTurns">Queued turns</div><div id="queue" class="value"></div></article>
|
||||
<article class="card"><div class="label" data-i18n="queuedSessions">Queued sessions</div><div id="sessions" class="value"></div></article>
|
||||
<article class="card full"><h2 data-i18n="recall">Automatic recall</h2><span id="recall-state" class="pill"></span><p id="recall-result"></p><p id="recall-time" class="muted label"></p><p id="other-queue" class="muted label" hidden></p></article>
|
||||
<article class="card full"><h2 data-i18n="activity">Recent activity</h2><ul id="activity" class="rows"></ul><p id="no-activity" class="muted" hidden></p></article>
|
||||
</div>
|
||||
</section>
|
||||
<section id="panel-dream" role="tabpanel" aria-labelledby="tab-dream" hidden>
|
||||
<div class="grid">
|
||||
<article class="card"><div class="label">Auto Dream</div><p><span id="dream-state" class="pill"></span></p></article>
|
||||
<article class="card"><div class="label" data-i18n="schedule">Schedule</div><div id="schedule" class="value compact"></div></article>
|
||||
<article class="card"><div class="label" data-i18n="timezone">Timezone</div><div id="timezone" class="value compact"></div></article>
|
||||
<article class="card"><div class="label" data-i18n="lastResult">Last result</div><div id="last-result" class="value compact"></div></article>
|
||||
<article class="card wide"><h2 data-i18n="next">Next run</h2><div id="next" class="value compact"></div><p class="muted" data-i18n="dreamHelp">The daily schedule runs while the ReMe MCP connection is alive.</p></article>
|
||||
<article class="card wide"><h2 data-i18n="last">Last consolidation</h2><p id="last"></p><p id="scheduler" class="muted"></p></article>
|
||||
</div>
|
||||
</section>
|
||||
<section id="panel-components" role="tabpanel" aria-labelledby="tab-components" hidden>
|
||||
<div class="grid">
|
||||
<article class="card wide"><div class="label" data-i18n="processMemory">Process memory (RSS)</div><div id="rss" class="value"></div></article>
|
||||
<article class="card wide"><div class="label" data-i18n="componentMemory">Estimated component memory</div><div id="component-total" class="value"></div></article>
|
||||
<article class="card full"><h2 data-i18n="components">Components</h2><div id="components" class="component-grid"></div><p id="no-components" class="muted" hidden></p><p class="note" data-i18n="componentHelp">Memory estimates from ReMe status. Component-level health is not included in the MCP response.</p></article>
|
||||
</div>
|
||||
<details><summary data-i18n="details">Service details</summary><pre id="details"></pre></details>
|
||||
</section>
|
||||
<p class="note" data-i18n="trust">Connection health does not establish Hook trust. Review ReMe in Codex Hooks settings.</p>
|
||||
<p class="note" data-i18n="saved">This view uses saved settings. Save changes in MCP settings, then refresh here.</p>
|
||||
</div>
|
||||
</main>
|
||||
<script>/* STATUS_APP */</script>
|
||||
</body>
|
||||
</html>
|
||||
228
integrations/codex/status-app/src/status.js
Normal file
228
integrations/codex/status-app/src/status.js
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
import { createAppTransport } from "@openai/mcp-extensions/app/transport";
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const transport = createAppTransport();
|
||||
let connected = false;
|
||||
let disposed = false;
|
||||
let language = "en";
|
||||
let zone = "UTC";
|
||||
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",
|
||||
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…", checked: "Checked", empty: "No activity recorded yet.",
|
||||
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.",
|
||||
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 · ${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: "总览", autoMemory: "自动记忆", consolidation: "记忆整理", components: "组件",
|
||||
service: "ReMe 服务", runtime: "运行状态", queuedTurns: "待提交轮数", queuedSessions: "待提交会话",
|
||||
automation: "记忆流程", prompt: "1 · 提问", response: "2 · 回复", workspace: "3 · 工作区",
|
||||
capture: "记录完成轮次", files: "记忆文件", interval: "批次轮数", timezone: "时区", lastResult: "最近结果",
|
||||
healthy: "健康", unhealthy: "异常", unknown: "未知", processMemory: "进程内存(RSS)",
|
||||
componentMemory: "组件内存估算", noComponents: "暂无组件内存信息。",
|
||||
componentHelp: "内存为 ReMe status 返回的估算值;MCP 响应不包含组件级健康信息。",
|
||||
dreamHelp: "只有 ReMe MCP 连接存活时,每日计划才会触发。",
|
||||
connection: "服务连接", memory: "自动记录", recall: "自动召回", schedule: "整理计划", next: "下次运行",
|
||||
scheduler: "调度状态", last: "最近整理", activity: "最近活动", details: "服务详情", on: "已开启", off: "已关闭",
|
||||
connected: "连接正常", offline: "无法连接", refresh: "刷新", refreshing: "刷新中…", checked: "检查时间",
|
||||
empty: "暂无活动记录。", waiting: "正在等待首次状态结果…", unavailable: "未能刷新状态,请重试或重新打开 ReMe status。",
|
||||
noHost: "请从 Codex 的 MCP 设置中打开 ReMe status 查看实时数据。",
|
||||
trust: "服务连接正常不代表 Hook 已获信任,请在 Codex Hooks 设置中审核 ReMe 条目。",
|
||||
saved: "此页面使用已保存的配置。修改 MCP 设置后,请先保存,再刷新此页面。",
|
||||
pending: (n) => `${n} 轮待提交`, batch: (n, s) => `每批 ${n} 轮 · ${s} 个会话待提交`,
|
||||
other: (n) => `另有 ${n} 轮等待提交至其他服务地址。`, noRecall: "暂无召回活动", noDream: "暂无整理记录", paused: "暂无计划",
|
||||
running: "运行中", starting: "启动中", updating: "正在应用配置", stopped: "已停止",
|
||||
scheduled: "定时", manual: "手动", completed: "已完成", failed: "失败", cancelled: "已取消", interrupted: "已中断",
|
||||
recall_found: "已召回相关记忆", recall_empty: "未找到相关记忆", recall_failed: "召回失败", recall_disabled: "自动召回已关闭",
|
||||
memory_queued: "已加入记录队列", memory_saved: "记忆已保存", memory_failed: "记忆提交失败", memory_disabled: "自动记录已关闭",
|
||||
retry_failed: "重试提交失败", dream_started: "开始整理", dream_completed: "整理已完成", dream_failed: "整理失败",
|
||||
dream_cancelled: "整理已取消", scheduled_dream_failed: "定时整理失败", search_found: "检索到相关记忆",
|
||||
search_empty: "未检索到相关记忆", search_failed: "检索失败", capture_skipped: "未捕获到完成轮次",
|
||||
SessionStart: "会话开始", SessionEnd: "会话结束", Stop: "轮次完成", SubagentStop: "子代理轮次完成", UserPromptSubmit: "已提交提问",
|
||||
},
|
||||
};
|
||||
const t = (key) => words[language][key] ?? key;
|
||||
const text = (id, value) => { $(id).textContent = value; };
|
||||
function formatTime(value) {
|
||||
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",
|
||||
}).format(date);
|
||||
}
|
||||
function pill(id, value, tone = "good") { text(id, value); $(id).dataset.tone = tone; }
|
||||
function toggle(id, enabled) { pill(id, t(enabled ? "on" : "off"), enabled ? "good" : "neutral"); }
|
||||
function applyHost(context = {}) {
|
||||
if (["dark", "light"].includes(context.theme)) document.documentElement.dataset.theme = context.theme;
|
||||
for (const key of ["--color-background-primary", "--color-text-primary"]) {
|
||||
const value = context.styles?.variables?.[key];
|
||||
if (typeof value === "string") document.documentElement.style.setProperty(key, value);
|
||||
}
|
||||
}
|
||||
function fail() {
|
||||
if (disposed) return;
|
||||
text("error", t("unavailable")); $("error").hidden = false;
|
||||
$("loading").hidden = true;
|
||||
document.querySelector("main").setAttribute("aria-busy", "false");
|
||||
}
|
||||
function render(result) {
|
||||
if (disposed) return;
|
||||
const data = result?.structuredContent;
|
||||
if (result?.isError || !data?.service?.health_check || !data?.auto_memory || !data?.auto_dream) { fail(); return; }
|
||||
language = data.language === "zh" ? "zh" : "en";
|
||||
zone = data.auto_dream.timezone;
|
||||
document.documentElement.lang = language === "zh" ? "zh-CN" : "en";
|
||||
document.querySelectorAll("[data-i18n]").forEach((node) => { node.textContent = t(node.dataset.i18n); });
|
||||
text("refresh", t(refreshing ? "refreshing" : "refresh"));
|
||||
const health = data.service.health_check;
|
||||
pill("connection-state", t(health.reachable ? "connected" : "offline"), health.reachable ? "good" : "bad");
|
||||
$("connection-state").hidden = false;
|
||||
const summary = typeof health.answer === "string" ? health.answer.trim().match(/(?:^|-\s*)(unhealthy|healthy)$/) : null;
|
||||
text("service-health", t(!health.reachable ? "offline" : summary?.[1] ?? "unknown"));
|
||||
$("service-health").dataset.tone = !health.reachable || summary?.[1] === "unhealthy" ? "bad" : summary ? "good" : "neutral";
|
||||
text("runtime", t(data.auto_dream.phase));
|
||||
text("overview-queue", data.auto_memory.queued_turns);
|
||||
text("overview-sessions", data.auto_memory.queued_sessions);
|
||||
text("health", health.reachable ? (typeof health.answer === "string" ? health.answer : JSON.stringify(health.answer)) : health.error);
|
||||
text("endpoint", data.mcpUrl);
|
||||
text("plugin-version", `ReMe for Codex · ${data.plugin_version}`);
|
||||
text("checked", `${t("checked")} ${formatTime(data.checked_at)}`);
|
||||
toggle("memory-state", data.auto_memory.enabled);
|
||||
text("queue", data.auto_memory.queued_turns);
|
||||
text("interval", data.auto_memory.interval);
|
||||
text("sessions", data.auto_memory.queued_sessions);
|
||||
$("other-queue").hidden = !data.auto_memory.other_endpoint_queued_turns;
|
||||
text("other-queue", t("other")(data.auto_memory.other_endpoint_queued_turns));
|
||||
toggle("recall-state", data.auto_recall.enabled);
|
||||
const activity = data.recent_activity ?? [];
|
||||
const recall = activity.findLast((row) => row.event?.startsWith("recall_"));
|
||||
text("recall-result", recall ? t(recall.event) : t("noRecall"));
|
||||
text("recall-time", recall ? formatTime(recall.time) : "—");
|
||||
const dream = data.auto_dream;
|
||||
toggle("dream-state", dream.enabled);
|
||||
text("schedule", dream.cron);
|
||||
text("timezone", dream.timezone);
|
||||
text("next", dream.next_run_at ? formatTime(dream.next_run_at) : t("paused"));
|
||||
text("scheduler", `${t("scheduler")}: ${t(dream.phase)}`);
|
||||
const last = dream.last_run;
|
||||
text("last-result", last ? t(last.status) : "—");
|
||||
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"));
|
||||
renderComponents(data.service.status);
|
||||
text("details", JSON.stringify(data.service, null, 2));
|
||||
$("error").hidden = true; $("loading").hidden = true; $("overview").hidden = false;
|
||||
document.querySelector("main").setAttribute("aria-busy", "false"); hasSnapshot = true;
|
||||
}
|
||||
|
||||
function renderComponents(status) {
|
||||
// ReMe's MCP contract returns the human-readable answer, not HTTP response metadata.
|
||||
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] ?? "—");
|
||||
text("component-total", report.match(new RegExp(`^ Components total +${amount}$`, "m"))?.[1] ?? "—");
|
||||
$("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("_", " ")} · ${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"));
|
||||
}
|
||||
const 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();
|
||||
});
|
||||
}
|
||||
|
||||
let resizeFrame;
|
||||
const 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: {} }, 125000)); }
|
||||
catch { fail(); }
|
||||
finally { refreshing = false; $("refresh").disabled = !connected; text("refresh", t("refresh")); }
|
||||
});
|
||||
async function connect() {
|
||||
if (window.parent === window) { text("loading", t("noHost")); return; }
|
||||
const host = await transport.request("ui/initialize", {
|
||||
protocolVersion: "2026-01-26", appInfo: { name: "reme-status", version: "0.2.1" },
|
||||
appCapabilities: { availableDisplayModes: ["fullscreen"] },
|
||||
});
|
||||
if (host.protocolVersion !== "2026-01-26") throw new Error("Unsupported UI protocol");
|
||||
connected = true; applyHost(host.hostContext); $("refresh").disabled = false;
|
||||
if (!hasSnapshot) text("loading", t("waiting"));
|
||||
transport.notify("ui/notifications/initialized");
|
||||
const context = host.hostContext ?? {};
|
||||
if (context.displayMode !== "fullscreen" && context.availableDisplayModes?.includes("fullscreen")) {
|
||||
// One supported request only; respect the host's final placement (including a settings modal).
|
||||
await transport.request("ui/request-display-mode", { mode: "fullscreen" }).catch(() => {});
|
||||
}
|
||||
}
|
||||
connect().catch(fail);
|
||||
162
tests/integration/test_codex_plugin_status_ui.py
Normal file
162
tests/integration/test_codex_plugin_status_ui.py
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
"""Opt-in browser verification of the bundled view using an MCP host harness, not Codex screenshots.
|
||||
|
||||
Run with REME_CODEX_STATUS_UI=1 pytest tests/integration/test_codex_plugin_status_ui.py.
|
||||
Requires Playwright and its Chromium browser; no model, service, or external network is used.
|
||||
"""
|
||||
|
||||
# pylint: disable=missing-function-docstring,redefined-outer-name
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
os.environ.get("REME_CODEX_STATUS_UI") != "1",
|
||||
reason="Opt-in status view browser check",
|
||||
)
|
||||
VIEW = Path(__file__).resolve().parents[2] / "integrations/codex/plugins/reme/ui/status.html"
|
||||
|
||||
|
||||
def snapshot(language):
|
||||
return {
|
||||
"checked_at": 1790757300,
|
||||
"language": language,
|
||||
"plugin_version": "0.2.1",
|
||||
"mcpUrl": "http://localhost:2333/mcp",
|
||||
"service": {
|
||||
"health_check": {"reachable": True, "answer": "ReMe v0.4.1.13 - healthy"},
|
||||
"status": {
|
||||
"reachable": True,
|
||||
"answer": (
|
||||
"Memory (estimated component object size)\n file_store:default 1.25 MiB\n"
|
||||
" keyword_index:default 512 B\n Components total 1.25 MiB\n Process RSS 120.50 MiB"
|
||||
),
|
||||
},
|
||||
},
|
||||
"auto_memory": {
|
||||
"enabled": True,
|
||||
"interval": 5,
|
||||
"queued_turns": 2,
|
||||
"queued_sessions": 1,
|
||||
"other_endpoint_queued_turns": 0,
|
||||
},
|
||||
"auto_recall": {"enabled": True},
|
||||
"auto_dream": {
|
||||
"enabled": True,
|
||||
"cron": "0 23 * * *",
|
||||
"timezone": "Asia/Shanghai",
|
||||
"phase": "running",
|
||||
"next_run_at": "2026-09-30T15:00:00+00:00",
|
||||
"last_run": None,
|
||||
},
|
||||
"recent_activity": [
|
||||
{"event": "memory_saved", "time": 1790757300, "turns": 1},
|
||||
{"event": "recall_found", "time": 1790757301, "turns": 0},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@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")
|
||||
initial = snapshot(language)
|
||||
with playwright.sync_playwright() as runtime:
|
||||
browser = runtime.chromium.launch()
|
||||
page = browser.new_page(viewport={"width": 980, "height": 1250})
|
||||
page.set_default_timeout(5000)
|
||||
errors, external = [], []
|
||||
page.on("pageerror", lambda error: errors.append(str(error)))
|
||||
page.route("**/*", lambda route: (external.append(route.request.url), route.abort()))
|
||||
page.set_content('<iframe title="MCP status test host" style="border:0;width:940px;height:1200px"></iframe>')
|
||||
page.evaluate(
|
||||
"""({html, initial, theme}) => {
|
||||
window.calls = [];
|
||||
window.result = {structuredContent: initial};
|
||||
window.failRefresh = false;
|
||||
const iframe = document.querySelector('iframe');
|
||||
function send(value) { iframe.contentWindow.postMessage({jsonrpc: '2.0', ...value}, '*'); }
|
||||
window.sendStatus = () => send({method:'ui/notifications/tool-result', params:window.result});
|
||||
window.addEventListener('message', event => {
|
||||
if (event.source !== iframe.contentWindow) return;
|
||||
const m = event.data;
|
||||
window.calls.push(m);
|
||||
if (m.method === 'ui/initialize') {
|
||||
send({id:m.id, result:{protocolVersion:'2026-01-26', hostCapabilities:{serverTools:{}},
|
||||
hostContext:{theme, displayMode:'fullscreen', availableDisplayModes:['fullscreen']}}});
|
||||
} else if (m.method === 'ui/notifications/initialized') {
|
||||
window.sendStatus();
|
||||
} else if (m.method === 'tools/call') {
|
||||
if (window.failRefresh) send({id:m.id, error:{code:-32603, message:'test failure'}});
|
||||
else send({id:m.id, result:window.result});
|
||||
}
|
||||
});
|
||||
iframe.srcdoc = html;
|
||||
}""",
|
||||
{"html": VIEW.read_text(), "initial": initial, "theme": theme},
|
||||
)
|
||||
frame = page.frame_locator("iframe")
|
||||
try:
|
||||
frame.locator("#overview").wait_for(state="visible")
|
||||
except playwright.TimeoutError:
|
||||
assert not errors, errors
|
||||
raise
|
||||
assert frame.locator("html").get_attribute("data-theme") == theme
|
||||
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 page.evaluate(
|
||||
"window.calls.some(m => m.method === 'tools/call')",
|
||||
) # Opening reuses the supplied result.
|
||||
page.screenshot(path=str(tmp_path / f"status-test-host-{theme}.png"), full_page=True)
|
||||
for name in ("memory", "dream", "components"):
|
||||
frame.locator(f"#tab-{name}").click()
|
||||
assert frame.locator(f"#panel-{name}").is_visible()
|
||||
assert frame.locator("#panel-overview").is_hidden()
|
||||
page.screenshot(path=str(tmp_path / f"status-test-host-{theme}-{name}.png"), full_page=True)
|
||||
assert frame.locator("#rss").inner_text() == "120.50 MiB"
|
||||
assert frame.locator("#components .component").count() == 2
|
||||
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")
|
||||
|
||||
offline = snapshot(language)
|
||||
offline["service"]["health_check"] = {"reachable": False, "error": "ConnectionError"}
|
||||
offline["service"]["status"] = {"reachable": False, "error": "ConnectionError"}
|
||||
offline["auto_memory"]["queued_turns"] = 3
|
||||
offline["mcpUrl"] = "<img src=x onerror=window.injected=true>"
|
||||
page.evaluate("value => window.result = {structuredContent:value}", offline)
|
||||
frame.locator("#refresh").click()
|
||||
playwright.expect(frame.locator("#connection-state")).to_have_attribute("data-tone", "bad")
|
||||
assert frame.locator("#endpoint").inner_text() == offline["mcpUrl"]
|
||||
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 frame.locator("#error").is_hidden() # Offline is a useful status result, not a broken panel.
|
||||
|
||||
frame.locator("#tab-memory").click()
|
||||
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.
|
||||
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 "ConnectionError" in frame.locator("#details").inner_text()
|
||||
unhealthy = snapshot(language)
|
||||
unhealthy["service"]["health_check"]["answer"] = "ReMe v0.4.1.13 - unhealthy"
|
||||
page.evaluate("value => { window.failRefresh = false; window.result = {structuredContent:value}; }", unhealthy)
|
||||
frame.locator("#tab-overview").click()
|
||||
frame.locator("#refresh").click()
|
||||
playwright.expect(frame.locator("#service-health")).to_have_attribute("data-tone", "bad")
|
||||
assert frame.locator("#connection-state").get_attribute("data-tone") == "good" # Reachable is not healthy.
|
||||
assert not errors and not external
|
||||
browser.close()
|
||||
|
|
@ -62,12 +62,14 @@ async def test_settings_remain_editable_offline(plugin, monkeypatch):
|
|||
capability = client.initialize_result.capabilities.experimental["openai/settings"]
|
||||
assert capability == {"readTool": module.SETTINGS_READ, "updateTool": module.SETTINGS_UPDATE}
|
||||
tools = {tool.name: tool for tool in await client.list_tools()}
|
||||
assert {module.SETTINGS_READ, module.SETTINGS_UPDATE, module.CHECK_CONNECTION} <= tools.keys()
|
||||
assert {module.SETTINGS_READ, module.SETTINGS_UPDATE, module.STATUS} <= tools.keys()
|
||||
assert "reme_check_connection" not in tools
|
||||
read = await client.call_tool(capability["readTool"], {})
|
||||
settings = read.structured_content
|
||||
assert settings["values"] == config.DEFAULTS
|
||||
assert settings["schema"]["properties"].keys() == config.DEFAULTS.keys()
|
||||
assert settings["layout"][0]["items"][1]["tool"] == module.CHECK_CONNECTION
|
||||
assert settings["layout"][0]["items"][1]["tool"] == module.STATUS
|
||||
assert len(settings["layout"][0]["items"]) == 2
|
||||
assert tools[module.SETTINGS_READ].annotations.readOnlyHint
|
||||
assert tools[module.SETTINGS_READ].outputSchema
|
||||
assert not (config.data_dir() / "config.json").exists()
|
||||
|
|
@ -171,23 +173,53 @@ async def test_hooks_call_mcp_and_preserve_structured_answers(plugin, monkeypatc
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_connection_action_uses_current_endpoint_and_propagates_failure(plugin, upstreams, monkeypatch):
|
||||
async def test_status_checks_current_connection_and_reports_failure_without_hiding_local_state(
|
||||
plugin,
|
||||
upstreams,
|
||||
monkeypatch,
|
||||
):
|
||||
config, module, _ = plugin
|
||||
call = AsyncMock(return_value={"success": True, "answer": "healthy"})
|
||||
monkeypatch.setattr(module, "call_async", call)
|
||||
monkeypatch.setattr(importlib.import_module("reme_tools"), "call_async", call)
|
||||
async with Client(module.create_server()) as client:
|
||||
await client.call_tool(
|
||||
module.SETTINGS_UPDATE,
|
||||
{"set": {"mcpUrl": "http://second.test/mcp", "requestTimeoutMs": 3000}},
|
||||
)
|
||||
assert (await client.call_tool(module.CHECK_CONNECTION, {})).data == "healthy"
|
||||
assert call.call_args.args == (config.load_config(), "health_check", {}, 3)
|
||||
result = await client.call_tool(module.STATUS, {})
|
||||
assert result.structured_content["service"]["health_check"] == {"reachable": True, "answer": "healthy"}
|
||||
assert {args.args[1] for args in call.call_args_list} == {"health_check", "status"}
|
||||
assert all(args.args[0] == config.load_config() and args.args[2:] == ({}, 3) for args in call.call_args_list)
|
||||
assert result.structured_content["checked_at"] > 0
|
||||
assert result.structured_content["language"] == "en"
|
||||
call.side_effect = ConnectionError("offline")
|
||||
with pytest.raises(ToolError, match="offline"):
|
||||
await client.call_tool(module.CHECK_CONNECTION, {})
|
||||
offline = (await client.call_tool(module.STATUS, {})).structured_content
|
||||
assert offline["service"]["health_check"] == {"reachable": False, "error": "ConnectionError"}
|
||||
assert offline["auto_memory"]["queued_turns"] == 0
|
||||
assert not upstreams
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_status_opens_a_self_contained_fullscreen_app_resource(plugin):
|
||||
_, module, _ = plugin
|
||||
async with Client(module.create_server()) as client:
|
||||
tools = {tool.name: tool for tool in await client.list_tools()}
|
||||
ui = tools["reme_status"].meta["ui"]
|
||||
assert ui["visibility"] == ["app", "model"]
|
||||
content = (await client.read_resource(ui["resourceUri"]))[0]
|
||||
assert content.mimeType == "text/html;profile=mcp-app"
|
||||
assert content.meta["openai/ui"] == {
|
||||
"preferredDisplayMode": "fullscreen",
|
||||
"availableDisplayModes": ["fullscreen"],
|
||||
}
|
||||
assert content.meta["ui"]["csp"] == {"connectDomains": [], "resourceDomains": []}
|
||||
assert 'id="refresh"' in content.text
|
||||
assert "ui/initialize" in content.text
|
||||
assert "ui/notifications/tool-result" in content.text
|
||||
assert "/* STATUS_APP */" not in content.text
|
||||
assert "<script src=" not in content.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_focused_search_uses_live_defaults_validates_input_and_reports_empty(plugin, monkeypatch):
|
||||
config, module, _ = plugin
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue