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
51 changes: 51 additions & 0 deletions executor/src/agents/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -836,6 +836,21 @@ impl CodexRunState {
.unwrap_or("")
.replace('_', "")
.to_ascii_lowercase();
if item_type == "plan" {
if let Some(text) = extract_text(item) {
log_codex_run_state_text(
"completed",
"set_plan",
phase.as_deref(),
params,
item,
&text,
);
self.final_text = text;
self.saw_delta = true;
}
return;
}
if !matches!(item_type.as_str(), "agentmessage" | "message") {
log_codex_run_state_text("completed", "skip_non_message", None, params, item, "");
return;
Expand Down Expand Up @@ -2450,6 +2465,42 @@ mod tests {
);
}

#[test]
fn codex_run_state_uses_completed_plan_as_final_content() {
let mut state = CodexRunState::default();

assert!(state
.handle_message(&json!({
"method": "item/completed",
"params": {
"item": {
"id": "turn-1-plan",
"type": "plan",
"text": "# Plan\n\n- Execute the steps."
}
}
}))
.is_none());

let outcome = state
.handle_message(&json!({
"method": "turn/completed",
"params": {
"turn": {
"status": "completed"
}
}
}))
.expect("turn completion should produce an outcome");

assert_eq!(
outcome,
ExecutionOutcome::Completed {
content: "# Plan\n\n- Execute the steps.".to_owned()
}
);
}

#[test]
fn codex_run_state_routes_item_id_deltas_by_started_phase() {
let mut state = CodexRunState::default();
Expand Down
52 changes: 51 additions & 1 deletion executor/src/runtime_work/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ use crate::{
use super::{
codex_notifications::{codex_notification, debug_ignored_codex_notification},
transcript::{tool_block_from_notification, tool_update_from_notification},
util::{extract_text, now_ms, raw_string_field, reasoning_content, string_field},
util::{
completed_plan_item_text, extract_text, now_ms, raw_string_field, reasoning_content,
string_field,
},
};

pub(crate) fn emit_response_event(
Expand Down Expand Up @@ -182,6 +185,19 @@ impl CodexNotificationEventMapper {
self.agent_message_phases.forget_item(notification.params);
return;
}
if let Some(text) = completed_plan_item_text(notification.params) {
self.reset_process_text();
emit_response_event(
event_tx,
device_id,
"response.output_text.delta",
local_task_id,
request,
json!({"delta": text}),
);
self.agent_message_phases.forget_item(notification.params);
return;
}
emit_tool_done(
event_tx,
device_id,
Expand Down Expand Up @@ -1339,6 +1355,40 @@ mod tests {
assert_eq!(event["payload"]["data"]["delta"], "Done.");
}

#[test]
fn maps_codex_completed_plan_items_to_output_text_delta() {
let (event_tx, mut event_rx) = broadcast::channel(4);
let request = ExecutionRequest {
task_id: 7,
subtask_id: 8,
..ExecutionRequest::default()
};

map_codex_notification(
&Some(event_tx),
"device-1",
"local-1",
&request,
json!({
"method": "item/completed",
"params": {
"item": {
"id": "turn-1-plan",
"type": "plan",
"text": "# Plan\n\n- Inspect the repo."
}
}
}),
);

let event = event_rx.try_recv().expect("event should be emitted");
assert_eq!(event["event"], "response.output_text.delta");
assert_eq!(
event["payload"]["data"]["delta"],
"# Plan\n\n- Inspect the repo."
);
}

#[test]
fn emits_codex_subagent_activity_events() {
let (event_tx, mut event_rx) = broadcast::channel(4);
Expand Down
51 changes: 50 additions & 1 deletion executor/src/runtime_work/runtime_handle_messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ use super::{
store::RuntimeWorkStore,
transcript::{tool_block_from_notification, tool_update_from_notification},
util::{
extract_text, integer_field, now_ms, raw_string_field, reasoning_content, string_field,
completed_plan_item_text, extract_text, integer_field, now_ms, raw_string_field,
reasoning_content, string_field,
},
};

Expand Down Expand Up @@ -158,6 +159,11 @@ impl CodexNotificationCacheMapper {
self.agent_message_phases.forget_item(params);
return;
}
if let Some(text) = completed_plan_item_text(params) {
append_runtime_assistant_content_delta(store, local_task_id, request, text);
self.agent_message_phases.forget_item(params);
return;
}
if let Some((block_id, updates)) = tool_update_from_notification(params) {
update_runtime_assistant_block(
store,
Expand Down Expand Up @@ -1252,6 +1258,49 @@ mod tests {
let _ = fs::remove_file(index_path);
}

#[test]
fn cache_codex_notification_appends_completed_plan_item_as_content() {
let index_path = temp_index_path("completed-plan-item");
let store = RuntimeWorkStore::new(index_path.clone());
let local_task_id = "runtime-cache";
store.upsert_task(RuntimeTaskLink::new_pending(
local_task_id.to_owned(),
"/tmp/project".to_owned(),
"Runtime cache".to_owned(),
));
let request = ExecutionRequest {
task_id: 1,
subtask_id: 42,
..ExecutionRequest::default()
};

cache_codex_notification(
&store,
local_task_id,
&request,
&json!({
"method": "item/completed",
"params": {
"item": {
"id": "turn-1-plan",
"type": "plan",
"text": "# Plan\n\n- Inspect the repo."
}
}
}),
);

let link = store
.get_task(local_task_id)
.expect("runtime task should exist");
let messages = cached_messages(&link);
assert_eq!(messages.len(), 1);
assert_eq!(messages[0]["content"], "# Plan\n\n- Inspect the repo.");
assert_eq!(messages[0]["blocks"].as_array().map(Vec::len), Some(0));

let _ = fs::remove_file(index_path);
}

#[test]
fn cache_codex_notification_ignores_subagent_agent_message_deltas() {
let index_path = temp_index_path("subagent-agent-delta");
Expand Down
9 changes: 9 additions & 0 deletions executor/src/runtime_work/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,15 @@ pub(crate) fn extract_text(item: &Value) -> Option<String> {
}
}

pub(crate) fn completed_plan_item_text(params: &Value) -> Option<String> {
let item = params.get("item").unwrap_or(params);
if item_type(item).as_str() != "plan" {
return None;
}

extract_text(item)
}

pub(crate) fn reasoning_content(item: &Value) -> Option<String> {
if let Some(summary) = item.get("summary").and_then(Value::as_array) {
let text = summary
Expand Down
9 changes: 8 additions & 1 deletion wework/src/components/chat/AssistantMarkdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@ const ASSISTANT_MARKDOWN_LINK_CLASS = [
'dark:text-blue-300 dark:hover:text-blue-200',
'[&_code]:!rounded-none [&_code]:!bg-transparent [&_code]:!px-0 [&_code]:!py-0 [&_code]:!font-[inherit] [&_code]:!text-inherit',
].join(' ')
const CODEX_PLAN_TAG_PATTERN = /<\/?\s*proposed_plan\s*>/gi

interface AssistantMarkdownProps {
content: string
onOpenFile?: (path: string) => void
}

export function AssistantMarkdown({ content, onOpenFile }: AssistantMarkdownProps) {
const displayContent = normalizeAssistantMarkdownContent(content)

return (
<div className="assistant-markdown min-w-0 overflow-x-hidden break-words">
<ReactMarkdown
Expand Down Expand Up @@ -98,12 +101,16 @@ export function AssistantMarkdown({ content, onOpenFile }: AssistantMarkdownProp
img: ({ src, alt }) => <AssistantMarkdownImage src={src} alt={alt} />,
}}
>
{content}
{displayContent}
</ReactMarkdown>
</div>
)
}

function normalizeAssistantMarkdownContent(content: string): string {
return content.replace(CODEX_PLAN_TAG_PATTERN, '')
}

function formatMarkdownLineLabel(target: Extract<MarkdownLinkTarget, { kind: 'file' }>): string {
if (typeof target.lineStart !== 'number') return ''
if (typeof target.lineEnd === 'number' && target.lineEnd !== target.lineStart) {
Expand Down
9 changes: 2 additions & 7 deletions wework/src/components/chat/FileChangesReviewPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ describe('FileChangesReviewPanel', () => {
expect(screen.getByText(/This diff is large|此差异较大/)).toBeInTheDocument()
expect(screen.getByTestId('file-changes-review-toolbar')).toHaveTextContent('上轮对话')
expect(screen.queryByTestId('file-changes-review-file-tree')).not.toBeInTheDocument()
expect(screen.queryByTestId('toggle-file-tree-button')).not.toBeInTheDocument()

const diff = screen.getByTestId('file-changes-review-diff')
await waitFor(() => {
Expand All @@ -164,9 +165,6 @@ describe('FileChangesReviewPanel', () => {
expect(getRenderedDiffText()).not.toContain('new 2')
})

await userEvent.click(screen.getByTestId('toggle-file-tree-button'))
expect(screen.getByTestId('pierre-file-tree')).toBeInTheDocument()

rerender(
<FileChangesReviewPanel
loading={false}
Expand Down Expand Up @@ -259,10 +257,7 @@ describe('FileChangesReviewPanel', () => {

await userEvent.click(screen.getByTestId('refresh-review-diff-button'))
expect(onRefresh).toHaveBeenCalledTimes(1)

await userEvent.click(screen.getByTestId('toggle-file-tree-button'))
expect(screen.queryByTestId('file-changes-review-file-tree')).not.toBeInTheDocument()
await userEvent.click(screen.getByTestId('toggle-file-tree-button'))
expect(screen.queryByTestId('toggle-file-tree-button')).not.toBeInTheDocument()
expect(screen.getByTestId('file-changes-review-file-tree')).toBeInTheDocument()

await userEvent.click(screen.getByTestId('toggle-line-wrap-button'))
Expand Down
36 changes: 1 addition & 35 deletions wework/src/components/chat/FileChangesReviewPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import {
GitBranch,
GitCompareArrows,
ListCollapse,
PanelRightClose,
PanelRightOpen,
RefreshCw,
Search,
WrapText,
Expand Down Expand Up @@ -178,11 +176,6 @@ export function FileChangesReviewPanel({
focusFilePath?: string
index: number
}>({ diff: '', index: 0 })
const [fileTreeVisibility, setFileTreeVisibility] = useState({
diff,
defaultFileTreeVisible,
visible: defaultFileTreeVisible,
})
const [wrapLines, setWrapLines] = useState(false)
const [hunksCollapsed, setHunksCollapsed] = useState(false)
const [collapsedSections, setCollapsedSections] = useState<Set<string>>(new Set())
Expand All @@ -203,11 +196,7 @@ export function FileChangesReviewPanel({
const diffStats = useMemo(() => getSectionsDiffStats(sections), [sections])
const isLargeDiff = useMemo(() => isLargeReviewDiff(sections), [sections])
const displayedSections = isLargeDiff && selectedSection ? [selectedSection] : sections
const fileTreeVisible =
fileTreeVisibility.diff === diff &&
fileTreeVisibility.defaultFileTreeVisible === defaultFileTreeVisible
? fileTreeVisibility.visible
: defaultFileTreeVisible
const fileTreeVisible = defaultFileTreeVisible

const selectSection = (index: number) => {
setSelection({ diff, focusFilePath, index })
Expand Down Expand Up @@ -250,18 +239,10 @@ export function FileChangesReviewPanel({
viewOptions={viewOptions}
additions={diffStats.additions}
deletions={diffStats.deletions}
fileTreeVisible={fileTreeVisible}
wrapLines={wrapLines}
hunksCollapsed={hunksCollapsed}
canRefresh={Boolean(onRefresh)}
onRefresh={onRefresh}
onToggleFileTree={() =>
setFileTreeVisibility({
diff,
defaultFileTreeVisible,
visible: !fileTreeVisible,
})
}
onToggleWrap={() => setWrapLines(value => !value)}
onToggleHunks={() => setHunksCollapsed(value => !value)}
onCopyGitApplyCommand={copyGitApplyCommand}
Expand Down Expand Up @@ -315,12 +296,10 @@ function ReviewToolbar({
viewOptions,
additions,
deletions,
fileTreeVisible,
wrapLines,
hunksCollapsed,
canRefresh,
onRefresh,
onToggleFileTree,
onToggleWrap,
onToggleHunks,
onCopyGitApplyCommand,
Expand All @@ -331,12 +310,10 @@ function ReviewToolbar({
viewOptions?: FileChangesReviewViewOption[]
additions: number
deletions: number
fileTreeVisible: boolean
wrapLines: boolean
hunksCollapsed: boolean
canRefresh: boolean
onRefresh?: () => void
onToggleFileTree: () => void
onToggleWrap: () => void
onToggleHunks: () => void
onCopyGitApplyCommand: () => void
Expand Down Expand Up @@ -438,17 +415,6 @@ function ReviewToolbar({
onClick={onCopyGitApplyCommand}
icon={Copy}
/>
<ToolbarButton
testId="toggle-file-tree-button"
label={
fileTreeVisible
? t('file_changes.actions.hide_files')
: t('file_changes.actions.show_files')
}
onClick={onToggleFileTree}
pressed={fileTreeVisible}
icon={fileTreeVisible ? PanelRightClose : PanelRightOpen}
/>
</div>
</div>
{hasBranchContext ? (
Expand Down
Loading
Loading