feat: add actor behavior editor to world
Add actor-local temporary behavior editing, reload-from-source, and confirmed updates to the shared source while guarding unsaved drafts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
2f9d500ca3
commit
c697010765
|
|
@ -13,6 +13,8 @@ All notable changes to this project will be documented in this file.
|
||||||
source commits, and explicit image-event collection.
|
source commits, and explicit image-event collection.
|
||||||
- Add committed source history browsing, revision preview, and confirmed
|
- Add committed source history browsing, revision preview, and confirmed
|
||||||
rollback to the graphical source editor.
|
rollback to the graphical source editor.
|
||||||
|
- Add actor-local temporary behavior editing and guarded shared-source updates
|
||||||
|
to the graphical actor inspector.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,9 @@ execution-stack inspection, and retained logs. Its source view supports
|
||||||
editing image sources with optimistic conflict checks, compilation diagnostics,
|
editing image sources with optimistic conflict checks, compilation diagnostics,
|
||||||
explicit history commits, and revision preview/rollback. Rollback requires
|
explicit history commits, and revision preview/rollback. Rollback requires
|
||||||
confirmation and creates a new current source version without deleting history.
|
confirmation and creates a new current source version without deleting history.
|
||||||
|
The actor inspector can also load and edit an actor-local temporary behavior,
|
||||||
|
save it with source-identity checks, reload the behavior from the image source,
|
||||||
|
or explicitly confirm updating the shared source from the temporary version.
|
||||||
The events view has a manual collection action:
|
The events view has a manual collection action:
|
||||||
collecting events consumes the image's shared event queue, so other clients do
|
collecting events consumes the image's shared event queue, so other clients do
|
||||||
not receive events collected by the world client. Rune frame locals and
|
not receive events collected by the world client. Rune frame locals and
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,13 @@ enum WorkerUpdate {
|
||||||
submitted_source: String,
|
submitted_source: String,
|
||||||
},
|
},
|
||||||
SourceDiagnostics(Vec<SourceDiagnostic>),
|
SourceDiagnostics(Vec<SourceDiagnostic>),
|
||||||
|
ActorBehavior {
|
||||||
|
actor_ref: String,
|
||||||
|
path: String,
|
||||||
|
source_id: String,
|
||||||
|
source: String,
|
||||||
|
submitted_source: Option<String>,
|
||||||
|
},
|
||||||
SourceHistory {
|
SourceHistory {
|
||||||
path: String,
|
path: String,
|
||||||
history: Vec<SourceCommitDto>,
|
history: Vec<SourceCommitDto>,
|
||||||
|
|
@ -74,6 +81,7 @@ enum WorldView {
|
||||||
struct WorkerSnapshot {
|
struct WorkerSnapshot {
|
||||||
actors: Vec<ActorSnapshotDto>,
|
actors: Vec<ActorSnapshotDto>,
|
||||||
sources: Vec<String>,
|
sources: Vec<String>,
|
||||||
|
source_ids: BTreeMap<String, String>,
|
||||||
actor: Option<ActorSnapshotDto>,
|
actor: Option<ActorSnapshotDto>,
|
||||||
execution: Option<ExecutionInspectionDto>,
|
execution: Option<ExecutionInspectionDto>,
|
||||||
logs: Vec<LogEntry>,
|
logs: Vec<LogEntry>,
|
||||||
|
|
@ -87,6 +95,7 @@ struct WorldApp {
|
||||||
notice: String,
|
notice: String,
|
||||||
actors: Vec<ActorSnapshotDto>,
|
actors: Vec<ActorSnapshotDto>,
|
||||||
sources: Vec<String>,
|
sources: Vec<String>,
|
||||||
|
source_ids: BTreeMap<String, String>,
|
||||||
selected_actor: Option<String>,
|
selected_actor: Option<String>,
|
||||||
actor: Option<ActorSnapshotDto>,
|
actor: Option<ActorSnapshotDto>,
|
||||||
execution: Option<ExecutionInspectionDto>,
|
execution: Option<ExecutionInspectionDto>,
|
||||||
|
|
@ -110,6 +119,10 @@ struct WorldApp {
|
||||||
revision_preview: Option<(String, String)>,
|
revision_preview: Option<(String, String)>,
|
||||||
rollback_confirmation: Option<(String, String)>,
|
rollback_confirmation: Option<(String, String)>,
|
||||||
events: Vec<EventDto>,
|
events: Vec<EventDto>,
|
||||||
|
actor_behavior: Option<SourceFileDto>,
|
||||||
|
actor_behavior_draft: String,
|
||||||
|
actor_behavior_dirty: bool,
|
||||||
|
update_behavior_confirmation: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WorldApp {
|
impl WorldApp {
|
||||||
|
|
@ -129,6 +142,7 @@ impl WorldApp {
|
||||||
notice: String::new(),
|
notice: String::new(),
|
||||||
actors: Vec::new(),
|
actors: Vec::new(),
|
||||||
sources: Vec::new(),
|
sources: Vec::new(),
|
||||||
|
source_ids: BTreeMap::new(),
|
||||||
selected_actor: None,
|
selected_actor: None,
|
||||||
actor: None,
|
actor: None,
|
||||||
execution: None,
|
execution: None,
|
||||||
|
|
@ -152,6 +166,10 @@ impl WorldApp {
|
||||||
revision_preview: None,
|
revision_preview: None,
|
||||||
rollback_confirmation: None,
|
rollback_confirmation: None,
|
||||||
events: Vec::new(),
|
events: Vec::new(),
|
||||||
|
actor_behavior: None,
|
||||||
|
actor_behavior_draft: String::new(),
|
||||||
|
actor_behavior_dirty: false,
|
||||||
|
update_behavior_confirmation: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -163,11 +181,20 @@ impl WorldApp {
|
||||||
|
|
||||||
fn choose_actor(&mut self, actor_ref: Option<String>) {
|
fn choose_actor(&mut self, actor_ref: Option<String>) {
|
||||||
if self.selected_actor != actor_ref {
|
if self.selected_actor != actor_ref {
|
||||||
|
if self.actor_behavior_dirty {
|
||||||
|
self.notice =
|
||||||
|
"Save or discard the current actor behavior draft before changing actors"
|
||||||
|
.to_owned();
|
||||||
|
return;
|
||||||
|
}
|
||||||
self.selected_actor = actor_ref.clone();
|
self.selected_actor = actor_ref.clone();
|
||||||
self.actor = None;
|
self.actor = None;
|
||||||
self.execution = None;
|
self.execution = None;
|
||||||
self.logs.clear();
|
self.logs.clear();
|
||||||
self.breakpoints.clear();
|
self.breakpoints.clear();
|
||||||
|
self.actor_behavior = None;
|
||||||
|
self.actor_behavior_draft.clear();
|
||||||
|
self.actor_behavior_dirty = false;
|
||||||
let _ = self.requests.send(WorkerRequest::Select(actor_ref));
|
let _ = self.requests.send(WorkerRequest::Select(actor_ref));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -202,6 +229,7 @@ impl WorldApp {
|
||||||
self.choose_actor(None);
|
self.choose_actor(None);
|
||||||
self.actors.clear();
|
self.actors.clear();
|
||||||
self.sources.clear();
|
self.sources.clear();
|
||||||
|
self.source_ids.clear();
|
||||||
self.actor = None;
|
self.actor = None;
|
||||||
self.execution = None;
|
self.execution = None;
|
||||||
self.logs.clear();
|
self.logs.clear();
|
||||||
|
|
@ -213,6 +241,9 @@ impl WorldApp {
|
||||||
self.selected_revision = None;
|
self.selected_revision = None;
|
||||||
self.revision_preview = None;
|
self.revision_preview = None;
|
||||||
self.events.clear();
|
self.events.clear();
|
||||||
|
self.actor_behavior = None;
|
||||||
|
self.actor_behavior_draft.clear();
|
||||||
|
self.actor_behavior_dirty = false;
|
||||||
}
|
}
|
||||||
Ok(WorkerUpdate::SourceLoaded(source)) => {
|
Ok(WorkerUpdate::SourceLoaded(source)) => {
|
||||||
if self.loading_source.as_deref() == Some(source.path.as_str()) {
|
if self.loading_source.as_deref() == Some(source.path.as_str()) {
|
||||||
|
|
@ -239,6 +270,28 @@ impl WorldApp {
|
||||||
Ok(WorkerUpdate::SourceDiagnostics(diagnostics)) => {
|
Ok(WorkerUpdate::SourceDiagnostics(diagnostics)) => {
|
||||||
self.source_diagnostics = diagnostics;
|
self.source_diagnostics = diagnostics;
|
||||||
}
|
}
|
||||||
|
Ok(WorkerUpdate::ActorBehavior {
|
||||||
|
actor_ref,
|
||||||
|
path,
|
||||||
|
source_id,
|
||||||
|
source,
|
||||||
|
submitted_source,
|
||||||
|
}) => {
|
||||||
|
if self.selected_actor.as_deref() == Some(&actor_ref) {
|
||||||
|
if submitted_source
|
||||||
|
.as_ref()
|
||||||
|
.is_none_or(|submitted| self.actor_behavior_draft == *submitted)
|
||||||
|
{
|
||||||
|
self.actor_behavior_draft.clone_from(&source);
|
||||||
|
}
|
||||||
|
self.actor_behavior_dirty = self.actor_behavior_draft != source;
|
||||||
|
self.actor_behavior = Some(SourceFileDto {
|
||||||
|
path,
|
||||||
|
source_id,
|
||||||
|
source,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(WorkerUpdate::SourceHistory { path, history }) => {
|
Ok(WorkerUpdate::SourceHistory { path, history }) => {
|
||||||
if self.source_path() != Some(path.as_str()) {
|
if self.source_path() != Some(path.as_str()) {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -272,8 +325,20 @@ impl WorldApp {
|
||||||
{
|
{
|
||||||
self.choose_actor(Some(first.actor_ref.clone()));
|
self.choose_actor(Some(first.actor_ref.clone()));
|
||||||
}
|
}
|
||||||
|
if let (Some(previous), Some(current)) = (&self.actor, &snapshot.actor)
|
||||||
|
&& previous.actor_ref == current.actor_ref
|
||||||
|
&& previous.temporary_behavior
|
||||||
|
&& !current.temporary_behavior
|
||||||
|
{
|
||||||
|
let _ =
|
||||||
|
self.requests
|
||||||
|
.send(WorkerRequest::Command(Command::ActorBehavior {
|
||||||
|
actor_ref: current.actor_ref.clone(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
self.actors = snapshot.actors;
|
self.actors = snapshot.actors;
|
||||||
self.sources = snapshot.sources;
|
self.sources = snapshot.sources;
|
||||||
|
self.source_ids = snapshot.source_ids;
|
||||||
self.actor = snapshot.actor;
|
self.actor = snapshot.actor;
|
||||||
self.execution = snapshot.execution;
|
self.execution = snapshot.execution;
|
||||||
self.logs = snapshot.logs;
|
self.logs = snapshot.logs;
|
||||||
|
|
@ -306,9 +371,10 @@ impl WorldApp {
|
||||||
ui.label("Image");
|
ui.label("Image");
|
||||||
ui.text_edit_singleline(&mut self.endpoint);
|
ui.text_edit_singleline(&mut self.endpoint);
|
||||||
if ui.button("Connect").clicked() {
|
if ui.button("Connect").clicked() {
|
||||||
if self.source_dirty {
|
if self.source_dirty || self.actor_behavior_dirty {
|
||||||
self.notice =
|
self.notice =
|
||||||
"Save or discard the current source draft before reconnecting".to_owned();
|
"Save or discard the current source and behavior drafts before reconnecting"
|
||||||
|
.to_owned();
|
||||||
} else {
|
} else {
|
||||||
match self.endpoint.parse() {
|
match self.endpoint.parse() {
|
||||||
Ok(address) => {
|
Ok(address) => {
|
||||||
|
|
@ -479,11 +545,136 @@ impl WorldApp {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
self.draw_message_sender(ui, &actor.actor_ref);
|
self.draw_message_sender(ui, &actor.actor_ref);
|
||||||
|
self.draw_actor_behavior_editor(ui, &actor);
|
||||||
self.draw_debugger(ui, &actor);
|
self.draw_debugger(ui, &actor);
|
||||||
self.draw_logs(ui, &actor.actor_ref);
|
self.draw_logs(ui, &actor.actor_ref);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn draw_actor_behavior_editor(&mut self, ui: &mut egui::Ui, actor: &ActorSnapshotDto) {
|
||||||
|
ui.collapsing("Actor behavior", |ui| {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.label(actor.behavior_path.as_deref().unwrap_or("No source path"));
|
||||||
|
if ui.button("Load active behavior").clicked() {
|
||||||
|
self.submit(Command::ActorBehavior {
|
||||||
|
actor_ref: actor.actor_ref.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if ui
|
||||||
|
.add_enabled(
|
||||||
|
!self.actor_behavior_dirty,
|
||||||
|
egui::Button::new("Reload from source"),
|
||||||
|
)
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
self.submit(Command::System {
|
||||||
|
actor_ref: actor.actor_ref.clone(),
|
||||||
|
message: SystemMessageDto::Reload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if self.actor_behavior_dirty {
|
||||||
|
ui.colored_label(egui::Color32::YELLOW, "Unsaved temporary draft");
|
||||||
|
if ui.button("Discard draft").clicked()
|
||||||
|
&& let Some(source) = &self.actor_behavior
|
||||||
|
{
|
||||||
|
self.actor_behavior_draft.clone_from(&source.source);
|
||||||
|
self.actor_behavior_dirty = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
if ui
|
||||||
|
.add_enabled(
|
||||||
|
self.actor_behavior.is_some() && self.actor_behavior_dirty,
|
||||||
|
egui::Button::new("Save as temporary behavior"),
|
||||||
|
)
|
||||||
|
.clicked()
|
||||||
|
&& let Some(source) = &self.actor_behavior
|
||||||
|
{
|
||||||
|
self.submit(Command::WriteActorBehavior {
|
||||||
|
actor_ref: actor.actor_ref.clone(),
|
||||||
|
expected_source_id: source.source_id.clone(),
|
||||||
|
source: self.actor_behavior_draft.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if ui
|
||||||
|
.add_enabled(
|
||||||
|
actor.temporary_behavior && !self.actor_behavior_dirty,
|
||||||
|
egui::Button::new("Update shared source"),
|
||||||
|
)
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
self.update_behavior_confirmation = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if self.actor_behavior.is_none() {
|
||||||
|
ui.label(
|
||||||
|
"Load the actor's current behavior to edit an actor-local temporary copy.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut layouter = |ui: &egui::Ui, source: &str, wrap_width: f32| {
|
||||||
|
let mut job = rune_layout_job(source);
|
||||||
|
job.wrap.max_width = wrap_width;
|
||||||
|
ui.fonts(|fonts| fonts.layout_job(job))
|
||||||
|
};
|
||||||
|
let response = ui.add(
|
||||||
|
egui::TextEdit::multiline(&mut self.actor_behavior_draft)
|
||||||
|
.code_editor()
|
||||||
|
.layouter(&mut layouter)
|
||||||
|
.desired_width(f32::INFINITY)
|
||||||
|
.desired_rows(12),
|
||||||
|
);
|
||||||
|
if response.changed() {
|
||||||
|
self.actor_behavior_dirty = self
|
||||||
|
.actor_behavior
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|source| self.actor_behavior_draft != source.source);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if self.update_behavior_confirmation {
|
||||||
|
egui::Window::new("Confirm shared behavior update")
|
||||||
|
.collapsible(false)
|
||||||
|
.resizable(false)
|
||||||
|
.show(ui.ctx(), |ui| {
|
||||||
|
ui.label(format!(
|
||||||
|
"Overwrite `{}` in the image source tree with this actor's temporary behavior?",
|
||||||
|
self.actor_behavior
|
||||||
|
.as_ref()
|
||||||
|
.map_or("unknown source", |source| source.path.as_str())
|
||||||
|
));
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
if ui.button("Update source").clicked() {
|
||||||
|
let Some(temporary) = &self.actor_behavior else {
|
||||||
|
self.update_behavior_confirmation = false;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let expected_source_id = self.source_ids.get(&temporary.path).cloned();
|
||||||
|
match expected_source_id {
|
||||||
|
Some(expected_source_id) => self.submit(
|
||||||
|
Command::UpdateBehaviorSource {
|
||||||
|
actor_ref: actor.actor_ref.clone(),
|
||||||
|
path: temporary.path.clone(),
|
||||||
|
expected_source_id,
|
||||||
|
temporary_source_id: temporary.source_id.clone(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
None => {
|
||||||
|
self.notice =
|
||||||
|
"Could not determine the current source identity; refresh and try again"
|
||||||
|
.to_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.update_behavior_confirmation = false;
|
||||||
|
}
|
||||||
|
if ui.button("Cancel").clicked() {
|
||||||
|
self.update_behavior_confirmation = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn draw_message_sender(&mut self, ui: &mut egui::Ui, actor_ref: &str) {
|
fn draw_message_sender(&mut self, ui: &mut egui::Ui, actor_ref: &str) {
|
||||||
ui.collapsing("Send message", |ui| {
|
ui.collapsing("Send message", |ui| {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
|
|
@ -864,6 +1055,7 @@ fn request_reply(client: &mut WebSocketClient, command: Command) -> Result<Reply
|
||||||
fn client_worker(requests: Receiver<WorkerRequest>, updates: Sender<WorkerUpdate>) {
|
fn client_worker(requests: Receiver<WorkerRequest>, updates: Sender<WorkerUpdate>) {
|
||||||
let mut client = None;
|
let mut client = None;
|
||||||
let mut selected_actor = None;
|
let mut selected_actor = None;
|
||||||
|
let mut selected_behavior_path = None;
|
||||||
let mut log_sequence = 0;
|
let mut log_sequence = 0;
|
||||||
let mut retained_logs = Vec::new();
|
let mut retained_logs = Vec::new();
|
||||||
let mut next_poll = Instant::now();
|
let mut next_poll = Instant::now();
|
||||||
|
|
@ -873,6 +1065,7 @@ fn client_worker(requests: Receiver<WorkerRequest>, updates: Sender<WorkerUpdate
|
||||||
Ok(connected) => {
|
Ok(connected) => {
|
||||||
client = Some(connected);
|
client = Some(connected);
|
||||||
selected_actor = None;
|
selected_actor = None;
|
||||||
|
selected_behavior_path = None;
|
||||||
log_sequence = 0;
|
log_sequence = 0;
|
||||||
retained_logs.clear();
|
retained_logs.clear();
|
||||||
next_poll = Instant::now();
|
next_poll = Instant::now();
|
||||||
|
|
@ -887,6 +1080,7 @@ fn client_worker(requests: Receiver<WorkerRequest>, updates: Sender<WorkerUpdate
|
||||||
},
|
},
|
||||||
Ok(WorkerRequest::Select(actor_ref)) => {
|
Ok(WorkerRequest::Select(actor_ref)) => {
|
||||||
selected_actor = actor_ref;
|
selected_actor = actor_ref;
|
||||||
|
selected_behavior_path = None;
|
||||||
log_sequence = 0;
|
log_sequence = 0;
|
||||||
retained_logs.clear();
|
retained_logs.clear();
|
||||||
next_poll = Instant::now();
|
next_poll = Instant::now();
|
||||||
|
|
@ -907,6 +1101,9 @@ fn client_worker(requests: Receiver<WorkerRequest>, updates: Sender<WorkerUpdate
|
||||||
let operation = match &command {
|
let operation = match &command {
|
||||||
Command::ReadSource { .. } => "read_source",
|
Command::ReadSource { .. } => "read_source",
|
||||||
Command::WriteSource { .. } => "write_source",
|
Command::WriteSource { .. } => "write_source",
|
||||||
|
Command::ActorBehavior { .. } => "actor_behavior",
|
||||||
|
Command::WriteActorBehavior { .. } => "write_actor_behavior",
|
||||||
|
Command::UpdateBehaviorSource { .. } => "update_behavior_source",
|
||||||
Command::CheckSource { .. } => "check_source",
|
Command::CheckSource { .. } => "check_source",
|
||||||
Command::GetEvents => "get_events",
|
Command::GetEvents => "get_events",
|
||||||
Command::CommitSource { .. } => "commit_source",
|
Command::CommitSource { .. } => "commit_source",
|
||||||
|
|
@ -916,7 +1113,8 @@ fn client_worker(requests: Receiver<WorkerRequest>, updates: Sender<WorkerUpdate
|
||||||
_ => "command",
|
_ => "command",
|
||||||
};
|
};
|
||||||
let submitted_source = match &command {
|
let submitted_source = match &command {
|
||||||
Command::WriteSource { source, .. } => Some(source.clone()),
|
Command::WriteSource { source, .. }
|
||||||
|
| Command::WriteActorBehavior { source, .. } => Some(source.clone()),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
let revision = match &command {
|
let revision = match &command {
|
||||||
|
|
@ -924,13 +1122,19 @@ fn client_worker(requests: Receiver<WorkerRequest>, updates: Sender<WorkerUpdate
|
||||||
| Command::RollbackSource { revision, .. } => Some(revision.clone()),
|
| Command::RollbackSource { revision, .. } => Some(revision.clone()),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
let requested_actor_ref = match &command {
|
||||||
|
Command::ActorBehavior { actor_ref }
|
||||||
|
| Command::WriteActorBehavior { actor_ref, .. } => Some(actor_ref.clone()),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
let source_path = match &command {
|
let source_path = match &command {
|
||||||
Command::ReadSource { path }
|
Command::ReadSource { path }
|
||||||
| Command::WriteSource { path, .. }
|
| Command::WriteSource { path, .. }
|
||||||
| Command::CommitSource { path }
|
| Command::CommitSource { path }
|
||||||
| Command::SourceHistory { path }
|
| Command::SourceHistory { path }
|
||||||
| Command::RevisionSource { path, .. }
|
| Command::RevisionSource { path, .. }
|
||||||
| Command::RollbackSource { path, .. } => Some(path.clone()),
|
| Command::RollbackSource { path, .. }
|
||||||
|
| Command::UpdateBehaviorSource { path, .. } => Some(path.clone()),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
match request_reply(connected, command) {
|
match request_reply(connected, command) {
|
||||||
|
|
@ -946,6 +1150,25 @@ fn client_worker(requests: Receiver<WorkerRequest>, updates: Sender<WorkerUpdate
|
||||||
.expect("write operation includes its submitted source"),
|
.expect("write operation includes its submitted source"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
("actor_behavior", Reply::ActorBehavior { source_id, source }) => {
|
||||||
|
Some(WorkerUpdate::ActorBehavior {
|
||||||
|
actor_ref: requested_actor_ref.unwrap_or_default(),
|
||||||
|
path: selected_behavior_path.clone().unwrap_or_default(),
|
||||||
|
source_id,
|
||||||
|
source,
|
||||||
|
submitted_source: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
(
|
||||||
|
"write_actor_behavior",
|
||||||
|
Reply::ActorBehavior { source_id, source },
|
||||||
|
) => Some(WorkerUpdate::ActorBehavior {
|
||||||
|
actor_ref: requested_actor_ref.unwrap_or_default(),
|
||||||
|
path: selected_behavior_path.clone().unwrap_or_default(),
|
||||||
|
source_id,
|
||||||
|
source,
|
||||||
|
submitted_source,
|
||||||
|
}),
|
||||||
("check_source", Reply::SourceChecked { diagnostics }) => {
|
("check_source", Reply::SourceChecked { diagnostics }) => {
|
||||||
Some(WorkerUpdate::SourceDiagnostics(diagnostics))
|
Some(WorkerUpdate::SourceDiagnostics(diagnostics))
|
||||||
}
|
}
|
||||||
|
|
@ -979,6 +1202,12 @@ fn client_worker(requests: Receiver<WorkerRequest>, updates: Sender<WorkerUpdate
|
||||||
submitted_source,
|
submitted_source,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
("update_behavior_source", Reply::Source(source)) => {
|
||||||
|
Some(WorkerUpdate::SourceSaved {
|
||||||
|
source: source.clone(),
|
||||||
|
submitted_source: source.source,
|
||||||
|
})
|
||||||
|
}
|
||||||
(_, reply) => Some(WorkerUpdate::Notice(format!(
|
(_, reply) => Some(WorkerUpdate::Notice(format!(
|
||||||
"Completed with unexpected response: {reply:?}"
|
"Completed with unexpected response: {reply:?}"
|
||||||
))),
|
))),
|
||||||
|
|
@ -1027,9 +1256,18 @@ fn client_worker(requests: Receiver<WorkerRequest>, updates: Sender<WorkerUpdate
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let sources = match request_reply(connected, Command::ListSources) {
|
let (sources, source_ids) = match request_reply(connected, Command::ListSources) {
|
||||||
Ok(Reply::Sources(sources)) => sources.into_iter().map(|source| source.path).collect(),
|
Ok(Reply::Sources(sources)) => {
|
||||||
Ok(_) => Vec::new(),
|
let source_ids = sources
|
||||||
|
.iter()
|
||||||
|
.map(|source| (source.path.clone(), source.source_id.clone()))
|
||||||
|
.collect();
|
||||||
|
(
|
||||||
|
sources.into_iter().map(|source| source.path).collect(),
|
||||||
|
source_ids,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Ok(_) => (Vec::new(), BTreeMap::new()),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
report_call_error(error, &updates, &mut client);
|
report_call_error(error, &updates, &mut client);
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -1041,6 +1279,7 @@ fn client_worker(requests: Receiver<WorkerRequest>, updates: Sender<WorkerUpdate
|
||||||
.find(|actor| &actor.actor_ref == selected)
|
.find(|actor| &actor.actor_ref == selected)
|
||||||
.cloned()
|
.cloned()
|
||||||
});
|
});
|
||||||
|
selected_behavior_path = actor.as_ref().and_then(|actor| actor.behavior_path.clone());
|
||||||
let (execution, logs) = if let Some(actor) = &actor {
|
let (execution, logs) = if let Some(actor) = &actor {
|
||||||
let execution = match request_reply(
|
let execution = match request_reply(
|
||||||
connected,
|
connected,
|
||||||
|
|
@ -1088,6 +1327,7 @@ fn client_worker(requests: Receiver<WorkerRequest>, updates: Sender<WorkerUpdate
|
||||||
let _ = updates.send(WorkerUpdate::Snapshot(Box::new(WorkerSnapshot {
|
let _ = updates.send(WorkerUpdate::Snapshot(Box::new(WorkerSnapshot {
|
||||||
actors,
|
actors,
|
||||||
sources,
|
sources,
|
||||||
|
source_ids,
|
||||||
actor,
|
actor,
|
||||||
execution,
|
execution,
|
||||||
logs,
|
logs,
|
||||||
|
|
|
||||||
|
|
@ -224,11 +224,13 @@ loopback image over the shared WebSocket protocol. It currently provides an
|
||||||
actor browser and inspector with message, lifecycle, log, breakpoint, bounded
|
actor browser and inspector with message, lifecycle, log, breakpoint, bounded
|
||||||
step, and execution-stack controls, plus a source editor with compilation
|
step, and execution-stack controls, plus a source editor with compilation
|
||||||
diagnostics, explicit source commits, committed-revision previews, and
|
diagnostics, explicit source commits, committed-revision previews, and
|
||||||
confirmed rollback. Image events can be collected manually; this operation
|
confirmed rollback. The actor inspector can edit an actor-local temporary
|
||||||
drains the shared queue and therefore consumes events for other clients too.
|
behavior and explicitly update its shared source or reload from that source.
|
||||||
Network requests run on a worker thread, not the UI frame loop. This is a
|
Image events can be collected manually; this operation drains the shared queue
|
||||||
client foundation, not yet a Rune-composed graphical world; frame locals and
|
and therefore consumes events for other clients too. Network requests run on a
|
||||||
expression evaluation remain unavailable.
|
worker thread, not the UI frame loop. This is a client foundation, not yet a
|
||||||
|
Rune-composed graphical world; frame locals and expression evaluation remain
|
||||||
|
unavailable.
|
||||||
|
|
||||||
## 8. Unified Programmatic Image API
|
## 8. Unified Programmatic Image API
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue