diff --git a/orbitdock-server/crates/cli/src/commands/review.rs b/orbitdock-server/crates/cli/src/commands/review.rs index 09e467a0..5fcd0e6f 100644 --- a/orbitdock-server/crates/cli/src/commands/review.rs +++ b/orbitdock-server/crates/cli/src/commands/review.rs @@ -55,17 +55,17 @@ pub async fn run(action: &ReviewAction, rest: &RestClient, output: &Output) -> i tag, turn, } => { - create( + create(CreateReviewArgs { rest, output, session_id, file, - *line, - *line_end, + line: *line, + line_end: *line_end, body, - tag.as_ref(), - turn.as_deref(), - ) + tag: tag.as_ref(), + turn: turn.as_deref(), + }) .await } ReviewAction::Update { @@ -126,18 +126,31 @@ async fn list(rest: &RestClient, output: &Output, session_id: &str, turn: Option } } -#[allow(clippy::too_many_arguments)] -async fn create( - rest: &RestClient, - output: &Output, - session_id: &str, - file: &str, +struct CreateReviewArgs<'a> { + rest: &'a RestClient, + output: &'a Output, + session_id: &'a str, + file: &'a str, line: u32, line_end: Option, - body: &str, - tag: Option<&ReviewTagFilter>, - turn: Option<&str>, -) -> i32 { + body: &'a str, + tag: Option<&'a ReviewTagFilter>, + turn: Option<&'a str>, +} + +async fn create(args: CreateReviewArgs<'_>) -> i32 { + let CreateReviewArgs { + rest, + output, + session_id, + file, + line, + line_end, + body, + tag, + turn, + } = args; + let tag_str = tag.map(ReviewTagFilter::as_str); let req = CreateReviewCommentRequest { diff --git a/orbitdock-server/crates/cli/src/commands/session.rs b/orbitdock-server/crates/cli/src/commands/session.rs index 14829c6a..6d6f73bd 100644 --- a/orbitdock-server/crates/cli/src/commands/session.rs +++ b/orbitdock-server/crates/cli/src/commands/session.rs @@ -71,16 +71,16 @@ pub async fn run( .map(|p| p.to_string_lossy().to_string()) .unwrap_or_else(|_| ".".to_string()), }; - create( + create(CreateSessionArgs { config, output, - provider, - &resolved_cwd, - model.as_deref(), - permission_mode.as_ref(), - effort.as_ref(), - system_prompt.as_deref(), - ) + provider_filter: provider, + cwd: &resolved_cwd, + model: model.as_deref(), + permission_mode: permission_mode.as_ref(), + effort: effort.as_ref(), + system_prompt: system_prompt.as_deref(), + }) .await } SessionAction::Send { @@ -296,17 +296,29 @@ async fn get(rest: &RestClient, output: &Output, session_id: &str, messages: boo // ── WS Commands ────────────────────────────────────────────── -#[allow(clippy::too_many_arguments)] -async fn create( - config: &ClientConfig, - output: &Output, - provider_filter: &ProviderFilter, - cwd: &str, - model: Option<&str>, - permission_mode: Option<&PermissionMode>, - effort: Option<&Effort>, - system_prompt: Option<&str>, -) -> i32 { +struct CreateSessionArgs<'a> { + config: &'a ClientConfig, + output: &'a Output, + provider_filter: &'a ProviderFilter, + cwd: &'a str, + model: Option<&'a str>, + permission_mode: Option<&'a PermissionMode>, + effort: Option<&'a Effort>, + system_prompt: Option<&'a str>, +} + +async fn create(args: CreateSessionArgs<'_>) -> i32 { + let CreateSessionArgs { + config, + output, + provider_filter, + cwd, + model, + permission_mode, + effort, + system_prompt, + } = args; + let Some(mut ws) = ws_connect(config, output).await else { return EXIT_CONNECTION_ERROR; }; @@ -383,7 +395,6 @@ async fn create( } } -#[allow(clippy::too_many_arguments)] async fn send_message( config: &ClientConfig, output: &Output, diff --git a/orbitdock-server/crates/cli/src/dev_console.rs b/orbitdock-server/crates/cli/src/dev_console.rs index 5e27c7ea..9288ece1 100644 --- a/orbitdock-server/crates/cli/src/dev_console.rs +++ b/orbitdock-server/crates/cli/src/dev_console.rs @@ -887,7 +887,6 @@ fn open_selected_event_in_pager( pager_result } -#[allow(clippy::needless_return)] fn copy_text_to_clipboard(text: &str) -> anyhow::Result<()> { #[cfg(target_os = "macos")] { diff --git a/orbitdock-server/crates/connector-claude/src/lib.rs b/orbitdock-server/crates/connector-claude/src/lib.rs index 03ba6ed4..a9fb38ac 100644 --- a/orbitdock-server/crates/connector-claude/src/lib.rs +++ b/orbitdock-server/crates/connector-claude/src/lib.rs @@ -276,15 +276,17 @@ fn make_tool_row( let render_hints = tool_render_hints(kind); let tool_display = Some( orbitdock_protocol::conversation_contracts::compute_tool_display( - kind, - family, - status, - tool_name, - subtitle.as_deref(), - None, - None, - raw_input, - None, + orbitdock_protocol::conversation_contracts::ToolDisplayInput { + kind, + family, + status, + title: tool_name, + subtitle: subtitle.as_deref(), + summary: None, + duration_ms: None, + invocation_input: raw_input, + result_output: None, + }, ), ); ToolRow { @@ -589,9 +591,25 @@ impl ClaudeConnector { if allow_bypass_permissions { args.push("--allow-dangerously-skip-permissions"); } - let allowed_joined = allowed_tools.join(","); + + // When permission_mode is "acceptEdits", ensure edit tools are explicitly + // in the allowedTools list. The --permission-prompt-tool stdio flag routes + // ALL permission decisions through the control protocol, so the CLI's + // internal --permission-mode auto-accept logic may not fire. Passing them + // as --allowedTools guarantees the CLI pre-approves them. + let mut effective_allowed: Vec = allowed_tools.to_vec(); + if permission_mode == Some("acceptEdits") { + for tool in ["Edit", "Write", "NotebookEdit"] { + let tool_str = tool.to_string(); + if !effective_allowed.contains(&tool_str) { + effective_allowed.push(tool_str); + } + } + } + + let allowed_joined = effective_allowed.join(","); let disallowed_joined = disallowed_tools.join(","); - if !allowed_tools.is_empty() { + if !effective_allowed.is_empty() { args.extend(["--allowedTools", &allowed_joined]); } if !disallowed_tools.is_empty() { @@ -2316,15 +2334,17 @@ impl ClaudeConnector { }; tr.tool_display = Some( orbitdock_protocol::conversation_contracts::compute_tool_display( - tr.kind, - tr.family, - tr.status, - &tr.title, - tr.subtitle.as_deref(), - tr.summary.as_deref(), - tr.duration_ms, - raw_input, - Some(&content), + orbitdock_protocol::conversation_contracts::ToolDisplayInput { + kind: tr.kind, + family: tr.family, + status: tr.status, + title: &tr.title, + subtitle: tr.subtitle.as_deref(), + summary: tr.summary.as_deref(), + duration_ms: tr.duration_ms, + invocation_input: raw_input, + result_output: Some(&content), + }, ), ); events.push(ConnectorEvent::ConversationRowUpdated { @@ -2467,7 +2487,6 @@ impl ClaudeConnector { } /// Handle `result` messages — turn completed/aborted with usage. - #[allow(clippy::too_many_arguments)] fn handle_result_message( raw: &Value, streaming_content: &mut String, diff --git a/orbitdock-server/crates/connector-codex/src/event_mapping/approvals.rs b/orbitdock-server/crates/connector-codex/src/event_mapping/approvals.rs index 185d8b62..21d06017 100644 --- a/orbitdock-server/crates/connector-codex/src/event_mapping/approvals.rs +++ b/orbitdock-server/crates/connector-codex/src/event_mapping/approvals.rs @@ -7,7 +7,7 @@ use codex_protocol::protocol::{ use codex_protocol::request_permissions::RequestPermissionsEvent; use orbitdock_connector_core::{ApprovalType, ConnectorEvent}; use orbitdock_protocol::conversation_contracts::{ - compute_tool_display, ConversationRow, ConversationRowEntry, ToolRow, + compute_tool_display, ConversationRow, ConversationRowEntry, ToolDisplayInput, ToolRow, }; use orbitdock_protocol::domain_events::{ToolFamily, ToolKind, ToolStatus}; use orbitdock_protocol::Provider; @@ -30,17 +30,17 @@ fn with_display(mut row: ToolRow) -> ToolRow { .as_ref() .and_then(|v| v.get("output").and_then(|o| o.as_str())) .map(String::from); - row.tool_display = Some(compute_tool_display( - row.kind, - row.family, - row.status, - &row.title, - row.subtitle.as_deref(), - row.summary.as_deref(), - row.duration_ms, - invocation_ref, - result_str.as_deref(), - )); + row.tool_display = Some(compute_tool_display(ToolDisplayInput { + kind: row.kind, + family: row.family, + status: row.status, + title: &row.title, + subtitle: row.subtitle.as_deref(), + summary: row.summary.as_deref(), + duration_ms: row.duration_ms, + invocation_input: invocation_ref, + result_output: result_str.as_deref(), + })); row } diff --git a/orbitdock-server/crates/connector-codex/src/event_mapping/collab.rs b/orbitdock-server/crates/connector-codex/src/event_mapping/collab.rs index 43c70083..94693960 100644 --- a/orbitdock-server/crates/connector-codex/src/event_mapping/collab.rs +++ b/orbitdock-server/crates/connector-codex/src/event_mapping/collab.rs @@ -10,7 +10,7 @@ use codex_protocol::protocol::{ }; use orbitdock_connector_core::ConnectorEvent; use orbitdock_protocol::conversation_contracts::{ - compute_tool_display, ConversationRow, ConversationRowEntry, ToolRow, + compute_tool_display, ConversationRow, ConversationRowEntry, ToolDisplayInput, ToolRow, }; use orbitdock_protocol::domain_events::{ToolFamily, ToolKind, ToolStatus}; use orbitdock_protocol::Provider; @@ -32,17 +32,17 @@ fn with_display(mut row: ToolRow) -> ToolRow { .as_ref() .and_then(|v| v.get("output").and_then(|o| o.as_str())) .map(String::from); - row.tool_display = Some(compute_tool_display( - row.kind, - row.family, - row.status, - &row.title, - row.subtitle.as_deref(), - row.summary.as_deref(), - row.duration_ms, - invocation_ref, - result_str.as_deref(), - )); + row.tool_display = Some(compute_tool_display(ToolDisplayInput { + kind: row.kind, + family: row.family, + status: row.status, + title: &row.title, + subtitle: row.subtitle.as_deref(), + summary: row.summary.as_deref(), + duration_ms: row.duration_ms, + invocation_input: invocation_ref, + result_output: result_str.as_deref(), + })); row } diff --git a/orbitdock-server/crates/connector-codex/src/event_mapping/guardian.rs b/orbitdock-server/crates/connector-codex/src/event_mapping/guardian.rs index 443d0219..07f036b7 100644 --- a/orbitdock-server/crates/connector-codex/src/event_mapping/guardian.rs +++ b/orbitdock-server/crates/connector-codex/src/event_mapping/guardian.rs @@ -1,5 +1,7 @@ use orbitdock_connector_core::ConnectorEvent; -use orbitdock_protocol::conversation_contracts::{compute_tool_display, ConversationRow, ToolRow}; +use orbitdock_protocol::conversation_contracts::{ + compute_tool_display, ConversationRow, ToolDisplayInput, ToolRow, +}; use orbitdock_protocol::domain_events::{ GenericInvocationPayload, GenericResultPayload, ToolFamily, ToolInvocationPayload, ToolKind, ToolResultPayload, ToolStatus, @@ -19,17 +21,17 @@ fn with_display(mut row: ToolRow) -> ToolRow { .ok() .filter(|text| !text.trim().is_empty()) }); - row.tool_display = Some(compute_tool_display( - row.kind, - row.family, - row.status, - &row.title, - row.subtitle.as_deref(), - row.summary.as_deref(), - row.duration_ms, - invocation_json.as_ref(), - result_text.as_deref(), - )); + row.tool_display = Some(compute_tool_display(ToolDisplayInput { + kind: row.kind, + family: row.family, + status: row.status, + title: &row.title, + subtitle: row.subtitle.as_deref(), + summary: row.summary.as_deref(), + duration_ms: row.duration_ms, + invocation_input: invocation_json.as_ref(), + result_output: result_text.as_deref(), + })); row } diff --git a/orbitdock-server/crates/connector-codex/src/event_mapping/runtime_signals.rs b/orbitdock-server/crates/connector-codex/src/event_mapping/runtime_signals.rs index aa26ba22..f3a0c93e 100644 --- a/orbitdock-server/crates/connector-codex/src/event_mapping/runtime_signals.rs +++ b/orbitdock-server/crates/connector-codex/src/event_mapping/runtime_signals.rs @@ -14,7 +14,7 @@ use codex_protocol::protocol::{ use orbitdock_connector_core::ConnectorEvent; use orbitdock_protocol::conversation_contracts::{ compute_tool_display, ConversationRow, ConversationRowEntry, HandoffRow, HookRow, - MessageRowContent, ToolRow, + MessageRowContent, ToolDisplayInput, ToolRow, }; use orbitdock_protocol::domain_events::{ HandoffPayload, HookPayload, PlanStepPayload, PlanStepStatus, ToolFamily, ToolKind, ToolStatus, @@ -41,17 +41,17 @@ fn with_display(mut row: ToolRow) -> ToolRow { .as_ref() .and_then(|v| v.get("output").and_then(|o| o.as_str())) .map(String::from); - row.tool_display = Some(compute_tool_display( - row.kind, - row.family, - row.status, - &row.title, - row.subtitle.as_deref(), - row.summary.as_deref(), - row.duration_ms, - invocation_ref, - result_str.as_deref(), - )); + row.tool_display = Some(compute_tool_display(ToolDisplayInput { + kind: row.kind, + family: row.family, + status: row.status, + title: &row.title, + subtitle: row.subtitle.as_deref(), + summary: row.summary.as_deref(), + duration_ms: row.duration_ms, + invocation_input: invocation_ref, + result_output: result_str.as_deref(), + })); row } diff --git a/orbitdock-server/crates/connector-codex/src/event_mapping/streaming.rs b/orbitdock-server/crates/connector-codex/src/event_mapping/streaming.rs index 3d346034..5be474df 100644 --- a/orbitdock-server/crates/connector-codex/src/event_mapping/streaming.rs +++ b/orbitdock-server/crates/connector-codex/src/event_mapping/streaming.rs @@ -15,7 +15,8 @@ use codex_protocol::protocol::{ }; use orbitdock_connector_core::ConnectorEvent; use orbitdock_protocol::conversation_contracts::{ - compute_tool_display, ConversationRow, ConversationRowEntry, MessageRowContent, ToolRow, + compute_tool_display, ConversationRow, ConversationRowEntry, MessageRowContent, + ToolDisplayInput, ToolRow, }; use orbitdock_protocol::domain_events::{ToolFamily, ToolKind, ToolStatus}; use orbitdock_protocol::Provider; @@ -40,17 +41,17 @@ fn with_display(mut row: ToolRow) -> ToolRow { .as_ref() .and_then(|v| v.get("output").and_then(|o| o.as_str())) .map(String::from); - row.tool_display = Some(compute_tool_display( - row.kind, - row.family, - row.status, - &row.title, - row.subtitle.as_deref(), - row.summary.as_deref(), - row.duration_ms, - invocation_ref, - result_str.as_deref(), - )); + row.tool_display = Some(compute_tool_display(ToolDisplayInput { + kind: row.kind, + family: row.family, + status: row.status, + title: &row.title, + subtitle: row.subtitle.as_deref(), + summary: row.summary.as_deref(), + duration_ms: row.duration_ms, + invocation_input: invocation_ref, + result_output: result_str.as_deref(), + })); row } diff --git a/orbitdock-server/crates/connector-codex/src/event_mapping/tools.rs b/orbitdock-server/crates/connector-codex/src/event_mapping/tools.rs index 43208878..1e581367 100644 --- a/orbitdock-server/crates/connector-codex/src/event_mapping/tools.rs +++ b/orbitdock-server/crates/connector-codex/src/event_mapping/tools.rs @@ -11,7 +11,7 @@ use codex_protocol::protocol::{ }; use orbitdock_connector_core::ConnectorEvent; use orbitdock_protocol::conversation_contracts::{ - compute_tool_display, ConversationRow, ConversationRowEntry, ToolRow, + compute_tool_display, ConversationRow, ConversationRowEntry, ToolDisplayInput, ToolRow, }; use orbitdock_protocol::domain_events::{ToolFamily, ToolKind, ToolStatus}; use orbitdock_protocol::Provider; @@ -33,17 +33,17 @@ fn with_display(mut row: ToolRow) -> ToolRow { .as_ref() .and_then(|v| v.get("output").and_then(|o| o.as_str())) .map(String::from); - row.tool_display = Some(compute_tool_display( - row.kind, - row.family, - row.status, - &row.title, - row.subtitle.as_deref(), - row.summary.as_deref(), - row.duration_ms, - invocation_ref, - result_str.as_deref(), - )); + row.tool_display = Some(compute_tool_display(ToolDisplayInput { + kind: row.kind, + family: row.family, + status: row.status, + title: &row.title, + subtitle: row.subtitle.as_deref(), + summary: row.summary.as_deref(), + duration_ms: row.duration_ms, + invocation_input: invocation_ref, + result_output: result_str.as_deref(), + })); row } diff --git a/orbitdock-server/crates/connector-core/src/transition.rs b/orbitdock-server/crates/connector-core/src/transition.rs index e1cf4b21..6c30543e 100644 --- a/orbitdock-server/crates/connector-core/src/transition.rs +++ b/orbitdock-server/crates/connector-core/src/transition.rs @@ -1325,16 +1325,16 @@ pub fn transition( (state, effects) } -struct ApprovalPreviewInput<'a> { - request_id: &'a str, - approval_type: ApprovalType, - tool_name: Option<&'a str>, - tool_input: Option<&'a str>, - command: Option<&'a str>, - file_path: Option<&'a str>, - diff: Option<&'a str>, - question: Option<&'a str>, - permission_reason: Option<&'a str>, +pub struct ApprovalPreviewInput<'a> { + pub request_id: &'a str, + pub approval_type: ApprovalType, + pub tool_name: Option<&'a str>, + pub tool_input: Option<&'a str>, + pub command: Option<&'a str>, + pub file_path: Option<&'a str>, + pub diff: Option<&'a str>, + pub question: Option<&'a str>, + pub permission_reason: Option<&'a str>, } pub fn approval_question_prompts( @@ -1356,29 +1356,8 @@ pub fn approval_question( .or_else(|| trim_non_empty(fallback_question)) } -#[allow(clippy::too_many_arguments)] -pub fn approval_preview( - request_id: &str, - approval_type: ApprovalType, - tool_name: Option<&str>, - tool_input: Option<&str>, - command: Option<&str>, - file_path: Option<&str>, - diff: Option<&str>, - question: Option<&str>, - permission_reason: Option<&str>, -) -> Option { - build_approval_preview(ApprovalPreviewInput { - request_id, - approval_type, - tool_name, - tool_input, - command, - file_path, - diff, - question, - permission_reason, - }) +pub fn approval_preview(input: ApprovalPreviewInput<'_>) -> Option { + build_approval_preview(input) } fn build_approval_preview(input_data: ApprovalPreviewInput<'_>) -> Option { @@ -1446,17 +1425,21 @@ fn build_approval_preview(input_data: ApprovalPreviewInput<'_>) -> Option) -> Option) -> Option) -> Option) -> Option { Some(format!(" {normalized} ")) } -#[allow(clippy::too_many_arguments)] -fn compose_approval_preview( - request_id: &str, +struct ApprovalPreviewContext<'a> { + request_id: &'a str, approval_type: ApprovalType, - tool_name: Option<&str>, - normalized_tool_name: &str, + tool_name: Option<&'a str>, + normalized_tool_name: &'a str, + risk_assessment: &'a ApprovalRiskAssessment, +} + +fn compose_approval_preview( + ctx: &ApprovalPreviewContext<'_>, preview_type: ApprovalPreviewType, value: String, shell_segments: Vec, - risk_assessment: &ApprovalRiskAssessment, ) -> ApprovalPreview { let compact = compact_detail_for_preview( preview_type, value.as_str(), shell_segments.as_slice(), - normalized_tool_name, + ctx.normalized_tool_name, ); let decision_scope = decision_scope_for_preview(preview_type).to_string(); let manifest = build_manifest_for_preview( - request_id, - approval_type, - tool_name, - risk_assessment, + ctx, preview_type, value.as_str(), shell_segments.as_slice(), @@ -1768,8 +1715,8 @@ fn compose_approval_preview( shell_segments, compact, decision_scope: Some(decision_scope), - risk_level: Some(risk_assessment.level), - risk_findings: risk_assessment.findings.clone(), + risk_level: Some(ctx.risk_assessment.level), + risk_findings: ctx.risk_assessment.findings.clone(), manifest: Some(manifest), } } @@ -1791,33 +1738,29 @@ fn decision_scope_for_preview(preview_type: ApprovalPreviewType) -> &'static str } } -#[allow(clippy::too_many_arguments)] fn build_manifest_for_preview( - request_id: &str, - approval_type: ApprovalType, - tool_name: Option<&str>, - risk_assessment: &ApprovalRiskAssessment, + ctx: &ApprovalPreviewContext<'_>, preview_type: ApprovalPreviewType, value: &str, shell_segments: &[ApprovalPreviewSegment], decision_scope: &str, ) -> String { - let resolved_tool = trim_non_empty(tool_name).unwrap_or_else(|| "unknown".to_string()); + let resolved_tool = trim_non_empty(ctx.tool_name).unwrap_or_else(|| "unknown".to_string()); let resolved_request_id = - trim_non_empty(Some(request_id)).unwrap_or_else(|| "unknown".to_string()); + trim_non_empty(Some(ctx.request_id)).unwrap_or_else(|| "unknown".to_string()); let mut lines: Vec = vec![ "APPROVAL MANIFEST".to_string(), format!("request_id: {resolved_request_id}"), - format!("approval_type: {}", approval_type_label(approval_type)), + format!("approval_type: {}", approval_type_label(ctx.approval_type)), format!("tool: {resolved_tool}"), - format!("risk_tier: {}", risk_level_label(risk_assessment.level)), + format!("risk_tier: {}", risk_level_label(ctx.risk_assessment.level)), ]; - if !risk_assessment.findings.is_empty() { + if !ctx.risk_assessment.findings.is_empty() { lines.push("risk_signals:".to_string()); lines.extend( - risk_assessment + ctx.risk_assessment .findings .iter() .map(|finding| format!("- {finding}")), diff --git a/orbitdock-server/crates/protocol/src/conversation_contracts/mod.rs b/orbitdock-server/crates/protocol/src/conversation_contracts/mod.rs index f9edbb9d..c988b14e 100644 --- a/orbitdock-server/crates/protocol/src/conversation_contracts/mod.rs +++ b/orbitdock-server/crates/protocol/src/conversation_contracts/mod.rs @@ -25,7 +25,7 @@ pub use rows::{ pub use tool_display::{ classify_tool_name, compute_diff_display, compute_expanded_output, compute_input_display, compute_tool_display, detect_language, extract_start_line, DiffLine, DiffLineKind, - ToolDiffPreview, ToolDisplay, ToolTodoItem, + ToolDiffPreview, ToolDisplay, ToolDisplayInput, ToolTodoItem, }; pub use tool_payloads::{ToolInvocationPayloadContract, ToolPreview, ToolResultPayloadContract}; pub use workers::WorkerRow; diff --git a/orbitdock-server/crates/protocol/src/conversation_contracts/rows.rs b/orbitdock-server/crates/protocol/src/conversation_contracts/rows.rs index 4b26249a..df1cadc8 100644 --- a/orbitdock-server/crates/protocol/src/conversation_contracts/rows.rs +++ b/orbitdock-server/crates/protocol/src/conversation_contracts/rows.rs @@ -3,7 +3,9 @@ use serde::{Deserialize, Serialize}; use crate::conversation_contracts::activity_groups::{ActivityGroupRow, ActivityGroupRowSummary}; use crate::conversation_contracts::approvals::{ApprovalRow, QuestionRow}; use crate::conversation_contracts::render_hints::RenderHints; -use crate::conversation_contracts::tool_display::{compute_tool_display, ToolDisplay}; +use crate::conversation_contracts::tool_display::{ + compute_tool_display, ToolDisplay, ToolDisplayInput, +}; use crate::conversation_contracts::tool_payloads::{ ToolInvocationPayloadContract, ToolPreview, ToolResultPayloadContract, }; @@ -361,17 +363,17 @@ impl ToolRow { pub fn to_summary(&self) -> ToolRowSummary { let display = self.tool_display.clone().unwrap_or_else(|| { let result_str = self.result.as_ref().and_then(|v| v.as_str()); - compute_tool_display( - self.kind, - self.family, - self.status, - &self.title, - self.subtitle.as_deref(), - self.summary.as_deref(), - self.duration_ms, - Some(&self.invocation), - result_str, - ) + compute_tool_display(ToolDisplayInput { + kind: self.kind, + family: self.family, + status: self.status, + title: &self.title, + subtitle: self.subtitle.as_deref(), + summary: self.summary.as_deref(), + duration_ms: self.duration_ms, + invocation_input: Some(&self.invocation), + result_output: result_str, + }) }); ToolRowSummary { id: self.id.clone(), diff --git a/orbitdock-server/crates/protocol/src/conversation_contracts/tool_display.rs b/orbitdock-server/crates/protocol/src/conversation_contracts/tool_display.rs index 6c6c3a20..af3ae9a7 100644 --- a/orbitdock-server/crates/protocol/src/conversation_contracts/tool_display.rs +++ b/orbitdock-server/crates/protocol/src/conversation_contracts/tool_display.rs @@ -172,22 +172,35 @@ fn joined_preview_text(lines: &[String], fallback: &str, max_chars: usize) -> St // Display computation // --------------------------------------------------------------------------- +/// Input struct for [`compute_tool_display`]. +pub struct ToolDisplayInput<'a> { + pub kind: ToolKind, + pub family: ToolFamily, + pub status: ToolStatus, + pub title: &'a str, + pub subtitle: Option<&'a str>, + pub summary: Option<&'a str>, + pub duration_ms: Option, + pub invocation_input: Option<&'a serde_json::Value>, + pub result_output: Option<&'a str>, +} + /// Compute a `ToolDisplay` from tool metadata. /// /// This is the single source of truth for how tools appear in the client. /// Called when building/updating ToolRows in connectors. -#[allow(clippy::too_many_arguments)] -pub fn compute_tool_display( - kind: ToolKind, - family: ToolFamily, - status: ToolStatus, - title: &str, - subtitle: Option<&str>, - summary: Option<&str>, - duration_ms: Option, - invocation_input: Option<&serde_json::Value>, - result_output: Option<&str>, -) -> ToolDisplay { +pub fn compute_tool_display(input: ToolDisplayInput<'_>) -> ToolDisplay { + let ToolDisplayInput { + kind, + family, + status, + title, + subtitle, + summary, + duration_ms, + invocation_input, + result_output, + } = input; // Unwrap the raw_input wrapper if present — hook data wraps input as // {"raw_input": {"command": "..."}, "tool_name": "Bash"} but extract // functions expect the flat {"command": "..."} shape. @@ -1346,25 +1359,26 @@ pub fn classify_tool_name(name: &str) -> (ToolFamily, ToolKind) { #[cfg(test)] mod tests { - use super::compute_tool_display; + use super::{compute_tool_display, ToolDisplayInput}; use crate::domain_events::{ToolFamily, ToolKind, ToolStatus}; #[test] fn codex_edit_diff_payload_produces_lightweight_preview() { - let display = compute_tool_display( - ToolKind::Edit, - ToolFamily::FileChange, - ToolStatus::Completed, - "Edit", - None, - None, - None, - Some(&serde_json::json!({ - "path": "/tmp/SessionStore+Events.swift", - "diff": "--- /tmp/SessionStore+Events.swift\n+++ /tmp/SessionStore+Events.swift\n@@ -10,2 +10,3 @@\n let keep = true\n+let preview = true\n let done = true" - })), - None, - ); + let input_json = serde_json::json!({ + "path": "/tmp/SessionStore+Events.swift", + "diff": "--- /tmp/SessionStore+Events.swift\n+++ /tmp/SessionStore+Events.swift\n@@ -10,2 +10,3 @@\n let keep = true\n+let preview = true\n let done = true" + }); + let display = compute_tool_display(ToolDisplayInput { + kind: ToolKind::Edit, + family: ToolFamily::FileChange, + status: ToolStatus::Completed, + title: "Edit", + subtitle: None, + summary: None, + duration_ms: None, + invocation_input: Some(&input_json), + result_output: None, + }); let preview = display .diff_preview @@ -1378,20 +1392,21 @@ mod tests { #[test] fn write_content_payload_still_produces_addition_preview() { - let display = compute_tool_display( - ToolKind::Write, - ToolFamily::FileChange, - ToolStatus::Completed, - "Write", - None, - None, - None, - Some(&serde_json::json!({ - "path": "/tmp/example.swift", - "content": "let a = 1\nlet b = 2" - })), - None, - ); + let input_json = serde_json::json!({ + "path": "/tmp/example.swift", + "content": "let a = 1\nlet b = 2" + }); + let display = compute_tool_display(ToolDisplayInput { + kind: ToolKind::Write, + family: ToolFamily::FileChange, + status: ToolStatus::Completed, + title: "Write", + subtitle: None, + summary: None, + duration_ms: None, + invocation_input: Some(&input_json), + result_output: None, + }); let preview = display .diff_preview diff --git a/orbitdock-server/crates/protocol/src/provider_normalization/codex.rs b/orbitdock-server/crates/protocol/src/provider_normalization/codex.rs index 64d57f63..e6220255 100644 --- a/orbitdock-server/crates/protocol/src/provider_normalization/codex.rs +++ b/orbitdock-server/crates/protocol/src/provider_normalization/codex.rs @@ -352,9 +352,9 @@ pub fn normalize_protocol_event( ), }; - normalized_event( - ProviderEventSource::SdkMessage, - raw_event_name, + normalized_event(NormalizedEventParams { + source: ProviderEventSource::SdkMessage, + raw_event_name: raw_event_name.to_string(), domain, action, status, @@ -362,7 +362,7 @@ pub fn normalize_protocol_event( thread_operation, tool_name, correlation, - ) + }) } /// Normalize a raw Codex response item kind. @@ -411,17 +411,17 @@ pub fn normalize_response_item( ), }; - normalized_event( - ProviderEventSource::ResponseItem, - raw_event_name, + normalized_event(NormalizedEventParams { + source: ProviderEventSource::ResponseItem, + raw_event_name: raw_event_name.to_string(), domain, action, status, concept, - None, + thread_operation: None, tool_name, correlation, - ) + }) } /// Normalize a rollout-parser event name from passive Codex watching. @@ -531,9 +531,9 @@ pub fn normalize_rollout_event( ), }; - normalized_event( - ProviderEventSource::Hook, - raw_event_name, + normalized_event(NormalizedEventParams { + source: ProviderEventSource::Hook, + raw_event_name: raw_event_name.to_string(), domain, action, status, @@ -541,13 +541,12 @@ pub fn normalize_rollout_event( thread_operation, tool_name, correlation, - ) + }) } -#[allow(clippy::too_many_arguments)] -fn normalized_event( +struct NormalizedEventParams { source: ProviderEventSource, - raw_event_name: &str, + raw_event_name: String, domain: ProviderEventDomain, action: ProviderEventAction, status: Option, @@ -555,20 +554,22 @@ fn normalized_event( thread_operation: Option, tool_name: Option, correlation: ProviderEventCorrelation, -) -> CodexNormalizedEvent { +} + +fn normalized_event(params: NormalizedEventParams) -> CodexNormalizedEvent { CodexNormalizedEvent { provider: Provider::Codex, - source, - domain, - action, - status, - correlation: correlation.into_some(), + source: params.source, + domain: params.domain, + action: params.action, + status: params.status, + correlation: params.correlation.into_some(), payload: CodexNormalizedPayload { - source_kind: source_kind_for_source(source), - concept, - raw_event_name: raw_event_name.to_string(), - thread_operation, - tool_name, + source_kind: source_kind_for_source(params.source), + concept: params.concept, + raw_event_name: params.raw_event_name, + thread_operation: params.thread_operation, + tool_name: params.tool_name, }, } } diff --git a/orbitdock-server/crates/server/src/admin/hook_forward.rs b/orbitdock-server/crates/server/src/admin/hook_forward.rs index a6ee6d9e..d0c6ee81 100644 --- a/orbitdock-server/crates/server/src/admin/hook_forward.rs +++ b/orbitdock-server/crates/server/src/admin/hook_forward.rs @@ -21,7 +21,7 @@ use crate::infrastructure::{crypto, paths}; const DEFAULT_SERVER_URL: &str = "http://127.0.0.1:4000"; #[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] -#[allow(clippy::enum_variant_names)] + pub enum HookForwardType { #[value(name = "claude-session-start")] SessionStart, diff --git a/orbitdock-server/crates/server/src/connectors/claude_hooks/handler.rs b/orbitdock-server/crates/server/src/connectors/claude_hooks/handler.rs index e7da171c..025e0847 100644 --- a/orbitdock-server/crates/server/src/connectors/claude_hooks/handler.rs +++ b/orbitdock-server/crates/server/src/connectors/claude_hooks/handler.rs @@ -19,7 +19,7 @@ use tracing::warn; use orbitdock_protocol::{ClientMessage, Provider, ServerMessage, SubagentInfo, SubagentStatus}; use crate::domain::sessions::transition::{ - approval_preview, approval_question, approval_question_prompts, + approval_preview, approval_question, approval_question_prompts, ApprovalPreviewInput, }; use crate::infrastructure::persistence::PersistCommand; use crate::runtime::session_commands::SessionCommand; @@ -1026,17 +1026,17 @@ pub async fn handle_hook_message(msg: ClientMessage, state: &Arc, } +struct SessionMetaEvent { + session_id: String, + cwd: String, + model_provider: Option, + originator: String, + source: SessionSource, + started_at: String, + transcript_path: String, + branch: Option, +} + +struct WorkStateUpdate { + session_id: String, + work_status: WorkStatus, + attention_reason: Option, + pending_tool_name: Option>, + pending_tool_input: Option>, + pending_question: Option>, + last_tool: Option>, + last_tool_at: Option>, + status: Option, +} + impl WatcherRuntime { pub(crate) async fn sweep_files(&mut self) -> anyhow::Result<()> { for path in self.tailer.active_candidates(STARTUP_SEED_RECENT_SECS) { @@ -210,16 +233,16 @@ impl WatcherRuntime { transcript_path, branch, } => { - self.handle_session_meta_event( + self.handle_session_meta_event(SessionMetaEvent { session_id, cwd, model_provider, originator, source, started_at, - &transcript_path, + transcript_path, branch, - ) + }) .await; } RolloutEvent::TurnContext { @@ -396,17 +419,17 @@ impl WatcherRuntime { } for (session_id, pending) in coalesced.work_state { - self.update_work_state( - &session_id, - pending.work_status, - pending.attention_reason, - pending.pending_tool_name, - pending.pending_tool_input, - pending.pending_question, - pending.last_tool, - pending.last_tool_at, - None, - ) + self.update_work_state(WorkStateUpdate { + session_id, + work_status: pending.work_status, + attention_reason: pending.attention_reason, + pending_tool_name: pending.pending_tool_name, + pending_tool_input: pending.pending_tool_input, + pending_question: pending.pending_question, + last_tool: pending.last_tool, + last_tool_at: pending.last_tool_at, + status: None, + }) .await; } @@ -467,18 +490,17 @@ impl WatcherRuntime { // ── Server orchestration (kept from original) ──────────────────────── - #[allow(clippy::too_many_arguments)] - async fn handle_session_meta_event( - &mut self, - session_id: String, - cwd: String, - model_provider: Option, - originator: String, - source: SessionSource, - started_at: String, - transcript_path: &str, - branch: Option, - ) { + async fn handle_session_meta_event(&mut self, event: SessionMetaEvent) { + let SessionMetaEvent { + session_id, + cwd, + model_provider, + originator, + source, + started_at, + transcript_path, + branch, + } = event; let is_direct = self.app_state.is_managed_codex_thread(&session_id); let is_direct_in_db = is_direct_thread_owned_async(&session_id) .await @@ -552,7 +574,7 @@ impl WatcherRuntime { if !exists { let mut handle = SessionHandle::new(session_id.clone(), Provider::Codex, cwd.clone()); handle.set_codex_integration_mode(Some(CodexIntegrationMode::Passive)); - handle.set_transcript_path(Some(transcript_path.to_string())); + handle.set_transcript_path(Some(transcript_path.clone())); handle.set_project_name(project_name.clone()); handle.set_model(model_provider.clone()); handle.set_started_at(Some(started_at.clone())); @@ -573,7 +595,7 @@ impl WatcherRuntime { .await; actor .send(SessionCommand::SetTranscriptPath { - path: Some(transcript_path.to_string()), + path: Some(transcript_path.clone()), }) .await; actor @@ -622,7 +644,7 @@ impl WatcherRuntime { branch, model: model_provider, context_label: Some(originator), - transcript_path: transcript_path.to_string(), + transcript_path, started_at, }) .await; @@ -935,17 +957,17 @@ impl WatcherRuntime { } } - self.update_work_state( - session_id, - WorkStatus::Working, - Some("none".to_string()), - None, - None, - Some(None), - None, - None, - None, - ) + self.update_work_state(WorkStateUpdate { + session_id: session_id.to_string(), + work_status: WorkStatus::Working, + attention_reason: Some("none".to_string()), + pending_tool_name: None, + pending_tool_input: None, + pending_question: Some(None), + last_tool: None, + last_tool_at: None, + status: None, + }) .await; } @@ -1187,19 +1209,18 @@ impl WatcherRuntime { }); } - #[allow(clippy::too_many_arguments)] - async fn update_work_state( - &mut self, - session_id: &str, - work_status: WorkStatus, - attention_reason: Option, - pending_tool_name: Option>, - pending_tool_input: Option>, - pending_question: Option>, - last_tool: Option>, - last_tool_at: Option>, - status: Option, - ) { + async fn update_work_state(&mut self, update: WorkStateUpdate) { + let WorkStateUpdate { + ref session_id, + work_status, + attention_reason, + pending_tool_name, + pending_tool_input, + pending_question, + last_tool, + last_tool_at, + status, + } = update; if let Some(actor) = self.app_state.get_session(session_id) { actor .send(SessionCommand::SetPendingAttention { diff --git a/orbitdock-server/crates/server/src/domain/mission_control/prompt.rs b/orbitdock-server/crates/server/src/domain/mission_control/prompt.rs index 6c3a1914..5dab62a0 100644 --- a/orbitdock-server/crates/server/src/domain/mission_control/prompt.rs +++ b/orbitdock-server/crates/server/src/domain/mission_control/prompt.rs @@ -1,19 +1,33 @@ use anyhow::{Context, Result}; use liquid::ParserBuilder; +/// Issue metadata passed to the prompt template renderer. +pub struct IssueContext<'a> { + pub issue_id: &'a str, + pub issue_identifier: &'a str, + pub issue_title: &'a str, + pub issue_description: Option<&'a str>, + pub issue_url: Option<&'a str>, + pub issue_state: Option<&'a str>, + pub issue_labels: &'a [String], +} + /// Render a Liquid prompt template with issue context. -#[allow(clippy::too_many_arguments)] pub fn render_prompt( template_source: &str, - issue_id: &str, - issue_identifier: &str, - issue_title: &str, - issue_description: Option<&str>, - issue_url: Option<&str>, - issue_state: Option<&str>, - issue_labels: &[String], + issue: &IssueContext<'_>, attempt: u32, ) -> Result { + let IssueContext { + issue_id, + issue_identifier, + issue_title, + issue_description, + issue_url, + issue_state, + issue_labels, + } = issue; + let parser = ParserBuilder::with_stdlib() .build() .context("build Liquid parser")?; @@ -25,9 +39,9 @@ pub fn render_prompt( let globals = liquid::object!({ "issue": { - "id": issue_id, - "identifier": issue_identifier, - "title": issue_title, + "id": *issue_id, + "identifier": *issue_identifier, + "title": *issue_title, "description": issue_description.unwrap_or(""), "url": issue_url.unwrap_or(""), "state": issue_state.unwrap_or(""), @@ -46,22 +60,30 @@ pub fn render_prompt( mod tests { use super::*; + fn default_issue<'a>() -> IssueContext<'a> { + IssueContext { + issue_id: "id-1", + issue_identifier: "PROJ-1", + issue_title: "Bug", + issue_description: None, + issue_url: None, + issue_state: None, + issue_labels: &[], + } + } + #[test] fn render_basic_template() { let template = "Fix issue {{ issue.identifier }}: {{ issue.title }}\n\n{{ issue.description }}"; - let result = render_prompt( - template, - "id-123", - "PROJ-42", - "Login broken", - Some("Users can't log in with Google OAuth"), - None, - None, - &[], - 1, - ) - .unwrap(); + let issue = IssueContext { + issue_id: "id-123", + issue_identifier: "PROJ-42", + issue_title: "Login broken", + issue_description: Some("Users can't log in with Google OAuth"), + ..default_issue() + }; + let result = render_prompt(template, &issue, 1).unwrap(); assert!(result.contains("PROJ-42")); assert!(result.contains("Login broken")); @@ -71,34 +93,26 @@ mod tests { #[test] fn render_with_attempt() { let template = "{% if attempt > 1 %}Retry attempt {{ attempt }}. {% endif %}Fix {{ issue.identifier }}"; - let result = - render_prompt(template, "id-1", "PROJ-1", "Bug", None, None, None, &[], 3).unwrap(); + let result = render_prompt(template, &default_issue(), 3).unwrap(); assert!(result.contains("Retry attempt 3")); } #[test] fn render_empty_description() { let template = "{{ issue.description }}"; - let result = - render_prompt(template, "id-1", "PROJ-1", "Bug", None, None, None, &[], 1).unwrap(); + let result = render_prompt(template, &default_issue(), 1).unwrap(); assert_eq!(result.trim(), ""); } #[test] fn render_url_and_state() { let template = "URL: {{ issue.url }} | State: {{ issue.state }}"; - let result = render_prompt( - template, - "id-1", - "PROJ-1", - "Bug", - None, - Some("https://linear.app/team/PROJ-1"), - Some("In Progress"), - &[], - 1, - ) - .unwrap(); + let issue = IssueContext { + issue_url: Some("https://linear.app/team/PROJ-1"), + issue_state: Some("In Progress"), + ..default_issue() + }; + let result = render_prompt(template, &issue, 1).unwrap(); assert!(result.contains("https://linear.app/team/PROJ-1")); assert!(result.contains("In Progress")); diff --git a/orbitdock-server/crates/server/src/domain/sessions/session.rs b/orbitdock-server/crates/server/src/domain/sessions/session.rs index 66aa5928..70ab5649 100644 --- a/orbitdock-server/crates/server/src/domain/sessions/session.rs +++ b/orbitdock-server/crates/server/src/domain/sessions/session.rs @@ -23,7 +23,9 @@ use tracing::info; use orbitdock_protocol::ServerMessage; use crate::domain::sessions::conversation::{ConversationBootstrap, ConversationPage}; -use crate::domain::sessions::transition::{approval_preview, TransitionState, WorkPhase}; +use crate::domain::sessions::transition::{ + approval_preview, ApprovalPreviewInput, TransitionState, WorkPhase, +}; /// Events that matter for the session list sidebar (status, mode, name changes). /// Per-message events (streaming deltas, message appends) are excluded to avoid @@ -281,17 +283,17 @@ fn preview_for_pending_approval( .map(str::trim) .filter(|value| !value.is_empty()) .unwrap_or("pending-approval"); - approval_preview( + approval_preview(ApprovalPreviewInput { request_id, approval_type, tool_name, tool_input, - None, - None, - None, + command: None, + file_path: None, + diff: None, question, - None, - ) + permission_reason: None, + }) } fn pending_tool_family_from_state( diff --git a/orbitdock-server/crates/server/src/infrastructure/persistence/mission_control.rs b/orbitdock-server/crates/server/src/infrastructure/persistence/mission_control.rs index 906caf3f..56146288 100644 --- a/orbitdock-server/crates/server/src/infrastructure/persistence/mission_control.rs +++ b/orbitdock-server/crates/server/src/infrastructure/persistence/mission_control.rs @@ -246,20 +246,34 @@ pub fn load_retry_ready_issues( Ok(rows) } +/// Fields to update on a mission issue's orchestration state. +/// `None` means "don't touch this field"; `Some(None)` (for nullable fields) means "set to NULL". +pub struct MissionIssueStateUpdate<'a> { + pub orchestration_state: &'a str, + pub session_id: Option<&'a str>, + pub attempt: Option, + pub last_error: Option>, + pub started_at: Option>, + pub completed_at: Option>, +} + /// Synchronously update a mission issue's orchestration state and optional fields. /// Used by dispatch paths that need the write to be visible before broadcasting. -#[allow(clippy::too_many_arguments)] pub fn update_mission_issue_state_sync( conn: &Connection, mission_id: &str, issue_id: &str, - orchestration_state: &str, - session_id: Option<&str>, - attempt: Option, - last_error: Option>, - started_at: Option>, - completed_at: Option>, + update: &MissionIssueStateUpdate<'_>, ) -> Result<()> { + let MissionIssueStateUpdate { + orchestration_state, + session_id, + attempt, + last_error, + started_at, + completed_at, + } = update; + let mut set_parts = vec![String::from("orchestration_state = ?1")]; let mut values: Vec> = vec![Box::new(orchestration_state.to_string())]; @@ -277,7 +291,7 @@ pub fn update_mission_issue_state_sync( push_field!("session_id", sid.to_string()); } if let Some(a) = attempt { - push_field!("attempt", a); + push_field!("attempt", *a); } if let Some(err) = last_error { push_field!("last_error", err.map(|s| s.to_string())); @@ -655,12 +669,14 @@ mod tests { &conn, "m1", "iss-1", - "running", - Some("session-abc"), - Some(1), - None, - Some(Some("2026-03-01T01:00:00.000Z")), - None, + &MissionIssueStateUpdate { + orchestration_state: "running", + session_id: Some("session-abc"), + attempt: Some(1), + last_error: None, + started_at: Some(Some("2026-03-01T01:00:00.000Z")), + completed_at: None, + }, ) .unwrap(); @@ -695,12 +711,14 @@ mod tests { &conn, "m1", "iss-1", - "failed", - None, - None, - Some(Some("something went wrong")), - None, - Some(Some("2026-03-01T02:00:00.000Z")), + &MissionIssueStateUpdate { + orchestration_state: "failed", + session_id: None, + attempt: None, + last_error: Some(Some("something went wrong")), + started_at: None, + completed_at: Some(Some("2026-03-01T02:00:00.000Z")), + }, ) .unwrap(); @@ -734,12 +752,14 @@ mod tests { &conn, "m1", "iss-1", - "retry_queued", - None, - None, - Some(None), // clear last_error - None, - None, + &MissionIssueStateUpdate { + orchestration_state: "retry_queued", + session_id: None, + attempt: None, + last_error: Some(None), // clear last_error + started_at: None, + completed_at: None, + }, ) .unwrap(); @@ -770,12 +790,14 @@ mod tests { &conn, "m1", "iss-1", - "completed", - None, - None, - None, - None, - None, + &MissionIssueStateUpdate { + orchestration_state: "completed", + session_id: None, + attempt: None, + last_error: None, + started_at: None, + completed_at: None, + }, ) .unwrap(); diff --git a/orbitdock-server/crates/server/src/infrastructure/persistence/mod.rs b/orbitdock-server/crates/server/src/infrastructure/persistence/mod.rs index 2fa804fd..a9617a2f 100644 --- a/orbitdock-server/crates/server/src/infrastructure/persistence/mod.rs +++ b/orbitdock-server/crates/server/src/infrastructure/persistence/mod.rs @@ -59,7 +59,9 @@ pub(crate) use transcripts::{ load_messages_from_transcript_path, load_token_usage_from_transcript_path, TranscriptCapabilities, }; -use usage::{persist_usage_event, upsert_usage_session_state, upsert_usage_turn_snapshot}; +use usage::{ + persist_usage_event, upsert_usage_session_state, upsert_usage_turn_snapshot, TurnSnapshotRow, +}; #[allow(unused_imports)] pub(crate) use worktrees::WorktreeRow; pub(crate) use worktrees::{ @@ -569,14 +571,16 @@ pub(super) fn execute_command( upsert_usage_turn_snapshot( conn, - &session_id, - &turn_id, - turn_seq, - input_tokens, - output_tokens, - cached_tokens, - context_window, - snapshot_kind, + &TurnSnapshotRow { + session_id: &session_id, + turn_id: &turn_id, + turn_seq, + input_tokens, + output_tokens, + cached_tokens, + context_window, + snapshot_kind, + }, )?; } diff --git a/orbitdock-server/crates/server/src/infrastructure/persistence/session_reads.rs b/orbitdock-server/crates/server/src/infrastructure/persistence/session_reads.rs index f2a361a3..b89d4c45 100644 --- a/orbitdock-server/crates/server/src/infrastructure/persistence/session_reads.rs +++ b/orbitdock-server/crates/server/src/infrastructure/persistence/session_reads.rs @@ -23,6 +23,36 @@ type StoredCodexConfigRow = ( Option, ); +/// Intermediate row from the active-sessions SQL query. +struct ActiveSessionRow { + id: String, + provider: String, + status: String, + work_status: String, + project_path: String, + transcript_path: Option, + project_name: Option, + model: Option, + custom_name: Option, + first_prompt: Option, + summary: Option, + codex_integration_mode: Option, + codex_thread_id: Option, + started_at: Option, + last_activity_at: Option, + approval_policy: Option, + sandbox_mode: Option, + permission_mode: Option, + pending_tool_name: Option, + pending_tool_input: Option, + pending_question: Option, + input_tokens: i64, + output_tokens: i64, + cached_tokens: i64, + context_window: i64, + token_usage_snapshot_kind_str: String, +} + /// A session restored from the database on startup. #[derive(Debug)] pub struct RestoredSession { @@ -212,99 +242,71 @@ pub async fn load_sessions_for_startup() -> Result, anyhow: datetime(s.started_at) DESC", )?; - #[allow(clippy::type_complexity)] - let session_rows: Vec<( - String, - String, - String, - String, - String, - Option, - Option, - Option, - Option, - Option, - Option, - Option, - Option, - Option, - Option, - Option, - Option, - Option, - Option, - Option, - Option, - i64, - i64, - i64, - i64, - String, - )> = stmt + let session_rows: Vec = stmt .query_map([], |row| { - Ok(( - row.get(0)?, - row.get(1)?, - row.get(2)?, - row.get(3)?, - row.get(4)?, - row.get(5)?, - row.get(6)?, - row.get(7)?, - row.get(8)?, - row.get(9)?, - row.get(10)?, - row.get(11)?, - row.get(12)?, - row.get(13)?, - row.get(14)?, - row.get(15)?, - row.get(16)?, - row.get(17)?, - row.get(18)?, - row.get(19)?, - row.get(20)?, - row.get(21)?, - row.get(22)?, - row.get(23)?, - row.get(24)?, - row.get(25)?, - )) + Ok(ActiveSessionRow { + id: row.get(0)?, + provider: row.get(1)?, + status: row.get(2)?, + work_status: row.get(3)?, + project_path: row.get(4)?, + transcript_path: row.get(5)?, + project_name: row.get(6)?, + model: row.get(7)?, + custom_name: row.get(8)?, + first_prompt: row.get(9)?, + summary: row.get(10)?, + codex_integration_mode: row.get(11)?, + codex_thread_id: row.get(12)?, + started_at: row.get(13)?, + last_activity_at: row.get(14)?, + approval_policy: row.get(15)?, + sandbox_mode: row.get(16)?, + permission_mode: row.get(17)?, + pending_tool_name: row.get(18)?, + pending_tool_input: row.get(19)?, + pending_question: row.get(20)?, + input_tokens: row.get(21)?, + output_tokens: row.get(22)?, + cached_tokens: row.get(23)?, + context_window: row.get(24)?, + token_usage_snapshot_kind_str: row.get(25)?, + }) })? .filter_map(|row| row.ok()) .collect(); let mut sessions = Vec::new(); - for ( - id, - provider, - status, - work_status, - project_path, - transcript_path, - project_name, - model, - custom_name, - first_prompt, - _summary, - codex_integration_mode, - codex_thread_id, - started_at, - last_activity_at, - approval_policy, - sandbox_mode, - permission_mode, - pending_tool_name, - pending_tool_input, - pending_question, - input_tokens, - output_tokens, - cached_tokens, - context_window, - token_usage_snapshot_kind_str, - ) in session_rows - { + for row in session_rows { + let ActiveSessionRow { + id, + provider, + status, + work_status, + project_path, + transcript_path, + project_name, + model, + custom_name, + first_prompt, + summary: _summary, + codex_integration_mode, + codex_thread_id, + started_at, + last_activity_at, + approval_policy, + sandbox_mode, + permission_mode, + pending_tool_name, + pending_tool_input, + pending_question, + input_tokens, + output_tokens, + cached_tokens, + context_window, + token_usage_snapshot_kind_str, + } = row; let token_usage_snapshot_kind = snapshot_kind_from_str(Some(token_usage_snapshot_kind_str.as_str())); diff --git a/orbitdock-server/crates/server/src/infrastructure/persistence/transcripts.rs b/orbitdock-server/crates/server/src/infrastructure/persistence/transcripts.rs index 1a1ee458..591e9dee 100644 --- a/orbitdock-server/crates/server/src/infrastructure/persistence/transcripts.rs +++ b/orbitdock-server/crates/server/src/infrastructure/persistence/transcripts.rs @@ -4,7 +4,7 @@ use std::io::{BufRead, BufReader}; use orbitdock_protocol::conversation_contracts::render_hints::RenderHints; use orbitdock_protocol::conversation_contracts::tool_display::{ - classify_tool_name, compute_tool_display, + classify_tool_name, compute_tool_display, ToolDisplayInput, }; use orbitdock_protocol::conversation_contracts::{ ConversationRow, ConversationRowEntry, MessageRowContent, ToolRow, @@ -247,17 +247,18 @@ pub(crate) fn load_messages_from_transcript( })); // Recompute tool_display with result data let input = tool_row.invocation.get("raw_input"); - tool_row.tool_display = Some(compute_tool_display( - tool_row.kind, - tool_row.family, - ToolStatus::Completed, - &tool_row.title, - None, - None, - None, - input, - Some(&tool_output), - )); + tool_row.tool_display = + Some(compute_tool_display(ToolDisplayInput { + kind: tool_row.kind, + family: tool_row.family, + status: ToolStatus::Completed, + title: &tool_row.title, + subtitle: None, + summary: None, + duration_ms: None, + invocation_input: input, + result_output: Some(&tool_output), + })); continue; } } @@ -270,17 +271,17 @@ pub(crate) fn load_messages_from_transcript( if let Some(id) = &tool_use_id { tool_use_index.insert(id.clone(), row_index); } - let td = Some(compute_tool_display( - ToolKind::Generic, - ToolFamily::Generic, - ToolStatus::Completed, - "Tool", - None, - None, - None, - None, - Some(&tool_output), - )); + let td = Some(compute_tool_display(ToolDisplayInput { + kind: ToolKind::Generic, + family: ToolFamily::Generic, + status: ToolStatus::Completed, + title: "Tool", + subtitle: None, + summary: None, + duration_ms: None, + invocation_input: None, + result_output: Some(&tool_output), + })); rows.push(ConversationRowEntry { session_id: String::new(), sequence, @@ -325,17 +326,17 @@ pub(crate) fn load_messages_from_transcript( if let Some(id) = &tool_use_id { tool_use_index.insert(id.clone(), row_index); } - let td = Some(compute_tool_display( + let td = Some(compute_tool_display(ToolDisplayInput { kind, family, - ToolStatus::Completed, - &tool_name, - None, - None, - None, - tool_input.as_ref(), - None, - )); + status: ToolStatus::Completed, + title: &tool_name, + subtitle: None, + summary: None, + duration_ms: None, + invocation_input: tool_input.as_ref(), + result_output: None, + })); rows.push(ConversationRowEntry { session_id: String::new(), sequence, diff --git a/orbitdock-server/crates/server/src/infrastructure/persistence/usage.rs b/orbitdock-server/crates/server/src/infrastructure/persistence/usage.rs index 9b5e6b4f..1be3a840 100644 --- a/orbitdock-server/crates/server/src/infrastructure/persistence/usage.rs +++ b/orbitdock-server/crates/server/src/infrastructure/persistence/usage.rs @@ -204,18 +204,32 @@ pub(super) fn upsert_usage_session_state( Ok(()) } -#[allow(clippy::too_many_arguments)] +pub(super) struct TurnSnapshotRow<'a> { + pub session_id: &'a str, + pub turn_id: &'a str, + pub turn_seq: u64, + pub input_tokens: u64, + pub output_tokens: u64, + pub cached_tokens: u64, + pub context_window: u64, + pub snapshot_kind: TokenUsageSnapshotKind, +} + pub(super) fn upsert_usage_turn_snapshot( conn: &Connection, - session_id: &str, - turn_id: &str, - turn_seq: u64, - input_tokens: u64, - output_tokens: u64, - cached_tokens: u64, - context_window: u64, - snapshot_kind: TokenUsageSnapshotKind, + row: &TurnSnapshotRow<'_>, ) -> Result<(), rusqlite::Error> { + let TurnSnapshotRow { + session_id, + turn_id, + turn_seq, + input_tokens, + output_tokens, + cached_tokens, + context_window, + snapshot_kind, + } = row; + let previous_input: i64 = conn .query_row( "SELECT input_tokens @@ -229,7 +243,7 @@ pub(super) fn upsert_usage_turn_snapshot( .optional()? .unwrap_or(0); - let input_tokens_i64 = input_tokens as i64; + let input_tokens_i64 = *input_tokens as i64; let input_delta_tokens = (input_tokens_i64 - previous_input).max(0); conn.execute( @@ -257,12 +271,12 @@ pub(super) fn upsert_usage_turn_snapshot( params![ session_id, turn_id, - turn_seq as i64, - snapshot_kind_to_str(snapshot_kind), - input_tokens as i64, - output_tokens as i64, - cached_tokens as i64, - context_window as i64, + *turn_seq as i64, + snapshot_kind_to_str(*snapshot_kind), + *input_tokens as i64, + *output_tokens as i64, + *cached_tokens as i64, + *context_window as i64, input_delta_tokens, chrono_now(), ], diff --git a/orbitdock-server/crates/server/src/runtime/mission_dispatch.rs b/orbitdock-server/crates/server/src/runtime/mission_dispatch.rs index 798bdb3c..ae4abbc0 100644 --- a/orbitdock-server/crates/server/src/runtime/mission_dispatch.rs +++ b/orbitdock-server/crates/server/src/runtime/mission_dispatch.rs @@ -8,9 +8,11 @@ use tracing::{info, warn}; use crate::connectors::claude_session::ClaudeAction; use crate::connectors::codex_session::CodexAction; use crate::domain::mission_control::config::AgentConfig; -use crate::domain::mission_control::prompt::render_prompt; +use crate::domain::mission_control::prompt::{render_prompt, IssueContext}; use crate::domain::mission_control::tracker::{Tracker, TrackerIssue}; -use crate::infrastructure::persistence::mission_control::update_mission_issue_state_sync; +use crate::infrastructure::persistence::mission_control::{ + update_mission_issue_state_sync, MissionIssueStateUpdate, +}; use crate::runtime::session_creation::{ launch_prepared_direct_session, prepare_persist_direct_session, DirectSessionRequest, }; @@ -63,12 +65,14 @@ pub async fn dispatch_issue( &conn, &mid, &iid, - "claimed", - None, - None, - Some(None), - Some(Some(&now)), - None, + &MissionIssueStateUpdate { + orchestration_state: "claimed", + session_id: None, + attempt: None, + last_error: Some(None), + started_at: Some(Some(&now)), + completed_at: None, + }, ) .ok() }) @@ -122,12 +126,14 @@ pub async fn dispatch_issue( &conn, &mid, &iid, - "failed", - None, - Some(attempt), - Some(Some(&err_msg)), - None, - Some(Some(&now)), + &MissionIssueStateUpdate { + orchestration_state: "failed", + session_id: None, + attempt: Some(attempt), + last_error: Some(Some(&err_msg)), + started_at: None, + completed_at: Some(Some(&now)), + }, ) .ok() }) @@ -183,17 +189,16 @@ pub async fn dispatch_issue( } // Render prompt - let prompt = render_prompt( - &ctx.prompt_template, - &issue.id, - &issue.identifier, - &issue.title, - issue.description.as_deref(), - issue.url.as_deref(), - Some(&issue.state), - &issue.labels, - attempt, - )?; + let issue_ctx = IssueContext { + issue_id: &issue.id, + issue_identifier: &issue.identifier, + issue_title: &issue.title, + issue_description: issue.description.as_deref(), + issue_url: issue.url.as_deref(), + issue_state: Some(&issue.state), + issue_labels: &issue.labels, + }; + let prompt = render_prompt(&ctx.prompt_template, &issue_ctx, attempt)?; // Create session let provider: Provider = match provider_str.parse() { @@ -218,12 +223,14 @@ pub async fn dispatch_issue( &conn, &mid, &iid, - "failed", - None, - Some(attempt), - Some(Some(&err_msg)), - None, - Some(Some(&now)), + &MissionIssueStateUpdate { + orchestration_state: "failed", + session_id: None, + attempt: Some(attempt), + last_error: Some(Some(&err_msg)), + started_at: None, + completed_at: Some(Some(&now)), + }, ) .ok() }) @@ -299,12 +306,14 @@ pub async fn dispatch_issue( &conn, &mid, &iid, - "running", - Some(&sid), - Some(attempt), - Some(None), - None, - None, + &MissionIssueStateUpdate { + orchestration_state: "running", + session_id: Some(&sid), + attempt: Some(attempt), + last_error: Some(None), + started_at: None, + completed_at: None, + }, ) .ok() }) diff --git a/orbitdock-server/crates/server/src/runtime/session_command_handler.rs b/orbitdock-server/crates/server/src/runtime/session_command_handler.rs index 813904b0..dd9895d5 100644 --- a/orbitdock-server/crates/server/src/runtime/session_command_handler.rs +++ b/orbitdock-server/crates/server/src/runtime/session_command_handler.rs @@ -7,7 +7,9 @@ use std::time::Duration; use orbitdock_connector_core::ConnectorEvent; -use orbitdock_protocol::conversation_contracts::{compute_tool_display, ConversationRow}; +use orbitdock_protocol::conversation_contracts::{ + compute_tool_display, ConversationRow, ToolDisplayInput, +}; use orbitdock_protocol::domain_events::ToolKind; use orbitdock_protocol::{ CodexIntegrationMode, Provider, ServerMessage, SessionListItem, SessionStatus, StateChanges, @@ -503,17 +505,17 @@ pub async fn handle_session_command( } else { None }; - tool.tool_display = Some(compute_tool_display( - tool.kind, - tool.family, - tool.status, - &tool.title, - tool.subtitle.as_deref(), - tool.summary.as_deref(), - tool.duration_ms, - raw_input, - Some(&answer_text), - )); + tool.tool_display = Some(compute_tool_display(ToolDisplayInput { + kind: tool.kind, + family: tool.family, + status: tool.status, + title: &tool.title, + subtitle: tool.subtitle.as_deref(), + summary: tool.summary.as_deref(), + duration_ms: tool.duration_ms, + invocation_input: raw_input, + result_output: Some(&answer_text), + })); } let session_id = handle.id().to_string(); diff --git a/orbitdock-server/crates/server/src/runtime/session_resume.rs b/orbitdock-server/crates/server/src/runtime/session_resume.rs index c122071c..e330d25f 100644 --- a/orbitdock-server/crates/server/src/runtime/session_resume.rs +++ b/orbitdock-server/crates/server/src/runtime/session_resume.rs @@ -90,13 +90,15 @@ pub(crate) async fn launch_resumed_session( state.register_claude_thread(&session_id, provider_resume_id.as_str()); spawn_claude_resume( state, - &session_id, - project, - prepared.model, - provider_resume_id, - prepared.handle, - prepared.row_count, - prepared.allow_bypass_permissions, + ClaudeResumeParams { + session_id: session_id.clone(), + project, + model: prepared.model, + provider_resume_id, + handle: prepared.handle, + message_count: prepared.row_count, + allow_bypass_permissions: prepared.allow_bypass_permissions, + }, ) .await; } @@ -131,18 +133,26 @@ pub(crate) async fn launch_resumed_session( Ok(ResumeSessionLaunch { summary }) } -#[allow(clippy::too_many_arguments)] -async fn spawn_claude_resume( - state: &Arc, - session_id: &str, +struct ClaudeResumeParams { + session_id: String, project: String, model: Option, provider_resume_id: orbitdock_protocol::ProviderSessionId, - mut handle: crate::domain::sessions::session::SessionHandle, + handle: crate::domain::sessions::session::SessionHandle, message_count: usize, allow_bypass_permissions: bool, -) { - let session_id = session_id.to_string(); +} + +async fn spawn_claude_resume(state: &Arc, params: ClaudeResumeParams) { + let ClaudeResumeParams { + session_id, + project, + model, + provider_resume_id, + mut handle, + message_count, + allow_bypass_permissions, + } = params; let persist_tx = state.persist().clone(); let restored_permission_mode = load_session_permission_mode(&session_id) .await diff --git a/orbitdock-server/crates/server/src/transport/http/mission_control.rs b/orbitdock-server/crates/server/src/transport/http/mission_control.rs index d46517e9..f8130fcf 100644 --- a/orbitdock-server/crates/server/src/transport/http/mission_control.rs +++ b/orbitdock-server/crates/server/src/transport/http/mission_control.rs @@ -417,17 +417,21 @@ pub async fn retry_mission_issue( let iid2 = issue_id.clone(); let _ = tokio::task::spawn_blocking(move || { let conn = rusqlite::Connection::open(&db_path).ok()?; - use crate::infrastructure::persistence::mission_control::update_mission_issue_state_sync; + use crate::infrastructure::persistence::mission_control::{ + update_mission_issue_state_sync, MissionIssueStateUpdate, + }; update_mission_issue_state_sync( &conn, &mid2, &iid2, - "queued", - None, - Some(0), - Some(None), - Some(None), - Some(None), + &MissionIssueStateUpdate { + orchestration_state: "queued", + session_id: None, + attempt: Some(0), + last_error: Some(None), + started_at: Some(None), + completed_at: Some(None), + }, ) .ok() })