refactor: rename pod crate to worker

This commit is contained in:
2026-06-26 00:05:57 +09:00
parent 4c677640f4
commit 6c59fe927b
194 changed files with 6637 additions and 6146 deletions
+151 -144
View File
@@ -4,7 +4,8 @@ use std::time::{Duration, Instant};
use protocol::{
AlertLevel, AlertSource, CompletionEntry, CompletionKind, ErrorCode, Event, InFlightBlock,
InFlightSnapshot, InFlightToolCallState, Method, PodStatus, RewindTarget, RunResult, Segment,
InFlightSnapshot, InFlightToolCallState, Method, RewindTarget, RunResult, Segment,
WorkerStatus,
};
use crate::block::{
@@ -40,7 +41,7 @@ pub struct CompletionState {
pub prefix_start: usize,
/// Text typed after the sigil (sigil itself excluded).
pub prefix: String,
/// Latest candidate set returned by the Pod for `(kind, prefix)`.
/// Latest candidate set returned by the Worker for `(kind, prefix)`.
/// Initially empty until `Event::Completions` lands.
pub entries: Vec<CompletionEntry>,
pub selected: usize,
@@ -71,7 +72,7 @@ pub struct RewindPickerState {
pub selected: usize,
pub scroll: RewindPickerScroll,
/// True after Enter submitted an authoritative `RewindTo` and before the
/// Pod replies with either `RewindApplied` or `Error`. While set, the
/// Worker replies with either `RewindApplied` or `Error`. While set, the
/// picker remains visible but further submits/navigation are ignored so a
/// destructive rewind cannot be queued multiple times by key repeat.
pub applying: bool,
@@ -227,14 +228,14 @@ impl ActionbarNotice {
}
pub struct App {
pub pod_name: String,
pub worker_name: String,
pub connected: bool,
/// Last controller status reported by the Pod. Drives the status line
/// Last controller status reported by the Worker. Drives the status line
/// and Ctrl-key routing; do not infer this solely from replayed history.
pub pod_status: PodStatus,
/// True while the Pod is in `PodStatus::Running`.
pub worker_status: WorkerStatus,
/// True while the Worker is in `WorkerStatus::Running`.
pub running: bool,
/// True while the Pod is in `PodStatus::Paused`.
/// True while the Worker is in `WorkerStatus::Paused`.
pub paused: bool,
pub run_requests: usize,
/// Sum of `input_tokens - cache_read_input_tokens` across the
@@ -243,7 +244,7 @@ pub struct App {
/// cache reads excluded). Reset on `RunEnd`.
pub run_upload_tokens: u64,
pub run_output_tokens: u64,
/// Latest session context tokens reported by the Pod. This is the raw
/// Latest session context tokens reported by the Worker. This is the raw
/// `input_tokens` value and is independent from per-run upload totals.
pub session_context_tokens: u64,
pub context_window: u64,
@@ -264,9 +265,9 @@ pub struct App {
pub command_registry: CommandRegistry,
command_completion_selected: Option<usize>,
pub quit: bool,
/// 2-tap guard for `Ctrl-C` when the Pod is not running. First press
/// 2-tap guard for `Ctrl-C` when the Worker is not running. First press
/// records the instant; a second press within the timeout exits the
/// TUI (the Pod itself stays alive).
/// TUI (the Worker itself stays alive).
pub quit_confirm: Option<std::time::Instant>,
/// Full display history in render order.
pub blocks: Vec<Block>,
@@ -284,18 +285,18 @@ pub struct App {
pub rewind_picker: Option<RewindPickerState>,
rewind_request_pending: bool,
/// After a successful rewind restore, ignore any queued live-update events
/// until the authoritative Pod status/snapshot catches up. This prevents
/// until the authoritative Worker status/snapshot catches up. This prevents
/// old stream tail events that were already in transit from re-polluting the
/// just-restored display.
rewind_refresh_fence: bool,
greeting: Option<protocol::Greeting>,
/// In-TUI mirror of the Pod's session task store, reconstructed
/// In-TUI mirror of the Worker's session task store, reconstructed
/// directly from observed `TaskCreate` / `TaskUpdate` tool calls and
/// `[Session TaskStore snapshot]` system messages — no protocol
/// surface added on the Pod side.
/// surface added on the Worker side.
pub task_store: TaskStore,
/// Transient single-Pod transcript text selection. This is viewport-local
/// UI state only; it is never sent to the Pod, persisted, or appended to
/// Transient single-Worker transcript text selection. This is viewport-local
/// UI state only; it is never sent to the Worker, persisted, or appended to
/// session history/model context.
pub text_selection: TextSelectionState,
/// Whether the right-side task pane is currently open.
@@ -303,14 +304,14 @@ pub struct App {
/// Top entry index of the task pane's visible window. Clamped on
/// render so it never points past the end of the list.
pub task_pane_scroll: usize,
/// TUI-local FIFO of user inputs submitted while the Pod is already running.
/// Entries have not been sent to the Pod yet, so they remain editable/cancellable locally.
/// TUI-local FIFO of user inputs submitted while the Worker is already running.
/// Entries have not been sent to the Worker yet, so they remain editable/cancellable locally.
queued_inputs: VecDeque<QueuedInput>,
/// TUI-local readline-style composer input history. This is intentionally
/// client-side only: recalled entries are plain drafts until submitted again.
input_history: ComposerInputHistory,
/// User-data backed persistence for composer recall entries. The saved
/// contents are private input drafts and must not be logged or sent to Pod.
/// contents are private input drafts and must not be logged or sent to Worker.
input_history_store: Option<ComposerHistoryStore>,
/// Local submit state kept until the accepted run either completes
/// normally or reports that the empty assistant turn was rolled back.
@@ -321,11 +322,11 @@ pub struct App {
}
impl App {
pub fn new(pod_name: String) -> Self {
pub fn new(worker_name: String) -> Self {
Self {
pod_name,
worker_name,
connected: false,
pod_status: PodStatus::Idle,
worker_status: WorkerStatus::Idle,
running: false,
paused: false,
run_requests: 0,
@@ -367,8 +368,8 @@ impl App {
}
}
pub fn new_with_persistent_input_history(pod_name: String, workspace_root: &Path) -> Self {
let mut app = Self::new(pod_name);
pub fn new_with_persistent_input_history(worker_name: String, workspace_root: &Path) -> Self {
let mut app = Self::new(worker_name);
match ComposerHistoryStore::default_for_workspace(workspace_root) {
Ok(Some(store)) => {
match store.load() {
@@ -407,8 +408,8 @@ impl App {
}
#[cfg(test)]
fn new_with_input_history_store(pod_name: String, store: ComposerHistoryStore) -> Self {
let mut app = Self::new(pod_name);
fn new_with_input_history_store(worker_name: String, store: ComposerHistoryStore) -> Self {
let mut app = Self::new(worker_name);
match store.load() {
Ok(entries) => {
app.input_history = ComposerInputHistory::with_entries(entries);
@@ -442,10 +443,10 @@ impl App {
self.task_pane_scroll = self.task_pane_scroll.saturating_add(n);
}
pub fn set_pod_status(&mut self, status: PodStatus) {
self.pod_status = status;
self.running = status == PodStatus::Running;
self.paused = status == PodStatus::Paused;
pub fn set_worker_status(&mut self, status: WorkerStatus) {
self.worker_status = status;
self.running = status == WorkerStatus::Running;
self.paused = status == WorkerStatus::Paused;
if self.running {
self.quit_confirm = None;
}
@@ -639,7 +640,7 @@ impl App {
pub fn submit_input(&mut self) -> Option<Method> {
let segments = self.input.submit_segments();
if segments_are_blank(&segments) {
// Empty Enter only does something meaningful when the Pod
// Empty Enter only does something meaningful when the Worker
// is paused: resume the interrupted turn. Otherwise no-op.
if self.paused {
self.input_history.cancel_browse();
@@ -660,7 +661,7 @@ impl App {
}
fn method_for_run(&mut self, segments: Vec<Segment>) -> Method {
// TurnHeader / UserMessage blocks are pushed only after the Pod
// TurnHeader / UserMessage blocks are pushed only after the Worker
// emits `Event::UserMessage` from a committed `LogEntry::UserInput`.
// Locally we only clear the input buffer and forward the method,
// while remembering enough local state to undo the visible submit if
@@ -812,7 +813,7 @@ impl App {
pub fn push_error(&mut self, message: impl Into<String>) {
self.blocks.push(Block::Alert {
level: AlertLevel::Error,
source: AlertSource::Pod,
source: AlertSource::Worker,
message: message.into(),
});
}
@@ -856,7 +857,7 @@ impl App {
self.blocks.push(Block::TurnHeader {
turn: self.turn_index,
});
// Pod attaches the original `Vec<Segment>` to user
// Worker attaches the original `Vec<Segment>` to user
// messages from live submissions, so we can rebuild
// typed atoms (paste chips, refs) here. Seed history
// loaded post-compaction has no `segments` field —
@@ -971,7 +972,7 @@ impl App {
}
}
pub fn handle_pod_event(&mut self, event: Event) -> Option<Method> {
pub fn handle_worker_event(&mut self, event: Event) -> Option<Method> {
if self.rewind_refresh_fence && event_is_stale_after_rewind(&event) {
return None;
}
@@ -995,7 +996,7 @@ impl App {
self.assistant_streaming = false;
}
Event::TurnStart { .. } => {
self.set_pod_status(PodStatus::Running);
self.set_worker_status(WorkerStatus::Running);
self.run_requests += 1;
self.current_tool = None;
self.latest_llm_wait_event = None;
@@ -1175,7 +1176,7 @@ impl App {
};
self.blocks.push(Block::Alert {
level,
source: AlertSource::Pod,
source: AlertSource::Worker,
message: format!("orphan tool result ({id}): {summary}"),
});
}
@@ -1211,9 +1212,9 @@ impl App {
});
self.pending_submit_rollback = None;
self.reset_run_state(match result {
RunResult::Paused => PodStatus::Paused,
RunResult::Paused => WorkerStatus::Paused,
RunResult::Finished | RunResult::LimitReached | RunResult::RolledBack => {
PodStatus::Idle
WorkerStatus::Idle
}
});
if matches!(result, RunResult::Finished | RunResult::LimitReached) {
@@ -1283,11 +1284,11 @@ impl App {
} => {
self.rewind_refresh_fence = false;
self.restore_snapshot(&entries, greeting, in_flight);
self.set_pod_status(status);
self.set_worker_status(status);
}
Event::Status { status } => {
self.rewind_refresh_fence = false;
self.set_pod_status(status);
self.set_worker_status(status);
}
Event::Completions { kind, entries } => {
// Apply only if the popup is still on the same
@@ -1324,7 +1325,7 @@ impl App {
};
self.completion = None;
self.close_rewind_picker();
self.reset_run_state(self.pod_status);
self.reset_run_state(self.worker_status);
let mut message = if restored_composer {
format!(
"Rewound session: discarded {} log entries; restored selected input to composer.",
@@ -1343,20 +1344,20 @@ impl App {
}
self.blocks.push(Block::Alert {
level: AlertLevel::Warn,
source: AlertSource::Pod,
source: AlertSource::Worker,
message,
});
}
Event::PodsListed { .. } | Event::PodRestored { .. } => {}
Event::WorkersListed { .. } | Event::WorkerRestored { .. } => {}
Event::PeerRegistered { result } => {
let source = result
.get("source")
.and_then(serde_json::Value::as_str)
.unwrap_or("this Pod");
.unwrap_or("this Worker");
let peer = result
.get("peer")
.and_then(serde_json::Value::as_str)
.unwrap_or("peer Pod");
.unwrap_or("peer Worker");
self.flash_actionbar_notice(
format!("Peer metadata registered: `{source}` ↔ `{peer}`"),
ActionbarNoticeLevel::Info,
@@ -1372,8 +1373,8 @@ impl App {
None
}
fn reset_run_state(&mut self, status: PodStatus) {
self.set_pod_status(status);
fn reset_run_state(&mut self, status: WorkerStatus) {
self.set_worker_status(status);
self.run_requests = 0;
self.run_upload_tokens = 0;
self.run_output_tokens = 0;
@@ -1403,10 +1404,10 @@ impl App {
"Rolled back empty assistant turn; no local submitted input was available to restore."
.to_owned()
};
self.reset_run_state(PodStatus::Idle);
self.reset_run_state(WorkerStatus::Idle);
self.blocks.push(Block::Alert {
level: AlertLevel::Warn,
source: AlertSource::Pod,
source: AlertSource::Worker,
message: hint,
});
}
@@ -1706,15 +1707,17 @@ impl App {
pub fn request_rewind_picker(&mut self) -> Option<Method> {
if self.rewind_submit_pending() {
self.push_command_diagnostic("rewind is already applying; wait for the Pod response");
self.push_command_diagnostic(
"rewind is already applying; wait for the Worker response",
);
return None;
}
if !self.connected {
self.push_command_diagnostic("cannot rewind before the Pod is connected");
self.push_command_diagnostic("cannot rewind before the Worker is connected");
return None;
}
if self.running {
self.push_command_diagnostic("cannot rewind while the Pod is running");
self.push_command_diagnostic("cannot rewind while the Worker is running");
return None;
}
self.completion = None;
@@ -1731,7 +1734,7 @@ impl App {
pub fn cancel_rewind_picker(&mut self) {
if self.rewind_submit_pending() {
self.flash_actionbar_notice(
"Rewind is applying; wait for the Pod response.",
"Rewind is applying; wait for the Worker response.",
ActionbarNoticeLevel::Warn,
ActionbarNoticeSource::Tui,
Duration::from_secs(3),
@@ -1764,12 +1767,14 @@ impl App {
pub fn submit_rewind_picker(&mut self) -> Option<Method> {
if self.rewind_submit_pending() {
self.push_command_diagnostic("rewind is already applying; wait for the Pod response");
self.push_command_diagnostic(
"rewind is already applying; wait for the Worker response",
);
return None;
}
if self.paused {
self.push_command_diagnostic(
"cannot apply rewind while the Pod is paused; resume or wait for idle first",
"cannot apply rewind while the Worker is paused; resume or wait for idle first",
);
return None;
}
@@ -1783,7 +1788,9 @@ impl App {
return None;
};
if picker.applying {
self.push_command_diagnostic("rewind is already applying; wait for the Pod response");
self.push_command_diagnostic(
"rewind is already applying; wait for the Worker response",
);
return None;
}
let (target_id, expected_head_entries) = match picker.selected_target() {
@@ -1849,7 +1856,7 @@ impl App {
fn push_command_diagnostic(&mut self, message: impl Into<String>) {
self.blocks.push(Block::Alert {
level: AlertLevel::Warn,
source: AlertSource::Pod,
source: AlertSource::Worker,
message: format!("TUI command: {}", message.into()),
});
}
@@ -1971,7 +1978,7 @@ impl App {
self.apply_in_flight_snapshot(in_flight);
}
/// Restore after a successful destructive rewind. The Pod's
/// Restore after a successful destructive rewind. The Worker's
/// `RewindApplied` event already contains the authoritative post-rewind
/// session tail; always clear/replay from it even if this TUI instance has
/// somehow lost connect-time greeting metadata. Skipping the restore in
@@ -1993,7 +2000,7 @@ impl App {
if missing_greeting {
self.blocks.push(Block::Alert {
level: AlertLevel::Warn,
source: AlertSource::Pod,
source: AlertSource::Worker,
message: "Rewind applied, but greeting metadata was unavailable; restored the session tail without the header.".to_owned(),
});
}
@@ -2023,7 +2030,7 @@ impl App {
/// Drop the derived view in preparation for replaying a new
/// `SegmentStart` (compaction / fork). Greeting is preserved
/// because the Pod identity hasn't changed.
/// because the Worker identity hasn't changed.
fn reset_for_rotation(&mut self) {
let greeting = self.blocks.iter().find_map(|b| match b {
Block::Greeting(g) => Some(g.clone()),
@@ -2084,7 +2091,7 @@ impl App {
///
/// Kind-based routing replaces the old free-text `[Notification]` /
/// `[File: …]` parsing path: each kind maps directly to a typed
/// block (`Block::Notify`, `Block::PodEvent`, …).
/// block (`Block::Notify`, `Block::WorkerEvent`, …).
fn apply_system_item(&mut self, value: &serde_json::Value) {
let Ok(item) = serde_json::from_value::<session_store::SystemItem>(value.clone()) else {
// Unknown / forward-compat shape: fall back to rendering the
@@ -2101,8 +2108,8 @@ impl App {
session_store::SystemItem::Notification { message, .. } => {
self.blocks.push(Block::Notify { message });
}
session_store::SystemItem::PodEvent { event, .. } => {
self.blocks.push(Block::PodEvent { event });
session_store::SystemItem::WorkerEvent { event, .. } => {
self.blocks.push(Block::WorkerEvent { event });
}
session_store::SystemItem::FileAttachment { body, .. }
| session_store::SystemItem::Knowledge { body, .. }
@@ -2230,7 +2237,7 @@ fn rollback_input_preview(text: &str) -> String {
pub fn alert_source_label(source: AlertSource) -> &'static str {
match source {
AlertSource::Pod => "pod",
AlertSource::Worker => "worker",
AlertSource::Engine => "engine",
AlertSource::Compactor => "compactor",
AlertSource::AgentsMd => "AGENTS.md",
@@ -2244,7 +2251,7 @@ mod llm_wait_event_tests {
#[test]
fn llm_retry_updates_and_progress_clears_transient_status() {
let mut app = App::new("test".into());
app.handle_pod_event(Event::LlmRetry {
app.handle_worker_event(Event::LlmRetry {
llm_call: 2,
failed_attempt: 1,
max_attempts: 4,
@@ -2258,14 +2265,14 @@ mod llm_wait_event_tests {
Some("retrying LLM request after HTTP 504 (attempt 2/4 in 1.2s)")
);
app.handle_pod_event(Event::TextDelta { text: "ok".into() });
app.handle_worker_event(Event::TextDelta { text: "ok".into() });
assert!(app.latest_llm_wait_event.is_none());
}
#[test]
fn llm_continuation_updates_transient_status() {
let mut app = App::new("test".into());
app.handle_pod_event(Event::LlmContinuation {
app.handle_worker_event(Event::LlmContinuation {
llm_call: 3,
attempt: 1,
max_attempts: 3,
@@ -2289,7 +2296,7 @@ mod actionbar_notice_tests {
let duration = Duration::from_secs(2);
app.flash_actionbar_notice_at(
"Pod keeps running",
"Worker keeps running",
ActionbarNoticeLevel::Warn,
ActionbarNoticeSource::Tui,
now,
@@ -2297,7 +2304,7 @@ mod actionbar_notice_tests {
);
let notice = app.current_actionbar_notice(now).expect("notice is active");
assert_eq!(notice.text, "Pod keeps running");
assert_eq!(notice.text, "Worker keeps running");
assert_eq!(notice.level, ActionbarNoticeLevel::Warn);
assert_eq!(notice.source, ActionbarNoticeSource::Tui);
assert_eq!(notice.expires_at, now + duration);
@@ -2325,7 +2332,7 @@ mod rewind_refresh_tests {
text: "old post-target output".into(),
});
app.handle_pod_event(Event::RewindApplied {
app.handle_worker_event(Event::RewindApplied {
entries: vec![],
input: vec![Segment::text("selected rewind input")],
summary: summary(3),
@@ -2344,7 +2351,7 @@ mod rewind_refresh_tests {
text: "old live tail without greeting".into(),
});
app.handle_pod_event(Event::RewindApplied {
app.handle_worker_event(Event::RewindApplied {
entries: vec![],
input: vec![Segment::text("rewound input")],
summary: summary(1),
@@ -2367,7 +2374,7 @@ mod rewind_refresh_tests {
assert!(app.rewind_picker.as_ref().unwrap().applying);
assert!(app.submit_rewind_picker().is_none());
app.handle_pod_event(Event::Error {
app.handle_worker_event(Event::Error {
code: ErrorCode::InvalidRequest,
message: "stale rewind target".into(),
});
@@ -2387,20 +2394,20 @@ mod rewind_refresh_tests {
text: "old tail before rewind".into(),
});
app.handle_pod_event(Event::RewindApplied {
app.handle_worker_event(Event::RewindApplied {
entries: vec![],
input: vec![Segment::text("rewound input")],
summary: summary(2),
});
app.handle_pod_event(Event::TextDelta {
app.handle_worker_event(Event::TextDelta {
text: "stale tail after rewind".into(),
});
assert!(!blocks_contain(&app, "stale tail after rewind"));
app.handle_pod_event(Event::Status {
status: PodStatus::Idle,
app.handle_worker_event(Event::Status {
status: WorkerStatus::Idle,
});
app.handle_pod_event(Event::TextDelta {
app.handle_worker_event(Event::TextDelta {
text: "new live tail after status".into(),
});
assert!(blocks_contain(&app, "new live tail after status"));
@@ -2431,7 +2438,7 @@ mod rewind_refresh_tests {
fn greeting() -> protocol::Greeting {
protocol::Greeting {
pod_name: "test".into(),
worker_name: "test".into(),
cwd: "/tmp".into(),
provider: "mock".into(),
model: "mock".into(),
@@ -2916,7 +2923,7 @@ mod completion_flow_tests {
}
let _ = app.refresh_completion();
// Reply for a different kind shouldn't overwrite state.
app.handle_pod_event(Event::Completions {
app.handle_worker_event(Event::Completions {
kind: CompletionKind::Workflow,
entries: vec![CompletionEntry {
value: "stale".into(),
@@ -2939,10 +2946,10 @@ mod completion_flow_tests {
compacted_from: None,
};
app.handle_pod_event(Event::SegmentRotated {
app.handle_worker_event(Event::SegmentRotated {
entry: serde_json::to_value(start).expect("LogEntry is Serialize"),
});
app.handle_pod_event(Event::UserMessage {
app.handle_worker_event(Event::UserMessage {
segments: vec![Segment::text("first persisted message")],
});
@@ -2960,20 +2967,20 @@ mod completion_flow_tests {
let submitted = submit_text(&mut app, "please wait");
assert_eq!(input_text(&app), "");
app.handle_pod_event(Event::UserMessage {
app.handle_worker_event(Event::UserMessage {
segments: submitted,
});
// Simulate run-derived attachment display after the submitted user line.
app.blocks.push(Block::SystemMessage {
text: "[File: README.md]".into(),
});
app.handle_pod_event(Event::TurnStart { turn: 1 });
app.handle_pod_event(Event::Usage {
app.handle_worker_event(Event::TurnStart { turn: 1 });
app.handle_worker_event(Event::Usage {
input_tokens: Some(100),
output_tokens: Some(0),
cache_read_input_tokens: Some(40),
});
app.handle_pod_event(Event::RunEnd {
app.handle_worker_event(Event::RunEnd {
result: RunResult::RolledBack,
});
@@ -2987,7 +2994,7 @@ mod completion_flow_tests {
| Block::TurnStats { .. }
)));
assert!(warning_contains(&app, "restored your input"));
assert!(matches!(app.pod_status, PodStatus::Idle));
assert!(matches!(app.worker_status, WorkerStatus::Idle));
assert!(!app.running);
assert!(!app.paused);
assert_eq!(app.run_requests, 0);
@@ -3000,14 +3007,14 @@ mod completion_flow_tests {
fn rolled_back_run_does_not_overwrite_existing_unsent_input() {
let mut app = App::new("test".into());
let submitted = submit_text(&mut app, "original submit");
app.handle_pod_event(Event::UserMessage {
app.handle_worker_event(Event::UserMessage {
segments: submitted,
});
for c in "draft while running".chars() {
app.insert_char(c);
}
app.handle_pod_event(Event::RunEnd {
app.handle_worker_event(Event::RunEnd {
result: RunResult::RolledBack,
});
@@ -3028,10 +3035,10 @@ mod completion_flow_tests {
for result in [RunResult::Paused, RunResult::Finished] {
let mut app = App::new("test".into());
let submitted = submit_text(&mut app, "normal run");
app.handle_pod_event(Event::UserMessage {
app.handle_worker_event(Event::UserMessage {
segments: submitted,
});
app.handle_pod_event(Event::RunEnd { result });
app.handle_worker_event(Event::RunEnd { result });
assert_eq!(input_text(&app), "");
assert!(
@@ -3057,7 +3064,7 @@ mod completion_flow_tests {
#[test]
fn running_submit_is_queued_locally_and_clears_composer() {
let mut app = App::new("test".into());
app.set_pod_status(PodStatus::Running);
app.set_worker_status(WorkerStatus::Running);
insert_text(&mut app, "queued turn");
assert!(app.submit_input().is_none());
@@ -3070,11 +3077,11 @@ mod completion_flow_tests {
#[test]
fn finished_run_auto_sends_next_queued_input() {
let mut app = App::new("test".into());
app.set_pod_status(PodStatus::Running);
app.set_worker_status(WorkerStatus::Running);
insert_text(&mut app, "next turn");
assert!(app.submit_input().is_none());
let method = app.handle_pod_event(Event::RunEnd {
let method = app.handle_worker_event(Event::RunEnd {
result: RunResult::Finished,
});
@@ -3090,11 +3097,11 @@ mod completion_flow_tests {
#[test]
fn limit_reached_run_auto_sends_next_queued_input() {
let mut app = App::new("test".into());
app.set_pod_status(PodStatus::Running);
app.set_worker_status(WorkerStatus::Running);
insert_text(&mut app, "next after limit");
assert!(app.submit_input().is_none());
let method = app.handle_pod_event(Event::RunEnd {
let method = app.handle_worker_event(Event::RunEnd {
result: RunResult::LimitReached,
});
@@ -3111,11 +3118,11 @@ mod completion_flow_tests {
fn paused_and_rolled_back_run_do_not_auto_send_queue() {
for result in [RunResult::Paused, RunResult::RolledBack] {
let mut app = App::new("test".into());
app.set_pod_status(PodStatus::Running);
app.set_worker_status(WorkerStatus::Running);
insert_text(&mut app, "held turn");
assert!(app.submit_input().is_none());
let method = app.handle_pod_event(Event::RunEnd { result });
let method = app.handle_worker_event(Event::RunEnd { result });
assert!(method.is_none());
assert_eq!(app.queued_input_count(), 1);
@@ -3126,7 +3133,7 @@ mod completion_flow_tests {
#[test]
fn paused_empty_submit_still_resumes_immediately() {
let mut app = App::new("test".into());
app.set_pod_status(PodStatus::Paused);
app.set_worker_status(WorkerStatus::Paused);
assert!(matches!(app.submit_input(), Some(Method::Resume)));
assert_eq!(app.queued_input_count(), 0);
@@ -3135,7 +3142,7 @@ mod completion_flow_tests {
#[test]
fn queued_input_can_be_restored_to_composer_or_cleared() {
let mut app = App::new("test".into());
app.set_pod_status(PodStatus::Running);
app.set_worker_status(WorkerStatus::Running);
insert_text(&mut app, "edit me");
assert!(app.submit_input().is_none());
@@ -3198,14 +3205,14 @@ mod completion_flow_tests {
compacted_from: None,
};
let session_start_value = serde_json::to_value(&session_start).unwrap();
app.handle_pod_event(Event::Snapshot {
app.handle_worker_event(Event::Snapshot {
greeting: test_greeting(),
entries: vec![session_start_value],
status: PodStatus::Running,
status: WorkerStatus::Running,
in_flight: Default::default(),
});
assert!(matches!(app.pod_status, PodStatus::Running));
assert!(matches!(app.worker_status, WorkerStatus::Running));
assert!(app.running);
assert!(matches!(
app.blocks.get(1),
@@ -3216,10 +3223,10 @@ mod completion_flow_tests {
#[test]
fn snapshot_in_flight_blocks_continue_with_live_deltas() {
let mut app = App::new("test".into());
app.handle_pod_event(Event::Snapshot {
app.handle_worker_event(Event::Snapshot {
greeting: test_greeting(),
entries: Vec::new(),
status: PodStatus::Running,
status: WorkerStatus::Running,
in_flight: InFlightSnapshot {
blocks: vec![
InFlightBlock::Thinking {
@@ -3240,9 +3247,9 @@ mod completion_flow_tests {
},
});
app.handle_pod_event(Event::TextDelta { text: "lo".into() });
app.handle_pod_event(Event::ThinkingDelta { text: "?".into() });
app.handle_pod_event(Event::ToolCallArgsDelta {
app.handle_worker_event(Event::TextDelta { text: "lo".into() });
app.handle_worker_event(Event::ThinkingDelta { text: "?".into() });
app.handle_worker_event(Event::ToolCallArgsDelta {
id: "call_1".into(),
json: r#"\":\"src/lib.rs\"}"#.into(),
});
@@ -3269,7 +3276,7 @@ mod completion_flow_tests {
"slug": "build",
"body": "[Workflow /build]\nRun the build",
});
app.handle_pod_event(Event::SystemItem { item });
app.handle_worker_event(Event::SystemItem { item });
assert!(matches!(
app.blocks.as_slice(),
@@ -3285,7 +3292,7 @@ mod completion_flow_tests {
"message": "hi",
"body": "[Notification] hi",
});
app.handle_pod_event(Event::SystemItem { item });
app.handle_worker_event(Event::SystemItem { item });
assert!(matches!(
app.blocks.as_slice(),
[Block::Notify { message }] if message == "hi"
@@ -3293,20 +3300,20 @@ mod completion_flow_tests {
}
#[test]
fn live_system_item_pod_event_appends_pod_event_block() {
fn live_system_item_worker_event_appends_worker_event_block() {
let mut app = App::new("test".into());
let item = serde_json::json!({
"kind": "pod_event",
"event": { "kind": "turn_ended", "pod_name": "child" },
"body": "[Notification] pod `child` finished a turn",
"kind": "worker_event",
"event": { "kind": "turn_ended", "worker_name": "child" },
"body": "[Notification] worker `child` finished a turn",
});
app.handle_pod_event(Event::SystemItem { item });
app.handle_worker_event(Event::SystemItem { item });
assert_eq!(app.blocks.len(), 1);
match &app.blocks[0] {
Block::PodEvent {
event: protocol::PodEvent::TurnEnded { pod_name },
} => assert_eq!(pod_name, "child"),
_ => panic!("expected a PodEvent block"),
Block::WorkerEvent {
event: protocol::WorkerEvent::TurnEnded { worker_name },
} => assert_eq!(worker_name, "child"),
_ => panic!("expected a WorkerEvent block"),
}
}
@@ -3315,8 +3322,8 @@ mod completion_flow_tests {
let mut app = App::new("test".into());
let id = uuid::Uuid::parse_str("12345678-1234-5678-1234-567812345678").unwrap();
app.handle_pod_event(Event::CompactStart);
app.handle_pod_event(Event::CompactDone { new_segment_id: id });
app.handle_worker_event(Event::CompactStart);
app.handle_worker_event(Event::CompactDone { new_segment_id: id });
assert_eq!(compact_block_count(&app), 1);
assert!(matches!(
@@ -3332,8 +3339,8 @@ mod completion_flow_tests {
fn compact_failed_replaces_live_block() {
let mut app = App::new("test".into());
app.handle_pod_event(Event::CompactStart);
app.handle_pod_event(Event::CompactFailed {
app.handle_worker_event(Event::CompactStart);
app.handle_worker_event(Event::CompactFailed {
error: "provider 429".into(),
});
@@ -3351,8 +3358,8 @@ mod completion_flow_tests {
fn shutdown_marks_live_compact_incomplete() {
let mut app = App::new("test".into());
app.handle_pod_event(Event::CompactStart);
app.handle_pod_event(Event::Shutdown);
app.handle_worker_event(Event::CompactStart);
app.handle_worker_event(Event::Shutdown);
assert!(app.quit);
assert!(matches!(
@@ -3372,7 +3379,7 @@ mod completion_flow_tests {
fn test_greeting() -> protocol::Greeting {
protocol::Greeting {
pod_name: "test".into(),
worker_name: "test".into(),
cwd: "/tmp".into(),
provider: "test-provider".into(),
model: "test-model".into(),
@@ -3390,10 +3397,10 @@ mod completion_flow_tests {
greeting.context_window = 123_000;
greeting.context_tokens = 45_000;
app.handle_pod_event(Event::Snapshot {
app.handle_worker_event(Event::Snapshot {
entries: Vec::new(),
greeting,
status: PodStatus::Idle,
status: WorkerStatus::Idle,
in_flight: Default::default(),
});
@@ -3405,7 +3412,7 @@ mod completion_flow_tests {
fn usage_updates_session_context_tokens_without_cache_discount() {
let mut app = App::new("test".into());
app.handle_pod_event(Event::Usage {
app.handle_worker_event(Event::Usage {
input_tokens: Some(42_000),
output_tokens: Some(9),
cache_read_input_tokens: Some(40_000),
@@ -3420,7 +3427,7 @@ mod completion_flow_tests {
fn memory_worker_event_updates_actionbar_state() {
let mut app = App::new("test".into());
app.handle_pod_event(Event::MemoryWorker(protocol::MemoryWorkerEvent {
app.handle_worker_event(Event::MemoryWorker(protocol::MemoryWorkerEvent {
worker: "extract".into(),
status: "done".into(),
run_id: "00000000-0000-0000-0000-000000000000".into(),
@@ -3441,7 +3448,7 @@ mod completion_flow_tests {
let mut app = App::new("test".into());
app.session_context_tokens = 42_000;
app.handle_pod_event(Event::CompactDone {
app.handle_worker_event(Event::CompactDone {
new_segment_id: uuid::Uuid::nil(),
});
@@ -3453,8 +3460,8 @@ mod completion_flow_tests {
let mut app = App::new("test".into());
app.session_context_tokens = 42_000;
app.handle_pod_event(Event::TurnStart { turn: 1 });
app.handle_pod_event(Event::RunEnd {
app.handle_worker_event(Event::TurnStart { turn: 1 });
app.handle_worker_event(Event::RunEnd {
result: RunResult::Finished,
});
@@ -3464,11 +3471,11 @@ mod completion_flow_tests {
#[test]
fn live_task_create_updates_task_store() {
let mut app = App::new("test".into());
app.handle_pod_event(Event::ToolCallStart {
app.handle_worker_event(Event::ToolCallStart {
id: "c1".into(),
name: "TaskCreate".into(),
});
app.handle_pod_event(Event::ToolCallDone {
app.handle_worker_event(Event::ToolCallDone {
id: "c1".into(),
name: "TaskCreate".into(),
arguments: r#"{"subject":"impl tasks","description":"do it"}"#.into(),
@@ -3491,11 +3498,11 @@ mod completion_flow_tests {
} else {
"TaskUpdate"
};
app.handle_pod_event(Event::ToolCallStart {
app.handle_worker_event(Event::ToolCallStart {
id: id.into(),
name: name.into(),
});
app.handle_pod_event(Event::ToolCallDone {
app.handle_worker_event(Event::ToolCallDone {
id: id.into(),
name: name.into(),
arguments: args.into(),
@@ -3511,11 +3518,11 @@ mod completion_flow_tests {
fn live_system_snapshot_replaces_task_store() {
let mut app = App::new("test".into());
// Stale entry that the snapshot must wipe out.
app.handle_pod_event(Event::ToolCallStart {
app.handle_worker_event(Event::ToolCallStart {
id: "c1".into(),
name: "TaskCreate".into(),
});
app.handle_pod_event(Event::ToolCallDone {
app.handle_worker_event(Event::ToolCallDone {
id: "c1".into(),
name: "TaskCreate".into(),
arguments: r#"{"subject":"stale","description":""}"#.into(),
@@ -3528,7 +3535,7 @@ mod completion_flow_tests {
\"description\": \"d\"\n }\n ]\n}\n```\n";
// Snapshot text injected as a workflow body (kind doesn't matter
// for task-store parsing, only the text contents do).
app.handle_pod_event(Event::SystemItem {
app.handle_worker_event(Event::SystemItem {
item: serde_json::json!({
"kind": "workflow",
"slug": "task-snapshot",
@@ -3547,11 +3554,11 @@ mod completion_flow_tests {
let mut app = App::new("test".into());
// Live tool call before the snapshot lands — restore must wipe
// this so it doesn't double-count after replay.
app.handle_pod_event(Event::ToolCallStart {
app.handle_worker_event(Event::ToolCallStart {
id: "live".into(),
name: "TaskCreate".into(),
});
app.handle_pod_event(Event::ToolCallDone {
app.handle_worker_event(Event::ToolCallDone {
id: "live".into(),
name: "TaskCreate".into(),
arguments: r#"{"subject":"live","description":""}"#.into(),
@@ -3589,10 +3596,10 @@ mod completion_flow_tests {
},
}),
];
app.handle_pod_event(Event::Snapshot {
app.handle_worker_event(Event::Snapshot {
greeting: test_greeting(),
entries: assistant_item_entries,
status: PodStatus::Running,
status: WorkerStatus::Running,
in_flight: Default::default(),
});
+5 -5
View File
@@ -9,7 +9,7 @@
use std::time::Instant;
use protocol::{AlertLevel, AlertSource, Greeting, PodEvent, Segment};
use protocol::{AlertLevel, AlertSource, Greeting, Segment, WorkerEvent};
pub enum Block {
Greeting(Greeting),
@@ -25,16 +25,16 @@ pub enum Block {
SystemMessage {
text: String,
},
/// Echo of `Method::Notify` received by this Pod, surfaced as a log
/// Echo of `Method::Notify` received by this Worker, surfaced as a log
/// element so subscribers see the external input that drove any
/// following auto-kicked turn.
Notify {
message: String,
},
/// Echo of `Method::PodEvent` received by this Pod. Same role as
/// Echo of `Method::WorkerEvent` received by this Worker. Same role as
/// `Notify` — an input log element, not a turn-control signal.
PodEvent {
event: PodEvent,
WorkerEvent {
event: WorkerEvent,
},
AssistantText {
text: String,
+12 -12
View File
@@ -142,7 +142,7 @@ impl CommandRegistry {
name: "compact",
aliases: &[],
usage: "compact",
description: "Request immediate Pod context compaction.",
description: "Request immediate Worker context compaction.",
argument_parser: compact_args,
can_execute: compact_available,
executor: compact_command,
@@ -159,8 +159,8 @@ impl CommandRegistry {
registry.register(CommandSpec {
name: "peer",
aliases: &[],
usage: "peer <pod-name>",
description: "Register another existing Pod as a reciprocal metadata peer.",
usage: "peer <worker-name>",
description: "Register another existing Worker as a reciprocal metadata peer.",
argument_parser: peer_args,
can_execute: peer_available,
executor: peer_command,
@@ -317,7 +317,7 @@ fn peer_args(raw: &str) -> Result<CommandArgs, CommandDiagnostic> {
Ok(args)
} else {
Err(CommandDiagnostic::new(
"Invalid arguments. Usage: peer <pod-name>",
"Invalid arguments. Usage: peer <worker-name>",
))
}
}
@@ -325,17 +325,17 @@ fn peer_args(raw: &str) -> Result<CommandArgs, CommandDiagnostic> {
fn compact_available(environment: &CommandEnvironment) -> Result<(), CommandDiagnostic> {
if !environment.connected {
return Err(CommandDiagnostic::new(
"Cannot compact: not connected to a Pod.",
"Cannot compact: not connected to a Worker.",
));
}
if environment.running {
return Err(CommandDiagnostic::new(
"Cannot compact while the Pod is running.",
"Cannot compact while the Worker is running.",
));
}
if environment.paused {
return Err(CommandDiagnostic::new(
"Cannot compact while the Pod is paused; resume or start a fresh turn first.",
"Cannot compact while the Worker is paused; resume or start a fresh turn first.",
));
}
Ok(())
@@ -344,12 +344,12 @@ fn compact_available(environment: &CommandEnvironment) -> Result<(), CommandDiag
fn rewind_available(environment: &CommandEnvironment) -> Result<(), CommandDiagnostic> {
if !environment.connected {
return Err(CommandDiagnostic::new(
"Cannot rewind before the Pod is connected.",
"Cannot rewind before the Worker is connected.",
));
}
if environment.running {
return Err(CommandDiagnostic::new(
"Cannot rewind while the Pod is running.",
"Cannot rewind while the Worker is running.",
));
}
Ok(())
@@ -358,12 +358,12 @@ fn rewind_available(environment: &CommandEnvironment) -> Result<(), CommandDiagn
fn peer_available(environment: &CommandEnvironment) -> Result<(), CommandDiagnostic> {
if !environment.connected {
return Err(CommandDiagnostic::new(
"Cannot register a peer before the Pod is connected.",
"Cannot register a peer before the Worker is connected.",
));
}
if environment.running {
return Err(CommandDiagnostic::new(
"Cannot register a peer while the Pod is running.",
"Cannot register a peer while the Worker is running.",
));
}
Ok(())
@@ -596,7 +596,7 @@ mod tests {
let registry = CommandRegistry::builtins();
let result = registry.dispatch("help peer", &env());
assert!(result.method.is_none());
assert!(result.diagnostics[0].message.contains("peer <pod-name>"));
assert!(result.diagnostics[0].message.contains("peer <worker-name>"));
assert!(result.diagnostics[0].message.contains("metadata peer"));
}
+1 -1
View File
@@ -32,7 +32,7 @@ impl ComposerEditAction {
}
}
/// Shared readline-style composer editing keymap used by the normal Pod TUI
/// Shared readline-style composer editing keymap used by the normal Worker TUI
/// and the workspace panel. Callers still own higher-level routing such as
/// completion popups, Enter submission, Tab target switching, Esc focus, and
/// row/list navigation.
+126 -125
View File
@@ -18,14 +18,14 @@ use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
use crossterm::{Command, execute};
#[cfg(feature = "e2e-test")]
use protocol::{Event, Greeting, RewindSummary, RewindTarget, RewindTargetId, Segment};
use protocol::{Method, PodStatus};
use protocol::{Method, WorkerStatus};
use ratatui::Terminal;
use ratatui::backend::CrosstermBackend;
use session_store::SegmentId;
use tokio::sync::mpsc;
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
use client::{PodClient, PodRuntimeCommand};
use client::{WorkerClient, WorkerRuntimeCommand};
use crate::app::{ActionbarNoticeLevel, ActionbarNoticeSource, App};
use crate::composer_keys::{ComposerEditAction, composer_edit_action};
@@ -35,9 +35,9 @@ use crate::{picker, spawn, ui};
pub(crate) type ConsoleTerminal = Terminal<CrosstermBackend<io::Stdout>>;
/// Narrow request bridge used when the workspace Dashboard opens a Pod Console.
/// Narrow request bridge used when the workspace Dashboard opens a Worker Console.
pub(crate) struct DashboardConsoleOpenRequest {
pub(crate) pod_name: String,
pub(crate) worker_name: String,
pub(crate) socket_override: Option<PathBuf>,
}
@@ -128,39 +128,39 @@ fn copy_selection_to_terminal(app: &mut App) -> bool {
copy_selection_to_writer(app, &mut stdout)
}
fn resolve_socket(pod_name: &str, override_path: Option<PathBuf>) -> PathBuf {
fn resolve_socket(worker_name: &str, override_path: Option<PathBuf>) -> PathBuf {
if let Some(p) = override_path {
return p;
}
manifest::paths::pod_socket_path(pod_name).unwrap_or_else(|| {
manifest::paths::pod_socket_path(worker_name).unwrap_or_else(|| {
PathBuf::from("/tmp")
.join("yoi")
.join(pod_name)
.join(worker_name)
.join("sock")
})
}
pub(crate) async fn run_pod_name(
pod_name: String,
pub(crate) async fn run_worker_name(
worker_name: String,
socket_override: Option<PathBuf>,
runtime_command: PodRuntimeCommand,
runtime_command: WorkerRuntimeCommand,
) -> Result<(), Box<dyn std::error::Error>> {
#[cfg(feature = "e2e-test")]
if std::env::var_os("YOI_TUI_TEST_REWIND_FIXTURE").is_some() {
let mut terminal = enter_fullscreen()?;
terminal.clear()?;
let result = run_e2e_rewind_fixture(&mut terminal, pod_name).await;
let result = run_e2e_rewind_fixture(&mut terminal, worker_name).await;
let _ = leave_fullscreen(&mut terminal);
return result;
}
if let Some(client) = try_connect_live_pod(&pod_name, socket_override.clone()).await {
if let Some(client) = try_connect_live_pod(&worker_name, socket_override.clone()).await {
let mut terminal = enter_fullscreen()?;
run_connected_pod(&mut terminal, pod_name, client, runtime_command.clone()).await?;
run_connected_pod(&mut terminal, worker_name, client, runtime_command.clone()).await?;
return Ok(());
}
let ready = match spawn::run_pod_name(pod_name, runtime_command.clone()).await? {
let ready = match spawn::run_worker_name(worker_name, runtime_command.clone()).await? {
SpawnOutcome::Ready(r) => r,
SpawnOutcome::Cancelled => return Ok(()),
};
@@ -173,12 +173,12 @@ pub(crate) async fn run_pod_name(
async fn run_connected_pod(
terminal: &mut ConsoleTerminal,
pod_name: String,
client: PodClient,
runtime_command: PodRuntimeCommand,
worker_name: String,
client: WorkerClient,
runtime_command: WorkerRuntimeCommand,
) -> Result<(), Box<dyn std::error::Error>> {
let workspace_root = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
let mut app = App::new_with_persistent_input_history(pod_name, &workspace_root);
let mut app = App::new_with_persistent_input_history(worker_name, &workspace_root);
app.connected = true;
run_loop(terminal, &mut app, client, runtime_command).await
}
@@ -186,29 +186,29 @@ async fn run_connected_pod(
pub(crate) async fn open_from_dashboard(
terminal: &mut ConsoleTerminal,
request: DashboardConsoleOpenRequest,
runtime_command: PodRuntimeCommand,
runtime_command: WorkerRuntimeCommand,
) -> Result<(), Box<dyn std::error::Error>> {
let DashboardConsoleOpenRequest {
pod_name,
worker_name,
socket_override,
} = request;
if let Some(client) = try_connect_live_pod(&pod_name, socket_override).await {
return run_connected_pod(terminal, pod_name, client, runtime_command.clone()).await;
if let Some(client) = try_connect_live_pod(&worker_name, socket_override).await {
return run_connected_pod(terminal, worker_name, client, runtime_command.clone()).await;
}
let ready =
spawn_pod_name_from_fullscreen(terminal, &pod_name, runtime_command.clone()).await?;
spawn_worker_name_from_fullscreen(terminal, &worker_name, runtime_command.clone()).await?;
run_ready_pod(terminal, ready, runtime_command).await
}
async fn spawn_pod_name_from_fullscreen(
async fn spawn_worker_name_from_fullscreen(
terminal: &mut ConsoleTerminal,
pod_name: &str,
runtime_command: PodRuntimeCommand,
worker_name: &str,
runtime_command: WorkerRuntimeCommand,
) -> Result<SpawnReady, Box<dyn std::error::Error>> {
leave_fullscreen(terminal)?;
let outcome = spawn::run_pod_name(pod_name.to_string(), runtime_command).await;
let outcome = spawn::run_worker_name(worker_name.to_string(), runtime_command).await;
enter_fullscreen_existing(terminal)?;
terminal.clear()?;
@@ -219,11 +219,11 @@ async fn spawn_pod_name_from_fullscreen(
}
async fn try_connect_live_pod(
pod_name: &str,
worker_name: &str,
socket_override: Option<PathBuf>,
) -> Option<PodClient> {
let preferred_socket = resolve_socket(pod_name, socket_override.clone());
connect_live_pod(pod_name, preferred_socket, socket_override.is_none())
) -> Option<WorkerClient> {
let preferred_socket = resolve_socket(worker_name, socket_override.clone());
connect_live_pod(worker_name, preferred_socket, socket_override.is_none())
.await
.map(|(_, client)| client)
}
@@ -233,7 +233,7 @@ struct NestedOpenCancelled;
impl std::fmt::Display for NestedOpenCancelled {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Pod open was cancelled")
f.write_str("Worker open was cancelled")
}
}
@@ -242,57 +242,57 @@ impl std::error::Error for NestedOpenCancelled {}
async fn run_ready_pod(
terminal: &mut ConsoleTerminal,
ready: SpawnReady,
runtime_command: PodRuntimeCommand,
runtime_command: WorkerRuntimeCommand,
) -> Result<(), Box<dyn std::error::Error>> {
let SpawnReady {
pod_name,
worker_name,
socket_path,
} = ready;
run(terminal, pod_name, &socket_path, runtime_command).await
run(terminal, worker_name, &socket_path, runtime_command).await
}
async fn connect_live_pod(
pod_name: &str,
worker_name: &str,
preferred_socket: PathBuf,
allow_registry_fallback: bool,
) -> Option<(PathBuf, PodClient)> {
if let Ok(client) = PodClient::connect(&preferred_socket).await {
) -> Option<(PathBuf, WorkerClient)> {
if let Ok(client) = WorkerClient::connect(&preferred_socket).await {
return Some((preferred_socket, client));
}
if !allow_registry_fallback {
return None;
}
let registry_socket = picker::live_socket_for_pod(pod_name)?;
let registry_socket = picker::live_socket_for_pod(worker_name)?;
if registry_socket == preferred_socket {
return None;
}
PodClient::connect(&registry_socket)
WorkerClient::connect(&registry_socket)
.await
.ok()
.map(|client| (registry_socket, client))
}
pub(crate) async fn run_resume(
runtime_command: PodRuntimeCommand,
runtime_command: WorkerRuntimeCommand,
workspace_root: PathBuf,
all: bool,
) -> Result<(), Box<dyn std::error::Error>> {
// Pick a Pod in its own inline viewport, dropping the viewport before
// Pick a Worker in its own inline viewport, dropping the viewport before
// attaching/restoring so each phase gets fresh vertical room.
let picker_options = if all {
picker::PickerOptions::all()
} else {
picker::PickerOptions::workspace(workspace_root)
};
let (pod_name, socket_override) = match picker::run(picker_options).await? {
let (worker_name, socket_override) = match picker::run(picker_options).await? {
PickerOutcome::Picked {
pod_name,
worker_name,
socket_override,
} => (pod_name, socket_override),
} => (worker_name, socket_override),
PickerOutcome::Cancelled => return Ok(()),
};
run_pod_name(pod_name, socket_override, runtime_command).await
run_worker_name(worker_name, socket_override, runtime_command).await
}
pub(crate) fn is_recoverable_dashboard_open_error(error: &(dyn Error + 'static)) -> bool {
@@ -301,32 +301,33 @@ pub(crate) fn is_recoverable_dashboard_open_error(error: &(dyn Error + 'static))
pub(crate) async fn run_spawn(
resume_from: Option<SegmentId>,
pod_name: Option<String>,
worker_name: Option<String>,
profile: Option<String>,
runtime_command: PodRuntimeCommand,
runtime_command: WorkerRuntimeCommand,
) -> Result<(), Box<dyn std::error::Error>> {
#[cfg(feature = "e2e-test")]
if std::env::var_os("YOI_TUI_TEST_REWIND_FIXTURE").is_some() {
let mut terminal = enter_fullscreen()?;
terminal.clear()?;
let fixture_pod_name = pod_name.unwrap_or_else(|| "e2e-rewind".to_string());
let result = run_e2e_rewind_fixture(&mut terminal, fixture_pod_name).await;
let fixture_worker_name = worker_name.unwrap_or_else(|| "e2e-rewind".to_string());
let result = run_e2e_rewind_fixture(&mut terminal, fixture_worker_name).await;
let _ = leave_fullscreen(&mut terminal);
return result;
}
let ready = match spawn::run(resume_from, pod_name, profile, runtime_command.clone()).await? {
let ready = match spawn::run(resume_from, worker_name, profile, runtime_command.clone()).await?
{
SpawnOutcome::Ready(r) => r,
SpawnOutcome::Cancelled => return Ok(()),
};
let SpawnReady {
pod_name,
worker_name,
socket_path,
} = ready;
let mut terminal = enter_fullscreen()?;
let result = run(&mut terminal, pod_name, &socket_path, runtime_command).await;
let result = run(&mut terminal, worker_name, &socket_path, runtime_command).await;
// Leave alt-screen explicitly before `main`'s terminal restore path.
let _ = execute!(
@@ -383,17 +384,17 @@ pub(crate) fn leave_dashboard_fullscreen(terminal: &mut ConsoleTerminal) -> io::
async fn run(
terminal: &mut ConsoleTerminal,
pod_name: String,
worker_name: String,
socket_path: &std::path::Path,
runtime_command: PodRuntimeCommand,
runtime_command: WorkerRuntimeCommand,
) -> Result<(), Box<dyn std::error::Error>> {
let workspace_root = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
let mut app = App::new_with_persistent_input_history(pod_name, &workspace_root);
let mut app = App::new_with_persistent_input_history(worker_name, &workspace_root);
match PodClient::connect(socket_path).await {
match WorkerClient::connect(socket_path).await {
Ok(client) => {
app.connected = true;
// The Pod sends `Event::Snapshot` automatically on connect;
// The Worker sends `Event::Snapshot` automatically on connect;
// no explicit method call is required to fetch history.
run_loop(terminal, &mut app, client, runtime_command).await?;
}
@@ -470,16 +471,16 @@ fn read_terminal_events(stop: Arc<AtomicBool>, tx: mpsc::UnboundedSender<Termina
#[cfg(feature = "e2e-test")]
async fn run_e2e_rewind_fixture(
terminal: &mut ConsoleTerminal,
pod_name: String,
worker_name: String,
) -> Result<(), Box<dyn std::error::Error>> {
let workspace_root = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
let mut app = App::new_with_persistent_input_history(pod_name.clone(), &workspace_root);
let mut app = App::new_with_persistent_input_history(worker_name.clone(), &workspace_root);
app.connected = true;
app.handle_pod_event(Event::Snapshot {
app.handle_worker_event(Event::Snapshot {
entries: Vec::new(),
status: PodStatus::Idle,
status: WorkerStatus::Idle,
greeting: Greeting {
pod_name: pod_name.clone(),
worker_name: worker_name.clone(),
cwd: workspace_root.display().to_string(),
provider: "e2e-fixture".to_string(),
model: "canned".to_string(),
@@ -500,9 +501,9 @@ async fn run_e2e_rewind_fixture(
let apply_delay = Duration::from_millis(400);
#[cfg(feature = "e2e-test")]
crate::e2e_observer::emit(
"single_pod",
"single_worker",
"rewind_fixture_ready",
serde_json::json!({ "pod": pod_name.clone() }),
serde_json::json!({ "worker": worker_name.clone() }),
);
terminal.draw(|frame| ui::draw(frame, &mut app))?;
@@ -539,7 +540,7 @@ async fn run_e2e_rewind_fixture(
if let Some(method) = handle_key(&mut app, key) {
match method {
Method::ListRewindTargets => {
app.handle_pod_event(Event::RewindTargets {
app.handle_worker_event(Event::RewindTargets {
head_entries: 3,
targets: vec![RewindTarget {
id: target_id.clone(),
@@ -554,7 +555,7 @@ async fn run_e2e_rewind_fixture(
}],
});
crate::e2e_observer::emit(
"single_pod",
"single_worker",
"rewind_picker_opened",
serde_json::json!({
"targets": 1,
@@ -569,7 +570,7 @@ async fn run_e2e_rewind_fixture(
rewind_submit_count += 1;
pending_apply = Some(std::time::Instant::now());
crate::e2e_observer::emit(
"single_pod",
"single_worker",
"rewind_submit_sent",
serde_json::json!({
"segment_id": target.segment_id.to_string(),
@@ -583,7 +584,7 @@ async fn run_e2e_rewind_fixture(
}
} else if duplicate_enter_pending {
crate::e2e_observer::emit(
"single_pod",
"single_worker",
"rewind_duplicate_enter_suppressed",
serde_json::json!({ "submit_count": rewind_submit_count }),
);
@@ -601,7 +602,7 @@ async fn run_e2e_rewind_fixture(
if let Some(submitted_at) = pending_apply {
if submitted_at.elapsed() >= apply_delay {
app.handle_pod_event(Event::RewindApplied {
app.handle_worker_event(Event::RewindApplied {
entries: Vec::new(),
input: vec![Segment::text("rewind-live-refresh")],
summary: RewindSummary {
@@ -613,7 +614,7 @@ async fn run_e2e_rewind_fixture(
pending_apply = None;
let composer_text = Segment::flatten_to_text(&app.input.submit_segments());
crate::e2e_observer::emit(
"single_pod",
"single_worker",
"rewind_applied",
serde_json::json!({
"composer_text": composer_text,
@@ -644,7 +645,7 @@ enum E2eRewindInput {
enum LoopInput<P> {
Terminal(TerminalEventResult),
Pod(Option<P>),
Worker(Option<P>),
}
async fn next_loop_input<P, F>(
@@ -666,15 +667,15 @@ where
))
}))
}
event = pod_next, if connected => LoopInput::Pod(event),
event = pod_next, if connected => LoopInput::Worker(event),
}
}
async fn drain_terminal_events(
app: &mut App,
client: &mut PodClient,
client: &mut WorkerClient,
term_rx: &mut mpsc::UnboundedReceiver<TerminalEventResult>,
runtime_command: &PodRuntimeCommand,
runtime_command: &WorkerRuntimeCommand,
) -> Result<bool, Box<dyn std::error::Error>> {
let mut handled = false;
for _ in 0..TERMINAL_EVENT_DRAIN_LIMIT {
@@ -698,16 +699,16 @@ async fn drain_terminal_events(
Ok(handled)
}
async fn drain_pod_events(
async fn drain_worker_events(
app: &mut App,
client: &mut PodClient,
client: &mut WorkerClient,
) -> Result<bool, Box<dyn std::error::Error>> {
let mut handled = false;
for _ in 0..POD_EVENT_DRAIN_LIMIT {
match client.try_next_event() {
Some(ev) => {
handled = true;
if let Some(method) = app.handle_pod_event(ev) {
if let Some(method) = app.handle_worker_event(ev) {
client.send(&method).await?;
}
}
@@ -720,8 +721,8 @@ async fn drain_pod_events(
async fn run_loop(
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
app: &mut App,
mut client: PodClient,
runtime_command: PodRuntimeCommand,
mut client: WorkerClient,
runtime_command: WorkerRuntimeCommand,
) -> Result<(), Box<dyn std::error::Error>> {
let (_terminal_reader, mut term_rx) = TerminalEventReader::spawn()?;
@@ -737,8 +738,8 @@ async fn run_loop(
if app.quit {
break;
}
let handled_pod_event = drain_pod_events(app, &mut client).await?;
if handled_term_event || handled_pod_event {
let handled_worker_event = drain_worker_events(app, &mut client).await?;
if handled_term_event || handled_worker_event {
terminal.draw(|f| ui::draw(f, app))?;
continue;
}
@@ -747,9 +748,9 @@ async fn run_loop(
LoopInput::Terminal(term_event) => {
handle_terminal_event(app, &mut client, term_event?, &runtime_command).await?;
}
LoopInput::Pod(event) => match event {
LoopInput::Worker(event) => match event {
Some(ev) => {
if let Some(method) = app.handle_pod_event(ev) {
if let Some(method) = app.handle_worker_event(ev) {
client.send(&method).await?;
}
}
@@ -769,9 +770,9 @@ async fn run_loop(
async fn handle_terminal_event(
app: &mut App,
client: &mut PodClient,
client: &mut WorkerClient,
event: TermEvent,
_runtime_command: &PodRuntimeCommand,
_runtime_command: &WorkerRuntimeCommand,
) -> Result<(), Box<dyn std::error::Error>> {
match event {
TermEvent::Key(key) => {
@@ -937,12 +938,12 @@ fn handle_key(app: &mut App, key: KeyEvent) -> Option<Method> {
Some(None)
}
KeyCode::Char('c') if ctrl => Some(handle_pause_or_quit(app)),
KeyCode::Char('x') if ctrl => Some(match app.pod_status {
PodStatus::Running | PodStatus::Paused => {
KeyCode::Char('x') if ctrl => Some(match app.worker_status {
WorkerStatus::Running | WorkerStatus::Paused => {
app.clear_queued_inputs();
Some(Method::Cancel)
}
PodStatus::Idle => Some(Method::Shutdown),
WorkerStatus::Idle => Some(Method::Shutdown),
}),
KeyCode::Char('d') if ctrl => {
app.quit = true;
@@ -1196,9 +1197,9 @@ fn handle_command_key(app: &mut App, key: KeyEvent) -> Option<Method> {
const CONFIRM_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3);
/// Running → send `Method::Pause`.
/// Idle / Paused → 2-tap to quit the TUI (the Pod keeps running).
/// Idle / Paused → 2-tap to quit the TUI (the Worker keeps running).
fn handle_pause_or_quit(app: &mut App) -> Option<Method> {
if app.pod_status == PodStatus::Running {
if app.worker_status == WorkerStatus::Running {
app.clear_queued_inputs();
return Some(Method::Pause);
}
@@ -1211,7 +1212,7 @@ fn handle_pause_or_quit(app: &mut App) -> Option<Method> {
}
app.quit_confirm = Some(std::time::Instant::now());
app.flash_actionbar_notice(
"Press Ctrl-C again within 3 s to exit the TUI (the Pod keeps running).",
"Press Ctrl-C again within 3 s to exit the TUI (the Worker keeps running).",
ActionbarNoticeLevel::Warn,
ActionbarNoticeSource::Tui,
CONFIRM_TIMEOUT,
@@ -1226,7 +1227,7 @@ mod tests {
use protocol::{Event, RewindTarget, RewindTargetId, Segment};
#[test]
fn single_pod_mouse_capture_avoids_drag_and_all_motion_modes() {
fn single_worker_mouse_capture_avoids_drag_and_all_motion_modes() {
let mut ansi = String::new();
Command::write_ansi(&EnableSinglePodMouseCapture, &mut ansi).unwrap();
@@ -1238,7 +1239,7 @@ mod tests {
#[test]
fn mouse_drag_updates_selection_state() {
let mut app = App::new("pod".into());
let mut app = App::new("worker".into());
app.text_selection.set_history_snapshot(
HistoryViewport {
x: 1,
@@ -1285,7 +1286,7 @@ mod tests {
#[test]
fn esc_clears_selection_without_editing_composer() {
let mut app = App::new("pod".into());
let mut app = App::new("worker".into());
app.text_selection.set_history_snapshot(
HistoryViewport {
x: 0,
@@ -1306,7 +1307,7 @@ mod tests {
#[test]
fn copy_selection_writes_osc52_and_clears_selection() {
let mut app = App::new("pod".into());
let mut app = App::new("worker".into());
app.text_selection.set_history_snapshot(
HistoryViewport {
x: 0,
@@ -1333,7 +1334,7 @@ mod tests {
}
#[tokio::test]
async fn terminal_event_is_selected_before_ready_pod_event() {
async fn terminal_event_is_selected_before_ready_worker_event() {
let (tx, mut rx) = mpsc::unbounded_channel();
tx.send(Ok(TermEvent::Key(KeyEvent::new(
KeyCode::Char('x'),
@@ -1345,17 +1346,17 @@ mod tests {
LoopInput::Terminal(Ok(TermEvent::Key(key))) => {
assert_eq!(key.code, KeyCode::Char('x'));
}
_ => panic!("ready terminal input should win over a ready Pod event"),
_ => panic!("ready terminal input should win over a ready Worker event"),
}
}
#[tokio::test]
async fn terminal_event_is_preserved_after_pod_event_wins() {
async fn terminal_event_is_preserved_after_worker_event_wins() {
let (tx, mut rx) = mpsc::unbounded_channel();
match next_loop_input(&mut rx, true, std::future::ready(Some(1_u8))).await {
LoopInput::Pod(Some(1)) => {}
_ => panic!("expected the first ready Pod event to win before any terminal input"),
LoopInput::Worker(Some(1)) => {}
_ => panic!("expected the first ready Worker event to win before any terminal input"),
}
tx.send(Ok(TermEvent::Key(KeyEvent::new(
@@ -1368,14 +1369,14 @@ mod tests {
LoopInput::Terminal(Ok(TermEvent::Key(key))) => {
assert_eq!(key.code, KeyCode::Char('y'));
}
_ => panic!("queued terminal input should not be lost to subsequent Pod events"),
_ => panic!("queued terminal input should not be lost to subsequent Worker events"),
}
}
#[test]
fn running_status_still_allows_text_editing() {
let mut app = App::new("agent".to_string());
app.set_pod_status(PodStatus::Running);
app.set_worker_status(WorkerStatus::Running);
assert!(
handle_key(
@@ -1409,7 +1410,7 @@ mod tests {
#[test]
fn running_enter_queues_instead_of_sending_run() {
let mut app = App::new("agent".to_string());
app.set_pod_status(PodStatus::Running);
app.set_worker_status(WorkerStatus::Running);
for c in "queued".chars() {
assert!(
handle_key(
@@ -1430,7 +1431,7 @@ mod tests {
#[test]
fn queued_input_keybindings_restore_and_clear() {
let mut app = App::new("agent".to_string());
app.set_pod_status(PodStatus::Running);
app.set_worker_status(WorkerStatus::Running);
for c in "edit queued".chars() {
assert!(
handle_key(
@@ -1478,7 +1479,7 @@ mod tests {
#[test]
fn pause_and_cancel_clear_queued_input() {
let mut app = App::new("agent".to_string());
app.set_pod_status(PodStatus::Running);
app.set_worker_status(WorkerStatus::Running);
for c in "queued".chars() {
assert!(
handle_key(
@@ -1521,7 +1522,7 @@ mod tests {
#[test]
fn ctrl_x_cancels_paused_turn_without_shutdown() {
let mut app = App::new("agent".to_string());
app.set_pod_status(PodStatus::Paused);
app.set_worker_status(WorkerStatus::Paused);
let cancel = handle_key(
&mut app,
@@ -1533,7 +1534,7 @@ mod tests {
#[test]
fn ctrl_x_shutdown_while_idle_is_unchanged() {
let mut app = App::new("agent".to_string());
app.set_pod_status(PodStatus::Idle);
app.set_worker_status(WorkerStatus::Idle);
let shutdown = handle_key(
&mut app,
@@ -1854,7 +1855,7 @@ mod tests {
#[test]
fn ctrl_c_quit_guard_uses_actionbar_notice_without_transcript_alert() {
let mut app = App::new("agent".to_string());
app.set_pod_status(PodStatus::Idle);
app.set_worker_status(WorkerStatus::Idle);
let method = handle_key(
&mut app,
@@ -1866,10 +1867,10 @@ mod tests {
let notice = app
.current_actionbar_notice(std::time::Instant::now())
.expect("quit guard notice is active");
assert!(notice.text.contains("Pod keeps running"));
assert!(notice.text.contains("Worker keeps running"));
assert_eq!(notice.level, ActionbarNoticeLevel::Warn);
assert_eq!(notice.source, ActionbarNoticeSource::Tui);
assert!(!has_alert(&app, "Pod keeps running"));
assert!(!has_alert(&app, "Worker keeps running"));
let method = handle_key(
&mut app,
@@ -1889,7 +1890,7 @@ mod tests {
);
assert!(matches!(idle, Some(Method::ListRewindTargets)));
app.set_pod_status(PodStatus::Paused);
app.set_worker_status(WorkerStatus::Paused);
let paused = handle_key(
&mut app,
KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL),
@@ -1901,7 +1902,7 @@ mod tests {
fn ctrl_r_is_rejected_while_running() {
let mut app = App::new("agent".to_string());
app.connected = true;
app.set_pod_status(PodStatus::Running);
app.set_worker_status(WorkerStatus::Running);
let method = handle_key(
&mut app,
@@ -1909,14 +1910,14 @@ mod tests {
);
assert!(method.is_none());
assert!(has_alert(&app, "cannot rewind while the Pod is running"));
assert!(has_alert(&app, "cannot rewind while the Worker is running"));
}
#[test]
fn rewind_picker_close_returns_to_history_view() {
let mut app = App::new("agent".to_string());
app.connected = true;
app.handle_pod_event(Event::RewindTargets {
app.handle_worker_event(Event::RewindTargets {
head_entries: 1,
targets: vec![],
});
@@ -1927,7 +1928,7 @@ mod tests {
KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL),
);
assert!(matches!(method, Some(Method::ListRewindTargets)));
app.handle_pod_event(Event::RewindTargets {
app.handle_worker_event(Event::RewindTargets {
head_entries: 1,
targets: vec![],
});
@@ -1942,13 +1943,13 @@ mod tests {
#[test]
fn rewind_applied_reseeds_display_and_restores_composer() {
let mut app = App::new("agent".to_string());
app.handle_pod_event(Event::Snapshot {
app.handle_worker_event(Event::Snapshot {
greeting: test_greeting(),
entries: vec![],
status: PodStatus::Idle,
status: WorkerStatus::Idle,
in_flight: Default::default(),
});
app.handle_pod_event(Event::RewindApplied {
app.handle_worker_event(Event::RewindApplied {
entries: vec![],
input: vec![Segment::Text {
content: "retry this".into(),
@@ -1968,15 +1969,15 @@ mod tests {
#[test]
fn rewind_applied_keeps_non_empty_composer() {
let mut app = App::new("agent".to_string());
app.handle_pod_event(Event::Snapshot {
app.handle_worker_event(Event::Snapshot {
greeting: test_greeting(),
entries: vec![],
status: PodStatus::Idle,
status: WorkerStatus::Idle,
in_flight: Default::default(),
});
type_keys(&mut app, "draft");
app.handle_pod_event(Event::RewindApplied {
app.handle_worker_event(Event::RewindApplied {
entries: vec![],
input: vec![Segment::Text {
content: "retry this".into(),
@@ -2005,11 +2006,11 @@ mod tests {
let mut app = App::new("agent".to_string());
app.rewind_picker = Some(crate::app::RewindPickerState::new(1, vec![rewind_target()]));
app.set_pod_status(PodStatus::Paused);
app.set_worker_status(WorkerStatus::Paused);
assert!(app.submit_rewind_picker().is_none());
assert!(has_alert(
&app,
"cannot apply rewind while the Pod is paused"
"cannot apply rewind while the Worker is paused"
));
}
@@ -2055,7 +2056,7 @@ mod tests {
fn test_greeting() -> protocol::Greeting {
protocol::Greeting {
pod_name: "agent".into(),
worker_name: "agent".into(),
cwd: "/tmp".into(),
provider: "test".into(),
model: "test".into(),
File diff suppressed because it is too large Load Diff
+7 -7
View File
@@ -296,7 +296,7 @@ pub(super) const TICKET_STATE_COLUMN_WIDTH: usize = 10;
pub(super) const POD_STATUS_COLUMN_WIDTH: usize = 18;
pub(super) fn panel_row_lines(row: &PanelRow, selected: bool, width: u16) -> Vec<Line<'static>> {
if row.kind == PanelRowKind::TicketIntakePod {
if row.kind == PanelRowKind::TicketIntakeWorker {
vec![panel_intake_child_line(row, selected, width)]
} else {
vec![
@@ -438,7 +438,7 @@ pub(super) fn panel_ticket_detail(row: &PanelRow) -> String {
return parts.join(" · ");
}
if row.kind == PanelRowKind::TicketIntakePod {
if row.kind == PanelRowKind::TicketIntakeWorker {
let mut parts = row
.subtitle
.as_ref()
@@ -538,8 +538,8 @@ pub(super) fn panel_ticket_reference(row: &PanelRow) -> String {
.map(|ticket| ticket.id.clone())
.unwrap_or_else(|| match &row.key {
PanelRowKey::Ticket(id) | PanelRowKey::InvalidTicket(id) => id.clone(),
PanelRowKey::TicketIntakePod { ticket_id, .. } => ticket_id.clone(),
PanelRowKey::Pod(name) => name.clone(),
PanelRowKey::TicketIntakeWorker { ticket_id, .. } => ticket_id.clone(),
PanelRowKey::Worker(name) => name.clone(),
})
}
@@ -597,7 +597,7 @@ pub(super) fn intake_status_style(status: &str) -> Style {
}
pub(super) fn section_rows(
list: &PodList,
list: &WorkerList,
section: &DashboardSection,
selected: Option<&PanelRowKey>,
width: u16,
@@ -616,7 +616,7 @@ pub(super) fn section_rows(
)));
for index in visible {
if let Some(entry) = list.entries.get(index) {
let key = PanelRowKey::Pod(entry.name.clone());
let key = PanelRowKey::Worker(entry.name.clone());
let selected = selected == Some(&key);
rows.push(PanelListRow::selectable(
row_line(entry, selected, width),
@@ -627,7 +627,7 @@ pub(super) fn section_rows(
rows
}
pub(super) fn row_line(entry: &PodListEntry, selected: bool, width: u16) -> Line<'static> {
pub(super) fn row_line(entry: &WorkerListEntry, selected: bool, width: u16) -> Line<'static> {
let marker = if selected { "" } else { " " };
let name_style = if selected {
Style::default()
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -8,7 +8,7 @@
//!
//! Display form: paste atoms render as
//! `[Clipboard #N | X chars, Y lines]`. Submit form: paste atoms expand
//! back to their original captured content so the Pod sees the full
//! back to their original captured content so the Worker sees the full
//! pasted text (without the placeholder label).
use ratatui::style::{Color, Style};
@@ -33,7 +33,7 @@ impl PasteRef {
}
/// `@<path>` chip — confirmed completion of a file-system reference.
/// Directories remain valid chips because Pod resolves normal directory refs
/// Directories remain valid chips because Worker resolves normal directory refs
/// to shallow `[Dir: <path>]` listings at submit time.
#[derive(Debug, Clone)]
pub struct FileRefAtom {
+22 -21
View File
@@ -12,7 +12,6 @@ mod input;
pub mod keys;
mod markdown;
mod picker;
mod pod_list;
mod role_session_registry;
mod scroll;
pub mod setup_model;
@@ -22,6 +21,7 @@ mod text_selection;
mod tool;
mod ui;
mod view_mode;
mod worker_list;
mod workspace_panel;
use std::io;
@@ -33,37 +33,37 @@ use crossterm::execute;
use crossterm::terminal::{LeaveAlternateScreen, disable_raw_mode, enable_raw_mode};
use session_store::SegmentId;
use client::PodRuntimeCommand;
use client::WorkerRuntimeCommand;
#[derive(Debug, Clone)]
pub struct LaunchOptions {
pub mode: LaunchMode,
pub runtime_command: PodRuntimeCommand,
pub runtime_command: WorkerRuntimeCommand,
pub workspace_root: PathBuf,
}
#[derive(Debug, Clone)]
pub enum LaunchMode {
Spawn {
pod_name: Option<String>,
worker_name: Option<String>,
profile: Option<String>,
},
/// `yoi --pod <name>`: attach to a live Pod by name if possible;
/// otherwise launch the Pod runtime command with `--pod <name>` so it
/// resumes from name-keyed state or creates a fresh same-name Pod.
PodName {
pod_name: String,
/// `yoi --worker <name>`: attach to a live Worker by name if possible;
/// otherwise launch the Worker runtime command with `--worker <name>` so it
/// resumes from name-keyed state or creates a fresh same-name Worker.
WorkerName {
worker_name: String,
socket_override: Option<PathBuf>,
},
/// `yoi resume`: open the Pod picker, then attach to the selected live Pod
/// or restore the selected stopped Pod by name. Without `--all`, the picker
/// `yoi resume`: open the Worker picker, then attach to the selected live Worker
/// or restore the selected stopped Worker by name. Without `--all`, the picker
/// is scoped to the current runtime workspace.
Resume { all: bool },
/// `yoi --session <UUID>`: skip the picker, go straight to the
/// resume name dialog with `id` baked in.
ResumeWithSession {
id: SegmentId,
pod_name: Option<String>,
worker_name: Option<String>,
},
/// `yoi panel`: open the workspace Dashboard from the current workspace.
Panel,
@@ -95,18 +95,19 @@ pub async fn launch(options: LaunchOptions) -> ExitCode {
}
let result = match mode {
LaunchMode::Spawn { pod_name, profile } => {
console::run_spawn(None, pod_name, profile, runtime_command).await
}
LaunchMode::PodName {
pod_name,
LaunchMode::Spawn {
worker_name,
profile,
} => console::run_spawn(None, worker_name, profile, runtime_command).await,
LaunchMode::WorkerName {
worker_name,
socket_override,
} => console::run_pod_name(pod_name, socket_override, runtime_command).await,
} => console::run_worker_name(worker_name, socket_override, runtime_command).await,
LaunchMode::Resume { all } => {
console::run_resume(runtime_command, workspace_root.clone(), all).await
}
LaunchMode::ResumeWithSession { id, pod_name } => {
console::run_spawn(Some(id), pod_name, None, runtime_command).await
LaunchMode::ResumeWithSession { id, worker_name } => {
console::run_spawn(Some(id), worker_name, None, runtime_command).await
}
LaunchMode::Panel => dashboard::launch(runtime_command).await,
};
@@ -138,7 +139,7 @@ pub async fn launch(options: LaunchOptions) -> ExitCode {
// SpawnError has already been painted into the inline
// viewport's final frame, so it's already visible in the
// user's scrollback — printing it again would be a noisy
// duplicate. Other errors (pod-name failures, terminal setup
// duplicate. Other errors (worker-name failures, terminal setup
// hiccups, etc.) need surfacing here.
if e.downcast_ref::<spawn::SpawnError>().is_none() {
eprintln!("yoi: {e}");
+61 -61
View File
@@ -1,15 +1,15 @@
//! Inline-viewport "pick a Pod to attach or restore" UX.
//! Inline-viewport "pick a Worker to attach or restore" UX.
//!
//! Reads live Pod allocations from the runtime registry and stopped Pod state
//! Reads live Worker allocations from the runtime registry and stopped Worker state
//! from the pod-store name-keyed metadata. Picking a live row attaches to
//! its socket; picking a stopped row restores via the Pod runtime command.
//! its socket; picking a stopped row restores via the Worker runtime command.
use std::io;
use std::path::PathBuf;
use std::time::Duration;
use crossterm::event::{self, Event as TermEvent, KeyCode, KeyEventKind, KeyModifiers};
use pod_store::FsPodStore;
use pod_store::FsWorkerStore;
use ratatui::Terminal;
use ratatui::backend::CrosstermBackend;
use ratatui::layout::{Constraint, Layout};
@@ -19,10 +19,10 @@ use ratatui::widgets::Paragraph;
use ratatui::{Frame, TerminalOptions, Viewport};
use session_store::FsStore;
use crate::pod_list::{
LivePodInfo, PodList, PodListEntry, PodVisibilitySource, StoredMetadataState, StoredPodInfo,
live_socket_for_pod as pod_list_live_socket_for_pod, read_reachable_live_pod_infos,
read_stored_pod_infos,
use crate::worker_list::{
LiveWorkerInfo, StoredMetadataState, StoredWorkerInfo, WorkerList, WorkerListEntry,
WorkerVisibilitySource, live_socket_for_pod as worker_list_live_socket_for_pod,
read_reachable_live_pod_infos, read_stored_worker_infos,
};
const MAX_ROWS: usize = 10;
@@ -32,7 +32,7 @@ const VIEWPORT_LINES: u16 = MAX_ROWS as u16 + 4;
pub enum PickerError {
Io(io::Error),
Store(session_store::StoreError),
NoPods { all: bool },
NoWorkers { all: bool },
}
impl std::fmt::Display for PickerError {
@@ -40,13 +40,13 @@ impl std::fmt::Display for PickerError {
match self {
Self::Io(e) => write!(f, "io error: {e}"),
Self::Store(e) => write!(f, "session store error: {e}"),
Self::NoPods { all: true } => write!(
Self::NoWorkers { all: true } => write!(
f,
"no pods found — start a fresh pod with `yoi` and try again"
"no workers found — start a fresh Worker with `yoi` and try again"
),
Self::NoPods { all: false } => write!(
Self::NoWorkers { all: false } => write!(
f,
"no pods found in this workspace — use `yoi resume --all` to list all host/data-dir Pods"
"no workers found in this workspace — use `yoi resume --all` to list all host/data-dir Pods"
),
}
}
@@ -67,11 +67,11 @@ impl From<session_store::StoreError> for PickerError {
}
pub enum PickerOutcome {
/// User picked a Pod. `socket_override` is set for live rows when the
/// User picked a Worker. `socket_override` is set for live rows when the
/// runtime registry knows the exact socket path; stopped rows leave it
/// empty so the caller restores by spawning the Pod runtime command.
/// empty so the caller restores by spawning the Worker runtime command.
Picked {
pod_name: String,
worker_name: String,
socket_override: Option<PathBuf>,
},
Cancelled,
@@ -103,13 +103,13 @@ enum PickerScope {
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PodRowState {
enum WorkerRowState {
Live,
Stopped,
Corrupt,
}
impl PodRowState {
impl WorkerRowState {
fn label(self) -> &'static str {
match self {
Self::Live => "live",
@@ -131,22 +131,22 @@ impl PodRowState {
fn list_for_options(
options: &PickerOptions,
stored_pods: Vec<StoredPodInfo>,
live_pods: Vec<LivePodInfo>,
) -> PodList {
stored_workers: Vec<StoredWorkerInfo>,
live_workers: Vec<LiveWorkerInfo>,
) -> WorkerList {
match &options.scope {
PickerScope::Workspace(workspace_root) => PodList::from_workspace_sources(
PodVisibilitySource::ResumePicker,
stored_pods,
live_pods,
PickerScope::Workspace(workspace_root) => WorkerList::from_workspace_sources(
WorkerVisibilitySource::ResumePicker,
stored_workers,
live_workers,
None,
MAX_ROWS,
workspace_root,
),
PickerScope::All => PodList::from_sources(
PodVisibilitySource::ResumePicker,
stored_pods,
live_pods,
PickerScope::All => WorkerList::from_sources(
WorkerVisibilitySource::ResumePicker,
stored_workers,
live_workers,
None,
MAX_ROWS,
),
@@ -156,14 +156,14 @@ fn list_for_options(
pub async fn run(options: PickerOptions) -> Result<PickerOutcome, PickerError> {
let store_dir = default_store_dir()?;
let store = FsStore::new(&store_dir)?;
let pod_store = FsPodStore::new(default_pod_store_dir()?).map_err(io::Error::other)?;
let stored_pods = read_stored_pod_infos(&store, &pod_store)?;
let live_pods = read_reachable_live_pod_infos(&store)
let pod_store = FsWorkerStore::new(default_pod_store_dir()?).map_err(io::Error::other)?;
let stored_workers = read_stored_worker_infos(&store, &pod_store)?;
let live_workers = read_reachable_live_pod_infos(&store)
.await
.unwrap_or_default();
let mut list = list_for_options(&options, stored_pods, live_pods);
let mut list = list_for_options(&options, stored_workers, live_workers);
if list.entries.is_empty() {
return Err(PickerError::NoPods {
return Err(PickerError::NoWorkers {
all: matches!(options.scope, PickerScope::All),
});
}
@@ -185,9 +185,9 @@ pub async fn run(options: PickerOptions) -> Result<PickerOutcome, PickerError> {
}
Some(Action::Submit) => {
close_viewport(&mut terminal)?;
let entry = list.selected_entry().expect("non-empty pod list");
let entry = list.selected_entry().expect("non-empty worker list");
return Ok(PickerOutcome::Picked {
pod_name: entry.name.clone(),
worker_name: entry.name.clone(),
socket_override: entry.attach_socket_path().map(PathBuf::from),
});
}
@@ -229,14 +229,14 @@ fn default_pod_store_dir() -> Result<PathBuf, PickerError> {
.ok_or_else(|| {
PickerError::Io(io::Error::new(
io::ErrorKind::NotFound,
"could not resolve pod state directory \
"could not resolve worker state directory \
(set YOI_HOME, YOI_DATA_DIR, or HOME)",
))
})
}
pub(crate) fn live_socket_for_pod(pod_name: &str) -> Option<PathBuf> {
pod_list_live_socket_for_pod(pod_name)
pub(crate) fn live_socket_for_pod(worker_name: &str) -> Option<PathBuf> {
worker_list_live_socket_for_pod(worker_name)
}
fn make_inline_terminal() -> io::Result<Terminal<CrosstermBackend<io::Stdout>>> {
@@ -278,7 +278,7 @@ fn poll_event() -> io::Result<Option<Action>> {
}
}
fn draw(f: &mut Frame<'_>, list: &PodList) {
fn draw(f: &mut Frame<'_>, list: &WorkerList) {
let area = f.area();
let mut constraints: Vec<Constraint> = Vec::with_capacity(list.entries.len() + 3);
constraints.push(Constraint::Length(1)); // title
@@ -320,10 +320,10 @@ fn draw(f: &mut Frame<'_>, list: &PodList) {
}
fn picker_title() -> &'static str {
"resume pod pick a pod"
"resume worker pick a worker"
}
fn row_line(entry: &PodListEntry, selected: bool) -> Line<'_> {
fn row_line(entry: &WorkerListEntry, selected: bool) -> Line<'_> {
let marker = if selected { "" } else { " " };
let name_style = if selected {
Style::default()
@@ -361,18 +361,18 @@ fn row_line(entry: &PodListEntry, selected: bool) -> Line<'_> {
Line::from(spans)
}
fn row_state(entry: &PodListEntry) -> PodRowState {
fn row_state(entry: &WorkerListEntry) -> WorkerRowState {
if entry.live.as_ref().is_some_and(|live| live.reachable) {
return PodRowState::Live;
return WorkerRowState::Live;
}
if entry
.stored
.as_ref()
.is_some_and(|stored| matches!(stored.metadata_state, StoredMetadataState::Corrupt(_)))
{
return PodRowState::Corrupt;
return WorkerRowState::Corrupt;
}
PodRowState::Stopped
WorkerRowState::Stopped
}
fn format_updated_at(updated_at: u64) -> String {
@@ -383,7 +383,7 @@ fn format_updated_at(updated_at: u64) -> String {
}
}
fn debug_ids(entry: &PodListEntry) -> String {
fn debug_ids(entry: &WorkerListEntry) -> String {
let session = entry
.summary
.active_session_id
@@ -407,20 +407,20 @@ mod tests {
#[test]
fn picker_title_names_pods_not_sessions() {
assert_eq!(picker_title(), "resume pod pick a pod");
assert_eq!(picker_title(), "resume worker pick a worker");
}
#[test]
fn picker_no_pods_message_mentions_all_for_workspace_scope() {
let message = PickerError::NoPods { all: false }.to_string();
assert!(message.contains("no pods found in this workspace"));
let message = PickerError::NoWorkers { all: false }.to_string();
assert!(message.contains("no workers found in this workspace"));
assert!(message.contains("yoi resume --all"));
}
#[test]
fn picker_no_pods_message_keeps_fresh_pod_hint_for_all_scope() {
let message = PickerError::NoPods { all: true }.to_string();
assert!(message.contains("start a fresh pod with `yoi`"));
let message = PickerError::NoWorkers { all: true }.to_string();
assert!(message.contains("start a fresh Worker with `yoi`"));
assert!(!message.contains("yoi resume --all"));
}
@@ -464,9 +464,9 @@ mod tests {
assert_eq!(names, vec!["current", "other", "legacy"]);
}
fn stored_pod(name: &str, workspace_root: Option<&str>, updated_at: u64) -> StoredPodInfo {
StoredPodInfo {
pod_name: name.to_string(),
fn stored_pod(name: &str, workspace_root: Option<&str>, updated_at: u64) -> StoredWorkerInfo {
StoredWorkerInfo {
worker_name: name.to_string(),
metadata_state: StoredMetadataState::Present,
active_session_id: None,
active_segment_id: None,
@@ -479,16 +479,16 @@ mod tests {
#[test]
fn picker_row_shows_live_pending_preview_and_runtime_segment_id() {
let segment_id = session_store::new_segment_id();
let entry = PodList::from_sources(
PodVisibilitySource::ResumePicker,
let entry = WorkerList::from_sources(
WorkerVisibilitySource::ResumePicker,
vec![],
vec![crate::pod_list::LivePodInfo {
pod_name: "pending".to_string(),
vec![crate::worker_list::LiveWorkerInfo {
worker_name: "pending".to_string(),
socket_path: PathBuf::from("/tmp/pending.sock"),
status: Some(protocol::PodStatus::Idle),
status: Some(protocol::WorkerStatus::Idle),
reachable: true,
segment_id: Some(segment_id),
summary: crate::pod_list::PodEntrySummary::default(),
summary: crate::worker_list::WorkerEntrySummary::default(),
}],
None,
10,
+16 -16
View File
@@ -28,7 +28,7 @@ pub(crate) struct RoleSessionRegistry {
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub(crate) struct RoleSessionRecord {
pub role: String,
pub pod_name: String,
pub worker_name: String,
pub origin: RoleSessionOrigin,
pub created_at: String,
pub updated_at: String,
@@ -58,7 +58,7 @@ pub(crate) struct TicketClaim {
pub ticket_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ticket_slug: Option<String>,
pub pod_name: String,
pub worker_name: String,
pub role: String,
}
@@ -89,7 +89,7 @@ impl std::fmt::Display for PanelRegistryError {
Self::TicketAlreadyClaimed(claim) => write!(
f,
"Ticket {} is already claimed locally by {} ({})",
claim.ticket_id, claim.pod_name, claim.role
claim.ticket_id, claim.worker_name, claim.role
),
}
}
@@ -165,33 +165,33 @@ impl PanelRegistryStore {
pub(crate) fn record_session(
&self,
pod_name: impl Into<String>,
worker_name: impl Into<String>,
role: impl Into<String>,
origin: RoleSessionOrigin,
session_id: Option<String>,
related_tickets: impl IntoIterator<Item = RelatedTicketRef>,
) -> Result<(), PanelRegistryError> {
let pod_name = pod_name.into();
let worker_name = worker_name.into();
let role = role.into();
let related_tickets: Vec<RelatedTicketRef> = related_tickets.into_iter().collect();
self.update_registry(|registry| {
let now = now_timestamp_string();
let mut tickets: BTreeSet<RelatedTicketRef> = registry
.sessions
.get(&pod_name)
.get(&worker_name)
.map(|record| record.related_tickets.iter().cloned().collect())
.unwrap_or_default();
tickets.extend(related_tickets);
let created_at = registry
.sessions
.get(&pod_name)
.get(&worker_name)
.map(|record| record.created_at.clone())
.unwrap_or_else(|| now.clone());
registry.sessions.insert(
pod_name.clone(),
worker_name.clone(),
RoleSessionRecord {
role,
pod_name,
worker_name,
origin,
created_at,
updated_at: now,
@@ -207,7 +207,7 @@ impl PanelRegistryStore {
&self,
ticket_id: &str,
ticket_slug: Option<&str>,
pod_name: &str,
worker_name: &str,
role: &str,
) -> Result<TicketClaimResult, PanelRegistryError> {
fs::create_dir_all(self.claims_dir())?;
@@ -215,13 +215,13 @@ impl PanelRegistryStore {
let claim = TicketClaim {
ticket_id: ticket_id.to_string(),
ticket_slug: ticket_slug.map(ToOwned::to_owned),
pod_name: pod_name.to_string(),
worker_name: worker_name.to_string(),
role: role.to_string(),
};
match self.create_claim_file(&claim_path, &claim) {
Ok(()) => {
if let Err(error) = self.record_session(
pod_name.to_string(),
worker_name.to_string(),
role.to_string(),
RoleSessionOrigin::TicketClaim,
None,
@@ -237,7 +237,7 @@ impl PanelRegistryStore {
}
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {
let existing = self.load_claim(ticket_id)?;
if existing.pod_name == pod_name && existing.role == role {
if existing.worker_name == worker_name && existing.role == role {
Ok(TicketClaimResult::AlreadyOwned(existing))
} else {
Err(PanelRegistryError::TicketAlreadyClaimed(existing))
@@ -485,7 +485,7 @@ mod tests {
.unwrap_err();
assert!(matches!(error, PanelRegistryError::TicketAlreadyClaimed(_)));
let claim = store.claim_for_ticket("T-1").unwrap().unwrap();
assert_eq!(claim.pod_name, "ticket-one-intake");
assert_eq!(claim.worker_name, "ticket-one-intake");
assert_eq!(claim.ticket_slug.as_deref(), Some("ticket-one"));
}
@@ -526,12 +526,12 @@ mod tests {
let preticket = snapshot
.sessions
.iter()
.find(|session| session.pod_name == "ticket-intake-preticket")
.find(|session| session.worker_name == "ticket-intake-preticket")
.unwrap();
let shared = snapshot
.sessions
.iter()
.find(|session| session.pod_name == "ticket-intake-shared")
.find(|session| session.worker_name == "ticket-intake-shared")
.unwrap();
assert!(preticket.related_tickets.is_empty());
+2 -2
View File
@@ -99,7 +99,7 @@ fn prompt_model_choice(
println!("yoi setup-model");
println!();
println!("Choose the default model Profile to write under the user config directory.");
println!("This command only writes Profile config; it does not start or attach a Pod.");
println!("This command only writes Profile config; it does not start or attach a Worker.");
println!();
for (idx, choice) in choices.iter().enumerate() {
println!(
@@ -237,7 +237,7 @@ return profile {{
task = {{ enabled = true }},
memory = {{ enabled = true }},
web = {{ enabled = true }},
pods = {{ enabled = false }},
workers = {{ enabled = false }},
ticket = {{ enabled = false, access = "lifecycle" }},
ticket_orchestration = {{ enabled = false }},
}},
+37 -37
View File
@@ -1,9 +1,9 @@
//! Inline-viewport "spawn Pod and attach" UX.
//! Inline-viewport "spawn Worker and attach" UX.
//!
//! Rendered at the user's current cursor position when `yoi` is invoked
//! with no positional argument. Discovers `.yoi/profiles.toml` profile
//! choices plus bundled profiles, defaults to the builtin profile, prompts for
//! the Pod's name, and on confirmation launches the Pod runtime command as an
//! the Worker's name, and on confirmation launches the Worker runtime command as an
//! independent process. Once the process reports its socket via the
//! `YOI-READY` stderr line, the dialog hands control back so main can
//! switch the terminal to alternate-screen mode.
@@ -15,7 +15,7 @@ use std::io;
use std::path::{Path, PathBuf};
use std::time::Duration;
use client::{PodRuntimeCommand, SpawnConfig, spawn_pod};
use client::{SpawnConfig, WorkerRuntimeCommand, spawn_worker};
use crossterm::event::{self, Event as TermEvent, KeyCode, KeyEventKind, KeyModifiers};
use manifest::ProfileDiscovery;
use ratatui::Terminal;
@@ -30,7 +30,7 @@ use session_store::SegmentId;
const VIEWPORT_LINES: u16 = 6;
pub struct SpawnReady {
pub pod_name: String,
pub worker_name: String,
pub socket_path: PathBuf,
}
@@ -71,13 +71,13 @@ impl From<client::SpawnError> for SpawnError {
type InlineTerminal = Terminal<CrosstermBackend<io::Stdout>>;
/// Source session for a resume run. `None` = fresh spawn (current
/// behaviour); `Some(id)` swaps the dialog into "Resume Pod" mode and
/// passes `--session <id>` to the spawned Pod runtime child.
/// behaviour); `Some(id)` swaps the dialog into "Resume Worker" mode and
/// passes `--session <id>` to the spawned Worker runtime child.
pub async fn run(
resume_from: Option<SegmentId>,
pod_name: Option<String>,
worker_name: Option<String>,
profile: Option<String>,
runtime_command: PodRuntimeCommand,
runtime_command: WorkerRuntimeCommand,
) -> Result<SpawnOutcome, SpawnError> {
let defaults = load_spawn_defaults()?;
let mut profile_choices = if resume_from.is_some() {
@@ -91,7 +91,7 @@ pub async fn run(
defaults.default_profile_index,
);
let selected_name = pod_name.unwrap_or(defaults.default_name);
let selected_name = worker_name.unwrap_or(defaults.default_name);
let immediate = resume_from.is_some() || profile.is_some() && !selected_name.is_empty();
let mut form = Form {
cwd: defaults.cwd.clone(),
@@ -145,18 +145,18 @@ pub async fn run(
)));
}
// Phase 2: launch pod and wait for ready line. Drop the cursor
// Phase 2: launch worker and wait for ready line. Drop the cursor
// out of the name field — subsequent frames are passive status
// updates, not input — so the cursor doesn't end up parked there
// when the inline terminal is finally dropped.
form.editing = false;
form.message = Some(("starting pod...".to_string(), MessageKind::Progress));
form.message = Some(("starting worker...".to_string(), MessageKind::Progress));
terminal.draw(|f| draw_form(f, &form))?;
match wait_for_ready(&mut terminal, &mut form, &runtime_command).await {
Ok(ready) => {
form.message = Some((
format!("ready: {} attaching...", ready.pod_name),
format!("ready: {} attaching...", ready.worker_name),
MessageKind::Ok,
));
terminal.draw(|f| draw_form(f, &form))?;
@@ -172,22 +172,22 @@ pub async fn run(
}
}
/// Launch a Pod runtime command with `--pod <name>` without opening the name dialog. The child Pod
/// resolves persisted Pod metadata if present, or creates a fresh same-name Pod
/// Launch a Worker runtime command with `--worker <name>` without opening the name dialog. The child Worker
/// resolves persisted Worker metadata if present, or creates a fresh same-name Worker
/// from the default profile.
pub async fn run_pod_name(
pod_name: String,
runtime_command: PodRuntimeCommand,
pub async fn run_worker_name(
worker_name: String,
runtime_command: WorkerRuntimeCommand,
) -> Result<SpawnOutcome, SpawnError> {
let defaults = load_spawn_defaults()?;
let mut form = form_for_pod_name(pod_name, defaults);
let mut form = form_for_worker_name(worker_name, defaults);
let mut terminal = make_inline_terminal()?;
terminal.draw(|f| draw_form(f, &form))?;
match wait_for_ready(&mut terminal, &mut form, &runtime_command).await {
Ok(ready) => {
form.message = Some((
format!("ready: {} attaching...", ready.pod_name),
format!("ready: {} attaching...", ready.worker_name),
MessageKind::Ok,
));
terminal.draw(|f| draw_form(f, &form))?;
@@ -226,7 +226,7 @@ fn load_spawn_defaults() -> Result<SpawnDefaults, SpawnError> {
.and_then(|s| s.to_str())
.map(sanitise_default_name)
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "pod".to_string());
.unwrap_or_else(|| "worker".to_string());
let (profile_choices, default_profile_index) = profile_choices_for_cwd(&cwd);
@@ -290,13 +290,13 @@ fn initial_profile_index(
choices.len() - 1
}
fn form_for_pod_name(pod_name: String, defaults: SpawnDefaults) -> Form {
fn form_for_worker_name(worker_name: String, defaults: SpawnDefaults) -> Form {
Form {
cwd: defaults.cwd,
scope_origin: defaults.scope_origin,
name_cursor: pod_name.chars().count(),
name: pod_name,
message: Some(("resuming pod...".to_string(), MessageKind::Progress)),
name_cursor: worker_name.chars().count(),
name: worker_name,
message: Some(("resuming worker...".to_string(), MessageKind::Progress)),
editing: false,
resume_from: None,
profile_choices: Vec::new(),
@@ -359,7 +359,7 @@ fn poll_event() -> io::Result<Option<Action>> {
}
fn is_safe_name_char(c: char) -> bool {
// Filesystem-safe; pod.name becomes a runtime-dir name.
// Filesystem-safe; worker.name becomes a runtime-dir name.
c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.')
}
@@ -372,23 +372,23 @@ fn sanitise_default_name(s: &str) -> String {
async fn wait_for_ready(
terminal: &mut InlineTerminal,
form: &mut Form,
runtime_command: &PodRuntimeCommand,
runtime_command: &WorkerRuntimeCommand,
) -> Result<SpawnReady, SpawnError> {
let config = SpawnConfig {
runtime_command: runtime_command.clone(),
pod_name: form.name.clone(),
worker_name: form.name.clone(),
profile: form.selected_profile_selector(),
workspace_root: form.cwd.clone(),
cwd: None,
resume_from: form.resume_from,
};
let ready = spawn_pod(config, |line| {
let ready = spawn_worker(config, |line| {
form.message = Some((line.to_string(), MessageKind::Progress));
let _ = terminal.draw(|f| draw_form(f, form));
})
.await?;
Ok(SpawnReady {
pod_name: ready.pod_name,
worker_name: ready.worker_name,
socket_path: ready.socket_path,
})
}
@@ -421,14 +421,14 @@ struct Form {
/// cursor stays out so it does not collide with the shell prompt
/// after the inline terminal is dropped.
editing: bool,
/// `Some(id)` flips the dialog into "Resume Pod" mode: the title
/// `Some(id)` flips the dialog into "Resume Worker" mode: the title
/// switches, the source session is shown to the user, and the
/// child pod is launched with `--session <id>` so it restores
/// child worker is launched with `--session <id>` so it restores
/// from `id` and appends to the same session log.
resume_from: Option<SegmentId>,
/// Optional profile choices passed with `--profile` for
/// fresh spawns. This is not used for resume/attach flows because those must
/// restore Pod state rather than re-evaluate a profile source.
/// restore Worker state rather than re-evaluate a profile source.
profile_choices: Vec<ProfileChoice>,
profile_index: usize,
}
@@ -526,8 +526,8 @@ fn draw_form(f: &mut Frame<'_>, form: &Form) {
.split(area);
let title_text = match form.resume_from {
Some(id) => format!("resume pod session: {}", short_segment(id)),
None => "spawn pod".to_string(),
Some(id) => format!("resume worker session: {}", short_segment(id)),
None => "spawn worker".to_string(),
};
let title = Paragraph::new(Line::from(vec![Span::styled(
title_text,
@@ -633,7 +633,7 @@ mod tests {
}
#[test]
fn pod_name_form_restores_or_creates_by_pod_name() {
fn worker_name_form_restores_or_creates_by_worker_name() {
let defaults = SpawnDefaults {
cwd: PathBuf::from("/work/example"),
scope_origin: ScopeOrigin::FromProfile,
@@ -641,7 +641,7 @@ mod tests {
default_profile_index: 0,
profile_choices: Vec::new(),
};
let f = form_for_pod_name("agent".to_string(), defaults);
let f = form_for_worker_name("agent".to_string(), defaults);
assert_eq!(f.name, "agent");
assert_eq!(f.name_cursor, "agent".chars().count());
@@ -649,7 +649,7 @@ mod tests {
assert!(!f.editing);
assert_eq!(
f.message,
Some(("resuming pod...".to_string(), MessageKind::Progress))
Some(("resuming worker...".to_string(), MessageKind::Progress))
);
}
+12 -12
View File
@@ -1,18 +1,18 @@
//! In-TUI mirror of the session-lifetime task store.
//!
//! This deliberately does NOT depend on the Pod TaskStore. The TUI is a
//! presentation layer; pulling in `pod` would drag along the runtime
//! This deliberately does NOT depend on the Worker TaskStore. The TUI is a
//! presentation layer; pulling in `worker` would drag along the runtime
//! feature surface. Instead we mirror the small subset we
//! need:
//!
//! - `TaskEntry` / `TaskStatus`: shaped to round-trip with Pod Task JSON
//! - `TaskEntry` / `TaskStatus`: shaped to round-trip with Worker Task JSON
//! serialization (`#[serde(rename_all = "lowercase")]` on the status,
//! matching field names on the entry).
//! - Just enough state machine to apply `TaskCreate` / `TaskUpdate`
//! tool-call arguments and the `[Session TaskStore snapshot]` system
//! message that compaction emits.
//!
//! The snapshot text format is owned by the Pod Task feature. The TUI keeps
//! The snapshot text format is owned by the Worker Task feature. The TUI keeps
//! local compatibility fixtures for the `[Session TaskStore snapshot]` system
//! message shape emitted during compaction and restored on resume.
@@ -90,7 +90,7 @@ impl TaskStore {
/// Apply a completed `TaskCreate` / `TaskUpdate` tool_call. Other
/// tool names and unparseable JSON are silent no-ops, matching the
/// resilience of the Pod TaskStore history replay.
/// resilience of the Worker TaskStore history replay.
pub fn apply_tool_call(&mut self, name: &str, arguments: &str) {
match name {
"TaskCreate" => {
@@ -236,8 +236,8 @@ mod tests {
assert_eq!(c.active(), 2);
}
/// Snapshot text matches the wrapping `Pod::try_pre_run_compact` and the
/// Pod Task feature snapshot fixture shape: header line, blank, overview
/// Snapshot text matches the wrapping `Worker::try_pre_run_compact` and the
/// Worker Task feature snapshot fixture shape: header line, blank, overview
/// line, blank, fenced JSON, trailing prose.
fn wrap_snapshot(json_body: &str, overview: &str) -> String {
format!(
@@ -314,16 +314,16 @@ mod tests {
}
/// Snapshot format compatibility tests. The TUI deliberately re-implements a
/// stripped-down TaskStore mirror instead of depending on the Pod Task feature;
/// stripped-down TaskStore mirror instead of depending on the Worker Task feature;
/// it only consumes task tool calls and `[Session TaskStore snapshot]` system
/// messages. These fixtures encode the Pod-owned Task snapshot JSON/text shape
/// messages. These fixtures encode the Worker-owned Task snapshot JSON/text shape
/// so accidental TUI parser drift still fails locally without making `tui`
/// depend on `pod` or `tools`.
/// depend on `worker` or `tools`.
#[cfg(test)]
mod snapshot_format_contract {
use super::*;
/// Mirrors the envelope `Pod::try_pre_run_compact` wraps the raw
/// Mirrors the envelope `Worker::try_pre_run_compact` wraps the raw
/// snapshot text in. Hand-rolled here so the test fails loudly if
/// the prose around the JSON fence ever shifts.
fn wrap_pod_style(snapshot_text: &str) -> String {
@@ -397,7 +397,7 @@ mod snapshot_format_contract {
#[test]
fn taskentry_field_shape_deserializes_into_tui_taskentry() {
// A single Pod TaskEntry as JSON. Field renames like `taskid` →
// A single Worker TaskEntry as JSON. Field renames like `taskid` →
// `task_id` or status case changes surface here as serde failures or
// wrong-status assertions.
let json = r#"{
+1 -1
View File
@@ -1,4 +1,4 @@
//! Local, non-persistent text selection state for the single-Pod transcript view.
//! Local, non-persistent text selection state for the single-Worker transcript view.
//!
//! This module deliberately stores only the most recent rendered history rows and
//! the active drag endpoints. Selected/copied text never leaves TUI-local state
+39 -36
View File
@@ -25,7 +25,7 @@ use ratatui::widgets::{
};
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
use protocol::{AlertLevel, CompletionEntry, Greeting, PodEvent, Segment};
use protocol::{AlertLevel, CompletionEntry, Greeting, Segment, WorkerEvent};
use crate::app::{ActionbarNoticeLevel, App, CompletionState, alert_source_label, fmt_tokens};
use crate::block::{Block, CompactEvent, ThinkingBlock, ThinkingState};
@@ -509,7 +509,7 @@ fn draw_rewind_picker(
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
),
Span::raw(" waiting for Pod response"),
Span::raw(" waiting for Worker response"),
]
} else {
vec![
@@ -871,7 +871,7 @@ fn render_block_into(lines: &mut Vec<Line<'static>>, block: &Block, width: u16,
match block {
Block::Greeting(g) => match mode {
Mode::Overview => {
let text = format!("{} {} ({})", g.pod_name, g.model, g.provider);
let text = format!("{} {} ({})", g.worker_name, g.model, g.provider);
lines.push(Line::from(Span::styled(
text,
Style::default().fg(Color::Cyan),
@@ -894,8 +894,8 @@ fn render_block_into(lines: &mut Vec<Line<'static>>, block: &Block, width: u16,
_ => push_padded_lines(lines, &text, MessageKind::Notify),
}
}
Block::PodEvent { event } => {
let text = format_pod_event(event);
Block::WorkerEvent { event } => {
let text = format_worker_event(event);
match mode {
Mode::Overview => push_overview_line(lines, &text, width, MessageKind::Notify, ""),
_ => push_padded_lines(lines, &text, MessageKind::Notify),
@@ -1595,7 +1595,7 @@ fn draw_status(frame: &mut Frame, app: &App, area: Rect) {
conn,
Span::raw(" "),
Span::styled(
app.pod_name.clone(),
app.worker_name.clone(),
Style::default().add_modifier(Modifier::BOLD),
),
];
@@ -1823,7 +1823,7 @@ fn greeting_lines(g: &Greeting) -> Vec<Line<'static>> {
let mut lines: Vec<Line<'static>> = Vec::new();
lines.push(Line::from(Span::styled(
g.pod_name.clone(),
g.worker_name.clone(),
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD),
@@ -1856,9 +1856,9 @@ fn greeting_lines(g: &Greeting) -> Vec<Line<'static>> {
pub enum MessageKind {
TurnHeader,
User,
/// External-input echoes (`Method::Notify` / `Method::PodEvent`).
/// External-input echoes (`Method::Notify` / `Method::WorkerEvent`).
/// Visually distinct from User / Assistant / Notice so it's clear
/// the line came from another Pod or operator, not the local user.
/// the line came from another Worker or operator, not the local user.
Notify,
/// Persisted role:system history item preview.
System,
@@ -1891,27 +1891,30 @@ pub fn kind_style(kind: MessageKind) -> Style {
}
}
/// One-line summary of a `PodEvent` for display in the activity log.
/// One-line summary of a `WorkerEvent` for display in the activity log.
/// Independent from the LLM-injection wrapper (`crate::ipc::event::render_event`
/// in the pod crate) — that path applies prompt-pack wrapping, while
/// in the worker crate) — that path applies prompt-pack wrapping, while
/// this is the human-facing rendering of the raw structured event.
fn format_pod_event(event: &PodEvent) -> String {
fn format_worker_event(event: &WorkerEvent) -> String {
match event {
PodEvent::TurnEnded { pod_name } => {
format!("[pod_event] {pod_name} → turn_ended")
WorkerEvent::TurnEnded { worker_name } => {
format!("[worker_event] {worker_name} → turn_ended")
}
PodEvent::Errored { pod_name, message } => {
format!("[pod_event] {pod_name} → errored: {message}")
WorkerEvent::Errored {
worker_name,
message,
} => {
format!("[worker_event] {worker_name} → errored: {message}")
}
PodEvent::ShutDown { pod_name } => {
format!("[pod_event] {pod_name} → shut_down")
WorkerEvent::ShutDown { worker_name } => {
format!("[worker_event] {worker_name} → shut_down")
}
PodEvent::ScopeSubDelegated {
parent_pod,
sub_pod,
WorkerEvent::ScopeSubDelegated {
parent_worker,
sub_worker,
..
} => {
format!("[pod_event] {parent_pod} → scope_sub_delegated: {sub_pod}")
format!("[worker_event] {parent_worker} → scope_sub_delegated: {sub_worker}")
}
}
}
@@ -1920,13 +1923,13 @@ fn format_pod_event(event: &PodEvent) -> String {
mod tests {
use super::*;
use crate::app::{ActionbarNoticeLevel, ActionbarNoticeSource, App};
use protocol::PodStatus;
use protocol::WorkerStatus;
use std::time::{Duration, Instant};
#[test]
fn queue_status_text_includes_count_and_preview() {
let mut app = App::new("test".into());
app.set_pod_status(PodStatus::Running);
app.set_worker_status(WorkerStatus::Running);
for c in "queued preview".chars() {
app.insert_char(c);
}
@@ -1952,7 +1955,7 @@ mod tests {
app.latest_llm_wait_event = Some("retrying LLM request".into());
app.latest_memory_worker_event = Some("memory extract running".into());
app.flash_actionbar_notice_at(
"Pod keeps running. Press Ctrl-C again to exit TUI.",
"Worker keeps running. Press Ctrl-C again to exit TUI.",
ActionbarNoticeLevel::Warn,
ActionbarNoticeSource::Tui,
now,
@@ -1961,10 +1964,10 @@ mod tests {
assert_eq!(
actionbar_left_item(&app, now).map(|(text, _)| text),
Some("Pod keeps running. Press Ctrl-C again to exit TUI.".into())
Some("Worker keeps running. Press Ctrl-C again to exit TUI.".into())
);
app.set_pod_status(PodStatus::Running);
app.set_worker_status(WorkerStatus::Running);
for c in "queued turn".chars() {
app.insert_char(c);
}
@@ -2037,7 +2040,7 @@ mod tests {
#[test]
fn consecutive_thinking_blocks_render_as_one_normal_group() {
let mut app = App::new("pod".to_string());
let mut app = App::new("worker".to_string());
app.mode = Mode::Normal;
app.blocks = vec![finished_thinking("alpha"), finished_thinking("beta")];
@@ -2055,7 +2058,7 @@ mod tests {
#[test]
fn thinking_group_detail_keeps_each_body_readable() {
let mut app = App::new("pod".to_string());
let mut app = App::new("worker".to_string());
app.mode = Mode::Detail;
app.blocks = vec![
finished_thinking("alpha line 1\nalpha line 2"),
@@ -2074,7 +2077,7 @@ mod tests {
#[test]
fn non_thinking_separator_breaks_thinking_group() {
let mut app = App::new("pod".to_string());
let mut app = App::new("worker".to_string());
app.mode = Mode::Normal;
app.blocks = vec![
finished_thinking("alpha"),
@@ -2097,7 +2100,7 @@ mod tests {
#[test]
fn turn_header_breaks_thinking_group() {
let mut app = App::new("pod".to_string());
let mut app = App::new("worker".to_string());
app.mode = Mode::Normal;
app.blocks = vec![
Block::TurnHeader { turn: 1 },
@@ -2119,7 +2122,7 @@ mod tests {
#[test]
fn thinking_group_preserves_streaming_and_incomplete_state_visibility() {
let mut app = App::new("pod".to_string());
let mut app = App::new("worker".to_string());
app.mode = Mode::Normal;
app.blocks = vec![
finished_thinking("finished"),
@@ -2141,7 +2144,7 @@ mod tests {
#[test]
fn single_thinking_block_rendering_stays_unchanged() {
let mut app = App::new("pod".to_string());
let mut app = App::new("worker".to_string());
app.mode = Mode::Normal;
app.blocks = vec![Block::Thinking(ThinkingBlock {
text: "private reasoning".to_string(),
@@ -2159,7 +2162,7 @@ mod tests {
fn single_tool_block_rendering_stays_unchanged() {
use crate::block::{ToolCallBlock, ToolCallState};
let mut app = App::new("pod".to_string());
let mut app = App::new("worker".to_string());
app.mode = Mode::Normal;
app.blocks = vec![Block::ToolCall(ToolCallBlock {
id: "bash-1".to_string(),
@@ -2183,7 +2186,7 @@ mod tests {
fn read_tool_aggregation_still_consumes_consecutive_tool_blocks() {
use crate::block::{ToolCallBlock, ToolCallState};
let mut app = App::new("pod".to_string());
let mut app = App::new("worker".to_string());
app.mode = Mode::Normal;
app.blocks = vec![
Block::ToolCall(ToolCallBlock {
@@ -2225,7 +2228,7 @@ mod tests {
#[test]
fn history_rows_mark_text_items_selectable_and_non_text_unselectable() {
let mut app = App::new("pod".to_string());
let mut app = App::new("worker".to_string());
app.blocks = vec![
Block::UserMessage {
segments: vec![Segment::Text {
@@ -3,45 +3,45 @@ use std::io;
use std::path::{Path, PathBuf};
use std::time::Duration;
use client::PodClient;
use client::WorkerClient;
use pod_registry::{LockFileGuard, default_registry_path};
use pod_store::{PodActiveSegmentRef, PodMetadata, PodMetadataStore};
use protocol::{Event, PodStatus};
use pod_store::{WorkerActiveSegmentRef, WorkerMetadata, WorkerMetadataStore};
use protocol::{Event, WorkerStatus};
use session_store::{FsStore, SegmentId, SessionId};
#[derive(Debug, Clone)]
pub(crate) struct PodList {
pub entries: Vec<PodListEntry>,
pub(crate) struct WorkerList {
pub entries: Vec<WorkerListEntry>,
pub selected_name: Option<String>,
}
impl PodList {
impl WorkerList {
pub(crate) fn from_sources(
source: PodVisibilitySource,
stored: Vec<StoredPodInfo>,
live: Vec<LivePodInfo>,
source: WorkerVisibilitySource,
stored: Vec<StoredWorkerInfo>,
live: Vec<LiveWorkerInfo>,
selected_name: Option<String>,
max_entries: usize,
) -> Self {
let mut entries_by_name: BTreeMap<String, PodListEntry> = BTreeMap::new();
let mut entries_by_name: BTreeMap<String, WorkerListEntry> = BTreeMap::new();
for stored_info in stored {
let name = stored_info.pod_name.clone();
let name = stored_info.worker_name.clone();
entries_by_name
.entry(name.clone())
.or_insert_with(|| PodListEntry::new(name, source))
.or_insert_with(|| WorkerListEntry::new(name, source))
.merge_stored(stored_info);
}
for live_info in live {
let name = live_info.pod_name.clone();
let name = live_info.worker_name.clone();
entries_by_name
.entry(name.clone())
.or_insert_with(|| PodListEntry::new(name, source))
.or_insert_with(|| WorkerListEntry::new(name, source))
.merge_live(live_info);
}
let mut entries: Vec<PodListEntry> = entries_by_name.into_values().collect();
let mut entries: Vec<WorkerListEntry> = entries_by_name.into_values().collect();
for entry in &mut entries {
entry.finalize();
}
@@ -64,9 +64,9 @@ impl PodList {
}
pub(crate) fn from_workspace_sources(
source: PodVisibilitySource,
stored: Vec<StoredPodInfo>,
live: Vec<LivePodInfo>,
source: WorkerVisibilitySource,
stored: Vec<StoredWorkerInfo>,
live: Vec<LiveWorkerInfo>,
selected_name: Option<String>,
max_entries: usize,
workspace_root: &Path,
@@ -81,14 +81,14 @@ impl PodList {
.as_deref()
.is_some_and(|root| workspace_root_key(root) == current_workspace);
if matches {
current_names.insert(info.pod_name.clone());
current_names.insert(info.worker_name.clone());
}
matches
})
.collect();
let live = live
.into_iter()
.filter(|info| current_names.contains(&info.pod_name))
.filter(|info| current_names.contains(&info.worker_name))
.collect();
Self::from_sources(source, stored, live, selected_name, max_entries)
}
@@ -124,7 +124,7 @@ impl PodList {
self.selected_name = self.entries.get(index).map(|entry| entry.name.clone());
}
pub(crate) fn selected_entry(&self) -> Option<&PodListEntry> {
pub(crate) fn selected_entry(&self) -> Option<&WorkerListEntry> {
let index = self.selected_index();
self.entries.get(index)
}
@@ -134,7 +134,7 @@ fn workspace_root_key(path: &Path) -> PathBuf {
path.canonicalize().unwrap_or_else(|_| path.to_path_buf())
}
fn entry_belongs_to_workspace(entry: &PodListEntry, current_workspace: &Path) -> bool {
fn entry_belongs_to_workspace(entry: &WorkerListEntry, current_workspace: &Path) -> bool {
entry
.stored
.as_ref()
@@ -143,48 +143,49 @@ fn entry_belongs_to_workspace(entry: &PodListEntry, current_workspace: &Path) ->
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum PodVisibilitySource {
pub(crate) enum WorkerVisibilitySource {
ResumePicker,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum PodListSourceKind {
pub(crate) enum WorkerListSourceKind {
RuntimeRegistry,
StoredMetadata,
}
#[derive(Debug, Clone)]
pub(crate) struct PodListEntry {
pub(crate) struct WorkerListEntry {
pub name: String,
pub visibility: PodVisibilitySource,
pub source_kinds: Vec<PodListSourceKind>,
pub live: Option<LivePodInfo>,
pub stored: Option<StoredPodInfo>,
pub summary: PodEntrySummary,
pub actions: PodEntryActions,
pub diagnostics: Vec<PodEntryDiagnostic>,
pub visibility: WorkerVisibilitySource,
pub source_kinds: Vec<WorkerListSourceKind>,
pub live: Option<LiveWorkerInfo>,
pub stored: Option<StoredWorkerInfo>,
pub summary: WorkerEntrySummary,
pub actions: WorkerEntryActions,
pub diagnostics: Vec<WorkerEntryDiagnostic>,
}
impl PodListEntry {
fn new(name: String, visibility: PodVisibilitySource) -> Self {
impl WorkerListEntry {
fn new(name: String, visibility: WorkerVisibilitySource) -> Self {
Self {
name,
visibility,
source_kinds: Vec::new(),
live: None,
stored: None,
summary: PodEntrySummary::default(),
actions: PodEntryActions::default(),
summary: WorkerEntrySummary::default(),
actions: WorkerEntryActions::default(),
diagnostics: Vec::new(),
}
}
fn merge_live(&mut self, live: LivePodInfo) {
fn merge_live(&mut self, live: LiveWorkerInfo) {
if !self
.source_kinds
.contains(&PodListSourceKind::RuntimeRegistry)
.contains(&WorkerListSourceKind::RuntimeRegistry)
{
self.source_kinds.push(PodListSourceKind::RuntimeRegistry);
self.source_kinds
.push(WorkerListSourceKind::RuntimeRegistry);
}
if live.summary.updated_at > self.summary.updated_at {
self.summary.updated_at = live.summary.updated_at;
@@ -201,12 +202,12 @@ impl PodListEntry {
self.live = Some(live);
}
fn merge_stored(&mut self, stored: StoredPodInfo) {
fn merge_stored(&mut self, stored: StoredWorkerInfo) {
if !self
.source_kinds
.contains(&PodListSourceKind::StoredMetadata)
.contains(&WorkerListSourceKind::StoredMetadata)
{
self.source_kinds.push(PodListSourceKind::StoredMetadata);
self.source_kinds.push(WorkerListSourceKind::StoredMetadata);
}
if stored.updated_at > self.summary.updated_at {
self.summary.updated_at = stored.updated_at;
@@ -254,18 +255,18 @@ impl PodListEntry {
}
#[derive(Debug, Clone)]
pub(crate) struct LivePodInfo {
pub pod_name: String,
pub(crate) struct LiveWorkerInfo {
pub worker_name: String,
pub socket_path: PathBuf,
pub status: Option<PodStatus>,
pub status: Option<WorkerStatus>,
pub reachable: bool,
pub segment_id: Option<SegmentId>,
pub summary: PodEntrySummary,
pub summary: WorkerEntrySummary,
}
#[derive(Debug, Clone)]
pub(crate) struct StoredPodInfo {
pub pod_name: String,
pub(crate) struct StoredWorkerInfo {
pub worker_name: String,
pub metadata_state: StoredMetadataState,
pub active_session_id: Option<SessionId>,
pub active_segment_id: Option<SegmentId>,
@@ -281,7 +282,7 @@ pub(crate) enum StoredMetadataState {
}
#[derive(Debug, Clone, Default)]
pub(crate) struct PodEntrySummary {
pub(crate) struct WorkerEntrySummary {
pub active_session_id: Option<SessionId>,
pub active_segment_id: Option<SegmentId>,
pub updated_at: u64,
@@ -289,7 +290,7 @@ pub(crate) struct PodEntrySummary {
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct PodEntryActions {
pub(crate) struct WorkerEntryActions {
pub can_open: bool,
pub can_restore: bool,
pub can_send_now: bool,
@@ -298,67 +299,67 @@ pub(crate) struct PodEntryActions {
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct PodEntryDiagnostic {
pub kind: PodEntryDiagnosticKind,
pub(crate) struct WorkerEntryDiagnostic {
pub kind: WorkerEntryDiagnosticKind,
pub message: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum PodEntryDiagnosticKind {
pub(crate) enum WorkerEntryDiagnosticKind {
StoredMetadataCorrupt,
LiveUnreachable,
MissingStoredMetadata,
MissingLiveStatus,
}
pub(crate) fn read_stored_pod_infos(
pub(crate) fn read_stored_worker_infos(
store: &FsStore,
pod_store: &impl PodMetadataStore,
) -> Result<Vec<StoredPodInfo>, io::Error> {
pod_store: &impl WorkerMetadataStore,
) -> Result<Vec<StoredWorkerInfo>, io::Error> {
let mut records = Vec::new();
for pod_name in pod_store.list_names().map_err(io::Error::other)? {
let info = match pod_store.read_by_name(&pod_name) {
Ok(Some(metadata)) => stored_info_from_metadata(store, pod_name, metadata),
for worker_name in pod_store.list_names().map_err(io::Error::other)? {
let info = match pod_store.read_by_name(&worker_name) {
Ok(Some(metadata)) => stored_info_from_metadata(store, worker_name, metadata),
Ok(None) => corrupt_stored_info(
pod_name,
worker_name,
"metadata disappeared during discovery".to_string(),
),
Err(e) => corrupt_stored_info(pod_name, e.to_string()),
Err(e) => corrupt_stored_info(worker_name, e.to_string()),
};
records.push(info);
}
Ok(records)
}
pub(crate) fn read_live_pod_infos() -> Result<Vec<LivePodInfo>, io::Error> {
pub(crate) fn read_live_pod_infos() -> Result<Vec<LiveWorkerInfo>, io::Error> {
let path = default_registry_path()?;
let guard = LockFileGuard::open(&path)?;
Ok(guard
.data()
.allocations
.iter()
.map(|allocation| LivePodInfo {
pod_name: allocation.pod_name.clone(),
.map(|allocation| LiveWorkerInfo {
worker_name: allocation.worker_name.clone(),
socket_path: allocation.socket.clone(),
status: None,
reachable: false,
segment_id: allocation.segment_id,
summary: PodEntrySummary::default(),
summary: WorkerEntrySummary::default(),
})
.collect())
}
pub(crate) async fn read_reachable_live_pod_infos(
store: &FsStore,
) -> Result<Vec<LivePodInfo>, io::Error> {
) -> Result<Vec<LiveWorkerInfo>, io::Error> {
let records = read_live_pod_infos()?;
probe_reachable_live_pod_infos(store, records).await
}
async fn probe_reachable_live_pod_infos(
_store: &FsStore,
records: Vec<LivePodInfo>,
) -> Result<Vec<LivePodInfo>, io::Error> {
records: Vec<LiveWorkerInfo>,
) -> Result<Vec<LiveWorkerInfo>, io::Error> {
let mut handles = Vec::with_capacity(records.len());
for record in records {
handles.push(tokio::spawn(probe_live_pod_info(record)));
@@ -377,33 +378,33 @@ async fn probe_reachable_live_pod_infos(
Ok(reachable)
}
async fn probe_live_pod_info(mut record: LivePodInfo) -> Result<LivePodInfo, io::Error> {
async fn probe_live_pod_info(mut record: LiveWorkerInfo) -> Result<LiveWorkerInfo, io::Error> {
let status = probe_live_status(&record.socket_path).await?;
record.reachable = true;
record.status = status;
Ok(record)
}
pub(crate) fn live_socket_for_pod(pod_name: &str) -> Option<PathBuf> {
pub(crate) fn live_socket_for_pod(worker_name: &str) -> Option<PathBuf> {
read_live_pod_infos()
.ok()?
.into_iter()
.find(|pod| pod.pod_name == pod_name)
.map(|pod| pod.socket_path)
.find(|worker| worker.worker_name == worker_name)
.map(|worker| worker.socket_path)
}
fn stored_info_from_metadata(
store: &FsStore,
pod_name: String,
metadata: PodMetadata,
) -> StoredPodInfo {
worker_name: String,
metadata: WorkerMetadata,
) -> StoredWorkerInfo {
let active = metadata.active;
let active_session_id = active.as_ref().map(|a| a.session_id);
let active_segment_id = active.as_ref().and_then(|a| a.segment_id);
let summary = summarize_metadata(store, active.as_ref());
StoredPodInfo {
pod_name,
StoredWorkerInfo {
worker_name,
metadata_state: StoredMetadataState::Present,
active_session_id,
active_segment_id,
@@ -413,9 +414,9 @@ fn stored_info_from_metadata(
}
}
fn corrupt_stored_info(pod_name: String, message: String) -> StoredPodInfo {
StoredPodInfo {
pod_name,
fn corrupt_stored_info(worker_name: String, message: String) -> StoredWorkerInfo {
StoredWorkerInfo {
worker_name,
metadata_state: StoredMetadataState::Corrupt(message.clone()),
active_session_id: None,
active_segment_id: None,
@@ -427,8 +428,8 @@ fn corrupt_stored_info(pod_name: String, message: String) -> StoredPodInfo {
const LIVE_STATUS_PROBE_TIMEOUT: Duration = Duration::from_millis(200);
async fn probe_live_status(socket_path: &Path) -> Result<Option<PodStatus>, io::Error> {
let mut client = PodClient::connect(socket_path).await?;
async fn probe_live_status(socket_path: &Path) -> Result<Option<WorkerStatus>, io::Error> {
let mut client = WorkerClient::connect(socket_path).await?;
let deadline = tokio::time::Instant::now() + LIVE_STATUS_PROBE_TIMEOUT;
loop {
@@ -446,7 +447,7 @@ async fn probe_live_status(socket_path: &Path) -> Result<Option<PodStatus>, io::
}
}
fn status_from_event(event: &Event) -> Option<PodStatus> {
fn status_from_event(event: &Event) -> Option<WorkerStatus> {
match event {
Event::Snapshot { status, .. } | Event::Status { status } => Some(*status),
_ => None,
@@ -459,7 +460,7 @@ struct SegmentSummary {
preview: Option<String>,
}
fn summarize_metadata(_store: &FsStore, active: Option<&PodActiveSegmentRef>) -> SegmentSummary {
fn summarize_metadata(_store: &FsStore, active: Option<&WorkerActiveSegmentRef>) -> SegmentSummary {
let Some(active) = active else {
return SegmentSummary {
updated_at: 0,
@@ -478,33 +479,33 @@ fn summarize_metadata(_store: &FsStore, active: Option<&PodActiveSegmentRef>) ->
}
}
fn build_diagnostics(entry: &PodListEntry) -> Vec<PodEntryDiagnostic> {
fn build_diagnostics(entry: &WorkerListEntry) -> Vec<WorkerEntryDiagnostic> {
let mut diagnostics = Vec::new();
if let Some(stored) = entry.stored.as_ref() {
if let StoredMetadataState::Corrupt(message) = &stored.metadata_state {
diagnostics.push(PodEntryDiagnostic {
kind: PodEntryDiagnosticKind::StoredMetadataCorrupt,
diagnostics.push(WorkerEntryDiagnostic {
kind: WorkerEntryDiagnosticKind::StoredMetadataCorrupt,
message: format!("metadata: {}", trim_one_line(message, 80)),
});
}
} else if entry.live.is_some() {
diagnostics.push(PodEntryDiagnostic {
kind: PodEntryDiagnosticKind::MissingStoredMetadata,
message: "no stored pod metadata".to_string(),
diagnostics.push(WorkerEntryDiagnostic {
kind: WorkerEntryDiagnosticKind::MissingStoredMetadata,
message: "no stored worker metadata".to_string(),
});
}
if let Some(live) = entry.live.as_ref() {
if !live.reachable {
diagnostics.push(PodEntryDiagnostic {
kind: PodEntryDiagnosticKind::LiveUnreachable,
diagnostics.push(WorkerEntryDiagnostic {
kind: WorkerEntryDiagnosticKind::LiveUnreachable,
message: format!("socket unreachable: {}", live.socket_path.display()),
});
} else if live.status.is_none() {
diagnostics.push(PodEntryDiagnostic {
kind: PodEntryDiagnosticKind::MissingLiveStatus,
message: "live pod status was not reported".to_string(),
diagnostics.push(WorkerEntryDiagnostic {
kind: WorkerEntryDiagnosticKind::MissingLiveStatus,
message: "live worker status was not reported".to_string(),
});
}
}
@@ -512,7 +513,7 @@ fn build_diagnostics(entry: &PodListEntry) -> Vec<PodEntryDiagnostic> {
diagnostics
}
fn build_actions(entry: &PodListEntry) -> PodEntryActions {
fn build_actions(entry: &WorkerListEntry) -> WorkerEntryActions {
let live_reachable = entry.live.as_ref().is_some_and(|live| live.reachable);
let stored_restorable = entry
.stored
@@ -522,19 +523,19 @@ fn build_actions(entry: &PodListEntry) -> PodEntryActions {
let can_restore = stored_restorable && !live_reachable;
let can_open = live_reachable || stored_restorable;
let can_send_now = live_reachable && live_status == Some(PodStatus::Idle);
let can_queue_send = live_reachable && live_status == Some(PodStatus::Running);
let can_send_now = live_reachable && live_status == Some(WorkerStatus::Idle);
let can_queue_send = live_reachable && live_status == Some(WorkerStatus::Running);
let disabled_reason = if can_open {
None
} else if entry.live.is_some() {
Some("live pod is unreachable".to_string())
Some("live worker is unreachable".to_string())
} else if entry.stored.is_some() {
Some("stored pod metadata is corrupt".to_string())
Some("stored worker metadata is corrupt".to_string())
} else {
Some("no live or stored pod state".to_string())
Some("no live or stored worker state".to_string())
};
PodEntryActions {
WorkerEntryActions {
can_open,
can_restore,
can_send_now,
@@ -559,15 +560,15 @@ mod tests {
use std::sync::Arc;
use llm_engine::llm_client::types::RequestConfig;
use pod_store::FsPodStore;
use pod_store::{PodActiveSegmentRef, PodMetadataStore};
use pod_store::FsWorkerStore;
use pod_store::{WorkerActiveSegmentRef, WorkerMetadataStore};
use protocol::stream::JsonLineWriter;
use session_store::{LogEntry, Store, new_segment_id, new_session_id};
use tempfile::tempdir;
use tokio::net::UnixListener;
use tokio::sync::Barrier;
const SOURCE: PodVisibilitySource = PodVisibilitySource::ResumePicker;
const SOURCE: WorkerVisibilitySource = WorkerVisibilitySource::ResumePicker;
#[test]
fn stored_metadata_summary_uses_segment_marker_without_reading_session_log() {
@@ -585,7 +586,7 @@ mod tests {
"session log text should not be scanned",
);
let entry = single_entry(PodList::from_sources(
let entry = single_entry(WorkerList::from_sources(
SOURCE,
vec![metadata_info(&store, "stored", session, segment)],
vec![],
@@ -606,9 +607,9 @@ mod tests {
let stopped = (0..10)
.map(|index| stopped_info_with_updated_at(&format!("stopped-{index}"), 1_000 - index))
.collect::<Vec<_>>();
let live = live_info_with_updated_at("live-pending", PodStatus::Idle, 0);
let live = live_info_with_updated_at("live-pending", WorkerStatus::Idle, 0);
let entries = PodList::from_sources(SOURCE, stopped, vec![live], None, 10).entries;
let entries = WorkerList::from_sources(SOURCE, stopped, vec![live], None, 10).entries;
assert_eq!(entries.len(), 10);
assert_eq!(entries[0].name, "live-pending");
@@ -617,11 +618,11 @@ mod tests {
#[test]
fn reachable_live_sort_does_not_promote_unreachable_registry_allocations() {
let mut unreachable = live_info_with_updated_at("unreachable", PodStatus::Idle, 0);
let mut unreachable = live_info_with_updated_at("unreachable", WorkerStatus::Idle, 0);
unreachable.reachable = false;
unreachable.status = None;
let entries = PodList::from_sources(
let entries = WorkerList::from_sources(
SOURCE,
vec![stopped_info_with_updated_at("stopped", 100)],
vec![unreachable],
@@ -638,12 +639,12 @@ mod tests {
fn live_pending_with_runtime_segment_is_attach_only_and_gets_pending_preview() {
let session_id = new_session_id();
let runtime_segment_id = new_segment_id();
let entry = single_entry(PodList::from_sources(
let entry = single_entry(WorkerList::from_sources(
SOURCE,
vec![pending_metadata_info("pending", session_id)],
vec![live_info_with_segment(
"pending",
PodStatus::Idle,
WorkerStatus::Idle,
runtime_segment_id,
)],
None,
@@ -668,12 +669,12 @@ mod tests {
#[test]
fn live_only_runtime_segment_is_attach_only_and_not_restorable() {
let runtime_segment_id = new_segment_id();
let entry = single_entry(PodList::from_sources(
let entry = single_entry(WorkerList::from_sources(
SOURCE,
vec![],
vec![live_info_with_segment(
"runtime-only",
PodStatus::Idle,
WorkerStatus::Idle,
runtime_segment_id,
)],
None,
@@ -701,7 +702,7 @@ mod tests {
let segment_id = new_segment_id();
append_start(&store, session_id, segment_id, 10);
let entry = single_entry(PodList::from_sources(
let entry = single_entry(WorkerList::from_sources(
SOURCE,
vec![metadata_info(&store, "stored", session_id, segment_id)],
vec![],
@@ -711,7 +712,10 @@ mod tests {
assert_eq!(entry.name, "stored");
assert_eq!(entry.visibility, SOURCE);
assert_eq!(entry.source_kinds, vec![PodListSourceKind::StoredMetadata]);
assert_eq!(
entry.source_kinds,
vec![WorkerListSourceKind::StoredMetadata]
);
assert!(entry.live.is_none());
assert!(entry.stored.is_some());
assert!(entry.actions.can_open);
@@ -722,17 +726,20 @@ mod tests {
#[test]
fn live_idle_reachable_row_can_open_and_send_now() {
let entry = single_entry(PodList::from_sources(
let entry = single_entry(WorkerList::from_sources(
SOURCE,
vec![],
vec![live_info("live", PodStatus::Idle)],
vec![live_info("live", WorkerStatus::Idle)],
None,
10,
));
assert_eq!(entry.name, "live");
assert_eq!(entry.visibility, SOURCE);
assert_eq!(entry.source_kinds, vec![PodListSourceKind::RuntimeRegistry]);
assert_eq!(
entry.source_kinds,
vec![WorkerListSourceKind::RuntimeRegistry]
);
assert!(entry.actions.can_open);
assert!(!entry.actions.can_restore);
assert!(entry.actions.can_send_now);
@@ -745,11 +752,17 @@ mod tests {
#[test]
fn live_reachable_row_without_reported_status_can_open_but_not_send_now() {
let mut live = live_info("live", PodStatus::Idle);
let mut live = live_info("live", WorkerStatus::Idle);
live.status = None;
live.reachable = true;
let entry = single_entry(PodList::from_sources(SOURCE, vec![], vec![live], None, 10));
let entry = single_entry(WorkerList::from_sources(
SOURCE,
vec![],
vec![live],
None,
10,
));
assert!(entry.actions.can_open);
assert!(!entry.actions.can_restore);
@@ -763,16 +776,16 @@ mod tests {
!entry
.diagnostics
.iter()
.any(|diagnostic| diagnostic.kind == PodEntryDiagnosticKind::LiveUnreachable)
.any(|diagnostic| diagnostic.kind == WorkerEntryDiagnosticKind::LiveUnreachable)
);
}
#[test]
fn live_running_reachable_row_can_open_but_not_send_now() {
let entry = single_entry(PodList::from_sources(
let entry = single_entry(WorkerList::from_sources(
SOURCE,
vec![],
vec![live_info("live", PodStatus::Running)],
vec![live_info("live", WorkerStatus::Running)],
None,
10,
));
@@ -785,11 +798,17 @@ mod tests {
#[test]
fn live_unreachable_row_has_diagnostic_and_cannot_open() {
let mut live = live_info("live", PodStatus::Idle);
let mut live = live_info("live", WorkerStatus::Idle);
live.reachable = false;
live.status = None;
let entry = single_entry(PodList::from_sources(SOURCE, vec![], vec![live], None, 10));
let entry = single_entry(WorkerList::from_sources(
SOURCE,
vec![],
vec![live],
None,
10,
));
assert!(!entry.actions.can_open);
assert!(!entry.actions.can_restore);
@@ -797,11 +816,11 @@ mod tests {
assert!(!entry.actions.can_queue_send);
assert_eq!(
entry.actions.disabled_reason.as_deref(),
Some("live pod is unreachable")
Some("live worker is unreachable")
);
assert_eq!(entry.attach_socket_path(), None);
assert!(entry.diagnostics.iter().any(|diagnostic| {
diagnostic.kind == PodEntryDiagnosticKind::LiveUnreachable
diagnostic.kind == WorkerEntryDiagnosticKind::LiveUnreachable
&& diagnostic.message.contains("/tmp/live.sock")
}));
}
@@ -811,20 +830,20 @@ mod tests {
let events = [
Event::Alert(protocol::Alert {
level: protocol::AlertLevel::Warn,
source: protocol::AlertSource::Pod,
source: protocol::AlertSource::Worker,
message: "warming up".to_string(),
timestamp_ms: 0,
}),
Event::Snapshot {
entries: vec![],
greeting: test_greeting(),
status: PodStatus::Idle,
status: WorkerStatus::Idle,
in_flight: Default::default(),
},
];
let status = events.iter().find_map(status_from_event);
assert_eq!(status, Some(PodStatus::Idle));
assert_eq!(status, Some(WorkerStatus::Idle));
}
#[tokio::test]
@@ -838,8 +857,8 @@ mod tests {
let mut servers = Vec::new();
for index in 0..probe_count {
let pod_name = format!("pod-{index}");
let socket_path = socket_dir.path().join(format!("{pod_name}.sock"));
let worker_name = format!("worker-{index}");
let socket_path = socket_dir.path().join(format!("{worker_name}.sock"));
let listener = UnixListener::bind(&socket_path).unwrap();
let barrier = Arc::clone(&barrier);
servers.push(tokio::spawn(async move {
@@ -848,12 +867,12 @@ mod tests {
let mut writer = JsonLineWriter::new(stream);
writer
.write(&Event::Status {
status: PodStatus::Idle,
status: WorkerStatus::Idle,
})
.await
.unwrap();
}));
records.push(live_probe_record(&pod_name, socket_path));
records.push(live_probe_record(&worker_name, socket_path));
}
let records = tokio::time::timeout(
@@ -869,7 +888,7 @@ mod tests {
assert!(
records
.iter()
.all(|record| record.status == Some(PodStatus::Idle))
.all(|record| record.status == Some(WorkerStatus::Idle))
);
for server in servers {
server.await.unwrap();
@@ -896,7 +915,7 @@ mod tests {
.unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0].pod_name, "silent");
assert_eq!(records[0].worker_name, "silent");
assert!(records[0].reachable);
assert_eq!(records[0].status, None);
assert_eq!(records[0].socket_path, socket_path);
@@ -905,7 +924,7 @@ mod tests {
#[test]
fn corrupt_stored_metadata_has_diagnostic() {
let entry = single_entry(PodList::from_sources(
let entry = single_entry(WorkerList::from_sources(
SOURCE,
vec![corrupt_stored_info(
"broken".to_string(),
@@ -919,7 +938,7 @@ mod tests {
assert_eq!(entry.name, "broken");
assert!(!entry.actions.can_open);
assert!(entry.diagnostics.iter().any(|diagnostic| {
diagnostic.kind == PodEntryDiagnosticKind::StoredMetadataCorrupt
diagnostic.kind == WorkerEntryDiagnosticKind::StoredMetadataCorrupt
&& diagnostic.message.contains("expected value")
}));
assert!(
@@ -933,25 +952,25 @@ mod tests {
}
#[test]
fn selected_pod_name_is_kept_after_rebuild() {
let first = PodList::from_sources(
fn selected_worker_name_is_kept_after_rebuild() {
let first = WorkerList::from_sources(
SOURCE,
vec![],
vec![
live_info("alpha", PodStatus::Idle),
live_info("beta", PodStatus::Idle),
live_info("alpha", WorkerStatus::Idle),
live_info("beta", WorkerStatus::Idle),
],
Some("alpha".to_string()),
10,
);
assert_eq!(first.selected_entry().unwrap().name, "alpha");
let rebuilt = PodList::from_sources(
let rebuilt = WorkerList::from_sources(
SOURCE,
vec![],
vec![
live_info_with_updated_at("beta", PodStatus::Idle, 20),
live_info_with_updated_at("alpha", PodStatus::Idle, 10),
live_info_with_updated_at("beta", WorkerStatus::Idle, 20),
live_info_with_updated_at("alpha", WorkerStatus::Idle, 10),
],
first.selected_name.clone(),
10,
@@ -963,17 +982,17 @@ mod tests {
}
#[test]
fn read_stored_pod_infos_reports_corrupt_metadata() {
fn read_stored_worker_infos_reports_corrupt_metadata() {
let dir = tempdir().unwrap();
let store = FsStore::new(dir.path()).unwrap();
let pod_store = FsPodStore::new(dir.path().join("pods")).unwrap();
let pod_store = FsWorkerStore::new(dir.path().join("pods")).unwrap();
let pod_dir = dir.path().join("pods").join("broken");
std::fs::create_dir_all(&pod_dir).unwrap();
std::fs::write(pod_dir.join("metadata.json"), "{not-json").unwrap();
let records = read_stored_pod_infos(&store, &pod_store).unwrap();
let records = read_stored_worker_infos(&store, &pod_store).unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0].pod_name, "broken");
assert_eq!(records[0].worker_name, "broken");
assert!(matches!(
records[0].metadata_state,
StoredMetadataState::Corrupt(_)
@@ -981,49 +1000,53 @@ mod tests {
}
#[test]
fn read_stored_pod_infos_reads_metadata() {
fn read_stored_worker_infos_reads_metadata() {
let dir = tempdir().unwrap();
let store = FsStore::new(dir.path()).unwrap();
let pod_store = FsPodStore::new(dir.path().join("pods")).unwrap();
let pod_store = FsWorkerStore::new(dir.path().join("pods")).unwrap();
let session_id = new_session_id();
let segment_id = new_segment_id();
pod_store
.write(&PodMetadata::new(
.write(&WorkerMetadata::new(
"agent",
Some(PodActiveSegmentRef::active_segment(session_id, segment_id)),
Some(WorkerActiveSegmentRef::active_segment(
session_id, segment_id,
)),
))
.unwrap();
let records = read_stored_pod_infos(&store, &pod_store).unwrap();
let records = read_stored_worker_infos(&store, &pod_store).unwrap();
assert_eq!(records.len(), 1);
assert_eq!(records[0].pod_name, "agent");
assert_eq!(records[0].worker_name, "agent");
assert_eq!(records[0].metadata_state, StoredMetadataState::Present);
}
fn single_entry(list: PodList) -> PodListEntry {
fn single_entry(list: WorkerList) -> WorkerListEntry {
assert_eq!(list.entries.len(), 1);
list.entries.into_iter().next().unwrap()
}
fn metadata_info(
store: &FsStore,
pod_name: &str,
worker_name: &str,
session_id: SessionId,
segment_id: SegmentId,
) -> StoredPodInfo {
) -> StoredWorkerInfo {
stored_info_from_metadata(
store,
pod_name.to_string(),
PodMetadata::new(
pod_name,
Some(PodActiveSegmentRef::active_segment(session_id, segment_id)),
worker_name.to_string(),
WorkerMetadata::new(
worker_name,
Some(WorkerActiveSegmentRef::active_segment(
session_id, segment_id,
)),
),
)
}
fn pending_metadata_info(pod_name: &str, session_id: SessionId) -> StoredPodInfo {
StoredPodInfo {
pod_name: pod_name.to_string(),
fn pending_metadata_info(worker_name: &str, session_id: SessionId) -> StoredWorkerInfo {
StoredWorkerInfo {
worker_name: worker_name.to_string(),
metadata_state: StoredMetadataState::Present,
active_session_id: Some(session_id),
active_segment_id: None,
@@ -1033,9 +1056,9 @@ mod tests {
}
}
fn stopped_info_with_updated_at(pod_name: &str, updated_at: u64) -> StoredPodInfo {
StoredPodInfo {
pod_name: pod_name.to_string(),
fn stopped_info_with_updated_at(worker_name: &str, updated_at: u64) -> StoredWorkerInfo {
StoredWorkerInfo {
worker_name: worker_name.to_string(),
metadata_state: StoredMetadataState::Present,
active_session_id: None,
active_segment_id: None,
@@ -1045,32 +1068,32 @@ mod tests {
}
}
fn live_info(pod_name: &str, status: PodStatus) -> LivePodInfo {
live_info_with_updated_at(pod_name, status, 0)
fn live_info(worker_name: &str, status: WorkerStatus) -> LiveWorkerInfo {
live_info_with_updated_at(worker_name, status, 0)
}
fn live_info_with_segment(
pod_name: &str,
status: PodStatus,
worker_name: &str,
status: WorkerStatus,
segment_id: SegmentId,
) -> LivePodInfo {
let mut info = live_info(pod_name, status);
) -> LiveWorkerInfo {
let mut info = live_info(worker_name, status);
info.segment_id = Some(segment_id);
info
}
fn live_info_with_updated_at(
pod_name: &str,
status: PodStatus,
worker_name: &str,
status: WorkerStatus,
updated_at: u64,
) -> LivePodInfo {
LivePodInfo {
pod_name: pod_name.to_string(),
socket_path: PathBuf::from(format!("/tmp/{pod_name}.sock")),
) -> LiveWorkerInfo {
LiveWorkerInfo {
worker_name: worker_name.to_string(),
socket_path: PathBuf::from(format!("/tmp/{worker_name}.sock")),
status: Some(status),
reachable: true,
segment_id: None,
summary: PodEntrySummary {
summary: WorkerEntrySummary {
active_session_id: None,
active_segment_id: None,
updated_at,
@@ -1079,20 +1102,20 @@ mod tests {
}
}
fn live_probe_record(pod_name: &str, socket_path: PathBuf) -> LivePodInfo {
LivePodInfo {
pod_name: pod_name.to_string(),
fn live_probe_record(worker_name: &str, socket_path: PathBuf) -> LiveWorkerInfo {
LiveWorkerInfo {
worker_name: worker_name.to_string(),
socket_path,
status: None,
reachable: false,
segment_id: None,
summary: PodEntrySummary::default(),
summary: WorkerEntrySummary::default(),
}
}
fn test_greeting() -> protocol::Greeting {
protocol::Greeting {
pod_name: "live".to_string(),
worker_name: "live".to_string(),
cwd: "/tmp".to_string(),
provider: "test".to_string(),
model: "test".to_string(),
@@ -1140,8 +1163,8 @@ mod tests {
.unwrap();
}
fn stopped_info_for_workspace(pod_name: &str, workspace_root: &Path) -> StoredPodInfo {
let mut info = stopped_info_with_updated_at(pod_name, 10);
fn stopped_info_for_workspace(worker_name: &str, workspace_root: &Path) -> StoredWorkerInfo {
let mut info = stopped_info_with_updated_at(worker_name, 10);
info.workspace_root = Some(workspace_root.to_path_buf());
info
}
@@ -1151,7 +1174,7 @@ mod tests {
let current = tempdir().unwrap();
let external = tempdir().unwrap();
let list = PodList::from_workspace_sources(
let list = WorkerList::from_workspace_sources(
SOURCE,
vec![
stopped_info_for_workspace("current", current.path()),
@@ -1161,11 +1184,11 @@ mod tests {
corrupt_stored_info("corrupt".to_string(), "invalid metadata".to_string()),
],
vec![
live_info("current", PodStatus::Idle),
live_info("current-orchestrator", PodStatus::Running),
live_info("other-workspace", PodStatus::Idle),
live_info("legacy-unknown", PodStatus::Idle),
live_info("live-only", PodStatus::Idle),
live_info("current", WorkerStatus::Idle),
live_info("current-orchestrator", WorkerStatus::Running),
live_info("other-workspace", WorkerStatus::Idle),
live_info("legacy-unknown", WorkerStatus::Idle),
live_info("live-only", WorkerStatus::Idle),
],
None,
10,
@@ -1186,20 +1209,20 @@ mod tests {
let current = tempdir().unwrap();
let worktree_cwd = current.path().join(".worktree/impl");
let list = PodList::from_workspace_sources(
let list = WorkerList::from_workspace_sources(
SOURCE,
vec![stopped_info_for_workspace("ticket-role", current.path())],
vec![live_info("ticket-role", PodStatus::Idle)],
vec![live_info("ticket-role", WorkerStatus::Idle)],
None,
10,
&worktree_cwd,
);
assert!(list.entries.is_empty());
let list = PodList::from_workspace_sources(
let list = WorkerList::from_workspace_sources(
SOURCE,
vec![stopped_info_for_workspace("ticket-role", current.path())],
vec![live_info("ticket-role", PodStatus::Idle)],
vec![live_info("ticket-role", WorkerStatus::Idle)],
None,
10,
current.path(),
File diff suppressed because it is too large Load Diff