Skip to content
Open
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
10 changes: 5 additions & 5 deletions crates/op-editor-core/src/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ pub struct ChatMessage {
pub design_block_expanded_overrides: Vec<Option<bool>>,
/// Per-action-step (subtask card) expanded-state overrides. Missing
/// / `None` entries fall back to the transcript default (expanded
/// only while the step is the active/streaming one).
/// while the step is active or failed, so diagnostics stay visible).
pub action_step_expanded_overrides: Vec<Option<bool>>,
/// True while this (assistant) message's turn streams in.
pub streaming: bool,
Expand Down Expand Up @@ -869,8 +869,8 @@ impl ChatState {
/// Begin a manual retry for the failed subtask row at
/// `activities[source_index]` in message `msg_idx` — the click handler
/// for the progress panel's per-row "Retry" button. Flips that
/// activity's status back to `Running` and clears its stale "Needs
/// attention" detail so the row shows a spinner immediately, then
/// activity's status back to `Running` and clears its stale failure
/// diagnostic so the row shows a spinner immediately, then
/// raises `pending_subtask_retry` for the desktop host to drain.
///
/// No-ops (leaves everything untouched) when the message/activity index
Expand Down Expand Up @@ -1355,7 +1355,7 @@ mod tests {
msg.activities.push(ChatActivity {
id: "hero".into(),
title: "Hero".into(),
detail: Some("Needs attention".into()),
detail: Some("Reason: provider returned no nodes".into()),
status: ChatActivityStatus::Error,
content_offset: Some(0),
});
Expand Down Expand Up @@ -1391,7 +1391,7 @@ mod tests {
msg.activities.push(ChatActivity {
id: "hero".into(),
title: "Hero".into(),
detail: Some("Needs attention".into()),
detail: Some("Reason: provider returned no nodes".into()),
status: ChatActivityStatus::Error,
content_offset: Some(0),
});
Expand Down
18 changes: 17 additions & 1 deletion crates/op-editor-host-core/src/design.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ pub enum DesignDelta {
/// Request from worker to UI to apply one editor mutation or batch boundary.
pub struct DesignCmdReq {
pub op: DesignCmdOp,
/// Page the design turn started on. The desktop applies every command
/// against this page even if the user switches pages while the LLM runs.
pub target_page_id: Option<String>,
pub ack: SyncSender<DesignCmdAck>,
}

Expand Down Expand Up @@ -151,19 +154,32 @@ impl DesignSession {
pub struct RemoteDocSink {
cmd_tx: Sender<DesignCmdReq>,
mirror: EditorState,
target_page_id: Option<String>,
}

impl RemoteDocSink {
pub fn new(cmd_tx: Sender<DesignCmdReq>, initial_state: EditorState) -> Self {
let target_page_id = initial_state
.doc
.pages
.as_ref()
.and_then(|pages| pages.get(initial_state.ui.active_page_index))
.map(|page| page.id.clone())
.or_else(|| Some("0".into()));
Self {
cmd_tx,
mirror: initial_state,
target_page_id,
}
}

fn send_and_wait(&mut self, op: DesignCmdOp) -> bool {
let (ack_tx, ack_rx) = mpsc::sync_channel::<DesignCmdAck>(1);
let req = DesignCmdReq { op, ack: ack_tx };
let req = DesignCmdReq {
op,
target_page_id: self.target_page_id.clone(),
ack: ack_tx,
};
if self.cmd_tx.send(req).is_err() {
return false;
}
Expand Down
2 changes: 2 additions & 0 deletions crates/op-editor-host-core/tests/design.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ fn remote_doc_sink_updates_mirror_on_ack() {

let ui_thread = thread::spawn(move || {
let req = rx.recv().expect("request");
assert_eq!(req.target_page_id.as_deref(), Some("0"));
let mut new_state = initial.clone();
new_state.viewport.zoom = 2.0;
req.ack
Expand Down Expand Up @@ -96,6 +97,7 @@ fn design_session_drains_progress_and_command_requests() {
cmd_tx
.send(DesignCmdReq {
op: DesignCmdOp::Apply(EditorCommand::ClearSelection),
target_page_id: None,
ack: ack_tx,
})
.expect("cmd");
Expand Down
7 changes: 4 additions & 3 deletions crates/op-editor-ui/src/widgets/ai_chat_transcript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,14 +317,15 @@ pub(crate) fn build_item(
.iter()
.flat_map(|line| wrap_units(line, budget.saturating_sub(4)))
.collect();
// Default: expanded only while this step is the active/streaming
// one. A user click records a per-step override (collapse/expand).
// Active work and failures default open: a terminal error must expose
// its concrete diagnostic without making the user discover a hidden
// accordion. A user click still records an explicit override.
let expanded = msg
.action_step_expanded_overrides
.get(i)
.copied()
.flatten()
.unwrap_or(active);
.unwrap_or(active || failed);
let step_h = action_step_height(expanded, details.len());
// `i` only aligns with `msg.activities`' own index when the
// structured (non-interleaved) path built `progress_steps` directly
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ fn failed_subtask_message(retryable: bool) -> ChatMessage {
m.activities.push(op_editor_core::ChatActivity {
id: "hero".into(),
title: "Hero".into(),
detail: Some("Needs attention".into()),
detail: Some("Reason: provider returned no nodes".into()),
status: op_editor_core::ChatActivityStatus::Error,
content_offset: Some(0),
});
Expand Down
2 changes: 1 addition & 1 deletion crates/op-editor-ui/src/widgets/ai_chat_transcript_flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ pub(crate) fn build_activity_flow(
.get(source_index)
.copied()
.flatten()
.unwrap_or(active);
.unwrap_or(active || failed);
let height = action_step_height(expanded, details.len());
// `index` is a direct `msg.activities` index here (unlike the
// legacy thinking-text-derived steps in `build_item`), so the
Expand Down
35 changes: 35 additions & 0 deletions crates/op-editor-ui/src/widgets/ai_chat_transcript_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,41 @@ fn structured_activities_render_as_compact_rows_without_thinking_text() {
assert!(items[0].bubble.is_none());
}

#[test]
fn failed_structured_activity_defaults_open_to_show_its_diagnostic() {
let mut message = ChatMessage::assistant("Finished with issues");
message.activities.push(op_editor_core::ChatActivity {
id: "customer-table".into(),
title: "Customer Table".into(),
detail: Some("Reason: parent_id=dashboard was not found".into()),
status: op_editor_core::ChatActivityStatus::Error,
content_offset: Some(0),
});

let items = build_transcript(
std::slice::from_ref(&message),
body(),
op_editor_core::Locale::EnUs,
);
assert!(items[0].steps[0].failed);
assert!(items[0].steps[0].expanded);
assert_eq!(
items[0].steps[0].details,
vec!["Reason: parent_id=dashboard was not found"]
);

message.action_step_expanded_overrides = vec![Some(false)];
let collapsed = build_transcript(
std::slice::from_ref(&message),
body(),
op_editor_core::Locale::EnUs,
);
assert!(
!collapsed[0].steps[0].expanded,
"an explicit user collapse must still win over the failure default"
);
}

#[test]
fn structured_activities_interleave_with_cli_narration_by_offset() {
let first = "I mapped the screen.";
Expand Down
4 changes: 3 additions & 1 deletion crates/op-host-desktop/src/chat_session_launch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@ pub fn launch_if_pending(
if launch_direct_modify_turn(host, &user_text, current_chat, current_design) {
return true;
}
} else if matches!(classify_intent(&user_text), Intent::Design) {
} else if !op_host_services::chat_intent::is_non_request_text(&user_text)
&& matches!(classify_intent(&user_text), Intent::Design)
{
// Phase 2.3: When the design-agent-loop flag is ON and a built-in
// provider is configured, run the agentic tool-loop with the 14-tool
// design toolset instead of the orchestrator pipeline. Flag OFF falls
Expand Down
54 changes: 50 additions & 4 deletions crates/op-host-desktop/src/design_session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,37 @@ pub fn pump_commands(
let state = host.editor_state_mut();
let mut any_applied = false;
for req in reqs {
let target_page_index =
req.target_page_id
.as_deref()
.and_then(|page_id| match state.doc.pages.as_ref() {
Some(pages) if !pages.is_empty() => pages
.iter()
.position(|page| page.id == page_id)
.or_else(|| {
page_id
.parse::<usize>()
.ok()
.filter(|idx| *idx < pages.len())
}),
_ if page_id == "0" => Some(0),
_ => None,
});
let original_page_index = state.ui.active_page_index;
let original_selection = state.selection.clone();
if let Some(target) = target_page_index {
state.ui.active_page_index = target;
if target != original_page_index {
state.clear_selection();
}
}
let target_available = req.target_page_id.is_none() || target_page_index.is_some();
let target_is_visible =
req.target_page_id.is_none() || target_page_index == Some(original_page_index);
let applied = match req.op {
DesignCmdOp::Apply(cmd) => {
let applied = state.apply(cmd);
if applied {
let applied = target_available && state.apply(cmd);
if applied && target_is_visible {
fit_design_viewport_to_content(state, viewport_width, viewport_height);
}
applied
Expand All @@ -60,7 +87,13 @@ pub fn pump_commands(
// functionally correct, just finer-grained than ideal.
DesignCmdOp::BeginUndoBatch | DesignCmdOp::EndUndoBatch => true,
};
// The worker mirror stays on the design target, while the visible
// editor returns to the page and selection the user is viewing.
let snapshot = state.clone();
if target_page_index.is_some_and(|target| target != original_page_index) {
state.ui.active_page_index = original_page_index;
state.selection = original_selection;
}
let ack = DesignCmdAck {
applied,
new_state: snapshot,
Expand Down Expand Up @@ -293,11 +326,11 @@ fn apply_progress(msg: &mut ChatMessage, progress: &[Progress], locale: Locale)
ChatActivityStatus::Done,
Some(element_count(locale, *node_count)),
),
Progress::SubtaskFailed { id, .. } => update_activity(
Progress::SubtaskFailed { id, error } => update_activity(
msg,
id,
ChatActivityStatus::Error,
Some(op_i18n::translate(locale, "ai.designProgress.detail.needsAttention").into()),
Some(subtask_failure_detail(locale, error)),
),
Progress::SubtaskRetry { id, attempt, .. } => update_activity(
msg,
Expand Down Expand Up @@ -500,6 +533,19 @@ fn element_count(locale: Locale, count: usize) -> String {
op_i18n::translate(locale, key).replace("{{count}}", &count.to_string())
}

fn subtask_failure_detail(locale: Locale, error: &str) -> String {
let compact = error.split_whitespace().collect::<Vec<_>>().join(" ");
if compact.is_empty() {
return op_i18n::translate(locale, "ai.designProgress.detail.noDiagnostic").into();
}
let mut visible: String = compact.chars().take(220).collect();
if compact.chars().count() > 220 {
visible.push('…');
}
op_i18n::translate(locale, "ai.designProgress.detail.failureReason")
.replace("{{reason}}", &visible)
}

fn planned_narration(locale: Locale, count: usize) -> String {
let key = if count == 1 {
"ai.designProgress.narration.plannedOne"
Expand Down
11 changes: 11 additions & 0 deletions crates/op-host-desktop/src/design_session_terminal_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,17 @@ fn companion_chat_disconnect_cannot_drop_validation_completion_or_retry_payload(
.status,
ChatActivityStatus::Error
);
assert_eq!(
message
.activities
.iter()
.find(|activity| activity.id == "sun_arc")
.unwrap()
.detail
.as_deref(),
Some("Reason: self-check failed"),
"the terminal summary must preserve the concrete subtask failure"
);
assert_eq!(message.failed_subtasks.len(), 1);
assert_eq!(message.failed_subtasks[0].subtask_id, "sun_arc");
assert_eq!(
Expand Down
Loading