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
+6 -6
View File
@@ -2,7 +2,7 @@
## Role
`tui` implements terminal UI clients for the single-Pod Console and workspace Dashboard surfaces.
`tui` implements terminal UI clients for the single-Worker Console and workspace Dashboard surfaces.
## Boundaries
@@ -10,21 +10,21 @@ Owns:
- terminal rendering and input handling
- local composer state and UI affordances
- single-Pod Console attach/restore/chat screens
- single-Worker Console attach/restore/chat screens
- workspace Dashboard presentation and role-action UI
Does not own:
- durable transcript authority (`session-store`)
- Pod current state (`pod-store`)
- Pod lifecycle policy (`pod`)
- Worker current state (`pod-store`)
- Worker lifecycle policy (`worker`)
- product CLI ownership (`yoi`)
## Design notes
The TUI should display committed events and Pod snapshots rather than inventing durable state. Local input history and optimistic UI affordances are editing conveniences; they must not become hidden model context.
The TUI should display committed events and Worker snapshots rather than inventing durable state. Local input history and optimistic UI affordances are editing conveniences; they must not become hidden model context.
## See also
- [`../../docs/design/context-history.md`](../../docs/design/context-history.md)
- [`../../docs/design/pod-session-state.md`](../../docs/design/pod-session-state.md)
- [`../../docs/design/worker-session-state.md`](../../docs/design/worker-session-state.md)
+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