fix: canonicalize workspace server root
This commit is contained in:
parent
eb998f0ad4
commit
48672e4317
0
.yoi/tickets/00001KVSKJ0EA/artifacts/.gitkeep
Normal file
0
.yoi/tickets/00001KVSKJ0EA/artifacts/.gitkeep
Normal file
105
.yoi/tickets/00001KVSKJ0EA/item.md
Normal file
105
.yoi/tickets/00001KVSKJ0EA/item.md
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
---
|
||||
title: 'Dashboard reload と初期表示で row を自動選択しない'
|
||||
state: 'ready'
|
||||
created_at: '2026-06-23T06:44:20Z'
|
||||
updated_at: '2026-06-23T06:46:36Z'
|
||||
assignee: null
|
||||
readiness: 'implementation_ready'
|
||||
risk_flags: ['tui-ux', 'panel-selection', 'reload-state']
|
||||
---
|
||||
|
||||
## User claims / request snapshot
|
||||
|
||||
- Dashboard で `Esc` を押して row selection を消しても、reload によって頻繁に選択が戻る。
|
||||
- そのため、実使用上は「何も選択していない状態」で composer を global / 新規 Intake として使うのが難しい。
|
||||
- reload で勝手に row selection しないようにしたい。
|
||||
- 追加合意: 初回 `yoi panel` 起動時から未選択にする。
|
||||
|
||||
## Confirmed facts / sources
|
||||
|
||||
- `crates/tui/src/dashboard/mod.rs`
|
||||
- Dashboard は `selected_row: Option<PanelRowKey>` を持ち、`None` の未選択状態が実装上存在する。
|
||||
- `Esc` 経路で `clear_panel_selection()` が呼ばれ、selection を消せる。
|
||||
- reload / snapshot 更新後の selection visibility 補正により、visible row があると selection が戻る可能性がある。
|
||||
- `crates/tui/src/dashboard/tests.rs`
|
||||
- `Esc` が row selection を clear する既存テストがある。
|
||||
- 関連 Ticket:
|
||||
- `00001KTNS1AA8` closed: Panel composer / no-selection / `Esc` semantics の過去文脈。
|
||||
- `00001KTFMMZP0` closed: background reload と selection/composer preservation の過去文脈。
|
||||
|
||||
## Unverified hypotheses
|
||||
|
||||
- 現行の reload 完了時の `ensure_selection_visible()` 系処理が、明示的な no-selection と「まだ selection がない初期/補正状態」を区別していない可能性が高い。
|
||||
- 初期表示時の自動選択も同じ selection visibility 補正または初期化経路に由来している可能性がある。
|
||||
|
||||
## Undecided points / open questions
|
||||
|
||||
- なし。初回表示から未選択にする方針で合意済み。
|
||||
|
||||
## Background
|
||||
|
||||
Dashboard composer は row selection と composer target の組み合わせで挙動が変わる。`TicketIntake` target で Ticket row が選択されていると、入力が既存 Ticket の詳細化 / planning return として扱われる経路がある。一方、ユーザーが global / 新規 Intake として入力したい場合は row selection がない状態を維持できる必要がある。
|
||||
|
||||
現状は `Esc` で selection を消せても、background reload 等で選択が戻るため、ユーザーが意図した no-selection 状態を維持しづらい。
|
||||
|
||||
## Requirements
|
||||
|
||||
- 初回 `yoi panel` 表示時は、Ticket row / Pod row を自動選択しない。
|
||||
- ユーザーが `Esc` で row selection を clear した後、reload / background reload / snapshot 更新が完了しても `selected_row = None` を維持する。
|
||||
- reload によって既存の明示選択がまだ visible な場合は、その selection を維持してよい。
|
||||
- reload によって選択中 row が消えた場合は安全に未選択へ落とす。
|
||||
- Up/Down、mouse click、その他既存の明示的な row selection 操作では従来どおり selection を作れる。
|
||||
- 未選択状態で `TicketIntake` target に非空 composer 入力して Enter した場合、既存 Ticket refinement ではなく global / 新規 Intake launch として扱われる。
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- 初回 `yoi panel` 起動後、visible row があっても row selection はない。
|
||||
- `Esc` で selection を clear した後、panel reload completion を挟んでも selection は復活しない。
|
||||
- 未選択状態で `TicketIntake` target に composer 入力して Enter すると、新規/global Intake handoff になり、既存 Ticket の詳細化 handoff にならない。
|
||||
- keyboard navigation または mouse click により、ユーザーは明示的に row を再選択できる。
|
||||
- 既存の selected-row action、queue/close/open 操作、Ticket workflow state semantics は変えない。
|
||||
- focused tests で少なくとも以下を確認する:
|
||||
- initial Dashboard state has no selected row when rows exist;
|
||||
- `Esc` clear -> reload completion -> still no selected row;
|
||||
- no-selection + `TicketIntake` composer submit routes to global/new Intake rather than selected Ticket refinement.
|
||||
|
||||
## Binding decisions / invariants
|
||||
|
||||
- no-selection は first-class UX state として扱う。
|
||||
- reload は、ユーザーが明示的に作った no-selection state を勝手に解除しない。
|
||||
- 初回表示でも自動選択しない。
|
||||
- composer draft は reload / selection clear によって失わない。
|
||||
- Ticket state transition / lifecycle authority は変更しない。
|
||||
- row click は selection のみを変更し、click だけで action dispatch しない既存方針を維持する。
|
||||
|
||||
## Implementation latitude
|
||||
|
||||
- 実装は `selected_row: Option<PanelRowKey>` に加えて「明示 no-selection」フラグを持つ、または selection visibility 補正の呼び出し条件を整理するなど、局所的に選んでよい。
|
||||
- 初期表示・reload 完了・row 消失・navigation 開始の各経路で、selection を作る責務を明示的 user action に寄せられればよい。
|
||||
- UI 表示文言の小変更は許容するが、この Ticket の主目的は selection semantics の修正であり、広い Dashboard layout redesign は含めない。
|
||||
|
||||
## Readiness
|
||||
|
||||
- readiness: implementation_ready
|
||||
- risk_flags: [tui-ux, panel-selection, reload-state]
|
||||
|
||||
## Escalation conditions
|
||||
|
||||
- 初回未選択にすると keyboard-only navigation や blank Enter action の既存 contract が大きく変わることが判明した場合。
|
||||
- selection visibility 補正が reload 以外の重要な safety/correctness を担っており、単純に止めると stale selection や action 誤爆が起きる場合。
|
||||
- focused tests では確認できず、実端末 `yoi panel` / PTY 経路での挙動確認が必要になった場合。
|
||||
|
||||
## Validation
|
||||
|
||||
- `cargo test -q -p tui workspace_panel`
|
||||
- 必要に応じて Dashboard 関連の focused test 名指定。
|
||||
- 変更範囲に応じて `cargo fmt --check`、`git diff --check`、`cargo check -q`。
|
||||
- terminal delivery / timing に依存する挙動が疑わしい場合は、既存の Panel PTY/E2E 系テストまたは実 `yoi panel` で確認する。
|
||||
|
||||
## Related work
|
||||
|
||||
- `00001KTNS1AA8` — Panel composer / no-selection / `Esc` semantics の関連 closed Ticket。
|
||||
- `00001KTFMMZP0` — Dashboard background reload / selection preservation の関連 closed Ticket。
|
||||
- `crates/tui/src/dashboard/mod.rs`
|
||||
- `crates/tui/src/dashboard/tests.rs`
|
||||
- `crates/tui/src/dashboard/render.rs`
|
||||
23
.yoi/tickets/00001KVSKJ0EA/thread.md
Normal file
23
.yoi/tickets/00001KVSKJ0EA/thread.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<!-- event: create author: LocalTicketBackend at: 2026-06-23T06:44:20Z -->
|
||||
|
||||
## 作成
|
||||
|
||||
LocalTicketBackend によって作成されました。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: intake_summary author: ticket-intake at: 2026-06-23T06:46:36Z -->
|
||||
|
||||
## Intake summary
|
||||
|
||||
ユーザー合意により ready 化。Ticket は `implementation_ready` で、初回 `yoi panel` 表示時から未選択、`Esc` 後の no-selection を reload が復活させない、未選択 + `TicketIntake` composer submit を global/new Intake に流す、という要件・受け入れ条件・validation が明記されている。未決定点なし。
|
||||
|
||||
---
|
||||
|
||||
<!-- event: state_changed author: ticket-intake at: 2026-06-23T06:46:36Z from: planning to: ready reason: user_requested_ready field: state -->
|
||||
|
||||
## State changed
|
||||
|
||||
ユーザーから `readyにして` の明示指示があり、対象 Ticket の item/thread を確認した。要件・受け入れ条件・risk flags・validation が揃っており、Orchestrator が routing 可能なため `ready` に遷移する。
|
||||
|
||||
---
|
||||
|
|
@ -115,7 +115,7 @@ impl LocalRuntimeBridge {
|
|||
self.workspace_root
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or("workspace")
|
||||
.expect("workspace root must have a final path component")
|
||||
),
|
||||
kind: "local_host".to_string(),
|
||||
status,
|
||||
|
|
|
|||
|
|
@ -147,6 +147,13 @@ fn parse_serve_options(args: &[String]) -> Result<ServeOptions, CliError> {
|
|||
index += 1;
|
||||
}
|
||||
|
||||
let workspace = workspace.canonicalize().map_err(|error| {
|
||||
CliError(format!(
|
||||
"failed to canonicalize workspace `{}`: {error}",
|
||||
workspace.display()
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(ServeOptions {
|
||||
workspace,
|
||||
db,
|
||||
|
|
|
|||
|
|
@ -38,10 +38,7 @@ pub struct ServerConfig {
|
|||
impl ServerConfig {
|
||||
pub fn local_dev(workspace_root: impl Into<PathBuf>) -> Self {
|
||||
let workspace_root = workspace_root.into();
|
||||
let display = workspace_root
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or("workspace");
|
||||
let display = workspace_display_name_from_root(&workspace_root);
|
||||
Self {
|
||||
workspace_id: format!("local:{display}"),
|
||||
workspace_root,
|
||||
|
|
@ -64,12 +61,7 @@ pub struct WorkspaceApi {
|
|||
|
||||
impl WorkspaceApi {
|
||||
pub async fn new(config: ServerConfig, store: Arc<dyn ControlPlaneStore>) -> Result<Self> {
|
||||
let display_name = config
|
||||
.workspace_root
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or("workspace")
|
||||
.to_string();
|
||||
let display_name = workspace_display_name_from_root(&config.workspace_root);
|
||||
store
|
||||
.upsert_workspace(&WorkspaceRecord {
|
||||
workspace_id: config.workspace_id.clone(),
|
||||
|
|
@ -103,14 +95,17 @@ impl WorkspaceApi {
|
|||
}
|
||||
|
||||
fn workspace_display_name(&self) -> String {
|
||||
self.config
|
||||
.workspace_root
|
||||
workspace_display_name_from_root(&self.config.workspace_root)
|
||||
}
|
||||
}
|
||||
|
||||
fn workspace_display_name_from_root(workspace_root: &std::path::Path) -> String {
|
||||
workspace_root
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or("workspace")
|
||||
.expect("workspace root must have a final path component")
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_router(api: WorkspaceApi) -> Router {
|
||||
Router::new()
|
||||
|
|
@ -243,14 +238,7 @@ async fn get_workspace(State(api): State<WorkspaceApi>) -> ApiResult<Json<Worksp
|
|||
let display_name = stored
|
||||
.as_ref()
|
||||
.map(|record| record.display_name.clone())
|
||||
.or_else(|| {
|
||||
api.config
|
||||
.workspace_root
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.map(str::to_string)
|
||||
})
|
||||
.unwrap_or_else(|| "workspace".to_string());
|
||||
.unwrap_or_else(|| workspace_display_name_from_root(&api.config.workspace_root));
|
||||
Ok(Json(WorkspaceResponse {
|
||||
workspace_id: api.config.workspace_id.clone(),
|
||||
display_name,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user