Skip to content

Commit cf05e8c

Browse files
sa-bucclaude
andcommitted
Add /export session transcript command and refresh roadmap.
Implement a Claude Code-style /export flow for the current session and update the README to document both the export feature and the Open Codex-specific update notification improvements. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent f7914cc commit cf05e8c

8 files changed

Lines changed: 215 additions & 29 deletions

File tree

README.md

Lines changed: 60 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,26 @@ Four new subsystems inspired by Claude Code and oh-my-codex:
8282

8383
This directly addresses roadmap item 3 (better memory mechanics).
8484

85+
### 4. Open Codex-aware update detection and upgrade prompts
86+
87+
From commit `e1e88af89d`:
88+
89+
- switches npm/bun update detection from upstream Codex metadata to the fork package `@leonw24/open-codex`
90+
- updates upgrade commands and release notes links so prompts point to Open Codex instead of `@openai/codex`
91+
- keeps the runtime check aligned with the actual package users install from npm
92+
93+
This is a fork-correctness patch: update notifications should describe the Open Codex release a user can actually install, not the upstream Codex CLI release stream.
94+
95+
### 5. Session export inside the TUI
96+
97+
From recent fork-specific changes:
98+
99+
- adds `/export <path>` for the current session transcript
100+
- supports user-chosen filenames like `/export talk.md` or `/export talk.txt`
101+
- writes a markdown transcript suitable for debugging, archival, and sharing
102+
103+
This brings a Claude Code-style export flow into the TUI without requiring external scripts or manual transcript scraping.
104+
85105
## Maintenance Philosophy
86106

87107
This fork is maintained with a conservative strategy:
@@ -109,30 +129,26 @@ The near-term roadmap is intentionally focused on a few CLI-facing improvements:
109129

110130
Improve the Codex CLI status line so it can surface token throughput directly, instead of only showing coarse task state. The aim is to make model responsiveness easier to judge in real time.
111131

112-
### 2. Session export
132+
### 2. ~~Session export~~ ✅ Completed
113133

114-
Implement a Claude Code-style export flow for the current session, so a user can export the active session record in a reusable format. The goal is to make debugging, sharing, and archival much easier.
134+
Implemented as a Claude Code-style `/export` flow for the current session, with user-defined filenames like `/export talk.txt` or `/export talk.md`. This now covers the debugging, sharing, and archival use case directly inside the TUI.
115135

116136
### 3. ~~Better memory mechanics~~ ✅ Completed
117137

118138
Implemented as the consolidated memory subsystem (see Current Delta section 3 above). Remaining improvements tracked below.
119139

120-
### 5. AutoDream background daemon
121-
122-
Replace the startup-blocking Phase 2 consolidation with a 3-gate background consolidator (time ≥ 24h, ≥ 5 new sessions, no lock), using a 4-phase merge pipeline: Orient → Gather → Consolidate → Prune. Inspired by Claude Code's AutoDream.
123-
124-
### 6. Notepad TUI sidebar
125-
126-
Show notepad sections in the TUI sidebar so users can view and edit memory context without running `/memories` commands.
127-
128-
### 7. Memory versioning
129-
130-
Keep a lightweight changelog of topic edits so agents can reason about what changed and when.
131-
132140
### 4. Better Zellij ergonomics
133141

134142
Continue improving the Codex CLI experience under `zellij`, especially around rendering, layout, contrast, and other interaction details that behave differently from plain terminal sessions or `tmux`.
135143

144+
### Next focus areas
145+
146+
- **Background AutoDream-style consolidation** — move consolidation fully off the startup path and replace it with a background 3-gate consolidator (time ≥ 24h, ≥ 5 new sessions, no lock), using a 4-phase pipeline: Orient → Gather → Consolidate → Prune.
147+
- **Notepad TUI sidebar** — surface notepad sections directly in the TUI sidebar so memory context can be viewed and edited without routing everything through `/memories`.
148+
- **Memory versioning** — keep a lightweight changelog for topic edits so agents can reason about what changed and when.
149+
- **More proactive subagent parallel planning** — let the agent split work and dispatch parallel subagents more aggressively instead of stepping through tasks strictly serially.
150+
- **Claude Code-style background execution** — automatically send long-running commands and agent work to the background rather than keeping the main process occupied by foreground waiting and polling.
151+
136152
## Community
137153

138154
Issues and pull requests are welcome in this fork.
@@ -264,6 +280,26 @@ Codex CLI 是开源的,但上游仓库当前对外部代码贡献采用 invita
264280

265281
直接完成了路线图第 3 项(更好的 memory 机制)。
266282

283+
### 4. 面向 Open Codex 的版本检测与升级提示
284+
285+
来自 commit `e1e88af89d`
286+
287+
- 把 npm/bun 更新检测从 upstream Codex 元数据切换到 fork 的 `@leonw24/open-codex`
288+
- 更新升级命令与 release notes 链接,使提示指向 Open Codex,而不是 `@openai/codex`
289+
- 让运行时版本提醒与用户实际通过 npm 安装的包保持一致
290+
291+
这是一个 fork 正确性修复:版本提醒应该描述用户真正能安装的 Open Codex 版本,而不是 upstream Codex CLI 的发布流。
292+
293+
### 5. TUI 内建 session 导出
294+
295+
来自最近几条 fork 自有改动:
296+
297+
- 为当前 session 增加 `/export <path>`
298+
- 支持用户自定义文件名,例如 `/export talk.md``/export talk.txt`
299+
- 导出 markdown transcript,便于调试、归档和分享
300+
301+
这让类似 Claude Code 的会话导出能力直接进入 TUI,而不需要额外脚本或手工抓 transcript。
302+
267303
## 维护思路
268304

269305
这个 fork 的维护策略是偏保守的:
@@ -291,30 +327,26 @@ Codex CLI 是开源的,但上游仓库当前对外部代码贡献采用 invita
291327

292328
改进 Codex CLI 的 status line,让它可以直接展示 token 吞吐,而不只是显示比较粗粒度的任务状态,便于更直观判断模型响应效率。
293329

294-
### 2. Session export
330+
### 2. ~~Session export~~ ✅ 已完成
295331

296-
实现类似 Claude Code 的 session 导出能力,让用户可以把当前会话记录导出成可复用格式,方便调试、归档和分享
332+
已实现类似 Claude Code 的 `/export` 会话导出能力,支持用户自定义文件名,例如 `/export talk.txt``/export talk.md`。当前已经覆盖调试、归档、分享这一类核心使用场景
297333

298334
### 3. ~~更好的 memory 机制~~ ✅ 已完成
299335

300336
已实现为合并 memory 子系统(见上方当前差异第 3 项)。后续改进见下方。
301337

302-
### 5. AutoDream 后台守护进程
303-
304-
用 3-gate 后台合并器(时间 ≥ 24h、≥ 5 个新 session、无锁)替代启动时阻塞的 Phase 2 合并,使用 4 阶段合并管线:Orient → Gather → Consolidate → Prune。借鉴 Claude Code 的 AutoDream。
305-
306-
### 6. Notepad TUI 侧边栏
307-
308-
在 TUI 侧边栏展示 notepad 分区,让用户无需运行 `/memories` 命令即可查看和编辑 memory 上下文。
309-
310-
### 7. Memory 版本管理
311-
312-
为 topic 编辑维护轻量级变更日志,让 agent 能推理内容何时发生了什么变化。
313-
314338
### 4. 更好的 Zellij 使用体验
315339

316340
继续针对 `zellij` 下的 Codex CLI 使用体验做优化,包括渲染、布局、对比度,以及其他与普通 terminal 或 `tmux` 表现不同的交互细节。
317341

342+
### 下一阶段重点
343+
344+
- **后台化的 AutoDream 式 consolidation** — 把 consolidation 完整移出启动路径,改为后台 3-gate 合并器(时间 ≥ 24h、≥ 5 个新 session、无锁),并使用 4 阶段管线:Orient → Gather → Consolidate → Prune。
345+
- **Notepad TUI 侧边栏** — 直接在 TUI 侧边栏展示 notepad 分区,让用户无需总是通过 `/memories` 查看和编辑 memory 上下文。
346+
- **Memory 版本管理** — 为 topic 编辑维护轻量级变更日志,让 agent 能推理内容何时发生了什么变化。
347+
- **更主动的 subagent 并行规划** — 让 agent 能更积极地拆分任务并并行派发 subagent,而不是严格串行地一步步推进。
348+
- **Claude Code 风格的后台执行** — 自动把长时间运行的命令和 agent 工作放到后台,而不是长时间占用主进程做前台等待或轮询。
349+
318350
## 社区协作
319351

320352
这个 fork 欢迎 issue 和 pull request。

codex-rs/tui/src/app.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,13 @@ pub(crate) struct App {
572572
pending_plugin_enabled_writes: HashMap<String, Option<bool>>,
573573
}
574574

575+
fn line_to_string(line: &Line<'static>) -> String {
576+
line.spans
577+
.iter()
578+
.map(|span| span.content.as_ref())
579+
.collect::<String>()
580+
}
581+
575582
fn active_turn_not_steerable_turn_error(error: &TypedRequestError) -> Option<AppServerTurnError> {
576583
let TypedRequestError::Server { source, .. } = error else {
577584
return None;
@@ -1169,6 +1176,39 @@ impl App {
11691176
}
11701177
}
11711178

1179+
impl App {
1180+
pub(crate) fn export_transcript_markdown(&self) -> String {
1181+
const EXPORT_WIDTH: u16 = 240;
1182+
1183+
let mut lines: Vec<String> = Vec::new();
1184+
let mut first = true;
1185+
for cell in &self.transcript_cells {
1186+
let transcript_lines = cell.transcript_lines(EXPORT_WIDTH);
1187+
if transcript_lines.is_empty() {
1188+
continue;
1189+
}
1190+
if !cell.is_stream_continuation() && !first {
1191+
lines.push(String::new());
1192+
}
1193+
lines.extend(transcript_lines.iter().map(line_to_string));
1194+
first = false;
1195+
}
1196+
1197+
if let Some(active_lines) = self.chat_widget.active_cell_transcript_lines(EXPORT_WIDTH) {
1198+
if !lines.is_empty() {
1199+
lines.push(String::new());
1200+
}
1201+
lines.extend(active_lines.iter().map(line_to_string));
1202+
}
1203+
1204+
let mut transcript = lines.join("\n");
1205+
if !transcript.ends_with('\n') {
1206+
transcript.push('\n');
1207+
}
1208+
transcript
1209+
}
1210+
}
1211+
11721212
impl Drop for App {
11731213
fn drop(&mut self) {
11741214
if let Err(err) = self.chat_widget.clear_managed_terminal_title() {

codex-rs/tui/src/app/event_dispatch.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,35 @@ impl App {
343343
));
344344
tui.frame_requester().schedule_frame();
345345
}
346+
AppEvent::ExportTranscript { path } => {
347+
let transcript = self.export_transcript_markdown();
348+
let tx = self.app_event_tx.clone();
349+
tokio::spawn(async move {
350+
if let Some(parent) = path.parent()
351+
&& let Err(err) = tokio::fs::create_dir_all(parent).await
352+
{
353+
let _ = tx.send(AppEvent::MemoryCommandResult {
354+
text: format!("Failed to create export directory: {}", err),
355+
is_error: true,
356+
});
357+
return;
358+
}
359+
match tokio::fs::write(&path, transcript).await {
360+
Ok(()) => {
361+
let _ = tx.send(AppEvent::MemoryCommandResult {
362+
text: format!("Exported session to {}.", path.display()),
363+
is_error: false,
364+
});
365+
}
366+
Err(err) => {
367+
let _ = tx.send(AppEvent::MemoryCommandResult {
368+
text: format!("Failed to export session: {}", err),
369+
is_error: true,
370+
});
371+
}
372+
}
373+
});
374+
}
346375
AppEvent::OpenAppLink {
347376
app_id,
348377
title,

codex-rs/tui/src/app/tests.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,26 @@ fn test_absolute_path(path: &str) -> AbsolutePathBuf {
104104
AbsolutePathBuf::try_from(PathBuf::from(path)).expect("absolute test path")
105105
}
106106

107+
#[tokio::test]
108+
async fn export_transcript_markdown_renders_committed_cells() {
109+
let mut app = make_test_app().await;
110+
app.transcript_cells = vec![
111+
Arc::new(UserHistoryCell {
112+
message: "hello".to_string(),
113+
text_elements: Vec::new(),
114+
local_image_paths: Vec::new(),
115+
remote_image_urls: Vec::new(),
116+
}),
117+
Arc::new(PlainHistoryCell::new(vec![Line::from("world")])),
118+
];
119+
120+
let rendered = app.export_transcript_markdown();
121+
122+
assert!(rendered.contains("hello"));
123+
assert!(rendered.contains("world"));
124+
assert!(rendered.ends_with('\n'));
125+
}
126+
107127
#[tokio::test]
108128
async fn handle_mcp_inventory_result_clears_committed_loading_cell() {
109129
let mut app = make_test_app().await;

codex-rs/tui/src/app_event.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,11 @@ pub(crate) enum AppEvent {
248248
/// Result of computing a `/diff` command.
249249
DiffResult(String),
250250

251+
/// Export the current session transcript to the provided path.
252+
ExportTranscript {
253+
path: PathBuf,
254+
},
255+
251256
/// Open the app link view in the bottom pane.
252257
OpenAppLink {
253258
app_id: String,

codex-rs/tui/src/chatwidget/slash_dispatch.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const SIDE_REVIEW_UNAVAILABLE_MESSAGE: &str =
3131
const SIDE_SLASH_COMMAND_UNAVAILABLE_HINT: &str = "Press Esc to return to the main thread first.";
3232
const GOAL_USAGE: &str = "Usage: /goal <objective>";
3333
const GOAL_USAGE_HINT: &str = "Example: /goal improve benchmark coverage";
34+
const EXPORT_USAGE: &str = "Usage: /export <path>";
3435

3536
impl ChatWidget {
3637
/// Dispatch a bare slash command and record its staged local-history entry.
@@ -319,6 +320,9 @@ impl ChatWidget {
319320
SlashCommand::Copy => {
320321
self.copy_last_agent_markdown();
321322
}
323+
SlashCommand::Export => {
324+
self.add_error_message(EXPORT_USAGE.to_string());
325+
}
322326
SlashCommand::Diff => {
323327
self.add_diff_in_progress();
324328
let tx = self.app_event_tx.clone();
@@ -570,6 +574,11 @@ impl ChatWidget {
570574
"verbose" => self.add_mcp_output(McpServerStatusDetail::Full),
571575
_ => self.add_error_message("Usage: /mcp [verbose]".to_string()),
572576
},
577+
SlashCommand::Export if !trimmed.is_empty() => {
578+
self.app_event_tx.send(AppEvent::ExportTranscript {
579+
path: self.config.cwd.join(trimmed).to_path_buf(),
580+
});
581+
}
573582
SlashCommand::Rename if !trimmed.is_empty() => {
574583
if !self.ensure_thread_rename_allowed() {
575584
return;
@@ -1003,6 +1012,7 @@ impl ChatWidget {
10031012
| SlashCommand::DebugConfig
10041013
| SlashCommand::Ps
10051014
| SlashCommand::Stop
1015+
| SlashCommand::Export
10061016
| SlashCommand::MemoryDrop
10071017
| SlashCommand::MemoryUpdate
10081018
| SlashCommand::Mcp

codex-rs/tui/src/chatwidget/tests/slash_commands.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1303,6 +1303,41 @@ async fn slash_copy_state_tracks_plan_item_completion() {
13031303
);
13041304
}
13051305

1306+
#[tokio::test]
1307+
async fn slash_export_with_path_requests_transcript_export() {
1308+
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
1309+
let tempdir = tempdir().unwrap();
1310+
chat.config.cwd = tempdir.path().to_path_buf().abs();
1311+
1312+
submit_composer_text(&mut chat, "/export talk.md");
1313+
1314+
assert_matches!(
1315+
rx.try_recv(),
1316+
Ok(AppEvent::ExportTranscript { path }) if path == tempdir.path().join("talk.md")
1317+
);
1318+
assert_matches!(op_rx.try_recv(), Err(TryRecvError::Empty));
1319+
assert_eq!(recall_latest_after_clearing(&mut chat), "/export talk.md");
1320+
}
1321+
1322+
#[tokio::test]
1323+
async fn slash_export_without_path_shows_usage() {
1324+
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
1325+
1326+
submit_composer_text(&mut chat, "/export");
1327+
1328+
let cells = drain_insert_history(&mut rx);
1329+
let rendered = cells
1330+
.iter()
1331+
.map(|cell| lines_to_single_string(cell))
1332+
.collect::<Vec<_>>()
1333+
.join("\n");
1334+
assert!(
1335+
rendered.contains("Usage: /export <path>"),
1336+
"expected usage message, got: {rendered:?}"
1337+
);
1338+
assert_eq!(recall_latest_after_clearing(&mut chat), "/export");
1339+
}
1340+
13061341
#[tokio::test]
13071342
async fn slash_copy_reports_when_no_agent_response_exists() {
13081343
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;

0 commit comments

Comments
 (0)