From 8cbade818f261be85d05548b6993767bcc4f671d Mon Sep 17 00:00:00 2001 From: Hare Date: Thu, 18 Jun 2026 22:41:54 +0900 Subject: [PATCH] chore: record panel followups --- .yoi/tickets/00001KTFQ109S/item.md | 4 +- .yoi/tickets/00001KTFQ109S/resolution.md | 10 + .yoi/tickets/00001KTFQ109S/thread.md | 27 +++ .yoi/tickets/00001KV5R5V2S/item.md | 4 +- .yoi/tickets/00001KV5R5V2S/resolution.md | 3 + .yoi/tickets/00001KV5R5V2S/thread.md | 20 ++ .yoi/tickets/00001KV62PF32/item.md | 2 +- .yoi/tickets/00001KV62PF32/thread.md | 13 ++ .yoi/tickets/00001KVDETSN6/artifacts/.gitkeep | 0 .../00001KVDETSN6/artifacts/relations.json | 29 +++ .yoi/tickets/00001KVDETSN6/item.md | 103 ++++++++++ .yoi/tickets/00001KVDETSN6/thread.md | 7 + crates/tui/src/multi_pod.rs | 58 ++++++ crates/tui/src/workspace_panel.rs | 191 +++++++++++++----- 14 files changed, 411 insertions(+), 60 deletions(-) create mode 100644 .yoi/tickets/00001KTFQ109S/resolution.md create mode 100644 .yoi/tickets/00001KV5R5V2S/resolution.md create mode 100644 .yoi/tickets/00001KVDETSN6/artifacts/.gitkeep create mode 100644 .yoi/tickets/00001KVDETSN6/artifacts/relations.json create mode 100644 .yoi/tickets/00001KVDETSN6/item.md create mode 100644 .yoi/tickets/00001KVDETSN6/thread.md diff --git a/.yoi/tickets/00001KTFQ109S/item.md b/.yoi/tickets/00001KTFQ109S/item.md index 77535e7b..28f72115 100644 --- a/.yoi/tickets/00001KTFQ109S/item.md +++ b/.yoi/tickets/00001KTFQ109S/item.md @@ -1,8 +1,8 @@ --- title: "Workspace panel Companion interface" -state: "planning" +state: 'closed' created_at: "2026-06-07T00:16:51Z" -updated_at: "2026-06-07T03:13:01Z" +updated_at: '2026-06-18T13:06:31Z' --- ## Background diff --git a/.yoi/tickets/00001KTFQ109S/resolution.md b/.yoi/tickets/00001KTFQ109S/resolution.md new file mode 100644 index 00000000..afe891c7 --- /dev/null +++ b/.yoi/tickets/00001KTFQ109S/resolution.md @@ -0,0 +1,10 @@ +Closed as completed by child Tickets. + +The original Workspace Panel Companion interface plan has been implemented through more specific work: +- direct selected-Pod send was removed from the Panel composer path; +- Panel composer routing now targets the workspace Companion and Ticket Intake explicitly; +- workspace Companion Pod lifecycle restore/spawn/observe behavior is implemented; +- local role/session registry and Ticket claim handling were added for Panel-launched role sessions; +- project role Profile feature defaults limit Companion authority and keep Ticket orchestration / Pods / Task disabled for Companion by default. + +The remaining work in this area should be tracked as targeted follow-up Tickets rather than keeping this umbrella planning Ticket open. diff --git a/.yoi/tickets/00001KTFQ109S/thread.md b/.yoi/tickets/00001KTFQ109S/thread.md index d9bb2bc8..cbf66e5c 100644 --- a/.yoi/tickets/00001KTFQ109S/thread.md +++ b/.yoi/tickets/00001KTFQ109S/thread.md @@ -74,4 +74,31 @@ Companion work is useful but not required for near-term panel operation. The pan Decision: downgrade Companion-related follow-up priority to P2 so near-term focus can stay on Ticket role config strictness/init, Orchestrator queue automation, and workflow/compaction reliability. +--- + + + +## State changed + +Ticket を closed にしました。 + + +--- + + + +## 完了 + +Closed as completed by child Tickets. + +The original Workspace Panel Companion interface plan has been implemented through more specific work: +- direct selected-Pod send was removed from the Panel composer path; +- Panel composer routing now targets the workspace Companion and Ticket Intake explicitly; +- workspace Companion Pod lifecycle restore/spawn/observe behavior is implemented; +- local role/session registry and Ticket claim handling were added for Panel-launched role sessions; +- project role Profile feature defaults limit Companion authority and keep Ticket orchestration / Pods / Task disabled for Companion by default. + +The remaining work in this area should be tracked as targeted follow-up Tickets rather than keeping this umbrella planning Ticket open. + + --- diff --git a/.yoi/tickets/00001KV5R5V2S/item.md b/.yoi/tickets/00001KV5R5V2S/item.md index 3264ffee..428c4161 100644 --- a/.yoi/tickets/00001KV5R5V2S/item.md +++ b/.yoi/tickets/00001KV5R5V2S/item.md @@ -1,8 +1,8 @@ --- title: 'Plugin: package discovery and explicit enablement resolver' -state: 'done' +state: 'closed' created_at: '2026-06-15T13:40:15Z' -updated_at: '2026-06-15T15:30:00Z' +updated_at: '2026-06-18T12:22:04Z' assignee: null readiness: 'implementation_ready' risk_flags: ['plugin', 'package-loading', 'discovery', 'enablement', 'capability-boundary', 'startup-restore'] diff --git a/.yoi/tickets/00001KV5R5V2S/resolution.md b/.yoi/tickets/00001KV5R5V2S/resolution.md new file mode 100644 index 00000000..62941033 --- /dev/null +++ b/.yoi/tickets/00001KV5R5V2S/resolution.md @@ -0,0 +1,3 @@ +Ticket `00001KV5R5V2S` (`Plugin: package discovery and explicit enablement resolver`) はすでに `state: done` に到達していたため、workspace Panel から close しました。 + +この Close action によって、実装作業、state 変更、Orchestrator/Companion launch、worker invocation は開始されていません。 diff --git a/.yoi/tickets/00001KV5R5V2S/thread.md b/.yoi/tickets/00001KV5R5V2S/thread.md index 4eea42c4..9ecf008a 100644 --- a/.yoi/tickets/00001KV5R5V2S/thread.md +++ b/.yoi/tickets/00001KV5R5V2S/thread.md @@ -524,4 +524,24 @@ Cleanup planned: Reviewer approved after requested fixes, implementation branch merged into the orchestration branch, and focused plus packaging validation passed in the Orchestrator worktree. Marking Ticket done in the orchestration branch. +--- + + + +## State changed + +Ticket を closed にしました。 + + +--- + + + +## 完了 + +Ticket `00001KV5R5V2S` (`Plugin: package discovery and explicit enablement resolver`) はすでに `state: done` に到達していたため、workspace Panel から close しました。 + +この Close action によって、実装作業、state 変更、Orchestrator/Companion launch、worker invocation は開始されていません。 + + --- diff --git a/.yoi/tickets/00001KV62PF32/item.md b/.yoi/tickets/00001KV62PF32/item.md index 9e01135b..185a55ce 100644 --- a/.yoi/tickets/00001KV62PF32/item.md +++ b/.yoi/tickets/00001KV62PF32/item.md @@ -2,7 +2,7 @@ title: 'Panel startup latency E2E を一覧データ描画完了基準に修正する' state: 'done' created_at: '2026-06-15T16:44:06Z' -updated_at: '2026-06-18T12:25:14Z' +updated_at: '2026-06-18T13:30:51Z' assignee: null readiness: 'implementation_ready' risk_flags: ['panel', 'e2e', 'startup-latency', 'readiness-metric', 'ticket-list-rendering'] diff --git a/.yoi/tickets/00001KV62PF32/thread.md b/.yoi/tickets/00001KV62PF32/thread.md index bad57f72..2447ee9e 100644 --- a/.yoi/tickets/00001KV62PF32/thread.md +++ b/.yoi/tickets/00001KV62PF32/thread.md @@ -281,4 +281,17 @@ Cleanup planned: Reviewer approved, implementation branch merged into the orchestration branch, and E2E-focused validation passed in the Orchestrator worktree. Marking Ticket done in the orchestration branch. +--- + + + +## Review: request changes + +Request changes. + +The current result still does not answer the user-facing latency problem. The problematic latency is the time from launching `yoi panel` / pressing Enter to seeing the actual workspace dashboard content. The current E2E measures a direct subprocess spawn to one concrete fixture Ticket row appearing in `rows_rendered`; it does not require the dashboard content to be complete from the user's perspective, and it does not reproduce or attribute the clearly long live-workspace delay. + +Do not treat fixture first-frame or single-row readiness numbers as evidence that no improvement is needed. The acceptance criterion must be strengthened to a user-visible dashboard-content-ready point and paired with slow-source attribution/improvement for the live-like Panel startup path. + + --- diff --git a/.yoi/tickets/00001KVDETSN6/artifacts/.gitkeep b/.yoi/tickets/00001KVDETSN6/artifacts/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/.yoi/tickets/00001KVDETSN6/artifacts/relations.json b/.yoi/tickets/00001KVDETSN6/artifacts/relations.json new file mode 100644 index 00000000..2f6d93cb --- /dev/null +++ b/.yoi/tickets/00001KVDETSN6/artifacts/relations.json @@ -0,0 +1,29 @@ +{ + "version": 1, + "relations": [ + { + "ticket_id": "00001KVDETSN6", + "kind": "related", + "target": "00001KV5D7MG5", + "note": "Dashboard content-ready fixture should include orchestration overlay state.", + "author": "yoi ticket", + "at": "2026-06-18T13:31:43Z" + }, + { + "ticket_id": "00001KVDETSN6", + "kind": "related", + "target": "00001KV5MRH6D", + "note": "Follows up Panel startup latency E2E work.", + "author": "yoi ticket", + "at": "2026-06-18T13:31:43Z" + }, + { + "ticket_id": "00001KVDETSN6", + "kind": "related", + "target": "00001KV62PF32", + "note": "Supersedes the insufficient single-row rows-ready E2E with user-visible dashboard content-ready measurement.", + "author": "yoi ticket", + "at": "2026-06-18T13:31:43Z" + } + ] +} diff --git a/.yoi/tickets/00001KVDETSN6/item.md b/.yoi/tickets/00001KVDETSN6/item.md new file mode 100644 index 00000000..6bcd0cc7 --- /dev/null +++ b/.yoi/tickets/00001KVDETSN6/item.md @@ -0,0 +1,103 @@ +--- +title: 'Panel startup latency をユーザー目線の dashboard content ready 基準で計測・改善する' +state: 'ready' +created_at: '2026-06-18T13:30:51Z' +updated_at: '2026-06-18T13:31:43Z' +assignee: null +readiness: 'implementation_ready' +risk_flags: ['panel', 'e2e', 'startup-latency', 'user-visible-readiness', 'dashboard-content', 'profiling'] +--- + +## Background + +ユーザーが問題にしている `yoi panel` startup latency は、first frame や fixture の単一 Ticket row が `rows_rendered` に出るまでではなく、**ユーザーが `yoi panel` を起動してから、workspace dashboard として実際に使えるコンテンツが画面に揃って見えるまで**の時間である。 + +既存の `00001KV62PF32` は `panel_ready` / first frame と単一 fixture Ticket row readiness の混同を修正したが、まだ以下の点で不十分だった。 + +- direct subprocess spawn から単一 fixture Ticket row の `rows_rendered` までを測っているだけで、workspace dashboard 全体の content ready ではない。 +- live workspace でユーザーが体感している明らかに長い遅延を再現・属性分解していない。 +- fixture 上の約 120ms rows-ready をもって「追加改善不要」と判断してしまうと、ユーザー視点の問題を取り逃がす。 + +この Ticket では、Panel startup latency の主基準を user-visible dashboard content ready に置き直し、遅延源を計測・改善する。 + +## Definitions + +- `panel_first_frame`: 初回 visible draw。loading / empty frame でもよい補助 metric。 +- `fixture_single_row_ready`: 具体的な fixture Ticket row が `rows_rendered` に現れる補助 metric。 +- `dashboard_content_ready`: ユーザーが workspace dashboard として必要な主要コンテンツが揃い、実際に画面へ描画された状態。この Ticket の主 metric。 + +`dashboard_content_ready` は少なくとも以下を含む。 + +- Ticket rows が fixture / live-like workspace の期待データと一致している。 + - id + - title + - state/status + - row kind + - primary action / disabled reason where relevant +- Pod / Companion / Orchestrator 関連 row または status が、fixture / live-like workspace の期待状態と一致している。 +- orchestration overlay を含む fixture では、local / orchestration state が表示上も期待通り反映されている。 +- loading / empty / partial single-row render だけでは ready とみなさない。 + +## Requirements + +- E2E / harness の readiness event または helper を追加・修正し、`dashboard_content_ready` を測れるようにする。 + - first frame / single-row readiness とは別 metric にする。 + - event 名・test 名・log 出力から意味が誤解されないようにする。 +- 測定開始点は、ユーザーの `yoi panel` 起動に十分近いものにする。 + - 基本は `Command::spawn` 直前からでよい。 + - interactive shell 入力まで含めない場合は、その範囲を test/report に明記する。 +- Fixture を live-like に強化する。 + - 複数 Ticket state を含める。 + - Pod metadata / Companion / Orchestrator 表示を含める。 + - orchestration overlay を含める。 + - 必要に応じて stale socket / slow observation / many Ticket records など、実遅延の候補を再現する fixture を追加する。 +- `dashboard_content_ready` は単なる `rows.len() >= N` や単一 Ticket row match だけで通さない。 + - expected dashboard snapshot / expected row set として比較する。 + - 欠落 row、wrong status、wrong action、overlay 未反映を fail にする。 +- Live workspace 相当の遅延源を属性分解する。 + - Ticket scan / parsing + - orchestration overlay worktree validation / read + - Pod metadata scan + - socket/status probing + - Companion / Orchestrator lifecycle observation + - role session / local claim scan + - git worktree / branch checks +- 明らかに長い遅延がある場合は改善する。 + - UI 初期化を content-ready 待ちで止めないだけでは不十分。 + - 実コンテンツが揃うまでの経路自体を短くする。 + - slow source を lazy / bounded / parallel / cached / timeout-shortened にできる場合は実装する。 +- Before / after の実測値を implementation report に記録する。 + - first frame + - dashboard content ready + - slow-source breakdown + - fixture 条件 / live-like 条件 +- 測定で改善不要と判断する場合でも、ユーザーが見ている長い live latency がなぜ再現しないか、またはどの範囲外かを明示する。 + +## Acceptance criteria + +- E2E が `dashboard_content_ready` を主 startup latency metric として測る。 +- `panel_first_frame` または単一 Ticket row readiness だけでは、この Ticket の主 E2E は通らない。 +- Expected dashboard snapshot に含まれる Ticket / Pod / Companion / Orchestrator / overlay 要素が揃って描画された時点を ready として扱う。 +- Missing row / wrong state / missing overlay / missing action label の fixture では ready 判定が fail する。 +- User-visible dashboard content ready の before / after 実測値が記録される。 +- 遅延源の breakdown が記録され、主要 slow source に対して具体的な改善または明示的な non-action rationale がある。 +- Live-like fixture または current workspace に近い条件で、ユーザー体感の長い遅延を取り逃がさない。 +- Existing Panel behavior に regression がない。 + - row selection + - composer target + - Queue action + - orchestration overlay display +- Validation: relevant `cargo test -p yoi-e2e --features e2e panel`, `cargo check`, `cargo fmt --check`, `git diff --check`, and `nix build .#yoi` if code/package/runtime behavior changes. + +## Non-goals + +- Interactive shell の command lookup / prompt rendering まで含めた OS/shell latency の厳密測定。 +- すべての background observation が完全 settle するまで UI を出さないこと。 +- Panel architecture の全面刷新。 +- Ticket lifecycle semantics の変更。 + +## Related work + +- `00001KV62PF32` — Panel startup latency E2E を一覧データ描画完了基準に修正する。単一 fixture row readiness までで不十分だったため request-changes 済み。 +- `00001KV5MRH6D` — Panel startup latency E2E / first visible frame separation work。 +- `00001KV5D7MG5` — Panel orchestration worktree Ticket state overlay。 diff --git a/.yoi/tickets/00001KVDETSN6/thread.md b/.yoi/tickets/00001KVDETSN6/thread.md new file mode 100644 index 00000000..f7f34e2e --- /dev/null +++ b/.yoi/tickets/00001KVDETSN6/thread.md @@ -0,0 +1,7 @@ + + +## 作成 + +LocalTicketBackend によって作成されました。 + +--- diff --git a/crates/tui/src/multi_pod.rs b/crates/tui/src/multi_pod.rs index 259d09ab..4724c3a1 100644 --- a/crates/tui/src/multi_pod.rs +++ b/crates/tui/src/multi_pod.rs @@ -5406,6 +5406,9 @@ fn panel_ticket_detail(row: &PanelRow) -> String { } let mut parts = vec![panel_ticket_reference(row)]; + if let Some(overlay_detail) = panel_ticket_overlay_detail(row) { + parts.push(overlay_detail); + } if let Some(blocked_reason) = row .ticket .as_ref() @@ -5441,6 +5444,24 @@ fn panel_ticket_action_label(row: &PanelRow, action: NextUserAction) -> &'static } } +fn panel_ticket_overlay_detail(row: &PanelRow) -> Option { + let ticket = row.ticket.as_ref()?; + let overlay = ticket.orchestration_overlay.as_ref()?; + let mut detail = format!( + "Overlay: local {} · {} {}", + ticket.workflow_state.as_str(), + overlay.source, + overlay.workflow_state.as_str() + ); + if matches!( + overlay.workflow_state, + TicketWorkflowState::Done | TicketWorkflowState::Closed + ) { + detail.push_str(" · merge pending"); + } + Some(detail) +} + fn panel_ticket_reason(row: &PanelRow) -> Option<&str> { row.disabled_reason .as_deref() @@ -7596,6 +7617,43 @@ branch = "orchestration/custom-panel" assert!(detail_line.ends_with('…')); } + #[test] + fn panel_orchestration_overlay_uses_compact_status_column_and_detail_line() { + let mut row = panel_test_ticket_row( + "00001OVERLAY", + "Overlay column regression", + ActionPriority::Background, + NextUserAction::Wait, + "queued", + ); + row.kind = PanelRowKind::Review; + row.status = "q→done".to_string(); + row.disabled_reason = Some( + "orchestration worktree overlay shows Ticket state done; local state remains queued" + .to_string(), + ); + row.ticket.as_mut().unwrap().orchestration_overlay = + Some(crate::workspace_panel::TicketStateOverlay { + source: "orchestration".to_string(), + workflow_state: TicketWorkflowState::Done, + }); + + let lines = panel_row_lines(&row, false, 160); + let title_line = plain_line(&lines[0]); + let detail_line = plain_line(&lines[1]); + let state_start = 2; + let title_start = state_start + TICKET_STATE_COLUMN_WIDTH + 1; + + assert!(row.status.width() <= TICKET_STATE_COLUMN_WIDTH); + assert_eq!(display_column(&title_line, "q→done"), state_start); + assert_eq!( + display_column(&title_line, "Overlay column regression"), + title_start + ); + assert!(!title_line.contains("orchestration")); + assert!(detail_line.contains("Overlay: local queued · orchestration done · merge pending")); + } + #[test] fn ready_ticket_with_waiting_gate_shows_queue_disabled_reason() { let mut row = panel_test_ticket_row( diff --git a/crates/tui/src/workspace_panel.rs b/crates/tui/src/workspace_panel.rs index d5e9082c..1c7d59c6 100644 --- a/crates/tui/src/workspace_panel.rs +++ b/crates/tui/src/workspace_panel.rs @@ -1089,15 +1089,25 @@ fn ticket_state_display( ) -> String { match overlay { Some(overlay) => format!( - "local: {} · {}: {}", - local.as_str(), - overlay.source, - overlay.workflow_state.as_str() + "{}→{}", + compact_ticket_state_label(local), + compact_ticket_state_label(overlay.workflow_state) ), None => local.as_str().to_string(), } } +fn compact_ticket_state_label(state: TicketWorkflowState) -> &'static str { + match state { + TicketWorkflowState::Planning => "plan", + TicketWorkflowState::Ready => "ready", + TicketWorkflowState::Queued => "q", + TicketWorkflowState::InProgress => "prog", + TicketWorkflowState::Done => "done", + TicketWorkflowState::Closed => "cls", + } +} + fn apply_orchestration_overlay_to_derived( derived: &mut DerivedTicketState, local: TicketWorkflowState, @@ -1138,51 +1148,90 @@ fn apply_orchestration_overlay_to_derived( } } +fn format_relation_blockers(blockers: &[&TicketRelationBlocker]) -> String { + let shown_blockers = blockers.iter().take(3).count(); + let mut formatted = blockers + .iter() + .take(3) + .map(|blocker| { + format!( + "{} via {} (state: {})", + blocker.blocking_ticket, + blocker.reason_kind, + blocker.blocking_state.as_str() + ) + }) + .collect::>() + .join(", "); + let remaining_blockers = blockers.len().saturating_sub(shown_blockers); + if remaining_blockers > 0 { + formatted.push_str(&format!(" (+{remaining_blockers} more)")); + } + formatted +} + +fn relation_blocker_allows_ready_queue(blocker: &TicketRelationBlocker) -> bool { + matches!( + blocker.blocking_state, + TicketWorkflowState::Queued | TicketWorkflowState::InProgress + ) +} + fn derive_ticket_state( summary: &TicketSummary, relation_blockers: &[TicketRelationBlocker], ) -> DerivedTicketState { if !relation_blockers.is_empty() { - let shown_blockers = relation_blockers.iter().take(3).count(); - let mut blockers = relation_blockers + let active_blockers = relation_blockers .iter() - .take(3) - .map(|blocker| { - format!( - "{} via {} (state: {})", - blocker.blocking_ticket, - blocker.reason_kind, - blocker.blocking_state.as_str() - ) - }) - .collect::>() - .join(", "); - let remaining_blockers = relation_blockers.len().saturating_sub(shown_blockers); - if remaining_blockers > 0 { - blockers.push_str(&format!(" (+{remaining_blockers} more)")); + .filter(|blocker| !relation_blocker_allows_ready_queue(blocker)) + .collect::>(); + if !active_blockers.is_empty() || summary.workflow_state != TicketWorkflowState::Ready { + let blockers_to_report = if active_blockers.is_empty() { + relation_blockers.iter().collect::>() + } else { + active_blockers + }; + let blockers = format_relation_blockers(&blockers_to_report); + let waiting_reason = format!("waiting for {blockers}"); + return DerivedTicketState { + kind: match summary.workflow_state { + TicketWorkflowState::Planning => PanelRowKind::Planning, + TicketWorkflowState::Queued | TicketWorkflowState::InProgress => { + PanelRowKind::ActiveWork + } + TicketWorkflowState::Done | TicketWorkflowState::Closed => PanelRowKind::Review, + TicketWorkflowState::Ready => PanelRowKind::Ticket, + }, + priority: match summary.workflow_state { + TicketWorkflowState::Queued | TicketWorkflowState::InProgress => { + ActionPriority::ActiveWork + } + _ => ActionPriority::Background, + }, + action: Some(NextUserAction::Wait), + disabled_reason: Some(format!( + "Queue disabled: {waiting_reason}. Resolve dependency/blocker before ready -> queued." + )), + key_hint: Some(format!("Gate: {waiting_reason}")), + blocked_reason: Some(blockers), + }; } - let waiting_reason = format!("waiting for {blockers}"); + + let blockers = format_relation_blockers( + &relation_blockers + .iter() + .collect::>(), + ); return DerivedTicketState { - kind: match summary.workflow_state { - TicketWorkflowState::Planning => PanelRowKind::Planning, - TicketWorkflowState::Queued | TicketWorkflowState::InProgress => { - PanelRowKind::ActiveWork - } - TicketWorkflowState::Done | TicketWorkflowState::Closed => PanelRowKind::Review, - TicketWorkflowState::Ready => PanelRowKind::Ticket, - }, - priority: match summary.workflow_state { - TicketWorkflowState::Queued | TicketWorkflowState::InProgress => { - ActionPriority::ActiveWork - } - _ => ActionPriority::Background, - }, - action: Some(NextUserAction::Wait), - disabled_reason: Some(format!( - "Queue disabled: {waiting_reason}. Resolve dependency/blocker before ready -> queued." + kind: PanelRowKind::Ticket, + priority: ActionPriority::ReadyForQueue, + action: Some(NextUserAction::Queue), + disabled_reason: None, + key_hint: Some(format!( + "Queue allowed: prerequisites are already queued/in progress; Orchestrator will preserve order ({blockers})." )), - key_hint: Some(format!("Gate: {waiting_reason}")), - blocked_reason: Some(blockers), + blocked_reason: None, }; } @@ -1635,15 +1684,6 @@ mod tests { .unwrap_or_else(|| panic!("missing row for {title}")) } - fn status_contains(row: &PanelRow, needle: &str) { - assert!( - row.status.contains(needle), - "status {:?} did not contain {:?}", - row.status, - needle - ); - } - fn live_pods(names: &[&str]) -> PodList { PodList::from_sources( crate::pod_list::PodVisibilitySource::ResumePicker, @@ -1731,8 +1771,7 @@ mod tests { let model = build_workspace_panel(temp.path(), &empty_pods()); let matched = ticket_row_by_title(&model, "Overlay Match"); - status_contains(matched, "local: queued"); - status_contains(matched, "orchestration: inprogress"); + assert_eq!(matched.status, "q→prog"); assert_eq!( matched.ticket.as_ref().unwrap().workflow_state, TicketWorkflowState::Queued @@ -1772,8 +1811,7 @@ mod tests { let model = build_workspace_panel(temp.path(), &empty_pods()); let row = ticket_row_by_title(&model, "Overlay In Progress"); - status_contains(row, "local: queued"); - status_contains(row, "orchestration: inprogress"); + assert_eq!(row.status, "q→prog"); assert_eq!(row.next_action, Some(NextUserAction::Wait)); assert_eq!(row.kind, PanelRowKind::ActiveWork); assert_eq!(fs::read_to_string(&local_item).unwrap(), before); @@ -1801,8 +1839,7 @@ mod tests { let model = build_workspace_panel(temp.path(), &empty_pods()); let row = ticket_row_by_title(&model, "Overlay Done"); - status_contains(row, "local: queued"); - status_contains(row, "orchestration: done"); + assert_eq!(row.status, "q→done"); assert_eq!(row.kind, PanelRowKind::Review); assert_eq!(row.next_action, Some(NextUserAction::Wait)); assert_ne!(row.next_action, Some(NextUserAction::Queue)); @@ -2123,6 +2160,50 @@ mod tests { ); } + #[test] + fn workspace_panel_allows_ready_ticket_when_relation_prerequisite_is_queued() { + let temp = TempDir::new().unwrap(); + write_ticket_config(temp.path()); + let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets")); + let mut ready_input = NewTicket::new("Ready After Queued Relation"); + ready_input.workflow_state = Some(TicketWorkflowState::Ready); + let ready = backend.create(ready_input).unwrap(); + let mut dependency_input = NewTicket::new("Queued Relation Dependency"); + dependency_input.workflow_state = Some(TicketWorkflowState::Queued); + let dependency = backend.create(dependency_input).unwrap(); + backend + .add_ticket_relation( + TicketIdOrSlug::Id(ready.id.clone()), + NewTicketRelation { + kind: TicketRelationKind::DependsOn, + target: dependency.id.clone(), + note: None, + author: Some("test".to_string()), + }, + ) + .unwrap(); + + let model = build_workspace_panel(temp.path(), &empty_pods()); + let row = model + .rows + .iter() + .find(|row| row.title == "Ready After Queued Relation") + .unwrap(); + + assert_eq!(row.kind, PanelRowKind::Ticket); + assert_eq!(row.next_action, Some(NextUserAction::Queue)); + assert_eq!(row.priority, ActionPriority::ReadyForQueue); + assert!(row.disabled_reason.is_none()); + assert!(row.ticket.as_ref().unwrap().blocked_reason.is_none()); + assert!( + row.key_hint + .as_deref() + .unwrap() + .contains("Queue allowed: prerequisites are already queued/in progress") + ); + assert!(row.key_hint.as_deref().unwrap().contains(&dependency.id)); + } + #[test] fn workspace_panel_defaults_missing_open_state_to_planning_and_displays_done_state() { let temp = TempDir::new().unwrap();