diff --git a/.yoi/tickets/00001KVSEBF56/item.md b/.yoi/tickets/00001KVSEBF56/item.md index 3368fc3d..88a70970 100644 --- a/.yoi/tickets/00001KVSEBF56/item.md +++ b/.yoi/tickets/00001KVSEBF56/item.md @@ -1,8 +1,8 @@ --- title: 'Generate Workspace web TypeScript types from protocol crate' -state: 'inprogress' +state: 'closed' created_at: '2026-06-23T05:13:22Z' -updated_at: '2026-06-23T05:42:14Z' +updated_at: '2026-06-23T06:22:01Z' assignee: null queued_by: 'workspace-panel' queued_at: '2026-06-23T05:40:01Z' diff --git a/.yoi/tickets/00001KVSEBF56/resolution.md b/.yoi/tickets/00001KVSEBF56/resolution.md new file mode 100644 index 00000000..125e14fc --- /dev/null +++ b/.yoi/tickets/00001KVSEBF56/resolution.md @@ -0,0 +1,29 @@ +Protocol crate 由来の Workspace web TypeScript type generation を実装し、Orchestrator worktree の `orchestration` branch に統合した。 + +主な成果: +- `crates/protocol` の `stream` module / tokio dependency を default `stream` feature に分離し、DTO-only build で tokio を不要にした。 +- Optional `typescript` feature と `ts-rs` による TypeScript export を追加。 +- Protocol DTOs に `cfg_attr(feature = "typescript", derive(ts_rs::TS))` を追加。 +- Deterministic generator を追加: + - `crates/protocol/src/typescript.rs` + - `crates/protocol/examples/generate_typescript.rs` +- Drift check を追加: + - `cargo test -p protocol --features typescript generated_protocol_types_are_current` +- Generated artifact を追加: + - `web/workspace/src/lib/generated/protocol.ts` +- Workspace web が generated root protocol types を re-export: + - `PodProtocolMethod` + - `PodProtocolEvent` + - `PodProtocolSegment` +- Workspace backend extension-point notes に、browser が Pod Unix socket に直接接続せず、将来の backend proxy が Worker identity と method allow/block boundary を enforce する方針を記録。 +- `Cargo.lock` と `package.nix` cargo hash を更新。 + +統合・検証: +- Merge commit: `9728b533 merge: protocol typescript generation` +- Implementation commit: `a13fb693 protocol: generate workspace TypeScript types` +- Reviewer final verdict: approve +- Validation passed: `cargo fmt --check`, `git diff --check HEAD^1..HEAD`, `cargo test -p protocol`, `cargo test -p protocol --features typescript generated_protocol_types_are_current`, `cargo test -p protocol --features typescript`, `cargo check -p protocol --target wasm32-unknown-unknown --no-default-features`, `deno task check`, `deno task build`, `cargo run -p yoi -- ticket doctor`, and `nix build .#yoi --no-link`。 + +既知の非ブロッキング事項: +- `ts-rs` は `#[serde(other)]` on `Segment::Unknown` に warning を出すが、generated artifact には `{ "kind": "unknown" }` が含まれ、current validation は pass。 +- 一部 `Option` + `skip_serializing_if` fields は TS で optional field ではなく required nullable に出る。将来 UI が該当 field を使う際は注意。 \ No newline at end of file diff --git a/.yoi/tickets/00001KVSEBF56/thread.md b/.yoi/tickets/00001KVSEBF56/thread.md index 46104735..693ad94e 100644 --- a/.yoi/tickets/00001KVSEBF56/thread.md +++ b/.yoi/tickets/00001KVSEBF56/thread.md @@ -126,3 +126,267 @@ Next action: - Wait for Coder implementation report, then spawn Reviewer read-only for the implementation diff。 --- + + + +## Implementation report + +Coder implementation report received from `yoi-coder-00001KVSEBF56`. + +Implementation commit: +- `a13fb693 protocol: generate workspace TypeScript types` + +Changed areas: +- `crates/protocol` + - Made `stream` optional behind default feature `stream`。 + - Added optional `typescript` feature using `ts-rs`。 + - Added `cfg_attr(feature = "typescript", derive(ts_rs::TS))` to protocol DTOs。 + - Added deterministic generator module and example: + - `crates/protocol/src/typescript.rs` + - `crates/protocol/examples/generate_typescript.rs` + - Added drift check: + - `cargo test -p protocol --features typescript generated_protocol_types_are_current` +- `web/workspace` + - Added generated TypeScript artifact: + - `web/workspace/src/lib/generated/protocol.ts` + - Re-exported generated root protocol types from workspace sidebar types: + - `PodProtocolMethod` + - `PodProtocolEvent` + - `PodProtocolSegment` +- `crates/workspace-server` + - Updated extension-point notes to explicitly preserve no direct browser-to-Pod socket exposure and require future Workspace-server proxy enforcement of Worker identity / method allow-block boundaries。 +- Packaging/dependencies: + - Updated `Cargo.lock`。 + - Updated `package.nix` `cargoHash`。 + +Coder validation reported: +- `cargo fmt --check`: passed +- `cargo test -p protocol`: passed +- `cargo test -p protocol --features typescript generated_protocol_types_are_current`: passed +- `cargo test -p protocol --features typescript`: passed +- `cargo check -p protocol --target wasm32-unknown-unknown --no-default-features`: passed +- `cd web/workspace && deno task check && deno task build`: passed +- `cargo run -p yoi -- ticket doctor`: passed +- `git diff --check`: passed +- `nix build .#yoi --no-link`: passed + +Generated artifact cleanup: +- Only intended generated TypeScript import artifact is tracked。 +- Orchestrator removed ignored `web/workspace/node_modules/` before Reviewer routing; no ignored frontend artifacts remain under `web/workspace`。 + +Orchestrator evidence checked: +- Worktree clean on `impl/00001KVSEBF56-protocol-typescript-types` at `a13fb693`。 +- Diff `746c51c7..HEAD` touches expected protocol/frontend/workspace-server/package files only。 +- `git diff --check 746c51c7..HEAD`: passed。 + +Next action: +- Route to read-only Reviewer Pod for acceptance review。 + +--- + + + +## Comment + +Review routing: + +Spawned read-only Reviewer Pod: +- `yoi-reviewer-00001KVSEBF56-r1` + +Review target: +- Worktree: `/home/hare/Projects/yoi/.worktree/00001KVSEBF56-protocol-typescript-types` +- Branch: `impl/00001KVSEBF56-protocol-typescript-types` +- Baseline: `746c51c7` +- Implementation commit: `a13fb693 protocol: generate workspace TypeScript types` +- Diff: `746c51c7..a13fb693` + +Reviewer scope: +- read-only `/home/hare/Projects/yoi` + +Reviewer focus: +- Generated TS shape vs serde wire shape。 +- `Method` / `Event` / `Segment` coverage。 +- Deterministic drift check。 +- `stream` feature gating and wasm/no-default protocol compile。 +- No direct browser-to-Pod socket implication。 +- Package/Nix updates。 + +Orchestrator will wait for reviewer verdict before integration。 + +--- + + + +## Review: approve + +Verdict: `approve` + +確認範囲: +- Ticket authority: + - `.yoi/tickets/00001KVSEBF56/item.md` + - `.yoi/tickets/00001KVSEBF56/thread.md` +- Diff `746c51c7..a13fb693`: + - `crates/protocol/Cargo.toml` + - `crates/protocol/src/lib.rs` + - `crates/protocol/src/typescript.rs` + - `crates/protocol/examples/generate_typescript.rs` + - `web/workspace/src/lib/generated/protocol.ts` + - `web/workspace/src/lib/workspace-sidebar/types.ts` + - `crates/workspace-server/src/server.rs` + - `Cargo.lock` + - `package.nix` + +Blocking issues: +- None。 + +Non-blocking concerns: +- `cargo test -p protocol --features typescript` emits `ts-rs` warnings for `#[serde(other)]` on `Segment::Unknown`: `ts-rs failed to parse this attribute. It will be ignored.` The generated artifact still includes `{ "kind": "unknown" }`, and this does not break current validation, but TS generator is not fully understanding serde fallback semantics。 +- Some `Option` fields with `skip_serializing_if = "Option::is_none"` are generated as required nullable rather than optional nullable, e.g. `RewindTarget.disabled_reason: string | null` and `warning: string | null`. Acceptable for this initial generated-types ticket, but future UI code should be careful because runtime JSON may omit those fields。 + +Review findings: +- `crates/protocol` remains Rust DTO authority。 +- `stream` is now an optional default feature: + - `default = ["stream"]` + - `stream = ["dep:tokio"]` + - `tokio` is optional。 +- DTO-only/no-default protocol build compiles for wasm without `tokio`。 +- `typescript` feature is optional and uses `ts-rs`。 +- Generated TS root includes `Method`, `Event`, `Segment`, plus related DTOs such as `PodEvent`, `ScopeRule`, `Greeting`, `InFlight*`, rewind DTOs, completion DTOs, status/result/error enums。 +- Generated TS file is committed at `web/workspace/src/lib/generated/protocol.ts`。 +- Workspace web re-exports generated root protocol types as `PodProtocolMethod`, `PodProtocolEvent`, and `PodProtocolSegment`。 +- Generated TS tagged enums and casing match observed serde wire shape for critical roots: + - `Method`: `{ "method": "...", "params": ... }` + - `Event`: `{ "event": "...", "data": ... }` + - `Segment` / `PodEvent` / `InFlightBlock`: internal `"kind"` tags with snake_case variants。 +- Drift check `generated_protocol_types_are_current` compares committed generated artifact to deterministic generator output。 +- Backend proxy stance is represented in Workspace API extension-point notes and does not add direct browser-to-Pod socket access or broad Worker operation UI。 + +Reviewer validation: +- `cargo fmt --check`: passed +- `git diff --check 746c51c7..HEAD`: passed +- `cargo test -p protocol`: passed (`40 passed`) +- `cargo test -p protocol --features typescript generated_protocol_types_are_current`: passed; emitted non-blocking `ts-rs` warning for `serde(other)`。 +- `cargo test -p protocol --features typescript`: passed (`41 passed`); emitted same non-blocking warning。 +- `cargo check -p protocol --target wasm32-unknown-unknown --no-default-features`: passed +- `cd web/workspace && deno task check && deno task build`: passed (`svelte-check found 0 errors and 0 warnings`; build succeeded) +- `cargo run -p yoi -- ticket doctor`: passed (`doctor: ok`) +- `nix build .#yoi --no-link`: passed + +Generated artifact status: +- `web/workspace/src/lib/generated/protocol.ts` remained unchanged after validation。 +- SHA-256 after validation: `3984a043ded1179545656e5402171c49b2d22b5529aa17f016b675e394b7e553`。 + +Worktree status: +- Clean on `impl/00001KVSEBF56-protocol-typescript-types` at `a13fb693`。 + +--- + + + +## Implementation report + +Reviewer r1 approvalを受けて、実装ブランチを Orchestrator worktree の `orchestration` branch に統合した。 + +統合: +- Merge commit: `9728b533 merge: protocol typescript generation` +- Merged branch: `impl/00001KVSEBF56-protocol-typescript-types` +- Implementation commit: + - `a13fb693 protocol: generate workspace TypeScript types` + +Orchestrator worktree validation: +- `cargo fmt --check`: passed +- `git diff --check HEAD^1..HEAD`: passed +- `cargo test -p protocol`: passed +- `cargo test -p protocol --features typescript generated_protocol_types_are_current`: passed +- `cargo test -p protocol --features typescript`: passed + - Non-blocking `ts-rs` warning for `#[serde(other)]` on `Segment::Unknown` remains as reviewer noted。 +- `cargo check -p protocol --target wasm32-unknown-unknown --no-default-features`: passed +- `cd web/workspace && rm -rf node_modules .svelte-kit build && deno task check`: passed (`svelte-check found 0 errors and 0 warnings`) +- `cd web/workspace && deno task build`: passed +- `cargo run -p yoi -- ticket doctor`: passed (`doctor: ok`) +- `nix build .#yoi --no-link`: passed + +Validation generated ignored frontend artifacts in Orchestrator worktree: +- `web/workspace/node_modules/` +- `web/workspace/.svelte-kit/` +- `web/workspace/build/` + +These were removed after validation. Final Orchestrator worktree status after validation cleanup is clean on `orchestration` at `9728b533`。 + +--- + + + +## State changed + +Reviewer approval、Orchestrator worktree への統合、protocol tests/drift check/wasm no-default check、Deno check/build、Ticket doctor、Nix build が完了したため `done` に遷移する。 + +--- + + + +## State changed + +Ticket を closed にしました。 + + +--- + + + +## 完了 + +Protocol crate 由来の Workspace web TypeScript type generation を実装し、Orchestrator worktree の `orchestration` branch に統合した。 + +主な成果: +- `crates/protocol` の `stream` module / tokio dependency を default `stream` feature に分離し、DTO-only build で tokio を不要にした。 +- Optional `typescript` feature と `ts-rs` による TypeScript export を追加。 +- Protocol DTOs に `cfg_attr(feature = "typescript", derive(ts_rs::TS))` を追加。 +- Deterministic generator を追加: + - `crates/protocol/src/typescript.rs` + - `crates/protocol/examples/generate_typescript.rs` +- Drift check を追加: + - `cargo test -p protocol --features typescript generated_protocol_types_are_current` +- Generated artifact を追加: + - `web/workspace/src/lib/generated/protocol.ts` +- Workspace web が generated root protocol types を re-export: + - `PodProtocolMethod` + - `PodProtocolEvent` + - `PodProtocolSegment` +- Workspace backend extension-point notes に、browser が Pod Unix socket に直接接続せず、将来の backend proxy が Worker identity と method allow/block boundary を enforce する方針を記録。 +- `Cargo.lock` と `package.nix` cargo hash を更新。 + +統合・検証: +- Merge commit: `9728b533 merge: protocol typescript generation` +- Implementation commit: `a13fb693 protocol: generate workspace TypeScript types` +- Reviewer final verdict: approve +- Validation passed: `cargo fmt --check`, `git diff --check HEAD^1..HEAD`, `cargo test -p protocol`, `cargo test -p protocol --features typescript generated_protocol_types_are_current`, `cargo test -p protocol --features typescript`, `cargo check -p protocol --target wasm32-unknown-unknown --no-default-features`, `deno task check`, `deno task build`, `cargo run -p yoi -- ticket doctor`, and `nix build .#yoi --no-link`。 + +既知の非ブロッキング事項: +- `ts-rs` は `#[serde(other)]` on `Segment::Unknown` に warning を出すが、generated artifact には `{ "kind": "unknown" }` が含まれ、current validation は pass。 +- 一部 `Option` + `skip_serializing_if` fields は TS で optional field ではなく required nullable に出る。将来 UI が該当 field を使う際は注意。 + +--- + + + +## Implementation report + +Post-close cleanup completed。 + +- Stopped child Pods and reclaimed scope: + - `yoi-coder-00001KVSEBF56` + - `yoi-reviewer-00001KVSEBF56-r1` +- Removed ignored frontend validation artifacts from child worktree before worktree removal if present: + - `web/workspace/node_modules/` + - `web/workspace/.svelte-kit/` + - `web/workspace/build/` +- Removed implementation worktree: + - `/home/hare/Projects/yoi/.worktree/00001KVSEBF56-protocol-typescript-types` +- Deleted implementation branch: + - `impl/00001KVSEBF56-protocol-typescript-types` +- Orchestrator worktree remains clean on `orchestration` at `b547203f`。 + +Root/original workspace was not used for merge/validation/cleanup operations。 + +--- diff --git a/.yoi/tickets/00001KVSFXY88/artifacts/orchestration-plan.jsonl b/.yoi/tickets/00001KVSFXY88/artifacts/orchestration-plan.jsonl new file mode 100644 index 00000000..35d959ef --- /dev/null +++ b/.yoi/tickets/00001KVSFXY88/artifacts/orchestration-plan.jsonl @@ -0,0 +1 @@ +{"id":"orch-plan-20260623-061132-1","ticket_id":"00001KVSFXY88","kind":"accepted_plan","accepted_plan":{"summary":"Remove redundant Workspace Dashboard title/actionbar key hints and selected-row textual status display while preserving row selection markers and Dashboard keyboard/action semantics, updating render tests accordingly.","branch":"impl/00001KVSFXY88-dashboard-hint-cleanup","worktree":"/home/hare/Projects/yoi/.worktree/00001KVSFXY88-dashboard-hint-cleanup","role_plan":"Orchestrator creates a dedicated child worktree and spawns a narrow-scope Coder. Reviewer will be spawned read-only after Coder reports implementation commit(s). After approval, Orchestrator integrates into `orchestration`, validates TUI tests, records closure, and cleans only the child worktree/branch."},"author":"yoi-orchestrator","at":"2026-06-23T06:11:32Z"} diff --git a/.yoi/tickets/00001KVSFXY88/item.md b/.yoi/tickets/00001KVSFXY88/item.md index a44bf56f..8d890dcf 100644 --- a/.yoi/tickets/00001KVSFXY88/item.md +++ b/.yoi/tickets/00001KVSFXY88/item.md @@ -1,8 +1,8 @@ --- title: 'TUI Dashboard の冗長な key hints と selected-row 状態表示を削る' -state: 'queued' +state: 'closed' created_at: '2026-06-23T05:40:56Z' -updated_at: '2026-06-23T06:08:42Z' +updated_at: '2026-06-23T06:33:21Z' assignee: null readiness: 'implementation_ready' risk_flags: ['tui-ux', 'terminal-layout'] diff --git a/.yoi/tickets/00001KVSFXY88/resolution.md b/.yoi/tickets/00001KVSFXY88/resolution.md new file mode 100644 index 00000000..29c0d388 --- /dev/null +++ b/.yoi/tickets/00001KVSFXY88/resolution.md @@ -0,0 +1,20 @@ +TUI Dashboard の冗長な key hints と selected-row textual status display を削除し、Orchestrator worktree の `orchestration` branch に統合した。 + +主な成果: +- Top title line から `Row selection`, `blank Enter`, `Tab target` などの key hint guidance を削除。 +- Selected Ticket / Pod / Intake / no-row selected textual status line を blank にした。 +- Composer bottom actionbar を always-on key hints ではなく notices / diagnostics only に最小化。 +- Row marker / highlighting rendering は維持。 +- Keyboard/action behavior は変更していない。 +- Unused selected-row display-status helper を削除。 +- Dashboard render/unit tests を新 display contract に更新し、selected row marker の visibility assertion は維持。 + +統合・検証: +- Merge commit: `5abf16f9 merge: dashboard hint cleanup` +- Implementation commit: `03ad525f tui: trim dashboard redundant hints` +- Reviewer final verdict: approve +- Validation passed: `cargo fmt --check`, `git diff --check HEAD^1..HEAD`, `cargo test -p tui dashboard --lib`, `cargo test -p tui workspace_panel --lib`, and `cargo run -p yoi -- ticket doctor`。 + +範囲外: +- Console / single-Pod TUI hints は変更していない。 +- Manual/PTY `yoi panel` visual check は実施していない。Focused render/unit tests を主 validation とした。 \ No newline at end of file diff --git a/.yoi/tickets/00001KVSFXY88/thread.md b/.yoi/tickets/00001KVSFXY88/thread.md index 194604e6..85359277 100644 --- a/.yoi/tickets/00001KVSFXY88/thread.md +++ b/.yoi/tickets/00001KVSFXY88/thread.md @@ -13,4 +13,294 @@ LocalTicketBackend によって作成されました。 Ticket を `workspace-panel` が queued にしました。 +--- + + + +## Decision + +Routing decision: `implementation_ready_parallel` + +Reason: +- Ticket body has concrete Dashboard render targets and acceptance criteria。 +- No relations / blockers / orchestration plan records exist。 +- Active work `00001KVSEBF56` (protocol TS generation) and `00001KVSGFM65` (Workspace web Kanban review) are separate from TUI Dashboard render code。 +- Orchestrator worktree is clean on `orchestration` at `0e635ba6`; target worktree / branch is not present。 +- Bounded code map confirms relevant functions in `crates/tui/src/dashboard/render.rs`: `draw_title`, `target_status_line`, `actionbar_left_text`, `actionbar_right_text`, `draw_actionbar`, and tests in `crates/tui/src/dashboard/tests.rs`。 + +IntentPacket: + +Intent: +- Remove redundant Dashboard key hints and selected-row textual status while preserving actual row selection visibility and keyboard behavior。 + +Binding decisions / invariants: +- This Ticket only changes Dashboard display cleanup。 +- Console / single-Pod TUI key hints are out of scope。 +- Do not remove list row selection markers / highlighting / keyboard navigation visibility。 +- Do not reintroduce direct selected-Pod send。 +- Companion lifecycle / Orchestrator lifecycle / Ticket workflow semantics must not change。 +- Existing Dashboard actions must remain: blank Enter row action, text Enter composer-target action, Tab target switching, Esc clear selection, Pod open/attach, Ticket Intake/Queue flows。 + +Requirements / acceptance criteria: +- Top title line no longer shows key hint guidance such as `Row selection`, `blank Enter`, `Tab target`。 +- Composer bottom actionbar no longer shows always-on key hint groups, or is minimized to non-redundant notices only。 +- Selected Ticket / selected Pod / selected Intake Pod / no row selected textual status line is removed or no longer recognized as redundant selected-row status。 +- List still visibly indicates current selected row。 +- Related render/unit tests are updated to the new display specification。 + +Implementation latitude: +- `target_status_line` layout row may be removed, made empty/minimal, or repurposed only if it does not show redundant selected-row status。 +- Actionbar may be fully removed, notice-only, or otherwise minimized depending on layout/tests。 +- Keep row markers like `▶` and selected styling unless explicitly escalated。 +- If layout row removal creates resize/hitbox complications, choose minimal empty/notice behavior and document the tradeoff。 + +Escalate if: +- Removing status/actionbar rows makes current composer target/action ambiguous enough to require alternative UI。 +- Row selection marker/visibility also needs removal。 +- Layout changes cause terminal resize / hitbox / mouse selection side effects that require broader Dashboard layout refactor。 + +Validation plan: +- `cargo fmt --check` +- `cargo test -p tui dashboard --lib` +- `cargo test -p tui workspace_panel --lib` if affected by shared tests/helpers。 +- `git diff --check` +- `cargo run -p yoi -- ticket doctor` +- Manual/PTY `yoi panel` check if practical, but focused render tests are primary。 + +--- + + + +## State changed + +Human authorized queue routing from Workspace Dashboard. Ticket has concrete Dashboard render acceptance criteria and no recorded blockers, so Orchestrator accepts implementation. + +--- + + + +## Implementation report + +Implementation start report: + +- Created child implementation worktree: + - `/home/hare/Projects/yoi/.worktree/00001KVSFXY88-dashboard-hint-cleanup` +- Created branch: + - `impl/00001KVSFXY88-dashboard-hint-cleanup` +- Base commit: + - `3bfd1ca0 ticket: accept dashboard hint cleanup` +- Confirmed tracked Ticket project records are visible in the child worktree。 +- Confirmed `.yoi/memory` has no tracked/untracked entries in the child worktree check。 +- Spawned sibling Coder Pod: + - `yoi-coder-00001KVSFXY88` +- Coder scope: + - read: `/home/hare/Projects/yoi` + - write: `/home/hare/Projects/yoi/.worktree/00001KVSFXY88-dashboard-hint-cleanup` + +Parallelization note: +- Active Workspace web / protocol work is separate from TUI Dashboard rendering. This Ticket should stay limited to `crates/tui/src/dashboard/*` unless tests reveal a narrow shared helper impact。 + +Next action: +- Wait for Coder implementation report, then spawn Reviewer read-only for the implementation diff。 + +--- + + + +## Implementation report + +Coder implementation report received from `yoi-coder-00001KVSFXY88`. + +Implementation commit: +- `03ad525f tui: trim dashboard redundant hints` + +Changed areas: +- `crates/tui/src/dashboard/render.rs` + - Removed top title key-hint guidance。 + - Made selected-row/composer target status line blank。 + - Minimized actionbar to notices/diagnostic-only text。 + - Preserved row marker/highlighting rendering。 +- `crates/tui/src/dashboard/mod.rs` + - Removed now-unused selected-row display-status helper。 +- `crates/tui/src/dashboard/tests.rs` + - Updated render/unit expectations for no redundant hints/status。 + - Added title guidance cleanup assertion。 + - Kept assertions that selected rows remain visibly marked。 + +Coder validation reported: +- `cargo fmt --check`: passed +- `cargo test -p tui dashboard --lib`: passed +- `cargo test -p tui workspace_panel --lib`: passed +- `git diff --check`: passed +- `cargo run -p yoi -- ticket doctor`: passed + +Manual/PTY `yoi panel` check: +- Not run; focused render/unit tests were used as primary validation。 + +Orchestrator evidence checked: +- Worktree clean on `impl/00001KVSFXY88-dashboard-hint-cleanup`。 +- HEAD is `03ad525f`。 +- Diff `3bfd1ca0..HEAD` touches expected dashboard render/module/tests files only。 +- `git diff --check 3bfd1ca0..HEAD`: passed。 + +Next action: +- Route to read-only Reviewer Pod for acceptance review。 + +--- + + + +## Comment + +Review routing: + +Spawned read-only Reviewer Pod: +- `yoi-reviewer-00001KVSFXY88-r1` + +Review target: +- Worktree: `/home/hare/Projects/yoi/.worktree/00001KVSFXY88-dashboard-hint-cleanup` +- Branch: `impl/00001KVSFXY88-dashboard-hint-cleanup` +- Baseline: `3bfd1ca0` +- Implementation commit: `03ad525f tui: trim dashboard redundant hints` +- Diff: `3bfd1ca0..03ad525f` + +Reviewer scope: +- read-only `/home/hare/Projects/yoi` + +Reviewer focus: +- Removal of redundant title/actionbar key hints and selected-row textual status。 +- Preservation of visible row selection marker/highlighting。 +- No keyboard behavior/action semantics changes。 +- Dashboard render tests updated to new display contract。 + +Orchestrator will wait for reviewer verdict before integration。 + +--- + + + +## Review: approve + +Verdict: `approve` + +確認範囲: +- Ticket authority: + - `.yoi/tickets/00001KVSFXY88/item.md` + - `.yoi/tickets/00001KVSFXY88/thread.md` +- Diff `3bfd1ca0..03ad525f`: + - `crates/tui/src/dashboard/render.rs` + - `crates/tui/src/dashboard/mod.rs` + - `crates/tui/src/dashboard/tests.rs` + +Evidence: +- Top title guidance was removed; `title_line` now keeps only `workspace dashboard` plus companion/orchestrator status。 +- `target_status_line` now renders blank, removing selected Ticket / Pod / Intake / no-row textual status。 +- Actionbar is reduced to sending/refreshing/notices/diagnostic text; always-on key hint groups are gone。 +- Row marker/highlighting paths remain; tests assert selected row marker strings such as `▶ ready`, `▶ queued`, and ` ▶ live`。 +- Keyboard/action behavior was not altered in the diff; removed helpers were display-only and no remaining references exist。 +- No Console/single-Pod TUI files changed。 +- No direct selected-Pod send was reintroduced; existing dashboard behavior tests still pass。 + +Blocking issues: +- None。 + +Non-blocking concerns: +- Manual `yoi panel` PTY visual check was not run; focused render/unit coverage was used as primary validation, as suggested。 + +Reviewer validation: +- `cargo fmt --check`: passed +- `git diff --check 3bfd1ca0..HEAD`: passed +- `cargo test -p tui dashboard --lib`: passed (`107 passed`) +- `cargo test -p tui workspace_panel --lib`: passed (`23 passed`) +- `cargo run -p yoi -- ticket doctor`: passed (`doctor: ok`) + +Worktree status: +- Clean on `impl/00001KVSFXY88-dashboard-hint-cleanup` at `03ad525f`。 + +--- + + + +## Implementation report + +Reviewer r1 approvalを受けて、実装ブランチを Orchestrator worktree の `orchestration` branch に統合した。 + +統合: +- Merge commit: `5abf16f9 merge: dashboard hint cleanup` +- Merged branch: `impl/00001KVSFXY88-dashboard-hint-cleanup` +- Implementation commit: + - `03ad525f tui: trim dashboard redundant hints` + +Orchestrator worktree validation: +- `cargo fmt --check`: passed +- `git diff --check HEAD^1..HEAD`: passed +- `cargo test -p tui dashboard --lib`: passed (`107 passed`) +- `cargo test -p tui workspace_panel --lib`: passed (`23 passed`) +- `cargo run -p yoi -- ticket doctor`: passed (`doctor: ok`) + +Final Orchestrator worktree status after validation is clean on `orchestration` at `5abf16f9`。 + +--- + + + +## State changed + +Reviewer approval、Orchestrator worktree への統合、focused Dashboard / workspace_panel tests、Ticket doctor が完了したため `done` に遷移する。 + +--- + + + +## State changed + +Ticket を closed にしました。 + + +--- + + + +## 完了 + +TUI Dashboard の冗長な key hints と selected-row textual status display を削除し、Orchestrator worktree の `orchestration` branch に統合した。 + +主な成果: +- Top title line から `Row selection`, `blank Enter`, `Tab target` などの key hint guidance を削除。 +- Selected Ticket / Pod / Intake / no-row selected textual status line を blank にした。 +- Composer bottom actionbar を always-on key hints ではなく notices / diagnostics only に最小化。 +- Row marker / highlighting rendering は維持。 +- Keyboard/action behavior は変更していない。 +- Unused selected-row display-status helper を削除。 +- Dashboard render/unit tests を新 display contract に更新し、selected row marker の visibility assertion は維持。 + +統合・検証: +- Merge commit: `5abf16f9 merge: dashboard hint cleanup` +- Implementation commit: `03ad525f tui: trim dashboard redundant hints` +- Reviewer final verdict: approve +- Validation passed: `cargo fmt --check`, `git diff --check HEAD^1..HEAD`, `cargo test -p tui dashboard --lib`, `cargo test -p tui workspace_panel --lib`, and `cargo run -p yoi -- ticket doctor`。 + +範囲外: +- Console / single-Pod TUI hints は変更していない。 +- Manual/PTY `yoi panel` visual check は実施していない。Focused render/unit tests を主 validation とした。 + +--- + + + +## Implementation report + +Post-close cleanup completed。 + +- Stopped child Pods and reclaimed scope: + - `yoi-coder-00001KVSFXY88` + - `yoi-reviewer-00001KVSFXY88-r1` +- Removed implementation worktree: + - `/home/hare/Projects/yoi/.worktree/00001KVSFXY88-dashboard-hint-cleanup` +- Deleted implementation branch: + - `impl/00001KVSFXY88-dashboard-hint-cleanup` +- Orchestrator worktree remains clean on `orchestration` at `10866666`。 + +Root/original workspace was not used for merge/validation/cleanup operations。 + --- diff --git a/.yoi/tickets/00001KVSGFM65/item.md b/.yoi/tickets/00001KVSGFM65/item.md index b1e11c33..b07d2814 100644 --- a/.yoi/tickets/00001KVSGFM65/item.md +++ b/.yoi/tickets/00001KVSGFM65/item.md @@ -1,8 +1,8 @@ --- title: 'Improve Workspace web ticket Kanban grouping and lazy rows' -state: 'inprogress' +state: 'closed' created_at: '2026-06-23T05:50:36Z' -updated_at: '2026-06-23T06:06:45Z' +updated_at: '2026-06-23T06:16:10Z' assignee: null queued_by: 'workspace-panel' queued_at: '2026-06-23T05:53:22Z' diff --git a/.yoi/tickets/00001KVSGFM65/resolution.md b/.yoi/tickets/00001KVSGFM65/resolution.md new file mode 100644 index 00000000..1f1bb04b --- /dev/null +++ b/.yoi/tickets/00001KVSGFM65/resolution.md @@ -0,0 +1,22 @@ +Workspace web Repository Ticket Kanban の grouping / lazy rows 改善を統合した。 + +主な成果: +- Repository Ticket Kanban を `RepositoryTicketKanban.svelte` component に分離。 +- `planning` + `ready` を display-only group とし、`ready` を `planning` より上に表示。 +- `queued` + `inprogress` を display-only group とし、`inprogress` を `queued` より上に表示。 +- `done`, `closed`, `other` は独立 group として維持。 +- 各 row に original Ticket state を表示。 +- 各 group の初期表示行数を 30 に cap。 +- 各 group に独立 scroll area と independent lazy visible count を実装。 +- High-volume `closed` group が page height を無制限に伸ばさないようにした。 +- `WorkspacePage.svelte` から inline Kanban logic/markup を削減。 +- Styling は existing design tokens を使い、backend/API/Ticket lifecycle semantics は変更していない。 + +統合・検証: +- Merge commit: `eea26f91 merge: kanban lazy rows` +- Implementation commit: `6f68bb8d web: group repository ticket kanban rows` +- Reviewer final verdict: approve +- Validation passed: `git diff --check HEAD^1..HEAD`, `deno task check`, `deno task build`, `cargo run -p yoi -- ticket doctor`, and `nix build .#yoi --no-link`。 + +範囲外: +- Backend pagination、Ticket state mutation UI、drag/drop Kanban、browser/manual scroll E2E tests は追加していない。 \ No newline at end of file diff --git a/.yoi/tickets/00001KVSGFM65/thread.md b/.yoi/tickets/00001KVSGFM65/thread.md index 26473176..77dc9e0e 100644 --- a/.yoi/tickets/00001KVSGFM65/thread.md +++ b/.yoi/tickets/00001KVSGFM65/thread.md @@ -206,3 +206,167 @@ Reviewer focus: Orchestrator will wait for reviewer verdict before integration。 --- + + + +## Review: approve + +Verdict: `approve` + +確認範囲: +- Ticket authority: + - `.yoi/tickets/00001KVSGFM65/item.md` + - `.yoi/tickets/00001KVSGFM65/thread.md` +- Diff `a6f9019e..6f68bb8d`: + - `web/workspace/src/lib/workspace-pages/RepositoryTicketKanban.svelte` + - `web/workspace/src/lib/workspace-pages/WorkspacePage.svelte` + - `web/workspace/src/app.css` + +Review evidence: +- Grouping/sort: + - `planning` + `ready` are grouped as `ready-planning`, labelled `Ready / Planning`。 + - `queued` + `inprogress` are grouped as `inprogress-queued`, labelled `In progress / Queued`。 + - `statePriority()` places `ready` before `planning`, and `inprogress` before `queued`。 +- Other states: + - `done`, `closed`, and `other` remain separate group keys via `state:${state}` / `state:other`。 +- Per-group lazy state: + - `visibleRowsByGroup` is keyed by group key。 + - Initial visible rows are `INITIAL_VISIBLE_ROWS = 30`。 + - Scroll handling updates only `[group.key]`。 + - Near-bottom threshold is `NEAR_BOTTOM_PX = 96`。 +- High-volume containment: + - Each non-empty group row list has its own `.ticket-list-scroll`。 + - `.ticket-list-scroll` uses `max-height: 34rem; overflow-y: auto;`, so `closed` cannot expand page vertically without bound。 +- Original row state: + - Each ticket row renders `{ticket.state}`。 +- Component boundary: + - Kanban logic and row rendering moved into `RepositoryTicketKanban.svelte`。 + - `WorkspacePage.svelte` now delegates with ``。 +- Design / boundary: + - Styling uses existing CSS tokens such as `var(--space-*)`, `var(--line)`, `var(--text-muted)`, `var(--text-faint)`, `var(--text-strong)`。 + - No backend/API/protocol TypeScript generation scope touched。 + - No mutation UI added。 + +Blocking issues: +- None found。 + +Non-blocking concerns: +- No browser/manual scroll interaction test was run; validation is by code inspection plus Svelte check/build. Given the ticket’s frontend-only scope and current test tooling, this is acceptable。 + +Reviewer validation: +- `git diff --check a6f9019e..6f68bb8d`: passed +- `git diff --check a6f9019e..HEAD`: passed +- `cd web/workspace && rm -rf node_modules .svelte-kit build && deno task check && deno task build`: passed (`svelte-check found 0 errors and 0 warnings`; static build completed) +- `cargo run -p yoi -- ticket doctor`: passed (`doctor: ok`) +- `nix build .#yoi --no-link`: passed +- `cargo test -p yoi-workspace-server`: not run; no backend/API changes were made。 + +Generated artifacts: +- Ignored validation artifacts present after review: + - `target/` + - `web/workspace/.svelte-kit/` + - `web/workspace/build/` + - `web/workspace/node_modules/` +- No generated artifacts are tracked。 + +Worktree status: +- Clean tracked state on `impl/00001KVSGFM65-kanban-lazy-rows` at `6f68bb8d`。 + +--- + + + +## Implementation report + +Reviewer r1 approvalを受けて、実装ブランチを Orchestrator worktree の `orchestration` branch に統合した。 + +統合: +- Merge commit: `eea26f91 merge: kanban lazy rows` +- Merged branch: `impl/00001KVSGFM65-kanban-lazy-rows` +- Implementation commit: + - `6f68bb8d web: group repository ticket kanban rows` + +Orchestrator worktree validation: +- `git diff --check HEAD^1..HEAD`: passed +- `cd web/workspace && rm -rf node_modules .svelte-kit build && deno task check`: passed (`svelte-check found 0 errors and 0 warnings`) +- `cd web/workspace && deno task build`: passed +- `cargo run -p yoi -- ticket doctor`: passed (`doctor: ok`) +- `nix build .#yoi --no-link`: passed + +Validation generated ignored frontend artifacts in Orchestrator worktree: +- `web/workspace/node_modules/` +- `web/workspace/.svelte-kit/` +- `web/workspace/build/` + +These were removed after validation. Final Orchestrator worktree status after validation cleanup is clean on `orchestration` at `eea26f91`。 + +--- + + + +## State changed + +Reviewer approval、Orchestrator worktree への統合、Deno check/build、Ticket doctor、Nix build が完了したため `done` に遷移する。 + +--- + + + +## State changed + +Ticket を closed にしました。 + + +--- + + + +## 完了 + +Workspace web Repository Ticket Kanban の grouping / lazy rows 改善を統合した。 + +主な成果: +- Repository Ticket Kanban を `RepositoryTicketKanban.svelte` component に分離。 +- `planning` + `ready` を display-only group とし、`ready` を `planning` より上に表示。 +- `queued` + `inprogress` を display-only group とし、`inprogress` を `queued` より上に表示。 +- `done`, `closed`, `other` は独立 group として維持。 +- 各 row に original Ticket state を表示。 +- 各 group の初期表示行数を 30 に cap。 +- 各 group に独立 scroll area と independent lazy visible count を実装。 +- High-volume `closed` group が page height を無制限に伸ばさないようにした。 +- `WorkspacePage.svelte` から inline Kanban logic/markup を削減。 +- Styling は existing design tokens を使い、backend/API/Ticket lifecycle semantics は変更していない。 + +統合・検証: +- Merge commit: `eea26f91 merge: kanban lazy rows` +- Implementation commit: `6f68bb8d web: group repository ticket kanban rows` +- Reviewer final verdict: approve +- Validation passed: `git diff --check HEAD^1..HEAD`, `deno task check`, `deno task build`, `cargo run -p yoi -- ticket doctor`, and `nix build .#yoi --no-link`。 + +範囲外: +- Backend pagination、Ticket state mutation UI、drag/drop Kanban、browser/manual scroll E2E tests は追加していない。 + +--- + + + +## Implementation report + +Post-close cleanup completed。 + +- Stopped child Pods and reclaimed scope: + - `yoi-coder-00001KVSGFM65` + - `yoi-reviewer-00001KVSGFM65-r1` +- Removed ignored frontend validation artifacts from child worktree before worktree removal if present: + - `web/workspace/node_modules/` + - `web/workspace/.svelte-kit/` + - `web/workspace/build/` +- Removed implementation worktree: + - `/home/hare/Projects/yoi/.worktree/00001KVSGFM65-kanban-lazy-rows` +- Deleted implementation branch: + - `impl/00001KVSGFM65-kanban-lazy-rows` +- Orchestrator worktree remains clean on `orchestration` at `9de04f72`。 + +Root/original workspace was not used for merge/validation/cleanup operations。 + +--- diff --git a/Cargo.lock b/Cargo.lock index da305388..b9102512 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3035,6 +3035,7 @@ dependencies = [ "serde", "serde_json", "tokio", + "ts-rs", "uuid", ] @@ -4696,6 +4697,28 @@ dependencies = [ "toml", ] +[[package]] +name = "ts-rs" +version = "12.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756050066659291d47a554a9f558125db17428b073c5ffce1daf5dcb0f7231d8" +dependencies = [ + "thiserror 2.0.18", + "ts-rs-macros", +] + +[[package]] +name = "ts-rs-macros" +version = "12.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d90eea51bc7988ef9e674bf80a85ba6804739e535e9cab48e4bb34a8b652aa" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "termcolor", +] + [[package]] name = "ttf-parser" version = "0.25.1" diff --git a/crates/protocol/Cargo.toml b/crates/protocol/Cargo.toml index a9b8184e..5b2ad758 100644 --- a/crates/protocol/Cargo.toml +++ b/crates/protocol/Cargo.toml @@ -4,8 +4,14 @@ version = "0.1.0" edition.workspace = true license.workspace = true +[features] +default = ["stream"] +stream = ["dep:tokio"] +typescript = ["dep:ts-rs"] + [dependencies] serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } -tokio = { workspace = true, features = ["io-util"] } +tokio = { workspace = true, features = ["io-util"], optional = true } +ts-rs = { version = "12.0.1", optional = true } uuid = { workspace = true, features = ["serde"] } diff --git a/crates/protocol/examples/generate_typescript.rs b/crates/protocol/examples/generate_typescript.rs new file mode 100644 index 00000000..b5b3fdb9 --- /dev/null +++ b/crates/protocol/examples/generate_typescript.rs @@ -0,0 +1,16 @@ +#[cfg(feature = "typescript")] +fn main() -> Result<(), Box> { + let path = protocol::typescript::generated_typescript_path(); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::write(&path, protocol::typescript::generated_protocol_types())?; + println!("wrote {}", path.display()); + Ok(()) +} + +#[cfg(not(feature = "typescript"))] +fn main() { + eprintln!("enable the `typescript` feature to generate protocol TypeScript bindings"); + std::process::exit(2); +} diff --git a/crates/protocol/src/lib.rs b/crates/protocol/src/lib.rs index ee2e38f8..7dde4707 100644 --- a/crates/protocol/src/lib.rs +++ b/crates/protocol/src/lib.rs @@ -1,4 +1,7 @@ +#[cfg(feature = "stream")] pub mod stream; +#[cfg(feature = "typescript")] +pub mod typescript; use std::path::PathBuf; @@ -21,6 +24,7 @@ fn is_false(value: &bool) -> bool { // --------------------------------------------------------------------------- #[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(tag = "method", content = "params", rename_all = "snake_case")] pub enum Method { Run { @@ -103,6 +107,7 @@ pub enum Method { /// delivery (e.g. `TurnEnded` arriving after `ShutDown` for the same /// child Pod). #[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(tag = "kind", rename_all = "snake_case")] pub enum PodEvent { /// Child finished one turn and is back to IDLE. @@ -175,6 +180,7 @@ impl PodEvent { /// placeholder into the LLM context so neither user nor LLM is blind to /// the dropped intent. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(tag = "kind", rename_all = "snake_case")] pub enum Segment { /// Free-form text. The fallback every client can produce. @@ -266,6 +272,7 @@ impl Method { // --------------------------------------------------------------------------- #[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(tag = "event", content = "data", rename_all = "snake_case")] pub enum Event { /// A user input message was accepted, persisted as @@ -294,6 +301,7 @@ pub enum Event { /// One event per `LogEntry::SystemItem` commit. Disk-side and /// wire-side are 1:1. SystemItem { + #[cfg_attr(feature = "typescript", ts(type = "unknown"))] item: serde_json::Value, }, /// A new self-driving cycle has begun (IDLE → active transition). @@ -453,6 +461,7 @@ pub enum Event { /// role-specific entry events (`SegmentRotated` / `SystemItem`) — /// there is no generic "every committed entry" broadcast. Snapshot { + #[cfg_attr(feature = "typescript", ts(type = "Array"))] entries: Vec, greeting: Greeting, #[serde(default)] @@ -471,6 +480,7 @@ pub enum Event { /// /// Payload is the JSON form of `session_store::LogEntry::SegmentStart`. SegmentRotated { + #[cfg_attr(feature = "typescript", ts(type = "unknown"))] entry: serde_json::Value, }, /// Current Pod controller status. Broadcast on every controller-level @@ -495,6 +505,7 @@ pub enum Event { /// A rewind has truncated the authoritative session. `entries` is the /// retained session-log prefix clients should use to reseed display state. RewindApplied { + #[cfg_attr(feature = "typescript", ts(type = "Array"))] entries: Vec, input: Vec, summary: RewindSummary, @@ -503,14 +514,17 @@ pub enum Event { /// crate can evolve discovery fields without introducing a protocol /// dependency on session-store. PodsListed { + #[cfg_attr(feature = "typescript", ts(type = "unknown"))] pods: serde_json::Value, }, /// Reply to `Method::RestorePod`. PodRestored { + #[cfg_attr(feature = "typescript", ts(type = "unknown"))] result: serde_json::Value, }, /// Reply to `Method::RegisterPeer`. PeerRegistered { + #[cfg_attr(feature = "typescript", ts(type = "unknown"))] result: serde_json::Value, }, Alert(Alert), @@ -530,6 +544,7 @@ pub enum Event { /// `new_segment_id` is the UUID of the freshly created session that /// replaced the old history. CompactDone { + #[cfg_attr(feature = "typescript", ts(type = "string"))] new_segment_id: uuid::Uuid, }, /// Compaction failed. The session is unchanged. @@ -546,6 +561,7 @@ pub enum Event { /// surfaced to the person driving the client. Keep messages short and /// human-readable. #[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] pub struct Alert { pub level: AlertLevel, pub source: AlertSource, @@ -555,6 +571,7 @@ pub struct Alert { } #[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] pub struct MemoryWorkerEvent { pub worker: String, pub status: String, @@ -568,6 +585,7 @@ pub struct MemoryWorkerEvent { } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(rename_all = "snake_case")] pub enum AlertLevel { Warn, @@ -575,6 +593,7 @@ pub enum AlertLevel { } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(rename_all = "snake_case")] pub enum AlertSource { Pod, @@ -591,6 +610,7 @@ pub enum AlertSource { /// nailed down here so the TUI side can ship without waiting for /// the memory / workflow tickets. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(rename_all = "snake_case")] pub enum CompletionKind { File, @@ -605,6 +625,7 @@ pub enum CompletionKind { /// keep a trailing `/` after a directory selection so the user can /// drill in without re-typing the prefix. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] pub struct CompletionEntry { pub value: String, #[serde(default)] @@ -612,12 +633,15 @@ pub struct CompletionEntry { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] pub struct RewindTargetId { + #[cfg_attr(feature = "typescript", ts(type = "string"))] pub segment_id: uuid::Uuid, pub user_input_entry_index: usize, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] pub struct RewindTarget { pub id: RewindTargetId, pub expected_head_entries: usize, @@ -633,6 +657,7 @@ pub struct RewindTarget { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] pub struct RewindSummary { pub truncated_to_entries: usize, pub discarded_entries: usize, @@ -647,6 +672,7 @@ pub struct RewindSummary { /// history. Finalized assistant items continue to come from ordinary snapshot /// entries. #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] pub struct InFlightSnapshot { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub blocks: Vec, @@ -659,6 +685,7 @@ impl InFlightSnapshot { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(tag = "kind", rename_all = "snake_case")] pub enum InFlightBlock { Text { @@ -681,6 +708,7 @@ pub enum InFlightBlock { } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(rename_all = "snake_case")] pub enum InFlightToolCallState { #[default] @@ -701,6 +729,7 @@ impl InFlightToolCallState { /// transmitted alongside `Event::Snapshot` so clients don't need /// their own view of the manifest. #[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] pub struct Greeting { pub pod_name: String, pub cwd: String, @@ -721,6 +750,7 @@ pub struct Greeting { // --------------------------------------------------------------------------- #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(rename_all = "snake_case")] pub enum PodStatus { #[default] @@ -730,6 +760,7 @@ pub enum PodStatus { } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(rename_all = "snake_case")] pub enum TurnResult { Finished, @@ -743,6 +774,7 @@ pub enum TurnResult { /// notify message, pod event body) is delivered by the immediately /// following Turn entry, not by the marker itself. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(rename_all = "snake_case")] pub enum InvokeKind { /// `Method::Run` — a user submission. @@ -762,6 +794,7 @@ pub enum InvokeKind { } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(rename_all = "snake_case")] pub enum RunResult { Finished, @@ -775,6 +808,7 @@ pub enum RunResult { } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(rename_all = "snake_case")] pub enum ErrorCode { AlreadyRunning, @@ -796,6 +830,7 @@ pub enum ErrorCode { /// A single allow or deny rule inside a scope configuration. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] pub struct ScopeRule { /// Target path. Must be absolute by the time a `Scope` is built from /// this rule — relative paths are resolved per-layer against the @@ -822,6 +857,7 @@ fn default_recursive() -> bool { /// everything below); deny rules cap the effective level **strictly /// below** the stated level. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] #[serde(rename_all = "lowercase")] pub enum Permission { Read, diff --git a/crates/protocol/src/typescript.rs b/crates/protocol/src/typescript.rs new file mode 100644 index 00000000..92a9f7ff --- /dev/null +++ b/crates/protocol/src/typescript.rs @@ -0,0 +1,97 @@ +use std::path::PathBuf; + +use ts_rs::{Config, TS}; + +use crate::{ + Alert, AlertLevel, AlertSource, CompletionEntry, CompletionKind, ErrorCode, Event, Greeting, + InFlightBlock, InFlightSnapshot, InFlightToolCallState, InvokeKind, MemoryWorkerEvent, Method, + Permission, PodEvent, PodStatus, RewindSummary, RewindTarget, RewindTargetId, RunResult, + ScopeRule, Segment, TurnResult, +}; + +const GENERATED_RELATIVE_PATH: &str = "../../web/workspace/src/lib/generated/protocol.ts"; + +/// Repository-relative destination for the Workspace web protocol bindings. +pub fn generated_typescript_path() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(GENERATED_RELATIVE_PATH) +} + +/// Render Workspace web TypeScript bindings for the Pod wire protocol DTOs. +/// +/// Rust DTOs in this crate remain the source of truth; this function is used by +/// both the checked-in artifact generator and the stale-output drift test. +pub fn generated_protocol_types() -> String { + let cfg = Config::new().with_large_int("number"); + let mut output = String::from( + "// @generated by `cargo run -p protocol --example generate_typescript --features typescript`\n\ + // Do not edit by hand. Rust DTO authority lives in `crates/protocol`.\n\ + // Large integer fields are JSON numbers on the wire and are emitted as TypeScript `number`.\n\n", + ); + + push_decl::(&cfg, &mut output); + push_decl::(&cfg, &mut output); + push_decl::(&cfg, &mut output); + push_decl::(&cfg, &mut output); + push_decl::(&cfg, &mut output); + push_decl::(&cfg, &mut output); + push_decl::(&cfg, &mut output); + push_decl::(&cfg, &mut output); + push_decl::(&cfg, &mut output); + push_decl::(&cfg, &mut output); + push_decl::(&cfg, &mut output); + push_decl::(&cfg, &mut output); + push_decl::(&cfg, &mut output); + push_decl::(&cfg, &mut output); + push_decl::(&cfg, &mut output); + push_decl::(&cfg, &mut output); + push_decl::(&cfg, &mut output); + push_decl::(&cfg, &mut output); + push_decl::(&cfg, &mut output); + push_decl::(&cfg, &mut output); + push_decl::(&cfg, &mut output); + push_decl::(&cfg, &mut output); + push_decl::(&cfg, &mut output); + push_decl::(&cfg, &mut output); + + normalize_typescript(output) +} + +fn normalize_typescript(output: String) -> String { + let mut lines = output.lines().map(str::trim_end).collect::>(); + while lines.last() == Some(&"") { + lines.pop(); + } + lines.join("\n") + "\n" +} + +fn push_decl(cfg: &Config, output: &mut String) { + let decl = T::decl(cfg); + output.push_str(&export_decl(&decl)); + output.push_str("\n\n"); +} + +fn export_decl(decl: &str) -> String { + for prefix in ["type ", "interface ", "enum "] { + if decl.starts_with(prefix) { + return format!("export {decl}"); + } + } + decl.to_string() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn generated_protocol_types_are_current() { + let expected = generated_protocol_types(); + let path = generated_typescript_path(); + let actual = std::fs::read_to_string(&path) + .unwrap_or_else(|err| panic!("failed to read {}: {err}", path.display())); + assert_eq!( + actual, expected, + "generated TypeScript protocol bindings are stale; run `cargo run -p protocol --example generate_typescript --features typescript`" + ); + } +} diff --git a/crates/tui/src/dashboard/mod.rs b/crates/tui/src/dashboard/mod.rs index e1e25a98..75db186d 100644 --- a/crates/tui/src/dashboard/mod.rs +++ b/crates/tui/src/dashboard/mod.rs @@ -1437,31 +1437,6 @@ impl DashboardApp { } } - pub(crate) fn selected_open_disabled_reason(&self) -> Option { - if let Some(row) = self - .selected_panel_row() - .filter(|row| row.is_ticket_action()) - { - return Some( - row.disabled_reason - .clone() - .or_else(|| row.key_hint.clone()) - .unwrap_or_else(|| { - "Enter dispatches this Ticket action after re-checking current Ticket authority." - .to_string() - }), - ); - } - if let Some(entry) = self.selected_pod_entry() { - if entry.actions.can_open { - return None; - } - return Some(open_disabled_reason(entry)); - } - self.selected_panel_row() - .and_then(|row| row.disabled_reason.clone().or_else(|| row.key_hint.clone())) - } - pub(crate) fn select_next(&mut self) { let visible = visible_panel_keys(&self.panel, &self.list); if visible.is_empty() { @@ -5037,33 +5012,6 @@ fn segments_are_blank(segments: &[Segment]) -> bool { }) } -fn open_disabled_reason(entry: &PodListEntry) -> String { - if let Some(live) = entry.live.as_ref() { - if !live.reachable { - return "Selected live Pod is unreachable.".to_string(); - } - return match live.status { - Some(PodStatus::Running) => { - "Selected Pod is running; Enter opens/attaches for inspection.".to_string() - } - Some(PodStatus::Paused) => { - "Selected Pod is paused; open it explicitly to resume or start a new turn." - .to_string() - } - Some(PodStatus::Idle) => "Selected Pod can be opened/attached.".to_string(), - None => "Selected Pod did not report a live status.".to_string(), - }; - } - if entry.stored.is_some() { - return "Selected Pod is stopped; Enter restores/opens for inspection.".to_string(); - } - entry - .actions - .disabled_reason - .clone() - .unwrap_or_else(|| "Selected Pod cannot be opened from this row.".to_string()) -} - fn selected_ticket_notice(row: Option<&PanelRow>) -> String { match row { Some(row) if row.is_ticket_action() => { diff --git a/crates/tui/src/dashboard/render.rs b/crates/tui/src/dashboard/render.rs index e631cc4f..b463e584 100644 --- a/crates/tui/src/dashboard/render.rs +++ b/crates/tui/src/dashboard/render.rs @@ -57,24 +57,14 @@ pub(super) fn input_area_height(render: &crate::input::InputRender, terminal_hei } pub(super) fn draw_title(frame: &mut Frame<'_>, app: &DashboardApp, area: Rect) { - let guidance = if app - .panel - .composer - .is_available(ComposerTarget::TicketIntake) - { - " Row selection: blank Enter opens/dispatches · text Enter uses target · Tab target" - } else if app.panel.header.ticket_configured { - " Row selection: blank Enter opens/dispatches · text Enter sends to Companion" - } else { - " Pod-centric view · Row selection: blank Enter opens · text Enter sends to Companion" - }; - let mut spans = vec![ - Span::styled( - "workspace dashboard", - Style::default().add_modifier(Modifier::BOLD), - ), - Span::styled(guidance, Style::default().fg(Color::DarkGray)), - ]; + frame.render_widget(Paragraph::new(title_line(app)), area); +} + +pub(super) fn title_line(app: &DashboardApp) -> Line<'static> { + let mut spans = vec![Span::styled( + "workspace dashboard", + Style::default().add_modifier(Modifier::BOLD), + )]; if let Some(companion) = &app.panel.header.companion { spans.push(Span::styled( " · companion ", @@ -101,7 +91,7 @@ pub(super) fn draw_title(frame: &mut Frame<'_>, app: &DashboardApp, area: Rect) orchestrator_status_style(orchestrator.status), )); } - frame.render_widget(Paragraph::new(Line::from(spans)), area); + Line::from(spans) } pub(super) fn companion_status_style(status: CompanionPanelStatus) -> Style { @@ -688,115 +678,8 @@ pub(super) fn draw_target_status(frame: &mut Frame<'_>, app: &DashboardApp, area frame.render_widget(Paragraph::new(target_status_line(app)), area); } -pub(super) fn target_status_line(app: &DashboardApp) -> Line<'static> { - if !app.composer_is_blank() { - return Line::from(vec![ - Span::styled("composer target ", Style::default().fg(Color::DarkGray)), - Span::styled( - app.composer_target().label(), - Style::default() - .fg(Color::Green) - .add_modifier(Modifier::BOLD), - ), - Span::styled(" · draft Enter ", Style::default().fg(Color::DarkGray)), - Span::styled( - composer_enter_status_text(app), - Style::default().fg(Color::Green), - ), - Span::styled( - " · row selection waits until composer is blank", - Style::default().fg(Color::DarkGray), - ), - ]); - } - - if let Some(row) = app - .selected_panel_row() - .filter(|row| row.is_ticket_action()) - { - let action = row - .next_action - .map(|action| panel_ticket_action_label(row, action)) - .unwrap_or("View"); - let mut spans = vec![ - Span::styled("composer target ", Style::default().fg(Color::DarkGray)), - Span::styled( - app.composer_target().label(), - Style::default() - .fg(Color::Magenta) - .add_modifier(Modifier::BOLD), - ), - Span::styled(" · selected Ticket ", Style::default().fg(Color::DarkGray)), - Span::styled(row.status.clone(), panel_priority_style(row.priority)), - Span::styled(" · blank Enter ", Style::default().fg(Color::DarkGray)), - Span::styled(action, Style::default().fg(Color::Magenta)), - ]; - if let Some(reason) = panel_ticket_reason(row) { - spans.push(Span::styled(" · ", Style::default().fg(Color::DarkGray))); - spans.push(Span::styled( - truncate_with_ellipsis(reason, 100), - ticket_detail_style(row), - )); - } - Line::from(spans) - } else if let Some(row) = app - .selected_panel_row() - .filter(|row| row.kind == PanelRowKind::TicketIntakePod) - { - let ticket_id = panel_ticket_reference(row); - let action = if row.next_action == Some(NextUserAction::OpenPod) { - "open/attach" - } else { - "unavailable" - }; - Line::from(vec![ - Span::styled("composer target ", Style::default().fg(Color::DarkGray)), - Span::styled( - app.composer_target().label(), - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::styled( - " · selected Intake Pod ", - Style::default().fg(Color::DarkGray), - ), - Span::styled(row.status.clone(), intake_status_style(&row.status)), - Span::styled( - format!(" · Ticket {ticket_id} · blank Enter {action}"), - Style::default().fg(Color::DarkGray), - ), - ]) - } else if let Some(entry) = app.selected_pod_entry() { - let (status, status_style) = row_status_label(entry); - Line::from(vec![ - Span::styled("composer target ", Style::default().fg(Color::DarkGray)), - Span::styled( - app.composer_target().label(), - Style::default() - .fg(Color::Green) - .add_modifier(Modifier::BOLD), - ), - Span::styled(" · selected Pod ", Style::default().fg(Color::DarkGray)), - Span::styled(status.to_string(), status_style), - Span::styled( - " · blank Enter open/attach", - Style::default().fg(Color::DarkGray), - ), - ]) - } else { - Line::from(vec![ - Span::styled("composer target ", Style::default().fg(Color::DarkGray)), - Span::styled( - app.composer_target().label(), - Style::default().fg(Color::DarkGray), - ), - Span::styled( - " · no row selected · ↑/↓ selects a row", - Style::default().fg(Color::DarkGray), - ), - ]) - } +pub(super) fn target_status_line(_app: &DashboardApp) -> Line<'static> { + Line::from(Span::raw("")) } pub(super) fn draw_input(frame: &mut Frame<'_>, render: &crate::input::InputRender, area: Rect) { @@ -817,103 +700,6 @@ pub(super) fn draw_input(frame: &mut Frame<'_>, render: &crate::input::InputRend } } -pub(super) fn composer_enter_status_text(app: &DashboardApp) -> String { - match app.composer_target() { - ComposerTarget::Companion => companion_enter_status_text(app), - ComposerTarget::TicketIntake - if app.selected_ticket_action() == Some(NextUserAction::Queue) => - { - "return selected ready Ticket to planning".to_string() - } - ComposerTarget::TicketIntake => "launch Intake with composer text".to_string(), - } -} - -pub(super) fn composer_enter_actionbar_text(app: &DashboardApp) -> String { - match app.composer_target() { - ComposerTarget::Companion => companion_enter_actionbar_text(app), - ComposerTarget::TicketIntake if app.selected_ticket_action() == Some(NextUserAction::Queue) => { - "Ticket Intake target: Enter records instructions and returns selected ready Ticket to planning".to_string() - } - ComposerTarget::TicketIntake => { - "Ticket Intake target: Enter launches Intake with composer text".to_string() - } - } -} - -pub(super) fn companion_enter_status_text(app: &DashboardApp) -> String { - match companion_send_availability(app) { - CompanionSendAvailability::Ready => "send composer text to workspace Companion".to_string(), - CompanionSendAvailability::Unavailable(reason) => format!("keep draft; {reason}"), - } -} - -pub(super) fn companion_enter_actionbar_text(app: &DashboardApp) -> String { - match companion_send_availability(app) { - CompanionSendAvailability::Ready => { - "Companion target: Enter sends composer text to workspace Companion".to_string() - } - CompanionSendAvailability::Unavailable(reason) => { - format!("Companion target: Enter keeps draft; {reason}") - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub(super) enum CompanionSendAvailability { - Ready, - Unavailable(String), -} - -pub(super) fn companion_send_availability(app: &DashboardApp) -> CompanionSendAvailability { - let Some(companion) = app.panel.header.companion.as_ref() else { - return CompanionSendAvailability::Unavailable( - "workspace Companion is unavailable".to_string(), - ); - }; - if matches!( - companion.status, - CompanionPanelStatus::Unavailable - | CompanionPanelStatus::Missing - | CompanionPanelStatus::Stopped - ) { - return CompanionSendAvailability::Unavailable(format!( - "workspace Companion is {}", - companion.status.label() - )); - } - let Some(entry) = app - .list - .entries - .iter() - .find(|entry| entry.name == companion.pod_name) - else { - return CompanionSendAvailability::Unavailable(format!( - "workspace Companion `{}` is not in the Pod list", - companion.pod_name - )); - }; - let Some(live) = entry.live.as_ref() else { - return CompanionSendAvailability::Unavailable(format!( - "workspace Companion `{}` is stopped", - companion.pod_name - )); - }; - if !live.reachable { - return CompanionSendAvailability::Unavailable(format!( - "workspace Companion `{}` is unreachable", - companion.pod_name - )); - } - if live.status == Some(PodStatus::Running) { - return CompanionSendAvailability::Unavailable(format!( - "workspace Companion `{}` is running", - companion.pod_name - )); - } - CompanionSendAvailability::Ready -} - pub(super) fn actionbar_left_text(app: &DashboardApp) -> String { if app.sending && app.composer_target() == ComposerTarget::TicketIntake { "launching Ticket Intake…".to_string() @@ -927,52 +713,20 @@ pub(super) fn actionbar_left_text(app: &DashboardApp) -> String { Some(notice) => format!("{notice} Refreshing workspace…"), None => "Refreshing workspace…".to_string(), } - } else if !app.composer_is_blank() { - composer_enter_actionbar_text(app) } else if let Some(notice) = app.notice.as_deref() { notice.to_string() - } else if let Some(reason) = app.selected_open_disabled_reason() { - reason } else { - match app.composer_target() { - ComposerTarget::Companion => { - "Composer target: Companion; type text to send, or use ↑/↓ then blank Enter for rows" - .to_string() - } - ComposerTarget::TicketIntake => { - if app.selected_ticket_action() == Some(NextUserAction::Queue) { - "Composer target: Ticket Intake; text + Enter returns selected ready Ticket to planning".to_string() - } else { - "Composer target: Ticket Intake; type a request, then Enter launches Intake".to_string() - } - } - } + String::new() } } pub(super) fn actionbar_right_text(app: &DashboardApp) -> &'static str { if app.panel_diagnostic_open { - "F2/Esc close details Ctrl+C quit" + "F2/Esc close details" } else if app.panel_diagnostic.is_some() { - "F2 details ↑/↓ select row Enter selected row Tab target Esc clear selection Left/Right cursor Ctrl+C quit" - } else if !app.composer_is_blank() { - if app - .panel - .composer - .is_available(ComposerTarget::TicketIntake) - { - "↑/↓ draft lines Left/Right cursor Enter composer target Tab target Esc clear selection Ctrl+C quit" - } else { - "↑/↓ draft lines Left/Right cursor Enter composer target Esc clear selection Ctrl+C quit" - } - } else if app - .panel - .composer - .is_available(ComposerTarget::TicketIntake) - { - "↑/↓ select row Enter selected row Tab target Esc clear selection Left/Right cursor Ctrl+C quit" + "F2 details" } else { - "↑/↓ select row Enter selected row Esc clear selection Left/Right cursor Ctrl+C quit" + "" } } diff --git a/crates/tui/src/dashboard/tests.rs b/crates/tui/src/dashboard/tests.rs index 9a8367f4..af3f3846 100644 --- a/crates/tui/src/dashboard/tests.rs +++ b/crates/tui/src/dashboard/tests.rs @@ -1072,7 +1072,9 @@ fn mouse_click_selects_panel_row_for_blank_enter_action() { ); assert_eq!(app.selected_panel_row().unwrap().title, "Queued"); assert_eq!(app.selected_ticket_action(), Some(NextUserAction::Wait)); - assert!(plain_line(&target_status_line(&app)).contains("blank Enter Wait")); + let selected_title = + plain_line(&panel_row_lines(app.selected_panel_row().unwrap(), true, 80)[0]); + assert!(selected_title.starts_with("▶ queued")); assert!(matches!( app.handle_key(key(KeyCode::Enter)), DashboardAction::DispatchTicketAction(request) if request.ticket_id == "TICKET-2" @@ -1152,7 +1154,7 @@ fn mouse_click_does_not_override_existing_composer_keyboard_behavior() { } #[test] -fn selected_ticket_row_with_non_empty_composer_shows_composer_enter_behavior() { +fn selected_ticket_row_with_non_empty_composer_hides_redundant_status_hints() { let mut panel = WorkspacePanelViewModel::empty(Path::new("test")); panel.header.companion = Some(CompanionPanelState::new( "yoi", @@ -1185,14 +1187,12 @@ fn selected_ticket_row_with_non_empty_composer_shows_composer_enter_behavior() { let actionbar_right = actionbar_right_text(&app); let target_status = plain_line(&target_status_line(&app)); - assert!(actionbar_left.contains("Companion target: Enter sends composer text")); - assert!(actionbar_right.contains("Enter composer target")); - assert!(!actionbar_left.contains("Queue")); - assert!(!actionbar_right.contains("selected row")); - assert!(target_status.contains("composer target Companion")); - assert!(target_status.contains("draft Enter send composer text to workspace Companion")); - assert!(target_status.contains("row selection waits until composer is blank")); - assert!(!target_status.contains("blank Enter Queue")); + assert_eq!(actionbar_left, ""); + assert_eq!(actionbar_right, ""); + assert_eq!(target_status, ""); + let selected_title = + plain_line(&panel_row_lines(app.selected_panel_row().unwrap(), true, 80)[0]); + assert!(selected_title.starts_with("▶ ready")); } #[test] @@ -1529,7 +1529,6 @@ fn dashboard_idle_live_selected_target_is_open_eligible() { let app = test_app(vec![live_info("idle", PodStatus::Idle)]); assert_eq!(app.selected_open_eligibility(), OpenEligibility::OpenNow); - assert!(app.selected_open_disabled_reason().is_none()); } #[test] @@ -1557,6 +1556,35 @@ fn dashboard_status_labels_preserve_explicit_live_statuses() { } } +#[test] +fn dashboard_title_omits_redundant_key_hint_guidance() { + let mut panel = WorkspacePanelViewModel::empty(Path::new("test")); + panel.header.ticket_configured = true; + panel.header.companion = Some(CompanionPanelState::new( + "yoi", + CompanionPanelStatus::Live, + Some("idle".to_string()), + )); + let app = app_with_panel( + PodList::from_sources( + PodVisibilitySource::ResumePicker, + vec![], + vec![live_info("yoi", PodStatus::Idle)], + None, + 10, + ), + panel, + ); + + let title = plain_line(&title_line(&app)); + + assert!(title.contains("workspace dashboard")); + assert!(title.contains("companion live")); + assert!(!title.contains("Row selection")); + assert!(!title.contains("blank Enter")); + assert!(!title.contains("Tab target")); +} + #[test] fn panel_ticket_rows_render_state_title_then_detail_line() { let row = panel_test_ticket_row( @@ -1725,7 +1753,7 @@ fn panel_ticket_intake_child_rows_render_as_indented_single_line() { } #[test] -fn selected_ticket_intake_child_status_is_not_rendered_as_generic_ticket_or_pod() { +fn selected_ticket_intake_child_keeps_row_marker_without_status_line() { let ticket_id = "00001TICKET"; let pod_name = "intake-live"; let mut panel = WorkspacePanelViewModel::empty(Path::new("test")); @@ -1750,11 +1778,11 @@ fn selected_ticket_intake_child_status_is_not_rendered_as_generic_ticket_or_pod( let status = plain_line(&target_status_line(&app)); - assert!(status.contains("selected Intake Pod live")); - assert!(status.contains("Ticket 00001TICKET")); - assert!(status.contains("blank Enter open/attach")); - assert!(!status.contains("selected Ticket")); - assert!(!status.contains("selected Pod live")); + assert_eq!(status, ""); + + let title_line = plain_line(&panel_row_lines(app.selected_panel_row().unwrap(), true, 80)[0]); + assert!(title_line.starts_with(" ▶ live")); + assert!(title_line.contains("Intake")); } #[test] @@ -1828,15 +1856,12 @@ fn dashboard_running_paused_and_stopped_targets_are_open_eligible() { app.ensure_selection_visible(); assert_eq!(app.selected_open_eligibility(), OpenEligibility::OpenNow); - assert!(app.selected_open_disabled_reason().is_none()); app.select_next(); assert_eq!(app.list.selected_entry().unwrap().name, "paused"); assert_eq!(app.selected_open_eligibility(), OpenEligibility::OpenNow); - assert!(app.selected_open_disabled_reason().is_none()); app.select_next(); assert_eq!(app.list.selected_entry().unwrap().name, "stopped"); assert_eq!(app.selected_open_eligibility(), OpenEligibility::OpenNow); - assert!(app.selected_open_disabled_reason().is_none()); } #[test] diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs index 822abd3d..6aada3ab 100644 --- a/crates/workspace-server/src/server.rs +++ b/crates/workspace-server/src/server.rs @@ -250,11 +250,11 @@ async fn get_workspace(State(api): State) -> ApiResult, }; + +export type Greeting = { pod_name: string, cwd: string, provider: string, model: string, scope_summary: string, tools: Array, +/** + * Model context window in tokens. Always filled by the Pod greeting. + */ +context_window: number, +/** + * Estimated current session context tokens at connect time. + */ +context_tokens: number, }; + +export type Alert = { level: AlertLevel, source: AlertSource, message: string, +/** + * Milliseconds since the Unix epoch. + */ +timestamp_ms: number, }; + +export type MemoryWorkerEvent = { worker: string, status: string, run_id: string, trigger: string, reason: string, +/** + * Human-readable compact form for actionbar rendering. + */ +message: string, +/** + * Milliseconds since the Unix epoch. + */ +timestamp_ms: number, }; + +export type Segment = { "kind": "text", content: string, } | { "kind": "paste", id: number, chars: number, lines: number, content: string, } | { "kind": "file_ref", path: string, } | { "kind": "knowledge_ref", slug: string, } | { "kind": "workflow_invoke", slug: string, } | { "kind": "unknown" }; + +export type PodEvent = { "kind": "turn_ended", pod_name: string, } | { "kind": "errored", pod_name: string, message: string, } | { "kind": "shut_down", pod_name: string, } | { "kind": "scope_sub_delegated", +/** + * Sub-delegating Pod (= the sender itself). + */ +parent_pod: string, +/** + * Name of the grandchild Pod. + */ +sub_pod: string, +/** + * Unix-socket path where the grandchild is reachable. + */ +sub_socket: string, +/** + * Scope delegated to the grandchild. + */ +scope: Array, }; + +export type Method = { "method": "run", "params": { input: Array, } } | { "method": "notify", "params": { message: string, auto_run?: boolean, } } | { "method": "pod_event", "params": PodEvent } | { "method": "resume" } | { "method": "cancel" } | { "method": "pause" } | { "method": "compact" } | { "method": "list_rewind_targets" } | { "method": "rewind_to", "params": { target: RewindTargetId, expected_head_entries: number, } } | { "method": "shutdown" } | { "method": "list_completions", "params": { kind: CompletionKind, prefix: string, } } | { "method": "list_pods" } | { "method": "restore_pod", "params": { name: string, } } | { "method": "register_peer", "params": { name: string, } }; + +export type Event = { "event": "user_message", "data": { segments: Array, } } | { "event": "system_item", "data": { item: unknown, } } | { "event": "invoke_start", "data": { kind: InvokeKind, } } | { "event": "turn_start", "data": { turn: number, } } | { "event": "turn_end", "data": { turn: number, result: TurnResult, } } | { "event": "llm_call_start", "data": { llm_call: number, } } | { "event": "llm_call_end", "data": { llm_call: number, } } | { "event": "llm_retry", "data": { llm_call: number, +/** + * The attempt that just failed. 1 origin. + */ +failed_attempt: number, max_attempts: number, wait_ms: number, elapsed_ms: number, status?: number | null, error: string, } } | { "event": "llm_continuation", "data": { llm_call: number, attempt: number, max_attempts: number, reason: string, } } | { "event": "text_delta", "data": { text: string, } } | { "event": "text_done", "data": { text: string, } } | { "event": "thinking_start" } | { "event": "thinking_delta", "data": { text: string, } } | { "event": "thinking_done", "data": { text: string, } } | { "event": "tool_call_start", "data": { id: string, name: string, } } | { "event": "tool_call_args_delta", "data": { id: string, json: string, } } | { "event": "tool_call_done", "data": { id: string, name: string, arguments: string, } } | { "event": "tool_result", "data": { id: string, +/** + * Short human-readable summary. Always present; used by clients + * that only want a 1-line rendering (e.g. collapsed views). + */ +summary: string, +/** + * Full tool output. Absent when the tool chose to return + * summary-only, or when the result was pruned. + */ +output?: string | null, is_error: boolean, } } | { "event": "usage", "data": { input_tokens: number | null, output_tokens: number | null, cache_read_input_tokens?: number | null, } } | { "event": "run_end", "data": { result: RunResult, } } | { "event": "error", "data": { code: ErrorCode, message: string, } } | { "event": "snapshot", "data": { entries: Array, greeting: Greeting, status: PodStatus, +/** + * Unfinished model output that has already streamed in the current + * run but is not yet represented by committed snapshot entries. + */ +in_flight?: InFlightSnapshot, } } | { "event": "segment_rotated", "data": { entry: unknown, } } | { "event": "status", "data": { status: PodStatus, } } | { "event": "completions", "data": { kind: CompletionKind, entries: Array, } } | { "event": "rewind_targets", "data": { head_entries: number, targets: Array, } } | { "event": "rewind_applied", "data": { entries: Array, input: Array, summary: RewindSummary, } } | { "event": "pods_listed", "data": { pods: unknown, } } | { "event": "pod_restored", "data": { result: unknown, } } | { "event": "peer_registered", "data": { result: unknown, } } | { "event": "alert", "data": Alert } | { "event": "memory_worker", "data": MemoryWorkerEvent } | { "event": "compact_start" } | { "event": "compact_done", "data": { new_segment_id: string, } } | { "event": "compact_failed", "data": { error: string, } } | { "event": "shutdown" }; diff --git a/web/workspace/src/lib/workspace-pages/RepositoryTicketKanban.svelte b/web/workspace/src/lib/workspace-pages/RepositoryTicketKanban.svelte new file mode 100644 index 00000000..e6475795 --- /dev/null +++ b/web/workspace/src/lib/workspace-pages/RepositoryTicketKanban.svelte @@ -0,0 +1,283 @@ + + +
+ {#each groups as group (group.key)} +
+
+
+

{group.label}

+

{group.states.join(' + ')}

+
+ {group.items.length} +
+ + {#if group.items.length === 0} +

No tickets.

+ {:else} +
onGroupScroll(group, event)} + > +
    + {#each visibleTickets(group) as ticket (ticket.id)} +
  • +
    + {ticket.title} + {ticket.state} +
    + {ticket.id} · updated {formatDate(ticket.updated_at)} +
  • + {/each} +
+ {#if hasMore(group)} +

Showing {visibleCount(group.key)} of {group.items.length}; scroll for more.

+ {/if} +
+ {/if} +
+ {/each} +
+ + diff --git a/web/workspace/src/lib/workspace-pages/WorkspacePage.svelte b/web/workspace/src/lib/workspace-pages/WorkspacePage.svelte index 232f579b..82b91414 100644 --- a/web/workspace/src/lib/workspace-pages/WorkspacePage.svelte +++ b/web/workspace/src/lib/workspace-pages/WorkspacePage.svelte @@ -1,4 +1,5 @@