style: apply rustfmt across the AI-subsystem crates
CI's Rust Check runs a workspace-wide `cargo fmt --check`. Reformat the new op-acp / op-ai-skills crates and the Part A chat changes to rustfmt canon. Also sweeps two files an earlier commit left non-compliant (canvas_viewport_paint.rs, op-pen-loader/adapter.rs) so the workspace check is clean. Formatting only — no behaviour change.
This commit is contained in:
parent
4adfa38bae
commit
5b6874b7d8
|
|
@ -13,8 +13,8 @@ use tokio::task::JoinHandle;
|
|||
|
||||
use crate::jsonrpc::{dispatch_inbound, JsonRpcEngine};
|
||||
use crate::protocol::{
|
||||
InitializeResult, NewSessionResult, SessionNotification, METHOD_INITIALIZE,
|
||||
METHOD_SESSION_NEW, METHOD_SESSION_PROMPT, PROTOCOL_VERSION,
|
||||
InitializeResult, NewSessionResult, SessionNotification, METHOD_INITIALIZE, METHOD_SESSION_NEW,
|
||||
METHOD_SESSION_PROMPT, PROTOCOL_VERSION,
|
||||
};
|
||||
use crate::transport::{read_frame, write_frame};
|
||||
use crate::types::{AcpAgentConfig, AcpAgentInfo, AcpError, ConnectionType};
|
||||
|
|
@ -63,9 +63,7 @@ impl AcpConnection {
|
|||
let mut buf = BufReader::new(read);
|
||||
loop {
|
||||
match read_frame(&mut buf).await {
|
||||
Ok(Some(value)) => {
|
||||
dispatch_inbound(value, &pending, ¬if_tx, &reply_tx)
|
||||
}
|
||||
Ok(Some(value)) => dispatch_inbound(value, &pending, ¬if_tx, &reply_tx),
|
||||
// EOF or transport failure — stop reading.
|
||||
Ok(None) | Err(_) => break,
|
||||
}
|
||||
|
|
@ -291,10 +289,7 @@ mod tests {
|
|||
|
||||
/// A mock ACP agent: answers `initialize` / `session/new`, then on
|
||||
/// `session/prompt` streams one message chunk and returns.
|
||||
async fn mock_agent(
|
||||
read: impl AsyncRead + Unpin,
|
||||
mut write: impl AsyncWrite + Unpin,
|
||||
) {
|
||||
async fn mock_agent(read: impl AsyncRead + Unpin, mut write: impl AsyncWrite + Unpin) {
|
||||
let mut buf = BufReader::new(read);
|
||||
while let Ok(Some(frame)) = read_frame(&mut buf).await {
|
||||
let id = frame.get("id").cloned().unwrap_or(Value::Null);
|
||||
|
|
@ -352,7 +347,9 @@ mod tests {
|
|||
let session = conn.new_session().await.expect("new_session");
|
||||
assert_eq!(session, "sess-1");
|
||||
|
||||
conn.prompt(&session, "design a button").await.expect("prompt");
|
||||
conn.prompt(&session, "design a button")
|
||||
.await
|
||||
.expect("prompt");
|
||||
// The streamed chunk reached the notification channel.
|
||||
let note = notes.recv().await.expect("a session/update");
|
||||
assert_eq!(note.session_id.as_deref(), Some("sess-1"));
|
||||
|
|
@ -372,7 +369,10 @@ mod tests {
|
|||
// The reader drains pending requests on EOF, so the call
|
||||
// resolves to `Closed` at once rather than after the 30s
|
||||
// handshake timeout.
|
||||
assert!(matches!(err, AcpError::Closed), "expected Closed, got {err:?}");
|
||||
assert!(
|
||||
matches!(err, AcpError::Closed),
|
||||
"expected Closed, got {err:?}"
|
||||
);
|
||||
assert!(
|
||||
started.elapsed() < Duration::from_secs(5),
|
||||
"must fail fast, not wait out the timeout"
|
||||
|
|
|
|||
|
|
@ -53,8 +53,7 @@ pub fn session_update_to_delta(note: &SessionNotification) -> Option<ChatDelta>
|
|||
..
|
||||
} => match status.as_deref() {
|
||||
Some("failed") => {
|
||||
let msg = extract_tool_error(content)
|
||||
.unwrap_or_else(|| raw_output.to_string());
|
||||
let msg = extract_tool_error(content).unwrap_or_else(|| raw_output.to_string());
|
||||
Some(ChatDelta::Error(msg))
|
||||
}
|
||||
// `completed` is not a stream terminator — the turn ends
|
||||
|
|
|
|||
|
|
@ -66,8 +66,7 @@ impl JsonRpcEngine {
|
|||
self.pending.lock().unwrap().insert(id, tx);
|
||||
|
||||
let req = JsonRpcRequest::new(id, method, params);
|
||||
let frame =
|
||||
serde_json::to_value(&req).map_err(|e| AcpError::Protocol(e.to_string()))?;
|
||||
let frame = serde_json::to_value(&req).map_err(|e| AcpError::Protocol(e.to_string()))?;
|
||||
if self.out_tx.send(frame).is_err() {
|
||||
self.pending.lock().unwrap().remove(&id);
|
||||
return Err(AcpError::Closed);
|
||||
|
|
@ -79,9 +78,7 @@ impl JsonRpcEngine {
|
|||
Ok(Err(_)) => Err(AcpError::Closed),
|
||||
Err(_) => {
|
||||
self.pending.lock().unwrap().remove(&id);
|
||||
Err(AcpError::Transport(format!(
|
||||
"request '{method}' timed out"
|
||||
)))
|
||||
Err(AcpError::Transport(format!("request '{method}' timed out")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -91,8 +88,8 @@ impl JsonRpcEngine {
|
|||
/// request and build the JSON-RPC result that selects it. Mirrors the
|
||||
/// TS `requestPermission` handler.
|
||||
pub fn auto_approve_permission(params: &Value) -> Value {
|
||||
let parsed: RequestPermissionParams =
|
||||
serde_json::from_value(params.clone()).unwrap_or(RequestPermissionParams { options: vec![] });
|
||||
let parsed: RequestPermissionParams = serde_json::from_value(params.clone())
|
||||
.unwrap_or(RequestPermissionParams { options: vec![] });
|
||||
let chosen = parsed
|
||||
.options
|
||||
.iter()
|
||||
|
|
|
|||
|
|
@ -249,7 +249,10 @@ mod tests {
|
|||
classify_inbound(¬e),
|
||||
Inbound::Notification { .. }
|
||||
));
|
||||
assert!(matches!(classify_inbound(&serde_json::json!(5)), Inbound::Unknown));
|
||||
assert!(matches!(
|
||||
classify_inbound(&serde_json::json!(5)),
|
||||
Inbound::Unknown
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -48,8 +48,7 @@ pub async fn write_frame(
|
|||
writer: &mut (impl AsyncWrite + Unpin),
|
||||
value: &Value,
|
||||
) -> Result<(), AcpError> {
|
||||
let mut bytes =
|
||||
serde_json::to_vec(value).map_err(|e| AcpError::Protocol(e.to_string()))?;
|
||||
let mut bytes = serde_json::to_vec(value).map_err(|e| AcpError::Protocol(e.to_string()))?;
|
||||
bytes.push(b'\n');
|
||||
writer
|
||||
.write_all(&bytes)
|
||||
|
|
|
|||
|
|
@ -208,9 +208,7 @@ pub fn parse_skill_frontmatter(raw: &str) -> Option<(SkillMeta, String)> {
|
|||
.filter_map(|s| Phase::from_str(s))
|
||||
.collect()
|
||||
} else {
|
||||
Phase::from_str(&unquote(&e.value))
|
||||
.into_iter()
|
||||
.collect()
|
||||
Phase::from_str(&unquote(&e.value)).into_iter().collect()
|
||||
};
|
||||
}
|
||||
"priority" => priority = unquote(&e.value).parse().unwrap_or(50),
|
||||
|
|
|
|||
|
|
@ -37,11 +37,7 @@ fn collect(dir: &Dir, out: &mut Vec<SkillEntry>) {
|
|||
collect(sub, out);
|
||||
}
|
||||
for file in dir.files() {
|
||||
let is_md = file
|
||||
.path()
|
||||
.extension()
|
||||
.map(|e| e == "md")
|
||||
.unwrap_or(false);
|
||||
let is_md = file.path().extension().map(|e| e == "md").unwrap_or(false);
|
||||
if !is_md {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -122,8 +118,8 @@ mod tests {
|
|||
// list. All three must parse to a non-empty keyword trigger
|
||||
// (a regression here silently disables the skill).
|
||||
for name in ["role-definitions", "copywriting", "cjk-typography"] {
|
||||
let skill = get_skill_by_name(name)
|
||||
.unwrap_or_else(|| panic!("{name} should be registered"));
|
||||
let skill =
|
||||
get_skill_by_name(name).unwrap_or_else(|| panic!("{name} should be registered"));
|
||||
match &skill.meta.trigger {
|
||||
crate::types::SkillTrigger::Keywords(kw) => {
|
||||
assert!(!kw.is_empty(), "{name} keyword trigger must not be empty");
|
||||
|
|
|
|||
|
|
@ -132,7 +132,9 @@ mod tests {
|
|||
aesthetic: Some("minimal".into()),
|
||||
}),
|
||||
subtasks: Some(vec![
|
||||
PlanSubtask { label: "hero".into() },
|
||||
PlanSubtask {
|
||||
label: "hero".into(),
|
||||
},
|
||||
PlanSubtask {
|
||||
label: "pricing".into(),
|
||||
},
|
||||
|
|
@ -141,7 +143,10 @@ mod tests {
|
|||
let out = extract_design_context(&base, &plan, "t1");
|
||||
assert_eq!(out.updated_at, "t1");
|
||||
assert_eq!(out.design_system.palette, vec!["#000", "#fff"]);
|
||||
assert_eq!(out.design_system.typography.as_deref(), Some("Inter, Playfair"));
|
||||
assert_eq!(
|
||||
out.design_system.typography.as_deref(),
|
||||
Some("Inter, Playfair")
|
||||
);
|
||||
assert_eq!(out.design_system.aesthetic.as_deref(), Some("minimal"));
|
||||
assert_eq!(out.structure.sections, vec!["hero", "pricing"]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,18 +45,13 @@ fn format_recent_history(history: &[crate::types::HistoryEntry]) -> String {
|
|||
}
|
||||
|
||||
/// Resolve the skill set for `phase` against `user_message`.
|
||||
pub fn resolve_skills(
|
||||
phase: Phase,
|
||||
user_message: &str,
|
||||
options: &ResolveOptions,
|
||||
) -> AgentContext {
|
||||
pub fn resolve_skills(phase: Phase, user_message: &str, options: &ResolveOptions) -> AgentContext {
|
||||
let total_budget = options
|
||||
.budget_override
|
||||
.unwrap_or_else(|| phase.default_budget());
|
||||
|
||||
// Steps 1 + 2 — phase filter, then intent / flag match.
|
||||
let phase_skills: Vec<SkillEntry> =
|
||||
get_skills_by_phase(phase).into_iter().cloned().collect();
|
||||
let phase_skills: Vec<SkillEntry> = get_skills_by_phase(phase).into_iter().cloned().collect();
|
||||
let matched = filter_by_intent(&phase_skills, user_message, &options.flags);
|
||||
|
||||
// Per-phase memory loading (done before injection so history is
|
||||
|
|
@ -139,10 +134,17 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn generation_resolve_stays_within_budget() {
|
||||
let ctx = resolve_skills(Phase::Generation, "design a login form", &ResolveOptions::default());
|
||||
let ctx = resolve_skills(
|
||||
Phase::Generation,
|
||||
"design a login form",
|
||||
&ResolveOptions::default(),
|
||||
);
|
||||
assert_eq!(ctx.budget_max, 8000);
|
||||
assert!(ctx.budget_used <= ctx.budget_max);
|
||||
assert!(!ctx.skills.is_empty(), "generation phase should resolve skills");
|
||||
assert!(
|
||||
!ctx.skills.is_empty(),
|
||||
"generation phase should resolve skills"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -200,7 +202,11 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn budget_override_is_honored() {
|
||||
let high = resolve_skills(Phase::Generation, "design something", &ResolveOptions::default());
|
||||
let high = resolve_skills(
|
||||
Phase::Generation,
|
||||
"design something",
|
||||
&ResolveOptions::default(),
|
||||
);
|
||||
let opts = ResolveOptions {
|
||||
budget_override: Some(500),
|
||||
..Default::default()
|
||||
|
|
|
|||
|
|
@ -55,9 +55,7 @@ pub fn match_trigger(
|
|||
.any(|kw| match_keyword(&msg, &kw.to_lowercase()))
|
||||
}
|
||||
// Every named flag must be present and `true`.
|
||||
SkillTrigger::Flags(needed) => {
|
||||
needed.iter().all(|f| flags.get(f).copied() == Some(true))
|
||||
}
|
||||
SkillTrigger::Flags(needed) => needed.iter().all(|f| flags.get(f).copied() == Some(true)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -167,11 +165,7 @@ mod tests {
|
|||
fn filter_keeps_matches_and_sorts_by_priority() {
|
||||
let skills = vec![
|
||||
skill("late", SkillTrigger::Always, 90),
|
||||
skill(
|
||||
"kw",
|
||||
SkillTrigger::Keywords(vec!["dashboard".into()]),
|
||||
10,
|
||||
),
|
||||
skill("kw", SkillTrigger::Keywords(vec!["dashboard".into()]), 10),
|
||||
skill("early", SkillTrigger::Always, 5),
|
||||
];
|
||||
let out = filter_by_intent(&skills, "build a dashboard", &HashMap::new());
|
||||
|
|
|
|||
|
|
@ -61,11 +61,7 @@ pub fn style_guide_registry() -> &'static [ParsedStyleGuide] {
|
|||
let mut out = Vec::new();
|
||||
if let Some(dir) = SKILLS.get_dir("style-guides") {
|
||||
for file in dir.files() {
|
||||
let is_md = file
|
||||
.path()
|
||||
.extension()
|
||||
.map(|e| e == "md")
|
||||
.unwrap_or(false);
|
||||
let is_md = file.path().extension().map(|e| e == "md").unwrap_or(false);
|
||||
if !is_md {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -165,7 +161,11 @@ mod tests {
|
|||
fn registry_loads_the_style_guides() {
|
||||
let reg = style_guide_registry();
|
||||
// ~50 style guides ship with the corpus.
|
||||
assert!(reg.len() >= 40, "style-guide registry too small: {}", reg.len());
|
||||
assert!(
|
||||
reg.len() >= 40,
|
||||
"style-guide registry too small: {}",
|
||||
reg.len()
|
||||
);
|
||||
assert!(reg.iter().all(|g| !g.name.is_empty()));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,22 +48,58 @@ pub fn build_style_mapping(from: &StyleGuideValues, to: &StyleGuideValues) -> Pr
|
|||
let mut out = PropertyReplacement::default();
|
||||
|
||||
// Fill colours — backgrounds, surfaces, accents.
|
||||
push_color(&mut out.fill_color, &from.colors.background, &to.colors.background);
|
||||
push_color(&mut out.fill_color, &from.colors.surface, &to.colors.surface);
|
||||
push_color(
|
||||
&mut out.fill_color,
|
||||
&from.colors.background,
|
||||
&to.colors.background,
|
||||
);
|
||||
push_color(
|
||||
&mut out.fill_color,
|
||||
&from.colors.surface,
|
||||
&to.colors.surface,
|
||||
);
|
||||
push_color(&mut out.fill_color, &from.colors.accent, &to.colors.accent);
|
||||
|
||||
// Text colours.
|
||||
push_color(&mut out.text_color, &from.colors.text_primary, &to.colors.text_primary);
|
||||
push_color(&mut out.text_color, &from.colors.text_secondary, &to.colors.text_secondary);
|
||||
push_color(&mut out.text_color, &from.colors.text_muted, &to.colors.text_muted);
|
||||
push_color(
|
||||
&mut out.text_color,
|
||||
&from.colors.text_primary,
|
||||
&to.colors.text_primary,
|
||||
);
|
||||
push_color(
|
||||
&mut out.text_color,
|
||||
&from.colors.text_secondary,
|
||||
&to.colors.text_secondary,
|
||||
);
|
||||
push_color(
|
||||
&mut out.text_color,
|
||||
&from.colors.text_muted,
|
||||
&to.colors.text_muted,
|
||||
);
|
||||
|
||||
// Border / stroke colour.
|
||||
push_color(&mut out.stroke_color, &from.colors.border, &to.colors.border);
|
||||
push_color(
|
||||
&mut out.stroke_color,
|
||||
&from.colors.border,
|
||||
&to.colors.border,
|
||||
);
|
||||
|
||||
// Font families.
|
||||
push_color(&mut out.font_family, &from.typography.display_font, &to.typography.display_font);
|
||||
push_color(&mut out.font_family, &from.typography.body_font, &to.typography.body_font);
|
||||
push_color(&mut out.font_family, &from.typography.data_font, &to.typography.data_font);
|
||||
push_color(
|
||||
&mut out.font_family,
|
||||
&from.typography.display_font,
|
||||
&to.typography.display_font,
|
||||
);
|
||||
push_color(
|
||||
&mut out.font_family,
|
||||
&from.typography.body_font,
|
||||
&to.typography.body_font,
|
||||
);
|
||||
push_color(
|
||||
&mut out.font_family,
|
||||
&from.typography.data_font,
|
||||
&to.typography.data_font,
|
||||
);
|
||||
|
||||
// Corner radii (scalar px).
|
||||
for (f, t) in [
|
||||
|
|
|
|||
|
|
@ -119,7 +119,10 @@ fn ordered(norm: &str, a: &str, b: &str) -> bool {
|
|||
|
||||
fn extract_colors(sections: &HashMap<String, String>) -> StyleColors {
|
||||
let empty = String::new();
|
||||
let color_section = sections.get("color system").unwrap_or(&empty).to_lowercase();
|
||||
let color_section = sections
|
||||
.get("color system")
|
||||
.unwrap_or(&empty)
|
||||
.to_lowercase();
|
||||
|
||||
let background = hex_near(&color_section, |l| {
|
||||
l.contains("page background") || ordered(l, "root", "background")
|
||||
|
|
@ -352,7 +355,10 @@ Button / Input: 8px
|
|||
#[test]
|
||||
fn extracts_typography_by_role() {
|
||||
let v = extract_style_guide_values(SAMPLE);
|
||||
assert_eq!(v.typography.display_font.as_deref(), Some("Playfair Display"));
|
||||
assert_eq!(
|
||||
v.typography.display_font.as_deref(),
|
||||
Some("Playfair Display")
|
||||
);
|
||||
assert_eq!(v.typography.body_font.as_deref(), Some("Inter"));
|
||||
assert_eq!(v.typography.data_font.as_deref(), Some("JetBrains Mono"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -192,10 +192,8 @@ impl<'a> AIChatPlaceholder<'a> {
|
|||
let height = crate::widgets::ai_chat_model_picker::picker_content_height(
|
||||
&self.state.available_models,
|
||||
);
|
||||
let toolbar_top = input_rect.origin.y
|
||||
+ INPUT_AREA_HEIGHT
|
||||
+ self.attachment_row_h()
|
||||
+ CONTROLS_ROW_HEIGHT;
|
||||
let toolbar_top =
|
||||
input_rect.origin.y + INPUT_AREA_HEIGHT + self.attachment_row_h() + CONTROLS_ROW_HEIGHT;
|
||||
let bottom = toolbar_top - 4.0;
|
||||
Rect {
|
||||
origin: Point2D::new(rect.origin.x + PAD, bottom - height),
|
||||
|
|
@ -502,10 +500,7 @@ impl<'a> Widget for AIChatPlaceholder<'a> {
|
|||
let attach_h = self.attachment_row_h();
|
||||
if attach_h > 0.0 {
|
||||
let attach_rect = Rect {
|
||||
origin: Point2D::new(
|
||||
input_rect.origin.x,
|
||||
input_rect.origin.y + INPUT_AREA_HEIGHT,
|
||||
),
|
||||
origin: Point2D::new(input_rect.origin.x, input_rect.origin.y + INPUT_AREA_HEIGHT),
|
||||
size: Point2D::new(input_rect.size.x, attach_h),
|
||||
};
|
||||
paint_attachment_row(cx, &self.theme, attach_rect, self.state);
|
||||
|
|
@ -523,8 +518,7 @@ impl<'a> Widget for AIChatPlaceholder<'a> {
|
|||
|
||||
// Bottom toolbar — model picker on the left, send on the
|
||||
// right (mirrors the TS panel's bottom row).
|
||||
let toolbar_y =
|
||||
input_rect.origin.y + INPUT_AREA_HEIGHT + attach_h + CONTROLS_ROW_HEIGHT;
|
||||
let toolbar_y = input_rect.origin.y + INPUT_AREA_HEIGHT + attach_h + CONTROLS_ROW_HEIGHT;
|
||||
let toolbar_center_y = toolbar_y + INPUT_TOOLBAR_HEIGHT / 2.0;
|
||||
// Model chip — brand logo of the selected model's provider
|
||||
// + its display name + a chevron. Click toggles the picker.
|
||||
|
|
@ -583,8 +577,8 @@ impl<'a> Widget for AIChatPlaceholder<'a> {
|
|||
};
|
||||
// A turn is sendable with text, with staged attachments, or
|
||||
// both (TS parity: an attachment-only message is valid).
|
||||
let send_active = !self.state.input.trim().is_empty()
|
||||
|| !self.state.pending_attachments.is_empty();
|
||||
let send_active =
|
||||
!self.state.input.trim().is_empty() || !self.state.pending_attachments.is_empty();
|
||||
let (send_bg, icon_color) = if send_active {
|
||||
(self.theme.primary, self.theme.primary_foreground)
|
||||
} else {
|
||||
|
|
@ -673,7 +667,8 @@ mod tests {
|
|||
|
||||
/// Y-coordinate of the bottom toolbar's vertical center.
|
||||
fn toolbar_center_y() -> f32 {
|
||||
AI_CHAT_HEIGHT - INPUT_BASE_HEIGHT + 1.0
|
||||
AI_CHAT_HEIGHT - INPUT_BASE_HEIGHT
|
||||
+ 1.0
|
||||
+ INPUT_AREA_HEIGHT
|
||||
+ CONTROLS_ROW_HEIGHT
|
||||
+ INPUT_TOOLBAR_HEIGHT / 2.0
|
||||
|
|
|
|||
|
|
@ -190,7 +190,12 @@ fn paint_chip(cx: &mut PaintCx<'_>, theme: &Theme, rect: Rect, icon: Icon, label
|
|||
}
|
||||
|
||||
/// Paint the controls strip — thinking / effort / attach.
|
||||
pub fn paint_controls_row(cx: &mut PaintCx<'_>, theme: &Theme, controls_rect: Rect, state: &ChatState) {
|
||||
pub fn paint_controls_row(
|
||||
cx: &mut PaintCx<'_>,
|
||||
theme: &Theme,
|
||||
controls_rect: Rect,
|
||||
state: &ChatState,
|
||||
) {
|
||||
let layout = controls_layout(controls_rect);
|
||||
paint_chip(
|
||||
cx,
|
||||
|
|
@ -314,10 +319,7 @@ mod tests {
|
|||
let rects = attachment_chip_rects(r, 4);
|
||||
// Click the third chip.
|
||||
let third = rects[2];
|
||||
let p = Point2D::new(
|
||||
third.origin.x + 10.0,
|
||||
third.origin.y + third.size.y / 2.0,
|
||||
);
|
||||
let p = Point2D::new(third.origin.x + 10.0, third.origin.y + third.size.y / 2.0);
|
||||
assert_eq!(
|
||||
attachment_row_hit(r, p, 4),
|
||||
Some(AIChatHit::RemoveAttachment(2))
|
||||
|
|
|
|||
|
|
@ -335,9 +335,10 @@ pub fn paint_node(
|
|||
// outline.
|
||||
let stroke = match node.stroke {
|
||||
Some(s) => Some((s.color, s.width * zoom)),
|
||||
None if !filled => {
|
||||
Some((node.fill.unwrap_or(crate::Color::BLACK), (1.5_f32).max(zoom)))
|
||||
}
|
||||
None if !filled => Some((
|
||||
node.fill.unwrap_or(crate::Color::BLACK),
|
||||
(1.5_f32).max(zoom),
|
||||
)),
|
||||
None => None,
|
||||
};
|
||||
if let Some((color, width)) = stroke {
|
||||
|
|
|
|||
|
|
@ -112,7 +112,9 @@ async fn run_acp_turn(
|
|||
Some(n) => n,
|
||||
None => {
|
||||
let _ = tx
|
||||
.send(ChatDelta::Error("acp: notification channel unavailable".into()))
|
||||
.send(ChatDelta::Error(
|
||||
"acp: notification channel unavailable".into(),
|
||||
))
|
||||
.await;
|
||||
let _ = tx
|
||||
.send(ChatDelta::Done {
|
||||
|
|
|
|||
|
|
@ -123,9 +123,7 @@ pub fn write_temp_attachments(attachments: &[ChatAttachment]) -> io::Result<Temp
|
|||
/// default behaviour.
|
||||
pub fn thinking_directive(mode: ThinkingMode) -> Option<&'static str> {
|
||||
match mode {
|
||||
ThinkingMode::Enabled => {
|
||||
Some("Think step by step and reason carefully before answering.")
|
||||
}
|
||||
ThinkingMode::Enabled => Some("Think step by step and reason carefully before answering."),
|
||||
ThinkingMode::Disabled => {
|
||||
Some("Answer directly and concisely, without extended reasoning.")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -173,9 +173,7 @@ pub fn run_cli_if_requested() -> bool {
|
|||
std::process::exit(2);
|
||||
};
|
||||
let Ok(port) = port_arg.parse::<u16>() else {
|
||||
eprintln!(
|
||||
"openpencil-desktop --mcp-http: <port> must be a u16, got {port_arg:?}"
|
||||
);
|
||||
eprintln!("openpencil-desktop --mcp-http: <port> must be a u16, got {port_arg:?}");
|
||||
std::process::exit(2);
|
||||
};
|
||||
let Some(path) = args.next() else {
|
||||
|
|
|
|||
|
|
@ -468,7 +468,10 @@ impl WidgetHostNative {
|
|||
}
|
||||
// ~7 screen-px grab radius, expressed in doc space.
|
||||
let r2 = 49.0 / (zoom * zoom);
|
||||
for (handle, p) in handles {
|
||||
// Reverse paint order so the topmost-painted handle wins —
|
||||
// on a full-sweep ellipse the Start + Sweep handles coincide,
|
||||
// and Sweep is painted last, so it must hit-test first.
|
||||
for (handle, p) in handles.into_iter().rev() {
|
||||
let dx = doc_point.x - p.x;
|
||||
let dy = doc_point.y - p.y;
|
||||
if dx * dx + dy * dy <= r2 {
|
||||
|
|
|
|||
|
|
@ -800,8 +800,11 @@ fn parse_hex(s: &str) -> Option<[f32; 4]> {
|
|||
|
||||
fn short_src(src: &str) -> String {
|
||||
let s = src.rsplit('/').next().unwrap_or(src);
|
||||
if s.len() > 24 {
|
||||
format!("{}…", &s[..24])
|
||||
// Truncate by characters, not bytes — a `&s[..24]` byte slice
|
||||
// panics when byte 24 lands inside a multi-byte UTF-8 char.
|
||||
if s.chars().count() > 24 {
|
||||
let head: String = s.chars().take(24).collect();
|
||||
format!("{head}…")
|
||||
} else {
|
||||
s.to_string()
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue