cli: support panel restore list
This commit is contained in:
parent
406104babd
commit
699290ccb1
|
|
@ -118,8 +118,11 @@ pub(crate) enum DashboardOutcome {
|
||||||
Open(OpenWorkerRequest),
|
Open(OpenWorkerRequest),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn launch(runtime_command: WorkerRuntimeCommand) -> Result<(), Box<dyn Error>> {
|
pub(crate) async fn launch(
|
||||||
let mut app = load_app(runtime_command.clone()).await?;
|
runtime_command: WorkerRuntimeCommand,
|
||||||
|
include_stopped: bool,
|
||||||
|
) -> Result<(), Box<dyn Error>> {
|
||||||
|
let mut app = load_app(runtime_command.clone(), include_stopped).await?;
|
||||||
let mut terminal = crate::console::enter_dashboard_fullscreen()?;
|
let mut terminal = crate::console::enter_dashboard_fullscreen()?;
|
||||||
loop {
|
loop {
|
||||||
match run_loop(&mut terminal, &mut app).await? {
|
match run_loop(&mut terminal, &mut app).await? {
|
||||||
|
|
@ -174,8 +177,9 @@ pub(crate) struct OpenWorkerRequest {
|
||||||
|
|
||||||
pub(crate) async fn load_app(
|
pub(crate) async fn load_app(
|
||||||
runtime_command: WorkerRuntimeCommand,
|
runtime_command: WorkerRuntimeCommand,
|
||||||
|
include_stopped: bool,
|
||||||
) -> Result<DashboardApp, DashboardError> {
|
) -> Result<DashboardApp, DashboardError> {
|
||||||
Ok(DashboardApp::loading(runtime_command))
|
Ok(DashboardApp::loading(runtime_command, include_stopped))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run_loop(
|
async fn run_loop(
|
||||||
|
|
@ -222,14 +226,14 @@ async fn run_loop(
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(mode) = deferred_enter_reload.take() {
|
if let Some(mode) = deferred_enter_reload.take() {
|
||||||
if pending_reload.start(mode) {
|
if pending_reload.start(mode, app.include_stopped) {
|
||||||
app.refreshing = true;
|
app.refreshing = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
if now >= next_poll {
|
if now >= next_poll {
|
||||||
pending_reload.start(OrchestratorLifecycleMode::Observe);
|
pending_reload.start(OrchestratorLifecycleMode::Observe, app.include_stopped);
|
||||||
next_poll = now + DASHBOARD_POLL_INTERVAL;
|
next_poll = now + DASHBOARD_POLL_INTERVAL;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -275,7 +279,8 @@ async fn run_loop(
|
||||||
terminal.draw(|f| render::draw(f, app))?;
|
terminal.draw(|f| render::draw(f, app))?;
|
||||||
let result = dispatch_ticket_action(request).await;
|
let result = dispatch_ticket_action(request).await;
|
||||||
app.finish_ticket_action_dispatch(result);
|
app.finish_ticket_action_dispatch(result);
|
||||||
if pending_reload.start(OrchestratorLifecycleMode::Observe) {
|
if pending_reload.start(OrchestratorLifecycleMode::Observe, app.include_stopped)
|
||||||
|
{
|
||||||
app.refreshing = true;
|
app.refreshing = true;
|
||||||
}
|
}
|
||||||
next_poll = Instant::now() + DASHBOARD_POLL_INTERVAL;
|
next_poll = Instant::now() + DASHBOARD_POLL_INTERVAL;
|
||||||
|
|
@ -311,7 +316,8 @@ async fn run_loop(
|
||||||
}
|
}
|
||||||
Err(error) => app.finish_ready_ticket_planning_return_error(error),
|
Err(error) => app.finish_ready_ticket_planning_return_error(error),
|
||||||
}
|
}
|
||||||
if pending_reload.start(OrchestratorLifecycleMode::Observe) {
|
if pending_reload.start(OrchestratorLifecycleMode::Observe, app.include_stopped)
|
||||||
|
{
|
||||||
app.refreshing = true;
|
app.refreshing = true;
|
||||||
}
|
}
|
||||||
next_poll = Instant::now() + DASHBOARD_POLL_INTERVAL;
|
next_poll = Instant::now() + DASHBOARD_POLL_INTERVAL;
|
||||||
|
|
@ -328,7 +334,8 @@ async fn run_loop(
|
||||||
terminal.draw(|f| render::draw(f, app))?;
|
terminal.draw(|f| render::draw(f, app))?;
|
||||||
let result = launch_intake_with_handoff(request).await;
|
let result = launch_intake_with_handoff(request).await;
|
||||||
app.finish_intake_launch(result);
|
app.finish_intake_launch(result);
|
||||||
if pending_reload.start(OrchestratorLifecycleMode::Observe) {
|
if pending_reload.start(OrchestratorLifecycleMode::Observe, app.include_stopped)
|
||||||
|
{
|
||||||
app.refreshing = true;
|
app.refreshing = true;
|
||||||
}
|
}
|
||||||
next_poll = Instant::now() + DASHBOARD_POLL_INTERVAL;
|
next_poll = Instant::now() + DASHBOARD_POLL_INTERVAL;
|
||||||
|
|
@ -345,7 +352,8 @@ async fn run_loop(
|
||||||
terminal.draw(|f| render::draw(f, app))?;
|
terminal.draw(|f| render::draw(f, app))?;
|
||||||
let result = dispatch_companion_message(request).await;
|
let result = dispatch_companion_message(request).await;
|
||||||
app.finish_companion_send(result);
|
app.finish_companion_send(result);
|
||||||
if pending_reload.start(OrchestratorLifecycleMode::Observe) {
|
if pending_reload.start(OrchestratorLifecycleMode::Observe, app.include_stopped)
|
||||||
|
{
|
||||||
app.refreshing = true;
|
app.refreshing = true;
|
||||||
}
|
}
|
||||||
next_poll = Instant::now() + DASHBOARD_POLL_INTERVAL;
|
next_poll = Instant::now() + DASHBOARD_POLL_INTERVAL;
|
||||||
|
|
@ -366,7 +374,7 @@ struct PendingReload {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PendingReload {
|
impl PendingReload {
|
||||||
fn start(&mut self, lifecycle_mode: OrchestratorLifecycleMode) -> bool {
|
fn start(&mut self, lifecycle_mode: OrchestratorLifecycleMode, include_stopped: bool) -> bool {
|
||||||
if self.handle.is_some() {
|
if self.handle.is_some() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -382,7 +390,7 @@ impl PendingReload {
|
||||||
self.handle = Some(tokio::spawn(async move {
|
self.handle = Some(tokio::spawn(async move {
|
||||||
#[cfg(feature = "e2e-test")]
|
#[cfg(feature = "e2e-test")]
|
||||||
crate::e2e_observer::hold_background_task_if_requested("reload").await;
|
crate::e2e_observer::hold_background_task_if_requested("reload").await;
|
||||||
load_dashboard_snapshot(None, lifecycle_mode).await
|
load_dashboard_snapshot(None, lifecycle_mode, include_stopped).await
|
||||||
}));
|
}));
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
@ -1219,6 +1227,7 @@ pub(crate) struct DashboardApp {
|
||||||
refreshing: bool,
|
refreshing: bool,
|
||||||
enter_reload: Option<OrchestratorLifecycleMode>,
|
enter_reload: Option<OrchestratorLifecycleMode>,
|
||||||
runtime_command: WorkerRuntimeCommand,
|
runtime_command: WorkerRuntimeCommand,
|
||||||
|
include_stopped: bool,
|
||||||
last_companion_lifecycle_failure: Option<CompanionPanelState>,
|
last_companion_lifecycle_failure: Option<CompanionPanelState>,
|
||||||
last_orchestrator_lifecycle_failure: Option<OrchestratorPanelState>,
|
last_orchestrator_lifecycle_failure: Option<OrchestratorPanelState>,
|
||||||
orchestrator_work_set: OrchestratorWorkSet,
|
orchestrator_work_set: OrchestratorWorkSet,
|
||||||
|
|
@ -1228,7 +1237,7 @@ pub(crate) struct DashboardApp {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DashboardApp {
|
impl DashboardApp {
|
||||||
fn loading(runtime_command: WorkerRuntimeCommand) -> Self {
|
fn loading(runtime_command: WorkerRuntimeCommand, include_stopped: bool) -> Self {
|
||||||
let workspace_root = current_workspace_root();
|
let workspace_root = current_workspace_root();
|
||||||
let mut panel = WorkspacePanelViewModel::empty(&workspace_root);
|
let mut panel = WorkspacePanelViewModel::empty(&workspace_root);
|
||||||
panel
|
panel
|
||||||
|
|
@ -1257,6 +1266,7 @@ impl DashboardApp {
|
||||||
runtime_command: runtime_command.clone(),
|
runtime_command: runtime_command.clone(),
|
||||||
}),
|
}),
|
||||||
runtime_command,
|
runtime_command,
|
||||||
|
include_stopped,
|
||||||
last_companion_lifecycle_failure: None,
|
last_companion_lifecycle_failure: None,
|
||||||
last_orchestrator_lifecycle_failure: None,
|
last_orchestrator_lifecycle_failure: None,
|
||||||
orchestrator_work_set: OrchestratorWorkSet::default(),
|
orchestrator_work_set: OrchestratorWorkSet::default(),
|
||||||
|
|
@ -2511,6 +2521,7 @@ enum OrchestratorLifecycleMode {
|
||||||
async fn load_dashboard_snapshot(
|
async fn load_dashboard_snapshot(
|
||||||
selected_name: Option<String>,
|
selected_name: Option<String>,
|
||||||
lifecycle_mode: OrchestratorLifecycleMode,
|
lifecycle_mode: OrchestratorLifecycleMode,
|
||||||
|
include_stopped: bool,
|
||||||
) -> Result<DashboardSnapshot, DashboardError> {
|
) -> Result<DashboardSnapshot, DashboardError> {
|
||||||
let workspace_root = current_workspace_root();
|
let workspace_root = current_workspace_root();
|
||||||
#[cfg(feature = "e2e-test")]
|
#[cfg(feature = "e2e-test")]
|
||||||
|
|
@ -2524,7 +2535,8 @@ async fn load_dashboard_snapshot(
|
||||||
|
|
||||||
#[cfg(feature = "e2e-test")]
|
#[cfg(feature = "e2e-test")]
|
||||||
let source_started = Instant::now();
|
let source_started = Instant::now();
|
||||||
let mut list = load_worker_list(list_selected_name.clone(), MAX_ENTRIES).await?;
|
let mut list =
|
||||||
|
load_worker_list(list_selected_name.clone(), MAX_ENTRIES, include_stopped).await?;
|
||||||
#[cfg(feature = "e2e-test")]
|
#[cfg(feature = "e2e-test")]
|
||||||
source_timings.push(PanelE2eSourceTiming {
|
source_timings.push(PanelE2eSourceTiming {
|
||||||
source: "pod_metadata_status_probe.initial",
|
source: "pod_metadata_status_probe.initial",
|
||||||
|
|
@ -2564,7 +2576,7 @@ async fn load_dashboard_snapshot(
|
||||||
if companion.reload_workers {
|
if companion.reload_workers {
|
||||||
#[cfg(feature = "e2e-test")]
|
#[cfg(feature = "e2e-test")]
|
||||||
let source_started = Instant::now();
|
let source_started = Instant::now();
|
||||||
list = load_worker_list(list_selected_name.clone(), MAX_ENTRIES).await?;
|
list = load_worker_list(list_selected_name.clone(), MAX_ENTRIES, include_stopped).await?;
|
||||||
#[cfg(feature = "e2e-test")]
|
#[cfg(feature = "e2e-test")]
|
||||||
source_timings.push(PanelE2eSourceTiming {
|
source_timings.push(PanelE2eSourceTiming {
|
||||||
source: "pod_metadata_status_probe.after_companion_reload",
|
source: "pod_metadata_status_probe.after_companion_reload",
|
||||||
|
|
@ -2622,7 +2634,7 @@ async fn load_dashboard_snapshot(
|
||||||
if orchestrator.reload_workers {
|
if orchestrator.reload_workers {
|
||||||
#[cfg(feature = "e2e-test")]
|
#[cfg(feature = "e2e-test")]
|
||||||
let source_started = Instant::now();
|
let source_started = Instant::now();
|
||||||
list = load_worker_list(list_selected_name, MAX_ENTRIES).await?;
|
list = load_worker_list(list_selected_name, MAX_ENTRIES, include_stopped).await?;
|
||||||
#[cfg(feature = "e2e-test")]
|
#[cfg(feature = "e2e-test")]
|
||||||
source_timings.push(PanelE2eSourceTiming {
|
source_timings.push(PanelE2eSourceTiming {
|
||||||
source: "pod_metadata_status_probe.after_orchestrator_reload",
|
source: "pod_metadata_status_probe.after_orchestrator_reload",
|
||||||
|
|
@ -3420,6 +3432,7 @@ fn existing_ticket_claim_notice(
|
||||||
async fn load_worker_list(
|
async fn load_worker_list(
|
||||||
selected_name: Option<String>,
|
selected_name: Option<String>,
|
||||||
max_entries: usize,
|
max_entries: usize,
|
||||||
|
include_stopped: bool,
|
||||||
) -> Result<WorkerList, DashboardError> {
|
) -> Result<WorkerList, DashboardError> {
|
||||||
let store_dir = default_store_dir()?;
|
let store_dir = default_store_dir()?;
|
||||||
let store = FsStore::new(&store_dir)?;
|
let store = FsStore::new(&store_dir)?;
|
||||||
|
|
@ -3429,14 +3442,18 @@ async fn load_worker_list(
|
||||||
let live = read_reachable_live_worker_infos(&store)
|
let live = read_reachable_live_worker_infos(&store)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
Ok(WorkerList::from_workspace_sources(
|
let mut list = WorkerList::from_workspace_sources(
|
||||||
WorkerVisibilitySource::ResumePicker,
|
WorkerVisibilitySource::ResumePicker,
|
||||||
stored,
|
stored,
|
||||||
live,
|
live,
|
||||||
selected_name,
|
selected_name,
|
||||||
max_entries,
|
max_entries,
|
||||||
¤t_workspace_root(),
|
¤t_workspace_root(),
|
||||||
))
|
);
|
||||||
|
if !include_stopped {
|
||||||
|
list.retain_live_entries();
|
||||||
|
}
|
||||||
|
Ok(list)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
|
||||||
|
|
@ -2329,7 +2329,7 @@ fn dashboard_open_failure_keeps_composer_and_sets_notice() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dashboard_loading_app_defers_initial_snapshot_to_enter_reload() {
|
fn dashboard_loading_app_defers_initial_snapshot_to_enter_reload() {
|
||||||
let app = DashboardApp::loading(WorkerRuntimeCommand::for_executable("/tmp/yoi"));
|
let app = DashboardApp::loading(WorkerRuntimeCommand::for_executable("/tmp/yoi"), false);
|
||||||
|
|
||||||
assert!(app.panel.rows.is_empty());
|
assert!(app.panel.rows.is_empty());
|
||||||
assert!(
|
assert!(
|
||||||
|
|
@ -3241,6 +3241,7 @@ fn app_with_panel(list: WorkerList, panel: WorkspacePanelViewModel) -> Dashboard
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
enter_reload: None,
|
enter_reload: None,
|
||||||
runtime_command: WorkerRuntimeCommand::for_executable("/tmp/yoi"),
|
runtime_command: WorkerRuntimeCommand::for_executable("/tmp/yoi"),
|
||||||
|
include_stopped: false,
|
||||||
last_companion_lifecycle_failure,
|
last_companion_lifecycle_failure,
|
||||||
last_orchestrator_lifecycle_failure,
|
last_orchestrator_lifecycle_failure,
|
||||||
orchestrator_work_set: OrchestratorWorkSet::default(),
|
orchestrator_work_set: OrchestratorWorkSet::default(),
|
||||||
|
|
@ -3392,7 +3393,7 @@ fn section_names<'a>(list: &'a WorkerList, section: &DashboardSection) -> Vec<&'
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ticket_action_error_records_f2_diagnostic_details() {
|
fn ticket_action_error_records_f2_diagnostic_details() {
|
||||||
let mut app = DashboardApp::loading(WorkerRuntimeCommand::for_executable("/tmp/yoi"));
|
let mut app = DashboardApp::loading(WorkerRuntimeCommand::for_executable("/tmp/yoi"), false);
|
||||||
let long_error = "root-clean failed for Ticket 00001KTWPE3KQ at /home/hare/Projects/yoi: dirty file crates/tui/src/dashboard.rs";
|
let long_error = "root-clean failed for Ticket 00001KTWPE3KQ at /home/hare/Projects/yoi: dirty file crates/tui/src/dashboard.rs";
|
||||||
|
|
||||||
app.finish_ticket_action_dispatch(Err(TicketActionError::Stale(long_error.to_string())));
|
app.finish_ticket_action_dispatch(Err(TicketActionError::Stale(long_error.to_string())));
|
||||||
|
|
|
||||||
|
|
@ -179,8 +179,8 @@ pub async fn launch(options: LaunchOptions) -> ExitCode {
|
||||||
}
|
}
|
||||||
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error>),
|
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error>),
|
||||||
},
|
},
|
||||||
LaunchMode::Panel { .. } => match target.dashboard() {
|
LaunchMode::Panel { include_stopped } => match target.dashboard() {
|
||||||
Ok(dashboard) => dashboard::launch(dashboard.runtime_command).await,
|
Ok(dashboard) => dashboard::launch(dashboard.runtime_command, include_stopped).await,
|
||||||
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error>),
|
Err(e) => Err(Box::new(e) as Box<dyn std::error::Error>),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,17 @@ impl WorkerList {
|
||||||
self.selected_name = self.entries.get(index).map(|entry| entry.name.clone());
|
self.selected_name = self.entries.get(index).map(|entry| entry.name.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn retain_live_entries(&mut self) {
|
||||||
|
self.entries.retain(|entry| entry.live.is_some());
|
||||||
|
if !self
|
||||||
|
.selected_name
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|selected| self.entries.iter().any(|entry| entry.name == *selected))
|
||||||
|
{
|
||||||
|
self.selected_name = self.entries.first().map(|entry| entry.name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn selected_entry(&self) -> Option<&WorkerListEntry> {
|
pub(crate) fn selected_entry(&self) -> Option<&WorkerListEntry> {
|
||||||
let index = self.selected_index();
|
let index = self.selected_index();
|
||||||
self.entries.get(index)
|
self.entries.get(index)
|
||||||
|
|
@ -736,6 +747,28 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn retain_live_entries_removes_stored_only_rows_and_reselects() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let store = FsStore::new(dir.path()).unwrap();
|
||||||
|
let session_id = new_session_id();
|
||||||
|
let segment_id = new_segment_id();
|
||||||
|
append_start(&store, session_id, segment_id, 10);
|
||||||
|
let mut list = WorkerList::from_sources(
|
||||||
|
SOURCE,
|
||||||
|
vec![metadata_info(&store, "stored", session_id, segment_id)],
|
||||||
|
vec![live_info("live", WorkerStatus::Idle)],
|
||||||
|
Some("stored".to_string()),
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
|
||||||
|
list.retain_live_entries();
|
||||||
|
|
||||||
|
assert_eq!(list.entries.len(), 1);
|
||||||
|
assert_eq!(list.entries[0].name, "live");
|
||||||
|
assert_eq!(list.selected_entry().unwrap().name, "live");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn stored_only_row_can_restore_and_open_but_not_direct_send() {
|
fn stored_only_row_can_restore_and_open_but_not_direct_send() {
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ use cli_connection::{
|
||||||
resolve_backend_cli_connection, resolve_connection_aware_cli_connection,
|
resolve_backend_cli_connection, resolve_connection_aware_cli_connection,
|
||||||
resolve_local_cli_connection,
|
resolve_local_cli_connection,
|
||||||
};
|
};
|
||||||
use client::{BackendAuthTarget, Target, start_device_login, wait_for_device_login};
|
use client::{BackendAuthTarget, Target, TargetKind, start_device_login, wait_for_device_login};
|
||||||
use memory_lint::{LintCliOptions, LintStatus};
|
use memory_lint::{LintCliOptions, LintStatus};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use session_store::SegmentId;
|
use session_store::SegmentId;
|
||||||
|
|
@ -388,19 +388,25 @@ fn parse_args_slice_with_connection_resolver<R: CliConnectionResolver + ?Sized>(
|
||||||
return Ok(Mode::Mcp(mcp_cli));
|
return Ok(Mode::Mcp(mcp_cli));
|
||||||
}
|
}
|
||||||
"panel" => {
|
"panel" => {
|
||||||
let workspace_root = parse_panel_workspace(&args[1..])?;
|
let panel_options = parse_panel_args(&args[1..])?;
|
||||||
let target = resolve_tui_target(
|
let target = resolve_tui_target(
|
||||||
connection_resolver,
|
connection_resolver,
|
||||||
CliCommand::Panel,
|
CliCommand::Panel,
|
||||||
&target_selection,
|
&target_selection,
|
||||||
&workspace_root,
|
&panel_options.workspace_root,
|
||||||
)?;
|
)?;
|
||||||
|
if panel_options.include_stopped && target.kind() == TargetKind::Backend {
|
||||||
|
return Err(ParseError(
|
||||||
|
"yoi panel -r is only supported for local targets; Backend panel restore UI is not implemented"
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
return Ok(Mode::Tui {
|
return Ok(Mode::Tui {
|
||||||
target,
|
target,
|
||||||
mode: LaunchMode::Panel {
|
mode: LaunchMode::Panel {
|
||||||
include_stopped: false,
|
include_stopped: panel_options.include_stopped,
|
||||||
},
|
},
|
||||||
workspace_root,
|
workspace_root: panel_options.workspace_root,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
"keys" => {
|
"keys" => {
|
||||||
|
|
@ -1554,33 +1560,48 @@ fn mcp_usage() -> &'static str {
|
||||||
"usage: yoi mcp list [--workspace PATH] [--profile REF] [--json]\n yoi mcp show <server> [--workspace PATH] [--profile REF] [--json]\n yoi mcp tools [server] [--workspace PATH] [--profile REF] [--json]\n yoi mcp resources [server] [--workspace PATH] [--profile REF] [--json]\n yoi mcp prompts [server] [--workspace PATH] [--profile REF] [--json]"
|
"usage: yoi mcp list [--workspace PATH] [--profile REF] [--json]\n yoi mcp show <server> [--workspace PATH] [--profile REF] [--json]\n yoi mcp tools [server] [--workspace PATH] [--profile REF] [--json]\n yoi mcp resources [server] [--workspace PATH] [--profile REF] [--json]\n yoi mcp prompts [server] [--workspace PATH] [--profile REF] [--json]"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_panel_workspace(args: &[String]) -> Result<PathBuf, ParseError> {
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
if let Some(arg) = args.iter().find(|arg| is_backend_target_option(arg)) {
|
struct PanelCliOptions {
|
||||||
return Err(backend_target_option_error_for_local_command(
|
workspace_root: PathBuf,
|
||||||
CliCommand::Panel,
|
include_stopped: bool,
|
||||||
arg,
|
}
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
match args {
|
fn parse_panel_args(args: &[String]) -> Result<PanelCliOptions, ParseError> {
|
||||||
[] => std::env::current_dir()
|
let mut workspace_root: Option<PathBuf> = None;
|
||||||
.map_err(|e| ParseError(format!("failed to resolve current directory: {e}"))),
|
let mut include_stopped = false;
|
||||||
[flag, value] if flag == "--workspace" => Ok(PathBuf::from(value)),
|
let mut i = 0;
|
||||||
[flag] if flag.starts_with("--workspace=") => {
|
while i < args.len() {
|
||||||
let value = flag.trim_start_matches("--workspace=");
|
match args[i].as_str() {
|
||||||
|
"--workspace" => {
|
||||||
|
let value = args
|
||||||
|
.get(i + 1)
|
||||||
|
.ok_or_else(|| ParseError("--workspace requires a path".to_string()))?;
|
||||||
|
workspace_root = Some(PathBuf::from(value));
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
|
arg if arg.starts_with("--workspace=") => {
|
||||||
|
let value = arg.trim_start_matches("--workspace=");
|
||||||
if value.is_empty() {
|
if value.is_empty() {
|
||||||
Err(ParseError("--workspace requires a value".to_string()))
|
return Err(ParseError("--workspace requires a path".to_string()));
|
||||||
} else {
|
}
|
||||||
Ok(PathBuf::from(value))
|
workspace_root = Some(PathBuf::from(value));
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
"-r" | "--stopped" | "--restoreable" => {
|
||||||
|
include_stopped = true;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
return Err(ParseError(format!(
|
||||||
|
"unknown panel option `{other}`; usage: yoi [TARGET] panel [-r|--stopped] [--workspace <PATH>]"
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
[flag] if flag == "--workspace" => {
|
|
||||||
Err(ParseError("--workspace requires a value".to_string()))
|
|
||||||
}
|
|
||||||
_ => Err(ParseError(
|
|
||||||
"yoi panel accepts only --workspace <PATH>".to_string(),
|
|
||||||
)),
|
|
||||||
}
|
}
|
||||||
|
Ok(PanelCliOptions {
|
||||||
|
workspace_root: workspace_root.unwrap_or(current_dir()?),
|
||||||
|
include_stopped,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_session_id(value: &str) -> Result<SegmentId, ParseError> {
|
fn parse_session_id(value: &str) -> Result<SegmentId, ParseError> {
|
||||||
|
|
@ -1595,7 +1616,7 @@ Usage:
|
||||||
yoi [TARGET] [CONSOLE_OPTIONS]
|
yoi [TARGET] [CONSOLE_OPTIONS]
|
||||||
yoi [TARGET] workers [-r|--stopped] [--workspace <PATH>] [--runtime-id <ID>]
|
yoi [TARGET] workers [-r|--stopped] [--workspace <PATH>] [--runtime-id <ID>]
|
||||||
yoi [TARGET] resume [--workspace <PATH>|--all] [--runtime-id <ID>]
|
yoi [TARGET] resume [--workspace <PATH>|--all] [--runtime-id <ID>]
|
||||||
yoi [TARGET] panel [--workspace <PATH>]
|
yoi [TARGET] panel [-r|--stopped] [--workspace <PATH>]
|
||||||
yoi [--backend <URL>] login [--no-wait]
|
yoi [--backend <URL>] login [--no-wait]
|
||||||
yoi <LOCAL_COMMAND> [OPTIONS]
|
yoi <LOCAL_COMMAND> [OPTIONS]
|
||||||
|
|
||||||
|
|
@ -1622,6 +1643,7 @@ Connection-aware commands:
|
||||||
yoi workers -r Include stopped Workers. --restoreable is accepted as a legacy alias.
|
yoi workers -r Include stopped Workers. --restoreable is accepted as a legacy alias.
|
||||||
yoi resume Open the Worker picker with stopped Workers included.
|
yoi resume Open the Worker picker with stopped Workers included.
|
||||||
yoi panel Open the dashboard/panel TUI for the selected target.
|
yoi panel Open the dashboard/panel TUI for the selected target.
|
||||||
|
yoi panel -r Local only: include stopped/restorable Worker rows.
|
||||||
|
|
||||||
Console options:
|
Console options:
|
||||||
--workspace <PATH> Local workspace root for local Console/Worker lists (defaults to cwd)
|
--workspace <PATH> Local workspace root for local Console/Worker lists (defaults to cwd)
|
||||||
|
|
@ -1813,12 +1835,6 @@ backend = "shared"
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_local_only_commands_reject_backend_target_options() {
|
fn parse_local_only_commands_reject_backend_target_options() {
|
||||||
let err = parse_args_from(["panel", "--runtime-id", "runtime-a"]).unwrap_err();
|
|
||||||
assert_eq!(
|
|
||||||
err.to_string(),
|
|
||||||
"yoi panel uses a local connection target and cannot accept Backend target option `--runtime-id`"
|
|
||||||
);
|
|
||||||
|
|
||||||
let err = parse_args_from(["keys", "--workspace-id=workspace-a"]).unwrap_err();
|
let err = parse_args_from(["keys", "--workspace-id=workspace-a"]).unwrap_err();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
err.to_string(),
|
err.to_string(),
|
||||||
|
|
@ -2418,6 +2434,33 @@ backend = "shared"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_panel_stopped_mode() {
|
||||||
|
for flag in ["-r", "--stopped", "--restoreable"] {
|
||||||
|
match parse_args_from(["panel", flag, "--workspace", "/tmp/other-workspace"]).unwrap() {
|
||||||
|
Mode::Tui {
|
||||||
|
mode:
|
||||||
|
LaunchMode::Panel {
|
||||||
|
include_stopped: true,
|
||||||
|
},
|
||||||
|
workspace_root,
|
||||||
|
..
|
||||||
|
} => assert_eq!(workspace_root, PathBuf::from("/tmp/other-workspace")),
|
||||||
|
_ => panic!("expected Panel stopped mode for {flag}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_backend_panel_stopped_is_not_supported() {
|
||||||
|
let err =
|
||||||
|
parse_args_from(["--backend", "http://127.0.0.1:8787", "panel", "-r"]).unwrap_err();
|
||||||
|
assert_eq!(
|
||||||
|
err.to_string(),
|
||||||
|
"yoi panel -r is only supported for local targets; Backend panel restore UI is not implemented"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_dashboard_word_is_not_an_alias_or_worker_name() {
|
fn parse_dashboard_word_is_not_an_alias_or_worker_name() {
|
||||||
let err = parse_args_from(["dashboard"]).unwrap_err();
|
let err = parse_args_from(["dashboard"]).unwrap_err();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user