refactor: rename pod crate to worker
This commit is contained in:
@@ -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
@@ -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(),
|
||||
});
|
||||
|
||||
|
||||
@@ -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
@@ -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"));
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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(®istry_socket)
|
||||
WorkerClient::connect(®istry_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(),
|
||||
|
||||
+398
-343
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||
|
||||
+258
-215
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
@@ -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
@@ -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,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
@@ -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(),
|
||||
+273
-230
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user