Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes follow [Keep a Changelog](https://keepachangelog.com/en/1.1.

## [Unreleased]

### Added

- Register a fullscreen viewer in the app list under **…** in Codex's left toolbar, where users can pin it, alongside the existing task right-panel entry. Both use the same app-only tool, open the exact task when valid context is available, and show a local task selector when it is unavailable.

## [1.0.0] - 2026-10-02

### Added
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

See what happened in a Codex task, where time went, and how many tokens it used.

Codex Trajectory is a plugin that turns local Codex logs into a searchable event ledger and interactive timeline. Open it directly in the Codex desktop side panel.
Codex Trajectory is a plugin that turns local Codex logs into a searchable event ledger and interactive timeline. Open it from the app list under **…** in Codex's left toolbar for a fullscreen viewer, or from a task's right panel.

![Codex Trajectory: English viewer](plugins/codex-trajectory/assets/screenshots/overview-en.png)

Expand Down Expand Up @@ -39,11 +39,11 @@ Open a new Codex task to load the plugin. After updating an existing installatio

## Use

1. In the task's right panel, select **+ → More tools… → Plugins and MCPs → Codex Trajectory**.
1. Open **…** in Codex's left toolbar and choose **Codex Trajectory** for a fullscreen viewer; you can pin it to the toolbar. You can also select **+ → More tools… → Plugins and MCPs → Codex Trajectory** in the task's right panel.
2. Browse the timeline and event ledger. Use search and filters to find a record; select it to inspect it.
3. Click **Live window** to follow activity, or **Load earlier records** to browse history.

The native entry opens the current task without a model turn. If the host cannot identify that task, it shows a local task selector.
Both native entries open without a model turn. With valid task context, the viewer opens that exact task; without available task context, it shows a local task selector.

You can also ask Codex:

Expand Down
6 changes: 3 additions & 3 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

看清 Codex 任务做了什么、时间花在哪里、用了多少 Token。

Codex Trajectory 将本地 Codex 日志整理成可搜索的事件账本和交互时间轴,直接在 Codex 桌面端侧栏中打开。
Codex Trajectory 将本地 Codex 日志整理成可搜索的事件账本和交互时间轴,可从 Codex 左侧工具栏 **…** 中的应用列表全屏打开,也可在任务右侧面板中打开。

![Codex Trajectory 中文查看器](plugins/codex-trajectory/assets/screenshots/desktop-zh-CN.png)

Expand Down Expand Up @@ -39,11 +39,11 @@ codex plugin add codex-trajectory@icesixgod

## 使用

1. 在任务右侧面板中选择 **+ → More tools… → Plugins and MCPs → Codex Trajectory**。
1. 在 Codex 左侧工具栏的 **…** 应用列表中打开 **Codex Trajectory**,进入全屏查看器,并可将其固定到工具栏。也可在任务右侧面板中选择 **+ → More tools… → Plugins and MCPs → Codex Trajectory**。
2. 浏览时间轴和事件账本,用搜索与筛选定位记录,点击记录查看详情。
3. 点击 **实时小窗**跟随任务活动,或点击**加载更早记录**浏览历史。

原生入口直接打开当前任务,不触发模型对话。宿主无法识别当前任务时,会显示本地任务选择器。
两个原生入口都不触发模型对话。有有效任务上下文时,查看器精确打开该任务;任务上下文不可用时,显示本地任务选择器。

也可以直接对 Codex 说:

Expand Down
4 changes: 2 additions & 2 deletions docs/interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

Codex Trajectory exposes three user-facing session MCP tools, four app-only helpers, and one `text/html;profile=mcp-app` resource at `ui://codex-trajectory/trajectory-v2.html`. The viewer uses the native MCP Apps host and exposes no task-control actions. `list_codex_sessions` updates a private derived index and advertises `readOnlyHint: false`; it never edits source task logs.

## Native conversation panel
## Native viewer entrypoints

`open_codex_trajectory` has an empty input schema and app-only visibility. Its descriptor declares `_meta.ui.resourceUri` and `_meta["openai/ui"].entrypoints: [{"type": "thread"}]`. The host can open it from the conversation's right-panel tools menu without a model turn.
`open_codex_trajectory` has an empty input schema and app-only visibility. Its descriptor declares `_meta.ui.resourceUri` and `_meta["openai/ui"].entrypoints: [{"type": "global"}, {"type": "thread"}]`. The global entry registers the viewer in the app list under **…** in Codex's left toolbar for fullscreen opening. Users can pin it to the toolbar; pinning is controlled by Codex rather than the plugin manifest. The thread entry retains the conversation's right-panel tools menu. Both use the same tool and open without a model turn. Valid task context opens that exact task; unavailable context shows a local task selector. After updating an existing installation, fully quit and reopen Codex to load the new registrations.

The dispatcher passes request `_meta` to this tool without storing task context in global state. On the tested Codex desktop client, `thread_id` and `threadId` contain the calling task ID. Either alias is accepted; both must match when present. Values must be bounded identifiers and resolve to the exact local task, including an archived task. Both successful reads and read-limit errors require an actual matching `session_meta.id` or legacy `session_meta.session_id`; missing or invalid metadata identities return task selection, even when the filename matches. Prefixes, file aliases, process `CODEX_THREAD_ID`, and anonymized `openai/session` metadata never substitute for native caller identity. The same exact-ID check applies to read-limit errors before offering an increased-budget retry; a mismatched or unavailable error identity returns task selection. When compressed metadata cannot be decoded within the current budget, filename-derived identities remain unverified and the native entry returns task selection. Increased-budget retries verify the returned session ID before displaying any trajectory. Missing, invalid, conflicting, or unavailable context returns `{viewerState: "select-session", reason}` with the same UI resource; the user explicitly chooses a task before any trajectory is displayed. This initialization envelope is separate from the frozen trajectory schemas.

Expand Down
4 changes: 2 additions & 2 deletions docs/native-viewer.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# Native viewer migration

The viewer now uses Codex's native conversation-panel extension. Open the right panel's **+ → More tools… → Plugins and MCPs → Codex Trajectory** entry. It opens the current task's safe summary without a model turn. A missing or unavailable task context shows a selector, so the viewer never guesses another task from recent activity.
The viewer uses Codex's native global and conversation-panel extensions. Open **…** in Codex's left toolbar and choose **Codex Trajectory** for a fullscreen viewer; you can pin it to the toolbar. The existing right-panel **+ → More tools… → Plugins and MCPs → Codex Trajectory** entry remains available. Both entries use the same app-only tool and open without a model turn. Valid task context opens that exact task's safe summary. A missing or unavailable task context shows a local task selector, so the viewer never guesses another task from recent activity. After updating an existing installation, fully quit and reopen Codex to load the new entries. Toolbar pinning is controlled by Codex, and the plugin manifest cannot request automatic pinning.

The public session-list, trajectory-read, and interactive-show tools retain their defaults. Live Token/cost/quota views, compressed and paginated history, bounded full-detail confirmation, and the frozen v1/v2 schemas are preserved. Task status reflects the selected task; live account quota uses the newest valid sample found in recent local active-task logs, updates independently of task revisions, and shows its sample time in the tooltip. Both can lag the service. Manual stopping and quota-triggered automatic stopping have been removed.

After **Load full details**, leave **Default to full details for all tasks on this device** checked and continue once to remember the choice across tasks, reopened panels, and Codex restarts. Uncheck it for the selected task in this panel only. **Restore safe summary default** clears the opt-in and reloads a summary. The choice is stored locally as a boolean through app-only tools, so it works in sandboxed panels without browser storage. Public tool defaults, initial native results, and every live surface remain safe summaries; the saved choice enables on-demand detail reads without a subsequent full read on opening. Opening, task switching, refresh, and history pagination request summaries. Clicking a record requests at most 50 full records ending there; confirmation requests the current selection. Pending overlapping page reads are shared, errors offer an inspector retry, and a 5 MiB panel cache reuses loaded details until refresh, task change, revocation, or teardown. Stale responses are discarded. Uncached backend projections still scan the complete log for aggregates; summary reads skip formatting hidden detail fields.

## Structure

- `tools.py` owns MCP descriptors, argument dispatch, and native opening.
- `tools.py` owns MCP descriptors, argument dispatch, and native opening. The native tool embeds the packaged plugin icon as a PNG data URI for the toolbar and panel entries.
- `native_view.py` validates per-call context and defines selection responses.
- `protocol.py` transports request metadata without shared task identity.
- `projection.py` projects bounded local rollouts and loads the self-contained UI resource.
Expand Down
2 changes: 1 addition & 1 deletion plugins/codex-trajectory/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"interface": {
"displayName": "Codex Trajectory",
"shortDescription": "Inspect turns, tools, tokens, and timing in local Codex tasks.",
"longDescription": "A privacy-aware trajectory viewer for local Codex tasks. Open the calling task from the native conversation side panel, inspect safe summaries or opt-in bounded details, and follow live events, tokens, local cost estimates, and quota windows without a debugging port.",
"longDescription": "A privacy-aware trajectory viewer for local Codex tasks. Open it in fullscreen from the app list under … in Codex's left toolbar, pin it there, or open it from a task's native right panel. Valid task context opens that exact task; otherwise, choose a local task. Inspect safe summaries or opt-in bounded details, and follow live events, tokens, local cost estimates, and quota windows without a debugging port.",
"developerName": "icesixgod",
"category": "Developer Tools",
"capabilities": [
Expand Down
8 changes: 6 additions & 2 deletions plugins/codex-trajectory/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@

View local Codex task history as an interactive timeline and event ledger, with timing, tokens, estimated costs, and live activity.

Open **+ → More tools… → Plugins and MCPs → Codex Trajectory** in the task's right panel. The native entry opens the current task's safe summary without a model turn; unavailable task context shows a selector. Full details require confirmation, and live views always use safe summaries. Source logs remain unchanged.
Open **…** in Codex's left toolbar and choose **Codex Trajectory** for a fullscreen viewer; you can pin it to the toolbar. You can also choose **+ → More tools… → Plugins and MCPs → Codex Trajectory** in the task's right panel. Both native entries open without a model turn. Valid task context opens that exact task's safe summary; unavailable context shows a local task selector. Full details require confirmation, and live views always use safe summaries. Source logs remain unchanged.

After updating an existing installation, fully quit and reopen Codex to load the new entries.

See the [English README](../../README.md) for installation, screenshots, usage, and privacy. Technical details are in the [interface reference](../../docs/interface.md) and [native viewer guide](../../docs/native-viewer.md).

Expand All @@ -16,6 +18,8 @@ See the [English README](../../README.md) for installation, screenshots, usage,

将本地 Codex 任务历史展示为交互时间轴和事件账本,查看耗时、Token、估算费用和实时活动。

在任务右侧面板中打开 **+ → More tools… → Plugins and MCPs → Codex Trajectory**。原生入口直接展示当前任务的安全摘要,不触发模型对话;无法识别任务时显示选择器。完整详情需要确认,实时视图始终使用安全摘要,源日志保持不变。
在 Codex 左侧工具栏的 **…** 应用列表中打开 **Codex Trajectory**,进入全屏查看器,并可将其固定到工具栏。也可在任务右侧面板中选择 **+ → More tools… → Plugins and MCPs → Codex Trajectory**。两个原生入口都不触发模型对话。有有效任务上下文时,精确展示该任务的安全摘要;上下文不可用时,显示本地任务选择器。完整详情需要确认,实时视图始终使用安全摘要,源日志保持不变。

更新已有安装后,请完整退出并重新打开 Codex,以加载新入口。

安装、截图、使用和隐私说明见[中文 README](../../README.zh-CN.md)。技术细节见[接口文档](../../docs/interface.md)和[原生查看器指南](../../docs/native-viewer.md)。
16 changes: 14 additions & 2 deletions plugins/codex-trajectory/scripts/codex_trajectory/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

from __future__ import annotations

from base64 import b64encode
from functools import cache
from pathlib import Path
from typing import Any

from . import projection
Expand All @@ -17,6 +20,13 @@
from .viewer_preferences import default_full_details, set_default_full_details


@cache
def _viewer_icon_uri() -> str:
"""Embed the packaged icon for native hosts that cannot load local paths."""
icon = Path(__file__).parents[2] / "assets" / "icon.png"
return "data:image/png;base64," + b64encode(icon.read_bytes()).decode("ascii")


def current_trajectory_result(metadata: dict[str, Any] | None) -> dict[str, Any]:
"""Open the exact calling task, or let the user choose a local task."""
try:
Expand Down Expand Up @@ -162,14 +172,16 @@ def tool_definitions() -> list[dict[str, Any]]:
{
"name": "open_codex_trajectory",
"title": "Codex Trajectory",
"icons": [{"src": _viewer_icon_uri(), "mimeType": "image/png"}],
"description": (
"Open the calling Codex task's safe trajectory in the native side panel."
"Open the native trajectory viewer from the sidebar or task side panel. "
"Show the calling task's safe summary, or a task selector without task context."
),
"inputSchema": {"type": "object", "properties": {}, "additionalProperties": False},
"annotations": read_only,
"_meta": {
"ui": {"resourceUri": UI_URI, "visibility": ["app"]},
"openai/ui": {"entrypoints": [{"type": "thread"}]},
"openai/ui": {"entrypoints": [{"type": "global"}, {"type": "thread"}]},
"openai/visibility": "private",
"openai/outputTemplate": UI_URI,
},
Expand Down
32 changes: 31 additions & 1 deletion plugins/codex-trajectory/tests/test_native_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

import json
from base64 import b64decode
from pathlib import Path
from typing import Any

Expand All @@ -24,13 +25,42 @@ def test_native_entrypoint_is_app_only_and_read_only() -> None:
tool = next(
item for item in projection.tool_definitions() if item["name"] == "open_codex_trajectory"
)
assert tool["_meta"]["openai/ui"]["entrypoints"] == [{"type": "thread"}]
assert tool["_meta"]["openai/ui"]["entrypoints"] == [{"type": "global"}, {"type": "thread"}]
assert tool["_meta"]["ui"] == {"resourceUri": projection.UI_URI, "visibility": ["app"]}
assert tool["_meta"]["openai/visibility"] == "private"
assert tool["annotations"]["readOnlyHint"] is True
assert tool["inputSchema"]["additionalProperties"] is False


def test_native_entrypoint_embeds_the_packaged_icon() -> None:
tool = next(
item for item in projection.tool_definitions() if item["name"] == "open_codex_trajectory"
)
icon = tool["icons"][0]
prefix, data = icon["src"].split(",", 1)
assert prefix == "data:image/png;base64"
assert icon["mimeType"] == "image/png"
assert (
b64decode(data, validate=True)
== (Path(__file__).parents[1] / "assets" / "icon.png").read_bytes()
)


def test_sidebar_without_context_selects_before_reading_a_trajectory(
codex_home: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
def unexpected_read(*args: Any, **kwargs: Any) -> dict[str, Any]:
pytest.fail("A sidebar entry without task context must wait for explicit selection.")

monkeypatch.setattr(projection, "trajectory_result", unexpected_read)
result = open_panel()
assert result["structuredContent"] == {
"viewerState": "select-session",
"reason": "missing-context",
}
assert result["_meta"]["ui"]["resourceUri"] == projection.UI_URI


def test_native_panel_binds_exact_caller_instead_of_latest(codex_home: Path) -> None:
write_rollout(codex_home / "sessions" / "rollout-newer.jsonl", rollout_events("session-newer"))
result = open_panel({"thread_id": "session-alpha", "threadId": "session-alpha"})
Expand Down
3 changes: 2 additions & 1 deletion plugins/codex-trajectory/tests/test_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ def test_tool_definitions_scope_reads_and_native_panel() -> None:
)
assert tools["show_codex_trajectory"]["_meta"]["ui"]["resourceUri"] == UI_URI
assert tools["open_codex_trajectory"]["_meta"]["openai/ui"]["entrypoints"] == [
{"type": "thread"}
{"type": "global"},
{"type": "thread"},
]
update = tools["get_codex_trajectory_update"]
assert update["_meta"]["ui"]["visibility"] == ["app"]
Expand Down
33 changes: 33 additions & 0 deletions plugins/codex-trajectory/tests/test_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -2083,6 +2083,39 @@ def test_native_unavailable_task_can_retry_and_select(page: Page, harness_url: s
expect(frame.locator("#refresh")).to_be_visible()


@pytest.mark.parametrize("language", ["en", "zh"])
def test_sidebar_fullscreen_selects_a_task_and_returns_from_live_view(
page: Page, harness_url: str, language: str
) -> None:
page.goto(f"{harness_url}/{language}-native-sidebar")
frame = viewer(page)
select = frame.locator("#sessionSelect")
expect(select.locator('option[value="session-beta"]')).to_have_count(1)
expect(select).to_have_value("")
assert frame.locator("#ledger").count() == 0
assert set(page.evaluate("window.__trajectoryToolNames")) == {
"list_codex_sessions",
"get_codex_trajectory_preferences",
}

select.select_option("session-beta")
expect(frame.locator("#refresh")).to_be_visible()
expect(select).to_have_value("session-beta")
calls = page.evaluate("window.__trajectoryCalls")
assert any(
call.get("sessionId") == "session-beta" and call.get("detailLevel") == "summary"
for call in calls
)
frame.locator("#openPip").click()
expect(frame.locator("#liveDock")).to_have_attribute("data-session-id", "session-beta")
frame.locator("#closeDock").click()
expect(frame.locator("#refresh")).to_be_visible()
expect(select).to_have_value("session-beta")
assert page.evaluate("window.__trajectoryDisplayModes") == []
assert "ui/message" not in page.evaluate("window.__trajectoryProtocol")
assert frame.get_by_role("alert").count() == 0


def test_native_host_context_controls_theme_locale_and_colors(page: Page, harness_url: str) -> None:
page.goto(f"{harness_url}/zh-native")
frame = viewer(page)
Expand Down
Loading
Loading